diff --git a/src/c3nav/editor/forms.py b/src/c3nav/editor/forms.py index bd84bef1..1141d0bd 100644 --- a/src/c3nav/editor/forms.py +++ b/src/c3nav/editor/forms.py @@ -283,7 +283,7 @@ def create_editor_form(editor_model): 'outside', 'can_search', 'can_describe', 'geometry', 'single', 'altitude', 'short_label', 'origin_space', 'target_space', 'data', 'comment', 'slow_down_factor', 'extra_seconds', 'speed', 'description', 'speed_up', 'description_up', 'enter_description', - 'level_change_description', 'base_mapdata_accessible', + 'level_change_description', 'base_mapdata_accessible', 'can_report_missing', 'label_settings', 'label_override', 'min_zoom', 'max_zoom', 'font_size', 'allow_levels', 'allow_spaces', 'allow_areas', 'allow_pois', 'left', 'top', 'right', 'bottom'] field_names = [field.name for field in editor_model._meta.get_fields() if not field.one_to_many] diff --git a/src/c3nav/locale/de/LC_MESSAGES/django.po b/src/c3nav/locale/de/LC_MESSAGES/django.po index 55dadc24..a18681a1 100644 --- a/src/c3nav/locale/de/LC_MESSAGES/django.po +++ b/src/c3nav/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2019-12-24 03:50+0100\n" +"POT-Creation-Date: 2019-12-24 17:22+0100\n" "PO-Revision-Date: 2019-12-22 00:44+0100\n" "Last-Translator: Jenny Danzmayr \n" "Language-Team: \n" @@ -42,7 +42,7 @@ msgstr "Nich angemeldet." msgid "Logout successful." msgstr "Login erfolgreich." -#: c3nav/api/models.py:14 +#: c3nav/api/models.py:14 c3nav/mapdata/models/report.py:79 msgid "secret" msgstr "secret" @@ -514,6 +514,7 @@ msgstr "zurück" #: c3nav/control/templates/control/user.html:73 #: c3nav/mapdata/models/access.py:77 c3nav/mapdata/models/geometry/space.py:385 +#: c3nav/mapdata/models/report.py:58 c3nav/mapdata/models/report.py:109 #: c3nav/site/models.py:17 msgid "author" msgstr "Autor" @@ -788,8 +789,8 @@ msgid "Change Set" msgstr "Änderungsset" #: c3nav/editor/models/changedobject.py:35 c3nav/editor/models/changeset.py:39 -#: c3nav/editor/views/changes.py:266 c3nav/site/models.py:14 -#: c3nav/site/models.py:54 +#: c3nav/editor/views/changes.py:266 c3nav/mapdata/models/report.py:56 +#: c3nav/site/models.py:14 c3nav/site/models.py:54 msgid "created" msgstr "erstellt" @@ -863,7 +864,7 @@ msgstr "letzte Statusänderung" #: c3nav/editor/models/changeset.py:48 c3nav/mapdata/models/base.py:64 #: c3nav/mapdata/models/graph.py:38 c3nav/mapdata/models/locations.py:256 -#: c3nav/mapdata/models/locations.py:421 c3nav/mapdata/utils/locations.py:344 +#: c3nav/mapdata/models/locations.py:423 c3nav/mapdata/utils/locations.py:344 msgid "Title" msgstr "Titel" @@ -871,7 +872,8 @@ msgstr "Titel" msgid "Description" msgstr "Beschreibung" -#: c3nav/editor/models/changeset.py:51 +#: c3nav/editor/models/changeset.py:51 c3nav/mapdata/models/report.py:66 +#: c3nav/mapdata/models/report.py:113 msgid "assigned to" msgstr "zugewiesen" @@ -898,7 +900,7 @@ msgid_plural "%(num)d objects changed" msgstr[0] "%(num)d Objekt geändert" msgstr[1] "%(num)d Objekte geändert" -#: c3nav/editor/models/changesetupdate.py:12 +#: c3nav/editor/models/changesetupdate.py:12 c3nav/mapdata/models/report.py:108 msgid "datetime" msgstr "Zeitpunkt" @@ -1150,7 +1152,7 @@ msgid "Log out" msgstr "Abmelden" #: c3nav/editor/templates/editor/fragment_nav.html:23 -#: c3nav/editor/views/account.py:27 c3nav/site/views.py:227 +#: c3nav/editor/views/account.py:27 c3nav/site/views.py:232 msgid "Log in" msgstr "Anmelden" @@ -1277,7 +1279,7 @@ msgid "Activate direct editing" msgstr "Direktes Bearbeiten aktivieren" #: c3nav/editor/templates/editor/user.html:54 c3nav/editor/views/account.py:85 -#: c3nav/site/templates/site/account.html:29 c3nav/site/views.py:292 +#: c3nav/site/templates/site/account.html:29 c3nav/site/views.py:297 msgid "Change password" msgstr "Passwort ändern" @@ -1298,11 +1300,11 @@ msgid "All recent change sets" msgstr "Alle kürzlichen Änderungssets" #: c3nav/editor/views/account.py:30 c3nav/editor/views/account.py:61 -#: c3nav/site/views.py:234 c3nav/site/views.py:269 +#: c3nav/site/views.py:239 c3nav/site/views.py:274 msgid "Create new account" msgstr "Neues Konto erstellen" -#: c3nav/editor/views/account.py:75 c3nav/site/views.py:283 +#: c3nav/editor/views/account.py:75 c3nav/site/views.py:288 msgid "Password successfully changed." msgstr "Passwort erfolgreich geändert." @@ -1509,7 +1511,7 @@ msgstr "Invalides GeoJSON." msgid "Could not clean geometry." msgstr "Konnte Geometrie nicht bereinigen." -#: c3nav/mapdata/forms.py:54 +#: c3nav/mapdata/forms.py:55 #, python-brace-format msgid "You have to choose a value for {field} in at least one language." msgstr "Du must das Feld {field} in mindestens einer Sprache ausfüllen." @@ -1679,9 +1681,11 @@ msgstr "Werte zurücksetzen" msgid "save result to the stats directory" msgstr "Ergebnis im stats-Ordner speichern" -#: c3nav/mapdata/models/access.py:23 +#: c3nav/mapdata/models/access.py:23 c3nav/mapdata/models/report.py:59 +#: c3nav/mapdata/models/report.py:110 +#: c3nav/site/templates/site/report_detail.html:9 msgid "open" -msgstr "öffnen" +msgstr "offen" #: c3nav/mapdata/models/access.py:24 msgid "Groups" @@ -1723,8 +1727,8 @@ msgstr "Zugangserlaubnis-Token" msgid "Access Permission Tokens" msgstr "Zugangserlaubnis-Token" -#: c3nav/mapdata/models/access.py:133 c3nav/site/views.py:79 -#: c3nav/site/views.py:326 +#: c3nav/mapdata/models/access.py:133 c3nav/site/views.py:84 +#: c3nav/site/views.py:331 msgid "Area successfully unlocked." msgid_plural "Areas successfully unlocked." msgstr[0] "Bereich erfolgreich freigeschaltet." @@ -1976,6 +1980,7 @@ msgstr "Zielraum" #: c3nav/mapdata/models/geometry/space.py:308 #: c3nav/mapdata/models/geometry/space.py:347 c3nav/mapdata/models/graph.py:48 +#: c3nav/mapdata/models/report.py:63 msgid "description" msgstr "Beschreibung" @@ -2000,6 +2005,7 @@ msgid "Cross descriptions" msgstr "Durchschreitungsbeschreibungen" #: c3nav/mapdata/models/geometry/space.py:386 +#: c3nav/mapdata/models/report.py:111 msgid "comment" msgstr "Kommentar" @@ -2159,7 +2165,7 @@ msgstr "name eines material icons" msgid "searchable" msgstr "suchbar" -#: c3nav/mapdata/models/locations.py:162 c3nav/mapdata/models/locations.py:321 +#: c3nav/mapdata/models/locations.py:162 c3nav/mapdata/models/locations.py:323 msgid "Location Groups" msgstr "Ortgruppen" @@ -2215,7 +2221,7 @@ msgstr "Ortgruppenkategorie" msgid "Location Group Categories" msgstr "Ortgruppenkategorien" -#: c3nav/mapdata/models/locations.py:309 c3nav/mapdata/models/locations.py:342 +#: c3nav/mapdata/models/locations.py:309 c3nav/mapdata/models/locations.py:344 msgid "Category" msgstr "Kategorie" @@ -2228,65 +2234,162 @@ msgid "unless location specifies otherwise" msgstr "kann von Orten überschrieben werden" #: c3nav/mapdata/models/locations.py:315 +msgid "for missing locations" +msgstr "for fehlende Orte" + +#: c3nav/mapdata/models/locations.py:316 +msgid "can be used when reporting a missing location" +msgstr "kann beim melden eines fehlenden Ortges genutzt werden" + +#: c3nav/mapdata/models/locations.py:317 msgid "background color" msgstr "Hintergrundfarbe" -#: c3nav/mapdata/models/locations.py:320 +#: c3nav/mapdata/models/locations.py:322 msgid "Location Group" msgstr "Ortgruppe" -#: c3nav/mapdata/models/locations.py:344 c3nav/mapdata/models/locations.py:359 +#: c3nav/mapdata/models/locations.py:346 c3nav/mapdata/models/locations.py:361 msgid "color" msgstr "Farbe" -#: c3nav/mapdata/models/locations.py:345 +#: c3nav/mapdata/models/locations.py:347 msgid "priority" msgstr "Priorität" -#: c3nav/mapdata/models/locations.py:355 +#: c3nav/mapdata/models/locations.py:357 msgid "search" msgstr "suchen" -#: c3nav/mapdata/models/locations.py:357 +#: c3nav/mapdata/models/locations.py:359 msgid "describe" msgstr "beschreiben" -#: c3nav/mapdata/models/locations.py:361 +#: c3nav/mapdata/models/locations.py:363 msgid "internal" msgstr "intern" -#: c3nav/mapdata/models/locations.py:378 +#: c3nav/mapdata/models/locations.py:380 #, python-brace-format msgid "{category_title}, {num_locations}" msgstr "{category_title}, {num_locations}" -#: c3nav/mapdata/models/locations.py:380 +#: c3nav/mapdata/models/locations.py:382 #, python-format msgid "%(num)d location" msgid_plural "%(num)d locations" msgstr[0] "%(num)d Ort" msgstr[1] "%(num)d Orte" -#: c3nav/mapdata/models/locations.py:403 +#: c3nav/mapdata/models/locations.py:405 msgid "target" msgstr "Ziel" -#: c3nav/mapdata/models/locations.py:422 +#: c3nav/mapdata/models/locations.py:424 msgid "min zoom" msgstr "Mindestzoom" -#: c3nav/mapdata/models/locations.py:425 +#: c3nav/mapdata/models/locations.py:427 msgid "max zoom" msgstr "Maximalzoom" -#: c3nav/mapdata/models/locations.py:428 +#: c3nav/mapdata/models/locations.py:430 msgid "font size" msgstr "Schriftgröße" -#: c3nav/mapdata/models/locations.py:444 c3nav/mapdata/models/locations.py:445 +#: c3nav/mapdata/models/locations.py:446 c3nav/mapdata/models/locations.py:447 msgid "Label Settings" msgstr "Labeleinstellungen" +#: c3nav/mapdata/models/report.py:52 +msgid "location issue" +msgstr "Ortfehler" + +#: c3nav/mapdata/models/report.py:53 +msgid "missing location" +msgstr "fehlender Ort" + +#: c3nav/mapdata/models/report.py:54 +msgid "route issue" +msgstr "Routenfehler" + +#: c3nav/mapdata/models/report.py:57 +msgid "category" +msgstr "Kategorie" + +#: c3nav/mapdata/models/report.py:60 +msgid "last_update" +msgstr "letzte Anderung" + +#: c3nav/mapdata/models/report.py:61 +msgid "title" +msgstr "Titel" + +#: c3nav/mapdata/models/report.py:62 +msgid "a short title for your report" +msgstr "ein kurzer Titel für deine Meldung" + +#: c3nav/mapdata/models/report.py:64 +msgid "tell us precisely what's wrong" +msgstr "Sag uns im Detail was falsch ist" + +#: c3nav/mapdata/models/report.py:68 +msgid "location" +msgstr "Ort" + +#: c3nav/mapdata/models/report.py:69 +msgid "coordinates" +msgstr "Koordinaten" + +#: c3nav/mapdata/models/report.py:70 +msgid "origin" +msgstr "Start" + +#: c3nav/mapdata/models/report.py:71 +msgid "destination" +msgstr "Ziel" + +#: c3nav/mapdata/models/report.py:72 +msgid "route options" +msgstr "Routenoptionen" + +#: c3nav/mapdata/models/report.py:74 +msgid "new location title" +msgstr "Neuer Ortstitel" + +#: c3nav/mapdata/models/report.py:75 +msgid "you have to supply a title in at least one language" +msgstr "Du must das Feld {field} in mindestens einer Sprache ausfüllen." + +#: c3nav/mapdata/models/report.py:76 +msgid "location groups" +msgstr "Ortgruppen" + +#: c3nav/mapdata/models/report.py:78 +msgid "select all groups that apply, if any" +msgstr "" + +#: c3nav/mapdata/models/report.py:86 +#: c3nav/site/templates/site/report_detail.html:6 +msgid "Report" +msgstr "Meldung" + +#: c3nav/mapdata/models/report.py:87 +msgid "Reports" +msgstr "Meldungen" + +#: c3nav/mapdata/models/report.py:114 +msgid "public" +msgstr "öffentlich" + +#: c3nav/mapdata/models/report.py:117 +msgid "Report update" +msgstr "Meldungsupdate" + +#: c3nav/mapdata/models/report.py:118 +msgid "Report updates" +msgstr "Meldungsupdates" + #: c3nav/mapdata/models/source.py:18 msgid "Source" msgstr "Vorlage" @@ -2408,7 +2511,7 @@ msgstr "Unerreichbarer Ort." msgid "No route found." msgstr "Keine Route gefunden." -#: c3nav/routing/api.py:110 +#: c3nav/routing/api.py:109 msgid "Invalid scan data." msgstr "Invalide Scandaten." @@ -2456,65 +2559,66 @@ msgstr "Invalider Scan. Unerlaubte Frequenz." msgid "Invalid Scan. Invalid last timestamp." msgstr "Invalider Scan. Letzter Zeitstempel ungültig." -#: c3nav/routing/models.py:21 c3nav/routing/models.py:22 +#: c3nav/routing/models.py:20 c3nav/routing/models.py:21 +#: c3nav/site/templates/site/fragment_report_meta.html:17 #: c3nav/site/templates/site/map.html:177 msgid "Route options" msgstr "Routenoptionen" -#: c3nav/routing/models.py:33 +#: c3nav/routing/models.py:32 msgid "Routing mode" msgstr "Routemodus" -#: c3nav/routing/models.py:34 +#: c3nav/routing/models.py:33 msgid "fastest" msgstr "schnellste" -#: c3nav/routing/models.py:34 +#: c3nav/routing/models.py:33 msgid "shortest" msgstr "kürzeste" -#: c3nav/routing/models.py:38 +#: c3nav/routing/models.py:37 msgid "Walk speed" msgstr "Gehgeschwindigkeit" -#: c3nav/routing/models.py:39 +#: c3nav/routing/models.py:38 msgid "slow" msgstr "langsam" -#: c3nav/routing/models.py:39 +#: c3nav/routing/models.py:38 msgid "default" msgstr "standard" -#: c3nav/routing/models.py:39 +#: c3nav/routing/models.py:38 msgid "fast" msgstr "schnell" -#: c3nav/routing/models.py:45 +#: c3nav/routing/models.py:44 msgid "allow" msgstr "erlaubt" -#: c3nav/routing/models.py:47 +#: c3nav/routing/models.py:46 msgid "avoid upwards" msgstr "aufwärts vermeiden" -#: c3nav/routing/models.py:48 +#: c3nav/routing/models.py:47 msgid "avoid downwards" msgstr "abwärts vermeiden" -#: c3nav/routing/models.py:49 +#: c3nav/routing/models.py:48 msgid "avoid completely" msgstr "komplett vermeiden" -#: c3nav/routing/models.py:51 +#: c3nav/routing/models.py:50 msgid "avoid" msgstr "vermeiden" -#: c3nav/routing/models.py:144 +#: c3nav/routing/models.py:143 #, python-format msgid "Unknown route option: %s" msgstr "Unbekannte Routenoption: %s" -#: c3nav/routing/models.py:148 +#: c3nav/routing/models.py:147 #, python-format msgid "Invalid value for route option %s." msgstr "Invalider Wert für Routenoption %s." @@ -2658,6 +2762,26 @@ msgstr "Du bist angemeldet als %(username)s." msgid "You can access the control panel." msgstr "Du kannst das Control Panel betreten." +#: c3nav/site/templates/site/fragment_report_meta.html:3 +msgid "You are reporting an issue with the following location:" +msgstr "Du meldest einen Fehler beim folgenden Ort:" + +#: c3nav/site/templates/site/fragment_report_meta.html:6 +msgid "You are reporting an missing location at the following position:" +msgstr "Du meldest einen fehlenden Ort an der folgenden Stelle." + +#: c3nav/site/templates/site/fragment_report_meta.html:9 +msgid "You are reporting an issue with the following route:" +msgstr "Du meldest einen Fehler bei der folgenden Route:" + +#: c3nav/site/templates/site/fragment_report_meta.html:11 +msgid "Origin" +msgstr "Start" + +#: c3nav/site/templates/site/fragment_report_meta.html:14 +msgid "Destination" +msgstr "Ziel" + #: c3nav/site/templates/site/language.html:7 msgid "Pick your language" msgstr "Sprache wählen" @@ -2697,11 +2821,11 @@ msgstr "Route von hier aus" #: c3nav/site/templates/site/map.html:52 c3nav/site/templates/site/map.html:149 #: c3nav/site/templates/site/map.html:170 +#: c3nav/site/templates/site/report_create.html:6 msgid "Report issue" msgstr "Fehler melden" #: c3nav/site/templates/site/map.html:56 c3nav/site/templates/site/map.html:153 -#| msgid "Select this location" msgid "Report missing location" msgstr "Fehlenden Ort melden" @@ -2770,39 +2894,70 @@ msgstr "" msgid "open in c3nav" msgstr "in c3nav öffnen" -#: c3nav/site/templates/site/report.html:6 -msgid "Coming soon" -msgstr "Coming soon" +#: c3nav/site/templates/site/report_create.html:13 +msgid "Submit" +msgstr "Absenden" -#: c3nav/site/views.py:71 c3nav/site/views.py:318 +#: c3nav/site/templates/site/report_detail.html:11 +msgid "closed" +msgstr "geschlossen" + +#: c3nav/site/templates/site/report_detail.html:15 +msgid "anonymous submission" +msgstr "anonyme Meldung" + +#: c3nav/site/templates/site/report_detail.html:17 +msgid "by" +msgstr "von" + +#: c3nav/site/templates/site/report_detail.html:35 +msgid "(none)" +msgstr "(keine)" + +#: c3nav/site/views.py:76 c3nav/site/views.py:323 msgid "You need to log in to unlock areas." msgstr "Du musst dich anmelden um Bereiche freizuschalten." -#: c3nav/site/views.py:206 +#: c3nav/site/views.py:211 msgid "Areas could not be unlocked because the token has expired." msgstr "" "Zugangserlaubnis konnte nicht gewährt werden weil der Code abgelaufen ist." -#: c3nav/site/views.py:249 +#: c3nav/site/views.py:254 msgid "account creation is currently disabled." msgstr "Benutzerregistrierung ist momentan deaktiviert." -#: c3nav/site/views.py:311 +#: c3nav/site/views.py:316 msgid "This token does not exist or was already redeemed." msgstr "Dieser Code existiert nicht oder wurde bereits eingelöst." -#: c3nav/site/views.py:331 +#: c3nav/site/views.py:336 msgid "Unlock area" msgid_plural "Unlock areas" msgstr[0] "Bereich freischalten" msgstr[1] "Bereiche freischalten" -#: c3nav/site/views.py:332 +#: c3nav/site/views.py:337 msgid "You have been invited to unlock the following area:" msgid_plural "You have been invited to unlock the following areas:" msgstr[0] "Du wurdest eingeladen, den folgenden Bereich freizuschalten:" msgstr[1] "Du wurdest eingeladen, die folgenden Bereiche freizuschalten:" +#: c3nav/site/views.py:408 +msgid "Your report was submitted." +msgstr "Deine Meldiung wurde abgesendet." + +#: c3nav/site/views.py:411 +msgid "You can keep track of it from your user dashboard." +msgstr "Du kannst sie in deinerm Benutzerdashboard verfolgen." + +#: c3nav/site/views.py:413 +msgid "You can keep track of it by revisiting the public URL mentioned below." +msgstr "Du kannst sie mit dem unten angegebenen öffentlichen link verfolgen." + +#~ msgid "Coming soon" +#~ msgstr "Coming soon" + #~ msgid "You can not edit this object." #~ msgstr "Du kannst dieses Objekt nicht bearbeiten." diff --git a/src/c3nav/mapdata/forms.py b/src/c3nav/mapdata/forms.py index c58d20c5..67713873 100644 --- a/src/c3nav/mapdata/forms.py +++ b/src/c3nav/mapdata/forms.py @@ -41,7 +41,8 @@ class I18nModelFormMixin(ModelForm): new_fields[sub_field_name] = CharField(label=field_title, required=False, initial=values[language].strip(), - max_length=model_field.i18n_max_length) + max_length=model_field.i18n_max_length, + help_text=form_field.help_text) if has_values: self.i18n_fields.append((model_field, values)) diff --git a/src/c3nav/mapdata/migrations/0078_reports.py b/src/c3nav/mapdata/migrations/0078_reports.py new file mode 100644 index 00000000..facab48b --- /dev/null +++ b/src/c3nav/mapdata/migrations/0078_reports.py @@ -0,0 +1,84 @@ +# Generated by Django 2.2.8 on 2019-12-24 15:52 + +import c3nav.mapdata.fields +import c3nav.mapdata.models.report +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('mapdata', '0077_obstacle_altitude'), + ] + + operations = [ + migrations.CreateModel( + name='Report', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True, verbose_name='created')), + ('category', models.CharField(choices=[('location-issue', 'location issue'), ('missing-location', 'missing location'), ('route-issue', 'route issue')], db_index=True, max_length=20, verbose_name='category')), + ('open', models.BooleanField(default=True, verbose_name='open')), + ('last_update', models.DateTimeField(auto_now=True, verbose_name='last_update')), + ('title', models.CharField(default='', help_text='a short title for your report', max_length=100, verbose_name='title')), + ('description', models.TextField(default='', help_text="tell us precisely what's wrong", max_length=1000, verbose_name='description')), + ('coordinates_id', models.CharField(max_length=48, null=True, verbose_name='coordinates')), + ('origin_id', models.CharField(max_length=48, null=True, verbose_name='origin')), + ('destination_id', models.CharField(max_length=48, null=True, verbose_name='destination')), + ('route_options', models.CharField(max_length=128, null=True, verbose_name='route options')), + ('created_title', c3nav.mapdata.fields.I18nField(fallback_any=True, help_text='you have to supply a title in at least one language', plural_name='titles', verbose_name='new location title')), + ('secret', models.CharField(default=c3nav.mapdata.models.report.get_report_secret, max_length=32, verbose_name='secret')), + ('assigned_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assigned_reports', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')), + ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='report', to=settings.AUTH_USER_MODEL, verbose_name='author')), + ], + options={ + 'verbose_name': 'Report', + 'verbose_name_plural': 'Reports', + 'default_related_name': 'report', + }, + ), + migrations.AlterModelOptions( + name='lineobstacle', + options={'default_related_name': 'lineobstacles', 'ordering': ('altitude', 'height'), 'verbose_name': 'Line Obstacle', 'verbose_name_plural': 'Line Obstacles'}, + ), + migrations.AlterModelOptions( + name='obstacle', + options={'default_related_name': 'obstacles', 'ordering': ('altitude', 'height'), 'verbose_name': 'Obstacle', 'verbose_name_plural': 'Obstacles'}, + ), + migrations.AddField( + model_name='locationgroup', + name='can_report_missing', + field=models.BooleanField(default=False, help_text='can be used when reporting a missing location', verbose_name='for missing locations'), + ), + migrations.CreateModel( + name='ReportUpdate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('datetime', models.DateTimeField(auto_now_add=True, verbose_name='datetime')), + ('open', models.NullBooleanField(verbose_name='open')), + ('comment', models.TextField(verbose_name='comment')), + ('public', models.BooleanField(verbose_name='public')), + ('assigned_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='report_update_assigns', to=settings.AUTH_USER_MODEL, verbose_name='assigned to')), + ('author', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, related_name='reportupdate', to=settings.AUTH_USER_MODEL, verbose_name='author')), + ('report', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reportupdate', to='mapdata.Report')), + ], + options={ + 'verbose_name': 'Report update', + 'verbose_name_plural': 'Report updates', + 'default_related_name': 'reportupdate', + }, + ), + migrations.AddField( + model_name='report', + name='created_groups', + field=models.ManyToManyField(blank=True, help_text='select all groups that apply, if any', limit_choices_to={'can_report_missing': True}, related_name='report', to='mapdata.LocationGroup', verbose_name='location groups'), + ), + migrations.AddField( + model_name='report', + name='location', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reports', to='mapdata.LocationSlug', verbose_name='location'), + ), + ] diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 1dbc74c4..2480e099 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -312,6 +312,8 @@ class LocationGroup(Location, models.Model): label_settings = models.ForeignKey('mapdata.LabelSettings', null=True, blank=True, on_delete=models.PROTECT, verbose_name=_('label settings'), help_text=_('unless location specifies otherwise')) + can_report_missing = models.BooleanField(default=False, verbose_name=_('for missing locations'), + help_text=_('can be used when reporting a missing location')) color = models.CharField(null=True, blank=True, max_length=32, verbose_name=_('background color')) objects = LocationGroupManager() diff --git a/src/c3nav/mapdata/models/report.py b/src/c3nav/mapdata/models/report.py new file mode 100644 index 00000000..0ceeed71 --- /dev/null +++ b/src/c3nav/mapdata/models/report.py @@ -0,0 +1,119 @@ +import string + +from django.conf import settings +from django.core.exceptions import ObjectDoesNotExist +from django.db import models +from django.utils.crypto import get_random_string +from django.utils.translation import ugettext_lazy as _ + +from c3nav.mapdata.fields import I18nField +from c3nav.mapdata.utils.locations import get_location_by_id_for_request + + +def get_report_secret(): + return get_random_string(32, string.ascii_letters) + + +class LocationById(): + def __init__(self): + super().__init__() + self.name = None + self.cached_id = None + self.cached_value = None + + def __set_name__(self, owner, name): + self.name = name + + def __get__(self, instance, owner=None): + value_id = getattr(instance, self.name+'_id') + if value_id is None: + self.cached_pk = None + self.cached_value = None + return None + + if value_id == self.cached_id: + return self.cached_value + + value = get_location_by_id_for_request(value_id, getattr(instance, 'request', None)) + if value is None: + raise ObjectDoesNotExist + self.cached_id = value_id + self.cached_value = value + return value + + def __set__(self, instance, value): + self.cached_id = value.pk + self.cached_value = value + setattr(instance, self.name+'_id', value.pk) + + +class Report(models.Model): + CATEGORIES = ( + ('location-issue', _('location issue')), + ('missing-location', _('missing location')), + ('route-issue', _('route issue')), + ) + created = models.DateTimeField(auto_now_add=True, verbose_name=_('created')) + category = models.CharField(max_length=20, db_index=True, choices=CATEGORIES, verbose_name=_('category')) + author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('author')) + open = models.BooleanField(default=True, verbose_name=_('open')) + last_update = models.DateTimeField(auto_now=True, verbose_name=_('last_update')) + title = models.CharField(max_length=100, default='', verbose_name=_('title'), + help_text=_('a short title for your report')) + description = models.TextField(max_length=1000, default='', verbose_name=_('description'), + help_text=_('tell us precisely what\'s wrong')) + assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, + related_name='assigned_reports', verbose_name=_('assigned to')) + location = models.ForeignKey('mapdata.LocationSlug', null=True, on_delete=models.SET_NULL, + related_name='reports', verbose_name=_('location')) + coordinates_id = models.CharField(_('coordinates'), null=True, max_length=48) + origin_id = models.CharField(_('origin'), null=True, max_length=48) + destination_id = models.CharField(_('destination'), null=True, max_length=48) + route_options = models.CharField(_('route options'), null=True, max_length=128) + + created_title = I18nField(_('new location title'), plural_name='titles', blank=False, fallback_any=True, + help_text=_('you have to supply a title in at least one language')) + created_groups = models.ManyToManyField('mapdata.LocationGroup', verbose_name=_('location groups'), blank=True, + limit_choices_to={'can_report_missing': True}, + help_text=_('select all groups that apply, if any')) + secret = models.CharField(_('secret'), max_length=32, default=get_report_secret) + + coordinates = LocationById() + origin = LocationById() + destination = LocationById() + + class Meta: + verbose_name = _('Report') + verbose_name_plural = _('Reports') + default_related_name = 'report' + + @property + def form_cls(self): + from c3nav.site.forms import ReportMissingLocationForm, ReportIssueForm + return ReportMissingLocationForm if self.category == 'missing-location' else ReportIssueForm + + @classmethod + def qs_for_request(cls, request): + if request.user.is_superuser: + # todo: permissions! + return cls.objects.all() + elif request.user.is_authenticated: + return cls.objects.filter(author=request.user) + else: + return cls.objects.none() + + +class ReportUpdate(models.Model): + report = models.ForeignKey(Report, on_delete=models.CASCADE) + datetime = models.DateTimeField(auto_now_add=True, verbose_name=_('datetime')) + author = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, verbose_name=_('author')) + open = models.NullBooleanField(verbose_name=_('open')) + comment = models.TextField(verbose_name=_('comment')) + assigned_to = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.PROTECT, + related_name='report_update_assigns', verbose_name=_('assigned to')) + public = models.BooleanField(verbose_name=_('public')) + + class Meta: + verbose_name = _('Report update') + verbose_name_plural = _('Report updates') + default_related_name = 'reportupdate' diff --git a/src/c3nav/mapdata/utils/locations.py b/src/c3nav/mapdata/utils/locations.py index 658e56be..7eb3ad24 100644 --- a/src/c3nav/mapdata/utils/locations.py +++ b/src/c3nav/mapdata/utils/locations.py @@ -418,3 +418,6 @@ class CustomLocation: @cached_property def subtitle(self): return self.title_subtitle[1] + + def get_icon(self): + return self.icon diff --git a/src/c3nav/routing/models.py b/src/c3nav/routing/models.py index 65bea5da..652a4f4f 100644 --- a/src/c3nav/routing/models.py +++ b/src/c3nav/routing/models.py @@ -174,6 +174,7 @@ class RouteOptions(models.Model): for choice_name, choice_title in field.choices ], 'value': self[name], + 'value_display': dict(field.choices)[self[name]], } for name, field in self.get_fields().items() ] @@ -181,6 +182,12 @@ class RouteOptions(models.Model): def serialize_string(self): return ','.join('%s=%s' % (key, val) for key, val in self.data.items()) + @classmethod + def unserialize_string(cls, data): + return RouteOptions( + data=dict(item.split('=') for item in data.split(',')) + ) + def save(self, *args, **kwargs): if self.request is None or self.request.user.is_authenticated: self.user = self.request.user diff --git a/src/c3nav/site/forms.py b/src/c3nav/site/forms.py new file mode 100644 index 00000000..b130827b --- /dev/null +++ b/src/c3nav/site/forms.py @@ -0,0 +1,16 @@ +from django.forms import ModelForm + +from c3nav.mapdata.forms import I18nModelFormMixin +from c3nav.mapdata.models.report import Report + + +class ReportIssueForm(I18nModelFormMixin, ModelForm): + class Meta: + model = Report + fields = ['title', 'description'] + + +class ReportMissingLocationForm(I18nModelFormMixin, ModelForm): + class Meta: + model = Report + fields = ['title', 'description', 'created_title', 'created_groups'] diff --git a/src/c3nav/site/static/site/css/c3nav.scss b/src/c3nav/site/static/site/css/c3nav.scss index b481f79e..9d669e97 100644 --- a/src/c3nav/site/static/site/css/c3nav.scss +++ b/src/c3nav/site/static/site/css/c3nav.scss @@ -547,6 +547,9 @@ main.show-options #resultswrapper #route-options { padding: 5px 10px 5px 53px; height: 55px; } +.location.location-form-value { + margin: -10px -10px 5px -10px; +} .location .icon { font-size: 36px; position: absolute; @@ -1256,6 +1259,25 @@ main .narrow p, main .narrow form, main .narrow button { main .narrow form button { width: 100%; } +main form > p, #modal form > p { + margin-bottom: 15px; + > :last-child { + margin-bottom: 0; + } + .helptext { + display: block; + margin-top: -15px; + font-style: italic; + color: #999999; + } + textarea { + resize: none; + height: 100px; + } + select { + height: 100px; + } +} .user-permissions-form label { font-weight: 400; diff --git a/src/c3nav/site/templates/site/account_form.html b/src/c3nav/site/templates/site/account_form.html index 23264994..4f69c273 100644 --- a/src/c3nav/site/templates/site/account_form.html +++ b/src/c3nav/site/templates/site/account_form.html @@ -14,7 +14,7 @@
{% csrf_token %} - {{ form }} + {{ form.as_p }} {% if bottom_link_url %} {{ bottom_link_text }} diff --git a/src/c3nav/site/templates/site/fragment_location.html b/src/c3nav/site/templates/site/fragment_location.html new file mode 100644 index 00000000..05285d11 --- /dev/null +++ b/src/c3nav/site/templates/site/fragment_location.html @@ -0,0 +1,5 @@ +
+ {% if location.get_icon %}{{ location.get_icon }}{% else %}place{% endif %} + {{ location.title }} + {% if add_subtitle %}{{ add_subtitle }}, {% endif %}{{ location.subtitle }} +
diff --git a/src/c3nav/site/templates/site/fragment_report_meta.html b/src/c3nav/site/templates/site/fragment_report_meta.html new file mode 100644 index 00000000..8a1e8155 --- /dev/null +++ b/src/c3nav/site/templates/site/fragment_report_meta.html @@ -0,0 +1,22 @@ +{% load i18n %} +{% if report.category == 'location-issue' %} +

