Compare commits

...

403 Commits

Author SHA1 Message Date
Anna Sirota
e7886261bd Work around cryptography requiring Rust compiler
Pins all the build dependencies required by poetry, otherwise
poetry installs arbitrary versions of them, which leads to a wrong
version of cryptography being installed and the build failing.
Note that Cloud dependencies (pillar and the like) has to have their
build dependencies pinned in the same exact manner, for the same reason.

Pins all the runtime dependencies as well, because Cloud can only use
poetry==1.0 due to its source dependencies, so there's no
"poetry lock --no-update" and each "poetry lock" updates arbitrary packages.
2021-03-18 18:42:10 +01:00
81c5687f02 Deploy: Pin poetry version to 1.0
It looks like more recent versions of poetry do not handle well nested
local package deps.
https://github.com/python-poetry/poetry/issues/3098
2021-01-20 11:08:23 +01:00
Anna Sirota
6b835e74f1 Handle date_deletion_requested in user-modified webhook D10139 2021-01-20 10:43:11 +01:00
b6127c736d Update gulp-sass 2020-07-23 18:43:17 +02:00
7280f1dbc1 Update poetry.lock 2020-07-23 18:43:01 +02:00
28ee78ec02 Learn: New featured projects 2020-07-23 12:20:05 +02:00
96a54695af Homepage: New featured projects 2020-07-23 12:19:37 +02:00
cbdcd04423 Fixed copy-paste bug in remoteip.conf 2020-04-17 15:15:16 +02:00
c618e6cf17 Fixed typo in Dockerfile 2020-04-17 14:09:43 +02:00
b8defe329e Apache: enabled & configured mod_remoteip
This module makes it possible to do access control & logging based on
client's real IP address, rather than the internal IP address of HaProxy.
2020-04-17 11:38:53 +02:00
cf887d8f5f Homepage: Fix project name
From workshop to workflow
2020-04-10 10:09:53 +02:00
11effcb580 ToS: Update base pricing
Closes T74691.
2020-04-09 20:08:51 +02:00
35ba45445f ToS: Update Blender Institute Address 2020-04-09 20:08:07 +02:00
6c720b7b08 Homepage: Update banners 2020-04-09 20:02:03 +02:00
344a66f0eb Update package-lock.json 2020-04-09 16:28:19 +02:00
941ed4a0e0 Deploy: Add redirect for coffee-run and settlers 2020-04-09 16:28:19 +02:00
acfffce48d Configured Poetry to not use virtualenvs in ./.venv
Having a virtualenv in `.venv` is very convenient because many tools
automatically pick up on it. However, this then also happens during the
construction of the Docker images, which subsequently breaks.

Until a proper fix is found, it's easiest to just put the virtualenv
outside of the project.
2020-03-19 17:42:45 +01:00
b6b097483a Update pages with assets featuring latest content 2019-11-14 12:12:50 +01:00
b76be2a7ba Add /design-system endpoint
This is where the representation of the design system will reside.
When the application runs in production (with DEBUG = False) the url
will return 404.
2019-11-13 18:47:28 +01:00
c168c09293 Tweak to gulp all command
First run gulp in pillar and other dependencies, then run gulp in the
current repo.
2019-11-13 10:43:25 +01:00
db74c89e6f UI Libraries Template: Wrong link to characters project.
Thanks @kednar for the report!
2019-07-26 12:27:32 +02:00
23d7e50df2 Fix link in HDRi section 2019-06-20 19:41:04 +02:00
a8afccba00 Fix T65655 2019-06-10 17:51:55 +02:00
24118b6777 Re-locked dependencies 2019-05-31 17:05:05 +02:00
76825fda39 Render avatar of current user using Vue.js
Requires Pillar 47474ac936ffb1d179161c8a3cac5d20e6005659
2019-05-31 17:05:05 +02:00
d49f69ecbd Upgraded Gulp 3.9 → 4.0 and removed gulp-livereload 2019-05-31 12:27:35 +02:00
c91a52046d Fixed deprecation warning from WTForms 2019-05-29 16:40:51 +02:00
577bf8b964 MongoDB: fixed deprecation warnings
- collection.count() → either counting the result or using count_documents()
- collection.update() → replaced by update_one()
2019-05-29 16:40:51 +02:00
049d71dc77 UnitTest.assertEquals → assertEqual 2019-05-29 16:40:51 +02:00
2075c8a790 Re-locked dependencies 2019-05-29 16:40:51 +02:00
2d6b5d4b67 Webhook: Update users' avatars with Celery task when changed on Blender ID 2019-05-28 16:19:01 +02:00
5f497fc645 Docker-compose: Upgraded Mongo 3.4.2 → 3.4 (so latest micro in 3.4.x)
This currently upgrades to 3.4.20
2019-05-28 16:18:27 +02:00
a321d3501a Docker-compose: upgraded Redis 3.2.8 → 5.0 2019-05-28 16:18:27 +02:00
d2d4a52846 Re-locked dependencies after Pillar updated deps 2019-05-28 16:18:27 +02:00
ae176cbbdf Re-locked dependencies 2019-05-23 13:54:59 +02:00
d5f5b63b3f Werkzeug update 0.15.2 → 0.15.4 2019-05-22 10:33:23 +02:00
274766d6f4 Added little note about rerunning poetry update after dependencies changed 2019-05-14 12:02:14 +02:00
e74d573063 Re-locked dependencies 2019-05-14 11:34:38 +02:00
f514cc4176 README: documented use of Poetry 2019-05-14 10:36:15 +02:00
3d567ff6f8 Docker: use variables instead of hard-coded stuff
WHEELHOUSE: since we're defining the variable we might as well use it.
DOCKER_IMAGE_NAME: introduced to prevent duplications of the name, and to
    add a little confirmation message when the script is done.
2019-05-14 10:36:15 +02:00
ba69dd46a0 Staging: be more selective about which branch of pillar-python-sdk to use
Because pillar-python-sdk doesn't have a `production` branch, it was always
using `master`. Now it's only using `master` if `STAGING_BRANCH`=`production`.
2019-05-14 10:36:15 +02:00
cfbb3d7e5a Poetry'ising the docker stuff 2019-05-14 10:36:15 +02:00
c9bbf26a71 Moved to Poetry 2019-05-14 10:36:15 +02:00
35675866ee Build our own HAproxy docker image
The HAproxy docker image we were using is no longer maintained (hasn't been
for years), but is built upon Alpine Linux which has a big security leak:
https://talosintelligence.com/vulnerability_reports/TALOS-2019-0782

The security leak is fixed in this build of the docker image, but we should
move to something else (lke Træfik).
2019-05-09 14:12:02 +02:00
d813935f43 Fixed unittest
Broke in 468fc8575106183b0eac64da5b334d3b9e0e1167
2019-04-26 12:53:26 +02:00
947dab3185 Use 16_9 picture for project thumbnail
This allows us to use picture_header as an actual header from now on.
2019-04-19 13:00:04 +02:00
c0cb80ceec Use absolute url of Open Graph image links 2019-04-19 12:54:22 +02:00
48c8f79371 Use _opengraph macro in landing.pug 2019-04-19 12:53:58 +02:00
53b22641f2 Improve readability of _opengraph macro 2019-04-19 12:53:24 +02:00
4f9699c7ae Remove 16_9 image from extension props
This property is now available on Project level.
2019-04-19 12:52:47 +02:00
a04e62e3e9 Rename project_type to category in Project
Requires renaming custom_props.cloud.project_type fields to
custom_props.cloud.category in all documents of the projects
collection.
2019-04-19 11:13:31 +02:00
5df18b670a Display field description if available 2019-04-19 10:43:38 +02:00
a5cd12ad87 Remove unneeded if statement
When rendering this template we do not provide the hidden_fields
list (this code was partially copied from project edit.pug).
2019-04-19 10:43:16 +02:00
2c51407196 Fix typo 2019-04-19 10:06:15 +02:00
0e0716449a UI Footer: Add link to Films. 2019-04-18 15:33:38 +02:00
f0c4f1576c UI Footer: Rename links to sections.
LEARN -> TRAINING
RESOURCES -> CLOUD
2019-04-18 15:33:23 +02:00
42edd9486b UI Footer: Fix link to YouTube 2019-04-18 15:32:26 +02:00
8534cdbaeb Services: Use 16_9 image for opengraph. 2019-04-18 14:45:55 +02:00
7209a3c525 UI Homepage: Three cards for featured projects. 2019-04-15 12:46:31 +02:00
81a564a9d9 UI Learn: swap thumbnails and link to asset/project in courses and worshops. 2019-04-12 17:32:49 +02:00
1c47197fd2 UI Learn: tweak in wording. 2019-04-12 17:32:00 +02:00
ea95e7b2b2 UI Learn: Minor layout adjustment. 2019-04-12 17:31:38 +02:00
be28b2d13d UI Learn: add quick links to 3 items per category. 2019-04-12 17:31:16 +02:00
ea9da2acdb UI Libraries: swap thumbnail and link to asset. 2019-04-12 17:30:27 +02:00
c66592a5ae Libraries: cleanup leftover. 2019-04-12 17:29:54 +02:00
1e3041d997 UI Libraries: Remove hand-on section. 2019-04-12 17:29:17 +02:00
f5db3c8da2 UI Libraries: Layout adjustments. 2019-04-12 17:29:04 +02:00
224e4dc1e0 UI Libraries: wording tweaks. 2019-04-12 17:28:41 +02:00
c1e52ae320 UI Libraries: Swap Textures for HDR Images 2019-04-12 17:28:19 +02:00
b8be19c729 UI Libraries: Add quick links to 3 items. 2019-04-12 17:27:28 +02:00
51d081971c Libraries: cleanup unused scripts. 2019-04-12 17:26:02 +02:00
b438c319b0 UI: Layout adjustments to category_list components. 2019-04-12 17:22:55 +02:00
87bc2b5378 Utility for marking the first item on a list as 'new'.
The span element of the first child will include a 'new' label on it.

Usage: add the class 'list-first-new' to a list.
2019-04-12 17:21:47 +02:00
c3261ed83a New images for gallery, training, and libraries. 2019-04-12 17:17:59 +02:00
468fc85751 Homepage: increase random featured assets to six. 2019-04-10 17:19:35 +02:00
049fdf3b63 Homepage: bring back two column homepage, only on XL screens. 2019-04-10 17:19:17 +02:00
ad31b6338f Homepage: sass file for homepage styling. 2019-04-10 17:18:23 +02:00
61b1ab0c20 Remove whitespace 2019-04-08 16:44:29 +02:00
08de073464 Remove whitespace 2019-04-08 16:44:29 +02:00
a756fb5f6e UI Landing: Fix alignment on Firefox.
Thanks Ines for the report!
2019-04-04 18:48:18 +02:00
2050f4b7d8 Front-page update 2019-04-04 16:47:44 +02:00
51e22eb414 UI Landing: padding on browse button. 2019-04-04 16:41:28 +02:00
cabfce12c0 UI Landing: Use 16 by 9 image for opengraph. 2019-04-04 16:41:28 +02:00
b6d9039e82 Add navigation and extension links to /browse 2019-04-04 15:31:52 +02:00
4a6e971e71 Show only groups and assets in browse endpoint 2019-04-04 15:31:28 +02:00
eeab0a407e UI Landing: Style tweaks 2019-04-04 14:20:19 +02:00
eafc1e981f UI Landing: Timeline -> Project Timeline 2019-04-04 14:20:09 +02:00
dfbd03e448 UI Landing: Jumbotron padding tweak and mobile. 2019-04-04 14:19:52 +02:00
12c64f13a2 UI: Rename 'Explore' to 'Browse' 2019-04-04 14:19:36 +02:00
b62f500b2e Cast empty string value in form_field to None
For FilmProjectForm, when no value is specified we want to save it
as None in the project document.
2019-04-04 13:09:53 +02:00
6d47946b1b Fix for exception in /open-movies
When extension_props.cloud.poster was set to empty string, we would
try to get file anyway and we would set the has_poster has_poster
convenience attribute to true. This would lead to an exception when
trying to access the poster file object in the template.
2019-04-04 11:49:12 +02:00
b4ecf93485 UI Browse: Remove description. 2019-04-04 02:04:00 +02:00
3d6b2452d6 Fix in is_cloud_project
Handle missing extension_props attribute.
2019-04-04 00:50:35 +02:00
4afe23e284 Introducing top level browsing
We introduce a new /p/<project_url>/browse endpoint, which allows to
see all top-level nodes of a project.
2019-04-04 00:27:14 +02:00
479153af9b Do not show hidden pages in project landing 2019-04-04 00:27:14 +02:00
aee369cc5a UI Landing: alt name on image. 2019-04-04 00:26:38 +02:00
8f0670d017 UI Landing: Link icon, text and Explore button to project_explore_url.
To be replaced with the actual 'explore' endpoint.
2019-04-03 23:44:37 +02:00
858bed66f4 UI Landing: padding and column size adjustments. 2019-04-03 23:40:13 +02:00
7a02f86a5b UI Landing: open video_url in the page overlay. 2019-04-03 23:39:53 +02:00
37667424ab Cleanup: remove unused font-pillar.css link.
They are built inside project-main.sass now
2019-04-03 23:10:33 +02:00
b4739521ed Layout Template: Introducing announcements.
Used for non-subscribers (current_user without .has-cap('subscriber'),
to give a friendly reminder about cool promos!
2019-04-03 22:54:00 +02:00
d125a6ac55 config_local: Example for announcements to non-subscribers. 2019-04-03 22:50:37 +02:00
4473d56379 Fix for exception
Check that ‘extension_props’ exists in project before looking for
EXTENSION_NAME.
2019-04-03 17:36:27 +02:00
c971f799ef Override /p/<project_url>
By overriding this Pillar endpoint, we allow more control over how
the landing page of a project is rendered, based on the presence
of the ‘cloud’ extension property.
2019-04-03 17:00:37 +02:00
36f31caf04 UI Landing: Show logo and watch url if any. 2019-04-03 16:59:25 +02:00
d54d5ec157 Use poster file as preview for film projects 2019-04-03 16:43:48 +02:00
4a180e3784 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.
2019-04-03 15:54:45 +02:00
6ac75c6a14 UI Production: use same header and opengraph as other collections. 2019-04-03 15:48:38 +02:00
4b7cc3f58e UI Libraries: Fix wrong URL for characters project. 2019-04-03 15:47:59 +02:00
cbbaf90002 Templates: Add opengraph to collections. 2019-04-03 15:43:53 +02:00
c5154240ca Cleanup. One line for block page_title. 2019-04-03 15:43:01 +02:00
1e62aff62c Cleanup: Unused mixin include. 2019-04-03 15:42:37 +02:00
b015cc8fa4 Cleanup: Remove URL from category_list_header component. 2019-04-03 15:33:04 +02:00
3be54da5c3 Cleanup: Remove unused components mixin. 2019-04-03 15:32:32 +02:00
5c1b94544a UI: Tweaks to descriptions in Learn. 2019-04-03 15:31:38 +02:00
1ed2a3937e UI: Tweaks to descriptions in Libraries. 2019-04-03 15:31:14 +02:00
80b69438ed UI: Tweaks to descriptions in Index Collection. 2019-04-03 15:30:46 +02:00
3f3112f272 Pug Components: category_list_item component.
Taken from Pillar. Used in Libraries, Training, etc.
2019-04-03 15:29:14 +02:00
1d7cbd8ba5 UI Films: minor style tweaks. 2019-04-03 15:03:52 +02:00
a53dc51680 UI Landing: Align header to top. 2019-04-03 15:03:30 +02:00
29ff9609e7 UI Films: use variable for project URL.
Instead of building url_for() many times.
2019-04-03 15:03:18 +02:00
bd07a63acd Index Collection Template: Use header and opengraph macros. 2019-04-03 15:02:11 +02:00
9bf6c0fcb3 Templates: New template for films. 2019-04-03 13:04:25 +02:00
142dd36e3c CSS: Add alias for pi-blender-cloud from font-pillar. 2019-04-03 13:01:39 +02:00
e03cf2f5da CSS: Include font-pillar as part of main.css 2019-04-03 13:01:08 +02:00
79b409c83a CSS: Include variables in project-main.sass 2019-04-03 11:49:17 +02:00
a67af63459 Template Libraries: Use header and opengraph macros. 2019-04-03 11:41:42 +02:00
362694f4b6 Template Learn: Use header and opengraph macros. 2019-04-03 11:41:33 +02:00
364fa76956 UI Landing: Use variable instead of magic number for background. 2019-04-03 11:40:55 +02:00
144dcf7a76 Sass: Introducing variables.sass file.
For Blender Cloud specific variables.
2019-04-03 11:40:01 +02:00
f8995fb657 Template Services: Use header and opengraph macros. 2019-04-03 11:39:11 +02:00
95762acf14 Templates: Introducing components.pug
For Blender Cloud specific components.

No need for them to be part of Pillar.
2019-04-03 11:38:27 +02:00
35a9986290 Templates: New macro for Opengraph.
To be used inside the opengraph block.

e.g
{% block og %}
  {{ opengraph(title, description, image, url) }}
{% endblock %}
2019-04-02 19:48:51 +02:00
e8c878d0f3 UI Blog: Light background color and border for edit bar.
Makes it stand out more especially when there is no image in the post.
2019-04-01 14:57:34 +02:00
fdf05d16de UI: Light background color for sidebar container. 2019-04-01 14:56:59 +02:00
49bffd108d UI Index Collection: Match style with Training and Libraries. 2019-04-01 12:33:47 +02:00
87c1eae1a6 UI Homepage: Replace 'film in production' with just Spring.
Since the film is no longer in production! We are done!
2019-04-01 12:32:49 +02:00
163aee6bbc UI Project: Show sidebar by default. 2019-03-29 15:46:43 +01:00
5f01b0d6ba CSS Cleanup
An oversight. We already styled node-details-description a few lines above.
2019-03-29 15:37:46 +01:00
da955ce4af UI Landing: No need to set 'landing' as title.
Just use the default since project landing is the same as project home.
2019-03-29 15:37:46 +01:00
ad1a55c5d5 UI Landing: Use dark background only on project home landing page. 2019-03-29 15:37:46 +01:00
db976a4d50 UI Landing: Bigger text in node description. 2019-03-29 15:37:46 +01:00
42215d2b02 UI Layout: New jinja block for adding custom classes to the body.
Usage: {% block bodyclasses %}custom-class-name{% endblock %}
2019-03-29 15:37:46 +01:00
9cf3a7147e Merge branch 'production' 2019-03-29 15:27:58 +01:00
fb016c3e3b build wheels using the correct Docker image 2019-03-29 15:18:10 +01:00
039ab1ce70 UI Project sidebar: padding and border classes adjustment. 2019-03-28 21:12:24 +01:00
1bd1c6e5fd Merge branch 'master' into wip-breadcrumbs 2019-03-28 21:04:13 +01:00
353660c7ba UI Project: Sidebar toggle button. 2019-03-28 21:03:24 +01:00
b77f3aaa49 Breadcrumbs: clicking a breadcrumb now calls displayNode(nodeId)
Requires Pillar 1fd17303a53016c15f685deebda65bea8e71be90
2019-03-28 16:43:15 +01:00
50c0842e72 Breadcrumbs: Move from sidebar to project context.
Since we will be able to toggle the sidebar from the breadcrumbs.
2019-03-28 16:06:32 +01:00
5d2ba7fc96 Main Stylesheet: Include breadcrumbs component from Pillar. 2019-03-28 16:04:56 +01:00
6972e1662d Show breadcrumbs in project navigation
Requires Pillar 4499f911.
2019-03-28 12:41:57 +01:00
af6022c997 UI Services: Minor layout adjustments.
* Align all items to the left.
* Match header with Training and Libraries.
* Make images a link.
2019-03-27 16:00:19 +01:00
2fc1738e99 UI Libraries: Use single-column listing.
Uses component from components.pug
2019-03-27 16:00:19 +01:00
149013d64c UI Learn: Use single-column listing.
Uses component from components.pug
2019-03-27 16:00:19 +01:00
e13a1bff1b UI Navigation: Move Training after Films.
So we have categories listing grouped together (Training/Libraries/Services).
2019-03-27 16:00:19 +01:00
9ac60fe922 Tweak build-all.sh to run with bash 3.2
The ;& statement is supported in 4.0 or higher, which is not natively
provided on macOS.
2019-03-27 15:38:22 +01:00
2f78f36f61 Update package-lock.json
The previous update was not sufficient apparently.
2019-03-27 15:10:36 +01:00
5edcf931e9 Update package-lock.json
The flatmap-stream dependency was removed from NPM since it was
malicious.
2019-03-27 14:02:21 +01:00
b417f25811 UI Homepage: Single-column layout. 2019-03-27 13:08:44 +01:00
691c1411bc Added jwtkeys/README.md 2019-03-27 12:58:08 +01:00
9f3946ba9e Documented deployment scripts 2019-03-27 12:57:50 +01:00
269daa0d43 UI Navigation: Padding adjustment. 2019-03-27 12:55:08 +01:00
39516e63cc UI Navigation: Cleanup (adding comments). 2019-03-27 12:54:54 +01:00
84881743ef UI Navigation: Category title 'Open Projects' rename to 'Films' 2019-03-27 12:54:10 +01:00
e975492869 UI Navigation: Link to project category.
Without it, going from one project to another in the same category (like
going from Textures library to HDRI) is cumbersome, having to go always
through the homepage first.
2019-03-27 12:53:27 +01:00
78e1c728fa UI Landing: Cleanup 2019-03-27 12:36:39 +01:00
be18dfb985 UI Landing: Margin top of project description. 2019-03-27 12:35:58 +01:00
8797b18754 UI Landing: Cleanup, remove unused attribute. 2019-03-27 12:34:44 +01:00
727ba3fd58 UI Landing: Simplify javascript code for overlay. 2019-03-27 12:34:11 +01:00
a369c04b38 UI Landing: Remove project name and gallery title. 2019-03-27 12:31:51 +01:00
fad5f803e8 UI Landing: Fade header to background color. 2019-03-27 12:18:15 +01:00
143cd27c55 UI Landing: Use dark background. 2019-03-27 12:17:15 +01:00
2f854ebeee UI Landing: Replace Blog updates with Timeline. 2019-03-27 12:14:24 +01:00
6fa3af50cf UI Landing: Show link to project edit. 2019-03-27 12:13:36 +01:00
1fd2443bd7 UI Landing: Show project name and summary in header. 2019-03-27 12:12:59 +01:00
c5f8add5f5 Made it easier to rebuild the Docker image after someone else built it
Because we only pushed the final image to Docker Hub, it was impossible to
pull the base image someone else created and "quickly" build a new deploy
image.

Now the deploy scripts push (some) of the intermediate images as well,
making it possible to pull them later. I've added `build-pull.sh` and
`full-pull.sh` to perform this pull and built up from the pulled images.
2019-03-13 15:47:12 +01:00
a8e5d593ac Address concern rBC6cbd5ca369ed
Improvements to the deployment script.
2019-02-13 12:12:36 +01:00
fc986b0ab6 Renamed docker/4_run/deploy to docker/4_run/staging
"Staging" covers the meaning of what is actually happening better than
"deploy". I want to keep "deploy" for actually deploying onto a production
server.
2019-02-13 10:39:18 +01:00
b7b6543043 Navigation: Unified cloud navigation
* Removed main drop down menu
* Added "My cloud" to user menu
* Attract/Flamenco is found under Production Tools menu
* Attract/Flamenco has the same navigation as its project
2019-02-07 14:45:55 +01:00
d32c44e50c Navigation: Unified cloud navigation
Welcome page was missing Blender cloud logo
2019-02-06 11:42:39 +01:00
369161e29f Navigation: Unified cloud navigation
* Removed main drop down menu
* Added "My cloud" to user menu
* Attract/Flamenco is found under Production Tools menu
* Attract/Flamenco has the same navigation as its project
2019-02-06 10:31:36 +01:00
3cd55e2a83 Added two scripts to make deployment a bit easier 2019-02-06 09:34:55 +01:00
1071915f27 Gulp fix for NodeJS 10 2019-01-04 14:21:27 +01:00
5f9406edd2 Vue Comments: Comments ported to Vue + DnD fileupload
* Drag and drop files to comment editor to add a file attachment
* Using Vue to render comments

Since comments now has attachments we need to update the schemas
./manage.py maintenance replace_pillar_node_type_schemas
2018-12-12 11:45:46 +01:00
5bf1693d5b Removed RabbitMQ docker container from docker-compose.yml
Now that Celery switched to using Redis as broker, we no longer need
RabbitMQ. Celery has been running on Redis for a while now and it all seems
fine, so it's time to wave the Rabbit goodbye.
2018-12-04 17:57:49 +01:00
27caff7e6e Docker: added little list of Redis database numbers we're using 2018-12-04 11:30:48 +01:00
63d25d1dca Fix broken thumbnail in Blog index 2018-11-23 14:56:57 +01:00
2950a4347a Quick-Search: Added Quick-search in the topbar
Changed how and what we store in elastic to unify it with how we store
things in mongodb so we can have more generic javascript code
to render the data.

Elastic changes:
  Added:
  Node.project.url

  Altered to store id instead of url
  Node.picture

  Made Post searchable

./manage.py elastic reset_index
./manage.py elastic reindex

Thanks to Pablo and Sybren
2018-11-22 15:31:52 +01:00
76a707e5bf Gulp: Watch for changes in both blender-cloud and pillar folders. 2018-11-20 19:19:22 +01:00
5218bd17e3 Project-Timeline: Introduced timeline on projects
Limited to projects of category assets and film for now.
2018-11-20 16:29:01 +01:00
37fe235d47 Lazy Home: Lazy load latest blog posts and assets and group by week and
project.

Javascript tutti.js and timeline.js is needed, and then the following to
init the timeline:

$('.timeline')
    .timeline({
        url: '/api/timeline'
    });

# Javascript Notes:
## ES6 transpile:
* Files in src/scripts/js/es6/common will be transpiled from
modern es6 js to old es5 js, and then added to tutti.js
* Files in src/scripts/js/es6/individual will be transpiled from
modern es6 js to old es5 js to individual module files
## JS Testing
* Added the Jest test framework to write javascript tests.
* `npm test` will run all the javascript tests

Thanks to Sybren for reviewing
2018-11-12 12:57:24 +01:00
1a49b24f8e Blog Bug fix: Unable to render blog before first post 2018-10-23 15:09:02 +02:00
0b2a3c99ce Loading bar: Introduced two event listeners on window 'pillar:workStart' and 'pillar:workStop' that (de)activates the loading bar.
Reason:
* To decouple code
* Have the loading bar active until whole page stopped working
* Have local loading info

Usage:
$.('.myClass')
   .on('pillar:workStart', function(){
    ... do stuff locally while loading ...
    })
   .on('pillar:workStop', function(){
   ... stop do stuff locally while loading ...
   })

$.('.myClass .mySubClass').trigger('pillar:workStart')
... do stuff ...
$.('.myClass .mySubClass').trigger('pillar:workStop')
2018-10-23 13:57:02 +02:00
a674de4db5 Remove CELERY_BEAT_SCHEDULE from config_local
CELERY_BEAT_SCHEDULE shouldn't need any changes in config_local for
production; the default should be production-ready.
2018-10-10 14:58:52 +02:00
d2815acd80 Free assets: Assets should not be advertised as free if the user is a logged in subscriber. 2018-10-04 17:44:08 +02:00
670c600382 Organizations: Added null check to properly render new Organizations 2018-10-04 15:48:26 +02:00
f2207bc4d4 Asset list item: Don't show user.full_name in latest and random assets 2018-10-04 12:30:05 +02:00
a9848c3fad Random asset bug fix: If 2 assets from the same project where returned, the second one would get a corrupt url 2018-10-04 10:11:38 +02:00
c81711de53 Video Duration: The duration of a video is now shown on thumbnails and bellow the video player
Asset nodes now have a new field called "properties.duration_seconds". This holds a copy of the duration stored on the referenced video file and stays in sync using eve hooks.

To migrate existing duration times from files to nodes you need to run the following:
./manage.py maintenance reconcile_node_video_duration -ag

There are 2 more maintenance commands to be used to determine if there are any missing durations in either files or nodes:
find_video_files_without_duration
find_video_nodes_without_duration

FFProbe is now used to detect what duration a video file has.

Reviewed by Sybren.
2018-10-03 18:30:40 +02:00
2a8a109d83 Homepage: Update sidebar image for Spring 2018-10-03 11:14:11 +02:00
35b1106ccc Tagged Asset: Added metadata
Video duration, Project link and pretty date
2018-09-26 11:29:15 +02:00
1f326b2728 Asset: Fix video progress not filling up correctly 2018-09-25 12:19:22 +02:00
ccb3187c17 Assets: Fix video progress not showing 2018-09-24 13:32:08 +02:00
734a8db145 Index collection: use gradient on header 2018-09-21 16:56:01 +02:00
82c6c30a0a Tweaks to featured item in index collection 2018-09-21 16:52:21 +02:00
594af19b2b Main dropdown tweaks for responsive.
Most of the changes are done in Pillar, in 0_navbar.js (part of tutti).
2018-09-21 16:19:13 +02:00
3de73ac35e CSS: Use bootstrap variable for button roundness 2018-09-21 16:14:58 +02:00
97c549de08 Project Landing: cleanup unused classes 2018-09-21 12:19:48 +02:00
b5ff89f4ca Node details: Center only on landing 2018-09-21 12:10:44 +02:00
ef8bd8d22b Use node.properties.status instead of node.status 2018-09-20 19:04:43 +02:00
4ff52a8af0 Add post status to posts query for homepage 2018-09-20 19:03:57 +02:00
218ba3831c Navigation: add Art Gallery to the libraries nav 2018-09-20 18:13:41 +02:00
a753f29ccc Navigation: Add Learn and Libraries to homepage nav
Also remove Courses, Workshops
2018-09-20 18:11:26 +02:00
1a7be4b565 Navigation: Move links in the main dropdown to their own macros
So they can be easily re-used in other templates.
2018-09-20 16:37:02 +02:00
765f36261a Blog: Remove unused macro 2018-09-20 16:36:00 +02:00
bce054d47d Tagged Assets: Set the loading bar when loading images 2018-09-20 15:27:41 +02:00
89ea34724b Tagged Assets: Initial 8, load 8 more 2018-09-20 15:27:22 +02:00
7983a7b038 Use loadingBar utility. 2018-09-20 15:20:58 +02:00
7ba8ff7580 New templates for /learn and /libraries 2018-09-20 15:00:10 +02:00
fd1db5d2e0 Production: make asset title a link 2018-09-20 13:17:50 +02:00
205e34289f Blog: show status if not published 2018-09-20 13:01:57 +02:00
e7190f09dc Landing: Center text 2018-09-20 12:14:47 +02:00
7e61d218b9 Navigation: Blender Cloud -> Homepage 2018-09-20 12:07:08 +02:00
12936c80ea Navigation: Remove Blog entry 2018-09-20 12:06:57 +02:00
dcac1317bf Rename secondary_navigation to navigation_project
And move to _navigation.pug with the other navigations macros.
2018-09-20 12:06:43 +02:00
afa0c96156 Navigation: add custom nav for services 2018-09-19 19:34:49 +02:00
012eaaef11 Production: Load 4 assets, load 4 more 2018-09-19 19:34:07 +02:00
54e4e76945 Footer: Production lessons
Also move Art Gallery to Libraries
2018-09-19 19:06:52 +02:00
6cbd5ca369 Improve asset building process
After running ./gulp for every project, we delete node_modules.
2018-09-19 16:57:07 +02:00
690b35bab1 Add more tags to NODE_TAGS 2018-09-19 16:15:31 +02:00
356e4705b3 Add digital-painting to NODE_TAGS 2018-09-19 15:53:56 +02:00
748190d15b Fix jumbotron in index collection 2018-09-19 15:50:03 +02:00
05ff27a12d Navigation: Position icons 2018-09-19 15:42:33 +02:00
74e18bb500 Fix navigation 2018-09-19 15:39:58 +02:00
c460359b31 Run ./gulp in every subproject dir
This is necessary since in our gulp files we reference assets in
node_modules using relative paths. This makes the asset building
process much slower, and should be addressed in the future.
2018-09-19 14:48:16 +02:00
14daead15d Use correct permission format for gulp-chmod 2018-09-19 14:45:56 +02:00
8e9d63df2b Follow art direction for Spring banner 2018-09-19 12:42:12 +02:00
10addb1521 Spring background for index collection 2018-09-19 12:38:35 +02:00
735e6400e3 Background for spring project 2018-09-19 12:38:35 +02:00
a1d84196cd Add additional dependencies to package.json 2018-09-19 12:00:37 +02:00
678a03dbf1 Update NODE_TAGS 2018-09-19 11:34:22 +02:00
811dc4d65b Mark Production Lessons as new 2018-09-19 11:33:40 +02:00
265794d4b7 Pass title to /production 2018-09-19 11:20:32 +02:00
ece0ba4ae7 Dropdowns tweaks based on feedback 2018-09-19 11:20:17 +02:00
2395bd8090 Production Lessons: Added more tags 2018-09-18 16:54:57 +02:00
fef7d5feac Homepage: update image for Spring 2018-09-18 13:57:44 +02:00
f0f96bf2f1 Home project: Fix creating new projects 2018-09-18 13:57:25 +02:00
e05a0c0e04 Tagged assets: Style 'load more items' button 2018-09-18 12:55:14 +02:00
dbba955afe Homepage: use asset list template for random assets 2018-09-18 12:54:57 +02:00
17240f5793 Landing: fix styling of gallery 2018-09-18 12:54:32 +02:00
8ff8975dbb Welcome page: Styling 2018-09-17 18:42:04 +02:00
0a144ec12d Blog: Edit post link 2018-09-17 18:34:43 +02:00
6d9fa89d90 Project: Darker tree 2018-09-17 18:15:49 +02:00
06e7ea53bb Footer: Fix broken links 2018-09-17 18:15:36 +02:00
00cd29befc Layout: move footer and main menu into their own files 2018-09-17 17:18:43 +02:00
a5c7ec285d Style tweaks 2018-09-17 17:09:43 +02:00
7fff47c5c5 Use spans for index_collection navigation 2018-09-17 15:04:07 +02:00
534e212802 Project Landing: Don't set title
As it's set by the pages themselves using node.properties.url
2018-09-17 15:03:53 +02:00
1fac97e3f8 Homepage: style sidebar and cleanup CSS
homepage.sass is like 10 lines now :)
2018-09-17 12:52:01 +02:00
0556c5ae9a Homepage: Style comments 2018-09-17 12:16:52 +02:00
bb2c351460 Generic classes for styling 2018-09-17 11:36:57 +02:00
a65d771bd6 Tagged Assets: Support passing arguments
Pass LOAD_INITIAL_COUNT and LOAD_NEXT_COUNT

