From 8be3dcf804447ca198b2c327bfc972a8eef76e53 Mon Sep 17 00:00:00 2001 From: Francesco Siddi Date: Fri, 13 Jun 2014 14:56:37 +0200 Subject: [PATCH] Initial code commit --- .gitignore | 6 + blender-bfct/application/__init__.py | 16 +++ .../application/controllers/__init__.py | 0 blender-bfct/application/controllers/admin.py | 64 +++++++++++ .../application/controllers/applications.py | 94 ++++++++++++++++ blender-bfct/application/controllers/main.py | 79 +++++++++++++ blender-bfct/application/helpers.py | 42 +++++++ blender-bfct/application/models/__init__.py | 0 .../application/models/applications.py | 61 ++++++++++ blender-bfct/application/models/users.py | 35 ++++++ .../templates/admin/layout_admin.html | 14 +++ .../templates/applications/index.html | 33 ++++++ .../templates/applications/view.html | 77 +++++++++++++ blender-bfct/application/templates/apply.html | 77 +++++++++++++ blender-bfct/application/templates/index.html | 18 +++ .../application/templates/layout.html | 104 ++++++++++++++++++ .../application/templates/my_application.html | 36 ++++++ blender-bfct/config.py.example | 46 ++++++++ blender-bfct/runserver.py | 5 + requirements.txt | 24 ++++ 20 files changed, 831 insertions(+) create mode 100644 .gitignore create mode 100644 blender-bfct/application/__init__.py create mode 100644 blender-bfct/application/controllers/__init__.py create mode 100644 blender-bfct/application/controllers/admin.py create mode 100644 blender-bfct/application/controllers/applications.py create mode 100644 blender-bfct/application/controllers/main.py create mode 100644 blender-bfct/application/helpers.py create mode 100644 blender-bfct/application/models/__init__.py create mode 100644 blender-bfct/application/models/applications.py create mode 100644 blender-bfct/application/models/users.py create mode 100644 blender-bfct/application/templates/admin/layout_admin.html create mode 100755 blender-bfct/application/templates/applications/index.html create mode 100755 blender-bfct/application/templates/applications/view.html create mode 100755 blender-bfct/application/templates/apply.html create mode 100755 blender-bfct/application/templates/index.html create mode 100644 blender-bfct/application/templates/layout.html create mode 100755 blender-bfct/application/templates/my_application.html create mode 100644 blender-bfct/config.py.example create mode 100644 blender-bfct/runserver.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..563f12b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc +venv +*.sublime-project +*.sublime-workspace +blender-bfct/config.py +blender-bfct/runserver.wsgi diff --git a/blender-bfct/application/__init__.py b/blender-bfct/application/__init__.py new file mode 100644 index 0000000..fa147d9 --- /dev/null +++ b/blender-bfct/application/__init__.py @@ -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') diff --git a/blender-bfct/application/controllers/__init__.py b/blender-bfct/application/controllers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blender-bfct/application/controllers/admin.py b/blender-bfct/application/controllers/admin.py new file mode 100644 index 0000000..4e87216 --- /dev/null +++ b/blender-bfct/application/controllers/admin.py @@ -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')) diff --git a/blender-bfct/application/controllers/applications.py b/blender-bfct/application/controllers/applications.py new file mode 100644 index 0000000..d14eea3 --- /dev/null +++ b/blender-bfct/application/controllers/applications.py @@ -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/') +@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//') +@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//') +@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)) diff --git a/blender-bfct/application/controllers/main.py b/blender-bfct/application/controllers/main.py new file mode 100644 index 0000000..7130e1f --- /dev/null +++ b/blender-bfct/application/controllers/main.py @@ -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) diff --git a/blender-bfct/application/helpers.py b/blender-bfct/application/helpers.py new file mode 100644 index 0000000..85d298e --- /dev/null +++ b/blender-bfct/application/helpers.py @@ -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" diff --git a/blender-bfct/application/models/__init__.py b/blender-bfct/application/models/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/blender-bfct/application/models/applications.py b/blender-bfct/application/models/applications.py new file mode 100644 index 0000000..c38057b --- /dev/null +++ b/blender-bfct/application/models/applications.py @@ -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) diff --git a/blender-bfct/application/models/users.py b/blender-bfct/application/models/users.py new file mode 100644 index 0000000..c842e18 --- /dev/null +++ b/blender-bfct/application/models/users.py @@ -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) diff --git a/blender-bfct/application/templates/admin/layout_admin.html b/blender-bfct/application/templates/admin/layout_admin.html new file mode 100644 index 0000000..5a4fa6c --- /dev/null +++ b/blender-bfct/application/templates/admin/layout_admin.html @@ -0,0 +1,14 @@ +{% extends 'admin/base.html' %} + +{% block access_control %} +{% if current_user.is_authenticated() %} + +{% endif %} +{% endblock %} diff --git a/blender-bfct/application/templates/applications/index.html b/blender-bfct/application/templates/applications/index.html new file mode 100755 index 0000000..2657dea --- /dev/null +++ b/blender-bfct/application/templates/applications/index.html @@ -0,0 +1,33 @@ +{% extends 'layout.html' %} +{% block body %} +
+
+ + + + + + + + + + + + + {% for application in applications %} + + + + + + + + + {% endfor %} + +
ApplicantCountryDateStatusApprovals
{{application.user.first_name}} {{application.user.last_name}}{{application.city_country}}{{application.pretty_submission_date}}{{application.status}}{{application.approve}} / {{application.approve + application.reject}}View
+ +
+
+ +{% endblock %} diff --git a/blender-bfct/application/templates/applications/view.html b/blender-bfct/application/templates/applications/view.html new file mode 100755 index 0000000..737d8b9 --- /dev/null +++ b/blender-bfct/application/templates/applications/view.html @@ -0,0 +1,77 @@ +{% extends 'layout.html' %} +{% block body %} + +
+
+

