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