Also only show 'Load more' if LOAD_NEXT_COUNT is not set to 0
2018-09-16 06:30:48 +02:00
b50a3e1fb3 Tagged assets: add video progress and watched label 2018-09-16 05:52:20 +02:00
6f88de3b20 Menu: Remove columns for trainings
Experiment with a more compact menu, more readable with not so much text.
2018-09-16 05:06:41 +02:00
6569e22fa8 Spacing 2018-09-16 05:03:12 +02:00
c773145bd6 Blog: use jumbotron overlay 2018-09-16 04:28:37 +02:00
ae907719d0 Index Collection: Limit columns to 3 2018-09-16 03:43:21 +02:00
88f936772d Blog: Layout adjustments 2018-09-16 03:06:08 +02:00
0f1088702d Blog: Fix showing wrong single post
Also center comments and other minor tweaks
2018-09-16 02:03:25 +02:00
40f6ebd99c Project Landing: Fix links in latest updates
Part of T56813
2018-09-15 22:19:47 +02:00
fca2b0f44f Project Landing: Center titles
Part of T56813
2018-09-15 22:15:05 +02:00
08b1b03802 Blog: name in title 2018-09-15 22:09:12 +02:00
23bf27ca75 Layout: Add Art Gallery to menu 2018-09-15 21:36:32 +02:00
15264877e6 Blog minor fixes and tweaks 2018-09-15 21:33:11 +02:00
2eb969f7ee Blog listing: Show posts as cards 2018-09-15 21:23:45 +02:00
1196f178e8 One class too much 2018-09-15 17:26:56 +02:00
aaeecc1429 Profile page: Styling and layout 2018-09-15 16:41:47 +02:00
df33a1803e Navigation menu: re-order items and minor tweaks 2018-09-15 06:15:49 +02:00
dc59bb53de New global navigation menu. 2018-09-15 05:36:23 +02:00
8fdd54eaad Class names and minor blog style tweak 2018-09-14 20:30:58 +02:00
11f44560bb Projects: Use render_secondary_navigation macro 2018-09-14 17:13:54 +02:00
1015254d93 Bring project-main from Pillar
Rename project-landing, to _project-landing and include it in project-main.
It's just a few lines of code to be worth keeping as a separate CSS.
2018-09-14 17:13:23 +02:00
dfa0c14bb0 Fix Scss paths 2018-09-14 14:43:55 +02:00
bbb643e371 Update package-lock.json 2018-09-14 13:12:07 +02:00
7d3c24d712 Project view: update videojs path
The new path removes the version number, check package.json for that.
2018-09-14 01:25:50 +02:00
e1433c3c2a Layout: Cleanup
* jQuery and Bootstrap are now part of tutti, built by Pillar.
* Markdown is no longer used as Pillar has its own converter.
2018-09-14 01:21:22 +02:00
90d6685add Gulp: Cleanup dead code
The task for building tutti was never used, since all functionality is
built by pillar.

Also remove the dependencies for jQuery and Bootstrap in package.json
2018-09-14 01:19:45 +02:00
9fd233a8dc Initial styling of production lessons. 2018-09-13 18:10:12 +02:00
a40eb5d6e4 Use the new navigation_links provided by pillar 2018-09-13 16:36:41 +02:00
fab0d412fa Navbar: padding for items 2018-09-12 19:00:54 +02:00
c5287da78c Layout: Search icon in the navbar 2018-09-11 19:41:16 +02:00
37726bee0f Cleanup 2018-09-11 19:35:05 +02:00
10f15185e0 Homepage: Use macro for listing assets 2018-09-11 17:46:09 +02:00
c90cd41e23 Use list-asset() mixin for homepage latest assets 2018-09-10 19:01:43 +02:00
c8261e5df6 User menu tweaks 2018-09-10 17:12:34 +02:00
34ae8e55c3 Project view: show blog and pages in header 2018-09-10 16:12:09 +02:00
f9368c0729 Layout: Use mixin for top navigation 2018-09-07 18:12:01 +02:00
b4c51007ab Style production listing 2018-09-07 18:11:46 +02:00
a17253c482 Project Landing: Fix missing pillar-font 2018-09-07 18:11:37 +02:00
e348b003b1 Use mixin components from Pillar 2018-09-07 17:19:36 +02:00
23fbb68cfc Replace #project-loading spinning icon with a .loader-bar 2018-09-07 14:56:38 +02:00
4a4e75ee59 Project view: Cleanup and minor layout tweaks 2018-09-07 12:47:31 +02:00
9aae856ac8 Project View: fix alignment of edit tools 2018-09-06 18:16:07 +02:00
94c2c6e550 Start of "production videos", a.k.a. tagged assets overview
Tagged assets are shown in a list per tag. The list is dynamically
loaded with JavaScript.
2018-09-06 16:08:23 +02:00
0b1f295480 Services: Fix layout 2018-09-06 14:33:44 +02:00
a64d3902fd Bootstrap popovers are no longer used. 2018-09-06 14:24:31 +02:00
8dd1de1018 Merge branch 'wip-redesign'
# Conflicts:
#	src/templates/homepage.pug
#	src/templates/services.pug
2018-09-06 14:13:22 +02:00
b12b320cf0 Project view: include video_plugins.min.js 2018-09-06 13:32:57 +02:00
95e51e90de Homepage cleanup. 2018-09-06 13:03:40 +02:00
92106459a0 Use Navigation Tabs for homepage and index collections 2018-09-06 13:03:22 +02:00
982047fc3b Pug: Tweaks to components 2018-09-06 12:59:48 +02:00
5a36888f61 Navigation: Dropdowns for Services and Open Projects 2018-09-06 12:58:56 +02:00
59e0adf3ca Pug: Bring new templates from Pillar 2018-09-06 12:54:15 +02:00
3fdbb92b93 Fixed typo 2018-09-05 15:11:46 +02:00
cf98883633 Make <meta property="og:url"> tags have an absolute URL
Most of those can simply use `{{ request.url }}`, as this already contains
the absolute URL of the currently displayed page.
2018-09-05 13:58:13 +02:00
7b32b97203 Remove https://cloud.blender.org from URLs
URLs should be host-relative, so that they also work on devservers.
URLs in emails should remain absolute, though; we may want to change those
to use {{ url_for(..., _external=True) }} at some point.
2018-09-05 13:57:15 +02:00
7f58be4568 Updated Blender Cloud add-on to 1.9.0
Also change the config_local.py so that we only have to change one variable
for a new version.
2018-09-05 13:40:24 +02:00
c25df6f0ad Cleanup 2018-08-31 19:33:14 +02:00
813750a006 Layout cleanup 2018-08-31 19:08:52 +02:00
9ba2735c8c CSS Cleanup 2018-08-31 19:08:23 +02:00
73e8a81f3c Minor style tweaks 2018-08-31 13:58:26 +02:00
9ffcde3348 Use styling from Pillar 2018-08-31 13:58:08 +02:00
f0b18e88f4 Use pug mixins for header, cards and navigation 2018-08-31 13:57:51 +02:00
ed211f9473 Introducing Pug mixin components
For now added jumbotron, secondary navigation, card decks and individual cards.

Thanks @sybren for the suggestion.
2018-08-31 13:57:09 +02:00
7a4c7d75f6 Project Landing uses new CSS 2018-08-31 13:55:35 +02:00
cc16351136 NPM: Upgrade libraries
New dependency is jQuery, we already depended on it it but now we use it
from npm package for easier version control and upgrades.
2018-08-31 13:54:39 +02:00
7dc1e6f9a1 Gulp: Only chmod files when in --production 2018-08-31 13:53:37 +02:00
fcf715b5b1 Services page: move to bootstrap 4 2018-08-31 13:52:40 +02:00
099984f97c Added #!/bin/sh at top of shell script 2018-08-30 12:53:34 +02:00
8bfb40ce54 Various Docker image upgrades, read the entire commit message!
- Ubuntu 17.10 → 18.04.
- Python 3.6.3 → 3.6.6.
- Use `DEBIAN_FRONTEND=noninteractive` to prevent prompts during
  installation.
- Install `tzdata` in the base image as it's required by subimages.
- Correctly set maintainer in Dockerfile.
2018-08-30 12:53:34 +02:00
d60a65c9f0 End BLENDER_ID_ENDPOINT with a slash 2018-08-30 12:46:58 +02:00
9cd2853e49 Upgrade pip after building Python 2018-08-30 12:31:31 +02:00
3d5554d9ce BLENDER_ID_ENDPOINT should end with a slash
There is always a path component in a URL.
2018-08-29 14:20:23 +02:00
764ccfa78e Add -e . to requirements-dev.txt 2018-08-29 12:24:45 +02:00
0b8ebecfea Don't import flask_login.request
`flask_login.request` is the exact same thing as `flask.request`, so
importing it from `flask_login` makes no sense. Also, it's been removed
from the new Flask-Login.
2018-08-29 12:19:40 +02:00
68d09dc886 CSS: cleanup 2018-08-28 15:56:06 +02:00
169a7f51f0 navbar-container: cleanup 2018-08-28 15:55:32 +02:00
f48a4883ae Index collection redesign 2018-08-27 16:58:01 +02:00
01b6693324 Cleanup styling. Use bootstrap classes instead 2018-08-27 16:57:34 +02:00
012ba06655 Use system fonts (see main.sass) 2018-08-27 16:55:48 +02:00
2be601d0b0 Introducing Bootstrap 4
Bootstrap (and its dependency popper.js) are now used from npm packages,
allowing better version control and custom building of only the required
components for both styling and javascript.

At this moment the whole styling of bootstrap is included, once the Cloud
redesign is over it will be stripped to only the used components.
2018-08-27 16:52:22 +02:00
4dc11b075a Gulp: watch Pillar styles folder for changes and compile Sass 2018-08-27 15:17:00 +02:00
4696d09fed Corrected rewrite rule for Caminandes 2018-07-06 14:56:40 +02:00
663cf7bf2d Update README with further setup instructions 2018-06-25 18:58:59 +02:00
4e8530478a Remove trailing slash from BLENDER_ID_ENDPOINT 2018-06-22 19:40:44 +02:00
b2d10b5ca7 Expose BLENDER_ID_ENDPOINT in example config 2018-06-22 19:40:10 +02:00
fad1aa75e9 Add virtualenv creation instructions 2018-06-22 16:23:44 +02:00
d9cf2d8631 Include package-lock.json for this project 2018-06-22 16:14:28 +02:00
34cb45d5f1 Fix link to Pillar website in README.md 2018-06-22 16:13:56 +02:00
3c74e79e4a Update README.md featuring Development setup instructions 2018-06-22 16:09:29 +02:00
fb2f245f1e Introducing "./gulp all" all command
It automates running the ./gulp command in all blender-cloud
related repos. Useful when setting up a project for the first time.
2018-06-22 15:39:24 +02:00
8c6fa1e423 Add config_local.example.py for development setup. 2018-06-21 19:08:07 +02:00
b66b6cf445 Reduced verbosity of mongo-backup.sh 2018-06-14 11:57:36 +02:00
b153cae70e Increased WSGI thread count 32 → 64 2018-06-13 10:47:28 +02:00
368e3f6d40 Use high-res image for landing header 2018-05-07 15:25:10 +02:00
e9ec850d90 Styling: tweaks for mobile 2018-04-17 00:28:49 +02:00
280d26801e Reduce scope of h2 styling 2018-04-16 20:36:23 +02:00
6c285c078f Cleanup for landing page 2018-04-16 18:35:16 +02:00
debfc4e356 Layout tweaks for landing page 2018-04-16 17:30:34 +02:00
541663ce0c Skip nodes with no image in landing gallery 2018-04-16 16:36:03 +02:00
1fb044e7c1 Use macro for rendering secondary navigation 2018-04-16 16:24:00 +02:00
4769e2e904 New landing page for Hero project 2018-04-16 14:38:08 +02:00
cf99383b9c Only load clipboard.min.js when authenticated
This is used in the attachments form, which is only available to
authenticated users.
2018-04-03 11:28:13 +02:00
8613ac7244 Merge branch 'master' into production 2018-04-03 11:10:01 +02:00
8c48a61114 Switched attachment rendering to shortcode system
Requires Pillar 3b452d14ce32e1a744fc526a922e1bb60b83ef25 or newer.
2018-04-03 11:02:33 +02:00
0259c5e0ec Add clipboard.min.js to layout
This is needed for the copy button on file asset uploading. Rather than
including it on all pages that could feature the file uploader it's now
loaded globally.
2018-03-29 17:34:22 +02:00
a726fd1fbe Remove time from logs; timestamp is added by Apache anyway. 2018-03-29 11:01:51 +02:00
df71738623 Nodes preview now uses typewatch and csrf_token 2018-03-28 23:38:09 +02:00
6a698daaa0 Remove time from logs; timestamp is added by Apache anyway. 2018-03-27 16:42:43 +02:00
7f538bdaee Replace "Intel" with "Intel Software" logo 2018-03-27 12:12:26 +02:00
5f07c7ce17 Use new hashing of static file names.
Every time the docker image is rebuilt a random hash is chosen.

Requires Pillar d560f89704e3a6f4490df57712525048c469bed2 or newer.
2018-03-23 17:39:04 +01:00
5a42e2dcb8 Flip condition to unindent pretty much all the code
No semantic changes.
2018-03-23 17:24:36 +01:00
d5a54b7cf1 Formatting 2018-03-23 16:37:58 +01:00
7cb4b37ae2 Renamed some docker files to Dockerfile
This makes it simpler to manage by using the default name. It also helps
my editor to recognise the file and highlight it properly.
2018-03-23 12:42:56 +01:00
1fca473257 Moved Apache files into separate subdir 2018-03-23 12:12:39 +01:00
5a6035a494 Change hostname from blender-cloud → cloud.local in docker-compose.yml 2018-03-23 12:11:59 +01:00
98698be7eb Add redirect for waking-the-forest project
Waking the Forest was originally part of the Art Gallery, but was
moved to its own dedicated workshop to increase visibility.
2018-03-19 11:17:45 +01:00
d4f072480c Add convenience redirect from /hero to /p/hero 2018-03-14 14:35:27 +01:00
2d036ee657 Fix rsync of MongoDB backup to Swami
- Forcing IPv4 no longer necessary
- Directory on Swami is determined by rrsync parameters on Swami side
  .ssh/authorized_keys file.
2018-03-09 14:02:39 +01:00
1bb762db6b Document simplified deployment procedure 2018-03-07 15:44:13 +01:00
11743c54e2 Fixed quarterly pricing + all pricing layout tweak 2018-03-07 15:12:36 +01:00
29d1d02bfd Prevent error when there are no Mongo backups to remove yet. 2018-02-22 10:01:59 +01:00
f7e5db2174 Copy files from work directory instead of $DOCKER_DEPLOYDIR
Those files aren't run from inside docker, so it's unnecessary to rebuild
the docker image when we want to deploy new versions.
2018-02-21 11:30:48 +01:00
2141aed06c Added script to run on server for nightly MongoDB backups
Forced to use IPv4 due to IPv6 connectivity issues with Swami.
2018-02-21 11:30:48 +01:00
6b56df9e9c Welcome page additions
New projects: Hero, Spring, and Minecraft Animation Workshop!
2018-02-20 18:56:18 +01:00
5a4519659a Welcome page: typo in Quarterly
Thanks to Michael Glass @WebSmith for reporting!
2018-02-14 12:06:20 +01:00
c3ddc831aa Stricter XSendFilePath in Apache config 2018-02-14 11:07:47 +01:00
1a63b51c48 Ignore files created by running celery-beat outside docker 2018-02-13 12:33:24 +01:00
484ac34c50 T53983: Explicitly version the picohttp docker image 2018-02-13 12:32:19 +01:00
908360eb1c Scripts for easier deployment without leaving ./deploy/
You can choose between build-quick.sh (which only does 4_run/build.sh) and
build-all.sh (which does a full Docker image rebuild).
2018-02-13 11:46:29 +01:00
3bf1c3ea1b About: introducing team profiles
CSS should be refactored, probably in the page itself.
2018-02-12 00:32:12 +01:00
fc58fbef5b Resize and cleanup team profile pictures 2018-02-12 00:29:22 +01:00
9e961580d3 Fix bad URL to store 2018-02-06 10:59:23 +01:00
87cf5a9844 Revert "Add https://cloud*/* as virtual host to haproxy config"
This reverts commit 3be926b9b3e9eb57851bc5fa39440e3924a84b19.
2018-02-02 13:00:57 +01:00
5a3a7a3883 LetsEncrypt fixes
- Changed virtual host weight for the letsencrypt docker so that it is
  higher than any other weight
- Copy the renewal script to the server (previously it was available
  to the host at /data/git/blender-cloud/…, but no longer.
2018-02-02 12:39:07 +01:00
3be926b9b3 Add https://cloud*/* as virtual host to haproxy config
This allows testing on https://cloud3/ for example, without having to
edit the docker-compose.yml file on the cloud3 server.
2018-02-02 12:20:39 +01:00
ff5af22771 Added missing hostname in README 2018-02-02 12:20:00 +01:00
6f73222dcd Add the most-changing files as last step for faster Docker rebuilds. 2018-02-02 12:09:16 +01:00
1617a119db No more branch check for subprojects
We only need the locations of those subprojects to get their Git URL, and
the state of the work directory doesn't matter.
2018-02-02 12:04:47 +01:00
bef402a6b0 Updated documentation for the new way to deploy Blender Cloud 2018-02-02 12:01:47 +01:00
94ef616593 Placing code + assets directly into Docker image
This radically changes the way we deploy to the production server, as a
Git checkout is no longer required there. All the necessary files are
now inside the docker image. As a result, /data/git should no longer be
mounted as a Docker volume.

- Renamed docker/build.sh → docker/full_rebuild.sh
  This makes it clearer that it performs a full rebuild of the Docker images.
- Full rebuilds should be done on a regular basis to pull in Ubuntu
  security updates.
- Removed rsync_ui.sh, we no longer need it. Other projects can also
  remove their rsync_ui.sh.
- Moved deploy.sh → deploy/2docker.sh and added deploy/2server.sh
2018-02-02 12:01:17 +01:00
ffc4f271e8 Upgrade Docker base image to Ubuntu 17.10 2018-02-01 15:53:33 +01:00
158 changed files with 13780 additions and 2506 deletions

9
.gitignore vendored
View File

@ -3,9 +3,11 @@
.coverage
*.pyc
__pycache__
*.js.map
*.css.map
/cloud/templates/
/cloud/static/assets/css/
/cloud/static/assets/
node_modules/
/config_local.py
@ -18,3 +20,8 @@ node_modules/
/google_app*.json
/docker/2_buildpy/python/
/docker/4_run/wheelhouse/
/docker/4_run/deploy/
/docker/4_run/staging/
/celerybeat-schedule.bak
/celerybeat-schedule.dat
/celerybeat-schedule.dir

142
README.md
View File

@ -3,45 +3,100 @@
Welcome to the [Blender Cloud](https://cloud.blender.org/) code repo!
Blender Cloud runs on the [Pillar](https://pillarframework.org/) framework.
## Quick setup
Set up a node with these commands.
## Development setup
Jumpstart Blender Cloud development with this simple guide.
### System setup
Blender Cloud relies on a number of services in order to run. Check out the [Pillar system setup](
https://pillarframework.org/development/system_setup/#step-by-step-setup) to set this up.
Add `cloud.local` to the `/etc/hosts` file on localhost. This is a development convention.
### Check out the code
Go to the local development directory and check out the following repositories, next to each other.
```
#!/usr/bin/env bash
sudo mkdir -p /data/{git,storage,config,certs}
sudo apt-get update
sudo apt-get -y install python3-pip
pip3 install docker-compose
cd /data/git
cd /home/guest/Developer
git clone git://git.blender.org/pillar-python-sdk.git
git clone git://git.blender.org/pillar.git -b production
git clone git://git.blender.org/attract.git -b production
git clone git://git.blender.org/flamenco.git -b production
git clone git://git.blender.org/blender-cloud.git -b production
git clone https://github.com/armadillica/grafista.git -b production
echo '0 8 * * * root docker exec -d grafista bash manage.sh collect' > /etc/cron.d/grafista
git clone git://git.blender.org/pillar.git
git clone git://git.blender.org/attract.git
git clone git://git.blender.org/flamenco.git
git clone git://git.blender.org/pillar-svnman.git
git clone git://git.blender.org/blender-cloud.git
```
After these commands, run `deploy.sh` to build the static files and deploy
those too (see below).
### Initial setup and configuration
Create a virtualenv for the project and install the requirements. Dependencies are managed via
[Poetry](https://poetry.eustace.io/). Install it using `pip install -U --user poetry`.
```
cd blender-cloud
pip install --user -U poetry
poetry install
```
NOTE: After a dependency changed its own dependencies (say a new library was added as dependency of
Pillar), you need to run `poetry update`. This will take the new dependencies into account and write
them to the `poetry.lock` file.
Build assets and templates for all Blender Cloud dependencies using Gulp.
```
./gulp all
```
Make a copy of the config_local example, which will be further edited as the application is
configured.
```
cp config_local.example.py config_local.py
```
Setup the database with the initial collections and the admin user.
```
poetry run ./manage.py setup setup_db your_email
```
The command will return the following message:
```
Created project <project_id> for user <user_id>
```
Copy the value of `<project_id>` and assign it as value for `MAIN_PROJECT_ID`.
Run the application:
```
poetry run ./manage.py runserver
```
## Deploying to production server
## Development
First of all, add those aliases to the `[alias]` section of your `~/.gitconfig`
When ready to commit, change the remotes to work over SSH. For example:
`git remote set-url origin git@git.blender.org:blender-cloud.git`
For more information, check out [this guide](https://wiki.blender.org/wiki/Tools/Git#Commit_Access).
## Preparing the production branch for deployment
All revisions to deploy to production should be on the `production` branches of all the relevant
repositories.
Make sure you have these aliases in the `[alias]` section of your `~/.gitconfig`:
```
prod = "!git checkout production && git fetch origin production && gitk --all"
ff = "merge --ff-only"
pp = "!git push && if [ -e deploy.sh ]; then ./deploy.sh; fi && git checkout master"
```
The following commands should be executed for each subproject; specifically for
Pillar and Attract:
The following commands should be executed for each (sub)project; specifically for
the current repository, Pillar, Attract, Flamenco, and Pillar-SVNMan:
```
cd $projectdir
@ -52,7 +107,7 @@ git stash # if you still have local stuff.
# pull from master, run unittests, push your changes to master.
git pull
py.test
poetry run py.test
git push
# Switch to production branch, and investigate the situation.
@ -62,37 +117,12 @@ git prod
git ff master
# Run tests again
py.test
poetry run py.test
# Push the production branch and run dummy deploy script.
git pp # pp = "Push to Production"
# The above alias waits for [ENTER] until all deploys are done.
# Let it wait, perform the other commands in another terminal.
# Push the production branch.
git push
```
Now follow the above receipe on the Blender Cloud project as well.
Contrary to the subprojects, `git pp` will actually perform the deploy
for real.
## Deploying to production server
Now you can press `[ENTER]` in the Pillar, Attract, and Flamenco terminals
that were still waiting for it.
After everything is done, your (sub)projects should all be back on
the master branch.
## Updating dependencies via Docker images
To update dependencies that need compiling, you need the `2_build` docker
container. To rebuild the lot, run `docker/build.sh`.
Follow these steps to deploy the new container on production:
1. run `docker/build.sh`
2. `docker push armadillica/blender_cloud`
On the production machine:
1. `docker pull armadillica/blender_cloud`
2. `docker-compose up -d` (from the `/data/git/blender-cloud/docker` directory)
See [deploy/README.md](deploy/README.md).

View File

@ -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
@ -41,6 +43,9 @@ class CloudExtension(PillarExtension):
'EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER': 'https://store.blender.org/api/',
'EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS': 10,
'BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET': 'oos9wah1Zoa0Yau6ahThohleiChephoi',
'NODE_TAGS': ['animation', 'modeling', 'rigging', 'sculpting', 'shading', 'texturing', 'lighting',
'character-pipeline', 'effects', 'video-editing', 'digital-painting', 'production-design',
'walk-through'],
}
def eve_settings(self):
@ -84,6 +89,54 @@ 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
"""
if project.extension_props is None:
# There are no extension_props on this project
return False
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

View File

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

15
cloud/forms.py Normal file
View File

@ -0,0 +1,15 @@
from flask_wtf import FlaskForm
from wtforms import BooleanField, StringField
from wtforms.fields.html5 import URLField
from wtforms.validators import URL
from pillar.web.utils.forms import FileSelectField
class FilmProjectForm(FlaskForm):
video_url = URLField(validators=[URL()])
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')

View File

@ -3,21 +3,32 @@ import json
import logging
import typing
from flask_login import current_user, login_required
import bson
from flask_login import login_required
import flask
from flask import Blueprint, render_template, redirect, session, url_for, abort, flash
import werkzeug.exceptions as wz_exceptions
from flask import Blueprint, render_template, redirect, session, url_for, abort, flash, request
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
from pillar.web.settings import blueprint as blueprint_settings
from pillar.web.nodes.routes import url_for_node
from pillar.web.nodes.custom.comments import render_comments_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 pillar.web.projects.routes import project_navigation_links
from cloud import current_cloud
from cloud.forms import FilmProjectForm
from . import EXTENSION_NAME
blueprint = Blueprint('cloud', __name__)
log = logging.getLogger(__name__)
@ -40,44 +51,6 @@ def _homepage_context() -> dict:
# Get latest blog posts
api = system_util.pillar_api()
latest_posts = Node.all({
'projection': {
'name': 1,
'project': 1,
'node_type': 1,
'picture': 1,
'properties.url': 1,
'properties.content': 1,
'properties.attachments': 1
},
'where': {'node_type': 'post', 'properties.status': 'published'},
'embedded': {'project': 1},
'sort': '-_created',
'max_results': '3'
}, api=api)
# Append picture Files to last_posts
for post in latest_posts._items:
post.picture = get_file(post.picture, api=api)
post.url = url_for_node(node=post)
# Render attachments
try:
post_contents = post['properties']['content']
except KeyError:
log.warning('Blog post %s has no content', post._id)
else:
post['properties']['content'] = pillar.web.nodes.attachments.render_attachments(
post, post_contents)
# Get latest assets added to any project
latest_assets = Node.latest('assets', api=api)
# Append picture Files to latest_assets
for asset in latest_assets._items:
asset.picture = get_file(asset.picture, api=api)
asset.url = url_for_node(node=asset)
# Get latest comments to any node
latest_comments = Node.latest('comments', api=api)
@ -96,6 +69,7 @@ def _homepage_context() -> dict:
'name': 1,
'node_type': 1,
'project': 1,
'parent': 1,
'properties.url': 1,
}},
api=api)
@ -122,23 +96,24 @@ def _homepage_context() -> dict:
main_project = Project.find(current_app.config['MAIN_PROJECT_ID'], api=api)
main_project.picture_header = get_file(main_project.picture_header, api=api)
# Merge latest assets and comments into one activity stream.
def sort_key(item):
return item._created
activity_stream = sorted(latest_assets._items, key=sort_key, reverse=True)
for node in activity_stream:
node.url = url_for_node(node=node)
return dict(
main_project=main_project,
latest_posts=latest_posts._items,
latest_comments=latest_comments._items,
activity_stream=activity_stream,
random_featured=random_featured)
@blueprint.route('/design-system')
def design_system():
"""Display the design system page.
This endpoing is intended for development only, and returns a
rendered template only if the app is running in debug mode.
"""
if not current_app.config['DEBUG']:
abort(404)
return render_template('design_system.html')
@blueprint.route('/login')
def login():
from flask import request
@ -176,6 +151,16 @@ def services():
return render_template('services.html')
@blueprint.route('/learn')
def learn():
return render_template('learn.html')
@blueprint.route('/libraries')
def libraries():
return render_template('libraries.html')
@blueprint.route('/stats')
def stats():
return render_template('stats.html')
@ -226,10 +211,35 @@ def courses():
def open_projects():
@current_app.cache.cached(timeout=3600, unless=current_user_is_authenticated)
def render_page():
projects = get_projects('film')
api = system_util.pillar_api()
projects = Project.all({
'where': {
'category': 'film',
'is_private': False
},
'sort': '-_created',
}, api=api)
for project in projects._items:
# Attach poster file (ensure the extension_props.cloud.poster
# attributes exists)
try:
# If the attribute exists, but is None, continue
if not project['extension_props'][EXTENSION_NAME]['poster']:
continue
# Fetch the file and embed it in the document
project.extension_props.cloud.poster = get_file(
project.extension_props.cloud.poster, api=api)
# Add convenience attribute that specifies the presence of the
# poster file
project.has_poster = True
# If there was a key error because one of the nested attributes is
# missing,
except KeyError:
continue
return render_template(
'projects_index_collection.html',
title='open-projects',
'films.html',
title='films',
projects=projects._items,
api=system_util.pillar_api())
@ -268,12 +278,13 @@ def get_random_featured_nodes() -> typing.List[dict]:
'summary': True,
'picture_square': True}},
{'$unwind': {'path': '$nodes_featured'}},
{'$sample': {'size': 3}},
{'$sample': {'size': 6}},
{'$lookup': {'from': 'nodes',
'localField': 'nodes_featured',
'foreignField': '_id',
'as': 'node'}},
{'$unwind': {'path': '$node'}},
{'$match': {'node._deleted': {'$ne': True}}},
{'$project': {'url': True,
'name': True,
'summary': True,
@ -283,7 +294,11 @@ def get_random_featured_nodes() -> typing.List[dict]:
'node.permissions': True,
'node.picture': True,
'node.properties.content_type': True,
'node.properties.url': True}},
'node.properties.duration_seconds': True,
'node.properties.url': True,
'node._created': True,
}
},
])
featured_node_documents = []
@ -292,11 +307,10 @@ def get_random_featured_nodes() -> typing.List[dict]:
# Turn the project-with-node doc into a node-with-project doc.
node_document = node_info.pop('node')
node_document['project'] = node_info
node_document['_id'] = str(node_document['_id'])
node = Node(node_document)
node.picture = get_file(node.picture, api=api)
node.url = url_for_node(node=node)
node.project.url = url_for('projects.view', project_url=node.project.url)
node.project.picture_square = get_file(node.project.picture_square, api=api)
featured_node_documents.append(node)
@ -398,6 +412,13 @@ def privacy():
return render_template('privacy.html')
@blueprint.route('/production')
def production():
return render_template(
'production.html',
title='production')
@blueprint.route('/emails/welcome.send')
@login_required
def emails_welcome_send():
@ -423,24 +444,212 @@ def emails_welcome_txt():
return flask.Response(txt, content_type='text/plain; charset=utf-8')
@blueprint.route('/nodes/<string(length=24):node_id>/comments')
def comments_for_node(node_id):
"""Overrides the default render_comments_for_node.
@blueprint.route('/p/<project_url>')
def project_landing(project_url):
"""Override Pillar project_view endpoint completely.
This is done in order to extend can_post_comments by requiring the
subscriber capability.
The first part of the function is identical to the one in Pillar, but the
second part (starting with 'Load custom project properties') extends the
behaviour to support film project landing pages.
"""
template_name = None
if request.args.get('format') == 'jstree':
log.warning('projects.view(%r) endpoint called with format=jstree, '
'redirecting to proper endpoint. URL is %s; referrer is %s',
project_url, request.url, request.referrer)
return redirect(url_for('projects.jstree', project_url=project_url))
api = system_util.pillar_api()
project = find_project_or_404(project_url,
embedded={'header_node': 1},
api=api)
node = Node.find(node_id, api=api)
project = Project({'_id': node.project})
can_post_comments = project.node_type_has_method('comment', 'POST', api=api)
can_comment_override = flask.request.args.get('can_comment', 'True') == 'True'
can_post_comments = can_post_comments and can_comment_override and current_user.has_cap(
'subscriber')
# Load the header video file, if there is any.
header_video_file = None
header_video_node = None
if project.header_node and project.header_node.node_type == 'asset' and \
project.header_node.properties.content_type == 'video':
header_video_node = project.header_node
header_video_file = get_file(project.header_node.properties.file)
header_video_node.picture = get_file(header_video_node.picture)
return render_comments_for_node(node_id, can_post_comments=can_post_comments)
extra_context = {
'header_video_file': header_video_file,
'header_video_node': header_video_node}
# Load custom project properties. If the project has a 'cloud' extension prop,
# render it using the projects/landing.html template and try to attach a
# number of additional attributes (pages, images, etc.).
if 'extension_props' in project and EXTENSION_NAME in project['extension_props']:
extension_props = project['extension_props'][EXTENSION_NAME]
extension_props['logo'] = get_file(extension_props['logo'])
pages = Node.all({
'where': {
'project': project._id,
'node_type': 'page',
'_deleted': {'$ne': True}},
'projection': {'name': 1}}, api=api)
extra_context.update({'pages': pages._items})
template_name = 'projects/landing.html'
return render_project(project, api,
extra_context=extra_context,
template_name=template_name)
@blueprint.route('/p/<project_url>/browse')
@project_view()
def project_browse(project: pillarsdk.Project):
"""Project view displaying all top-level nodes.
We render a regular project view, but we introduce an additional template
variable: browse. By doing that we prevent the regular project view
from loading and fetch via AJAX a "group" node-like view instead (see
project_browse_view_nodes).
"""
return render_template(
'projects/view.html',
api=system_util.pillar_api(),
project=project,
node=None,
show_project=True,
browse=True,
og_picture=None,
navigation_links=project_navigation_links(project, system_util.pillar_api()),
extension_sidebar_links=current_app.extension_sidebar_links(project))
@blueprint.route('/p/<project_url>/browse/nodes')
@project_view()
def project_browse_view_nodes(project: pillarsdk.Project):
"""Display top-level nodes for a Project.
This view is always meant to be served embedded, as part of project_browse.
"""
api = system_util.pillar_api()
# Get top level nodes
projection = {
'project': 1,
'name': 1,
'picture': 1,
'node_type': 1,
'properties.order': 1,
'properties.status': 1,
'user': 1,
'properties.content_type': 1,
'permissions.world': 1}
where = {
'project': project['_id'],
'parent': {'$exists': False},
'properties.status': 'published',
'_deleted': {'$ne': True},
'node_type': {'$in': ['group', 'asset']},
}
try:
nodes = Node.all({
'projection': projection,
'where': where,
'sort': [('properties.order', 1), ('name', 1)]}, api=api)
except pillarsdk.exceptions.ForbiddenAccess:
return render_template('errors/403_embed.html')
nodes = nodes._items
for child in nodes:
child.picture = get_file(child.picture, api=api)
return render_template(
'projects/browse_embed.html',
nodes=nodes)
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.category
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]
category = cloud_props['category']
if category != 'film':
log.error('No interface available to edit %s projects, yet' % category)
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
if form_field.data == '':
form_field.data = None
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):

54
cloud/setup.py Normal file
View File

@ -0,0 +1,54 @@
"""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 = {
'category': '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
'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)

3
cloud/tagged/__init__.py Normal file
View File

@ -0,0 +1,3 @@
"""Routes for fetching tagged assets."""

16
cloud/tagged/routes.py Normal file
View File

@ -0,0 +1,16 @@
import logging
import datetime
import functools
from flask import Blueprint, jsonify
blueprint = Blueprint('cloud.tagged', __name__, url_prefix='/tagged')
log = logging.getLogger(__name__)
@blueprint.route('/')
def index():
"""Return all tagged assets as JSON, grouped by tag."""

View File

@ -7,8 +7,7 @@ import json
import logging
import typing
from flask_login import request
from flask import Blueprint
from flask import Blueprint, request
import werkzeug.exceptions as wz_exceptions
from pillar import current_app
@ -102,8 +101,8 @@ def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]:
{'auth.provider': 'blender-id', 'auth.user_id': bid_str},
{'email': {'$in': [wh_payload['old_email'], email]}},
]}
db_users = users_coll.find(query)
user_count = db_users.count()
db_users = list(users_coll.find(query))
user_count = len(db_users)
if user_count > 1:
# Now we have to pay the price for finding users in one query; we
# have to prioritise them and return the one we think is most reliable.
@ -118,6 +117,10 @@ def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]:
my_log.debug('found user %s', db_user['email'])
return db_user
if wh_payload.get('date_deletion_requested'):
my_log.info('Received update for a deleted user %s, not creating', bid_str)
return None
# Pretend to create the user, so that we can inspect the resulting
# capabilities. This is more future-proof than looking at the list
# of roles in the webhook payload.
@ -165,6 +168,7 @@ def user_modified():
'old_email': 'old@example.com',
'full_name': 'Harry',
'email': 'new@example'com,
'avatar_changed': True,
'roles': ['role1', 'role2', ]}
"""
my_log = log.getChild('user_modified')
@ -181,6 +185,10 @@ def user_modified():
my_log.info('Received update for unknown user %r', payload['old_email'])
return '', 204
if payload.get('date_deletion_requested'):
delete_user(db_user, payload)
return '', 204
# Use direct database updates to change the email and full name.
# Also updates the db_user dict so that local_user below will have
# the updated information.
@ -200,6 +208,11 @@ def user_modified():
updates['full_name'] = db_user['username']
db_user['full_name'] = updates['full_name']
if payload.get('avatar_changed'):
import pillar.celery.avatar
my_log.info('User %s changed avatar, scheduling download', db_user['_id'])
pillar.celery.avatar.sync_avatar_for_user.delay(str(db_user['_id']))
if updates:
users_coll = current_app.db('users')
update_res = users_coll.update_one({'_id': db_user['_id']},
@ -214,3 +227,37 @@ def user_modified():
subscription.do_update_subscription(local_user, payload)
return '', 204
def delete_user(db_user, payload):
"""Handle deletion request coming from BID."""
my_log = log.getChild('delete_user')
date_deletion_requested = payload['date_deletion_requested']
bid_str = str(payload['id'])
local_id = db_user['_id']
my_log.info(
'User %s with BID=%s requested deletion on %s, soft-deleting the user',
local_id, bid_str, date_deletion_requested,
)
# Delete all session tokens linked to this user
token_coll = current_app.db('tokens')
delete_res = token_coll.delete_many({'user': local_id})
my_log.info('Deleted %s session tokens of user %s', delete_res.deleted_count, local_id)
# Soft-delete the user and clear their PII
users_coll = current_app.db('users')
updates = {
'_deleted': True,
'email': None,
'full_name': None,
'username': None,
'auth': [],
}
update_res = users_coll.update_one({'_id': local_id}, {'$set': updates})
if update_res.matched_count != 1:
my_log.error(
'Soft-deleted %s users %s with BID=%s',
update_res.matched_count, local_id, bid_str,
)
else:
my_log.warning('Soft-deleted user %s with BID=%s', local_id, bid_str)

43
config_local.example.py Normal file
View File

@ -0,0 +1,43 @@
import os
DEBUG = True
BLENDER_ID_ENDPOINT = 'http://id.local:8000/'
SERVER_NAME = 'cloud.local:5001'
SCHEME = 'http'
PILLAR_SERVER_ENDPOINT = f'{SCHEME}://{SERVER_NAME}/api/'
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true'
os.environ['PILLAR_MONGO_DBNAME'] = 'cloud'
os.environ['PILLAR_MONGO_PORT'] = '27017'
os.environ['PILLAR_MONGO_HOST'] = 'mongo'
os.environ['PILLAR_SERVER_ENDPOINT'] = PILLAR_SERVER_ENDPOINT
SECRET_KEY = '##DEFINE##'
OAUTH_CREDENTIALS = {
'blender-id': {
'id': 'CLOUD-OF-SNOWFLAKES-42',
'secret': '##DEFINE##',
}
}
MAIN_PROJECT_ID = '##DEFINE##'
URLER_SERVICE_AUTH_TOKEN = '##DEFINE##'
ZENCODER_API_KEY = '##DEFINE##'
ZENCODER_NOTIFICATIONS_SECRET = '##DEFINE##'
ZENCODER_NOTIFICATIONS_URL = 'http://zencoderfetcher/'
# Special announcement on top of every page, for non-subscribers.
# category: 'string', can be 'info', 'warning', 'danger', or 'success'.
# message: 'string', any text, it gets markdowned.
# icon: 'string', any icon in font-pillar. e.g. 'pi-heart-filled'
UI_ANNOUNCEMENT_NON_SUBSCRIBERS = {
'category': 'danger',
'message': 'Spring will swing away the gray clouds, until then, '
'[take cover under Blender Cloud](https://cloud.blender.org)!',
'icon': 'pi-heart-filled',
}

164
deploy.sh
View File

@ -1,164 +0,0 @@
#!/bin/bash -e
case $1 in
cloud*)
DEPLOYHOST="$1"
;;
*)
echo "Use $0 cloud{nr}|cloud.blender.org" >&2
exit 1
esac
echo -n "Deploying to ${DEPLOYHOST}... "
if ! ping ${DEPLOYHOST} -q -c 1 -W 2 >/dev/null; then
echo "host ${DEPLOYHOST} cannot be pinged, refusing to deploy." >&2
exit 2
fi
echo "press [ENTER] to continue, Ctrl+C to abort."
read dummy
# Deploys the current production branch to the production machine.
PROJECT_NAME="blender-cloud"
DOCKER_NAME="blender_cloud"
CELERY_WORKER_DOCKER_NAME="celery_worker"
CELERY_BEAT_DOCKER_NAME="celery_beat"
REMOTE_ROOT="/data/git/${PROJECT_NAME}"
SSH="ssh -o ClearAllForwardings=yes -o PermitLocalCommand=no ${DEPLOYHOST}"
# macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then
command -v greadlink 2>/dev/null 2>&1 || { echo >&2 "Install greadlink using brew."; exit 1; }
readlink='greadlink'
else
readlink='readlink'
fi
ROOT="$(dirname "$($readlink -f "$0")")"
cd ${ROOT}
# Check that we're on production branch.
if [ $(git rev-parse --abbrev-ref HEAD) != "production" ]; then
echo "You are NOT on the production branch, refusing to deploy." >&2
exit 1
fi
# Check that production branch has been pushed.
if [ -n "$(git log origin/production..production --oneline)" ]; then
echo "WARNING: not all changes to the production branch have been pushed."
echo "Press [ENTER] to continue deploying current origin/production, CTRL+C to abort."
read dummy
fi
function find_module()
{
MODULE_NAME=$1
MODULE_DIR=$(python <<EOT
from __future__ import print_function
import os.path
try:
import ${MODULE_NAME}
except ImportError:
raise SystemExit('${MODULE_NAME} not found on Python path. Are you in the correct venv?')
print(os.path.dirname(os.path.dirname(${MODULE_NAME}.__file__)))
EOT
)
if [ $(git -C $MODULE_DIR rev-parse --abbrev-ref HEAD) != "production" ]; then
echo "${MODULE_NAME}: ($MODULE_DIR) NOT on the production branch, refusing to deploy." >&2
exit 1
fi
echo $MODULE_DIR
}
# Find our modules
PILLAR_DIR=$(find_module pillar)
ATTRACT_DIR=$(find_module attract)
FLAMENCO_DIR=$(find_module flamenco)
SVNMAN_DIR=$(find_module svnman)
echo "Pillar : $PILLAR_DIR"
echo "Attract : $ATTRACT_DIR"
echo "Flamenco: $FLAMENCO_DIR"
echo "SVNMan : $SVNMAN_DIR"
if [ -z "$PILLAR_DIR" -o -z "$ATTRACT_DIR" -o -z "$FLAMENCO_DIR" -o -z "$SVNMAN_DIR" ];
then
exit 1
fi
# SSH to cloud to pull all files in
function git_pull() {
PROJECT_NAME="$1"
BRANCH="$2"
REMOTE_ROOT="/data/git/${PROJECT_NAME}"
echo "==================================================================="
echo "UPDATING FILES ON ${PROJECT_NAME}"
${SSH} git -C ${REMOTE_ROOT} fetch origin ${BRANCH}
${SSH} git -C ${REMOTE_ROOT} log origin/${BRANCH}..${BRANCH} --oneline
${SSH} git -C ${REMOTE_ROOT} merge --ff-only origin/${BRANCH}
}
git_pull pillar-python-sdk master
git_pull pillar production
git_pull attract production
git_pull flamenco production
git_pull pillar-svnman production
git_pull blender-cloud production
# Update the virtualenv
#${SSH} -t docker exec ${DOCKER_NAME} /data/venv/bin/pip install -U -r ${REMOTE_ROOT}/requirements.txt --exists-action w
# RSync the world
$ATTRACT_DIR/rsync_ui.sh ${DEPLOYHOST}
$FLAMENCO_DIR/rsync_ui.sh ${DEPLOYHOST}
$SVNMAN_DIR/rsync_ui.sh ${DEPLOYHOST}
./rsync_ui.sh ${DEPLOYHOST}
# Notify Sentry of this new deploy.
# See https://sentry.io/blender-institute/blender-cloud/settings/release-tracking/
# and https://docs.sentry.io/api/releases/post-organization-releases/
# and https://sentry.io/api/
echo
echo "==================================================================="
REVISION=$(date +'%Y%m%d.%H%M%S.%Z')
echo "Notifying Sentry of this new deploy of revision ${REVISION}."
SENTRY_RELEASE_URL="$(${SSH} python3 -c "\"import sys; sys.path.append('${REMOTE_ROOT}'); import config_local; print(config_local.SENTRY_RELEASE_URL)\"")"
curl -vs "$SENTRY_RELEASE_URL" -XPOST -H 'Content-Type: application/json' -d "{\"version\": \"$REVISION\"}" | json_pp
echo
# Wait for [ENTER] to restart the server
echo
echo "==================================================================="
echo "NOTE: If you want to edit config_local.py on the server, do so now."
echo "NOTE: Press [ENTER] to continue and restart the server process."
read dummy
echo "Gracefully restarting server process"
${SSH} docker exec ${DOCKER_NAME} apache2ctl graceful
echo "Server process restarted"
echo
echo "==================================================================="
echo "Restarting Celery worker."
${SSH} docker restart ${CELERY_WORKER_DOCKER_NAME}
echo "Celery worker docker restarted"
echo "Restarting Celery beat."
${SSH} docker restart ${CELERY_BEAT_DOCKER_NAME}
echo "Celery beat docker restarted"
echo
echo "==================================================================="
echo "Clearing front page from Redis cache."
${SSH} docker exec redis redis-cli DEL pwview//
echo
echo "==================================================================="
echo "Deploy of ${PROJECT_NAME} is done."
echo "==================================================================="

130
deploy/2docker.sh Executable file
View File

@ -0,0 +1,130 @@
#!/bin/bash -e
STAGING_BRANCH=${STAGING_BRANCH:-production}
# macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then
command -v greadlink 2>/dev/null 2>&1 || { echo >&2 "Install greadlink using brew."; exit 1; }
readlink='greadlink'
else
readlink='readlink'
fi
ROOT="$(dirname "$(dirname "$($readlink -f "$0")")")"
STAGINGDIR="$ROOT/docker/4_run/staging"
PROJECT_NAME="$(basename $ROOT)"
if [ -e $STAGINGDIR ]; then
echo "$STAGINGDIR already exists, press [ENTER] to destroy and re-install, Ctrl+C to abort."
read dummy
rm -rf $STAGINGDIR
else
echo -n "Installing into $STAGINGDIR"
echo "press [ENTER] to continue, Ctrl+C to abort."
read dummy
fi
cd ${ROOT}
mkdir -p $STAGINGDIR
REMOTE_ROOT="$STAGINGDIR/$PROJECT_NAME"
if [ -z "$SKIP_BRANCH_CHECK" ]; then
# Check that we're on production branch.
if [ $(git rev-parse --abbrev-ref HEAD) != "$STAGING_BRANCH" ]; then
echo "You are NOT on the $STAGING_BRANCH branch, refusing to stage." >&2
exit 1
fi
# Check that production branch has been pushed.
if [ -n "$(git log origin/$STAGING_BRANCH..$STAGING_BRANCH --oneline)" ]; then
echo "WARNING: not all changes to the $STAGING_BRANCH branch have been pushed."
echo "Press [ENTER] to continue staging current origin/$STAGING_BRANCH, CTRL+C to abort."
read dummy
fi
fi
function find_module()
{
MODULE_NAME=$1
MODULE_DIR=$(python <<EOT
from __future__ import print_function
import os.path
try:
import ${MODULE_NAME}
except ImportError:
raise SystemExit('${MODULE_NAME} not found on Python path. Are you in the correct venv?')
print(os.path.dirname(os.path.dirname(${MODULE_NAME}.__file__)))
EOT
)
echo $MODULE_DIR
}
# Find our modules
echo "==================================================================="
echo "LOCAL MODULE LOCATIONS"
PILLAR_DIR=$(find_module pillar)
ATTRACT_DIR=$(find_module attract)
FLAMENCO_DIR=$(find_module flamenco)
SVNMAN_DIR=$(find_module svnman)
SDK_DIR=$(find_module pillarsdk)
echo "Pillar : $PILLAR_DIR"
echo "Attract : $ATTRACT_DIR"
echo "Flamenco: $FLAMENCO_DIR"
echo "SVNMan : $SVNMAN_DIR"
echo "SDK : $SDK_DIR"
if [ -z "$PILLAR_DIR" -o -z "$ATTRACT_DIR" -o -z "$FLAMENCO_DIR" -o -z "$SVNMAN_DIR" -o -z "$SDK_DIR" ];
then
exit 1
fi
function git_clone() {
PROJECT_NAME="$1"
BRANCH="$2"
LOCAL_ROOT="$3"
echo "==================================================================="
echo "CLONING REPO ON $PROJECT_NAME @$BRANCH"
URL=$(git -C $LOCAL_ROOT remote get-url origin)
git -C $STAGINGDIR clone --depth 1 --branch $BRANCH $URL $PROJECT_NAME
}
if [ "$STAGING_BRANCH" == "production" ]; then
SDK_STAGING_BRANCH=master # SDK doesn't have a production branch
else
SDK_STAGING_BRANCH=$STAGING_BRANCH
fi
git_clone pillar-python-sdk $SDK_STAGING_BRANCH $SDK_DIR
git_clone pillar $STAGING_BRANCH $PILLAR_DIR
git_clone attract $STAGING_BRANCH $ATTRACT_DIR
git_clone flamenco $STAGING_BRANCH $FLAMENCO_DIR
git_clone pillar-svnman $STAGING_BRANCH $SVNMAN_DIR
git_clone blender-cloud $STAGING_BRANCH $ROOT
# Gulp everywhere
GULP=$ROOT/node_modules/.bin/gulp
if [ ! -e $GULP -o gulpfile.js -nt $GULP ]; then
npm install
touch $GULP # installer doesn't always touch this after a build, so we do.
fi
# List of projects
PROJECTS="pillar attract flamenco pillar-svnman blender-cloud"
# Run ./gulp for every project
for PROJECT in $PROJECTS; do
pushd $STAGINGDIR/$PROJECT; ./gulp --production; popd;
done
# Remove node_modules (only after all projects with interdependencies have been built)
for PROJECT in $PROJECTS; do
pushd $STAGINGDIR/$PROJECT; rm -r node_modules; popd;
done
echo
echo "==================================================================="
echo "Staging of ${PROJECT_NAME} is ready for dockerisation."
echo "==================================================================="

80
deploy/2server.sh Executable file
View File

@ -0,0 +1,80 @@
#!/bin/bash -e
# macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then
command -v greadlink 2>/dev/null 2>&1 || { echo >&2 "Install greadlink using brew."; exit 1; }
readlink='greadlink'
else
readlink='readlink'
fi
ROOT="$(dirname "$(dirname "$($readlink -f "$0")")")"
PROJECT_NAME="$(basename $ROOT)"
DOCKER_IMAGE="armadillica/blender_cloud:latest"
REMOTE_SECRET_CONFIG_DIR="/data/config"
REMOTE_DOCKER_COMPOSE_DIR="/root/docker"
#################################################################################
case $1 in
cloud*)
DEPLOYHOST="$1"
;;
*)
echo "Use $0 cloud{nr}|cloud.blender.org" >&2
exit 1
esac
SSH_OPTS="-o ClearAllForwardings=yes -o PermitLocalCommand=no"
SSH="ssh $SSH_OPTS $DEPLOYHOST"
SCP="scp $SSH_OPTS"
echo -n "Deploying to $DEPLOYHOST"
if ! ping $DEPLOYHOST -q -c 1 -W 2 >/dev/null; then
echo "host $DEPLOYHOST cannot be pinged, refusing to deploy." >&2
exit 2
fi
cat <<EOT
[ping OK]
Make sure that you have pushed the $DOCKER_IMAGE
docker image to Docker Hub.
press [ENTER] to continue, Ctrl+C to abort.
EOT
read dummy
#################################################################################
echo "==================================================================="
echo "Bringing remote Docker up to date…"
$SSH mkdir -p $REMOTE_DOCKER_COMPOSE_DIR
$SCP \
$ROOT/docker/{docker-compose.yml,renew-letsencrypt.sh,mongo-backup.{cron,sh}} \
$DEPLOYHOST:$REMOTE_DOCKER_COMPOSE_DIR
$SSH -T <<EOT
set -e
cd $REMOTE_DOCKER_COMPOSE_DIR
docker pull $DOCKER_IMAGE
docker-compose up -d
echo
echo "==================================================================="
echo "Clearing front page from Redis cache."
docker exec redis redis-cli DEL pwview//
EOT
# Notify Sentry of this new deploy.
# See https://sentry.io/blender-institute/blender-cloud/settings/release-tracking/
# and https://docs.sentry.io/api/releases/post-organization-releases/
# and https://sentry.io/api/
echo
echo "==================================================================="
REVISION=$(date +'%Y%m%d.%H%M%S.%Z')
echo "Notifying Sentry of this new deploy of revision $REVISION."
SENTRY_RELEASE_URL="$($SSH env PYTHONPATH="$REMOTE_SECRET_CONFIG_DIR" python3 -c "\"import config_secrets; print(config_secrets.SENTRY_RELEASE_URL)\"")"
curl -s "$SENTRY_RELEASE_URL" -XPOST -H 'Content-Type: application/json' -d "{\"version\": \"$REVISION\"}" | json_pp
echo
echo
echo "==================================================================="
echo "Deploy to $DEPLOYHOST done."
echo "==================================================================="

