diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..fc7738c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,21 @@ +[run] +branch = True +source = + shiftings +plugins = + django_coverage_plugin + +[report] +show_missing = True +skip_covered = False +omit = + */migrations/* + */test_*.py + */tests/* + */manage.py + +[html] +directory = coverage_html + +[django_coverage_plugin] +template_extensions = html,htm,txt diff --git a/.gitignore b/.gitignore index 1f2dbb0..dd93c6e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ gunicorn.py /build/ /src/shiftings/templates/local/ -ruff.toml \ No newline at end of file +ruff.toml +.coverage diff --git a/README.md b/README.md index 030bd3b..feae614 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,10 @@ See [Django docs](https://docs.djangoproject.com/en/4.1/howto/deployment/) for d 1. Add a local_settings.py. See local_settings.sample.py for inspiration. 1. Add a cron, systemd timer unit or similar for recurring shift creation +#### Testing + +Testing quality beyond line coverage is documented in the [Testing Quality Scorecard](docs/testing_quality.md). + ## Authors [lewellien](https://github.com/lewellien) & [Tjeri](https://github.com/tjeri) diff --git a/docs/development.md b/docs/development.md index 1cb1c16..2fe7076 100644 --- a/docs/development.md +++ b/docs/development.md @@ -1,3 +1,5 @@ # Development For information on development, refer to the project [README](https://github.com/HaDiNet/shiftings/). + +Testing quality beyond line coverage is documented in the [Testing Quality Scorecard](testing_quality.md). diff --git a/docs/testing_quality.md b/docs/testing_quality.md new file mode 100644 index 0000000..2f49d9d --- /dev/null +++ b/docs/testing_quality.md @@ -0,0 +1,119 @@ +# Testing Quality Scorecard + +This scorecard complements line coverage with behavior-focused indicators. + +## Why + +Line coverage answers: "Did code run?" + +This scorecard answers: "Would tests catch bad behavior?" + +## Current Baseline (2026-04-17) + +- Full-suite line coverage: 41% +- Critical auth behavior coverage: + - accounts/views/auth.py: 98% + - accounts/views/user.py: 97% +- Core shared view mixin coverage: + - utils/views/base.py: 98% + +## Behavior Quality Metrics + +Use these together. No single metric is enough. + +### 1) Risk-Weighted Coverage + +Track coverage only for high-risk modules first (auth, permission, shift lifecycle). + +Suggested risk tiers: + +- Tier 1 (critical): authentication, authorization, user identity, participation permissions +- Tier 2 (important): shift creation/update/delete and recurring generation +- Tier 3 (supporting): helpers and UI integration + +### 2) Scenario Coverage + +Maintain a behavior matrix and mark each scenario as: + +- Covered: at least one deterministic automated test +- Partial: path exists but edge/error branches missing +- Missing: no automated coverage + +#### Scenario Matrix + +| Domain | Scenario | Status | Test Reference | +|---|---|---|---| +| Auth | Local login rendering and fallback flows | Covered | `accounts/tests/test_auth_views.py` | +| Auth | SSO login initiation (enabled/disabled) | Covered | `accounts/tests/test_auth_views.py` | +| Auth | OAuth callback success/failure/exception | Covered | `accounts/tests/test_auth_views.py` | +| Auth | Confirm-email valid/invalid/malformed token paths | Covered | `accounts/tests/test_user_views.py` | +| Auth | Logout and relogin cache/session edge behavior | Partial | `accounts/tests/test_auth_views.py` | +| Auth | Password-reset confirmation invalid/expired token behavior | Missing | `n/a` | +| Auth | Brute-force and repeated failed-login throttling behavior | Missing | `n/a` | +| Permissions | Missing-permission behavior (redirect, 403, fail URL) | Covered | `utils/tests/test_base_views.py` | +| Permissions | Shift participation permission update workflow | Covered | `shifts/tests/test_permission_views.py` | +| Permissions | Duplicate organization rejection in permission formset | Covered | `shifts/tests/test_permission_forms.py` | +| Permissions | Cross-organization isolation for permission edits | Missing | `n/a` | +| Permissions | Permission precedence conflicts across global/org scopes | Partial | `shifts/tests/test_permission_forms.py` | +| Shifts | Participant add/remove for self and others | Covered | `shifts/tests/test_participant_views.py` | +| Shifts | Shift CRUD edge cases (date bounds, authz, redirects) | Partial | `shifts/tests/*` | +| Shifts | Summary view filtering and boundary scenarios | Missing | `n/a` | +| Shifts | Concurrent participant slot contention and max-user enforcement | Missing | `n/a` | +| Shifts | Shift copy/template creation with invalid source references | Missing | `n/a` | +| Recurring shifts | Form validation for recurring setup | Partial | `shifts/tests/*` | +| Recurring shifts | Recurring generation command and date edge cases | Missing | `n/a` | +| Recurring shifts | Timezone and DST boundary generation correctness | Missing | `n/a` | +| Recurring shifts | Idempotency of repeated recurring generation runs | Missing | `n/a` | +| Mail | Attachment size and date-range validation | Covered | `mail/tests/test_mail_forms.py` | +| Mail | Mail view send flows and permission boundaries | Partial | `mail/tests/*` | +| Mail | Delivery failure/retry behavior and user-visible error states | Missing | `n/a` | +| Mail | Recipient resolution with mixed member/group filters | Partial | `mail/tests/*` | +| Utils | Protected media header dispatch by backend type | Covered | `utils/tests/test_protected_content.py` | +| Utils | Saved-path breadcrumb behavior with complex query params | Partial | `utils/tests/test_base_views.py` | +| Utils | Http403 middleware integration and custom error page wiring | Missing | `n/a` | + +### 3) Mutation Score (Recommended) + +Measure if tests fail when implementation is intentionally perturbed. + +Target: + +- Tier 1 modules: > 70% +- Tier 2 modules: > 55% + +Note: + +- Start with small scope to keep runtime manageable. +- Prioritize accounts/views/auth.py, utils/views/base.py, shifts/views/shift.py. + +### 4) Regression Protection Index + +For each bug fix, add a test that fails before the fix. + +Track monthly: + +- Bugs fixed with regression test / total bugs fixed + +Target: + +- 100% for Tier 1 bugs + +## Scorecard Template + +Update this table in each release cycle. + +| Metric | Current | Target | Trend | +|---|---:|---:|---| +| Full-suite line coverage | 41% | 50% | up | +| Tier 1 weighted coverage | n/a | 75% | n/a | +| Scenario coverage (Tier 1) | 8/13 | 10/13 | up | +| Mutation score (Tier 1) | n/a | 70% | n/a | +| Regression protection index (Tier 1) | n/a | 100% | n/a | + +## Update Procedure + +1. Run full tests with coverage. +2. Update Tier 1/Tier 2 module coverage values. +3. Review scenario matrix and mark new covered paths. +4. Record newly fixed bugs and whether regression tests exist. +5. Update scorecard table and share in release notes. diff --git a/mkdocs.yml b/mkdocs.yml index 266344a..17463f9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -45,4 +45,6 @@ nav: - Shift Summary: 'organizations/shift_summary.md' - Events: - Coming Soon: 'events/index.md' - - Development: 'development.md' \ No newline at end of file + - Development: + - Overview: 'development.md' + - Testing Quality Scorecard: 'testing_quality.md' diff --git a/pyproject.toml b/pyproject.toml index dcfd8f6..1a78330 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,12 +55,14 @@ dev = [ 'types-python-dateutil==2.9.0.20260124', 'types-requests==2.32.4.20260107', 'mypy==1.19.1', + 'django-extensions==4.1', ] tests = [ 'pytest==9.0.2', 'pytest-django==4.12.0', 'pytest-cov==7.0.0', + 'django-coverage-plugin==3.2.2', ] docs = [ diff --git a/src/locale/de/LC_MESSAGES/django.po b/src/locale/de/LC_MESSAGES/django.po index 03ac8c4..4134de8 100644 --- a/src/locale/de/LC_MESSAGES/django.po +++ b/src/locale/de/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-25 15:12+0000\n" +"POT-Creation-Date: 2026-04-17 05:17+0200\n" "PO-Revision-Date: 2023-04-17 23:37+0200\n" "Last-Translator: \n" "Language-Team: \n" @@ -18,1689 +18,98 @@ msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Generator: Poedit 3.1.1\n" -#: venv/lib/python3.11/site-packages/click/_termui_impl.py:518 -#, python-brace-format -msgid "{editor}: Editing failed" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/_termui_impl.py:522 -#, python-brace-format -msgid "{editor}: Editing failed: {e}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1120 -#, fuzzy -#| msgid "Abort" -msgid "Aborted!" -msgstr "Abbrechen" - -#: venv/lib/python3.11/site-packages/click/core.py:1309 -#: venv/lib/python3.11/site-packages/click/decorators.py:559 -msgid "Show this message and exit." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1340 -#: venv/lib/python3.11/site-packages/click/core.py:1370 -#, python-brace-format -msgid "(Deprecated) {text}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1387 -#, fuzzy -#| msgid "Actions" -msgid "Options" -msgstr "Aktionen" - -#: venv/lib/python3.11/site-packages/click/core.py:1413 -#, python-brace-format -msgid "Got unexpected extra argument ({args})" -msgid_plural "Got unexpected extra arguments ({args})" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/core.py:1429 -msgid "DeprecationWarning: The command {name!r} is deprecated." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1636 -msgid "Commands" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1668 -msgid "Missing command." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1746 -msgid "No such command {name!r}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2310 -msgid "Value must be an iterable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2331 -#, python-brace-format -msgid "Takes {nargs} values but 1 was given." -msgid_plural "Takes {nargs} values but {len} were given." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/core.py:2778 -#, python-brace-format -msgid "env var: {var}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2808 -msgid "(dynamic)" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2821 -#, python-brace-format -msgid "default: {default}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2834 -#, fuzzy -#| msgid "Required" -msgid "required" -msgstr "Benötigt" - -#: venv/lib/python3.11/site-packages/click/decorators.py:465 -#, python-format -msgid "%(prog)s, version %(version)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/decorators.py:528 -msgid "Show the version and exit." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:44 -#: venv/lib/python3.11/site-packages/click/exceptions.py:80 -#, python-brace-format -msgid "Error: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:72 -#, python-brace-format -msgid "Try '{command} {option}' for help." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:121 -#, python-brace-format -msgid "Invalid value: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:123 -#, python-brace-format -msgid "Invalid value for {param_hint}: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:179 -msgid "Missing argument" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:181 -msgid "Missing option" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:183 -msgid "Missing parameter" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:185 -#, python-brace-format -msgid "Missing {param_type}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:192 -#, python-brace-format -msgid "Missing parameter: {param_name}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:212 -#, python-brace-format -msgid "No such option: {name}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:224 -#, python-brace-format -msgid "Did you mean {possibility}?" -msgid_plural "(Possible options: {possibilities})" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:262 -#, fuzzy -#| msgid "Unknown User" -msgid "unknown error" -msgstr "Unbekannter Benutzer" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:269 -msgid "Could not open file {filename!r}: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:231 -msgid "Argument {name!r} takes {nargs} values." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:413 -msgid "Option {name!r} does not take a value." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:474 -msgid "Option {name!r} requires an argument." -msgid_plural "Option {name!r} requires {nargs} arguments." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/shell_completion.py:319 -msgid "Shell completion is not supported for Bash versions older than 4.4." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/shell_completion.py:326 -msgid "Couldn't detect Bash version, shell completion is not supported." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:158 -msgid "Repeat for confirmation" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:174 -msgid "Error: The value you entered was invalid." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:176 -#, python-brace-format -msgid "Error: {e.message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:187 -msgid "Error: The two entered values do not match." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:243 -msgid "Error: invalid input" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:773 -msgid "Press any key to continue..." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:266 -#, python-brace-format -msgid "" -"Choose from:\n" -"\t{choices}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:298 -msgid "{value!r} is not {choice}." -msgid_plural "{value!r} is not one of {choices}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/types.py:392 -msgid "{value!r} does not match the format {format}." -msgid_plural "{value!r} does not match the formats {formats}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/types.py:414 -msgid "{value!r} is not a valid {number_type}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:470 -#, python-brace-format -msgid "{value} is not in the range {range}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:611 -msgid "{value!r} is not a valid boolean." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:635 -msgid "{value!r} is not a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:822 -#, fuzzy -#| msgid "Profile" -msgid "file" -msgstr "Profil" - -#: venv/lib/python3.11/site-packages/click/types.py:824 -msgid "directory" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:826 -msgid "path" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:877 -msgid "{name} {filename!r} does not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:886 -msgid "{name} {filename!r} is a file." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:894 -#, python-brace-format -msgid "{name} '{filename}' is a directory." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:903 -msgid "{name} {filename!r} is not readable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:912 -msgid "{name} {filename!r} is not writable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:921 -msgid "{name} {filename!r} is not executable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:988 -#, python-brace-format -msgid "{len_type} values are required, but {len_value} was given." -msgid_plural "{len_type} values are required, but {len_value} were given." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/colorfield/validators.py:9 -msgid "Enter a valid hex color, eg. #000000" -msgstr "" - -#: venv/lib/python3.11/site-packages/colorfield/validators.py:17 -msgid "Enter a valid hexa color, eg. #00000000" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/messages/apps.py:7 -msgid "Messages" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/sitemaps/apps.py:8 -msgid "Site Maps" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/staticfiles/apps.py:9 -msgid "Static Files" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/syndication/apps.py:7 -#, fuzzy -#| msgid "Organization" -msgid "Syndication" -msgstr "Organisation" - -#. Translators: String used to replace omitted page numbers in elided page -#. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. -#: venv/lib/python3.11/site-packages/django/core/paginator.py:30 -msgid "…" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:50 -msgid "That page number is not an integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:52 -msgid "That page number is less than 1" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:57 -msgid "That page contains no results" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:23 -msgid "Enter a valid value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:105 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:743 -msgid "Enter a valid URL." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:166 -msgid "Enter a valid integer." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:177 -msgid "Enter a valid email address." -msgstr "" - -#. Translators: "letters" means latin letters: a-z and A-Z. -#: venv/lib/python3.11/site-packages/django/core/validators.py:285 -msgid "" -"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:293 -msgid "" -"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " -"hyphens." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:305 -#: venv/lib/python3.11/site-packages/django/core/validators.py:313 -#: venv/lib/python3.11/site-packages/django/core/validators.py:342 -msgid "Enter a valid IPv4 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:322 -#: venv/lib/python3.11/site-packages/django/core/validators.py:343 -msgid "Enter a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:334 -#: venv/lib/python3.11/site-packages/django/core/validators.py:341 -msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:377 -msgid "Enter only digits separated by commas." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:383 -#, python-format -msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:418 -#, python-format -msgid "Ensure this value is less than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:427 -#, python-format -msgid "Ensure this value is greater than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:437 -#, python-format -msgid "" -"Ensure this value has at least %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at least %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:455 -#, python-format -msgid "" -"Ensure this value has at most %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at most %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:478 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:343 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:378 -#, fuzzy -#| msgid "Enter Name" -msgid "Enter a number." -msgstr "Name einegeben" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:480 -#, python-format -msgid "Ensure that there are no more than %(max)s digit in total." -msgid_plural "Ensure that there are no more than %(max)s digits in total." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:485 -#, python-format -msgid "Ensure that there are no more than %(max)s decimal place." -msgid_plural "Ensure that there are no more than %(max)s decimal places." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:490 -#, python-format -msgid "" -"Ensure that there are no more than %(max)s digit before the decimal point." -msgid_plural "" -"Ensure that there are no more than %(max)s digits before the decimal point." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:559 -#, python-format -msgid "" -"File extension “%(extension)s” is not allowed. Allowed extensions are: " -"%(allowed_extensions)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:620 -msgid "Null characters are not allowed." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/base.py:1364 -#: venv/lib/python3.11/site-packages/django/forms/models.py:894 -#, fuzzy -#| msgid "End" -msgid "and" -msgstr "Ende" - -#: venv/lib/python3.11/site-packages/django/db/models/base.py:1366 -#, python-format -msgid "%(model_name)s with this %(field_labels)s already exists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:129 -#, python-format -msgid "Value %(value)r is not a valid choice." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:130 -msgid "This field cannot be null." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:131 -msgid "This field cannot be blank." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:132 -#, python-format -msgid "%(model_name)s with this %(field_label)s already exists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:136 -#, python-format -msgid "" -"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:156 -#, python-format -msgid "Field of type: %(field_type)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1050 -#, python-format -msgid "“%(value)s” value must be either True or False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1051 -#, python-format -msgid "“%(value)s” value must be either True, False, or None." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1053 -msgid "Boolean (Either True or False)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1094 -#, python-format -msgid "String (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1192 -msgid "Comma-separated integers" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1293 -#, python-format -msgid "" -"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " -"format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1297 -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1432 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " -"date." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1301 -msgid "Date (without time)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1428 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." -"uuuuuu]][TZ] format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1436 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" -"[TZ]) but it is an invalid date/time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1441 -msgid "Date (with time)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1565 -#, python-format -msgid "“%(value)s” value must be a decimal number." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1567 -#, fuzzy -#| msgid "December" -msgid "Decimal number" -msgstr "Dezember" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1724 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." -"uuuuuu] format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1728 -#: src/shiftings/shifts/forms/template.py:40 -#: src/shiftings/shifts/models/template.py:23 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:11 -msgid "Duration" -msgstr "Dauer" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1780 -msgid "Email address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1805 -msgid "File path" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1883 -#, python-format -msgid "“%(value)s” value must be a float." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1885 -msgid "Floating point number" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1925 -#, python-format -msgid "“%(value)s” value must be an integer." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1927 -msgid "Integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2019 -msgid "Big (8 byte) integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2036 -msgid "Small integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2044 -msgid "IPv4 address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2075 -msgid "IP address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2168 -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2169 -#, python-format -msgid "“%(value)s” value must be either None, True or False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2171 -msgid "Boolean (Either True, False or None)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2222 -msgid "Positive big integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2237 -msgid "Positive integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2252 -msgid "Positive small integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2268 -#, python-format -msgid "Slug (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2304 -#: src/shiftings/mail/forms/mail.py:11 -msgid "Text" -msgstr "Text" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2374 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " -"format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2378 -#, python-format -msgid "" -"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " -"invalid time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2382 -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:23 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:197 -#: src/shiftings/shifts/templates/shifts/template/group.html:43 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:23 -#: src/shiftings/shifts/templates/shifts/template/shift.html:10 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:33 -msgid "Time" -msgstr "Zeit" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2490 -msgid "URL" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2514 -msgid "Raw binary data" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2579 -#, python-format -msgid "“%(value)s” is not a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2581 -msgid "Universally unique identifier" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/files.py:232 -msgid "File" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/files.py:392 -msgid "Image" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/json.py:18 -#, fuzzy -#| msgid "Subject" -msgid "A JSON object" -msgstr "Betreff" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/json.py:20 -msgid "Value must be valid JSON." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:903 -#, python-format -msgid "%(model)s instance with %(field)s %(value)r does not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:905 -msgid "Foreign Key (type determined by related field)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1204 -msgid "One-to-one relationship" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1261 -#, python-format -msgid "%(from)s-%(to)s relationship" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1263 -#, python-format -msgid "%(from)s-%(to)s relationships" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1311 -msgid "Many-to-many relationship" -msgstr "" - -#. Translators: If found as last label character, these punctuation -#. characters will prevent the default label_suffix to be appended to the label -#: venv/lib/python3.11/site-packages/django/forms/boundfield.py:176 -msgid ":?.!" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:91 -#, fuzzy -#| msgid "One of the user fields is required!" -msgid "This field is required." -msgstr "Eines der Benutzerfelder ist benötigt!" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:298 -msgid "Enter a whole number." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:459 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1232 -msgid "Enter a valid date." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:482 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1233 -#, fuzzy -#| msgid "Enter Name" -msgid "Enter a valid time." -msgstr "Name einegeben" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:509 -msgid "Enter a valid date/time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:543 -#, fuzzy -#| msgid "Enter Name or Organization" -msgid "Enter a valid duration." -msgstr "Name oder Organisation angeben" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:544 -#, python-brace-format -msgid "The number of days must be between {min_days} and {max_days}." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:610 -msgid "No file was submitted. Check the encoding type on the form." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:611 -msgid "No file was submitted." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:612 -msgid "The submitted file is empty." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:614 -#, python-format -msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." -msgid_plural "" -"Ensure this filename has at most %(max)d characters (it has %(length)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:619 -msgid "Please either submit a file or check the clear checkbox, not both." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:685 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:848 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:940 -#: venv/lib/python3.11/site-packages/django/forms/models.py:1563 -#, python-format -msgid "Select a valid choice. %(value)s is not one of the available choices." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:942 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1061 -#: venv/lib/python3.11/site-packages/django/forms/models.py:1561 -msgid "Enter a list of values." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1062 -msgid "Enter a complete value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1301 -msgid "Enter a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1331 -msgid "Enter a valid JSON." -msgstr "" - -#. Translators: This is the default suffix added to form field labels -#: venv/lib/python3.11/site-packages/django/forms/forms.py:98 -msgid ":" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/forms.py:248 -#: venv/lib/python3.11/site-packages/django/forms/forms.py:328 -#, python-format -msgid "(Hidden field %(name)s) %(error)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:67 -#, python-format -msgid "" -"ManagementForm data is missing or has been tampered with. Missing fields: " -"%(field_names)s. You may need to file a bug report if the issue persists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:418 -#, python-format -msgid "Please submit at most %d form." -msgid_plural "Please submit at most %d forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:434 -#, python-format -msgid "Please submit at least %d form." -msgid_plural "Please submit at least %d forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:470 -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:477 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:38 -msgid "Order" -msgstr "Reihenfolge" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:483 -#, fuzzy -#| msgid "Delete Shift" -msgid "Delete" -msgstr "Schicht löschen" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:887 -#, python-format -msgid "Please correct the duplicate data for %(field)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:892 -#, python-format -msgid "Please correct the duplicate data for %(field)s, which must be unique." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:899 -#, python-format -msgid "" -"Please correct the duplicate data for %(field_name)s which must be unique " -"for the %(lookup)s in %(date_field)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:908 -msgid "Please correct the duplicate values below." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1335 -msgid "The inline value did not match the parent instance." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1426 -msgid "Select a valid choice. That choice is not one of the available choices." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1565 -#, python-format -msgid "“%(pk)s” is not a valid value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/utils.py:204 -#, python-format -msgid "" -"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " -"may be ambiguous or it may not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:434 -msgid "Clear" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:435 -#, fuzzy -#| msgid "Current Events" -msgid "Currently" -msgstr "Aktuelle Events" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:436 -msgid "Change" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:765 -#: src/shiftings/accounts/templates/accounts/user_detail.html:55 -#: src/shiftings/accounts/templates/accounts/user_detail.html:65 -msgid "Unknown" -msgstr "Unbekannt" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:766 -#: src/shiftings/templates/generic/delete.html:9 -msgid "Yes" -msgstr "Ja" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:767 -#: src/shiftings/templates/generic/delete.html:11 -msgid "No" -msgstr "Nein" - -#. Translators: Please do not add spaces around commas. -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:858 -msgid "yes,no,maybe" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:888 -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:905 -#, python-format -msgid "%(size)d byte" -msgid_plural "%(size)d bytes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:907 -#, python-format -msgid "%s KB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:909 -#, python-format -msgid "%s MB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:911 -#, python-format -msgid "%s GB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:913 -#, python-format -msgid "%s TB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:915 -#, python-format -msgid "%s PB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:77 -msgid "p.m." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:78 -msgid "a.m." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:83 -#, fuzzy -#| msgid "M" -msgid "PM" -msgstr "M" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:84 -#, fuzzy -#| msgid "M" -msgid "AM" -msgstr "M" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:155 -msgid "midnight" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:157 -msgid "noon" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:7 -#: src/shiftings/utils/time/week.py:13 -msgid "Monday" -msgstr "Montag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:8 -#: src/shiftings/utils/time/week.py:14 -msgid "Tuesday" -msgstr "Dienstag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:9 -#: src/shiftings/utils/time/week.py:15 -msgid "Wednesday" -msgstr "Mittwoch" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:10 -#: src/shiftings/utils/time/week.py:16 -msgid "Thursday" -msgstr "Donnerstag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:11 -#: src/shiftings/utils/time/week.py:17 -msgid "Friday" -msgstr "Freitag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:12 -#: src/shiftings/utils/time/week.py:18 -msgid "Saturday" -msgstr "Samstag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:13 -#: src/shiftings/utils/time/week.py:19 -msgid "Sunday" -msgstr "Sonntag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:16 -#, fuzzy -#| msgid "Month" -msgid "Mon" -msgstr "Monat" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:17 -#, fuzzy -#| msgid "Tuesday" -msgid "Tue" -msgstr "Dienstag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:18 -msgid "Wed" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:19 -msgid "Thu" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:20 -#, fuzzy -#| msgid "Friday" -msgid "Fri" -msgstr "Freitag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:21 -#, fuzzy -#| msgid "Start" -msgid "Sat" -msgstr "Start" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:22 -#, fuzzy -#| msgid "Sunday" -msgid "Sun" -msgstr "Sonntag" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:25 -#: src/shiftings/utils/time/month.py:10 -msgid "January" -msgstr "Januar" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:26 -#: src/shiftings/utils/time/month.py:11 -msgid "February" -msgstr "Februar" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:27 -#: src/shiftings/utils/time/month.py:12 -msgid "March" -msgstr "März" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:28 -#: src/shiftings/utils/time/month.py:13 -msgid "April" -msgstr "April" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:29 -#: src/shiftings/utils/time/month.py:14 -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:30 -#: src/shiftings/utils/time/month.py:15 -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:31 -#: src/shiftings/utils/time/month.py:16 -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:32 -#: src/shiftings/utils/time/month.py:17 -msgid "August" -msgstr "August" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:33 -#: src/shiftings/utils/time/month.py:18 -msgid "September" -msgstr "September" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:34 -#: src/shiftings/utils/time/month.py:19 -msgid "October" -msgstr "Oktober" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:35 -#: src/shiftings/utils/time/month.py:20 -msgid "November" -msgstr "November" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:36 -#: src/shiftings/utils/time/month.py:21 -msgid "December" -msgstr "Dezember" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:39 -msgid "jan" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:40 -msgid "feb" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:41 -msgid "mar" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:42 -msgid "apr" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:43 -#, fuzzy -#| msgid "May" -msgid "may" -msgstr "Mai" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:44 -msgid "jun" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:45 -msgid "jul" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:46 -msgid "aug" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:47 -msgid "sep" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:48 -msgid "oct" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:49 -msgid "nov" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:50 -msgid "dec" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:53 -msgctxt "abbrev. month" -msgid "Jan." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:54 -msgctxt "abbrev. month" -msgid "Feb." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:55 -#, fuzzy -#| msgid "March" -msgctxt "abbrev. month" -msgid "March" -msgstr "März" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:56 -#, fuzzy -#| msgid "April" -msgctxt "abbrev. month" -msgid "April" -msgstr "April" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:57 -#, fuzzy -#| msgid "May" -msgctxt "abbrev. month" -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:58 -#, fuzzy -#| msgid "June" -msgctxt "abbrev. month" -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:59 -#, fuzzy -#| msgid "July" -msgctxt "abbrev. month" -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:60 -msgctxt "abbrev. month" -msgid "Aug." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:61 -msgctxt "abbrev. month" -msgid "Sept." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:62 -msgctxt "abbrev. month" -msgid "Oct." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:63 -msgctxt "abbrev. month" -msgid "Nov." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:64 -msgctxt "abbrev. month" -msgid "Dec." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:67 -#, fuzzy -#| msgid "January" -msgctxt "alt. month" -msgid "January" -msgstr "Januar" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:68 -#, fuzzy -#| msgid "February" -msgctxt "alt. month" -msgid "February" -msgstr "Februar" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:69 -#, fuzzy -#| msgid "March" -msgctxt "alt. month" -msgid "March" -msgstr "März" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:70 -#, fuzzy -#| msgid "April" -msgctxt "alt. month" -msgid "April" -msgstr "April" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:71 -#, fuzzy -#| msgid "May" -msgctxt "alt. month" -msgid "May" -msgstr "Mai" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:72 -#, fuzzy -#| msgid "June" -msgctxt "alt. month" -msgid "June" -msgstr "Juni" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:73 -#, fuzzy -#| msgid "July" -msgctxt "alt. month" -msgid "July" -msgstr "Juli" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:74 -#, fuzzy -#| msgid "August" -msgctxt "alt. month" -msgid "August" -msgstr "August" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:75 -#, fuzzy -#| msgid "September" -msgctxt "alt. month" -msgid "September" -msgstr "September" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:76 -#, fuzzy -#| msgid "October" -msgctxt "alt. month" -msgid "October" -msgstr "Oktober" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:77 -#, fuzzy -#| msgid "November" -msgctxt "alt. month" -msgid "November" -msgstr "November" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:78 -#, fuzzy -#| msgid "December" -msgctxt "alt. month" -msgid "December" -msgstr "Dezember" - -#: venv/lib/python3.11/site-packages/django/utils/ipv6.py:8 -msgid "This is not a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/text.py:77 -#, python-format -msgctxt "String to return when truncating text" -msgid "%(truncated_text)s…" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/text.py:253 -msgid "or" -msgstr "" - -#. Translators: This string is used as a separator between list elements -#: venv/lib/python3.11/site-packages/django/utils/text.py:272 -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:94 -msgid ", " -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:9 -#, python-format -msgid "%(num)d year" -msgid_plural "%(num)d years" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:10 -#, python-format -msgid "%(num)d month" -msgid_plural "%(num)d months" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:11 -#, python-format -msgid "%(num)d week" -msgid_plural "%(num)d weeks" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:12 -#, python-format -msgid "%(num)d day" -msgid_plural "%(num)d days" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:13 -#, python-format -msgid "%(num)d hour" -msgid_plural "%(num)d hours" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:14 -#, python-format -msgid "%(num)d minute" -msgid_plural "%(num)d minutes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:111 -#, fuzzy -#| msgid "Forbidden Method GET" -msgid "Forbidden" -msgstr "Verbotene Methode GET" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:112 -msgid "CSRF verification failed. Request aborted." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:116 -msgid "" -"You are seeing this message because this HTTPS site requires a “Referer " -"header” to be sent by your web browser, but none was sent. This header is " -"required for security reasons, to ensure that your browser is not being " -"hijacked by third parties." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:122 -msgid "" -"If you have configured your browser to disable “Referer” headers, please re-" -"enable them, at least for this site, or for HTTPS connections, or for “same-" -"origin” requests." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:127 -msgid "" -"If you are using the tag or " -"including the “Referrer-Policy: no-referrer” header, please remove them. The " -"CSRF protection requires the “Referer” header to do strict referer checking. " -"If you’re concerned about privacy, use alternatives like for links to third-party sites." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:136 -msgid "" -"You are seeing this message because this site requires a CSRF cookie when " -"submitting forms. This cookie is required for security reasons, to ensure " -"that your browser is not being hijacked by third parties." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:142 -msgid "" -"If you have configured your browser to disable cookies, please re-enable " -"them, at least for this site, or for “same-origin” requests." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:148 -msgid "More information is available with DEBUG=True." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:44 -msgid "No year specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:64 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:115 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:214 -msgid "Date out of range" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:94 -msgid "No month specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:147 -msgid "No day specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:194 -msgid "No week specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:349 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:380 -#, python-format -msgid "No %(verbose_name_plural)s available" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:652 -#, python-format -msgid "" -"Future %(verbose_name_plural)s not available because %(class_name)s." -"allow_future is False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:692 -#, python-format -msgid "Invalid date string “%(datestr)s” given format “%(format)s”" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/detail.py:56 -#, python-format -msgid "No %(verbose_name)s found matching the query" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:70 -msgid "Page is not “last”, nor can it be converted to an int." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:77 -#, python-format -msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:169 -#, python-format -msgid "Empty list and “%(class_name)s.allow_empty” is False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:39 -msgid "Directory indexes are not allowed here." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:41 -#, python-format -msgid "“%(path)s” does not exist" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:80 -#, python-format -msgid "Index of %(directory)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:7 -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:221 -msgid "The install worked successfully! Congratulations!" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:207 -#, python-format -msgid "" -"View release notes for Django %(version)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:222 -#, python-format -msgid "" -"You are seeing this page because DEBUG=True is in your settings file and you have not " -"configured any URLs." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:230 -msgid "Django Documentation" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:231 -msgid "Topics, references, & how-to’s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:239 -msgid "Tutorial: A Polling App" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:240 -msgid "Get started with Django" -msgstr "" +#: shiftings/accounts/forms/user_form.py:13 +msgid "Confirm password" +msgstr "Passwort bestätigen" -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:248 -msgid "Django Community" -msgstr "" +#: shiftings/accounts/forms/user_form.py:31 +#: shiftings/accounts/forms/user_form.py:32 +msgid "Please enter matching passwords" +msgstr "Bitte gebe identische Passwörter ein" -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:249 -msgid "Connect, get help, or contribute" +#: shiftings/accounts/forms/user_form.py:65 +msgid "Cannot change first name, last name or email for LDAP users." msgstr "" -#: venv/lib/python3.11/site-packages/django_bootstrap5/components.py:26 -#, fuzzy -#| msgid "Close" -msgid "close" -msgstr "Schließen" +#: shiftings/accounts/models/user.py:33 shiftings/templates/theme.html:28 +msgid "Light" +msgstr "Hell" -#: venv/lib/python3.11/site-packages/isort/main.py:158 -msgid "show this help message and exit" -msgstr "" +#: shiftings/accounts/models/user.py:34 shiftings/templates/theme.html:31 +msgid "Dark" +msgstr "Dunkel" -#: venv/lib/python3.11/site-packages/mypy/main.py:376 -#, python-format -msgid "%(prog)s: error: %(message)s\n" +#: shiftings/accounts/models/user.py:35 shiftings/templates/theme.html:34 +msgid "Auto" msgstr "" -#: src/shiftings/accounts/forms/user_form.py:11 -msgid "Confirm password" -msgstr "Passwort bestätigen" - -#: src/shiftings/accounts/forms/user_form.py:30 -msgid "Please enter matching passwords" -msgstr "Bitte gebe identische Passwörter ein" - -#: src/shiftings/accounts/models/user.py:32 -#: src/shiftings/accounts/templates/accounts/user_detail.html:52 -#: src/shiftings/shifts/models/participant.py:15 +#: shiftings/accounts/models/user.py:37 +#: shiftings/accounts/templates/accounts/user_detail.html:52 +#: shiftings/shifts/models/participant.py:15 msgid "Display Name" msgstr "Anzeigename" -#: src/shiftings/accounts/models/user.py:33 -#: src/shiftings/events/models/event.py:29 -#: src/shiftings/organizations/models/organization.py:28 +#: shiftings/accounts/models/user.py:38 shiftings/events/models/event.py:29 +#: shiftings/organizations/models/organization.py:28 msgid "Telephone Number" msgstr "Telefonnummer" -#: src/shiftings/accounts/templates/accounts/confirm_email.html:7 +#: shiftings/accounts/models/user.py:42 +msgid "Theme Preference" +msgstr "" + +#: shiftings/accounts/templates/accounts/confirm_email.html:7 msgid "Go to Login" msgstr "Zum Login" -#: src/shiftings/accounts/templates/accounts/login_form.html:6 -#: src/shiftings/accounts/templates/accounts/login_multiple.html:12 +#: shiftings/accounts/templates/accounts/login_form.html:6 +#: shiftings/accounts/templates/accounts/login_multiple.html:12 msgid "Login with local Account" msgstr "Login mit lokalem Account" -#: src/shiftings/accounts/templates/accounts/login_form.html:8 -#: src/shiftings/accounts/templates/accounts/login_multiple.html:20 +#: shiftings/accounts/templates/accounts/login_form.html:8 +#: shiftings/accounts/templates/accounts/login_multiple.html:20 msgid "Login with LDAP Account" msgstr "Login mit LDAP Account" -#: src/shiftings/accounts/templates/accounts/login_form.html:14 -#: src/shiftings/accounts/views/auth.py:27 -#: src/shiftings/templates/top_nav.html:57 +#: shiftings/accounts/templates/accounts/login_form.html:14 +#: shiftings/accounts/views/auth.py:26 shiftings/templates/top_nav.html:61 msgid "Login" msgstr "Einloggen" -#: src/shiftings/accounts/templates/accounts/login_form.html:16 -#: src/shiftings/accounts/templates/accounts/password_reset/prompt.html:10 +#: shiftings/accounts/templates/accounts/login_form.html:16 +#: shiftings/accounts/templates/accounts/password_reset/prompt.html:10 msgid "Reset Password" msgstr "Passwort zurücksetzen" -#: src/shiftings/accounts/templates/accounts/login_form.html:18 +#: shiftings/accounts/templates/accounts/login_form.html:18 msgid "Return to selection" msgstr "Zur Auswahl zurückkehren" -#: src/shiftings/accounts/templates/accounts/login_form.html:29 +#: shiftings/accounts/templates/accounts/login_form.html:29 msgid "You don't have an Account?" msgstr "Noch kein Account?" -#: src/shiftings/accounts/templates/accounts/login_form.html:30 +#: shiftings/accounts/templates/accounts/login_form.html:30 msgid "Register here" msgstr "Hier registrieren" -#: src/shiftings/accounts/templates/accounts/login_form.html:32 +#: shiftings/accounts/templates/accounts/login_form.html:32 msgid "Or login using another Method" msgstr "Oder nutze eine andere Loginmethode" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:5 +#: shiftings/accounts/templates/accounts/login_multiple.html:5 msgid "Welcome to Shiftings" msgstr "Wilkommen bei Shiftings" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:6 +#: shiftings/accounts/templates/accounts/login_multiple.html:6 msgid "" "To manage your shifts please authenticate with one of the given methods:" msgstr "" "Um deine Schichten zu verwalten melde dich bitte mit einer dieser Methoden " "an:" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:31 +#: shiftings/accounts/templates/accounts/login_multiple.html:31 #, fuzzy, python-format #| msgid "" #| "\n" @@ -1715,28 +124,28 @@ msgstr "" " vor %(time)s\n" " " -#: src/shiftings/accounts/templates/accounts/logout.html:6 +#: shiftings/accounts/templates/accounts/logout.html:6 msgid "Successfully logged out" msgstr "Erfolgreich ausgeloggt" -#: src/shiftings/accounts/templates/accounts/logout.html:7 +#: shiftings/accounts/templates/accounts/logout.html:7 msgid "Login again" msgstr "Wieder einloggen" -#: src/shiftings/accounts/templates/accounts/password_reset/confirm.html:7 +#: shiftings/accounts/templates/accounts/password_reset/confirm.html:7 msgid "Change Password" msgstr "Passwort ändern" -#: src/shiftings/accounts/templates/accounts/password_reset/done.html:4 +#: shiftings/accounts/templates/accounts/password_reset/done.html:4 msgid "Your password has been reset, check your email!" msgstr "Dein Passwort wurde zurückgesetzt, überprüfe deine E-Mails!" -#: src/shiftings/accounts/templates/accounts/password_reset/done.html:5 -#: src/shiftings/accounts/templates/accounts/password_reset/success.html:5 +#: shiftings/accounts/templates/accounts/password_reset/done.html:5 +#: shiftings/accounts/templates/accounts/password_reset/success.html:5 msgid "Back to login" msgstr "Zurück zum Login" -#: src/shiftings/accounts/templates/accounts/password_reset/prompt.html:4 +#: shiftings/accounts/templates/accounts/password_reset/prompt.html:4 msgid "" "\n" " If you are an local user enter your email-adress below to reset your " @@ -1748,15 +157,15 @@ msgstr "" "Passwort zurückzusetzen!\n" " " -#: src/shiftings/accounts/templates/accounts/password_reset/success.html:4 +#: shiftings/accounts/templates/accounts/password_reset/success.html:4 msgid "Your password has been successfully reset!" msgstr "Deine Passwort wurde erfolgreich zurückgesetzt!" -#: src/shiftings/accounts/templates/accounts/user_detail.html:3 +#: shiftings/accounts/templates/accounts/user_detail.html:3 msgid "Confirm User Deletion" msgstr "Bestätige die Löschung des Benutzers" -#: src/shiftings/accounts/templates/accounts/user_detail.html:5 +#: shiftings/accounts/templates/accounts/user_detail.html:5 #, python-format msgid "" "\n" @@ -1772,128 +181,131 @@ msgstr "" "werden.\n" " " -#: src/shiftings/accounts/templates/accounts/user_detail.html:17 +#: shiftings/accounts/templates/accounts/user_detail.html:17 msgid "Yes, I want to permanently delete my Data" msgstr "Ja, ich möchte meine Daten entgültig löschen" -#: src/shiftings/accounts/templates/accounts/user_detail.html:19 +#: shiftings/accounts/templates/accounts/user_detail.html:19 msgid "Abort" msgstr "Abbrechen" -#: src/shiftings/accounts/templates/accounts/user_detail.html:29 -#: src/shiftings/organizations/models/membership.py:68 -#: src/shiftings/shifts/models/participant.py:13 -#: src/shiftings/templates/top_nav.html:47 +#: shiftings/accounts/templates/accounts/user_detail.html:29 +#: shiftings/organizations/models/membership.py:68 +#: shiftings/shifts/models/participant.py:13 +#: shiftings/templates/top_nav.html:48 msgid "User" msgstr "Benutzer" -#: src/shiftings/accounts/templates/accounts/user_detail.html:37 -#: src/shiftings/accounts/templates/accounts/user_detail.html:57 +#: shiftings/accounts/templates/accounts/user_detail.html:37 +#: shiftings/accounts/templates/accounts/user_detail.html:57 msgid "Edit user data" msgstr "Benutzerdaten bearbeiten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:48 -#: src/shiftings/organizations/forms/membership.py:16 +#: shiftings/accounts/templates/accounts/user_detail.html:48 +#: shiftings/organizations/forms/membership.py:16 msgid "Username" msgstr "Benutzername" -#: src/shiftings/accounts/templates/accounts/user_detail.html:50 -#: src/shiftings/events/models/event.py:25 -#: src/shiftings/events/templates/events/event.html:85 -#: src/shiftings/organizations/models/membership.py:10 -#: src/shiftings/organizations/models/organization.py:24 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:195 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:239 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:278 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:388 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:39 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:13 -#: src/shiftings/shifts/models/base.py:8 -#: src/shiftings/shifts/models/recurring.py:32 -#: src/shiftings/shifts/models/template.py:17 -#: src/shiftings/shifts/models/template_group.py:20 -#: src/shiftings/shifts/models/type.py:40 -#: src/shiftings/shifts/models/type_group.py:16 -#: src/shiftings/shifts/templates/shifts/group/list.html:6 -#: src/shiftings/shifts/templates/shifts/list.html:6 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:6 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:6 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:5 -#: src/shiftings/shifts/templates/shifts/type_group/list.html:7 +#: shiftings/accounts/templates/accounts/user_detail.html:50 +#: shiftings/events/models/event.py:25 +#: shiftings/events/templates/events/event.html:85 +#: shiftings/organizations/models/membership.py:10 +#: shiftings/organizations/models/organization.py:24 +#: shiftings/organizations/templates/organizations/organization_admin.html:195 +#: shiftings/organizations/templates/organizations/organization_admin.html:239 +#: shiftings/organizations/templates/organizations/organization_admin.html:278 +#: shiftings/organizations/templates/organizations/organization_admin.html:388 +#: shiftings/organizations/templates/organizations/organization_settings.html:39 +#: shiftings/organizations/templates/organizations/user/claim_users.html:13 +#: shiftings/shifts/models/base.py:8 shiftings/shifts/models/recurring.py:32 +#: shiftings/shifts/models/template.py:17 +#: shiftings/shifts/models/template_group.py:20 +#: shiftings/shifts/models/type.py:40 shiftings/shifts/models/type_group.py:16 +#: shiftings/shifts/templates/shifts/group/list.html:6 +#: shiftings/shifts/templates/shifts/list.html:6 +#: shiftings/shifts/templates/shifts/recurring/list.html:6 +#: shiftings/shifts/templates/shifts/template/group_list.html:6 +#: shiftings/shifts/templates/shifts/template/template_form.html:5 +#: shiftings/shifts/templates/shifts/type_group/list.html:7 msgid "Name" msgstr "Name" -#: src/shiftings/accounts/templates/accounts/user_detail.html:58 +#: shiftings/accounts/templates/accounts/user_detail.html:55 +#: shiftings/accounts/templates/accounts/user_detail.html:65 +msgid "Unknown" +msgstr "Unbekannt" + +#: shiftings/accounts/templates/accounts/user_detail.html:58 msgid "Set Display Name" msgstr "Setze Anzeigename" -#: src/shiftings/accounts/templates/accounts/user_detail.html:62 +#: shiftings/accounts/templates/accounts/user_detail.html:62 msgid "Email" msgstr "Email" -#: src/shiftings/accounts/templates/accounts/user_detail.html:64 +#: shiftings/accounts/templates/accounts/user_detail.html:64 msgid "Phone Number" msgstr "Telefonnummer" -#: src/shiftings/accounts/templates/accounts/user_detail.html:66 +#: shiftings/accounts/templates/accounts/user_detail.html:66 msgid "Member since" msgstr "Mitglied seit" -#: src/shiftings/accounts/templates/accounts/user_detail.html:68 +#: shiftings/accounts/templates/accounts/user_detail.html:68 msgid "Total Shifts" msgstr "Anzahl Schichten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:70 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:124 +#: shiftings/accounts/templates/accounts/user_detail.html:70 +#: shiftings/organizations/templates/organizations/organization_admin.html:124 msgid "Groups" msgstr "Gruppen" -#: src/shiftings/accounts/templates/accounts/user_detail.html:71 -#: src/shiftings/events/templates/events/template/event.html:32 -#: src/shiftings/shifts/templates/shifts/template/template.html:19 +#: shiftings/accounts/templates/accounts/user_detail.html:71 +#: shiftings/events/templates/events/template/event.html:32 +#: shiftings/shifts/templates/shifts/template/template.html:19 msgid "None" msgstr "Keine" -#: src/shiftings/accounts/templates/accounts/user_detail.html:77 +#: shiftings/accounts/templates/accounts/user_detail.html:77 msgid "Organizations" msgstr "Organisationen" -#: src/shiftings/accounts/templates/accounts/user_detail.html:83 +#: shiftings/accounts/templates/accounts/user_detail.html:83 msgid "Hide organizations" msgstr "Organisationen einklappen" -#: src/shiftings/accounts/templates/accounts/user_detail.html:102 -#: src/shiftings/events/templates/events/template/event.html:12 +#: shiftings/accounts/templates/accounts/user_detail.html:102 +#: shiftings/events/templates/events/template/event.html:12 msgid "No Logo available" msgstr "Kein Logo verfügbar" -#: src/shiftings/accounts/templates/accounts/user_detail.html:125 +#: shiftings/accounts/templates/accounts/user_detail.html:125 msgid "Recent Shifts" msgstr "Letzte Schichten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:127 -#: src/shiftings/accounts/templates/accounts/user_detail.html:132 +#: shiftings/accounts/templates/accounts/user_detail.html:127 +#: shiftings/accounts/templates/accounts/user_detail.html:132 msgid "Upcoming Shifts" msgstr "Anstehende Schichten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:136 +#: shiftings/accounts/templates/accounts/user_detail.html:136 msgid "Past Shifts" msgstr "Vergangene Schichten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:157 +#: shiftings/accounts/templates/accounts/user_detail.html:157 msgid "No recent Shifts" msgstr "Keine vergangenen Schichten" -#: src/shiftings/accounts/templates/accounts/user_detail.html:159 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:135 +#: shiftings/accounts/templates/accounts/user_detail.html:159 +#: shiftings/organizations/templates/organizations/organization_shifts.html:135 msgid "No upcoming shifts" msgstr "Keine anstehenden Schichten" -#: src/shiftings/accounts/templates/accounts/user_form.html:6 +#: shiftings/accounts/templates/accounts/user_form.html:6 msgid "Register" msgstr "Registrieren" -#: src/shiftings/accounts/templates/accounts/user_form.html:8 +#: shiftings/accounts/templates/accounts/user_form.html:8 #, python-format msgid "" "\n" @@ -1904,434 +316,440 @@ msgstr "" " Bearbeite Benutzer %(user)s\n" " " -#: src/shiftings/accounts/templates/accounts/user_form.html:19 -#: src/shiftings/templates/template/simple_form_modal.html:21 +#: shiftings/accounts/templates/accounts/user_form.html:19 +#: shiftings/templates/template/simple_form_modal.html:21 msgid "Submit" msgstr "Bestätigen" -#: src/shiftings/accounts/views/auth.py:83 +#: shiftings/accounts/views/auth.py:82 msgid "Error while creating the user instance!" msgstr "Fehler beim erstellen der Benutzerinstanz!" -#: src/shiftings/accounts/views/auth.py:87 +#: shiftings/accounts/views/auth.py:86 msgid "Successfully logged in!" msgstr "Erfolgreich eingeloggt!" -#: src/shiftings/accounts/views/auth.py:90 +#: shiftings/accounts/views/auth.py:89 #, python-format msgid "Error while trying to authenticate with sso! %(message)s" msgstr "Fehler beim Versuch gegen SSO zu authentifizieren! %(message)s" -#: src/shiftings/accounts/views/auth.py:131 -#: src/shiftings/templates/top_nav.html:52 +#: shiftings/accounts/views/auth.py:130 shiftings/templates/top_nav.html:55 msgid "Logout" msgstr "Ausloggen" -#: src/shiftings/accounts/views/password.py:13 +#: shiftings/accounts/views/password.py:13 msgid "Password Reset" msgstr "Passwort zurücksetzen" -#: src/shiftings/accounts/views/password.py:19 +#: shiftings/accounts/views/password.py:19 msgid "Confirm Password Reset" msgstr "Passwort Zurücksetzung bestätigen" -#: src/shiftings/accounts/views/user.py:35 -#: src/shiftings/templates/top_nav.html:15 +#: shiftings/accounts/views/user.py:35 shiftings/templates/top_nav.html:15 msgid "My Shifts" msgstr "Meine Schichten" -#: src/shiftings/accounts/views/user.py:100 +#: shiftings/accounts/views/user.py:101 msgid "Could not find your user." msgstr "Konnte deinen Benutzer nicht finden." -#: src/shiftings/accounts/views/user.py:106 +#: shiftings/accounts/views/user.py:107 msgid "Your EMail was confirmed. You can now login." msgstr "Deine EMail wurde bestätigt. Du kannst dich jetzt einloggen." -#: src/shiftings/accounts/views/user.py:109 +#: shiftings/accounts/views/user.py:110 msgid "Your activation link was invalid." msgstr "Dein Aktivierungslink war ungültig." -#: src/shiftings/accounts/views/user.py:126 +#: shiftings/accounts/views/user.py:127 msgid "Error while deleting your data: Please confirm deletion!" msgstr "Fehler beim Löschen deiner Daten: Bitte bestätige die Löschung!" -#: src/shiftings/cal/feed/event.py:44 +#: shiftings/cal/feed/event.py:44 msgid "Public Events" msgstr "Öffentliche Events" -#: src/shiftings/cal/forms/day_form.py:8 +#: shiftings/cal/forms/day_form.py:8 msgid "Select Day" msgstr "Tag auswählen" -#: src/shiftings/cal/templates/cal/calendar_base.html:14 -#: src/shiftings/cal/templates/cal/calendar_base.html:24 +#: shiftings/cal/templates/cal/calendar_base.html:14 +#: shiftings/cal/templates/cal/calendar_base.html:24 msgid "Day View" msgstr "Tagesansicht" -#: src/shiftings/cal/templates/cal/calendar_base.html:20 +#: shiftings/cal/templates/cal/calendar_base.html:20 msgid "Day (Detailed)" msgstr "Tag (Detailliert)" -#: src/shiftings/cal/templates/cal/calendar_base.html:30 +#: shiftings/cal/templates/cal/calendar_base.html:30 msgid "Day (By Types)" msgstr "Tag (Nach Typ)" -#: src/shiftings/cal/templates/cal/calendar_base.html:37 +#: shiftings/cal/templates/cal/calendar_base.html:37 msgid "Month View" msgstr "Monatsansicht" -#: src/shiftings/cal/templates/cal/calendar_base.html:38 -#: src/shiftings/utils/time/month.py:25 -#: src/shiftings/utils/time/timerange.py:12 +#: shiftings/cal/templates/cal/calendar_base.html:38 +#: shiftings/utils/time/month.py:25 shiftings/utils/time/timerange.py:12 msgid "Month" msgstr "Monat" -#: src/shiftings/cal/templates/cal/calendar_base.html:42 -#: src/shiftings/cal/templates/cal/calendar_base.html:48 +#: shiftings/cal/templates/cal/calendar_base.html:42 +#: shiftings/cal/templates/cal/calendar_base.html:48 msgid "List View" msgstr "Listenansicht" -#: src/shiftings/cal/templates/cal/calendar_base.html:44 +#: shiftings/cal/templates/cal/calendar_base.html:44 msgid "List (Detailed)" msgstr "Liste (Detailliert)" -#: src/shiftings/cal/templates/cal/calendar_base.html:50 +#: shiftings/cal/templates/cal/calendar_base.html:50 msgid "List (By Types)" msgstr "Liste (Nach Typ)" -#: src/shiftings/cal/templates/cal/calendar_templates/cal_shift_create_entry.html:4 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:106 -#: src/shiftings/events/templates/events/event.html:74 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:376 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:120 +#: shiftings/cal/templates/cal/calendar_templates/cal_shift_create_entry.html:4 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:106 +#: shiftings/events/templates/events/event.html:74 +#: shiftings/organizations/templates/organizations/organization_admin.html:376 +#: shiftings/organizations/templates/organizations/organization_shifts.html:120 msgid "Create Shift" msgstr "Schicht erstellen" -#: src/shiftings/cal/templates/cal/calendar_templates/recurring_shift_entry.html:5 +#: shiftings/cal/templates/cal/calendar_templates/recurring_shift_entry.html:5 msgid "Recurring" msgstr "Wiederholend" -#: src/shiftings/cal/templates/cal/day_calendar.html:14 +#: shiftings/cal/templates/cal/day_calendar.html:14 msgid "Previous Day" msgstr "Letzter Tag" -#: src/shiftings/cal/templates/cal/day_calendar.html:22 +#: shiftings/cal/templates/cal/day_calendar.html:22 msgid "Jump to Date" msgstr "Zum Datum springen" -#: src/shiftings/cal/templates/cal/day_calendar.html:28 +#: shiftings/cal/templates/cal/day_calendar.html:28 msgid "Jump to Day" msgstr "Zum Tag springen" -#: src/shiftings/cal/templates/cal/day_calendar.html:30 +#: shiftings/cal/templates/cal/day_calendar.html:30 msgid "Select" msgstr "Auswählen" -#: src/shiftings/cal/templates/cal/day_calendar.html:38 +#: shiftings/cal/templates/cal/day_calendar.html:38 msgid "Next Day" msgstr "Nächster Tag" -#: src/shiftings/cal/templates/cal/list_calendar.html:12 +#: shiftings/cal/templates/cal/list_calendar.html:12 #, fuzzy #| msgid "Shifts" msgid "Shift list" -msgstr "Schichten" +msgstr "Schichtenliste" + +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:3 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:3 +#, python-format +msgid "" +"\n" +" Your query was capped to the server limit of %(max_entries)s. Please " +"filter more specifically.\n" +" " +msgstr "" +"\n" +" Deine Anfrage wurde auf %(max_entries)s Schichten begrenzt. Bitte gib " +"genauere Filter an.\n" +" " -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:7 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:48 -#: src/shiftings/shifts/templates/shifts/shift_participants.html:4 +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:14 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:48 +#: shiftings/shifts/templates/shifts/shift_participants.html:4 msgid "Add with Name to Shift" msgstr "Mit Namen zur Schicht hinzufügen" -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:33 -#: src/shiftings/organizations/models/membership.py:12 +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:30 +#: shiftings/organizations/templates/organizations/organization_admin.html:197 +#: shiftings/shifts/templates/shifts/template/group.html:43 +#: shiftings/shifts/templates/shifts/template/group_template.html:23 +#: shiftings/shifts/templates/shifts/template/shift.html:10 +#: shiftings/shifts/templates/shifts/template/template_form.html:33 +msgid "Time" +msgstr "Zeit" + +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:40 +#: shiftings/organizations/models/membership.py:12 msgid "Default" msgstr "Standard" -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:38 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:95 +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:45 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:95 msgid "No Shifts found with these parameters or planned on this day" msgstr "" "An diesem Tag sind keine Schichten mit diesen Parametern gefunden oder " "geplant" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:3 -msgid "" -"\n" -" Your query was capped to the server limit of %(max_entries)s. Please " -"filter more specifically.\n" -" " -msgstr "" -"\n" -" Deine Anfrage wurde auf %(max_entries)s Schichten begrenzt. Bitte " -"gib genauere Filter an.\n" -" " - -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:12 -#: src/shiftings/events/models/event.py:24 -#: src/shiftings/shifts/models/base.py:12 -#: src/shiftings/shifts/models/recurring.py:34 -#: src/shiftings/shifts/models/template_group.py:24 -#: src/shiftings/shifts/models/type.py:37 -#: src/shiftings/shifts/models/type_group.py:14 -#: src/shiftings/shifts/templates/shifts/create_shift.html:79 -#: src/shiftings/shifts/templates/shifts/list.html:9 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:9 -#: src/shiftings/shifts/templates/shifts/shift.html:47 -#: src/shiftings/shifts/templates/shifts/template/group.html:39 -#: src/shiftings/shifts/templates/shifts/template/group.html:45 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:8 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:25 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:12 +#: shiftings/events/models/event.py:24 shiftings/shifts/models/base.py:12 +#: shiftings/shifts/models/recurring.py:34 +#: shiftings/shifts/models/template_group.py:24 +#: shiftings/shifts/models/type.py:37 shiftings/shifts/models/type_group.py:14 +#: shiftings/shifts/templates/shifts/create_shift.html:89 +#: shiftings/shifts/templates/shifts/list.html:9 +#: shiftings/shifts/templates/shifts/recurring/list.html:9 +#: shiftings/shifts/templates/shifts/shift.html:47 +#: shiftings/shifts/templates/shifts/template/group.html:39 +#: shiftings/shifts/templates/shifts/template/group.html:45 +#: shiftings/shifts/templates/shifts/template/group_list.html:8 +#: shiftings/shifts/templates/shifts/template/group_template.html:25 msgid "Organization" msgstr "Organisation" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:13 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:13 msgid "Org" msgstr "Org" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:14 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:14 msgid "Shift" msgstr "Schicht" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:15 -#: src/shiftings/shifts/templates/shifts/create_shift.html:56 -#: src/shiftings/shifts/templates/shifts/template/shift.html:12 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:21 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:15 +#: shiftings/shifts/templates/shifts/create_shift.html:66 +#: shiftings/shifts/templates/shifts/template/shift.html:12 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:21 msgid "Participants" msgstr "Teilnehmer" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:16 -#: src/shiftings/shifts/templates/shifts/list.html:10 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:16 +#: shiftings/shifts/templates/shifts/list.html:10 msgid "Start" msgstr "Start" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:17 -#: src/shiftings/shifts/templates/shifts/list.html:11 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:17 +#: shiftings/shifts/templates/shifts/list.html:11 msgid "End" msgstr "Ende" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:36 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:7 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:36 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:7 msgid "Shift Details" msgstr "Schicht Details" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:61 -#: src/shiftings/shifts/templates/shifts/shift_participants.html:23 -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:29 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:44 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:61 +#: shiftings/shifts/templates/shifts/shift_participants.html:23 +#: shiftings/shifts/templates/shifts/template/slots_display.html:29 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:44 msgid "Add me" msgstr "Mich hinzufügen" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:112 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:31 -#: src/shiftings/shifts/templates/shifts/edit_templates.html:26 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:112 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:31 +#: shiftings/shifts/templates/shifts/edit_templates.html:26 msgid "Add Shift" msgstr "Schicht hinzufügen" -#: src/shiftings/cal/templates/cal/template/month_calendar.html:7 +#: shiftings/cal/templates/cal/template/month_calendar.html:7 msgid "Previous Month" msgstr "Letzter Monat" -#: src/shiftings/cal/templates/cal/template/month_calendar.html:15 +#: shiftings/cal/templates/cal/template/month_calendar.html:15 msgid "Next Month" msgstr "Nächster Monat" -#: src/shiftings/cal/templates/cal/week_calendar.html:9 +#: shiftings/cal/templates/cal/week_calendar.html:9 msgid "Previous Week" msgstr "Letzte Woche" -#: src/shiftings/cal/templates/cal/week_calendar.html:13 +#: shiftings/cal/templates/cal/week_calendar.html:13 msgid "Week" msgstr "Woche" -#: src/shiftings/cal/templates/cal/week_calendar.html:17 +#: shiftings/cal/templates/cal/week_calendar.html:17 msgid "Next Week" msgstr "Nächste Woche" -#: src/shiftings/cal/views/day_calendar.py:24 +#: shiftings/cal/views/day_calendar.py:24 #, python-brace-format msgid "Day {date}" msgstr "Tag {date}" -#: src/shiftings/cal/views/day_calendar.py:25 +#: shiftings/cal/views/day_calendar.py:25 msgid "Day Overview" msgstr "Tagesübersicht" -#: src/shiftings/cal/views/list.py:24 -#: src/shiftings/organizations/templates/organizations/template/org_info.html:6 +#: shiftings/cal/views/list.py:24 +#: shiftings/organizations/templates/organizations/template/org_info.html:6 msgid "Shift Overview" msgstr "Schichtenübersicht" -#: src/shiftings/cal/views/month_calendar.py:24 +#: shiftings/cal/views/month_calendar.py:24 #, python-brace-format msgid "Month {year}/{month:0>2}" msgstr "Monat {year}/{month:0>2}" -#: src/shiftings/cal/views/month_calendar.py:26 -#: src/shiftings/events/templates/events/event.html:52 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:162 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:28 +#: shiftings/cal/views/month_calendar.py:26 +#: shiftings/events/templates/events/event.html:52 +#: shiftings/organizations/templates/organizations/organization_admin.html:162 +#: shiftings/organizations/templates/organizations/organization_shifts.html:28 msgid "Month Overview" msgstr "Monatsübersicht" -#: src/shiftings/events/forms/event.py:24 +#: shiftings/events/forms/event.py:24 #, python-format msgid "Start date %(start)s was after end_date %(end)s" msgstr "Startdatum %(start)s ist nach dem Enddatum %(end)s" -#: src/shiftings/events/models/event.py:26 -#: src/shiftings/events/templates/events/event.html:19 -#: src/shiftings/organizations/models/organization.py:25 -#: src/shiftings/organizations/templates/organizations/template/org_info.html:58 +#: shiftings/events/models/event.py:26 +#: shiftings/events/templates/events/event.html:19 +#: shiftings/organizations/models/organization.py:25 +#: shiftings/organizations/templates/organizations/template/org_info.html:58 msgid "Logo" msgstr "Logo" -#: src/shiftings/events/models/event.py:28 -#: src/shiftings/organizations/models/organization.py:27 +#: shiftings/events/models/event.py:28 +#: shiftings/organizations/models/organization.py:27 msgid "E-Mail" msgstr "E-Mail" -#: src/shiftings/events/models/event.py:30 -#: src/shiftings/organizations/models/organization.py:29 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:81 -#: src/shiftings/organizations/templates/organizations/template/organization.html:27 +#: shiftings/events/models/event.py:30 +#: shiftings/organizations/models/organization.py:29 +#: shiftings/organizations/templates/organizations/organization_shifts.html:81 +#: shiftings/organizations/templates/organizations/template/organization.html:27 msgid "Website" msgstr "Webseite" -#: src/shiftings/events/models/event.py:32 +#: shiftings/events/models/event.py:32 msgid "Start Date" msgstr "Startdatum" -#: src/shiftings/events/models/event.py:32 +#: shiftings/events/models/event.py:32 msgid "Earliest date where there are shifts available" msgstr "Frühestes Datum an dem Schichten verfügbar sind" -#: src/shiftings/events/models/event.py:33 +#: shiftings/events/models/event.py:33 msgid "End Date" msgstr "Enddatum" -#: src/shiftings/events/models/event.py:33 +#: shiftings/events/models/event.py:33 msgid "Latest date where there are shifts available" msgstr "Spätestes Datum an dem Schichten verfügbar sind" -#: src/shiftings/events/models/event.py:35 -#: src/shiftings/organizations/models/organization.py:32 +#: shiftings/events/models/event.py:35 +#: shiftings/organizations/models/organization.py:32 msgid "Description" msgstr "Beschreibung" -#: src/shiftings/events/models/event.py:55 +#: shiftings/events/models/event.py:55 #, python-brace-format msgid "{name} (by {organization})" msgstr "{name} (von {organization})" -#: src/shiftings/events/templates/events/event.html:10 +#: shiftings/events/templates/events/event.html:10 msgid "Edit Event" msgstr "Event bearbeiten" -#: src/shiftings/events/templates/events/event.html:55 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:165 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:31 +#: shiftings/events/templates/events/event.html:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:165 +#: shiftings/organizations/templates/organizations/organization_shifts.html:31 msgid "Complete Overview" msgstr "Komplette Übersicht" -#: src/shiftings/events/templates/events/event.html:69 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:363 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:109 +#: shiftings/events/templates/events/event.html:69 +#: shiftings/organizations/templates/organizations/organization_admin.html:363 +#: shiftings/organizations/templates/organizations/organization_shifts.html:109 msgid "Shifts" msgstr "Schichten" -#: src/shiftings/events/templates/events/event.html:71 -#: src/shiftings/events/templates/events/event.html:72 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:371 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:112 +#: shiftings/events/templates/events/event.html:71 +#: shiftings/events/templates/events/event.html:72 +#: shiftings/organizations/templates/organizations/organization_admin.html:371 +#: shiftings/organizations/templates/organizations/organization_shifts.html:112 msgid "ical Export" msgstr "ical Export" -#: src/shiftings/events/templates/events/event.html:86 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:389 -#: src/shiftings/shifts/templates/shifts/list.html:7 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:7 -#: src/shiftings/shifts/templates/shifts/template/shift.html:8 -#: src/shiftings/shifts/templates/shifts/template/template.html:7 +#: shiftings/events/templates/events/event.html:86 +#: shiftings/organizations/templates/organizations/organization_admin.html:389 +#: shiftings/shifts/templates/shifts/list.html:7 +#: shiftings/shifts/templates/shifts/recurring/list.html:7 +#: shiftings/shifts/templates/shifts/template/shift.html:8 +#: shiftings/shifts/templates/shifts/template/template.html:7 msgid "Type" msgstr "Typ" -#: src/shiftings/events/templates/events/event.html:87 -#: src/shiftings/mail/forms/mail.py:39 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:241 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:390 -#: src/shiftings/shifts/models/template_group.py:26 -#: src/shiftings/shifts/templates/shifts/shift.html:43 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:9 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:17 -#: src/shiftings/shifts/templates/shifts/template/template.html:13 +#: shiftings/events/templates/events/event.html:87 +#: shiftings/mail/forms/mail.py:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:241 +#: shiftings/organizations/templates/organizations/organization_admin.html:390 +#: shiftings/shifts/models/template_group.py:26 +#: shiftings/shifts/templates/shifts/shift.html:43 +#: shiftings/shifts/templates/shifts/template/group_list.html:9 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:17 +#: shiftings/shifts/templates/shifts/template/template.html:13 msgid "Start Time" msgstr "Startzeit" -#: src/shiftings/events/templates/events/event.html:88 -#: src/shiftings/mail/forms/mail.py:40 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:391 -#: src/shiftings/shifts/templates/shifts/shift.html:45 -#: src/shiftings/shifts/templates/shifts/template/template.html:15 +#: shiftings/events/templates/events/event.html:88 +#: shiftings/mail/forms/mail.py:56 +#: shiftings/organizations/templates/organizations/organization_admin.html:391 +#: shiftings/shifts/templates/shifts/shift.html:45 +#: shiftings/shifts/templates/shifts/template/template.html:15 msgid "End Time" msgstr "Endzeit" -#: src/shiftings/events/templates/events/event.html:89 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:198 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:240 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:392 -#: src/shiftings/shifts/models/base.py:9 -#: src/shiftings/shifts/models/template_group.py:21 -#: src/shiftings/shifts/templates/shifts/list.html:8 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:8 -#: src/shiftings/shifts/templates/shifts/shift.html:41 -#: src/shiftings/shifts/templates/shifts/template/group.html:41 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:7 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:21 -#: src/shiftings/shifts/templates/shifts/template/shift_card.html:12 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:18 +#: shiftings/events/templates/events/event.html:89 +#: shiftings/organizations/templates/organizations/organization_admin.html:198 +#: shiftings/organizations/templates/organizations/organization_admin.html:240 +#: shiftings/organizations/templates/organizations/organization_admin.html:392 +#: shiftings/shifts/models/base.py:9 +#: shiftings/shifts/models/template_group.py:21 +#: shiftings/shifts/templates/shifts/list.html:8 +#: shiftings/shifts/templates/shifts/recurring/list.html:8 +#: shiftings/shifts/templates/shifts/shift.html:41 +#: shiftings/shifts/templates/shifts/template/group.html:41 +#: shiftings/shifts/templates/shifts/template/group_list.html:7 +#: shiftings/shifts/templates/shifts/template/group_template.html:21 +#: shiftings/shifts/templates/shifts/template/shift_card.html:12 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:18 msgid "Place" msgstr "Ort" -#: src/shiftings/events/templates/events/event.html:90 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:393 +#: shiftings/events/templates/events/event.html:90 +#: shiftings/organizations/templates/organizations/organization_admin.html:393 msgid "Users/Required/Max" msgstr "Benutzer/Benötigt/Maximum" -#: src/shiftings/events/templates/events/event.html:108 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:416 +#: shiftings/events/templates/events/event.html:108 +#: shiftings/organizations/templates/organizations/organization_admin.html:416 msgid "No current shifts" msgstr "Keine aktuellen Schichten" -#: src/shiftings/events/templates/events/list.html:5 -#: src/shiftings/events/templates/events/list.html:13 -#: src/shiftings/events/templates/events/list.html:22 +#: shiftings/events/templates/events/list.html:5 +#: shiftings/events/templates/events/list.html:13 +#: shiftings/events/templates/events/list.html:22 msgid "All Events" msgstr "Alle Events" -#: src/shiftings/events/templates/events/list.html:7 -#: src/shiftings/templates/top_nav.html:22 +#: shiftings/events/templates/events/list.html:7 +#: shiftings/templates/top_nav.html:22 msgid "Current Events" msgstr "Aktuelle Events" -#: src/shiftings/events/templates/events/list.html:16 +#: shiftings/events/templates/events/list.html:16 msgid "All upcoming Events" msgstr "Alle anstehenden Events" -#: src/shiftings/events/templates/events/list.html:20 +#: shiftings/events/templates/events/list.html:20 msgid "Enter Name or Organization" msgstr "Name oder Organisation angeben" -#: src/shiftings/events/templates/events/list.html:26 +#: shiftings/events/templates/events/list.html:26 msgid "Future Events" msgstr "Zukünftige Events" -#: src/shiftings/events/templates/events/list.html:39 +#: shiftings/events/templates/events/list.html:39 msgid "No Events found." msgstr "Keine Events gefunden." -#: src/shiftings/events/templates/events/template/event.html:23 +#: shiftings/events/templates/events/template/event.html:23 #, python-format msgid "" "\n" @@ -2342,69 +760,81 @@ msgstr "" " %(start_date)s bis %(end_date)s\n" " " -#: src/shiftings/events/templates/events/template/event.html:31 +#: shiftings/events/templates/events/template/event.html:31 msgid "Open Shifts" msgstr "Offene Schichten" -#: src/shiftings/events/templates/events/template/event.html:32 +#: shiftings/events/templates/events/template/event.html:32 msgid "Required People" msgstr "Benötigte Personen" -#: src/shiftings/events/templates/events/template/event.html:33 +#: shiftings/events/templates/events/template/event.html:33 msgid "Visibility" msgstr "Sichtbarkeit" -#: src/shiftings/events/templates/events/template/event.html:33 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:350 +#: shiftings/events/templates/events/template/event.html:33 +#: shiftings/organizations/templates/organizations/organization_admin.html:350 msgid "Public,Private,Unknown" msgstr "Öffentlich,Privat,Unbekannt" -#: src/shiftings/events/templates/events/template/event_stats.html:2 +#: shiftings/events/templates/events/template/event_stats.html:2 msgid "Shifts that are below the required amount of users" msgstr "Schichten mit weniger als den benötigten Benutzern" -#: src/shiftings/events/templates/events/template/event_stats.html:3 +#: shiftings/events/templates/events/template/event_stats.html:3 msgid "Shifts missing users" msgstr "Schichten mit fehlenden Benutzern" -#: src/shiftings/events/templates/events/template/event_stats.html:6 +#: shiftings/events/templates/events/template/event_stats.html:6 msgid "Shifts that are below the required and maximum amount of users" msgstr "Schichten unter der benötigten und maximalen Benutzeranzahl" -#: src/shiftings/events/templates/events/template/event_stats.html:7 +#: shiftings/events/templates/events/template/event_stats.html:7 msgid "Shifts with open slots" msgstr "Schichten mit offenen Positionen" -#: src/shiftings/events/templates/events/template/event_stats.html:10 +#: shiftings/events/templates/events/template/event_stats.html:10 msgid "Number of Shift slots filled with users" msgstr "Anzahl mit Benutzern gefüllte Positionen" -#: src/shiftings/events/templates/events/template/event_stats.html:11 +#: shiftings/events/templates/events/template/event_stats.html:11 msgid "Filled Slots" msgstr "Gefüllte Positionen" -#: src/shiftings/events/templates/events/template/event_stats.html:12 -#: src/shiftings/events/templates/events/template/event_stats.html:16 +#: shiftings/events/templates/events/template/event_stats.html:12 +#: shiftings/events/templates/events/template/event_stats.html:16 msgid "No Shifts available" msgstr "Keine Schichten verfügbar" -#: src/shiftings/events/templates/events/template/event_stats.html:14 +#: shiftings/events/templates/events/template/event_stats.html:14 msgid "Number of Shift slots that are still open" msgstr "Anzahl offene Postiionen" -#: src/shiftings/events/templates/events/template/event_stats.html:15 +#: shiftings/events/templates/events/template/event_stats.html:15 msgid "Open Slots" msgstr "Offene Postionen" -#: src/shiftings/mail/forms/mail.py:10 +#: shiftings/mail/forms/mail.py:13 msgid "Subject" msgstr "Betreff" -#: src/shiftings/mail/forms/mail.py:12 +#: shiftings/mail/forms/mail.py:14 +msgid "Text" +msgstr "Text" + +#: shiftings/mail/forms/mail.py:15 msgid "Attachments" msgstr "Anhänge" -#: src/shiftings/mail/forms/mail.py:22 +#: shiftings/mail/forms/mail.py:26 +msgid "Each attachment must be smaller than 10 MB." +msgstr "" + +#: shiftings/mail/forms/mail.py:30 +msgid "Total attachment size must be smaller than 25 MB." +msgstr "" + +#: shiftings/mail/forms/mail.py:38 msgid "" "Send the mail only to specific membership types. Or leave blank to send to " "all." @@ -2412,7 +842,7 @@ msgstr "" "Email nur an bestimmte Mitgliedertypen senden oder leerlassen um sie an alle " "zu schicken." -#: src/shiftings/mail/forms/mail.py:37 +#: shiftings/mail/forms/mail.py:53 #, fuzzy #| msgid "" #| "Send the mail only to specific membership types. Or leave blank to send " @@ -2423,193 +853,195 @@ msgstr "" "Email nur an bestimmte Mitgliedertypen senden oder leerlassen um sie an alle " "zu schicken." -#: src/shiftings/mail/templates/mail/mail.html:8 +#: shiftings/mail/forms/mail.py:71 +msgid "Start time must be before end time." +msgstr "" + +#: shiftings/mail/templates/mail/mail.html:8 msgid "Send Mail" msgstr "Email versenden" -#: src/shiftings/mail/views/mail.py:45 +#: shiftings/mail/views/mail.py:45 #, python-brace-format msgid "E-Mail sent to {count} user(s)." msgstr "Email an {count} Benutzer verschickt." -#: src/shiftings/organizations/forms/membership.py:40 -#: src/shiftings/shifts/forms/participant.py:57 +#: shiftings/organizations/forms/membership.py:40 +#: shiftings/shifts/forms/participant.py:57 msgid "The user you entered could not be found." msgstr "Der eingetragene Benutzer konnte nicht gefunden werden." -#: src/shiftings/organizations/models/membership.py:11 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:68 -#: src/shiftings/templates/top_nav.html:50 +#: shiftings/organizations/models/membership.py:11 +#: shiftings/organizations/templates/organizations/organization_admin.html:68 +#: shiftings/templates/top_nav.html:51 msgid "Admin" msgstr "Admin" -#: src/shiftings/organizations/models/membership.py:13 +#: shiftings/organizations/models/membership.py:13 msgid "Permissions" msgstr "Berechtigungen" -#: src/shiftings/organizations/models/membership.py:19 +#: shiftings/organizations/models/membership.py:19 msgid "edit organization details" msgstr "Organisationdetails bearbeiten" -#: src/shiftings/organizations/models/membership.py:20 +#: shiftings/organizations/models/membership.py:20 msgid "see members of the organization" msgstr "Mitglieder der Organisation sehen" -#: src/shiftings/organizations/models/membership.py:21 +#: shiftings/organizations/models/membership.py:21 msgid "see shift participation statistics" msgstr "Schicht Teilnahmestatistik der Organistation sehen" -#: src/shiftings/organizations/models/membership.py:22 +#: shiftings/organizations/models/membership.py:22 msgid "send emails to everyone in the organization" msgstr "Emails an alle in der Organisation senden" -#: src/shiftings/organizations/models/membership.py:24 +#: shiftings/organizations/models/membership.py:24 msgid "create and update membership types for the organization" msgstr "Mitgliedschaftstypen in der Organisation erstellen und ändern" -#: src/shiftings/organizations/models/membership.py:25 +#: shiftings/organizations/models/membership.py:25 msgid "add and remove members for the organization" msgstr "Mitglieder in der Organisation hinzufügen und entfernen" -#: src/shiftings/organizations/models/membership.py:27 +#: shiftings/organizations/models/membership.py:27 msgid "create and update events for the organization" msgstr "Events für die Organisation erstellen und bearbeiten" -#: src/shiftings/organizations/models/membership.py:29 +#: shiftings/organizations/models/membership.py:29 msgid "create and update recurring shifts for the organization" msgstr "Wiederkehrende Schichten für die Organisation erstellen und bearbeiten" -#: src/shiftings/organizations/models/membership.py:30 +#: shiftings/organizations/models/membership.py:30 msgid "create and update shifts templates for the organization" msgstr "Schichtvorlagen für die Organisation erstellen und bearbeiten" -#: src/shiftings/organizations/models/membership.py:31 +#: shiftings/organizations/models/membership.py:31 msgid "create and update shifts for the organization" msgstr "Schichten für die Organisation erstellen und bearbeiten" -#: src/shiftings/organizations/models/membership.py:32 +#: shiftings/organizations/models/membership.py:32 msgid "delete uncompleted shifts for the organization" msgstr "Unabgeschlossene Schichten der Organistation löschen" -#: src/shiftings/organizations/models/membership.py:34 +#: shiftings/organizations/models/membership.py:34 msgid "remove others from shifts" msgstr "Andere von Schichten entfernen" -#: src/shiftings/organizations/models/membership.py:35 +#: shiftings/organizations/models/membership.py:35 msgid "add other users that are not members of the organization to shifts" msgstr "" "Andere Benutzer zu Schichten hinzufügen, die keine Mitglieder der " "Organisation sind" -#: src/shiftings/organizations/models/membership.py:36 +#: shiftings/organizations/models/membership.py:36 msgid "add other organization members to shifts" msgstr "Andere Organisationsmitglieder zu Schichten hinzufügen" -#: src/shiftings/organizations/models/membership.py:37 +#: shiftings/organizations/models/membership.py:37 msgid "participate in shifts" msgstr "An Schichten teilnehmen" -#: src/shiftings/organizations/models/membership.py:38 +#: shiftings/organizations/models/membership.py:38 msgid "add self/others (depending on other permissions) to past shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:57 +#: shiftings/organizations/models/membership.py:57 #, python-brace-format msgid "{name} (Admin)" msgstr "{name} (Admin)" -#: src/shiftings/organizations/models/membership.py:59 +#: shiftings/organizations/models/membership.py:59 #, python-brace-format msgid "{name} (Default)" msgstr "{name} (Standard)" -#: src/shiftings/organizations/models/membership.py:65 +#: shiftings/organizations/models/membership.py:65 msgid "Membership Type" msgstr "Mitgliedschaftstyp" -#: src/shiftings/organizations/models/membership.py:69 -#: src/shiftings/shifts/models/type.py:38 +#: shiftings/organizations/models/membership.py:69 +#: shiftings/shifts/models/type.py:38 msgid "Group" msgstr "Gruppe" -#: src/shiftings/organizations/models/membership.py:100 +#: shiftings/organizations/models/membership.py:100 msgid "Membership can only be either user or group, not both." msgstr "" "Mitgliedschaft kann nur für Benutzer oder Gruppe eingetragen werden nicht " "beides auf einmal." -#: src/shiftings/organizations/models/membership.py:102 +#: shiftings/organizations/models/membership.py:102 msgid "Membership must consist of a user or a group." msgstr "Mitgliedschaft muss aus einem Benutzer oder einer Gruppe bestehen." -#: src/shiftings/organizations/models/organization.py:30 -msgid "Include Protocol i.E. https://example.com" -msgstr "Inklusive Protokoll z.B. https://example.com" +#: shiftings/organizations/models/organization.py:30 +msgid "Include protocol, e.g." +msgstr "" -#: src/shiftings/organizations/models/organization.py:33 -#: src/shiftings/shifts/models/base.py:24 -#: src/shiftings/shifts/models/recurring.py:56 -#: src/shiftings/shifts/models/recurring.py:63 -#: src/shiftings/shifts/models/shift.py:32 -#: src/shiftings/shifts/models/template.py:33 +#: shiftings/organizations/models/organization.py:33 +#: shiftings/shifts/models/base.py:24 shiftings/shifts/models/recurring.py:56 +#: shiftings/shifts/models/recurring.py:63 shiftings/shifts/models/shift.py:32 +#: shiftings/shifts/models/template.py:33 #, python-brace-format msgid "A maximum of {amount} characters is allowed" msgstr "Es sind maximal {amount} Zeichen erlaubt" -#: src/shiftings/organizations/models/organization.py:34 +#: shiftings/organizations/models/organization.py:34 msgid "Confirm Participants" msgstr "Teilnehmer bestätigen" -#: src/shiftings/organizations/models/organization.py:35 +#: shiftings/organizations/models/organization.py:35 msgid "Whether the participants can be confirmed or not. Default: False" msgstr "Telnehmer können bestätigt werden. Standard: False" -#: src/shiftings/organizations/models/organization.py:49 +#: shiftings/organizations/models/organization.py:49 msgid "manage all organizations" msgstr "Alle Organisationen bearbeiten" -#: src/shiftings/organizations/models/user.py:10 +#: shiftings/organizations/models/user.py:10 msgid "Claimed by" msgstr "Beansprucht von" -#: src/shiftings/organizations/templates/organizations/list.html:5 +#: shiftings/organizations/templates/organizations/list.html:5 msgid "All Organizations" msgstr "Alle Organisationen" -#: src/shiftings/organizations/templates/organizations/list.html:7 -#: src/shiftings/templates/top_nav.html:18 +#: shiftings/organizations/templates/organizations/list.html:7 +#: shiftings/templates/top_nav.html:18 msgid "My Organizations" msgstr "Meine Organisationen" -#: src/shiftings/organizations/templates/organizations/list.html:12 +#: shiftings/organizations/templates/organizations/list.html:12 msgid "Enter Name" msgstr "Name einegeben" -#: src/shiftings/organizations/templates/organizations/list.html:14 +#: shiftings/organizations/templates/organizations/list.html:14 msgid "Add new Organization" msgstr "Neue Organisation hinzufügen" -#: src/shiftings/organizations/templates/organizations/list.html:25 +#: shiftings/organizations/templates/organizations/list.html:25 msgid "No Organizations found." msgstr "Keine Organisationen gefunden." -#: src/shiftings/organizations/templates/organizations/organization_admin.html:6 +#: shiftings/organizations/templates/organizations/organization_admin.html:6 msgid "Show membership Permissions" msgstr "Mitgliedschaftsberechtigungen anzeigen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:10 +#: shiftings/organizations/templates/organizations/organization_admin.html:10 msgid "All Permissions" msgstr "Alle Berechtigungen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:19 +#: shiftings/organizations/templates/organizations/organization_admin.html:19 msgid "No permissions" msgstr "Keine Berechtigungen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:26 +#: shiftings/organizations/templates/organizations/organization_admin.html:26 msgid "Add member" msgstr "Mitglied hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:41 +#: shiftings/organizations/templates/organizations/organization_admin.html:41 #, python-format msgid "" "\n" @@ -2620,183 +1052,187 @@ msgstr "" " Mitglieder von %(organization)s\n" " " -#: src/shiftings/organizations/templates/organizations/organization_admin.html:48 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:8 +#: shiftings/organizations/templates/organizations/organization_admin.html:48 +#: shiftings/organizations/templates/organizations/organization_shifts.html:8 msgid "Member Shift Summary" msgstr "Mitglieder Schichten Zusammenfassung" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:54 +#: shiftings/organizations/templates/organizations/organization_admin.html:54 msgid "Add Membership Type" msgstr "Mitgliedschaftstyp hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:78 +#: shiftings/organizations/templates/organizations/organization_admin.html:78 msgid "Show user permissions" msgstr "Benutzerberechtigungen anzeigen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:81 +#: shiftings/organizations/templates/organizations/organization_admin.html:81 msgid "Edit Membership Type" msgstr "Mitgliedschaftstyp bearbeiten" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:90 +#: shiftings/organizations/templates/organizations/organization_admin.html:90 msgid "Add Member" msgstr "Mitglied hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:100 +#: shiftings/organizations/templates/organizations/organization_admin.html:100 msgid "No Members of this type!" msgstr "Keine Mitglieder von diesem Typ!" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:106 +#: shiftings/organizations/templates/organizations/organization_admin.html:106 msgid "Direct Members" msgstr "Direkte Mitglieder" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:150 +#: shiftings/organizations/templates/organizations/organization_admin.html:150 msgid "Next Shift" msgstr "Nächste Schicht" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:154 +#: shiftings/organizations/templates/organizations/organization_admin.html:154 msgid "No Shifts planned" msgstr "Keine Schichten geplant" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:179 -#: src/shiftings/shifts/templates/shifts/template/group.html:52 +#: shiftings/organizations/templates/organizations/organization_admin.html:179 +#: shiftings/shifts/templates/shifts/template/group.html:52 msgid "Recurring Shifts" msgstr "Wiederkehrende Schichten" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:184 +#: shiftings/organizations/templates/organizations/organization_admin.html:184 msgid "Add recurring shift" msgstr "Wiederkehrende Schicht hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:196 +#: shiftings/organizations/templates/organizations/organization_admin.html:196 msgid "Shift count" msgstr "Schichten Anzahl" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:199 +#: shiftings/organizations/templates/organizations/organization_admin.html:199 msgid "Status" msgstr "Status" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:210 +#: shiftings/organizations/templates/organizations/organization_admin.html:210 msgid "Inactive,Active,Undefined" msgstr "Inaktiv,Aktiv,Unbekannt" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:216 +#: shiftings/organizations/templates/organizations/organization_admin.html:216 msgid "No recurring shifts" msgstr "Keine wiederkehrenden Schichten" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:223 +#: shiftings/organizations/templates/organizations/organization_admin.html:223 msgid "Shifts Templates" msgstr "Schichtvorlagen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:228 +#: shiftings/organizations/templates/organizations/organization_admin.html:228 msgid "Add shifts template" msgstr "Schichtvorlagen hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:255 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:309 +#: shiftings/organizations/templates/organizations/organization_admin.html:255 +#: shiftings/organizations/templates/organizations/organization_admin.html:309 msgid "No shifts templates" msgstr "Keine Schichtvorlagen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:262 +#: shiftings/organizations/templates/organizations/organization_admin.html:262 msgid "Shifts Types" msgstr "Schichttypen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:267 +#: shiftings/organizations/templates/organizations/organization_admin.html:267 msgid "Add Shift Type" msgstr "Schichttyp hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:279 +#: shiftings/organizations/templates/organizations/organization_admin.html:279 msgid "Color" msgstr "Farbe" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:280 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:15 +#: shiftings/organizations/templates/organizations/organization_admin.html:280 +#: shiftings/organizations/templates/organizations/user/claim_users.html:15 msgid "Shift Count" msgstr "Schichten Anzahl" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:281 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:41 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:16 +#: shiftings/organizations/templates/organizations/organization_admin.html:281 +#: shiftings/organizations/templates/organizations/organization_settings.html:41 +#: shiftings/organizations/templates/organizations/user/claim_users.html:16 msgid "Action" msgstr "Aktion" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:290 +#: shiftings/organizations/templates/organizations/organization_admin.html:290 msgid "Shift Type Background Color" msgstr "Schichttyp Hintergrundfarbe" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:317 +#: shiftings/organizations/templates/organizations/organization_admin.html:317 msgid "Upcoming Events" msgstr "Anstehende Events" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:321 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:50 +#: shiftings/organizations/templates/organizations/organization_admin.html:321 +#: shiftings/organizations/templates/organizations/organization_shifts.html:50 msgid "Expired Events" msgstr "Abgelaufene Events" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:326 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:326 +#: shiftings/organizations/templates/organizations/organization_shifts.html:55 msgid "Add Event" msgstr "Event hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:356 +#: shiftings/organizations/templates/organizations/organization_admin.html:356 msgid "No upcoming Events planned" msgstr "Keine anstehenden Events" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:367 +#: shiftings/organizations/templates/organizations/organization_admin.html:367 msgid "Claim Legacy Shifts" msgstr "Beanspruche alte Schichten" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:19 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:36 -#: src/shiftings/shifts/templates/shifts/edit_templates.html:31 -#: src/shiftings/shifts/templates/shifts/recurring/form.html:25 -#: src/shiftings/templates/generic/create_or_update.html:20 -#: src/shiftings/templates/generic/formset.html:17 +#: shiftings/organizations/templates/organizations/organization_settings.html:19 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:36 +#: shiftings/shifts/templates/shifts/edit_templates.html:31 +#: shiftings/shifts/templates/shifts/recurring/form.html:25 +#: shiftings/templates/generic/create_or_update.html:20 +#: shiftings/templates/generic/formset.html:17 msgid "Save" msgstr "Speichern" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:26 +#: shiftings/organizations/templates/organizations/organization_settings.html:26 msgid "Shift Type Groups" msgstr "Schichttypgruppen" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:29 +#: shiftings/organizations/templates/organizations/organization_settings.html:29 msgid "Create Type Group" msgstr "Schichttypgruppe hinzufügen" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:40 +#: shiftings/organizations/templates/organizations/organization_settings.html:38 +msgid "Order" +msgstr "Reihenfolge" + +#: shiftings/organizations/templates/organizations/organization_settings.html:40 msgid "Types" msgstr "Typen" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:61 +#: shiftings/organizations/templates/organizations/organization_settings.html:61 msgid "Edit Type Group" msgstr "Typgruppe editieren" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:72 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:77 +#: shiftings/organizations/templates/organizations/organization_settings.html:72 +#: shiftings/organizations/templates/organizations/organization_settings.html:77 msgid "Move Up" msgstr "Nach oben" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:84 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:89 +#: shiftings/organizations/templates/organizations/organization_settings.html:84 +#: shiftings/organizations/templates/organizations/organization_settings.html:89 msgid "Move Down" msgstr "Nach unten" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:11 +#: shiftings/organizations/templates/organizations/organization_shifts.html:11 msgid "Full Summary" msgstr "Komplette Zusammenfassung" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:46 +#: shiftings/organizations/templates/organizations/organization_shifts.html:46 msgid "Upcoming Event" msgstr "Anstehendes Event" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:66 -#: src/shiftings/shifts/models/shift.py:21 +#: shiftings/organizations/templates/organizations/organization_shifts.html:66 +#: shiftings/shifts/models/shift.py:21 msgid "Event" msgstr "Event" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:68 +#: shiftings/organizations/templates/organizations/organization_shifts.html:68 msgid "Go to Event" msgstr "Zum Event springen" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:74 +#: shiftings/organizations/templates/organizations/organization_shifts.html:74 #, python-format msgid "" "\n" @@ -2811,128 +1247,126 @@ msgstr "" "td>\n" " " -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:94 +#: shiftings/organizations/templates/organizations/organization_shifts.html:94 msgid "No Events planned" msgstr "Keine geplanten Events" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:102 +#: shiftings/organizations/templates/organizations/organization_shifts.html:102 msgid "Events disabled" msgstr "Events deaktiviert" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:117 +#: shiftings/organizations/templates/organizations/organization_shifts.html:117 msgid "Create Shift from Template" msgstr "Schichten aus Vorlage erstellen" -#: src/shiftings/organizations/templates/organizations/template/member_group.html:6 +#: shiftings/organizations/templates/organizations/template/member_group.html:6 msgid "Click to expand members" msgstr "Klicke für detaillierte Mitgliederansicht" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:8 +#: shiftings/organizations/templates/organizations/template/org_info.html:8 msgid "Organization Overview" msgstr "Organisationsübersicht" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:12 +#: shiftings/organizations/templates/organizations/template/org_info.html:12 msgid "Administrate Organization" msgstr "Organisation administrieren" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:14 +#: shiftings/organizations/templates/organizations/template/org_info.html:14 msgid "Organization Admin View" msgstr "Organisation Admin Ansicht" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:19 +#: shiftings/organizations/templates/organizations/template/org_info.html:19 msgid "Organization Settings" msgstr "Organisationseinstellungen" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:21 +#: shiftings/organizations/templates/organizations/template/org_info.html:21 msgid "Organization Settings View" msgstr "Organisations Einstellungen" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:35 +#: shiftings/organizations/templates/organizations/template/org_info.html:35 #, fuzzy #| msgid "See Shift Participants" msgid "Send Mail to shift participants" msgstr "Schichtteilnehmer sehen" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:41 +#: shiftings/organizations/templates/organizations/template/org_info.html:41 msgid "Edit Organization" msgstr "Organisation bearbeiten" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:47 +#: shiftings/organizations/templates/organizations/template/org_info.html:47 msgid "Update Shift Permissions" msgstr "Schichtberechtigungen aktualisieren" -#: src/shiftings/organizations/templates/organizations/template/organization.html:12 +#: shiftings/organizations/templates/organizations/template/organization.html:12 msgid "No Image available" msgstr "Kein Bild verfügbar" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:6 +#: shiftings/organizations/templates/organizations/user/claim_users.html:6 msgid "Imported Users" msgstr "Importierte Benutzer" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:14 +#: shiftings/organizations/templates/organizations/user/claim_users.html:14 msgid "Claimed" msgstr "Beansprucht" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:36 +#: shiftings/organizations/templates/organizations/user/claim_users.html:36 msgid "Claim" msgstr "Beanspruchen" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:43 +#: shiftings/organizations/templates/organizations/user/claim_users.html:43 msgid "Unclaim" msgstr "Freigeben" -#: src/shiftings/organizations/views/membership.py:55 -#: src/shiftings/organizations/views/membership_type.py:57 +#: shiftings/organizations/views/membership.py:55 +#: shiftings/organizations/views/membership_type.py:57 msgid "Membership removed" msgstr "Mitgliedschaft entfernt" -#: src/shiftings/organizations/views/organization.py:62 +#: shiftings/organizations/views/organization.py:62 #, python-brace-format msgid "{org} Overview" msgstr "{org}Übersicht" -#: src/shiftings/organizations/views/organization.py:89 +#: shiftings/organizations/views/organization.py:89 #, python-brace-format msgid "{org} Administration" msgstr "{org} Administration" -#: src/shiftings/organizations/views/organization.py:135 +#: shiftings/organizations/views/organization.py:135 #, python-brace-format msgid "{org} Settings" msgstr "{org} Einstellungen" -#: src/shiftings/organizations/views/user.py:36 +#: shiftings/organizations/views/user.py:36 msgid "You can't claim users that are already claimed." msgstr "Du kannst keine Benutzer beanspruchen, die bereits beansprucht sind." -#: src/shiftings/shifts/forms/filters.py:12 +#: shiftings/shifts/forms/filters.py:12 msgid "Shifts I participate in" msgstr "Schichten, an denen ich teilnehme" -#: src/shiftings/shifts/forms/filters.py:17 -#: src/shiftings/shifts/forms/filters.py:19 +#: shiftings/shifts/forms/filters.py:17 shiftings/shifts/forms/filters.py:19 msgid "Date YYYY-MM-DD" msgstr "Datum YYYY-MM-DD" -#: src/shiftings/shifts/forms/filters.py:18 -#: src/shiftings/shifts/forms/filters.py:20 +#: shiftings/shifts/forms/filters.py:18 shiftings/shifts/forms/filters.py:20 msgid "Time HH:MM" msgstr "Uhrzeit HH:MM" -#: src/shiftings/shifts/forms/participant.py:28 +#: shiftings/shifts/forms/participant.py:28 #, python-brace-format msgid "User {user} is already registered for this shift." msgstr "Benutzer {user} wurde bereits für diese Schicht eingetragen." -#: src/shiftings/shifts/forms/participant.py:32 +#: shiftings/shifts/forms/participant.py:32 msgid "Organization Users" msgstr "Organisationsmitglieder" -#: src/shiftings/shifts/forms/participant.py:33 +#: shiftings/shifts/forms/participant.py:33 msgid "Other Users" msgstr "Andere Benutzer" -#: src/shiftings/shifts/forms/participant.py:34 +#: shiftings/shifts/forms/participant.py:34 msgid "" "To add users that do not belong to your organization please enter their " "username." @@ -2940,21 +1374,21 @@ msgstr "" "Um Benutzer, die nicht Teil der Organisation sind, hinzuzufügen gib deren " "Benutzernamen an." -#: src/shiftings/shifts/forms/participant.py:65 +#: shiftings/shifts/forms/participant.py:65 msgid "Only select one type of user!" msgstr "Wähle nur einen Benutzertyp aus!" -#: src/shiftings/shifts/forms/participant.py:75 +#: shiftings/shifts/forms/participant.py:75 msgid "One of the user fields is required!" msgstr "Eines der Benutzerfelder ist benötigt!" -#: src/shiftings/shifts/forms/participant.py:80 +#: shiftings/shifts/forms/participant.py:80 #, python-brace-format msgid "Cannot add {user} multiple times to this shift" msgstr "Benutzer {user} kann nicht mehrmals zur Schicht hinzugefügt werden" -#: src/shiftings/shifts/forms/permission.py:36 -#: src/shiftings/shifts/forms/permission.py:90 +#: shiftings/shifts/forms/permission.py:36 +#: shiftings/shifts/forms/permission.py:90 #, python-brace-format msgid "" "Permission would have no effect. Permission for all Users is more extensive: " @@ -2963,7 +1397,7 @@ msgstr "" "Berechtigung hätte keinen Effekt. Berechtigung für alle Benutzer ist " "weitergehender: {permission}" -#: src/shiftings/shifts/forms/permission.py:47 +#: shiftings/shifts/forms/permission.py:47 #, python-brace-format msgid "" "Permission would have no effect. There is an inherited more extensive " @@ -2972,7 +1406,7 @@ msgstr "" "Berechtigung hätte keinen Effekt. Es existiert eine weitergehende " "Berechtigung: {referred_object} ({permission})" -#: src/shiftings/shifts/forms/permission.py:56 +#: shiftings/shifts/forms/permission.py:56 #, python-brace-format msgid "" "Permission would have no effect. There is an inherited more extensive " @@ -2981,166 +1415,169 @@ msgstr "" "Berechtigung hätte keinen Effekt. Es existiert eine weitergehende " "Berechtigung für alle Benutzer: {referred_object} ({permission})" -#: src/shiftings/shifts/forms/permission.py:78 +#: shiftings/shifts/forms/permission.py:78 #, python-brace-format msgid "Can only set one permission for organization \"{organization}\"" msgstr "" "Es kann nur eine Berechtigung für die Organistation \"{organization}\" " "gesetzt werden" -#: src/shiftings/shifts/forms/permission.py:80 +#: shiftings/shifts/forms/permission.py:80 msgid "Can only set one permission for all Users" msgstr "Es kann nur eine Berechtigung für alle Benutzer gesetzt werden" -#: src/shiftings/shifts/forms/recurring.py:13 +#: shiftings/shifts/forms/recurring.py:13 msgid "Ordinal" msgstr "Ordinal" -#: src/shiftings/shifts/forms/recurring.py:35 +#: shiftings/shifts/forms/recurring.py:35 msgid "Weekday can't be empty with the chosen time frame type." msgstr "Der Wochentag ist erforderlich für den angegeben Zeitraumtyp." -#: src/shiftings/shifts/forms/recurring.py:37 +#: shiftings/shifts/forms/recurring.py:37 msgid "Month can't be empty with the chosen time frame type." msgstr "Der Monat ist erforderlich für den angegebenen Zeitraumtyp." -#: src/shiftings/shifts/forms/recurring.py:39 +#: shiftings/shifts/forms/recurring.py:39 msgid "You need to add a warning for weekend handling." msgstr "Du musst eine Warnung für die Wochenendproblembehandlung hinzufügen." -#: src/shiftings/shifts/forms/recurring.py:41 +#: shiftings/shifts/forms/recurring.py:41 msgid "You need to add a warning for holiday handling." msgstr "Du musst eine Warnung für die Feiertagsproblembehandlung hinzufügen." -#: src/shiftings/shifts/forms/shift.py:38 -#: src/shiftings/shifts/forms/template.py:66 +#: shiftings/shifts/forms/shift.py:41 +msgid "End time must be after start time" +msgstr "" + +#: shiftings/shifts/forms/shift.py:46 shiftings/shifts/forms/template.py:66 #, python-brace-format msgid "Shift is too long, can at most be {max} long" msgstr "Diese Schicht ist zu lang, sie kann Maximal {max} lang sein" -#: src/shiftings/shifts/forms/template.py:18 +#: shiftings/shifts/forms/template.py:18 msgid "Date" msgstr "Datum" -#: src/shiftings/shifts/forms/template.py:18 +#: shiftings/shifts/forms/template.py:18 msgid "Date to create the template on" msgstr "Datum an dem die Vorlage erstellt wird" -#: src/shiftings/shifts/forms/template.py:38 -#: src/shiftings/shifts/models/template.py:22 +#: shiftings/shifts/forms/template.py:38 shiftings/shifts/models/template.py:22 msgid "Start Delay" msgstr "Startverzögerung" -#: src/shiftings/shifts/forms/template.py:58 +#: shiftings/shifts/forms/template.py:40 shiftings/shifts/models/template.py:23 +#: shiftings/shifts/templates/shifts/recurring/list.html:11 +msgid "Duration" +msgstr "Dauer" + +#: shiftings/shifts/forms/template.py:58 msgid "Start Delay was None please reload and use the slider." msgstr "" "Anfangsverzögerung war None bitte lade die Seite neu und benutze den Slider." -#: src/shiftings/shifts/forms/template.py:64 +#: shiftings/shifts/forms/template.py:64 msgid "Duration was None please reload and use the slider." msgstr "Dauer war None bitte lade die Seite neu und benutze den Slider." -#: src/shiftings/shifts/forms/type.py:22 +#: shiftings/shifts/forms/type.py:22 msgid "Can't name a shift type \"System\"." msgstr "Ungültiger Name \"System\" für einen Schichttyp." -#: src/shiftings/shifts/models/base.py:13 -#: src/shiftings/shifts/models/template.py:19 -#: src/shiftings/shifts/templates/shifts/shift.html:39 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:18 +#: shiftings/shifts/models/base.py:13 shiftings/shifts/models/template.py:19 +#: shiftings/shifts/templates/shifts/shift.html:39 +#: shiftings/shifts/templates/shifts/template/template_form.html:18 msgid "Shift Type" msgstr "Schicht Typ" -#: src/shiftings/shifts/models/base.py:16 +#: shiftings/shifts/models/base.py:16 msgid "Required Users" msgstr "Benötigte Benutzer" -#: src/shiftings/shifts/models/base.py:18 -#: src/shiftings/shifts/models/template.py:27 +#: shiftings/shifts/models/base.py:18 shiftings/shifts/models/template.py:27 msgid "A maximum of 32 users can be required" msgstr "Es können maximal 32 Benutzer benötigt werden" -#: src/shiftings/shifts/models/base.py:19 +#: shiftings/shifts/models/base.py:19 msgid "Maximum Users" msgstr "Maximale Benutzer" -#: src/shiftings/shifts/models/base.py:21 -#: src/shiftings/shifts/models/template.py:30 +#: shiftings/shifts/models/base.py:21 shiftings/shifts/models/template.py:30 msgid "A maximum of 64 users can be present" msgstr "Es können maximal 64 Benutzer teilnehmen" -#: src/shiftings/shifts/models/base.py:23 -#: src/shiftings/shifts/models/template.py:32 -#: src/shiftings/shifts/templates/shifts/shift.html:79 -#: src/shiftings/shifts/templates/shifts/template/template.html:26 +#: shiftings/shifts/models/base.py:23 shiftings/shifts/models/template.py:32 +#: shiftings/shifts/templates/shifts/shift.html:79 +#: shiftings/shifts/templates/shifts/template/template.html:26 msgid "Additional Infos" msgstr "Zusätzliche Informationen" -#: src/shiftings/shifts/models/participant.py:16 +#: shiftings/shifts/models/participant.py:16 msgid "Display Name is optional, and will be shown instead of the username" msgstr "" "Der Anzeigename ist optional und wird anstatt dem Benutzernamen angezeigt" -#: src/shiftings/shifts/models/participant.py:17 +#: shiftings/shifts/models/participant.py:17 msgid "Participation confirmed" msgstr "Teilnahme bestätigt" -#: src/shiftings/shifts/models/participant.py:18 +#: shiftings/shifts/models/participant.py:18 msgid "" "Whether the participant has taken part in the shift or not. Default: False" msgstr "Hat der Teilnehmer die Schicht ausgeführt. Standard: False" -#: src/shiftings/shifts/models/permission.py:18 +#: shiftings/shifts/models/permission.py:18 msgid "No Permission" msgstr "Keine Berechtigung" -#: src/shiftings/shifts/models/permission.py:19 +#: shiftings/shifts/models/permission.py:19 msgid "See Shift exists" msgstr "Schichttexistenz sehen" -#: src/shiftings/shifts/models/permission.py:20 +#: shiftings/shifts/models/permission.py:20 msgid "See Shift Details" msgstr "Schichtdetails sehen" -#: src/shiftings/shifts/models/permission.py:21 +#: shiftings/shifts/models/permission.py:21 msgid "See Shift Participants" msgstr "Schichtteilnehmer sehen" -#: src/shiftings/shifts/models/permission.py:22 +#: shiftings/shifts/models/permission.py:22 msgid "Participate" msgstr "Teilnehmen" -#: src/shiftings/shifts/models/permission.py:80 +#: shiftings/shifts/models/permission.py:80 msgid "Permission Type" msgstr "Berechtigungstyp" -#: src/shiftings/shifts/models/permission.py:82 +#: shiftings/shifts/models/permission.py:82 msgid "Affected Organization" msgstr "Betroffene Organisationen" -#: src/shiftings/shifts/models/recurring.py:26 +#: shiftings/shifts/models/recurring.py:26 msgid "ignore" msgstr "ignorieren" -#: src/shiftings/shifts/models/recurring.py:27 +#: shiftings/shifts/models/recurring.py:27 msgid "cancel" msgstr "abbrechen" -#: src/shiftings/shifts/models/recurring.py:28 +#: shiftings/shifts/models/recurring.py:28 msgid "show warning" msgstr "Warnung anzeigen" -#: src/shiftings/shifts/models/recurring.py:36 +#: shiftings/shifts/models/recurring.py:36 msgid "Timeframe Type" msgstr "Zeitraum Typ" -#: src/shiftings/shifts/models/recurring.py:41 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:10 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:53 +#: shiftings/shifts/models/recurring.py:41 +#: shiftings/shifts/templates/shifts/recurring/list.html:10 +#: shiftings/shifts/templates/shifts/recurring/shift.html:53 msgid "First Occurrence" msgstr "Erster Termin" -#: src/shiftings/shifts/models/recurring.py:42 +#: shiftings/shifts/models/recurring.py:42 msgid "" "Choose a minimum day for first occurrence and the system will automatically " "choose the next applicable day." @@ -3148,142 +1585,142 @@ msgstr "" "Wähle einen Tag für die erste Angabe und das system wird automatisch den " "nächsten zutreffenden Tag berechnen." -#: src/shiftings/shifts/models/recurring.py:46 +#: shiftings/shifts/models/recurring.py:46 msgid "Auto create days" msgstr "" -#: src/shiftings/shifts/models/recurring.py:46 +#: shiftings/shifts/models/recurring.py:46 msgid "How many days in advance should the shifts be created?" msgstr "" -#: src/shiftings/shifts/models/recurring.py:48 +#: shiftings/shifts/models/recurring.py:48 msgid "Shift Template" msgstr "Schichtvorlage" -#: src/shiftings/shifts/models/recurring.py:52 +#: shiftings/shifts/models/recurring.py:52 msgid "Weekend problem handling" msgstr "Wochenendproblembehandlung" -#: src/shiftings/shifts/models/recurring.py:54 +#: shiftings/shifts/models/recurring.py:54 msgid "Warning text for weekend" msgstr "Warnung für Wochenenden" -#: src/shiftings/shifts/models/recurring.py:59 +#: shiftings/shifts/models/recurring.py:59 msgid "Holidays problem handling" msgstr "Feiertagsproblembehandlung" -#: src/shiftings/shifts/models/recurring.py:61 +#: shiftings/shifts/models/recurring.py:61 msgid "Warning text for holidays" msgstr "Warnung für Feiertage" -#: src/shiftings/shifts/models/recurring.py:67 +#: shiftings/shifts/models/recurring.py:67 msgid "Manually Disabled" msgstr "Manuell deaktiviert" -#: src/shiftings/shifts/models/recurring.py:88 +#: shiftings/shifts/models/recurring.py:88 msgid "Nth" msgstr "Nte" -#: src/shiftings/shifts/models/recurring.py:89 +#: shiftings/shifts/models/recurring.py:89 msgid "[weekday]" msgstr "[Wochentag]" -#: src/shiftings/shifts/models/recurring.py:90 +#: shiftings/shifts/models/recurring.py:90 msgid "[month]" msgstr "[Monat]" -#: src/shiftings/shifts/models/shift.py:24 +#: shiftings/shifts/models/shift.py:24 msgid "Start Date and Time" msgstr "Startdatum und Zeit" -#: src/shiftings/shifts/models/shift.py:25 +#: shiftings/shifts/models/shift.py:25 msgid "End Date and Time" msgstr "Enddatum und Zeit" -#: src/shiftings/shifts/models/shift.py:27 -#: src/shiftings/shifts/templates/shifts/template/shift.html:13 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:21 +#: shiftings/shifts/models/shift.py:27 +#: shiftings/shifts/templates/shifts/template/shift.html:13 +#: shiftings/shifts/templates/shifts/template/template_form.html:21 msgid "Users" msgstr "Benutzer" -#: src/shiftings/shifts/models/shift.py:30 +#: shiftings/shifts/models/shift.py:30 msgid "Locked for Participation" msgstr "Teilnahme geschlossen" -#: src/shiftings/shifts/models/shift.py:31 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:58 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:64 +#: shiftings/shifts/models/shift.py:31 +#: shiftings/shifts/templates/shifts/recurring/shift.html:58 +#: shiftings/shifts/templates/shifts/recurring/shift.html:64 msgid "Warning" msgstr "Warnung" -#: src/shiftings/shifts/models/shift.py:35 +#: shiftings/shifts/models/shift.py:35 msgid "Created by Recurring Shift" msgstr "Erstellt von wiederkehrender Schicht" -#: src/shiftings/shifts/models/shift.py:37 -#: src/shiftings/shifts/templates/shifts/shift.html:53 +#: shiftings/shifts/models/shift.py:37 +#: shiftings/shifts/templates/shifts/shift.html:53 msgid "Created" msgstr "Erstellt" -#: src/shiftings/shifts/models/shift.py:38 -#: src/shiftings/shifts/templates/shifts/shift.html:55 +#: shiftings/shifts/models/shift.py:38 +#: shiftings/shifts/templates/shifts/shift.html:55 msgid "Last Modified" msgstr "Zuletzt geändert" -#: src/shiftings/shifts/models/shift.py:54 +#: shiftings/shifts/models/shift.py:54 msgid "Organization and Event Organization must be identical." msgstr "Organistion und Event Organistation müssen identisch sein." -#: src/shiftings/shifts/models/shift.py:57 +#: shiftings/shifts/models/shift.py:57 #, python-brace-format msgid "Shift {name} on {time}" msgstr "Schicht {name} am {time}" -#: src/shiftings/shifts/models/shift.py:65 +#: shiftings/shifts/models/shift.py:65 #, python-brace-format msgid "{name} from {start} to {end} " msgstr "{name} von {start} bis {end} " -#: src/shiftings/shifts/models/shift.py:72 +#: shiftings/shifts/models/shift.py:72 #, python-brace-format msgid "{start} to {end_time} on {end_date} " msgstr "{start} bis {end_time} am {end_date} " -#: src/shiftings/shifts/models/shift.py:76 +#: shiftings/shifts/models/shift.py:76 #, python-brace-format msgid "{start} to {end_time}" msgstr "{start} bis {end_time}" -#: src/shiftings/shifts/models/summary.py:11 +#: shiftings/shifts/models/summary.py:11 msgid "\"Other\" Shift Type Group Name" msgstr "\"Sonstige\" Schichttyp Gruppenname" -#: src/shiftings/shifts/models/summary.py:14 +#: shiftings/shifts/models/summary.py:14 msgid "Default time range for summary" msgstr "Standard Zeitraum für die Zusammenfassung" -#: src/shiftings/shifts/models/summary.py:25 +#: shiftings/shifts/models/summary.py:25 #, python-brace-format msgid "Organization summary settings of {organization}" msgstr "Zusammenfassungseinstellungen von {organization}" -#: src/shiftings/shifts/models/template.py:16 +#: shiftings/shifts/models/template.py:16 msgid "Template Group" msgstr "Vorlagengruppe" -#: src/shiftings/shifts/models/template.py:25 +#: shiftings/shifts/models/template.py:25 msgid "Required User" msgstr "Benötigte Benutzer" -#: src/shiftings/shifts/models/template.py:28 +#: shiftings/shifts/models/template.py:28 msgid "Maximum User" msgstr "Maximale Benutzer" -#: src/shiftings/shifts/signals.py:26 +#: shiftings/shifts/signals.py:26 msgid "Could not find a valid first occurrence." msgstr "Konnte keinen validen ersten Termin finden." -#: src/shiftings/shifts/templates/shifts/create_shift.html:11 +#: shiftings/shifts/templates/shifts/create_shift.html:11 #, python-format msgid "" "\n" @@ -3294,7 +1731,7 @@ msgstr "" "

Neue Schicht für %(organization)s erstellen

\n" " " -#: src/shiftings/shifts/templates/shifts/create_shift.html:15 +#: shiftings/shifts/templates/shifts/create_shift.html:15 #, python-format msgid "" "\n" @@ -3306,70 +1743,70 @@ msgstr "" "h4>\n" " " -#: src/shiftings/shifts/templates/shifts/create_shift.html:22 -#: src/shiftings/shifts/templates/shifts/create_shift.html:83 -#: src/shiftings/shifts/templates/shifts/recurring/form.html:23 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:19 -#: src/shiftings/templates/generic/create_or_update.html:18 -#: src/shiftings/templates/generic/form_card.html:8 +#: shiftings/shifts/templates/shifts/create_shift.html:22 +#: shiftings/shifts/templates/shifts/create_shift.html:93 +#: shiftings/shifts/templates/shifts/recurring/form.html:23 +#: shiftings/shifts/templates/shifts/recurring/shift.html:19 +#: shiftings/templates/generic/create_or_update.html:18 +#: shiftings/templates/generic/form_card.html:8 msgid "Create" msgstr "Erstellen" -#: src/shiftings/shifts/templates/shifts/create_shift.html:24 +#: shiftings/shifts/templates/shifts/create_shift.html:24 msgid "Update" msgstr "Aktualisieren" -#: src/shiftings/shifts/templates/shifts/create_shift.html:26 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:34 -#: src/shiftings/templates/generic/create_or_update.html:22 +#: shiftings/shifts/templates/shifts/create_shift.html:26 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:34 +#: shiftings/templates/generic/create_or_update.html:22 msgid "Back" msgstr "Zurück" -#: src/shiftings/shifts/templates/shifts/create_shift.html:47 +#: shiftings/shifts/templates/shifts/create_shift.html:57 msgid "Shift starting time" msgstr "Schicht Startzeit" -#: src/shiftings/shifts/templates/shifts/create_shift.html:48 +#: shiftings/shifts/templates/shifts/create_shift.html:58 msgctxt "Shift time" msgid "Start Time" msgstr "Startzeit" -#: src/shiftings/shifts/templates/shifts/create_shift.html:52 +#: shiftings/shifts/templates/shifts/create_shift.html:62 msgid "Shift end time" msgstr "Schichtende" -#: src/shiftings/shifts/templates/shifts/create_shift.html:53 +#: shiftings/shifts/templates/shifts/create_shift.html:63 msgctxt "Shift time" msgid "End Time" msgstr "Endzeit" -#: src/shiftings/shifts/templates/shifts/create_shift.html:59 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:24 +#: shiftings/shifts/templates/shifts/create_shift.html:69 +#: shiftings/shifts/templates/shifts/template/template_form.html:24 msgid "Required number of users to fill this shift" msgstr "Benötigte Anzahl an Benutzern um die Schicht zu füllen" -#: src/shiftings/shifts/templates/shifts/create_shift.html:60 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:25 +#: shiftings/shifts/templates/shifts/create_shift.html:70 +#: shiftings/shifts/templates/shifts/template/template_form.html:25 msgctxt "Shift users" msgid "Required" msgstr "Benötigt" -#: src/shiftings/shifts/templates/shifts/create_shift.html:64 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:29 +#: shiftings/shifts/templates/shifts/create_shift.html:74 +#: shiftings/shifts/templates/shifts/template/template_form.html:29 msgid "Maximum number of users allowed in this shift" msgstr "Maximale Anzahl an Benutzern in dieser Schicht" -#: src/shiftings/shifts/templates/shifts/create_shift.html:65 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:30 +#: shiftings/shifts/templates/shifts/create_shift.html:75 +#: shiftings/shifts/templates/shifts/template/template_form.html:30 msgctxt "Shift users" msgid "Maximum" msgstr "Maximum" -#: src/shiftings/shifts/templates/shifts/create_shift.html:76 +#: shiftings/shifts/templates/shifts/create_shift.html:86 msgid "Create from Template" msgstr "Aus Vorlage erstellen" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:17 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:17 #, python-format msgid "" "\n" @@ -3382,39 +1819,39 @@ msgstr "" "mit \"+\" hinzu\n" " " -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:26 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:26 msgid "Edit Shift Permissions for" msgstr "Schichtberechtigungen bearbeiten" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:38 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:38 msgid "How does it work?" msgstr "Wie funktioniert es?" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:53 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:53 msgid "Inherited Permissions" msgstr "Geerbte Berechtigungen" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:65 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:65 msgid "All Users" msgstr "Alle Benutzer" -#: src/shiftings/shifts/templates/shifts/edit_templates.html:21 +#: shiftings/shifts/templates/shifts/edit_templates.html:21 msgid "Edit Templates for" msgstr "Bearbeite Vorlagen für" -#: src/shiftings/shifts/templates/shifts/edit_templates.html:29 +#: shiftings/shifts/templates/shifts/edit_templates.html:29 msgid "Cancel" msgstr "Abbrechen" -#: src/shiftings/shifts/templates/shifts/recurring/form.html:33 +#: shiftings/shifts/templates/shifts/recurring/form.html:33 msgid "Recurring Time Frame" msgstr "Wiederkehrender Zeitraum" -#: src/shiftings/shifts/templates/shifts/recurring/form.html:42 +#: shiftings/shifts/templates/shifts/recurring/form.html:42 msgid "Shift Information" msgstr "Schichtinformation" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:16 +#: shiftings/shifts/templates/shifts/recurring/shift.html:16 #, python-format msgid "" "\n" @@ -3425,11 +1862,11 @@ msgstr "" " Möchtest du diese Schicht am %(date)s erstellen\n" " " -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:37 +#: shiftings/shifts/templates/shifts/recurring/shift.html:37 msgid "Edit Recurring Shift" msgstr "Wiederkehrende Schicht bearbeiten" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:44 +#: shiftings/shifts/templates/shifts/recurring/shift.html:44 #, python-format msgid "" "\n" @@ -3442,7 +1879,7 @@ msgstr "" "%(time)s\n" " " -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:48 +#: shiftings/shifts/templates/shifts/recurring/shift.html:48 #, fuzzy, python-format #| msgid "" #| "\n" @@ -3457,44 +1894,44 @@ msgstr "" " vor %(time)s\n" " " -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:55 +#: shiftings/shifts/templates/shifts/recurring/shift.html:55 msgid "Weekend Handling" msgstr "Wochenendvorgehen" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:61 +#: shiftings/shifts/templates/shifts/recurring/shift.html:61 msgid "Holiday Handling" msgstr "Feiertagsvorgehen" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:75 +#: shiftings/shifts/templates/shifts/recurring/shift.html:75 msgid "Upcoming recurring Shifts" msgstr "Anstehende wiederkehrende Schichten" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:86 +#: shiftings/shifts/templates/shifts/recurring/shift.html:86 msgid "No upcoming created Shifts" msgstr "Keine anstehenden erstellten Schichten" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:93 +#: shiftings/shifts/templates/shifts/recurring/shift.html:93 msgid "Passed recurring Shifts" msgstr "Vergangenen Wiederkehrende Schichten" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:104 +#: shiftings/shifts/templates/shifts/recurring/shift.html:104 msgid "No created Shifts have passed yet" msgstr "Keine erstellten Schichten sind abgelaufen" -#: src/shiftings/shifts/templates/shifts/shift.html:16 -#: src/shiftings/shifts/views/shift.py:78 +#: shiftings/shifts/templates/shifts/shift.html:16 +#: shiftings/shifts/views/shift.py:78 msgid "Edit Shift" msgstr "Schicht bearbeiten" -#: src/shiftings/shifts/templates/shifts/shift.html:22 +#: shiftings/shifts/templates/shifts/shift.html:22 msgid "Delete Shift" msgstr "Schicht löschen" -#: src/shiftings/shifts/templates/shifts/shift.html:28 +#: shiftings/shifts/templates/shifts/shift.html:28 msgid "Edit Permissions" msgstr "Berechtigungen bearbeiten" -#: src/shiftings/shifts/templates/shifts/shift.html:57 +#: shiftings/shifts/templates/shifts/shift.html:57 #, python-format msgid "" "\n" @@ -3505,219 +1942,219 @@ msgstr "" " vor %(time)s\n" " " -#: src/shiftings/shifts/templates/shifts/shift.html:66 -#: src/shiftings/shifts/templates/shifts/template/shift_card.html:13 +#: shiftings/shifts/templates/shifts/shift.html:66 +#: shiftings/shifts/templates/shifts/template/shift_card.html:13 msgid "Shift Participants" msgstr "Schichtteilnehmer" -#: src/shiftings/shifts/templates/shifts/shift.html:71 +#: shiftings/shifts/templates/shifts/shift.html:71 msgid "Add participant" msgstr "Teilnehmer hinzufügen" -#: src/shiftings/shifts/templates/shifts/shift.html:84 +#: shiftings/shifts/templates/shifts/shift.html:84 msgid "Nothing to consider." msgstr "Nichts zu beachten." -#: src/shiftings/shifts/templates/shifts/shift_participants.html:11 +#: shiftings/shifts/templates/shifts/shift_participants.html:11 msgid "" "This shift is over, do you really want/need to add yourself as participant?" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift_url_filters.html:8 +#: shiftings/shifts/templates/shifts/shift_url_filters.html:8 msgid "Toggle Shift Filters" msgstr "Schichtfilter umschalten" -#: src/shiftings/shifts/templates/shifts/summary/summary.html:6 +#: shiftings/shifts/templates/shifts/summary/summary.html:6 msgid "Shift Summary" msgstr "Mitglieder Schichten Zusammenfassung" -#: src/shiftings/shifts/templates/shifts/template/group.html:17 +#: shiftings/shifts/templates/shifts/template/group.html:17 msgid "Delete Template" msgstr "Vorlage löschen" -#: src/shiftings/shifts/templates/shifts/template/group.html:21 +#: shiftings/shifts/templates/shifts/template/group.html:21 msgid "Edit Template" msgstr "Vorlage bearbeiten" -#: src/shiftings/shifts/templates/shifts/template/group.html:25 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:13 +#: shiftings/shifts/templates/shifts/template/group.html:25 +#: shiftings/shifts/templates/shifts/template/group_template.html:13 msgid "Edit Shifts" msgstr "Schichten bearbeiten" -#: src/shiftings/shifts/templates/shifts/template/group.html:31 +#: shiftings/shifts/templates/shifts/template/group.html:31 msgid "Edit Template Permissions" msgstr "Vorlagenberechtigungen bearbeiten" -#: src/shiftings/shifts/templates/shifts/template/group.html:70 +#: shiftings/shifts/templates/shifts/template/group.html:70 msgid "Create Shift Templates" msgstr "Schichtvorlagen erstellen" -#: src/shiftings/shifts/templates/shifts/template/group_template.html:9 +#: shiftings/shifts/templates/shifts/template/group_template.html:9 msgid "Edit Shifts Template" msgstr "Schichtvorlage bearbeiten" -#: src/shiftings/shifts/templates/shifts/template/member_shift_summary.html:3 +#: shiftings/shifts/templates/shifts/template/member_shift_summary.html:3 msgid "Member" msgstr "Mitglied" -#: src/shiftings/shifts/templates/shifts/template/member_shift_summary.html:10 +#: shiftings/shifts/templates/shifts/template/member_shift_summary.html:10 msgid "Total" msgstr "Summe" -#: src/shiftings/shifts/templates/shifts/template/participation_permission_form.html:9 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:10 +#: shiftings/shifts/templates/shifts/template/participation_permission_form.html:9 +#: shiftings/shifts/templates/shifts/template/template_form.html:10 msgid "Remove Shift" msgstr "Schicht löschen" -#: src/shiftings/shifts/templates/shifts/template/participation_permission_form.html:12 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:13 +#: shiftings/shifts/templates/shifts/template/participation_permission_form.html:12 +#: shiftings/shifts/templates/shifts/template/template_form.html:13 msgid "Restore Shift" msgstr "Schicht wiederherstellen" -#: src/shiftings/shifts/templates/shifts/template/select_org.html:3 +#: shiftings/shifts/templates/shifts/template/select_org.html:3 msgid "Select Organization to create Shift for" msgstr "Organisation auswählen für die eine Schicht erstellt werden soll" -#: src/shiftings/shifts/templates/shifts/template/shift.html:5 +#: shiftings/shifts/templates/shifts/template/shift.html:5 msgid "Go to Shift" msgstr "Zur Schicht springen" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:5 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:5 msgid "Filters" msgstr "Filter" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:8 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:8 msgid "Reset Filter" msgstr "Filter zurürcksetzen" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:12 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:12 msgid "Apply Filter" msgstr "Filter anwenden" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:15 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:15 msgid "Parameters" msgstr "Parameter" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:29 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:29 msgid "Select Organizations" msgstr "Wähle Organisationen" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:47 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:47 msgid "Shift starts after" msgstr "Schicht startet nach" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:52 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:52 msgid "Shift ends before" msgstr "Schicht endet vor" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:65 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:65 msgid "Select Events" msgstr "Events auswählen" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:4 +#: shiftings/shifts/templates/shifts/template/slots_display.html:4 msgid "Required to carry out this Shift" msgstr "Benötigte um die Schicht zu erledigen" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:6 +#: shiftings/shifts/templates/shifts/template/slots_display.html:6 msgid "Open Shift Slot" msgstr "Offene Postiion" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:32 +#: shiftings/shifts/templates/shifts/template/slots_display.html:32 msgid "Free" msgstr "Frei" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:20 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:20 msgid "Required" msgstr "Benötigt" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:23 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:23 msgid "Confirmed" msgstr "Bestätigt" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:29 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:29 msgid "Participating" msgstr "Teilnehmer" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:34 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:34 msgid "Full" msgstr "Voll" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:40 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:40 msgid "Additional Users required" msgstr "Zusätzliche Mitglieder benötigt" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:42 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:42 msgid "Additional Slots available" msgstr "Zusätzliche Plätze verfügbar" -#: src/shiftings/shifts/templates/shifts/template/template.html:11 +#: shiftings/shifts/templates/shifts/template/template.html:11 msgid "Calendar Background Color" msgstr "Kalender Hintergrundfarbe" -#: src/shiftings/shifts/templates/shifts/template/template.html:17 +#: shiftings/shifts/templates/shifts/template/template.html:17 msgid "Required Users/Max Users" msgstr "Benötigte Benutzer/Maximale Benutzer" -#: src/shiftings/shifts/templates/shifts/template/template.html:21 +#: shiftings/shifts/templates/shifts/template/template.html:21 msgid "Unlimited" msgstr "Unbegrenzt" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:36 +#: shiftings/shifts/templates/shifts/template/template_form.html:36 msgid "Delay to starting time" msgstr "Verzögerung zur Startzeit" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:37 +#: shiftings/shifts/templates/shifts/template/template_form.html:37 msgctxt "Shift time" msgid "Start Delay" msgstr "Startverzögerung" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:41 +#: shiftings/shifts/templates/shifts/template/template_form.html:41 msgid "Shift Duration" msgstr "Dauer der Schicht" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:42 +#: shiftings/shifts/templates/shifts/template/template_form.html:42 msgctxt "Shift time" msgid "Duration" msgstr "Dauer" -#: src/shiftings/shifts/templates/shifts/type_group/list.html:8 +#: shiftings/shifts/templates/shifts/type_group/list.html:8 msgid "Actions" msgstr "Aktionen" -#: src/shiftings/shifts/templatetags/shifts.py:90 +#: shiftings/shifts/templatetags/shifts.py:90 #, python-brace-format msgid "Days + {delta_days}" msgstr "Tage + {delta_days}" -#: src/shiftings/shifts/utils/time_frame.py:17 +#: shiftings/shifts/utils/time_frame.py:17 msgid "Nth [weekday] of each month" msgstr "Nte [Wochentag] jeden Monat" -#: src/shiftings/shifts/utils/time_frame.py:18 +#: shiftings/shifts/utils/time_frame.py:18 msgid "Nth day of each month" msgstr "Nte Tag jeden Monat" -#: src/shiftings/shifts/utils/time_frame.py:19 +#: shiftings/shifts/utils/time_frame.py:19 msgid "every Nth [weekday]" msgstr "Jeden Nte [Wochentag]" -#: src/shiftings/shifts/utils/time_frame.py:20 +#: shiftings/shifts/utils/time_frame.py:20 msgid "Nth workday of each month" msgstr "Jeden Nte Werktag jedes Monats" -#: src/shiftings/shifts/utils/time_frame.py:21 +#: shiftings/shifts/utils/time_frame.py:21 msgid "Nth day of [month]" msgstr "Nte Tag des [Monats]" -#: src/shiftings/shifts/utils/time_frame.py:22 +#: shiftings/shifts/utils/time_frame.py:22 msgid "Nth workday of [month]" msgstr "Nte Werktag des [Monats]" -#: src/shiftings/shifts/views/participant.py:42 +#: shiftings/shifts/views/participant.py:42 msgid "This shift is over, do you really want/need to add a participant?" msgstr "" -#: src/shiftings/shifts/views/permission.py:50 +#: shiftings/shifts/views/permission.py:50 #, python-brace-format msgid "" "

Any Permission present will be applied to all Shifts belonging to " @@ -3732,35 +2169,36 @@ msgstr "" "weitergehenden Berechtigungen. Berechtigungen können für \"Alle Benutzer " "oder spezielle Organisationen festgelegt werden.\"

" -#: src/shiftings/shifts/views/recurring.py:85 +#: shiftings/shifts/views/recurring.py:85 msgid "Forbidden Method GET" msgstr "Verbotene Methode GET" -#: src/shiftings/shifts/views/recurring.py:98 +#: shiftings/shifts/views/recurring.py:98 +#, python-brace-format msgid "Created Shifts on {}" msgstr "Schicht am {} erstellt" -#: src/shiftings/shifts/views/recurring.py:102 +#: shiftings/shifts/views/recurring.py:102 msgid "Error while creating shifts. Invalid Date" msgstr "Fehler beim erstellen der Schichten. Ungültiges Datum" -#: src/shiftings/shifts/views/shift.py:164 +#: shiftings/shifts/views/shift.py:164 msgid "Unable to delete past shifts." msgstr "Konnte vergangene Schichten nicht löschen." -#: src/shiftings/templates/403.html:6 +#: shiftings/templates/403.html:6 msgid "You don't have the permission to do that!" msgstr "Du besitzt nicht die benötigten Berechtigungen!" -#: src/shiftings/templates/404.html:6 +#: shiftings/templates/404.html:6 msgid "The site you requested was not found." msgstr "Die gesuchte Seite wurde nicht gefunden." -#: src/shiftings/templates/500.html:6 +#: shiftings/templates/500.html:6 msgid "A server error occured the admins have been notified" msgstr "Ein Fehler ist aufgereten die Admins wurden benachrichtigt" -#: src/shiftings/templates/generic/create_or_update.html:5 +#: shiftings/templates/generic/create_or_update.html:5 #, python-format msgid "" "\n" @@ -3771,7 +2209,7 @@ msgstr "" "

Erstelle %(object)s

\n" " " -#: src/shiftings/templates/generic/create_or_update.html:9 +#: shiftings/templates/generic/create_or_update.html:9 #, python-format msgid "" "\n" @@ -3782,7 +2220,7 @@ msgstr "" "

Aktualisiere %(object_name)s

\n" " " -#: src/shiftings/templates/generic/delete.html:6 +#: shiftings/templates/generic/delete.html:6 #, python-format msgid "" "\n" @@ -3794,53 +2232,66 @@ msgstr "" "p>\n" " " -#: src/shiftings/templates/generic/modular_search.html:4 +#: shiftings/templates/generic/delete.html:9 +msgid "Yes" +msgstr "Ja" + +#: shiftings/templates/generic/delete.html:11 +msgid "No" +msgstr "Nein" + +#: shiftings/templates/generic/modular_search.html:4 msgid "Filter by Name" msgstr "Nach Namen filtern" -#: src/shiftings/templates/generic/modular_search.html:6 +#: shiftings/templates/generic/modular_search.html:6 msgid "submit search" msgstr "Suche starten" -#: src/shiftings/templates/language.html:16 +#: shiftings/templates/language.html:16 msgid "Language" msgstr "Sprache" -#: src/shiftings/templates/language.html:21 +#: shiftings/templates/language.html:21 msgid "German" msgstr "Deutsch" -#: src/shiftings/templates/language.html:24 +#: shiftings/templates/language.html:24 msgid "English" msgstr "Englisch" -#: src/shiftings/templates/month_view.html:2 +#: shiftings/templates/local/gdpr_template.sample.html:29 +#: shiftings/templates/template/remove_modal.html:39 +#: shiftings/templates/template/simple_display_modal.html:15 +#: shiftings/templates/template/simple_form_modal.html:22 +msgid "Close" +msgstr "Schließen" + +#: shiftings/templates/month_view.html:2 msgid "M" msgstr "M" -#: src/shiftings/templates/month_view.html:3 -#: src/shiftings/templates/month_view.html:5 +#: shiftings/templates/month_view.html:3 shiftings/templates/month_view.html:5 msgid "T" msgstr "T" -#: src/shiftings/templates/month_view.html:4 +#: shiftings/templates/month_view.html:4 msgid "W" msgstr "W" -#: src/shiftings/templates/month_view.html:6 +#: shiftings/templates/month_view.html:6 msgid "F" msgstr "F" -#: src/shiftings/templates/month_view.html:7 -#: src/shiftings/templates/month_view.html:8 +#: shiftings/templates/month_view.html:7 shiftings/templates/month_view.html:8 msgid "S" msgstr "S" -#: src/shiftings/templates/switch_user.html:5 +#: shiftings/templates/switch_user.html:5 msgid "Switch User" msgstr "Benutzer wechseln" -#: src/shiftings/templates/template/remove_modal.html:12 +#: shiftings/templates/template/remove_modal.html:12 #, python-format msgid "" "\n" @@ -3853,7 +2304,7 @@ msgstr "" "bold\"> von %(target)s\n" " " -#: src/shiftings/templates/template/remove_modal.html:16 +#: shiftings/templates/template/remove_modal.html:16 msgid "" "\n" " Removing \n" " " -#: src/shiftings/templates/template/remove_modal.html:26 +#: shiftings/templates/template/remove_modal.html:26 #, python-format msgid "" "\n" @@ -3880,7 +2331,7 @@ msgstr "" "text-danger\">\" von %(target)s\n" " " -#: src/shiftings/templates/template/remove_modal.html:31 +#: shiftings/templates/template/remove_modal.html:31 msgid "" "\n" " Are you sure you want to remove\n" @@ -3894,101 +2345,404 @@ msgstr "" "text-danger\"> löschen möchtest\"\n" " " -#: src/shiftings/templates/template/remove_modal.html:38 +#: shiftings/templates/template/remove_modal.html:38 msgid "Remove" msgstr "Entfernen" -#: src/shiftings/templates/template/remove_modal.html:39 -#: src/shiftings/templates/template/simple_display_modal.html:15 -#: src/shiftings/templates/template/simple_form_modal.html:22 -msgid "Close" -msgstr "Schließen" +#: shiftings/templates/theme.html:23 +msgid "Theme" +msgstr "" -#: src/shiftings/templates/top_nav.html:12 +#: shiftings/templates/top_nav.html:12 msgid "Overview" msgstr "Übersicht" -#: src/shiftings/templates/top_nav.html:32 -#: src/shiftings/templates/top_nav.html:33 +#: shiftings/templates/top_nav.html:32 shiftings/templates/top_nav.html:33 msgid "Help" msgstr "Hilfe" -#: src/shiftings/templates/top_nav.html:48 +#: shiftings/templates/top_nav.html:49 msgid "Profile" msgstr "Profil" -#: src/shiftings/templates/widgets/time_slider.html:9 +#: shiftings/templates/widgets/time_slider.html:9 msgid "You need to enter a valid timeframe in the form of HH:MM." msgstr "Du muss einen validen Zeitraum inder Form HH:MM eingeben." -#: src/shiftings/templates/widgets/time_slider.html:13 +#: shiftings/templates/widgets/time_slider.html:13 msgid "Day: +" msgstr "Tag: +" -#: src/shiftings/utils/string.py:5 +#: shiftings/utils/string.py:5 #, python-format msgid "The function %(name)s has to be implemented." msgstr "Die Funktion %(name)s muss implementiert werden." -#: src/shiftings/utils/time/month.py:32 +#: shiftings/utils/time/month.py:10 +msgid "January" +msgstr "Januar" + +#: shiftings/utils/time/month.py:11 +msgid "February" +msgstr "Februar" + +#: shiftings/utils/time/month.py:12 +msgid "March" +msgstr "März" + +#: shiftings/utils/time/month.py:13 +msgid "April" +msgstr "April" + +#: shiftings/utils/time/month.py:14 +msgid "May" +msgstr "Mai" + +#: shiftings/utils/time/month.py:15 +msgid "June" +msgstr "Juni" + +#: shiftings/utils/time/month.py:16 +msgid "July" +msgstr "Juli" + +#: shiftings/utils/time/month.py:17 +msgid "August" +msgstr "August" + +#: shiftings/utils/time/month.py:18 +msgid "September" +msgstr "September" + +#: shiftings/utils/time/month.py:19 +msgid "October" +msgstr "Oktober" + +#: shiftings/utils/time/month.py:20 +msgid "November" +msgstr "November" + +#: shiftings/utils/time/month.py:21 +msgid "December" +msgstr "Dezember" + +#: shiftings/utils/time/month.py:32 msgid "Day of the Month" msgstr "Tag des Monats" -#: src/shiftings/utils/time/timerange.py:13 +#: shiftings/utils/time/timerange.py:13 msgid "Quarter" msgstr "Quartal" -#: src/shiftings/utils/time/timerange.py:14 +#: shiftings/utils/time/timerange.py:14 msgid "Half Year" msgstr "Halbjahr" -#: src/shiftings/utils/time/timerange.py:15 +#: shiftings/utils/time/timerange.py:15 msgid "Year" msgstr "Jahr" -#: src/shiftings/utils/time/timerange.py:16 +#: shiftings/utils/time/timerange.py:16 msgid "Decade" msgstr "Dekade" -#: src/shiftings/utils/time/timerange.py:17 +#: shiftings/utils/time/timerange.py:17 msgid "Century" msgstr "Jahrhundert" -#: src/shiftings/utils/time/timerange.py:18 +#: shiftings/utils/time/timerange.py:18 msgid "Millennium" msgstr "Millennium" -#: src/shiftings/utils/time/week.py:27 +#: shiftings/utils/time/week.py:13 +msgid "Monday" +msgstr "Montag" + +#: shiftings/utils/time/week.py:14 +msgid "Tuesday" +msgstr "Dienstag" + +#: shiftings/utils/time/week.py:15 +msgid "Wednesday" +msgstr "Mittwoch" + +#: shiftings/utils/time/week.py:16 +msgid "Thursday" +msgstr "Donnerstag" + +#: shiftings/utils/time/week.py:17 +msgid "Friday" +msgstr "Freitag" + +#: shiftings/utils/time/week.py:18 +msgid "Saturday" +msgstr "Samstag" + +#: shiftings/utils/time/week.py:19 +msgid "Sunday" +msgstr "Sonntag" + +#: shiftings/utils/time/week.py:27 msgid "Day of the Week" msgstr "Wochentag" -#: src/shiftings/utils/typing.py:20 +#: shiftings/utils/typing.py:20 msgid "Value is not supposed to be None." msgstr "Dieser Wert sollte nicht None sein." -#: src/shiftings/utils/views/base.py:62 +#: shiftings/utils/views/base.py:62 msgid "The pk is missing from the url. This is not supposed to be possible." msgstr "Der PK fehlt in der url. Das sollte nicht passieren." -#: src/shiftings/utils/views/base.py:65 +#: shiftings/utils/views/base.py:65 #, python-format msgid "There is no %(name)s with that pk." msgstr "Es existiert kein %(name)s mit diesem pk." -#: src/shiftings/utils/views/base.py:73 +#: shiftings/utils/views/base.py:73 msgid "You don't have the required permission." msgstr "Du besitzt nicht die benötigten Berechtigungen." -#: src/shiftings/utils/views/base.py:90 +#: shiftings/utils/views/base.py:90 msgid "No URL to redirect to. Provide a success_url." msgstr "Keine URL zum verweisen. Bitte gib eine success_url an." -#: src/shiftings/utils/views/base.py:108 +#: shiftings/utils/views/base.py:108 msgid "Could not find the requested page. This might be a configuration error." msgstr "" "Konnte die angefragte Seite nicht finden. Dies könnte ein Einstellungsfehler " "sein." +#~ msgid "Include Protocol i.E. https://example.com" +#~ msgstr "Inklusive Protokoll z.B. https://example.com" + +#, fuzzy +#~| msgid "Abort" +#~ msgid "Aborted!" +#~ msgstr "Abbrechen" + +#, fuzzy +#~| msgid "Actions" +#~ msgid "Options" +#~ msgstr "Aktionen" + +#, fuzzy +#~| msgid "Required" +#~ msgid "required" +#~ msgstr "Benötigt" + +#, fuzzy +#~| msgid "Unknown User" +#~ msgid "unknown error" +#~ msgstr "Unbekannter Benutzer" + +#, fuzzy +#~| msgid "Profile" +#~ msgid "file" +#~ msgstr "Profil" + +#, fuzzy +#~| msgid "Organization" +#~ msgid "Syndication" +#~ msgstr "Organisation" + +#, fuzzy +#~| msgid "Enter Name" +#~ msgid "Enter a number." +#~ msgstr "Name einegeben" + +#, fuzzy +#~| msgid "End" +#~ msgid "and" +#~ msgstr "Ende" + +#, fuzzy +#~| msgid "December" +#~ msgid "Decimal number" +#~ msgstr "Dezember" + +#, fuzzy +#~| msgid "Subject" +#~ msgid "A JSON object" +#~ msgstr "Betreff" + +#, fuzzy +#~| msgid "One of the user fields is required!" +#~ msgid "This field is required." +#~ msgstr "Eines der Benutzerfelder ist benötigt!" + +#, fuzzy +#~| msgid "Enter Name" +#~ msgid "Enter a valid time." +#~ msgstr "Name einegeben" + +#, fuzzy +#~| msgid "Enter Name or Organization" +#~ msgid "Enter a valid duration." +#~ msgstr "Name oder Organisation angeben" + +#, fuzzy +#~| msgid "Delete Shift" +#~ msgid "Delete" +#~ msgstr "Schicht löschen" + +#, fuzzy +#~| msgid "Current Events" +#~ msgid "Currently" +#~ msgstr "Aktuelle Events" + +#, fuzzy +#~| msgid "M" +#~ msgid "PM" +#~ msgstr "M" + +#, fuzzy +#~| msgid "M" +#~ msgid "AM" +#~ msgstr "M" + +#, fuzzy +#~| msgid "Month" +#~ msgid "Mon" +#~ msgstr "Monat" + +#, fuzzy +#~| msgid "Tuesday" +#~ msgid "Tue" +#~ msgstr "Dienstag" + +#, fuzzy +#~| msgid "Friday" +#~ msgid "Fri" +#~ msgstr "Freitag" + +#, fuzzy +#~| msgid "Start" +#~ msgid "Sat" +#~ msgstr "Start" + +#, fuzzy +#~| msgid "Sunday" +#~ msgid "Sun" +#~ msgstr "Sonntag" + +#, fuzzy +#~| msgid "May" +#~ msgid "may" +#~ msgstr "Mai" + +#, fuzzy +#~| msgid "March" +#~ msgctxt "abbrev. month" +#~ msgid "March" +#~ msgstr "März" + +#, fuzzy +#~| msgid "April" +#~ msgctxt "abbrev. month" +#~ msgid "April" +#~ msgstr "April" + +#, fuzzy +#~| msgid "May" +#~ msgctxt "abbrev. month" +#~ msgid "May" +#~ msgstr "Mai" + +#, fuzzy +#~| msgid "June" +#~ msgctxt "abbrev. month" +#~ msgid "June" +#~ msgstr "Juni" + +#, fuzzy +#~| msgid "July" +#~ msgctxt "abbrev. month" +#~ msgid "July" +#~ msgstr "Juli" + +#, fuzzy +#~| msgid "January" +#~ msgctxt "alt. month" +#~ msgid "January" +#~ msgstr "Januar" + +#, fuzzy +#~| msgid "February" +#~ msgctxt "alt. month" +#~ msgid "February" +#~ msgstr "Februar" + +#, fuzzy +#~| msgid "March" +#~ msgctxt "alt. month" +#~ msgid "March" +#~ msgstr "März" + +#, fuzzy +#~| msgid "April" +#~ msgctxt "alt. month" +#~ msgid "April" +#~ msgstr "April" + +#, fuzzy +#~| msgid "May" +#~ msgctxt "alt. month" +#~ msgid "May" +#~ msgstr "Mai" + +#, fuzzy +#~| msgid "June" +#~ msgctxt "alt. month" +#~ msgid "June" +#~ msgstr "Juni" + +#, fuzzy +#~| msgid "July" +#~ msgctxt "alt. month" +#~ msgid "July" +#~ msgstr "Juli" + +#, fuzzy +#~| msgid "August" +#~ msgctxt "alt. month" +#~ msgid "August" +#~ msgstr "August" + +#, fuzzy +#~| msgid "September" +#~ msgctxt "alt. month" +#~ msgid "September" +#~ msgstr "September" + +#, fuzzy +#~| msgid "October" +#~ msgctxt "alt. month" +#~ msgid "October" +#~ msgstr "Oktober" + +#, fuzzy +#~| msgid "November" +#~ msgctxt "alt. month" +#~ msgid "November" +#~ msgstr "November" + +#, fuzzy +#~| msgid "December" +#~ msgctxt "alt. month" +#~ msgid "December" +#~ msgstr "Dezember" + +#, fuzzy +#~| msgid "Forbidden Method GET" +#~ msgid "Forbidden" +#~ msgstr "Verbotene Methode GET" + +#, fuzzy +#~| msgid "Close" +#~ msgid "close" +#~ msgstr "Schließen" + #, python-format #~ msgid "" #~ "\n" diff --git a/src/locale/en/LC_MESSAGES/django.po b/src/locale/en/LC_MESSAGES/django.po index 335980f..8a4f643 100644 --- a/src/locale/en/LC_MESSAGES/django.po +++ b/src/locale/en/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-09-25 15:12+0000\n" +"POT-Creation-Date: 2026-04-17 05:19+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -18,1603 +18,96 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" -#: venv/lib/python3.11/site-packages/click/_termui_impl.py:518 -#, python-brace-format -msgid "{editor}: Editing failed" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/_termui_impl.py:522 -#, python-brace-format -msgid "{editor}: Editing failed: {e}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1120 -msgid "Aborted!" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1309 -#: venv/lib/python3.11/site-packages/click/decorators.py:559 -msgid "Show this message and exit." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1340 -#: venv/lib/python3.11/site-packages/click/core.py:1370 -#, python-brace-format -msgid "(Deprecated) {text}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1387 -msgid "Options" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1413 -#, python-brace-format -msgid "Got unexpected extra argument ({args})" -msgid_plural "Got unexpected extra arguments ({args})" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/core.py:1429 -msgid "DeprecationWarning: The command {name!r} is deprecated." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1636 -msgid "Commands" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1668 -msgid "Missing command." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:1746 -msgid "No such command {name!r}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2310 -msgid "Value must be an iterable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2331 -#, python-brace-format -msgid "Takes {nargs} values but 1 was given." -msgid_plural "Takes {nargs} values but {len} were given." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/core.py:2778 -#, python-brace-format -msgid "env var: {var}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2808 -msgid "(dynamic)" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2821 -#, python-brace-format -msgid "default: {default}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/core.py:2834 -msgid "required" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/decorators.py:465 -#, python-format -msgid "%(prog)s, version %(version)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/decorators.py:528 -msgid "Show the version and exit." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:44 -#: venv/lib/python3.11/site-packages/click/exceptions.py:80 -#, python-brace-format -msgid "Error: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:72 -#, python-brace-format -msgid "Try '{command} {option}' for help." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:121 -#, python-brace-format -msgid "Invalid value: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:123 -#, python-brace-format -msgid "Invalid value for {param_hint}: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:179 -msgid "Missing argument" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:181 -msgid "Missing option" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:183 -msgid "Missing parameter" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:185 -#, python-brace-format -msgid "Missing {param_type}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:192 -#, python-brace-format -msgid "Missing parameter: {param_name}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:212 -#, python-brace-format -msgid "No such option: {name}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:224 -#, python-brace-format -msgid "Did you mean {possibility}?" -msgid_plural "(Possible options: {possibilities})" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:262 -msgid "unknown error" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/exceptions.py:269 -msgid "Could not open file {filename!r}: {message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:231 -msgid "Argument {name!r} takes {nargs} values." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:413 -msgid "Option {name!r} does not take a value." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/parser.py:474 -msgid "Option {name!r} requires an argument." -msgid_plural "Option {name!r} requires {nargs} arguments." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/shell_completion.py:319 -msgid "Shell completion is not supported for Bash versions older than 4.4." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/shell_completion.py:326 -msgid "Couldn't detect Bash version, shell completion is not supported." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:158 -msgid "Repeat for confirmation" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:174 -msgid "Error: The value you entered was invalid." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:176 -#, python-brace-format -msgid "Error: {e.message}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:187 -msgid "Error: The two entered values do not match." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:243 -msgid "Error: invalid input" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/termui.py:773 -msgid "Press any key to continue..." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:266 -#, python-brace-format -msgid "" -"Choose from:\n" -"\t{choices}" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:298 -msgid "{value!r} is not {choice}." -msgid_plural "{value!r} is not one of {choices}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/types.py:392 -msgid "{value!r} does not match the format {format}." -msgid_plural "{value!r} does not match the formats {formats}." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/click/types.py:414 -msgid "{value!r} is not a valid {number_type}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:470 -#, python-brace-format -msgid "{value} is not in the range {range}." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:611 -msgid "{value!r} is not a valid boolean." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:635 -msgid "{value!r} is not a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:822 -msgid "file" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:824 -msgid "directory" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:826 -msgid "path" -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:877 -msgid "{name} {filename!r} does not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:886 -msgid "{name} {filename!r} is a file." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:894 -#, python-brace-format -msgid "{name} '{filename}' is a directory." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:903 -msgid "{name} {filename!r} is not readable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:912 -msgid "{name} {filename!r} is not writable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:921 -msgid "{name} {filename!r} is not executable." -msgstr "" - -#: venv/lib/python3.11/site-packages/click/types.py:988 -#, python-brace-format -msgid "{len_type} values are required, but {len_value} was given." -msgid_plural "{len_type} values are required, but {len_value} were given." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/colorfield/validators.py:9 -msgid "Enter a valid hex color, eg. #000000" -msgstr "" - -#: venv/lib/python3.11/site-packages/colorfield/validators.py:17 -msgid "Enter a valid hexa color, eg. #00000000" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/messages/apps.py:7 -msgid "Messages" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/sitemaps/apps.py:8 -msgid "Site Maps" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/staticfiles/apps.py:9 -msgid "Static Files" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/contrib/syndication/apps.py:7 -msgid "Syndication" -msgstr "" - -#. Translators: String used to replace omitted page numbers in elided page -#. range generated by paginators, e.g. [1, 2, '…', 5, 6, 7, '…', 9, 10]. -#: venv/lib/python3.11/site-packages/django/core/paginator.py:30 -msgid "…" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:50 -msgid "That page number is not an integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:52 -msgid "That page number is less than 1" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/paginator.py:57 -msgid "That page contains no results" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:23 -msgid "Enter a valid value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:105 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:743 -msgid "Enter a valid URL." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:166 -msgid "Enter a valid integer." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:177 -msgid "Enter a valid email address." -msgstr "" - -#. Translators: "letters" means latin letters: a-z and A-Z. -#: venv/lib/python3.11/site-packages/django/core/validators.py:285 -msgid "" -"Enter a valid “slug” consisting of letters, numbers, underscores or hyphens." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:293 -msgid "" -"Enter a valid “slug” consisting of Unicode letters, numbers, underscores, or " -"hyphens." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:305 -#: venv/lib/python3.11/site-packages/django/core/validators.py:313 -#: venv/lib/python3.11/site-packages/django/core/validators.py:342 -msgid "Enter a valid IPv4 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:322 -#: venv/lib/python3.11/site-packages/django/core/validators.py:343 -msgid "Enter a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:334 -#: venv/lib/python3.11/site-packages/django/core/validators.py:341 -msgid "Enter a valid IPv4 or IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:377 -msgid "Enter only digits separated by commas." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:383 -#, python-format -msgid "Ensure this value is %(limit_value)s (it is %(show_value)s)." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:418 -#, python-format -msgid "Ensure this value is less than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:427 -#, python-format -msgid "Ensure this value is greater than or equal to %(limit_value)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:437 -#, python-format -msgid "" -"Ensure this value has at least %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at least %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:455 -#, python-format -msgid "" -"Ensure this value has at most %(limit_value)d character (it has " -"%(show_value)d)." -msgid_plural "" -"Ensure this value has at most %(limit_value)d characters (it has " -"%(show_value)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:478 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:343 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:378 -msgid "Enter a number." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:480 -#, python-format -msgid "Ensure that there are no more than %(max)s digit in total." -msgid_plural "Ensure that there are no more than %(max)s digits in total." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:485 -#, python-format -msgid "Ensure that there are no more than %(max)s decimal place." -msgid_plural "Ensure that there are no more than %(max)s decimal places." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:490 -#, python-format -msgid "" -"Ensure that there are no more than %(max)s digit before the decimal point." -msgid_plural "" -"Ensure that there are no more than %(max)s digits before the decimal point." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:559 -#, python-format -msgid "" -"File extension “%(extension)s” is not allowed. Allowed extensions are: " -"%(allowed_extensions)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/core/validators.py:620 -msgid "Null characters are not allowed." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/base.py:1364 -#: venv/lib/python3.11/site-packages/django/forms/models.py:894 -msgid "and" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/base.py:1366 -#, python-format -msgid "%(model_name)s with this %(field_labels)s already exists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:129 -#, python-format -msgid "Value %(value)r is not a valid choice." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:130 -msgid "This field cannot be null." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:131 -msgid "This field cannot be blank." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:132 -#, python-format -msgid "%(model_name)s with this %(field_label)s already exists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:136 -#, python-format -msgid "" -"%(field_label)s must be unique for %(date_field_label)s %(lookup_type)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:156 -#, python-format -msgid "Field of type: %(field_type)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1050 -#, python-format -msgid "“%(value)s” value must be either True or False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1051 -#, python-format -msgid "“%(value)s” value must be either True, False, or None." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1053 -msgid "Boolean (Either True or False)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1094 -#, python-format -msgid "String (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1192 -msgid "Comma-separated integers" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1293 -#, python-format -msgid "" -"“%(value)s” value has an invalid date format. It must be in YYYY-MM-DD " -"format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1297 -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1432 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD) but it is an invalid " -"date." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1301 -msgid "Date (without time)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1428 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in YYYY-MM-DD HH:MM[:ss[." -"uuuuuu]][TZ] format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1436 -#, python-format -msgid "" -"“%(value)s” value has the correct format (YYYY-MM-DD HH:MM[:ss[.uuuuuu]]" -"[TZ]) but it is an invalid date/time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1441 -msgid "Date (with time)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1565 -#, python-format -msgid "“%(value)s” value must be a decimal number." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1567 -msgid "Decimal number" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1724 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in [DD] [[HH:]MM:]ss[." -"uuuuuu] format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1728 -#: src/shiftings/shifts/forms/template.py:40 -#: src/shiftings/shifts/models/template.py:23 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:11 -msgid "Duration" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1780 -msgid "Email address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1805 -msgid "File path" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1883 -#, python-format -msgid "“%(value)s” value must be a float." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1885 -msgid "Floating point number" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1925 -#, python-format -msgid "“%(value)s” value must be an integer." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:1927 -msgid "Integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2019 -msgid "Big (8 byte) integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2036 -msgid "Small integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2044 -msgid "IPv4 address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2075 -msgid "IP address" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2168 -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2169 -#, python-format -msgid "“%(value)s” value must be either None, True or False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2171 -msgid "Boolean (Either True, False or None)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2222 -msgid "Positive big integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2237 -msgid "Positive integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2252 -msgid "Positive small integer" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2268 -#, python-format -msgid "Slug (up to %(max_length)s)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2304 -#: src/shiftings/mail/forms/mail.py:11 -msgid "Text" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2374 -#, python-format -msgid "" -"“%(value)s” value has an invalid format. It must be in HH:MM[:ss[.uuuuuu]] " -"format." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2378 -#, python-format -msgid "" -"“%(value)s” value has the correct format (HH:MM[:ss[.uuuuuu]]) but it is an " -"invalid time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2382 -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:23 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:197 -#: src/shiftings/shifts/templates/shifts/template/group.html:43 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:23 -#: src/shiftings/shifts/templates/shifts/template/shift.html:10 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:33 -msgid "Time" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2490 -msgid "URL" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2514 -msgid "Raw binary data" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2579 -#, python-format -msgid "“%(value)s” is not a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/__init__.py:2581 -msgid "Universally unique identifier" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/files.py:232 -msgid "File" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/files.py:392 -msgid "Image" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/json.py:18 -msgid "A JSON object" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/json.py:20 -msgid "Value must be valid JSON." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:903 -#, python-format -msgid "%(model)s instance with %(field)s %(value)r does not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:905 -msgid "Foreign Key (type determined by related field)" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1204 -msgid "One-to-one relationship" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1261 -#, python-format -msgid "%(from)s-%(to)s relationship" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1263 -#, python-format -msgid "%(from)s-%(to)s relationships" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/db/models/fields/related.py:1311 -msgid "Many-to-many relationship" -msgstr "" - -#. Translators: If found as last label character, these punctuation -#. characters will prevent the default label_suffix to be appended to the label -#: venv/lib/python3.11/site-packages/django/forms/boundfield.py:176 -msgid ":?.!" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:91 -msgid "This field is required." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:298 -msgid "Enter a whole number." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:459 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1232 -msgid "Enter a valid date." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:482 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1233 -msgid "Enter a valid time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:509 -msgid "Enter a valid date/time." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:543 -msgid "Enter a valid duration." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:544 -#, python-brace-format -msgid "The number of days must be between {min_days} and {max_days}." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:610 -msgid "No file was submitted. Check the encoding type on the form." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:611 -msgid "No file was submitted." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:612 -msgid "The submitted file is empty." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:614 -#, python-format -msgid "Ensure this filename has at most %(max)d character (it has %(length)d)." -msgid_plural "" -"Ensure this filename has at most %(max)d characters (it has %(length)d)." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:619 -msgid "Please either submit a file or check the clear checkbox, not both." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:685 -msgid "" -"Upload a valid image. The file you uploaded was either not an image or a " -"corrupted image." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:848 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:940 -#: venv/lib/python3.11/site-packages/django/forms/models.py:1563 -#, python-format -msgid "Select a valid choice. %(value)s is not one of the available choices." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:942 -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1061 -#: venv/lib/python3.11/site-packages/django/forms/models.py:1561 -msgid "Enter a list of values." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1062 -msgid "Enter a complete value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1301 -msgid "Enter a valid UUID." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/fields.py:1331 -msgid "Enter a valid JSON." -msgstr "" - -#. Translators: This is the default suffix added to form field labels -#: venv/lib/python3.11/site-packages/django/forms/forms.py:98 -msgid ":" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/forms.py:248 -#: venv/lib/python3.11/site-packages/django/forms/forms.py:328 -#, python-format -msgid "(Hidden field %(name)s) %(error)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:67 -#, python-format -msgid "" -"ManagementForm data is missing or has been tampered with. Missing fields: " -"%(field_names)s. You may need to file a bug report if the issue persists." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:418 -#, python-format -msgid "Please submit at most %d form." -msgid_plural "Please submit at most %d forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:434 -#, python-format -msgid "Please submit at least %d form." -msgid_plural "Please submit at least %d forms." -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:470 -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:477 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:38 -msgid "Order" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/formsets.py:483 -msgid "Delete" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:887 -#, python-format -msgid "Please correct the duplicate data for %(field)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:892 -#, python-format -msgid "Please correct the duplicate data for %(field)s, which must be unique." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:899 -#, python-format -msgid "" -"Please correct the duplicate data for %(field_name)s which must be unique " -"for the %(lookup)s in %(date_field)s." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:908 -msgid "Please correct the duplicate values below." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1335 -msgid "The inline value did not match the parent instance." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1426 -msgid "Select a valid choice. That choice is not one of the available choices." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/models.py:1565 -#, python-format -msgid "“%(pk)s” is not a valid value." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/utils.py:204 -#, python-format -msgid "" -"%(datetime)s couldn’t be interpreted in time zone %(current_timezone)s; it " -"may be ambiguous or it may not exist." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:434 -msgid "Clear" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:435 -msgid "Currently" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:436 -msgid "Change" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:765 -#: src/shiftings/accounts/templates/accounts/user_detail.html:55 -#: src/shiftings/accounts/templates/accounts/user_detail.html:65 -msgid "Unknown" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:766 -#: src/shiftings/templates/generic/delete.html:9 -msgid "Yes" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/forms/widgets.py:767 -#: src/shiftings/templates/generic/delete.html:11 -msgid "No" -msgstr "" - -#. Translators: Please do not add spaces around commas. -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:858 -msgid "yes,no,maybe" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:888 -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:905 -#, python-format -msgid "%(size)d byte" -msgid_plural "%(size)d bytes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:907 -#, python-format -msgid "%s KB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:909 -#, python-format -msgid "%s MB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:911 -#, python-format -msgid "%s GB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:913 -#, python-format -msgid "%s TB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/template/defaultfilters.py:915 -#, python-format -msgid "%s PB" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:77 -msgid "p.m." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:78 -msgid "a.m." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:83 -msgid "PM" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:84 -msgid "AM" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:155 -msgid "midnight" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dateformat.py:157 -msgid "noon" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:7 -#: src/shiftings/utils/time/week.py:13 -msgid "Monday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:8 -#: src/shiftings/utils/time/week.py:14 -msgid "Tuesday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:9 -#: src/shiftings/utils/time/week.py:15 -msgid "Wednesday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:10 -#: src/shiftings/utils/time/week.py:16 -msgid "Thursday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:11 -#: src/shiftings/utils/time/week.py:17 -msgid "Friday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:12 -#: src/shiftings/utils/time/week.py:18 -msgid "Saturday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:13 -#: src/shiftings/utils/time/week.py:19 -msgid "Sunday" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:16 -msgid "Mon" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:17 -msgid "Tue" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:18 -msgid "Wed" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:19 -msgid "Thu" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:20 -msgid "Fri" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:21 -msgid "Sat" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:22 -msgid "Sun" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:25 -#: src/shiftings/utils/time/month.py:10 -msgid "January" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:26 -#: src/shiftings/utils/time/month.py:11 -msgid "February" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:27 -#: src/shiftings/utils/time/month.py:12 -msgid "March" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:28 -#: src/shiftings/utils/time/month.py:13 -msgid "April" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:29 -#: src/shiftings/utils/time/month.py:14 -msgid "May" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:30 -#: src/shiftings/utils/time/month.py:15 -msgid "June" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:31 -#: src/shiftings/utils/time/month.py:16 -msgid "July" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:32 -#: src/shiftings/utils/time/month.py:17 -msgid "August" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:33 -#: src/shiftings/utils/time/month.py:18 -msgid "September" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:34 -#: src/shiftings/utils/time/month.py:19 -msgid "October" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:35 -#: src/shiftings/utils/time/month.py:20 -msgid "November" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:36 -#: src/shiftings/utils/time/month.py:21 -msgid "December" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:39 -msgid "jan" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:40 -msgid "feb" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:41 -msgid "mar" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:42 -msgid "apr" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:43 -msgid "may" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:44 -msgid "jun" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:45 -msgid "jul" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:46 -msgid "aug" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:47 -msgid "sep" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:48 -msgid "oct" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:49 -msgid "nov" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:50 -msgid "dec" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:53 -msgctxt "abbrev. month" -msgid "Jan." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:54 -msgctxt "abbrev. month" -msgid "Feb." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:55 -msgctxt "abbrev. month" -msgid "March" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:56 -msgctxt "abbrev. month" -msgid "April" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:57 -msgctxt "abbrev. month" -msgid "May" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:58 -msgctxt "abbrev. month" -msgid "June" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:59 -msgctxt "abbrev. month" -msgid "July" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:60 -msgctxt "abbrev. month" -msgid "Aug." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:61 -msgctxt "abbrev. month" -msgid "Sept." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:62 -msgctxt "abbrev. month" -msgid "Oct." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:63 -msgctxt "abbrev. month" -msgid "Nov." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:64 -msgctxt "abbrev. month" -msgid "Dec." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:67 -msgctxt "alt. month" -msgid "January" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:68 -msgctxt "alt. month" -msgid "February" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:69 -msgctxt "alt. month" -msgid "March" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:70 -msgctxt "alt. month" -msgid "April" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:71 -msgctxt "alt. month" -msgid "May" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:72 -msgctxt "alt. month" -msgid "June" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:73 -msgctxt "alt. month" -msgid "July" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:74 -msgctxt "alt. month" -msgid "August" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:75 -msgctxt "alt. month" -msgid "September" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:76 -msgctxt "alt. month" -msgid "October" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:77 -msgctxt "alt. month" -msgid "November" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/dates.py:78 -msgctxt "alt. month" -msgid "December" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/ipv6.py:8 -msgid "This is not a valid IPv6 address." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/text.py:77 -#, python-format -msgctxt "String to return when truncating text" -msgid "%(truncated_text)s…" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/text.py:253 -msgid "or" -msgstr "" - -#. Translators: This string is used as a separator between list elements -#: venv/lib/python3.11/site-packages/django/utils/text.py:272 -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:94 -msgid ", " -msgstr "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:9 -#, python-format -msgid "%(num)d year" -msgid_plural "%(num)d years" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:10 -#, python-format -msgid "%(num)d month" -msgid_plural "%(num)d months" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:11 -#, python-format -msgid "%(num)d week" -msgid_plural "%(num)d weeks" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:12 -#, python-format -msgid "%(num)d day" -msgid_plural "%(num)d days" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:13 -#, python-format -msgid "%(num)d hour" -msgid_plural "%(num)d hours" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/utils/timesince.py:14 -#, python-format -msgid "%(num)d minute" -msgid_plural "%(num)d minutes" -msgstr[0] "" -msgstr[1] "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:111 -msgid "Forbidden" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:112 -msgid "CSRF verification failed. Request aborted." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:116 -msgid "" -"You are seeing this message because this HTTPS site requires a “Referer " -"header” to be sent by your web browser, but none was sent. This header is " -"required for security reasons, to ensure that your browser is not being " -"hijacked by third parties." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:122 -msgid "" -"If you have configured your browser to disable “Referer” headers, please re-" -"enable them, at least for this site, or for HTTPS connections, or for “same-" -"origin” requests." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:127 -msgid "" -"If you are using the tag or " -"including the “Referrer-Policy: no-referrer” header, please remove them. The " -"CSRF protection requires the “Referer” header to do strict referer checking. " -"If you’re concerned about privacy, use alternatives like for links to third-party sites." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:136 -msgid "" -"You are seeing this message because this site requires a CSRF cookie when " -"submitting forms. This cookie is required for security reasons, to ensure " -"that your browser is not being hijacked by third parties." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:142 -msgid "" -"If you have configured your browser to disable cookies, please re-enable " -"them, at least for this site, or for “same-origin” requests." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/csrf.py:148 -msgid "More information is available with DEBUG=True." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:44 -msgid "No year specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:64 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:115 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:214 -msgid "Date out of range" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:94 -msgid "No month specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:147 -msgid "No day specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:194 -msgid "No week specified" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:349 -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:380 -#, python-format -msgid "No %(verbose_name_plural)s available" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:652 -#, python-format -msgid "" -"Future %(verbose_name_plural)s not available because %(class_name)s." -"allow_future is False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/dates.py:692 -#, python-format -msgid "Invalid date string “%(datestr)s” given format “%(format)s”" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/detail.py:56 -#, python-format -msgid "No %(verbose_name)s found matching the query" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:70 -msgid "Page is not “last”, nor can it be converted to an int." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:77 -#, python-format -msgid "Invalid page (%(page_number)s): %(message)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/generic/list.py:169 -#, python-format -msgid "Empty list and “%(class_name)s.allow_empty” is False." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:39 -msgid "Directory indexes are not allowed here." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:41 -#, python-format -msgid "“%(path)s” does not exist" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/static.py:80 -#, python-format -msgid "Index of %(directory)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:7 -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:221 -msgid "The install worked successfully! Congratulations!" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:207 -#, python-format -msgid "" -"View release notes for Django %(version)s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:222 -#, python-format -msgid "" -"You are seeing this page because DEBUG=True is in your settings file and you have not " -"configured any URLs." -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:230 -msgid "Django Documentation" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:231 -msgid "Topics, references, & how-to’s" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:239 -msgid "Tutorial: A Polling App" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:240 -msgid "Get started with Django" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:248 -msgid "Django Community" -msgstr "" - -#: venv/lib/python3.11/site-packages/django/views/templates/default_urlconf.html:249 -msgid "Connect, get help, or contribute" +#: shiftings/accounts/forms/user_form.py:13 +msgid "Confirm password" msgstr "" -#: venv/lib/python3.11/site-packages/django_bootstrap5/components.py:26 -msgid "close" +#: shiftings/accounts/forms/user_form.py:31 +#: shiftings/accounts/forms/user_form.py:32 +msgid "Please enter matching passwords" msgstr "" -#: venv/lib/python3.11/site-packages/isort/main.py:158 -msgid "show this help message and exit" +#: shiftings/accounts/forms/user_form.py:65 +msgid "Cannot change first name, last name or email for LDAP users." msgstr "" -#: venv/lib/python3.11/site-packages/mypy/main.py:376 -#, python-format -msgid "%(prog)s: error: %(message)s\n" +#: shiftings/accounts/models/user.py:33 shiftings/templates/theme.html:28 +msgid "Light" msgstr "" -#: src/shiftings/accounts/forms/user_form.py:11 -msgid "Confirm password" +#: shiftings/accounts/models/user.py:34 shiftings/templates/theme.html:31 +msgid "Dark" msgstr "" -#: src/shiftings/accounts/forms/user_form.py:30 -msgid "Please enter matching passwords" +#: shiftings/accounts/models/user.py:35 shiftings/templates/theme.html:34 +msgid "Auto" msgstr "" -#: src/shiftings/accounts/models/user.py:32 -#: src/shiftings/accounts/templates/accounts/user_detail.html:52 -#: src/shiftings/shifts/models/participant.py:15 +#: shiftings/accounts/models/user.py:37 +#: shiftings/accounts/templates/accounts/user_detail.html:52 +#: shiftings/shifts/models/participant.py:15 msgid "Display Name" msgstr "" -#: src/shiftings/accounts/models/user.py:33 -#: src/shiftings/events/models/event.py:29 -#: src/shiftings/organizations/models/organization.py:28 +#: shiftings/accounts/models/user.py:38 shiftings/events/models/event.py:29 +#: shiftings/organizations/models/organization.py:28 msgid "Telephone Number" msgstr "" -#: src/shiftings/accounts/templates/accounts/confirm_email.html:7 +#: shiftings/accounts/models/user.py:42 +msgid "Theme Preference" +msgstr "" + +#: shiftings/accounts/templates/accounts/confirm_email.html:7 msgid "Go to Login" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:6 -#: src/shiftings/accounts/templates/accounts/login_multiple.html:12 +#: shiftings/accounts/templates/accounts/login_form.html:6 +#: shiftings/accounts/templates/accounts/login_multiple.html:12 msgid "Login with local Account" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:8 -#: src/shiftings/accounts/templates/accounts/login_multiple.html:20 +#: shiftings/accounts/templates/accounts/login_form.html:8 +#: shiftings/accounts/templates/accounts/login_multiple.html:20 msgid "Login with LDAP Account" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:14 -#: src/shiftings/accounts/views/auth.py:27 -#: src/shiftings/templates/top_nav.html:57 +#: shiftings/accounts/templates/accounts/login_form.html:14 +#: shiftings/accounts/views/auth.py:26 shiftings/templates/top_nav.html:61 msgid "Login" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:16 -#: src/shiftings/accounts/templates/accounts/password_reset/prompt.html:10 +#: shiftings/accounts/templates/accounts/login_form.html:16 +#: shiftings/accounts/templates/accounts/password_reset/prompt.html:10 msgid "Reset Password" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:18 +#: shiftings/accounts/templates/accounts/login_form.html:18 msgid "Return to selection" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:29 +#: shiftings/accounts/templates/accounts/login_form.html:29 msgid "You don't have an Account?" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:30 +#: shiftings/accounts/templates/accounts/login_form.html:30 msgid "Register here" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_form.html:32 +#: shiftings/accounts/templates/accounts/login_form.html:32 msgid "Or login using another Method" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:5 +#: shiftings/accounts/templates/accounts/login_multiple.html:5 msgid "Welcome to Shiftings" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:6 +#: shiftings/accounts/templates/accounts/login_multiple.html:6 msgid "" "To manage your shifts please authenticate with one of the given methods:" msgstr "" -#: src/shiftings/accounts/templates/accounts/login_multiple.html:31 +#: shiftings/accounts/templates/accounts/login_multiple.html:31 #, python-format msgid "" "\n" @@ -1622,28 +115,28 @@ msgid "" " " msgstr "" -#: src/shiftings/accounts/templates/accounts/logout.html:6 +#: shiftings/accounts/templates/accounts/logout.html:6 msgid "Successfully logged out" msgstr "" -#: src/shiftings/accounts/templates/accounts/logout.html:7 +#: shiftings/accounts/templates/accounts/logout.html:7 msgid "Login again" msgstr "" -#: src/shiftings/accounts/templates/accounts/password_reset/confirm.html:7 +#: shiftings/accounts/templates/accounts/password_reset/confirm.html:7 msgid "Change Password" msgstr "" -#: src/shiftings/accounts/templates/accounts/password_reset/done.html:4 +#: shiftings/accounts/templates/accounts/password_reset/done.html:4 msgid "Your password has been reset, check your email!" msgstr "" -#: src/shiftings/accounts/templates/accounts/password_reset/done.html:5 -#: src/shiftings/accounts/templates/accounts/password_reset/success.html:5 +#: shiftings/accounts/templates/accounts/password_reset/done.html:5 +#: shiftings/accounts/templates/accounts/password_reset/success.html:5 msgid "Back to login" msgstr "" -#: src/shiftings/accounts/templates/accounts/password_reset/prompt.html:4 +#: shiftings/accounts/templates/accounts/password_reset/prompt.html:4 msgid "" "\n" " If you are an local user enter your email-adress below to reset your " @@ -1651,15 +144,15 @@ msgid "" " " msgstr "" -#: src/shiftings/accounts/templates/accounts/password_reset/success.html:4 +#: shiftings/accounts/templates/accounts/password_reset/success.html:4 msgid "Your password has been successfully reset!" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:3 +#: shiftings/accounts/templates/accounts/user_detail.html:3 msgid "Confirm User Deletion" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:5 +#: shiftings/accounts/templates/accounts/user_detail.html:5 #, python-format msgid "" "\n" @@ -1669,128 +162,131 @@ msgid "" " " msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:17 +#: shiftings/accounts/templates/accounts/user_detail.html:17 msgid "Yes, I want to permanently delete my Data" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:19 +#: shiftings/accounts/templates/accounts/user_detail.html:19 msgid "Abort" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:29 -#: src/shiftings/organizations/models/membership.py:68 -#: src/shiftings/shifts/models/participant.py:13 -#: src/shiftings/templates/top_nav.html:47 +#: shiftings/accounts/templates/accounts/user_detail.html:29 +#: shiftings/organizations/models/membership.py:68 +#: shiftings/shifts/models/participant.py:13 +#: shiftings/templates/top_nav.html:48 msgid "User" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:37 -#: src/shiftings/accounts/templates/accounts/user_detail.html:57 +#: shiftings/accounts/templates/accounts/user_detail.html:37 +#: shiftings/accounts/templates/accounts/user_detail.html:57 msgid "Edit user data" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:48 -#: src/shiftings/organizations/forms/membership.py:16 +#: shiftings/accounts/templates/accounts/user_detail.html:48 +#: shiftings/organizations/forms/membership.py:16 msgid "Username" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:50 -#: src/shiftings/events/models/event.py:25 -#: src/shiftings/events/templates/events/event.html:85 -#: src/shiftings/organizations/models/membership.py:10 -#: src/shiftings/organizations/models/organization.py:24 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:195 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:239 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:278 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:388 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:39 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:13 -#: src/shiftings/shifts/models/base.py:8 -#: src/shiftings/shifts/models/recurring.py:32 -#: src/shiftings/shifts/models/template.py:17 -#: src/shiftings/shifts/models/template_group.py:20 -#: src/shiftings/shifts/models/type.py:40 -#: src/shiftings/shifts/models/type_group.py:16 -#: src/shiftings/shifts/templates/shifts/group/list.html:6 -#: src/shiftings/shifts/templates/shifts/list.html:6 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:6 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:6 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:5 -#: src/shiftings/shifts/templates/shifts/type_group/list.html:7 +#: shiftings/accounts/templates/accounts/user_detail.html:50 +#: shiftings/events/models/event.py:25 +#: shiftings/events/templates/events/event.html:85 +#: shiftings/organizations/models/membership.py:10 +#: shiftings/organizations/models/organization.py:24 +#: shiftings/organizations/templates/organizations/organization_admin.html:195 +#: shiftings/organizations/templates/organizations/organization_admin.html:239 +#: shiftings/organizations/templates/organizations/organization_admin.html:278 +#: shiftings/organizations/templates/organizations/organization_admin.html:388 +#: shiftings/organizations/templates/organizations/organization_settings.html:39 +#: shiftings/organizations/templates/organizations/user/claim_users.html:13 +#: shiftings/shifts/models/base.py:8 shiftings/shifts/models/recurring.py:32 +#: shiftings/shifts/models/template.py:17 +#: shiftings/shifts/models/template_group.py:20 +#: shiftings/shifts/models/type.py:40 shiftings/shifts/models/type_group.py:16 +#: shiftings/shifts/templates/shifts/group/list.html:6 +#: shiftings/shifts/templates/shifts/list.html:6 +#: shiftings/shifts/templates/shifts/recurring/list.html:6 +#: shiftings/shifts/templates/shifts/template/group_list.html:6 +#: shiftings/shifts/templates/shifts/template/template_form.html:5 +#: shiftings/shifts/templates/shifts/type_group/list.html:7 msgid "Name" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:58 +#: shiftings/accounts/templates/accounts/user_detail.html:55 +#: shiftings/accounts/templates/accounts/user_detail.html:65 +msgid "Unknown" +msgstr "" + +#: shiftings/accounts/templates/accounts/user_detail.html:58 msgid "Set Display Name" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:62 +#: shiftings/accounts/templates/accounts/user_detail.html:62 msgid "Email" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:64 +#: shiftings/accounts/templates/accounts/user_detail.html:64 msgid "Phone Number" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:66 +#: shiftings/accounts/templates/accounts/user_detail.html:66 msgid "Member since" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:68 +#: shiftings/accounts/templates/accounts/user_detail.html:68 msgid "Total Shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:70 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:124 +#: shiftings/accounts/templates/accounts/user_detail.html:70 +#: shiftings/organizations/templates/organizations/organization_admin.html:124 msgid "Groups" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:71 -#: src/shiftings/events/templates/events/template/event.html:32 -#: src/shiftings/shifts/templates/shifts/template/template.html:19 +#: shiftings/accounts/templates/accounts/user_detail.html:71 +#: shiftings/events/templates/events/template/event.html:32 +#: shiftings/shifts/templates/shifts/template/template.html:19 msgid "None" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:77 +#: shiftings/accounts/templates/accounts/user_detail.html:77 msgid "Organizations" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:83 +#: shiftings/accounts/templates/accounts/user_detail.html:83 msgid "Hide organizations" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:102 -#: src/shiftings/events/templates/events/template/event.html:12 +#: shiftings/accounts/templates/accounts/user_detail.html:102 +#: shiftings/events/templates/events/template/event.html:12 msgid "No Logo available" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:125 +#: shiftings/accounts/templates/accounts/user_detail.html:125 msgid "Recent Shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:127 -#: src/shiftings/accounts/templates/accounts/user_detail.html:132 +#: shiftings/accounts/templates/accounts/user_detail.html:127 +#: shiftings/accounts/templates/accounts/user_detail.html:132 msgid "Upcoming Shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:136 +#: shiftings/accounts/templates/accounts/user_detail.html:136 msgid "Past Shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:157 +#: shiftings/accounts/templates/accounts/user_detail.html:157 msgid "No recent Shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_detail.html:159 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:135 +#: shiftings/accounts/templates/accounts/user_detail.html:159 +#: shiftings/organizations/templates/organizations/organization_shifts.html:135 msgid "No upcoming shifts" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_form.html:6 +#: shiftings/accounts/templates/accounts/user_form.html:6 msgid "Register" msgstr "" -#: src/shiftings/accounts/templates/accounts/user_form.html:8 +#: shiftings/accounts/templates/accounts/user_form.html:8 #, python-format msgid "" "\n" @@ -1798,155 +294,137 @@ msgid "" " " msgstr "" -#: src/shiftings/accounts/templates/accounts/user_form.html:19 -#: src/shiftings/templates/template/simple_form_modal.html:21 +#: shiftings/accounts/templates/accounts/user_form.html:19 +#: shiftings/templates/template/simple_form_modal.html:21 msgid "Submit" msgstr "" -#: src/shiftings/accounts/views/auth.py:83 +#: shiftings/accounts/views/auth.py:82 msgid "Error while creating the user instance!" msgstr "" -#: src/shiftings/accounts/views/auth.py:87 +#: shiftings/accounts/views/auth.py:86 msgid "Successfully logged in!" msgstr "" -#: src/shiftings/accounts/views/auth.py:90 +#: shiftings/accounts/views/auth.py:89 #, python-format msgid "Error while trying to authenticate with sso! %(message)s" msgstr "" -#: src/shiftings/accounts/views/auth.py:131 -#: src/shiftings/templates/top_nav.html:52 +#: shiftings/accounts/views/auth.py:130 shiftings/templates/top_nav.html:55 msgid "Logout" msgstr "" -#: src/shiftings/accounts/views/password.py:13 +#: shiftings/accounts/views/password.py:13 msgid "Password Reset" msgstr "" -#: src/shiftings/accounts/views/password.py:19 +#: shiftings/accounts/views/password.py:19 msgid "Confirm Password Reset" msgstr "" -#: src/shiftings/accounts/views/user.py:35 -#: src/shiftings/templates/top_nav.html:15 +#: shiftings/accounts/views/user.py:35 shiftings/templates/top_nav.html:15 msgid "My Shifts" msgstr "" -#: src/shiftings/accounts/views/user.py:100 +#: shiftings/accounts/views/user.py:101 msgid "Could not find your user." msgstr "" -#: src/shiftings/accounts/views/user.py:106 +#: shiftings/accounts/views/user.py:107 msgid "Your EMail was confirmed. You can now login." msgstr "" -#: src/shiftings/accounts/views/user.py:109 +#: shiftings/accounts/views/user.py:110 msgid "Your activation link was invalid." msgstr "" -#: src/shiftings/accounts/views/user.py:126 +#: shiftings/accounts/views/user.py:127 msgid "Error while deleting your data: Please confirm deletion!" msgstr "" -#: src/shiftings/cal/feed/event.py:44 +#: shiftings/cal/feed/event.py:44 msgid "Public Events" msgstr "" -#: src/shiftings/cal/forms/day_form.py:8 +#: shiftings/cal/forms/day_form.py:8 msgid "Select Day" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:14 -#: src/shiftings/cal/templates/cal/calendar_base.html:24 +#: shiftings/cal/templates/cal/calendar_base.html:14 +#: shiftings/cal/templates/cal/calendar_base.html:24 msgid "Day View" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:20 +#: shiftings/cal/templates/cal/calendar_base.html:20 msgid "Day (Detailed)" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:30 +#: shiftings/cal/templates/cal/calendar_base.html:30 msgid "Day (By Types)" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:37 +#: shiftings/cal/templates/cal/calendar_base.html:37 msgid "Month View" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:38 -#: src/shiftings/utils/time/month.py:25 -#: src/shiftings/utils/time/timerange.py:12 +#: shiftings/cal/templates/cal/calendar_base.html:38 +#: shiftings/utils/time/month.py:25 shiftings/utils/time/timerange.py:12 msgid "Month" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:42 -#: src/shiftings/cal/templates/cal/calendar_base.html:48 +#: shiftings/cal/templates/cal/calendar_base.html:42 +#: shiftings/cal/templates/cal/calendar_base.html:48 msgid "List View" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:44 +#: shiftings/cal/templates/cal/calendar_base.html:44 msgid "List (Detailed)" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_base.html:50 +#: shiftings/cal/templates/cal/calendar_base.html:50 msgid "List (By Types)" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_templates/cal_shift_create_entry.html:4 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:106 -#: src/shiftings/events/templates/events/event.html:74 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:376 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:120 +#: shiftings/cal/templates/cal/calendar_templates/cal_shift_create_entry.html:4 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:106 +#: shiftings/events/templates/events/event.html:74 +#: shiftings/organizations/templates/organizations/organization_admin.html:376 +#: shiftings/organizations/templates/organizations/organization_shifts.html:120 msgid "Create Shift" msgstr "" -#: src/shiftings/cal/templates/cal/calendar_templates/recurring_shift_entry.html:5 +#: shiftings/cal/templates/cal/calendar_templates/recurring_shift_entry.html:5 msgid "Recurring" msgstr "" -#: src/shiftings/cal/templates/cal/day_calendar.html:14 +#: shiftings/cal/templates/cal/day_calendar.html:14 msgid "Previous Day" msgstr "" -#: src/shiftings/cal/templates/cal/day_calendar.html:22 +#: shiftings/cal/templates/cal/day_calendar.html:22 msgid "Jump to Date" msgstr "" -#: src/shiftings/cal/templates/cal/day_calendar.html:28 +#: shiftings/cal/templates/cal/day_calendar.html:28 msgid "Jump to Day" msgstr "" -#: src/shiftings/cal/templates/cal/day_calendar.html:30 +#: shiftings/cal/templates/cal/day_calendar.html:30 msgid "Select" msgstr "" -#: src/shiftings/cal/templates/cal/day_calendar.html:38 +#: shiftings/cal/templates/cal/day_calendar.html:38 msgid "Next Day" msgstr "" -#: src/shiftings/cal/templates/cal/list_calendar.html:12 +#: shiftings/cal/templates/cal/list_calendar.html:12 msgid "Shift list" -msgstr "Schichtenliste" - -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:7 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:48 -#: src/shiftings/shifts/templates/shifts/shift_participants.html:4 -msgid "Add with Name to Shift" -msgstr "" - -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:33 -#: src/shiftings/organizations/models/membership.py:12 -msgid "Default" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_shift_types.html:38 -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:95 -msgid "No Shifts found with these parameters or planned on this day" -msgstr "" - -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:3 +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:3 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:3 #, python-format msgid "" "\n" @@ -1955,270 +433,293 @@ msgid "" " " msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:12 -#: src/shiftings/events/models/event.py:24 -#: src/shiftings/shifts/models/base.py:12 -#: src/shiftings/shifts/models/recurring.py:34 -#: src/shiftings/shifts/models/template_group.py:24 -#: src/shiftings/shifts/models/type.py:37 -#: src/shiftings/shifts/models/type_group.py:14 -#: src/shiftings/shifts/templates/shifts/create_shift.html:79 -#: src/shiftings/shifts/templates/shifts/list.html:9 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:9 -#: src/shiftings/shifts/templates/shifts/shift.html:47 -#: src/shiftings/shifts/templates/shifts/template/group.html:39 -#: src/shiftings/shifts/templates/shifts/template/group.html:45 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:8 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:25 +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:14 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:48 +#: shiftings/shifts/templates/shifts/shift_participants.html:4 +msgid "Add with Name to Shift" +msgstr "" + +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:30 +#: shiftings/organizations/templates/organizations/organization_admin.html:197 +#: shiftings/shifts/templates/shifts/template/group.html:43 +#: shiftings/shifts/templates/shifts/template/group_template.html:23 +#: shiftings/shifts/templates/shifts/template/shift.html:10 +#: shiftings/shifts/templates/shifts/template/template_form.html:33 +msgid "Time" +msgstr "" + +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:40 +#: shiftings/organizations/models/membership.py:12 +msgid "Default" +msgstr "" + +#: shiftings/cal/templates/cal/template/day_calendar_shift_types.html:45 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:95 +msgid "No Shifts found with these parameters or planned on this day" +msgstr "" + +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:12 +#: shiftings/events/models/event.py:24 shiftings/shifts/models/base.py:12 +#: shiftings/shifts/models/recurring.py:34 +#: shiftings/shifts/models/template_group.py:24 +#: shiftings/shifts/models/type.py:37 shiftings/shifts/models/type_group.py:14 +#: shiftings/shifts/templates/shifts/create_shift.html:89 +#: shiftings/shifts/templates/shifts/list.html:9 +#: shiftings/shifts/templates/shifts/recurring/list.html:9 +#: shiftings/shifts/templates/shifts/shift.html:47 +#: shiftings/shifts/templates/shifts/template/group.html:39 +#: shiftings/shifts/templates/shifts/template/group.html:45 +#: shiftings/shifts/templates/shifts/template/group_list.html:8 +#: shiftings/shifts/templates/shifts/template/group_template.html:25 msgid "Organization" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:13 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:13 msgid "Org" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:14 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:14 msgid "Shift" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:15 -#: src/shiftings/shifts/templates/shifts/create_shift.html:56 -#: src/shiftings/shifts/templates/shifts/template/shift.html:12 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:21 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:15 +#: shiftings/shifts/templates/shifts/create_shift.html:66 +#: shiftings/shifts/templates/shifts/template/shift.html:12 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:21 msgid "Participants" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:16 -#: src/shiftings/shifts/templates/shifts/list.html:10 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:16 +#: shiftings/shifts/templates/shifts/list.html:10 msgid "Start" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:17 -#: src/shiftings/shifts/templates/shifts/list.html:11 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:17 +#: shiftings/shifts/templates/shifts/list.html:11 msgid "End" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:36 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:7 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:36 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:7 msgid "Shift Details" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:61 -#: src/shiftings/shifts/templates/shifts/shift_participants.html:23 -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:29 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:44 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:61 +#: shiftings/shifts/templates/shifts/shift_participants.html:23 +#: shiftings/shifts/templates/shifts/template/slots_display.html:29 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:44 msgid "Add me" msgstr "" -#: src/shiftings/cal/templates/cal/template/day_calendar_xs.html:112 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:31 -#: src/shiftings/shifts/templates/shifts/edit_templates.html:26 +#: shiftings/cal/templates/cal/template/day_calendar_xs.html:112 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:31 +#: shiftings/shifts/templates/shifts/edit_templates.html:26 msgid "Add Shift" msgstr "" -#: src/shiftings/cal/templates/cal/template/month_calendar.html:7 +#: shiftings/cal/templates/cal/template/month_calendar.html:7 msgid "Previous Month" msgstr "" -#: src/shiftings/cal/templates/cal/template/month_calendar.html:15 +#: shiftings/cal/templates/cal/template/month_calendar.html:15 msgid "Next Month" msgstr "" -#: src/shiftings/cal/templates/cal/week_calendar.html:9 +#: shiftings/cal/templates/cal/week_calendar.html:9 msgid "Previous Week" msgstr "" -#: src/shiftings/cal/templates/cal/week_calendar.html:13 +#: shiftings/cal/templates/cal/week_calendar.html:13 msgid "Week" msgstr "" -#: src/shiftings/cal/templates/cal/week_calendar.html:17 +#: shiftings/cal/templates/cal/week_calendar.html:17 msgid "Next Week" msgstr "" -#: src/shiftings/cal/views/day_calendar.py:24 +#: shiftings/cal/views/day_calendar.py:24 #, python-brace-format msgid "Day {date}" msgstr "" -#: src/shiftings/cal/views/day_calendar.py:25 +#: shiftings/cal/views/day_calendar.py:25 msgid "Day Overview" msgstr "" -#: src/shiftings/cal/views/list.py:24 -#: src/shiftings/organizations/templates/organizations/template/org_info.html:6 +#: shiftings/cal/views/list.py:24 +#: shiftings/organizations/templates/organizations/template/org_info.html:6 msgid "Shift Overview" msgstr "" -#: src/shiftings/cal/views/month_calendar.py:24 +#: shiftings/cal/views/month_calendar.py:24 #, python-brace-format msgid "Month {year}/{month:0>2}" msgstr "" -#: src/shiftings/cal/views/month_calendar.py:26 -#: src/shiftings/events/templates/events/event.html:52 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:162 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:28 +#: shiftings/cal/views/month_calendar.py:26 +#: shiftings/events/templates/events/event.html:52 +#: shiftings/organizations/templates/organizations/organization_admin.html:162 +#: shiftings/organizations/templates/organizations/organization_shifts.html:28 msgid "Month Overview" msgstr "" -#: src/shiftings/events/forms/event.py:24 +#: shiftings/events/forms/event.py:24 #, python-format msgid "Start date %(start)s was after end_date %(end)s" msgstr "" -#: src/shiftings/events/models/event.py:26 -#: src/shiftings/events/templates/events/event.html:19 -#: src/shiftings/organizations/models/organization.py:25 -#: src/shiftings/organizations/templates/organizations/template/org_info.html:58 +#: shiftings/events/models/event.py:26 +#: shiftings/events/templates/events/event.html:19 +#: shiftings/organizations/models/organization.py:25 +#: shiftings/organizations/templates/organizations/template/org_info.html:58 msgid "Logo" msgstr "" -#: src/shiftings/events/models/event.py:28 -#: src/shiftings/organizations/models/organization.py:27 +#: shiftings/events/models/event.py:28 +#: shiftings/organizations/models/organization.py:27 msgid "E-Mail" msgstr "" -#: src/shiftings/events/models/event.py:30 -#: src/shiftings/organizations/models/organization.py:29 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:81 -#: src/shiftings/organizations/templates/organizations/template/organization.html:27 +#: shiftings/events/models/event.py:30 +#: shiftings/organizations/models/organization.py:29 +#: shiftings/organizations/templates/organizations/organization_shifts.html:81 +#: shiftings/organizations/templates/organizations/template/organization.html:27 msgid "Website" msgstr "" -#: src/shiftings/events/models/event.py:32 +#: shiftings/events/models/event.py:32 msgid "Start Date" msgstr "" -#: src/shiftings/events/models/event.py:32 +#: shiftings/events/models/event.py:32 msgid "Earliest date where there are shifts available" msgstr "" -#: src/shiftings/events/models/event.py:33 +#: shiftings/events/models/event.py:33 msgid "End Date" msgstr "" -#: src/shiftings/events/models/event.py:33 +#: shiftings/events/models/event.py:33 msgid "Latest date where there are shifts available" msgstr "" -#: src/shiftings/events/models/event.py:35 -#: src/shiftings/organizations/models/organization.py:32 +#: shiftings/events/models/event.py:35 +#: shiftings/organizations/models/organization.py:32 msgid "Description" msgstr "" -#: src/shiftings/events/models/event.py:55 +#: shiftings/events/models/event.py:55 #, python-brace-format msgid "{name} (by {organization})" msgstr "" -#: src/shiftings/events/templates/events/event.html:10 +#: shiftings/events/templates/events/event.html:10 msgid "Edit Event" msgstr "" -#: src/shiftings/events/templates/events/event.html:55 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:165 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:31 +#: shiftings/events/templates/events/event.html:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:165 +#: shiftings/organizations/templates/organizations/organization_shifts.html:31 msgid "Complete Overview" msgstr "" -#: src/shiftings/events/templates/events/event.html:69 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:363 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:109 +#: shiftings/events/templates/events/event.html:69 +#: shiftings/organizations/templates/organizations/organization_admin.html:363 +#: shiftings/organizations/templates/organizations/organization_shifts.html:109 msgid "Shifts" msgstr "" -#: src/shiftings/events/templates/events/event.html:71 -#: src/shiftings/events/templates/events/event.html:72 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:371 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:112 +#: shiftings/events/templates/events/event.html:71 +#: shiftings/events/templates/events/event.html:72 +#: shiftings/organizations/templates/organizations/organization_admin.html:371 +#: shiftings/organizations/templates/organizations/organization_shifts.html:112 msgid "ical Export" msgstr "" -#: src/shiftings/events/templates/events/event.html:86 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:389 -#: src/shiftings/shifts/templates/shifts/list.html:7 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:7 -#: src/shiftings/shifts/templates/shifts/template/shift.html:8 -#: src/shiftings/shifts/templates/shifts/template/template.html:7 +#: shiftings/events/templates/events/event.html:86 +#: shiftings/organizations/templates/organizations/organization_admin.html:389 +#: shiftings/shifts/templates/shifts/list.html:7 +#: shiftings/shifts/templates/shifts/recurring/list.html:7 +#: shiftings/shifts/templates/shifts/template/shift.html:8 +#: shiftings/shifts/templates/shifts/template/template.html:7 msgid "Type" msgstr "" -#: src/shiftings/events/templates/events/event.html:87 -#: src/shiftings/mail/forms/mail.py:39 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:241 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:390 -#: src/shiftings/shifts/models/template_group.py:26 -#: src/shiftings/shifts/templates/shifts/shift.html:43 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:9 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:17 -#: src/shiftings/shifts/templates/shifts/template/template.html:13 +#: shiftings/events/templates/events/event.html:87 +#: shiftings/mail/forms/mail.py:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:241 +#: shiftings/organizations/templates/organizations/organization_admin.html:390 +#: shiftings/shifts/models/template_group.py:26 +#: shiftings/shifts/templates/shifts/shift.html:43 +#: shiftings/shifts/templates/shifts/template/group_list.html:9 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:17 +#: shiftings/shifts/templates/shifts/template/template.html:13 msgid "Start Time" msgstr "" -#: src/shiftings/events/templates/events/event.html:88 -#: src/shiftings/mail/forms/mail.py:40 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:391 -#: src/shiftings/shifts/templates/shifts/shift.html:45 -#: src/shiftings/shifts/templates/shifts/template/template.html:15 +#: shiftings/events/templates/events/event.html:88 +#: shiftings/mail/forms/mail.py:56 +#: shiftings/organizations/templates/organizations/organization_admin.html:391 +#: shiftings/shifts/templates/shifts/shift.html:45 +#: shiftings/shifts/templates/shifts/template/template.html:15 msgid "End Time" msgstr "" -#: src/shiftings/events/templates/events/event.html:89 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:198 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:240 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:392 -#: src/shiftings/shifts/models/base.py:9 -#: src/shiftings/shifts/models/template_group.py:21 -#: src/shiftings/shifts/templates/shifts/list.html:8 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:8 -#: src/shiftings/shifts/templates/shifts/shift.html:41 -#: src/shiftings/shifts/templates/shifts/template/group.html:41 -#: src/shiftings/shifts/templates/shifts/template/group_list.html:7 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:21 -#: src/shiftings/shifts/templates/shifts/template/shift_card.html:12 -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:18 +#: shiftings/events/templates/events/event.html:89 +#: shiftings/organizations/templates/organizations/organization_admin.html:198 +#: shiftings/organizations/templates/organizations/organization_admin.html:240 +#: shiftings/organizations/templates/organizations/organization_admin.html:392 +#: shiftings/shifts/models/base.py:9 +#: shiftings/shifts/models/template_group.py:21 +#: shiftings/shifts/templates/shifts/list.html:8 +#: shiftings/shifts/templates/shifts/recurring/list.html:8 +#: shiftings/shifts/templates/shifts/shift.html:41 +#: shiftings/shifts/templates/shifts/template/group.html:41 +#: shiftings/shifts/templates/shifts/template/group_list.html:7 +#: shiftings/shifts/templates/shifts/template/group_template.html:21 +#: shiftings/shifts/templates/shifts/template/shift_card.html:12 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:18 msgid "Place" msgstr "" -#: src/shiftings/events/templates/events/event.html:90 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:393 +#: shiftings/events/templates/events/event.html:90 +#: shiftings/organizations/templates/organizations/organization_admin.html:393 msgid "Users/Required/Max" msgstr "" -#: src/shiftings/events/templates/events/event.html:108 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:416 +#: shiftings/events/templates/events/event.html:108 +#: shiftings/organizations/templates/organizations/organization_admin.html:416 msgid "No current shifts" msgstr "" -#: src/shiftings/events/templates/events/list.html:5 -#: src/shiftings/events/templates/events/list.html:13 -#: src/shiftings/events/templates/events/list.html:22 +#: shiftings/events/templates/events/list.html:5 +#: shiftings/events/templates/events/list.html:13 +#: shiftings/events/templates/events/list.html:22 msgid "All Events" msgstr "" -#: src/shiftings/events/templates/events/list.html:7 -#: src/shiftings/templates/top_nav.html:22 +#: shiftings/events/templates/events/list.html:7 +#: shiftings/templates/top_nav.html:22 msgid "Current Events" msgstr "" -#: src/shiftings/events/templates/events/list.html:16 +#: shiftings/events/templates/events/list.html:16 msgid "All upcoming Events" msgstr "" -#: src/shiftings/events/templates/events/list.html:20 +#: shiftings/events/templates/events/list.html:20 msgid "Enter Name or Organization" msgstr "" -#: src/shiftings/events/templates/events/list.html:26 +#: shiftings/events/templates/events/list.html:26 msgid "Future Events" msgstr "" -#: src/shiftings/events/templates/events/list.html:39 +#: shiftings/events/templates/events/list.html:39 msgid "No Events found." msgstr "" -#: src/shiftings/events/templates/events/template/event.html:23 +#: shiftings/events/templates/events/template/event.html:23 #, python-format msgid "" "\n" @@ -2226,262 +727,276 @@ msgid "" " " msgstr "" -#: src/shiftings/events/templates/events/template/event.html:31 +#: shiftings/events/templates/events/template/event.html:31 msgid "Open Shifts" msgstr "" -#: src/shiftings/events/templates/events/template/event.html:32 +#: shiftings/events/templates/events/template/event.html:32 msgid "Required People" msgstr "" -#: src/shiftings/events/templates/events/template/event.html:33 +#: shiftings/events/templates/events/template/event.html:33 msgid "Visibility" msgstr "" -#: src/shiftings/events/templates/events/template/event.html:33 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:350 +#: shiftings/events/templates/events/template/event.html:33 +#: shiftings/organizations/templates/organizations/organization_admin.html:350 msgid "Public,Private,Unknown" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:2 +#: shiftings/events/templates/events/template/event_stats.html:2 msgid "Shifts that are below the required amount of users" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:3 +#: shiftings/events/templates/events/template/event_stats.html:3 msgid "Shifts missing users" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:6 +#: shiftings/events/templates/events/template/event_stats.html:6 msgid "Shifts that are below the required and maximum amount of users" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:7 +#: shiftings/events/templates/events/template/event_stats.html:7 msgid "Shifts with open slots" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:10 +#: shiftings/events/templates/events/template/event_stats.html:10 msgid "Number of Shift slots filled with users" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:11 +#: shiftings/events/templates/events/template/event_stats.html:11 msgid "Filled Slots" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:12 -#: src/shiftings/events/templates/events/template/event_stats.html:16 +#: shiftings/events/templates/events/template/event_stats.html:12 +#: shiftings/events/templates/events/template/event_stats.html:16 msgid "No Shifts available" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:14 +#: shiftings/events/templates/events/template/event_stats.html:14 msgid "Number of Shift slots that are still open" msgstr "" -#: src/shiftings/events/templates/events/template/event_stats.html:15 +#: shiftings/events/templates/events/template/event_stats.html:15 msgid "Open Slots" msgstr "" -#: src/shiftings/mail/forms/mail.py:10 +#: shiftings/mail/forms/mail.py:13 msgid "Subject" msgstr "" -#: src/shiftings/mail/forms/mail.py:12 +#: shiftings/mail/forms/mail.py:14 +msgid "Text" +msgstr "" + +#: shiftings/mail/forms/mail.py:15 msgid "Attachments" msgstr "" -#: src/shiftings/mail/forms/mail.py:22 +#: shiftings/mail/forms/mail.py:26 +msgid "Each attachment must be smaller than 10 MB." +msgstr "" + +#: shiftings/mail/forms/mail.py:30 +msgid "Total attachment size must be smaller than 25 MB." +msgstr "" + +#: shiftings/mail/forms/mail.py:38 msgid "" "Send the mail only to specific membership types. Or leave blank to send to " "all." msgstr "" -#: src/shiftings/mail/forms/mail.py:37 +#: shiftings/mail/forms/mail.py:53 msgid "" "Send the mail only to specific shift types. Or leave blank to send to all." msgstr "" -#: src/shiftings/mail/templates/mail/mail.html:8 +#: shiftings/mail/forms/mail.py:71 +msgid "Start time must be before end time." +msgstr "" + +#: shiftings/mail/templates/mail/mail.html:8 msgid "Send Mail" msgstr "" -#: src/shiftings/mail/views/mail.py:45 +#: shiftings/mail/views/mail.py:45 #, python-brace-format msgid "E-Mail sent to {count} user(s)." msgstr "" -#: src/shiftings/organizations/forms/membership.py:40 -#: src/shiftings/shifts/forms/participant.py:57 +#: shiftings/organizations/forms/membership.py:40 +#: shiftings/shifts/forms/participant.py:57 msgid "The user you entered could not be found." msgstr "" -#: src/shiftings/organizations/models/membership.py:11 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:68 -#: src/shiftings/templates/top_nav.html:50 +#: shiftings/organizations/models/membership.py:11 +#: shiftings/organizations/templates/organizations/organization_admin.html:68 +#: shiftings/templates/top_nav.html:51 msgid "Admin" msgstr "" -#: src/shiftings/organizations/models/membership.py:13 +#: shiftings/organizations/models/membership.py:13 msgid "Permissions" msgstr "" -#: src/shiftings/organizations/models/membership.py:19 +#: shiftings/organizations/models/membership.py:19 msgid "edit organization details" msgstr "" -#: src/shiftings/organizations/models/membership.py:20 +#: shiftings/organizations/models/membership.py:20 msgid "see members of the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:21 +#: shiftings/organizations/models/membership.py:21 msgid "see shift participation statistics" msgstr "" -#: src/shiftings/organizations/models/membership.py:22 +#: shiftings/organizations/models/membership.py:22 msgid "send emails to everyone in the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:24 +#: shiftings/organizations/models/membership.py:24 msgid "create and update membership types for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:25 +#: shiftings/organizations/models/membership.py:25 msgid "add and remove members for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:27 +#: shiftings/organizations/models/membership.py:27 msgid "create and update events for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:29 +#: shiftings/organizations/models/membership.py:29 msgid "create and update recurring shifts for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:30 +#: shiftings/organizations/models/membership.py:30 msgid "create and update shifts templates for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:31 +#: shiftings/organizations/models/membership.py:31 msgid "create and update shifts for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:32 +#: shiftings/organizations/models/membership.py:32 msgid "delete uncompleted shifts for the organization" msgstr "" -#: src/shiftings/organizations/models/membership.py:34 +#: shiftings/organizations/models/membership.py:34 msgid "remove others from shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:35 +#: shiftings/organizations/models/membership.py:35 msgid "add other users that are not members of the organization to shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:36 +#: shiftings/organizations/models/membership.py:36 msgid "add other organization members to shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:37 +#: shiftings/organizations/models/membership.py:37 msgid "participate in shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:38 +#: shiftings/organizations/models/membership.py:38 msgid "add self/others (depending on other permissions) to past shifts" msgstr "" -#: src/shiftings/organizations/models/membership.py:57 +#: shiftings/organizations/models/membership.py:57 #, python-brace-format msgid "{name} (Admin)" msgstr "" -#: src/shiftings/organizations/models/membership.py:59 +#: shiftings/organizations/models/membership.py:59 #, python-brace-format msgid "{name} (Default)" msgstr "" -#: src/shiftings/organizations/models/membership.py:65 +#: shiftings/organizations/models/membership.py:65 msgid "Membership Type" msgstr "" -#: src/shiftings/organizations/models/membership.py:69 -#: src/shiftings/shifts/models/type.py:38 +#: shiftings/organizations/models/membership.py:69 +#: shiftings/shifts/models/type.py:38 msgid "Group" msgstr "" -#: src/shiftings/organizations/models/membership.py:100 +#: shiftings/organizations/models/membership.py:100 msgid "Membership can only be either user or group, not both." msgstr "" -#: src/shiftings/organizations/models/membership.py:102 +#: shiftings/organizations/models/membership.py:102 msgid "Membership must consist of a user or a group." msgstr "" -#: src/shiftings/organizations/models/organization.py:30 -msgid "Include Protocol i.E. https://example.com" +#: shiftings/organizations/models/organization.py:30 +msgid "Include protocol, e.g." msgstr "" -#: src/shiftings/organizations/models/organization.py:33 -#: src/shiftings/shifts/models/base.py:24 -#: src/shiftings/shifts/models/recurring.py:56 -#: src/shiftings/shifts/models/recurring.py:63 -#: src/shiftings/shifts/models/shift.py:32 -#: src/shiftings/shifts/models/template.py:33 +#: shiftings/organizations/models/organization.py:33 +#: shiftings/shifts/models/base.py:24 shiftings/shifts/models/recurring.py:56 +#: shiftings/shifts/models/recurring.py:63 shiftings/shifts/models/shift.py:32 +#: shiftings/shifts/models/template.py:33 #, python-brace-format msgid "A maximum of {amount} characters is allowed" msgstr "" -#: src/shiftings/organizations/models/organization.py:34 +#: shiftings/organizations/models/organization.py:34 msgid "Confirm Participants" msgstr "" -#: src/shiftings/organizations/models/organization.py:35 +#: shiftings/organizations/models/organization.py:35 msgid "Whether the participants can be confirmed or not. Default: False" msgstr "" -#: src/shiftings/organizations/models/organization.py:49 +#: shiftings/organizations/models/organization.py:49 msgid "manage all organizations" msgstr "" -#: src/shiftings/organizations/models/user.py:10 +#: shiftings/organizations/models/user.py:10 msgid "Claimed by" msgstr "" -#: src/shiftings/organizations/templates/organizations/list.html:5 +#: shiftings/organizations/templates/organizations/list.html:5 msgid "All Organizations" msgstr "" -#: src/shiftings/organizations/templates/organizations/list.html:7 -#: src/shiftings/templates/top_nav.html:18 +#: shiftings/organizations/templates/organizations/list.html:7 +#: shiftings/templates/top_nav.html:18 msgid "My Organizations" msgstr "" -#: src/shiftings/organizations/templates/organizations/list.html:12 +#: shiftings/organizations/templates/organizations/list.html:12 msgid "Enter Name" msgstr "" -#: src/shiftings/organizations/templates/organizations/list.html:14 +#: shiftings/organizations/templates/organizations/list.html:14 msgid "Add new Organization" msgstr "" -#: src/shiftings/organizations/templates/organizations/list.html:25 +#: shiftings/organizations/templates/organizations/list.html:25 msgid "No Organizations found." msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:6 +#: shiftings/organizations/templates/organizations/organization_admin.html:6 msgid "Show membership Permissions" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:10 +#: shiftings/organizations/templates/organizations/organization_admin.html:10 msgid "All Permissions" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:19 +#: shiftings/organizations/templates/organizations/organization_admin.html:19 msgid "No permissions" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:26 +#: shiftings/organizations/templates/organizations/organization_admin.html:26 msgid "Add member" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:41 +#: shiftings/organizations/templates/organizations/organization_admin.html:41 #, python-format msgid "" "\n" @@ -2489,183 +1004,187 @@ msgid "" " " msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:48 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:8 +#: shiftings/organizations/templates/organizations/organization_admin.html:48 +#: shiftings/organizations/templates/organizations/organization_shifts.html:8 msgid "Member Shift Summary" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:54 +#: shiftings/organizations/templates/organizations/organization_admin.html:54 msgid "Add Membership Type" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:78 +#: shiftings/organizations/templates/organizations/organization_admin.html:78 msgid "Show user permissions" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:81 +#: shiftings/organizations/templates/organizations/organization_admin.html:81 msgid "Edit Membership Type" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:90 +#: shiftings/organizations/templates/organizations/organization_admin.html:90 msgid "Add Member" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:100 +#: shiftings/organizations/templates/organizations/organization_admin.html:100 msgid "No Members of this type!" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:106 +#: shiftings/organizations/templates/organizations/organization_admin.html:106 msgid "Direct Members" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:150 +#: shiftings/organizations/templates/organizations/organization_admin.html:150 msgid "Next Shift" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:154 +#: shiftings/organizations/templates/organizations/organization_admin.html:154 msgid "No Shifts planned" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:179 -#: src/shiftings/shifts/templates/shifts/template/group.html:52 +#: shiftings/organizations/templates/organizations/organization_admin.html:179 +#: shiftings/shifts/templates/shifts/template/group.html:52 msgid "Recurring Shifts" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:184 +#: shiftings/organizations/templates/organizations/organization_admin.html:184 msgid "Add recurring shift" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:196 +#: shiftings/organizations/templates/organizations/organization_admin.html:196 msgid "Shift count" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:199 +#: shiftings/organizations/templates/organizations/organization_admin.html:199 msgid "Status" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:210 +#: shiftings/organizations/templates/organizations/organization_admin.html:210 msgid "Inactive,Active,Undefined" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:216 +#: shiftings/organizations/templates/organizations/organization_admin.html:216 msgid "No recurring shifts" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:223 +#: shiftings/organizations/templates/organizations/organization_admin.html:223 msgid "Shifts Templates" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:228 +#: shiftings/organizations/templates/organizations/organization_admin.html:228 msgid "Add shifts template" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:255 -#: src/shiftings/organizations/templates/organizations/organization_admin.html:309 +#: shiftings/organizations/templates/organizations/organization_admin.html:255 +#: shiftings/organizations/templates/organizations/organization_admin.html:309 msgid "No shifts templates" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:262 +#: shiftings/organizations/templates/organizations/organization_admin.html:262 msgid "Shifts Types" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:267 +#: shiftings/organizations/templates/organizations/organization_admin.html:267 msgid "Add Shift Type" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:279 +#: shiftings/organizations/templates/organizations/organization_admin.html:279 msgid "Color" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:280 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:15 +#: shiftings/organizations/templates/organizations/organization_admin.html:280 +#: shiftings/organizations/templates/organizations/user/claim_users.html:15 msgid "Shift Count" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:281 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:41 -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:16 +#: shiftings/organizations/templates/organizations/organization_admin.html:281 +#: shiftings/organizations/templates/organizations/organization_settings.html:41 +#: shiftings/organizations/templates/organizations/user/claim_users.html:16 msgid "Action" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:290 +#: shiftings/organizations/templates/organizations/organization_admin.html:290 msgid "Shift Type Background Color" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:317 +#: shiftings/organizations/templates/organizations/organization_admin.html:317 msgid "Upcoming Events" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:321 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:50 +#: shiftings/organizations/templates/organizations/organization_admin.html:321 +#: shiftings/organizations/templates/organizations/organization_shifts.html:50 msgid "Expired Events" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:326 -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:55 +#: shiftings/organizations/templates/organizations/organization_admin.html:326 +#: shiftings/organizations/templates/organizations/organization_shifts.html:55 msgid "Add Event" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:356 +#: shiftings/organizations/templates/organizations/organization_admin.html:356 msgid "No upcoming Events planned" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_admin.html:367 +#: shiftings/organizations/templates/organizations/organization_admin.html:367 msgid "Claim Legacy Shifts" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:19 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:36 -#: src/shiftings/shifts/templates/shifts/edit_templates.html:31 -#: src/shiftings/shifts/templates/shifts/recurring/form.html:25 -#: src/shiftings/templates/generic/create_or_update.html:20 -#: src/shiftings/templates/generic/formset.html:17 +#: shiftings/organizations/templates/organizations/organization_settings.html:19 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:36 +#: shiftings/shifts/templates/shifts/edit_templates.html:31 +#: shiftings/shifts/templates/shifts/recurring/form.html:25 +#: shiftings/templates/generic/create_or_update.html:20 +#: shiftings/templates/generic/formset.html:17 msgid "Save" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:26 +#: shiftings/organizations/templates/organizations/organization_settings.html:26 msgid "Shift Type Groups" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:29 +#: shiftings/organizations/templates/organizations/organization_settings.html:29 msgid "Create Type Group" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:40 +#: shiftings/organizations/templates/organizations/organization_settings.html:38 +msgid "Order" +msgstr "" + +#: shiftings/organizations/templates/organizations/organization_settings.html:40 msgid "Types" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:61 +#: shiftings/organizations/templates/organizations/organization_settings.html:61 msgid "Edit Type Group" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:72 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:77 +#: shiftings/organizations/templates/organizations/organization_settings.html:72 +#: shiftings/organizations/templates/organizations/organization_settings.html:77 msgid "Move Up" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_settings.html:84 -#: src/shiftings/organizations/templates/organizations/organization_settings.html:89 +#: shiftings/organizations/templates/organizations/organization_settings.html:84 +#: shiftings/organizations/templates/organizations/organization_settings.html:89 msgid "Move Down" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:11 +#: shiftings/organizations/templates/organizations/organization_shifts.html:11 msgid "Full Summary" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:46 +#: shiftings/organizations/templates/organizations/organization_shifts.html:46 msgid "Upcoming Event" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:66 -#: src/shiftings/shifts/models/shift.py:21 +#: shiftings/organizations/templates/organizations/organization_shifts.html:66 +#: shiftings/shifts/models/shift.py:21 msgid "Event" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:68 +#: shiftings/organizations/templates/organizations/organization_shifts.html:68 msgid "Go to Event" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:74 +#: shiftings/organizations/templates/organizations/organization_shifts.html:74 #, python-format msgid "" "\n" @@ -2675,463 +1194,464 @@ msgid "" " " msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:94 +#: shiftings/organizations/templates/organizations/organization_shifts.html:94 msgid "No Events planned" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:102 +#: shiftings/organizations/templates/organizations/organization_shifts.html:102 msgid "Events disabled" msgstr "" -#: src/shiftings/organizations/templates/organizations/organization_shifts.html:117 +#: shiftings/organizations/templates/organizations/organization_shifts.html:117 msgid "Create Shift from Template" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/member_group.html:6 +#: shiftings/organizations/templates/organizations/template/member_group.html:6 msgid "Click to expand members" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:8 +#: shiftings/organizations/templates/organizations/template/org_info.html:8 msgid "Organization Overview" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:12 +#: shiftings/organizations/templates/organizations/template/org_info.html:12 msgid "Administrate Organization" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:14 +#: shiftings/organizations/templates/organizations/template/org_info.html:14 msgid "Organization Admin View" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:19 +#: shiftings/organizations/templates/organizations/template/org_info.html:19 msgid "Organization Settings" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:21 +#: shiftings/organizations/templates/organizations/template/org_info.html:21 msgid "Organization Settings View" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:35 +#: shiftings/organizations/templates/organizations/template/org_info.html:35 msgid "Send Mail to shift participants" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:41 +#: shiftings/organizations/templates/organizations/template/org_info.html:41 msgid "Edit Organization" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/org_info.html:47 +#: shiftings/organizations/templates/organizations/template/org_info.html:47 msgid "Update Shift Permissions" msgstr "" -#: src/shiftings/organizations/templates/organizations/template/organization.html:12 +#: shiftings/organizations/templates/organizations/template/organization.html:12 msgid "No Image available" msgstr "" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:6 +#: shiftings/organizations/templates/organizations/user/claim_users.html:6 msgid "Imported Users" msgstr "" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:14 +#: shiftings/organizations/templates/organizations/user/claim_users.html:14 msgid "Claimed" msgstr "" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:36 +#: shiftings/organizations/templates/organizations/user/claim_users.html:36 msgid "Claim" msgstr "" -#: src/shiftings/organizations/templates/organizations/user/claim_users.html:43 +#: shiftings/organizations/templates/organizations/user/claim_users.html:43 msgid "Unclaim" msgstr "" -#: src/shiftings/organizations/views/membership.py:55 -#: src/shiftings/organizations/views/membership_type.py:57 +#: shiftings/organizations/views/membership.py:55 +#: shiftings/organizations/views/membership_type.py:57 msgid "Membership removed" msgstr "" -#: src/shiftings/organizations/views/organization.py:62 +#: shiftings/organizations/views/organization.py:62 #, python-brace-format msgid "{org} Overview" msgstr "" -#: src/shiftings/organizations/views/organization.py:89 +#: shiftings/organizations/views/organization.py:89 #, python-brace-format msgid "{org} Administration" msgstr "" -#: src/shiftings/organizations/views/organization.py:135 +#: shiftings/organizations/views/organization.py:135 #, python-brace-format msgid "{org} Settings" msgstr "" -#: src/shiftings/organizations/views/user.py:36 +#: shiftings/organizations/views/user.py:36 msgid "You can't claim users that are already claimed." msgstr "" -#: src/shiftings/shifts/forms/filters.py:12 +#: shiftings/shifts/forms/filters.py:12 msgid "Shifts I participate in" msgstr "" -#: src/shiftings/shifts/forms/filters.py:17 -#: src/shiftings/shifts/forms/filters.py:19 +#: shiftings/shifts/forms/filters.py:17 shiftings/shifts/forms/filters.py:19 msgid "Date YYYY-MM-DD" msgstr "" -#: src/shiftings/shifts/forms/filters.py:18 -#: src/shiftings/shifts/forms/filters.py:20 +#: shiftings/shifts/forms/filters.py:18 shiftings/shifts/forms/filters.py:20 msgid "Time HH:MM" msgstr "" -#: src/shiftings/shifts/forms/participant.py:28 +#: shiftings/shifts/forms/participant.py:28 #, python-brace-format msgid "User {user} is already registered for this shift." msgstr "" -#: src/shiftings/shifts/forms/participant.py:32 +#: shiftings/shifts/forms/participant.py:32 msgid "Organization Users" msgstr "" -#: src/shiftings/shifts/forms/participant.py:33 +#: shiftings/shifts/forms/participant.py:33 msgid "Other Users" msgstr "" -#: src/shiftings/shifts/forms/participant.py:34 +#: shiftings/shifts/forms/participant.py:34 msgid "" "To add users that do not belong to your organization please enter their " "username." msgstr "" -#: src/shiftings/shifts/forms/participant.py:65 +#: shiftings/shifts/forms/participant.py:65 msgid "Only select one type of user!" msgstr "" -#: src/shiftings/shifts/forms/participant.py:75 +#: shiftings/shifts/forms/participant.py:75 msgid "One of the user fields is required!" msgstr "" -#: src/shiftings/shifts/forms/participant.py:80 +#: shiftings/shifts/forms/participant.py:80 #, python-brace-format msgid "Cannot add {user} multiple times to this shift" msgstr "" -#: src/shiftings/shifts/forms/permission.py:36 -#: src/shiftings/shifts/forms/permission.py:90 +#: shiftings/shifts/forms/permission.py:36 +#: shiftings/shifts/forms/permission.py:90 #, python-brace-format msgid "" "Permission would have no effect. Permission for all Users is more extensive: " "{permission}" msgstr "" -#: src/shiftings/shifts/forms/permission.py:47 +#: shiftings/shifts/forms/permission.py:47 #, python-brace-format msgid "" "Permission would have no effect. There is an inherited more extensive " "permission: {referred_object} ({permission})" msgstr "" -#: src/shiftings/shifts/forms/permission.py:56 +#: shiftings/shifts/forms/permission.py:56 #, python-brace-format msgid "" "Permission would have no effect. There is an inherited more extensive " "permission for all Users: {referred_object} ({permission})" msgstr "" -#: src/shiftings/shifts/forms/permission.py:78 +#: shiftings/shifts/forms/permission.py:78 #, python-brace-format msgid "Can only set one permission for organization \"{organization}\"" msgstr "" -#: src/shiftings/shifts/forms/permission.py:80 +#: shiftings/shifts/forms/permission.py:80 msgid "Can only set one permission for all Users" msgstr "" -#: src/shiftings/shifts/forms/recurring.py:13 +#: shiftings/shifts/forms/recurring.py:13 msgid "Ordinal" msgstr "" -#: src/shiftings/shifts/forms/recurring.py:35 +#: shiftings/shifts/forms/recurring.py:35 msgid "Weekday can't be empty with the chosen time frame type." msgstr "" -#: src/shiftings/shifts/forms/recurring.py:37 +#: shiftings/shifts/forms/recurring.py:37 msgid "Month can't be empty with the chosen time frame type." msgstr "" -#: src/shiftings/shifts/forms/recurring.py:39 +#: shiftings/shifts/forms/recurring.py:39 msgid "You need to add a warning for weekend handling." msgstr "" -#: src/shiftings/shifts/forms/recurring.py:41 +#: shiftings/shifts/forms/recurring.py:41 msgid "You need to add a warning for holiday handling." msgstr "" -#: src/shiftings/shifts/forms/shift.py:38 -#: src/shiftings/shifts/forms/template.py:66 +#: shiftings/shifts/forms/shift.py:41 +msgid "End time must be after start time" +msgstr "" + +#: shiftings/shifts/forms/shift.py:46 shiftings/shifts/forms/template.py:66 #, python-brace-format msgid "Shift is too long, can at most be {max} long" msgstr "" -#: src/shiftings/shifts/forms/template.py:18 +#: shiftings/shifts/forms/template.py:18 msgid "Date" msgstr "" -#: src/shiftings/shifts/forms/template.py:18 +#: shiftings/shifts/forms/template.py:18 msgid "Date to create the template on" msgstr "" -#: src/shiftings/shifts/forms/template.py:38 -#: src/shiftings/shifts/models/template.py:22 +#: shiftings/shifts/forms/template.py:38 shiftings/shifts/models/template.py:22 msgid "Start Delay" msgstr "" -#: src/shiftings/shifts/forms/template.py:58 +#: shiftings/shifts/forms/template.py:40 shiftings/shifts/models/template.py:23 +#: shiftings/shifts/templates/shifts/recurring/list.html:11 +msgid "Duration" +msgstr "" + +#: shiftings/shifts/forms/template.py:58 msgid "Start Delay was None please reload and use the slider." msgstr "" -#: src/shiftings/shifts/forms/template.py:64 +#: shiftings/shifts/forms/template.py:64 msgid "Duration was None please reload and use the slider." msgstr "" -#: src/shiftings/shifts/forms/type.py:22 +#: shiftings/shifts/forms/type.py:22 msgid "Can't name a shift type \"System\"." msgstr "" -#: src/shiftings/shifts/models/base.py:13 -#: src/shiftings/shifts/models/template.py:19 -#: src/shiftings/shifts/templates/shifts/shift.html:39 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:18 +#: shiftings/shifts/models/base.py:13 shiftings/shifts/models/template.py:19 +#: shiftings/shifts/templates/shifts/shift.html:39 +#: shiftings/shifts/templates/shifts/template/template_form.html:18 msgid "Shift Type" msgstr "" -#: src/shiftings/shifts/models/base.py:16 +#: shiftings/shifts/models/base.py:16 msgid "Required Users" msgstr "" -#: src/shiftings/shifts/models/base.py:18 -#: src/shiftings/shifts/models/template.py:27 +#: shiftings/shifts/models/base.py:18 shiftings/shifts/models/template.py:27 msgid "A maximum of 32 users can be required" msgstr "" -#: src/shiftings/shifts/models/base.py:19 +#: shiftings/shifts/models/base.py:19 msgid "Maximum Users" msgstr "" -#: src/shiftings/shifts/models/base.py:21 -#: src/shiftings/shifts/models/template.py:30 +#: shiftings/shifts/models/base.py:21 shiftings/shifts/models/template.py:30 msgid "A maximum of 64 users can be present" msgstr "" -#: src/shiftings/shifts/models/base.py:23 -#: src/shiftings/shifts/models/template.py:32 -#: src/shiftings/shifts/templates/shifts/shift.html:79 -#: src/shiftings/shifts/templates/shifts/template/template.html:26 +#: shiftings/shifts/models/base.py:23 shiftings/shifts/models/template.py:32 +#: shiftings/shifts/templates/shifts/shift.html:79 +#: shiftings/shifts/templates/shifts/template/template.html:26 msgid "Additional Infos" msgstr "" -#: src/shiftings/shifts/models/participant.py:16 +#: shiftings/shifts/models/participant.py:16 msgid "Display Name is optional, and will be shown instead of the username" msgstr "" -#: src/shiftings/shifts/models/participant.py:17 +#: shiftings/shifts/models/participant.py:17 msgid "Participation confirmed" msgstr "" -#: src/shiftings/shifts/models/participant.py:18 +#: shiftings/shifts/models/participant.py:18 msgid "" "Whether the participant has taken part in the shift or not. Default: False" msgstr "" -#: src/shiftings/shifts/models/permission.py:18 +#: shiftings/shifts/models/permission.py:18 msgid "No Permission" msgstr "" -#: src/shiftings/shifts/models/permission.py:19 +#: shiftings/shifts/models/permission.py:19 msgid "See Shift exists" msgstr "" -#: src/shiftings/shifts/models/permission.py:20 +#: shiftings/shifts/models/permission.py:20 msgid "See Shift Details" msgstr "" -#: src/shiftings/shifts/models/permission.py:21 +#: shiftings/shifts/models/permission.py:21 msgid "See Shift Participants" msgstr "" -#: src/shiftings/shifts/models/permission.py:22 +#: shiftings/shifts/models/permission.py:22 msgid "Participate" msgstr "" -#: src/shiftings/shifts/models/permission.py:80 +#: shiftings/shifts/models/permission.py:80 msgid "Permission Type" msgstr "" -#: src/shiftings/shifts/models/permission.py:82 +#: shiftings/shifts/models/permission.py:82 msgid "Affected Organization" msgstr "" -#: src/shiftings/shifts/models/recurring.py:26 +#: shiftings/shifts/models/recurring.py:26 msgid "ignore" msgstr "" -#: src/shiftings/shifts/models/recurring.py:27 +#: shiftings/shifts/models/recurring.py:27 msgid "cancel" msgstr "" -#: src/shiftings/shifts/models/recurring.py:28 +#: shiftings/shifts/models/recurring.py:28 msgid "show warning" msgstr "" -#: src/shiftings/shifts/models/recurring.py:36 +#: shiftings/shifts/models/recurring.py:36 msgid "Timeframe Type" msgstr "" -#: src/shiftings/shifts/models/recurring.py:41 -#: src/shiftings/shifts/templates/shifts/recurring/list.html:10 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:53 +#: shiftings/shifts/models/recurring.py:41 +#: shiftings/shifts/templates/shifts/recurring/list.html:10 +#: shiftings/shifts/templates/shifts/recurring/shift.html:53 msgid "First Occurrence" msgstr "" -#: src/shiftings/shifts/models/recurring.py:42 +#: shiftings/shifts/models/recurring.py:42 msgid "" "Choose a minimum day for first occurrence and the system will automatically " "choose the next applicable day." msgstr "" -#: src/shiftings/shifts/models/recurring.py:46 +#: shiftings/shifts/models/recurring.py:46 msgid "Auto create days" msgstr "" -#: src/shiftings/shifts/models/recurring.py:46 +#: shiftings/shifts/models/recurring.py:46 msgid "How many days in advance should the shifts be created?" msgstr "" -#: src/shiftings/shifts/models/recurring.py:48 +#: shiftings/shifts/models/recurring.py:48 msgid "Shift Template" msgstr "" -#: src/shiftings/shifts/models/recurring.py:52 +#: shiftings/shifts/models/recurring.py:52 msgid "Weekend problem handling" msgstr "" -#: src/shiftings/shifts/models/recurring.py:54 +#: shiftings/shifts/models/recurring.py:54 msgid "Warning text for weekend" msgstr "" -#: src/shiftings/shifts/models/recurring.py:59 +#: shiftings/shifts/models/recurring.py:59 msgid "Holidays problem handling" msgstr "" -#: src/shiftings/shifts/models/recurring.py:61 +#: shiftings/shifts/models/recurring.py:61 msgid "Warning text for holidays" msgstr "" -#: src/shiftings/shifts/models/recurring.py:67 +#: shiftings/shifts/models/recurring.py:67 msgid "Manually Disabled" msgstr "" -#: src/shiftings/shifts/models/recurring.py:88 +#: shiftings/shifts/models/recurring.py:88 msgid "Nth" msgstr "" -#: src/shiftings/shifts/models/recurring.py:89 +#: shiftings/shifts/models/recurring.py:89 msgid "[weekday]" msgstr "" -#: src/shiftings/shifts/models/recurring.py:90 +#: shiftings/shifts/models/recurring.py:90 msgid "[month]" msgstr "" -#: src/shiftings/shifts/models/shift.py:24 +#: shiftings/shifts/models/shift.py:24 msgid "Start Date and Time" msgstr "" -#: src/shiftings/shifts/models/shift.py:25 +#: shiftings/shifts/models/shift.py:25 msgid "End Date and Time" msgstr "" -#: src/shiftings/shifts/models/shift.py:27 -#: src/shiftings/shifts/templates/shifts/template/shift.html:13 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:21 +#: shiftings/shifts/models/shift.py:27 +#: shiftings/shifts/templates/shifts/template/shift.html:13 +#: shiftings/shifts/templates/shifts/template/template_form.html:21 msgid "Users" msgstr "" -#: src/shiftings/shifts/models/shift.py:30 +#: shiftings/shifts/models/shift.py:30 msgid "Locked for Participation" msgstr "" -#: src/shiftings/shifts/models/shift.py:31 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:58 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:64 +#: shiftings/shifts/models/shift.py:31 +#: shiftings/shifts/templates/shifts/recurring/shift.html:58 +#: shiftings/shifts/templates/shifts/recurring/shift.html:64 msgid "Warning" msgstr "" -#: src/shiftings/shifts/models/shift.py:35 +#: shiftings/shifts/models/shift.py:35 msgid "Created by Recurring Shift" msgstr "" -#: src/shiftings/shifts/models/shift.py:37 -#: src/shiftings/shifts/templates/shifts/shift.html:53 +#: shiftings/shifts/models/shift.py:37 +#: shiftings/shifts/templates/shifts/shift.html:53 msgid "Created" msgstr "" -#: src/shiftings/shifts/models/shift.py:38 -#: src/shiftings/shifts/templates/shifts/shift.html:55 +#: shiftings/shifts/models/shift.py:38 +#: shiftings/shifts/templates/shifts/shift.html:55 msgid "Last Modified" msgstr "" -#: src/shiftings/shifts/models/shift.py:54 +#: shiftings/shifts/models/shift.py:54 msgid "Organization and Event Organization must be identical." msgstr "" -#: src/shiftings/shifts/models/shift.py:57 +#: shiftings/shifts/models/shift.py:57 #, python-brace-format msgid "Shift {name} on {time}" msgstr "" -#: src/shiftings/shifts/models/shift.py:65 +#: shiftings/shifts/models/shift.py:65 #, python-brace-format msgid "{name} from {start} to {end} " msgstr "" -#: src/shiftings/shifts/models/shift.py:72 +#: shiftings/shifts/models/shift.py:72 #, python-brace-format msgid "{start} to {end_time} on {end_date} " msgstr "" -#: src/shiftings/shifts/models/shift.py:76 +#: shiftings/shifts/models/shift.py:76 #, python-brace-format msgid "{start} to {end_time}" msgstr "" -#: src/shiftings/shifts/models/summary.py:11 +#: shiftings/shifts/models/summary.py:11 msgid "\"Other\" Shift Type Group Name" msgstr "" -#: src/shiftings/shifts/models/summary.py:14 +#: shiftings/shifts/models/summary.py:14 msgid "Default time range for summary" msgstr "" -#: src/shiftings/shifts/models/summary.py:25 +#: shiftings/shifts/models/summary.py:25 #, python-brace-format msgid "Organization summary settings of {organization}" msgstr "" -#: src/shiftings/shifts/models/template.py:16 +#: shiftings/shifts/models/template.py:16 msgid "Template Group" msgstr "" -#: src/shiftings/shifts/models/template.py:25 +#: shiftings/shifts/models/template.py:25 msgid "Required User" msgstr "" -#: src/shiftings/shifts/models/template.py:28 +#: shiftings/shifts/models/template.py:28 msgid "Maximum User" msgstr "" -#: src/shiftings/shifts/signals.py:26 +#: shiftings/shifts/signals.py:26 msgid "Could not find a valid first occurrence." msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:11 +#: shiftings/shifts/templates/shifts/create_shift.html:11 #, python-format msgid "" "\n" @@ -3139,7 +1659,7 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:15 +#: shiftings/shifts/templates/shifts/create_shift.html:15 #, python-format msgid "" "\n" @@ -3147,70 +1667,70 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:22 -#: src/shiftings/shifts/templates/shifts/create_shift.html:83 -#: src/shiftings/shifts/templates/shifts/recurring/form.html:23 -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:19 -#: src/shiftings/templates/generic/create_or_update.html:18 -#: src/shiftings/templates/generic/form_card.html:8 +#: shiftings/shifts/templates/shifts/create_shift.html:22 +#: shiftings/shifts/templates/shifts/create_shift.html:93 +#: shiftings/shifts/templates/shifts/recurring/form.html:23 +#: shiftings/shifts/templates/shifts/recurring/shift.html:19 +#: shiftings/templates/generic/create_or_update.html:18 +#: shiftings/templates/generic/form_card.html:8 msgid "Create" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:24 +#: shiftings/shifts/templates/shifts/create_shift.html:24 msgid "Update" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:26 -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:34 -#: src/shiftings/templates/generic/create_or_update.html:22 +#: shiftings/shifts/templates/shifts/create_shift.html:26 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:34 +#: shiftings/templates/generic/create_or_update.html:22 msgid "Back" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:47 +#: shiftings/shifts/templates/shifts/create_shift.html:57 msgid "Shift starting time" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:48 +#: shiftings/shifts/templates/shifts/create_shift.html:58 msgctxt "Shift time" msgid "Start Time" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:52 +#: shiftings/shifts/templates/shifts/create_shift.html:62 msgid "Shift end time" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:53 +#: shiftings/shifts/templates/shifts/create_shift.html:63 msgctxt "Shift time" msgid "End Time" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:59 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:24 +#: shiftings/shifts/templates/shifts/create_shift.html:69 +#: shiftings/shifts/templates/shifts/template/template_form.html:24 msgid "Required number of users to fill this shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:60 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:25 +#: shiftings/shifts/templates/shifts/create_shift.html:70 +#: shiftings/shifts/templates/shifts/template/template_form.html:25 msgctxt "Shift users" msgid "Required" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:64 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:29 +#: shiftings/shifts/templates/shifts/create_shift.html:74 +#: shiftings/shifts/templates/shifts/template/template_form.html:29 msgid "Maximum number of users allowed in this shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:65 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:30 +#: shiftings/shifts/templates/shifts/create_shift.html:75 +#: shiftings/shifts/templates/shifts/template/template_form.html:30 msgctxt "Shift users" msgid "Maximum" msgstr "" -#: src/shiftings/shifts/templates/shifts/create_shift.html:76 +#: shiftings/shifts/templates/shifts/create_shift.html:86 msgid "Create from Template" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:17 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:17 #, python-format msgid "" "\n" @@ -3219,39 +1739,39 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:26 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:26 msgid "Edit Shift Permissions for" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:38 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:38 msgid "How does it work?" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:53 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:53 msgid "Inherited Permissions" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_participation_permissions.html:65 +#: shiftings/shifts/templates/shifts/edit_participation_permissions.html:65 msgid "All Users" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_templates.html:21 +#: shiftings/shifts/templates/shifts/edit_templates.html:21 msgid "Edit Templates for" msgstr "" -#: src/shiftings/shifts/templates/shifts/edit_templates.html:29 +#: shiftings/shifts/templates/shifts/edit_templates.html:29 msgid "Cancel" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/form.html:33 +#: shiftings/shifts/templates/shifts/recurring/form.html:33 msgid "Recurring Time Frame" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/form.html:42 +#: shiftings/shifts/templates/shifts/recurring/form.html:42 msgid "Shift Information" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:16 +#: shiftings/shifts/templates/shifts/recurring/shift.html:16 #, python-format msgid "" "\n" @@ -3259,11 +1779,11 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:37 +#: shiftings/shifts/templates/shifts/recurring/shift.html:37 msgid "Edit Recurring Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:44 +#: shiftings/shifts/templates/shifts/recurring/shift.html:44 #, python-format msgid "" "\n" @@ -3272,7 +1792,7 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:48 +#: shiftings/shifts/templates/shifts/recurring/shift.html:48 #, python-format msgid "" "\n" @@ -3280,44 +1800,44 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:55 +#: shiftings/shifts/templates/shifts/recurring/shift.html:55 msgid "Weekend Handling" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:61 +#: shiftings/shifts/templates/shifts/recurring/shift.html:61 msgid "Holiday Handling" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:75 +#: shiftings/shifts/templates/shifts/recurring/shift.html:75 msgid "Upcoming recurring Shifts" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:86 +#: shiftings/shifts/templates/shifts/recurring/shift.html:86 msgid "No upcoming created Shifts" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:93 +#: shiftings/shifts/templates/shifts/recurring/shift.html:93 msgid "Passed recurring Shifts" msgstr "" -#: src/shiftings/shifts/templates/shifts/recurring/shift.html:104 +#: shiftings/shifts/templates/shifts/recurring/shift.html:104 msgid "No created Shifts have passed yet" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:16 -#: src/shiftings/shifts/views/shift.py:78 +#: shiftings/shifts/templates/shifts/shift.html:16 +#: shiftings/shifts/views/shift.py:78 msgid "Edit Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:22 +#: shiftings/shifts/templates/shifts/shift.html:22 msgid "Delete Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:28 +#: shiftings/shifts/templates/shifts/shift.html:28 msgid "Edit Permissions" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:57 +#: shiftings/shifts/templates/shifts/shift.html:57 #, python-format msgid "" "\n" @@ -3325,219 +1845,219 @@ msgid "" " " msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:66 -#: src/shiftings/shifts/templates/shifts/template/shift_card.html:13 +#: shiftings/shifts/templates/shifts/shift.html:66 +#: shiftings/shifts/templates/shifts/template/shift_card.html:13 msgid "Shift Participants" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:71 +#: shiftings/shifts/templates/shifts/shift.html:71 msgid "Add participant" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift.html:84 +#: shiftings/shifts/templates/shifts/shift.html:84 msgid "Nothing to consider." msgstr "" -#: src/shiftings/shifts/templates/shifts/shift_participants.html:11 +#: shiftings/shifts/templates/shifts/shift_participants.html:11 msgid "" "This shift is over, do you really want/need to add yourself as participant?" msgstr "" -#: src/shiftings/shifts/templates/shifts/shift_url_filters.html:8 +#: shiftings/shifts/templates/shifts/shift_url_filters.html:8 msgid "Toggle Shift Filters" msgstr "" -#: src/shiftings/shifts/templates/shifts/summary/summary.html:6 +#: shiftings/shifts/templates/shifts/summary/summary.html:6 msgid "Shift Summary" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group.html:17 +#: shiftings/shifts/templates/shifts/template/group.html:17 msgid "Delete Template" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group.html:21 +#: shiftings/shifts/templates/shifts/template/group.html:21 msgid "Edit Template" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group.html:25 -#: src/shiftings/shifts/templates/shifts/template/group_template.html:13 +#: shiftings/shifts/templates/shifts/template/group.html:25 +#: shiftings/shifts/templates/shifts/template/group_template.html:13 msgid "Edit Shifts" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group.html:31 +#: shiftings/shifts/templates/shifts/template/group.html:31 msgid "Edit Template Permissions" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group.html:70 +#: shiftings/shifts/templates/shifts/template/group.html:70 msgid "Create Shift Templates" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/group_template.html:9 +#: shiftings/shifts/templates/shifts/template/group_template.html:9 msgid "Edit Shifts Template" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/member_shift_summary.html:3 +#: shiftings/shifts/templates/shifts/template/member_shift_summary.html:3 msgid "Member" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/member_shift_summary.html:10 +#: shiftings/shifts/templates/shifts/template/member_shift_summary.html:10 msgid "Total" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/participation_permission_form.html:9 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:10 +#: shiftings/shifts/templates/shifts/template/participation_permission_form.html:9 +#: shiftings/shifts/templates/shifts/template/template_form.html:10 msgid "Remove Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/participation_permission_form.html:12 -#: src/shiftings/shifts/templates/shifts/template/template_form.html:13 +#: shiftings/shifts/templates/shifts/template/participation_permission_form.html:12 +#: shiftings/shifts/templates/shifts/template/template_form.html:13 msgid "Restore Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/select_org.html:3 +#: shiftings/shifts/templates/shifts/template/select_org.html:3 msgid "Select Organization to create Shift for" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift.html:5 +#: shiftings/shifts/templates/shifts/template/shift.html:5 msgid "Go to Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:5 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:5 msgid "Filters" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:8 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:8 msgid "Reset Filter" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:12 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:12 msgid "Apply Filter" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:15 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:15 msgid "Parameters" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:29 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:29 msgid "Select Organizations" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:47 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:47 msgid "Shift starts after" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:52 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:52 msgid "Shift ends before" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:65 +#: shiftings/shifts/templates/shifts/template/shift_url_filter_form.html:65 msgid "Select Events" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:4 +#: shiftings/shifts/templates/shifts/template/slots_display.html:4 msgid "Required to carry out this Shift" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:6 +#: shiftings/shifts/templates/shifts/template/slots_display.html:6 msgid "Open Shift Slot" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/slots_display.html:32 +#: shiftings/shifts/templates/shifts/template/slots_display.html:32 msgid "Free" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:20 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:20 msgid "Required" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:23 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:23 msgid "Confirmed" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:29 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:29 msgid "Participating" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:34 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:34 msgid "Full" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:40 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:40 msgid "Additional Users required" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/small_shift_display.html:42 +#: shiftings/shifts/templates/shifts/template/small_shift_display.html:42 msgid "Additional Slots available" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template.html:11 +#: shiftings/shifts/templates/shifts/template/template.html:11 msgid "Calendar Background Color" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template.html:17 +#: shiftings/shifts/templates/shifts/template/template.html:17 msgid "Required Users/Max Users" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template.html:21 +#: shiftings/shifts/templates/shifts/template/template.html:21 msgid "Unlimited" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:36 +#: shiftings/shifts/templates/shifts/template/template_form.html:36 msgid "Delay to starting time" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:37 +#: shiftings/shifts/templates/shifts/template/template_form.html:37 msgctxt "Shift time" msgid "Start Delay" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:41 +#: shiftings/shifts/templates/shifts/template/template_form.html:41 msgid "Shift Duration" msgstr "" -#: src/shiftings/shifts/templates/shifts/template/template_form.html:42 +#: shiftings/shifts/templates/shifts/template/template_form.html:42 msgctxt "Shift time" msgid "Duration" msgstr "" -#: src/shiftings/shifts/templates/shifts/type_group/list.html:8 +#: shiftings/shifts/templates/shifts/type_group/list.html:8 msgid "Actions" msgstr "" -#: src/shiftings/shifts/templatetags/shifts.py:90 +#: shiftings/shifts/templatetags/shifts.py:90 #, python-brace-format msgid "Days + {delta_days}" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:17 +#: shiftings/shifts/utils/time_frame.py:17 msgid "Nth [weekday] of each month" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:18 +#: shiftings/shifts/utils/time_frame.py:18 msgid "Nth day of each month" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:19 +#: shiftings/shifts/utils/time_frame.py:19 msgid "every Nth [weekday]" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:20 +#: shiftings/shifts/utils/time_frame.py:20 msgid "Nth workday of each month" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:21 +#: shiftings/shifts/utils/time_frame.py:21 msgid "Nth day of [month]" msgstr "" -#: src/shiftings/shifts/utils/time_frame.py:22 +#: shiftings/shifts/utils/time_frame.py:22 msgid "Nth workday of [month]" msgstr "" -#: src/shiftings/shifts/views/participant.py:42 +#: shiftings/shifts/views/participant.py:42 msgid "This shift is over, do you really want/need to add a participant?" msgstr "" -#: src/shiftings/shifts/views/permission.py:50 +#: shiftings/shifts/views/permission.py:50 #, python-brace-format msgid "" "

Any Permission present will be applied to all Shifts belonging to " @@ -3547,35 +2067,36 @@ msgid "" "organizations.

" msgstr "" -#: src/shiftings/shifts/views/recurring.py:85 +#: shiftings/shifts/views/recurring.py:85 msgid "Forbidden Method GET" msgstr "" -#: src/shiftings/shifts/views/recurring.py:98 +#: shiftings/shifts/views/recurring.py:98 +#, python-brace-format msgid "Created Shifts on {}" msgstr "" -#: src/shiftings/shifts/views/recurring.py:102 +#: shiftings/shifts/views/recurring.py:102 msgid "Error while creating shifts. Invalid Date" msgstr "" -#: src/shiftings/shifts/views/shift.py:164 +#: shiftings/shifts/views/shift.py:164 msgid "Unable to delete past shifts." msgstr "" -#: src/shiftings/templates/403.html:6 +#: shiftings/templates/403.html:6 msgid "You don't have the permission to do that!" msgstr "" -#: src/shiftings/templates/404.html:6 +#: shiftings/templates/404.html:6 msgid "The site you requested was not found." msgstr "" -#: src/shiftings/templates/500.html:6 +#: shiftings/templates/500.html:6 msgid "A server error occured the admins have been notified" msgstr "" -#: src/shiftings/templates/generic/create_or_update.html:5 +#: shiftings/templates/generic/create_or_update.html:5 #, python-format msgid "" "\n" @@ -3583,7 +2104,7 @@ msgid "" " " msgstr "" -#: src/shiftings/templates/generic/create_or_update.html:9 +#: shiftings/templates/generic/create_or_update.html:9 #, python-format msgid "" "\n" @@ -3591,7 +2112,7 @@ msgid "" " " msgstr "" -#: src/shiftings/templates/generic/delete.html:6 +#: shiftings/templates/generic/delete.html:6 #, python-format msgid "" "\n" @@ -3599,53 +2120,66 @@ msgid "" " " msgstr "" -#: src/shiftings/templates/generic/modular_search.html:4 +#: shiftings/templates/generic/delete.html:9 +msgid "Yes" +msgstr "" + +#: shiftings/templates/generic/delete.html:11 +msgid "No" +msgstr "" + +#: shiftings/templates/generic/modular_search.html:4 msgid "Filter by Name" msgstr "" -#: src/shiftings/templates/generic/modular_search.html:6 +#: shiftings/templates/generic/modular_search.html:6 msgid "submit search" msgstr "" -#: src/shiftings/templates/language.html:16 +#: shiftings/templates/language.html:16 msgid "Language" msgstr "" -#: src/shiftings/templates/language.html:21 +#: shiftings/templates/language.html:21 msgid "German" msgstr "" -#: src/shiftings/templates/language.html:24 +#: shiftings/templates/language.html:24 msgid "English" msgstr "" -#: src/shiftings/templates/month_view.html:2 +#: shiftings/templates/local/gdpr_template.sample.html:29 +#: shiftings/templates/template/remove_modal.html:39 +#: shiftings/templates/template/simple_display_modal.html:15 +#: shiftings/templates/template/simple_form_modal.html:22 +msgid "Close" +msgstr "" + +#: shiftings/templates/month_view.html:2 msgid "M" msgstr "" -#: src/shiftings/templates/month_view.html:3 -#: src/shiftings/templates/month_view.html:5 +#: shiftings/templates/month_view.html:3 shiftings/templates/month_view.html:5 msgid "T" msgstr "" -#: src/shiftings/templates/month_view.html:4 +#: shiftings/templates/month_view.html:4 msgid "W" msgstr "" -#: src/shiftings/templates/month_view.html:6 +#: shiftings/templates/month_view.html:6 msgid "F" msgstr "" -#: src/shiftings/templates/month_view.html:7 -#: src/shiftings/templates/month_view.html:8 +#: shiftings/templates/month_view.html:7 shiftings/templates/month_view.html:8 msgid "S" msgstr "" -#: src/shiftings/templates/switch_user.html:5 +#: shiftings/templates/switch_user.html:5 msgid "Switch User" msgstr "" -#: src/shiftings/templates/template/remove_modal.html:12 +#: shiftings/templates/template/remove_modal.html:12 #, python-format msgid "" "\n" @@ -3654,7 +2188,7 @@ msgid "" " " msgstr "" -#: src/shiftings/templates/template/remove_modal.html:16 +#: shiftings/templates/template/remove_modal.html:16 msgid "" "\n" " Removing str: class User(BaseUser): + class ThemePreference(models.TextChoices): + LIGHT = 'light', _('Light') + DARK = 'dark', _('Dark') + AUTO = 'auto', _('Auto') + display_name = models.CharField(max_length=150, verbose_name=_('Display Name'), null=True, blank=True) phone_number = PhoneNumberField(verbose_name=_('Telephone Number'), blank=True, null=True) + theme_preference = models.CharField(max_length=5, + choices=ThemePreference.choices, + default=ThemePreference.AUTO, + verbose_name=_('Theme Preference')) class Meta: default_permissions = () - ordering = ['username'] + ordering = ['display_name', 'username'] def __str__(self): return self.display diff --git a/src/shiftings/accounts/templates/accounts/confirm_email.html b/src/shiftings/accounts/templates/accounts/confirm_email.html index 5343e7b..cf78b41 100644 --- a/src/shiftings/accounts/templates/accounts/confirm_email.html +++ b/src/shiftings/accounts/templates/accounts/confirm_email.html @@ -9,4 +9,4 @@ -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/accounts/templates/accounts/login_form.html b/src/shiftings/accounts/templates/accounts/login_form.html index 22df72d..4f3d068 100644 --- a/src/shiftings/accounts/templates/accounts/login_form.html +++ b/src/shiftings/accounts/templates/accounts/login_form.html @@ -1,6 +1,6 @@ {% extends "generic/form_base.html" %} {% block form %} -
+ {% if feature.gdpr_template %} -
+ {% endif %} {% if is_local and feature.registration %} -
+ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/accounts/templates/accounts/login_multiple.html b/src/shiftings/accounts/templates/accounts/login_multiple.html index 69d3260..fb9b056 100644 --- a/src/shiftings/accounts/templates/accounts/login_multiple.html +++ b/src/shiftings/accounts/templates/accounts/login_multiple.html @@ -8,7 +8,7 @@

{% trans "Welcome to Shiftings" %}

- \ No newline at end of file +
diff --git a/src/shiftings/cal/templates/cal/template/day_calendar_xs.html b/src/shiftings/cal/templates/cal/template/day_calendar_xs.html index 4252685..438ec2a 100644 --- a/src/shiftings/cal/templates/cal/template/day_calendar_xs.html +++ b/src/shiftings/cal/templates/cal/template/day_calendar_xs.html @@ -6,7 +6,7 @@
{% endif %}
- +
@@ -91,8 +91,8 @@ {% endif %} {% empty %} - {% endfor %} @@ -113,4 +113,4 @@

{% trans "No Shifts found with these parameters or planned on this day" %} {% endif %} - \ No newline at end of file + diff --git a/src/shiftings/cal/templates/cal/week_calendar.html b/src/shiftings/cal/templates/cal/week_calendar.html index 8cfea8f..23d813b 100644 --- a/src/shiftings/cal/templates/cal/week_calendar.html +++ b/src/shiftings/cal/templates/cal/week_calendar.html @@ -4,19 +4,13 @@
- + {% trans "Previous Week" as previous_week_label %} + {% include "cal/week_nav_link.html" with href=last_url label=previous_week_label icon_class="fa-solid fa-angles-left" icon_before=True %}

{% trans "Week" %} {{ week|date:"W Y" }}

- + {% trans "Next Week" as next_week_label %} + {% include "cal/week_nav_link.html" with href=next_url label=next_week_label icon_class="fa-solid fa-angles-right" icon_before=False %}
@@ -74,4 +68,4 @@
{% trans day_name %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/cal/templates/cal/week_nav_link.html b/src/shiftings/cal/templates/cal/week_nav_link.html new file mode 100644 index 0000000..3f2f060 --- /dev/null +++ b/src/shiftings/cal/templates/cal/week_nav_link.html @@ -0,0 +1,9 @@ + diff --git a/src/shiftings/cal/views/day_calendar.py b/src/shiftings/cal/views/day_calendar.py index bcd6d1f..28c01f9 100644 --- a/src/shiftings/cal/views/day_calendar.py +++ b/src/shiftings/cal/views/day_calendar.py @@ -10,8 +10,9 @@ from shiftings.cal.forms.day_form import SelectDayForm from shiftings.cal.views.calendar_base import CalendarBaseView +from shiftings.cal.views.helpers import build_shift_type_index, get_shifts_for_date from shiftings.shifts.forms.participant import AddSelfParticipantForm -from shiftings.shifts.models import Shift, ShiftType +from shiftings.shifts.models import Shift class DayView(CalendarBaseView, ABC): @@ -60,9 +61,7 @@ class DetailDayView(DayView): } def get_shifts(self, theday: date) -> Any: - shift_filter = (Q(start__date=theday) | Q(end__date=theday, end__gt=theday) | - Q(start__lt=theday, end__gt=theday)) - shift_filter &= self.get_filters() + shift_filter = get_shifts_for_date(theday) & self.get_filters() shifts = Shift.objects.filter(shift_filter).order_by('start', 'end', 'shift_type') return [shift for shift in shifts if shift.can_see(self.request.user)] @@ -77,18 +76,4 @@ def get_shifts(self, theday: date): shift_filter = Q(start__date=theday) & self.get_filters() shifts = Shift.objects.filter(shift_filter).order_by('start', 'shift_type') shifts = [shift for shift in shifts if shift.can_see_details(self.request.user)] - add_default = False - shift_idx_type = { - 'time_containers': {}, - 'types': list(ShiftType.objects.filter(shift__in=shifts).distinct()) - } - for shift in shifts: - if shift.shift_type is None: - add_default = True - type_name = 'Default' - else: - type_name = shift.shift_type.name - shift_idx_type['time_containers'].setdefault(shift.start.hour, {}).setdefault(type_name, []).append(shift) - if add_default: - shift_idx_type['types'].append(None) - return shift_idx_type + return build_shift_type_index(shifts) diff --git a/src/shiftings/cal/views/helpers.py b/src/shiftings/cal/views/helpers.py new file mode 100644 index 0000000..af2f994 --- /dev/null +++ b/src/shiftings/cal/views/helpers.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from datetime import date +from typing import Any + +from django.db.models import Q + +from shiftings.shifts.models import Shift, ShiftType + + +def get_shifts_for_date(target_date: date) -> Q: + """Build Q filter for shifts that overlap with the given date. + + Matches shifts that: + - Start on target_date + - End on target_date (and extends past it) + - Span across target_date + """ + return (Q(start__date=target_date) | Q(end__date=target_date, end__gt=target_date) | + Q(start__lt=target_date, end__gt=target_date)) + + +def build_shift_type_index(shifts: list[Shift]) -> dict[str, Any]: + add_default = False + shift_idx_type = { + 'time_containers': {}, + 'types': list(ShiftType.objects.filter(shift__in=shifts).distinct()) + } + for shift in shifts: + if shift.shift_type is None: + add_default = True + type_name = 'Default' + else: + type_name = shift.shift_type.name + shift_idx_type['time_containers'].setdefault(shift.start.hour, {}).setdefault(type_name, []).append(shift) + if add_default: + shift_idx_type['types'].append(None) + return shift_idx_type diff --git a/src/shiftings/cal/views/list.py b/src/shiftings/cal/views/list.py index cce6c08..58379b2 100644 --- a/src/shiftings/cal/views/list.py +++ b/src/shiftings/cal/views/list.py @@ -11,8 +11,9 @@ from shiftings.cal.forms.day_form import SelectDayForm from shiftings.cal.views.calendar_base import CalendarBaseView +from shiftings.cal.views.helpers import build_shift_type_index from shiftings.shifts.forms.participant import AddSelfParticipantForm -from shiftings.shifts.models import Shift, ShiftType +from shiftings.shifts.models import Shift class ListView(CalendarBaseView, ABC): @@ -70,18 +71,4 @@ def get_shifts(self): shift_filter = self.get_filters() shifts = Shift.objects.filter(shift_filter).order_by('start', 'shift_type')[:settings.MAX_LIST_ENTRIES] shifts = [shift for shift in shifts if shift.can_see_details(self.request.user)] - add_default = False - shift_idx_type = { - 'time_containers': {}, - 'types': list(ShiftType.objects.filter(shift__in=shifts).distinct()) - } - for shift in shifts: - if shift.shift_type is None: - add_default = True - type_name = 'Default' - else: - type_name = shift.shift_type.name - shift_idx_type['time_containers'].setdefault(shift.start.hour, {}).setdefault(type_name, []).append(shift) - if add_default: - shift_idx_type['types'].append(None) - return shift_idx_type + return build_shift_type_index(shifts) diff --git a/src/shiftings/cal/views/month_calendar.py b/src/shiftings/cal/views/month_calendar.py index 407c7c0..982f7c4 100644 --- a/src/shiftings/cal/views/month_calendar.py +++ b/src/shiftings/cal/views/month_calendar.py @@ -12,6 +12,7 @@ from shiftings.accounts.models import User from shiftings.cal.views.calendar_base import CalendarBaseView +from shiftings.cal.views.helpers import get_shifts_for_date from shiftings.shifts.models import RecurringShift, Shift @@ -64,9 +65,7 @@ def __init__(self, _date: date, user: User, request, shift_filter) -> None: self.shift_filter = shift_filter def get_shifts(self, _date: date) -> Union[List[Shift], QuerySet[Shift]]: - - time_filter = (Q(start__date=_date) | Q(end__date=_date, end__gt=_date) | - Q(start__lt=_date, end__gt=_date)) + time_filter = get_shifts_for_date(_date) return Shift.objects.filter(self.shift_filter & time_filter).order_by('start', 'end') def can_see_shift(self, shift: Shift) -> bool: diff --git a/src/shiftings/cal/views/week_calendar.py b/src/shiftings/cal/views/week_calendar.py index ee3fe77..50cf9e5 100644 --- a/src/shiftings/cal/views/week_calendar.py +++ b/src/shiftings/cal/views/week_calendar.py @@ -6,6 +6,7 @@ from django.db.models import Q from shiftings.cal.views.calendar_base import CalendarBaseView +from shiftings.cal.views.helpers import get_shifts_for_date from shiftings.events.models import Event from shiftings.organizations.models import Organization from shiftings.shifts.models import Shift @@ -17,9 +18,7 @@ class WeekView(CalendarBaseView): def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context = super().get_context_data(**kwargs) theday = date.fromisoformat(self.kwargs.get('theday')) if 'theday' in self.kwargs else date.today() - shift_filter = (Q(start__date=theday) | Q(end__date=theday) | - Q(start__lt=theday, end__gt=theday)) - shift_filter &= self.get_filters() + shift_filter = get_shifts_for_date(theday) & self.get_filters() shifts = Shift.objects.filter(shift_filter).order_by('shift_type', 'start', 'end') context.update({ 'theday': theday, diff --git a/src/shiftings/events/admin.py b/src/shiftings/events/admin.py index a69763b..dcd36a1 100644 --- a/src/shiftings/events/admin.py +++ b/src/shiftings/events/admin.py @@ -1,5 +1,4 @@ -from django.contrib import admin - from shiftings.events.models import Event +from shiftings.utils.admin import register_models -admin.site.register(Event) +register_models(Event) diff --git a/src/shiftings/events/tests/test_event_forms.py b/src/shiftings/events/tests/test_event_forms.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shiftings/events/urls/event.py b/src/shiftings/events/urls/event.py index 960dc2a..2918f45 100644 --- a/src/shiftings/events/urls/event.py +++ b/src/shiftings/events/urls/event.py @@ -3,6 +3,7 @@ from shiftings.cal.feed.event import EventFeed, PublicEventsFeed from shiftings.events.views.event import EventDetailView, EventEditView, EventListView, FutureEventsListView, \ MyEventsListView, MyFutureEventsListView +from shiftings.utils.url_patterns import organization_crud_paths urlpatterns = [ path('', EventListView.as_view(), name='events'), @@ -11,7 +12,8 @@ path('my_upcoming/', MyFutureEventsListView.as_view(), name='my_future_events'), path('calendar/', PublicEventsFeed(), name='public_events_calendar'), path('/', EventDetailView.as_view(), name='event'), - path('create//', EventEditView.as_view(), name='event_create'), - path('/update/', EventEditView.as_view(), name='event_update'), path('/calendar/', EventFeed(), name='event_calendar'), -] +] + organization_crud_paths( + edit_view=EventEditView, + name_prefix='event', +) diff --git a/src/shiftings/events/views/event.py b/src/shiftings/events/views/event.py index 6a8772b..c153a17 100644 --- a/src/shiftings/events/views/event.py +++ b/src/shiftings/events/views/event.py @@ -12,7 +12,7 @@ from shiftings.events.forms.event import EventForm from shiftings.events.models import Event from shiftings.organizations.models import Organization -from shiftings.organizations.views.organization_base import OrganizationPermissionMixin +from shiftings.organizations.views.organization_base import OrganizationCreateUpdateMixin, OrganizationPermissionMixin from shiftings.utils.typing import UserRequest from shiftings.utils.views.base import BaseMixin, BasePermissionMixin from shiftings.utils.views.create_update_view import CreateOrUpdateViewWithImageUpload @@ -83,24 +83,15 @@ class EventDetailView(EventMixin, DetailView): context_object_name = 'event' -class EventEditView(BasePermissionMixin, CreateOrUpdateViewWithImageUpload): +class EventEditView(OrganizationCreateUpdateMixin, BasePermissionMixin, CreateOrUpdateViewWithImageUpload): model = Event form_class = EventForm permission_required = 'organizations.edit_events' - - def get_initial(self) -> dict[str, Any]: - initial = super().get_initial() - initial['organization'] = self.get_organization() - return initial + set_organization_initial_on_create_only = False def get_event(self) -> Event: return self._get_object(Event, 'pk') - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.get_event().organization - def get_obj(self) -> Optional[Event]: if self.is_create(): return None diff --git a/src/shiftings/mail/tests/test_mail_forms.py b/src/shiftings/mail/tests/test_mail_forms.py new file mode 100644 index 0000000..ea46a34 --- /dev/null +++ b/src/shiftings/mail/tests/test_mail_forms.py @@ -0,0 +1,66 @@ +from django.core.files.uploadedfile import SimpleUploadedFile +from django.test import TestCase +from django.utils.datastructures import MultiValueDict + +from shiftings.mail.forms.mail import MailForm, OrganizationMailForm, ShiftParticipantMailForm +from shiftings.mail.settings import MAX_ATTACHMENT_SIZE_MB, MAX_TOTAL_ATTACHMENT_SIZE_MB +from shiftings.organizations.models import Organization + + +class MailFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def _mb_file(self, name: str, size_mb: int) -> SimpleUploadedFile: + return SimpleUploadedFile(name, b'x' * (size_mb * 1024 * 1024)) + + def test_single_attachment_size_limit(self) -> None: + form = MailForm( + data={'subject': 'Subject', 'text': 'Body'}, + files=MultiValueDict({'attachments': [self._mb_file('too-large.txt', MAX_ATTACHMENT_SIZE_MB + 1)]}), + ) + + self.assertFalse(form.is_valid()) + self.assertIn('attachments', form.errors) + + def test_total_attachment_size_limit(self) -> None: + half = MAX_TOTAL_ATTACHMENT_SIZE_MB // 2 + form = MailForm( + data={'subject': 'Subject', 'text': 'Body'}, + files=MultiValueDict({'attachments': [ + self._mb_file('a.txt', half + 1), + self._mb_file('b.txt', half + 1), + ]}), + ) + + self.assertFalse(form.is_valid()) + self.assertIn('attachments', form.errors) + + def test_organization_mail_form_uses_organization_membership_types(self) -> None: + organization = Organization.objects.get(pk=1) + + form = OrganizationMailForm(organization=organization) + + self.assertEqual( + set(form.fields['membership_types'].queryset.values_list('pk', flat=True)), + set(organization.membership_types.values_list('pk', flat=True)), + ) + + +class ShiftParticipantMailFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def test_rejects_start_after_end(self) -> None: + organization = Organization.objects.get(pk=1) + form = ShiftParticipantMailForm( + organization=organization, + data={ + 'subject': 'Subject', + 'text': 'Body', + 'start': '2024-01-02 12:00:00', + 'end': '2024-01-01 12:00:00', + 'shift_types': [], + }, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('__all__', form.errors) diff --git a/src/shiftings/organizations/admin.py b/src/shiftings/organizations/admin.py index c571d5c..d14435d 100644 --- a/src/shiftings/organizations/admin.py +++ b/src/shiftings/organizations/admin.py @@ -1,8 +1,9 @@ -from django.contrib import admin - from shiftings.organizations.models import Membership, MembershipType, Organization, OrganizationDummyUser +from shiftings.utils.admin import register_models -admin.site.register(Organization) -admin.site.register(Membership) -admin.site.register(MembershipType) -admin.site.register(OrganizationDummyUser) +register_models( + Organization, + Membership, + MembershipType, + OrganizationDummyUser, +) diff --git a/src/shiftings/organizations/models/organization.py b/src/shiftings/organizations/models/organization.py index eb2634c..317aa9e 100644 --- a/src/shiftings/organizations/models/organization.py +++ b/src/shiftings/organizations/models/organization.py @@ -27,7 +27,7 @@ class Organization(models.Model): email = models.EmailField(verbose_name=_('E-Mail'), blank=True, null=True) telephone_number = PhoneNumberField(verbose_name=_('Telephone Number'), blank=True, null=True) website = models.URLField(verbose_name=_('Website'), blank=True, null=True, - help_text=_('Include Protocol i.E. https://example.com')) + help_text=_('Include protocol, e.g.') + ' https://example.com') description = models.TextField(max_length=1000, verbose_name=_('Description'), blank=True, null=True, help_text=_('A maximum of {amount} characters is allowed').format(amount=1000)) diff --git a/src/shiftings/organizations/templates/organizations/organization_admin.html b/src/shiftings/organizations/templates/organizations/organization_admin.html index 659dbe3..c17f8ad 100644 --- a/src/shiftings/organizations/templates/organizations/organization_admin.html +++ b/src/shiftings/organizations/templates/organizations/organization_admin.html @@ -295,11 +295,8 @@
- - - + {% url 'shift_type_delete' shift_type.pk as shift_type_delete_url %} + {% include "template/remove_modal_trigger.html" with tag="a" button_class="btn btn-sm btn-danger ms-1" href="#confirmRemoveModal" remove_name=shift_type.name remove_url=shift_type_delete_url icon_class="fas fa-trash" %} {% endfor %} @@ -419,4 +416,4 @@
{% trans "Shifts" %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/organizations/templates/organizations/organization_settings.html b/src/shiftings/organizations/templates/organizations/organization_settings.html index 54f7b68..6d616f0 100644 --- a/src/shiftings/organizations/templates/organizations/organization_settings.html +++ b/src/shiftings/organizations/templates/organizations/organization_settings.html @@ -61,11 +61,8 @@

{% trans "Shift Type Groups" %}

title="{% trans "Edit Type Group" %}"> - - - + {% url 'shift_type_group_remove' type_group.pk as shift_type_group_remove_url %} + {% include "template/remove_modal_trigger.html" with tag="a" button_class="btn btn-danger ms-1" href="#confirmRemoveModal" remove_name=type_group.name remove_url=shift_type_group_remove_url icon_class="fas fa-trash" %} {% if not forloop.first %}
{% csrf_token %} @@ -97,4 +94,4 @@

{% trans "Shift Type Groups" %}

{% trans "Organization" %} -

{% trans "No Shifts found with these parameters or planned on this day" %}

+
+

{% trans "No Shifts found with these parameters or planned on this day" %}

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/organizations/templates/organizations/organization_shifts.html b/src/shiftings/organizations/templates/organizations/organization_shifts.html index d2767ba..307c12b 100644 --- a/src/shiftings/organizations/templates/organizations/organization_shifts.html +++ b/src/shiftings/organizations/templates/organizations/organization_shifts.html @@ -13,7 +13,7 @@

{% trans "Member Shift Summary" %} ({{ organization.summary_settings.default

- {% member_shift_summary organization False False %} + {% shift_users_summary organization False False %}
{% endif %} diff --git a/src/shiftings/organizations/templates/organizations/template/member.html b/src/shiftings/organizations/templates/organizations/template/member.html index 2465291..89b6e00 100644 --- a/src/shiftings/organizations/templates/organizations/template/member.html +++ b/src/shiftings/organizations/templates/organizations/template/member.html @@ -5,12 +5,9 @@ {% if org_perms.edit_members %} {% if org_perms.admin or not membership_type.admin %} - + {% url 'membership_remove' organization.pk member.pk as membership_remove_url %} + {% include "template/remove_modal_trigger.html" with tag="button" button_class="btn btn-danger px-2 py-0 ms-2 me-4" remove_name=member remove_url=membership_remove_url icon_class="fas fa-x pe-none" %} {% endif %} {% endif %} - \ No newline at end of file + diff --git a/src/shiftings/organizations/templates/organizations/template/member_group.html b/src/shiftings/organizations/templates/organizations/template/member_group.html index fdfaac2..37d3c0c 100644 --- a/src/shiftings/organizations/templates/organizations/template/member_group.html +++ b/src/shiftings/organizations/templates/organizations/template/member_group.html @@ -1,4 +1,4 @@ -
+
{% if org_perms.edit_members %} {% if org_perms.admin or not membership_type.admin %} - + {% url 'membership_remove' organization.pk member.pk as membership_remove_url %} + {% include "template/remove_modal_trigger.html" with tag="button" button_class="btn btn-danger py-0 px-2 me-2" remove_name=member_group remove_url=membership_remove_url icon_class="fas fa-x pe-none" %} {% endif %} {% endif %}
@@ -35,4 +32,4 @@
- \ No newline at end of file + diff --git a/src/shiftings/organizations/tests/test_membership_forms.py b/src/shiftings/organizations/tests/test_membership_forms.py new file mode 100644 index 0000000..34e935c --- /dev/null +++ b/src/shiftings/organizations/tests/test_membership_forms.py @@ -0,0 +1,69 @@ +from django.core.exceptions import ValidationError +from django.test import TestCase + +from shiftings.accounts.models import User +from shiftings.organizations.forms.membership import MembershipForm +from shiftings.organizations.models import Membership, MembershipType, Organization + + +class MembershipFormTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.admin_user = User.objects.get(username='bob') + self.member_user = User.objects.get(username='perry') + self.target_user = User.objects.get(username='carla') + self.non_admin_user = User.objects.create_user(username='membership-form-user', password='secret') + + def test_non_admin_cannot_select_admin_membership_type(self) -> None: + form = MembershipForm( + user=self.non_admin_user, + initial={'organization': self.organization}, + ) + + type_pks = set(form.fields['type'].queryset.values_list('pk', flat=True)) + self.assertNotIn(MembershipType.objects.get(organization=self.organization, admin=True).pk, type_pks) + + def test_clean_user_returns_user_object_for_existing_username(self) -> None: + form = MembershipForm( + user=self.admin_user, + data={ + 'organization': self.organization.pk, + 'type': MembershipType.objects.get(organization=self.organization, default=True).pk, + 'user': self.target_user.username, + 'group': '', + }, + initial={'organization': self.organization}, + ) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['user'], self.target_user) + + def test_clean_user_rejects_unknown_username(self) -> None: + form = MembershipForm( + user=self.admin_user, + data={ + 'organization': self.organization.pk, + 'type': MembershipType.objects.get(organization=self.organization, default=True).pk, + 'user': 'missing-user', + 'group': '', + }, + initial={'organization': self.organization}, + ) + + self.assertFalse(form.is_valid()) + self.assertEqual(form.errors['user'], ['The user you entered could not be found.']) + + +class MembershipModelConstraintTest(TestCase): + fixtures = ['user', 'organization'] + + def test_membership_requires_user_or_group(self) -> None: + organization = Organization.objects.get(pk=1) + membership_type = MembershipType.objects.get(organization=organization, default=True) + + membership = Membership(organization=organization, type=membership_type) + + with self.assertRaises(ValidationError): + membership.full_clean() \ No newline at end of file diff --git a/src/shiftings/organizations/tests/test_membership_views.py b/src/shiftings/organizations/tests/test_membership_views.py new file mode 100644 index 0000000..0536cc6 --- /dev/null +++ b/src/shiftings/organizations/tests/test_membership_views.py @@ -0,0 +1,114 @@ +from django.contrib.messages import get_messages +from django.test import RequestFactory, TestCase +from django.urls import reverse + +from shiftings.accounts.models import User +from shiftings.organizations.models import Membership, MembershipType, Organization +from shiftings.organizations.views.membership import MembershipAddView +from shiftings.organizations.views.membership_type import MembershipTypeEditView, MembershipTypeRemoveView + + +class MembershipViewTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.factory = RequestFactory() + self.organization = Organization.objects.get(pk=1) + self.user = User.objects.get(username='elliot') + + def _setup_view(self, view, **kwargs): + request = self.factory.get('/organizations/test/') + request.user = self.user + view.setup(request, **kwargs) + view.request = request + view.kwargs = kwargs + return view + + def test_membership_add_view_resolves_organization_and_redirect(self) -> None: + view = self._setup_view(MembershipAddView(), org_pk=self.organization.pk) + + self.assertEqual(view.get_organization(), self.organization) + self.assertEqual( + view.get_success_url(), + reverse('organization_admin', args=[self.organization.pk]), + ) + + def test_membership_type_edit_redirects_to_organization_admin(self) -> None: + view = self._setup_view(MembershipTypeEditView(), org_pk=self.organization.pk) + + self.assertEqual( + view.get_success_url(), + reverse('organization_admin', args=[self.organization.pk]), + ) + + def test_membership_type_remove_redirects_to_organization_overview(self) -> None: + membership_type = MembershipType.objects.filter(organization=self.organization).first() + self.assertIsNotNone(membership_type) + + view = self._setup_view( + MembershipTypeRemoveView(), + org_pk=self.organization.pk, + member_pk=membership_type.pk, + ) + + self.assertEqual(view.get_success_url(), self.organization.get_absolute_url()) + + +class MembershipEndpointIntegrationTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.admin_user = User.objects.get(username='bob') + self.target_user = User.objects.get(username='perry') + self.client.force_login(self.admin_user) + + def test_add_member_endpoint_creates_membership(self) -> None: + Membership.objects.filter(organization=self.organization, user=self.target_user).delete() + default_membership_type = self.organization.default_membership_type + + response = self.client.post( + reverse('membership_add_member', kwargs={'org_pk': self.organization.pk}), + { + 'organization': self.organization.pk, + 'type': default_membership_type.pk, + 'user': self.target_user.username, + 'group': '', + }, + ) + + self.assertEqual(response.status_code, 302) + self.assertRedirects( + response, + reverse('organization_admin', args=[self.organization.pk]), + fetch_redirect_response=False, + ) + self.assertTrue( + Membership.objects.filter( + organization=self.organization, + type=default_membership_type, + user=self.target_user, + ).exists() + ) + + def test_remove_member_endpoint_deletes_membership_and_sets_success_message(self) -> None: + default_membership_type = self.organization.default_membership_type + membership = Membership.objects.create( + organization=self.organization, + type=default_membership_type, + user=self.target_user, + ) + + response = self.client.post( + reverse( + 'membership_remove', + kwargs={'org_pk': self.organization.pk, 'member_pk': membership.pk}, + ), + follow=True, + ) + + self.assertEqual(response.status_code, 200) + self.assertFalse(Membership.objects.filter(pk=membership.pk).exists()) + self.assertEqual(response.redirect_chain[0][0], reverse('organization_admin', args=[self.organization.pk])) + messages = list(get_messages(response.wsgi_request)) + self.assertTrue(any(str(message) == 'Membership removed' for message in messages)) diff --git a/src/shiftings/organizations/views/membership.py b/src/shiftings/organizations/views/membership.py index f7e0905..2d1f029 100644 --- a/src/shiftings/organizations/views/membership.py +++ b/src/shiftings/organizations/views/membership.py @@ -1,30 +1,20 @@ from typing import Any -from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin -from django.http import HttpResponse -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from django.views.generic import DeleteView from django.views.generic.edit import FormMixin from shiftings.organizations.forms.membership import MembershipForm -from shiftings.organizations.models import Membership, Organization -from shiftings.organizations.views.organization import OrganizationPermissionMixin +from shiftings.organizations.models import Membership +from shiftings.organizations.views.membership_base import MembershipSuccessMessageMixin, OrganizationScopedMembershipMixin from shiftings.utils.views.create_update_view import CreateOrUpdateView -class MembershipViewMixin(OrganizationPermissionMixin): +class MembershipViewMixin(OrganizationScopedMembershipMixin): model = Membership pk_url_kwarg = 'None' permission_required = 'organizations.edit_members' - def get_organization(self) -> Organization: - return self._get_object(Organization, 'org_pk') - - def get_success_url(self): - return reverse('organization_admin', args=[self.get_organization().pk]) - class MembershipAddView(MembershipViewMixin, CreateOrUpdateView): membership_name: str @@ -43,14 +33,9 @@ def get_initial(self) -> dict[str, Any]: return initial -class MembershipRemoveView(MembershipViewMixin, UserPassesTestMixin, DeleteView, FormMixin): +class MembershipRemoveView(MembershipSuccessMessageMixin, MembershipViewMixin, UserPassesTestMixin, DeleteView, FormMixin): pk_url_kwarg = 'member_pk' def test_func(self) -> bool: return not self._get_object(Membership, self.pk_url_kwarg).type.admin \ or self.get_organization().is_admin(self.request.user) - - def form_valid(self, form: Any) -> HttpResponse: - result = super().form_valid(form) - messages.success(self.request, _('Membership removed')) - return result diff --git a/src/shiftings/organizations/views/membership_base.py b/src/shiftings/organizations/views/membership_base.py new file mode 100644 index 0000000..e4b7468 --- /dev/null +++ b/src/shiftings/organizations/views/membership_base.py @@ -0,0 +1,32 @@ +from abc import ABC + +from django.contrib import messages +from django.http import HttpResponse +from django.utils.translation import gettext_lazy as _ + +from shiftings.organizations.models import Organization +from shiftings.organizations.views.organization_base import OrganizationPermissionMixin + + +class OrganizationScopedMembershipMixin(OrganizationPermissionMixin, ABC): + organization_pk_url_kwarg: str = 'org_pk' + organization_success_view_name: str = 'organization_admin' + use_organization_absolute_success_url: bool = False + + def get_organization(self) -> Organization: + return self._get_object(Organization, self.organization_pk_url_kwarg) + + def get_success_url(self) -> str: + organization = self.get_organization() + if self.use_organization_absolute_success_url: + return organization.get_absolute_url() + return self.organization_reverse(self.organization_success_view_name) + + +class MembershipSuccessMessageMixin(ABC): + success_message = _('Membership removed') + + def form_valid(self, form) -> HttpResponse: + response = super().form_valid(form) + messages.success(self.request, self.success_message) + return response diff --git a/src/shiftings/organizations/views/membership_type.py b/src/shiftings/organizations/views/membership_type.py index 0990e3a..0574403 100644 --- a/src/shiftings/organizations/views/membership_type.py +++ b/src/shiftings/organizations/views/membership_type.py @@ -1,35 +1,30 @@ from typing import Any, Dict -from django.contrib import messages from django.contrib.auth.mixins import UserPassesTestMixin -from django.http import HttpResponse -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from django.views.generic import DeleteView from django.views.generic.edit import FormMixin from shiftings.organizations.forms.membership import MembershipTypeForm -from shiftings.organizations.models import Organization from shiftings.organizations.models.membership import MembershipType -from shiftings.organizations.views.organization import OrganizationPermissionMixin +from shiftings.organizations.views.membership_base import ( + MembershipSuccessMessageMixin, + OrganizationScopedMembershipMixin, +) from shiftings.utils.views.create_update_view import CreateOrUpdateView -class MembershipTypeViewMixin(OrganizationPermissionMixin): +class MembershipTypeViewMixin(OrganizationScopedMembershipMixin): model = MembershipType permission_required = 'organizations.edit_membership_types' pk_url_kwarg = 'member_pk' - - def get_organization(self) -> Organization: - return self._get_object(Organization, 'org_pk') - - def get_success_url(self) -> str: - return self.get_organization().get_absolute_url() + use_organization_absolute_success_url = True class MembershipTypeEditView(MembershipTypeViewMixin, CreateOrUpdateView): membership_name: str form_class = MembershipTypeForm + use_organization_absolute_success_url = False + organization_success_view_name = 'organization_admin' def get_form_kwargs(self) -> Dict[str, Any]: kwargs = super().get_form_kwargs() @@ -42,17 +37,14 @@ def get_initial(self) -> Dict[str, Any]: initial['organization'] = self.get_organization() return initial - def get_success_url(self): - return reverse('organization_admin', args=[self.get_organization().pk]) - - -class MembershipTypeRemoveView(MembershipTypeViewMixin, UserPassesTestMixin, DeleteView, FormMixin): +class MembershipTypeRemoveView( + MembershipSuccessMessageMixin, + MembershipTypeViewMixin, + UserPassesTestMixin, + DeleteView, + FormMixin, +): def test_func(self) -> bool: membership_type = self._get_object(MembershipType, self.pk_url_kwarg) return not (membership_type.admin or membership_type.default) - - def form_valid(self, form: Any) -> HttpResponse: - result = super().form_valid(form) - messages.success(self.request, _('Membership removed')) - return result diff --git a/src/shiftings/organizations/views/organization_base.py b/src/shiftings/organizations/views/organization_base.py index f5956bf..c950adc 100644 --- a/src/shiftings/organizations/views/organization_base.py +++ b/src/shiftings/organizations/views/organization_base.py @@ -2,6 +2,7 @@ from typing import Any from django.contrib.auth.mixins import UserPassesTestMixin +from django.urls import reverse from shiftings.organizations.models import Organization from shiftings.utils.permissions import has_any_permission @@ -23,6 +24,9 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context['organization'] = self.get_organization() return context + def organization_reverse(self, view_name: str) -> str: + return reverse(view_name, args=[self.get_organization().pk]) + class OrganizationMemberMixin(OrganizationMixin, UserPassesTestMixin, ABC): def test_func(self) -> bool: @@ -42,3 +46,30 @@ def has_permission(self) -> bool: class OrganizationAdminMixin(OrganizationMixin, UserPassesTestMixin, ABC): def test_func(self) -> bool: return self.request.user.has_perm('organizations.admin') or self.get_organization().is_admin(self.request.user) + + +class OrganizationCreateUpdateMixin(ABC): + organization_create_pk_url_kwarg: str = 'org_pk' + organization_initial_field: str = 'organization' + set_organization_initial_on_create_only: bool = True + + def get_organization(self) -> Organization: + if self.is_create(): + return self._get_object(Organization, self.organization_create_pk_url_kwarg) + return self.get_object().organization + + def get_initial(self) -> dict[str, Any]: + initial = super().get_initial() + if not self.set_organization_initial_on_create_only or self.is_create(): + initial[self.organization_initial_field] = self.get_organization() + return initial + + +class OrganizationObjectRedirectMixin(ABC): + organization_success_view_name: str = 'organization_admin' + + def get_organization(self) -> Organization: + return self.get_object().organization + + def get_success_url(self) -> str: + return reverse(self.organization_success_view_name, args=[self.get_organization().pk]) diff --git a/src/shiftings/organizations/views/user.py b/src/shiftings/organizations/views/user.py index f56ffb3..d48c58a 100644 --- a/src/shiftings/organizations/views/user.py +++ b/src/shiftings/organizations/views/user.py @@ -1,7 +1,6 @@ from django.db.models import QuerySet from django.forms import ModelForm from django.http import HttpResponse -from django.urls import reverse from django.utils.translation import gettext_lazy as _ from django.views.generic import ListView @@ -38,7 +37,7 @@ def form_valid(self, form: ModelForm) -> HttpResponse: return super().form_valid(form) def get_success_url(self) -> str: - return reverse('claim_user_list', args=[self.get_organization().pk]) + return self.organization_reverse('claim_user_list') class UnclaimUserView(OrganizationPermissionMixin, CreateOrUpdateView[OrganizationDummyUser]): @@ -55,4 +54,4 @@ def form_valid(self, form: ModelForm) -> HttpResponse: return super().form_valid(form) def get_success_url(self) -> str: - return reverse('claim_user_list', args=[self.get_organization().pk]) + return self.organization_reverse('claim_user_list') diff --git a/src/shiftings/settings.py b/src/shiftings/settings.py index 65a21e3..cc203c3 100644 --- a/src/shiftings/settings.py +++ b/src/shiftings/settings.py @@ -98,6 +98,7 @@ 'shiftings.utils.context_processors.debug', 'shiftings.utils.context_processors.feature', 'shiftings.utils.context_processors.today', + 'shiftings.utils.context_processors.theme', 'shiftings.organizations.context_processors.organization_permissions', 'django.template.context_processors.debug', 'django.template.context_processors.request', diff --git a/src/shiftings/shifts/admin.py b/src/shiftings/shifts/admin.py index c9defcf..df39a50 100644 --- a/src/shiftings/shifts/admin.py +++ b/src/shiftings/shifts/admin.py @@ -1,16 +1,17 @@ -from django.contrib import admin - from shiftings.shifts.models import ( OrganizationSummarySettings, Participant, ParticipationPermission, RecurringShift, Shift, ShiftTemplate, ShiftTemplateGroup, ShiftType, ShiftTypeGroup ) +from shiftings.utils.admin import register_models -admin.site.register(OrganizationSummarySettings) -admin.site.register(Participant) -admin.site.register(ParticipationPermission) -admin.site.register(RecurringShift) -admin.site.register(Shift) -admin.site.register(ShiftType) -admin.site.register(ShiftTypeGroup) -admin.site.register(ShiftTemplateGroup) -admin.site.register(ShiftTemplate) +register_models( + OrganizationSummarySettings, + Participant, + ParticipationPermission, + RecurringShift, + Shift, + ShiftType, + ShiftTypeGroup, + ShiftTemplateGroup, + ShiftTemplate, +) diff --git a/src/shiftings/shifts/forms/shift.py b/src/shiftings/shifts/forms/shift.py index 147e851..9070ab2 100644 --- a/src/shiftings/shifts/forms/shift.py +++ b/src/shiftings/shifts/forms/shift.py @@ -36,6 +36,8 @@ def clean(self) -> Dict[str, Any]: start = cleaned_data.get('start') end = cleaned_data.get('end') if start and end and start > end: + self.add_error('end', ValidationError("")) + self.add_error('start', ValidationError("")) raise ValidationError(_('End time must be after start time')) ## TODO: raise form error if not valid, but first implement proper error display in template diff --git a/src/shiftings/shifts/models/permission.py b/src/shiftings/shifts/models/permission.py index 209cb1c..95a0088 100644 --- a/src/shiftings/shifts/models/permission.py +++ b/src/shiftings/shifts/models/permission.py @@ -50,25 +50,25 @@ def get_for_instance(self, instance: models.Model, organization: Organization | None) -> ParticipationPermission | None: return self.filter_instance(instance).filter(organization=organization).first() - def get_best_for_instance_and_user(self, instance: models.Model, user: User) -> ParticipationPermissionType: - if user.has_perm('organizations.admin'): - return ParticipationPermissionType.Participate - best_permission = self.filter_instance(instance) \ - .filter(Q(organization__isnull=True) | Q(organization__in=user.organizations)) \ - .aggregate(best=models.Max('permission_type_field'))['best'] + def _get_best_permission_for_user_from_queryset(self, queryset: QuerySet[ParticipationPermission], + user: User) -> ParticipationPermissionType: + """Extract best permission from a queryset filtered by user's organizations.""" + best_permission = queryset.filter( + Q(organization__isnull=True) | Q(organization__in=user.organizations) + ).aggregate(best=models.Max('permission_type_field'))['best'] if not best_permission: return ParticipationPermissionType.NoPermission return ParticipationPermissionType(best_permission) + def get_best_for_instance_and_user(self, instance: models.Model, user: User) -> ParticipationPermissionType: + if user.has_perm('organizations.admin'): + return ParticipationPermissionType.Participate + return self._get_best_permission_for_user_from_queryset(self.filter_instance(instance), user) + def get_best_for_user(self, user: User, *objects: models.Model) -> ParticipationPermissionType: if user.has_perm('organizations.admin'): return ParticipationPermissionType.Participate - best_permission = self.filter_instances(*objects) \ - .filter(Q(organization__isnull=True) | Q(organization__in=user.organizations)) \ - .aggregate(best=models.Max('permission_type_field'))['best'] - if not best_permission: - return ParticipationPermissionType.NoPermission - return ParticipationPermissionType(best_permission) + return self._get_best_permission_for_user_from_queryset(self.filter_instances(*objects), user) class ParticipationPermission(models.Model): diff --git a/src/shiftings/shifts/templates/shifts/create_shift.html b/src/shiftings/shifts/templates/shifts/create_shift.html index 9b5b425..c41abad 100644 --- a/src/shiftings/shifts/templates/shifts/create_shift.html +++ b/src/shiftings/shifts/templates/shifts/create_shift.html @@ -29,6 +29,16 @@

Update Shift "{{ name }} for {{ organization }}"

{% csrf_token %} + {% if form.non_field_errors %} + + {% endif %} {% bootstrap_field form.name %} {% bootstrap_field form.place %}
@@ -87,4 +97,4 @@

{% trans "Create from Template" %}

{% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/shifts/templates/shifts/recurring/shift.html b/src/shiftings/shifts/templates/shifts/recurring/shift.html index 6cf6248..8872c7b 100644 --- a/src/shiftings/shifts/templates/shifts/recurring/shift.html +++ b/src/shiftings/shifts/templates/shifts/recurring/shift.html @@ -28,11 +28,8 @@

{{ shift.name }}

{% if org_perms.edit_recurring_shifts %} - - - + {% url 'recurring_shift_delete' shift.pk as recurring_shift_delete_url %} + {% include "template/remove_modal_trigger.html" with tag="a" button_class="btn btn-danger ms-1" href="#confirmRemoveModal" remove_name=shift.name remove_url=recurring_shift_delete_url icon_class="fas fa-trash" %} @@ -109,4 +106,4 @@
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/shifts/templates/shifts/shift.html b/src/shiftings/shifts/templates/shifts/shift.html index 0a87453..36eb244 100644 --- a/src/shiftings/shifts/templates/shifts/shift.html +++ b/src/shiftings/shifts/templates/shifts/shift.html @@ -12,22 +12,19 @@

{% if not shift.shift_type.is_system or perms.organizations.admin %} - - - + {% url 'shift_update' shift.pk as shift_update_url %} + {% trans "Edit Shift" as edit_shift_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-success px-2 py-1" href=shift_update_url title=edit_shift_title icon_class="fa-solid fa-pen-to-square" %} {% now "Y-m-d" as today %} {% if org_perms.admin or shift.start|date:"Y-m-d" >= today and org_perms.delete_shifts %} - - - + {% url 'shift_delete' shift.pk as shift_delete_url %} + {% trans "Delete Shift" as delete_shift_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-danger px-2 py-1" href=shift_delete_url title=delete_shift_title icon_class="fa-solid fa-trash" %} {% endif %} {% if org_perms.admin %} - - - + {% url 'shift_part_permissions_edit' shift.pk as shift_part_permissions_edit_url %} + {% trans "Edit Permissions" as edit_permissions_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-info px-2 py-1" href=shift_part_permissions_edit_url title=edit_permissions_title icon_class="fa-solid fa-shield-halved" %} {% endif %} {% endif %}
@@ -67,9 +64,9 @@

{% if not shift.is_full %} {% if org_perms.add_non_members_to_shifts or org_perms.add_members_to_shifts %} {% if shift.start.date >= current_date or org_perms.add_to_past_shift %} - - {% trans "Add participant" %} - + {% url 'add_participant_other' shift.pk as add_participant_url %} + {% trans "Add participant" as add_participant_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-outline-success ms-3" href=add_participant_url title=add_participant_title icon_class="fa-solid fa-person-circle-plus me-2" label=add_participant_title %} {% endif %} {% endif %} {% endif %} @@ -92,4 +89,4 @@

-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/shifts/templates/shifts/summary/summary.html b/src/shiftings/shifts/templates/shifts/summary/summary.html index c2512cb..1c86975 100644 --- a/src/shiftings/shifts/templates/shifts/summary/summary.html +++ b/src/shiftings/shifts/templates/shifts/summary/summary.html @@ -13,6 +13,9 @@

{% trans "Shift Summary" %} {{ display }}

{% endif %}
+ {% if org_users_only %} + + {% endif %} {% bootstrap_form select_timerange_form show_label=False wrapper_class='' %}
@@ -25,7 +28,7 @@

{% trans "Shift Summary" %} {{ display }}

- {% member_shift_summary organization True %} + {% shift_users_summary organization True %}
{% endblock %} \ No newline at end of file diff --git a/src/shiftings/shifts/templates/shifts/template/group.html b/src/shiftings/shifts/templates/shifts/template/group.html index 7d40f71..0d80de1 100644 --- a/src/shiftings/shifts/templates/shifts/template/group.html +++ b/src/shiftings/shifts/templates/shifts/template/group.html @@ -11,26 +11,19 @@

Shift Template {{ group.name }}

{% if org_perms.edit_shift_templates %} - - - - - - - - - + {% url "shift_template_group_delete" group.pk as shift_template_group_delete_url %} + {% include "template/remove_modal_trigger.html" with tag="a" button_class="btn btn-danger ms-1" href="#confirmRemoveModal" remove_name=group.name remove_url=shift_template_group_delete_url icon_class="fas fa-trash" %} + {% url "shift_template_group_update" group.pk as shift_template_group_update_url %} + {% trans "Edit Template" as edit_template_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-success" href=shift_template_group_update_url title=edit_template_title icon_class="fa-solid fa-pen-to-square" %} + {% url "template_group_update_shifts" group.pk as template_group_update_shifts_url %} + {% trans "Edit Shifts" as edit_shifts_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-success" href=template_group_update_shifts_url title=edit_shifts_title icon_class="fa-solid fa-file-pen" %} {% endif %} {% if org_perms.admin %} - - - + {% url "template_group_update_permissions" group.pk as template_group_update_permissions_url %} + {% trans "Edit Template Permissions" as edit_template_permissions_title %} + {% include "template/icon_action_button.html" with button_class="btn btn-info" href=template_group_update_permissions_url title=edit_template_permissions_title icon_class="fa-solid fa-shield-halved" %} {% endif %}
@@ -71,4 +64,4 @@
{% trans "Recurring Shifts" %}
{% endfor %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/shifts/templates/shifts/template/member_shift_summary.html b/src/shiftings/shifts/templates/shifts/template/member_shift_summary.html deleted file mode 100644 index a2ef581..0000000 --- a/src/shiftings/shifts/templates/shifts/template/member_shift_summary.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - {% for group in groups %} - - {% endfor %} - {% if has_others %} - - {% endif %} - - - - {% for member in members %} - - - {% for group in member.groups %} - - {% endfor %} - {% if has_others %} - - {% endif %} - - - {% endfor %} - -
{% trans 'Member' %}{{ group.name }}{{ organization.summary_settings.other_shifts_group_name }}{% trans 'Total' %}
{{ member.name }}{{ group }}{{ member.other }}{{ member.total }}
\ No newline at end of file diff --git a/src/shiftings/shifts/templates/shifts/template/participation_permission_form.html b/src/shiftings/shifts/templates/shifts/template/participation_permission_form.html index d998883..7de7438 100644 --- a/src/shiftings/shifts/templates/shifts/template/participation_permission_form.html +++ b/src/shiftings/shifts/templates/shifts/template/participation_permission_form.html @@ -5,13 +5,8 @@
{% bootstrap_field form.organization show_label=False wrapper_class="" %}
{% bootstrap_field form.permission_type_field show_label=False wrapper_class="" %}
- - - - - - + {% include "template/formset_remove_restore_buttons.html" with form_prefix=form.prefix button_classes="btn btn-danger px-2 py-1" %}
- \ No newline at end of file + diff --git a/src/shiftings/shifts/templates/shifts/template/shift_users_summary.html b/src/shiftings/shifts/templates/shifts/template/shift_users_summary.html new file mode 100644 index 0000000..d87912d --- /dev/null +++ b/src/shiftings/shifts/templates/shifts/template/shift_users_summary.html @@ -0,0 +1,74 @@ +
+ + {% for key, value in request.GET.items %} + {% if key != 'org_users_only' %} + + {% endif %} + {% endfor %} +
+ + +
+ +
+ + + + + {% for group in groups %} + + {% endfor %} + {% if has_others %} + + {% endif %} + + + + {% for member in members %} + + + {% for group in member.groups %} + + {% endfor %} + {% if has_others %} + + {% endif %} + + + {% endfor %} + +
{% trans 'Member' %}{{ group.name }}{{ organization.summary_settings.other_shifts_group_name }}{% trans 'Total' %}
{{ member.name }}{{ group }}{{ member.other }}{{ member.total }}
+ +{% if not show_all_users and has_other_users_section %} +
+
+ {% trans "Other Users" %} ({{ other_users_count }}) +
+ + + {% for member in other_users %} + + + {% for group in member.groups %} + + {% endfor %} + {% if has_others %} + + {% endif %} + + + {% endfor %} + +
{{ member.name }}{{ group }}{{ member.other }}{{ member.total }}
+
+
+
+{% endif %} diff --git a/src/shiftings/shifts/templates/shifts/template/slots_display.html b/src/shiftings/shifts/templates/shifts/template/slots_display.html index e60fd2c..cec3193 100644 --- a/src/shiftings/shifts/templates/shifts/template/slots_display.html +++ b/src/shiftings/shifts/templates/shifts/template/slots_display.html @@ -8,13 +8,8 @@ {% if display %} {% if request.user.pk == display.user.pk or org_perms.remove_others_from_shifts %} {% if org_perms.admin or shift.end.date >= current_date %} - + {% url 'remove_participant' shift.pk display.pk as remove_participant_url %} + {% include "template/remove_modal_trigger.html" with tag="button" button_class="btn border border-2 link-remove w-100 mx-2 text-truncate mw-100" remove_name=display.display remove_url=remove_participant_url success_url=request.path label=display.display %} {% else %} {{ display.display }} {% endif %} @@ -34,4 +29,4 @@ {% endif %} {% endif %} - \ No newline at end of file + diff --git a/src/shiftings/shifts/templates/shifts/template/template_form.html b/src/shiftings/shifts/templates/shifts/template/template_form.html index 80506dd..ca26571 100644 --- a/src/shiftings/shifts/templates/shifts/template/template_form.html +++ b/src/shiftings/shifts/templates/shifts/template/template_form.html @@ -6,12 +6,7 @@

{% trans "Name" %}

{% bootstrap_field form.name show_label=False form_group_class='' %}
- - - - - - + {% include "template/formset_remove_restore_buttons.html" with form_prefix=form.prefix button_classes="btn btn-danger px-2 py-1 ms-auto" %}
@@ -45,4 +40,4 @@

{% trans "Name" %}

{% bootstrap_field form.additional_infos %}
- \ No newline at end of file + diff --git a/src/shiftings/shifts/templatetags/shifts.py b/src/shiftings/shifts/templatetags/shifts.py index 823758d..f637788 100644 --- a/src/shiftings/shifts/templatetags/shifts.py +++ b/src/shiftings/shifts/templatetags/shifts.py @@ -28,14 +28,20 @@ def shift_card(context, shift) -> dict[str, Any]: return context -@register.inclusion_tag('shifts/template/member_shift_summary.html', takes_context=True) -def member_shift_summary(context, org, show_all_users: bool = False, show_future_shifts: bool = True) -> dict[str, Any]: +@register.inclusion_tag('shifts/template/shift_users_summary.html', takes_context=True) +def shift_users_summary(context, org, show_all_users: bool = False, show_future_shifts: bool = True) -> dict[str, Any]: def get_int(name: str, default: int) -> int: try: return int(context['request'].GET.get(name, default)) except ValueError: return default + def get_bool(name: str, default: bool = False) -> bool: + value = context['request'].GET.get(name) + if value is None: + return default + return value.lower() in {'1', 'true', 'yes', 'on'} + try: time_range_type = TimeRangeType(get_int('time_range', org.summary_settings.default_time_range_type)) except ValueError: @@ -50,17 +56,27 @@ def get_int(name: str, default: int) -> int: groups = list(org.shift_type_groups.all()) context['groups'] = groups context['has_others'] = org.shifts.filter(time_filter, other_filter).exists() - users = org.users - if show_all_users: - user_ids = set(org.shifts.filter(time_filter).values_list('participants__user', flat=True)) - user_ids.discard(None) - filtered_user_ids = set(User.objects.filter(pk__in=user_ids).values_list('pk', flat=True)) - filtered_dummy_users = OrganizationDummyUser.objects.filter(pk__in=user_ids) - filtered_user_ids.update(filtered_dummy_users.filter(claimed_by__isnull=True).values_list('pk', flat=True)) - claimed_ids = filtered_dummy_users.filter(claimed_by__isnull=False).values_list('claimed_by__pk', flat=True) - filtered_user_ids.update(BaseUser.objects.filter(pk__in=claimed_ids).values_list('pk', flat=True)) - users = BaseUser.objects.filter(pk__in=filtered_user_ids) - members = [] + org_users_only = get_bool('org_users_only', False) + + participant_ids = set(org.shifts.filter(time_filter).values_list('participants__user', flat=True)) + participant_ids.discard(None) + + # Resolve participants to either direct users, unclaimed org dummy users, or their claimed-by users. + participant_user_ids = set(User.objects.filter(pk__in=participant_ids).values_list('pk', flat=True)) + participant_dummy_users = OrganizationDummyUser.objects.filter(pk__in=participant_ids) + participant_user_ids.update(participant_dummy_users.filter(claimed_by__isnull=True).values_list('pk', flat=True)) + claimed_ids = participant_dummy_users.filter(claimed_by__isnull=False).values_list('claimed_by__pk', flat=True) + participant_user_ids.update(BaseUser.objects.filter(pk__in=claimed_ids).values_list('pk', flat=True)) + + org_user_ids = set(org.users.values_list('pk', flat=True)) + + if org_users_only: + users = BaseUser.objects.filter(pk__in=org_user_ids) + elif show_all_users: + users = BaseUser.objects.filter(pk__in=participant_user_ids) + else: + users = BaseUser.objects.filter(pk__in=(participant_user_ids | org_user_ids)) + member_entries = [] for user in users.order_by('username'): pks = [user.pk] + list(OrganizationDummyUser.objects.filter(claimed_by=user).values_list('pk', flat=True)) group_amounts = [ @@ -68,15 +84,34 @@ def get_int(name: str, default: int) -> int: for shift_type_group in groups ] others_amount = org.shifts.filter(time_filter, other_filter, participants__user__pk__in=pks).count() - members.append({ + member_entries.append({ 'pk': user.pk, 'name': user.display, + 'is_org_user': user.pk in org_user_ids, 'groups': group_amounts, 'other': others_amount, 'total': sum(group_amounts) + others_amount }) - members.sort(key=lambda member: -member['total']) - context['members'] = members + + participants = [member for member in member_entries if member['total'] > 0] + participants.sort(key=lambda member: (-member['total'], member['name'])) + + if show_all_users: + context['members'] = participants + context['other_users'] = [] + context['other_users_count'] = 0 + context['has_other_users_section'] = False + else: + context['members'] = participants + other_users = [member for member in member_entries if member['is_org_user'] and member['total'] == 0] + context['other_users'] = other_users + context['other_users_count'] = len(other_users) + context['has_other_users_section'] = len(other_users) > 0 + + context['other_participants'] = [] + context['has_other_participants_section'] = False + context['show_all_users'] = show_all_users + context['org_users_only'] = org_users_only return context diff --git a/src/shiftings/shifts/tests/test_participant_views.py b/src/shiftings/shifts/tests/test_participant_views.py new file mode 100644 index 0000000..9fb8e70 --- /dev/null +++ b/src/shiftings/shifts/tests/test_participant_views.py @@ -0,0 +1,79 @@ +from datetime import datetime + +from django.test import TestCase +from django.urls import reverse + +from shiftings.accounts.models import User +from shiftings.organizations.models import Organization +from shiftings.shifts.models import Participant, Shift, ShiftType + + +class ParticipantViewsIntegrationTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + self.shift = Shift.objects.create( + organization=self.organization, + shift_type=self.shift_type, + name='Participant integration shift', + place='K1 Bar', + start=datetime(2099, 1, 5, 10, 0, 0), + end=datetime(2099, 1, 5, 12, 0, 0), + required_users=1, + max_users=2, + ) + self.admin = User.objects.get(username='bob') + self.member = User.objects.get(username='jd') + + def test_add_self_participant_endpoint_adds_current_user(self) -> None: + self.client.force_login(self.admin) + + response = self.client.post( + reverse('add_participant_self', kwargs={'pk': self.shift.pk}), + {'user': self.admin.pk, 'display_name': ''}, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, self.shift.get_absolute_url()) + self.assertTrue(self.shift.participants.filter(user=self.admin).exists()) + + def test_add_other_participant_with_org_user(self) -> None: + self.client.force_login(self.admin) + + response = self.client.post( + reverse('add_participant_other', kwargs={'pk': self.shift.pk}), + {'org_user': self.member.pk, 'other_user': '', 'display_name': ''}, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, self.shift.get_absolute_url()) + self.assertTrue(self.shift.participants.filter(user=self.member).exists()) + + def test_add_other_participant_with_non_member_username(self) -> None: + outsider = User.objects.create_user(username='outsider-user', password='secret') + self.client.force_login(self.admin) + + response = self.client.post( + reverse('add_participant_other', kwargs={'pk': self.shift.pk}), + {'org_user': '', 'other_user': outsider.username, 'display_name': ''}, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, self.shift.get_absolute_url()) + self.assertTrue(self.shift.participants.filter(user=outsider).exists()) + + def test_remove_participant_allows_self_removal_and_uses_success_url_override(self) -> None: + participant = Participant.objects.create(user=self.member, display_name='') + self.shift.participants.add(participant) + self.client.force_login(self.member) + + response = self.client.post( + reverse('remove_participant', kwargs={'pk': self.shift.pk, 'ppk': participant.pk}), + {'success_url': reverse('user_profile')}, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse('user_profile')) + self.assertFalse(Participant.objects.filter(pk=participant.pk).exists()) diff --git a/src/shiftings/shifts/tests/test_permission_forms.py b/src/shiftings/shifts/tests/test_permission_forms.py new file mode 100644 index 0000000..f68ef51 --- /dev/null +++ b/src/shiftings/shifts/tests/test_permission_forms.py @@ -0,0 +1,101 @@ +from datetime import datetime + +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase + +from shiftings.organizations.models import Organization +from shiftings.shifts.forms.permission import ParticipationPermissionForm, ParticipationPermissionFormSet +from shiftings.shifts.models import ParticipationPermission, ParticipationPermissionType, Shift, ShiftType + + +class ParticipationPermissionFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.other_organization = Organization.objects.create(name='Other Org') + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + self.shift = Shift.objects.create( + organization=self.organization, + shift_type=self.shift_type, + name='Permission shift', + place='K1 Bar', + start=datetime(2024, 1, 3, 10, 0, 0), + end=datetime(2024, 1, 3, 12, 0, 0), + required_users=1, + max_users=2, + ) + + def test_rejects_org_permission_if_all_users_permission_is_more_extensive(self) -> None: + ParticipationPermission.objects.create_for_instance( + self.shift, + organization=None, + permission_type_field=ParticipationPermissionType.ShiftParticipants, + ) + + form = ParticipationPermissionForm( + related_object=self.shift, + data={ + 'organization': self.other_organization.pk, + 'permission_type_field': ParticipationPermissionType.Existence, + }, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('permission_type_field', form.errors) + + +class ParticipationPermissionFormSetTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.other_organization = Organization.objects.create(name='Formset Org A') + self.third_organization = Organization.objects.create(name='Formset Org B') + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + self.shift = Shift.objects.create( + organization=self.organization, + shift_type=self.shift_type, + name='Formset shift', + place='K1 Bar', + start=datetime(2024, 1, 4, 10, 0, 0), + end=datetime(2024, 1, 4, 12, 0, 0), + required_users=1, + max_users=2, + ) + content_type = ContentType.objects.get_for_model(self.shift) + self.permission_one = ParticipationPermission.objects.create( + referred_content_type=content_type, + referred_object_id=self.shift.pk, + organization=self.other_organization, + permission_type_field=ParticipationPermissionType.Existence, + ) + self.permission_two = ParticipationPermission.objects.create( + referred_content_type=content_type, + referred_object_id=self.shift.pk, + organization=self.third_organization, + permission_type_field=ParticipationPermissionType.Participate, + ) + + def test_rejects_duplicate_organization_entries(self) -> None: + queryset = ParticipationPermission.objects.filter(pk__in=[self.permission_one.pk, self.permission_two.pk]) + formset = ParticipationPermissionFormSet( + data={ + 'form-TOTAL_FORMS': '2', + 'form-INITIAL_FORMS': '2', + 'form-MIN_NUM_FORMS': '0', + 'form-MAX_NUM_FORMS': '1000', + 'form-0-id': str(self.permission_one.pk), + 'form-0-organization': str(self.other_organization.pk), + 'form-0-permission_type_field': str(ParticipationPermissionType.Existence.value), + 'form-1-id': str(self.permission_two.pk), + 'form-1-organization': str(self.other_organization.pk), + 'form-1-permission_type_field': str(ParticipationPermissionType.ShiftDetails.value), + }, + queryset=queryset, + form_kwargs={'related_object': self.shift}, + ) + + self.assertFalse(formset.is_valid()) + form_errors = [form.errors for form in formset.forms] + self.assertTrue(any('organization' in errors for errors in form_errors)) diff --git a/src/shiftings/shifts/tests/test_permission_views.py b/src/shiftings/shifts/tests/test_permission_views.py new file mode 100644 index 0000000..557c4a8 --- /dev/null +++ b/src/shiftings/shifts/tests/test_permission_views.py @@ -0,0 +1,65 @@ +from datetime import datetime + +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.urls import reverse + +from shiftings.accounts.models import User +from shiftings.organizations.models import Organization +from shiftings.shifts.models import ParticipationPermission, ParticipationPermissionType, Shift, ShiftType + + +class ShiftParticipationPermissionViewTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + self.shift = Shift.objects.create( + organization=self.organization, + shift_type=self.shift_type, + name='Permissions integration shift', + place='K1 Bar', + start=datetime(2024, 1, 6, 10, 0, 0), + end=datetime(2024, 1, 6, 12, 0, 0), + required_users=1, + max_users=2, + ) + self.admin = User.objects.get(username='bob') + + def test_get_permissions_page_for_shift(self) -> None: + self.client.force_login(self.admin) + + response = self.client.get(reverse('shift_part_permissions_edit', kwargs={'pk': self.shift.pk})) + + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context['object'], self.shift) + self.assertEqual(response.context['organization'], self.organization) + + def test_post_updates_existing_permission_and_redirects(self) -> None: + content_type = ContentType.objects.get_for_model(self.shift) + permission = ParticipationPermission.objects.create( + referred_content_type=content_type, + referred_object_id=self.shift.pk, + organization=None, + permission_type_field=ParticipationPermissionType.Existence, + ) + self.client.force_login(self.admin) + + response = self.client.post( + reverse('shift_part_permissions_edit', kwargs={'pk': self.shift.pk}), + { + 'form-TOTAL_FORMS': '1', + 'form-INITIAL_FORMS': '1', + 'form-MIN_NUM_FORMS': '0', + 'form-MAX_NUM_FORMS': '1000', + 'form-0-id': str(permission.pk), + 'form-0-organization': '', + 'form-0-permission_type_field': str(ParticipationPermissionType.Participate.value), + }, + ) + + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, self.shift.get_absolute_url()) + permission.refresh_from_db() + self.assertEqual(permission.permission_type, ParticipationPermissionType.Participate) diff --git a/src/shiftings/shifts/tests/test_shift_forms.py b/src/shiftings/shifts/tests/test_shift_forms.py new file mode 100644 index 0000000..8cf5856 --- /dev/null +++ b/src/shiftings/shifts/tests/test_shift_forms.py @@ -0,0 +1,246 @@ +from datetime import datetime, timedelta + +from django.conf import settings +from django.test import TestCase + +from shiftings.accounts.models import User +from shiftings.shifts.forms.participant import AddOtherParticipantForm, AddSelfParticipantForm +from shiftings.shifts.forms.recurring import RecurringShiftForm +from shiftings.shifts.forms.shift import ShiftForm +from shiftings.shifts.forms.template import ShiftTemplateForm +from shiftings.shifts.models import Participant, Shift, ShiftTemplateGroup, ShiftType +from shiftings.shifts.models.recurring import ProblemHandling +from shiftings.shifts.utils.time_frame import TimeFrameType +from shiftings.organizations.models import Organization +from shiftings.utils.time.week import WeekDay + + +class ShiftFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + + def test_valid_shift_data_passes_validation(self) -> None: + form = ShiftForm( + instance=Shift(organization=self.organization, shift_type=self.shift_type), + data={ + 'name': 'Morning shift', + 'place': 'K1 Bar', + 'organization': self.organization.pk, + 'event': '', + 'shift_type': self.shift_type.pk, + 'start': '2024-01-01 10:00:00', + 'end': '2024-01-01 12:00:00', + 'required_users': 1, + 'max_users': 2, + 'additional_infos': '', + 'locked': False, + }, + ) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['start'].hour, 10) + self.assertEqual(form.cleaned_data['end'].hour, 12) + + def test_shift_end_before_start_is_rejected(self) -> None: + form = ShiftForm( + instance=Shift(organization=self.organization, shift_type=self.shift_type), + data={ + 'name': 'Broken shift', + 'place': 'K1 Bar', + 'organization': self.organization.pk, + 'event': '', + 'shift_type': self.shift_type.pk, + 'start': '2024-01-01 12:00:00', + 'end': '2024-01-01 10:00:00', + 'required_users': 1, + 'max_users': 2, + 'additional_infos': '', + 'locked': False, + }, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('start', form.errors) + self.assertIn('end', form.errors) + + +class RecurringShiftFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.template_group = ShiftTemplateGroup.objects.get(pk=1) + + def test_weekly_recurring_shift_validates(self) -> None: + form = RecurringShiftForm( + data={ + 'name': 'Weekly bar shift', + 'organization': self.organization.pk, + 'time_frame_field': TimeFrameType.EveryNthWeekday.value, + 'ordinal': 2, + 'week_day_field': WeekDay.Monday.value, + 'month_field': '', + 'first_occurrence': '2024-01-01', + 'auto_create_days': 7, + 'color': '#FD7E14', + 'template': self.template_group.pk, + 'weekend_handling_field': ProblemHandling.Ignore.value, + 'weekend_warning': '', + 'holiday_handling_field': ProblemHandling.Ignore.value, + 'holiday_warning': '', + }, + initial={'organization': self.organization}, + ) + + self.assertTrue(form.is_valid()) + + def test_weekday_timeframe_requires_weekday(self) -> None: + form = RecurringShiftForm( + data={ + 'name': 'Weekly bar shift', + 'organization': self.organization.pk, + 'time_frame_field': TimeFrameType.EveryNthWeekday.value, + 'ordinal': 2, + 'week_day_field': '', + 'month_field': '', + 'first_occurrence': '2024-01-01', + 'auto_create_days': 7, + 'color': '#FD7E14', + 'template': self.template_group.pk, + 'weekend_handling_field': ProblemHandling.Ignore.value, + 'weekend_warning': '', + 'holiday_handling_field': ProblemHandling.Ignore.value, + 'holiday_warning': '', + }, + initial={'organization': self.organization}, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('week_day_field', form.errors) + + def test_month_timeframe_requires_month(self) -> None: + form = RecurringShiftForm( + data={ + 'name': 'Monthly bar shift', + 'organization': self.organization.pk, + 'time_frame_field': TimeFrameType.NthDayOfASpecificMonth.value, + 'ordinal': 2, + 'week_day_field': '', + 'month_field': '', + 'first_occurrence': '2024-01-01', + 'auto_create_days': 7, + 'color': '#FD7E14', + 'template': self.template_group.pk, + 'weekend_handling_field': ProblemHandling.Ignore.value, + 'weekend_warning': '', + 'holiday_handling_field': ProblemHandling.Ignore.value, + 'holiday_warning': '', + }, + initial={'organization': self.organization}, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('month_field', form.errors) + + +class ShiftTemplateFormTest(TestCase): + fixtures = ['user', 'organization', 'shift'] + + def setUp(self) -> None: + self.template_group = ShiftTemplateGroup.objects.get(pk=1) + self.shift_type = ShiftType.objects.get(pk=1) + + def test_valid_template_form_converts_slider_values_to_timedelta(self) -> None: + form = ShiftTemplateForm( + self.template_group, + data={ + 'name': 'Late shift', + 'shift_type': self.shift_type.pk, + 'start_delay': 15, + 'duration': 120, + 'required_users': 1, + 'max_users': 2, + 'additional_infos': '', + }, + ) + + self.assertTrue(form.is_valid()) + instance = form.save(commit=False) + self.assertEqual(instance.start_delay, timedelta(minutes=15)) + self.assertEqual(instance.duration, timedelta(minutes=120)) + + def test_duration_cannot_exceed_max_shift_length(self) -> None: + form = ShiftTemplateForm( + self.template_group, + data={ + 'name': 'Too long shift', + 'shift_type': self.shift_type.pk, + 'start_delay': 15, + 'duration': settings.MAX_SHIFT_LENGTH_MINUTES + 1, + 'required_users': 1, + 'max_users': 2, + 'additional_infos': '', + }, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('duration', form.errors) + + +class ParticipantFormTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.shift_type = ShiftType.objects.filter(organization=self.organization).first() + self.shift = Shift.objects.create( + organization=self.organization, + shift_type=self.shift_type, + name='Participant test shift', + place='K1 Bar', + start=datetime(2024, 1, 1, 10, 0, 0), + end=datetime(2024, 1, 1, 12, 0, 0), + required_users=1, + max_users=2, + ) + self.member_user = User.objects.get(username='jd') + self.target_user = User.objects.get(username='bob') + + def test_add_self_participant_rejects_duplicates(self) -> None: + participant = Participant.objects.create(user=self.member_user, display_name='') + self.shift.participants.add(participant) + + form = AddSelfParticipantForm( + self.shift, + data={'user': self.member_user.pk, 'display_name': ''}, + initial={'user': self.member_user}, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('__all__', form.errors) + + def test_add_other_participant_accepts_org_member_choice(self) -> None: + form = AddOtherParticipantForm( + self.shift, + data={'org_user': self.member_user.pk, 'other_user': '', 'display_name': ''}, + ) + + self.assertTrue(form.is_valid()) + self.assertEqual(form.cleaned_data['user'], self.member_user) + + def test_add_other_participant_rejects_both_user_inputs(self) -> None: + form = AddOtherParticipantForm( + self.shift, + data={ + 'org_user': self.member_user.pk, + 'other_user': self.target_user.username, + 'display_name': '', + }, + ) + + self.assertFalse(form.is_valid()) + self.assertIn('org_user', form.errors) + self.assertIn('other_user', form.errors) \ No newline at end of file diff --git a/src/shiftings/shifts/tests/test_shift_users_summary.py b/src/shiftings/shifts/tests/test_shift_users_summary.py new file mode 100644 index 0000000..41b3ba3 --- /dev/null +++ b/src/shiftings/shifts/tests/test_shift_users_summary.py @@ -0,0 +1,109 @@ +from datetime import datetime, timedelta + +from django.test import RequestFactory, TestCase +from django.urls import reverse + +from shiftings.accounts.models import User +from shiftings.organizations.models import Organization +from shiftings.shifts.models import Participant, Shift +from shiftings.shifts.templatetags.shifts import shift_users_summary + + +class ShiftUsersSummaryTagTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.factory = RequestFactory() + self.organization = Organization.objects.get(pk=1) + self.org_user = User.objects.get(username='jd') + self.outsider = User.objects.create_user(username='summary-outsider', password='secret') + + self.shift = Shift.objects.create( + organization=self.organization, + name='Summary shift', + place='Main Hall', + start=datetime.now() - timedelta(days=1), + end=datetime.now(), + required_users=1, + max_users=4, + ) + self.shift.participants.add(Participant.objects.create(user=self.org_user)) + self.shift.participants.add(Participant.objects.create(user=self.outsider)) + + self.additional_shift = Shift.objects.create( + organization=self.organization, + name='Summary shift 2', + place='Main Hall', + start=datetime.now() - timedelta(hours=12), + end=datetime.now() - timedelta(hours=11), + required_users=1, + max_users=4, + ) + self.additional_shift.participants.add(Participant.objects.create(user=self.org_user)) + + def test_shift_users_summary_splits_active_and_other_users(self) -> None: + request = self.factory.get('/') + request.user = self.org_user + + context = shift_users_summary({'request': request}, self.organization, show_all_users=False, show_future_shifts=True) + + members_by_pk = {member['pk']: member for member in context['members']} + self.assertIn(self.org_user.pk, members_by_pk) + self.assertTrue(members_by_pk[self.org_user.pk]['is_org_user']) + self.assertIn(self.outsider.pk, members_by_pk) + self.assertFalse(members_by_pk[self.outsider.pk]['is_org_user']) + + totals = [member['total'] for member in context['members']] + self.assertEqual(totals, sorted(totals, reverse=True)) + + self.assertEqual(context['other_participants'], []) + + other_users_by_pk = {member['pk']: member for member in context['other_users']} + self.assertIn(self.organization.users.exclude(pk=self.org_user.pk).first().pk, other_users_by_pk) + + def test_shift_users_summary_can_filter_to_org_users_only(self) -> None: + request = self.factory.get('/', {'org_users_only': '1'}) + request.user = self.org_user + + context = shift_users_summary({'request': request}, self.organization, show_all_users=False, show_future_shifts=True) + + member_pks = {member['pk'] for member in context['members']} + self.assertNotIn(self.outsider.pk, member_pks) + self.assertIn(self.org_user.pk, member_pks) + self.assertEqual(context['other_participants'], []) + self.assertGreater(len(context['other_users']), 0) + self.assertGreater(context['other_users_count'], 0) + + def test_shift_users_summary_full_summary_has_no_other_users_section(self) -> None: + request = self.factory.get('/') + request.user = self.org_user + + context = shift_users_summary({'request': request}, self.organization, show_all_users=True, show_future_shifts=True) + + self.assertTrue(context['show_all_users']) + self.assertEqual(context['other_participants'], []) + self.assertEqual(context['other_users'], []) + self.assertEqual(context['other_users_count'], 0) + member_pks = {member['pk'] for member in context['members']} + self.assertIn(self.outsider.pk, member_pks) + self.assertIn(self.org_user.pk, member_pks) + + +class OrganizationShiftSummaryViewTest(TestCase): + fixtures = ['user', 'organization'] + + def setUp(self) -> None: + self.organization = Organization.objects.get(pk=1) + self.admin = User.objects.get(username='bob') + self.client.force_login(self.admin) + + def test_navigation_links_keep_org_users_only_filter(self) -> None: + response = self.client.get( + reverse('organization_shift_summary', kwargs={'pk': self.organization.pk}), + {'org_users_only': '1'}, + ) + + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context['org_users_only']) + self.assertIn('org_users_only=1', response.context['previous_url']) + self.assertIn('org_users_only=1', response.context['next_url']) diff --git a/src/shiftings/shifts/urls/recurring.py b/src/shiftings/shifts/urls/recurring.py index 8ecc486..b08b3bd 100644 --- a/src/shiftings/shifts/urls/recurring.py +++ b/src/shiftings/shifts/urls/recurring.py @@ -3,14 +3,16 @@ from shiftings.shifts.views.recurring import ( RecurringShiftCreateShiftsView, RecurringShiftDeleteView, RecurringShiftDetailView, RecurringShiftEditView ) +from shiftings.utils.url_patterns import organization_crud_paths urlpatterns = [ path('/', RecurringShiftDetailView.as_view(), name='recurring_shift'), - path('create/', RecurringShiftEditView.as_view(), name='recurring_shift_create'), - path('/update/', RecurringShiftEditView.as_view(), name='recurring_shift_update'), - path('/delete/', RecurringShiftDeleteView.as_view(), name='recurring_shift_delete'), path('/create_shift', RecurringShiftCreateShiftsView.as_view(), name='recurring_create_shifts'), -] +] + organization_crud_paths( + edit_view=RecurringShiftEditView, + delete_view=RecurringShiftDeleteView, + name_prefix='recurring_shift', +) diff --git a/src/shiftings/shifts/urls/shift.py b/src/shiftings/shifts/urls/shift.py index 988cc79..791bcdb 100644 --- a/src/shiftings/shifts/urls/shift.py +++ b/src/shiftings/shifts/urls/shift.py @@ -4,14 +4,17 @@ from shiftings.shifts.views.shift import ( CreateShiftFromTemplateGroup, ShiftDeleteView, ShiftDetailView, ShiftEditView, ShiftOrgSelectView ) +from shiftings.utils.url_patterns import organization_crud_paths urlpatterns = [ path('/', ShiftDetailView.as_view(), name='shift'), - path('create/', ShiftEditView.as_view(), name='shift_create'), path('create//template', CreateShiftFromTemplateGroup.as_view(), name='shift_create_from_template'), path('select_org/', ShiftOrgSelectView.as_view(), name='shift_org_select'), - path('/update/', ShiftEditView.as_view(), name='shift_update'), - path('/delete/', ShiftDeleteView.as_view(), name='shift_delete'), path('/permissions/', ShiftParticipationPermissionEditView.as_view(), name='shift_part_permissions_edit'), path('/participant/', include('shiftings.shifts.urls.participant')) -] +] + organization_crud_paths( + edit_view=ShiftEditView, + delete_view=ShiftDeleteView, + name_prefix='shift', + create_pattern='create/', +) diff --git a/src/shiftings/shifts/urls/templates.py b/src/shiftings/shifts/urls/templates.py index a369744..b9942e7 100644 --- a/src/shiftings/shifts/urls/templates.py +++ b/src/shiftings/shifts/urls/templates.py @@ -4,13 +4,15 @@ ShiftTemplateGroupDeleteView, ShiftTemplateGroupDetailView, ShiftTemplateGroupEditView, TemplateGroupAddShiftsView, TemplateGroupParticipationPermissionEditView ) +from shiftings.utils.url_patterns import organization_crud_paths urlpatterns = [ path('/', ShiftTemplateGroupDetailView.as_view(), name='shift_template_group'), - path('create//', ShiftTemplateGroupEditView.as_view(), name='shift_template_group_create'), - path('/update/', ShiftTemplateGroupEditView.as_view(), name='shift_template_group_update'), - path('/delete/', ShiftTemplateGroupDeleteView.as_view(), name='shift_template_group_delete'), path('/templates/', TemplateGroupAddShiftsView.as_view(), name='template_group_update_shifts'), path('/permissions/', TemplateGroupParticipationPermissionEditView.as_view(), name='template_group_update_permissions'), -] +] + organization_crud_paths( + edit_view=ShiftTemplateGroupEditView, + delete_view=ShiftTemplateGroupDeleteView, + name_prefix='shift_template_group', +) diff --git a/src/shiftings/shifts/urls/type.py b/src/shiftings/shifts/urls/type.py index ec287f0..596aacd 100644 --- a/src/shiftings/shifts/urls/type.py +++ b/src/shiftings/shifts/urls/type.py @@ -1,9 +1,8 @@ -from django.urls import path - from shiftings.shifts.views.type import ShiftTypeDeleteView, ShiftTypeEditView +from shiftings.utils.url_patterns import organization_crud_paths -urlpatterns = [ - path('create//', ShiftTypeEditView.as_view(), name='shift_type_create'), - path('/update/', ShiftTypeEditView.as_view(), name='shift_type_update'), - path('/delete/', ShiftTypeDeleteView.as_view(), name='shift_type_delete'), -] +urlpatterns = organization_crud_paths( + edit_view=ShiftTypeEditView, + delete_view=ShiftTypeDeleteView, + name_prefix='shift_type', +) diff --git a/src/shiftings/shifts/urls/type_group.py b/src/shiftings/shifts/urls/type_group.py index f139d2a..aa0b1e5 100644 --- a/src/shiftings/shifts/urls/type_group.py +++ b/src/shiftings/shifts/urls/type_group.py @@ -4,13 +4,16 @@ ShiftTypeGroupDetailView, ShiftTypeGroupEditView, ShiftTypeGroupListView, ShiftTypeGroupMoveDownView, ShiftTypeGroupMoveUpView, ShiftTypeGroupRemoveView ) +from shiftings.utils.url_patterns import organization_crud_paths urlpatterns = [ path('/', ShiftTypeGroupListView.as_view(), name='shift_type_groups'), - path('create//', ShiftTypeGroupEditView.as_view(), name='shift_type_group_create'), - path('/', ShiftTypeGroupDetailView.as_view(), name='shift_type_group_detail'), - path('/update/', ShiftTypeGroupEditView.as_view(), name='shift_type_group_update'), + path('detail//', ShiftTypeGroupDetailView.as_view(), name='shift_type_group_detail'), path('/move_up/', ShiftTypeGroupMoveUpView.as_view(), name='shift_type_group_move_up'), path('/move_down/', ShiftTypeGroupMoveDownView.as_view(), name='shift_type_group_move_down'), - path('/remove/', ShiftTypeGroupRemoveView.as_view(), name='shift_type_group_remove'), -] +] + organization_crud_paths( + edit_view=ShiftTypeGroupEditView, + delete_view=ShiftTypeGroupRemoveView, + name_prefix='shift_type_group', + delete_segment='remove', +) diff --git a/src/shiftings/shifts/views/helpers.py b/src/shiftings/shifts/views/helpers.py new file mode 100644 index 0000000..9b47e55 --- /dev/null +++ b/src/shiftings/shifts/views/helpers.py @@ -0,0 +1,15 @@ +"""Helper utilities for shift views.""" + +from datetime import date, datetime + +from shiftings.shifts.models import Shift + + +def shift_is_past(shift: Shift) -> bool: + """Check if a shift start time is in the past (before today).""" + return shift.start.date() < date.today() + + +def shift_datetime_is_past(shift: Shift) -> bool: + """Check if a shift start time is in the past (before now).""" + return shift.start < datetime.now() diff --git a/src/shiftings/shifts/views/participant.py b/src/shiftings/shifts/views/participant.py index 57af757..29e03cc 100644 --- a/src/shiftings/shifts/views/participant.py +++ b/src/shiftings/shifts/views/participant.py @@ -1,4 +1,3 @@ -from datetime import date from typing import Any from django.http import HttpResponse @@ -9,6 +8,7 @@ from shiftings.organizations.views.organization_base import OrganizationPermissionMixin from shiftings.shifts.forms.participant import AddOtherParticipantForm, AddSelfParticipantForm from shiftings.shifts.models import Participant, Shift +from shiftings.shifts.views.helpers import shift_is_past from shiftings.utils.exceptions import Http403 from shiftings.utils.views.create_update_view import CreateView @@ -35,7 +35,7 @@ def get_form_kwargs(self) -> dict[str, Any]: def get_context_data(self, **kwargs: Any) -> dict[str, Any]: context = super().get_context_data(**kwargs) shift = self.get_shift() - if shift.start.date() < date.today(): + if shift_is_past(shift): form = context['form'] if not hasattr(form, 'cleaned_data'): form.cleaned_data = {} @@ -44,16 +44,19 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: def form_valid(self, form: AddSelfParticipantForm) -> HttpResponse: shift = self.get_shift() - if (shift.start.date() < date.today() + if (shift_is_past(shift) and not self.request.user.has_perm('organizations.add_to_past_shift', self.get_organization())): raise Http403() if (not self.request.user.has_perm('organizations.add_non_members_to_shifts', self.get_organization()) and not self.get_organization().is_member(form.cleaned_data['user'])): raise Http403() + self.save_participant_for_shift(form, shift) + return self.success + + def save_participant_for_shift(self, form: AddSelfParticipantForm, shift: Shift) -> None: self.object = form.save() shift.participants.add(self.object) shift.save() - return self.success def get_success_url(self) -> str: return self.get_shift().get_absolute_url() @@ -64,7 +67,7 @@ class AddSelfParticipantView(AddOtherParticipantView): permission_required = 'organizations.participate_in_shift' def has_permission(self) -> bool: - if (self.get_shift().start.date() < date.today() + if (shift_is_past(self.get_shift()) and not self.request.user.has_perm('organizations.add_to_past_shift', self.get_organization())): return False if self.get_shift().can_participate(self.request.user): @@ -78,9 +81,7 @@ def get_initial(self) -> dict[str, Any]: def form_valid(self, form: AddSelfParticipantForm) -> HttpResponse: shift = self.get_shift() - self.object = form.save() - shift.participants.add(self.object) - shift.save() + self.save_participant_for_shift(form, shift) return self.success @@ -96,7 +97,7 @@ def get_shift(self) -> Shift: return self._get_object(Shift, 'pk') def has_permission(self) -> bool: - if self.get_shift().start.date() < date.today(): + if shift_is_past(self.get_shift()): return self.get_organization().is_admin(self.request.user) if self.get_object().user.pk == self.request.user.pk: return True diff --git a/src/shiftings/shifts/views/recurring.py b/src/shiftings/shifts/views/recurring.py index ee7072e..0f998e2 100644 --- a/src/shiftings/shifts/views/recurring.py +++ b/src/shiftings/shifts/views/recurring.py @@ -13,7 +13,12 @@ from django.views.generic.edit import BaseFormView, DeleteView from shiftings.organizations.models import Organization -from shiftings.organizations.views.organization_base import OrganizationMemberMixin, OrganizationPermissionMixin +from shiftings.organizations.views.organization_base import ( + OrganizationCreateUpdateMixin, + OrganizationMemberMixin, + OrganizationObjectRedirectMixin, + OrganizationPermissionMixin, +) from shiftings.shifts.forms.recurring import RecurringShiftCreateShiftsForm, RecurringShiftForm from shiftings.shifts.models import RecurringShift from shiftings.utils.views.create_update_view import CreateOrUpdateView @@ -40,21 +45,15 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]: return context -class RecurringShiftEditView(OrganizationPermissionMixin, CreateOrUpdateView): +class RecurringShiftEditView(OrganizationCreateUpdateMixin, OrganizationPermissionMixin, CreateOrUpdateView): template_name = 'shifts/recurring/form.html' model = RecurringShift form_class = RecurringShiftForm permission_required = 'organizations.edit_recurring_shifts' - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.get_object().organization - def get_initial(self) -> dict[str, Any]: initial = super().get_initial() if self.is_create(): - initial['organization'] = self.get_organization() initial['first_occurrence'] = date.today() return initial @@ -62,16 +61,10 @@ def get_success_url(self) -> str: return reverse('recurring_shift', args=[self.object.pk]) -class RecurringShiftDeleteView(OrganizationPermissionMixin, DeleteView): +class RecurringShiftDeleteView(OrganizationObjectRedirectMixin, OrganizationPermissionMixin, DeleteView): permission_required = 'organizations.edit_shift_templates' model = RecurringShift - def get_organization(self) -> Organization: - return self.get_object().organization - - def get_success_url(self) -> str: - return reverse('organization_admin', args=[self.get_organization().pk]) - class RecurringShiftCreateShiftsView(OrganizationPermissionMixin, SingleObjectMixin, BaseFormView): permission_required = 'organizations.edit_shifts' diff --git a/src/shiftings/shifts/views/shift.py b/src/shiftings/shifts/views/shift.py index 0f5052d..bc8e139 100644 --- a/src/shiftings/shifts/views/shift.py +++ b/src/shiftings/shifts/views/shift.py @@ -1,6 +1,6 @@ from __future__ import annotations -from datetime import date, datetime +from datetime import date from typing import Any, Dict, Optional from django.contrib import messages @@ -14,11 +14,16 @@ from django.views.generic.edit import DeleteView, FormView from shiftings.organizations.models import Organization -from shiftings.organizations.views.organization_base import OrganizationPermissionMixin +from shiftings.organizations.views.organization_base import ( + OrganizationCreateUpdateMixin, + OrganizationObjectRedirectMixin, + OrganizationPermissionMixin, +) from shiftings.shifts.forms.participant import AddSelfParticipantForm from shiftings.shifts.forms.shift import SelectOrgForm, ShiftForm from shiftings.shifts.forms.template import SelectOrgShiftTemplateGroupForm from shiftings.shifts.models import Shift, ShiftTemplateGroup +from shiftings.shifts.views.helpers import shift_datetime_is_past from shiftings.utils.views.base import BaseLoginMixin from shiftings.utils.views.create_update_view import CreateOrUpdateView @@ -70,7 +75,7 @@ def get_success_url(self): return reverse('shift_create', args=[self.org_id]) + f'?date={self.action_date.strftime("%Y-%m-%d")}' -class ShiftEditView(OrganizationPermissionMixin, CreateOrUpdateView): +class ShiftEditView(OrganizationCreateUpdateMixin, OrganizationPermissionMixin, CreateOrUpdateView): model = Shift form_class = ShiftForm permission_required = 'organizations.edit_shifts' @@ -85,15 +90,9 @@ def has_permission(self) -> bool: return self.request.user.has_perm('organizations.admin') return super().has_permission() - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.get_object().organization - def get_initial(self) -> Dict[str, Any]: initial = super().get_initial() if self.is_create(): - initial['organization'] = self.get_organization() initial['start'] = self.request.GET.get('date') return initial @@ -149,21 +148,17 @@ def get_success_url(self) -> str: return self.get_organization().get_absolute_url() -class ShiftDeleteView(OrganizationPermissionMixin, DeleteView): +class ShiftDeleteView(OrganizationObjectRedirectMixin, OrganizationPermissionMixin, DeleteView): model = Shift object: Shift permission_required = 'organizations.delete_shifts' template_name = 'generic/delete.html' - - def get_organization(self) -> Organization: - return self.get_object().organization + organization_success_view_name = 'organization' def delete(self, request, *args, **kwargs): self.object = self.get_object() - if self.object.start < datetime.now(): + if shift_datetime_is_past(self.object): messages.error(request, _('Unable to delete past shifts.')) return self.render_to_response(self.get_context_data()) self.object.delete() - def get_success_url(self) -> str: - return reverse('organization', args=[self.object.organization.pk]) diff --git a/src/shiftings/shifts/views/summary.py b/src/shiftings/shifts/views/summary.py index 6d8b939..47a7d09 100644 --- a/src/shiftings/shifts/views/summary.py +++ b/src/shiftings/shifts/views/summary.py @@ -24,7 +24,7 @@ def get_organization(self) -> Organization: return self.get_object().organization def get_success_url(self): - return reverse('organization_settings', args=[self.get_organization().pk]) + return self.organization_reverse('organization_settings') class OrganizationShiftSummaryView(OrganizationPermissionMixin, DetailView): @@ -43,13 +43,21 @@ def get_int(name: str, default: int) -> int: except ValueError: return default + def get_bool(name: str, default: bool = False) -> bool: + value = self.request.GET.get(name) + if value is None: + return default + return value.lower() in {'1', 'true', 'yes', 'on'} + def get_url(_date: date) -> str: - return reverse('organization_shift_summary', args=[organization.pk]) + '?' + urlencode( - {'time_range': time_range.value, 'year': _date.year, 'month': _date.month} - ) + params = {'time_range': time_range.value, 'year': _date.year, 'month': _date.month} + if org_users_only: + params['org_users_only'] = '1' + return reverse('organization_shift_summary', args=[organization.pk]) + '?' + urlencode(params) context_data = super().get_context_data(**kwargs) organization = self.get_organization() + org_users_only = get_bool('org_users_only', False) try: time_range = TimeRangeType(get_int('time_range', organization.summary_settings.default_time_range_type)) except ValueError: @@ -76,4 +84,5 @@ def get_url(_date: date) -> str: 'year': year, 'month': month, }) + context_data['org_users_only'] = org_users_only return context_data diff --git a/src/shiftings/shifts/views/template.py b/src/shiftings/shifts/views/template.py index 43f32a7..6a64ce8 100644 --- a/src/shiftings/shifts/views/template.py +++ b/src/shiftings/shifts/views/template.py @@ -2,12 +2,15 @@ from django.db.models import QuerySet from django.http import HttpResponse -from django.urls import reverse from django.views.generic import DetailView, TemplateView from shiftings.organizations.models import Organization from shiftings.organizations.views.organization import OrganizationMemberMixin, OrganizationPermissionMixin -from shiftings.organizations.views.organization_base import OrganizationMixin +from shiftings.organizations.views.organization_base import ( + OrganizationCreateUpdateMixin, + OrganizationMixin, + OrganizationObjectRedirectMixin, +) from shiftings.shifts.forms.template import ShiftTemplateFormSet, ShiftTemplateGroupForm from shiftings.shifts.models import ShiftTemplate, ShiftTemplateGroup from shiftings.shifts.views.permission import ParticipationPermissionEditView @@ -31,30 +34,25 @@ def get_organization(self) -> Organization: return self.get_object().organization -class ShiftTemplateGroupEditView(ShiftTemplateGroupMixin, CreateOrUpdateView, OrganizationPermissionMixin): +class ShiftTemplateGroupEditView( + ShiftTemplateGroupMixin, + OrganizationCreateUpdateMixin, + CreateOrUpdateView, + OrganizationPermissionMixin, +): form_class = ShiftTemplateGroupForm permission_required = 'organizations.edit_shift_templates' + set_organization_initial_on_create_only = False - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.object.organization - - def get_initial(self) -> dict[str, Any]: - initial = super().get_initial() - initial['organization'] = self.get_organization() - return initial - -class ShiftTemplateGroupDeleteView(ShiftTemplateGroupMixin, OrganizationPermissionMixin, DeleteView): +class ShiftTemplateGroupDeleteView( + ShiftTemplateGroupMixin, + OrganizationPermissionMixin, + OrganizationObjectRedirectMixin, + DeleteView, +): permission_required = 'organizations.edit_shift_templates' - def get_organization(self) -> Organization: - return self.get_object().organization - - def get_success_url(self) -> str: - return reverse('organization_admin', args=[self.get_organization().pk]) - class TemplateGroupAddShiftsView(OrganizationPermissionMixin, ModelFormsetBaseView[ShiftTemplate], TemplateView): model = ShiftTemplate diff --git a/src/shiftings/shifts/views/type.py b/src/shiftings/shifts/views/type.py index cda5d9b..d98a431 100644 --- a/src/shiftings/shifts/views/type.py +++ b/src/shiftings/shifts/views/type.py @@ -1,18 +1,19 @@ -from typing import Any - -from django.urls import reverse from django.views.generic import DeleteView -from shiftings.organizations.models import Organization -from shiftings.organizations.views.organization_base import OrganizationAdminMixin, OrganizationPermissionMixin +from shiftings.organizations.views.organization_base import ( + OrganizationAdminMixin, + OrganizationCreateUpdateMixin, + OrganizationObjectRedirectMixin, +) from shiftings.shifts.forms.type import ShiftTypeForm from shiftings.shifts.models import ShiftType from shiftings.utils.views.create_update_view import CreateOrUpdateView -class ShiftTypeEditView(OrganizationAdminMixin, CreateOrUpdateView[ShiftType]): +class ShiftTypeEditView(OrganizationCreateUpdateMixin, OrganizationAdminMixin, CreateOrUpdateView[ShiftType]): model = ShiftType form_class = ShiftTypeForm + set_organization_initial_on_create_only = False def test_func(self) -> bool: test = super().test_func() @@ -20,25 +21,9 @@ def test_func(self) -> bool: return test return self.get_object().name != 'System' - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.get_object().organization - - def get_initial(self) -> dict[str, Any]: - initial = super().get_initial() - initial['organization'] = self.get_organization() - return initial - def get_success_url(self): - return reverse('organization_admin', args=[self.object.organization.pk]) + return self.organization_reverse('organization_admin') -class ShiftTypeDeleteView(OrganizationAdminMixin, DeleteView): +class ShiftTypeDeleteView(OrganizationObjectRedirectMixin, OrganizationAdminMixin, DeleteView): model = ShiftType - - def get_organization(self) -> Organization: - return self.get_object().organization - - def get_success_url(self) -> str: - return reverse('organization_admin', args=[self.get_organization().pk]) diff --git a/src/shiftings/shifts/views/type_group.py b/src/shiftings/shifts/views/type_group.py index 3b6a555..305cf31 100644 --- a/src/shiftings/shifts/views/type_group.py +++ b/src/shiftings/shifts/views/type_group.py @@ -4,11 +4,14 @@ from django.db.models import QuerySet from django.forms import ModelForm from django.http import HttpResponse -from django.urls import reverse from django.views.generic import DetailView, ListView from shiftings.organizations.models import Organization -from shiftings.organizations.views.organization_base import OrganizationAdminMixin +from shiftings.organizations.views.organization_base import ( + OrganizationAdminMixin, + OrganizationCreateUpdateMixin, + OrganizationObjectRedirectMixin, +) from shiftings.shifts.forms.type_group import ShiftTypeGroupForm from shiftings.shifts.models import ShiftTypeGroup from shiftings.utils.views.create_update_view import CreateOrUpdateView @@ -37,18 +40,13 @@ def get_organization(self) -> Organization: return self.object.organization -class ShiftTypeGroupEditView(OrganizationAdminMixin, CreateOrUpdateView[ShiftTypeGroup]): +class ShiftTypeGroupEditView(OrganizationCreateUpdateMixin, OrganizationAdminMixin, CreateOrUpdateView[ShiftTypeGroup]): model = ShiftTypeGroup form_class = ShiftTypeGroupForm - - def get_organization(self) -> Organization: - if self.is_create(): - return self._get_object(Organization, 'org_pk') - return self.get_object().organization + set_organization_initial_on_create_only = False def get_initial(self) -> dict[str, Any]: initial = super().get_initial() - initial['organization'] = self.get_organization() if not self.is_create(): initial['shift_types'] = self.get_object().shift_types.all() return initial @@ -68,17 +66,12 @@ def form_valid(self, form: ShiftTypeGroupForm) -> HttpResponse: return response def get_success_url(self): - return reverse('organization_settings', args=[self.get_organization().pk]) + return self.organization_reverse('organization_settings') -class ShiftTypeGroupRemoveView(OrganizationAdminMixin, DeleteView): +class ShiftTypeGroupRemoveView(OrganizationObjectRedirectMixin, OrganizationAdminMixin, DeleteView): model = ShiftTypeGroup - - def get_organization(self) -> Organization: - return self.get_object().organization - - def get_success_url(self) -> str: - return reverse('organization_settings', args=[self.get_organization().pk]) + organization_success_view_name = 'organization_settings' class ShiftTypeGroupMoveView(OrganizationAdminMixin, CreateOrUpdateView[ShiftTypeGroup]): @@ -110,7 +103,7 @@ def form_valid(self, form: ModelForm) -> HttpResponse: return self.success def get_success_url(self) -> str: - return reverse('organization_settings', args=[self.get_organization().pk]) + return self.organization_reverse('organization_settings') class ShiftTypeGroupMoveUpView(ShiftTypeGroupMoveView): diff --git a/src/shiftings/static/css/base.css b/src/shiftings/static/css/base.css index 839d25b..250d3c2 100644 --- a/src/shiftings/static/css/base.css +++ b/src/shiftings/static/css/base.css @@ -39,19 +39,128 @@ --bs-dark-rgb: 21, 22, 24; --bs-white-rgb: 255, 255, 255; --bs-black-rgb: 0, 0, 0; - --bs-body-color-rgb: 255, 255, 255; - --bs-body-bg-rgb: 255, 255, 255; + --bs-body-color-rgb: 33, 37, 41; + --bs-body-bg-rgb: 248, 249, 250; --bs-body-font-size: 1rem; --bs-body-font-weight: 400; --bs-body-line-height: 1.5; + --bs-body-color: #212529; + --bs-body-bg: #e6eaee; + --bs-menu-bg: #ced5dc; + --bs-form-control-bg: #f2f5f7; + --bs-form-control-color: #212529; + --bs-surface-bg: #eceff2; + --bs-surface-color: #212529; + --bs-surface-striped-bg: rgba(0, 0, 0, .04); + --organization-logo-bg: #cfd7dd; + --organization-panel-bg: #d5dde3; + --set-display-name-color: #0b7285; + --set-display-name-border: #0b7285; + --set-display-name-bg-hover: #c9dee4; + --member-summary-own-bg: rgba(13, 110, 253, .12); + --filter-surface-bg: #e6ecf0; + --filter-surface-border: #c2cad0; + --org-filter-border: rgba(33, 37, 41, .07); + --login-surface-bg: #e1e7eb; + --login-surface-text: #212529; + --theme-border-dark: #6c757d; + --theme-border-light: #b5bdc4; + --theme-btn-dark-bg: #495057; + --theme-btn-dark-border: #495057; + --theme-btn-dark-color: #fff; + --table-head-bg: #d5dce2; + --table-head-color: #212529; + --bs-form-select-valid-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); +} + +[data-bs-theme='dark'] { + --bs-body-color-rgb: 255, 255, 255; + --bs-body-bg-rgb: 33, 37, 41; --bs-body-color: #fff; --bs-body-bg: #212529; --bs-menu-bg: #0b0c0e; + --bs-form-control-bg: #212529; + --bs-form-control-color: #fff; + --bs-surface-bg: #343a40; + --bs-surface-color: #fff; + --bs-surface-striped-bg: rgba(255, 255, 255, .06); + --organization-logo-bg: #4d5459; + --organization-panel-bg: #4d5459; + --set-display-name-color: #0dcaf0; + --set-display-name-border: #0dcaf0; + --set-display-name-bg-hover: rgba(13, 202, 240, .2); + --member-summary-hover-bg: rgba(255, 255, 255, .16); + --member-summary-own-bg: rgba(13, 202, 240, .34); + --filter-surface-bg: #212529; + --filter-surface-border: #4d5459; + --org-filter-border: rgba(255, 255, 255, .14); + --login-surface-bg: #4d5459; + --login-surface-text: #fff; + --theme-border-dark: #212529; + --theme-border-light: #f8f9fa; + --theme-btn-dark-bg: #212529; + --theme-btn-dark-border: #212529; + --theme-btn-dark-color: #fff; + --table-head-bg: #40464d; + --table-head-color: #fff; + --bs-form-select-valid-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e"); +} + +.bg-dark { + background-color: var(--bs-surface-bg) !important; + color: var(--bs-surface-color) !important; +} + +.table-dark { + --bs-table-bg: var(--bs-surface-bg); + --bs-table-color: var(--bs-surface-color); + --bs-table-striped-bg: var(--bs-surface-striped-bg); + --bs-table-striped-color: var(--bs-surface-color); + --bs-table-border-color: var(--bs-gray-700); +} + +.dropdown-menu, +.dropdown-menu-dark { + --bs-dropdown-bg: var(--bs-surface-bg); + --bs-dropdown-color: var(--bs-surface-color); + --bs-dropdown-link-color: var(--bs-surface-color); + --bs-dropdown-link-hover-color: var(--bs-surface-color); + --bs-dropdown-link-hover-bg: var(--bs-secondary); +} + +.dropdown-menu { + background-color: var(--bs-dropdown-bg); + color: var(--bs-dropdown-color); +} + +.navbar-dark .navbar-brand, +.navbar-dark .navbar-nav .nav-link { + color: var(--bs-body-color); } -input, textarea, select, .form-control, .form-select { - background-color: var(--bs-dark) !important; - color: var(--bs-body-color) !important; +.border-dark { + border-color: var(--theme-border-dark) !important; +} + +.border-light { + border-color: var(--theme-border-light) !important; +} + +.btn-dark { + background-color: var(--theme-btn-dark-bg); + border-color: var(--theme-btn-dark-border); + color: var(--theme-btn-dark-color); +} + +.btn-dark:hover, +.btn-dark:focus { + filter: brightness(.95); + color: var(--theme-btn-dark-color); +} + +input:not([type='checkbox']):not([type='radio']), textarea, select, .form-control, .form-select { + background-color: var(--bs-form-control-bg) !important; + color: var(--bs-form-control-color) !important; } input:disabled { @@ -62,6 +171,15 @@ nav .dropdown { z-index: 99999; } +header { + position: relative; + z-index: 1060; +} + +footer { + z-index: 1060; +} + /*pagination*/ .page-item.disabled .page-link { @@ -97,6 +215,45 @@ nav .dropdown { border-color: var(--bs-body-color) !important; } +.form-check-input:checked { + background-color: var(--bs-success) !important; + border-color: var(--bs-success) !important; +} + +[data-bs-theme='light'] .form-check-input:focus { + box-shadow: 0 0 0 .25rem rgba(var(--bs-success-rgb), .25); +} + +.form-control.is-valid:not([required]), +.form-control-color.is-valid:not([required]) { + border-color: var(--bs-gray-700); + box-shadow: none; + background-image: none; + padding-right: .75rem; +} + +.form-select.is-valid { + background-image: var(--bs-form-select-valid-icon); + background-position: right .75rem center; + background-size: 16px 12px; +} + +.form-select.is-valid:not([required]) { + border-color: var(--bs-gray-700); + box-shadow: none; + padding-right: 2.25rem; +} + +.form-check-input.is-valid:not([required]) { + border-color: var(--bs-body-color) !important; + background-color: transparent; + box-shadow: none; +} + +.form-check-input.is-valid:not([required]) ~ .form-check-label { + color: var(--bs-body-color) !important; +} + .plain-link, .link, .link-remove { cursor: pointer; text-decoration: none; @@ -131,6 +288,25 @@ nav .dropdown { min-width: 200px; } +table thead th, +table thead td, +.table thead th, +.table thead td { + background-color: var(--table-head-bg); + color: var(--table-head-color); + border-color: var(--theme-border-light); +} + +.menu .navbar-brand, +.menu .nav-link { + color: var(--bs-body-color); +} + +.menu .navbar-brand:hover, +.menu .nav-link:hover { + color: var(--bs-teal); +} + .search-menu { width: 20rem; max-width: 250px; @@ -157,8 +333,8 @@ nav .dropdown { } .search-bar:hover { - color: white; - border-color: white; + color: var(--bs-body-color); + border-color: var(--bs-body-color); } .search-bar:focus-within { @@ -201,6 +377,16 @@ nav .dropdown { height: 100px; } +.login-surface.bg-secondary, +.login-option.bg-secondary { + background-color: var(--login-surface-bg) !important; + color: var(--login-surface-text) !important; +} + +.login-option.text-light { + color: var(--login-surface-text) !important; +} + .login-option:hover { -webkit-transition: width 4s ease-in-out; -moz-transition: width 4s ease-in-out; @@ -369,11 +555,40 @@ dl.detail-horizontal dd { display: flex; justify-content: center; align-content: center; - background-color: var(--bs-gray-700); + background-color: var(--organization-logo-bg); border-left: 2px solid var(--bs-body-color); border-bottom: 2px solid var(--bs-body-color); } +.organization-content .bg-secondary { + background-color: var(--organization-panel-bg) !important; +} + +.organization-content .card-header, +.organization-content .organization-url, +.organization-content .organization-contact { + background-color: var(--organization-panel-bg); +} + +.member-group-accordion .accordion-item.bg-secondary, +.member-group-accordion .accordion-header.bg-secondary, +.member-group-accordion .accordion-collapse.bg-dark, +.member-group-accordion .bg-dark.border-start { + background-color: var(--organization-panel-bg) !important; + color: var(--bs-body-color) !important; +} + +.set-display-name-btn { + color: var(--set-display-name-color) !important; + border-color: var(--set-display-name-border) !important; +} + +.set-display-name-btn:hover { + color: var(--set-display-name-color) !important; + border-color: var(--set-display-name-border) !important; + background-color: var(--set-display-name-bg-hover) !important; +} + .organization-logo img { margin: auto; max-width: 100%; @@ -447,27 +662,55 @@ dl.detail-horizontal dd { .filter-accordion { --bs-accordion-color: var(--bs-body-color); - --bs-accordion-bg: transparent; - --bs-accordion-border-color: transparent; + --bs-accordion-bg: var(--filter-surface-bg); + --bs-accordion-border-color: var(--filter-surface-border); --bs-accordion-btn-color: var(--bs-body-color); - --bs-accordion-btn-bg: transparent; - --bs-accordion-btn-focus-border-color: transparent; + --bs-accordion-btn-bg: var(--filter-surface-bg); + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5 L8 11 L14 5'/%3e%3c/svg%3e"); + --bs-accordion-btn-focus-border-color: var(--filter-surface-border); --bs-accordion-btn-focus-box-shadow: unset; --bs-accordion-btn-active-icon: var(--bs-accordion-btn-icon); --bs-accordion-active-color: var(--bs-body-color); - --bs-accordion-active-bg: transparent; + --bs-accordion-active-bg: var(--filter-surface-bg); +} + +[data-bs-theme='dark'] .filter-accordion { + --bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='M2 5 L8 11 L14 5'/%3e%3c/svg%3e"); +} +.filter-accordion .accordion-item, +.filter-accordion .accordion-body, +.filter-accordion .accordion-collapse { + background-color: var(--filter-surface-bg); +} + +#orgFilter .accordion-header.bg-secondary, +#orgFilter .accordion-collapse { + border-color: var(--org-filter-border) !important; + border-width: 1px !important; +} + +.filter-accordion .accordion-header.bg-secondary, +.filter-accordion .accordion-button { + background-color: var(--filter-surface-bg) !important; + color: var(--bs-body-color); +} + +.member-group-accordion .accordion-item.bg-secondary, +.member-group-accordion .accordion-header.bg-secondary { + background-color: var(--filter-surface-bg) !important; + color: var(--bs-body-color); } .popover-body { - background-color: var(--bs-dark); + background-color: var(--bs-body-bg); --bs-popover-body-color: var(--bs-body-color); } .shift-info-popover { --bs-popover-max-width: 300px; - --bs-popover-border-color: var(--bs-dark); + --bs-popover-border-color: var(--bs-secondary); --bs-popover-header-bg: var(--bs-secondary); --bs-popover-body-padding-x: 1rem; --bs-popover-body-padding-y: .5rem; @@ -487,6 +730,10 @@ dl.detail-horizontal dd { white-space: nowrap; } +.member-shift-summary-table .table-active > * { + background-color: var(--member-summary-own-bg) !important; +} + td.disabled { opacity: 5%; } @@ -527,15 +774,3 @@ td.disabled { border: 1px solid var(--bs-body-color); } -/* -Easy (hopefully temporary) fix for issue, where backdrop was rendered above modal on IOS devices using safari browser. -This caused users on IOS to be soft locked out of the modal, as they could not interact with it. - -All positive z-index values (including 0) were also causing the issue. -Changing z-index to -1 makes sure backdrop is always behind the modal. -Changing the position of the modal to anything other than fixed also seemed to fix the issue. -*/ -/* TODO: Remove once this is not required anymore */ -.modal-backdrop { - z-index: -1 !important; -} diff --git a/src/shiftings/static/js/tests/dom-harness.cjs b/src/shiftings/static/js/tests/dom-harness.cjs new file mode 100644 index 0000000..c5f1764 --- /dev/null +++ b/src/shiftings/static/js/tests/dom-harness.cjs @@ -0,0 +1,304 @@ +const fs = require('node:fs'); +const vm = require('node:vm'); + +function createClassList(initialClasses = []) { + const classes = new Set(initialClasses); + return { + add(...classNames) { + for (const className of classNames) { + classes.add(className); + } + }, + remove(...classNames) { + for (const className of classNames) { + classes.delete(className); + } + }, + toggle(className, force) { + if (force === undefined) { + if (classes.has(className)) { + classes.delete(className); + return false; + } + classes.add(className); + return true; + } + if (force) { + classes.add(className); + } else { + classes.delete(className); + } + return force; + }, + contains(className) { + return classes.has(className); + }, + toArray() { + return Array.from(classes); + }, + }; +} + +function matchesSelector(element, selector) { + const attributeMatch = selector.match(/^\[([^=\]]+)=([^\]]+)\]$/); + if (attributeMatch) { + const attributeName = attributeMatch[1]; + const attributeValue = attributeMatch[2].replace(/^['"]|['"]$/g, ''); + const attribute = element.attributes?.[attributeName]; + return element[attributeName] === attributeValue || attribute === attributeValue || attribute?.nodeValue === attributeValue; + } + return false; +} + +function walk(element, callback) { + callback(element); + for (const child of element.children) { + walk(child, callback); + } +} + +function createElement({ + id = '', + tagName = 'DIV', + classNames = [], + dataset = {}, + value = '', + innerText = '', + textContent = '', + name = '', + type = '', + attributes = {}, +} = {}) { + const listeners = {}; + const element = { + id, + tagName: String(tagName).toUpperCase(), + classList: createClassList(classNames), + dataset, + children: [], + parentNode: null, + parentElement: null, + style: {}, + value, + innerText, + textContent, + name, + type, + attributes, + reportValidity() { + return true; + }, + focusCalled: false, + focus() { + element.focusCalled = true; + }, + addEventListener(eventName, callback) { + listeners[eventName] = callback; + }, + dispatchEvent(event) { + const dispatchedEvent = event || { type: undefined }; + if (!dispatchedEvent.target) { + dispatchedEvent.target = element; + } + dispatchedEvent.currentTarget = element; + if (listeners[dispatchedEvent.type]) { + listeners[dispatchedEvent.type](dispatchedEvent); + } + if (dispatchedEvent.bubbles && global.document && global.document !== element.ownerDocument) { + global.document.dispatchEvent(dispatchedEvent); + } + return true; + }, + appendChild(child) { + child.parentNode = element; + child.parentElement = element; + child.ownerDocument = element.ownerDocument; + element.children.push(child); + if (global.document && typeof global.document._registerTree === 'function') { + global.document._registerTree(child); + } + return child; + }, + getElementsByClassName(className) { + const result = []; + walk(element, (node) => { + if (node.classList.contains(className)) { + result.push(node); + } + }); + return result; + }, + getElementsByName(searchName) { + const result = []; + walk(element, (node) => { + if (node.name === searchName) { + result.push(node); + } + }); + return result; + }, + querySelectorAll(selector) { + const result = []; + if (selector.startsWith('.')) { + const className = selector.slice(1); + walk(element, (node) => { + if (node.classList.contains(className)) { + result.push(node); + } + }); + } + return result; + }, + querySelector(selector) { + let found = null; + walk(element, (node) => { + if (!found && matchesSelector(node, selector)) { + found = node; + } + }); + return found; + }, + getAttribute(attributeName) { + const attribute = attributes[attributeName]; + return attribute?.nodeValue ?? attribute; + }, + setAttribute(attributeName, attributeValue) { + attributes[attributeName] = { nodeValue: attributeValue }; + }, + }; + return element; +} + +function createDocument({ root = createElement({ tagName: 'HTML', id: 'root' }), locationHref = 'about:blank' } = {}) { + const elementsById = new Map(); + const listeners = {}; + const document = { + documentElement: root, + location: { href: locationHref }, + addEventListener(eventName, callback) { + listeners[eventName] = callback; + }, + dispatchEvent(event) { + const dispatchedEvent = typeof event === 'string' ? { type: event } : event; + if (!dispatchedEvent.target) { + dispatchedEvent.target = document; + } + dispatchedEvent.currentTarget = document; + if (listeners[dispatchedEvent.type]) { + listeners[dispatchedEvent.type](dispatchedEvent); + } + return true; + }, + getElementById(id) { + return elementsById.get(id) || null; + }, + getElementsByClassName(className) { + const result = []; + walk(root, (node) => { + if (node.classList.contains(className)) { + result.push(node); + } + }); + return result; + }, + getElementsByName(searchName) { + const result = []; + walk(root, (node) => { + if (node.name === searchName) { + result.push(node); + } + }); + return result; + }, + querySelectorAll(selector) { + const result = []; + if (selector.startsWith('.')) { + const className = selector.slice(1); + walk(root, (node) => { + if (node.classList.contains(className)) { + result.push(node); + } + }); + } + return result; + }, + createElement(tagName) { + const element = createElement({ tagName }); + element.ownerDocument = document; + return element; + }, + _registerTree(element) { + walk(element, (node) => { + node.ownerDocument = document; + if (node.id) { + elementsById.set(node.id, node); + } + }); + }, + }; + root.ownerDocument = document; + document._registerTree(root); + return document; +} + +class MockMutationObserver { + constructor(callback) { + this.callback = callback; + this.target = null; + this.options = null; + MockMutationObserver.instances.push(this); + } + + observe(target, options) { + this.target = target; + this.options = options; + } + + trigger(records = []) { + this.callback(records, this); + } +} + +MockMutationObserver.instances = []; + +class MockCustomEvent { + constructor(type, init = {}) { + this.type = type; + Object.assign(this, init); + } +} + +function installDomGlobals(document, extras = {}) { + global.document = document; + global.window = extras.window || {}; + global.CustomEvent = extras.CustomEvent || MockCustomEvent; + global.MutationObserver = extras.MutationObserver || MockMutationObserver; + if (extras.saveAndRestore) { + global.saveAndRestore = extras.saveAndRestore; + } + return document; +} + +function teardownDomGlobals() { + delete global.document; + delete global.window; + delete global.CustomEvent; + delete global.MutationObserver; + delete global.localStorage; + delete global.saveAndRestore; +} + +function loadScript(scriptPath) { + const scriptContent = fs.readFileSync(scriptPath, 'utf8'); + vm.runInThisContext(scriptContent, { filename: scriptPath }); +} + +module.exports = { + MockCustomEvent, + MockMutationObserver, + createDocument, + createElement, + installDomGlobals, + loadScript, + teardownDomGlobals, +}; diff --git a/src/shiftings/static/js/tests/formset.test.cjs b/src/shiftings/static/js/tests/formset.test.cjs new file mode 100644 index 0000000..39d4e71 --- /dev/null +++ b/src/shiftings/static/js/tests/formset.test.cjs @@ -0,0 +1,322 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const scriptPath = path.resolve(__dirname, '..', 'formset.js'); +const scriptContent = fs.readFileSync(scriptPath, 'utf8'); + +function makeClassList(initialClasses = []) { + const classes = new Set(initialClasses); + return { + add(...classNames) { + for (const className of classNames) { + classes.add(className); + } + }, + remove(...classNames) { + for (const className of classNames) { + classes.delete(className); + } + }, + toggle(className, force) { + if (force === undefined) { + if (classes.has(className)) { + classes.delete(className); + return false; + } + classes.add(className); + return true; + } + if (force) { + classes.add(className); + } else { + classes.delete(className); + } + return force; + }, + contains(className) { + return classes.has(className); + }, + }; +} + +function makeElement({ id = '', tagName = 'DIV', classNames = [], innerHTML = '', value = '' } = {}) { + const listeners = {}; + const element = { + id, + tagName, + classList: makeClassList(classNames), + innerHTML, + value, + parentNode: null, + children: [], + style: {}, + addEventListener(eventName, callback) { + listeners[eventName] = callback; + }, + click() { + const callback = listeners.click; + if (callback) { + callback({ currentTarget: element }); + } + }, + appendChild(child) { + if (child && child.isFragment) { + for (const node of child.children) { + element.appendChild(node); + } + return child; + } + child.parentNode = element; + child.parentElement = element; + element.children.push(child); + if (global.document && typeof global.document._registerTree === 'function') { + global.document._registerTree(child); + } + return child; + }, + insertAdjacentElement(position, node) { + if (position === 'beforeend') { + return element.appendChild(node); + } + return null; + }, + insertBefore(node, referenceNode) { + const index = element.children.indexOf(referenceNode); + if (index < 0) { + return element.appendChild(node); + } + node.parentNode = element; + node.parentElement = element; + element.children.splice(index, 0, node); + if (global.document && typeof global.document._registerTree === 'function') { + global.document._registerTree(node); + } + return node; + }, + insertAdjacentHTML(position, html) { + if (position !== 'beforeend') { + return; + } + const idMatch = html.match(/id=\"([^\"]+)\"/); + const id = idMatch ? idMatch[1] : ''; + const checkbox = makeElement({ id, tagName: 'INPUT', classNames: ['d-none'] }); + checkbox.checked = true; + checkbox.name = html.match(/name=\"([^\"]+)\"/)?.[1] || ''; + element.appendChild(checkbox); + }, + getElementsByClassName(className) { + const result = []; + for (const child of element.children) { + if (child.classList.contains(className)) { + result.push(child); + } + result.push(...child.getElementsByClassName(className)); + } + return result; + }, + querySelector(selector) { + if (selector === '[type=submit]') { + for (const child of element.children) { + if (child.type === 'submit') { + return child; + } + const nested = child.querySelector(selector); + if (nested) { + return nested; + } + } + } + return null; + }, + }; + return element; +} + +function cloneTree(node) { + const copy = makeElement({ + id: node.id, + tagName: node.tagName, + innerHTML: node.innerHTML, + classNames: [], + value: node.value, + }); + for (const className of ['formset-form-remove', 'formset-form-restore', 'border', 'd-none']) { + if (node.classList.contains(className)) { + copy.classList.add(className); + } + } + for (const child of node.children) { + copy.appendChild(cloneTree(child)); + } + return copy; +} + +function makeDocument(elements = []) { + const elementsById = new Map(); + const domListeners = {}; + + const registerTree = (element) => { + if (element.id) { + elementsById.set(element.id, element); + } + for (const child of element.children) { + registerTree(child); + } + }; + + for (const element of elements) { + registerTree(element); + } + + return { + _registerTree: registerTree, + addEventListener(eventName, callback) { + domListeners[eventName] = callback; + }, + dispatch(eventName) { + const callback = domListeners[eventName]; + if (callback) { + callback(); + } + }, + getElementById(id) { + return elementsById.get(id) || null; + }, + getElementsByClassName(className) { + const result = []; + for (const element of elementsById.values()) { + if (element.classList.contains(className)) { + result.push(element); + } + } + return result; + }, + createElement(tagName) { + return makeElement({ tagName: String(tagName).toUpperCase() }); + }, + }; +} + +function loadFormsetScript() { + // eslint-disable-next-line no-new-func + new Function(scriptContent)(); +} + +function setupAddFormEnvironment(maxForms = '1') { + const root = makeElement({ id: 'root' }); + const form = makeElement({ id: 'formset_form' }); + const insert = makeElement({ id: 'formset_insert' }); + const addLink = makeElement({ id: 'formset_add', tagName: 'A' }); + const total = makeElement({ id: 'id_form-TOTAL_FORMS', tagName: 'INPUT', value: '0' }); + const max = makeElement({ id: 'id_form-MAX_NUM_FORMS', tagName: 'INPUT', value: maxForms }); + const emptyAlert = makeElement({ id: 'empty_alert', classNames: [] }); + + root.appendChild(form); + form.appendChild(addLink); + form.appendChild(total); + form.appendChild(max); + form.appendChild(insert); + form.appendChild(emptyAlert); + + const template = makeElement({ id: 'formset_template', tagName: 'TEMPLATE' }); + const childContainer = makeElement({ id: 'id_form-__prefix___container', classNames: ['border'] }); + const childRemove = makeElement({ id: 'id_form-__prefix___remove', classNames: ['formset-form-remove'] }); + const childRestore = makeElement({ + id: 'id_form-__prefix___restore', + classNames: ['formset-form-restore', 'd-none'], + }); + + template.content = { + cloneNode() { + return { + isFragment: true, + children: [cloneTree(childContainer), cloneTree(childRemove), cloneTree(childRestore)], + }; + }, + }; + + const document = makeDocument([root, form, addLink, insert, total, max, emptyAlert, template]); + global.document = document; + + return { document, insert, total, emptyAlert }; +} + +function setupRemoveRestoreEnvironment() { + const form = makeElement({ id: 'id_form-0_container', classNames: ['border'] }); + const innerBorder = makeElement({ id: 'id_form-0_inner', classNames: ['border'] }); + const remove = makeElement({ id: 'id_form-0_remove', classNames: ['formset-form-remove'] }); + const restore = makeElement({ id: 'id_form-0_restore', classNames: ['formset-form-restore', 'd-none'] }); + + form.appendChild(innerBorder); + form.appendChild(remove); + form.appendChild(restore); + + const total = makeElement({ id: 'id_form-TOTAL_FORMS', tagName: 'INPUT', value: '1' }); + const max = makeElement({ id: 'id_form-MAX_NUM_FORMS', tagName: 'INPUT', value: '10' }); + const template = makeElement({ id: 'formset_template', tagName: 'TEMPLATE' }); + template.content = { cloneNode: () => ({ isFragment: true, children: [] }) }; + + const holder = makeElement({ id: 'holder' }); + holder.appendChild(total); + holder.appendChild(max); + holder.appendChild(form); + + const document = makeDocument([holder, total, max, form, innerBorder, remove, restore, template]); + global.document = document; + + return { document, form, innerBorder, remove, restore }; +} + +function teardown() { + delete global.document; +} + +test('adds new form, updates total, and hides add button at max', () => { + const env = setupAddFormEnvironment('1'); + loadFormsetScript(); + env.document.dispatch('DOMContentLoaded'); + + const addLink = env.document.getElementById('formset_add'); + assert.ok(addLink); + + addLink.click(); + + assert.equal(env.emptyAlert.classList.contains('d-none'), true); + assert.equal(String(env.total.value), '1'); + assert.equal(addLink.classList.contains('d-none'), true); + assert.ok(env.document.getElementById('id_form-0_container')); + assert.ok(env.document.getElementById('id_form-0_remove')); + assert.ok(env.document.getElementById('id_form-0_restore')); + + teardown(); +}); + +test('remove and restore toggles delete checkbox and visual state', () => { + const env = setupRemoveRestoreEnvironment(); + loadFormsetScript(); + env.document.dispatch('DOMContentLoaded'); + + env.remove.click(); + + const deleteElement = env.document.getElementById('id_form-0-DELETE'); + assert.ok(deleteElement); + assert.equal(deleteElement.checked, true); + assert.equal(env.remove.classList.contains('d-none'), true); + assert.equal(env.restore.classList.contains('d-none'), false); + assert.equal(env.form.classList.contains('border-danger'), true); + assert.equal(env.innerBorder.classList.contains('border-danger'), true); + assert.equal(env.form.style.opacity, '.5'); + + env.restore.click(); + + assert.equal(deleteElement.checked, false); + assert.equal(env.remove.classList.contains('d-none'), false); + assert.equal(env.restore.classList.contains('d-none'), true); + assert.equal(env.form.classList.contains('border-danger'), false); + assert.equal(env.innerBorder.classList.contains('border-danger'), false); + assert.equal(env.form.style.opacity, '1'); + + teardown(); +}); diff --git a/src/shiftings/static/js/tests/modal-handlers.test.cjs b/src/shiftings/static/js/tests/modal-handlers.test.cjs new file mode 100644 index 0000000..98033b2 --- /dev/null +++ b/src/shiftings/static/js/tests/modal-handlers.test.cjs @@ -0,0 +1,111 @@ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const test = require('node:test'); + +const { + MockCustomEvent, + createDocument, + createElement, + installDomGlobals, + loadScript, + teardownDomGlobals, +} = require('./dom-harness.cjs'); + +function setupRemoveModalEnvironment() { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const modal = createElement({ id: 'confirmRemoveModal' }); + const placeholder1 = createElement({ id: 'remove_object_placeholder_1' }); + const placeholder2 = createElement({ id: 'remove_object_placeholder_2' }); + const successUrl = createElement({ id: 'id_success_url', tagName: 'INPUT', value: '' }); + const form = createElement({ id: 'id_remove_form' }); + root.appendChild(modal); + root.appendChild(placeholder1); + root.appendChild(placeholder2); + root.appendChild(successUrl); + root.appendChild(form); + + const document = createDocument({ root }); + installDomGlobals(document, { CustomEvent: MockCustomEvent }); + return { document, modal, placeholder1, placeholder2, successUrl, form }; +} + +function setupAutofocusEnvironment() { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const modal = createElement({ id: 'modal' }); + const autofocusField = createElement({ id: 'focus-me', tagName: 'INPUT' }); + autofocusField.setAttribute('autofocus', 'autofocus'); + modal.appendChild(autofocusField); + root.appendChild(modal); + + const document = createDocument({ root }); + installDomGlobals(document, { CustomEvent: MockCustomEvent }); + return { document, modal, autofocusField }; +} + +function setupSelectShiftOrgEnvironment() { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const modal = createElement({ id: 'selectShiftOrgModal' }); + const actionDate = createElement({ id: 'id_action_date', tagName: 'INPUT', value: '' }); + root.appendChild(modal); + root.appendChild(actionDate); + + const document = createDocument({ root }); + installDomGlobals(document, { CustomEvent: MockCustomEvent }); + return { document, modal, actionDate }; +} + +test('remove modal fills placeholders from triggering button', () => { + const env = setupRemoveModalEnvironment(); + loadScript(path.resolve(__dirname, '..', 'remove_modal.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + const button = { + getAttribute(attributeName) { + return { + 'data-bs-remove-url': '/delete/1/', + 'data-bs-remove-name': 'Test item', + 'data-bs-success-url': '/success/', + }[attributeName]; + }, + }; + + env.modal.dispatchEvent(new MockCustomEvent('show.bs.modal', { relatedTarget: button })); + + assert.equal(env.form.action, '/delete/1/'); + assert.equal(env.placeholder1.textContent, 'Test item'); + assert.equal(env.placeholder2.textContent, 'Test item'); + assert.equal(env.successUrl.value, '/success/'); + + teardownDomGlobals(); +}); + +test('modal autofocus focuses first autofocus field', () => { + const env = setupAutofocusEnvironment(); + loadScript(path.resolve(__dirname, '..', '..', '..', 'shifts', 'static', 'js', 'modal_autofocus.js')); + + env.document.dispatchEvent(new MockCustomEvent('shown.bs.modal', { target: env.modal })); + + assert.equal(env.autofocusField.focusCalled, true); + + teardownDomGlobals(); +}); + +test('calendar shift modal copies date into action field', () => { + const env = setupSelectShiftOrgEnvironment(); + loadScript(path.resolve(__dirname, '..', '..', '..', 'cal', 'static', 'js', 'set_cal_shift_create_date.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + const button = { + getAttribute(attributeName) { + return { + 'data-bs-date': '2026-04-17', + }[attributeName]; + }, + }; + + env.modal.dispatchEvent(new MockCustomEvent('show.bs.modal', { relatedTarget: button })); + + assert.equal(env.actionDate.value, '2026-04-17'); + + teardownDomGlobals(); +}); diff --git a/src/shiftings/static/js/tests/navigation-and-sliders.test.cjs b/src/shiftings/static/js/tests/navigation-and-sliders.test.cjs new file mode 100644 index 0000000..a069fec --- /dev/null +++ b/src/shiftings/static/js/tests/navigation-and-sliders.test.cjs @@ -0,0 +1,199 @@ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const test = require('node:test'); + +const { + MockCustomEvent, + MockMutationObserver, + createDocument, + createElement, + installDomGlobals, + loadScript, + teardownDomGlobals, +} = require('./dom-harness.cjs'); + +function makeTimeSliderGroup({ sliderId = 'slider-1', name = 'start_delay_1', value = '20', start = '0:50', startDay = '0', min = '0', max = '90' } = {}) { + const outer = createElement({ id: `${sliderId}-outer` }); + const middle = createElement({ id: `${sliderId}-middle` }); + const slider = createElement({ id: sliderId, tagName: 'INPUT', classNames: ['time-slider'], value, name }); + slider.attributes = { + start: { nodeValue: start }, + startDay: { nodeValue: startDay }, + }; + slider.min = min; + slider.max = max; + + const display = createElement({ id: `${sliderId}-display`, tagName: 'INPUT', classNames: ['time-slider-display'], value: '' }); + const dayContainer = createElement({ id: `${sliderId}-day-container`, classNames: ['time-slider-day-container'] }); + const day = createElement({ id: `${sliderId}-day`, classNames: ['time-slider-day'], innerText: '' }); + + middle.appendChild(slider); + middle.appendChild(display); + middle.appendChild(dayContainer); + middle.appendChild(day); + outer.appendChild(middle); + + return { outer, middle, slider, display, dayContainer, day }; +} + +function setupTimeSliderEnvironment(groups) { + const root = createElement({ tagName: 'HTML', id: 'root' }); + for (const group of groups) { + root.appendChild(group.outer); + } + const document = createDocument({ root }); + installDomGlobals(document, { MutationObserver: MockMutationObserver, CustomEvent: MockCustomEvent }); + return { document, root }; +} + +function setupLinkSlidersEnvironment() { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const group = makeTimeSliderGroup({ sliderId: 'start-slider', name: 'start_delay_1', value: '30', start: '1:15', startDay: '2' }); + group.display.value = '1:15'; + group.day.innerText = '2'; + const duration = createElement({ id: 'duration-1', tagName: 'INPUT', name: 'duration_1' }); + duration.attributes = { + start: { nodeValue: '' }, + startDay: { nodeValue: '' }, + }; + const eventSpy = { calls: 0, lastEvent: null }; + duration.addEventListener('startChanged', (event) => { + eventSpy.calls += 1; + eventSpy.lastEvent = event; + }); + + root.appendChild(group.outer); + root.appendChild(duration); + const document = createDocument({ root }); + installDomGlobals(document, { MutationObserver: MockMutationObserver, CustomEvent: MockCustomEvent }); + return { document, group, duration, eventSpy }; +} + +function setupTimeFrameVisibilityEnvironment(initialValue = '1') { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const timeFrame = createElement({ id: 'id_time_frame_field', tagName: 'SELECT', value: initialValue }); + const monthWrapper = createElement({ id: 'month-wrapper' }); + const month = createElement({ id: 'id_month_field', tagName: 'INPUT', value: '4' }); + monthWrapper.appendChild(month); + const weekdayWrapper = createElement({ id: 'weekday-wrapper' }); + const weekday = createElement({ id: 'id_week_day_field', tagName: 'INPUT', value: '2' }); + weekdayWrapper.appendChild(weekday); + root.appendChild(timeFrame); + root.appendChild(monthWrapper); + root.appendChild(weekdayWrapper); + + const calls = { save: [], restore: [] }; + const saveAndRestore = { + save(...args) { + calls.save.push(args); + }, + restore(...args) { + calls.restore.push(args); + }, + }; + + const document = createDocument({ root }); + installDomGlobals(document, { MutationObserver: MockMutationObserver, CustomEvent: MockCustomEvent, saveAndRestore }); + return { document, timeFrame, month, monthWrapper, weekday, weekdayWrapper, calls }; +} + +test('chickens easter egg redirects after key sequence', () => { + const root = createElement({ tagName: 'HTML', id: 'root' }); + const document = createDocument({ root, locationHref: '/start/' }); + installDomGlobals(document, { MutationObserver: MockMutationObserver, CustomEvent: MockCustomEvent }); + loadScript(path.resolve(__dirname, '..', 'chickens.js')); + + for (const key of ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight', 'b', 'a']) { + document.dispatchEvent({ type: 'keyup', key }); + } + + assert.equal(document.location.href, 'https://www.youtube.com/watch?v=AVlzryCQg8s'); + + teardownDomGlobals(); +}); + +test('time slider updates display and day from slider input', () => { + const group = makeTimeSliderGroup(); + const env = setupTimeSliderEnvironment([group]); + loadScript(path.resolve(__dirname, '..', 'time_slider.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + assert.equal(group.display.value, '1:10'); + assert.equal(group.dayContainer.classList.contains('d-none'), true); + assert.equal(group.day.innerText, '0'); + + group.slider.value = '90'; + env.document.dispatchEvent({ type: 'input', target: group.slider }); + + assert.equal(group.display.value, '2:20'); + assert.equal(group.dayContainer.classList.contains('d-none'), true); + assert.equal(group.day.innerText, '0'); + + group.display.value = '3:30'; + env.document.dispatchEvent({ type: 'input', target: group.display }); + + assert.equal(group.slider.value, '90'); + assert.equal(group.display.value, '2:20'); + + teardownDomGlobals(); +}); + +test('time slider initializes newly observed nodes', () => { + const group = makeTimeSliderGroup({ sliderId: 'slider-1' }); + const env = setupTimeSliderEnvironment([group]); + loadScript(path.resolve(__dirname, '..', 'time_slider.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + const newGroup = makeTimeSliderGroup({ sliderId: 'slider-2', name: 'start_delay_2', value: '15', start: '2:00', startDay: '1' }); + env.root.appendChild(newGroup.outer); + const observer = MockMutationObserver.instances.at(-1); + observer.trigger(); + + assert.equal(newGroup.display.value, '2:15'); + assert.equal(newGroup.day.innerText, '1'); + + teardownDomGlobals(); +}); + +test('link sliders copies start values into duration target and emits startChanged', () => { + const env = setupLinkSlidersEnvironment(); + loadScript(path.resolve(__dirname, '..', '..', '..', 'shifts', 'static', 'js', 'link_sliders.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + assert.equal(env.duration.attributes.start.nodeValue, '1:15'); + assert.equal(env.duration.attributes.startDay.nodeValue, '2'); + assert.equal(env.eventSpy.calls, 1); + assert.equal(env.eventSpy.lastEvent.target, env.duration); + + env.group.display.value = '2:05'; + env.group.day.innerText = '3'; + env.group.slider.value = '75'; + env.document.dispatchEvent({ type: 'input', target: env.group.slider }); + + assert.equal(env.duration.attributes.start.nodeValue, '2:05'); + assert.equal(env.duration.attributes.startDay.nodeValue, '3'); + assert.equal(env.eventSpy.calls, 2); + + teardownDomGlobals(); +}); + +test('time frame visibility toggles month and weekday fields', () => { + const env = setupTimeFrameVisibilityEnvironment('1'); + loadScript(path.resolve(__dirname, '..', '..', '..', 'shifts', 'static', 'js', 'time_frame_visibility.js')); + env.document.dispatchEvent('DOMContentLoaded'); + + assert.equal(env.monthWrapper.classList.contains('d-none'), true); + assert.equal(env.weekdayWrapper.classList.contains('d-none'), false); + assert.equal(env.calls.save.length, 1); + assert.equal(env.calls.restore.length, 1); + + env.timeFrame.value = '5'; + env.timeFrame.dispatchEvent({ type: 'change', target: env.timeFrame }); + + assert.equal(env.monthWrapper.classList.contains('d-none'), false); + assert.equal(env.weekdayWrapper.classList.contains('d-none'), true); + assert.equal(env.calls.save.length, 2); + assert.equal(env.calls.restore.length, 2); + + teardownDomGlobals(); +}); diff --git a/src/shiftings/static/js/tests/save_and_restore.test.cjs b/src/shiftings/static/js/tests/save_and_restore.test.cjs new file mode 100644 index 0000000..2bba434 --- /dev/null +++ b/src/shiftings/static/js/tests/save_and_restore.test.cjs @@ -0,0 +1,82 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const test = require('node:test'); + +const { createElement } = require('./dom-harness.cjs'); + +const scriptPath = path.resolve(__dirname, '..', 'save_and_restore.js'); +const scriptContent = fs.readFileSync(scriptPath, 'utf8'); + +function loadSaveAndRestore() { + // eslint-disable-next-line no-new-func + return new Function(`${scriptContent}\nreturn saveAndRestore;`)(); +} + +function makeComponent(tagName, id, value) { + const wrapper = createElement({ id: `${id}-wrapper` }); + const component = createElement({ tagName, id, value, innerText: value }); + if (tagName === 'SELECT') { + component.selectedIndex = value; + } + wrapper.appendChild(component); + return component; +} + +test('save hides and clears components, restore shows and restores values', () => { + const saveAndRestore = loadSaveAndRestore(); + + const input = makeComponent('INPUT', 'input-id', 'abc'); + const select = makeComponent('SELECT', 'select-id', 2); + const text = makeComponent('SPAN', 'text-id', 'hello'); + + saveAndRestore.save(true, true, input, select, text); + + assert.equal(input.parentNode.classList.contains('d-none'), true); + assert.equal(select.parentNode.classList.contains('d-none'), true); + assert.equal(text.parentNode.classList.contains('d-none'), true); + + assert.equal(input.value, ''); + assert.equal(select.selectedIndex, 0); + assert.equal(text.innerText, ''); + + saveAndRestore.restore(true, input, select, text); + + assert.equal(input.parentNode.classList.contains('d-none'), false); + assert.equal(select.parentNode.classList.contains('d-none'), false); + assert.equal(text.parentNode.classList.contains('d-none'), false); + + assert.equal(input.value, 'abc'); + assert.equal(select.selectedIndex, 2); + assert.equal(text.innerText, 'hello'); +}); + +test('save keeps previously stored non-empty value when later value is empty', () => { + const saveAndRestore = loadSaveAndRestore(); + + const input = makeComponent('INPUT', 'persist-id', 'first'); + saveAndRestore.save(false, false, input); + + input.value = ''; + saveAndRestore.save(false, false, input); + + input.value = 'changed'; + saveAndRestore.restore(false, input); + + assert.equal(input.value, 'first'); +}); + +test('restore only applies once for stored values', () => { + const saveAndRestore = loadSaveAndRestore(); + + const input = makeComponent('INPUT', 'once-id', 'value-1'); + saveAndRestore.save(false, false, input); + + input.value = 'value-2'; + saveAndRestore.restore(false, input); + assert.equal(input.value, 'value-1'); + + input.value = 'value-3'; + saveAndRestore.restore(false, input); + assert.equal(input.value, 'value-3'); +}); diff --git a/src/shiftings/static/js/tests/theme-toggle.test.cjs b/src/shiftings/static/js/tests/theme-toggle.test.cjs new file mode 100644 index 0000000..fe2ab9f --- /dev/null +++ b/src/shiftings/static/js/tests/theme-toggle.test.cjs @@ -0,0 +1,175 @@ +const assert = require('node:assert/strict'); +const path = require('node:path'); +const test = require('node:test'); + +const { + createDocument, + createElement, + installDomGlobals, + loadScript, + teardownDomGlobals, +} = require('./dom-harness.cjs'); + +function setupEnvironment({ userTheme = 'auto', storageTheme = null, xMatch = false, lightMatch = false } = {}) { + const root = createElement({ tagName: 'HTML', id: 'root', dataset: { userTheme } }); + const lightOption = createElement({ id: 'light-option', tagName: 'A', classNames: ['js-theme-option'], dataset: { theme: 'light', themeForm: 'theme-light-form' } }); + const darkOption = createElement({ id: 'dark-option', tagName: 'A', classNames: ['js-theme-option'], dataset: { theme: 'dark', themeForm: 'theme-dark-form' } }); + const autoOption = createElement({ id: 'auto-option', tagName: 'A', classNames: ['js-theme-option'], dataset: { theme: 'auto', themeForm: 'theme-auto-form' } }); + const lightForm = createElement({ id: 'theme-light-form', tagName: 'FORM' }); + const darkForm = createElement({ id: 'theme-dark-form', tagName: 'FORM' }); + const autoForm = createElement({ id: 'theme-auto-form', tagName: 'FORM' }); + + lightForm.submit = function submit() { + this.submitted = true; + }; + darkForm.submit = function submit() { + this.submitted = true; + }; + autoForm.submit = function submit() { + this.submitted = true; + }; + + root.appendChild(lightOption); + root.appendChild(darkOption); + root.appendChild(autoOption); + root.appendChild(lightForm); + root.appendChild(darkForm); + root.appendChild(autoForm); + + const document = createDocument({ root }); + const storage = new Map(); + if (storageTheme !== null) { + storage.set('shiftings-theme-preference', storageTheme); + } + + const queryMatch = new Map([ + ['(prefers-color-scheme: x)', xMatch], + ['(prefers-color-scheme: light)', lightMatch], + ]); + const listenersByQuery = new Map(); + + installDomGlobals(document, { + window: { + matchMedia(query) { + const listeners = []; + listenersByQuery.set(query, listeners); + return { + get matches() { + return Boolean(queryMatch.get(query)); + }, + addEventListener(eventName, callback) { + if (eventName === 'change') { + listeners.push(callback); + } + }, + }; + }, + }, + }); + global.localStorage = { + getItem(key) { + return storage.has(key) ? storage.get(key) : null; + }, + setItem(key, value) { + storage.set(key, value); + }, + }; + + return { + document, + root, + lightOption, + darkOption, + autoOption, + lightForm, + darkForm, + autoForm, + storage, + queryMatch, + listenersByQuery, + }; +} + +function teardownEnvironment() { + teardownDomGlobals(); +} + +function runThemeToggleScript() { + loadScript(path.resolve(__dirname, '..', 'theme-toggle.js')); +} + +test('prefers localStorage theme over account theme', () => { + const env = setupEnvironment({ userTheme: 'dark', storageTheme: 'light' }); + runThemeToggleScript(); + + assert.equal(env.root.dataset.themeMode, 'light'); + assert.equal(env.root.dataset.bsTheme, 'light'); + assert.equal(env.lightOption.classList.contains('active'), true); + assert.equal(env.darkOption.classList.contains('active'), false); + assert.equal(env.autoOption.classList.contains('active'), false); + + teardownEnvironment(); +}); + +test('falls back to account theme when no localStorage value exists', () => { + const env = setupEnvironment({ userTheme: 'dark', storageTheme: null }); + runThemeToggleScript(); + + assert.equal(env.root.dataset.themeMode, 'dark'); + assert.equal(env.root.dataset.bsTheme, 'dark'); + + teardownEnvironment(); +}); + +test('clicking option saves preference, updates active entry, and submits linked form', () => { + const env = setupEnvironment({ userTheme: 'auto', storageTheme: null, xMatch: true }); + runThemeToggleScript(); + + const autoClickEvent = { + defaultPrevented: false, + preventDefault() { + this.defaultPrevented = true; + }, + type: 'click', + }; + env.autoOption.dispatchEvent(autoClickEvent); + + assert.equal(autoClickEvent.defaultPrevented, true); + assert.equal(env.storage.get('shiftings-theme-preference'), 'auto'); + assert.equal(env.root.dataset.themeMode, 'auto'); + assert.equal(env.root.dataset.bsTheme, 'light'); + assert.equal(env.autoForm.submitted, true); + assert.equal(env.autoOption.classList.contains('active'), true); + assert.equal(env.lightOption.classList.contains('active'), false); + assert.equal(env.darkOption.classList.contains('active'), false); + + teardownEnvironment(); +}); + +test('system theme change re-applies theme only in auto mode', () => { + const env = setupEnvironment({ userTheme: 'auto', storageTheme: 'auto', xMatch: false, lightMatch: false }); + runThemeToggleScript(); + + const systemListeners = env.listenersByQuery.get('(prefers-color-scheme: light)'); + assert.ok(systemListeners); + assert.equal(systemListeners.length, 1); + + env.queryMatch.set('(prefers-color-scheme: x)', true); + env.queryMatch.set('(prefers-color-scheme: light)', true); + systemListeners[0](); + + assert.equal(env.root.dataset.themeMode, 'auto'); + assert.equal(env.root.dataset.bsTheme, 'light'); + + env.darkOption.dispatchEvent({ type: 'click', preventDefault() {} }); + assert.equal(env.root.dataset.themeMode, 'dark'); + + env.queryMatch.set('(prefers-color-scheme: x)', false); + env.queryMatch.set('(prefers-color-scheme: light)', false); + systemListeners[0](); + + assert.equal(env.root.dataset.themeMode, 'dark'); + assert.equal(env.root.dataset.bsTheme, 'dark'); + + teardownEnvironment(); +}); diff --git a/src/shiftings/static/js/theme-toggle.js b/src/shiftings/static/js/theme-toggle.js new file mode 100644 index 0000000..fd3f254 --- /dev/null +++ b/src/shiftings/static/js/theme-toggle.js @@ -0,0 +1,75 @@ +(() => { + const STORAGE_KEY = 'shiftings-theme-preference'; + const root = document.documentElement; + + const resolveTheme = (mode) => { + if (mode === 'auto') { + if (!window.matchMedia) { + return 'dark'; + } + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + return mode; + }; + + const applyTheme = (mode) => { + root.dataset.themeMode = mode; + root.dataset.bsTheme = resolveTheme(mode); + }; + + const saveTheme = (mode) => { + localStorage.setItem(STORAGE_KEY, mode); + applyTheme(mode); + }; + + const updateActiveEntry = (mode) => { + const entries = document.querySelectorAll('.js-theme-option'); + entries.forEach((entry) => { + entry.classList.toggle('active', entry.dataset.theme === mode); + }); + }; + + const storageTheme = localStorage.getItem(STORAGE_KEY); + const hasStorageTheme = storageTheme !== null; + const accountTheme = root.dataset.userTheme || 'auto'; + const preferredTheme = hasStorageTheme ? storageTheme : accountTheme; + + applyTheme(preferredTheme); + updateActiveEntry(preferredTheme); + + if (window.matchMedia) { + const systemTheme = window.matchMedia('(prefers-color-scheme: light)'); + const onSystemChange = () => { + if ((root.dataset.themeMode || 'auto') === 'auto') { + applyTheme('auto'); + updateActiveEntry('auto'); + } + }; + if (typeof systemTheme.addEventListener === 'function') { + systemTheme.addEventListener('change', onSystemChange); + } else if (typeof systemTheme.addListener === 'function') { + systemTheme.addListener(onSystemChange); + } + } + + const options = document.querySelectorAll('.js-theme-option'); + options.forEach((option) => { + option.addEventListener('click', (event) => { + event.preventDefault(); + const mode = option.dataset.theme; + if (!mode) { + return; + } + saveTheme(mode); + updateActiveEntry(mode); + const formId = option.dataset.themeForm; + if (!formId) { + return; + } + const form = document.getElementById(formId); + if (form) { + form.submit(); + } + }); + }); +})(); diff --git a/src/shiftings/templates/base.html b/src/shiftings/templates/base.html index 4675027..44f7981 100644 --- a/src/shiftings/templates/base.html +++ b/src/shiftings/templates/base.html @@ -1,10 +1,29 @@ - + {# #} + {% bootstrap_css %} @@ -60,10 +79,12 @@ {% endif %} {% block content %}{% endblock %} +{% block modals %}{% endblock %}
© bjökgda🐔, 2022-{{ today.year }}
{% bootstrap_javascript %} + - \ No newline at end of file + diff --git a/src/shiftings/templates/generic/fluid_form_base.html b/src/shiftings/templates/generic/fluid_form_base.html index 6ed6b78..3a46858 100644 --- a/src/shiftings/templates/generic/fluid_form_base.html +++ b/src/shiftings/templates/generic/fluid_form_base.html @@ -1,12 +1,3 @@ -{% extends "base.html" %} -{% block breadcrumbs %} - {% include "template/breadcrumbs.html" %} -{% endblock %} -{% block content %} -
-
- {% block form %} - {% endblock %} -
-
-{% endblock %} \ No newline at end of file +{% extends "generic/form_base.html" %} +{% block form_container_class %}container-fluid{% endblock %} +{% block form_content_class %}col-lg-12 col-xl-8{% endblock %} diff --git a/src/shiftings/templates/generic/form_base.html b/src/shiftings/templates/generic/form_base.html index 63fd9f1..d5286a2 100644 --- a/src/shiftings/templates/generic/form_base.html +++ b/src/shiftings/templates/generic/form_base.html @@ -3,10 +3,10 @@ {% include "template/breadcrumbs.html" %} {% endblock %} {% block content %} -
-
+
+
{% block form %} {% endblock %}
-{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/shiftings/templates/language.html b/src/shiftings/templates/language.html index bbacdb1..6e61a17 100644 --- a/src/shiftings/templates/language.html +++ b/src/shiftings/templates/language.html @@ -16,7 +16,7 @@ {% trans "Language" %} - \ No newline at end of file +
diff --git a/src/shiftings/templates/switch_user.html b/src/shiftings/templates/switch_user.html index ccebc2e..c254b21 100644 --- a/src/shiftings/templates/switch_user.html +++ b/src/shiftings/templates/switch_user.html @@ -5,7 +5,7 @@ {% trans "Switch User" %} - -{% endif %} \ No newline at end of file +{% endif %} diff --git a/src/shiftings/templates/template/formset_remove_restore_buttons.html b/src/shiftings/templates/template/formset_remove_restore_buttons.html new file mode 100644 index 0000000..70c1325 --- /dev/null +++ b/src/shiftings/templates/template/formset_remove_restore_buttons.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/shiftings/templates/template/icon_action_button.html b/src/shiftings/templates/template/icon_action_button.html new file mode 100644 index 0000000..0ba2068 --- /dev/null +++ b/src/shiftings/templates/template/icon_action_button.html @@ -0,0 +1,3 @@ + + {% if label %} {{ label }}{% endif %} + \ No newline at end of file diff --git a/src/shiftings/templates/template/modal_base.html b/src/shiftings/templates/template/modal_base.html new file mode 100644 index 0000000..eb3f075 --- /dev/null +++ b/src/shiftings/templates/template/modal_base.html @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/src/shiftings/templates/template/remove_modal.html b/src/shiftings/templates/template/remove_modal.html index 064be70..73a503b 100644 --- a/src/shiftings/templates/template/remove_modal.html +++ b/src/shiftings/templates/template/remove_modal.html @@ -1,44 +1,42 @@ - - -