252 Commits

Author SHA1 Message Date
917417a8d5 Email: send link to homepage via login endpoint
This way we avoid displaying the welcome page if they are not
logged in.
2018-01-26 17:14:57 +01:00
48fd9f401a Email: tweaks to welcome message layout and content
According to the discussion after commit c2518e9ae1.
2018-01-26 17:09:45 +01:00
7ba1c55609 Biling page: moved user classification logic from template → view code
This is the kind of stuff that's much easier expressed in Python than in
the template code.

API calls removed:
  - fetching the user isn't necessary, since we have
    pillar.auth.current_user anyway.
  - a call for each group the user is member of, since we only used it to
    check whether the user has demo access anyway.
2018-01-25 14:06:13 +01:00
33aa819040 Override Roles & Caps explanation with something more specific for Cloud 2018-01-25 14:06:13 +01:00
70f28074a0 Use Jinja2 inheritance to render user settings pages.
This requires Pillar e55d38261b36756a2850716a453c08c9ee6be9e2 or newer.
2018-01-25 14:06:13 +01:00
14d77da47a store.blender.org → EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER
I've only done this on the billing page, because there it's important
for debugging purposes to know which URL was actually checked to obtain
the subscription expiry information.
2018-01-25 14:06:13 +01:00
0908a13519 Avoid error when Store cannot be reached. 2018-01-25 14:06:13 +01:00
06414ab0ed Override for comments_for_node
Make use of the render_comments_for_node function, and define custom logic to determine the value of can_post_comments.
2018-01-20 00:45:57 +01:00
67ce16fc78 Limit video max-width in homepage blog list 2018-01-18 11:57:53 +01:00
d00c32310b Update stats URL to the new Kibana dashboard. 2018-01-12 14:03:42 +01:00
add20f0c6c Fixed indentation 2018-01-12 12:33:48 +01:00
dde590d388 gulp: Don't pass --production to Pillar's gulp when deploying 2018-01-12 12:24:39 +01:00
996beaf090 Bump version of elasticproxy to 1.2 2018-01-12 11:55:13 +01:00
37b84cf75a Upgraded ElasticSearch and Kibana to 6.1.1
Requires a reset + reindex of everything (well, that's the easiest way to
get things indexed properly again), which will loose us the Cloud stats.
Before doing this, export those to MongDB and upgrade the statscollector
to the version that I'll be committing soon.
2018-01-12 11:55:13 +01:00
eb2a058ce2 Python 3.6.3 → 3.6.4 2018-01-12 11:55:13 +01:00
4891803552 Docker: never cache the base image when rebuilding 2018-01-12 11:55:13 +01:00
ab11f98331 Removed notifserv from docker-compose.yml 2018-01-12 11:55:13 +01:00
0ed03240e7 Made docker-compose.yml indentation consistent. 2018-01-12 11:55:07 +01:00
5c26756626 remove algolia 2018-01-12 11:54:39 +01:00
de6cdbaf19 Fixed broken networking with new docker-compose.yml
- No more 'links', all dockers can reach each other by name
- Added 'depends_on', which handles startup sequence
- Allowed haproxy connection to the docker daemon socket
- Told haproxy explicitly which services to proxy. The 'docker:' prefix
  comes from the fact that the directory containing the docker-compose.yml
  file is called 'docker'.
2018-01-03 15:27:28 +01:00
e641565e6a Docker: Limit logging for celery worker 2017-12-22 11:58:47 +01:00
617b600ce8 Upgraded docker-compose.yaml file format from 1 to 3.4
This allows us to set logging options, which weren't available in version 1.
I've also added newlines around each service definition, and made the
formatting consistent across the entire file (using align-yaml, one of the
tools of the atom-beautify plugin for Atom).
2017-12-22 11:54:06 +01:00
4c2632669b PEP8 formatting 2017-12-21 15:26:32 +01:00
c2518e9ae1 Send welcome email to new Cloud subscribers 2017-12-21 15:26:23 +01:00
0b34c5c1c6 Also create user when member of organisation 2017-12-20 14:22:26 +01:00
fd68f3fc8b Allow user creation from Blender ID webhook "user modified"
When the webhook indicates that the user has a Cloud subscription (demo,
active, or renewable), the user is immediately created.
2017-12-20 12:56:48 +01:00
a2c24375e5 Webhooks: smarter way to find the user the webhook applies to.
We now also match on Blender ID (through the 'auth' section of the user
document), so that even when we have missed an email change we can still
apply the changes from the webhook.
2017-12-20 12:22:51 +01:00
16e378b7ad Removed unit test for unknown emails
This behaviour is going to change, and by splitting that change up into
two commits the diff makes much more sense.
2017-12-20 11:43:07 +01:00
be4b36a661 Also accept user-modified webhook when old email address is unknown.
When the old email address is unknown, and the new one does map to a user,
use the webhook to update the new user.
2017-12-20 11:40:56 +01:00
ae700b5eef Added payload description of Blender ID webhook 2017-12-20 11:19:15 +01:00
3712ad6ddc Added test for store.py 2017-12-19 11:09:45 +01:00
b2cad0fb9a Fixed mistake in capability check 2017-12-19 10:51:59 +01:00
5e15185166 HaProxy: Explicitly configure allowed TLS ciphers 2017-12-13 14:00:51 +01:00
2a35c3e157 Deploy script: notify Sentry instead of Bugsnag of the deploy 2017-12-13 12:50:45 +01:00
8a4f6c3649 Switch from macros to blocks for navigation menus
For more informations see a7693aa78dcf0a0a77e113f34afa63fb4f615441 in pillar.git
2017-12-13 11:13:47 +01:00
9c2e6f9081 Fixed forced login for user switching 2017-12-12 11:25:48 +01:00
be62a38cd5 Merge branch 'production' 2017-12-12 10:22:14 +01:00
501fb76c7e Revert "Revert "Removed flamenco-user and attract-user role linking to subscriber/demo/admin""
This reverts commit d46b5d645b.
2017-12-12 10:22:07 +01:00
d46b5d645b Revert "Removed flamenco-user and attract-user role linking to subscriber/demo/admin"
Temporarily reverting due to an issue with missing roles and permissions.

This reverts commit c031c1e8ae.
2017-12-08 17:42:35 +01:00
5efca08c13 Docker Apache: added svnman XSendFilePath 2017-12-08 17:06:56 +01:00
abb0ded3f2 Lowered log level for webhook call for unknown user 2017-12-08 14:08:04 +01:00
f948969b20 Fixed webhook so that users' full_name isn't reset to the empty string. 2017-12-08 14:04:11 +01:00
1ae090789b Deploy: added PermitLocalCommand=no to SSH command
I'm using a LocalCommand invocation to change the terminal colours when
I SSH into a production server; this shouldn't be done by the deploy
script.
2017-12-08 10:12:55 +01:00
c031c1e8ae Removed flamenco-user and attract-user role linking to subscriber/demo/admin
It was a hack due to the lack of a capabilities system.
2017-12-07 17:09:06 +01:00
00805c3372 Upgraded Python to 3.6.3 2017-12-07 14:02:35 +01:00
035382d1e5 docker: allow running 1_base/build.sh with --no-cache
This rebuilds the image from scratch, also pulling in security updates.
2017-12-07 14:02:20 +01:00
7dcc0d5ead Size-based Apache log rotation 2017-12-07 10:21:55 +01:00
bff51e1e83 Added /renew endpoint to redirect to Blender Store renewal URL. 2017-12-06 14:36:02 +01:00
561db0428d Using new blender_id.get_user_blenderid() function 2017-12-05 11:56:33 +01:00
f7ce8d74a7 Implemented Blender ID webhook for user change notifications
Handles changes in:

 - roles
 - full name
 - email address
2017-12-01 19:06:16 +01:00
532bf60041 Obtain subscription info from Store.
This was in Pillar, and is now moved to Blender Cloud. It's used to obtain
the subscription expiration date from Blender Store.
2017-11-30 15:45:40 +01:00
d5d189de8c Reworked subscription reconciliation to use Blender ID instead of Store 2017-11-30 15:30:41 +01:00
5d281dc6ce Introduce 'has_subscription' role with 'can-renew-subscription' capability
This is in preparation of presenting different messages to users, when their
subscription should be renewed, or when they should buy a (new)
subscription.
2017-11-30 15:30:08 +01:00
efc18c5628 Open "this user on" links in new tab 2017-11-29 14:09:13 +01:00
127e422775 Updated Blender ID link to the new Blender ID 2017-11-29 14:05:39 +01:00
59f4edf192 Moved 'This user on Blender Store/ID' links from Pillar to Blender Cloud 2017-11-29 13:59:12 +01:00
5ea47b6f8f Fix navbar-toggle not visible on mobile 2017-11-28 16:06:11 +01:00
e27d8a3f08 Welcome: Replace all store URLs with variable
Also link Hjalti's video to actual video asset
2017-11-28 15:10:40 +01:00
c6407b4f7a Welcome page: Fix links to the store
And make some titles links as well
2017-11-28 14:44:54 +01:00
71bdff82b3 Home: In Production section for ongoing projects 2017-11-27 10:39:34 +01:00
98f695c54b Home: In Production section for ongoing projects 2017-11-24 19:36:43 +01:00
30fe3e1360 Welcome page: Explore should go to cloud.blender.org when logged in
Pointed out by Dr. Sybren
2017-11-24 16:47:32 +01:00
fd6c9ec02d Welcome page: Update prices 2017-11-23 18:31:57 +01:00
62dcd93584 Only load markdown.min.js if current_user has subscriber capabilities
Since commenting or node editing is the only place we use js markdown now.
2017-11-23 16:51:31 +01:00
8fc9529752 Homepage: Re-use render_blog_post macro to display posts
No need to have custom code when we have a nice little macro for it
2017-11-23 16:17:38 +01:00
f4e277f1d1 Homepage: Render attachments on posts 2017-11-23 16:16:10 +01:00
22f5ea7b62 Fixed creation of image sharing group node 2017-11-21 15:16:38 +01:00
1dd3fd3623 Simplified code a bit 2017-11-21 15:16:22 +01:00
48dc3837b5 cloud_share_img.py: stop using my personal hardcoded IDs 2017-11-21 15:09:13 +01:00
c34ffb66db Added CLI script to share images to the Cloud.
Assumes that you are logged in on Blender ID with the Blender ID Add-on.

Haven't tested what happens when you've never shared any images yet.
2017-11-21 15:00:23 +01:00
59d165b6d0 Subscription refresh: refresh the page on a 403
A 403 indicates the session isn't valid any more, and a window reload will
fix that (either by automatically logging in via Blender ID or by
providing a login prompt).
2017-11-17 12:13:25 +01:00
9dfe75ed1e Include pillar-svnman in docker image 2017-11-14 16:28:52 +01:00
494f307900 Fixed deploy script for SVNMan 2017-11-14 16:25:03 +01:00
59dd3352cd Added svnman to deploy.sh 2017-11-14 16:13:03 +01:00
4207d051a9 Added subscriber-pro role.
This is used in svnman to bind to the svn-use capability.
2017-11-14 16:09:29 +01:00
f1d990c03d Added SVNMan extension 2017-11-14 16:09:29 +01:00
08f56877ca Homepage: Small cleanup of classes and more space between posts
Also fix selectors for comments/assets
2017-11-10 17:35:19 +01:00
0c4db84940 Fix link to comments 2017-11-09 19:38:56 +01:00
5be0f3baf0 Update cache for CSS/JS 2017-11-09 18:59:44 +01:00
8288b4eab1 New Homepage: Featuring more in the blog posts 2017-11-09 18:37:32 +01:00
a34f3435c1 gitignore: ignore cloud/static/assets/css 2017-11-09 17:15:26 +01:00
ba9a2eb134 gitignore: absolute path for cloud/templates 2017-11-09 17:15:16 +01:00
73a0de3157 Merge branch 'master' of git.blender.org:blender-cloud 2017-11-08 16:25:12 +01:00
bcab6ac5b7 Stats files are no longer needed 2017-11-08 16:24:58 +01:00
c2492e8710 When logging in from /welcome, redirect to index
Backend logic simplified, to support the next arg if provided, otherwise simply redirect to the request.referrer (last visited page).
2017-11-07 18:25:42 +01:00
08b388f363 Display Log in on button only if current_user.is_anonymous 2017-11-07 18:25:42 +01:00
470fdf89f4 Add alt tag to images in welcome page 2017-11-07 18:25:42 +01:00
c00121d71c Introducing main.sass stylesheet
Blender Cloud used main.css from Pillar

As we try to strip Blender Cloud-specific content from Pillar,
this commit brings three .sass files over to this repository.

There are still plenty of Blender Cloud classes all over Pillar,
they will be brought here over time.
2017-11-07 16:56:45 +01:00
97dff66159 Add rewrite rule for new project 2017-10-25 15:52:33 +02:00
047737ef41 New block in the navigation menu for user specific entries
Called "navigation_user", used at the moment by the
/welcome page for turning "Login" into "Login and Explore"

Also added the block name to {% endblock %} where it makes sense.
There is no functional change, just easier to read.
2017-10-25 15:43:16 +02:00
5eaa202d49 Welcome Page: Fix count of awesome people (Cloud subscribers) 2017-10-23 15:34:31 +02:00
a9788e70c9 Welcome Page: Add links to titles and images 2017-10-23 15:34:00 +02:00
e19a38959e Merge branch 'production' 2017-10-19 14:59:38 +02:00
0c4fbcf65d Refresh the /welcome page 2017-10-18 20:06:34 +02:00
e86cc9df77 Remove deprecated classes 2017-10-18 20:06:11 +02:00
cb08c113af Fix missing background on /about page
Fixes T52867
2017-10-17 15:45:22 +02:00
5d26e3940e Bugsnag app revision: use timestamp instead of Git hash
This turns the 'revision' we send to Bugsnag into an incremental number
that denotes the current time. This is more indicative of the application
version than the git revision of the Blender Cloud repository, as the
latter doesn't include any changes in the other repositories.
2017-10-17 12:32:15 +02:00
5dc4b398c2 updateTitle is no longer needed since we now have DocumentTitleAPI
taking care of titles in 0_navbar.js (part of tutti.min.js)
2017-10-05 15:33:50 +02:00
c25aae82b5 Update title when notifications count has been updated 2017-10-02 19:55:08 +02:00
14d20edbb7 Gulp: converted package.json indentation to tabs 2017-09-28 15:42:50 +02:00
2c4527c4d6 Gulp: added 'cleanup' task that erases all gulp-generated files.
This runs automatically when using --production
2017-09-28 15:37:33 +02:00
3a7f5f7a7d Gulp: replaced hardcoded paths with variables. 2017-09-28 15:37:13 +02:00
ec99b3207a Gulp: fixed license expression 2017-09-28 15:36:42 +02:00
ac7dd4c60a Gulp: fixed project name and repo URL 2017-09-28 15:36:17 +02:00
e45976d4e5 Added ElasticProxy to the docker/README.md file. 2017-09-26 12:27:36 +02:00
565e5e95a5 Disabled web frontend for Grafista.
It still collects daily statistics, until we're 100% sure our own
pillar-statscollector is doing its job correctly.
2017-09-26 12:27:24 +02:00
c2fff6a9cb Removed -verbose from elasticproxy CLI 2017-09-26 11:58:58 +02:00
fa249d1952 Kibana: include config file that doesn't refer to xpack
The default Kibana config file still has an option to enable X-pack, which
is now removed.
2017-09-26 11:53:22 +02:00
d02de8e18b Disable Dev Tools panel. 2017-09-26 11:47:09 +02:00
c6d645f664 Removed old X-pack options
These aren't necessary any more now that we use an image with X-pack.
2017-09-26 11:46:59 +02:00
d90794a4f0 Put elasticproxy in between Kibana and ElasticSearch
This blocks any data-changing HTTP request from reaching ElasticSearch.
2017-09-26 11:27:45 +02:00
13ed89c480 Add terms and conditions and privacy statement 2017-09-21 22:17:52 +02:00
ab41f3afcd Spaces to tabs in pug file 2017-09-21 22:17:05 +02:00
5128675f55 Homepage Featured Project: summary link to project 2017-09-21 19:13:50 +02:00
5d0b8a1dc4 Added static page that embeds stats from Kibana.
It just has a full-width iframe that embeds the dashboard from
https://stats.cloud.blender.org/
2017-09-21 14:25:14 +02:00
82a4bcd185 Allow letsencrypt to run on stats.cloud.blender.org 2017-09-21 13:50:44 +02:00
4432e5b645 Added note in docker/README about permissions on /data/storage/elasticsearch 2017-09-21 13:50:31 +02:00
5948ee7c6f Kibana: more frequent garbage collection to limit memory usage
See https://github.com/elastic/kibana/issues/5170#issuecomment-163042525
2017-09-21 11:52:22 +02:00
5b5005f33e Oops 2017-09-21 11:46:01 +02:00
98177df7bd Elastic: moved memory limit to environment variable
This allows us to easily change it without rebuilding the Docker image.
2017-09-21 11:38:17 +02:00
8e6fc604e3 Build custom images for ElasticSearch & Kibana
We can then remove X-Pack and control ElasticSearch's memory usage.

This also gives us the opportunity to let Kibana do its optimization when
we build the image, rather than every time the container is recreated.
2017-09-21 11:28:16 +02:00
2e2fc791e1 Added Kibana to the docker-compose stack 2017-09-20 16:58:18 +02:00
00eb6d8685 Upgraded ElasticSearch 5.6.0 → 5.6.1 2017-09-19 13:48:53 +02:00
41d47cdeb8 PEP8 formatting 2017-09-19 13:45:48 +02:00
19ccffa4ae Cache the homepage template context.
This requires pillar-python-sdk c8eec9fa9d8a198df198538a38ca1ad2367bb3e6
or newer.
2017-09-19 13:45:10 +02:00
aaf95f96a7 Fixed user switching.
Basically this copies bd976e6c2e7867fee70c8654cc887bf1d3973bc1 from Pillar.
2017-09-19 13:39:41 +02:00
9034c36564 Added ElasticSearch docker container.
So far it's just a standalone docker container, as there is no publicly
accessible Kibana container yet. To use, just SSH-tunnel port 9200.
2017-09-18 17:38:55 +02:00
f3e0484328 Revert "Cache the entire homepage for 5 minutes."
This reverts commit 9a692d475b.
2017-09-18 13:05:29 +02:00
f74e05229c Replace pillar-web with blender-cloud, and add HTTPS support
Other vhosts are already configured to use the 'blender-cloud' hostname,
and now the main one is too. It also adds HTTPS support, so that you can
test locally without having to set FORCE_SSL to false. This does require
you to create a TLS certificate in /data/certs/blender-cloud.pem, using:

openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
cat key.pem cert.pem > blender-cloud.pem
rm key.pem cert.pem
2017-09-18 12:21:33 +02:00
6d9192f0ab Upgrade Python 3.6.1 → 3.6.2 2017-09-18 12:17:50 +02:00
869e228069 Homepage Random Featured: Show project more prominently for first item 2017-09-17 20:12:47 +02:00
9a692d475b Cache the entire homepage for 5 minutes.
This means that comments and blog posts will take at most 5 minutes to show
up on /.
2017-09-15 17:46:20 +02:00
0b61e517d2 Added missing attached_to.url assignment 2017-09-15 17:03:31 +02:00
0c251c0470 Added missing projection 2017-09-15 15:46:52 +02:00
e103a712b3 Performance improvements for the homepage (activity, blog, random featured) 2017-09-15 15:25:27 +02:00
a7889214cf Locally hosting the Roboto font.
This removes the loadCSS() call, which uses setTimer() to repeatedly do
stuff while other stuff is loading. This saves quite a bit of CPU time
spent on JavaScript.
2017-09-15 11:45:09 +02:00
b554756f54 HaProxy: enable gzip compression for certain content types. 2017-09-15 11:03:28 +02:00
ea8ef9c8bb Restart Celery Beat docker container after deploy 2017-09-14 17:21:24 +02:00
f2ad7b8868 Bumped thread count to 32, lowered proc count to 2
We have 2 CPUs at our DigitalOcean droplet, hence the proc count.
2017-09-14 17:17:45 +02:00
22b8ed0bc0 Added "celery beat" docker container 2017-09-14 15:58:03 +02:00
8df67bfeb5 After building the image, tell the user the image name
This makes pushing easier, as you can copy-paste the name into the push
command.
2017-09-12 12:10:32 +02:00
f4ac9635a9 Fixed two mistakes in stats query count_nodes()
- 'p.x' → 'project.x'
- is_private: {$ne: true} → is_private: False, because true is the default.
2017-09-12 11:29:36 +02:00
ea0f88b4b8 rsync_ui: delete old files when rsyncing.
This is especially necessary for custom node templates, as those are used
when they exist, with fallback to generic templates. When those files are
removed from the repository, they should be removed from the production
server too.
2017-09-07 15:48:38 +02:00
020f70b2cb rsync_ui: nicer error message 2017-09-07 15:47:42 +02:00
2bac3997f6 Link to Blender Cloud add-on (and not the obsolete bundle link) 2017-09-05 11:32:24 +02:00
ff8e338f86 Don't include raw & chars in HTML 2017-09-05 11:31:39 +02:00
d80fc48635 Redirect properly after login
- Use next=xxx URL parameter
- Redirect to / when no next=xxx was given and the referrer is /welcome
2017-09-01 17:04:41 +02:00
9a49cb536b Run ./gulp script when deploying, rather than running gulp directly
This ensures dependencies are rebuilt when they changed.
2017-09-01 11:29:42 +02:00
a9280dd05f Introducing settings for blender-cloud
Moved Blender Cloud specific settings from Pillar into this application.
2017-08-30 23:13:06 +02:00
0b2583c22e Layout: Headers backdrops are no longer used 2017-08-30 15:42:03 +02:00
835851c1d7 Gulp: Fix livereload 2017-08-30 15:06:09 +02:00
1fb3c88a8f Override /login endpoint
For Blender Cloud we want to point directly to blender-id OAuth.
2017-08-25 11:56:43 +02:00
732fe7bc7c Fixed calls to do_badger to use keyword arguments for role(s)
- updated call to pass multiple roles at once
- updated call to pass single role with keyword arg

This requires Pillar 87afbc52f6c7d5eb63f0da745625d05b01e81783
2017-08-23 09:32:01 +02:00
838d85a2b7 Use capabilities system to check whether the user is a subscriber. 2017-08-22 16:40:05 +02:00
2e4ed6f3c8 Also allow demo users access to Flamenco & Attract 2017-08-15 15:25:53 +02:00
fd600c9fa0 Fix for broken url_for 2017-07-27 09:30:09 +02:00
7f65b77824 New redirect from /training to /courses 2017-07-26 16:51:35 +02:00
4575e8bf8b Public project changes
Now we organize training content into 'courses' and 'workshops'. This commit updates various endpoints and menus to reflect that decision.
2017-07-26 16:51:16 +02:00
fe408c3151 Moved projects index_collection from Pillar 2017-07-26 16:49:40 +02:00
9bf6a36bdd Return cached view for welcome page only if user is anonymous 2017-07-16 00:06:18 +02:00
560c13d3a8 Add node_modules to .gitignore 2017-07-16 00:05:35 +02:00
f33bb946ac Fix for static path 2017-07-14 12:57:58 +02:00
bda679a3bf Remove about.html
This is build with Gulp from a .pug file now.
2017-07-14 12:45:35 +02:00
0ea8b02920 Tweaks to rsync_ui.sh
Move Blender Cloud checks before Pillar deploy commands. Also add prefixes to distinguish between Pillar vs. cloud assets.
2017-07-14 12:44:32 +02:00
ae33a6f71e Moving Blender Cloud specific pages from Pillar
These pages were originally in the pillar repository, but they actually belong here. We also extended rsync_ui.sh to build and rsync to the Blender Cloud server.
2017-07-13 18:35:18 +02:00
f6df37ec24 Introducing the gulp command
From now on we work with .pug files for templates.
2017-07-13 18:26:54 +02:00
fca78faca0 Replace blurry image 2017-07-13 12:46:48 +02:00
c648fe0ca5 Thumbnails for Services page 2017-07-11 18:45:46 +02:00
b155b0916e CLI reconcile_users: process 10 users in parallel 2017-07-11 14:57:57 +02:00
f493d0a566 CLI reconcile_subscribers: show user count + index 2017-07-11 14:44:00 +02:00
093d80905f CLI reconcile_subscribers: show user count before starting 2017-07-11 14:40:46 +02:00
54e9aca16f CLI reconcile_subscribers uses do_badger & re-grants the subscriber role
This ensures that subscriber-linked roles like flamenco-user are synced
along with the subscriber status, and that Algolia picks up on those
changes as well.
2017-07-11 14:29:14 +02:00
3e5ad280b1 CLI command to create standard groups admin/demo/subscriber
This is part of what setup_db also does, but only for creating those
groups. Only really useful for debugging/developing.
2017-07-11 14:27:33 +02:00
0a9f0ebddb Roles '{flamenco,attract}-user' are now linked to 'subscriber'
All users who get 'subscriber' role automatically get 'flamenco-user' and
'attract-user', and when 'subscriber' is revoked, so are
'{flamenco,attract}-user'.
2017-07-11 12:40:13 +02:00
a43e84f93e Made script executable 2017-07-07 12:03:18 +02:00
56af1cd96c Added renewal script for Let's Encrypt 2017-07-07 12:02:49 +02:00
69862b8416 Docker: removed blender-cloud/.well-known from letsencrypt
That domain is handled by a different host.
2017-07-07 11:40:27 +02:00
b9a7f501ba Docker: Dropping support for cloudapi.blender.org 2017-07-07 11:40:25 +02:00
6fc77522ea Removed localhost declaration 2017-07-07 11:24:14 +02:00
538a10af60 Added docker container for serving letsencrypt webroot. 2017-07-07 11:23:38 +02:00
fd700ca6d9 Inject current_user_is_subscriber into Jinja context
This variable is set to True iff the user has a subscriber/demo/admin role.
It's an attempt to get some of the Cloud subscription specific stuff out
of Pillar.
2017-06-14 16:26:29 +02:00
e69ac4c5b9 Restart Celery worker after deploying new version. 2017-06-08 11:55:52 +02:00
a62d1e3d7d Updated celery-worker.sh to actually star the Celery worker. 2017-06-07 17:19:02 +02:00
d56e1afe73 Upgraded docker images Python 3.6.0 → 3.6.1 2017-06-07 13:22:57 +02:00
8b110df32a Convert all Redirects to RewriteRules in Apache conf
This allows for more precise redirection. Using the correct code (301, permanently moved) and L (Last rule, prevent any further evaluation).
2017-06-02 11:18:57 +02:00
9da841efc3 Added Celery worker docker container
This docker container uses the Blender Cloud image, but a different entry
point. It is not intended to be network-reachable from the outside world.
All it needs are connections to the databases (mongo, redis, rabbit).
2017-06-02 10:55:18 +02:00
c73efd5998 Added RabbitMQ, for serving as Celery broker. 2017-06-02 10:55:18 +02:00
93edfb789f Add Blender Cloud specific redirects
Originially hardcoded in Pillar, these redirects are rarely changed or added.
2017-06-01 17:27:00 +02:00
f0e53245a6 New images for the homepage 2017-05-22 16:01:35 +02:00
c775126cba Docker startup: create ${APACHE_LOG_DIR} if it doesn't exist yet 2017-05-17 10:34:13 +02:00
39ce21f7f9 Copy /lib/systemd/system/docker.service to /etc/systemd/system/docker.service
This allows later upgrading of docker without overwriting the changes
indicated in the README.
2017-05-17 10:12:22 +02:00
3308018887 Docker: replaced "latest-py36" with "latest" in README.md 2017-05-17 10:11:49 +02:00
b93448f682 Start cron when starting the Cloud docker
This will regularly run logrotate, preventing the build-up of log files
of multiple gigabytes.
2017-05-17 10:08:27 +02:00
2892a46a27 Docker: store Cloud container /var/log on host in /data/log 2017-05-17 10:07:40 +02:00
e2efc70e44 Import new subscription info function 2017-05-04 18:17:37 +02:00
bd9265e4f1 Tag blender_cloud Docker image as latest 2017-04-11 12:52:44 +02:00
57aeea337b Switch from id --group id -g
This makes the script compatible with macOS.
2017-04-11 12:34:07 +02:00
5c3d76c660 Added echo command to deploy.sh
We had a problem where the 'docker exec' command would hang, and having
an explicit "I'm now doing this thing" in the console helps analyse this.
2017-04-07 12:27:30 +02:00
9b0e996c10 Background images for Agent 327 and Andy's Waking the Forest 2017-03-22 21:57:12 +01:00
cad77f001c Fix start date of Blender Cloud in the About page 2017-03-18 12:47:08 +01:00
4c149b0d24 Use -W instead of -w for ping in deploy.sh
Use the waittime argument instead of deadline, since the latter is not supported on macOS.
2017-03-17 12:02:05 +01:00
28f9ec5ffa Support the before query for user count and blender syc users 2017-03-17 11:41:44 +01:00
56b5c89edf Fix missing cast for the before var 2017-03-17 11:41:00 +01:00
7472cf2b16 Properly count Blender Sync users
Use a dedicated aggregation pipeline to filter one startup.blend per home project and count them.
2017-03-17 10:46:29 +01:00
e09c73ec39 Introducing endpoint for stats
This endpoint can be queried on a daily basis to retrieve cloud usage  stats. For assets and comments we take into considerations only those who belong to public projects.
2017-03-17 09:40:50 +01:00
59ffebd33e Mount grafista storage volume in the correct path 2017-03-12 12:38:59 +01:00
e5e05bf086 Add cronjob for grafista data collection 2017-03-12 12:15:44 +01:00
3e9e075b1f Use production branch when checking out repos
Also added grafista repo to the list.
2017-03-12 12:15:21 +01:00
5c01c6ec3f Fixed path error in runserver.wsgi + no more hardcoded path. 2017-03-10 16:52:16 +01:00
343398a813 Improve image quality pictures in About page 2017-03-10 16:45:33 +01:00
88ff479140 New photo for Sybren 2017-03-10 16:22:54 +01:00
34affb0eac Removed executable permission from cli.py 2017-03-10 16:16:36 +01:00
17dfdc1825 Removed executable permission from images 2017-03-10 16:14:23 +01:00
c9aaebb370 Add images to about page 2017-03-10 16:04:42 +01:00
1d687d5c01 Introducing Cloud extension
We use a Pillar extension to register Blender Cloud specific endpoints.
2017-03-10 15:36:55 +01:00
2f1a32178a Explicitly pass hostname to deploy script.
The script now also pings the hostname to deply to, to see if it's alive
before doing anything else.
2017-03-10 14:24:22 +01:00
6cfe00c3ca Docker-compose: pinning new version of haproxy
This is actually the one we used in production.
2017-03-10 11:16:28 +01:00
727707e611 Allow deploying to either production or staging.
Requires that you set up 'cloud2' as a hostname for the staging server.
2017-03-10 09:55:04 +01:00
6e1425ab25 Docker: removed superfluous ; 2017-03-10 09:54:27 +01:00
85f3f19c34 Added some more deployment documentation 2017-03-09 15:45:51 +01:00
557ce1b922 Docker: add useful tail /var/log/apache2/access.log to bash_history 2017-03-09 15:30:45 +01:00
d2d04b2398 Docker: added missing libraries for JPEG and PNG support in Pillow. 2017-03-09 15:30:32 +01:00
e31b3cf8b4 Docker: pin specific versions for images, for reproducible deploys. 2017-03-09 11:02:04 +01:00
85c2b1bcd6 Docker: stop on errors in 3_buildwheels/build.sh 2017-03-09 11:01:36 +01:00
79b8194b2a Docker: exec single commands
This replaces bash with the docker command, freeing memory and
automatically returning the exit code of the docker command as the exit
code of the shell script.
2017-03-09 11:01:24 +01:00
06cc338b08 Docker: always apt-get update before apt-get install 2017-03-09 11:00:21 +01:00
b0ab696e49 Started documenting steps to set up a production machine from scratch. 2017-03-08 17:23:00 +01:00
30c9cfd538 Use armadillica/blender_cloud:latest-py36 in this branch 2017-03-08 17:18:52 +01:00
3af92b4436 Don't install subpackages as editable in requirements.txt
Doing this would require editable (and thus writable) checkouts, which
we don't have on our production machines.
2017-03-08 17:17:53 +01:00
2f6049edee Docker images: renamed pillar_py:3.6 to armadillica/pillar_py:3.6
This allows us to push the Python image to Docker Hub.
2017-03-08 13:55:02 +01:00
71a1a69f16 Updated paths for XSendFilePath
Now that we use egg links, and not symlinks, to install our packages,
we can use the actual paths.
2017-03-08 13:02:36 +01:00
fab68aa802 Removed virtualenv from manage.sh, and using exec 2017-03-08 12:38:16 +01:00
e27f5b7cec Docker-compose: use /data/git as one volume, instead of mapping all subdirs 2017-03-08 12:38:00 +01:00
d42762b457 Cleaned up runserver.wsgi to not depend on flup
It's not necessary; we already don't install it any more either.
2017-03-08 12:37:26 +01:00
a332f627a4 Tweaked docker-entrypoint.sh to properly install packages. 2017-03-08 12:36:06 +01:00
9fe7d7fd2b WIP: More docker tweaks 2017-03-08 12:35:35 +01:00
6adf45a94a Be more selective in what we install on the production docker image. 2017-03-08 12:34:48 +01:00
e443885460 Create links python and pip to python3 and pip3. 2017-03-08 12:33:15 +01:00
e086862567 WIP: building mod_wsgi against Python 3.6
The module is included in the built Python directory, in
/opt/python/mod-wsgi/mod_wsgi.so
2017-03-08 12:32:54 +01:00
5ad9f275ef Strip Python install, saves roughly 90 MB in final image size. 2017-03-07 23:01:19 +01:00
faf38dea7e Uncommented some accidentally commented-out stuff 2017-03-07 23:00:10 +01:00
d7e4995cfa WIP: more work on the docker structure, still not finished with 4_run
1_base: builds a base image, based on Ubuntu 16.10
2_buildpy: builds two images:
	2a: an image that can build Python 3.6
	2b: an image that contains the built Python 3.6 in /opt/python
