Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

edx-username-changer plugin added #420

Merged
merged 9 commits into from
Jan 9, 2025
28 changes: 28 additions & 0 deletions src/edx_username_changer/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
python_sources(
name="edx_username_changer_source",
)

python_distribution(
name="edx_username_changer_package",
dependencies=[
":edx_username_changer_source",
],
provides=setup_py(
name="edx-username-changer",
version="0.3.2",
marslanabdulrauf marked this conversation as resolved.
Show resolved Hide resolved
description="An edX plugin to change username of edx accounts through admin panel",
license="BSD-3-Clause",
author="MIT Office of Digital Learning",
include_package_data=True,
zip_safe=False,
keywords="Python edx",
entry_points={
"lms.djangoapp": [
"edx_username_changer = edx_username_changer.apps:EdxUsernameChangerConfig",
],
"cms.djangoapp": [
"edx_username_changer = edx_username_changer.apps:EdxUsernameChangerConfig",
],
},
),
)
28 changes: 28 additions & 0 deletions src/edx_username_changer/LICENCE
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We generally use the LICENSE files with file extension e.g. .txt. I believe open edx uses the same format

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I checked recently updated plugins e.g., ol_social_auth and openedx_companion_auth and they had without .txt.

I will update it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (C) 2022 MIT Open Learning
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Copyright (C) 2022 MIT Open Learning
Copyright (C) 2025 MIT Open Learning


All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
56 changes: 56 additions & 0 deletions src/edx_username_changer/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
Edx Username Changer
=======================

A plugin to enable update usernames through admin panel in Open edX (and other apps that log into Open edX via OAuth).

Version Compatibility
---------------------

It only supports koa and latest releases of Open edX.

Comment on lines +6 to +10
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How did we decide it? We've had different updates in the package e.g. Python, Django, etc. I think this should specify the version compatibility map or something we do in rapid response plugin where we specify which plugin version is compatible with which edX release.

Installing The Plugin
---------------------

You can install this plugin into any Open edX instance by using any of the following methods:

**Option 1: Install from PyPI**

.. code-block::

# If running devstack in docker, first open a shell in LMS (make lms-shell)

pip install edx-username-changer


**Option 2: Build the package locally and install it**

Follow these steps in a terminal on your machine:

1. Navigate to ``open-edx-plugins`` directory
2. Run ``./pants package ::``. This will create a "dist" directory inside "open-edx-plugins" directory with ".whl" & ".tar.gz" format packages for all plugins in the src directory
3. Move/copy any of the ".whl" or ".tar.gz" files for this plugin that were generated in the above step to the machine/container running Open edX (NOTE: If running devstack via Docker, you can use ``docker cp`` to copy these files into your LMS or CMS containers)
4. Run a shell in the machine/container running Open edX, and install this plugin using pip


``Note``: In some cases you might need to restart edx-platform after installing the plugin to reflect the changes.

Configurations
--------------
To configure this plugin, you need to do the following one step:

1. Add/Enable a feature flag (ENABLE_EDX_USERNAME_CHANGER) into your environment variables (through ``private.py`` in LMS or CMS, depending upon where you are installing the plugin)

.. code-block::
FEATURES["ENABLE_EDX_USERNAME_CHANGER"] = True

Comment on lines +42 to +45
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not rendered properly
image

How to use
----------
Its usage is as simple as changing the username of a user account through django's admin panel. Here are the steps (for clarity):

1. Install edx-username-changer plugin.
2. Use an admin account to access django admin panel.
3. Go to Users model and select an account to change its username.
4. In the account editor page change the username field.
5. Hit Save (present at the bottom-right of page).

The whole process can be done on lms or studio or on both of them.
3 changes: 3 additions & 0 deletions src/edx_username_changer/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# pylint: disable=missing-module-docstring

default_app_config = "edx_username_changer.apps.EdxUsernameChangerConfig"
38 changes: 38 additions & 0 deletions src/edx_username_changer/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""
Django admin pages for edx-username-changer plugin
"""

import contextlib

from common.djangoapps.student.admin import (
UserAdmin as BaseUserAdmin,
)
from django.conf import settings
from django.contrib import admin
from django.contrib.admin.sites import NotRegistered
from django.contrib.auth import get_user_model

User = get_user_model()


class UserAdmin(BaseUserAdmin):
"""
Admin interface for the User model.
"""

def get_readonly_fields(self, request, obj=None):
"""
Remove 'username' from the read-only fields
to make it editable through the admin panel
"""
readonly_fields = super().get_readonly_fields(request, obj)
if settings.FEATURES.get("ENABLE_EDX_USERNAME_CHANGER") and obj:
return tuple([name for name in readonly_fields if name != "username"])
return readonly_fields


# We must first un-register the User model since it was registered by edX's core code.
with contextlib.suppress(NotRegistered):
admin.site.unregister(User)

admin.site.register(User, UserAdmin)
33 changes: 33 additions & 0 deletions src/edx_username_changer/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""
App configuration for edx-username-changer plugin
"""

