From eec700af51f7bbacfc0303e4c3cd1d32709c370a Mon Sep 17 00:00:00 2001 From: __init__ Date: Mon, 23 Feb 2026 20:31:53 +0530 Subject: [PATCH] Backend Draft --- backend/accounts/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 219 bytes .../__pycache__/admin.cpython-314.pyc | Bin 0 -> 940 bytes .../accounts/__pycache__/apps.cpython-314.pyc | Bin 0 -> 584 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 1433 bytes .../__pycache__/permissions.cpython-314.pyc | Bin 0 -> 4206 bytes .../__pycache__/serializers.cpython-314.pyc | Bin 0 -> 3295 bytes .../__pycache__/tests.cpython-314.pyc | Bin 0 -> 4142 bytes .../accounts/__pycache__/urls.cpython-314.pyc | Bin 0 -> 990 bytes .../__pycache__/views.cpython-314.pyc | Bin 0 -> 2708 bytes backend/accounts/admin.py | 12 + backend/accounts/apps.py | 6 + backend/accounts/migrations/0001_initial.py | 154 +++++++++ .../migrations/0002_alter_user_managers.py | 20 ++ backend/accounts/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-314.pyc | Bin 0 -> 4042 bytes .../0002_alter_user_managers.cpython-314.pyc | Bin 0 -> 967 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 230 bytes backend/accounts/models.py | 22 ++ backend/accounts/permissions.py | 57 ++++ backend/accounts/serializers.py | 44 +++ backend/accounts/tests.py | 59 ++++ backend/accounts/urls.py | 11 + backend/accounts/views.py | 33 ++ backend/analytics/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 220 bytes .../__pycache__/admin.cpython-314.pyc | Bin 0 -> 264 bytes .../__pycache__/apps.cpython-314.pyc | Bin 0 -> 796 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 3657 bytes .../__pycache__/serializers.cpython-314.pyc | Bin 0 -> 1659 bytes .../__pycache__/signals.cpython-314.pyc | Bin 0 -> 7230 bytes .../__pycache__/urls.cpython-314.pyc | Bin 0 -> 561 bytes .../__pycache__/views.cpython-314.pyc | Bin 0 -> 6027 bytes backend/analytics/admin.py | 3 + backend/analytics/apps.py | 9 + backend/analytics/migrations/0001_initial.py | 109 +++++++ backend/analytics/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-314.pyc | Bin 0 -> 3447 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 231 bytes backend/analytics/models.py | 45 +++ backend/analytics/serializers.py | 18 ++ backend/analytics/signals.py | 162 ++++++++++ backend/analytics/tests.py | 3 + backend/analytics/urls.py | 7 + backend/analytics/views.py | 95 ++++++ backend/core/__init__.py | 3 + .../core/__pycache__/__init__.cpython-314.pyc | Bin 0 -> 289 bytes .../core/__pycache__/celery.cpython-314.pyc | Bin 0 -> 940 bytes .../core/__pycache__/settings.cpython-314.pyc | Bin 0 -> 2600 bytes backend/core/__pycache__/urls.cpython-314.pyc | Bin 0 -> 1045 bytes backend/core/__pycache__/wsgi.cpython-314.pyc | Bin 0 -> 696 bytes backend/core/asgi.py | 7 + backend/core/celery.py | 20 ++ backend/core/settings/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 224 bytes .../settings/__pycache__/base.cpython-314.pyc | Bin 0 -> 4406 bytes .../settings/__pycache__/dev.cpython-314.pyc | Bin 0 -> 1543 bytes backend/core/settings/base.py | 158 ++++++++++ backend/core/settings/dev.py | 56 ++++ backend/core/settings/prod.py | 79 +++++ backend/core/urls.py | 13 + backend/core/wsgi.py | 16 + backend/dashboard/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 220 bytes .../__pycache__/admin.cpython-314.pyc | Bin 0 -> 264 bytes .../__pycache__/apps.cpython-314.pyc | Bin 0 -> 587 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 2075 bytes .../__pycache__/serializers.cpython-314.pyc | Bin 0 -> 954 bytes .../__pycache__/urls.cpython-314.pyc | Bin 0 -> 746 bytes .../__pycache__/views.cpython-314.pyc | Bin 0 -> 4599 bytes backend/dashboard/admin.py | 3 + backend/dashboard/apps.py | 6 + backend/dashboard/migrations/0001_initial.py | 62 ++++ backend/dashboard/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-314.pyc | Bin 0 -> 2309 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 231 bytes backend/dashboard/models.py | 24 ++ backend/dashboard/serializers.py | 8 + backend/dashboard/tasks.py | 23 ++ backend/dashboard/tests.py | 3 + backend/dashboard/urls.py | 12 + backend/dashboard/views.py | 62 ++++ backend/manage.py | 23 ++ backend/projects/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 219 bytes .../__pycache__/admin.cpython-314.pyc | Bin 0 -> 263 bytes .../projects/__pycache__/apps.cpython-314.pyc | Bin 0 -> 584 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 4255 bytes .../__pycache__/serializers.cpython-314.pyc | Bin 0 -> 3502 bytes .../__pycache__/tests.cpython-314.pyc | Bin 0 -> 7057 bytes .../projects/__pycache__/urls.cpython-314.pyc | Bin 0 -> 924 bytes .../__pycache__/views.cpython-314.pyc | Bin 0 -> 7186 bytes backend/projects/admin.py | 3 + backend/projects/apps.py | 6 + backend/projects/migrations/0001_initial.py | 160 ++++++++++ ...projects_pr_tenant__5d8ad4_idx_and_more.py | 21 ++ backend/projects/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-314.pyc | Bin 0 -> 4351 bytes ...enant__5d8ad4_idx_and_more.cpython-314.pyc | Bin 0 -> 1075 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 230 bytes backend/projects/models.py | 69 ++++ backend/projects/serializers.py | 43 +++ backend/projects/tests.py | 83 +++++ backend/projects/urls.py | 16 + backend/projects/views.py | 88 ++++++ backend/tenants/__init__.py | 0 .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 218 bytes .../tenants/__pycache__/admin.cpython-314.pyc | Bin 0 -> 774 bytes .../tenants/__pycache__/apps.cpython-314.pyc | Bin 0 -> 581 bytes .../__pycache__/managers.cpython-314.pyc | Bin 0 -> 1587 bytes .../__pycache__/middleware.cpython-314.pyc | Bin 0 -> 2270 bytes .../__pycache__/models.cpython-314.pyc | Bin 0 -> 1091 bytes .../__pycache__/serializers.cpython-314.pyc | Bin 0 -> 915 bytes backend/tenants/admin.py | 8 + backend/tenants/apps.py | 6 + .../populate_test_data.cpython-314.pyc | Bin 0 -> 18374 bytes .../management/commands/populate_test_data.py | 294 ++++++++++++++++++ backend/tenants/managers.py | 15 + backend/tenants/middleware.py | 39 +++ backend/tenants/migrations/0001_initial.py | 34 ++ backend/tenants/migrations/__init__.py | 0 .../__pycache__/0001_initial.cpython-314.pyc | Bin 0 -> 1284 bytes .../__pycache__/__init__.cpython-314.pyc | Bin 0 -> 229 bytes backend/tenants/models.py | 10 + backend/tenants/serializers.py | 8 + backend/tenants/tests.py | 3 + backend/tenants/views.py | 3 + 127 files changed, 2356 insertions(+) create mode 100644 backend/accounts/__init__.py create mode 100644 backend/accounts/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/admin.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/apps.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/models.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/permissions.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/serializers.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/tests.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/urls.cpython-314.pyc create mode 100644 backend/accounts/__pycache__/views.cpython-314.pyc create mode 100644 backend/accounts/admin.py create mode 100644 backend/accounts/apps.py create mode 100644 backend/accounts/migrations/0001_initial.py create mode 100644 backend/accounts/migrations/0002_alter_user_managers.py create mode 100644 backend/accounts/migrations/__init__.py create mode 100644 backend/accounts/migrations/__pycache__/0001_initial.cpython-314.pyc create mode 100644 backend/accounts/migrations/__pycache__/0002_alter_user_managers.cpython-314.pyc create mode 100644 backend/accounts/migrations/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/accounts/models.py create mode 100644 backend/accounts/permissions.py create mode 100644 backend/accounts/serializers.py create mode 100644 backend/accounts/tests.py create mode 100644 backend/accounts/urls.py create mode 100644 backend/accounts/views.py create mode 100644 backend/analytics/__init__.py create mode 100644 backend/analytics/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/admin.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/apps.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/models.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/serializers.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/signals.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/urls.cpython-314.pyc create mode 100644 backend/analytics/__pycache__/views.cpython-314.pyc create mode 100644 backend/analytics/admin.py create mode 100644 backend/analytics/apps.py create mode 100644 backend/analytics/migrations/0001_initial.py create mode 100644 backend/analytics/migrations/__init__.py create mode 100644 backend/analytics/migrations/__pycache__/0001_initial.cpython-314.pyc create mode 100644 backend/analytics/migrations/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/analytics/models.py create mode 100644 backend/analytics/serializers.py create mode 100644 backend/analytics/signals.py create mode 100644 backend/analytics/tests.py create mode 100644 backend/analytics/urls.py create mode 100644 backend/analytics/views.py create mode 100644 backend/core/__init__.py create mode 100644 backend/core/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/core/__pycache__/celery.cpython-314.pyc create mode 100644 backend/core/__pycache__/settings.cpython-314.pyc create mode 100644 backend/core/__pycache__/urls.cpython-314.pyc create mode 100644 backend/core/__pycache__/wsgi.cpython-314.pyc create mode 100644 backend/core/asgi.py create mode 100644 backend/core/celery.py create mode 100644 backend/core/settings/__init__.py create mode 100644 backend/core/settings/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/core/settings/__pycache__/base.cpython-314.pyc create mode 100644 backend/core/settings/__pycache__/dev.cpython-314.pyc create mode 100644 backend/core/settings/base.py create mode 100644 backend/core/settings/dev.py create mode 100644 backend/core/settings/prod.py create mode 100644 backend/core/urls.py create mode 100644 backend/core/wsgi.py create mode 100644 backend/dashboard/__init__.py create mode 100644 backend/dashboard/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/admin.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/apps.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/models.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/serializers.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/urls.cpython-314.pyc create mode 100644 backend/dashboard/__pycache__/views.cpython-314.pyc create mode 100644 backend/dashboard/admin.py create mode 100644 backend/dashboard/apps.py create mode 100644 backend/dashboard/migrations/0001_initial.py create mode 100644 backend/dashboard/migrations/__init__.py create mode 100644 backend/dashboard/migrations/__pycache__/0001_initial.cpython-314.pyc create mode 100644 backend/dashboard/migrations/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/dashboard/models.py create mode 100644 backend/dashboard/serializers.py create mode 100644 backend/dashboard/tasks.py create mode 100644 backend/dashboard/tests.py create mode 100644 backend/dashboard/urls.py create mode 100644 backend/dashboard/views.py create mode 100755 backend/manage.py create mode 100644 backend/projects/__init__.py create mode 100644 backend/projects/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/projects/__pycache__/admin.cpython-314.pyc create mode 100644 backend/projects/__pycache__/apps.cpython-314.pyc create mode 100644 backend/projects/__pycache__/models.cpython-314.pyc create mode 100644 backend/projects/__pycache__/serializers.cpython-314.pyc create mode 100644 backend/projects/__pycache__/tests.cpython-314.pyc create mode 100644 backend/projects/__pycache__/urls.cpython-314.pyc create mode 100644 backend/projects/__pycache__/views.cpython-314.pyc create mode 100644 backend/projects/admin.py create mode 100644 backend/projects/apps.py create mode 100644 backend/projects/migrations/0001_initial.py create mode 100644 backend/projects/migrations/0002_project_projects_pr_tenant__5d8ad4_idx_and_more.py create mode 100644 backend/projects/migrations/__init__.py create mode 100644 backend/projects/migrations/__pycache__/0001_initial.cpython-314.pyc create mode 100644 backend/projects/migrations/__pycache__/0002_project_projects_pr_tenant__5d8ad4_idx_and_more.cpython-314.pyc create mode 100644 backend/projects/migrations/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/projects/models.py create mode 100644 backend/projects/serializers.py create mode 100644 backend/projects/tests.py create mode 100644 backend/projects/urls.py create mode 100644 backend/projects/views.py create mode 100644 backend/tenants/__init__.py create mode 100644 backend/tenants/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/admin.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/apps.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/managers.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/middleware.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/models.cpython-314.pyc create mode 100644 backend/tenants/__pycache__/serializers.cpython-314.pyc create mode 100644 backend/tenants/admin.py create mode 100644 backend/tenants/apps.py create mode 100644 backend/tenants/management/commands/__pycache__/populate_test_data.cpython-314.pyc create mode 100644 backend/tenants/management/commands/populate_test_data.py create mode 100644 backend/tenants/managers.py create mode 100644 backend/tenants/middleware.py create mode 100644 backend/tenants/migrations/0001_initial.py create mode 100644 backend/tenants/migrations/__init__.py create mode 100644 backend/tenants/migrations/__pycache__/0001_initial.cpython-314.pyc create mode 100644 backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc create mode 100644 backend/tenants/models.py create mode 100644 backend/tenants/serializers.py create mode 100644 backend/tenants/tests.py create mode 100644 backend/tenants/views.py diff --git a/backend/accounts/__init__.py b/backend/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/accounts/__pycache__/__init__.cpython-314.pyc b/backend/accounts/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e4c064cfecea61223ac4579adc84fb2e0b7c097 GIT binary patch literal 219 zcmdPqqu5N3#8LFFwDo80`A a(wtPgB37V7Ku#?NF+MRfGBOr116csE@jI^o literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/admin.cpython-314.pyc b/backend/accounts/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caad718ad79db6b2d18571326559ff8a91019b4e GIT binary patch literal 940 zcmZuwO-~dt7;fi_nFW_6h=Lc!?7^%bJBgrPjES%sHc=!a9+##&(}n8n47MF3p4P~P9=QTFk#)>ISg<)v6trc6?^I!&C1)+5rgm%YVG|PP9bl_TbBzhj# zrq1yQb?lC_Xq0Rogiaka#(rqpfYQcspQZ|UFF0Wjhs4q(8s+O~4asS!zyNfBfFTgZ zhAqsFB~*d`nF(QNgmwTIj0JdeD4>HFYbqPhpm0M|c%h{Y$;suMi>&|G#=G%#lHyeO z?X;Ikr$ ziQsY~p{im``F4VLc>9|x^0%KIc0 z{2ckWe1vF91wz4N(q%bjJHd0jiJ8BgWO?i_VNPPdh51%D!z>P-t}idOf-a6;ku(l) z6lJ*pQ#nf;LX&yhEbdxx z#ts?rn2~oBkRNjjVvjnpPb-a<*&L>6Sr?=mr4M(qw^+=|L6!{WWhQbx7|GeNrgC43 zJTqNv>0jAFf7NsW3Y-f7vj$-Kb?)$WfcG`#?PLpJDwU>O7NRKacsmnQtaAdivUcWl z14lMJ08L0T1td~3sYz%6wQ7wloGU}oYb}(HDpaUXgqmYCDbso`tbzY1d=vu6LJ~l< zFA`OwI*nf93{^PJ%Q_23s6`e|P#-5Hsw{f(YCM`m38wEvkwr_35muERRHw@Q=_&;l z=nb1bvCG>}z6Cp%>%ET?wY8id^nJ759?&f-_qk@>d1#0?EHd4_=2}nHwr$NK$9F!z`O9p%+WH5*MxQMJ literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/models.cpython-314.pyc b/backend/accounts/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..741847b16a1eee7048d16c2f02cace05601221c3 GIT binary patch literal 1433 zcmZ8g&u<$=6rQ!$UjP0<;zVhi)D{xLAZ!kmdWld|JE<+&BwDAHD&Y*{-HE;Jde_X% zMx;_D-vDvo+9TKA$_4&~9-~sJWuyopPTZmf0Y~0=y|`jln*H9JH#6V+-kT2#`3!<*+c2(K2ZoH!leTL)RL?7 zzlQn-tdtg>2$?Z64xfH~E{#Y-+;X(K_}9?ka$h$!GjTmiI@{0H(6xDp$oC6xSw%Cs zibTmYMEM>P@0cl&CnNQl9tIbIO+6tK0SLnn>bgw(9tKgUpfbm(O9ZvCN5)j@Uf{Yi zF>s0ZupXD0G5Hd^)EkPCG^~;DSd>e{rbi@jg{3X%B87oy>f#cBDY}PL#s5Eml(q}< zrxX!&A7GtR_lT?Q@5$q)BFvi;!U#X6Y)qPM-wMV+o-a`KqbkOZ=Ln3OJ9NMT!cLk! zG9;|pa{a)r-Xxsb)i&Xyflrv-ymQdH*=`Pq^^kgYlUSA?r~|{kXiQF|fid(C#xkew zZ_KREpVwy2;(mT;wr7dDq?)(FfLrjqn@vp&Ar>(xYgO~wzhjrwWQ4K_>4M3 z?^Alhk~1EytzK)Zy)Bbs_IzZzH6m=sp{^};=DU>XvoL?4C~{^fs}RQG$hoSV6(uYY zj)jR3%ozlNav)L0*m4Qywqps4x-Q1h7WucFuub*H4YmR^OGlGp$M3`R&sR{QH?ib$ za(er`gbK^A@*6Mm8&8g(t^Bm{!^ZRB>1Usxj@*~|@pR{Pw)`r4`9=2f_w3o`^R<`R z-n9K&VeQ-T^wuwx_kXT@Fx~xI*AnaJ2qv}`-L(s^>5~0`ctgKo`JQ6mAb}V)!X{$s zjfOg;=TY_`)N`%`k0NK9mO^>J85T5DElf>4pzvaqu1sx)#r%Y+r>}J2>oA?iHBI{! a-8e&6&QR?Pef+0h($>G;{0qTvw*6mjG>20F literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/permissions.cpython-314.pyc b/backend/accounts/__pycache__/permissions.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46eb3a8206295c77da9d7c66a71916ed103ca610 GIT binary patch literal 4206 zcmds4-A^3X6~D7H`@!zQVlNABjN_pJ$~wU2gGzSnTE^kSNCY+ubt;==G&?&sQx7}j z-n&M)R^+!PPsUGnpXx}FB9ilb7vOz3vgaDTAjHc zzk6ryIluXxv#)lvM+scFzIKT1ZvPj) zc3N^{t0rBs7_*!rGY*bLh~#&6UW0}XUU{EzDMxtV1DVU+!~q|11wF%qIT>syr||G~ z!XvpL>>qODGi=c;>P7xrX3~33blkR`HKyu@VKSyVMSET4j;fdVvRUMop>wmKmH^7s zwPi=mo3>NDEf6Xf+l;~v%u&rED^U|R$p79n_=s)ZHEkcETFkL^4ksCn8h%*BAuw#v zkL-K~jR$0@dNguf$_50Y&Rmp!<@XD}CZv~~Jp|W^4mE3gj$9k6d!*=$`R@hpljhQj ztK=QWb`^Ar+i5Ya&R1^8bIYdeD$KNRxnXL4STY%RmAjU?#_;@ugM;oLGU$JnS=Fh| z?n12f%!Ff0}(prwwenr0O(u4$PmGfzu8UC%7&w{@z%Z9AocdR1o-p9!5U=N+9E zGVd(DeRU#}*Nr=7v5u$%g&a{Iu_jD~MM(Zad@WKrI4FUQgk=&Sm zJo{+2(sg62>&DafK41EDsWNc2JaE+;`Fic!``_HJOy4L^-}p=7=2qn94tmhQKvyqp zi=eMUqb}%jj*R(X-w$*@2J5=qiVi@M+I{`~TOrS1BjgqME+BvbifXf%mfbKMQ|FwD z0g9*GRA?M-v>i*~!D23kfNM7r~Up9HL2@`|m>{T$}$k@q^H=jf* zvA*wPecQ?I$8SA)>uJyDy`T10`o_xeOO9>D$DSpU^d#Un$y~b4jc8g?Uokb!jcVG8 zQz+Tkj%(V7CEfOC5}J0)qKw;C(JVSJ9oDphV`v)fgncM59}b{tjKkMxXn1-nU*aYM znY{$&e}9C7{W~BBBmap=!6j)g zBnP{m6ENZs0&DG#wiF4GESAuG2QeDCh<%i0SK+7hPnF+wFU~K@YlMj?IA)daxZ& zKAinzc8>(wr=;h?`a_j?x*SjM$p;gE_pp`tnR5KhR^MbfKDiTCI-}2t(iZhd@(b8^ zZZkrWQ1mpKUNmRWyo^Th_$;5RB}Ff_ChTWI<9skBVdee^XnsE#xRrd|v^LFunQyr4^y_+iM{0GO)y z#VfIa?_&cfewQCz_EJeX`TAD;^=AoRF$5XXN00&aj2=#gQJhADL}HC(kb@~yr<6wq zguNgc`i~()8>Rx?SOy-Ht&$iSeiV3?(X5Pd>H2v5ZMuUl3480(W zGRLs!#0*>Dr2>?LIa4ppLs@=rHR_Kr^|r}XP>jNesMmdV4NXrj4y)HD$0vr>`RwA% z{MU)IyF@t>wTyhhcGch#SS%qfi-@9U3COhlmRA#rFF$RR%Swo{sxT) zSbD>~@!GH3-V=3OOH8W@ZGYXxBi1R6Pa`Bp&i{^(0;C405~Xa5Y`KKmC9bUapIPmB zcg&<-Wta;vQwbo1>(jR#-}E=cP1CF%}*>GMqITi zhz$fDgRR&!mG3lBdd9ap$Dbv8>V>m4bD>fp6io)7T%(Clx9vrOsEJ@aHY02< z9dSF5CE6_t)#sW+?|A3$z<(au{r{+5K`1^4A-m!W5f4KZ_x2O@bL5g&yI!VOU;=AE zmIw1(mL%y%GPtY6q^^yNmE=e{IkHQjv-?t;6#e*4rDLevF|Gluy0!UjiNR4*v(f>Pvk9 literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/serializers.cpython-314.pyc b/backend/accounts/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..88f48f4c463a88ddf7700bd7154f7aefc5fb283d GIT binary patch literal 3295 zcma(TTWlLu_KxRi=V3dJW1rf18t178zh$g`xm2{YRChfEnJKVW$ zw=5s|VuQ5O!v4_RA0objv|9C(Px#`uZi*bPsKg3>_zOy)@W(lKY{v-(_DXZ++;h)8 z=iGZ<_sRag7=bbPpF5tV5%LXAT0?CjTmJ=QnOq^JG)X2TCXFB-nT$-xOcuC2sZ6L$ z6}U2~P3TM?A#?H}GS(Eqj5Ik!Of^GHZB8GPLo$mBst#0RPMRKUGI#vioIQdw3Y@V& z;OsSfj*xuscPJ$<@t(k?p6z>Yxio-j|5cY+HK19GRmb&#l9+WXc7@Hf*?=}YTfN~{ zUY=*RSDCgwy0cjBFpe~jLAJ($Y7<*pCUb;ICSj2)29t+KRhm;uC9m!X;w zrj;b7m&mgcGr(M#kDRLoth)HyiX$DG(KDdj*!l>NaZvkudvQiCkvCwyB#lWSaVhdT z@{3Ud<4mb_tEGrZMuEO-Jz~nO^`p&QPKJ@Li zLaV;Z^?=nJx5Bu_#HCO1Xoy&z159dATXHFnqK_DQfa`$Tj8WPPqId+j6fT$jfGg-j zu7+Gu%PRYJ5Li~A0N?0ncA5XR_zG|Y#U-2C!A+Mg+QswLa%~Z`2Rm5yMAovripMOg zc)^{gHJjcp&e&INnmy-NYfkn>J8+%sc{{i^U$v=Iyfl06#q-5^yA0MjMY~+C)=(g* zY6V$g={Aj{wfR5rXrX<~D6j`p0Df8~-zL)U>-EH;)!cgG=!&|nk>TvBvbwP5t?zq! zMO|5Vx9_XsnY-+h*FI&9;!F37FWs5_@bbSde|Y8JSMHf5;J8sY_J188TbxJrXQu7eS*Qw8>e`M~t@yWhX>BFu)-h1sp>+tgGPM92UY0<% zRDg_!%r{I~q(o}BhQcKXb(#bjbQr)?-VhURV5Fmuxauz2p3h^#O~Ro(3SrL#O0kZI zX%h7U%PuqTrkjt_{YXB5056q327oK5nX94#a&5tLeJ9|F_URxFkU~dr1KJW^H~NVy z!b&U*I1R95ze6?>gDWaeF05!92eWt1JTUf!J5IrB`!HP$p;(uM%~Y^r4D@pX-^`+vqEHbbH)i=FCZu)=;jRJr8BVlID7%t z1HQmXDoG_-_tty-&#HMu|Ep85IM0yL~`D1ps zG8uNr|Hlrcb4OR1(5ZK1cID7ay~SY~CDN@OJ_ANNaAVLyyRMr?r>)(>5gKaLjGb+s zdMP5BYEOr&2|Wu+_V}6N&2yc)Z`Ry^P385S4PDvMpLva99B{olf88y!0J;Fw1qfZN zLR^NH#r1Nv!rZ?wk+|@3Q3BUr;Qcq@k>voVV*#|ONLO$>c4JZExfV*|!(AdcT*4#> z{s~~2d=*dqkuGiwWE%sy`ao{=)wTKcfm3aI=1+}GzMjdi z9seYE}MpeSZA2;}1?x zy*Kv3`1|9l(JxX@G*YMTr%v6QeQh&N634cPk%+A*n*#)@TTd9ffkDj|` z(+gNW!y6U}^8&1d>68U5FHvz$Y(&lG_Y^W9z&agZp8h17UwnTfp1U8<-BljMPcKhA zQX*cM<+o9-bHW`z@{uo(nB)(kQ5%0lMl(o|LN&EOdivxxfys6vDGlB^&=?-850CvwU?Q&N7yoL`ivR!s literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/tests.cpython-314.pyc b/backend/accounts/__pycache__/tests.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcf49ff60ae5a53dee55b01021b810de38e744ff GIT binary patch literal 4142 zcmbtXO>7&-6&{jHa``Wr7DZZiNky(5RyJunQq)FI*m+hWu++OELP-7rcH6# z*`ewfPM}jO14V7OKrNpvpX{JLI6#2!IGfB%3d{<5pGdY0nX)lk9C>@jKILEz z&O0)~l#@9*FJxR(Zsz8^Gb2uUn1}POjCab%d~tHcGe}07rkTHw3=+xRPbBe*ZNy=U zSwQj(k)-z@*i70CaYohIq@t_9yQ#XYQqFoNvYE+ZK`k+myL6_oiVj?GQDt&PSE;;*l69GNC(Xi*>@E^4 zOtO#JO*z=IZi&nVtiFUNxLDGlqJ>h=#}}rwxkqFMnHjsl&QK{ z(3wh&UZ!1FOR_f4Kq;4%0yX^nI?GgBC=^vg#1=OqqG49Rr<9o&CONuSell_gk>-sTcV6S7<=6__liFR1ghqRRHYzC2Qm3~xl<z6k6kE{xtV(fV=Q5_uFh>e0K_gqKb+^lXJ-TMp7L z;fSF(*0fQ!L7!G=bd0oRTcOaF&LJr9k_}p1yB=u|&#l{@?Ad+RsCF$m+v}EG5cFkG@=}hKDx7msiDx=&y^>niyS+)?-I&v7>)puBR{5(ib+x zx1Wc?cW&LjRS(5$q4<-1)lhsR1VIBs?u2fK)`WU=xE39*9yz@cJ+moJv`y`O7<&*~ z&wM4OXcCsPBQOyCd0_k=D7;3{G)uPWbXFVdZ0jv)OU?J@6#?NGYX(o*CJE5fDcRC? z?uM;brEMvTxUs8%-BxI{RkL<{Yg(b^fL@`t*5YOz*;cq%GD7BBfs@_amRelfWx-~b zP?1Ei8nt@2>+c=s`QC9|?MmX2j?gL#u&Qn+hix?Kh4 z!vNYtLbTo?8k5X!dZBnt%ji4+qSN%QI7#q|7fSW-^zi+tv(wAwQk$?Z8lxi-E>4S1Kq)onnSfhmf2Cl5}wxrf&joc+E@*$Iwv} z_{|N;JO?y-tnA2wY#pZtu!dsdb%aB*X+;moY^uYD#n4GVWzgxh2xE4Cx z=!-Y{UT^g7YnXVx?IAs{za-8c$UL@tNdMsakDgqszV&XkZ+g|+>|X7ZC_oC*TneKpFF#@^~O6@F}^AOv@sBWIR0S#;pqpbpN!Op$7{pm z8v`e+Vzl*2RAb3{Y_t{|{dBoLmZ^7(?rK}JX5{r7LcN4~KKo!)N}E@%2* zJyB=YwoQ=o{qf$A)fm3~HKnwurO+iQJhzb2G$oMf>*kL@7(AKD3lz2lKP1=9zWi?@ zxKm>WS-5E+r>NG!;XYHsp{RV4#51L@1RC1XGYYq}C<5CT<-&x;wXO zCd5L=uKWpX9r-h&BvOQ>ih+r(s1h@GPO3^>a>L#G-h1D7-#yE#Gt*OmuYc&9{Wt=^ zZvn1WSmEF@F9Glw>;hBT1SN?{iz1F}MoLi@<*{T&H|3JTl#ZMccV@yABqL{BVbn6l9j!gO76u8Q-)W6uM-_;|ARcBNZsl!85A^yzavNUGCk`dz z+XfH#dzrOIxQio?99YzDwH>^F#Bzt+Iwi7bQbw`DW1@;JCN@o!Vu)Iz8t7b;&yr8e zrQg9NuYrI;F$W6Uh#-21NejWESMyugW%N2qKT2Dc?b^(;;A>nZJ|f5P18N|WE;ycF zPj4WK>**q*&8mk;9lqHvY!qP?)%dLH5Y=j)F9gM-a_!?lTW@)F-@z}*9d3gEA%K46 z+eK88q;qip96bL8cKTrFuL_j-N$S)Y&ad?5SNikWvsVMT@Ke@K?skpr(8%|UeBXH5 zeNi6B?|Hf#U+AV*hpDw*YOSAo^5fk=-V)vSPOV`w+e>Eq$=uoQKrV(7r|Uywxo0f* Ujm()okk>DCbvE)vCrR$*KWc9PQ~&?~ literal 0 HcmV?d00001 diff --git a/backend/accounts/__pycache__/views.cpython-314.pyc b/backend/accounts/__pycache__/views.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..156d6239724b1153edc35bfe59e2df472a657c81 GIT binary patch literal 2708 zcmaJ@&2Jk;6rZ)%_WC2ZvI*-E}E)qhyMhi6SwR^~7{MS{6g$Y6@1hx>9?+3-0^b zN9rLm4af{2jXp?M-A5h^ky$|I0D1a3hwJe#j@VreA^_*orYT91cqFdII%ee4dpZbAiF(i!~hVyqXV>zD9 zxg51DE^OgCmMsKx7n_1|Ko0?W7Y{6(6}&XBtl7-w>kw7DG8Z__Rq6|Vpc(HNmgV}c zu&m0*bdfb}wpqDpFWbzR^@3)@_{iq8Va(ZlWihZ>qw?vk*^lNbi?##VXjE*+37Y6I z%v5=8ljUIVP-viV6km{?;^pV+2pj2{4uv-ct2lO7K=g$-i{Y(>PbReQiyGc>VC<7U zRo6tio~exu0AN~HL=KWQ)J;~d#}mv?7`KH|p5+0^bQHrVj)9OW22!Si#Rt?8T*htB zliH&uWt+fFhF4_AQK$&kW;`$0nDIB|@dGMHIis8v5QpcFf-#C(u_v;i@Dd1!m|pni z$yZOd^~sJtxus9FsyoF}TOHY%II}%b?o5>1b9$Sg1Jt3gXuD-lTI3F? z;p)Js^)lVJ4z87jQcR{8g@AG))h2B7Opz+5-V#T@XBkFGX7Hg$DUcHbXDJ`pkYgOP z(K?8s(b0{zW<j{ypr)s$0z<@M|2{z-9>a3}X9HcsW83~!g@ZNwLig|wg zNd{E|NN-?nxZ(N+1jTTgj6rA&L4Dg7z5OE|MU+~vL++yp(k$6G?W#3=kORYnetM-#zZyQXfnmnX_;lBf zY`+DROhy-zX_s5JBiwZ=Ght;8Yq(596&9gdO0pB+kBx(nNq8U7d)O4N;A@atz#7n= z7B|@`nD$dThqUe05jox;^^nm$lY!q|0?{Hr_v*sQj(+m{(?48zc47Pcwa)o#KaRBZ zlUw@2wtlyx-)(>1XfHk5(pg($2eki>@|Lo!M&Lm2I81+b4t5MFFNfFcW!Hyh6ZjG1 z8P?mVn7xG>7Oj4&U%*8aSep1n5Fyq7h{Y4yD~+5u`)|hY6kmfyP z;dQeVTC%|GZhz%bMGu34-6u+J4pBq1j6xxpsm+Hy11~2>P3QqGU8lF#8hti=uriA; zr-u$>a=7$M?fvLqz;*Tk;IUTl28dU2MNwXos~vLn1-br$lwXp29dhrN)aX{K*i!dY zMY+;8F76Td-Al%mcb~quT_|-5r9A?Zy-Y%xdb+YbHr*MU-Xk#C%V!j>An07DaPCin IvoN~<07$29#{d8T literal 0 HcmV?d00001 diff --git a/backend/accounts/admin.py b/backend/accounts/admin.py new file mode 100644 index 0000000..a3a6b87 --- /dev/null +++ b/backend/accounts/admin.py @@ -0,0 +1,12 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User + +class CustomUserAdmin(UserAdmin): + model = User + fieldsets = UserAdmin.fieldsets + ( + ('Tenant Info', {'fields': ('tenant', 'role')}), + ) + list_display = ['username', 'email', 'first_name', 'last_name', 'role', 'tenant', 'is_staff'] + +admin.site.register(User, CustomUserAdmin) diff --git a/backend/accounts/apps.py b/backend/accounts/apps.py new file mode 100644 index 0000000..0cb51e6 --- /dev/null +++ b/backend/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "accounts" diff --git a/backend/accounts/migrations/0001_initial.py b/backend/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..54cce46 --- /dev/null +++ b/backend/accounts/migrations/0001_initial.py @@ -0,0 +1,154 @@ +# Generated by Django 6.0.2 on 2026-02-20 18:33 + +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0012_alter_user_first_name_max_length"), + ("tenants", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={ + "unique": "A user with that username already exists." + }, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name="username", + ), + ), + ( + "first_name", + models.CharField( + blank=True, max_length=150, verbose_name="first name" + ), + ), + ( + "last_name", + models.CharField( + blank=True, max_length=150, verbose_name="last name" + ), + ), + ( + "email", + models.EmailField( + blank=True, max_length=254, verbose_name="email address" + ), + ), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", + verbose_name="active", + ), + ), + ( + "date_joined", + models.DateTimeField( + default=django.utils.timezone.now, verbose_name="date joined" + ), + ), + ( + "role", + models.CharField( + choices=[ + ("super_admin", "Super Admin"), + ("institution_admin", "Institution Admin"), + ("teacher", "Teacher"), + ("student", "Student"), + ("project_manager", "Project Manager"), + ], + default="student", + max_length=20, + ), + ), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "tenant", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="users", + to="tenants.tenant", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + ), + ] diff --git a/backend/accounts/migrations/0002_alter_user_managers.py b/backend/accounts/migrations/0002_alter_user_managers.py new file mode 100644 index 0000000..a579cb0 --- /dev/null +++ b/backend/accounts/migrations/0002_alter_user_managers.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.28 on 2026-02-20 18:37 + +import django.contrib.auth.models +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0001_initial"), + ] + + operations = [ + migrations.AlterModelManagers( + name="user", + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/backend/accounts/migrations/__init__.py b/backend/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/accounts/migrations/__pycache__/0001_initial.cpython-314.pyc b/backend/accounts/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..384a96f4aa121b9270c016b03bf0fe93c5e32e3c GIT binary patch literal 4042 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 Zjh~=&X3i+)huomB)-SC61;Q9#@DEGl^=<$F literal 0 HcmV?d00001 diff --git a/backend/accounts/migrations/__pycache__/__init__.cpython-314.pyc b/backend/accounts/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d220d3938886cbe998e1306dd7b631559b8228b GIT binary patch literal 230 zcmdPqquFgG*3D6u3nKd)FH lW}aR_PO4oIE6^n%w-$pKpO_gL8H<>KECBd*JQ@H1 literal 0 HcmV?d00001 diff --git a/backend/analytics/__pycache__/admin.cpython-314.pyc b/backend/analytics/__pycache__/admin.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..853807fe6b27fe3b9441a8cf84ce7fa3aa4c9f7f GIT binary patch literal 264 zcmdPqPO4oI Z2hbUeKwJ!Bd|+l|WW3LyP{ao0003jjNWK67 literal 0 HcmV?d00001 diff --git a/backend/analytics/__pycache__/apps.cpython-314.pyc b/backend/analytics/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01265664ce4ce20a6934eea1d53936447af8b2db GIT binary patch literal 796 zcmZ8fO^XvT7*6J+wp+W)itVz3I7JYPICJsfLBxKr=)paVAGh6*&ZIS(&WuS$sHfh% zdh{>&55)f}j`ya>$~+*wKFM?jc=^J$>Oh()HMf*fq1$�Jdo~SDH+pFnInoaXC z9fz|#WkQ7mKJAw}Kj2KHra?FSWRuwDci@Av>&tBbo7M^(V@TpiYdwSxcF&zd#itNR z^&I0VCML256lHlg0#ioM6q70{sHEx*lQSBP@}!)xOsl^rxE&C}Gp-4V4%k?hRL-L( zbV_9~6nU8j1FBdWjHr4s&Z$hJ$4`fYQ8cE>E0(3vc_|Ss3KbS}<4Z=umpfULQf4-CnTdYMu^!W1SVDri!T!Lx};)l*&$@YrP6|DEX%=sDPiQcDEJL k#y9nQRlE1)KBzXax(wpX#TcKW?vI*}yYKl=1lP*z4^MX0CIA2c literal 0 HcmV?d00001 diff --git a/backend/analytics/__pycache__/models.cpython-314.pyc b/backend/analytics/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..04189347e8e8803928d4d8e8cec3ecd05a719947 GIT binary patch literal 3657 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 literal 0 HcmV?d00001 diff --git a/backend/analytics/__pycache__/urls.cpython-314.pyc b/backend/analytics/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..15d9ace9f973192b67953f916b0f587b2dea3243 GIT binary patch literal 561 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 literal 0 HcmV?d00001 diff --git a/backend/analytics/admin.py b/backend/analytics/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/backend/analytics/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/analytics/apps.py b/backend/analytics/apps.py new file mode 100644 index 0000000..5a013ae --- /dev/null +++ b/backend/analytics/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class AnalyticsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "analytics" + + def ready(self): + import analytics.signals diff --git a/backend/analytics/migrations/0001_initial.py b/backend/analytics/migrations/0001_initial.py new file mode 100644 index 0000000..9d6d24a --- /dev/null +++ b/backend/analytics/migrations/0001_initial.py @@ -0,0 +1,109 @@ +# Generated by Django 4.2.28 on 2026-02-20 19:49 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("tenants", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="AuditLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("action", models.CharField(max_length=50)), + ("model_name", models.CharField(max_length=100)), + ("object_id", models.CharField(max_length=255)), + ("changes", models.JSONField(blank=True, default=dict)), + ("ip_address", models.GenericIPAddressField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="audit_logs", + to="tenants.tenant", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="audit_logs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created_at"], + "indexes": [ + models.Index( + fields=["tenant", "created_at"], + name="analytics_a_tenant__9c95af_idx", + ) + ], + }, + ), + migrations.CreateModel( + name="ActivityLog", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("action", models.CharField(max_length=100)), + ("target_type", models.CharField(max_length=100)), + ("target_id", models.CharField(max_length=255)), + ("metadata", models.JSONField(blank=True, default=dict)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "tenant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="activity_logs", + to="tenants.tenant", + ), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="activity_logs", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created_at"], + "indexes": [ + models.Index( + fields=["tenant", "created_at"], + name="analytics_a_tenant__75e066_idx", + ) + ], + }, + ), + ] diff --git a/backend/analytics/migrations/__init__.py b/backend/analytics/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/analytics/migrations/__pycache__/0001_initial.cpython-314.pyc b/backend/analytics/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..37d108255f46ae3cdd4be3a7d96e05c3a4ac0c39 GIT binary patch literal 3447 zcmd57%Q6yEjk+Hn&*b@~%0w&Nr;riq<4rBO+tb?xQ{(l*+poJdDw@5J5K-Ziu1 z()82=LL3nna)c9b>#^e0BL@zSiDYXyP>B<_NDW-!jn`=s;z;F3f|YE)dGpQ8d*6KX zc6_$`m|ueD*|!_|uicXLvuNx-?zZt_1&qhi9Z8iJq`*oOdx-tLn;^AO=PhufN>)!pWRr*I08NF| zo|HrFS0lSzr{uIl9oWt4NG%TjYg#<0u_#2^S+fUmu_JK@wHOE7t7-B7sIZ~`RAED} zpfKeX6_z+kVNmf-tHZ2c9f4Z>8u#7UExapS$y7C+Et*B{(6w3aSjZ(^Jbadl)gW0fVu~29?aB^{CtSStEu4sKoAXK5IZF$k(?cwgPGh_h0KyJ zdJ@kgbwU=Wm`hKA(TSJU4#d&>BS64a8{7Uec8L1w#|2zZ{4&_ zm!=mO@0%kA>`Y-{DPwEK?mU)*&0W#U#BTa$O-l5l@h%A1qum4PncSh}xzbVtsFr)P zndNLIcbzNC*H_TZ+l!0by&!g%`^^g3y`}UaWHGq06hMQF>iH@o7D80Kw_dH z-MopZmcFr)y_QSoap6Ajntmy^^xg$3y?;Yfm38Wca9Upq7}oc29r##!-rf6nZrdaI zjyIGub>+<0N@MKm=GfIAluT0@-gY~~p2v&ZfmVOCu0)%m2%!5;H^cpn@R@q}Omj5Z z92sjSgt6mu_np{wE1}-)Ua7Z#<6cPm>l zwH91%JE!WgDZs@9?s<5i71?M#Jlaf*eUSu6t{UxRPJr3EX%)oBzfp3Y3xTSzT+XX?D^E+2##z9M}Ct;t2N6X Dv~mr2 literal 0 HcmV?d00001 diff --git a/backend/analytics/migrations/__pycache__/__init__.cpython-314.pyc b/backend/analytics/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d9101b42c3092ae71623dac32ef87ff0c905df7 GIT binary patch literal 231 zcmdPqC%vi7iqA| z;_!>pGUkJTw!I*Ya$^nlnqnLd8@~w!9brf`Otc1TwSHwQifr)e+8K1J1qAnCumY7d zc~_a*yMHGSq}F9!dac!|$*=?L)g{~l%7tyZLe9aNLMGtuvIekF%d5paQ5od-rY=<8 npi%1whiyMhkE*8VtU1Q9{#e1yh`t5q{GIK6MB7gjoYLe6+E!0h literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/celery.cpython-314.pyc b/backend/core/__pycache__/celery.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81f3ea8cb59dbd76389b3116b794bdb2974e67e3 GIT binary patch literal 940 zcmZWm&rcIU6n?wiZRuK2h?MGu8Y9vey2doo5Mv^3#TqEhQZy#wX1Y7A3+-;5nI(Z! zi3d39#UD4X{s&(CA4(*Wj4{#Z#T(jxz}fzwB)-YK`DWg{@6CMg+SsTD!rpy)>%5i$ zeg>O?seRDl0<_>M7*YXPu*wDLzRculScg)0aDBLzqC-4DM#xYyqC#LIPrd|(nvr^Y zrmm}{(H=hSjj$0hMs5zX)Y940@gC0fj|8h)8vj3TM8(hsEXJ8B7-X=hAzg-4?01k< zOi4VP#dS=!c|7;vZgFwNT+Nruh2r9>xxA8FTgvZ@TOPq_iWzg<8co}Hi>nQ0RqUPf z_6BlmUfS~9>ipn>;L9)NjmLsH>Y@gwO=RJ6O6E$%acw^8)Lg-55={L%v-)CmKcEO7 z60>tlxE4G)i5c(^N_|1PH~Vg0j=>FiAn3GcmH3#v6E*O2A5%7;EQ+#flQ=Hp5z;#p z&r&F^S82dL!->8|F`;@B5k$8zX&^o4S$+e%j1Ez9E@_&M>oC*Q@8JsZ5!u#PQ4Nt~ zw(j|Masg3nCv%8Ct9Xdm`f@qDkkc#3+QhD{2LkF=SLJkbn`<_%_%)Lux=Di8PKZh4 z!nNR+65TJnwBOWU)ek1Gw6&Qp%GHjDKuY7XM|rr%LIRn11iM=f@m#J6?QL8|LQoz; z%_g4+I^xvKD)AboSJ}W8;}ghdp6yV}6W{C>r(8LC!v`Se1Sc5_h(~TWJln71JLIC+ z20~F!xZ{u{NnauQ4K5sqK??0@yV?;<9>8Qfo^D^y9zyO%IJW=jh1Uul$3Rk!AbtSx w_SD?RL>uCVun?s6PY)sST~hWEyNP}FKGRBkmS+FRk~G^1ft>mqpzeeI0n)PGegFUf literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/settings.cpython-314.pyc b/backend/core/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a131c09ffd90c6ef873e9dea8eb21430e17cd10 GIT binary patch literal 2600 zcmb7GTTdHD6yCM*{em$N$W7gdHi;qZDuG;7r74TOfE8oAvnI5WlGWNf;3ex_XJ-s$ zpZwkz9`g^X{u|P&O0-Xr`qn2WQD3W`^#xl%RcgU_X3ja^cjlZqXS|S1L>c(}`STn7 zD9$i{c#FZOD}lx5zhP)Ih@s3&2C;c|!$*BzFfV*Jm>2$Kwx?c>49te#%l&D2fHV{_ z=k3uT^3f3T(=gA_2+yD^C6-3v`zn0L&=`%Q01BcI?1YgYMUW@ER$>KK7~@%*Krxy` zag;zwH2xxpQg>oQLcu4nbR3PN34w(%-5n>pE@P=>)n&)4)Fo|5Ip&PNP|R z30(}X4tjHgt-Y+_9p#DLGLP^>>_&zP3kb z>s4ND(K_E$2|2KAO@Wx}T9=34BG?wGr_-WX)*HH^+B!Xa*3+F9$ z?$Y|u{@aV%XRSuVGRu7*hX5-V1t_)}*w$-j5dzp;Y!TPLRmnObC=IID1m}7m7J}BW zG;9!lRo@j`)LPTAp%pVhXX-SuS6a^G>0OAdbM@3_H_s-ehfPeJnNvannTJ|CD`XG-8!G zO9JH%3F3Qs`VUoN@99syGw9AY+xBHeSHGj5pc@0-vyUzRtG;RDEdK`5Y z&O81&$X-~SrBli$M?S}3Aj{0SAw_{!ttf7&30=|9tF{l`CO51o-u?0RZA>gF?M-&0 zoB3R>AZ?3Ca^t8}swi8ikS!J0+(<>*EEmLzB)h3?c|Gsx7xG!r>x6D3Csss{R(5B_ zt;&W{7G-(6gmTI=v5*I02^wgkAQsoR#C1u@mU0pR<~Jqfd8sJ5;Vp1DfA>+*4SKW6 zbMU+Z$XSowP3NRFajQ^K0J)^B<)uRI7v{6~K)Y1%`W+GAQzg%HDR-oUCjyoXF>29`pE;u zHxJ$LW9Tp~13$690w=tmF(d_J$Ir6tM_-g3`)ihAFMkX&@zhcHrzh`XH{Ql>{1%&g z7n^?@n|~L(`!;sB9quHVBx2jqPLPQt+JO(LbUX1W8cO@x$xrEU(g%_$CUvzP?}V91 z`gk(;KAb%Ay$`34U^ekS&V6D6bIFcBG!^Yc;(=htpG^4Lkq*a9EgUl`fDa_!hf~LC z-ZKopdHj3oRwv+3fnuPmc!i0KAE%yo{5~)S4p@{&7vGI8P;W7vxaK=%W;-{TiP?7I iLwuqg`4Ag#hd;m(f}J>%oB_9^%%%IkGHH7bBJ&SkX;>8i literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/urls.cpython-314.pyc b/backend/core/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c2ff788ed3ab8478734e2a6865d69189712c93c9 GIT binary patch literal 1045 zcmZuv&rcIU6rOFTyDg>V_gX^?CB#Efx(P195F-&9Y!fw2t6ofPrn_Ui*zPtnvk-bR zo;cbQ_$T-uaPjW3VM&Oa7!IDiVfY7}*%pdP-(mK9-}l~|Z(jC6GBE~toP0mFHbMaW z7Rhxa2HDo$Px z8-9z^Lwt>Nez%CAFH(WLTh{rlBL35qK#PvlVu3a`Qi})L_(&~Lk&CnayKbM0Kp&)~ zWNBhO+)o0BlNDuj2d|u3ANVd!54*Aoilw~*$`eZb!Y zZPdbjFQ&u$I2Gk4ssTMl`@UCP>s4mlPnx^Pt~*)7u^F*yM6B134@lvhK+ceepyy2T zp-P8a$?oyDQzDB3MHE14G3-Op<|~L+h3?0B%6H2!vFSH(#p{6I*4|J|sMbaV(LGGK z|D0pEEzI{dMCp}uwQAWGt5&rgTq7av>Gx# zV%yXVhhR;3$+i#t=#!RXx=s9)%y9$a*PuuIxC~2@^b4df!2JvG;;#%MvBT7twWE#h z#?fYX(}PPtVf1jmTR(O@HS0l5q-yuh@q#y%@!&E~d$GG;*LzEkym{S&Yomz6d%e{1 ud201Ewd$qxlgDQ;7fkJ`56@NoRMk%!XX^7G%`^D!aw0kr`Wz=S+}l5~w+w#( literal 0 HcmV?d00001 diff --git a/backend/core/__pycache__/wsgi.cpython-314.pyc b/backend/core/__pycache__/wsgi.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f87b18310537c09168dbb4e1d3065ccda066fad GIT binary patch literal 696 zcmYjPO>Yx15Vey)s*M@}qP=21E)jHHP)-OT(QI3SXj&zyIKjr*oorlo?Un7Mq$fZ^ z9J%rn;NKKc329Ft#2u7BfH!HB`e47&e7rZ~xm#UzkUgKieC8i8LceokaZ2Z48d~rc zwUI%uP#ZU~!S*dQ3r~xtcmut}2P=z|<$F+B;st6K+ogJ8ZrQxzO;kU3nagc_8`UnI zoH;K#yG;^EG2jsyNM)e{k}7!wfpHzDX$ZVYr3OvR5Xd~5#WCy0K$s?sBr+Vuuo1%; zVlrlm&s>5f5RzVxrD@CqX1El+o~^fIDUzIm3(Jz!@5o>W7qu zGSF^##6%<)ngW?n5VY<-phJ0VBn=^rqp0sxN z{myo`+idN2{Jnke@cH)1)x2w0gE3r0mJr6Za#r?zOYi$xLF(+vT!cFd%X5{-FCmQQ@QW&iR4XzM-|#b@z0$afZBK)mz`IcivZ2zF7YO DaHY)w literal 0 HcmV?d00001 diff --git a/backend/core/asgi.py b/backend/core/asgi.py new file mode 100644 index 0000000..e9d70c5 --- /dev/null +++ b/backend/core/asgi.py @@ -0,0 +1,7 @@ +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.dev') + +application = get_asgi_application() diff --git a/backend/core/celery.py b/backend/core/celery.py new file mode 100644 index 0000000..ad749d2 --- /dev/null +++ b/backend/core/celery.py @@ -0,0 +1,20 @@ +import os +from celery import Celery + +# Set the default Django settings module for the 'celery' program. +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings.dev') + +app = Celery('mtcbd') + +# Using a string here means the worker doesn't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object('django.conf:settings', namespace='CELERY') + +# Load task modules from all registered Django apps. +app.autodiscover_tasks() + +@app.task(bind=True, ignore_result=True) +def debug_task(self): + print(f'Request: {self.request!r}') diff --git a/backend/core/settings/__init__.py b/backend/core/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/core/settings/__pycache__/__init__.cpython-314.pyc b/backend/core/settings/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0ec35023767c968028ab2a6de8b7702bfbe2cbe GIT binary patch literal 224 zcmdPq>P{wCAAftgHh(Vb_lhJP_LlF~@{~08Ct5!d>IJKx) zzaX(FvA8U?C^u2xB|o_|H#M)M7{OA|Rfvzz%*!l^kJoogO)4r)EUMHGPE1cMQgF`6 zFHKQ!N-R!IQE*8t&Pd8nEK1S$4RLmI(N9WD&Q8rs(NE4VO4Tn;Eh)*&OE1=knWR@x hd5gm)H$SB`C)KWq73dm}dy7GgPt1&rj77{q766H?JqZ8+ literal 0 HcmV?d00001 diff --git a/backend/core/settings/__pycache__/base.cpython-314.pyc b/backend/core/settings/__pycache__/base.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c793a51914dbad33aef7c3885185188cc9a56abd GIT binary patch literal 4406 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 GlKcaaWU2rF literal 0 HcmV?d00001 diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py new file mode 100644 index 0000000..29ecf5a --- /dev/null +++ b/backend/core/settings/base.py @@ -0,0 +1,158 @@ +import os +from pathlib import Path +from datetime import timedelta +from dotenv import load_dotenv + +# Load .env file from the project root (not backend dir) +BASE_DIR = Path(__file__).resolve().parent.parent.parent +PROJECT_DIR = BASE_DIR.parent +load_dotenv(PROJECT_DIR / ".env") + +SECRET_KEY = os.getenv("SECRET_KEY", "django-insecure-default-key-generate-yours") + +# Installed Apps +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + + # Third party + "rest_framework", + "corsheaders", + "rest_framework_simplejwt", + "django_extensions", + "drf_spectacular", + "django_filters", + + # Local Apps + "tenants", + "accounts", + "dashboard", + "projects", + "analytics", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", + # Custom Middleware + "tenants.middleware.TenantMiddleware", +] + +ROOT_URLCONF = "core.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "core.wsgi.application" +ASGI_APPLICATION = "core.asgi.application" + +# Database defaults +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +# Use Accounts app custom User model +AUTH_USER_MODEL = "accounts.User" + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +LANGUAGE_CODE = "en-us" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / "staticfiles" + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +# REST Framework settings +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": ( + "rest_framework_simplejwt.authentication.JWTAuthentication", + ), + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", + "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination", + "PAGE_SIZE": 10, + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_THROTTLE_CLASSES": [ + "rest_framework.throttling.AnonRateThrottle", + "rest_framework.throttling.UserRateThrottle" + ], + "DEFAULT_THROTTLE_RATES": { + "anon": "100/day", + "user": "1000/day" + } +} + +# Simple JWT Settings +SIMPLE_JWT = { + "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60), + "REFRESH_TOKEN_LIFETIME": timedelta(days=7), + "ROTATE_REFRESH_TOKENS": True, + "AUTH_HEADER_TYPES": ("Bearer",), +} + +# DRF Spectacular +SPECTACULAR_SETTINGS = { + "TITLE": "MTCBD API", + "DESCRIPTION": "Multi-Tenant Cloud Based Dashboard APIs", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, +} + +# Redis Caching +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": os.getenv("REDIS_URL", "redis://127.0.0.1:6379/1"), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + +# Celery Configuration +CELERY_BROKER_URL = os.getenv("REDIS_URL", "redis://127.0.0.1:6379/2") +CELERY_RESULT_BACKEND = os.getenv("REDIS_URL", "redis://127.0.0.1:6379/2") +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' diff --git a/backend/core/settings/dev.py b/backend/core/settings/dev.py new file mode 100644 index 0000000..bdd76f9 --- /dev/null +++ b/backend/core/settings/dev.py @@ -0,0 +1,56 @@ +from .base import * +import os + +DEBUG = True + +ALLOWED_HOSTS = ["*"] + +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_HEADERS = [ + "accept", + "accept-encoding", + "authorization", + "content-type", + "dnt", + "origin", + "user-agent", + "x-csrftoken", + "x-requested-with", + "x-tenant-id", # Allow custom tenant header +] +CORS_EXPOSE_HEADERS = ["content-type", "x-tenant-id"] + +# Ensure SQLite is used for early local dev if env variables are missing for mysql +_DB_NAME = os.getenv("DB_NAME", "") +if _DB_NAME: + DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.getenv("DB_NAME", "mtcbd_db"), + "USER": os.getenv("DB_USER", "root"), + "PASSWORD": os.getenv("DB_PASSWORD", ""), + "HOST": os.getenv("DB_HOST", "127.0.0.1"), + "PORT": os.getenv("DB_PORT", "3306"), + "OPTIONS": { + "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", + "charset": "utf8mb4", + }, + } + } +else: + print("WARNING: Using SQLite3 DB. For MySQL, configure DB_NAME in .env.") + +# Basic console logging for localdev +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "console": { + "class": "logging.StreamHandler", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, +} diff --git a/backend/core/settings/prod.py b/backend/core/settings/prod.py new file mode 100644 index 0000000..e989299 --- /dev/null +++ b/backend/core/settings/prod.py @@ -0,0 +1,79 @@ +from .base import * +import dj_database_url +import os + +DEBUG = False + +# Comma-separated list of allowed hosts +_allowed = os.getenv("ALLOWED_HOSTS", "") +ALLOWED_HOSTS = [h.strip() for h in _allowed.split(",") if h.strip()] + +# Strict CORS +_cors = os.getenv("CORS_ALLOWED_ORIGINS", "") +CORS_ALLOWED_ORIGINS = [h.strip() for h in _cors.split(",") if h.strip()] +CORS_ALLOW_ALL_ORIGINS = False + +# Production Database with Connection Pooling Settings +DATABASES = { + "default": { + "ENGINE": "django.db.backends.mysql", + "NAME": os.getenv("DB_NAME", "mtcbd_db"), + "USER": os.getenv("DB_USER", "root"), + "PASSWORD": os.getenv("DB_PASSWORD", ""), + "HOST": os.getenv("DB_HOST", "127.0.0.1"), + "PORT": os.getenv("DB_PORT", "3306"), + "OPTIONS": { + "init_command": "SET sql_mode='STRICT_TRANS_TABLES'", + "charset": "utf8mb4", + }, + "CONN_MAX_AGE": int(os.getenv("DB_CONN_MAX_AGE", "60")), + } +} + +# Production security settings +SECURE_SSL_REDIRECT = os.getenv("SECURE_SSL_REDIRECT", "True") == "True" +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True + +# Redis caching +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": os.getenv("REDIS_URL", "redis://127.0.0.1:6379/1"), + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + } + } +} + +# Production Logging +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": { + "format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}", + "style": "{", + }, + }, + "handlers": { + "console": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "verbose", + }, + }, + "root": { + "handlers": ["console"], + "level": "INFO", + }, + "loggers": { + "django": { + "handlers": ["console"], + "level": os.getenv("DJANGO_LOG_LEVEL", "INFO"), + "propagate": False, + }, + }, +} diff --git a/backend/core/urls.py b/backend/core/urls.py new file mode 100644 index 0000000..bca6ac2 --- /dev/null +++ b/backend/core/urls.py @@ -0,0 +1,13 @@ +from django.contrib import admin +from django.urls import path, include +from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView + +urlpatterns = [ + path('admin/', admin.site.urls), + path('api/auth/', include('accounts.urls')), + path('api/', include('projects.urls')), + path('api/dashboard/', include('dashboard.urls')), + path('api/analytics/', include('analytics.urls')), + path('api/schema/', SpectacularAPIView.as_view(), name='schema'), + path('api/docs/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), +] diff --git a/backend/core/wsgi.py b/backend/core/wsgi.py new file mode 100644 index 0000000..fb0eec0 --- /dev/null +++ b/backend/core/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for core project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "core.settings.dev") + +application = get_wsgi_application() diff --git a/backend/dashboard/__init__.py b/backend/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/dashboard/__pycache__/__init__.cpython-314.pyc b/backend/dashboard/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0209a7d96d010883e85911a8c59d4ac5084c5111 GIT binary patch literal 220 zcmdPqRPLC`fTYnT+PVYqQ~eckBmJQl*Ge zr;7Loh<`&FtwcqIM1f>P&2G*PQ_Q^4yf@ODd2)Mgh4}dO<+Hr>^+#yt(VBtjV-HTr zA<5_~@{$_*kZh1`l0{o2ix23txZ#;7OCFI<>t|>?)GhDUb-yY{ay)DZO{TME`oM!z zazG5th=~pp6KB*US!7ySY!;ebu025gKCe*mX4T1keuU+?>hiohskkU~wL~^ z6x=QIIsYr!?<~3{fD$JHz^wpyfh_{Q1@O^gF`KjjMp9{0$WoM*AFp#ULR%O>zt_*g z;v|sW27oqbD`s>AODtjwz+z R$@ZUxh;CiTzr>eo)jye^pdJ7K literal 0 HcmV?d00001 diff --git a/backend/dashboard/__pycache__/models.cpython-314.pyc b/backend/dashboard/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..812e65649ee732aafcfe57314db22304f061f1ad GIT binary patch literal 2075 zcma)7O>7fK6rQ!$|7!>5mmgzn{#1(+qbig_12n`5Nt=Xdlb~Lb>3DY>7S_9TcFdo8 z@To&$K|mQ4|%#g z9v+ME$UfAA@<lMK99JhA|6->^I$Ks(Y!qERlT{u zf9BDA7-mx2y~0u@HE?1x+vtC;j!k#Jy7qtUn1Gm_udd9ag@4rG38+B~o`k53ueZxQ z8fclk10r(mbG{BJ$ZBY7pEN`rd~H-%jleIuRcRMQzVX_~h#K44uV=ouEr&i2^*ErO z7zp@l`A${oLwuK-~+8wSkWpkrqXoMTKH4Ow$dOO=4YvAcfSpjV=2+ zCc3_jEpn^EjWJN#xRT~URqB_xJRh1CJH9mXs2|vUK`^6*5X45*Adjjt)E9Qs?e+Cf zQci9qj%tj0l@KntTQR7qJKv(ILeLF!kKe9bb0~8ohJztgFT~s^_UgvijbjW)qh?aj z6^uWw5mR&otPYGT26MP+Sk$t?5w)3485|uZXzPZ?EuG$gUB!bsZV$!|hsJA|aL$Z! zjZ+6>Cg{5%>U6Ic=O7KIc$F~XT%)W?iX~gCRjI|D7h3LU4r9YIIL5_MT4puEZWd?A zB4N2<)2`{cbHt%~u0)(mWt%X)I5|6fu2d`&?FzN@qQ2$#PkjH?o31Px#X2E}H2j^t zAU;KF-T8ZGZ%2PsI`2;0nP}urHK(g^DjF`^f4$I#sD7@nIMQSs!3 z(#V9Ler)&>VWWbD?4XE>&C(m(v)xFQIu2a2u5!+{O-d|3OeF3`=hO*5N(RPVag!w= z^{i2)zBB%U(h2p>q1`cxtA#`Md?TD1H1vX5V)Wox!o@zL7hUj|Np)&I~rejig?Ztn}}gB_X_ zhTj}P75`lx2cv`jeQ}1p3lpM~j_^MXN|LmWGV3U}j*hLP{4=>zI(_f$uLi#u{0o8V GZSgNu7UzHf literal 0 HcmV?d00001 diff --git a/backend/dashboard/__pycache__/serializers.cpython-314.pyc b/backend/dashboard/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8404bef3a6d40599696277da0bf1c4557919b8f1 GIT binary patch literal 954 zcmZuw%T5$Q6us3ihKD>f2#UxU6O++MZ&WZgF-oHvy=7BkDd;9Qe*E<|scxu~gRmKrxo)p?O;d+6Lb8Ry(s z)V+b1uv*pIRvHZM$KVruz`c#1VY5uUE3`1Lq;83Lsc5uYGPkD3s&}iLt6W*m;|H>? z+d}Wf8)94N>S9*3)9QjSGOaEN^P*k|oyKcV7Z;Y|x@hdkJdM+{^K*kSySwKC5>(S< z(L|by2nXdh}6rwK?bQ~s#kyjaXg;;b7 zB$*-M58$`whpX%-5SFgwMU5TS(pcWq*pSzS-qAGTF6!uPO3_EwNc7&!l?DDT>MHlj rM&8%gF#-O6U}g}$`;4(;xOEKGA3?xY+0paQ)AJvv=YIiNovZu-55(>I literal 0 HcmV?d00001 diff --git a/backend/dashboard/__pycache__/urls.cpython-314.pyc b/backend/dashboard/__pycache__/urls.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..69765ef4aef9a2c34079ec66de6219876cce1b5f GIT binary patch literal 746 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 MM#lf*z;SZ?17F59hyVZp literal 0 HcmV?d00001 diff --git a/backend/dashboard/migrations/__pycache__/__init__.cpython-314.pyc b/backend/dashboard/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..28d1b88d55ce0efe2aba5588fb85045a79a8b63a GIT binary patch literal 231 zcmdPqb?ZFe`=3&0wgedo`B( zO6J0LtCs$i9Q2oM7ogxX31C+M9KX&zz7FuN=6O5W0GKFkFjs{vN;lpVa>DBzfje0@ z^SY5I+a7>Hjw%C=s8w2{Gyr={*yV@SmFKhp~X5D*eh&LRv-6zepcJ-!f{maaj YOTR-(>36d8XDOiDAK&~Xu3T;X1Lw`3O#lD@ literal 0 HcmV?d00001 diff --git a/backend/projects/__pycache__/models.cpython-314.pyc b/backend/projects/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22605d7d84e28b1413975a6a34b8ae96855aa1db GIT binary patch literal 4255 zcmd57&-6&{kyB}Fbtk@~eQOO#}%C2UKnEC0nmjZ8|GY)Ph9lp7b27c1^sTASKc zW{0w67!6M?8UzU1OLNqr2k8{&l;jY&J=HyxRtZQLz(9+hdQ&5#DSBz&+a)Plirn

