Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ form-workflows/
__pycache__/
.venv/
FEATURE_GAP_ANALYSIS.md
node_modules/
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
22
64 changes: 61 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Enhancement suggestions are tracked as GitHub issues. When creating an enhanceme
2. **Make your changes** following the coding standards
3. **Add tests** for new functionality
4. **Update documentation** as needed
5. **Ensure tests pass**: `pytest`
5. **Ensure tests pass**: `pytest` (and `npm test` if you touched front-end js)
6. **Ensure code quality**: `black .`, `flake8`, `isort .`
7. **Commit with clear messages**
Comment on lines 36 to 40
8. **Push to your fork** and submit a pull request
Expand Down Expand Up @@ -84,6 +84,28 @@ flake8
mypy django_forms_workflows
```

### 6. (Optional) Install JS Test Tooling

Only needed if you're changing the visual Form/Workflow Builder's client-side
JS (`django_forms_workflows/static/django_forms_workflows/js/`). Requires
Node.js ^20.19.0 or >=22.12.0 / npm — an `.nvmrc` is committed at the repo root, so `nvm use`
picks the right version automatically if you have nvm installed.

```bash
npm install
npm test # run once
npm run test:watch # re-run on change, useful while iterating
```

Test files live in `tests_js/`, its own top-level directory paralleling
`tests/`. Within it, one subfolder per source file under
`django_forms_workflows/static/django_forms_workflows/js/`, each holding one test
file per method/behavior under test.

There's a `helpers/` subfolder for anything shared across that
source file's tests (e.g. `tests_js/form-builder/helpers/loadFormBuilderClass.js`,
which loads the real, un-exported class for testing — see the example below).

## Coding Standards

### Python Style
Expand All @@ -99,6 +121,16 @@ mypy django_forms_workflows
- Use Django's built-in features when possible
- Avoid reinventing the wheel

### JavaScript Style

- Vanilla JS, no framework or bundler dependency — native ES modules
(`<script type="module">`) only
- New client-side logic for the visual builders should be added as an
importable module with a corresponding Vitest test under
`tests_js/form-builder/` or `tests_js/workflow-builder/` (as appropriate),
rather than appended to the existing `form-builder.js`/`workflow-builder.js`
monoliths

### Documentation

- **Docstrings** for all public modules, classes, and functions
Expand Down Expand Up @@ -129,6 +161,29 @@ def test_form_submission_with_approval_creates_tasks():
assert submission.approval_tasks.count() > 0
```

JS changes under `django_forms_workflows/static/django_forms_workflows/js/`
follow the same "add tests with the change" expectation, via Vitest (see
"Development Setup" above). One file per method/behavior under test, in the
matching `tests_js/<source-name>/` folder — e.g.
`tests_js/form-builder/addFieldAtPosition.test.js`:

```js
import { describe, expect, it, vi } from 'vitest';
import { loadFormBuilderClass } from './helpers/loadFormBuilderClass.js';

describe('FormBuilder#addFieldAtPosition', () => {
it('inserts correctly on an empty canvas, when SortableJS reports an out-of-range drop position', () => {
// Arrange / Act / Assert
});
});
```

`form-builder.js`/`workflow-builder.js` are loaded as classic `<script>`
tags, not ES modules, so their classes have no `export` — For now, `loadFormBuilderClass()`
(and its future `loadWorkflowBuilderClass()` counterpart) evaluates the real,
unmodified source in a function scope and returns the class from it, rather
than duplicating any logic in the test.

## Project Structure

