96 Commits

Author SHA1 Message Date
62e890245e ... 2025-10-29 20:00:41 +01:00
138846d84d ... 2025-10-29 19:46:20 +01:00
13ed86a780 [Feature] Identify Missing Rules (#177)
Fixes #97
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#177
2025-10-30 00:47:45 +13:00
f1b4c5aadb [Feature] Adding list_display to various django admin sites (#180)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#180
2025-10-29 22:26:28 +13:00
37e0e18a28 [Fix] Fixed Incremental Prediction Typo (#176)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#176
2025-10-28 23:29:08 +13:00
de44c22606 [Migration] Added missing Migration for JobLog (#175)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#175
2025-10-27 22:41:16 +13:00
a952c08469 [Feature] Basic logging of Jobs, Model Evaluation (#169)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#169
2025-10-27 22:34:05 +13:00
551cfc7768 [Enhancement] Create ML Models (#173)
## Changes

- Ability to change the threshold from a command line argument.
- Names of data packages included in model name
- Names of data, rule and eval packages included in the model description
- EnviFormer models are now viewable on the admin site
- Ignore CO2 for training and evaluating EnviFormer

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#173
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-23 06:20:22 +13:00
8fda2577ee [Feature] Dump/Restore of enviFormer Models (#170)
Dump:
`./manage.py  dump_enviformer d544303c-a1ca-439d-b036-5e3413ce4a48 --output test.tar.gz`

Restore:
`./manage.py load_enviformer test.tar.gz 1062eb09-5ec7-4bdd-a8f2-ae0252eb4b06`

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#170
2025-10-22 10:39:22 +13:00
819a94aced [Fix] Catch Exception for Adding Structures / Show PubChem Substances (#168)
Fixes #163
Fixes #165

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#168
2025-10-22 01:13:06 +13:00
376fd65785 [Feature] ML model caching for reducing prediction overhead (#156)
The caching is now finished. The cache is created in `settings.py` giving us the most flexibility for using it in the future.

The cache is currently updated/accessed by `tasks.py/get_ml_model` which can be called from whatever task needs to access ml models in this way (currently, `predict` and `predict_simple`).

This implementation currently caches all ml models including the relative reasoning. If we don't want this and only want to cache enviFormer, i can change it to that. However, I don't think there is a harm in having the other models be cached as well.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#156
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-16 08:58:36 +13:00
d5ebb23622 [Fix] AppDomain Leftovers (#161)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#161
2025-10-16 08:17:39 +13:00
93dd811e39 [Fix] Pathway SVG Export (#157)
Fixes #103

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#157
2025-10-16 02:25:30 +13:00
9a4735246f [Fix] Fix for sending mails (#160)
Captured by Sentry: https://envipath-limited.sentry.io/issues/66662009/?project=4509569727922256

```
SMTPSenderRefused
Level: Error
(504, b'5.5.2 <webmaster@localhost>: Sender address rejected: need fully-qualified address', 'webmaster@localhost')
```

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#160
2025-10-16 02:24:51 +13:00
1f863fdcd6 [Fix] Remove Scenarios from Objects (#159)
Fixes #155

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#159
2025-10-15 20:23:52 +13:00
1effaeb342 [Migration] EnzymeLink Migration (#158)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#158
2025-10-15 19:57:03 +13:00
386098b8a6 [Feature] EnzymeLink Annotations (#152)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#152
2025-10-15 19:35:26 +13:00
ef697ac5f5 [Fix] Added UZH Affiliation, Update UoA Images (#153)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#153
2025-10-15 19:25:23 +13:00
68a3f3b982 [Feature] Alias Support (#151)
Fixes #149

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#151
2025-10-09 23:14:34 +13:00
afeb56622c [Chore] Linted Files (#150)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#150
2025-10-09 07:25:13 +13:00
22f0bbe10b [Feature] Eval package evaluation
`evaluate_model` in `PackageBasedModel` and `EnviFormer` now use evaluation packages if any are present instead of the random splits.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#148
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-08 19:03:21 +13:00
36879c266b [Feature] Documentation for development setup
## Summary

This PR improves the local development setup experience by adding Docker Compose and Makefile for streamlined setup.

## Changes

- **Added `docker-compose.yml`**: for one-command PostgreSQL database setup
- **Added `Makefile`**: Convenient shortcuts for common dev tasks (\`make setup\`, \`make dev\`, etc.)
- **Updated `README.md`**: Quick development setup instructions using Make
-
- **Added**: RDkit installation pain point documentation
- **Fixed**: Made Java feature properly dependent

## Why these changes?

The application uses PostgreSQL-specific features (\`ArrayField\`) and requires an anonymous user created by the bootstrap command. This PR makes the setup process trivial for new developers:

```bash
cp .env.local.example .env
make setup  # Starts DB, runs migrations, bootstraps data
make dev    # Starts development server
```

Java fix:
Moved global Java import to inline to avoid everyone having to configure the Java path.

Numerous changes to view and settings.
- Applied ruff-formatting

## Testing

Verified complete setup from scratch works with:
- PostgreSQL running in Docker
- All migrations applied
- Bootstrap data loaded successfully
- Anonymous user created
- The development server starts correctly.

Co-authored-by: Tobias O <tobias.olenyi@tum.de>
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-authored-by: Liam <62733830+limmooo@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#143
Reviewed-by: jebus <lorsbach@envipath.com>
Reviewed-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-authored-by: t03i <mail+envipath@t03i.net>
Co-committed-by: t03i <mail+envipath@t03i.net>
2025-10-08 18:51:50 +13:00
c2c46fbfa7 [Migration] Added missing Migration for #141 (#147)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#147
2025-10-07 21:22:13 +13:00
d2f4fdc58a [Feature] Enviformer fine tuning and evaluation
## Changes
- I have finished the backend integration of EnviFormer (#19), this includes, dataset building, model finetuning, model evaluation and model prediction with the finetuned model.
- `PackageBasedModel` has been adjusted to be more abstract, this includes making the `_save_model` method and making `compute_averages` a static class function.
- I had to bump the python-version in `pyproject.toml` to >=3.12 from >=3.11 otherwise uv failed to install EnviFormer.
- The default EnviFormer loading during `settings.py` has been removed.

## Future Fix
I noticed you have a little bit of code in `PackageBasedModel` -> `evaluate_model` for using the `eval_packages` during evaluation instead of train/test splits on `data_packages`. It doesn't seem finished, I presume we want this for all models, so I will take care of that in a new branch/pullrequest after this request is merged.

Also, I haven't done anything for a POST request to finetune the model, I'm not sure if that is something we want now.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#141
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-07 21:14:10 +13:00
3f2b046bd6 [Feature] More on Legacy API (#142)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#142
2025-10-03 00:07:30 +13:00
7ad4112343 [Feature] External Identifier/References
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#139
2025-10-02 00:40:00 +13:00
3f5bb76633 [Fix] Remove all Scenarios, catch empty SMILES, prevent default Package delete (#134)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#134
2025-09-30 19:10:57 +13:00
b757a07f91 [Misc] Performance improvements, SMIRKS Coverage, Minor Bugfixes (#132)
Bump Python Version to 3.12
Make use of "epauth" optional
Cache `srs` property of rules to speed up apply
Adjust view names for use of `reverse()`
Fix Views for Scenario Attachments
Added Simply Compare View/Template to identify differences between rdkit and ambit
Make migrations consistent with tests + compare
Fixes #76
Set default year for Scenario Modal
Fix html tags for package description
Added Tests for Pathway / Rule
Added remove stereo for apply

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#132
2025-09-26 19:33:03 +12:00
b5c759d74e [Feature] Register / Login / Logout View Testing (#126)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#126
2025-09-19 06:44:25 +12:00
b3079834c1 [Fix] Registering Issue (#125)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#125
2025-09-19 01:17:03 +12:00
f03ade0e94 [Migration] Multigen Eval Flag Migration (#123)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#123
2025-09-18 18:44:02 +12:00
50db2fb372 [Feature] MultiGen Eval (Backend) (#117)
Fixes #16

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#117
2025-09-18 18:40:45 +12:00
762a6b7baf [Feature] Package Export/Import (#116)
Fixes #90
Fixes #91
Fixes #115
Fixes #104

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#116
2025-09-16 02:41:10 +12:00
ce349a287b [Migration] Migration file for #113 (#114)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#114
2025-09-11 18:23:14 +12:00
7905a8c2c1 [Bug] Fixes for Group handling, Load all objects only if necessary (#113)
Fixes #109
Fixes #110
Fixes #111
Fixes #112

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#113
2025-09-11 18:19:27 +12:00
af3981d96e [Bug] Fix duplicate IDs in SVG defs (#108)
Fixes #107

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#108
2025-09-11 10:20:33 +12:00
62e6448448 [Feature] Legacy Package Import (#106)
Fixes #105

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#106
2025-09-11 01:18:04 +12:00
31783306e2 [Feature] Pathway SVG Export (#102)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#102
2025-09-10 18:44:18 +12:00
e82fe7e87e [Feature] Initial Active Directory / Entra Login (#101)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#101
2025-09-10 08:29:27 +12:00
4463bf1bc8 [Fix] Add RuleBasedRelativeReasoning to localize_urls command (#95)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#95
2025-09-09 21:43:11 +12:00
abfd683640 [Migration] Migration for Rule Based Modelling (#94)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#94
2025-09-09 21:27:10 +12:00
3453a169e1 [Feature] Async Prediction Status Poll (#93)
Fixed #81

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#93
2025-09-09 20:39:26 +12:00
5477b5b3d4 [Feature] Rule Based Model (#92)
Fixes #89

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#92
2025-09-09 19:32:12 +12:00
1a6608287d [Feature] Password Reset Flow (#88)
Fixes #83

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#88
2025-09-06 19:53:36 +12:00
a16035677c [Bugfix] Added Missing Paper in Cite Modal (#87)
Fixes #77

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#87
2025-09-05 08:21:46 +12:00
498a53ab3d [Feature] Move Login exempt urls to settings (#86)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#86
2025-09-05 08:04:20 +12:00
aa3b53e94b [Bugfix] Login required exempts auth for SSO Flow (#85)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#85
2025-09-05 07:59:14 +12:00
3c8f0e80cb [Feature] OAuth2 Provider (#84)
Fixes #74

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#84
2025-09-05 06:50:16 +12:00
4158bd36cb [Feature] Legacy API Layer (#80)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#80
2025-09-03 01:35:51 +12:00
4e02910c62 Add Link to Community in Navbar (#79)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#79
2025-09-02 08:28:37 +12:00
2babe7f7e2 [Feature] Scenario Creation (#78)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#78
2025-09-02 08:06:18 +12:00
7da3880a9b Log only unhandled or logged by 3rd party logger exceptions (#73)
Fixes #67

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#73
2025-09-01 00:16:59 +12:00
52931526c1 If Molecule contains a ~ load it as SMARTS (#71)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#71
2025-08-29 09:14:24 +12:00
dd7b28046c CSS Fix for Index Page (#69)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#69
2025-08-29 08:19:43 +12:00
8592cfae50 Change how List Pages are populated (#68)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#68
2025-08-29 08:09:57 +12:00
ec2b941a85 Fixes #60 (#66)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#66
2025-08-28 06:56:38 +12:00
02d84a9b29 Fix wrong Button Label in "Compound -> Edit" (#64)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#64
2025-08-28 06:32:42 +12:00
00d9188c0c Copy Objects between Packages (#59)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#59
2025-08-28 06:27:11 +12:00
13816ecaf3 Make URL a Field instead a property (#63)
This PR adds a new Field to all existing Models.
As its a data migrations the Migration is added.

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#63
2025-08-27 06:46:09 +12:00
6a4c8d96c3 Adjust Matomo Site ID (#57)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#57
2025-08-26 06:17:09 +12:00
97d0527565 Merge pull request 'fixed calls to create MLRelativeReasoning during bootstrap.py and test_model.py' (#56) from fix/mlrr_arguments into develop
Reviewed-on: enviPath/enviPy#56
2025-08-22 11:29:02 +12:00
b45c99f7d3 fixed calls to create MLRelativeReasoning during bootstrap.py and test_model.py 2025-08-22 11:28:01 +12:00
b95ec98a2f Added Shortcut to make Packages Public (#54)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#54
2025-08-22 07:31:08 +12:00
c79a1f2040 Fix "Impersonation" (#53)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#53
2025-08-22 07:01:49 +12:00
6e6b394289 Cleanup (#52)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#52
2025-08-22 06:36:22 +12:00
ec387cc12e Added UI elements to add/remove Scenarios to various objects (#51)
Fixes #23

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#51
2025-08-21 07:56:44 +12:00
a7637d046a External References (#50)
Fix for #24

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#50
2025-08-21 06:11:05 +12:00
fc8192fb0d Fix App Domain Bug when a Rule can be applied more than once (#49)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#49
2025-08-19 22:10:18 +12:00
c3c1d4f5cf App Domain Pathway Prediction (#47)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#47
2025-08-19 02:53:56 +12:00
3308d47071 Fix bond breaking (#46)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#46
2025-08-15 09:06:07 +12:00
1267ca8ace Enable App Domain Assessment on Model Page (#45)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#45
2025-08-12 09:02:11 +12:00
ec52b8872d Functional Group Calculation (#44)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#44
2025-08-11 09:07:07 +12:00
579cd519d0 Experimental App Domain (#43)
Backend App Domain done, Frontend missing

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#43
2025-08-08 20:52:21 +12:00
280ddc7205 Fixed UUID vs str comparison when getting User (#42)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#42
2025-07-31 09:11:52 +12:00
c9d6d8b024 Delete Stale Edges when removing a Node from a Pathway (#41)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#41
2025-07-31 07:50:50 +12:00
a1aebfa54d Model Building UI Flag (#39)
Fixes #8
Flag only disables UI Elements

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#39
2025-07-31 07:00:54 +12:00
79b4b1586c Download Pathway Functionality (#38)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#38
2025-07-31 01:30:16 +12:00
aec61151ce Correct Label assignment (#37)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#37
2025-07-31 00:43:28 +12:00
026189ccd9 First Issues / Improvements detected in Beta (#36)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#36
2025-07-24 09:57:50 +12:00
2c9f9038f3 Added Community Snippet (#35)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#35
2025-07-23 20:59:36 +12:00
2b8b6f286d Fix Pathway Creation (#34)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#34
2025-07-23 10:04:13 +12:00
2c4c9d95d9 Add Edge Template (#33)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#33
2025-07-23 07:50:39 +12:00
43c95e3da7 feature/enforce_login (#32)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#32
2025-07-23 07:27:57 +12:00
df896878f1 Basic System (#31)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#31
2025-07-23 06:47:07 +12:00
49e02ed97d feature/additional_information (#30)
Fixes #12

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#30
2025-07-19 08:10:40 +12:00
4fff78541b Implement Admin approval (#29)
This PR fixes #7

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#29
2025-07-19 06:42:50 +12:00
9323a9f7d7 Implement Compound CRUD (#22)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#22
2025-07-03 07:17:04 +12:00
4e58a1fad7 Implement functionality to assigne Licenses to Packages and show existing Licenses (#17)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#17
2025-07-03 06:28:49 +12:00
9950112311 Implemented Reaction Page (#5)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#5
2025-06-28 06:40:58 +12:00
6eb1d1bd65 Added Sentry (#4)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#4
2025-06-28 06:00:15 +12:00
bcd9451450 Implement basic Group handling (#3)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#3
2025-06-26 00:18:40 +12:00
844d0708c9 Granting/Updating Permission for Packages (#2)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#2
2025-06-25 08:49:58 +12:00
7c3bc69b38 Scenario Import + Gitea PR Test (#1)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#1
2025-06-24 23:56:45 +12:00
843e8e6f07 make use of prediction_settings() method to ensure that a default setting is used in case no setting is set 2025-06-24 11:33:58 +02:00
acdb62c08f added uv.lock to avoid dep issues 2025-06-24 09:07:32 +02:00
ded50edaa2 Current Dev State 2025-06-23 20:13:54 +02:00
22733 changed files with 4737351 additions and 172 deletions

22
.env.local.example Normal file
View File

@ -0,0 +1,22 @@
# Django settings
SECRET_KEY='a-secure-secret-key-for-development'
DEBUG=True
ALLOWED_HOSTS=*
# Database settings (using PostgreSQL for local development)
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=envipath
POSTGRES_SERVICE_NAME=localhost
POSTGRES_PORT=5432
# Celery settings
CELERY_BROKER_URL='redis://localhost:6379/0'
CELERY_RESULT_BACKEND='redis://localhost:6379/0'
FLAG_CELERY_PRESENT=False
# Other settings
LOG_LEVEL='INFO'
SERVER_URL='http://localhost:8000'
PLUGINS_ENABLED=True
EP_DATA_DIR='data'

18
.env.prod.example Normal file
View File

@ -0,0 +1,18 @@
# settings.py
EP_DATA_DIR=
ALLOWED_HOSTS=
DEBUG=
LOG_LEVEL=
ENVIFORMER_PRESENT=
FLAG_CELERY_PRESENT=
SERVER_URL=
PLUGINS_ENABLED=
# DB
POSTGRES_SERVICE_NAME=
POSTGRES_DB=
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_PORT=
# MAIL
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=

180
.gitignore vendored
View File

@ -1,176 +1,12 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
*.pyc
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.idea/
static/admin/
static/django_extensions/
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
debug.log
scratches/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
data/
.DS_Store

30
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,30 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
hooks:
# Run the linter.
- id: ruff-check
types_or: [python, pyi]
args: [--fix]
# Run the formatter.
- id: ruff-format
types_or: [python, pyi]
# - repo: local
# hooks:
# - id: django-check
# name: Run Django Check
# entry: uv run python manage.py check
# language: system
# pass_filenames: false
# types: [python]

1
.python-version Normal file
View File

@ -0,0 +1 @@
3.12

View File

@ -1,2 +1,91 @@
# enviPy
## Local Development Setup
These instructions will guide you through setting up the project for local development.
### Prerequisites
- Python 3.11 or later
- [uv](https://github.com/astral-sh/uv) - A fast Python package installer and resolver.
- **Docker and Docker Compose** - Required for running the PostgreSQL database.
- Git
> **Note:** This application requires PostgreSQL, which uses `ArrayField`. Docker is the recommended way to run PostgreSQL locally.
### 1. Install Dependencies
This project uses `uv` to manage dependencies and `poe-the-poet` for task running. First, [install `uv` if you don't have it yet](https://docs.astral.sh/uv/guides/install-python/).
Then, sync the project dependencies. This will create a virtual environment in `.venv` and install all necessary packages, including `poe-the-poet`.
```bash
uv sync --dev
```
> **Note on RDkit:** If you have a different version of rdkit installed globally, the dependency installation may fail. If this happens, please uninstall the global version and run `uv sync` again.
### 2. Set Up Environment File
Copy the example environment file for local setup:
```bash
cp .env.local.example .env
```
This file contains the necessary environment variables for local development.
### 3. Quick Setup with Poe
The easiest way to set up the development environment is by using the `poe` task runner, which is executed via `uv run`.
```bash
uv run poe setup
```
This single command will:
1. Start the PostgreSQL database using Docker Compose.
2. Run database migrations.
3. Bootstrap initial data (anonymous user, default packages, models).
After setup, start the development server:
```bash
uv run poe dev
```
The application will be available at `http://localhost:8000`.
#### Other useful Poe commands:
You can list all available commands by running `uv run poe --help`.
```bash
uv run poe db-up # Start PostgreSQL only
uv run poe db-down # Stop PostgreSQL
uv run poe migrate # Run migrations only
uv run poe bootstrap # Bootstrap data only
uv run poe shell # Open the Django shell
uv run poe clean # Remove database volumes (WARNING: destroys all data)
```
### Troubleshooting
* **Docker Connection Error:** If you see an error like `open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified` (on Windows), it likely means your Docker Desktop application is not running. Please start Docker Desktop and try the command again.
* **SSH Keys for Git Dependencies:** Some dependencies are installed from private git repositories and require SSH authentication. Ensure your SSH keys are configured correctly for Git.
* For a general guide, see [GitHub's official documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent).
* **Windows Users:** If `uv sync` hangs while fetching git dependencies, you may need to explicitly configure Git to use the Windows OpenSSH client and use the `ssh-agent` to manage your key's passphrase.
1. **Point Git to the correct SSH executable:**
```powershell
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
```
2. **Enable and use the SSH agent:**
```powershell
# Run these commands in an administrator PowerShell
Get-Service ssh-agent | Set-Service -StartupType Automatic -PassThru | Start-Service
# Add your key to the agent. It will prompt for the passphrase once.
ssh-add
```

20
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,20 @@
services:
db:
image: postgres:15
container_name: envipath-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: envipath
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:

5
envipath/__init__.py Normal file
View File

@ -0,0 +1,5 @@
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ("celery_app",)

12
envipath/api.py Normal file
View File

@ -0,0 +1,12 @@
from epdb.api import router as epdb_app_router
from epdb.legacy_api import router as epdb_legacy_app_router
from ninja import NinjaAPI
api = NinjaAPI()
api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1")
api_legacy = NinjaAPI(title="Legacy API Docs", urls_namespace="api-legacy")
# Add routers
api_v1.add_router("/", epdb_app_router)
api_legacy.add_router("/", epdb_legacy_app_router)

16
envipath/asgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
ASGI config for envipath project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
application = get_asgi_application()

27
envipath/celery.py Normal file
View File

@ -0,0 +1,27 @@
import os
from celery import Celery
from celery.signals import setup_logging
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
app = Celery("envipath")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object("django.conf:settings", namespace="CELERY")
@setup_logging.connect
def config_loggers(*args, **kwargs):
from logging.config import dictConfig
from django.conf import settings
dictConfig(settings.LOGGING)
# Load task modules from all registered Django apps.
app.autodiscover_tasks()

381
envipath/settings.py Normal file
View File

@ -0,0 +1,381 @@
"""
Django settings for envipath project.
Generated by 'django-admin startproject' using Django 4.2.17.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
from dotenv import load_dotenv
from envipy_plugins import Classifier, Property, Descriptor
from sklearn.ensemble import RandomForestClassifier
from sklearn.tree import DecisionTreeClassifier
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / ".env", override=False)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = os.environ.get("SECRET_KEY", "secret-key")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
# Application definition
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 3rd party
"django_extensions",
"oauth2_provider",
# Custom
"epdb",
"migration",
]
# Add the TENANT providing implementations for
# Required
# - Package
# - Compound (TODO)
# - CompoundStructure (TODO)
# Optional
# - PackageManager (TODO)
# - GroupManager (TODO)
# - SettingManager (TODO)
TENANT = os.environ.get("TENANT", "public")
INSTALLED_APPS.append(TENANT)
PACKAGE_IMPLEMENTATION = f"{TENANT}.Package"
PACKAGE_MODULE_PATH = f"{TENANT}.models.Package"
def GET_PACKAGE_MODEL():
from django.apps import apps
return apps.get_model(TENANT, "Package")
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
]
OAUTH2_PROVIDER = {
"PKCE_REQUIRED": False, # Accept PKCE requests but dont require them
}
if os.environ.get("REGISTRATION_MANDATORY", False) == "True":
MIDDLEWARE.append("epdb.middleware.login_required_middleware.LoginRequiredMiddleware")
ROOT_URLCONF = "envipath.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": (os.path.join(BASE_DIR, "templates"),),
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "envipath.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"USER": os.environ["POSTGRES_USER"],
"NAME": os.environ["POSTGRES_DB"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ["POSTGRES_SERVICE_NAME"],
"PORT": os.environ["POSTGRES_PORT"],
}
}
# Password validation
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
EMAIL_SUBJECT_PREFIX = "[enviPath] "
if DEBUG:
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else:
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS = True
EMAIL_HOST = "mail.gandi.net"
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = os.environ["DEFAULT_FROM_EMAIL"]
SERVER_EMAIL = os.environ["SERVER_EMAIL"]
AUTH_USER_MODEL = "epdb.User"
ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") == "True"
# # SESAME
# SESAME_MAX_AGE = 300
# # TODO set to "home"
# LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/login/"
SERVER_URL = os.environ.get("SERVER_URL", "http://localhost:8000")
CSRF_TRUSTED_ORIGINS = [SERVER_URL]
AMBIT_URL = "http://localhost:9001"
DEFAULT_VALUES = {"description": "no description"}
EP_DATA_DIR = os.environ["EP_DATA_DIR"]
if not os.path.exists(EP_DATA_DIR):
os.mkdir(EP_DATA_DIR)
MODEL_DIR = os.path.join(EP_DATA_DIR, "models")
if not os.path.exists(MODEL_DIR):
os.mkdir(MODEL_DIR)
STATIC_DIR = os.path.join(EP_DATA_DIR, "static")
if not os.path.exists(STATIC_DIR):
os.mkdir(STATIC_DIR)
LOG_DIR = os.path.join(EP_DATA_DIR, "log")
if not os.path.exists(LOG_DIR):
os.mkdir(LOG_DIR)
PLUGIN_DIR = os.path.join(EP_DATA_DIR, "plugins")
if not os.path.exists(PLUGIN_DIR):
os.mkdir(PLUGIN_DIR)
# Set this as our static root dir
STATIC_ROOT = STATIC_DIR
STATIC_URL = "/static/"
# Where the sources are stored...
STATICFILES_DIRS = (BASE_DIR / "static",)
FIXTURE_DIRS = (BASE_DIR / "fixtures",)
# Logging
LOGGING = {
"version": 1,
"disable_existing_loggers": True,
"formatters": {
"simple": {
"format": "[%(asctime)s] %(levelname)s %(module)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
"console": {
"level": "INFO",
"class": "logging.StreamHandler",
"formatter": "simple",
},
"file": {
"level": "DEBUG", # Or higher
"class": "logging.FileHandler",
"filename": os.path.join(LOG_DIR, "debug.log"),
"formatter": "simple",
},
},
"loggers": {
# For everything under epdb/ loaded via getlogger(__name__)
"epdb": {
"handlers": ["file"], # "console",
"propagate": True,
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
# For everything under envipath/ loaded via getlogger(__name__)
"envipath": {
"handlers": ["file", "console"],
"propagate": True,
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
# For everything under utilities/ loaded via getlogger(__name__)
"utilities": {
"handlers": ["file", "console"],
"propagate": True,
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
},
}
# Flags
ENVIFORMER_PRESENT = os.environ.get("ENVIFORMER_PRESENT", "False") == "True"
ENVIFORMER_DEVICE = os.environ.get("ENVIFORMER_DEVICE", "cpu")
# If celery is not present set always eager to true which will cause delayed tasks to block until finished
FLAG_CELERY_PRESENT = os.environ.get("FLAG_CELERY_PRESENT", "False") == "True"
if not FLAG_CELERY_PRESENT:
CELERY_TASK_ALWAYS_EAGER = True
# Celery Configuration Options
CELERY_TIMEZONE = "Europe/Berlin"
# Celery Configuration
CELERY_BROKER_URL = "redis://localhost:6379/0" # Use Redis as message broker
CELERY_RESULT_BACKEND = "redis://localhost:6379/1"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
MODEL_BUILDING_ENABLED = os.environ.get("MODEL_BUILDING_ENABLED", "False") == "True"
APPLICABILITY_DOMAIN_ENABLED = os.environ.get("APPLICABILITY_DOMAIN_ENABLED", "False") == "True"
DEFAULT_RF_MODEL_PARAMS = {
"base_clf": RandomForestClassifier(
n_estimators=100,
max_features="log2",
random_state=42,
criterion="entropy",
ccp_alpha=0.0,
max_depth=3,
min_samples_leaf=1,
),
"num_chains": 10,
}
DEFAULT_MODEL_PARAMS = {
"base_clf": DecisionTreeClassifier(
criterion="entropy",
max_depth=3,
min_samples_split=5,
# min_samples_leaf=5,
max_features="sqrt",
# class_weight='balanced',
random_state=42,
),
"num_chains": 10,
}
DEFAULT_MAX_NUMBER_OF_NODES = 30
DEFAULT_MAX_DEPTH = 5
DEFAULT_MODEL_THRESHOLD = 0.25
# Loading Plugins
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
if PLUGINS_ENABLED:
from utilities.plugin import discover_plugins
CLASSIFIER_PLUGINS = discover_plugins(_cls=Classifier)
PROPERTY_PLUGINS = discover_plugins(_cls=Property)
DESCRIPTOR_PLUGINS = discover_plugins(_cls=Descriptor)
else:
CLASSIFIER_PLUGINS = {}
PROPERTY_PLUGINS = {}
DESCRIPTOR_PLUGINS = {}
SENTRY_ENABLED = os.environ.get("SENTRY_ENABLED", "False") == "True"
if SENTRY_ENABLED:
import sentry_sdk
def before_send(event, hint):
# Check if was a handled exception by one of our loggers
if event.get("logger"):
for log_path in LOGGING.get("loggers").keys():
if event["logger"].startswith(log_path):
return None
return event
sentry_sdk.init(
dsn=os.environ.get("SENTRY_DSN"),
# Add data like request headers and IP for users,
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
send_default_pii=True,
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
before_send=before_send,
)
# compile into digestible flags
FLAGS = {
"MODEL_BUILDING": MODEL_BUILDING_ENABLED,
"CELERY": FLAG_CELERY_PRESENT,
"PLUGINS": PLUGINS_ENABLED,
"SENTRY": SENTRY_ENABLED,
"ENVIFORMER": ENVIFORMER_PRESENT,
"APPLICABILITY_DOMAIN": APPLICABILITY_DOMAIN_ENABLED,
}
# path of the URL are checked via "startswith"
# -> /password_reset/done is covered as well
LOGIN_EXEMPT_URLS = [
"/register",
"/api/legacy/",
"/o/token/",
"/o/userinfo/",
"/password_reset/",
"/reset/",
"/microsoft/",
]
# MS AD/Entra
MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True"
if MS_ENTRA_ENABLED:
# Add app to installed apps
INSTALLED_APPS.append("epauth")
# Set vars required by app
MS_ENTRA_CLIENT_ID = os.environ["MS_CLIENT_ID"]
MS_ENTRA_CLIENT_SECRET = os.environ["MS_CLIENT_SECRET"]
MS_ENTRA_TENANT_ID = os.environ["MS_TENANT_ID"]
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")

34
envipath/urls.py Normal file
View File

@ -0,0 +1,34 @@
"""
URL configuration for envipath project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/4.2/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings as s
from django.contrib import admin
from django.urls import include, path
from .api import api_v1, api_legacy
urlpatterns = [
path("", include("epdb.urls")),
path("", include("migration.urls")),
path("admin/", admin.site.urls),
path("api/v1/", api_v1.urls),
path("api/legacy/", api_legacy.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
]
if s.MS_ENTRA_ENABLED:
urlpatterns.append(path("", include("epauth.urls")))

16
envipath/wsgi.py Normal file
View File

@ -0,0 +1,16 @@
"""
WSGI config for envipath project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
application = get_wsgi_application()

0
epauth/__init__.py Normal file
View File

3
epauth/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
epauth/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class EpauthConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'epauth'

View File

3
epauth/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
epauth/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

8
epauth/urls.py Normal file
View File

@ -0,0 +1,8 @@
from django.urls import path
from . import views
urlpatterns = [
path("microsoft/login/", views.microsoft_login, name="microsoft_login"),
path("microsoft/callback/", views.microsoft_callback, name="microsoft_callback"),
]

66
epauth/views.py Normal file
View File

@ -0,0 +1,66 @@
import msal
from django.conf import settings as s
from django.contrib.auth import login
from django.shortcuts import redirect
from django.contrib.auth import get_user_model
from epdb.logic import UserManager
def microsoft_login(request):
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY
)
flow = msal_app.initiate_auth_code_flow(
scopes=s.MS_ENTRA_SCOPES,
redirect_uri=s.MS_ENTRA_REDIRECT_URI
)
request.session["msal_auth_flow"] = flow
return redirect(flow["auth_uri"])
def microsoft_callback(request):
msal_app = msal.ConfidentialClientApplication(
client_id=s.MS_ENTRA_CLIENT_ID,
client_credential=s.MS_ENTRA_CLIENT_SECRET,
authority=s.MS_ENTRA_AUTHORITY
)
flow = request.session.pop("msal_auth_flow", None)
if not flow:
return redirect("/")
# Acquire token using the flow and callback request
result = msal_app.acquire_token_by_auth_code_flow(flow, request.GET)
if "access_token" in result:
# Optional: Fetch user info from Microsoft Graph
import requests
resp = requests.get(
"https://graph.microsoft.com/v1.0/me",
headers={"Authorization": f"Bearer {result['access_token']}"}
)
user_info = resp.json()
user_name = user_info["displayName"]
user_email = user_info["mail"]
user_oid = user_info["id"]
# Get implementing class
User = get_user_model()
if User.objects.filter(uuid=user_oid).exists():
login(request, User.objects.get(uuid=user_oid))
else:
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
login(request, u)
# TODO Group Sync
return redirect("/")
return redirect("/") # Handle errors

0
epdb/__init__.py Normal file
View File

135
epdb/admin.py Normal file
View File

@ -0,0 +1,135 @@
from django.contrib import admin
from django.conf import settings as s
from .models import (
User,
UserPackagePermission,
Group,
GroupPackagePermission,
MLRelativeReasoning,
EnviFormer,
Compound,
CompoundStructure,
SimpleAmbitRule,
ParallelRule,
Reaction,
Pathway,
Node,
Edge,
Scenario,
Setting,
ExternalDatabase,
ExternalIdentifier,
JobLog,
)
Package = s.GET_PACKAGE_MODEL()
class UserAdmin(admin.ModelAdmin):
list_display = ["username", "email", "is_active"]
class UserPackagePermissionAdmin(admin.ModelAdmin):
pass
class GroupAdmin(admin.ModelAdmin):
pass
class GroupPackagePermissionAdmin(admin.ModelAdmin):
pass
class JobLogAdmin(admin.ModelAdmin):
pass
class EPAdmin(admin.ModelAdmin):
search_fields = ["name", "description"]
list_display = ["name", "url", "created"]
ordering = ["-created"]
class PackageAdmin(EPAdmin):
pass
class MLRelativeReasoningAdmin(EPAdmin):
pass
class EnviFormerAdmin(EPAdmin):
pass
class CompoundAdmin(EPAdmin):
pass
class CompoundStructureAdmin(EPAdmin):
pass
class SimpleAmbitRuleAdmin(EPAdmin):
pass
class ParallelRuleAdmin(EPAdmin):
pass
class ReactionAdmin(EPAdmin):
pass
class PathwayAdmin(EPAdmin):
pass
class NodeAdmin(EPAdmin):
pass
class EdgeAdmin(EPAdmin):
pass
class ScenarioAdmin(EPAdmin):
pass
class SettingAdmin(EPAdmin):
pass
class ExternalDatabaseAdmin(admin.ModelAdmin):
pass
class ExternalIdentifierAdmin(admin.ModelAdmin):
pass
admin.site.register(User, UserAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin)
admin.site.register(JobLog, JobLogAdmin)
admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(EnviFormer, EnviFormerAdmin)
admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
admin.site.register(ParallelRule, ParallelRuleAdmin)
admin.site.register(Reaction, ReactionAdmin)
admin.site.register(Pathway, PathwayAdmin)
admin.site.register(Node, NodeAdmin)
admin.site.register(Edge, EdgeAdmin)
admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin)
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)

113
epdb/api.py Normal file
View File

@ -0,0 +1,113 @@
from typing import List
from django.contrib.auth import get_user_model
from ninja import Router, Schema, Field
from ninja.errors import HttpError
from ninja.pagination import paginate
from ninja.security import HttpBearer
from .logic import PackageManager
from .models import User, Compound, APIToken
class BearerTokenAuth(HttpBearer):
def authenticate(self, request, token):
for token_obj in APIToken.objects.select_related("user").all():
if token_obj.check_token(token) and token_obj.is_valid():
return token_obj.user
raise HttpError(401, "Invalid or expired token")
def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous:
return request.user
return get_user_model().objects.get(username="anonymous")
router = Router(auth=BearerTokenAuth())
class UserSchema(Schema):
email: str
username: str
id: str = Field(None, alias="url")
class PackageIn(Schema):
name: str
description: str
class PackageOut(Schema):
id: str = Field(None, alias="url")
name: str
reviewed: bool
compound_links: str = None
@staticmethod
def resolve_compound_links(obj):
return f"{obj.url}/compound"
class Error(Schema):
message: str
class CompoundSchema(Schema):
name: str = Field(None, alias="name")
id: str = Field(None, alias="url")
smiles: str = Field(None, alias="default_structure.smiles")
reviewed: bool = Field(None, alias="package.reviewed")
@router.get("/user", response={200: List[UserSchema], 403: Error})
def get_users(request):
return User.objects.all()
@router.get("/package", response={200: List[PackageOut], 403: Error})
def get_packages(request):
return PackageManager.get_all_readable_packages(request.user, include_reviewed=True)
@router.post("/package", response=PackageOut)
def create_package(request, package: PackageIn):
user = request.auth
name = package.name.strip()
description = package.description.strip()
p = PackageManager.create_package(user, name, description=description)
return p
@router.get("/package/{uuid:package_uuid}", response={200: PackageOut, 403: Error})
def get_package(request, package_uuid):
try:
return PackageManager.get_package_by_id(request.auth, package_id=package_uuid)
except ValueError:
return 403, {
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
}
@router.get("/compound", response={200: List[CompoundSchema], 403: Error})
@paginate
def get_compounds(request):
qs = Compound.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Compound.objects.filter(package=p)
return qs
@router.get(
"/package/{uuid:package_uuid}/compound", response={200: List[CompoundSchema], 403: Error}
)
@paginate
def get_package_compounds(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.auth, package_uuid)
return Compound.objects.filter(package=p)
except ValueError:
return 403, {
"message": f"Getting Compounds for Package with id {package_uuid} failed due to insufficient rights!"
}

9
epdb/apps.py Normal file
View File

@ -0,0 +1,9 @@
from django.apps import AppConfig
class EPDBConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "epdb"
def ready(self):
import epdb.signals # noqa: F401

1243
epdb/legacy_api.py Normal file

File diff suppressed because it is too large Load Diff

1685
epdb/logic.py Normal file

File diff suppressed because it is too large Load Diff

View File

View File

View File

@ -0,0 +1,224 @@
import json
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager
from epdb.models import (
UserSettingPermission,
MLRelativeReasoning,
EnviFormer,
Permission,
User,
ExternalDatabase,
)
class Command(BaseCommand):
def create_users(self):
# Anonymous User
if not User.objects.filter(email="anon@envipath.com").exists():
anon = UserManager.create_user(
"anonymous",
"anon@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
else:
anon = User.objects.get(email="anon@envipath.com")
# Admin User
if not User.objects.filter(email="admin@envipath.com").exists():
admin = UserManager.create_user(
"admin",
"admin@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
admin.is_staff = True
admin.is_superuser = True
admin.save()
else:
admin = User.objects.get(email="admin@envipath.com")
# System Group
g = GroupManager.create_group(admin, "enviPath Users", "All enviPath Users")
g.public = True
g.save()
g.user_member.add(anon)
g.save()
anon.default_group = g
anon.save()
admin.default_group = g
admin.save()
if not User.objects.filter(email="user0@envipath.com").exists():
user0 = UserManager.create_user(
"user0",
"user0@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
user0.is_staff = True
user0.is_superuser = True
user0.save()
else:
user0 = User.objects.get(email="user0@envipath.com")
g.user_member.add(user0)
g.save()
user0.default_group = g
user0.save()
return anon, admin, g, user0
def import_package(self, data, owner):
return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
)
def create_default_setting(self, owner, packages):
s = SettingManager.create_setting(
owner,
name="Global Default Setting",
description="Global Default Setting containing BBD Rules and Max 30 Nodes and Max Depth of 8",
max_nodes=30,
max_depth=5,
rule_packages=packages,
model=None,
model_threshold=None,
)
return s
def populate_common_external_databases(self):
"""
Helper function to populate common external databases.
This can be called from a Django management command.
"""
databases = [
{
"name": "PubChem Compound",
"full_name": "PubChem Compound Database",
"description": "Chemical database of small organic molecules",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/compound/{id}",
},
{
"name": "PubChem Substance",
"full_name": "PubChem Substance Database",
"description": "Database of chemical substances",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/substance/{id}",
},
{
"name": "ChEBI",
"full_name": "Chemical Entities of Biological Interest",
"description": "Dictionary of molecular entities",
"base_url": "https://www.ebi.ac.uk/chebi",
"url_pattern": "https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}",
},
{
"name": "RHEA",
"full_name": "RHEA Reaction Database",
"description": "Comprehensive resource of biochemical reactions",
"base_url": "https://www.rhea-db.org",
"url_pattern": "https://www.rhea-db.org/rhea/{id}",
},
{
"name": "KEGG Reaction",
"full_name": "KEGG Reaction Database",
"description": "Database of biochemical reactions",
"base_url": "https://www.genome.jp",
"url_pattern": "https://www.genome.jp/entry/{id}",
},
{
"name": "UniProt",
"full_name": "MetaCyc Metabolic Pathway Database",
"description": "UniProt is a freely accessible database of protein sequence and functional information",
"base_url": "https://www.uniprot.org",
"url_pattern": 'https://www.uniprot.org/uniprotkb?query="{id}"',
},
]
for db_info in databases:
ExternalDatabase.objects.get_or_create(name=db_info["name"], defaults=db_info)
@transaction.atomic
def handle(self, *args, **options):
# Create users
anon, admin, g, user0 = self.create_users()
self.populate_common_external_databases()
# Import Packages
packages = [
"EAWAG-BBD.json",
"EAWAG-SOIL.json",
"EAWAG-SLUDGE.json",
"EAWAG-SEDIMENT.json",
]
mapping = {}
for p in packages:
print(f"Importing {p}...")
package_data = json.loads(
open(
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
).read()
)
imported_package = self.import_package(package_data, admin)
mapping[p.replace(".json", "")] = imported_package
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
setting.public = True
setting.save()
setting.make_global_default()
for u in [anon, user0]:
u.default_setting = setting
u.save()
usp = UserSettingPermission()
usp.user = u
usp.setting = setting
usp.permission = Permission.READ[0]
usp.save()
# Create Model Package
pack = PackageManager.create_package(
admin,
"Public Prediction Models",
"Package to make Prediction Models publicly available",
)
pack.reviewed = True
pack.save()
# Create RR
ml_model = MLRelativeReasoning.create(
package=pack,
rule_packages=[mapping["EAWAG-BBD"]],
data_packages=[mapping["EAWAG-BBD"]],
eval_packages=[],
threshold=0.5,
name="ECC - BBD - T0.5",
description="ML Relative Reasoning",
)
ml_model.build_dataset()
ml_model.build_model()
# If available, create EnviFormerModel
if s.ENVIFORMER_PRESENT:
EnviFormer.create(pack, "EnviFormer - T0.5", "EnviFormer Model with Threshold 0.5", 0.5)

View File

@ -0,0 +1,123 @@
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer, MLRelativeReasoning
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):
"""This command can be run with
`python manage.py create_ml_models [model_names] -d [data_packages] FOR MLRR ONLY: -r [rule_packages]
OPTIONAL: -e [eval_packages] -t threshold`
For example, to train both EnviFormer and MLRelativeReasoning on BBD and SOIL and evaluate them on SLUDGE with a
threshold of 0.6, the below command would be used:
`python manage.py create_ml_models enviformer mlrr -d bbd soil -e sludge -t 0.6
"""
def add_arguments(self, parser):
parser.add_argument(
"model_names",
nargs="+",
type=str,
help="The names of models to train. Options are: enviformer, mlrr",
)
parser.add_argument(
"-d", "--data-packages", nargs="+", type=str, help="Packages for training"
)
parser.add_argument(
"-e", "--eval-packages", nargs="*", type=str, help="Packages for evaluation", default=[]
)
parser.add_argument(
"-r",
"--rule-packages",
nargs="*",
type=str,
help="Rule Packages mandatory for MLRR",
default=[],
)
parser.add_argument(
"-t",
"--threshold",
type=float,
help="Model prediction threshold",
default=0.5,
)
@transaction.atomic
def handle(self, *args, **options):
# Find Public Prediction Models package to add new models to
try:
pack = Package.objects.filter(name="Public Prediction Models")[0]
bbd = Package.objects.filter(name="EAWAG-BBD")[0]
soil = Package.objects.filter(name="EAWAG-SOIL")[0]
sludge = Package.objects.filter(name="EAWAG-SLUDGE")[0]
sediment = Package.objects.filter(name="EAWAG-SEDIMENT")[0]
except IndexError:
raise IndexError(
"Can't find correct packages. They should be created with the bootstrap command"
)
def decode_packages(package_list):
"""Decode package strings into their respective packages"""
packages = []
for p in package_list:
p = p.lower()
if p == "bbd":
packages.append(bbd)
elif p == "soil":
packages.append(soil)
elif p == "sludge":
packages.append(sludge)
elif p == "sediment":
packages.append(sediment)
else:
raise ValueError(f"Unknown package {p}")
return packages
# Iteratively create models in options["model_names"]
print(
f"Creating models: {options['model_names']}\n"
f"Data packages: {options['data_packages']}\n"
f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
f"Eval Packages: {options['eval_packages']}\n"
f"Threshold: {options['threshold']:.2f}"
)
data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"])
for model_name in options["model_names"]:
model_name = model_name.lower()
if model_name == "enviformer" and s.ENVIFORMER_PRESENT:
model = EnviFormer.create(
pack,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options["threshold"],
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"EnviFormer transformer trained on {options['data_packages']} "
f"evaluated on {options['eval_packages']}.",
)
elif model_name == "mlrr":
model = MLRelativeReasoning.create(
package=pack,
rule_packages=rule_packages,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options["threshold"],
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",
)
else:
raise ValueError(f"Cannot create model of type {model_name}, unknown model type")
# Build the dataset for the model, train it, evaluate it and save it
print(f"Building dataset for {model_name}")
model.build_dataset()
print(f"Training {model_name}")
model.build_model()
print(f"Evaluating {model_name}")
model.evaluate_model(False, eval_packages=eval_packages)
print(f"Saving {model_name}")
model.save()

View File

@ -0,0 +1,59 @@
import json
import os
import tarfile
from tempfile import TemporaryDirectory
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"model",
type=str,
help="Model UUID of the Model to Dump",
)
parser.add_argument("--output", type=str)
def package_dict_and_folder(self, dict_data, folder_path, output_path):
with TemporaryDirectory() as tmpdir:
dict_filename = os.path.join(tmpdir, "data.json")
with open(dict_filename, "w", encoding="utf-8") as f:
json.dump(dict_data, f, indent=2)
with tarfile.open(output_path, "w:gz") as tar:
tar.add(dict_filename, arcname="data.json")
tar.add(folder_path, arcname=os.path.basename(folder_path))
os.remove(dict_filename)
@transaction.atomic
def handle(self, *args, **options):
output = options["output"]
if os.path.exists(output):
raise ValueError(f"Output file {output} already exists")
model = EnviFormer.objects.get(uuid=options["model"])
data = {
"uuid": str(model.uuid),
"name": model.name,
"description": model.description,
"kv": model.kv,
"data_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"eval_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"threshold": model.threshold,
"eval_results": model.eval_results,
"multigen_eval": model.multigen_eval,
"model_status": model.model_status,
}
model_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
self.package_dict_and_folder(data, model_folder, output)

View File

@ -0,0 +1,58 @@
from csv import DictReader
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import Compound, CompoundStructure, Reaction, ExternalDatabase, ExternalIdentifier
class Command(BaseCommand):
STR_TO_MODEL = {
"Compound": Compound,
"CompoundStructure": CompoundStructure,
"Reaction": Reaction,
}
STR_TO_DATABASE = {
"ChEBI": ExternalDatabase.objects.get(name="ChEBI"),
"RHEA": ExternalDatabase.objects.get(name="RHEA"),
"KEGG Reaction": ExternalDatabase.objects.get(name="KEGG Reaction"),
"PubChem Compound": ExternalDatabase.objects.get(name="PubChem Compound"),
"PubChem Substance": ExternalDatabase.objects.get(name="PubChem Substance"),
}
def add_arguments(self, parser):
parser.add_argument(
"--data",
type=str,
help="Path of the ID Mapping file.",
required=True,
)
parser.add_argument(
"--replace-host",
type=str,
help="Replace https://envipath.org/ with this host, e.g. http://localhost:8000/",
)
@transaction.atomic
def handle(self, *args, **options):
with open(options["data"]) as fh:
reader = DictReader(fh)
for row in reader:
clz = self.STR_TO_MODEL[row["model"]]
url = row["url"]
if options["replace_host"]:
url = url.replace("https://envipath.org/", options["replace_host"])
instance = clz.objects.get(url=url)
db = self.STR_TO_DATABASE[row["identifier_type"]]
ExternalIdentifier.objects.create(
content_object=instance,
database=db,
identifier_value=row["identifier_value"],
url=db.url_pattern.format(id=row["identifier_value"]),
is_primary=False,
)

View File

@ -0,0 +1,29 @@
import json
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import PackageManager
from epdb.models import User
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--data",
type=str,
help="Path of the Package to import.",
required=True,
)
parser.add_argument(
"--owner",
type=str,
help="Username of the desired Owner.",
required=True,
)
@transaction.atomic
def handle(self, *args, **options):
owner = User.objects.get(username=options["owner"])
package_data = json.load(open(options["data"]))
PackageManager.import_legacy_package(package_data, owner)

View File

@ -0,0 +1,83 @@
import json
import os
import shutil
import tarfile
from tempfile import TemporaryDirectory
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"input",
type=str,
help=".tar.gz file containing the Model dump.",
)
parser.add_argument(
"package",
type=str,
help="Package UUID where the Model should be loaded to.",
)
def read_dict_and_folder_from_archive(self, archive_path, extract_to="extracted_folder"):
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(extract_to)
dict_path = os.path.join(extract_to, "data.json")
if not os.path.exists(dict_path):
raise FileNotFoundError("data.json not found in the archive.")
with open(dict_path, "r", encoding="utf-8") as f:
data_dict = json.load(f)
extracted_items = os.listdir(extract_to)
folders = [item for item in extracted_items if item != "data.json"]
folder_path = os.path.join(extract_to, folders[0]) if folders else None
return data_dict, folder_path
@transaction.atomic
def handle(self, *args, **options):
if not os.path.exists(options["input"]):
raise ValueError(f"Input file {options['input']} does not exist.")
target_package = Package.objects.get(uuid=options["package"])
with TemporaryDirectory() as tmpdir:
data, folder = self.read_dict_and_folder_from_archive(options["input"], tmpdir)
model = EnviFormer()
model.package = target_package
# model.uuid = data["uuid"]
model.name = data["name"]
model.description = data["description"]
model.kv = data["kv"]
model.threshold = float(data["threshold"])
model.eval_results = data["eval_results"]
model.multigen_eval = data["multigen_eval"]
model.model_status = data["model_status"]
model.save()
for p_uuid in data["data_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.data_packages.add(p)
for p_uuid in data["eval_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.eval_packages.add(p)
target_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
shutil.copytree(folder, target_folder)
os.rename(
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{data['uuid']}.ckpt"),
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{model.uuid}.ckpt"),
)

View File

@ -0,0 +1,67 @@
from django.apps import apps
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db.models import F, JSONField, TextField, Value
from django.db.models.functions import Cast, Replace
from epdb.models import EnviPathModel
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--old",
type=str,
help="Old Host, most likely https://envipath.org/",
required=True,
)
parser.add_argument(
"--new",
type=str,
help="New Host, most likely http://localhost:8000/",
required=True,
)
def handle(self, *args, **options):
Package = s.GET_PACKAGE_MODEL()
print("Localizing urls for Package")
Package.objects.update(url=Replace(F("url"), Value(options["old"]), Value(options["new"])))
MODELS = [
"User",
"Group",
"Compound",
"CompoundStructure",
"Pathway",
"Edge",
"Node",
"Reaction",
"SimpleAmbitRule",
"SimpleRDKitRule",
"ParallelRule",
"SequentialRule",
"Scenario",
"Setting",
"MLRelativeReasoning",
"RuleBasedRelativeReasoning",
"EnviFormer",
"ApplicabilityDomain",
"EnzymeLink",
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
print(f"Localizing urls for {model}")
obj_cls.objects.update(
url=Replace(F("url"), Value(options["old"]), Value(options["new"]))
)
if issubclass(obj_cls, EnviPathModel):
obj_cls.objects.update(
kv=Cast(
Replace(
Cast(F("kv"), output_field=TextField()),
Value(options["old"]),
Value(options["new"]),
),
output_field=JSONField(),
)
)

View File

@ -0,0 +1,38 @@
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import JobLog
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--cleanup",
type=int,
default=None,
help="Remove all logs older than this number of days. Default is None, which does not remove any logs.",
)
@transaction.atomic
def handle(self, *args, **options):
if options["cleanup"] is not None:
cleanup_dt = date.today() - timedelta(days=options["cleanup"])
print(JobLog.objects.filter(created__lt=cleanup_dt).delete())
logs = JobLog.objects.filter(status="INITIAL")
print(f"Found {logs.count()} logs to update")
updated = 0
for log in logs:
res = log.check_for_update()
if res:
updated += 1
print(f"Updated {updated} logs")
from django.db.models import Count
qs = JobLog.objects.values("status").annotate(total=Count("status"))
for r in qs:
print(r["status"], r["total"])

View File

View File

@ -0,0 +1,27 @@
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse
from urllib.parse import quote
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.exempt_urls = [
reverse("login"),
reverse("logout"),
reverse("admin:login"),
reverse("admin:index"),
] + getattr(settings, "LOGIN_EXEMPT_URLS", [])
def __call__(self, request):
if not request.user.is_authenticated:
path = request.path_info
if not any(path.startswith(url) for url in self.exempt_urls):
if request.method == "GET":
if request.get_full_path() and request.get_full_path() != "/":
return redirect(
f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}"
)
return redirect(settings.LOGIN_URL)
return self.get_response(request)

View File

@ -0,0 +1,594 @@
# Generated by Django 5.2.1 on 2025-07-22 20:58
import datetime
import django.contrib.auth.models
import django.contrib.auth.validators
import django.contrib.postgres.fields
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Compound',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
),
migrations.CreateModel(
name='EPModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
],
),
migrations.CreateModel(
name='License',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.URLField(verbose_name='link')),
('image_link', models.URLField(verbose_name='Image link')),
],
),
migrations.CreateModel(
name='Rule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='APIToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hashed_key', models.CharField(max_length=128, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CompoundStructure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('smiles', models.TextField(verbose_name='SMILES')),
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
('normalized_structure', models.BooleanField(default=False)),
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='compound',
name='default_structure',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
),
migrations.CreateModel(
name='Edge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='EnviFormer',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='PluginModel',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='RuleBaseRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(verbose_name='Group name')),
('public', models.BooleanField(default=False, verbose_name='Public Group')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='default_group',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('depth', models.IntegerField(verbose_name='Node depth')),
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='end_nodes',
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
),
migrations.AddField(
model_name='edge',
name='start_nodes',
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='epmodel',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='compound',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='user',
name='default_package',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
),
migrations.CreateModel(
name='SequentialRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.CreateModel(
name='SimpleRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AddField(
model_name='rule',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='rule',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.CreateModel(
name='Pathway',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='node',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.AddField(
model_name='edge',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.CreateModel(
name='Reaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='edge_label',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
),
migrations.CreateModel(
name='Scenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('scenario_date', models.CharField(default='No date', max_length=256)),
('scenario_type', models.CharField(default='Not specified', max_length=256)),
('additional_information', models.JSONField(verbose_name='Additional Information')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='rule',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='reaction',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='pathway',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='node',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='edge',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compoundstructure',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compound',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.CreateModel(
name='Setting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('public', models.BooleanField(default=False)),
('global_default', models.BooleanField(default=False)),
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='pathway',
name='setting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
),
migrations.AddField(
model_name='user',
name='default_setting',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
),
migrations.CreateModel(
name='MLRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='ApplicabilityDomain',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('num_neighbours', models.FloatField(default=5)),
('reliability_threshold', models.FloatField(default=0.5)),
('local_compatibilty_threshold', models.FloatField(default=0.5)),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SimpleAmbitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('smirks', models.TextField(verbose_name='SMIRKS')),
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SimpleRDKitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SequentialRuleOrdering',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_index', models.IntegerField()),
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
],
),
migrations.AddField(
model_name='sequentialrule',
name='simple_rules',
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
),
migrations.CreateModel(
name='ParallelRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AlterUniqueTogether(
name='compound',
unique_together={('uuid', 'package')},
),
migrations.CreateModel(
name='GroupPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
],
options={
'unique_together': {('package', 'group')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('package', 'user')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserSettingPermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('setting', 'user')},
},
bases=('epdb.permission',),
),
]

View File

@ -0,0 +1,128 @@
# Generated by Django 5.2.1 on 2025-08-25 18:07
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('epdb', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExternalDatabase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
],
options={
'verbose_name': 'External Database',
'verbose_name_plural': 'External Databases',
'db_table': 'epdb_external_database',
'ordering': ['name'],
},
),
migrations.AlterModelOptions(
name='apitoken',
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
),
migrations.AlterModelOptions(
name='edge',
options={},
),
migrations.RemoveField(
model_name='edge',
name='polymorphic_ctype',
),
migrations.AddField(
model_name='apitoken',
name='is_active',
field=models.BooleanField(default=True, help_text='Whether this token is active'),
),
migrations.AddField(
model_name='apitoken',
name='modified',
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
),
migrations.AddField(
model_name='applicabilitydomain',
name='functional_groups',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='mlrelativereasoning',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AlterField(
model_name='apitoken',
name='created',
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
),
migrations.AlterField(
model_name='apitoken',
name='expires_at',
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
),
migrations.AlterField(
model_name='apitoken',
name='hashed_key',
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
),
migrations.AlterField(
model_name='apitoken',
name='name',
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
),
migrations.AlterField(
model_name='apitoken',
name='user',
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='applicabilitydomain',
name='num_neighbours',
field=models.IntegerField(default=5),
),
migrations.AlterModelTable(
name='apitoken',
table='epdb_api_token',
),
migrations.CreateModel(
name='ExternalIdentifier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('object_id', models.IntegerField()),
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
],
options={
'verbose_name': 'External Identifier',
'verbose_name_plural': 'External Identifiers',
'db_table': 'epdb_external_identifier',
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
},
),
]

View File

@ -0,0 +1,228 @@
# Generated by Django 5.2.1 on 2025-08-26 17:05
from django.db import migrations, models
def populate_url(apps, schema_editor):
MODELS = [
'User',
'Group',
'Package',
'Compound',
'CompoundStructure',
'Pathway',
'Edge',
'Node',
'Reaction',
'SimpleAmbitRule',
'SimpleRDKitRule',
'ParallelRule',
'SequentialRule',
'Scenario',
'Setting',
'MLRelativeReasoning',
'EnviFormer',
'ApplicabilityDomain',
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
for obj in obj_cls.objects.all():
obj.url = assemble_url(obj)
if obj.url is None:
raise ValueError(f"Could not assemble url for {obj}")
obj.save()
def assemble_url(obj):
from django.conf import settings as s
match obj.__class__.__name__:
case 'User':
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
case 'Group':
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
case 'Package':
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
case 'Compound':
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
case 'CompoundStructure':
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
case 'SimpleAmbitRule':
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
case 'SimpleRDKitRule':
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
case 'ParallelRule':
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
case 'SequentialRule':
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
case 'Reaction':
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
case 'Pathway':
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
case 'Node':
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
case 'Edge':
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
case 'MLRelativeReasoning':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'EnviFormer':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'ApplicabilityDomain':
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
case 'Scenario':
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
case 'Setting':
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
case _:
raise ValueError(f"Unknown model {obj.__class__.__name__}")
class Migration(migrations.Migration):
dependencies = [
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
]
operations = [
migrations.AddField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
]

View File

@ -0,0 +1,55 @@
# Generated by Django 5.2.1 on 2025-09-09 09:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='mlrelativereasoning',
options={},
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='data_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='eval_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
),
migrations.AlterField(
model_name='mlrelativereasoning',
name='rule_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
),
migrations.CreateModel(
name='RuleBasedRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('min_count', models.IntegerField(default=10)),
('max_count', models.IntegerField(default=0)),
('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')),
('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')),
],
options={
'abstract': False,
},
bases=('epdb.epmodel',),
),
migrations.DeleteModel(
name='RuleBaseRelativeReasoning',
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.1 on 2025-09-11 06:21
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0004_alter_mlrelativereasoning_options_and_more'),
]
operations = [
migrations.AlterField(
model_name='group',
name='group_member',
field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'),
),
]

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.1 on 2025-09-18 06:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0005_alter_group_group_member'),
]
operations = [
migrations.AddField(
model_name='mlrelativereasoning',
name='multigen_eval',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='rulebasedrelativereasoning',
name='multigen_eval',
field=models.BooleanField(default=False),
),
]

View File

@ -0,0 +1,53 @@
# Generated by Django 5.2.1 on 2025-10-07 08:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='enviformer',
options={},
),
migrations.AddField(
model_name='enviformer',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AddField(
model_name='enviformer',
name='data_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_results',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='enviformer',
name='model_status',
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
),
migrations.AddField(
model_name='enviformer',
name='multigen_eval',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='enviformer',
name='rule_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
),
]

View File

@ -0,0 +1,64 @@
# Generated by Django 5.2.7 on 2025-10-10 06:58
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
]
operations = [
migrations.CreateModel(
name="EnzymeLink",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("ec_number", models.TextField(verbose_name="EC Number")),
("classification_level", models.IntegerField(verbose_name="Classification Level")),
("linking_method", models.TextField(verbose_name="Linking Method")),
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
(
"rule",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,66 @@
# Generated by Django 5.2.7 on 2025-10-27 09:39
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0008_enzymelink"),
]
operations = [
migrations.CreateModel(
name="JobLog",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
("task_id", models.UUIDField(unique=True)),
("job_name", models.TextField()),
(
"status",
models.CharField(
choices=[
("INITIAL", "Initial"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
("REVOKED", "Revoked"),
("IGNORED", "Ignored"),
],
default="INITIAL",
max_length=20,
),
),
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
("task_result", models.TextField(blank=True, default=None, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -0,0 +1,190 @@
# Generated by Django 5.2.7 on 2025-10-29 13:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0009_joblog"),
("public", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="userpackagepermission",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Permission on",
),
),
migrations.AlterField(
model_name="grouppackagepermission",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Permission on",
),
),
migrations.AlterField(
model_name="epmodel",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="rule",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="compound",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="scenario",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="pathway",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="reaction",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="user",
name="default_package",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="public.package",
verbose_name="Default Package",
),
),
migrations.AlterField(
model_name="enviformer",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="enviformer",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="enviformer",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="setting",
name="rule_packages",
field=models.ManyToManyField(
blank=True,
related_name="setting_rule_packages",
to="public.package",
verbose_name="Setting Rule Packages",
),
),
migrations.DeleteModel(
name="Package",
),
]

View File

3733
epdb/models.py Normal file

File diff suppressed because it is too large Load Diff

37
epdb/signals.py Normal file
View File

@ -0,0 +1,37 @@
import logging
import os
from django.conf import settings as s
from django.db import transaction
from django.db.models.signals import pre_delete, post_delete
from django.dispatch import receiver
from epdb.models import Node, Edge, EPModel
logger = logging.getLogger(__name__)
@receiver(pre_delete, sender=Node)
@transaction.atomic
def delete_orphan_edges(sender, instance, **kwargs):
# check if the node that is about to be deleted is the only start node
for edge in Edge.objects.filter(start_nodes=instance):
if edge.start_nodes.count() == 1:
edge.delete()
# same for end_nodes
for edge in Edge.objects.filter(end_nodes=instance):
# check if the node that is about to be deleted is the only start node
if edge.end_nodes.count() == 1:
edge.delete()
@receiver(post_delete, sender=EPModel)
def delete_epmodel_files(sender, instance, **kwargs):
# Delete the files on disk for the deleted model
mod_uuid = str(instance.uuid)
for f in os.listdir(s.MODEL_DIR):
if f.startswith(mod_uuid):
logger.info(f"Deleting {os.path.join(s.MODEL_DIR, f)}")
os.remove(os.path.join(s.MODEL_DIR, f))

286
epdb/tasks.py Normal file
View File

@ -0,0 +1,286 @@
import csv
import io
import logging
from datetime import datetime
from typing import Any, Callable, List, Optional
from uuid import uuid4
from celery import shared_task
from celery.utils.functional import LRUCache
from django.conf import settings as s
from epdb.logic import SPathway
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
def get_ml_model(model_pk: int):
if model_pk not in ML_CACHE:
ML_CACHE[model_pk] = EPModel.objects.get(id=model_pk)
return ML_CACHE[model_pk]
def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
try:
x = job(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = uuid4()
log.job_name = job.__name__
log.status = "SUCCESS"
log.done_at = datetime.now()
log.task_result = str(x) if x else None
log.save()
return x
except Exception as e:
logger.exception(e)
raise e
def dispatch(user: "User", job: Callable, *args, **kwargs):
try:
x = job.delay(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = x.task_id
log.job_name = job.__name__
log.status = "INITIAL"
log.save()
return x.result
except Exception as e:
logger.exception(e)
raise e
@shared_task(queue="background")
def mul(a, b):
return a * b
@shared_task(queue="predict")
def predict_simple(model_pk: int, smiles: str):
mod = get_ml_model(model_pk)
res = mod.predict(smiles)
return res
@shared_task(queue="background")
def send_registration_mail(user_pk: int):
pass
@shared_task(bind=True, queue="model")
def build_model(self, model_pk: int):
mod = EPModel.objects.get(id=model_pk)
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.build_dataset()
mod.build_model()
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(bind=True, queue="model")
def evaluate_model(self, model_pk: int, multigen: bool, package_pks: Optional[list] = None):
packages = None
if package_pks:
packages = Package.objects.filter(pk__in=package_pks)
mod = EPModel.objects.get(id=model_pk)
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.evaluate_model(multigen, eval_packages=packages)
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(queue="model")
def retrain(model_pk: int):
mod = EPModel.objects.get(id=model_pk)
mod.retrain()
@shared_task(bind=True, queue="predict")
def predict(
self,
pw_pk: int,
pred_setting_pk: int,
limit: Optional[int] = None,
node_pk: Optional[int] = None,
) -> Pathway:
pw = Pathway.objects.get(id=pw_pk)
setting = Setting.objects.get(id=pred_setting_pk)
# If the setting has a model add/restore it from the cache
if setting.model is not None:
setting.model = get_ml_model(setting.model.pk)
pw.kv.update(**{"status": "running"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=pw.url)
try:
# regular prediction
if limit is not None:
spw = SPathway(prediction_setting=setting, persist=pw)
level = 0
while not spw.done:
spw.predict_step(from_depth=level)
level += 1
# break in case we are in incremental mode
if limit != -1:
if level >= limit:
break
elif node_pk is not None:
n = Node.objects.get(id=node_pk, pathway=pw)
spw = SPathway.from_pathway(pw)
spw.predict_step(from_node=n)
else:
raise ValueError("Neither limit nor node_pk given!")
except Exception as e:
pw.kv.update({"status": "failed"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=pw.url
)
raise e
pw.kv.update(**{"status": "completed"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=pw.url)
return pw.url
@shared_task(bind=True, queue="background")
def identify_missing_rules(
self,
pw_pks: List[int],
rule_package_pk: int,
):
from utilities.misc import PathwayUtils
rules = Package.objects.get(pk=rule_package_pk).get_applicable_rules()
rows: List[Any] = []
header = [
"Package Name",
"Pathway Name",
"Educt Name",
"Educt SMILES",
"Reaction Name",
"Reaction SMIRKS",
"Triggered Rules",
"Reactant SMARTS",
"Product SMARTS",
"Product Names",
"Product SMILES",
]
rows.append(header)
for pw in Pathway.objects.filter(pk__in=pw_pks):
pu = PathwayUtils(pw)
missing_rules = pu.find_missing_rules(rules)
package_name = pw.package.name
pathway_name = pw.name
for edge_url, rule_chain in missing_rules.items():
row: List[Any] = [package_name, pathway_name]
edge = Edge.objects.get(url=edge_url)
educts = edge.start_nodes.all()
for educt in educts:
row.append(educt.default_node_label.name)
row.append(educt.default_node_label.smiles)
row.append(edge.edge_label.name)
row.append(edge.edge_label.smirks())
rule_names = []
reactant_smarts = []
product_smarts = []
for r in rule_chain:
r = Rule.objects.get(url=r[0])
rule_names.append(r.name)
rs = r.reactants_smarts
if isinstance(rs, set):
rs = list(rs)
ps = r.products_smarts
if isinstance(ps, set):
ps = list(ps)
reactant_smarts.append(rs)
product_smarts.append(ps)
row.append(rule_names)
row.append(reactant_smarts)
row.append(product_smarts)
products = edge.end_nodes.all()
product_names = []
product_smiles = []
for product in products:
product_names.append(product.default_node_label.name)
product_smiles.append(product.default_node_label.smiles)
row.append(product_names)
row.append(product_smiles)
rows.append(row)
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerows(rows)
buffer.seek(0)
return buffer.getvalue()

View File

View File

@ -0,0 +1,21 @@
from django import template
from pydantic import AnyHttpUrl, ValidationError
from pydantic.type_adapter import TypeAdapter
register = template.Library()
url_adapter = TypeAdapter(AnyHttpUrl)
@register.filter
def classname(obj):
return obj.__class__.__name__
@register.filter
def is_url(value):
try:
url_adapter.validate_python(value)
return True
except ValidationError:
return False

196
epdb/urls.py Normal file
View File

@ -0,0 +1,196 @@
from django.contrib.auth import views as auth_views
from django.urls import path, re_path
from . import views as v
UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
urlpatterns = [
# Home
re_path(r"^$", v.index, name="index"),
# Login
re_path(r"^login", v.login, name="login"),
re_path(r"^logout", v.logout, name="logout"),
re_path(r"^register", v.register, name="register"),
# Built-In views
path(
"password_reset/",
auth_views.PasswordResetView.as_view(template_name="static/password_reset_form.html"),
name="password_reset",
),
path(
"password_reset/done/",
auth_views.PasswordResetDoneView.as_view(template_name="static/password_reset_done.html"),
name="password_reset_done",
),
path(
"reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
template_name="static/password_reset_confirm.html"
),
name="password_reset_confirm",
),
path(
"reset/done/",
auth_views.PasswordResetCompleteView.as_view(
template_name="static/password_reset_complete.html"
),
name="password_reset_complete",
),
# Top level urls
re_path(r"^package$", v.packages, name="packages"),
re_path(r"^compound$", v.compounds, name="compounds"),
re_path(r"^rule$", v.rules, name="rules"),
re_path(r"^reaction$", v.reactions, name="reactions"),
re_path(r"^pathway$", v.pathways, name="pathways"),
re_path(r"^scenario$", v.scenarios, name="scenarios"),
re_path(r"^model$", v.models, name="model"),
re_path(r"^user$", v.users, name="users"),
re_path(r"^group$", v.groups, name="groups"),
re_path(r"^search$", v.search, name="search"),
# User Detail
re_path(rf"^user/(?P<user_uuid>{UUID})", v.user, name="user"),
# Group Detail
re_path(rf"^group/(?P<group_uuid>{UUID})$", v.group, name="group detail"),
# "in package" urls
re_path(rf"^package/(?P<package_uuid>{UUID})$", v.package, name="package detail"),
# Compound
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound$",
v.package_compounds,
name="package compound list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})$",
v.package_compound,
name="package compound detail",
),
# Compound Structure
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure$",
v.package_compound_structures,
name="package compound structure list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure/(?P<structure_uuid>{UUID})$",
v.package_compound_structure,
name="package compound structure detail",
),
# Rule
re_path(rf"^package/(?P<package_uuid>{UUID})/rule$", v.package_rules, name="package rule list"),
re_path(
rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
# re_path(
# rf"^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$",
# v.package_rule,
# name="package rule detail",
# ),
re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
# re_path(
# rf"^package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$",
# v.package_rule,
# name="package rule detail",
# ),
# EnzymeLinks
re_path(
rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
# Reaction
re_path(
rf"^package/(?P<package_uuid>{UUID})/reaction$",
v.package_reactions,
name="package reaction list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/reaction/(?P<reaction_uuid>{UUID})$",
v.package_reaction,
name="package reaction detail",
),
# # Pathway
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway$",
v.package_pathways,
name="package pathway list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})$",
v.package_pathway,
name="package pathway detail",
),
# Pathway Nodes
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$",
v.package_pathway_nodes,
name="package pathway node list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node/(?P<node_uuid>{UUID})$",
v.package_pathway_node,
name="package pathway node detail",
),
# Pathway Edges
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge$",
v.package_pathway_edges,
name="package pathway edge list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge/(?P<edge_uuid>{UUID})$",
v.package_pathway_edge,
name="package pathway edge detail",
),
# Scenario
re_path(
rf"^package/(?P<package_uuid>{UUID})/scenario$",
v.package_scenarios,
name="package scenario list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/scenario/(?P<scenario_uuid>{UUID})$",
v.package_scenario,
name="package scenario detail",
),
# Model
re_path(
rf"^package/(?P<package_uuid>{UUID})/model$", v.package_models, name="package model list"
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/model/(?P<model_uuid>{UUID})$",
v.package_model,
name="package model detail",
),
re_path(r"^setting$", v.settings, name="settings"),
re_path(rf"^setting/(?P<setting_uuid>{UUID})", v.setting, name="setting"),
re_path(r"^indigo/info$", v.indigo, name="indigo_info"),
re_path(r"^indigo/aromatize$", v.aromatize, name="indigo_aromatize"),
re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"),
re_path(r"^indigo/layout$", v.layout, name="indigo_layout"),
re_path(r"^depict$", v.depict, name="depict"),
re_path(r"^jobs", v.jobs, name="jobs"),
# OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
]

2840
epdb/views.py Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

22
manage.py Executable file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'envipath.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
migration/__init__.py Normal file
View File

3
migration/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
migration/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class MigrationConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'migration'

View File

3
migration/models.py Normal file
View File

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

3
migration/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

16
migration/urls.py Normal file
View File

@ -0,0 +1,16 @@
from django.urls import re_path
from . import views as v
UUID = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
urlpatterns = [
re_path(rf'^migration$', v.migration, name='migration'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/simple-rule/(?P<rule_uuid>{UUID})$',v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/compare$', v.compare, name='compare'),
]

265
migration/views.py Normal file
View File

@ -0,0 +1,265 @@
import json
import logging
import os.path
from django.conf import settings as s
from django.http import HttpResponseNotAllowed
from django.shortcuts import render
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
from epdb.models import CompoundStructure, Rule, SimpleAmbitRule
from epdb.views import get_base_context
from utilities.chem import FormatConverter
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
def normalize_smiles(smiles):
m1 = Chem.MolFromSmiles(smiles)
if m1 is None:
print("Couldnt read smi: ", smiles)
return smiles
Chem.RemoveStereochemistry(m1)
# Normalizer takes care of charge/tautomer/resonance standardization
normalizer = rdMolStandardize.Normalizer()
return Chem.MolToSmiles(normalizer.normalize(m1), canonical=True)
def run_both_engines(SMILES, SMIRKS):
from envipy_ambit import apply
ambit_res = apply(SMIRKS, SMILES)
# ambit_res, ambit_errors = FormatConverter.sanitize_smiles([str(s) for s in ambit_res])
ambit_res = list(
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles([str(s) for s in ambit_res])[0]
]
)
)
products = FormatConverter.apply(SMILES, SMIRKS)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
# all_rdkit_res, rdkit_errors = FormatConverter.sanitize_smiles(all_rdkit_prods)
all_rdkit_res = list(
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles([str(s) for s in all_rdkit_prods])[0]
]
)
)
# return ambit_res, ambit_errors, all_rdkit_res, rdkit_errors
return ambit_res, 0, all_rdkit_res, 0
def migration(request):
if request.method == "GET":
context = get_base_context(request)
if (
os.path.exists(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json")
and request.GET.get("force") is None
):
migration_status = json.load(
open(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json")
)
else:
BBD = Package.objects.get(
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
)
ALL_SMILES = [
cs.smiles for cs in CompoundStructure.objects.filter(compound__package=BBD)
]
RULES = SimpleAmbitRule.objects.filter(package=BBD)
results = list()
num_rules = len(RULES)
success = 0
error = 0
total = 0
for i, r in enumerate(RULES):
logger.debug(f"\r{i + 1:03d}/{num_rules}")
res = True
for smiles in ALL_SMILES:
try:
ambit_res, _, rdkit_res, _ = run_both_engines(smiles, r.smirks)
res &= set(ambit_res) == set(rdkit_res)
except Exception as e:
logger.error(e)
results.append(
{
"name": r.name,
"detail_url": s.SERVER_URL
+ "/migration/"
+ r.url.replace("https://envipath.org/", "").replace(
"http://localhost:8000/", ""
),
"id": str(r.uuid),
"url": r.url,
"status": res,
}
)
if res:
success += 1
else:
error += 1
total += 1
results = sorted(results, key=lambda x: (x["status"], x["name"]))
migration_status = {
"results": results,
"success": success,
"error": error,
"total": total,
}
json.dump(
migration_status,
open(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json", "w"),
)
for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
context.update(**migration_status)
return render(request, "migration.html", context)
def migration_detail(request, package_uuid, rule_uuid):
if request.method == "GET":
context = get_base_context(request)
BBD = Package.objects.get(name="EAWAG-BBD")
STRUCTURES = CompoundStructure.objects.filter(compound__package=BBD)
rule = Rule.objects.get(package=BBD, uuid=rule_uuid)
bt_rule_name = rule.name
smirks = rule.smirks
res = True
results = []
all_prods = set()
for structure in STRUCTURES:
ambit_smiles, ambit_errors, rdkit_smiles, rdkit_errors = run_both_engines(
structure.smiles, smirks
)
for x in ambit_smiles:
all_prods.add(x)
# TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0)
# FAILED (failures=18)
# TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(set(ambit_smiles))
# FAILED (failures=34)
# TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=30)
if len(ambit_smiles) or len(rdkit_smiles):
temp = {
"url": structure.url,
"id": str(structure.uuid),
"name": structure.name,
"initial_smiles": structure.smiles,
"ambit_smiles": sorted(list(ambit_smiles)),
"rdkit_smiles": sorted(list(rdkit_smiles)),
"status": set(ambit_smiles) == set(rdkit_smiles),
}
detail = f"""
BT: {bt_rule_name}
SMIRKS: {smirks}
Compound: {structure.smiles}
Compound URL: {structure.url}
Num ambit: {len(set(ambit_smiles))}
Num rdkit: {len(set(rdkit_smiles))}
Num Intersection A: {len(set(ambit_smiles).intersection(set(rdkit_smiles)))}
Num Intersection B: {len(set(rdkit_smiles).intersection(set(ambit_smiles)))}
Difference A: {set(ambit_smiles).difference(set(rdkit_smiles))}
Difference B: {set(rdkit_smiles).difference(set(ambit_smiles))}
ambit products: {ambit_smiles}
rdkit products: {rdkit_smiles}
ambit_errors: {ambit_errors}
rdkit_errors: {rdkit_errors}
"""
temp["detail"] = "\n".join([x.strip() for x in detail.split("\n")])
results.append(temp)
res &= partial_res
results = sorted(results, key=lambda x: x["status"])
context["results"] = results
context["res"] = res
context["bt_rule_name"] = bt_rule_name
return render(request, "migration_detail.html", context)
def compare(request):
context = get_base_context(request)
if request.method == "GET":
context["smirks"] = (
"[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O"
)
context["smiles"] = "C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
return render(request, "compare.html", context)
elif request.method == "POST":
smiles = request.POST.get("smiles")
smirks = request.POST.get("smirks")
from envipy_ambit import apply
ambit_res = apply(smirks, smiles)
ambit_res, _ = FormatConverter.sanitize_smiles([str(x) for x in ambit_res])
products = FormatConverter.apply(smiles, smirks)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
rdkit_res, _ = FormatConverter.sanitize_smiles(all_rdkit_prods)
context["result"] = True
context["ambit_res"] = sorted(set(ambit_res))
context["rdkit_res"] = sorted(set(rdkit_res))
context["diff"] = sorted(set(ambit_res).difference(set(rdkit_res)))
context["smirks"] = smirks
context["smiles"] = smiles
r = SimpleAmbitRule.objects.filter(smirks=smirks)
if r.exists():
context["rule"] = r.first()
return render(request, "compare.html", context)
else:
return HttpResponseNotAllowed(["GET", "POST"])

0
public/__init__.py Normal file
View File

1
public/admin.py Normal file
View File

@ -0,0 +1 @@
# Register your models here.

6
public/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PublicConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "public"

View File

@ -0,0 +1,56 @@
# Generated by Django 5.2.7 on 2025-10-29 13:32
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Package",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("reviewed", models.BooleanField(default=False, verbose_name="Reviewstatus")),
],
options={
"db_table": "epdb_package",
"managed": False,
},
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 5.2.7 on 2025-10-29 18:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("public", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="package",
options={},
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2.7 on 2025-10-29 18:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0010_alter_userpackagepermission_package_and_more"),
("public", "0002_alter_package_options"),
]
operations = [
migrations.AddField(
model_name="package",
name="license",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="epdb.license",
verbose_name="License",
),
),
]

View File

6
public/models.py Normal file
View File

@ -0,0 +1,6 @@
from epdb.models import AbstractPackage
class Package(AbstractPackage):
class Meta:
db_table = "epdb_package"

1
public/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

1
public/views.py Normal file
View File

@ -0,0 +1 @@
# Create your views here.

91
pyproject.toml Normal file
View File

@ -0,0 +1,91 @@
[project]
name = "envipy"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"celery>=5.5.2",
"django>=5.2.1",
"django-extensions>=4.1",
"django-model-utils>=5.0.0",
"django-ninja>=1.4.1",
"django-oauth-toolkit>=3.0.1",
"django-polymorphic>=4.1.0",
"enviformer",
"envipy-additional-information",
"envipy-ambit>=0.1.0",
"envipy-plugins",
"epam-indigo>=1.30.1",
"gunicorn>=23.0.0",
"networkx>=3.4.2",
"psycopg2-binary>=2.9.10",
"python-dotenv>=1.1.0",
"rdkit>=2025.3.2",
"redis>=6.1.0",
"requests>=2.32.3",
"scikit-learn>=1.6.1",
"sentry-sdk[django]>=2.32.0",
"setuptools>=80.8.0",
]
[tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"}
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
[project.optional-dependencies]
ms-login = ["msal>=1.33.0"]
dev = [
"celery-stubs==0.1.3",
"django-stubs>=5.2.4",
"poethepoet>=0.37.0",
"pre-commit>=4.3.0",
"ruff>=0.13.3",
]
[tool.ruff]
line-length = 100
[tool.ruff.lint]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
docstring-code-format = true
# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories.
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
[tool.poe.tasks]
# Main tasks
setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" }
dev = { cmd = "python manage.py runserver", help = "Start the development server", deps = ["db-up"] }
# Database tasks
db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" }
db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" }
# Full cleanup tasks
clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" }
clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." }
# Django tasks
migrate = { cmd = "python manage.py migrate", help = "Run database migrations" }
bootstrap = { shell = """
echo "Bootstrapping initial data..."
echo "This will take a bit . Get yourself some coffee..."
python manage.py bootstrap
echo " Bootstrap complete"
echo ""
echo "Default admin credentials:"
echo " Username: admin"
echo " Email: admin@envipath.com"
echo " Password: SuperSafe"
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
shell = { cmd = "python manage.py shell", help = "Open Django shell" }

168
static/css/c3.css Normal file
View File

@ -0,0 +1,168 @@
/*-- Chart --*/
.c3 svg {
font: 10px sans-serif;
-webkit-tap-highlight-color: transparent; }
.c3 path, .c3 line {
fill: none;
stroke: #000;
}
.c3 text {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none; }
.c3-legend-item-tile,
.c3-xgrid-focus,
.c3-ygrid,
.c3-event-rect,
.c3-bars path {
shape-rendering: crispEdges; }
.c3-chart-arc path {
stroke: #fff; }
.c3-chart-arc text {
fill: #fff;
font-size: 13px; }
/*-- Axis --*/
/*-- Grid --*/
.c3-grid line {
stroke: #aaa; }
.c3-grid text {
fill: #aaa; }
.c3-xgrid, .c3-ygrid {
stroke-dasharray: 3 3; }
/*-- Text on Chart --*/
.c3-text.c3-empty {
fill: #808080;
font-size: 2em; }
/*-- Line --*/
.c3-line {
stroke-width: 1px; }
/*-- Point --*/
.c3-circle._expanded_ {
stroke-width: 1px;
stroke: white; }
.c3-selected-circle {
fill: white;
stroke-width: 2px; }
/*-- Bar --*/
.c3-bar {
stroke-width: 0; }
.c3-bar._expanded_ {
fill-opacity: 0.75; }
/*-- Focus --*/
.c3-target.c3-focused {
opacity: 1; }
.c3-target.c3-focused path.c3-line, .c3-target.c3-focused path.c3-step {
stroke-width: 2px; }
.c3-target.c3-defocused {
opacity: 0.3 !important; }
/*-- Region --*/
.c3-region {
fill: steelblue;
fill-opacity: .1; }
/*-- Brush --*/
.c3-brush .extent {
fill-opacity: .1; }
/*-- Select - Drag --*/
/*-- Legend --*/
.c3-legend-item {
font-size: 12px; }
.c3-legend-item-hidden {
opacity: 0.15; }
.c3-legend-background {
opacity: 0.75;
fill: white;
stroke: lightgray;
stroke-width: 1; }
/*-- Title --*/
.c3-title {
font: 14px sans-serif; }
/*-- Tooltip --*/
.c3-tooltip-container {
z-index: 10; }
.c3-tooltip {
border-collapse: collapse;
border-spacing: 0;
background-color: #fff;
empty-cells: show;
-webkit-box-shadow: 7px 7px 12px -9px #777777;
-moz-box-shadow: 7px 7px 12px -9px #777777;
box-shadow: 7px 7px 12px -9px #777777;
opacity: 0.9; }
.c3-tooltip tr {
border: 1px solid #CCC; }
.c3-tooltip th {
background-color: #aaa;
font-size: 14px;
padding: 2px 5px;
text-align: left;
color: #FFF; }
.c3-tooltip td {
font-size: 13px;
padding: 3px 6px;
background-color: #fff;
border-left: 1px dotted #999; }
.c3-tooltip td > span {
display: inline-block;
width: 10px;
height: 10px;
margin-right: 6px; }
.c3-tooltip td.value {
text-align: right; }
/*-- Area --*/
.c3-area {
stroke-width: 0;
opacity: 0.2; }
/*-- Arc --*/
.c3-chart-arcs-title {
dominant-baseline: middle;
font-size: 1.3em; }
.c3-chart-arcs .c3-chart-arcs-background {
fill: #e0e0e0;
stroke: none; }
.c3-chart-arcs .c3-chart-arcs-gauge-unit {
fill: #000;
font-size: 16px; }
.c3-chart-arcs .c3-chart-arcs-gauge-max {
fill: #777; }
.c3-chart-arcs .c3-chart-arcs-gauge-min {
fill: #777; }
.c3-chart-arc .c3-gauge-value {
fill: #000;
/* font-size: 28px !important;*/ }

126
static/css/epp.css Normal file
View File

@ -0,0 +1,126 @@
.mini-submenu{
display:none;
background-color: rgba(0, 0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0.9);
border-radius: 4px;
padding: 9px;
/*position: relative;*/
width: 42px;
}
.mini-submenu:hover{
cursor: pointer;
}
.mini-submenu .icon-bar {
border-radius: 1px;
display: block;
height: 2px;
width: 22px;
margin-top: 3px;
}
.mini-submenu .icon-bar {
background-color: #000;
}
#slide-submenu{
background: rgba(0, 0, 0, 0.45);
display: inline-block;
padding: 0 8px;
border-radius: 4px;
cursor: pointer;
}
#eductsdiv {
float:left;
}
#agentsdiv {
float:left;
}
#productsdiv {
float:left;
}
#actions {
float:left;
}
.modal.fade .modal-dialog {
overflow-y: auto;
max-height: 800px;
}
.modal-dialog-multiple {
position: relative;
overflow: visible;
width: 600px;
margin: 30px auto;
max-height: 800px;
}
.modal-element{
padding-top: 10px;
margin-top: 5px;
margin-bottom: 5px;
border-top: 1px solid #e5e5e5;
}
.modal-dialog-pps{
position: relative;
display: table;
margin-left: 35%;
margin-right: 35%;
overflow-y: auto;
overflow-x: auto;
width: 30%;
}
.modal-dialog-pps-big{
position: relative;
display: table;
margin-left: 25%;
margin-right: 25%;
overflow-y: auto;
overflow-x: auto;
width: 50%;
}
@media (max-width: 1220px) {
.navbar-header-framework {
float: none;
}
.navbar-left-framework,.navbar-right-framework {
float: none !important;
}
.navbar-toggle-framework {
display: block;
}
.navbar-collapse-framework {
border-top: 1px solid transparent;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.1);
}
.navbar-fixed-top-framework {
top: 0;
border-width: 0 0 1px;
}
.navbar-collapse-framework.collapse {
display: none!important;
}
.navbar-nav-framework {
float: none!important;
margin-top: 7.5px;
}
.navbar-nav-framework>li {
float: none;
}
.navbar-nav-framework>li>a {
padding-top: 10px;
padding-bottom: 10px;
}
.collapse-framework.in{
display:block !important;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

BIN
static/images/ealogo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Some files were not shown because too many files have changed in this diff Show More