{% trans 'You are reporting an issue with the following location:' %}

+ {% include 'site/fragment_location.html' with form_value=1 location=report.location %} +{% elif report.category == 'missing-location' %} +

{% trans 'You are reporting an missing location at the following position:' %}

+ {% include 'site/fragment_location.html' with form_value=1 location=report.coordinates add_subtitle=report.coordinates_id %} +{% elif report.category == 'route-issue' %} +

{% trans 'You are reporting an issue with the following route:' %}

+ + + {% include 'site/fragment_location.html' with form_value=1 location=report.origin %} + + + {% include 'site/fragment_location.html' with form_value=1 location=report.destination %} + + + {% for option in options.serialize %} + {{ option.label }}: {{ option.value_display }}
+ {% endfor %} +

+{% endif %} diff --git a/src/c3nav/site/templates/site/report.html b/src/c3nav/site/templates/site/report.html deleted file mode 100644 index 53e02dd0..00000000 --- a/src/c3nav/site/templates/site/report.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends 'site/base.html' %} -{% load i18n %} - -{% block content %} -
-

{% trans 'Coming soon' %}

- - {% include 'site/fragment_messages.html' %} -
-{% endblock %} diff --git a/src/c3nav/site/templates/site/report_create.html b/src/c3nav/site/templates/site/report_create.html new file mode 100644 index 00000000..1a9dd7af --- /dev/null +++ b/src/c3nav/site/templates/site/report_create.html @@ -0,0 +1,16 @@ +{% extends 'site/base.html' %} +{% load i18n %} + +{% block content %} +
+

