From 409691584cbc0702a7c91be13495d18e7abf552f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Tue, 11 Mar 2025 00:11:47 +0100 Subject: [PATCH 01/82] Rebuild account registration form in vue --- .../account/AccountRegistrationForm.vue | 854 ++++++++++++++++++ .../AccountRegistrationHelpTextCard.vue | 0 .../accountRegistrationMutation.graphql | 7 + aleksis/core/frontend/messages/en.json | 124 ++- aleksis/core/frontend/routes.js | 5 +- aleksis/core/schema/__init__.py | 3 + aleksis/core/schema/person.py | 83 +- 7 files changed, 1069 insertions(+), 7 deletions(-) create mode 100644 aleksis/core/frontend/components/account/AccountRegistrationForm.vue create mode 100644 aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue create mode 100644 aleksis/core/frontend/components/account/accountRegistrationMutation.graphql diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue new file mode 100644 index 000000000..94bd45f28 --- /dev/null +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -0,0 +1,854 @@ +<script setup> +import ControlRow from "aleksis.core/components/generic/multi_step/ControlRow.vue"; +import DateField from "aleksis.core/components/generic/forms/DateField.vue"; +import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; +import SexSelect from "aleksis.core/components/generic/forms/SexSelect.vue"; + +import PrimaryActionButton from "aleksis.core/components/generic/buttons/PrimaryActionButton.vue"; + +import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.vue"; +</script> + +<template> + <div> + <div v-if="accountRegistrationSent"> + <v-card> + <v-card-title> + <v-icon class="mr-2" color="success">mdi-check-circle-outline</v-icon> + {{ $t("paweljong.event_registration.form.submitted.thank_you") }} + </v-card-title> + <v-card-text class="text-body-1 black--text"> + {{ + $t( + "paweljong.event_registration.form.submitted.submitted_successfully", + ) + }} + </v-card-text> + <v-card-actions> + <primary-action-button + :to="{ name: 'core.account.login' }" + i18n-key="paweljong.event_registration.form.submitted.payment_button" + /> + </v-card-actions> + </v-card> + </div> + <div v-else> + <v-alert type="info" dense outlined class="mb-4"> + {{ + $t( + "accounts.signup.existing_account_alert", + ) + }} + </v-alert> + <v-stepper v-model="step" class="mb-4"> + <v-stepper-header> + <template v-for="(stepChoice, index) in steps"> + <v-stepper-step + :complete="step > index + 1" + :step="index + 1" + :key="stepChoice.name" + :ref="`step-${index}`" + > + {{ $t(stepChoice.titleKey) }} + </v-stepper-step> + <v-divider v-if="index + 1 < steps.length"></v-divider> + </template> + </v-stepper-header> + <v-stepper-items> + <v-stepper-content + v-if="isStepEnabled('email')" + :step="getStepIndex('email')" + > + <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("email")) }}</h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-tabs + v-model="emailMode" + :grow="!$vuetify.breakpoint.mdAndDown" + optional + show-arrows + > + <v-tab style="max-width: 50vw" class="primary--text"> + {{ + $t( + "paweljong.event_registration.form.steps.email.choose_mode.continue_aleksis", + ) + }} + </v-tab> + <v-tab style="max-width: 50vw" class="primary--text"> + {{ + $t( + "paweljong.event_registration.form.steps.email.choose_mode.continue_own", + ) + }} + </v-tab> + </v-tabs> + <v-form v-model="validationStatuses['email']"> + <v-tabs-items v-model="emailMode"> + <v-tab-item> + <v-row class="mt-4"> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.email.localPart" + :label=" + $t( + 'postbuero.mail_addresses.data_table.local_part', + ) + " + :rules=" + emailMode === 0 + ? $rules().required.build(rules.emailLocalPart) + : [] + " + required + ></v-text-field> + </div> + </v-col> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-autocomplete + outlined + hide-no-data + :items="mailDomains" + item-text="domain" + item-value="id" + :loading="$apollo.queries.mailDomains.loading" + prepend-icon="mdi-at" + v-model="data.email.domain" + :label=" + $t('postbuero.mail_addresses.data_table.domain') + " + required + :rules=" + emailMode === 0 ? $rules().required.build() : [] + " + /> + </div> + </v-col> + </v-row> + </v-tab-item> + <v-tab-item> + <v-row class="mt-4"> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.user.email" + :label=" + $t( + 'paweljong.event_registration.form.steps.email.fields.email.label', + ) + " + required + :rules=" + emailMode === 1 + ? $rules().required.build(rules.email) + : [] + " + prepend-icon="mdi-email-outline" + ></v-text-field> + </div> + </v-col> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.user.confirmEmail" + :label=" + $t( + 'paweljong.event_registration.form.steps.email.fields.confirm_email.label', + ) + " + required + :rules=" + emailMode === 1 + ? $rules().required.build(rules.confirmEmail) + : [] + " + prepend-icon="mdi-email-outline" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-tab-item> + </v-tabs-items> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled=" + emailMode === undefined || + emailMode === null || + !validationStatuses['email'] + " + /> + </v-stepper-content> + + <v-stepper-content + :step="getStepIndex('account')" + > + <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("account")) }}</h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-form v-model="validationStatuses['account']"> + <v-row> + <v-col cols="12"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.user.username" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.username.label', + ) + " + required + :rules="$rules().required.build()" + prepend-icon="mdi-account-outline" + ></v-text-field> + </div> + </v-col> + </v-row> + <v-row> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.user.password" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.password.label', + ) + " + required + :rules="$rules().required.build()" + type="password" + prepend-icon="mdi-form-textbox-password" + ></v-text-field> + </div> + </v-col> + <v-col cols="12" md="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.user.confirmPassword" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.confirm_password.label', + ) + " + required + :rules="$rules().required.build(rules.confirmPassword)" + type="password" + prepend-icon="mdi-form-textbox-password" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled="!validationStatuses['account']" + /> + </v-stepper-content> + + <v-stepper-content + :step="getStepIndex('base_data')" + > + <h2 class="text-h6 mb-4"> + {{ $t(getStepTitleKey("base_data")) }} + </h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-form v-model="validationStatuses['base_data']"> + <v-row> + <v-col> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.firstName" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.first_name.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + <v-col> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.additionalName" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.last_name.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + <v-col> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.lastName" + :label=" + $t( + 'paweljong.event_registration.form.steps.register.fields.last_name.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled="!validationStatuses['base_data']" + /> + </v-stepper-content> + + <v-stepper-content + :step="getStepIndex('address_data')" + > + <h2 class="text-h6 mb-4"> + {{ $t(getStepTitleKey("address_data")) }} + </h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-form v-model="validationStatuses['address_data']"> + <v-row> + <v-col v-if="isFieldVisible('street')" cols="12" lg="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.address.street" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.street.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + <v-col v-if="isFieldVisible('housenumber')" cols="12" lg="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.address.housenumber" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.housenumber.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + </v-row> + <v-row> + <v-col v-if="isFieldVisible('postal_code')" cols="12" lg="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.address.postalCode" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.postal_code.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + <v-col v-if="isFieldVisible('place')" cols="12" lg="6"> + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.address.place" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.place.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled="!validationStatuses['address_data']" + /> + </v-stepper-content> + + <v-stepper-content + :step="getStepIndex('contact_data')" + > + <h2 class="text-h6 mb-4"> + {{ $t(getStepTitleKey("contact_data")) }} + </h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-form v-model="validationStatuses['contact_data']"> + <v-row> + <v-col + v-if="isFieldVisible('mobile_number')" + cols="12" + md="6" + > + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.mobileNumber" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.mobile_number.label', + ) + " + required + prepend-icon="mdi-phone-outline" + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + <v-col + v-if="isFieldVisible('phone_number')" + cols="12" + md="6" + > + <div aria-required="true"> + <v-text-field + outlined + v-model="data.person.phoneNumber" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.mobile_number.label', + ) + " + required + prepend-icon="mdi-phone-outline" + :rules="$rules().required.build()" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled="!validationStatuses['contact_data']" + /> + </v-stepper-content> + + <v-stepper-content + :step="getStepIndex('additional_data')" + > + <h2 class="text-h6 mb-4"> + {{ $t(getStepTitleKey("additional_data")) }} + </h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-form v-model="validationStatuses['additional_data']"> + <v-row> + <v-col + v-if="isFieldVisible('date_of_birth')" + cols="12" + md="6" + > + <div aria-required="true"> + <date-field + outlined + v-model="data.person.dateOfBirth" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.date_of_birth.label', + ) + " + required + :rules="$rules().required.build()" + prepend-icon="mdi-cake-variant-outline" + /> + </div> + </v-col> + <v-col + v-if="isFieldVisible('place_of_birth')" + cols="12" + md="6" + > + <div aria-required="true"> + <date-field + outlined + v-model="data.person.dateOfBirth" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.date_of_birth.label', + ) + " + required + :rules="$rules().required.build()" + prepend-icon="mdi-cake-variant-outline" + /> + </div> + </v-col> + </v-row> + <v-row> + <v-col v-if="isFieldVisible('sex')" cols="12" md="6"> + <div aria-required="true"> + <!-- FIXME: Prefilling data does not work due to upper-/lowercase situation; will be fixed with core person form refactoring --> + <sex-select + outlined + v-model="data.person.sex" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.sex.label', + ) + " + :hint=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.sex.help_text', + ) + " + persistent-hint + required + :rules="$rules().required.build()" + /> + </div> + </v-col> + <v-col v-if="isFieldVisible('photo')" cols="12" md="6"> + <div aria-required="false"> + <file-field + outlined + v-model="data.person.photo" + accept="image/jpeg, image/png" + /> + </div> + </v-col> + </v-row> + <v-row> + <v-col v-if="isFieldVisible('description')" cols="12"> + <v-text-field + outlined + v-model="data.person.description" + :label=" + $t( + 'paweljong.event_registration.form.steps.contact_details.fields.street.label', + ) + " + required + :rules="$rules().required.build()" + ></v-text-field> + </v-col> + </v-row> + </v-form> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="setStep" + :next-disabled="!validationStatuses['additional_data']" + /> + </v-stepper-content> + + <v-stepper-content :step="getStepIndex('confirm')"> + <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> + <div class="mb-4"> + <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> + <v-text-field + outlined + v-model="data.comment" + :label=" + $t( + 'paweljong.event_registration.form.steps.confirm.fields.comment.label', + ) + " + ></v-text-field> + </div> + <v-divider class="my-4" /> + + <!-- TODO: Add summary --> + + <message-box type="info" class="mb-4"> + {{ $t("paweljong.event_registration.form.steps.confirm.hint") }} + </message-box> + <ApolloMutation + :mutation="require('./accountRegistrationMutation.graphql')" + :variables="{ + accountRegistration: dataForSubmit, + }" + @done="accountRegistrationDone" + > + <template #default="{ mutate, loading, error }"> + <control-row + :step="step" + final-step + @set-step="setStep" + @confirm="mutate" + :next-loading="loading" + /> + <v-alert v-if="error" type="error" outlined>{{ + error.message + }}</v-alert> + </template> + </ApolloMutation> + </v-stepper-content> + </v-stepper-items> + </v-stepper> + </div> + </div> +</template> + +<script> +// import { +// mailDomainsForUser, +// disallowedLocalParts, +// } from "aleksis.apps.postbuero/components/mail_addresses/mailAddresses.graphql"; + +import formRulesMixin from "../../mixins/formRulesMixin"; + +export default { + name: "AccountRegistrationForm", + apollo: { + // mailDomains: mailDomainsForUser, + // disallowedLocalParts: disallowedLocalParts, + }, + mixins: [formRulesMixin], + methods: { + setStep(step) { + this.step = step; + this.valid = false; + }, + accountRegistrationDone({ data }) { + if (data.sendAccountRegistration.ok) { + this.accountRegistrationSent = true; + } + }, + isFieldVisible(fieldName) { + return this.event?.contactInformationVisibleFields.includes(fieldName); + }, + isStepEnabled(stepName) { + return this.steps.some((s) => s.name === stepName); + }, + getStepIndex(stepName) { + return this.steps.findIndex((s) => s.name === stepName) + 1; + }, + getStepTitleKey(stepName) { + return this.steps.find((s) => s.name === stepName)?.titleKey; + }, + setValidationStatus(stepName, validationStatus) { + this.validationStatuses[stepName] = validationStatus; + }, + getValidationStatus(stepName) { + if (this.validationStatuses[stepName]) { + return this.validationStatuses[stepName]; + } + return false; + }, + }, + computed: { + rules() { + return { + name: [ + (v) => !!v || this.$t("order.rules.name.required"), + (v) => v.length <= 255 || this.$t("order.rules.name.max"), + ], + email: [ + (v) => + /.+@.+\..+/.test(v) || + this.$t("paweljong.event_registration.form.rules.email.valid"), + ], + confirmEmail: [ + (v) => + this.data.user.email == v || + this.$t( + "paweljong.event_registration.form.rules.confirm_email.no_match", + ), + ], + emailLocalPart: [ + (v) => + /^\w+([.!#$%&'*+-\/=?^_`{|}~]?\w+)*$/.test(v) || + this.$t( + "postbuero.mail_addresses.data_table.errors.local_part_invalid_characters", + ), + (v) => + this.disallowedLocalParts.indexOf(v) === -1 || + this.$t( + "postbuero.mail_addresses.data_table.errors.local_part_disallowed", + ), + ], + confirmPassword: [ + (v) => + this.data.user.password == v || + this.$t( + "paweljong.event_registration.form.rules.confirm_password.no_match", + ), + ], + amount: [ + (v) => + v >= this.event.minCost || + this.$t("paweljong.event_registration.form.rules.amount.too_low"), + (v) => + this.event.maxCost === null || + v <= this.event.maxCost || + this.$t("paweljong.event_registration.form.rules.amount.too_high"), + ], + }; + }, + dataForSubmit() { + const { confirmEmail, confirmPassword, ...filteredUserData } = + this.data.user; + + if (!this.data.email.localPart && !this.data.email.domain) { + const { email, ...filteredData } = data; + data = filteredData; + } + return data; + }, + steps() { + return [ + { + name: "email", + titleKey: "accounts.signup.form.steps.email.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationGuardiansParticipantHelpText, + }, + { + name: "accounts", + titleKey: "accounts.signup.form.steps.accounts.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + { + name: "base_data", + titleKey: "accounts.signup.form.steps.base_data.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + { + name: "address_data", + titleKey: "accounts.signup.form.steps.address_data.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + { + name: "contact_data", + titleKey: "accounts.signup.form.steps.contact_data.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + { + name: "additional_data", + titleKey: "accounts.signup.form.steps.additional_data.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + { + name: "confirm", + titleKey: "accounts.signup.form.steps.confirm.title", + participantHelpText: + this.paweljongSitePreferences + ?.eventRegistrationAdditionalParticipantHelpText, + }, + ]; + }, + currentParticipantHelpText() { + return this.steps[this.step - 1].participantHelpText; + }, + currentGuardianHelpText() { + return this.steps[this.step - 1].guardianHelpText; + }, + }, + data() { + return { + validationStatuses: {}, + accountRegistrationSent: false, + step: 1, + emailMode: null, + data: { + email: { + localPart: "", + domain: "", + }, + person: { + firstName: "", + additionalName: "", + lastName: "", + shortName: "", + dateOfBirth: "", + placeOfBirth: "", + sex: "", + address: { + street: "", + housenumber: "", + postalCode: "", + place: "", + }, + mobileNumber: "", + phoneNumber: "", + description: "", + photo: {}, + }, + user: { + username: "", + email: "", + confirmEmail: "", + password: "", + confirmPassword: "", + }, + }, + }; + }, + watch: { + step() { + const comp = this.$refs[`step-${this.step - 1}`][0]; + comp.$el.scrollIntoView(); + }, + }, +}; +</script> + +<style> +.btn-multiline > span { + width: 100%; +} +.v-stepper__header { + overflow: auto; + display: flex; + flex-wrap: nowrap; + justify-content: left; +} +</style> diff --git a/aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue b/aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue new file mode 100644 index 000000000..e69de29bb diff --git a/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql b/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql new file mode 100644 index 000000000..918502edf --- /dev/null +++ b/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql @@ -0,0 +1,7 @@ +mutation sendAccountRegistration( + $accountRegistration: AccountRegistrationInputType! +) { + sendAccountRegistration(event: $event, accountRegistration: $accountRegistration) { + ok + } +} diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index aec2bae34..0a24de3cd 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -39,8 +39,128 @@ "menu_title": "Logout" }, "signup": { - "menu_title": "Sign Up" - }, + "menu_title": "Sign Up", + "form": { + "submitted": { + "thank_you": "Thanks!", + "submitted_successfully": "Your account has been successfully registered. You can sign in now." + }, + "existing_account_alert": "Already have an account? Then please sign in.", + "steps": { + "email": { + "title": "E-Mail address", + "choose_mode": { + "continue_aleksis": "New email address", + "continue_own": "Existing email address", + "continue_existing_account": "I already have an account", + "back": "Go back" + }, + "fields": { + "email": { + "label": "E-Mail address" + }, + "confirm_email": { + "label": "Confirm e-mail address" + } + } + }, + "account": { + "title": "Account", + "fields": { + "first_name": { + "label": "First name" + }, + "last_name": { + "label": "Last name" + }, + "username": { + "label": "Username" + }, + "password": { + "label": "Password" + }, + "confirm_password": { + "label": "Confirm password" + } + } + }, + "base_data": { + "title": "Base data", + "fields": { + "first_name": { + "label": "First name" + }, + "additional_name": { + "label": "Additional name(s)" + }, + "last_name": { + "label": "Last name" + }, + "short_name": { + "label": "Short name" + } + } + } + }, + "address_data": { + "title": "Address data", + "fields": { + "street": { + "label": "Street" + }, + "housenumber": { + "label": "Housenumber" + }, + "postal_code": { + "label": "Postal code" + }, + "place": { + "label": "Place" + } + } + }, + "contact_data": { + "title": "Contact data", + "fields": { + "mobile_number": { + "label": "Mobile number" + }, + "phone_number": { + "label": "Home phone number" + } + } + }, + "additional_data": { + "title": "Additional data", + "fields": { + "date_of_birth": { + "label": "Date of birth" + }, + "place_of_birth": { + "label": "Date of birth" + }, + "sex": { + "label": "Sex" + } + } + } + }, + "rules": { + "email": { + "valid": "This is not a valid e-mail address" + }, + "confirm_email": { + "no_match": "The e-mail addresses do not match" + }, + "confirm_password": { + "no_match": "The passwords do not match" + } + }, + "help_text": { + "guardian": "For guardians", + "participant": "For participants" + } + }, "social_connections": { "menu_title": "Third-party Accounts" }, diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index b211c4a09..023a4e863 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -32,10 +32,7 @@ const routes = [ { path: "/accounts/signup/", name: "core.accounts.signup", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, + component: () => import("./components/account/AccountRegistrationForm.vue"), meta: { inMenu: true, icon: "mdi-account-plus-outline", diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 5043165d9..5b6fdf711 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -87,6 +87,7 @@ from .person import ( PersonBatchDeleteMutation, PersonBatchPatchMutation, PersonType, + SendAccountRegistrationMutation, ) from .personal_event import ( PersonalEventBatchCreateMutation, @@ -522,6 +523,8 @@ class Mutation(graphene.ObjectType): delete_oauth_applications = OAuthApplicationBatchDeleteMutation.Field() patch_oauth_applications = OAuthApplicationBatchPatchMutation.Field() + send_account_registration = SendAccountRegistrationMutation.Field() + def build_global_schema(): """Build global GraphQL schema from all apps.""" diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index c0d8f938d..fb77b8fe5 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,6 +1,7 @@ from typing import Union -from django.core.exceptions import PermissionDenied +from django.core.exceptions import PermissionDenied, SuspiciousOperation +from django.db import transaction from django.db.models import Q from django.utils import timezone @@ -10,11 +11,13 @@ from graphene_django import DjangoObjectType from ..filters import PersonFilter from ..models import ( + Activity, Address, AddressType, DummyPerson, Person, PersonGroupThrough, + PersonInvitation, PersonRelationship, Role, ) @@ -565,3 +568,81 @@ class PersonBatchPatchMutation( pass cls._handle_address(root, info, input, obj, full_input) cls._handle_guardians(root, info, input, obj, full_input) + + +class AccountRegistrationInputType(graphene.InputObjectType): + from .user import UserInputType # noqa + + # email = graphene.Field(MailAddressInputType, required=False) + person = graphene.Field(PersonInputType, required=True) + user = graphene.Field(UserInputType, required=True) + + +class SendAccountRegistrationMutation(graphene.Mutation): + class Arguments: + account_registration = AccountRegistrationInputType(required=True) + + ok = graphene.Boolean() + + @transaction.atomic + def mutate(self, info, account_registration: AccountRegistrationInputType, **kwargs): + # Create user + try: + user = User.objects.create( + username=account_registration["user"]["username"], + email=account_registration["user"]["email"], + ) + except IntegrityError: + raise ValidationError(_("A user with this username or e-mail already exists.")) + + user.set_password(account_registration["user"]["password"]) + user.save() + + try: + person, created = Person.objects.get_or_create( + user=user, + defaults={ + "email": account_registration["user"]["email"], + "first_name": account_registration["person"]["first_name"], + "last_name": account_registration["person"]["last_name"], + }, + ) + except IntegrityError: + raise ValidationError(_("A person using the e-mail address %s already exists.") % account_registration["user"]["email"]) + + # Store contact information in database + for field in Person._meta.get_fields(): + if ( + account_registration["person"] is not None + and account_registration["person"][field.name] is not None + and account_registration["person"][field.name] != "" + ): + if field.name == "address": + for addr_field in ["street", "housenumber", "postal_code", "place"]: + setattr( + person, addr_field, account_registration["person"]["address"][addr_field] + ) + else: + setattr(person, field_name, account_registration["person"][field.name]) + person.save() + + # Accept invitation, if exists + invitation_code = info.context.session.get("invitation_code") + if invitation_code: + from invitations.views import accept_invitation # noqa + + try: + invitation = PersonInvitation.objects.get(key=invitation_code) + except PersonInvitation.DoesNotExist as exc: + raise SuspiciousOperation from exc + + accept_invitation(invitation, request, user) + + _act = Activity( + title=_("You registered an account"), + description=_("You registered an account with the username %s" % user.username), + app="Core", + user=person, + ) + + return SendAccountRegistrationMutation(ok=True) -- GitLab From 16e741a83ebbe387201550f25b33189cfb63334e Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 01:02:19 +0100 Subject: [PATCH 02/82] Use site preference to check if signup is enabled --- .../components/app/systemProperties.graphql | 1 + aleksis/core/frontend/routeValidators.js | 13 ++++++++++++- aleksis/core/frontend/routes.js | 4 ++-- aleksis/core/schema/site_preferences.py | 4 ++++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/aleksis/core/frontend/components/app/systemProperties.graphql b/aleksis/core/frontend/components/app/systemProperties.graphql index 7fdd18002..c71e1f865 100644 --- a/aleksis/core/frontend/components/app/systemProperties.graphql +++ b/aleksis/core/frontend/components/app/systemProperties.graphql @@ -23,6 +23,7 @@ query gqlSystemProperties { footerImprintUrl footerPrivacyUrl inviteEnabled + signupEnabled } } } diff --git a/aleksis/core/frontend/routeValidators.js b/aleksis/core/frontend/routeValidators.js index 24caa2870..f4fe977d6 100644 --- a/aleksis/core/frontend/routeValidators.js +++ b/aleksis/core/frontend/routeValidators.js @@ -22,4 +22,15 @@ const inviteEnabledValidator = (_, systemProperties) => { return systemProperties && systemProperties.sitePreferences.inviteEnabled; }; -export { notLoggedInValidator, hasPersonValidator, inviteEnabledValidator }; +/** + * Check whether signup is enabled. + * + * @param {Object} systemProperties object as returned by the systemProperties query + * @returns true if invites are enabled and false otherwise + */ +const signupEnabledValidator = (_, systemProperties) => { + return systemProperties && systemProperties.sitePreferences.signupEnabled; +}; + + +export { notLoggedInValidator, hasPersonValidator, inviteEnabledValidator, signupEnabledValidator }; diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index 023a4e863..69f347662 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -11,6 +11,7 @@ import { appObjects } from "aleksisAppImporter"; import { notLoggedInValidator, inviteEnabledValidator, + signupEnabledValidator, } from "./routeValidators"; const routes = [ @@ -38,8 +39,7 @@ const routes = [ icon: "mdi-account-plus-outline", iconActive: "mdi-account-plus", titleKey: "accounts.signup.menu_title", - menuPermission: "core.signup_rule", - validators: [notLoggedInValidator], + validators: [signupEnabledValidator, notLoggedInValidator], invalidate: "leave", }, }, diff --git a/aleksis/core/schema/site_preferences.py b/aleksis/core/schema/site_preferences.py index a4c8af0b1..730e12cae 100644 --- a/aleksis/core/schema/site_preferences.py +++ b/aleksis/core/schema/site_preferences.py @@ -24,6 +24,7 @@ class SitePreferencesType(graphene.ObjectType): editable_fields_person = graphene.List(graphene.String) invite_enabled = graphene.Boolean() + signup_enabled = graphene.Boolean() auth_allowed_username_regex = graphene.String() auth_disallowed_uids = graphene.List(graphene.String) @@ -72,3 +73,6 @@ class SitePreferencesType(graphene.ObjectType): def resolve_auth_disallowed_uids(parent, info, **kwargs): return parent["auth__disallowed_uids"].split(",") + + def resolve_signup_enabled(parent, info, **kwargs): + return parent["auth__signup_enabled"] -- GitLab From 91412c09add445ca613218c8221c76b5f6ef9ca6 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 01:03:33 +0100 Subject: [PATCH 03/82] Refactor imports --- .../components/account/AccountRegistrationForm.vue | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 94bd45f28..c33d5b988 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -1,10 +1,9 @@ <script setup> -import ControlRow from "aleksis.core/components/generic/multi_step/ControlRow.vue"; -import DateField from "aleksis.core/components/generic/forms/DateField.vue"; -import PositiveSmallIntegerField from "aleksis.core/components/generic/forms/PositiveSmallIntegerField.vue"; -import SexSelect from "aleksis.core/components/generic/forms/SexSelect.vue"; +import ControlRow from "../generic/multi_step/ControlRow.vue"; +import DateField from "../generic/forms/DateField.vue"; +import SexSelect from "../generic/forms/SexSelect.vue"; -import PrimaryActionButton from "aleksis.core/components/generic/buttons/PrimaryActionButton.vue"; +import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.vue"; </script> -- GitLab From da66c997ac7a08a8422c0effbaf1bebcc2ec905f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 18:26:36 +0100 Subject: [PATCH 04/82] Remove unused variable --- .../components/account/accountRegistrationMutation.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql b/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql index 918502edf..6a54aecd7 100644 --- a/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql +++ b/aleksis/core/frontend/components/account/accountRegistrationMutation.graphql @@ -1,7 +1,7 @@ mutation sendAccountRegistration( $accountRegistration: AccountRegistrationInputType! ) { - sendAccountRegistration(event: $event, accountRegistration: $accountRegistration) { + sendAccountRegistration(accountRegistration: $accountRegistration) { ok } } -- GitLab From 51b135bf1f2961f7f2e41666a6a5d31964f13a13 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 18:26:46 +0100 Subject: [PATCH 05/82] Fix translations --- aleksis/core/frontend/messages/en.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 0a24de3cd..2126146fd 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -100,7 +100,6 @@ "label": "Short name" } } - } }, "address_data": { "title": "Address data", @@ -143,6 +142,9 @@ "label": "Sex" } } + }, + "confirm": { + "title": "Confirm" } }, "rules": { @@ -160,7 +162,8 @@ "guardian": "For guardians", "participant": "For participants" } - }, + } + }, "social_connections": { "menu_title": "Third-party Accounts" }, -- GitLab From d933f25150fb352c2b6a6254d9c3fee869f563f4 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 18:27:17 +0100 Subject: [PATCH 06/82] Fix and add photo/avatar fields to person input type --- aleksis/core/schema/person.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index fb77b8fe5..9fed324d1 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -8,6 +8,7 @@ from django.utils import timezone import graphene import graphene_django_optimizer from graphene_django import DjangoObjectType +from graphene_file_upload.scalars import Upload from ..filters import PersonFilter from ..models import ( @@ -337,14 +338,17 @@ class PersonInputType(graphene.InputObjectType): email = graphene.String(required=False) - date_of_birth = graphene.String(required=False) - place_of_birth = graphene.Date(required=False) + date_of_birth = graphene.Date(required=False) + place_of_birth = graphene.String(required=False) sex = graphene.String(required=False) address = graphene.Field(AddressInputType, required=False) # TODO: Photo and avatar + photo = Upload() + avatar = Upload() + guardians = graphene.List(lambda: PersonInputType, required=False) # TODO: Primary Group -- GitLab From 630258cb237e1988cb70261b60cbd62dec35c14c Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 13 Mar 2025 23:48:11 +0100 Subject: [PATCH 07/82] Fix mutation --- aleksis/core/schema/person.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 9fed324d1..1a9d239f0 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,9 +1,11 @@ from typing import Union +from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied, SuspiciousOperation -from django.db import transaction +from django.db import IntegrityError, transaction from django.db.models import Q from django.utils import timezone +from django.utils.translation import gettext as _ import graphene import graphene_django_optimizer @@ -592,16 +594,15 @@ class SendAccountRegistrationMutation(graphene.Mutation): def mutate(self, info, account_registration: AccountRegistrationInputType, **kwargs): # Create user try: - user = User.objects.create( + user = get_user_model().objects.create_user( username=account_registration["user"]["username"], email=account_registration["user"]["email"], + password=account_registration["user"]["password"] ) except IntegrityError: raise ValidationError(_("A user with this username or e-mail already exists.")) - user.set_password(account_registration["user"]["password"]) - user.save() - + # Create person try: person, created = Person.objects.get_or_create( user=user, @@ -617,17 +618,21 @@ class SendAccountRegistrationMutation(graphene.Mutation): # Store contact information in database for field in Person._meta.get_fields(): if ( - account_registration["person"] is not None + field.name in ["street", "housenumber", "postal_code", "place"] + and "address" in account_registration["person"] + and field.name in account_registration["person"]["address"] + and account_registration["person"]["address"][field.name] is not None + and account_registration["person"]["address"][field.name] != "" + ): + setattr( + person, field.name, account_registration["person"]["address"][field.name] + ) + elif ( + field.name in account_registration["person"] and account_registration["person"][field.name] is not None and account_registration["person"][field.name] != "" ): - if field.name == "address": - for addr_field in ["street", "housenumber", "postal_code", "place"]: - setattr( - person, addr_field, account_registration["person"]["address"][addr_field] - ) - else: - setattr(person, field_name, account_registration["person"][field.name]) + setattr(person, field.name, account_registration["person"][field.name]) person.save() # Accept invitation, if exists @@ -640,7 +645,7 @@ class SendAccountRegistrationMutation(graphene.Mutation): except PersonInvitation.DoesNotExist as exc: raise SuspiciousOperation from exc - accept_invitation(invitation, request, user) + accept_invitation(invitation, info.context, info.context.user) _act = Activity( title=_("You registered an account"), -- GitLab From 960b9617452d22c8847180e49023609a1e8f2f2f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:08:24 +0100 Subject: [PATCH 08/82] Use correct translation strings --- .../account/AccountRegistrationForm.vue | 145 ++++++++---------- aleksis/core/frontend/messages/en.json | 17 +- 2 files changed, 73 insertions(+), 89 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index c33d5b988..7aeec70b0 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -4,8 +4,6 @@ import DateField from "../generic/forms/DateField.vue"; import SexSelect from "../generic/forms/SexSelect.vue"; import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; - -import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.vue"; </script> <template> @@ -14,19 +12,19 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v <v-card> <v-card-title> <v-icon class="mr-2" color="success">mdi-check-circle-outline</v-icon> - {{ $t("paweljong.event_registration.form.submitted.thank_you") }} + {{ $t("accounts.signup.form.form.submitted.thank_you") }} </v-card-title> <v-card-text class="text-body-1 black--text"> {{ $t( - "paweljong.event_registration.form.submitted.submitted_successfully", + "accounts.signup.form.submitted.submitted_successfully", ) }} </v-card-text> <v-card-actions> <primary-action-button :to="{ name: 'core.account.login' }" - i18n-key="paweljong.event_registration.form.submitted.payment_button" + i18n-key="accounts.signup.form.submitted.login_button" /> </v-card-actions> </v-card> @@ -35,7 +33,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v <v-alert type="info" dense outlined class="mb-4"> {{ $t( - "accounts.signup.existing_account_alert", + "accounts.signup.form.existing_account_alert", ) }} </v-alert> @@ -54,7 +52,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v </template> </v-stepper-header> <v-stepper-items> - <v-stepper-content + <!-- <v-stepper-content v-if="isStepEnabled('email')" :step="getStepIndex('email')" > @@ -70,14 +68,14 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v <v-tab style="max-width: 50vw" class="primary--text"> {{ $t( - "paweljong.event_registration.form.steps.email.choose_mode.continue_aleksis", + "accounts.signup.form.steps.email.choose_mode.continue_aleksis", ) }} </v-tab> <v-tab style="max-width: 50vw" class="primary--text"> {{ $t( - "paweljong.event_registration.form.steps.email.choose_mode.continue_own", + "accounts.signup.form.steps.email.choose_mode.continue_own", ) }} </v-tab> @@ -137,7 +135,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.user.email" :label=" $t( - 'paweljong.event_registration.form.steps.email.fields.email.label', + 'accounts.signup.form.steps.email.fields.email.label', ) " required @@ -157,7 +155,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.user.confirmEmail" :label=" $t( - 'paweljong.event_registration.form.steps.email.fields.confirm_email.label', + 'accounts.signup.form.steps.email.fields.confirm_email.label', ) " required @@ -185,14 +183,13 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v !validationStatuses['email'] " /> - </v-stepper-content> + </v-stepper-content> --> <v-stepper-content :step="getStepIndex('account')" > <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("account")) }}</h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> <v-form v-model="validationStatuses['account']"> <v-row> <v-col cols="12"> @@ -202,7 +199,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.user.username" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.username.label', + 'accounts.signup.form.steps.account.fields.username.label', ) " required @@ -220,7 +217,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.user.password" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.password.label', + 'accounts.signup.form.steps.account.fields.password.label', ) " required @@ -237,7 +234,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.user.confirmPassword" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.confirm_password.label', + 'accounts.signup.form.steps.account.fields.confirm_password.label', ) " required @@ -265,7 +262,6 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v {{ $t(getStepTitleKey("base_data")) }} </h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> <v-form v-model="validationStatuses['base_data']"> <v-row> <v-col> @@ -275,7 +271,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.firstName" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.first_name.label', + 'accounts.signup.form.steps.base_data.fields.first_name.label', ) " required @@ -290,7 +286,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.additionalName" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.last_name.label', + 'accounts.signup.form.steps.base_data.fields.additional_name.label', ) " required @@ -305,7 +301,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.lastName" :label=" $t( - 'paweljong.event_registration.form.steps.register.fields.last_name.label', + 'accounts.signup.form.steps.base_data.fields.last_name.label', ) " required @@ -331,17 +327,16 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v {{ $t(getStepTitleKey("address_data")) }} </h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> <v-form v-model="validationStatuses['address_data']"> <v-row> - <v-col v-if="isFieldVisible('street')" cols="12" lg="6"> + <v-col cols="12" lg="6"> <div aria-required="true"> <v-text-field outlined v-model="data.person.address.street" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.street.label', + 'accounts.signup.form.steps.address_data.fields.street.label', ) " required @@ -349,14 +344,14 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v ></v-text-field> </div> </v-col> - <v-col v-if="isFieldVisible('housenumber')" cols="12" lg="6"> + <v-col cols="12" lg="6"> <div aria-required="true"> <v-text-field outlined v-model="data.person.address.housenumber" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.housenumber.label', + 'accounts.signup.form.steps.address_data.fields.housenumber.label', ) " required @@ -366,14 +361,14 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v </v-col> </v-row> <v-row> - <v-col v-if="isFieldVisible('postal_code')" cols="12" lg="6"> + <v-col cols="12" lg="6"> <div aria-required="true"> <v-text-field outlined v-model="data.person.address.postalCode" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.postal_code.label', + 'accounts.signup.form.steps.address_data.fields.postal_code.label', ) " required @@ -381,14 +376,14 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v ></v-text-field> </div> </v-col> - <v-col v-if="isFieldVisible('place')" cols="12" lg="6"> + <v-col cols="12" lg="6"> <div aria-required="true"> <v-text-field outlined v-model="data.person.address.place" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.place.label', + 'accounts.signup.form.steps.address_data.fields.place.label', ) " required @@ -414,11 +409,9 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v {{ $t(getStepTitleKey("contact_data")) }} </h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> <v-form v-model="validationStatuses['contact_data']"> <v-row> <v-col - v-if="isFieldVisible('mobile_number')" cols="12" md="6" > @@ -428,7 +421,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.mobileNumber" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.mobile_number.label', + 'accounts.signup.form.steps.contact_data.fields.mobile_number.label', ) " required @@ -438,7 +431,6 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v </div> </v-col> <v-col - v-if="isFieldVisible('phone_number')" cols="12" md="6" > @@ -448,7 +440,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.phoneNumber" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.mobile_number.label', + 'accounts.signup.form.steps.contact_data.fields.mobile_number.label', ) " required @@ -475,11 +467,9 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v {{ $t(getStepTitleKey("additional_data")) }} </h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> <v-form v-model="validationStatuses['additional_data']"> <v-row> <v-col - v-if="isFieldVisible('date_of_birth')" cols="12" md="6" > @@ -489,7 +479,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.dateOfBirth" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.date_of_birth.label', + 'accounts.signup.form.steps.additional_data.fields.date_of_birth.label', ) " required @@ -499,7 +489,6 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v </div> </v-col> <v-col - v-if="isFieldVisible('place_of_birth')" cols="12" md="6" > @@ -509,7 +498,7 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v v-model="data.person.dateOfBirth" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.date_of_birth.label', + 'accounts.signup.form.steps.additional_data.fields.date_of_birth.label', ) " required @@ -520,20 +509,19 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v </v-col> </v-row> <v-row> - <v-col v-if="isFieldVisible('sex')" cols="12" md="6"> + <v-col cols="12" md="6"> <div aria-required="true"> - <!-- FIXME: Prefilling data does not work due to upper-/lowercase situation; will be fixed with core person form refactoring --> <sex-select outlined v-model="data.person.sex" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.sex.label', + 'accounts.signup.form.steps.additional_data.fields.sex.label', ) " :hint=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.sex.help_text', + 'accounts.signup.form.steps.additional_data.fields.sex.help_text', ) " persistent-hint @@ -542,24 +530,31 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v /> </div> </v-col> - <v-col v-if="isFieldVisible('photo')" cols="12" md="6"> + <v-col cols="12" md="6"> <div aria-required="false"> <file-field outlined v-model="data.person.photo" accept="image/jpeg, image/png" + :label=" + $t( + 'accounts.signup.form.steps.additional_data.fields.photo.label', + ) + " + required + :rules="$rules().required.build()" /> </div> </v-col> </v-row> <v-row> - <v-col v-if="isFieldVisible('description')" cols="12"> + <v-col cols="12"> <v-text-field outlined v-model="data.person.description" :label=" $t( - 'paweljong.event_registration.form.steps.contact_details.fields.street.label', + 'accounts.signup.form.steps.additional_data.fields.description.label', ) " required @@ -579,25 +574,8 @@ import AccountRegistrationHelpTextCard from "./AccountRegistrationHelpTextCard.v <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> - <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> - <v-text-field - outlined - v-model="data.comment" - :label=" - $t( - 'paweljong.event_registration.form.steps.confirm.fields.comment.label', - ) - " - ></v-text-field> - </div> - <v-divider class="my-4" /> - <!-- TODO: Add summary --> - <message-box type="info" class="mb-4"> - {{ $t("paweljong.event_registration.form.steps.confirm.hint") }} - </message-box> <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" :variables="{ @@ -650,8 +628,8 @@ export default { this.accountRegistrationSent = true; } }, - isFieldVisible(fieldName) { - return this.event?.contactInformationVisibleFields.includes(fieldName); + isFieldRequired(fieldName) { + return this.accountRegistrationRequiredFields.includes(fieldName); }, isStepEnabled(stepName) { return this.steps.some((s) => s.name === stepName); @@ -682,13 +660,13 @@ export default { email: [ (v) => /.+@.+\..+/.test(v) || - this.$t("paweljong.event_registration.form.rules.email.valid"), + this.$t("accounts.signup.form.rules.email.valid"), ], confirmEmail: [ (v) => this.data.user.email == v || this.$t( - "paweljong.event_registration.form.rules.confirm_email.no_match", + "accounts.signup.form.rules.confirm_email.no_match", ), ], emailLocalPart: [ @@ -707,17 +685,17 @@ export default { (v) => this.data.user.password == v || this.$t( - "paweljong.event_registration.form.rules.confirm_password.no_match", + "accounts.signup.form.rules.confirm_password.no_match", ), ], amount: [ (v) => v >= this.event.minCost || - this.$t("paweljong.event_registration.form.rules.amount.too_low"), + this.$t("accounts.signup.form.rules.amount.too_low"), (v) => this.event.maxCost === null || v <= this.event.maxCost || - this.$t("paweljong.event_registration.form.rules.amount.too_high"), + this.$t("accounts.signup.form.rules.amount.too_high"), ], }; }, @@ -725,6 +703,11 @@ export default { const { confirmEmail, confirmPassword, ...filteredUserData } = this.data.user; + let data = { + ...this.data, + user: filteredUserData, + }; + if (!this.data.email.localPart && !this.data.email.domain) { const { email, ...filteredData } = data; data = filteredData; @@ -733,16 +716,16 @@ export default { }, steps() { return [ + // { + // name: "email", + // titleKey: "accounts.signup.form.steps.email.title", + // participantHelpText: + // this.paweljongSitePreferences + // ?.eventRegistrationGuardiansParticipantHelpText, + // }, { - name: "email", - titleKey: "accounts.signup.form.steps.email.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationGuardiansParticipantHelpText, - }, - { - name: "accounts", - titleKey: "accounts.signup.form.steps.accounts.title", + name: "account", + titleKey: "accounts.signup.form.steps.account.title", participantHelpText: this.paweljongSitePreferences ?.eventRegistrationAdditionalParticipantHelpText, @@ -807,7 +790,7 @@ export default { additionalName: "", lastName: "", shortName: "", - dateOfBirth: "", + dateOfBirth: null, placeOfBirth: "", sex: "", address: { @@ -819,7 +802,7 @@ export default { mobileNumber: "", phoneNumber: "", description: "", - photo: {}, + photo: null, }, user: { username: "", diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 2126146fd..1b011381f 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -43,7 +43,8 @@ "form": { "submitted": { "thank_you": "Thanks!", - "submitted_successfully": "Your account has been successfully registered. You can sign in now." + "submitted_successfully": "Your account has been successfully registered. You can log in with your credentials now.", + "login_button": "Log in" }, "existing_account_alert": "Already have an account? Then please sign in.", "steps": { @@ -67,12 +68,6 @@ "account": { "title": "Account", "fields": { - "first_name": { - "label": "First name" - }, - "last_name": { - "label": "Last name" - }, "username": { "label": "Username" }, @@ -136,10 +131,16 @@ "label": "Date of birth" }, "place_of_birth": { - "label": "Date of birth" + "label": "Place of birth" }, "sex": { "label": "Sex" + }, + "photo": { + "label": "Photo" + }, + "description": { + "label": "Description" } } }, -- GitLab From adc96feaf78ce01a97468a5b1d0a120ff613bfeb Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:38:35 +0100 Subject: [PATCH 09/82] Implement configurable required fields in signup form --- .../account/AccountRegistrationForm.vue | 76 +++++++++---------- .../AccountRegistrationHelpTextCard.vue | 0 .../components/account/helpers.graphql | 7 ++ aleksis/core/preferences.py | 14 ++++ aleksis/core/schema/site_preferences.py | 4 + 5 files changed, 63 insertions(+), 38 deletions(-) delete mode 100644 aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue create mode 100644 aleksis/core/frontend/components/account/helpers.graphql diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 7aeec70b0..bde115e02 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -280,7 +280,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; </div> </v-col> <v-col> - <div aria-required="true"> + <div :aria-required="isFieldRequired('additional_name')"> <v-text-field outlined v-model="data.person.additionalName" @@ -290,7 +290,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('additional_name') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> @@ -330,7 +330,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <v-form v-model="validationStatuses['address_data']"> <v-row> <v-col cols="12" lg="6"> - <div aria-required="true"> + <div :aria-required="isFieldRequired('street')"> <v-text-field outlined v-model="data.person.address.street" @@ -340,12 +340,12 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('street') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> <v-col cols="12" lg="6"> - <div aria-required="true"> + <div :aria-required="isFieldRequired('housenumber')"> <v-text-field outlined v-model="data.person.address.housenumber" @@ -355,14 +355,14 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('housenumber') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> </v-row> <v-row> <v-col cols="12" lg="6"> - <div aria-required="true"> + <div :aria-required="isFieldRequired('postal_code')"> <v-text-field outlined v-model="data.person.address.postalCode" @@ -372,12 +372,12 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('postal_code') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> <v-col cols="12" lg="6"> - <div aria-required="true"> + <div :aria-required="isFieldRequired('place')"> <v-text-field outlined v-model="data.person.address.place" @@ -387,7 +387,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('place') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> @@ -415,7 +415,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; cols="12" md="6" > - <div aria-required="true"> + <div :aria-required="isFieldRequired('mobile_number')"> <v-text-field outlined v-model="data.person.mobileNumber" @@ -425,8 +425,8 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - prepend-icon="mdi-phone-outline" - :rules="$rules().required.build()" + prepend-icon="mdi-cellphone-basic" + :rules="isFieldRequired('mobile_number') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> @@ -434,18 +434,18 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; cols="12" md="6" > - <div aria-required="true"> + <div :aria-required="isFieldRequired('phone_number')"> <v-text-field outlined v-model="data.person.phoneNumber" :label=" $t( - 'accounts.signup.form.steps.contact_data.fields.mobile_number.label', + 'accounts.signup.form.steps.contact_data.fields.phone_number.label', ) " required prepend-icon="mdi-phone-outline" - :rules="$rules().required.build()" + :rules="isFieldRequired('phone_number') ? $rules().required.build() : []" ></v-text-field> </div> </v-col> @@ -473,7 +473,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; cols="12" md="6" > - <div aria-required="true"> + <div :aria-required="isFieldRequired('date_of_birth')"> <date-field outlined v-model="data.person.dateOfBirth" @@ -483,7 +483,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('date_of_birth') ? $rules().required.build() : []" prepend-icon="mdi-cake-variant-outline" /> </div> @@ -492,25 +492,24 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; cols="12" md="6" > - <div aria-required="true"> - <date-field + <div :aria-required="isFieldRequired('place_of_birth')"> + <v-text-field outlined - v-model="data.person.dateOfBirth" + v-model="data.person.placeOfBirth" :label=" $t( - 'accounts.signup.form.steps.additional_data.fields.date_of_birth.label', + 'accounts.signup.form.steps.additional_data.fields.place_of_birth.label', ) " required - :rules="$rules().required.build()" - prepend-icon="mdi-cake-variant-outline" + :rules="isFieldRequired('place_of_birth') ? $rules().required.build() : []" /> </div> </v-col> </v-row> <v-row> <v-col cols="12" md="6"> - <div aria-required="true"> + <div :aria-required="isFieldRequired('sex')"> <sex-select outlined v-model="data.person.sex" @@ -519,19 +518,13 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; 'accounts.signup.form.steps.additional_data.fields.sex.label', ) " - :hint=" - $t( - 'accounts.signup.form.steps.additional_data.fields.sex.help_text', - ) - " - persistent-hint required - :rules="$rules().required.build()" + :rules="isFieldRequired('sex') ? $rules().required.build() : []" /> </div> </v-col> <v-col cols="12" md="6"> - <div aria-required="false"> + <div :aria-required="isFieldRequired('photo')"> <file-field outlined v-model="data.person.photo" @@ -542,14 +535,15 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" + :rules="isFieldRequired('photo') ? $rules().required.build() : []" /> </div> </v-col> </v-row> <v-row> <v-col cols="12"> - <v-text-field + <div :aria-required="isFieldRequired('description')"> + <v-text-field outlined v-model="data.person.description" :label=" @@ -558,8 +552,9 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="$rules().required.build()" - ></v-text-field> + :rules="isFieldRequired('description') ? $rules().required.build() : []" + /> + </div> </v-col> </v-row> </v-form> @@ -609,6 +604,8 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; // disallowedLocalParts, // } from "aleksis.apps.postbuero/components/mail_addresses/mailAddresses.graphql"; +import { gqlRequiredFieldsPreference } from "./helpers.graphql"; + import formRulesMixin from "../../mixins/formRulesMixin"; export default { @@ -616,6 +613,9 @@ export default { apollo: { // mailDomains: mailDomainsForUser, // disallowedLocalParts: disallowedLocalParts, + systemProperties: { + query: gqlRequiredFieldsPreference, + }, }, mixins: [formRulesMixin], methods: { @@ -629,7 +629,7 @@ export default { } }, isFieldRequired(fieldName) { - return this.accountRegistrationRequiredFields.includes(fieldName); + return this?.systemProperties?.sitePreferences?.signupRequiredFields.includes(fieldName); }, isStepEnabled(stepName) { return this.steps.some((s) => s.name === stepName); diff --git a/aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue b/aleksis/core/frontend/components/account/AccountRegistrationHelpTextCard.vue deleted file mode 100644 index e69de29bb..000000000 diff --git a/aleksis/core/frontend/components/account/helpers.graphql b/aleksis/core/frontend/components/account/helpers.graphql new file mode 100644 index 000000000..8a076250e --- /dev/null +++ b/aleksis/core/frontend/components/account/helpers.graphql @@ -0,0 +1,7 @@ +query gqlRequiredFieldsPreference { + systemProperties { + sitePreferences { + signupRequiredFields + } + } +} diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index ed94efc34..384999530 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -304,6 +304,20 @@ class SignupEnabled(BooleanPreference): verbose_name = _("Enable signup") +@site_preferences_registry.register +class SignupRequiredFields(MultipleChoicePreference): + """Fields on person model which are required when signing up.""" + + section = auth + name = "signup_required_fields" + default = [] + widget = SelectMultiple + verbose_name = _("Fields on person model which are required when signing up. The first and last name fields are always required.") + field_attribute = {"initial": []} + choices = [(field.name, field.name) for field in Person.syncable_fields() if getattr(field, "blank", False)] + required = False + + @site_preferences_registry.register class AllowedUsernameRegex(StringPreference): section = auth diff --git a/aleksis/core/schema/site_preferences.py b/aleksis/core/schema/site_preferences.py index 730e12cae..bd25aedc0 100644 --- a/aleksis/core/schema/site_preferences.py +++ b/aleksis/core/schema/site_preferences.py @@ -25,6 +25,7 @@ class SitePreferencesType(graphene.ObjectType): invite_enabled = graphene.Boolean() signup_enabled = graphene.Boolean() + signup_required_fields = graphene.List(graphene.String) auth_allowed_username_regex = graphene.String() auth_disallowed_uids = graphene.List(graphene.String) @@ -76,3 +77,6 @@ class SitePreferencesType(graphene.ObjectType): def resolve_signup_enabled(parent, info, **kwargs): return parent["auth__signup_enabled"] + + def resolve_signup_required_fields(parent, info, **kwargs): + return parent["auth__signup_required_fields"] -- GitLab From c2b9f784ac0c6df3f96e53da7cf7198e2158d065 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:39:28 +0100 Subject: [PATCH 10/82] Add missing import --- aleksis/core/schema/person.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 1a9d239f0..61d4f5288 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,7 +1,7 @@ from typing import Union from django.contrib.auth import get_user_model -from django.core.exceptions import PermissionDenied, SuspiciousOperation +from django.core.exceptions import PermissionDenied, SuspiciousOperation, ValidationError from django.db import IntegrityError, transaction from django.db.models import Q from django.utils import timezone -- GitLab From 7838aa8a3663c12aad587cc27bc8a6dc34a8217d Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:41:28 +0100 Subject: [PATCH 11/82] Add margin to error alert --- .../frontend/components/account/AccountRegistrationForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index bde115e02..8cc230171 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -586,7 +586,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; @confirm="mutate" :next-loading="loading" /> - <v-alert v-if="error" type="error" outlined>{{ + <v-alert v-if="error" type="error" outlined class="mt-4">{{ error.message }}</v-alert> </template> -- GitLab From 4db0406192eddbc68b515bd5b06bb4c3cb63d122 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:42:28 +0100 Subject: [PATCH 12/82] Fix translation key --- .../frontend/components/account/AccountRegistrationForm.vue | 2 +- aleksis/core/frontend/messages/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 8cc230171..2588ed072 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -12,7 +12,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <v-card> <v-card-title> <v-icon class="mr-2" color="success">mdi-check-circle-outline</v-icon> - {{ $t("accounts.signup.form.form.submitted.thank_you") }} + {{ $t("accounts.signup.form.submitted.title") }} </v-card-title> <v-card-text class="text-body-1 black--text"> {{ diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 1b011381f..9cfd4c2fa 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -42,7 +42,7 @@ "menu_title": "Sign Up", "form": { "submitted": { - "thank_you": "Thanks!", + "title": "Registration successful!", "submitted_successfully": "Your account has been successfully registered. You can log in with your credentials now.", "login_button": "Log in" }, -- GitLab From b6aec29f12cb8b444754ee4ddff8b18e00f6b423 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:45:13 +0100 Subject: [PATCH 13/82] Add missing file field import --- .../core/frontend/components/account/AccountRegistrationForm.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 2588ed072..8b9659508 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -1,6 +1,7 @@ <script setup> import ControlRow from "../generic/multi_step/ControlRow.vue"; import DateField from "../generic/forms/DateField.vue"; +import FileField from "../generic/forms/FileField.vue"; import SexSelect from "../generic/forms/SexSelect.vue"; import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; -- GitLab From e0a1a6855c678204e338f9a10dd5a4df77d5da85 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 14 Mar 2025 00:46:10 +0100 Subject: [PATCH 14/82] Remove unneeded todo --- aleksis/core/schema/person.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 61d4f5288..148815650 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -346,8 +346,6 @@ class PersonInputType(graphene.InputObjectType): address = graphene.Field(AddressInputType, required=False) - # TODO: Photo and avatar - photo = Upload() avatar = Upload() -- GitLab From 7d362d950ff6ce41e24fa8a8bfeec5a0aa58343e Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sat, 15 Mar 2025 23:53:44 +0100 Subject: [PATCH 15/82] Add login button in alert --- .../account/AccountRegistrationForm.vue | 26 ++++++++++++++----- aleksis/core/frontend/messages/en.json | 6 ++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 8b9659508..803232615 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -25,18 +25,32 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <v-card-actions> <primary-action-button :to="{ name: 'core.account.login' }" - i18n-key="accounts.signup.form.submitted.login_button" + i18n-key="accounts.signup.form.login_button" /> </v-card-actions> </v-card> </div> <div v-else> <v-alert type="info" dense outlined class="mb-4"> - {{ - $t( - "accounts.signup.form.existing_account_alert", - ) - }} + <v-row align="center" no-gutters> + <v-col cols="12" md="9"> + {{ + $t( + "accounts.signup.form.existing_account_alert", + ) + }} + </v-col> + <v-col cols="12" md="3" align="right"> + <v-btn + color="info" + outlined + small + :to="{ name: 'core.account.login' }" + > + {{ $t("accounts.signup.form.login_button") }} + </v-btn> + </v-col> + </v-row> </v-alert> <v-stepper v-model="step" class="mb-4"> <v-stepper-header> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 9cfd4c2fa..9f803575b 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -41,12 +41,12 @@ "signup": { "menu_title": "Sign Up", "form": { + "login_button": "Go to login", "submitted": { "title": "Registration successful!", - "submitted_successfully": "Your account has been successfully registered. You can log in with your credentials now.", - "login_button": "Log in" + "submitted_successfully": "Your account has been successfully registered. You can log in with your credentials now." }, - "existing_account_alert": "Already have an account? Then please sign in.", + "existing_account_alert": "Already have an account? Then please log in.", "steps": { "email": { "title": "E-Mail address", -- GitLab From 1545fa77ac06f98bc6f2039ac11e3c6b8ffd51af Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sat, 15 Mar 2025 23:56:48 +0100 Subject: [PATCH 16/82] Remove help text references --- .../account/AccountRegistrationForm.vue | 24 ------------------- 1 file changed, 24 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 803232615..95766e8f5 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -741,53 +741,29 @@ export default { { name: "account", titleKey: "accounts.signup.form.steps.account.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, { name: "base_data", titleKey: "accounts.signup.form.steps.base_data.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, { name: "address_data", titleKey: "accounts.signup.form.steps.address_data.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, { name: "contact_data", titleKey: "accounts.signup.form.steps.contact_data.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, { name: "additional_data", titleKey: "accounts.signup.form.steps.additional_data.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, { name: "confirm", titleKey: "accounts.signup.form.steps.confirm.title", - participantHelpText: - this.paweljongSitePreferences - ?.eventRegistrationAdditionalParticipantHelpText, }, ]; }, - currentParticipantHelpText() { - return this.steps[this.step - 1].participantHelpText; - }, - currentGuardianHelpText() { - return this.steps[this.step - 1].guardianHelpText; - }, }, data() { return { -- GitLab From 58a03b1f65f45db8d54cd1c8042a5d72f79d02fb Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 16 Mar 2025 00:46:23 +0100 Subject: [PATCH 17/82] Remove unused rules --- .../components/account/AccountRegistrationForm.vue | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 95766e8f5..9b63d1dc2 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -668,10 +668,6 @@ export default { computed: { rules() { return { - name: [ - (v) => !!v || this.$t("order.rules.name.required"), - (v) => v.length <= 255 || this.$t("order.rules.name.max"), - ], email: [ (v) => /.+@.+\..+/.test(v) || @@ -703,15 +699,6 @@ export default { "accounts.signup.form.rules.confirm_password.no_match", ), ], - amount: [ - (v) => - v >= this.event.minCost || - this.$t("accounts.signup.form.rules.amount.too_low"), - (v) => - this.event.maxCost === null || - v <= this.event.maxCost || - this.$t("accounts.signup.form.rules.amount.too_high"), - ], }; }, dataForSubmit() { -- GitLab From 620c54f18c57e5396cb066610916d8d1a17b5420 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 16 Mar 2025 02:05:34 +0100 Subject: [PATCH 18/82] Allow injection of mail step from paweljong --- aleksis/core/frontend/collections.js | 4 + .../account/AccountRegistrationForm.vue | 197 ++++-------------- 2 files changed, 47 insertions(+), 154 deletions(-) diff --git a/aleksis/core/frontend/collections.js b/aleksis/core/frontend/collections.js index 98cf666d9..cd439f4f7 100644 --- a/aleksis/core/frontend/collections.js +++ b/aleksis/core/frontend/collections.js @@ -21,6 +21,10 @@ export const collections = [ name: "personWidgets", type: Object, }, + { + name: "accountRegistrationSteps", + type: Object, + }, ]; export const collectionItems = { diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 9b63d1dc2..1d47bad81 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -67,126 +67,18 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; </template> </v-stepper-header> <v-stepper-items> - <!-- <v-stepper-content + <v-stepper-content v-if="isStepEnabled('email')" :step="getStepIndex('email')" > <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("email")) }}</h2> <div class="mb-4"> - <event-registration-help-text-cards :participant-help-text="currentGuardianHelpText" :guardian-help-text="currentGuardianHelpText" /> - <v-tabs - v-model="emailMode" - :grow="!$vuetify.breakpoint.mdAndDown" - optional - show-arrows - > - <v-tab style="max-width: 50vw" class="primary--text"> - {{ - $t( - "accounts.signup.form.steps.email.choose_mode.continue_aleksis", - ) - }} - </v-tab> - <v-tab style="max-width: 50vw" class="primary--text"> - {{ - $t( - "accounts.signup.form.steps.email.choose_mode.continue_own", - ) - }} - </v-tab> - </v-tabs> - <v-form v-model="validationStatuses['email']"> - <v-tabs-items v-model="emailMode"> - <v-tab-item> - <v-row class="mt-4"> - <v-col cols="12" md="6"> - <div aria-required="true"> - <v-text-field - outlined - v-model="data.email.localPart" - :label=" - $t( - 'postbuero.mail_addresses.data_table.local_part', - ) - " - :rules=" - emailMode === 0 - ? $rules().required.build(rules.emailLocalPart) - : [] - " - required - ></v-text-field> - </div> - </v-col> - <v-col cols="12" md="6"> - <div aria-required="true"> - <v-autocomplete - outlined - hide-no-data - :items="mailDomains" - item-text="domain" - item-value="id" - :loading="$apollo.queries.mailDomains.loading" - prepend-icon="mdi-at" - v-model="data.email.domain" - :label=" - $t('postbuero.mail_addresses.data_table.domain') - " - required - :rules=" - emailMode === 0 ? $rules().required.build() : [] - " - /> - </div> - </v-col> - </v-row> - </v-tab-item> - <v-tab-item> - <v-row class="mt-4"> - <v-col cols="12" md="6"> - <div aria-required="true"> - <v-text-field - outlined - v-model="data.user.email" - :label=" - $t( - 'accounts.signup.form.steps.email.fields.email.label', - ) - " - required - :rules=" - emailMode === 1 - ? $rules().required.build(rules.email) - : [] - " - prepend-icon="mdi-email-outline" - ></v-text-field> - </div> - </v-col> - <v-col cols="12" md="6"> - <div aria-required="true"> - <v-text-field - outlined - v-model="data.user.confirmEmail" - :label=" - $t( - 'accounts.signup.form.steps.email.fields.confirm_email.label', - ) - " - required - :rules=" - emailMode === 1 - ? $rules().required.build(rules.confirmEmail) - : [] - " - prepend-icon="mdi-email-outline" - ></v-text-field> - </div> - </v-col> - </v-row> - </v-tab-item> - </v-tabs-items> - </v-form> + <component + :is="collectionSteps.find((s) => s.key === 'postbuero-mail-address-form-step')?.component" + @dataChange="mergeIncomingData" + @emailModeChange="setEmailMode" + v-model="validationStatuses['email']" + /> </div> <v-divider class="mb-4" /> <control-row @@ -198,7 +90,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; !validationStatuses['email'] " /> - </v-stepper-content> --> + </v-stepper-content> <v-stepper-content :step="getStepIndex('account')" @@ -614,20 +506,14 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; </template> <script> -// import { -// mailDomainsForUser, -// disallowedLocalParts, -// } from "aleksis.apps.postbuero/components/mail_addresses/mailAddresses.graphql"; - import { gqlRequiredFieldsPreference } from "./helpers.graphql"; +import { collections } from "aleksisAppImporter"; import formRulesMixin from "../../mixins/formRulesMixin"; export default { name: "AccountRegistrationForm", apollo: { - // mailDomains: mailDomainsForUser, - // disallowedLocalParts: disallowedLocalParts, systemProperties: { query: gqlRequiredFieldsPreference, }, @@ -664,34 +550,30 @@ export default { } return false; }, + deepMerge(existing, incoming) { + return Object.entries(incoming).reduce((merged, [key, value]) => { + if (typeof value === "object") { + if (Array.isArray(value)) { + merged[key] = this.deepMerge([], value); + } else { + merged[key] = this.deepMerge({}, value); + } + } else { + merged[key] = value; + } + return merged; + }, {...existing}); + }, + mergeIncomingData(incomingData) { + this.data = this.deepMerge(this.data, incomingData); + }, + setEmailMode(emailMode) { + this.emailMode = emailMode; + } }, computed: { rules() { return { - email: [ - (v) => - /.+@.+\..+/.test(v) || - this.$t("accounts.signup.form.rules.email.valid"), - ], - confirmEmail: [ - (v) => - this.data.user.email == v || - this.$t( - "accounts.signup.form.rules.confirm_email.no_match", - ), - ], - emailLocalPart: [ - (v) => - /^\w+([.!#$%&'*+-\/=?^_`{|}~]?\w+)*$/.test(v) || - this.$t( - "postbuero.mail_addresses.data_table.errors.local_part_invalid_characters", - ), - (v) => - this.disallowedLocalParts.indexOf(v) === -1 || - this.$t( - "postbuero.mail_addresses.data_table.errors.local_part_disallowed", - ), - ], confirmPassword: [ (v) => this.data.user.password == v || @@ -718,13 +600,14 @@ export default { }, steps() { return [ - // { - // name: "email", - // titleKey: "accounts.signup.form.steps.email.title", - // participantHelpText: - // this.paweljongSitePreferences - // ?.eventRegistrationGuardiansParticipantHelpText, - // }, + ...(this.collectionSteps.some((s) => s.key === "postbuero-mail-address-form-step") + ? [ + { + name: "email", + titleKey: "accounts.signup.form.steps.email.title", + }, + ] + : []), { name: "account", titleKey: "accounts.signup.form.steps.account.title", @@ -751,6 +634,12 @@ export default { }, ]; }, + collectionSteps() { + if (Object.hasOwn(collections, "coreAccountRegistrationSteps")) { + return collections.coreAccountRegistrationSteps.items; + } + return []; + }, }, data() { return { -- GitLab From 106f643e17a4c45fc8873faea3e7c7b9f92c1dd7 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Sun, 16 Mar 2025 02:09:27 +0100 Subject: [PATCH 19/82] Reformat --- .../account/AccountRegistrationForm.vue | 179 ++++++++++-------- aleksis/core/frontend/routeValidators.js | 8 +- aleksis/core/preferences.py | 10 +- aleksis/core/schema/person.py | 11 +- 4 files changed, 123 insertions(+), 85 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 1d47bad81..10b32ed38 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -16,11 +16,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; {{ $t("accounts.signup.form.submitted.title") }} </v-card-title> <v-card-text class="text-body-1 black--text"> - {{ - $t( - "accounts.signup.form.submitted.submitted_successfully", - ) - }} + {{ $t("accounts.signup.form.submitted.submitted_successfully") }} </v-card-text> <v-card-actions> <primary-action-button @@ -34,11 +30,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <v-alert type="info" dense outlined class="mb-4"> <v-row align="center" no-gutters> <v-col cols="12" md="9"> - {{ - $t( - "accounts.signup.form.existing_account_alert", - ) - }} + {{ $t("accounts.signup.form.existing_account_alert") }} </v-col> <v-col cols="12" md="3" align="right"> <v-btn @@ -74,7 +66,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("email")) }}</h2> <div class="mb-4"> <component - :is="collectionSteps.find((s) => s.key === 'postbuero-mail-address-form-step')?.component" + :is=" + collectionSteps.find( + (s) => s.key === 'postbuero-mail-address-form-step', + )?.component + " @dataChange="mergeIncomingData" @emailModeChange="setEmailMode" v-model="validationStatuses['email']" @@ -92,9 +88,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; /> </v-stepper-content> - <v-stepper-content - :step="getStepIndex('account')" - > + <v-stepper-content :step="getStepIndex('account')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("account")) }}</h2> <div class="mb-4"> <v-form v-model="validationStatuses['account']"> @@ -162,9 +156,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; /> </v-stepper-content> - <v-stepper-content - :step="getStepIndex('base_data')" - > + <v-stepper-content :step="getStepIndex('base_data')"> <h2 class="text-h6 mb-4"> {{ $t(getStepTitleKey("base_data")) }} </h2> @@ -197,7 +189,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('additional_name') ? $rules().required.build() : []" + :rules=" + isFieldRequired('additional_name') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -227,9 +223,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; /> </v-stepper-content> - <v-stepper-content - :step="getStepIndex('address_data')" - > + <v-stepper-content :step="getStepIndex('address_data')"> <h2 class="text-h6 mb-4"> {{ $t(getStepTitleKey("address_data")) }} </h2> @@ -247,7 +241,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('street') ? $rules().required.build() : []" + :rules=" + isFieldRequired('street') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -262,7 +260,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('housenumber') ? $rules().required.build() : []" + :rules=" + isFieldRequired('housenumber') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -279,7 +281,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('postal_code') ? $rules().required.build() : []" + :rules=" + isFieldRequired('postal_code') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -294,7 +300,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('place') ? $rules().required.build() : []" + :rules=" + isFieldRequired('place') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -309,19 +319,14 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; /> </v-stepper-content> - <v-stepper-content - :step="getStepIndex('contact_data')" - > + <v-stepper-content :step="getStepIndex('contact_data')"> <h2 class="text-h6 mb-4"> {{ $t(getStepTitleKey("contact_data")) }} </h2> <div class="mb-4"> <v-form v-model="validationStatuses['contact_data']"> <v-row> - <v-col - cols="12" - md="6" - > + <v-col cols="12" md="6"> <div :aria-required="isFieldRequired('mobile_number')"> <v-text-field outlined @@ -333,14 +338,15 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; " required prepend-icon="mdi-cellphone-basic" - :rules="isFieldRequired('mobile_number') ? $rules().required.build() : []" + :rules=" + isFieldRequired('mobile_number') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> - <v-col - cols="12" - md="6" - > + <v-col cols="12" md="6"> <div :aria-required="isFieldRequired('phone_number')"> <v-text-field outlined @@ -352,7 +358,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; " required prepend-icon="mdi-phone-outline" - :rules="isFieldRequired('phone_number') ? $rules().required.build() : []" + :rules=" + isFieldRequired('phone_number') + ? $rules().required.build() + : [] + " ></v-text-field> </div> </v-col> @@ -367,19 +377,14 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; /> </v-stepper-content> - <v-stepper-content - :step="getStepIndex('additional_data')" - > + <v-stepper-content :step="getStepIndex('additional_data')"> <h2 class="text-h6 mb-4"> {{ $t(getStepTitleKey("additional_data")) }} </h2> <div class="mb-4"> <v-form v-model="validationStatuses['additional_data']"> <v-row> - <v-col - cols="12" - md="6" - > + <v-col cols="12" md="6"> <div :aria-required="isFieldRequired('date_of_birth')"> <date-field outlined @@ -390,17 +395,18 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('date_of_birth') ? $rules().required.build() : []" + :rules=" + isFieldRequired('date_of_birth') + ? $rules().required.build() + : [] + " prepend-icon="mdi-cake-variant-outline" /> </div> </v-col> - <v-col - cols="12" - md="6" - > - <div :aria-required="isFieldRequired('place_of_birth')"> - <v-text-field + <v-col cols="12" md="6"> + <div :aria-required="isFieldRequired('place_of_birth')"> + <v-text-field outlined v-model="data.person.placeOfBirth" :label=" @@ -409,7 +415,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('place_of_birth') ? $rules().required.build() : []" + :rules=" + isFieldRequired('place_of_birth') + ? $rules().required.build() + : [] + " /> </div> </v-col> @@ -426,7 +436,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('sex') ? $rules().required.build() : []" + :rules=" + isFieldRequired('sex') + ? $rules().required.build() + : [] + " /> </div> </v-col> @@ -442,7 +456,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('photo') ? $rules().required.build() : []" + :rules=" + isFieldRequired('photo') + ? $rules().required.build() + : [] + " /> </div> </v-col> @@ -459,7 +477,11 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; ) " required - :rules="isFieldRequired('description') ? $rules().required.build() : []" + :rules=" + isFieldRequired('description') + ? $rules().required.build() + : [] + " /> </div> </v-col> @@ -530,7 +552,9 @@ export default { } }, isFieldRequired(fieldName) { - return this?.systemProperties?.sitePreferences?.signupRequiredFields.includes(fieldName); + return this?.systemProperties?.sitePreferences?.signupRequiredFields.includes( + fieldName, + ); }, isStepEnabled(stepName) { return this.steps.some((s) => s.name === stepName); @@ -551,25 +575,28 @@ export default { return false; }, deepMerge(existing, incoming) { - return Object.entries(incoming).reduce((merged, [key, value]) => { - if (typeof value === "object") { - if (Array.isArray(value)) { - merged[key] = this.deepMerge([], value); + return Object.entries(incoming).reduce( + (merged, [key, value]) => { + if (typeof value === "object") { + if (Array.isArray(value)) { + merged[key] = this.deepMerge([], value); + } else { + merged[key] = this.deepMerge({}, value); + } } else { - merged[key] = this.deepMerge({}, value); + merged[key] = value; } - } else { - merged[key] = value; - } - return merged; - }, {...existing}); + return merged; + }, + { ...existing }, + ); }, mergeIncomingData(incomingData) { this.data = this.deepMerge(this.data, incomingData); }, setEmailMode(emailMode) { this.emailMode = emailMode; - } + }, }, computed: { rules() { @@ -577,9 +604,7 @@ export default { confirmPassword: [ (v) => this.data.user.password == v || - this.$t( - "accounts.signup.form.rules.confirm_password.no_match", - ), + this.$t("accounts.signup.form.rules.confirm_password.no_match"), ], }; }, @@ -587,10 +612,10 @@ export default { const { confirmEmail, confirmPassword, ...filteredUserData } = this.data.user; - let data = { - ...this.data, - user: filteredUserData, - }; + let data = { + ...this.data, + user: filteredUserData, + }; if (!this.data.email.localPart && !this.data.email.domain) { const { email, ...filteredData } = data; @@ -600,7 +625,9 @@ export default { }, steps() { return [ - ...(this.collectionSteps.some((s) => s.key === "postbuero-mail-address-form-step") + ...(this.collectionSteps.some( + (s) => s.key === "postbuero-mail-address-form-step", + ) ? [ { name: "email", diff --git a/aleksis/core/frontend/routeValidators.js b/aleksis/core/frontend/routeValidators.js index f4fe977d6..6fb57fdad 100644 --- a/aleksis/core/frontend/routeValidators.js +++ b/aleksis/core/frontend/routeValidators.js @@ -32,5 +32,9 @@ const signupEnabledValidator = (_, systemProperties) => { return systemProperties && systemProperties.sitePreferences.signupEnabled; }; - -export { notLoggedInValidator, hasPersonValidator, inviteEnabledValidator, signupEnabledValidator }; +export { + notLoggedInValidator, + hasPersonValidator, + inviteEnabledValidator, + signupEnabledValidator, +}; diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 384999530..b945a7951 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -312,9 +312,15 @@ class SignupRequiredFields(MultipleChoicePreference): name = "signup_required_fields" default = [] widget = SelectMultiple - verbose_name = _("Fields on person model which are required when signing up. The first and last name fields are always required.") + verbose_name = _( + "Fields on person model which are required when signing up. The first and last name fields are always required." + ) field_attribute = {"initial": []} - choices = [(field.name, field.name) for field in Person.syncable_fields() if getattr(field, "blank", False)] + choices = [ + (field.name, field.name) + for field in Person.syncable_fields() + if getattr(field, "blank", False) + ] required = False diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 148815650..3a84d1ec9 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -595,7 +595,7 @@ class SendAccountRegistrationMutation(graphene.Mutation): user = get_user_model().objects.create_user( username=account_registration["user"]["username"], email=account_registration["user"]["email"], - password=account_registration["user"]["password"] + password=account_registration["user"]["password"], ) except IntegrityError: raise ValidationError(_("A user with this username or e-mail already exists.")) @@ -611,7 +611,10 @@ class SendAccountRegistrationMutation(graphene.Mutation): }, ) except IntegrityError: - raise ValidationError(_("A person using the e-mail address %s already exists.") % account_registration["user"]["email"]) + raise ValidationError( + _("A person using the e-mail address %s already exists.") + % account_registration["user"]["email"] + ) # Store contact information in database for field in Person._meta.get_fields(): @@ -622,9 +625,7 @@ class SendAccountRegistrationMutation(graphene.Mutation): and account_registration["person"]["address"][field.name] is not None and account_registration["person"]["address"][field.name] != "" ): - setattr( - person, field.name, account_registration["person"]["address"][field.name] - ) + setattr(person, field.name, account_registration["person"]["address"][field.name]) elif ( field.name in account_registration["person"] and account_registration["person"][field.name] is not None -- GitLab From 3b09c4d9189cbb5c0d6af37051f8f71edd5addee Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 19 Mar 2025 01:49:28 +0100 Subject: [PATCH 20/82] Adapt to new address model and display summary at end of wizard --- .../account/AccountRegistrationForm.vue | 37 +++- .../components/person/PersonDetailsCard.vue | 191 ++++++++++++++++++ .../components/person/PersonOverview.vue | 181 +---------------- aleksis/core/schema/person.py | 35 ++-- 4 files changed, 234 insertions(+), 210 deletions(-) create mode 100644 aleksis/core/frontend/components/person/PersonDetailsCard.vue diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 10b32ed38..f84bfc3f8 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -5,6 +5,8 @@ import FileField from "../generic/forms/FileField.vue"; import SexSelect from "../generic/forms/SexSelect.vue"; import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; + +import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </script> <template> @@ -234,7 +236,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <div :aria-required="isFieldRequired('street')"> <v-text-field outlined - v-model="data.person.address.street" + v-model="data.person.street" :label=" $t( 'accounts.signup.form.steps.address_data.fields.street.label', @@ -253,7 +255,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <div :aria-required="isFieldRequired('housenumber')"> <v-text-field outlined - v-model="data.person.address.housenumber" + v-model="data.person.housenumber" :label=" $t( 'accounts.signup.form.steps.address_data.fields.housenumber.label', @@ -274,7 +276,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <div :aria-required="isFieldRequired('postal_code')"> <v-text-field outlined - v-model="data.person.address.postalCode" + v-model="data.person.postalCode" :label=" $t( 'accounts.signup.form.steps.address_data.fields.postal_code.label', @@ -293,7 +295,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <div :aria-required="isFieldRequired('place')"> <v-text-field outlined - v-model="data.person.address.place" + v-model="data.person.place" :label=" $t( 'accounts.signup.form.steps.address_data.fields.place.label', @@ -498,7 +500,7 @@ import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> - <!-- TODO: Add summary --> + <person-details-card class="mb-4" :person="personDataForSummary" /> <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" @@ -623,6 +625,20 @@ export default { } return data; }, + personDataForSummary() { + return { + ...this.data.person, + addresses: [ + { + street: this.data.person.street, + housenumber: this.data.person.housenumber, + postalCode: this.data.person.postalCode, + place: this.data.person.place, + country: this.data.person.country, + }, + ], + } + }, steps() { return [ ...(this.collectionSteps.some( @@ -687,12 +703,11 @@ export default { dateOfBirth: null, placeOfBirth: "", sex: "", - address: { - street: "", - housenumber: "", - postalCode: "", - place: "", - }, + street: "", + housenumber: "", + postalCode: "", + place: "", + country: "", mobileNumber: "", phoneNumber: "", description: "", diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue new file mode 100644 index 000000000..c7aeaef77 --- /dev/null +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -0,0 +1,191 @@ +<template> + <v-card v-bind="$attrs"> + <v-card-title>{{ $t("person.details") }}</v-card-title> + + <v-list two-line> + <v-list-item> + <v-list-item-icon> + <v-icon> mdi-account-outline</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + {{ person.firstName }} + {{ person.additionalName }} + {{ person.lastName }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.name") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + + <v-list-item> + <v-list-item-icon> + <v-icon> mdi-human-non-binary</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + {{ + person.sex + ? $t("person.sex." + person.sex.toLowerCase()) + : "–" + }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.sex_description") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + + + <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> + {{ 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 + v-for="addresstype in address.addressTypes" + :key="addresstype.id" + > + {{ addresstype.name }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + + <v-list-item + :href="person.phoneNumber ? 'tel:' + person.phoneNumber : ''" + > + <v-list-item-icon> + <v-icon> mdi-phone-outline</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + {{ person.phoneNumber || "–" }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.home") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + + <v-list-item + :href=" + person.mobileNumber ? 'tel:' + person.mobileNumber : '' + " + > + <v-list-item-action></v-list-item-action> + + <v-list-item-content> + <v-list-item-title> + {{ person.mobileNumber || "–" }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.mobile") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + + <v-list-item + :href="person.email ? 'mailto:' + person.email : ''" + > + <v-list-item-icon> + <v-icon>mdi-email-outline</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + {{ person.email || "–" }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.email_address") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + + <v-list-item> + <v-list-item-icon> + <v-icon> mdi-cake-variant-outline</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + <span v-if="person.dateOfBirth && person.placeOfBirth"> + {{ + $t("person.birth_date_and_birth_place_formatted", { + date: $d( + $parseISODate(person.dateOfBirth), + "short", + ), + place: person.placeOfBirth, + }) + }} + </span> + <span v-else-if="person.dateOfBirth">{{ + $d($parseISODate(person.dateOfBirth), "short") + }}</span> + <span v-else-if="person.placeOfBirth">{{ + person.placeOfBirth + }}</span> + <span v-else>–</span> + </v-list-item-title> + <v-list-item-subtitle> + <span v-if="!person.dateOfBirth === !person.placeOfBirth"> + {{ $t("person.birth_date_and_birth_place") }} + </span> + <span v-else-if="person.dateOfBirth"> + {{ $t("person.birth_date") }} + </span> + <span v-else-if="person.placeOfBirth"> + {{ $t("person.birth_place") }} + </span> + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + </v-list> + </v-card> +</template> + +<script> +export default { + name: "PersonDetailsCard", + props: { + person: { + type: Object, + required: true, + }, + }, +} +</script> diff --git a/aleksis/core/frontend/components/person/PersonOverview.vue b/aleksis/core/frontend/components/person/PersonOverview.vue index 326fac470..e57d9b21c 100644 --- a/aleksis/core/frontend/components/person/PersonOverview.vue +++ b/aleksis/core/frontend/components/person/PersonOverview.vue @@ -48,184 +48,7 @@ <v-row> <v-col cols="12" lg="4"> - <v-card class="mb-6"> - <v-card-title>{{ $t("person.details") }}</v-card-title> - - <v-list two-line> - <v-list-item> - <v-list-item-icon> - <v-icon> mdi-account-outline</v-icon> - </v-list-item-icon> - - <v-list-item-content> - <v-list-item-title> - {{ person.firstName }} - {{ person.additionalName }} - {{ person.lastName }} - </v-list-item-title> - <v-list-item-subtitle> - {{ $t("person.name") }} - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - <v-divider inset /> - - <v-list-item> - <v-list-item-icon> - <v-icon> mdi-human-non-binary</v-icon> - </v-list-item-icon> - - <v-list-item-content> - <v-list-item-title> - {{ - person.sex - ? $t("person.sex." + person.sex.toLowerCase()) - : "–" - }} - </v-list-item-title> - <v-list-item-subtitle> - {{ $t("person.sex_description") }} - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - <v-divider inset /> - - <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> - {{ 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 - 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 - :href="person.phoneNumber ? 'tel:' + person.phoneNumber : ''" - > - <v-list-item-icon> - <v-icon> mdi-phone-outline</v-icon> - </v-list-item-icon> - - <v-list-item-content> - <v-list-item-title> - {{ person.phoneNumber || "–" }} - </v-list-item-title> - <v-list-item-subtitle> - {{ $t("person.home") }} - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - - <v-list-item - :href=" - person.mobileNumber ? 'tel:' + person.mobileNumber : '' - " - > - <v-list-item-action></v-list-item-action> - - <v-list-item-content> - <v-list-item-title> - {{ person.mobileNumber || "–" }} - </v-list-item-title> - <v-list-item-subtitle> - {{ $t("person.mobile") }} - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - <v-divider inset /> - - <v-list-item - :href="person.email ? 'mailto:' + person.email : ''" - > - <v-list-item-icon> - <v-icon>mdi-email-outline</v-icon> - </v-list-item-icon> - - <v-list-item-content> - <v-list-item-title> - {{ person.email || "–" }} - </v-list-item-title> - <v-list-item-subtitle> - {{ $t("person.email_address") }} - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - <v-divider inset /> - - <v-list-item> - <v-list-item-icon> - <v-icon> mdi-cake-variant-outline</v-icon> - </v-list-item-icon> - - <v-list-item-content> - <v-list-item-title> - <span v-if="person.dateOfBirth && person.placeOfBirth"> - {{ - $t("person.birth_date_and_birth_place_formatted", { - date: $d( - $parseISODate(person.dateOfBirth), - "short", - ), - place: person.placeOfBirth, - }) - }} - </span> - <span v-else-if="person.dateOfBirth">{{ - $d($parseISODate(person.dateOfBirth), "short") - }}</span> - <span v-else-if="person.placeOfBirth">{{ - person.placeOfBirth - }}</span> - <span v-else>–</span> - </v-list-item-title> - <v-list-item-subtitle> - <span v-if="!person.dateOfBirth === !person.placeOfBirth"> - {{ $t("person.birth_date_and_birth_place") }} - </span> - <span v-else-if="person.dateOfBirth"> - {{ $t("person.birth_date") }} - </span> - <span v-else-if="person.placeOfBirth"> - {{ $t("person.birth_place") }} - </span> - </v-list-item-subtitle> - </v-list-item-content> - </v-list-item> - </v-list> - </v-card> + <person-details-card class="mb-6" :person="person" /> <additional-image :src="person.secondaryImageUrl" /> </v-col> @@ -315,6 +138,7 @@ import ObjectOverview from "../generic/ObjectOverview.vue"; import PersonActions from "./PersonActions.vue"; import PersonAvatarClickbox from "./PersonAvatarClickbox.vue"; import PersonCollection from "./PersonCollection.vue"; +import PersonDetailsCard from "./PersonDetailsCard.vue"; import gqlPersonOverview from "./personOverview.graphql"; @@ -329,6 +153,7 @@ export default { PersonActions, PersonAvatarClickbox, PersonCollection, + PersonDetailsCard, }, data() { return { diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 3a84d1ec9..a48e487bc 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -315,13 +315,6 @@ class PersonType(PermissionsTypeMixin, DjangoFilterMixin, DjangoObjectType): return info.context.user.has_perm("core.delete_person_rule", root) -class AddressInputType(graphene.InputObjectType): - street = graphene.String(required=False) - housenumber = graphene.String(required=False) - postal_code = graphene.String(required=False) - place = graphene.String(required=False) - - class PersonInputType(graphene.InputObjectType): id = graphene.ID(required=False) # noqa @@ -344,7 +337,11 @@ class PersonInputType(graphene.InputObjectType): place_of_birth = graphene.String(required=False) sex = graphene.String(required=False) - address = graphene.Field(AddressInputType, required=False) + street = graphene.String(required=False) + housenumber = graphene.String(required=False) + postal_code = graphene.String(required=False) + place = graphene.String(required=False) + country = graphene.String(required=False) photo = Upload() avatar = Upload() @@ -398,7 +395,7 @@ class PersonAddressMutationMixin: ADDRESS_FIELDS = ["street", "housenumber", "postal_code", "place", "country"] @classmethod - def _handle_address(cls, root, info, input, obj, full_input): # noqa: A002 + def _handle_address(cls, root, info, input, obj): # noqa: A002 """Handle and save address input.""" address_type = AddressType.get_default() @@ -483,8 +480,8 @@ class PersonBatchCreateMutation( @classmethod def after_create_obj(cls, root, info, input, obj, full_input): # noqa: A002 super().after_create_obj(root, info, input, obj, full_input) - cls._handle_address(root, info, input, obj, full_input) cls._handle_guardians(root, info, input, obj, full_input) + cls._handle_address(root, info, input, obj) class PersonBatchPatchMutation( @@ -570,8 +567,8 @@ class PersonBatchPatchMutation( "User not allowed to edit the given fields for own person." ) pass - cls._handle_address(root, info, input, obj, full_input) cls._handle_guardians(root, info, input, obj, full_input) + cls._handle_address(root, info, input, obj) class AccountRegistrationInputType(graphene.InputObjectType): @@ -582,14 +579,15 @@ class AccountRegistrationInputType(graphene.InputObjectType): user = graphene.Field(UserInputType, required=True) -class SendAccountRegistrationMutation(graphene.Mutation): +class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutation): class Arguments: account_registration = AccountRegistrationInputType(required=True) ok = graphene.Boolean() + @classmethod @transaction.atomic - def mutate(self, info, account_registration: AccountRegistrationInputType, **kwargs): + def mutate(cls, root, info, account_registration: AccountRegistrationInputType): # Create user try: user = get_user_model().objects.create_user( @@ -619,14 +617,6 @@ class SendAccountRegistrationMutation(graphene.Mutation): # Store contact information in database for field in Person._meta.get_fields(): if ( - field.name in ["street", "housenumber", "postal_code", "place"] - and "address" in account_registration["person"] - and field.name in account_registration["person"]["address"] - and account_registration["person"]["address"][field.name] is not None - and account_registration["person"]["address"][field.name] != "" - ): - setattr(person, field.name, account_registration["person"]["address"][field.name]) - elif ( field.name in account_registration["person"] and account_registration["person"][field.name] is not None and account_registration["person"][field.name] != "" @@ -634,6 +624,9 @@ class SendAccountRegistrationMutation(graphene.Mutation): setattr(person, field.name, account_registration["person"][field.name]) person.save() + # Store address information + cls._handle_address(root, info, account_registration["person"], person) + # Accept invitation, if exists invitation_code = info.context.session.get("invitation_code") if invitation_code: -- GitLab From 7cbdaa615c34d73f6208b2a2c4dfbea06e70e72b Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 20 Mar 2025 23:26:00 +0100 Subject: [PATCH 21/82] Implement proper handling of mail addresses in backend --- aleksis/core/schema/person.py | 43 +++++++++++++++++++++++++++++++---- 1 file changed, 38 insertions(+), 5 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index a48e487bc..6e04349a0 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,5 +1,6 @@ from typing import Union +from django.apps import apps from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied, SuspiciousOperation, ValidationError from django.db import IntegrityError, transaction @@ -23,7 +24,7 @@ from ..models import ( PersonInvitation, PersonRelationship, Role, -) +from ..util.apps import AppConfig from ..util.core_helpers import get_site_preferences, has_person from .address import AddressType as GraphQLAddressType from .base import ( @@ -38,6 +39,9 @@ from .base import ( from .group import PersonGroupThroughType from .notification import NotificationType +if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)]: + from aleksis.apps.postbuero.models import MailAddress, MailDomain + class PersonPreferencesType(graphene.ObjectType): theme_design_mode = graphene.String() @@ -574,7 +578,11 @@ class PersonBatchPatchMutation( class AccountRegistrationInputType(graphene.InputObjectType): from .user import UserInputType # noqa - # email = graphene.Field(MailAddressInputType, required=False) + if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)]: + from aleksis.apps.postbuero.schema import MailAddressInputType + + email = graphene.Field(MailAddressInputType, required=False) + person = graphene.Field(PersonInputType, required=True) user = graphene.Field(UserInputType, required=True) @@ -588,11 +596,31 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat @classmethod @transaction.atomic def mutate(cls, root, info, account_registration: AccountRegistrationInputType): + # Create email + email = None + + if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: + try: + domain = MailDomain.objects.get(pk=account_registration["email"]["domain"]) + except IntegrityError: + raise ValidationError(_("Mail domain does not exist.")) + try: + _mail_address = MailAddress.objects.create( + local_part=account_registration["email"]["local_part"], + domain=domain, + ) + except IntegrityError: + raise ValidationError(_("Mail address already in use.")) + + email = str(_mail_address) + elif account_registration["user"] is not None: + email = account_registration["user"]["email"] + # Create user try: user = get_user_model().objects.create_user( username=account_registration["user"]["username"], - email=account_registration["user"]["email"], + email=email, password=account_registration["user"]["password"], ) except IntegrityError: @@ -603,7 +631,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat person, created = Person.objects.get_or_create( user=user, defaults={ - "email": account_registration["user"]["email"], + "email": email, "first_name": account_registration["person"]["first_name"], "last_name": account_registration["person"]["last_name"], }, @@ -611,7 +639,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat except IntegrityError: raise ValidationError( _("A person using the e-mail address %s already exists.") - % account_registration["user"]["email"] + % email ) # Store contact information in database @@ -627,6 +655,11 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Store address information cls._handle_address(root, info, account_registration["person"], person) + # Link person to postbuero mail address, if created + if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: + _mail_address.person = person + _mail_address.save() + # Accept invitation, if exists invitation_code = info.context.session.get("invitation_code") if invitation_code: -- GitLab From 1f4e4bbff1a18ccef6969d59a78920c6384e5305 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Tue, 25 Mar 2025 23:48:49 +0100 Subject: [PATCH 22/82] Add country field to account wizard --- .../account/AccountRegistrationForm.vue | 24 +++++++++++++++++-- aleksis/core/frontend/messages/en.json | 3 +++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index f84bfc3f8..b2c2b75ab 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -3,6 +3,7 @@ import ControlRow from "../generic/multi_step/ControlRow.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 PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; @@ -272,7 +273,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> <v-row> - <v-col cols="12" lg="6"> + <v-col cols="12" lg="4"> <div :aria-required="isFieldRequired('postal_code')"> <v-text-field outlined @@ -291,7 +292,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col cols="12" lg="6"> + <v-col cols="12" lg="4"> <div :aria-required="isFieldRequired('place')"> <v-text-field outlined @@ -310,6 +311,25 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> + <v-col cols="12" lg="4"> + <div :aria-required="isFieldRequired('country')"> + <country-field + outlined + v-model="data.person.country" + :label=" + $t( + 'accounts.signup.form.steps.address_data.fields.country.label', + ) + " + required + :rules=" + isFieldRequired('country') + ? $rules().required.build() + : [] + " + /> + </div> + </v-col> </v-row> </v-form> </div> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 9f803575b..970256ff3 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -110,6 +110,9 @@ }, "place": { "label": "Place" + }, + "country": { + "label": "Country" } } }, -- GitLab From 6c108b934dcfbc3b0cfb08e621a7467e110a6450 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 26 Mar 2025 00:05:04 +0100 Subject: [PATCH 23/82] Add preference for required fields of address model in account registration wizard --- .../account/AccountRegistrationForm.vue | 4 +++- .../components/account/helpers.graphql | 1 + aleksis/core/preferences.py | 22 ++++++++++++++++++- aleksis/core/schema/site_preferences.py | 4 ++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index b2c2b75ab..6df9a9951 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -574,7 +574,9 @@ export default { } }, isFieldRequired(fieldName) { - return this?.systemProperties?.sitePreferences?.signupRequiredFields.includes( + return this?.systemProperties?.sitePreferences?.signupRequiredFields?.includes( + fieldName, + ) || this?.systemProperties?.sitePreferences?.signupAddressRequiredFields?.includes( fieldName, ); }, diff --git a/aleksis/core/frontend/components/account/helpers.graphql b/aleksis/core/frontend/components/account/helpers.graphql index 8a076250e..9970d1691 100644 --- a/aleksis/core/frontend/components/account/helpers.graphql +++ b/aleksis/core/frontend/components/account/helpers.graphql @@ -2,6 +2,7 @@ query gqlRequiredFieldsPreference { systemProperties { sitePreferences { signupRequiredFields + signupAddressRequiredFields } } } diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index b945a7951..5270da348 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -18,7 +18,7 @@ from dynamic_preferences.types import ( from oauth2_provider.models import AbstractApplication from .mixins import CalendarEventMixin, PublicFilePreferenceMixin -from .models import Group, Person, Role +from .models import Address, Group, Person, Role from .registries import person_preferences_registry, site_preferences_registry from .util.notifications import get_notification_choices_lazy @@ -324,6 +324,26 @@ class SignupRequiredFields(MultipleChoicePreference): required = False +@site_preferences_registry.register +class SignupAddressRequiredFields(MultipleChoicePreference): + """Fields on address model which are required when signing up.""" + + section = auth + name = "signup_address_required_fields" + default = [] + widget = SelectMultiple + verbose_name = _( + "Fields on address model which are required when signing up." + ) + field_attribute = {"initial": []} + choices = [ + (field.name, field.name) + for field in Address.syncable_fields() + if getattr(field, "blank", False) + ] + required = False + + @site_preferences_registry.register class AllowedUsernameRegex(StringPreference): section = auth diff --git a/aleksis/core/schema/site_preferences.py b/aleksis/core/schema/site_preferences.py index bd25aedc0..83d4372d5 100644 --- a/aleksis/core/schema/site_preferences.py +++ b/aleksis/core/schema/site_preferences.py @@ -26,6 +26,7 @@ class SitePreferencesType(graphene.ObjectType): invite_enabled = graphene.Boolean() signup_enabled = graphene.Boolean() signup_required_fields = graphene.List(graphene.String) + signup_address_required_fields = graphene.List(graphene.String) auth_allowed_username_regex = graphene.String() auth_disallowed_uids = graphene.List(graphene.String) @@ -80,3 +81,6 @@ class SitePreferencesType(graphene.ObjectType): def resolve_signup_required_fields(parent, info, **kwargs): return parent["auth__signup_required_fields"] + + def resolve_signup_address_required_fields(parent, info, **kwargs): + return parent["auth__signup_address_required_fields"] -- GitLab From 21aed7c58b4e8e538af9bae6d55b23e7732706b2 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 26 Mar 2025 01:06:54 +0100 Subject: [PATCH 24/82] Complete account wizard summary --- .../account/AccountRegistrationForm.vue | 23 +++++++++++++-- .../components/person/PersonDetailsCard.vue | 28 ++++++++++++++++++- aleksis/core/frontend/messages/en.json | 3 +- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 6df9a9951..340644118 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -520,7 +520,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> - <person-details-card class="mb-4" :person="personDataForSummary" /> + <person-details-card class="mb-4" :person="personDataForSummary" :show-username="true" title-key="accounts.signup.form.steps.confirm.card_title" /> <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" @@ -644,9 +644,26 @@ export default { if (!this.data.email.localPart && !this.data.email.domain) { const { email, ...filteredData } = data; data = filteredData; + } else { + data = { + ...data, + email: { + localPart: data.email.localPart, + domain: data.email.domain.id, + }, + }; } return data; }, + currentEmail() { + if (this.emailMode === 0 && this.data.email.localPart && this.data.email.domain) { + return `${this.data.email.localPart}@${this.data.email.domain.domain}`; + } else if (this.data.user.email) { + return this.data.user.email; + } else { + return null; + } + }, personDataForSummary() { return { ...this.data.person, @@ -659,6 +676,8 @@ export default { country: this.data.person.country, }, ], + username: this.data.user.username, + email: this.currentEmail, } }, steps() { @@ -715,7 +734,7 @@ export default { data: { email: { localPart: "", - domain: "", + domain: {}, }, person: { firstName: "", diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index c7aeaef77..b0468cca2 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -1,6 +1,6 @@ <template> <v-card v-bind="$attrs"> - <v-card-title>{{ $t("person.details") }}</v-card-title> + <v-card-title>{{ $t(titleKey) }}</v-card-title> <v-list two-line> <v-list-item> @@ -21,6 +21,22 @@ </v-list-item> <v-divider inset /> + <v-list-item v-if="showUsername"> + <v-list-item-icon> + <v-icon> mdi-login-variant</v-icon> + </v-list-item-icon> + + <v-list-item-content> + <v-list-item-title> + {{ person.username }} + </v-list-item-title> + <v-list-item-subtitle> + {{ $t("person.username") }} + </v-list-item-subtitle> + </v-list-item-content> + </v-list-item> + <v-divider inset /> + <v-list-item> <v-list-item-icon> <v-icon> mdi-human-non-binary</v-icon> @@ -186,6 +202,16 @@ export default { type: Object, required: true, }, + showUsername: { + type: Boolean, + required: false, + default: false, + }, + titleKey: { + type: String, + required: false, + default: "person.details", + }, }, } </script> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 970256ff3..c5dcea2a6 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -148,7 +148,8 @@ } }, "confirm": { - "title": "Confirm" + "title": "Confirm account registration", + "card_title": "Your account data" } }, "rules": { -- GitLab From f811396321f6c414c868143a8909801c78172873 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 26 Mar 2025 01:51:29 +0100 Subject: [PATCH 25/82] WIP: Implement permissions --- aleksis/core/rules.py | 2 +- aleksis/core/schema/person.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index c1bde1498..405797868 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -402,7 +402,7 @@ edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_defau rules.add_perm("core.edit_default_dashboard_rule", edit_default_dashboard_predicate) # django-allauth -signup_predicate = is_site_preference_set(section="auth", pref="signup_enabled") +signup_predicate = is_site_preference_set(section="auth", pref="signup_enabled") | (is_site_preference_set(section="auth", pref="signup_enabled") & is_invitation_code_in_session ) rules.add_perm("core.signup_rule", signup_predicate) change_password_predicate = has_person & is_site_preference_set( diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 6e04349a0..09fdc0a09 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -596,6 +596,11 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat @classmethod @transaction.atomic def mutate(cls, root, info, account_registration: AccountRegistrationInputType): + invitation_code = info.context.session.get("invitation_code") + + if not get_site_preferences()["auth__signup_enabled"] and not (get_site_preferences()["auth__invite_enabled"] and invitation_code): + raise PermissionDenied(_("Signup is not enabled.")) + # Create email email = None @@ -661,7 +666,6 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat _mail_address.save() # Accept invitation, if exists - invitation_code = info.context.session.get("invitation_code") if invitation_code: from invitations.views import accept_invitation # noqa -- GitLab From c2a92a1aa4858afcae92e2dcae13ca9374b9bbd9 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 27 Mar 2025 00:21:59 +0100 Subject: [PATCH 26/82] Implement invitations in account registration component --- .../account/AccountRegistrationForm.vue | 134 +++++++++++++++--- .../components/account/helpers.graphql | 9 ++ .../generic/multi_step/ControlRow.vue | 6 +- aleksis/core/frontend/messages/en.json | 15 ++ aleksis/core/frontend/mixins/permissions.js | 9 +- aleksis/core/frontend/routes.js | 1 + aleksis/core/models.py | 1 + aleksis/core/rules.py | 2 +- aleksis/core/schema/__init__.py | 11 ++ aleksis/core/schema/person.py | 84 ++++++----- aleksis/core/schema/person_invitation.py | 28 ++++ aleksis/core/schema/user.py | 2 - 12 files changed, 237 insertions(+), 65 deletions(-) create mode 100644 aleksis/core/schema/person_invitation.py diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 340644118..2103a7045 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -47,7 +47,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> </v-alert> - <v-stepper v-model="step" class="mb-4"> + <v-stepper v-model="step" class="mb-4" v-if="isPermissionFetched('core.invite_enabled')"> <v-stepper-header> <template v-for="(stepChoice, index) in steps"> <v-stepper-step @@ -62,6 +62,52 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </template> </v-stepper-header> <v-stepper-items> + <v-stepper-content + v-if="isStepEnabled('invitation')" + :step="getStepIndex('invitation')" + > + <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("invitation")) }}</h2> + <div class="mb-4"> + <v-form v-model="validationStatuses['invitation']"> + <div :aria-required="invitationCodeRequired"> + <v-text-field + outlined + v-model="data.invitationCode" + :label=" + $t( + 'accounts.signup.form.steps.invitation.fields.invitation_code.label', + ) + " + :hint=" + $t( + 'accounts.signup.form.steps.invitation.fields.invitation_code.help_text', + ) + " + persistent-hint + required + :rules=" + invitationCodeRequired + ? $rules().required.build() + : [] + " + ></v-text-field> + </div> + </v-form> + <v-alert v-if="invitationCodeInvalid" type="error" outlined class="mt-4">{{ + $t("accounts.signup.form.steps.invitation.not_valid") + }}</v-alert> + </div> + <v-divider class="mb-4" /> + <control-row + :step="step" + @set-step="checkInvitationCode" + :next-i18n-key="invitationNextI18nKey" + :next-disabled=" + !validationStatuses['invitation'] + " + /> + </v-stepper-content> + <v-stepper-content v-if="isStepEnabled('email')" :step="getStepIndex('email')" @@ -520,6 +566,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> + <!-- TODO: this should somehow also indicate whether an invitation code was used --> <person-details-card class="mb-4" :person="personDataForSummary" :show-username="true" title-key="accounts.signup.form.steps.confirm.card_title" /> <ApolloMutation @@ -550,10 +597,11 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </template> <script> -import { gqlRequiredFieldsPreference } from "./helpers.graphql"; +import { gqlRequiredFieldsPreference, gqlPersonInvitationByCode } from "./helpers.graphql"; import { collections } from "aleksisAppImporter"; import formRulesMixin from "../../mixins/formRulesMixin"; +import permissionsMixin from "../../mixins/permissions"; export default { name: "AccountRegistrationForm", @@ -561,13 +609,35 @@ export default { systemProperties: { query: gqlRequiredFieldsPreference, }, + personInvitationByCode: { + query: gqlPersonInvitationByCode, + variables() { + return { + code: this.data.invitationCode, + }; + }, + skip: true, + }, }, - mixins: [formRulesMixin], + mixins: [formRulesMixin, permissionsMixin], methods: { setStep(step) { this.step = step; this.valid = false; }, + checkInvitationCode(step) { + this.invitationCodeInvalid = false; + this.$apollo.queries.personInvitationByCode.skip = false; + this.$apollo.queries.personInvitationByCode.options.result = ({ data, loading, networkStatus }) => { + if (data?.personInvitationByCode?.valid) { + this.invitation = data.personInvitationByCode; + this.setStep(step); + } else { + this.invitationCodeInvalid = true; + } + }; + this.$apollo.queries.personInvitationByCode.refetch(); + }, accountRegistrationDone({ data }) { if (data.sendAccountRegistration.ok) { this.accountRegistrationSent = true; @@ -682,9 +752,17 @@ export default { }, steps() { return [ + ...(this.checkPermission("core.invite_enabled") + ? [ + { + name: "invitation", + titleKey: "accounts.signup.form.steps.invitation.title", + }, + ] + : []), ...(this.collectionSteps.some( (s) => s.key === "postbuero-mail-address-form-step", - ) + ) && !this.invitation?.hasEmail ? [ { name: "email", @@ -696,22 +774,26 @@ export default { name: "account", titleKey: "accounts.signup.form.steps.account.title", }, - { - name: "base_data", - titleKey: "accounts.signup.form.steps.base_data.title", - }, - { - name: "address_data", - titleKey: "accounts.signup.form.steps.address_data.title", - }, - { - name: "contact_data", - titleKey: "accounts.signup.form.steps.contact_data.title", - }, - { - name: "additional_data", - titleKey: "accounts.signup.form.steps.additional_data.title", - }, + ...(!this.invitation?.hasPerson + ? [ + { + name: "base_data", + titleKey: "accounts.signup.form.steps.base_data.title", + }, + { + name: "address_data", + titleKey: "accounts.signup.form.steps.address_data.title", + }, + { + name: "contact_data", + titleKey: "accounts.signup.form.steps.contact_data.title", + }, + { + name: "additional_data", + titleKey: "accounts.signup.form.steps.additional_data.title", + }, + ] + : []), { name: "confirm", titleKey: "accounts.signup.form.steps.confirm.title", @@ -724,10 +806,18 @@ export default { } return []; }, + invitationNextI18nKey() { + return this.data.invitationCode ? "accounts.signup.form.steps.invitation.next.with_code" : this.invitationCodeRequired ? "accounts.signup.form.steps.invitation.next.code_required" : "accounts.signup.form.steps.invitation.next.without_code"; + }, + invitationCodeRequired() { + return this.checkPermission("core.invite_enabled") && !this.checkPermission("core.signup_rule"); + }, }, data() { return { validationStatuses: {}, + invitation: null, + invitationCodeInvalid: false, accountRegistrationSent: false, step: 1, emailMode: null, @@ -761,6 +851,7 @@ export default { password: "", confirmPassword: "", }, + invitationCode: "", }, }; }, @@ -770,6 +861,9 @@ export default { comp.$el.scrollIntoView(); }, }, + mounted() { + this.addPermissions(["core.signup_rule", "core.invite_enabled"]); + }, }; </script> diff --git a/aleksis/core/frontend/components/account/helpers.graphql b/aleksis/core/frontend/components/account/helpers.graphql index 9970d1691..ee0eaf997 100644 --- a/aleksis/core/frontend/components/account/helpers.graphql +++ b/aleksis/core/frontend/components/account/helpers.graphql @@ -6,3 +6,12 @@ query gqlRequiredFieldsPreference { } } } + +query gqlPersonInvitationByCode($code: String!) { + personInvitationByCode(code: $code) { + id + valid + hasEmail + hasPerson + } +} diff --git a/aleksis/core/frontend/components/generic/multi_step/ControlRow.vue b/aleksis/core/frontend/components/generic/multi_step/ControlRow.vue index fa81f4c41..ab90a1e4b 100644 --- a/aleksis/core/frontend/components/generic/multi_step/ControlRow.vue +++ b/aleksis/core/frontend/components/generic/multi_step/ControlRow.vue @@ -27,7 +27,7 @@ :loading="nextLoading" @click="$emit('set-step', step + 1)" > - {{ $t("actions.next") }} + {{ $t(nextI18nKey) }} <v-icon right>mdi-chevron-right</v-icon> </v-btn> </div> @@ -53,6 +53,10 @@ export default { type: Boolean, default: false, }, + nextI18nKey: { + type: String, + default: "actions.next", + }, }, }; </script> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index c5dcea2a6..f530a924e 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -48,6 +48,21 @@ }, "existing_account_alert": "Already have an account? Then please log in.", "steps": { + "invitation": { + "title": "Invitation code", + "next": { + "with_code": "Continue with code", + "without_code": "Continue without code", + "code_required": "Invitation code required" + }, + "not_valid": "The invitation code you entered is not valid.", + "fields": { + "invitation_code": { + "label": "Invitation code", + "help_text": "If you have an invitation code, please enter it." + } + } + }, "email": { "title": "E-Mail address", "choose_mode": { diff --git a/aleksis/core/frontend/mixins/permissions.js b/aleksis/core/frontend/mixins/permissions.js index 2dd7a462d..3156b5d47 100644 --- a/aleksis/core/frontend/mixins/permissions.js +++ b/aleksis/core/frontend/mixins/permissions.js @@ -4,10 +4,15 @@ const permissionsMixin = { methods: { - checkPermission(permissionName) { + isPermissionFetched(permissionName) { return ( this.$root.permissions && - this.$root.permissions.find((p) => p.name === permissionName) && + this.$root.permissions.find((p) => p.name === permissionName) + ); + }, + checkPermission(permissionName) { + return ( + this.isPermissionFetched(permissionName) && this.$root.permissions.find((p) => p.name === permissionName).result ); }, diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index 69f347662..e7f1d1e25 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -30,6 +30,7 @@ const routes = [ invalidate: "leave", }, }, + // TODO: Use rule checking (maybe) and add invitation code to URL { path: "/accounts/signup/", name: "core.accounts.signup", diff --git a/aleksis/core/models.py b/aleksis/core/models.py index d1641fa09..bcaef14e5 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1436,6 +1436,7 @@ class PersonInvitation(AbstractBaseInvitation, PureDjangoModel): def send_invitation(self, request, **kwargs): """Send the invitation email to the person.""" + # TODO: Use correct URL to new signup wizard invite_url = reverse("invitations:accept-invite", args=[self.key]) invite_url = request.build_absolute_uri(invite_url).replace("/django", "") context = kwargs diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index 405797868..c1bde1498 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -402,7 +402,7 @@ edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_defau rules.add_perm("core.edit_default_dashboard_rule", edit_default_dashboard_predicate) # django-allauth -signup_predicate = is_site_preference_set(section="auth", pref="signup_enabled") | (is_site_preference_set(section="auth", pref="signup_enabled") & is_invitation_code_in_session ) +signup_predicate = is_site_preference_set(section="auth", pref="signup_enabled") rules.add_perm("core.signup_rule", signup_predicate) change_password_predicate = has_person & is_site_preference_set( diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 5b6fdf711..b515c321f 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -26,6 +26,7 @@ from ..models import ( OAuthApplication, PDFFile, Person, + PersonInvitation, Role, Room, SchoolTerm, @@ -89,6 +90,7 @@ from .person import ( PersonType, SendAccountRegistrationMutation, ) +from .person_invitation import PersonInvitationType from .personal_event import ( PersonalEventBatchCreateMutation, PersonalEventBatchDeleteMutation, @@ -193,6 +195,8 @@ class Query(graphene.ObjectType): countries = graphene.List(CountryType) + person_invitation_by_code = graphene.Field(PersonInvitationType, code=graphene.String()) + def resolve_ping(root, info, payload) -> str: return payload @@ -474,6 +478,13 @@ class Query(graphene.ObjectType): def resolve_countries(root, info, **kwargs): return countries + @staticmethod + def resolve_person_invitation_by_code(root, info, code, **kwargs): + formatted_code = "".join(code.lower().split("-")) + if PersonInvitation.objects.filter(key=formatted_code).exists(): + return PersonInvitation.objects.get(key=formatted_code) + return None + class Mutation(graphene.ObjectType): delete_persons = PersonBatchDeleteMutation.Field() diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 09fdc0a09..1998ac20f 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -12,6 +12,7 @@ import graphene import graphene_django_optimizer from graphene_django import DjangoObjectType from graphene_file_upload.scalars import Upload +from invitations.views import accept_invitation from ..filters import PersonFilter from ..models import ( @@ -585,6 +586,7 @@ class AccountRegistrationInputType(graphene.InputObjectType): person = graphene.Field(PersonInputType, required=True) user = graphene.Field(UserInputType, required=True) + invitation_code = graphene.String(required=False) class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutation): @@ -596,15 +598,21 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat @classmethod @transaction.atomic def mutate(cls, root, info, account_registration: AccountRegistrationInputType): - invitation_code = info.context.session.get("invitation_code") - - if not get_site_preferences()["auth__signup_enabled"] and not (get_site_preferences()["auth__invite_enabled"] and invitation_code): + if account_registration["invitation_code"]: + try: + invitation = PersonInvitation.objects.get(key=account_registration["invitation_code"]) + except PersonInvitation.DoesNotExist as exc: + raise SuspiciousOperation from exc + + if not get_site_preferences()["auth__signup_enabled"] and not (get_site_preferences()["auth__invite_enabled"] and invitation): raise PermissionDenied(_("Signup is not enabled.")) # Create email email = None - if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: + if invitation and invitation.email: + email = invitation.email + elif "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: try: domain = MailDomain.objects.get(pk=account_registration["email"]["domain"]) except IntegrityError: @@ -631,49 +639,47 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat except IntegrityError: raise ValidationError(_("A user with this username or e-mail already exists.")) - # Create person - try: - person, created = Person.objects.get_or_create( - user=user, - defaults={ - "email": email, - "first_name": account_registration["person"]["first_name"], - "last_name": account_registration["person"]["last_name"], - }, - ) - except IntegrityError: - raise ValidationError( - _("A person using the e-mail address %s already exists.") - % email - ) + # Create person if no invitation is given or if invitation isn't linked to a person + if invitation and invitation.person: + person = invitation.person + person.user = user + person.save() + else: + try: + person, created = Person.objects.get_or_create( + user=user, + defaults={ + "email": email, + "first_name": account_registration["person"]["first_name"], + "last_name": account_registration["person"]["last_name"], + }, + ) + except IntegrityError: + raise ValidationError( + _("A person using the e-mail address %s already exists.") + % email + ) - # Store contact information in database - for field in Person._meta.get_fields(): - if ( - field.name in account_registration["person"] - and account_registration["person"][field.name] is not None - and account_registration["person"][field.name] != "" - ): - setattr(person, field.name, account_registration["person"][field.name]) - person.save() + # Store contact information in database + for field in Person._meta.get_fields(): + if ( + field.name in account_registration["person"] + and account_registration["person"][field.name] is not None + and account_registration["person"][field.name] != "" + ): + setattr(person, field.name, account_registration["person"][field.name]) + person.save() - # Store address information - cls._handle_address(root, info, account_registration["person"], person) + # Store address information + cls._handle_address(root, info, account_registration["person"], person) # Link person to postbuero mail address, if created - if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: + if _mail_address: _mail_address.person = person _mail_address.save() # Accept invitation, if exists - if invitation_code: - from invitations.views import accept_invitation # noqa - - try: - invitation = PersonInvitation.objects.get(key=invitation_code) - except PersonInvitation.DoesNotExist as exc: - raise SuspiciousOperation from exc - + if invitation: accept_invitation(invitation, info.context, info.context.user) _act = Activity( diff --git a/aleksis/core/schema/person_invitation.py b/aleksis/core/schema/person_invitation.py new file mode 100644 index 000000000..a6abf77ea --- /dev/null +++ b/aleksis/core/schema/person_invitation.py @@ -0,0 +1,28 @@ +import graphene +from graphene_django import DjangoObjectType + +from ..models import PersonInvitation + + +class PersonInvitationType(DjangoObjectType): + class Meta: + model = PersonInvitation + fields = [ + "id", + ] + + valid = graphene.Boolean() + has_email = graphene.Boolean() + has_person = graphene.Boolean() + + @staticmethod + def resolve_valid(root, info, **kwargs): + return not root.accepted and not root.key_expired() + + @staticmethod + def resolve_has_email(root, info, **kwargs): + return bool(root.email) + + @staticmethod + def resolve_has_person(root, info, **kwargs): + return bool(root.person) diff --git a/aleksis/core/schema/user.py b/aleksis/core/schema/user.py index 13fbbc4e6..c3d875c92 100644 --- a/aleksis/core/schema/user.py +++ b/aleksis/core/schema/user.py @@ -21,8 +21,6 @@ class UserType(graphene.ObjectType): ) def resolve_global_permissions_by_name(root, info, permissions, **kwargs): - if root.is_anonymous: - return [{"name": permission_name, "result": False} for permission_name in permissions] return [ {"name": permission_name, "result": info.context.user.has_perm(permission_name)} for permission_name in permissions -- GitLab From 8ceee7c89ccbd075c6b8979c8c783de6db7f40ba Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:16:35 +0100 Subject: [PATCH 27/82] Allow autofill of invitation code from URL --- .../components/account/AccountRegistrationForm.vue | 8 ++++++++ aleksis/core/frontend/messages/en.json | 1 + 2 files changed, 9 insertions(+) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 2103a7045..42fa27a90 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -67,6 +67,9 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; :step="getStepIndex('invitation')" > <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("invitation")) }}</h2> + <v-alert v-if="invitationCodeAutofilled" type="info" outlined class="mb-4">{{ + $t("accounts.signup.form.steps.invitation.autofilled") + }}</v-alert> <div class="mb-4"> <v-form v-model="validationStatuses['invitation']"> <div :aria-required="invitationCodeRequired"> @@ -818,6 +821,7 @@ export default { validationStatuses: {}, invitation: null, invitationCodeInvalid: false, + invitationCodeAutofilled: false, accountRegistrationSent: false, step: 1, emailMode: null, @@ -863,6 +867,10 @@ export default { }, mounted() { this.addPermissions(["core.signup_rule", "core.invite_enabled"]); + if (this.$route.query.invitation_code) { + this.data.invitationCode = this.$route.query.invitation_code; + this.invitationCodeAutofilled = true; + } }, }; </script> diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index f530a924e..ed512592d 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -56,6 +56,7 @@ "code_required": "Invitation code required" }, "not_valid": "The invitation code you entered is not valid.", + "autofilled": "The invitation code was automatically filled in.", "fields": { "invitation_code": { "label": "Invitation code", -- GitLab From 984125b32b0811adff879087c25a05e201e1a883 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:27:23 +0100 Subject: [PATCH 28/82] Use correct new URL for invitation mails --- aleksis/core/models.py | 2 +- aleksis/core/urls.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index bcaef14e5..5555739d9 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1437,7 +1437,7 @@ class PersonInvitation(AbstractBaseInvitation, PureDjangoModel): def send_invitation(self, request, **kwargs): """Send the invitation email to the person.""" # TODO: Use correct URL to new signup wizard - invite_url = reverse("invitations:accept-invite", args=[self.key]) + invite_url = f"{reverse("account_signup")}?invitation_code={self.key}" invite_url = request.build_absolute_uri(invite_url).replace("/django", "") context = kwargs context.update( diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index cde100096..e66cf1b7a 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -107,15 +107,15 @@ urlpatterns = [ views.DAVSingleResourceView.as_view(), name="dav_resource_contact", ), + path( + "accounts/signup/", views.TemplateView.as_view(template_name="core/vue_index.html"), name="account_signup" + ), path("", include("django_prometheus.urls")), path( "django/", include( [ path("account/login/", views.LoginView.as_view(), name="login"), - path( - "accounts/signup/", views.AccountRegisterView.as_view(), name="account_signup" - ), path("accounts/logout/", views.CustomLogoutView.as_view(), name="logout"), path( "accounts/password/change/", -- GitLab From 234e2f6ce934181b643a2bc1c1f956d3b8a623d8 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:42:50 +0100 Subject: [PATCH 29/82] Fix email handling --- .../frontend/components/account/AccountRegistrationForm.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 42fa27a90..2dc64b98c 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -714,7 +714,7 @@ export default { user: filteredUserData, }; - if (!this.data.email.localPart && !this.data.email.domain) { + if (!this.data.email.localPart && !this.data.email.domain?.id) { const { email, ...filteredData } = data; data = filteredData; } else { -- GitLab From 412f5672dc68fca8e3865f8ff423c107411a651e Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:53:56 +0100 Subject: [PATCH 30/82] Fix invitation related step behavior --- .../account/AccountRegistrationForm.vue | 137 +++++++++--------- 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 2dc64b98c..58acb2f31 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -29,6 +29,58 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-card-actions> </v-card> </div> + <div v-else-if="checkPermission('core.invite_enabled') && !invitationCodeEntered"> + <v-card> + <v-card-title> + {{ $t("accounts.signup.form.steps.invitation.title") }} + </v-card-title> + <v-card-text> + <v-alert v-if="invitationCodeAutofilled" type="info" outlined class="mb-4">{{ + $t("accounts.signup.form.steps.invitation.autofilled") + }}</v-alert> + <div class="mb-4"> + <v-form v-model="invitationCodeValidationStatus"> + <div :aria-required="invitationCodeRequired"> + <v-text-field + outlined + v-model="data.invitationCode" + :label=" + $t( + 'accounts.signup.form.steps.invitation.fields.invitation_code.label', + ) + " + :hint=" + $t( + 'accounts.signup.form.steps.invitation.fields.invitation_code.help_text', + ) + " + persistent-hint + required + :rules=" + invitationCodeRequired + ? $rules().required.build() + : [] + " + ></v-text-field> + </div> + </v-form> + </div> + <v-alert v-if="invitationCodeInvalid" type="error" outlined class="mb-4">{{ + $t("accounts.signup.form.steps.invitation.not_valid") + }}</v-alert> + </v-card-text> + <v-card-actions> + <v-spacer /> + <primary-action-button + @click="checkInvitationCode" + :i18n-key="invitationNextI18nKey" + :disabled=" + !invitationCodeValidationStatus + " + /> + </v-card-actions> + </v-card> + </div> <div v-else> <v-alert type="info" dense outlined class="mb-4"> <v-row align="center" no-gutters> @@ -62,55 +114,6 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </template> </v-stepper-header> <v-stepper-items> - <v-stepper-content - v-if="isStepEnabled('invitation')" - :step="getStepIndex('invitation')" - > - <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("invitation")) }}</h2> - <v-alert v-if="invitationCodeAutofilled" type="info" outlined class="mb-4">{{ - $t("accounts.signup.form.steps.invitation.autofilled") - }}</v-alert> - <div class="mb-4"> - <v-form v-model="validationStatuses['invitation']"> - <div :aria-required="invitationCodeRequired"> - <v-text-field - outlined - v-model="data.invitationCode" - :label=" - $t( - 'accounts.signup.form.steps.invitation.fields.invitation_code.label', - ) - " - :hint=" - $t( - 'accounts.signup.form.steps.invitation.fields.invitation_code.help_text', - ) - " - persistent-hint - required - :rules=" - invitationCodeRequired - ? $rules().required.build() - : [] - " - ></v-text-field> - </div> - </v-form> - <v-alert v-if="invitationCodeInvalid" type="error" outlined class="mt-4">{{ - $t("accounts.signup.form.steps.invitation.not_valid") - }}</v-alert> - </div> - <v-divider class="mb-4" /> - <control-row - :step="step" - @set-step="checkInvitationCode" - :next-i18n-key="invitationNextI18nKey" - :next-disabled=" - !validationStatuses['invitation'] - " - /> - </v-stepper-content> - <v-stepper-content v-if="isStepEnabled('email')" :step="getStepIndex('email')" @@ -619,6 +622,14 @@ export default { code: this.data.invitationCode, }; }, + result({ data, loading, networkStatus }) { + if (data?.personInvitationByCode?.valid) { + this.invitation = data.personInvitationByCode; + this.invitationCodeEntered = true; + } else { + this.invitationCodeInvalid = true; + } + }, skip: true, }, }, @@ -628,18 +639,14 @@ export default { this.step = step; this.valid = false; }, - checkInvitationCode(step) { + checkInvitationCode() { this.invitationCodeInvalid = false; - this.$apollo.queries.personInvitationByCode.skip = false; - this.$apollo.queries.personInvitationByCode.options.result = ({ data, loading, networkStatus }) => { - if (data?.personInvitationByCode?.valid) { - this.invitation = data.personInvitationByCode; - this.setStep(step); - } else { - this.invitationCodeInvalid = true; - } - }; - this.$apollo.queries.personInvitationByCode.refetch(); + if (this.data.invitationCode) { + this.$apollo.queries.personInvitationByCode.skip = false; + this.$apollo.queries.personInvitationByCode.refetch(); + } else { + this.invitationCodeEntered = true; + } }, accountRegistrationDone({ data }) { if (data.sendAccountRegistration.ok) { @@ -755,14 +762,6 @@ export default { }, steps() { return [ - ...(this.checkPermission("core.invite_enabled") - ? [ - { - name: "invitation", - titleKey: "accounts.signup.form.steps.invitation.title", - }, - ] - : []), ...(this.collectionSteps.some( (s) => s.key === "postbuero-mail-address-form-step", ) && !this.invitation?.hasEmail @@ -820,6 +819,8 @@ export default { return { validationStatuses: {}, invitation: null, + invitationCodeEntered: false, + invitationCodeValidationStatus: false, invitationCodeInvalid: false, invitationCodeAutofilled: false, accountRegistrationSent: false, -- GitLab From 60183db55b6a2c61ed10197cfebafe990a59123c Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:55:43 +0100 Subject: [PATCH 31/82] Fix mutation mail address handling --- aleksis/core/schema/person.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 1998ac20f..a8347fa2e 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -609,6 +609,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Create email email = None + _mail_address = None if invitation and invitation.email: email = invitation.email -- GitLab From 3e77bff983c352e4f9e23d23b7d318dac0dc4885 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 19:59:34 +0100 Subject: [PATCH 32/82] Implement correct rules handling --- aleksis/core/frontend/routes.js | 19 ++----------------- aleksis/core/rules.py | 27 +++++++++++++++------------ 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/aleksis/core/frontend/routes.js b/aleksis/core/frontend/routes.js index e7f1d1e25..155ac0d4f 100644 --- a/aleksis/core/frontend/routes.js +++ b/aleksis/core/frontend/routes.js @@ -30,7 +30,6 @@ const routes = [ invalidate: "leave", }, }, - // TODO: Use rule checking (maybe) and add invitation code to URL { path: "/accounts/signup/", name: "core.accounts.signup", @@ -40,25 +39,11 @@ const routes = [ icon: "mdi-account-plus-outline", iconActive: "mdi-account-plus", titleKey: "accounts.signup.menu_title", - validators: [signupEnabledValidator, notLoggedInValidator], + validators: [notLoggedInValidator], + permission: "core.signup_menu_rule", invalidate: "leave", }, }, - { - path: "/invitations/code/enter/", - name: "core.invitations.enterCode", - component: () => import("./components/LegacyBaseTemplate.vue"), - props: { - byTheGreatnessOfTheAlmightyAleksolotlISwearIAmWorthyOfUsingTheLegacyBaseTemplate: true, - }, - meta: { - inMenu: true, - icon: "mdi-key-outline", - iconActive: "mdi-key-outline", - titleKey: "accounts.invitation.accept_invitation.menu_title", - validators: [inviteEnabledValidator, notLoggedInValidator], - }, - }, { path: "", name: "dashboard", diff --git a/aleksis/core/rules.py b/aleksis/core/rules.py index c1bde1498..2a6dccf8b 100644 --- a/aleksis/core/rules.py +++ b/aleksis/core/rules.py @@ -401,18 +401,6 @@ rules.add_perm("core.edit_dashboard_rule", edit_dashboard_predicate) edit_default_dashboard_predicate = has_person & has_global_perm("core.edit_default_dashboard") rules.add_perm("core.edit_default_dashboard_rule", edit_default_dashboard_predicate) -# django-allauth -signup_predicate = is_site_preference_set(section="auth", pref="signup_enabled") -rules.add_perm("core.signup_rule", signup_predicate) - -change_password_predicate = has_person & is_site_preference_set( - section="auth", pref="allow_password_change" -) -rules.add_perm("core.change_password_rule", change_password_predicate) - -reset_password_predicate = is_site_preference_set(section="auth", pref="allow_password_reset") -rules.add_perm("core.reset_password_rule", reset_password_predicate) - # django-invitations invite_enabled_predicate = is_site_preference_set(section="auth", pref="invite_enabled") rules.add_perm("core.invite_enabled", invite_enabled_predicate) @@ -423,6 +411,21 @@ rules.add_perm("core.accept_invite_rule", accept_invite_predicate) invite_predicate = has_person & invite_enabled_predicate & has_global_perm("core.invite") rules.add_perm("core.invite_rule", invite_predicate) +# django-allauth +signup_enabled_predicate = is_site_preference_set(section="auth", pref="signup_enabled") +rules.add_perm("core.signup_rule", signup_enabled_predicate) + +signup_menu_predicate = signup_enabled_predicate | invite_enabled_predicate +rules.add_perm("core.signup_menu_rule", signup_menu_predicate) + +change_password_predicate = has_person & is_site_preference_set( + section="auth", pref="allow_password_change" +) +rules.add_perm("core.change_password_rule", change_password_predicate) + +reset_password_predicate = is_site_preference_set(section="auth", pref="allow_password_reset") +rules.add_perm("core.reset_password_rule", reset_password_predicate) + # OAuth2 permissions view_oauthapplication_predicate = has_person & has_global_perm("core.view_oauthapplication") rules.add_perm("core.view_oauthapplication_rule", view_oauthapplication_predicate) -- GitLab From 1452558c5636cc6cd4d514b948d3ae738b939d73 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 20:13:49 +0100 Subject: [PATCH 33/82] Add preference for visible fields --- .../account/AccountRegistrationForm.vue | 45 +++++++++++++------ .../components/account/helpers.graphql | 2 + aleksis/core/preferences.py | 40 +++++++++++++++++ aleksis/core/schema/site_preferences.py | 8 ++++ 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 58acb2f31..32eeb01b0 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -233,7 +233,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col> + <v-col v-if="isFieldVisible('additional_name')"> <div :aria-required="isFieldRequired('additional_name')"> <v-text-field outlined @@ -285,7 +285,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <v-form v-model="validationStatuses['address_data']"> <v-row> - <v-col cols="12" lg="6"> + <v-col cols="12" lg="6" v-if="isFieldVisible('street')"> <div :aria-required="isFieldRequired('street')"> <v-text-field outlined @@ -304,7 +304,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col cols="12" lg="6"> + <v-col cols="12" lg="6" v-if="isFieldVisible('housenumber')"> <div :aria-required="isFieldRequired('housenumber')"> <v-text-field outlined @@ -325,7 +325,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> <v-row> - <v-col cols="12" lg="4"> + <v-col cols="12" lg="4" v-if="isFieldVisible('postal_code')"> <div :aria-required="isFieldRequired('postal_code')"> <v-text-field outlined @@ -344,7 +344,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col cols="12" lg="4"> + <v-col cols="12" lg="4" v-if="isFieldVisible('place')"> <div :aria-required="isFieldRequired('place')"> <v-text-field outlined @@ -363,7 +363,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col cols="12" lg="4"> + <v-col cols="12" lg="4" v-if="isFieldVisible('country')"> <div :aria-required="isFieldRequired('country')"> <country-field outlined @@ -400,7 +400,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <v-form v-model="validationStatuses['contact_data']"> <v-row> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('mobile_number')"> <div :aria-required="isFieldRequired('mobile_number')"> <v-text-field outlined @@ -420,7 +420,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ></v-text-field> </div> </v-col> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('phone_number')"> <div :aria-required="isFieldRequired('phone_number')"> <v-text-field outlined @@ -458,7 +458,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <v-form v-model="validationStatuses['additional_data']"> <v-row> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('date_of_birth')"> <div :aria-required="isFieldRequired('date_of_birth')"> <date-field outlined @@ -478,7 +478,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; /> </div> </v-col> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('place_of_birth')"> <div :aria-required="isFieldRequired('place_of_birth')"> <v-text-field outlined @@ -499,7 +499,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> <v-row> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('sex')"> <div :aria-required="isFieldRequired('sex')"> <sex-select outlined @@ -518,7 +518,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; /> </div> </v-col> - <v-col cols="12" md="6"> + <v-col cols="12" md="6" v-if="isFieldVisible('photo')"> <div :aria-required="isFieldRequired('photo')"> <file-field outlined @@ -540,7 +540,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> <v-row> - <v-col cols="12"> + <v-col cols="12" v-if="isFieldVisible('description')"> <div :aria-required="isFieldRequired('description')"> <v-text-field outlined @@ -660,6 +660,13 @@ export default { fieldName, ); }, + isFieldVisible(fieldName) { + return this?.systemProperties?.sitePreferences?.signupVisibleFields?.includes( + fieldName, + ) || this?.systemProperties?.sitePreferences?.signupAddressVisibleFields?.includes( + fieldName, + ); + }, isStepEnabled(stepName) { return this.steps.some((s) => s.name === stepName); }, @@ -782,14 +789,26 @@ export default { name: "base_data", titleKey: "accounts.signup.form.steps.base_data.title", }, + ] + : []), + ...(!this.invitation?.hasPerson && (this.isFieldVisible("street") | this.isFieldVisible("housenumber") | this.isFieldVisible("postal_code") | this.isFieldVisible("place") | this.isFieldVisible("country")) + ? [ { name: "address_data", titleKey: "accounts.signup.form.steps.address_data.title", }, + ] + : []), + ...(!this.invitation?.hasPerson && (this.isFieldVisible("mobile_number") | this.isFieldVisible("phone_number")) + ? [ { name: "contact_data", titleKey: "accounts.signup.form.steps.contact_data.title", }, + ] + : []), + ...(!this.invitation?.hasPerson && (this.isFieldVisible("date_of_birth") | this.isFieldVisible("place_of_birth") | this.isFieldVisible("sex") | this.isFieldVisible("photo") | this.isFieldVisible("description")) + ? [ { name: "additional_data", titleKey: "accounts.signup.form.steps.additional_data.title", diff --git a/aleksis/core/frontend/components/account/helpers.graphql b/aleksis/core/frontend/components/account/helpers.graphql index ee0eaf997..5fb52db46 100644 --- a/aleksis/core/frontend/components/account/helpers.graphql +++ b/aleksis/core/frontend/components/account/helpers.graphql @@ -3,6 +3,8 @@ query gqlRequiredFieldsPreference { sitePreferences { signupRequiredFields signupAddressRequiredFields + signupVisibleFields + signupAddressVisibleFields } } } diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 5270da348..08173de00 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -344,6 +344,46 @@ class SignupAddressRequiredFields(MultipleChoicePreference): required = False +@site_preferences_registry.register +class SignupVisibleFields(MultipleChoicePreference): + """Fields on person model which are visible when signing up.""" + + section = auth + name = "signup_visible_fields" + default = [] + widget = SelectMultiple + verbose_name = _( + "Fields on person model which are visible when signing up. The first and last name fields are always visible." + ) + field_attribute = {"initial": []} + choices = [ + (field.name, field.name) + for field in Person.syncable_fields() + if getattr(field, "blank", False) + ] + required = False + + +@site_preferences_registry.register +class SignupAddressVisibleFields(MultipleChoicePreference): + """Fields on address model which are visible when signing up.""" + + section = auth + name = "signup_address_visible_fields" + default = [] + widget = SelectMultiple + verbose_name = _( + "Fields on address model which are visible when signing up." + ) + field_attribute = {"initial": []} + choices = [ + (field.name, field.name) + for field in Address.syncable_fields() + if getattr(field, "blank", False) + ] + required = False + + @site_preferences_registry.register class AllowedUsernameRegex(StringPreference): section = auth diff --git a/aleksis/core/schema/site_preferences.py b/aleksis/core/schema/site_preferences.py index 83d4372d5..a0b5f56dd 100644 --- a/aleksis/core/schema/site_preferences.py +++ b/aleksis/core/schema/site_preferences.py @@ -27,6 +27,8 @@ class SitePreferencesType(graphene.ObjectType): signup_enabled = graphene.Boolean() signup_required_fields = graphene.List(graphene.String) signup_address_required_fields = graphene.List(graphene.String) + signup_visible_fields = graphene.List(graphene.String) + signup_address_visible_fields = graphene.List(graphene.String) auth_allowed_username_regex = graphene.String() auth_disallowed_uids = graphene.List(graphene.String) @@ -84,3 +86,9 @@ class SitePreferencesType(graphene.ObjectType): def resolve_signup_address_required_fields(parent, info, **kwargs): return parent["auth__signup_address_required_fields"] + + def resolve_signup_visible_fields(parent, info, **kwargs): + return parent["auth__signup_visible_fields"] + + def resolve_signup_address_required_fields(parent, info, **kwargs): + return parent["auth__signup_address_visible_fields"] -- GitLab From 443eec5d58e9cefe939b2f2fd09c9c20bc11e9b0 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 20:16:06 +0100 Subject: [PATCH 34/82] Fix invitation handling in mutation --- aleksis/core/schema/person.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index a8347fa2e..05539c458 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -598,6 +598,8 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat @classmethod @transaction.atomic def mutate(cls, root, info, account_registration: AccountRegistrationInputType): + invitation = None + if account_registration["invitation_code"]: try: invitation = PersonInvitation.objects.get(key=account_registration["invitation_code"]) -- GitLab From 74d035e4f42539466446c689ae4892ccefd282d3 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 20:20:14 +0100 Subject: [PATCH 35/82] Add info alert in case invitation was used --- .../frontend/components/account/AccountRegistrationForm.vue | 4 +++- aleksis/core/frontend/messages/en.json | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 32eeb01b0..a905c47ce 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -572,7 +572,9 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> - <!-- TODO: this should somehow also indicate whether an invitation code was used --> + <v-alert v-if="invitation && (invitation.hasEmail || invitation.hasPerson)" type="info" outlined class="mb-4">{{ + $t("accounts.signup.form.steps.confirm.invitation_used") + }}</v-alert> <person-details-card class="mb-4" :person="personDataForSummary" :show-username="true" title-key="accounts.signup.form.steps.confirm.card_title" /> <ApolloMutation diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index ed512592d..9ef8a9245 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -165,6 +165,7 @@ }, "confirm": { "title": "Confirm account registration", + "invitation_used": "Some personal data will be taken over from the data associated with the invitation you accepted.", "card_title": "Your account data" } }, -- GitLab From 609fa361f0b5e7213823b82489d269bca20ff6aa Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 20:21:45 +0100 Subject: [PATCH 36/82] Reformat --- .../account/AccountRegistrationForm.vue | 145 +++++++++++++----- .../components/person/PersonDetailsCard.vue | 30 +--- aleksis/core/preferences.py | 8 +- aleksis/core/schema/person.py | 19 ++- aleksis/core/urls.py | 4 +- 5 files changed, 133 insertions(+), 73 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index a905c47ce..0b40676c4 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -29,15 +29,25 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-card-actions> </v-card> </div> - <div v-else-if="checkPermission('core.invite_enabled') && !invitationCodeEntered"> + <div + v-else-if=" + checkPermission('core.invite_enabled') && !invitationCodeEntered + " + > <v-card> <v-card-title> {{ $t("accounts.signup.form.steps.invitation.title") }} </v-card-title> <v-card-text> - <v-alert v-if="invitationCodeAutofilled" type="info" outlined class="mb-4">{{ - $t("accounts.signup.form.steps.invitation.autofilled") - }}</v-alert> + <v-alert + v-if="invitationCodeAutofilled" + type="info" + outlined + class="mb-4" + >{{ + $t("accounts.signup.form.steps.invitation.autofilled") + }}</v-alert + > <div class="mb-4"> <v-form v-model="invitationCodeValidationStatus"> <div :aria-required="invitationCodeRequired"> @@ -57,26 +67,28 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; persistent-hint required :rules=" - invitationCodeRequired - ? $rules().required.build() - : [] + invitationCodeRequired ? $rules().required.build() : [] " ></v-text-field> </div> </v-form> </div> - <v-alert v-if="invitationCodeInvalid" type="error" outlined class="mb-4">{{ - $t("accounts.signup.form.steps.invitation.not_valid") - }}</v-alert> + <v-alert + v-if="invitationCodeInvalid" + type="error" + outlined + class="mb-4" + >{{ + $t("accounts.signup.form.steps.invitation.not_valid") + }}</v-alert + > </v-card-text> <v-card-actions> <v-spacer /> <primary-action-button @click="checkInvitationCode" :i18n-key="invitationNextI18nKey" - :disabled=" - !invitationCodeValidationStatus - " + :disabled="!invitationCodeValidationStatus" /> </v-card-actions> </v-card> @@ -99,7 +111,11 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </v-col> </v-row> </v-alert> - <v-stepper v-model="step" class="mb-4" v-if="isPermissionFetched('core.invite_enabled')"> + <v-stepper + v-model="step" + class="mb-4" + v-if="isPermissionFetched('core.invite_enabled')" + > <v-stepper-header> <template v-for="(stepChoice, index) in steps"> <v-stepper-step @@ -400,7 +416,11 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <v-form v-model="validationStatuses['contact_data']"> <v-row> - <v-col cols="12" md="6" v-if="isFieldVisible('mobile_number')"> + <v-col + cols="12" + md="6" + v-if="isFieldVisible('mobile_number')" + > <div :aria-required="isFieldRequired('mobile_number')"> <v-text-field outlined @@ -458,7 +478,11 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <v-form v-model="validationStatuses['additional_data']"> <v-row> - <v-col cols="12" md="6" v-if="isFieldVisible('date_of_birth')"> + <v-col + cols="12" + md="6" + v-if="isFieldVisible('date_of_birth')" + > <div :aria-required="isFieldRequired('date_of_birth')"> <date-field outlined @@ -478,7 +502,11 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; /> </div> </v-col> - <v-col cols="12" md="6" v-if="isFieldVisible('place_of_birth')"> + <v-col + cols="12" + md="6" + v-if="isFieldVisible('place_of_birth')" + > <div :aria-required="isFieldRequired('place_of_birth')"> <v-text-field outlined @@ -572,10 +600,21 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-stepper-content :step="getStepIndex('confirm')"> <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("confirm")) }}</h2> - <v-alert v-if="invitation && (invitation.hasEmail || invitation.hasPerson)" type="info" outlined class="mb-4">{{ - $t("accounts.signup.form.steps.confirm.invitation_used") - }}</v-alert> - <person-details-card class="mb-4" :person="personDataForSummary" :show-username="true" title-key="accounts.signup.form.steps.confirm.card_title" /> + <v-alert + v-if="invitation && (invitation.hasEmail || invitation.hasPerson)" + type="info" + outlined + class="mb-4" + >{{ + $t("accounts.signup.form.steps.confirm.invitation_used") + }}</v-alert + > + <person-details-card + class="mb-4" + :person="personDataForSummary" + :show-username="true" + title-key="accounts.signup.form.steps.confirm.card_title" + /> <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" @@ -605,7 +644,10 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </template> <script> -import { gqlRequiredFieldsPreference, gqlPersonInvitationByCode } from "./helpers.graphql"; +import { + gqlRequiredFieldsPreference, + gqlPersonInvitationByCode, +} from "./helpers.graphql"; import { collections } from "aleksisAppImporter"; import formRulesMixin from "../../mixins/formRulesMixin"; @@ -656,17 +698,23 @@ export default { } }, isFieldRequired(fieldName) { - return this?.systemProperties?.sitePreferences?.signupRequiredFields?.includes( - fieldName, - ) || this?.systemProperties?.sitePreferences?.signupAddressRequiredFields?.includes( - fieldName, + return ( + this?.systemProperties?.sitePreferences?.signupRequiredFields?.includes( + fieldName, + ) || + this?.systemProperties?.sitePreferences?.signupAddressRequiredFields?.includes( + fieldName, + ) ); }, isFieldVisible(fieldName) { - return this?.systemProperties?.sitePreferences?.signupVisibleFields?.includes( - fieldName, - ) || this?.systemProperties?.sitePreferences?.signupAddressVisibleFields?.includes( - fieldName, + return ( + this?.systemProperties?.sitePreferences?.signupVisibleFields?.includes( + fieldName, + ) || + this?.systemProperties?.sitePreferences?.signupAddressVisibleFields?.includes( + fieldName, + ) ); }, isStepEnabled(stepName) { @@ -745,7 +793,11 @@ export default { return data; }, currentEmail() { - if (this.emailMode === 0 && this.data.email.localPart && this.data.email.domain) { + if ( + this.emailMode === 0 && + this.data.email.localPart && + this.data.email.domain + ) { return `${this.data.email.localPart}@${this.data.email.domain.domain}`; } else if (this.data.user.email) { return this.data.user.email; @@ -767,7 +819,7 @@ export default { ], username: this.data.user.username, email: this.currentEmail, - } + }; }, steps() { return [ @@ -793,7 +845,12 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && (this.isFieldVisible("street") | this.isFieldVisible("housenumber") | this.isFieldVisible("postal_code") | this.isFieldVisible("place") | this.isFieldVisible("country")) + ...(!this.invitation?.hasPerson && + this.isFieldVisible("street") | + this.isFieldVisible("housenumber") | + this.isFieldVisible("postal_code") | + this.isFieldVisible("place") | + this.isFieldVisible("country") ? [ { name: "address_data", @@ -801,7 +858,9 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && (this.isFieldVisible("mobile_number") | this.isFieldVisible("phone_number")) + ...(!this.invitation?.hasPerson && + this.isFieldVisible("mobile_number") | + this.isFieldVisible("phone_number") ? [ { name: "contact_data", @@ -809,7 +868,12 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && (this.isFieldVisible("date_of_birth") | this.isFieldVisible("place_of_birth") | this.isFieldVisible("sex") | this.isFieldVisible("photo") | this.isFieldVisible("description")) + ...(!this.invitation?.hasPerson && + this.isFieldVisible("date_of_birth") | + this.isFieldVisible("place_of_birth") | + this.isFieldVisible("sex") | + this.isFieldVisible("photo") | + this.isFieldVisible("description") ? [ { name: "additional_data", @@ -830,10 +894,17 @@ export default { return []; }, invitationNextI18nKey() { - return this.data.invitationCode ? "accounts.signup.form.steps.invitation.next.with_code" : this.invitationCodeRequired ? "accounts.signup.form.steps.invitation.next.code_required" : "accounts.signup.form.steps.invitation.next.without_code"; + return this.data.invitationCode + ? "accounts.signup.form.steps.invitation.next.with_code" + : this.invitationCodeRequired + ? "accounts.signup.form.steps.invitation.next.code_required" + : "accounts.signup.form.steps.invitation.next.without_code"; }, invitationCodeRequired() { - return this.checkPermission("core.invite_enabled") && !this.checkPermission("core.signup_rule"); + return ( + this.checkPermission("core.invite_enabled") && + !this.checkPermission("core.signup_rule") + ); }, }, data() { diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index b0468cca2..f5b3f8f12 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -45,9 +45,7 @@ <v-list-item-content> <v-list-item-title> {{ - person.sex - ? $t("person.sex." + person.sex.toLowerCase()) - : "–" + person.sex ? $t("person.sex." + person.sex.toLowerCase()) : "–" }} </v-list-item-title> <v-list-item-subtitle> @@ -69,20 +67,13 @@ <v-list-item-content> <v-list-item-title> {{ address.street || "–" }} {{ address.housenumber }} - <span - v-if=" - address.postalCode || address.place || address.country - " - > + <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 - " + v-if="(address.postalCode || address.place) && address.country" > , </span> @@ -116,9 +107,7 @@ </v-list-item> <v-list-item - :href=" - person.mobileNumber ? 'tel:' + person.mobileNumber : '' - " + :href="person.mobileNumber ? 'tel:' + person.mobileNumber : ''" > <v-list-item-action></v-list-item-action> @@ -133,9 +122,7 @@ </v-list-item> <v-divider inset /> - <v-list-item - :href="person.email ? 'mailto:' + person.email : ''" - > + <v-list-item :href="person.email ? 'mailto:' + person.email : ''"> <v-list-item-icon> <v-icon>mdi-email-outline</v-icon> </v-list-item-icon> @@ -161,10 +148,7 @@ <span v-if="person.dateOfBirth && person.placeOfBirth"> {{ $t("person.birth_date_and_birth_place_formatted", { - date: $d( - $parseISODate(person.dateOfBirth), - "short", - ), + date: $d($parseISODate(person.dateOfBirth), "short"), place: person.placeOfBirth, }) }} @@ -213,5 +197,5 @@ export default { default: "person.details", }, }, -} +}; </script> diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 08173de00..16666fc9d 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -332,9 +332,7 @@ class SignupAddressRequiredFields(MultipleChoicePreference): name = "signup_address_required_fields" default = [] widget = SelectMultiple - verbose_name = _( - "Fields on address model which are required when signing up." - ) + verbose_name = _("Fields on address model which are required when signing up.") field_attribute = {"initial": []} choices = [ (field.name, field.name) @@ -372,9 +370,7 @@ class SignupAddressVisibleFields(MultipleChoicePreference): name = "signup_address_visible_fields" default = [] widget = SelectMultiple - verbose_name = _( - "Fields on address model which are visible when signing up." - ) + verbose_name = _("Fields on address model which are visible when signing up.") field_attribute = {"initial": []} choices = [ (field.name, field.name) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 05539c458..8a8d72c47 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -602,11 +602,15 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat if account_registration["invitation_code"]: try: - invitation = PersonInvitation.objects.get(key=account_registration["invitation_code"]) + invitation = PersonInvitation.objects.get( + key=account_registration["invitation_code"] + ) except PersonInvitation.DoesNotExist as exc: raise SuspiciousOperation from exc - - if not get_site_preferences()["auth__signup_enabled"] and not (get_site_preferences()["auth__invite_enabled"] and invitation): + + if not get_site_preferences()["auth__signup_enabled"] and not ( + get_site_preferences()["auth__invite_enabled"] and invitation + ): raise PermissionDenied(_("Signup is not enabled.")) # Create email @@ -615,7 +619,11 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat if invitation and invitation.email: email = invitation.email - elif "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] and account_registration["email"] is not None: + elif ( + "postbuero" + in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] + and account_registration["email"] is not None + ): try: domain = MailDomain.objects.get(pk=account_registration["email"]["domain"]) except IntegrityError: @@ -659,8 +667,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat ) except IntegrityError: raise ValidationError( - _("A person using the e-mail address %s already exists.") - % email + _("A person using the e-mail address %s already exists.") % email ) # Store contact information in database diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index e66cf1b7a..40a2445fc 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -108,7 +108,9 @@ urlpatterns = [ name="dav_resource_contact", ), path( - "accounts/signup/", views.TemplateView.as_view(template_name="core/vue_index.html"), name="account_signup" + "accounts/signup/", + views.TemplateView.as_view(template_name="core/vue_index.html"), + name="account_signup", ), path("", include("django_prometheus.urls")), path( -- GitLab From eb88444887a18adc3962950e0d1c53f6233e472a Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 28 Mar 2025 20:23:02 +0100 Subject: [PATCH 37/82] Fix typo --- aleksis/core/schema/site_preferences.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/schema/site_preferences.py b/aleksis/core/schema/site_preferences.py index a0b5f56dd..4821746b4 100644 --- a/aleksis/core/schema/site_preferences.py +++ b/aleksis/core/schema/site_preferences.py @@ -90,5 +90,5 @@ class SitePreferencesType(graphene.ObjectType): def resolve_signup_visible_fields(parent, info, **kwargs): return parent["auth__signup_visible_fields"] - def resolve_signup_address_required_fields(parent, info, **kwargs): + def resolve_signup_address_visible_fields(parent, info, **kwargs): return parent["auth__signup_address_visible_fields"] -- GitLab From 13401fe642df1f53245ab47d7b3a11c2cf484f08 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 28 Mar 2025 21:30:04 +0100 Subject: [PATCH 38/82] Fix syntax --- aleksis/core/models.py | 2 +- aleksis/core/schema/person.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 5555739d9..ec8311358 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1437,7 +1437,7 @@ class PersonInvitation(AbstractBaseInvitation, PureDjangoModel): def send_invitation(self, request, **kwargs): """Send the invitation email to the person.""" # TODO: Use correct URL to new signup wizard - invite_url = f"{reverse("account_signup")}?invitation_code={self.key}" + invite_url = f"{reverse('account_signup')}?invitation_code={self.key}" invite_url = request.build_absolute_uri(invite_url).replace("/django", "") context = kwargs context.update( diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 8a8d72c47..d648209fd 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -25,6 +25,7 @@ from ..models import ( PersonInvitation, PersonRelationship, Role, +) from ..util.apps import AppConfig from ..util.core_helpers import get_site_preferences, has_person from .address import AddressType as GraphQLAddressType -- GitLab From d19915fd2a81ce63fc4c126b250bb62ddb982858 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 2 Apr 2025 18:50:02 +0200 Subject: [PATCH 39/82] Reformat --- .../account/AccountRegistrationForm.vue | 7 +++-- .../components/person/PersonDetailsCard.vue | 2 +- .../components/person/PersonOverview.vue | 1 - aleksis/core/models.py | 15 +++-------- aleksis/core/preferences.py | 16 ++++++------ aleksis/core/schema/person.py | 26 ++++++++----------- 6 files changed, 28 insertions(+), 39 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 0b40676c4..832378cb4 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -121,12 +121,15 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-stepper-step :complete="step > index + 1" :step="index + 1" - :key="stepChoice.name" + :key="`${index}-step`" :ref="`step-${index}`" > {{ $t(stepChoice.titleKey) }} </v-stepper-step> - <v-divider v-if="index + 1 < steps.length"></v-divider> + <v-divider + v-if="index + 1 < steps.length" + :key="`${index}-divider`" + ></v-divider> </template> </v-stepper-header> <v-stepper-items> diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index f5b3f8f12..b06ee2c5e 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -1,3 +1,4 @@ +<!-- eslint-disable @intlify/vue-i18n/no-raw-text --> <template> <v-card v-bind="$attrs"> <v-card-title>{{ $t(titleKey) }}</v-card-title> @@ -55,7 +56,6 @@ </v-list-item> <v-divider inset /> - <v-list-item v-for="(address, index) in person.addresses" :key="address.id" diff --git a/aleksis/core/frontend/components/person/PersonOverview.vue b/aleksis/core/frontend/components/person/PersonOverview.vue index e57d9b21c..6ce12c676 100644 --- a/aleksis/core/frontend/components/person/PersonOverview.vue +++ b/aleksis/core/frontend/components/person/PersonOverview.vue @@ -1,4 +1,3 @@ -<!-- eslint-disable @intlify/vue-i18n/no-raw-text --> <template> <object-overview :query="query" title-attr="fullName" :id="id" ref="overview"> <template #loading> diff --git a/aleksis/core/models.py b/aleksis/core/models.py index ec8311358..16e3aeaac 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -485,10 +485,7 @@ class Person(ContactMixin, ExtensibleModel): card.append(f"TEL;TYPE=cell:{self.mobile_number}") # Addresses - if ( - not self._is_unrequested_prop("ADR", params) - and self.addresses.exists() - ): + if not self._is_unrequested_prop("ADR", params) and self.addresses.exists(): for address in self.addresses.all(): address_types = ",".join(address.address_types.values_list("name", flat=True)) card.append( @@ -2327,17 +2324,11 @@ class Organisation(ContactMixin, ExtensibleModel): card.append(f"ORG:{self.name}") # Email - if ( - not self._is_unrequested_prop("EMAIL", params) - and self.email - ): + if not self._is_unrequested_prop("EMAIL", params) and self.email: card.append(f"EMAIL:{self.email}") # Addresses - if ( - not self._is_unrequested_prop("ADR", params) - and self.addresses.exists() - ): + if not self._is_unrequested_prop("ADR", params) and self.addresses.exists(): for address in self.addresses.all(): address_types = ",".join(address.address_types.values_list("name", flat=True)) card.append( diff --git a/aleksis/core/preferences.py b/aleksis/core/preferences.py index 16666fc9d..4ae635d89 100644 --- a/aleksis/core/preferences.py +++ b/aleksis/core/preferences.py @@ -306,14 +306,14 @@ class SignupEnabled(BooleanPreference): @site_preferences_registry.register class SignupRequiredFields(MultipleChoicePreference): - """Fields on person model which are required when signing up.""" + """Required fields on the person model for sign-up.""" section = auth name = "signup_required_fields" default = [] widget = SelectMultiple verbose_name = _( - "Fields on person model which are required when signing up. The first and last name fields are always required." + "Required fields on the person model for sign-up. First and last name are always required." ) field_attribute = {"initial": []} choices = [ @@ -326,13 +326,13 @@ class SignupRequiredFields(MultipleChoicePreference): @site_preferences_registry.register class SignupAddressRequiredFields(MultipleChoicePreference): - """Fields on address model which are required when signing up.""" + """Required fields on the address model for sign-up.""" section = auth name = "signup_address_required_fields" default = [] widget = SelectMultiple - verbose_name = _("Fields on address model which are required when signing up.") + verbose_name = _("Required fields on the address model for sign-up.") field_attribute = {"initial": []} choices = [ (field.name, field.name) @@ -344,14 +344,14 @@ class SignupAddressRequiredFields(MultipleChoicePreference): @site_preferences_registry.register class SignupVisibleFields(MultipleChoicePreference): - """Fields on person model which are visible when signing up.""" + """Visible fields on the person model for sign-up.""" section = auth name = "signup_visible_fields" default = [] widget = SelectMultiple verbose_name = _( - "Fields on person model which are visible when signing up. The first and last name fields are always visible." + "Visible fields on the person model for sign-up. First and last name are always visible." ) field_attribute = {"initial": []} choices = [ @@ -364,13 +364,13 @@ class SignupVisibleFields(MultipleChoicePreference): @site_preferences_registry.register class SignupAddressVisibleFields(MultipleChoicePreference): - """Fields on address model which are visible when signing up.""" + """Visible fields on the address model for sign-up.""" section = auth name = "signup_address_visible_fields" default = [] widget = SelectMultiple - verbose_name = _("Fields on address model which are visible when signing up.") + verbose_name = _("Visible fields on the address model for sign-up.") field_attribute = {"initial": []} choices = [ (field.name, field.name) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index d648209fd..56015d8a2 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -426,9 +426,7 @@ class PersonAddressMutationMixin: class PersonBatchCreateMutation( - PersonAddressMutationMixin, - PersonGuardianMutationMixin, - BaseBatchCreateMutation + PersonAddressMutationMixin, PersonGuardianMutationMixin, BaseBatchCreateMutation ): class Meta: model = Person @@ -491,9 +489,7 @@ class PersonBatchCreateMutation( class PersonBatchPatchMutation( - PersonAddressMutationMixin, - PersonGuardianMutationMixin, - BaseBatchPatchMutation + PersonAddressMutationMixin, PersonGuardianMutationMixin, BaseBatchPatchMutation ): class Meta: model = Person @@ -627,15 +623,15 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat ): try: domain = MailDomain.objects.get(pk=account_registration["email"]["domain"]) - except IntegrityError: - raise ValidationError(_("Mail domain does not exist.")) + except IntegrityError as exc: + raise ValidationError(_("Mail domain does not exist.")) from exc try: _mail_address = MailAddress.objects.create( local_part=account_registration["email"]["local_part"], domain=domain, ) - except IntegrityError: - raise ValidationError(_("Mail address already in use.")) + except IntegrityError as exc: + raise ValidationError(_("Mail address already in use.")) from exc email = str(_mail_address) elif account_registration["user"] is not None: @@ -648,8 +644,8 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat email=email, password=account_registration["user"]["password"], ) - except IntegrityError: - raise ValidationError(_("A user with this username or e-mail already exists.")) + except IntegrityError as exc: + raise ValidationError(_("A user with this username or e-mail already exists.")) from exc # Create person if no invitation is given or if invitation isn't linked to a person if invitation and invitation.person: @@ -666,10 +662,10 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat "last_name": account_registration["person"]["last_name"], }, ) - except IntegrityError: + except IntegrityError as exc: raise ValidationError( _("A person using the e-mail address %s already exists.") % email - ) + ) from exc # Store contact information in database for field in Person._meta.get_fields(): @@ -695,7 +691,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat _act = Activity( title=_("You registered an account"), - description=_("You registered an account with the username %s" % user.username), + description=_(f"You registered an account with the username {user.username}"), app="Core", user=person, ) -- GitLab From 1a9621a8696bf6bf0cf9e44de25e09e42bcc6267 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 2 Apr 2025 18:56:17 +0200 Subject: [PATCH 40/82] Add username checking --- .../frontend/components/account/AccountRegistrationForm.vue | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 832378cb4..dbddcb11a 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -178,7 +178,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ) " required - :rules="$rules().required.build()" + :rules="$rules().required.build([ ...usernameRules.usernameAllowed, ...usernameRules.usernameASCII ])" prepend-icon="mdi-account-outline" ></v-text-field> </div> @@ -655,6 +655,7 @@ import { collections } from "aleksisAppImporter"; import formRulesMixin from "../../mixins/formRulesMixin"; import permissionsMixin from "../../mixins/permissions"; +import usernameRulesMixin from "../../mixins/usernameRulesMixin"; export default { name: "AccountRegistrationForm", @@ -680,7 +681,7 @@ export default { skip: true, }, }, - mixins: [formRulesMixin, permissionsMixin], + mixins: [formRulesMixin, permissionsMixin, usernameRulesMixin], methods: { setStep(step) { this.step = step; -- GitLab From dce61e3912d3052190736f34779536a342e6ee18 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 2 Apr 2025 18:58:55 +0200 Subject: [PATCH 41/82] Add username checking in background --- aleksis/core/schema/person.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 56015d8a2..309b60792 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -27,6 +27,7 @@ from ..models import ( Role, ) from ..util.apps import AppConfig +from ..util.auth_helpers import custom_username_validators from ..util.core_helpers import get_site_preferences, has_person from .address import AddressType as GraphQLAddressType from .base import ( @@ -637,6 +638,10 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat elif account_registration["user"] is not None: email = account_registration["user"]["email"] + # Check username + for validator in custom_username_validators: + validator(account_registration["user"]["username"]) + # Create user try: user = get_user_model().objects.create_user( -- GitLab From 8d29da0ce6d03b7018fa16137e29bce699846d28 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 2 Apr 2025 19:08:37 +0200 Subject: [PATCH 42/82] Removed unused CSS rule --- .../frontend/components/account/AccountRegistrationForm.vue | 3 --- 1 file changed, 3 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index dbddcb11a..e3dd7c976 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -973,9 +973,6 @@ export default { </script> <style> -.btn-multiline > span { - width: 100%; -} .v-stepper__header { overflow: auto; display: flex; -- GitLab From eba6aad0aa64473a5bb211b1b64a3430c2553b24 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 9 Apr 2025 18:18:59 +0200 Subject: [PATCH 43/82] Reformat --- .../components/account/AccountRegistrationForm.vue | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index e3dd7c976..e6b4a110c 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -178,7 +178,12 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; ) " required - :rules="$rules().required.build([ ...usernameRules.usernameAllowed, ...usernameRules.usernameASCII ])" + :rules=" + $rules().required.build([ + ...usernameRules.usernameAllowed, + ...usernameRules.usernameASCII, + ]) + " prepend-icon="mdi-account-outline" ></v-text-field> </div> -- GitLab From 019c9b1164757ad6a8bdf04387125bee35aad1cc Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Wed, 9 Apr 2025 19:43:16 +0200 Subject: [PATCH 44/82] Provide more helpful error message --- aleksis/core/schema/person.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 309b60792..e88d73ce0 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -640,7 +640,10 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Check username for validator in custom_username_validators: - validator(account_registration["user"]["username"]) + try: + validator(account_registration["user"]["username"]) + except ValidationError as exc: + raise ValidationError(_("This username is not allowed.")) from exc # Create user try: -- GitLab From 20682f904d4f1a91b3dc552ecfc4c7e9dc1ecaf4 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 9 Apr 2025 19:45:04 +0200 Subject: [PATCH 45/82] Fix username rules mixin --- aleksis/core/frontend/mixins/usernameRulesMixin.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aleksis/core/frontend/mixins/usernameRulesMixin.js b/aleksis/core/frontend/mixins/usernameRulesMixin.js index ec9d60b54..ce866f433 100644 --- a/aleksis/core/frontend/mixins/usernameRulesMixin.js +++ b/aleksis/core/frontend/mixins/usernameRulesMixin.js @@ -5,7 +5,7 @@ import { gqlUsernamePreferences } from "../components/app/systemProperties.graph */ export default { apollo: { - systemProperties: gqlUsernamePreferences, + usernameSystemProperties: gqlUsernamePreferences, }, computed: { usernameRules() { @@ -31,13 +31,13 @@ export default { return string.match(/[^\x00-\x7F]/g) || []; }, checkDisallowed(string) { - return this.systemProperties.sitePreferences.authDisallowedUids?.includes( + return this.usernameSystemProperties.sitePreferences.authDisallowedUids?.includes( string, ); }, checkAllowed(string) { const regEx = new RegExp( - this.systemProperties.sitePreferences.authAllowedUsernameRegex, + this.usernameSystemProperties.sitePreferences.authAllowedUsernameRegex, ); return regEx.test(string); }, -- GitLab From 068c75107904c4de95a08e66824742dcc70b8a69 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Wed, 9 Apr 2025 21:21:28 +0200 Subject: [PATCH 46/82] Change username site preference gql naming --- aleksis/core/frontend/components/app/systemProperties.graphql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/app/systemProperties.graphql b/aleksis/core/frontend/components/app/systemProperties.graphql index c71e1f865..060100171 100644 --- a/aleksis/core/frontend/components/app/systemProperties.graphql +++ b/aleksis/core/frontend/components/app/systemProperties.graphql @@ -29,7 +29,7 @@ query gqlSystemProperties { } query gqlUsernamePreferences { - systemProperties { + usernameSystemProperties: systemProperties { sitePreferences { authAllowedUsernameRegex authDisallowedUids -- GitLab From 680408ff51c71be130e4eb74fcf6973fd47623a7 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 11:29:02 +0200 Subject: [PATCH 47/82] Fix email step --- .../account/AccountRegistrationForm.vue | 64 +++++++++++++++++-- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index e6b4a110c..b19347599 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -139,7 +139,9 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; > <h2 class="text-h6 mb-4">{{ $t(getStepTitleKey("email")) }}</h2> <div class="mb-4"> + <!-- TODO: Optional email fields when using injected component --> <component + v-if="postbueroStepEnabled" :is=" collectionSteps.find( (s) => s.key === 'postbuero-mail-address-form-step', @@ -149,14 +151,58 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; @emailModeChange="setEmailMode" v-model="validationStatuses['email']" /> + <v-form v-else v-model="validationStatuses['email']"> + <v-row class="mt-4"> + <v-col cols="12" md="6"> + <div :aria-required="isFieldRequired('email')"> + <v-text-field + outlined + v-model="data.user.email" + :label=" + $t( + 'accounts.signup.form.steps.email.fields.email.label', + ) + " + required + :rules=" + isFieldRequired('email') + ? $rules().required.isEmail.build() + : $rules().isEmail.build() + " + prepend-icon="mdi-email-outline" + ></v-text-field> + </div> + </v-col> + <v-col cols="12" md="6"> + <div :aria-required="isFieldRequired('email')"> + <v-text-field + outlined + v-model="data.user.confirmEmail" + :label=" + $t( + 'accounts.signup.form.steps.email.fields.confirm_email.label', + ) + " + required + :rules=" + isFieldRequired('email') + ? $rules().required.build(rules.confirmEmail) + : rules.confirmEmail + " + prepend-icon="mdi-email-outline" + ></v-text-field> + </div> + </v-col> + </v-row> + </v-form> </div> <v-divider class="mb-4" /> <control-row :step="step" @set-step="setStep" :next-disabled=" - emailMode === undefined || - emailMode === null || + (postbueroStepEnabled && + (emailMode === undefined || emailMode === null)) || !validationStatuses['email'] " /> @@ -769,6 +815,11 @@ export default { }, }, computed: { + postbueroStepEnabled() { + return this.collectionSteps.some( + (s) => s.key === "postbuero-mail-address-form-step", + ); + }, rules() { return { confirmPassword: [ @@ -776,6 +827,11 @@ export default { this.data.user.password == v || this.$t("accounts.signup.form.rules.confirm_password.no_match"), ], + confirmEmail: [ + (v) => + this.data.user.email == v || + this.$t("accounts.signup.form.rules.confirm_email.no_match"), + ], }; }, dataForSubmit() { @@ -832,9 +888,7 @@ export default { }, steps() { return [ - ...(this.collectionSteps.some( - (s) => s.key === "postbuero-mail-address-form-step", - ) && !this.invitation?.hasEmail + ...(!this.invitation?.hasEmail && this.isFieldVisible("email") ? [ { name: "email", -- GitLab From ccd91918060b5b778685061baa52b1676b5ad2f6 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 11:51:11 +0200 Subject: [PATCH 48/82] Add privacy policy checkbox --- .../account/AccountRegistrationForm.vue | 39 ++++++++++++++++++- .../components/account/helpers.graphql | 4 +- aleksis/core/frontend/messages/en.json | 4 ++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index b19347599..af9609424 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -670,6 +670,36 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; title-key="accounts.signup.form.steps.confirm.card_title" /> + <div + v-if="systemProperties.sitePreferences.footerPrivacyUrl" + aria-required="true" + class="mb-4" + > + <v-checkbox required v-model="privacyPolicyAccepted"> + <template #label> + <i18n + path="accounts.signup.form.steps.confirm.privacy_policy.label" + tag="div" + > + <template #url> + <a + @click.stop + :href=" + systemProperties.sitePreferences.footerPrivacyUrl + " + target="_blank" + >{{ + $t( + "accounts.signup.form.steps.confirm.privacy_policy.url_text", + ) + }}</a + > + </template> + </i18n> + </template> + </v-checkbox> + </div> + <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" :variables="{ @@ -684,6 +714,10 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; @set-step="setStep" @confirm="mutate" :next-loading="loading" + :next-disabled=" + systemProperties.sitePreferences.footerPrivacyUrl && + !privacyPolicyAccepted + " /> <v-alert v-if="error" type="error" outlined class="mt-4">{{ error.message @@ -699,7 +733,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <script> import { - gqlRequiredFieldsPreference, + gqlAccountWizardSystemProperties, gqlPersonInvitationByCode, } from "./helpers.graphql"; import { collections } from "aleksisAppImporter"; @@ -712,7 +746,7 @@ export default { name: "AccountRegistrationForm", apollo: { systemProperties: { - query: gqlRequiredFieldsPreference, + query: gqlAccountWizardSystemProperties, }, personInvitationByCode: { query: gqlPersonInvitationByCode, @@ -981,6 +1015,7 @@ export default { accountRegistrationSent: false, step: 1, emailMode: null, + privacyPolicyAccepted: false, data: { email: { localPart: "", diff --git a/aleksis/core/frontend/components/account/helpers.graphql b/aleksis/core/frontend/components/account/helpers.graphql index 5fb52db46..5491983d6 100644 --- a/aleksis/core/frontend/components/account/helpers.graphql +++ b/aleksis/core/frontend/components/account/helpers.graphql @@ -1,10 +1,12 @@ -query gqlRequiredFieldsPreference { +query gqlAccountWizardSystemProperties { systemProperties { sitePreferences { signupRequiredFields signupAddressRequiredFields signupVisibleFields signupAddressVisibleFields + + footerPrivacyUrl } } } diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 9ef8a9245..535442abe 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -164,6 +164,10 @@ } }, "confirm": { + "privacy_policy": { + "label": "I have read and agree to the {url}.", + "url_text": "privacy policy" + }, "title": "Confirm account registration", "invitation_used": "Some personal data will be taken over from the data associated with the invitation you accepted.", "card_title": "Your account data" -- GitLab From 7d801f6a2b007623234c3a40c5c0482d18a47ba2 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:21:10 +0200 Subject: [PATCH 49/82] Add password validation in frontend --- .../account/AccountRegistrationForm.vue | 6 +- .../generic/forms/PasswordField.vue | 63 +++++++++++++++++++ .../components/generic/forms/password.graphql | 7 +++ aleksis/core/schema/__init__.py | 17 +++++ 4 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 aleksis/core/frontend/components/generic/forms/PasswordField.vue create mode 100644 aleksis/core/frontend/components/generic/forms/password.graphql diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index af9609424..b292259d7 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -4,6 +4,7 @@ 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 PasswordField from "../generic/forms/PasswordField.vue"; import PrimaryActionButton from "../generic/buttons/PrimaryActionButton.vue"; @@ -238,7 +239,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <v-row> <v-col cols="12" md="6"> <div aria-required="true"> - <v-text-field + <password-field outlined v-model="data.user.password" :label=" @@ -248,9 +249,8 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; " required :rules="$rules().required.build()" - type="password" prepend-icon="mdi-form-textbox-password" - ></v-text-field> + /> </div> </v-col> <v-col cols="12" md="6"> diff --git a/aleksis/core/frontend/components/generic/forms/PasswordField.vue b/aleksis/core/frontend/components/generic/forms/PasswordField.vue new file mode 100644 index 000000000..e3cf38f30 --- /dev/null +++ b/aleksis/core/frontend/components/generic/forms/PasswordField.vue @@ -0,0 +1,63 @@ +<template> + <v-text-field + v-bind="$attrs" + v-on="$listeners" + :rules="[...rules, ...passwordRules.passwordValidation]" + type="password" + :hint="passwordHelpTexts.join(' · ')" + persistent-hint + :loading="$apollo.queries.passwordValidationStatus.loading" + @change="refetchValidation" + validate-on-blur + ref="passwordField" + /> +</template> + +<script> +import { gqlPasswordHelpTexts, gqlPasswordValidationStatus } from "./password.graphql"; + +export default { + name: "PasswordField", + extends: "v-text-field", + data() { + return { + passwordHelpTexts: [], + passwordValidationStatus: [], + }; + }, + props: { + rules: { + type: Array, + required: false, + default: () => [], + }, + }, + computed: { + passwordRules() { + return { + passwordValidation: [ + (v) => this.passwordValidationStatus.length === 0 || this.passwordValidationStatus.join(' · ') + ], + } + } + }, + apollo: { + passwordHelpTexts: gqlPasswordHelpTexts, + passwordValidationStatus: { + query: gqlPasswordValidationStatus, + skip: true, + result ({ data, loading, networkStatus }) { + this.$refs.passwordField.validate(); + }, + }, + }, + methods: { + refetchValidation(password) { + this.$apollo.queries.passwordValidationStatus.setVariables({ + password: password + }); + this.$apollo.queries.passwordValidationStatus.skip = false; + }, + }, +}; +</script> diff --git a/aleksis/core/frontend/components/generic/forms/password.graphql b/aleksis/core/frontend/components/generic/forms/password.graphql new file mode 100644 index 000000000..0ea07b04b --- /dev/null +++ b/aleksis/core/frontend/components/generic/forms/password.graphql @@ -0,0 +1,7 @@ +query gqlPasswordHelpTexts { + passwordHelpTexts +} + +query gqlPasswordValidationStatus($password: String!) { + passwordValidationStatus(password: $password) +} diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index b515c321f..59f0c72d6 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -1,7 +1,9 @@ from django.apps import apps from django.contrib.auth import get_user_model +from django.contrib.auth.password_validation import validate_password, password_validators_help_texts from django.contrib.contenttypes.models import ContentType from django.contrib.messages import get_messages +from django.core.exceptions import ValidationError from django.db.models import Q import graphene @@ -197,6 +199,9 @@ class Query(graphene.ObjectType): person_invitation_by_code = graphene.Field(PersonInvitationType, code=graphene.String()) + password_help_texts = graphene.List(graphene.String) + password_validation_status = graphene.List(graphene.String, password=graphene.String()) + def resolve_ping(root, info, payload) -> str: return payload @@ -485,6 +490,18 @@ class Query(graphene.ObjectType): return PersonInvitation.objects.get(key=formatted_code) return None + @staticmethod + def resolve_password_help_texts(root, info, **kwargs): + return password_validators_help_texts() + + @staticmethod + def resolve_password_validation_status(root, info, password, **kwargs): + try: + validate_password(password, info.context.user) + return [] + except ValidationError as exc: + return exc.messages + class Mutation(graphene.ObjectType): delete_persons = PersonBatchDeleteMutation.Field() -- GitLab From 7e5a77634d0d2e2556695fd6219127f26065f416 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:28:16 +0200 Subject: [PATCH 50/82] Validate password in backend --- aleksis/core/schema/person.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index e88d73ce0..376217349 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -2,6 +2,7 @@ from typing import Union from django.apps import apps from django.contrib.auth import get_user_model +from django.contrib.auth.password_validation import validate_password from django.core.exceptions import PermissionDenied, SuspiciousOperation, ValidationError from django.db import IntegrityError, transaction from django.db.models import Q @@ -655,6 +656,8 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat except IntegrityError as exc: raise ValidationError(_("A user with this username or e-mail already exists.")) from exc + validate_password(account_registration["user"]["password"], user) + # Create person if no invitation is given or if invitation isn't linked to a person if invitation and invitation.person: person = invitation.person -- GitLab From bc6b1652c3e0953b018a090cc8f5087bc0f01782 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:37:27 +0200 Subject: [PATCH 51/82] Hide empty fields in person summary of account registration --- .../account/AccountRegistrationForm.vue | 1 + .../components/person/PersonDetailsCard.vue | 16 +++++++++++++--- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index b292259d7..92280302a 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -667,6 +667,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; class="mb-4" :person="personDataForSummary" :show-username="true" + :show-when-empty="false" title-key="accounts.signup.form.steps.confirm.card_title" /> diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index b06ee2c5e..4ea8edfaf 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -38,7 +38,9 @@ </v-list-item> <v-divider inset /> - <v-list-item> + <v-list-item + v-if="showWhenEmpty || person.sex" + > <v-list-item-icon> <v-icon> mdi-human-non-binary</v-icon> </v-list-item-icon> @@ -58,6 +60,7 @@ <v-list-item v-for="(address, index) in person.addresses" + v-if="showWhenEmpty || address.street || address.housenumber || address.postalCode || address.place || address.country" :key="address.id" > <v-list-item-icon v-if="index === 0"> @@ -90,6 +93,7 @@ </v-list-item> <v-list-item + v-if="showWhenEmpty || person.phoneNumber" :href="person.phoneNumber ? 'tel:' + person.phoneNumber : ''" > <v-list-item-icon> @@ -107,6 +111,7 @@ </v-list-item> <v-list-item + v-if="showWhenEmpty || person.mobileNumber" :href="person.mobileNumber ? 'tel:' + person.mobileNumber : ''" > <v-list-item-action></v-list-item-action> @@ -122,7 +127,7 @@ </v-list-item> <v-divider inset /> - <v-list-item :href="person.email ? 'mailto:' + person.email : ''"> + <v-list-item v-if="showWhenEmpty || person.email" :href="person.email ? 'mailto:' + person.email : ''"> <v-list-item-icon> <v-icon>mdi-email-outline</v-icon> </v-list-item-icon> @@ -138,7 +143,7 @@ </v-list-item> <v-divider inset /> - <v-list-item> + <v-list-item v-if="showWhenEmpty || (person.dateOfBirth || person.placeOfBirth)"> <v-list-item-icon> <v-icon> mdi-cake-variant-outline</v-icon> </v-list-item-icon> @@ -196,6 +201,11 @@ export default { required: false, default: "person.details", }, + showWhenEmpty: { + type: Boolean, + required: false, + default: true, + }, }, }; </script> -- GitLab From 123b01bcde4f061eab45a254989d6b2fca2e823d Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Thu, 10 Apr 2025 16:41:50 +0200 Subject: [PATCH 52/82] Reformat --- .../generic/forms/PasswordField.vue | 17 ++++++---- .../components/person/PersonDetailsCard.vue | 31 +++++++++++++------ aleksis/core/schema/__init__.py | 5 ++- 3 files changed, 37 insertions(+), 16 deletions(-) diff --git a/aleksis/core/frontend/components/generic/forms/PasswordField.vue b/aleksis/core/frontend/components/generic/forms/PasswordField.vue index e3cf38f30..f66c97420 100644 --- a/aleksis/core/frontend/components/generic/forms/PasswordField.vue +++ b/aleksis/core/frontend/components/generic/forms/PasswordField.vue @@ -14,7 +14,10 @@ </template> <script> -import { gqlPasswordHelpTexts, gqlPasswordValidationStatus } from "./password.graphql"; +import { + gqlPasswordHelpTexts, + gqlPasswordValidationStatus, +} from "./password.graphql"; export default { name: "PasswordField", @@ -36,17 +39,19 @@ export default { passwordRules() { return { passwordValidation: [ - (v) => this.passwordValidationStatus.length === 0 || this.passwordValidationStatus.join(' · ') + (v) => + this.passwordValidationStatus.length === 0 || + this.passwordValidationStatus.join(" · "), ], - } - } + }; + }, }, apollo: { passwordHelpTexts: gqlPasswordHelpTexts, passwordValidationStatus: { query: gqlPasswordValidationStatus, skip: true, - result ({ data, loading, networkStatus }) { + result({ data, loading, networkStatus }) { this.$refs.passwordField.validate(); }, }, @@ -54,7 +59,7 @@ export default { methods: { refetchValidation(password) { this.$apollo.queries.passwordValidationStatus.setVariables({ - password: password + password: password, }); this.$apollo.queries.passwordValidationStatus.skip = false; }, diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index 4ea8edfaf..96514a21e 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -38,9 +38,7 @@ </v-list-item> <v-divider inset /> - <v-list-item - v-if="showWhenEmpty || person.sex" - > + <v-list-item v-if="showWhenEmpty || person.sex"> <v-list-item-icon> <v-icon> mdi-human-non-binary</v-icon> </v-list-item-icon> @@ -59,8 +57,7 @@ <v-divider inset /> <v-list-item - v-for="(address, index) in person.addresses" - v-if="showWhenEmpty || address.street || address.housenumber || address.postalCode || address.place || address.country" + v-for="(address, index) in filteredAddresses" :key="address.id" > <v-list-item-icon v-if="index === 0"> @@ -93,7 +90,7 @@ </v-list-item> <v-list-item - v-if="showWhenEmpty || person.phoneNumber" + v-if="showWhenEmpty || person.phoneNumber" :href="person.phoneNumber ? 'tel:' + person.phoneNumber : ''" > <v-list-item-icon> @@ -111,7 +108,7 @@ </v-list-item> <v-list-item - v-if="showWhenEmpty || person.mobileNumber" + v-if="showWhenEmpty || person.mobileNumber" :href="person.mobileNumber ? 'tel:' + person.mobileNumber : ''" > <v-list-item-action></v-list-item-action> @@ -127,7 +124,10 @@ </v-list-item> <v-divider inset /> - <v-list-item v-if="showWhenEmpty || person.email" :href="person.email ? 'mailto:' + person.email : ''"> + <v-list-item + v-if="showWhenEmpty || person.email" + :href="person.email ? 'mailto:' + person.email : ''" + > <v-list-item-icon> <v-icon>mdi-email-outline</v-icon> </v-list-item-icon> @@ -143,7 +143,9 @@ </v-list-item> <v-divider inset /> - <v-list-item v-if="showWhenEmpty || (person.dateOfBirth || person.placeOfBirth)"> + <v-list-item + v-if="showWhenEmpty || person.dateOfBirth || person.placeOfBirth" + > <v-list-item-icon> <v-icon> mdi-cake-variant-outline</v-icon> </v-list-item-icon> @@ -207,5 +209,16 @@ export default { default: true, }, }, + computed: { + filteredAddresses() { + if (this.showWhenEmpty) { + return this.person.addresses; + } + return this.person.addresses.filter( + (a) => + a.street || a.housenumber || a.postalCode || a.place || a.country, + ); + }, + }, }; </script> diff --git a/aleksis/core/schema/__init__.py b/aleksis/core/schema/__init__.py index 59f0c72d6..209de177d 100644 --- a/aleksis/core/schema/__init__.py +++ b/aleksis/core/schema/__init__.py @@ -1,6 +1,9 @@ from django.apps import apps from django.contrib.auth import get_user_model -from django.contrib.auth.password_validation import validate_password, password_validators_help_texts +from django.contrib.auth.password_validation import ( + password_validators_help_texts, + validate_password, +) from django.contrib.contenttypes.models import ContentType from django.contrib.messages import get_messages from django.core.exceptions import ValidationError -- GitLab From c12a9c5acfed2dafe5b79afc2bbd42339a943cb8 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Thu, 10 Apr 2025 18:10:37 +0200 Subject: [PATCH 53/82] Accept formatted codes --- aleksis/core/schema/person.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 376217349..93ecd7299 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -599,10 +599,11 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat def mutate(cls, root, info, account_registration: AccountRegistrationInputType): invitation = None - if account_registration["invitation_code"]: + if code := account_registration["invitation_code"]: + formatted_code = "".join(code.lower().split("-")) try: invitation = PersonInvitation.objects.get( - key=account_registration["invitation_code"] + key=formatted_code, ) except PersonInvitation.DoesNotExist as exc: raise SuspiciousOperation from exc -- GitLab From 0f0e75c1425b965a987da68ba67542bc5e5c85f1 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Thu, 10 Apr 2025 18:42:24 +0200 Subject: [PATCH 54/82] Hide name from PersonDetailsCard if empty --- aleksis/core/frontend/components/person/PersonDetailsCard.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index 96514a21e..89d1e8617 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -4,7 +4,7 @@ <v-card-title>{{ $t(titleKey) }}</v-card-title> <v-list two-line> - <v-list-item> + <v-list-item v-if="showWhenEmpty || person.firstName || person.additionalName || person.lastName"> <v-list-item-icon> <v-icon> mdi-account-outline</v-icon> </v-list-item-icon> -- GitLab From 97f6e8b0526f9d4f6d5d48fa2c07885c32e9966f Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 13:37:56 +0200 Subject: [PATCH 55/82] Remove Postbuero dependency --- aleksis/core/schema/person.py | 33 --------------------------------- 1 file changed, 33 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 93ecd7299..f12d4b16e 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -1,6 +1,5 @@ from typing import Union -from django.apps import apps from django.contrib.auth import get_user_model from django.contrib.auth.password_validation import validate_password from django.core.exceptions import PermissionDenied, SuspiciousOperation, ValidationError @@ -27,7 +26,6 @@ from ..models import ( PersonRelationship, Role, ) -from ..util.apps import AppConfig from ..util.auth_helpers import custom_username_validators from ..util.core_helpers import get_site_preferences, has_person from .address import AddressType as GraphQLAddressType @@ -43,9 +41,6 @@ from .base import ( from .group import PersonGroupThroughType from .notification import NotificationType -if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)]: - from aleksis.apps.postbuero.models import MailAddress, MailDomain - class PersonPreferencesType(graphene.ObjectType): theme_design_mode = graphene.String() @@ -578,11 +573,6 @@ class PersonBatchPatchMutation( class AccountRegistrationInputType(graphene.InputObjectType): from .user import UserInputType # noqa - if "postbuero" in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)]: - from aleksis.apps.postbuero.schema import MailAddressInputType - - email = graphene.Field(MailAddressInputType, required=False) - person = graphene.Field(PersonInputType, required=True) user = graphene.Field(UserInputType, required=True) invitation_code = graphene.String(required=False) @@ -619,24 +609,6 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat if invitation and invitation.email: email = invitation.email - elif ( - "postbuero" - in [app.label for app in apps.get_app_configs() if isinstance(app, AppConfig)] - and account_registration["email"] is not None - ): - try: - domain = MailDomain.objects.get(pk=account_registration["email"]["domain"]) - except IntegrityError as exc: - raise ValidationError(_("Mail domain does not exist.")) from exc - try: - _mail_address = MailAddress.objects.create( - local_part=account_registration["email"]["local_part"], - domain=domain, - ) - except IntegrityError as exc: - raise ValidationError(_("Mail address already in use.")) from exc - - email = str(_mail_address) elif account_registration["user"] is not None: email = account_registration["user"]["email"] @@ -692,11 +664,6 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Store address information cls._handle_address(root, info, account_registration["person"], person) - # Link person to postbuero mail address, if created - if _mail_address: - _mail_address.person = person - _mail_address.save() - # Accept invitation, if exists if invitation: accept_invitation(invitation, info.context, info.context.user) -- GitLab From 878d25b803bd5a4fededb0285e4b61ed5d1e40fd Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 13:38:14 +0200 Subject: [PATCH 56/82] Store registering person in request --- aleksis/core/schema/person.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index f12d4b16e..f349c0d2d 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -675,4 +675,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat user=person, ) + # Store person in request to make it accessible for injected registration mutations + info.context._registering_person = person + return SendAccountRegistrationMutation(ok=True) -- GitLab From 4135b892e30829f0fe84f192801abecf96c78520 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 14:12:44 +0200 Subject: [PATCH 57/82] Generalize account registration step injection --- .../account/AccountRegistrationForm.vue | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 92280302a..7f2d88317 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -142,10 +142,10 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div class="mb-4"> <!-- TODO: Optional email fields when using injected component --> <component - v-if="postbueroStepEnabled" + v-if="stepOverwrittenByInjection('email')" :is=" collectionSteps.find( - (s) => s.key === 'postbuero-mail-address-form-step', + (s) => s.key === 'email', )?.component " @dataChange="mergeIncomingData" @@ -202,7 +202,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; :step="step" @set-step="setStep" :next-disabled=" - (postbueroStepEnabled && + (stepOverwrittenByInjection('email') && (emailMode === undefined || emailMode === null)) || !validationStatuses['email'] " @@ -769,6 +769,11 @@ export default { }, mixins: [formRulesMixin, permissionsMixin, usernameRulesMixin], methods: { + stepOverwrittenByInjection(step) { + return this.collectionSteps.some( + (s) => s.key === step, + ); + }, setStep(step) { this.step = step; this.valid = false; @@ -850,11 +855,6 @@ export default { }, }, computed: { - postbueroStepEnabled() { - return this.collectionSteps.some( - (s) => s.key === "postbuero-mail-address-form-step", - ); - }, rules() { return { confirmPassword: [ -- GitLab From b88be1f2aac110bbb61784e6d33cb1eba0676c4d Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 14:51:25 +0200 Subject: [PATCH 58/82] WIP: Remove Postbuero-specifics --- .../account/AccountRegistrationForm.vue | 37 +------------------ 1 file changed, 2 insertions(+), 35 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 7f2d88317..b181b61f1 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -149,7 +149,6 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; )?.component " @dataChange="mergeIncomingData" - @emailModeChange="setEmailMode" v-model="validationStatuses['email']" /> <v-form v-else v-model="validationStatuses['email']"> @@ -201,10 +200,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <control-row :step="step" @set-step="setStep" - :next-disabled=" - (stepOverwrittenByInjection('email') && - (emailMode === undefined || emailMode === null)) || - !validationStatuses['email'] + :next-disabled="!validationStatuses['email'] " /> </v-stepper-content> @@ -850,9 +846,6 @@ export default { mergeIncomingData(incomingData) { this.data = this.deepMerge(this.data, incomingData); }, - setEmailMode(emailMode) { - this.emailMode = emailMode; - }, }, computed: { rules() { @@ -878,33 +871,8 @@ export default { user: filteredUserData, }; - if (!this.data.email.localPart && !this.data.email.domain?.id) { - const { email, ...filteredData } = data; - data = filteredData; - } else { - data = { - ...data, - email: { - localPart: data.email.localPart, - domain: data.email.domain.id, - }, - }; - } return data; }, - currentEmail() { - if ( - this.emailMode === 0 && - this.data.email.localPart && - this.data.email.domain - ) { - return `${this.data.email.localPart}@${this.data.email.domain.domain}`; - } else if (this.data.user.email) { - return this.data.user.email; - } else { - return null; - } - }, personDataForSummary() { return { ...this.data.person, @@ -918,7 +886,7 @@ export default { }, ], username: this.data.user.username, - email: this.currentEmail, + email: this.data.user.email, }; }, steps() { @@ -1015,7 +983,6 @@ export default { invitationCodeAutofilled: false, accountRegistrationSent: false, step: 1, - emailMode: null, privacyPolicyAccepted: false, data: { email: { -- GitLab From 7fa45e86d7612820e04ff5269031323ffdb594f8 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 14:51:25 +0200 Subject: [PATCH 59/82] WIP: Remove Postbuero-specifics --- .../account/AccountRegistrationForm.vue | 41 +------------------ 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 7f2d88317..5a1e5ee6d 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -149,7 +149,6 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; )?.component " @dataChange="mergeIncomingData" - @emailModeChange="setEmailMode" v-model="validationStatuses['email']" /> <v-form v-else v-model="validationStatuses['email']"> @@ -201,10 +200,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <control-row :step="step" @set-step="setStep" - :next-disabled=" - (stepOverwrittenByInjection('email') && - (emailMode === undefined || emailMode === null)) || - !validationStatuses['email'] + :next-disabled="!validationStatuses['email'] " /> </v-stepper-content> @@ -850,9 +846,6 @@ export default { mergeIncomingData(incomingData) { this.data = this.deepMerge(this.data, incomingData); }, - setEmailMode(emailMode) { - this.emailMode = emailMode; - }, }, computed: { rules() { @@ -878,33 +871,8 @@ export default { user: filteredUserData, }; - if (!this.data.email.localPart && !this.data.email.domain?.id) { - const { email, ...filteredData } = data; - data = filteredData; - } else { - data = { - ...data, - email: { - localPart: data.email.localPart, - domain: data.email.domain.id, - }, - }; - } return data; }, - currentEmail() { - if ( - this.emailMode === 0 && - this.data.email.localPart && - this.data.email.domain - ) { - return `${this.data.email.localPart}@${this.data.email.domain.domain}`; - } else if (this.data.user.email) { - return this.data.user.email; - } else { - return null; - } - }, personDataForSummary() { return { ...this.data.person, @@ -918,7 +886,7 @@ export default { }, ], username: this.data.user.username, - email: this.currentEmail, + email: this.data.user.email, }; }, steps() { @@ -1015,13 +983,8 @@ export default { invitationCodeAutofilled: false, accountRegistrationSent: false, step: 1, - emailMode: null, privacyPolicyAccepted: false, data: { - email: { - localPart: "", - domain: {}, - }, person: { firstName: "", additionalName: "", -- GitLab From 8e08626685e2c4d66dec86ef5149c0c2f64a7b5d Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 11 Apr 2025 16:25:51 +0200 Subject: [PATCH 60/82] Refactor data structure --- .../account/AccountRegistrationForm.vue | 141 +++++++++--------- 1 file changed, 67 insertions(+), 74 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 5a1e5ee6d..52f8191be 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -54,7 +54,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="invitationCodeRequired"> <v-text-field outlined - v-model="data.invitationCode" + v-model="data.accountRegistration.invitationCode" :label=" $t( 'accounts.signup.form.steps.invitation.fields.invitation_code.label', @@ -157,7 +157,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('email')"> <v-text-field outlined - v-model="data.user.email" + v-model="data.accountRegistration.user.email" :label=" $t( 'accounts.signup.form.steps.email.fields.email.label', @@ -177,7 +177,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('email')"> <v-text-field outlined - v-model="data.user.confirmEmail" + v-model="confirmFields.email" :label=" $t( 'accounts.signup.form.steps.email.fields.confirm_email.label', @@ -214,7 +214,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div aria-required="true"> <v-text-field outlined - v-model="data.user.username" + v-model="data.accountRegistration.user.username" :label=" $t( 'accounts.signup.form.steps.account.fields.username.label', @@ -237,7 +237,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div aria-required="true"> <password-field outlined - v-model="data.user.password" + v-model="data.accountRegistration.user.password" :label=" $t( 'accounts.signup.form.steps.account.fields.password.label', @@ -253,7 +253,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div aria-required="true"> <v-text-field outlined - v-model="data.user.confirmPassword" + v-model="confirmFields.password" :label=" $t( 'accounts.signup.form.steps.account.fields.confirm_password.label', @@ -288,7 +288,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div aria-required="true"> <v-text-field outlined - v-model="data.person.firstName" + v-model="data.accountRegistration.person.firstName" :label=" $t( 'accounts.signup.form.steps.base_data.fields.first_name.label', @@ -303,7 +303,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('additional_name')"> <v-text-field outlined - v-model="data.person.additionalName" + v-model="data.accountRegistration.person.additionalName" :label=" $t( 'accounts.signup.form.steps.base_data.fields.additional_name.label', @@ -322,7 +322,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div aria-required="true"> <v-text-field outlined - v-model="data.person.lastName" + v-model="data.accountRegistration.person.lastName" :label=" $t( 'accounts.signup.form.steps.base_data.fields.last_name.label', @@ -355,7 +355,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('street')"> <v-text-field outlined - v-model="data.person.street" + v-model="data.accountRegistration.person.street" :label=" $t( 'accounts.signup.form.steps.address_data.fields.street.label', @@ -374,7 +374,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('housenumber')"> <v-text-field outlined - v-model="data.person.housenumber" + v-model="data.accountRegistration.person.housenumber" :label=" $t( 'accounts.signup.form.steps.address_data.fields.housenumber.label', @@ -395,7 +395,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('postal_code')"> <v-text-field outlined - v-model="data.person.postalCode" + v-model="data.accountRegistration.person.postalCode" :label=" $t( 'accounts.signup.form.steps.address_data.fields.postal_code.label', @@ -414,7 +414,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('place')"> <v-text-field outlined - v-model="data.person.place" + v-model="data.accountRegistration.person.place" :label=" $t( 'accounts.signup.form.steps.address_data.fields.place.label', @@ -433,7 +433,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('country')"> <country-field outlined - v-model="data.person.country" + v-model="data.accountRegistration.person.country" :label=" $t( 'accounts.signup.form.steps.address_data.fields.country.label', @@ -474,7 +474,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('mobile_number')"> <v-text-field outlined - v-model="data.person.mobileNumber" + v-model="data.accountRegistration.person.mobileNumber" :label=" $t( 'accounts.signup.form.steps.contact_data.fields.mobile_number.label', @@ -494,7 +494,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('phone_number')"> <v-text-field outlined - v-model="data.person.phoneNumber" + v-model="data.accountRegistration.person.phoneNumber" :label=" $t( 'accounts.signup.form.steps.contact_data.fields.phone_number.label', @@ -536,7 +536,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('date_of_birth')"> <date-field outlined - v-model="data.person.dateOfBirth" + v-model="data.accountRegistration.person.dateOfBirth" :label=" $t( 'accounts.signup.form.steps.additional_data.fields.date_of_birth.label', @@ -560,7 +560,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('place_of_birth')"> <v-text-field outlined - v-model="data.person.placeOfBirth" + v-model="data.accountRegistration.person.placeOfBirth" :label=" $t( 'accounts.signup.form.steps.additional_data.fields.place_of_birth.label', @@ -581,7 +581,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('sex')"> <sex-select outlined - v-model="data.person.sex" + v-model="data.accountRegistration.person.sex" :label=" $t( 'accounts.signup.form.steps.additional_data.fields.sex.label', @@ -600,7 +600,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('photo')"> <file-field outlined - v-model="data.person.photo" + v-model="data.accountRegistration.person.photo" accept="image/jpeg, image/png" :label=" $t( @@ -622,7 +622,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <div :aria-required="isFieldRequired('description')"> <v-text-field outlined - v-model="data.person.description" + v-model="data.accountRegistration.person.description" :label=" $t( 'accounts.signup.form.steps.additional_data.fields.description.label', @@ -699,9 +699,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <ApolloMutation :mutation="require('./accountRegistrationMutation.graphql')" - :variables="{ - accountRegistration: dataForSubmit, - }" + :variables="data" @done="accountRegistrationDone" > <template #default="{ mutate, loading, error }"> @@ -739,6 +737,8 @@ import formRulesMixin from "../../mixins/formRulesMixin"; import permissionsMixin from "../../mixins/permissions"; import usernameRulesMixin from "../../mixins/usernameRulesMixin"; +import gql from "graphql-tag"; + export default { name: "AccountRegistrationForm", apollo: { @@ -749,7 +749,7 @@ export default { query: gqlPersonInvitationByCode, variables() { return { - code: this.data.invitationCode, + code: this.data.accountRegistration.invitationCode, }; }, result({ data, loading, networkStatus }) { @@ -776,7 +776,7 @@ export default { }, checkInvitationCode() { this.invitationCodeInvalid = false; - if (this.data.invitationCode) { + if (this.data.accountRegistration.invitationCode) { this.$apollo.queries.personInvitationByCode.skip = false; this.$apollo.queries.personInvitationByCode.refetch(); } else { @@ -852,41 +852,30 @@ export default { return { confirmPassword: [ (v) => - this.data.user.password == v || + this.data.accountRegistration.user.password == v || this.$t("accounts.signup.form.rules.confirm_password.no_match"), ], confirmEmail: [ (v) => - this.data.user.email == v || + this.data.accountRegistration.user.email == v || this.$t("accounts.signup.form.rules.confirm_email.no_match"), ], }; }, - dataForSubmit() { - const { confirmEmail, confirmPassword, ...filteredUserData } = - this.data.user; - - let data = { - ...this.data, - user: filteredUserData, - }; - - return data; - }, personDataForSummary() { return { - ...this.data.person, + ...this.data.accountRegistration.person, addresses: [ { - street: this.data.person.street, - housenumber: this.data.person.housenumber, - postalCode: this.data.person.postalCode, - place: this.data.person.place, - country: this.data.person.country, + street: this.data.accountRegistration.person.street, + housenumber: this.data.accountRegistration.person.housenumber, + postalCode: this.data.accountRegistration.person.postalCode, + place: this.data.accountRegistration.person.place, + country: this.data.accountRegistration.person.country, }, ], - username: this.data.user.username, - email: this.data.user.email, + username: this.data.accountRegistration.user.username, + email: this.data.accountRegistration.user.email, }; }, steps() { @@ -960,7 +949,7 @@ export default { return []; }, invitationNextI18nKey() { - return this.data.invitationCode + return this.data.accountRegistration.invitationCode ? "accounts.signup.form.steps.invitation.next.with_code" : this.invitationCodeRequired ? "accounts.signup.form.steps.invitation.next.code_required" @@ -984,33 +973,37 @@ export default { accountRegistrationSent: false, step: 1, privacyPolicyAccepted: false, + confirmFields: { + email: "", + password: "", + }, data: { - person: { - firstName: "", - additionalName: "", - lastName: "", - shortName: "", - dateOfBirth: null, - placeOfBirth: "", - sex: "", - street: "", - housenumber: "", - postalCode: "", - place: "", - country: "", - mobileNumber: "", - phoneNumber: "", - description: "", - photo: null, - }, - user: { - username: "", - email: "", - confirmEmail: "", - password: "", - confirmPassword: "", + accountRegistration: { + person: { + firstName: "", + additionalName: "", + lastName: "", + shortName: "", + dateOfBirth: null, + placeOfBirth: "", + sex: "", + street: "", + housenumber: "", + postalCode: "", + place: "", + country: "", + mobileNumber: "", + phoneNumber: "", + description: "", + photo: null, + }, + user: { + username: "", + email: "", + password: "", + }, + invitationCode: "", }, - invitationCode: "", }, }; }, @@ -1023,7 +1016,7 @@ export default { mounted() { this.addPermissions(["core.signup_rule", "core.invite_enabled"]); if (this.$route.query.invitation_code) { - this.data.invitationCode = this.$route.query.invitation_code; + this.data.accountRegistration.invitationCode = this.$route.query.invitation_code; this.invitationCodeAutofilled = true; } }, -- GitLab From 8d1e1322fc02b3d54e4f2ee09a7279545d84094e Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 11 Apr 2025 17:22:17 +0200 Subject: [PATCH 61/82] Allow dynamic merging of registration mutation --- aleksis/core/frontend/collections.js | 4 +++ .../account/AccountRegistrationForm.vue | 31 ++++++++++++++++--- aleksis/core/settings.py | 1 + 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/aleksis/core/frontend/collections.js b/aleksis/core/frontend/collections.js index cd439f4f7..a310de146 100644 --- a/aleksis/core/frontend/collections.js +++ b/aleksis/core/frontend/collections.js @@ -25,6 +25,10 @@ export const collections = [ name: "accountRegistrationSteps", type: Object, }, + { + name: "accountRegistrationExtraMutations", + type: Object, + }, ]; export const collectionItems = { diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 52f8191be..95dc65ad7 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -698,7 +698,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; </div> <ApolloMutation - :mutation="require('./accountRegistrationMutation.graphql')" + :mutation="combinedMutation" :variables="data" @done="accountRegistrationDone" > @@ -731,13 +731,14 @@ import { gqlAccountWizardSystemProperties, gqlPersonInvitationByCode, } from "./helpers.graphql"; +import { sendAccountRegistration } from "./accountRegistrationMutation.graphql"; import { collections } from "aleksisAppImporter"; import formRulesMixin from "../../mixins/formRulesMixin"; import permissionsMixin from "../../mixins/permissions"; import usernameRulesMixin from "../../mixins/usernameRulesMixin"; -import gql from "graphql-tag"; +import combineQuery from "@/graphql-combine-query"; export default { name: "AccountRegistrationForm", @@ -831,9 +832,9 @@ export default { (merged, [key, value]) => { if (typeof value === "object") { if (Array.isArray(value)) { - merged[key] = this.deepMerge([], value); + merged[key] = this.deepMerge(existing[key] || [], value); } else { - merged[key] = this.deepMerge({}, value); + merged[key] = this.deepMerge(existing[key] || [], value); } } else { merged[key] = value; @@ -948,6 +949,12 @@ export default { } return []; }, + collectionExtraMutations() { + if (Object.hasOwn(collections, "coreAccountRegistrationExtraMutations")) { + return collections.coreAccountRegistrationExtraMutations.items; + } + return []; + }, invitationNextI18nKey() { return this.data.accountRegistration.invitationCode ? "accounts.signup.form.steps.invitation.next.with_code" @@ -961,6 +968,22 @@ export default { !this.checkPermission("core.signup_rule") ); }, + combinedMutation() { + let combinedQuery = combineQuery("combinedMutation") + .add(sendAccountRegistration); + + this.collectionExtraMutations.forEach( + (extraMutation) => { + if (Object.hasOwn(extraMutation, "mutation")) { + combinedQuery = combinedQuery.add(extraMutation.mutation); + } + } + ); + + const { document, variables } = combinedQuery; + + return document; + }, }, data() { return { diff --git a/aleksis/core/settings.py b/aleksis/core/settings.py index 0bb5ddb12..17918cf3c 100644 --- a/aleksis/core/settings.py +++ b/aleksis/core/settings.py @@ -616,6 +616,7 @@ YARN_INSTALLED_APPS = [ "apollo-upload-client@^18.0.1", "@vitejs/plugin-legacy@^6.0.0", "terser@^5.37.0", + "graphql-combine-query@^1.2.4", ] merge_app_settings("YARN_INSTALLED_APPS", YARN_INSTALLED_APPS, True) -- GitLab From fa140c2261ac8b298316855b37086613f1260798 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 11 Apr 2025 17:26:29 +0200 Subject: [PATCH 62/82] Remove unused variable --- aleksis/core/schema/person.py | 1 - 1 file changed, 1 deletion(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index f349c0d2d..b28b69307 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -605,7 +605,6 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Create email email = None - _mail_address = None if invitation and invitation.email: email = invitation.email -- GitLab From 12cff9ec616d2121fb56f2bb0b5a62e6cc76959c Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 11 Apr 2025 17:42:18 +0200 Subject: [PATCH 63/82] Reformat --- .../account/AccountRegistrationForm.vue | 31 +++++++------------ .../components/person/PersonDetailsCard.vue | 9 +++++- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 95dc65ad7..943d92b92 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -143,11 +143,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <!-- TODO: Optional email fields when using injected component --> <component v-if="stepOverwrittenByInjection('email')" - :is=" - collectionSteps.find( - (s) => s.key === 'email', - )?.component - " + :is="collectionSteps.find((s) => s.key === 'email')?.component" @dataChange="mergeIncomingData" v-model="validationStatuses['email']" /> @@ -200,8 +196,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <control-row :step="step" @set-step="setStep" - :next-disabled="!validationStatuses['email'] - " + :next-disabled="!validationStatuses['email']" /> </v-stepper-content> @@ -767,9 +762,7 @@ export default { mixins: [formRulesMixin, permissionsMixin, usernameRulesMixin], methods: { stepOverwrittenByInjection(step) { - return this.collectionSteps.some( - (s) => s.key === step, - ); + return this.collectionSteps.some((s) => s.key === step); }, setStep(step) { this.step = step; @@ -969,16 +962,15 @@ export default { ); }, combinedMutation() { - let combinedQuery = combineQuery("combinedMutation") - .add(sendAccountRegistration); + let combinedQuery = combineQuery("combinedMutation").add( + sendAccountRegistration, + ); - this.collectionExtraMutations.forEach( - (extraMutation) => { - if (Object.hasOwn(extraMutation, "mutation")) { - combinedQuery = combinedQuery.add(extraMutation.mutation); - } + this.collectionExtraMutations.forEach((extraMutation) => { + if (Object.hasOwn(extraMutation, "mutation")) { + combinedQuery = combinedQuery.add(extraMutation.mutation); } - ); + }); const { document, variables } = combinedQuery; @@ -1039,7 +1031,8 @@ export default { mounted() { this.addPermissions(["core.signup_rule", "core.invite_enabled"]); if (this.$route.query.invitation_code) { - this.data.accountRegistration.invitationCode = this.$route.query.invitation_code; + this.data.accountRegistration.invitationCode = + this.$route.query.invitation_code; this.invitationCodeAutofilled = true; } }, diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index 89d1e8617..65f41c1ba 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -4,7 +4,14 @@ <v-card-title>{{ $t(titleKey) }}</v-card-title> <v-list two-line> - <v-list-item v-if="showWhenEmpty || person.firstName || person.additionalName || person.lastName"> + <v-list-item + v-if=" + showWhenEmpty || + person.firstName || + person.additionalName || + person.lastName + " + > <v-list-item-icon> <v-icon> mdi-account-outline</v-icon> </v-list-item-icon> -- GitLab From 165504777f52b1884801a09cdd2f00566afeed68 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 11 Apr 2025 21:00:51 +0200 Subject: [PATCH 64/82] Fix confirm button disabled status --- .../components/account/AccountRegistrationForm.vue | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 943d92b92..c7e54d94c 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -704,10 +704,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; @set-step="setStep" @confirm="mutate" :next-loading="loading" - :next-disabled=" - systemProperties.sitePreferences.footerPrivacyUrl && - !privacyPolicyAccepted - " + :next-disabled="disableConfirm" /> <v-alert v-if="error" type="error" outlined class="mt-4">{{ error.message @@ -976,6 +973,12 @@ export default { return document; }, + disableConfirm() { + return ( + !!this?.systemProperties?.sitePreferences?.footerPrivacyUrl && + !this.privacyPolicyAccepted + ); + }, }, data() { return { -- GitLab From 07b68699b029f6ebc7711b96e608e81e03514325 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 21:20:39 +0200 Subject: [PATCH 65/82] Indicate registration process to other mutations --- aleksis/core/schema/person.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index b28b69307..4ef10d7ab 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -587,6 +587,9 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat @classmethod @transaction.atomic def mutate(cls, root, info, account_registration: AccountRegistrationInputType): + # Initialize registering person to indicate that registration is in progress + info.context._registering_person = None + invitation = None if code := account_registration["invitation_code"]: -- GitLab From 08a0eb88cb7b2ac69b41a2f138bebccf86371359 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 22:07:35 +0200 Subject: [PATCH 66/82] Show and save additional data also to invited persons --- .../account/AccountRegistrationForm.vue | 9 +++---- aleksis/core/schema/person.py | 26 ++++++++++--------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index c7e54d94c..1f31df18d 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -891,8 +891,7 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && - this.isFieldVisible("street") | + ...(this.isFieldVisible("street") | this.isFieldVisible("housenumber") | this.isFieldVisible("postal_code") | this.isFieldVisible("place") | @@ -904,8 +903,7 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && - this.isFieldVisible("mobile_number") | + ...(this.isFieldVisible("mobile_number") | this.isFieldVisible("phone_number") ? [ { @@ -914,8 +912,7 @@ export default { }, ] : []), - ...(!this.invitation?.hasPerson && - this.isFieldVisible("date_of_birth") | + ...(this.isFieldVisible("date_of_birth") | this.isFieldVisible("place_of_birth") | this.isFieldVisible("sex") | this.isFieldVisible("photo") | diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 4ef10d7ab..259530513 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -653,18 +653,20 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat _("A person using the e-mail address %s already exists.") % email ) from exc - # Store contact information in database - for field in Person._meta.get_fields(): - if ( - field.name in account_registration["person"] - and account_registration["person"][field.name] is not None - and account_registration["person"][field.name] != "" - ): - setattr(person, field.name, account_registration["person"][field.name]) - person.save() - - # Store address information - cls._handle_address(root, info, account_registration["person"], person) + # Store contact information in database + person.email = email + + for field in Person._meta.get_fields(): + if ( + field.name in account_registration["person"] + and account_registration["person"][field.name] is not None + and account_registration["person"][field.name] != "" + ): + setattr(person, field.name, account_registration["person"][field.name]) + person.save() + + # Store address information + cls._handle_address(root, info, account_registration["person"], person) # Accept invitation, if exists if invitation: -- GitLab From fe3a02d75ef2320eed6f811826c3f73d7a2f9b2a Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 11 Apr 2025 22:32:43 +0200 Subject: [PATCH 67/82] Do not overwrite address fields if not set in mutation --- aleksis/core/schema/person.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 259530513..dbe333b19 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -410,9 +410,9 @@ class PersonAddressMutationMixin: address.address_types.add(address_type) for key in cls.ADDRESS_FIELDS: - if key not in input: + if key not in input or not input[key]: continue - setattr(address, key, input[key] or "") + setattr(address, key, input[key]) address.full_clean() address.save() -- GitLab From 43b09d036ae526797765bb92e269d846a27f2855 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Mon, 14 Apr 2025 08:49:58 +0200 Subject: [PATCH 68/82] Fix registration for invited persons without email --- aleksis/core/schema/person.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 4ef10d7ab..7882dfc77 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -636,6 +636,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat # Create person if no invitation is given or if invitation isn't linked to a person if invitation and invitation.person: person = invitation.person + person.email = email person.user = user person.save() else: -- GitLab From 1d13369e93ebf0a65d98598c854234bc9cd950b0 Mon Sep 17 00:00:00 2001 From: Nik | Klampfradler <dominik.george@teckids.org> Date: Wed, 30 Apr 2025 19:27:17 +0000 Subject: [PATCH 69/82] Fix code style issues --- .../core/frontend/components/person/PersonDetailsCard.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/aleksis/core/frontend/components/person/PersonDetailsCard.vue b/aleksis/core/frontend/components/person/PersonDetailsCard.vue index 65f41c1ba..6dc27dba8 100644 --- a/aleksis/core/frontend/components/person/PersonDetailsCard.vue +++ b/aleksis/core/frontend/components/person/PersonDetailsCard.vue @@ -13,7 +13,7 @@ " > <v-list-item-icon> - <v-icon> mdi-account-outline</v-icon> + <v-icon>mdi-account-outline</v-icon> </v-list-item-icon> <v-list-item-content> @@ -31,7 +31,7 @@ <v-list-item v-if="showUsername"> <v-list-item-icon> - <v-icon> mdi-login-variant</v-icon> + <v-icon>mdi-login-variant</v-icon> </v-list-item-icon> <v-list-item-content> @@ -47,13 +47,13 @@ <v-list-item v-if="showWhenEmpty || person.sex"> <v-list-item-icon> - <v-icon> mdi-human-non-binary</v-icon> + <v-icon>mdi-human-non-binary</v-icon> </v-list-item-icon> <v-list-item-content> <v-list-item-title> {{ - person.sex ? $t("person.sex." + person.sex.toLowerCase()) : "–" + person.sex ? $t(`person.sex.${person.sex.toLowerCase()}`) : "–" }} </v-list-item-title> <v-list-item-subtitle> -- GitLab From 0252b6a456e71200b068f2d160f35478ebe43913 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 13:19:50 +0200 Subject: [PATCH 70/82] Use more consistent query naming --- aleksis/core/frontend/components/app/systemProperties.graphql | 2 +- aleksis/core/frontend/mixins/usernameRulesMixin.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aleksis/core/frontend/components/app/systemProperties.graphql b/aleksis/core/frontend/components/app/systemProperties.graphql index 060100171..39d4d8678 100644 --- a/aleksis/core/frontend/components/app/systemProperties.graphql +++ b/aleksis/core/frontend/components/app/systemProperties.graphql @@ -28,7 +28,7 @@ query gqlSystemProperties { } } -query gqlUsernamePreferences { +query gqlUsernameSystemProperties { usernameSystemProperties: systemProperties { sitePreferences { authAllowedUsernameRegex diff --git a/aleksis/core/frontend/mixins/usernameRulesMixin.js b/aleksis/core/frontend/mixins/usernameRulesMixin.js index ce866f433..43df2e766 100644 --- a/aleksis/core/frontend/mixins/usernameRulesMixin.js +++ b/aleksis/core/frontend/mixins/usernameRulesMixin.js @@ -1,11 +1,11 @@ -import { gqlUsernamePreferences } from "../components/app/systemProperties.graphql"; +import { gqlUsernameSystemProperties } from "../components/app/systemProperties.graphql"; /** * This mixin provides rules checking whether a string conforms with the requiremends set out for usernames set in the site preferences. */ export default { apollo: { - usernameSystemProperties: gqlUsernamePreferences, + usernameSystemProperties: gqlUsernameSystemProperties, }, computed: { usernameRules() { -- GitLab From d80d7b144ff4c6b19d0dd9836d8b2ec716076b84 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 13:40:33 +0200 Subject: [PATCH 71/82] Hard code signup url with invitation code --- aleksis/core/models.py | 5 ++--- aleksis/core/urls.py | 5 ----- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index 16e3aeaac..c77197261 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -1433,9 +1433,8 @@ class PersonInvitation(AbstractBaseInvitation, PureDjangoModel): def send_invitation(self, request, **kwargs): """Send the invitation email to the person.""" - # TODO: Use correct URL to new signup wizard - invite_url = f"{reverse('account_signup')}?invitation_code={self.key}" - invite_url = request.build_absolute_uri(invite_url).replace("/django", "") + invite_url = f"/accounts/signup?invitation_code={self.key}" + invite_url = request.build_absolute_uri(invite_url) context = kwargs context.update( { diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 40a2445fc..228b9e93c 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -107,11 +107,6 @@ urlpatterns = [ views.DAVSingleResourceView.as_view(), name="dav_resource_contact", ), - path( - "accounts/signup/", - views.TemplateView.as_view(template_name="core/vue_index.html"), - name="account_signup", - ), path("", include("django_prometheus.urls")), path( "django/", -- GitLab From 9ae502bb6bb50870410ac1ecc9f8d70ba21cc635 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 13:42:31 +0200 Subject: [PATCH 72/82] Drop legacy invitation code view --- aleksis/core/forms.py | 9 ---- aleksis/core/templates/invitations/enter.html | 44 ------------------- aleksis/core/urls.py | 5 --- aleksis/core/views.py | 19 -------- 4 files changed, 77 deletions(-) delete mode 100644 aleksis/core/templates/invitations/enter.html diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index e89d59404..6515f40e6 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -128,15 +128,6 @@ DashboardWidgetOrderFormSet = forms.formset_factory( ) -class InvitationCodeForm(forms.Form): - """Form to enter an invitation code.""" - - code = forms.CharField( - label=_("Invitation code"), - help_text=_("Please enter your invitation code."), - ) - - class PersonCreateInviteForm(InviteForm): """Custom form to create a person and invite them.""" diff --git a/aleksis/core/templates/invitations/enter.html b/aleksis/core/templates/invitations/enter.html deleted file mode 100644 index 03605a710..000000000 --- a/aleksis/core/templates/invitations/enter.html +++ /dev/null @@ -1,44 +0,0 @@ -{# -*- engine:django -*- #} - -{% extends "core/base.html" %} - -{% load material_form i18n %} - -{% block browser_title %}{% blocktrans %}Accept invitation{% endblocktrans %}{% endblock %} -{% block no_page_title %}{% endblock %} - -{% block extra_head %} - {{ form.media.css }} -{% endblock %} - -{% block content %} - <div class="row"> - <div class="col m1 l2 xl3"></div> - <div class="col s12 m10 l8 xl6"> - <div class="card"> - <form method="post"> - <div class="card-content"> - <div class="card-title">{% trans "Accept your invitation" %}</div> - <div class="alert primary"> - <div> - <i class="material-icons left">info</i> - {% blocktrans %} - Please enter your invitation code to register - your new user account: - {% endblocktrans %} - </div> - </div> - {% csrf_token %} - {% form form=form %}{% endform %} - </div> - <div class="card-action-light"> - <button type="submit" class="btn green waves-effect waves-light"> - <i class="material-icons left">card_giftcard</i> - {% trans "Accept invite" %} - </button> - </div> - </form> - </div> - </div> - {{ form.media.js }} -{% endblock %} diff --git a/aleksis/core/urls.py b/aleksis/core/urls.py index 228b9e93c..c7aedac4e 100644 --- a/aleksis/core/urls.py +++ b/aleksis/core/urls.py @@ -134,11 +134,6 @@ urlpatterns = [ path( "invitations/send-invite/", views.InvitePerson.as_view(), name="invite_person" ), - path( - "invitations/code/enter/", - views.EnterInvitationCode.as_view(), - name="enter_invitation_code", - ), path( "invitations/code/generate/", views.GenerateInvitationCode.as_view(), diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 98360a2dd..5cea1864a 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -707,25 +707,6 @@ class InvitePerson(PermissionRequiredMixin, SingleTableView, SendInvite): return super().get_context_data(**kwargs) -class EnterInvitationCode(FormView): - """View to enter an invitation code.""" - - template_name = "invitations/enter.html" - form_class = InvitationCodeForm - - def form_valid(self, form): - code = "".join(form.cleaned_data["code"].lower().split("-")) - # Check if valid invitations exists - if ( - PersonInvitation.objects.filter(key=code).exists() - and not PersonInvitation.objects.get(key=code).accepted - and not PersonInvitation.objects.get(key=code).key_expired() - ): - self.request.session["invitation_code"] = code - return redirect("account_signup") - return redirect("invitations:accept-invite", code) - - class GenerateInvitationCode(View): """View to generate an invitation code.""" -- GitLab From 30283c0e6b611275dff375097dfbe21ed3a973f3 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 13:45:30 +0200 Subject: [PATCH 73/82] Drop legacy account registration --- aleksis/core/forms.py | 121 --------------------- aleksis/core/templates/account/signup.html | 26 ----- aleksis/core/views.py | 30 ----- 3 files changed, 177 deletions(-) delete mode 100644 aleksis/core/templates/account/signup.html diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 6515f40e6..610badca4 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -268,127 +268,6 @@ class AssignPermissionForm(forms.Form): assign_perm(permission_name, django_group, instance) -class AccountRegisterForm(SignupForm, ExtensibleForm): - """Form to register new user accounts.""" - - class Meta: - model = Person - fields = [ - "first_name", - "additional_name", - "last_name", - "addresses", - "date_of_birth", - "place_of_birth", - "sex", - "photo", - "mobile_number", - "phone_number", - "short_name", - "description", - ] - - layout = Layout( - Fieldset( - _("Base data"), - Row("first_name", "additional_name", "last_name"), - "short_name", - ), - Fieldset( - _("Address data"), - Row("addresses"), - ), - Fieldset(_("Contact data"), Row("mobile_number", "phone_number")), - Fieldset( - _("Additional data"), - Row("date_of_birth", "place_of_birth"), - Row("sex", "photo"), - "description", - ), - Fieldset( - _("Account data"), - "username", - Row("email", "email2"), - Row("password1", "password2"), - ), - ) - - password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput) - - if settings.SIGNUP_PASSWORD_ENTER_TWICE: - password2 = forms.CharField(label=_("Password (again)"), widget=forms.PasswordInput) - - def __init__(self, *args, **kwargs): - request = kwargs.pop("request", None) - super().__init__(*args, **kwargs) - - person = None - if request.session.get("account_verified_email"): - email = request.session["account_verified_email"] - - try: - person = Person.objects.get(email=email) - except (Person.DoesNotExist, Person.MultipleObjectsReturned) as exc: - raise SuspiciousOperation from exc - - elif request.session.get("invitation_code"): - try: - invitation = PersonInvitation.objects.get( - key=request.session.get("invitation_code") - ) - except PersonInvitation.DoesNotExist as exc: - raise SuspiciousOperation from exc - - person = invitation.person - - if person: - self.instance = person - available_fields = [field.name for field in Person._meta.get_fields()] - if person.email: - self.fields["email"].disabled = True - self.fields["email2"].disabled = True - self.fields["email2"].initial = person.email - for field in self.fields: - if field in available_fields and getattr(person, field): - self.fields[field].disabled = True - self.fields[field].initial = getattr(person, field) - - def save(self, request): - adapter = get_adapter(request) - user = adapter.new_user(request) - adapter.save_user(request, user, self) - # Create person - data = {} - for field in Person._meta.get_fields(): - if field.name in self.cleaned_data: - data[field.name] = self.cleaned_data[field.name] - if self.instance: - person_qs = Person.objects.filter(pk=self.instance.pk) - else: - person_qs = Person.objects.filter(email=data["email"]) - if not person_qs.exists() and get_site_preferences()["account__auto_create_person"]: - Person.objects.create(user=user, **data) - if person_qs.exists(): - person = person_qs.first() - for field, value in data.items(): - setattr(person, field, value) - person.user = user - person.save() - invitation_code = request.session.get("invitation_code") - if invitation_code: - from invitations.views import accept_invitation # noqa - - try: - invitation = PersonInvitation.objects.get(key=invitation_code) - except PersonInvitation.DoesNotExist as exc: - raise SuspiciousOperation from exc - - accept_invitation(invitation, request, user) - self.custom_signup(request, user) - setup_user_email(request, user, []) - return user - - class ActionForm(forms.Form): """Generic form for executing actions on multiple items of a queryset. diff --git a/aleksis/core/templates/account/signup.html b/aleksis/core/templates/account/signup.html deleted file mode 100644 index eb606114e..000000000 --- a/aleksis/core/templates/account/signup.html +++ /dev/null @@ -1,26 +0,0 @@ -{% extends "core/base.html" %} - -{% load i18n material_form %} - -{% block browser_title %}{% trans "Signup" %}{% endblock %} -{% block page_title %}{% trans "Signup" %}{% endblock %} - -{% block content %} - <div class="alert warning"> - <p> - <i class="material-icons iconify left" data-icon="mdi:alert-outline"></i> - {% blocktrans %}Already have an account? Then please <a href="{{ login_url }}">sign in</a>.{% endblocktrans %} - </p> - </div> - - <form method="post" action="{% url 'account_signup' %}"> - {% csrf_token %} - {% form form=form %}{% endform %} - {% if redirect_field_value %} - <input type="hidden" name="{{ redirect_field_name }}" value="{{ redirect_field_value }}" /> - {% endif %} - {% trans "Sign up" as caption %} - {% include "core/partials/save_button.html" with caption=caption icon="mdi:account-plus-outline" %} - </form> - -{% endblock %} diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 5cea1864a..599985ffc 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -935,36 +935,6 @@ def server_error( return HttpResponseServerError(template.render(context)) -class AccountRegisterView(SignupView): - """Custom view to register a user account. - - Rewrites dispatch function from allauth to check if signup is open or if the user - has a verified email address from an invitation; otherwise raises permission denied. - """ - - form_class = AccountRegisterForm - success_url = reverse_lazy("index") - - def dispatch(self, request, *args, **kwargs): - if ( - not request.user.has_perm("core.signup_rule") - and not request.session.get("account_verified_email") - and not request.session.get("invitation_code") - ): - raise PermissionDenied() - return super().dispatch(request, *args, **kwargs) - - def get_form_kwargs(self): - kwargs = super().get_form_kwargs() - kwargs["request"] = self.request - return kwargs - - def get_context_data(self, **kwargs): - context = super().get_context_data(**kwargs) - context["login_url"] = reverse(settings.LOGIN_URL) - return context - - class InvitePersonByID(PermissionRequiredMixin, SingleObjectMixin, View): """Custom view to invite person by their ID.""" -- GitLab From cbf7ebe6ace1f1611c5863c95a3161a79ebc100a Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 13:49:45 +0200 Subject: [PATCH 74/82] Drop unneeded imports --- aleksis/core/views.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 599985ffc..8e06b975c 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -85,12 +85,10 @@ from .filters import ( UserObjectPermissionFilter, ) from .forms import ( - AccountRegisterForm, AssignPermissionForm, DashboardWidgetOrderFormSet, EditGroupForm, GroupPreferenceForm, - InvitationCodeForm, MaintenanceModeForm, PersonPreferenceForm, SelectPermissionForm, -- GitLab From 632aa12aa8a12d5045c226219a4f740612146234 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 14:01:56 +0200 Subject: [PATCH 75/82] Reformat --- aleksis/core/forms.py | 8 ++------ .../account/AccountRegistrationForm.vue | 18 +++++++++--------- aleksis/core/static/public/theme.scss | 5 ++--- .../core/templates/templated_email/email.css | 5 ++--- aleksis/core/views.py | 2 +- 5 files changed, 16 insertions(+), 22 deletions(-) diff --git a/aleksis/core/forms.py b/aleksis/core/forms.py index 610badca4..e0a52c461 100644 --- a/aleksis/core/forms.py +++ b/aleksis/core/forms.py @@ -2,16 +2,12 @@ from collections.abc import Sequence from typing import Any, Callable from django import forms -from django.conf import settings from django.contrib.auth.models import Permission -from django.core.exceptions import SuspiciousOperation, ValidationError +from django.core.exceptions import ValidationError from django.db.models import QuerySet from django.http import HttpRequest from django.utils.translation import gettext_lazy as _ -from allauth.account.adapter import get_adapter -from allauth.account.forms import SignupForm -from allauth.account.utils import setup_user_email from django_select2.forms import ModelSelect2MultipleWidget, ModelSelect2Widget from dynamic_preferences.forms import PreferenceForm from guardian.shortcuts import assign_perm @@ -31,7 +27,7 @@ from .registries import ( person_preferences_registry, site_preferences_registry, ) -from .util.core_helpers import get_site_preferences, queryset_rules_filter +from .util.core_helpers import queryset_rules_filter class EditGroupForm(SchoolTermRelatedExtensibleForm): diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 1f31df18d..8b10aa463 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -892,10 +892,10 @@ export default { ] : []), ...(this.isFieldVisible("street") | - this.isFieldVisible("housenumber") | - this.isFieldVisible("postal_code") | - this.isFieldVisible("place") | - this.isFieldVisible("country") + this.isFieldVisible("housenumber") | + this.isFieldVisible("postal_code") | + this.isFieldVisible("place") | + this.isFieldVisible("country") ? [ { name: "address_data", @@ -904,7 +904,7 @@ export default { ] : []), ...(this.isFieldVisible("mobile_number") | - this.isFieldVisible("phone_number") + this.isFieldVisible("phone_number") ? [ { name: "contact_data", @@ -913,10 +913,10 @@ export default { ] : []), ...(this.isFieldVisible("date_of_birth") | - this.isFieldVisible("place_of_birth") | - this.isFieldVisible("sex") | - this.isFieldVisible("photo") | - this.isFieldVisible("description") + this.isFieldVisible("place_of_birth") | + this.isFieldVisible("sex") | + this.isFieldVisible("photo") | + this.isFieldVisible("description") ? [ { name: "additional_data", diff --git a/aleksis/core/static/public/theme.scss b/aleksis/core/static/public/theme.scss index 894450e29..1cddad1c4 100644 --- a/aleksis/core/static/public/theme.scss +++ b/aleksis/core/static/public/theme.scss @@ -295,9 +295,8 @@ $toast-action-color: #eeff41; // 20. Typography // ========================================================================== -$font-stack: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, - Cantarell, "Helvetica Neue", sans-serif !default; +$font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, + Ubuntu, Cantarell, "Helvetica Neue", sans-serif !default; $off-black: rgba(0, 0, 0, 0.87) !default; // Header Styles $h1-fontsize: 4.2rem !default; diff --git a/aleksis/core/templates/templated_email/email.css b/aleksis/core/templates/templated_email/email.css index 75f645fd2..a0ba96813 100644 --- a/aleksis/core/templates/templated_email/email.css +++ b/aleksis/core/templates/templated_email/email.css @@ -1,8 +1,7 @@ body { line-height: 1.5; - font-family: - -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, - Cantarell, "Helvetica Neue", sans-serif; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-weight: normal; color: rgba(0, 0, 0, 0.87); display: flex; diff --git a/aleksis/core/views.py b/aleksis/core/views.py index 8e06b975c..34ea0ef75 100644 --- a/aleksis/core/views.py +++ b/aleksis/core/views.py @@ -41,7 +41,7 @@ from django.views.generic.list import ListView import reversion from allauth.account.utils import has_verified_email, send_email_confirmation -from allauth.account.views import PasswordChangeView, PasswordResetView, SignupView +from allauth.account.views import PasswordChangeView, PasswordResetView from allauth.socialaccount.adapter import get_adapter from allauth.socialaccount.models import SocialAccount from django_celery_results.models import TaskResult -- GitLab From e41b5337443cc3ab95518384549b5f3b378bed7f Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 14:03:06 +0200 Subject: [PATCH 76/82] Add changelog --- CHANGELOG.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d69569cd1..0492093da 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -15,6 +15,7 @@ Changed * Announcements are shown in calendar. * DialogObjectForm is now slightly wider. * [Dev] DialogObjectForm now implicitly handles a missing ``isCreate`` prop depending on the presence of ``editItem``. +* The account registration and invitation code forms were migrated to the new frontend. Fixed ~~~~~ -- GitLab From ac021ba33b9debcb0832e22928de29f8caafccbb Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 14:41:37 +0200 Subject: [PATCH 77/82] Reformat --- aleksis/core/models.py | 15 ++++++++++++--- aleksis/core/static/public/theme.scss | 5 +++-- aleksis/core/templates/templated_email/email.css | 5 +++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/aleksis/core/models.py b/aleksis/core/models.py index dcff00f0a..f1b69c835 100644 --- a/aleksis/core/models.py +++ b/aleksis/core/models.py @@ -486,7 +486,10 @@ class Person(ContactMixin, ExtensibleModel): card.append(f"TEL;TYPE=cell:{self.mobile_number}") # Addresses - if not self._is_unrequested_prop("ADR", params) and self.addresses.exists(): + if ( + not self._is_unrequested_prop("ADR", params) + and self.addresses.exists() + ): for address in self.addresses.all(): address_types = ",".join(address.address_types.values_list("name", flat=True)) card.append( @@ -2306,11 +2309,17 @@ class Organisation(ContactMixin, ExtensibleModel): card.append(f"ORG:{self.name}") # Email - if not self._is_unrequested_prop("EMAIL", params) and self.email: + if ( + not self._is_unrequested_prop("EMAIL", params) + and self.email + ): card.append(f"EMAIL:{self.email}") # Addresses - if not self._is_unrequested_prop("ADR", params) and self.addresses.exists(): + if ( + not self._is_unrequested_prop("ADR", params) + and self.addresses.exists() + ): for address in self.addresses.all(): address_types = ",".join(address.address_types.values_list("name", flat=True)) card.append( diff --git a/aleksis/core/static/public/theme.scss b/aleksis/core/static/public/theme.scss index 1cddad1c4..894450e29 100644 --- a/aleksis/core/static/public/theme.scss +++ b/aleksis/core/static/public/theme.scss @@ -295,8 +295,9 @@ $toast-action-color: #eeff41; // 20. Typography // ========================================================================== -$font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, - Ubuntu, Cantarell, "Helvetica Neue", sans-serif !default; +$font-stack: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, + Cantarell, "Helvetica Neue", sans-serif !default; $off-black: rgba(0, 0, 0, 0.87) !default; // Header Styles $h1-fontsize: 4.2rem !default; diff --git a/aleksis/core/templates/templated_email/email.css b/aleksis/core/templates/templated_email/email.css index a0ba96813..75f645fd2 100644 --- a/aleksis/core/templates/templated_email/email.css +++ b/aleksis/core/templates/templated_email/email.css @@ -1,7 +1,8 @@ body { line-height: 1.5; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, - Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; + font-family: + -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, + Cantarell, "Helvetica Neue", sans-serif; font-weight: normal; color: rgba(0, 0, 0, 0.87); display: flex; -- GitLab From 41a30513285b16c11e8253758090d092f7ff705a Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 14:49:55 +0200 Subject: [PATCH 78/82] Run full_clean before saving person --- aleksis/core/schema/person.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index a4807d918..3be46d000 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -672,6 +672,7 @@ class SendAccountRegistrationMutation(PersonAddressMutationMixin, graphene.Mutat and account_registration["person"][field.name] != "" ): setattr(person, field.name, account_registration["person"][field.name]) + person.full_clean() person.save() # Store address information -- GitLab From 6aa5ef035831976346e65b67b291bd6eba2e6062 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 14:52:25 +0200 Subject: [PATCH 79/82] Remove unused translation string --- aleksis/core/frontend/messages/en.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/aleksis/core/frontend/messages/en.json b/aleksis/core/frontend/messages/en.json index 535442abe..a0d92555b 100644 --- a/aleksis/core/frontend/messages/en.json +++ b/aleksis/core/frontend/messages/en.json @@ -174,9 +174,6 @@ } }, "rules": { - "email": { - "valid": "This is not a valid e-mail address" - }, "confirm_email": { "no_match": "The e-mail addresses do not match" }, -- GitLab From 8c394349e76123796617d06300f4c8405ba9b7a5 Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 15:09:24 +0200 Subject: [PATCH 80/82] Re-add optional address field --- aleksis/core/schema/person.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 3be46d000..435d4d5fb 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -348,6 +348,7 @@ class PersonInputType(graphene.InputObjectType): place_of_birth = graphene.String(required=False) sex = graphene.String(required=False) + address = graphene.Field(AddressInputType, required=False) street = graphene.String(required=False) housenumber = graphene.String(required=False) postal_code = graphene.String(required=False) -- GitLab From a3d826e44c50d3edf9967857e9521f7a56222625 Mon Sep 17 00:00:00 2001 From: magicfelix <felix@felix-zauberer.de> Date: Fri, 2 May 2025 17:36:31 +0200 Subject: [PATCH 81/82] Revert "Do not overwrite address fields if not set in mutation" This reverts commit fe3a02d75ef2320eed6f811826c3f73d7a2f9b2a. --- aleksis/core/schema/person.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aleksis/core/schema/person.py b/aleksis/core/schema/person.py index 435d4d5fb..74b36db33 100644 --- a/aleksis/core/schema/person.py +++ b/aleksis/core/schema/person.py @@ -419,9 +419,9 @@ class PersonAddressMutationMixin: address.address_types.add(address_type) for key in cls.ADDRESS_FIELDS: - if key not in input or not input[key]: + if key not in input: continue - setattr(address, key, input[key]) + setattr(address, key, input[key] or "") address.full_clean() address.save() -- GitLab From c621e4944dad6a85b87b30ea4ff4c23e40468dab Mon Sep 17 00:00:00 2001 From: Hangzhi Yu <hangzhi@protonmail.com> Date: Fri, 2 May 2025 18:45:55 +0200 Subject: [PATCH 82/82] Don't send address fields if empty in registration --- .../account/AccountRegistrationForm.vue | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue index 8b10aa463..0207214ca 100644 --- a/aleksis/core/frontend/components/account/AccountRegistrationForm.vue +++ b/aleksis/core/frontend/components/account/AccountRegistrationForm.vue @@ -694,7 +694,7 @@ import PersonDetailsCard from "../person/PersonDetailsCard.vue"; <ApolloMutation :mutation="combinedMutation" - :variables="data" + :variables="dataForMutation" @done="accountRegistrationDone" > <template #default="{ mutate, loading, error }"> @@ -976,6 +976,29 @@ export default { !this.privacyPolicyAccepted ); }, + dataForMutation() { + // Due to the backend logic used for handling addresses, empty values have to be filtered out + const addressKeys = [ + "street", + "housenumber", + "postalCode", + "place", + "country", + ]; + + const cleanedPerson = Object.fromEntries( + Object.entries(this.data.accountRegistration.person).filter( + ([key, value]) => !(addressKeys.includes(key) && value === ""), + ), + ); + + return { + accountRegistration: { + ...this.data.accountRegistration, + person: cleanedPerson, + }, + }; + }, }, data() { return { -- GitLab