├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE.txt ├── README.md ├── activflow ├── __init__.py ├── core │ ├── __init__.py │ ├── constants.py │ ├── helpers.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── mixins.py │ ├── models.py │ ├── processors.py │ ├── templatetags │ │ ├── __init__.py │ │ └── core_tags.py │ ├── urls.py │ └── views.py ├── settings │ ├── __init__.py │ ├── base.py │ ├── development.py │ ├── production.py │ └── staging.py ├── static │ ├── admin │ │ ├── css │ │ │ ├── base.css │ │ │ ├── changelists.css │ │ │ ├── dashboard.css │ │ │ ├── fonts.css │ │ │ ├── forms.css │ │ │ ├── login.css │ │ │ ├── rtl.css │ │ │ └── widgets.css │ │ ├── fonts │ │ │ ├── LICENSE.txt │ │ │ ├── README.txt │ │ │ ├── Roboto-Bold-webfont.woff │ │ │ ├── Roboto-Light-webfont.woff │ │ │ └── Roboto-Regular-webfont.woff │ │ ├── img │ │ │ ├── LICENSE │ │ │ ├── README.txt │ │ │ ├── calendar-icons.svg │ │ │ ├── gis │ │ │ │ ├── move_vertex_off.svg │ │ │ │ └── move_vertex_on.svg │ │ │ ├── icon-addlink.svg │ │ │ ├── icon-alert.svg │ │ │ ├── icon-calendar.svg │ │ │ ├── icon-changelink.svg │ │ │ ├── icon-clock.svg │ │ │ ├── icon-deletelink.svg │ │ │ ├── icon-no.svg │ │ │ ├── icon-unknown-alt.svg │ │ │ ├── icon-unknown.svg │ │ │ ├── icon-yes.svg │ │ │ ├── inline-delete.svg │ │ │ ├── search.svg │ │ │ ├── selector-icons.svg │ │ │ ├── sorting-icons.svg │ │ │ ├── tooltag-add.svg │ │ │ └── tooltag-arrowright.svg │ │ └── js │ │ │ ├── SelectBox.js │ │ │ ├── SelectFilter2.js │ │ │ ├── actions.js │ │ │ ├── actions.min.js │ │ │ ├── admin │ │ │ ├── DateTimeShortcuts.js │ │ │ └── RelatedObjectLookups.js │ │ │ ├── calendar.js │ │ │ ├── collapse.js │ │ │ ├── collapse.min.js │ │ │ ├── core.js │ │ │ ├── inlines.js │ │ │ ├── inlines.min.js │ │ │ ├── jquery.init.js │ │ │ ├── prepopulate.js │ │ │ ├── prepopulate.min.js │ │ │ ├── timeparse.js │ │ │ ├── urlify.js │ │ │ └── vendor │ │ │ ├── jquery │ │ │ ├── LICENSE-JQUERY.txt │ │ │ ├── jquery.js │ │ │ └── jquery.min.js │ │ │ └── xregexp │ │ │ ├── LICENSE-XREGEXP.txt │ │ │ └── xregexp.min.js │ ├── css │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ └── bootstrap.min.css │ ├── django_extensions │ │ ├── css │ │ │ └── jquery.autocomplete.css │ │ ├── img │ │ │ └── indicator.gif │ │ └── js │ │ │ ├── jquery.ajaxQueue.js │ │ │ ├── jquery.autocomplete.js │ │ │ └── jquery.bgiframe.js │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.woff2 │ └── js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── jquery-2.1.3.js │ │ ├── jquery-2.1.3.min.js │ │ ├── jquery.formset.js │ │ └── npm.js ├── templates │ ├── base.html │ ├── core │ │ ├── create.html │ │ ├── denied.html │ │ ├── detail.html │ │ ├── update.html │ │ ├── widgets │ │ │ ├── confirmation.html │ │ │ ├── form.html │ │ │ ├── history.html │ │ │ ├── readonly.html │ │ │ └── validation.html │ │ └── workflow.html │ ├── index.html │ └── registration │ │ └── login.html ├── tests │ ├── __init__.py │ ├── actions.py │ ├── config.py │ ├── flow.py │ ├── forms.py │ ├── migrations │ │ ├── 0001_initial.py │ │ └── __init__.py │ ├── models.py │ ├── rules.py │ ├── tests.py │ └── validators.py ├── urls.py └── wsgi.py ├── config └── nginx │ └── activflow.conf ├── demo.py ├── docker-compose.yml ├── manage.py ├── requirements.txt └── scripts └── on-container-start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | ### Django ### 2 | *.log 3 | *.pot 4 | *.pyc 5 | __pycache__/ 6 | local_settings.py 7 | db.sqlite3 8 | media 9 | .vscode/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | - "3.9" 8 | 9 | env: 10 | - DJANGO_VERSION=3.0 11 | - DJANGO_VERSION=3.1 12 | - DJANGO_VERSION=3.2 13 | 14 | # command to install dependencies 15 | install: 16 | - pip install -q Django==$DJANGO_VERSION 17 | - pip install -r requirements.txt 18 | - pip install pylint 19 | - pip install coveralls 20 | 21 | # command to run tests 22 | script: 23 | - coverage run --source='.' --omit='demo.py,*wsgi.py' manage.py test activflow.tests 24 | 25 | after_success: 26 | - coveralls 27 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | LABEL maintainer="Fawad Qureshi " \ 4 | version="1.0.0" 5 | 6 | ENV PYTHONUNBUFFERED 1 7 | 8 | # Prepare 9 | RUN mkdir /app 10 | WORKDIR /app 11 | 12 | # Handle dependencies 13 | COPY requirements.txt requirements.txt 14 | RUN pip install -r requirements.txt 15 | 16 | # Deploy codebase 17 | COPY . . 18 | 19 | EXPOSE 8000 20 | 21 | # Execute on image run / container start  22 | ENTRYPOINT ["sh", "./scripts/on-container-start.sh"] 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Fawad H Qureshi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # activflow 2 | [![Build Status](https://travis-ci.org/faxad/ActivFlow.svg?branch=master)](https://travis-ci.org/faxad/ActivFlow) 3 | [![Coverage Status](https://coveralls.io/repos/github/faxad/ActivFlow/badge.svg?branch=master)](https://coveralls.io/github/faxad/ActivFlow?branch=master) 4 | [![Codacy Badge](https://api.codacy.com/project/badge/grade/f1cb2c6766cb4539ac1c3d4057996047)](https://www.codacy.com/app/fawadhq/ActivFlow) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffaxad%2FActivFlow.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffaxad%2FActivFlow?ref=badge_shield) 6 | 7 | ### Introduction 8 | **ActivFlow** is a generic, light-weight and extensible workflow engine for agile development and automation of complex Business Process operations. 9 | 10 | Developers can emphasize towards mapping the Business Process model as ActivFlow workflow without having to worry about implementing the core workflow processing logic. The generic implementation of the workflow engine manages the automation of the Business Processes from start to finish in accordance with the defined flow. 11 | 12 | What is an ActivFlow workflow? 13 | - Business Process flow mapped as ActivFlow configuration 14 | - Definition of data to be captured for each activity (state) 15 | - Business Roles mapped to workflow activities 16 | - Rules applied on transitions between activities 17 | 18 | ![alt tag](https://user-images.githubusercontent.com/6130967/28086399-5625a4e8-6698-11e7-8a00-ccf3180d70be.png) 19 | 20 | ### Technology Stack 21 | - Python 3.6+ 22 | - Django 3.x 23 | - Bootstrap 3.x 24 | 25 | ### Usage & Configuration 26 | 27 | #### Step 1: Workflow App Registration 28 | - A Business Process must be represented in terms of a Django app 29 | - All apps must be registered to **WORKFLOW_APPS** under **core/constants** 30 | ```python 31 | WORKFLOW_APPS = ['leave_request'] 32 | ``` 33 | 34 | #### Step 2: Activity Configuration 35 | - Activities (States/Nodes) are represented as Django models 36 | - Activity must inherit from **AbstractInitialActivity**/**AbstractActivity** respectively 37 | - Custom validation logic must be defined under **clean()** on the activity model 38 | - Custom field specific validation should go under **app/validator** and applied to the field as **validators** attribute 39 | ```python 40 | from activflow.core.models import AbstractActivity, AbstractInitialActivity, AbstractEntity 41 | from activflow.leave_request.validators import validate_initial_cap 42 | 43 | class SampleRequest(AbstractInitialActivity): 44 | """Sample leave request details""" 45 | employee_name = CharField("Employee", max_length=200, validators=[validate_initial_cap]) 46 | from = DateField("From Date") 47 | to = DateField("To Date") 48 | reason = TextField("Purpose of Leave", blank=True) 49 | 50 | def clean(self): 51 | """Custom validation logic should go here""" 52 | pass 53 | 54 | class Itinerary(AbstractEntity): 55 | """Itinerary details""" 56 | request = ForeignKey(RequestInitiation, related_name="itineraries") 57 | destination = CharField("Destination", max_length=200) 58 | date = DateField("Visit Date") 59 | 60 | def clean(self): 61 | """Custom validation logic should go here""" 62 | pass 63 | 64 | class ManagementReview(AbstractActivity): 65 | """Management review/approval""" 66 | approval_status = CharField(verbose_name="Status", max_length=3, choices=( 67 | ('APP', 'Approved'), ('REJ', 'Rejected'))) 68 | remarks = TextField("Remarks") 69 | 70 | def clean(self): 71 | """Custom validation logic should go here""" 72 | pass 73 | 74 | ``` 75 | #### Step 3: Flow Definition 76 | - A flow is represented by collection of Activities (States/Nodes) connected using Transitions (Edges) 77 | - Rules are applied on transitions to allow routing between activities, provided, the condition satisfies 78 | - Business Process flow must be defined as **FLOW** under **app/flow** 79 | - As a default behavior, the Role maps OTO with django Group (developers, feel free to customize) 80 | ```python 81 | from activflow.leave_request.models import SampleRequest, ManagementReview 82 | from activflow.leave_request.rules import validate_request 83 | 84 | FLOW = { 85 | 'sample_leave_request': { 86 | 'name': 'Sample Request', 87 | 'model': SampleRequest, 88 | 'role': 'Submitter', 89 | 'transitions': { 90 | 'management_review': validate_request, 91 | } 92 | }, 93 | 'management_review': { 94 | 'name': 'Management's Review', 95 | 'model': ManagementReview, 96 | 'role': 'Approver', 97 | 'transitions': None 98 | } 99 | } 100 | 101 | INITIAL = 'sample_leave_request' 102 | ``` 103 | #### Step 4: Business Rules 104 | - Transition rules and conditions must be defined under **app/rules** 105 | ```python 106 | def validate_request(self): 107 | return self.reason == 'Emergency' 108 | ``` 109 | 110 | #### Step 5: Configure Field Visibility & Custom Forms (Optional) 111 | - Include **config.py** in the workflow app and define **ACTIVITY_CONFIG** as Nested Ordered Dictionary to have more control over what gets displayed on the UI. 112 | - Define for each activity model, the visibility of fields, for display on templates and forms 113 | - **create:** field will appear on activity create/initiate form 114 | - **update:** field will be available for activity update/revise operation 115 | - **display:** available for display in activity detail view 116 | ```python 117 | from collections import OrderedDict 118 | 119 | 120 | ACTIVITY_CONFIG = OrderedDict([ 121 | ('Foo', OrderedDict([ 122 | ('Fields', OrderedDict([ 123 | ('subject', ['create', 'update', 'display']), 124 | ('employee_name', ['create', 'update', 'display']), 125 | ('from', ['create', 'update', 'display']), 126 | ('to', ['create', 'update', 'display']), 127 | ('reason', ['create', 'update', 'display']), 128 | ('creation_date', ['display']), 129 | ('last_updated', ['display']) 130 | ])), 131 | ('Relations', OrderedDict([ 132 | ('Itinerary', OrderedDict([ 133 | ('destination', ['create', 'update', 'display']), 134 | ('date', ['create', 'update', 'display']) 135 | ])), 136 | ... 137 | ])) 138 | ])), 139 | ... 140 | ]) 141 | 142 | # register fields that need WYSIWYG editor 143 | 144 | WYSIWYG_CONFIG = { 145 | 'RequestInitiation': ['reason'] 146 | } 147 | 148 | # register custom forms 149 | 150 | FORM_CONFIG = { 151 | 'RequestInitiation': 'CustomRequestForm' 152 | } 153 | ``` 154 | 155 | #### Step 6: Access/Permission Configuration (Optional) 156 | The core logic to restrict access is defined as **AccessDeniedMixin** under **core/mixins** which developers can customize depending on the requirements 157 | 158 | #### Demo Instructions 159 | Execute the below command to configure ActivFlow for demo purpose 160 | ``` 161 | python demo.py 162 | ``` 163 | Alternatively, launch the docker containers by simply running 164 | ``` 165 | docker-compose up 166 | ``` 167 | **Submitter:** john.doe/12345, **Reviewer:** jane.smith/12345 168 | 169 | 170 | ## License 171 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Ffaxad%2FActivFlow.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Ffaxad%2FActivFlow?ref=badge_large) -------------------------------------------------------------------------------- /activflow/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/__init__.py -------------------------------------------------------------------------------- /activflow/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/core/__init__.py -------------------------------------------------------------------------------- /activflow/core/constants.py: -------------------------------------------------------------------------------- 1 | """Core workflow configuration/constants""" 2 | 3 | REQUEST_IDENTIFIER = 'Initial' 4 | 5 | REQUEST_STATUS = ( 6 | ('Initiated', 'Initiated'), 7 | ('Withdrawn', 'Withdrawn'), 8 | ('Completed', 'Completed') 9 | ) 10 | 11 | TASK_STATUS = ( 12 | ('Not Started', 'Not Started'), 13 | ('In Progress', 'In Progress'), 14 | ('Rolled Back', 'Rolled Back'), 15 | ('Completed', 'Completed') 16 | ) 17 | # register workflow apps here 18 | # enable 'tests' app only for manual testing purpose 19 | 20 | WORKFLOW_APPS = [ 21 | 'tests' 22 | ] 23 | -------------------------------------------------------------------------------- /activflow/core/helpers.py: -------------------------------------------------------------------------------- 1 | """Helpers""" 2 | 3 | import inspect 4 | from importlib import import_module 5 | 6 | from django.apps import apps 7 | from django.forms import inlineformset_factory 8 | from django.forms.models import modelform_factory 9 | 10 | 11 | # Configuration Loaders 12 | 13 | def workflow_config(module): 14 | """Returns workflow configuration""" 15 | return import_module( 16 | '{}.config'.format( 17 | apps.get_app_config(module).name)) 18 | 19 | 20 | def activity_config(module, model): 21 | """Returns activity configuration""" 22 | return workflow_config( 23 | module).ACTIVITY_CONFIG[model] 24 | 25 | 26 | def flow_config(module): 27 | """Returns flow configuration""" 28 | return import_module( 29 | '{}.flow'.format(apps.get_app_config(module).name)) 30 | 31 | 32 | def transition_config(module, activity): 33 | """Returns all possible transitions 34 | for a given workflow module and activity 35 | """ 36 | return flow_config( 37 | module).FLOW[activity]['transitions'] 38 | 39 | 40 | def wysiwyg_config(module, activity): 41 | """Returns configuration for WYSIWYG editor""" 42 | return workflow_config( 43 | module).WYSIWYG_CONFIG[activity] 44 | 45 | 46 | def form_config(module, activity): 47 | """Returns custom form configuration""" 48 | return workflow_config( 49 | module).FORM_CONFIG[activity] 50 | 51 | 52 | # Request Helpers 53 | 54 | 55 | def get_request_params(what, request=None, **kwargs): 56 | """Returns requested argument value""" 57 | args = {'app_name': 1, 'model_name': 2, 'pk': 4} 58 | 59 | try: 60 | return kwargs.get( 61 | what, request.path.split('/')[ 62 | args[what]] if request else None) 63 | except IndexError: 64 | pass 65 | 66 | 67 | # Model Helpers 68 | 69 | 70 | def get_app_model_as_params(**kwargs): 71 | """Returns a list of app and model name as params""" 72 | return [get_request_params( 73 | key, **kwargs) for key in ('app_name', 'model_name')] 74 | 75 | 76 | def get_model(**kwargs): 77 | """Returns model""" 78 | args = get_app_model_as_params(**kwargs) 79 | return apps.get_model(*args) 80 | 81 | 82 | def get_model_instance(**kwargs): 83 | """Returns model instance""" 84 | return get_model(**kwargs).objects.get(id=kwargs.get("pk")) 85 | 86 | 87 | def get_fk(instances, **kwargs): 88 | """Returns foreign key field""" 89 | fields = [] 90 | model = get_model(**kwargs) 91 | for field in instances[0]._meta.fields: 92 | if field.get_internal_type() == 'ForeignKey': 93 | fields.append(field) 94 | 95 | for field in fields: 96 | if field.related_model == model: 97 | return field.name 98 | 99 | 100 | # Form Helpers 101 | 102 | def get_custom_form(**kwargs): 103 | """Returns custom form instance""" 104 | try: 105 | args = get_app_model_as_params(**kwargs) 106 | config = form_config(*args) 107 | return getattr(import_module( 108 | '{}.forms'.format(apps.get_app_config(args[0]).name)), config) 109 | except (AttributeError, KeyError): 110 | return None 111 | 112 | 113 | def get_form_config(what, **kwargs): 114 | """Returns form config""" 115 | args = get_app_model_as_params(**kwargs) 116 | return activity_config(*args)[what] 117 | 118 | 119 | def get_form_fields(operation, field_config): 120 | """Returns form fields""" 121 | return [field for field in field_config if ( 122 | operation in field_config[field])] 123 | 124 | 125 | def get_form(**kwargs): 126 | """Returns form instance""" 127 | callee = type(inspect.currentframe().f_back.f_locals['self']).__name__ 128 | operation = 'create' if 'Create' in callee else 'update' 129 | 130 | try: 131 | config = get_form_config('Fields', **kwargs) 132 | fields = get_form_fields(operation, config) 133 | except KeyError: 134 | fields = [field for field in ( 135 | field.name for field in get_model(**kwargs)().class_meta. 136 | get_fields()) if field not in [ 137 | 'id', 'task', 'task_id', 'last_updated', 'creation_date']] 138 | arguments = {'fields': fields} 139 | custom_form = get_custom_form(**kwargs) 140 | 141 | if custom_form: 142 | arguments['form'] = custom_form 143 | 144 | return modelform_factory(get_model(**kwargs), **arguments) 145 | 146 | 147 | def get_formsets(what, extra=0, **kwargs): 148 | """Returns a list of formset instances""" 149 | try: 150 | related_fields = {} 151 | relation_config = get_form_config('Relations', **kwargs) 152 | operation = 'create' if 'Create' in what else 'update' 153 | 154 | for relation in relation_config: 155 | field_config = relation_config[relation] 156 | related_fields[relation] = get_form_fields(operation, field_config) 157 | 158 | def get_related_model(relation): 159 | """Returns related model""" 160 | args = get_app_model_as_params(**kwargs) 161 | args.pop() 162 | args.append(relation) 163 | 164 | return apps.get_model(*args) 165 | 166 | return [inlineformset_factory( 167 | get_model(**kwargs), 168 | get_related_model(relation), 169 | fields=related_fields[relation], 170 | extra=extra 171 | ) for relation in related_fields] 172 | except KeyError: 173 | return [] 174 | -------------------------------------------------------------------------------- /activflow/core/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ('auth', '0007_alter_validators_add_error_messages'), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Request', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation Date')), 24 | ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), 25 | ('module_ref', models.CharField(max_length=100)), 26 | ('status', models.CharField(choices=[('Initiated', 'Initiated'), ('Withdrawn', 'Withdrawn'), ('Completed', 'Completed')], max_length=30, verbose_name='Status')), 27 | ('requester', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='requests', to=settings.AUTH_USER_MODEL)), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | migrations.CreateModel( 34 | name='Task', 35 | fields=[ 36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 37 | ('creation_date', models.DateTimeField(auto_now_add=True, verbose_name='Creation Date')), 38 | ('last_updated', models.DateTimeField(auto_now=True, verbose_name='Last Updated')), 39 | ('activity_ref', models.CharField(max_length=100)), 40 | ('status', models.CharField(choices=[('Not Started', 'Not Started'), ('In Progress', 'In Progress'), ('Rolled Back', 'Rolled Back'), ('Completed', 'Completed')], max_length=30, verbose_name='Status')), 41 | ('assignee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')), 42 | ('request', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tasks', to='core.Request')), 43 | ('updated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), 44 | ], 45 | options={ 46 | 'abstract': False, 47 | }, 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /activflow/core/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/core/migrations/__init__.py -------------------------------------------------------------------------------- /activflow/core/mixins.py: -------------------------------------------------------------------------------- 1 | """Mixins""" 2 | 3 | from django.contrib.auth.mixins import LoginRequiredMixin 4 | from django.db.models import Q 5 | from django.shortcuts import render 6 | 7 | from activflow.core.constants import REQUEST_IDENTIFIER 8 | from activflow.core.helpers import ( 9 | get_model, 10 | flow_config, 11 | get_request_params 12 | ) 13 | 14 | from activflow.core.models import Task 15 | 16 | 17 | class AccessDeniedMixin(LoginRequiredMixin, object): 18 | """Checks the permission""" 19 | def check(self, request, **kwargs): 20 | """ 21 | - Super user can perform all activities 22 | - Requester can view all activities 23 | - Assignee can view all assigned activities 24 | - Assignee can initiate activity operation 25 | - Assignee can update activity details 26 | - Historical activities cannot be updated 27 | - TODO: Entire request can be deleted 28 | 29 | *assignee: Users who belong to a Group configured to play 30 | a specific role in the Business Process 31 | """ 32 | model = get_model(**kwargs) 33 | view = self.__class__.__name__ 34 | user = request.user 35 | groups = list(user.groups.all()) 36 | 37 | if self.request.user.is_superuser: 38 | return 39 | 40 | def assignee_check(): 41 | """Checks if logged-in user is task assignee""" 42 | return model.objects.filter(task__assignee__in=groups).count() == 0 43 | 44 | def check_for_view(): 45 | """Check for view/display operation""" 46 | return model.objects.filter( 47 | Q(task__assignee__in=groups) | 48 | Q(task__request__requester=user) 49 | ).count() == 0 50 | 51 | def check_for_create(): 52 | """Check for create/initiate operation""" 53 | module = get_request_params('app_name', request, **kwargs) 54 | flow = flow_config(module).FLOW 55 | initial = flow_config(module).INITIAL 56 | identifier = get_request_params( 57 | 'pk', request, **kwargs) 58 | 59 | activity = initial if identifier == REQUEST_IDENTIFIER \ 60 | else Task.objects.get(id=identifier).activity_ref 61 | 62 | return flow[activity]['role'] not in [ 63 | group.name for group in groups] 64 | 65 | def check_for_update(): 66 | """Check for update/revise operation""" 67 | return any([ 68 | assignee_check(), 69 | not self.task.can_revise_activity if hasattr( 70 | self, 'task') else False 71 | ]) 72 | 73 | return render( 74 | request, 'core/denied.html') if { 75 | 'ViewActivity': check_for_view, 76 | 'CreateActivity': check_for_create, 77 | 'UpdateActivity': check_for_update, 78 | }.get(view)() else None 79 | -------------------------------------------------------------------------------- /activflow/core/models.py: -------------------------------------------------------------------------------- 1 | """Model definition for workflow operations""" 2 | 3 | from django.contrib.auth.models import User, Group 4 | from django.db.models import ( 5 | Model, 6 | CharField, 7 | DateTimeField, 8 | OneToOneField, 9 | ForeignKey, 10 | CASCADE) 11 | 12 | from activflow.core.constants import ( 13 | REQUEST_STATUS, 14 | TASK_STATUS) 15 | 16 | from activflow.core.helpers import ( 17 | flow_config, 18 | transition_config) 19 | 20 | 21 | class AbstractEntity(Model): 22 | """Common attributes for all models""" 23 | creation_date = DateTimeField('Creation Date', auto_now_add=True) 24 | last_updated = DateTimeField('Last Updated', auto_now=True) 25 | 26 | class Meta(object): 27 | abstract = True 28 | 29 | @property 30 | def class_meta(self): 31 | """Returns class meta""" 32 | return self._meta 33 | 34 | @property 35 | def title(self): 36 | """Returns entity title""" 37 | return self.__class__.__name__ 38 | 39 | @property 40 | def module_label(self): 41 | """Returns module label""" 42 | return self.class_meta.app_label 43 | 44 | @property 45 | def code(self): 46 | """Returns a unique code""" 47 | return "{0}-{1}-{2}".format( 48 | self.class_meta.app_label, 49 | self.title, 50 | self.id) 51 | 52 | def __unicode__(self): 53 | """Returns ID""" 54 | return str(self.id) 55 | 56 | 57 | class Request(AbstractEntity): 58 | """Defines the workflow request""" 59 | requester = ForeignKey(User, related_name='requests', on_delete=CASCADE) 60 | module_ref = CharField(max_length=100) 61 | status = CharField( 62 | verbose_name="Status", max_length=30, choices=REQUEST_STATUS) 63 | 64 | 65 | class Task(AbstractEntity): 66 | """Defines the workflow task""" 67 | request = ForeignKey(Request, related_name='tasks', on_delete=CASCADE) 68 | assignee = ForeignKey(Group, on_delete=CASCADE) 69 | updated_by = ForeignKey(User, on_delete=CASCADE) 70 | activity_ref = CharField(max_length=100) 71 | status = CharField( 72 | verbose_name="Status", max_length=30, choices=TASK_STATUS) 73 | 74 | @property 75 | def activity(self): 76 | """Returns the activity associated with the task""" 77 | flow = flow_config(self.request.module_ref).FLOW 78 | return getattr( 79 | self, flow[self.activity_ref]['model']().title.lower(), None) 80 | 81 | @property 82 | def is_active(self): 83 | """Checks if the current task is active / most recent""" 84 | return self == self.request.tasks.latest('id') 85 | 86 | @property 87 | def is_final(self): 88 | """Checks if the current task is final / end task""" 89 | transitions = transition_config( 90 | self.request.module_ref, self.activity_ref) 91 | 92 | return not transitions 93 | 94 | @property 95 | def previous(self): 96 | """Returns previous task""" 97 | return Task.objects.filter( 98 | request=self.request, id__lt=self.id).latest('id') 99 | 100 | @property 101 | def can_view_activity(self): 102 | """Checks if activity can be viewed""" 103 | return self.activity 104 | 105 | @property 106 | def can_initiate_activity(self): 107 | """Checks if new activity can be initiated""" 108 | return not self.activity 109 | 110 | @property 111 | def can_revise_activity(self): 112 | """Checks if activity can be revised""" 113 | return all([self.activity, self.is_active]) 114 | 115 | @property 116 | def can_rollback(self): 117 | """Checks if activity can be rolled back""" 118 | return not any([ 119 | self.activity.is_initial, 120 | self.status == 'Completed']) 121 | 122 | def initiate(self): 123 | """Initializes the task""" 124 | self.status = 'In Progress' 125 | self.save() 126 | 127 | def submit(self, module, user, next_activity=None): 128 | """Submits the task""" 129 | config = flow_config(module) 130 | transitions = transition_config(module, self.activity_ref) 131 | role = Group.objects.get( 132 | name=config.FLOW[next_activity]['role']) 133 | 134 | self.status = 'Completed' 135 | self.save() 136 | 137 | if transitions is not None: 138 | Task.objects.create( 139 | request=self.request, 140 | assignee=role, 141 | updated_by=user, 142 | activity_ref=next_activity, 143 | status='Not Started') 144 | else: 145 | self.request.status = 'Completed' 146 | self.request.save() 147 | 148 | def rollback(self): 149 | """Rollback to previous task""" 150 | previous = self.previous 151 | previous.status = 'Rolled Back' 152 | previous.save() 153 | self.status = 'Rolled Back' 154 | self.save() 155 | 156 | # Clone Task 157 | task = self.previous 158 | task.id = None 159 | task.status = 'Not Started' 160 | task.save() 161 | 162 | # Clone Activity 163 | activity = self.previous.activity 164 | activity.id = None 165 | activity.task = task 166 | activity.save() 167 | 168 | 169 | class AbstractActivity(AbstractEntity): 170 | """Common attributes for all activities""" 171 | task = OneToOneField(Task, null=True, on_delete=CASCADE) 172 | 173 | class Meta(object): 174 | abstract = True 175 | 176 | @property 177 | def is_initial(self): 178 | """Checks if the activity is initial activity""" 179 | config = flow_config(self.module_label) 180 | return True if self.title == config.FLOW[ 181 | config.INITIAL]['model']().title else False 182 | 183 | def next_activity(self): 184 | """Compute the next possible activities""" 185 | transitions = transition_config( 186 | self.module_label, self.task.activity_ref) 187 | 188 | return [transition for transition in 189 | transitions if transitions[transition]( 190 | self)] if transitions else None 191 | 192 | def validate_rule(self, identifier): 193 | """Validates the rule for the current 194 | transition""" 195 | transitions = transition_config( 196 | self.module_label, self.task.activity_ref) 197 | 198 | return transitions[identifier](self) 199 | 200 | def assign_task(self, identifier): 201 | """Link activity with task""" 202 | self.task = Task.objects.get(id=identifier) 203 | self.save() 204 | 205 | def update(self): 206 | """On activity save""" 207 | self.task.status = 'In Progress' 208 | self.task.save() 209 | 210 | def finish(self): 211 | """On activity finish""" 212 | self.task.status = 'Completed' 213 | self.task.save() 214 | self.task.request.status = 'Completed' 215 | self.task.request.save() 216 | 217 | 218 | class AbstractInitialActivity(AbstractActivity): 219 | """Common attributes for initial activity""" 220 | subject = CharField(verbose_name="Subject", max_length=70) 221 | 222 | class Meta(object): 223 | abstract = True 224 | 225 | def initiate_request(self, user, module): 226 | """Initiates new workflow requests""" 227 | config = flow_config(self.module_label) 228 | role = Group.objects.get( 229 | name=config.FLOW[config.INITIAL]['role']) 230 | 231 | request = Request.objects.create( 232 | requester=user, 233 | module_ref=module, 234 | status='Initiated') 235 | 236 | task = Task.objects.create( 237 | request=request, 238 | assignee=role, 239 | updated_by=user, 240 | activity_ref=config.INITIAL, 241 | status='In Progress') 242 | 243 | self.task = task 244 | self.save() 245 | 246 | 247 | def get_workflows_requests(module): 248 | """Returns all requests for specified workflow""" 249 | return Request.objects.filter(module_ref=module) 250 | 251 | 252 | def get_task(identifier): 253 | """Returns task instance""" 254 | return Task.objects.get(id=identifier) 255 | -------------------------------------------------------------------------------- /activflow/core/processors.py: -------------------------------------------------------------------------------- 1 | """Common template processors""" 2 | 3 | from activflow.core.helpers import ( 4 | get_request_params, 5 | flow_config 6 | ) 7 | 8 | 9 | def global_context(request): 10 | """Sets up global template context""" 11 | def get_value(key): 12 | """Returns value against specified key""" 13 | return get_request_params(key, request) 14 | 15 | app_title = get_value('app_name') 16 | activity_identifier = get_value('model_name') 17 | 18 | try: 19 | flow = flow_config(app_title).FLOW 20 | activity_title = flow[ 21 | [identifier for identifier in flow if flow[identifier][ 22 | 'model']().title == activity_identifier][0]]['name'] 23 | except (IndexError, LookupError, ImportError): 24 | activity_title = None 25 | 26 | return { 27 | 'entity_title': activity_identifier, 28 | 'app_title': app_title, 29 | 'identifier': get_value('pk'), 30 | 'activity_title': activity_title 31 | } 32 | -------------------------------------------------------------------------------- /activflow/core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/core/templatetags/__init__.py -------------------------------------------------------------------------------- /activflow/core/templatetags/core_tags.py: -------------------------------------------------------------------------------- 1 | """Template Tags""" 2 | 3 | import itertools 4 | from importlib import import_module 5 | from collections import OrderedDict 6 | 7 | from django.apps import apps 8 | from django import template 9 | 10 | from activflow.core.constants import REQUEST_IDENTIFIER 11 | from activflow.core.helpers import ( 12 | activity_config, 13 | wysiwyg_config 14 | ) 15 | 16 | from activflow.core.models import Task 17 | 18 | register = template.Library() 19 | 20 | 21 | @register.filter(is_safe=True) 22 | def label_with_class(value, arg): 23 | """Style adjustments""" 24 | return value.label_tag(attrs={'class': arg}) 25 | 26 | 27 | @register.simple_tag(takes_context=True) 28 | def activity_data(context, instance, option, _type): 29 | """Returns activity data as in field/value pair""" 30 | app = context['app_title'] 31 | model = type(instance) 32 | 33 | def filter(configuration): 34 | """Filter fields to display based on configuration""" 35 | for field_name in configuration: 36 | if option in configuration[field_name]: 37 | yield field_name 38 | 39 | def get_fields_from_config(model, instance, config): 40 | """Returns field/value pairs from configuration""" 41 | return OrderedDict([ 42 | ( 43 | model().class_meta.get_field(field_name).verbose_name, 44 | getattr(instance, field_name) 45 | ) 46 | for field_name in itertools.islice(filter(config), len(config)) 47 | ]) 48 | 49 | def get_all_fields(instance, exclude=None): 50 | """Returns all field/value pairs on the model""" 51 | exclude = exclude or [] 52 | fields = [field for field in ( 53 | ( 54 | field.name, 55 | field.verbose_name 56 | ) for field in instance.class_meta.get_fields() 57 | ) if field[0] not in ['id', 'task', 'task_id'] + exclude] 58 | return {field[1]: getattr( 59 | instance, field[0]) for field in fields} 60 | 61 | if _type == 'model': 62 | try: 63 | field_config = activity_config( 64 | app, model.__name__)['Fields'] 65 | return get_fields_from_config(model, instance, field_config) 66 | except KeyError: 67 | return get_all_fields(instance) 68 | else: 69 | related_model_fields = {} 70 | for relation in model().class_meta.related_objects: 71 | related_model = relation.related_model 72 | for field in related_model().class_meta.fields: 73 | if all([ 74 | field.get_internal_type() == 'ForeignKey', 75 | field.related_model == model] 76 | ): 77 | instances = related_model.objects.filter( 78 | **{field.name: instance}) 79 | try: 80 | field_config = activity_config( 81 | app, 82 | model.__name__ 83 | )['Relations'][related_model.__name__] 84 | relatd_items_detail = [get_fields_from_config( 85 | related_model, 86 | inst, 87 | field_config 88 | ) for inst in instances] 89 | except KeyError: 90 | relatd_items_detail = [] 91 | for inst in instances: 92 | relatd_items_detail.append( 93 | get_all_fields(inst, exclude=[field.name])) 94 | related_model_fields[related_model.__name__] = relatd_items_detail 95 | return related_model_fields 96 | 97 | 98 | @register.simple_tag(takes_context=True) 99 | def wysiwyg_form_fields(context): 100 | """Returns activity data as in field/value pair""" 101 | app = context['app_title'] 102 | model = context['entity_title'] 103 | 104 | try: 105 | return wysiwyg_config(app, model) 106 | except (KeyError, AttributeError): 107 | return 108 | 109 | 110 | @register.simple_tag 111 | def activity_title(ref, app): 112 | """Returns activity name""" 113 | return import_module( 114 | '{}.flow'.format(apps.get_app_config(app).name) 115 | ).FLOW[ref]['model']().title 116 | 117 | 118 | @register.simple_tag 119 | def activity_friendly_name(ref, app): 120 | """Returns activity friendly name""" 121 | return import_module( 122 | '{}.flow'.format(apps.get_app_config(app).name) 123 | ).FLOW[ref]['name'] 124 | 125 | 126 | @register.simple_tag 127 | def request_instance(task_identifier): 128 | """Returns request instance""" 129 | return Task.objects.get( 130 | id=task_identifier 131 | ).request if task_identifier != REQUEST_IDENTIFIER else None 132 | -------------------------------------------------------------------------------- /activflow/core/urls.py: -------------------------------------------------------------------------------- 1 | """URL configuration for core app""" 2 | 3 | from django.conf.urls import url 4 | from django.views.generic import TemplateView 5 | 6 | from activflow.core.constants import REQUEST_IDENTIFIER 7 | from activflow.core.views import ( 8 | workflows, 9 | WorkflowDetail, 10 | CreateActivity, 11 | ViewActivity, 12 | UpdateActivity, 13 | DeleteActivity, 14 | RollBackActivity, 15 | ) 16 | 17 | urlpatterns = [ 18 | url(r'^$', workflows, name='workflows'), 19 | url( 20 | r'^(?P\w+)$', 21 | WorkflowDetail.as_view(), 22 | name='workflow-detail' 23 | ), 24 | url( 25 | r'^(?P\w+)/(?P\w+)/Create/(?P\d+|{})$'. 26 | format(REQUEST_IDENTIFIER), 27 | CreateActivity.as_view(), 28 | name='create' 29 | ), 30 | url( 31 | r'^(?P\w+)/(?P\w+)/Update/(?P\d+|None)/$', 32 | UpdateActivity.as_view(), 33 | name='update' 34 | ), 35 | url( 36 | r'^(?P\w+)/(?P\w+)/View/(?P\d+|None)/$', 37 | ViewActivity.as_view(), 38 | name='view' 39 | ), 40 | url( 41 | r'^(?P\w+)/(?P\w+)/Delete/(?P\d+)$', 42 | DeleteActivity.as_view(), 43 | name='delete' 44 | ), 45 | url( 46 | r'^(?P\w+)/RollBack/(?P\d+)/$', 47 | RollBackActivity.as_view(), 48 | name='rollback' 49 | ), 50 | url( 51 | r'^Denied/$', 52 | TemplateView.as_view(template_name='core/denied.html'), 53 | name='denied' 54 | ) 55 | ] 56 | -------------------------------------------------------------------------------- /activflow/core/views.py: -------------------------------------------------------------------------------- 1 | """Generic workflow engine views""" 2 | 3 | from django.contrib.auth.decorators import login_required 4 | from django.contrib.auth.mixins import LoginRequiredMixin 5 | from django.urls import reverse, reverse_lazy 6 | from django.db import transaction 7 | from django.http import HttpResponseRedirect 8 | from django.shortcuts import render 9 | from django.views import generic 10 | 11 | from activflow.core.constants import WORKFLOW_APPS, REQUEST_IDENTIFIER 12 | from activflow.core.helpers import ( 13 | get_model, 14 | get_model_instance, 15 | get_form, 16 | get_formsets, 17 | get_request_params, 18 | flow_config, 19 | get_fk 20 | ) 21 | 22 | from activflow.core.mixins import AccessDeniedMixin 23 | from activflow.core.models import get_workflows_requests, get_task 24 | 25 | 26 | @login_required 27 | def workflows(request): 28 | """Lists down registered workflows""" 29 | return render(request, 'index.html', {'workflows': WORKFLOW_APPS}) 30 | 31 | 32 | class WorkflowDetail(LoginRequiredMixin, generic.TemplateView): 33 | """Generic view to list worflow requests & tasks""" 34 | template_name = 'core/workflow.html' 35 | 36 | def get_context_data(self, **kwargs): 37 | """Retrieve context data<""" 38 | context = super(WorkflowDetail, self).get_context_data(**kwargs) 39 | app_title = get_request_params('app_name', **kwargs) 40 | config = flow_config(app_title) 41 | model = config.FLOW[config.INITIAL]['model']().title 42 | context['requests'] = get_workflows_requests(app_title) 43 | context['request_identifier'] = REQUEST_IDENTIFIER 44 | context['workflow_title'] = config.TITLE 45 | context['description'] = config.DESCRIPTION 46 | context['initial'] = model 47 | 48 | return context 49 | 50 | 51 | class ViewActivity(AccessDeniedMixin, generic.DetailView): 52 | """Generic view to display activity details""" 53 | template_name = 'core/detail.html' 54 | 55 | def dispatch(self, request, *args, **kwargs): 56 | """Overriding dispatch on DetailView""" 57 | self.model = get_model(**kwargs) 58 | denied = self.check(request, **kwargs) 59 | return denied if denied else super(ViewActivity, self).dispatch( 60 | request, *args, **kwargs) 61 | 62 | 63 | class RollBackActivity(LoginRequiredMixin, generic.View): 64 | """Rollbacks workflow task""" 65 | @transaction.atomic 66 | def post(self, request, **kwargs): 67 | """POST request handler for rollback""" 68 | app_title = get_request_params('app_name', **kwargs) 69 | identifier = get_request_params('pk', **kwargs) 70 | get_task(identifier).rollback() 71 | 72 | return HttpResponseRedirect( 73 | reverse('workflow-detail', args=[app_title])) 74 | 75 | 76 | class DeleteActivity(LoginRequiredMixin, generic.DeleteView): 77 | """Deletes activity instance""" 78 | def dispatch(self, request, *args, **kwargs): 79 | """Overriding dispatch on DeleteView""" 80 | self.model = get_model(**kwargs) 81 | self.success_url = reverse_lazy( 82 | 'workflow-detail', args=[get_request_params( 83 | 'app_name', **kwargs)]) 84 | 85 | return super(DeleteActivity, self).dispatch( 86 | request, *args, **kwargs) 87 | 88 | 89 | class CreateActivity(AccessDeniedMixin, generic.View): 90 | """Generic view to initiate activity""" 91 | def get(self, request, **kwargs): 92 | """GET request handler for Create operation""" 93 | form = get_form(**kwargs) 94 | formsets = [formset( 95 | prefix=formset.form.__name__) for formset in get_formsets( 96 | self.__class__.__name__, extra=1, **kwargs)] 97 | context = {'form': form, 'formsets': formsets} 98 | 99 | denied = self.check(request, **kwargs) 100 | return denied if denied else render( 101 | request, 'core/create.html', context) 102 | 103 | @transaction.atomic 104 | def post(self, request, **kwargs): 105 | """POST request handler for Create operation""" 106 | operation = self.__class__.__name__ 107 | instance = None 108 | form = get_form(**kwargs)(request.POST) 109 | formsets = get_formsets(operation, **kwargs) 110 | app_title = get_request_params('app_name', **kwargs) 111 | 112 | (result, context) = FormHandler( 113 | operation, request, form, formsets).handle(**kwargs) 114 | 115 | if not result: 116 | return render(request, 'core/create.html', context) 117 | else: 118 | instance = context 119 | 120 | if instance.is_initial: 121 | instance.initiate_request(request.user, app_title) 122 | else: 123 | instance.assign_task( 124 | get_request_params('pk', **kwargs)) 125 | instance.task.initiate() 126 | 127 | return HttpResponseRedirect( 128 | reverse('update', args=( 129 | app_title, instance.title, instance.id))) 130 | 131 | 132 | class UpdateActivity(AccessDeniedMixin, generic.View): 133 | """Generic view to update activity""" 134 | def get(self, request, **kwargs): 135 | """GET request handler for Update operation""" 136 | instance = get_model_instance(**kwargs) 137 | form = get_form(**kwargs) 138 | formsets = get_formsets(self.__class__.__name__, extra=1, **kwargs) 139 | context = { 140 | 'form': form(instance=instance), 141 | 'formsets': [formset( 142 | instance=instance, 143 | prefix=formset.form.__name__ 144 | ) for formset in formsets], 145 | 'object': instance, 146 | 'next': instance.next_activity() 147 | } 148 | 149 | denied = self.check(request, **kwargs) 150 | return denied if denied else render( 151 | request, 'core/update.html', context) 152 | 153 | @transaction.atomic 154 | def post(self, request, **kwargs): 155 | """POST request handler for Update operation""" 156 | operation = self.__class__.__name__ 157 | redirect_to_update = False 158 | instance = get_model_instance(**kwargs) 159 | app_title = get_request_params('app_name', **kwargs) 160 | form = get_form(**kwargs)(request.POST, instance=instance) 161 | formsets = get_formsets(operation, **kwargs) 162 | 163 | (result, context) = FormHandler( 164 | operation, request, form, formsets, instance).handle(**kwargs) 165 | 166 | if not result: 167 | return render(request, 'core/update.html', context) 168 | 169 | if 'save' in request.POST: 170 | redirect_to_update = True 171 | instance.update() 172 | elif 'finish' in request.POST: 173 | instance.finish() 174 | else: 175 | next_activity = request.POST['submit'] 176 | if not instance.validate_rule(next_activity): 177 | redirect_to_update = True 178 | else: 179 | instance.task.submit( 180 | app_title, self.request.user, next_activity) 181 | 182 | return HttpResponseRedirect( 183 | reverse('update', args=( 184 | app_title, instance.title, instance.id)) 185 | ) if redirect_to_update else HttpResponseRedirect( 186 | reverse( 187 | 'workflow-detail', 188 | args=[app_title] 189 | ) 190 | ) 191 | 192 | 193 | # Handlers 194 | 195 | 196 | class FormHandler(object): 197 | """Form and Formsets Manager""" 198 | def __init__(self, operation, request, form, formsets, instance=None): 199 | """Initializes FormHandler""" 200 | self.instance = instance 201 | self.request = request 202 | self.operation = operation 203 | self.form = form 204 | self.formsets = formsets 205 | 206 | def add(self, formsets, instruction): 207 | """Includes an additional form in the formset(s)""" 208 | request = self.request.POST.copy() 209 | 210 | for formset in formsets: 211 | form_title = formset.form.__name__ 212 | if 'add-' + form_title.replace('Form', '') in instruction: 213 | total_forms = form_title + '-TOTAL_FORMS' 214 | request[total_forms] = int(request[total_forms]) + 1 215 | 216 | formsets = [formset( 217 | request, prefix=formset.form.__name__) for formset in formsets] 218 | 219 | context = { 220 | 'form': self.form, 221 | 'formsets': formsets, 222 | } 223 | 224 | if self.instance: 225 | context['object'] = self.instance 226 | 227 | return (False, context) 228 | 229 | def save(self, formsets, **kwargs): 230 | """Persists validated formset(s)""" 231 | # form 232 | instance = self.form.save() 233 | # formsets 234 | for formset in formsets: 235 | if not self.instance: # create operation 236 | objects = formset.save(commit=False) 237 | if objects: 238 | fk = get_fk(objects, **kwargs) 239 | for obj in objects: 240 | setattr(obj, fk, instance) 241 | obj.save() 242 | else: # update operation 243 | formset.save() 244 | return (True, instance) 245 | 246 | def report(self, formsets): 247 | """Report validation errors""" 248 | errors = '' 249 | 250 | for formset in formsets: 251 | for error in formset.errors: 252 | errors = errors + str(error) 253 | 254 | context = { 255 | 'form': self.form, 256 | 'formsets': [formset( 257 | self.request.POST, 258 | prefix=formset.form.__name__ 259 | ) for formset in self.formsets], 260 | 'error_message': errors + str(self.form.errors) 261 | } 262 | 263 | if self.instance: 264 | context['object'] = self.instance 265 | context['next'] = self.instance.next_activity() 266 | return (False, context) 267 | 268 | def handle(self, **kwargs): 269 | """Adds, validates and persist formsets""" 270 | instruction = next(iter(filter( 271 | lambda key: 'add-' in key, self.request.POST)), None) 272 | 273 | # Handle adding related instance 274 | 275 | if instruction: 276 | formsets = get_formsets(self.operation, **kwargs) 277 | return self.add(formsets, instruction) 278 | 279 | # Validate and save form/formsets 280 | 281 | formsets = [] 282 | 283 | for formset in self.formsets: 284 | formsets.append(formset( 285 | self.request.POST, 286 | instance=self.instance, # None for create operation 287 | prefix=formset.form.__name__ 288 | )) 289 | 290 | if self.form.is_valid() and all( 291 | formset.is_valid() for formset in formsets 292 | ): 293 | return self.save(formsets, **kwargs) 294 | else: 295 | return self.report(formsets) 296 | -------------------------------------------------------------------------------- /activflow/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/settings/__init__.py -------------------------------------------------------------------------------- /activflow/settings/base.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | 5 | SECRET_KEY = 'test_s*kbi04s%5u921e+d52kaa(e=d)%i4w@s6a6u8-x&bij^l8!q-' 6 | 7 | ALLOWED_HOSTS = ['activflow'] 8 | 9 | # Application definition 10 | 11 | INSTALLED_APPS = ( 12 | 'django.contrib.admin', 13 | 'django.contrib.auth', 14 | 'django.contrib.contenttypes', 15 | 'django.contrib.sessions', 16 | 'django.contrib.messages', 17 | 'django.contrib.staticfiles', 18 | 'django_extensions', 19 | 'activflow.core', 20 | 'activflow.tests' 21 | ) 22 | 23 | MIDDLEWARE = ( 24 | 'django.middleware.security.SecurityMiddleware', 25 | 'django.contrib.sessions.middleware.SessionMiddleware', 26 | 'django.middleware.common.CommonMiddleware', 27 | 'django.middleware.csrf.CsrfViewMiddleware', 28 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 29 | 'django.contrib.messages.middleware.MessageMiddleware', 30 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 31 | ) 32 | 33 | ROOT_URLCONF = 'activflow.urls' 34 | 35 | TEMPLATES = [ 36 | { 37 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 38 | 'DIRS': [os.path.join(BASE_DIR, '../activflow/templates')], 39 | 'APP_DIRS': True, 40 | 'OPTIONS': { 41 | 'context_processors': [ 42 | 'django.template.context_processors.debug', 43 | 'django.template.context_processors.request', 44 | 'django.contrib.auth.context_processors.auth', 45 | 'django.contrib.messages.context_processors.messages', 46 | 'activflow.core.processors.global_context' 47 | ], 48 | }, 49 | }, 50 | ] 51 | 52 | WSGI_APPLICATION = 'activflow.wsgi.application' 53 | 54 | # Internationalization 55 | 56 | LANGUAGE_CODE = 'en-us' 57 | 58 | TIME_ZONE = 'UTC' 59 | 60 | USE_I18N = True 61 | 62 | USE_L10N = True 63 | 64 | USE_TZ = True 65 | 66 | # Static files (CSS, JavaScript, Images) 67 | 68 | STATIC_URL = '/static/' 69 | 70 | STATICFILES_FINDERS = ( 71 | 'django.contrib.staticfiles.finders.FileSystemFinder', 72 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 73 | ) 74 | 75 | # Auth 76 | 77 | LOGIN_REDIRECT_URL = '/' 78 | LOGIN_URL = '/auth/login/' 79 | -------------------------------------------------------------------------------- /activflow/settings/development.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | 5 | # Database 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.sqlite3', 10 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 11 | } 12 | } 13 | 14 | # Static files (CSS, JavaScript, Images) 15 | 16 | STATICFILES_DIRS = ( 17 | os.path.join(BASE_DIR, 'static'), 18 | ) -------------------------------------------------------------------------------- /activflow/settings/production.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | -------------------------------------------------------------------------------- /activflow/settings/staging.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | 3 | DEBUG = True 4 | 5 | # Database 6 | 7 | DATABASES = { 8 | 'default': { 9 | 'ENGINE': 'django.db.backends.postgresql', 10 | 'NAME': 'postgres', 11 | 'USER': 'postgres', 12 | 'PASSWORD': 'password', 13 | 'HOST': 'db', 14 | 'PORT': 5432, 15 | } 16 | } 17 | 18 | # Static files (CSS, JavaScript, Images) 19 | 20 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 21 | -------------------------------------------------------------------------------- /activflow/static/admin/css/changelists.css: -------------------------------------------------------------------------------- 1 | /* CHANGELISTS */ 2 | 3 | #changelist { 4 | position: relative; 5 | width: 100%; 6 | } 7 | 8 | #changelist table { 9 | width: 100%; 10 | } 11 | 12 | .change-list .hiddenfields { display:none; } 13 | 14 | .change-list .filtered table { 15 | border-right: none; 16 | } 17 | 18 | .change-list .filtered { 19 | min-height: 400px; 20 | } 21 | 22 | .change-list .filtered .results, .change-list .filtered .paginator, 23 | .filtered #toolbar, .filtered div.xfull { 24 | margin-right: 280px; 25 | width: auto; 26 | } 27 | 28 | .change-list .filtered table tbody th { 29 | padding-right: 1em; 30 | } 31 | 32 | #changelist-form .results { 33 | overflow-x: auto; 34 | } 35 | 36 | #changelist .toplinks { 37 | border-bottom: 1px solid #ddd; 38 | } 39 | 40 | #changelist .paginator { 41 | color: #666; 42 | border-bottom: 1px solid #eee; 43 | background: #fff; 44 | overflow: hidden; 45 | } 46 | 47 | /* CHANGELIST TABLES */ 48 | 49 | #changelist table thead th { 50 | padding: 0; 51 | white-space: nowrap; 52 | vertical-align: middle; 53 | } 54 | 55 | #changelist table thead th.action-checkbox-column { 56 | width: 1.5em; 57 | text-align: center; 58 | } 59 | 60 | #changelist table tbody td.action-checkbox { 61 | text-align: center; 62 | } 63 | 64 | #changelist table tfoot { 65 | color: #666; 66 | } 67 | 68 | /* TOOLBAR */ 69 | 70 | #changelist #toolbar { 71 | padding: 8px 10px; 72 | margin-bottom: 15px; 73 | border-top: 1px solid #eee; 74 | border-bottom: 1px solid #eee; 75 | background: #f8f8f8; 76 | color: #666; 77 | } 78 | 79 | #changelist #toolbar form input { 80 | border-radius: 4px; 81 | font-size: 14px; 82 | padding: 5px; 83 | color: #333; 84 | } 85 | 86 | #changelist #toolbar form #searchbar { 87 | height: 19px; 88 | border: 1px solid #ccc; 89 | padding: 2px 5px; 90 | margin: 0; 91 | vertical-align: top; 92 | font-size: 13px; 93 | } 94 | 95 | #changelist #toolbar form #searchbar:focus { 96 | border-color: #999; 97 | } 98 | 99 | #changelist #toolbar form input[type="submit"] { 100 | border: 1px solid #ccc; 101 | padding: 2px 10px; 102 | margin: 0; 103 | vertical-align: middle; 104 | background: #fff; 105 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 106 | cursor: pointer; 107 | color: #333; 108 | } 109 | 110 | #changelist #toolbar form input[type="submit"]:focus, 111 | #changelist #toolbar form input[type="submit"]:hover { 112 | border-color: #999; 113 | } 114 | 115 | #changelist #changelist-search img { 116 | vertical-align: middle; 117 | margin-right: 4px; 118 | } 119 | 120 | /* FILTER COLUMN */ 121 | 122 | #changelist-filter { 123 | position: absolute; 124 | top: 0; 125 | right: 0; 126 | z-index: 1000; 127 | width: 240px; 128 | background: #f8f8f8; 129 | border-left: none; 130 | margin: 0; 131 | } 132 | 133 | #changelist-filter h2 { 134 | font-size: 14px; 135 | text-transform: uppercase; 136 | letter-spacing: 0.5px; 137 | padding: 5px 15px; 138 | margin-bottom: 12px; 139 | border-bottom: none; 140 | } 141 | 142 | #changelist-filter h3 { 143 | font-weight: 400; 144 | font-size: 14px; 145 | padding: 0 15px; 146 | margin-bottom: 10px; 147 | } 148 | 149 | #changelist-filter ul { 150 | margin: 5px 0; 151 | padding: 0 15px 15px; 152 | border-bottom: 1px solid #eaeaea; 153 | } 154 | 155 | #changelist-filter ul:last-child { 156 | border-bottom: none; 157 | padding-bottom: none; 158 | } 159 | 160 | #changelist-filter li { 161 | list-style-type: none; 162 | margin-left: 0; 163 | padding-left: 0; 164 | } 165 | 166 | #changelist-filter a { 167 | display: block; 168 | color: #999; 169 | } 170 | 171 | #changelist-filter li.selected { 172 | border-left: 5px solid #eaeaea; 173 | padding-left: 10px; 174 | margin-left: -15px; 175 | } 176 | 177 | #changelist-filter li.selected a { 178 | color: #5b80b2; 179 | } 180 | 181 | #changelist-filter a:focus, #changelist-filter a:hover, 182 | #changelist-filter li.selected a:focus, 183 | #changelist-filter li.selected a:hover { 184 | color: #036; 185 | } 186 | 187 | /* DATE DRILLDOWN */ 188 | 189 | .change-list ul.toplinks { 190 | display: block; 191 | float: left; 192 | padding: 0; 193 | margin: 0; 194 | width: 100%; 195 | } 196 | 197 | .change-list ul.toplinks li { 198 | padding: 3px 6px; 199 | font-weight: bold; 200 | list-style-type: none; 201 | display: inline-block; 202 | } 203 | 204 | .change-list ul.toplinks .date-back a { 205 | color: #999; 206 | } 207 | 208 | .change-list ul.toplinks .date-back a:focus, 209 | .change-list ul.toplinks .date-back a:hover { 210 | color: #036; 211 | } 212 | 213 | /* PAGINATOR */ 214 | 215 | .paginator { 216 | font-size: 13px; 217 | padding-top: 10px; 218 | padding-bottom: 10px; 219 | line-height: 22px; 220 | margin: 0; 221 | border-top: 1px solid #ddd; 222 | } 223 | 224 | .paginator a:link, .paginator a:visited { 225 | padding: 2px 6px; 226 | background: #79aec8; 227 | text-decoration: none; 228 | color: #fff; 229 | } 230 | 231 | .paginator a.showall { 232 | padding: 0; 233 | border: none; 234 | background: none; 235 | color: #5b80b2; 236 | } 237 | 238 | .paginator a.showall:focus, .paginator a.showall:hover { 239 | background: none; 240 | color: #036; 241 | } 242 | 243 | .paginator .end { 244 | margin-right: 6px; 245 | } 246 | 247 | .paginator .this-page { 248 | padding: 2px 6px; 249 | font-weight: bold; 250 | font-size: 13px; 251 | vertical-align: top; 252 | } 253 | 254 | .paginator a:focus, .paginator a:hover { 255 | color: white; 256 | background: #036; 257 | } 258 | 259 | /* ACTIONS */ 260 | 261 | .filtered .actions { 262 | margin-right: 280px; 263 | border-right: none; 264 | } 265 | 266 | #changelist table input { 267 | margin: 0; 268 | vertical-align: baseline; 269 | } 270 | 271 | #changelist table tbody tr.selected { 272 | background-color: #FFFFCC; 273 | } 274 | 275 | #changelist .actions { 276 | padding: 10px; 277 | background: #fff; 278 | border-top: none; 279 | border-bottom: none; 280 | line-height: 24px; 281 | color: #999; 282 | } 283 | 284 | #changelist .actions.selected { 285 | background: #fffccf; 286 | border-top: 1px solid #fffee8; 287 | border-bottom: 1px solid #edecd6; 288 | } 289 | 290 | #changelist .actions span.all, 291 | #changelist .actions span.action-counter, 292 | #changelist .actions span.clear, 293 | #changelist .actions span.question { 294 | font-size: 13px; 295 | margin: 0 0.5em; 296 | display: none; 297 | } 298 | 299 | #changelist .actions:last-child { 300 | border-bottom: none; 301 | } 302 | 303 | #changelist .actions select { 304 | vertical-align: top; 305 | height: 24px; 306 | background: none; 307 | border: 1px solid #ccc; 308 | border-radius: 4px; 309 | font-size: 14px; 310 | padding: 0 0 0 4px; 311 | margin: 0; 312 | margin-left: 10px; 313 | } 314 | 315 | #changelist .actions select:focus { 316 | border-color: #999; 317 | } 318 | 319 | #changelist .actions label { 320 | display: inline-block; 321 | vertical-align: middle; 322 | font-size: 13px; 323 | } 324 | 325 | #changelist .actions .button { 326 | font-size: 13px; 327 | border: 1px solid #ccc; 328 | border-radius: 4px; 329 | background: #fff; 330 | box-shadow: 0 -15px 20px -10px rgba(0, 0, 0, 0.15) inset; 331 | cursor: pointer; 332 | height: 24px; 333 | line-height: 1; 334 | padding: 4px 8px; 335 | margin: 0; 336 | color: #333; 337 | } 338 | 339 | #changelist .actions .button:focus, #changelist .actions .button:hover { 340 | border-color: #999; 341 | } 342 | -------------------------------------------------------------------------------- /activflow/static/admin/css/dashboard.css: -------------------------------------------------------------------------------- 1 | /* DASHBOARD */ 2 | 3 | .dashboard .module table th { 4 | width: 100%; 5 | } 6 | 7 | .dashboard .module table td { 8 | white-space: nowrap; 9 | } 10 | 11 | .dashboard .module table td a { 12 | display: block; 13 | padding-right: .6em; 14 | } 15 | 16 | /* RECENT ACTIONS MODULE */ 17 | 18 | .module ul.actionlist { 19 | margin-left: 0; 20 | } 21 | 22 | ul.actionlist li { 23 | list-style-type: none; 24 | } 25 | 26 | ul.actionlist li { 27 | overflow: hidden; 28 | text-overflow: ellipsis; 29 | -o-text-overflow: ellipsis; 30 | } 31 | -------------------------------------------------------------------------------- /activflow/static/admin/css/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | src: url('../fonts/Roboto-Bold-webfont.woff'); 4 | font-weight: 700; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'Roboto'; 10 | src: url('../fonts/Roboto-Regular-webfont.woff'); 11 | font-weight: 400; 12 | font-style: normal; 13 | } 14 | 15 | @font-face { 16 | font-family: 'Roboto'; 17 | src: url('../fonts/Roboto-Light-webfont.woff'); 18 | font-weight: 300; 19 | font-style: normal; 20 | } 21 | -------------------------------------------------------------------------------- /activflow/static/admin/css/forms.css: -------------------------------------------------------------------------------- 1 | @import url('widgets.css'); 2 | 3 | /* FORM ROWS */ 4 | 5 | .form-row { 6 | overflow: hidden; 7 | padding: 10px; 8 | font-size: 13px; 9 | border-bottom: 1px solid #eee; 10 | } 11 | 12 | .form-row img, .form-row input { 13 | vertical-align: middle; 14 | } 15 | 16 | .form-row label input[type="checkbox"] { 17 | margin-top: 0; 18 | vertical-align: 0; 19 | } 20 | 21 | form .form-row p { 22 | padding-left: 0; 23 | } 24 | 25 | .hidden { 26 | display: none; 27 | } 28 | 29 | /* FORM LABELS */ 30 | 31 | label { 32 | font-weight: normal; 33 | color: #666; 34 | font-size: 13px; 35 | } 36 | 37 | .required label, label.required { 38 | font-weight: bold; 39 | color: #333; 40 | } 41 | 42 | /* RADIO BUTTONS */ 43 | 44 | form ul.radiolist li { 45 | list-style-type: none; 46 | } 47 | 48 | form ul.radiolist label { 49 | float: none; 50 | display: inline; 51 | } 52 | 53 | form ul.radiolist input[type="radio"] { 54 | margin: -2px 4px 0 0; 55 | padding: 0; 56 | } 57 | 58 | form ul.inline { 59 | margin-left: 0; 60 | padding: 0; 61 | } 62 | 63 | form ul.inline li { 64 | float: left; 65 | padding-right: 7px; 66 | } 67 | 68 | /* ALIGNED FIELDSETS */ 69 | 70 | .aligned label { 71 | display: block; 72 | padding: 4px 10px 0 0; 73 | float: left; 74 | width: 160px; 75 | word-wrap: break-word; 76 | line-height: 1; 77 | } 78 | 79 | .aligned label:not(.vCheckboxLabel):after { 80 | content: ''; 81 | display: inline-block; 82 | vertical-align: middle; 83 | height: 26px; 84 | } 85 | 86 | .aligned label + p { 87 | padding: 6px 0; 88 | margin-top: 0; 89 | margin-bottom: 0; 90 | margin-left: 170px; 91 | } 92 | 93 | .aligned ul label { 94 | display: inline; 95 | float: none; 96 | width: auto; 97 | } 98 | 99 | .aligned .form-row input { 100 | margin-bottom: 0; 101 | } 102 | 103 | .colMS .aligned .vLargeTextField, .colMS .aligned .vXMLLargeTextField { 104 | width: 350px; 105 | } 106 | 107 | form .aligned ul { 108 | margin-left: 160px; 109 | padding-left: 10px; 110 | } 111 | 112 | form .aligned ul.radiolist { 113 | display: inline-block; 114 | margin: 0; 115 | padding: 0; 116 | } 117 | 118 | form .aligned p.help { 119 | clear: left; 120 | margin-top: 0; 121 | margin-left: 160px; 122 | padding-left: 10px; 123 | } 124 | 125 | form .aligned label + p.help { 126 | margin-left: 0; 127 | padding-left: 0; 128 | } 129 | 130 | form .aligned p.help:last-child { 131 | margin-bottom: 0; 132 | padding-bottom: 0; 133 | } 134 | 135 | form .aligned input + p.help, 136 | form .aligned textarea + p.help, 137 | form .aligned select + p.help { 138 | margin-left: 160px; 139 | padding-left: 10px; 140 | } 141 | 142 | form .aligned ul li { 143 | list-style: none; 144 | } 145 | 146 | form .aligned table p { 147 | margin-left: 0; 148 | padding-left: 0; 149 | } 150 | 151 | .aligned .vCheckboxLabel { 152 | float: none; 153 | width: auto; 154 | display: inline-block; 155 | vertical-align: -3px; 156 | padding: 0 0 5px 5px; 157 | } 158 | 159 | .aligned .vCheckboxLabel + p.help { 160 | margin-top: -4px; 161 | } 162 | 163 | .colM .aligned .vLargeTextField, .colM .aligned .vXMLLargeTextField { 164 | width: 610px; 165 | } 166 | 167 | .checkbox-row p.help { 168 | margin-left: 0; 169 | padding-left: 0; 170 | } 171 | 172 | fieldset .field-box { 173 | float: left; 174 | margin-right: 20px; 175 | } 176 | 177 | /* WIDE FIELDSETS */ 178 | 179 | .wide label { 180 | width: 200px; 181 | } 182 | 183 | form .wide p, form .wide input + p.help { 184 | margin-left: 200px; 185 | } 186 | 187 | form .wide p.help { 188 | padding-left: 38px; 189 | } 190 | 191 | .colM fieldset.wide .vLargeTextField, .colM fieldset.wide .vXMLLargeTextField { 192 | width: 450px; 193 | } 194 | 195 | /* COLLAPSED FIELDSETS */ 196 | 197 | fieldset.collapsed * { 198 | display: none; 199 | } 200 | 201 | fieldset.collapsed h2, fieldset.collapsed { 202 | display: block; 203 | } 204 | 205 | fieldset.collapsed { 206 | border: 1px solid #eee; 207 | border-radius: 4px; 208 | overflow: hidden; 209 | } 210 | 211 | fieldset.collapsed h2 { 212 | background: #f8f8f8; 213 | color: #666; 214 | } 215 | 216 | fieldset .collapse-toggle { 217 | color: #fff; 218 | } 219 | 220 | fieldset.collapsed .collapse-toggle { 221 | background: transparent; 222 | display: inline; 223 | color: #447e9b; 224 | } 225 | 226 | /* MONOSPACE TEXTAREAS */ 227 | 228 | fieldset.monospace textarea { 229 | font-family: "Bitstream Vera Sans Mono", Monaco, "Courier New", Courier, monospace; 230 | } 231 | 232 | /* SUBMIT ROW */ 233 | 234 | .submit-row { 235 | padding: 12px 14px; 236 | margin: 0 0 20px; 237 | background: #f8f8f8; 238 | border: 1px solid #eee; 239 | border-radius: 4px; 240 | text-align: right; 241 | overflow: hidden; 242 | } 243 | 244 | body.popup .submit-row { 245 | overflow: auto; 246 | } 247 | 248 | .submit-row input { 249 | height: 35px; 250 | line-height: 15px; 251 | margin: 0 0 0 5px; 252 | } 253 | 254 | .submit-row input.default { 255 | margin: 0 0 0 8px; 256 | text-transform: uppercase; 257 | } 258 | 259 | .submit-row p { 260 | margin: 0.3em; 261 | } 262 | 263 | .submit-row p.deletelink-box { 264 | float: left; 265 | margin: 0; 266 | } 267 | 268 | .submit-row a.deletelink { 269 | display: block; 270 | background: #ba2121; 271 | border-radius: 4px; 272 | padding: 10px 15px; 273 | height: 15px; 274 | line-height: 15px; 275 | color: #fff; 276 | } 277 | 278 | .submit-row a.deletelink:focus, 279 | .submit-row a.deletelink:hover, 280 | .submit-row a.deletelink:active { 281 | background: #a41515; 282 | } 283 | 284 | /* CUSTOM FORM FIELDS */ 285 | 286 | .vSelectMultipleField { 287 | vertical-align: top; 288 | } 289 | 290 | .vCheckboxField { 291 | border: none; 292 | } 293 | 294 | .vDateField, .vTimeField { 295 | margin-right: 2px; 296 | margin-bottom: 4px; 297 | } 298 | 299 | .vDateField { 300 | min-width: 6.85em; 301 | } 302 | 303 | .vTimeField { 304 | min-width: 4.7em; 305 | } 306 | 307 | .vURLField { 308 | width: 30em; 309 | } 310 | 311 | .vLargeTextField, .vXMLLargeTextField { 312 | width: 48em; 313 | } 314 | 315 | .flatpages-flatpage #id_content { 316 | height: 40.2em; 317 | } 318 | 319 | .module table .vPositiveSmallIntegerField { 320 | width: 2.2em; 321 | } 322 | 323 | .vTextField { 324 | width: 20em; 325 | } 326 | 327 | .vIntegerField { 328 | width: 5em; 329 | } 330 | 331 | .vBigIntegerField { 332 | width: 10em; 333 | } 334 | 335 | .vForeignKeyRawIdAdminField { 336 | width: 5em; 337 | } 338 | 339 | /* INLINES */ 340 | 341 | .inline-group { 342 | padding: 0; 343 | margin: 0 0 30px; 344 | } 345 | 346 | .inline-group thead th { 347 | padding: 8px 10px; 348 | } 349 | 350 | .inline-group .aligned label { 351 | width: 160px; 352 | } 353 | 354 | .inline-related { 355 | position: relative; 356 | } 357 | 358 | .inline-related h3 { 359 | margin: 0; 360 | color: #666; 361 | padding: 5px; 362 | font-size: 13px; 363 | background: #f8f8f8; 364 | border-top: 1px solid #eee; 365 | border-bottom: 1px solid #eee; 366 | } 367 | 368 | .inline-related h3 span.delete { 369 | float: right; 370 | } 371 | 372 | .inline-related h3 span.delete label { 373 | margin-left: 2px; 374 | font-size: 11px; 375 | } 376 | 377 | .inline-related fieldset { 378 | margin: 0; 379 | background: #fff; 380 | border: none; 381 | width: 100%; 382 | } 383 | 384 | .inline-related fieldset.module h3 { 385 | margin: 0; 386 | padding: 2px 5px 3px 5px; 387 | font-size: 11px; 388 | text-align: left; 389 | font-weight: bold; 390 | background: #bcd; 391 | color: #fff; 392 | } 393 | 394 | .inline-group .tabular fieldset.module { 395 | border: none; 396 | } 397 | 398 | .inline-related.tabular fieldset.module table { 399 | width: 100%; 400 | } 401 | 402 | .last-related fieldset { 403 | border: none; 404 | } 405 | 406 | .inline-group .tabular tr.has_original td { 407 | padding-top: 2em; 408 | } 409 | 410 | .inline-group .tabular tr td.original { 411 | padding: 2px 0 0 0; 412 | width: 0; 413 | _position: relative; 414 | } 415 | 416 | .inline-group .tabular th.original { 417 | width: 0px; 418 | padding: 0; 419 | } 420 | 421 | .inline-group .tabular td.original p { 422 | position: absolute; 423 | left: 0; 424 | height: 1.1em; 425 | padding: 2px 9px; 426 | overflow: hidden; 427 | font-size: 9px; 428 | font-weight: bold; 429 | color: #666; 430 | _width: 700px; 431 | } 432 | 433 | .inline-group ul.tools { 434 | padding: 0; 435 | margin: 0; 436 | list-style: none; 437 | } 438 | 439 | .inline-group ul.tools li { 440 | display: inline; 441 | padding: 0 5px; 442 | } 443 | 444 | .inline-group div.add-row, 445 | .inline-group .tabular tr.add-row td { 446 | color: #666; 447 | background: #f8f8f8; 448 | padding: 8px 10px; 449 | border-bottom: 1px solid #eee; 450 | } 451 | 452 | .inline-group .tabular tr.add-row td { 453 | padding: 8px 10px; 454 | border-bottom: 1px solid #eee; 455 | } 456 | 457 | .inline-group ul.tools a.add, 458 | .inline-group div.add-row a, 459 | .inline-group .tabular tr.add-row td a { 460 | background: url(../img/icon-addlink.svg) 0 1px no-repeat; 461 | padding-left: 16px; 462 | font-size: 12px; 463 | } 464 | 465 | .empty-form { 466 | display: none; 467 | } 468 | 469 | /* RELATED FIELD ADD ONE / LOOKUP */ 470 | 471 | .add-another, .related-lookup { 472 | margin-left: 5px; 473 | display: inline-block; 474 | vertical-align: middle; 475 | background-repeat: no-repeat; 476 | background-size: 14px; 477 | } 478 | 479 | .add-another { 480 | width: 16px; 481 | height: 16px; 482 | background-image: url(../img/icon-addlink.svg); 483 | } 484 | 485 | .related-lookup { 486 | width: 16px; 487 | height: 16px; 488 | background-image: url(../img/search.svg); 489 | } 490 | 491 | form .related-widget-wrapper ul { 492 | display: inline-block; 493 | margin-left: 0; 494 | padding-left: 0; 495 | } 496 | 497 | .clearable-file-input input { 498 | margin-top: 0; 499 | } 500 | -------------------------------------------------------------------------------- /activflow/static/admin/css/login.css: -------------------------------------------------------------------------------- 1 | /* LOGIN FORM */ 2 | 3 | body.login { 4 | background: #f8f8f8; 5 | } 6 | 7 | .login #header { 8 | height: auto; 9 | padding: 5px 16px; 10 | } 11 | 12 | .login #header h1 { 13 | font-size: 18px; 14 | } 15 | 16 | .login #header h1 a { 17 | color: #fff; 18 | } 19 | 20 | .login #content { 21 | padding: 20px 20px 0; 22 | } 23 | 24 | .login #container { 25 | background: #fff; 26 | border: 1px solid #eaeaea; 27 | border-radius: 4px; 28 | overflow: hidden; 29 | width: 28em; 30 | min-width: 300px; 31 | margin: 100px auto; 32 | } 33 | 34 | .login #content-main { 35 | width: 100%; 36 | } 37 | 38 | .login .form-row { 39 | padding: 4px 0; 40 | float: left; 41 | width: 100%; 42 | border-bottom: none; 43 | } 44 | 45 | .login .form-row label { 46 | padding-right: 0.5em; 47 | line-height: 2em; 48 | font-size: 1em; 49 | clear: both; 50 | color: #333; 51 | } 52 | 53 | .login .form-row #id_username, .login .form-row #id_password { 54 | clear: both; 55 | padding: 8px; 56 | width: 100%; 57 | -webkit-box-sizing: border-box; 58 | -moz-box-sizing: border-box; 59 | box-sizing: border-box; 60 | } 61 | 62 | .login span.help { 63 | font-size: 10px; 64 | display: block; 65 | } 66 | 67 | .login .submit-row { 68 | clear: both; 69 | padding: 1em 0 0 9.4em; 70 | margin: 0; 71 | border: none; 72 | background: none; 73 | text-align: left; 74 | } 75 | 76 | .login .password-reset-link { 77 | text-align: center; 78 | } 79 | -------------------------------------------------------------------------------- /activflow/static/admin/css/rtl.css: -------------------------------------------------------------------------------- 1 | body { 2 | direction: rtl; 3 | } 4 | 5 | /* LOGIN */ 6 | 7 | .login .form-row { 8 | float: right; 9 | } 10 | 11 | .login .form-row label { 12 | float: right; 13 | padding-left: 0.5em; 14 | padding-right: 0; 15 | text-align: left; 16 | } 17 | 18 | .login .submit-row { 19 | clear: both; 20 | padding: 1em 9.4em 0 0; 21 | } 22 | 23 | /* GLOBAL */ 24 | 25 | th { 26 | text-align: right; 27 | } 28 | 29 | .module h2, .module caption { 30 | text-align: right; 31 | } 32 | 33 | .module ul, .module ol { 34 | margin-left: 0; 35 | margin-right: 1.5em; 36 | } 37 | 38 | .addlink, .changelink { 39 | padding-left: 0; 40 | padding-right: 16px; 41 | background-position: 100% 1px; 42 | } 43 | 44 | .deletelink { 45 | padding-left: 0; 46 | padding-right: 16px; 47 | background-position: 100% 1px; 48 | } 49 | 50 | .object-tools { 51 | float: left; 52 | } 53 | 54 | thead th:first-child, 55 | tfoot td:first-child { 56 | border-left: none; 57 | } 58 | 59 | /* LAYOUT */ 60 | 61 | #user-tools { 62 | right: auto; 63 | left: 0; 64 | text-align: left; 65 | } 66 | 67 | div.breadcrumbs { 68 | text-align: right; 69 | } 70 | 71 | #content-main { 72 | float: right; 73 | } 74 | 75 | #content-related { 76 | float: left; 77 | margin-left: -300px; 78 | margin-right: auto; 79 | } 80 | 81 | .colMS { 82 | margin-left: 300px; 83 | margin-right: 0; 84 | } 85 | 86 | /* SORTABLE TABLES */ 87 | 88 | table thead th.sorted .sortoptions { 89 | float: left; 90 | } 91 | 92 | thead th.sorted .text { 93 | padding-right: 0; 94 | padding-left: 42px; 95 | } 96 | 97 | /* dashboard styles */ 98 | 99 | .dashboard .module table td a { 100 | padding-left: .6em; 101 | padding-right: 16px; 102 | } 103 | 104 | /* changelists styles */ 105 | 106 | .change-list .filtered table { 107 | border-left: none; 108 | border-right: 0px none; 109 | } 110 | 111 | #changelist-filter { 112 | right: auto; 113 | left: 0; 114 | border-left: none; 115 | border-right: none; 116 | } 117 | 118 | .change-list .filtered .results, .change-list .filtered .paginator, .filtered #toolbar, .filtered div.xfull { 119 | margin-right: 0; 120 | margin-left: 280px; 121 | } 122 | 123 | #changelist-filter li.selected { 124 | border-left: none; 125 | padding-left: 10px; 126 | margin-left: 0; 127 | border-right: 5px solid #eaeaea; 128 | padding-right: 10px; 129 | margin-right: -15px; 130 | } 131 | 132 | .filtered .actions { 133 | margin-left: 280px; 134 | margin-right: 0; 135 | } 136 | 137 | #changelist table tbody td:first-child, #changelist table tbody th:first-child { 138 | border-right: none; 139 | border-left: none; 140 | } 141 | 142 | /* FORMS */ 143 | 144 | .aligned label { 145 | padding: 0 0 3px 1em; 146 | float: right; 147 | } 148 | 149 | .submit-row { 150 | text-align: left 151 | } 152 | 153 | .submit-row p.deletelink-box { 154 | float: right; 155 | } 156 | 157 | .submit-row input.default { 158 | margin-left: 0; 159 | } 160 | 161 | .vDateField, .vTimeField { 162 | margin-left: 2px; 163 | } 164 | 165 | .aligned .form-row input { 166 | margin-left: 5px; 167 | } 168 | 169 | form ul.inline li { 170 | float: right; 171 | padding-right: 0; 172 | padding-left: 7px; 173 | } 174 | 175 | input[type=submit].default, .submit-row input.default { 176 | float: left; 177 | } 178 | 179 | fieldset .field-box { 180 | float: right; 181 | margin-left: 20px; 182 | margin-right: 0; 183 | } 184 | 185 | .errorlist li { 186 | background-position: 100% 12px; 187 | padding: 0; 188 | } 189 | 190 | .errornote { 191 | background-position: 100% 12px; 192 | padding: 10px 12px; 193 | } 194 | 195 | /* WIDGETS */ 196 | 197 | .calendarnav-previous { 198 | top: 0; 199 | left: auto; 200 | right: 10px; 201 | } 202 | 203 | .calendarnav-next { 204 | top: 0; 205 | right: auto; 206 | left: 10px; 207 | } 208 | 209 | .calendar caption, .calendarbox h2 { 210 | text-align: center; 211 | } 212 | 213 | .selector { 214 | float: right; 215 | } 216 | 217 | .selector .selector-filter { 218 | text-align: right; 219 | } 220 | 221 | .inline-deletelink { 222 | float: left; 223 | } 224 | 225 | form .form-row p.datetime { 226 | overflow: hidden; 227 | } 228 | 229 | /* MISC */ 230 | 231 | .inline-related h2, .inline-group h2 { 232 | text-align: right 233 | } 234 | 235 | .inline-related h3 span.delete { 236 | padding-right: 20px; 237 | padding-left: inherit; 238 | left: 10px; 239 | right: inherit; 240 | float:left; 241 | } 242 | 243 | .inline-related h3 span.delete label { 244 | margin-left: inherit; 245 | margin-right: 2px; 246 | } 247 | 248 | /* IE7 specific bug fixes */ 249 | 250 | div.colM { 251 | position: relative; 252 | } 253 | 254 | .submit-row input { 255 | float: left; 256 | } 257 | -------------------------------------------------------------------------------- /activflow/static/admin/fonts/README.txt: -------------------------------------------------------------------------------- 1 | Roboto webfont source: https://www.google.com/fonts/specimen/Roboto 2 | Weights used in this project: Light (300), Regular (400), Bold (700) 3 | -------------------------------------------------------------------------------- /activflow/static/admin/fonts/Roboto-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/static/admin/fonts/Roboto-Bold-webfont.woff -------------------------------------------------------------------------------- /activflow/static/admin/fonts/Roboto-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/static/admin/fonts/Roboto-Light-webfont.woff -------------------------------------------------------------------------------- /activflow/static/admin/fonts/Roboto-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/static/admin/fonts/Roboto-Regular-webfont.woff -------------------------------------------------------------------------------- /activflow/static/admin/img/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Code Charm Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /activflow/static/admin/img/README.txt: -------------------------------------------------------------------------------- 1 | All icons are taken from Font Awesome (http://fontawesome.io/) project. 2 | The Font Awesome font is licensed under the SIL OFL 1.1: 3 | - http://scripts.sil.org/OFL 4 | 5 | SVG icons source: https://github.com/encharm/Font-Awesome-SVG-PNG 6 | Font-Awesome-SVG-PNG is licensed under the MIT license (see file license 7 | in current folder). 8 | -------------------------------------------------------------------------------- /activflow/static/admin/img/calendar-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /activflow/static/admin/img/gis/move_vertex_off.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /activflow/static/admin/img/gis/move_vertex_on.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-addlink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-alert.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-calendar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-changelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-clock.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-deletelink.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-no.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-unknown-alt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-unknown.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/icon-yes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/inline-delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/search.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/selector-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /activflow/static/admin/img/sorting-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /activflow/static/admin/img/tooltag-add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/img/tooltag-arrowright.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /activflow/static/admin/js/SelectBox.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var SelectBox = { 4 | cache: {}, 5 | init: function(id) { 6 | var box = document.getElementById(id); 7 | var node; 8 | SelectBox.cache[id] = []; 9 | var cache = SelectBox.cache[id]; 10 | for (var i = 0, j = box.options.length; i < j; i++) { 11 | node = box.options[i]; 12 | cache.push({value: node.value, text: node.text, displayed: 1}); 13 | } 14 | }, 15 | redisplay: function(id) { 16 | // Repopulate HTML select box from cache 17 | var box = document.getElementById(id); 18 | var node; 19 | box.options.length = 0; // clear all options 20 | var cache = SelectBox.cache[id]; 21 | for (var i = 0, j = cache.length; i < j; i++) { 22 | node = cache[i]; 23 | if (node.displayed) { 24 | var new_option = new Option(node.text, node.value, false, false); 25 | // Shows a tooltip when hovering over the option 26 | new_option.setAttribute("title", node.text); 27 | box.options[box.options.length] = new_option; 28 | } 29 | } 30 | }, 31 | filter: function(id, text) { 32 | // Redisplay the HTML select box, displaying only the choices containing ALL 33 | // the words in text. (It's an AND search.) 34 | var tokens = text.toLowerCase().split(/\s+/); 35 | var node, token; 36 | var cache = SelectBox.cache[id]; 37 | for (var i = 0, j = cache.length; i < j; i++) { 38 | node = cache[i]; 39 | node.displayed = 1; 40 | var numTokens = tokens.length; 41 | for (var k = 0; k < numTokens; k++) { 42 | token = tokens[k]; 43 | if (node.text.toLowerCase().indexOf(token) === -1) { 44 | node.displayed = 0; 45 | } 46 | } 47 | } 48 | SelectBox.redisplay(id); 49 | }, 50 | delete_from_cache: function(id, value) { 51 | var node, delete_index = null; 52 | var cache = SelectBox.cache[id]; 53 | for (var i = 0, j = cache.length; i < j; i++) { 54 | node = cache[i]; 55 | if (node.value === value) { 56 | delete_index = i; 57 | break; 58 | } 59 | } 60 | var k = cache.length - 1; 61 | for (i = delete_index; i < k; i++) { 62 | cache[i] = cache[i + 1]; 63 | } 64 | cache.length--; 65 | }, 66 | add_to_cache: function(id, option) { 67 | SelectBox.cache[id].push({value: option.value, text: option.text, displayed: 1}); 68 | }, 69 | cache_contains: function(id, value) { 70 | // Check if an item is contained in the cache 71 | var node; 72 | var cache = SelectBox.cache[id]; 73 | for (var i = 0, j = cache.length; i < j; i++) { 74 | node = cache[i]; 75 | if (node.value === value) { 76 | return true; 77 | } 78 | } 79 | return false; 80 | }, 81 | move: function(from, to) { 82 | var from_box = document.getElementById(from); 83 | var option; 84 | var boxOptions = from_box.options; 85 | for (var i = 0, j = boxOptions.length; i < j; i++) { 86 | option = boxOptions[i]; 87 | if (option.selected && SelectBox.cache_contains(from, option.value)) { 88 | SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); 89 | SelectBox.delete_from_cache(from, option.value); 90 | } 91 | } 92 | SelectBox.redisplay(from); 93 | SelectBox.redisplay(to); 94 | }, 95 | move_all: function(from, to) { 96 | var from_box = document.getElementById(from); 97 | var option; 98 | var boxOptions = from_box.options; 99 | for (var i = 0, j = boxOptions.length; i < j; i++) { 100 | option = boxOptions[i]; 101 | if (SelectBox.cache_contains(from, option.value)) { 102 | SelectBox.add_to_cache(to, {value: option.value, text: option.text, displayed: 1}); 103 | SelectBox.delete_from_cache(from, option.value); 104 | } 105 | } 106 | SelectBox.redisplay(from); 107 | SelectBox.redisplay(to); 108 | }, 109 | sort: function(id) { 110 | SelectBox.cache[id].sort(function(a, b) { 111 | a = a.text.toLowerCase(); 112 | b = b.text.toLowerCase(); 113 | try { 114 | if (a > b) { 115 | return 1; 116 | } 117 | if (a < b) { 118 | return -1; 119 | } 120 | } 121 | catch (e) { 122 | // silently fail on IE 'unknown' exception 123 | } 124 | return 0; 125 | } ); 126 | }, 127 | select_all: function(id) { 128 | var box = document.getElementById(id); 129 | for (var i = 0; i < box.options.length; i++) { 130 | box.options[i].selected = 'selected'; 131 | } 132 | } 133 | }; 134 | window.SelectBox = SelectBox; 135 | })(); 136 | -------------------------------------------------------------------------------- /activflow/static/admin/js/SelectFilter2.js: -------------------------------------------------------------------------------- 1 | /*global SelectBox, addEvent, gettext, interpolate, quickElement, SelectFilter*/ 2 | /* 3 | SelectFilter2 - Turns a multiple-select box into a filter interface. 4 | 5 | Requires core.js, SelectBox.js and addevent.js. 6 | */ 7 | (function($) { 8 | 'use strict'; 9 | function findForm(node) { 10 | // returns the node of the form containing the given node 11 | if (node.tagName.toLowerCase() !== 'form') { 12 | return findForm(node.parentNode); 13 | } 14 | return node; 15 | } 16 | 17 | window.SelectFilter = { 18 | init: function(field_id, field_name, is_stacked) { 19 | if (field_id.match(/__prefix__/)) { 20 | // Don't initialize on empty forms. 21 | return; 22 | } 23 | var from_box = document.getElementById(field_id); 24 | from_box.id += '_from'; // change its ID 25 | from_box.className = 'filtered'; 26 | 27 | var ps = from_box.parentNode.getElementsByTagName('p'); 28 | for (var i = 0; i < ps.length; i++) { 29 | if (ps[i].className.indexOf("info") !== -1) { 30 | // Remove

