Repository: fossasia/eventyay-tickets
Pull request #582: Add Wikimedia Nick Name field
Author: login: Gagan-Ram
Created: 2025-03-15T19:52:52Z | Merged: 2025-03-21T05:16:08Z

This PR updates the user model to include the wikimedia_username field.
Fixes: #558 

###  Changes Made
 - Added a `wikimedia_username` field to the user model.
 - Updated the account settings UI to include the nickname field and set it to read-only.

### Potential Issues / Considerations
 - Since Wikimedia uses the username as the primary key, a single user can create multiple Wikimedia accounts with the same email ID.
 - If a user signs in to our platform using multiple Wikimedia accounts linked to the same email, their nickname will be updated to the latest Wikimedia account they used.
 - However, they won’t be able to sign in as multiple separate users on our platform using those duplicate Wikimedia accounts.

### Screenshot after the update
![Account settings eventyay-common](https://github.com/user-attachments/assets/d9b4c5a3-1aaf-46f8-bc01-7b873b147c0c)

## Summary by Sourcery

Adds a `wikimedia_username` field to the user model and displays it as a read-only "Nick name" field in the account settings UI. The field is automatically populated when a user logs in via Wikimedia.

Enhancements:
- Populate the wikimedia_username field upon user creation or update if the user logs in via Wikimedia.
- Display the Wikimedia username as a read-only "Nick name" field in the account settings UI.


Diff:
diff --git a/.gitignore b/.gitignore
index 731d500cc56..1a2ca2ea1f6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,9 @@ local/
 .vscode/tasks.json
 .vscode/extensions.json
 
+.gnupg
+.ssh
+.vscode-server
 
 # Generated by Docker development containers
 build/
diff --git a/src/pretix/base/forms/user.py b/src/pretix/base/forms/user.py
index 6edd12a6bf2..52279d00603 100644
--- a/src/pretix/base/forms/user.py
+++ b/src/pretix/base/forms/user.py
@@ -46,10 +46,14 @@ class Meta:
         model = User
         fields = [
             'fullname',
+            'wikimedia_username',
             'locale',
             'timezone',
             'email'
         ]
+        labels = {
+            'wikimedia_username': 'Nick name',
+        }
         widgets = {
             'locale': SingleLanguageWidget
         }
@@ -58,6 +62,7 @@ def __init__(self, *args, **kwargs):
         self.user = kwargs.pop('user')
         super().__init__(*args, **kwargs)
         self.fields['email'].required = True
+        self.fields['wikimedia_username'].widget.attrs['readonly'] = True
         if self.user.auth_backend != 'native':
             del self.fields['old_pw']
             del self.fields['new_pw']
diff --git a/src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py b/src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py
new file mode 100644
index 00000000000..eb516d9b393
--- /dev/null
+++ b/src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.1.7 on 2025-03-15 09:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pretixbase', '0008_cartposition_job_title_orderposition_job_title'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='wikimedia_username',
+            field=models.CharField(max_length=255, null=True),
+        ),
+    ]
diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py
index 81009ce8d74..51f4d7c344b 100644
--- a/src/pretix/base/models/auth.py
+++ b/src/pretix/base/models/auth.py
@@ -87,6 +87,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
                               verbose_name=_('E-mail'), max_length=190)
     fullname = models.CharField(max_length=255, blank=True, null=True,
                                 verbose_name=_('Full name'))
