Compare commits
1391 Commits
1.8.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 | |||
33c589a1a0 | |||
0a21c37645 | |||
e4dac45d2e | |||
c9eefc2eb3 | |||
68d3c1ff26 | |||
e5680cdbce | |||
067dd98eee | |||
b153f648f9 | |||
062e846875 | |||
0e1f778b80 | |||
aca32b14be | |||
16076f0ca3 | |||
6d69b72368 | |||
d4c6c08710 | |||
05d4c4a94d | |||
4d5f029913 | |||
51e7f875a2 | |||
b17bc5af75 | |||
80db9022b7 | |||
288abb5373 | |||
f06c9f6091 | |||
d55010d63f | |||
eeb7075f83 | |||
1c34fbad08 | |||
4549379e62 | |||
64fd692d9c | |||
795f029c75 | |||
a091be7b5e | |||
a1c9533555 | |||
20f52d856b | |||
6e296a3713 | |||
58b73b9b29 | |||
cd65d8e93d | |||
f26b0706ae | |||
1a0839a2c8 | |||
cc0f92e132 | |||
b14262fcad | |||
1474f47087 | |||
e56007d4b1 | |||
0602070adb | |||
760463705b | |||
8eeb5780c6 | |||
7b6d6935d4 | |||
1cec44f7bd | |||
51ec6cb0e5 | |||
9b62ba6367 | |||
96420f3d23 | |||
eb38e70c17 | |||
dbafbe1a5c | |||
90bee291bd | |||
5eb120886d | |||
6e4e63a52e | |||
8d39fdb9e4 | |||
7aa7627faf | |||
29fc7e7e07 | |||
780ab35b26 | |||
b072ce9fbc | |||
fa757f2edf | |||
fccaacbf7c | |||
0f1ff924ed | |||
75598dff94 | |||
475205f95b | |||
bf74a4c4db | |||
c15966c694 | |||
56ced08c4d | |||
c8d6135b83 | |||
98159c1840 | |||
8140c1f945 | |||
eff1255419 | |||
8f2bf3d71e | |||
1a7bade7f3 | |||
a7912531e3 | |||
32c6991730 | |||
98c02c4101 | |||
d8b12aeb7e | |||
5b02f2c751 | |||
0d4ad05ac9 | |||
def43c1134 | |||
3c08580c10 | |||
bc329d2453 | |||
72dfac9d22 | |||
a74550e1f3 | |||
382b1d2a30 | |||
9c2ef50c3b | |||
3536a59e5a | |||
0213d45f8a | |||
41cf9aae41 | |||
0b25a85e26 | |||
4caf26686c | |||
58c19388c7 | |||
6a211677a8 | |||
45d05a183d | |||
f144129b4d | |||
e038d08544 | |||
05c9e3f066 | |||
a1851918e3 | |||
136f907cd5 | |||
969aaea884 | |||
5a07892b25 | |||
5d17e1a49c | |||
185fb6c332 | |||
e690012da8 | |||
76b6a57583 | |||
7bea5a6156 | |||
45b2f083de | |||
643894cdfc | |||
443cbce73b | |||
9a5f4e90ff | |||
9c7e572e00 | |||
876a1622dd | |||
161751f97f | |||
ac0e756c77 | |||
115159b8a6 | |||
10e2550621 | |||
edb6aa6eb7 | |||
2534557105 | |||
2e6e73353e | |||
37d6ba202d | |||
f3a4123fb6 | |||
b20abd82e1 | |||
d11e7fa71d | |||
96a8a6a158 | |||
c0695c8c84 | |||
64c2f1b0d9 | |||
37a8368a4e | |||
ba9c811e16 | |||
ae47b06a72 | |||
bc8815d607 | |||
8a06a0dd3a | |||
46381fc138 | |||
be2e0ae837 | |||
cc34e43262 | |||
a3d0b10a75 | |||
eafc364ba5 | |||
0304010d0e | |||
5d3eadfd56 | |||
fe7e9169be | |||
9d109d2f9a | |||
f9b0b694f9 | |||
d19e742cc5 | |||
a077506eb0 | |||
585f1c01e8 | |||
f84759720c | |||
91c5216120 | |||
98868aab32 | |||
0a8c3cfffb | |||
bff5ea3fe8 | |||
e7729afc98 | |||
7358b0a90c | |||
e67ede0e74 | |||
1e914d1345 | |||
ba7c0cd117 | |||
029e5a6d08 | |||
f71fe3d6d7 | |||
c97cdf276f | |||
c2f7e25579 | |||
9e7f731a0e | |||
1f366ab8fd | |||
e59e808b7f | |||
39f48aadb1 | |||
e121741a6c | |||
0145ee9349 | |||
7a0be93752 | |||
89eecad448 | |||
a5cbf108cb | |||
67e892dbaa | |||
8477700125 | |||
59603e0455 | |||
0bc3f420f0 | |||
960646a68c | |||
d1dbe965f9 | |||
9fa6ec46dd | |||
d03afa3017 | |||
f157545b01 | |||
e18f70a20c | |||
e8ce851c31 | |||
4dd4215fd0 | |||
dad843f1d4 | |||
47b1dd7732 | |||
a599cc9f9c | |||
893066942d | |||
858538e3eb | |||
b42a5a9abb | |||
9ea8422007 | |||
39fdc9ef1d | |||
a161677f79 | |||
013c31272b | |||
911a086963 | |||
c1a47045e7 | |||
644188f952 | |||
717bc5750b | |||
3ca1aa3756 | |||
afd20ac4e3 | |||
79c96ff2b5 | |||
b135b0690e | |||
041b837068 | |||
ea8a56322f | |||
0b418b6f2c | |||
a2abfc83ee | |||
03ce6824ee | |||
c12f9925ae | |||
3efa835295 | |||
d8fdb17112 | |||
5842e81455 | |||
f057693a61 | |||
c77d33e910 | |||
ecb7116f4a | |||
3464fcf93b | |||
c8f0c54549 | |||
4367878396 | |||
c950daa011 | |||
725b009ee6 | |||
9fcb793bb2 | |||
bd574ce952 | |||
77f0554893 | |||
e07af389ba | |||
3060fd0b86 | |||
3184e5fb07 | |||
02140ce731 | |||
dd06d9e1ff | |||
ca4e9add88 | |||
657bbfb25a | |||
64e344206c | |||
a132baa838 | |||
f833b93074 | |||
06991853ff | |||
cff343989b | |||
ff8d422308 | |||
aded110307 | |||
fbc6a3c338 | |||
7629c6cf9b | |||
96b2eec050 | |||
30394644bf | |||
610df69e21 | |||
0b0b527b55 | |||
724e7e6fb0 | |||
30160fb033 | |||
317a66b714 | |||
aa9c0c7552 | |||
22f46160be | |||
745e1f6a66 | |||
d8fae15995 | |||
5d8332015b | |||
91e83b0ad9 | |||
f29a8ebc23 | |||
68a2ca73cc | |||
121ee7b861 | |||
af0b05cdd5 | |||
73c3f0ac34 | |||
0db2c09be4 | |||
21a85555a7 | |||
cf35677464 | |||
c508cfc8a0 | |||
2e321d22bc | |||
16199cbd30 | |||
29443a51fc | |||
bac22d6a9d | |||
f86038c169 | |||
38314ac977 | |||
83fb6d005b | |||
c755feb9a7 | |||
6f0a67a957 | |||
66b6ba9ae9 | |||
f1e1e76266 | |||
22fcd42689 | |||
efc95ef736 | |||
184f775af4 | |||
4eec08ffda | |||
9c2b0cbe66 | |||
84499bf5d1 | |||
0338e5c8de | |||
b9c4228edc | |||
c907efbadb | |||
c54c54d821 | |||
d608a3d138 | |||
18ddd92509 | |||
02afaf29ff | |||
bb99b89a38 | |||
b988fe3084 | |||
697ebbb96b | |||
241f18a055 | |||
a32713f6b7 | |||
f910503f3c | |||
9cb142fa4b | |||
991d30e1fd | |||
504437f8cc | |||
33fa7fd61c | |||
16a827c73b | |||
805835721e | |||
82bd63b80d | |||
caa6a6df99 | |||
34f7dd834d | |||
3e6206aaad | |||
6e789e6966 | |||
d1364c6696 | |||
1e71d0d39b | |||
b8b534e499 | |||
245962668f | |||
d5dd58f78e | |||
fcb212e66e | |||
038a3404d7 | |||
b6e0a90848 | |||
29dd03557c | |||
fe73472aa4 | |||
6bab430466 | |||
776449ba10 | |||
9ba2fd6779 | |||
02595ca010 | |||
59c541486d | |||
9b1d3384cd | |||
fe27218256 | |||
64c9ed7978 | |||
919bfd9d98 | |||
1ba41d94fb | |||
0ed80217f2 | |||
346fc57ba7 | |||
ba443c4e64 | |||
c753b799ee | |||
70baac6fb2 | |||
1ea5937881 | |||
33726b9114 | |||
a89d13c2b1 | |||
0d32fd3528 | |||
3dce2dad05 | |||
e17164d336 | |||
c36e9a5293 | |||
dcf8136191 | |||
10e63b3135 | |||
2baee0e4d0 | |||
ae94285dc7 | |||
c3a0c563a6 | |||
d0590b8515 | |||
f0c9c0cdc6 | |||
b57d68082b | |||
af494acd7f | |||
e205b0a7a6 | |||
c7fc8055f6 | |||
76cc4e1e8a | |||
4bbff26bd2 | |||
2f9b65a326 | |||
27b7550114 | |||
06cefb9223 | |||
67a5e63b33 | |||
4dae20b056 | |||
4adb38fdd2 | |||
ab7c840c60 | |||
997d2df4b7 | |||
4160d974f5 | |||
baf7d0226e | |||
36992f8469 | |||
325509aa16 | |||
57cc0d2caa | |||
4833245e34 | |||
d57dfb809b | |||
98d47c69d2 | |||
ded0cc925c | |||
857bcc6652 | |||
f3be33431c | |||
8bd3a8fb45 | |||
67db68c5ea | |||
3baae2a6cd | |||
4d3a8e768b | |||
169b8dbf4d | |||
8d7d5a127f | |||
ad4d030a40 | |||
ce0cc856a7 | |||
fae11ca310 | |||
95af0d3848 | |||
596c708212 | |||
2f9b6c6027 | |||
84ac2d3562 | |||
f9837e0ec4 | |||
73a63f5af4 | |||
5066cf7536 | |||
6522d274a3 | |||
759e774928 | |||
56aab474e9 | |||
1bd1d546fc | |||
fe3688791f | |||
a00db4e7f6 | |||
f80ed4d366 | |||
cd47e643d5 | |||
db12750dc0 | |||
59a9a1ca75 | |||
942104ebed | |||
df7b40ba03 | |||
1488c6e96a | |||
b419c03774 | |||
1ad370e7cf | |||
b7cdb21136 | |||
78afe7c86b | |||
5507c4a366 | |||
a4788aba75 | |||
b6593c6825 | |||
f9393a59d2 | |||
22b02dc044 | |||
e5b0ed4c69 | |||
96565aa2c5 | |||
17a29405c5 | |||
c5eb26e62b | |||
49088f1f68 | |||
9140a886f2 | |||
894ed14842 | |||
d7a8840444 | |||
624f892a73 | |||
26e475a42f | |||
de2722f6ca | |||
32913895c9 | |||
744d9a2ec4 | |||
98bc0fc1d7 | |||
f7543ced8d | |||
7190354d6a | |||
467fc18bd1 | |||
7572763129 | |||
76e7d64341 | |||
120380a95c | |||
4c5c460d88 | |||
2865c3d32a | |||
efd205c244 | |||
c91857850b | |||
a58cb1ee95 | |||
995a102da1 | |||
40a15ed4f0 | |||
52e852c647 | |||
9a35d023d9 | |||
cbecdae5d1 | |||
bcc71ee866 | |||
824244f217 | |||
06e0943e30 | |||
b8cac94f9a | |||
a1207bfb0f | |||
b0127584d6 |
2
.cz.yaml
2
.cz.yaml
@ -17,5 +17,5 @@ commitizen:
|
||||
prerelease_offset: 1
|
||||
tag_format: $version
|
||||
update_changelog_on_bump: false
|
||||
version: 1.8.0
|
||||
version: 1.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,
|
||||
}
|
1386
CHANGELOG.md
1386
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -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,19 +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',
|
||||
|
@ -0,0 +1,49 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-09 11:07
|
||||
|
||||
import access.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0002_alter_organization_options_alter_team_options_and_more'),
|
||||
('core', '0012_modelnotes'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='team',
|
||||
name='organization',
|
||||
field=models.ForeignKey(help_text='Organization this belongs to', on_delete=django.db.models.deletion.CASCADE, to='access.organization', validators=[access.models.team.Team.validatate_organization_exists], verbose_name='Organization'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='OrganizationNotes',
|
||||
fields=[
|
||||
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.organization', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization Note',
|
||||
'verbose_name_plural': 'Organization Notes',
|
||||
'db_table': 'access_organization_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamNotes',
|
||||
fields=[
|
||||
('modelnotes_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelnotes')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='access.team', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team Note',
|
||||
'verbose_name_plural': 'Team Notes',
|
||||
'db_table': 'access_team_notes',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelnotes',),
|
||||
),
|
||||
]
|
@ -0,0 +1,43 @@
|
||||
# Generated by Django 5.1.5 on 2025-02-20 13:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('access', '0003_alter_team_organization_organizationnotes_teamnotes'),
|
||||
('core', '0015_modelhistory_manufacturerhistory_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationHistory',
|
||||
fields=[
|
||||
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.organization', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Organization History',
|
||||
'verbose_name_plural': 'Organization History',
|
||||
'db_table': 'access_organization_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TeamHistory',
|
||||
fields=[
|
||||
('modelhistory_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='core.modelhistory')),
|
||||
('model', models.ForeignKey(help_text='Model this note belongs to', on_delete=django.db.models.deletion.CASCADE, related_name='history', to='access.team', verbose_name='Model')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Team History',
|
||||
'verbose_name_plural': 'Team History',
|
||||
'db_table': 'access_team_history',
|
||||
'ordering': ['-created'],
|
||||
},
|
||||
bases=('core.modelhistory',),
|
||||
),
|
||||
]
|
@ -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,5 +1,13 @@
|
||||
from django.contrib.auth.models import User, Group
|
||||
from access.models import Organization, Team
|
||||
import django
|
||||
|
||||
from django.contrib.auth.models import Group
|
||||
from django.db import models
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.team import Team
|
||||
|
||||
User = django.contrib.auth.get_user_model()
|
||||
|
||||
|
||||
|
||||
class OrganizationMixin:
|
||||
@ -38,19 +46,19 @@ class OrganizationMixin:
|
||||
return self._obj_organization
|
||||
|
||||
|
||||
_obj_organization: Organization = None
|
||||
|
||||
|
||||
if obj:
|
||||
|
||||
_obj_organization = getattr(obj, 'organization', None)
|
||||
self._obj_organization = getattr(obj, 'organization', None)
|
||||
|
||||
|
||||
if not _obj_organization:
|
||||
if not self._obj_organization:
|
||||
|
||||
_obj_organization = getattr(obj, 'get_organization', lambda: None)()
|
||||
self._obj_organization = getattr(obj, 'get_organization', lambda: None)()
|
||||
|
||||
elif request:
|
||||
elif (
|
||||
request
|
||||
and not self.kwargs.get('pk', None)
|
||||
):
|
||||
|
||||
if getattr(request.stream, 'method', '') != 'DELETE':
|
||||
|
||||
@ -72,23 +80,31 @@ class OrganizationMixin:
|
||||
|
||||
if data_organization:
|
||||
|
||||
_obj_organization = Organization.objects.get(
|
||||
self._obj_organization = Organization.objects.get(
|
||||
pk = int( data_organization )
|
||||
)
|
||||
|
||||
elif self.kwargs.get('pk', None):
|
||||
|
||||
|
||||
obj = self.model.objects.get( pk = self.kwargs.get('pk', None) )
|
||||
|
||||
if getattr(obj, 'organization', None):
|
||||
|
||||
self._obj_organization = obj.organization
|
||||
|
||||
elif str(self.model._meta.verbose_name).lower() == 'tenant':
|
||||
|
||||
self._obj_organization = obj
|
||||
|
||||
|
||||
if self.get_parent_model(): # if defined is to overwrite object organization
|
||||
|
||||
parent_obj = self.get_parent_obj()
|
||||
|
||||
_obj_organization = parent_obj.get_organization()
|
||||
self._obj_organization = parent_obj.get_organization()
|
||||
|
||||
|
||||
|
||||
if _obj_organization:
|
||||
|
||||
self._obj_organization = _obj_organization
|
||||
|
||||
return self._obj_organization
|
||||
|
||||
|
||||
@ -118,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])
|
||||
|
||||
|
||||
|
||||
@ -245,7 +261,7 @@ class OrganizationMixin:
|
||||
|
||||
|
||||
|
||||
parent_model: str = None
|
||||
parent_model: models.Model = None
|
||||
""" Parent Model
|
||||
|
||||
This attribute defines the parent model for the model in question. The parent model when defined
|
||||
|
@ -5,7 +5,7 @@ from django.core.exceptions import ObjectDoesNotExist
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.permissions import DjangoObjectPermissions
|
||||
|
||||
from access.models import TenancyObject
|
||||
from access.models.tenancy import 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.
|
||||
|
||||
@ -115,6 +115,33 @@ class OrganizationPermissionMixin(
|
||||
|
||||
try:
|
||||
|
||||
if (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
return True
|
||||
|
||||
elif (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id != int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id != int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
|
||||
return False
|
||||
|
||||
|
||||
has_permission_required: bool = False
|
||||
|
||||
@ -139,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
|
||||
)
|
||||
|
||||
@ -159,10 +186,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'delete'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif (
|
||||
view.action == 'list'
|
||||
):
|
||||
@ -176,10 +199,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'change'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif (
|
||||
view.action == 'update'
|
||||
and request.method == 'PUT'
|
||||
@ -187,10 +206,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'change'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif(
|
||||
view.action == 'retrieve'
|
||||
and request.method == 'GET'
|
||||
@ -198,10 +213,6 @@ class OrganizationPermissionMixin(
|
||||
|
||||
view_action = 'view'
|
||||
|
||||
obj_organization: Organization = view.get_obj_organization(
|
||||
obj = view.get_object()
|
||||
)
|
||||
|
||||
elif(
|
||||
view.action == 'metadata'
|
||||
and request.method == 'OPTIONS'
|
||||
@ -265,17 +276,32 @@ class OrganizationPermissionMixin(
|
||||
|
||||
try:
|
||||
|
||||
|
||||
if request.user.is_anonymous:
|
||||
|
||||
return False
|
||||
|
||||
|
||||
object_organization: int = getattr(view.get_obj_organization( obj = obj ), 'id', None)
|
||||
if (
|
||||
(
|
||||
view.model.__name__ == 'UserSettings'
|
||||
and request._user.id == int(view.kwargs.get('pk', 0))
|
||||
)
|
||||
or (
|
||||
view.model.__name__ == 'AuthToken'
|
||||
and request._user.id == int(view.kwargs.get('model_id', 0))
|
||||
)
|
||||
):
|
||||
|
||||
return True
|
||||
|
||||
|
||||
object_organization = view._obj_organization
|
||||
|
||||
if object_organization:
|
||||
|
||||
if(
|
||||
object_organization
|
||||
int(object_organization)
|
||||
in view.get_permission_organizations( view.get_permission_required() )
|
||||
or request.user.is_superuser
|
||||
or getattr(request.app_settings.global_organization, 'id', 0) == int(object_organization)
|
||||
|
@ -1,594 +0,0 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
|
||||
from rest_framework.reverse import reverse
|
||||
|
||||
from .fields import *
|
||||
|
||||
from core import exceptions as centurion_exceptions
|
||||
from core.middleware.get_request import get_request
|
||||
from core.mixin.history_save import SaveHistory
|
||||
|
||||
|
||||
|
||||
class Organization(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Organization"
|
||||
verbose_name_plural = "Organizations"
|
||||
ordering = ['name']
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
if self.slug == '_':
|
||||
self.slug = self.name.lower().replace(' ', '_')
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name of this Organization',
|
||||
max_length = 50,
|
||||
unique = True,
|
||||
verbose_name = 'Name'
|
||||
)
|
||||
|
||||
manager = models.ForeignKey(
|
||||
User,
|
||||
blank = False,
|
||||
help_text = 'Manager for this organization',
|
||||
null = True,
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name = 'Manager'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null= True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
slug = AutoSlugField()
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
|
||||
def get_organization(self):
|
||||
return self
|
||||
|
||||
def __int__(self):
|
||||
|
||||
return self.id
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
table_fields: list = [
|
||||
'nbsp',
|
||||
'name',
|
||||
'created',
|
||||
'modified',
|
||||
'nbsp'
|
||||
]
|
||||
|
||||
page_layout: list = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'name',
|
||||
'manager',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Teams",
|
||||
"slug": "teams",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "teams"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
if request:
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", request=request, kwargs={'pk': self.id})
|
||||
|
||||
return reverse("v2:_api_v2_organization-detail", kwargs={'pk': self.id})
|
||||
|
||||
|
||||
|
||||
class TenancyManager(models.Manager):
|
||||
"""Multi-Tennant Object Manager
|
||||
|
||||
This manager specifically caters for the multi-tenancy features of Centurion ERP.
|
||||
"""
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
""" Fetch the data
|
||||
|
||||
This function filters the data fetched from the database to that which is from the organizations
|
||||
the user is a part of.
|
||||
|
||||
!!! danger "Requirement"
|
||||
This method may be overridden however must still be called from the overriding function. i.e. `super().get_queryset()`
|
||||
|
||||
## Workflow
|
||||
|
||||
This functions workflow is as follows:
|
||||
|
||||
- Fetch the user from the request
|
||||
|
||||
- Check if the user is authenticated
|
||||
|
||||
- Iterate over the users teams
|
||||
|
||||
- Store unique organizations from users teams
|
||||
|
||||
- return results
|
||||
|
||||
Returns:
|
||||
(queryset): **super user**: return unfiltered data.
|
||||
(queryset): **not super user**: return data from the stored unique organizations.
|
||||
"""
|
||||
|
||||
request = get_request()
|
||||
|
||||
user_organizations: list(str()) = []
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
if request.app_settings.global_organization:
|
||||
|
||||
user_organizations += [ request.app_settings.global_organization.id ]
|
||||
|
||||
|
||||
user = request.user
|
||||
|
||||
|
||||
if user.is_authenticated:
|
||||
|
||||
for team in request.tenancy._user_teams:
|
||||
|
||||
|
||||
if team.organization.id not in user_organizations:
|
||||
|
||||
if not user_organizations:
|
||||
|
||||
self.user_organizations = []
|
||||
|
||||
user_organizations += [ team.organization.id ]
|
||||
|
||||
|
||||
# if len(user_organizations) > 0 and not user.is_superuser and self.model.is_global is not None:
|
||||
if len(user_organizations) > 0 and not user.is_superuser:
|
||||
|
||||
if getattr(self.model, 'is_global', False) is True:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
|
|
||||
models.Q(is_global = True)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
return super().get_queryset().filter(
|
||||
models.Q(organization__in=user_organizations)
|
||||
)
|
||||
|
||||
return super().get_queryset()
|
||||
|
||||
|
||||
|
||||
class TenancyObject(SaveHistory):
|
||||
""" Tenancy Model Abstrct class.
|
||||
|
||||
This class is for inclusion wihtin **every** model within Centurion ERP.
|
||||
Provides the required fields, functions and methods for multi tennant objects.
|
||||
Unless otherwise stated, **no** object within this class may be overridden.
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization
|
||||
"""
|
||||
|
||||
objects = TenancyManager()
|
||||
""" Multi-Tenanant Objects """
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def validatate_organization_exists(self):
|
||||
"""Ensure that the user did provide an organization
|
||||
|
||||
Raises:
|
||||
ValidationError: User failed to supply organization.
|
||||
"""
|
||||
|
||||
if not self:
|
||||
raise ValidationError('You must provide an organization')
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of the item',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
organization = models.ForeignKey(
|
||||
Organization,
|
||||
blank = False,
|
||||
help_text = 'Organization this belongs to',
|
||||
null = False,
|
||||
on_delete = models.CASCADE,
|
||||
validators = [validatate_organization_exists],
|
||||
verbose_name = 'Organization'
|
||||
)
|
||||
|
||||
is_global = models.BooleanField(
|
||||
blank = False,
|
||||
default = False,
|
||||
help_text = 'Is this a global object?',
|
||||
verbose_name = 'Global Object'
|
||||
)
|
||||
|
||||
model_notes = models.TextField(
|
||||
blank = True,
|
||||
default = None,
|
||||
help_text = 'Tid bits of information',
|
||||
null = True,
|
||||
verbose_name = 'Notes',
|
||||
)
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
"""Fetch the models URL
|
||||
|
||||
If URL kwargs are required to generate the URL, define a `get_url_kwargs` that returns them.
|
||||
|
||||
Args:
|
||||
request (object, optional): The request object that was made by the end user. Defaults to None.
|
||||
|
||||
Returns:
|
||||
str: Canonical URL of the model if the `request` object was provided. Otherwise the relative URL.
|
||||
"""
|
||||
|
||||
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_{model_name}-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.clean()
|
||||
|
||||
if not getattr(self, 'organization', None):
|
||||
|
||||
raise centurion_exceptions.ValidationError(
|
||||
detail = {
|
||||
'organization': 'Organization is required'
|
||||
},
|
||||
code = 'required'
|
||||
)
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
|
||||
class Team(Group, TenancyObject):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = [ 'team_name' ]
|
||||
|
||||
verbose_name = 'Team'
|
||||
|
||||
verbose_name_plural = "Teams"
|
||||
|
||||
|
||||
def save(self, force_insert=False, force_update=False, using=None, update_fields=None):
|
||||
|
||||
self.name = self.organization.name.lower().replace(' ', '_') + '_' + self.team_name.lower().replace(' ', '_')
|
||||
|
||||
super().save(force_insert=force_insert, force_update=force_update, using=using, update_fields=update_fields)
|
||||
|
||||
|
||||
team_name = models.CharField(
|
||||
blank = False,
|
||||
help_text = 'Name to give this team',
|
||||
max_length = 50,
|
||||
unique = False,
|
||||
verbose_name = 'Name',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: dict = [
|
||||
{
|
||||
"name": "Details",
|
||||
"slug": "details",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "double",
|
||||
"left": [
|
||||
'organization',
|
||||
'team_name',
|
||||
'created',
|
||||
'modified',
|
||||
],
|
||||
"right": [
|
||||
'model_notes',
|
||||
]
|
||||
},
|
||||
{
|
||||
"layout": "table",
|
||||
"name": "Users",
|
||||
"field": "users",
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Knowledge Base",
|
||||
"slug": "kb_articles",
|
||||
"sections": [
|
||||
{
|
||||
"layout": "table",
|
||||
"field": "knowledge_base",
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notes",
|
||||
"slug": "notes",
|
||||
"sections": []
|
||||
},
|
||||
]
|
||||
|
||||
table_fields: list = [
|
||||
'team_name',
|
||||
'modified',
|
||||
'created',
|
||||
]
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
model_name = str(self._meta.verbose_name.lower()).replace(' ', '_')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", request=request, kwargs = self.get_url_kwargs() )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team-detail", kwargs = self.get_url_kwargs() )
|
||||
|
||||
|
||||
def get_url_kwargs(self) -> dict:
|
||||
"""Fetch the URL kwargs
|
||||
|
||||
Returns:
|
||||
dict: kwargs required for generating the URL with `reverse`
|
||||
"""
|
||||
|
||||
return {
|
||||
'organization_id': self.organization.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.organization
|
||||
|
||||
|
||||
def permission_list(self) -> list:
|
||||
|
||||
permission_list = []
|
||||
|
||||
for permission in self.permissions.all():
|
||||
|
||||
if str(permission.content_type.app_label + '.' + permission.codename) in permission_list:
|
||||
continue
|
||||
|
||||
permission_list += [ str(permission.content_type.app_label + '.' + permission.codename) ]
|
||||
|
||||
return [permission_list, self.permissions.all()]
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return self.organization.name + ', ' + self.team_name
|
||||
|
||||
|
||||
|
||||
class TeamUsers(SaveHistory):
|
||||
|
||||
class Meta:
|
||||
|
||||
ordering = ['user']
|
||||
|
||||
verbose_name = "Team User"
|
||||
|
||||
verbose_name_plural = "Team Users"
|
||||
|
||||
|
||||
id = models.AutoField(
|
||||
blank=False,
|
||||
help_text = 'ID of this Team User',
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
verbose_name = 'ID'
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
blank = False,
|
||||
help_text = 'Team user belongs to',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="team",
|
||||
verbose_name = 'Team'
|
||||
)
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
blank = False,
|
||||
help_text = 'User who will be added to the team',
|
||||
null = False,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name = 'User'
|
||||
)
|
||||
|
||||
manager = models.BooleanField(
|
||||
blank=True,
|
||||
default=False,
|
||||
help_text = 'Is this user to be a manager of this team',
|
||||
verbose_name='manager',
|
||||
)
|
||||
|
||||
created = AutoCreatedField()
|
||||
|
||||
modified = AutoLastModifiedField()
|
||||
|
||||
page_layout: list = []
|
||||
|
||||
table_fields: list = [
|
||||
'user',
|
||||
'manager'
|
||||
]
|
||||
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
""" Delete Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of Groups, remove the user to the team.
|
||||
"""
|
||||
|
||||
super().delete(using=using, keep_parents=keep_parents)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.remove(group)
|
||||
|
||||
|
||||
def get_organization(self) -> Organization:
|
||||
return self.team.organization
|
||||
|
||||
|
||||
def get_url( self, request = None ) -> str:
|
||||
|
||||
url_kwargs: dict = {
|
||||
'organization_id': self.team.organization.id,
|
||||
'team_id': self.team.id,
|
||||
'pk': self.id
|
||||
}
|
||||
|
||||
print(f'url kwargs are: {url_kwargs}')
|
||||
|
||||
|
||||
if request:
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", request=request, kwargs = url_kwargs )
|
||||
|
||||
return reverse(f"v2:_api_v2_organization_team_user-detail", kwargs = url_kwargs )
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Save Team
|
||||
|
||||
Overrides, post-action
|
||||
As teams are an extension of groups, add the user to the matching group.
|
||||
"""
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
group = Group.objects.get(pk=self.team.id)
|
||||
|
||||
user = User.objects.get(pk=self.user_id)
|
||||
|
||||
user.groups.add(group)
|
||||
|
||||
|
||||
@property
|
||||
def parent_object(self):
|
||||
""" Fetch the parent object """
|
||||
|
||||
return self.team
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
||||
|
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')
|
||||
@ -60,6 +61,13 @@ class OrganizationModelSerializer(
|
||||
'model_pk': item.pk
|
||||
}
|
||||
),
|
||||
'notes': reverse(
|
||||
"v2:_api_v2_organization_note-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'model_id': item.pk
|
||||
}
|
||||
),
|
||||
'teams': reverse("v2:_api_v2_organization_team-list", request=self._context['view'].request, kwargs={'organization_id': item.pk}),
|
||||
}
|
||||
|
||||
@ -67,7 +75,7 @@ class OrganizationModelSerializer(
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Organization
|
||||
model = Tenant
|
||||
|
||||
fields = '__all__'
|
||||
|
||||
@ -91,7 +99,7 @@ class OrganizationModelSerializer(
|
||||
]
|
||||
|
||||
|
||||
class OrganizationViewSerializer(OrganizationModelSerializer):
|
||||
class TenantViewSerializer(TenantModelSerializer):
|
||||
pass
|
||||
|
||||
manager = UserBaseSerializer(many=False, read_only = True)
|
||||
|
48
app/access/serializers/organization_notes.py
Normal file
48
app/access/serializers/organization_notes.py
Normal file
@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core.serializers.model_notes import (
|
||||
ModelNotes,
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class OrganizationNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class OrganizationNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class OrganizationNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
OrganizationNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
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
|
48
app/access/serializers/team_notes.py
Normal file
48
app/access/serializers/team_notes.py
Normal file
@ -0,0 +1,48 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
from app.serializers.user import UserBaseSerializer
|
||||
|
||||
from core.serializers.model_notes import (
|
||||
ModelNotes,
|
||||
ModelNoteBaseSerializer,
|
||||
ModelNoteModelSerializer,
|
||||
ModelNoteViewSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class TeamNoteBaseSerializer(ModelNoteBaseSerializer):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class TeamNoteModelSerializer(
|
||||
ModelNoteModelSerializer
|
||||
):
|
||||
|
||||
|
||||
class Meta:
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
fields = ModelNoteModelSerializer.Meta.fields + [
|
||||
'model',
|
||||
]
|
||||
|
||||
read_only_fields = ModelNoteModelSerializer.Meta.read_only_fields + [
|
||||
'model',
|
||||
'content_type',
|
||||
]
|
||||
|
||||
|
||||
|
||||
class TeamNoteViewSerializer(
|
||||
ModelNoteViewSerializer,
|
||||
TeamNoteModelSerializer,
|
||||
):
|
||||
|
||||
pass
|
@ -2,7 +2,7 @@ from rest_framework.reverse import reverse
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from access.models import TeamUsers
|
||||
from access.models.team_user import TeamUsers
|
||||
|
||||
from api.serializers import common
|
||||
|
||||
@ -53,9 +53,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,16 +61,9 @@ class TeamModelSerializer(
|
||||
|
||||
def get_url(self, item) -> dict:
|
||||
|
||||
return {
|
||||
'_self': item.get_url( request = self._context['view'].request ),
|
||||
'knowledge_base': reverse(
|
||||
"v2:_api_v2_model_kb-list",
|
||||
request=self._context['view'].request,
|
||||
kwargs={
|
||||
'model': self.Meta.model._meta.model_name,
|
||||
'model_pk': item.pk
|
||||
}
|
||||
),
|
||||
get_url = super().get_url( item = item )
|
||||
|
||||
get_url.update({
|
||||
'users': reverse(
|
||||
'v2:_api_v2_organization_team_user-list',
|
||||
request=self.context['view'].request,
|
||||
@ -79,7 +72,10 @@ class TeamModelSerializer(
|
||||
'team_id': item.pk
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return get_url
|
||||
|
||||
|
||||
team_name = centurion_field.CharField( autolink = True )
|
||||
|
||||
@ -131,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'
|
@ -0,0 +1,116 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.viewsets.organization_notes import ViewSet
|
||||
|
||||
from core.tests.abstract.test_functional_notes_viewset import (
|
||||
ModelNotesViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
ModelNotesPermissionsAPI,
|
||||
ModelNotesSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetBase(
|
||||
ModelNotesViewSetBase
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
url_name = '_api_v2_organization_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.item = self.viewset.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.other_org_item = self.viewset.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.different_organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.global_org_item = self.viewset.model.objects.create(
|
||||
organization = self.global_organization,
|
||||
content = 'a random comment global_organization',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.global_organization,
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.url_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesPermissionsAPI(
|
||||
ViewSetBase,
|
||||
ModelNotesPermissionsAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||
"""Check items returned
|
||||
|
||||
This test case is a over-ride of a test case with the same name.
|
||||
This model is not a global model making this test not-applicable.
|
||||
|
||||
Items returned from the query Must be from the users organization and
|
||||
global ONLY!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesSerializer(
|
||||
ViewSetBase,
|
||||
ModelNotesSerializer,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class OrganizationModelNotesMetadata(
|
||||
ViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,53 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models.tenant import Tenant as Organization
|
||||
from access.models.organization_notes import OrganizationNotes
|
||||
|
||||
|
||||
class OrganizationNotesAPI(
|
||||
ModelNotesNotesAPIFields,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = OrganizationNotes
|
||||
|
||||
view_name: str = '_api_v2_organization_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Call parent setup
|
||||
2. Create a model note
|
||||
3. add url kwargs
|
||||
4. make the API request
|
||||
|
||||
"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = Organization.objects.create(
|
||||
name = 'dev'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.pk
|
||||
}
|
||||
|
||||
self.make_request()
|
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:
|
||||
|
@ -0,0 +1,127 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from access.viewsets.team_notes import ViewSet
|
||||
|
||||
from core.tests.abstract.test_functional_notes_viewset import (
|
||||
ModelNotesViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
ModelNotesPermissionsAPI,
|
||||
ModelNotesSerializer
|
||||
)
|
||||
|
||||
|
||||
|
||||
class ViewSetBase(
|
||||
ModelNotesViewSetBase
|
||||
):
|
||||
|
||||
viewset = ViewSet
|
||||
|
||||
url_name = '_api_v2_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
self.item = self.viewset.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.other_org_item = self.viewset.model.objects.create(
|
||||
organization = self.different_organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.different_organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.global_org_item = self.viewset.model.objects.create(
|
||||
organization = self.global_organization,
|
||||
content = 'a random comment global_organization',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = self.viewset.model.model.field.related_model.objects.create(
|
||||
organization = self.global_organization,
|
||||
name = 'note model global_organization'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
self.url_kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': self.item.model.pk,
|
||||
}
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'organization_id': self.organization.id,
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.id
|
||||
}
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesPermissionsAPI(
|
||||
ViewSetBase,
|
||||
ModelNotesPermissionsAPI,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
|
||||
def test_returned_data_from_user_and_global_organizations_only(self):
|
||||
"""Check items returned
|
||||
|
||||
This test case is a over-ride of a test case with the same name.
|
||||
This model is not a global model making this test not-applicable.
|
||||
|
||||
Items returned from the query Must be from the users organization and
|
||||
global ONLY!
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesSerializer(
|
||||
ViewSetBase,
|
||||
ModelNotesSerializer,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
|
||||
class TeamModelNotesMetadata(
|
||||
ViewSetBase,
|
||||
ModelNotesMetadata,
|
||||
TestCase,
|
||||
|
||||
):
|
||||
|
||||
pass
|
@ -0,0 +1,56 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from core.tests.abstract.model_notes_api_fields import ModelNotesNotesAPIFields
|
||||
|
||||
from access.models.team import Team
|
||||
from access.models.team_notes import TeamNotes
|
||||
|
||||
|
||||
|
||||
class TeamNotesAPI(
|
||||
ModelNotesNotesAPIFields,
|
||||
TestCase,
|
||||
):
|
||||
|
||||
model = TeamNotes
|
||||
|
||||
view_name: str = '_api_v2_team_note'
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(self):
|
||||
"""Setup Test
|
||||
|
||||
1. Call parent setup
|
||||
2. Create a model note
|
||||
3. add url kwargs
|
||||
4. make the API request
|
||||
|
||||
"""
|
||||
|
||||
super().setUpTestData()
|
||||
|
||||
|
||||
self.item = self.model.objects.create(
|
||||
organization = self.organization,
|
||||
content = 'a random comment',
|
||||
content_type = ContentType.objects.get(
|
||||
app_label = str(self.model._meta.app_label).lower(),
|
||||
model = str(self.model.model.field.related_model.__name__).replace(' ', '').lower(),
|
||||
),
|
||||
model = Team.objects.create(
|
||||
organization = self.organization,
|
||||
name = 'note model'
|
||||
),
|
||||
created_by = self.view_user,
|
||||
modified_by = self.view_user,
|
||||
)
|
||||
|
||||
|
||||
self.url_view_kwargs = {
|
||||
'organization_id': self.organization.pk,
|
||||
'model_id': self.item.model.pk,
|
||||
'pk': self.item.pk
|
||||
}
|
||||
|
||||
self.make_request()
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user