{{application.user.first_name}} {{application.user.last_name}} {{application.city_country}}

+

{{application.bio_message}}

+ +
+
+

Status: {{application.status}}

+
+
+ + + + + + + + + {% for review in reviews%} + + + + + {% endfor %} + +
ReviewerVote
+ {% if review.reviewer.id == current_user.id %} + You + {% endif %} + {{review.reviewer.first_name}} {{review.reviewer.last_name}} + + {% if review.approved %} + √ + {% endif %} +
+ +
+
+
+ {% if not review %} +
+ Approve +
+
+ Reject +
+ {% else %} +
+

You {% if review.approved %} approved {% else %} rejected {% endif %} this candidate.

+
+ {% endif %} +
+
+ {% if current_user.has_role('admin') %} + {% if not application.review_end_date %} + + + {% endif %} + {% endif %} +
+
+
+ + +{% endblock %} diff --git a/blender-bfct/application/templates/apply.html b/blender-bfct/application/templates/apply.html new file mode 100755 index 0000000..2d5923d --- /dev/null +++ b/blender-bfct/application/templates/apply.html @@ -0,0 +1,77 @@ +{% extends 'layout.html' %} +{% block body %} +
+
+
+
+ {{ form.hidden_tag() }} + + + + + + + + + + + + + + + + + + + +
+
+
+
+ +{% endblock %} diff --git a/blender-bfct/application/templates/index.html b/blender-bfct/application/templates/index.html new file mode 100755 index 0000000..9845925 --- /dev/null +++ b/blender-bfct/application/templates/index.html @@ -0,0 +1,18 @@ +{% extends 'layout.html' %} +{% block body %} +
+
+

BFCT program

+

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.

+ +

The goals of the BFCT program are:

+ +
    +
  • Provide a standard for Certification for everyone who is interested in teaching Blender professionally
  • +
  • Help experienced Blender artists and developers to get into training business
  • +
  • Increase the quality and quantity of Blender training worldwide
  • +
+
+
+ +{% endblock %} diff --git a/blender-bfct/application/templates/layout.html b/blender-bfct/application/templates/layout.html new file mode 100644 index 0000000..888dce0 --- /dev/null +++ b/blender-bfct/application/templates/layout.html @@ -0,0 +1,104 @@ + + + + + Blender Cloud + + + + + + + + + + + + + + + + + + + + + + + {% block modal %} + + {% endblock %} + + + +
+
+
+ {% for message in get_flashed_messages() %} +
+ + {{ message }} +
+ {% endfor %} + {% block body %}{% endblock %} +
+
+
+
+

Blender-BFCT is part of the blender.org project

+
+ +
+ + + {% block footer_scripts %}{% endblock %} + + + + + diff --git a/blender-bfct/application/templates/my_application.html b/blender-bfct/application/templates/my_application.html new file mode 100755 index 0000000..b9bdc3f --- /dev/null +++ b/blender-bfct/application/templates/my_application.html @@ -0,0 +1,36 @@ +{% extends 'layout.html' %} +{% block body %} +
+
+

Your BFCT status dashboard

+ + + + + + + + + + + + + + {% if application.show_pretty_date('review_start') %} + + + + + {% endif%} + {% if application.show_pretty_date('review_end') %} + + + + + {% endif%} + +
DateStage
{{application.show_pretty_date('submission')}}You submitted your application
{{application.show_pretty_date('review_start')}}Application review started
{{application.show_pretty_date('review_end')}}Application review ended
+
+
+ +{% endblock %} diff --git a/blender-bfct/config.py.example b/blender-bfct/config.py.example new file mode 100644 index 0000000..299e3b2 --- /dev/null +++ b/blender-bfct/config.py.example @@ -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 = '' diff --git a/blender-bfct/runserver.py b/blender-bfct/runserver.py new file mode 100644 index 0000000..70c1d64 --- /dev/null +++ b/blender-bfct/runserver.py @@ -0,0 +1,5 @@ +from application import app +from application import db +db.create_all(bind=['users']) +db.create_all(bind=None) +app.run() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c181189 --- /dev/null +++ b/requirements.txt @@ -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