Merge branch 'develop' into feature/3.0_tvpaint_asset_name_validation
266
CHANGELOG.md
|
|
@ -1,18 +1,264 @@
|
|||
# Changelog
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-24)
|
||||
## [2.16.1](https://github.com/pypeclub/pype/tree/2.16.1) (2021-04-13)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.16.0...2.16.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: comp renders mix up [\#1301](https://github.com/pypeclub/pype/pull/1301)
|
||||
- Validate project settings [\#1297](https://github.com/pypeclub/pype/pull/1297)
|
||||
- After Effects: added SubsetManager [\#1234](https://github.com/pypeclub/pype/pull/1234)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack custom attributes in bulks [\#1312](https://github.com/pypeclub/pype/pull/1312)
|
||||
- Ftrack optional pypclub role [\#1303](https://github.com/pypeclub/pype/pull/1303)
|
||||
- AE remove orphaned instance from workfile - fix self.stub [\#1282](https://github.com/pypeclub/pype/pull/1282)
|
||||
- Avalon schema names [\#1242](https://github.com/pypeclub/pype/pull/1242)
|
||||
- Handle duplication of Task name [\#1226](https://github.com/pypeclub/pype/pull/1226)
|
||||
- Modified path of plugin loads for Harmony and TVPaint [\#1217](https://github.com/pypeclub/pype/pull/1217)
|
||||
- Regex checks in profiles filtering [\#1214](https://github.com/pypeclub/pype/pull/1214)
|
||||
- Bulk mov strict task [\#1204](https://github.com/pypeclub/pype/pull/1204)
|
||||
- Update custom ftrack session attributes [\#1202](https://github.com/pypeclub/pype/pull/1202)
|
||||
- Nuke: write node colorspace ignore `default\(\)` label [\#1199](https://github.com/pypeclub/pype/pull/1199)
|
||||
- Nuke: reverse search to make it more versatile [\#1178](https://github.com/pypeclub/pype/pull/1178)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Forward compatible ftrack group [\#1243](https://github.com/pypeclub/pype/pull/1243)
|
||||
- Error message in pyblish UI [\#1206](https://github.com/pypeclub/pype/pull/1206)
|
||||
- Nuke: deadline submission with search replaced env values from preset [\#1194](https://github.com/pypeclub/pype/pull/1194)
|
||||
|
||||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167)
|
||||
- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156)
|
||||
- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152)
|
||||
- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146)
|
||||
- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142)
|
||||
- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138)
|
||||
- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127)
|
||||
- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124)
|
||||
- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117)
|
||||
- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106)
|
||||
- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101)
|
||||
- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093)
|
||||
- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088)
|
||||
- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080)
|
||||
- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078)
|
||||
- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072)
|
||||
- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008)
|
||||
- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073)
|
||||
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174)
|
||||
- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166)
|
||||
- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163)
|
||||
- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125)
|
||||
- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090)
|
||||
- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082)
|
||||
- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067)
|
||||
- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064)
|
||||
|
||||
## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053)
|
||||
- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085)
|
||||
- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059)
|
||||
- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057)
|
||||
- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046)
|
||||
|
||||
## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040)
|
||||
- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036)
|
||||
- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031)
|
||||
|
||||
## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011)
|
||||
- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006)
|
||||
- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922)
|
||||
|
||||
## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932)
|
||||
- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926)
|
||||
- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894)
|
||||
- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993)
|
||||
- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986)
|
||||
- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981)
|
||||
- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965)
|
||||
- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944)
|
||||
- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941)
|
||||
- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938)
|
||||
- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903)
|
||||
- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893)
|
||||
- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891)
|
||||
- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884)
|
||||
- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881)
|
||||
- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840)
|
||||
- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824)
|
||||
- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795)
|
||||
- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995)
|
||||
- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982)
|
||||
- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960)
|
||||
- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839)
|
||||
- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990)
|
||||
- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988)
|
||||
- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984)
|
||||
- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979)
|
||||
- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972)
|
||||
- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971)
|
||||
- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967)
|
||||
- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964)
|
||||
- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959)
|
||||
- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956)
|
||||
- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954)
|
||||
- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953)
|
||||
- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933)
|
||||
- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920)
|
||||
- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909)
|
||||
- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896)
|
||||
- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889)
|
||||
- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831)
|
||||
- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934)
|
||||
|
||||
## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892)
|
||||
- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869)
|
||||
|
||||
## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866)
|
||||
|
||||
## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837)
|
||||
|
||||
## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809)
|
||||
- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807)
|
||||
- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823)
|
||||
|
||||
## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768)
|
||||
- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766)
|
||||
- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781)
|
||||
- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769)
|
||||
|
||||
## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770)
|
||||
- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760)
|
||||
- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754)
|
||||
- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752)
|
||||
- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748)
|
||||
- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744)
|
||||
- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742)
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736)
|
||||
- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721)
|
||||
- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719)
|
||||
- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716)
|
||||
- 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699)
|
||||
- TV Paint: initial implementation of creators and local rendering [\#693](https://github.com/pypeclub/pype/pull/693)
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678)
|
||||
- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675)
|
||||
- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674)
|
||||
|
|
@ -21,25 +267,33 @@
|
|||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740)
|
||||
- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722)
|
||||
- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682)
|
||||
- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717)
|
||||
- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Application manager [\#728](https://github.com/pypeclub/pype/pull/728)
|
||||
- Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706)
|
||||
- Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700)
|
||||
- 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697)
|
||||
|
||||
## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)
|
||||
|
||||
**Merged pull requests:**
|
||||
**Fixed bugs:**
|
||||
|
||||
- fix\(SP\): getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
|
||||
# Changelog
|
||||
|
||||
|
|
|
|||
268
HISTORY.md
|
|
@ -1,3 +1,268 @@
|
|||
## [2.16.0](https://github.com/pypeclub/pype/tree/2.16.0) (2021-03-22)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.3...2.16.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Nuke: deadline submit limit group filter [\#1167](https://github.com/pypeclub/pype/pull/1167)
|
||||
- Maya: support for Deadline Group and Limit Groups - backport 2.x [\#1156](https://github.com/pypeclub/pype/pull/1156)
|
||||
- Maya: fixes for Redshift support [\#1152](https://github.com/pypeclub/pype/pull/1152)
|
||||
- Nuke: adding preset for a Read node name to all img and mov Loaders [\#1146](https://github.com/pypeclub/pype/pull/1146)
|
||||
- nuke deadline submit with environ var from presets overrides [\#1142](https://github.com/pypeclub/pype/pull/1142)
|
||||
- Change timers after task change [\#1138](https://github.com/pypeclub/pype/pull/1138)
|
||||
- Nuke: shortcuts for Pype menu [\#1127](https://github.com/pypeclub/pype/pull/1127)
|
||||
- Nuke: workfile template [\#1124](https://github.com/pypeclub/pype/pull/1124)
|
||||
- Sites local settings by site name [\#1117](https://github.com/pypeclub/pype/pull/1117)
|
||||
- Reset loader's asset selection on context change [\#1106](https://github.com/pypeclub/pype/pull/1106)
|
||||
- Bulk mov render publishing [\#1101](https://github.com/pypeclub/pype/pull/1101)
|
||||
- Photoshop: mark publishable instances [\#1093](https://github.com/pypeclub/pype/pull/1093)
|
||||
- Added ability to define BG color for extract review [\#1088](https://github.com/pypeclub/pype/pull/1088)
|
||||
- TVPaint extractor enhancement [\#1080](https://github.com/pypeclub/pype/pull/1080)
|
||||
- Photoshop: added support for .psb in workfiles [\#1078](https://github.com/pypeclub/pype/pull/1078)
|
||||
- Optionally add task to subset name [\#1072](https://github.com/pypeclub/pype/pull/1072)
|
||||
- Only extend clip range when collecting. [\#1008](https://github.com/pypeclub/pype/pull/1008)
|
||||
- Collect audio for farm reviews. [\#1073](https://github.com/pypeclub/pype/pull/1073)
|
||||
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix path spaces in jpeg extractor [\#1174](https://github.com/pypeclub/pype/pull/1174)
|
||||
- Maya: Bugfix: superclass for CreateCameraRig [\#1166](https://github.com/pypeclub/pype/pull/1166)
|
||||
- Maya: Submit to Deadline - fix typo in condition [\#1163](https://github.com/pypeclub/pype/pull/1163)
|
||||
- Avoid dot in repre extension [\#1125](https://github.com/pypeclub/pype/pull/1125)
|
||||
- Fix versions variable usage in standalone publisher [\#1090](https://github.com/pypeclub/pype/pull/1090)
|
||||
- Collect instance data fix subset query [\#1082](https://github.com/pypeclub/pype/pull/1082)
|
||||
- Fix getting the camera name. [\#1067](https://github.com/pypeclub/pype/pull/1067)
|
||||
- Nuke: Ensure "NUKE\_TEMP\_DIR" is not part of the Deadline job environment. [\#1064](https://github.com/pypeclub/pype/pull/1064)
|
||||
|
||||
## [2.15.3](https://github.com/pypeclub/pype/tree/2.15.3) (2021-02-26)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.2...2.15.3)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: speedup renderable camera collection [\#1053](https://github.com/pypeclub/pype/pull/1053)
|
||||
- Harmony - add regex search to filter allowed task names for collectin… [\#1047](https://github.com/pypeclub/pype/pull/1047)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Ftrack integrate hierarchy fix [\#1085](https://github.com/pypeclub/pype/pull/1085)
|
||||
- Explicit subset filter in anatomy instance data [\#1059](https://github.com/pypeclub/pype/pull/1059)
|
||||
- TVPaint frame offset [\#1057](https://github.com/pypeclub/pype/pull/1057)
|
||||
- Auto fix unicode strings [\#1046](https://github.com/pypeclub/pype/pull/1046)
|
||||
|
||||
## [2.15.2](https://github.com/pypeclub/pype/tree/2.15.2) (2021-02-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.1...2.15.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Maya: Vray scene publishing [\#1013](https://github.com/pypeclub/pype/pull/1013)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Fix entity move under project [\#1040](https://github.com/pypeclub/pype/pull/1040)
|
||||
- smaller nuke fixes from production [\#1036](https://github.com/pypeclub/pype/pull/1036)
|
||||
- TVPaint thumbnail extract fix [\#1031](https://github.com/pypeclub/pype/pull/1031)
|
||||
|
||||
## [2.15.1](https://github.com/pypeclub/pype/tree/2.15.1) (2021-02-12)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.15.0...2.15.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Delete version as loader action [\#1011](https://github.com/pypeclub/pype/pull/1011)
|
||||
- Delete old versions [\#445](https://github.com/pypeclub/pype/pull/445)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- PS - remove obsolete functions from pywin32 [\#1006](https://github.com/pypeclub/pype/pull/1006)
|
||||
- Clone description of review session objects. [\#922](https://github.com/pypeclub/pype/pull/922)
|
||||
|
||||
## [2.15.0](https://github.com/pypeclub/pype/tree/2.15.0) (2021-02-09)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.6...2.15.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Resolve - loading and updating clips [\#932](https://github.com/pypeclub/pype/pull/932)
|
||||
- Release/2.15.0 [\#926](https://github.com/pypeclub/pype/pull/926)
|
||||
- Photoshop: add option for template.psd and prelaunch hook [\#894](https://github.com/pypeclub/pype/pull/894)
|
||||
- Nuke: deadline presets [\#993](https://github.com/pypeclub/pype/pull/993)
|
||||
- Maya: Alembic only set attributes that exists. [\#986](https://github.com/pypeclub/pype/pull/986)
|
||||
- Harmony: render local and handle fixes [\#981](https://github.com/pypeclub/pype/pull/981)
|
||||
- PSD Bulk export of ANIM group [\#965](https://github.com/pypeclub/pype/pull/965)
|
||||
- AE - added prelaunch hook for opening last or workfile from template [\#944](https://github.com/pypeclub/pype/pull/944)
|
||||
- PS - safer handling of loading of workfile [\#941](https://github.com/pypeclub/pype/pull/941)
|
||||
- Maya: Handling Arnold referenced AOVs [\#938](https://github.com/pypeclub/pype/pull/938)
|
||||
- TVPaint: switch layer IDs for layer names during identification [\#903](https://github.com/pypeclub/pype/pull/903)
|
||||
- TVPaint audio/sound loader [\#893](https://github.com/pypeclub/pype/pull/893)
|
||||
- Clone review session with children. [\#891](https://github.com/pypeclub/pype/pull/891)
|
||||
- Simple compositing data packager for freelancers [\#884](https://github.com/pypeclub/pype/pull/884)
|
||||
- Harmony deadline submission [\#881](https://github.com/pypeclub/pype/pull/881)
|
||||
- Maya: Optionally hide image planes from reviews. [\#840](https://github.com/pypeclub/pype/pull/840)
|
||||
- Maya: handle referenced AOVs for Vray [\#824](https://github.com/pypeclub/pype/pull/824)
|
||||
- DWAA/DWAB support on windows [\#795](https://github.com/pypeclub/pype/pull/795)
|
||||
- Unreal: animation, layout and setdress updates [\#695](https://github.com/pypeclub/pype/pull/695)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Maya: Looks - disable hardlinks [\#995](https://github.com/pypeclub/pype/pull/995)
|
||||
- Fix Ftrack custom attribute update [\#982](https://github.com/pypeclub/pype/pull/982)
|
||||
- Prores ks in burnin script [\#960](https://github.com/pypeclub/pype/pull/960)
|
||||
- terminal.py crash on import [\#839](https://github.com/pypeclub/pype/pull/839)
|
||||
- Extract review handle bizarre pixel aspect ratio [\#990](https://github.com/pypeclub/pype/pull/990)
|
||||
- Nuke: add nuke related env var to sumbission [\#988](https://github.com/pypeclub/pype/pull/988)
|
||||
- Nuke: missing preset's variable [\#984](https://github.com/pypeclub/pype/pull/984)
|
||||
- Get creator by name fix [\#979](https://github.com/pypeclub/pype/pull/979)
|
||||
- Fix update of project's tasks on Ftrack sync [\#972](https://github.com/pypeclub/pype/pull/972)
|
||||
- nuke: wrong frame offset in mov loader [\#971](https://github.com/pypeclub/pype/pull/971)
|
||||
- Create project structure action fix multiroot [\#967](https://github.com/pypeclub/pype/pull/967)
|
||||
- PS: remove pywin installation from hook [\#964](https://github.com/pypeclub/pype/pull/964)
|
||||
- Prores ks in burnin script [\#959](https://github.com/pypeclub/pype/pull/959)
|
||||
- Subset family is now stored in subset document [\#956](https://github.com/pypeclub/pype/pull/956)
|
||||
- DJV new version arguments [\#954](https://github.com/pypeclub/pype/pull/954)
|
||||
- TV Paint: Fix single frame Sequence [\#953](https://github.com/pypeclub/pype/pull/953)
|
||||
- nuke: missing `file` knob update [\#933](https://github.com/pypeclub/pype/pull/933)
|
||||
- Photoshop: Create from single layer was failing [\#920](https://github.com/pypeclub/pype/pull/920)
|
||||
- Nuke: baking mov with correct colorspace inherited from write [\#909](https://github.com/pypeclub/pype/pull/909)
|
||||
- Launcher fix actions discover [\#896](https://github.com/pypeclub/pype/pull/896)
|
||||
- Get the correct file path for the updated mov. [\#889](https://github.com/pypeclub/pype/pull/889)
|
||||
- Maya: Deadline submitter - shared data access violation [\#831](https://github.com/pypeclub/pype/pull/831)
|
||||
- Maya: Take into account vray master AOV switch [\#822](https://github.com/pypeclub/pype/pull/822)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Refactor blender to 3.0 format [\#934](https://github.com/pypeclub/pype/pull/934)
|
||||
|
||||
## [2.14.6](https://github.com/pypeclub/pype/tree/2.14.6) (2021-01-15)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.5...2.14.6)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Nuke: improving of hashing path [\#885](https://github.com/pypeclub/pype/pull/885)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Hiero: cut videos with correct secons [\#892](https://github.com/pypeclub/pype/pull/892)
|
||||
- Faster sync to avalon preparation [\#869](https://github.com/pypeclub/pype/pull/869)
|
||||
|
||||
## [2.14.5](https://github.com/pypeclub/pype/tree/2.14.5) (2021-01-06)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.4...2.14.5)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Pype logger refactor [\#866](https://github.com/pypeclub/pype/pull/866)
|
||||
|
||||
## [2.14.4](https://github.com/pypeclub/pype/tree/2.14.4) (2020-12-18)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.3...2.14.4)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Fix - AE - added explicit cast to int [\#837](https://github.com/pypeclub/pype/pull/837)
|
||||
|
||||
## [2.14.3](https://github.com/pypeclub/pype/tree/2.14.3) (2020-12-16)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.2...2.14.3)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- TVPaint repair invalid metadata [\#809](https://github.com/pypeclub/pype/pull/809)
|
||||
- Feature/push hier value to nonhier action [\#807](https://github.com/pypeclub/pype/pull/807)
|
||||
- Harmony: fix palette and image sequence loader [\#806](https://github.com/pypeclub/pype/pull/806)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- respecting space in path [\#823](https://github.com/pypeclub/pype/pull/823)
|
||||
|
||||
## [2.14.2](https://github.com/pypeclub/pype/tree/2.14.2) (2020-12-04)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.1...2.14.2)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Collapsible wrapper in settings [\#767](https://github.com/pypeclub/pype/pull/767)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Harmony: template extraction and palettes thumbnails on mac [\#768](https://github.com/pypeclub/pype/pull/768)
|
||||
- TVPaint store context to workfile metadata \(764\) [\#766](https://github.com/pypeclub/pype/pull/766)
|
||||
- Extract review audio cut fix [\#763](https://github.com/pypeclub/pype/pull/763)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- AE: fix publish after background load [\#781](https://github.com/pypeclub/pype/pull/781)
|
||||
- TVPaint store members key [\#769](https://github.com/pypeclub/pype/pull/769)
|
||||
|
||||
## [2.14.1](https://github.com/pypeclub/pype/tree/2.14.1) (2020-11-27)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.14.0...2.14.1)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Settings required keys in modifiable dict [\#770](https://github.com/pypeclub/pype/pull/770)
|
||||
- Extract review may not add audio to output [\#761](https://github.com/pypeclub/pype/pull/761)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- After Effects: frame range, file format and render source scene fixes [\#760](https://github.com/pypeclub/pype/pull/760)
|
||||
- Hiero: trimming review with clip event number [\#754](https://github.com/pypeclub/pype/pull/754)
|
||||
- TVPaint: fix updating of loaded subsets [\#752](https://github.com/pypeclub/pype/pull/752)
|
||||
- Maya: Vray handling of default aov [\#748](https://github.com/pypeclub/pype/pull/748)
|
||||
- Maya: multiple renderable cameras in layer didn't work [\#744](https://github.com/pypeclub/pype/pull/744)
|
||||
- Ftrack integrate custom attributes fix [\#742](https://github.com/pypeclub/pype/pull/742)
|
||||
|
||||
## [2.14.0](https://github.com/pypeclub/pype/tree/2.14.0) (2020-11-23)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.7...2.14.0)
|
||||
|
||||
**Enhancements:**
|
||||
|
||||
- Render publish plugins abstraction [\#687](https://github.com/pypeclub/pype/pull/687)
|
||||
- Shot asset build trigger status [\#736](https://github.com/pypeclub/pype/pull/736)
|
||||
- Maya: add camera rig publishing option [\#721](https://github.com/pypeclub/pype/pull/721)
|
||||
- Sort instances by label in pyblish gui [\#719](https://github.com/pypeclub/pype/pull/719)
|
||||
- Synchronize ftrack hierarchical and shot attributes [\#716](https://github.com/pypeclub/pype/pull/716)
|
||||
- 686 standalonepublisher editorial from image sequences [\#699](https://github.com/pypeclub/pype/pull/699)
|
||||
- Ask user to select non-default camera from scene or create a new. [\#678](https://github.com/pypeclub/pype/pull/678)
|
||||
- TVPaint: image loader with options [\#675](https://github.com/pypeclub/pype/pull/675)
|
||||
- Maya: Camera name can be added to burnins. [\#674](https://github.com/pypeclub/pype/pull/674)
|
||||
- After Effects: base integration with loaders [\#667](https://github.com/pypeclub/pype/pull/667)
|
||||
- Harmony: Javascript refactoring and overall stability improvements [\#666](https://github.com/pypeclub/pype/pull/666)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Bugfix Hiero Review / Plate representation publish [\#743](https://github.com/pypeclub/pype/pull/743)
|
||||
- Asset fetch second fix [\#726](https://github.com/pypeclub/pype/pull/726)
|
||||
- TVPaint extract review fix [\#740](https://github.com/pypeclub/pype/pull/740)
|
||||
- After Effects: Review were not being sent to ftrack [\#738](https://github.com/pypeclub/pype/pull/738)
|
||||
- Maya: vray proxy was not loading [\#722](https://github.com/pypeclub/pype/pull/722)
|
||||
- Maya: Vray expected file fixes [\#682](https://github.com/pypeclub/pype/pull/682)
|
||||
- Missing audio on farm submission. [\#639](https://github.com/pypeclub/pype/pull/639)
|
||||
|
||||
**Deprecated:**
|
||||
|
||||
- Removed artist view from pyblish gui [\#717](https://github.com/pypeclub/pype/pull/717)
|
||||
- Maya: disable legacy override check for cameras [\#715](https://github.com/pypeclub/pype/pull/715)
|
||||
|
||||
**Merged pull requests:**
|
||||
|
||||
- Application manager [\#728](https://github.com/pypeclub/pype/pull/728)
|
||||
- Feature \#664 3.0 lib refactor [\#706](https://github.com/pypeclub/pype/pull/706)
|
||||
- Lib from illicit part 2 [\#700](https://github.com/pypeclub/pype/pull/700)
|
||||
- 3.0 lib refactor - path tools [\#697](https://github.com/pypeclub/pype/pull/697)
|
||||
|
||||
## [2.13.7](https://github.com/pypeclub/pype/tree/2.13.7) (2020-11-19)
|
||||
|
||||
[Full Changelog](https://github.com/pypeclub/pype/compare/2.13.6...2.13.7)
|
||||
|
||||
**Fixed bugs:**
|
||||
|
||||
- Standalone Publisher: getting fps from context instead of nonexistent entity [\#729](https://github.com/pypeclub/pype/pull/729)
|
||||
|
||||
# Changelog
|
||||
|
||||
## [2.13.6](https://github.com/pypeclub/pype/tree/2.13.6) (2020-11-15)
|
||||
|
|
@ -789,4 +1054,7 @@ A large cleanup release. Most of the change are under the hood.
|
|||
- _(avalon)_ subsets in maya 2019 weren't behaving correctly in the outliner
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
||||
|
||||
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||
|
|
|
|||
|
|
@ -80,25 +80,31 @@ class PreCollectNukeInstances(pyblish.api.ContextPlugin):
|
|||
|
||||
# Add all nodes in group instances.
|
||||
if node.Class() == "Group":
|
||||
# check if it is write node in family
|
||||
if "write" in families:
|
||||
# only alter families for render family
|
||||
if "write" in families_ak:
|
||||
target = node["render"].value()
|
||||
if target == "Use existing frames":
|
||||
# Local rendering
|
||||
self.log.info("flagged for no render")
|
||||
families.append("render")
|
||||
families.append(family)
|
||||
elif target == "Local":
|
||||
# Local rendering
|
||||
self.log.info("flagged for local render")
|
||||
families.append("{}.local".format("render"))
|
||||
families.append("{}.local".format(family))
|
||||
elif target == "On farm":
|
||||
# Farm rendering
|
||||
self.log.info("flagged for farm render")
|
||||
instance.data["transfer"] = False
|
||||
families.append("{}.farm".format("render"))
|
||||
families.append("{}.farm".format(family))
|
||||
|
||||
# suffle family to `write` as it is main family
|
||||
# this will be changed later on in process
|
||||
if "render" in families:
|
||||
families.remove("render")
|
||||
family = "write"
|
||||
elif "prerender" in families:
|
||||
families.remove("prerender")
|
||||
family = "write"
|
||||
|
||||
node.begin()
|
||||
for i in nuke.allNodes():
|
||||
|
|
|
|||
|
|
@ -108,6 +108,8 @@ class CollectNukeWrites(pyblish.api.InstancePlugin):
|
|||
|
||||
# Add version data to instance
|
||||
version_data = {
|
||||
"families": [f.replace(".local", "").replace(".farm", "")
|
||||
for f in families if "write" not in f],
|
||||
"colorspace": node["colorspace"].value(),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
))
|
||||
|
||||
for instance_data in workfile_instances:
|
||||
instance_data["fps"] = context.data["fps"]
|
||||
instance_data["fps"] = context.data["sceneFps"]
|
||||
|
||||
# Store workfile instance data to instance data
|
||||
instance_data["originData"] = copy.deepcopy(instance_data)
|
||||
|
|
@ -32,6 +32,11 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
subset_name = instance_data["subset"]
|
||||
name = instance_data.get("name", subset_name)
|
||||
instance_data["name"] = name
|
||||
instance_data["label"] = "{} [{}-{}]".format(
|
||||
name,
|
||||
context.data["sceneFrameStart"],
|
||||
context.data["sceneFrameEnd"]
|
||||
)
|
||||
|
||||
active = instance_data.get("active", True)
|
||||
instance_data["active"] = active
|
||||
|
|
|
|||
|
|
@ -141,11 +141,11 @@ class CollectWorkfileData(pyblish.api.ContextPlugin):
|
|||
"currentFile": workfile_path,
|
||||
"sceneWidth": width,
|
||||
"sceneHeight": height,
|
||||
"pixelAspect": pixel_apsect,
|
||||
"frameStart": frame_start,
|
||||
"frameEnd": frame_end,
|
||||
"fps": frame_rate,
|
||||
"fieldOrder": field_order
|
||||
"scenePixelAspect": pixel_apsect,
|
||||
"sceneFrameStart": frame_start,
|
||||
"sceneFrameEnd": frame_end,
|
||||
"sceneFps": frame_rate,
|
||||
"sceneFieldOrder": field_order
|
||||
}
|
||||
self.log.debug(
|
||||
"Scene data: {}".format(json.dumps(scene_data, indent=4))
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
import json
|
||||
|
||||
import pyblish.api
|
||||
|
||||
|
||||
class ValidateProjectSettings(pyblish.api.ContextPlugin):
|
||||
"""Validate project settings against database.
|
||||
"""
|
||||
|
||||
label = "Validate Project Settings"
|
||||
order = pyblish.api.ValidatorOrder
|
||||
optional = True
|
||||
|
||||
def process(self, context):
|
||||
scene_data = {
|
||||
"frameStart": context.data.get("sceneFrameStart"),
|
||||
"frameEnd": context.data.get("sceneFrameEnd"),
|
||||
"fps": context.data.get("sceneFps"),
|
||||
"resolutionWidth": context.data.get("sceneWidth"),
|
||||
"resolutionHeight": context.data.get("sceneHeight"),
|
||||
"pixelAspect": context.data.get("scenePixelAspect")
|
||||
}
|
||||
invalid = {}
|
||||
for k in scene_data.keys():
|
||||
expected_value = context.data["assetEntity"]["data"][k]
|
||||
if scene_data[k] != expected_value:
|
||||
invalid[k] = {
|
||||
"current": scene_data[k], "expected": expected_value
|
||||
}
|
||||
|
||||
if invalid:
|
||||
raise AssertionError(
|
||||
"Project settings does not match database:\n{}".format(
|
||||
json.dumps(invalid, sort_keys=True, indent=4)
|
||||
)
|
||||
)
|
||||
|
|
@ -41,7 +41,7 @@ from .log_viewer import LogViewModule
|
|||
from .muster import MusterModule
|
||||
from .deadline import DeadlineModule
|
||||
from .standalonepublish_action import StandAlonePublishAction
|
||||
from .sync_server import SyncServer
|
||||
from .sync_server import SyncServerModule
|
||||
|
||||
|
||||
__all__ = (
|
||||
|
|
@ -82,5 +82,5 @@ __all__ = (
|
|||
"DeadlineModule",
|
||||
"StandAlonePublishAction",
|
||||
|
||||
"SyncServer"
|
||||
"SyncServerModule"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin):
|
|||
|
||||
hosts = ["fusion", "maya", "nuke", "celaction", "aftereffects", "harmony"]
|
||||
|
||||
families = ["render.farm", "prerender",
|
||||
families = ["render.farm", "prerender.farm",
|
||||
"renderlayer", "imagesequence", "vrayscene"]
|
||||
|
||||
aov_filter = {"maya": [r".+(?:\.|_)([Bb]eauty)(?:\.|_).*"],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,365 @@
|
|||
import json
|
||||
|
||||
from openpype.api import ProjectSettings
|
||||
|
||||
from openpype.modules.ftrack.lib import ServerAction
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
||||
class PrepareProjectServer(ServerAction):
|
||||
"""Prepare project attributes in Anatomy."""
|
||||
|
||||
identifier = "prepare.project.server"
|
||||
label = "OpenPype Admin"
|
||||
variant = "- Prepare Project (Server)"
|
||||
description = "Set basic attributes on the project"
|
||||
|
||||
settings_key = "prepare_project"
|
||||
|
||||
role_list = ["Pypeclub", "Administrator", "Project Manager"]
|
||||
|
||||
# Key to store info about trigerring create folder structure
|
||||
item_splitter = {"type": "label", "value": "---"}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
"""Show only on project."""
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != "project"
|
||||
):
|
||||
return False
|
||||
|
||||
return self.valid_roles(session, entities, event)
|
||||
|
||||
def interface(self, session, entities, event):
|
||||
if event['data'].get('values', {}):
|
||||
return
|
||||
|
||||
# Inform user that this may take a while
|
||||
self.show_message(event, "Preparing data... Please wait", True)
|
||||
self.log.debug("Preparing data which will be shown")
|
||||
|
||||
self.log.debug("Loading custom attributes")
|
||||
|
||||
project_entity = entities[0]
|
||||
project_name = project_entity["full_name"]
|
||||
|
||||
try:
|
||||
project_settings = ProjectSettings(project_name)
|
||||
except ValueError:
|
||||
return {
|
||||
"message": "Project is not synchronized yet",
|
||||
"success": False
|
||||
}
|
||||
|
||||
project_anatom_settings = project_settings["project_anatomy"]
|
||||
root_items = self.prepare_root_items(project_anatom_settings)
|
||||
|
||||
ca_items, multiselect_enumerators = (
|
||||
self.prepare_custom_attribute_items(project_anatom_settings)
|
||||
)
|
||||
|
||||
self.log.debug("Heavy items are ready. Preparing last items group.")
|
||||
|
||||
title = "Prepare Project"
|
||||
items = []
|
||||
|
||||
# Add root items
|
||||
items.extend(root_items)
|
||||
|
||||
items.append(self.item_splitter)
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Set basic Attributes:</h3>"
|
||||
})
|
||||
|
||||
items.extend(ca_items)
|
||||
|
||||
# This item will be last (before enumerators)
|
||||
# - sets value of auto synchronization
|
||||
auto_sync_name = "avalon_auto_sync"
|
||||
auto_sync_value = project_entity["custom_attributes"].get(
|
||||
CUST_ATTR_AUTO_SYNC, False
|
||||
)
|
||||
auto_sync_item = {
|
||||
"name": auto_sync_name,
|
||||
"type": "boolean",
|
||||
"value": auto_sync_value,
|
||||
"label": "AutoSync to Avalon"
|
||||
}
|
||||
# Add autosync attribute
|
||||
items.append(auto_sync_item)
|
||||
|
||||
# Add enumerator items at the end
|
||||
for item in multiselect_enumerators:
|
||||
items.append(item)
|
||||
|
||||
return {
|
||||
"items": items,
|
||||
"title": title
|
||||
}
|
||||
|
||||
def prepare_root_items(self, project_anatom_settings):
|
||||
self.log.debug("Root items preparation begins.")
|
||||
|
||||
root_items = []
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Check your Project root settings</h3>"
|
||||
})
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<p><i>NOTE: Roots are <b>crutial</b> for path filling"
|
||||
" (and creating folder structure).</i></p>"
|
||||
)
|
||||
})
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"<p><i>WARNING: Do not change roots on running project,"
|
||||
" that <b>will cause workflow issues</b>.</i></p>"
|
||||
)
|
||||
})
|
||||
|
||||
empty_text = "Enter root path here..."
|
||||
|
||||
roots_entity = project_anatom_settings["roots"]
|
||||
for root_name, root_entity in roots_entity.items():
|
||||
root_items.append(self.item_splitter)
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "Root: \"{}\"".format(root_name)
|
||||
})
|
||||
for platform_name, value_entity in root_entity.items():
|
||||
root_items.append({
|
||||
"label": platform_name,
|
||||
"name": "__root__{}__{}".format(root_name, platform_name),
|
||||
"type": "text",
|
||||
"value": value_entity.value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(list(roots_entity.keys()))
|
||||
})
|
||||
|
||||
self.log.debug("Root items preparation ended.")
|
||||
return root_items
|
||||
|
||||
def _attributes_to_set(self, project_anatom_settings):
|
||||
attributes_to_set = {}
|
||||
|
||||
attribute_values_by_key = {}
|
||||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
key = attr["key"]
|
||||
if key.startswith("avalon_"):
|
||||
continue
|
||||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
for attr in cust_attrs:
|
||||
if attr["entity_type"].lower() != "show":
|
||||
continue
|
||||
key = attr["key"]
|
||||
if key.startswith("avalon_"):
|
||||
continue
|
||||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
# Sort by label
|
||||
attributes_to_set = dict(sorted(
|
||||
attributes_to_set.items(),
|
||||
key=lambda x: x[1]["label"]
|
||||
))
|
||||
return attributes_to_set
|
||||
|
||||
def prepare_custom_attribute_items(self, project_anatom_settings):
|
||||
items = []
|
||||
multiselect_enumerators = []
|
||||
attributes_to_set = self._attributes_to_set(project_anatom_settings)
|
||||
|
||||
self.log.debug("Preparing interface for keys: \"{}\"".format(
|
||||
str([key for key in attributes_to_set])
|
||||
))
|
||||
|
||||
for key, in_data in attributes_to_set.items():
|
||||
attr = in_data["object"]
|
||||
|
||||
# initial item definition
|
||||
item = {
|
||||
"name": key,
|
||||
"label": in_data["label"]
|
||||
}
|
||||
|
||||
# cust attr type - may have different visualization
|
||||
type_name = attr["type"]["name"].lower()
|
||||
easy_types = ["text", "boolean", "date", "number"]
|
||||
|
||||
easy_type = False
|
||||
if type_name in easy_types:
|
||||
easy_type = True
|
||||
|
||||
elif type_name == "enumerator":
|
||||
|
||||
attr_config = json.loads(attr["config"])
|
||||
attr_config_data = json.loads(attr_config["data"])
|
||||
|
||||
if attr_config["multiSelect"] is True:
|
||||
multiselect_enumerators.append(self.item_splitter)
|
||||
multiselect_enumerators.append({
|
||||
"type": "label",
|
||||
"value": in_data["label"]
|
||||
})
|
||||
|
||||
default = in_data["default"]
|
||||
names = []
|
||||
for option in sorted(
|
||||
attr_config_data, key=lambda x: x["menu"]
|
||||
):
|
||||
name = option["value"]
|
||||
new_name = "__{}__{}".format(key, name)
|
||||
names.append(new_name)
|
||||
item = {
|
||||
"name": new_name,
|
||||
"type": "boolean",
|
||||
"label": "- {}".format(option["menu"])
|
||||
}
|
||||
if default:
|
||||
if isinstance(default, (list, tuple)):
|
||||
if name in default:
|
||||
item["value"] = True
|
||||
else:
|
||||
if name == default:
|
||||
item["value"] = True
|
||||
|
||||
multiselect_enumerators.append(item)
|
||||
|
||||
multiselect_enumerators.append({
|
||||
"type": "hidden",
|
||||
"name": "__hidden__{}".format(key),
|
||||
"value": json.dumps(names)
|
||||
})
|
||||
else:
|
||||
easy_type = True
|
||||
item["data"] = attr_config_data
|
||||
|
||||
else:
|
||||
self.log.warning((
|
||||
"Custom attribute \"{}\" has type \"{}\"."
|
||||
" I don't know how to handle"
|
||||
).format(key, type_name))
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": (
|
||||
"!!! Can't handle Custom attritubte type \"{}\""
|
||||
" (key: \"{}\")"
|
||||
).format(type_name, key)
|
||||
})
|
||||
|
||||
if easy_type:
|
||||
item["type"] = type_name
|
||||
|
||||
# default value in interface
|
||||
default = in_data["default"]
|
||||
if default is not None:
|
||||
item["value"] = default
|
||||
|
||||
items.append(item)
|
||||
|
||||
return items, multiselect_enumerators
|
||||
|
||||
def launch(self, session, entities, event):
|
||||
if not event['data'].get('values', {}):
|
||||
return
|
||||
|
||||
in_data = event['data']['values']
|
||||
|
||||
root_values = {}
|
||||
root_key = "__root__"
|
||||
for key in tuple(in_data.keys()):
|
||||
if key.startswith(root_key):
|
||||
_key = key[len(root_key):]
|
||||
root_values[_key] = in_data.pop(key)
|
||||
|
||||
root_names = in_data.pop("__rootnames__", None)
|
||||
root_data = {}
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
prefix = "{}__".format(root_name)
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
_key = key[len(prefix):]
|
||||
root_data[root_name][_key] = value
|
||||
|
||||
# Find hidden items for multiselect enumerators
|
||||
keys_to_process = []
|
||||
for key in in_data:
|
||||
if key.startswith("__hidden__"):
|
||||
keys_to_process.append(key)
|
||||
|
||||
self.log.debug("Preparing data for Multiselect Enumerators")
|
||||
enumerators = {}
|
||||
for key in keys_to_process:
|
||||
new_key = key.replace("__hidden__", "")
|
||||
enumerator_items = in_data.pop(key)
|
||||
enumerators[new_key] = json.loads(enumerator_items)
|
||||
|
||||
# find values set for multiselect enumerator
|
||||
for key, enumerator_items in enumerators.items():
|
||||
in_data[key] = []
|
||||
|
||||
name = "__{}__".format(key)
|
||||
|
||||
for item in enumerator_items:
|
||||
value = in_data.pop(item)
|
||||
if value is True:
|
||||
new_key = item.replace(name, "")
|
||||
in_data[key].append(new_key)
|
||||
|
||||
self.log.debug("Setting Custom Attribute values")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_settings = ProjectSettings(project_name)
|
||||
project_anatomy_settings = project_settings["project_anatomy"]
|
||||
project_anatomy_settings["roots"] = root_data
|
||||
|
||||
custom_attribute_values = {}
|
||||
attributes_entity = project_anatomy_settings["attributes"]
|
||||
for key, value in in_data.items():
|
||||
if key not in attributes_entity:
|
||||
custom_attribute_values[key] = value
|
||||
else:
|
||||
attributes_entity[key] = value
|
||||
|
||||
project_settings.save()
|
||||
|
||||
entity = entities[0]
|
||||
for key, value in custom_attribute_values.items():
|
||||
entity["custom_attributes"][key] = value
|
||||
self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def register(session):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
PrepareProjectServer(session).register()
|
||||
|
|
@ -1,31 +1,34 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
from openpype.modules.ftrack.lib import BaseAction, statics_icon
|
||||
from openpype.api import config, Anatomy
|
||||
from openpype.modules.ftrack.lib.avalon_sync import get_pype_attr
|
||||
from openpype.api import ProjectSettings
|
||||
|
||||
from openpype.modules.ftrack.lib import (
|
||||
BaseAction,
|
||||
statics_icon
|
||||
)
|
||||
from openpype.modules.ftrack.lib.avalon_sync import (
|
||||
get_pype_attr,
|
||||
CUST_ATTR_AUTO_SYNC
|
||||
)
|
||||
|
||||
|
||||
class PrepareProject(BaseAction):
|
||||
'''Edit meta data action.'''
|
||||
class PrepareProjectLocal(BaseAction):
|
||||
"""Prepare project attributes in Anatomy."""
|
||||
|
||||
#: Action identifier.
|
||||
identifier = 'prepare.project'
|
||||
#: Action label.
|
||||
label = 'Prepare Project'
|
||||
#: Action description.
|
||||
description = 'Set basic attributes on the project'
|
||||
#: roles that are allowed to register this action
|
||||
identifier = "prepare.project.local"
|
||||
label = "Prepare Project"
|
||||
description = "Set basic attributes on the project"
|
||||
icon = statics_icon("ftrack", "action_icons", "PrepareProject.svg")
|
||||
|
||||
role_list = ["Pypeclub", "Administrator", "Project Manager"]
|
||||
|
||||
settings_key = "prepare_project"
|
||||
|
||||
# Key to store info about trigerring create folder structure
|
||||
create_project_structure_key = "create_folder_structure"
|
||||
item_splitter = {'type': 'label', 'value': '---'}
|
||||
item_splitter = {"type": "label", "value": "---"}
|
||||
|
||||
def discover(self, session, entities, event):
|
||||
''' Validation '''
|
||||
"""Show only on project."""
|
||||
if (
|
||||
len(entities) != 1
|
||||
or entities[0].entity_type.lower() != "project"
|
||||
|
|
@ -44,27 +47,22 @@ class PrepareProject(BaseAction):
|
|||
|
||||
self.log.debug("Loading custom attributes")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_entity = entities[0]
|
||||
project_name = project_entity["full_name"]
|
||||
|
||||
project_defaults = (
|
||||
config.get_presets(project_name)
|
||||
.get("ftrack", {})
|
||||
.get("project_defaults", {})
|
||||
)
|
||||
|
||||
anatomy = Anatomy(project_name)
|
||||
if not anatomy.roots:
|
||||
try:
|
||||
project_settings = ProjectSettings(project_name)
|
||||
except ValueError:
|
||||
return {
|
||||
"success": False,
|
||||
"message": (
|
||||
"Have issues with loading Roots for project \"{}\"."
|
||||
).format(anatomy.project_name)
|
||||
"message": "Project is not synchronized yet",
|
||||
"success": False
|
||||
}
|
||||
|
||||
root_items = self.prepare_root_items(anatomy)
|
||||
project_anatom_settings = project_settings["project_anatomy"]
|
||||
root_items = self.prepare_root_items(project_anatom_settings)
|
||||
|
||||
ca_items, multiselect_enumerators = (
|
||||
self.prepare_custom_attribute_items(project_defaults)
|
||||
self.prepare_custom_attribute_items(project_anatom_settings)
|
||||
)
|
||||
|
||||
self.log.debug("Heavy items are ready. Preparing last items group.")
|
||||
|
|
@ -74,19 +72,6 @@ class PrepareProject(BaseAction):
|
|||
|
||||
# Add root items
|
||||
items.extend(root_items)
|
||||
items.append(self.item_splitter)
|
||||
|
||||
# Ask if want to trigger Action Create Folder Structure
|
||||
items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Want to create basic Folder Structure?</h3>"
|
||||
})
|
||||
items.append({
|
||||
"name": self.create_project_structure_key,
|
||||
"type": "boolean",
|
||||
"value": False,
|
||||
"label": "Check if Yes"
|
||||
})
|
||||
|
||||
items.append(self.item_splitter)
|
||||
items.append({
|
||||
|
|
@ -99,10 +84,13 @@ class PrepareProject(BaseAction):
|
|||
# This item will be last (before enumerators)
|
||||
# - sets value of auto synchronization
|
||||
auto_sync_name = "avalon_auto_sync"
|
||||
auto_sync_value = project_entity["custom_attributes"].get(
|
||||
CUST_ATTR_AUTO_SYNC, False
|
||||
)
|
||||
auto_sync_item = {
|
||||
"name": auto_sync_name,
|
||||
"type": "boolean",
|
||||
"value": project_defaults.get(auto_sync_name, False),
|
||||
"value": auto_sync_value,
|
||||
"label": "AutoSync to Avalon"
|
||||
}
|
||||
# Add autosync attribute
|
||||
|
|
@ -117,13 +105,10 @@ class PrepareProject(BaseAction):
|
|||
"title": title
|
||||
}
|
||||
|
||||
def prepare_root_items(self, anatomy):
|
||||
root_items = []
|
||||
def prepare_root_items(self, project_anatom_settings):
|
||||
self.log.debug("Root items preparation begins.")
|
||||
|
||||
root_names = anatomy.root_names()
|
||||
roots = anatomy.roots
|
||||
|
||||
root_items = []
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "<h3>Check your Project root settings</h3>"
|
||||
|
|
@ -143,85 +128,40 @@ class PrepareProject(BaseAction):
|
|||
)
|
||||
})
|
||||
|
||||
default_roots = anatomy.roots
|
||||
while isinstance(default_roots, dict):
|
||||
key = tuple(default_roots.keys())[0]
|
||||
default_roots = default_roots[key]
|
||||
|
||||
empty_text = "Enter root path here..."
|
||||
|
||||
# Root names is None when anatomy templates contain "{root}"
|
||||
all_platforms = ["windows", "linux", "darwin"]
|
||||
if root_names is None:
|
||||
root_items.append(self.item_splitter)
|
||||
# find first possible key
|
||||
for platform in all_platforms:
|
||||
value = default_roots.raw_data.get(platform) or ""
|
||||
root_items.append({
|
||||
"label": platform,
|
||||
"name": "__root__{}".format(platform),
|
||||
"type": "text",
|
||||
"value": value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
return root_items
|
||||
|
||||
root_name_data = {}
|
||||
missing_roots = []
|
||||
for root_name in root_names:
|
||||
root_name_data[root_name] = {}
|
||||
if not isinstance(roots, dict):
|
||||
missing_roots.append(root_name)
|
||||
continue
|
||||
|
||||
root_item = roots.get(root_name)
|
||||
if not root_item:
|
||||
missing_roots.append(root_name)
|
||||
continue
|
||||
|
||||
for platform in all_platforms:
|
||||
root_name_data[root_name][platform] = (
|
||||
root_item.raw_data.get(platform) or ""
|
||||
)
|
||||
|
||||
if missing_roots:
|
||||
default_values = {}
|
||||
for platform in all_platforms:
|
||||
default_values[platform] = (
|
||||
default_roots.raw_data.get(platform) or ""
|
||||
)
|
||||
|
||||
for root_name in missing_roots:
|
||||
root_name_data[root_name] = default_values
|
||||
|
||||
root_names = list(root_name_data.keys())
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(root_names)
|
||||
})
|
||||
|
||||
for root_name, values in root_name_data.items():
|
||||
roots_entity = project_anatom_settings["roots"]
|
||||
for root_name, root_entity in roots_entity.items():
|
||||
root_items.append(self.item_splitter)
|
||||
root_items.append({
|
||||
"type": "label",
|
||||
"value": "Root: \"{}\"".format(root_name)
|
||||
})
|
||||
for platform, value in values.items():
|
||||
for platform_name, value_entity in root_entity.items():
|
||||
root_items.append({
|
||||
"label": platform,
|
||||
"name": "__root__{}{}".format(root_name, platform),
|
||||
"label": platform_name,
|
||||
"name": "__root__{}__{}".format(root_name, platform_name),
|
||||
"type": "text",
|
||||
"value": value,
|
||||
"value": value_entity.value,
|
||||
"empty_text": empty_text
|
||||
})
|
||||
|
||||
root_items.append({
|
||||
"type": "hidden",
|
||||
"name": "__rootnames__",
|
||||
"value": json.dumps(list(roots_entity.keys()))
|
||||
})
|
||||
|
||||
self.log.debug("Root items preparation ended.")
|
||||
return root_items
|
||||
|
||||
def _attributes_to_set(self, project_defaults):
|
||||
def _attributes_to_set(self, project_anatom_settings):
|
||||
attributes_to_set = {}
|
||||
|
||||
attribute_values_by_key = {}
|
||||
for key, entity in project_anatom_settings["attributes"].items():
|
||||
attribute_values_by_key[key] = entity.value
|
||||
|
||||
cust_attrs, hier_cust_attrs = get_pype_attr(self.session, True)
|
||||
|
||||
for attr in hier_cust_attrs:
|
||||
|
|
@ -231,7 +171,7 @@ class PrepareProject(BaseAction):
|
|||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": project_defaults.get(key)
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
for attr in cust_attrs:
|
||||
|
|
@ -243,7 +183,7 @@ class PrepareProject(BaseAction):
|
|||
attributes_to_set[key] = {
|
||||
"label": attr["label"],
|
||||
"object": attr,
|
||||
"default": project_defaults.get(key)
|
||||
"default": attribute_values_by_key.get(key)
|
||||
}
|
||||
|
||||
# Sort by label
|
||||
|
|
@ -253,10 +193,10 @@ class PrepareProject(BaseAction):
|
|||
))
|
||||
return attributes_to_set
|
||||
|
||||
def prepare_custom_attribute_items(self, project_defaults):
|
||||
def prepare_custom_attribute_items(self, project_anatom_settings):
|
||||
items = []
|
||||
multiselect_enumerators = []
|
||||
attributes_to_set = self._attributes_to_set(project_defaults)
|
||||
attributes_to_set = self._attributes_to_set(project_anatom_settings)
|
||||
|
||||
self.log.debug("Preparing interface for keys: \"{}\"".format(
|
||||
str([key for key in attributes_to_set])
|
||||
|
|
@ -363,24 +303,15 @@ class PrepareProject(BaseAction):
|
|||
|
||||
root_names = in_data.pop("__rootnames__", None)
|
||||
root_data = {}
|
||||
if root_names:
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
if key.startswith(root_name):
|
||||
_key = key[len(root_name):]
|
||||
root_data[root_name][_key] = value
|
||||
for root_name in json.loads(root_names):
|
||||
root_data[root_name] = {}
|
||||
for key, value in tuple(root_values.items()):
|
||||
prefix = "{}__".format(root_name)
|
||||
if not key.startswith(prefix):
|
||||
continue
|
||||
|
||||
else:
|
||||
for key, value in root_values.items():
|
||||
root_data[key] = value
|
||||
|
||||
# TODO implement creating of anatomy for new projects
|
||||
# project_name = entities[0]["full_name"]
|
||||
# anatomy = Anatomy(project_name)
|
||||
|
||||
# pop out info about creating project structure
|
||||
create_proj_struct = in_data.pop(self.create_project_structure_key)
|
||||
_key = key[len(prefix):]
|
||||
root_data[root_name][_key] = value
|
||||
|
||||
# Find hidden items for multiselect enumerators
|
||||
keys_to_process = []
|
||||
|
|
@ -407,54 +338,31 @@ class PrepareProject(BaseAction):
|
|||
new_key = item.replace(name, "")
|
||||
in_data[key].append(new_key)
|
||||
|
||||
self.log.debug("Setting Custom Attribute values:")
|
||||
entity = entities[0]
|
||||
self.log.debug("Setting Custom Attribute values")
|
||||
|
||||
project_name = entities[0]["full_name"]
|
||||
project_settings = ProjectSettings(project_name)
|
||||
project_anatomy_settings = project_settings["project_anatomy"]
|
||||
project_anatomy_settings["roots"] = root_data
|
||||
|
||||
custom_attribute_values = {}
|
||||
attributes_entity = project_anatomy_settings["attributes"]
|
||||
for key, value in in_data.items():
|
||||
if key not in attributes_entity:
|
||||
custom_attribute_values[key] = value
|
||||
else:
|
||||
attributes_entity[key] = value
|
||||
|
||||
project_settings.save()
|
||||
|
||||
entity = entities[0]
|
||||
for key, value in custom_attribute_values.items():
|
||||
entity["custom_attributes"][key] = value
|
||||
self.log.debug("- Key \"{}\" set to \"{}\"".format(key, value))
|
||||
|
||||
session.commit()
|
||||
|
||||
# Create project structure
|
||||
self.create_project_specific_config(entities[0]["full_name"], in_data)
|
||||
|
||||
# Trigger Create Project Structure action
|
||||
if create_proj_struct is True:
|
||||
self.trigger_action("create.project.structure", event)
|
||||
|
||||
return True
|
||||
|
||||
def create_project_specific_config(self, project_name, json_data):
|
||||
self.log.debug("*** Creating project specifig configs ***")
|
||||
project_specific_path = project_overrides_dir_path(project_name)
|
||||
if not os.path.exists(project_specific_path):
|
||||
os.makedirs(project_specific_path)
|
||||
self.log.debug((
|
||||
"Project specific config folder for project \"{}\" created."
|
||||
).format(project_name))
|
||||
|
||||
# Presets ####################################
|
||||
self.log.debug("--- Processing Presets Begins: ---")
|
||||
|
||||
project_defaults_dir = os.path.normpath(os.path.join(
|
||||
project_specific_path, "presets", "ftrack"
|
||||
))
|
||||
project_defaults_path = os.path.normpath(os.path.join(
|
||||
project_defaults_dir, "project_defaults.json"
|
||||
))
|
||||
# Create folder if not exist
|
||||
if not os.path.exists(project_defaults_dir):
|
||||
self.log.debug("Creating Ftrack Presets folder: \"{}\"".format(
|
||||
project_defaults_dir
|
||||
))
|
||||
os.makedirs(project_defaults_dir)
|
||||
|
||||
with open(project_defaults_path, 'w') as file_stream:
|
||||
json.dump(json_data, file_stream, indent=4)
|
||||
|
||||
self.log.debug("*** Creating project specifig configs Finished ***")
|
||||
|
||||
|
||||
def register(session):
|
||||
'''Register plugin. Called when used as an plugin.'''
|
||||
PrepareProject(session).register()
|
||||
PrepareProjectLocal(session).register()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
from openpype.modules.sync_server.sync_server import SyncServer
|
||||
from openpype.modules.sync_server.sync_server_module import SyncServerModule
|
||||
|
||||
|
||||
def tray_init(tray_widget, main_widget):
|
||||
return SyncServer()
|
||||
return SyncServerModule()
|
||||
|
|
|
|||
0
openpype/modules/sync_server/providers/__init__.py
Normal file
|
|
@ -1,16 +1,23 @@
|
|||
from abc import ABCMeta, abstractmethod
|
||||
import abc
|
||||
import six
|
||||
from openpype.api import Logger
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class AbstractProvider(metaclass=ABCMeta):
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class AbstractProvider:
|
||||
|
||||
def __init__(self, site_name, tree=None, presets=None):
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.site_name = site_name
|
||||
|
||||
self.presets = presets
|
||||
|
||||
@abstractmethod
|
||||
super(AbstractProvider, self).__init__()
|
||||
|
||||
@abc.abstractmethod
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns True if provider is activated, eg. has working credentials.
|
||||
|
|
@ -18,36 +25,54 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
(boolean)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def upload_file(self, source_path, target_path, overwrite=True):
|
||||
@abc.abstractmethod
|
||||
def upload_file(self, source_path, path,
|
||||
server, collection, file, representation, site,
|
||||
overwrite=False):
|
||||
"""
|
||||
Copy file from 'source_path' to 'target_path' on provider.
|
||||
Use 'overwrite' boolean to rewrite existing file on provider
|
||||
|
||||
Args:
|
||||
source_path (string): absolute path on local system
|
||||
target_path (string): absolute path on provider (GDrive etc.)
|
||||
overwrite (boolean): True if overwite existing
|
||||
source_path (string):
|
||||
path (string): absolute path with or without name of the file
|
||||
overwrite (boolean): replace existing file
|
||||
|
||||
arguments for saving progress:
|
||||
server (SyncServer): server instance to call update_db on
|
||||
collection (str): name of collection
|
||||
file (dict): info about uploaded file (matches structure from db)
|
||||
representation (dict): complete repre containing 'file'
|
||||
site (str): site name
|
||||
Returns:
|
||||
(string) file_id of created file, raises exception
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def download_file(self, source_path, local_path, overwrite=True):
|
||||
@abc.abstractmethod
|
||||
def download_file(self, source_path, local_path,
|
||||
server, collection, file, representation, site,
|
||||
overwrite=False):
|
||||
"""
|
||||
Download file from provider into local system
|
||||
|
||||
Args:
|
||||
source_path (string): absolute path on provider
|
||||
local_path (string): absolute path on local
|
||||
overwrite (bool): default set to True
|
||||
local_path (string): absolute path with or without name of the file
|
||||
overwrite (boolean): replace existing file
|
||||
|
||||
arguments for saving progress:
|
||||
server (SyncServer): server instance to call update_db on
|
||||
collection (str): name of collection
|
||||
file (dict): info about uploaded file (matches structure from db)
|
||||
representation (dict): complete repre containing 'file'
|
||||
site (str): site name
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def delete_file(self, path):
|
||||
"""
|
||||
Deletes file from 'path'. Expects path to specific file.
|
||||
|
|
@ -60,7 +85,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def list_folder(self, folder_path):
|
||||
"""
|
||||
List all files and subfolders of particular path non-recursively.
|
||||
|
|
@ -72,7 +97,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def create_folder(self, folder_path):
|
||||
"""
|
||||
Create all nonexistent folders and subfolders in 'path'.
|
||||
|
|
@ -85,7 +110,7 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def get_tree(self):
|
||||
"""
|
||||
Creates folder structure for providers which do not provide
|
||||
|
|
@ -94,16 +119,50 @@ class AbstractProvider(metaclass=ABCMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
@abc.abstractmethod
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Replaces root placeholders with appropriate real value from
|
||||
'root_configs' (from Settings or Local Settings) or Anatomy
|
||||
(mainly for 'studio' site)
|
||||
Returns root values for path resolving
|
||||
|
||||
Args:
|
||||
path(string): path with '{root[work]}/...'
|
||||
root_config(dict): from Settings or Local Settings
|
||||
anatomy (Anatomy): prepared anatomy object for project
|
||||
Takes value from Anatomy which takes values from Settings
|
||||
overridden by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
pass
|
||||
|
||||
def resolve_path(self, path, root_config=None, anatomy=None):
|
||||
"""
|
||||
Replaces all root placeholders with proper values
|
||||
|
||||
Args:
|
||||
path(string): root[work]/folder...
|
||||
root_config (dict): {'work': "c:/..."...}
|
||||
anatomy (Anatomy): object of Anatomy
|
||||
Returns:
|
||||
(string): proper url
|
||||
"""
|
||||
if not root_config:
|
||||
root_config = self.get_roots_config(anatomy)
|
||||
|
||||
if root_config and not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
|
||||
try:
|
||||
if not root_config:
|
||||
raise KeyError
|
||||
|
||||
path = path.format(**root_config)
|
||||
except KeyError:
|
||||
try:
|
||||
path = anatomy.fill_root(path)
|
||||
except KeyError:
|
||||
msg = "Error in resolving local root from anatomy"
|
||||
log.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
return path
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from openpype.api import get_system_settings
|
|||
from ..utils import time_function
|
||||
import time
|
||||
|
||||
|
||||
SCOPES = ['https://www.googleapis.com/auth/drive.metadata.readonly',
|
||||
'https://www.googleapis.com/auth/drive.file',
|
||||
'https://www.googleapis.com/auth/drive.readonly'] # for write|delete
|
||||
|
|
@ -45,9 +46,10 @@ class GDriveHandler(AbstractProvider):
|
|||
MY_DRIVE_STR = 'My Drive' # name of root folder of regular Google drive
|
||||
CHUNK_SIZE = 2097152 # must be divisible by 256!
|
||||
|
||||
def __init__(self, site_name, tree=None, presets=None):
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.project_name = project_name
|
||||
self.site_name = site_name
|
||||
|
||||
self.presets = presets
|
||||
|
|
@ -65,137 +67,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self._tree = tree
|
||||
self.active = True
|
||||
|
||||
def _get_gd_service(self):
|
||||
"""
|
||||
Authorize client with 'credentials.json', uses service account.
|
||||
Service account needs to have target folder shared with.
|
||||
Produces service that communicates with GDrive API.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
creds = service_account.Credentials.from_service_account_file(
|
||||
self.presets["credentials_url"],
|
||||
scopes=SCOPES)
|
||||
service = build('drive', 'v3',
|
||||
credentials=creds, cache_discovery=False)
|
||||
return service
|
||||
|
||||
def _prepare_root_info(self):
|
||||
"""
|
||||
Prepare info about roots and theirs folder ids from 'presets'.
|
||||
Configuration might be for single or multiroot projects.
|
||||
Regular My Drive and Shared drives are implemented, their root
|
||||
folder ids need to be queried in slightly different way.
|
||||
|
||||
Returns:
|
||||
(dicts) of dicts where root folders are keys
|
||||
"""
|
||||
roots = {}
|
||||
for path in self.get_roots_config().values():
|
||||
if self.MY_DRIVE_STR in path:
|
||||
roots[self.MY_DRIVE_STR] = self.service.files()\
|
||||
.get(fileId='root').execute()
|
||||
else:
|
||||
shared_drives = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
response = self.service.drives().list(
|
||||
pageSize=100,
|
||||
pageToken=page_token).execute()
|
||||
shared_drives.extend(response.get('drives', []))
|
||||
page_token = response.get('nextPageToken', None)
|
||||
if page_token is None:
|
||||
break
|
||||
|
||||
folders = path.split('/')
|
||||
if len(folders) < 2:
|
||||
raise ValueError("Wrong root folder definition {}".
|
||||
format(path))
|
||||
|
||||
for shared_drive in shared_drives:
|
||||
if folders[1] in shared_drive["name"]:
|
||||
roots[shared_drive["name"]] = {
|
||||
"name": shared_drive["name"],
|
||||
"id": shared_drive["id"]}
|
||||
if self.MY_DRIVE_STR not in roots: # add My Drive always
|
||||
roots[self.MY_DRIVE_STR] = self.service.files() \
|
||||
.get(fileId='root').execute()
|
||||
|
||||
return roots
|
||||
|
||||
@time_function
|
||||
def _build_tree(self, folders):
|
||||
"""
|
||||
Create in-memory structure resolving paths to folder id as
|
||||
recursive querying might be slower.
|
||||
Initialized in the time of class initialization.
|
||||
Maybe should be persisted
|
||||
Tree is structure of path to id:
|
||||
'/ROOT': {'id': '1234567'}
|
||||
'/ROOT/PROJECT_FOLDER': {'id':'222222'}
|
||||
'/ROOT/PROJECT_FOLDER/Assets': {'id': '3434545'}
|
||||
Args:
|
||||
folders (list): list of dictionaries with folder metadata
|
||||
Returns:
|
||||
(dictionary) path as a key, folder id as a value
|
||||
"""
|
||||
log.debug("build_tree len {}".format(len(folders)))
|
||||
root_ids = []
|
||||
default_root_id = None
|
||||
tree = {}
|
||||
ending_by = {}
|
||||
for root_name, root in self.root.items(): # might be multiple roots
|
||||
if root["id"] not in root_ids:
|
||||
tree["/" + root_name] = {"id": root["id"]}
|
||||
ending_by[root["id"]] = "/" + root_name
|
||||
root_ids.append(root["id"])
|
||||
|
||||
if self.MY_DRIVE_STR == root_name:
|
||||
default_root_id = root["id"]
|
||||
|
||||
no_parents_yet = {}
|
||||
while folders:
|
||||
folder = folders.pop(0)
|
||||
parents = folder.get("parents", [])
|
||||
# weird cases, shared folders, etc, parent under root
|
||||
if not parents:
|
||||
parent = default_root_id
|
||||
else:
|
||||
parent = parents[0]
|
||||
|
||||
if folder["id"] in root_ids: # do not process root
|
||||
continue
|
||||
|
||||
if parent in ending_by:
|
||||
path_key = ending_by[parent] + "/" + folder["name"]
|
||||
ending_by[folder["id"]] = path_key
|
||||
tree[path_key] = {"id": folder["id"]}
|
||||
else:
|
||||
no_parents_yet.setdefault(parent, []).append((folder["id"],
|
||||
folder["name"]))
|
||||
loop_cnt = 0
|
||||
# break if looped more then X times - safety against infinite loop
|
||||
while no_parents_yet and loop_cnt < 20:
|
||||
|
||||
keys = list(no_parents_yet.keys())
|
||||
for parent in keys:
|
||||
if parent in ending_by.keys():
|
||||
subfolders = no_parents_yet.pop(parent)
|
||||
for folder_id, folder_name in subfolders:
|
||||
path_key = ending_by[parent] + "/" + folder_name
|
||||
ending_by[folder_id] = path_key
|
||||
tree[path_key] = {"id": folder_id}
|
||||
loop_cnt += 1
|
||||
|
||||
if len(no_parents_yet) > 0:
|
||||
log.debug("Some folders path are not resolved {}".
|
||||
format(no_parents_yet))
|
||||
log.debug("Remove deleted folders from trash.")
|
||||
|
||||
return tree
|
||||
|
||||
def is_active(self):
|
||||
"""
|
||||
Returns True if provider is activated, eg. has working credentials.
|
||||
|
|
@ -204,6 +75,21 @@ class GDriveHandler(AbstractProvider):
|
|||
"""
|
||||
return self.active
|
||||
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Returns root values for path resolving
|
||||
|
||||
Use only Settings as GDrive cannot be modified by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
# GDrive roots cannot be locally overridden
|
||||
return self.presets['root']
|
||||
|
||||
def get_tree(self):
|
||||
"""
|
||||
Building of the folder tree could be potentially expensive,
|
||||
|
|
@ -217,26 +103,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self._tree = self._build_tree(self.list_folders())
|
||||
return self._tree
|
||||
|
||||
def get_roots_config(self):
|
||||
"""
|
||||
Returns value from presets of roots. It calculates with multi
|
||||
roots. Config should be simple key value, or dictionary.
|
||||
|
||||
Examples:
|
||||
"root": "/My Drive"
|
||||
OR
|
||||
"root": {"root_ONE": "value", "root_TWO":"value}
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
roots = self.presets["root"]
|
||||
if isinstance(roots, str):
|
||||
roots = {"root": roots}
|
||||
return roots
|
||||
|
||||
def create_folder(self, path):
|
||||
"""
|
||||
Create all nonexistent folders and subfolders in 'path'.
|
||||
|
|
@ -510,20 +376,6 @@ class GDriveHandler(AbstractProvider):
|
|||
self.service.files().delete(fileId=file["id"],
|
||||
supportsAllDrives=True).execute()
|
||||
|
||||
def _get_folder_metadata(self, path):
|
||||
"""
|
||||
Get info about folder with 'path'
|
||||
Args:
|
||||
path (string):
|
||||
|
||||
Returns:
|
||||
(dictionary) with metadata or raises ValueError
|
||||
"""
|
||||
try:
|
||||
return self.get_tree()[path]
|
||||
except Exception:
|
||||
raise ValueError("Uknown folder id {}".format(id))
|
||||
|
||||
def list_folder(self, folder_path):
|
||||
"""
|
||||
List all files and subfolders of particular path non-recursively.
|
||||
|
|
@ -678,15 +530,151 @@ class GDriveHandler(AbstractProvider):
|
|||
return
|
||||
return provider_presets
|
||||
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
if not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
def _get_gd_service(self):
|
||||
"""
|
||||
Authorize client with 'credentials.json', uses service account.
|
||||
Service account needs to have target folder shared with.
|
||||
Produces service that communicates with GDrive API.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
creds = service_account.Credentials.from_service_account_file(
|
||||
self.presets["credentials_url"],
|
||||
scopes=SCOPES)
|
||||
service = build('drive', 'v3',
|
||||
credentials=creds, cache_discovery=False)
|
||||
return service
|
||||
|
||||
def _prepare_root_info(self):
|
||||
"""
|
||||
Prepare info about roots and theirs folder ids from 'presets'.
|
||||
Configuration might be for single or multiroot projects.
|
||||
Regular My Drive and Shared drives are implemented, their root
|
||||
folder ids need to be queried in slightly different way.
|
||||
|
||||
Returns:
|
||||
(dicts) of dicts where root folders are keys
|
||||
"""
|
||||
roots = {}
|
||||
config_roots = self.get_roots_config()
|
||||
for path in config_roots.values():
|
||||
if self.MY_DRIVE_STR in path:
|
||||
roots[self.MY_DRIVE_STR] = self.service.files()\
|
||||
.get(fileId='root').execute()
|
||||
else:
|
||||
shared_drives = []
|
||||
page_token = None
|
||||
|
||||
while True:
|
||||
response = self.service.drives().list(
|
||||
pageSize=100,
|
||||
pageToken=page_token).execute()
|
||||
shared_drives.extend(response.get('drives', []))
|
||||
page_token = response.get('nextPageToken', None)
|
||||
if page_token is None:
|
||||
break
|
||||
|
||||
folders = path.split('/')
|
||||
if len(folders) < 2:
|
||||
raise ValueError("Wrong root folder definition {}".
|
||||
format(path))
|
||||
|
||||
for shared_drive in shared_drives:
|
||||
if folders[1] in shared_drive["name"]:
|
||||
roots[shared_drive["name"]] = {
|
||||
"name": shared_drive["name"],
|
||||
"id": shared_drive["id"]}
|
||||
if self.MY_DRIVE_STR not in roots: # add My Drive always
|
||||
roots[self.MY_DRIVE_STR] = self.service.files() \
|
||||
.get(fileId='root').execute()
|
||||
|
||||
return roots
|
||||
|
||||
@time_function
|
||||
def _build_tree(self, folders):
|
||||
"""
|
||||
Create in-memory structure resolving paths to folder id as
|
||||
recursive querying might be slower.
|
||||
Initialized in the time of class initialization.
|
||||
Maybe should be persisted
|
||||
Tree is structure of path to id:
|
||||
'/ROOT': {'id': '1234567'}
|
||||
'/ROOT/PROJECT_FOLDER': {'id':'222222'}
|
||||
'/ROOT/PROJECT_FOLDER/Assets': {'id': '3434545'}
|
||||
Args:
|
||||
folders (list): list of dictionaries with folder metadata
|
||||
Returns:
|
||||
(dictionary) path as a key, folder id as a value
|
||||
"""
|
||||
log.debug("build_tree len {}".format(len(folders)))
|
||||
root_ids = []
|
||||
default_root_id = None
|
||||
tree = {}
|
||||
ending_by = {}
|
||||
for root_name, root in self.root.items(): # might be multiple roots
|
||||
if root["id"] not in root_ids:
|
||||
tree["/" + root_name] = {"id": root["id"]}
|
||||
ending_by[root["id"]] = "/" + root_name
|
||||
root_ids.append(root["id"])
|
||||
|
||||
if self.MY_DRIVE_STR == root_name:
|
||||
default_root_id = root["id"]
|
||||
|
||||
no_parents_yet = {}
|
||||
while folders:
|
||||
folder = folders.pop(0)
|
||||
parents = folder.get("parents", [])
|
||||
# weird cases, shared folders, etc, parent under root
|
||||
if not parents:
|
||||
parent = default_root_id
|
||||
else:
|
||||
parent = parents[0]
|
||||
|
||||
if folder["id"] in root_ids: # do not process root
|
||||
continue
|
||||
|
||||
if parent in ending_by:
|
||||
path_key = ending_by[parent] + "/" + folder["name"]
|
||||
ending_by[folder["id"]] = path_key
|
||||
tree[path_key] = {"id": folder["id"]}
|
||||
else:
|
||||
no_parents_yet.setdefault(parent, []).append((folder["id"],
|
||||
folder["name"]))
|
||||
loop_cnt = 0
|
||||
# break if looped more then X times - safety against infinite loop
|
||||
while no_parents_yet and loop_cnt < 20:
|
||||
|
||||
keys = list(no_parents_yet.keys())
|
||||
for parent in keys:
|
||||
if parent in ending_by.keys():
|
||||
subfolders = no_parents_yet.pop(parent)
|
||||
for folder_id, folder_name in subfolders:
|
||||
path_key = ending_by[parent] + "/" + folder_name
|
||||
ending_by[folder_id] = path_key
|
||||
tree[path_key] = {"id": folder_id}
|
||||
loop_cnt += 1
|
||||
|
||||
if len(no_parents_yet) > 0:
|
||||
log.debug("Some folders path are not resolved {}".
|
||||
format(no_parents_yet))
|
||||
log.debug("Remove deleted folders from trash.")
|
||||
|
||||
return tree
|
||||
|
||||
def _get_folder_metadata(self, path):
|
||||
"""
|
||||
Get info about folder with 'path'
|
||||
Args:
|
||||
path (string):
|
||||
|
||||
Returns:
|
||||
(dictionary) with metadata or raises ValueError
|
||||
"""
|
||||
try:
|
||||
return path.format(**root_config)
|
||||
except KeyError:
|
||||
msg = "Error in resolving remote root, unknown key"
|
||||
log.error(msg)
|
||||
return self.get_tree()[path]
|
||||
except Exception:
|
||||
raise ValueError("Uknown folder id {}".format(id))
|
||||
|
||||
def _handle_q(self, q, trashed=False):
|
||||
""" API list call contain trashed and hidden files/folder by default.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
from enum import Enum
|
||||
from .gdrive import GDriveHandler
|
||||
from .local_drive import LocalDriveHandler
|
||||
|
||||
|
|
@ -25,7 +24,8 @@ class ProviderFactory:
|
|||
"""
|
||||
self.providers[provider] = (creator, batch_limit)
|
||||
|
||||
def get_provider(self, provider, site_name, tree=None, presets=None):
|
||||
def get_provider(self, provider, project_name, site_name,
|
||||
tree=None, presets=None):
|
||||
"""
|
||||
Returns new instance of provider client for specific site.
|
||||
One provider could have multiple sites.
|
||||
|
|
@ -37,6 +37,7 @@ class ProviderFactory:
|
|||
provider (string): 'gdrive','S3'
|
||||
site_name (string): descriptor of site, different service accounts
|
||||
must have different site name
|
||||
project_name (string): different projects could have diff. sites
|
||||
tree (dictionary): - folder paths to folder id structure
|
||||
presets (dictionary): config for provider and site (eg.
|
||||
"credentials_url"..)
|
||||
|
|
@ -44,7 +45,8 @@ class ProviderFactory:
|
|||
(implementation of AbstractProvider)
|
||||
"""
|
||||
creator_info = self._get_creator_info(provider)
|
||||
site = creator_info[0](site_name, tree, presets) # call init
|
||||
# call init
|
||||
site = creator_info[0](project_name, site_name, tree, presets)
|
||||
|
||||
return site
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import shutil
|
|||
import threading
|
||||
import time
|
||||
|
||||
from openpype.api import Logger
|
||||
from openpype.api import Logger, Anatomy
|
||||
from .abstract_provider import AbstractProvider
|
||||
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
|
@ -12,6 +12,14 @@ log = Logger().get_logger("SyncServer")
|
|||
|
||||
class LocalDriveHandler(AbstractProvider):
|
||||
""" Handles required operations on mounted disks with OS """
|
||||
def __init__(self, project_name, site_name, tree=None, presets=None):
|
||||
self.presets = None
|
||||
self.active = False
|
||||
self.project_name = project_name
|
||||
self.site_name = site_name
|
||||
|
||||
self.active = self.is_active()
|
||||
|
||||
def is_active(self):
|
||||
return True
|
||||
|
||||
|
|
@ -82,27 +90,37 @@ class LocalDriveHandler(AbstractProvider):
|
|||
os.makedirs(folder_path, exist_ok=True)
|
||||
return folder_path
|
||||
|
||||
def get_roots_config(self, anatomy=None):
|
||||
"""
|
||||
Returns root values for path resolving
|
||||
|
||||
Takes value from Anatomy which takes values from Settings
|
||||
overridden by Local Settings
|
||||
|
||||
Returns:
|
||||
(dict) - {"root": {"root": "/My Drive"}}
|
||||
OR
|
||||
{"root": {"root_ONE": "value", "root_TWO":"value}}
|
||||
Format is importing for usage of python's format ** approach
|
||||
"""
|
||||
if not anatomy:
|
||||
anatomy = Anatomy(self.project_name,
|
||||
self._normalize_site_name(self.site_name))
|
||||
|
||||
return {'root': anatomy.roots}
|
||||
|
||||
def get_tree(self):
|
||||
return
|
||||
|
||||
def resolve_path(self, path, root_config, anatomy=None):
|
||||
if root_config and not root_config.get("root"):
|
||||
root_config = {"root": root_config}
|
||||
def get_configurable_items_for_site(self):
|
||||
"""
|
||||
Returns list of items that should be configurable by User
|
||||
|
||||
try:
|
||||
if not root_config:
|
||||
raise KeyError
|
||||
|
||||
path = path.format(**root_config)
|
||||
except KeyError:
|
||||
try:
|
||||
path = anatomy.fill_root(path)
|
||||
except KeyError:
|
||||
msg = "Error in resolving local root from anatomy"
|
||||
log.error(msg)
|
||||
raise ValueError(msg)
|
||||
|
||||
return path
|
||||
Returns:
|
||||
(list of dict)
|
||||
[{key:"root", label:"root", value:"valueFromSettings"}]
|
||||
"""
|
||||
pass
|
||||
|
||||
def _copy(self, source_path, target_path):
|
||||
print("copying {}->{}".format(source_path, target_path))
|
||||
|
|
@ -133,3 +151,9 @@ class LocalDriveHandler(AbstractProvider):
|
|||
)
|
||||
target_file_size = os.path.getsize(target_path)
|
||||
time.sleep(0.5)
|
||||
|
||||
def _normalize_site_name(self, site_name):
|
||||
"""Transform user id to 'local' for Local settings"""
|
||||
if site_name != 'studio':
|
||||
return 'local'
|
||||
return site_name
|
||||
|
|
|
|||
1193
openpype/modules/sync_server/sync_server_module.py
Normal file
52
openpype/modules/sync_server/tray/lib.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from Qt import QtCore
|
||||
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
STATUS = {
|
||||
0: 'In Progress',
|
||||
1: 'Queued',
|
||||
2: 'Failed',
|
||||
3: 'Paused',
|
||||
4: 'Synced OK',
|
||||
-1: 'Not available'
|
||||
}
|
||||
|
||||
DUMMY_PROJECT = "No project configured"
|
||||
|
||||
ProviderRole = QtCore.Qt.UserRole + 2
|
||||
ProgressRole = QtCore.Qt.UserRole + 4
|
||||
DateRole = QtCore.Qt.UserRole + 6
|
||||
FailedRole = QtCore.Qt.UserRole + 8
|
||||
|
||||
|
||||
def pretty_size(value, suffix='B'):
|
||||
for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']:
|
||||
if abs(value) < 1024.0:
|
||||
return "%3.1f%s%s" % (value, unit, suffix)
|
||||
value /= 1024.0
|
||||
return "%.1f%s%s" % (value, 'Yi', suffix)
|
||||
|
||||
|
||||
def convert_progress(value):
|
||||
try:
|
||||
progress = float(value)
|
||||
except (ValueError, TypeError):
|
||||
progress = 0.0
|
||||
|
||||
return progress
|
||||
|
||||
|
||||
def translate_provider_for_icon(sync_server, project, site):
|
||||
"""
|
||||
Get provider for 'site'
|
||||
|
||||
This is used for getting icon, 'studio' should have different icon
|
||||
then local sites, even the provider 'local_drive' is same
|
||||
|
||||
"""
|
||||
if site == sync_server.DEFAULT_SITE:
|
||||
return sync_server.DEFAULT_SITE
|
||||
return sync_server.get_provider_for_site(project, site)
|
||||
1124
openpype/modules/sync_server/tray/models.py
Normal file
820
openpype/modules/sync_server/tray/widgets.py
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from Qt import QtWidgets, QtCore, QtGui
|
||||
from Qt.QtCore import Qt
|
||||
|
||||
from openpype.tools.settings import (
|
||||
ProjectListWidget,
|
||||
style
|
||||
)
|
||||
|
||||
from openpype.api import get_local_site_id
|
||||
from openpype.lib import PypeLogger
|
||||
|
||||
from avalon.tools.delegates import pretty_timestamp
|
||||
|
||||
from openpype.modules.sync_server.tray.models import (
|
||||
SyncRepresentationSummaryModel,
|
||||
SyncRepresentationDetailModel
|
||||
)
|
||||
|
||||
from openpype.modules.sync_server.tray import lib
|
||||
|
||||
log = PypeLogger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class SyncProjectListWidget(ProjectListWidget):
|
||||
"""
|
||||
Lists all projects that are synchronized to choose from
|
||||
"""
|
||||
|
||||
def __init__(self, sync_server, parent):
|
||||
super(SyncProjectListWidget, self).__init__(parent)
|
||||
self.sync_server = sync_server
|
||||
self.project_list.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.project_list.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
self.project_name = None
|
||||
self.local_site = None
|
||||
self.icons = {}
|
||||
|
||||
def validate_context_change(self):
|
||||
return True
|
||||
|
||||
def refresh(self):
|
||||
model = self.project_list.model()
|
||||
model.clear()
|
||||
|
||||
project_name = None
|
||||
for project_name in self.sync_server.sync_project_settings.\
|
||||
keys():
|
||||
if self.sync_server.is_paused() or \
|
||||
self.sync_server.is_project_paused(project_name):
|
||||
icon = self._get_icon("paused")
|
||||
else:
|
||||
icon = self._get_icon("synced")
|
||||
|
||||
model.appendRow(QtGui.QStandardItem(icon, project_name))
|
||||
|
||||
if len(self.sync_server.sync_project_settings.keys()) == 0:
|
||||
model.appendRow(QtGui.QStandardItem(lib.DUMMY_PROJECT))
|
||||
|
||||
self.current_project = self.project_list.currentIndex().data(
|
||||
QtCore.Qt.DisplayRole
|
||||
)
|
||||
if not self.current_project:
|
||||
self.current_project = self.project_list.model().item(0). \
|
||||
data(QtCore.Qt.DisplayRole)
|
||||
|
||||
if project_name:
|
||||
self.local_site = self.sync_server.get_active_site(project_name)
|
||||
|
||||
def _get_icon(self, status):
|
||||
if not self.icons.get(status):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, status)
|
||||
icon = QtGui.QIcon(pix_url)
|
||||
self.icons[status] = icon
|
||||
else:
|
||||
icon = self.icons[status]
|
||||
return icon
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
point_index = self.project_list.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.project_name = point_index.data(QtCore.Qt.DisplayRole)
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
|
||||
if self.sync_server.is_project_paused(self.project_name):
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
else:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.local_site == get_local_site_id():
|
||||
action = QtWidgets.QAction("Clear local project")
|
||||
actions_mapping[action] = self._clear_project
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
if to_run:
|
||||
to_run()
|
||||
|
||||
def _pause(self):
|
||||
if self.project_name:
|
||||
self.sync_server.pause_project(self.project_name)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
def _unpause(self):
|
||||
if self.project_name:
|
||||
self.sync_server.unpause_project(self.project_name)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
def _clear_project(self):
|
||||
if self.project_name:
|
||||
self.sync_server.clear_project(self.project_name, self.local_site)
|
||||
self.project_name = None
|
||||
self.refresh()
|
||||
|
||||
|
||||
class SyncRepresentationWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Summary dialog with list of representations that matches current
|
||||
settings 'local_site' and 'remote_site'.
|
||||
"""
|
||||
active_changed = QtCore.Signal() # active index changed
|
||||
message_generated = QtCore.Signal(str)
|
||||
|
||||
default_widths = (
|
||||
("asset", 220),
|
||||
("subset", 190),
|
||||
("version", 55),
|
||||
("representation", 95),
|
||||
("local_site", 170),
|
||||
("remote_site", 170),
|
||||
("files_count", 50),
|
||||
("files_size", 60),
|
||||
("priority", 50),
|
||||
("state", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, project=None, parent=None):
|
||||
super(SyncRepresentationWidget, self).__init__(parent)
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
self._selected_id = None # keep last selected _id
|
||||
self.representation_id = None
|
||||
self.site_name = None # to pause/unpause representation
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representations..")
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationSummaryModel(sync_server, headers, project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QtWidgets.QAbstractItemView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(
|
||||
-1, Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.table_view.doubleClicked.connect(self._double_clicked)
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
def _selection_changed(self, _new_selection):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = \
|
||||
self.table_view.model().data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
Sets selection to 'self._selected_id' if exists.
|
||||
|
||||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
self.selection_model.setCurrentIndex(index, mode)
|
||||
else:
|
||||
self._selected_id = None
|
||||
|
||||
def _double_clicked(self, index):
|
||||
"""
|
||||
Opens representation dialog with all files after doubleclick
|
||||
"""
|
||||
_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
detail_window = SyncServerDetailWindow(
|
||||
self.sync_server, _id, self.table_view.model().project)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
"""
|
||||
Shows menu with loader actions on Right-click.
|
||||
"""
|
||||
point_index = self.table_view.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
self.representation_id = self.item._id
|
||||
log.debug("menu representation _id:: {}".
|
||||
format(self.representation_id))
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
local_site = self.item.local_site
|
||||
local_progress = self.item.local_progress
|
||||
remote_site = self.item.remote_site
|
||||
remote_progress = self.item.remote_progress
|
||||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
if 'studio' in site:
|
||||
txt = " studio version"
|
||||
else:
|
||||
txt = " local version"
|
||||
action = QtWidgets.QAction("Open in explorer" + txt)
|
||||
if progress == 1.0:
|
||||
actions_mapping[action] = self._open_in_explorer
|
||||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
# progress smaller then 1.0 --> in progress or queued
|
||||
if local_progress < 1.0:
|
||||
self.site_name = local_site
|
||||
else:
|
||||
self.site_name = remote_site
|
||||
|
||||
if self.item.state in [lib.STATUS[0], lib.STATUS[1]]:
|
||||
action = QtWidgets.QAction("Pause")
|
||||
actions_mapping[action] = self._pause
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[3]:
|
||||
action = QtWidgets.QAction("Unpause")
|
||||
actions_mapping[action] = self._unpause
|
||||
menu.addAction(action)
|
||||
|
||||
# if self.item.state == lib.STATUS[1]:
|
||||
# action = QtWidgets.QAction("Open error detail")
|
||||
# actions_mapping[action] = self._show_detail
|
||||
# menu.addAction(action)
|
||||
|
||||
if remote_progress == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync Active site")
|
||||
actions_mapping[action] = self._reset_local_site
|
||||
menu.addAction(action)
|
||||
|
||||
if local_progress == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync Remote site")
|
||||
actions_mapping[action] = self._reset_remote_site
|
||||
menu.addAction(action)
|
||||
|
||||
if local_site != self.sync_server.DEFAULT_SITE:
|
||||
action = QtWidgets.QAction("Completely remove from local")
|
||||
actions_mapping[action] = self._remove_site
|
||||
menu.addAction(action)
|
||||
else:
|
||||
action = QtWidgets.QAction("Mark for sync to local")
|
||||
actions_mapping[action] = self._add_site
|
||||
menu.addAction(action)
|
||||
|
||||
if not actions_mapping:
|
||||
action = QtWidgets.QAction("< No action >")
|
||||
actions_mapping[action] = None
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
to_run_kwargs = actions_kwargs_mapping.get(result, {})
|
||||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
self.table_view.model().refresh()
|
||||
|
||||
def _pause(self):
|
||||
self.sync_server.pause_representation(self.table_view.model().project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
self.message_generated.emit("Paused {}".format(self.representation_id))
|
||||
|
||||
def _unpause(self):
|
||||
self.sync_server.unpause_representation(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
self.site_name)
|
||||
self.site_name = None
|
||||
self.message_generated.emit("Unpaused {}".format(
|
||||
self.representation_id))
|
||||
|
||||
# temporary here for testing, will be removed TODO
|
||||
def _add_site(self):
|
||||
log.info(self.representation_id)
|
||||
project_name = self.table_view.model().project
|
||||
local_site_name = get_local_site_id()
|
||||
try:
|
||||
self.sync_server.add_site(
|
||||
project_name,
|
||||
self.representation_id,
|
||||
local_site_name
|
||||
)
|
||||
self.message_generated.emit(
|
||||
"Site {} added for {}".format(local_site_name,
|
||||
self.representation_id))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
|
||||
def _remove_site(self):
|
||||
"""
|
||||
Removes site record AND files.
|
||||
|
||||
This is ONLY for representations stored on local site, which
|
||||
cannot be same as SyncServer.DEFAULT_SITE.
|
||||
|
||||
This could only happen when artist work on local machine, not
|
||||
connected to studio mounted drives.
|
||||
"""
|
||||
log.info("Removing {}".format(self.representation_id))
|
||||
try:
|
||||
local_site = get_local_site_id()
|
||||
self.sync_server.remove_site(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
local_site,
|
||||
True)
|
||||
self.message_generated.emit("Site {} removed".format(local_site))
|
||||
except ValueError as exp:
|
||||
self.message_generated.emit("Error {}".format(str(exp)))
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'local')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'remote')
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.table_view.model().project
|
||||
fpath = self.sync_server.get_local_file_path(project,
|
||||
site,
|
||||
fpath)
|
||||
|
||||
fpath = os.path.normpath(os.path.dirname(fpath))
|
||||
if os.path.isdir(fpath):
|
||||
if 'win' in sys.platform: # windows
|
||||
subprocess.Popen('explorer "%s"' % fpath)
|
||||
elif sys.platform == 'darwin': # macOS
|
||||
subprocess.Popen(['open', fpath])
|
||||
else: # linux
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', fpath])
|
||||
except OSError:
|
||||
raise OSError('unsupported xdg-open call??')
|
||||
|
||||
def _save_scrollbar(self):
|
||||
self._scrollbar_pos = self.table_view.verticalScrollBar().value()
|
||||
|
||||
def _set_scrollbar(self):
|
||||
if self._scrollbar_pos:
|
||||
self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)
|
||||
|
||||
|
||||
class SyncRepresentationDetailWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Widget to display list of synchronizable files for single repre.
|
||||
|
||||
Args:
|
||||
_id (str): representation _id
|
||||
project (str): name of project with repre
|
||||
parent (QDialog): SyncServerDetailWindow
|
||||
"""
|
||||
active_changed = QtCore.Signal() # active index changed
|
||||
|
||||
default_widths = (
|
||||
("file", 290),
|
||||
("local_site", 185),
|
||||
("remote_site", 185),
|
||||
("size", 60),
|
||||
("priority", 25),
|
||||
("state", 110)
|
||||
)
|
||||
|
||||
def __init__(self, sync_server, _id=None, project=None, parent=None):
|
||||
super(SyncRepresentationDetailWidget, self).__init__(parent)
|
||||
|
||||
log.debug("Representation_id:{}".format(_id))
|
||||
self.representation_id = _id
|
||||
self.item = None # set to item that mouse was clicked over
|
||||
self.project = project
|
||||
|
||||
self.sync_server = sync_server
|
||||
|
||||
self._selected_id = None
|
||||
|
||||
self.filter = QtWidgets.QLineEdit()
|
||||
self.filter.setPlaceholderText("Filter representation..")
|
||||
|
||||
self._scrollbar_pos = None
|
||||
|
||||
top_bar_layout = QtWidgets.QHBoxLayout()
|
||||
top_bar_layout.addWidget(self.filter)
|
||||
|
||||
self.table_view = QtWidgets.QTableView()
|
||||
headers = [item[0] for item in self.default_widths]
|
||||
|
||||
model = SyncRepresentationDetailModel(sync_server, headers, _id,
|
||||
project)
|
||||
self.table_view.setModel(model)
|
||||
self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
||||
self.table_view.setSelectionMode(
|
||||
QtWidgets.QAbstractItemView.SingleSelection)
|
||||
self.table_view.setSelectionBehavior(
|
||||
QtWidgets.QTableView.SelectRows)
|
||||
self.table_view.horizontalHeader().setSortIndicator(-1,
|
||||
Qt.AscendingOrder)
|
||||
self.table_view.setSortingEnabled(True)
|
||||
self.table_view.horizontalHeader().setSortIndicatorShown(True)
|
||||
self.table_view.setAlternatingRowColors(True)
|
||||
self.table_view.verticalHeader().hide()
|
||||
|
||||
column = self.table_view.model().get_header_index("local_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
column = self.table_view.model().get_header_index("remote_site")
|
||||
delegate = ImageDelegate(self)
|
||||
self.table_view.setItemDelegateForColumn(column, delegate)
|
||||
|
||||
for column_name, width in self.default_widths:
|
||||
idx = model.get_header_index(column_name)
|
||||
self.table_view.setColumnWidth(idx, width)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.addLayout(top_bar_layout)
|
||||
layout.addWidget(self.table_view)
|
||||
|
||||
self.filter.textChanged.connect(lambda: model.set_filter(
|
||||
self.filter.text()))
|
||||
self.table_view.customContextMenuRequested.connect(
|
||||
self._on_context_menu)
|
||||
|
||||
model.refresh_started.connect(self._save_scrollbar)
|
||||
model.refresh_finished.connect(self._set_scrollbar)
|
||||
self.table_view.model().modelReset.connect(self._set_selection)
|
||||
|
||||
self.selection_model = self.table_view.selectionModel()
|
||||
self.selection_model.selectionChanged.connect(self._selection_changed)
|
||||
|
||||
def _selection_changed(self):
|
||||
index = self.selection_model.currentIndex()
|
||||
self._selected_id = self.table_view.model().data(index, Qt.UserRole)
|
||||
|
||||
def _set_selection(self):
|
||||
"""
|
||||
Sets selection to 'self._selected_id' if exists.
|
||||
|
||||
Keep selection during model refresh.
|
||||
"""
|
||||
if self._selected_id:
|
||||
index = self.table_view.model().get_index(self._selected_id)
|
||||
if index and index.isValid():
|
||||
mode = QtCore.QItemSelectionModel.Select | \
|
||||
QtCore.QItemSelectionModel.Rows
|
||||
self.selection_model.setCurrentIndex(index, mode)
|
||||
else:
|
||||
self._selected_id = None
|
||||
|
||||
def _show_detail(self):
|
||||
"""
|
||||
Shows windows with error message for failed sync of a file.
|
||||
"""
|
||||
dt = max(self.item.created_dt, self.item.sync_dt)
|
||||
detail_window = SyncRepresentationErrorWindow(self.item._id,
|
||||
self.project,
|
||||
dt,
|
||||
self.item.tries,
|
||||
self.item.error)
|
||||
detail_window.exec()
|
||||
|
||||
def _on_context_menu(self, point):
|
||||
"""
|
||||
Shows menu with loader actions on Right-click.
|
||||
"""
|
||||
point_index = self.table_view.indexAt(point)
|
||||
if not point_index.isValid():
|
||||
return
|
||||
|
||||
self.item = self.table_view.model()._data[point_index.row()]
|
||||
|
||||
menu = QtWidgets.QMenu()
|
||||
menu.setStyleSheet(style.load_stylesheet())
|
||||
actions_mapping = {}
|
||||
actions_kwargs_mapping = {}
|
||||
|
||||
local_site = self.item.local_site
|
||||
local_progress = self.item.local_progress
|
||||
remote_site = self.item.remote_site
|
||||
remote_progress = self.item.remote_progress
|
||||
|
||||
for site, progress in {local_site: local_progress,
|
||||
remote_site: remote_progress}.items():
|
||||
project = self.table_view.model().project
|
||||
provider = self.sync_server.get_provider_for_site(project,
|
||||
site)
|
||||
if provider == 'local_drive':
|
||||
if 'studio' in site:
|
||||
txt = " studio version"
|
||||
else:
|
||||
txt = " local version"
|
||||
action = QtWidgets.QAction("Open in explorer" + txt)
|
||||
if progress == 1:
|
||||
actions_mapping[action] = self._open_in_explorer
|
||||
actions_kwargs_mapping[action] = {'site': site}
|
||||
menu.addAction(action)
|
||||
|
||||
if self.item.state == lib.STATUS[2]:
|
||||
action = QtWidgets.QAction("Open error detail")
|
||||
actions_mapping[action] = self._show_detail
|
||||
menu.addAction(action)
|
||||
|
||||
if float(remote_progress) == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync active site")
|
||||
actions_mapping[action] = self._reset_local_site
|
||||
menu.addAction(action)
|
||||
|
||||
if float(local_progress) == 1.0:
|
||||
action = QtWidgets.QAction("Re-sync remote site")
|
||||
actions_mapping[action] = self._reset_remote_site
|
||||
menu.addAction(action)
|
||||
|
||||
if not actions_mapping:
|
||||
action = QtWidgets.QAction("< No action >")
|
||||
actions_mapping[action] = None
|
||||
menu.addAction(action)
|
||||
|
||||
result = menu.exec_(QtGui.QCursor.pos())
|
||||
if result:
|
||||
to_run = actions_mapping[result]
|
||||
to_run_kwargs = actions_kwargs_mapping.get(result, {})
|
||||
if to_run:
|
||||
to_run(**to_run_kwargs)
|
||||
|
||||
def _reset_local_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'local',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _reset_remote_site(self):
|
||||
"""
|
||||
Removes errors or success metadata for particular file >> forces
|
||||
redo of upload/download
|
||||
"""
|
||||
self.sync_server.reset_provider_for_file(
|
||||
self.table_view.model().project,
|
||||
self.representation_id,
|
||||
'remote',
|
||||
self.item._id)
|
||||
self.table_view.model().refresh(
|
||||
load_records=self.table_view.model()._rec_loaded)
|
||||
|
||||
def _open_in_explorer(self, site):
|
||||
if not self.item:
|
||||
return
|
||||
|
||||
fpath = self.item.path
|
||||
project = self.project
|
||||
fpath = self.sync_server.get_local_file_path(project, site, fpath)
|
||||
|
||||
fpath = os.path.normpath(os.path.dirname(fpath))
|
||||
if os.path.isdir(fpath):
|
||||
if 'win' in sys.platform: # windows
|
||||
subprocess.Popen('explorer "%s"' % fpath)
|
||||
elif sys.platform == 'darwin': # macOS
|
||||
subprocess.Popen(['open', fpath])
|
||||
else: # linux
|
||||
try:
|
||||
subprocess.Popen(['xdg-open', fpath])
|
||||
except OSError:
|
||||
raise OSError('unsupported xdg-open call??')
|
||||
|
||||
def _save_scrollbar(self):
|
||||
self._scrollbar_pos = self.table_view.verticalScrollBar().value()
|
||||
|
||||
def _set_scrollbar(self):
|
||||
if self._scrollbar_pos:
|
||||
self.table_view.verticalScrollBar().setValue(self._scrollbar_pos)
|
||||
|
||||
|
||||
class SyncRepresentationErrorWidget(QtWidgets.QWidget):
|
||||
"""
|
||||
Dialog to show when sync error happened, prints error message
|
||||
"""
|
||||
|
||||
def __init__(self, _id, dt, tries, msg, parent=None):
|
||||
super(SyncRepresentationErrorWidget, self).__init__(parent)
|
||||
|
||||
layout = QtWidgets.QHBoxLayout(self)
|
||||
|
||||
txts = []
|
||||
txts.append("{}: {}".format("Last update date", pretty_timestamp(dt)))
|
||||
txts.append("{}: {}".format("Retries", str(tries)))
|
||||
txts.append("{}: {}".format("Error message", msg))
|
||||
|
||||
text_area = QtWidgets.QPlainTextEdit("\n\n".join(txts))
|
||||
text_area.setReadOnly(True)
|
||||
layout.addWidget(text_area)
|
||||
|
||||
|
||||
class ImageDelegate(QtWidgets.QStyledItemDelegate):
|
||||
"""
|
||||
Prints icon of site and progress of synchronization
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super(ImageDelegate, self).__init__(parent)
|
||||
self.icons = {}
|
||||
|
||||
def paint(self, painter, option, index):
|
||||
super(ImageDelegate, self).paint(painter, option, index)
|
||||
option = QtWidgets.QStyleOptionViewItem(option)
|
||||
option.showDecorationSelected = True
|
||||
|
||||
provider = index.data(lib.ProviderRole)
|
||||
value = index.data(lib.ProgressRole)
|
||||
date_value = index.data(lib.DateRole)
|
||||
is_failed = index.data(lib.FailedRole)
|
||||
|
||||
if not self.icons.get(provider):
|
||||
resource_path = os.path.dirname(__file__)
|
||||
resource_path = os.path.join(resource_path, "..",
|
||||
"providers", "resources")
|
||||
pix_url = "{}/{}.png".format(resource_path, provider)
|
||||
pixmap = QtGui.QPixmap(pix_url)
|
||||
self.icons[provider] = pixmap
|
||||
else:
|
||||
pixmap = self.icons[provider]
|
||||
|
||||
padding = 10
|
||||
point = QtCore.QPoint(option.rect.x() + padding,
|
||||
option.rect.y() +
|
||||
(option.rect.height() - pixmap.height()) / 2)
|
||||
painter.drawPixmap(point, pixmap)
|
||||
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value)))
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(0, 0, 0, 100)))
|
||||
text_rect = option.rect.translated(10, 0)
|
||||
painter.drawText(text_rect,
|
||||
QtCore.Qt.AlignCenter,
|
||||
date_value)
|
||||
|
||||
if is_failed:
|
||||
overlay_rect = option.rect.translated(0, 0)
|
||||
painter.fillRect(overlay_rect,
|
||||
QtGui.QBrush(QtGui.QColor(255, 0, 0, 35)))
|
||||
|
||||
|
||||
class SyncServerDetailWindow(QtWidgets.QDialog):
|
||||
def __init__(self, sync_server, _id, project, parent=None):
|
||||
log.debug(
|
||||
"!!! SyncServerDetailWindow _id:: {}".format(_id))
|
||||
super(SyncServerDetailWindow, self).__init__(parent)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
self.resize(1000, 400)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
footer = QtWidgets.QWidget()
|
||||
footer.setFixedHeight(20)
|
||||
|
||||
container = SyncRepresentationDetailWidget(sync_server, _id, project,
|
||||
parent=self)
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
self.message = QtWidgets.QLabel()
|
||||
self.message.hide()
|
||||
|
||||
footer_layout = QtWidgets.QVBoxLayout(footer)
|
||||
footer_layout.addWidget(self.message)
|
||||
footer_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
layout.addWidget(footer)
|
||||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Detail")
|
||||
|
||||
|
||||
class SyncRepresentationErrorWindow(QtWidgets.QDialog):
|
||||
def __init__(self, _id, project, dt, tries, msg, parent=None):
|
||||
super(SyncRepresentationErrorWindow, self).__init__(parent)
|
||||
self.setWindowFlags(QtCore.Qt.Window)
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
|
||||
self.setStyleSheet(style.load_stylesheet())
|
||||
self.setWindowIcon(QtGui.QIcon(style.app_icon_path()))
|
||||
self.resize(900, 150)
|
||||
|
||||
body = QtWidgets.QWidget()
|
||||
|
||||
container = SyncRepresentationErrorWidget(_id, dt, tries, msg,
|
||||
parent=self)
|
||||
body_layout = QtWidgets.QHBoxLayout(body)
|
||||
body_layout.addWidget(container)
|
||||
body_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
message = QtWidgets.QLabel()
|
||||
message.hide()
|
||||
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
layout.addWidget(body)
|
||||
|
||||
self.setLayout(body_layout)
|
||||
self.setWindowTitle("Sync Representation Error Detail")
|
||||
|
|
@ -1,8 +1,14 @@
|
|||
import time
|
||||
from openpype.api import Logger
|
||||
from openpype.api import Logger
|
||||
log = Logger().get_logger("SyncServer")
|
||||
|
||||
|
||||
class SyncStatus:
|
||||
DO_NOTHING = 0
|
||||
DO_UPLOAD = 1
|
||||
DO_DOWNLOAD = 2
|
||||
|
||||
|
||||
def time_function(method):
|
||||
""" Decorator to print how much time function took.
|
||||
For debugging.
|
||||
|
|
|
|||
33
openpype/plugins/load/add_site.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from avalon import api
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
class AddSyncSite(api.Loader):
|
||||
"""Add sync site to representation"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Add Sync Site"
|
||||
order = 2 # lower means better
|
||||
icon = "download"
|
||||
color = "#999999"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
self.log.info("Adding {} to representation: {}".format(
|
||||
data["site_name"], data["_id"]))
|
||||
self.add_site_to_representation(data["project_name"],
|
||||
data["_id"],
|
||||
data["site_name"])
|
||||
self.log.debug("Site added.")
|
||||
|
||||
@staticmethod
|
||||
def add_site_to_representation(project_name, representation_id, site_name):
|
||||
"""Adds new site to representation_id, resets if exists"""
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
sync_server.add_site(project_name, representation_id, site_name,
|
||||
force=True)
|
||||
|
||||
def filepath_from_context(self, context):
|
||||
"""No real file loading"""
|
||||
return ""
|
||||
|
|
@ -15,11 +15,12 @@ from openpype.api import Anatomy
|
|||
|
||||
|
||||
class DeleteOldVersions(api.Loader):
|
||||
|
||||
"""Deletes specific number of old version"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Delete Old Versions"
|
||||
order = 35
|
||||
icon = "trash"
|
||||
color = "#d8d8d8"
|
||||
|
||||
|
|
@ -421,8 +422,9 @@ class DeleteOldVersions(api.Loader):
|
|||
|
||||
|
||||
class CalculateOldVersions(DeleteOldVersions):
|
||||
|
||||
"""Calculate file size of old versions"""
|
||||
label = "Calculate Old Versions"
|
||||
order = 30
|
||||
|
||||
options = [
|
||||
qargparse.Integer(
|
||||
|
|
|
|||
33
openpype/plugins/load/remove_site.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
from avalon import api
|
||||
from openpype.modules import ModulesManager
|
||||
|
||||
|
||||
class RemoveSyncSite(api.Loader):
|
||||
"""Remove sync site and its files on representation"""
|
||||
representations = ["*"]
|
||||
families = ["*"]
|
||||
|
||||
label = "Remove Sync Site"
|
||||
order = 4
|
||||
icon = "download"
|
||||
color = "#999999"
|
||||
|
||||
def load(self, context, name=None, namespace=None, data=None):
|
||||
self.log.info("Removing {} on representation: {}".format(
|
||||
data["site_name"], data["_id"]))
|
||||
self.remove_site_on_representation(data["project_name"],
|
||||
data["_id"],
|
||||
data["site_name"])
|
||||
self.log.debug("Site added.")
|
||||
|
||||
@staticmethod
|
||||
def remove_site_on_representation(project_name, representation_id,
|
||||
site_name):
|
||||
manager = ModulesManager()
|
||||
sync_server = manager.modules_by_name["sync_server"]
|
||||
sync_server.remove_site(project_name, representation_id,
|
||||
site_name, True)
|
||||
|
||||
def filepath_from_context(self, context):
|
||||
"""No real file loading"""
|
||||
return ""
|
||||
|
|
@ -976,6 +976,9 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
local_site = local_site_id
|
||||
|
||||
remote_site = sync_server_presets["config"].get("remote_site")
|
||||
if remote_site == local_site:
|
||||
remote_site = None
|
||||
|
||||
if remote_site == 'local':
|
||||
remote_site = local_site_id
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,14 @@
|
|||
"not ready"
|
||||
]
|
||||
},
|
||||
"prepare_project": {
|
||||
"enabled": true,
|
||||
"role_list": [
|
||||
"Pypeclub",
|
||||
"Administrator",
|
||||
"Project manager"
|
||||
]
|
||||
},
|
||||
"sync_hier_entity_attributes": {
|
||||
"enabled": true,
|
||||
"interest_entity_types": [
|
||||
|
|
@ -195,7 +203,7 @@
|
|||
"publish": {
|
||||
"IntegrateFtrackNote": {
|
||||
"enabled": true,
|
||||
"note_with_intent_template": "",
|
||||
"note_with_intent_template": "{intent}: {comment}",
|
||||
"note_labels": []
|
||||
},
|
||||
"ValidateFtrackAttributes": {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
"ExtractJpegEXR": {
|
||||
"enabled": true,
|
||||
"ffmpeg_args": {
|
||||
"input": [],
|
||||
"input": [
|
||||
"-gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -313,8 +313,8 @@
|
|||
"rendererName": "vp2Renderer"
|
||||
},
|
||||
"Resolution": {
|
||||
"width": 1080,
|
||||
"height": 1920,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"percent": 1.0,
|
||||
"mode": "Custom"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
"ExtractThumbnailSP": {
|
||||
"ffmpeg_args": {
|
||||
"input": [
|
||||
"gamma 2.2"
|
||||
"-gamma 2.2"
|
||||
],
|
||||
"output": []
|
||||
}
|
||||
|
|
|
|||
10
openpype/settings/defaults/project_settings/tvpaint.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"publish": {
|
||||
"ValidateMissingLayers": {
|
||||
"enabled": true,
|
||||
"optional": true,
|
||||
"active": true
|
||||
}
|
||||
},
|
||||
"filters": {}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
"host_name": "maya",
|
||||
"environment": {
|
||||
"PYTHONPATH": [
|
||||
"{OPENPYPE_ROOT}/pype/hosts/maya/startup",
|
||||
"{OPENPYPE_ROOT}/openpype/hosts/maya/startup",
|
||||
"{OPENPYPE_ROOT}/repos/avalon-core/setup/maya",
|
||||
"{OPENPYPE_ROOT}/repos/maya-look-assigner",
|
||||
"{PYTHONPATH}"
|
||||
|
|
@ -715,7 +715,7 @@
|
|||
"{OPENPYPE_ROOT}/repos/avalon-core/setup/blender",
|
||||
"{PYTHONPATH}"
|
||||
],
|
||||
"CREATE_NEW_CONSOLE": "yes"
|
||||
"QT_PREFERRED_BINDING": "PySide2"
|
||||
},
|
||||
"variants": {
|
||||
"2-83": {
|
||||
|
|
|
|||
|
|
@ -82,6 +82,10 @@
|
|||
"type": "schema",
|
||||
"name": "schema_project_harmony"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_tvpaint"
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_project_celaction"
|
||||
|
|
|
|||
|
|
@ -36,6 +36,25 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "prepare_project",
|
||||
"label": "Prepare Project",
|
||||
"checkbox_key": "enabled",
|
||||
"children": [
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "enabled",
|
||||
"label": "Enabled"
|
||||
},
|
||||
{
|
||||
"type": "list",
|
||||
"key": "role_list",
|
||||
"label": "Roles",
|
||||
"object_type": "text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"key": "sync_hier_entity_attributes",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "tvpaint",
|
||||
"label": "TVPaint",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
"key": "publish",
|
||||
"label": "Publish plugins",
|
||||
"is_file": true,
|
||||
"children": [
|
||||
{
|
||||
"type": "schema_template",
|
||||
"name": "template_publish_plugin",
|
||||
"template_data": [
|
||||
{
|
||||
"key": "ValidateMissingLayers",
|
||||
"label": "ValidateMissingLayers"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "schema",
|
||||
"name": "schema_publish_gui_filter"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import os
|
||||
from openpype import resources
|
||||
|
||||
|
||||
def load_stylesheet():
|
||||
|
|
@ -9,4 +10,4 @@ def load_stylesheet():
|
|||
|
||||
|
||||
def app_icon_path():
|
||||
return os.path.join(os.path.dirname(__file__), "openpype_icon.png")
|
||||
return resources.pype_icon_filepath()
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 3.7 KiB |
|
|
@ -1,3 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Package declaring Pype version."""
|
||||
__version__ = "3.0.0-beta"
|
||||
__version__ = "3.0.0-beta2"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "OpenPype"
|
||||
version = "3.0.0-alpha1"
|
||||
version = "3.0.0-beta2"
|
||||
description = "Multi-platform open-source pipeline built around the Avalon platform, expanding it with extra features and integrations."
|
||||
authors = ["OpenPype Team <info@openpype.io>"]
|
||||
license = "MIT License"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit bbba8765c431ee124590e4f12d2e56db4d62eacd
|
||||
Subproject commit 911bd8999ab0030d0f7412dde6fd545c1a73b62d
|
||||
226
website/docs/artist_hosts_blender.md
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
---
|
||||
id: artist_hosts_blender
|
||||
title: Blender
|
||||
sidebar_label: Blender
|
||||
---
|
||||
|
||||
## OpenPype global tools
|
||||
|
||||
- [Set Context](artist_tools.md#set-context)
|
||||
- [Work Files](artist_tools.md#workfiles)
|
||||
- [Create](artist_tools.md#creator)
|
||||
- [Load](artist_tools.md#loader)
|
||||
- [Manage (Inventory)](artist_tools.md#inventory)
|
||||
- [Publish](artist_tools.md#publisher)
|
||||
- [Library Loader](artist_tools.md#library-loader)
|
||||
|
||||
## Working with OpenPype in Blender
|
||||
|
||||
OpenPype is here to ease you the burden of working on project with lots of
|
||||
collaborators, worrying about naming, setting stuff, browsing through endless
|
||||
directories, loading and exporting and so on. To achieve that, OpenPype is using
|
||||
concept of being _"data driven"_. This means that what happens when publishing
|
||||
is influenced by data in scene. This can by slightly confusing so let's get to
|
||||
it with few examples.
|
||||
|
||||
|
||||
## Setting scene data
|
||||
|
||||
Blender settings concerning framerate, resolution and frame range are handled
|
||||
by OpenPype. If set correctly in Ftrack, Blender will automatically set the
|
||||
values for you.
|
||||
|
||||
|
||||
## Publishing models
|
||||
|
||||
### Intro
|
||||
|
||||
Publishing models in Blender is pretty straightforward. Create your model as you
|
||||
need. You might need to adhere to specifications of your studio that can be different
|
||||
between studios and projects but by default your geometry does not need any
|
||||
other convention.
|
||||
|
||||

|
||||
|
||||
### Creating instance
|
||||
|
||||
Now create **Model instance** from it to let OpenPype know what in the scene you want to
|
||||
publish. Go **OpenPype → Create... → Model**.
|
||||
|
||||

|
||||
|
||||
`Asset` field is a name of asset you are working on - it should be already filled
|
||||
with correct name as you've started Blender or switched context to specific asset. You
|
||||
can edit that field to change it to different asset (but that one must already exists).
|
||||
|
||||
`Subset` field is a name you can decide on. It should describe what kind of data you
|
||||
have in the model. For example, you can name it `Proxy` to indicate that this is
|
||||
low resolution stuff. See [Subset](artist_concepts#subset).
|
||||
|
||||
<!-- :::note LOD support
|
||||
By changing subset name you can take advantage of _LOD support_ in OpenPype. Your
|
||||
asset can contain various resolution defined by different subsets. You can then
|
||||
switch between them very easy using [Inventory (Manage)](artist_tools#inventory).
|
||||
There LODs are conveniently grouped so they don't clutter Inventory view.
|
||||
|
||||
Name your subset like `main_LOD1`. Important part is that `_LOD1`. You can have as many LODs as you need.
|
||||
::: -->
|
||||
|
||||
Read-only field just under it show final subset name, adding subset field to
|
||||
name of the group you have selected.
|
||||
|
||||
`Use selection` checkbox will use whatever you have selected in Outliner to be
|
||||
wrapped in Model instance. This is usually what you want. Click on **Create** button.
|
||||
|
||||
You'll notice then after you've created new Model instance, there is a new
|
||||
collection in Outliner called after your asset and subset, in our case it is
|
||||
`character1_modelDefault`. The assets selected when creating the Model instance
|
||||
are linked in the new collection.
|
||||
|
||||
And that's it, you have your first model ready to publish.
|
||||
|
||||
Now save your scene (if you didn't do it already). You will notice that path
|
||||
in Save dialog is already set to place where scenes related to modeling task on
|
||||
your asset should reside. As in our case we are working on asset called
|
||||
**character1** and on task **modeling**, path relative to your project directory will be
|
||||
`project_XY/assets/character1/work/modeling`. The default name for the file will
|
||||
be `project_XY_asset_task_version`, so in our case
|
||||
`simonetest_character1_modeling_v001.blend`. Let's save it.
|
||||
|
||||

|
||||
|
||||
### Publishing models
|
||||
|
||||
Now let's publish it. Go **OpenPype → Publish...**. You will be presented with following window:
|
||||
|
||||

|
||||
|
||||
Note that content of this window can differs by your pipeline configuration.
|
||||
For more detail see [Publisher](artist_tools#publisher).
|
||||
|
||||
Items in left column are instances you will be publishing. You can disable them
|
||||
by clicking on square next to them. White filled square indicate they are ready for
|
||||
publishing, red means something went wrong either during collection phase
|
||||
or publishing phase. Empty one with gray text is disabled.
|
||||
|
||||
See that in this case we are publishing from the scene file
|
||||
`simonetest_character1_modeling_v001.blend` the Blender model named
|
||||
`character1_modelDefault`.
|
||||
|
||||
Right column lists all tasks that are run during collection, validation,
|
||||
extraction and integration phase. White items are optional and you can disable
|
||||
them by clicking on them.
|
||||
|
||||
Lets do dry-run on publishing to see if we pass all validators. Click on flask
|
||||
icon at the bottom. Validators are run. Ideally you will end up with everything
|
||||
green in validator section.
|
||||
|
||||
### Fixing problems
|
||||
|
||||
For the sake of demonstration, I intentionally kept the model in Edit Mode, to
|
||||
trigger the validator designed to check just this.
|
||||
|
||||

|
||||
|
||||
You can see our model is now marked red in left column and in right we have
|
||||
red box next to `Mesh is in Object Mode` validator.
|
||||
|
||||
You can click on arrow next to it to see more details:
|
||||
|
||||

|
||||
|
||||
From there you can see in **Records** entry that there is problem with the
|
||||
object `Suzanne`. Some validators have option to fix problem for you or just
|
||||
select objects that cause trouble. This is the case with our failed validator.
|
||||
|
||||
In main overview you can notice little A in a circle next to validator
|
||||
name. Right click on it and you can see menu item `select invalid`. This
|
||||
will select offending object in Blender.
|
||||
|
||||
Fix is easy. Without closing Publisher window we just turn back the Object Mode.
|
||||
Then we need to reset it to make it notice changes we've made. Click on arrow
|
||||
circle button at the bottom and it will reset the Publisher to initial state. Run
|
||||
validators again (flask icon) to see if everything is ok.
|
||||
|
||||
It should OK be now. Write some comment if you want and click play icon button
|
||||
when ready.
|
||||
|
||||
Publish process will now take its course. Depending on data you are publishing
|
||||
it can take a while. You should end up with everything green and message
|
||||
**Finished successfully ...** You can now close publisher window.
|
||||
|
||||
To check for yourself that model is published, open
|
||||
[Asset Loader](artist_tools#loader) - **OpenPype → Load...**.
|
||||
There you should see your model, named `modelDefault`.
|
||||
|
||||
### Loading models
|
||||
|
||||
You can load model with [Loader](artist_tools.md#loader). Go **OpenPype → Load...**,
|
||||
select your rig, right click on it and click **Link model (blend)**.
|
||||
|
||||
## Creating Rigs
|
||||
|
||||
Creating and publishing rigs with OpenPype follows similar workflow as with
|
||||
other data types. Create your rig and mark parts of your hierarchy in sets to
|
||||
help OpenPype validators and extractors to check it and publish it.
|
||||
|
||||
### Preparing rig for publish
|
||||
|
||||
When creating rigs in Blender, it is important to keep a specific structure for
|
||||
the bones and the geometry. Let's first create a model and its rig. For
|
||||
demonstration, I'll create a simple model for a robotic arm made of simple boxes.
|
||||
|
||||

|
||||
|
||||
I have now created the armature `RIG_RobotArm`. While the naming is not important,
|
||||
you can just adhere to your naming conventions, the hierarchy is. Once the models
|
||||
are skinned to the armature, the geometry must be organized in a separate Collection.
|
||||
In this case, I have the armature in the main Collection, and the geometry in
|
||||
the `Geometry` Collection.
|
||||
|
||||

|
||||
|
||||
When you've prepared your hierarchy, it's time to create *Rig instance* in OpenPype.
|
||||
Select your whole rig hierarchy and go **OpenPype → Create...**. Select **Rig**.
|
||||
|
||||

|
||||
|
||||
A new collection named after the selected Asset and Subset should have been created.
|
||||
In our case, it is `character1_rigDefault`. All the selected armature and models
|
||||
have been linked in this new collection. You should end up with something like
|
||||
this:
|
||||
|
||||

|
||||
|
||||
### Publishing rigs
|
||||
|
||||
Publishing rig is done in same way as publishing everything else. Save your scene
|
||||
and go **OpenPype → Publish**. For more detail see [Publisher](artist_tools#publisher).
|
||||
|
||||
### Loading rigs
|
||||
|
||||
You can load rig with [Loader](artist_tools.md#loader). Go **OpenPype → Load...**,
|
||||
select your rig, right click on it and click **Link rig (blend)**.
|
||||
|
||||
## Layouts in Blender
|
||||
|
||||
A layout is a set of elements that populate a scene. OpenPype allows to version
|
||||
and manage those sets.
|
||||
|
||||
### Publishing a layout
|
||||
|
||||
Working with Layout is easy. Just load your assets into scene with
|
||||
[Loader](artist_tools.md#loader) (**OpenPype → Load...**). Populate your scene as
|
||||
you wish, translate each piece to fit your need. When ready, select all imported
|
||||
stuff and go **OpenPype → Create...** and select **Layout**. When selecting rigs,
|
||||
you need to select only the armature, the geometry will automatically be included.
|
||||
This will create set containing your selection and marking it for publishing.
|
||||
|
||||
Now you can publish is with **OpenPype → Publish**.
|
||||
|
||||
### Loading layouts
|
||||
|
||||
You can load a Layout using [Loader](artist_tools.md#loader)
|
||||
(**OpenPype → Load...**). Select your layout, right click on it and
|
||||
select **Link Layout (blend)**. This will populate your scene with all those
|
||||
models you've put into layout.
|
||||
BIN
website/docs/assets/blender-model_create_instance.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
website/docs/assets/blender-model_error_details.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
website/docs/assets/blender-model_example.jpg
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
website/docs/assets/blender-model_pre_publish.jpg
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
website/docs/assets/blender-model_publish_error.jpg
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
website/docs/assets/blender-rig_create.jpg
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
website/docs/assets/blender-rig_hierarchy_before_publish.jpg
Normal file
|
After Width: | Height: | Size: 106 KiB |
BIN
website/docs/assets/blender-rig_hierarchy_example.jpg
Normal file
|
After Width: | Height: | Size: 113 KiB |
BIN
website/docs/assets/blender-rig_model_setup.jpg
Normal file
|
After Width: | Height: | Size: 60 KiB |
BIN
website/docs/assets/blender-save_modelling_file.jpg
Normal file
|
After Width: | Height: | Size: 57 KiB |
|
|
@ -19,6 +19,7 @@ module.exports = {
|
|||
"artist_hosts_nukestudio",
|
||||
"artist_hosts_nuke",
|
||||
"artist_hosts_maya",
|
||||
"artist_hosts_blender",
|
||||
"artist_hosts_harmony",
|
||||
"artist_hosts_aftereffects",
|
||||
"artist_hosts_photoshop",
|
||||
|
|
|
|||