```
Expand All @@ -151,9 +206,12 @@ django-forms-workflows/
│ ├── migrations/ # Database migrations
│ └── management/ # Management commands
├── docs/ # Documentation
├── tests/ # Test suite
├── tests/ # Python test suite (pytest)
├── tests_js/ # JS test suite (Vitest); one subfolder per source file, e.g. tests_js/form-builder/
├── example_project/ # Example Django project
├── setup.py # Package configuration
├── pyproject.toml # Package configuration
├── package.json # JS test tooling (Vitest) — dev-only, no runtime JS deps
├── vitest.config.js
├── README.md
├── LICENSE
├── CHANGELOG.md
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@ graph TB
- Optional: `openpyxl` for Excel spreadsheet field support (`pip install django-forms-workflows[excel]`)
- Optional: `django-auth-ldap` for LDAP/AD integration (`pip install django-forms-workflows[ldap]`)
- Optional: WeasyPrint for PDF export (`pip install django-forms-workflows[pdf]`)
- Optional: Node.js ^20.19.0 or >=22.12.0 / npm, to run the JS test suite (`npm test`) covering the visual Form/Workflow Builder's client-side code (an `.nvmrc` is committed at the repo root)

## Testing

Expand All @@ -362,6 +363,17 @@ python -m pytest tests/ -v

The test suite covers models, forms, workflow engine (including dynamic assignees, conditional stages, multi-workflow parallel tracks, sub-workflows), sync API, post-submission action executor, views, signals, conditions, and utilities — **298 tests**.

### JavaScript tests

The visual Form Builder's client-side JS (`django_forms_workflows/static/django_forms_workflows/js/form-builder.js`) has its own suite, run with [Vitest](https://vitest.dev/):

```bash
npm install
npm test
```

`npm run test:watch` re-runs on file changes. Coverage is still minimal (tooling was just added) — see [CONTRIBUTING.md](CONTRIBUTING.md) if you're adding to it.

## Contributing

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,26 +306,8 @@ class FormBuilder {
canvas.addEventListener('drop', (e) => {
e.preventDefault();

// Check if we're dropping a new field from palette
if (this.draggingFieldType) {
const fieldType = this.draggingFieldType;

// Remove placeholder
this.cleanupDragPlaceholder();

// Calculate the position where to insert
const afterElement = this.getDragAfterElement(canvas, e.clientY);
let insertIndex = this.fields.length;

if (afterElement) {
const afterIndex = parseInt(afterElement.dataset.index);
if (!isNaN(afterIndex)) {
insertIndex = afterIndex;
}
}

this.addFieldAtPosition(fieldType, insertIndex);
}
this.cleanupDragPlaceholder();
});
}

Expand Down Expand Up @@ -388,14 +370,14 @@ class FormBuilder {
}
};

// Insert at the specified position
this.fields.splice(position, 0, field);
const insertIndex = Math.min(position, this.fields.length);
this.fields.splice(insertIndex, 0, field);
this.updateFieldOrders();
this.renderCanvas();
this.updatePreview();

// Automatically open property editor for new field
this.editField(position, true); // true = isNew
this.editField(insertIndex, true); // true = isNew
}

setupEventListeners() {
Expand Down Expand Up @@ -676,6 +658,7 @@ class FormBuilder {
// Build property form
const form = this.buildPropertyForm(field);
document.getElementById('fieldPropertyForm').innerHTML = form;
this.initializePropertyFormTabs(field);

// Show modal
const modalElement = document.getElementById('fieldPropertyModal');
Expand Down Expand Up @@ -961,16 +944,6 @@ class FormBuilder {
</div>
</div>
</div>

<script>
// Toggle conditional rules container
document.getElementById('propEnableConditional').addEventListener('change', function(e) {
document.getElementById('conditionalRulesContainer').style.display = e.target.checked ? 'block' : 'none';
});

// Initialize conditions list
window.formBuilder.initializeConditionsList(${JSON.stringify(field.conditional_rules?.conditions || [])});
</script>
`;
}

Expand Down Expand Up @@ -1005,11 +978,6 @@ class FormBuilder {
<small class="text-muted">You can edit the JSON directly for advanced configurations</small>
</div>
</div>

<script>
// Initialize validation rules list
window.formBuilder.initializeValidationRulesList(${JSON.stringify(field.validation_rules || [])});
</script>
`;
}

Expand Down Expand Up @@ -1050,14 +1018,24 @@ class FormBuilder {
<small class="text-muted">You can edit the JSON directly for advanced configurations</small>
</div>
</div>

<script>
// Initialize dependencies list
window.formBuilder.initializeDependenciesList(${JSON.stringify(field.field_dependencies || [])});
</script>
`;
}

initializePropertyFormTabs(field) {
// Wires up the interactive bits of the Conditional Logic, Validation,
// and Dependencies tabs.
const enableConditional = document.getElementById('propEnableConditional');
const conditionalRulesContainer = document.getElementById('conditionalRulesContainer');
if (enableConditional && conditionalRulesContainer) {
enableConditional.addEventListener('change', (e) => {
conditionalRulesContainer.style.display = e.target.checked ? 'block' : 'none';
});
}
this.initializeConditionsList(field.conditional_rules?.conditions || []);
this.initializeValidationRulesList(field.validation_rules || []);
this.initializeDependenciesList(field.field_dependencies || []);
}

initializeConditionsList(conditions) {
const container = document.getElementById('conditionsList');
if (!container) return;
Expand Down
Loading
Loading