diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2023-05-10 19:50:26 +0200 | 
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2023-05-10 19:50:26 +0200 | 
| commit | 844a652e47b3a9d5fd44eee8ec0d6a49b9bde91c (patch) | |
| tree | 4ef239d0a84537f73f873714bbf66a8929dacb13 | |
| parent | 5b40a857d02b8768c5bc14306d1934ed354b38d6 (diff) | |
| parent | 8b0ae0417d914541157c10404f11c3d0f7dffbc0 (diff) | |
| download | fietsboek-844a652e47b3a9d5fd44eee8ec0d6a49b9bde91c.tar.gz fietsboek-844a652e47b3a9d5fd44eee8ec0d6a49b9bde91c.tar.bz2 fietsboek-844a652e47b3a9d5fd44eee8ec0d6a49b9bde91c.zip  | |
Merge branch 'browse-sorting'
| -rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.mo | bin | 13379 -> 13563 bytes | |||
| -rw-r--r-- | fietsboek/locale/de/LC_MESSAGES/messages.po | 75 | ||||
| -rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.mo | bin | 12561 -> 12726 bytes | |||
| -rw-r--r-- | fietsboek/locale/en/LC_MESSAGES/messages.po | 72 | ||||
| -rw-r--r-- | fietsboek/locale/fietslog.pot | 72 | ||||
| -rw-r--r-- | fietsboek/templates/browse.jinja2 | 10 | ||||
| -rw-r--r-- | fietsboek/views/browse.py | 145 | 
7 files changed, 244 insertions, 130 deletions
diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.mo b/fietsboek/locale/de/LC_MESSAGES/messages.mo Binary files differindex 0fa3ba1..eb24766 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/de/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/de/LC_MESSAGES/messages.po b/fietsboek/locale/de/LC_MESSAGES/messages.po index bb60809..0c32f64 100644 --- a/fietsboek/locale/de/LC_MESSAGES/messages.po +++ b/fietsboek/locale/de/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid ""  msgstr ""  "Project-Id-Version: PROJECT VERSION\n"  "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-04-23 10:47+0200\n" +"POT-Creation-Date: 2023-05-09 19:55+0200\n"  "PO-Revision-Date: 2022-07-02 17:35+0200\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language: de\n" @@ -16,7 +16,7 @@ msgstr ""  "MIME-Version: 1.0\n"  "Content-Type: text/plain; charset=utf-8\n"  "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.11.0\n" +"Generated-By: Babel 2.12.1\n"  #: fietsboek/util.py:280  msgid "password_constraint.mismatch" @@ -146,73 +146,85 @@ msgstr "Filter zurücksetzen"  msgid "page.browse.filters.expand_advanced"  msgstr "Erweitert" -#: fietsboek/templates/browse.jinja2:122 +#: fietsboek/templates/browse.jinja2:110 fietsboek/templates/browse.jinja2:111 +msgid "page.browse.sort.date" +msgstr "Nach Datum sortieren" + +#: fietsboek/templates/browse.jinja2:112 fietsboek/templates/browse.jinja2:113 +msgid "page.browse.sort.length" +msgstr "Nach Länge sortieren" + +#: fietsboek/templates/browse.jinja2:114 fietsboek/templates/browse.jinja2:115 +msgid "page.browse.sort.duration" +msgstr "Nach Dauer sortieren" + +#: fietsboek/templates/browse.jinja2:132  msgid "page.browse.organic_tooltip"  msgstr "Dies ist eine Aufnahme einer Strecke" -#: fietsboek/templates/browse.jinja2:124 +#: fietsboek/templates/browse.jinja2:134  msgid "page.browse.synthetic_tooltip"  msgstr "Dies ist eine geplante Strecke" -#: fietsboek/templates/browse.jinja2:132 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:142 fietsboek/templates/details.jinja2:90  #: fietsboek/templates/profile.jinja2:15  msgid "page.details.date"  msgstr "Datum" -#: fietsboek/templates/browse.jinja2:134 fietsboek/templates/details.jinja2:104 +#: fietsboek/templates/browse.jinja2:144 fietsboek/templates/details.jinja2:104  #: fietsboek/templates/profile.jinja2:17  msgid "page.details.length"  msgstr "Länge" -#: fietsboek/templates/browse.jinja2:139 fietsboek/templates/details.jinja2:95 +#: fietsboek/templates/browse.jinja2:149 fietsboek/templates/details.jinja2:95  #: fietsboek/templates/profile.jinja2:21  msgid "page.details.start_time"  msgstr "Startzeit" -#: fietsboek/templates/browse.jinja2:141 fietsboek/templates/details.jinja2:99 +#: fietsboek/templates/browse.jinja2:151 fietsboek/templates/details.jinja2:99  #: fietsboek/templates/profile.jinja2:23  msgid "page.details.end_time"  msgstr "Endzeit" -#: fietsboek/templates/browse.jinja2:146 fietsboek/templates/details.jinja2:108 +#: fietsboek/templates/browse.jinja2:156 fietsboek/templates/details.jinja2:108  #: fietsboek/templates/profile.jinja2:27  msgid "page.details.uphill"  msgstr "Bergauf" -#: fietsboek/templates/browse.jinja2:148 fietsboek/templates/details.jinja2:112 +#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:112  #: fietsboek/templates/profile.jinja2:29  msgid "page.details.downhill"  msgstr "Bergab" -#: fietsboek/templates/browse.jinja2:153 fietsboek/templates/details.jinja2:117 +#: fietsboek/templates/browse.jinja2:163 fietsboek/templates/details.jinja2:117  #: fietsboek/templates/profile.jinja2:33  msgid "page.details.moving_time"  msgstr "Fahrzeit" -#: fietsboek/templates/browse.jinja2:155 fietsboek/templates/details.jinja2:121 +#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:121  #: fietsboek/templates/profile.jinja2:35  msgid "page.details.stopped_time"  msgstr "Haltezeit" -#: fietsboek/templates/browse.jinja2:159 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125  #: fietsboek/templates/profile.jinja2:39  msgid "page.details.max_speed"  msgstr "maximale Geschwindigkeit" -#: fietsboek/templates/browse.jinja2:161 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129  #: fietsboek/templates/profile.jinja2:41  msgid "page.details.avg_speed"  msgstr "durchschnittliche Geschwindigkeit" -#: fietsboek/templates/browse.jinja2:179 +#: fietsboek/templates/browse.jinja2:189  msgid "page.browse.download_multiple"  msgstr "ausgewählte Herunterladen" -#: fietsboek/templates/browse.jinja2:181 +#: fietsboek/templates/browse.jinja2:191  msgid "page.browse.no_results"  msgstr "Es wurden keine Strecken gefunden, die den Filtern entsprechen." -#: fietsboek/templates/browse.jinja2:183 +#: fietsboek/templates/browse.jinja2:193  msgid "page.browse.no_tracks"  msgstr ""  "Es wurden keine Strecken gefunden, auf die Du Zugriff hast. Versuche, " @@ -476,14 +488,14 @@ msgstr ""  "Es sind noch nicht abgeschlossene Uploads vorhanden. Klicke auf die "  "Links, um sie fortzusetzen:" -#: fietsboek/templates/home.jinja2:22 fietsboek/templates/home.jinja2:29 -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.summary.track"  msgid_plural "page.home.summary.tracks"  msgstr[0] "%(num)d Strecke"  msgstr[1] "%(num)d Strecken" -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.total"  msgstr "Gesamt" @@ -729,8 +741,7 @@ msgstr "Pausen entfernen"  #: fietsboek/transformers/breaks.py:35  msgid "transformers.remove-breaks.description" -msgstr "" -"Diese Transformation entfernt längere Pausen aus der Aufnahme." +msgstr "Diese Transformation entfernt längere Pausen aus der Aufnahme."  #: fietsboek/views/account.py:54  msgid "flash.invalid_name" @@ -767,35 +778,35 @@ msgstr "Wappen bearbeitet"  msgid "flash.badge_deleted"  msgstr "Wappen gelöscht" -#: fietsboek/views/default.py:117 +#: fietsboek/views/default.py:121  msgid "flash.invalid_credentials"  msgstr "Ungültige Nutzerdaten" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:125  msgid "flash.account_not_verified"  msgstr "Konto noch nicht bestätigt" -#: fietsboek/views/default.py:124 +#: fietsboek/views/default.py:128  msgid "flash.logged_in"  msgstr "Du bist nun angemeldet" -#: fietsboek/views/default.py:146 +#: fietsboek/views/default.py:150  msgid "flash.logged_out"  msgstr "Du bist nun abgemeldet" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:184  msgid "flash.reset_invalid_email"  msgstr "Ungültige E-Mail-Adresse angegeben" -#: fietsboek/views/default.py:185 +#: fietsboek/views/default.py:189  msgid "flash.password_token_generated"  msgstr "Ein Link zum Zurücksetzen des Passworts wurde versandt" -#: fietsboek/views/default.py:190 +#: fietsboek/views/default.py:194  msgid "page.password_reset.email.subject"  msgstr "Fietsboek Passwortzurücksetzung" -#: fietsboek/views/default.py:193 +#: fietsboek/views/default.py:197  msgid "page.password_reset.email.body"  msgstr ""  "Du kannst Dein Fietsboek-Passwort hier zurücksetzen: {}\n" @@ -803,11 +814,11 @@ msgstr ""  "Falls Du keine Passwortzurücksetzung beantragt hast, dann ignoriere diese"  " E-Mail." -#: fietsboek/views/default.py:226 +#: fietsboek/views/default.py:230  msgid "flash.email_verified"  msgstr "E-Mail-Adresse bestätigt" -#: fietsboek/views/default.py:240 +#: fietsboek/views/default.py:244  msgid "flash.password_updated"  msgstr "Passwort aktualisiert" diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.mo b/fietsboek/locale/en/LC_MESSAGES/messages.mo Binary files differindex 2b8aa2d..055dbd8 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.mo +++ b/fietsboek/locale/en/LC_MESSAGES/messages.mo diff --git a/fietsboek/locale/en/LC_MESSAGES/messages.po b/fietsboek/locale/en/LC_MESSAGES/messages.po index e3914e3..8f8d1f3 100644 --- a/fietsboek/locale/en/LC_MESSAGES/messages.po +++ b/fietsboek/locale/en/LC_MESSAGES/messages.po @@ -7,7 +7,7 @@ msgid ""  msgstr ""  "Project-Id-Version: PROJECT VERSION\n"  "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-04-23 10:47+0200\n" +"POT-Creation-Date: 2023-05-09 19:55+0200\n"  "PO-Revision-Date: 2023-04-03 20:42+0200\n"  "Last-Translator: \n"  "Language: en\n" @@ -16,7 +16,7 @@ msgstr ""  "MIME-Version: 1.0\n"  "Content-Type: text/plain; charset=utf-8\n"  "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.11.0\n" +"Generated-By: Babel 2.12.1\n"  #: fietsboek/util.py:280  msgid "password_constraint.mismatch" @@ -146,73 +146,85 @@ msgstr "Remove filters"  msgid "page.browse.filters.expand_advanced"  msgstr "Advanced" -#: fietsboek/templates/browse.jinja2:122 +#: fietsboek/templates/browse.jinja2:110 fietsboek/templates/browse.jinja2:111 +msgid "page.browse.sort.date" +msgstr "Sort by date" + +#: fietsboek/templates/browse.jinja2:112 fietsboek/templates/browse.jinja2:113 +msgid "page.browse.sort.length" +msgstr "Sort by length" + +#: fietsboek/templates/browse.jinja2:114 fietsboek/templates/browse.jinja2:115 +msgid "page.browse.sort.duration" +msgstr "Sort by duration" + +#: fietsboek/templates/browse.jinja2:132  msgid "page.browse.organic_tooltip"  msgstr "This is a recording of a track" -#: fietsboek/templates/browse.jinja2:124 +#: fietsboek/templates/browse.jinja2:134  msgid "page.browse.synthetic_tooltip"  msgstr "This is a pre-planned track" -#: fietsboek/templates/browse.jinja2:132 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:142 fietsboek/templates/details.jinja2:90  #: fietsboek/templates/profile.jinja2:15  msgid "page.details.date"  msgstr "Date" -#: fietsboek/templates/browse.jinja2:134 fietsboek/templates/details.jinja2:104 +#: fietsboek/templates/browse.jinja2:144 fietsboek/templates/details.jinja2:104  #: fietsboek/templates/profile.jinja2:17  msgid "page.details.length"  msgstr "Length" -#: fietsboek/templates/browse.jinja2:139 fietsboek/templates/details.jinja2:95 +#: fietsboek/templates/browse.jinja2:149 fietsboek/templates/details.jinja2:95  #: fietsboek/templates/profile.jinja2:21  msgid "page.details.start_time"  msgstr "Record Start" -#: fietsboek/templates/browse.jinja2:141 fietsboek/templates/details.jinja2:99 +#: fietsboek/templates/browse.jinja2:151 fietsboek/templates/details.jinja2:99  #: fietsboek/templates/profile.jinja2:23  msgid "page.details.end_time"  msgstr "Record End" -#: fietsboek/templates/browse.jinja2:146 fietsboek/templates/details.jinja2:108 +#: fietsboek/templates/browse.jinja2:156 fietsboek/templates/details.jinja2:108  #: fietsboek/templates/profile.jinja2:27  msgid "page.details.uphill"  msgstr "Uphill" -#: fietsboek/templates/browse.jinja2:148 fietsboek/templates/details.jinja2:112 +#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:112  #: fietsboek/templates/profile.jinja2:29  msgid "page.details.downhill"  msgstr "Downhill" -#: fietsboek/templates/browse.jinja2:153 fietsboek/templates/details.jinja2:117 +#: fietsboek/templates/browse.jinja2:163 fietsboek/templates/details.jinja2:117  #: fietsboek/templates/profile.jinja2:33  msgid "page.details.moving_time"  msgstr "Moving Time" -#: fietsboek/templates/browse.jinja2:155 fietsboek/templates/details.jinja2:121 +#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:121  #: fietsboek/templates/profile.jinja2:35  msgid "page.details.stopped_time"  msgstr "Stopped Time" -#: fietsboek/templates/browse.jinja2:159 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125  #: fietsboek/templates/profile.jinja2:39  msgid "page.details.max_speed"  msgstr "Max Speed" -#: fietsboek/templates/browse.jinja2:161 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129  #: fietsboek/templates/profile.jinja2:41  msgid "page.details.avg_speed"  msgstr "Average Speed" -#: fietsboek/templates/browse.jinja2:179 +#: fietsboek/templates/browse.jinja2:189  msgid "page.browse.download_multiple"  msgstr "Download selected" -#: fietsboek/templates/browse.jinja2:181 +#: fietsboek/templates/browse.jinja2:191  msgid "page.browse.no_results"  msgstr "No results matching the filters were found." -#: fietsboek/templates/browse.jinja2:183 +#: fietsboek/templates/browse.jinja2:193  msgid "page.browse.no_tracks"  msgstr "You currently do not have access to any tracks. Try logging in." @@ -470,14 +482,14 @@ msgstr "Home"  msgid "page.home.unfinished_uploads"  msgstr "You have unfinished uploads. Click on the links below to resume them:" -#: fietsboek/templates/home.jinja2:22 fietsboek/templates/home.jinja2:29 -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.summary.track"  msgid_plural "page.home.summary.tracks"  msgstr[0] "%(num)d track"  msgstr[1] "%(num)d tracks" -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.total"  msgstr "Total" @@ -758,46 +770,46 @@ msgstr "Badge has been modified"  msgid "flash.badge_deleted"  msgstr "Badge has been deleted" -#: fietsboek/views/default.py:117 +#: fietsboek/views/default.py:121  msgid "flash.invalid_credentials"  msgstr "Invalid login credentials" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:125  msgid "flash.account_not_verified"  msgstr "Your account is not verified yet" -#: fietsboek/views/default.py:124 +#: fietsboek/views/default.py:128  msgid "flash.logged_in"  msgstr "You are now logged in" -#: fietsboek/views/default.py:146 +#: fietsboek/views/default.py:150  msgid "flash.logged_out"  msgstr "You have been logged out" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:184  msgid "flash.reset_invalid_email"  msgstr "Invalid email address provided" -#: fietsboek/views/default.py:185 +#: fietsboek/views/default.py:189  msgid "flash.password_token_generated"  msgstr "A password reset email has been sent" -#: fietsboek/views/default.py:190 +#: fietsboek/views/default.py:194  msgid "page.password_reset.email.subject"  msgstr "Fietsboek Password Reset" -#: fietsboek/views/default.py:193 +#: fietsboek/views/default.py:197  msgid "page.password_reset.email.body"  msgstr ""  "You can reset your Fietsboek password here: {}\n"  "\n"  "If you did not request a password reset, ignore this email." -#: fietsboek/views/default.py:226 +#: fietsboek/views/default.py:230  msgid "flash.email_verified"  msgstr "Your email address has been verified" -#: fietsboek/views/default.py:240 +#: fietsboek/views/default.py:244  msgid "flash.password_updated"  msgstr "Password has been updated" diff --git a/fietsboek/locale/fietslog.pot b/fietsboek/locale/fietslog.pot index 6cebc11..8cab0ed 100644 --- a/fietsboek/locale/fietslog.pot +++ b/fietsboek/locale/fietslog.pot @@ -8,14 +8,14 @@ msgid ""  msgstr ""  "Project-Id-Version: PROJECT VERSION\n"  "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2023-04-23 10:47+0200\n" +"POT-Creation-Date: 2023-05-09 19:55+0200\n"  "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"  "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"  "Language-Team: LANGUAGE <LL@li.org>\n"  "MIME-Version: 1.0\n"  "Content-Type: text/plain; charset=utf-8\n"  "Content-Transfer-Encoding: 8bit\n" -"Generated-By: Babel 2.11.0\n" +"Generated-By: Babel 2.12.1\n"  #: fietsboek/util.py:280  msgid "password_constraint.mismatch" @@ -145,73 +145,85 @@ msgstr ""  msgid "page.browse.filters.expand_advanced"  msgstr "" -#: fietsboek/templates/browse.jinja2:122 +#: fietsboek/templates/browse.jinja2:110 fietsboek/templates/browse.jinja2:111 +msgid "page.browse.sort.date" +msgstr "" + +#: fietsboek/templates/browse.jinja2:112 fietsboek/templates/browse.jinja2:113 +msgid "page.browse.sort.length" +msgstr "" + +#: fietsboek/templates/browse.jinja2:114 fietsboek/templates/browse.jinja2:115 +msgid "page.browse.sort.duration" +msgstr "" + +#: fietsboek/templates/browse.jinja2:132  msgid "page.browse.organic_tooltip"  msgstr "" -#: fietsboek/templates/browse.jinja2:124 +#: fietsboek/templates/browse.jinja2:134  msgid "page.browse.synthetic_tooltip"  msgstr "" -#: fietsboek/templates/browse.jinja2:132 fietsboek/templates/details.jinja2:90 +#: fietsboek/templates/browse.jinja2:142 fietsboek/templates/details.jinja2:90  #: fietsboek/templates/profile.jinja2:15  msgid "page.details.date"  msgstr "" -#: fietsboek/templates/browse.jinja2:134 fietsboek/templates/details.jinja2:104 +#: fietsboek/templates/browse.jinja2:144 fietsboek/templates/details.jinja2:104  #: fietsboek/templates/profile.jinja2:17  msgid "page.details.length"  msgstr "" -#: fietsboek/templates/browse.jinja2:139 fietsboek/templates/details.jinja2:95 +#: fietsboek/templates/browse.jinja2:149 fietsboek/templates/details.jinja2:95  #: fietsboek/templates/profile.jinja2:21  msgid "page.details.start_time"  msgstr "" -#: fietsboek/templates/browse.jinja2:141 fietsboek/templates/details.jinja2:99 +#: fietsboek/templates/browse.jinja2:151 fietsboek/templates/details.jinja2:99  #: fietsboek/templates/profile.jinja2:23  msgid "page.details.end_time"  msgstr "" -#: fietsboek/templates/browse.jinja2:146 fietsboek/templates/details.jinja2:108 +#: fietsboek/templates/browse.jinja2:156 fietsboek/templates/details.jinja2:108  #: fietsboek/templates/profile.jinja2:27  msgid "page.details.uphill"  msgstr "" -#: fietsboek/templates/browse.jinja2:148 fietsboek/templates/details.jinja2:112 +#: fietsboek/templates/browse.jinja2:158 fietsboek/templates/details.jinja2:112  #: fietsboek/templates/profile.jinja2:29  msgid "page.details.downhill"  msgstr "" -#: fietsboek/templates/browse.jinja2:153 fietsboek/templates/details.jinja2:117 +#: fietsboek/templates/browse.jinja2:163 fietsboek/templates/details.jinja2:117  #: fietsboek/templates/profile.jinja2:33  msgid "page.details.moving_time"  msgstr "" -#: fietsboek/templates/browse.jinja2:155 fietsboek/templates/details.jinja2:121 +#: fietsboek/templates/browse.jinja2:165 fietsboek/templates/details.jinja2:121  #: fietsboek/templates/profile.jinja2:35  msgid "page.details.stopped_time"  msgstr "" -#: fietsboek/templates/browse.jinja2:159 fietsboek/templates/details.jinja2:125 +#: fietsboek/templates/browse.jinja2:169 fietsboek/templates/details.jinja2:125  #: fietsboek/templates/profile.jinja2:39  msgid "page.details.max_speed"  msgstr "" -#: fietsboek/templates/browse.jinja2:161 fietsboek/templates/details.jinja2:129 +#: fietsboek/templates/browse.jinja2:171 fietsboek/templates/details.jinja2:129  #: fietsboek/templates/profile.jinja2:41  msgid "page.details.avg_speed"  msgstr "" -#: fietsboek/templates/browse.jinja2:179 +#: fietsboek/templates/browse.jinja2:189  msgid "page.browse.download_multiple"  msgstr "" -#: fietsboek/templates/browse.jinja2:181 +#: fietsboek/templates/browse.jinja2:191  msgid "page.browse.no_results"  msgstr "" -#: fietsboek/templates/browse.jinja2:183 +#: fietsboek/templates/browse.jinja2:193  msgid "page.browse.no_tracks"  msgstr "" @@ -467,14 +479,14 @@ msgstr ""  msgid "page.home.unfinished_uploads"  msgstr "" -#: fietsboek/templates/home.jinja2:22 fietsboek/templates/home.jinja2:29 -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:27 fietsboek/templates/home.jinja2:34 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.summary.track"  msgid_plural "page.home.summary.tracks"  msgstr[0] ""  msgstr[1] "" -#: fietsboek/templates/home.jinja2:47 +#: fietsboek/templates/home.jinja2:52  msgid "page.home.total"  msgstr "" @@ -750,43 +762,43 @@ msgstr ""  msgid "flash.badge_deleted"  msgstr "" -#: fietsboek/views/default.py:117 +#: fietsboek/views/default.py:121  msgid "flash.invalid_credentials"  msgstr "" -#: fietsboek/views/default.py:121 +#: fietsboek/views/default.py:125  msgid "flash.account_not_verified"  msgstr "" -#: fietsboek/views/default.py:124 +#: fietsboek/views/default.py:128  msgid "flash.logged_in"  msgstr "" -#: fietsboek/views/default.py:146 +#: fietsboek/views/default.py:150  msgid "flash.logged_out"  msgstr "" -#: fietsboek/views/default.py:180 +#: fietsboek/views/default.py:184  msgid "flash.reset_invalid_email"  msgstr "" -#: fietsboek/views/default.py:185 +#: fietsboek/views/default.py:189  msgid "flash.password_token_generated"  msgstr "" -#: fietsboek/views/default.py:190 +#: fietsboek/views/default.py:194  msgid "page.password_reset.email.subject"  msgstr "" -#: fietsboek/views/default.py:193 +#: fietsboek/views/default.py:197  msgid "page.password_reset.email.body"  msgstr "" -#: fietsboek/views/default.py:226 +#: fietsboek/views/default.py:230  msgid "flash.email_verified"  msgstr "" -#: fietsboek/views/default.py:240 +#: fietsboek/views/default.py:244  msgid "flash.password_updated"  msgstr "" diff --git a/fietsboek/templates/browse.jinja2 b/fietsboek/templates/browse.jinja2 index f8937bb..9860201 100644 --- a/fietsboek/templates/browse.jinja2 +++ b/fietsboek/templates/browse.jinja2 @@ -105,6 +105,16 @@              {{ _("page.browse.filters.expand_advanced") }}            </button>          </div> +        <div class="col"> +          <select class="form-select" name="sort"> +            <option value="DATE_DESC" {% if request.params.get("sort") == "DATE_DESC" %}selected{% endif %}>{{ _("page.browse.sort.date") }} ↓</option> +            <option value="DATE_ASC" {% if request.params.get("sort") == "DATE_ASC" %}selected{% endif %}>{{ _("page.browse.sort.date") }} ↑</option> +            <option value="LENGTH_DESC" {% if request.params.get("sort") == "LENGTH_DESC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} ↓</option> +            <option value="LENGTH_ASC" {% if request.params.get("sort") == "LENGTH_ASC" %}selected{% endif %}>{{ _("page.browse.sort.length") }} ↑</option> +            <option value="DURATION_DESC" {% if request.params.get("sort") == "DURATION_DESC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} ↓</option> +            <option value="DURATION_ASC" {% if request.params.get("sort") == "DURATION_ASC" %}selected{% endif %}>{{ _("page.browse.sort.duration") }} ↑</option> +          </select> +        </div>        </div>      </form>    </div> diff --git a/fietsboek/views/browse.py b/fietsboek/views/browse.py index ede6b21..1e97187 100644 --- a/fietsboek/views/browse.py +++ b/fietsboek/views/browse.py @@ -1,18 +1,25 @@  """Views for browsing all tracks."""  import datetime +from collections.abc import Callable, Iterable +from enum import Enum  from io import RawIOBase -from typing import List +from typing import TypeVar  from zipfile import ZIP_DEFLATED, ZipFile  from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPNotFound +from pyramid.request import Request  from pyramid.response import Response  from pyramid.view import view_config  from sqlalchemy import func, or_, select -from sqlalchemy.orm import aliased +from sqlalchemy.orm import DeclarativeMeta, aliased +from sqlalchemy.orm.util import AliasedClass +from sqlalchemy.sql import Selectable  from .. import models, util  from ..models.track import TrackType, TrackWithMetadata +T = TypeVar("T", bound=Enum) +  class Stream(RawIOBase):      """A :class:`Stream` represents an in-memory buffered FIFO. @@ -25,41 +32,59 @@ class Stream(RawIOBase):          super().__init__()          self.buffer = [] -    def write(self, b): +    # The following definition violates the substitution principle, so mypy +    # would complain. However, I think we're good acting like we take only +    # "bytes". +    def write(self, b: bytes) -> int:  # type: ignore +        b = bytes(b)          self.buffer.append(b)          return len(b) -    def readall(self): +    def readall(self) -> bytes:          buf = self.buffer          self.buffer = []          return b"".join(buf) -def _get_int(request, name): +def _get_int(request: Request, name: str) -> int:      try:          return int(request.params.get(name))      except ValueError as exc:          raise HTTPBadRequest(f"Invalid integer in {name!r}") from exc -def _get_date(request, name): +def _get_date(request: Request, name: str) -> datetime.date:      try:          return datetime.date.fromisoformat(request.params.get(name))      except ValueError as exc:          raise HTTPBadRequest(f"Invalid date in {name!r}") from exc -def _get_enum(enum, value): +def _get_enum(enum: type[T], value: str) -> T:      try:          return enum[value]      except KeyError as exc:          raise HTTPBadRequest(f"Invalid enum value {value!r}") from exc +class ResultOrder(Enum): +    """Enum representing the different ways in which the tracks can be sorted +    in the result.""" + +    DATE_ASC = "date-asc" +    DATE_DESC = "date-desc" +    LENGTH_ASC = "length-asc" +    LENGTH_DESC = "length-desc" +    DURATION_ASC = "duration-asc" +    DURATION_DESC = "duration-desc" + +  class Filter:      """A class representing a filter that the user can apply to the track list.""" -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          """Compile the filter into the SQL query.          Returns the modified query. @@ -75,7 +100,7 @@ class Filter:          # pylint: disable=unused-argument          return query -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          """Check if the given track matches the filter.          :param track: The track to check. @@ -87,40 +112,50 @@ class Filter:  class LambdaFilter(Filter):      """A :class:`Filter` that works by provided lambda functions.""" -    def __init__(self, compiler, matcher): +    def __init__( +        self, +        compiler: Callable[[Selectable, AliasedClass, type[DeclarativeMeta]], Selectable], +        matcher: Callable[[TrackWithMetadata], bool], +    ):          self.compiler = compiler          self.matcher = matcher -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          return self.compiler(query, track, track_cache) -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          return self.matcher(track)  class SearchFilter(Filter):      """A :class:`Filter` that looks for the given search terms.""" -    def __init__(self, search_terms): +    def __init__(self, search_terms: Iterable[str]):          self.search_terms = search_terms -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          for term in self.search_terms:              term = term.lower()              query = query.where(func.lower(track.title).contains(term))          return query -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          return all(term.lower() in track.title.lower() for term in self.search_terms)  class TagFilter(Filter):      """A :class:`Filter` that looks for the given tags.""" -    def __init__(self, tags): +    def __init__(self, tags: Iterable[str]):          self.tags = tags -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          lower_tags = [tag.lower() for tag in self.tags]          for tag in lower_tags:              exists_query = ( @@ -132,7 +167,7 @@ class TagFilter(Filter):              query = query.where(exists_query)          return query -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          lower_track_tags = {tag.lower() for tag in track.text_tags()}          lower_tags = {tag.lower() for tag in self.tags}          return all(tag in lower_track_tags for tag in lower_tags) @@ -141,10 +176,12 @@ class TagFilter(Filter):  class PersonFilter(Filter):      """A :class:`Filter` that looks for the given tagged people, based on their name.""" -    def __init__(self, names): +    def __init__(self, names: Iterable[str]):          self.names = names -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          lower_names = [name.lower() for name in self.names]          for name in lower_names:              tpa = models.track.track_people_assoc @@ -164,7 +201,7 @@ class PersonFilter(Filter):              query = query.where(or_(exists_query, is_owner))          return query -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          lower_names = {person.name.lower() for person in track.tagged_people}          lower_names.add(track.owner.name.lower())          return all(name.lower() in lower_names for name in self.names) @@ -173,10 +210,12 @@ class PersonFilter(Filter):  class UserTaggedFilter(Filter):      """A :class:`Filter` that looks for a specific user to be tagged.""" -    def __init__(self, user): +    def __init__(self, user: models.User):          self.user = user -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          tpa = models.track.track_people_assoc          return query.where(              or_( @@ -190,39 +229,39 @@ class UserTaggedFilter(Filter):              )          ) -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          return track.owner == self.user or self.user in track.tagged_people  class FilterCollection(Filter):      """A class that applies multiple :class:`Filter`.""" -    def __init__(self, filters): +    def __init__(self, filters: Iterable[Filter]):          self._filters = filters      def __bool__(self):          return bool(self._filters) -    def compile(self, query, track, track_cache): +    def compile( +        self, query: Selectable, track: AliasedClass, track_cache: type[DeclarativeMeta] +    ) -> Selectable:          for filty in self._filters:              query = filty.compile(query, track, track_cache)          return query -    def apply(self, track): +    def apply(self, track: TrackWithMetadata) -> bool:          return all(filty.apply(track) for filty in self._filters)      @classmethod -    def parse(cls, request): +    def parse(cls, request: Request) -> "FilterCollection":          """Parse the filters from the given request.          :raises HTTPBadRequest: If the filters are malformed.          :param request: The request. -        :type request: pyramid.request.Request          :return: The parsed filter. -        :rtype: FilterCollection          """          # pylint: disable=singleton-comparison -        filters: List[Filter] = [] +        filters: list[Filter] = []          if request.params.get("search-terms"):              term = request.params.get("search-terms").strip()              filters.append(SearchFilter([term])) @@ -318,16 +357,44 @@ class FilterCollection(Filter):          return cls(filters) +def apply_order(query: Selectable, track: AliasedClass, order: ResultOrder) -> Selectable: +    """Applies a ``ORDER BY`` clause to the query. + +    :raises ValueError: If the given order does not exist. +    :param query: The query to adjust. +    :param track: The aliased track class. +    :param order: The order, one of the values given above. +    :return: The modified query with the ORDER BY clause applied. +    """ +    if order == ResultOrder.DATE_DESC: +        query = query.order_by(track.date_raw.desc()) +    elif order == ResultOrder.DATE_ASC: +        query = query.order_by(track.date_raw.asc()) +    # Thanks to SQLAlchemy, the join for the ORDER BY query is done +    # automatically. +    elif order == ResultOrder.LENGTH_DESC: +        query = query.order_by(models.TrackCache.length.desc()) +    elif order == ResultOrder.LENGTH_ASC: +        query = query.order_by(models.TrackCache.length.asc()) +    elif order == ResultOrder.DURATION_DESC: +        query = query.order_by( +            (models.TrackCache.moving_time + models.TrackCache.stopped_time).desc() +        ) +    elif order == ResultOrder.DURATION_ASC: +        query = query.order_by( +            (models.TrackCache.moving_time + models.TrackCache.stopped_time).asc() +        ) +    return query + +  @view_config(      route_name="browse", renderer="fietsboek:templates/browse.jinja2", request_method="GET"  ) -def browse(request): +def browse(request: Request) -> Response:      """Returns the page that lets a user browse all visible tracks.      :param request: The Pyramid request. -    :type request: pyramid.request.Request      :return: The HTTP response. -    :rtype: pyramid.response.Response      """      filters = FilterCollection.parse(request)      track = aliased(models.Track, models.User.visible_tracks_query(request.identity).subquery()) @@ -335,7 +402,11 @@ def browse(request):      # Build our query      query = select(track).join(models.TrackCache, isouter=True)      query = filters.compile(query, track, models.TrackCache) -    query = query.order_by(track.date_raw.desc()) + +    order = ResultOrder.DATE_DESC +    if request.params.get("sort"): +        order = _get_enum(ResultOrder, request.params.get("sort")) +    query = apply_order(query, track, order)      tracks = request.dbsession.execute(query).scalars()      tracks = (TrackWithMetadata(track, request.data_manager) for track in tracks) @@ -348,13 +419,11 @@ def browse(request):  @view_config(route_name="track-archive", request_method="GET") -def archive(request): +def archive(request: Request) -> Response:      """Packs multiple tracks into a single archive.      :param request: The Pyramid request. -    :type request: pyramid.request.Request      :return: The HTTP response. -    :rtype: pyramid.response.Response      """      track_ids = set(map(int, request.params.getall("track_id[]")))      tracks = (  | 