dl%{PxTZ{ECbcp@4J5opt2ePY}K?XTFWHGfsv`4E&6xl3fvI2rRWPY>$eaqpOq z`5fIhE{yq^-_eC}aV)?BJ*18F64~EDWO3G$ZLc}q{T}kUI+Gk^0Fc4|f{e&b{UjCo z8%|7lxVUI(dcg)QXzPp_<~-_RPB&GPf!5+`(|K!2*Ctg{o!4o8cL9V1s-2zRcnDEp zkF$h%WWv06MdoWK78xLN{HBKqv;M5qIMLO6$!q{dn)i>$UfFl9w!bF8wC>0BlDq$w zg>&%B*jN})ovz*}F@~~OM@0@?Ni^HtIM3Aqo!oB)j{!M&PH;z9{J{0K&I+Scj7X668Q+$Xi`kryT7d?2sH|hi?;hL~e$& zPw+sNTKDxl|nkH*jv$S+t%UblHb)9zJVr;F-f#>y8Lb(7hA5VNZvD&$Ishm7tkuGf24d=_rp^9{7*KoO0>5rJx=V48^^VC>khGkMLWjr=K zlN(cJrblFD@@8gaf(NHZa>~@q#0141(_REcGH~5<#-i?esi(Ngc&NCfIkiA#6t64~ zSaVouZ0CUJ57Ea7w8s@CUr=pZGx9927Yd33*CWzkggE0lyz86;4dO%;=+u4&;-5c< z(0}a}+Qrh;wnU<>o00B~NcShpPuoB5{;d1?{YvCcX>=II&rq*9$mH1HU`d^~Ge~R{(u6-l; z{h_}T5Z^>S{zKaYgqte3qda`-4Gq|Ku0REUjZJ&&Y}$fs3ar`+-~L%K8+E5NN?_F> zj3gRut}gq4FX3KbW1$0Qhrh#Y2{3rM_B4l+{k5XYqJa7qR)z8`%7Hqn`M{OLv&Z)Q z0CWM3H)Br&kD?p|uC2bhSnGi+bXhr6k3`!WkbZcH>>}F%iN$75bM_<3;d6E71>EU9 zAm}bo>Fhr4+Glt(nc;;r~}7uziuaS6iD3|7$T55Uksg--3}k`uQo2WAj~;Q7vY)VYuWnF5d8SFj#(--PuLyknHOIO!xjLKlqr1ul$%gkBIB z4_-xTfd|u7E!FpJY)jukH_cgj{H8pfyYm|9(u*+Wur0;+i<3kyNjY)ET%b7=S5W*2 z1)c$Y7sW6N8HGcq@c1??Sjh@+LSl7EHK-MJ1*w!?MPM2Q#@k_4I)WNjp6?H@!f@uW{?`N+bu|8f)8(^=!m?)=rntT`S+ZQ;GegG*-8+=AO1zVrRip* zCBZ8XRR2G5Np}B8xo7L2#Q)rGx;O{a58&OB!h@IGhyN8?fB`r&V&i4p_IW&>m!#_@ sNxme#FUi}lghQS~Uo1a+^5v6P1k_h&k9p2~G5T!c%ZYywP#u^50SFX|v;Y7A literal 0 HcmV?d00001 diff --git a/backend/projects/__pycache__/serializers.cpython-314.pyc b/backend/projects/__pycache__/serializers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46f00884f016e71e761dc95b3e8504997c8afa4b GIT binary patch literal 3502 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 LkYA?oMK0nG^dItr literal 0 HcmV?d00001 diff --git a/backend/projects/__pycache__/views.cpython-314.pyc b/backend/projects/__pycache__/views.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fe08436bd0069dc6e01283c010e2c7b3dc21189 GIT binary patch literal 7186 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 Tb^pd!3yz%!$NtICmSyGN2!Ph) literal 0 HcmV?d00001 diff --git a/backend/projects/migrations/__pycache__/0002_project_projects_pr_tenant__5d8ad4_idx_and_more.cpython-314.pyc b/backend/projects/migrations/__pycache__/0002_project_projects_pr_tenant__5d8ad4_idx_and_more.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..80a58bd50f7a6283ef13023bf147201ddbea9629 GIT binary patch literal 1075 zcmah|O-~dt818&BGrI~w2_}fF5luG24j2p`j2}P<7$IcT(yJ$Ln?UqxJG;YjfY_wd=S|=D>D%Y&+&q8I19H^AeTe3D0Dh?; zA66F}K9FD+tb;&X21^>(mUOOlA5CW1Xg9WlbojGE<*UCg<^G@ zjQSzmh$!Q6l#nD<+n6+o>>&vw!l*J{I1e#iR&7yJg;s`zmo`am{t$$W%ZCt30XU6n zElwDOR5^GkClH_gXN*whHxWhbC7})EFQj3sK@!e-s5DuES^p6Fi=;+di0=4LP#w|I ze4Ms$X$~=hOAClSuce4$f92`?+=5?2;g)RK&xP=N&(fEY+>%CQe&y>K&Onr4DHA1? z<_=YVLH1D~d)041+Xn9SwxM6NcOSPs;9l5wMt(RW2g9X@X$99Z8#3`beJNc#xuX&Z=WfT` mdvr{Onw(4#wGB&n{s+h)tNsUqE*MGx literal 0 HcmV?d00001 diff --git a/backend/projects/migrations/__pycache__/__init__.cpython-314.pyc b/backend/projects/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a354cdc8dfea6a537364ba75c4328208e3bf504f GIT binary patch literal 230 zcmdPq+CDWP<3&AOZ#$p^VRLKt=;Y5Q8#{v5=C$Usdrc5=dKsb{0oTygB36>;ASo#(>mGY(rA97QP z-%t&N%yooR3!yZN^ThC52))a3Qh9D8bV|9931x(3#+^_hQB1`=!3(u62gG literal 0 HcmV?d00001 diff --git a/backend/tenants/__pycache__/apps.cpython-314.pyc b/backend/tenants/__pycache__/apps.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98e4bb71113baa8f89b3e80acab15ebd5527ad11 GIT binary patch literal 581 zcmYLGv2N5r5Z$$XxsY%KML7@!;E0A3;@qU5KuEbeMWi5w31u=G-|ih7e73VYMhaC5 zM2)EU1H|7@Mk`TKAsVDZ)a=HNm}2&g=FQW-nMXHzYs|&3FQ3&X_x|w3RdiageCWVA zJ7y{0XU{qD`>fBlSsL7CX?VnU!iGmd8a-fx&QD(sxV^SpRpYXlsrj@aG?}->@}2|d z?1&Lh83o4?g(;^f4XBfbw9;tV-k~h8AkzrFyDLtxn3qG54Hu=5xfzbse7B}@Psv=^ zE;a10>|oHfs{jQqB!FE5aP&I&_&UI8jd|PI0GKIlC|89nO4r^La)xzIfKJu*ylmvj zwhv$kNlgKXw90Cd20(Y6jivWwP4db}ZITK#nzvFfFqxFodLiBa|D*VE44?`{0Lh-r zbdCBfd5Lq>@i;GQ5s%PF5l_&(&PvoGd2u)%O_B_!Z)72o)?tEGWro$6_D8x*kprgZ z(g${N=gGI|*2VhP`-$GT>K_b3yWXzP4{Lw6X53q7h&LUwt52I{J=2@6^!GAbF8vNU U=ik}(pOt|3Kkol!?zx=$2Zj}$>;M1& literal 0 HcmV?d00001 diff --git a/backend/tenants/__pycache__/managers.cpython-314.pyc b/backend/tenants/__pycache__/managers.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0e5b92a785d5e58832aa8c1684862a8b18ba2c2 GIT binary patch literal 1587 zcmZux&u<$=6rNdo?Im%$4NhXIjTA#9Be#l8s|X~5NYt9Dt<)%Nichqo@$SS~Snr0J zF_4>YaD)@-spUj-gA@M)7g7bO)ryJ(CvGT-C`aDgT_D>d00!g%ap-Mwi)uQL*tl)n%IVxZnIUfwCs!d({ytr|hKEnz zOSu$lGx0RRvjCo@Q;#7!@hm#U1yZ;EMnj#(g5IVoP|8TU!&CS+vc=J5 z034FNPc(IiHXh`4Zk9xloMTi-Ky}{9X|qjq!J!bsjHmC3kcWE1jkbmNQD4ZN4WZZE z;Ro@%d3c}(>*zfI1Jb5%;Vy_*CS;DZ$vSlIth6EgyV?E%_&_j0Lhc!H zUWs5+V&@cwt<~Z$n;$D7Rdbt5uD%em$D6Il?e|2e)hVhjRvGg{Uo+OcDLS&xh zce$+I2%^4Mz0Q^Jsx7XzIuVy%^X|PH*IUgFcRv%M*Gw}}%^nYV7os(`cakLHOXFJ7 zNZpYW>Jq2{`EzpWdAaia@^{ORH@{szC@=1p7N6VYZzhkxO!mkgU8@&ki?NXR1Y_|8 zV{niC0C9=2&-*;cEH-1CzEnE!LlH)drD@DCrZm^S%edCk@AS1$aCC|>H{eQnzN=jk z1dQ#`_}s`Q9K*h^N@SQU3>Ds@vO)d#H6;Ji$eGiJ1e82K^cc^mx54}hqb4#&(Fy@J znMi%$mFeVl0eDq(47ln-Tpn%W-L%E-C{M7H39DKF2IP|y8}MnAePBg_U98}Y0LL=l zj9+G7`C+%7OX`lzWN_jf^E!1ZC*g3?np)DDxYQLIt^p{ogpR9Y5^juoB*!c8jREW(FJ85p12rJP)Ms|}sa-v=-+X4*pW5{&wFCR=zI8Pv z1TrO!XvrzWucLy2CvqCqX%;iMoJEDr8RgzZ+(eZmihc?A4h?Fq(w7BlUOOb9(tzl< zR=48Uyia-9jT&A@VngEDZ0$x;G{je~*w%g&Dv4}}t>GUlz3*a1v?QoZN=Vp^lMq|) zr{nj5#QztzRb2t~PmWUhg3P@ji^usqo!))t(aOV>V*>EFWYTN9wMUJIjeiNiB+>r? DX-raE literal 0 HcmV?d00001 diff --git a/backend/tenants/__pycache__/middleware.cpython-314.pyc b/backend/tenants/__pycache__/middleware.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..deaf4f1e59299bd053cf30e9aa063728927b8064 GIT binary patch literal 2270 zcmb6aOH3PAaCX<&#u!Xu2!Zgi1BEyNZ&lJ(C@88Dmj)1o7)sJM&1&&J+nX-C&c5da zP!5)QDE&xjRaI4Q^wjp2W6y05J=!6vqgA6+>Y>UFg-D?}b>6NG6y=hUcHX>s^XC6% zPqj3M0Kxd+J3ZR~@HgMo!B-=;Uqhk<*B~uS!Ucf|eVi62#S0RX`k)=+kd`_i?K>@c zH<&No&<_d!W4=uYPI!u1W}47~Wm1BB{{k^plVv<<4-2);_AnAEn)?u#kOn4R^D*fN zSZKThP_c-(AuTcgBCtT(hh`d_R!`rIu4x9jrP?G@bHvrjX$c^>J%B_BF1MO4o1-L# zaZy+Vvu+)&phoF=KvuP7-IQk&0Xv9-YhhU}=rYaZ$+9Z1h1{KHWvIN?>e6bFUl z;hnYU9>*p*oibv}Ye)B(%=(C!vQ4r!p?P`j@ZL$mwz9fG#ibA%}<;O5K;lNGXD zU`jz{d5Wh#N{p=Ch@Md%-r%6KD!)sKP33}WtMn$Z(an^VDJ~O}(LE|Q5>pi2)S05l z6J*IQs`jcpujW)ccHXdxTI`%ki55$#G{0o2wkFRkoIjV6m(&cptjWx+8EY1r}JPTlJz6(Eue=HmEb}5iTlCMCt|QCwC?*O*tv~d_+Q2R$Z#tb#Nbq+1XsC; z3ulB&zA4ZW0Nv0Bi^3@ONCz0fMVQ-P^O|%koUTz5VSu3;okQK-Fx>>ui}jxAZRD#s z(-nUSeWz<3Q`f0=a-C^uUNCq4K%W9Nllzu=p?!a4R6-G-yg?J;AP{A?YG~$XSYuIL5q47 zyo##_X@5E(3+Z6GvF<}r?16sJ;f&M+x{wHEc3&yp@ZmR#-}d{6#)ae94JBBBi5NgZ zm>1`edl2TZ?gwFUw;C%@YUqRM;{a|ypyk&kv?|Q<=f>Zi6QWGD8NH>md_r=>QqrPOutDSzebp<8*4+8=1tYmwU}kcbkjSGc@H}_nTpY3p;;o2KtIO+V;~ufOxf?1N}WH9Am<4s2ZBjEpcauJw2=cLOs>?)CoRFd{TUycJ zbXZYt6jh^k5>b?_Zc}FHCNV8Vv734B`+4r$6@^z;&nPNmw!TzkgyQ`}n0iB{RMRso zLwJ*74>a#%;x+yb-*PKu^HJ?0UU%AtXy+5`+b3eEA@uC%k%p67fXI&2#b)t$-K1St z&776YGgfeW!fEmPfF>~vVoTywh(Gi7 L3n#Y$k$dew7AgP3 literal 0 HcmV?d00001 diff --git a/backend/tenants/__pycache__/models.cpython-314.pyc b/backend/tenants/__pycache__/models.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0fb3c8bc1dbb84bcf26c8e9c74fd68dcdca6df8 GIT binary patch literal 1091 zcmZ`&O-vI(6rQEq|CY8Ye_~>lVA2rTn2_Ls7(*K(Mv$z?37AZG23Xzh!pyAFTW`vV zt4EJ`^XSod@oJhTkWD;!@U?P<0CEZT|2wb#*q-G*Huo;uFVdS9=eI*pxUxYxw<{wj=1<`8B#D7`T9DpY_ zdnPFf%wkm4AC)0fKkt_Y)|LMA2#2irIV)=>Mv%;zNts_ma?nhH*>sRtWiD|g_{}uP zK6aS9Eq7KcF|Moz3E#E%d={i=9osHt`@qW)UwYW}USdKi1Eh*9$_zrkE7E>QV) z8G|reVr(q1I`;{08c)bJ;pM91`LsMw1f%5|5j%B{aB3{AR_AL*omkJAOAQ%%7e@4@ zr+1q{5@R7b#=&4Tm+4-f22mmhp@mKgmk*l<%`d6ZuCntEEmsOb3S(sg3Nsi(JKs?_ zhw+|IoZd+R<1L#D>DVrFJ&d{f%0ariL->Nt9LiPyAT9hl^%}%>gM5uhwrV$6cpPLb z&R~OLB7@w#=Q)hH(Mj4Cm{`(=ITcioA*Gr~+XAZOcHNgufSeM>mP3S~wk0j*I2gmw z^L?98rlxcAu(AWJA`mF9)Rd_0L_3$==AP1+F^DEyL&L;g%jcE z(W6KGH=3A8I^pEWn+ZM=Pgc(?t}w}b^VM|KS2a~NcSkP|0bYOJzZG=?@YNb;m)|F+ zZ6pV<2@zR_Hqm4nHc8|xJ8f6H(=Y+IAad&vc^l+u zmx3i?pG$>iu+CGK>h2&A!0Q{QcaR*w251t2b~Zij7Fh{qb>^MYOM8)vIXukan_8*1 z_AkXb-F^gI)b+Ky;c34Gk^h9~Ai}R~IrH(_ zYSWihtjl;Q>v2<6c_+?#Or+BG+f;?3tgFQ?Z&po(QtL#i8KSh8#d%`+2&FG`mh?}? zDBTiL=|rSF%_x<&HO5D8o|s@u@FZ5Ih6ThlOOrixF5XN~sx;H0i$!TEI=SWwgNHHr z6mKy5;WHeA3U`=fYM09%3zxEP-s7oO1J=0Tpj4znQyMtS>Gsg&o%2=4l-2VzMgyZ{V$+J>%KdNxLz2(^MVd5RPk;N4^<< zd=wntXuP}k@p|~VG9~SqylTOgzS<1rm^Eq^#w}d2P&iClX1&UwDa(R0uw7Fl2#4_7 zasAn!fKWPG9~3sDk}FNOBu?a2Cby;CUK13JR%O@6R&3V?t5wRMPiZO-%1J(yH&Fn8 iEl`$xcL^aUaQg(V{qQ_8`{wnR`rN1b+%G_*DC93gBjf1+ literal 0 HcmV?d00001 diff --git a/backend/tenants/admin.py b/backend/tenants/admin.py new file mode 100644 index 0000000..22a1ade --- /dev/null +++ b/backend/tenants/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin +from .models import Tenant + +@admin.register(Tenant) +class TenantAdmin(admin.ModelAdmin): + list_display = ('name', 'subdomain', 'is_active', 'created_at') + search_fields = ('name', 'subdomain') + list_filter = ('is_active',) diff --git a/backend/tenants/apps.py b/backend/tenants/apps.py new file mode 100644 index 0000000..668b464 --- /dev/null +++ b/backend/tenants/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TenantsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tenants" diff --git a/backend/tenants/management/commands/__pycache__/populate_test_data.cpython-314.pyc b/backend/tenants/management/commands/__pycache__/populate_test_data.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3f4394fa5843e9f2c1643192b8d5559bdbbfc5f3 GIT binary patch literal 18374 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 0.3 else '', + created_by=creator, + status=random.choice(statuses), + created_at=random_date(timezone.now() - datetime.timedelta(days=365), timezone.now()), + ) + projects.append(project) + self.stdout.write(f' ✓ Project: {project.name}') + return projects + + def create_tasks_for_projects(self, projects, users, range_tuple): + self.stdout.write(f'\n Creating tasks...') + total_tasks = 0 + statuses = ['todo', 'in_progress', 'review', 'done'] + priorities = ['low', 'medium', 'high', 'urgent'] + + for project in projects: + num_tasks = random.randint(*range_tuple) + project_users = [u for u in users if u.tenant == project.tenant] + + for i in range(num_tasks): + assigned_to = random.choice(project_users) if random.random() > 0.3 else None + due_date = random_date(timezone.now() + datetime.timedelta(days=1), timezone.now() + datetime.timedelta(days=90)) if random.random() > 0.4 else None + + task = Task.objects.create( + tenant=project.tenant, + project=project, + title=fake.sentence(nb_words=6).rstrip('.'), + description=fake.text(max_nb_chars=150) if random.random() > 0.5 else '', + assigned_to=assigned_to, + status=random.choice(statuses), + priority=random.choice(priorities), + due_date=due_date, + ) + total_tasks += 1 + self.stdout.write(f' → {project.name}: {num_tasks} tasks') + + self.stdout.write(f' ✓ Total tasks created: {total_tasks}') + return total_tasks + + def create_notifications(self, users, range_tuple): + self.stdout.write(f'\n Creating notifications...') + total_notifications = 0 + titles = [ + 'New task assigned', 'Project update', 'Deadline approaching', + 'Comment on your task', 'Project completed', 'Meeting reminder', + ] + + for user in users: + num_notifications = random.randint(*range_tuple) + for i in range(num_notifications): + Notification.objects.create( + tenant=user.tenant, + user=user, + title=random.choice(titles), + message=fake.sentence(), + is_read=random.choice([True, False, False, False]), + link=fake.url() if random.random() > 0.7 else None, + created_at=random_date(timezone.now() - datetime.timedelta(days=30), timezone.now()), + ) + total_notifications += 1 + self.stdout.write(f' ✓ Total notifications: {total_notifications}') + return total_notifications + + def create_activity_logs(self, tenant, users, count): + self.stdout.write(f'\n Creating activity logs for {tenant.subdomain}...') + actions = ['created', 'updated', 'deleted', 'logged_in', 'assigned', 'completed'] + target_types = ['project', 'task', 'user', 'notification'] + + all_users = list(User.objects.all()) + total_logs = 0 + + for i in range(count): + user = random.choice(all_users) if all_users else None + ActivityLog.objects.create( + tenant=tenant, + user=user, + action=random.choice(actions), + target_type=random.choice(target_types), + target_id=str(random.randint(1, 1000)), + metadata={ + 'ip_address': fake.ipv4(), + 'user_agent': fake.user_agent(), + } if random.random() > 0.5 else {}, + created_at=random_date(timezone.now() - datetime.timedelta(days=180), timezone.now()), + ) + total_logs += 1 + + self.stdout.write(f' ✓ Activity logs: {total_logs}') + return total_logs + + def print_summary(self, tenants, total_users, total_projects, total_tasks, total_notifications, total_logs): + self.stdout.write("\n" + "="*60) + self.stdout.write(" POPULATION COMPLETE".center(60)) + self.stdout.write("="*60) + self.stdout.write(f' Tenants created: {len(tenants)}') + self.stdout.write(f' Total users: {total_users}') + self.stdout.write(f' Total projects: {total_projects}') + self.stdout.write(f' Total tasks: {total_tasks}') + self.stdout.write(f' Total notifications: {total_notifications}') + self.stdout.write(f' Total activity logs: {total_logs}') + self.stdout.write("="*60) + self.stdout.write('\nTest accounts per tenant:') + self.stdout.write(' Username: super_admin_ / Password: password123') + self.stdout.write(' Username: institution_admin_ / Password: password123') + self.stdout.write(' Other users: _1, _2, ...') + self.stdout.write('\nLogin example (use first tenant ID as X-Tenant-ID):') + self.stdout.write(' POST /api/auth/login/') + self.stdout.write(' Headers: X-Tenant-ID: 1') + self.stdout.write(' Body: {"username": "super_admin_techacademy", "password": "password123"}') + self.stdout.write("="*60) + + # Also print tenant IDs for reference + self.stdout.write('\nTenant IDs:') + for t in tenants: + self.stdout.write(f' {t.id}: {t.name} ({t.subdomain})') + +def random_date(start_date, end_date): + delta = end_date - start_date + random_days = random.randint(0, delta.days) + return start_date + datetime.timedelta(days=random_days) diff --git a/backend/tenants/managers.py b/backend/tenants/managers.py new file mode 100644 index 0000000..0eba805 --- /dev/null +++ b/backend/tenants/managers.py @@ -0,0 +1,15 @@ +from django.db import models +from django.core.exceptions import FieldError + +class TenantScopedQuerySet(models.QuerySet): + def tenant(self, tenant=None): + if tenant: + return self.filter(tenant=tenant) + return self + +class TenantScopedManager(models.Manager): + def get_queryset(self): + return TenantScopedQuerySet(self.model, using=self._db) + + def tenant(self, tenant=None): + return self.get_queryset().tenant(tenant) diff --git a/backend/tenants/middleware.py b/backend/tenants/middleware.py new file mode 100644 index 0000000..e28c934 --- /dev/null +++ b/backend/tenants/middleware.py @@ -0,0 +1,39 @@ +from django.http import JsonResponse +from tenants.models import Tenant + +class TenantMiddleware: + def __init__(self, get_response): + self.get_response = get_response + self.exempt_paths = [ + '/admin/', + '/api/schema/', + '/api/docs/', + '/api/auth/login/', + '/api/auth/register/', + '/api/auth/token/refresh/', + '/api/auth/profile/', + ] + + def __call__(self, request): + if any(request.path.startswith(path) for path in self.exempt_paths): + request.tenant = None + return self.get_response(request) + + # 1. Check Header + tenant_id = request.headers.get('X-Tenant-ID') + if not tenant_id: + # 2. Check Subdomain (Optional, skipping for now, can implement later) + # host = request.get_host().split(':')[0] + # subdomain = host.split('.')[0] + pass + + if tenant_id: + try: + request.tenant = Tenant.objects.get(id=tenant_id, is_active=True) + except Tenant.DoesNotExist: + return JsonResponse({"detail": "Invalid or inactive tenant ID supplied."}, status=403) + else: + # Normally we might enforce tenant_id, but we'll let permission classes handle it. + request.tenant = None + + return self.get_response(request) diff --git a/backend/tenants/migrations/0001_initial.py b/backend/tenants/migrations/0001_initial.py new file mode 100644 index 0000000..463dc8c --- /dev/null +++ b/backend/tenants/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 6.0.2 on 2026-02-20 18:33 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="Tenant", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ( + "subdomain", + models.CharField(db_index=True, max_length=100, unique=True), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("is_active", models.BooleanField(default=True)), + ], + ), + ] diff --git a/backend/tenants/migrations/__init__.py b/backend/tenants/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/tenants/migrations/__pycache__/0001_initial.cpython-314.pyc b/backend/tenants/migrations/__pycache__/0001_initial.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b59a8afb836026ae7b5d9c1c9647449c679fec60 GIT binary patch literal 1284 zcmZ`&%}*Og6rWw&i*1atoi&XkWSiD0T~)O~L_)2qHU&gJR3fw?M;ML0gPCT%>&(v5 z&=ZF$=N|bBs`S=ldh4-AY9ZO06H-q-RI&tWkL{aXEOY6scINkHe(%lunve5ySp?&+ zw`c5^1VVo(VKP!T;OZfOQ*?lA?Fo9U3GJ~i^cpIoC1mUGAv>|J?HUuM>G}ZdsUZ?} z@&jb0{#1gc%WRKzIT6hF0%`Pohq^&W9Zg2=23+0K)V2FaXf_i1fdPgzk!&K7+BbId z6Z&*j?tnAt-O_Y#g5XSpFKMT2V?7z~uIcs+u+0f)x-OxEf9>=-+^+`kGXKZ3wF3w2 zX4h|m1@0_x7h=uZbL2tpmd!cf-M!6-am33{dCinPzplq;O-;<%1(COlkkFFM>`XGb zCyhPo5l={+IoncyvSlS@j)cO;9Zrd$j-2gt)+77`zoaKJ6Hv~G%f6#BcT9QP4=DCX zkIJN)Ee(o9USpSfU2z2Du(@q;R&!MmRJoqGy&||McDzi7&U?M;}zS;A? z#l&%Bh6R{(1UpuSXB>J+LRVNiS0$9jA)~Gn{Dhua^D>PiFEEx_jG;9`mjcdV{3;}_ z@(iSTj1L(Pgv&hY`LHp=JSHF*nR95Lf@@xf(SWOh$l1*(`W@9}T$NqsHdyy7$ji36 zR%SMj2#?|17Nlv9^=K^SHhkZu#EYe@-=~wFEn^%A=-&>+DmZJ0f(96KRmv;)k$ls7 z0qG4|eZolq+3k_mmfs0`)DyuJ)jz3Y$RraOx3+1Uhftf=bJ8WezUlg*Q{Nx~b?RFr zIBNTZJFT6)&5f;An{-}6kS!6FJ7`TGQ)^{qWi_%0$!PXZxO$V(q(JZ^OmEO-zI6KN z@=p2m>#>0{cZTWeAYC05DmAgm8xgQ!`j-Qw)U%eQQJN* zJQ@`j&kl#x)j@UjUE`wq<$2-Zc&TXSuTy3wJ5DT=jPIZF4=0X4gD%cE-w?0sH=TA= z(x{4YXe#3|cNvgOe;#+t7u*6-z1?6Drg1{kw0{hwS=Yv*X8e#H78<`78h;}YBJKYG DjT%Y& literal 0 HcmV?d00001 diff --git a/backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc b/backend/tenants/migrations/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e9c7f9296c447c6a7488cc663404c3d378f5e634 GIT binary patch literal 229 zcmdPq+CDWP<3&AOZ#$p^VRLKt=;Y5Q8#u koL)iYEe@O9{FKt1RJ$Tppp!t3E(S3^F*7nU7BK@^0B~|Y!2kdN literal 0 HcmV?d00001 diff --git a/backend/tenants/models.py b/backend/tenants/models.py new file mode 100644 index 0000000..737ef3d --- /dev/null +++ b/backend/tenants/models.py @@ -0,0 +1,10 @@ +from django.db import models + +class Tenant(models.Model): + name = models.CharField(max_length=255) + subdomain = models.CharField(max_length=100, unique=True, db_index=True) + created_at = models.DateTimeField(auto_now_add=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name diff --git a/backend/tenants/serializers.py b/backend/tenants/serializers.py new file mode 100644 index 0000000..d97b15b --- /dev/null +++ b/backend/tenants/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from .models import Tenant + +class TenantSerializer(serializers.ModelSerializer): + class Meta: + model = Tenant + fields = ['id', 'name', 'subdomain', 'created_at', 'is_active'] + read_only_fields = ['id', 'created_at'] diff --git a/backend/tenants/tests.py b/backend/tenants/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/backend/tenants/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/backend/tenants/views.py b/backend/tenants/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/backend/tenants/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here.