Compare commits
959 Commits
1.10.0
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
3d2d759d6b | |||
b73de03d27 | |||
a9e953812c | |||
4344265ed5 | |||
4fb94bdd6d | |||
e2582373ad | |||
309483ea08 | |||
b2bc200eb1 | |||
a0ab0deb2a | |||
1f9491ce73 | |||
1663f19b2a | |||
07277862cf | |||
f1016bd9cc | |||
c39f479b96 | |||
a4d1a2bf76 | |||
8bba04305f | |||
516d6fc136 | |||
2e096cb495 | |||
3505f915c5 | |||
254cd02649 | |||
97e37f34a1 | |||
ae6b66b270 | |||
7a1aecebd5 | |||
387ffc9ade | |||
d0c3537753 | |||
eb97bfbeeb | |||
7fee33999f | |||
99c0e92336 | |||
46494c033c | |||
85986ce13e | |||
bdeae5b38b | |||
edc7aedbe0 | |||
900e03791a | |||
2a977bbf47 | |||
d29df73d05 | |||
1150e1b047 | |||
cf7eeb5bde | |||
c2a367bd28 | |||
3c19e9d4cf | |||
e295e53f86 | |||
be02061b94 | |||
08dbe1e35b | |||
8e9dca56bc | |||
2e9fe29e99 | |||
b0a6f207cd | |||
a51a79a763 | |||
b2d1903009 | |||
53f99f620c | |||
312267567f | |||
34b2571a2e | |||
b79874d056 | |||
27e42fac56 | |||
4124d5eb38 | |||
bb8c6378bc | |||
c3109f1894 | |||
59b4b5ff39 | |||
2d7335ff85 | |||
2e49de8573 | |||
a4772e3c25 | |||
de9937606e | |||
1b749e9f1a | |||
f0b3748596 | |||
2ea42d10e6 | |||
55cf10d289 | |||
09ab52b971 | |||
c00aa6fa73 | |||
817c8d63e1 | |||
cce885d961 | |||
6d92c484cd | |||
f5be3b0f8e | |||
85afbbc01e | |||
e0c0c69d35 | |||
df82650931 | |||
3196006bad | |||
820755b9e0 | |||
1946c7aa88 | |||
7c35a2d427 | |||
c4ee706591 | |||
4ee5f349f0 | |||
21a1974ba1 | |||
969b4d22fc | |||
6169324ffd | |||
003cad1f58 | |||
c8df4b21a0 | |||
7f49fb2d0e | |||
1cb028c273 | |||
4ad6c10a64 | |||
899df95994 | |||
0fa7ffa34a | |||
c46a597828 | |||
764f1b20d9 | |||
01b4a681da | |||
16299d480e | |||
b0cb9f6fd0 | |||
5b133c951d | |||
e29a7ec0e2 | |||
e7213d8a70 | |||
4d7510ad3a | |||
d8ef918a67 | |||
cb49d0fbf7 | |||
70c835eb93 | |||
40b51f1a77 | |||
1bff76c637 | |||
24b6bcfa47 | |||
b22baefa5a | |||
a6e0f4e728 | |||
e366220e8b | |||
f71e304731 | |||
55f58bb689 | |||
03b752759f | |||
85c6ed8483 | |||
a081c6a371 | |||
6c3122a3d8 | |||
0c80b87606 | |||
b6da539fcd | |||
d4d99772b9 | |||
4f8be43527 | |||
eb2282efac | |||
85b5bf7b58 | |||
626a5ccb11 | |||
806ffb2754 | |||
5900c13e08 | |||
d399698eb1 | |||
457d329b0b | |||
45c428d30a | |||
7ddb72239e | |||
580820ef44 | |||
d037150eb3 | |||
35a102e4a9 | |||
f7c75df9be | |||
d0e18fe75a | |||
e30c08fd5b | |||
1058659174 | |||
b9ac588f87 | |||
40b4bb0a3e | |||
9bd9652b3d | |||
46c4fe9516 | |||
a71b5e6aba | |||
e0cbf1447f | |||
b69e54f1e9 | |||
ecb1c21179 | |||
acff19ad58 | |||
f79f796a14 | |||
a08a43e2bd | |||
8238e97a45 | |||
88202b57ee | |||
324fa39b56 | |||
233393e853 | |||
84afc3274a | |||
a5bfc4977d | |||
19205bbe8b | |||
f144e0b8ef | |||
8473d00148 | |||
c8391caf07 | |||
d19bb3a204 | |||
ed71e935fc | |||
5ba243a1ea | |||
88a30650a5 | |||
420b223ca4 | |||
7168d519d1 | |||
4a09463f0a | |||
0a52029840 | |||
6841b30a77 | |||
dbaff89b8d | |||
b7cd9ea75c | |||
b54f3b7ab4 | |||
83d7c38c38 | |||
644dbc8159 | |||
e566a5edf1 | |||
ef5c6b73af | |||
3d7b133005 | |||
37a18d3c6e | |||
d6290471ba | |||
c13b360ca5 | |||
e5cb0261ba | |||
6c91cb008c | |||
9561301500 | |||
349d67fa7b | |||
ce64664447 | |||
370b8cd40f | |||
efde919689 | |||
9a88b75654 | |||
0ceab03334 | |||
c8cec06d85 | |||
018cd7d245 | |||
471b5c08f6 | |||
b11a978962 | |||
95aba2b44b | |||
5d6d0e95ec | |||
f0f75ecaa8 | |||
975a71bdd6 | |||
f9e59835c1 | |||
6ea66f7379 | |||
fb768d1432 | |||
baa61155f7 | |||
c773fbc3a5 | |||
8c84ff7c52 | |||
85d4cc8220 | |||
70a2d502ef | |||
393c8ce0ce | |||
92bc4aec43 | |||
4c46681d39 | |||
a898549469 | |||
4e75faafb8 | |||
1718a1cf14 | |||
64757826da | |||
927776b9a7 | |||
4ea5ddf122 | |||
847a5550e8 | |||
ea38665dab | |||
440adc09f4 | |||
9fcb1a8a44 | |||
22808cf6c8 | |||
f978839ccc | |||
54af17d75a | |||
c7eef1a97c | |||
59741e4197 | |||
2518873e23 | |||
029ab6bc06 | |||
a583db4b65 | |||
9f8789f390 | |||
c0b27c2886 | |||
cdacb19c30 | |||
471a1249c0 | |||
53abf219a3 | |||
51c238734a | |||
efb0f52e8a | |||
39dc8ad39f | |||
db47d0e160 | |||
da13343b27 | |||
8100030870 | |||
5097db6f94 | |||
b729842a1c | |||
bc1d5ffc03 | |||
ae1f600147 | |||
edc3f9fd3c | |||
8dc922a8bb | |||
9461cdaf9d | |||
a923a21660 | |||
dcd60a5152 | |||
50882a4732 | |||
9cd59e8446 | |||
9905edc28c | |||
98479f39e6 | |||
bdfd3f42c4 | |||
ca022e0697 | |||
1719b6c417 | |||
bb0f9b56e6 | |||
107a3aa17f | |||
1c9be27e1f | |||
dc25d483fc | |||
33362a28d5 | |||
b476bd458e | |||
c152dd13f1 | |||
677cc55e04 | |||
b6146b7d14 | |||
dbc849d3f1 | |||
8d1098ec9e | |||
63923e8a9b | |||
7261f60e3b | |||
fef14b8be8 | |||
17e4cb4148 | |||
99211b7505 | |||
c4e81ffbed | |||
683b134053 | |||
2a5c86d961 | |||
ca28a67194 | |||
d904ce7100 | |||
daf30de835 | |||
7c9819efd1 | |||
42bea64ff5 | |||
537f167b9e | |||
0a630cf51a | |||
29e07e6cac | |||
0f21f8daf0 | |||
6289fad8ad | |||
705b245514 | |||
9e1a426240 | |||
c80a5b5232 | |||
eddd2534ea | |||
9dc1ae2d05 | |||
4b8cb9633e | |||
8016c4b4c8 | |||
3efadfa75f | |||
66bbace8be | |||
7ff638ed58 | |||
ddebf50e88 | |||
64144ea76f | |||
633df58964 | |||
f280f408ee | |||
d1715f4457 | |||
e80647ffd1 | |||
1a5f328a24 | |||
4872c6d1c8 | |||
a695b4563d | |||
b89993f89d | |||
8f69e6ec79 | |||
8984aeebb8 | |||
a0d750b1f5 | |||
75618de977 | |||
2e1358da83 | |||
8ecb1fe01a | |||
0ff8f8650b | |||
529a7fa385 | |||
6258fb2a33 | |||
0a2ede62b7 | |||
559e65f990 | |||
e71f7a6942 | |||
f12a2e37f3 | |||
c1614b1182 | |||
6b4bd4db35 | |||
163d7bcd6c | |||
d131d2499b | |||
4e03c2bf31 | |||
9e05410f28 | |||
9c88c23bb3 | |||
e4588771e8 | |||
507c42faea | |||
9302627c6f | |||
04780767c0 | |||
2d8275d2af | |||
db25eabfdd | |||
71bfb5b9e4 | |||
3146417001 | |||
32d97932d1 | |||
32251d0a08 | |||
b694df0330 | |||
52ffb58276 | |||
9ebc65f3b3 | |||
bdc4066a17 | |||
ee018802a9 | |||
d4221913d2 | |||
ab4ebdab24 | |||
a18d9c2789 | |||
f2b13e5a0c | |||
3763eb8727 | |||
b99b38f623 | |||
7aae162453 | |||
aad1407647 | |||
79efc2258f | |||
8b32faa736 | |||
3b04b0c9a4 | |||
4518449232 | |||
6d708d0c00 | |||
5bf89e1ca6 | |||
23213c148c | |||
dd4175a6ba | |||
1d9ce9d310 | |||
813470f084 | |||
bb27bcc280 | |||
1b53faf9ce | |||
33d20a4c0a | |||
fe7e2b9d22 | |||
c563c07345 | |||
3b72df61a4 | |||
c3da9503d2 | |||
c42037cef6 | |||
192f46022c | |||
81860cac84 | |||
b66feadb5a | |||
b01a2a9a47 | |||
f7d3cdd463 | |||
3bb63b2a5b | |||
05ec53331a | |||
6aebde7845 | |||
4de24a7d88 | |||
322b7a1c41 | |||
825683e162 | |||
6bb5a47dd3 | |||
47381c7bf7 | |||
8b08d03d95 | |||
360bf60578 | |||
c8c2fcabd2 | |||
1830b86309 | |||
9e712d3624 | |||
a2a79be7c1 | |||
d5a2adc3a9 | |||
779335458e | |||
8bc0b3338c | |||
9d8bcff2a0 | |||
8a8023f510 | |||
7289758ed9 | |||
d7405a500d | |||
d63c249120 | |||
e920a66a47 | |||
cb4a12f2a0 | |||
d8263b36f7 | |||
74482956a4 | |||
1bc199493c | |||
5cccca865e | |||
03d258ca57 | |||
dfea1fdba0 | |||
edff3eb889 | |||
bdd55a4df6 | |||
ca0bb96808 | |||
5b0d9d8d81 | |||
dd264920a6 | |||
050b2d7602 | |||
ea7c359cc8 | |||
b3391c7e3e | |||
22e369aa3e | |||
d0f710d9f8 | |||
bd58d1d9cf | |||
184820780a | |||
6941668709 | |||
f2cdd403e3 | |||
cb43c56efb | |||
9bbfbbba4b | |||
296188b202 | |||
de26f4b4e9 | |||
600a12177f | |||
167f4140ba | |||
2d6c347859 | |||
2998f48e33 | |||
03a9582703 | |||
3417e37317 | |||
aba9f44552 | |||
569a256dd5 | |||
22785a095d | |||
209e67e0b1 | |||
b50b0bbd8b | |||
bbbf7cb38a | |||
3624885f1e | |||
1f817109ae | |||
711c546e69 | |||
11e32435a2 | |||
8c0b9bf182 | |||
44ec81c3ae | |||
24132a0b1c | |||
b887bb6169 | |||
93ce31d2cf | |||
7f4ff50ceb | |||
534dab2ca6 | |||
705a775ddd | |||
7e79385558 | |||
798cfbe975 | |||
bfefbae686 | |||
8363cd379f | |||
9496ae6aaf | |||
5208340370 | |||
9b58e5913f | |||
748dbea515 | |||
b7880de54d | |||
69b727a06c | |||
26cdd7495a | |||
e2182fe37e | |||
e8b30796ab | |||
36f314fc6f | |||
1d1c76e033 | |||
e8bc98c315 | |||
853906e9ee | |||
403b6be252 | |||
ac78032cca | |||
08fd187692 | |||
8bd90df582 | |||
85a2779563 | |||
9cc5db7869 | |||
e65b2531ed | |||
3a9198f63c | |||
4d8fc508d4 | |||
67b0187a58 | |||
12ef8918ba | |||
5f3990e15a | |||
491e0dba64 | |||
50cb54ab0c | |||
7638fa39da | |||
b0df5713b2 | |||
57c5947c55 | |||
bfd54c112b | |||
e2ca5b8587 | |||
f406e7bf3b | |||
1e127d7180 | |||
a0dd0384bf | |||
60cc64ba19 | |||
2e9470be83 | |||
ade836911f | |||
84f2e8d8c3 | |||
64b677eaa9 | |||
4bd5a890db | |||
48a7a206d2 | |||
b837338140 | |||
0ad80a6f9a | |||
a30cad25bc | |||
668a64bb79 | |||
9d67624e9d | |||
ca2e4e00fa | |||
57cd4851a8 | |||
e6f576ef1a | |||
b34c76afde | |||
f0171fcfda | |||
04b5b4dc24 | |||
77e42db3c9 | |||
25e0f6d950 | |||
e725efb9b7 | |||
f6bf6df31c | |||
2994cfd783 | |||
d006da803f | |||
b1f80cb1b2 | |||
9485d4fce7 | |||
f5e8dd95db | |||
83d937ce7a | |||
ee4ff23618 | |||
2b19f466f2 | |||
8caa8646b4 | |||
dc9d1d283f | |||
0c82fd2bb1 | |||
5483c1878d | |||
bd22604d9d | |||
e8c246a949 | |||
69c631d59b | |||
561e175723 | |||
4c6c27a4bd | |||
cb95cb506a | |||
2b3070c5c2 | |||
9229036b72 | |||
b5b8030c81 | |||
8d4aad7745 | |||
c5e33c4e3d | |||
23773a8776 | |||
ef0b024a12 | |||
3cdf0c7324 | |||
051995efca | |||
717bd1e221 | |||
e158be3cb2 | |||
7a7281ecfc | |||
88d1abaef7 | |||
5804abc367 | |||
96f8949be2 | |||
cce802ea9e | |||
c767a528eb | |||
895cfb8b22 | |||
8dfaec8df7 | |||
69cabda78e | |||
8abced87de | |||
983311dda5 | |||
2becbeef87 | |||
9583631473 | |||
44e1461c6b | |||
105e6509b0 | |||
20d979963a | |||
928dee74b5 | |||
bde2b2e758 | |||
54b97b43ed | |||
405538fa35 | |||
7a9680d988 | |||
d7d85bd01d | |||
becb1eef26 | |||
bf973d3765 | |||
7912a67ab7 | |||
6272eef45f | |||
176537d583 | |||
2491ab611b | |||
3925b96cb0 | |||
8488642651 | |||
cb96d33f74 | |||
70978b9fdc | |||
0452293546 | |||
9c65d7f355 | |||
32137334ad | |||
a9fb70fcc7 | |||
c34903abf2 | |||
70c7100b1d | |||
8eb1650273 | |||
68364906f9 | |||
f99c1e2132 | |||
179340e1a2 | |||
b4d41970aa | |||
5f62828f28 | |||
46996b7f14 | |||
2929e347c7 | |||
b8281a3dd7 | |||
3888ab737b | |||
acc31ef079 | |||
19ce303045 | |||
0dbde5e1d6 | |||
f3a45d6ec2 | |||
f24f02df5d | |||
3ab5babd31 | |||
14c193d72a | |||
b22f4e2ea9 | |||
29553474f2 | |||
19d6b4f7e8 | |||
f1fb3cd7ff | |||
f1570a1997 | |||
2f95482150 | |||
b2e82fd8c9 | |||
192efadb08 | |||
00d16f841f | |||
c5107861a1 | |||
1d9580f14b | |||
aa77c69ed7 | |||
acf9f20ab9 | |||
6cf6a23964 | |||
a69e102432 | |||
89e2de437f | |||
dcebd4b8d6 | |||
2e35b651ef | |||
2f06814d99 | |||
faff43510d | |||
42105d4621 | |||
f61799dbfe | |||
427584c8d9 | |||
a07ee8472e | |||
3eb02602d6 | |||
7dac29971f | |||
0c6fb786dd | |||
d09e649765 | |||
8741e0b636 | |||
529512be3e | |||
d038165146 | |||
b63f6b7092 | |||
7b6fe804a9 | |||
b874fc7298 | |||
4f6debac88 | |||
5fdc0b32a6 | |||
e607999a62 | |||
5dd4bddea9 | |||
b60aa3be7a | |||
53956e0772 | |||
f4ccd3d164 | |||
629f5aec8e | |||
69eb7eb294 | |||
e317a96e45 | |||
9720ae527a | |||
33644a25d1 | |||
eb7ff47873 | |||
b542e92bbd | |||
b515b26203 | |||
b562e09622 | |||
748a1c80ef | |||
492bbfb521 | |||
86d5ce2062 | |||
541b7734e5 | |||
9fcea0528a | |||
0a5778258b | |||
75412df8b6 | |||
28e80bff50 | |||
f0ec8e4e56 | |||
8124d58014 | |||
9f1a73d7a5 | |||
b411d1fb24 | |||
41727d0a16 | |||
2eafb88367 | |||
6858c04bfd | |||
124c96512a | |||
23793e2133 | |||
4b06d6a2a1 | |||
8a56fdfcdb | |||
c4b640fb53 | |||
1afa102679 | |||
ddf3449b3f | |||
95c5f271ba | |||
db0cf389c3 | |||
8b17ea54c7 | |||
330d00a73d | |||
5f691748bc | |||
4876919015 | |||
55e30ab4f5 | |||
4d6438833d | |||
5b97f5400f | |||
8a787a516f | |||
51013d12d3 | |||
44a750f32b | |||
5938a51193 | |||
f833121c08 | |||
90a1e4baad | |||
20a1f69077 | |||
d79b13d98e | |||
05cf5b2835 | |||
1edc398f41 | |||
adfeec5fef | |||
7fbe6fda95 | |||
40f7f7739f | |||
51807b4747 | |||
ef1742c537 | |||
1216092413 | |||
ee23cb1f6e | |||
158e8436d8 | |||
235e4db5b6 | |||
aa1cd3eda2 | |||
1ed96ff9fc | |||
6fabdb6d17 | |||
c6a38684db | |||
4ecc841462 | |||
eda1fb673b | |||
d23e05ac7b | |||
1818ee94e7 | |||
9d88bf8827 | |||
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 |
2
.cz.yaml
2
.cz.yaml
@ -17,5 +17,5 @@ commitizen:
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 1.10.0
|
||||
version: 1.18.0
|
||||
version_scheme: semver
|
||||
|
121
.github/ISSUE_TEMPLATE/new_model.md
vendored
Normal file
121
.github/ISSUE_TEMPLATE/new_model.md
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
---
|
||||
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](https://nofusscomputing.com/projects/centurion_erp/development/models/)
|
||||
|
||||
- [ ] 🛠️ Migrations added
|
||||
|
||||
- [ ] ♻️ Serializer Created
|
||||
|
||||
- [ ] 🔄 [ViewSet Created](https://nofusscomputing.com/projects/centurion_erp/development/views/)
|
||||
|
||||
- [ ] 🔗 URL Route 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)
|
||||
- [ ] tag added to `app/core/lib/slash_commands/linked_model.CommandLinkedModel.get_model()`
|
||||
- [ ] ⚒️ Migration _Ticket Linked Item item_type choices update_
|
||||
|
||||
>[!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
|
||||
|
||||
- Sub-Models **_ONLY_**
|
||||
|
||||
- [ ] Model class variable [`history_app_label`](https://nofusscomputing.com/projects/centurion_erp/development/models/#history) set to correct application label
|
||||
|
||||
- [ ] Model class variable [`history_model_name`](https://nofusscomputing.com/projects/centurion_erp/development/models/#history) set to correct model label
|
||||
|
||||
- [ ] 📓 New [Notes model](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/) created
|
||||
- [ ] 🆕 Model Created
|
||||
- [ ] 🛠️ Migrations added
|
||||
- [ ] Add `app_label` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().model_apps`
|
||||
- [ ] _(Notes not used/required) -_ Add `model_name` to KB Models `app/assistance/models/model_knowledge_base_article.all_models().excluded_models`
|
||||
- [ ] 🧪 [Unit tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
|
||||
- [ ] 🧪 [Functional tested](https://nofusscomputing.com/projects/centurion_erp/development/core/model_notes/#testing)
|
||||
|
||||
- [ ] 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 Tests
|
||||
- [ ] API Render (fields)
|
||||
- [ ] [Model](https://nofusscomputing.com/projects/centurion_erp/development/models/#tests)
|
||||
- [ ] ViewSet
|
||||
- Function Test
|
||||
- [ ] History API Render (fields)
|
||||
- [ ] History Entries
|
||||
- [ ] API Metadata
|
||||
- [ ] API Permissions
|
||||
- [ ] Model
|
||||
- [ ] Serializer
|
||||
- [ ] ViewSet
|
||||
|
||||
|
||||
## ✅ 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)
|
||||
|
||||
<!--
|
||||
|
||||
When detailing requirements the following must be taken into account:
|
||||
|
||||
- what the user should be able to do
|
||||
|
||||
- what the user should not be able to do
|
||||
|
||||
- what should occur when a user performs an action
|
||||
|
||||
-->
|
||||
|
||||
- Functional Requirements
|
||||
|
||||
|
||||
- Non-Functional Requirements
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- Add additional requirement here and as a check box list -->
|
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@ -20,7 +20,7 @@
|
||||
|
||||
<!-- dont remove tasks below strike through including the checkbox by enclosing in double tidle '~~' -->
|
||||
|
||||
- [ ] **Feature Release ONLY** :red_square: Squash migration files :red_square:
|
||||
- [ ] **Feature Release ONLY** :red_square: [Squash migration files](https://docs.djangoproject.com/en/5.2/topics/migrations/#squashing-migrations) :red_square:
|
||||
_Multiple migration files created as part of this release are to be sqauashed into a few files as possible so as to limit the number of migrations_
|
||||
|
||||
- [ ] :firecracker: Contains breaking-change Any Breaking change(s)?
|
||||
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -1,9 +1,10 @@
|
||||
venv/**
|
||||
*/static/**
|
||||
__pycache__
|
||||
**.sqlite3
|
||||
**.sqlite*
|
||||
**.sqlite
|
||||
**.coverage
|
||||
.coverage*
|
||||
artifacts/
|
||||
**.tmp.*
|
||||
volumes/
|
||||
@ -15,3 +16,8 @@ node_modules/
|
||||
package-lock.json
|
||||
package.json
|
||||
**.junit.xml
|
||||
**.JUnit.xml
|
||||
feature_flags.json
|
||||
coverage_*.json
|
||||
*-coverage.xml
|
||||
log/
|
||||
|
45
.vscode/launch.json
vendored
45
.vscode/launch.json
vendored
@ -17,7 +17,6 @@
|
||||
"program": "${workspaceFolder}/app/manage.py"
|
||||
},
|
||||
{
|
||||
|
||||
"name": "Debug: Gunicorn",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
@ -31,7 +30,6 @@
|
||||
"--bind",
|
||||
"0.0.0.0:8002",
|
||||
"app.wsgi:application",
|
||||
|
||||
],
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
@ -40,6 +38,18 @@
|
||||
"PROMETHEUS_MULTIPROC_DIR": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Centurion Feature Flag (Management Command)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"feature_flag",
|
||||
// "0.0.0.0:8002"
|
||||
],
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
"program": "${workspaceFolder}/app/manage.py"
|
||||
},
|
||||
{
|
||||
"name": "Migrate",
|
||||
"type": "debugpy",
|
||||
@ -50,7 +60,6 @@
|
||||
"django": true,
|
||||
"autoStartBrowser": false,
|
||||
"program": "${workspaceFolder}/app/manage.py"
|
||||
|
||||
},
|
||||
{
|
||||
"name": "Debug: Celery",
|
||||
@ -68,6 +77,34 @@
|
||||
"debug-itsm@%h"
|
||||
],
|
||||
"cwd": "${workspaceFolder}/app"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug pytest (collect)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "pytest",
|
||||
"args": [
|
||||
"--override-ini", "addopts=",
|
||||
"--collect-only",
|
||||
"app",
|
||||
],
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": false
|
||||
},
|
||||
{
|
||||
"name": "Python Debugger: Local Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "localhost",
|
||||
"port": 5678
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "."
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
15
.vscode/settings.json
vendored
15
.vscode/settings.json
vendored
@ -5,18 +5,23 @@
|
||||
"!python"
|
||||
],
|
||||
"python.testing.pytestArgs": [
|
||||
// "-v",
|
||||
// "--cov",
|
||||
// "--cov-report xml",
|
||||
"-s",
|
||||
"--override-ini", "addopts=",
|
||||
"app",
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"testing.coverageToolbarEnabled": true,
|
||||
"cSpell.words": [
|
||||
"ITSM"
|
||||
],
|
||||
"cSpell.language": "en-AU",
|
||||
"jest.enable": false,
|
||||
"pylint.enabled": true,
|
||||
"testing.showCoverageInExplorer": true,
|
||||
"testing.coverageToolbarEnabled": true,
|
||||
"testing.coverageBarThresholds": {
|
||||
"red": 0,
|
||||
"yellow": 60,
|
||||
"green": 90
|
||||
},
|
||||
"telemetry.feedback.enabled": false,
|
||||
}
|
949
CHANGELOG.md
949
CHANGELOG.md
@ -1,3 +1,952 @@
|
||||
## 1.18.0 (2025-07-03)
|
||||
|
||||
### feat
|
||||
|
||||
- **python**: upgrade django 5.1.9 -> 5.1.10
|
||||
|
||||
### Fixes
|
||||
|
||||
- **itim**: Correct config that is in the incorrect format
|
||||
|
||||
## 1.17.1 (2025-06-02)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **base**: Add python metrics to prometheus exporter
|
||||
|
||||
## 1.17.0 (2025-05-16)
|
||||
|
||||
### feat
|
||||
|
||||
- **access**: model access.Company feature flag `2025-00008`
|
||||
- **access**: URL route for model access.Company
|
||||
- **access**: Migration for model access.Company
|
||||
- **access**: Serializer for model access.Company
|
||||
- **access**: New model access.Company
|
||||
- **access**: Organization -> Tenant Permission Migration
|
||||
- **docker**: Serve a robots.txt file for NO indexing
|
||||
- **access**: Organization -> Tenant Permission Migration
|
||||
- **base**: Add var `AUTH_USER_MODEL` to settings
|
||||
- **core**: Add Action comments on ticket change
|
||||
- **core**: Remove add, change and delete permissions for model TicketCommentAction from permission selector
|
||||
- **core**: Serializer for model TicketCommentAction
|
||||
- **core**: Migrations for model TicketCommentAction
|
||||
- **core**: New model TicketCommentAction
|
||||
- **core**: Setup serializer to meet requirements
|
||||
- **core**: Setup model to meet requirements
|
||||
- **api**: Add exception logging to ViewSetCommon
|
||||
- **python**: Upgrade DRF Spectacular 0.27.2 -> 0.28.0
|
||||
- **python**: Upgrade DRF 3.15.2 -> 3.16.0
|
||||
- **core**: When processing slash command duration, cater for new ticket models
|
||||
- **api**: Add Logging function to Common ViewSet
|
||||
- **access**: Add Logging function to Tenancy model
|
||||
- **base**: Enable user to customize log file location
|
||||
- **core**: Do validate the comment_type field for TicketCommentBase
|
||||
- **itam**: Add Feature Flag `2025-00007` ITAMAssetBase
|
||||
- **itam**: Add endpoint for ITAMAssetBase
|
||||
- Model tag migration for Asset and IT Asset
|
||||
- **itam**: Model tag for ITAsset
|
||||
- **accounting**: Model tag for Asset
|
||||
- **accounting**: Add app label to kb articles for notes
|
||||
- **accounting**: Migrations for notes model for AssetBase
|
||||
- **accounting**: Migrations for history model for AssetBase
|
||||
- **accounting**: Notes Viewset for AssetBase
|
||||
- **accounting**: Notes Serializer for AssetBase
|
||||
- **accounting**: Notes model for AssetBase
|
||||
- **accounting**: History model for AssetBase
|
||||
- **itam**: Serializer for ITAssetBase
|
||||
- **itam**: Migrations for ITAssetBase
|
||||
- **itam**: Add Model ITAssetBase
|
||||
- **accounting**: Viewset for Assets
|
||||
- **accounting**: Serializer for model AssetBase
|
||||
- **accounting**: Migrations for model AssetBase
|
||||
- **accounting**: Add Model AssetBase
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: Dont try to access attribute if not exist in common viewset
|
||||
- **api**: Dont try to access attribute if not exist in common viewset
|
||||
- **api**: Correct ViewSet Sub-Model lookup
|
||||
- **core**: Only take action on ticket comment if view exists
|
||||
- **api**: Ensure multi-nested searching for sub-models works
|
||||
- **core**: ensure slash command is called on ticket description
|
||||
- **core**: Spent slash command is valid for time spent
|
||||
- **core**: Correct logic for TicketCommentSolution
|
||||
- **core**: Correct logic for TicketCommentBase
|
||||
- **accounting**: Ensure correct sub-model check is conducted within model type
|
||||
- **itam**: ensure RO field asset_type is set
|
||||
- **itim**: Ensure that itam base model is always imported
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **human_resources**: Update Functional ViewSet to use PyTest for Employee Model
|
||||
- **Access**: Update Functional ViewSet to use PyTest for Person Model
|
||||
- **Access**: Update Functional ViewSet to use PyTest for Entity Model
|
||||
- **Access**: Update Functional ViewSet to use PyTest for Contact Model
|
||||
- **Access**: Update Functional Permission to use PyTest for Person Model
|
||||
- **Access**: Update Functional Permission to use PyTest for Entity Model
|
||||
- **Access**: Update Functional Permission to use PyTest for Contact Model
|
||||
- **Access**: Update Functional Serializer to use PyTest for Contact Model
|
||||
- **Access**: Update Functional Serializer to use PyTest for Entity Model
|
||||
- **Access**: Update Functional Serializer to use PyTest for Person Model
|
||||
- **human_resources**: Update Functional Serializer to use PyTest for Employee Model
|
||||
- **human_resources**: Update Functional Permissions to use PyTest for Employee Model
|
||||
- **human_resources**: Update Functional Metadata to use PyTest for Employee Model
|
||||
- **access**: Update Functional Metadata to use PyTest for Person Model
|
||||
- **access**: Update Functional Metadata to use PyTest for Entity Model
|
||||
- **access**: Update Functional Metadata to use PyTest for Contact Model
|
||||
- **access**: Update Model Entity to use PyTest for Model Test Suite
|
||||
- **access**: Update Model Contact to use PyTest for Model Test Suite
|
||||
- **access**: Update Model Person to use PyTest for Model Test Suite
|
||||
- **human_resources**: Update Model Employee to use PyTest for Model Test Suite
|
||||
- **human_resources**: Update Model Employee to use PyTest API Fields Render
|
||||
- **access**: Update Model Person to use PyTest API Fields Render
|
||||
- **access**: Update Model Contact to use PyTest API Fields Render
|
||||
- **access**: Update Model Entity to use PyTest API Fields Render
|
||||
- **access**: Rename model Organization -> Tenant
|
||||
- **settings**: Update all references to `User` to use `get_user_model()`
|
||||
- **project_management**: Update all references to `User` to use `get_user_model()`
|
||||
- **itam**: Update all references to `User` to use `get_user_model()`
|
||||
- **devops**: Update all references to `User` to use `get_user_model()`
|
||||
- **core**: Update all references to `User` to use `get_user_model()`
|
||||
- **config_management**: Update all references to `User` to use `get_user_model()`
|
||||
- **assistance**: Update all references to `User` to use `get_user_model()`
|
||||
- **app**: Update all references to `User` to use `get_user_model()`
|
||||
- **api**: Update all references to `User` to use `get_user_model()`
|
||||
- **accounting**: Update all references to `User` to use `get_user_model()`
|
||||
- **access**: Update all references to `User` to use `get_user_model()`
|
||||
- **access**: when fetching parent object, use the parent_model get function
|
||||
- **api**: Limit url pk regex to ensure the value is a number
|
||||
|
||||
### Tests
|
||||
|
||||
- **access**: Functional ViewSet Test Suite Company model
|
||||
- **access**: Functional Serializer Test Suite Company model
|
||||
- **access**: Functional Permissions Test Suite Company model
|
||||
- **access**: Functional MetaData Test Suite Company model
|
||||
- **access**: ViewSet Test Suite Company model
|
||||
- **access**: API field render Test Suite Company model
|
||||
- **access**: Model Test Suite Company model
|
||||
- **core**: Unit viewset Test Cases for TicketCommentAction model
|
||||
- **core**: Unit model Test Cases for TicketCommentAction model
|
||||
- **core**: Unit API Render Test Cases for TicketCommentAction model
|
||||
- **core**: Interim Functional model Test Case TicketCommentAction
|
||||
- **core**: Ensure that a ticket milestone comes from the same assigned project
|
||||
- **core**: SKIP Tests TicketBase Description Slash command Checks
|
||||
- **core**: TicketBase Description Slash command Checks
|
||||
- **core**: TicketBase Remaining Serializer Chacks
|
||||
- **core**: Partial functional Model Test Suite covering some slash commande for TicketCommentSolution
|
||||
- **core**: ensure ticket is un-solved for ticketcomment unit api render fields check
|
||||
- **core**: ensure slash command is called on ticket comment
|
||||
- **core**: Unit ViewSet Test Suite for TicketCommentSolution
|
||||
- **core**: Unit ViewSet Test Suite for TicketCommentBase
|
||||
- **core**: Skip Related slash command checks until migrating tickets to new model
|
||||
- **core**: Add ability to unit api field rendering test case for second api request if required
|
||||
- **core**: Partial Functional Model test cases (Slash Commands) for TicketCommentBase
|
||||
- **core**: Functional Model test cases (Slash Commands) for TicketBaseModel
|
||||
- **core**: Partial Slash Command re-write
|
||||
- **core**: correct field so its valid for unit TicketCommentBase model
|
||||
- **core**: Unit API Fields Render for TicketCommentSolution model
|
||||
- **core**: Unit API Fields Render for TicketCommentBase model
|
||||
- **core**: Unit Model assert save and call are called for TicketBase
|
||||
- **core**: Unit Model Checks for TicketCommentSolution
|
||||
- **core**: Unit Model Checks for TicketCommentBase
|
||||
- **itam**: test meta attribute itam_sub_model_type for ITAMBaseModel
|
||||
- **itam**: Dont use constants where variables should be used
|
||||
- **itam**: Remaining Unit Model test cases for AssetBase
|
||||
- **accounting**: Remaining Unit Model test cases for AssetBase
|
||||
- **itam**: Functional ViewSet Test Cases for ITAMAssetBase
|
||||
- **itam**: Functional Serializer Test Cases for ITAMAssetBase
|
||||
- **itam**: Functional Permissions Test Cases for ITAMAssetBase
|
||||
- **itam**: Functional Metadata Test Cases for ITAMAssetBase
|
||||
- **itam**: Functional History Test Cases for ITAMAssetBase
|
||||
- **accounting**: Functional ViewSet Test Cases for AssetBase
|
||||
- **accounting**: Functional Serializer Test Cases for AssetBase
|
||||
- **accounting**: Functional Permissions Test Cases for AssetBase
|
||||
- **accounting**: Functional Metadata Test Cases for AssetBase
|
||||
- **accounting**: History Test Cases for AssetBase
|
||||
- add missing merge of add_data for api permissions tests
|
||||
- remove ticket only vars from api permissions tests
|
||||
- **api**: dont use constants for variable data
|
||||
- correct viewset tests
|
||||
- **itam**: Unit Viewset checks for AssetBase Model
|
||||
- **core**: Add missing fields is_global checks for ticket base
|
||||
- **api**: Add submodel url resolution for metadata
|
||||
- **itam**: Unit API Fields checks for ITAM AssetBase Model
|
||||
- **accounting**: Unit API Fields checks for AssetBase Model
|
||||
- Support variables that were defined as properties.
|
||||
- **api**: Ensure that model notes is added to model create for api field tests
|
||||
- **accounting**: Unit Viewset checks for AssetBase Model
|
||||
- **itam**: Unit Model checks for ITAMAssetBase Model
|
||||
- **base**: update Model base test suite for model_notes field
|
||||
- **accounting**: Unit Model checks for AssetBase Model
|
||||
|
||||
## 1.16.0 (2025-05-04)
|
||||
|
||||
### feat
|
||||
|
||||
- **core**: Add ViewSet for Ticket Comments
|
||||
- **project_management**: Depreciate Project Task Ticket Endpoint
|
||||
- **itim**: Depreciate Problem Ticket Endpoint
|
||||
- **itim**: Depreciate Incident Ticket Endpoint
|
||||
- **itim**: Depreciate Change Ticket Endpoint
|
||||
- **assistance**: Depreciate Ticket Comment
|
||||
- **assistance**: Depreciate Request Ticket Endpoint
|
||||
- **core**: Add routes for Ticket Comments
|
||||
- **core**: update ticket serializer to use new comment base url
|
||||
- **core**: Add permissions `import`, `purge` and `triage` to model TicketCommentSolution
|
||||
- **core**: Add permissions `import`, `purge` and `triage` to model TicketCommentBase
|
||||
- **core**: Filter ticket_comment_model routes to those defined in `Meta.sub_model_type`
|
||||
- **core**: Filter ticket_model routes to those defined in `Meta.sub_model_type`
|
||||
- **access**: Filter entity_model routes to thos defined in `Meta.sub_model_type`
|
||||
- **core**: Serializer for TicketCommentBase
|
||||
- **core**: Serializer for TicketCommentSolution
|
||||
- **core**: Ticket Comment Get URL functions
|
||||
- **core**: Ticket Comment Validation for comment_type
|
||||
- **core**: Update choices fields for TicketCommentBase model
|
||||
- **core**: init for model TicketCommentSolution
|
||||
- **core**: Migrations for choice fields for TicketBase model
|
||||
- **core**: Migrations for model TicketCommentSolution
|
||||
- **core**: Update choice fields for TicketBase model
|
||||
- **core**: New model TicketCommentSolution
|
||||
- **api**: when fetching related_object, default to base_model for SubModelViewSet
|
||||
- Add field `Meta.sub_model_type` to sub-models
|
||||
- **core**: New interim model TicketCommentSolution
|
||||
- **core**: add ticket routes
|
||||
- **itim**: serializer for SLMTicketBase
|
||||
- **itim**: Serializer for RequestTicket
|
||||
- **itim**: migrations for RequestTicket
|
||||
- **itim**: New Model RequestTicket
|
||||
- **itim**: migration for SLMTicketBase
|
||||
- **itim**: New Model SLMTicketBase
|
||||
- **core**: migrations for TicketCommentBase
|
||||
- **core**: New Model TicketCommentBase
|
||||
- **core**: viewset for TicketBase
|
||||
- **core**: serializer for TicketBase
|
||||
- **core**: migrations for TicketBase
|
||||
- **core**: New Model TicketBase
|
||||
- **project_management**: add estimation field to project api fields
|
||||
- **human_resources**: nav menu entries for Employee
|
||||
- **human_resources**: Serializer for Employee
|
||||
- **human_resources**: Migration for Employee
|
||||
- **human_resources**: New model Employee
|
||||
- **devops**: add missing api index menu entry for devops
|
||||
- **access**: add missing nav menu entries for entities
|
||||
- **human_resources**: add module to perms selector
|
||||
|
||||
### Fixes
|
||||
|
||||
- **test**: correct typo in attribute parameterized_
|
||||
- **core**: Ticktet comment can have empty body
|
||||
- **core**: If model does not save history, dont attempt to cache before
|
||||
- **itam**: provide return_url as part of software version meta
|
||||
- **itim**: correct ticket_slm serializer
|
||||
- **itim**: correct ticket_request serializer
|
||||
- **api**: SubModelViewSet.related_objects must be the same class as the base model
|
||||
- **access**: Ensure related model is a sub-model
|
||||
- **human_resources**: Correct history link generation and add docs
|
||||
- **human_resources**: Correct history link generation
|
||||
- **access**: add missing attribute to Tenancy object
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **test**: rewrite model unit tests to use PyTest
|
||||
- **test**: Update test parameterization
|
||||
- **api**: SubModelViewSet must inherit from ModelViewSet as it's an extension
|
||||
- **core**: rename ticket model filename in preparation for base ticket model
|
||||
- **access**: migrate sub-model viewset logic to common
|
||||
- **project_management**: add duration field to project api fields
|
||||
- **human_resources**: Move employee details to its own section
|
||||
|
||||
### Tests
|
||||
|
||||
- **core**: Serializer Validation for ticket status change for TicketBase model
|
||||
- **core**: Prevent Closing / Solving of TicketBase Model if not ready
|
||||
- **itim**: Incomplete Model Unit Tests for RequestTicket
|
||||
- **itim**: Incomplete Model Unit Tests for SLMTicketBase
|
||||
- **core**: Incomplete Model Unit Tests for TicketBase
|
||||
- **itim**: RequestTicket Updated, yet incomplete Test Suite for Serializer
|
||||
- **itim**: SLMTicketBase Updated, yet incomplete Test Suite for Serializer
|
||||
- **core**: TicketBase Updated, yet incomplete Test Suite for Serializer
|
||||
- Correct Test Suite for Serializer for models TicketBase, TicketRequest and TicketSLM
|
||||
- **itim**: RequestTicket Initial Test Suite for Serializer
|
||||
- **itim**: SLMTicket Initial Test Suite for Serializer
|
||||
- **core**: TicketBase Initial Test Suite for Serializer
|
||||
- **core**: SLMTicket Test Suite for ViewSet
|
||||
- **core**: SLMTicket Test Suite for Metadata
|
||||
- **core**: Request Test Suite for ViewSet
|
||||
- **core**: Request Test Suite for Metadata
|
||||
- **core**: TicketBase Test Suite for ViewSet
|
||||
- **core**: TicketBase Test Suite for Metadata
|
||||
- **api**: update test cases for SubModelViewSet Base Test Class
|
||||
- **itim**: RequestTicket ViewSet Test Suite
|
||||
- **core**: TicketBase ViewSet Test Suite
|
||||
- **api**: Incomplete SubModelViewSet Test Cases
|
||||
- **api**: SubModelViewSet Test Suite Setup
|
||||
- correct tests from Meta.sub_model_type changes
|
||||
- correct serializer imports from recent file renames
|
||||
- Fixture for creating model with random data
|
||||
- **itim**: API Field checks for TicketSLMBase
|
||||
- **itim**: API Field checks for TicketRequest
|
||||
- **core**: API fields Tests for TicketBase
|
||||
- **core**: API fields Unit Test Suite
|
||||
- **core**: Correct model notes test suite
|
||||
- **core**: API Permission Test Cases for ticket_base model
|
||||
- **api**: add API Permission Test Cases
|
||||
- **access**: Correct history link test cases
|
||||
- **project_management**: Add test cases for api field render for model fields `estimation_project` and `duration_project`
|
||||
- **human_resources**: History Serializer and ViewSet Functional test suites for employee
|
||||
- **human_resources**: APIv2, History, Model and ViewSet Unit test suites for employee
|
||||
- Migrate models to use refactored model tests
|
||||
- Consolidate All model tests to remove duplicates and to simplify
|
||||
|
||||
## 1.15.1 (2025-04-10)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **python**: Downgrade django 5.2 -> 5.1.8
|
||||
|
||||
## 1.15.0 (2025-04-10)
|
||||
|
||||
### feat
|
||||
|
||||
- **settings**: Move Ticket Comment Category from settings to ITOps menu
|
||||
- **settings**: Move Ticket Category from settings to ITOps menu
|
||||
- **access**: place roles nav behind feature flag 2025-00003
|
||||
- **access**: place directory nav behind feature flag 2025-00002
|
||||
- **accounting**: add new module
|
||||
- **access**: Ensure that the same person cant be created more than once
|
||||
- **access**: Place Roles Model behind feature flag `2025-00003`
|
||||
- **access**: When querying permissions, select related field `content_type`
|
||||
- **core**: Model tag for Access/Role
|
||||
- **access**: Model Role notes endpoint
|
||||
- **access**: Add navigation entry for roles
|
||||
- **access**: Model Role History migrations
|
||||
- **access**: Add model Role History
|
||||
- **access**: Role Notes model viewset
|
||||
- **access**: Role Notes model serializer
|
||||
- **access**: Model Role Notes migrations
|
||||
- **access**: Add model Role Notes
|
||||
- **access**: Role model viewset
|
||||
- **access**: Role model serializer
|
||||
- **access**: Model Role migrations
|
||||
- **access**: Add model Role
|
||||
- **python**: Upgrade Django 5.1.7 -> 5.2
|
||||
- **access**: Place Entity URLs behind feature flag `2025-00002`
|
||||
- **access**: Add detail page layout for contact model
|
||||
- **access**: Add Menu entry for corporate directory
|
||||
- **access**: Add back_url to Entity metadata
|
||||
- **core**: Add Entity model tag
|
||||
- **access**: Update Entity field `entity_type` if it does not match the entity type
|
||||
- **access**: All Entity models to use the entity history endpoint
|
||||
- **access**: Enable specifying the history model to use for audit history for a model
|
||||
- **access**: Enable specifying the kb model to use for linking kb article to a model
|
||||
- **access**: All Entity models to use the entity notes endpoint
|
||||
- **access**: Enable specifying the notes `basename` for a model
|
||||
- **access**: ViewSet for Entity Notes model
|
||||
- **access**: Serializer for Entity Notes model
|
||||
- **access**: new model Entity Notes
|
||||
- **access**: New model Entity History
|
||||
- **access**: Add Entity URL routes
|
||||
- **access**: new serializer Contact
|
||||
- **access**: new model Contact
|
||||
- **access**: new serializer Person
|
||||
- **access**: new model Person
|
||||
- **access**: new ViewSet for for Entity and sub-entities
|
||||
- **access**: new serializer Entity
|
||||
- **access**: new model Entity
|
||||
- **human_resources**: Add navigation menu entry for Human Resources (HR)
|
||||
- **human_resources**: Add module Human Resources (HR) to API Urls
|
||||
- **base**: Add module Human Resources (HR) to installed apps
|
||||
- Add module Human Resources (HR)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **api**: Correct documentation link to use models verbose name
|
||||
- **feature_flag**: cater for settings flag overrides
|
||||
- **access**: Add missing field directory to contact model
|
||||
- **settings**: Add Application Settings to Admin page
|
||||
- **access**: Remove app_namespace from Entity
|
||||
- **access**: add missing tenancy object fields to non-tenancy object models
|
||||
- **core**: Dont attempt to fetch history related objects if no history exists
|
||||
- **api**: Dont attempt to access kwargs if not exists within common serializer
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **core**: When saving history, ensure field `_prefetched_objects_cache` is not included
|
||||
|
||||
### Tests
|
||||
|
||||
- **settings**: Correct nav menu entry for Ticket Category and Ticket Comment Category
|
||||
- **access**: Ensure Model Contacts inherits from Person Model
|
||||
- **access**: Functional Test Suite for Contact API Metadata, API Permissions and ViewSet
|
||||
- **access**: Functional Test Suite for Contact serializer
|
||||
- **access**: Functional Test Suite for Contact history
|
||||
- **access**: Correct Entity and person functional Test Suite so sub-model testing works
|
||||
- **access**: Correct table_fields test case to cater for dynamic field
|
||||
- **access**: Unit Test for Contact ViewSet
|
||||
- **access**: Unit Test for Contact model
|
||||
- **access**: Unit Test for Contact history API field checks
|
||||
- **access**: Unit Test for Contact API field checks
|
||||
- **access**: Unit Test for Person Tenancy Object
|
||||
- **access**: Correct Entity and person unit Test Suite so sub-model testing works
|
||||
- **access**: Entity Function Serializer test cases
|
||||
- **access**: Person Model field test cases
|
||||
- **access**: Functional Test for Person ViewSet, Permissions and Metadata
|
||||
- **access**: Functional Test for Person History
|
||||
- **access**: Correct Entity Function Test Suite so sub-model testing works
|
||||
- **access**: Unit Test for Person ViewSet
|
||||
- **access**: Unit Test for Person Model
|
||||
- **access**: Unit Test for Person History API fields
|
||||
- **access**: Unit Test for Person API fields
|
||||
- **access**: Unit Test for Person Tenancy Object
|
||||
- **access**: Correct Entity Test Suite so sub-model testing works
|
||||
- **app**: exclude any field check that ends in `_ptr_id`
|
||||
- **access**: Remove teardown from Function Test cases for Role serializer
|
||||
- **access**: Test cases for Role serializer
|
||||
- **access**: Function Test cases for Role SPI Permissions, ViewSet and Metadata
|
||||
- **access**: Function Test cases for Role History
|
||||
- **access**: Unit Test case to ensure Role is by organization
|
||||
- **access**: Unit Test case to ensure Role cant be set as global object
|
||||
- **access**: Unit Test cases for Role ViewSet
|
||||
- **access**: Unit Test cases for Role model
|
||||
- **access**: Unit Test cases for Role History API v2
|
||||
- **access**: Unit Test cases for Role API v2
|
||||
- **access**: Unit Test cases for Role Tenancy Object
|
||||
- During testing add debug_feature_flags so object behind can be tested
|
||||
- **access**: Notes ViewSet Functional Tests for Entity Model
|
||||
- **access**: Notes API Field Functional Tests for Entity Model
|
||||
- **access**: Correct functional ViewSet test suite for Entity model
|
||||
- **access**: History functional Tests for Entity model
|
||||
- **access**: PermissionsAPI, ViewSet and Metadata Tests for Entity model
|
||||
- **access**: Model test cases for Entity
|
||||
- **access**: API Rendering test cases for Entity model
|
||||
- **api**: Ensure that when mocking the request the viewset is instantiated
|
||||
- **access**: History tests for Entity model
|
||||
- **access**: ViewSet tests for Entity model
|
||||
- **access**: Tenancy object test for Entity model
|
||||
|
||||
## 1.14.0 (2025-03-29)
|
||||
|
||||
### feat
|
||||
|
||||
- **itops**: Add navigation menu
|
||||
- New Module ITOps
|
||||
- **devops**: Ensure GitHub Groups can't be nested
|
||||
- **devops**: Models Git Repository must use organization from `git_group` as must group if parent set
|
||||
- **devops**: Add git provider badge to git_group table fields
|
||||
- **devops**: Add git provider badge to git_repository table fields
|
||||
- **devops**: Add Git GRoup to navigation
|
||||
- **itam**: Add `back_url` to Software Version ViewSet
|
||||
- **itam**: Add `back_url` to Operating System ViewSet
|
||||
- **devops**: Add `page_layout` to Git Group model
|
||||
- **devops**: Add `page_layout` to GitLab repository model
|
||||
- **devops**: Add `page_layout` to GitHub repository model
|
||||
- **devops**: git_repository ViewSet updated to fetch queryset based off of repository provider
|
||||
- **devops**: Add ti git_repository ViewSet return and back urls
|
||||
- **devops**: Make fields `provider` and `provider_id` unique_together for git_repository model
|
||||
- **devops**: Add fields to ALL git_repository serializers
|
||||
- **devops**: Add fetching of URL to base git_repository model
|
||||
- **api**: Enable fetching of app_namespace from model
|
||||
- **access**: Add function get_page_layout
|
||||
- **feature_flag**: Provide user with ability to override feature flags
|
||||
- **base**: Add middleware feature_flag
|
||||
- **devops**: Disable notes for GIT Repository Base Model
|
||||
- **devops**: Add git_repository model tag migration
|
||||
- **devops**: Add git_repository as a model that can be linked to a ticket
|
||||
- **devops**: Git Group Notes Migration
|
||||
- **devops**: Git Group Notes ViewSet
|
||||
- **devops**: Git Group Notes Serializer
|
||||
- **devops**: Git Group Notes Model
|
||||
- **devops**: GitHub and GitLab Repository Notes Migrations
|
||||
- **devops**: GitLab Repository Notes Viewset
|
||||
- **devops**: GitHub Repository Notes Viewset
|
||||
- **devops**: GitLab Repository Notes Serializer
|
||||
- **devops**: GitHub Repository Notes Serializer
|
||||
- **devops**: GitLab Repository Notes Model
|
||||
- **devops**: GitHub Repository Notes Model
|
||||
- **devops**: Git Group History Migrations
|
||||
- **devops**: Git Group History
|
||||
- **devops**: GitLab and GitHub Repository History Migrations
|
||||
- **devops**: GitLab Repository History
|
||||
- **devops**: GitHub Repository History
|
||||
- **devops**: [2025-00001] Git Group and Repositories URLs
|
||||
- **devops**: Git Group and Repositories Migrations
|
||||
- **devops**: GIT Group ViewSet
|
||||
- **devops**: GIT Group Serializer
|
||||
- **devops**: GIT Group Model
|
||||
- **devops**: GIT Repositories Viewset
|
||||
- **devops**: GitLab Serializer for git repositories
|
||||
- **devops**: GitHub Serializer for git repositories
|
||||
- **devops**: Base Serializer for git repositories
|
||||
- **devops**: GitLab Repository Model
|
||||
- **devops**: GitHub Repository Model
|
||||
- **devops**: Base model for git repositories
|
||||
- **core**: Enable slash command related ticket to have multiple ticket references
|
||||
- **core**: Enable slash command linked model to have multiple models
|
||||
- **core**: process ticket slash commands by line
|
||||
- **core**: Migrations for new slash commands
|
||||
- **project_management**: Add project_state slash command
|
||||
- **core**: Add ticket_comment_category slash command
|
||||
- **core**: Add ticket_category slash command
|
||||
- **itam**: when displaying software version, add prefix with software name
|
||||
- **itam**: Add markdown tag $software_version
|
||||
- **itam**: Enable ticket tab on software version page
|
||||
|
||||
### Fixes
|
||||
|
||||
- **devops**: Correct git_group serializer parameter name
|
||||
- **devops**: Correct field path to no be unique for git_repository
|
||||
- **feature_flag**: if over_rides not set ensure val set to empty dict
|
||||
- **devops**: git_group serializers must define fields
|
||||
- **devops**: git_group serializers must return urls
|
||||
- **devops**: Correct git_repository notes urls
|
||||
- **devops**: Correct git_repository url regex
|
||||
- **devops**: Correct ViewSerializer for GitLab Repository
|
||||
- **devops**: Correct ViewSerializer for GitHib Repository
|
||||
- **devops**: Correct model git_group modified field name part 2
|
||||
- **devops**: Correct model git_group modified field name
|
||||
- **api**: Fetching of serializer_class must be dynamic
|
||||
- **core**: Don't create an empty ticket comment if the body is empty when slash commands removed
|
||||
- **core**: when processing slash commands trim each line prior to processing
|
||||
- **core**: slash command NL char is `\r\n` not `\n`, however support both
|
||||
- **core**: When processing slash commands trim whitespace on return
|
||||
- **core**: Ensure linked ticket models are unique
|
||||
- **itam**: Add back url to software_version model
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **devops**: remove model unique_together constraint for git group and repository
|
||||
- **devops**: Field `provider_id` must not be user editable for git group or repository
|
||||
- **api**: mv _nav property to function get_nav_items
|
||||
|
||||
### Tests
|
||||
|
||||
- **api**: Correct test cases for view_name and view_description
|
||||
- Refactor all ViewSet Unit Test cases to use new test cases class
|
||||
- **api**: Common ViewSet classes Tests and Test cases for classes that inherit them
|
||||
- **api**: correct nav menu setup to use mock request
|
||||
- **core**: un-mark tests as skipped so that multiple linked items per ticket can be tested
|
||||
- **core**: correct ticket linked item to prevent duplicate creation
|
||||
|
||||
## 1.13.1 (2025-03-17)
|
||||
|
||||
### Fixes
|
||||
|
||||
- **devops**: After fetching feature flags dont attempt to access results unless status=200
|
||||
- **docker**: only download feature flags when not a worker
|
||||
- **devops**: Use correct stderr function when using feature_flag management command
|
||||
- **devops**: Cater for connection timeout when fetching feature flags
|
||||
- when building feature flag version, use first 8 chars of build hash
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **docker**: Use crontabs not cron.d
|
||||
|
||||
## 1.13.0 (2025-03-16)
|
||||
|
||||
### feat
|
||||
|
||||
- **devops**: Add ability for user to turn off feature flagging check-in
|
||||
- **devops**: When displaying the feature_flag deployments, limit to last 24-hours
|
||||
- **devops**: During feature flag `Checkin` derive the version from the last field of the user-agent
|
||||
- **devops**: Add missing column to model `Checkin`
|
||||
- **devops**: Remove model `Checkin` permissions from permissions selector
|
||||
- **devops**: Display the days total unique check-ins for feature flags within software feature flagging tab
|
||||
- **devops**: Record to check-in table every time feature flags are obtained
|
||||
- **devops**: Migrations for model `CheckIns`
|
||||
- **devops**: New model `CheckIns`
|
||||
- Generate a deployment unique ID
|
||||
- **devops**: Provide user with option to disable downloading feature flags
|
||||
- **devops**: Feature Flagging url.path wrapper
|
||||
- **docker**: Configure cron to download feature flags every four hours
|
||||
- **docker**: Start and run crond within container
|
||||
- **docker**: Download feature flags on container start
|
||||
- **devops**: Feature Flagging DRF Router wrapper
|
||||
- **devops**: Feature Flagging middleware
|
||||
- **devops**: Feature Flagging management command
|
||||
- **devops**: Add Feature Flagging lib
|
||||
- **devops**: add temp application for feature flag client
|
||||
- **devops**: public feature flag endpoint pagination limited to 20 results
|
||||
- **devops**: Add support for `if-modified-since` header for Feature Flags public endpoint
|
||||
- **api**: Add public API feature flag index endpoint
|
||||
- **api**: Add public API endpoint
|
||||
- **devops**: Add feature flag public ViewSet
|
||||
- **devops**: Add feature flag public serializer
|
||||
- **api**: Add common viewset for public RO list
|
||||
- Remove serializer caching from ALL viewsets
|
||||
- **devops**: Add delete col to software enabled feature flags
|
||||
- **devops**: Prevent deletion of software when it has feature flagging enabled and/or feature flags
|
||||
- **devops**: limit feature_flag to organizations that's had feature flags enabled
|
||||
- **devops**: limit feature_flag to software that's had feature flags enabled
|
||||
- **python**: Update Django 5.1.5 -> 5.1.7
|
||||
- **devops**: Serializer limiting of software and os disabled for time being
|
||||
- **devops**: Serializer validate software and org
|
||||
- **devops**: Serializer software filter to enabled feature_flag software
|
||||
- **devops**: Serializer org filter to enabled feature_flag organizations
|
||||
- **devops**: Add endpoint for enabling software for feature flagging
|
||||
- **devops**: Add serializer for enabling software for feature flagging
|
||||
- **devops**: Add model for enabling software for feature flagging
|
||||
- **devops**: Add model tag feature_flag to ticket linked item
|
||||
- **devops**: Add KB tab to feature flag model
|
||||
- **devops**: Add Notes to feature flag model
|
||||
- **core**: Migration for feature_flag model reference
|
||||
- **core**: url endpoints added for ticket comment category and ticket category notes
|
||||
- **itam**: disable model notes for model device os
|
||||
- **api**: disable model notes for model auth token
|
||||
- **core**: disable model notes for model teamuser
|
||||
- **core**: disable model notes for model notes
|
||||
- **core**: Migrations for adding notes to ticket category and ticket comment category
|
||||
- **core**: Add Feature Flag model reference
|
||||
- **devops**: Add devops module to installed applications
|
||||
- **devops**: Add Feature Flag viewset
|
||||
- **devops**: Add Feature Flag serializer
|
||||
- **devops**: Add devops Navigation menu
|
||||
- **devops**: Add devops module URL includes
|
||||
- **devops**: Add devops to permissions
|
||||
- **devops**: DB Migrations for Feature Flag and History model
|
||||
- **devops**: Add Feature Flag History model
|
||||
- **devops**: Add Feature Flag model
|
||||
- **access**: add support for nested application namespaces
|
||||
- **devops**: Add devops module
|
||||
|
||||
### Fixes
|
||||
|
||||
- **devops**: Only track checkin if no other error occured
|
||||
- **devops**: during feature flag checkin, if no `client-id` provided, use value `not-provided`
|
||||
- **devops**: When init the feature flag clients, look for all args within settings
|
||||
- **devops**: Only add `Last-Modified` header to response if exists
|
||||
- **devops**: Correct logic for data changed check for public endpoint for feature flagging
|
||||
- **devops**: feature flag public ViewSet serializer name correction and qs cache correction
|
||||
- **devops**: feature flag public endpoint field modified name typo
|
||||
- **devops**: Filter public feature flag endpoint to org and software where software is enabled
|
||||
- **devops**: Move software field filter for feature flag to the serializer
|
||||
- **devops**: Dont attempt to validate feature flag software or organization if it is absent
|
||||
- **devops**: Correct feature flagging validation for enabled software and enabled orgs
|
||||
- **devops**: dont cache serializer for featureflag
|
||||
- **devops**: Correct Feature Flag serializer validation to cater for edit
|
||||
- **devops**: Feature Flag field is mandatory
|
||||
- **api**: make history url dynamic. only display if history should save
|
||||
- **devops**: if software is deleted delete feature flags
|
||||
- **core**: disable of notes for models not requiring it
|
||||
- **api**: when generating notes url, use correct object
|
||||
- **api**: Add missing import for featurenotused
|
||||
- **core**: Add ability to add notes for ticket comment category
|
||||
- **core**: Add ability to add notes for ticket category
|
||||
- **core**: Serializer `_urls.notes` URL generation now dynamic
|
||||
- **api**: Dont attempt to access model.get_app_namespace if it doesnt exist
|
||||
|
||||
### Tests
|
||||
|
||||
- **devops**: Feature Flag History API render checks
|
||||
- **devops**: Feature Flag Serializer checks
|
||||
- **devops**: CheckIn Entry created of fetching feature flags
|
||||
- **devops**: CheckIn model test cases
|
||||
- **devops**: public feature flag fields corrections
|
||||
- **devops**: public feature flag functional ViewSet checks
|
||||
- **devops**: feature flag ViewSet checks
|
||||
- **api**: Update vieset test cases to cater for mockrequest to contain headers attribute
|
||||
- **devops**: feature flag public endpoint API field, header checks
|
||||
- **devops**: Ensure that only enabled org and enabled software is possible
|
||||
- **devops**: software_feature_flag_enable ViewSet checks
|
||||
- **devops**: software_feature_flag_enable Serializer checks
|
||||
- **devops**: Update feature flag test case setup to enable feature flag for testing software
|
||||
- **devops**: Update feature flag test case setup to enable feature flag for testing software
|
||||
- **api**: Remove serializer cache test cases
|
||||
- **devops**: software_feature_flag_enable api field checks
|
||||
- **devops**: software_feature_flag_enable viewset checks
|
||||
- **devops**: software_feature_flag_enable model checks
|
||||
- **devops**: software_feature_flag_enable tenancy object checks
|
||||
- **devops**: correct dir name for tests
|
||||
- **devops**: Notes feature flag model checks
|
||||
- **core**: Ticket Comment Category Notes checks
|
||||
- **core**: Ticket Category Notes checks
|
||||
- **app**: Model test cases for api field rendering `_urls.notes`
|
||||
- **app**: Model test cases for get_url_kwargs_notes function
|
||||
- **access**: Correct Team notes url route name
|
||||
- **devops**: Feature Flag viewset unit Checks
|
||||
- **devops**: Feature Flag model Checks
|
||||
- **devops**: Feature Flag api Checks
|
||||
- **devops**: Feature Flag tenancy object Checks
|
||||
- **devops**: Feature Flag viewset functional Checks
|
||||
- **devops**: Feature Flag serializer Checks
|
||||
- **devops**: Feature Flag History Checks
|
||||
|
||||
## 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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
134
Release-Notes.md
134
Release-Notes.md
@ -1,3 +1,137 @@
|
||||
## Version 1.17.0
|
||||
|
||||
- Added setting for log files.
|
||||
|
||||
Enables user to specify a default path for centurion's logging. Add the following to your settings file `/etc/itsm/settings.py`
|
||||
|
||||
``` py
|
||||
LOG_FILES = {
|
||||
"centurion": "/var/log/centurion.log", # Normal Centurion Operations
|
||||
"weblog": "/var/log/weblog.log", # All web requests made to Centurion
|
||||
"rest_api": "/var/log/rest_api.log", # Rest API
|
||||
"catch_all":"/var/log/catch-all.log" # A catch all log. Note: does not log anything that has already been logged.
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
With this new setting, the previous setting `LOGGING` will no longer function.
|
||||
|
||||
- Renamed `Organization` model to `Tenant` so as to reflect what is actually is.
|
||||
|
||||
- `robots.txt` file now being served from the API container at path `/robots.txt` with `User-agent: *` and `Disallow: /`
|
||||
|
||||
|
||||
## Version 1.16.0
|
||||
|
||||
- Employees model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
|
||||
|
||||
- Ticket and Ticket Comment added behind feature flag `2025-00006` and will remain behind this flag until production ready.
|
||||
|
||||
- In preparation of the [Ticket and Ticket Comment model re-write](https://github.com/nofusscomputing/centurion_erp/issues/564)
|
||||
|
||||
- Depreciated Change Ticket
|
||||
|
||||
- Depreciated Ticket Comment Endpoint
|
||||
|
||||
- Depreciated Request Ticket
|
||||
|
||||
- Depreciated Incident Ticket
|
||||
|
||||
- Depreciated Problem Ticket
|
||||
|
||||
- Depreciated Project Task Ticket
|
||||
|
||||
These endpoints still work and will remain so until the new Ticket and Ticket Comment Models are production ready.
|
||||
|
||||
|
||||
## Version 1.15.0
|
||||
|
||||
- Entities model added behind feature flag `2025-00002` and will remain behind this flag until production ready.
|
||||
|
||||
- Roles model added behind feature flag `2025-00003` and will remain behind this flag until production ready.
|
||||
|
||||
- Accounting Module added behind feature flag `2025-00004` and will remain behind this flag until production ready.
|
||||
|
||||
|
||||
## Version 1.14.0
|
||||
|
||||
- Git Repository and Git Group Models added behind feature flag `2025-00001`. They will remain behind this feature flag until the Git features are fully developed and ready for use.
|
||||
|
||||
|
||||
## Version 1.13.0
|
||||
|
||||
- DevOps Module added.
|
||||
|
||||
- Feature Flagging Component added as par of the DevOps module.
|
||||
|
||||
|
||||
## 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.
|
||||
|
@ -1,17 +0,0 @@
|
||||
[run]
|
||||
source = .
|
||||
omit =
|
||||
*migrations/*
|
||||
*tests/*/*
|
||||
|
||||
[report]
|
||||
omit =
|
||||
*/tests/*/*
|
||||
*/migrations/*
|
||||
*apps.py
|
||||
*manage.py
|
||||
*__init__.py
|
||||
*asgi*
|
||||
*wsgi*
|
||||
*admin.py
|
||||
*urls.py
|
@ -1,8 +1,14 @@
|
||||
import django
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.auth.admin import UserAdmin
|
||||
|
||||
from .models import *
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
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.tenant import Tenant as 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
|
||||
|
||||
|
@ -9,10 +9,13 @@ def permission_queryset():
|
||||
|
||||
apps = [
|
||||
'access',
|
||||
'accounting',
|
||||
'assistance',
|
||||
'config_management',
|
||||
'core',
|
||||
'devops',
|
||||
'django_celery_results',
|
||||
'human_resources',
|
||||
'itam',
|
||||
'itim',
|
||||
'project_management',
|
||||
@ -24,20 +27,32 @@ def permission_queryset():
|
||||
'chordcounter',
|
||||
'comment',
|
||||
'groupresult',
|
||||
'history',
|
||||
'modelnotes',
|
||||
'usersettings',
|
||||
]
|
||||
|
||||
exclude_permissions = [
|
||||
'add_checkin',
|
||||
'add_history',
|
||||
'add_organization',
|
||||
'add_taskresult',
|
||||
'add_ticketcommentaction',
|
||||
'change_checkin',
|
||||
'change_history',
|
||||
'change_organization',
|
||||
'change_taskresult',
|
||||
'change_ticketcommentaction',
|
||||
'delete_checkin',
|
||||
'delete_history',
|
||||
'delete_organization',
|
||||
'delete_taskresult',
|
||||
'delete_ticketcommentaction',
|
||||
'view_checkin',
|
||||
'view_history',
|
||||
]
|
||||
|
||||
return Permission.objects.filter(
|
||||
return Permission.objects.select_related('content_type').filter(
|
||||
content_type__app_label__in=apps,
|
||||
).exclude(
|
||||
content_type__model__in=exclude_models
|
||||
|
@ -1,15 +1,22 @@
|
||||
import django
|
||||
|
||||
from django.contrib.auth.middleware import (
|
||||
AuthenticationMiddleware,
|
||||
SimpleLazyObject,
|
||||
partial,
|
||||
)
|
||||
from django.contrib.auth.models import User, Group
|
||||
from django.contrib.auth.models import Group
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
|
||||
from access.models import Organization, Team
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
class RequestTenancy(MiddlewareMixin):
|
||||
"""Access Middleware
|
||||
|
@ -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',
|
||||
|
@ -16,7 +16,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.Team.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.team.Team.validatate_organization_exists], verbose_name='Organization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizationNotes',
|
||||
|
@ -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',),
|
||||
),
|
||||
]
|
@ -0,0 +1,140 @@
|
||||
# Generated by Django 5.2 on 2025-04-10 02:34
|
||||
|
||||
import access.fields
|
||||
import access.models.tenancy
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0004_organizationhistory_teamhistory'),
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('core', '0021_alter_ticketlinkeditem_item_type'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Entity',
|
||||
fields=[
|
||||
('is_global', models.BooleanField(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')),
|
||||
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
|
||||
('entity_type', models.CharField(default='entity', help_text='Type this entity is', max_length=30, verbose_name='Entity Type')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
|
||||
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Entity',
|
||||
'verbose_name_plural': 'Entities',
|
||||
'ordering': ['created', 'modified', 'organization'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Person',
|
||||
fields=[
|
||||
('entity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.entity')),
|
||||
('f_name', models.CharField(help_text='The persons first name', max_length=64, verbose_name='First Name')),
|
||||
('m_name', models.CharField(blank=True, default=None, help_text='The persons middle name(s)', max_length=100, null=True, verbose_name='Middle Name(s)')),
|
||||
('l_name', models.CharField(help_text='The persons Last name', max_length=64, verbose_name='Last Name')),
|
||||
('dob', models.DateField(blank=True, default=None, help_text='The Persons Date of Birth (DOB)', null=True, verbose_name='DOB')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Person',
|
||||
'verbose_name_plural': 'People',
|
||||
'ordering': ['l_name', 'm_name', 'f_name', 'dob'],
|
||||
},
|
||||
bases=('access.entity',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EntityHistory',
|
||||
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.entity', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Entity History',
|
||||
'verbose_name_plural': 'Entity History',
|
||||
'db_table': 'access_entity_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EntityNotes',
|
||||
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.entity', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Entity Note',
|
||||
'verbose_name_plural': 'Entity Notes',
|
||||
'db_table': 'access_entity_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('model_notes', models.TextField(blank=True, default=None, help_text='Tid bits of information', null=True, verbose_name='Notes')),
|
||||
('id', models.AutoField(help_text='Primary key of the entry', primary_key=True, serialize=False, unique=True, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Name of this role', max_length=30, verbose_name='Name')),
|
||||
('created', access.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of creation', verbose_name='Created')),
|
||||
('modified', access.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, help_text='Date and time of last modification', verbose_name='Modified')),
|
||||
('organization', models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.organization', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Organization')),
|
||||
('permissions', models.ManyToManyField(blank=True, help_text='Permissions part of this role', related_name='roles', to='auth.permission', verbose_name='Permissions')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Role',
|
||||
'verbose_name_plural': 'Roles',
|
||||
'ordering': ['organization', 'name'],
|
||||
'unique_together': {('organization', 'name')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleHistory',
|
||||
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.role', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Role History',
|
||||
'verbose_name_plural': 'Role History',
|
||||
'db_table': 'access_role_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoleNotes',
|
||||
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.role', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Role Note',
|
||||
'verbose_name_plural': 'Role Notes',
|
||||
'db_table': 'access_role_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Contact',
|
||||
fields=[
|
||||
('person_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.person')),
|
||||
('directory', models.BooleanField(blank=True, default=True, help_text='Show contact details in directory', verbose_name='Show in Directory')),
|
||||
('email', models.EmailField(help_text='E-mail address for this person', max_length=254, unique=True, verbose_name='E-Mail')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Contact',
|
||||
'verbose_name_plural': 'Contacts',
|
||||
'ordering': ['email'],
|
||||
},
|
||||
bases=('access.person',),
|
||||
),
|
||||
]
|
25
app/access/migrations/0007_rename_organization_tenant.py
Normal file
25
app/access/migrations/0007_rename_organization_tenant.py
Normal file
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-14 11:06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0005_entity_person_entityhistory_entitynotes_role_and_more'),
|
||||
('assistance', '0005_knowledgebasecategoryhistory_knowledgebasehistory'),
|
||||
('config_management', '0007_configgroupshistory_configgrouphostshistory_and_more'),
|
||||
('core', '0022_ticketcommentbase_ticketbase_ticketcommentsolution_and_more'),
|
||||
('devops', '0011_alter_gitgroup_unique_together_and_more'),
|
||||
('itam', '0010_alter_software_organization'),
|
||||
('itim', '0009_slmticket_requestticket'),
|
||||
('project_management', '0005_projecthistory_projectmilestonehistory_and_more'),
|
||||
('settings', '0011_appsettingshistory_externallinkhistory'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name = 'Organization',
|
||||
new_name = 'Tenant'
|
||||
),
|
||||
]
|
@ -0,0 +1,47 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-14 13:48
|
||||
|
||||
import access.models.team
|
||||
import access.models.tenancy
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0007_rename_organization_tenant'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='tenant',
|
||||
options={'ordering': ['name'], 'verbose_name': 'Tenant', 'verbose_name_plural': 'Tenants'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entity',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Tenancy this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.tenant', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Tenant'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='role',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Tenancy this belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='+', to='access.tenant', validators=[access.models.tenancy.TenancyObject.validatate_organization_exists], verbose_name='Tenant'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Tenant this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.tenant', validators=[access.models.team.Team.validatate_organization_exists], verbose_name='Tenant'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenant',
|
||||
name='manager',
|
||||
field=models.ForeignKey(help_text='Manager for this Tenancy', null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Manager'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='tenant',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Name of this Tenancy', max_length=50, unique=True, verbose_name='Name'),
|
||||
),
|
||||
]
|
@ -0,0 +1,135 @@
|
||||
|
||||
from django.contrib.auth.models import ContentType, Permission
|
||||
from django.db import migrations
|
||||
|
||||
from access.models.team import Team
|
||||
|
||||
ContentType.DoesNotExist
|
||||
|
||||
def add_tenancy_permissions(apps, schema_editor):
|
||||
|
||||
print('')
|
||||
print(f"Begin permission migration for rename of Organization to Tenant.")
|
||||
|
||||
try:
|
||||
|
||||
add_permission = Permission.objects.get(
|
||||
codename = 'add_tenant',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = 'access',
|
||||
model = 'tenant',
|
||||
)
|
||||
)
|
||||
|
||||
change_permission = Permission.objects.get(
|
||||
codename = 'change_tenant',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = 'access',
|
||||
model = 'tenant',
|
||||
)
|
||||
)
|
||||
|
||||
delete_permission = Permission.objects.get(
|
||||
codename = 'delete_tenant',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = 'access',
|
||||
model = 'tenant',
|
||||
)
|
||||
)
|
||||
|
||||
view_permission = Permission.objects.get(
|
||||
codename = 'view_tenant',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = 'access',
|
||||
model = 'tenant',
|
||||
)
|
||||
)
|
||||
|
||||
print(f' Searching for Teams.')
|
||||
|
||||
teams = Team.objects.select_related('group_ptr__permissions')
|
||||
|
||||
print(f'Found {str(len(teams))} Teams.')
|
||||
|
||||
for team in teams:
|
||||
|
||||
print(f' Processing Team {str(team.team_name)}.')
|
||||
|
||||
permissions = team.group_ptr.permissions.all()
|
||||
|
||||
print(f' Searching for Organization Permissions.')
|
||||
print(f' Found {str(len(permissions))} Permissions.')
|
||||
|
||||
for permission in permissions:
|
||||
|
||||
if '_organization' not in permission.codename:
|
||||
|
||||
continue
|
||||
|
||||
action = str(permission.codename).split('_')[0]
|
||||
|
||||
print(f' Found Organization Permission {str(action)}')
|
||||
|
||||
if action == 'add':
|
||||
|
||||
team.group_ptr.permissions.add( add_permission )
|
||||
|
||||
print(f' Add Tenant Permission {str(action)}')
|
||||
|
||||
team.group_ptr.permissions.remove( permission )
|
||||
|
||||
print(f' Remove Organization Permission {str(action)}')
|
||||
|
||||
elif action == 'change':
|
||||
|
||||
team.group_ptr.permissions.add( change_permission )
|
||||
|
||||
print(f' Add Tenant Permission {str(action)}')
|
||||
|
||||
team.group_ptr.permissions.remove( permission )
|
||||
|
||||
print(f' Remove Organization Permission {str(action)}')
|
||||
|
||||
elif action == 'delete':
|
||||
|
||||
team.group_ptr.permissions.add( delete_permission )
|
||||
|
||||
print(f' Add Tenant Permission {str(action)}')
|
||||
|
||||
team.group_ptr.permissions.remove( permission )
|
||||
|
||||
print(f' Remove Organization Permission {str(action)}')
|
||||
|
||||
elif action == 'view':
|
||||
|
||||
team.group_ptr.permissions.add( view_permission )
|
||||
|
||||
print(f' Add Tenant Permission {str(action)}')
|
||||
|
||||
team.group_ptr.permissions.remove( permission )
|
||||
|
||||
print(f' Remove Organization Permission {str(action)}')
|
||||
|
||||
|
||||
print(f' Completed Team {str(team.team_name)}.')
|
||||
|
||||
except ContentType.DoesNotExist:
|
||||
# DB is new so no content types. no migration to be done.
|
||||
pass
|
||||
|
||||
print(' Permission Migration Actions Complete.')
|
||||
|
||||
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0008_alter_tenant_options_alter_entity_organization_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(add_tenancy_permissions),
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-16 09:58
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0009_migrate_organization_permission_tenant'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Company',
|
||||
fields=[
|
||||
('entity_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='access.entity')),
|
||||
('name', models.CharField(help_text='The name of this entity', max_length=80, verbose_name='Name')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Company',
|
||||
'verbose_name_plural': 'Companies',
|
||||
'ordering': ['name'],
|
||||
'sub_model_type': 'company',
|
||||
},
|
||||
bases=('access.entity',),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='entity',
|
||||
name='entity_type',
|
||||
field=models.CharField(help_text='Type this entity is', max_length=30, verbose_name='Entity Type'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='dob',
|
||||
field=models.DateField(blank=True, help_text='The Persons Date of Birth (DOB)', null=True, verbose_name='DOB'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='person',
|
||||
name='m_name',
|
||||
field=models.CharField(blank=True, help_text='The persons middle name(s)', max_length=100, null=True, verbose_name='Middle Name(s)'),
|
||||
),
|
||||
]
|
@ -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.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
|
||||
|
||||
|
||||
class OrganizationMixin():
|
||||
@ -258,7 +260,7 @@ class OrganizationMixin():
|
||||
self.permission_required = permissions_required
|
||||
|
||||
organization_manager_models = [
|
||||
'access.organization',
|
||||
'access.tenant',
|
||||
'access.team',
|
||||
'access.teamusers',
|
||||
]
|
||||
@ -324,7 +326,7 @@ class OrganizationMixin():
|
||||
|
||||
if required_permission.replace(
|
||||
'view_', ''
|
||||
) == 'access.organization' and len(self.kwargs) == 0:
|
||||
) == 'access.tenant' and len(self.kwargs) == 0:
|
||||
|
||||
return True
|
||||
|
||||
|
@ -1,7 +1,13 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
import django
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
|
||||
from access.models import Organization, Team
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class OrganizationMixin:
|
||||
@ -87,7 +93,7 @@ class OrganizationMixin:
|
||||
|
||||
self._obj_organization = obj.organization
|
||||
|
||||
elif str(self.model._meta.verbose_name).lower() == 'organization':
|
||||
elif str(self.model._meta.verbose_name).lower() == 'tenant':
|
||||
|
||||
self._obj_organization = obj
|
||||
|
||||
@ -128,7 +134,7 @@ class OrganizationMixin:
|
||||
parent_model (Model): with PK from kwargs['pk']
|
||||
"""
|
||||
|
||||
return self.parent_model.objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
|
||||
return self.get_parent_model().objects.get(pk=self.kwargs[self.parent_model_pk_kwarg])
|
||||
|
||||
|
||||
|
||||
|
@ -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 Tenant, TenancyObject
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
|
||||
@ -14,10 +14,10 @@ from core import exceptions as centurion_exceptions
|
||||
class OrganizationPermissionMixin(
|
||||
DjangoObjectPermissions,
|
||||
):
|
||||
"""Organization Permission Mixin
|
||||
"""Tenant Permission Mixin
|
||||
|
||||
This class is to be used as the permission class for API `Views`/`ViewSets`.
|
||||
In combination with the `OrganizationPermissionsMixin`, permission checking
|
||||
In combination with the `TenantPermissionsMixin`, permission checking
|
||||
will be done to ensure the user has the correct permissions to perform the
|
||||
CRUD operation.
|
||||
|
||||
@ -116,15 +116,27 @@ class OrganizationPermissionMixin(
|
||||
try:
|
||||
|
||||
if (
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
(
|
||||
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))
|
||||
(
|
||||
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))
|
||||
)
|
||||
):
|
||||
|
||||
|
||||
@ -154,7 +166,7 @@ class OrganizationPermissionMixin(
|
||||
raise centurion_exceptions.PermissionDenied()
|
||||
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj_organization: Tenant = view.get_obj_organization(
|
||||
request = request
|
||||
)
|
||||
|
||||
@ -271,8 +283,14 @@ class OrganizationPermissionMixin(
|
||||
|
||||
|
||||
if (
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
(
|
||||
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
|
||||
|
@ -1,706 +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,
|
||||
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):
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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')
|
||||
|
||||
|
||||
|
||||
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:
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 )
|
4
app/access/models/__init__.py
Normal file
4
app/access/models/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from . import contact
|
||||
from . import company_base
|
||||
from . import person
|
||||
from . import role
|
102
app/access/models/company_base.py
Normal file
102
app/access/models/company_base.py
Normal file
@ -0,0 +1,102 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
|
||||
|
||||
class Company(
|
||||
Entity
|
||||
):
|
||||
# This model is intended to be called `Organization`, however at the time of
|
||||
# creation this was not possible as Tenant (ne Organization) still has
|
||||
# references in code to `organization` witch clashes with the intended name of
|
||||
# this model.
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'name',
|
||||
]
|
||||
|
||||
sub_model_type = 'company'
|
||||
|
||||
verbose_name = 'Company'
|
||||
|
||||
verbose_name_plural = 'Companies'
|
||||
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'The name of this entity',
|
||||
max_length = 80,
|
||||
unique = False,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
return self.name
|
||||
|
||||
|
||||
documentation = ''
|
||||
|
||||
history_model_name = 'company'
|
||||
|
||||
page_layout: dict = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'name',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
'created',
|
||||
'modified',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tickets",
|
||||
"slug": "tickets",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "tickets",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
table_fields: list = [
|
||||
'name',
|
||||
'organization',
|
||||
'created',
|
||||
]
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
119
app/access/models/contact.py
Normal file
119
app/access/models/contact.py
Normal file
@ -0,0 +1,119 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.person import Person
|
||||
|
||||
|
||||
|
||||
class Contact(
|
||||
Person
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'email',
|
||||
]
|
||||
|
||||
sub_model_type = 'contact'
|
||||
|
||||
verbose_name = 'Contact'
|
||||
|
||||
verbose_name_plural = 'Contacts'
|
||||
|
||||
|
||||
directory = models.BooleanField(
|
||||
blank = True,
|
||||
default = True,
|
||||
help_text = 'Show contact details in directory',
|
||||
null = False,
|
||||
verbose_name = 'Show in Directory',
|
||||
)
|
||||
|
||||
email = models.EmailField(
|
||||
blank = False,
|
||||
help_text = 'E-mail address for this person',
|
||||
unique = True,
|
||||
verbose_name = 'E-Mail',
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
return self.f_name + ' ' + self.l_name
|
||||
|
||||
documentation = ''
|
||||
|
||||
history_model_name = 'contact'
|
||||
|
||||
page_layout: list = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
'directory',
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Personal Details",
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'display_name',
|
||||
'dob',
|
||||
],
|
||||
"right": [
|
||||
'f_name',
|
||||
'm_name',
|
||||
'l_name',
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "",
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'email',
|
||||
],
|
||||
"right": [
|
||||
'',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
table_fields: list = [
|
||||
{
|
||||
"field": "display_name",
|
||||
"type": "link",
|
||||
"key": "_self"
|
||||
},
|
||||
'f_name',
|
||||
'l_name',
|
||||
'email',
|
||||
'organization',
|
||||
'created',
|
||||
]
|
249
app/access/models/entity.py
Normal file
249
app/access/models/entity.py
Normal file
@ -0,0 +1,249 @@
|
||||
from django.db import models
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import AutoCreatedField, AutoLastModifiedField
|
||||
from access.models.tenancy import TenancyObject
|
||||
|
||||
from core.lib.feature_not_used import FeatureNotUsed
|
||||
|
||||
|
||||
|
||||
class Entity(
|
||||
TenancyObject
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'created',
|
||||
'modified',
|
||||
'organization',
|
||||
]
|
||||
|
||||
sub_model_type = 'entity'
|
||||
|
||||
verbose_name = 'Entity'
|
||||
|
||||
verbose_name_plural = 'Entities'
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'Primary key of the entry',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
|
||||
entity_type = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Type this entity is',
|
||||
max_length = 30,
|
||||
unique = False,
|
||||
verbose_name = 'Entity Type'
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
related_model = self.get_related_model()
|
||||
|
||||
if related_model is None:
|
||||
|
||||
return f'{self.entity_type} {self.pk}'
|
||||
|
||||
|
||||
return str( related_model )
|
||||
|
||||
|
||||
|
||||
# app_namespace = 'access'
|
||||
|
||||
history_app_label = 'access'
|
||||
|
||||
history_model_name = 'entity'
|
||||
|
||||
kb_model_name = 'entity'
|
||||
|
||||
note_basename = '_api_v2_entity_note'
|
||||
|
||||
documentation = ''
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
|
||||
table_fields: list = [
|
||||
'organization',
|
||||
'entity_type',
|
||||
'display_name',
|
||||
'created',
|
||||
'modified',
|
||||
]
|
||||
|
||||
|
||||
def get_related_field_name(self) -> str:
|
||||
|
||||
meta = getattr(self, '_meta')
|
||||
|
||||
for related_object in getattr(meta, 'related_objects', []):
|
||||
|
||||
if not issubclass(related_object.related_model, Entity):
|
||||
|
||||
continue
|
||||
|
||||
if getattr(self, related_object.name, None):
|
||||
|
||||
if(
|
||||
not str(related_object.name).endswith('history')
|
||||
and not str(related_object.name).endswith('notes')
|
||||
):
|
||||
|
||||
return related_object.name
|
||||
break
|
||||
|
||||
|
||||
return ''
|
||||
|
||||
|
||||
def get_related_model(self):
|
||||
"""Recursive model Fetch
|
||||
|
||||
Returns the lowest model found in a chain of inherited models.
|
||||
|
||||
Args:
|
||||
model (models.Model, optional): Model to fetch the child model from. Defaults to None.
|
||||
|
||||
Returns:
|
||||
models.Model: Lowset model found in inherited model chain
|
||||
"""
|
||||
|
||||
related_model_name = self.get_related_field_name()
|
||||
|
||||
related_model = getattr(self, related_model_name, None)
|
||||
|
||||
if related_model_name == '':
|
||||
|
||||
related_model = None
|
||||
|
||||
elif related_model is None:
|
||||
|
||||
related_model = self
|
||||
|
||||
elif hasattr(related_model, 'get_related_field_name'):
|
||||
|
||||
if related_model.get_related_field_name() != '':
|
||||
|
||||
related_model = related_model.get_related_model()
|
||||
|
||||
|
||||
return related_model
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
|
||||
model = self.get_related_model()
|
||||
|
||||
if len(self._meta.parents) == 0 and model is None:
|
||||
|
||||
return {
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
if model is None:
|
||||
|
||||
model = self
|
||||
|
||||
kwargs = {
|
||||
'entity_model': str(model._meta.verbose_name).lower().replace(' ', '_'),
|
||||
}
|
||||
|
||||
if model.pk:
|
||||
|
||||
kwargs.update({
|
||||
'pk': model.id
|
||||
})
|
||||
|
||||
return kwargs
|
||||
|
||||
|
||||
|
||||
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 = None
|
||||
|
||||
if getattr(self, 'get_related_model', None):
|
||||
|
||||
model = self.get_related_model()
|
||||
|
||||
|
||||
|
||||
if model is None:
|
||||
|
||||
model = self
|
||||
|
||||
|
||||
sub_entity = ''
|
||||
if model._meta.model_name != 'entity':
|
||||
|
||||
sub_entity = '_sub'
|
||||
|
||||
|
||||
kwargs = self.get_url_kwargs()
|
||||
|
||||
view = 'list'
|
||||
if 'pk' in kwargs:
|
||||
|
||||
view = 'detail'
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, request=request, kwargs = kwargs )
|
||||
|
||||
return reverse(f"v2:" + model.get_app_namespace() + f"_api_v2_entity" + sub_entity + "-" + view, kwargs = kwargs )
|
||||
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
related_model = self.get_related_model()
|
||||
|
||||
if related_model is None:
|
||||
|
||||
related_model = self
|
||||
|
||||
if self.entity_type != str(related_model._meta.verbose_name).lower().replace(' ', '_'):
|
||||
|
||||
self.entity_type = str(related_model._meta.verbose_name).lower().replace(' ', '_')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
def save_history(self, before: dict, after: dict) -> bool:
|
||||
|
||||
from access.models.entity_history import EntityHistory
|
||||
|
||||
history = super().save_history(
|
||||
before = before,
|
||||
after = after,
|
||||
history_model = EntityHistory
|
||||
)
|
||||
|
||||
return history
|
55
app/access/models/entity_history.py
Normal file
55
app/access/models/entity_history.py
Normal file
@ -0,0 +1,55 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
from core.models.model_history import ModelHistory
|
||||
|
||||
from devops.models.feature_flag import FeatureFlag
|
||||
|
||||
|
||||
|
||||
class EntityHistory(
|
||||
ModelHistory
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_entity_history'
|
||||
|
||||
ordering = ModelHistory._meta.ordering
|
||||
|
||||
verbose_name = 'Entity History'
|
||||
|
||||
verbose_name_plural = 'Entity History'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Entity,
|
||||
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.entity import BaseSerializer
|
||||
|
||||
model = BaseSerializer(self.model, context = serializer_context)
|
||||
|
||||
return model
|
45
app/access/models/entity_notes.py
Normal file
45
app/access/models/entity_notes.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
from core.models.model_notes import ModelNotes
|
||||
|
||||
|
||||
|
||||
class EntityNotes(
|
||||
ModelNotes
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_entity_notes'
|
||||
|
||||
ordering = ModelNotes._meta.ordering
|
||||
|
||||
verbose_name = 'Entity Note'
|
||||
|
||||
verbose_name_plural = 'Entity Notes'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Entity,
|
||||
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
|
||||
}
|
1
app/access/models/organization.py
Normal file
1
app/access/models/organization.py
Normal file
@ -0,0 +1 @@
|
||||
from .tenant import Tenant as Organization
|
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.tenant import Tenant
|
||||
|
||||
|
||||
|
||||
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(
|
||||
Tenant,
|
||||
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 TenantBaseSerializer
|
||||
|
||||
model = TenantBaseSerializer(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.tenant import Tenant
|
||||
|
||||
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(
|
||||
Tenant,
|
||||
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
|
||||
}
|
119
app/access/models/person.py
Normal file
119
app/access/models/person.py
Normal file
@ -0,0 +1,119 @@
|
||||
from django.db import models
|
||||
|
||||
from core.exceptions import ValidationError
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
|
||||
|
||||
class Person(
|
||||
Entity
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'l_name',
|
||||
'm_name',
|
||||
'f_name',
|
||||
'dob',
|
||||
]
|
||||
|
||||
sub_model_type = 'person'
|
||||
|
||||
verbose_name = 'Person'
|
||||
|
||||
verbose_name_plural = 'People'
|
||||
|
||||
f_name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'The persons first name',
|
||||
max_length = 64,
|
||||
unique = False,
|
||||
verbose_name = 'First Name'
|
||||
)
|
||||
|
||||
m_name = models.CharField(
|
||||
blank = True,
|
||||
help_text = 'The persons middle name(s)',
|
||||
max_length = 100,
|
||||
null = True,
|
||||
unique = False,
|
||||
verbose_name = 'Middle Name(s)'
|
||||
)
|
||||
|
||||
l_name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'The persons Last name',
|
||||
max_length = 64,
|
||||
unique = False,
|
||||
verbose_name = 'Last Name'
|
||||
)
|
||||
|
||||
dob = models.DateField(
|
||||
blank = True,
|
||||
help_text = 'The Persons Date of Birth (DOB)',
|
||||
null = True,
|
||||
unique = False,
|
||||
verbose_name = 'DOB',
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
return self.f_name + ' ' + self.l_name + f' (DOB: {self.dob})'
|
||||
|
||||
documentation = ''
|
||||
|
||||
history_model_name = 'person'
|
||||
|
||||
page_layout: dict = []
|
||||
|
||||
table_fields: list = [
|
||||
'organization',
|
||||
'f_name',
|
||||
'l_name',
|
||||
'dob',
|
||||
'created',
|
||||
]
|
||||
|
||||
|
||||
def clean(self):
|
||||
|
||||
super().clean()
|
||||
|
||||
if self.dob is not None:
|
||||
|
||||
if self.pk:
|
||||
|
||||
duplicate_entry = Person.objects.filter(
|
||||
f_name = self.f_name,
|
||||
l_name = self.l_name,
|
||||
).exclude(
|
||||
pk = self.pk
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
duplicate_entry = Person.objects.filter(
|
||||
f_name = self.f_name,
|
||||
l_name = self.l_name,
|
||||
)
|
||||
|
||||
|
||||
for entry in duplicate_entry:
|
||||
|
||||
if(
|
||||
entry.f_name == self.f_name
|
||||
and entry.m_name == self.m_name
|
||||
and entry.l_name == self.l_name
|
||||
and entry.dob == self.dob
|
||||
):
|
||||
|
||||
raise ValidationError(
|
||||
detail = {
|
||||
'dob': f'Person {self.f_name} {self.l_name} already exists with this birthday {entry.dob}'
|
||||
},
|
||||
code = 'duplicate_person_on_dob'
|
||||
)
|
||||
|
143
app/access/models/role.py
Normal file
143
app/access/models/role.py
Normal file
@ -0,0 +1,143 @@
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.db import models
|
||||
|
||||
from access.fields import AutoCreatedField, AutoLastModifiedField
|
||||
from access.models.tenancy import TenancyObject
|
||||
|
||||
|
||||
|
||||
class Role(
|
||||
TenancyObject
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [
|
||||
'organization',
|
||||
'name',
|
||||
]
|
||||
|
||||
unique_together = [
|
||||
'organization',
|
||||
'name'
|
||||
]
|
||||
|
||||
verbose_name = 'Role'
|
||||
|
||||
verbose_name_plural = 'Roles'
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'Primary key of the entry',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of this role',
|
||||
max_length = 30,
|
||||
unique = False,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
permissions = models.ManyToManyField(
|
||||
Permission,
|
||||
blank = True,
|
||||
help_text = 'Permissions part of this role',
|
||||
related_name = 'roles',
|
||||
symmetrical = False,
|
||||
verbose_name = 'Permissions'
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
is_global = None
|
||||
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
return str( self.organization ) + ' / ' + self.name
|
||||
|
||||
|
||||
documentation = ''
|
||||
|
||||
page_layout: dict = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'name',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "single",
|
||||
"name": "Permissions",
|
||||
"fields": [
|
||||
"permissions",
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Tickets",
|
||||
"slug": "tickets",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "tickets",
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
table_fields: list = [
|
||||
'organization',
|
||||
'name',
|
||||
'created',
|
||||
'modified',
|
||||
]
|
||||
|
||||
|
||||
def save_history(self, before: dict, after: dict) -> bool:
|
||||
|
||||
from access.models.role_history import RoleHistory
|
||||
|
||||
history = super().save_history(
|
||||
before = before,
|
||||
after = after,
|
||||
history_model = RoleHistory
|
||||
)
|
||||
|
||||
return history
|
53
app/access/models/role_history.py
Normal file
53
app/access/models/role_history.py
Normal file
@ -0,0 +1,53 @@
|
||||
from django.db import models
|
||||
|
||||
from core.models.model_history import ModelHistory
|
||||
|
||||
from access.models.role import Role
|
||||
|
||||
|
||||
|
||||
class RoleHistory(
|
||||
ModelHistory
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_role_history'
|
||||
|
||||
ordering = ModelHistory._meta.ordering
|
||||
|
||||
verbose_name = 'Role History'
|
||||
|
||||
verbose_name_plural = 'Role History'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Role,
|
||||
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.role import BaseSerializer
|
||||
|
||||
model = BaseSerializer(self.model, context = serializer_context)
|
||||
|
||||
return model
|
45
app/access/models/role_notes.py
Normal file
45
app/access/models/role_notes.py
Normal file
@ -0,0 +1,45 @@
|
||||
from django.db import models
|
||||
|
||||
from access.models.role import Role
|
||||
|
||||
from core.models.model_notes import ModelNotes
|
||||
|
||||
|
||||
|
||||
class RoleNotes(
|
||||
ModelNotes
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
db_table = 'access_role_notes'
|
||||
|
||||
ordering = ModelNotes._meta.ordering
|
||||
|
||||
verbose_name = 'Role Note'
|
||||
|
||||
verbose_name_plural = 'Role Notes'
|
||||
|
||||
|
||||
model = models.ForeignKey(
|
||||
Role,
|
||||
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
|
||||
}
|
191
app/access/models/team.py
Normal file
191
app/access/models/team.py
Normal file
@ -0,0 +1,191 @@
|
||||
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.tenant import Tenant
|
||||
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):
|
||||
|
||||
if self.organization_id:
|
||||
|
||||
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(
|
||||
Tenant,
|
||||
blank = False,
|
||||
help_text = 'Tenant this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Tenant'
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
def get_url_kwargs_notes(self) -> dict:
|
||||
"""Fetch the URL kwargs for model notes
|
||||
|
||||
Returns:
|
||||
dict: notes kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': 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_team_note-detail", request=request, kwargs = kwargs )
|
||||
|
||||
return reverse("v2:_api_v2_team_note-detail", kwargs = kwargs )
|
152
app/access/models/team_user.py
Normal file
152
app/access/models/team_user.py
Normal file
@ -0,0 +1,152 @@
|
||||
import django
|
||||
|
||||
from django.conf import settings
|
||||
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.tenant import Tenant
|
||||
from access.models.team import Team
|
||||
|
||||
from core.lib.feature_not_used import FeatureNotUsed
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
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'
|
||||
]
|
||||
|
||||
history_app_label: str = None
|
||||
history_model_name: str = None
|
||||
kb_model_name: str = None
|
||||
note_basename: str = None
|
||||
|
||||
|
||||
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) -> Tenant:
|
||||
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 get_url_kwargs_notes(self):
|
||||
|
||||
return FeatureNotUsed
|
||||
|
||||
|
||||
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
|
299
app/access/models/tenancy.py
Normal file
299
app/access/models/tenancy.py
Normal file
@ -0,0 +1,299 @@
|
||||
import django
|
||||
import logging
|
||||
|
||||
from django.db import models
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.tenant import Tenant
|
||||
|
||||
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(
|
||||
Tenant,
|
||||
blank = False,
|
||||
help_text = 'Tenancy this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
related_name = '+',
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Tenant'
|
||||
)
|
||||
|
||||
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) -> Tenant:
|
||||
return self.organization
|
||||
|
||||
app_namespace: str = None
|
||||
"""Application namespace.
|
||||
|
||||
Specify the applications namespace i.e. `devops`, without including
|
||||
the API version, i.e. `v2:devops`.
|
||||
"""
|
||||
|
||||
history_app_label: str = None
|
||||
"""History Model Application Label
|
||||
|
||||
This value is derived from `<model>._meta.app_label`. This value should
|
||||
only be used when there is model inheritence.
|
||||
"""
|
||||
|
||||
history_model_name: str = None
|
||||
"""History Model Model Name
|
||||
|
||||
This value is derived from `<model>._meta.model_name`. This value should
|
||||
only be used when there is model inheritence.
|
||||
"""
|
||||
|
||||
kb_model_name: str = None
|
||||
"""Model name to use for KB article linking
|
||||
|
||||
This value is derived from `<model>._meta.model_name`. This value should
|
||||
only be used when there is model inheritence.
|
||||
"""
|
||||
|
||||
_log: logging.Logger = None
|
||||
|
||||
def get_log(self):
|
||||
|
||||
if self._log is None:
|
||||
|
||||
self._log = logging.getLogger('centurion.' + self._meta.app_label)
|
||||
|
||||
return self._log
|
||||
|
||||
page_layout: list = None
|
||||
|
||||
note_basename: str = None
|
||||
"""URL BaseName for the notes endpoint.
|
||||
|
||||
Don't specify the `app_namespace`, use property `app_namespace` above.
|
||||
"""
|
||||
|
||||
|
||||
def get_page_layout(self):
|
||||
""" FEtch the page layout"""
|
||||
|
||||
return self.page_layout
|
||||
|
||||
|
||||
def get_app_namespace(self) -> str:
|
||||
"""Fetch the Application namespace if specified.
|
||||
|
||||
Returns:
|
||||
str: Application namespace suffixed with colin `:`
|
||||
None: No application namespace found.
|
||||
"""
|
||||
|
||||
app_namespace = ''
|
||||
|
||||
if self.app_namespace:
|
||||
|
||||
app_namespace = self.app_namespace + ':'
|
||||
|
||||
return str(app_namespace)
|
||||
|
||||
|
||||
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:" + self.get_app_namespace() + f"_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:" + self.get_app_namespace() + f"_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 get_url_kwargs_notes(self) -> dict:
|
||||
"""Fetch the URL kwargs for model notes
|
||||
|
||||
Returns:
|
||||
dict: notes kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'model_id': 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': 'Tenant is required'
|
||||
},
|
||||
code = 'required'
|
||||
)
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
161
app/access/models/tenant.py
Normal file
161
app/access/models/tenant.py
Normal file
@ -0,0 +1,161 @@
|
||||
import django
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.fields import (
|
||||
AutoCreatedField,
|
||||
AutoLastModifiedField,
|
||||
AutoSlugField
|
||||
)
|
||||
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
class Tenant(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Tenant"
|
||||
verbose_name_plural = "Tenants"
|
||||
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 Tenancy',
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
manager = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank = False,
|
||||
help_text = 'Manager for this Tenancy',
|
||||
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
|
||||
|
||||
|
||||
|
||||
Organization = Tenant
|
0
app/access/serializers/__init__.py
Normal file
0
app/access/serializers/__init__.py
Normal file
90
app/access/serializers/entity.py
Normal file
90
app/access/serializers/entity.py
Normal file
@ -0,0 +1,90 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'EntityBaseBaseSerializer')
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
display_name = serializers.SerializerMethodField('get_display_name')
|
||||
|
||||
def get_display_name(self, item) -> str:
|
||||
|
||||
return str( item )
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, item) -> str:
|
||||
|
||||
return item.get_url( request = self.context['view'].request )
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Entity
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'EntityBaseModelSerializer')
|
||||
class ModelSerializer(
|
||||
common.CommonModelSerializer,
|
||||
BaseSerializer
|
||||
):
|
||||
"""Entity Base Model"""
|
||||
|
||||
|
||||
_urls = serializers.SerializerMethodField('get_url')
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Entity
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'organization',
|
||||
'entity_type',
|
||||
'display_name',
|
||||
'model_notes',
|
||||
'is_global',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'entity_type',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'EntityBaseViewSerializer')
|
||||
class ViewSerializer(ModelSerializer):
|
||||
"""Entity Base View Model"""
|
||||
|
||||
organization = TenantBaseSerializer(many=False, read_only=True)
|
70
app/access/serializers/entity_company.py
Normal file
70
app/access/serializers/entity_company.py
Normal file
@ -0,0 +1,70 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from access.models.company_base import Company
|
||||
|
||||
from access.serializers.entity import (
|
||||
BaseSerializer as BaseBaseSerializer,
|
||||
ModelSerializer as BaseModelSerializer,
|
||||
)
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
|
||||
|
||||
class BaseSerializer(
|
||||
BaseBaseSerializer,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'CompanyEntityModelSerializer')
|
||||
class ModelSerializer(
|
||||
BaseSerializer,
|
||||
BaseModelSerializer,
|
||||
):
|
||||
"""Company Model
|
||||
|
||||
This model inherits from the Entity base model.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Company
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'entity_ptr_id',
|
||||
'organization',
|
||||
'entity_type',
|
||||
'display_name',
|
||||
'name',
|
||||
'model_notes',
|
||||
'is_global',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'entity_type',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'CompanyEntityViewSerializer')
|
||||
class ViewSerializer(
|
||||
ModelSerializer,
|
||||
):
|
||||
"""Company View Model
|
||||
|
||||
This model inherits from the Entity base model.
|
||||
"""
|
||||
|
||||
organization = TenantBaseSerializer(many=False, read_only=True)
|
75
app/access/serializers/entity_contact.py
Normal file
75
app/access/serializers/entity_contact.py
Normal file
@ -0,0 +1,75 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from access.models.contact import Contact
|
||||
|
||||
from access.serializers.entity_person import (
|
||||
BaseSerializer as BaseBaseSerializer,
|
||||
ModelSerializer as BaseModelSerializer,
|
||||
)
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
|
||||
|
||||
class BaseSerializer(
|
||||
BaseBaseSerializer,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'ContactEntityModelSerializer')
|
||||
class ModelSerializer(
|
||||
BaseSerializer,
|
||||
BaseModelSerializer,
|
||||
):
|
||||
"""Contact Model
|
||||
|
||||
This model first inherits from Person then inherits from the Entity Base model.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Contact
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'person_ptr_id',
|
||||
'organization',
|
||||
'entity_type',
|
||||
'display_name',
|
||||
'f_name',
|
||||
'm_name',
|
||||
'l_name',
|
||||
'dob',
|
||||
'email',
|
||||
'directory',
|
||||
'model_notes',
|
||||
'is_global',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'entity_type',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'ContactEntityViewSerializer')
|
||||
class ViewSerializer(
|
||||
ModelSerializer,
|
||||
):
|
||||
"""Contact View Model
|
||||
|
||||
This model inherits from the Person model.
|
||||
"""
|
||||
|
||||
organization = TenantBaseSerializer(many=False, read_only=True)
|
41
app/access/serializers/entity_notes.py
Normal file
41
app/access/serializers/entity_notes.py
Normal file
@ -0,0 +1,41 @@
|
||||
from core.serializers.model_notes import (
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
from access.models.entity_notes import EntityNotes
|
||||
|
||||
|
||||
|
||||
class EntityNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EntityNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = EntityNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class EntityNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
EntityNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
73
app/access/serializers/entity_person.py
Normal file
73
app/access/serializers/entity_person.py
Normal file
@ -0,0 +1,73 @@
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from access.models.person import Person
|
||||
|
||||
from access.serializers.entity import (
|
||||
BaseSerializer as BaseBaseSerializer,
|
||||
ModelSerializer as BaseModelSerializer,
|
||||
)
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
|
||||
|
||||
class BaseSerializer(
|
||||
BaseBaseSerializer,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'PersonEntityModelSerializer')
|
||||
class ModelSerializer(
|
||||
BaseSerializer,
|
||||
BaseModelSerializer,
|
||||
):
|
||||
"""Person Model
|
||||
|
||||
This model inherits from the Entity base model.
|
||||
"""
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Person
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'entity_ptr_id',
|
||||
'organization',
|
||||
'entity_type',
|
||||
'display_name',
|
||||
'f_name',
|
||||
'm_name',
|
||||
'l_name',
|
||||
'dob',
|
||||
'model_notes',
|
||||
'is_global',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'entity_type',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'PersonEntityViewSerializer')
|
||||
class ViewSerializer(
|
||||
ModelSerializer,
|
||||
):
|
||||
"""Person View Model
|
||||
|
||||
This model inherits from the Entity base model.
|
||||
"""
|
||||
|
||||
organization = TenantBaseSerializer(many=False, read_only=True)
|
@ -2,15 +2,16 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import Organization
|
||||
from access.models.tenant import Tenant
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core import fields as centurion_field
|
||||
|
||||
Organization = Tenant
|
||||
|
||||
|
||||
class OrganizationBaseSerializer(serializers.ModelSerializer):
|
||||
class TenantBaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
display_name = serializers.SerializerMethodField('get_display_name')
|
||||
|
||||
@ -24,7 +25,7 @@ class OrganizationBaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Organization
|
||||
model = Tenant
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
@ -42,8 +43,8 @@ class OrganizationBaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
|
||||
class OrganizationModelSerializer(
|
||||
OrganizationBaseSerializer
|
||||
class TenantModelSerializer(
|
||||
TenantBaseSerializer
|
||||
):
|
||||
|
||||
_urls = serializers.SerializerMethodField('get_url')
|
||||
@ -74,7 +75,7 @@ class OrganizationModelSerializer(
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Organization
|
||||
model = Tenant
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
@ -98,7 +99,7 @@ class OrganizationModelSerializer(
|
||||
]
|
||||
|
||||
|
||||
class OrganizationViewSerializer(OrganizationModelSerializer):
|
||||
class TenantViewSerializer(TenantModelSerializer):
|
||||
pass
|
||||
|
||||
manager = UserBaseSerializer(many=False, read_only = True)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import OrganizationNotes
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
|
114
app/access/serializers/role.py
Normal file
114
app/access/serializers/role.py
Normal file
@ -0,0 +1,114 @@
|
||||
from rest_framework import serializers
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from drf_spectacular.utils import extend_schema_serializer
|
||||
|
||||
from access.functions.permissions import permission_queryset
|
||||
from access.models.role import Role
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.permission import PermissionBaseSerializer
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'RoleBaseSerializer')
|
||||
class BaseSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
display_name = serializers.SerializerMethodField('get_display_name')
|
||||
|
||||
def get_display_name(self, item) -> str:
|
||||
|
||||
return str( item )
|
||||
|
||||
url = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, item) -> str:
|
||||
|
||||
return item.get_url( request = self.context['view'].request )
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Role
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'url',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'RoleModelSerializer')
|
||||
class ModelSerializer(
|
||||
common.CommonModelSerializer,
|
||||
BaseSerializer
|
||||
):
|
||||
"""Role Base Model"""
|
||||
|
||||
|
||||
_urls = serializers.SerializerMethodField('get_url')
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
get_url = super().get_url( item = item )
|
||||
|
||||
get_url.update({
|
||||
'tickets': reverse(
|
||||
"v2:_api_v2_item_tickets-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'item_class': self.Meta.model._meta.model_name,
|
||||
'item_id': item.pk
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
permissions = serializers.PrimaryKeyRelatedField(many = True, queryset=permission_queryset(), required = False)
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Role
|
||||
|
||||
fields = [
|
||||
'id',
|
||||
'organization',
|
||||
'display_name',
|
||||
'name',
|
||||
'permissions',
|
||||
'model_notes',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
read_only_fields = [
|
||||
'id',
|
||||
'display_name',
|
||||
'created',
|
||||
'modified',
|
||||
'_urls',
|
||||
]
|
||||
|
||||
|
||||
|
||||
@extend_schema_serializer(component_name = 'RoleViewSerializer')
|
||||
class ViewSerializer(ModelSerializer):
|
||||
"""Role Base View Model"""
|
||||
|
||||
organization = TenantBaseSerializer( many=False, read_only=True )
|
||||
|
||||
permissions = PermissionBaseSerializer( many=True, read_only=True )
|
48
app/access/serializers/role_notes.py
Normal file
48
app/access/serializers/role_notes.py
Normal file
@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models.role_notes import RoleNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core.serializers.model_notes import (
|
||||
ModelNotes,
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class RoleNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RoleNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = RoleNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class RoleNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
RoleNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import TeamNotes
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
|
@ -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,13 @@ 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']
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
class Meta:
|
||||
|
@ -2,12 +2,12 @@ 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
|
||||
|
||||
from access.functions.permissions import permission_queryset
|
||||
from access.serializers.organization import OrganizationBaseSerializer
|
||||
from access.serializers.organization import TenantBaseSerializer
|
||||
|
||||
from app.serializers.permission import Permission, PermissionBaseSerializer
|
||||
|
||||
@ -61,24 +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
|
||||
}
|
||||
),
|
||||
'notes': reverse(
|
||||
"v2:_api_v2_organization_team_note-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'organization_id': item.organization.pk,
|
||||
'model_id': 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,
|
||||
@ -87,7 +72,10 @@ class TeamModelSerializer(
|
||||
'team_id': item.pk
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
team_name = centurion_field.CharField( autolink = True )
|
||||
|
||||
@ -139,6 +127,6 @@ class TeamModelSerializer(
|
||||
|
||||
class TeamViewSerializer(TeamModelSerializer):
|
||||
|
||||
organization = OrganizationBaseSerializer(many=False, read_only=True)
|
||||
organization = TenantBaseSerializer(many=False, read_only=True)
|
||||
|
||||
permissions = PermissionBaseSerializer(many = True)
|
||||
|
@ -1,7 +1,3 @@
|
||||
import pytest
|
||||
import unittest
|
||||
|
||||
from access.models import TenancyManager
|
||||
|
||||
|
||||
|
||||
@ -19,83 +15,18 @@ class TenancyObject:
|
||||
"""
|
||||
|
||||
|
||||
def test_history_save(self):
|
||||
"""Confirm the desired intent for saving model history."""
|
||||
# def test_history_save(self):
|
||||
# """Confirm the desired intent for saving model history."""
|
||||
|
||||
assert self.model.save_model_history == self.should_model_history_be_saved
|
||||
|
||||
|
||||
def test_has_attr_get_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function get_organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'get_organization')
|
||||
|
||||
|
||||
def test_has_attr_is_global(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field is_global
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'is_global')
|
||||
# assert self.model.save_model_history == self.should_model_history_be_saved
|
||||
|
||||
|
||||
|
||||
def test_has_attr_model_notes(self):
|
||||
""" TenancyObject attribute check
|
||||
# @pytest.mark.skip(reason="to be written")
|
||||
# def test_edit_no_organization_fails(self):
|
||||
# """ Devices must be assigned an organization
|
||||
|
||||
TenancyObject has field model_notes
|
||||
"""
|
||||
# Must not be able to edit an item without an organization
|
||||
# """
|
||||
# pass
|
||||
|
||||
assert hasattr(self.model, 'model_notes')
|
||||
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has field organization
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'organization')
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_create_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to create an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
@pytest.mark.skip(reason="to be written")
|
||||
def test_edit_no_organization_fails(self):
|
||||
""" Devices must be assigned an organization
|
||||
|
||||
Must not be able to edit an item without an organization
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def test_has_attr_organization(self):
|
||||
""" TenancyObject attribute check
|
||||
|
||||
TenancyObject has function objects
|
||||
"""
|
||||
|
||||
assert hasattr(self.model, 'objects')
|
||||
|
||||
|
||||
def test_attribute_is_type_objects(self):
|
||||
""" Attribute Check
|
||||
|
||||
attribute `objects` must be set to `access.models.TenancyManager()`
|
||||
"""
|
||||
|
||||
assert type(self.model.objects) is TenancyManager
|
||||
|
24
app/access/tests/functional/company/conftest.py
Normal file
24
app/access/tests/functional/company/conftest.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from access.models.company_base import Company
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = Company
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def create_serializer():
|
||||
|
||||
from access.serializers.entity_company import ModelSerializer
|
||||
|
||||
|
||||
yield ModelSerializer
|
@ -0,0 +1,72 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.company_base import Company
|
||||
from access.tests.functional.entity.test_functional_entity_metadata import (
|
||||
EntityMetadataInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CompanyMetadataTestCases(
|
||||
EntityMetadataInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'name': 'Ian1'
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'name': 'Ian2',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'name': 'Ian3',
|
||||
}
|
||||
|
||||
model = Company
|
||||
|
||||
|
||||
|
||||
|
||||
class CompanyMetadataInheritedCases(
|
||||
CompanyMetadataTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
# self.url_kwargs = {
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# }
|
||||
|
||||
# self.url_view_kwargs = {
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# }
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class CompanyMetadataTest(
|
||||
CompanyMetadataTestCases,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
pass
|
@ -0,0 +1,43 @@
|
||||
import pytest
|
||||
|
||||
from access.tests.functional.entity.test_functional_entity_permission import (
|
||||
EntityPermissionsAPIInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CompanyPermissionsAPITestCases(
|
||||
EntityPermissionsAPIInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'name': 'Ian1',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'name': 'Ian2',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'name': 'Ian3',
|
||||
}
|
||||
|
||||
|
||||
|
||||
class CompanyPermissionsAPIInheritedCases(
|
||||
CompanyPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
kwargs_create_item: dict = None
|
||||
|
||||
kwargs_create_item_diff_org: dict = None
|
||||
|
||||
|
||||
|
||||
class CompanyPermissionsAPIPyTest(
|
||||
CompanyPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.tests.functional.entity.test_functional_entity_serializer import (
|
||||
MockView,
|
||||
EntitySerializerInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class CompanySerializerTestCases(
|
||||
EntitySerializerInheritedCases
|
||||
):
|
||||
|
||||
|
||||
parameterized_test_data: dict = {
|
||||
"name": {
|
||||
'will_create': False,
|
||||
'exception_key': 'required'
|
||||
},
|
||||
}
|
||||
|
||||
valid_data: dict = {
|
||||
'name': 'Ian',
|
||||
}
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
class CompanySerializerInheritedCases(
|
||||
CompanySerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
||||
|
||||
valid_data: dict = None
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
class CompanySerializerPyTest(
|
||||
CompanySerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
@ -0,0 +1,58 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.company_base import Company
|
||||
from access.tests.functional.entity.test_functional_entity_viewset import (
|
||||
EntityViewSetInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetTestCases(
|
||||
EntityViewSetInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'name': 'Ian',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'name': 'Ian2',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'name': 'Ian3',
|
||||
}
|
||||
|
||||
model = Company
|
||||
|
||||
|
||||
|
||||
class CompanyViewSetInheritedCases(
|
||||
ViewSetTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class CompanyViewSetTest(
|
||||
ViewSetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
pass
|
24
app/access/tests/functional/contact/conftest.py
Normal file
24
app/access/tests/functional/contact/conftest.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from access.models.contact import Contact
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = Contact
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def create_serializer():
|
||||
|
||||
from access.serializers.entity_contact import ModelSerializer
|
||||
|
||||
|
||||
yield ModelSerializer
|
@ -0,0 +1,60 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.contact import Contact
|
||||
from access.tests.functional.person.test_functional_person_history import (
|
||||
PersonHistoryInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ContactTestCases(
|
||||
PersonHistoryInheritedCases,
|
||||
):
|
||||
|
||||
field_name = 'model_notes'
|
||||
|
||||
kwargs_create_obj: dict = {
|
||||
'email': 'ipfunny@unit.test',
|
||||
}
|
||||
|
||||
kwargs_delete_obj: dict = {
|
||||
'email': 'ipweird@unit.test',
|
||||
}
|
||||
|
||||
model = Contact
|
||||
|
||||
|
||||
|
||||
class ContactHistoryInheritedCases(
|
||||
ContactTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
"""Entity model to test"""
|
||||
|
||||
kwargs_create_obj: dict = None
|
||||
|
||||
kwargs_delete_obj: dict = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_obj.update(
|
||||
super().kwargs_create_obj
|
||||
)
|
||||
|
||||
self.kwargs_delete_obj.update(
|
||||
super().kwargs_delete_obj
|
||||
)
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class ContactHistoryTest(
|
||||
ContactTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,65 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.contact import Contact
|
||||
|
||||
from access.tests.functional.person.test_functional_person_metadata import (
|
||||
PersonMetadataInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ContactMetadataTestCases(
|
||||
PersonMetadataInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'email': 'ipfunny@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'email': 'ipweird@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'email': 'ipstrange@unit.test',
|
||||
}
|
||||
|
||||
model = Contact
|
||||
|
||||
|
||||
|
||||
|
||||
class ContactMetadataInheritedCases(
|
||||
ContactMetadataTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class ContactMetadataTest(
|
||||
ContactMetadataTestCases,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
pass
|
@ -0,0 +1,70 @@
|
||||
import pytest
|
||||
|
||||
from access.tests.functional.person.test_functional_person_permission import (
|
||||
PersonPermissionsAPIInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ContactPermissionsAPITestCases(
|
||||
PersonPermissionsAPIInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'email': 'ipfunny@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'email': 'ipweird@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'email': 'ipstrange@unit.test',
|
||||
}
|
||||
|
||||
|
||||
|
||||
class ContactPermissionsAPIInheritedCases(
|
||||
ContactPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
kwargs_create_item: dict = None
|
||||
|
||||
kwargs_create_item_diff_org: dict = None
|
||||
|
||||
# url_name = '_api_v2_entity_sub'
|
||||
|
||||
|
||||
# @pytest.fixture(scope='class')
|
||||
# def inherited_var_setup(self, request):
|
||||
|
||||
# request.cls.url_kwargs.update({
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# })
|
||||
|
||||
# request.cls.url_view_kwargs.update({
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# })
|
||||
|
||||
|
||||
|
||||
# @pytest.fixture(scope='class', autouse = True)
|
||||
# def class_setup(self, request, django_db_blocker,
|
||||
# model,
|
||||
# var_setup,
|
||||
# prepare,
|
||||
# inherited_var_setup,
|
||||
# diff_org_model,
|
||||
# create_model,
|
||||
# ):
|
||||
|
||||
# pass
|
||||
|
||||
|
||||
class ContactPermissionsAPIPyTest(
|
||||
ContactPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,46 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.tests.functional.person.test_functional_person_serializer import (
|
||||
MockView,
|
||||
PersonSerializerInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ContactSerializerTestCases(
|
||||
PersonSerializerInheritedCases
|
||||
):
|
||||
|
||||
|
||||
parameterized_test_data: dict = {
|
||||
"email": {
|
||||
'will_create': False,
|
||||
'exception_key': 'required'
|
||||
}
|
||||
}
|
||||
|
||||
valid_data: dict = {
|
||||
'email': 'contactentityduplicatetwo@unit.test',
|
||||
}
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
class ContactSerializerInheritedCases(
|
||||
ContactSerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
||||
|
||||
valid_data: dict = None
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
class ContactSerializerPyTest(
|
||||
ContactSerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
@ -0,0 +1,58 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.contact import Contact
|
||||
from access.tests.functional.person.test_functional_person_viewset import (
|
||||
PersonViewSetInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetTestCases(
|
||||
PersonViewSetInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'email': 'ipfunny@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'email': 'ipweird@unit.test',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'email': 'ipstrange@unit.test',
|
||||
}
|
||||
|
||||
model = Contact
|
||||
|
||||
|
||||
|
||||
class ContactViewSetInheritedCases(
|
||||
ViewSetTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class ContactViewSetTest(
|
||||
ViewSetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
pass
|
24
app/access/tests/functional/entity/conftest.py
Normal file
24
app/access/tests/functional/entity/conftest.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from access.models.entity import Entity
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = Entity
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def create_serializer():
|
||||
|
||||
from access.serializers.entity import ModelSerializer
|
||||
|
||||
|
||||
yield ModelSerializer
|
@ -0,0 +1,78 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.entity_history import Entity, EntityHistory
|
||||
|
||||
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
|
||||
|
||||
|
||||
|
||||
class HistoryTestCases(
|
||||
HistoryEntriesCommon,
|
||||
):
|
||||
|
||||
field_name = 'model_notes'
|
||||
|
||||
history_model = EntityHistory
|
||||
|
||||
kwargs_create_obj: dict = {}
|
||||
|
||||
kwargs_delete_obj: dict = {}
|
||||
|
||||
model = Entity
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.obj = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
model_notes = self.field_value_original,
|
||||
**self.kwargs_create_obj,
|
||||
)
|
||||
|
||||
self.obj_delete = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
model_notes = 'another note',
|
||||
**self.kwargs_delete_obj,
|
||||
)
|
||||
|
||||
self.call_the_banners()
|
||||
|
||||
|
||||
|
||||
class EntityHistoryInheritedCases(
|
||||
HistoryTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
"""Entity model to test"""
|
||||
|
||||
kwargs_create_obj: dict = None
|
||||
|
||||
kwargs_delete_obj: dict = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_obj.update(
|
||||
super().kwargs_create_obj
|
||||
)
|
||||
|
||||
self.kwargs_delete_obj.update(
|
||||
super().kwargs_delete_obj
|
||||
)
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class EntityHistoryTest(
|
||||
HistoryTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,260 @@
|
||||
import django
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.entity import Entity
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from accounting.models.asset_base import AssetBase
|
||||
|
||||
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class EntityMetadataTestCases(
|
||||
MetadataAttributesFunctional,
|
||||
):
|
||||
|
||||
add_data: dict = {}
|
||||
|
||||
app_namespace = 'v2'
|
||||
|
||||
base_model = Entity
|
||||
"""Base model for this sub model
|
||||
don't change or override this value
|
||||
"""
|
||||
|
||||
change_data = None
|
||||
|
||||
delete_data = {}
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
model = Entity
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
url_name = None
|
||||
|
||||
|
||||
@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
|
||||
|
||||
self.different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = organization,
|
||||
**self.kwargs_create_item
|
||||
)
|
||||
|
||||
self.other_org_item = self.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
**self.kwargs_create_item_diff_org
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs.update({ 'pk': self.item.id })
|
||||
|
||||
if self.add_data is not None:
|
||||
|
||||
self.add_data.update({
|
||||
'organization': self.organization.id,
|
||||
})
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_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 = self.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
|
||||
)
|
||||
|
||||
|
||||
def test_sanity_is_entity_sub_model(self):
|
||||
"""Sanity Test
|
||||
|
||||
This test ensures that the model being tested `self.model` is a
|
||||
sub-model of `self.base_model`.
|
||||
This test is required as the same viewset is used for all sub-models
|
||||
of `AssetBase`
|
||||
"""
|
||||
|
||||
assert issubclass(self.model, self.base_model)
|
||||
|
||||
|
||||
|
||||
class EntityMetadataInheritedCases(
|
||||
EntityMetadataTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
url_name = '_api_v2_entity_sub'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
self.url_kwargs = {
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class EntityMetadataTest(
|
||||
EntityMetadataTestCases,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
url_name = '_api_v2_entity'
|
@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
|
||||
from api.tests.functional.test_functional_api_permissions import (
|
||||
APIPermissionsInheritedCases,
|
||||
)
|
||||
|
||||
|
||||
|
||||
class EntityPermissionsAPITestCases(
|
||||
APIPermissionsInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {}
|
||||
|
||||
app_namespace = 'v2'
|
||||
|
||||
change_data = {}
|
||||
|
||||
delete_data = {}
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_name = '_api_v2_entity'
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
|
||||
class EntityPermissionsAPIInheritedCases(
|
||||
EntityPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
kwargs_create_item: dict = None
|
||||
|
||||
kwargs_create_item_diff_org: dict = None
|
||||
|
||||
url_name = '_api_v2_entity_sub'
|
||||
|
||||
|
||||
@pytest.fixture(scope='class')
|
||||
def inherited_var_setup(self, request):
|
||||
|
||||
request.cls.url_kwargs.update({
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
})
|
||||
|
||||
request.cls.url_view_kwargs.update({
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
})
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope='class', autouse = True)
|
||||
def class_setup(self, request, django_db_blocker,
|
||||
model,
|
||||
var_setup,
|
||||
prepare,
|
||||
inherited_var_setup,
|
||||
diff_org_model,
|
||||
create_model,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class EntityPermissionsAPIPyTest(
|
||||
EntityPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,213 @@
|
||||
import django
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class MockView:
|
||||
|
||||
_has_import: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has import permission.
|
||||
"""
|
||||
|
||||
_has_purge: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has purge permission.
|
||||
"""
|
||||
|
||||
_has_triage: bool = False
|
||||
"""User Permission
|
||||
|
||||
get_permission_required() sets this to `True` when user has triage permission.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class EntitySerializerTestCases:
|
||||
|
||||
|
||||
parameterized_test_data: dict = {
|
||||
"model_notes": {
|
||||
'will_create': True,
|
||||
}
|
||||
}
|
||||
|
||||
valid_data: dict = {
|
||||
'model_notes': 'model notes field'
|
||||
}
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def setup_data(self,
|
||||
request,
|
||||
model,
|
||||
django_db_blocker,
|
||||
organization_one,
|
||||
):
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.organization = organization_one
|
||||
|
||||
valid_data = {}
|
||||
|
||||
for base in reversed(request.cls.__mro__):
|
||||
|
||||
if hasattr(base, 'valid_data'):
|
||||
|
||||
if base.valid_data is None:
|
||||
|
||||
continue
|
||||
|
||||
valid_data.update(**base.valid_data)
|
||||
|
||||
|
||||
if len(valid_data) > 0:
|
||||
|
||||
request.cls.valid_data = valid_data
|
||||
|
||||
|
||||
if 'organization' not in request.cls.valid_data:
|
||||
|
||||
request.cls.valid_data.update({
|
||||
'organization': request.cls.organization.pk
|
||||
})
|
||||
|
||||
|
||||
request.cls.view_user = User.objects.create_user(username="cafs_test_user_view", password="password")
|
||||
|
||||
|
||||
yield
|
||||
|
||||
with django_db_blocker.unblock():
|
||||
|
||||
request.cls.view_user.delete()
|
||||
|
||||
del request.cls.valid_data
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class', autouse = True)
|
||||
def class_setup(self,
|
||||
setup_data,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def test_serializer_valid_data(self, create_serializer):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating an object with valid data, no validation
|
||||
error occurs.
|
||||
"""
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'view': view_set,
|
||||
},
|
||||
data = self.valid_data
|
||||
)
|
||||
|
||||
assert serializer.is_valid(raise_exception = True)
|
||||
|
||||
|
||||
|
||||
def test_serializer_valid_data_missing_field_is_valid(self, parameterized, param_key_test_data,
|
||||
create_serializer,
|
||||
param_value,
|
||||
param_will_create,
|
||||
):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating an object with a user with import permission
|
||||
and with valid data, no validation error occurs.
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
del valid_data[param_value]
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
view_set._has_import = True
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
is_valid = serializer.is_valid(raise_exception = False)
|
||||
|
||||
assert (
|
||||
(
|
||||
not param_will_create
|
||||
and param_will_create == is_valid
|
||||
)
|
||||
or param_will_create == is_valid
|
||||
)
|
||||
|
||||
|
||||
|
||||
class EntitySerializerInheritedCases(
|
||||
EntitySerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
||||
|
||||
model = None
|
||||
"""Model to test"""
|
||||
|
||||
valid_data: dict = None
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
def test_serializer_valid_data_missing_field_raises_exception(self, parameterized, param_key_test_data,
|
||||
create_serializer,
|
||||
param_value,
|
||||
param_exception_key,
|
||||
):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating an object with a user with import permission
|
||||
and with valid data, no validation error occurs.
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
del valid_data[param_value]
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
is_valid = serializer.is_valid(raise_exception = True)
|
||||
|
||||
assert err.value.get_codes()[param_value][0] == param_exception_key
|
||||
|
||||
|
||||
|
||||
class EntitySerializerPyTest(
|
||||
EntitySerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
@ -0,0 +1,266 @@
|
||||
import django
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.entity import Entity
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
|
||||
add_data: dict = {
|
||||
'model_notes': 'added model note'
|
||||
}
|
||||
|
||||
app_namespace = 'v2'
|
||||
|
||||
base_model = Entity
|
||||
"""Base model for this sub model
|
||||
don't change or override this value
|
||||
"""
|
||||
|
||||
change_data = None
|
||||
|
||||
delete_data = {}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'model_notes': 'added model note'
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'model_notes': 'added model note'
|
||||
}
|
||||
|
||||
model = None
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
url_name = None
|
||||
|
||||
|
||||
@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
|
||||
|
||||
self.different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = organization,
|
||||
**self.kwargs_create_item
|
||||
)
|
||||
|
||||
self.other_org_item = self.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
**self.kwargs_create_item_diff_org
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs.update({ 'pk': self.item.id })
|
||||
|
||||
if self.add_data is not None:
|
||||
|
||||
self.add_data.update({
|
||||
'organization': self.organization.id,
|
||||
})
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_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 = self.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
|
||||
)
|
||||
|
||||
|
||||
def test_sanity_is_asset_sub_model(self):
|
||||
"""Sanity Test
|
||||
|
||||
This test ensures that the model being tested `self.model` is a
|
||||
sub-model of `self.base_model`.
|
||||
This test is required as the same viewset is used for all sub-models
|
||||
of `Entity`
|
||||
"""
|
||||
|
||||
assert issubclass(self.model, self.base_model)
|
||||
|
||||
|
||||
|
||||
class ViewSetTestCases(
|
||||
ViewSetBase,
|
||||
SerializersTestCases,
|
||||
):
|
||||
|
||||
model = Entity
|
||||
|
||||
|
||||
|
||||
class EntityViewSetInheritedCases(
|
||||
ViewSetTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
url_name = '_api_v2_entity_sub'
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
self.url_kwargs = {
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'entity_model': self.model._meta.sub_model_type
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class EntityViewSetTest(
|
||||
ViewSetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
url_name = '_api_v2_entity'
|
@ -0,0 +1,81 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.entity_notes import Entity, EntityNotes
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
|
||||
|
||||
class NotesAPITestCases(
|
||||
ModelNotesNotesAPIFields,
|
||||
):
|
||||
|
||||
entity_model = None
|
||||
|
||||
model = EntityNotes
|
||||
|
||||
kwargs_model_create: dict = None
|
||||
|
||||
# url_view_kwargs: dict = None
|
||||
|
||||
view_name: str = '_api_v2_entity_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 = self.entity_model.objects.create(
|
||||
organization = self.organization,
|
||||
model_notes = 'text',
|
||||
**self.kwargs_model_create
|
||||
),
|
||||
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()
|
||||
|
||||
|
||||
|
||||
class EntityNotesAPIInheritedCases(
|
||||
NotesAPITestCases,
|
||||
):
|
||||
|
||||
entity_model = None
|
||||
|
||||
kwargs_model_create = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesAPITest(
|
||||
NotesAPITestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
entity_model = Entity
|
||||
|
||||
kwargs_model_create = {}
|
@ -0,0 +1,162 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.viewsets.entity_notes import ViewSet
|
||||
|
||||
from core.tests.abstract.test_functional_notes_viewset import (
|
||||
ModelNotesViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
ModelNotesPermissionsAPI,
|
||||
ModelNotesSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetBase(
|
||||
ModelNotesViewSetBase
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
kwargs_create_model_item: dict = {}
|
||||
|
||||
kwargs_create_model_item_other_org: dict = {}
|
||||
|
||||
url_name = '_api_v2_entity_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,
|
||||
model_notes = 'text',
|
||||
**self.kwargs_create_model_item
|
||||
),
|
||||
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.organization,
|
||||
model_notes = 'text',
|
||||
**self.kwargs_create_model_item_other_org
|
||||
),
|
||||
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 NotesPermissionsAPITestCases(
|
||||
ViewSetBase,
|
||||
ModelNotesPermissionsAPI,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
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 EntityNotesPermissionsAPIInheritedCases(
|
||||
NotesPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesPermissionsAPITest(
|
||||
NotesPermissionsAPITestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
|
||||
|
||||
class NotesSerializerTestCases(
|
||||
ViewSetBase,
|
||||
ModelNotesSerializer,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesSerializerInheritedCases(
|
||||
NotesSerializerTestCases,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesSerializerTest(
|
||||
NotesSerializerTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
|
||||
|
||||
class NotesMetadataTestCases(
|
||||
ViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesMetadataInheritedCases(
|
||||
NotesMetadataTestCases,
|
||||
):
|
||||
|
||||
viewset = None
|
||||
|
||||
|
||||
|
||||
class EntityNotesMetadataTest(
|
||||
NotesMetadataTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
@ -0,0 +1,32 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.organization_history import Tenant as 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()
|
@ -1,22 +1,24 @@
|
||||
import django
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.serializers.organization import (
|
||||
Organization,
|
||||
OrganizationModelSerializer
|
||||
Tenant,
|
||||
TenantModelSerializer
|
||||
)
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class OrganizationValidationAPI(
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = Organization
|
||||
model = Tenant
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
@ -45,7 +47,7 @@ class OrganizationValidationAPI(
|
||||
Ensure that if creating and no name is provided a validation error occurs
|
||||
"""
|
||||
|
||||
serializer = OrganizationModelSerializer(
|
||||
serializer = TenantModelSerializer(
|
||||
data = self.valid_data
|
||||
)
|
||||
|
||||
@ -65,7 +67,7 @@ class OrganizationValidationAPI(
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = OrganizationModelSerializer(
|
||||
serializer = TenantModelSerializer(
|
||||
data = data
|
||||
)
|
||||
|
||||
@ -85,7 +87,7 @@ class OrganizationValidationAPI(
|
||||
|
||||
del data['manager']
|
||||
|
||||
serializer = OrganizationModelSerializer(
|
||||
serializer = TenantModelSerializer(
|
||||
data = data
|
||||
)
|
||||
|
||||
|
@ -1,20 +1,25 @@
|
||||
import django
|
||||
import pytest
|
||||
import unittest
|
||||
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
|
||||
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.tenant import Tenant as 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
|
||||
from api.tests.abstract.test_metadata_functional import MetadataAttributesFunctional, MetaDataNavigationEntriesFunctional
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
@ -313,4 +318,4 @@ class OrganizationMetadata(
|
||||
|
||||
menu_id = 'access'
|
||||
|
||||
menu_entry_id = 'organization'
|
||||
menu_entry_id = 'tenant'
|
@ -3,8 +3,8 @@ from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models import Organization, OrganizationNotes
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
|
||||
class OrganizationNotesAPI(
|
||||
|
24
app/access/tests/functional/person/conftest.py
Normal file
24
app/access/tests/functional/person/conftest.py
Normal file
@ -0,0 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from access.models.person import Person
|
||||
|
||||
|
||||
|
||||
@pytest.fixture( scope = 'class')
|
||||
def model(request):
|
||||
|
||||
request.cls.model = Person
|
||||
|
||||
yield request.cls.model
|
||||
|
||||
del request.cls.model
|
||||
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def create_serializer():
|
||||
|
||||
from access.serializers.entity_person import ModelSerializer
|
||||
|
||||
|
||||
yield ModelSerializer
|
@ -0,0 +1,65 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.person import Person
|
||||
from access.tests.functional.entity.test_functional_entity_history import (
|
||||
EntityHistoryInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class PersonTestCases(
|
||||
EntityHistoryInheritedCases,
|
||||
):
|
||||
|
||||
field_name = 'model_notes'
|
||||
|
||||
kwargs_create_obj: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Funny',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_delete_obj: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Weird',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
model = Person
|
||||
|
||||
|
||||
class PersonHistoryInheritedCases(
|
||||
PersonTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
"""Entity model to test"""
|
||||
|
||||
kwargs_create_obj: dict = None
|
||||
|
||||
kwargs_delete_obj: dict = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_obj.update(
|
||||
super().kwargs_create_obj
|
||||
)
|
||||
|
||||
self.kwargs_delete_obj.update(
|
||||
super().kwargs_delete_obj
|
||||
)
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class PersonHistoryTest(
|
||||
PersonTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,83 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.person import Person
|
||||
from access.tests.functional.entity.test_functional_entity_metadata import (
|
||||
EntityMetadataInheritedCases
|
||||
)
|
||||
|
||||
from accounting.models.asset_base import AssetBase
|
||||
|
||||
|
||||
|
||||
class PersonMetadataTestCases(
|
||||
EntityMetadataInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Strange',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Weird',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Funny',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
model = Person
|
||||
|
||||
|
||||
|
||||
|
||||
class PersonMetadataInheritedCases(
|
||||
PersonMetadataTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
# self.url_kwargs = {
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# }
|
||||
|
||||
# self.url_view_kwargs = {
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# }
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class PersonMetadataTest(
|
||||
PersonMetadataTestCases,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
pass
|
@ -0,0 +1,91 @@
|
||||
import pytest
|
||||
|
||||
from access.tests.functional.entity.test_functional_entity_permission import (
|
||||
EntityPermissionsAPIInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class PersonPermissionsAPITestCases(
|
||||
EntityPermissionsAPIInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Strange',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
# app_namespace = 'v2'
|
||||
|
||||
# change_data = {}
|
||||
|
||||
# delete_data = {}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Weird',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Funny',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
# url_kwargs: dict = {}
|
||||
|
||||
# url_name = '_api_v2_entity'
|
||||
|
||||
# url_view_kwargs: dict = {}
|
||||
|
||||
|
||||
|
||||
class PersonPermissionsAPIInheritedCases(
|
||||
PersonPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
kwargs_create_item: dict = None
|
||||
|
||||
kwargs_create_item_diff_org: dict = None
|
||||
|
||||
# url_name = '_api_v2_entity_sub'
|
||||
|
||||
|
||||
# @pytest.fixture(scope='class')
|
||||
# def inherited_var_setup(self, request):
|
||||
|
||||
# request.cls.url_kwargs.update({
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# })
|
||||
|
||||
# request.cls.url_view_kwargs.update({
|
||||
# 'entity_model': self.model._meta.sub_model_type
|
||||
# })
|
||||
|
||||
|
||||
|
||||
# @pytest.fixture(scope='class', autouse = True)
|
||||
# def class_setup(self, request, django_db_blocker,
|
||||
# model,
|
||||
# var_setup,
|
||||
# prepare,
|
||||
# inherited_var_setup,
|
||||
# diff_org_model,
|
||||
# create_model,
|
||||
# ):
|
||||
|
||||
# pass
|
||||
|
||||
|
||||
class PersonPermissionsAPIPyTest(
|
||||
PersonPermissionsAPITestCases,
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,111 @@
|
||||
import pytest
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
from access.tests.functional.entity.test_functional_entity_serializer import (
|
||||
MockView,
|
||||
EntitySerializerInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class PersonSerializerTestCases(
|
||||
EntitySerializerInheritedCases
|
||||
):
|
||||
|
||||
|
||||
parameterized_test_data: dict = {
|
||||
"model_notes": {
|
||||
'will_create': True,
|
||||
},
|
||||
"f_name": {
|
||||
'will_create': False,
|
||||
'exception_key': 'required'
|
||||
},
|
||||
"m_name": {
|
||||
'will_create': True,
|
||||
},
|
||||
"l_name": {
|
||||
'will_create': False,
|
||||
'exception_key': 'required'
|
||||
},
|
||||
"dob": {
|
||||
'will_create': True,
|
||||
}
|
||||
}
|
||||
|
||||
valid_data: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Funny',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
def test_serializer_validation_duplicate_f_name_l_name_dob(self, model, create_serializer):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating with valid data and fields f_name, l_name and
|
||||
dob already exists in the db a validation error occurs.
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
valid_data['f_name'] = 'duplicate'
|
||||
|
||||
valid_data['organization'] = self.organization
|
||||
|
||||
obj = model.objects.create(
|
||||
**valid_data
|
||||
)
|
||||
|
||||
valid_data['organization'] = self.organization.id
|
||||
|
||||
if 'email' in valid_data: # Contact Entity
|
||||
|
||||
valid_data['email'] = 'abc@xyz.qwe'
|
||||
|
||||
if 'name' in valid_data: # Company Entity
|
||||
|
||||
valid_data['name'] = 'diff'
|
||||
|
||||
if 'employee_number' in valid_data: # Employee Entity
|
||||
|
||||
valid_data['employee_number'] = 13579
|
||||
|
||||
view_set = MockView()
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = create_serializer(
|
||||
context = {
|
||||
'view': view_set,
|
||||
},
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
serializer.save()
|
||||
|
||||
assert err.value.get_codes()['dob'] == 'duplicate_person_on_dob'
|
||||
|
||||
|
||||
|
||||
class PersonSerializerInheritedCases(
|
||||
PersonSerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
||||
|
||||
valid_data: dict = None
|
||||
"""Valid data used by serializer to create object"""
|
||||
|
||||
|
||||
|
||||
class PersonSerializerPyTest(
|
||||
PersonSerializerTestCases,
|
||||
):
|
||||
|
||||
parameterized_test_data: dict = None
|
@ -0,0 +1,67 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.person import Person
|
||||
from access.tests.functional.entity.test_functional_entity_viewset import (
|
||||
EntityViewSetInheritedCases
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetTestCases(
|
||||
EntityViewSetInheritedCases,
|
||||
):
|
||||
|
||||
add_data: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Strange',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_create_item: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Weird',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {
|
||||
'f_name': 'Ian',
|
||||
'm_name': 'Peter',
|
||||
'l_name': 'Funny',
|
||||
'dob': '2025-04-08',
|
||||
}
|
||||
|
||||
model = Person
|
||||
|
||||
|
||||
|
||||
class PersonViewSetInheritedCases(
|
||||
ViewSetTestCases,
|
||||
):
|
||||
|
||||
model = None
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
self.kwargs_create_item = {
|
||||
**super().kwargs_create_item,
|
||||
**self.kwargs_create_item
|
||||
}
|
||||
|
||||
self.kwargs_create_item_diff_org = {
|
||||
**super().kwargs_create_item_diff_org,
|
||||
**self.kwargs_create_item_diff_org
|
||||
}
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
|
||||
class PersonViewSetTest(
|
||||
ViewSetTestCases,
|
||||
TestCase,
|
||||
):
|
||||
pass
|
@ -0,0 +1,55 @@
|
||||
from django.test import TestCase
|
||||
|
||||
from access.models.role_history import Role, RoleHistory
|
||||
|
||||
from core.tests.abstract.test_functional_history import HistoryEntriesCommon
|
||||
|
||||
|
||||
|
||||
class HistoryTestCases(
|
||||
HistoryEntriesCommon,
|
||||
):
|
||||
|
||||
history_model = RoleHistory
|
||||
|
||||
kwargs_create_obj: dict = {}
|
||||
|
||||
kwargs_delete_obj: dict = {}
|
||||
|
||||
model = Role
|
||||
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.obj = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
model_notes = self.field_value_original,
|
||||
**self.kwargs_create_obj,
|
||||
)
|
||||
|
||||
self.obj_delete = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
model_notes = 'another note',
|
||||
**self.kwargs_delete_obj,
|
||||
)
|
||||
|
||||
self.call_the_banners()
|
||||
|
||||
|
||||
|
||||
class RoleHistoryTest(
|
||||
HistoryTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
kwargs_create_obj: dict = {
|
||||
'name': 'original_name'
|
||||
}
|
||||
|
||||
kwargs_delete_obj: dict = {
|
||||
'name': 'delete obj'
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
import pytest
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from rest_framework.exceptions import ValidationError
|
||||
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.serializers.role import Role, ModelSerializer
|
||||
|
||||
|
||||
|
||||
class ValidationSerializer(
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = Role
|
||||
|
||||
serializer = ModelSerializer
|
||||
|
||||
|
||||
@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.diff_organization = Organization.objects.create(name='test_org_diff_org')
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'one',
|
||||
)
|
||||
|
||||
self.valid_data = {
|
||||
'organization': self.organization.id,
|
||||
'name': 'two',
|
||||
'model_notes': 'dfsdfsd',
|
||||
}
|
||||
|
||||
|
||||
|
||||
def test_serializer_validation_valid_data(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that if creating and no name is provided a validation error occurs
|
||||
"""
|
||||
|
||||
serializer = self.serializer(
|
||||
data = self.valid_data
|
||||
)
|
||||
|
||||
|
||||
assert serializer.is_valid( raise_exception = True )
|
||||
|
||||
|
||||
def test_serializer_validation_no_name_exception(self):
|
||||
"""Serializer Validation Check
|
||||
|
||||
Ensure that when creating and field name is not provided a
|
||||
validation error occurs
|
||||
"""
|
||||
|
||||
valid_data = self.valid_data.copy()
|
||||
|
||||
del valid_data['name']
|
||||
|
||||
with pytest.raises(ValidationError) as err:
|
||||
|
||||
serializer = self.serializer(
|
||||
data = valid_data
|
||||
)
|
||||
|
||||
serializer.is_valid(raise_exception = True)
|
||||
|
||||
assert err.value.get_codes()['name'][0] == 'required'
|
286
app/access/tests/functional/role/test_functional_role_viewset.py
Normal file
286
app/access/tests/functional/role/test_functional_role_viewset.py
Normal file
@ -0,0 +1,286 @@
|
||||
import django
|
||||
from django.contrib.auth.models import Permission
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import Client, TestCase
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from access.models.role import Role
|
||||
from access.models.tenant import Tenant as 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
|
||||
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
|
||||
add_data: dict = None
|
||||
|
||||
app_namespace = 'v2'
|
||||
|
||||
change_data = { 'name': 'changed name' }
|
||||
|
||||
delete_data = {}
|
||||
|
||||
kwargs_create_item: dict = {}
|
||||
|
||||
kwargs_create_item_diff_org: dict = {}
|
||||
|
||||
kwargs_create_item_global_org_org: dict = {}
|
||||
|
||||
model = None
|
||||
|
||||
url_kwargs: dict = None
|
||||
|
||||
url_view_kwargs: dict = None
|
||||
|
||||
url_name = None
|
||||
|
||||
|
||||
@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
|
||||
|
||||
self.different_organization = Organization.objects.create(name='test_different_organization')
|
||||
|
||||
self.global_organization = Organization.objects.create(name='test_global_organization')
|
||||
|
||||
app_settings = AppSettings.objects.get(
|
||||
owner_organization = None
|
||||
)
|
||||
|
||||
app_settings.global_organization = self.global_organization
|
||||
|
||||
app_settings.save()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = organization,
|
||||
model_notes = 'some notes',
|
||||
**self.kwargs_create_item
|
||||
)
|
||||
|
||||
self.other_org_item = self.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
model_notes = 'some more notes',
|
||||
**self.kwargs_create_item_diff_org
|
||||
)
|
||||
|
||||
self.global_org_item = self.model.objects.create(
|
||||
organization = self.global_organization,
|
||||
model_notes = 'some more notes',
|
||||
**self.kwargs_create_item_global_org_org
|
||||
)
|
||||
|
||||
|
||||
# self.url_kwargs = {'organization_id': self.organization.id}
|
||||
|
||||
self.url_view_kwargs.update({ 'pk': self.item.id })
|
||||
|
||||
if self.add_data is not None:
|
||||
|
||||
self.add_data.update({'organization': self.organization.id})
|
||||
|
||||
|
||||
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.view_user = User.objects.create_user(username="test_user_view", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = view_team,
|
||||
user = self.view_user
|
||||
)
|
||||
|
||||
self.add_user = User.objects.create_user(username="test_user_add", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = add_team,
|
||||
user = self.add_user
|
||||
)
|
||||
|
||||
self.change_user = User.objects.create_user(username="test_user_change", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = change_team,
|
||||
user = self.change_user
|
||||
)
|
||||
|
||||
self.delete_user = User.objects.create_user(username="test_user_delete", password="password")
|
||||
TeamUsers.objects.create(
|
||||
team = delete_team,
|
||||
user = self.delete_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 = self.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 RolePermissionsAPITest(
|
||||
ViewSetBase,
|
||||
APIPermissions,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
add_data: dict = { 'name': 'added model note' }
|
||||
|
||||
kwargs_create_item: dict = { 'name': 'create item' }
|
||||
|
||||
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
|
||||
|
||||
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
|
||||
|
||||
model = Role
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
url_name = '_api_v2_role'
|
||||
|
||||
|
||||
|
||||
class RoleViewSetTest(
|
||||
ViewSetBase,
|
||||
SerializersTestCases,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
kwargs_create_item: dict = { 'name': 'create item' }
|
||||
|
||||
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
|
||||
|
||||
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
|
||||
|
||||
model = Role
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
url_name = '_api_v2_role'
|
||||
|
||||
|
||||
|
||||
class RoleMetadataTest(
|
||||
ViewSetBase,
|
||||
MetadataAttributesFunctional,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
kwargs_create_item: dict = { 'name': 'create item' }
|
||||
|
||||
kwargs_create_item_diff_org: dict = { 'name': 'diff org create' }
|
||||
|
||||
kwargs_create_item_global_org_org: dict = { 'name': 'global org create' }
|
||||
|
||||
model = Role
|
||||
|
||||
url_kwargs: dict = {}
|
||||
|
||||
url_view_kwargs: dict = {}
|
||||
|
||||
url_name = '_api_v2_role'
|
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()
|
@ -1,21 +1,27 @@
|
||||
import django
|
||||
import pytest
|
||||
import unittest
|
||||
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
|
||||
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.tenant import Tenant as 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
|
||||
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
|
@ -1,13 +1,15 @@
|
||||
import django
|
||||
import pytest
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.models import Permission
|
||||
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.tenant import Tenant as Organization
|
||||
|
||||
from access.serializers.teams import (
|
||||
Team,
|
||||
@ -16,6 +18,8 @@ from access.serializers.teams import (
|
||||
|
||||
from settings.models.app_settings import AppSettings
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class MockView:
|
||||
|
@ -18,7 +18,7 @@ class ViewSetBase(
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
url_name = '_api_v2_organization_team_note'
|
||||
url_name = '_api_v2_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
@ -3,7 +3,8 @@ from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models import Team, TeamNotes
|
||||
from access.models.team import Team
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
|
||||
|
||||
@ -14,7 +15,7 @@ class TeamNotesAPI(
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
view_name: str = '_api_v2_organization_team_note'
|
||||
view_name: str = '_api_v2_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
@ -1,21 +1,26 @@
|
||||
import django
|
||||
import pytest
|
||||
import unittest
|
||||
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
|
||||
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.tenant import Tenant as 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
|
||||
from api.tests.abstract.api_serializer_viewset import SerializersTestCases
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class ViewSetBase:
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user