# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # # Old info, kept here for reference, so that we can merge wiki pages, # descriptions, etc. # # bl_info = { # "name": "Attract", # "author": "Francesco Siddi, InĂªs Almeida, Antony Riakiotakis", # "version": (0, 2, 0), # "blender": (2, 76, 0), # "location": "Video Sequence Editor", # "description": # "Blender integration with the Attract task tracking service" # ". *requires the Blender ID add-on", # "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" # "Scripts/Workflow/Attract", # "category": "Workflow", # "support": "TESTING" # } if "bpy" in locals(): import importlib importlib.reload(draw) else: from . import draw import bpy from pillarsdk.api import Api from pillarsdk.nodes import Node from pillarsdk.projects import Project from pillarsdk import utils from pillarsdk.exceptions import ResourceNotFound from bpy.props import StringProperty from bpy.types import Operator, Panel, AddonPreferences def active_strip(context): try: return context.scene.sequence_editor.active_strip except AttributeError: return None def remove_atc_props(strip): """Resets the attract custom properties assigned to a VSE strip""" strip.atc_cut_in = 0 strip.atc_cut_out = 0 strip.atc_name = "" strip.atc_description = "" strip.atc_object_id = "" strip.atc_is_synced = False class ToolsPanel(Panel): bl_label = 'Attract' bl_space_type = 'SEQUENCE_EDITOR' bl_region_type = 'UI' def draw_header(self, context): strip = active_strip(context) if strip and strip.atc_object_id: self.layout.prop(strip, 'atc_is_synced', text='') def draw(self, context): strip = active_strip(context) layout = self.layout strip_types = {'MOVIE', 'IMAGE'} if strip and strip.atc_object_id and strip.type in strip_types: layout.prop(strip, 'atc_name', text='Name') layout.prop(strip, 'atc_description', text='Description') layout.prop(strip, 'atc_notes', text='Notes') layout.prop(strip, 'atc_status', text='Status') layout.prop(strip, 'atc_cut_in', text='Cut in') # layout.prop(strip, 'atc_cut_out', text='Cut out') if strip.atc_is_synced: layout.operator('attract.shot_submit_update') layout.operator('attract.shot_delete') layout.operator('attract.strip_unlink') elif strip and strip.type in strip_types: layout.operator('attract.shot_submit_new') layout.operator('attract.shot_relink') else: layout.label(text='Select a Movie or Image strip') layout.operator('attract.shots_order_update') class AttractOperatorMixin: """Mix-in class for all Attract operators.""" def _project_needs_setup_error(self): self.report({'ERROR'}, 'Your Blender Cloud project is not set up for Attract.') return {'CANCELLED'} def find_node_type(self, node_type_name: str) -> dict: from .. import pillar, blender prefs = blender.preferences() project = pillar.call(Project.find_one, { 'where': {'_id': prefs.project_uuid}, 'projection': {'node_types': {'$elemMatch': {'name': node_type_name}}} }) # FIXME: Eve doesn't seem to handle the $elemMatch projection properly, # even though it works fine in MongoDB itself. As a result, we have to # search for the node type. node_type_list = project['node_types'] node_type = next((nt for nt in node_type_list if nt['name'] == node_type_name), None) if not node_type: return self._project_needs_setup_error() return node_type class AttractShotSubmitNew(AttractOperatorMixin, Operator): bl_idname = "attract.shot_submit_new" bl_label = "Submit to Attract" @classmethod def poll(cls, context): strip = active_strip(context) return not strip.atc_object_id def execute(self, context): from .. import pillar, blender import blender_id strip = active_strip(context) if strip.atc_object_id: return node_type = self.find_node_type('shot') if isinstance(node_type, set): # in case of error return node_type # Define the shot properties prop = {'name': strip.name, 'description': '', 'properties': {'status': 'on_hold', 'notes': '', 'cut_in': strip.frame_offset_start, 'cut_out': strip.frame_offset_start + strip.frame_final_duration}, 'order': 0, 'node_type': 'shot', 'project': blender.preferences().project_uuid, 'user': blender_id.get_active_user_id()} # Create a Node item with the attract API node = Node(prop) post = pillar.call(node.create) # Populate the strip with the freshly generated ObjectID and info if not post: self.report({'ERROR'}, 'Error creating node! Check the console for now.') return {'CANCELLED'} strip.atc_object_id = node['_id'] strip.atc_is_synced = True strip.atc_name = node['name'] strip.atc_cut_in = node['properties']['cut_in'] strip.atc_cut_out = node['properties']['cut_out'] return {'FINISHED'} class AttractShotRelink(AttractOperatorMixin, Operator): bl_idname = "attract.shot_relink" bl_label = "Relink to Attract" strip_atc_object_id = bpy.props.StringProperty() def execute(self, context): from .. import pillar strip = active_strip(context) try: node = pillar.call(Node.find, self.strip_atc_object_id) except ResourceNotFound: self.report({'ERROR'}, 'Shot %r not found on the server, unable to relink.' % self.strip_atc_object_id) return {'CANCELLED'} strip.atc_object_id = self.strip_atc_object_id strip.atc_is_synced = True strip.atc_name = node.name strip.atc_cut_in = node.properties.cut_in strip.atc_cut_out = node.properties.cut_out strip.atc_description = node.description self.report({'INFO'}, "Shot {0} relinked".format(node.name)) return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def draw(self, context): layout = self.layout col = layout.column() col.prop(self, 'strip_atc_object_id', text='Shot ID') class AttractShotSubmitUpdate(AttractOperatorMixin, Operator): bl_idname = 'attract.shot_submit_update' bl_label = 'Update' bl_description = 'Syncronizes local and remote changes' def execute(self, context): strip = active_strip(context) # Update cut_in and cut_out properties on the strip # strip.atc_cut_in = strip.frame_offset_start # strip.atc_cut_out = strip.frame_offset_start + strip.frame_final_duration # print("Query Attract server with {0}".format(strip.atc_object_id)) strip.atc_cut_out = strip.atc_cut_in + strip.frame_final_duration - 1 node = Node.find(strip.atc_object_id) node.name = strip.atc_name node.description = strip.atc_description node.properties.cut_in = strip.atc_cut_in node.properties.cut_out = strip.atc_cut_out node.update() return {'FINISHED'} class AttractShotDelete(AttractOperatorMixin, Operator): bl_idname = 'attract.shot_delete' bl_label = 'Delete' bl_description = 'Remove from Attract' def execute(self, context): strip = active_strip(context) node = Node.find(strip.atc_object_id) if node.delete(): remove_atc_props(strip) return {'FINISHED'} class AttractStripUnlink(AttractOperatorMixin, Operator): bl_idname = 'attract.strip_unlink' bl_label = 'Unlink' bl_description = 'Remove Attract props from the strip' def execute(self, context): strip = active_strip(context) remove_atc_props(strip) return {'FINISHED'} class AttractShotsOrderUpdate(AttractOperatorMixin, Operator): bl_idname = 'attract.shots_order_update' bl_label = 'Update shots order' def execute(self, context): from .. import pillar # Get all shot nodes from server, build dictionary using ObjectID # as indexes node_type = self.find_node_type('shot') if isinstance(node_type, set): # in case of error return node_type shots = pillar.call(Node.all, { 'where': {'node_type': node_type._id}, 'max_results': 100}) shots = shots._items # TODO (fsiddi) take into account pagination. Currently we do not do it # and it makes this dict useless. # We should use the pagination info from the node_type_list query and # keep querying until we have all the items. shots_dict = {} for shot in shots: shots_dict[shot._id] = shot # Build ordered list of strips from the edit. strips_with_atc_object_id = [strip for strip in context.scene.sequence_editor.sequences_all if strip.atc_object_id] strips_with_atc_object_id.sort( key=lambda strip: strip.frame_start + strip.frame_offset_start) index = 1 for strip in strips_with_atc_object_id: """ # Currently we use the code below to force update all nodes. # Check that the shot is in the list of retrieved shots if strip.atc_order != index: #or shots_dict[strip.atc_object_id]['order'] != index: # If there is an update in the order, retrieve and update # the node, as well as the VSE strip # shot_node = Node.find(strip.atc_object_id) # shot_node.order = index # shot_node.update() # strip.atc_order = index print ("{0} > {1}".format(strip.atc_order, index)) """ # We get all nodes one by one. This is bad and stupid. try: shot_node = pillar.call(Node.find, strip.atc_object_id) # if shot_node.properties.order != index: shot_node.order = index shot_node.update() print('{0} - updating {1}'.format(shot_node.order, shot_node.name)) strip.atc_order = index index += 1 except ResourceNotFound: # Reset the attract properties for any shot not found on the server # print("Error: shot {0} not found".format(strip.atc_object_id)) remove_atc_props(strip) return {'FINISHED'} def register(): bpy.types.Sequence.atc_is_synced = bpy.props.BoolProperty(name="Is synced") bpy.types.Sequence.atc_object_id = bpy.props.StringProperty(name="Attract Object ID") bpy.types.Sequence.atc_name = bpy.props.StringProperty(name="Shot Name") bpy.types.Sequence.atc_description = bpy.props.StringProperty(name="Shot description") bpy.types.Sequence.atc_notes = bpy.props.StringProperty(name="Shot notes") bpy.types.Sequence.atc_cut_in = bpy.props.IntProperty(name="Cut in") bpy.types.Sequence.atc_cut_out = bpy.props.IntProperty(name="Cut out") bpy.types.Sequence.atc_status = bpy.props.EnumProperty( items=[ ('on_hold', 'On hold', 'The shot is on hold'), ('todo', 'Todo', 'Waiting'), ('in_progress', 'In progress', 'The show has been assigned')], name="Status") bpy.types.Sequence.atc_order = bpy.props.IntProperty(name="Order") bpy.utils.register_class(ToolsPanel) bpy.utils.register_class(AttractShotSubmitNew) bpy.utils.register_class(AttractShotRelink) bpy.utils.register_class(AttractShotSubmitUpdate) bpy.utils.register_class(AttractShotDelete) bpy.utils.register_class(AttractStripUnlink) bpy.utils.register_class(AttractShotsOrderUpdate) draw.callback_enable() def unregister(): draw.callback_disable() del bpy.types.Sequence.atc_is_synced del bpy.types.Sequence.atc_object_id del bpy.types.Sequence.atc_name del bpy.types.Sequence.atc_description del bpy.types.Sequence.atc_notes del bpy.types.Sequence.atc_cut_in del bpy.types.Sequence.atc_cut_out del bpy.types.Sequence.atc_status del bpy.types.Sequence.atc_order bpy.utils.unregister_module(__name__)