Free and open source ticket system written in python

feat: move ticket attachments storage

Signed-off-by: A. Ottr <alex@otter.foo>

+618 -239
+1
.gitignore
··· 156 156 157 157 static/* 158 158 media/* 159 + secure_media/* 159 160 .DS_Store 160 161 !paw/locale/**/django.mo 161 162 .vscode
+22
compose.yaml
··· 1 + version: "3.8" 2 + services: 3 + paw: 4 + build: . 5 + container_name: paw-ticket-system 6 + command: gunicorn paw.wsgi:application --bind 0.0.0.0:8000 7 + restart: unless-stopped 8 + ports: 9 + - "127.0.0.1:8000:8000" 10 + volumes: 11 + - ./data:/usr/src/app/data 12 + - media:/usr/src/app/media 13 + - secure_media:/usr/src/app/secure_media 14 + environment: 15 + - DATABASE_ENGINE=sqlite3 16 + - DEBUG=true 17 + - ALLOWED_HOSTS=localhost,127.0.0.1 18 + - SECRET_KEY=your-secret-key 19 + 20 + volumes: 21 + media: 22 + secure_media:
+3 -1
docs/docker_compose_deployment.md
··· 21 21 volumes: 22 22 - db:/usr/src/app/data 23 23 - media:/usr/src/app/media 24 + - secure_media:/usr/src/app/secure_media # storage for ticket attachments 24 25 environment: 25 26 - DATABASE_ENGINE=sqlite3 26 27 - DEBUG=true ··· 41 42 - /opt/paw/data:/usr/src/app/data 42 43 - /opt/paw/media:/usr/src/app/media 43 44 - /opt/paw/static:/usr/src/app/static 45 + - /opt/paw/secure_media:/usr/src/app/secure_media # storage for ticket attachments 44 46 ``` 45 47 46 48 Now you write directives in your config to host these files, the following snipped shows an example nginx config: ··· 59 61 listen [::]:80; 60 62 61 63 location /media/ { 62 - # media files, uploaded by us 64 + # media files, uploaded by us (profile pictures, etc.) 63 65 alias /opt/paw/media/; # ending slash is required 64 66 } 65 67
paw/locale/de/LC_MESSAGES/django.mo

This is a binary file and will not be displayed.