{% trans 'Report issue' %}

+ {% include 'site/fragment_messages.html' %} + + + {% csrf_token %} + {% include 'site/fragment_report_meta.html' %} + {{ form.as_p }} + + +
+{% endblock %} diff --git a/src/c3nav/site/templates/site/report_detail.html b/src/c3nav/site/templates/site/report_detail.html new file mode 100644 index 00000000..3e726a0f --- /dev/null +++ b/src/c3nav/site/templates/site/report_detail.html @@ -0,0 +1,44 @@ +{% extends 'site/base.html' %} +{% load i18n %} + +{% block content %} +
+

{% trans 'Report' %}: {{ report.title }}

+

+ {% if report.open %} + {% trans 'open' %} + {% else %} + {% trans 'closed' %} + {% endif %} + – + {% if report.author %} + {% trans 'anonymous submission' %} + {% else %} + {% trans 'by' %} {{ request.author.username }} + {% endif %} + – + {{ report.created }} +

+ {% include 'site/fragment_messages.html' %} + {% include 'site/fragment_report_meta.html' %} + + {% for field in form %} + {% if field.name != 'title' %} +

+ {{ field.label }}:
+ {% if field.name == 'description' %} + {{ report.description | linebreaksbr }} + {% elif field.name == 'created_groups' %} + {% for group in report.created_groups.all %} + {{ group.title }}
+ {% empty %} + {% trans '(none)' %} + {% endfor %} + {% else %} + {{ field.value }} + {% endif %} +