3_buildwheels: builds an image to build wheel files, puts them in ../4_run
4_run: the production runtime image, which can't build anything and just runs.
2017-03-07 22:41:05 +01:00
af14910fa9 WIP breaking stuff: updating docker image build process for Python 3.6
This requires a new way to pass requirements.txt files to Docker (since
they now link to each other), as well as building Python ourselves (since
even Ubuntu 16.10 doesn't have a decent Python 3.6).

This is just a WIP commit, will be fixed soon(ish).
2017-03-07 16:51:51 +01:00
b6f729f35e Added requirements-dev.txt
It just links to the requirements-dev.txt files of the subprojects.
2017-03-07 14:21:15 +01:00
d3427bb73a Updated README for Python 3.6
Also mentioned Flamenco, made the "mkdir" command a bit more efficient, and used Python 3's pip.
2017-03-07 13:59:49 +01:00
039983dc00 README: added missing slashes to URLs 2017-03-07 13:57:41 +01:00
4a148f9163 Re-enabled Flamenco, as it seems to be working on Py36 2017-03-03 17:37:13 +01:00
df137c3258 Re-enabled Attract, it seems to work on Py3.6 2017-03-03 17:00:02 +01:00
3b239130d8 Removed all requirements; referring to other requirements.txt files
Also using -e to install required packages that aren't pip-installable.
2017-03-03 14:41:14 +01:00
d4984c495e Python 3.6: removed unnecessary __future__ import 2017-03-03 14:40:35 +01:00
566c89d745 Disabled Flamenco and Attract, until they are also ported to Python 3.6 2017-03-03 14:40:01 +01:00
cb44509a18 rsync_ui.sh: error out when one of the commands in the script errors. 2017-02-21 13:25:57 +01:00
155 changed files with 7064 additions and 367 deletions

7
.gitignore vendored
View File

@@ -4,6 +4,10 @@
*.pyc *.pyc
__pycache__ __pycache__
/cloud/templates/
/cloud/static/assets/css/
node_modules/
/config_local.py /config_local.py
/build /build
@@ -12,4 +16,5 @@ __pycache__
/.eggs/ /.eggs/
/dump/ /dump/
/google_app*.json /google_app*.json
/docker/3_run/wheelhouse/ /docker/2_buildpy/python/
/docker/4_run/wheelhouse/

View File

@@ -1,31 +1,35 @@
# Blender Cloud # Blender Cloud
Welcome to the [Blender Cloud](https://cloud.blender.org) code repo! Welcome to the [Blender Cloud](https://cloud.blender.org/) code repo!
Blender Cloud runs on the [Pillar](https://pillarframework.org) framework. Blender Cloud runs on the [Pillar](https://pillarframework.org/) framework.
## Quick setup ## Quick setup
Set up a node with these commands. Note that that script is already outdated... Set up a node with these commands.
``` ```
#!/usr/bin/env bash #!/usr/bin/env bash
mkdir -p /data/git sudo mkdir -p /data/{git,storage,config,certs}
mkdir -p /data/storage
mkdir -p /data/config
mkdir -p /data/certs
sudo apt-get update sudo apt-get update
sudo apt-get -y install python-pip sudo apt-get -y install python3-pip
pip install docker-compose pip3 install docker-compose
cd /data/git cd /data/git
git clone git://git.blender.org/pillar-python-sdk.git git clone git://git.blender.org/pillar-python-sdk.git
git clone git://git.blender.org/pillar-server.git pillar git clone git://git.blender.org/pillar.git -b production
git clone git://git.blender.org/attract.git git clone git://git.blender.org/attract.git -b production
git clone git://git.blender.org/flamenco.git git clone git://git.blender.org/flamenco.git -b production
git clone git://git.blender.org/blender-cloud.git 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
``` ```
After these commands, run `deploy.sh` to build the static files and deploy
those too (see below).
## Deploying to production server ## Deploying to production server
First of all, add those aliases to the `[alias]` section of your `~/.gitconfig` First of all, add those aliases to the `[alias]` section of your `~/.gitconfig`
@@ -71,8 +75,8 @@ Now follow the above receipe on the Blender Cloud project as well.
Contrary to the subprojects, `git pp` will actually perform the deploy Contrary to the subprojects, `git pp` will actually perform the deploy
for real. for real.
Now you can press `[ENTER]` in the Pillar and Attract terminals that Now you can press `[ENTER]` in the Pillar, Attract, and Flamenco terminals
were still waiting for it. that were still waiting for it.
After everything is done, your (sub)projects should all be back on After everything is done, your (sub)projects should all be back on
the master branch. the master branch.

102
cloud/__init__.py Normal file
View File

@@ -0,0 +1,102 @@
import logging
import flask
from werkzeug.local import LocalProxy
from pillar.api.utils import authorization
from pillar.extension import PillarExtension
EXTENSION_NAME = 'cloud'
class CloudExtension(PillarExtension):
has_context_processor = True
user_roles = {'subscriber-pro', 'has_subscription'}
user_roles_indexable = {'subscriber-pro', 'has_subscription'}
user_caps = {
'has_subscription': {'can-renew-subscription'},
}
def __init__(self):
self._log = logging.getLogger('%s.CloudExtension' % __name__)
@property
def name(self):
return EXTENSION_NAME
def flask_config(self):
"""Returns extension-specific defaults for the Flask configuration.
Use this to set sensible default values for configuration settings
introduced by the extension.
:rtype: dict
"""
# Just so that it registers the management commands.
from . import cli
return {
'EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER': 'https://store.blender.org/api/',
'EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS': 10,
'BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET': 'oos9wah1Zoa0Yau6ahThohleiChephoi',
}
def eve_settings(self):
"""Returns extensions to the Eve settings.
Currently only the DOMAIN key is used to insert new resources into
Eve's configuration.
:rtype: dict
"""
return {}
def blueprints(self):
"""Returns the list of top-level blueprints for the extension.
These blueprints will be mounted at the url prefix given to
app.load_extension().
:rtype: list of flask.Blueprint objects.
"""
from . import routes
import cloud.stats.routes
return [
routes.blueprint,
cloud.stats.routes.blueprint,
]
@property
def template_path(self):
import os.path
return os.path.join(os.path.dirname(__file__), 'templates')
@property
def static_path(self):
import os.path
return os.path.join(os.path.dirname(__file__), 'static')
def context_processor(self):
return {
'current_user_is_subscriber': authorization.user_has_cap('subscriber')
}
def setup_app(self, app):
from . import routes, webhooks, eve_hooks, email
routes.setup_app(app)
app.register_api_blueprint(webhooks.blueprint, '/webhooks')
eve_hooks.setup_app(app)
email.setup_app(app)
def _get_current_cloud():
"""Returns the Cloud extension of the current application."""
return flask.current_app.pillar_extensions[EXTENSION_NAME]
current_cloud = LocalProxy(_get_current_cloud)
"""Cloud extension of the current app."""

129
cloud/cli.py Normal file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python
import logging
from urllib.parse import urljoin
from flask import current_app
from flask_script import Manager
import requests
from pillar.cli import manager
from pillar.api import service
log = logging.getLogger(__name__)
manager_cloud = Manager(
current_app, usage="Blender Cloud scripts")
@manager_cloud.command
def create_groups():
"""Creates the admin/demo/subscriber groups."""
import pprint
group_ids = {}
groups_coll = current_app.db('groups')
for group_name in ['admin', 'demo', 'subscriber']:
if groups_coll.find({'name': group_name}).count():
log.info('Group %s already exists, skipping', group_name)
continue
result = groups_coll.insert_one({'name': group_name})
group_ids[group_name] = result.inserted_id
service.fetch_role_to_group_id_map()
log.info('Created groups:\n%s', pprint.pformat(group_ids))
@manager_cloud.command
def reconcile_subscribers():
"""For every user, check their subscription status with the store."""
import threading
import concurrent.futures
from pillar.auth import UserClass
from pillar.api import blender_id
from pillar.api.blender_cloud.subscription import do_update_subscription
sessions = threading.local()
service.fetch_role_to_group_id_map()
users_coll = current_app.data.driver.db['users']
found = users_coll.find({'auth.provider': 'blender-id'})
count_users = found.count()
count_skipped = count_processed = 0
log.info('Processing %i users', count_users)
lock = threading.Lock()
real_current_app = current_app._get_current_object()
api_token = real_current_app.config['BLENDER_ID_USER_INFO_TOKEN']
api_url = real_current_app.config['BLENDER_ID_USER_INFO_API']
def do_user(idx, user):
nonlocal count_skipped, count_processed
log.info('Processing %i/%i %s', idx + 1, count_users, user['email'])
# Get the Requests session for this thread.
try:
sess = sessions.session
except AttributeError:
sess = sessions.session = requests.Session()
# Get the info from Blender ID
bid_user_id = blender_id.get_user_blenderid(user)
if not bid_user_id:
with lock:
count_skipped += 1
return
url = urljoin(api_url, bid_user_id)
resp = sess.get(url, headers={'Authorization': f'Bearer {api_token}'})
if resp.status_code == 404:
log.info('User %s with Blender ID %s not found, skipping', user['email'], bid_user_id)
with lock:
count_skipped += 1
return
if resp.status_code != 200:
log.error('Unable to reach Blender ID (code %d), aborting', resp.status_code)
with lock:
count_skipped += 1
return
bid_user = resp.json()
if not bid_user:
log.error('Unable to parse response for user %s, aborting', user['email'])
with lock:
count_skipped += 1
return
# Actually update the user, and do it thread-safe just to be sure.
with real_current_app.app_context():
local_user = UserClass.construct('', user)
with lock:
do_update_subscription(local_user, bid_user)
count_processed += 1
with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
future_to_user = {executor.submit(do_user, idx, user): user
for idx, user in enumerate(found)}
for future in concurrent.futures.as_completed(future_to_user):
user = future_to_user[future]
try:
future.result()
except Exception as ex:
log.exception('Error updating user %s', user)
log.info('Done reconciling %d subscribers', count_users)
log.info(' processed: %d', count_processed)
log.info(' skipped : %d', count_skipped)
manager.add_command("cloud", manager_cloud)

33
cloud/email.py Normal file
View File

@@ -0,0 +1,33 @@
import functools
import logging
import flask
from pillar.auth import UserClass
log = logging.getLogger(__name__)
def queue_welcome_mail(user: UserClass):
"""Queue a welcome email for execution by Celery."""
assert user.email
log.info('queueing welcome email to %s', user.email)
subject = 'Welcome to Blender Cloud'
render = functools.partial(flask.render_template, subject=subject, user=user)
text = render('emails/welcome.txt')
html = render('emails/welcome.html')
from pillar.celery import email_tasks
email_tasks.send_email.delay(user.full_name, user.email, subject, text, html)
def user_subscription_changed(user: UserClass, *, grant_roles: set, revoke_roles: set):
if user.has_cap('subscriber') and 'has_subscription' in grant_roles:
log.info('user %s just got a new subscription', user.email)
queue_welcome_mail(user)
def setup_app(app):
from pillar.api.blender_cloud import subscription
subscription.user_subscription_updated.connect(user_subscription_changed)

39
cloud/eve_hooks.py Normal file
View File

@@ -0,0 +1,39 @@
import logging
import typing
from pillar.auth import UserClass
from . import email
log = logging.getLogger(__name__)
def welcome_new_user(user_doc: dict):
"""Sends a welcome email to a new user."""
user_email = user_doc.get('email')
if not user_email:
log.warning('user %s has no email address', user_doc.get('_id', '-no-id-'))
return
# Only send mail to new users when they actually are subscribers.
user = UserClass.construct('', user_doc)
if not (user.has_cap('subscriber') or user.has_cap('can-renew-subscription')):
log.debug('user %s is new, but not a subscriber, so no email for them.', user_email)
return
email.queue_welcome_mail(user)
def welcome_new_users(user_docs: typing.List[dict]):
"""Sends a welcome email to new users."""
for user_doc in user_docs:
try:
welcome_new_user(user_doc)
except Exception:
log.exception('error sending welcome mail to user %s', user_doc)
def setup_app(app):
app.on_inserted_users += welcome_new_users

449
cloud/routes.py Normal file
View File

@@ -0,0 +1,449 @@
import functools
import json
import logging
import typing
from flask_login import current_user, login_required
import flask
from flask import Blueprint, render_template, redirect, session, url_for, abort, flash
from pillarsdk import Node, Project, User, exceptions as sdk_exceptions, Group
from pillarsdk.exceptions import ResourceNotFound
from pillar import current_app
import pillar.api
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
blueprint = Blueprint('cloud', __name__)
log = logging.getLogger(__name__)
@blueprint.route('/')
def homepage():
if current_user.is_anonymous:
return redirect(url_for('cloud.welcome'))
return render_template(
'homepage.html',
api=system_util.pillar_api(),
**_homepage_context(),
)
def _homepage_context() -> dict:
"""Returns homepage template context variables."""
# 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)
# Get a list of random featured assets
random_featured = get_random_featured_nodes()
# Parse results for replies
to_remove = []
@functools.lru_cache()
def _find_parent(parent_node_id) -> Node:
return Node.find(parent_node_id,
{'projection': {
'_id': 1,
'name': 1,
'node_type': 1,
'project': 1,
'properties.url': 1,
}},
api=api)
for idx, comment in enumerate(latest_comments._items):
if comment.properties.is_reply:
try:
comment.attached_to = _find_parent(comment.parent.parent)
except ResourceNotFound:
# Remove this comment
to_remove.append(idx)
else:
comment.attached_to = comment.parent
for idx in reversed(to_remove):
del latest_comments._items[idx]
for comment in latest_comments._items:
if not comment.attached_to:
continue
comment.attached_to.url = url_for_node(node=comment.attached_to)
comment.url = url_for_node(node=comment)
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('/login')
def login():
from flask import request
if request.args.get('force'):
log.debug('Forcing logout of user before rendering login page.')
pillar.auth.logout_user()
next_after_login = request.args.get('next')
if not next_after_login:
next_after_login = request.referrer
session['next_after_login'] = next_after_login
return redirect(url_for('users.oauth_authorize', provider='blender-id'))
@blueprint.route('/welcome')
def welcome():
# Workaround to cache rendering of a page if user not logged in
@current_app.cache.cached(timeout=3600, unless=current_user_is_authenticated)
def render_page():
return render_template('welcome.html')
return render_page()
@blueprint.route('/about')
def about():
return render_template('about.html')
@blueprint.route('/services')
def services():
return render_template('services.html')
@blueprint.route('/stats')
def stats():
return render_template('stats.html')
@blueprint.route('/join')
def join():
"""Join page"""
return redirect('https://store.blender.org/product/membership/')
@blueprint.route('/renew')
def renew_subscription():
return render_template('renew_subscription.html')
def get_projects(category):
"""Utility to get projects based on category. Should be moved on the API
and improved with more extensive filtering capabilities.
"""
api = system_util.pillar_api()
projects = Project.all({
'where': {
'category': category,
'is_private': False},
'sort': '-_created',
}, api=api)
for project in projects._items:
attach_project_pictures(project, api)
return projects
@blueprint.route('/courses')
def courses():
@current_app.cache.cached(timeout=3600, unless=current_user_is_authenticated)
def render_page():
projects = get_projects('course')
return render_template(
'projects_index_collection.html',
title='courses',
projects=projects._items,
api=system_util.pillar_api())
return render_page()
@blueprint.route('/open-projects')
def open_projects():
@current_app.cache.cached(timeout=3600, unless=current_user_is_authenticated)
def render_page():
projects = get_projects('film')
return render_template(
'projects_index_collection.html',
title='open-projects',
projects=projects._items,
api=system_util.pillar_api())
return render_page()
@blueprint.route('/workshops')
def workshops():
@current_app.cache.cached(timeout=3600, unless=current_user_is_authenticated)
def render_page():
projects = get_projects('workshop')
return render_template(
'projects_index_collection.html',
title='workshops',
projects=projects._items,
api=system_util.pillar_api())
return render_page()
def get_random_featured_nodes() -> typing.List[dict]:
"""Returns a list of project/node combinations for featured nodes.
A random subset of 3 featured nodes from all public projects is returned.
Assumes that the user actually has access to the public projects' nodes.
The dict is a node, with a 'project' key that contains a projected project.
"""
proj_coll = current_app.db('projects')
featured_nodes = proj_coll.aggregate([
{'$match': {'is_private': False}},
{'$project': {'nodes_featured': True,
'url': True,
'name': True,
'summary': True,
'picture_square': True}},
{'$unwind': {'path': '$nodes_featured'}},
{'$sample': {'size': 3}},
{'$lookup': {'from': 'nodes',
'localField': 'nodes_featured',
'foreignField': '_id',
'as': 'node'}},
{'$unwind': {'path': '$node'}},
{'$project': {'url': True,
'name': True,
'summary': True,
'picture_square': True,
'node._id': True,
'node.name': True,
'node.permissions': True,
'node.picture': True,
'node.properties.content_type': True,
'node.properties.url': True}},
])
featured_node_documents = []
api = system_util.pillar_api()
for node_info in featured_nodes:
# Turn the project-with-node doc into a node-with-project doc.
node_document = node_info.pop('node')
node_document['project'] = node_info
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)
return featured_node_documents
@blueprint_settings.route('/emails', methods=['GET', 'POST'])
@login_required
def emails():
"""Main email settings.
"""
if current_user.has_role('protected'):
return abort(404) # TODO: make this 403, handle template properly
api = system_util.pillar_api()
user = User.find(current_user.objectid, api=api)
# Force creation of settings for the user (safely remove this code once
# implemented on account creation level, and after adding settings to all
# existing users)
if not user.settings:
user.settings = dict(email_communications=1)
user.update(api=api)
if user.settings.email_communications is None:
user.settings.email_communications = 1
user.update(api=api)
# Generate form
form = forms.UserSettingsEmailsForm(
email_communications=user.settings.email_communications)
if form.validate_on_submit():
try:
user.settings.email_communications = form.email_communications.data
user.update(api=api)
flash("Profile updated", 'success')
except sdk_exceptions.ResourceInvalid as e:
message = json.loads(e.content)
flash(message)
return render_template('users/settings/emails.html', form=form, title='emails')
@blueprint_settings.route('/billing')
@login_required
def billing():
"""View the subscription status of a user
"""
from . import store
log.debug('START OF REQUEST')
if current_user.has_role('protected'):
return abort(404) # TODO: make this 403, handle template properly
expiration_date = 'No subscription to expire'
# Classify the user based on their roles and capabilities
cap_subs = current_user.has_cap('subscriber')
if current_user.has_role('demo'):
user_cls = 'demo'
elif not cap_subs and current_user.has_cap('can-renew-subscription'):
# This user has an inactive but renewable subscription.
user_cls = 'subscriber-expired'
elif cap_subs:
if current_user.has_role('subscriber'):
# This user pays for their own subscription. Only in this case do we need to fetch
# the expiration date from the Store.
user_cls = 'subscriber'
store_user = store.fetch_subscription_info(current_user.email)
if store_user is None:
expiration_date = 'Unable to reach Blender Store to check'
else:
expiration_date = store_user['expiration_date'][:10]
elif current_user.has_role('org-subscriber'):
# An organisation pays for this subscription.
user_cls = 'subscriber-org'
else:
# This user gets the subscription cap from somewhere else (like an organisation).
user_cls = 'subscriber-other'
else:
user_cls = 'outsider'
return render_template(
'users/settings/billing.html',
user_cls=user_cls,
expiration_date=expiration_date,
title='billing')
@blueprint.route('/terms-and-conditions')
def terms_and_conditions():
return render_template('terms_and_conditions.html')
@blueprint.route('/privacy')
def privacy():
return render_template('privacy.html')
@blueprint.route('/emails/welcome.send')
@login_required
def emails_welcome_send():
from cloud import email
email.queue_welcome_mail(current_user)
return f'queued mail to {current_user.email}'
@blueprint.route('/emails/welcome.html')
@login_required
def emails_welcome_html():
return render_template('emails/welcome.html',
subject='Welcome to Blender Cloud',
user=current_user)
@blueprint.route('/emails/welcome.txt')
@login_required
def emails_welcome_txt():
txt = render_template('emails/welcome.txt',
subject='Welcome to Blender Cloud',
user=current_user)
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.
This is done in order to extend can_post_comments by requiring the
subscriber capability.
"""
api = system_util.pillar_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')
return render_comments_for_node(node_id, can_post_comments=can_post_comments)
def setup_app(app):
global _homepage_context
cached = app.cache.cached(timeout=300)
_homepage_context = cached(_homepage_context)

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

89
cloud/stats/__init__.py Normal file
View File

@@ -0,0 +1,89 @@
"""Interesting usage metrics"""
from flask import current_app
def count_nodes(query=None) -> int:
pipeline = [
{'$match': {'_deleted': {'$ne': 'true'}}},
{
'$lookup':
{
'from': "projects",
'localField': "project",
'foreignField': "_id",
'as': "project",
}
},
{
'$unwind':
{
'path': '$project',
}
},
{
'$project':
{
'project.is_private': 1,
}
},
{'$match': {'project.is_private': False}},
{'$count': 'tot'}
]
c = current_app.db()['nodes']
# If we provide a query, we extend the first $match step in the aggregation pipeline with
# with the extra parameters (for example node_type)
if query:
pipeline[0]['$match'].update(query)
# Return either a list with one item or an empty list
r = list(c.aggregate(pipeline=pipeline))
count = 0 if not r else r[0]['tot']
return count
def count_users(query=None) -> int:
u = current_app.db()['users']
return u.count(query)
def count_blender_sync(query=None) -> int:
pipeline = [
# 0 Find all startups.blend that are not deleted
{
'$match': {
'_deleted': {'$ne': 'true'},
'name': 'startup.blend',
}
},
# 1 Group them per project (drops any duplicates)
{'$group': {'_id': '$project'}},
# 2 Join the project info
{
'$lookup':
{
'from': "projects",
'localField': "_id",
'foreignField': "_id",
'as': "project",
}
},
# 3 Unwind the project list (there is always only one project)
{
'$unwind':
{
'path': '$project',
}
},
# 4 Find all home projects
{'$match': {'project.category': 'home'}},
{'$count': 'tot'}
]
c = current_app.db()['nodes']
# If we provide a query, we extend the first $match step in the aggregation pipeline with
# with the extra parameters (for example _created)
if query:
pipeline[0]['$match'].update(query)
# Return either a list with one item or an empty list
r = list(c.aggregate(pipeline=pipeline))
count = 0 if not r else r[0]['tot']
return count

57
cloud/stats/routes.py Normal file
View File

@@ -0,0 +1,57 @@
import logging
import datetime
import functools
from flask import Blueprint, jsonify
from cloud.stats import count_nodes, count_users, count_blender_sync
blueprint = Blueprint('cloud.stats', __name__, url_prefix='/s')
log = logging.getLogger(__name__)
@functools.lru_cache()
def get_stats(before: datetime.datetime):
query_comments = {'node_type': 'comment'}
query_assets = {'node_type': 'asset'}
date_query = {}
if before:
date_query = {'_created': {'$lt': before}}
query_comments.update(date_query)
query_assets.update(date_query)
stats = {
'comments': count_nodes(query_comments),
'assets': count_nodes(query_assets),
'users_total': count_users(date_query),
'users_blender_sync': count_blender_sync(date_query),
}
return stats
@blueprint.route('/')
@blueprint.route('/before/<int:before>')
def index(before: int=0):
"""
This endpoint is queried on a daily basis by grafista to retrieve cloud usage
stats. For assets and comments we take into considerations only those who belong
to public projects.
These is the data we retrieve
- Comments count
- Assets count (video, images and files)
- Users count (subscribers count goes via store)
- Blender Sync users
"""
# TODO: Implement project-level metrics (and update ad every child update)
if before:
before = datetime.datetime.strptime(str(before), '%Y%m%d')
else:
today = datetime.date.today()
before = datetime.datetime(today.year, today.month, today.day)
return jsonify(get_stats(before))

72
cloud/store.py Normal file
View File

@@ -0,0 +1,72 @@
"""Blender Store interface."""
import logging
import typing
from pillar import current_app
log = logging.getLogger(__name__)
def fetch_subscription_info(email: str) -> typing.Optional[dict]:
"""Returns the user info dict from the external subscriptions management server.
:returns: the store user info, or None if the user can't be found or there
was an error communicating. A dict like this is returned:
{
"shop_id": 700,
"cloud_access": 1,
"paid_balance": 314.75,
"balance_currency": "EUR",
"start_date": "2014-08-25 17:05:46",
"expiration_date": "2016-08-24 13:38:45",
"subscription_status": "wc-active",
"expiration_date_approximate": true
}
"""
from requests.adapters import HTTPAdapter
import requests.exceptions
external_subscriptions_server = current_app.config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER']
if log.isEnabledFor(logging.DEBUG):
import urllib.parse
log_email = urllib.parse.quote(email)
log.debug('Connecting to store at %s?blenderid=%s',
external_subscriptions_server, log_email)
# Retry a few times when contacting the store.
s = requests.Session()
s.mount(external_subscriptions_server, HTTPAdapter(max_retries=5))
try:
r = s.get(external_subscriptions_server,
params={'blenderid': email},
verify=current_app.config['TLS_CERT_FILE'],
timeout=current_app.config.get('EXTERNAL_SUBSCRIPTIONS_TIMEOUT_SECS', 10))
except requests.exceptions.ConnectionError as ex:
log.error('Error connecting to %s: %s', external_subscriptions_server, ex)
return None
except requests.exceptions.Timeout as ex:
log.error('Timeout communicating with %s: %s', external_subscriptions_server, ex)
return None
except requests.exceptions.RequestException as ex:
log.error('Some error communicating with %s: %s', external_subscriptions_server, ex)
return None
if r.status_code != 200:
log.warning("Error communicating with %s, code=%i, unable to check "
"subscription status of user %s",
external_subscriptions_server, r.status_code, email)
return None
store_user = r.json()
if log.isEnabledFor(logging.DEBUG):
import json
log.debug('Received JSON from store API: %s',
json.dumps(store_user, sort_keys=False, indent=4))
return store_user

216
cloud/webhooks.py Normal file
View File

@@ -0,0 +1,216 @@
"""Blender ID webhooks."""
import functools
import hashlib
import hmac
import json
import logging
import typing
from flask_login import request
from flask import Blueprint
import werkzeug.exceptions as wz_exceptions
from pillar import current_app
from pillar.api.blender_cloud import subscription
from pillar.api.utils.authentication import create_new_user_document, make_unique_username
from pillar.auth import UserClass
blueprint = Blueprint('cloud-webhooks', __name__)
log = logging.getLogger(__name__)
WEBHOOK_MAX_BODY_SIZE = 1024 * 10 # 10 kB is large enough for
def webhook_payload(hmac_secret: str) -> dict:
"""Obtains the webhook payload from the request, verifying its HMAC.
:returns the webhook payload as dictionary.
"""
# Check the content type
if request.content_type != 'application/json':
log.info('request from %s to %s had bad content type %s',
request.remote_addr, request, request.content_type)
raise wz_exceptions.BadRequest('Content type not supported')
# Check the length of the body
if request.content_length > WEBHOOK_MAX_BODY_SIZE:
raise wz_exceptions.BadRequest('Request too large')
body = request.get_data()
if len(body) > request.content_length:
raise wz_exceptions.BadRequest('Larger body than Content-Length header')
# Validate the request
mac = hmac.new(hmac_secret.encode(), body, hashlib.sha256)
req_hmac = request.headers.get('X-Webhook-HMAC', '')
our_hmac = mac.hexdigest()
if not hmac.compare_digest(req_hmac, our_hmac):
log.info('request from %s to %s had bad HMAC %r, expected %r',
request.remote_addr, request, req_hmac, our_hmac)
raise wz_exceptions.BadRequest('Bad HMAC')
try:
return json.loads(body)
except json.JSONDecodeError as ex:
log.warning('request from %s to %s had bad JSON: %s',
request.remote_addr, request, ex)
raise wz_exceptions.BadRequest('Bad JSON')
def score(wh_payload: dict, user: dict) -> int:
"""Determine how likely it is that this is the correct user to modify.
:param wh_payload: the info we received from Blender ID;
see user_modified()
:param user: the user in our database
:return: the score for this user
"""
bid_str = str(wh_payload['id'])
try:
match_on_bid = any((auth['provider'] == 'blender-id' and auth['user_id'] == bid_str)
for auth in user['auth'])
except KeyError:
match_on_bid = False
match_on_old_email = user.get('email', 'none') == wh_payload.get('old_email', 'nothere')
match_on_new_email = user.get('email', 'none') == wh_payload.get('email', 'nothere')
return match_on_bid * 10 + match_on_old_email + match_on_new_email * 2
def insert_or_fetch_user(wh_payload: dict) -> typing.Optional[dict]:
"""Fetch the user from the DB or create it.
Only creates it if the webhook payload indicates they could actually use
Blender Cloud (i.e. demo or subscriber). This prevents us from creating
Cloud accounts for Blender Network users.
:returns the user document, or None when not created.
"""
users_coll = current_app.db('users')
my_log = log.getChild('insert_or_fetch_user')
bid_str = str(wh_payload['id'])
email = wh_payload['email']
# Find the user by their Blender ID, or any of their email addresses.
# We use one query to find all matching users. This is done as a
# consistency check; if more than one user is returned, we know the
# database is inconsistent with Blender ID and can emit a warning
# about this.
query = {'$or': [
{'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()
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.
calc_score = functools.partial(score, wh_payload)
best_score = max(db_users, key=calc_score)
my_log.error('%d users found for query %s, picking user %s (%s)',
user_count, query, best_score['_id'], best_score['email'])
return best_score
if user_count:
db_user = db_users[0]
my_log.debug('found user %s', db_user['email'])
return db_user
# 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.
username = make_unique_username(email)
user_doc = create_new_user_document(email, bid_str, username,
provider='blender-id',
full_name=wh_payload['full_name'])
# Figure out the user's eventual roles. These aren't stored in the document yet,
# because that's handled by the badger service.
eventual_roles = [subscription.ROLES_BID_TO_PILLAR[r]
for r in wh_payload.get('roles', [])
if r in subscription.ROLES_BID_TO_PILLAR]
user_ob = UserClass.construct('', user_doc)
user_ob.roles = eventual_roles
user_ob.collect_capabilities()
create = (user_ob.has_cap('subscriber') or
user_ob.has_cap('can-renew-subscription') or
current_app.org_manager.user_is_unknown_member(email))
if not create:
my_log.info('Received update for unknown user %r without Cloud access (caps=%s)',
wh_payload['old_email'], user_ob.capabilities)
return None
# Actually create the user in the database.
r, _, _, status = current_app.post_internal('users', user_doc)
if status != 201:
my_log.error('unable to create user %s: : %r %r', email, status, r)
raise wz_exceptions.InternalServerError('unable to create user')
user_doc.update(r)
my_log.info('created user %r = %s to allow immediate Cloud access', email, user_doc['_id'])
return user_doc
@blueprint.route('/user-modified', methods=['POST'])
def user_modified():
"""Update the local user based on the info from Blender ID.
If the payload indicates the user has access to Blender Cloud (or at least
a renewable subscription), create the user if not already in our DB.
The payload we expect is a dictionary like:
{'id': 12345, # the user's ID in Blender ID
'old_email': 'old@example.com',
'full_name': 'Harry',
'email': 'new@example'com,
'roles': ['role1', 'role2', …]}
"""
my_log = log.getChild('user_modified')
my_log.debug('Received request from %s', request.remote_addr)
hmac_secret = current_app.config['BLENDER_ID_WEBHOOK_USER_CHANGED_SECRET']
payload = webhook_payload(hmac_secret)
my_log.info('payload: %s', payload)
# Update the user
db_user = insert_or_fetch_user(payload)
if not db_user:
my_log.info('Received update for unknown user %r', payload['old_email'])
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.
updates = {}
if db_user['email'] != payload['email']:
my_log.info('User changed email from %s to %s', payload['old_email'], payload['email'])
updates['email'] = payload['email']
db_user['email'] = payload['email']
if db_user['full_name'] != payload['full_name']:
my_log.info('User changed full name from %r to %r',
db_user['full_name'], payload['full_name'])
if payload['full_name']:
updates['full_name'] = payload['full_name']
else:
# Fall back to the username when the full name was erased.
updates['full_name'] = db_user['username']
db_user['full_name'] = updates['full_name']
if updates:
users_coll = current_app.db('users')
update_res = users_coll.update_one({'_id': db_user['_id']},
{'$set': updates})
if update_res.matched_count != 1:
my_log.error('Unable to find user %s to update, even though '
'we found them by email address %s',
db_user['_id'], payload['old_email'])
# Defer to Pillar to do the role updates.
local_user = UserClass.construct('', db_user)
subscription.do_update_subscription(local_user, payload)
return '', 204

296
cloud_share_img.py Executable file
View File

@@ -0,0 +1,296 @@
#!/usr/bin/env python3
from __future__ import print_function
"""CLI command for sharing an image via Blender Cloud.
Assumes that you are logged in on Blender ID with the Blender ID Add-on.
The user_config_dir and user_data_dir functions come from
https://github.com/ActiveState/appdirs/blob/master/appdirs.py and
are licensed under the MIT license.
"""
import argparse
import json
import mimetypes
import os.path
import pprint
import sys
import webbrowser
from urllib.parse import urljoin
import requests
cli = argparse.Namespace() # CLI args from argparser
sess = requests.Session()
IMAGE_SHARING_GROUP_NODE_NAME = 'Image sharing'
if sys.platform.startswith('java'):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
system = 'win32'
elif os_name.startswith('Mac'): # "Mac OS X", etc.
system = 'darwin'
else: # "Linux", "SunOS", "FreeBSD", etc.
# Setting this to "linux2" is not ideal, but only Windows or Mac
# are actually checked for and the rest of the module expects
# *sys.platform* style strings.
system = 'linux2'
else:
system = sys.platform
def request(method: str, rel_url: str, **kwargs) -> requests.Response:
kwargs.setdefault('auth', (cli.token, ''))
url = urljoin(cli.server_url, rel_url)
return sess.request(method, url, **kwargs)
def get(rel_url: str, **kwargs) -> requests.Response:
return request('GET', rel_url, **kwargs)
def post(rel_url: str, **kwargs) -> requests.Response:
return request('POST', rel_url, **kwargs)
def find_user_id() -> str:
"""Returns the current user ID."""
print(15 * '=', 'User info', 15 * '=')
resp = get('/api/users/me')
resp.raise_for_status()
user_info = resp.json()
print('You are logged in as %(full_name)s (%(_id)s)' % user_info)
return user_info['_id']
def find_home_project_id() -> dict:
resp = get('/api/bcloud/home-project')
resp.raise_for_status()
proj = resp.json()
proj_id = proj['_id']
print('Your home project ID is %s' % proj_id)
return proj_id
def find_image_sharing_group_id(home_project_id, user_id) -> str:
"""Find the top-level image sharing group node."""
node_doc = {
'project': home_project_id,
'node_type': 'group',
'name': IMAGE_SHARING_GROUP_NODE_NAME,
'user': user_id,
}
resp = get('/api/nodes', params={'where': json.dumps(node_doc)})
resp.raise_for_status()
items = resp.json()['_items']
if not items:
print('Share group not found, creating one.')
node_doc.update({
'properties': {},
})
resp = post('/api/nodes', json=node_doc)
resp.raise_for_status()
share_group = resp.json()
else:
share_group = items[0]
# print('Share group:', share_group)
return share_group['_id']
def upload_image():
user_id = find_user_id()
home_project_id = find_home_project_id()
group_id = find_image_sharing_group_id(home_project_id, user_id)
basename = os.path.basename(cli.imgfile)
print('Sharing group ID is %s' % group_id)
# Upload the image to the project.
print('Uploading %r' % cli.imgfile)
mimetype, _ = mimetypes.guess_type(cli.imgfile, strict=False)
with open(cli.imgfile, mode='rb') as infile:
resp = post('api/storage/stream/%s' % home_project_id,
files={'file': (basename, infile, mimetype)})
resp.raise_for_status()
file_upload_resp = resp.json()
file_upload_status = file_upload_resp.get('_status') or file_upload_resp.get('status')
if file_upload_status != 'ok':
raise ValueError('Received bad status %s from Pillar: %s' %
(file_upload_status, json.dumps(file_upload_resp)))
file_id = file_upload_resp['file_id']
print('File ID is', file_id)
# Create the asset node
asset_node = {
'project': home_project_id,
'node_type': 'asset',
'name': basename,
'parent': group_id,
'properties': {
'content_type': mimetype,
'file': file_id,
},
}
resp = post('api/nodes', json=asset_node)
resp.raise_for_status()
node_info = resp.json()
node_id = node_info['_id']
print('Created asset node', node_id)
# Share the node to get a public URL.
resp = post('api/nodes/%s/share' % node_id)
resp.raise_for_status()
share_info = resp.json()
print(json.dumps(share_info, indent=4))
url = share_info.get('short_link')
print('Opening %s in a browser' % url)
webbrowser.open_new_tab(url)
def find_credentials():
"""Finds BlenderID credentials.
:rtype: str
:returns: the authentication token to use.
"""
import glob
# Find BlenderID profile file.
configpath = user_config_dir('blender', 'Blender Foundation', roaming=True)
found = glob.glob(os.path.join(configpath, '*'))
for confpath in reversed(sorted(found)):
profiles_path = os.path.join(confpath, 'config', 'blender_id', 'profiles.json')
if not os.path.exists(profiles_path):
continue
print('Reading credentials from %s' % profiles_path)
with open(profiles_path) as infile:
profiles = json.load(infile, encoding='utf8')
if profiles:
break
else:
print('Unable to find Blender ID credentials. Log in with the Blender ID add-on in '
'Blender first.')
raise SystemExit()
active_profile = profiles[u'active_profile']
profile = profiles[u'profiles'][active_profile]
print('Logging in as %s' % profile[u'username'])
return profile[u'token']
def main():
global cli
parser = argparse.ArgumentParser()
parser.add_argument('imgfile', help='The image file to share.')
parser.add_argument('-u', '--server-url', default='https://cloud.blender.org/',
help='URL of the Flamenco server.')
parser.add_argument('-t', '--token',
help='Authentication token to use. If not given, your token from the '
'Blender ID add-on is used.')
cli = parser.parse_args()
if not cli.token:
cli.token = find_credentials()
upload_image()
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user config directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by default "~/.config/<AppName>".
"""
if system in {"win32", "darwin"}:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: ~/Library/Application Support/<AppName>
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
"""
if system == "win32":
raise RuntimeError("Sorry, Windows is not supported for now.")
elif system == 'darwin':
path = os.path.expanduser('~/Library/Application Support/')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
if __name__ == '__main__':
main()

View File

@@ -1,11 +1,33 @@
#!/bin/bash -e #!/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. # Deploys the current production branch to the production machine.
PROJECT_NAME="blender-cloud" PROJECT_NAME="blender-cloud"
DOCKER_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}" REMOTE_ROOT="/data/git/${PROJECT_NAME}"
SSH="ssh -o ClearAllForwardings=yes cloud.blender.org" SSH="ssh -o ClearAllForwardings=yes -o PermitLocalCommand=no ${DEPLOYHOST}"
# macOS does not support readlink -f, so we use greadlink instead # macOS does not support readlink -f, so we use greadlink instead
if [[ `uname` == 'Darwin' ]]; then if [[ `uname` == 'Darwin' ]]; then
@@ -58,12 +80,14 @@ EOT
PILLAR_DIR=$(find_module pillar) PILLAR_DIR=$(find_module pillar)
ATTRACT_DIR=$(find_module attract) ATTRACT_DIR=$(find_module attract)
FLAMENCO_DIR=$(find_module flamenco) FLAMENCO_DIR=$(find_module flamenco)
SVNMAN_DIR=$(find_module svnman)
echo "Pillar : $PILLAR_DIR" echo "Pillar : $PILLAR_DIR"
echo "Attract : $ATTRACT_DIR" echo "Attract : $ATTRACT_DIR"
echo "Flamenco: $FLAMENCO_DIR" echo "Flamenco: $FLAMENCO_DIR"
echo "SVNMan : $SVNMAN_DIR"
if [ -z "$PILLAR_DIR" -o -z "$ATTRACT_DIR" -o -z "$FLAMENCO_DIR" ]; if [ -z "$PILLAR_DIR" -o -z "$ATTRACT_DIR" -o -z "$FLAMENCO_DIR" -o -z "$SVNMAN_DIR" ];
then then
exit 1 exit 1
fi fi
@@ -85,23 +109,28 @@ git_pull pillar-python-sdk master
git_pull pillar production git_pull pillar production
git_pull attract production git_pull attract production
git_pull flamenco production git_pull flamenco production
git_pull pillar-svnman production
git_pull blender-cloud production git_pull blender-cloud production
# Update the virtualenv # Update the virtualenv
#${SSH} -t docker exec ${DOCKER_NAME} /data/venv/bin/pip install -U -r ${REMOTE_ROOT}/requirements.txt --exists-action w #${SSH} -t docker exec ${DOCKER_NAME} /data/venv/bin/pip install -U -r ${REMOTE_ROOT}/requirements.txt --exists-action w
# RSync the world # RSync the world
$ATTRACT_DIR/rsync_ui.sh $ATTRACT_DIR/rsync_ui.sh ${DEPLOYHOST}
$FLAMENCO_DIR/rsync_ui.sh $FLAMENCO_DIR/rsync_ui.sh ${DEPLOYHOST}
./rsync_ui.sh $SVNMAN_DIR/rsync_ui.sh ${DEPLOYHOST}
./rsync_ui.sh ${DEPLOYHOST}
# Notify Bugsnag of this new deploy. # 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
echo "===================================================================" echo "==================================================================="
GIT_REVISION=$(${SSH} git -C ${REMOTE_ROOT} describe --always) REVISION=$(date +'%Y%m%d.%H%M%S.%Z')
echo "Notifying Bugsnag of this new deploy of revision ${GIT_REVISION}." echo "Notifying Sentry of this new deploy of revision ${REVISION}."
BUGSNAG_API_KEY=$(${SSH} python -c "\"import sys; sys.path.append('${REMOTE_ROOT}'); import config_local; print(config_local.BUGSNAG_API_KEY)\"") SENTRY_RELEASE_URL="$(${SSH} python3 -c "\"import sys; sys.path.append('${REMOTE_ROOT}'); import config_local; print(config_local.SENTRY_RELEASE_URL)\"")"
curl --data "apiKey=${BUGSNAG_API_KEY}&revision=${GIT_REVISION}" https://notify.bugsnag.com/deploy curl -vs "$SENTRY_RELEASE_URL" -XPOST -H 'Content-Type: application/json' -d "{\"version\": \"$REVISION\"}" | json_pp
echo echo
# Wait for [ENTER] to restart the server # Wait for [ENTER] to restart the server
@@ -110,9 +139,20 @@ echo "==================================================================="
echo "NOTE: If you want to edit config_local.py on the server, do so now." 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." echo "NOTE: Press [ENTER] to continue and restart the server process."
read dummy read dummy
echo "Gracefully restarting server process"
${SSH} docker exec ${DOCKER_NAME} apache2ctl graceful ${SSH} docker exec ${DOCKER_NAME} apache2ctl graceful
echo "Server process restarted" 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 "===================================================================" echo "==================================================================="
echo "Clearing front page from Redis cache." echo "Clearing front page from Redis cache."

14
docker/1_base/base.docker Executable file → Normal file
View File

@@ -1,16 +1,6 @@
FROM ubuntu:16.04 FROM ubuntu:16.10
MAINTAINER Francesco Siddi <francesco@blender.org> MAINTAINER Francesco Siddi <francesco@blender.org>
RUN apt-get update && apt-get install -qyy \ RUN apt-get update && apt-get install -qyy \
-o APT::Install-Recommends=false -o APT::Install-Suggests=false \ -o APT::Install-Recommends=false -o APT::Install-Suggests=false \
python-pip libffi6 openssl ffmpeg rsyslog logrotate openssl ca-certificates
RUN mkdir -p /data/git/pillar \
&& mkdir -p /data/storage \
&& mkdir -p /data/config \
&& mkdir -p /data/venv \
&& mkdir -p /data/wheelhouse
RUN pip install virtualenv
RUN virtualenv /data/venv
RUN . /data/venv/bin/activate && pip install -U pip && pip install wheel

3
docker/1_base/build.sh Normal file → Executable file
View File

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

View File

@@ -1,3 +0,0 @@
#!/usr/bin/env bash
. /data/venv/bin/activate && pip wheel --wheel-dir=/data/wheelhouse -r /requirements.txt

View File

@@ -1,26 +0,0 @@
FROM pillar_base
MAINTAINER Francesco Siddi <francesco@blender.org>
RUN apt-get update && apt-get install -qy \
git \
gcc \
libffi-dev \
libssl-dev \
pypy-dev \
python-dev \
python-imaging \
zlib1g-dev \
libjpeg-dev \
libtiff-dev \
python-crypto \
python-openssl
ENV WHEELHOUSE=/data/wheelhouse
ENV PIP_WHEEL_DIR=/data/wheelhouse
ENV PIP_FIND_LINKS=/data/wheelhouse
VOLUME /data/wheelhouse
ADD requirements.txt /requirements.txt
ADD build-wheels.sh /build-wheels.sh
ENTRYPOINT ["bash", "build-wheels.sh"]

View File

@@ -1,11 +0,0 @@
#!/usr/bin/env bash
mkdir -p ../3_run/wheelhouse;
cp ../../requirements.txt .;
docker build -t pillar_build -f build.docker .;
docker run --rm \
-v "$(pwd)"/../3_run/wheelhouse:/data/wheelhouse \
pillar_build;
rm requirements.txt;

View File

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

58
docker/2_buildpy/build.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -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
PYTHONTARGET=$($readlink -f ./python)
mkdir -p "$PYTHONTARGET"
echo "Python will be built to $PYTHONTARGET"
docker build -t pillar_build -f buildpy.docker .
# Use the docker image to build Python 3.6 and mod-wsgi
GID=$(id -g)
docker run --rm -i \
-v "$PYTHONTARGET:/opt/python" \
pillar_build <<EOT
set -e
cd \$PYTHONSOURCE
./configure \
--prefix=/opt/python \
--enable-ipv6 \
--enable-shared \
--with-ensurepip=upgrade
make -j8 install
# Make sure we can run Python
ldconfig
# Build mod-wsgi-py3 for Python 3.6
cd /dpkg/mod-wsgi-*
./configure --with-python=/opt/python/bin/python3
make -j8 install
mkdir -p /opt/python/mod-wsgi
cp /usr/lib/apache2/modules/mod_wsgi.so /opt/python/mod-wsgi
chown -R $UID:$GID /opt/python/*
EOT
# Strip some stuff we don't need from the Python install.
rm -rf $PYTHONTARGET/lib/python3.*/test
rm -rf $PYTHONTARGET/lib/python3.*/config-3.*/libpython3.*.a
find $PYTHONTARGET/lib -name '*.so.*' -o -name '*.so' | while read libname; do
chmod u+w "$libname"
strip "$libname"
done
# Create another docker image which contains the actual Python.
# This one will serve as base for the Wheel builder and the
# production image.
docker build -t armadillica/pillar_py:3.6 -f includepy.docker .

View File

@@ -0,0 +1,35 @@
FROM pillar_base
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 \
build-essential \
apache2-dev \
checkinstall \
curl
RUN apt-get build-dep -y python3.5
ADD Python-3.6.4.tar.xz.md5 /Python-3.6.4.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
# Install mod-wsgi sources
RUN mkdir -p /dpkg && cd /dpkg && apt-get source libapache2-mod-wsgi-py3
# To be able to install Python outside the docker.
VOLUME /opt/python
# To be able to run Python; after building, ldconfig has to be re-run to do this.
# This makes it easier to use Python right after building (for example to build
# mod-wsgi for Python 3.6).
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

View File

@@ -0,0 +1,14 @@
FROM pillar_base
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
ADD python /opt/python
RUN echo /opt/python/lib > /etc/ld.so.conf.d/python.conf
RUN ldconfig
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

View File

@@ -0,0 +1,18 @@
FROM armadillica/pillar_py:3.6
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
RUN apt-get update && apt-get install -qy \
git \
build-essential \
checkinstall \
libffi-dev \
libssl-dev \
libjpeg-dev \
zlib1g-dev
ENV WHEELHOUSE=/data/wheelhouse
ENV PIP_WHEEL_DIR=/data/wheelhouse
ENV PIP_FIND_LINKS=/data/wheelhouse
RUN mkdir -p $WHEELHOUSE
VOLUME /data/wheelhouse

45
docker/3_buildwheels/build.sh Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -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
TOPDEVDIR="$($readlink -f ../../..)"
echo "Top-level development dir is $TOPDEVDIR"
WHEELHOUSE="$($readlink -f ../4_run/wheelhouse)"
if [ -z "$WHEELHOUSE" ]; then
echo "Error, ../4_run might not exist." >&2
exit 2
fi
echo "Wheelhouse is $WHEELHOUSE"
mkdir -p "$WHEELHOUSE"
docker build -t pillar_wheelbuilder -f build.docker .
GID=$(id -g)
docker run --rm -i \
-v "$WHEELHOUSE:/data/wheelhouse" \
-v "$TOPDEVDIR:/data/topdev" \
pillar_wheelbuilder <<EOT
set -e
# 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
EOT
# Remove our own projects, they shouldn't be installed as wheel (for now).
rm -f $WHEELHOUSE/{attract,flamenco,pillar,pillarsdk}*.whl

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env bash
cp ../../requirements.txt .;
docker build -t armadillica/blender_cloud -f run.docker .;
rm requirements.txt;

View File

@@ -1,25 +0,0 @@
#!/usr/bin/env bash
if [ ! -f /installed ]; then
echo "Installing pillar and pillar-sdk"
# TODO: curretly doing pip install -e takes a long time, so we symlink
# . /data/venv/bin/activate && pip install -e /data/git/pillar
ln -s /data/git/pillar/pillar /data/venv/lib/python2.7/site-packages/pillar
# . /data/venv/bin/activate && pip install -e /data/git/attract
ln -s /data/git/attract/attract /data/venv/lib/python2.7/site-packages/attract
# . /data/venv/bin/activate && pip install -e /data/git/flamenco/packages/flamenco
ln -s /data/git/flamenco/packages/flamenco/flamenco/ /data/venv/lib/python2.7/site-packages/flamenco
# . /data/venv/bin/activate && pip install -e /data/git/pillar-python-sdk
ln -s /data/git/pillar-python-sdk/pillarsdk /data/venv/lib/python2.7/site-packages/pillarsdk
touch installed
fi
if [ "$DEV" = "true" ]; then
echo "Running in development mode"
cd /data/git/blender-cloud
bash /manage.sh runserver --host='0.0.0.0'
else
# Run Apache
a2enmod rewrite
/usr/sbin/apache2ctl -D FOREGROUND
fi

View File

@@ -1,5 +0,0 @@
#!/usr/bin/env bash -e
. /data/venv/bin/activate
cd /data/git/blender-cloud
python manage.py "$@"

View File

@@ -1,46 +0,0 @@
FROM pillar_base
RUN apt-get update && apt-get install -qyy \
-o APT::Install-Recommends=true -o APT::Install-Suggests=false \
git \
apache2 \
libapache2-mod-wsgi \
libapache2-mod-xsendfile \
libjpeg8 \
libtiff5 \
nano vim curl \
&& rm -rf /var/lib/apt/lists/*
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 requirements.txt /requirements.txt
ADD wheelhouse /data/wheelhouse
RUN . /data/venv/bin/activate \
&& pip install --no-index --find-links=/data/wheelhouse -r requirements.txt \
&& rm /requirements.txt
VOLUME /data/git/blender-cloud
VOLUME /data/git/pillar
VOLUME /data/git/pillar-python-sdk
VOLUME /data/config
VOLUME /data/storage
ENV USE_X_SENDFILE True
EXPOSE 80
EXPOSE 5000
ADD apache2.conf /etc/apache2/apache2.conf
ADD 000-default.conf /etc/apache2/sites-available/000-default.conf
ADD docker-entrypoint.sh /docker-entrypoint.sh
ADD manage.sh /manage.sh
ENTRYPOINT ["bash", "/docker-entrypoint.sh"]

View File

@@ -1,10 +1,10 @@
<VirtualHost *:80> <VirtualHost *:80>
# EnableSendfile on
XSendFile on XSendFile on
XSendFilePath /data/storage/pillar XSendFilePath /data/storage/pillar
XSendFilePath /data/git/pillar XSendFilePath /data/git/pillar
XSendFilePath /data/venv/lib/python2.7/site-packages/attract/static/ XSendFilePath /data/git/attract/attract/static/
XSendFilePath /data/venv/lib/python2.7/site-packages/flamenco/static/ XSendFilePath /data/git/flamenco/flamenco/static/
XsendFilePath /data/git/pillar-svnman/svnman/static/
XsendFilePath /data/git/blender-cloud XsendFilePath /data/git/blender-cloud
ServerAdmin webmaster@localhost ServerAdmin webmaster@localhost
@@ -19,7 +19,7 @@
ErrorLog ${APACHE_LOG_DIR}/error.log ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined CustomLog ${APACHE_LOG_DIR}/access.log combined
WSGIDaemonProcess cloud processes=4 threads=1 maximum-requests=10000 WSGIDaemonProcess cloud processes=2 threads=32 maximum-requests=10000
WSGIPassAuthorization On WSGIPassAuthorization On
WSGIScriptAlias / /data/git/blender-cloud/runserver.wsgi \ WSGIScriptAlias / /data/git/blender-cloud/runserver.wsgi \
@@ -36,4 +36,16 @@
RewriteCond "%{HTTP_HOST}" "^cloudapi\.blender\.org" [NC] RewriteCond "%{HTTP_HOST}" "^cloudapi\.blender\.org" [NC]
RewriteRule (.*) /api$1 [PT] RewriteRule (.*) /api$1 [PT]
# 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 "^/cf2/?$" "/p/creature-factory-2" [R=301,L]
RewriteRule "^/characters/?$" "/p/characters" [R=301,L]
RewriteRule "^/gallery/?$" "/p/gallery" [R=301,L]
RewriteRule "^/hdri/?$" "/p/hdri" [R=301,L]
RewriteRule "^/textures/?$" "/p/textures" [R=301,L]
RewriteRule "^/training/?$" "/courses" [R=301,L]
RewriteRule "^/spring/?$" "/p/spring" [R=301,L]
</VirtualHost> </VirtualHost>

View File

@@ -0,0 +1,21 @@
/var/log/apache2/*.log {
daily
missingok
rotate 14
size 100M
compress
delaycompress
notifempty
create 640 root adm
sharedscripts
postrotate
if /etc/init.d/apache2 status > /dev/null ; then \
/etc/init.d/apache2 reload > /dev/null; \
fi;
endscript
prerotate
if [ -d /etc/logrotate.d/httpd-prerotate ]; then \
run-parts /etc/logrotate.d/httpd-prerotate; \
fi; \
endscript
}

View File

@@ -0,0 +1,9 @@
bash docker-entrypoint.sh
env | sort
apache2ctl start
apache2ctl graceful
/manage.sh operations worker -- -C
celery status --broker amqp://guest:guest@rabbit:5672//
celery events --broker amqp://guest:guest@rabbit:5672//
tail -n 40 -f /var/log/apache2/access.log
tail -n 40 -f /var/log/apache2/error.log

5
docker/4_run/build.sh Executable file
View File

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

6
docker/4_run/celery-beat.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env bash
source /install_scripts.sh
source /manage.sh celery beat -- \
--schedule /data/storage/pillar/celerybeat-schedule.db \
--pid /data/storage/pillar/celerybeat.pid

4
docker/4_run/celery-worker.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
source /install_scripts.sh
source /manage.sh celery worker -- -C

View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
source /install_scripts.sh
# Make sure that log rotation works.
mkdir -p ${APACHE_LOG_DIR}
service cron start
if [ "$DEV" = "true" ]; then
echo "Running in development mode"
cd /data/git/blender-cloud
exec bash /manage.sh runserver --host='0.0.0.0'
else
exec /usr/sbin/apache2ctl -D FOREGROUND
fi

View File

@@ -0,0 +1,18 @@
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
fi

5
docker/4_run/manage.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -e
cd /data/git/blender-cloud
exec python manage.py "$@"

55
docker/4_run/run.docker Executable file
View File

@@ -0,0 +1,55 @@
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

122
docker/4_run/wsgi-py36.conf Normal file
View File

@@ -0,0 +1,122 @@
<IfModule mod_wsgi.c>
#This config file is provided to give an overview of the directives,
#which are only allowed in the 'server config' context.
#For a detailed description of all avaiable directives please read
#http://code.google.com/p/modwsgi/wiki/ConfigurationDirectives
#WSGISocketPrefix: Configure directory to use for daemon sockets.
#
#Apache's DEFAULT_REL_RUNTIMEDIR should be the proper place for WSGI's
#Socket. In case you want to mess with the permissions of the directory,
#you need to define WSGISocketPrefix to an alternative directory.
#See http://code.google.com/p/modwsgi/wiki/ConfigurationIssues for more
#information
#WSGISocketPrefix /var/run/apache2/wsgi
#WSGIPythonOptimize: Enables basic Python optimisation features.
#
#Sets the level of Python compiler optimisations. The default is '0'
#which means no optimisations are applied.
#Setting the optimisation level to '1' or above will have the effect
#of enabling basic Python optimisations and changes the filename
#extension for compiled (bytecode) files from .pyc to .pyo.
#When the optimisation level is set to '2', doc strings will not be
#generated and retained. This will result in a smaller memory footprint,
#but may cause some Python packages which interrogate doc strings in some
#way to fail.
#WSGIPythonOptimize 0
#WSGIPythonPath: Additional directories to search for Python modules,
# overriding the PYTHONPATH environment variable.
#
#Used to specify additional directories to search for Python modules.
#If multiple directories are specified they should be separated by a ':'.
WSGIPythonPath /opt/python/lib/python3.6/site-packages
#WSGIPythonEggs: Directory to use for Python eggs cache.
#
#Used to specify the directory to be used as the Python eggs cache
#directory for all sub interpreters created within embedded mode.
#This directive achieves the same affect as having set the
#PYTHON_EGG_CACHE environment variable.
#Note that the directory specified must exist and be writable by the user
#that the Apache child processes run as. The directive only applies to
#mod_wsgi embedded mode. To set the Python eggs cache directory for
#mod_wsgi daemon processes, use the 'python-eggs' option to the
#WSGIDaemonProcess directive instead.
#WSGIPythonEggs directory
#WSGIRestrictEmbedded: Enable restrictions on use of embedded mode.
#
#The WSGIRestrictEmbedded directive determines whether mod_wsgi embedded
#mode is enabled or not. If set to 'On' and the restriction on embedded
#mode is therefore enabled, any attempt to make a request against a
#WSGI application which hasn't been properly configured so as to be
#delegated to a daemon mode process will fail with a HTTP internal server
#error response.
#WSGIRestrictEmbedded On|Off
#WSGIRestrictStdin: Enable restrictions on use of STDIN.
#WSGIRestrictStdout: Enable restrictions on use of STDOUT.
#WSGIRestrictSignal: Enable restrictions on use of signal().
#
#Well behaved WSGI applications neither should try to read/write from/to
#STDIN/STDOUT, nor should they try to register signal handlers. If your
#application needs an exception from this rule, you can disable the
#restrictions here.
#WSGIRestrictStdin On
#WSGIRestrictStdout On
#WSGIRestrictSignal On
#WSGIAcceptMutex: Specify type of accept mutex used by daemon processes.
#
#The WSGIAcceptMutex directive sets the method that mod_wsgi will use to
#serialize multiple daemon processes in a process group accepting requests
#on a socket connection from the Apache child processes. If this directive
#is not defined then the same type of mutex mechanism as used by Apache for
#the main Apache child processes when accepting connections from a client
#will be used. If set the method types are the same as for the Apache
#AcceptMutex directive.
#WSGIAcceptMutex default
#WSGIImportScript: Specify a script file to be loaded on process start.
#
#The WSGIImportScript directive can be used to specify a script file to be
#loaded when a process starts. Options must be provided to indicate the
#name of the process group and the application group into which the script
#will be loaded.
#WSGIImportScript process-group=name application-group=name
#WSGILazyInitialization: Enable/disable lazy initialisation of Python.
#
#The WSGILazyInitialization directives sets whether or not the Python
#interpreter is preinitialised within the Apache parent process or whether
#lazy initialisation is performed, and the Python interpreter only
#initialised in the Apache server processes or mod_wsgi daemon processes
#after they have forked from the Apache parent process.
#WSGILazyInitialization On|Off
</IfModule>

View File

@@ -0,0 +1 @@
LoadModule wsgi_module /opt/python/mod-wsgi/mod_wsgi.so

92
docker/README.md Normal file
View File

@@ -0,0 +1,92 @@
# Setting up a production machine
To get the docker stack up and running, we use the following, on an Ubuntu 16.10 machine.
## 0. Basic stuff
Install the machine, use `locale-gen nl_NL.UTF-8` or similar commands to generate locale
definitions. Set up automatic security updates and backups, the usual.
## 1. Install Docker
Install Docker itself, as described in the
[Docker CE for Ubuntu manual](https://store.docker.com/editions/community/docker-ce-server-ubuntu?tab=description):
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -
add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable"
apt-get update
apt-get install docker-ce
## 2. Configure Docker to use "overlay"
Configure Docker to use "overlay" instead of "aufs" for the images. This prevents
[segfaults in auplink](https://bugs.launchpad.net/ubuntu/+source/aufs-tools/+bug/1442568).
1. Set `DOCKER_OPTS="-s overlay"` in `/etc/defaults/docker`
2. Copy `/lib/systemd/system/docker.service` to `/etc/systemd/system/docker.service`.
This allows later upgrading of docker without overwriting the changes we're about to do.
2. Edit the `[Service]` section of `/etc/systemd/system/docker.service`:
1. Add `EnvironmentFile=/etc/default/docker`
2. Append ` $DOCKER_OPTS` to the `ExecStart` line
3. Run `systemctl daemon-reload`
4. Remove all your containers and images.
5. Restart Docker: `systemctl restart docker`
## 3. Pull the Blender Cloud docker image
`docker pull armadillica/blender_cloud:latest`
## 4. Get docker-compose + our repositories
See the [Quick setup](../README.md) on how to get those. Then run:
cd /data/git/blender-cloud/docker
docker-compose up -d
Set up permissions for Docker volumes; the following should be writable by
- `/data/storage/pillar`: writable by `www-data` and `root` (do a `chown root:www-data`
and `chmod 2770`).
- `/data/storage/db`: writable by uid 999.
## 5. Set up TLS
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:
- `/data/git/blender_cloud/config_local.py` with machine-local configuration overrides
- `/data/config/google_app.json` with Google Cloud Storage credentials.
## 7. ElasticSearch & kibana
ElasticSearch and Kibana run in our self-rolled images. This is needed because by default
- ElasticSearch uses up to 2 GB of RAM, which is too much for our droplet, and
- the Docker images contain the proprietary X-Pack plugin, which we don't want.
This also gives us the opportunity to let Kibana do its optimization when we build the image, rather
than every time the container is recreated.
`/data/storage/elasticsearch` needs to be writable by UID 1000, GID 1000.
Kibana connects to [ElasticProxy](https://github.com/armadillica/elasticproxy), which only allows
GET, HEAD, and some specific POST requests. This ensures that the public-facing Kibana cannot be
used to change the ElasticSearch database.
Production Kibana can be placed in read-only mode, but this is not necessary now that we use
ElasticProxy. However, I've left this in here as reference.
`curl -XPUT 'localhost:9200/.kibana/_settings' -d '{ "index.blocks.read_only" : true }'`
If editing is desired, temporarily turn off read-only mode:
`curl -XPUT 'localhost:9200/.kibana/_settings' -d '{ "index.blocks.read_only" : false }'`

View File

@@ -1,16 +1,17 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -x; set -xe
set -e;
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
cd $DIR;
cd 1_base/; cd $DIR/1_base
bash build.sh; bash build.sh
cd ../2_build/; cd $DIR/2_buildpy
bash build.sh; bash build.sh
cd ../3_run/; cd $DIR/3_buildwheels
bash build.sh; bash build.sh
cd $DIR/4_run
bash build.sh

View File

@@ -1,68 +1,160 @@
mongo: version: '3.4'
image: mongo services:
container_name: mongo mongo:
restart: always image: mongo:3.4.2
volumes: container_name: mongo
- /data/storage/db:/data/db restart: always
ports: volumes:
- "127.0.0.1:27017:27017" - /data/storage/db:/data/db
redis: - /data/storage/db-bak:/data/db-bak # for backing up stuff etc.
image: redis ports:
container_name: redis - "127.0.0.1:27017:27017"
restart: always
blender_cloud: redis:
image: armadillica/blender_cloud image: redis:3.2.8
container_name: blender_cloud container_name: redis
restart: always restart: always
environment: ports:
VIRTUAL_HOST: http://cloudapi.blender.org/*,https://cloudapi.blender.org/*,http://cloud.blender.org/*,https://cloud.blender.org/*,http://pillar-web/* - "127.0.0.1:6379:6379"
VIRTUAL_HOST_WEIGHT: 10
FORCE_SSL: "true" rabbit:
volumes: image: rabbitmq:3.6.10
- /data/git/blender-cloud:/data/git/blender-cloud:ro container_name: rabbit
- /data/git/attract:/data/git/attract:ro restart: always
- /data/git/flamenco:/data/git/flamenco:ro ports:
- /data/git/pillar:/data/git/pillar:ro - "127.0.0.1:5672:5672"
- /data/git/pillar-python-sdk:/data/git/pillar-python-sdk:ro
- /data/config:/data/config:ro elastic:
- /data/storage/pillar:/data/storage/pillar image: armadillica/elasticsearch:6.1.1
links: container_name: elastic
- mongo restart: always
- redis volumes:
# notifserv: # NOTE: this path must be writable by UID=1000 GID=1000.
# container_name: notifserv - /data/storage/elastic:/usr/share/elasticsearch/data
# image: armadillica/pillar-notifserv:cd8fa678436563ac3b800b2721e36830c32e4656 ports:
# restart: always - "127.0.0.1:9200:9200"
# links: environment:
# - mongo ES_JAVA_OPTS: "-Xms256m -Xmx256m"
# environment:
# VIRTUAL_HOST: https://cloud.blender.org/notifications*,http://pillar-web/notifications* elasticproxy:
# VIRTUAL_HOST_WEIGHT: 20 image: armadillica/elasticproxy:1.2
# FORCE_SSL: true container_name: elasticproxy
grafista: restart: always
image: armadillica/grafista command: /elasticproxy -elastic http://elastic:9200/
container_name: grafista depends_on:
restart: always - elastic
environment:
VIRTUAL_HOST: http://cloud.blender.org/stats/*,https://cloud.blender.org/stats/*,http://blender-cloud/stats/* kibana:
VIRTUAL_HOST_WEIGHT: 20 image: armadillica/kibana:6.1.1
FORCE_SSL: "true" container_name: kibana
volumes: restart: always
- /data/git/grafista:/data/git/grafista:ro environment:
- /data/storage/grafista:/data/storage SERVER_NAME: "stats.cloud.blender.org"
haproxy: ELASTICSEARCH_URL: http://elasticproxy:9200
image: dockercloud/haproxy CONSOLE_ENABLED: 'false'
container_name: haproxy VIRTUAL_HOST: http://stats.cloud.blender.org/*,https://stats.cloud.blender.org/*,http://stats.blender-cloud/*,https://stats.blender-cloud/*
restart: always VIRTUAL_HOST_WEIGHT: 20
ports: FORCE_SSL: "true"
- "443:443"
- "80:80" # See https://github.com/elastic/kibana/issues/5170#issuecomment-163042525
environment: NODE_OPTIONS: "--max-old-space-size=200"
- CERT_FOLDER=/certs/ depends_on:
- TIMEOUT=connect 5s, client 5m, server 10m - elasticproxy
links:
- blender_cloud blender_cloud:
- grafista image: armadillica/blender_cloud:latest
# - notifserv container_name: blender_cloud
volumes: restart: always
- '/data/certs:/certs' 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
restart: always
volumes:
- /data/git/grafista:/data/git/grafista:ro
- /data/storage/grafista:/data/storage/grafista
letsencrypt:
image: armadillica/picohttp:latest
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
volumes:
- /data/letsencrypt:/data/letsencrypt
haproxy:
image: dockercloud/haproxy:1.5.3
container_name: haproxy
restart: always
ports:
- "443:443"
- "80:80"
environment:
- ADDITIONAL_SERVICES=docker:blender_cloud,docker:letsencrypt,docker:kibana
- CERT_FOLDER=/certs/
- TIMEOUT=connect 5s, client 5m, server 10m
- SSL_BIND_CIPHERS=ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS
- SSL_BIND_OPTIONS=no-sslv3
- EXTRA_GLOBAL_SETTINGS=tune.ssl.default-dh-param 2048
depends_on:
- blender_cloud
- letsencrypt
- kibana
volumes:
- '/data/certs:/certs'
- /var/run/docker.sock:/var/run/docker.sock

View File

@@ -0,0 +1,10 @@
FROM docker.elastic.co/elasticsearch/elasticsearch:6.1.1
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
RUN elasticsearch-plugin remove --purge x-pack
ADD elasticsearch.yml jvm.options /usr/share/elasticsearch/config/
USER root
RUN chown -R elasticsearch:elasticsearch /usr/share/elasticsearch/config/
USER elasticsearch

View File

@@ -0,0 +1,6 @@
FROM docker.elastic.co/kibana/kibana:6.1.1
LABEL maintainer Sybren A. Stüvel <sybren@blender.studio>
RUN bin/kibana-plugin remove x-pack
ADD kibana.yml /usr/share/kibana/config/kibana.yml
RUN kibana 2>&1 | grep -m 1 "Optimization of .* complete"

15
docker/elastic/build.sh Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/bash -e
# When updating this, also update the versions in Dockerfile-*, and make sure that
# it matches the versions of the elasticsearch and elasticsearch_dsl packages
# used in Pillar. Those don't have to match exactly, but the major version should.
VERSION=6.1.1
docker build -t armadillica/elasticsearch:${VERSION} -f Dockerfile-elastic .
docker build -t armadillica/kibana:${VERSION} -f Dockerfile-kibana .
docker tag armadillica/elasticsearch:${VERSION} armadillica/elasticsearch:latest
docker tag armadillica/kibana:${VERSION} armadillica/kibana:latest
echo "Done, built armadillica/elasticsearch:${VERSION} and armadillica/kibana:${VERSION}"
echo "Also tagged as armadillica/elasticsearch:latest and armadillica/kibana:latest"

View File

@@ -0,0 +1,7 @@
cluster.name: "blender-cloud"
network.host: 0.0.0.0
# minimum_master_nodes need to be explicitly set when bound on a public IP
# set to 1 to allow single node clusters
# Details: https://github.com/elastic/elasticsearch/pull/17288
discovery.zen.minimum_master_nodes: 1

112
docker/elastic/jvm.options Normal file
View File

@@ -0,0 +1,112 @@
## JVM configuration
################################################################
## IMPORTANT: JVM heap size
################################################################
##
## You should always set the min and max JVM heap
## size to the same value. For example, to set
## the heap to 4 GB, set:
##
## -Xms4g
## -Xmx4g
##
## See https://www.elastic.co/guide/en/elasticsearch/reference/current/heap-size.html
## for more information
##
################################################################
# Xms represents the initial size of total heap space
# Xmx represents the maximum size of total heap space
# Sybren: uncommented so that we can set those options using the ES_JAVA_OPTS environment variable.
#-Xms512m
#-Xmx512m
################################################################
## Expert settings
################################################################
##
## All settings below this section are considered
## expert settings. Don't tamper with them unless
## you understand what you are doing
##
################################################################
## GC configuration
-XX:+UseConcMarkSweepGC
-XX:CMSInitiatingOccupancyFraction=75
-XX:+UseCMSInitiatingOccupancyOnly
## optimizations
# pre-touch memory pages used by the JVM during initialization
-XX:+AlwaysPreTouch
## basic
# force the server VM (remove on 32-bit client JVMs)
-server
# explicitly set the stack size (reduce to 320k on 32-bit client JVMs)
-Xss1m
# set to headless, just in case
-Djava.awt.headless=true
# ensure UTF-8 encoding by default (e.g. filenames)
-Dfile.encoding=UTF-8
# use our provided JNA always versus the system one
-Djna.nosys=true
# use old-style file permissions on JDK9
-Djdk.io.permissionsUseCanonicalPath=true
# flags to configure Netty
-Dio.netty.noUnsafe=true
-Dio.netty.noKeySetOptimization=true
-Dio.netty.recycler.maxCapacityPerThread=0
# log4j 2
-Dlog4j.shutdownHookEnabled=false
-Dlog4j2.disable.jmx=true
-Dlog4j.skipJansi=true
## heap dumps
# generate a heap dump when an allocation from the Java heap fails
# heap dumps are created in the working directory of the JVM
-XX:+HeapDumpOnOutOfMemoryError
# specify an alternative path for heap dumps
# ensure the directory exists and has sufficient space
#-XX:HeapDumpPath=${heap.dump.path}
## GC logging
#-XX:+PrintGCDetails
#-XX:+PrintGCTimeStamps
#-XX:+PrintGCDateStamps
#-XX:+PrintClassHistogram
#-XX:+PrintTenuringDistribution
#-XX:+PrintGCApplicationStoppedTime
# log GC status to a file with time stamps
# ensure the directory exists
#-Xloggc:${loggc}
# By default, the GC log file will not rotate.
# By uncommenting the lines below, the GC log file
# will be rotated every 128MB at most 32 times.
#-XX:+UseGCLogFileRotation
#-XX:NumberOfGCLogFiles=32
#-XX:GCLogFileSize=128M
# Elasticsearch 5.0.0 will throw an exception on unquoted field names in JSON.
# If documents were already indexed with unquoted fields in a previous version
# of Elasticsearch, some operations may throw errors.
#
# WARNING: This option will be removed in Elasticsearch 6.0.0 and is provided
# only for migration purposes.
#-Delasticsearch.json.allow_unquoted_field_names=true

View File

@@ -0,0 +1,8 @@
---
server.name: kibana
server.host: "0"
elasticsearch.url: http://elasticsearch:9200
# Hide dev tools
console.enabled: false

27
docker/renew-letsencrypt.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash -e
# First time creating a certificate for a domain, use:
# certbot certonly --webroot -w /data/letsencrypt -d $DOMAINNAME
cd /data/letsencrypt
certbot renew
echo
echo "Recreating HAProxy certificates"
for certdir in /etc/letsencrypt/live/*; do
domain=$(basename $certdir)
echo " - $domain"
cat $certdir/privkey.pem $certdir/fullchain.pem > $domain.pem
mv $domain.pem /data/certs/
done
echo
echo -n "Restarting "
docker restart haproxy
echo "Certificate renewal completed."

19
gulp Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/bash -ex
GULP=./node_modules/.bin/gulp
function install() {
npm install
touch $GULP # installer doesn't always touch this after a build, so we do.
}
# Rebuild Gulp if missing or outdated.
[ -e $GULP ] || install
[ gulpfile.js -nt $GULP ] && install
if [ "$1" == "watch" ]; then
# Treat "gulp watch" as "gulp && gulp watch"
$GULP
fi
exec $GULP "$@"

128
gulpfile.js Normal file
View File

@@ -0,0 +1,128 @@
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');
var enabled = {
uglify: argv.production,
maps: argv.production,
failCheck: !argv.production,
prettyPug: !argv.production,
cachify: !argv.production,
cleanup: argv.production,
};
var destination = {
css: 'cloud/static/assets/css',
pug: 'cloud/templates',
js: 'cloud/static/assets/js',
}
/* CSS */
gulp.task('styles', function() {
gulp.src('src/styles/**/*.sass')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.maps, sourcemaps.init()))
.pipe(sass({
outputStyle: 'compressed'}
))
.pipe(autoprefixer("last 3 versions"))
.pipe(gulpif(enabled.maps, sourcemaps.write(".")))
.pipe(gulp.dest(destination.css))
.pipe(gulpif(argv.livereload, livereload()));
});
/* Templates - Pug */
gulp.task('templates', function() {
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()));
// 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()));
});
/* Individual Uglified Scripts */
gulp.task('scripts', function() {
gulp.src('src/scripts/*.js')
.pipe(gulpif(enabled.failCheck, plumber()))
.pipe(gulpif(enabled.cachify, cache('scripting')))
.pipe(gulpif(enabled.maps, sourcemaps.init()))
.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()));
});
/* 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.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']);
});
// Erases all generated files in output directories.
gulp.task('cleanup', function() {
var paths = [];
for (attr in destination) {
paths.push(destination[attr]);
}
git.clean({ args: '-f -X ' + paths.join(' ') }, function (err) {
if(err) throw err;
});
});
// Run 'gulp' to build everything at once
var tasks = [];
if (enabled.cleanup) tasks.push('cleanup');
gulp.task('default', tasks.concat(['styles', 'templates', 'scripts', 'scripts_concat_tutti']));

View File

@@ -1,40 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function
import logging
from flask import current_app
from pillar import cli from pillar import cli
from pillar.cli import manager_maintenance from runserver import app
from cloud import app
log = logging.getLogger(__name__)
@manager_maintenance.command
def reconcile_subscribers():
"""For every user, check their subscription status with the store."""
from pillar.auth.subscriptions import fetch_user
users_coll = current_app.data.driver.db['users']
unsubscribed_users = []
for user in users_coll.find({'roles': 'subscriber'}):
print('Processing %s' % user['email'])
print(' Checking subscription')
user_store = fetch_user(user['email'])
if user_store['cloud_access'] == 0:
print(' Removing subscriber role')
users_coll.update(
{'_id': user['_id']},
{'$pull': {'roles': 'subscriber'}})
unsubscribed_users.append(user['email'])
if not unsubscribed_users:
return
print('The following users have been unsubscribed')
for user in unsubscribed_users:
print(user)
cli.manager.app = app cli.manager.app = app
cli.manager.run() cli.manager.run()

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "blender-cloud",
"license": "GPL-2.0+",
"author": "Blender Institute",
"repository": {
"type": "git",
"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",
"minimist": "^1.2.0"
}
}

11
requirements-dev.txt Normal file
View File

@@ -0,0 +1,11 @@
-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,67 +1,4 @@
# Primary requirements -r ../pillar/requirements.txt
# pillarsdk -r ../attract/requirements.txt
# pillar -r ../flamenco/requirements.txt
# attract -r ../pillar-svnman/requirements.txt
# flamenco
# Secondary requirements (i.e. pulled in from primary requirements)
algoliasearch==1.8.0
attrs==16.2.0
bcrypt==2.0.0
blinker==1.4
bugsnag==2.3.1
bleach==1.4.3
Cerberus==0.9.2
cffi==1.7.0
commonmark==0.7.2
cryptography==1.4
enum34==1.1.6
Eve==0.6.3
Events==0.2.1
Flask==0.10.1
Flask-Cache==0.13.1
Flask-Script==2.0.5
Flask-Login==0.3.2
Flask-OAuthlib==0.9.3
Flask-PyMongo==0.4.1
Flask-WTF==0.12
flup==1.0.2
future==0.15.2
gcloud==0.12.0
google-apitools==0.4.11
googleapis-common-protos==1.2.0
html5lib==0.9999999
httplib2==0.9.2
idna==2.0
ipaddress==1.0.16
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
markdown==2.6.7
ndg-httpsclient==0.4.0
oauth2client==3.0.0
oauthlib==1.1.2
pathlib2==2.2.1
Pillow==2.8.1
protobuf==3.0.0
protorpc==0.11.1
pyasn1==0.1.9
pyasn1-modules==0.0.8
pycparser==2.14
pycrypto==2.6.1
pylru==1.0.4
pymongo==3.3.0
pyOpenSSL==0.15.1
python-dateutil==2.5.3
redis==2.10.5
requests==2.9.1
requests-oauthlib==0.6.2
rsa==3.4.2
scandir==1.4
simplejson==3.8.2
six==1.10.0
svn==0.3.43
WebOb==1.5.0
Werkzeug==0.11.10
WTForms==2.1
zencoder==0.6.5

View File

@@ -1,5 +1,42 @@
#!/usr/bin/env bash #!/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 PILLAR_DIR=$(python <<EOT
from __future__ import print_function from __future__ import print_function
import os.path import os.path
@@ -9,32 +46,46 @@ print(os.path.dirname(os.path.dirname(pillar.__file__)))
EOT EOT
) )
ASSETS="$PILLAR_DIR/pillar/web/static/assets/" PILLAR_ASSETS="$PILLAR_DIR/pillar/web/static/assets/"
TEMPLATES="$PILLAR_DIR/pillar/web/templates/" PILLAR_TEMPLATES="$PILLAR_DIR/pillar/web/templates/"
if [ ! -d "$ASSETS" ]; then if [ ! -d "$PILLAR_ASSETS" ]; then
echo "Unable to find assets dir $ASSETS" echo "Unable to find assets dir $PILLAR_ASSETS"
exit 1 exit 1
fi fi
cd $PILLAR_DIR cd $PILLAR_DIR
if [ $(git rev-parse --abbrev-ref HEAD) != "production" ]; then if [ $(git rev-parse --abbrev-ref HEAD) != "production" ]; then
echo "You are NOT on the production branch, refusing to rsync_ui." >&2 echo "Pillar is NOT on the production branch, refusing to rsync_ui." >&2
exit 1 exit 1
fi fi
echo echo
echo "*** GULPA GULPA ***" echo "*** GULPA GULPA PILLAR ***"
if [ -x ./node_modules/.bin/gulp ]; then # TODO(Pablo): this command fails when passing the --production CLI
./node_modules/.bin/gulp --production # arg.
else ./gulp
gulp --production
fi
echo echo
echo "*** SYNCING ASSETS ***" echo "*** SYNCING PILLAR_ASSETS ***"
rsync -avh $ASSETS root@cloud.blender.org:/data/git/pillar/pillar/web/static/assets/ rsync -avh $PILLAR_ASSETS root@${DEPLOYHOST}:/data/git/pillar/pillar/web/static/assets/ --delete-after
echo echo
echo "*** SYNCING TEMPLATES ***" echo "*** SYNCING PILLAR_TEMPLATES ***"
rsync -avh $TEMPLATES root@cloud.blender.org:/data/git/pillar/pillar/web/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

@@ -3,13 +3,19 @@
from pillar import PillarServer from pillar import PillarServer
from attract import AttractExtension from attract import AttractExtension
from flamenco import FlamencoExtension from flamenco import FlamencoExtension
from svnman import SVNManExtension
from cloud import CloudExtension
attract = AttractExtension() attract = AttractExtension()
flamenco = FlamencoExtension() flamenco = FlamencoExtension()
svnman = SVNManExtension()
cloud = CloudExtension()
app = PillarServer('.') app = PillarServer('.')
app.load_extension(attract, '/attract') app.load_extension(attract, '/attract')
app.load_extension(flamenco, '/flamenco') app.load_extension(flamenco, '/flamenco')
app.load_extension(svnman, '/svn')
app.load_extension(cloud, None)
app.process_extensions() app.process_extensions()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -1,22 +1,23 @@
from os.path import abspath, dirname from os.path import abspath, dirname
import sys import sys
activate_this = '/data/venv/bin/activate_this.py' my_path = dirname(abspath(__file__))
execfile(activate_this, dict(__file__=activate_this)) sys.path.append(my_path)
from flup.server.fcgi import WSGIServer
from pillar import PillarServer from pillar import PillarServer
from attract import AttractExtension from attract import AttractExtension
from flamenco import FlamencoExtension from flamenco import FlamencoExtension
from svnman import SVNManExtension
sys.path.append('/data/git/blender-cloud/') from cloud import CloudExtension
attract = AttractExtension() attract = AttractExtension()
flamenco = FlamencoExtension() flamenco = FlamencoExtension()
svnman = SVNManExtension()
cloud = CloudExtension()
application = PillarServer(dirname(abspath(__file__))) application = PillarServer(my_path)
application.load_extension(attract, '/attract') application.load_extension(attract, '/attract')
application.load_extension(flamenco, '/flamenco') application.load_extension(flamenco, '/flamenco')
application.load_extension(svnman, '/svn')
application.load_extension(cloud, None)
application.process_extensions() application.process_extensions()
if __name__ == '__main__':
WSGIServer(application).run()

5
setup.cfg Normal file
View File

@@ -0,0 +1,5 @@
[tool:pytest]
addopts = -v --cov cloud --cov-report term-missing --ignore node_modules --ignore docker
[pep8]
max-line-length = 100

752
src/styles/_homepage.sass Normal file
View File

@@ -0,0 +1,752 @@
.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
section.dashboard-main,
section.dashboard-secondary
+media-xs
width: 100%
margin: 20px auto
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
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

39
src/styles/_services.sass Normal file
View File

@@ -0,0 +1,39 @@
.services
#page-header
text-shadow: 1px 1px 0 rgba(black, .2)
border-bottom: none
align-items: initial
.page-title
text-align: left
font-size: 3em
margin: 0
.page-title-summary
max-width: 900px
text-align: left
font-size: 1.4em
.page-card-side
img
max-width: 100%
border-radius: 3px
.tip
margin-top: 15px
color: $color-text-dark-secondary
font-size: .8em
a
color: $color-primary
text-decoration: underline
span.text-background
+text-background(white, #358, 2px, 5px 0)
.navbar-backdrop-overlay
background-image: linear-gradient(rgba(black, .3), rgba(black, .8))
#blender-sync
small strong
color: $color-success

401
src/styles/_welcome.sass Normal file
View File

@@ -0,0 +1,401 @@
.join
position: relative
nav.navbar
background-color: white
.navbar-brand
color: $color-text
li a.navbar-item
color: $color-text
.navbar-container
+container-behavior
.navbar-toggle
border: 2px solid $color-text-dark-primary
color: $color-text
.navbar-nav
+media-xs
padding: 10px
#page-header
+media-lg
min-height: 600px
+media-xs
background-size: cover
background-position: right
min-height: 500px
.page-title,
.page-title-summary
line-height: 1.2em
text-align: left
text-shadow: 1px 1px 0 rgba(black, .5), 0 0 25px rgba(black, .5)
.page-title-summary
font-size: 1.4em
li.special
color: $color-success
font-weight: bold
.page-card
&:nth-child(even)
background-color: white
&-header
margin-bottom: 0
&-side
+media-xs
width: 100%
max-width: 100%
&.text
+media-xs
padding-left: 50px
padding:
bottom: 150px
top: 150px
&.right
background-color: $color-background-light
.page-card-title,
.page-card-summary
padding-left: 25px
.page-card-title:after
left: 25px
.text
+media-xs
padding-left: 25px
&.intro
+media-xs
margin-left: 50px
.page-card-side
+media-sm
max-width: 500px
max-width: 600px
padding:
bottom: 0
top: 50px
&+.page-card-header
padding-top: 0
.page-card-summary
font-size: 1.2em
.page-card-image
align-items: center
display: flex
height: 100%
justify-content: center
&.light
overflow: hidden
position: relative
.learn
color: white
text-decoration: underline
.page-card
&-title
color: white
font-weight: bold
&:after
border-color: white
&-summary
color: white
position: relative
text-shadow: 1px 1px 0 rgba(black, .5), 0 0 25px rgba(black, .2)
z-index: 1
p
font-size: .9em
&.summary-action
font-size: .8em
a.page-card-cta
font-size: .9em
a
color: white
text-decoration: underline
a.page-card-cta
background: #ff4970
border-radius: 3px
border: none
box-shadow: 1px 1px 0 rgba(black, .2)
font-weight: bold
margin-right: 15px
margin-top: 25px
padding: 7px 20px
text-decoration: none
text-shadow: none
&:hover
background: lighten(#ff4970, 5%)
.page-card-image
img
+media-xs
display: none
max-width: initial
position: absolute
width: initial
z-index: 0
&.training
margin-top: 45px
a.page-card-cta
background: #0082ff
.page-card-image
img
right: 50px
top: 50%
transform: translateY(-50%)
&.open-movies
.page-card-image
img
top: 50px
left: 50px
&.services
.page-card-image
img
left: 50px
&.subscribe
padding: 40px 0
text-align: center
span strong
color: $color-danger
.page-card-side
width: 100%
max-width: 100%
.page-card-title
line-height: 1.5em
color: white
text-align: center
padding-bottom: 15px
&:after
border: none
.page-card-summary
color: $color-text-light-primary
.page-card-cta
text-align: center
font-size: 1.3em
padding: 7px 35px
margin-right: initial
border: thin solid rgba(white, .8)
border-radius: 3px
.training-other
color: $color-text-dark-secondary
font-size: .9em
padding: 30px
text-align: center
a
color: $color-text-dark-secondary
text-decoration: underline
&:hover
color: $color-text-dark-primary
.pricing
+media-xs
padding-top: 30px
margin-bottom: 0
padding-bottom: 100px
.supported-by
padding: 50px 0
text-align: center
background-color: $color-background
border-top: 5px solid $color-background-dark
img.logos
padding: 60px 0 100px 0
max-height: 500px
max-width: 100%
+media-xs
max-width: 100%
.assets
padding-bottom: 80px
.flex
+media-xs
flex-direction: column
display: flex
.box
flex: 1
margin: 20px
.page-triplet-container
.triplet-card
+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
width: 100%
section.pricing
padding: 50px 0
margin-bottom: 25px
background-color: $color-background-light
+clearfix
+media-xs
padding: 0
h2
text-align: center
font-size: 2.3em
text-shadow: 1px 1px 0 white
margin:
top: 10px
bottom: 50px
padding: 0
+media-xs
font-size: 1.6em
.box
margin-top: 40px
padding: 20px 20px 60px 20px
border: 1px solid $color-text-dark-hint
background-color: white
position: relative
text-align: center
border-top: 3px solid rgba($color-text-dark-secondary, .5)
border-bottom-left-radius: 3px
border-bottom-right-radius: 3px
+media-xs
margin-bottom: 15px
&.yearly
border-top: 3px solid $color-info
transform: scale(1.1)
+media-xs
transform: scale(1)
a.sign-up-now
+button($color-primary, 3px, true)
h3
font:
size: 1.8em
family: $font-body
padding-bottom: 0
margin: 25px 0 0 10px
.pricing-display
position: relative
color: $color-info
padding: 10px 0
.currency-sign,
.digit-dec
font-size: 1.7em
position: relative
top: -25px
font-weight: 100
.digit-int
font-size: 4.5em
font-weight: 100
.pricing-caption
color: $color-text-dark-hint
text-align: center
padding-bottom: 25px
ul
text-align: left
+list-bullets
ul
color: $color-text-dark-primary
padding-left: 10px
margin-top: 15px
li
padding-bottom: 8px
.sign-up-now
position: absolute
bottom: 25px
width: 65%
left: 50%
transform: translateX(-50%)
font-size: 1.2em
+button($color-primary, 3px)
padding: 5px 25px
white-space: nowrap
text-align: center
.education
color: $color-text-dark-primary
font-size: 1.1em
padding: 25px 15px 0
text-align: center
h3
color: $color-primary-dark
.btn
margin-top: 15px
min-width: 200px

25
src/styles/main.sass Normal file
View File

@@ -0,0 +1,25 @@
@import ../../../pillar/src/styles/_normalize
@import ../../../pillar/src/styles/_config
@import ../../../pillar/src/styles/_utils
/* Generic styles (comments, notifications, etc) come from base.css */
/* 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
/* services, about, etc */
@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
/* CSS for pillar-font comes from fontello.com using static/assets/font/config.json */

229
src/templates/about.pug Normal file
View File

@@ -0,0 +1,229 @@
| {% extends 'layout.html' %}
| {% block page_title %}Welcome{% endblock %}
| {% block css %}
| {{ super() }}
style.
.page-card-side {
padding: 60px 10px !important;
}
| {% endblock css %}
| {% block body %}
#page-container
#page-header(style="background-image: url({{ url_for('static', filename='assets/img/backgrounds/pattern_01.jpg')}})")
.page-title(style='text-align: left')
em ABOUT
i.pi-blender-cloud-logo
.page-title-summary
| Blender Cloud means inspiration, knowledge, and tools in one place.
br
| Started in 2014, it has been pushing the meaning of recurring crowdfunding ever since.
br
| By subscribing to Blender Cloud you support the creation of open content,
br
| the development of high-end production tools like Blender and access a
br
| unique set of learning and creative resources.
#page-content
section.page-card
.page-card-side
h2.page-card-title
| Launch at SXSW
small March 9th, 2014
.page-card-summary
| 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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Gooseberry | Cosmos Laundromat
small March 10th, 2015
.page-card-summary
| Weekly folders with updates for subscribers. Initial development of Attract, which will become the new cloud some months later on.
section.page-card
.page-card-side
h2.page-card-title
| Glass Half
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
| ! But the biggest outcome from Glass Half was definitely
a(href='https://cloud.blender.org/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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Art Gallery
small November 19th, 2015
.page-card-summary
| Learn by example. Introducing a place for amazing artwork to be shared, along with its blendfiles and breakdowns.
section.page-card
.page-card-side
h2.page-card-title
| Blender Institute Podcast
small November 24th, 2015
.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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Blenrig
small December 1st, 2015
.page-card-summary
| The most powerful and versatile rigging framework for Blender, used and tested through Cosmos Laundromat and the Caminandes series, is now part of Blender Cloud!
section.page-card
.page-card-side
h2.page-card-title
| Texture Library
small December 23rd, 2015
.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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Character Library
small January 5th, 2016
.page-card-summary
| High-quality, animation-ready characters collection from all the Blender Institute open projects, plus a brand new one: Vincent!
section.page-card
.page-card-side
h2.page-card-title
| Caminandes: Llamigos
small January 30th, 2016
.page-card-summary
| The
a(href='https://www.youtube.com/watch?v=SkVqJ1SGeL0') third episode
| of the Caminandes series was completely done -and sponsored! through Blender Cloud. It's also the only project til date to have
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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Sybren
small March 1st, 2016
.page-card-summary
| Dr. Sybren Stüvel starts working at the Blender Institute!
section.page-card
.page-card-side
h2.page-card-title
| Private Projects
small May 3rd, 2016
.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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Project Sharing
small May 9th, 2016
.page-card-summary
| Team work! Share your projects with other Blender Cloud subscribers.
section.page-card
.page-card-side
h2.page-card-title
| Blender Cloud add-on with Texture Library
small May 11th, 2016
.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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Private Texture Libraries
small May 23rd, 2016
.page-card-summary
| Create your own private textures library and browse it in Blender with our add-on.
section.page-card
.page-card-side
h2.page-card-title
| Blender Sync
small June 30th, 2016
.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') }}")
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') }}")
.page-card-side
h2.page-card-title
| Image Sharing
small July 14th, 2016
.page-card-summary
| Quickly share renders and Blender screenshots within Blender with our add-on.
section.page-card
.page-card-side
h2.page-card-title
a(href='https://cloud.blender.org/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') }}")
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') }}")
.page-card-side
h2.page-card-title
a(href='https://cloud.blender.org/blog/new-training-toon-character-workflow')
| Toon Character Workflow
small December 6th, 2016
.page-card-summary
| YouTube star Dillon Gu joins Blender Cloud for a new tutorial series that will guide you from the basics to a finished toon-shaded character.
section.page-card
.page-card-side
h2.page-card-title
| Agent 327 - Barbershop
.page-card-summary
p
| Follow the ongoing progress of the Barbershop fight scene, an animation test for the Agent 327 project. By subscribing to Blender Cloud, you get access
| 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') }}")
| {% endblock body%}

View File

@@ -0,0 +1,93 @@
doctype html
html
head
title {% block title %}{{ subject }}{% endblock %}
style.
@import url('https://fonts.googleapis.com/css?family=Roboto');
html, body {
font-family: 'Roboto', 'Noto Sans', sans-serif;
font-size: 11pt;
background-color: #eaebec;
color: black;
}
section {
max-width: 522px;
/*width: 522px;*/
margin: 0 auto 15px auto;
padding: 10px 25px;
box-shadow: rgba(0, 0, 0, 0.298039) 0px 1px 4px -1px;
background-color: white;
}
a:link {
color: #2e99b8;
}
a:link, a:visited {
text-decoration: none;
}
section.about, p.ps {
color: #888;
font-size: smaller;
}
section.about a:link, p.ps a:link {
color: #7297ab;
}
h1 {
text-shadow: 1px 1px 1px rgba(0, 0, 0, 0.5), 0 0 25px rgba(0, 0, 0, 0.6);
color: white;
font-size: 30px;
background: #d5d0cb url('https://cloud.blender.org/static/assets/img/email/background_caminandes_3_03.jpg') no-repeat top center;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
padding: 35px 25px;
max-width: 522px;
/*width: 522px;*/
margin: 15px auto 0 auto;
}
h2, h3 {
font-weight: 300;
color: #eb5e28;
position: relative;
}
p {
line-height: 150%;
}
p.closing {
margin-top: 2.5em;
}
p.buttons {
text-align: center;
margin: 4ex;
}
a.button {
text-align: center;
max-width: 30ex;
padding: 5px 30px;
text-decoration: none;
border-radius: 3px;
border: thin solid #2e99b8;
color: #2e99b8;
background-color: transparent;
text-shadow: none;
transition: color 350ms ease-out, border 150ms ease-in-out, opacity 150ms ease-in-out, background-color 150ms ease-in-out;
}
a.button:hover {
color: white;
background-color: #2e99b8;
}
body
| {% block body %}{% endblock %}

View File

@@ -0,0 +1 @@
{% block body %}{% endblock %}

View File

@@ -0,0 +1,53 @@
| {% extends "emails/layout.html" %}
| {% block body %}
h1 Welcome to Blender Cloud!
section
h2 Hi {{ user.full_name or user.email }},
p.
Thanks for joining Blender Cloud, the Open Content creation platform! Your subscription helps
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 >
p.
Here is a quick guide to help you get started with Blender Cloud.
h2 Discover the Training Content
p.
Our high quality training is organised in #[a(href="{{ abs_url('cloud.courses') }}", target='_blank') Courses],
where experienced trainers teach you step-by-step specific techniques, and
#[a(href="{{ abs_url('cloud.workshops') }}", target='_blank') Workshops],
where you get the feeling of peeking behind the shoulders of an artist explaining their creative workflow.
h2 Try our Services
p.
Make sure you download the #[a(href="{{ abs_url('cloud.services') }}", target='_blank') Blender Cloud Add-on],
so you can synchronize your Blender settings across multiple computers with
#[a(href="https://cloud.blender.org/blog/introducing-blender-sync", target='_blank') Blender Sync],
access our Texture and HDRI libraries directly within Blender, and much more.
h2 Follow the Open Projects
p.
Follow #[a(href='https://cloud.blender.org/p/hero/') Hero] and
#[a(href='https://cloud.blender.org/p/spring/') Spring], access exclusive making-of content and
assets from our current and past
#[a(href="{{ abs_url('cloud.open_projects') }}", target='_blank') Open Projects].
h2 We are here for you
p.
Do you have any question about your subscription? Any suggestion on how to improve Blender Cloud?
Just reply to this message or write us at
#[a(href='mailto:cloudsupport@blender.org') cloudsupport@blender.org] on working days we'll
get back to you within a day.
p
| Cheers,
br
| Sybren and the Blender Cloud Team
hr
small.
PS: If you do not want to receive other emails from us,
#[a(href="{{ abs_url('settings.emails') }}") we've got you covered].
| {% endblock %}

View File

@@ -0,0 +1,66 @@
{% extends "emails/layout.txt" %}
{% set blender_cloud = abs_url('cloud.homepage') %}
{% block body %}
Welcome to Blender Cloud, {{ user.full_name or user.nickname }}!
Thanks for joining Blender Cloud, the Open Content creation
platform. Your subscription helps our team to create more Open
Projects, training, services and of course to make Blender the
best CG pipeline in the world. You rock!
{{ blender_cloud }}
Here is a quick guide to help you get started with Blender Cloud.
## Discover the Training Content
Our high quality training is organised in courses, where
experienced trainers teach you step-by-step specific techniques,
and workshops, where you get the feeling of peeking behind the
shoulders of an artist explaining their creative workflow.
{{ abs_url('cloud.courses') }}
{{ abs_url('cloud.workshops') }}
## Try our Services
Make sure you download the Blender Cloud Add-on, so you can
synchronize your Blender settings across multiple computers with
Blender Sync, access our Texture and HDRI libraries directly
within Blender, and much more.
{{ abs_url('cloud.services') }}
https://cloud.blender.org/blog/introducing-blender-sync
## Follow the Open Projects
Follow Hero and Spring, access exclusive making-of content and
assets from our current and past Open Projects.
https://cloud.blender.org/p/hero/
https://cloud.blender.org/p/spring/
{{ abs_url('cloud.open_projects') }}
## We are here for you
Do you have any question about your subscription? Any suggestion
on how to improve Blender Cloud? Just reply to this message or
write us at
cloudsupport@blender.org
on working days we'll get back to you within a day.
Cheers,
Sybren and the Blender Cloud Team
PS: If you do not want to receive other emails from us, we've got
you covered. {{ abs_url('settings.emails') }}
{% endblock %}

274
src/templates/homepage.pug Normal file
View File

@@ -0,0 +1,274 @@
| {% extends 'layout.html' %}
| {% from '_macros/_navigation.html' import navigation_tabs %}
| {% from 'nodes/custom/blog/_macros.html' import render_blog_post %}
| {% set title = 'homepage' %}
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="https://cloud.blender.org/")
meta(property="og:title", content="Blender Cloud")
meta(name="twitter:title", content="Blender Cloud")
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.")
meta(name="twitter: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(property="og: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 %}")
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 body %}
.dashboard-container
section.dashboard-main
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 %}
.more
a(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
section.dashboard-secondary
| {{ navigation_tabs(title) }}
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) }}")
| {% 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 }}
| {% 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 }}
| {% endif %}
| {% endfor %}
section.comments
h4 Latest Comments
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 }}
| {% endif %}
li.when
a(href="{{ n.url }}", title="{{ n._created }}")
| {{ n._created | pretty_date_time }}
| {% endfor %}
| {% else %}
li.activity-stream__list-item.empty#activity-stream__empty
| No comments... yet!
| {% endif %}
| {% endblock %}
| {% block footer_scripts %}
script.
$(function () {
/* cleanup mentions in comments */
$('.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 %}

351
src/templates/layout.pug Normal file
View File

@@ -0,0 +1,351 @@
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="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")
| {% if config['GOOGLE_SITE_VERIFICATION'] %}
meta(name="google-site-verification" content="{{ config['GOOGLE_SITE_VERIFICATION'] }}")
| {% endif %}
meta(property="og:site_name", content="Blender Cloud")
meta(property="og:locale", content="en_US")
meta(name="twitter:card", content="summary_large_image")
meta(name="twitter:site", content="@Blender_Cloud")
| {% block og %}
meta(property="og:title", content="Blender Cloud")
meta(property="og:url", content="https://cloud.blender.org")
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.")
meta(name="twitter:title", content="Blender Cloud")
meta(name="twitter: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="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.
| {% 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) }}")
| {% 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")
| {% else %}
link(href="{{ url_for('static', filename='cloud/assets/css/main.css', v=9112017) }}", rel="stylesheet")
| {% endif %}
| {% endblock css %}
| {% if not title %}{% set title="default" %}{% endif %}
body(class="{{ title }}")
.container-page
| {% with messages = get_flashed_messages(with_categories=True) %}
| {% if messages %}
| {% 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 %}
| {% endif %}
| {% endwith %}
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_search %}
.search-input
input#cloud-search(
type="text",
placeholder="Search assets, tutorials...")
i.search-icon.pi-search
| {% endblock navigation_search %}
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 %}
| {% 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 %}
| {% 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 %}
| {% endblock footer_container %}
#notification-pop(data-url="", data-read-toggle="")
.nc-progress
a#pop-close(href="#", title="Dismiss")
i.pi-cancel
.nc-item
.nc-avatar
.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) }}")
script.
$(document).ready(function() {
{% if current_user.is_authenticated %}
getNotificationsLoop(); // Check for new notifications in the background
// Resize #notifications and change overflow for scrollbars
$(window).on("resize", function() { notificationsResize(); });
{% endif %}
});
if (typeof $().tooltip != 'undefined'){
$('[data-toggle="tooltip"]').tooltip({'delay' : {'show': 0, 'hide': 0}});
}
if(typeof($.fn.popover) != 'undefined'){
$('[data-toggle="popover"]').popover();
}
| {% block footer_scripts_pre %}{% endblock %}
| {% block footer_scripts %}{% endblock %}
script.
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{{ config.GOOGLE_ANALYTICS_TRACKING_ID }} ', 'auto', {'allowAnchor': true});
ga('send', 'pageview');

View File

@@ -0,0 +1,64 @@
| {% extends 'menus/user_base.html' %}
| {% if current_user.has_role('demo') %}
| {% set subscription = 'demo' %}
| {% elif current_user.has_cap('subscriber') %}
| {% set subscription = 'subscriber' %}
| {% else %}
| {% set subscription = 'none' %}
| {% 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")
.special(class="{{ subscription }}")
| {% if subscription == 'subscriber' %}
i.pi-check
| {% elif subscription == 'demo' %}
i.pi-heart-filled
| {% else %}
i.pi-attention
| {% endif %}
| {% 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")
i.pi-grin
span Your subscription is active!
| {% elif subscription == 'demo' %}
a.navbar-item(
href="{{url_for('settings.billing')}}"
title="View subscription info")
i.pi-heart-filled
span You have a free account.
| {% elif current_user.has_cap('can-renew-subscription') %}
a.navbar-item(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")
i.pi-unhappy
span.info Your subscription is not active.
span.renew Click here to renew.
| {% endif %}
| {{ super() }}
li
a.navbar-item(
href="{{ url_for('settings.billing') }}"
title="Billing")
i.pi-credit-card
| Subscription
| {% endblock menu_list %}

127
src/templates/privacy.pug Normal file
View File

@@ -0,0 +1,127 @@
| {% extends 'layout.html' %}
| {% block page_title %}Terms and Conditions{% endblock %}
| {% block css %}
| {{ super() }}
style.
#page-content {
padding: 2% 20% !important;
}
| {% endblock css %}
| {% block body %}
#page-container
#page-content
h2 Privacy Policy of cloud.blender.org
p.
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,
institute@blender.org
p.
Blender Institute has been authorised by Stichting Blender Foundation to conduct these
services. Blender Institute is committed to comply to the goals of the public benefit
free/open source project and community at blender.org.
p This Privacy Policy outlines how this is being achieved.
h3 Purpose of Service
p Blender Cloud is a service that provides or will provide:
ol
li.
Identification of a person via a unique BlenderID. Websites within the blender.org
domain can use it to enable a single login for users of the websites.
li.
Membership - via monthly subscription - to access and share data from Open Projects at
blender.org, post feedback or reviews.
li.
Project space - via monthly subscription - to upload and share data with other members.
p.
Information provided by Users, such as email address and billing information, will only be
used for the purpose of the service itself and to monitor traffic and usage.
h3 Types of Data collected
p.
Among the types of Personal Data that this Application collects, by itself or through third
parties, there are: Cookie and Usage Data, via Google Analytics.
The Personal Data may be freely provided by the User, or collected automatically when using
this Application.
Any use of Cookies - or of other tracking tools - by this Application or by the owners of
third party services used by this Application, unless stated otherwise, serves to identify
Users and remember their preferences, for the sole purpose of providing the service required
by the User.
The User assumes responsibility for the Personal Data of third parties published or shared
through this Application and declares to have the right to communicate or broadcast them,
thus relieving the Data Controller of all responsibility.
h3 Mode and place of processing the Data
h4 Method of processing
p.
The Data Controller processes the Data of Users in proper manner and shall take appropriate
security measures to prevent unauthorized access, disclosure, modification or unauthorized
destruction of the Data.
The Data processing is carried out using computers and/or IT enabled tools, following
organizational procedures and modes strictly related to the purposes indicated. In addition
to the Data Controller, in some cases, the Data may be accessible to certain types of
persons in charge, involved with the operation of the site (administration, sales,
marketing, legal, system administration) or external parties (such as third party technical
service providers, mail carriers, hosting providers, IT companies, communications agencies)
appointed, if necessary, as Data Processors by the Owner. The updated list of these
parties may be requested from the Data Controller at any time.
h4 Place
p.
The Data is processed at the Data Controller headquarters, unless stated otherwise in the
rest of this document.
h4 Conservation Time
p.
The Data is kept for the time necessary to provide the service requested by the User, or
stated by the purposes outlined in this document, and the User can always request the Data
Controller for their suspension or removal.
h3 The use of the collected Data
p.
The Data concerning the User is collected to allow the Application to provide its services,
as well as for the following purposes: Traffic and Usage Analytics.
The Personal Data used for each purpose is outlined in the specific sections of this document.
p.
Personal Data is collected for the following purposes and using the following services:
ul
li Google Analytics
h3 Additional information about Data collection and processing
h4 Legal Action
p.
The User's Personal Data may be used for legal purposes by the Data Controller, in Court or
in the stages leading to possible legal action arising from improper use of this
Application or the related services.
h4 Additional Information about User's Personal Data
p.
In addition to the information in this privacy policy, this Application may provide the User
with contextual information concerning particular services or the collection and processing
of Personal Data.
h4 System Logs and Maintenance
p.
For operation and maintenance purposes, this Application and any third party services may
collect files that record interaction with this Application (System Logs) or use for this
purpose other Personal Data (such as IP Address).
h4 Information not contained in this policy
p.
More details concerning the collection or processing of Personal Data may be requested from
the Data Controller at any time at its contact information.
h4 The rights of Users
p.
Users have the right, at any time, to know whether their Personal Data has been stored and
can consult the Data Controller to learn about their contents and origin, to verify their
accuracy or to ask for them to be supplemented, cancelled, updated or corrected, or for their
transformation into anonymous format or to block any data held in violation of the law, as
well as to oppose their treatment for any and all legitimate reasons. Requests should be sent
to the Data Controller at the contact information set out above.
This Application does not support “do not track” requests.
To understand if any of the third party services it uses honor the “do not track” requests,
please read their privacy policies.
h4 Changes to this privacy policy
p.
The Data Controller reserves the right to make changes to this privacy policy at any time by
giving notice to its Users on this page. It is strongly recommended to check this page often,
referring to the date of the last modification listed at the bottom. If a User objects to any
of the changes to the Policy, the User must cease using this Application and can request the
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
| {% endblock body%}

View File

@@ -0,0 +1,97 @@
| {% extends 'layout.html' %}
| {# 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.' %}
| {% 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.' %}
| {% elif 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.' %}
| {% 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 }}")
| {% endblock %}
| {% block page_title %}
| {{ page_title }}
| {% endblock %}
| {% block body %}
#project-container
#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 }}
.node_index-collection
| {% 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}}
| {% endif %}
a.learn-more LEARN MORE
| {% endif %}
| {% endfor %}
| {% endblock %}
| {% block footer_scripts %}
script.
$('.node_index-collection-card.project').on('click', function(e){
e.preventDefault();
window.location.href = $(this).data('url');
});
| {% endblock %}

View File

@@ -0,0 +1,28 @@
| {% extends "errors/layout.html" %}
| {% block title %}Renew | Blender Cloud{% endblock %}
| {% block head %}
noscript
meta(http-equiv="refresh", content="2; url=https://store.blender.org/renew-my-subscription.php")
| {% endblock %}
| {% block body %}
#error-container(class="error-404")
#error-box
.error-top-container
.error-title
i.pi-heart
| Renew your Blender Cloud subscription
.error-lead
p
='You are being forwarded to '
a(href='https://store.blender.org/renew-my-subscription.php') the Blender Store
=' to renew your subscription.'
.error-lead.extra
p
="If you aren't forwarded in a few seconds, use "
a(href="https://store.blender.org/renew-my-subscription.php") this direct link
=' instead.'
script.
// Use replace() so that the back button doesn't even visit this page.
window.location.replace('https://store.blender.org/renew-my-subscription.php');
| {% endblock %}

241
src/templates/services.pug Normal file
View File

@@ -0,0 +1,241 @@
| {% extends 'layout.html' %}
| {% block page_title %}Services{% endblock %}
| {% set title = 'services' %}
| {% block og %}
meta(property="og:type", content="website")
meta(property="og:url", content="{{ url_for('cloud.services') }}")
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')}}")
| {% endblock %}
| {% 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.
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!
.navbar-backdrop-overlay
- var addon_text = 'Available through the <a href="{{ url_for(\'cloud.services\') }}#blender-cloud-add-on">Blender Cloud add-on</a>'
#page-content
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!
hr
small Blender Cloud add-on requires Blender 2.78 or newer
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 }}
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/blender_cloud_addon_thumbnail.png')}}")
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-cta(
href="https://cloud.blender.org/blog/introducing-blender-sync")
| Learn More
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/sync_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!
.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
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/tex_library_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!
.tip !{addon_text}
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')}}")
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.
a.page-card-cta(
href="https://cloud.blender.org/blog/introducing-private-projects")
| Learn More
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/projects_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.
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.page-card-cta(
href="https://cloud.blender.org/blog/attract-and-flamenco-public-beta",
title="Learn more about Attract")
| Learn More
.page-card-side
img(
src="{{ url_for('static', filename='assets/img/features/attract_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.
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
a.page-card-cta(
href="https://flamenco.io",
title="Learn more about Flamenco")
| Learn More
.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 %}
| {% endblock %}
| {% block footer_scripts %}
script.
// Click anywhere in the page to hide the overlay
function hideOverlay() {
$('#page-overlay.video').removeClass('active');
$('#page-overlay.video .video-embed').html('');
}
$(document).click(function() {
hideOverlay();
});
$(document).keyup(function(e) {
if (e.keyCode == 27) {
hideOverlay();
}
});
$('a.js-watch-video').click(function(e){
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>')
});
| {% endblock %}

30
src/templates/stats.pug Normal file
View File

@@ -0,0 +1,30 @@
| {% extends 'layout.html' %}
| {% block page_title %}Statistics{% endblock %}
| {% set title = 'statistics' %}
| {% block og %}
meta(property="og:title", content="Blender Cloud Statistics")
meta(property="og:url", content="https://cloud.blender.org/stats")
meta(property="og:image", content="{{ url_for('static', filename='assets/img/backgrounds/background_andy_hdribot_01.jpg')}}")
| {% endblock %}
| {% block head %}
style.
iframe {
width: 100%;
min-height: 1000px;
margin: 0;
padding: 0;
border: none;
}
| {% endblock %}
| {% block body %}
#page-container
#page-content
iframe(src="https://stats.cloud.blender.org/app/kibana#/dashboard/a2ae48b0-f798-11e7-a3e6-1b054745db54?embed=true&_g=(time:(from:now-90d,mode:quick,to:now),refreshInterval:(display:Off,pause:!f,section:0,value:0))")
| {% endblock %}

View File

@@ -0,0 +1,137 @@
| {% extends 'layout.html' %}
| {% block page_title %}Terms and Conditions{% endblock %}
| {% block css %}
| {{ super() }}
style.
#page-content {
padding: 2% 20% !important;
}
| {% endblock css %}
| {% block body %}
#page-container
#page-content
h1 Terms and Conditions
p.
The Blender Cloud (we or us, or the Service) is an open production funding/donation
platform that offers access for registered members to project space and repositories of
creative content and data. By registering as a member or by using the service in any way,
you accept these Terms of Service ("Agreement"), which forms a binding agreement between you
and the Blender Cloud. If you do not wish to be bound by this Agreement, do not use the
Blender Cloud Service.
p.
Blender Cloud is an activity of Blender Institute B.V. - Entrepotdok 57A - 1018 AD Amsterdam
- the Netherlands, contact: institute@blender.org.
p.
Blender Institute has been authorised by Stichting Blender Foundation to conduct these
services on blender.org. Blender Institute is committed to comply to and support the goals of
the public benefit free/open source project and community at blender.org.
h2 Registration, the Blender ID
p.
By registration at the Blender Cloud, your email address will become the identification
("Blender ID") for services on blender.org in general. Registration is free, and only
requires a confirmation of the owner of the email address - by responding to an authentication
email.
p.
Blender Cloud currently offers no additional services for Blender ID registrations. These
might be offered later, and will always be approved by Blender Foundation and be offered
as opt-in.
h2 Badges: Supporters and Members
p.
After registration to the Blender Cloud, a user will get options to upgrade his account with
a number of options. Currently these options are for supporting, crediting or membership. All
options are called "Badges" - which will be used to show as a special property (icon) as part
of your Blender ID - for these places at blender.org that support it (forums or reviews).
p.
The Membership Badge is based on a monthly subscription, which can be applied for via
recurring payments or via a pre-payment. Members get access to all services of Blender Cloud,
which includes previous Open Movies, Open Workshop videos and the current running Open
Production. By keeping your membership active a user can earn a Credit Badge. Rules for this
will be mentioned on the campaign page for an Open Production.
p.
The other Badges are sold as special 'funding perks' - such as access to a film download,
an online screening or a film/website credit. A supporter can choose to claim a badge, and
only agree to pay for it after the production campaign reached its funding limit.
h2 Blender Cloud Membership fee
p.
To register and activate a membership an additional charge will apply, including a minimum of
3 months of membership fees. Fees are: (Feb 23, 2014)
p.
45 euro (59 USD), membership registration, which includes 3 months Cloud membership.
After that, 10 euro (13.50 USD), monthly membership fee
h2 Refund policy
p.
Membership fees and supporter contributions are non-refundable. That money we really need to
keep our services running, and to make open source and open content productions possible.
p.
We really care about our members and supporters though - in case you are really not happy
with our Blender Cloud we will kindly respond to your comments and try to settle a dispute
with you in harmony.
p.
Refunds will happen without delay when we get a report of fraud, abuse or non-authorised
usage of funds.
p Contact: cloudsupport@blender.org.
h2 Blender Cloud Membership cancellation
p.
You may delete your account at any time via your personal account page, or by contacting us
at cloudsupport@blender.org.
p.
In case of suspected fraud or abuse of the service after registration and membership payment,
the account owner will be contacted and asked for clarification. Failure to alleviate
charges of fraud may result in the termination of the members account.
p.
Upon termination, all licenses granted by the Blender Cloud will terminate. In the event of
account deletion for any reason, content that you submitted may no longer be available.
The Blender Cloud shall not be responsible for the loss of such content.
h2 Blender Cloud licences
p.
Unless notified otherwise, all digital content (webpages, video, artwork, 3D data) is
available under the Creative Commons Attribution 4.0.
p.
All software provided on Blender Cloud will be under a free and open license as recognised
as such by the Free Software Foundation.
p.
Trademarks and logos - including the Blender logo - are copyrighted properties, with all
rights reserved by its owners.
h2 Indemnification
p.
You will indemnify, defend, and hold harmless the Blender Cloud and its affiliates, directors,
officers, employees, and agents, from and against all third party actions that: arise from
your activities on the Blender Cloud Service; assert a violation by you of any term of this
Agreement; or assert that any content you submitted to the Blender Cloud violates any law or
infringes any third party right, including any intellectual property or privacy right.
h2 Availability of the service
p.
The Blender Cloud is not responsible for any damages resulting from the unavailability of
the Service.
h2 Modification
p.
This Agreement may not be modified except by a revised Terms of Service posted by the Blender
Cloud on the Blender Cloud Site or a written amendment signed by an authorized representative
of the Blender Cloud. A revised Terms of Service will be effective as of the date it is
posted on the Blender Cloud Site.
p.
THE BLENDER CLOUD WILL NOT BE LIABLE FOR ANY LOSS OF INCOME, LOSS OF PROFITS, LOSS OF
CONTRACTS, LOSS OF DATA OR FOR ANY INDIRECT, INCIDENTAL, EXEMPLARY, SPECIAL, PUNITIVE OR
CONSEQUENTIAL LOSS OR DAMAGE OF ANY KIND HOWSOEVER ARISING AND WHETHER CAUSED BY TORT
(INCLUDING NEGLIGENCE), BREACH OF CONTRACT, WARRANTY OR OTHERWISE. OUR MAXIMUM AGGREGATE
LIABILITY UNDER THESE TERMS AND CONDITIONS WHETHER IN TORT (INCLUDING NEGLIGENCE) OR
OTHERWISE SHALL IN NO CIRCUMSTANCES EXCEED YOUR ANNUAL MEMBERSHIP FEE.
h2 Disputes
p.
These Terms and any Additional Terms are governed by and construed by the laws of the
Netherlands exclusively. The parties agree that any disputes or proceedings between us
and you concerning these Terms, any Additional Terms, and/or any of the Websites or Services
shall be brought in a court of competent jurisdiction sitting in the Netherlands, and hereby
consent to the personal jurisdiction and venue of such court.
| {% endblock body%}

View File

@@ -0,0 +1,9 @@
| {% extends "users/edit_embed_base.html" %}
| {% block user_links %}
p This user on:
=' '
a(href="https://store.blender.org/wp-admin/users.php?s={{ user.email | urlencode }}",target='_blank') Blender Store
=' | '
a(href="https://www.blender.org/id/admin/bid_main/user/?q={{ user.email | urlencode }}",target='_blank') Blender ID
| {% endblock %}

View File

@@ -0,0 +1,92 @@
| {% extends 'users/settings/page.html' %}
| {% block head %}
| {{ super() }}
style(type='text/css').
button#recheck_subscription {
margin-top: 1em;
}
| {% endblock %}
| {% block settings_page_title %}Subscription{% endblock %}
| {% block settings_page_content %}
//--------------------------------------------------------------------------------------------------
| {% if user_cls == 'demo' %}
h3.subscription-demo
i.pi-heart-filled
| You have a free account
hr
p.
You have full access to the Blender Cloud, provided by the Blender Institute. This account is
meant for free evaluation of the service. Get in touch with
#[a(href="mailto:cloudsupport@blender.org") cloudsupport@blender.org] if you have any questions.
//--------------------------------------------------------------------------------------------------
| {% elif user_cls == 'outsider' %}
h3.subscription-missing
i.pi-info
| You do not have an active subscription.
hr
h3
a(href="https://store.blender.org/product/membership/") Get full access to Blender Cloud now!
//--------------------------------------------------------------------------------------------------
| {% elif user_cls == 'subscriber-expired' %}
| {% set renew_url = url_for('cloud.renew_subscription') %}
h3.subscription-missing
i.pi-info
a(href="{{renew_url}}") Your subscription can be renewed
hr
p.text-danger Subscription expired on: <strong>{{ expiration_date }}</strong>
p
a.btn.btn-success(href="{{renew_url}}") Renew now
//--------------------------------------------------------------------------------------------------
| {% elif current_user.has_cap('subscriber') %}
h3.subscription-active
i.pi-check
| Your subscription is active
//---------------------------------
| {% if user_cls == 'subscriber' %}
h4 Thank you for supporting us!
hr
p Subscription expires on: <strong>{{ expiration_date }}</strong>
p
a(href="{{ config['EXTERNAL_SUBSCRIPTIONS_MANAGEMENT_SERVER'] | urljoin('my-account/subscriptions/') }}") Manage your subscription on Blender Store
//---------------------------------
| {% elif user_cls == 'subscriber-org' %}
p Your organisation provides you with your subscription.
| {% endif %}
//--------------------------------------------------------------------------------------------------
| {% endif %}
hr
p
button#recheck_subscription.btn.btn-default(onclick="javascript:recheck_subscription(this)") Re-check my subscription
hr
script.
function recheck_subscription(button) {
$(button).text('Checking');
$.get('/api/bcloud/update-subscription')
.done(function() {
window.location.reload();
})
.fail(function(err) {
if (err.status == 403) {
/* This happens when we are no longer logged in properly, so just refresh the
* page to get a proper status. */
window.location.reload();
return;
}
alert('Unable to update subscription, please check your internet connection.');
})
;
}
| {% endblock %}

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