39
deploy/README.md Normal file
View File

@ -0,0 +1,39 @@
# Deploying to Production
```
workon blender-cloud # activate your virtualenv
cd $projectdir/deploy
./full-pull.sh
```
## The Details
Deployment consists of a few steps:
1. Populate a staging directory with the files from the production branches of the various projects.
2. Create Docker images.
3. Push the docker images to Docker Hub.
4. Pull the docker images on the production server and rebuild+restart the containers.
The scripts involved are:
- `2docker.sh`: performs step 1. above.
- `build-{xxx}.sh`: performs steps 2. and 3. above.
- `2server.sh`: performs step 4. above.
The `full-{xxx}.sh` scripts perform all the steps, and call into `build-{xxx}.sh`.
For `xxx` there are:
- `all`: Rebuild all Docker images from scratch. This is good for getting the latest updates to the
base image.
- `pull`: Pull the base and intermediate images from Docker Hub so that they are the same as the
last time someone pushed to production, then rebuilds the final Docker image.
- `quick`: Just rebuild the final Docker image. Only use this if the last time a deployment to
the production server was done was by you, on the machine you're working on now.
## Hacking Stuff
To deploy another branch than `production`, do `export STAGING_BRANCH=otherbranch` before starting
the above commands.

43
deploy/build-all.sh Executable file
View File

@ -0,0 +1,43 @@
#!/bin/bash -e
# macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then
command -v greadlink 2>/dev/null 2>&1 || { echo >&2 "Install greadlink using brew."; exit 1; }
readlink='greadlink'
else
readlink='readlink'
fi
ROOT="$(dirname "$(dirname "$($readlink -f "$0")")")"
case "$(basename "$0")" in
build-pull.sh)
docker pull armadillica/pillar_py:3.6
docker pull armadillica/pillar_wheelbuilder:latest
pushd "$ROOT/docker/3_buildwheels"
./build.sh
popd
pushd "$ROOT/docker/4_run"
./build.sh
;;
build-quick.sh)
pushd "$ROOT/docker/4_run"
./build.sh
;;
build-all.sh)
pushd "$ROOT/docker"
./full_rebuild.sh
;;
*)
echo "Unknown script $0, aborting" >&2
exit 1
esac
popd
echo
echo "Press [ENTER] to push the new Docker images."
read dummy
docker push armadillica/pillar_py:3.6
docker push armadillica/pillar_wheelbuilder:latest
docker push armadillica/blender_cloud:latest
echo
echo "Build is done, ready to update the server."

1
deploy/build-pull.sh Symbolic link
View File

@ -0,0 +1 @@
build-quick.sh

1
deploy/build-quick.sh Symbolic link
View File

@ -0,0 +1 @@
build-all.sh

9
deploy/full-all.sh Executable file
View File

@ -0,0 +1,9 @@
#!/bin/bash
set -e
NAME="$(basename "$0")"
./2docker.sh
./${NAME/full-/build-}
./2server.sh cloud2

1
deploy/full-pull.sh Symbolic link
View File

@ -0,0 +1 @@
full-all.sh

1
deploy/full-quick.sh Symbolic link
View File

@ -0,0 +1 @@
full-all.sh

10
docker/1_base/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM ubuntu:18.04
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install \
-qyy -o APT::Install-Recommends=false -o APT::Install-Suggests=false \
tzdata openssl ca-certificates locales; \
locale-gen en_US.UTF-8 en_GB.UTF-8 nl_NL.UTF-8
ENV LANG en_US.UTF-8

View File

@ -1,6 +0,0 @@
FROM ubuntu:16.10
MAINTAINER Francesco Siddi <francesco@blender.org>
RUN apt-get update && apt-get install -qyy \
-o APT::Install-Recommends=false -o APT::Install-Suggests=false \
openssl ca-certificates

View File

@ -1,4 +1,4 @@
#!/usr/bin/env bash
# Uses --no-cache to always get the latest upstream (security) upgrades.
exec docker build --no-cache "$@" -t pillar_base -f base.docker .
exec docker build --no-cache "$@" -t pillar_base .

View File

@ -1 +0,0 @@
1325134dd525b4a2c3272a1a0214dd54 Python-3.6.4.tar.xz

View File

@ -0,0 +1 @@
c3f30a0aff425dda77d19e02f420d6ba Python-3.6.6.tar.xz

View File

@ -34,6 +34,9 @@ make -j8 install
# Make sure we can run Python
ldconfig
# Upgrade pip
/opt/python/bin/python3 -m pip install -U pip
# Build mod-wsgi-py3 for Python 3.6
cd /dpkg/mod-wsgi-*
./configure --with-python=/opt/python/bin/python3

View File

@ -1,23 +1,23 @@
FROM pillar_base
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
RUN sed -i 's/^# deb-src/deb-src/' /etc/apt/sources.list && \
apt-get update && \
apt-get install -qy \
DEBIAN_FRONTEND=noninteractive apt-get install -qy \
build-essential \
apache2-dev \
checkinstall \
curl
RUN apt-get build-dep -y python3.5
RUN apt-get build-dep -y python3.6
ADD Python-3.6.4.tar.xz.md5 /Python-3.6.4.tar.xz.md5
ADD Python-3.6.6.tar.xz.md5 /Python-3.6.6.tar.xz.md5
# Install Python sources
RUN curl -O https://www.python.org/ftp/python/3.6.4/Python-3.6.4.tar.xz && \
md5sum -c Python-3.6.4.tar.xz.md5 && \
tar xf Python-3.6.4.tar.xz && \
rm -v Python-3.6.4.tar.xz
RUN curl -O https://www.python.org/ftp/python/3.6.6/Python-3.6.6.tar.xz && \
md5sum -c Python-3.6.6.tar.xz.md5 && \
tar xf Python-3.6.6.tar.xz && \
rm -v Python-3.6.6.tar.xz
# Install mod-wsgi sources
RUN mkdir -p /dpkg && cd /dpkg && apt-get source libapache2-mod-wsgi-py3
@ -32,4 +32,4 @@ RUN echo /opt/python/lib > /etc/ld.so.conf.d/python.conf
RUN ldconfig
ENV PATH=/opt/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
ENV PYTHONSOURCE=/Python-3.6.4
ENV PYTHONSOURCE=/Python-3.6.6

View File

@ -1,5 +1,5 @@
FROM pillar_base
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
ADD python /opt/python
@ -10,5 +10,4 @@ RUN echo Python is installed in /opt/python/ > README.python
ENV PATH=/opt/python/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
RUN cd /opt/python/bin && \
ln -s python3 python && \
ln -s pip3 pip
ln -s python3 python

View File

@ -1,7 +1,9 @@
FROM armadillica/pillar_py:3.6
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
RUN apt-get update && apt-get install -qy \
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install -qy \
git \
build-essential \
checkinstall \

View File

@ -1,5 +1,7 @@
#!/usr/bin/env bash
DOCKER_IMAGE_NAME=armadillica/pillar_wheelbuilder
set -e
# macOS does not support readlink -f, so we use greadlink instead
@ -21,25 +23,40 @@ fi
echo "Wheelhouse is $WHEELHOUSE"
mkdir -p "$WHEELHOUSE"
rm -f "$WHEELHOUSE"/*
docker build -t pillar_wheelbuilder -f build.docker .
docker build -t $DOCKER_IMAGE_NAME:latest .
GID=$(id -g)
docker run --rm -i \
-v "$WHEELHOUSE:/data/wheelhouse" \
-v "$TOPDEVDIR:/data/topdev" \
pillar_wheelbuilder <<EOT
$DOCKER_IMAGE_NAME <<EOT
set -e
set -x
# Globally upgrade Pip, so that we can get a compatible version of the cryptography package.
# See https://github.com/pyca/cryptography/issues/5771
pip3 install --upgrade pip setuptools wheel
# Pin poetry to 1.0, as more recent version to not support nested filesystem package
# dependencies.
pip3 install wheel poetry==1.0 cryptography==2.7
# Build wheels for all dependencies.
cd /data/topdev/blender-cloud
pip3 install wheel
pip3 wheel --wheel-dir=/data/wheelhouse -r requirements.txt
chown -R $UID:$GID /data/wheelhouse
# Install the dependencies so that we can get a full freeze.
pip3 install --no-index --find-links=/data/wheelhouse -r requirements.txt
pip3 freeze | grep -v '^-[ef] ' > /data/wheelhouse/requirements.txt
poetry install --no-dev
# Apparently pip doesn't like projects without setup.py, so it think we have 'pillar-svnman' as
# requirement (because that's the name of the directory). We have to grep that out.
poetry run pip3 freeze | grep -v '\(pillar\)\|\(^-[ef] \)' > \$WHEELHOUSE/requirements.txt
pip3 wheel --wheel-dir=\$WHEELHOUSE -r \$WHEELHOUSE/requirements.txt
chown -R $UID:$GID \$WHEELHOUSE
EOT
# Remove our own projects, they shouldn't be installed as wheel (for now).
rm -f $WHEELHOUSE/{attract,flamenco,pillar,pillarsdk}*.whl
echo "Build of $DOCKER_IMAGE_NAME:latest is done."

65
docker/4_run/Dockerfile Executable file
View File

@ -0,0 +1,65 @@
FROM armadillica/pillar_py:3.6
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive apt-get install -qy \
-o APT::Install-Recommends=false -o APT::Install-Suggests=false \
git \
apache2 \
libapache2-mod-xsendfile \
libjpeg8 \
libtiff5 \
ffmpeg \
rsyslog logrotate \
nano vim-tiny curl; \
rm -rf /var/lib/apt/lists/*
RUN ln -s /usr/bin/vim.tiny /usr/bin/vim
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2
RUN mkdir -p $APACHE_RUN_DIR $APACHE_LOCK_DIR $APACHE_LOG_DIR
ADD wheelhouse /data/wheelhouse
RUN pip3 install --no-index --find-links=/data/wheelhouse -r /data/wheelhouse/requirements.txt
VOLUME /data/config
VOLUME /data/storage
VOLUME /var/log
ENV USE_X_SENDFILE True
EXPOSE 80
EXPOSE 5000
ADD apache/remoteip.conf /etc/apache2/mods-available/
ADD apache/wsgi-py36.* /etc/apache2/mods-available/
RUN a2enmod remoteip && a2enmod rewrite && a2enmod wsgi-py36
ADD apache/apache2.conf /etc/apache2/apache2.conf
ADD apache/000-default.conf /etc/apache2/sites-available/000-default.conf
ADD apache/logrotate.conf /etc/logrotate.d/apache2
ADD *.sh /
# Remove some empty top-level directories we won't use anyway.
RUN rmdir /media /home 2>/dev/null || true
# This file includes some useful commands to have in the shell history
# for easy access.
ADD bash_history /root/.bash_history
ENTRYPOINT /docker-entrypoint.sh
# Add the most-changing files as last step for faster rebuilds.
ADD config_local.py /data/git/blender-cloud/
ADD staging /data/git
RUN python3 -c "import re, secrets; \
f = open('/data/git/blender-cloud/config_local.py', 'a'); \
h = re.sub(r'[_.~-]', '', secrets.token_urlsafe())[:8]; \
print(f'STATIC_FILE_HASH = {h!r}', file=f)"

View File

@ -1,11 +1,12 @@
<VirtualHost *:80>
XSendFile on
XSendFilePath /data/storage/pillar
XSendFilePath /data/git/pillar
XSendFilePath /data/git/pillar/pillar/web/static/
XSendFilePath /data/git/attract/attract/static/
XSendFilePath /data/git/flamenco/flamenco/static/
XsendFilePath /data/git/pillar-svnman/svnman/static/
XsendFilePath /data/git/blender-cloud
XsendFilePath /data/git/blender-cloud/static/
XsendFilePath /data/git/blender-cloud/cloud/static/
ServerAdmin webmaster@localhost
DocumentRoot /var/www/html
@ -19,7 +20,7 @@
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
WSGIDaemonProcess cloud processes=2 threads=32 maximum-requests=10000
WSGIDaemonProcess cloud processes=2 threads=64 maximum-requests=10000
WSGIPassAuthorization On
WSGIScriptAlias / /data/git/blender-cloud/runserver.wsgi \
@ -39,7 +40,7 @@
# Redirects for blender-cloud projects
RewriteRule "^/p/blender-cloud/?$" "/blog" [R=301,L]
RewriteRule "^/agent327/?$" "/p/agent-327" [R=301,L]
RewriteRule "^/caminandes/?$" "/p/caminandes" [R=301,L]
RewriteRule "^/caminandes/?$" "/p/caminandes-3" [R=301,L]
RewriteRule "^/cf2/?$" "/p/creature-factory-2" [R=301,L]
RewriteRule "^/characters/?$" "/p/characters" [R=301,L]
RewriteRule "^/gallery/?$" "/p/gallery" [R=301,L]
@ -47,5 +48,9 @@
RewriteRule "^/textures/?$" "/p/textures" [R=301,L]
RewriteRule "^/training/?$" "/courses" [R=301,L]
RewriteRule "^/spring/?$" "/p/spring" [R=301,L]
RewriteRule "^/hero/?$" "/p/hero" [R=301,L]
RewriteRule "^/coffee-run/?$" "/p/coffee-run" [R=301,L]
RewriteRule "^/settlers/?$" "/p/settlers" [R=301,L]
# Waking the forest was moved from the art gallery to its own workshop
RewriteRule "^/p/gallery/58cfec4f88ac8f1440aeb309/?$" "/p/waking-the-forest" [R=301,L]
</VirtualHost>

View File

@ -133,9 +133,9 @@ AccessFileName .htaccess
# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.
# Use mod_remoteip instead.
#
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%a %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent

View File

@ -0,0 +1,2 @@
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 172.16.0.0/12

View File

@ -1,5 +1,5 @@
#!/bin/bash -e
docker build -t armadillica/blender_cloud:latest -f run.docker .
docker build -t armadillica/blender_cloud:latest .
echo "Done, built armadillica/blender_cloud:latest"

View File

@ -0,0 +1,123 @@
import os
from collections import defaultdict
DEBUG = False
SCHEME = 'https'
PREFERRED_URL_SCHEME = 'https'
SERVER_NAME = 'cloud.blender.org'
# os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = 'true'
os.environ['PILLAR_MONGO_DBNAME'] = 'cloud'
os.environ['PILLAR_MONGO_PORT'] = '27017'
os.environ['PILLAR_MONGO_HOST'] = 'mongo'
USE_X_SENDFILE = True
STORAGE_BACKEND = 'gcs'
CDN_SERVICE_DOMAIN = 'blendercloud-pro.r.worldssl.net'
CDN_CONTENT_SUBFOLDER = ''
CDN_STORAGE_ADDRESS = 'push-11.cdnsun.com'
CACHE_TYPE = 'redis' # null
CACHE_KEY_PREFIX = 'pw_'
CACHE_REDIS_HOST = 'redis'
CACHE_REDIS_PORT = '6379'
CACHE_REDIS_URL = 'redis://redis:6379'
PILLAR_SERVER_ENDPOINT = 'https://cloud.blender.org/api/'
BLENDER_ID_ENDPOINT = 'https://www.blender.org/id/'
GCLOUD_APP_CREDENTIALS = '/data/config/google_app.json'
GCLOUD_PROJECT = 'blender-cloud'
MAIN_PROJECT_ID = '563a9c8cf0e722006ce97b03'
# MAIN_PROJECT_ID = '57aa07c088bef606e89078bd'
ALGOLIA_INDEX_USERS = 'pro_Users'
ALGOLIA_INDEX_NODES = 'pro_Nodes'
ZENCODER_NOTIFICATIONS_URL = 'https://cloud.blender.org/api/encoding/zencoder/notifications'
FILE_LINK_VALIDITY = defaultdict(
lambda: 3600 * 24 * 30, # default of 1 month.
gcs=3600 * 23, # 23 hours for Google Cloud Storage.
cdnsun=3600 * 23
)
LOGGING = {
'version': 1,
'formatters': {
'default': {'format': '%(levelname)8s %(name)s %(message)s'}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'default',
'stream': 'ext://sys.stderr',
}
},
'loggers': {
'pillar': {'level': 'INFO'},
# 'pillar.auth': {'level': 'DEBUG'},
# 'pillar.api.blender_id': {'level': 'DEBUG'},
# 'pillar.api.blender_cloud.subscription': {'level': 'DEBUG'},
'bcloud': {'level': 'INFO'},
'cloud': {'level': 'INFO'},
'attract': {'level': 'INFO'},
'flamenco': {'level': 'INFO'},
# 'pillar.api.file_storage': {'level': 'DEBUG'},
# 'pillar.api.file_storage.ensure_valid_link': {'level': 'INFO'},
'pillar.api.file_storage.refresh_links_for_backend': {'level': 'DEBUG'},
'werkzeug': {'level': 'DEBUG'},
'eve': {'level': 'WARNING'},
# 'elasticsearch': {'level': 'DEBUG'},
},
'root': {
'level': 'WARNING',
'handlers': [
'console',
],
}
}
# Latest version of the add-on.
BLENDER_CLOUD_ADDON_VERSION = '1.9.0'
REDIRECTS = {
# For old links, refer to the services page (hopefully it refreshes then)
'downloads/blender_cloud-latest-bundle.zip': 'https://cloud.blender.org/services#blender-addon',
# Latest Blender Cloud add-on.
'downloads/blender_cloud-latest-addon.zip':
f'https://storage.googleapis.com/institute-storage/addons/'
f'blender_cloud-{BLENDER_CLOUD_ADDON_VERSION}.addon.zip',
# Redirect old Grafista endpoint to /stats
'/stats/': '/stats',
}
UTM_LINKS = {
'cartoon_brew': {
'image': 'https://imgur.com/13nQTi3.png',
'link': 'https://store.blender.org/product/membership/'
}
}
SVNMAN_REPO_URL = 'https://svn.blender.cloud/repo/'
SVNMAN_API_URL = 'https://svn.blender.cloud/api/'
# Mail options, see pillar.celery.email_tasks.
SMTP_HOST = 'proog.blender.org'
SMTP_PORT = 25
SMTP_USERNAME = 'server@blender.cloud'
SMTP_TIMEOUT = 30 # timeout in seconds, https://docs.python.org/3/library/smtplib.html#smtplib.SMTP
MAIL_RETRY = 180 # in seconds, delay until trying to send an email again.
MAIL_DEFAULT_FROM_NAME = 'Blender Cloud'
MAIL_DEFAULT_FROM_ADDR = 'cloudsupport@blender.org'
# MUST be 8 characters long, see pillar.flask_extra.HashedPathConverter
# STATIC_FILE_HASH = '12345678'
# The value used in production is appended from Dockerfile.

View File

@ -1,18 +1,21 @@
#!/bin/sh
if [ ! -f /installed ]; then
SITEPKG=$(echo /opt/python/lib/python3.*/site-packages)
echo "Installing Blender Cloud packages into $SITEPKG"
# TODO: 'pip3 install -e' runs 'setup.py develop', which runs 'setup.py egg_info',
# which can't write the egg info to the read-only /data/git volume. This is why
# we manually install the links.
for SUBPROJ in /data/git/{pillar,pillar-python-sdk,attract,flamenco,pillar-svnman}; do
NAME=$(python3 $SUBPROJ/setup.py --name)
echo "... $NAME"
echo $SUBPROJ >> $SITEPKG/easy-install.pth
echo $SUBPROJ > $SITEPKG/$NAME.egg-link
done
echo "All packages installed."
touch /installed
if [ -f /installed ]; then
return
fi
SITEPKG=$(echo /opt/python/lib/python3.*/site-packages)
echo "Installing Blender Cloud packages into $SITEPKG"
# TODO: 'pip3 install -e' runs 'setup.py develop', which runs 'setup.py egg_info',
# which can't write the egg info to the read-only /data/git volume. This is why
# we manually install the links.
for SUBPROJ in /data/git/{pillar,pillar-python-sdk,attract,flamenco,pillar-svnman}; do
NAME=$(python3 $SUBPROJ/setup.py --name)
echo "... $NAME"
echo $SUBPROJ >> $SITEPKG/easy-install.pth
echo $SUBPROJ > $SITEPKG/$NAME.egg-link
done
echo "All packages installed."
touch /installed

