Compare commits

..

25 Commits

Author SHA1 Message Date
8065ab88a4 Bumped version to 1.8.0 2018-01-03 14:09:01 +01:00
6baf43e53b Fix KeyError when browsing HDRIs 2018-01-02 17:24:03 +01:00
f1fa273370 Updated changelog 2018-01-02 16:48:47 +01:00
bf96638c88 Updated changelog 2018-01-02 16:44:33 +01:00
bc8a985228 Added button to open a Cloud project in webbrowser. 2018-01-02 16:44:29 +01:00
ba14c33b6d Store project-specific settings in the preferences.
This stores project-specific settings, such as filesystem paths, for each
project, and restores those settings when the project is selected again.
Does not touch settings that haven't been set for the newly selected
project.
2018-01-02 16:42:37 +01:00
0a7e7195a2 Changed default Flamenco path to tempfile.gettempdir()
The previous defaults were very Blender Institute specific.
2018-01-02 15:37:38 +01:00
ecab0f6163 Upgraded cryptography package version + its dependencies
This was required to get the package to build on Kubuntu 17.10.
2018-01-02 15:36:19 +01:00
3c91ccced6 Texture Browser: use DPI from user preferences 2018-01-02 15:10:27 +01:00
c9ed6c7d23 Texture browser: show which map types are available in GUI 2018-01-02 14:53:30 +01:00
5fa01daf9e Revisit previous path when re-opening texture browser. 2018-01-02 14:11:30 +01:00
77664fb6d7 Distinguish between 'renew' and 'join' in messages & URL to open. 2018-01-02 14:02:17 +01:00
45cffc5365 Bumped version to 1.7.99 (surrogate for 1.8-dev) 2018-01-02 14:02:17 +01:00
fb5433d473 Bumped version to 1.7.5 2017-10-06 12:39:38 +02:00
a17fe45712 Allow overriding the render output path on a per-scene basis. 2017-10-06 12:39:18 +02:00
1bfba64bdc Formatting 2017-10-06 12:38:17 +02:00
cdb4bf4f4f Renamed 'Job File Path' to 'Job Storage Path' so it's more explicit. 2017-10-06 12:37:44 +02:00
15254b8951 Sorting the project list in user prefs alphabetically 2017-10-06 12:35:25 +02:00
3ed5f2c187 Bumped version to 1.7.4 2017-09-05 11:26:52 +02:00
0be3bf7f49 Fixed unit test, it still mocked sys.platform
We now use platform.system() to detect the platform.
2017-09-05 11:25:25 +02:00
f207e14664 Added link to changelog 2017-09-05 11:16:22 +02:00
9932003400 Fix T48852: screenshot always shows "Communicating with Blender Cloud" 2017-09-05 11:16:17 +02:00
e7035e6f0c Updated changelog 2017-09-05 11:11:31 +02:00
014a36d24e Fix T52621: class name collision upon add-on registration
This is checked since Blender 2.79.
2017-09-05 11:07:33 +02:00
068451a7aa Mark 1.7.3 as released in changelog 2017-09-05 11:06:28 +02:00
13 changed files with 402 additions and 114 deletions

View File

