class ContentTypeForm(forms.Form):
    ctype = forms.ModelChoiceField(
                queryset=ctypes, 
                required=True, 
                empty_label='(select a content-type)',
                label='content type',
                help_text=('clone all instances of the selected content type and push '
                           'them in the test process of the application')
            ) class Process(models.Model):
    """A process holds the map that describes the flow of work.
    
    The process map is made of activities and transitions.
    The instances you create on the map will begin the flow in
    the configured begin activity. Instances can be moved
    forward from activity to activity, going through transitions,
    until they reach the End activity.
    """
    enabled = models.BooleanField(default=True)
    date = models.DateTimeField(auto_now=True)
    title = models.CharField(max_length=100)
    description = models.TextField()
    description.allow_tags=True
    begin = models.ForeignKey('Activity', related_name='bprocess', verbose_name='initial activity', null=True, blank=True)
    end = models.ForeignKey('Activity', related_name='eprocess', verbose_name='final activity', null=True, blank=True,
                            help_text='a default end activity will be created if blank')
    priority = models.IntegerField(default=0)
        
    # add new ProcessManager
    objects = ProcessManager()
    
    class Meta:
        verbose_name_plural = 'Processes'
        permissions = (
            ("can_instantiate", "Can instantiate"),
            ("can_browse", "Can browse"),
        )
    
    def __unicode__(self):
        return self.title
    
    @allow_tags
    def summary(self):
        return '<pre>%s</pre>' % self.description
    
    @allow_tags
    def action(self):
        return 'add <a href=../activity/add/>a new activity</a> or <a href=../activity/>copy</a> an activity from another process'

    
    def add_activity(self, name):
        '''
        name: name of activity (get or created)
        '''
        a, created = Activity.objects.get_or_create(title=name, process=self, 
                                                    defaults={'description':'(add description)'})
        return a 
    
    def add_transition(self, name, activity_out, activity_in):
        t, created = Transition.objects.get_or_create(name=name, process=self,
                                             output=activity_out,
                                             defaults={'input':activity_in})
        return t
    
    def create_authorized_group_if_not_exists(self):
        g, created = Group.objects.get_or_create(name=self.title)
        if created:
            ptype = ContentType.objects.get_for_model(Process)
            cip = Permission.objects.get(content_type=ptype, codename='can_instantiate')
            g.permissions.add(cip)
        
    def save(self, no_end=False):
        models.Model.save(self)
        # instantiation group
        self.create_authorized_group_if_not_exists()
        
        if not no_end and not self.end:
            self.end = Activity.objects.create(title='End', process=self, kind='dummy', autostart=True)
            models.Model.save(self)
        try:
            if self.end and self.end.process.id != self.id:
                a = self.end
                a.id = None
                a.process = self
                a.save()
                self.end = a
                models.Model.save(self)
            if self.begin and self.begin.process.id != self.id:
                a = self.begin
                a.id = None
                a.process = self
                a.save()
                self.begin = a
                models.Model.save(self)
        except Exception:
            # admin console error ?!?
            pass class Application(models.Model):
    """ An application is a python view that can be called by URL.
    
        Activities can call applications.
        A commmon prefix may be defined: see settings.WF_APPS_PREFIX
    """
    url = models.CharField(max_length=255, unique=True, help_text='relative to prefix in settings.WF_APPS_PREFIX')
    # TODO: drop abbreviations (perhaps here not so necessary to ??
    SUFF_CHOICES = (
                    ('w', 'workitem.id'),
                    ('i', 'instance.id'),
                    ('o', 'object.id'),
                    )
    suffix =  models.CharField(max_length=1, choices=SUFF_CHOICES, verbose_name='suffix', null=True, blank=True,
                               default='w', help_text='http://[host]/[settings.WF_APPS_PREFIX/][url]/[suffix]')
    detected_as_auto = None
    
    def __unicode__(self):
        return self.url
    
    def get_app_url(self, workitem=None, extern_for_user=None):
        from django.conf import settings
        path = '%s/%s/' % (settings.WF_APPS_PREFIX, self.url)
        if workitem:
            if self.suffix:
                if self.suffix == 'w': path = '%s%d/' % (path, workitem.id)
                if self.suffix == 'i': path = '%s%d/' % (path, workitem.instance.id)
                if self.suffix == 'o': path = '%s%d/' % (path, workitem.instance.content_object.id)
            else:
                path = '%s?workitem_id=%d' % (path, workitem.id) 
        if extern_for_user:
            profile = UserProfile.objects.get(user=user)
            path = 'http://%s%s' % (profile.web_host, path)
        return path
    
    def get_handler(self):
        '''returns handler mapped to url.
        '''
        try:
            func, args, kwargs = resolve(self.get_app_url() + '0/')
            self.detected_as_auto = False
        except Exception:
            func, args, kwargs = resolve(self.get_app_url())
            self.detected_as_auto = True
        return func
    
    @allow_tags
    def documentation(self):
        doc = ''
        if self.detected_as_auto:
            doc = 'detected as auto application.<hr>'
        try:
            doc += '<pre>%s</pre>' % self.get_handler().__doc__
        except Exception as v:
            doc += 'WARNING: the url %s is not resolved.' % self.get_app_url()
        return doc
    
    def has_test_env(self):
        if Process.objects.filter(title='test_%s' % self.url).count() > 0:
            return True
        return False
    
    def create_test_env(self, user=None):
        if self.has_test_env(): 
            return
        
        g = Group.objects.create(name='test_%s' % self.url)
        ptype = ContentType.objects.get_for_model(Process)
        cip = Permission.objects.get(content_type=ptype, codename='can_instantiate')
        g.permissions.add(cip)
        # group added to current user
        if user: 
            user.groups.add(g)

        p = Process.objects.create(title='test_%s' % self.url, description='unit test process')
        p.begin = p.end
        p.begin.autostart = False
        p.begin.title = "test_activity"
        p.begin.kind = 'standard'
        p.begin.application = self
        p.begin.description = 'test activity for application %s' % self.url
        p.begin.save()
        
        p.begin.roles.add(g)
        
        p.save()
    
    def remove_test_env(self):
        if not self.has_test_env(): return
        Process.objects.filter(title='test_%s' % self.url).delete()
        Group.objects.filter(name='test_%s' % self.url).delete()
    
    @allow_tags
    def test(self):
        if self.has_test_env():
            return ('<a href=teststart/%d/>start test instances</a> | '
                    '<a href=testenv/remove/%d/>remove unit test env</a>') % (self.id, self.id)
        else:
            return '<a href=testenv/create/%d/>create unit test env</a>' % self.id class Transition(models.Model):
    """ A Transition connects two Activities: a From and a To activity.
    
    Since the transition is oriented you can think at it as being a
    link starting from the From and ending in the To activity.
    Linking the activities in your process you will be able to draw
    the map.
    
    Each transition is associated to a condition that will be tested
    each time an instance has to choose which path to follow.
    If the only transition whose condition is evaluated to true will
    be the transition choosen for the forwarding of the instance.
    """
    name = models.CharField(max_length=50, null=True, blank=True)
    process = models.ForeignKey(Process, related_name='transitions')
    input = models.ForeignKey(Activity, related_name='transition_inputs')
    condition = models.CharField(max_length=200, null=True, blank=True,
                                 help_text='ex: instance.condition=="OK" | OK')
    output = models.ForeignKey(Activity, related_name='transition_outputs')
    description = models.CharField(max_length=100, null=True, blank=True)
    precondition = models.SlugField(null=True, blank=True, help_text='object method that return True if transition is posible')
    
    def is_transition(self):
        ''' used in admin templates.
        '''
        return True
    
    def save(self):
        if self.input.process != self.process or self.output.process != self.process:
            raise Exception("a transition and its activities must be linked to the same process")
        models.Model.save(self)
    
    def __unicode__(self):
        return self.name or 't%s' % str(self.pk)

    class Meta:
        pass
        #unique_together = (("input", "condition"),) class ProcessInstance(models.Model):
    """ This is a process instance.
    
    A process instance is created when someone decides to do something,
    and doing this thing means start using a process defined in GoFlow.
    That's why it is called "process instance". The process is a class
    (=the definition of the process), and each time you want to
    "do what is defined in this process", that means you want to create
    an INSTANCE of this process.
    
    So from this point of view, an instance represents your dynamic
    part of a process. While the process definition contains the map
    of the workflow, the instance stores your usage, your history,
    your state of this process.
    
    The ProcessInstance will collect and handle workitems (see definition)
    to be passed from activity to activity in the process.
    
    Each instance can have more than one workitem depending on the
    number of split actions encountered in the process flow.
    That means that an instance is actually the collection of all of
    the instance "pieces" (workitems) that we get from splits of the
    same original process instance.
    
    Each ProcessInstance keeps track of its history through a graph.
    Each node of the graph represents an activity the instance has
    gone through (normal graph nodes) or an activity the instance is
    now pending on (a graph leaf node). Tracking the ProcessInstance history
    can be very useful for the ProcessInstance monitoring.
    
    When a process instance starts, the instance has to carry an
    implementation object that contains the application data. The
    specifications for the implementation class is:
    
    (nothing: now managed by generic relation)
    
    From the instance, the implementation object is reached as following:
      obj = instance.content_object (or instance.wfobject()).
    In a template, a field date1 will be displayed like this:
      {{ instance.wfobject.date1 }} or {{ instance.content_object.date1 }}
    
    From the object, instances may be reached with the reverse generic relation:
    the following can be added to the model:
      wfinstances = generic.GenericRelation(ProcessInstance)
    
    """
    STATUS_CHOICES = (
                      ('initiated', 'initiated'),
                      ('running', 'running'),
                      ('active', 'active'),
                      ('complete', 'complete'),
                      ('terminated', 'terminated'),
                      ('suspended', 'suspended'),
                      )
    title = models.CharField(max_length=100)
    process = models.ForeignKey(Process, related_name='instances', null=True, blank=True)
    creationTime = models.DateTimeField(auto_now_add=True)
    user = models.ForeignKey(User, related_name='instances')
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='initiated')
    old_status = models.CharField(max_length=10, choices=STATUS_CHOICES, null=True, blank=True)
    condition = models.CharField(max_length=50, null=True, blank=True)
    
    # refactoring
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = generic.GenericForeignKey('content_type', 'object_id')
    
    # add new ProcessInstanceManager
    objects = ProcessInstanceManager()
    
    def wfobject(self):
        return self.content_object
    
    @allow_tags
    def workitems_list(self):
        '''provide html link to workitems for a process instance in admin change list.
        @rtype: string
        @return: html href link "../workitem/?instance__id__exact=[self.id]&ot=asc&o=0"
        '''
        nbwi = self.workitems.count()
        return '<a href=../workitem/?instance__id__exact=%d&ot=asc&o=0>%d item(s)</a>' % (self.pk, nbwi)
    
    def __str__(self):
        return str(self.pk)
    
    def __unicode__(self):
        return self.title
    
    def set_status(self, status):
        if not status in [x for x,y in ProcessInstance.STATUS_CHOICES]:
            raise Exception('instance status incorrect :%s' % status)
        self.old_status = self.status
        self.status = status
        self.save() class WorkItem(models.Model):
    """A workitem object represents an activity you are performing.
    
    An Activity object defines the activity, while the workitem object
    represents that you are performing this activity. So workitem is
    an "instance" of the activity.
    """
    STATUS_CHOICES = (
                      ('blocked', 'blocked'),
                      ('inactive', 'inactive'),
                      ('active', 'active'),
                      ('suspended', 'suspended'),
                      ('fallout', 'fallout'),
                      ('complete', 'complete'),
                      )
    date = models.DateTimeField(auto_now=True)
    user = models.ForeignKey(User, related_name='workitems', null=True, blank=True)
    instance = models.ForeignKey(ProcessInstance, related_name='workitems')
    activity = models.ForeignKey(Activity, related_name='workitems')
    workitem_from = models.ForeignKey('self', related_name='workitems_to', null=True, blank=True)
    others_workitems_from = models.ManyToManyField('self', related_name='others_workitems_to', null=True, blank=True)
    push_roles = models.ManyToManyField(Group, related_name='push_workitems', null=True, blank=True)
    pull_roles = models.ManyToManyField(Group, related_name='pull_workitems', null=True, blank=True)
    blocked = models.BooleanField(default=False)
    priority = models.IntegerField(default=0)
    status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='inactive')

    objects = WorkItemManager()
    
    def forward(self, timeout_forwarding=False, subflow_workitem=None):
        # forward_workitem(workitem, path=None, timeout_forwarding=False, subflow_workitem=None):
        '''
        Convenience procedure to forwards workitems to valid destination activities.
        
        @type path: string??
        @param path: XXX TODO: This is not used, so don't know why it's here.
        @type timeoutForwarding: bool
        @param timeoutForwarding:
        @type: subflow_workitem: WorkItem
        @param subflow_workitem: a workitem associated with a subflow ???
        
        '''
        log.info('forward_workitem %s', self.__unicode__())
        if not timeout_forwarding:
            if self.status != 'complete':
                return
        if self.has_workitems_to() and not subflow_workitem:
            log.debug('forward_workitem canceled for %s: ' 
                       'workitem.has_workitems_to()', self.__unicode__())
            return
        
        if timeout_forwarding:
            log.info('timeout forwarding')
            Event.objects.create(name='timeout', workitem=self)
        
        for destination in self.get_destinations(timeout_forwarding):
            self._forward_workitem_to_activity(destination)
            if self.activity.split_mode == 'xor': break

    def _forward_workitem_to_activity(self, target_activity):
        '''
        Passes the process instance embedded in the given workitem 
        to a new workitem that is associated with the destination activity.
        
        @type target_activity: Activity
        @param target_activity: the activity instance to which the workitem 
                                should be forwarded
        @rtype: WorkItem
        @return: a workitem that has been passed on to the next 
                 activity (and next user)
        '''
        instance = self.instance
        # search a blocked workitem first
        qwi = WorkItem.objects.filter(instance=instance, activity=target_activity, status='blocked')
        if qwi.count() == 0:
            wi = WorkItem.objects.create(instance=instance, activity=target_activity,
                                         user=None, priority=self.priority)
            created = True
            log.info('forwarded to %s', target_activity.title)
            Event.objects.create(name='creation by %s' % self.user.username, workitem=wi)
            Event.objects.create(name='forwarded to %s' % target_activity.title, workitem=self)
            wi.workitem_from = self
        else:
            created = False
            wi = qwi[0]
        
        if target_activity.join_mode == 'and':
            nb_input_transitions = target_activity.nb_input_transitions()
            if nb_input_transitions > 1:
                if created:
                    # first worktem: block it
                    wi.block()
                    return    
                else:
                    wi.others_workitems_from.add(self)
                    if wi.others_workitems_from.all().count() + 1 < nb_input_transitions:
                        # keep blocked
                        return
                    else:
                        # check if the join is OK
                        if wi.check_join():
                            wi.status = 'inactive'
                            wi.save()
                            log.info('activity %s: workitem %s unblocked', target_activity.title, str(wi))
                        else:
                            return
        else:
            if not created:
                # join_mode='and'
                log.error('activity %s: join_mode must be and', target_activity.title)
                self.fall_out()
                wi.fall_out()
                return
        
        if target_activity.autostart:
            log.info('run auto activity %s workitem %s', target_activity.title, str(wi))
            try:
                auto_user = User.objects.get(username=settings.WF_USER_AUTO)
            except Exception:
                error = 'a user named %s (settings.WF_USER_AUTO) must be defined for auto activities'
                raise Exception(error % settings.WF_USER_AUTO)
            wi.activate(actor=auto_user)
            if wi.exec_auto_application():
                wi.complete(actor=auto_user)
            return wi
        
        if target_activity.push_application:
            target_user = wi.exec_push_application()
            log.info('application pushed to user %s', target_user.username)
            wi.user = target_user
            wi.save()
            Event.objects.create(name='assigned to %s' % target_user.username, workitem=wi)
            WorkItem.objects.notify_if_needed(user=target_user)
        else:
            wi.pull_roles = wi.activity.roles.all()
            wi.save()
            WorkItem.objects.notify_if_needed(roles=wi.pull_roles)
        return wi
    
    def check_join(self):
        log.warning('workitem check_join NYI- useful ?')
        return True
    
    def _check(self, user, status=('inactive','active')):
        '''
        helper function to determine whether process is:
            - enabled, etc..
        
        '''
        if type(status)==type(''):
            status = (status,)
            
        if not self.activity.process.enabled:
            error = 'process %s disabled.' % self.activity.process.title
            log.error('workitem._check: %s' % error)
            raise Exception(error)
            
        if not self.check_user(user):
            error = 'user %s cannot take workitem %d.' % (user.username, self.pk)
            log.error('workitem._check: %s' % error)
            self.fall_out()
            raise Exception(error)
            
        if not self.status in status:
            error = 'workitem %d has not a correct status (%s/%s).' % (
                self.pk, self.status, str(status))
            log.error('workitem._check: %s' % error)
            raise Exception(error)
        return
    
    def get_destinations(self, timeout_forwarding=False):
        #get_destinations(workitem, path=None, timeout_forwarding=False):
        '''
        Return list of destination activities that meet the conditions of each transition
        
        @type path: string??
        @param path: XXX TODO: This is not used, so don't know why it's here.
        @type timeout_forwarding: bool
        @param timeout_forwarding: a workitem with a time-delay??
        @rtype: [Activity]
        @return: list of destination activities.
        '''
        transitions = Transition.objects.filter(input=self.activity)
        if timeout_forwarding:
            transitions = transitions.filter(condition__contains='workitem.time_out')
        destinations = []
        for t in transitions:
            if self.eval_transition_condition(t):
                destinations.append(t.output)
        return destinations
    
    def eval_transition_condition(self, transition):
        '''
        evaluate the condition of a transition
        '''
        if not transition.condition:
            return True
        instance = self.instance
        wfobject = instance.wfobject()
        log.debug('eval_transition_condition %s - %s', 
            transition.condition, instance.condition)
        try:
            result = eval(transition.condition)
            
            # boolean expr
            if type(result) == type(True):
                log.debug('eval_transition_condition boolean %s', str(result))
                return result
            if type(result) == type(''):
                log.debug('eval_transition_condition cmp instance.condition %s', str(instance.condition==result))
                return (instance.condition==result)
        except Exception as v:
            log.debug('eval_transition_condition [%s]: %s', transition.condition, v)
            return (instance.condition==transition.condition)
            #log.error('eval_transition_condition [%s]: %s', transition.condition, v)
        return False
    
    def exec_push_application(self):
        '''
        Execute push application in workitem
        '''
        if not self.activity.process.enabled:
            raise Exception('process %s disabled.' % self.activity.process.title)
        params = self.activity.pushapp_param
        try:
            if params: kwargs = eval(params)
            else: kwargs = {}
            result = self.activity.push_application.execute(self, **kwargs)
        except Exception as v:
            log.error('exec_push_application %s', v)
            result = None
            self.fall_out()
        return result
    
    def exec_auto_application(self):
        '''
        creates a test auto application for activities that don't yet have applications
        @rtype: bool
        '''
        try:
            if not self.activity.process.enabled:
                raise Exception('process %s disabled.' % workitem.activity.process.title)
            # no application: default auto app
            if not self.activity.application:
                return self.default_auto_app()
            
            func, args, kwargs = resolve(self.activity.application.get_app_url())
            params = self.activity.app_param
            # params values defined in activity override those defined in urls.py
            if params:
                params = eval('{'+params.lstrip('{').rstrip('}')+'}')
                kwargs.update(params)
            func(workitem=self , **kwargs)
            return True
        except Exception as v:
            log.error('execution wi %s:%s', self, v)
        return False
    
    def default_auto_app(self):
        '''
        retrieves wfobject, logs info to it saves
        
        @rtype: bool
        @return: always returns True
        '''
        obj = self.instance.wfobject()
        obj.history += '\n>>> execute auto activity: [%s]' % self.activity.title
        obj.save()
        return True
    
    def activate(self, actor):
        '''
        changes workitem status to 'active' and logs event, activator
        
        '''
        self._check(actor, ('inactive', 'active'))
        if self.status == 'active':
            log.warning('activate_workitem actor %s workitem %s already active', 
                        actor.username, str(self))
            return
        self.status = 'active'
        self.user = actor
        self.save()
        log.info('activate_workitem actor %s workitem %s', 
                 actor.username, str(self))
        Event.objects.create(name='activated by %s' % actor.username, workitem=self)
    
    def complete(self, actor):
        '''
        changes status of workitem to 'complete' and logs event
        '''
        self._check(actor, 'active')
        self.status = 'complete'
        self.user = actor
        self.save()
        log.info('complete_workitem actor %s workitem %s', actor.username, str(self))
        Event.objects.create(name='completed by %s' % actor.username, workitem=self)
        
        if self.activity.autofinish:
            log.debug('activity autofinish: forward')
            self.forward()
        
        # if end activity, instance is complete
        if self.instance.process.end == self.activity:
            log.info('activity end process %s' % self.instance.process.title)
            # first test subflow
            lwi = WorkItem.objects.filter(activity__subflow=self.instance.process,
                                          status='blocked',
                                          instance=self.instance)
            if lwi.count() > 0:
                log.info('parent process for subflow %s' % self.instance.process.title)
                workitem0 = lwi[0]
                workitem0.instance.process = workitem0.activity.process
                workitem0.instance.save()
                log.info('process change for instance %s' % workitem0.instance.title)
                workitem0.status = 'complete'
                workitem0.save()
                workitem0.forward(subflow_workitem=self)
            else:
                self.instance.set_status('complete')
    
    def start_subflow(self, actor=None):
        '''
        starts subflow and blocks passed in workitem
        '''
        if not actor: actor = self.user
        subflow_begin_activity = self.activity.subflow.begin
        instance = self.instance
        instance.process = self.activity.subflow
        instance.save()
        self.status = 'blocked'
        self.blocked = True
        self.save()
        
        sub_workitem = self._forward_workitem_to_activity(subflow_begin_activity)
        return sub_workitem
    
    def eval_condition(self, transition):
        '''
        evaluate the condition of a transition
        '''
        raise Exception("New API (not yet implemented)")
    
    def __str__(self):
        return str(self.pk)
    
    def __unicode__(self):
        return '%s-%s-%s' % (self.instance.__unicode__(), self.activity.__unicode__(), str(self.pk))
    
    def has_workitems_to(self):
        b = ( self.workitems_to.count() > 0 )
        return b
    
    def check_user(self, user):
        """returns True if authorized, False if not.
        
        For dummy activities, returns always True
        """
        if self.activity.kind == 'dummy':
            return True
        
        if user and self.user and self.user != user:
            return False
        ugroups = user.groups.all()
        agroups = self.activity.roles.all()
        authorized = False
        if agroups and len(agroups) > 0:
            for g in ugroups:
                if g in agroups:
                    authorized = True
                    break
        else:
            authorized = True
        return authorized
            
    def set_user(self, user, commit=True):
        """affect user if he has a role authorized for activity.
        
        return True if authorized, False if not (workitem then falls out)
        """
        if self.check_user(user):
            self.user = user
            if commit: self.save()
            return True
        self.fallOut()
        return False
    
    def can_priority_change(self):
        '''can the user change priority.
        
        @rtype: bool
        @return: returns True if the user can change priority
        
        the user must belong to a group with "workitem.can_change_priority"  permission,
        and this group's name must be the same as the process title.
        '''
        if self.user.has_perm("workitem.can_change_priority"):
            lst = self.user.groups.filter(name=self.instance.process.title)
            if lst.count()==0 or \
               (lst[0].permissions.filter(codename='can_change_priority').count() == 0):
                return False
            return True
        return False
    
    def block(self):
        self.status = 'blocked'
        self.save()
        Event.objects.create(name='blocked', workitem=self)
    
    def fall_out(self):
        self.status = 'fallout'
        self.save()
        Event.objects.create(name='fallout', workitem=self)
        if not settings.DEBUG:
            mail_admins(subject='workflow workitem %s fall out' % str(self.pk),
                    message='''
The workitem [%s] was falling out.

Process:  %s
Activity: %s
instance: %s
----------------------------------
                    ''' % (
                           self.instance.process,
                           self.activity,
                           self.instance,
                           ))
    
    def html_action(self):
        label = 'action'
        if self.status == 'inactive':
            label = 'activate'
            url='activate/%d/' % self.id
        if self.status == 'active':
            label = 'complete'
            url='complete/%d/' % self.id
        if self.status == 'complete':
            return 'completed'
        return '<a href=%s>%s</a>' % (url, label)
    
    def html_action_link(self):
        if self.status == 'inactive':
            url='activate/%d/' % self.id
        if self.status == 'active':
            url='complete/%d/' % self.id
        if self.status == 'complete':
            raise Exception('no action for completed workitems')
        return url
    
    def time_out(self, delay, unit='days'):
        '''
        return True if timeout reached
          delay:    nb units
          unit: 'weeks' | 'days' | 'hours' ... (see timedelta)
        '''
        tdelta = eval('timedelta('+unit+'=delay)')
        now = datetime.now()
        return (now > (self.date + tdelta))
    
    @allow_tags
    def events_list(self):
        '''provide html link to events for a workitem in admin change list.
        @rtype: string
        @return: html href link "../event/?workitem__id__exact=[self.id]&ot=asc&o=0"
        '''
        nbevt = self.events.count()
        return '<a href=../event/?workitem__id__exact=%d&ot=asc&o=0>%d item(s)</a>' % (self.pk, nbevt)
    
    class Meta:
        permissions = (
            ("can_change_priority", "Can change priority"),
        ) class DefaultAppModel(models.Model):
    """Default implementation object class  for process instances.
    
    When a process instance starts, the instance has to carry an
    implementation object that contains the application data. The
    specifications for the implementation class is:
    
    (nothing: now managed by generic relation)
    
    This model is used in process simulations: you don't have to define
    application in activities for this; the DefaultAppModel is used
    to keep workflow history for displaying to users.
    """
    history = models.TextField(editable=False, null=True, blank=True)
    comment = models.TextField(null=True, blank=True)
    
    def __unicode__(self):
        return 'simulation model %s' % str(self.id)
    class Admin:
        list_display = ('__unicode__',)
    class Meta:
        verbose_name='Simulation object' class Icon(models.Model):
    '''
    An image accessible by an url.

    Tip: all of the "Image" objects can be imported as "Icon" from the
    admin panel.
    '''
    category = models.CharField(max_length=20, null=True, blank=True)
    url = models.URLField()
    
    @allow_tags
    def graphic(self):
        '''
        generates an *img* html tag for html rendering
        '''
        return '<img name="image%d" src="%s">' % (self.pk, self.url)
    
    @allow_tags
    def graphic_input(self):
        '''
        generates an *input* html tag with type=image for html rendering
        '''
        return '<input type=image name=icon src="%s">' % self.url
    
    def __unicode__(self):
        return self.url class Image(models.Model):
    '''
    An image stored in the database
    '''
    category = models.CharField(max_length=20, null=True, blank=True)
    file = models.ImageField(upload_to='images')
    
    def url(self):
        return "%s%s" % (settings.MEDIA_URL, self.file)
    
    @allow_tags
    def graphic(self):
        '''
        generates an *img* html tag for html rendering
        '''
        return '<img name="image%d" src="%s">' % (self.pk, self.url())
    
    @allow_tags
    def graphic_input(self):
        '''
        generates an *input* html tag with type=image for html rendering
        '''
        return '<input type=image name=icon src=%s>' % self.get_file_url()
    
    def __unicode__(self):
        return str(self.file) class ImageButton(models.Model):
    '''
    Mapping object between an "action" and an "Icon".
    
    ImageButton objects have also a textual field: label.
    '''
    action = models.SlugField(primary_key=True)
    label = models.CharField(max_length=100)
    icon = models.ForeignKey(Icon)
    
    @allow_tags
    def graphic(self):
        '''
        generates an *img* html tag for html rendering
        '''
        return '<img name="image-%s" src="%s">' % (self.pk, self.icon.url)
    
    @allow_tags
    def graphic_input(self):
        '''
        generates an *input* html tag with type=image for html rendering
        '''
        return '<input type=image name=image src="%s" value="%s" title="%s">' % (self.icon.url, self.pk, self.label)
    
    def __unicode__(self):
        return self.label class DefaultAppForm(BaseForm):
    def save(self, workitem=None, submit_value=None, commit=True):
        ob = super(DefaultAppForm, self).save(commit=False)
        if ob.comment:
            if not ob.history:
                ob.history = 'Init'
            ob.history += '\n---------'
            if workitem:
                ob.history += '\nActivity: [%s]' % workitem.activity.title
            ob.history += '\n%s\n%s' % (datetime.now().isoformat(' '), ob.comment)
            ob.comment = None
        if submit_value:
            if ob.history:
                ob.history += '\n button clicked: [%s]' % submit_value
        ob.save()
        return ob

    class Meta:
         model = DefaultAppModel
         exclude = ('reasonDenial',) class Log(object):
    def __init__(self, module):
        self.log = logging.getLogger(module)
        # self._event = get_model('runtime', 'Event').objects

    def __getattr__(self, name):
        try:
            return getattr(self.log, name)
        except AttributeError as e:
            return getattr(self, name)

    def __call__(self, *args):
        if len(args) == 0: 
            return
        elif len(args) == 1:
            self.log.info(args[0] )
        else:
            self.log.info(" ".join([str(i) for i in args]))

    def event(self, msg, workitem):
        # self._event.create(name=msg, workitem=workitem)
        self.log.info('EVENT: [%s] %s' % (workitem.__unicode__(), msg)) def send_mail(workitems=None, user=None, subject='message', template='mail.txt'):
    # subject
    try:
        subject = settings.EMAIL_SUBJECT_PREFIX + subject
    except Exception:
        pass
    t = Template(subject)
    subject = t.render(Context({'workitems': workitems,'user':user}))
    from goflow.workflow.models import UserProfile
    profile = UserProfile.objects.get(user=user) 
    message = render_to_string(template, {
                                          'workitems': workitems,
                                          'user':user,
                                          'url_prefix':'http://%s/' % profile.web_host
                                          })
    
    user.email_user(subject, message)
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.http import HttpResponseRedirect, HttpResponse
from django.contrib.contenttypes.models import ContentType
from .forms import ContentTypeForm
from goflow.workflow.models import Process, Application, Transition
from goflow.runtime.models import ProcessInstance, WorkItem
from django.db import models
from django.contrib.auth.models import User
from django.forms.models import modelform_factory
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.decorators import login_required
from .models import DefaultAppModel, Icon, Image, ImageButton
from .forms import DefaultAppForm
from django.conf import settings
from goflow.workflow.logger import Log; log = Log('goflow.apptools.views')
from goflow.workflow.notification import send_mail
    import re
        name for submit buttons - default='image'
    cancel_value
        cancel button value - default='Cancel'
    extra_context
        default={}
    '''
    workitem = WorkItem.objects.get_safe(int(id), user=request.user)
    activity = workitem.activity
    if activity.split_mode != 'xor':
        raise Exception('choice_application: split_mode xor required')
    list_trans = Transition.objects.filter(input=activity)
    ok_values = []
    for t in list_trans:
        if ImageButton.objects.filter(action=t.condition).count() == 0:
            raise Exception('no ImageButton for action [%s]' % t.condition)
        ok_values.append(t.condition)
        
    return view_application(request, id, template, redirect, title,
               submit_name, ok_values, cancel_action, extra_context)

def sendmail(workitem, subject='goflow.apptools sendmail message', template='goflow/app_sendmail.txt'):
    '''send a mail notification to the workitem user.
    
    parameters:
    
    subject
        default='goflow.apptools sendmail message'
    template
        default='goflow/app_sendmail.txt'
    '''
    send_mail(workitems=(workitem,), user=workitem.user, subject=subject, template=template)

def override_app_params(activity, name, value):
    '''
    usage: param = _override_app_params(activity, 'param', param)
    '''
    try:
        if not activity.app_param:
            return value
        dicparams = eval(activity.app_param)
        if name in dicparams:
            return dicparams[name]
    except Exception as v:
        log.error('_override_app_params %s %s - %s', activity, name, v)
    return value


@login_required
def app_env(request, action, id, template=None):
    """
    creates/removes unit test environment for applications.
    
    a process named "test_[app]" with one activity
    a group with appropriate permission
    """
    app = Application.objects.get(id=int(id))
    rep = 'Nothing done.'
    if action == 'create':
        app.create_test_env(user=request.user)
        rep = 'test env created for app %s' % app.url
    if action == 'remove':
        app.remove_test_env()
        rep = 'test env removed for app %s' % app.url
    
    rep += '<hr><p><b><a href=../../../>return</a></b>'
    return HttpResponse(rep)

@login_required
def test_start(request, id, template='goflow/test_start.html'):
    """
    starts test instances.
    
    for a given application, with its unit test environment, the user
    choose a content-type then generates unit test process instances
    by cloning existing content-type objects (**Work In Progress**).
    """
    app = Application.objects.get(id=int(id))
    context = {}
    if request.method == 'POST':
        submit_value = request.POST['action']
        if submit_value == 'Create':
            ctype = ContentType.objects.get(id=int(request.POST['ctype']))
            model = ctype.model_class()
            for inst in model.objects.all():
                # just objects without link to a workflow instance
                if ProcessInstance.objects.filter(
                    content_type__pk=ctype.id,
                    object_id=inst.id
                ).count() > 0:
                    continue
                inst.id = None
                inst.save()
                #TODO: convert this to method
                ProcessInstance.objects.start(
                #start_instance(
                            process_name='test_%s' % app.url,
                            user=request.user, item=inst, 
                            title="%s test instance for app %s" % (
                                ctype.name, app.url
                            ))
            request.user.message_set.create(message='test instances created')
        return HttpResponseRedirect('../..')
    form = ContentTypeForm()
    context['form'] = form
    return render_to_response(template, context)


@login_required
def image_update(request):
    '''
    Import Image instances as Icon instances.
    
    GoFlow can use local images as well as http distant images.
    For a genericity reason, local images (Image) are obtained as
    Icon images by their url; this view is useful to wrap all local
    images in a row. 
    '''
    rep = '<h1>Update Icons from Images</h1>'
    for im in Image.objects.all():
        if Icon.objects.filter(url__endswith=str(im.url)).count() == 0:

            ic, created = Icon.objects.get_or_create(category='local-'+im.category, url=im.url())