Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hansegucker/AlekSIS-Core
  • pinguin/AlekSIS-Core
  • AlekSIS/official/AlekSIS-Core
  • sunweaver/AlekSIS-Core
  • sggua/AlekSIS-Core
  • edward/AlekSIS-Core
  • magicfelix/AlekSIS-Core
7 results
Show changes
Commits on Source (107)
Showing
with 296 additions and 73 deletions
......@@ -34,6 +34,13 @@ As legacy pages are no longer themed, you should update them to the new frontend
To make setting names consistent, the setting `auth.login.registration.unique_email`
was renamed to `auth.registration.unique_email`.
To prevent collisions with fields, the class variable `name` on `RegistryObject` has been renamed
to `_class_name`. Please update any references and subclasses.
To prevent collisions with fields, the class variables `verbose_name`, `link`, `color`,
`description` and `permission_required` on `DAVResource` have been prefixed with `dav_`.
Please update any references and subclasses.
Added
~~~~~
......@@ -87,7 +94,13 @@ Changed
* Use new auth rate limiting settings
* Setting `auth.login.registration.unique_email` was renamed to `auth.registration.unique_email`
* Bump Python version to 3.10
* Factor out addresses in their own model and allow multiple addresses with different types
(e. g. home, business) for one person
* Adapt permission scheme for announcements to other permissions.
* Use Firefox instead of Chromium for PDF creation and support external webdriver via
`selenium.url` option, e.g. for use in containers.
* Rename `RegistryObject`'s class var `name` to `_class_name`.
* Prefix `DAVResource`'s class vars with `dav_`.
Fixed
~~~~~
......
......@@ -30,8 +30,6 @@ RUN apt-get -y update && \
eatmydata apt-get -y upgrade && \
eatmydata apt-get install -y --no-install-recommends \
build-essential \
chromium \
chromium-driver \
curl \
dumb-init \
gettext \
......
......@@ -8,6 +8,8 @@ from reversion.admin import VersionAdmin
from .mixins import BaseModelAdmin
from .models import (
Activity,
Address,
AddressType,
Announcement,
AnnouncementRecipient,
CustomMenuItem,
......@@ -42,3 +44,5 @@ admin.site.register(DataCheckResult)
admin.site.register(Person, GuardedVersionAdmin)
admin.site.register(Group, GuardedVersionAdmin)
admin.site.register(Organisation)
admin.site.register(Address)
admin.site.register(AddressType)
......@@ -176,7 +176,7 @@ class CoreConfig(AppConfig):
scopes |= {
"openid": _("OpenID Connect scope"),
"profile": _("Given name, family name, link to profile and picture if existing."),
"address": _("Full home postal address"),
"addresses": _("Postal addresses"),
"email": _("Email address"),
"phone": _("Home and mobile phone"),
"groups": _("Groups"),
......@@ -213,14 +213,15 @@ class CoreConfig(AppConfig):
else:
claims["email"] = request.user.email
if "address" in scopes and has_person(request.user):
claims["address"] = {
"street_address": request.user.person.street
+ " "
+ request.user.person.housenumber,
"locality": request.user.person.place,
"postal_code": request.user.person.postal_code,
}
if "addresses" in scopes and has_person(request.user):
claims["addresses"] = [
{
"street_address": address.street + " " + address.housenumber,
"locality": address.place,
"postal_code": address.postal_code,
}
for address in request.user.person.addresses.all()
]
if "phone" in scopes and has_person(request.user):
claims["mobile_number"] = request.user.person.mobile_number
......
......@@ -40,7 +40,7 @@ class SolveOption:
from django.utils.translation import gettext as _
class DeleteSolveOption(SolveOption):
name = "delete" # has to be unqiue
_class_name = "delete" # has to be unqiue
verbose_name = _("Delete") # should make use of i18n
@classmethod
......@@ -52,7 +52,7 @@ class SolveOption:
the corresponding data check result has to be deleted.
"""
name: str = "default"
_class_name: str = "default"
verbose_name: str = ""
@classmethod
......@@ -63,7 +63,7 @@ class SolveOption:
class IgnoreSolveOption(SolveOption):
"""Mark the object with data issues as solved."""
name = "ignore"
_class_name = "ignore"
verbose_name = _("Ignore problem")
@classmethod
......@@ -101,7 +101,7 @@ class DataCheck(RegistryObject):
required_for_migrations = True # Make mandatory for migrations
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption
IgnoreSolveOption._class_name: IgnoreSolveOption
}
@classmethod
......@@ -132,7 +132,7 @@ class DataCheck(RegistryObject):
The dictionary ``solve_options`` should include at least the IgnoreSolveOption,
but preferably also own solve options. The keys in this dictionary
have to be ``<YourOption>SolveOption.name``
have to be ``<YourOption>SolveOption._class_name``
and the values must be the corresponding solve option classes.
The class method ``check_data`` does the actual work. In this method
......@@ -170,7 +170,7 @@ class DataCheck(RegistryObject):
required_for_migrations: bool = False
migration_dependencies: list[str] = []
solve_options = {IgnoreSolveOption.name: IgnoreSolveOption}
solve_options = {IgnoreSolveOption._class_name: IgnoreSolveOption}
_current_results = []
......@@ -206,7 +206,7 @@ class DataCheck(RegistryObject):
def get_results(cls):
DataCheckResult = apps.get_model("core", "DataCheckResult")
return DataCheckResult.objects.filter(data_check=cls.name)
return DataCheckResult.objects.filter(data_check=cls._class_name)
@classmethod
def register_result(cls, instance) -> "DataCheckResult":
......@@ -219,7 +219,7 @@ class DataCheck(RegistryObject):
ct = ContentType.objects.get_for_model(instance)
result, __ = DataCheckResult.objects.get_or_create(
data_check=cls.name, content_type=ct, object_id=instance.id
data_check=cls._class_name, content_type=ct, object_id=instance.id
)
# Track all existing problems (for deleting old results)
......@@ -242,7 +242,7 @@ class DataCheck(RegistryObject):
@classproperty
def data_checks_choices(cls):
return [(check.name, check.verbose_name) for check in cls.registered_objects_list]
return [(check._class_name, check.verbose_name) for check in cls.registered_objects_list]
@recorded_task(run_every=timedelta(minutes=15))
......@@ -325,7 +325,7 @@ def send_emails_for_data_checks():
class DeactivateDashboardWidgetSolveOption(SolveOption):
name = "deactivate_dashboard_widget"
_class_name = "deactivate_dashboard_widget"
verbose_name = _("Deactivate DashboardWidget")
@classmethod
......@@ -337,12 +337,12 @@ class DeactivateDashboardWidgetSolveOption(SolveOption):
class BrokenDashboardWidgetDataCheck(DataCheck):
name = "broken_dashboard_widgets"
_class_name = "broken_dashboard_widgets"
verbose_name = _("Ensure that there are no broken DashboardWidgets.")
problem_name = _("The DashboardWidget was reported broken automatically.")
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption.name: DeactivateDashboardWidgetSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
DeactivateDashboardWidgetSolveOption._class_name: DeactivateDashboardWidgetSolveOption,
}
@classmethod
......@@ -366,7 +366,7 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
)
problem_name = _("The field {} couldn't be validated successfully.").format(field_name)
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
......@@ -385,11 +385,11 @@ def field_validation_data_check_factory(app_name: str, model_name: str, field_na
class DisallowedUIDDataCheck(DataCheck):
name = "disallowed_uid"
_class_name = "disallowed_uid"
verbose_name = _("Ensure that there are no disallowed usernames.")
problem_name = _("A user with a disallowed username was reported automatically.")
solve_options = {
IgnoreSolveOption.name: IgnoreSolveOption,
IgnoreSolveOption._class_name: IgnoreSolveOption,
}
@classmethod
......@@ -407,19 +407,19 @@ field_validation_data_check_factory("core", "CustomMenuItem", "icon")
class ChangeEmailAddressSolveOption(SolveOption):
name = "change_email_address"
_class_name = "change_email_address"
verbose_name = _("Change email address")
class EmailUniqueDataCheck(DataCheck):
name = "email_unique"
_class_name = "email_unique"
verbose_name = _("Ensure that email addresses are unique among all persons")
problem_name = _("There was a non-unique email address.")
required_for_migrations = True
migration_dependencies = [("core", "0057_drop_otp_yubikey")]
solve_options = {ChangeEmailAddressSolveOption.name: ChangeEmailAddressSolveOption}
solve_options = {ChangeEmailAddressSolveOption._class_name: ChangeEmailAddressSolveOption}
@classmethod
def check_data(cls):
......
......@@ -44,11 +44,9 @@ class PersonFilter(FilterSet):
)
contact = MultipleCharFilter(
[
"street__icontains",
"housenumber__icontains",
"postal_code__icontains",
"place__icontains",
"phone_number__icontains",
"addresses__street__icontains",
"postal_code__street__icontains",
"place__street__icontains" "phone_number__icontains",
"mobile_number__icontains",
"email__icontains",
],
......
......@@ -286,10 +286,7 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
"first_name",
"additional_name",
"last_name",
"street",
"housenumber",
"postal_code",
"place",
"addresses",
"date_of_birth",
"place_of_birth",
"sex",
......@@ -308,8 +305,7 @@ class AccountRegisterForm(SignupForm, ExtensibleForm):
),
Fieldset(
_("Address data"),
Row("street", "housenumber"),
Row("postal_code", "place"),
Row("addresses"),
),
Fieldset(_("Contact data"), Row("mobile_number", "phone_number")),
Fieldset(
......
......@@ -265,7 +265,7 @@ import Mascot from "../generic/mascot/Mascot.vue";
import gqlWhoAmI from "./whoAmI.graphql";
import gqlMessages from "./messages.graphql";
import gqlSystemProperties from "./systemProperties.graphql";
import { gqlSystemProperties } from "./systemProperties.graphql";
import gqlObjectPermissions from "./objectPermissions.graphql";
import useRegisterSWMixin from "../../mixins/useRegisterSW";
......
query systemProperties {
query gqlSystemProperties {
systemProperties {
availableLanguages {
code
......@@ -22,6 +22,16 @@ query systemProperties {
accountPersonPreferPhoto
footerImprintUrl
footerPrivacyUrl
inviteEnabled
}
}
}
query gqlUsernamePreferences {
systemProperties {
sitePreferences {
authAllowedUsernameRegex
authDisallowedUids
}
}
}
<template>
<v-autocomplete
v-bind="$attrs"
v-on="$listeners"
hide-no-data
:items="items"
:item-text="getItemText"
item-value="code"
:loading="loading"
clearable
/>
</template>
<script>
import queryMixin from "../../../mixins/queryMixin.js";
import { countries } from "./country.graphql";
export default {
name: "CountryField",
extends: "v-autocomplete",
mixins: [queryMixin],
props: {
/**
* The graphQL query used to retrieve the countries.
*/
gqlQuery: {
type: Object,
required: false,
default: () => countries,
},
},
methods: {
getItemText(country) {
return `${country.name} (${country.code})`;
},
},
};
</script>
query countries {
items: countries {
code
name
}
}
<template>
<div class="d-flex">
<v-btn
v-if="step > 1"
color="primary"
text
@click="$emit('set-step', step - 1)"
>
<v-icon left>mdi-chevron-left</v-icon>
{{ $t("actions.back") }}
</v-btn>
<v-spacer />
<v-btn
v-if="finalStep"
color="primary"
:disabled="nextDisabled || nextLoading"
:loading="nextLoading"
@click="$emit('confirm')"
>
{{ $t("actions.confirm") }}
<v-icon right>mdi-send-outline</v-icon>
</v-btn>
<v-btn
v-else
color="primary"
:disabled="nextDisabled || nextLoading"
:loading="nextLoading"
@click="$emit('set-step', step + 1)"
>
{{ $t("actions.next") }}
<v-icon right>mdi-chevron-right</v-icon>
</v-btn>
</div>
</template>
<script>
export default {
name: "ControlRow",
props: {
step: {
type: Number,
required: true,
},
nextDisabled: {
type: Boolean,
default: false,
},
finalStep: {
type: Boolean,
default: false,
},
nextLoading: {
type: Boolean,
default: false,
},
},
};
</script>
......@@ -4,6 +4,7 @@ import GroupField from "../generic/forms/GroupField.vue";
import DateField from "../generic/forms/DateField.vue";
import FileField from "../generic/forms/FileField.vue";
import SexSelect from "../generic/forms/SexSelect.vue";
import CountryField from "../generic/forms/CountryField.vue";
import FullscreenDialogObjectForm from "../generic/crud/FullscreenDialogObjectForm.vue";
import AdditionalImage from "./AdditionalImage.vue";
</script>
......@@ -92,6 +93,12 @@ import AdditionalImage from "./AdditionalImage.vue";
<v-text-field v-bind="attrs" v-on="on" />
</div>
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #country.field="{ attrs, on }">
<country-field v-bind="attrs" v-on="on" />
</template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #email.field="{ attrs, on }">
<div aria-required="false">
......@@ -201,7 +208,12 @@ export default {
{
text: this.$t("person.place"),
value: "place",
cols: 8,
cols: 4,
},
{
text: this.$t("person.country"),
value: "country",
cols: 4,
},
{
text: this.$t("person.form.titles.contact_data"),
......@@ -339,10 +351,11 @@ export default {
user: this.person.userid,
description: this.person.description,
sex: this.person.sex,
street: this.person.street,
housenumber: this.person.housenumber,
postalCode: this.person.postalCode,
place: this.person.place,
street: this.person.addresses[0]?.street,
housenumber: this.person.addresses[0]?.housenumber,
postalCode: this.person.addresses[0]?.postalCode,
place: this.person.addresses[0]?.place,
country: this.person.addresses[0]?.country,
phoneNumber: this.person.phoneNumber,
mobileNumber: this.person.mobileNumber,
email: this.person.email,
......
......@@ -17,6 +17,7 @@ export default {
return this.$route.query._ui_action === "create"
? {
fallbackUrl: { name: "core.persons" },
isCreate: true,
}
: {};
},
......
<!-- eslint-disable @intlify/vue-i18n/no-raw-text -->
<template>
<object-overview :query="query" title-attr="fullName" :id="id" ref="overview">
<template #loading>
......@@ -89,23 +90,46 @@
</v-list-item>
<v-divider inset />
<v-list-item>
<v-list-item-icon>
<v-icon> mdi-map-marker-outline</v-icon>
<v-list-item
v-for="(address, index) in person.addresses"
:key="address.id"
>
<v-list-item-icon v-if="index === 0">
<v-icon>mdi-map-marker-outline</v-icon>
</v-list-item-icon>
<v-list-item-action v-else />
<v-list-item-content>
<v-list-item-title>
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
{{ person.street || "–" }} {{ person.housenumber }},
{{ person.postalCode }}
{{ person.place }}
{{ address.street || "–" }} {{ address.housenumber }}
<span
v-if="
address.postalCode || address.place || address.country
"
>
,
</span>
<br v-if="address.postalCode || address.place" />
{{ address.postalCode }} {{ address.place }}
<span
v-if="
(address.postalCode || address.place) &&
address.country
"
>
,
</span>
<br v-if="address.country" />
{{ address.country }}
</v-list-item-title>
<v-list-item-subtitle>
{{ $t("person.address") }}
<v-list-item-subtitle
v-for="addresstype in address.addressTypes"
:key="addresstype.id"
>
{{ addresstype.name }}
</v-list-item-subtitle>
</v-list-item-content>
</v-list-item>
<v-divider inset />
<v-list-item
......
......@@ -8,11 +8,20 @@ mutation createPersons($input: [BatchCreatePersonInput]!) {
shortName
fullName
addresses {
id
addressTypes {
id
name
}
street
housenumber
postalCode
place
country
}
sex
street
housenumber
postalCode
place
phoneNumber
mobileNumber
email
......@@ -46,11 +55,20 @@ mutation updatePersons($input: [BatchPatchPersonInput]!) {
shortName
fullName
addresses {
id
addressTypes {
id
name
}
street
housenumber
postalCode
place
country
}
sex
street
housenumber
postalCode
place
phoneNumber
mobileNumber
email
......
......@@ -11,10 +11,18 @@ query person($id: ID) {
description
sex
street
housenumber
postalCode
place
addresses {
id
addressTypes {
id
name
}
street
housenumber
postalCode
place
country
}
phoneNumber
mobileNumber
email
......
......@@ -96,6 +96,7 @@
"select_all": "Alle auswählen",
"stop_editing": "Bearbeiten beenden",
"title": "Aktionen",
"type_to_search": "Tippen Sie, um zu suchen",
"update": "Aktualisieren"
},
"administration": {
......@@ -239,6 +240,7 @@
"date_too_late": "Bitte geben Sie ein früheres Datum ein.",
"invalid_color": "Dies ist keine gültige Farbe.",
"invalid_date": "Dies ist kein gültiges Datum.",
"invalid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"invalid_time": "Dies ist keine gültige Zeit.",
"not_a_number": "Keine gültige Nummer",
"not_a_whole_number": "Bitte geben Sie eine ganze Zahl ein",
......@@ -247,6 +249,9 @@
"required": "Dieses Feld ist verpflichtend.",
"string_too_long": "Bitte geben Sie weniger als {maxLength} Zeichen ein."
},
"file": {
"hint": "Existierende Datei: {fileName}"
},
"labels": {
"comment": "Kommentar",
"end": "Ende",
......@@ -402,6 +407,7 @@
"person": {
"account_menu_title": "Konto",
"additional_image": "Weiteres Bild",
"additional_name": "Zusätzlicher Name",
"address": "Adresse",
"avatar": "Avatar",
"birth_date": "Geburtsdatum",
......@@ -410,12 +416,26 @@
"birth_place": "Geburtsort",
"children": "Kinder",
"confirm_delete": "Wollen Sie wirklich diese Person löschen?",
"date_of_birth": "Geburtsdatum",
"delete": "Löschen",
"description": "Beschreibung",
"details": "Kontaktdaten",
"email": "E-Mail",
"email_address": "E-Mail-Adresse",
"first_name": "Vorname",
"form": {
"create": "Person erstellen",
"edit": "Person bearbeiten",
"titles": {
"address": "Adresse",
"advanced_personal_data": "Zusätzliche persönliche Daten",
"base_data": "Basisdaten",
"contact_data": "Kontaktdaten"
}
},
"guardians": "Erziehungsberechtigte / Eltern",
"home": "Festnetz",
"housenumber": "Hausnummer",
"impersonation": {
"impersonate": "Verkleiden",
"impersonating": "Verkleidet als",
......@@ -426,10 +446,16 @@
"logged_in_as": "Angemeldet als",
"menu_title": "Personen",
"mobile": "Handy",
"mobile_number": "Handynummer",
"name": "Name",
"no_additional_image": "Diese Person hat kein weiteres Bild hochgeladen",
"no_persons": "Keine Personen",
"page_title": "Person",
"phone_number": "Telefonnummer",
"photo": "Foto",
"place": "Ort",
"place_of_birth": "Geburtsort",
"postal_code": "Postleitzahl",
"primary_group": "Primärgruppe",
"sex": {
"f": "Weiblich",
......@@ -438,9 +464,11 @@
"x": "Divers"
},
"sex_description": "Geschlecht",
"short_name": "Kürzel",
"short_name": "Kurzname",
"street": "Straße",
"title": "Person",
"title_plural": "Personen",
"user": "Verknüpfter Benutzer",
"username": "Benutzername",
"view_in_new_tab": "{fullName} in neuem Tab anzeigen"
},
......
......@@ -342,7 +342,7 @@
"person": {
"account_menu_title": "Account",
"additional_image": "Additional Image",
"address": "Address",
"addresses": "Addresses",
"avatar": "Avatar",
"photo": "Photo",
"birth_date": "Date of birth",
......@@ -400,6 +400,7 @@
"housenumber": "Housenumber",
"postal_code": "Postal code",
"place": "Place",
"country": "Country",
"phone_number": "Phone number",
"mobile_number": "Mobile number",
"email": "Email",
......@@ -482,7 +483,9 @@
"date_too_early": "Please enter a later date.",
"date_too_late": "Please enter an earlier date.",
"string_too_long": "Please enter less than {maxLength} characters.",
"invalid_email": "Please enter a valid email address"
"invalid_email": "Please enter a valid email address",
"username_not_allowed": "This is not an allowed username",
"username_not_ascii": "This username contains the following non-allowed characters: {characters}."
},
"recurrence": {
"frequencies": {
......
......@@ -54,16 +54,22 @@
},
"person": {
"birth_date": "Dies natalis",
"date_of_birth": "Dies natalis",
"description": "Descriptio",
"guardians": "Parentes",
"home": "Numerus telephoni domi",
"menu_title": "personae",
"mobile": "Numerus telephoni mobilis",
"name": "Nomen",
"page_title": "Persona",
"photo": "Photographia",
"place": "Urbs",
"postal_code": "Numerus directorius",
"sex": {
"field": "Genus"
},
"sex_description": "Genus",
"street": "Via",
"title": "Persona",
"title_plural": "personae"
},
......