class Task(models.Model):
    unique_identifier = models.CharField(max_length=255, null=True, blank=True, help_text="Unique Identifier to group multiple Task Versions")
    version_major = models.PositiveIntegerField(null=True, blank=True, default=0)
    version_minor = models.PositiveIntegerField(null=True, blank=True, default=0)

    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    is_readonly = models.BooleanField(default=False, help_text="Only Team-Admins can edit")
    is_builtin = models.BooleanField(default=False, help_text="Will be executed as Python Module on 3bot Platform")

    owner = models.ForeignKey(Organization, help_text="Task owner")
    title = models.CharField(max_length=255)
    slug = models.SlugField(_('slug'), max_length=50, unique=True)
    desc = models.TextField(null=True, blank=True)

    template = models.TextField(default="#!/bin/sh\n", help_text="To declare an input, type: {{ <data type>.<identifier> }}")

    inputs_fingerprint = models.CharField(max_length=32, blank=True, null=True)
    changelog = models.TextField(blank=True, null=True)

    class Meta():
        ordering = ['owner', 'title']
        verbose_name = _("Task")
        verbose_name_plural = _("Tasks")
        unique_together = (('unique_identifier', 'version_major', 'version_minor'),)

    def clean(self):
        valid_data_types = 'Valid data types are: '
        for el in Parameter.DATA_TYPE_CHOICES:
            valid_data_types += '"%s", ' % el[0]

        valid_identifier = VALID_IDENTIFIER

        inputs = self.extract_inputs()

        if not inputs:
            return

        for name, data_type in inputs.iteritems():
            field = 'template'
            if not any(data_type == el[0] for el in Parameter.DATA_TYPE_CHOICES):
                msg = '"%s" is not a valid data type' % data_type
                raise ValidationError({field: [msg, valid_data_types, ]})
            if not is_valid_identifier(name):
                msg = '"%s" is not a valid identifier' % name
                raise ValidationError({field: [msg, valid_identifier, ]})

    def extract_inputs(self):
        """returns a dict of variables declared in the tempalte string"""
        """ {u'some_words': u'string', u'some_word': u'string'} """
        found_inputs = re.findall(r'\{{(.+?)\}}', self.template)
        inputs = {}
        field = 'template'

        for i in found_inputs:
            msg = '"%s" is not a valid input declaration.' % str(i.encode('utf-8').strip())
            inp = i.strip().split(".")
            if inp[0] not in RESERVED_IDENTIFIERS:
                try:
                    if not inp[0] or not inp[1]:
                        raise ValidationError({field: [msg, ]})
                    inputs[inp[1]] = inp[0]
                except IndexError:
                    raise ValidationError({field: [msg, ]})

        return inputs

    @property
    def required_inputs(self):
        return self.extract_inputs()

    @property
    def type(self):
        known_types = ['bash', 'sh', 'python', 'ruby', 'pearl', 'php', 'node', 'osascript']

        if self.is_builtin:
            return "built-in"
        if self.template.startswith('#!'):
            shebang = self.template.split('\n', 1)[0]
            for type in known_types:
                if shebang.find(type) > 0:
                    return type
        return "unknown"

    @property
    def version(self):
        return "%i.%i" % (self.version_major, self.version_minor)

    def save(self, *args, **kwargs):
        if not self.pk:  # first save
            self.slug = random.randint(1000, 9999)
            super(Task, self).save(*args, **kwargs)

        if not self.unique_identifier:
            md_hash = hashlib.md5()
            md_hash.update(self.title.encode('utf-8') + str(datetime.datetime.now()))
            self.unique_identifier = md_hash.hexdigest()

        s = str(self.owner.id) + "-" + str(self.id) + "-" + slugify(self.title)
        self.slug = s[:50]
        fingerprint = hashlib.md5()
        fingerprint.update(str(self.required_inputs).encode('utf-8'))
        self.inputs_fingerprint = fingerprint.hexdigest()
        super(Task, self).save(*args, **kwargs)

    @models.permalink
    def get_absolute_url(self):
        return ('core_task_detail', (), {
            'slug': self.slug})

    def permalink(self):
        return reverse('core_task_permalink', kwargs={'id': self.id})

    def __str__(self):
        return self.title or self.desc or str(self.pk) class Worker(models.Model):
    ACCESS_REQUEST_TIMEOUT = 1
    STATUS_OFFLINE = 0
    STATUS_ONLINE = 1
    STATUS_BUSY = 9
    STATUS_MUTED = 10
    STATUS_CHOICES = (
        (STATUS_OFFLINE, 'offline'),
        (STATUS_ONLINE, 'online'),
        (STATUS_BUSY, 'busy'),
        (STATUS_MUTED, 'muted'),
    )

    owner = models.ForeignKey(Organization, help_text="Worker owner")
    title = models.CharField(max_length=255)
    slug = models.SlugField(_('slug'), max_length=50, unique=True)

    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    ip = models.GenericIPAddressField("IP-Address",)
    addr = models.CharField(max_length=200, null=True, blank=True, help_text='For now we use the IP to connect to the worker')
    port = models.PositiveIntegerField(default=55555)
    secret_key = models.CharField(max_length=200, null=True, blank=True, help_text='The Secret Key. Never share yours.')

    muted = models.BooleanField(default=False, help_text="Mute a Worker to prevent accessibility checks and improve performance.")

    pre_task = models.TextField(null=True, blank=True, help_text='this will run as a Script before the worker performs a workflow')
    post_task = models.TextField(null=True, blank=True, help_text='this will run as a Script after the worker has performed a workflow')

    class Meta():
        ordering = ['owner', 'title']
        verbose_name = _("Worker")
        verbose_name_plural = _("Workers")

    def get_status_display(self):
        choices = dict(self.STATUS_CHOICES)
        return choices[self.status]

    @cached_property
    def status(self):
        if self.muted:
            return self.STATUS_MUTED
        if self.is_busy:
            return self.STATUS_BUSY
        if self.is_accessible:
            return self.STATUS_ONLINE
        else:
            return self.STATUS_OFFLINE

    @cached_property
    def is_busy(self):
        """Identifies is a Worfkflow is currently blocking this worker."""
        return WorkflowLog.objects.filter(exit_code=WorkflowLog.PENDING, performed_on=self).exists()

    @cached_property
    def is_accessible(self):
        if self.muted:
            return False

        try:
            from .botconnection import BotConnection
            from .tasks import send_script

            protocol = "tcp"
            WORKER_ENDPOINT = "%s://%s:%s" % (protocol, self.ip, str(self.port))
            WORKER_SECRET_KEY = str(self.secret_key)
            conn = BotConnection(WORKER_ENDPOINT, WORKER_SECRET_KEY)
            conn.connect()

            req = {'type': 'ACC'}
            resp = send_script(req, conn, REQUEST_TIMEOUT=1000, REQUEST_RETRIES=1)
            conn.close()

            if resp and resp['type'] == 'ACK':
                return True
        except Exception as e:
            logger.error(e, exc_info=True)
        return False

    def save(self, *args, **kwargs):
        if not self.pk:  # first save
            self.slug = random.randint(1000, 9999)
            super(Worker, self).save(*args, **kwargs)

        s = str(self.owner.id) + "-" + str(self.id) + "-" + slugify(self.title)
        self.slug = s[:50]
        super(Worker, self).save(*args, **kwargs)

    @models.permalink
    def get_absolute_url(self):
        return ('core_worker_detail', (), {
            'slug': self.slug})

    def permalink(self):
        return reverse('core_worker_permalink', kwargs={'id': self.id})

    def __str__(self):
        return self.title class Workflow(models.Model):
    unique_identifier = models.CharField(max_length=255, null=True, blank=True, help_text="Unique Identifier to group multiple Workflow Versions")
    version_major = models.PositiveIntegerField(null=True, blank=True, default=0)
    version_minor = models.PositiveIntegerField(null=True, blank=True, default=0)

    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    owner = models.ForeignKey(Organization, help_text="Workflow owner")
    title = models.CharField(max_length=255)
    slug = models.SlugField(_('slug'), max_length=50, unique=True)
    desc = models.TextField(null=True, blank=True)

    pre_task = models.TextField(null=True, blank=True, help_text='This will run as a Script before the worker runs a task of the current workflow. (Runs before each task)')
    post_task = models.TextField(null=True, blank=True, help_text='This will run as a Script after the worker has executed a task of the current workflow. (Runs after each task)')

    tasks = models.ManyToManyField(Task, through='WorkflowTask')

    class Meta():
        ordering = ['owner', 'title']
        verbose_name = _("Workflow")
        verbose_name_plural = _("Workflows")
        unique_together = (('unique_identifier', 'version_major', 'version_minor'),)

    def number_of_tasks(self):
        return WorkflowTask.objects.filter(workflow=self).count()

    def save(self, *args, **kwargs):
        if not self.pk:  # first save
            self.slug = random.randint(1000, 9999)
            super(Workflow, self).save(*args, **kwargs)

        if not self.unique_identifier:
            md_hash = hashlib.md5()
            md_hash.update(self.title.encode('utf-8') + str(datetime.datetime.now()))
            self.unique_identifier = md_hash.hexdigest()

        s = str(self.owner_id) + "-" + str(self.id) + "-" + slugify(self.title)
        self.slug = s[:50]
        super(Workflow, self).save(*args, **kwargs)

    @models.permalink
    def get_absolute_url(self):
        return ('core_workflow_detail', (), {
            'slug': self.slug})

    def permalink(self):
        return reverse('core_workflow_permalink', kwargs={'id': self.id})

    def __str__(self):
        return self.title or self.desc or str(self.pk) class WorkflowTask(models.Model):
    workflow = models.ForeignKey(Workflow, verbose_name=_("Workflow"))
    task = models.ForeignKey(Task, verbose_name=_("Task"))
    prev_workflow_task = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("The previous Workflow Task"), related_name="prev_wt")
    next_workflow_task = models.ForeignKey('self', null=True, blank=True, on_delete=models.SET_NULL, verbose_name=_("The next Workflow Task"), related_name="next_wt")

    class Meta():
        ordering = ['workflow', 'task', ]
        verbose_name = _("Workflow Task")
        verbose_name_plural = _("Workflow Tasks")

    def delete(self, *args, **kwargs):
        if self.prev_workflow_task and self.next_workflow_task:  # between
            self.prev_workflow_task.next_workflow_task = self.next_workflow_task
            self.next_workflow_task.prev_workflow_task = self.prev_workflow_task
        elif self.prev_workflow_task and not self.next_workflow_task:  # last element
            self.prev_workflow_task.next_workflow_task = None
        elif not self.prev_workflow_task and self.next_workflow_task:  # first element
            self.next_workflow_task.prev_workflow_task = None
        else:  # one and only element
            pass

        super(WorkflowTask, self).delete(*args, **kwargs)  # Call the "real" save() method

    def __str__(self):
        if self.prev_workflow_task:
            s = str(self.prev_workflow_task.task.slug)
        else:
            s = "START"
        if self.next_workflow_task:
            n = str(self.next_workflow_task.task.slug)
        else:
            n = "END"
        return "%s: %s -(%s)-> %s" % (str(self.workflow), str(s), str(self.task.slug), str(n)) class WorkflowPreset(models.Model):
    workflow = models.ForeignKey(Workflow, verbose_name=_("Workflow"))
    user = models.ForeignKey(settings.AUTH_USER_MODEL, verbose_name=("User"))
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    defaults = JSONField(null=True, blank=True, default={})

    class Meta():
        ordering = ['date_modified', 'workflow', 'user', ]
        unique_together = (('workflow', 'user',), )
        verbose_name = _("Workflow Preset")
        verbose_name_plural = _("Workflow Presets")

    def all_presets_set(self):
        wf_tasks = WorkflowTask.objects.filter(workflow=self.workflow)

        # skip empty workflows
        if not wf_tasks:
            return False

        errors = False
        org = self.workflow.owner

        for workflow_task in wf_tasks:

            for param_name, param_data_type in workflow_task.task.required_inputs.iteritems():
                wanted_key = '%s.%s' % (param_data_type, param_name)

                if str(workflow_task.id) in self.defaults:
                    if not wanted_key in self.defaults[str(workflow_task.id)]:
                        errors = True
                    else:
                        parameter_path = self.defaults[str(workflow_task.id)][wanted_key]
                        path_units = parameter_path.strip().split('.')
                        if path_units[0] == 'user':
                            if not UserParameter.objects.filter(name=path_units[2], data_type=path_units[1], owner=self.user).exists():
                                errors = True
                        elif path_units[0] == 'global':
                            if not OrganizationParameter.objects.filter(name=path_units[2], data_type=path_units[1], owner=org).exists():
                                errors = True
                        elif path_units[0] == 'output':
                                errors = False
                        else:
                            errors = True
                else:
                    errors = True

        return not errors

    def __str__(self):
        return "Preset for %s (%s)" % (self.workflow, self.user) class UserParameter(Parameter):
    owner = models.ForeignKey(settings.AUTH_USER_MODEL, help_text="Parameter owner")

    class Meta():
        ordering = ['owner', 'data_type', ]
        verbose_name = _("Parameter for User")
        verbose_name_plural = _("Parameters for Users")

    def save(self, *args, **kwargs):
        super(UserParameter, self).save(*args, **kwargs)

    def validate_unique(self, *args, **kwargs):
        # this is called every time the obj gets updated or saved
        # the try/except is needed to prevent DoesNotExist errors for the owner field.
        try:
            self.owner
        except:
            return
        # Checks that no two parameters have the same name, data_type and owner
        if UserParameter.objects.filter(name=self.name, data_type=self.data_type, owner=self.owner).exists():
            same_param = UserParameter.objects.get(name=self.name, data_type=self.data_type, owner=self.owner)
            if not same_param.id == self.id:
                raise ValidationError(
                    {
                        NON_FIELD_ERRORS:
                        ('Parameter with this name and data type already exists',)
                    }
                )

    def __str__(self):
        return "%s:%s (%s)" % (self.data_type, self.name, self.owner)

    @models.permalink
    def get_absolute_url(self):
        return ('core_user_parameter_detail', (), {
            'id': self.id}) class Parameter(models.Model):
    STRING = 'string'
    EMAIL = 'email'
    PASSWORD = 'password'
    BOOLEAN = 'bool'
    INT = 'int'
    FLOAT = 'float'
    IPv4 = 'ipv4'
    IPv6 = 'ipv6'

    DATA_TYPE_CHOICES = (
        (STRING, 'String'),
        (EMAIL, 'Email'),
        #(PASSWORD, 'Password'),
        (BOOLEAN, 'Boolean'),
        # (INT, 'Integer'),
        # (FLOAT, 'Floating Number'),
        # (IPv4, 'IP 4'),
        # (IPv6, 'IP 6'),
    )

    data_type = models.CharField(max_length=20, choices=DATA_TYPE_CHOICES, default=STRING)
    name = models.CharField("identifier", max_length=255, blank=False, null=False)
    value = models.CharField(max_length=255, blank=False, null=False)

    def clean(self):
        field = 'name'
        if not is_valid_identifier(self.name):
            valid_identifier = VALID_IDENTIFIER
            msg = '"%s" is not a valid identifier' % self.name
            raise ValidationError({field: [msg, valid_identifier, ]})

        # Checks that the parameters validates for the selected data type
        if self.data_type == self.EMAIL and self.value is not None:
            validate_email(self.value)

        elif self.data_type == self.BOOLEAN and not (self.value == 'True' or self.value == 'False'):
            raise ValidationError("Boolean Parameter can be either True or False")

        elif self.data_type == self.STRING and self.value is not None:
            pass class OrganizationParameter(Parameter):
    owner = models.ForeignKey(Organization, help_text="Parameter owner")

    class Meta():
        ordering = ['owner', 'data_type', ]
        verbose_name = _("Parameter for Team")
        verbose_name_plural = _("Parameters for Team")

    def save(self, *args, **kwargs):
        super(OrganizationParameter, self).save(*args, **kwargs)

    def validate_unique(self, *args, **kwargs):
        # this is called every time the obj gets updated or saved
        # the try/except is needed to prevent DoesNotExist errors for the owner field.
        try:
            self.owner
        except:
            return
        # Checks that no two parameters have the same name, data_type and owner
        if OrganizationParameter.objects.filter(name=self.name, data_type=self.data_type, owner=self.owner).exists():
            same_param = OrganizationParameter.objects.get(name=self.name, data_type=self.data_type, owner=self.owner)
            if not same_param.id == self.id:
                raise ValidationError(
                    {
                        NON_FIELD_ERRORS:
                        ('Parameter with this name and data type already exists',)
                    }
                )

    def __str__(self):
        return "%s:%s (%s)" % (self.data_type, self.name, self.owner)

    @models.permalink
    def get_absolute_url(self):
        return ('core_organization_parameter_detail', (), {
            'slug': self.owner.slug,
            'id': self.id}) class ParameterList(models.Model):
    """
    ParameterList
    """
    date_created = models.DateTimeField(auto_now_add=True)
    date_modified = models.DateTimeField(auto_now=True)

    title = models.CharField(max_length=255)
    owner = models.ForeignKey(Organization, help_text="Parameter List owner")
    parameters = models.ManyToManyField(OrganizationParameter, related_name="lists", help_text="Select Parameters for this Team.")

    class Meta():
        ordering = ['title', 'date_created', ]
        verbose_name = _("Parameter List")
        verbose_name_plural = _("Parameter Lists")

    def __str__(self):
        return self.title class WorkflowLog(models.Model):
    SUCCESS = 0
    ERROR = 1
    PENDING = 2

    EXIT_CODE_CHOICES = (
        (SUCCESS, 'Success'),
        (ERROR, 'Error'),
        (PENDING, 'Pending'),
    )

    workflow = models.ForeignKey(Workflow, verbose_name=_("Workflow"))
    date_created = models.DateTimeField(auto_now_add=True, help_text='Date the workflow was performed')
    date_started = models.DateTimeField(help_text='Date the workflow was performed', blank=True, null=True)
    date_finished = models.DateTimeField(help_text='Date the workflow was performed', blank=True, null=True)
    exit_code = models.PositiveIntegerField(choices=EXIT_CODE_CHOICES, default=PENDING)
    performed_by = models.ForeignKey(settings.AUTH_USER_MODEL, help_text="The User who performed the Worfkflow")
    performed_on = models.ForeignKey(Worker, help_text="The Worker Worfkflow was performed on")

    inputs = JSONField()
    outputs = JSONField(null=True, blank=True)

    class Meta():
        ordering = ['-date_created', ]
        verbose_name = _("Workflow Log")
        verbose_name_plural = _("Workflow Logs")

    @models.permalink
    def get_absolute_url(self):
        return ('core_workflow_log_detail', (), {
            'slug': self.workflow.slug,
            'id': self.id})

    def permalink(self):
        return reverse('core_workflow_log_permalink', kwargs={'id': self.id})

    def __str__(self):
        return "%s - %s logged %s" % (self.date_created.strftime('%d.%m.%y %H:%M'), str(self.performed_by), self.workflow.title, ) @background(schedule=1)
