Skip to content

Commit

Permalink
Merge branch 'release/0.1'
Browse files Browse the repository at this point in the history
* release/0.1:
  bump version
  updates CI
  updates CI
  updates CI
  updates CI
  updates
  add slug
  add docs and FieldDefinition.content_type
  add pre-commit.yaml
  add missed migration
  improves ValidatorMixi
  updates
  add auto-updateb of ContentType based Fieldset
  bug fixing
  bug fixing
  updates  README
  reset migrations
  • Loading branch information
saxix committed Oct 11, 2024
2 parents d1620dd + c5194ca commit 7267861
Show file tree
Hide file tree
Showing 62 changed files with 1,339 additions and 1,967 deletions.
47 changes: 27 additions & 20 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ jobs:
max-parallel: 1
matrix:
python-version: [ "3.11", "3.12" ]
django-version: [ "3.2", "4.2", "5.0" ]
django-version: [ "4.2", "5.1" ]
fail-fast: true
needs: [ changes ]
if: needs.changes.outputs.run_tests || needs.changes.outputs.lint
Expand All @@ -66,40 +66,47 @@ jobs:
python-version: ${{ matrix.python-version }}
architecture: 'x64'

- name: Cache virtualenv
uses: actions/cache@v3
- name: Restore cached venv
id: cache-venv-restore
uses: actions/cache/restore@v4
with:
key: venv-${{ runner.os }}-${{ steps.setup_python.outputs.python-version}}-${{ hashFiles('pdm.lock') }}
path: .venv
path: |
.cache-uv/
.venv/
key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv

- uses: yezz123/setup-uv@v4
with:
python: ${{ matrix.python-version }}

- name: Test
continue-on-error: true
run: |
pip install pdm
pdm venv create -q
pdm install
- name: lint
if: needs.changes.outputs.lint
run: |
pdm run isort src/ --check-only
pdm run flake8 src/
uv export -q --no-hashes -o {work_dir}/requirements.txt
pip install -r {work_dir}/requirements.txt
pip install '{env:DJANGO}'
pytest tests --create-db --cov --junit-xml junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml
- name: Test
if: needs.changes.outputs.run_tests
run: |
pdm run pytest tests --create-db --junit-xml junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml
- name: Cache venv
if: steps.cache-venv-restore.outputs.cache-hit != 'true'
id: cache-venv-save
uses: actions/cache/save@v4
with:
path: |
.cache-uv/
.venv/
key: ${{ matrix.python-version }}-${{matrix.django-version}}-${{ hashFiles('pyproject.toml') }}-venv

- name: Upload pytest test results
uses: actions/upload-artifact@v4
with:
name: pytest-results-${{ matrix.python-version }}-${{matrix.django-version}}
path: junit-${{ matrix.python-version }}-${{matrix.django-version}}.xml
if: ${{ always() }}
if: matrix.python-version == 3.12

- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
if: ${{ always() }}
if: matrix.python-version == 3.12
continue-on-error: true
with:
env_vars: OS,PYTHON
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ dist/
build/
coverage.xml
*.sqlite3
Makefile
uv.lock
!.git
!.github
!.flake8
Expand Down
31 changes: 30 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,39 @@ repos:
hooks:
- id: flake8
args: [--config=.flake8]

additional_dependencies: [flake8-bugbear==22.9.23]
stages: [ commit ]
- repo: https://github.com/PyCQA/bandit
rev: '1.7.8' # Update me!
rev: '1.7.9' # Update me!
hooks:
- id: bandit
args: ["-c", "bandit.yaml"]
- repo: https://github.com/twisted/towncrier
rev: 23.11.0
hooks:
- id: towncrier-check

- repo: https://github.com/saxix/pch
rev: '0.1'
hooks:
- id: check-missed-migrations
args:
- src
stages: [ commit ]
additional_dependencies: [ setuptools ]

- id: check-untracked
args:
- src
- tests
stages: [ push ]