+90 -58
paw/locale/de/LC_MESSAGES/django.po
··· 8 8 msgstr "" 9 9 "Project-Id-Version: PACKAGE VERSION\n" 10 10 "Report-Msgid-Bugs-To: \n" 11 - "POT-Creation-Date: 2025-08-09 01:16+0000\n" 11 + "POT-Creation-Date: 2026-02-22 00:58+0000\n" 12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 14 14 "Language-Team: LANGUAGE <LL@li.org>\n" ··· 18 18 "Content-Transfer-Encoding: 8bit\n" 19 19 "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 20 21 - #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:33 22 - #: paw/templates/core/login.html:25 paw/templates/core/register.html:24 21 + #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:32 22 + #: paw/templates/core/login.html:23 paw/templates/core/register.html:23 23 23 msgid "Username" 24 24 msgstr "Benutzername" 25 25 ··· 58 58 msgid "Google SSO User" 59 59 msgstr "" 60 60 61 - #: paw/settings.py:135 61 + #: paw/settings.py:140 62 62 msgid "English" 63 63 msgstr "English" 64 64 65 - #: paw/settings.py:136 65 + #: paw/settings.py:141 66 66 msgid "French" 67 67 msgstr "Français" 68 68 69 - #: paw/settings.py:137 69 + #: paw/settings.py:142 70 70 msgid "German" 71 71 msgstr "Deutsch" 72 72 73 - #: paw/settings.py:138 73 + #: paw/settings.py:143 74 74 msgid "Dutch" 75 75 msgstr "Nederlands" 76 76 77 + #: paw/templates/404.html:9 78 + msgid "Page Not Found" 79 + msgstr "Seite nicht gefunden" 80 + 81 + #: paw/templates/404.html:11 82 + msgid "" 83 + "Sorry, we couldn't find the page you're looking for. The page may have been " 84 + "moved, deleted, or the URL might be incorrect." 85 + msgstr "" 86 + "Entschuldigung, wir konnten die gesuchte Seite nicht finden. Die Seite wurde " 87 + "möglicherweise verschoben, gelöscht oder die URL ist falsch." 88 + 89 + #: paw/templates/404.html:20 paw/templates/500.html:20 90 + msgid "Go Home" 91 + msgstr "Zur Startseite" 92 + 93 + #: paw/templates/404.html:28 paw/templates/500.html:28 94 + msgid "Go to Tickets" 95 + msgstr "Zu den Tickets" 96 + 97 + #: paw/templates/404.html:35 paw/templates/500.html:35 98 + #: paw/templates/dashboard_base.html:35 99 + #: paw/templates/ticketing/create_ticket.html:51 100 + msgid "Create Ticket" 101 + msgstr "Ticket erstellen" 102 + 103 + #: paw/templates/500.html:9 104 + msgid "Internal Server Error" 105 + msgstr "Interner Serverfehler" 106 + 107 + #: paw/templates/500.html:11 108 + msgid "" 109 + "We're sorry, but something went wrong on our end. Please try again later." 110 + msgstr "" 111 + "Entschuldigung, auf unserer Seite ist etwas schiefgelaufen. Bitte versuchen " 112 + "Sie es später erneut." 113 + 77 114 #: paw/templates/base.html:20 paw/templates/dashboard_base.html:41 78 115 msgid "Tickets" 79 116 msgstr "Tickets" ··· 99 136 msgid "Done" 100 137 msgstr "Fertig" 101 138 102 - #: paw/templates/core/account_finish.html:38 103 - #: paw/templates/core/settings.html:55 139 + #: paw/templates/core/account_finish.html:36 140 + #: paw/templates/core/settings.html:51 104 141 msgid "Save" 105 142 msgstr "Speichern" 106 143 107 - #: paw/templates/core/login.html:7 paw/templates/core/login.html:40 144 + #: paw/templates/core/login.html:7 paw/templates/core/login.html:32 108 145 msgid "Log In" 109 146 msgstr "Anmelden" 110 147 ··· 112 149 msgid "Register Account" 113 150 msgstr "Account erstellen" 114 151 115 - #: paw/templates/core/login.html:31 paw/templates/core/register.html:36 152 + #: paw/templates/core/login.html:27 paw/templates/core/register.html:32 116 153 msgid "Password" 117 154 msgstr "Passwort" 118 155 119 - #: paw/templates/core/login.html:36 156 + #: paw/templates/core/login.html:29 120 157 msgid "Password Reset" 121 158 msgstr "Passwort zurücksetzen" 122 159 123 - #: paw/templates/core/login.html:47 160 + #: paw/templates/core/login.html:39 124 161 msgid "Log in with Google" 125 162 msgstr "Mit Google anmelden" 126 163 127 - #: paw/templates/core/register.html:30 164 + #: paw/templates/core/register.html:28 128 165 msgid "Email Address" 129 166 msgstr "Mail Adresse" 130 167 131 - #: paw/templates/core/register.html:42 168 + #: paw/templates/core/register.html:36 132 169 msgid "Confirm Password" 133 170 msgstr "Passwort bestätigen" 134 171 135 - #: paw/templates/core/register.html:47 172 + #: paw/templates/core/register.html:40 136 173 msgid "Register" 137 174 msgstr "Account erstellen" 138 175 ··· 140 177 msgid "Settings" 141 178 msgstr "Einstellungen" 142 179 143 - #: paw/templates/core/settings.html:12 180 + #: paw/templates/core/settings.html:11 144 181 msgid "Mail Address" 145 182 msgstr "Mail Adresse" 146 183 147 - #: paw/templates/core/settings.html:23 184 + #: paw/templates/core/settings.html:19 148 185 msgid "Language" 149 186 msgstr "Sprache" 150 187 151 - #: paw/templates/core/settings.html:30 188 + #: paw/templates/core/settings.html:25 152 189 msgid "Use Darkmode" 153 190 msgstr "Darkmode benutzen" 154 191 155 - #: paw/templates/core/settings.html:37 192 + #: paw/templates/core/settings.html:31 156 193 msgid "Receive Email Notifications" 157 194 msgstr "Mail-Benachrichtigungen erhalten" 158 195 159 - #: paw/templates/core/settings.html:44 196 + #: paw/templates/core/settings.html:37 160 197 msgid "Profile Picture" 161 198 msgstr "Profilbild" 162 199 163 - #: paw/templates/core/settings.html:49 164 - #: paw/templates/ticketing/ticket_detail.html:152 200 + #: paw/templates/core/settings.html:42 201 + #: paw/templates/ticketing/ticket_detail.html:154 165 202 msgid "Contact" 166 203 msgstr "Kontakt" 167 204 168 - #: paw/templates/dashboard_base.html:35 169 - #: paw/templates/ticketing/create_ticket.html:52 170 - msgid "Create Ticket" 171 - msgstr "Ticket erstellen" 172 - 173 205 #: paw/templates/dashboard_base.html:47 174 206 #: paw/templates/ticketing/tickets_history.html:5 175 207 msgid "History" ··· 184 216 msgstr "Abmelden" 185 217 186 218 #: paw/templates/partials/assigned_to.html:17 187 - #: paw/templates/ticketing/ticket_detail.html:201 219 + #: paw/templates/ticketing/ticket_detail.html:203 188 220 msgid "Unassigned" 189 221 msgstr "Nicht zugewiesen" 190 222 191 - #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:45 223 + #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:47 192 224 msgid "Low" 193 225 msgstr "Niedrig" 194 226 195 - #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:46 227 + #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:48 196 228 msgid "Medium" 197 229 msgstr "Mittel" 198 230 199 - #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:47 231 + #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:49 200 232 msgid "High" 201 233 msgstr "Hoch" 202 234 203 - #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:40 235 + #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:42 204 236 msgid "Open" 205 237 msgstr "Offen" 206 238 207 - #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:41 239 + #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:43 208 240 msgid "In Progress" 209 241 msgstr "In Bearbeitung" 210 242 211 - #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:42 243 + #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:44 212 244 msgid "Closed" 213 245 msgstr "Geschlossen" 214 246 ··· 216 248 msgid "Create a new ticket" 217 249 msgstr "Neues Ticket erstellen" 218 250 219 - #: paw/templates/ticketing/create_ticket.html:12 220 - #: paw/templates/ticketing/ticket_detail.html:165 251 + #: paw/templates/ticketing/create_ticket.html:11 252 + #: paw/templates/ticketing/ticket_detail.html:167 221 253 #: paw/templates/ticketing/tickets.html:19 222 254 #: paw/templates/ticketing/tickets_history.html:19 223 255 msgid "Category" 224 256 msgstr "Kategorie" 225 257 226 - #: paw/templates/ticketing/create_ticket.html:19 258 + #: paw/templates/ticketing/create_ticket.html:15 227 259 #: paw/templates/ticketing/tickets.html:17 228 260 #: paw/templates/ticketing/tickets_history.html:17 ticketing/forms.py:73 229 261 msgid "Title" 230 262 msgstr "Titel" 231 263 232 - #: paw/templates/ticketing/create_ticket.html:26 ticketing/forms.py:74 264 + #: paw/templates/ticketing/create_ticket.html:23 ticketing/forms.py:74 233 265 msgid "Description" 234 266 msgstr "Beschreibung" 235 267 236 - #: paw/templates/ticketing/create_ticket.html:34 268 + #: paw/templates/ticketing/create_ticket.html:31 237 269 #: paw/templates/ticketing/ticket_detail.html:9 238 270 msgid "Attachments" 239 271 msgstr "Anhänge" 240 272 241 - #: paw/templates/ticketing/create_ticket.html:43 273 + #: paw/templates/ticketing/create_ticket.html:40 242 274 msgid "Create as follow-up to a closed ticket" 243 275 msgstr "Als Nachfolge-Ticket zu einem geschlossenen Ticket erstellen" 244 276 ··· 266 298 msgid "Apply Template" 267 299 msgstr "Vorlage anwenden" 268 300 269 - #: paw/templates/ticketing/ticket_detail.html:96 301 + #: paw/templates/ticketing/ticket_detail.html:98 270 302 msgid "Add Comment" 271 303 msgstr "Kommentar hinzufügen" 272 304 273 - #: paw/templates/ticketing/ticket_detail.html:98 305 + #: paw/templates/ticketing/ticket_detail.html:100 274 306 msgid "Close Ticket" 275 307 msgstr "Ticket schliessen" 276 308 277 - #: paw/templates/ticketing/ticket_detail.html:107 309 + #: paw/templates/ticketing/ticket_detail.html:109 278 310 msgid "Make this an internal comment" 279 311 msgstr "Mache dies einen internen Kommentar" 280 312 281 - #: paw/templates/ticketing/ticket_detail.html:116 313 + #: paw/templates/ticketing/ticket_detail.html:118 282 314 msgid "Ticket has been closed" 283 315 msgstr "Das Ticket wurde geschlossen" 284 316 285 - #: paw/templates/ticketing/ticket_detail.html:121 317 + #: paw/templates/ticketing/ticket_detail.html:123 286 318 msgid "Re-Open Ticket" 287 319 msgstr "Ticket wieder öffnen" 288 320 289 - #: paw/templates/ticketing/ticket_detail.html:132 321 + #: paw/templates/ticketing/ticket_detail.html:134 290 322 msgid "Created by" 291 323 msgstr "Erstellt von" 292 324 293 - #: paw/templates/ticketing/ticket_detail.html:145 325 + #: paw/templates/ticketing/ticket_detail.html:147 294 326 msgid "Created on" 295 327 msgstr "Erstellt am" 296 328 297 - #: paw/templates/ticketing/ticket_detail.html:149 329 + #: paw/templates/ticketing/ticket_detail.html:151 298 330 msgid "Last updated" 299 331 msgstr "Zuletzt aktualisiert" 300 332 301 - #: paw/templates/ticketing/ticket_detail.html:171 333 + #: paw/templates/ticketing/ticket_detail.html:173 302 334 #: paw/templates/ticketing/tickets.html:42 303 335 #: paw/templates/ticketing/tickets_history.html:42 ticketing/forms.py:81 304 - #: ticketing/forms.py:104 ticketing/models.py:165 ticketing/models.py:178 336 + #: ticketing/forms.py:104 ticketing/models.py:167 ticketing/models.py:180 305 337 msgid "General" 306 338 msgstr "Allgemein" 307 339 308 - #: paw/templates/ticketing/ticket_detail.html:177 340 + #: paw/templates/ticketing/ticket_detail.html:179 309 341 msgid "Assign to new category" 310 342 msgstr "Kategorie zuweisen" 311 343 312 - #: paw/templates/ticketing/ticket_detail.html:180 313 - #: paw/templates/ticketing/ticket_detail.html:210 344 + #: paw/templates/ticketing/ticket_detail.html:182 345 + #: paw/templates/ticketing/ticket_detail.html:212 314 346 msgid "Assign" 315 347 msgstr "Zuweisen" 316 348 317 - #: paw/templates/ticketing/ticket_detail.html:185 349 + #: paw/templates/ticketing/ticket_detail.html:187 318 350 msgid "Assignees" 319 351 msgstr "Bearbeiter" 320 352 321 - #: paw/templates/ticketing/ticket_detail.html:192 353 + #: paw/templates/ticketing/ticket_detail.html:194 322 354 msgid "Assign to me" 323 355 msgstr "Weise mir zu" 324 356 325 - #: paw/templates/ticketing/ticket_detail.html:207 357 + #: paw/templates/ticketing/ticket_detail.html:209 326 358 msgid "Assign to new team" 327 359 msgstr "Team zuweisen" 328 360 ··· 410 442 msgid "No Team" 411 443 msgstr "Kein Team" 412 444 413 - #: ticketing/models.py:31 445 + #: ticketing/models.py:33 414 446 msgid "" 415 447 "If a team is selected, new tickets will automatically assigned to this team." 416 448 msgstr ""
+86 -58
paw/locale/en/LC_MESSAGES/django.po
··· 8 8 msgstr "" 9 9 "Project-Id-Version: PACKAGE VERSION\n" 10 10 "Report-Msgid-Bugs-To: \n" 11 - "POT-Creation-Date: 2025-08-09 01:16+0000\n" 11 + "POT-Creation-Date: 2026-02-22 00:57+0000\n" 12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 14 14 "Language-Team: LANGUAGE <LL@li.org>\n" ··· 18 18 "Content-Transfer-Encoding: 8bit\n" 19 19 "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 20 21 - #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:33 22 - #: paw/templates/core/login.html:25 paw/templates/core/register.html:24 21 + #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:32 22 + #: paw/templates/core/login.html:23 paw/templates/core/register.html:23 23 23 msgid "Username" 24 24 msgstr "" 25 25 ··· 57 57 msgid "Google SSO User" 58 58 msgstr "" 59 59 60 - #: paw/settings.py:135 60 + #: paw/settings.py:140 61 61 msgid "English" 62 62 msgstr "English" 63 63 64 - #: paw/settings.py:136 64 + #: paw/settings.py:141 65 65 msgid "French" 66 66 msgstr "Français" 67 67 68 - #: paw/settings.py:137 68 + #: paw/settings.py:142 69 69 msgid "German" 70 70 msgstr "Deutsch" 71 71 72 - #: paw/settings.py:138 72 + #: paw/settings.py:143 73 73 msgid "Dutch" 74 74 msgstr "Nederlands" 75 75 76 + #: paw/templates/404.html:9 77 + msgid "Page Not Found" 78 + msgstr "" 79 + 80 + #: paw/templates/404.html:11 81 + msgid "" 82 + "Sorry, we couldn't find the page you're looking for. The page may have been " 83 + "moved, deleted, or the URL might be incorrect." 84 + msgstr "" 85 + 86 + #: paw/templates/404.html:20 paw/templates/500.html:20 87 + msgid "Go Home" 88 + msgstr "" 89 + 90 + #: paw/templates/404.html:28 paw/templates/500.html:28 91 + msgid "Go to Tickets" 92 + msgstr "" 93 + 94 + #: paw/templates/404.html:35 paw/templates/500.html:35 95 + #: paw/templates/dashboard_base.html:35 96 + #: paw/templates/ticketing/create_ticket.html:51 97 + msgid "Create Ticket" 98 + msgstr "" 99 + 100 + #: paw/templates/500.html:9 101 + msgid "Internal Server Error" 102 + msgstr "" 103 + 104 + #: paw/templates/500.html:11 105 + msgid "" 106 + "We're sorry, but something went wrong on our end. Please try again later." 107 + msgstr "" 108 + 76 109 #: paw/templates/base.html:20 paw/templates/dashboard_base.html:41 77 110 msgid "Tickets" 78 111 msgstr "" ··· 98 131 msgid "Done" 99 132 msgstr "" 100 133 101 - #: paw/templates/core/account_finish.html:38 102 - #: paw/templates/core/settings.html:55 134 + #: paw/templates/core/account_finish.html:36 135 + #: paw/templates/core/settings.html:51 103 136 msgid "Save" 104 137 msgstr "" 105 138 106 - #: paw/templates/core/login.html:7 paw/templates/core/login.html:40 139 + #: paw/templates/core/login.html:7 paw/templates/core/login.html:32 107 140 msgid "Log In" 108 141 msgstr "" 109 142 ··· 111 144 msgid "Register Account" 112 145 msgstr "" 113 146 114 - #: paw/templates/core/login.html:31 paw/templates/core/register.html:36 147 + #: paw/templates/core/login.html:27 paw/templates/core/register.html:32 115 148 msgid "Password" 116 149 msgstr "" 117 150 118 - #: paw/templates/core/login.html:36 151 + #: paw/templates/core/login.html:29 119 152 msgid "Password Reset" 120 153 msgstr "" 121 154 122 - #: paw/templates/core/login.html:47 155 + #: paw/templates/core/login.html:39 123 156 msgid "Log in with Google" 124 157 msgstr "" 125 158 126 - #: paw/templates/core/register.html:30 159 + #: paw/templates/core/register.html:28 127 160 msgid "Email Address" 128 161 msgstr "" 129 162 130 - #: paw/templates/core/register.html:42 163 + #: paw/templates/core/register.html:36 131 164 msgid "Confirm Password" 132 165 msgstr "" 133 166 134 - #: paw/templates/core/register.html:47 167 + #: paw/templates/core/register.html:40 135 168 msgid "Register" 136 169 msgstr "" 137 170 ··· 139 172 msgid "Settings" 140 173 msgstr "Settings" 141 174 142 - #: paw/templates/core/settings.html:12 175 + #: paw/templates/core/settings.html:11 143 176 msgid "Mail Address" 144 177 msgstr "" 145 178 146 - #: paw/templates/core/settings.html:23 179 + #: paw/templates/core/settings.html:19 147 180 msgid "Language" 148 181 msgstr "" 149 182 150 - #: paw/templates/core/settings.html:30 183 + #: paw/templates/core/settings.html:25 151 184 msgid "Use Darkmode" 152 185 msgstr "" 153 186 154 - #: paw/templates/core/settings.html:37 187 + #: paw/templates/core/settings.html:31 155 188 msgid "Receive Email Notifications" 156 189 msgstr "" 157 190 158 - #: paw/templates/core/settings.html:44 191 + #: paw/templates/core/settings.html:37 159 192 msgid "Profile Picture" 160 193 msgstr "" 161 194 162 - #: paw/templates/core/settings.html:49 163 - #: paw/templates/ticketing/ticket_detail.html:152 195 + #: paw/templates/core/settings.html:42 196 + #: paw/templates/ticketing/ticket_detail.html:154 164 197 msgid "Contact" 165 198 msgstr "" 166 199 167 - #: paw/templates/dashboard_base.html:35 168 - #: paw/templates/ticketing/create_ticket.html:52 169 - msgid "Create Ticket" 170 - msgstr "" 171 - 172 200 #: paw/templates/dashboard_base.html:47 173 201 #: paw/templates/ticketing/tickets_history.html:5 174 202 msgid "History" ··· 183 211 msgstr "" 184 212 185 213 #: paw/templates/partials/assigned_to.html:17 186 - #: paw/templates/ticketing/ticket_detail.html:201 214 + #: paw/templates/ticketing/ticket_detail.html:203 187 215 msgid "Unassigned" 188 216 msgstr "" 189 217 190 - #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:45 218 + #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:47 191 219 msgid "Low" 192 220 msgstr "" 193 221 194 - #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:46 222 + #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:48 195 223 msgid "Medium" 196 224 msgstr "" 197 225 198 - #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:47 226 + #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:49 199 227 msgid "High" 200 228 msgstr "" 201 229 202 - #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:40 230 + #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:42 203 231 msgid "Open" 204 232 msgstr "" 205 233 206 - #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:41 234 + #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:43 207 235 msgid "In Progress" 208 236 msgstr "" 209 237 210 - #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:42 238 + #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:44 211 239 msgid "Closed" 212 240 msgstr "" 213 241 ··· 215 243 msgid "Create a new ticket" 216 244 msgstr "" 217 245 218 - #: paw/templates/ticketing/create_ticket.html:12 219 - #: paw/templates/ticketing/ticket_detail.html:165 246 + #: paw/templates/ticketing/create_ticket.html:11 247 + #: paw/templates/ticketing/ticket_detail.html:167 220 248 #: paw/templates/ticketing/tickets.html:19 221 249 #: paw/templates/ticketing/tickets_history.html:19 222 250 msgid "Category" 223 251 msgstr "" 224 252 225 - #: paw/templates/ticketing/create_ticket.html:19 253 + #: paw/templates/ticketing/create_ticket.html:15 226 254 #: paw/templates/ticketing/tickets.html:17 227 255 #: paw/templates/ticketing/tickets_history.html:17 ticketing/forms.py:73 228 256 msgid "Title" 229 257 msgstr "" 230 258 231 - #: paw/templates/ticketing/create_ticket.html:26 ticketing/forms.py:74 259 + #: paw/templates/ticketing/create_ticket.html:23 ticketing/forms.py:74 232 260 msgid "Description" 233 261 msgstr "" 234 262 235 - #: paw/templates/ticketing/create_ticket.html:34 263 + #: paw/templates/ticketing/create_ticket.html:31 236 264 #: paw/templates/ticketing/ticket_detail.html:9 237 265 msgid "Attachments" 238 266 msgstr "" 239 267 240 - #: paw/templates/ticketing/create_ticket.html:43 268 + #: paw/templates/ticketing/create_ticket.html:40 241 269 msgid "Create as follow-up to a closed ticket" 242 270 msgstr "" 243 271 ··· 265 293 msgid "Apply Template" 266 294 msgstr "" 267 295 268 - #: paw/templates/ticketing/ticket_detail.html:96 296 + #: paw/templates/ticketing/ticket_detail.html:98 269 297 msgid "Add Comment" 270 298 msgstr "" 271 299 272 - #: paw/templates/ticketing/ticket_detail.html:98 300 + #: paw/templates/ticketing/ticket_detail.html:100 273 301 msgid "Close Ticket" 274 302 msgstr "" 275 303 276 - #: paw/templates/ticketing/ticket_detail.html:107 304 + #: paw/templates/ticketing/ticket_detail.html:109 277 305 msgid "Make this an internal comment" 278 306 msgstr "" 279 307 280 - #: paw/templates/ticketing/ticket_detail.html:116 308 + #: paw/templates/ticketing/ticket_detail.html:118 281 309 msgid "Ticket has been closed" 282 310 msgstr "" 283 311 284 - #: paw/templates/ticketing/ticket_detail.html:121 312 + #: paw/templates/ticketing/ticket_detail.html:123 285 313 msgid "Re-Open Ticket" 286 314 msgstr "" 287 315 288 - #: paw/templates/ticketing/ticket_detail.html:132 316 + #: paw/templates/ticketing/ticket_detail.html:134 289 317 msgid "Created by" 290 318 msgstr "" 291 319 292 - #: paw/templates/ticketing/ticket_detail.html:145 320 + #: paw/templates/ticketing/ticket_detail.html:147 293 321 msgid "Created on" 294 322 msgstr "" 295 323 296 - #: paw/templates/ticketing/ticket_detail.html:149 324 + #: paw/templates/ticketing/ticket_detail.html:151 297 325 msgid "Last updated" 298 326 msgstr "" 299 327 300 - #: paw/templates/ticketing/ticket_detail.html:171 328 + #: paw/templates/ticketing/ticket_detail.html:173 301 329 #: paw/templates/ticketing/tickets.html:42 302 330 #: paw/templates/ticketing/tickets_history.html:42 ticketing/forms.py:81 303 - #: ticketing/forms.py:104 ticketing/models.py:165 ticketing/models.py:178 331 + #: ticketing/forms.py:104 ticketing/models.py:167 ticketing/models.py:180 304 332 msgid "General" 305 333 msgstr "" 306 334 307 - #: paw/templates/ticketing/ticket_detail.html:177 335 + #: paw/templates/ticketing/ticket_detail.html:179 308 336 msgid "Assign to new category" 309 337 msgstr "" 310 338 311 - #: paw/templates/ticketing/ticket_detail.html:180 312 - #: paw/templates/ticketing/ticket_detail.html:210 339 + #: paw/templates/ticketing/ticket_detail.html:182 340 + #: paw/templates/ticketing/ticket_detail.html:212 313 341 msgid "Assign" 314 342 msgstr "" 315 343 316 - #: paw/templates/ticketing/ticket_detail.html:185 344 + #: paw/templates/ticketing/ticket_detail.html:187 317 345 msgid "Assignees" 318 346 msgstr "" 319 347 320 - #: paw/templates/ticketing/ticket_detail.html:192 348 + #: paw/templates/ticketing/ticket_detail.html:194 321 349 msgid "Assign to me" 322 350 msgstr "" 323 351 324 - #: paw/templates/ticketing/ticket_detail.html:207 352 + #: paw/templates/ticketing/ticket_detail.html:209 325 353 msgid "Assign to new team" 326 354 msgstr "" 327 355 ··· 409 437 msgid "No Team" 410 438 msgstr "" 411 439 412 - #: ticketing/models.py:31 440 + #: ticketing/models.py:33 413 441 msgid "" 414 442 "If a team is selected, new tickets will automatically assigned to this team." 415 443 msgstr ""
paw/locale/fr/LC_MESSAGES/django.mo