def run_workflow(workflow_log_id):
    """
    expects an empty workflow_log,
    performes its tasks on the given worker(s) and
    returns updated workflow_log
    """
    outputs = {}
    protocol = "tcp"

    workflow_log = WorkflowLog.objects.get(id=workflow_log_id)
    worker = workflow_log.performed_on

    WORKER_ENDPOINT = "%s://%s:%s" % (protocol, worker.ip, str(worker.port))
    WORKER_SECRET_KEY = worker.secret_key

    conn = BotConnection(WORKER_ENDPOINT, WORKER_SECRET_KEY)
    conn.connect()

    # Make a JSON
    request_header = {'workflow_log_id': workflow_log.id,
                      'workflow': slugify(workflow_log.workflow.title),
                      'workflow_log_time': workflow_log.date_created.strftime('%Y%m%d-%H%M%S'),
                      'script': {},
                      'hooks': {},  # see doc/HOOKS.md
                      }

    # hooks for this workflow
    if workflow_log.workflow.pre_task:
        request_header['hooks']['pre_task'] = workflow_log.workflow.pre_task

    if workflow_log.workflow.post_task:
        request_header['hooks']['post_task'] = workflow_log.workflow.post_task

    ordered_workflows = order_workflow_tasks(workflow_log.workflow)

    workflow_log.date_started = timezone.now()
    for idx, workflow_task in enumerate(ordered_workflows):
        template = render_template(workflow_log, workflow_task)

        if workflow_task.task.is_builtin:
            m = importCode(template, "test")
            output = {}
            output['stdout'] = str(m.run())
            output['exit_code'] = workflow_log.SUCCESS
        else:
            request = request_header
            request['script']['id'] = idx
            request['script']['body'] = template

            output = send_script(request, conn)

        outputs['%i_%s' % (workflow_task.id, workflow_task.task.title)] = output

        # loop over all next wf_tasks and add this scripts output to inputs
        current = workflow_task
        while current.next_workflow_task:
            current = current.next_workflow_task

            # deepcopy dict to prevent runtime error
            inp = deepcopy(workflow_log.inputs)
            # loop key, value pairs and look if this output needs to be set as input
            for key, value in inp[str(current.id)]['string'].iteritems():
                if value == 'output_%s' % str(workflow_task.id):
                    workflow_log.inputs[str(current.id)]['string'][key] = output['stdout']

        if 'exit_code' not in output or output['exit_code'] is not workflow_log.SUCCESS:
            workflow_log.exit_code = workflow_log.ERROR
            workflow_log.save()
            break
        else:
            workflow_log.exit_code = workflow_log.SUCCESS

    conn.close()

    workflow_log.date_finished = timezone.now()
    workflow_log.outputs = outputs
    workflow_log.save()

    # Notify user in case of failure
    if workflow_log.exit_code == workflow_log.ERROR:
        send_failiure_notification(workflow_log) def order_workflow_tasks(workflow):
    try:
        curr = workflow.workflowtask_set.get(prev_workflow_task=None)
        workflow_tasks = [curr]
        while curr.next_workflow_task:
            curr = curr.next_workflow_task
            workflow_tasks.append(curr)
    except WorkflowTask.DoesNotExist:
        workflow_tasks = []

    return workflow_tasks def render_templates(workflow_log, mask=False):
    # mask = True -> hide templates on buildin or readonly tasks, replace sensitive data like passwords
    templates = []

    for wf_task in order_workflow_tasks(workflow_log.workflow):
        if mask:
            if wf_task.task.is_readonly or wf_task.task.is_builtin:
                # dont show template
                tpl = "No Template. Task is either built-in or readonly"
            else:
                # show template but hide sensitive data
                tpl = render_template(workflow_log, wf_task, mask=True)
        else:
            tpl = render_template(workflow_log, wf_task)
        templates.append(tpl)

    return templates @login_required