View File

@ -1,55 +0,0 @@
FROM armadillica/pillar_py:3.6
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
RUN apt-get update && apt-get install -qyy \
-o APT::Install-Recommends=false -o APT::Install-Suggests=false \
git \
apache2 \
libapache2-mod-xsendfile \
libjpeg8 \
libtiff5 \
ffmpeg \
rsyslog logrotate \
nano vim-tiny curl \
&& rm -rf /var/lib/apt/lists/*
RUN ln -s /usr/bin/vim.tiny /usr/bin/vim
ENV APACHE_RUN_USER www-data
ENV APACHE_RUN_GROUP www-data
ENV APACHE_LOG_DIR /var/log/apache2
ENV APACHE_PID_FILE /var/run/apache2.pid
ENV APACHE_RUN_DIR /var/run/apache2
ENV APACHE_LOCK_DIR /var/lock/apache2
RUN mkdir -p $APACHE_RUN_DIR $APACHE_LOCK_DIR $APACHE_LOG_DIR
ADD wheelhouse /data/wheelhouse
RUN pip3 install --no-index --find-links=/data/wheelhouse -r /data/wheelhouse/requirements.txt
VOLUME /data/git
VOLUME /data/config
VOLUME /data/storage
VOLUME /var/log
ENV USE_X_SENDFILE True
EXPOSE 80
EXPOSE 5000
ADD wsgi-py36.* /etc/apache2/mods-available/
RUN a2enmod rewrite && a2enmod wsgi-py36
ADD apache2.conf /etc/apache2/apache2.conf
ADD 000-default.conf /etc/apache2/sites-available/000-default.conf
ADD apache-logrotate.conf /etc/logrotate.d/apache2
ADD *.sh /
# Remove some empty top-level directories we won't use anyway.
RUN rmdir /media /home 2>/dev/null || true
# This file includes some useful commands to have in the shell history
# for easy access.
ADD bash_history /root/.bash_history
ENTRYPOINT /docker-entrypoint.sh

View File

@ -58,6 +58,7 @@ Place TLS certificates in `/data/certs/{cloud,cloudapi}.blender.org.pem`.
They should contain (in order) the private key, the host certificate, and the
CA certificate.
## 6. Create a local config
Blender Cloud expects the following files to exist:
@ -65,6 +66,9 @@ Blender Cloud expects the following files to exist:
- `/data/git/blender_cloud/config_local.py` with machine-local configuration overrides
- `/data/config/google_app.json` with Google Cloud Storage credentials.
When run from Docker, the `docker/4_run/config_local.py` file will be used. Overrides for that file
can be placed in `/data/config/config_secrets.py`.
## 7. ElasticSearch & kibana

View File

@ -1,7 +1,7 @@
version: '3.4'
services:
mongo:
image: mongo:3.4.2
image: mongo:3.4
container_name: mongo
restart: always
volumes:
@ -9,22 +9,30 @@ services:
- /data/storage/db-bak:/data/db-bak # for backing up stuff etc.
ports:
- "127.0.0.1:27017:27017"
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
# Databases in use:
# 0: Flask Cache
# 1: Celery (backend)
# 2: Celery (broker)
redis:
image: redis:3.2.8
image: redis:5.0
container_name: redis
restart: always
ports:
- "127.0.0.1:6379:6379"
rabbit:
image: rabbitmq:3.6.10
container_name: rabbit
restart: always
ports:
- "127.0.0.1:5672:5672"
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
elastic:
# This image is defined in blender-cloud/docker/elastic
image: armadillica/elasticsearch:6.1.1
container_name: elastic
restart: always
@ -35,6 +43,11 @@ services:
- "127.0.0.1:9200:9200"
environment:
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
elasticproxy:
image: armadillica/elasticproxy:1.2
@ -43,8 +56,14 @@ services:
command: /elasticproxy -elastic http://elastic:9200/
depends_on:
- elastic
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
kibana:
# This image is defined in blender-cloud/docker/elastic
image: armadillica/kibana:6.1.1
container_name: kibana
restart: always
@ -52,7 +71,7 @@ services:
SERVER_NAME: "stats.cloud.blender.org"
ELASTICSEARCH_URL: http://elasticproxy:9200
CONSOLE_ENABLED: 'false'
VIRTUAL_HOST: http://stats.cloud.blender.org/*,https://stats.cloud.blender.org/*,http://stats.blender-cloud/*,https://stats.blender-cloud/*
VIRTUAL_HOST: http://stats.cloud.blender.org/*,https://stats.cloud.blender.org/*,http://stats.cloud.local/*,https://stats.cloud.local/*
VIRTUAL_HOST_WEIGHT: 20
FORCE_SSL: "true"
@ -60,85 +79,89 @@ services:
NODE_OPTIONS: "--max-old-space-size=200"
depends_on:
- elasticproxy
blender_cloud:
image: armadillica/blender_cloud:latest
container_name: blender_cloud
restart: always
environment:
VIRTUAL_HOST: http://cloud.blender.org/*,https://cloud.blender.org/*,http://blender-cloud/*,https://blender-cloud/*
VIRTUAL_HOST_WEIGHT: 10
FORCE_SSL: "true"
GZIP_COMPRESSION_TYPE: "text/html text/plain text/css application/javascript"
volumes:
# format: HOST:CONTAINER
- /data/git:/data/git:ro
- /data/config:/data/config:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
- rabbit
celery_worker:
image: armadillica/blender_cloud:latest
entrypoint: /celery-worker.sh
container_name: celery_worker
restart: always
volumes:
# format: HOST:CONTAINER
- /data/git:/data/git:ro
- /data/config:/data/config:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
- rabbit
celery_beat:
image: armadillica/blender_cloud:latest
entrypoint: /celery-beat.sh
container_name: celery_beat
restart: always
volumes:
# format: HOST:CONTAINER
- /data/git:/data/git:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
- rabbit
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
grafista:
image: armadillica/grafista:latest
container_name: grafista
blender_cloud:
image: armadillica/blender_cloud:latest
container_name: blender_cloud
restart: always
environment:
VIRTUAL_HOST: http://cloud.blender.org/*,https://cloud.blender.org/*,http://cloud.local/*,https://cloud.local/*
VIRTUAL_HOST_WEIGHT: 10
FORCE_SSL: "true"
GZIP_COMPRESSION_TYPE: "text/html text/plain text/css application/javascript"
PILLAR_CONFIG: /data/config/config_secrets.py
volumes:
- /data/git/grafista:/data/git/grafista:ro
- /data/storage/grafista:/data/storage/grafista
# format: HOST:CONTAINER
- /data/config:/data/config:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
celery_worker:
image: armadillica/blender_cloud:latest
entrypoint: /celery-worker.sh
container_name: celery_worker
restart: always
environment:
PILLAR_CONFIG: /data/config/config_secrets.py
volumes:
# format: HOST:CONTAINER
- /data/config:/data/config:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
celery_beat:
image: armadillica/blender_cloud:latest
entrypoint: /celery-beat.sh
container_name: celery_beat
restart: always
environment:
PILLAR_CONFIG: /data/config/config_secrets.py
volumes:
# format: HOST:CONTAINER
- /data/config:/data/config:ro
- /data/storage/pillar:/data/storage/pillar
- /data/log:/var/log
depends_on:
- mongo
- redis
- celery_worker
logging:
driver: "json-file"
options:
max-size: "200k"
max-file: "20"
letsencrypt:
image: armadillica/picohttp:latest
image: armadillica/picohttp:1.0
container_name: letsencrypt
restart: always
environment:
WEBROOT: /data/letsencrypt
LISTEN: '[::]:80'
VIRTUAL_HOST: http://cloud.blender.org/.well-known/*, http://stats.cloud.blender.org/.well-known/*
VIRTUAL_HOST_WEIGHT: 20
VIRTUAL_HOST_WEIGHT: 30
volumes:
- /data/letsencrypt:/data/letsencrypt
haproxy:
image: dockercloud/haproxy:1.5.3
# This image is defined in blender-cloud/docker/haproxy
image: armadillica/haproxy:1.6.7
container_name: haproxy
restart: always
ports:

View File

@ -0,0 +1,5 @@
FROM dockercloud/haproxy:1.6.7
LABEL maintainer="Sybren A. Stüvel <sybren@blender.studio>"
# Fix https://talosintelligence.com/vulnerability_reports/TALOS-2019-0782
RUN sed 's/root::/root:!:/' -i /etc/shadow

10
docker/haproxy/build.sh Executable file
View File

@ -0,0 +1,10 @@
#!/bin/bash -e
# When updating this, also update the version in Dockerfile
VERSION=1.6.7
docker build -t armadillica/haproxy:${VERSION} .
docker tag armadillica/haproxy:${VERSION} armadillica/haproxy:latest
echo "Done, built armadillica/haproxy:${VERSION}"
echo "Also tagged as armadillica/haproxy:latest"

5
docker/mongo-backup.cron Normal file
View File

@ -0,0 +1,5 @@
# Change to suit your needs, then place in /etc/cron.d/mongo-backup
# (so remove the .cron from the name)
MAILTO=yourname@youraddress.org
30 5 * * * root /root/docker/mongo-backup.sh

28
docker/mongo-backup.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash -e
BACKUPDIR=/data/storage/db-bak
DATE=$(date +'%Y-%m-%d-%H%M')
ARCHIVE=$BACKUPDIR/mongo-live-$DATE.tar.xz
# Just a sanity check before we give it to 'rm -rf'
if [ -z "$DATE" ]; then
echo "Empty string found where the date should be, aborting."
exit 1
fi
# /data/db-bak in Docker is /data/storage/db-bak on the host.
docker exec mongo mongodump -d cloud \
--out /data/db-bak/dump-$DATE \
--excludeCollection tokens \
--excludeCollection flamenco_task_logs \
--quiet
cd $BACKUPDIR
tar -Jcf $ARCHIVE dump-$DATE/
rm -rf dump-$DATE
TO_DELETE="$(ls $BACKUPDIR/mongo-live-*.tar.xz | head -n -7)"
[ -z "$TO_DELETE" ] || rm "$TO_DELETE"
rsync -a $BACKUPDIR/mongo-live-*.tar.xz cloud-backup@swami-direct.blender.cloud:

14
gulp
View File

@ -14,6 +14,20 @@ function install() {
if [ "$1" == "watch" ]; then
# Treat "gulp watch" as "gulp && gulp watch"
$GULP
elif [ "$1" == "all" ]; then
pushd .
# This is useful when building the Blender Cloud project for the first time.
# Run ./gulp in all depending projects (pillar, attract, flamenco, pillar-svnman)
declare -a repos=("pillar" "attract" "flamenco" "pillar-svnman")
for r in "${repos[@]}"
do
cd ../$r
./gulp
done
popd
# Run "gulp" once inside the repo
$GULP
exit 1
fi
exec $GULP "$@"

View File

@ -1,37 +1,41 @@
var argv = require('minimist')(process.argv.slice(2));
var autoprefixer = require('gulp-autoprefixer');
var chmod = require('gulp-chmod');
var concat = require('gulp-concat');
var git = require('gulp-git');
var gulp = require('gulp');
var gulpif = require('gulp-if');
var pug = require('gulp-pug');
var livereload = require('gulp-livereload');
var plumber = require('gulp-plumber');
var rename = require('gulp-rename');
var sass = require('gulp-sass');
var sourcemaps = require('gulp-sourcemaps');
var uglify = require('gulp-uglify');
var cache = require('gulp-cached');
let argv = require('minimist')(process.argv.slice(2));
let autoprefixer = require('gulp-autoprefixer');
let cache = require('gulp-cached');
let chmod = require('gulp-chmod');
let concat = require('gulp-concat');
let git = require('gulp-git');
let gulp = require('gulp');
let gulpif = require('gulp-if');
let pug = require('gulp-pug');
let plumber = require('gulp-plumber');
let rename = require('gulp-rename');
let sass = require('gulp-sass');
let sourcemaps = require('gulp-sourcemaps');
let uglify = require('gulp-uglify-es').default;
var enabled = {
let enabled = {
uglify: argv.production,
maps: argv.production,
maps: !argv.production,
failCheck: !argv.production,
prettyPug: !argv.production,
cachify: !argv.production,
cleanup: argv.production,
chmod: argv.production,
};
var destination = {
let destination = {
css: 'cloud/static/assets/css',
pug: 'cloud/templates',
js: 'cloud/static/assets/js',
}
let source = {
pillar: '../pillar/'
}
/* CSS */
gulp.task('styles', function() {
gulp.task('styles', function(done) {
gulp.src('src/styles/**/*.sass')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.maps, sourcemaps.init()))
@ -40,32 +44,34 @@ gulp.task('styles', function() {
))
.pipe(autoprefixer("last 3 versions"))
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
.pipe(gulp.dest(destination.css))
.pipe(gulpif(argv.livereload, livereload()));
.pipe(gulp.dest(destination.css));
done();
});
/* Templates - Pug */
gulp.task('templates', function() {
gulp.task('templates', function(done) {
gulp.src('src/templates/**/*.pug')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.cachify, cache('templating')))
.pipe(pug({
pretty: enabled.prettyPug
}))
.pipe(gulp.dest(destination.pug))
.pipe(gulpif(argv.livereload, livereload()));
.pipe(gulp.dest(destination.pug));
// TODO(venomgfx): please check why 'gulp watch' doesn't pick up on .txt changes.
gulp.src('src/templates/**/*.txt')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.cachify, cache('templating')))
.pipe(gulp.dest(destination.pug))
.pipe(gulpif(argv.livereload, livereload()));
.pipe(gulp.dest(destination.pug));
done();
});
/* Tutti gets built by Pillar. See gulpfile.js in pillar.*/
/* Individual Uglified Scripts */
gulp.task('scripts', function() {
gulp.task('scripts', function(done) {
gulp.src('src/scripts/*.js')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.cachify, cache('scripting')))
@ -73,44 +79,39 @@ gulp.task('scripts', function() {
.pipe(gulpif(enabled.uglify, uglify()))
.pipe(rename({suffix: '.min'}))
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
.pipe(chmod(644))
.pipe(gulp.dest(destination.js))
.pipe(gulpif(argv.livereload, livereload()));
.pipe(gulpif(enabled.chmod, chmod(0o644)))
.pipe(gulp.dest(destination.js));
done();
});
/* Collection of scripts in src/scripts/tutti/ to merge into tutti.min.js */
/* Since it's always loaded, it's only for functions that we want site-wide */
gulp.task('scripts_concat_tutti', function() {
gulp.src('src/scripts/tutti/**/*.js')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.maps, sourcemaps.init()))
.pipe(concat("tutti.min.js"))
.pipe(gulpif(enabled.uglify, uglify()))
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
.pipe(chmod(644))
.pipe(gulp.dest(destination.js))
.pipe(gulpif(argv.livereload, livereload()));
});
// While developing, run 'gulp watch'
gulp.task('watch',function() {
// Only listen for live reloads if ran with --livereload
if (argv.livereload){
livereload.listen();
}
gulp.task('watch',function(done) {
let watchStyles = [
'src/styles/**/*.sass',
source.pillar + 'src/styles/**/*.sass',
];
gulp.watch('src/styles/**/*.sass',['styles']);
gulp.watch('src/templates/**/*.pug',['templates']);
gulp.watch('src/scripts/*.js',['scripts']);
gulp.watch('src/scripts/tutti/**/*.js',['scripts_concat_tutti']);
let watchScripts = [
'src/scripts/**/*.js',
source.pillar + 'src/scripts/**/*.js',
];
let watchTemplates = [
'src/templates/**/*.pug',
source.pillar + 'src/templates/**/*.pug',
];
gulp.watch(watchStyles, gulp.series('styles'));
gulp.watch(watchScripts, gulp.series('scripts'));
gulp.watch(watchTemplates, gulp.series('templates'));
done();
});
// Erases all generated files in output directories.
gulp.task('cleanup', function() {
var paths = [];
gulp.task('cleanup', function(done) {
let paths = [];
for (attr in destination) {
paths.push(destination[attr]);
}
@ -118,11 +119,12 @@ gulp.task('cleanup', function() {
git.clean({ args: '-f -X ' + paths.join(' ') }, function (err) {
if(err) throw err;
});
done();
});
// Run 'gulp' to build everything at once
var tasks = [];
let tasks = [];
if (enabled.cleanup) tasks.push('cleanup');
gulp.task('default', tasks.concat(['styles', 'templates', 'scripts', 'scripts_concat_tutti']));
gulp.task('default', gulp.parallel(tasks.concat(['styles', 'templates', 'scripts'])));

6
jwtkeys/README.md Normal file
View File

@ -0,0 +1,6 @@
# Flamenco Server JWT keys
To generate a keypair for `ES256`:
openssl ecparam -genkey -name prime256v1 -noout -out es256-private.pem
openssl ec -in es256-private.pem -pubout -out es256-public.pem

6417
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,20 +7,26 @@
"url": "git://git.blender.org/blender-cloud.git"
},
"devDependencies": {
"gulp": "~3.9.1",
"gulp-autoprefixer": "~2.3.1",
"gulp-cached": "~1.1.0",
"gulp-chmod": "~1.3.0",
"gulp-concat": "~2.6.0",
"gulp-if": "^2.0.1",
"gulp-git": "~2.4.2",
"gulp-pug": "~3.2.0",
"gulp-livereload": "~3.8.1",
"gulp-plumber": "~1.1.0",
"gulp-rename": "~1.2.2",
"gulp-sass": "~2.3.1",
"gulp-sourcemaps": "~1.6.0",
"gulp-uglify": "~1.5.3",
"gulp": "~4.0",
"gulp-autoprefixer": "~6.0.0",
"gulp-cached": "~1.1.1",
"gulp-chmod": "~2.0.0",
"gulp-concat": "~2.6.1",
"gulp-if": "^2.0.2",
"gulp-git": "~2.8.0",
"gulp-plumber": "~1.2.0",
"gulp-pug": "~4.0.1",
"gulp-rename": "~1.4.0",
"gulp-sass": "~4.1.0",
"gulp-sourcemaps": "~2.6.4",
"gulp-uglify-es": "^1.0.4",
"minimist": "^1.2.0"
},
"dependencies": {
"bootstrap": "^4.1.3",
"jquery": "^3.3.1",
"natives": "^1.1.6",
"popper.js": "^1.14.4",
"video.js": "^7.2.2"
}
}

1917
poetry.lock generated Normal file

File diff suppressed because it is too large Load Diff

2
poetry.toml Normal file
View File

@ -0,0 +1,2 @@
[virtualenvs]
in-project = false

75
pyproject.toml Normal file
View File

@ -0,0 +1,75 @@
[tool.poetry]
name = "blender-cloud"
version = "1.0"
description = ""
authors = [
"Francesco Siddi <francesco@blender.org>",
"Pablo Vazquez <pablo@blender.studio>",
"Sybren Stüvel <sybren@blender.studio>",
]
include = ["README.md", "LICENSE.txt"]
[tool.poetry.dependencies]
python = "~3.6"
pillar = {path = "../pillar"}
attract = {path = "../attract"}
flamenco = {path = "../flamenco"}
svnman = {path = "../pillar-svnman"}
amqp = "2.5.0"
asn1crypto = "0.24.0"
attrs = "19.1.0"
babel = "2.7.0"
bcrypt = "3.1.6"
billiard = "3.6.0.0"
bleach = "3.1.0"
celery = "4.3.0"
cerberus = "1.3.1"
certifi = "2019.3.9"
cffi = "1.12.3"
chardet = "3.0.4"
click = "7.0"
commonmark = "0.9.0"
cryptography = "2.7"
eve = "0.9.1"
fasteners = "0.15"
flask = "1.0.3"
flask-wtf = "0.14.2"
future = "0.17.1"
google-apitools = "0.5.28"
googleapis-common-protos = "1.6.0"
grpcio = "1.21.1"
httplib2 = "0.12.3"
ipaddress = "1.0.22"
jinja2 = "2.10.1"
kombu = "4.6.0"
protobuf = "3.8.0"
pyasn1 = "0.4.5"
pyasn1-modules = "0.2.5"
pycparser = "2.19"
pymongo = "3.8.0"
pyopenssl = "19.0.0"
pytz = "2019.1"
requests = "2.22.0"
rsa = "4.0"
setuptools = "51.0.0"
shortcodes = "2.5.0"
simplejson = "3.16.0"
six = "1.12.0"
wheel = "0.35.1"
wtforms = "2.2.1"
[tool.poetry.dev-dependencies]
pillar-devdeps = {path = "../pillar/devdeps"}
responses = "0.10.6"
zipp = "0.5.1"
py = "1.8.0"
colorama = "0.4.1"
importlib-metadata = "0.17"
more-itertools = "7.0.0"
coverage = "4.5.3"
pluggy = "0.12.0"
atomicwrites = "1.3.0"
[build-system]
requires = ["poetry==1.0","cryptography==2.7","setuptools==51.0.0","wheel==0.35.1"]
build-backend = "poetry.masonry.api"

View File

@ -1,11 +0,0 @@
-r ../pillar-python-sdk/requirements-dev.txt
-r ../pillar/requirements-dev.txt
-r ../attract/requirements-dev.txt
-r ../flamenco/requirements-dev.txt
-r ../pillar-svnman/requirements-dev.txt
-e ../pillar-python-sdk
-e ../pillar
-e ../attract
-e ../flamenco
-e ../pillar-svnman

View File

@ -1,4 +0,0 @@
-r ../pillar/requirements.txt
-r ../attract/requirements.txt
-r ../flamenco/requirements.txt
-r ../pillar-svnman/requirements.txt

View File

@ -1,91 +0,0 @@
#!/usr/bin/env bash
set -e # error out when one of the commands in the script errors.
if [ -z "$1" ]; then
echo "Usage: $0 {host-to-deploy-to}" >&2
exit 1
fi
DEPLOYHOST="$1"
# macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then
command -v greadlink 2>/dev/null 2>&1 || { echo >&2 "Install greadlink using brew."; exit 1; }
readlink='greadlink'
else
readlink='readlink'
fi
BLENDER_CLOUD_DIR="$(dirname "$($readlink -f "$0")")"
if [ ! -d "$BLENDER_CLOUD_DIR" ]; then
echo "Unable to find Blender Cloud dir '$BLENDER_CLOUD_DIR'"
exit 1
fi
BLENDER_CLOUD_ASSETS="$BLENDER_CLOUD_DIR/cloud/static/"
BLENDER_CLOUD_TEMPLATES="$BLENDER_CLOUD_DIR/cloud/templates/"
if [ ! -d "$BLENDER_CLOUD_ASSETS" ]; then
echo "Unable to find assets dir $BLENDER_CLOUD_ASSETS"
exit 1
fi
cd $BLENDER_CLOUD_DIR
if [ $(git rev-parse --abbrev-ref HEAD) != "production" ]; then
echo "You are NOT on the production branch, refusing to rsync_ui." >&2
exit 1
fi
PILLAR_DIR=$(python <<EOT
from __future__ import print_function
import os.path
import pillar
print(os.path.dirname(os.path.dirname(pillar.__file__)))
EOT
)
PILLAR_ASSETS="$PILLAR_DIR/pillar/web/static/assets/"
PILLAR_TEMPLATES="$PILLAR_DIR/pillar/web/templates/"
if [ ! -d "$PILLAR_ASSETS" ]; then
echo "Unable to find assets dir $PILLAR_ASSETS"
exit 1
fi
cd $PILLAR_DIR
if [ $(git rev-parse --abbrev-ref HEAD) != "production" ]; then
echo "Pillar is NOT on the production branch, refusing to rsync_ui." >&2
exit 1
fi
echo
echo "*** GULPA GULPA PILLAR ***"
# TODO(Pablo): this command fails when passing the --production CLI
# arg.
./gulp
echo
echo "*** SYNCING PILLAR_ASSETS ***"
rsync -avh $PILLAR_ASSETS root@${DEPLOYHOST}:/data/git/pillar/pillar/web/static/assets/ --delete-after
echo
echo "*** SYNCING PILLAR_TEMPLATES ***"
rsync -avh $PILLAR_TEMPLATES root@${DEPLOYHOST}:/data/git/pillar/pillar/web/templates/ --delete-after
cd $BLENDER_CLOUD_DIR
echo
echo "*** GULPA GULPA BLENDER_CLOUD ***"
./gulp --production
echo
echo "*** SYNCING BLENDER_CLOUD_ASSETS ***"
# Exclude files managed by Git.
rsync -avh $BLENDER_CLOUD_ASSETS --exclude js/vendor/ root@${DEPLOYHOST}:/data/git/blender-cloud/cloud/static/ --delete-after
echo
echo "*** SYNCING BLENDER_CLOUD_TEMPLATES ***"
rsync -avh $BLENDER_CLOUD_TEMPLATES root@${DEPLOYHOST}:/data/git/blender-cloud/cloud/templates/ --delete-after

View File

@ -1,18 +0,0 @@
#!/usr/bin/env python
"""Setup file for testing, not for packaging/distribution."""
import setuptools
setuptools.setup(
name='blender-cloud',
version='1.0',
packages=setuptools.find_packages('.', exclude=['tests']),
tests_require=[
'pytest>=2.9.1',
'responses>=0.5.1',
'pytest-cov>=2.2.1',
'mock>=2.0.0',
],
zip_safe=False,
)

View File

@ -0,0 +1,29 @@
/**
* Support for fetching & rendering assets by tags.
*/
(function($) {
$.fn.loadTaggedAssets = function(load_initial_count, load_next_count, has_subscription) {
mark_if_public = !has_subscription;
this.each(function(index, each) {
let $card_deck_element = $(each)
$card_deck_element.trigger('pillar:workStart');
$.get('/api/nodes/tagged/' + $card_deck_element.data('assetTag'))
.fail(function(error) {
let msg = xhrErrorResponseMessage(error);
$card_deck_element
.append(
$('<p>').addClass('bg-danger').text(msg)
);
})
.done(function(resp) {
// 'resp' is a list of node documents.
$card_deck_element.append(
pillar.templates.Nodes.createListOf$nodeItems(resp, load_initial_count, load_next_count)
);
})
.always(function() {
$card_deck_element.trigger('pillar:workStop');
});
});
};
}(jQuery));

9
src/styles/_about.sass Normal file
View File

@ -0,0 +1,9 @@
section.team
h2, .people-container
text-align: center
h3
margin-bottom: 0
h3 small
display: block
.people-intro, .row
margin-bottom: 20px

View File

@ -1,752 +1,60 @@
.dashboard-container
+container-behavior
+media-xs
flex-direction: column
align-content: center
align-items: flex-start
display: flex
justify-content: space-around
word-break: break-word
.random-featured
// Hide irrelevant info from cards.
li
&.item-type,
&.item-date
@extend .d-none
section.dashboard-main,
section.dashboard-secondary
+media-xs
width: 100%
margin: 20px auto
// Fit 3 cards per row.
&.card-deck.card-deck-responsive
.card
+media-md
flex: 1 0 30%
max-width: 30%
flex: 1 0 31%
max-width: 31%
img
max-width: 100%
section.dashboard-main
+container-box
width: 52%
section.dashboard-secondary
width: 46%
flex-direction: column
margin-right: auto
span.section-lead
display: block
padding: 10px 0
color: $color-text-dark-secondary
section.dashboard-main,
section.dashboard-secondary
h4
padding-bottom: 5px
margin-bottom: 20px
position: relative
&:before
position: absolute
width: 50px
height: 2px
top: 125%
content: ' '
display: block
background-color: $color-primary
.homepage
.timeline
.card-text,
.card-title
margin-bottom: 0 !important
padding: 0 !important
// Hide project name, it's already in the timeline.
a
color: $color-text
&:hover
color: $color-primary
cursor: pointer
nav#nav-tabs,
nav#sub-nav-tabs
ul#nav-tabs__list,
ul#sub-nav-tabs__list
margin: 0
padding: 0
list-style: none
border-bottom: thin solid $color-background
+clearfix
li.nav-tabs__list-tab
float: left
border: none
border-bottom: 3px solid transparent
color: $color-text-dark-primary
user-select: none
&:hover
border-color: rgba($color-secondary, .3)
cursor: pointer
color: $color-text-dark
a
color: $color-text-dark
a
display: block
text-decoration: none
padding: 10px 15px 5px
color: $color-text-dark-primary
i
margin-right: 5px
color: $color-text-dark-secondary
font-size: .9em
&.pi-blender
margin-right: 10px
span
color: $color-text-dark-hint
margin-left: 5px
&.active
border-color: $color-secondary
color: $color-secondary-dark
a, i
color: $color-secondary-dark
&.disabled
border-color: $color-background-light
color: $color-text-dark-hint
cursor: default
a, i
color: $color-text-dark-hint
&:hover
border-color: $color-background-light
pointer-events: none
li.create
cursor: pointer
display: inline-block
float: right
font:
size: 1.2em
weight: 400
padding: 5px 10px
margin-top: 3px
a
color: $color-success
text-decoration: none
&.disabled
cursor: wait
border-color: $color-success
opacity: .8
a
cursor: wait
section.stream
background-color: white
border-bottom: thin solid $color-background-dark
ul.activity-stream__list
list-style: none
margin: 0
padding: 0
$activity-stream-thumbnail-size: 110px
> li
position: relative
display: flex
padding: 10px 0
overflow: hidden
border-top: thin solid $color-background-dark
&:first-child
border: none
&.active .activity-stream__list-details .title
color: $color-primary
&:hover
.title
text-decoration: underline
&.video
a.image
&:hover
i
font-size: 3.5em
img
opacity: .9
img
opacity: .7
z-index: 0
transition: opacity 150ms ease-in-out
i
+position-center-translate
z-index: 1
color: rgba(white, .6)
font-size: 3em
transition: font-size 100ms ease-in-out
&.comment
.activity-stream__list-thumbnail
background: transparent
color: $node-type-comment
font-size: 1.2em
box-shadow: none
i
+position-center-translate
left: 22px
top: 19px
.activity-stream__list-details
padding: 0
.title
color: $color-text-dark
padding: 7px 10px 2px 10px
font-size: 1em
margin: 0
ul.meta
padding: 0 10px 7px 10px
li
&.where-parent:before
content: '\e83a'
font-family: 'pillar-font'
&.what:before
display: none
&.post
.activity-stream__list-thumbnail
border-color: $node-type-post
background-color: $node-type-post
.activity-stream__list-details .title
color: darken($node-type-post, 15%)
font:
size: 1.3em
weight: 500
&.asset, &.comment, &.post
&:hover
cursor: pointer
&.empty
display: none
color: $color-text-dark-primary
padding: 20px
text-align: center
span
color: $color-primary
&:hover
text-decoration: underline
cursor: pointer
&.with-picture
min-height: $activity-stream-thumbnail-size
.activity-stream__list-thumbnail
background-color: black
width: $activity-stream-thumbnail-size * 1.69
min-width: $activity-stream-thumbnail-size * 1.69
.activity-stream__list-thumbnail-icon
position: absolute
top: 0
left: 0
right: 0
bottom: 0
font-size: 1.3em
text-shadow: 1px 1px 0 rgba(black, .2)
background-image: linear-gradient(10deg, rgba(black, .5) 0%, transparent 40%)
i
position: absolute
bottom: -8px
left: 20px
top: initial
right: initial
color: white
.activity-stream__list-thumbnail
position: relative
display: flex
justify-content: center
align-items: center
overflow: hidden
width: 35px
height: auto
min-width: 35px
min-height: auto
+media-xs
display: none
&.image i
color: $node-type-asset_image
&.file i
color: $node-type-asset_file
&.video i
color: $node-type-asset_video
i
+position-center-translate
left: 23px
top: 21px
font-size: 1.1em
img
max-height: $activity-stream-thumbnail-size
+position-center-translate
.activity-stream__list-details
display: flex
flex-direction: column
justify-content: space-around
flex: 1
overflow: hidden
position: relative
max-width: 100%
margin-right: auto
padding: 10px 0
+media-xs
margin-left: 0
.ribbon
+ribbon
right: -47px
top: 5px
font:
size: 12px
weight: 500
span
padding: 1px 50px
.title
display: inline-block
padding: 0 10px
color: $color-text-dark
font-size: 1.1em
span
@include badge(hsl(hue($color-success), 60%, 45%), 3px)
font-size: .7em
padding: 1px 5px
margin-right: 5px
ul.meta
+list-meta
padding: 5px 10px 0 10px
font-size: .85em
color: $color-text-dark-secondary
display: flex
white-space: nowrap
&.extra
margin-top: auto
li
padding-left: 10px
&:before
left: -5px
&.where-project
+text-overflow-ellipsis
section.comments
padding: 0 15px 5px
ul
padding: 0
> ul
list-style-type: none
margin: 10px 0 0
> li
+text-overflow-ellipsis
border-top: thin solid $color-background-dark
padding: 10px 0
&:first-child
border: none
> a
+text-overflow-ellipsis
color: $color-text
display: block
padding-bottom: 5px
section.blog-stream
+media-md
padding-left: 10px
+media-sm
padding-left: 10px
position: relative
.feed
position: absolute
top: 10px
right: 10px
font-size: 1.4em
color: lighten($color-text-dark-hint, 10%)
&:hover
color: $color-primary
> ul
margin: 0
padding: 0
list-style: none
border-top: thin solid $color-background
.blog_index-item
+container-box
display: flex
flex-direction: column
margin-bottom: 50px
&:before
height: 1px
background-color: $color-background-dark
position: absolute
bottom: -26px
left: 25px
right: 25px
content: ' '
&:last-child
margin-bottom: 0
&:before
display: none
video
max-width: 100%
a.item-title
font-size: 1.6em
padding: 5px 15px
display: block
color: $color-text
&:hover
color: $color-primary
ul.meta
+list-meta
font-size: .9em
padding: 15px 15px 5px
&.blog-non-featured
border-radius: 0
margin: 0
.item-content
+node-details-description
padding: 10px 15px
.blog-stream__list-details
.title
color: $color-text-dark-primary
display: block
font-size: 1.3em
&:hover
color: $color-primary
ul.meta
+list-meta
padding-top: 5px
font-size: .9em
color: $color-text-dark-secondary
li
padding-left: 10px
&:before
left: -5px
.blog_index-header
display: block
position: relative
img
border-top-left-radius: 3px
border-top-right-radius: 3px
width: 100%
.more
text-align: center
a
color: $color-text
display: block
padding: 25px 0
text-decoration: underline
width: 100%
&:hover
color: $color-primary
section.random-asset
border-bottom: thin solid $color-background-dark
ul.random-asset__list
list-style: none
padding: 0
> li
align-items: center
border-top: thin solid $color-background
display: flex
padding: 7px 0
position: relative
overflow: hidden
&:first-child
border-top: none
.ribbon
+ribbon
right: -47px
top: 5px
font:
size: 12px
weight: 500
z-index: 1
span
padding: 1px 50px
.random-asset__list-thumbnail
background-color: $color-background
display: block
height: 50px
margin-right: 15px
min-height: 50px
min-width: 50px
overflow: hidden
position: relative
width: 50px
img
width: 100%
i
+position-center-translate
font-size: 1.6em
color: $color-text-light
&.image
background-color: $node-type-asset_image
&.file
background-color: $node-type-asset_file
font-size: .8em
&.video
background-color: $node-type-asset_video
font-size: .8em
&.None
background-color: $node-type-group
.random-asset__list-details
.title
display: block
font-size: 1em
color: $color-text-dark-primary
&:hover
color: $color-primary
ul.meta
+list-meta
padding-top: 5px
font-size: .9em
li
&:before
left: -5px
&.what
text-transform: capitalize
&.featured
align-items: flex-start
flex-direction: column
padding: 0
a.title
font-size: 1.1em
padding: 10px 0 5px
display: block
color: $color-text
&:hover
color: $color-primary
a.random-asset__thumbnail
display: block
position: relative
&.video
background-color: black
img
opacity: .7
img
transition: opacity 150ms ease-in-out
width: 100%
max-width: 100%
i
+position-center-translate
color: white
font-size: 3em
text-shadow: 0 0 25px black
transition: font-size 150ms ease-in-out
&:hover
i
font-size: 3.5em
img
opacity: .85
ul.meta
+list-meta
padding-bottom: 10px
section.announcement
+container-box
margin-left: 15px
margin-right: 15px
.header-icons
display: flex
align-items: center
justify-content: center
padding: 20px 0 5px 0
i
font-size: 2.5em
color: $color-info
&.pi-heart-filled
color: $color-danger
margin-left: 5px
img.header
width: 100%
margin: 0 auto
border-top-left-radius: 3px
border-top-right-radius: 3px
iframe
width: 100%
position: relative
left: 15px
margin: 25px auto
+media-sm
height: 500px
+media-md
height: 520px
+media-lg
height: 580px
.text
padding: 15px
.title
padding-bottom: 10px
font:
family: $font-body
size: 1.4em
weight: 300
+media-xs
font-size: 1.4em
strong
color: $color-primary-dark
a
color: $color-text-dark-primary
.lead
font-size: 1em
+list-bullets
ul
margin-top: 10px
padding-left: 10px
hr
border: none
height: 1px
width: 100%
margin: 10px 0
background-color: $color-background
clear: both
+media-xs
padding-left: 10px
.buttons
margin: 15px auto 0 auto
display: flex
align-items: center
justify-content: space-around
flex-wrap: wrap
a
+button($color-text-light, 3px)
padding: 5px 0
margin:
bottom: 5px
right: auto
left: auto
font-size: .9em
opacity: 1
flex: 1
+media-xs
margin: 10px auto
width: 100%
&:first-child
margin-right: 15px
&.blue
+button(hsl(hue($color-info), 60%, 45%), 3px)
&.orange
+button(hsl(hue($color-secondary), 50%, 50%), 3px)
padding: 5px 15px
&.green
+button(hsl(hue($color-success), 60%, 40%), 3px, true)
section.dashboard-in-production
.in-production-project
border-bottom: thin solid $color-background-dark
color: $color-text-dark-primary
display: block
font-size: 1.1em
margin-bottom: 15px
> img
margin-bottom: 15px
body.homepage
.dashboard-container
.dashboard-main
+media-xs
width: 100%
background-color: transparent
box-shadow: none
width: 60%
.dashboard-secondary
+container-box
+media-xs
width: 100%
width: 38%
> section
padding: 15px
display: none
// On blog posts, center text and title.
.h1.text-uppercase
+media-md
text-align: center
+media-xl
text-align: left
.node-details-description
+media-md
margin-left: auto
margin-right: auto
+media-xl
margin-left: 0
margin-right: auto
.featured-projects
+media-sm
padding-left: $spacer
padding-right: $spacer
.featured-project-card
+media-xl
.card-thumbnail
height: 100%
border-radius: $border-radius
.card-body
padding-left: 15px !important
padding-top: 0 !important
.homepage
.title-underline
padding-bottom: 2px

View File

@ -0,0 +1,37 @@
body.films
.page-content
@extend .text-white
background-color: $color-bg-dark-pages
h1
@extend .text-white
hr
background-color: lighten($color-bg-dark-pages, 20%)
.films-item
animation: fade-in-down .33s ease-out
animation-fill-mode: both
img
box-shadow: 0 10px 25px rgba($black, .5)
transition: box-shadow 800ms ease-in-out, transform 200ms ease-in-out
&:hover
box-shadow: 0 0 25px rgba($primary, .1), 0 0 50px rgba(white, .1)
transform: scale(1.05)
@for $i from 1 through 20
.films-item:nth-child(#{$i}n)
animation-delay: #{$i * 0.125}s
@keyframes fade-in-down
0%
opacity: 0
transform: translateY(-25px)
80%
opacity: 1
100%
transform: translateY(0)

View File

@ -0,0 +1,85 @@
.landing-home
.page-content
@extend .text-white
background-color: $color-bg-dark-pages
.jumbotron
+media-xs
background-position: top right
background-position: top center
padding-bottom: 16em
padding-top: 8em
.card
@extend .bg-transparent
@extend .text-white
.card-text
@extend .text-secondary
.node-details-description
@extend .mx-auto
color: #ddd
font-size: 1.3em
a
color: $color-primary
.btn-outline-primary
border-color: $color-text-light
color: $color-text-light
&:hover
border-color: transparent
.gallery
max-width: 1024px
.thumbnail
float: left
position: relative
width: 23%
padding-bottom: 23%
margin: 0.83%
overflow: hidden
transition: box-shadow 150ms ease-in-out
&:hover
box-shadow: 2px 6px 50px 0 rgba(black, .2)
.img-container
position: absolute
width: 100%
height: 100%
img
width: 300%
transform: translate(-20%,-10%)
@media screen and (max-width: 992px)
.thumbnail
width: 22%
padding-bottom: 22%
margin: 1.5%
@media screen and (max-width: 720px)
.thumbnail
width: 29%
padding-bottom: 29%
margin: 2.16%
@media screen and (max-width: 470px)
.thumbnail
width: 44%
padding-bottom: 44%
margin: 3%
.jumbotron
&.jumbotron-overlay-gradient-fade-to-gray
*
z-index: 1
&:after
background-color: transparent
background-image: linear-gradient(transparent 60%, $color-bg-dark-pages 100%)
display: block
visibility: visible

4
src/styles/_utils.sass Normal file
View File

@ -0,0 +1,4 @@
.list-first-new
li:first-child
span
@extend .new

View File

@ -0,0 +1,5 @@
$color-bg-dark-pages: #151515
// Alias for Blender Cloud logo used in project edit.
.pi-cloud
@extend .pi-blender-cloud

View File

@ -10,9 +10,6 @@
li a.navbar-item
color: $color-text
.navbar-container
+container-behavior
.navbar-toggle
border: 2px solid $color-text-dark-primary
color: $color-text
@ -267,22 +264,6 @@
+media-xs
margin-top: 20px
.navbar
.nav-item-sign-in
a.navbar-item
background-color: $color-primary
border: none
border-radius: 3px
color: white
height: auto
font-weight: bold
margin-top: 5px
margin-left: 10px
padding: 10px 20px
&:hover
background-color: lighten($color-primary, 10%)
box-shadow: none
.container.wide-on-sm
+media-sm
@ -331,12 +312,10 @@ section.pricing
transform: scale(1)
a.sign-up-now
+button($color-primary, 3px, true)
+button($color-primary, $btn-border-radius, true)
h3
font:
size: 1.8em
family: $font-body
font-size: 1.8em
padding-bottom: 0
margin: 25px 0 0 10px
@ -382,7 +361,7 @@ section.pricing
transform: translateX(-50%)
font-size: 1.2em
+button($color-primary, 3px)
+button($color-primary, $btn-border-radius)
padding: 5px 25px
white-space: nowrap
text-align: center

View File

@ -1,25 +1,108 @@
@import ../../../pillar/src/styles/_normalize
@import ../../../pillar/src/styles/_config
@import ../../../pillar/src/styles/_utils
// Bootstrap variables and utilities.
@import "../../../pillar/node_modules/bootstrap/scss/functions"
@import "../../../pillar/node_modules/bootstrap/scss/variables"
@import "../../../pillar/node_modules/bootstrap/scss/mixins"
/* Generic styles (comments, notifications, etc) come from base.css */
// Pillar variables and utilities.
@import "../../../pillar/src/styles/config"
@import "../../../pillar/src/styles/utils"
$pillar-font-path: "../../../../static/pillar/assets/font"
@import "../../../pillar/src/styles/font-pillar"
// Bootstrap components.
@import "../../../pillar/node_modules/bootstrap/scss/root"
@import "../../../pillar/node_modules/bootstrap/scss/reboot"
@import "../../../pillar/node_modules/bootstrap/scss/type"
@import "../../../pillar/node_modules/bootstrap/scss/images"
@import "../../../pillar/node_modules/bootstrap/scss/code"
@import "../../../pillar/node_modules/bootstrap/scss/grid"
@import "../../../pillar/node_modules/bootstrap/scss/tables"
@import "../../../pillar/node_modules/bootstrap/scss/forms"
@import "../../../pillar/node_modules/bootstrap/scss/buttons"
@import "../../../pillar/node_modules/bootstrap/scss/transitions"
@import "../../../pillar/node_modules/bootstrap/scss/dropdown"
@import "../../../pillar/node_modules/bootstrap/scss/button-group"
@import "../../../pillar/node_modules/bootstrap/scss/input-group"
@import "../../../pillar/node_modules/bootstrap/scss/custom-forms"
@import "../../../pillar/node_modules/bootstrap/scss/nav"
@import "../../../pillar/node_modules/bootstrap/scss/navbar"
@import "../../../pillar/node_modules/bootstrap/scss/card"
@import "../../../pillar/node_modules/bootstrap/scss/breadcrumb"
@import "../../../pillar/node_modules/bootstrap/scss/pagination"
@import "../../../pillar/node_modules/bootstrap/scss/badge"
@import "../../../pillar/node_modules/bootstrap/scss/jumbotron"
@import "../../../pillar/node_modules/bootstrap/scss/alert"
@import "../../../pillar/node_modules/bootstrap/scss/progress"
@import "../../../pillar/node_modules/bootstrap/scss/media"
@import "../../../pillar/node_modules/bootstrap/scss/list-group"
@import "../../../pillar/node_modules/bootstrap/scss/close"
@import "../../../pillar/node_modules/bootstrap/scss/modal"
@import "../../../pillar/node_modules/bootstrap/scss/tooltip"
@import "../../../pillar/node_modules/bootstrap/scss/popover"
@import "../../../pillar/node_modules/bootstrap/scss/carousel"
@import "../../../pillar/node_modules/bootstrap/scss/utilities"
@import "../../../pillar/node_modules/bootstrap/scss/print"
// Pillar components.
@import "../../../pillar/src/styles/apps_base"
@import "../../../pillar/src/styles/error"
@import "../../../pillar/src/styles/components/base"
@import "../../../pillar/src/styles/components/jumbotron"
@import "../../../pillar/src/styles/components/alerts"
@import "../../../pillar/src/styles/components/navbar"
@import "../../../pillar/src/styles/components/dropdown"
@import "../../../pillar/src/styles/components/footer"
@import "../../../pillar/src/styles/components/shortcode"
@import "../../../pillar/src/styles/components/statusbar"
@import "../../../pillar/src/styles/components/search"
@import "../../../pillar/src/styles/components/flyout"
@import "../../../pillar/src/styles/components/forms"
@import "../../../pillar/src/styles/components/inputs"
@import "../../../pillar/src/styles/components/buttons"
@import "../../../pillar/src/styles/components/popover"
@import "../../../pillar/src/styles/components/tooltip"
@import "../../../pillar/src/styles/components/checkbox"
@import "../../../pillar/src/styles/components/overlay"
@import "../../../pillar/src/styles/components/card"
@import "../../../pillar/src/styles/components/placeholder"
@import "../../../pillar/src/styles/components/timeline"
@import "../../../pillar/src/styles/comments"
@import "../../../pillar/src/styles/notifications"
/* Blender Cloud specific styles */
@import ../../../pillar/src/styles/_project
@import ../../../pillar/src/styles/_project-sharing
@import ../../../pillar/src/styles/_project-dashboard
@import ../../../pillar/src/styles/_user
@import _welcome
@import _homepage
@import _services
@import ../../../pillar/src/styles/_search
@import ../../../pillar/src/styles/_organizations
@import "../../../pillar/src/styles/_project"
@import "../../../pillar/src/styles/_project-sharing"
@import "../../../pillar/src/styles/_project-dashboard"
@import "../../../pillar/src/styles/_user"
@import "../../../pillar/src/styles/_search"
@import "../../../pillar/src/styles/_organizations"
/* services, about, etc */
@import ../../../pillar/src/styles/_pages
@import "../../../pillar/src/styles/_pages"
/* plugins are included here, don't include in base unless needed by other pillar apps */
@import ../../../pillar/src/styles/plugins/_jstree
@import ../../../pillar/src/styles/plugins/_js_select2
@import "../../../pillar/src/styles/plugins/_jstree"
@import "../../../pillar/src/styles/plugins/_js_select2"
/* CSS for pillar-font comes from fontello.com using static/assets/font/config.json */
@import variables
@import utils
@import welcome
@import services
@import about
@import homepage
@import list_films

View File

@ -0,0 +1,93 @@
// Bootstrap variables and utilities.
@import "../../../pillar/node_modules/bootstrap/scss/functions"
@import "../../../pillar/node_modules/bootstrap/scss/variables"
@import "../../../pillar/node_modules/bootstrap/scss/mixins"
// Pillar variables and utilities.
@import "../../../pillar/src/styles/_config"
@import "../../../pillar/src/styles/_utils"
$pillar-font-path: "../../../../static/pillar/assets/font"
@import "../../../pillar/src/styles/font-pillar"
// Bootstrap components.
@import "../../../pillar/node_modules/bootstrap/scss/root"
@import "../../../pillar/node_modules/bootstrap/scss/reboot"
@import "../../../pillar/node_modules/bootstrap/scss/type"
@import "../../../pillar/node_modules/bootstrap/scss/images"
@import "../../../pillar/node_modules/bootstrap/scss/code"
@import "../../../pillar/node_modules/bootstrap/scss/grid"
@import "../../../pillar/node_modules/bootstrap/scss/tables"
@import "../../../pillar/node_modules/bootstrap/scss/forms"
@import "../../../pillar/node_modules/bootstrap/scss/buttons"
@import "../../../pillar/node_modules/bootstrap/scss/transitions"
@import "../../../pillar/node_modules/bootstrap/scss/dropdown"
@import "../../../pillar/node_modules/bootstrap/scss/button-group"
@import "../../../pillar/node_modules/bootstrap/scss/input-group"
@import "../../../pillar/node_modules/bootstrap/scss/custom-forms"
@import "../../../pillar/node_modules/bootstrap/scss/nav"
@import "../../../pillar/node_modules/bootstrap/scss/navbar"
@import "../../../pillar/node_modules/bootstrap/scss/card"
@import "../../../pillar/node_modules/bootstrap/scss/breadcrumb"
@import "../../../pillar/node_modules/bootstrap/scss/pagination"
@import "../../../pillar/node_modules/bootstrap/scss/badge"
@import "../../../pillar/node_modules/bootstrap/scss/jumbotron"
@import "../../../pillar/node_modules/bootstrap/scss/alert"
@import "../../../pillar/node_modules/bootstrap/scss/progress"
@import "../../../pillar/node_modules/bootstrap/scss/media"
@import "../../../pillar/node_modules/bootstrap/scss/list-group"
@import "../../../pillar/node_modules/bootstrap/scss/close"
@import "../../../pillar/node_modules/bootstrap/scss/modal"
@import "../../../pillar/node_modules/bootstrap/scss/tooltip"
@import "../../../pillar/node_modules/bootstrap/scss/popover"
@import "../../../pillar/node_modules/bootstrap/scss/carousel"
@import "../../../pillar/node_modules/bootstrap/scss/utilities"
@import "../../../pillar/node_modules/bootstrap/scss/print"
// Pillar components.
@import "../../../pillar/src/styles/apps_base"
@import "../../../pillar/src/styles/components/base"
@import "../../../pillar/src/styles/components/jumbotron"
@import "../../../pillar/src/styles/components/alerts"
@import "../../../pillar/src/styles/components/navbar"
@import "../../../pillar/src/styles/components/dropdown"
@import "../../../pillar/src/styles/components/footer"
@import "../../../pillar/src/styles/components/shortcode"
@import "../../../pillar/src/styles/components/statusbar"
@import "../../../pillar/src/styles/components/search"
@import "../../../pillar/src/styles/components/placeholder"
@import "../../../pillar/src/styles/components/timeline"
@import "../../../pillar/src/styles/components/flyout"
@import "../../../pillar/src/styles/components/forms"
@import "../../../pillar/src/styles/components/inputs"
@import "../../../pillar/src/styles/components/buttons"
@import "../../../pillar/src/styles/components/popover"
@import "../../../pillar/src/styles/components/tooltip"
@import "../../../pillar/src/styles/components/checkbox"
@import "../../../pillar/src/styles/components/overlay"
@import "../../../pillar/src/styles/components/card"
@import "../../../pillar/src/styles/components/breadcrumbs"
@import "../../../pillar/src/styles/_notifications"
@import "../../../pillar/src/styles/_comments"
@import "../../../pillar/src/styles/_project"
@import "../../../pillar/src/styles/_project-sharing"
@import "../../../pillar/src/styles/_project-dashboard"
@import "../../../pillar/src/styles/_error"
@import "../../../pillar/src/styles/_search"
@import "../../../pillar/src/styles/plugins/_jstree"
@import "../../../pillar/src/styles/plugins/_js_select2"
// Cloud components.
@import variables
@import "_project-landing"

124
src/templates/_footer.pug Normal file
View File

@ -0,0 +1,124 @@
.footer-wrapper
| {% block footer_navigation %}
.footer-navigation
.container
.row
.col-md-4.col-xs-6
h4
a(href="{{ url_for('main.homepage') }}")
i.pi-blender-cloud-logo
p.pl-2.
Blender Cloud is the creative hub for your projects,
powered by Free and Open Source Software.
h5.d-flex
a.px-2(href="https://www.youtube.com/BlenderCloudOfficial",
title="Blender Cloud YouTube Channel")
i.pi-social-youtube
a.px-2(href="https://twitter.com/Blender_Cloud",
title="Follow us on Twitter")
i.pi-social-twitter
a.px-2(href="https://www.facebook.com/BlenderCloudOfficial/",
title="Follow us on Facebook")
i.pi-social-facebook
.col-md-2.col-xs-6
h7.font-weight-bold
a.d-block.pb-2(href="{{ url_for('cloud.learn') }}")
| TRAINING
ul.list-unstyled.mb-3
li
a(href="{{ url_for('cloud.courses') }}")
| Courses
li
a(href="{{ url_for('cloud.workshops') }}")
| Workshops
li
a(href="{{ url_for('cloud.production') }}")
span.new Production Lessons
h7.font-weight-bold
a.d-block.pb-2(href="{{ url_for('cloud.open_projects') }}")
| FILMS
.col-md-2.col-xs-6
h7.font-weight-bold
a.d-block.pb-2(href="{{ url_for('cloud.libraries') }}")
| LIBRARIES
ul.list-unstyled
li
a(href="{{ url_for('projects.view', project_url='hdri') }}",
title="HDRI Library")
| HDRIs
li
a(href="{{ url_for('projects.view', project_url='textures') }}",
title="Texture Library")
| Textures
li
a(href="{{ url_for('projects.view', project_url='characters') }}",
title="Characters")
| Characters
li
a(href="{{ url_for('projects.view', project_url='gallery') }}")
| Art Gallery
.col-md-2.col-xs-6
h7.font-weight-bold
a.d-block.pb-2(href="{{ url_for('cloud.services') }}")
| SERVICES
ul.list-unstyled
li
a(href="{{ url_for('cloud.services') }}#blender-cloud-add-on",
title="Blender Cloud add-on")
| Add-on
li
a(href="{{ url_for('projects.home_project') }}",
title="Your synced Blender settings")
| Blender Sync
li
a(href="/attract",
title="Production management")
| Attract
li
a(href="/flamenco",
title="Render management")
| Flamenco
li
a(href="{{ url_for('projects.home_project_shared_images')}}",
title="Share your images from within Blender")
| Image Sharing
.col-md-2.col-xs-6
h7.font-weight-bold
a.d-block.pb-2(href="{{ url_for('main.homepage') }}")
| CLOUD
ul.list-unstyled
li
a(href="{{ url_for('main.main_blog') }}",
title="Blender Cloud Blog")
| Blog
li
a(href="{{ url_for('cloud.terms_and_conditions') }}",
title="Terms and Conditions")
| Terms and Conditions
li
a(href="{{ url_for('cloud.privacy') }}",
title="Privacy")
| Privacy Policy
li.dropdown-divider
li
a(href="https://www.blender.org",
title="Home of Blender, the Free and Open Source creative suite")
| blender.org
| {% endblock footer_navigation %}
#hop(title="Be awesome in space")
i.pi-angle-up

View File

@ -0,0 +1,174 @@
include ../../../../pillar/src/templates/mixins/components
| {#
| Secondary Navigation Bars.
| #}
| {% macro navigation_homepage(title) %}
button.navbar-toggler(
type="button",
data-toggle="collapse",
data-target="#navigationLinks",
aria-controls="navigationLinks",
aria-expanded="false",
aria-label="Toggle navigation"
)
i.pi-blender-cloud
i.pi-angle-down
+nav-secondary(class="collapse navbar-collapse")#navigationLinks
+nav-secondary-link(
href="{{ url_for('main.homepage') }}")
i.pi-blender-cloud-logo
+nav-secondary-link(
href="{{ url_for('cloud.open_projects') }}",
class="{% if title == 'films' %}active{% endif %}")
span Films
+nav-secondary-link(
href="{{ url_for('cloud.learn') }}",
class="{% if title in ('learn', 'courses', 'workshops') %}active{% endif %}")
span Training
+nav-secondary-link(
href="{{ url_for('cloud.libraries') }}",
class="{% if title == 'libraries' %}active{% endif %}")
span Libraries
+nav-secondary-link(
href="{{ url_for('cloud.services') }}",
class="{% if title == 'services' %}active{% endif %}")
span Services
| {% endmacro %}
| {% macro navigation_home_project(title) %}
button.navbar-toggler(
type="button",
data-toggle="collapse",
data-target="#navigationLinks",
aria-controls="navigationLinks",
aria-expanded="false",
aria-label="Toggle navigation"
)
i.pi-blender-cloud
i.pi-angle-down
+nav-secondary(class="collapse navbar-collapse")#navigationLinks
+nav-secondary-link(
href="{{ url_for('main.homepage') }}")
i.pi-blender-cloud
+nav-secondary-link(
href="{{ url_for('projects.index') }}",
class="{% if title == 'dashboard' %}active{% endif %}")
i.pi-star.pr-2
span My Projects
| {% if current_user.has_organizations() %}
+nav-secondary-link(
href="{{ url_for('pillar.web.organizations.index') }}",
class="{% if title == 'organizations' %}active{% endif %}")
i.pi-users.pr-2
span My Organizations
| {% endif %}
+nav-secondary-link(
href="{{ url_for('projects.home_project_shared_images')}}",
class="{% if title == 'images' %}active{% endif %}")
i.pi-picture.pr-2
span Image Sharing
+nav-secondary-link(
href="{{ url_for('projects.home_project') }}",
class="{% if title == 'blender-sync' %}active{% endif %}")
i.pi-blender.pr-2
span Blender Sync
| {% endmacro %}
| {% macro navigation_project(project, navigation_links, extension_sidebar_links, title) %}
| {% if project.category == 'course' %}
| {% set category_url = url_for('cloud.courses') %}
| {% set category_title = 'Courses' %}
| {% elif project.category == 'workshop' %}
| {% set category_url = url_for('cloud.workshops') %}
| {% set category_title = 'Workshops' %}
| {% elif project.category == 'film' %}
| {% set category_url = url_for('cloud.open_projects') %}
| {% set category_title = 'Films' %}
| {% elif project.category == 'assets' %}
| {% set category_url = url_for('cloud.libraries') %}
| {% set category_title = 'Libraries' %}
| {% else %}
| {% set category_url = url_for('main.homepage') %}
| {% set category_title = project.category %}
| {% endif %}
button.navbar-toggler(
type="button",
data-toggle="collapse",
data-target="#navigationLinks",
aria-controls="navigationLinks",
aria-expanded="false",
aria-label="Toggle navigation"
)
i.pi-blender-cloud
i.pi-angle-down
+nav-secondary(class="collapse navbar-collapse")#navigationLinks
//- Blender Cloud logo.
+nav-secondary-link(
href="{{ url_for('main.homepage') }}")
i.pi-blender-cloud
//- Category (Films, Courses, etc).
+nav-secondary-link(
href="{{ category_url }}",
class="px-0")
span {{ category_title }}
li(class="nav-item px-1")
i.pi-angle-right
//- Project Name.
| {% if project.url != 'blender-cloud' %}
+nav-secondary-link(
class="font-weight-bold{% if title == 'default' %} active{% endif %} px-0",
href="{{url_for('projects.view', project_url=project.url, _external=True)}}")
span {{ project.name }}
| {% endif %}
//- Pages (Blog, Team, Awards, etc).
| {% for link in navigation_links %}
+nav-secondary-link(
href="{{ link['url'] }}",
class="{% if link['slug'] == title %}active{% endif %}")
span {{ link['label'] }}
| {% endfor %}
+nav-secondary-link(
href="{{ url_for('cloud.project_browse', project_url=project.url) }}",
title="Browse {{ project.name }}",
class="{% if title == 'project' %}active{% endif %}")
span Browse
//- Link to Production Tools (Attract, Flamenco, SVN, etc).
| {% if extension_sidebar_links %}
+nav-secondary()
li.nav-item.dropdown
a.nav-link.dropdown-toggle(
class="{% if title == 'production-tools' %}active{% endif %}"
href="#"
data-toggle="dropdown")
span Production Tools
i.pi-angle-down
ul.dropdown-menu
| {{ extension_sidebar_links }}
| {% endif %}
| {% endmacro %}

View File

@ -0,0 +1,17 @@
//- Opengraph/Twitter cards for social media.
| {% macro opengraph(title, description, url_image, url_page) %}
meta(property="og:type", content="website")
meta(property="og:url", content="{{ url_page }}")
meta(property="og:title", content="{{ title }} on Blender Cloud")
meta(name="twitter:title", content="{{ title }} on Blender Cloud")
meta(property="og:description", content="{{ description }}")
meta(name="twitter:description", content="{{ description }}")
| {% if url_image %}
meta(property="og:image", content="{{ url_image }}")
meta(name="twitter:image", content="{{ url_image }}")
| {% endif %}
| {% endmacro %}

View File

@ -24,6 +24,148 @@ style.
br
| unique set of learning and creative resources.
#page-content
section.team
.container
h2.
Meet a restless team of artists and developers <br/>
wants to share their work with you.
.people-container
.people-intro
h3 Blender Institute
span Amsterdam, The Netherlands
.row
.col-md-3
a.face(
href="https://twitter.com/tonroosendaal",
data-blenderhead='ton')
img(alt="Ton", src="{{ url_for('static', filename='assets/img/people/ton.jpg')}}")
.bio
h3 Ton Roosendaal
small CEO Blender Foundation. Producer Blender Institute
span The Netherlands
.col-md-3
a.face(
href="https://twitter.com/fsiddi",
data-blenderhead='francesco')
img(alt="Francesco", src="{{ url_for('static', filename='assets/img/people/francesco.jpg')}}")
.bio
h3 Francesco Siddi
small Pipeline Tools & Back-end Web Development
span Italy
.col-md-3
a.face(
href="https://twitter.com/hjalti",
data-blenderhead='hjalti')
img(alt="Hjalti", src="{{ url_for('static', filename='assets/img/people/hjalti.jpg')}}")
.bio
h3 Hjalti Hjálmarsson
small Director. Animation. Layout.
span Iceland
.col-md-3
a.face(
href="https://twitter.com/PabloVazquez_",
data-blenderhead='pablo')
img(alt="Pablo", src="{{ url_for('static', filename='assets/img/people/pablo.jpg')}}")
.bio
h3 Pablo Vázquez
small Lighting, Rendering. Front-end Web Development
span Argentina
.row
.col-md-3
a.face(
href="https://twitter.com/artificial3d",
data-blenderhead='andy')
img(alt="Andy", src="{{ url_for('static', filename='assets/img/people/andy.jpg')}}")
.bio
h3 Andy Goralczyk
small Shading, Lighting, Rendering, FX
span Germany
.col-md-3
a.face(
href="https://developer.blender.org/p/sergey/",
data-blenderhead='sergey')
img(alt="Sergey", src="{{ url_for('static', filename='assets/img/people/sergey.jpg')}}")
.bio
h3 Sergey Sharybin
small Blender & Cycles Core Developer
span Russia
.col-md-3
a.face(
href="https://twitter.com/sastuvel",
data-blenderhead='sybren')
img(alt="Sybren", src="{{ url_for('static', filename='assets/img/people/sybren.jpg')}}")
.bio
h3 Sybren Stüvel
small Blender Cloud Developer
span The Netherlands
.col-md-3
a.face(
href="https://twitter.com/dfelinto",
data-blenderhead='dalai')
img(alt="dalai", src="{{ url_for('static', filename='assets/img/people/dalai.jpg')}}")
.bio
h3 Dalai Felinto
small Blender Developer
span Brazil
.people-container.online
.people-intro
h3 Online Collaborators
span Contributing to Blender Cloud from all over the globe.
.row
.col-md-3
a.face(
href="https://twitter.com/davidrevoy",
data-blenderhead='david')
img(alt="David", src="{{ url_for('static', filename='assets/img/people/david.jpg')}}")
.bio
h3 David Revoy
small Illustrator & Concept Artist
span France
.col-md-3
a.face(
href="https://twitter.com/s_koenig",
data-blenderhead='sebastian')
img(alt="Sebastian", src="{{ url_for('static', filename='assets/img/people/sebastian.jpg')}}")
.bio
h3 Sebastian König
small VFX
span Germany
.col-md-3
a.face(
href="https://twitter.com/gleb_alexandrov",
data-blenderhead='gleb')
img(alt="Gleb", src="{{ url_for('static', filename='assets/img/people/gleb.jpg')}}")
.bio
h3 Gleb Alexandrov
small Lighting & Shading
span Belarus
.col-md-3
a.face(
href="https://twitter.com/the_mantissa",
data-blenderhead='midge')
img(alt="Midge", src="{{ url_for('static', filename='assets/img/people/midge.jpg')}}")
.bio
h3 Midge Sinnaeve
small Motion Graphics
span Belgium
.row
.col-md-3
a.face(
href="https://twitter.com/jpbouza",
data-blenderhead='jpbouza')
img(alt="Juan Pablo", src="{{ url_for('static', filename='assets/img/people/jpbouza.jpg')}}")
.bio
h3 Juan Pablo Bouza
small Rigging
span Argentina
section.page-card
h2 A bit of History
section.page-card
.page-card-side
h2.page-card-title
@ -33,11 +175,11 @@ style.
| First happy cloud video and crowdfunding for Cosmos Laundromat Pilot.
.page-card-side
a(href='https://gooseberry.blender.org/gooseberry-campaign-launched-we-need-10k-people-to-help/')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2014_03_09_sxsw.jpg') }}")
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2014_03_09_sxsw.jpg') }}", alt="SXSW")
section.page-card
.page-card-side
a(href='https://gooseberry.blender.org/gooseberry-campaign-launched-we-need-10k-people-to-help/')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2014_03_10_cosmos.jpg') }}")
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2014_03_10_cosmos.jpg') }}", alt="Cosmos Laundromat")
.page-card-side
h2.page-card-title
| Gooseberry | Cosmos Laundromat
@ -51,17 +193,17 @@ style.
small October 30th, 2015
.page-card-summary
| Introducing integrated blogs in Blender Cloud projects. Glass Half is the first project fully developed on the new Blender Cloud. It's also the first and only project to have share its
a(href='https://cloud.blender.org/p/glass-half/5627bb22f0e7220061109c9f') animation dailies
a(href='/p/glass-half/5627bb22f0e7220061109c9f') animation dailies
| ! But the biggest outcome from Glass Half was definitely
a(href='https://cloud.blender.org/p/glass-half/569d6044c379cf445461293e') Flexirig
a(href='/p/glass-half/569d6044c379cf445461293e') Flexirig
| .
.page-card-side
a(href='https://cloud.blender.org/p/glass-half/blog/glass-half-premiere')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_10_30_glass.jpg') }}")
a(href='/p/glass-half/blog/glass-half-premiere')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_10_30_glass.jpg') }}", alt="Glass Half")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/new-art-gallery-with-gleb-alexandrov')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_11_19_art.jpg') }}")
a(href='/blog/new-art-gallery-with-gleb-alexandrov')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_11_19_art.jpg') }}", alt="Art Gallery")
.page-card-side
h2.page-card-title
| Art Gallery
@ -76,12 +218,12 @@ style.
.page-card-summary
| With so much going on in the Cloud at at the studio. The Blender Institute Podcast was born! Sharing our daily studio work, Blender community news, and interacting with the awesome Blender Cloud subscribers.
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-blender-institute-podcast')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_11_24_bip.jpg') }}")
a(href='/blog/introducing-blender-institute-podcast')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_11_24_bip.jpg') }}", alt="Blender Institute Podcast")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/p/blenrig/blog/welcome-to-the-blenrig-project')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_12_01_blenrig.jpg') }}")
a(href='/p/blenrig/blog/welcome-to-the-blenrig-project')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_12_01_blenrig.jpg') }}", alt="Blenrig")
.page-card-side
h2.page-card-title
| Blenrig
@ -96,12 +238,12 @@ style.
.page-card-summary
| The biggest source for CC0/Public Domain textures on the interwebs goes live. First as beta, as a quick gift right before Xmas 2015!
.page-card-side
a(href='https://cloud.blender.org/blog/new-texture-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_12_23_textures.jpg') }}")
a(href='/blog/new-texture-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2015_12_23_textures.jpg') }}", alt="Texture Library")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/nraryew-the-character-lib')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_01_05_charlib.jpg') }}")
a(href='/blog/nraryew-the-character-lib')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_01_05_charlib.jpg') }}", alt="Character Library")
.page-card-side
h2.page-card-title
| Character Library
@ -120,12 +262,12 @@ style.
a(href='https://www.youtube.com/watch?v=kQH897V9bDg&list=PLI2TkLMzCSr_H6ppmzDtU0ut0RwxGvXjv') nicely edited Weekly video reports
| .
.page-card-side
a(href='https://cloud.blender.org/p/caminandes-3/blog/caminandes-llamigos')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_01_30_llamigos.jpg') }}")
a(href='/p/caminandes-3/blog/caminandes-llamigos')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_01_30_llamigos.jpg') }}", alt="Caminandes: Llamigos")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/welcome-sybren')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_03_01_sybren.jpg') }}")
a(href='/blog/welcome-sybren')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_03_01_sybren.jpg') }}", alt="Dr. Sybren!")
.page-card-side
h2.page-card-title
| Sybren
@ -140,12 +282,12 @@ style.
.page-card-summary
| Create your own private projects on Blender Cloud.
.page-card-side
a(href='https://cloud.blender.org/blog/welcome-sybren')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_03_projects.jpg') }}")
a(href='/blog/welcome-sybren')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_03_projects.jpg') }}", alt="Projects")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-project-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_09_projectsharing.jpg') }}")
a(href='/blog/introducing-project-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_09_projectsharing.jpg') }}", alt="Sharing")
.page-card-side
h2.page-card-title
| Project Sharing
@ -160,12 +302,12 @@ style.
.page-card-summary
| Browse the textures from within Blender!
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-project-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_11_addon.jpg') }}")
a(href='/blog/introducing-project-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_11_addon.jpg') }}", alt="Blender Cloud Add-on")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-private-texture-libraries')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_23_privtextures.jpg') }}")
a(href='/blog/introducing-private-texture-libraries')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_05_23_privtextures.jpg') }}", alt="Texture Libraries")
.page-card-side
h2.page-card-title
| Private Texture Libraries
@ -180,12 +322,12 @@ style.
.page-card-summary
| Sync your Blender preferences across multiple devices.
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-blender-sync')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_06_30_sync.jpg') }}")
a(href='/blog/introducing-blender-sync')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_06_30_sync.jpg') }}", alt="Blender Sync")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-image-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_07_14_image.jpg') }}")
a(href='/blog/introducing-image-sharing')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_07_14_image.jpg') }}", alt="Image Sharing")
.page-card-side
h2.page-card-title
| Image Sharing
@ -195,21 +337,21 @@ style.
section.page-card
.page-card-side
h2.page-card-title
a(href='https://cloud.blender.org/blog/introducing-the-hdri-library')
a(href='/blog/introducing-the-hdri-library')
| HDRI Library
small July 27th, 2016
.page-card-summary
| High-dynamic range images are now available on Blender Cloud! With their own special viewer. Also available via the Blender Cloud add-on.
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-the-hdri-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_07_27_hdri.jpg') }}")
a(href='/blog/introducing-the-hdri-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_07_27_hdri.jpg') }}", alt="HDRI Library")
section.page-card
.page-card-side
a(href='https://cloud.blender.org/blog/introducing-the-hdri-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_12_06_toon.jpg') }}")
a(href='/blog/introducing-the-hdri-library')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2016_12_06_toon.jpg') }}", alt="Hdri Library")
.page-card-side
h2.page-card-title
a(href='https://cloud.blender.org/blog/new-training-toon-character-workflow')
a(href='/blog/new-training-toon-character-workflow')
| Toon Character Workflow
small December 6th, 2016
.page-card-summary
@ -224,6 +366,9 @@ style.
| to all resources and training produced so far!
a.page-card-cta(href='https://store.blender.org/product/membership/') Subscribe
.page-card-side
a(href='https://cloud.blender.org/p/agent-327')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2017_03_10_agent.jpg') }}")
a(href='/p/agent-327')
img.img-responsive(src="{{ url_for('static_cloud', filename='img/2017_03_10_agent.jpg') }}", alt="Agent 327")
| {% endblock body%}

