diff --git a/blender_cloud/blender.py b/blender_cloud/blender.py index 634374a..ff369bf 100644 --- a/blender_cloud/blender.py +++ b/blender_cloud/blender.py @@ -52,6 +52,7 @@ class SyncStatusProperties(PropertyGroup): ('INFO', 'INFO', ''), ('WARNING', 'WARNING', ''), ('ERROR', 'ERROR', ''), + ('SUBSCRIBE', 'SUBSCRIBE', ''), ], name='level', update=redraw) @@ -64,7 +65,11 @@ class SyncStatusProperties(PropertyGroup): # Message can also be empty, just to erase it from the GUI. # No need to actually log those. if message: - log.log(logging._nameToLevel[self.level], message) + try: + loglevel = logging._nameToLevel[self.level] + except KeyError: + loglevel = logging.WARNING + log.log(loglevel, message) # List of syncable versions is stored in 'available_blender_versions' ID property, # because I don't know how to store a variable list of strings in a proper RNA property. @@ -157,15 +162,25 @@ class BlenderCloudPreferences(AddonPreferences): 'INFO': 'NONE', 'WARNING': 'INFO', 'ERROR': 'ERROR', + 'SUBSCRIBE': 'ERROR', } message_container = row.row() message_container.label(bss.message, icon=icon_for_level[bss.level]) - message_container.alert = True # bss.level in {'WARNING', 'ERROR'} sub = bsync_box.column() - sub.enabled = bss.status in {'NONE', 'IDLE'} - buttons = sub.column() + if bss.level == 'SUBSCRIBE': + self.draw_subscribe_button(sub) + else: + self.draw_sync_buttons(sub, bss) + + def draw_subscribe_button(self, layout): + layout.operator('pillar.subscribe', icon='WORLD') + + def draw_sync_buttons(self, layout, bss): + layout.enabled = bss.status in {'NONE', 'IDLE'} + + buttons = layout.column() row_buttons = buttons.row().split(percentage=0.5) row_pull = row_buttons.row(align=True) row_push = row_buttons.row() @@ -194,7 +209,8 @@ class BlenderCloudPreferences(AddonPreferences): row_pull.label('Cloud Sync is running.') -class PillarCredentialsUpdate(Operator): +class PillarCredentialsUpdate(pillar.PillarOperatorMixin, + Operator): """Updates the Pillar URL and tests the new URL.""" bl_idname = 'pillar.credentials_update' bl_label = 'Update credentials' @@ -224,7 +240,7 @@ class PillarCredentialsUpdate(Operator): try: loop = asyncio.get_event_loop() - loop.run_until_complete(pillar.refresh_pillar_credentials()) + loop.run_until_complete(self.check_credentials(context, set())) except blender_id.BlenderIdCommError as ex: log.exception('Error sending subclient-specific token to Blender ID') self.report({'ERROR'}, 'Failed to sync Blender ID to Blender Cloud') @@ -238,6 +254,20 @@ class PillarCredentialsUpdate(Operator): return {'FINISHED'} +class PILLAR_OT_subscribe(Operator): + """Opens a browser to subscribe the user to the Cloud.""" + bl_idname = 'pillar.subscribe' + bl_label = 'Subscribe to the Cloud' + + def execute(self, context): + import webbrowser + + webbrowser.open_new_tab('https://cloud.blender.org/join') + self.report({'INFO'}, 'We just started a browser for you.') + + return {'FINISHED'} + + def preferences() -> BlenderCloudPreferences: return bpy.context.user_preferences.addons[ADDON_NAME].preferences @@ -246,6 +276,7 @@ def register(): bpy.utils.register_class(BlenderCloudPreferences) bpy.utils.register_class(PillarCredentialsUpdate) bpy.utils.register_class(SyncStatusProperties) + bpy.utils.register_class(PILLAR_OT_subscribe) addon_prefs = preferences() @@ -274,6 +305,7 @@ def unregister(): bpy.utils.unregister_class(PillarCredentialsUpdate) bpy.utils.unregister_class(BlenderCloudPreferences) bpy.utils.unregister_class(SyncStatusProperties) + bpy.utils.unregister_class(PILLAR_OT_subscribe) del WindowManager.last_blender_cloud_location del WindowManager.blender_sync_status diff --git a/blender_cloud/gui.py b/blender_cloud/gui.py index 77bd178..c75104d 100644 --- a/blender_cloud/gui.py +++ b/blender_cloud/gui.py @@ -30,6 +30,8 @@ import os import pillarsdk from . import async_loop, pillar, cache +REQUIRED_ROLES_FOR_TEXTURE_BROWSER = {'subscriber', 'demo'} + icon_width = 128 icon_height = 128 target_item_width = 400 @@ -320,7 +322,7 @@ class BlenderCloudBrowser(pillar.PillarOperatorMixin, self.log.debug('Checking credentials') try: - user_id = await self.check_credentials(context) + user_id = 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() diff --git a/blender_cloud/pillar.py b/blender_cloud/pillar.py index 814bca8..6268055 100644 --- a/blender_cloud/pillar.py +++ b/blender_cloud/pillar.py @@ -177,9 +177,10 @@ async def pillar_call(pillar_func, *args, caching=True, **kwargs): return await loop.run_in_executor(None, partial) -async def check_pillar_credentials(): +async def check_pillar_credentials(required_roles: set): """Tries to obtain the user at Pillar using the user's credentials. + :param required_roles: set of roles to require -- having one of those is enough. :raises UserNotLoggedInError: when the user is not logged in on Blender ID. :raises CredentialsNotSyncedError: when the user is logged in on Blender ID but doesn't have a valid subclient token for Pillar. @@ -203,9 +204,9 @@ async def check_pillar_credentials(): except (pillarsdk.UnauthorizedAccess, pillarsdk.ResourceNotFound, pillarsdk.ForbiddenAccess): raise CredentialsNotSyncedError() - roles = db_user.roles + roles = db_user.roles or set() log.debug('User has roles %r', roles) - if not roles or not {'subscriber', 'demo'}.intersection(set(roles)): + if required_roles and not required_roles.intersection(set(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] @@ -215,7 +216,7 @@ async def check_pillar_credentials(): return pillar_user_id -async def refresh_pillar_credentials(): +async def refresh_pillar_credentials(required_roles: set): """Refreshes the authentication token on Pillar. :raises blender_id.BlenderIdCommError: when Blender ID refuses to send a token to Pillar. @@ -239,7 +240,7 @@ async def refresh_pillar_credentials(): # Test the new URL _pillar_api = None - return await check_pillar_credentials() + return await check_pillar_credentials(required_roles) async def get_project_uuid(project_url: str) -> str: @@ -259,7 +260,7 @@ async def get_project_uuid(project_url: str) -> str: async def get_nodes(project_uuid: str = None, parent_node_uuid: str = None, - node_type = None) -> list: + node_type=None) -> list: """Gets nodes for either a project or given a parent node. @param project_uuid: the UUID of the project, or None if only querying by parent_node_uuid. @@ -662,8 +663,7 @@ def is_cancelled(future: asyncio.Future) -> bool: class PillarOperatorMixin: - - async def check_credentials(self, context) -> bool: + async def check_credentials(self, context, required_roles) -> bool: """Checks credentials with Pillar, and if ok returns the user ID. Returns None if the user cannot be found, or if the user is not a Cloud subscriber. @@ -672,7 +672,7 @@ class PillarOperatorMixin: # self.report({'INFO'}, 'Checking Blender Cloud credentials') try: - user_id = await check_pillar_credentials() + user_id = await check_pillar_credentials(required_roles) except NotSubscribedToCloudError: self._log_subscription_needed() raise @@ -683,7 +683,7 @@ class PillarOperatorMixin: return user_id try: - user_id = await refresh_pillar_credentials() + user_id = await refresh_pillar_credentials(required_roles) except NotSubscribedToCloudError: self._log_subscription_needed() raise diff --git a/blender_cloud/settings_sync.py b/blender_cloud/settings_sync.py index fbb12af..6e56035 100644 --- a/blender_cloud/settings_sync.py +++ b/blender_cloud/settings_sync.py @@ -30,6 +30,7 @@ LOCAL_SETTINGS_RNA = [ (b'tempdir', 'filepaths.temporary_directory'), ] +REQUIRES_ROLES_FOR_SYNC = {'subscriber', 'demo'} HOME_PROJECT_ENDPOINT = '/bcloud/home-project' SYNC_GROUP_NODE_NAME = 'Blender Sync' SYNC_GROUP_NODE_DESC = 'The [Blender Cloud Addon](https://cloud.blender.org/services' \ @@ -354,9 +355,22 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, self.log.info('Performing action %s', action) try: - self.user_id = await self.check_credentials(context) - log.debug('Found user ID: %s', self.user_id) + # Refresh credentials + try: + self.user_id = await self.check_credentials(context, REQUIRES_ROLES_FOR_SYNC) + 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.') + self._state = 'QUIT' + return + except pillar.CredentialsNotSyncedError: + self.log.exception('Error checking/refreshing credentials.') + self.bss_report({'ERROR'}, 'Please log in on Blender ID first.') + self._state = 'QUIT' + return + # Find the home project. try: self.home_project_id = await get_home_project_id() except sdk_exceptions.ForbiddenAccess: @@ -390,9 +404,6 @@ class PILLAR_OT_sync(pillar.PillarOperatorMixin, 'REFRESH': self.action_refresh, }[action] await action_method(context) - except pillar.CredentialsNotSyncedError: - self.log.exception('Error checking/refreshing credentials.') - self.bss_report({'ERROR'}, 'Please log in on Blender ID first.') except Exception as ex: self.log.exception('Unexpected exception caught.') self.bss_report({'ERROR'}, 'Unexpected error: %s' % ex)