def get_my_orgs(request, an_user=None):
    if request is not None and an_user is None:
        an_user = request.user

    default_org, created = Organization.objects.get_or_create(slug='3bot', name="3bot")

    if not OrganizationUser.objects.filter(organization=default_org, user=an_user).exists():
        # we create an OrganizationUser, so each user is member of our default_org
        # if default_org was just created, we also mark this user as admin and create an OrganizationOwner
        org_user = OrganizationUser(organization=default_org, user=an_user, is_admin=created)
        org_user.save()
        if created:
            org_owner = OrganizationOwner(organization=default_org, organization_user=org_user)
            org_owner.save()

    orgs = Organization.objects.filter(users=an_user)

    return orgs def filter_workflow_log_history(workflow=None, teams=None, exit_code=None, user=None, worker=None, quantity=None):
    """returns a queryset of workflow-logs filtered by given parameters"""
    _filter = {}
    if workflow:
        _filter['workflow'] = workflow
    if teams:
        _filter['workflow__owner__in'] = teams
    if user:
        _filter['performed_by'] = user
    if worker:
        _filter['performed_on'] = worker

    logs = WorkflowLog.objects.filter(**_filter).select_related('workflow', 'performed_by', 'performed_on')[:quantity]
    return logs def send_failiure_notification(workflow_log):
    """Notify user in case of failure."""
    subject = "[3BOT] Workflow '%s' has failed" % (workflow_log.workflow.title)
    message = "Your workflow %s%s has failed.\n -- 3bot" % (Site.objects.get_current(), workflow_log.get_absolute_url())
    workflow_log.performed_by.email_user(subject, message) class TaskParameterForm(forms.Form):
    def __init__(self, *args, **kwargs):
        request = kwargs.pop('request')
        extra = kwargs.pop('extra')
        workflow_task = kwargs.pop('workflow_task')
        super(TaskParameterForm, self).__init__(*args, **kwargs)

        for name, data_type in extra.iteritems():
            possible_parameters = get_possible_parameters(request, workflow_task, data_type)
            preset_parameter = get_preset_param(request, workflow_task, name, data_type)
            self.fields['wt_task_%i.%s.%s' % (workflow_task.id, data_type, name)] = forms.ChoiceField(
                label="%s (%s)" % (name, data_type),
                choices=possible_parameters,
                initial=preset_parameter,
                widget=forms.Select(attrs={'class': 'form-control', }),
                )

            # each param gets a hidden field for saving prompted data
            self.fields['prompt_wt_task_%i.%s.%s' % (workflow_task.id, data_type, name)] = forms.CharField(widget=forms.HiddenInput(), required=False, ) class WorkflowReorderForm(forms.Form):
    workflow_id = forms.CharField(max_length=30, widget=forms.HiddenInput())
    order = forms.CharField(widget=forms.HiddenInput()) class WorkflowCreateForm(WorkflowForm):
    """Form to create a Workflow"""
    class Meta(WorkflowForm.Meta):
        fields = ['owner', ] + WorkflowForm.Meta.fields
        WorkflowForm.Meta.widgets['owner'] = forms.Select(attrs={'class': 'form-control'})

    def __init__(self, *args, **kwargs):
        request = kwargs.pop('request')
        super(WorkflowCreateForm, self).__init__(*args, **kwargs)
        self.fields['owner'].queryset = get_possible_owners(request)
        self.fields['owner'].initial = get_curr_org(request) class WorkflowChangeForm(WorkflowForm):
    """Form to edit a Workflow"""
    pass class WorkerSelectForm(forms.Form):
    def __init__(self, *args, **kwargs):
        request = kwargs.pop('request')
        workflow = kwargs.pop('workflow')

        super(WorkerSelectForm, self).__init__(*args, **kwargs)

        possible_workers = get_possible_worker(request, as_list=True)
        preset_worker = get_preset_worker(request, workflow, flat=True)

        self.fields['worker'] = forms.MultipleChoiceField(
            required=True,
            label="Worker",
            choices=possible_workers,
            initial=preset_worker,
            widget=forms.SelectMultiple(attrs={'class': 'form-control', }),
        )
        self.fields['worker'].empty_label = None

        # workaround for displaying a message to the user
        # if no worker is acessible while initializing the form
        if len(possible_workers) <= 0:
            self.cleaned_data = {}
            msg = "No Worker found. Please configure a Worker first."
            self.add_error('worker', msg)

    def clean(self):
        cleaned_data = super(WorkerSelectForm, self).clean()
        worker_ids = cleaned_data.get('worker', [])
        workers = Worker.objects.filter(id__in=worker_ids)
        for worker in workers:
            if not worker.is_accessible:
                raise forms.ValidationError("worker not accessible")
        # Always return the full collection of cleaned data.
        # This has changed in django 1.7 but raises errors in django <1.7
        # We want to support both, so we return cleanded_data
        return cleaned_data class UserParameterCreateForm(forms.ModelForm):
    next = forms.CharField(required=False, widget=forms.HiddenInput())

    class Meta:
        model = UserParameter
        fields = ['data_type', 'name', 'value', 'owner', ]
        widgets = {
            'data_type': forms.Select(attrs={'class': 'form-control', }),
            'name': forms.TextInput(attrs={'class': 'form-control', }),
            'value': forms.TextInput(attrs={'class': 'form-control', }),
            'owner': forms.HiddenInput(),
        }

    def __init__(self, *args, **kwargs):
        user = kwargs.pop('user')
        super(UserParameterCreateForm, self).__init__(*args, **kwargs)
        self.fields['owner'].initial = user class ParameterListSelectForm(forms.Form):
    def __init__(self, *args, **kwargs):
        request = kwargs.pop('request')
        workflow = kwargs.pop('workflow')
        self.workflow = workflow

        super(ParameterListSelectForm, self).__init__(*args, **kwargs)

        possible_lists = [('', '--Choose a Parameter List--')]
        possible_lists += get_possible_lists(request, workflow)
        preset_list_id = get_preset_list(request, workflow, id=True)

        self.fields['parameter_list'] = forms.ChoiceField(
            label="Parameter List",
            choices=possible_lists,
            initial=preset_list_id,
            widget=forms.Select(attrs={'class': 'form-control', }),
        )
        self.fields['parameter_list'].empty_label = None

    def clean_parameter_list(self, *args, **kwargs):
        list_id = self.cleaned_data['parameter_list']
        parameter_list = ParameterList.objects.get(id=list_id)
        workflow_tasks = order_workflow_tasks(self.workflow)

        # make a list of ::all_required_inputs, with name and data type
        all_required_inputs = []
        for wf_task in workflow_tasks:
            for name, data_type in wf_task.task.required_inputs.iteritems():
                if name not in all_required_inputs:
                    all_required_inputs.append((name, data_type))

        # make a list of ::all_parameters from the list, with name and data type
        all_parameters = []
        for parameter in parameter_list.parameters.all():
            all_parameters.append((parameter.name, parameter.data_type))

        # if not every element of ::all_parameters is also in ::all_required_inputs
        # the list is not valid for this workflow, raise an form validation error
        if not False not in [e in all_parameters for e in all_required_inputs]:
            error_message = "Not all required inputs found in ParameterList."
            error_message += "<br><br><strong>Required inputs by the Tasks are</strong>:"
            for param in all_required_inputs:
                error_message += "<br>" + str(param[0]) + ":" + str(param[1])

            error_message += "<br><br><strong>List contains</strong>:"
            for param in all_parameters:
                error_message += "<br>" + str(param[0]) + ":" + str(param[1])
            raise forms.ValidationError(mark_safe(error_message))
        return list_id