@@ -1,6 +1,37 @@
# Blender Cloud changelog
## Version 1.7.3 (in development)
## Version 1.8 (in development)
- Distinguish between 'please subscribe' (to get a new subscription) and 'please renew' (to renew an
existing subscription).
- When re-opening the Texture Browser it now opens in the same folder as where it was when closed.
- In the texture browser, draw the components of the texture (i.e. which map types are available),
such as 'bump, normal, specular'.
- Use Interface Scale setting from user preferences to draw the Texture Browser text.
- Store project-specific settings in the preferences, such as filesystem paths, for each project,
and restore those settings when the project is selected again. Does not touch settings that
haven't been set for the newly selected project. These settings are only saved when a setting
is updated, so to save your current settings need to update a single setting; this saves all
settings for the project.
- Added button in the User Preferences to open a Cloud project in your webbrowser.
## Version 1.7.5 (2017-10-06)
- Sorting the project list alphabetically.
- Renamed 'Job File Path' to 'Job Storage Path' so it's more explicit.
- Allow overriding the render output path on a per-scene basis.
## Version 1.7.4 (2017-09-05)
- Fix [T52621](https://developer.blender.org/T52621): Fixed class name collision upon add-on
registration. This is checked since Blender 2.79.
- Fix [T48852](https://developer.blender.org/T48852): Screenshot no longer shows "Communicating with
Blender Cloud".
## Version 1.7.3 (2017-08-08)
- Default to scene frame range when no frame range is given.
- Refuse to render on Flamenco before blend file is saved at least once.

View File

@@ -21,7 +21,7 @@
bl_info = {
'name': 'Blender Cloud',
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
'version': (1, 7, 3),
'version': (1, 8, 0),
'blender': (2, 77, 0),
'location': 'Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser',
'description': 'Texture library browser and Blender Sync. Requires the Blender ID addon '
@@ -79,6 +79,7 @@ def register():
reload_mod('blendfile')
reload_mod('home_project')
reload_mod('utils')
reload_mod('pillar')
async_loop = reload_mod('async_loop')
flamenco = reload_mod('flamenco')
@@ -87,9 +88,10 @@ def register():
settings_sync = reload_mod('settings_sync')
image_sharing = reload_mod('image_sharing')
blender = reload_mod('blender')
project_specific = reload_mod('project_specific')
else:
from . import (blender, texture_browser, async_loop, settings_sync, blendfile, home_project,
image_sharing, attract, flamenco)
image_sharing, attract, flamenco, project_specific)
async_loop.setup_asyncio_executor()
async_loop.register()
@@ -101,7 +103,7 @@ def register():
image_sharing.register()
blender.register()
blender.handle_project_update()
project_specific.handle_project_update()
def _monkey_patch_requests():

View File

@@ -173,7 +173,7 @@ class AttractPollMixin:
return attract_is_active
class ToolsPanel(AttractPollMixin, Panel):
class AttractToolsPanel(AttractPollMixin, Panel):
bl_label = 'Attract'
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
@@ -974,7 +974,7 @@ def register():
bpy.types.SEQUENCER_PT_edit.append(draw_strip_movie_meta)
bpy.utils.register_class(ToolsPanel)
bpy.utils.register_class(AttractToolsPanel)
bpy.utils.register_class(AttractShotRelink)
bpy.utils.register_class(AttractShotDelete)
bpy.utils.register_class(AttractStripUnlink)

View File

@@ -23,13 +23,14 @@ Separated from __init__.py so that we can import & run from non-Blender environm
import functools
import logging
import os.path
import tempfile
import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
import rna_prop_ui
from . import pillar, async_loop, flamenco
from . import pillar, async_loop, flamenco, project_specific
from .utils import pyside_cache, redraw
PILLAR_WEB_SERVER_URL = os.environ.get('BCLOUD_SERVER', 'https://cloud.blender.org/')
@@ -37,7 +38,6 @@ PILLAR_SERVER_URL = '%sapi/' % PILLAR_WEB_SERVER_URL
ADDON_NAME = 'blender_cloud'
log = logging.getLogger(__name__)
icons = None
@@ -139,30 +139,6 @@ def project_extensions(project_id) -> set:
return set(proj.get('enabled_for', ()))
def handle_project_update(_=None, _2=None):
"""Handles changing projects, which may cause extensions to be disabled/enabled.
Ignores arguments so that it can be used as property update callback.
"""
project_id = preferences().project.project
log.info('Updating internal state to reflect extensions enabled on current project %s.',
project_id)
project_extensions.cache_clear()
from blender_cloud import attract, flamenco
attract.deactivate()
flamenco.deactivate()
enabled_for = project_extensions(project_id)
log.info('Project extensions: %s', enabled_for)
if 'attract' in enabled_for:
attract.activate()
if 'flamenco' in enabled_for:
flamenco.activate()
class BlenderCloudProjectGroup(PropertyGroup):
status = EnumProperty(
items=[
@@ -177,7 +153,7 @@ class BlenderCloudProjectGroup(PropertyGroup):
items=bcloud_available_projects,
name='Cloud project',
description='Which Blender Cloud project to work with',
update=handle_project_update
update=project_specific.handle_project_update
)
# List of projects is stored in 'available_projects' ID property,
@@ -189,13 +165,13 @@ class BlenderCloudProjectGroup(PropertyGroup):
@available_projects.setter
def available_projects(self, new_projects):
self['available_projects'] = new_projects
handle_project_update()
project_specific.handle_project_update()
class BlenderCloudPreferences(AddonPreferences):
bl_idname = ADDON_NAME
# The following two properties are read-only to limit the scope of the
# The following property is read-only to limit the scope of the
# addon and allow for proper testing within this scope.
pillar_server = StringProperty(
name='Blender Cloud Server',
@@ -224,29 +200,32 @@ class BlenderCloudPreferences(AddonPreferences):
description='Local path of your Attract project, used to search for blend files; '
'usually best to set to an absolute path',
subtype='DIR_PATH',
default='//../')
default='//../',
update=project_specific.store,
)
flamenco_manager = PointerProperty(type=flamenco.FlamencoManagerGroup)
flamenco_exclude_filter = StringProperty(
name='File Exclude Filter',
description='Filter like "*.abc;*.mkv" to prevent certain files to be packed '
'into the output directory',
default='')
# TODO: before making Flamenco public, change the defaults to something less Institute-specific.
# NOTE: The assumption is that the workers can also find the files in the same path.
# This assumption is true for the Blender Institute.
default='',
update=project_specific.store,
)
flamenco_job_file_path = StringProperty(
name='Job File Path',
name='Job Storage Path',
description='Path where to store job files, should be accesible for Workers too',
subtype='DIR_PATH',
default='/render/_flamenco/storage')
# TODO: before making Flamenco public, change the defaults to something less Institute-specific.
default=tempfile.gettempdir(),
update=project_specific.store,
)
flamenco_job_output_path = StringProperty(
name='Job Output Path',
description='Path where to store output files, should be accessible for Workers',
subtype='DIR_PATH',
default='/render/_flamenco/output')
default=tempfile.gettempdir(),
update=project_specific.store,
)
flamenco_job_output_strip_components = IntProperty(
name='Job Output Path Strip Components',
description='The final output path comprises of the job output path, and the blend file '
@@ -255,11 +234,12 @@ class BlenderCloudPreferences(AddonPreferences):
min=0,
default=0,
soft_max=4,
update=project_specific.store,
)
flamenco_open_browser_after_submit = BoolProperty(
name='Open Browser after Submitting Job',
description='When enabled, Blender will open a webbrowser',
default=True
default=True,
)
def draw(self, context):
@@ -406,6 +386,10 @@ class BlenderCloudPreferences(AddonPreferences):
row_buttons.operator('pillar.projects',
text='',
icon='FILE_REFRESH')
props = row_buttons.operator('pillar.project_open_in_browser',
text='',
icon='WORLD')
props.project_id = project
else:
row_buttons.label('Fetching available projects.')
@@ -545,6 +529,36 @@ class PILLAR_OT_subscribe(Operator):
return {'FINISHED'}
class PILLAR_OT_project_open_in_browser(Operator):
bl_idname = 'pillar.project_open_in_browser'
bl_label = 'Open in Browser'
bl_description = 'Opens a webbrowser to show the project'
project_id = StringProperty(name='Project ID')
def execute(self, context):
if not self.project_id:
return {'CANCELLED'}
import webbrowser
import urllib.parse
import pillarsdk
from .pillar import sync_call
project = sync_call(pillarsdk.Project.find, self.project_id, {'projection': {'url': True}})
if log.isEnabledFor(logging.DEBUG):
import pprint
log.debug('found project: %s', pprint.pformat(project.to_dict()))
url = urllib.parse.urljoin(PILLAR_WEB_SERVER_URL, 'p/' + project.url)
webbrowser.open_new_tab(url)
self.report({'INFO'}, 'Opened a browser at %s' % url)
return {'FINISHED'}
class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillar.AuthenticatedPillarOperatorMixin,
Operator):
@@ -571,7 +585,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillarsdk.Project.all,
{'where': {'user': self.user_id,
'category': {'$ne': 'home'}},
'sort': '-_created',
'sort': '-name',
'projection': {'_id': True,
'name': True,
'extension_props': True},
@@ -581,7 +595,7 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
pillarsdk.Project.all,
{'where': {'user': {'$ne': self.user_id},
'permissions.groups.group': {'$in': self.db_user.groups}},
'sort': '-_created',
'sort': '-name',
'projection': {'_id': True,
'name': True,
'extension_props': True},
@@ -605,7 +619,10 @@ class PILLAR_OT_projects(async_loop.AsyncModalOperatorMixin,
projects = list(reduce_properties(projects_user['_items'])) + \
list(reduce_properties(projects_shared['_items']))
preferences().project.available_projects = projects
def proj_sort_key(project):
return project.get('name')
preferences().project.available_projects = sorted(projects, key=proj_sort_key)
self.quit()
@@ -669,6 +686,7 @@ def register():
bpy.utils.register_class(SyncStatusProperties)
bpy.utils.register_class(PILLAR_OT_subscribe)
bpy.utils.register_class(PILLAR_OT_projects)
bpy.utils.register_class(PILLAR_OT_project_open_in_browser)
bpy.utils.register_class(PILLAR_PT_image_custom_properties)
addon_prefs = preferences()
@@ -703,6 +721,7 @@ def unregister():
bpy.utils.unregister_class(SyncStatusProperties)
bpy.utils.unregister_class(PILLAR_OT_subscribe)
bpy.utils.unregister_class(PILLAR_OT_projects)
bpy.utils.unregister_class(PILLAR_OT_project_open_in_browser)
bpy.utils.unregister_class(PILLAR_PT_image_custom_properties)
del WindowManager.last_blender_cloud_location

View File

@@ -27,7 +27,6 @@ import os
from pathlib import Path, PurePath
import typing
if "bpy" in locals():
import importlib
@@ -39,15 +38,13 @@ if "bpy" in locals():
else:
from . import bam_interface, sdk
import bpy
from bpy.types import AddonPreferences, Operator, WindowManager, Scene, PropertyGroup
from bpy.props import StringProperty, EnumProperty, PointerProperty, BoolProperty, IntProperty
from .. import async_loop, pillar
from .. import async_loop, pillar, project_specific
from ..utils import pyside_cache, redraw
log = logging.getLogger(__name__)
# Global flag used to determine whether panels etc. can be drawn.
@@ -70,7 +67,9 @@ class FlamencoManagerGroup(PropertyGroup):
manager = EnumProperty(
items=available_managers,
name='Flamenco Manager',
description='Which Flamenco Manager to use for jobs')
description='Which Flamenco Manager to use for jobs',
update=project_specific.store,
)
status = EnumProperty(
items=[
@@ -455,6 +454,30 @@ class FLAMENCO_OT_explore_file_path(FlamencoPollMixin,
return {'FINISHED'}
class FLAMENCO_OT_enable_output_path_override(Operator):
"""Enables the 'override output path' setting."""
bl_idname = 'flamenco.enable_output_path_override'
bl_label = 'Enable Overriding of Output Path'
bl_description = 'Click to specify a non-default Output Path for this particular job'
def execute(self, context):
context.scene.flamenco_do_override_output_path = True
return {'FINISHED'}
class FLAMENCO_OT_disable_output_path_override(Operator):
"""Disables the 'override output path' setting."""
bl_idname = 'flamenco.disable_output_path_override'
bl_label = 'disable Overriding of Output Path'
bl_description = 'Click to use the default Output Path'
def execute(self, context):
context.scene.flamenco_do_override_output_path = False
return {'FINISHED'}
async def create_job(user_id: str,
project_id: str,
manager_id: str,
@@ -509,6 +532,7 @@ def _render_output_path(
flamenco_job_output_path: str,
render_image_format: str,
flamenco_render_frame_range: str,
include_rel_path: bool = True,
) -> typing.Optional[PurePath]:
"""Cached version of render_output_path()
@@ -533,8 +557,7 @@ def _render_output_path(
except ValueError:
return None
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:]
output_top = Path(flamenco_job_output_path)
output_top = PurePath(flamenco_job_output_path)
# Strip off '.flamenco' too; we use 'xxx.flamenco.blend' as job file, but
# don't want to have all the output paths ending in '.flamenco'.
@@ -542,7 +565,11 @@ def _render_output_path(
if stem.endswith('.flamenco'):
stem = stem[:-9]
dir_components = output_top.joinpath(*rel_parts) / stem
if include_rel_path:
rel_parts = proj_rel.parts[flamenco_job_output_strip_components:]
dir_components = output_top.joinpath(*rel_parts) / stem
else:
dir_components = output_top
# Blender will have to append the file extensions by itself.
if is_image_type(render_image_format):
@@ -567,13 +594,19 @@ def render_output_path(context, filepath: Path = None) -> typing.Optional[PurePa
if filepath is None:
filepath = Path(context.blend_data.filepath)
if scene.flamenco_do_override_output_path:
job_output_path = scene.flamenco_override_output_path
else:
job_output_path = prefs.flamenco_job_output_path
return _render_output_path(
prefs.cloud_project_local_path,
filepath,
prefs.flamenco_job_output_strip_components,
prefs.flamenco_job_output_path,
job_output_path,
scene.render.image_settings.file_format,
scene.flamenco_render_frame_range,
include_rel_path=not scene.flamenco_do_override_output_path,
)
@@ -607,8 +640,9 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
if getattr(context.scene, 'flamenco_render_job_type', None) == 'blender-render-progressive':
layout.prop(context.scene, 'flamenco_render_schunk_count')
readonly_stuff = layout.column(align=True)
labeled_row = readonly_stuff.split(0.25, align=True)
paths_layout = layout.column(align=True)
labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Storage:')
prop_btn_row = labeled_row.row(align=True)
prop_btn_row.label(prefs.flamenco_job_file_path)
@@ -616,19 +650,33 @@ class FLAMENCO_PT_render(bpy.types.Panel, FlamencoPollMixin):
text='', icon='DISK_DRIVE')
props.path = prefs.flamenco_job_file_path
labeled_row = readonly_stuff.split(0.25, align=True)
labeled_row.label('Output:')
prop_btn_row = labeled_row.row(align=True)
render_output = render_output_path(context)
if render_output is None:
prop_btn_row.label('Unable to render with Flamenco, outside of project directory.')
paths_layout.label('Unable to render with Flamenco, outside of project directory.')
else:
prop_btn_row.label(str(render_output))
labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Output:')
prop_btn_row = labeled_row.row(align=True)
if context.scene.flamenco_do_override_output_path:
prop_btn_row.prop(context.scene, 'flamenco_override_output_path', text='')
op = FLAMENCO_OT_disable_output_path_override.bl_idname
icon = 'X'
else:
prop_btn_row.label(str(render_output))
op = FLAMENCO_OT_enable_output_path_override.bl_idname
icon = 'GREASEPENCIL'
prop_btn_row.operator(op, icon=icon, text='')
props = prop_btn_row.operator(FLAMENCO_OT_explore_file_path.bl_idname,
text='', icon='DISK_DRIVE')
props.path = str(render_output.parent)
if context.scene.flamenco_do_override_output_path:
labeled_row = paths_layout.split(0.25, align=True)
labeled_row.label('Effective Output Path:')
labeled_row.label(str(render_output))
flamenco_status = context.window_manager.flamenco_status
if flamenco_status == 'IDLE':
layout.operator(FLAMENCO_OT_render.bl_idname,
@@ -660,6 +708,22 @@ def deactivate():
_render_output_path.cache_clear()
def flamenco_do_override_output_path_updated(scene, context):
"""Set the override paths to the default, if not yet set."""
# Only set a default when enabling the override.
if not scene.flamenco_do_override_output_path:
return
# Don't overwrite existing setting.
if scene.flamenco_override_output_path:
return
from ..blender import preferences
scene.flamenco_override_output_path = preferences().flamenco_job_output_path
log.info('Setting Override Output Path to %s', scene.flamenco_override_output_path)
def register():
from ..utils import redraw
@@ -669,6 +733,8 @@ def register():
bpy.utils.register_class(FLAMENCO_OT_scene_to_frame_range)
bpy.utils.register_class(FLAMENCO_OT_copy_files)
bpy.utils.register_class(FLAMENCO_OT_explore_file_path)
bpy.utils.register_class(FLAMENCO_OT_enable_output_path_override)
bpy.utils.register_class(FLAMENCO_OT_disable_output_path_override)
bpy.utils.register_class(FLAMENCO_PT_render)
scene = bpy.types.Scene
@@ -706,6 +772,19 @@ def register():
description='Higher numbers mean higher priority'
)
scene.flamenco_do_override_output_path = BoolProperty(
name='Override Output Path for this Job',
description='When enabled, allows you to specify a non-default Output path '
'for this particular job',
default=False,
update=flamenco_do_override_output_path_updated
)
scene.flamenco_override_output_path = StringProperty(
name='Override Output Path',
description='Path where to store output files, should be accessible for Workers',
subtype='DIR_PATH',
default='')
bpy.types.WindowManager.flamenco_status = EnumProperty(
items=[
('IDLE', 'IDLE', 'Not doing anything.'),
@@ -742,6 +821,14 @@ def unregister():
del bpy.types.Scene.flamenco_render_job_priority
except AttributeError:
pass
try:
del bpy.types.Scene.flamenco_do_override_output_path
except AttributeError:
pass
try:
del bpy.types.Scene.flamenco_override_output_path
except AttributeError:
pass
try:
del bpy.types.WindowManager.flamenco_status
except AttributeError:

View File

@@ -112,7 +112,11 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
async def async_execute(self, context):
"""Entry point of the asynchronous operator."""
self.report({'INFO'}, 'Communicating with Blender Cloud')
# We don't want to influence what is included in the screen shot.
if self.target == 'SCREENSHOT':
print('Blender Cloud add-on is communicating with Blender Cloud')
else:
self.report({'INFO'}, 'Communicating with Blender Cloud')
try:
# Refresh credentials
@@ -120,9 +124,8 @@ class PILLAR_OT_image_share(pillar.PillarOperatorMixin,
db_user = await self.check_credentials(context, REQUIRES_ROLES_FOR_IMAGE_SHARING)
self.user_id = db_user['_id']
self.log.debug('Found user ID: %s', self.user_id)
except pillar.NotSubscribedToCloudError:
self.log.exception('User not subscribed to cloud.')
self.report({'ERROR'}, 'Please subscribe to the Blender Cloud.')
except pillar.NotSubscribedToCloudError as ex:
self._log_subscription_needed(can_renew=ex.can_renew)
self._state = 'QUIT'
return
except pillar.UserNotLoggedInError:

View File

@@ -62,7 +62,16 @@ class CredentialsNotSyncedError(UserNotLoggedInError):
class NotSubscribedToCloudError(UserNotLoggedInError):
"""Raised when the user may be logged in on Blender ID, but has no Blender Cloud token."""
"""Raised when the user does not have an active Cloud subscription.
:ivar can_renew: True when the user has an inactive subscription that can be renewed,
or False when the user has no subscription at all.
"""
def __init__(self, can_renew: bool):
super().__init__()
self.can_renew = can_renew
log.warning('Not subscribed to cloud, can_renew=%s', can_renew)
class PillarError(RuntimeError):
@@ -273,14 +282,15 @@ async def check_pillar_credentials(required_roles: set):
except (pillarsdk.UnauthorizedAccess, pillarsdk.ResourceNotFound, pillarsdk.ForbiddenAccess):
raise CredentialsNotSyncedError()
roles = db_user.roles or set()
log.debug('User has roles %r', roles)
if required_roles and not required_roles.intersection(set(roles)):
roles = set(db_user.roles or set())
log.getChild('check_pillar_credentials').debug('user has roles %r', roles)
if required_roles and not required_roles.intersection(roles):
# Delete the subclient info. This forces a re-check later, which can
# then pick up on the user's new status.
del profile.subclients[SUBCLIENT_ID]
profile.save_json()
raise NotSubscribedToCloudError()
raise NotSubscribedToCloudError(can_renew='has_subscription' in roles)
return db_user
@@ -834,7 +844,6 @@ class PillarOperatorMixin:
try:
db_user = await check_pillar_credentials(required_roles)
except NotSubscribedToCloudError:
self._log_subscription_needed()
raise
except CredentialsNotSyncedError:
self.log.info('Credentials not synced, re-syncing automatically.')
@@ -845,7 +854,6 @@ class PillarOperatorMixin:
try:
db_user = await refresh_pillar_credentials(required_roles)
except NotSubscribedToCloudError:
self._log_subscription_needed()
raise
except CredentialsNotSyncedError:
self.log.info('Credentials not synced after refreshing, handling as not logged in.')
@@ -857,11 +865,13 @@ class PillarOperatorMixin:
self.log.info('Credentials refreshed and ok.')
return db_user
def _log_subscription_needed(self):
self.log.warning(
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
self.report({'INFO'},
'Please subscribe to the blender cloud at https://cloud.blender.org/join')
def _log_subscription_needed(self, *, can_renew: bool, level='ERROR'):
if can_renew:
msg = 'Please renew your Blender Cloud subscription at https://cloud.blender.org/renew'
else:
msg = 'Please subscribe to the blender cloud at https://cloud.blender.org/join'
self.log.warning(msg)
self.report({level}, msg)
class AuthenticatedPillarOperatorMixin(PillarOperatorMixin):

View File

@@ -0,0 +1,101 @@
"""Handle saving and loading project-specific settings."""
import logging
# Names of BlenderCloudPreferences properties that are both project-specific
# and simple enough to store directly in a dict.
PROJECT_SPECIFIC_SIMPLE_PROPS = (
'cloud_project_local_path',
'flamenco_exclude_filter',
'flamenco_job_file_path',
'flamenco_job_output_path',
'flamenco_job_output_strip_components'
)
log = logging.getLogger(__name__)
project_settings_loading = False
def handle_project_update(_=None, _2=None):
"""Handles changing projects, which may cause extensions to be disabled/enabled.
Ignores arguments so that it can be used as property update callback.
"""
from .blender import preferences, project_extensions
global project_settings_loading
project_settings_loading = True
try:
prefs = preferences()
project_id = prefs.project.project
log.info('Updating internal state to reflect extensions enabled on current project %s.',
project_id)
project_extensions.cache_clear()
from blender_cloud import attract, flamenco
attract.deactivate()
flamenco.deactivate()
enabled_for = project_extensions(project_id)
log.info('Project extensions: %s', enabled_for)
if 'attract' in enabled_for:
attract.activate()
if 'flamenco' in enabled_for:
flamenco.activate()
# Load project-specific settings from the last time we visited this project.
ps = prefs.get('project_settings', {}).get(project_id, {})
if not ps:
log.debug('no project-specific settings are available, not touching options')
return
if log.isEnabledFor(logging.DEBUG):
from pprint import pformat
log.debug('loading project-specific settings:\n%s', pformat(ps.to_dict()))
for name in PROJECT_SPECIFIC_SIMPLE_PROPS:
if name in ps and hasattr(prefs, name):
setattr(prefs, name, ps[name])
if ps.get('flamenco_manager'):
prefs.flamenco_manager.manager = ps['flamenco_manager']
log.debug('setting flamenco manager to %s', ps['flamenco_manager'])
finally:
project_settings_loading = False
def store(_=None, _2=None):
"""Remember project-specific settings as soon as one of them changes.
Ignores arguments so that it can be used as property update callback.
No-op when project_settings_loading=True, to prevent saving project-
specific settings while they are actually being loaded.
"""
from .blender import preferences
global project_settings_loading
if project_settings_loading:
return
prefs = preferences()
project_id = prefs.project.project
all_settings = prefs.get('project_settings', {})
ps = all_settings.get(project_id, {})
for name in PROJECT_SPECIFIC_SIMPLE_PROPS:
ps[name] = getattr(prefs, name)
ps['flamenco_manager'] = prefs.flamenco_manager.manager
if log.isEnabledFor(logging.DEBUG):
from pprint import pformat
if hasattr(ps, 'to_dict'):
ps_to_log = ps.to_dict()
else:
ps_to_log = ps
log.debug('saving project-specific settings:\n%s', pformat(ps_to_log))
all_settings[project_id] = ps
prefs['project_settings'] = all_settings

View File

@@ -284,9 +284,8 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin,
db_user = await self.check_credentials(context, REQUIRES_ROLES_FOR_SYNC)
self.user_id = db_user['_id']
log.debug('Found user ID: %s', self.user_id)
except pillar.NotSubscribedToCloudError:
self.log.exception('User not subscribed to cloud.')
self.bss_report({'SUBSCRIBE'}, 'Please subscribe to the Blender Cloud.')
except pillar.NotSubscribedToCloudError as ex:
self._log_subscription_needed(can_renew=ex.can_renew)
self._state = 'QUIT'
return
except pillar.UserNotLoggedInError:

View File

@@ -76,8 +76,8 @@ class MenuItem:
icon_margin_y = 4
text_margin_x = 6
text_height = 16
text_width = 72
text_size = 12
text_size_small = 10
DEFAULT_ICONS = {
'FOLDER': os.path.join(library_icons_path, 'folder.png'),
@@ -214,12 +214,32 @@ class MenuItem:
# draw some text
font_id = 0
blf.position(font_id,
self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x,
self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_height, 0)
blf.size(font_id, self.text_height, self.text_width)
text_dpi = bpy.context.user_preferences.system.dpi
text_x = self.x + self.icon_margin_x + ICON_WIDTH + self.text_margin_x
text_y = self.y + ICON_HEIGHT * 0.5 - 0.25 * self.text_size
blf.position(font_id, text_x, text_y, 0)
blf.size(font_id, self.text_size, text_dpi)
blf.draw(font_id, self.label_text)
# Draw the components of the texture (i.e. which map types are available)
try:
node_files = self.node.properties.files
except AttributeError:
# Happens for nodes that don't have .properties.files.
node_files = None
if not node_files:
return
map_types = {f.map_type for f in node_files if f.map_type}
map_types.discard('color') # all textures have colour
if not map_types:
return
bgl.glColor4f(1.0, 1.0, 1.0, 0.5)
blf.size(font_id, self.text_size_small, text_dpi)
blf.position(font_id, text_x, self.y + 0.5 * self.text_size_small, 0)
blf.draw(font_id, ', '.join(sorted(map_types)))
def hits(self, mouse_x: int, mouse_y: int) -> bool:
return self.x < mouse_x < self.x + self.width and self.y < mouse_y < self.y + self.height
@@ -311,8 +331,8 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
self.mouse_y = event.mouse_y
left_mouse_release = event.type == 'LEFTMOUSE' and event.value == 'RELEASE'
if self._state == 'PLEASE_SUBSCRIBE' and left_mouse_release:
self.open_browser_subscribe()
if left_mouse_release and self._state in {'PLEASE_SUBSCRIBE', 'PLEASE_RENEW'}:
self.open_browser_subscribe(renew=self._state == 'PLEASE_RENEW')
self._finish(context)
return {'FINISHED'}
@@ -365,9 +385,9 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
try:
db_user = await self.check_credentials(context, REQUIRED_ROLES_FOR_TEXTURE_BROWSER)
except pillar.NotSubscribedToCloudError:
self.log.info('User not subscribed to Blender Cloud.')
self._show_subscribe_screen()
except pillar.NotSubscribedToCloudError as ex:
self._log_subscription_needed(can_renew=ex.can_renew, level='INFO')
self._show_subscribe_screen(can_renew=ex.can_renew)
return None
if db_user is None:
@@ -375,10 +395,14 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
await self.async_download_previews()
def _show_subscribe_screen(self):
def _show_subscribe_screen(self, *, can_renew: bool):
"""Shows the "You need to subscribe" screen."""
self._state = 'PLEASE_SUBSCRIBE'
if can_renew:
self._state = 'PLEASE_RENEW'
else:
self._state = 'PLEASE_SUBSCRIBE'
bpy.context.window.cursor_set('HAND')
def descend_node(self, menu_item: MenuItem):
@@ -548,6 +572,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
def browse_assets(self):
self.log.debug('Browsing assets at %r', self.current_path)
bpy.context.window_manager.last_blender_cloud_location = str(self.current_path)
self._new_async_task(self.async_download_previews())
def draw_menu(self, context):
@@ -560,6 +585,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
'DOWNLOADING_TEXTURE': self._draw_downloading,
'EXCEPTION': self._draw_exception,
'PLEASE_SUBSCRIBE': self._draw_subscribe,
'PLEASE_RENEW': self._draw_renew,
}
if self._state in drawers:
@@ -727,6 +753,11 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
'Click to subscribe to the Blender Cloud',
(0.0, 0.0, 0.2, 0.6))
def _draw_renew(self, context):
self._draw_text_on_colour(context,
'Click to renew your Blender Cloud subscription',
(0.0, 0.0, 0.2, 0.6))
def get_clicked(self) -> MenuItem:
for item in self.current_display_content:
@@ -807,11 +838,11 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin,
future=signalling_future))
self.async_task.add_done_callback(texture_download_completed)
def open_browser_subscribe(self):
def open_browser_subscribe(self, *, renew: bool):
import webbrowser
webbrowser.open_new_tab('https://cloud.blender.org/join')
url = 'renew' if renew else 'join'
webbrowser.open_new_tab('https://cloud.blender.org/%s' % url)
self.report({'INFO'}, 'We just started a browser for you.')
def _scroll_smooth(self):
@@ -866,9 +897,8 @@ class PILLAR_OT_switch_hdri(pillar.PillarOperatorMixin,
try:
db_user = await self.check_credentials(context, REQUIRED_ROLES_FOR_TEXTURE_BROWSER)
user_id = db_user['_id']
except pillar.NotSubscribedToCloudError:
self.log.exception('User not subscribed to cloud.')
self.report({'ERROR'}, 'Please subscribe to the Blender Cloud.')
except pillar.NotSubscribedToCloudError as ex:
self._log_subscription_needed(can_renew=ex.can_renew)
self._state = 'QUIT'
return
except pillar.UserNotLoggedInError:

View File

@@ -6,11 +6,12 @@ wheel==0.29.0
blender-bam==1.1.7
# Secondary requirements:
cffi==1.6.0
cryptography==1.3.1
idna==2.1
asn1crypto==0.24.0
cffi==1.11.2
cryptography==2.1.4
idna==2.6
pyasn1==0.1.9
pycparser==2.14
pyOpenSSL==16.0.0
pycparser==2.18
pyOpenSSL==17.5.0
requests==2.10.0
six==1.10.0
six==1.11.0

View File

@@ -232,7 +232,7 @@ setup(
'wheels': BuildWheels},
name='blender_cloud',
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.',
version='1.7.3',
version='1.8.0',
author='Sybren A. Stüvel',
author_email='sybren@stuvel.eu',
packages=find_packages('.'),

View File

@@ -83,8 +83,13 @@ class PathReplacementTest(unittest.TestCase):
def _do_test(self, test_paths, platform, pathclass):
self.test_manager.PurePlatformPath = pathclass
with unittest.mock.patch('sys.platform', platform):
def mocked_system():
return platform
with unittest.mock.patch('platform.system', mocked_system):
for expected_result, input_path in test_paths:
as_path_instance = pathclass(input_path)
self.assertEqual(expected_result,
self.test_manager.replace_path(pathclass(input_path)),
'for input %s on platform %s' % (input_path, platform))
self.test_manager.replace_path(as_path_instance),
'for input %r on platform %s' % (as_path_instance, platform))