Compare commits
703 Commits
Author | SHA1 | Date | |
---|---|---|---|
fa03f7471b | |||
a28a295d19 | |||
f9abd0101e | |||
053839af9a | |||
5abb4172a9 | |||
d8cfff12ad | |||
526e9e876b | |||
88163a954f | |||
63184edc88 | |||
e3ec1a4da3 | |||
77316fc682 | |||
38208f3235 | |||
04ffe056ae | |||
1372609e83 | |||
c62f472546 | |||
1b85ffca3e | |||
57cb5b4105 | |||
8597c5e474 | |||
eaabbb06c3 | |||
5bc7d585ad | |||
6c1690fccf | |||
f158bed904 | |||
9503f88155 | |||
79199e27c7 | |||
3058f7212a | |||
57d41d12a3 | |||
fe997ee2f8 | |||
cecad1c368 | |||
df2120ab85 | |||
63400c9ab6 | |||
4ea29dedb9 | |||
701608b3ed | |||
4bf54babb2 | |||
c1873e8c6d | |||
31eded4f69 | |||
94737e7ae7 | |||
13d697e7de | |||
37aa9ff968 | |||
1663e98a1a | |||
f765fe1fa0 | |||
1561b3a2e6 | |||
4174bacea1 | |||
553f4d8579 | |||
72314ea2a4 | |||
2c2ed724c3 | |||
0275c7a211 | |||
b2f1fba86a | |||
61207c55f9 | |||
cabb8d8c4e | |||
ef1f764b4b | |||
19ccc5af9b | |||
90dd1a3ded | |||
bc8af15d8b | |||
3884253a2b | |||
a9698eada1 | |||
e91ba06256 | |||
c506925f6e | |||
b007e05386 | |||
cadcb8b5d2 | |||
cc6d577978 | |||
7f2eaaca6a | |||
9b0d09e001 | |||
77226f75fc | |||
aeff3f610c | |||
635159f364 | |||
c0e8769781 | |||
04491fbc34 | |||
89e6bc362c | |||
79511d0270 | |||
69bb6f149f | |||
381ceb723d | |||
67e010e001 | |||
1a1317b584 | |||
2a3051c2ae | |||
dcc86a824a | |||
44a6d1da2d | |||
bd06be083b | |||
2a14cd6119 | |||
334dbfdf7f | |||
0c961d2709 | |||
99900a2e90 | |||
ede4009933 | |||
1323531254 | |||
e9022edb1d | |||
749fe57d4f | |||
5e9f2a4763 | |||
edb5148b7d | |||
3d98f54b3c | |||
126516a1ff | |||
7169bb1618 | |||
9cd72e604e | |||
135fba1f50 | |||
80fa12b747 | |||
77786d28e7 | |||
2775e0a01b | |||
dd312c6351 | |||
91c964b581 | |||
db5ce2352e | |||
5c3c26c318 | |||
f77d778cb7 | |||
9731cb0eaa | |||
e31bc90869 | |||
a5156bd476 | |||
675f46ce66 | |||
ccb85b16ae | |||
9d7750ce44 | |||
d10faa4b3c | |||
7028cbf02b | |||
3a2d66a213 | |||
9f03b52f58 | |||
38ff4b9ee7 | |||
a70a2019b4 | |||
edef6aa2bc | |||
4d02276f55 | |||
2a2ce8e2e0 | |||
5b0f1b9b82 | |||
58cca3e954 | |||
1ddfd852d4 | |||
22eec00f40 | |||
23fe7e189e | |||
f4edbe80d5 | |||
076b35dedc | |||
4432591168 | |||
bb8a3ba716 | |||
af71907264 | |||
53d2da43ef | |||
c226a0a910 | |||
4d70925a3d | |||
6c61ff0121 | |||
48966b9009 | |||
9a428c4e98 | |||
1ef90e96e0 | |||
74f652aaf4 | |||
c5999c2e04 | |||
46e7f3fada | |||
afc1c6b6fc | |||
1b5044cc42 | |||
413ddbc59b | |||
7e64e23d32 | |||
3aacc17f54 | |||
1485183aab | |||
14e81e0bdb | |||
a66c441f5a | |||
1cf844d22d | |||
aaba1c5ad6 | |||
2b1f8a6e05 | |||
6cd5698f6e | |||
eb137c7ae3 | |||
9ca5cdb88a | |||
0ec202d633 | |||
18ba0704ec | |||
70d24b1094 | |||
a9419fd742 | |||
e17a0ac5e8 | |||
09bff87030 | |||
1b1f9840e3 | |||
8802d396c5 | |||
87f73ac5b0 | |||
36de9817b5 | |||
a3a33085c8 | |||
1c601f5ce1 | |||
6b10795b2d | |||
cdf3068b50 | |||
73d5ac6b10 | |||
18cbd71d28 | |||
cf52db1434 | |||
403b5226cb | |||
fcdb2fe057 | |||
e04e3e4572 | |||
4c14e6db3f | |||
e254ec6e78 | |||
f815df41c4 | |||
13425c432b | |||
d273c35baa | |||
c587ae7102 | |||
cafaf3178a | |||
947a9c88fd | |||
ec06c417e1 | |||
f954b2507c | |||
1173029981 | |||
fa0a81e835 | |||
e8e3939de7 | |||
2d7e621251 | |||
9132a8f62c | |||
cda3730943 | |||
70db017ac2 | |||
8055bd79dd | |||
a73f5feccc | |||
08a3042866 | |||
86116f4563 | |||
92fd22ae5c | |||
6ccb63c921 | |||
4e5d2378d8 | |||
290a7de736 | |||
8673862cd3 | |||
1de5c634d0 | |||
f5d59c703d | |||
0ee8369e24 | |||
a67c3fb919 | |||
4dd19ba22f | |||
d55ecabb2d | |||
a0ad877830 | |||
9f192ef28d | |||
edf0fca794 | |||
3f4c8e93fc | |||
1c7b63c4d0 | |||
aa43c2e46a | |||
4d9e06c55b | |||
2027c4fd0e | |||
d708f85883 | |||
1d6ec4023a | |||
2059d014d7 | |||
9493543b92 | |||
589c7fab26 | |||
740d33ece1 | |||
0a364f0f7f | |||
f8a9e7dcad | |||
b77bb3d10c | |||
d21fc512be | |||
6852aa4748 | |||
723bc7aed4 | |||
358c1720d5 | |||
9b8f644261 | |||
57060975b0 | |||
24e4c95928 | |||
8f15b8bf20 | |||
0d6bdad29b | |||
4b2be4855b | |||
b738c4be20 | |||
8512bff4f1 | |||
507db1051c | |||
7cd4cb965b | |||
460bf40175 | |||
93e7a9097f | |||
1541a89937 | |||
25373fc5b6 | |||
a9a5c189d2 | |||
f2a995d277 | |||
75b5f48f9e | |||
bb021a00d9 | |||
db07262623 | |||
6f94c95221 | |||
103f25be1d | |||
cc02976c3a | |||
af4a1d52c5 | |||
7ac56a35ae | |||
030560d697 | |||
b7dd2dfaba | |||
008a879e6f | |||
71382a4689 | |||
1963bf0817 | |||
31a45a1ab1 | |||
f7818fe2e8 | |||
16388f0a10 | |||
2e0b835af9 | |||
81e856967b | |||
348b2f270e | |||
9a4521a604 | |||
83dab570a8 | |||
a189a60e1b | |||
91bea05496 | |||
ed41e0b8a9 | |||
64ca3415d4 | |||
ec6bc7d03a | |||
0d1a0bff19 | |||
cc46a1426d | |||
30b24a1969 | |||
ad27501334 | |||
1109beb8e9 | |||
8b7550f573 | |||
4f2a28bb52 | |||
33c589a1a0 | |||
0a21c37645 | |||
e4dac45d2e | |||
c9eefc2eb3 | |||
68d3c1ff26 | |||
e5680cdbce | |||
067dd98eee | |||
b153f648f9 | |||
062e846875 | |||
0e1f778b80 | |||
aca32b14be | |||
16076f0ca3 | |||
6d69b72368 | |||
d4c6c08710 | |||
05d4c4a94d | |||
4d5f029913 | |||
51e7f875a2 | |||
b17bc5af75 | |||
80db9022b7 | |||
288abb5373 | |||
f06c9f6091 | |||
d55010d63f | |||
eeb7075f83 | |||
1c34fbad08 | |||
4549379e62 | |||
64fd692d9c | |||
795f029c75 | |||
a091be7b5e | |||
a1c9533555 | |||
20f52d856b | |||
6e296a3713 | |||
58b73b9b29 | |||
cd65d8e93d | |||
f26b0706ae | |||
1a0839a2c8 | |||
cc0f92e132 | |||
b14262fcad | |||
1474f47087 | |||
e56007d4b1 | |||
0602070adb | |||
760463705b | |||
8eeb5780c6 | |||
7b6d6935d4 | |||
1cec44f7bd | |||
51ec6cb0e5 | |||
9b62ba6367 | |||
96420f3d23 | |||
eb38e70c17 | |||
dbafbe1a5c | |||
90bee291bd | |||
5eb120886d | |||
6e4e63a52e | |||
8d39fdb9e4 | |||
7aa7627faf | |||
29fc7e7e07 | |||
780ab35b26 | |||
b072ce9fbc | |||
fa757f2edf | |||
fccaacbf7c | |||
0f1ff924ed | |||
75598dff94 | |||
475205f95b | |||
bf74a4c4db | |||
c15966c694 | |||
56ced08c4d | |||
c8d6135b83 | |||
98159c1840 | |||
8140c1f945 | |||
eff1255419 | |||
8f2bf3d71e | |||
1a7bade7f3 | |||
a7912531e3 | |||
32c6991730 | |||
98c02c4101 | |||
d8b12aeb7e | |||
5b02f2c751 | |||
0d4ad05ac9 | |||
def43c1134 | |||
3c08580c10 | |||
bc329d2453 | |||
72dfac9d22 | |||
a74550e1f3 | |||
382b1d2a30 | |||
9c2ef50c3b | |||
3536a59e5a | |||
0213d45f8a | |||
41cf9aae41 | |||
0b25a85e26 | |||
4caf26686c | |||
58c19388c7 | |||
6a211677a8 | |||
45d05a183d | |||
f144129b4d | |||
e038d08544 | |||
05c9e3f066 | |||
a1851918e3 | |||
136f907cd5 | |||
969aaea884 | |||
5a07892b25 | |||
5d17e1a49c | |||
185fb6c332 | |||
e690012da8 | |||
76b6a57583 | |||
7bea5a6156 | |||
45b2f083de | |||
643894cdfc | |||
443cbce73b | |||
9a5f4e90ff | |||
9c7e572e00 | |||
876a1622dd | |||
161751f97f | |||
ac0e756c77 | |||
115159b8a6 | |||
10e2550621 | |||
edb6aa6eb7 | |||
2534557105 | |||
2e6e73353e | |||
37d6ba202d | |||
f3a4123fb6 | |||
b20abd82e1 | |||
d11e7fa71d | |||
96a8a6a158 | |||
c0695c8c84 | |||
64c2f1b0d9 | |||
37a8368a4e | |||
ba9c811e16 | |||
ae47b06a72 | |||
bc8815d607 | |||
8a06a0dd3a | |||
46381fc138 | |||
be2e0ae837 | |||
cc34e43262 | |||
a3d0b10a75 | |||
eafc364ba5 | |||
0304010d0e | |||
5d3eadfd56 | |||
fe7e9169be | |||
9d109d2f9a | |||
f9b0b694f9 | |||
d19e742cc5 | |||
a077506eb0 | |||
585f1c01e8 | |||
f84759720c | |||
91c5216120 | |||
98868aab32 | |||
0a8c3cfffb | |||
bff5ea3fe8 | |||
e7729afc98 | |||
7358b0a90c | |||
e67ede0e74 | |||
1e914d1345 | |||
ba7c0cd117 | |||
029e5a6d08 | |||
f71fe3d6d7 | |||
c97cdf276f | |||
c2f7e25579 | |||
9e7f731a0e | |||
1f366ab8fd | |||
e59e808b7f | |||
39f48aadb1 | |||
e121741a6c | |||
0145ee9349 | |||
7a0be93752 | |||
89eecad448 | |||
a5cbf108cb | |||
67e892dbaa | |||
8477700125 | |||
59603e0455 | |||
0bc3f420f0 | |||
960646a68c | |||
d1dbe965f9 | |||
9fa6ec46dd | |||
d03afa3017 | |||
f157545b01 | |||
e18f70a20c | |||
e8ce851c31 | |||
4dd4215fd0 | |||
dad843f1d4 | |||
47b1dd7732 | |||
a599cc9f9c | |||
893066942d | |||
858538e3eb | |||
b42a5a9abb | |||
9ea8422007 | |||
39fdc9ef1d | |||
a161677f79 | |||
013c31272b | |||
911a086963 | |||
c1a47045e7 | |||
644188f952 | |||
717bc5750b | |||
3ca1aa3756 | |||
afd20ac4e3 | |||
79c96ff2b5 | |||
b135b0690e | |||
041b837068 | |||
ea8a56322f | |||
0b418b6f2c | |||
a2abfc83ee | |||
03ce6824ee | |||
c12f9925ae | |||
3efa835295 | |||
d8fdb17112 | |||
5842e81455 | |||
f057693a61 | |||
c77d33e910 | |||
ecb7116f4a | |||
3464fcf93b | |||
c8f0c54549 | |||
4367878396 | |||
c950daa011 | |||
725b009ee6 | |||
9fcb793bb2 | |||
bd574ce952 | |||
77f0554893 | |||
e07af389ba | |||
3060fd0b86 | |||
3184e5fb07 | |||
02140ce731 | |||
dd06d9e1ff | |||
ca4e9add88 | |||
657bbfb25a | |||
64e344206c | |||
a132baa838 | |||
f833b93074 | |||
06991853ff | |||
cff343989b | |||
ff8d422308 | |||
aded110307 | |||
fbc6a3c338 | |||
7629c6cf9b | |||
96b2eec050 | |||
30394644bf | |||
610df69e21 | |||
0b0b527b55 | |||
724e7e6fb0 | |||
30160fb033 | |||
317a66b714 | |||
aa9c0c7552 | |||
22f46160be | |||
745e1f6a66 | |||
d8fae15995 | |||
5d8332015b | |||
91e83b0ad9 | |||
f29a8ebc23 | |||
68a2ca73cc | |||
121ee7b861 | |||
af0b05cdd5 | |||
73c3f0ac34 | |||
0db2c09be4 | |||
21a85555a7 | |||
cf35677464 | |||
c508cfc8a0 | |||
2e321d22bc | |||
16199cbd30 | |||
29443a51fc | |||
bac22d6a9d | |||
f86038c169 | |||
38314ac977 | |||
83fb6d005b | |||
c755feb9a7 | |||
6f0a67a957 | |||
66b6ba9ae9 | |||
f1e1e76266 | |||
22fcd42689 | |||
efc95ef736 | |||
184f775af4 | |||
4eec08ffda | |||
9c2b0cbe66 | |||
84499bf5d1 | |||
0338e5c8de | |||
b9c4228edc | |||
c907efbadb | |||
c54c54d821 | |||
d608a3d138 | |||
18ddd92509 | |||
02afaf29ff | |||
bb99b89a38 | |||
b988fe3084 | |||
697ebbb96b | |||
241f18a055 | |||
a32713f6b7 | |||
f910503f3c | |||
9cb142fa4b | |||
991d30e1fd | |||
504437f8cc | |||
33fa7fd61c | |||
16a827c73b | |||
805835721e | |||
82bd63b80d | |||
caa6a6df99 | |||
34f7dd834d | |||
3e6206aaad | |||
6e789e6966 | |||
d1364c6696 | |||
1e71d0d39b | |||
b8b534e499 | |||
245962668f | |||
d5dd58f78e | |||
fcb212e66e | |||
038a3404d7 | |||
b6e0a90848 | |||
29dd03557c | |||
fe73472aa4 | |||
6bab430466 | |||
776449ba10 | |||
9ba2fd6779 | |||
02595ca010 | |||
59c541486d | |||
9b1d3384cd | |||
fe27218256 | |||
64c9ed7978 | |||
919bfd9d98 | |||
1ba41d94fb | |||
0ed80217f2 | |||
346fc57ba7 | |||
ba443c4e64 | |||
c753b799ee | |||
70baac6fb2 | |||
1ea5937881 | |||
33726b9114 | |||
a89d13c2b1 | |||
0d32fd3528 | |||
3dce2dad05 | |||
e17164d336 | |||
c36e9a5293 | |||
dcf8136191 | |||
10e63b3135 | |||
2baee0e4d0 | |||
ae94285dc7 | |||
c3a0c563a6 | |||
d0590b8515 | |||
f0c9c0cdc6 | |||
b57d68082b | |||
af494acd7f | |||
e205b0a7a6 | |||
c7fc8055f6 | |||
76cc4e1e8a | |||
4bbff26bd2 | |||
2f9b65a326 | |||
27b7550114 | |||
06cefb9223 | |||
67a5e63b33 | |||
4dae20b056 | |||
4adb38fdd2 | |||
ab7c840c60 | |||
997d2df4b7 | |||
4160d974f5 | |||
baf7d0226e | |||
36992f8469 | |||
325509aa16 | |||
57cc0d2caa | |||
4833245e34 | |||
d57dfb809b | |||
98d47c69d2 | |||
ded0cc925c | |||
857bcc6652 | |||
f3be33431c | |||
8bd3a8fb45 | |||
67db68c5ea | |||
3baae2a6cd | |||
4d3a8e768b | |||
169b8dbf4d | |||
8d7d5a127f | |||
ad4d030a40 | |||
ce0cc856a7 | |||
fae11ca310 | |||
95af0d3848 | |||
596c708212 | |||
2f9b6c6027 | |||
84ac2d3562 | |||
f9837e0ec4 | |||
73a63f5af4 | |||
5066cf7536 | |||
6522d274a3 | |||
759e774928 | |||
56aab474e9 | |||
1bd1d546fc | |||
fe3688791f | |||
a00db4e7f6 | |||
f80ed4d366 | |||
cd47e643d5 | |||
db12750dc0 | |||
59a9a1ca75 | |||
942104ebed | |||
df7b40ba03 | |||
1488c6e96a | |||
b419c03774 | |||
1ad370e7cf | |||
b7cdb21136 | |||
78afe7c86b | |||
5507c4a366 | |||
a4788aba75 | |||
b6593c6825 | |||
f9393a59d2 | |||
22b02dc044 | |||
e5b0ed4c69 | |||
96565aa2c5 | |||
17a29405c5 | |||
c5eb26e62b | |||
49088f1f68 | |||
9140a886f2 | |||
894ed14842 | |||
d7a8840444 | |||
624f892a73 | |||
26e475a42f | |||
de2722f6ca | |||
32913895c9 | |||
744d9a2ec4 | |||
98bc0fc1d7 | |||
f7543ced8d | |||
7190354d6a | |||
467fc18bd1 | |||
7572763129 | |||
76e7d64341 | |||
120380a95c | |||
4c5c460d88 | |||
2865c3d32a | |||
efd205c244 | |||
c91857850b | |||
a58cb1ee95 | |||
995a102da1 | |||
40a15ed4f0 | |||
52e852c647 | |||
9a35d023d9 | |||
cbecdae5d1 | |||
bcc71ee866 | |||
824244f217 | |||
06e0943e30 | |||
b8cac94f9a | |||
a1207bfb0f | |||
b0127584d6 |
2
.cz.yaml
2
.cz.yaml
@ -17,5 +17,5 @@ commitizen:
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 1.8.0
|
||||
version: 1.12.0
|
||||
version_scheme: semver
|
||||
|
81
.github/ISSUE_TEMPLATE/new_model.md
vendored
Normal file
81
.github/ISSUE_TEMPLATE/new_model.md
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
---
|
||||
name: New Database Model
|
||||
about: Use when creating a new database model.
|
||||
title: "New Model - <model table name>"
|
||||
type: Task
|
||||
labels: task::feature, triage, type::task
|
||||
---
|
||||
|
||||
<!-- Add an intro -->
|
||||
|
||||
|
||||
<!-- describe a use case if not covered in intro -->
|
||||
|
||||
|
||||
## 📝 Details
|
||||
<!--
|
||||
|
||||
Describe in detail the following:
|
||||
|
||||
- New model field
|
||||
- if foreign key field, what it's name will be or if it's not to be linked ensure specified and coded with `related_name = '+' to disable the link`.
|
||||
- How the UI will work, be layed out, new ui features etc
|
||||
- custom permissions if required
|
||||
|
||||
-->
|
||||
|
||||
|
||||
### 🚧 Tasks
|
||||
|
||||
<!-- Don't remove tasks strike them out. use `~~` before and after the item. i.e. `- ~~[ ] Model Created~~` note: don't include the list dash-->
|
||||
|
||||
- [ ] 🆕 Model Created
|
||||
|
||||
- [ ] 🛠️ Migrations added
|
||||
|
||||
- [ ] 🏷️ Model tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()` function
|
||||
|
||||
- [ ] Tag updated in the [docs](https://nofusscomputing.com/projects/centurion_erp/user/core/markdown/#model-reference)
|
||||
|
||||
>[!note]
|
||||
> Ensure that when creating the tag the following is adhered to:
|
||||
> - Two words are not to contain a space char, `\s`. It is to be replaced with an underscore `_`
|
||||
> - As much as practical, keep the tag as close to the model name as possible
|
||||
|
||||
- [ ] 📝 New [History model](https://nofusscomputing.com/projects/centurion_erp/development/core/model_history/) created
|
||||
|
||||
- [ ] 📓 New [Notes model](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/) created
|
||||
|
||||
|
||||
- [ ] Admin Documentation added/updated _if applicable_
|
||||
- [ ] Developer Documentation added/updated _if applicable_
|
||||
- [ ] User Documentation added/updated
|
||||
|
||||
---
|
||||
|
||||
<!-- Add additional tasks here and as a check box list -->
|
||||
|
||||
|
||||
|
||||
#### 🧪 Tests
|
||||
|
||||
- [ ] Unit Test Model
|
||||
- [ ] Unit Test Serializer
|
||||
- [ ] Unit Test Tenancy Object
|
||||
- [ ] Unit Test ViewSet
|
||||
- [ ] Function Test ViewSet
|
||||
- [ ] Function Test API Metadata
|
||||
- [ ] Function Test API Permissions
|
||||
- [ ] Function Test API Render (fields)
|
||||
- [ ] Function Test History Entries
|
||||
|
||||
|
||||
### ✅ Requirements
|
||||
|
||||
A Requirement is a must have. In addition will also be tested.
|
||||
|
||||
- [ ] Must have a [model_tag](https://nofusscomputing.com/projects/centurion_erp/user/core/markdown/#model-reference)
|
||||
|
||||
---
|
||||
|
||||
<!-- Add additional requirement here and as a check box list -->
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -19,4 +19,6 @@
|
||||
],
|
||||
"cSpell.language": "en-AU",
|
||||
"jest.enable": false,
|
||||
"pylint.enabled": true,
|
||||
"pylint.importStrategy": "fromEnvironment",
|
||||
}
|
712
CHANGELOG.md
712
CHANGELOG.md
@ -1,3 +1,715 @@
|
||||
## 1.12.0 (2025-03-01)
|
||||
|
||||
### feat
|
||||
|
||||
- **api**: Add delete column to AuthToken Table
|
||||
- **docker**: Upgrade system packages on build
|
||||
- **api**: AuthToken requires viewset get_back_url
|
||||
- **api**: Add auth token api endpoint
|
||||
- **settings**: Add section title to auth tokens
|
||||
- **settings**: Add tokens url to user settings `_urls`
|
||||
- **api**: Update Auth Token model for use with serializer
|
||||
- **api**: Add user Auth Token viewset
|
||||
- **api**: Add user Auth Token serializer
|
||||
- **settings**: Add `page_layout` attribute to User Settings
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: correct usage of `AuthToken.generate` to a property
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: AuthToken ViewSet checks (unit)
|
||||
- **api**: AuthToken API Field checks
|
||||
- **api**: AuthToken Serializer checks
|
||||
- **api**: AuthToken ViewSet checks
|
||||
|
||||
## 1.11.0 (2025-02-21)
|
||||
|
||||
### feat
|
||||
|
||||
- **core**: Enable App settings History to save without specifying an organization
|
||||
- **settings**: save_history method added to App Settings
|
||||
- **settings**: History Model for App Settings Version added
|
||||
- **core**: Migration for history data to new history tables
|
||||
- **access**: save_history method added to Team
|
||||
- **access**: History Model for Team added
|
||||
- **access**: save_history method added to Organization
|
||||
- **access**: History Model for Organization added
|
||||
- **core**: add org field History Model api rendering
|
||||
- **core**: Show the model name within history
|
||||
- **project_management**: Project Milestone added to modelhistory.child_history_models
|
||||
- **settings**: History Model migrations for External Link
|
||||
- **settings**: save_history method added to External Link
|
||||
- **settings**: History Model for External Link added
|
||||
- **project_management**: History Model migrations for Project Type
|
||||
- **project_management**: save_history method added to Project Type
|
||||
- **project_management**: History Model for Project TYpe added
|
||||
- **project_management**: History Model migrations for Project State
|
||||
- **project_management**: save_history method added to Project State
|
||||
- **project_management**: History Model for Project State added
|
||||
- **project_management**: History Model migrations for Project Milestone
|
||||
- **project_management**: save_history method added to Project Milestonr
|
||||
- **project_management**: History Model for Project Milestone added
|
||||
- **project_management**: History Model migrations for Project
|
||||
- **project_management**: save_history method added to Project
|
||||
- **project_management**: History Model for Project added
|
||||
- **itim**: History Model migrations for Service
|
||||
- **itim**: save_history method added to Service
|
||||
- **itim**: History Model for Service added
|
||||
- **itim**: History Model migrations for Port
|
||||
- **itim**: save_history method added to Port
|
||||
- **itim**: History Model for Port added
|
||||
- **itim**: History Model migrations for Cluster Type
|
||||
- **itim**: save_history method added to Cluster TYpe
|
||||
- **itim**: History Model for Cluster Type added
|
||||
- **itim**: History Model migrations for Cluster
|
||||
- **itim**: save_history method added to Cluster
|
||||
- **itim**: History Model for Cluster added
|
||||
- **itam**: History Model migrations for Software Version
|
||||
- **itam**: save_history method added to Software Version
|
||||
- **itam**: History Model for Software Version added
|
||||
- **itam**: History Model migrations for Software Category
|
||||
- **itam**: save_history method added to Software Category
|
||||
- **itam**: History Model for Software Category added
|
||||
- **itam**: History Model migrations for Software
|
||||
- **itam**: save_history method added to Software
|
||||
- **itam**: History Model for Software added
|
||||
- **itam**: History Model migrations for Operating System Version
|
||||
- **itam**: save_history method added to Operating System Version
|
||||
- **itam**: History Model for Operating System Version added
|
||||
- **itam**: History Model migrations for Device Type
|
||||
- **itam**: save_history method added to Device Type
|
||||
- **itam**: History Model for Device Type added
|
||||
- **itam**: History Model migrations for Device Operating System
|
||||
- **itam**: save_history method added to Device Operating System
|
||||
- **itam**: History Model for Device Operating System added
|
||||
- **itam**: History Model migrations for Operating System
|
||||
- **itam**: save_history method added to Operating System
|
||||
- **itam**: History Model migrations for Operating System
|
||||
- **itam**: History Model migrations for Device Software
|
||||
- **itam**: History Model for Device Software added
|
||||
- **itam**: save_history method added to Device
|
||||
- **itam**: History Model migrations for Device Model
|
||||
- **itam**: save_history method added to Device Model
|
||||
- **itam**: History Model for Device Model added
|
||||
- **core**: History Model migrations for Ticket Comment Category
|
||||
- **core**: save_history method added to Ticket Comment Category
|
||||
- **core**: History Model for Ticket Comment Category added
|
||||
- **config_management**: Child History Models added to child model lists for config group hosts and software
|
||||
- **core**: History Model migrations for Ticket Category
|
||||
- **core**: save_history method added to Ticket Category
|
||||
- **core**: History Model for Ticket Category added
|
||||
- **core**: History Model migrations for Manufacturer
|
||||
- **core**: save_history method added to Manufacturer
|
||||
- **core**: History Model for Manufacturer added
|
||||
- **config_management**: save_history method added to Config Group Software
|
||||
- **config_management**: save_history method added to Config Group Hosts
|
||||
- **config_management**: save_history method added to Config Groups
|
||||
- **assistance**: save_history method added to Knowledge base
|
||||
- **assistance**: save_history method added to Knowledge base category
|
||||
- **config_management**: History Model migrations for Config Groupse + children
|
||||
- **config_management**: History Model for Config Group Software added
|
||||
- **config_management**: History Model for Config Group Hosts added
|
||||
- **config_management**: History Model for Config Groups added
|
||||
- **assistance**: History Model migrations for Knowledge base + children
|
||||
- **assistance**: History Model for Knowledge base category added
|
||||
- **assistance**: History Model for Knowledge base added
|
||||
- **itam**: Add device history model
|
||||
- **core**: History view to only display objects from the model being requested
|
||||
- **core**: Add new history model to History Serializer
|
||||
- **core**: Add new history model
|
||||
- **development**: lint for un-used imports
|
||||
- **development**: add pylit settings
|
||||
- **core**: added new history model
|
||||
- **api**: Device Software Viewset requires its own function to obtain the model view serializer
|
||||
- **api**: Ticket Comment Viewset requires its own function to obtain the model view serializer
|
||||
- **api**: Ticket Viewset requires its own function to obtain the model view serializer
|
||||
- **api**: Always use a models `View` serializer for the response
|
||||
- **core**: Add logic to ensure when organization changes, an action comment is created
|
||||
- **core**: Add logic to ensure when parnet ticket changes, an action comment is created
|
||||
|
||||
### Fixes
|
||||
|
||||
- **settings**: App settings serializer fielad name does not exist
|
||||
- **access**: dont use organization property within organization model
|
||||
- **project_management**: Project milestone is not a child model
|
||||
- **core**: Child models on delete must make model field null
|
||||
- **project_management**: Project Milestone History is a primaryu model
|
||||
- **core**: When a child model is deleted ensure entry is still created on parent model history
|
||||
- **core**: when fetching url_kwargs for model history, make it dynamic for related field name
|
||||
- **core**: Xorrect logic for determining view_action
|
||||
- **core**: dynamically search for history object name
|
||||
- **config_management**: Remove parent property from config groups
|
||||
- **tests**: Correct Permission Import due to removing from access.models
|
||||
- **project_management**: project Model serializer must inherit common serializer
|
||||
- **core**: History audit objects must be a valid dict
|
||||
- **api**: history app names can contain an underscore
|
||||
- **core**: when saving history, use audit_model for content type
|
||||
- **core**: add missing functions for fetching item url
|
||||
- **project_management**: Opened by field set to read only for project task ticket
|
||||
- **itim**: Opened by field set to read only for problem ticket
|
||||
- **itim**: Opened by field set to read only for incident ticket
|
||||
- **itim**: Opened by field set to read only for change ticket
|
||||
- **assistance**: Opened by field set to read only for request ticket
|
||||
- **core**: Ensure that if the parent ticket changes, that the logic caters for none
|
||||
- **assistance**: Category can be empty for Project Task Ticket
|
||||
- **assistance**: Category can be empty for Problem Ticket
|
||||
- **assistance**: Category can be empty for Incident Ticket
|
||||
- **assistance**: Category can be empty for Change Ticket
|
||||
- **assistance**: Category can be empty for Request Ticket
|
||||
- **core**: Ticket Action comment for category change must use category field
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **core**: Update access imports to new path
|
||||
- **core**: Update access imports to new path
|
||||
- Update migrations imports to new path
|
||||
- **config_management**: Update access imports to new path
|
||||
- **api**: Update access imports to neew path
|
||||
- **settings**: Update access imports to new path
|
||||
- **project_management**: Update access imports to new path
|
||||
- **itim**: Update access imports to new path
|
||||
- **itam**: Update access imports to new path
|
||||
- **core**: Update access imports to new path
|
||||
- **config_management**: Update access imports to new path
|
||||
- **assistance**: Update access imports to new path
|
||||
- **base**: Update access imports to new path
|
||||
- **api**: Update access imports to neew path
|
||||
- **access**: Update access imports to neew path
|
||||
- **access**: Move models to their own file
|
||||
- **core**: move get_url to common serializer
|
||||
- **api**: Update history url kwargs to use vals from model._meta
|
||||
- **core**: superuser changed from import to triage access
|
||||
- **core**: Ticket action comment logic only requires a single check
|
||||
|
||||
### Tests
|
||||
|
||||
- **settings**: History Entry checks for App Settings History
|
||||
- **settings**: API Field Checks for App Settings History
|
||||
- Model History not to save history on self
|
||||
- **core**: Correct lookup for model history test setup
|
||||
- **access**: remove test cases for Team prarent_object
|
||||
- **access**: History Entry checks for Team model
|
||||
- **access**: API Field Checks for Team History
|
||||
- **access**: History Entry checks for Organization model
|
||||
- **access**: API Field Checks for Organization History
|
||||
- Fix History API checks for kb
|
||||
- Fix History API checks for project milestone
|
||||
- Fix History Entry checks for models
|
||||
- **config_management**: History Entry checks for Config_group_hosts model
|
||||
- **settings**: History Entry checks for External Link model
|
||||
- **project_management**: History Entry checks for Project Type model
|
||||
- **project_management**: History Entry checks for Project State model
|
||||
- **project_management**: History Entry checks for Project Milestone model
|
||||
- **project_management**: History Entry checks for Project model
|
||||
- **itim**: History Entry checks for Service model
|
||||
- **itim**: History Entry checks for Cluster Type model
|
||||
- **itim**: History Entry checks for Port model
|
||||
- **itim**: History Entry checks for Cluster model
|
||||
- **itam**: History Entry checks for Software Version model
|
||||
- **itam**: History Entry checks for Software Category model
|
||||
- **itam**: History Entry checks for Software model
|
||||
- **itam**: History Entry checks for Operating System Version model
|
||||
- **itam**: History Entry checks for Operating System model
|
||||
- **itam**: History Entry checks for Device Type model
|
||||
- **itam**: History Entry checks for Device OS model
|
||||
- **itam**: History Entry checks for Device Model model
|
||||
- **itam**: History Entry checks for Device model
|
||||
- **core**: History Entry checks for Ticket Comment Category model
|
||||
- **core**: History Entry checks forTicket Category model
|
||||
- **config_management**: History Entry checks for Config Groups Software model
|
||||
- **config_management**: History Entry checks for Config Groups model
|
||||
- **assistance**: History Entry checks for Knowledge base category model
|
||||
- **assistance**: History Entry checks for Knowledge base model
|
||||
- **itam**: Device Software History Entry checks
|
||||
- **core**: Manufacturer History Entry checks
|
||||
- **core**: Model History Entries Test Suite
|
||||
- **core**: History Model Unit test cases for model and tenancy checks
|
||||
- **settings**: API Field Checks for External Links History
|
||||
- **project_management**: API Field Checks for Project Type History
|
||||
- **project_management**: API Field Checks for Project State History
|
||||
- **project_management**: API Field Checks for Project Milestone History
|
||||
- **project_management**: API Field Checks for Project History
|
||||
- **itim**: API Field Checks for Service History
|
||||
- **itim**: API Field Checks for Port History
|
||||
- **itim**: API Field Checks for Cluster Type History
|
||||
- **itim**: API Field Checks for Cluster History
|
||||
- **core**: API Field Checks for Ticket Comment Category History
|
||||
- **core**: API Field Checks for Ticket Category History
|
||||
- **config_management**: API Field Checks for Config Group Software History
|
||||
- **config_management**: API Field Checks for Config Group Hosts History
|
||||
- **config_management**: API Field Checks for Config Group History
|
||||
- **assistance**: API Field Checks for Knowledge base category History
|
||||
- **assistance**: API Field Checks for Knowledge base History
|
||||
- **itam**: API Field Checks for Software Version History
|
||||
- **itam**: API Field Checks for Software Category History
|
||||
- **itam**: API Field Checks for Software History
|
||||
- **itam**: API Field Checks for Operating System Version History
|
||||
- **itam**: API Field Checks for Operating System History
|
||||
- **itam**: API Field Checks for Device Type History
|
||||
- **itam**: API Field Checks for Device OS History
|
||||
- **itam**: API Field Checks for Device Model History
|
||||
- **itam**: API Field Checks for Device History
|
||||
- **core**: Unit Test Suite for History Model API field checks urls can either be str or hyperlink
|
||||
- **itam**: API Field Checks for Device Software History
|
||||
- **core**: API Field Checks for Manufacturer History
|
||||
- **core**: API Field Checks for Model History
|
||||
- **core**: Unit Test Suite for History Model API field checks
|
||||
- **core**: Functional Test for History Model APIPermission updated to cater for tenancy obj
|
||||
- **core**: Functional Test for History Model API Permissions and Metadata
|
||||
- **core**: Unit Test for History Model Viewset
|
||||
- **itam**: remove test cases for os version model.parent_object as it's not required
|
||||
- **core**: disable hisotry viewset function test
|
||||
- **core**: correct kwargs for history tests
|
||||
- **core**: Remove old history model viewset tests
|
||||
- Disable Old History Model test suites
|
||||
- **core**: Ensure that when parent_ticket changes on a ticket an action comment is created
|
||||
- **core**: Confirm on category change to ticket that an action comment is created
|
||||
|
||||
## 1.10.1 (2025-02-14)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **python**: Dont use system TimeZone data, use python zoneinfo module zone data
|
||||
|
||||
## 1.10.0 (2025-02-10)
|
||||
|
||||
### feat
|
||||
|
||||
- **settings**: Provide user with the ability to set browser mode
|
||||
- **core**: Parent Ticket validation added to ticket serializer
|
||||
- **core**: Add to ticket endpoint the ability to filter using `parent_ticket`
|
||||
- **core**: Add to ticket model a function for circular dependecy check of parent ticket
|
||||
- **core**: Migrate Notes data to new table
|
||||
- **project_management**: Add notes tab to Project Milestone details page
|
||||
- **itam**: Add notes tab to Software Version details page
|
||||
- **itam**: Add notes tab to Operating System details page
|
||||
- **core**: Ensure when editing a model note, the modified user is updated.
|
||||
- **assistance**: Knowledge Base Category Notes viewset
|
||||
- **assistance**: Knowledge Base Category Notes Serializer
|
||||
- **assistance**: Knowledge Base Category Notes Model
|
||||
- **project_management**: Project Type Notes ViewSet
|
||||
- **project_management**: Project Type Notes Serializer
|
||||
- **project_management**: Project Type Notes Model
|
||||
- **project_management**: Project State Notes ViewSet
|
||||
- **project_management**: Project State Notes Serializer
|
||||
- **project_management**: Project State Notes Model
|
||||
- **project_management**: Project Milestone Notes ViewSet
|
||||
- **project_management**: Project Milestone Notes Serializer
|
||||
- **project_management**: Project Milestone Notes Model
|
||||
- **itam**: Software Version Notes ViewSet
|
||||
- **itam**: Software Version Notes Serializer
|
||||
- **itam**: Software Version Notes Model
|
||||
- **itam**: Software Category Notes ViewSet
|
||||
- **itam**: Software Category Notes Serializer
|
||||
- **itam**: Software Category Notes Model
|
||||
- **itam**: Operating System Version Notes ViewSet
|
||||
- **itam**: Operating System Version Notes Serializer
|
||||
- **itam**: Operating System Version Notes Model
|
||||
- **settings**: External Link Notes ViewSet
|
||||
- **settings**: External Link Notes Serializer
|
||||
- **settings**: External Link Notes Model
|
||||
- **itam**: Device Model Notes ViewSet
|
||||
- **itam**: Device Model Notes Serializer
|
||||
- **itam**: Device Model Notes Model
|
||||
- **itam**: Device Type Notes ViewSet
|
||||
- **itam**: Device Type Notes Serializer
|
||||
- **itam**: Device Type Notes Model
|
||||
- **core**: Create an action comment on a ticket when the category changes
|
||||
- **itim**: Porte Notes ViewSet
|
||||
- **itim**: Porte Notes Serializer
|
||||
- **itim**: Porte Notes Model
|
||||
- **itim**: Cluster Type Notes ViewSet
|
||||
- **itim**: Cluster Type Notes Serializer
|
||||
- **itim**: Cluster Type Notes Model
|
||||
- **project_management**: Project Notes ViewSet
|
||||
- **project_management**: Project Notes Serializer
|
||||
- **project_management**: Project Notes Model
|
||||
- **itim**: Service Notes ViewSet
|
||||
- **itim**: Service Notes Serializer
|
||||
- **itim**: Service Notes Model
|
||||
- **itim**: Cluster Notes ViewSet
|
||||
- **itim**: Cluster Notes Serializer
|
||||
- **itim**: Cluster Notes Model
|
||||
- **itam**: Software Notes ViewSet
|
||||
- **itam**: Software Notes Serilaizer
|
||||
- **itam**: Software Notes Model
|
||||
- **itam**: Operating System Notes ViewSet
|
||||
- **itam**: Operating System Notes Serializer
|
||||
- **itam**: Operating System Notes Model
|
||||
- **core**: Manufacturer Notes viewset
|
||||
- **core**: Manufacturer Notes serializer
|
||||
- **core**: Manufacturer Notes Model
|
||||
- **config_management**: Config Group Notes ViewSet
|
||||
- **config_management**: Config Group Notes Serializer
|
||||
- **config_management**: Config Group Notes Model
|
||||
- **assistance**: Knowledge Base Notes ViewSet
|
||||
- **assistance**: Knowledge Base Notes Serializer
|
||||
- **assistance**: Knowledge Base Notes Model
|
||||
- **access**: Team Notes ViewSet
|
||||
- **access**: Team Notes Serializer
|
||||
- **access**: Team Notes Model
|
||||
- **access**: Organization Notes ViewSet
|
||||
- **access**: Organization Notes Serializer
|
||||
- **access**: Organization Notes Model
|
||||
- **itam**: Device Notes ViewSet
|
||||
- **itam**: Device Notes Serializer
|
||||
- **itam**: Device Notes Model
|
||||
- **core**: Base viewset for model notes
|
||||
- **core**: Base serializer for model notes
|
||||
- **core**: Base model for model notes
|
||||
- **core**: Add failsafe to throw an exception if no action comment will be created
|
||||
- **core**: Add field parent_ticket to base ticket view serializer
|
||||
- **project_management**: Add field parent_ticket to project task ticket view serializer
|
||||
- **itim**: Add field parent_ticket to problem ticket view serializer
|
||||
- **itim**: Add field parent_ticket to incident ticket view serializer
|
||||
- **itim**: Add field parent_ticket to change ticket view serializer
|
||||
- **assistance**: Add field parent_ticket to request ticket view serializer
|
||||
- **core**: Add field parent to ticket model
|
||||
|
||||
### Fixes
|
||||
|
||||
- **core**: Dont attempt to access parent_ticket field during ticket validation if it does not exist
|
||||
- **core**: Permissions require the parent model for model notes
|
||||
- **access**: field organization requires team related_model for org
|
||||
- **core**: Use generic APIError for ticket save when no action comment will be created
|
||||
|
||||
### Refactoring
|
||||
|
||||
- Squash migrations so there is less of them for model notes
|
||||
- **access**: Dont add releationship from tenancyObject.organization to organization model
|
||||
|
||||
### Tests
|
||||
|
||||
- **settings**: Test User Settings API render to ensure browser_model exists and is the correct type
|
||||
- **settings**: Test User Settings model to ensure `browser_mode` field exists
|
||||
- **access**: Team Note Model Check object requires org
|
||||
- **settings**: External Links Note Model Checks
|
||||
- **project_management**: Project Type Note Model Checks
|
||||
- **project_management**: Project State Note Model Checks
|
||||
- **project_management**: Project Note Model Checks
|
||||
- **project_management**: Project Milestone Note Model Checks
|
||||
- **itim**: Service Note Model Checks
|
||||
- **itim**: Port Note Model Checks
|
||||
- **itim**: Cluster Type Note Model Checks
|
||||
- **itim**: Cluster Note Model Checks
|
||||
- **itam**: Software Version Note Model Checks
|
||||
- **itam**: Software Note Model Checks
|
||||
- **itam**: Software Category Note Model Checks
|
||||
- **itam**: Operating System Version Note Model Checks
|
||||
- **itam**: Operating System Note Model Checks
|
||||
- **itam**: Device Type Note Model Checks
|
||||
- **itam**: Device Note Model Checks
|
||||
- **itam**: Device Model Note Model Checks
|
||||
- **core**: Manufacturer Note Model Checks
|
||||
- **config_management**: Config Group Note Model Checks
|
||||
- **assistance**: KB Note Model Checks
|
||||
- **assistance**: KB Category Note Model Checks
|
||||
- **access**: Team Note Model Checks
|
||||
- **access**: Organization Note Model Checks
|
||||
- **core**: Model Notes Test Suite
|
||||
- **settings**: Serializer Checks for External Links Notes
|
||||
- **Project_management**: Serializer Checks for Project Type Notes
|
||||
- **Project_management**: Serializer Checks for Project State Notes
|
||||
- **Project_management**: Serializer Checks for Project Notes
|
||||
- **Project_management**: Serializer Checks for Project Milestone Notes
|
||||
- **itim**: Serializer Checks for Service Notes
|
||||
- **itim**: Serializer Checks for Port Notes
|
||||
- **itim**: Serializer Checks for Cluster Type Notes
|
||||
- **itim**: Serializer Checks for Cluster Notes
|
||||
- **itam**: Serializer Checks for Software Version Notes
|
||||
- **itam**: Serializer Checks for Software Notes
|
||||
- **itam**: Serializer Checks for Software Category Notes
|
||||
- **itam**: Serializer Checks for Operating System Version Notes
|
||||
- **itam**: Serializer Checks for Operating System Notes
|
||||
- **itam**: Serializer Checks for Device Type Notes
|
||||
- **itam**: Serializer Checks for Device Notes
|
||||
- **itam**: Serializer Checks for Device Model Notes
|
||||
- **core**: Serializer Checks for Manufacturer Notes
|
||||
- **config_management**: Serializer Checks for Config Groups Notes
|
||||
- **assistance**: Serializer Checks for KB Notes
|
||||
- **assistance**: Serializer Checks for KB Category Notes
|
||||
- **access**: Serializer Checks for Team Notes
|
||||
- **access**: Serializer Checks for Organization Notes
|
||||
- **core**: Test Suite for Model Notes checks
|
||||
- **api**: API Fileds user to be super user for tests to run
|
||||
- **settings**: External Links Notes Function Viewset Tests
|
||||
- **project_management**: Project Type Notes Function Viewset Tests
|
||||
- **project_management**: Project State Notes Function Viewset Tests
|
||||
- **project_management**: Project Notes Function Viewset Tests
|
||||
- **project_management**: Project Milestone Notes Function Viewset Tests
|
||||
- **itim**: Service Notes Function Viewset Tests
|
||||
- **itim**: Port Notes Function Viewset Tests
|
||||
- **itim**: Cluster Types Notes Function Viewset Tests
|
||||
- **itim**: Cluster Notes Function Viewset Tests
|
||||
- **itam**: Software Version Notes Function Viewset Tests
|
||||
- **itam**: Software Notes Function Viewset Tests
|
||||
- **itam**: Software Category Notes Function Viewset Tests
|
||||
- **itam**: Operating System Version Notes Function Viewset Tests
|
||||
- **itam**: Operating System Notes Function Viewset Tests
|
||||
- **itam**: Device Type Notes Function Viewset Tests
|
||||
- **itam**: Device Notes Function Viewset Tests
|
||||
- **itam**: Device Model Notes Function Viewset Tests
|
||||
- **core**: Manufacturer Notes Function Viewset Tests
|
||||
- **config_management**: Config Groups Notes Function Viewset Tests
|
||||
- **assistance**: Knowledge Base Notes Function Viewset Tests
|
||||
- **assistance**: Knowledge Base Category Notes Function Viewset Tests
|
||||
- **access**: Team Notes Function Viewset Tests
|
||||
- **access**: Organization Notes Function Viewset Tests
|
||||
- **core**: Model Notes Test Cases
|
||||
- Remove old notes model tests
|
||||
- **settings**: External Notes Test Cases for ViewSet
|
||||
- **project_management**: Project Type Notes Test Cases for ViewSet
|
||||
- **project_management**: Project State Notes Test Cases for ViewSet
|
||||
- **project_management**: Project Notes Test Cases for ViewSet
|
||||
- **project_management**: Project Milestone Notes Test Cases for ViewSet
|
||||
- **itim**: Service Notes Test Cases for ViewSet
|
||||
- **itim**: Port Notes Test Cases for ViewSet
|
||||
- **itim**: Cluster Type Notes Test Cases for ViewSet
|
||||
- **itim**: Cluster Notes Test Cases for ViewSet
|
||||
- **itam**: Software Version Notes Test Cases for ViewSet
|
||||
- **itam**: Software Notes Test Cases for ViewSet
|
||||
- **itam**: Software Category Notes Test Cases for ViewSet
|
||||
- **itam**: Operating System Version Notes Test Cases for ViewSet
|
||||
- **itam**: Operating_system Notes Test Cases for ViewSet
|
||||
- **itam**: Device Type Notes Test Cases for ViewSet
|
||||
- **itam**: Device Notes Test Cases for ViewSet
|
||||
- **itam**: Device Model Notes Test Cases for ViewSet
|
||||
- **core**: Manufacturer Notes Test Cases for ViewSet
|
||||
- **config_management**: Config Groups Notes Test Cases for ViewSet
|
||||
- **assistance**: Knowledge Base Notes Test Cases for ViewSet
|
||||
- **assistance**: Knowledge Base Category Notes Test Cases for ViewSet
|
||||
- **access**: Team Notes Test Cases for ViewSet
|
||||
- **access**: Organization Notes Test Cases for ViewSet
|
||||
- **project_management**: Correct kwargs for Project Milestone Notes Test Cases for API Field Checks
|
||||
- **assistance**: Knowledge Base Category Notes Test Cases for API Field Checks
|
||||
- **Settings**: External Link Notes Test Cases for API Field Checks
|
||||
- **project_management**: Project Type Notes Test Cases for API Field Checks
|
||||
- **project_management**: Project State Notes Test Cases for API Field Checks
|
||||
- **project_management**: Project Notes Test Cases for API Field Checks
|
||||
- **project_management**: Project Milestone Notes Test Cases for API Field Checks
|
||||
- **itim**: Service Notes Test Cases for API Field Checks
|
||||
- **itim**: Port Notes Test Cases for API Field Checks
|
||||
- **itim**: Cluster Types Notes Test Cases for API Field Checks
|
||||
- **itim**: Cluster Notes Test Cases for API Field Checks
|
||||
- **itam**: Software Version Notes Test Cases for API Field Checks
|
||||
- **itam**: Software Notes Test Cases for API Field Checks
|
||||
- **itam**: Software Category Notes Test Cases for API Field Checks
|
||||
- **itam**: Device Type Notes Test Cases for API Field Checks
|
||||
- **itam**: Device Notes Test Cases for API Field Checks
|
||||
- **itam**: Device Model Notes Test Cases for API Field Checks
|
||||
- **itam**: Operating System Test Cases for API Field Checks
|
||||
- **itam**: Operating System Version Test Cases for API Field Checks
|
||||
- **core**: Manufacturer Test Cases for API Field Checks
|
||||
- **config_management**: Config Group Test Cases for API Field Checks
|
||||
- **assistance**: KB Test Cases for API Field Checks
|
||||
- **access**: Team Test Cases for API Field Checks
|
||||
- **access**: Organization Test Cases for API Field Checks
|
||||
- **core**: Model Notes Base Test Cases for API Field Checks
|
||||
- remove old notes model tests
|
||||
- Update url_name to match new notes endpoint
|
||||
- **config_Management**: Update url_name to match new notes endpoint
|
||||
- **core**: Remove notes test cases for previous notes model
|
||||
|
||||
## 1.9.0 (2025-02-06)
|
||||
|
||||
### feat
|
||||
|
||||
- **core**: Validate user field to ensure ticket comments always have user who added comment
|
||||
- **core**: Cache ticket linked item queryset
|
||||
- Views to cache discovered serializer
|
||||
- **core**: When changing ticket description create an action comment with the details
|
||||
- **core**: When changing a ticket real finish date create an action comment with the details
|
||||
- **core**: When changing a ticket real start date create an action comment with the details
|
||||
- **core**: When changing a ticket planned finish date create an action comment with the details
|
||||
- **core**: When changing a ticket planned start date create an action comment with the details
|
||||
- **core**: When changing a ticket milestone create an action comment with the changed details
|
||||
- **core**: Add Priority badge field to ALL ticket types
|
||||
- **core**: Add Impact badge field to ALL ticket types
|
||||
- **core**: Add urgency badge field to ALL ticket types
|
||||
|
||||
### Fixes
|
||||
|
||||
- **project_management**: Add missing attribute `view_description` to project tasks viewset
|
||||
- **settings**: Add missing attribute `view_description` to user settings viewset
|
||||
- **settings**: Add missing attribute `view_description` to app settings viewset
|
||||
- **itim**: Add missing attribuite to problem ticket viewset
|
||||
- **itim**: Add missing attribuite to incident ticket viewset
|
||||
- **itim**: Add missing attribuite to change ticket viewset
|
||||
- **itimm**: correct truthy check for service device ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for service cluster ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for service cluster ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for service device ViewSet property when evaluating serializer_class
|
||||
- **itam**: add missing attribute view_name to celery log viewset
|
||||
- **api**: correct get_view_name to prioritize view_name over model.verbose_name
|
||||
- **itam**: correct truthy check for software version ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for os version ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for device software ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for device os ViewSet property when evaluating queryset
|
||||
- **itam**: correct truthy check for software version ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for software category ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for os version ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for device software ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for device os ViewSet property when evaluating serializer_class
|
||||
- **core**: correct varname for queryset within notes queryset
|
||||
- **core**: add missing attribute view_description to ticket linked item viewset
|
||||
- **core**: add missing attribute view_description to ticket comment viewset
|
||||
- **core**: add missing attribute view_description to note viewset
|
||||
- **core**: add missing attribute view_description to related ticket log viewset
|
||||
- **core**: add missing attribute view_description to celery log viewset
|
||||
- **core**: add missing attribute view_description to history viewset
|
||||
- **core**: correct truthy check for notes ViewSet property when evaluating serializer_class
|
||||
- **core**: correct truthy check for history ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for notes ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for related ticket ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for ticket comment ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for ticket linked items ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for ticket linked items ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for ticket comment ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for related ticket ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for history ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for celery log ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for Ticket Base ViewSet property when evaluating queryset
|
||||
- **core**: correct truthy check for ticket base ViewSet property when evaluating serializer_class
|
||||
- **assistance**: Add missing attribute `view_description` to request ticket ViewSet
|
||||
- **settinggs**: Add missing attribute `view_description` to external links ViewSet
|
||||
- **core**: Add missing attribute `view_description` to ticket comment category ViewSet
|
||||
- **core**: Add missing attribute `view_description` to ticekt category ViewSet
|
||||
- **core**: Add missing attribute `view_description` to Manufacturer ViewSet
|
||||
- **project_management**: correct truthy check for project milestone ViewSet property when evaluating queryset
|
||||
- **settings**: correct truthy check for user settings ViewSet property when evaluating serializer_class
|
||||
- **settings**: correct truthy check for external links ViewSet property when evaluating serializer_class
|
||||
- **settings**: correct truthy check for app settings ViewSet property when evaluating serializer_class
|
||||
- **project_management**: correct truthy check for project milestone ViewSet property when evaluating queryset
|
||||
- **project_management**: correct truthy check for project ViewSet property when evaluating serializer_class
|
||||
- **project_management**: correct truthy check for project type ViewSet property when evaluating serializer_class
|
||||
- **project_management**: correct truthy check for project state ViewSet property when evaluating serializer_class
|
||||
- **project_management**: correct truthy check for project milestone ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for service ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for port ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for cluster ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for cluster type ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for software ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for os ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for device ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for device type ViewSet property when evaluating serializer_class
|
||||
- **itam**: correct truthy check for device model ViewSet property when evaluating serializer_class
|
||||
- **core**: correct truthy check for ticket comment category ViewSet property when evaluating serializer_class
|
||||
- **core**: correct truthy check for ticket category ViewSet property when evaluating serializer_class
|
||||
- **core**: correct truthy check for manufacturer ViewSet property when evaluating serializer_class
|
||||
- **config_management**: correct truthy check for config group ViewSet property when evaluating queryset
|
||||
- **config_management**: correct truthy check for config group ViewSet property when evaluating serializer_class
|
||||
- **config_management**: correct truthy check for config group software ViewSet property when evaluating serializer_class
|
||||
- **config_management**: correct truthy check for config group software ViewSet property when evaluating queryset
|
||||
- **assistance**: correct truthy check for model kb article ViewSet property when evaluating queryset
|
||||
- **assistance**: correct truthy check for model kb article ViewSet property when evaluating serializer_class
|
||||
- **assistance**: correct truthy check for kb ViewSet property when evaluating serializer_class
|
||||
- **assistance**: correct truthy check for kb ViewSet property when evaluating serializer_class
|
||||
- **access**: correct truthy check for team ViewSet property when evaluating serializer_class
|
||||
- **access**: correct truthy check for team ViewSet property when evaluating queryset
|
||||
- **access**: correct truthy check for team user ViewSet property when evaluating queryset
|
||||
- **access**: correct truthy check for team user ViewSet property when evaluating serializer_class
|
||||
- **access**: correct truthy check for organization ViewSet property when evaluating serializer_class
|
||||
- **api**: correct truthy check for set property when evaluating serializer_class
|
||||
- **api**: correct truthy check for set property when evaluating queryset
|
||||
- **api**: correct variable name in common viewset for queryset
|
||||
- **config_management**: config group software viewset must cache queryset
|
||||
- **config_management**: config group viewset must cache queryset
|
||||
- **assistance**: Knowledge base category viewset must cache serializer_class
|
||||
- **access**: Team viewset must cache serializer_class
|
||||
- **access**: Team viewset must cach queryset
|
||||
- **access**: Team User viewset must cach queryset
|
||||
- **api**: Add missing property `bacjk_url` to Common viewset
|
||||
- **api**: Common viewset to cache and use queryset Object
|
||||
- **access**: When conduting permission check for user settings, if user not owner of settings, deny access
|
||||
- **access**: when checking object permissions, dont cast obj to int untill checking it exists
|
||||
- **access**: org mixin get_obj_org not to call get_object
|
||||
- **core**: ensure item_type exists before trying to get queryset
|
||||
- **core**: Ticket Action comment date fields must be checked if empty before use
|
||||
- **settings**: grant the user access to their own settings object
|
||||
- **settings**: grant the user access to their own settings
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **access**: when checking obj permission use view cached obj organization
|
||||
- **access**: When fetching obj org, if pk exist attempt to fetch object
|
||||
- **core**: When fetching a ticket, fetch related fields
|
||||
- **core**: Ticket action comment for changing milestone to use item tasg
|
||||
- **core**: Ticket action comment for changing project to use item tasg
|
||||
|
||||
### Tests
|
||||
|
||||
- **core**: Add missing unit tests for notes ticket viewset
|
||||
- **settings**: Add missing unit tests for user settings ticket viewset
|
||||
- **settings**: Add missing unit tests for app settings ticket viewset
|
||||
- **project_management**: Add missing unit tests for project task ticket viewset
|
||||
- **api**: Add kwargs as arg to test cases
|
||||
- **itim**: Add missing unit tests for problem ticket viewset
|
||||
- **itim**: Add missing unit tests for incident ticket viewset
|
||||
- **itim**: Add missing unit tests for change ticket viewset
|
||||
- **core**: add permisssion class override test case for celery results
|
||||
- Add empty kwargs to ViewSet index page test cases
|
||||
- **itam**: Add missing unit tests for software version viewset
|
||||
- **itam**: Add missing unit tests for software categories viewset
|
||||
- **itam**: Add missing unit tests for os versions viewset
|
||||
- **itam**: Add missing unit tests for software installs viewset
|
||||
- **itam**: Add missing unit tests for os installs viewset
|
||||
- **itam**: Add missing unit tests for device software viewset
|
||||
- **itam**: Add missing unit tests for device operating system viewset
|
||||
- **api**: Add kwargs as arg to test cases
|
||||
- **core**: Add missing unit tests for ticket linked items viewset
|
||||
- **core**: Add missing unit tests for ticket comment viewset
|
||||
- **core**: Add missing unit tests for celery log viewset
|
||||
- **core**: Add missing unit tests for history viewset
|
||||
- **core**: Add missing unit tests for related tickets viewset
|
||||
- **assistance**: Add missing unit tests for request ticket viewset
|
||||
- **api**: queryset and serializer_class test cases updated to use Fake request object
|
||||
- **settings**: Add missing unit tests for external links viewset
|
||||
- **project_management**: Add missing unit tests for project type viewset
|
||||
- **project_management**: Add missing unit tests for project state viewset
|
||||
- **project_management**: Add missing unit tests for project milestone viewset
|
||||
- **project_management**: Add missing unit tests for project viewset
|
||||
- **itim**: Add missing unit tests for service viewset
|
||||
- **itim**: Add missing unit tests for ports viewset
|
||||
- **itim**: Add missing unit tests for cluster types viewset
|
||||
- **itim**: Add missing unit tests for cluster viewset
|
||||
- **itam**: Add missing unit tests for Software viewset
|
||||
- **itam**: Add missing unit tests for Operating System viewset
|
||||
- **itam**: Add missing unit tests for Device Type viewset
|
||||
- **itam**: Add missing unit tests for Device Model viewset
|
||||
- **itam**: Add missing unit tests for Device viewset
|
||||
- **api**: dont mock the qs bool
|
||||
- **core**: Add missing unit tests for Ticket Comment Category viewset
|
||||
- **core**: Add missing unit tests for Ticket Category viewset
|
||||
- **core**: Add missing unit tests for manufacturer viewset
|
||||
- **config_management**: Add missing unit tests for config groups software viewset
|
||||
- **config_management**: Add missing unit tests for config groups viewset
|
||||
- **assistance**: Add missing unit tests for Model Knowledge Base Article viewset
|
||||
- **assistance**: Add missing unit tests for Knowledge Base Category viewset
|
||||
- **assistance**: Add missing unit tests for Knowledge Base viewset
|
||||
- **access**: Add missing unit tests for team user viewset
|
||||
- **access**: Add missing unit tests for team viewset
|
||||
- **access**: Add missing unit tests for organization viewset
|
||||
- **base**: Ensure viewsets are caching and using the serializer_class object
|
||||
- **base**: Ensure viewsets are caching and using the queryset object
|
||||
- **core**: Test case to ensure ticket comment always has user added
|
||||
- **settings**: when checking if user can delete own settings, user must be owner of settings
|
||||
- **settings**: regardless of permissions a user can change their own settings
|
||||
- **access**: during permission check function has_permission ensure `get_object` not called
|
||||
- **core**: Ensure that an action comment is created when ticket description is edited
|
||||
- **core**: Ticket Action comment test cases for real_finish_date actions
|
||||
- **core**: Ticket Action comment test cases for real_start_date actions
|
||||
- **core**: Ticket Action comment test cases for planned_finish_date actions
|
||||
- **core**: Ticket Action comment test cases for planned_start_date actions
|
||||
- **core**: Ticket Action comment test cases for milestone actions
|
||||
- **core**: Ticket Action comment test cases for project actions
|
||||
- **core**: Ticket Action comment tests moved to their own suite
|
||||
- **core**: Unit test cases for ticket urgency_badge field checks
|
||||
- **core**: Unit test cases for ticket priority_badge field checks
|
||||
- **core**: Unit test cases for ticket impact_badge field checks
|
||||
- **settings**: Remove no-permission failure test as user settings require no permissions
|
||||
|
||||
## 1.8.0 (2025-01-23)
|
||||
|
||||
### feat
|
||||
|
@ -95,6 +95,34 @@ clear; \
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Tips / Handy info
|
||||
|
||||
- To obtain a list of models _(in in the same order as the file system)_ using the db shell `python3 manage.py dbshell` run the following sql command:
|
||||
|
||||
``` sql
|
||||
|
||||
SELECT model FROM django_content_type ORDER BY app_label ASC, model ASC;
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Old working docs
|
||||
|
||||
|
||||
|
@ -1,3 +1,71 @@
|
||||
|
||||
## Version 1.11.0
|
||||
|
||||
**Note:** Migrations should be performed offline. **Failing to perform** an online migration, the option provided below will not be available if the migration crashes. Running the below commands to reset the database for the migrations to re-run will cause data loss if users are making changes to Centurion.
|
||||
|
||||
- History views removed from original Centurion interface.
|
||||
|
||||
- History views removed from API v1.
|
||||
|
||||
- A migration exists that will move the history from the old tables to the new ones.
|
||||
|
||||
if for some reason the migration crashes enter the following commands in the dbshell `python manage.py dbshell` and restart the migrations
|
||||
|
||||
``` sql
|
||||
|
||||
delete from access_organization_history;
|
||||
delete from access_team_history;
|
||||
|
||||
delete from assistance_knowledge_base_history;
|
||||
delete from assistance_knowledge_base_category_history;
|
||||
|
||||
delete from config_management_configgroups_history;
|
||||
delete from config_management_configgroupsoftware_history;
|
||||
delete from config_management_configgrouphosts_history;
|
||||
|
||||
delete from core_manufacturer_history;
|
||||
delete from core_ticketcategory_history;
|
||||
delete from core_ticketcommentcategory_history;
|
||||
|
||||
delete from itam_device_history;
|
||||
delete from itam_devicemodel_history;
|
||||
delete from itam_devicetype_history;
|
||||
delete from itam_deviceoperatingsystem_history;
|
||||
delete from itam_devicesoftware_history;
|
||||
delete from itam_operatingsystem_history;
|
||||
delete from itam_operatingsystemversion_history;
|
||||
delete from itam_software_history;
|
||||
delete from itam_softwareversion_history;
|
||||
delete from itam_softwarecategory_history;
|
||||
|
||||
delete from itim_cluster_history;
|
||||
delete from itim_clustertype_history;
|
||||
delete from itim_port_history;
|
||||
delete from itim_service_history;
|
||||
|
||||
delete from project_management_project_history;
|
||||
delete from project_management_projectmilestone_history;
|
||||
delete from project_management_projectstate_history;
|
||||
delete from project_management_projecttype_history;
|
||||
delete from settings_externallink_history;
|
||||
|
||||
delete from core_model_history;
|
||||
|
||||
```
|
||||
|
||||
The above commands truncate the data from the new history tables so the migration can run again.
|
||||
|
||||
|
||||
## Version 1.10.0
|
||||
|
||||
- Nothing significant to report
|
||||
|
||||
|
||||
## Version 1.9.0
|
||||
|
||||
- Nothing significant to report
|
||||
|
||||
|
||||
## Version 1.8.0
|
||||
|
||||
- Prometheus exporter added. To enable metrics for the database you will have to update the database backend. see the [docs](https://nofusscomputing.com/projects/centurion_erp/administration/monitoring/#django-exporter-setup) for further information.
|
||||
|
@ -2,7 +2,10 @@ from django.contrib import admin
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .models import *
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
admin.site.unregister(Group)
|
||||
|
||||
|
@ -3,7 +3,7 @@ from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.organization import Organization
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
@ -4,7 +4,7 @@ from django.forms import inlineformset_factory
|
||||
|
||||
from .team_users import TeamUsersForm, TeamUsers
|
||||
|
||||
from access.models import Team
|
||||
from access.models.team import Team
|
||||
from access.functions import permissions
|
||||
|
||||
from app import settings
|
||||
|
@ -2,7 +2,7 @@ from django.db.models import Q
|
||||
|
||||
from app import settings
|
||||
|
||||
from access.models import TeamUsers
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from core.forms.common import CommonModelForm
|
||||
|
||||
|
@ -24,16 +24,22 @@ def permission_queryset():
|
||||
'chordcounter',
|
||||
'comment',
|
||||
'groupresult',
|
||||
'history',
|
||||
'modelnotes',
|
||||
'usersettings',
|
||||
]
|
||||
|
||||
exclude_permissions = [
|
||||
'add_history',
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'change_history',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'delete_history',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
'view_history',
|
||||
]
|
||||
|
||||
return Permission.objects.filter(
|
||||
|
@ -6,7 +6,10 @@ from django.contrib.auth.middleware import (
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
from access.models import Organization, Team
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
@ -44,7 +44,7 @@ class Migration(migrations.Migration):
|
||||
('team_name', models.CharField(default='', max_length=50, verbose_name='Name')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False)),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False)),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists])),
|
||||
('organization', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists])),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Teams',
|
||||
|
@ -59,7 +59,7 @@ class Migration(migrations.Migration):
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.TenancyObject.validatate_organization_exists], verbose_name='Organization'),
|
||||
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
|
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-09 11:07
|
||||
|
||||
import access.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0002_alter_organization_options_alter_team_options_and_more'),
|
||||
('core', '0012_modelnotes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.team.Team.validatate_organization_exists], verbose_name='Organization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizationNotes',
|
||||
fields=[
|
||||
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.organization', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization Note',
|
||||
'verbose_name_plural': 'Organization Notes',
|
||||
'db_table': 'access_organization_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamNotes',
|
||||
fields=[
|
||||
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.team', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team Note',
|
||||
'verbose_name_plural': 'Team Notes',
|
||||
'db_table': 'access_team_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
]
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-20 13:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0003_alter_team_organization_organizationnotes_teamnotes'),
|
||||
('core', '0015_modelhistory_manufacturerhistory_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationHistory',
|
||||
fields=[
|
||||
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.organization', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization History',
|
||||
'verbose_name_plural': 'Organization History',
|
||||
'db_table': 'access_organization_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamHistory',
|
||||
fields=[
|
||||
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.team', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team History',
|
||||
'verbose_name_plural': 'Team History',
|
||||
'db_table': 'access_team_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
]
|
@ -4,7 +4,9 @@ from django.contrib.auth.models import Group
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
from .models import Organization, Team
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
|
||||
class OrganizationMixin():
|
||||
|
@ -1,5 +1,9 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
from access.models import Organization, Team
|
||||
from django.db import models
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
|
||||
class OrganizationMixin:
|
||||
@ -38,19 +42,19 @@ class OrganizationMixin:
|
||||
return self._obj_organization
|
||||
|
||||
|
||||
_obj_organization: Organization = None
|
||||
|
||||
|
||||
if obj:
|
||||
|
||||
_obj_organization = getattr(obj, 'organization', None)
|
||||
self._obj_organization = getattr(obj, 'organization', None)
|
||||
|
||||
|
||||
if not _obj_organization:
|
||||
if not self._obj_organization:
|
||||
|
||||
_obj_organization = getattr(obj, 'get_organization', lambda: None)()
|
||||
self._obj_organization = getattr(obj, 'get_organization', lambda: None)()
|
||||
|
||||
elif request:
|
||||
elif (
|
||||
request
|
||||
and not self.kwargs.get('pk', None)
|
||||
):
|
||||
|
||||
if getattr(request.stream, 'method', '') != 'DELETE':
|
||||
|
||||
@ -72,23 +76,31 @@ class OrganizationMixin:
|
||||
|
||||
if data_organization:
|
||||
|
||||
_obj_organization = Organization.objects.get(
|
||||
self._obj_organization = Organization.objects.get(
|
||||
pk = int( data_organization )
|
||||
)
|
||||
|
||||
elif self.kwargs.get('pk', None):
|
||||
|
||||
|
||||
obj = self.model.objects.get( pk = self.kwargs.get('pk', None) )
|
||||
|
||||
if getattr(obj, 'organization', None):
|
||||
|
||||
self._obj_organization = obj.organization
|
||||
|
||||
elif str(self.model._meta.verbose_name).lower() == 'organization':
|
||||
|
||||
self._obj_organization = obj
|
||||
|
||||
|
||||
if self.get_parent_model(): # if defined is to overwrite object organization
|
||||
|
||||
parent_obj = self.get_parent_obj()
|
||||
|
||||
_obj_organization = parent_obj.get_organization()
|
||||
self._obj_organization = parent_obj.get_organization()
|
||||
|
||||
|
||||
|
||||
if _obj_organization:
|
||||
|
||||
self._obj_organization = _obj_organization
|
||||
|
||||
return self._obj_organization
|
||||
|
||||
|
||||
@ -245,7 +257,7 @@ class OrganizationMixin:
|
||||
|
||||
|
||||
|
||||
parent_model: str = None
|
||||
parent_model: models.Model = None
|
||||
""" Parent Model
|
||||
|
||||
This attribute defines the parent model for the model in question. The parent model when defined
|
||||
|
@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from access.models import TenancyObject
|
||||
from access.models.tenancy import Organization, TenancyObject
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
|
||||
@ -115,6 +115,33 @@ class OrganizationPermissionMixin(
|
||||
|
||||
try:
|
||||
|
||||
if (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
return True
|
||||
|
||||
elif (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id != int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id != int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
has_permission_required: bool = False
|
||||
|
||||
@ -159,10 +186,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'delete'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif (
|
||||
view.action == 'list'
|
||||
):
|
||||
@ -176,10 +199,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'change'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif (
|
||||
view.action == 'update'
|
||||
and request.method == 'PUT'
|
||||
@ -187,10 +206,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'change'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif(
|
||||
view.action == 'retrieve'
|
||||
and request.method == 'GET'
|
||||
@ -198,10 +213,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'view'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif(
|
||||
view.action == 'metadata'
|
||||
and request.method == 'OPTIONS'
|
||||
@ -265,17 +276,32 @@ class OrganizationPermissionMixin(
|
||||
|
||||
try:
|
||||
|
||||
|
||||
if request.user.is_anonymous:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
object_organization: int = getattr(view.get_obj_organization( obj = obj ), 'id', None)
|
||||
if (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
object_organization = view._obj_organization
|
||||
|
||||
if object_organization:
|
||||
|
||||
if(
|
||||
object_organization
|
||||
int(object_organization)
|
||||
in view.get_permission_organizations( view.get_permission_required() )
|
||||
or request.user.is_superuser
|
||||
or getattr(request.app_settings.global_organization, 'id', 0) == int(object_organization)
|
||||
|
@ -1,594 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from .fields import *
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
from core.middleware.get_request import get_request
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
|
||||
class Organization(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Organization"
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ['name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.slug == '_':
|
||||
self.slug = self.name.lower().replace(' ', '_')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of this Organization',
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
manager = models.ForeignKey(
|
||||
User,
|
||||
blank = False,
|
||||
help_text = 'Manager for this organization',
|
||||
null = True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name = 'Manager'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __int__(self):
|
||||
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
table_fields: list = [
|
||||
'nbsp',
|
||||
'name',
|
||||
'created',
|
||||
'modified',
|
||||
'nbsp'
|
||||
]
|
||||
|
||||
page_layout: list = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'manager',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Teams",
|
||||
"slug": "teams",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "teams"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
if request:
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", request=request, kwargs={'pk': self.id})
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", kwargs={'pk': self.id})
|
||||
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
"""Multi-Tennant Object Manager
|
||||
|
||||
This manager specifically caters for the multi-tenancy features of Centurion ERP.
|
||||
"""
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
""" Fetch the data
|
||||
|
||||
This function filters the data fetched from the database to that which is from the organizations
|
||||
the user is a part of.
|
||||
|
||||
!!! danger "Requirement"
|
||||
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
|
||||
|
||||
## Workflow
|
||||
|
||||
This functions workflow is as follows:
|
||||
|
||||
- Fetch the user from the request
|
||||
|
||||
- Check if the user is authenticated
|
||||
|
||||
- Iterate over the users teams
|
||||
|
||||
- Store unique organizations from users teams
|
||||
|
||||
- return results
|
||||
|
||||
Returns:
|
||||
(queryset): **super user**: return unfiltered data.
|
||||
(queryset): **not super user**: return data from the stored unique organizations.
|
||||
"""
|
||||
|
||||
request = get_request()
|
||||
|
||||
user_organizations: list(str()) = []
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
if request.app_settings.global_organization:
|
||||
|
||||
user_organizations += [ request.app_settings.global_organization.id ]
|
||||
|
||||
|
||||
user = request.user
|
||||
|
||||
|
||||
if user.is_authenticated:
|
||||
|
||||
for team in request.tenancy._user_teams:
|
||||
|
||||
|
||||
if team.organization.id not in user_organizations:
|
||||
|
||||
if not user_organizations:
|
||||
|
||||
self.user_organizations = []
|
||||
|
||||
user_organizations += [ team.organization.id ]
|
||||
|
||||
|
||||
# if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
|
||||
if getattr(self.model, 'is_global', False) is True:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
|
||||
class TenancyObject(SaveHistory):
|
||||
""" Tenancy Model Abstrct class.
|
||||
|
||||
This class is for inclusion wihtin **every** model within Centurion ERP.
|
||||
Provides the required fields, functions and methods for multi tennant objects.
|
||||
Unless otherwise stated, **no** object within this class may be overridden.
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization
|
||||
"""
|
||||
|
||||
objects = TenancyManager()
|
||||
""" Multi-Tenanant Objects """
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise ValidationError('You must provide an organization')
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of the item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Organization this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Organization'
|
||||
)
|
||||
|
||||
is_global = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this a global object?',
|
||||
verbose_name = 'Global Object'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null = True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
"""Fetch the models URL
|
||||
|
||||
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
|
||||
|
||||
Args:
|
||||
request (object, optional): The request object that was made by the end user. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
|
||||
"""
|
||||
|
||||
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.clean()
|
||||
|
||||
if not getattr(self, 'organization', None):
|
||||
|
||||
raise centurion_exceptions.ValidationError(
|
||||
detail = {
|
||||
'organization': 'Organization is required'
|
||||
},
|
||||
code = 'required'
|
||||
)
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [ 'team_name' ]
|
||||
|
||||
verbose_name = 'Team'
|
||||
|
||||
verbose_name_plural = "Teams"
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
team_name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name to give this team',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: dict = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'team_name',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "table",
|
||||
"name": "Users",
|
||||
"field": "users",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
table_fields: list = [
|
||||
'team_name',
|
||||
'modified',
|
||||
'created',
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'organization_id': self.organization.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.organization
|
||||
|
||||
|
||||
def permission_list(self) -> list:
|
||||
|
||||
permission_list = []
|
||||
|
||||
for permission in self.permissions.all():
|
||||
|
||||
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
|
||||
continue
|
||||
|
||||
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
|
||||
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.organization.name + ', ' + self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = ['user']
|
||||
|
||||
verbose_name = "Team User"
|
||||
|
||||
verbose_name_plural = "Team Users"
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this Team User',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
blank = False,
|
||||
help_text = 'Team user belongs to',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="team",
|
||||
verbose_name = 'Team'
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank = False,
|
||||
help_text = 'User who will be added to the team',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name = 'User'
|
||||
)
|
||||
|
||||
manager = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text = 'Is this user to be a manager of this team',
|
||||
verbose_name='manager',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: list = []
|
||||
|
||||
table_fields: list = [
|
||||
'user',
|
||||
'manager'
|
||||
]
|
||||
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
""" Delete Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of Groups, remove the user to the team.
|
||||
"""
|
||||
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.remove(group)
|
||||
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.team.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
url_kwargs: dict = {
|
||||
'organization_id': self.team.organization.id,
|
||||
'team_id': self.team.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
print(f'url kwargs are: {url_kwargs}')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", request=request, kwargs = url_kwargs )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", kwargs = url_kwargs )
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Save Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of groups, add the user to the matching group.
|
||||
"""
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.add(group)
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
0
app/access/models/__init__.py
Normal file
0
app/access/models/__init__.py
Normal file
155
app/access/models/organization.py
Normal file
155
app/access/models/organization.py
Normal file
@ -0,0 +1,155 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import (
|
||||
AutoCreatedField,
|
||||
AutoLastModifiedField,
|
||||
AutoSlugField
|
||||
)
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
|
||||
class Organization(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Organization"
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ['name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.slug == '_':
|
||||
self.slug = self.name.lower().replace(' ', '_')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of this Organization',
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
manager = models.ForeignKey(
|
||||
User,
|
||||
blank = False,
|
||||
help_text = 'Manager for this organization',
|
||||
null = True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name = 'Manager'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __int__(self):
|
||||
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
table_fields: list = [
|
||||
'nbsp',
|
||||
'name',
|
||||
'created',
|
||||
'modified',
|
||||
'nbsp'
|
||||
]
|
||||
|
||||
page_layout: list = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'manager',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Teams",
|
||||
"slug": "teams",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "teams"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
if request:
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", request=request, kwargs={'pk': self.id})
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", kwargs={'pk': self.id})
|
||||
|
||||
|
||||
def save_history(self, before: dict, after: dict) -> bool:
|
||||
|
||||
from access.models.organization_history import OrganizationHistory
|
||||
|
||||
history = super().save_history(
|
||||
before = before,
|
||||
after = after,
|
||||
history_model = OrganizationHistory
|
||||
)
|
||||
|
||||
|
||||
return history
|
53
app/access/models/organization_history.py
Normal file
53
app/access/models/organization_history.py
Normal file
@ -0,0 +1,53 @@
|
||||
from django.db import models
|
||||
|
||||
from core.models.model_history import ModelHistory
|
||||
|
||||
from access.models.organization import Organization
|
||||
|
||||
|
||||
|
||||
class OrganizationHistory(
|
||||
ModelHistory
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_organization_history'
|
||||
|
||||
ordering = ModelHistory._meta.ordering
|
||||
|
||||
verbose_name = 'Organization History'
|
||||
|
||||
verbose_name_plural = 'Organization History'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Model this note belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'history',
|
||||
verbose_name = 'Model',
|
||||
)
|
||||
|
||||
table_fields: list = []
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
|
||||
def get_object(self):
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def get_serialized_model(self, serializer_context):
|
||||
|
||||
model = None
|
||||
|
||||
from access.serializers.organization import OrganizationBaseSerializer
|
||||
|
||||
model = OrganizationBaseSerializer(self.model, context = serializer_context)
|
||||
|
||||
return model
|
45
app/access/models/organization_notes.py
Normal file
45
app/access/models/organization_notes.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.organization import Organization
|
||||
|
||||
from core.models.model_notes import ModelNotes
|
||||
|
||||
|
||||
|
||||
class OrganizationNotes(
|
||||
ModelNotes
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_organization_notes'
|
||||
|
||||
ordering = ModelNotes._meta.ordering
|
||||
|
||||
verbose_name = 'Organization Note'
|
||||
|
||||
verbose_name_plural = 'Organization Notes'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Model this note belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'notes',
|
||||
verbose_name = 'Model',
|
||||
)
|
||||
|
||||
table_fields: list = []
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
|
||||
return {
|
||||
'model_id': self.model.pk,
|
||||
'pk': self.pk
|
||||
}
|
175
app/access/models/team.py
Normal file
175
app/access/models/team.py
Normal file
@ -0,0 +1,175 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import (
|
||||
AutoCreatedField,
|
||||
AutoLastModifiedField
|
||||
)
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.tenancy import TenancyObject
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [ 'team_name' ]
|
||||
|
||||
verbose_name = 'Team'
|
||||
|
||||
verbose_name_plural = "Teams"
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise centurion_exceptions.ValidationError('You must provide an organization')
|
||||
|
||||
|
||||
|
||||
team_name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name to give this team',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Organization this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Organization'
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: dict = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'team_name',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "table",
|
||||
"name": "Users",
|
||||
"field": "users",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
table_fields: list = [
|
||||
'team_name',
|
||||
'modified',
|
||||
'created',
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'organization_id': self.organization.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
# @property
|
||||
# def parent_object(self):
|
||||
# """ Fetch the parent object """
|
||||
|
||||
# return self.organization
|
||||
|
||||
|
||||
def permission_list(self) -> list:
|
||||
|
||||
permission_list = []
|
||||
|
||||
for permission in self.permissions.all():
|
||||
|
||||
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
|
||||
continue
|
||||
|
||||
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
|
||||
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.organization.name + ', ' + self.team_name
|
||||
|
||||
|
||||
def save_history(self, before: dict, after: dict) -> bool:
|
||||
|
||||
from access.models.team_history import TeamHistory
|
||||
|
||||
history = super().save_history(
|
||||
before = before,
|
||||
after = after,
|
||||
history_model = TeamHistory
|
||||
)
|
||||
|
||||
|
||||
return history
|
53
app/access/models/team_history.py
Normal file
53
app/access/models/team_history.py
Normal file
@ -0,0 +1,53 @@
|
||||
from django.db import models
|
||||
|
||||
from core.models.model_history import ModelHistory
|
||||
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
|
||||
class TeamHistory(
|
||||
ModelHistory
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_team_history'
|
||||
|
||||
ordering = ModelHistory._meta.ordering
|
||||
|
||||
verbose_name = 'Team History'
|
||||
|
||||
verbose_name_plural = 'Team History'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Team,
|
||||
blank = False,
|
||||
help_text = 'Model this note belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'history',
|
||||
verbose_name = 'Model',
|
||||
)
|
||||
|
||||
table_fields: list = []
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
|
||||
def get_object(self):
|
||||
|
||||
return self
|
||||
|
||||
|
||||
def get_serialized_model(self, serializer_context):
|
||||
|
||||
model = None
|
||||
|
||||
from access.serializers.teams import TeamBaseSerializer
|
||||
|
||||
model = TeamBaseSerializer(self.model, context = serializer_context)
|
||||
|
||||
return model
|
54
app/access/models/team_notes.py
Normal file
54
app/access/models/team_notes.py
Normal file
@ -0,0 +1,54 @@
|
||||
from django.db import models
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.team import Team
|
||||
|
||||
from core.models.model_notes import ModelNotes
|
||||
|
||||
|
||||
|
||||
class TeamNotes(
|
||||
ModelNotes
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_team_notes'
|
||||
|
||||
ordering = ModelNotes._meta.ordering
|
||||
|
||||
verbose_name = 'Team Note'
|
||||
|
||||
verbose_name_plural = 'Team Notes'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Team,
|
||||
blank = False,
|
||||
help_text = 'Model this note belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = 'notes',
|
||||
verbose_name = 'Model',
|
||||
)
|
||||
|
||||
table_fields: list = []
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
kwargs = {
|
||||
'organization_id': self.organization.pk,
|
||||
'model_id': self.model.pk,
|
||||
'pk': self.pk
|
||||
}
|
||||
|
||||
if request:
|
||||
|
||||
return reverse("v2:_api_v2_organization_team_note-detail", request=request, kwargs = kwargs )
|
||||
|
||||
return reverse("v2:_api_v2_organization_team_note-detail", kwargs = kwargs )
|
137
app/access/models/team_user.py
Normal file
137
app/access/models/team_user.py
Normal file
@ -0,0 +1,137 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, Group
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import (
|
||||
AutoCreatedField,
|
||||
AutoLastModifiedField
|
||||
)
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = ['user']
|
||||
|
||||
verbose_name = "Team User"
|
||||
|
||||
verbose_name_plural = "Team Users"
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this Team User',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
blank = False,
|
||||
help_text = 'Team user belongs to',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="team",
|
||||
verbose_name = 'Team'
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank = False,
|
||||
help_text = 'User who will be added to the team',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name = 'User'
|
||||
)
|
||||
|
||||
manager = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text = 'Is this user to be a manager of this team',
|
||||
verbose_name='manager',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: list = []
|
||||
|
||||
table_fields: list = [
|
||||
'user',
|
||||
'manager'
|
||||
]
|
||||
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
""" Delete Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of Groups, remove the user to the team.
|
||||
"""
|
||||
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.remove(group)
|
||||
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.team.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
url_kwargs: dict = {
|
||||
'organization_id': self.team.organization.id,
|
||||
'team_id': self.team.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
print(f'url kwargs are: {url_kwargs}')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", request=request, kwargs = url_kwargs )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", kwargs = url_kwargs )
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Save Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of groups, add the user to the matching group.
|
||||
"""
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.add(group)
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
219
app/access/models/tenancy.py
Normal file
219
app/access/models/tenancy.py
Normal file
@ -0,0 +1,219 @@
|
||||
# from django.conf import settings
|
||||
from django.db import models
|
||||
# from django.contrib.auth.models import User, Group
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
# from .fields import *
|
||||
|
||||
from access.models.organization import Organization
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
from core.middleware.get_request import get_request
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
"""Multi-Tennant Object Manager
|
||||
|
||||
This manager specifically caters for the multi-tenancy features of Centurion ERP.
|
||||
"""
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
""" Fetch the data
|
||||
|
||||
This function filters the data fetched from the database to that which is from the organizations
|
||||
the user is a part of.
|
||||
|
||||
!!! danger "Requirement"
|
||||
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
|
||||
|
||||
## Workflow
|
||||
|
||||
This functions workflow is as follows:
|
||||
|
||||
- Fetch the user from the request
|
||||
|
||||
- Check if the user is authenticated
|
||||
|
||||
- Iterate over the users teams
|
||||
|
||||
- Store unique organizations from users teams
|
||||
|
||||
- return results
|
||||
|
||||
Returns:
|
||||
(queryset): **super user**: return unfiltered data.
|
||||
(queryset): **not super user**: return data from the stored unique organizations.
|
||||
"""
|
||||
|
||||
request = get_request()
|
||||
|
||||
user_organizations: list(str()) = []
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
if request.app_settings.global_organization:
|
||||
|
||||
user_organizations += [ request.app_settings.global_organization.id ]
|
||||
|
||||
|
||||
user = request.user
|
||||
|
||||
|
||||
if user.is_authenticated:
|
||||
|
||||
for team in request.tenancy._user_teams:
|
||||
|
||||
|
||||
if team.organization.id not in user_organizations:
|
||||
|
||||
if not user_organizations:
|
||||
|
||||
self.user_organizations = []
|
||||
|
||||
user_organizations += [ team.organization.id ]
|
||||
|
||||
|
||||
# if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
|
||||
if getattr(self.model, 'is_global', False) is True:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
|
||||
class TenancyObject(SaveHistory):
|
||||
""" Tenancy Model Abstrct class.
|
||||
|
||||
This class is for inclusion wihtin **every** model within Centurion ERP.
|
||||
Provides the required fields, functions and methods for multi tennant objects.
|
||||
Unless otherwise stated, **no** object within this class may be overridden.
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization
|
||||
"""
|
||||
|
||||
objects = TenancyManager()
|
||||
""" Multi-Tenanant Objects """
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise centurion_exceptions.ValidationError('You must provide an organization')
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of the item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Organization this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = '+',
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Organization'
|
||||
)
|
||||
|
||||
is_global = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this a global object?',
|
||||
verbose_name = 'Global Object'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null = True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
"""Fetch the models URL
|
||||
|
||||
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
|
||||
|
||||
Args:
|
||||
request (object, optional): The request object that was made by the end user. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
|
||||
"""
|
||||
|
||||
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.clean()
|
||||
|
||||
if(
|
||||
not getattr(self, 'organization', None)
|
||||
and self._meta.model_name !='appsettingshistory' # App Settings for
|
||||
):
|
||||
|
||||
raise centurion_exceptions.ValidationError(
|
||||
detail = {
|
||||
'organization': 'Organization is required'
|
||||
},
|
||||
code = 'required'
|
||||
)
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
@ -2,7 +2,7 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.organization import Organization
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
@ -60,6 +60,13 @@ class OrganizationModelSerializer(
|
||||
'model_pk': item.pk
|
||||
}
|
||||
),
|
||||
'notes': reverse(
|
||||
"v2:_api_v2_organization_note-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'model_id': item.pk
|
||||
}
|
||||
),
|
||||
'teams': reverse("v2:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}),
|
||||
}
|
||||
|
||||
|
48
app/access/serializers/organization_notes.py
Normal file
48
app/access/serializers/organization_notes.py
Normal file
@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core.serializers.model_notes import (
|
||||
ModelNotes,
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class OrganizationNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class OrganizationNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
OrganizationNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
48
app/access/serializers/team_notes.py
Normal file
48
app/access/serializers/team_notes.py
Normal file
@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core.serializers.model_notes import (
|
||||
ModelNotes,
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TeamNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TeamNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class TeamNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
TeamNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
@ -2,7 +2,7 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import TeamUsers
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
@ -53,9 +53,15 @@ class TeamUserModelSerializer(
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
return {
|
||||
'_self': item.get_url( request = self._context['view'].request )
|
||||
}
|
||||
get_url = super().get_url( item = item )
|
||||
|
||||
del get_url['history']
|
||||
|
||||
del get_url['knowledge_base']
|
||||
|
||||
del get_url['notes']
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
class Meta:
|
||||
|
@ -2,7 +2,7 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import Team
|
||||
from access.models.team import Team
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
@ -61,16 +61,9 @@ class TeamModelSerializer(
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
return {
|
||||
'_self': item.get_url( request = self._context['view'].request ),
|
||||
'knowledge_base': reverse(
|
||||
"v2:_api_v2_model_kb-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'model': self.Meta.model._meta.model_name,
|
||||
'model_pk': item.pk
|
||||
}
|
||||
),
|
||||
get_url = super().get_url( item = item )
|
||||
|
||||
get_url.update({
|
||||
'users': reverse(
|
||||
'v2:_api_v2_organization_team_user-list',
|
||||
request=self.context['view'].request,
|
||||
@ -79,7 +72,10 @@ class TeamModelSerializer(
|
||||
'team_id': item.pk
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
team_name = centurion_field.CharField( autolink = True )
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from access.models import TenancyManager
|
||||
from access.models.tenancy import TenancyManager
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.organization_history import Organization, OrganizationHistory
|
||||
|
||||
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
|
||||
|
||||
|
||||
|
||||
class History(
|
||||
HistoryEntriesCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = Organization
|
||||
|
||||
history_model = OrganizationHistory
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.obj = self.model.objects.create(
|
||||
name = self.field_value_original,
|
||||
)
|
||||
|
||||
self.obj_delete = self.model.objects.create(
|
||||
name = self.field_value_delete,
|
||||
)
|
||||
|
||||
self.call_the_banners()
|
@ -4,12 +4,14 @@ import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_permissions_viewset import APIPermissions
|
||||
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
|
||||
|
@ -0,0 +1,116 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.viewsets.organization_notes import ViewSet
|
||||
|
||||
from core.tests.abstract.test_functional_notes_viewset import (
|
||||
ModelNotesViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
ModelNotesPermissionsAPI,
|
||||
ModelNotesSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetBase(
|
||||
ModelNotesViewSetBase
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
url_name = '_api_v2_organization_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.item = self.viewset.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.other_org_item = self.viewset.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.different_organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.global_org_item = self.viewset.model.objects.create(
|
||||
organization = self.global_organization,
|
||||
content = 'a random comment global_organization',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.global_organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.url_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesPermissionsAPI(
|
||||
ViewSetBase,
|
||||
ModelNotesPermissionsAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||
"""Check items returned
|
||||
|
||||
This test case is a over-ride of a test case with the same name.
|
||||
This model is not a global model making this test not-applicable.
|
||||
|
||||
Items returned from the query Must be from the users organization and
|
||||
global ONLY!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesSerializer(
|
||||
ViewSetBase,
|
||||
ModelNotesSerializer,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesMetadata(
|
||||
ViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,53 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
|
||||
class OrganizationNotesAPI(
|
||||
ModelNotesNotesAPIFields,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
view_name: str = '_api_v2_organization_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Call parent setup
|
||||
2. Create a model note
|
||||
3. add url kwargs
|
||||
4. make the API request
|
||||
|
||||
"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = Organization.objects.create(
|
||||
name = 'dev'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.pk
|
||||
}
|
||||
|
||||
self.make_request()
|
37
app/access/tests/functional/team/test_team_history.py
Normal file
37
app/access/tests/functional/team/test_team_history.py
Normal file
@ -0,0 +1,37 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.team_history import Team, TeamHistory
|
||||
|
||||
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
|
||||
|
||||
|
||||
|
||||
class History(
|
||||
HistoryEntriesCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = Team
|
||||
|
||||
history_model = TeamHistory
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.field_name = 'team_name'
|
||||
|
||||
self.obj = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
# name = self.field_value_original,
|
||||
team_name = self.field_value_original
|
||||
)
|
||||
|
||||
self.obj_delete = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = self.field_value_delete,
|
||||
)
|
||||
|
||||
self.call_the_banners()
|
@ -4,13 +4,16 @@ import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
|
||||
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
|
||||
from api.tests.abstract.api_permissions_viewset import APIPermissions
|
||||
|
@ -1,13 +1,14 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.middleware.request import Tenancy
|
||||
from access.models import Organization, Permission
|
||||
|
||||
from access.models.organization import Organization
|
||||
|
||||
from access.serializers.teams import (
|
||||
Team,
|
||||
|
@ -0,0 +1,127 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.viewsets.team_notes import ViewSet
|
||||
|
||||
from core.tests.abstract.test_functional_notes_viewset import (
|
||||
ModelNotesViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
ModelNotesPermissionsAPI,
|
||||
ModelNotesSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetBase(
|
||||
ModelNotesViewSetBase
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
url_name = '_api_v2_organization_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.item = self.viewset.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.other_org_item = self.viewset.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.different_organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.global_org_item = self.viewset.model.objects.create(
|
||||
organization = self.global_organization,
|
||||
content = 'a random comment global_organization',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.global_organization,
|
||||
name = 'note model global_organization'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.url_kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': self.item.model.pk,
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesPermissionsAPI(
|
||||
ViewSetBase,
|
||||
ModelNotesPermissionsAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||
"""Check items returned
|
||||
|
||||
This test case is a over-ride of a test case with the same name.
|
||||
This model is not a global model making this test not-applicable.
|
||||
|
||||
Items returned from the query Must be from the users organization and
|
||||
global ONLY!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesSerializer(
|
||||
ViewSetBase,
|
||||
ModelNotesSerializer,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesMetadata(
|
||||
ViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,56 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models.team import Team
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
|
||||
|
||||
class TeamNotesAPI(
|
||||
ModelNotesNotesAPIFields,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
view_name: str = '_api_v2_organization_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Call parent setup
|
||||
2. Create a model note
|
||||
3. add url kwargs
|
||||
4. make the API request
|
||||
|
||||
"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = Team.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'organization_id': self.organization.pk,
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.pk
|
||||
}
|
||||
|
||||
self.make_request()
|
@ -4,13 +4,15 @@ import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
|
||||
from api.tests.abstract.api_permissions_viewset import APIPermissions
|
||||
|
@ -1,12 +1,13 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.models import Organization, Permission, Team
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
from access.serializers.team_user import (
|
||||
TeamUsers,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.auth.models import User, Permission, AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
@ -13,7 +13,10 @@ from api.viewsets.common import ModelViewSet
|
||||
|
||||
from access.mixins.organization import OrganizationMixin
|
||||
from access.mixins.permissions import OrganizationPermissionMixin
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
from core.models.manufacturer import Manufacturer
|
||||
@ -429,7 +432,7 @@ class HasPermission(
|
||||
|
||||
|
||||
@patch.object(GenericAPIView, 'get_object')
|
||||
def test_action_has_permission_function_get_object_called_once(self, get_object):
|
||||
def test_action_has_permission_function_get_object_called_none(self, get_object):
|
||||
|
||||
get_object.return_value = self.obj
|
||||
|
||||
@ -464,7 +467,7 @@ class HasPermission(
|
||||
|
||||
OrganizationPermissionMixin().has_permission(request = view.request, view = view)
|
||||
|
||||
assert get_object.call_count == 1
|
||||
assert get_object.call_count == 0
|
||||
|
||||
|
||||
|
||||
@ -1331,7 +1334,7 @@ class HasPermissionDifferentOrganization(
|
||||
):
|
||||
|
||||
@patch.object(GenericAPIView, 'get_object')
|
||||
def test_action_has_permission_different_org_get_object_called_once(self, get_object):
|
||||
def test_action_has_permission_different_org_get_object_called_none(self, get_object):
|
||||
|
||||
get_object.return_value = self.obj
|
||||
|
||||
@ -1365,7 +1368,7 @@ class HasPermissionDifferentOrganization(
|
||||
|
||||
OrganizationPermissionMixin().has_permission(request = view.request, view = view)
|
||||
|
||||
assert get_object.call_count == 1
|
||||
assert get_object.call_count == 0
|
||||
|
||||
|
||||
|
||||
|
@ -2,14 +2,17 @@ import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -2,14 +2,17 @@ import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
|
||||
from api.tests.abstract.api_fields import APICommonFields
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
@ -9,7 +9,9 @@ import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionChange, OrganizationManagerModelPermissionView
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissionsView, ModelPermissionsChange
|
||||
|
@ -2,12 +2,14 @@ import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissionChange, APIPermissionView
|
||||
|
||||
|
@ -6,5 +6,8 @@ import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from access.models import Organization, Team
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
|
@ -1,187 +0,0 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
|
||||
|
||||
class OrganizationHistory(TestCase):
|
||||
|
||||
model = Organization
|
||||
|
||||
model_name = 'organization'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
name = 'test_item_' + self.model_name,
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = int(History.Actions.ADD),
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.name = 'test_item_' + self.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = int(History.Actions.UPDATE),
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
name = 'test_item_delete_' + self.model_name,
|
||||
)
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.filter(
|
||||
item_pk = self.item_delete.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.item_delete.pk,
|
||||
item_parent_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_action(self):
|
||||
""" Ensure action is "add" for item creation """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['action'] == int(History.Actions.ADD)
|
||||
# assert type(history['action']) is int
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_entry_item_add_field_after(self):
|
||||
""" Ensure after field contains correct value """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['after'] == str('{}')
|
||||
# assert type(history['after']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_before(self):
|
||||
""" Ensure before field is an empty JSON string for create """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['before'] == str('{}')
|
||||
# assert type(history['before']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_item_pk(self):
|
||||
""" Ensure history entry field item_pk is the created items pk """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['item_pk'] == self.item_create.pk
|
||||
# assert type(history['item_pk']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_add_field_item_class(self):
|
||||
""" Ensure history entry field item_class is the model name """
|
||||
|
||||
history = self.history_create.__dict__
|
||||
|
||||
assert history['item_class'] == self.model._meta.model_name
|
||||
# assert type(history['item_class']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
################################## Change ##################################
|
||||
|
||||
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_action(self):
|
||||
""" Ensure action is "add" for item creation """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['action'] == int(History.Actions.UPDATE)
|
||||
# assert type(history['action']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_after(self):
|
||||
""" Ensure after field contains correct value """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['after'] == str('{"name": "test_item_' + self.model_name + '_changed"}')
|
||||
# assert type(history['after']) is str
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_history_entry_item_change_field_before(self):
|
||||
""" Ensure before field is an empty JSON string for create """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['before'] == str('{}')
|
||||
# assert type(history['before']) is str
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_item_pk(self):
|
||||
""" Ensure history entry field item_pk is the created items pk """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['item_pk'] == self.item_create.pk
|
||||
# assert type(history['item_pk']) is int
|
||||
|
||||
|
||||
def test_history_entry_item_change_field_item_class(self):
|
||||
""" Ensure history entry field item_class is the model name """
|
||||
|
||||
history = self.history_change.__dict__
|
||||
|
||||
assert history['item_class'] == self.model._meta.model_name
|
||||
# assert type(history['item_class']) is str
|
||||
|
||||
|
||||
|
||||
|
||||
################################## Delete ##################################
|
||||
|
||||
|
||||
|
||||
|
||||
def test_device_history_entry_delete(self):
|
||||
""" When an item is deleted, it's history entries must be removed """
|
||||
|
||||
assert self.history_delete.exists() is False
|
||||
|
||||
|
||||
def test_device_history_entry_children_delete(self):
|
||||
""" When an item is deleted, it's history entries must be removed """
|
||||
|
||||
assert self.history_delete_children.exists() is False
|
||||
|
||||
|
@ -1,165 +0,0 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
|
||||
class OrganizationHistoryPermissions(TestCase):
|
||||
|
||||
|
||||
item_model = Organization
|
||||
|
||||
|
||||
model = History
|
||||
|
||||
model_name = 'history'
|
||||
|
||||
app_label = 'core'
|
||||
|
||||
namespace = ''
|
||||
|
||||
name_view = '_history'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.organization
|
||||
|
||||
self.history_model_name = self.item._meta.model_name
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_auth_view_history_user_anon_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302 and response.url.startswith('/account/login')
|
||||
|
||||
|
||||
def test_auth_view_history_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_different_organizaiton_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_has_permission(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
@ -0,0 +1,66 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.viewsets.organization import ViewSet
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetModel
|
||||
|
||||
|
||||
|
||||
class ViewsetCommon(
|
||||
ViewSetModel,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
route_name = 'API:_api_v2_organization'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization
|
||||
3. create super user
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||
|
||||
self.kwargs = {}
|
||||
|
||||
|
||||
class OrganizationViewsetList(
|
||||
ViewsetCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. make list request
|
||||
"""
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list'
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
@ -0,0 +1,40 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_history_api_v2 import PrimaryModelHistoryAPI
|
||||
|
||||
from access.models.organization_history import Organization, OrganizationHistory
|
||||
|
||||
|
||||
|
||||
class ModelHistoryAPI(
|
||||
PrimaryModelHistoryAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
audit_model = Organization
|
||||
|
||||
model = OrganizationHistory
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.audit_object = self.organization
|
||||
|
||||
self.history_entry = self.model.objects.create(
|
||||
organization = self.audit_object,
|
||||
action = self.model.Actions.ADD,
|
||||
user = self.view_user,
|
||||
before = {},
|
||||
after = {},
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.audit_object._meta.app_label,
|
||||
model = self.audit_object._meta.model_name,
|
||||
),
|
||||
model = self.audit_object,
|
||||
)
|
||||
|
||||
|
||||
self.make_request()
|
@ -0,0 +1,36 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_notes_model import ModelNotesModel
|
||||
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
|
||||
|
||||
class OrganizationNotesModel(
|
||||
ModelNotesModel,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment for an exiting item',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.model.model.field.related_model.objects.create(
|
||||
name = 'note model existing item',
|
||||
),
|
||||
created_by = self.user,
|
||||
)
|
@ -0,0 +1,57 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_notes_serializer import ModelNotesSerializerTestCases
|
||||
|
||||
from access.serializers.organization_notes import OrganizationNotes, OrganizationNoteModelSerializer
|
||||
|
||||
|
||||
|
||||
class OrganizationNotesSerializer(
|
||||
ModelNotesSerializerTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
model_serializer = OrganizationNoteModelSerializer
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.note_model = self.model.model.field.related_model.objects.create(
|
||||
name = 'note model',
|
||||
)
|
||||
|
||||
self.note_model_two = self.model.model.field.related_model.objects.create(
|
||||
name = 'note model two',
|
||||
)
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment for an exiting item',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.model.model.field.related_model.objects.create(
|
||||
name = 'note model existing item',
|
||||
),
|
||||
created_by = self.user_two,
|
||||
)
|
||||
|
||||
|
||||
self.valid_data = {
|
||||
'organization': self.organization_two.id,
|
||||
'content': 'a random comment',
|
||||
'content_type': self.content_type_two.id,
|
||||
'model': self.note_model_two.id,
|
||||
'created_by': self.user_two.id,
|
||||
'modified_by': self.user_two.id,
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.viewsets.team_notes import ViewSet
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetModel
|
||||
|
||||
|
||||
|
||||
class ViewsetCommon(
|
||||
ViewSetModel,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
route_name = 'v2:_api_v2_organization_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization
|
||||
3. create super user
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||
|
||||
|
||||
|
||||
|
||||
class OrganizationNotesViewsetList(
|
||||
ViewsetCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create object that is to be tested against
|
||||
2. add kwargs
|
||||
3. make list request
|
||||
"""
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.kwargs = {
|
||||
'model_id': self.organization.id,
|
||||
}
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list',
|
||||
kwargs = self.kwargs
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
@ -1,9 +1,12 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from app.tests.abstract.models import TenancyModel
|
||||
|
||||
@ -34,22 +37,22 @@ class TeamModel(
|
||||
)
|
||||
|
||||
|
||||
def test_model_has_property_parent_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
# def test_model_has_property_parent_object(self):
|
||||
# """ Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
# This is a required property for all models that have a parent
|
||||
# """
|
||||
|
||||
assert hasattr(self.model, 'parent_object')
|
||||
# assert hasattr(self.model, 'parent_object')
|
||||
|
||||
|
||||
def test_model_property_parent_object_returns_object(self):
|
||||
""" Check if model contains 'parent_object'
|
||||
# def test_model_property_parent_object_returns_object(self):
|
||||
# """ Check if model contains 'parent_object'
|
||||
|
||||
This is a required property for all models that have a parent
|
||||
"""
|
||||
# This is a required property for all models that have a parent
|
||||
# """
|
||||
|
||||
assert self.item.parent_object is self.parent_item
|
||||
# assert self.item.parent_object is self.parent_item
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
|
@ -4,14 +4,16 @@ import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
# from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_fields import APITenancyObject
|
||||
|
||||
|
@ -1,79 +0,0 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
|
||||
|
||||
from access.models import Team
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
|
||||
|
||||
class TeamHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
|
||||
|
||||
|
||||
model = Team
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = organization
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
team_name = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = int(History.Actions.ADD),
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.team_name = 'test_item_' + self.model._meta.model_name + '_changed'
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"name": "test_org_' + self.item_change.team_name + '", "team_name": "' + self.item_change.team_name + '"}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = int(History.Actions.UPDATE),
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
debug = Group.objects.all()
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
team_name = 'test_item_delete_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.get(
|
||||
action = int(History.Actions.DELETE),
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
@ -1,168 +0,0 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
|
||||
from core.models.history import History
|
||||
|
||||
|
||||
class TeamHistoryPermissions(TestCase):
|
||||
|
||||
|
||||
item_model = Team
|
||||
|
||||
|
||||
model = History
|
||||
|
||||
model_name = 'history'
|
||||
|
||||
app_label = 'core'
|
||||
|
||||
namespace = ''
|
||||
|
||||
name_view = '_history'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
2. create an organization that is different to item
|
||||
3. Create a device
|
||||
4. Add history device history entry as item
|
||||
5. create a user
|
||||
6. create user in different organization (with the required permission)
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.item = self.item_model.objects.create(
|
||||
organization=organization,
|
||||
name = 'deviceone'
|
||||
)
|
||||
|
||||
self.history_model_name = self.item._meta.model_name
|
||||
|
||||
self.history = self.model.objects.get(
|
||||
item_pk = self.item.id,
|
||||
item_class = self.item._meta.model_name,
|
||||
action = self.model.Actions.ADD,
|
||||
)
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.app_label,
|
||||
model = self.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_auth_view_history_user_anon_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as anon user
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 302 and response.url.startswith('/account/login')
|
||||
|
||||
|
||||
def test_auth_view_history_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.no_permissions_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_different_organizaiton_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view with user from different organization
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.different_organization_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
def test_auth_view_history_has_permission(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
Attempt to view as user with view permission
|
||||
"""
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.namespace + self.name_view, kwargs={'model_name': self.history_model_name, 'model_pk': self.item.id})
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.get(url)
|
||||
|
||||
assert response.status_code == 200
|
@ -1,6 +1,6 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
@ -9,7 +9,9 @@ import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissions
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissions
|
||||
|
@ -4,11 +4,13 @@ import requests
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_permissions import APIPermissions
|
||||
|
||||
|
43
app/access/tests/unit/team/test_unit_team_history_api_v2.py
Normal file
43
app/access/tests/unit/team/test_unit_team_history_api_v2.py
Normal file
@ -0,0 +1,43 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_history_api_v2 import PrimaryModelHistoryAPI
|
||||
|
||||
from access.models.team_history import Team, TeamHistory
|
||||
|
||||
|
||||
|
||||
class ModelHistoryAPI(
|
||||
PrimaryModelHistoryAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
audit_model = Team
|
||||
|
||||
model = TeamHistory
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.audit_object = self.audit_model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
)
|
||||
|
||||
self.history_entry = self.model.objects.create(
|
||||
organization = self.audit_object.organization,
|
||||
action = self.model.Actions.ADD,
|
||||
user = self.view_user,
|
||||
before = {},
|
||||
after = {},
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.audit_object._meta.app_label,
|
||||
model = self.audit_object._meta.model_name,
|
||||
),
|
||||
model = self.audit_object,
|
||||
)
|
||||
|
||||
|
||||
self.make_request()
|
67
app/access/tests/unit/team/test_unit_team_viewset.py
Normal file
67
app/access/tests/unit/team/test_unit_team_viewset.py
Normal file
@ -0,0 +1,67 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.viewsets.team import ViewSet
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetModel
|
||||
|
||||
|
||||
|
||||
class ViewsetCommon(
|
||||
ViewSetModel,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
route_name = 'API:_api_v2_organization_team'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization
|
||||
3. create super user
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||
|
||||
self.kwargs = { 'organization_id': self.organization.id }
|
||||
|
||||
|
||||
|
||||
class TeamViewsetList(
|
||||
ViewsetCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. make list request
|
||||
"""
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list',
|
||||
kwargs = self.kwargs
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
@ -0,0 +1,37 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_notes_model import ModelNotesModel
|
||||
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
|
||||
|
||||
class TeamNotesModel(
|
||||
ModelNotesModel,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment for an exiting item',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model existing item',
|
||||
),
|
||||
created_by = self.user,
|
||||
)
|
@ -0,0 +1,60 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.test_unit_model_notes_serializer import ModelNotesSerializerTestCases
|
||||
|
||||
from access.serializers.team_notes import TeamNotes, TeamNoteModelSerializer
|
||||
|
||||
|
||||
|
||||
class TeamNotesSerializer(
|
||||
ModelNotesSerializerTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
model_serializer = TeamNoteModelSerializer
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.note_model = self.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
team_name = 'note model',
|
||||
)
|
||||
|
||||
self.note_model_two = self.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
team_name = 'note model two',
|
||||
)
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment for an exiting item',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
team_name = 'note model existing item',
|
||||
),
|
||||
created_by = self.user_two,
|
||||
)
|
||||
|
||||
|
||||
self.valid_data = {
|
||||
'organization': self.organization_two.id,
|
||||
'content': 'a random comment',
|
||||
'content_type': self.content_type_two.id,
|
||||
'model': self.note_model_two.id,
|
||||
'created_by': self.user_two.id,
|
||||
'modified_by': self.user_two.id,
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.viewsets.team_notes import ViewSet
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetModel
|
||||
|
||||
|
||||
|
||||
class ViewsetCommon(
|
||||
ViewSetModel,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
route_name = 'v2:_api_v2_organization_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization
|
||||
3. create super user
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||
|
||||
|
||||
|
||||
|
||||
class TeamNotesViewsetList(
|
||||
ViewsetCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create object that is to be tested against
|
||||
2. add kwargs
|
||||
3. make list request
|
||||
"""
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.note_model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model'
|
||||
)
|
||||
|
||||
self.kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': self.note_model.pk,
|
||||
}
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list',
|
||||
kwargs = self.kwargs
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
@ -2,9 +2,11 @@ import pytest
|
||||
import unittest
|
||||
|
||||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from app.tests.abstract.models import BaseModel
|
||||
|
||||
|
@ -1,13 +1,15 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.relations import Hyperlink
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_fields import APICommonFields
|
||||
|
||||
|
@ -1,92 +0,0 @@
|
||||
|
||||
import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from core.models.history import History
|
||||
from core.tests.abstract.history_entry import HistoryEntry
|
||||
from core.tests.abstract.history_entry_child_model import HistoryEntryChildItem
|
||||
|
||||
from access.models import Team, TeamUsers
|
||||
|
||||
|
||||
|
||||
class TeamUsersHistory(TestCase, HistoryEntry, HistoryEntryChildItem):
|
||||
|
||||
model = TeamUsers
|
||||
|
||||
model_name = 'teamusers'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
""" Setup Test """
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.item_parent = Team.objects.create(
|
||||
team_name = 'test_item_' + self.model._meta.model_name,
|
||||
organization = self.organization
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
username = 'test_item_' + self.model._meta.model_name,
|
||||
password = 'a random password'
|
||||
)
|
||||
|
||||
self.item_create = self.model.objects.create(
|
||||
user = self.user,
|
||||
team = self.item_parent
|
||||
)
|
||||
|
||||
|
||||
self.history_create = History.objects.get(
|
||||
action = int(History.Actions.ADD),
|
||||
item_pk = self.item_create.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.item_change = self.item_create
|
||||
self.item_change.manager = True
|
||||
self.item_change.save()
|
||||
|
||||
self.field_after_expected_value = '{"manager": true}'
|
||||
|
||||
self.history_change = History.objects.get(
|
||||
action = int(History.Actions.UPDATE),
|
||||
item_pk = self.item_change.pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
|
||||
self.user_delete = User.objects.create(
|
||||
username = 'test_item_delete' + self.model._meta.model_name,
|
||||
password = 'a random password'
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
user = self.user_delete,
|
||||
team = self.item_parent
|
||||
)
|
||||
|
||||
self.deleted_pk = self.item_delete.pk
|
||||
|
||||
self.item_delete.delete()
|
||||
|
||||
self.history_delete = History.objects.get(
|
||||
action = int(History.Actions.DELETE),
|
||||
item_pk = self.deleted_pk,
|
||||
item_class = self.model._meta.model_name,
|
||||
)
|
||||
|
||||
self.history_delete_children = History.objects.filter(
|
||||
item_parent_pk = self.deleted_pk,
|
||||
item_parent_class = self.item_parent._meta.model_name,
|
||||
)
|
@ -6,7 +6,9 @@ import unittest
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import ValidationError
|
||||
from access.models import Organization, Team
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
# @pytest.fixture
|
||||
|
@ -1,6 +1,6 @@
|
||||
# from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
@ -9,7 +9,10 @@ import pytest
|
||||
import unittest
|
||||
import requests
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from access.tests.abstract.model_permissions_organization_manager import OrganizationManagerModelPermissionAdd, OrganizationManagerModelPermissionDelete
|
||||
|
||||
from app.tests.abstract.model_permissions import ModelPermissionsAdd, ModelPermissionsChange, ModelPermissionsDelete
|
||||
|
76
app/access/tests/unit/team_user/test_team_user_viewset.py
Normal file
76
app/access/tests/unit/team_user/test_team_user_viewset.py
Normal file
@ -0,0 +1,76 @@
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.viewsets.team_user import ViewSet
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetModel
|
||||
|
||||
|
||||
|
||||
class ViewsetCommon(
|
||||
ViewSetModel,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
route_name = 'API:_api_v2_organization_team_user'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization
|
||||
3. create super user
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.team = Team.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'team'
|
||||
)
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_view_user", password="password", is_superuser=True)
|
||||
|
||||
self.kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'team_id': self.team.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
class TeamUserViewsetList(
|
||||
ViewsetCommon,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. make list request
|
||||
"""
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
client = Client()
|
||||
|
||||
url = reverse(
|
||||
self.route_name + '-list',
|
||||
kwargs = self.kwargs
|
||||
)
|
||||
|
||||
client.force_login(self.view_user)
|
||||
|
||||
self.http_options_response_list = client.options(url)
|
@ -3,7 +3,8 @@ import unittest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models import TenancyObject, TenancyManager
|
||||
from access.models.tenancy import TenancyManager
|
||||
from access.models.tenancy import TenancyObject
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
@ -4,7 +4,7 @@ from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.organization import Organization
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetCommon
|
||||
|
||||
@ -43,6 +43,8 @@ class AccessViewset(
|
||||
client.force_login(self.view_user)
|
||||
self.http_options_response_list = client.options(url)
|
||||
|
||||
self.kwargs = {}
|
||||
|
||||
|
||||
|
||||
def test_view_attr_permission_classes_value(self):
|
||||
|
@ -4,7 +4,7 @@ from django.utils.decorators import method_decorator
|
||||
from django.views import generic
|
||||
|
||||
from access.mixin import *
|
||||
from access.models import *
|
||||
from access.models.organization import Organization
|
||||
|
||||
from access.forms.organization import OrganizationForm
|
||||
|
||||
|
@ -4,8 +4,10 @@ from django.utils.decorators import method_decorator
|
||||
from django.urls import reverse
|
||||
|
||||
from access.forms.team import TeamForm, TeamFormAdd
|
||||
from access.models import Team, TeamUsers, Organization
|
||||
from access.mixin import *
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from core.views.common import AddView, ChangeView, DeleteView
|
||||
|
||||
|
@ -2,11 +2,13 @@ from django.contrib.auth import decorators as auth_decorator
|
||||
from django.urls import reverse
|
||||
|
||||
from access.forms.team_users import TeamUsersForm
|
||||
from access.models import Team, TeamUsers
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from core.views.common import AddView, DeleteView
|
||||
|
||||
|
||||
|
||||
class Add(AddView):
|
||||
|
||||
context_object_name = "teamuser"
|
||||
|
@ -1,5 +1,7 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
# THis import only exists so that the migrations can be created
|
||||
from access.models.organization_history import OrganizationHistory # pylint: disable=W0611:unused-import
|
||||
from access.serializers.organization import (
|
||||
Organization,
|
||||
OrganizationModelSerializer,
|
||||
@ -75,13 +77,22 @@ class ViewSet( ModelViewSet ):
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.serializer_class is not None:
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
if (
|
||||
self.action == 'list'
|
||||
or self.action == 'retrieve'
|
||||
):
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
|
||||
|
||||
else:
|
||||
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']
|
||||
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']
|
||||
return self.serializer_class
|
||||
|
||||
|
65
app/access/viewsets/organization_notes.py
Normal file
65
app/access/viewsets/organization_notes.py
Normal file
@ -0,0 +1,65 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
from access.serializers.organization_notes import (
|
||||
OrganizationNotes,
|
||||
OrganizationNoteModelSerializer,
|
||||
OrganizationNoteViewSerializer,
|
||||
)
|
||||
|
||||
from core.viewsets.model_notes import ModelNoteViewSet
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
summary = 'Add a note to an organization',
|
||||
description = '',
|
||||
responses = {
|
||||
201: OpenApiResponse(description='created', response=OrganizationNoteViewSerializer),
|
||||
400: OpenApiResponse(description='Validation failed.'),
|
||||
403: OpenApiResponse(description='User is missing create permissions'),
|
||||
}
|
||||
),
|
||||
destroy = extend_schema(
|
||||
summary = 'Delete an organization note',
|
||||
description = ''
|
||||
),
|
||||
list = extend_schema(
|
||||
summary = 'Fetch all organization notes',
|
||||
description='',
|
||||
),
|
||||
retrieve = extend_schema(
|
||||
summary = 'Fetch a single organization note',
|
||||
description='',
|
||||
),
|
||||
update = extend_schema(exclude = True),
|
||||
partial_update = extend_schema(
|
||||
summary = 'Update an organization note',
|
||||
description = ''
|
||||
),
|
||||
)
|
||||
class ViewSet(ModelNoteViewSet):
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.serializer_class is not None:
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
if (
|
||||
self.action == 'list'
|
||||
or self.action == 'retrieve'
|
||||
):
|
||||
|
||||
self.serializer_class = OrganizationNoteViewSerializer
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self.serializer_class = OrganizationNoteModelSerializer
|
||||
|
||||
return self.serializer_class
|
@ -1,7 +1,8 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse
|
||||
|
||||
from access.models import Organization
|
||||
|
||||
from access.models.organization import Organization
|
||||
# THis import only exists so that the migrations can be created
|
||||
from access.models.team_history import TeamHistory # pylint: disable=W0611:unused-import
|
||||
from access.serializers.teams import (
|
||||
Team,
|
||||
TeamModelSerializer,
|
||||
@ -144,6 +145,10 @@ class ViewSet( ModelViewSet ):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
if self.queryset is not None:
|
||||
|
||||
return self.queryset
|
||||
|
||||
queryset = super().get_queryset()
|
||||
|
||||
queryset = queryset.filter(organization_id=self.kwargs['organization_id'])
|
||||
@ -154,15 +159,22 @@ class ViewSet( ModelViewSet ):
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.serializer_class is not None:
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
if (
|
||||
self.action == 'list'
|
||||
or self.action == 'retrieve'
|
||||
):
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name) + 'ViewSerializer']
|
||||
|
||||
else:
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name) + 'ModelSerializer']
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
def get_return_url(self) -> str:
|
||||
|
65
app/access/viewsets/team_notes.py
Normal file
65
app/access/viewsets/team_notes.py
Normal file
@ -0,0 +1,65 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse
|
||||
|
||||
from access.serializers.team_notes import (
|
||||
TeamNotes,
|
||||
TeamNoteModelSerializer,
|
||||
TeamNoteViewSerializer,
|
||||
)
|
||||
|
||||
from core.viewsets.model_notes import ModelNoteViewSet
|
||||
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
create=extend_schema(
|
||||
summary = 'Add a note to a Team',
|
||||
description = '',
|
||||
responses = {
|
||||
201: OpenApiResponse(description='created', response=TeamNoteViewSerializer),
|
||||
400: OpenApiResponse(description='Validation failed.'),
|
||||
403: OpenApiResponse(description='User is missing create permissions'),
|
||||
}
|
||||
),
|
||||
destroy = extend_schema(
|
||||
summary = 'Delete a team note',
|
||||
description = ''
|
||||
),
|
||||
list = extend_schema(
|
||||
summary = 'Fetch all team notes',
|
||||
description='',
|
||||
),
|
||||
retrieve = extend_schema(
|
||||
summary = 'Fetch a single team note',
|
||||
description='',
|
||||
),
|
||||
update = extend_schema(exclude = True),
|
||||
partial_update = extend_schema(
|
||||
summary = 'Update a team note',
|
||||
description = ''
|
||||
),
|
||||
)
|
||||
class ViewSet(ModelNoteViewSet):
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.serializer_class is not None:
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
if (
|
||||
self.action == 'list'
|
||||
or self.action == 'retrieve'
|
||||
):
|
||||
|
||||
self.serializer_class = TeamNoteViewSerializer
|
||||
|
||||
|
||||
else:
|
||||
|
||||
self.serializer_class = TeamNoteModelSerializer
|
||||
|
||||
return self.serializer_class
|
@ -1,6 +1,6 @@
|
||||
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiResponse
|
||||
|
||||
from access.models import Team
|
||||
from access.models.team import Team
|
||||
|
||||
from access.serializers.team_user import (
|
||||
TeamUsers,
|
||||
@ -171,28 +171,39 @@ class ViewSet( ModelViewSet ):
|
||||
|
||||
def get_queryset(self):
|
||||
|
||||
queryset = super().get_queryset()
|
||||
if self.queryset is not None:
|
||||
|
||||
queryset = queryset.filter(
|
||||
return self.queryset
|
||||
|
||||
self.queryset = super().get_queryset()
|
||||
|
||||
self.queryset = self.queryset.filter(
|
||||
team_id = self.kwargs['team_id']
|
||||
)
|
||||
|
||||
self.queryset = queryset
|
||||
|
||||
return self.queryset
|
||||
|
||||
|
||||
def get_serializer_class(self):
|
||||
|
||||
if self.serializer_class is not None:
|
||||
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
if (
|
||||
self.action == 'list'
|
||||
or self.action == 'retrieve'
|
||||
):
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ViewSerializer']
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ViewSerializer']
|
||||
|
||||
else:
|
||||
|
||||
self.serializer_class = globals()[str( self.model._meta.verbose_name).replace(' ' , '') + 'ModelSerializer']
|
||||
|
||||
|
||||
return globals()[str( self.model._meta.verbose_name).replace(' ', '') + 'ModelSerializer']
|
||||
return self.serializer_class
|
||||
|
||||
|
||||
def get_return_url(self):
|
||||
|
@ -32,7 +32,7 @@ class AuthTokenForm(CommonModelForm):
|
||||
|
||||
if self.prefix + '-gen_token' not in self.data:
|
||||
|
||||
generated_token = self.instance.generate()
|
||||
generated_token = self.instance.generate
|
||||
|
||||
else:
|
||||
|
||||
|
17
app/api/migrations/0003_alter_authtoken_options.py
Normal file
17
app/api/migrations/0003_alter_authtoken_options.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-28 15:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0002_alter_authtoken_expires_alter_authtoken_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='authtoken',
|
||||
options={'ordering': ['expires'], 'verbose_name': 'Auth Token', 'verbose_name_plural': 'Auth Tokens'},
|
||||
),
|
||||
]
|
@ -3,20 +3,34 @@ import random
|
||||
import string
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.db import models
|
||||
from django.db.models import Field
|
||||
from django.forms import ValidationError
|
||||
|
||||
from access.fields import *
|
||||
from access.models import TenancyObject
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import (
|
||||
AutoCreatedField,
|
||||
AutoLastModifiedField
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AuthToken(models.Model):
|
||||
|
||||
|
||||
def validate_note_no_token(self, note, token):
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'expires'
|
||||
]
|
||||
|
||||
verbose_name = 'Auth Token'
|
||||
|
||||
verbose_name_plural = 'Auth Tokens'
|
||||
|
||||
|
||||
|
||||
def validate_note_no_token(self, note, token, raise_exception = True) -> bool:
|
||||
""" Ensure plaintext token cant be saved to notes field.
|
||||
|
||||
called from app.settings.views.user_settings.TokenAdd.form_valid()
|
||||
@ -41,10 +55,12 @@ class AuthToken(models.Model):
|
||||
|
||||
validation = False
|
||||
|
||||
if not validation:
|
||||
if not validation and raise_exception:
|
||||
|
||||
raise ValidationError('Token can not be placed in the notes field.')
|
||||
|
||||
return validation
|
||||
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
@ -95,6 +111,7 @@ class AuthToken(models.Model):
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
@property
|
||||
def generate(self) -> str:
|
||||
|
||||
return str(hashlib.sha256(str(self.randomword()).encode('utf-8')).hexdigest())
|
||||
@ -115,3 +132,33 @@ class AuthToken(models.Model):
|
||||
def __str__(self):
|
||||
|
||||
return self.token
|
||||
|
||||
|
||||
table_fields: list = [
|
||||
'note',
|
||||
'created',
|
||||
'expires',
|
||||
'-action_delete-',
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_user_settings_token-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_user_settings_token-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'model_id': self.user.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ from rest_framework.utils.field_mapping import ClassLookupDict
|
||||
|
||||
from rest_framework_json_api.utils import get_related_resource_type
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.organization import Organization
|
||||
|
||||
from app.serializers.user import User, UserBaseSerializer
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
from rest_framework import serializers, request
|
||||
from rest_framework.reverse import reverse
|
||||
from access.models import Organization, Team
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
|
||||
from django.contrib.auth.models import Permission
|
||||
|
||||
|
136
app/api/serializers/auth_token.py
Normal file
136
app/api/serializers/auth_token.py
Normal file
@ -0,0 +1,136 @@
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.serializers.organization import OrganizationBaseSerializer
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
from api.serializers import common
|
||||
|
||||
from core import exceptions as centurion_exception
|
||||
|
||||
|
||||
|
||||
class AuthTokenBaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
display_name = serializers.SerializerMethodField('get_display_name')
|
||||
|
||||
def get_display_name(self, item) -> str:
|
||||
|
||||
return str( item )
|
||||
|
||||
url = serializers.HyperlinkedIdentityField(
|
||||
view_name="v2:_api_v2_cluster-detail",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthToken
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
class AuthTokenModelSerializer(
|
||||
common.CommonModelSerializer,
|
||||
AuthTokenBaseSerializer
|
||||
):
|
||||
|
||||
_urls = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
get_url = super().get_url( item = item )
|
||||
|
||||
del get_url['history']
|
||||
del get_url['knowledge_base']
|
||||
del get_url['notes']
|
||||
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
expires = serializers.DateTimeField(
|
||||
initial = (datetime.datetime.now(tz=datetime.timezone.utc) + datetime.timedelta(days=90)).replace(microsecond=0).isoformat(),
|
||||
)
|
||||
|
||||
token = serializers.CharField(initial=AuthToken().generate, write_only = True )
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = AuthToken
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'note',
|
||||
'token',
|
||||
'user',
|
||||
'expires',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'user',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
def validate(self, attrs):
|
||||
|
||||
if self.context['request'].user.id != int(self.context['view'].kwargs['model_id']):
|
||||
|
||||
raise centurion_exception.PermissionDenied()
|
||||
|
||||
|
||||
if not self.Meta.model().validate_note_no_token(attrs['note'], attrs['token']):
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
"note": "No more than nine chars of token can be contained within the notes field"
|
||||
},
|
||||
code = 'note_no_contain_token'
|
||||
)
|
||||
|
||||
|
||||
if not re.fullmatch(r'[0-9|a-f]{64}', str(attrs['token']).lower()):
|
||||
|
||||
|
||||
raise centurion_exception.ValidationError(
|
||||
detail = {
|
||||
"token": "Token appears to have been edited."
|
||||
},
|
||||
code = 'token_not_sha256'
|
||||
)
|
||||
|
||||
|
||||
attrs['token'] = self.Meta.model().token_hash(attrs['token'])
|
||||
|
||||
attrs = super().validate(attrs)
|
||||
|
||||
attrs['user'] = self.context['request'].user
|
||||
|
||||
return attrs
|
||||
|
||||
|
||||
|
||||
class AuthTokenViewSerializer(AuthTokenModelSerializer):
|
||||
|
||||
organization = OrganizationBaseSerializer( many = False, read_only = True )
|
@ -1,11 +1,12 @@
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.serializers.organization import Organization, OrganizationBaseSerializer
|
||||
from access.serializers.organization import Organization
|
||||
|
||||
from core import fields as centurion_field
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
|
||||
class OrganizationField(serializers.PrimaryKeyRelatedField):
|
||||
@ -50,4 +51,36 @@ class CommonModelSerializer(CommonBaseSerializer):
|
||||
|
||||
model_notes = centurion_field.MarkdownField( required = False )
|
||||
|
||||
organization = OrganizationField(required = False)
|
||||
organization = OrganizationField(required = False)
|
||||
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
return {
|
||||
'_self': item.get_url( request = self._context['view'].request ),
|
||||
|
||||
'history': reverse(
|
||||
"v2:_api_v2_model_history-list",
|
||||
request = self._context['view'].request,
|
||||
kwargs = {
|
||||
'app_label': self.Meta.model._meta.app_label,
|
||||
'model_name': self.Meta.model._meta.model_name,
|
||||
'model_id': item.pk
|
||||
}
|
||||
),
|
||||
'knowledge_base': reverse(
|
||||
"v2:_api_v2_model_kb-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'model': self.Meta.model._meta.model_name,
|
||||
'model_pk': item.pk
|
||||
}
|
||||
),
|
||||
'notes': reverse(
|
||||
"v2:_api_v2_operating_system_note-list",
|
||||
request = self._context['view'].request,
|
||||
kwargs = {
|
||||
'model_id': item.pk
|
||||
}
|
||||
),
|
||||
}
|
||||
|
@ -1,7 +1,72 @@
|
||||
from django.contrib.auth.models import ContentType, Permission, User
|
||||
|
||||
from unittest.mock import patch, PropertyMock
|
||||
|
||||
from access.mixins.permissions import OrganizationPermissionMixin
|
||||
|
||||
from api.react_ui_metadata import ReactUIMetadata
|
||||
|
||||
from access.middleware.request import Tenancy
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
|
||||
|
||||
class MockRequest:
|
||||
"""Fake Request
|
||||
|
||||
contains the user and tenancy object for permission checks
|
||||
|
||||
Some ViewSets rely upon the request object for obtaining the user and
|
||||
fetching the tenacy object for permission checking.
|
||||
"""
|
||||
|
||||
data = {}
|
||||
|
||||
kwargs = {}
|
||||
|
||||
tenancy: Tenancy = None
|
||||
|
||||
user: User = None
|
||||
|
||||
def __init__(self, user: User, organization: Organization, viewset):
|
||||
|
||||
self.user = user
|
||||
|
||||
view_permission = Permission.objects.get(
|
||||
codename = 'view_' + viewset.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = viewset.model._meta.app_label,
|
||||
model = viewset.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permission])
|
||||
|
||||
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = user
|
||||
)
|
||||
|
||||
|
||||
self.app_settings = AppSettings.objects.select_related('global_organization').get(
|
||||
owner_organization = None
|
||||
)
|
||||
|
||||
self.tenancy = Tenancy(
|
||||
user = user,
|
||||
app_settings = self.app_settings
|
||||
)
|
||||
|
||||
|
||||
|
||||
class AllViewSet:
|
||||
@ -34,6 +99,8 @@ class AllViewSet:
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
|
||||
assert view_set.allowed_methods is not None
|
||||
|
||||
|
||||
@ -45,6 +112,8 @@ class AllViewSet:
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
|
||||
assert type(view_set.allowed_methods) is list
|
||||
|
||||
|
||||
@ -367,7 +436,7 @@ class ModelViewSet(AllViewSet):
|
||||
|
||||
assert (
|
||||
type(view_set.documentation) is str
|
||||
or type(view_set.documentation) is None
|
||||
or view_set.documentation is None
|
||||
)
|
||||
|
||||
|
||||
@ -425,6 +494,8 @@ class ModelViewSet(AllViewSet):
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
|
||||
for method in list(view_set.allowed_methods):
|
||||
|
||||
if method not in valid_values:
|
||||
@ -584,4 +655,165 @@ class ViewSetModel(
|
||||
APIRenderModelViewSet (class): Tests to check API rendering to ensure data is present, includes `APIRenderViewSet` tests.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
def test_view_func_get_queryset_cache_result(self):
|
||||
"""Viewset Test
|
||||
|
||||
Ensure that the `get_queryset` function caches the result under
|
||||
attribute `<viewset>.queryset`
|
||||
"""
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.request = MockRequest(
|
||||
user = self.view_user,
|
||||
organization = self.organization,
|
||||
viewset = self.viewset
|
||||
)
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
|
||||
view_set.action = 'list'
|
||||
|
||||
view_set.detail = False
|
||||
|
||||
assert view_set.queryset is None # Must be empty before init
|
||||
|
||||
q = view_set.get_queryset()
|
||||
|
||||
assert view_set.queryset is not None # Must not be empty after init
|
||||
|
||||
assert q == view_set.queryset
|
||||
|
||||
|
||||
def test_view_func_get_queryset_cache_result_used(self):
|
||||
"""Viewset Test
|
||||
|
||||
Ensure that the `get_queryset` function caches the result under
|
||||
attribute `<viewset>.queryset`
|
||||
"""
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.request = MockRequest(
|
||||
user = self.view_user,
|
||||
organization = self.organization,
|
||||
viewset = self.viewset
|
||||
)
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
view_set.action = 'list'
|
||||
view_set.detail = False
|
||||
|
||||
mock_return = view_set.get_queryset() # Real item to be used as mock return Some
|
||||
# functions use `Queryset` for additional filtering
|
||||
|
||||
setter_not_called = True
|
||||
|
||||
|
||||
with patch.object(self.viewset, 'queryset', new_callable=PropertyMock) as qs:
|
||||
|
||||
qs.return_value = mock_return
|
||||
|
||||
mocked_view_set = self.viewset()
|
||||
|
||||
mocked_view_set.kwargs = self.kwargs
|
||||
mocked_view_set.action = 'list'
|
||||
mocked_view_set.detail = False
|
||||
|
||||
qs.reset_mock() # Just in case
|
||||
|
||||
mocked_setup = mocked_view_set.get_queryset() # should only add two calls, if exists and the return
|
||||
|
||||
|
||||
for mock_call in list(qs.mock_calls): # mock_calls with args means setter was called
|
||||
|
||||
if len(mock_call.args) > 0:
|
||||
|
||||
setter_not_called = False
|
||||
|
||||
|
||||
assert setter_not_called
|
||||
assert qs.call_count == 2
|
||||
|
||||
|
||||
|
||||
def test_view_func_get_serializer_class_cache_result(self):
|
||||
"""Viewset Test
|
||||
|
||||
Ensure that the `get_serializer_class` function caches the result under
|
||||
attribute `<viewset>.serializer_class`
|
||||
"""
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.request = MockRequest(
|
||||
user = self.view_user,
|
||||
organization = self.organization,
|
||||
viewset = self.viewset
|
||||
)
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
|
||||
view_set.action = 'list'
|
||||
|
||||
view_set.detail = False
|
||||
|
||||
assert view_set.serializer_class is None # Must be empty before init
|
||||
|
||||
q = view_set.get_serializer_class()
|
||||
|
||||
assert view_set.serializer_class is not None # Must not be empty after init
|
||||
|
||||
assert q == view_set.serializer_class
|
||||
|
||||
|
||||
def test_view_func_get_serializer_class_cache_result_used(self):
|
||||
"""Viewset Test
|
||||
|
||||
Ensure that the `get_serializer_class` function caches the result under
|
||||
attribute `<viewset>.serializer_class`
|
||||
"""
|
||||
|
||||
view_set = self.viewset()
|
||||
|
||||
view_set.request = MockRequest(
|
||||
user = self.view_user,
|
||||
organization = self.organization,
|
||||
viewset = self.viewset
|
||||
)
|
||||
|
||||
view_set.kwargs = self.kwargs
|
||||
view_set.action = 'list'
|
||||
view_set.detail = False
|
||||
|
||||
mock_return = view_set.get_serializer_class() # Real item to be used as mock return Some
|
||||
# functions use `Queryset` for additional filtering
|
||||
|
||||
setter_not_called = True
|
||||
|
||||
|
||||
with patch.object(self.viewset, 'serializer_class', new_callable=PropertyMock) as qs:
|
||||
|
||||
qs.return_value = mock_return
|
||||
|
||||
mocked_view_set = self.viewset()
|
||||
|
||||
mocked_view_set.kwargs = self.kwargs
|
||||
mocked_view_set.action = 'list'
|
||||
mocked_view_set.detail = False
|
||||
|
||||
qs.reset_mock() # Just in case
|
||||
|
||||
mocked_setup = mocked_view_set.get_serializer_class() # should only add two calls, if exists and the return
|
||||
|
||||
|
||||
for mock_call in list(qs.mock_calls): # mock_calls with args means setter was called
|
||||
|
||||
if len(mock_call.args) > 0:
|
||||
|
||||
setter_not_called = False
|
||||
|
||||
|
||||
assert setter_not_called
|
||||
assert qs.call_count == 2
|
||||
|
@ -0,0 +1,162 @@
|
||||
import pytest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.exceptions import ValidationError, PermissionDenied
|
||||
|
||||
from access.models.organization import Organization
|
||||
|
||||
from api.serializers.auth_token import AuthToken, AuthTokenModelSerializer
|
||||
|
||||
from app.tests.abstract.mock_view import MockView, User
|
||||
|
||||
# from core.serializers.manufacturer import Manufacturer, ManufacturerModelSerializer
|
||||
|
||||
|
||||
|
||||
class ValidationAPI(
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = AuthToken
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an org
|
||||
2. Create an item
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
self.user = User.objects.create_user(username="test_user_view", password="password")
|
||||
|
||||
self.valid_data = {
|
||||
'note': 'a note',
|
||||
'token': self.model().generate,
|
||||
'user': self.user.id,
|
||||
'expires': '2025-02-26T00:09Z'
|
||||
}
|
||||
|
||||
self.mock_view = MockView( user = self.user )
|
||||
|
||||
self.mock_view.kwargs = {
|
||||
'model_id': self.user.id
|
||||
}
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
note = 'object note',
|
||||
token = self.model().generate,
|
||||
user = self.user,
|
||||
expires = '2025-02-26T00:07Z'
|
||||
)
|
||||
|
||||
|
||||
|
||||
def test_serializer_validation_valid_data(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that if creating with valid data, the object is created
|
||||
"""
|
||||
|
||||
serializer = AuthTokenModelSerializer(
|
||||
context = {
|
||||
'request': self.mock_view.request,
|
||||
'view': self.mock_view,
|
||||
},
|
||||
data = self.valid_data
|
||||
)
|
||||
|
||||
assert serializer.is_valid(raise_exception = True)
|
||||
|
||||
|
||||
|
||||
def test_serializer_validation_valid_data_different_user(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that if adding the same manufacturer
|
||||
it raises a validation error
|
||||
"""
|
||||
|
||||
class MockUser:
|
||||
|
||||
id = 99
|
||||
|
||||
mock_view = MockView( user = self.user )
|
||||
|
||||
mock_view.request.user = MockUser()
|
||||
|
||||
mock_view.kwargs = {
|
||||
'model_id': self.user.id
|
||||
}
|
||||
|
||||
with pytest.raises(PermissionDenied) as err:
|
||||
|
||||
serializer = AuthTokenModelSerializer(
|
||||
context = {
|
||||
'request': mock_view.request,
|
||||
'view': mock_view,
|
||||
},
|
||||
data = self.valid_data
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
assert err.value.get_codes() == 'permission_denied'
|
||||
|
||||
|
||||
|
||||
def test_serializer_validation_valid_data_token_not_sha256_same_length(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that if adding the same manufacturer
|
||||
it raises a validation error
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwert'
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = AuthTokenModelSerializer(
|
||||
context = {
|
||||
'request': self.mock_view.request,
|
||||
'view': self.mock_view,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
assert err.value.get_codes()['token'][0] == 'token_not_sha256'
|
||||
|
||||
|
||||
|
||||
def test_serializer_validation_valid_data_token_not_sha256_wrong_length(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that if adding the same manufacturer
|
||||
it raises a validation error
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
valid_data['token'] = str(valid_data['token'])[:-5] + 'qwer'
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = AuthTokenModelSerializer(
|
||||
context = {
|
||||
'request': self.mock_view.request,
|
||||
'view': self.mock_view,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
assert err.value.get_codes()['token'][0] == 'token_not_sha256'
|
@ -0,0 +1,382 @@
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.shortcuts import reverse
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
from api.tests.abstract.api_permissions_viewset import (
|
||||
APIPermissionAdd,
|
||||
APIPermissionDelete,
|
||||
APIPermissionView,
|
||||
)
|
||||
from api.tests.abstract.api_serializer_viewset import (
|
||||
SerializerAdd,
|
||||
SerializerDelete,
|
||||
SerializerView,
|
||||
)
|
||||
from api.tests.abstract.test_metadata_functional import (
|
||||
MetadataAttributesFunctionalEndpoint,
|
||||
MetadataAttributesFunctionalBase,
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
|
||||
model = AuthToken
|
||||
|
||||
app_namespace = 'v2'
|
||||
|
||||
url_name = '_api_v2_user_settings_token'
|
||||
|
||||
change_data = {'device_model_is_global': True}
|
||||
|
||||
delete_data = {}
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Create an organization for user and item
|
||||
. create an organization that is different to item
|
||||
2. Create a team
|
||||
3. create teams with each permission: view, add, change, delete
|
||||
4. create a user per team
|
||||
"""
|
||||
|
||||
organization = Organization.objects.create(name='test_org')
|
||||
|
||||
self.organization = organization
|
||||
|
||||
different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.different_organization = different_organization
|
||||
|
||||
|
||||
view_permissions = Permission.objects.get(
|
||||
codename = 'view_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
view_team = Team.objects.create(
|
||||
team_name = 'view_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
view_team.permissions.set([view_permissions])
|
||||
|
||||
|
||||
|
||||
add_permissions = Permission.objects.get(
|
||||
codename = 'add_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
add_team = Team.objects.create(
|
||||
team_name = 'add_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
add_team.permissions.set([add_permissions])
|
||||
|
||||
|
||||
|
||||
change_permissions = Permission.objects.get(
|
||||
codename = 'change_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
change_team = Team.objects.create(
|
||||
team_name = 'change_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
change_team.permissions.set([change_permissions])
|
||||
|
||||
|
||||
|
||||
delete_permissions = Permission.objects.get(
|
||||
codename = 'delete_' + self.model._meta.model_name,
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = self.model._meta.app_label,
|
||||
model = self.model._meta.model_name,
|
||||
)
|
||||
)
|
||||
|
||||
delete_team = Team.objects.create(
|
||||
team_name = 'delete_team',
|
||||
organization = organization,
|
||||
)
|
||||
|
||||
delete_team.permissions.set([delete_permissions])
|
||||
|
||||
|
||||
self.no_permissions_user = User.objects.create_user(username="test_no_permissions", password="password")
|
||||
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_user
|
||||
)
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
teamuser = TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
note = 'a note',
|
||||
token = self.model().generate,
|
||||
user = self.view_user,
|
||||
expires = '2025-02-25T23:14Z'
|
||||
)
|
||||
|
||||
self.item_delete = self.model.objects.create(
|
||||
note = 'a note',
|
||||
token = self.model().generate,
|
||||
user = self.delete_user,
|
||||
expires = '2025-02-25T23:14Z'
|
||||
)
|
||||
|
||||
# self.item.default_organization = self.organization
|
||||
|
||||
# self.item.save()
|
||||
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'model_id': self.view_user.id,
|
||||
'pk': self.item.id,
|
||||
}
|
||||
|
||||
self.url_kwargs = {
|
||||
'model_id': self.view_user.id,
|
||||
}
|
||||
|
||||
self.add_data = {
|
||||
'note': 'a note',
|
||||
'token': self.model().generate,
|
||||
'expires': '2025-02-26T23:14Z'
|
||||
}
|
||||
|
||||
|
||||
|
||||
self.different_organization_user = User.objects.create_user(username="test_different_organization_user", password="password")
|
||||
|
||||
|
||||
different_organization_team = Team.objects.create(
|
||||
team_name = 'different_organization_team',
|
||||
organization = different_organization,
|
||||
)
|
||||
|
||||
different_organization_team.permissions.set([
|
||||
view_permissions,
|
||||
add_permissions,
|
||||
change_permissions,
|
||||
delete_permissions,
|
||||
])
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = different_organization_team,
|
||||
user = self.different_organization_user
|
||||
)
|
||||
|
||||
|
||||
|
||||
class PermissionsAPI(
|
||||
ViewSetBase,
|
||||
APIPermissionAdd,
|
||||
APIPermissionDelete,
|
||||
APIPermissionView,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||
"""Check items returned
|
||||
|
||||
This test case is a over-ride of a test case with the same name.
|
||||
This model is not a tenancy model making this test not-applicable.
|
||||
|
||||
Items returned from the query Must be from the users organization and
|
||||
global ONLY!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def test_add_has_permission(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add as user with permission
|
||||
"""
|
||||
|
||||
url_kwargs = self.url_kwargs.copy()
|
||||
|
||||
url_kwargs['model_id'] = self.add_user.id
|
||||
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
|
||||
|
||||
|
||||
client.force_login(self.add_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_add_permission_view_denied(self):
|
||||
""" Check correct permission for add
|
||||
|
||||
Attempt to add a user with view permission
|
||||
"""
|
||||
|
||||
url_kwargs = self.url_kwargs.copy()
|
||||
|
||||
url_kwargs['model_id'] = self.add_user.id
|
||||
|
||||
client = Client()
|
||||
if self.url_kwargs:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-list', kwargs = url_kwargs)
|
||||
|
||||
else:
|
||||
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-list')
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.post(url, data=self.add_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_delete_has_permission(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Delete item as user with delete permission
|
||||
"""
|
||||
|
||||
url_view_kwargs = self.url_view_kwargs.copy()
|
||||
|
||||
url_view_kwargs['model_id'] = self.delete_user.id
|
||||
url_view_kwargs['pk'] = self.item_delete.id
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.delete_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 204
|
||||
|
||||
|
||||
def test_delete_permission_view_denied(self):
|
||||
""" Check correct permission for delete
|
||||
|
||||
Attempt to delete as user with veiw permission only
|
||||
"""
|
||||
|
||||
url_view_kwargs = self.url_view_kwargs.copy()
|
||||
|
||||
url_view_kwargs['model_id'] = self.delete_user.id
|
||||
url_view_kwargs['pk'] = self.item_delete.id
|
||||
|
||||
client = Client()
|
||||
url = reverse(self.app_namespace + ':' + self.url_name + '-detail', kwargs=url_view_kwargs)
|
||||
|
||||
|
||||
client.force_login(self.view_user)
|
||||
response = client.delete(url, data=self.delete_data)
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
|
||||
def test_returned_results_only_user_orgs(self):
|
||||
"""Test not required
|
||||
|
||||
this test is not required as this model is not a tenancy model
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
def test_view_no_permission_denied(self):
|
||||
""" Check correct permission for view
|
||||
|
||||
This test case is a duplicate of a test case with the same name.
|
||||
This test is not required for this model as there are no permissions
|
||||
assosiated with accessing this model.
|
||||
|
||||
Attempt to view with user missing permission
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class ViewSet(
|
||||
ViewSetBase,
|
||||
SerializerAdd,
|
||||
SerializerDelete,
|
||||
SerializerView,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class Metadata(
|
||||
ViewSetBase,
|
||||
MetadataAttributesFunctionalEndpoint,
|
||||
MetadataAttributesFunctionalBase,
|
||||
TestCase
|
||||
):
|
||||
|
||||
viewset_type = 'detail'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.url_kwargs = self.url_view_kwargs
|
@ -4,7 +4,7 @@ from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.organization import Organization
|
||||
|
||||
from api.tests.abstract.viewsets import ViewSetCommon
|
||||
|
||||
@ -43,6 +43,8 @@ class HomeViewset(
|
||||
client.force_login(self.view_user)
|
||||
self.http_options_response_list = client.options(url)
|
||||
|
||||
self.kwargs = {}
|
||||
|
||||
|
||||
|
||||
def test_view_attr_permission_classes_value(self):
|
||||
|
@ -1,8 +1,10 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.react_ui_metadata import ReactUIMetadata
|
||||
|
||||
|
@ -6,11 +6,13 @@ import unittest
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
from django.contrib.auth.models import AnonymousUser, Permission, User
|
||||
from django.shortcuts import reverse
|
||||
from django.test import TestCase, Client
|
||||
|
||||
from access.models import Organization, Team, TeamUsers, Permission
|
||||
from access.models.organization import Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.models.tokens import AuthToken
|
||||
|
||||
@ -53,7 +55,7 @@ class APIAuthToken(TestCase):
|
||||
expires=expires
|
||||
)
|
||||
|
||||
self.api_token_valid = token.generate()
|
||||
self.api_token_valid = token.generate
|
||||
self.hashed_token = token.token_hash(self.api_token_valid)
|
||||
token.token = self.hashed_token
|
||||
|
||||
@ -67,7 +69,7 @@ class APIAuthToken(TestCase):
|
||||
expires = expires.strftime('%Y-%m-%d %H:%M:%S%z')
|
||||
|
||||
|
||||
self.api_token_expired = token.generate()
|
||||
self.api_token_expired = token.generate
|
||||
|
||||
self.hashed_token_expired = token.token_hash(self.api_token_expired)
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user