import json
    from urllib.parse import unquote_plus
    from urllib import unquote_plus
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from threebot.models import Task
from threebot.models import Worker
from threebot.models import Workflow
from threebot.models import WorkflowTask
from threebot.models import WorkflowPreset
from threebot.models import UserParameter, Parameter
from threebot.models import OrganizationParameter, ParameterList
from threebot.models import WorkflowLog
from threebot.tasks import run_workflow
from threebot.utils import order_workflow_tasks, render_templates, get_my_orgs, filter_workflow_log_history
from threebot.utils import send_failiure_notification
from threebot.forms import TaskParameterForm, WorkflowReorderForm, WorkflowCreateForm, WorkflowChangeForm, WorkerSelectForm, UserParameterCreateForm, ParameterListSelectForm
            # redirect to reorder view
            return redirect('core_workflow_detail_reorder', slug=workflow.slug)

    return render(request, template, {'form': form})


@login_required
def workflow_permalink(request, id):
    workflow = get_object_or_404(Workflow, id=id)
    return redirect('core_workflow_detail', slug=workflow.slug)


@login_required
def detail_digest(request, slug, template='threebot/workflow/detail_digest.html'):
    orgs = get_my_orgs(request)
    workflow = get_object_or_404(Workflow, owner__in=orgs, slug=slug)
    logs = filter_workflow_log_history(workflow=workflow, quantity=20)

    return render(request, template, {'workflow': workflow, 'logs': logs})