View File

@ -0,0 +1,7 @@
| {% extends 'layout.html' %}
| {% block body %}
.row.py-2
.col.text-center
h1 Design System goes here
| {% endblock %}

View File

@ -11,7 +11,7 @@ section
our team to create more Open Projects, training, services and of course to make Blender the best
CG pipeline in the world. You rock!
p.buttons
a.button(href="{{ abs_url('cloud.login', next='/') }}", target='_blank') Explore Now >
a.button(href="{{ abs_url('cloud.login', next='/') }}", target='_blank') Browse Now >
p.
Here is a quick guide to help you get started with Blender Cloud.

61
src/templates/films.pug Normal file
View File

@ -0,0 +1,61 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
include mixins/components
| {% set page_title = 'Films' %}
| {% set page_description = 'The iconic Blender Open Movies. Featuring all the production files, assets, artwork, and never-seen-before content.' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_agent327_01.jpg', _external=true) %}
| {% block page_title %}{{ page_title }}{% endblock %}
| {% block og %}
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.container.py-4
+category_list_header('{{ page_title }}', '{{ page_description }}')
.row.films-list
| {% for project in projects %}
| {% if (project.status == 'published') or (project.status == 'pending' and current_user.is_authenticated) and project._id != config.MAIN_PROJECT_ID %}
| {% set project_url = url_for('projects.view', project_url=project.url) %}
.films-item.col-md-4.col-sm-6.col-lg-3.my-5
.d-flex.flex-column.h-100
| {% if project.has_poster %} {# Check convenience attribute set in open_projects() #}
a.mx-auto(
href="{{ project_url }}",
tabindex='{{ loop.index }}')
img.rounded.w-100(
alt="{{ project.name }}",
src="{{ project.extension_props.cloud.poster.thumbnail('l', api=api) }}")
| {% endif %}
h4.pt-5.pb-3.text-center
a.text-white(href="{{ project_url }}")
| {{ project.name }}
| {% if project.summary %}
a.lead.text-secondary(href="{{ project_url }}")
small {{ project | markdowned('summary') }}
.d-flex.align-items-center.mt-auto
a.btn-link.mr-auto.my-3(href="{{ project_url }}")
| Browse #[i.pi-angle-right]
| {% endif %}
| {% if project.status == 'pending' and current_user.is_authenticated and current_user.has_role('admin') %}
p.text-danger
small Project Not Published
| {% endif %}
| {% endif %}
| {% endfor %}
| {% endblock body %}

View File

@ -1,12 +1,15 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_tabs %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_asset_list_item.html' import asset_list_item %}
| {% from 'nodes/custom/blog/_macros.html' import render_blog_post %}
include ../../../pillar/src/templates/mixins/components
| {% set title = 'homepage' %}
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="https://cloud.blender.org/")
meta(property="og:url", content="{{ request.url }}")
meta(property="og:title", content="Blender Cloud")
meta(name="twitter:title", content="Blender Cloud")
@ -18,238 +21,125 @@ meta(property="og:image", content="{% if main_project.picture_header %}{{ main_p
meta(name="twitter:image", content="{% if main_project.picture_header %}{{ main_project.picture_header.thumbnail('l', api=api) }}{% else %}{{ url_for('static', filename='assets/img/backgrounds/background_agent327_04.jpg')}}{% endif %}")
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
mixin featured_project_card(title, description, url, image)
a.featured-project-card.card.asset.my-2(href=url)
img.card-thumbnail(alt=title, src=image)
.card-body.py-2()
.card-title.mb-1.font-weight-bold
=title
.card-text
=description
.btn-link
| See more
i.pi-angle-right
mixin featured_projects()
section.py-2.mb-3
h6.title-underline
| Featured Projects
.featured-projects
+card-deck(3)&attributes(attributes)
+featured_project_card(
"MASTER SHADING NODES",
"Dive into a sea of nodes with this training by Simon Thommes.",
"/p/procedural-shading",
"{{ url_for('static', filename='assets/img/features/training_procedural_shading_01.jpg')}}")
+featured_project_card(
"IMPROVE YOUR RIGS",
"The ultimate guide to learn about this crucial step while rigging.",
"/p/weight-painting",
"{{ url_for('static', filename='assets/img/features/training_weight_painting_01.jpg')}}")
+featured_project_card(
"COFFEE RUN",
"This 2d-scroller-inspired short film will take you on the journey of a lifetime.",
"/p/coffee-run",
"{{ url_for('static', filename='assets/img/features/coffee_run_02.jpg')}}")
| {% block body %}
.dashboard-container
section.dashboard-main
.container-fluid.dashboard-container.imgs-fluid
.row.mt-3
.col-md-10.col-lg-9.col-xl-8.mx-auto
.d-xl-none
+featured_projects()
section.blog-stream
ul.blog-stream__list
| {% if latest_posts %}
| {% for node in latest_posts %}
| {{ render_blog_post(node) }}
| {% endfor %}
| {% else %}
li
.blog-stream__list-details
ul.meta
li.when No blog entries... yet!
| {% endif %}
+timeline()
.more
a(href="{{ url_for('main.main_blog') }}")
.d-block.text-center
a.d-inline-block.p-3.text-muted(href="{{ url_for('main.main_blog') }}")
| See All Blog Posts
a.feed(
href="{{ url_for('main.feeds_blogs') }}",
title="Blogs Feed",
data-toggle="tooltip",
data-placement="left")
i.pi-rss
a.d-inline-block.p-3.text-muted(
href="{{ url_for('main.feeds_blogs') }}",
title="Blogs Feed",
data-toggle="tooltip",
data-placement="left")
i.pi-rss
| RSS Feed
.col-md-10.col-lg-9.col-xl-4.mx-auto
.d-lg-none.d-xl-block
+featured_projects()(class="card-deck-vertical border-bottom pb-3")
section.dashboard-secondary
| {{ navigation_tabs(title) }}
section.py-2.border-bottom.mb-3
h6.title-underline
a.text-muted(href="{{ url_for('main.nodes_search_index') }}")
| Random Awesome
section.dashboard-in-production
h4 In Production
span.section-lead.
Check out these projects currently in production!
a.in-production-project(href="https://cloud.blender.org/p/spring/")
img(src="{{ url_for('static', filename='assets/img/projects/spring_450x150.jpg')}}")
p.
#[strong Spring] - A poetic short film about a mountain spirit and her wise little dog.
a.in-production-project(href="https://cloud.blender.org/p/hero/")
img(src="{{ url_for('static', filename='assets/img/projects/hero_450x150.jpg')}}")
p.
#[strong Hero] - A '2D' trailer-style movie focused on getting grease pencil
production ready for Blender 2.8.
section.stream
h4 Latest Assets
ul.activity-stream__list
| {% for n in activity_stream %}
li(
class="{{ n.node_type }} {{ n.properties.content_type }} {% if n.picture %}with-picture{% endif %}",
data-url="{{ n.url }}")
a.activity-stream__list-thumbnail(
class="{{ n.properties.content_type }}",
href="{{ n.url }}")
| {% if n.picture %}
img(src="{{ n.picture.thumbnail('m', api=api) }}")
| {% endif %}
.activity-stream__list-thumbnail-icon
| {% if n.node_type == 'asset' %}
| {% if n.properties.content_type == 'video' %}
i.pi-play
| {% elif n.properties.content_type == 'image' %}
i.pi-picture
| {% elif n.properties.content_type == 'file' %}
i.pi-file-archive
| {% else %}
i.pi-folder
| {% endif %}
| {% endif %}
.activity-stream__list-details
a.title(href="{{ n.url }}")
| {{ n.name }}
| {% if n.permissions.world %}
.ribbon
span free
| {% endif %}
ul.meta
| {% if not n.picture %}
li.when
a(href="{{ n.url }}", title="{{ n._created }}") {{ n._created | pretty_date_time }}
li.who {{ n.user.full_name }}
| {% endif %}
| {% if n.attached_to %}
li.where-parent
a(href="{{ n.attached_to.url }}") {{ n.attached_to.name }}
| {% endif %}
li.where-project
a.project(href="{{ url_for('projects.view', project_url=n.project.url) }}") {{ n.project.name }}
li.what
| {% if n.node_type == 'asset' %}
| {{ n.properties.content_type | undertitle }}
| {% endif %}
| {% if n.picture %}
ul.meta.extra
li.when
a(href="{{ n.url }}", title="{{ n._created }}") {{ n._created | pretty_date_time }}
li.who {{ n.user.full_name }}
| {% endif %}
| {% endfor %}
li.activity-stream__list-item.empty#activity-stream__empty
| No items to list.
section.random-asset
h4
a(href="/search") Explore the Cloud
span.section-lead Random selection of the best assets &amp; tutorials
ul.random-asset__list
| {% for n in random_featured %}
| {% if n.picture and loop.first %}
li.random-asset__list-item.project
| {% if n.project.picture_square %}
a.random-asset__list-thumbnail(
href="{{ n.project.url }}")
img.image(src="{{ n.project.picture_square.thumbnail('s', api=api) }}")
| {% if random_featured %}
+card-deck()(class='pl-3 random-featured')
| {% for child in random_featured %}
| {% if child.node_type not in ['comment'] %}
| {{ asset_list_item(child, current_user) }}
| {% endif %}
.random-asset__list-details
a.title(href="{{ n.project.url }}") {{ n.project.name }}
| {% if n.project.summary %}
ul.meta
li.what
a(href="{{ n.project.url }}") {{ n.project.summary }}
| {% endif %}
li.random-asset__list-item.featured
| {% if n.permissions.world %}
.ribbon
span free
| {% endif %}
a.random-asset__thumbnail(
href="{{ n.url }}",
class="{{ n.properties.content_type }}")
| {% if n.picture %}
img(src="{{ n.picture.thumbnail('l', api=api) }}")
| {% if n.properties.content_type == 'video' %}
i.pi-play
| {% endif %}
| {% endif %}
a.title(href="{{ n.url }}")
| {{ n.name }}
ul.meta
li.what
a(href="{{ n.url }}")
| {% if n.properties.content_type %}{{ n.properties.content_type | undertitle }}{% else %}Folder{% endif %}
li.where
a(href="{{ n.project.url }}")
| {{ n.project.name }}
| {% endfor %}
| {% else %}
li
| {% if n.permissions.world %}
.ribbon
span free
| {% endif %}
a.random-asset__list-thumbnail(
href="{{ n.url }}",
class="{{ n.properties.content_type }}")
| {% if n.picture %}
img.image(src="{{ n.picture.thumbnail('s', api=api) }}")
| {% else %}
| {% if n.properties.content_type == 'video' %}
i.pi-film-thick
| {% elif n.properties.content_type == 'image' %}
i.pi-picture
| {% elif n.properties.content_type == 'file' %}
i.pi-file-archive
| {% else %}
i.pi-folder
| {% endif %}
| {% endif %}
.random-asset__list-details
a.title(href="{{ n.url }}") {{ n.name }}
ul.meta
li.what
a(href="{{ n.url }}")
| {% if n.properties.content_type %}{{ n.properties.content_type }}{% else %}Folder{% endif %}
li.where
a(href="{{ n.project.url }}") {{ n.project.name }}
.card
.card-body
h6.card-title
| No random featured.
| {% endif %}
| {% endfor %}
section.py-3
h6.title-underline Latest Comments
section.comments
ul.list-unstyled.pt-2
| {% if latest_comments %}
| {% for n in latest_comments %}
li.pb-2.mb-2.border-bottom.text-truncate
h4 Latest Comments
a.js-comment-content.text-muted(href="{{ n.url }}")
| {{ n.properties.content | striptags | truncate(200) }}
ul
| {% if latest_comments %}
| {% for n in latest_comments %}
li(
class="{{ n.node_type }}",
data-url="{{ n.url }}")
a.comment-content(href="{{ n.url }}")
| {{ n.properties.content | striptags | truncate(200) }}
ul.meta
li.who {{ n.user.full_name }}
| {% if n.attached_to %}
li.where-parent
a(href="{{ n.attached_to.url }}") {{ n.attached_to.name }}
.d-flex.align-items-baseline
a.text-muted.text-truncate(href="{{ n.attached_to.url }}")
small.pr-2.font-weight-bold {{ n.project.name }}
small {{ n.attached_to.name }}
| {% endif %}
li.when
a(href="{{ n.url }}", title="{{ n._created }}")
| {{ n._created | pretty_date_time }}
| {% endfor %}
.d-flex.align-items-baseline
small.pr-2.font-weight-bold {{ n.user.full_name }}
| {% else %}
li.activity-stream__list-item.empty#activity-stream__empty
| No comments... yet!
a.text-muted(href="{{ n.url }}", title="{{ n._created }}")
small {{ n._created | pretty_date }}
| {% endfor %}
| {% else %}
span
| No comments... yet!
| {% endif %}
| {% endif %}
| {% endblock %}
@ -259,16 +149,10 @@ script.
$(function () {
/* cleanup mentions in comments */
$('.comment-content').each(function(){
$('.js-comment-content').each(function(){
$(this).text($(this).text().replace(/\*|\@|\<(.*?)\>/g, ''));
});
/* Click on the whole asset/comment row to go */
$('.activity-stream__list li, .comments ul li').click(function(e){
window.location.href = $(this).data('url');
$(this).addClass('active');
});
hopToTop(); // Display jump to top button
});
| {% endblock %}

View File

@ -1,9 +1,11 @@
include ../../../pillar/src/templates/mixins/components
doctype
html(lang="en")
head
meta(charset="utf-8")
title {% if self.page_title() %}{% block page_title %}{% endblock %} — {% endif %}Blender Cloud
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(name="viewport", content="width=device-width, initial-scale=1, shrink-to-fit=no")
meta(name="description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
meta(name="author", content="Blender Institute")
meta(name="theme-color", content="#3e92aa")
@ -19,7 +21,7 @@ html(lang="en")
| {% block og %}
meta(property="og:title", content="Blender Cloud")
meta(property="og:url", content="https://cloud.blender.org")
meta(property="og:url", content="{{ request.url }}")
meta(property="og:type", content="website")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_gleb_locomotive.jpg')}}")
meta(property="og:description", content="Blender Cloud is a web based service developed by Blender Institute that allows people to access the training videos and all the data from the open projects.")
@ -29,282 +31,103 @@ html(lang="en")
meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_gleb_locomotive.jpg')}}")
| {% endblock og %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery-3.1.0.min.js', v=9112017)}}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js', v=9112017)}}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/js.cookie-2.0.3.min.js', v=9112017)}}")
script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js') }}")
script.
| {% if current_user.has_cap('subscriber') %}
| {# Only load if we can comment (for converting markdown as-we-type) #}
script(src="{{ url_for('static_pillar', filename='assets/js/markdown.min.js', v=9112017) }}")
pillar.utils.initCurrentUser({{ current_user | json | safe }});
script(src="{{ url_for('static_pillar', filename='assets/js/timeline.min.js') }}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js')}}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/js.cookie-2.0.3.min.js')}}")
| {% if current_user.is_authenticated %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/clipboard.min.js')}}")
| {% endif %}
script(src="{{ url_for('static_pillar', filename='assets/js/tutti.min.js', v=9112017) }}")
link(href="{{ url_for('static', filename='assets/img/favicon.png') }}", rel="shortcut icon")
link(href="{{ url_for('static', filename='assets/img/apple-touch-icon-precomposed.png') }}", rel="icon apple-touch-icon-precomposed", sizes="192x192")
link(href="{{ url_for('static_pillar', filename='assets/css/vendor/bootstrap.min.css', v=9112017) }}", rel="stylesheet")
link(href="{{ url_for('static', filename='assets/google-font-roboto/roboto.css', v=9112017) }}", rel="stylesheet")
| {% block head %}{% endblock %}
| {% block css %}
link(href="{{ url_for('static_pillar', filename='assets/css/font-pillar.css', v=9112017) }}", rel="stylesheet")
link(href="{{ url_for('static_pillar', filename='assets/css/base.css', v=9112017) }}", rel="stylesheet")
| {% if title == 'blog' %}
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css', v=9112017) }}", rel="stylesheet")
link(href="{{ url_for('static_pillar', filename='assets/css/blog.css') }}", rel="stylesheet")
| {% else %}
link(href="{{ url_for('static', filename='cloud/assets/css/main.css', v=9112017) }}", rel="stylesheet")
link(href="{{ url_for('static_cloud', filename='assets/css/main.css') }}", rel="stylesheet")
| {% endif %}
| {% endblock css %}
| {% if not title %}{% set title="default" %}{% endif %}
body(class="{{ title }} {{'project' if project and project.url != 'blender-cloud'}} {% block bodyclasses %}{% endblock %}"
"{% block bodyattrs %}{% endblock %}"
)
| {% with messages = get_flashed_messages(with_categories=True) %}
| {% if messages or (config.UI_ANNOUNCEMENT_NON_SUBSCRIBERS and not current_user.has_cap('subscriber')) %}
| {% if config.UI_ANNOUNCEMENT_NON_SUBSCRIBERS %}
.alert.d-flex.justify-content-center(
role="alert",
class="alert-{{ config.UI_ANNOUNCEMENT_NON_SUBSCRIBERS['category'] }}")
i.pr-2(class="{{ config.UI_ANNOUNCEMENT_NON_SUBSCRIBERS['icon'] }}")
| {{ config.UI_ANNOUNCEMENT_NON_SUBSCRIBERS['message'] | markdown }}
| {% endif %}
| {% if not title %}{% set title="default" %}{% endif %}
| {% for (category, message) in messages %}
.alert.d-flex.justify-content-center(
role="alert",
class="alert-{{ category }}")
i.alert-icon(class="{{ category }}")
span {{ message }}
button.close(type="button", data-dismiss="alert")
i.pi-cancel
| {% endfor %}
| {% endif %}
| {% endwith %}
body(class="{{ title }}")
.container-page
| {% with messages = get_flashed_messages(with_categories=True) %}
| {% if messages %}
nav.navbar.navbar-expand-md.fixed-top.bg-white
| {% block navigation_tabs %}
+nav-secondary(class="collapse navbar-collapse")#navigationLinks
+nav-secondary-link(
href="{{ url_for('main.homepage') }}")
i.pi-blender-cloud-logo
| {% endblock navigation_tabs %}
+nav-secondary()(class="m-auto keep-when-overlay")
div.nav-item.quick-search.qs-input#qs-input
| {% for (category, message) in messages %}
.alert(role="alert", class="alert-{{ category }}")
i.alert-icon(class="{{ category }}")
span {{ message }}
button.close(type="button", data-dismiss="alert")
i.pi-cancel
| {% endfor %}
+nav-secondary()(class="ml-auto")
| {% if node and node.properties and node.properties.category %}
| {% set category = node.properties.category %}
| {% else %}
| {% set category = title %}
| {% endif %}
li.nav-item.quick-search.cursor-pointer.px-3.pi-search#qs-toggle
| {% endif %}
| {% endwith %}
| {% block navigation_sections %}
| {% endblock navigation_sections %}
nav.navbar
.navbar-container
header.navbar-header
button.navbar-toggle(data-target=".navbar-collapse", data-toggle="collapse", type="button")
span.sr-only Toggle navigation
i.pi-menu
a.navbar-brand(
href="{{ url_for('main.homepage') }}",
title="Blender Cloud")
span.app-logo
i.pi-blender-cloud
| {% block navigation_user %}
| {% include 'menus/notifications.html' %}
| {% include 'menus/user.html' %}
| {% endblock navigation_user %}
| {% block navigation_search %}
.search-input
input#cloud-search(
type="text",
placeholder="Search assets, tutorials...")
i.search-icon.pi-search
| {% endblock navigation_search %}
| {% if current_user.is_anonymous %}
li
a.btn.btn-sm.btn-primary.px-4.mx-1(
href="https://store.blender.org/product/membership/",
title="Sign up") Sign up
| {% endif %}
nav.collapse.navbar-collapse
ul.nav.navbar-nav.navbar-right
| {% if node and node.properties and node.properties.category %}
| {% set category = node.properties.category %}
| {% else %}
| {% set category = title %}
| {% endif %}
.loading-bar
| {% block navigation_sections %}
li
a.navbar-item(
href="{{ url_for('main.main_blog') }}",
title="Blender Cloud Blog",
data-toggle="tooltip",
data-placement="bottom",
class="{% if category == 'blog' %}active{% endif %}")
span Blog
li(class="dropdown libraries")
a.navbar-item.dropdown-toggle(
href="",
data-toggle="dropdown",
title="Libraries")
span Libraries
i.pi-angle-down
ul.dropdown-menu
li
a.navbar-item(
href="{{ url_for('projects.view', project_url='hdri') }}",
title="HDRI Library",
data-toggle="tooltip",
data-placement="left")
i.pi-globe
| HDRI
li
a.navbar-item(
href="{{ url_for('projects.view', project_url='textures') }}",
title="Textures Library",
data-toggle="tooltip",
data-placement="left")
i.pi-folder-texture
| Textures
li
a.navbar-item(
href="{{ url_for('projects.view', project_url='characters') }}",
title="Character Library",
data-toggle="tooltip",
data-placement="left")
i.pi-character
| Characters
li(class="dropdown libraries")
a.navbar-item.dropdown-toggle(
href="",
data-toggle="dropdown",
title="Training")
span Training
i.pi-angle-down
ul.dropdown-menu
li
a.navbar-item(
href="{{ url_for('cloud.courses') }}",
title="Courses",
data-toggle="tooltip",
data-placement="left")
i.pi-graduation-cap
| Courses
li
a.navbar-item(
href="{{ url_for('cloud.workshops') }}",
title="Workshops",
data-toggle="tooltip",
data-placement="left")
i.pi-lightbulb
| Workshops
li
a.navbar-item(
href="{{ url_for('projects.view', project_url='gallery') }}",
title="Curated artwork collection",
data-toggle="tooltip",
data-placement="left")
i.pi-image
| Art Gallery
li
a.navbar-item(
href="{{ url_for('cloud.open_projects') }}",
title="Browse all the Open Projects",
data-toggle="tooltip",
data-placement="bottom",
class="{% if category in ['open-projects', 'film'] %}active{% endif %}")
span Open Projects
li
a.navbar-item(
href="{{ url_for('cloud.services') }}",
title="Blender Cloud Services",
data-toggle="tooltip",
data-placement="bottom",
class="{% if category == 'services' %}active{% endif %}")
span Services
| {% endblock navigation_sections %}
| {% if current_user.is_anonymous %}
li
a.navbar-item(
href="https://store.blender.org/product/membership/",
title="Sign up") Sign up
| {% endif %}
| {% block navigation_user %}
| {% include 'menus/notifications.html' %}
| {% include 'menus/user.html' %}
| {% endblock navigation_user %}
.page-content
#search-overlay
| {% block page_overlay %}
#page-overlay
| {% endblock page_overlay %}
.page-body
| {% block body %}{% endblock %}
.page-content
.quick-search.container-fluid.m-auto.p-5#search-overlay
ul.qs-loading.text-center
i.h1.pi-spin.spinner
h2 Loading
| {% block page_overlay %}
#page-overlay
| {% endblock page_overlay %}
.page-body
| {% block body %}{% endblock %}
| {% block footer_container %}
#footer-container
| {% block footer_navigation %}
#footer-navigation
.container
.row
.col-md-4.col-xs-6
.footer-support
h4 Support & Feedback
p.
Let us know what you think or if you have any issues
just write to cloudsupport at blender dot org
.col-md-2.col-xs-6
ul.footer-social
li
a(href="https://www.facebook.com/BlenderCloudOfficial/",
title="Follow us on Facebook")
i.pi-social-facebook
li
a(href="https://twitter.com/Blender_Cloud",
title="Follow us on Twitter")
i.pi-social-twitter
.col-md-2.col-xs-6
h4
a(href="{{ url_for('main.homepage') }}")
| Blender Cloud
ul.footer-links
li
a(href="{{ url_for('main.main_blog') }}",
title="Blender Cloud Blog")
| Blog
li
a(href="{{ url_for('cloud.services') }}",
title="Blender Cloud Services")
| Services
li
a(href="{{ url_for('cloud.about') }}",
title="About Blender Cloud")
| About
li
a(href="{{ url_for('cloud.terms_and_conditions') }}",
title="Terms and Conditions")
| Terms and Conditions
li
a(href="{{ url_for('cloud.privacy') }}",
title="Privacy")
| Privacy
.col-md-2.col-xs-6
h4
a(href="https://www.blender.org",
title="Blender official Website")
| Blender
ul.footer-links
li
a(href="https://www.blender.org",
title="Blender official Website")
| Blender.org
li
a(href="https://store.blender.org/",
title="The official Blender Store")
| Blender Store
.col-md-2.col-xs-6.special
| With the support of the <br/> MEDIA Programme of the European Union<br/><br/>
img(alt="MEDIA Programme of the European Union",
src="https://gooseberry.blender.org/wp-content/uploads/2014/01/media_programme.png")
| {% endblock footer_navigation %}
| {% block footer %}
footer.container
#hop(title="Be awesome in space")
i.pi-angle-up
| {% endblock footer %}
| {% include '_footer.html' %}
| {% endblock footer_container %}
#notification-pop(data-url="", data-read-toggle="")
@ -316,11 +139,19 @@ html(lang="en")
.nc-text
span.nc-date
a(href="")
noscript
link(href='//fonts.googleapis.com/css?family=Roboto:300,400', rel='stylesheet', type='text/css')
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.bootstrap-3.3.7.min.js', v=9112017) }}")
| {% if current_user.is_authenticated %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typewatch-3.0.0.min.js') }}")
script.
// When sending an AJAX request, always add the X-CSRFToken header to it.
var csrf_token = "{{ csrf_token() }}";
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type) && !this.crossDomain) {
xhr.setRequestHeader("X-CSRFToken", csrf_token);
}
}
});
| {% endif %}
script.
$(document).ready(function() {
@ -332,13 +163,43 @@ html(lang="en")
{% endif %}
});
// Enable all tooltips.
if (typeof $().tooltip != 'undefined'){
$('[data-toggle="tooltip"]').tooltip({'delay' : {'show': 0, 'hide': 0}});
}
if(typeof($.fn.popover) != 'undefined'){
$('[data-toggle="popover"]').popover();
// Enable Quick Search
let searches = {
{% if project and not project.is_private %}
project: {
name: 'Project',
uiUrl: '{{ url_for("projects.search", project_url=project.url)}}',
apiUrl: '/api/newsearch/multisearch',
searchParams: [
{name: 'Assets', params: {project: '{{ project._id }}', node_type: 'asset'}},
{name: 'Blog', params: {project: '{{ project._id }}', node_type: 'post'}},
{name: 'Groups', params: {project: '{{ project._id }}', node_type: 'group'}},
]
},
{% endif %}
cloud: {
name: 'Cloud',
uiUrl: '/search',
apiUrl: '/api/newsearch/multisearch',
searchParams: [
{name: 'Assets', params: {node_type: 'asset'}},
{name: 'Blog', params: {node_type: 'post'}},
{name: 'Groups', params: {node_type: 'group'}},
]
},
}
$('#qs-toggle').quickSearch({
resultTarget: '#search-overlay',
inputTarget: '#qs-input',
searches: searches,
});
| {% block footer_scripts_pre %}{% endblock %}
| {% block footer_scripts %}{% endblock %}

103
src/templates/learn.pug Normal file
View File

@ -0,0 +1,103 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
include mixins/components
| {% set title = 'learn' %}
| {% set page_title = 'Learn' %}
| {% set page_description = 'Production quality training by Blender professionals.' %}
| {% set page_header_image = url_for('static', filename='assets/img/features/training_minecraft_animation.jpg', _external=true) %}
| {% block page_title %}{{ page_title }}{% endblock %}
| {% block og %}
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.container.py-4
+category_list_header('{{ page_title }}', '{{ page_description }}')
+category_list_item(
'COURSES',
'In-depth training for mastering every corner in Blender.',
"{{ url_for('cloud.courses') }}",
"{{ url_for('static', filename='assets/img/features/training_animation_fundamentals_01.jpg')}}",
'/p/animation-fundamentals')
ul.list-unstyled.mt-3.mb-0.column-count-2.list-first-new
-
var projects = {
'Procedural Shading': '/p/procedural-shading',
'Weight Painting':'/p/weight-painting',
'Animation Fundamentals':'/p/animation-fundamentals',
'Stylized Character Workflow':'/p/stylized-character-workflow',
'Scripting for Artists':'/p/scripting-for-artists'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('cloud.courses') }}")
| See all Courses
i.pi-angle-right
hr.mb-4
+category_list_item(
'WORKSHOPS',
'Enter the artist workshop and learn by example.',
"{{ url_for('cloud.workshops') }}",
"{{ url_for('static', filename='assets/img/features/training_anglerfish_01.jpg')}}",
'/p/anglerfish')
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Anglerfish':'/p/anglerfish',
'Speed Sculpting':'/p/speed-sculpting',
'Minecraft Animation':'/p/minecraft-animation-workshop',
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('cloud.workshops') }}")
| See all Workshops
i.pi-angle-right
hr.mb-4
+category_list_item(
'PRODUCTION LESSONS',
'Tips and tricks by the Blender Open Movies crew.',
"{{ url_for('cloud.production') }}",
"{{ url_for('static', filename='assets/img/features/open_movies_spring_03.jpg')}}")
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Walk-throughs':'/production#walk-through',
'Animation Tips':'/production#animation',
'Character Pipeline':'/production#character-pipeline'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('cloud.production') }}")
| See all Production Lessons
i.pi-angle-right
| {% endblock body %}