+    wikimedia_username = models.CharField(max_length=255, blank=True, null=True,
+                                     verbose_name=('Wikimedia username'))
     is_active = models.BooleanField(default=True,
                                     verbose_name=_('Is active'))
     is_staff = models.BooleanField(default=False,
@@ -162,10 +164,13 @@ def get_full_name(self) -> str:
         Returns the first of the following user properties that is found to exist:
 
         * Full name
+        * Wikimedia username
         * Email address
         """
         if self.fullname:
             return self.fullname
+        elif self.wikimedia_username:
+            return self.wikimedia_username
         else:
             return self.email
 
diff --git a/src/pretix/control/templates/pretixcontrol/user/settings.html b/src/pretix/control/templates/pretixcontrol/user/settings.html
index 139bfcbd5e8..9d4ec2a84a8 100644
--- a/src/pretix/control/templates/pretixcontrol/user/settings.html
+++ b/src/pretix/control/templates/pretixcontrol/user/settings.html
@@ -10,6 +10,7 @@ <h1>{% trans "Account settings" %}</h1>
     <fieldset>
         <legend>{% trans "General settings" %}</legend>
         {% bootstrap_field form.fullname layout='horizontal' %}
+        {% bootstrap_field form.wikimedia_username layout='horizontal' readonly='readonly' %}
         {% bootstrap_field form.locale layout='horizontal' %}
         {% bootstrap_field form.timezone layout='horizontal' %}
         <div class="form-group">
diff --git a/src/pretix/eventyay_common/templates/eventyay_common/account/settings.html b/src/pretix/eventyay_common/templates/eventyay_common/account/settings.html
index 779cf1de5e2..c6a7a40e0d9 100644
--- a/src/pretix/eventyay_common/templates/eventyay_common/account/settings.html
+++ b/src/pretix/eventyay_common/templates/eventyay_common/account/settings.html
@@ -10,6 +10,7 @@ <h1>{% trans "Account settings" %}</h1>
     <fieldset>
         <legend>{% trans "General settings" %}</legend>
         {% bootstrap_field form.fullname layout='horizontal' %}
+        {% bootstrap_field form.wikimedia_username layout='horizontal' readonly='readonly' %}
         {% bootstrap_field form.locale layout='horizontal' %}
         {% bootstrap_field form.timezone layout='horizontal' %}
         <div class="form-group">
diff --git a/src/pretix/plugins/socialauth/views.py b/src/pretix/plugins/socialauth/views.py
index 62bdddd2587..436a8e3c2ac 100644
--- a/src/pretix/plugins/socialauth/views.py
+++ b/src/pretix/plugins/socialauth/views.py
@@ -104,15 +104,30 @@ def get_or_create_user(request: HttpRequest) -> User:
         """
         Get or create a user from social auth information.
         """
-        return User.objects.get_or_create(
+        social_account = request.user.socialaccount_set.filter(provider="mediawiki").last()  # Fetch only the latest signed in Wikimedia account
+        wikimedia_username = ""
+
+        if social_account:
+            extra_data = social_account.extra_data
+            wikimedia_username = extra_data.get("username", extra_data.get("realname", ""))
+
+        user, created = User.objects.get_or_create(
             email=request.user.email,
             defaults={
                 "locale": getattr(request, "LANGUAGE_CODE", settings.LANGUAGE_CODE),
                 "timezone": getattr(request, "timezone", settings.TIME_ZONE),
                 "auth_backend": "native",
                 "password": "",
+                "wikimedia_username": wikimedia_username,
             },
-        )[0]
+        )
+
+        # Update wikimedia_username if the user exists but has no wikimedia_username value set (Basically our existing users), or if the user has updated his username in his wikimedia account
+        if not created and (not user.wikimedia_username or user.wikimedia_username != wikimedia_username):
+            user.wikimedia_username = wikimedia_username
+            user.save()
+        
+        return user
 
 
 class SocialLoginView(AdministratorPermissionRequiredMixin, TemplateView):


Reviews:
user: login: sourcery-ai[bot]
body: Hey @Gagan-Ram - I've reviewed your changes - here's some feedback:

**Overall Comments**:
- Consider adding a unique constraint to the `wikimedia_username` field to prevent multiple users from having the same username.
- The migration file includes changes to all models, but it should only include changes related to the `User` model.

<details>
<summary>Here's what I looked at during the review</summary>

- 🟢 **General issues**: all looks good
- 🟢 **Security**: all looks good
- 🟢 **Testing**: all looks good
- 🟡 **Complexity**: 1 issue found
- 🟢 **Documentation**: all looks good
</details>

***

<details>
<summary>Sourcery is free for open source - if you like our reviews please consider sharing them ✨</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20got%20an%20instant%20code%20review%20from%20%40SourceryAI%2C%20and%20it%20was%20brilliant%21%20It%27s%20free%20for%20open%20source%20and%20has%20a%20free%20trial%20for%20private%20code.%20Check%20it%20out%20https%3A//sourcery.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https://sourcery.ai)
- [Facebook](https://www.facebook.com/sharer/sharer.php?u=https://sourcery.ai)

</details>

<sub>
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T19:53:52Z
user: login: Gagan-Ram
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-16T08:48:35Z
user: login: sourcery-ai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-16T08:48:43Z
user: login: hongquan
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-20T05:16:26Z
user: login: Gagan-Ram
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-20T08:37:02Z
user: login: hongquan
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-21T05:15:45Z

Review comments:
diff_hunk: @@ -0,0 +1,313 @@
+# Generated by Django 5.1.7 on 2025-03-15 09:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pretixbase', '0008_cartposition_job_title_orderposition_job_title'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='wikimedia_username',
+            field=models.CharField(max_length=255, null=True),
+        ),
+        migrations.AlterField(
path: src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py
user: login: sourcery-ai[bot]
body: **issue (complexity):** Consider using a loop to generate the AlterField operations for each model name to reduce repetition and improve maintainability of the migration file.

You can reduce this repetition by listing all affected model names and generating the corresponding operations in a loop. For example, instead of manually writing a block for each model, you could write:

```python
# Generated by Django 5.1.7 on 2025-03-15 09:17
from django.db import migrations, models

class Migration(migrations.Migration):

    dependencies = [
        ('pretixbase', '0008_cartposition_job_title_orderposition_job_title'),
    ]

    def generate_alter_field_operations():
        model_names = [
            'cachedcombinedticket', 'cachedticket', 'cancellationrequest', 'cartposition', 'checkin',
            'checkinlist', 'device', 'event', 'event_settingsstore', 'eventmetaproperty', 'eventmetavalue',
            'gate', 'giftcard', 'giftcardacceptance', 'giftcardtransaction', 'globalsettingsobject_settingsstore',
            'invoice', 'invoiceaddress', 'invoiceline', 'item', 'itemaddon', 'itembundle', 'itemcategory',
            'itemmetaproperty', 'itemmetavalue', 'itemvariation', 'logentry', 'notificationsetting', 'order',
            'orderfee', 'orderpayment', 'orderposition', 'orderrefund', 'organizer', 'organizer_settingsstore',
            'question', 'questionanswer', 'questionoption', 'quota', 'requiredaction', 'revokedticketsecret',
            'seat', 'seatcategorymapping', 'seatingplan', 'staffsession', 'staffsessionauditlog', 'subevent',
            'subeventitem', 'subeventitemvariation', 'subeventmetavalue', 'taxrule', 'team', 'teamapitoken',
            'teaminvite', 'u2fdevice', 'user', 'voucher', 'waitinglistentry', 'webauthndevice'
        ]
        return [
            migrations.AlterField(
                model_name=model_name,
                name='id',
                field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False),
            )
            for model_name in model_names
        ]

    operations = [
        migrations.AddField(
            model_name='user',
            name='wikimedia_username',
            field=models.CharField(max_length=255, null=True),
        ),
    ] + generate_alter_field_operations()
```

This refactoring retains all functionality while cutting down on redundancy and improving maintainability.
created_at: 2025-03-15T19:53:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,313 @@
+# Generated by Django 5.1.7 on 2025-03-15 09:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pretixbase', '0008_cartposition_job_title_orderposition_job_title'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='wikimedia_username',
+            field=models.CharField(max_length=255, null=True),
+        ),
+        migrations.AlterField(
path: src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py
user: login: Gagan-Ram
body: 1. We don't let user to modify the wikimedia_username so not adding unique constraint should be okay.
2. The migration files are autogenerated when I ran python manage.py makemigrations command
created_at: 2025-03-16T08:48:35Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,313 @@
+# Generated by Django 5.1.7 on 2025-03-15 09:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('pretixbase', '0008_cartposition_job_title_orderposition_job_title'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='user',
+            name='wikimedia_username',
+            field=models.CharField(max_length=255, null=True),
+        ),
+        migrations.AlterField(
path: src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py
user: login: sourcery-ai[bot]
body: Okay, I understand.

Regarding your points:

1.  **`wikimedia_username` and unique constraint:** You're right, since the `wikimedia_username` is not intended to be modified by the user and is populated automatically upon login via Wikimedia, adding a unique constraint might not be necessary and could even cause issues if, for some reason, the same username is associated with multiple accounts in your system (although your code prevents multiple logins).

2.  **Autogenerated migration files:** I understand that the migration files are autogenerated. My suggestion was aimed at improving the structure and maintainability of the generated code. While the code works as is, refactoring the migration to reduce repetition can make it easier to understand and modify in the future. However, if the autogeneration process is likely to overwrite manual changes frequently, then my suggestion might not be practical.

Given that the migration files are autogenerated, and the `wikimedia_username` field doesn't need a unique constraint, it's perfectly reasonable to keep the migration as is. My suggestion was more of a "nice-to-have" rather than a critical change.

created_at: 2025-03-16T08:48:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -162,10 +164,13 @@ def get_full_name(self) -> str:
         Returns the first of the following user properties that is found to exist:
 
         * Full name
+        * wikimedia_username
path: src/pretix/base/models/auth.py
user: login: hongquan
body: Please look at the lines above and below and follow the same style.
created_at: 2025-03-20T03:39:47Z
author_association: MEMBER
diff_hunk: @@ -162,10 +164,13 @@ def get_full_name(self) -> str:
         Returns the first of the following user properties that is found to exist:
 
         * Full name
+        * wikimedia_username
path: src/pretix/base/models/auth.py
user: login: Gagan-Ram
body: Done, check once
created_at: 2025-03-20T08:37:02Z
author_association: MEMBER

PR comments:
user: login: sourcery-ai[bot]
created_at: 2025-03-15T19:52:56Z
body: <!-- Generated by sourcery-ai[bot]: start review_guide -->

## Reviewer's Guide by Sourcery

This PR introduces the `wikimedia_username` field to the user model. It updates the user model, account settings UI, and the social auth view to populate this field upon login via Wikimedia. The field is displayed as a read-only 'Nick name' in the account settings. Additionally, it updates migrations in banktransfer, ticketoutputpdf, and helpers.

_No diagrams generated as the changes look simple and do not need a visual representation._

### File-Level Changes

| Change | Details | Files |
| ------ | ------- | ----- |
| Added a `wikimedia_username` field to the user model. | <ul><li>Added `wikimedia_username` field to the `User` model in `pretix/base/models/auth.py`.</li><li>Created a database migration to add the `wikimedia_username` field.</li><li>Updated the `get_full_name` method to return `wikimedia_username` if `fullname` is not available.</li></ul> | `src/pretix/base/models/auth.py`<br/>`src/pretix/base/migrations/0009_user_wikimedia_username_and_more.py` |
| Updated the account settings UI to include the nickname field and set it to read-only. | <ul><li>Added `wikimedia_username` field to the `UserForm` in `pretix/base/forms/user.py`.</li><li>Set the `wikimedia_username` field to read-only in the `UserForm`.</li><li>Added the `wikimedia_username` field to the account settings templates.</li><li>Updated the label for the `wikimedia_username` field to 'Nick name'.</li></ul> | `src/pretix/base/forms/user.py`<br/>`src/pretix/control/templates/pretixcontrol/user/settings.html`<br/>`src/pretix/eventyay_common/templates/eventyay_common/account/settings.html` |
| Populate and update the `wikimedia_username` field upon user login via Wikimedia. | <ul><li>Fetch the latest signed-in Wikimedia account.</li><li>Extract the Wikimedia username from the social account's extra data.</li><li>Set the `wikimedia_username` when creating a new user.</li><li>Update the `wikimedia_username` if it's empty or has been updated in the Wikimedia account.</li></ul> | `src/pretix/plugins/socialauth/views.py` |
| Updated migrations in banktransfer, ticketoutputpdf, and helpers. | <ul><li>Updated the migrations in banktransfer to use BigAutoField.</li><li>Updated the migrations in ticketoutputpdf to use BigAutoField.</li><li>Updated the migrations in helpers to use BigAutoField.</li></ul> | `src/pretix/plugins/banktransfer/migrations/0003_alter_bankimportjob_id_alter_banktransaction_id_and_more.py`<br/>`src/pretix/plugins/ticketoutputpdf/migrations/0002_alter_ticketlayout_id_alter_ticketlayoutitem_id.py`<br/>`src/pretix/helpers/migrations/0002_alter_thumbnail_id.py` |

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it. You can also reply to a
  review comment with `@sourcery-ai issue` to create an issue from it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time. You can also comment
  `@sourcery-ai title` on the pull request to (re-)generate the title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time exactly where you
  want it. You can also comment `@sourcery-ai summary` on the pull request to
  (re-)generate the summary at any time.
- **Generate reviewer's guide:** Comment `@sourcery-ai guide` on the pull
  request to (re-)generate the reviewer's guide at any time.
- **Resolve all Sourcery comments:** Comment `@sourcery-ai resolve` on the
  pull request to resolve all Sourcery comments. Useful if you've already
  addressed all the comments and don't want to see them anymore.
- **Dismiss all Sourcery reviews:** Comment `@sourcery-ai dismiss` on the pull
  request to dismiss all existing Sourcery reviews. Especially useful if you
  want to start fresh with a new review - don't forget to comment
  `@sourcery-ai review` to trigger a new review!
- **Generate a plan of action for an issue:** Comment `@sourcery-ai plan` on
  an issue to generate a plan of action for it.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
author_association: CONTRIBUTOR
user: login: Gagan-Ram
created_at: 2025-03-16T08:49:02Z
body: @hongquan could you please look into this
author_association: MEMBER
user: login: hongquan
created_at: 2025-03-18T07:30:19Z
body: @Gagan-Ram Please delete all DB migration `AlterField` lines which are not related to your changes of model.

You said that they are generated when you ran `python manage.py makemigrations`. There are two possible causes:
- Your localhost environment was damaged.
- The existing DB migration files are broken.

We need time to check and determine which is the case. In the meantime, to prevent the codebase from being broken if the cause is the 1st case, you need to delete those lines from this PR.
author_association: MEMBER
user: login: Gagan-Ram
created_at: 2025-03-19T10:21:43Z
body: @hongquan   I noticed that Django is automatically applying BigAutoField because of DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' in settings.py.
I also identified the models related to the User model:
EmailAddress, LogEntry, NotificationSetting, OAuthAccessToken, OAuthApplication, OAuthGrant, OAuthIDToken, OAuthRefreshToken, RequiredAction, SocialAccount, StaffSession, StaffSessionAuditLog, StaticDevice, TOTPDevice, Team, U2FDevice, WebAuthnDevice.
However, none of these models are affected when the User model is modified.
So I am deleting all migrations which is altering the field 'id' into BigAutoField.
author_association: MEMBER
user: login: hongquan
created_at: 2025-03-20T03:38:02Z
body: Of course we know that Django applies `BigAutoField` because of the settings. The problem is that it shouldn't generate those `AlterField` lines, because those fields should  "already" be `BigAutoField` before your PR.
author_association: MEMBER
user: login: Gagan-Ram
created_at: 2025-03-20T03:52:10Z
body: It seems like we still need to figure out the problem.
By the way, does the PR look good now? I've deleted the AlterField lines.
author_association: MEMBER
user: login: hongquan
created_at: 2025-03-20T06:02:42Z
body: Hi, I added a comment yesterday but forgot to submit. Please check.
author_association: MEMBER