- id: check-forbidden
args:
- -p
- /\.showbrowser\(/
- -p
- /print\(111/
stages: [ commit ]
additional_dependencies: [ setuptools ]
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,49 @@
# HOPR FlexFields
# HOPE FlexFields

[![Test](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml/badge.svg)](https://github.com/unicef/hope-flex-fields/actions/workflows/test.yml)
[![codecov](https://codecov.io/gh/unicef/hope-flex-fields/graph/badge.svg?token=GSYAH4IEUK)](https://codecov.io/gh/unicef/hope-flex-fields)

This library provides the ability to define a set of fields and related validation rules dynamically. It has been designed as part of the [HOPE](https://github.com/unicef/hct-mis) project to manage user-customizable fields (FlexField). The idea is to have a central business logic repository for data import validation.


It provides four classes:

- FieldDefinition: This represents a collection of reusable pre-configured fields
- FlexField: Instance like representation of `FieldDefinition` inside a `Fieldset`
- Fieldset: Group of FlexField
- DataChecker: Compound of fieldset

From the design point of view a high level comparison with Django components could be:

- `FieldDefinition` = `class forms.Field`
- `Fieldset` = `forms.Form`
- `FlexField` = `forms.Field()`
- `DataChecker` = `[forms.Form(),...]`

... and some utilities

- Automatic creation of FieldSets inspecting [exiting models](http://localhost:8000/hope_flex_fields/fieldset/create_from_content_type/?)
- Automatic creation of XLS file matching an existing [Datachecker](http://localhost:8000/hope_flex_fields/datachecker/)
- Validate XLS against an existing [Datachecker](http://localhost:8000/hope_flex_fields/datachecker/)


```mermaid
classDiagram
class AbstractField
class FieldDefinition
class FlexField
class Fieldset
class DataChecker
AbstractField <|-- FlexField
AbstractField <|-- FieldDefinition
Fieldset *-- FlexField
FlexField --> FieldDefinition
DataChecker o-- Fieldset
```


## Install
CSP_SCRIPT_SRC = [
...
Expand Down
15 changes: 15 additions & 0 deletions docs/_theme/css/style.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions docs/_theme/css/style.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions docs/_theme/css/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
.align-center{
align-content: center;
text-align: center;
width: 100%;
}
.md-typeset__table {
width: 100%;
}

.md-typeset__table table:not([class]) {
display: table
}
50 changes: 50 additions & 0 deletions docs/_theme/js/address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const clickHandler = function () {
let currentAddr = Cookies.get('address') || "https://127.0.0.1/";
let addr = prompt("Set your HOPE server address", currentAddr);
Cookies.set('address', addr, currentAddr);
location.reload();
};
const setAddress = function () {
let cookieAddr = Cookies.get('address');
if (!cookieAddr) {
cookieAddr = "[SERVER_ADDRESS]"
}
for (const cell of document.getElementsByTagName('code')) {
cell.innerHTML = cell.innerHTML.replace('[SERVER_ADDRESS]', cookieAddr);
}
};
// addEventListener('click', function(e) {
// setTimeout(setAddress, 500);
// })
addEventListener('load', function (e) {
setAddress();
let btn = document.getElementById("set-address");
if (btn) {
btn.addEventListener('click', clickHandler);
}
});

var open = window.XMLHttpRequest.prototype.open,
send = window.XMLHttpRequest.prototype.send, onReadyStateChange;

function sendReplacement(data) {
console.warn('Sending HTTP request data : ', data);

if (this.onreadystatechange) {
this._onreadystatechange = this.onreadystatechange;
}
this.onreadystatechange = onReadyStateChangeReplacement;
return send.apply(this, arguments);
}

function onReadyStateChangeReplacement() {
if (this.readyState === XMLHttpRequest.DONE) {
setTimeout(setAddress, 100);
}
if (this._onreadystatechange) {
return this._onreadystatechange.apply(this, arguments);
}

}

window.XMLHttpRequest.prototype.send = sendReplacement;
1 change: 1 addition & 0 deletions docs/_theme/js/address.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 59 additions & 0 deletions docs/_theme/js/js.cookie.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*! js-cookie v3.0.5 | MIT */
!function (e, t) {
"object" == typeof exports && "undefined" != typeof module ? module.exports = t() : "function" == typeof define && define.amd ? define(t) : (e = "undefined" != typeof globalThis ? globalThis : e || self, function () {
var n = e.Cookies, o = e.Cookies = t();
o.noConflict = function () {
return e.Cookies = n, o
}
}())
}(this, (function () {
"use strict";

function e(e) {
for (var t = 1; t < arguments.length; t++) {
var n = arguments[t];
for (var o in n) e[o] = n[o]
}
return e
}

var t = function t(n, o) {
function r(t, r, i) {
if ("undefined" != typeof document) {
"number" == typeof (i = e({}, o, i)).expires && (i.expires = new Date(Date.now() + 864e5 * i.expires)), i.expires && (i.expires = i.expires.toUTCString()), t = encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent).replace(/[()]/g, escape);
var c = "";
for (var u in i) i[u] && (c += "; " + u, !0 !== i[u] && (c += "=" + i[u].split(";")[0]));
return document.cookie = t + "=" + n.write(r, t) + c
}
}

return Object.create({
set: r, get: function (e) {
if ("undefined" != typeof document && (!arguments.length || e)) {
for (var t = document.cookie ? document.cookie.split("; ") : [], o = {}, r = 0; r < t.length; r++) {
var i = t[r].split("="), c = i.slice(1).join("=");
try {
var u = decodeURIComponent(i[0]);
if (o[u] = n.read(c, u), e === u) break
} catch (e) {
}
}
return e ? o[e] : o
}
}, remove: function (t, n) {
r(t, "", e({}, n, {expires: -1}))
}, withAttributes: function (n) {
return t(this.converter, e({}, this.attributes, n))
}, withConverter: function (n) {
return t(e({}, this.converter, n), this.attributes)
}
}, {attributes: {value: Object.freeze(o)}, converter: {value: Object.freeze(n)}})
}({
read: function (e) {
return '"' === e[0] && (e = e.slice(1, -1)), e.replace(/(%[\dA-F]{2})+/gi, decodeURIComponent)
}, write: function (e) {
return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent)
}
}, {path: "/"});
return t
}));
1 change: 1 addition & 0 deletions docs/_theme/js/js.cookie.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions docs/_theme/overrides/term.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "base.html" %}
{%- block content %}
<h5><a href="../../">glossary</a> / {{ page.title }}</h5>
{{ super() }}
{%- endblock %}
3 changes: 3 additions & 0 deletions docs/src/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
!**/.pages
!.includes
_theme/.templates
6 changes: 6 additions & 0 deletions docs/src/.pages
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
nav:
- Home: index.md
- install.md
- contributing.md
- reference.md
- examples.md
44 changes: 44 additions & 0 deletions docs/src/best_practices.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Best Practices


!!! note "General best practices"

- Always return any "readable" value (es. True)
- Uses [Persistent revokes](https://docs.celeryq.dev/en/stable/userguide/workers.html#worker-persistent-revokes)


## Display progress

Inform what is happening inside your task

@celery.task(bind=True)
def long_task(self)-> bool:
record = 1

for entry in Model.objects.all():
self.update_state(state='PROGRESS', meta={'current': record, 'entry': str(entry)})
record += 1
return True


## Sentry Integration

In case you use [Sentry](https://sentry.io/), add some useful information

from functools import wraps

def sentry_tags(func: Callable) -> Callable:
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> Any:
with configure_scope() as scope:
scope.set_tag("celery", True)
scope.set_tag("celery_task", func.__name__)
return func(*args, **kwargs)

return wrapper

@celery.task(bind=True)
@sentry_tags
def task(self) -> bool:
...
return True
Loading

0 comments on commit 7267861

Please sign in to comment.