128
src/templates/libraries.pug Normal file
View File

@ -0,0 +1,128 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
include mixins/components
| {% set title = 'libraries' %}
| {% set page_title = 'Libraries' %}
| {% set page_description = 'Download 1000s of files and assets.' %}
| {% set page_header_image = url_for('static', filename='assets/img/features/characters_01.jpg', _external=true) %}
| {% block page_title %}{{ page_title }}{% endblock %}
| {% block og %}
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.container.py-4
+category_list_header('LIBRARIES', null)
.lead
| Browse Textures and HDRI within Blender with our #[a(href="/services#blender-cloud-add-on") Blender Cloud add-on!]
+category_list_item(
'HDR IMAGES',
'Up to 16K and 24 EVs HDRI to enhance your renders.',
"{{ url_for('projects.view', project_url='hdri') }}",
"{{ url_for('static', filename='assets/img/features/hdri_02.jpg')}}")
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Myanmar' : '/p/hdri/58d824e588ac8f2107b314e1',
'Indoor' : '/p/hdri/57976801c379cf39de54cc5d',
'Skies' : '/p/hdri/585ab5521f47427f2f7b3604'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('projects.view', project_url='hdri') }}")
| See all HDRIs
i.pi-angle-right
hr.mb-4
+category_list_item(
'TEXTURES',
'More than 1500 public-domain textures.',
"{{ url_for('projects.view', project_url='textures') }}",
"{{ url_for('static', filename='assets/img/features/textures_02.jpg')}}")
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Metal' : '/p/textures/5677e2f4c379cf0007b31fe1',
'Wood' : '/p/textures/567800d4c379cf211051a439',
'Bricks': '/p/textures/5672df9fc379cf0007b3198b'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('projects.view', project_url='textures') }}")
| See all Textures
i.pi-angle-right
hr.mb-4
+category_list_item(
'ART GALLERY',
'Dive into the most interesting .blend files from the community.',
"{{ url_for('projects.view', project_url='gallery') }}",
"{{ url_for('static', filename='assets/img/features/gallery_01.jpg')}}",
'/p/gallery/564a15bec379cf089a7ad514')
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Grease Pencil Files' : '/p/gallery/5b642e25bf419c1042056fc6',
'Gleb Alexandrov' : '/p/gallery/57907fb8c379cf33d47a098d',
'Midge "Mantissa" Sinnaeve' : '/p/gallery/5800d64ee5e20f084523a059'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('projects.view', project_url='gallery') }}")
| Visit the Gallery
i.pi-angle-right
hr.mb-4
+category_list_item(
'CHARACTERS',
'Production quality characters ready to animate and render.',
"{{ url_for('projects.view', project_url='characters') }}",
"{{ url_for('static', filename='assets/img/features/characters_01.jpg')}}")
ul.list-unstyled.mt-3.mb-0.column-count-2
-
var projects = {
'Vincent' : '/p/characters/5718a967c379cf04929a4247',
'Big Buck Bunny' : '/p/characters/56cb2785c379cf0079716c19',
'Min (Glass Half)' : '/p/characters/5672d39bc379cf0007b31911'
};
each url, title in projects
li
a.d-block.py-1.text-primary(href=url)
span=title
li
a.d-block.py-1.text-primary(href="{{ url_for('projects.view', project_url='characters') }}")
| See all Characters
i.pi-angle-right
| {% endblock body %}

View File

