def _load_step_and_service(workflow: Workflow, step_id: int, param: str) -> Tuple[Step, oauth.OAuthService]: """Load Step and OAuthService from the database, or raise. Raise Step.DoesNotExist if the Step is deleted or missing. Raise SecretDoesNotExist if the Step does not have the given param. Invoke this within a Workflow.cooperative_lock(). """ # raises Step.DoesNotExist step = Step.live_in_workflow(workflow).get(pk=step_id) # raises KeyError, RuntimeError try: module_zipfile = MODULE_REGISTRY.latest(step.module_id_name) except KeyError: raise SecretDoesNotExist( f"Module {step.module_id_name} does not exist") module_spec = module_zipfile.get_spec() for field in module_spec.param_fields: if (isinstance(field, ParamField.Secret) and field.id_name == param and (isinstance(field.secret_logic, ParamField.Secret.Logic.Oauth1a) or isinstance(field.secret_logic, ParamField.Secret.Logic.Oauth2))): service_name = field.secret_logic.service service = oauth.OAuthService.lookup_or_none(service_name) if service is None: raise OauthServiceNotConfigured( f'OAuth not configured for "{service_name}" service') return step, service else: raise SecretDoesNotExist( f"Param {param} does not point to an OAuth secret")
def _get_workflow_as_clientside_update(user, session, workflow_id: int) -> WorkflowUpdateData: """ Return (clientside.Update, delta_id). Raise Workflow.DoesNotExist if a race deletes the Workflow. The purpose of this method is to hide races from users who disconnect and reconnect while changes are being made. It's okay for things to be slightly off, as long as users don't notice. (Long-term, we can build better a more-correct synchronization strategy.) """ with Workflow.authorized_lookup_and_cooperative_lock( "read", user, session, pk=workflow_id) as workflow_lock: workflow = workflow_lock.workflow update = clientside.Update( workflow=workflow.to_clientside(), tabs={tab.slug: tab.to_clientside() for tab in workflow.live_tabs}, steps={ step.id: step.to_clientside() for step in Step.live_in_workflow(workflow) }, ) return WorkflowUpdateData(update, workflow.last_delta_id)
def make_init_state(request, workflow: Workflow, modules: Dict[str, ModuleZipfile]) -> Dict[str, Any]: """Build a dict to embed as JSON in `window.initState` in HTML. Raise Http404 if the workflow disappeared. Side-effect: update workflow.last_viewed_at. """ try: with workflow.cooperative_lock(): # raise DoesNotExist on race if request.user.is_anonymous: user = None else: lock_user_by_id(request.user.id, for_write=False) user = query_clientside_user(request.user.id) workflow.last_viewed_at = datetime.datetime.now() workflow.save(update_fields=["last_viewed_at"]) state = clientside.Init( user=user, workflow=workflow.to_clientside(), tabs={ tab.slug: tab.to_clientside() for tab in workflow.live_tabs }, steps={ step.id: step.to_clientside( force_module_zipfile=modules.get(step.module_id_name)) for step in Step.live_in_workflow( workflow).prefetch_related("tab") }, modules={ module_id: clientside.Module( spec=module.get_spec(), js_module=module.get_optional_js_module(), ) for module_id, module in modules.items() }, blocks={ block.slug: block.to_clientside() for block in workflow.blocks.all() }, settings={ "bigTableRowsPerTile": settings.BIG_TABLE_ROWS_PER_TILE, "bigTableColumnsPerTile": settings.BIG_TABLE_COLUMNS_PER_TILE, }, ) except Workflow.DoesNotExist: raise Http404("Workflow was recently deleted") ctx = JsonizeContext(request.locale_id, modules) return jsonize_clientside_init(state, ctx)
def _write_step_position(workflow: Workflow, step_id: int) -> None: """Write position in DB, or raise (Workflow|Tab|Step).DoesNotExist.""" with workflow.cooperative_lock(): # raises Workflow.DoesNotExist # Raises Step.DoesNotExist, e.g. if tab.is_deleted step = Step.live_in_workflow(workflow).get(pk=step_id) tab = step.tab tab.selected_step_position = step.order tab.save(update_fields=["selected_step_position"]) workflow.selected_tab_position = tab.position workflow.save(update_fields=["selected_tab_position"])
def load_database_objects(workflow_id: int, step_id: int) -> DatabaseObjects: """Query Step info. Raise `Step.DoesNotExist` or `Workflow.DoesNotExist` if the step was deleted. Catch a `ModuleError` from migrate_params() and return it as part of the `DatabaseObjects`. """ with Workflow.lookup_and_cooperative_lock(id=workflow_id) as workflow: # raise Step.DoesNotExist step = Step.live_in_workflow(workflow).get(id=step_id) # module_zipfile try: module_zipfile = MODULE_REGISTRY.latest(step.module_id_name) except KeyError: module_zipfile = None # migrated_params_or_error if module_zipfile is None: migrated_params_or_error = {} else: try: migrated_params_or_error = cjwstate.params.get_migrated_params( step, module_zipfile=module_zipfile) # raise ModuleError except ModuleError as err: migrated_params_or_error = err # stored_object try: stored_object = step.stored_objects.get( stored_at=step.stored_data_version) except StoredObject.DoesNotExist: stored_object = None # input_crr try: # raise Step.DoesNotExist -- but we'll catch this one prev_module = step.tab.live_steps.get(order=step.order - 1) input_crr = prev_module.cached_render_result # may be None except Step.DoesNotExist: input_crr = None return DatabaseObjects( step, module_zipfile, migrated_params_or_error, stored_object, input_crr, )
def _get_workflow_as_clientside_update(self) -> WorkflowUpdateData: """Return (clientside.Update, delta_id). Raise Workflow.DoesNotExist if a race deletes the Workflow. """ with self._lookup_requested_workflow_with_auth_and_cooperative_lock( ) as workflow_lock: workflow = workflow_lock.workflow update = clientside.Update( workflow=workflow.to_clientside(), tabs={ tab.slug: tab.to_clientside() for tab in workflow.live_tabs }, steps={ step.id: step.to_clientside() for step in Step.live_in_workflow(workflow) }, ) return WorkflowUpdateData(update, workflow.last_delta_id)
def _load_workflow_and_step_sync( request: HttpRequest, workflow_id_or_secret_id: Union[int, str], step_slug: str, accessing: Literal["all", "chart", "table"], ) -> Tuple[Workflow, Step]: """Load (Workflow, Step) from database, or raise Http404 or PermissionDenied. `Step.tab` will be loaded. (`Step.tab.workflow_id` is needed to access the render cache.) To avoid PermissionDenied: * The workflow must be public; OR * The user must be workflow owner, editor or viewer; OR * The user must be workflow report-viewer and the step must be a chart or table in the report. """ try: if isinstance(workflow_id_or_secret_id, int): search = {"id": workflow_id_or_secret_id} has_secret = False else: search = {"secret_id": workflow_id_or_secret_id} has_secret = True with Workflow.lookup_and_cooperative_lock(**search) as workflow_lock: workflow = workflow_lock.workflow if (has_secret or workflow.public or workflow.request_authorized_owner(request)): need_report_auth = False elif request.user is None or request.user.is_anonymous: raise PermissionDenied() else: try: acl_entry = workflow.acl.filter( email=request.user.email).get() except AclEntry.DoesNotExist: raise PermissionDenied() if acl_entry.role in {Role.VIEWER, Role.EDITOR}: need_report_auth = False elif acl_entry.role == Role.REPORT_VIEWER: need_report_auth = True else: raise PermissionDenied() # role we don't handle yet step = (Step.live_in_workflow( workflow.id).select_related("tab").get(slug=step_slug) ) # or Step.DoesNotExist if need_report_auth: # user is report-viewer if workflow.has_custom_report: if (accessing == "chart" and workflow.blocks.filter(step_id=step.id).exists()): pass # the step is a chart elif (accessing == "table" and workflow.blocks.filter(tab_id=step.tab_id).exists() and not step.tab.live_steps.filter( order__gt=step.order)): pass # step is a table (last step of a report-included tab) else: raise PermissionDenied() else: # Auto-report: all Charts are allowed; everything else is not try: if accessing == "chart" and (MODULE_REGISTRY.latest( step.module_id_name).get_spec().html_output): pass else: raise PermissionDenied() except KeyError: # not a module raise PermissionDenied() return workflow, step except (Workflow.DoesNotExist, Step.DoesNotExist): raise Http404()
def _workflow_has_notifications(workflow_id: int) -> bool: """Detect whether a workflow sends email on changes.""" return Step.live_in_workflow(workflow_id).filter( notifications=True).exists()
def _load_step_by_slug(workflow: Workflow, step_slug: str) -> Step: """Return a Step or raises HandlerError.""" try: return Step.live_in_workflow(workflow).get(slug=step_slug) except Step.DoesNotExist: raise HandlerError("DoesNotExist: Step not found")
def _load_step_by_id(workflow: Workflow, step_id: int) -> Step: """Return a Step or raises HandlerError.""" try: return Step.live_in_workflow(workflow).get(id=step_id) except Step.DoesNotExist: raise HandlerError("DoesNotExist: Step not found")