This is a binary file and will not be displayed.

+90 -58
paw/locale/fr/LC_MESSAGES/django.po
··· 8 8 msgstr "" 9 9 "Project-Id-Version: PACKAGE VERSION\n" 10 10 "Report-Msgid-Bugs-To: \n" 11 - "POT-Creation-Date: 2025-08-09 01:16+0000\n" 11 + "POT-Creation-Date: 2026-02-22 00:58+0000\n" 12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 14 14 "Language-Team: LANGUAGE <LL@li.org>\n" ··· 18 18 "Content-Transfer-Encoding: 8bit\n" 19 19 "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 20 21 - #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:33 22 - #: paw/templates/core/login.html:25 paw/templates/core/register.html:24 21 + #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:32 22 + #: paw/templates/core/login.html:23 paw/templates/core/register.html:23 23 23 msgid "Username" 24 24 msgstr "Nom d'utilisateur" 25 25 ··· 60 60 msgid "Google SSO User" 61 61 msgstr "Utilisateur Google SSO" 62 62 63 - #: paw/settings.py:135 63 + #: paw/settings.py:140 64 64 msgid "English" 65 65 msgstr "English" 66 66 67 - #: paw/settings.py:136 67 + #: paw/settings.py:141 68 68 msgid "French" 69 69 msgstr "Français" 70 70 71 - #: paw/settings.py:137 71 + #: paw/settings.py:142 72 72 msgid "German" 73 73 msgstr "Deutsch" 74 74 75 - #: paw/settings.py:138 75 + #: paw/settings.py:143 76 76 msgid "Dutch" 77 77 msgstr "Nederlands" 78 78 79 + #: paw/templates/404.html:9 80 + msgid "Page Not Found" 81 + msgstr "Page introuvable" 82 + 83 + #: paw/templates/404.html:11 84 + msgid "" 85 + "Sorry, we couldn't find the page you're looking for. The page may have been " 86 + "moved, deleted, or the URL might be incorrect." 87 + msgstr "" 88 + "Désolé, nous n'avons pas pu trouver la page que vous recherchez. La page a " 89 + "peut-être été déplacée, supprimée ou l'URL est incorrecte." 90 + 91 + #: paw/templates/404.html:20 paw/templates/500.html:20 92 + msgid "Go Home" 93 + msgstr "Retour à l'accueil" 94 + 95 + #: paw/templates/404.html:28 paw/templates/500.html:28 96 + msgid "Go to Tickets" 97 + msgstr "Aller aux tickets" 98 + 99 + #: paw/templates/404.html:35 paw/templates/500.html:35 100 + #: paw/templates/dashboard_base.html:35 101 + #: paw/templates/ticketing/create_ticket.html:51 102 + msgid "Create Ticket" 103 + msgstr "Nouveau ticket" 104 + 105 + #: paw/templates/500.html:9 106 + msgid "Internal Server Error" 107 + msgstr "Erreur interne du serveur" 108 + 109 + #: paw/templates/500.html:11 110 + msgid "" 111 + "We're sorry, but something went wrong on our end. Please try again later." 112 + msgstr "" 113 + "Nous sommes désolés, une erreur s'est produite de notre côté. Veuillez " 114 + "réessayer plus tard." 115 + 79 116 #: paw/templates/base.html:20 paw/templates/dashboard_base.html:41 80 117 msgid "Tickets" 81 118 msgstr "Tickets" ··· 101 138 msgid "Done" 102 139 msgstr "Terminer" 103 140 104 - #: paw/templates/core/account_finish.html:38 105 - #: paw/templates/core/settings.html:55 141 + #: paw/templates/core/account_finish.html:36 142 + #: paw/templates/core/settings.html:51 106 143 msgid "Save" 107 144 msgstr "Enregistrer" 108 145 109 - #: paw/templates/core/login.html:7 paw/templates/core/login.html:40 146 + #: paw/templates/core/login.html:7 paw/templates/core/login.html:32 110 147 msgid "Log In" 111 148 msgstr "Connexion" 112 149 ··· 114 151 msgid "Register Account" 115 152 msgstr "Inscription" 116 153 117 - #: paw/templates/core/login.html:31 paw/templates/core/register.html:36 154 + #: paw/templates/core/login.html:27 paw/templates/core/register.html:32 118 155 msgid "Password" 119 156 msgstr "Mot de passe" 120 157 121 - #: paw/templates/core/login.html:36 158 + #: paw/templates/core/login.html:29 122 159 msgid "Password Reset" 123 160 msgstr "Redéfinir le mot de passe" 124 161 125 - #: paw/templates/core/login.html:47 162 + #: paw/templates/core/login.html:39 126 163 msgid "Log in with Google" 127 164 msgstr "Se connecter avec Google" 128 165 129 - #: paw/templates/core/register.html:30 166 + #: paw/templates/core/register.html:28 130 167 msgid "Email Address" 131 168 msgstr "Adresse e-mail" 132 169 133 - #: paw/templates/core/register.html:42 170 + #: paw/templates/core/register.html:36 134 171 msgid "Confirm Password" 135 172 msgstr "Confirmer le mot de passe" 136 173 137 - #: paw/templates/core/register.html:47 174 + #: paw/templates/core/register.html:40 138 175 msgid "Register" 139 176 msgstr "S'enregistrer" 140 177 ··· 142 179 msgid "Settings" 143 180 msgstr "Paramètres" 144 181 145 - #: paw/templates/core/settings.html:12 182 + #: paw/templates/core/settings.html:11 146 183 msgid "Mail Address" 147 184 msgstr "Adresse e-mail" 148 185 149 - #: paw/templates/core/settings.html:23 186 + #: paw/templates/core/settings.html:19 150 187 msgid "Language" 151 188 msgstr "Langue" 152 189 153 - #: paw/templates/core/settings.html:30 190 + #: paw/templates/core/settings.html:25 154 191 msgid "Use Darkmode" 155 192 msgstr "Utiliser le mode sombre" 156 193 157 - #: paw/templates/core/settings.html:37 194 + #: paw/templates/core/settings.html:31 158 195 msgid "Receive Email Notifications" 159 196 msgstr "Me notifier par e-mail" 160 197 161 - #: paw/templates/core/settings.html:44 198 + #: paw/templates/core/settings.html:37 162 199 msgid "Profile Picture" 163 200 msgstr "Image du profil" 164 201 165 - #: paw/templates/core/settings.html:49 166 - #: paw/templates/ticketing/ticket_detail.html:152 202 + #: paw/templates/core/settings.html:42 203 + #: paw/templates/ticketing/ticket_detail.html:154 167 204 msgid "Contact" 168 205 msgstr "Contact" 169 206 170 - #: paw/templates/dashboard_base.html:35 171 - #: paw/templates/ticketing/create_ticket.html:52 172 - msgid "Create Ticket" 173 - msgstr "Nouveau ticket" 174 - 175 207 #: paw/templates/dashboard_base.html:47 176 208 #: paw/templates/ticketing/tickets_history.html:5 177 209 msgid "History" ··· 186 218 msgstr "Se déconnecter" 187 219 188 220 #: paw/templates/partials/assigned_to.html:17 189 - #: paw/templates/ticketing/ticket_detail.html:201 221 + #: paw/templates/ticketing/ticket_detail.html:203 190 222 msgid "Unassigned" 191 223 msgstr "Non attribué" 192 224 193 - #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:45 225 + #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:47 194 226 msgid "Low" 195 227 msgstr "Bas" 196 228 197 - #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:46 229 + #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:48 198 230 msgid "Medium" 199 231 msgstr "Moyen" 200 232 201 - #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:47 233 + #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:49 202 234 msgid "High" 203 235 msgstr "Élevé" 204 236 205 - #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:40 237 + #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:42 206 238 msgid "Open" 207 239 msgstr "Ouvert" 208 240 209 - #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:41 241 + #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:43 210 242 msgid "In Progress" 211 243 msgstr "En cours" 212 244 213 - #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:42 245 + #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:44 214 246 msgid "Closed" 215 247 msgstr "Fermé" 216 248 ··· 218 250 msgid "Create a new ticket" 219 251 msgstr "Créer un ticket" 220 252 221 - #: paw/templates/ticketing/create_ticket.html:12 222 - #: paw/templates/ticketing/ticket_detail.html:165 253 + #: paw/templates/ticketing/create_ticket.html:11 254 + #: paw/templates/ticketing/ticket_detail.html:167 223 255 #: paw/templates/ticketing/tickets.html:19 224 256 #: paw/templates/ticketing/tickets_history.html:19 225 257 msgid "Category" 226 258 msgstr "Catégorie" 227 259 228 - #: paw/templates/ticketing/create_ticket.html:19 260 + #: paw/templates/ticketing/create_ticket.html:15 229 261 #: paw/templates/ticketing/tickets.html:17 230 262 #: paw/templates/ticketing/tickets_history.html:17 ticketing/forms.py:73 231 263 msgid "Title" 232 264 msgstr "Titre" 233 265 234 - #: paw/templates/ticketing/create_ticket.html:26 ticketing/forms.py:74 266 + #: paw/templates/ticketing/create_ticket.html:23 ticketing/forms.py:74 235 267 msgid "Description" 236 268 msgstr "Description" 237 269 238 - #: paw/templates/ticketing/create_ticket.html:34 270 + #: paw/templates/ticketing/create_ticket.html:31 239 271 #: paw/templates/ticketing/ticket_detail.html:9 240 272 msgid "Attachments" 241 273 msgstr "Pièces jointes" 242 274 243 - #: paw/templates/ticketing/create_ticket.html:43 275 + #: paw/templates/ticketing/create_ticket.html:40 244 276 msgid "Create as follow-up to a closed ticket" 245 277 msgstr "Créer comme le suivi d'un ticket fermé" 246 278 ··· 268 300 msgid "Apply Template" 269 301 msgstr "Appliquer le modèle" 270 302 271 - #: paw/templates/ticketing/ticket_detail.html:96 303 + #: paw/templates/ticketing/ticket_detail.html:98 272 304 msgid "Add Comment" 273 305 msgstr "Ajouter un commentaire" 274 306 275 - #: paw/templates/ticketing/ticket_detail.html:98 307 + #: paw/templates/ticketing/ticket_detail.html:100 276 308 msgid "Close Ticket" 277 309 msgstr "Fermer" 278 310 279 - #: paw/templates/ticketing/ticket_detail.html:107 311 + #: paw/templates/ticketing/ticket_detail.html:109 280 312 msgid "Make this an internal comment" 281 313 msgstr "Rendre le commentaire interne" 282 314 283 - #: paw/templates/ticketing/ticket_detail.html:116 315 + #: paw/templates/ticketing/ticket_detail.html:118 284 316 msgid "Ticket has been closed" 285 317 msgstr "Le ticket a été fermé" 286 318 287 - #: paw/templates/ticketing/ticket_detail.html:121 319 + #: paw/templates/ticketing/ticket_detail.html:123 288 320 msgid "Re-Open Ticket" 289 321 msgstr "Réouverture du ticket" 290 322 291 - #: paw/templates/ticketing/ticket_detail.html:132 323 + #: paw/templates/ticketing/ticket_detail.html:134 292 324 msgid "Created by" 293 325 msgstr "Créé par" 294 326 295 - #: paw/templates/ticketing/ticket_detail.html:145 327 + #: paw/templates/ticketing/ticket_detail.html:147 296 328 msgid "Created on" 297 329 msgstr "Créé le" 298 330 299 - #: paw/templates/ticketing/ticket_detail.html:149 331 + #: paw/templates/ticketing/ticket_detail.html:151 300 332 msgid "Last updated" 301 333 msgstr "Dernière mise à jour le" 302 334 303 - #: paw/templates/ticketing/ticket_detail.html:171 335 + #: paw/templates/ticketing/ticket_detail.html:173 304 336 #: paw/templates/ticketing/tickets.html:42 305 337 #: paw/templates/ticketing/tickets_history.html:42 ticketing/forms.py:81 306 - #: ticketing/forms.py:104 ticketing/models.py:165 ticketing/models.py:178 338 + #: ticketing/forms.py:104 ticketing/models.py:167 ticketing/models.py:180 307 339 msgid "General" 308 340 msgstr "Général" 309 341 310 - #: paw/templates/ticketing/ticket_detail.html:177 342 + #: paw/templates/ticketing/ticket_detail.html:179 311 343 msgid "Assign to new category" 312 344 msgstr "Attribuer à une nouvelle catégorie" 313 345 314 - #: paw/templates/ticketing/ticket_detail.html:180 315 - #: paw/templates/ticketing/ticket_detail.html:210 346 + #: paw/templates/ticketing/ticket_detail.html:182 347 + #: paw/templates/ticketing/ticket_detail.html:212 316 348 msgid "Assign" 317 349 msgstr "Attribuer" 318 350 319 - #: paw/templates/ticketing/ticket_detail.html:185 351 + #: paw/templates/ticketing/ticket_detail.html:187 320 352 msgid "Assignees" 321 353 msgstr "Attribué à" 322 354 323 - #: paw/templates/ticketing/ticket_detail.html:192 355 + #: paw/templates/ticketing/ticket_detail.html:194 324 356 msgid "Assign to me" 325 357 msgstr "Me l'attribuer" 326 358 327 - #: paw/templates/ticketing/ticket_detail.html:207 359 + #: paw/templates/ticketing/ticket_detail.html:209 328 360 msgid "Assign to new team" 329 361 msgstr "Attribuer à une nouvelle équipe" 330 362 ··· 413 445 msgid "No Team" 414 446 msgstr "Aucune équipe" 415 447 416 - #: ticketing/models.py:31 448 + #: ticketing/models.py:33 417 449 msgid "" 418 450 "If a team is selected, new tickets will automatically assigned to this team." 419 451 msgstr ""
paw/locale/nl/LC_MESSAGES/django.mo