@ -9,11 +9,8 @@
| {% endif %}
| {% block menu_avatar %}
a.navbar-item.dropdown-toggle(href="#", data-toggle="dropdown", title="{{ current_user.email }}")
img.gravatar(
src="{{ current_user.gravatar }}",
class="{{ subscription }}",
alt="Avatar")
a.navbar-item.dropdown-toggle(href="{{ url_for('settings.profile') }}", data-toggle="dropdown")
current-user-avatar
.special(class="{{ subscription }}")
| {% if subscription == 'subscriber' %}
i.pi-check
@ -22,32 +19,34 @@ a.navbar-item.dropdown-toggle(href="#", data-toggle="dropdown", title="{{ curren
| {% else %}
i.pi-attention
| {% endif %}
script.
new Vue({el: 'current-user-avatar'})
| {% endblock menu_avatar %}
| {% block menu_list %}
li.subscription-status(class="{{ subscription }}")
| {% if subscription == 'subscriber' %}
a.navbar-item(
href="{{url_for('settings.billing')}}"
title="View subscription info")
a.navbar-item.pt-2.pl-2.pr-3(
href="{{ url_for('settings.billing') }}"
title="View subscription info")
i.pi-grin
span Your subscription is active!
span.subitem Your subscription is active!
| {% elif subscription == 'demo' %}
a.navbar-item(
href="{{url_for('settings.billing')}}"
title="View subscription info")
a.navbar-item.pt-2.pl-2.pr-3(
href="{{url_for('settings.billing')}}"
title="View subscription info")
i.pi-heart-filled
span You have a free account.
span.subitem You have a free account.
| {% elif current_user.has_cap('can-renew-subscription') %}
a.navbar-item(target='_blank', href="/renew", title="Renew subscription")
a.navbar-item.pt-2.pl-2.pr-3(target='_blank', href="/renew", title="Renew subscription")
i.pi-heart
span.info Your subscription is not active.
span.renew Click here to renew.
| {% else %}
a.navbar-item(
href="https://store.blender.org/product/membership/"
title="Renew subscription")
href="https://store.blender.org/product/membership/"
title="Renew subscription")
i.pi-unhappy
span.info Your subscription is not active.
span.renew Click here to renew.
@ -56,9 +55,9 @@ li.subscription-status(class="{{ subscription }}")
| {{ super() }}
li
a.navbar-item(
href="{{ url_for('settings.billing') }}"
title="Billing")
a.navbar-item.px-2(
href="{{ url_for('settings.billing') }}"
title="Billing")
i.pi-credit-card
| Subscription
| {% endblock menu_list %}

View File

@ -0,0 +1,33 @@
//- Category listing (Learn, Libraries, etc).
//- Header
mixin category_list_header(title, text)
.row.pt-2.pb-3.mb-4.border-bottom&attributes(attributes)
.col-md-9
h1.py-2.font-weight-bold
=title
.lead
=text
if block
block
//- List Item
mixin category_list_item(title, text, url, image, image_link_url)
.row.pb-2.my-2&attributes(attributes)
.col-md-8
a(href=url, title=title)
h3.font-weight-bold.text-muted
=title
.lead
=text
if block
block
.col-md-4
if image_link_url
- var url = image_link_url
a(href=url, title=title)
img.img-fluid.rounded(alt=title, src=image)

View File

@ -0,0 +1,171 @@
include ../../../../../../pillar/src/templates/mixins/components
| {% macro render_blog_post(node, project=None, pages=None) %}
.expand-image-links.imgs-fluid
| {% if node.picture %}
+jumbotron(
"{{ node.name }}",
"{{ node._created | pretty_date }}{% if node.user.full_name %} · {{ node.user.full_name }}{% endif %}{% if node.properties.status != 'published' %} · {{ node.properties.status }}{% endif %}",
"{{ node.picture.thumbnail('h', api=api) }}",
"{{ node.url }}")(
class="jumbotron-overlay")
| {% else %}
.pt-5.text-center.text-muted
h2.pb-2
a.text-muted(href="{{ node.url }}")
| {{ node.name }}
ul.d-flex.list-unstyled.justify-content-center
| {% if node.properties.status != 'published' %}
li.mr-3(title="Status {{ node.properties.status }}")
span.badge.badge-danger {{ node.properties.status | undertitle }}
| {% endif %}
| {% if node.project.name %}
li.pr-2 {{ node.project.name }}
| {% endif %}
| {% if node.user.full_name %}
li.pr-2
| {{ node.user.full_name }}
| {% endif %}
li
a.px-2.text-muted(href="{{ node.url }}",
title="Updated {{ node._updated | pretty_date }}")
| {{ node._created | pretty_date }}
li
a.px-2(href="{{ node.url }}#comments")
| Leave a comment
| {% endif %}
.node-details-description.mx-auto.py-5
| {{ node.properties | markdowned('content') }}
hr.my-4
comments-tree#comments-embed.justify-content-center.mx-auto(
parent-id="{{ node._id }}"
read-only=false
)
| {% endmacro %}
//- ******************************************************* -//
| {% macro render_blog_list_item(node) %}
a.card.asset.card-image-fade(
href="{{ node.url }}")
.card-thumbnail
| {% if node.picture %}
img.card-img-top(src="{{ node.picture.thumbnail('m', api=api) }}", alt="{{ node.name }}")
| {% else %}
.card-img-top
i.pi-document-text
| {% endif %}
.card-body.py-2.d-flex.flex-column
.card-title.mb-1.font-weight-bold
| {{ node.name }}
ul.card-text.list-unstyled.d-flex.text-black-50.mt-auto
li.pr-2 {{ node.user.full_name }}
li {{ node._created | pretty_date }}
| {% if node.properties.status != 'published' %}
li.text-info.font-weight-bold {{ node.properties.status}}
| {% endif %}
| {% endmacro %}
//- ******************************************************* -//
| {% macro render_blog_index(current_post, project, posts, can_create_blog_posts, api, more_posts_available, posts_meta, pages=None) %}
| {% if can_create_blog_posts or (current_post and current_post.has_method('PUT')) %}
+nav-secondary(class="bg-light border-bottom")
| {% if can_create_blog_posts %}
+nav-secondary-link(href="{{url_for('nodes.posts_create', project_id=project._id)}}")
i.pi-plus.pr-2
span Create New Blog Post
| {% endif %}
| {% if (current_post and current_post.has_method('PUT')) %}
+nav-secondary-link(href="{{url_for('nodes.edit', node_id=current_post._id)}}")
i.pi-edit.pr-2
span Edit Post
| {% endif %}
| {% endif %}
| {% if posts %}
| {{ render_blog_post(current_post, project=project, pages=pages) }}
.container
.pt-4.text-center
h5
| {% if more_posts_available %}
a.text-muted.py-3.d-block(href="{{ project.blog_archive_url }}")
| More from {{ project.name }} blog
| {% else %}
| More from {{ project.name }} blog
| {% endif %}
+card-deck(class="px-2")
| {% for node in posts %}
| {# Skip listing the current post #}
| {% if node._id != current_post._id %}
| {{ render_blog_list_item(node) }}
| {% endif %}
| {% endfor %}
| {% if more_posts_available %}
a.d-block.pb-4.text-center(href="{{ project.blog_archive_url }}")
| {{posts_meta.total - posts|length}} more blog posts over here
i.pi-angle-right
| {% endif %}
| {% else %}
.text-center
p No posts... yet!
| {% endif %} {# posts #}
| {% endmacro %}
//- Macro for rendering the navigation buttons for prev/next pages -//
| {% macro render_archive_pagination(project) %}
.d-flex.justify-content-center
| {% if project.blog_archive_prev %}
a.px-5.py-3(
href="{{ project.blog_archive_prev }}", rel="prev")
i.pi-angle-left
| Previous page
| {% else %}
span.px-5.py-3.text-black-50
i.pi-angle-left
| Previous page
| {% endif %}
a.px-5.py-3(
href="{{ url_for('main.project_blog', project_url=project.url) }}")
| Blog Index
| {% if project.blog_archive_next %}
a.px-5.py-3(
href="{{ project.blog_archive_next }}", rel="next")
| Next page
i.pi-angle-right
| {% else %}
span.px-5.py-3.text-black-50
| Next page
i.pi-angle-right
| {% endif %}
| {% endmacro %}
| {% macro render_archive(project, posts, posts_meta) %}
| {{ render_archive_pagination(project) }}
+card-deck(class="px-2")
| {% for node in posts %}
| {{ render_blog_list_item(node) }}
| {% endfor %}
| {{ render_archive_pagination(project) }}
| {% endmacro %}

View File

@ -0,0 +1,212 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_home_project %}
include ../../../../pillar/src/templates/mixins/components
| {% set title = 'organizations' %}
| {% block page_title %}Organizations{% endblock %}
| {% block og %}
meta(property="og:title", content="Dashboard")
meta(name="twitter:title", content="Blender Cloud")
meta(property="og:url", content="https://cloud.blender.org/{{ request.path }}")
meta(property="og:type", content="website")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_home_project(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
+nav-secondary
| {% if can_create_organization %}
+nav-secondary-link(
class="create",
onclick='createNewOrganization(this)')
i.pi-plus.text-success
span.text-success
| Create Organization
| {% endif %}
li#create_organization_result_panel.result
.container-fluid.dashboard-container
.row
.col-md-6
ul.projects__list
| {% if organizations %}
| {% for organization in organizations['_items'] %}
| {% set link_url = url_for('pillar.web.organizations.view_embed', organization_id=organization._id) %}
li.projects__list-item(
data-url="{{ link_url }}",
id="organization-{{ organization._id }}")
a.projects__list-thumbnail(
href="{{ link_url }}")
i.pi-users
.projects__list-details
a.title(href="{{ link_url }}")
| {{ organization.name }}
ul.meta
li(title="Members")
| {{ organization.members|hide_none|count }} Member{{ organization.members|hide_none|count|pluralize }}
| {% if (organization.unknown_members|hide_none|count) != 0 %}
| ({{ organization.unknown_members|hide_none|count }} pending)
| {% endif %}
li(title="Seats")
| {{ organization.seat_count }} Seat{{ organization.seat_count|pluralize }}
| {% endfor %}
| {% else %}
li.projects__list-item
a.projects__list-thumbnail
i.pi-blender-cloud
.projects__list-details
span Create an Organization to get started!
| {% endif %}
.col-md-6.py-1.pb-3
#item-details
| {% endblock %}
| {% block footer_scripts %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.typeahead-0.11.1.min.js')}}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/jquery.autocomplete-0.22.0.min.js') }}", async=true)
script.
/* Returns a more-or-less reasonable message given an error response object. */
function xhrErrorResponseMessage(err) {
if (typeof err.responseJSON == 'undefined')
return err.statusText;
if (typeof err.responseJSON._error != 'undefined' && typeof err.responseJSON._error.message != 'undefined')
return err.responseJSON._error.message;
if (typeof err.responseJSON._message != 'undefined')
return err.responseJSON._message
return err.statusText;
}
/**
* Open an organization in the #item-details div.
*/
function item_open(item_id, pushState)
{
if (item_id === undefined ) {
throw new ReferenceError("item_open(" + item_id + ") called.");
}
// Style elements starting with item_type and dash, e.g. "#job-uuid"
var clean_classes = 'active processing';
var current_item = $('#organization-' + item_id);
$('[id^="organization-"]').removeClass(clean_classes);
current_item
.removeClass(clean_classes)
.addClass('processing');
var item_url = '/o/' + item_id;
$.get(item_url, function(item_data) {
$('#item-details').html(item_data);
current_item
.removeClass(clean_classes)
.addClass('active');
}).fail(function(xhr) {
if (console) {
console.log('Error fetching organization', item_id, 'from', item_url);
console.log('XHR:', xhr);
}
current_item.removeClass(clean_classes);
toastr.error('Failed to open organization');
if (xhr.status) {
$('#item-details').html(xhr.responseText);
} else {
$('#item-details').html('<p class="text-danger">Opening ' + item_type + ' failed. There possibly was ' +
'an error connecting to the server. Please check your network connection and ' +
'try again.</p>');
}
});
// Determine whether we should push the new state or not.
pushState = (typeof pushState !== 'undefined') ? pushState : true;
if (!pushState) return;
// Push the correct URL onto the history.
var push_state = {itemId: item_id};
window.history.pushState(
push_state,
'Organization: ' + item_id,
item_url
);
}
$('li.projects__list-item').click(function(e){
url = $(this).data('url');
if (typeof url === 'undefined') return;
window.location.href = url;
if (console) console.log(url);
$(this).addClass('active');
$(this).find('.projects__list-thumbnail i')
.removeAttr('class')
.addClass('pi-spin spin');
});
{% if open_organization_id %}
$(function() { item_open('{{ open_organization_id }}', false); });
{% endif %}
{% if can_create_organization %}
function createNewOrganization(button) {
$(button)
.attr('disabled', 'disabled')
.fadeTo(200, 0.1);
$('#create_organization_result_panel').html('');
// TODO: create a form to get the initial info from the user.
$.post(
'{{ url_for('pillar.web.organizations.create_new') }}',
{
name: 'New Organization',
seat_count: 1,
}
)
.done(function(result) {
var $p = $('<p>').text('organization created, reloading list.')
$('#create_organization_result_panel').html($p);
window.location.href = result.location;
})
.fail(function(err) {
var msg = xhrErrorResponseMessage(err);
$('#create_organization_result_panel').html('Error creating organization: ' + msg);
$(button)
.fadeTo(1000, 1.0)
.queue(function() {
$(this)
.removeAttr('disabled')
.dequeue()
;
})
})
;
return false;
}
{% endif %}
| {% endblock %}

View File

@ -15,7 +15,7 @@ style.
This Application collects some Personal Data from its Users.
h3 Data Controller and Owner
p.
Blender Institute B.V. - Entrepotdok 57A - 1018 AD Amsterdam - the Netherlands,
Blender Institute B.V. - Buikslotermeerplein 161 - 1025 ET Amsterdam - the Netherlands,
institute@blender.org
p.
Blender Institute has been authorised by Stichting Blender Foundation to conduct these
@ -121,7 +121,8 @@ style.
Data Controller to erase the Personal Data. Unless stated otherwise, the then-current privacy
policy applies to all Personal Data the Data Controller has about Users.
h4 Definitions and legal references
p Latest update: February 27, 2014
p Original issue: February 27, 2014
p Latest update: June 10, 2019 (Updated Blender Institute address)
| {% endblock body%}

View File

@ -0,0 +1,64 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
include mixins/components
include ../../../pillar/src/templates/mixins/components
mixin group(title, tag)
.row(id=tag)
section.py-4.my-3.border-bottom.col-12
h4.title-underline.mt-2.mb-4
a.text-muted(href="#" + tag)= title
+card-deck(data-asset-tag=tag, class="js-asset-list p-3")
| {% set title = 'learn' %}
| {% set page_title = 'Production Lessons' %}
| {% set page_description = 'Tips and tricks by the Blender Open Movies crew.' %}
| {% set page_header_image = url_for('static', filename='assets/img/features/open_movies_02.jpg', _external=true) %}
| {% block page_title %}{{ page_title }}{% endblock %}
| {% block og %}
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block head %}
script(src="{{ url_for('static_cloud', filename='assets/js/tagged_assets.min.js') }}")
script.
$(function() {
let is_subscriber = {{ current_user.has_cap('subscriber')|string|lower }};
$('.js-asset-list').loadTaggedAssets(8, 8, is_subscriber);
});
| {% endblock %}
| {% block body %}
.container.py-4
+category_list_header('{{ page_title }}', '{{ page_description }}')
.row
.col-12
+group('Walk-through', 'walk-through')
+group('Modeling', 'modeling')
+group('Sculpting', 'sculpting')
+group('Animation', 'animation')
+group('Shading', 'shading')
+group('Texturing', 'texturing')
+group('Character Pipeline', 'character-pipeline')
+group('Rigging', 'rigging')
+group('Lighting & Rendering', 'lighting')
+group('Simulation & Effects', 'effects')
+group('Video Editing', 'video-editing')
+group('Digital Painting', 'digital-painting')
+group('Production Design', 'production-design')
a.d-block.py-5.text-center.text-muted(
href="{{ url_for('main.nodes_search_index') }}")
| Search Blender Cloud to find even more content
i.pi-angle-right.pl-1
| {% endblock body%}

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

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

View File

@ -0,0 +1,66 @@
| {% 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 %}
.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.description %}
small.form-text.text-muted
| {{ field.description }}
| {% endif %}
| {% if field.errors %}
ul.error
| {% for error in field.errors %}
li {{ error }}
| {% endfor %}
| {% 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 serializedData = $(form).serializeArray()
$.post(url, serializedData)
.done(function(xhr) {
toastr.success('Properties saved');
})
.fail(function(err) {
toastr.error(xhrErrorResponseElement(err, 'Error saving properties: '));
});
}
| {% endblock %}

View File

@ -0,0 +1,23 @@
| {% from '_macros/_asset_list_item.html' import asset_list_item %}
include ../../../../pillar/src/templates/mixins/components
| {% block body %}
#node-container
section.d-flex
h4.p-4 Browse
section.container-fluid
| {% if nodes %}
+card-deck(id="asset_list_explore", class="pl-4")
| {% for node in nodes %}
| {{ asset_list_item(node, current_user) }}
| {% endfor %}
| {% else %}
.list-node-children-container
.list-node-children-empty No items... yet!
| {% endif %}
script.
// Generate GA pageview
ga('send', 'pageview', location.pathname);
| {% endblock %}

View File

@ -0,0 +1,61 @@
| {% extends 'projects/home_layout.html' %}
| {% set title = 'blender-sync' %}
| {% set subtab = 'blender_sync' %}
| {% set learn_more_btn_url = '/blog/introducing-blender-sync' %}
| {% block currenttab %}
.container-fluid
section.nav-tabs__tab.active#tab-blender_sync
.tab_header-container
.tab_header-intro(
style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/pattern_01.jpg')}})")
.tab_header-intro_text
h2 Connect Blender with the Cloud
p
| Save your Blender preferences and keymaps once, load them anywhere.
<br/>
| Use the
=' '
a(href='https://cloud.blender.org/r/downloads/blender_cloud-latest-bundle.zip') Blender Cloud add-on
=' '
| to synchronise your settings from within Blender.
| {% if show_addon_download_buttons %}
.row
.col-md-6
a.btn.btn-block.btn-outline-success(
href="https://cloud.blender.org/r/downloads/blender_cloud-latest-bundle.zip")
i.pi-download
| Download <small>v</small>{{ config.BLENDER_CLOUD_ADDON_VERSION }}
.col-md-6
a.btn.btn-link(
href="{{ learn_more_btn_url }}")
| Learn More
i.pi-angle-right
| {% endif %}
.tab_header-intro_icons
i.pi-blender
i.pi-heart-filled
i.pi-blender-cloud
| {% for version in synced_versions %}
.blender_sync-main
.blender_sync-main-header
h5.blender_sync-main-title
i.pi-blender
| Blender {{ version.version }}
.blender_sync-main-last
| Last synced on: {{ version.date|pretty_date }}
| {% else %}
.blender_sync-main.empty
.blender_sync-main-header
span.blender_sync-main-title
| No settings synced yet
<hr/>
a.download(
href='https://cloud.blender.org/r/downloads/blender_cloud-latest-bundle.zip')
| Download add-on
| {% endfor %}
| {% endblock %}

View File

@ -0,0 +1,42 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_home_project %}
include ../../../../pillar/src/templates/mixins/components
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="https://cloud.blender.org{{ request.path }}")
meta(property="og:title", content="Blender Cloud - Home")
meta(name="twitter:title", content="Blender Cloud")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
| {% endblock %}
| {% block page_title %}
| {{current_user.full_name}}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_home_project(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.dashboard-container
section#projects.bg-white
| {% block currenttab %}{% endblock %}
| {% endblock %}
| {% block footer_scripts %}
script.
$(document).ready(function () {
$('#subtab-{{ subtab }}').addClass('active');
var $nav_tabs = $('#sub-nav-tabs__list').find('a.nav-link');
$nav_tabs.on('click', function (e) {
console.log($(this));
window.location = $(this).attr('data-tab-url');
});
});
| {% endblock %}

View File

@ -0,0 +1,304 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_home_project %}
include ../../../../pillar/src/templates/mixins/components
| {% set title = 'dashboard' %}
| {% block og %}
meta(property="og:title", content="Dashboard")
meta(name="twitter:title", content="Blender Cloud")
meta(property="og:url", content="https://cloud.blender.org/{{ request.path }}")
meta(property="og:type", content="website")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/backgrounds/cloud_services_oti.jpg')}}")
| {% endblock %}
| {% block page_title %}
| {{current_user.full_name}}
| {% endblock %}
| {% block css %}
| {{ super() }}
style.
.deleted-projects-toggle {
z-index: 10;
position: absolute;
right: 0;
font-size: 20px;
padding: 3px;
text-shadow: 0 0 2px white;
}
.deleted-projects-toggle .show-deleted {
color: #aaa;
}
.deleted-projects-toggle .hide-deleted {
color: #bbb;
}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_home_project(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.dashboard-container
section.dashboard-main
section#projects.bg-white
.d-flex
+nav-secondary()(id='sub-nav-tabs__list')
+nav-secondary-link(data-tab-toggle='own_projects', class="active")
span
| Own Projects
| {% if projects_user | length != 0 %}
.d-inline.text-muted.pl-1 ({{ projects_user|length }})
| {% endif %}
+nav-secondary-link(data-tab-toggle='shared')
span
| Shared with me
| {% if projects_shared | length != 0 %}
.d-inline.text-muted.pl-1 ({{ projects_shared|length }})
| {% endif %}
+nav-secondary()()
| {% if current_user.has_cap('subscriber') %}
+nav-secondary-link(
id="project-create",
data-url="{{ url_for('projects.create') }}",
href="{{ url_for('projects.create') }}")
span.text-success Create New Project...
| {% elif current_user.has_cap('can-renew-subscription') %}
+nav-secondary-link(
id="project-create",
data-url="{{ url_for('projects.create') }}",
href="/renew",
target="_blank")
i.pi-heart-filled.text-danger.pr-1
span Resubscribe to Create a Project
| {% endif %}
nav.nav-tabs__tab.active#own_projects
.deleted-projects-toggle
| {% if show_deleted_projects %}
a.hide-deleted(href="{{ request.base_url }}", title='Hide deleted projects')
i.pi-trash
| {% else %}
a.show-deleted(href="{{ request.base_url }}?deleted=1", title='Show deleted projects')
i.pi-trash
| {% endif %}
ul.projects__list
| {% for project in projects_deleted %}
li.projects__list-item.deleted
span.projects__list-thumbnail
| {% if project.picture_square %}
img(src="{{ project.picture_square.thumbnail('s', api=api) }}")
| {% else %}
i.pi-blender-cloud
| {% endif %}
.projects__list-details
span.title {{ project.name }}
ul.meta
li.status.deleted Deleted
li.edit
a(href="javascript:undelete_project('{{ project._id }}')") Restore project
| {% else %}
| {% if show_deleted_projects %}
li.projects__list-item.deleted You have no recenly deleted projects. Deleted projects can be restored within a month after deletion.
| {% endif %}
| {% endfor %}
| {% for project in projects_user %}
li.projects__list-item(
data-url="{{ url_for('projects.view', project_url=project.url) }}")
a.projects__list-thumbnail(
href="{{ url_for('projects.view', project_url=project.url) }}")
| {% if project.picture_square %}
img(src="{{ project.picture_square.thumbnail('s', api=api) }}")
| {% else %}
i.pi-blender-cloud
| {% endif %}
.projects__list-details
a.title(href="{{ url_for('projects.view', project_url=project.url) }}")
| {{ project.name }}
ul.meta
li.status(
class="{{ project.is_private | yesno('private,public,') }}",
title="{{ project.is_private | yesno('Private Project,Public Project,') }}")
| {{ project.is_private | yesno('Private,Public,') }}
li.when(title="{{ project._created }}") {{ project._created | pretty_date }}
li.edit
a(href="{{ url_for('projects.edit', project_url=project.url) }}") Edit
| {% if project.status == 'pending' and current_user.has_cap('view-pending-nodes') %}
li.pending Not Published
| {% endif %}
| {% else %}
| {% if current_user.has_cap('subscriber') %}
li.projects__list-item(data-url="{{ url_for('projects.create') }}")
a.projects__list-thumbnail
i.pi-plus
.projects__list-details
a.title(href="{{ url_for('projects.create') }}")
| Create a project to get started!
| {% elif current_user.has_cap('can-renew-subscription') %}
li.projects__list-item(data-url="https://store.blender.org/renew-my-subscription.php")
a.projects__list-thumbnail
i.pi-plus
.projects__list-details
a.title(href="https://store.blender.org/renew-my-subscription.php")
| Renew your Blender Cloud subscription to create your own projects!
| {% else %}
li.projects__list-item(data-url="/join")
a.projects__list-thumbnail
i.pi-plus
.projects__list-details
a.title(href="/join")
| Join Blender Cloud to create your own projects!
| {% endif %}
| {% endfor %}
section.nav-tabs__tab#shared(style='display: none')
ul.projects__list
| {% if projects_shared %}
| {% for project in projects_shared %}
li.projects__list-item(
data-url="{{ url_for('projects.view', project_url=project.url) }}")
a.projects__list-thumbnail(
href="{{ url_for('projects.view', project_url=project.url) }}")
| {% if project.picture_square %}
img(src="{{ project.picture_square.thumbnail('s', api=api) }}")
| {% else %}
i.pi-blender-cloud
| {% endif %}
.projects__list-details
a.title(href="{{ url_for('projects.view', project_url=project.url) }}")
| {{ project.name }}
ul.meta
li.status(
class="{{ project.is_private | yesno('private,public,') }}",
title="{{ project.is_private | yesno('Private Project,Public Project,') }}")
| {{ project.is_private | yesno('Private,Public,') }}
li.when {{ project._created | pretty_date }}
li.who by {{ project.user.full_name }}
li.edit
a(href="{{ url_for('projects.edit', project_url=project.url) }}") Edit
| {% if project.status == 'pending' and current_user.has_cap('view-pending-nodes') %}
li.pending Not Published
| {% endif %}
li.leave
span.user-remove-prompt
| Leave Project
span.user-remove
| Are you sure?
span.user-remove-confirm(
user-id="{{ current_user.objectid }}",
project-url="{{url_for('projects.sharing', project_url=project.url)}}")
i.pi-check
| Yes, leave
span.user-remove-cancel
i.pi-cancel
| No, cancel
| {% endfor %}
| {% else %}
li.projects__list-item
a.projects__list-thumbnail
i.pi-heart-broken
.projects__list-details
.title
| No projects shared with you... yet!
| {% endif %}
| {% endblock %}
| {% block footer_scripts %}
script.
$(document).ready(function() {
$('li.projects__list-item').click(function(e){
url = $(this).data('url');
if (typeof url === 'undefined') return;
window.location.href = url;
if (console) console.log(url);
$(this).addClass('active');
$(this).find('.projects__list-thumbnail i')
.removeAttr('class')
.addClass('pi-spin spin');
});
// Tabs behavior
var $nav_tabs_list = $('#sub-nav-tabs__list');
var $nav_tabs = $nav_tabs_list.find('a.nav-link');
$nav_tabs.on('click', function(e){
e.preventDefault();
$nav_tabs.removeClass('active');
$(this).addClass('active');
$('.nav-tabs__tab').hide();
$('#' + $(this).attr('data-tab-toggle')).show();
});
// Leave project
var $projects_list = $('ul.projects__list');
$projects_list.find('span.user-remove-prompt').on('click', function(e){
e.stopPropagation();
e.preventDefault();
$(this).next().show();
$(this).hide();
});
$projects_list.find('span.user-remove-cancel').on('click', function(e){
e.stopPropagation();
e.preventDefault();
$(this).parent().prev().show();
$(this).parent().hide();
});
$projects_list.find('span.user-remove-confirm').on('click', function(e){
e.stopPropagation();
e.preventDefault();
var parent = $(this).closest('.projects__list-item');
function removeUser(userId, projectUrl){
$.post(projectUrl, {user_id: userId, action: 'remove'})
.done(function (data) {
parent.remove();
});
}
removeUser($(this).attr('user-id'), $(this).attr('project-url'));
});
hopToTop(); // Display jump to top button
});
var patch_url = '{{ url_for('projects.patch.patch_project', project_id='PROJECTID') }}';
function undelete_project(project_id) {
console.log('undeleting project', project_id);
$.ajax({
url: patch_url.replace('PROJECTID', project_id),
method: 'PATCH',
data: JSON.stringify({'op': 'undelete'}),
contentType: 'application/json'
})
.done(function(data, textStatus, jqXHR) {
location.href = jqXHR.getResponseHeader('Location');
})
.fail(function(err) {
toastr.error(xhrErrorResponseMessage(err), 'Undeletion failed');
})
}
| {% endblock %}

View File

@ -0,0 +1,189 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_project %}
| {% from '_macros/_opengraph.html' import opengraph %}
include ../../../../pillar/src/templates/mixins/components
| {% block bodyclasses %}{{ super() }} landing-home{% endblock %}
| {% from '_macros/_asset_list_item.html' import asset_list_item %}
| {% block page_title %}{{ project.name }}{% endblock%}
| {% if og_picture %}
| {% set og_picture_url = og_picture.thumbnail('l', api=api) %}
| {% else %}
| {% set og_picture_url = None %}
| {% endif %}
| {% block og %}
| {{ opengraph(project.name, project.summary, og_picture_url, url_for('cloud.project_landing', project_url=project.url, _external=True)) }}
| {% endblock %}
| {% block page_overlay %}
#page-overlay.video
.video-embed
#others
| {% endblock %}
| {% block css %}
link(href="{{ url_for('static_cloud', filename='assets/css/project-main.css') }}", rel="stylesheet")
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_project(project, navigation_links, extension_sidebar_links, title) }}
| {% endblock navigation_tabs %}
| {% block body %}
| {% if project.has_method('PUT') %}
+nav-secondary
+nav-secondary-link(
href="{{ url_for('projects.edit', project_url=project.url) }}",
class="text-white")
i.pi-edit.pr-2
span Edit Project
| {% endif %}
| {% if project.picture_header %}
| {% set project_header = project.picture_header.thumbnail('h', api=api) %}
| {% endif %}
| {% set project_browse_url = url_for('cloud.project_browse', project_url=project.url) %}
.jumbotron.text-white.jumbotron-overlay-gradient-fade-to-gray(
style="background-image: url(\'{{ project_header }}\');")
.container
.row
.col-md-6
.display-4.text-uppercase.font-weight-bold
| {% if project.extension_props.cloud.logo %}
a(href="{{ project_browse_url }}")
img.img-fluid(
alt="{{ project.name }}",
src="{{ project.extension_props.cloud.logo.thumbnail('m', api=api) }}")
| {% else %}
a.text-white(href="{{ project_browse_url }}")
| {{ project.name }}
| {% endif %}
.lead.pt-3
| {% if project.summary %}
| {{ project | markdowned('summary') }}
| {% endif %}
.d-flex.pt-4
| {% if project.extension_props.cloud.video_url %}
a.btn.btn-primary.px-5(
class="js-open-overlay-video",
href="{{ project.extension_props.cloud.video_url }}",
target="_blank")
i.pi-play.pr-2
| WATCH
| {% endif %}
a.btn.btn-link.px-4.text-white(href="{{ project_browse_url }}")
| Browse
i.pi-angle-right.pl-2
.container-fluid.landing
.row
.col-md-8.mx-auto.mt-5
.node-details-description
| {% if project.description %}
| {{ project | markdowned('description') }}
| {% endif %}
.row
.col-md-10.mx-auto
section.py-5
.gallery.mx-auto
| {% for n in activity_stream %}
| {% if n.node_type not in ['comment', 'post'] and n.picture %}
.thumbnail.expand-image-links
.img-container
a.js-open-overlay-image(
title="{{ n.name }}",
href="{{ n.picture.thumbnail('l', api=api) }}")
img(
alt="{{ n.name }}",
src="{{ n.picture.thumbnail('l', api=api) }}")
| {% endif %}
| {% endfor %}
.clearfix
.text-center.mx-auto.py-3
a.btn.btn-outline-primary.px-5(
href="{{ project_browse_url }}")
| See More Artwork
.row.mt-5
.col-md-10.mx-auto
h2.pb-3 Project Timeline
.timeline-dark
+timeline("{{ project._id }}")
| {% endblock body %}
| {% block footer_scripts %}
script.
function showOverlay(html_content) {
$('#page-overlay')
.addClass('active')
.html(html_content);
}
function hideOverlay() {
$('#page-overlay')
.removeClass('active')
.html('');
}
$("a.js-open-overlay-image").on( "click", function(e) {
e.preventDefault();
e.stopPropagation();
var url = $(this).attr('href');
showOverlay('<img src="' + url + '"/>');
});
{% if project.extension_props.cloud.video_url %}
//- By isherwood - http://jsfiddle.net/isherwood/cH6e8/
function getYoutubeId(url) {
var regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
var match = url.match(regExp);
if (match && match[2].length == 11) {
return match[2];
} else {
return 'error';
}
}
var videoId = getYoutubeId('{{ project.extension_props.cloud.video_url }}');
var iframeMarkup = '<iframe width="960" height="540" src="//www.youtube.com/embed/'
+ videoId + '" frameborder="0" allowfullscreen></iframe>';
$("a.js-open-overlay-video").on( "click", function(e) {
e.preventDefault();
e.stopPropagation();
showOverlay(iframeMarkup);
});
{% endif %}
//- Click anywhere on the page or hit Escape to hide the overlay.
$(document).click(function () {
hideOverlay();
});
$(document).keyup(function (e) {
if (e.keyCode == 27) {
hideOverlay();
}
});
| {% endblock %}

View File