from django.apps import AppConfig
from edx_django_utils.plugins.constants import PluginSignals
from openedx.core.djangoapps.plugins.constants import (
ProjectType,
)


class EdxUsernameChangerConfig(AppConfig):
name = "edx_username_changer"
verbose_name = "Open edX Username Changer"

plugin_app = {
PluginSignals.CONFIG: {
ProjectType.LMS: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are adding the LMS project type only, but the documentation mentions that it can be used from CMS and LMS both. I believe anything under this would be registered in LMS.

PluginSignals.RECEIVERS: [
{
PluginSignals.RECEIVER_FUNC_NAME: "user_pre_save_callback",
PluginSignals.SIGNAL_PATH: "django.db.models.signals.pre_save",
PluginSignals.SENDER_PATH: "django.contrib.auth.models.User",
},
{
PluginSignals.RECEIVER_FUNC_NAME: "user_post_save_callback",
PluginSignals.SIGNAL_PATH: "django.db.models.signals.post_save",
PluginSignals.SENDER_PATH: "django.contrib.auth.models.User",
},
],
},
},
}
18 changes: 18 additions & 0 deletions src/edx_username_changer/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""
Exceptions for edx-username-changer plugin
"""


class UpdateFailedException(Exception): # noqa: N818
"""
Exception to throw when there is an update failure in username
"""

def __init__(self, url, new_username):
self.url = url
self.new_username = new_username

def __str__(self):
return (
f"Username update failed for username: {self.new_username}, url: {self.url}"
)
42 changes: 42 additions & 0 deletions src/edx_username_changer/signals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Signals and Signal Handlers for edx-username-changer plugin
"""

from common.djangoapps.util.model_utils import ( # pylint: disable=import-error
get_changed_fields_dict,
)
from django.conf import settings
from django.db import transaction
from edx_username_changer.tasks import task_update_username_in_forum
from edx_username_changer.utils import update_user_social_auth_uid


def user_pre_save_callback(sender, **kwargs):
"""
Pre-save signal handler of User model to store changed fields to be used later
"""
if settings.FEATURES.get("ENABLE_EDX_USERNAME_CHANGER"):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should have settings files like other applications to specify flags that are not part of open edx but the plugin itself. An example can be seen in https://github.com/mitodl/open-edx-plugins/tree/main/src/ol_openedx_chat/settings

user = kwargs["instance"]
fields_to_update = get_changed_fields_dict(user, sender)
if "username" in fields_to_update:
fields_to_update.update({"new_username": user.username})
user._updated_fields = fields_to_update # noqa: SLF001


def user_post_save_callback(sender, **kwargs): # noqa: ARG001
"""
Post-save signal handler of User model to update username throughout the application
"""
if settings.FEATURES.get("ENABLE_EDX_USERNAME_CHANGER"):
user = kwargs["instance"]
if (
hasattr(user, "_updated_fields")
and user._updated_fields # noqa: SLF001
and {"username", "new_username"}.issubset(user._updated_fields) # noqa: SLF001
):
new_username = user._updated_fields["new_username"] # noqa: SLF001
transaction.on_commit(
lambda: task_update_username_in_forum.delay(new_username)
)
update_user_social_auth_uid(user._updated_fields["username"], new_username) # noqa: SLF001
delattr(user, "_updated_fields")
42 changes: 42 additions & 0 deletions src/edx_username_changer/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
This file contains celery tasks related to edx_username_changer plugin.
"""

from celery import shared_task
from django.contrib.auth import get_user_model
from edx_username_changer.utils import (
get_authored_threads_and_comments,
get_enrolled_course_ids,
update_comment_user_username,
update_comment_username,
update_thread_username,
)
from openedx.core.djangoapps.django_comment_common.comment_client.user import (
User as CommentUser,
)

COMMENT_TYPE = "comment"
THREAD_TYPE = "thread"
User = get_user_model()


@shared_task()
def task_update_username_in_forum(username):
"""
Change username in Discussion-Forum service
"""
user = User.objects.get(username=username)
comment_user = CommentUser.from_django_user(user)
update_comment_user_username(comment_user, user.username)
enrolled_course_ids = get_enrolled_course_ids(user)
authored_items = get_authored_threads_and_comments(
comment_user, enrolled_course_ids
)

for authored_item in authored_items:
item_id = authored_item["id"]
item_type = str(authored_item.get("type"))
if item_type == THREAD_TYPE:
update_thread_username(item_id, user.username)
elif item_type == COMMENT_TYPE:
update_comment_username(item_id, user.username)
Loading
Loading