Skip to content

Commit

Permalink
edx-username-changer plugin added (#420)
Browse files Browse the repository at this point in the history
* feat: edx-username-changer plugin added

* fix: absolute import added

* build: paver removed from OpenedX master

* build: keeping the version same

* fix: pants build fixed

* docs: unnecessary steps removed
  • Loading branch information
marslanabdulrauf authored Jan 9, 2025
1 parent de9c2ca commit 5496c24
Show file tree
Hide file tree
Showing 10 changed files with 416 additions and 0 deletions.
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",
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
Copyright (C) 2022 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.

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
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: {
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"):
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

0 comments on commit 5496c24

Please sign in to comment.