+ {% endif %} + {% endfor %} +
+{% endblock %} diff --git a/src/c3nav/site/urls.py b/src/c3nav/site/urls.py index 67ea455d..529bb36e 100644 --- a/src/c3nav/site/urls.py +++ b/src/c3nav/site/urls.py @@ -1,7 +1,7 @@ from django.conf.urls import url from c3nav.site.views import (about_view, access_redeem_view, account_view, change_password_view, choose_language, - login_view, logout_view, map_index, qr_code, register_view, report_view) + login_view, logout_view, map_index, qr_code, register_view, report_create, report_detail) slug = r'(?P[a-z0-9-_.:]+)' coordinates = r'(?P[a-z0-9-_:]+:-?\d+(\.\d+)?:-?\d+(\.\d+)?)' @@ -27,7 +27,10 @@ urlpatterns = [ url(r'^lang/$', choose_language, name='site.language'), url(r'^about/$', about_view, name='site.about'), url(r'^report/$', about_view, name='site.about'), - url(r'^report/l/%s/$' % coordinates, report_view, name='site.report'), - url(r'^report/l/(?P\d+)/$', report_view, name='site.report'), - url(r'^report/r/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/$', report_view, name='site.report'), + url(r'^report/(?P\d+)/$', report_detail, name='site.report_detail'), + url(r'^report/(?P\d+)/(?P[^/]+)/$', report_detail, name='site.report_detail'), + url(r'^report/l/%s/$' % coordinates, report_create, name='site.report_create'), + url(r'^report/l/(?P\d+)/$', report_create, name='site.report_create'), + url(r'^report/r/(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/$', + report_create, name='site.report_create'), ] diff --git a/src/c3nav/site/views.py b/src/c3nav/site/views.py index 51b04e76..4fbfacda 100644 --- a/src/c3nav/site/views.py +++ b/src/c3nav/site/views.py @@ -9,11 +9,12 @@ from django.contrib.auth import login, logout from django.contrib.auth.decorators import login_required from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm, UserCreationForm from django.contrib.auth.views import redirect_to_login +from django.core.exceptions import ObjectDoesNotExist, SuspiciousOperation from django.core.serializers.json import DjangoJSONEncoder from django.db import transaction -from django.http import HttpResponse, HttpResponseBadRequest +from django.http import Http404, HttpResponse, HttpResponseBadRequest from django.middleware import csrf -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.utils.translation import ugettext_lazy as _ @@ -27,9 +28,12 @@ from c3nav.mapdata.grid import grid from c3nav.mapdata.models import Location, Source from c3nav.mapdata.models.access import AccessPermissionToken from c3nav.mapdata.models.locations import LocationRedirect, SpecificLocation -from c3nav.mapdata.utils.locations import get_location_by_slug_for_request, levels_by_short_label_for_request +from c3nav.mapdata.models.report import Report +from c3nav.mapdata.utils.locations import (get_location_by_id_for_request, get_location_by_slug_for_request, + levels_by_short_label_for_request) from c3nav.mapdata.utils.user import can_access_editor, get_user_data from c3nav.mapdata.views import set_tile_access_cookie +from c3nav.routing.models import RouteOptions from c3nav.site.models import Announcement, SiteUpdate @@ -350,6 +354,86 @@ def about_view(request): }) +def get_report_location_for_request(pk, request): + location = get_location_by_id_for_request(pk, request) + if location is None: + raise Http404 + return location + + @never_cache -def report_view(request, coordinates=None, location=None, origin=None, destination=None, options=None): - return render(request, 'site/report.html', {}) +def report_create(request, coordinates=None, location=None, origin=None, destination=None, options=None): + report = Report() + report.request = request + + if coordinates: + report.category = 'missing-location' + report.coordinates_id = coordinates + try: + report.coordinates + except ObjectDoesNotExist: + raise Http404 + elif location: + report.category = 'location-issue' + report.location = get_report_location_for_request(location, request) + if report.location is None: + raise Http404 + report.location = location + elif origin: + report.category = 'route-issue' + report.origin_id = origin + report.destination_id = destination + try: + # noinspection PyStatementEffect + report.origin + # noinspection PyStatementEffect + report.destination + except ObjectDoesNotExist: + raise Http404 + try: + options = RouteOptions.unserialize_string(options) + except Exception: + raise SuspiciousOperation + report.options = options.serialize_string() + + if request.method == 'POST': + form = report.form_cls(instance=report, data=request.POST) + if form.is_valid(): + report = form.instance + if request.user.is_authenticated: + report.author = request.user + report.save() + + success_messages = [_('Your report was submitted.')] + success_kwargs = {'pk': report.pk} + if request.user.is_authenticated: + success_messages.append(_('You can keep track of it from your user dashboard.')) + else: + success_messages.append(_('You can keep track of it by revisiting the public URL mentioned below.')) + success_kwargs = {'secret': report.secret} + messages.success(request, ' '.join(str(s) for s in success_messages)) + return redirect(reverse('site.report_detail', kwargs=success_kwargs)) + else: + form = report.form_cls(instance=report) + + return render(request, 'site/report_create.html', { + 'report': report, + 'options': options, + 'form': form, + }) + + +def report_detail(request, pk, secret=None): + if secret: + qs = Report.objects.filter(secret=secret) + else: + qs = Report.qs_for_request(request) + report = get_object_or_404(qs, pk=pk) + report.request = request + + form = report.form_cls(instance=report) + + return render(request, 'site/report_detail.html', { + 'report': report, + 'form': form, + })