diff --git a/docs/commands.md b/docs/commands.md index fdf42497..3c4023ef 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -15,6 +15,7 @@ - [`sync_mailinglist_stats`](#sync_mailinglist_stats) - [`update_library_version_dependencies`](#update_library_version_dependencies) - [`release_tasks`](#release_tasks) + - [`refresh_users_github_photos`](#refresh_users_github_photos) ## `boost_setup` @@ -323,3 +324,35 @@ For this to work `SLACK_BOT_API` must be set in the `.env` file. ```bash ./manage.py link_contributors_to_users ``` + +## `refresh_users_github_photos` + +**Purpose**: Refresh GitHub profile photos for all users who have a GitHub username. This command fetches the latest profile photo from GitHub for each user and updates their local profile image. This is useful for local dev/testing, isn't used for production where a periodic celery task is used. + +**Example** + +```bash +./manage.py refresh_users_github_photos +``` + +**Options** + +| Options | Format | Description | +|--------------|--------|----------------------------------------------------------------------------------------------| +| `--dry-run` | bool | Show which users would be updated without actually updating them. Useful for testing. | + +**Usage Examples** + +Refresh photos for all users with GitHub usernames: +```bash +./manage.py refresh_users_github_photos +``` + +Preview which users would be updated: +```bash +./manage.py refresh_users_github_photos --dry-run +``` +**Process** + +- Calls the `refresh_users_github_photos()` Celery task which queues photo updates for all users with GitHub usernames +- With `--dry-run`, displays information about which users would be updated without making any changes diff --git a/users/forms.py b/users/forms.py index fbd2e0f0..ab371766 100644 --- a/users/forms.py +++ b/users/forms.py @@ -140,11 +140,12 @@ class UserProfilePhotoForm(forms.ModelForm): old_image = self.instance.image # Save the new image user = super().save(commit=False) - - if old_image: + if not old_image: + # reset image on image delete checked + user.image_uploaded = False + elif self.cleaned_data["image"] != old_image: # Delete the old image file if there's a new image being uploaded - if self.cleaned_data["image"] != old_image: - old_image.delete(save=False) + old_image.delete(save=False) if self.cleaned_data.get("image"): new_image = self.cleaned_data["image"] @@ -155,6 +156,7 @@ class UserProfilePhotoForm(forms.ModelForm): new_image.name = f"{user.profile_image_filename_root}.{file_extension}" user.image = new_image + user.image_uploaded = True if commit: user.save() diff --git a/users/management/commands/refresh_users_github_photos.py b/users/management/commands/refresh_users_github_photos.py new file mode 100644 index 00000000..3aa33c77 --- /dev/null +++ b/users/management/commands/refresh_users_github_photos.py @@ -0,0 +1,39 @@ +import djclick as click +from django.contrib.auth import get_user_model + +from users.tasks import refresh_users_github_photos + +User = get_user_model() + + +@click.command() +@click.option( + "--dry-run", + is_flag=True, + help="Show which users would be updated without actually updating them", + default=False, +) +def command(dry_run): + """Refresh GitHub photos for all users who have a GitHub username. + + This command fetches the latest profile photo from GitHub for each user + and updates their local profile image. This is useful for keeping user + avatars up-to-date as users change their GitHub profile photos. + + When run without --dry-run, this calls the refresh_users_github_photos() + Celery task which queues photo updates for all users with GitHub usernames. + + With --dry-run, displays information about which users would be updated + without making any changes. + """ + users = User.objects.exclude(github_username="") + user_count = users.count() + + if dry_run: + click.secho(f"Refreshing {user_count} users, Github users:", fg="yellow") + for user in users: + click.echo(f" - User {user.pk}: {user.github_username}") + else: + click.secho(f"Refreshing photos, {user_count} users", fg="green") + refresh_users_github_photos() + click.secho(f"Triggered photo refresh task, {user_count} users", fg="green") diff --git a/users/migrations/0019_user_image_uploaded.py b/users/migrations/0019_user_image_uploaded.py new file mode 100644 index 00000000..6fe08713 --- /dev/null +++ b/users/migrations/0019_user_image_uploaded.py @@ -0,0 +1,21 @@ +# Generated by Django 5.2.7 on 2025-10-31 22:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0018_user_is_commit_author_name_overridden"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="image_uploaded", + field=models.BooleanField( + default=False, + help_text="Indicates if the user manually uploaded an image, prevents import overwrites", + ), + ), + ] diff --git a/users/models.py b/users/models.py index fa45469a..b26fdc8a 100644 --- a/users/models.py +++ b/users/models.py @@ -230,6 +230,10 @@ class User(BaseUser): format="JPEG", options={"quality": 90}, ) + image_uploaded = models.BooleanField( + default=False, + help_text="Indicates if the user manually uploaded an image, prevents import overwrites", + ) claimed = models.BooleanField( _("claimed"), default=True, diff --git a/users/signals.py b/users/signals.py index bfd7b9d9..55e9da2b 100644 --- a/users/signals.py +++ b/users/signals.py @@ -31,7 +31,7 @@ def import_social_profile_data(sender, instance, created, **kwargs): elif instance.provider == GOOGLE: avatar_url = instance.extra_data.get("picture") - if avatar_url: + if avatar_url and not instance.user.image_uploaded: instance.user.save_image_from_provider(avatar_url) instance.user.display_name = instance.extra_data.get("name") diff --git a/users/tasks.py b/users/tasks.py index d3ca2e40..3bec7a2f 100644 --- a/users/tasks.py +++ b/users/tasks.py @@ -4,6 +4,7 @@ import structlog from django.contrib.auth import get_user_model from django.core.mail import send_mail +from django.db.models import Q from django.utils import timezone from django.conf import settings @@ -47,10 +48,11 @@ def update_user_github_photo(user_pk): @app.task def refresh_users_github_photos(): """ - Refreshes the GitHub photos for all users who have a GitHub username. + Refreshes the GitHub photos for all users who have a GitHub username and haven't + uploaded an image manually. This is intended to be run periodically to ensure user photos are up-to-date. """ - users = User.objects.exclude(github_username="") + users = User.objects.exclude(Q(github_username="") | Q(image_uploaded=True)) for user in users: try: logger.info(f"updating {user.pk=}")