, because it just gets in the way. 31 | from_box.parentNode.removeChild(ps[i]); 32 | } else if (ps[i].className.indexOf("help") !== -1) { 33 | // Move help text up to the top so it isn't below the select 34 | // boxes or wrapped off on the side to the right of the add 35 | // button: 36 | from_box.parentNode.insertBefore(ps[i], from_box.parentNode.firstChild); 37 | } 38 | } 39 | 40 | //

or
41 | var selector_div = quickElement('div', from_box.parentNode); 42 | selector_div.className = is_stacked ? 'selector stacked' : 'selector'; 43 | 44 | //
45 | var selector_available = quickElement('div', selector_div); 46 | selector_available.className = 'selector-available'; 47 | var title_available = quickElement('h2', selector_available, interpolate(gettext('Available %s') + ' ', [field_name])); 48 | quickElement( 49 | 'span', title_available, '', 50 | 'class', 'help help-tooltip help-icon', 51 | 'title', interpolate( 52 | gettext( 53 | 'This is the list of available %s. You may choose some by ' + 54 | 'selecting them in the box below and then clicking the ' + 55 | '"Choose" arrow between the two boxes.' 56 | ), 57 | [field_name] 58 | ) 59 | ); 60 | 61 | var filter_p = quickElement('p', selector_available, '', 'id', field_id + '_filter'); 62 | filter_p.className = 'selector-filter'; 63 | 64 | var search_filter_label = quickElement('label', filter_p, '', 'for', field_id + '_input'); 65 | 66 | quickElement( 67 | 'span', search_filter_label, '', 68 | 'class', 'help-tooltip search-label-icon', 69 | 'title', interpolate(gettext("Type into this box to filter down the list of available %s."), [field_name]) 70 | ); 71 | 72 | filter_p.appendChild(document.createTextNode(' ')); 73 | 74 | var filter_input = quickElement('input', filter_p, '', 'type', 'text', 'placeholder', gettext("Filter")); 75 | filter_input.id = field_id + '_input'; 76 | 77 | selector_available.appendChild(from_box); 78 | var choose_all = quickElement('a', selector_available, gettext('Choose all'), 'title', interpolate(gettext('Click to choose all %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_add_all_link'); 79 | choose_all.className = 'selector-chooseall'; 80 | 81 | //
    82 | var selector_chooser = quickElement('ul', selector_div); 83 | selector_chooser.className = 'selector-chooser'; 84 | var add_link = quickElement('a', quickElement('li', selector_chooser), gettext('Choose'), 'title', gettext('Choose'), 'href', 'javascript:void(0);', 'id', field_id + '_add_link'); 85 | add_link.className = 'selector-add'; 86 | var remove_link = quickElement('a', quickElement('li', selector_chooser), gettext('Remove'), 'title', gettext('Remove'), 'href', 'javascript:void(0);', 'id', field_id + '_remove_link'); 87 | remove_link.className = 'selector-remove'; 88 | 89 | //
    90 | var selector_chosen = quickElement('div', selector_div); 91 | selector_chosen.className = 'selector-chosen'; 92 | var title_chosen = quickElement('h2', selector_chosen, interpolate(gettext('Chosen %s') + ' ', [field_name])); 93 | quickElement( 94 | 'span', title_chosen, '', 95 | 'class', 'help help-tooltip help-icon', 96 | 'title', interpolate( 97 | gettext( 98 | 'This is the list of chosen %s. You may remove some by ' + 99 | 'selecting them in the box below and then clicking the ' + 100 | '"Remove" arrow between the two boxes.' 101 | ), 102 | [field_name] 103 | ) 104 | ); 105 | 106 | var to_box = quickElement('select', selector_chosen, '', 'id', field_id + '_to', 'multiple', 'multiple', 'size', from_box.size, 'name', from_box.getAttribute('name')); 107 | to_box.className = 'filtered'; 108 | var clear_all = quickElement('a', selector_chosen, gettext('Remove all'), 'title', interpolate(gettext('Click to remove all chosen %s at once.'), [field_name]), 'href', 'javascript:void(0);', 'id', field_id + '_remove_all_link'); 109 | clear_all.className = 'selector-clearall'; 110 | 111 | from_box.setAttribute('name', from_box.getAttribute('name') + '_old'); 112 | 113 | // Set up the JavaScript event handlers for the select box filter interface 114 | addEvent(choose_all, 'click', function() { SelectBox.move_all(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); }); 115 | addEvent(add_link, 'click', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); }); 116 | addEvent(remove_link, 'click', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); }); 117 | addEvent(clear_all, 'click', function() { SelectBox.move_all(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); }); 118 | addEvent(filter_input, 'keypress', function(e) { SelectFilter.filter_key_press(e, field_id); }); 119 | addEvent(filter_input, 'keyup', function(e) { SelectFilter.filter_key_up(e, field_id); }); 120 | addEvent(filter_input, 'keydown', function(e) { SelectFilter.filter_key_down(e, field_id); }); 121 | addEvent(from_box, 'change', function(e) { SelectFilter.refresh_icons(field_id); }); 122 | addEvent(to_box, 'change', function(e) { SelectFilter.refresh_icons(field_id); }); 123 | addEvent(from_box, 'dblclick', function() { SelectBox.move(field_id + '_from', field_id + '_to'); SelectFilter.refresh_icons(field_id); }); 124 | addEvent(to_box, 'dblclick', function() { SelectBox.move(field_id + '_to', field_id + '_from'); SelectFilter.refresh_icons(field_id); }); 125 | addEvent(findForm(from_box), 'submit', function() { SelectBox.select_all(field_id + '_to'); }); 126 | SelectBox.init(field_id + '_from'); 127 | SelectBox.init(field_id + '_to'); 128 | // Move selected from_box options to to_box 129 | SelectBox.move(field_id + '_from', field_id + '_to'); 130 | 131 | if (!is_stacked) { 132 | // In horizontal mode, give the same height to the two boxes. 133 | var j_from_box = $(from_box); 134 | var j_to_box = $(to_box); 135 | var resize_filters = function() { j_to_box.height($(filter_p).outerHeight() + j_from_box.outerHeight()); }; 136 | if (j_from_box.outerHeight() > 0) { 137 | resize_filters(); // This fieldset is already open. Resize now. 138 | } else { 139 | // This fieldset is probably collapsed. Wait for its 'show' event. 140 | j_to_box.closest('fieldset').one('show.fieldset', resize_filters); 141 | } 142 | } 143 | 144 | // Initial icon refresh 145 | SelectFilter.refresh_icons(field_id); 146 | }, 147 | refresh_icons: function(field_id) { 148 | var from = $('#' + field_id + '_from'); 149 | var to = $('#' + field_id + '_to'); 150 | var is_from_selected = from.find('option:selected').length > 0; 151 | var is_to_selected = to.find('option:selected').length > 0; 152 | // Active if at least one item is selected 153 | $('#' + field_id + '_add_link').toggleClass('active', is_from_selected); 154 | $('#' + field_id + '_remove_link').toggleClass('active', is_to_selected); 155 | // Active if the corresponding box isn't empty 156 | $('#' + field_id + '_add_all_link').toggleClass('active', from.find('option').length > 0); 157 | $('#' + field_id + '_remove_all_link').toggleClass('active', to.find('option').length > 0); 158 | }, 159 | filter_key_press: function(event, field_id) { 160 | var from = document.getElementById(field_id + '_from'); 161 | // don't submit form if user pressed Enter 162 | if ((event.which && event.which === 13) || (event.keyCode && event.keyCode === 13)) { 163 | from.selectedIndex = 0; 164 | SelectBox.move(field_id + '_from', field_id + '_to'); 165 | from.selectedIndex = 0; 166 | event.preventDefault(); 167 | return false; 168 | } 169 | }, 170 | filter_key_up: function(event, field_id) { 171 | var from = document.getElementById(field_id + '_from'); 172 | var temp = from.selectedIndex; 173 | SelectBox.filter(field_id + '_from', document.getElementById(field_id + '_input').value); 174 | from.selectedIndex = temp; 175 | return true; 176 | }, 177 | filter_key_down: function(event, field_id) { 178 | var from = document.getElementById(field_id + '_from'); 179 | // right arrow -- move across 180 | if ((event.which && event.which === 39) || (event.keyCode && event.keyCode === 39)) { 181 | var old_index = from.selectedIndex; 182 | SelectBox.move(field_id + '_from', field_id + '_to'); 183 | from.selectedIndex = (old_index === from.length) ? from.length - 1 : old_index; 184 | return false; 185 | } 186 | // down arrow -- wrap around 187 | if ((event.which && event.which === 40) || (event.keyCode && event.keyCode === 40)) { 188 | from.selectedIndex = (from.length === from.selectedIndex + 1) ? 0 : from.selectedIndex + 1; 189 | } 190 | // up arrow -- wrap around 191 | if ((event.which && event.which === 38) || (event.keyCode && event.keyCode === 38)) { 192 | from.selectedIndex = (from.selectedIndex === 0) ? from.length - 1 : from.selectedIndex - 1; 193 | } 194 | return true; 195 | } 196 | }; 197 | 198 | })(django.jQuery); 199 | -------------------------------------------------------------------------------- /activflow/static/admin/js/actions.js: -------------------------------------------------------------------------------- 1 | /*global _actions_icnt, gettext, interpolate, ngettext*/ 2 | (function($) { 3 | 'use strict'; 4 | var lastChecked; 5 | 6 | $.fn.actions = function(opts) { 7 | var options = $.extend({}, $.fn.actions.defaults, opts); 8 | var actionCheckboxes = $(this); 9 | var list_editable_changed = false; 10 | var showQuestion = function() { 11 | $(options.acrossClears).hide(); 12 | $(options.acrossQuestions).show(); 13 | $(options.allContainer).hide(); 14 | }, 15 | showClear = function() { 16 | $(options.acrossClears).show(); 17 | $(options.acrossQuestions).hide(); 18 | $(options.actionContainer).toggleClass(options.selectedClass); 19 | $(options.allContainer).show(); 20 | $(options.counterContainer).hide(); 21 | }, 22 | reset = function() { 23 | $(options.acrossClears).hide(); 24 | $(options.acrossQuestions).hide(); 25 | $(options.allContainer).hide(); 26 | $(options.counterContainer).show(); 27 | }, 28 | clearAcross = function() { 29 | reset(); 30 | $(options.acrossInput).val(0); 31 | $(options.actionContainer).removeClass(options.selectedClass); 32 | }, 33 | checker = function(checked) { 34 | if (checked) { 35 | showQuestion(); 36 | } else { 37 | reset(); 38 | } 39 | $(actionCheckboxes).prop("checked", checked) 40 | .parent().parent().toggleClass(options.selectedClass, checked); 41 | }, 42 | updateCounter = function() { 43 | var sel = $(actionCheckboxes).filter(":checked").length; 44 | // _actions_icnt is defined in the generated HTML 45 | // and contains the total amount of objects in the queryset 46 | $(options.counterContainer).html(interpolate( 47 | ngettext('%(sel)s of %(cnt)s selected', '%(sel)s of %(cnt)s selected', sel), { 48 | sel: sel, 49 | cnt: _actions_icnt 50 | }, true)); 51 | $(options.allToggle).prop("checked", function() { 52 | var value; 53 | if (sel === actionCheckboxes.length) { 54 | value = true; 55 | showQuestion(); 56 | } else { 57 | value = false; 58 | clearAcross(); 59 | } 60 | return value; 61 | }); 62 | }; 63 | // Show counter by default 64 | $(options.counterContainer).show(); 65 | // Check state of checkboxes and reinit state if needed 66 | $(this).filter(":checked").each(function(i) { 67 | $(this).parent().parent().toggleClass(options.selectedClass); 68 | updateCounter(); 69 | if ($(options.acrossInput).val() === 1) { 70 | showClear(); 71 | } 72 | }); 73 | $(options.allToggle).show().click(function() { 74 | checker($(this).prop("checked")); 75 | updateCounter(); 76 | }); 77 | $("a", options.acrossQuestions).click(function(event) { 78 | event.preventDefault(); 79 | $(options.acrossInput).val(1); 80 | showClear(); 81 | }); 82 | $("a", options.acrossClears).click(function(event) { 83 | event.preventDefault(); 84 | $(options.allToggle).prop("checked", false); 85 | clearAcross(); 86 | checker(0); 87 | updateCounter(); 88 | }); 89 | lastChecked = null; 90 | $(actionCheckboxes).click(function(event) { 91 | if (!event) { event = window.event; } 92 | var target = event.target ? event.target : event.srcElement; 93 | if (lastChecked && $.data(lastChecked) !== $.data(target) && event.shiftKey === true) { 94 | var inrange = false; 95 | $(lastChecked).prop("checked", target.checked) 96 | .parent().parent().toggleClass(options.selectedClass, target.checked); 97 | $(actionCheckboxes).each(function() { 98 | if ($.data(this) === $.data(lastChecked) || $.data(this) === $.data(target)) { 99 | inrange = (inrange) ? false : true; 100 | } 101 | if (inrange) { 102 | $(this).prop("checked", target.checked) 103 | .parent().parent().toggleClass(options.selectedClass, target.checked); 104 | } 105 | }); 106 | } 107 | $(target).parent().parent().toggleClass(options.selectedClass, target.checked); 108 | lastChecked = target; 109 | updateCounter(); 110 | }); 111 | $('form#changelist-form table#result_list tr').find('td:gt(0) :input').change(function() { 112 | list_editable_changed = true; 113 | }); 114 | $('form#changelist-form button[name="index"]').click(function(event) { 115 | if (list_editable_changed) { 116 | return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost.")); 117 | } 118 | }); 119 | $('form#changelist-form input[name="_save"]').click(function(event) { 120 | var action_changed = false; 121 | $('select option:selected', options.actionContainer).each(function() { 122 | if ($(this).val()) { 123 | action_changed = true; 124 | } 125 | }); 126 | if (action_changed) { 127 | if (list_editable_changed) { 128 | return confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")); 129 | } else { 130 | return confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button.")); 131 | } 132 | } 133 | }); 134 | }; 135 | /* Setup plugin defaults */ 136 | $.fn.actions.defaults = { 137 | actionContainer: "div.actions", 138 | counterContainer: "span.action-counter", 139 | allContainer: "div.actions span.all", 140 | acrossInput: "div.actions input.select-across", 141 | acrossQuestions: "div.actions span.question", 142 | acrossClears: "div.actions span.clear", 143 | allToggle: "#action-toggle", 144 | selectedClass: "selected" 145 | }; 146 | })(django.jQuery); 147 | -------------------------------------------------------------------------------- /activflow/static/admin/js/actions.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var f;a.fn.actions=function(q){var b=a.extend({},a.fn.actions.defaults,q),g=a(this),e=!1,k=function(){a(b.acrossClears).hide();a(b.acrossQuestions).show();a(b.allContainer).hide()},l=function(){a(b.acrossClears).show();a(b.acrossQuestions).hide();a(b.actionContainer).toggleClass(b.selectedClass);a(b.allContainer).show();a(b.counterContainer).hide()},m=function(){a(b.acrossClears).hide();a(b.acrossQuestions).hide();a(b.allContainer).hide();a(b.counterContainer).show()},n=function(){m(); 2 | a(b.acrossInput).val(0);a(b.actionContainer).removeClass(b.selectedClass)},p=function(c){c?k():m();a(g).prop("checked",c).parent().parent().toggleClass(b.selectedClass,c)},h=function(){var c=a(g).filter(":checked").length;a(b.counterContainer).html(interpolate(ngettext("%(sel)s of %(cnt)s selected","%(sel)s of %(cnt)s selected",c),{sel:c,cnt:_actions_icnt},!0));a(b.allToggle).prop("checked",function(){var a;c===g.length?(a=!0,k()):(a=!1,n());return a})};a(b.counterContainer).show();a(this).filter(":checked").each(function(c){a(this).parent().parent().toggleClass(b.selectedClass); 3 | h();1===a(b.acrossInput).val()&&l()});a(b.allToggle).show().click(function(){p(a(this).prop("checked"));h()});a("a",b.acrossQuestions).click(function(c){c.preventDefault();a(b.acrossInput).val(1);l()});a("a",b.acrossClears).click(function(c){c.preventDefault();a(b.allToggle).prop("checked",!1);n();p(0);h()});f=null;a(g).click(function(c){c||(c=window.event);var d=c.target?c.target:c.srcElement;if(f&&a.data(f)!==a.data(d)&&!0===c.shiftKey){var e=!1;a(f).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass, 4 | d.checked);a(g).each(function(){if(a.data(this)===a.data(f)||a.data(this)===a.data(d))e=e?!1:!0;e&&a(this).prop("checked",d.checked).parent().parent().toggleClass(b.selectedClass,d.checked)})}a(d).parent().parent().toggleClass(b.selectedClass,d.checked);f=d;h()});a("form#changelist-form table#result_list tr").find("td:gt(0) :input").change(function(){e=!0});a('form#changelist-form button[name="index"]').click(function(a){if(e)return confirm(gettext("You have unsaved changes on individual editable fields. If you run an action, your unsaved changes will be lost."))}); 5 | a('form#changelist-form input[name="_save"]').click(function(c){var d=!1;a("select option:selected",b.actionContainer).each(function(){a(this).val()&&(d=!0)});if(d)return e?confirm(gettext("You have selected an action, but you haven't saved your changes to individual fields yet. Please click OK to save. You'll need to re-run the action.")):confirm(gettext("You have selected an action, and you haven't made any changes on individual fields. You're probably looking for the Go button rather than the Save button."))})}; 6 | a.fn.actions.defaults={actionContainer:"div.actions",counterContainer:"span.action-counter",allContainer:"div.actions span.all",acrossInput:"div.actions input.select-across",acrossQuestions:"div.actions span.question",acrossClears:"div.actions span.clear",allToggle:"#action-toggle",selectedClass:"selected"}})(django.jQuery); 7 | -------------------------------------------------------------------------------- /activflow/static/admin/js/admin/RelatedObjectLookups.js: -------------------------------------------------------------------------------- 1 | /*global SelectBox, interpolate*/ 2 | // Handles related-objects functionality: lookup link for raw_id_fields 3 | // and Add Another links. 4 | 5 | (function() { 6 | 'use strict'; 7 | 8 | function html_unescape(text) { 9 | // Unescape a string that was escaped using django.utils.html.escape. 10 | text = text.replace(/</g, '<'); 11 | text = text.replace(/>/g, '>'); 12 | text = text.replace(/"/g, '"'); 13 | text = text.replace(/'/g, "'"); 14 | text = text.replace(/&/g, '&'); 15 | return text; 16 | } 17 | 18 | // IE doesn't accept periods or dashes in the window name, but the element IDs 19 | // we use to generate popup window names may contain them, therefore we map them 20 | // to allowed characters in a reversible way so that we can locate the correct 21 | // element when the popup window is dismissed. 22 | function id_to_windowname(text) { 23 | text = text.replace(/\./g, '__dot__'); 24 | text = text.replace(/\-/g, '__dash__'); 25 | return text; 26 | } 27 | 28 | function windowname_to_id(text) { 29 | text = text.replace(/__dot__/g, '.'); 30 | text = text.replace(/__dash__/g, '-'); 31 | return text; 32 | } 33 | 34 | function showAdminPopup(triggeringLink, name_regexp, add_popup) { 35 | var name = triggeringLink.id.replace(name_regexp, ''); 36 | name = id_to_windowname(name); 37 | var href = triggeringLink.href; 38 | if (add_popup) { 39 | if (href.indexOf('?') === -1) { 40 | href += '?_popup=1'; 41 | } else { 42 | href += '&_popup=1'; 43 | } 44 | } 45 | var win = window.open(href, name, 'height=500,width=800,resizable=yes,scrollbars=yes'); 46 | win.focus(); 47 | return false; 48 | } 49 | 50 | function showRelatedObjectLookupPopup(triggeringLink) { 51 | return showAdminPopup(triggeringLink, /^lookup_/, true); 52 | } 53 | 54 | function dismissRelatedLookupPopup(win, chosenId) { 55 | var name = windowname_to_id(win.name); 56 | var elem = document.getElementById(name); 57 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) { 58 | elem.value += ',' + chosenId; 59 | } else { 60 | document.getElementById(name).value = chosenId; 61 | } 62 | win.close(); 63 | } 64 | 65 | function showRelatedObjectPopup(triggeringLink) { 66 | return showAdminPopup(triggeringLink, /^(change|add|delete)_/, false); 67 | } 68 | 69 | function updateRelatedObjectLinks(triggeringLink) { 70 | var $this = django.jQuery(triggeringLink); 71 | var siblings = $this.nextAll('.change-related, .delete-related'); 72 | if (!siblings.length) { 73 | return; 74 | } 75 | var value = $this.val(); 76 | if (value) { 77 | siblings.each(function() { 78 | var elm = django.jQuery(this); 79 | elm.attr('href', elm.attr('data-href-template').replace('__fk__', value)); 80 | }); 81 | } else { 82 | siblings.removeAttr('href'); 83 | } 84 | } 85 | 86 | function dismissAddRelatedObjectPopup(win, newId, newRepr) { 87 | // newId and newRepr are expected to have previously been escaped by 88 | // django.utils.html.escape. 89 | newId = html_unescape(newId); 90 | newRepr = html_unescape(newRepr); 91 | var name = windowname_to_id(win.name); 92 | var elem = document.getElementById(name); 93 | if (elem) { 94 | var elemName = elem.nodeName.toUpperCase(); 95 | if (elemName === 'SELECT') { 96 | elem.options[elem.options.length] = new Option(newRepr, newId, true, true); 97 | } else if (elemName === 'INPUT') { 98 | if (elem.className.indexOf('vManyToManyRawIdAdminField') !== -1 && elem.value) { 99 | elem.value += ',' + newId; 100 | } else { 101 | elem.value = newId; 102 | } 103 | } 104 | // Trigger a change event to update related links if required. 105 | django.jQuery(elem).trigger('change'); 106 | } else { 107 | var toId = name + "_to"; 108 | var o = new Option(newRepr, newId); 109 | SelectBox.add_to_cache(toId, o); 110 | SelectBox.redisplay(toId); 111 | } 112 | win.close(); 113 | } 114 | 115 | function dismissChangeRelatedObjectPopup(win, objId, newRepr, newId) { 116 | objId = html_unescape(objId); 117 | newRepr = html_unescape(newRepr); 118 | var id = windowname_to_id(win.name).replace(/^edit_/, ''); 119 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 120 | var selects = django.jQuery(selectsSelector); 121 | selects.find('option').each(function() { 122 | if (this.value === objId) { 123 | this.innerHTML = newRepr; 124 | this.value = newId; 125 | } 126 | }); 127 | win.close(); 128 | } 129 | 130 | function dismissDeleteRelatedObjectPopup(win, objId) { 131 | objId = html_unescape(objId); 132 | var id = windowname_to_id(win.name).replace(/^delete_/, ''); 133 | var selectsSelector = interpolate('#%s, #%s_from, #%s_to', [id, id, id]); 134 | var selects = django.jQuery(selectsSelector); 135 | selects.find('option').each(function() { 136 | if (this.value === objId) { 137 | django.jQuery(this).remove(); 138 | } 139 | }).trigger('change'); 140 | win.close(); 141 | } 142 | 143 | // Global for testing purposes 144 | window.html_unescape = html_unescape; 145 | window.id_to_windowname = id_to_windowname; 146 | window.windowname_to_id = windowname_to_id; 147 | 148 | window.showRelatedObjectLookupPopup = showRelatedObjectLookupPopup; 149 | window.dismissRelatedLookupPopup = dismissRelatedLookupPopup; 150 | window.showRelatedObjectPopup = showRelatedObjectPopup; 151 | window.updateRelatedObjectLinks = updateRelatedObjectLinks; 152 | window.dismissAddRelatedObjectPopup = dismissAddRelatedObjectPopup; 153 | window.dismissChangeRelatedObjectPopup = dismissChangeRelatedObjectPopup; 154 | window.dismissDeleteRelatedObjectPopup = dismissDeleteRelatedObjectPopup; 155 | 156 | // Kept for backward compatibility 157 | window.showAddAnotherPopup = showRelatedObjectPopup; 158 | window.dismissAddAnotherPopup = dismissAddRelatedObjectPopup; 159 | 160 | })(); 161 | -------------------------------------------------------------------------------- /activflow/static/admin/js/calendar.js: -------------------------------------------------------------------------------- 1 | /*global gettext, get_format, quickElement, removeChildren*/ 2 | /* 3 | calendar.js - Calendar functions by Adrian Holovaty 4 | depends on core.js for utility functions like removeChildren or quickElement 5 | */ 6 | 7 | (function() { 8 | 'use strict'; 9 | // CalendarNamespace -- Provides a collection of HTML calendar-related helper functions 10 | var CalendarNamespace = { 11 | monthsOfYear: gettext('January February March April May June July August September October November December').split(' '), 12 | daysOfWeek: gettext('S M T W T F S').split(' '), 13 | firstDayOfWeek: parseInt(get_format('FIRST_DAY_OF_WEEK')), 14 | isLeapYear: function(year) { 15 | return (((year % 4) === 0) && ((year % 100) !== 0 ) || ((year % 400) === 0)); 16 | }, 17 | getDaysInMonth: function(month, year) { 18 | var days; 19 | if (month === 1 || month === 3 || month === 5 || month === 7 || month === 8 || month === 10 || month === 12) { 20 | days = 31; 21 | } 22 | else if (month === 4 || month === 6 || month === 9 || month === 11) { 23 | days = 30; 24 | } 25 | else if (month === 2 && CalendarNamespace.isLeapYear(year)) { 26 | days = 29; 27 | } 28 | else { 29 | days = 28; 30 | } 31 | return days; 32 | }, 33 | draw: function(month, year, div_id, callback, selected) { // month = 1-12, year = 1-9999 34 | var today = new Date(); 35 | var todayDay = today.getDate(); 36 | var todayMonth = today.getMonth() + 1; 37 | var todayYear = today.getFullYear(); 38 | var todayClass = ''; 39 | 40 | // Use UTC functions here because the date field does not contain time 41 | // and using the UTC function variants prevent the local time offset 42 | // from altering the date, specifically the day field. For example: 43 | // 44 | // ``` 45 | // var x = new Date('2013-10-02'); 46 | // var day = x.getDate(); 47 | // ``` 48 | // 49 | // The day variable above will be 1 instead of 2 in, say, US Pacific time 50 | // zone. 51 | var isSelectedMonth = false; 52 | if (typeof selected !== 'undefined') { 53 | isSelectedMonth = (selected.getUTCFullYear() === year && (selected.getUTCMonth() + 1) === month); 54 | } 55 | 56 | month = parseInt(month); 57 | year = parseInt(year); 58 | var calDiv = document.getElementById(div_id); 59 | removeChildren(calDiv); 60 | var calTable = document.createElement('table'); 61 | quickElement('caption', calTable, CalendarNamespace.monthsOfYear[month - 1] + ' ' + year); 62 | var tableBody = quickElement('tbody', calTable); 63 | 64 | // Draw days-of-week header 65 | var tableRow = quickElement('tr', tableBody); 66 | for (var i = 0; i < 7; i++) { 67 | quickElement('th', tableRow, CalendarNamespace.daysOfWeek[(i + CalendarNamespace.firstDayOfWeek) % 7]); 68 | } 69 | 70 | var startingPos = new Date(year, month - 1, 1 - CalendarNamespace.firstDayOfWeek).getDay(); 71 | var days = CalendarNamespace.getDaysInMonth(month, year); 72 | 73 | var nonDayCell; 74 | 75 | // Draw blanks before first of month 76 | tableRow = quickElement('tr', tableBody); 77 | for (i = 0; i < startingPos; i++) { 78 | nonDayCell = quickElement('td', tableRow, ' '); 79 | nonDayCell.className = "nonday"; 80 | } 81 | 82 | // Draw days of month 83 | var currentDay = 1; 84 | for (i = startingPos; currentDay <= days; i++) { 85 | if (i % 7 === 0 && currentDay !== 1) { 86 | tableRow = quickElement('tr', tableBody); 87 | } 88 | if ((currentDay === todayDay) && (month === todayMonth) && (year === todayYear)) { 89 | todayClass = 'today'; 90 | } else { 91 | todayClass = ''; 92 | } 93 | 94 | // use UTC function; see above for explanation. 95 | if (isSelectedMonth && currentDay === selected.getUTCDate()) { 96 | if (todayClass !== '') { 97 | todayClass += " "; 98 | } 99 | todayClass += "selected"; 100 | } 101 | 102 | var cell = quickElement('td', tableRow, '', 'class', todayClass); 103 | 104 | quickElement('a', cell, currentDay, 'href', 'javascript:void(' + callback + '(' + year + ',' + month + ',' + currentDay + '));'); 105 | currentDay++; 106 | } 107 | 108 | // Draw blanks after end of month (optional, but makes for valid code) 109 | while (tableRow.childNodes.length < 7) { 110 | nonDayCell = quickElement('td', tableRow, ' '); 111 | nonDayCell.className = "nonday"; 112 | } 113 | 114 | calDiv.appendChild(calTable); 115 | } 116 | }; 117 | 118 | // Calendar -- A calendar instance 119 | function Calendar(div_id, callback, selected) { 120 | // div_id (string) is the ID of the element in which the calendar will 121 | // be displayed 122 | // callback (string) is the name of a JavaScript function that will be 123 | // called with the parameters (year, month, day) when a day in the 124 | // calendar is clicked 125 | this.div_id = div_id; 126 | this.callback = callback; 127 | this.today = new Date(); 128 | this.currentMonth = this.today.getMonth() + 1; 129 | this.currentYear = this.today.getFullYear(); 130 | if (typeof selected !== 'undefined') { 131 | this.selected = selected; 132 | } 133 | } 134 | Calendar.prototype = { 135 | drawCurrent: function() { 136 | CalendarNamespace.draw(this.currentMonth, this.currentYear, this.div_id, this.callback, this.selected); 137 | }, 138 | drawDate: function(month, year, selected) { 139 | this.currentMonth = month; 140 | this.currentYear = year; 141 | 142 | if(selected) { 143 | this.selected = selected; 144 | } 145 | 146 | this.drawCurrent(); 147 | }, 148 | drawPreviousMonth: function() { 149 | if (this.currentMonth === 1) { 150 | this.currentMonth = 12; 151 | this.currentYear--; 152 | } 153 | else { 154 | this.currentMonth--; 155 | } 156 | this.drawCurrent(); 157 | }, 158 | drawNextMonth: function() { 159 | if (this.currentMonth === 12) { 160 | this.currentMonth = 1; 161 | this.currentYear++; 162 | } 163 | else { 164 | this.currentMonth++; 165 | } 166 | this.drawCurrent(); 167 | }, 168 | drawPreviousYear: function() { 169 | this.currentYear--; 170 | this.drawCurrent(); 171 | }, 172 | drawNextYear: function() { 173 | this.currentYear++; 174 | this.drawCurrent(); 175 | } 176 | }; 177 | window.Calendar = Calendar; 178 | })(); 179 | -------------------------------------------------------------------------------- /activflow/static/admin/js/collapse.js: -------------------------------------------------------------------------------- 1 | /*global gettext*/ 2 | (function($) { 3 | 'use strict'; 4 | $(document).ready(function() { 5 | // Add anchor tag for Show/Hide link 6 | $("fieldset.collapse").each(function(i, elem) { 7 | // Don't hide if fields in this fieldset have errors 8 | if ($(elem).find("div.errors").length === 0) { 9 | $(elem).addClass("collapsed").find("h2").first().append(' (' + gettext("Show") + 11 | ')'); 12 | } 13 | }); 14 | // Add toggle to anchor tag 15 | $("fieldset.collapse a.collapse-toggle").click(function(ev) { 16 | if ($(this).closest("fieldset").hasClass("collapsed")) { 17 | // Show 18 | $(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset", [$(this).attr("id")]); 19 | } else { 20 | // Hide 21 | $(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", [$(this).attr("id")]); 22 | } 23 | return false; 24 | }); 25 | }); 26 | })(django.jQuery); 27 | -------------------------------------------------------------------------------- /activflow/static/admin/js/collapse.min.js: -------------------------------------------------------------------------------- 1 | (function(a){a(document).ready(function(){a("fieldset.collapse").each(function(b,c){0===a(c).find("div.errors").length&&a(c).addClass("collapsed").find("h2").first().append(' ('+gettext("Show")+")")});a("fieldset.collapse a.collapse-toggle").click(function(b){a(this).closest("fieldset").hasClass("collapsed")?a(this).text(gettext("Hide")).closest("fieldset").removeClass("collapsed").trigger("show.fieldset",[a(this).attr("id")]):a(this).text(gettext("Show")).closest("fieldset").addClass("collapsed").trigger("hide.fieldset", 2 | [a(this).attr("id")]);return!1})})})(django.jQuery); 3 | -------------------------------------------------------------------------------- /activflow/static/admin/js/core.js: -------------------------------------------------------------------------------- 1 | // Core javascript helper functions 2 | 3 | // basic browser identification & version 4 | var isOpera = (navigator.userAgent.indexOf("Opera") >= 0) && parseFloat(navigator.appVersion); 5 | var isIE = ((document.all) && (!isOpera)) && parseFloat(navigator.appVersion.split("MSIE ")[1].split(";")[0]); 6 | 7 | // Cross-browser event handlers. 8 | function addEvent(obj, evType, fn) { 9 | 'use strict'; 10 | if (obj.addEventListener) { 11 | obj.addEventListener(evType, fn, false); 12 | return true; 13 | } else if (obj.attachEvent) { 14 | var r = obj.attachEvent("on" + evType, fn); 15 | return r; 16 | } else { 17 | return false; 18 | } 19 | } 20 | 21 | function removeEvent(obj, evType, fn) { 22 | 'use strict'; 23 | if (obj.removeEventListener) { 24 | obj.removeEventListener(evType, fn, false); 25 | return true; 26 | } else if (obj.detachEvent) { 27 | obj.detachEvent("on" + evType, fn); 28 | return true; 29 | } else { 30 | return false; 31 | } 32 | } 33 | 34 | function cancelEventPropagation(e) { 35 | 'use strict'; 36 | if (!e) { 37 | e = window.event; 38 | } 39 | e.cancelBubble = true; 40 | if (e.stopPropagation) { 41 | e.stopPropagation(); 42 | } 43 | } 44 | 45 | // quickElement(tagType, parentReference [, textInChildNode, attribute, attributeValue ...]); 46 | function quickElement() { 47 | 'use strict'; 48 | var obj = document.createElement(arguments[0]); 49 | if (arguments[2]) { 50 | var textNode = document.createTextNode(arguments[2]); 51 | obj.appendChild(textNode); 52 | } 53 | var len = arguments.length; 54 | for (var i = 3; i < len; i += 2) { 55 | obj.setAttribute(arguments[i], arguments[i + 1]); 56 | } 57 | arguments[1].appendChild(obj); 58 | return obj; 59 | } 60 | 61 | // "a" is reference to an object 62 | function removeChildren(a) { 63 | 'use strict'; 64 | while (a.hasChildNodes()) { 65 | a.removeChild(a.lastChild); 66 | } 67 | } 68 | 69 | // ---------------------------------------------------------------------------- 70 | // Cross-browser xmlhttp object 71 | // from http://jibbering.com/2002/4/httprequest.html 72 | // ---------------------------------------------------------------------------- 73 | var xmlhttp; 74 | /*@cc_on @*/ 75 | /*@if (@_jscript_version >= 5) 76 | try { 77 | xmlhttp = new ActiveXObject("Msxml2.XMLHTTP"); 78 | } catch (e) { 79 | try { 80 | xmlhttp = new ActiveXObject("Microsoft.XMLHTTP"); 81 | } catch (E) { 82 | xmlhttp = false; 83 | } 84 | } 85 | @else 86 | xmlhttp = false; 87 | @end @*/ 88 | if (!xmlhttp && typeof XMLHttpRequest !== 'undefined') { 89 | xmlhttp = new XMLHttpRequest(); 90 | } 91 | 92 | // ---------------------------------------------------------------------------- 93 | // Find-position functions by PPK 94 | // See http://www.quirksmode.org/js/findpos.html 95 | // ---------------------------------------------------------------------------- 96 | function findPosX(obj) { 97 | 'use strict'; 98 | var curleft = 0; 99 | if (obj.offsetParent) { 100 | while (obj.offsetParent) { 101 | curleft += obj.offsetLeft - ((isOpera) ? 0 : obj.scrollLeft); 102 | obj = obj.offsetParent; 103 | } 104 | // IE offsetParent does not include the top-level 105 | if (isIE && obj.parentElement) { 106 | curleft += obj.offsetLeft - obj.scrollLeft; 107 | } 108 | } else if (obj.x) { 109 | curleft += obj.x; 110 | } 111 | return curleft; 112 | } 113 | 114 | function findPosY(obj) { 115 | 'use strict'; 116 | var curtop = 0; 117 | if (obj.offsetParent) { 118 | while (obj.offsetParent) { 119 | curtop += obj.offsetTop - ((isOpera) ? 0 : obj.scrollTop); 120 | obj = obj.offsetParent; 121 | } 122 | // IE offsetParent does not include the top-level 123 | if (isIE && obj.parentElement) { 124 | curtop += obj.offsetTop - obj.scrollTop; 125 | } 126 | } else if (obj.y) { 127 | curtop += obj.y; 128 | } 129 | return curtop; 130 | } 131 | 132 | //----------------------------------------------------------------------------- 133 | // Date object extensions 134 | // ---------------------------------------------------------------------------- 135 | (function() { 136 | 'use strict'; 137 | Date.prototype.getTwelveHours = function() { 138 | var hours = this.getHours(); 139 | if (hours === 0) { 140 | return 12; 141 | } 142 | else { 143 | return hours <= 12 ? hours : hours - 12; 144 | } 145 | }; 146 | 147 | Date.prototype.getTwoDigitMonth = function() { 148 | return (this.getMonth() < 9) ? '0' + (this.getMonth() + 1) : (this.getMonth() + 1); 149 | }; 150 | 151 | Date.prototype.getTwoDigitDate = function() { 152 | return (this.getDate() < 10) ? '0' + this.getDate() : this.getDate(); 153 | }; 154 | 155 | Date.prototype.getTwoDigitTwelveHour = function() { 156 | return (this.getTwelveHours() < 10) ? '0' + this.getTwelveHours() : this.getTwelveHours(); 157 | }; 158 | 159 | Date.prototype.getTwoDigitHour = function() { 160 | return (this.getHours() < 10) ? '0' + this.getHours() : this.getHours(); 161 | }; 162 | 163 | Date.prototype.getTwoDigitMinute = function() { 164 | return (this.getMinutes() < 10) ? '0' + this.getMinutes() : this.getMinutes(); 165 | }; 166 | 167 | Date.prototype.getTwoDigitSecond = function() { 168 | return (this.getSeconds() < 10) ? '0' + this.getSeconds() : this.getSeconds(); 169 | }; 170 | 171 | Date.prototype.getHourMinute = function() { 172 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute(); 173 | }; 174 | 175 | Date.prototype.getHourMinuteSecond = function() { 176 | return this.getTwoDigitHour() + ':' + this.getTwoDigitMinute() + ':' + this.getTwoDigitSecond(); 177 | }; 178 | 179 | Date.prototype.strftime = function(format) { 180 | var fields = { 181 | c: this.toString(), 182 | d: this.getTwoDigitDate(), 183 | H: this.getTwoDigitHour(), 184 | I: this.getTwoDigitTwelveHour(), 185 | m: this.getTwoDigitMonth(), 186 | M: this.getTwoDigitMinute(), 187 | p: (this.getHours() >= 12) ? 'PM' : 'AM', 188 | S: this.getTwoDigitSecond(), 189 | w: '0' + this.getDay(), 190 | x: this.toLocaleDateString(), 191 | X: this.toLocaleTimeString(), 192 | y: ('' + this.getFullYear()).substr(2, 4), 193 | Y: '' + this.getFullYear(), 194 | '%': '%' 195 | }; 196 | var result = '', i = 0; 197 | while (i < format.length) { 198 | if (format.charAt(i) === '%') { 199 | result = result + fields[format.charAt(i + 1)]; 200 | ++i; 201 | } 202 | else { 203 | result = result + format.charAt(i); 204 | } 205 | ++i; 206 | } 207 | return result; 208 | }; 209 | 210 | // ---------------------------------------------------------------------------- 211 | // String object extensions 212 | // ---------------------------------------------------------------------------- 213 | String.prototype.pad_left = function(pad_length, pad_string) { 214 | var new_string = this; 215 | for (var i = 0; new_string.length < pad_length; i++) { 216 | new_string = pad_string + new_string; 217 | } 218 | return new_string; 219 | }; 220 | 221 | String.prototype.strptime = function(format) { 222 | var split_format = format.split(/[.\-/]/); 223 | var date = this.split(/[.\-/]/); 224 | var i = 0; 225 | var day, month, year; 226 | while (i < split_format.length) { 227 | switch (split_format[i]) { 228 | case "%d": 229 | day = date[i]; 230 | break; 231 | case "%m": 232 | month = date[i] - 1; 233 | break; 234 | case "%Y": 235 | year = date[i]; 236 | break; 237 | case "%y": 238 | year = date[i]; 239 | break; 240 | } 241 | ++i; 242 | } 243 | return new Date(year, month, day); 244 | }; 245 | 246 | })(); 247 | // ---------------------------------------------------------------------------- 248 | // Get the computed style for and element 249 | // ---------------------------------------------------------------------------- 250 | function getStyle(oElm, strCssRule) { 251 | 'use strict'; 252 | var strValue = ""; 253 | if(document.defaultView && document.defaultView.getComputedStyle) { 254 | strValue = document.defaultView.getComputedStyle(oElm, "").getPropertyValue(strCssRule); 255 | } 256 | else if(oElm.currentStyle) { 257 | strCssRule = strCssRule.replace(/\-(\w)/g, function(strMatch, p1) { 258 | return p1.toUpperCase(); 259 | }); 260 | strValue = oElm.currentStyle[strCssRule]; 261 | } 262 | return strValue; 263 | } 264 | -------------------------------------------------------------------------------- /activflow/static/admin/js/inlines.min.js: -------------------------------------------------------------------------------- 1 | (function(b){b.fn.formset=function(d){var a=b.extend({},b.fn.formset.defaults,d),e=b(this);d=e.parent();var k=function(a,f,l){var c=new RegExp("("+f+"-(\\d+|__prefix__))");f=f+"-"+l;b(a).prop("for")&&b(a).prop("for",b(a).prop("for").replace(c,f));a.id&&(a.id=a.id.replace(c,f));a.name&&(a.name=a.name.replace(c,f))},h=b("#id_"+a.prefix+"-TOTAL_FORMS").prop("autocomplete","off"),l=parseInt(h.val(),10),f=b("#id_"+a.prefix+"-MAX_NUM_FORMS").prop("autocomplete","off"),c=""===f.val()||0'+a.addText+""),m=d.find("tr:last a")):(e.filter(":last").after('"),m=e.filter(":last").next().find("a"));m.click(function(c){c.preventDefault();c=b("#"+a.prefix+ 3 | "-empty");var g=c.clone(!0);g.removeClass(a.emptyCssClass).addClass(a.formCssClass).attr("id",a.prefix+"-"+l);g.is("tr")?g.children(":last").append('"):g.is("ul")||g.is("ol")?g.append('
  • '+a.deleteText+"
  • "):g.children(":first").append(''+a.deleteText+"");g.find("*").each(function(){k(this, 4 | a.prefix,h.val())});g.insertBefore(b(c));b(h).val(parseInt(h.val(),10)+1);l+=1;""!==f.val()&&0>=f.val()-h.val()&&m.parent().hide();g.find("a."+a.deleteCssClass).click(function(c){c.preventDefault();g.remove();--l;a.removed&&a.removed(g);b(document).trigger("formset:removed",[g,a.prefix]);c=b("."+a.formCssClass);b("#id_"+a.prefix+"-TOTAL_FORMS").val(c.length);(""===f.val()||0 0) { 26 | values.push(field.val()); 27 | } 28 | }); 29 | prepopulatedField.val(URLify(values.join(' '), maxLength, allowUnicode)); 30 | }; 31 | 32 | prepopulatedField.data('_changed', false); 33 | prepopulatedField.change(function() { 34 | prepopulatedField.data('_changed', true); 35 | }); 36 | 37 | if (!prepopulatedField.val()) { 38 | $(dependencies.join(',')).keyup(populate).change(populate).focus(populate); 39 | } 40 | }); 41 | }; 42 | })(django.jQuery); 43 | -------------------------------------------------------------------------------- /activflow/static/admin/js/prepopulate.min.js: -------------------------------------------------------------------------------- 1 | (function(c){c.fn.prepopulate=function(e,f,g){return this.each(function(){var a=c(this),b=function(){if(!a.data("_changed")){var b=[];c.each(e,function(a,d){d=c(d);0 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /activflow/static/django_extensions/css/jquery.autocomplete.css: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileOverview CSS for jquery-autocomplete, the jQuery Autocompleter 3 | * @author Dylan Verheul 4 | * @license MIT | GPL | Apache 2.0, see LICENSE.txt 5 | * @see https://github.com/dyve/jquery-autocomplete 6 | */ 7 | .acResults { 8 | padding: 0px; 9 | border: 1px solid WindowFrame; 10 | background-color: Window; 11 | overflow: hidden; 12 | } 13 | 14 | .acResults ul { 15 | margin: 0px; 16 | padding: 0px; 17 | list-style-position: outside; 18 | list-style: none; 19 | } 20 | 21 | .acResults ul li { 22 | margin: 0px; 23 | padding: 2px 5px; 24 | cursor: pointer; 25 | display: block; 26 | font: menu; 27 | font-size: 12px; 28 | overflow: hidden; 29 | } 30 | 31 | .acLoading { 32 | background : url('../img/indicator.gif') right center no-repeat; 33 | } 34 | 35 | .acSelect { 36 | background-color: Highlight; 37 | color: HighlightText; 38 | } 39 | -------------------------------------------------------------------------------- /activflow/static/django_extensions/img/indicator.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/faxad/activflow/f861598de0067a42bbb52ae3401c08dc88451653/activflow/static/django_extensions/img/indicator.gif -------------------------------------------------------------------------------- /activflow/static/django_extensions/js/jquery.ajaxQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Ajax Queue Plugin 3 | * 4 | * Homepage: http://jquery.com/plugins/project/ajaxqueue 5 | * Documentation: http://docs.jquery.com/AjaxQueue 6 | */ 7 | 8 | /** 9 | 10 | 30 |
      31 | 32 | */ 33 | /* 34 | * Queued Ajax requests. 35 | * A new Ajax request won't be started until the previous queued 36 | * request has finished. 37 | */ 38 | 39 | /* 40 | * Synced Ajax requests. 41 | * The Ajax request will happen as soon as you call this method, but 42 | * the callbacks (success/error/complete) won't fire until all previous 43 | * synced requests have been completed. 44 | */ 45 | 46 | 47 | (function(jQuery) { 48 | 49 | var ajax = jQuery.ajax; 50 | 51 | var pendingRequests = {}; 52 | 53 | var synced = []; 54 | var syncedData = []; 55 | 56 | jQuery.ajax = function(settings) { 57 | // create settings for compatibility with ajaxSetup 58 | settings = jQuery.extend(settings, jQuery.extend({}, jQuery.ajaxSettings, settings)); 59 | 60 | var port = settings.port; 61 | 62 | switch(settings.mode) { 63 | case "abort": 64 | if ( pendingRequests[port] ) { 65 | pendingRequests[port].abort(); 66 | } 67 | return pendingRequests[port] = ajax.apply(this, arguments); 68 | case "queue": 69 | var _old = settings.complete; 70 | settings.complete = function(){ 71 | if ( _old ) 72 | _old.apply( this, arguments ); 73 | jQuery([ajax]).dequeue("ajax" + port );; 74 | }; 75 | 76 | jQuery([ ajax ]).queue("ajax" + port, function(){ 77 | ajax( settings ); 78 | }); 79 | return; 80 | case "sync": 81 | var pos = synced.length; 82 | 83 | synced[ pos ] = { 84 | error: settings.error, 85 | success: settings.success, 86 | complete: settings.complete, 87 | done: false 88 | }; 89 | 90 | syncedData[ pos ] = { 91 | error: [], 92 | success: [], 93 | complete: [] 94 | }; 95 | 96 | settings.error = function(){ syncedData[ pos ].error = arguments; }; 97 | settings.success = function(){ syncedData[ pos ].success = arguments; }; 98 | settings.complete = function(){ 99 | syncedData[ pos ].complete = arguments; 100 | synced[ pos ].done = true; 101 | 102 | if ( pos == 0 || !synced[ pos-1 ] ) 103 | for ( var i = pos; i < synced.length && synced[i].done; i++ ) { 104 | if ( synced[i].error ) synced[i].error.apply( jQuery, syncedData[i].error ); 105 | if ( synced[i].success ) synced[i].success.apply( jQuery, syncedData[i].success ); 106 | if ( synced[i].complete ) synced[i].complete.apply( jQuery, syncedData[i].complete ); 107 | 108 | synced[i] = null; 109 | syncedData[i] = null; 110 | } 111 | }; 112 | } 113 | return ajax.apply(this, arguments); 114 | }; 115 | 116 | })((typeof window.jQuery == 'undefined' && typeof window.django != 'undefined') 117 | ? django.jQuery 118 | : jQuery 119 | ); 120 | -------------------------------------------------------------------------------- /activflow/static/django_extensions/js/jquery.bgiframe.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Version 2.1.2 5 | */ 6 | 7 | (function($){ 8 | 9 | $.fn.bgiframe = ($.browser.msie && /msie 6\.0/i.test(navigator.userAgent) ? function(s) { 10 | s = $.extend({ 11 | top : 'auto', // auto == .currentStyle.borderTopWidth 12 | left : 'auto', // auto == .currentStyle.borderLeftWidth 13 | width : 'auto', // auto == offsetWidth 14 | height : 'auto', // auto == offsetHeight 15 | opacity : true, 16 | src : 'javascript:false;' 17 | }, s); 18 | var html = '