From bf0a3c22c07d9e6566fa8780a31f0936cc33a2c1 Mon Sep 17 00:00:00 2001 From: Kevin Heyer Date: Tue, 9 Sep 2025 14:25:59 +0200 Subject: [PATCH] Refactor risk management application with enhanced localization, user authentication, and UI improvements - Added verbose names for Incident and ResidualRisk models for better clarity in admin interface. - Updated impact choices for ResidualRisk and Risk models to ensure consistency and clarity. - Implemented gettext_lazy for translatable strings in models and choices. - Enhanced the Risk, ResidualRisk, Control, AuditLog, and Incident models with Meta options for better admin representation. - Added login required decorators to views for improved security. - Introduced new CSS variables and classes for better visual representation of risk levels. - Created custom template tags for dynamic CSS class assignment based on risk likelihood and impact. - Improved dashboard and statistics views with user authentication checks. - Updated templates for risks, controls, incidents, and admin interface to include edit and delete options for staff users. - Added new login and logout templates for user authentication. - Enhanced list views for risks, controls, and incidents to include action buttons for staff users. - Improved overall UI/UX with Bulma CSS framework for a more modern look and feel. --- config/__pycache__/settings.cpython-311.pyc | Bin 5097 -> 5391 bytes config/__pycache__/urls.cpython-311.pyc | Bin 1803 -> 2002 bytes config/settings.py | 17 +- config/urls.py | 2 + db.sqlite3 | Bin 221184 -> 221184 bytes locale/de/LC_MESSAGES/django.mo | Bin 0 -> 1968 bytes locale/de/LC_MESSAGES/django.po | 211 ++++++++++++++++++ locale/de/LC_MESSAGES/formats.py | 3 + locale/en/LC_MESSAGES/django.mo | Bin 0 -> 380 bytes locale/en/LC_MESSAGES/django.po | 199 +++++++++++++++++ risks/admin.py | 14 +- risks/apps.py | 8 +- ..._options_alter_control_options_and_more.py | 108 +++++++++ .../migrations/0019_alter_incident_options.py | 16 ++ ...r_residualrisk_impact_alter_risk_impact.py | 40 ++++ risks/models.py | 95 +++++--- risks/templatetags/risk_extras.py | 57 +++++ risks/views.py | 24 +- static/css/design.css | 83 +++++++ templates/admin/base_site.html | 24 ++ templates/base.html | 58 ++++- templates/registration/logged_out.html | 1 + templates/registration/login.html | 109 +++++++++ templates/risks/item_control.html | 10 + templates/risks/item_incident.html | 9 + templates/risks/item_risk.html | 95 ++++---- templates/risks/list_controls.html | 35 ++- templates/risks/list_incidents.html | 35 ++- templates/risks/list_risks.html | 44 +++- 29 files changed, 1174 insertions(+), 123 deletions(-) create mode 100644 locale/de/LC_MESSAGES/django.mo create mode 100644 locale/de/LC_MESSAGES/django.po create mode 100644 locale/de/LC_MESSAGES/formats.py create mode 100644 locale/en/LC_MESSAGES/django.mo create mode 100644 locale/en/LC_MESSAGES/django.po create mode 100644 risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py create mode 100644 risks/migrations/0019_alter_incident_options.py create mode 100644 risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py create mode 100644 templates/admin/base_site.html create mode 100644 templates/registration/logged_out.html create mode 100644 templates/registration/login.html diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d2e5ac0dba70470959de3287c19a7635b88926e1..d95596c785d51c21d5caaf104ce2a23faf315f6c 100644 GIT binary patch delta 719 zcmZWmOHUI~6u$S;2h(=?(vnsM6p&h!qBIENi)k4PQXUED;w~UV&&{+l&X5^Nw42U9 zFxrhKE^vbj*_i$TH||)}uxV!H!i~G`+H`m-KK~JxBXsg7(90dKo6^6^PLR&Ow-hJ1U_=%n#GGAVMmpSK%%l=JCCg zI1TsdHF%(6cnC8p0qFyw87Az8-7)_H^3c8S+am~l<~#-+VIy^Ojm4m#;+lvMjj=e) zp7?QgVfXrJ>E9OQJ__iBb()oT&6 zYHeB5pjeenLouP2g=dPc8ria8vg~uEs%1Bo@)py<;wjxScpKSG=Z~`v0ag>`9n;um zV|iw3RXsz@G~dke&$68tR=JWP4q>|EmvvRES{29VZbLNIOa<2o&NW|(1Gtfhp(HI8pGZY{)d`iB3sOnmSdrG|RvX{Kh2)q!#;0s=^YzSe z9+)}znP{vD5VsObjB3R6svTm6ze%mQOc>D3-~54WEM7ONU=%bSi`oAiGg7?5JNzeN~%gx zNUC6pZ8~EWF9SoAV5(4x9h5DUDz!irq_>_S#eNwhP#+LO03$<+LzG&IW0X1&YcMgS za;7+?IAfTu36^n5amA3)0?W9ixMRp@r+7r^0DY$m#Cj>7K!548r7#9FXnJjKV)12U za=r3dXyE!Fv-yhPWkyjE i20_sV-WxoE4V*W)`5QQHaPl^=ZT=)I$;c7|)C~ZHFk`L& diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 31827bceeb989a573cc7baab96358ce35c157a9c..9f89ecc8210cca19db71269844bc8bcc77a09de3 100644 GIT binary patch delta 328 zcmeC?yTq@)oR^o20SFX$4`lSQFfcp@abSQK%J{rzqxyVCHYSEt&J_O1SxhpM4=}Pa z3#2km7GM$)OJ!Qd#K5o`h#>%|j3Y&8@+U?^c44p<5ttTkQ7~UDg=3BQWIZNXdG;u& zRF*7hn9d^UR4K4-39vdzpgvVjsbGc_Al8(gJcH?}ENiBrMV@|@Xi8RMUV6S>a(-T# zUTINIu^vc#at*VxY87W|arNl|8!USerU23Yy#`OF=RO#DonHCY`P zMZ_nlPf))gWq47@=!%fhMHb^LEXI?Q*&IZqC!|kEzaVFEQOxp+nB_$lt1B#4ljpL@ SbMSNUGc|C35Sx62%^d*ag;DhY delta 231 zcmcb_-_57KoR^o20SIhf?$6+6VPJR+;=lkml<_%dqxyVCom9qUObiUGffxdq7*aV> z_*0p%hzd-WVlw0w1ghXn5lZ1$BRsi|NmiaUN-C8lOB$xXNIF#tY=#I}o#^BvU&!on~hz znU6#TX@-JrZ%y~GmmYg6cnFa$l0$+Zf(k}k5D%Jzmllyiq3xlCIpKsS~N!lPiniR1zk0$wa3;;9B04 zhcREjy{r4vs&Bf*cv7_NX}#q0n3sC78a)ihYn-ZyJS&QMMacK>jnso1YECWcEv;26 zS%z8ji_WxfIW=!9fYT&N6BU-vt2`Eqds2zc5WOHM5#d;TydD^qUv(-Qh=0f>r{=p( zb#q!XinimLFP$K09A*VhQZc{Od}Df7ihjGD9HJU%5Ovju`@TTArF(rMPF+gC2z_ZW zyg+BeL4r<%{R7+gb#}f-Q9}Jz?c}MgSgZ-6CU7h#=OtO1GH0z?#SKi)Gu@+cbHqGv zx~(yLHhw)cXSsTB#s2s#Yi#bPFdUA4UlY~(h!a5E2pX1HUdFtle75E^*BtbF5p8eW zxPuM=7uh9flESLIqGILyHPps1G+a)=eYgsL!f)^k{0xubA>4)Qa0`~;GOR4^nEn!8 zOOgmtbgO3Kno2iT{zgNQdN~1q!5{D|JcXYk?IU;qKf)cj84*@tIZ`(O+DHn~ammCb z-Mk6JcO6VQKU0z}oGYEfqAcsP!PJaxpXVGKE4C3>d9LxOo47JO>l8Hm6RsKhv2u0QTa_PtELmg`!``g#sk5f;m!eS^=OqiwH6A+TfLeu~^ZhDpx=v8JsiZN4m zT&A(!OMEyw9^I5V$GGN9V7aEzbNb@N)6~WSalT*pEnuv2M&>zl$A2^5kzWPKvw|$E ziV$|BBf>ruz8ewve}$&8+DGgzd}O(v&(w6=>^aEvjObgMAzQbqjBXfi6tej1gs+#& zjOXiq;I%eSZQUGeoT5oG%VS01^Ud`G#Me9j_rjVB{RElaxf#Qj?`?091E}#$H}U4@ N*T#tEwK4Mj>@y#%L%RR~ delta 493 zcmZoTz}s+ucY-uy+(a2?#<-0MOZb_U*|tw+7jWO$n8~(zyI6q)8@D_cvn%IxMrTHu z$)5ThjLMrY>6;j^?PlQj;oH5jaXp_vBOenVL!&(dhdM7WF9)aNe;DfnR)3& zrK!cmsYONxMy9$3mbwNO3Wf$&CKgsEmU>1eCdQUVlYR0_`3=mi3=FM|Ec7hQ%`J>9 zn=j;VzmU)PEuN8c`tkFOT3~qtD^oK)BO^mobJJ}X7+DmgxcT=p@L%Wu&Ht7EJ^xGo z$NYEsFY+JdKh3{?vtYqO{^|SwGr9s5onqks!~dQC6aQQO7eGb#_;2!G;XluR3aDre z|MpY-OslvRax4lAObrYy%PbR3E7J-xa*Qo<%u_7V%1ui%ONvb@r{9xh+TvO4YhqlENsnjGQl?;4nqoDB1`k+A~8f0o9E zmX_Nm$}kl(OL24EXW*aAug15V&x7|euNu!|9tZ9V+!|c>H#Q#R+J0Y)X_5#*r){5T g&%`Vsf@TAY9J3?i^f|IjQtgNQnYJJDXSNdn02aBKC;$Ke diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..0584151e16258e0a5e42c405d6b8f04fca053d71 GIT binary patch literal 1968 zcmZvbPi$K?9LLSrU<-c+9WVxid5j?@^h#PN8Yw|b|7Fz{wMw&|IN;qIP8>NP@%_E$CPTuC{jqU9|rQQr+)@e+1$IFEn8ZMFbMR^KCP?r83Z!>@ z4buHR5M^R7prcgmIdp1NLwXl!;GmHoY9~vuXCu#mQ*ll>k50Kz9Jnv?pq#1ETd1Cp zOVM7!$kETClg=qe%8S}7C5rKEoWB}LZ$5?oJUV?6RGSykshyyqk><*jqw>Ltpe&Z& z%fK3=R0VCb@x-&nwoqDhl~!SwEs7vB>oC#Q%akqJe5hq8W~Q%FX@U~vNo#_$8Yc?+ z@r1XfA6ny8S4+0&Bs`{E1h#|;cXDVQxm%LH=hU!BZPoB~XEP_gXP6Hs^Ij1V#&Sa2 z$j?!xYeQ+^XI%?Z(Agf{Z_AHlj{>w~B;MV{+^cjXG;b?E`0olg3L6IA5=LY)#~TY{ zx#juV4h7iQSmUdvZ(Uy2vx7QHc5Rp*yl+!F)mM~~vy?-!nkKM$6u-uIloN?VkR%@m zsgx-uEuX10Sr}zL>DU^ttgj_)xvl78LPe|b+3M_EQhgUcKJ!-X?YTFqv(;+ls_-G{ zIAOdN!MYm1f?I}Pmb=pN_Xf1MB(Dql1E+g7e;*f2Ho^;&jud%~HznR;#?~57Wu>*+ zTApM*GhMCF|BH$wo!uca3G#Y)bEt(f^Srku95hwG)>%m|PR>z6ed&^A)3d2ES&d)p zs!&$CH`oYr4A{A``iT$ry!0i$?3LWhxoS|{R7t{$kDW}e|x zfB$aQ6>cC^U{_Eu2v|whgu=FwlJ;*7b$fjJUt2 zA*KUy@9zcp-~XxKH~3_m67_~8A*&mtaC(cDTd*{tRYVSa8{aoH9w0C^u#+iX4s{o% t0u$|(cI+>!4Brtij$sLvCl9C`7U*1R)))kQKX8K5(UVafi2VjF*}pi}zu^D? literal 0 HcmV?d00001 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po new file mode 100644 index 0000000..cbe4abe --- /dev/null +++ b/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,211 @@ +msgid "" +msgstr "" +"Project-Id-Version: wira-risk-management\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-09 14:20+0200\n" +"PO-Revision-Date: 2025-09-09 13:45+0200\n" +"Last-Translator: Kevin Heyer \n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: risks/admin.py:6 risks/admin.py:8 +msgid "Administration" +msgstr "Verwaltung" + +#: risks/admin.py:7 +msgid "Admin" +msgstr "Admin" + +#: risks/admin.py:13 +msgid "SSO Information" +msgstr "SSO-Informationen" + +#: risks/admin.py:20 +msgid "Risks Owned" +msgstr "Eigene Risiken" + +#: risks/admin.py:24 +msgid "Controls Responsible" +msgstr "Verantwortlich für Maßnahmen" + +#: risks/apps.py:7 +msgid "Risk Management" +msgstr "Risikomanagement" + +#: risks/models.py:35 +msgid "Risk" +msgstr "Risiko" + +#: risks/models.py:36 +msgid "Risks" +msgstr "Risiken" + +#: risks/models.py:39 +#, fuzzy +#| msgid "Very low occurs less than once every 5 years" +msgid "Very low – occurs less than once every 5 years" +msgstr "Sehr niedrig – tritt seltener als einmal in fünf Jahren auf" + +#: risks/models.py:40 +#, fuzzy +#| msgid "Low once every 15 years" +msgid "Low – once every 1–5 years" +msgstr "Niedrig – einmal in 1–5 Jahren" + +#: risks/models.py:41 +#, fuzzy +#| msgid "Likely once per year or more" +msgid "Likely – once per year or more" +msgstr "Wahrscheinlich – einmal pro Jahr oder öfter" + +#: risks/models.py:42 +#, fuzzy +#| msgid "Very likely multiple times per year/monthly" +msgid "Very likely – multiple times per year/monthly" +msgstr "Sehr wahrscheinlich – mehrmals pro Jahr/monatlich" + +#: risks/models.py:45 +#, fuzzy +#| msgid "Low (< 1,000 minor operational impact)" +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "Sehr Gering (< 1.000 € – geringe betriebliche Auswirkungen)" + +#: risks/models.py:46 +#, fuzzy +#| msgid "Medium (1,0005,000 local impact)" +msgid "Low (1,000–5,000 € – local impact)" +msgstr "Gering (1.000–5.000 € – lokale Auswirkungen)" + +#: risks/models.py:47 +#, fuzzy +#| msgid "High (5,00015,000 team-level impact)" +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "Hoch (5.000–15.000 € – Auswirkungen auf Teamebene)" + +#: risks/models.py:48 +#, fuzzy +#| msgid "Severe (50,000100,000 regional impact)" +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "Schwerwiegend (50.000–100.000 € – regionale Auswirkungen)" + +#: risks/models.py:49 +#, fuzzy +#| msgid "Critical (> 100,000 existential threat)" +msgid "Critical (> 100,000 € – existential threat)" +msgstr "Kritisch (> 100.000 € – existenzielle Bedrohung)" + +#: risks/models.py:52 +msgid "Confidentiality" +msgstr "Vertraulichkeit" + +#: risks/models.py:53 +msgid "Integrity" +msgstr "Integrität" + +#: risks/models.py:54 +msgid "Availability" +msgstr "Verfügbarkeit" + +#: risks/models.py:58 risks/models.py:186 risks/models.py:251 +msgid "Title" +msgstr "Titel" + +#: risks/models.py:59 risks/models.py:252 +msgid "Description" +msgstr "Beschreibung" + +#: risks/models.py:60 +msgid "Asset" +msgstr "Asset" + +#: risks/models.py:61 +msgid "Process" +msgstr "Prozess" + +#: risks/models.py:62 +msgid "Category" +msgstr "Kategorie" + +#: risks/models.py:63 +msgid "Created at" +msgstr "Erstellt am" + +#: risks/models.py:64 +msgid "Updated at" +msgstr "Aktualisiert am" + +#: risks/models.py:119 +msgid "Residual Risk" +msgstr "Restrisiko" + +#: risks/models.py:120 +msgid "Residual Risks" +msgstr "Restrisiken" + +#: risks/models.py:175 +msgid "Control" +msgstr "Maßnahme" + +#: risks/models.py:176 +msgid "Controls" +msgstr "Maßnahmen" + +#: risks/models.py:179 +msgid "Planned" +msgstr "Geplant" + +#: risks/models.py:180 +msgid "In progress" +msgstr "In Bearbeitung" + +#: risks/models.py:181 +msgid "Completed" +msgstr "Abgeschlossen" + +#: risks/models.py:182 +msgid "Verified" +msgstr "Verifiziert" + +#: risks/models.py:183 +msgid "Rejected" +msgstr "Abgelehnt" + +#: risks/models.py:212 +msgid "Auditlog" +msgstr "Audit-Log" + +#: risks/models.py:213 +msgid "Auditlogs" +msgstr "Audit-Logs" + +#: risks/models.py:243 +msgid "Incident" +msgstr "Vorfall" + +#: risks/models.py:244 +msgid "Incidents" +msgstr "Vorfälle" + +#: risks/models.py:247 +msgid "Opened" +msgstr "Eröffnet" + +#: risks/models.py:248 +msgid "In Progress" +msgstr "In Bearbeitung" + +#: risks/models.py:249 +msgid "Closed" +msgstr "Geschlossen" + +#: risks/models.py:253 +msgid "Date reported" +msgstr "Meldedatum" + +#: risks/models.py:255 +msgid "Reported by" +msgstr "Gemeldet von" diff --git a/locale/de/LC_MESSAGES/formats.py b/locale/de/LC_MESSAGES/formats.py new file mode 100644 index 0000000..e7b3e15 --- /dev/null +++ b/locale/de/LC_MESSAGES/formats.py @@ -0,0 +1,3 @@ +DATE_FORMAT = "d.m.Y" +DATETIME_FORMAT = "d.m.Y H:i" +TIME_FORMAT = "H:i" \ No newline at end of file diff --git a/locale/en/LC_MESSAGES/django.mo b/locale/en/LC_MESSAGES/django.mo new file mode 100644 index 0000000000000000000000000000000000000000..71cbdf3e9d8d54be31066ec4ad8628bc2c1f2845 GIT binary patch literal 380 zcmYL@K~KUk7=|%=+R?Lz&%}d9i{c3jGZa>EvE7z2Nc2{r&Y96JZ6W$Y{CoZuJ5A(G zp7i_Dx9RhJeDu}vIq;l#&OC>nD^HugXY4QU{MmN?lNtRkR}RH%w3NnHT4Bh@vF%H^(V-=Ii1iQ$Qo9Pt!I1Rhe%oml#`f^NEGFCKEL->Rc=KoQ6a?!10%_7(V7ey8`V`;n{war z20Z3;uifk31QV^CRQ|iq#``$=;jWunRB8aLH({)F;i8zL{=V00y-I_qTIqGAN(}v% i$^}`yHKImSZ8jEzYJOK6-VWez49^vuhS0kh1f3tbb!oc* literal 0 HcmV?d00001 diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po new file mode 100644 index 0000000..84faae8 --- /dev/null +++ b/locale/en/LC_MESSAGES/django.po @@ -0,0 +1,199 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-09-09 14:20+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: risks/admin.py:6 risks/admin.py:8 +msgid "Administration" +msgstr "" + +#: risks/admin.py:7 +msgid "Admin" +msgstr "" + +#: risks/admin.py:13 +msgid "SSO Information" +msgstr "" + +#: risks/admin.py:20 +msgid "Risks Owned" +msgstr "" + +#: risks/admin.py:24 +msgid "Controls Responsible" +msgstr "" + +#: risks/apps.py:7 +msgid "Risk Management" +msgstr "" + +#: risks/models.py:35 +msgid "Risk" +msgstr "" + +#: risks/models.py:36 +msgid "Risks" +msgstr "" + +#: risks/models.py:39 +msgid "Very low – occurs less than once every 5 years" +msgstr "" + +#: risks/models.py:40 +msgid "Low – once every 1–5 years" +msgstr "" + +#: risks/models.py:41 +msgid "Likely – once per year or more" +msgstr "" + +#: risks/models.py:42 +msgid "Very likely – multiple times per year/monthly" +msgstr "" + +#: risks/models.py:45 +msgid "Very Low (< 1,000 € – minor operational impact)" +msgstr "" + +#: risks/models.py:46 +msgid "Low (1,000–5,000 € – local impact)" +msgstr "" + +#: risks/models.py:47 +msgid "High (5,000–15,000 € – team-level impact)" +msgstr "" + +#: risks/models.py:48 +msgid "Severe (50,000–100,000 € – regional impact)" +msgstr "" + +#: risks/models.py:49 +msgid "Critical (> 100,000 € – existential threat)" +msgstr "" + +#: risks/models.py:52 +msgid "Confidentiality" +msgstr "" + +#: risks/models.py:53 +msgid "Integrity" +msgstr "" + +#: risks/models.py:54 +msgid "Availability" +msgstr "" + +#: risks/models.py:58 risks/models.py:186 risks/models.py:251 +msgid "Title" +msgstr "" + +#: risks/models.py:59 risks/models.py:252 +msgid "Description" +msgstr "" + +#: risks/models.py:60 +msgid "Asset" +msgstr "" + +#: risks/models.py:61 +msgid "Process" +msgstr "" + +#: risks/models.py:62 +msgid "Category" +msgstr "" + +#: risks/models.py:63 +msgid "Created at" +msgstr "" + +#: risks/models.py:64 +msgid "Updated at" +msgstr "" + +#: risks/models.py:119 +msgid "Residual Risk" +msgstr "" + +#: risks/models.py:120 +msgid "Residual Risks" +msgstr "" + +#: risks/models.py:175 +msgid "Control" +msgstr "" + +#: risks/models.py:176 +msgid "Controls" +msgstr "" + +#: risks/models.py:179 +msgid "Planned" +msgstr "" + +#: risks/models.py:180 +msgid "In progress" +msgstr "" + +#: risks/models.py:181 +msgid "Completed" +msgstr "" + +#: risks/models.py:182 +msgid "Verified" +msgstr "" + +#: risks/models.py:183 +msgid "Rejected" +msgstr "" + +#: risks/models.py:212 +msgid "Auditlog" +msgstr "" + +#: risks/models.py:213 +msgid "Auditlogs" +msgstr "" + +#: risks/models.py:243 +msgid "Incident" +msgstr "" + +#: risks/models.py:244 +msgid "Incidents" +msgstr "" + +#: risks/models.py:247 +msgid "Opened" +msgstr "" + +#: risks/models.py:248 +msgid "In Progress" +msgstr "" + +#: risks/models.py:249 +msgid "Closed" +msgstr "" + +#: risks/models.py:253 +msgid "Date reported" +msgstr "" + +#: risks/models.py:255 +msgid "Reported by" +msgstr "" diff --git a/risks/admin.py b/risks/admin.py index 32ee87d..7163e70 100644 --- a/risks/admin.py +++ b/risks/admin.py @@ -1,23 +1,27 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin +from django.utils.translation import gettext_lazy as _ from .models import User, Risk, ResidualRisk, Control, Incident +admin.site.site_header = _("Administration") +admin.site.site_title = _("Admin") +admin.site.index_title = _("Administration") @admin.register(User) class UserAdmin(BaseUserAdmin): fieldsets = BaseUserAdmin.fieldsets + ( - ("SSO Information", {"fields": ("is_sso_user",)}), + (_("SSO Information"), {"fields": ("is_sso_user",)}), ) list_display = ("username", "email", "is_staff", "is_superuser", "is_sso_user", "owned_risks_count", "responsible_controls_count") def owned_risks_count(self, obj): return obj.risks_owned.count() - owned_risks_count.short_description = "Risks Owned" + owned_risks_count.short_description = _("Risks Owned") def responsible_controls_count(self, obj): return obj.controls_responsible.count() - responsible_controls_count.short_description = "Controls Responsible" + responsible_controls_count.short_description = _("Controls Responsible") class ResidualRiskInline(admin.StackedInline): """ @@ -25,7 +29,7 @@ class ResidualRiskInline(admin.StackedInline): """ model = ResidualRisk extra = 0 - can_delete = False # Since each Risk can have at most one residual risk + can_delete = False readonly_fields = ("score", "level", "review_required") fields = ("likelihood", "impact", "score", "level", "review_required") @@ -48,7 +52,7 @@ class RiskAdmin(admin.ModelAdmin): ) list_filter = ("level", "likelihood", "impact", "owner") search_fields = ("title", "asset", "process", "category") - inlines = [ResidualRiskInline, ControlRisksInline] # Controls hier verknüpfen + inlines = [ResidualRiskInline, ControlRisksInline] def save_model(self, request, obj, form, change): obj._changed_by = request.user diff --git a/risks/apps.py b/risks/apps.py index 8b6096a..8f3dd84 100644 --- a/risks/apps.py +++ b/risks/apps.py @@ -1,10 +1,10 @@ from django.apps import AppConfig - +from django.utils.translation import gettext_lazy as _ class RisksConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'risks' + default_auto_field = "django.db.models.BigAutoField" + name = "risks" + verbose_name = _("Risk Management") def ready(self): - # Import signals when app is ready import risks.signals \ No newline at end of file diff --git a/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py b/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py new file mode 100644 index 0000000..1b34164 --- /dev/null +++ b/risks/migrations/0018_alter_auditlog_options_alter_control_options_and_more.py @@ -0,0 +1,108 @@ +# Generated by Django 5.2.6 on 2025-09-09 11:53 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0017_alter_incident_status"), + ] + + operations = [ + migrations.AlterModelOptions( + name="auditlog", + options={"verbose_name": "Auditlog", "verbose_name_plural": "Auditlogs"}, + ), + migrations.AlterModelOptions( + name="control", + options={"verbose_name": "Control", "verbose_name_plural": "Controls"}, + ), + migrations.AlterModelOptions( + name="residualrisk", + options={ + "verbose_name": "Residual Risk", + "verbose_name_plural": "Residual Risks", + }, + ), + migrations.AlterModelOptions( + name="risk", + options={"verbose_name": "Risk", "verbose_name_plural": "Risks"}, + ), + migrations.AlterField( + model_name="control", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="incident", + name="date_reported", + field=models.DateField(blank=True, null=True, verbose_name="Date reported"), + ), + migrations.AlterField( + model_name="incident", + name="description", + field=models.TextField(blank=True, null=True, verbose_name="Description"), + ), + migrations.AlterField( + model_name="incident", + name="reported_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="incidents", + to=settings.AUTH_USER_MODEL, + verbose_name="Reported by", + ), + ), + migrations.AlterField( + model_name="incident", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="risk", + name="asset", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Asset" + ), + ), + migrations.AlterField( + model_name="risk", + name="category", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Category" + ), + ), + migrations.AlterField( + model_name="risk", + name="created_at", + field=models.DateTimeField(auto_now_add=True, verbose_name="Created at"), + ), + migrations.AlterField( + model_name="risk", + name="description", + field=models.TextField( + blank=True, max_length=225, null=True, verbose_name="Description" + ), + ), + migrations.AlterField( + model_name="risk", + name="process", + field=models.CharField( + blank=True, max_length=255, null=True, verbose_name="Process" + ), + ), + migrations.AlterField( + model_name="risk", + name="title", + field=models.CharField(max_length=255, verbose_name="Title"), + ), + migrations.AlterField( + model_name="risk", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="Updated at"), + ), + ] diff --git a/risks/migrations/0019_alter_incident_options.py b/risks/migrations/0019_alter_incident_options.py new file mode 100644 index 0000000..818f699 --- /dev/null +++ b/risks/migrations/0019_alter_incident_options.py @@ -0,0 +1,16 @@ +# Generated by Django 5.2.6 on 2025-09-09 11:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0018_alter_auditlog_options_alter_control_options_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="incident", + options={"verbose_name": "Incident", "verbose_name_plural": "Incidents"}, + ), + ] diff --git a/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py b/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py new file mode 100644 index 0000000..6fab0b5 --- /dev/null +++ b/risks/migrations/0020_alter_residualrisk_impact_alter_risk_impact.py @@ -0,0 +1,40 @@ +# Generated by Django 5.2.6 on 2025-09-09 12:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("risks", "0019_alter_incident_options"), + ] + + operations = [ + migrations.AlterField( + model_name="residualrisk", + name="impact", + field=models.IntegerField( + choices=[ + (1, "Very Low (< 1,000 € – minor operational impact)"), + (2, "Low (1,000–5,000 € – local impact)"), + (3, "High (5,000–15,000 € – team-level impact)"), + (4, "Severe (50,000–100,000 € – regional impact)"), + (5, "Critical (> 100,000 € – existential threat)"), + ], + default=1, + ), + ), + migrations.AlterField( + model_name="risk", + name="impact", + field=models.IntegerField( + choices=[ + (1, "Very Low (< 1,000 € – minor operational impact)"), + (2, "Low (1,000–5,000 € – local impact)"), + (3, "High (5,000–15,000 € – team-level impact)"), + (4, "Severe (50,000–100,000 € – regional impact)"), + (5, "Critical (> 100,000 € – existential threat)"), + ], + default=1, + ), + ), + ] diff --git a/risks/models.py b/risks/models.py index 6d5ace5..07c739a 100644 --- a/risks/models.py +++ b/risks/models.py @@ -2,6 +2,7 @@ from django.conf import settings from django.contrib.auth.models import AbstractUser from django.core.serializers.json import DjangoJSONEncoder from django.db import models +from django.utils.translation import gettext_lazy as _ from multiselectfield import MultiSelectField import datetime import json @@ -29,39 +30,38 @@ class User(AbstractUser): return self.responsible_controls.all() class Risk(models.Model): - """ - Represents an information security risk. - """ + + class Meta: + verbose_name = _("Risk") + verbose_name_plural = _("Risks") LIKELIHOOD_CHOICES = [ - (1, "Very low – occurs less than once every 5 years"), - (2, "Low – once every 1–5 years"), - (3, "Likely – once per year or more"), - (4, "Very likely – multiple times per year/monthly"), + (1, _("Very low – occurs less than once every 5 years")), + (2, _("Low – once every 1–5 years")), + (3, _("Likely – once per year or more")), + (4, _("Very likely – multiple times per year/monthly")), ] - IMPACT_CHOICES = [ - (1, "Low (< 1,000 € – minor operational impact)"), - (2, "Medium (1,000–5,000 € – local impact)"), - (3, "High (5,000–15,000 € – team-level impact)"), - (4, "Severe (50,000–100,000 € – regional impact)"), - (5, "Critical (> 100,000 € – existential threat)"), + (1, _("Very Low (< 1,000 € – minor operational impact)")), + (2, _("Low (1,000–5,000 € – local impact)")), + (3, _("High (5,000–15,000 € – team-level impact)")), + (4, _("Severe (50,000–100,000 € – regional impact)")), + (5, _("Critical (> 100,000 € – existential threat)")), ] - CIA_CHOICES = [ - ("1", "Confidentiality"), - ("2", "Integrity"), - ("3", "Availability") + ("1", _("Confidentiality")), + ("2", _("Integrity")), + ("3", _("Availability")), ] # Basic information - title = models.CharField(max_length=255) - description = models.TextField(max_length=225, blank=True, null=True) - asset = models.CharField(max_length=255, blank=True, null=True) - process = models.CharField(max_length=255, blank=True, null=True) - category = models.CharField(max_length=255, blank=True, null=True) - created_at = models.DateTimeField(auto_now_add=True,) - updated_at = models.DateTimeField(auto_now=True) + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description"), max_length=225, blank=True, null=True) + asset = models.CharField(_("Asset"), max_length=255, blank=True, null=True) + process = models.CharField(_("Process"), max_length=255, blank=True, null=True) + category = models.CharField(_("Category"), max_length=255, blank=True, null=True) + created_at = models.DateTimeField(_("Created at"), auto_now_add=True) + updated_at = models.DateTimeField(_("Updated at"), auto_now=True) # CIA Protection Goals cia = MultiSelectField(choices=CIA_CHOICES, max_length=100, blank=True, null=True) @@ -115,6 +115,10 @@ class ResidualRisk(models.Model): Residual Risk after implementing controls """ + class Meta: + verbose_name = _("Residual Risk") + verbose_name_plural = _("Residual Risks") + risk = models.OneToOneField( Risk, on_delete=models.CASCADE, @@ -167,15 +171,19 @@ class Control(models.Model): """ A security control/measure linked to a risk. """ + class Meta: + verbose_name = _("Control") + verbose_name_plural = _("Controls") + STATUS_CHOICES = [ - ("planned", "Planned"), - ("in_progress", "In progress"), - ("completed", "Completed"), - ("verified", "Verified"), - ("rejected", "Rejected"), + ("planned", _("Planned")), + ("in_progress", _("In progress")), + ("completed", _("Completed")), + ("verified", _("Verified")), + ("rejected", _("Rejected")), ] - title = models.CharField(max_length=255) + title = models.CharField(_("Title"), max_length=255) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="planned") due_date = models.DateField(blank=True, null=True) responsible = models.ForeignKey( @@ -199,6 +207,11 @@ class AuditLog(models.Model): """ Generic audit log entry for tracking changes. """ + + class Meta: + verbose_name = _("Auditlog") + verbose_name_plural = _("Auditlogs") + ACTION_CHOICES = [ ("create", "Created"), ("update", "Updated"), @@ -225,15 +238,23 @@ class Incident(models.Model): """ Incidents and related risks """ + + class Meta: + verbose_name = _("Incident") + verbose_name_plural = _("Incidents") + STATUS_CHOICES = [ - ("open", "Opened"), - ("in_progress", "In Progress"), - ("closed", "Closed"), + ("open", _("Opened")), + ("in_progress", _("In Progress")), + ("closed", _("Closed")), ] - title = models.CharField(max_length=255) - description = models.TextField(blank=True, null=True) - date_reported = models.DateField(blank=True, null=True) - reported_by = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents") + title = models.CharField(_("Title"), max_length=255) + description = models.TextField(_("Description"), blank=True, null=True) + date_reported = models.DateField(_("Date reported"), blank=True, null=True) + reported_by = models.ForeignKey( + settings.AUTH_USER_MODEL, verbose_name=_("Reported by"), + null=True, blank=True, on_delete=models.SET_NULL, related_name="incidents" + ) status = models.CharField(max_length=12, choices=STATUS_CHOICES) related_risks = models.ManyToManyField("Risk", blank=True, related_name="incidents") created_at = models.DateTimeField(auto_now_add=True,) diff --git a/risks/templatetags/risk_extras.py b/risks/templatetags/risk_extras.py index ed5945d..524b0f0 100644 --- a/risks/templatetags/risk_extras.py +++ b/risks/templatetags/risk_extras.py @@ -7,3 +7,60 @@ register = template.Library() def cia_label(value): mapping = dict(Risk.CIA_CHOICES) return mapping.get(value, value) + +LIKELIHOOD_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", +} + +IMPACT_MAP = { + 1: "is-control-verylow", + 2: "is-control-low", + 3: "is-control-mid", + 4: "is-control-high", + 5: "is-control-veryhigh", +} + +LEVEL_MAP = { + "Low": "is-control-low", + "Medium": "is-control-mid", + "High": "is-control-high", + "Critical": "is-control-veryhigh", +} + +@register.filter +def likelihood_class(val): + try: + return LIKELIHOOD_MAP.get(int(val), "is-light") + except (TypeError, ValueError): + return "is-light" + +@register.filter +def impact_class(val): + try: + return IMPACT_MAP.get(int(val), "is-light") + except (TypeError, ValueError): + return "is-light" + +@register.filter +def level_class(level): + return LEVEL_MAP.get(str(level), "is-light") + +@register.filter +def score_class(score): + """Score 1..20 → 5 Stufen""" + try: + s = int(score) + except (TypeError, ValueError): + return "is-light" + if s <= 4: + return "is-control-verylow" + if s <= 8: + return "is-control-low" + if s <= 12: + return "is-control-mid" + if s <= 16: + return "is-control-high" + return "is-control-veryhigh" \ No newline at end of file diff --git a/risks/views.py b/risks/views.py index f3dc6f0..fab38da 100644 --- a/risks/views.py +++ b/risks/views.py @@ -1,5 +1,6 @@ from django.contrib.admin.models import LogEntry from django.contrib.auth import get_user_model +from django.contrib.auth.decorators import login_required from django.contrib.contenttypes.models import ContentType from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated @@ -95,12 +96,15 @@ class IncidentViewSet(viewsets.ModelViewSet): # Web # --------------------------------------------------------------------------- +@login_required def dashboard(request): return render(request, "risks/dashboard.html") +@login_required def stats(request): return render(request, "risks/statistics.html") +@login_required def list_risks(request): qs = Risk.objects.all().select_related("owner") @@ -127,16 +131,18 @@ def list_risks(request): "owners": owners, }) +@login_required def show_risk(request, id): - risk = get_object_or_404(Risk, pk=id) + risk = get_object_or_404( + Risk.objects.select_related("residual_risk", "owner").prefetch_related("controls"), + pk=id, + ) ct = ContentType.objects.get_for_model(Risk) - logs = LogEntry.objects.filter( - content_type=ct, - object_id=risk.pk - ).order_by("-action_time") - + logs = LogEntry.objects.filter(content_type=ct, object_id=risk.pk).order_by("-action_time") + return render(request, "risks/item_risk.html", {"risk": risk, "logs": logs}) +@login_required def list_controls(request): qs = Control.objects.all().select_related("responsible") @@ -166,6 +172,7 @@ def list_controls(request): "status_choices": Control.STATUS_CHOICES, }) +@login_required def show_control(request, id): control = get_object_or_404(Control, pk=id) ct = ContentType.objects.get_for_model(Control) @@ -176,7 +183,7 @@ def show_control(request, id): return render(request, "risks/item_control.html", {"control": control, "logs": logs}) - +@login_required def list_incidents(request): qs = Incident.objects.all().select_related("reported_by").prefetch_related("related_risks") @@ -203,6 +210,7 @@ def list_incidents(request): "status_choices": Incident.STATUS_CHOICES, }) +@login_required def show_incident(request, id): incident = get_object_or_404(Incident, pk=id) ct = ContentType.objects.get_for_model(Incident) @@ -211,4 +219,4 @@ def show_incident(request, id): object_id=incident.pk ).order_by("-action_time") - return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) \ No newline at end of file + return render(request, "risks/item_incident.html", {"incident": incident, "logs": logs}) diff --git a/static/css/design.css b/static/css/design.css index a24aa1f..dcbc5d7 100644 --- a/static/css/design.css +++ b/static/css/design.css @@ -1,3 +1,86 @@ +/* Base palette */ +:root{ + --c-verylow:#22c55e; --c-verylow-100:#dcfce7; --c-verylow-300:#86efac; --c-verylow-inv:#fff; + --c-low:#84cc16; --c-low-100:#ecfccb; --c-low-300:#bef264; --c-low-inv:#111; + --c-mid:#eab308; --c-mid-100:#fef9c3; --c-mid-300:#fde047; --c-mid-inv:#111; + --c-high:#f97316; --c-high-100:#ffedd5; --c-high-300:#fbbf24; --c-high-inv:#111; + --c-veryhigh:#dc2626; --c-veryhigh-100:#fee2e2;--c-veryhigh-300:#fca5a5; --c-veryhigh-inv:#fff; +} + +/* Helpers (wie Bulma) */ +.has-text-control-verylow{color:var(--c-verylow)!important} +.has-text-control-low{color:var(--c-low)!important} +.has-text-control-mid{color:var(--c-mid)!important} +.has-text-control-high{color:var(--c-high)!important} +.has-text-control-veryhigh{color:var(--c-veryhigh)!important} + +.has-background-control-verylow{background:var(--c-verylow)!important;color:var(--c-verylow-inv)!important} +.has-background-control-low{background:var(--c-low)!important;color:var(--c-low-inv)!important} +.has-background-control-mid{background:var(--c-mid)!important;color:var(--c-mid-inv)!important} +.has-background-control-high{background:var(--c-high)!important;color:var(--c-high-inv)!important} +.has-background-control-veryhigh{background:var(--c-veryhigh)!important;color:var(--c-veryhigh-inv)!important} + +/* Buttons */ +.button.is-control-verylow{background:var(--c-verylow);border-color:transparent;color:var(--c-verylow-inv)} +.button.is-control-low{background:var(--c-low);border-color:transparent;color:var(--c-low-inv)} +.button.is-control-mid{background:var(--c-mid);border-color:transparent;color:var(--c-mid-inv)} +.button.is-control-high{background:var(--c-high);border-color:transparent;color:var(--c-high-inv)} +.button.is-control-veryhigh{background:var(--c-veryhigh);border-color:transparent;color:var(--c-veryhigh-inv)} +.button.is-control-verylow:hover{filter:brightness(.92)} +.button.is-control-low:hover{filter:brightness(.92)} +.button.is-control-mid:hover{filter:brightness(.92)} +.button.is-control-high:hover{filter:brightness(.92)} +.button.is-control-veryhigh:hover{filter:brightness(.92)} +.button.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)} +.button.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)} +.button.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)} +.button.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)} +.button.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)} + +/* Tags */ +.tag.is-control-verylow{background:var(--c-verylow);color:var(--c-verylow-inv)} +.tag.is-control-low{background:var(--c-low);color:var(--c-low-inv)} +.tag.is-control-mid{background:var(--c-mid);color:var(--c-mid-inv)} +.tag.is-control-high{background:var(--c-high);color:var(--c-high-inv)} +.tag.is-control-veryhigh{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)} +.tag.is-control-verylow.is-light{background:var(--c-verylow-100);color:var(--c-verylow)} +.tag.is-control-low.is-light{background:var(--c-low-100);color:var(--c-low)} +.tag.is-control-mid.is-light{background:var(--c-mid-100);color:var(--c-mid)} +.tag.is-control-high.is-light{background:var(--c-high-100);color:var(--c-high)} +.tag.is-control-veryhigh.is-light{background:var(--c-veryhigh-100);color:var(--c-veryhigh)} + +/* Notifications */ +.notification.is-control-verylow{background:var(--c-verylow-100);border-left:4px solid var(--c-verylow);color:#111} +.notification.is-control-low{background:var(--c-low-100);border-left:4px solid var(--c-low);color:#111} +.notification.is-control-mid{background:var(--c-mid-100);border-left:4px solid var(--c-mid);color:#111} +.notification.is-control-high{background:var(--c-high-100);border-left:4px solid var(--c-high);color:#111} +.notification.is-control-veryhigh{background:var(--c-veryhigh-100);border-left:4px solid var(--c-veryhigh);color:#111} + +/* Messages */ +.message.is-control-verylow .message-header{background:var(--c-verylow);color:var(--c-verylow-inv)} +.message.is-control-low .message-header{background:var(--c-low);color:var(--c-low-inv)} +.message.is-control-mid .message-header{background:var(--c-mid);color:var(--c-mid-inv)} +.message.is-control-high .message-header{background:var(--c-high);color:var(--c-high-inv)} +.message.is-control-veryhigh .message-header{background:var(--c-veryhigh);color:var(--c-veryhigh-inv)} +.message.is-control-verylow .message-body{border-color:var(--c-verylow-300)} +.message.is-control-low .message-body{border-color:var(--c-low-300)} +.message.is-control-mid .message-body{border-color:var(--c-mid-300)} +.message.is-control-high .message-body{border-color:var(--c-high-300)} +.message.is-control-veryhigh .message-body{border-color:var(--c-veryhigh-300)} + +/* Progress (optional) */ +.progress.is-control-verylow::-webkit-progress-value{background:var(--c-verylow)} +.progress.is-control-low::-webkit-progress-value{background:var(--c-low)} +.progress.is-control-mid::-webkit-progress-value{background:var(--c-mid)} +.progress.is-control-high::-webkit-progress-value{background:var(--c-high)} +.progress.is-control-veryhigh::-webkit-progress-value{background:var(--c-veryhigh)} +.progress.is-control-verylow::-moz-progress-bar{background:var(--c-verylow)} +.progress.is-control-low::-moz-progress-bar{background:var(--c-low)} +.progress.is-control-mid::-moz-progress-bar{background:var(--c-mid)} +.progress.is-control-high::-moz-progress-bar{background:var(--c-high)} +.progress.is-control-veryhigh::-moz-progress-bar{background:var(--c-veryhigh)} + + /* Topbar-Farbe erzwingen (Bulma überschreibt sonst mit weiß) */ .navbar.topbar-nav { background-color: #d6801e !important; /* Orange wie im Screenshot */ diff --git a/templates/admin/base_site.html b/templates/admin/base_site.html new file mode 100644 index 0000000..70747f6 --- /dev/null +++ b/templates/admin/base_site.html @@ -0,0 +1,24 @@ +{# templates/admin/base_site.html #} +{% extends "admin/base.html" %} +{% load i18n %} + +{% block title %}{{ title }} | {{ site_title }}{% endblock %} + +{% block branding %} +

{{ site_header }}

+{% endblock %} + +{% block usertools %} +{{ block.super }} +
+ {% csrf_token %} + + +
+{% endblock %} diff --git a/templates/base.html b/templates/base.html index 6112d79..e9fea9d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -47,14 +47,60 @@ +