Introducing setup_for_film functionality
It is now possible, only for user with admin capability, to setup a project as ‘film’. This action can be performed via CLI using ./manage.py cloud setup_for_film <project_url> or via the web interface in the Cloud settings area. Setting up a project for film creates a number of extension props under the ‘cloud’ key. Such properties are listed in the cloud_extension_props variable in setup.py. At this moment the functionality exists for a very specific purpose: improving the presentation of public Film projects in the Blender Cloud. It can be further extended to improve the presentation of Training and Libraries later on.
This commit is contained in:
parent
6ac75c6a14
commit
4a180e3784
@ -3,6 +3,8 @@ import logging
|
||||
import flask
|
||||
from werkzeug.local import LocalProxy
|
||||
|
||||
import pillarsdk
|
||||
import pillar.auth
|
||||
from pillar.api.utils import authorization
|
||||
from pillar.extension import PillarExtension
|
||||
|
||||
@ -87,6 +89,50 @@ class CloudExtension(PillarExtension):
|
||||
'current_user_is_subscriber': authorization.user_has_cap('subscriber')
|
||||
}
|
||||
|
||||
def is_cloud_project(self, project):
|
||||
"""Returns whether the project is set up for Blender Cloud.
|
||||
|
||||
Requires the presence of the 'cloud' key in extension_props
|
||||
"""
|
||||
|
||||
try:
|
||||
pprops = project.extension_props[EXTENSION_NAME]
|
||||
except AttributeError:
|
||||
self._log.warning("is_cloud_project: Project url=%r doesn't have any "
|
||||
"extension properties.", project['url'])
|
||||
if self._log.isEnabledFor(logging.DEBUG):
|
||||
import pprint
|
||||
self._log.debug('Project: %s', pprint.pformat(project.to_dict()))
|
||||
return False
|
||||
except KeyError:
|
||||
# Not set up for Blender Cloud
|
||||
return False
|
||||
|
||||
if pprops is None:
|
||||
self._log.debug("is_cloud_project: Project url=%r doesn't have Blender Cloud"
|
||||
" extension properties.", project['url'])
|
||||
return False
|
||||
return True
|
||||
|
||||
@property
|
||||
def has_project_settings(self) -> bool:
|
||||
# Only available for admins
|
||||
return pillar.auth.current_user.has_cap('admin')
|
||||
|
||||
def project_settings(self, project: pillarsdk.Project, **template_args: dict) -> flask.Response:
|
||||
"""Renders the project settings page for this extension.
|
||||
|
||||
Set YourExtension.has_project_settings = True and Pillar will call this function.
|
||||
|
||||
:param project: the project for which to render the settings.
|
||||
:param template_args: additional template arguments.
|
||||
:returns: a Flask HTTP response
|
||||
"""
|
||||
|
||||
from cloud.routes import project_settings
|
||||
|
||||
return project_settings(project, **template_args)
|
||||
|
||||
def setup_app(self, app):
|
||||
from . import routes, webhooks, eve_hooks, email
|
||||
|
||||
|
10
cloud/cli.py
10
cloud/cli.py
@ -9,6 +9,8 @@ import requests
|
||||
|
||||
from pillar.cli import manager
|
||||
from pillar.api import service
|
||||
from pillar.api.utils import authentication
|
||||
import cloud.setup
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@ -126,4 +128,12 @@ def reconcile_subscribers():
|
||||
log.info(' skipped : %d', count_skipped)
|
||||
|
||||
|
||||
@manager_cloud.command
|
||||
def setup_for_film(project_url):
|
||||
"""Adds Blender Cloud film custom properties to a project."""
|
||||
|
||||
authentication.force_cli_user()
|
||||
cloud.setup.setup_for_film(project_url)
|
||||
|
||||
|
||||
manager.add_command("cloud", manager_cloud)
|
||||
|
16
cloud/forms.py
Normal file
16
cloud/forms.py
Normal file
@ -0,0 +1,16 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import BooleanField, StringField
|
||||
from wtforms.validators import URL
|
||||
from flask_wtf.html5 import URLField
|
||||
|
||||
from pillar.web.utils.forms import FileSelectField
|
||||
|
||||
|
||||
class FilmProjectForm(FlaskForm):
|
||||
video_url = URLField(validators=[URL()])
|
||||
picture_16_9 = FileSelectField('Picture 16x9', file_format='image')
|
||||
poster = FileSelectField('Poster Image', file_format='image')
|
||||
logo = FileSelectField('Logo', file_format='image')
|
||||
is_in_production = BooleanField('In Production')
|
||||
is_featured = BooleanField('Featured')
|
||||
theme_color = StringField('Theme Color')
|
105
cloud/routes.py
105
cloud/routes.py
@ -3,14 +3,19 @@ import json
|
||||
import logging
|
||||
import typing
|
||||
|
||||
from flask_login import current_user, login_required
|
||||
import bson
|
||||
from flask_login import login_required
|
||||
import flask
|
||||
import werkzeug.exceptions as wz_exceptions
|
||||
from flask import Blueprint, render_template, redirect, session, url_for, abort, flash
|
||||
from pillarsdk import Node, Project, User, exceptions as sdk_exceptions, Group
|
||||
from pillarsdk.exceptions import ResourceNotFound
|
||||
|
||||
import pillar
|
||||
import pillarsdk
|
||||
from pillar import current_app
|
||||
import pillar.api
|
||||
from pillar.api.utils import authorization
|
||||
from pillar.auth import current_user
|
||||
from pillar.web.users import forms
|
||||
from pillar.web.utils import system_util, get_file, current_user_is_authenticated
|
||||
from pillar.web.utils import attach_project_pictures
|
||||
@ -18,7 +23,11 @@ from pillar.web.settings import blueprint as blueprint_settings
|
||||
from pillar.web.nodes.routes import url_for_node
|
||||
from pillar.web.projects.routes import render_project
|
||||
from pillar.web.projects.routes import find_project_or_404
|
||||
from pillar.web.projects.routes import project_view
|
||||
|
||||
from cloud import current_cloud
|
||||
from cloud.forms import FilmProjectForm
|
||||
from . import EXTENSION_NAME
|
||||
|
||||
blueprint = Blueprint('cloud', __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
@ -412,6 +421,14 @@ def project_hero():
|
||||
header_video_file = get_file(project.header_node.properties.file)
|
||||
header_video_node.picture = get_file(header_video_node.picture)
|
||||
|
||||
# Load custom project properties
|
||||
if EXTENSION_NAME in project.extension_props:
|
||||
extension_props = project['extension_props'][EXTENSION_NAME]
|
||||
file_props = {'picture_16_9', 'logo'}
|
||||
for f in file_props:
|
||||
if f in extension_props:
|
||||
extension_props[f] = get_file(extension_props[f])
|
||||
|
||||
pages = Node.all({
|
||||
'where': {'project': project._id, 'node_type': 'page'},
|
||||
'projection': {'name': 1}}, api=api)
|
||||
@ -423,6 +440,90 @@ def project_hero():
|
||||
template_name='projects/landing.html')
|
||||
|
||||
|
||||
def project_settings(project: pillarsdk.Project, **template_args: dict):
|
||||
"""Renders the project settings page for Blender Cloud projects.
|
||||
|
||||
If the project has been setup for Blender Cloud, check for the cloud.project_type
|
||||
property, to render the proper form.
|
||||
"""
|
||||
|
||||
# Based on the project state, we can render a different template.
|
||||
if not current_cloud.is_cloud_project(project):
|
||||
return render_template('project_settings/offer_setup.html',
|
||||
project=project, **template_args)
|
||||
|
||||
cloud_props = project['extension_props'][EXTENSION_NAME]
|
||||
|
||||
project_type = cloud_props['project_type']
|
||||
if project_type != 'film':
|
||||
log.error('No interface available to edit %s projects, yet' % project_type)
|
||||
|
||||
form = FilmProjectForm()
|
||||
|
||||
# Iterate over the form fields and set the data if exists in the project document
|
||||
for field_name in form.data:
|
||||
if field_name not in cloud_props:
|
||||
continue
|
||||
# Skip csrf_token field
|
||||
if field_name == 'csrf_token':
|
||||
continue
|
||||
form_field = getattr(form, field_name)
|
||||
form_field.data = cloud_props[field_name]
|
||||
|
||||
return render_template('project_settings/settings.html',
|
||||
project=project,
|
||||
form=form,
|
||||
**template_args)
|
||||
|
||||
|
||||
@blueprint.route('/<project_url>/settings/film', methods=['POST'])
|
||||
@authorization.require_login(require_cap='admin')
|
||||
@project_view()
|
||||
def save_film_settings(project: pillarsdk.Project):
|
||||
# Ensure that the project is setup for Cloud (see @attract_project_view for example)
|
||||
form = FilmProjectForm()
|
||||
if not form.validate_on_submit():
|
||||
log.debug('Form submission failed')
|
||||
# Return list of validation errors
|
||||
|
||||
updated_extension_props = {}
|
||||
for field_name in form.data:
|
||||
# Skip csrf_token field
|
||||
if field_name == 'csrf_token':
|
||||
continue
|
||||
form_field = getattr(form, field_name)
|
||||
# TODO(fsiddi) if form_field type is FileSelectField, convert it to ObjectId
|
||||
# Currently this raises TypeError: Object of type 'ObjectId' is not JSON serializable
|
||||
updated_extension_props[field_name] = form_field.data
|
||||
|
||||
# Update extension props and save project
|
||||
extension_props = project['extension_props'][EXTENSION_NAME]
|
||||
# Project is a Resource, so we update properties iteratively
|
||||
for k, v in updated_extension_props.items():
|
||||
extension_props[k] = v
|
||||
project.update(api=system_util.pillar_api())
|
||||
return '', 204
|
||||
|
||||
|
||||
@blueprint.route('/<project_url>/setup-for-film', methods=['POST'])
|
||||
@login_required
|
||||
@project_view()
|
||||
def setup_for_film(project: pillarsdk.Project):
|
||||
import cloud.setup
|
||||
|
||||
project_id = project._id
|
||||
|
||||
if not project.has_method('PUT'):
|
||||
log.warning('User %s tries to set up project %s for Blender Cloud, but has no PUT rights.',
|
||||
current_user, project_id)
|
||||
raise wz_exceptions.Forbidden()
|
||||
|
||||
log.info('User %s sets up project %s for Blender Cloud', current_user, project_id)
|
||||
cloud.setup.setup_for_film(project.url)
|
||||
|
||||
return '', 204
|
||||
|
||||
|
||||
def setup_app(app):
|
||||
global _homepage_context
|
||||
cached = app.cache.cached(timeout=300)
|
||||
|
55
cloud/setup.py
Normal file
55
cloud/setup.py
Normal file
@ -0,0 +1,55 @@
|
||||
"""Setting up projects for Blender Cloud."""
|
||||
|
||||
import logging
|
||||
|
||||
from bson import ObjectId
|
||||
from eve.methods.put import put_internal
|
||||
from flask import current_app
|
||||
|
||||
from pillar.api.utils import remove_private_keys
|
||||
from . import EXTENSION_NAME
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def setup_for_film(project_url):
|
||||
"""Add Blender Cloud extension_props specific for film projects.
|
||||
|
||||
Returns the updated project.
|
||||
"""
|
||||
|
||||
projects_collection = current_app.data.driver.db['projects']
|
||||
|
||||
# Find the project in the database.
|
||||
project = projects_collection.find_one({'url': project_url})
|
||||
if not project:
|
||||
raise RuntimeError('Project %s does not exist.' % project_url)
|
||||
|
||||
# Set default extension properties. Be careful not to overwrite any properties that
|
||||
# are already there.
|
||||
all_extension_props = project.setdefault('extension_props', {})
|
||||
cloud_extension_props = {
|
||||
'project_type': 'film',
|
||||
'theme_css': '',
|
||||
# The accent color (can be 'blue' or '#FFBBAA' or 'rgba(1, 1, 1, 1)
|
||||
'theme_color': '',
|
||||
'is_in_production': False,
|
||||
'video_url': '', # Oembeddable url
|
||||
'poster': None, # File ObjectId
|
||||
'logo': None, # File ObjectId
|
||||
# TODO(fsiddi) when we introduce other setup_for_* in Blender Cloud, make available
|
||||
# at a higher scope
|
||||
'picture_16_9': None,
|
||||
'is_featured': False,
|
||||
}
|
||||
|
||||
all_extension_props.setdefault(EXTENSION_NAME, cloud_extension_props)
|
||||
|
||||
project_id = ObjectId(project['_id'])
|
||||
project = remove_private_keys(project)
|
||||
result, _, _, status_code = put_internal('projects', project, _id=project_id)
|
||||
|
||||
if status_code != 200:
|
||||
raise RuntimeError("Can't update project %s, issues: %s", project_id, result)
|
||||
|
||||
log.info('Project %s was updated for Blender Cloud.', project_url)
|
21
src/templates/project_settings/cloud_layout.pug
Normal file
21
src/templates/project_settings/cloud_layout.pug
Normal file
@ -0,0 +1,21 @@
|
||||
| {% extends 'projects/edit_layout.html' %}
|
||||
| {% set title = 'cloud' %}
|
||||
| {% block page_title %}Blender Cloud settings for {{ project.name }}{% endblock %}
|
||||
|
||||
| {% block head %}
|
||||
script(src="{{ url_for('static_attract', filename='assets/js/generated/tutti.min.js') }}")
|
||||
| {% endblock %}
|
||||
|
||||
| {% block project_context %}
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-12
|
||||
h5.pt-3 {{ self.page_title() }}
|
||||
|
||||
hr
|
||||
|
||||
#node-edit-container
|
||||
| {% block cloud_container %}
|
||||
| {% endblock cloud_container %}
|
||||
|
||||
| {% endblock project_context %}
|
28
src/templates/project_settings/offer_setup.pug
Normal file
28
src/templates/project_settings/offer_setup.pug
Normal file
@ -0,0 +1,28 @@
|
||||
| {% extends 'project_settings/cloud_layout.html' %}
|
||||
|
||||
| {% block cloud_container %}
|
||||
#node-edit-form
|
||||
p This project is not setup for Blender Cloud #[span.text-muted (yet!)]
|
||||
p
|
||||
button.btn.btn-outline-primary.px-3(onclick='setupForFilm()')
|
||||
i.pr-2.pi-blender-cloud
|
||||
| Setup Project for Film
|
||||
|
||||
| {% endblock cloud_container %}
|
||||
|
||||
| {% block footer_scripts %}
|
||||
script.
|
||||
function setupForFilm() {
|
||||
$.ajax({
|
||||
url: '{{ url_for( "cloud.setup_for_film", project_url=project.url) }}',
|
||||
method: 'POST',
|
||||
})
|
||||
.done(function() {
|
||||
window.location.reload();
|
||||
})
|
||||
.fail(function(err) {
|
||||
var err_elt = xhrErrorResponseElement(err, 'Error setting up your project: ');
|
||||
toastr.error(err_elt);
|
||||
});
|
||||
}
|
||||
| {% endblock %}
|
67
src/templates/project_settings/settings.pug
Normal file
67
src/templates/project_settings/settings.pug
Normal file
@ -0,0 +1,67 @@
|
||||
| {% extends 'project_settings/cloud_layout.html' %}
|
||||
|
||||
| {% block cloud_container %}
|
||||
#node-edit-form
|
||||
form(onsubmit="save(this, '{{ url_for('cloud.save_film_settings', project_url=project['url']) }}'); return false;")
|
||||
| {% for field in form %}
|
||||
|
||||
| {% if field.name == 'csrf_token' %}
|
||||
| {{ field }}
|
||||
| {% else %}
|
||||
| {% if field.type == 'HiddenField' %}
|
||||
| {{ field }}
|
||||
| {% else %}
|
||||
|
||||
| {% if field.name not in hidden_fields %}
|
||||
|
||||
.form-group(class="{{field.name}}{% if field.errors %} error{% endif %}")
|
||||
| {{ field.label }}
|
||||
| {% if field.name == 'picture' %}
|
||||
| {% if post.picture %}
|
||||
img.node-preview-thumbnail(src="{{ post.picture.thumbnail('m', api=api) }}")
|
||||
a(href="#", class="file_delete", data-field-name="picture", data-file_id="{{post.picture._id}}") Delete
|
||||
| {% endif %}
|
||||
| {% endif %}
|
||||
| {{ field(class='form-control') }}
|
||||
|
||||
| {% if field.errors %}
|
||||
ul.error
|
||||
| {% for error in field.errors %}
|
||||
li {{ error }}
|
||||
| {% endfor %}
|
||||
| {% endif %}
|
||||
|
||||
| {% else %}
|
||||
| {{ field(class='d-none') }}
|
||||
| {% endif %}
|
||||
|
||||
| {% endif %}
|
||||
| {% endif %}
|
||||
|
||||
| {% endfor %}
|
||||
button.btn.btn-outline-success.btn-block(type='submit')
|
||||
i.pi-check
|
||||
| Save
|
||||
|
||||
|
||||
| {% endblock cloud_container %}
|
||||
| {% block footer_scripts %}
|
||||
script(type='text/javascript', src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.ui.widget.min.js') }}")
|
||||
script(type='text/javascript', src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.iframe-transport.min.js') }}")
|
||||
script(type='text/javascript', src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.fileupload.min.js') }}")
|
||||
script(type='text/javascript', src="{{ url_for('static_pillar', filename='assets/js/file_upload.min.js') }}")
|
||||
|
||||
script.
|
||||
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: true, nodeId: ''});
|
||||
|
||||
function save(form, url) {
|
||||
let serizalizedData = $(form).serializeArray()
|
||||
$.post(url, serizalizedData)
|
||||
.done(function(xhr) {
|
||||
toastr.success('Properties saved');
|
||||
})
|
||||
.fail(function(err) {
|
||||
toastr.error(xhrErrorResponseElement(err, 'Error saving properties: '));
|
||||
});
|
||||
}
|
||||
| {% endblock %}
|
Loading…
x
Reference in New Issue
Block a user