@ -0,0 +1,640 @@
| {% extends 'layout.html' %}
| {% from '_macros/_add_new_menu.html' import add_new_menu %}
| {% from '_macros/_navigation.html' import navigation_project %}
include ../../../../pillar/src/templates/mixins/components
| {% block page_title %}{{ project.name }}{% endblock%}
| {% if title is not defined %}
| {% set title = 'project' %}
| {% endif %}
| {% block og %}
meta(property="og:type", content="website")
| {% if og_picture %}
meta(property="og:image", content="{{ og_picture.thumbnail('l', api=api) }}")
meta(name="twitter:image", content="{{ og_picture.thumbnail('l', api=api) }}")
| {% elif node and node.picture %}
meta(property="og:image", content="{{ node.picture.thumbnail('l', api=api) }}")
meta(name="twitter:image", content="{{ node.picture.thumbnail('l', api=api) }}")
| {% elif project.picture_header %}
meta(property="og:image", content="{{ project.picture_header.thumbnail('l', api=api) }}")
meta(name="twitter:image", content="{{ project.picture_header.thumbnail('l', api=api) }}")
| {% endif %}
| {% if show_project %}
meta(property="og:title", content="{{ project.name }} - Blender Cloud")
meta(name="twitter:title", content="{{ project.name }} - Blender Cloud")
meta(property="og:description", content="{{ project.summary }}")
meta(name="twitter:description", content="{{ project.summary }}")
meta(property="og:url", content="{{ url_for('projects.view', project_url=project.url, _external=True) }}")
| {% else %}
| {% if node %}
meta(property="og:title", content="{{ node.name }} - Blender Cloud")
meta(name="twitter:title", content="{{ node.name }} on Blender Cloud")
| {% if node.node_type == 'post' %}
| {% if node.properties.content %}
meta(property="og:description", content="{{ node.properties.content | truncate(180) }}")
meta(name="twitter:description", content="{{ node.properties.content | truncate(180) }}")
| {% else %}
meta(property="og:description", content="Blender Cloud, your source for open content and training")
meta(name="twitter:description", content="Blender Cloud, your source for open content and training")
| {% endif %}
| {% else %}
| {% if node.description %}
meta(property="og:description", content="{{ node.description | truncate(180) }}")
meta(name="twitter:description", content="{{ node.description | truncate(180) }}")
| {% else %}
meta(property="og:description", content="Blender Cloud, your source for open content and training")
meta(name="twitter:description", content="Blender Cloud, your source for open content and training")
| {% endif %}
| {% endif %}
meta(property="og:url", content="{{url_for('projects.view_node', project_url=project.url, node_id=node._id)}}")
| {% else %}
meta(property="og:title", content="{{ project.name }} Blog on Blender Cloud")
meta(name="twitter:title", content="{{ project.name }} Blog on Blender Cloud")
meta(property="og:description", content="{{ project.summary }}")
meta(name="twitter:description", content="{{ project.summary }}")
meta(property="og:url", content="{{url_for('projects.view', project_url=project.url, _external=True)}}")
| {% endif %}
| {% endif %}
| {% endblock %}
| {% block head %}
link(href="{{ url_for('static_pillar', filename='assets/jstree/themes/default/style.min.css') }}", rel="stylesheet")
| {% if node %}
link(rel="amphtml", href="{{ url_for('nodes.view', node_id=node._id, _external=True, format='amp') }}")
| {% endif %}
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/video.min.js') }}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-ga-0.4.2.min.js') }}")
script(src="{{ url_for('static_pillar', filename='assets/js/vendor/videojs-hotkeys-0.2.20.min.js') }}")
script(src="{{ url_for('static_pillar', filename='assets/js/video_plugins.min.js') }}")
| {% endblock %}
| {% block css %}
link(href="{{ url_for('static_cloud', filename='assets/css/project-main.css') }}", rel="stylesheet")
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_project(project, navigation_links, extension_sidebar_links, title) }}
| {% endblock navigation_tabs %}
| {% block body %}
#project-container.is-sidebar-visible
#project-side-container.bg-light
#project_nav(class="{{ title }}")
#project_nav-container
| {% if title != 'about' %}
button.project-sidebar-toggle.btn.btn-sm.btn-link.px-1.rounded-0.bg-light.text-muted.position-absolute(
type="button",
class="js-project-sidebar-toggle")
i.pi-angle-double-left
| {% block project_tree %}
#project_tree.bg-light.p-1
| {% endblock project_tree %}
| {% endif %}
#project_context-container
.breadcrumbs-container.bg-dark.fixed-top
button.project-sidebar-toggle.btn.btn-sm.btn-link.px-1.bg-dark.rounded-0.text-muted(
type="button",
class="js-project-sidebar-toggle")
i.pi-menu
node-breadcrumbs(node-id="{{ node._id }}", @navigate="(nodeId)=>{displayNode(nodeId)}")
script.
new Vue({el:'node-breadcrumbs'});
| {% if project.has_method('PUT') %}
#project_context-header.position-absolute
ul.project-edit-tools.disabled.d-flex.list-unstyled.py-2.mb-0
li.dropdown(
title="Create...",
data-toggle="tooltip",
data-placement="left")
button.dropdown-toggle.btn.btn-sm.btn-outline-secondary(
id="item_add",
class="project-mode-view",
type="button",
data-toggle="dropdown",
aria-haspopup="true",
aria-expanded="false")
i.pi-collection-plus
ul.dropdown-menu.dropdown-menu-right(
class="add_new-menu")
| {{ add_new_menu(project.node_types) }}
li.button-edit
a.btn.btn-sm.btn-outline-secondary.ml-2.px-2(
id="item_edit",
class="project-mode-view",
href="javascript:void(0);",
title="Edit",
data-project_id="{{project._id}}",
data-toggle="tooltip",
data-placement="top")
i.button-edit-icon.pi-edit
li.dropdown
button.dropdown-toggle.btn.btn-sm.btn-outline-secondary.mx-2(
class="project-mode-view",
type="button",
data-toggle="dropdown",
aria-haspopup="true",
aria-expanded="false")
i.pi-more-vertical.p-0
ul.dropdown-menu.dropdown-menu-right
| {% if current_user.has_cap('admin') %}
li
a.dropdown-item(
id="item_featured",
href="javascript:void(0);",
title="Feature on project's homepage",
data-toggle="tooltip",
data-placement="left")
i.pi-star.pr-2
| Toggle Featured
li
a.dropdown-item(
id="item_toggle_public",
href="javascript:void(0);",
title="Make it accessible to anyone",
data-toggle="tooltip",
data-placement="left")
i.pi-lock-open.pr-2
| Toggle Public
| {% endif %}
li
a.dropdown-item(
id="item_toggle_projheader",
href="javascript:void(0);",
title="Feature as project's header",
data-toggle="tooltip",
data-placement="left")
i.pi-star.pr-2
| Toggle Project Header video
li.button-move
a.dropdown-item(
id="item_move",
href="javascript:void(0);",
title="Move into a folder...",
data-toggle="tooltip",
data-placement="left")
i.button-move-icon.pi-move.pr-2
| Move
li.button-delete
a.dropdown-item(
id="item_delete",
href="javascript:void(0);",
title="Can be undone within a month",
data-toggle="tooltip",
data-placement="left")
i.pi-trash.pr-2
| Delete Project
// Edit Mode
li.button-cancel
a.btn.btn-outline-secondary(
id="item_cancel",
class="project-mode-edit",
href="javascript:void(0);",
title="Cancel changes")
i.button-cancel-icon.pi-cancel
| Cancel
li.button-save
a.btn.btn-outline-success.mx-2(
id="item_save",
class="project-mode-edit",
href="javascript:void(0);",
title="Save changes")
i.button-save-icon.pi-check
| Save Changes
| {% endif %}
| {% set utm_source = request.args.get('utm_source') %}
| {% if config.UTM_LINKS and utm_source in config.UTM_LINKS %}
#utm_container
a(href="{{config.UTM_LINKS[utm_source]['link']}}")
img(src="{{config.UTM_LINKS[utm_source]['image']}}", alt="gift", class="img-responsive")
| {% endif %}
#project_context
| {% block project_context %}
| {% if show_project and not browse %}
| {# Embed the project view only if we are not exploring it. #}
| {% include "projects/view_embed.html" %}
| {% endif %}
| {% endblock project_context %}
#overlay-mode-move-container
.overlay-container
.title
i.pi-angle-left
| Select the <strong>folder</strong> where you want to move it
.buttons
button#item_move_accept.move.disabled
| Select a Folder
button#item_move_cancel.cancel
i.pi-cancel
| Cancel
| {% endblock %}
| {% block footer_container %}{% endblock %}
| {% block footer_scripts_pre %}
| {% if project.has_method('PUT') %}
| {# JS containing the Edit, Add, Featured, and Move functions #}
script(type="text/javascript", src="{{ url_for('static_pillar', filename='assets/js/project-edit.min.js') }}")
| {% endif %}
script.
loadProjectSidebar();
$('body').on('click', '.js-project-sidebar-toggle', function(e){
e.preventDefault();
toggleProjectSidebar();
});
function updateToggleProjHeaderMenuItem() {
var $toggle_projheader = $('#item_toggle_projheader');
if (ProjectUtils.isProject()) {
$toggle_projheader.hide();
return;
}
if (ProjectUtils.nodeType() == 'asset') {
$toggle_projheader.show();
} else {
$toggle_projheader.hide();
}
}
$(updateToggleProjHeaderMenuItem);
// Function to update the interface on loadNodeContent, and edit/saving assets
function updateUi(nodeId, mode) {
if (mode === 'view') {
$('.project-mode-view').displayAs('inline-block');
$('.project-mode-edit').hide();
$("#node-edit-form").unbind("submit");
$("#item_save").unbind("click");
$("#item_cancel").unbind("click");
} else if (mode === 'edit') {
$('.project-mode-view').hide();
$('.project-mode-edit').displayAs('inline-block');
} else {
if (console) console.log('Invalid mode:', mode);
}
// Prevent flicker by scrolling to top.
$("#project_context-container").scrollTop(0);
// Enable specific items under the Add New dropdown
if (ProjectUtils.nodeType() === 'group') {
addMenuEnable(['asset', 'group']);
} else if (ProjectUtils.nodeType() === 'group_texture') {
addMenuEnable(['group_texture', 'texture']);
} else if (ProjectUtils.nodeType() === 'group_hdri') {
addMenuEnable(['group_hdri', 'hdri']);
} else if (!ProjectUtils.isProject()) {
addMenuEnable(false);
}
updateToggleProjHeaderMenuItem();
// Set the page title on the document
var page_title = $('#node-title').text() + " - {{ project.name }} — Blender Cloud";
DocumentTitleAPI.set_page_title(page_title);
// TODO: Maybe remove this, now it's also in loadNodeContent(), but double-check
// it's done like that in all users of updateUi().
loadingBarHide();
}
| {% endblock %}
| {% block footer_scripts %}
script(src="{{ url_for('static_pillar', filename='assets/jstree/jstree.min.js') }}")
script.
{% if show_project %}
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: true, nodeId: ''});
{% else %}
{% if node %}
ProjectUtils.setProjectAttributes({projectId: "{{project._id}}", isProject: false, nodeId: '{{node._id}}'});
{% endif %}
{% endif %}
var projectTree = document.getElementById('project_tree');
var urlNodeMove = "{{url_for('projects.move_node')}}";
var urlNodeFeature = "{{url_for('projects.add_featured_node')}}";
var urlNodeDelete = "{{url_for('projects.delete_node')}}";
var urlNodeTogglePublic = "{{url_for('projects.toggle_node_public')}}";
var urlNodeToggleProjHeader = "{{url_for('projects.toggle_node_project_header')}}";
var urlProjectDelete = "{{url_for('projects.delete')}}";
var urlProjectEdit = "{{url_for('projects.edit', project_url=project.url)}}";
function loadNodeContent(url, nodeId) {
var $projectContext = $('#project_context')
$projectContext.trigger('pillar:workStart')
$.get(url, function(dataHtml) {
// Update the DOM injecting the generate HTML into the page
$projectContext.html(dataHtml);
})
.done(function(){
pillar.events.Nodes.triggerLoaded(nodeId);
updateUi(nodeId, 'view');
})
.fail(function(dataResponse) {
$projectContext.html($('<iframe id="server_error"/>'));
$('#server_error').attr('src', url);
})
.always(function(){
$projectContext.trigger('pillar:workStop')
$('.button-edit-icon').addClass('pi-edit').removeClass('pi-spin spin');
});
}
function loadProjectContent(url) {
var $projectContext = $('#project_context')
$projectContext.trigger('pillar:workStart')
$.get(url, function(dataHtml) {
// Update the DOM injecting the generated HTML into the page
$projectContext.html(dataHtml);
})
.done(function() {
updateUi('', 'view');
addMenuEnable();
addMenuDisable(['texture']);
})
.fail(function(dataResponse) {
$projectContext.html($('<iframe id="server_error"/>'));
$('#server_error').attr('src', url);
})
.always(function(){
$projectContext.trigger('pillar:workStop')
$('.button-edit-icon').addClass('pi-edit').removeClass('pi-spin spin');
});
}
function displayStorage(storageNodeId, path) {
var url = '/nodes/' + storageNodeId + '/view?path=' + path;
loadNodeContent(url);
}
function displayNode(nodeId, pushState) {
// Remove the 'n_' suffix from the id
if (nodeId.substring(0, 2) == 'n_') {
nodeId = nodeId.substr(2);
}
var url = '/nodes/' + nodeId + '/view';
loadNodeContent(url, nodeId);
// Determine whether we should push the new state or not.
pushState = (typeof pushState !== 'undefined') ? pushState : true;
if (!pushState) return;
// Push the correct URL onto the history.
var push_state = {nodeId: nodeId, url: url};
var push_url = '{{url_for("projects.view", project_url=project.url)}}' + nodeId;
// console.log('Pushing state ', push_state, ' with URL ', push_url);
window.history.pushState(
push_state,
'Node ' + nodeId, // TODO: use sensible title
push_url
);
}
function redirectToNode(nodeId) {
var generic_url = '{{ url_for("projects.view_node", project_url=project.url, node_id="theNodeId") }}';
var node_url = generic_url.replace('theNodeId', nodeId);
// This makes the user skip the current page when using the 'back' button,
// i.e. it works as a proper redirect.
location.replace(node_url);
}
window.onpopstate = function(event) {
var state = event.state;
// console.log('State popped. location:', document.location, 'state:', state);
// Deselect any selected node. We'll select the visited node (if any) later on.
var jstreeAPI = $(projectTree).jstree(true);
jstreeAPI.deselect_all(true);
if (state == null) {
// Went back to the project.
displayProject();
return;
}
// Went back to a node.
loadNodeContent(state.url, state.nodeId);
// Annoying hack because jstreeAPI.select_node() can only suppress the
// changed.jstree event, and NOT the selected_node.jstree event.
projectTree.dataset.ignoreSelectNode = true;
jstreeAPI.select_node('n_' + state.nodeId, true);
delete projectTree.dataset.ignoreSelectNode;
};
function displayProject() {
var url = "{{url_for('projects.view', project_url=project.url, embed=1)}}";
loadProjectContent(url);
}
function getHashId() {
if (console)
console.log('getHashId() should not be used any more!');
}
/* Loaded once, on page load */
function loadContent() {
var nodeId = ProjectUtils.nodeId();
var isProject = ProjectUtils.isProject();
if (isProject) {
// No need to asynchronously load the project, as it's embedded by Jinja.
// displayProject() is still needed, though, when people use 'back' to go there.
if (location.hash) {
// Handle old-style /p/{url}/#node-ID links, and redirect them to the correct spot.
redirectToNode(location.hash.substr(1));
}
$('.project-mode-view').displayAs('inline-block');
$('.project-mode-edit').hide();
{% if browse %}
let url = "{{url_for('cloud.project_browse_view_nodes', project_url=project.url)}}";
loadProjectContent(url);
{% endif %}
} else {
displayNode(nodeId, false);
}
$(projectTree).jstree({
'core': {
'data': function (obj, callback) {
if(obj.id === '#') { //tree root
if (isProject) {
$.getJSON("{{url_for('projects.jstree', project_url=project.url)}}", function (jsonObject) {
callback.call(this, jsonObject['items']);
});
} else {
$.getJSON('/nodes/' + nodeId + '/jstree', function(jsonObject) {
callback.call(this, jsonObject['items']);
});
}
} else { //normal node
var childNodeId;
if (obj.original.type == 'group_storage') {
childNodeId = obj.original.storage_node;
$.getJSON('/nodes/' + childNodeId + '/jstree?children=1&path=' + obj.original.path, function(jsonObject) {
callback.call(this, jsonObject.children);
});
} else {
// Remove the 'n_' suffix from the id
childNodeId = obj.id.substring(2);
$.getJSON('/nodes/' + childNodeId + '/jstree?children=1', function(jsonObject) {
callback.call(this, jsonObject.children);
});
}
}
}
},
"types" : {
"#": {"valid_children": ["collection"]},
"chapter" : {"icon": "pi-folder"},
"group" : {"icon": "pi-folder"},
"group_texture" : {"icon": "pi-folder-texture"},
"group_hdri" : {"icon": "pi-folder-texture", "max_children": 0},
"group_storage" : {"icon": "pi-folder"},
"filesystem_node" : {"icon": "pi-folder"},
"file" : {"icon": "pi-document", "max_children": 0},
"filesystem_file" : {"icon": "pi-document", "max_children": 0},
"image" : {"icon": "pi-image", "max_children": 0},
"hdri" : {"icon": "pi-globe", "max_children": 0},
"texture" : {"icon": "pi-texture", "max_children": 0},
"video" : {"icon": "pi-film-thick", "max_children": 0},
"blog" : {"icon": "pi-newspaper", "max_children": 0},
"page" : {"icon": "pi-document-text", "max_children": 0},
"default" : {"icon": "pi-document"}
},
"plugins": ["types",] //, "state", "sort"
});
var jstreeAPI = $(projectTree).jstree(true);
$(projectTree).on("select_node.jstree", function (e, data) {
var selectedNodeId = data.node.id.substr(2);
// Ignore events that can't be suppressed otherwise.
// This can be removed if jstreeAPI.select_node() allows suppressing
// the select_node.jstree event.
if (e.target.dataset.ignoreSelectNode === 'true') return;
if (typeof(data.node.original.path) === 'undefined') {
var movingMode = Cookies.getJSON('bcloud_moving_node');
// Check if we are in the process of moving a node
if (movingMode) {
// Allow moving nodes only inside of node_type group
if (data.node.original.type != 'group' || movingMode.node_id === selectedNodeId || movingMode.node_id === ProjectUtils.parentNodeId()) {
if (movingMode.node_type === 'texture') {
if (data.node.original.type === 'group_texture') {
$('#item_move_accept').html('<i class="pi-check"></i>Move Here').removeClass('disabled');
} else {
$('#item_move_accept').html('Select a Texture Folder').addClass('disabled');
}
} else if (movingMode.node_type === 'hdri') {
if (data.node.original.type === 'group_hdri') {
$('#item_move_accept').html('<i class="pi-check"></i>Move Here').removeClass('disabled');
} else {
$('#item_move_accept').html('Select an HDRi Folder').addClass('disabled');
}
} else {
$('#item_move_accept').html('Select a Folder').addClass('disabled');
}
} else {
$('#item_move_accept').html('<i class="pi-check"></i>Move Here').removeClass('disabled');
}
}
// Check the type of node and act accordingly
if (data.node.original.custom_view) {
window.location = data.node.a_attr.href;
} else {
var currentNodeId = ProjectUtils.nodeId();
if (currentNodeId != selectedNodeId) {
displayNode(selectedNodeId);
}
jstreeAPI.open_node(data.node);
}
} else {
displayStorage(data.node.original.storage_node, data.node.original.path);
jstreeAPI.toggle_node(data.node);
}
});
};
{% if is_embedded_edit is not defined or is_embedded_edit %}
// Initialize the page if we are not directly editing a node (most of the time)
loadContent();
{% endif %}
var project_container = document.getElementById('project-container');
/* UI Stuff */
$(window).on("load resize",function(){
containerResizeY($(window).height());
});
{% if current_user_is_subscriber %}
$(projectTree).addClass('is_subscriber');
{% endif %}
| {% endblock %}
| {% block comment_scripts %} {% endblock%}

View File

@ -1,96 +1,74 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
| {# Default case is Open Projects #}
| {% set page_title = 'Open Projects' %}
| {% set page_description = 'Full production data and tutorials from all open movies, for you to use freely' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_agent327_01.jpg') %}
| {% set page_header_text = 'The iconic Blender Institute Open Movies. Featuring all the production files, assets, artwork, and never-seen-before content.' %}
include ../../../pillar/src/templates/mixins/components
include mixins/components
| {% if title == 'courses' %}
| {% set page_title = 'Courses' %}
| {% set page_description = 'Production quality training by 3D professionals' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_caminandes_3_03.jpg') %}
| {% set page_header_text = 'Character modeling, 3D printing, VFX, rigging and more.' %}
| {# Default collection is 'Courses' #}
| {% set page_title = 'Courses' %}
| {% set page_description = 'In-depth training on character modeling, 3D printing, rigging, VFX and more.' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_agent327_04.jpg', _external=True) %}
| {% elif title == 'workshops' %}
| {% if title == 'workshops' %}
| {% set page_title = 'Workshops' %}
| {% set page_description = 'Production quality training by 3D professionals' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_caminandes_3_03.jpg') %}
| {% set page_header_text = 'Enter the artist workshop and learn by example.' %}
| {% set page_description = 'Enter the artist workshop and learn by example.' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_agent327_04.jpg', _external=True) %}
| {% endif %}
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="https://cloud.blender.org")
meta(property="og:title", content="{{ page_title }} on Blender Cloud")
meta(name="twitter:title", content="{{ page_title }} on Blender Cloud")
meta(property="og:description", content="{{ page_description }}")
meta(name="twitter:description", content="{{ page_description }}")
meta(property="og:image", content="{{ page_header_image }}")
meta(name="twitter:image", content="{{ page_header_image }}")
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block page_title %}
| {{ page_title }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block body %}
.container.py-4
+category_list_header('{{ page_title }}', '{{ page_description }}', '{{ request.url }}')
#project-container
+card-deck()
| {% for project in projects %}
#node_index-container
#node_index-header.collection
img.background-header(src="{{ page_header_image }}")
#node_index-collection-info
.node_index-collection-name
span {{ page_title }}
.node_index-collection-description
span.
{{ page_header_text }}
| {% if (project.status == 'published') or (project.status == 'pending' and current_user.is_authenticated) and project._id != config.MAIN_PROJECT_ID %}
+card(
class='js-project-go card-fade cursor-pointer mb-4',
style="min-width: 30%",
data-url="{{ url_for('projects.view', project_url=project.url) }}",
tabindex='{{ loop.index }}')
.node_index-collection
| {% if project.picture_16_9 %}
a.card-thumbnail(href="{{ url_for('projects.view', project_url=project.url) }}")
img.card-img-top(
alt="{{ project.name }}",
src="{{ project.picture_16_9.thumbnail('l', api=api) }}")
| {% endif %}
| {% for project in projects %}
| {% if (project.status == 'published') or (project.status == 'pending' and current_user.is_authenticated) and project._id != config.MAIN_PROJECT_ID %}
.node_index-collection-card.project(
data-url="{{ url_for('projects.view', project_url=project.url) }}",
tabindex="{{ loop.index }}")
| {% if project.picture_header %}
a.item-header(
href="{{ url_for('projects.view', project_url=project.url) }}")
img(src="{{ project.picture_header.thumbnail('l', api=api) }}")
| {% endif %}
.item-info
a.item-title(
href="{{ url_for('projects.view', project_url=project.url) }}")
| {{project.name}}
| {% if project.status == 'pending' and current_user.is_authenticated and current_user.has_role('admin') %}
small (pending)
| {% endif %}
| {% if project.summary %}
p.item-description
| {{project.summary|safe}}
.card-body
h5.card-title
| {{ project.name }}
| {% if project.status == 'pending' and current_user.is_authenticated and current_user.has_role('admin') %}
small (pending)
| {% endif %}
a.learn-more LEARN MORE
| {% endif %}
| {% endfor %}
| {% if project.summary %}
p.card-text
| {{project.summary|safe}}
| {% endif %}
| {% endif %}
| {% endfor %}
| {% endblock %}
| {% block footer_scripts %}
script.
$('.node_index-collection-card.project').on('click', function(e){
$('.js-project-go').on('click', function(e){
e.preventDefault();
window.location.href = $(this).data('url');
});

View File

@ -1,223 +1,251 @@
| {% extends 'layout.html' %}
| {% block page_title %}Services{% endblock %}
| {% from '_macros/_navigation.html' import navigation_homepage %}
| {% from '_macros/_opengraph.html' import opengraph %}
include ../../../pillar/src/templates/mixins/components
include mixins/components
| {% set title = 'services' %}
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="{{ url_for('cloud.services') }}")
| {% set page_title = 'Services' %}
| {% set page_description = 'On Blender Cloud you can create and share personal projects, access our texture and HDRI library (or create your own), keep track of your production, manage your renders and much more!' %}
| {% set page_header_image = url_for('static', filename='assets/img/backgrounds/background_services_16_9.jpg', _external=true) %}
meta(property="og:title", content="Services - Blender Cloud")
meta(name="twitter:title", content="Services - Blender Cloud")
meta(property="og:description", content="Personal Projects · Blender Integration · Texture Browsing · Production Management")
meta(name="twitter:description", content="Personal Projects · Blender Integration · Texture Browsing · Production Management")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_services.jpg')}}")
meta(name="twitter:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_services.jpg')}}")
| {% block page_title %}{{ page_title }}{% endblock %}
| {% block og %}
| {{ opengraph(page_title, page_description, page_header_image, request.url) }}
| {% endblock %}
| {% block navigation_tabs %}
| {{ navigation_homepage(title) }}
| {% endblock navigation_tabs %}
| {% block page_overlay %}
#page-overlay.video
.video-embed
| {% endblock %}
| {% block body %}
#page-container
#page-header(style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/services_projects.jpg')}})")
.container
.page-title
| Blender Cloud Services
.page-title-summary
span.text-background
p.
Blender Cloud is the creative hub for your projects, powered by Free and Open Source software.
.container.py-4
+category_list_header('{{ page_title }}', '{{ page_description }}', '{{ request.url }}')
p.
On Blender Cloud you can create and share personal projects, access our texture and HDRI
library (or create your own), keep track of your production, manage your renders and much more!
- var addon_text = 'Available through the <a href="{{ url_for(\'cloud.services\') }}#blender-cloud-add-on">Blender Cloud add-on</a>';
section#blender-cloud-add-on.page-card
.page-card-side
h2.page-card-title
| Blender Cloud add-on
.page-card-summary
p.
The Blender Cloud add-on provides access to most of our services directly within Blender.
p.
Use the add-on to share images online, submit renders to Flamenco or browse textures and HDRI libraries!
.navbar-backdrop-overlay
hr
- var addon_text = 'Available through the <a href="{{ url_for(\'cloud.services\') }}#blender-cloud-add-on">Blender Cloud add-on</a>'
small Blender Cloud add-on requires Blender 2.78 or newer
#page-content
a.btn.btn-primary(
href="/r/downloads/blender_cloud-latest-addon.zip",
title="Download Blender Cloud add-on")
i.pi-download
| Download add-on &nbsp;<small>v</small> {{ config.BLENDER_CLOUD_ADDON_VERSION }}
section#blender-cloud-add-on.page-card
.page-card-side
h2.page-card-title
| Blender Cloud add-on
.page-card-summary
p.
The Blender Cloud add-on provides access to most of our services directly within Blender.
p.
Use the add-on to share images online, submit renders to Flamenco or browse textures and HDRI libraries!
a.page-card-side(
href="/r/downloads/blender_cloud-latest-addon.zip",
title="Download Blender Cloud add-on")
img(
src="{{ url_for('static', filename='assets/img/features/blender_cloud_addon_thumbnail.png')}}")
hr
section#blender-sync.page-card
.page-card-side
h2.page-card-title Blender Sync
.page-card-summary
| Save your settings once. Use them anywhere.
| Carry your Blender configuration with you,
| use our add-on to sync your keymaps and preferences.
hr
small Blender Sync is <strong>free</strong> for everyone! No subscription required.
small This add-on requires Blender 2.78 or newer.
small Blender Cloud add-on requires Blender 2.78 or newer
.tip !{addon_text}
a.page-card-cta.download(
href="https://cloud.blender.org/r/downloads/blender_cloud-latest-addon.zip")
i.pi-download
| Download add-on &nbsp;<small>v</small> {{ config.BLENDER_CLOUD_ADDON_VERSION }}
a.btn.btn-outline-primary(
href="/r/downloads/blender_cloud-latest-addon.zip",
title="Download Blender Cloud add-on")
i.pi-download
| Download add-on &nbsp;<small>v</small> {{ config.BLENDER_CLOUD_ADDON_VERSION }}
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/blender_cloud_addon_thumbnail.png')}}")
a.btn.btn-link(
href="/blog/introducing-blender-sync",
title="Learn more about Blender Sync")
| Learn More
i.pi-angle-right
section#blender-sync.page-card.right
.page-card-side
h2.page-card-title Blender Sync
.page-card-summary
| Save your settings once. Use them anywhere.
| Carry your Blender configuration with you,
| use our add-on to sync your keymaps and preferences.
hr
small Blender Sync is <strong>free</strong> for everyone! No subscription required.
small This add-on requires Blender 2.78 or newer.
.tip !{addon_text}
a.page-card-side(
href="/blog/introducing-blender-sync",
title="Learn more about Blender Sync")
img(
src="{{ url_for('static', filename='assets/img/features/sync_thumbnail.jpg')}}")
a.page-card-cta(
href="https://cloud.blender.org/blog/introducing-blender-sync")
| Learn More
section#texture-browser.page-card
.page-card-side
h2.page-card-title Texture & HDRI Browser
.page-card-summary
p.
Access the <a href="/p/textures/">Blender Cloud Textures</a>
library from within Blender using our exclusive add-on.
Create, manage and share <em>your own</em> texture libraries!
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/sync_thumbnail.jpg')}}")
.tip !{addon_text}
a.btn.btn-outline-primary.js-watch-video(
href="https://www.youtube.com/watch?v=-srXYv2Osjw",
data-youtube-id="-srXYv2Osjw",
title="Watch video about Texture and HDRI Browser")
i.pi-play
| Watch Video
a.page-card-side(
class="js-watch-video",
href="https://www.youtube.com/watch?v=-srXYv2Osjw",
data-youtube-id="-srXYv2Osjw",
title="Watch video about Texture and HDRI Browser")
img(
src="{{ url_for('static', filename='assets/img/features/tex_library_thumbnail.jpg')}}")
section#texture-browser.page-card.right
.page-card-side
h2.page-card-title Texture & HDRI Browser
.page-card-summary
p.
Access the <a href="https://cloud.blender.org/p/textures/">Blender Cloud Textures</a>
library from within Blender using our exclusive add-on.
Create, manage and share <em>your own</em> texture libraries!
section#image-sharing.page-card
.page-card-side
h2.page-card-title Image Sharing
.page-card-summary
| Got a nice render, a Blender oddity, a cool screenshot?
| Share it instantly from within Blender to the Cloud, to the world!
.tip !{addon_text}
.tip !{addon_text}
a.page-card-cta.js-watch-video.download(
href="https://www.youtube.com/watch?v=-srXYv2Osjw",
data-youtube-id="-srXYv2Osjw")
i.pi-play
| Watch Video
a.btn.btn-outline-primary.js-watch-video(
href="https://www.youtube.com/watch?v=yvtqeMBOAyk",
data-youtube-id="yvtqeMBOAyk",
title="Watch video about Image Sharing")
i.pi-play
| Watch Video
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/tex_library_thumbnail.jpg')}}")
a.btn.btn-link(
href="/blog/introducing-image-sharing",
title="Learn more about Image Sharing")
| Learn More
i.pi-angle-right
a.page-card-side(
class="js-watch-video",
href="https://www.youtube.com/watch?v=yvtqeMBOAyk",
data-youtube-id="yvtqeMBOAyk",
title="Watch video about Image Sharing")
img(
src="{{ url_for('static', filename='assets/img/features/image_sharing_thumbnail.jpg')}}")
section#image-sharing.page-card.right
.page-card-side
h2.page-card-title Image Sharing
.page-card-summary
| Got a nice render, a Blender oddity, a cool screenshot?
| Share it instantly from within Blender to the Cloud, to the world!
section#projects.page-card
.page-card-side
h2.page-card-title Private Projects
.page-card-summary.
Create and manage your own personal projects.
Upload assets and collaborate with other Blender Cloud members.
.tip !{addon_text}
a.btn.btn-link(
href="/blog/introducing-private-projects",
title="Learn more about Private Projects")
| Learn More
i.pi-angle-right
a.page-card-cta.download.js-watch-video(
href="https://www.youtube.com/watch?v=yvtqeMBOAyk",
data-youtube-id="yvtqeMBOAyk")
i.pi-play
| Watch Video
a.page-card-cta.outline(
href="https://cloud.blender.org/blog/introducing-image-sharing")
| Learn More
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/image_sharing_thumbnail.jpg')}}")
a.page-card-side(
href="/blog/introducing-private-projects",
title="Learn more about Private Projects")
img(
src="{{ url_for('static', filename='assets/img/features/projects_thumbnail.jpg')}}")
section#projects.page-card.right
.page-card-side
h2.page-card-title Private Projects
.page-card-summary.
Create and manage your own personal projects.
Upload assets and collaborate with other Blender Cloud members.
section#attract.page-card
.page-card-side
h2.page-card-title
| Attract
.page-card-summary.
Production-management software for your film, game, or commercial projects.
a.page-card-cta(
href="https://cloud.blender.org/blog/introducing-private-projects")
| Learn More
a.btn.btn-outline-primary.js-watch-video(
href="https://www.youtube.com/watch?v=b9x1rlyyt_o",
data-youtube-id="b9x1rlyyt_o")
i.pi-play
| Watch Video
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/projects_thumbnail.jpg')}}")
a.btn.btn-link(
href="/blog/attract-and-flamenco-public-beta",
title="Learn more about Attract")
| Learn More
i.pi-angle-right
a.page-card-side(
href="/blog/attract-and-flamenco-public-beta",
title="Learn more about Attract")
img(
src="{{ url_for('static', filename='assets/img/features/attract_thumbnail.jpg')}}")
section#attract.page-card.right
.page-card-side
h2.page-card-title
| Attract
.page-card-summary.
Production-management software for your film, game, or commercial projects.
section#flamenco.page-card
.page-card-side
h2.page-card-title
| Flamenco
.page-card-summary.
Take control of your computing infrastructure and get things done.
a.page-card-cta.download.js-watch-video(
href="https://www.youtube.com/watch?v=b9x1rlyyt_o",
data-youtube-id="b9x1rlyyt_o")
i.pi-play
| Watch Video
a.btn.btn-outline-primary.js-watch-video(
href="https://www.youtube.com/watch?v=7cnFKhsM67Q",
data-youtube-id="7cnFKhsM67Q")
i.pi-play
| Watch Video
a.page-card-cta(
href="https://cloud.blender.org/blog/attract-and-flamenco-public-beta",
title="Learn more about Attract")
| Learn More
a.btn.btn-link(
href="https://flamenco.io",
title="Learn more about Flamenco")
| Learn More
i.pi-angle-right
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/attract_thumbnail.jpg')}}")
a.page-card-side(
href="https://flamenco.io",
title="Learn more about Flamenco")
img(
src="{{ url_for('static', filename='assets/img/features/flamenco_thumbnail.jpg')}}")
section#flamenco.page-card.right
.page-card-side
h2.page-card-title
| Flamenco
.page-card-summary.
Take control of your computing infrastructure and get things done.
| {% if not current_user.has_role('subscriber') %}
section.page-card(
style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/pattern_01.jpg')}})")
.page-card-side
a.page-card-cta.download.js-watch-video(
href="https://www.youtube.com/watch?v=7cnFKhsM67Q",
data-youtube-id="7cnFKhsM67Q")
i.pi-play
| Watch Video
h2.page-card-title
| All of this, plus hours of training and production assets.
a.page-card-cta(
href="https://flamenco.io",
title="Learn more about Flamenco")
| Learn More
.page-card-summary.text-white
| Join us for only $9.90/month!
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/flamenco_thumbnail.jpg')}}")
| {% if not current_user.has_role('subscriber') %}
section.page-card.subscribe(
style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/pattern_01.jpg')}})")
.page-card-side
h2.page-card-title
| All of this, plus hours of training and production assets.
.page-card-summary
| Join us for only $9.90/month!
a.page-card-cta(
href="https://store.blender.org/product/membership/")
| Subscribe Now
| {% endif %}
a.btn.btn-outline-light.px-3(href="https://store.blender.org/product/membership/")
i.pi-heart.mr-2
| Subscribe Now
| {% endif %}
| {% endblock %}
| {% block footer_scripts %}
script.
// Click anywhere in the page to hide the overlay
// Hide the video overlay.
function hideOverlay() {
$('#page-overlay.video').removeClass('active');
$('#page-overlay.video .video-embed').html('');
}
// Click anywhere in the page or hit Esc to hide the overlay.
$(document).click(function() {
hideOverlay();
});
@ -229,12 +257,12 @@ script.
});
$('a.js-watch-video').click(function(e){
var videoId = $(this).attr('data-youtube-id');
e.preventDefault();
e.stopPropagation();
$('#page-overlay.video').addClass('active');
var videoId = $(this).attr('data-youtube-id');
$('#page-overlay .video-embed').html('<iframe src="https://www.youtube.com/embed/' + videoId +'?rel=0&amp;showinfo=0;autoplay=1" frameborder="0" allowfullscreen></iframe>')
});

View File

@ -5,7 +5,7 @@
| {% block og %}
meta(property="og:title", content="Blender Cloud Statistics")
meta(property="og:url", content="https://cloud.blender.org/stats")
meta(property="og:url", content="{{ request.url }}")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_andy_hdribot_01.jpg')}}")
| {% endblock %}

Some files were not shown because too many files have changed in this diff Show More