This is a binary file and will not be displayed.

+89 -58
paw/locale/nl/LC_MESSAGES/django.po
··· 8 8 msgstr "" 9 9 "Project-Id-Version: PACKAGE VERSION\n" 10 10 "Report-Msgid-Bugs-To: \n" 11 - "POT-Creation-Date: 2025-08-09 01:16+0000\n" 11 + "POT-Creation-Date: 2026-02-22 00:58+0000\n" 12 12 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 13 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 14 14 "Language-Team: LANGUAGE <LL@li.org>\n" ··· 18 18 "Content-Transfer-Encoding: 8bit\n" 19 19 "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 20 21 - #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:33 22 - #: paw/templates/core/login.html:25 paw/templates/core/register.html:24 21 + #: core/forms.py:27 core/forms.py:35 paw/templates/core/account_finish.html:32 22 + #: paw/templates/core/login.html:23 paw/templates/core/register.html:23 23 23 msgid "Username" 24 24 msgstr "Gebruikersnaam" 25 25 ··· 59 59 msgid "Google SSO User" 60 60 msgstr "Google SSO Gebruiker" 61 61 62 - #: paw/settings.py:135 62 + #: paw/settings.py:140 63 63 msgid "English" 64 64 msgstr "English" 65 65 66 - #: paw/settings.py:136 66 + #: paw/settings.py:141 67 67 msgid "French" 68 68 msgstr "Français" 69 69 70 - #: paw/settings.py:137 70 + #: paw/settings.py:142 71 71 msgid "German" 72 72 msgstr "Deutsch" 73 73 74 - #: paw/settings.py:138 74 + #: paw/settings.py:143 75 75 msgid "Dutch" 76 76 msgstr "Nederlands" 77 77 78 + #: paw/templates/404.html:9 79 + msgid "Page Not Found" 80 + msgstr "Pagina niet gevonden" 81 + 82 + #: paw/templates/404.html:11 83 + msgid "" 84 + "Sorry, we couldn't find the page you're looking for. The page may have been " 85 + "moved, deleted, or the URL might be incorrect." 86 + msgstr "" 87 + "Sorry, we kunnen de pagina die u zoekt niet vinden. De pagina is mogelijk " 88 + "verplaatst, verwijderd of de URL is onjuist." 89 + 90 + #: paw/templates/404.html:20 paw/templates/500.html:20 91 + msgid "Go Home" 92 + msgstr "Naar startpagina" 93 + 94 + #: paw/templates/404.html:28 paw/templates/500.html:28 95 + msgid "Go to Tickets" 96 + msgstr "Ga naar tickets" 97 + 98 + #: paw/templates/404.html:35 paw/templates/500.html:35 99 + #: paw/templates/dashboard_base.html:35 100 + #: paw/templates/ticketing/create_ticket.html:51 101 + msgid "Create Ticket" 102 + msgstr "Ticketje aanmaken" 103 + 104 + #: paw/templates/500.html:9 105 + msgid "Internal Server Error" 106 + msgstr "Interne serverfout" 107 + 108 + #: paw/templates/500.html:11 109 + msgid "" 110 + "We're sorry, but something went wrong on our end. Please try again later." 111 + msgstr "" 112 + "Sorry, er is iets misgegaan aan onze kant. Probeer het later opnieuw." 113 + 78 114 #: paw/templates/base.html:20 paw/templates/dashboard_base.html:41 79 115 msgid "Tickets" 80 116 msgstr "Ticketjes" ··· 100 136 msgid "Done" 101 137 msgstr "Gedaan" 102 138 103 - #: paw/templates/core/account_finish.html:38 104 - #: paw/templates/core/settings.html:55 139 + #: paw/templates/core/account_finish.html:36 140 + #: paw/templates/core/settings.html:51 105 141 msgid "Save" 106 142 msgstr "Opslaan" 107 143 108 - #: paw/templates/core/login.html:7 paw/templates/core/login.html:40 144 + #: paw/templates/core/login.html:7 paw/templates/core/login.html:32 109 145 msgid "Log In" 110 146 msgstr "Inloggen" 111 147 ··· 113 149 msgid "Register Account" 114 150 msgstr "Registreren" 115 151 116 - #: paw/templates/core/login.html:31 paw/templates/core/register.html:36 152 + #: paw/templates/core/login.html:27 paw/templates/core/register.html:32 117 153 msgid "Password" 118 154 msgstr "Wachtwoord" 119 155 120 - #: paw/templates/core/login.html:36 156 + #: paw/templates/core/login.html:29 121 157 msgid "Password Reset" 122 158 msgstr "Wachtwoord resetten" 123 159 124 - #: paw/templates/core/login.html:47 160 + #: paw/templates/core/login.html:39 125 161 msgid "Log in with Google" 126 162 msgstr "Inloggen met Google" 127 163 128 - #: paw/templates/core/register.html:30 164 + #: paw/templates/core/register.html:28 129 165 msgid "Email Address" 130 166 msgstr "E-mailadres" 131 167 132 - #: paw/templates/core/register.html:42 168 + #: paw/templates/core/register.html:36 133 169 msgid "Confirm Password" 134 170 msgstr "Wachtwoordbevestiging" 135 171 136 - #: paw/templates/core/register.html:47 172 + #: paw/templates/core/register.html:40 137 173 msgid "Register" 138 174 msgstr "Registreren" 139 175 ··· 141 177 msgid "Settings" 142 178 msgstr "Instellingen" 143 179 144 - #: paw/templates/core/settings.html:12 180 + #: paw/templates/core/settings.html:11 145 181 msgid "Mail Address" 146 182 msgstr "E-mailadres" 147 183 148 - #: paw/templates/core/settings.html:23 184 + #: paw/templates/core/settings.html:19 149 185 msgid "Language" 150 186 msgstr "Taal" 151 187 152 - #: paw/templates/core/settings.html:30 188 + #: paw/templates/core/settings.html:25 153 189 msgid "Use Darkmode" 154 190 msgstr "Gebruik donkere modus" 155 191 156 - #: paw/templates/core/settings.html:37 192 + #: paw/templates/core/settings.html:31 157 193 msgid "Receive Email Notifications" 158 194 msgstr "Meldingen per e-mail" 159 195 160 - #: paw/templates/core/settings.html:44 196 + #: paw/templates/core/settings.html:37 161 197 msgid "Profile Picture" 162 198 msgstr "Profielfoto" 163 199 164 - #: paw/templates/core/settings.html:49 165 - #: paw/templates/ticketing/ticket_detail.html:152 200 + #: paw/templates/core/settings.html:42 201 + #: paw/templates/ticketing/ticket_detail.html:154 166 202 msgid "Contact" 167 203 msgstr "Contact" 168 204 169 - #: paw/templates/dashboard_base.html:35 170 - #: paw/templates/ticketing/create_ticket.html:52 171 - msgid "Create Ticket" 172 - msgstr "Ticketje aanmaken" 173 - 174 205 #: paw/templates/dashboard_base.html:47 175 206 #: paw/templates/ticketing/tickets_history.html:5 176 207 msgid "History" ··· 185 216 msgstr "Uitloggen" 186 217 187 218 #: paw/templates/partials/assigned_to.html:17 188 - #: paw/templates/ticketing/ticket_detail.html:201 219 + #: paw/templates/ticketing/ticket_detail.html:203 189 220 msgid "Unassigned" 190 221 msgstr "Niet toegewezen" 191 222 192 - #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:45 223 + #: paw/templates/partials/ticket_priority_badge.html:4 ticketing/models.py:47 193 224 msgid "Low" 194 225 msgstr "Laag" 195 226 196 - #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:46 227 + #: paw/templates/partials/ticket_priority_badge.html:6 ticketing/models.py:48 197 228 msgid "Medium" 198 229 msgstr "Gemiddeld" 199 230 200 - #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:47 231 + #: paw/templates/partials/ticket_priority_badge.html:8 ticketing/models.py:49 201 232 msgid "High" 202 233 msgstr "Hoog" 203 234 204 - #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:40 235 + #: paw/templates/partials/ticket_status_badge.html:4 ticketing/models.py:42 205 236 msgid "Open" 206 237 msgstr "Open" 207 238 208 - #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:41 239 + #: paw/templates/partials/ticket_status_badge.html:6 ticketing/models.py:43 209 240 msgid "In Progress" 210 241 msgstr "Lopend" 211 242 212 - #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:42 243 + #: paw/templates/partials/ticket_status_badge.html:8 ticketing/models.py:44 213 244 msgid "Closed" 214 245 msgstr "Gesloten" 215 246 ··· 217 248 msgid "Create a new ticket" 218 249 msgstr "Maak een nieuw ticketje aan" 219 250 220 - #: paw/templates/ticketing/create_ticket.html:12 221 - #: paw/templates/ticketing/ticket_detail.html:165 251 + #: paw/templates/ticketing/create_ticket.html:11 252 + #: paw/templates/ticketing/ticket_detail.html:167 222 253 #: paw/templates/ticketing/tickets.html:19 223 254 #: paw/templates/ticketing/tickets_history.html:19 224 255 msgid "Category" 225 256 msgstr "Categorie" 226 257 227 - #: paw/templates/ticketing/create_ticket.html:19 258 + #: paw/templates/ticketing/create_ticket.html:15 228 259 #: paw/templates/ticketing/tickets.html:17 229 260 #: paw/templates/ticketing/tickets_history.html:17 ticketing/forms.py:73 230 261 msgid "Title" 231 262 msgstr "Onderwerp" 232 263 233 - #: paw/templates/ticketing/create_ticket.html:26 ticketing/forms.py:74 264 + #: paw/templates/ticketing/create_ticket.html:23 ticketing/forms.py:74 234 265 msgid "Description" 235 266 msgstr "Beschrijving" 236 267 237 - #: paw/templates/ticketing/create_ticket.html:34 268 + #: paw/templates/ticketing/create_ticket.html:31 238 269 #: paw/templates/ticketing/ticket_detail.html:9 239 270 msgid "Attachments" 240 271 msgstr "Bijlagen" 241 272 242 - #: paw/templates/ticketing/create_ticket.html:43 273 + #: paw/templates/ticketing/create_ticket.html:40 243 274 msgid "Create as follow-up to a closed ticket" 244 275 msgstr "Maak als vervolg op een gesloten ticket" 245 276 ··· 267 298 msgid "Apply Template" 268 299 msgstr "Gebruik model" 269 300 270 - #: paw/templates/ticketing/ticket_detail.html:96 301 + #: paw/templates/ticketing/ticket_detail.html:98 271 302 msgid "Add Comment" 272 303 msgstr "Opmerking toevoegen" 273 304 274 - #: paw/templates/ticketing/ticket_detail.html:98 305 + #: paw/templates/ticketing/ticket_detail.html:100 275 306 msgid "Close Ticket" 276 307 msgstr "Ticketje sluiten" 277 308 278 - #: paw/templates/ticketing/ticket_detail.html:107 309 + #: paw/templates/ticketing/ticket_detail.html:109 279 310 msgid "Make this an internal comment" 280 311 msgstr "Maak dit opmerking privaat" 281 312 282 - #: paw/templates/ticketing/ticket_detail.html:116 313 + #: paw/templates/ticketing/ticket_detail.html:118 283 314 msgid "Ticket has been closed" 284 315 msgstr "Ticketje is geslooten" 285 316 286 - #: paw/templates/ticketing/ticket_detail.html:121 317 + #: paw/templates/ticketing/ticket_detail.html:123 287 318 msgid "Re-Open Ticket" 288 319 msgstr "Heropen dit ticketje" 289 320 290 - #: paw/templates/ticketing/ticket_detail.html:132 321 + #: paw/templates/ticketing/ticket_detail.html:134 291 322 msgid "Created by" 292 323 msgstr "Gemaakt door" 293 324 294 - #: paw/templates/ticketing/ticket_detail.html:145 325 + #: paw/templates/ticketing/ticket_detail.html:147 295 326 msgid "Created on" 296 327 msgstr "Gemaakt op" 297 328 298 - #: paw/templates/ticketing/ticket_detail.html:149 329 + #: paw/templates/ticketing/ticket_detail.html:151 299 330 msgid "Last updated" 300 331 msgstr "Laatst bijgewerkt" 301 332 302 - #: paw/templates/ticketing/ticket_detail.html:171 333 + #: paw/templates/ticketing/ticket_detail.html:173 303 334 #: paw/templates/ticketing/tickets.html:42 304 335 #: paw/templates/ticketing/tickets_history.html:42 ticketing/forms.py:81 305 - #: ticketing/forms.py:104 ticketing/models.py:165 ticketing/models.py:178 336 + #: ticketing/forms.py:104 ticketing/models.py:167 ticketing/models.py:180 306 337 msgid "General" 307 338 msgstr "Generaal" 308 339 309 - #: paw/templates/ticketing/ticket_detail.html:177 340 + #: paw/templates/ticketing/ticket_detail.html:179 310 341 msgid "Assign to new category" 311 342 msgstr "Categorie opnieuw toewijzen" 312 343 313 - #: paw/templates/ticketing/ticket_detail.html:180 314 - #: paw/templates/ticketing/ticket_detail.html:210 344 + #: paw/templates/ticketing/ticket_detail.html:182 345 + #: paw/templates/ticketing/ticket_detail.html:212 315 346 msgid "Assign" 316 347 msgstr "Toewijzen" 317 348 318 - #: paw/templates/ticketing/ticket_detail.html:185 349 + #: paw/templates/ticketing/ticket_detail.html:187 319 350 msgid "Assignees" 320 351 msgstr "Toegewezen" 321 352 322 - #: paw/templates/ticketing/ticket_detail.html:192 353 + #: paw/templates/ticketing/ticket_detail.html:194 323 354 msgid "Assign to me" 324 355 msgstr "Toewijzen aan mij" 325 356 326 - #: paw/templates/ticketing/ticket_detail.html:207 357 + #: paw/templates/ticketing/ticket_detail.html:209 327 358 msgid "Assign to new team" 328 359 msgstr "Team opnieuw toewijzen" 329 360 ··· 412 443 msgid "No Team" 413 444 msgstr "Geen team" 414 445 415 - #: ticketing/models.py:31 446 + #: ticketing/models.py:33 416 447 msgid "" 417 448 "If a team is selected, new tickets will automatically assigned to this team." 418 449 msgstr ""
+5
paw/settings.py
··· 165 165 MEDIA_URL = '/media/' 166 166 MEDIA_ROOT = BASE_DIR / 'media' 167 167 168 + # Secure storage for ticket attachments 169 + # This directory should NOT be served by the web server 170 + SECURE_MEDIA_ENABLED = environ.get('SECURE_MEDIA_ENABLED', 'false').lower() == 'true' 171 + SECURE_MEDIA_ROOT = BASE_DIR / 'secure_media' 172 + 168 173 169 174 # Default primary key field type 170 175 # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
+62
paw/static/css/paw.css
··· 15 15 --container-sm: 24rem; 16 16 --container-lg: 32rem; 17 17 --container-xl: 36rem; 18 + --container-2xl: 42rem; 18 19 --container-3xl: 48rem; 19 20 --container-4xl: 56rem; 20 21 --text-xs: 0.75rem; ··· 33 34 --text-3xl--line-height: calc(2.25 / 1.875); 34 35 --text-4xl: 2.25rem; 35 36 --text-4xl--line-height: calc(2.5 / 2.25); 37 + --text-9xl: 8rem; 38 + --text-9xl--line-height: 1; 36 39 --font-weight-semibold: 600; 37 40 --font-weight-bold: 700; 38 41 --default-font-family: var(--font-sans); ··· 2001 2004 .mb-6 { 2002 2005 margin-bottom: calc(var(--spacing) * 6); 2003 2006 } 2007 + .mb-8 { 2008 + margin-bottom: calc(var(--spacing) * 8); 2009 + } 2004 2010 .mb-10 { 2005 2011 margin-bottom: calc(var(--spacing) * 10); 2006 2012 } ··· 2222 2228 .w-full { 2223 2229 width: 100%; 2224 2230 } 2231 + .max-w-2xl { 2232 + max-width: var(--container-2xl); 2233 + } 2225 2234 .max-w-3xl { 2226 2235 max-width: var(--container-3xl); 2227 2236 } ··· 2309 2318 } 2310 2319 .gap-2 { 2311 2320 gap: calc(var(--spacing) * 2); 2321 + } 2322 + .gap-4 { 2323 + gap: calc(var(--spacing) * 4); 2312 2324 } 2313 2325 .self-center { 2314 2326 align-self: center; ··· 2403 2415 .px-3 { 2404 2416 padding-inline: calc(var(--spacing) * 3); 2405 2417 } 2418 + .px-4 { 2419 + padding-inline: calc(var(--spacing) * 4); 2420 + } 2406 2421 .py-1 { 2407 2422 padding-block: calc(var(--spacing) * 1); 2408 2423 } ··· 2427 2442 font-size: var(--text-4xl); 2428 2443 line-height: var(--tw-leading, var(--text-4xl--line-height)); 2429 2444 } 2445 + .text-9xl { 2446 + font-size: var(--text-9xl); 2447 + line-height: var(--tw-leading, var(--text-9xl--line-height)); 2448 + } 2430 2449 .text-base { 2431 2450 font-size: var(--text-base); 2432 2451 line-height: var(--tw-leading, var(--text-base--line-height)); ··· 2516 2535 .text-base-content { 2517 2536 color: var(--color-base-content); 2518 2537 } 2538 + .text-base-content\/70 { 2539 + color: var(--color-base-content); 2540 + @supports (color: color-mix(in lab, red, red)) { 2541 + color: color-mix(in oklab, var(--color-base-content) 70%, transparent); 2542 + } 2543 + } 2519 2544 .text-base-content\/80 { 2520 2545 color: var(--color-base-content); 2521 2546 @supports (color: color-mix(in lab, red, red)) { ··· 2576 2601 } 2577 2602 } 2578 2603 } 2604 + .btn-outline { 2605 + @layer daisyui.l1 { 2606 + &:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) { 2607 + --btn-shadow: ""; 2608 + --btn-bg: #0000; 2609 + --btn-fg: var(--btn-color); 2610 + --btn-border: var(--btn-color); 2611 + --btn-noise: none; 2612 + } 2613 + @media (hover: none) { 2614 + &:not(.btn-active, :active, :focus-visible, input:checked:not(.filter .btn)):hover { 2615 + --btn-shadow: ""; 2616 + --btn-bg: #0000; 2617 + --btn-fg: var(--btn-color); 2618 + --btn-border: var(--btn-color); 2619 + --btn-noise: none; 2620 + } 2621 + } 2622 + } 2623 + } 2579 2624 .btn-lg { 2580 2625 @layer daisyui.l1.l2 { 2581 2626 --fontsize: 1.125rem; ··· 2645 2690 --btn-fg: var(--color-neutral-content); 2646 2691 } 2647 2692 } 2693 + .btn-primary { 2694 + @layer daisyui.l1.l2.l3 { 2695 + --btn-color: var(--color-primary); 2696 + --btn-fg: var(--color-primary-content); 2697 + } 2698 + } 2699 + .btn-secondary { 2700 + @layer daisyui.l1.l2.l3 { 2701 + --btn-color: var(--color-secondary); 2702 + --btn-fg: var(--color-secondary-content); 2703 + } 2704 + } 2648 2705 .btn-success { 2649 2706 @layer daisyui.l1.l2.l3 { 2650 2707 --btn-color: var(--color-success); ··· 2674 2731 .sm\:block { 2675 2732 @media (width >= 40rem) { 2676 2733 display: block; 2734 + } 2735 + } 2736 + .sm\:flex-row { 2737 + @media (width >= 40rem) { 2738 + flex-direction: row; 2677 2739 } 2678 2740 } 2679 2741 .lg\:drawer-open {
+42
paw/templates/404.html
··· 1 + {% extends 'base.html' %} 2 + {% load i18n %} 3 + 4 + {% block content %} 5 + <div class="flex items-center justify-center w-full"> 6 + <div class="text-center max-w-2xl mx-auto px-4"> 7 + <div class="mb-8"> 8 + <h1 class="text-9xl font-bold text-error mb-4">404</h1> 9 + <h2 class="text-4xl font-bold mb-4">{% trans 'Page Not Found' %}</h2> 10 + <p class="text-lg text-base-content/70 mb-8"> 11 + {% trans "Sorry, we couldn't find the page you're looking for. The page may have been moved, deleted, or the URL might be incorrect." %} 12 + </p> 13 + </div> 14 + <div class="flex flex-col sm:flex-row gap-4 justify-center"> 15 + <a href="/" class="btn btn-outline"> 16 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 17 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 18 + <path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /> 19 + </svg> 20 + {% trans 'Go Home' %} 21 + </a> 22 + {% if request.user.is_authenticated %} 23 + <a href="{% url 'all_tickets' %}" class="btn btn-primary"> 24 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 25 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 26 + <path d="M15 5l0 2" /><path d="M15 11l0 2" /><path d="M15 17l0 2" /><path d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" /> 27 + </svg> 28 + {% trans 'Go to Tickets' %} 29 + </a> 30 + <a href="{% url 'create_ticket' %}" class="btn btn-secondary"> 31 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 32 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 33 + <path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" /><path d="M13.5 6.5l4 4" /> 34 + </svg> 35 + {% trans 'Create Ticket' %} 36 + </a> 37 + {% endif %} 38 + </div> 39 + </div> 40 + </div> 41 + {% endblock %} 42 +
+42
paw/templates/500.html
··· 1 + {% extends 'base.html' %} 2 + {% load i18n %} 3 + 4 + {% block content %} 5 + <div class="flex items-center justify-center w-full"> 6 + <div class="text-center max-w-2xl mx-auto px-4"> 7 + <div class="mb-8"> 8 + <h1 class="text-9xl font-bold text-error mb-4">500</h1> 9 + <h2 class="text-4xl font-bold mb-4">{% trans 'Internal Server Error' %}</h2> 10 + <p class="text-lg text-base-content/70 mb-8"> 11 + {% trans "We're sorry, but something went wrong on our end. Please try again later." %} 12 + </p> 13 + </div> 14 + <div class="flex flex-col sm:flex-row gap-4 justify-center"> 15 + <a href="/" class="btn btn-outline"> 16 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 17 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 18 + <path d="M5 12l14 0" /><path d="M5 12l6 6" /><path d="M5 12l6 -6" /> 19 + </svg> 20 + {% trans 'Go Home' %} 21 + </a> 22 + {% if request.user.is_authenticated %} 23 + <a href="{% url 'all_tickets' %}" class="btn btn-primary"> 24 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 25 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 26 + <path d="M15 5l0 2" /><path d="M15 11l0 2" /><path d="M15 17l0 2" /><path d="M5 5h14a2 2 0 0 1 2 2v3a2 2 0 0 0 0 4v3a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-3a2 2 0 0 0 0 -4v-3a2 2 0 0 1 2 -2" /> 27 + </svg> 28 + {% trans 'Go to Tickets' %} 29 + </a> 30 + <a href="{% url 'create_ticket' %}" class="btn btn-secondary"> 31 + <svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5 mr-2" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> 32 + <path stroke="none" d="M0 0h24v24H0z" fill="none"/> 33 + <path d="M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4" /><path d="M13.5 6.5l4 4" /> 34 + </svg> 35 + {% trans 'Create Ticket' %} 36 + </a> 37 + {% endif %} 38 + </div> 39 + </div> 40 + </div> 41 + {% endblock %} 42 +
+1 -1
paw/templates/ticketing/ticket_detail.html
··· 9 9 <h2 class="font-semibold text-sm mb-1">{% trans 'Attachments' %}</h2> 10 10 <div class="flex flex-wrap mb-2"> 11 11 {% for attachment in attachments %} 12 - <a href="{{ attachment.url }}" target="_blank" class="badge badge-lg badge-accent flex items-center mr-1 mb-1"> 12 + <a href="{% url 'download_attachment' attachment.id %}" target="_blank" class="badge badge-lg badge-accent flex items-center mr-1 mb-1"> 13 13 <svg xmlns="http://www.w3.org/2000/svg"class="w-4 h-4 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 7l-6.5 6.5a1.5 1.5 0 0 0 3 3l6.5 -6.5a3 3 0 0 0 -6 -6l-6.5 6.5a4.5 4.5 0 0 0 9 9l6.5 -6.5" /></svg> 14 14 {% trans 'Attachment' %} {{ forloop.counter }} 15 15 </a>
+13 -2
ticketing/models.py
··· 6 6 from django.dispatch import receiver 7 7 from uuid import uuid4 8 8 from core.models import MailTemplate 9 + from .storage import SecureFileStorage 10 + from django.conf import settings 9 11 10 12 11 13 def ticket_directory_path(instance, filename): 12 - """ file will be uploaded to MEDIA_ROOT/ticket_<id>/<filename> """ 14 + """ file will be uploaded to (SECURE_)MEDIA_ROOT/attachments/ticket_<id>/<filename> """ 13 15 ext = filename.split('.')[-1] 14 16 return "attachments/ticket_{0}/{1}.{2}".format(instance.ticket.id, uuid4(), ext) 15 17 ··· 225 227 class FileAttachment(models.Model): 226 228 ticket = models.ForeignKey(Ticket, on_delete=models.CASCADE) 227 229 file = models.FileField( 228 - upload_to=ticket_directory_path, max_length=255) 230 + upload_to=ticket_directory_path, 231 + max_length=255, 232 + storage=SecureFileStorage() if settings.SECURE_MEDIA_ENABLED else None) 229 233 uploaded_at = models.DateTimeField(auto_now_add=True) 230 234 231 235 def __str__(self): 232 236 return f'{_('Attachment for')} {self.ticket.title}' 237 + 238 + def get_attachment_url(self): 239 + """ 240 + Returns the URL for downloading the attachment. 241 + """ 242 + from django.urls import reverse 243 + return reverse('download_attachment', args=[self.id]) 233 244 234 245 235 246 class Template(models.Model):
+28
ticketing/storage.py
··· 1 + """ 2 + Custom storage backend for secure file attachments. 3 + Files stored here are not publicly accessible via web server. 4 + """ 5 + from django.core.files.storage import FileSystemStorage 6 + from django.conf import settings 7 + import os 8 + 9 + 10 + class SecureFileStorage(FileSystemStorage): 11 + """ 12 + Storage backend that stores files in a secure location 13 + outside the public media directory. 14 + """ 15 + 16 + def __init__(self, location=None, base_url=None): 17 + if location is None: 18 + location = settings.SECURE_MEDIA_ROOT 19 + # base_url is None to prevent public URL generation 20 + super().__init__(location=location, base_url=None) 21 + 22 + def url(self, name): 23 + """ 24 + Override url() to return None or raise an error. 25 + Files should only be accessed through the secure download view. 26 + """ 27 + return None 28 +
+2 -1
ticketing/urls.py
··· 1 1 from django.urls import path 2 2 3 - from .views import show_ticket, show_tickets, create_ticket, dashboard, show_tickets_history 3 + from .views import show_ticket, show_tickets, create_ticket, dashboard, show_tickets_history, download_attachment 4 4 5 5 urlpatterns = [ 6 6 path("tickets/<int:ticket_id>", show_ticket, name="ticket_detail"), ··· 8 8 path("tickets", show_tickets, name="all_tickets"), 9 9 path("tickets/history", show_tickets_history, name="tickets_history"), 10 10 path("dashboard", dashboard, name="dashboard"), 11 + path("tickets/attachments/<int:attachment_id>/", download_attachment, name="download_attachment"), 11 12 ]
+42 -2
ticketing/views.py
··· 1 1 from django.shortcuts import render, redirect 2 2 from django.contrib.auth.decorators import login_required 3 3 from django.shortcuts import get_object_or_404 4 - from .models import Ticket, Template 4 + from .models import Ticket, Template, FileAttachment 5 5 from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger 6 6 from django.db.models import Q 7 7 from .forms import CommentForm, TicketForm, TemplateForm, TeamAssignmentForm, CategoryAssignmentForm 8 + from django.http import Http404, FileResponse 9 + from django.conf import settings 10 + import os 8 11 9 12 10 13 @login_required ··· 92 95 93 96 comments = ticket.comment_set.all() 94 97 context = { 95 - "ticket": ticket, "comments": comments, "attachments": [attachment.file for attachment in ticket.fileattachment_set.all()], 98 + "ticket": ticket, "comments": comments, "attachments": ticket.fileattachment_set.all(), 96 99 "form": form, "template_form": template_form, 97 100 "team_assignment_form": team_assignment_form, "category_assignment_form": category_assignment_form, 98 101 "can_edit": can_edit ··· 132 135 in_progress_tickets = tickets.filter(status=Ticket.Status.IN_PROGRESS) 133 136 closed_tickets = tickets.filter(status=Ticket.Status.CLOSED) 134 137 return render(request, "ticketing/dashboard.html", {"tickets": tickets, "open_tickets": open_tickets, "in_progress_tickets": in_progress_tickets, "closed_tickets": closed_tickets}) 138 + 139 + 140 + @login_required 141 + def download_attachment(request, attachment_id): 142 + """ 143 + View that checks if the user has permission 144 + to access the ticket before serving the file. 145 + """ 146 + attachment = get_object_or_404(FileAttachment, pk=attachment_id) 147 + ticket = attachment.ticket 148 + 149 + if not ticket.can_open(request.user): 150 + raise Http404("File not found") 151 + 152 + if not attachment.file: 153 + raise Http404("File not found") 154 + 155 + try: 156 + file = attachment.file.open('rb') 157 + response = FileResponse(file, content_type='application/octet-stream') 158 + 159 + filename = os.path.basename(attachment.file.name) 160 + response['Content-Disposition'] = f'inline; filename="{filename}"' 161 + return response 162 + except (ValueError, IOError, OSError): 163 + # backwards compatibility 164 + try: 165 + old_path = os.path.join(settings.MEDIA_ROOT, attachment.file.name) 166 + if os.path.exists(old_path): 167 + file = open(old_path, 'rb') 168 + response = FileResponse(file, content_type='application/octet-stream') 169 + filename = os.path.basename(attachment.file.name) 170 + response['Content-Disposition'] = f'inline; filename="{filename}"' 171 + return response 172 + except (ValueError, IOError, OSError): 173 + pass 174 + raise Http404("File not found")