@login_required
def detail_edit(request, slug, template='threebot/workflow/detail_edit.html'):
    orgs = get_my_orgs(request)
    workflow = get_object_or_404(Workflow, owner__in=orgs, slug=slug)

    form = WorkflowChangeForm(request.POST or None, instance=workflow, )

    if form.is_valid():
        workflow = form.save()

        if '_continue' in form.data:
            # rerirect back to detailview
            return redirect('core_workflow_detail_reorder', slug=workflow.slug)
        elif '_addanother' in form.data:
            # redirect to create page
            return redirect('core_workflow_create')
        else:  # _save
            # redirect to listview
            return redirect('core_workflow_list')

    return render(request, template, {'form': form, 'workflow': workflow})


@login_required
def detail_perf(request, slug, template='threebot/workflow/detail_perf.html'):
    orgs = get_my_orgs(request)
    workflow = get_object_or_404(Workflow, owner__in=orgs, slug=slug)

    wf_preset, created = WorkflowPreset.objects.get_or_create(user=request.user, workflow=workflow)
    preset = {}
    ready_to_perform = False  # if true: each form is valid an request method is POST

    logs = filter_workflow_log_history(workflow=workflow, quantity=5)

    workflow_tasks = order_workflow_tasks(workflow)

    # serve relevant forms
    initials_for_worker_form = {}
    if request.GET.get('worker'):
        initials_for_worker_form['worker'] = request.GET.get('worker')
    worker_form = WorkerSelectForm(request.POST or None, request=request, workflow=workflow, initial=initials_for_worker_form)
    for wf_task in workflow_tasks:
        extra = wf_task.task.required_inputs
        wf_task.form = TaskParameterForm(request.POST or None, request=request, extra=extra, workflow_task=wf_task)

    if request.method == 'POST':
        ready_to_perform = True

        if worker_form.is_valid():
            worker_ids = worker_form.cleaned_data['worker']
            preset.update({'worker_id': worker_ids})
        else:
            ready_to_perform = False

        for wf_task in workflow_tasks:
            if wf_task.form.is_valid():
                pass
            else:
                ready_to_perform = False

    if ready_to_perform:
        inp = {}

        for wf_task in workflow_tasks:
            data = wf_task.form.cleaned_data
            preset.update({wf_task.id: {}})

            form_dict = {}
            for data_type in Parameter.DATA_TYPE_CHOICES:
                form_dict[data_type[0]] = {}

            for template_input, path_to_value in data.iteritems():  # TODO: better identifier
                # template_input = "wt_task_1.email.recipient_email"
                # path_to_value = "prompt.email.name@provider.com"
                # path_to_value = "output.string.output_<WorkflowTask.id>"

                template_input_list = template_input.split('.')
                wf_task_id = template_input_list[0]
                template_input_data_type = template_input_list[1]
                template_input_name = template_input_list[2]

                path_to_value_list = path_to_value.split('.')
                param_owner = path_to_value_list[0]
                if len(path_to_value_list) == 3:
                    param_data_type = path_to_value_list[1]
                    param_name = path_to_value_list[2]

                # k[0] = wt_task id:
                # k[1] = data_type:
                # k[2] = value:

                # val[0] = param owner
                # val[1] = param data_type
                # val[2] = param name

                if param_owner == 'global':
                    value = OrganizationParameter.objects.get(data_type=param_data_type, name=param_name)
                    form_dict[template_input_data_type][template_input_name] = value.value
                if param_owner == 'user':

                    value = UserParameter.objects.get(data_type=param_data_type, name=param_name, owner=request.user)