Merge pull request #122 from revsys/121-profile-photos

Underlying code for profile photos
This commit is contained in:
Frank Wiles
2023-02-17 07:58:54 -06:00
committed by GitHub
12 changed files with 271 additions and 131 deletions

1
.gitignore vendored
View File

@@ -143,6 +143,7 @@ cython_debug/
.env
deployed_static
static_deploy
media
python.log
# direnv

View File

@@ -1,5 +1,6 @@
import environs
import logging
import os
import structlog
import subprocess
import sys
@@ -207,6 +208,11 @@ STATICFILES_DIRS = [
# after running collectstatic
STATIC_ROOT = str(BASE_DIR.joinpath("static_deploy"))
# Directory where uploaded media is saved.
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
# Public URL at the browser
MEDIA_URL = "/media/"
# Logging setup
# Configure struct log
structlog.configure(

View File

@@ -1,4 +1,6 @@
from django.conf import settings
from django.conf.urls import include, re_path
from django.conf.urls.static import static
from django.contrib import admin
from django.views.generic import TemplateView
from django.urls import path
@@ -35,129 +37,135 @@ router.register(r"versions", VersionViewSet, basename="versions")
router.register(r"libraries", LibrarySearchView, basename="libraries")
urlpatterns = [
path("", HomepageView.as_view(), name="home"),
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path("users/me/", CurrentUserView.as_view(), name="current-user"),
path("users/<int:pk>/", ProfileViewSet.as_view(), name="profile-user"),
path("api/v1/", include(router.urls)),
path("200", OKView.as_view(), name="ok"),
path("403", ForbiddenView.as_view(), name="forbidden"),
path("404", NotFoundView.as_view(), name="not_found"),
path("500", InternalServerErrorView.as_view(), name="internal_server_error"),
path(
"about/",
TemplateView.as_view(template_name="boost/about.html"),
name="boost-about",
),
path("health/", include("health_check.urls")),
path("forum/", include(machina_urls)),
path(
"donate/",
TemplateView.as_view(template_name="donate/donate.html"),
name="donate",
),
path(
"libraries-by-category/<slug:category>/",
LibraryByCategory.as_view(),
name="libraries-by-category",
),
path("libraries/", LibraryList.as_view(), name="libraries"),
path(
"libraries/<slug:slug>/",
LibraryDetail.as_view(),
name="library-detail",
),
path(
"people/detail/",
TemplateView.as_view(template_name="boost/people_detail.html"),
name="boost-people-detail",
),
path(
"people/",
TemplateView.as_view(
template_name="boost/people.html", extra_context={"range": range(50)}
urlpatterns = (
[
path("", HomepageView.as_view(), name="home"),
path("admin/", admin.site.urls),
path("accounts/", include("allauth.urls")),
path("users/me/", CurrentUserView.as_view(), name="current-user"),
path("users/<int:pk>/", ProfileViewSet.as_view(), name="profile-user"),
path("api/v1/", include(router.urls)),
path("200", OKView.as_view(), name="ok"),
path("403", ForbiddenView.as_view(), name="forbidden"),
path("404", NotFoundView.as_view(), name="not_found"),
path("500", InternalServerErrorView.as_view(), name="internal_server_error"),
path(
"about/",
TemplateView.as_view(template_name="boost/about.html"),
name="boost-about",
),
name="boost-people",
),
path(
"moderators/",
TemplateView.as_view(
template_name="boost/moderators.html", extra_context={"range": range(50)}
path("health/", include("health_check.urls")),
path("forum/", include(machina_urls)),
path(
"donate/",
TemplateView.as_view(template_name="donate/donate.html"),
name="donate",
),
name="boost-moderators",
),
path(
"resources/",
TemplateView.as_view(template_name="resources/resources.html"),
name="resources",
),
path(
"review/past/",
TemplateView.as_view(template_name="review/past_reviews.html"),
name="review-past",
),
path(
"review/request/",
TemplateView.as_view(template_name="review/review_form.html"),
name="review-request",
),
path(
"review/upcoming/",
TemplateView.as_view(template_name="review/upcoming_reviews.html"),
name="review-upcoming",
),
path(
"review/detail/",
TemplateView.as_view(template_name="review/review_detail.html"),
name="review-request",
),
path(
"review/",
TemplateView.as_view(template_name="review/review_process.html"),
name="review-process",
),
path(
"news/detail/",
TemplateView.as_view(template_name="news/news_detail.html"),
name="news_detail",
),
path(
"news/",
TemplateView.as_view(template_name="news/news_list.html"),
name="news",
),
# support and contact views
path("support/", SupportView.as_view(), name="support"),
path(
"getting-started/",
TemplateView.as_view(template_name="support/getting_started.html"),
name="getting-started",
),
path("contact/", ContactView.as_view(), name="contact"),
# Boost versions views
path(
"versions/<slug:version_slug>/libraries-by-category/<slug:category>/",
LibraryListByVersionByCategory.as_view(),
name="libraries-by-version-by-category",
),
path(
"versions/<slug:slug>/libraries/",
LibraryListByVersion.as_view(),
name="libraries-by-version",
),
path(
"versions/<slug:version_slug>/<slug:slug>/",
LibraryDetailByVersion.as_view(),
name="library-detail-by-version",
),
path("versions/<slug:slug>/", VersionDetail.as_view(), name="version-detail"),
path("versions/", VersionList.as_view(), name="version-list"),
# Markdown content
re_path(
r"^(?P<content_path>.+)/?",
MarkdownTemplateView.as_view(),
name="markdown-page",
),
]
path(
"libraries-by-category/<slug:category>/",
LibraryByCategory.as_view(),
name="libraries-by-category",
),
path("libraries/", LibraryList.as_view(), name="libraries"),
path(
"libraries/<slug:slug>/",
LibraryDetail.as_view(),
name="library-detail",
),
path(
"people/detail/",
TemplateView.as_view(template_name="boost/people_detail.html"),
name="boost-people-detail",
),
path(
"people/",
TemplateView.as_view(
template_name="boost/people.html", extra_context={"range": range(50)}
),
name="boost-people",
),
path(
"moderators/",
TemplateView.as_view(
template_name="boost/moderators.html",
extra_context={"range": range(50)},
),
name="boost-moderators",
),
path(
"resources/",
TemplateView.as_view(template_name="resources/resources.html"),
name="resources",
),
path(
"review/past/",
TemplateView.as_view(template_name="review/past_reviews.html"),
name="review-past",
),
path(
"review/request/",
TemplateView.as_view(template_name="review/review_form.html"),
name="review-request",
),
path(
"review/upcoming/",
TemplateView.as_view(template_name="review/upcoming_reviews.html"),
name="review-upcoming",
),
path(
"review/detail/",
TemplateView.as_view(template_name="review/review_detail.html"),
name="review-request",
),
path(
"review/",
TemplateView.as_view(template_name="review/review_process.html"),
name="review-process",
),
path(
"news/detail/",
TemplateView.as_view(template_name="news/news_detail.html"),
name="news_detail",
),
path(
"news/",
TemplateView.as_view(template_name="news/news_list.html"),
name="news",
),
# support and contact views
path("support/", SupportView.as_view(), name="support"),
path(
"getting-started/",
TemplateView.as_view(template_name="support/getting_started.html"),
name="getting-started",
),
path("contact/", ContactView.as_view(), name="contact"),
# Boost versions views
path(
"versions/<slug:version_slug>/libraries-by-category/<slug:category>/",
LibraryListByVersionByCategory.as_view(),
name="libraries-by-version-by-category",
),
path(
"versions/<slug:slug>/libraries/",
LibraryListByVersion.as_view(),
name="libraries-by-version",
),
path(
"versions/<slug:version_slug>/<slug:slug>/",
LibraryDetailByVersion.as_view(),
name="library-detail-by-version",
),
path("versions/<slug:slug>/", VersionDetail.as_view(), name="version-detail"),
path("versions/", VersionList.as_view(), name="version-list"),
]
+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+ [
# Markdown content
re_path(
r"^(?P<content_path>.+)/?",
MarkdownTemplateView.as_view(),
name="markdown-page",
),
]
)

View File

@@ -22,6 +22,11 @@ def get_api():
return GhApi(token=token)
def get_user_by_username(api, username):
"""Return the response from GitHub's /users/{username}/"""
return api.users.get_by_username(username=username)
def get_repo(api, owner, repo):
"""
Return the response from GitHub's /repos/{owner}/{repo}

View File

@@ -75,6 +75,22 @@ def github_api_get_tree_response(db):
}
@pytest.fixture
def github_api_get_user_by_username_response(db):
"""Returns a JSON example of GhApi().api.users.get_by_username(username)"""
return {
"login": "testerson",
"id": 2286306,
"avatar_url": "https://avatars.githubusercontent.com/u/2286306?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/testerson",
"name": "Tester Testerson",
"email": None,
"created_at": "2012-09-05T17:17:25Z",
"updated_at": "2023-02-03T15:41:37Z",
}
@pytest.fixture
def github_api_get_repo_response(db):
"""Returns a JSON example of GhApi().api.repos.get(owner=owner, repo=repo)"""

View File

@@ -6,7 +6,12 @@ from dateutil.parser import parse
from ghapi.all import GhApi
from model_bakery import baker
from libraries.github import GithubUpdater, LibraryUpdater, get_api
from libraries.github import (
GithubUpdater,
LibraryUpdater,
get_api,
get_user_by_username,
)
from libraries.models import Issue, Library, PullRequest
@@ -15,6 +20,16 @@ def test_get_api():
assert isinstance(result, GhApi)
@pytest.mark.skip("The mock isn't working and is hitting the live API")
def test_get_user_by_username(github_api_get_user_by_username_response):
api = get_api()
with patch("libraries.github.get_user_by_username") as get_user_mock:
get_user_mock.return_value = github_api_get_user_by_username_response
result = get_user_by_username(api, "testerson")
assert result == github_api_get_user_by_username_response
assert "avatar_url" in result
# GithubUpdater tests

View File

@@ -5,7 +5,7 @@
{% block subnav %}
<div class="py-3 px-4 md:px-0 flex border-b border-slate md:border-0">
<div>
<img src="{% static 'img/fpo/user.png' %}" alt="user" class="inline" />
<img src="{{ user.image.url }}" alt="user" class="inline" />
</div>
<div class="text-sm ml-4">
<span class="block">{{ user.get_full_name }}</span>

View File

@@ -8,7 +8,10 @@ from .models import User
class EmailUserAdmin(UserAdmin):
fieldsets = (
(None, {"fields": ("email", "password")}),
(_("Personal info"), {"fields": ("first_name", "last_name")}),
(
_("Personal info"),
{"fields": ("first_name", "last_name", "github_username")},
),
(
_("Permissions"),
{
@@ -23,6 +26,7 @@ class EmailUserAdmin(UserAdmin):
),
(_("Important dates"), {"fields": ("last_login", "date_joined")}),
(_("Data"), {"fields": ("data",)}),
(_("Image"), {"fields": ("image",)}),
)
add_fieldsets = (
(None, {"classes": ("wide",), "fields": ("email", "password1", "password2")}),

View File

@@ -0,0 +1,28 @@
from django.core.management import BaseCommand
from django.contrib.auth import get_user_model
User = get_user_model()
class Command(BaseCommand):
def __init__(self, *args, **kwargs):
super(Command, self).__init__(*args, **kwargs)
help = "Convenience command to test saving a photo from a GH profile"
def handle(self, *args, **options):
"""
Having trouble getting the patch to work in the test for the underlying function.
This is a functional test.
Replace "testing" with your username and run `./manage.py photos_test`,
then you will see your photo in the admin for whoever your first user is.
"""
user = User.objects.first()
user.image = None
user.github_username = "testing"
user.save()
user.refresh_from_db()
assert bool(user.image) is False
user.save_image_from_github()
user.refresh_from_db()
assert bool(user.image) is True

View File

@@ -0,0 +1,18 @@
# Generated by Django 3.2.2 on 2023-02-15 21:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("users", "0006_auto_20220209_1545"),
]
operations = [
migrations.AddField(
model_name="user",
name="image",
field=models.FileField(blank=True, null=True, upload_to="profile-images"),
),
]

View File

@@ -1,16 +1,24 @@
import logging
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
import os
import requests
from django.conf import settings
from django.contrib.auth.models import (
AbstractBaseUser,
PermissionsMixin,
BaseUserManager,
)
from django.core.files import File
from django.core.mail import send_mail
from django.utils.translation import gettext_lazy as _
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from libraries.github import get_api as get_github_api
from libraries.github import get_user_by_username as get_github_user
logger = logging.getLogger(__name__)
@@ -138,6 +146,34 @@ class Badge(models.Model):
class User(BaseUser):
badges = models.ManyToManyField(Badge)
github_username = models.CharField(_("github username"), max_length=100, blank=True)
image = models.FileField(upload_to="profile-images", null=True, blank=True)
def save_image_from_github(self):
if not self.github_username:
# todo: log
return
api = get_github_api()
result = get_github_user(api, self.github_username)
avatar_url = result.get("avatar_url")
if not avatar_url:
# todo: log
return
response = requests.get(avatar_url)
base_filename = f"{slugify(self.get_full_name())}-profile"
filename = f"{base_filename}.png"
img_path = os.path.join(
settings.MEDIA_ROOT, "media", "profile-images", filename
)
with open(filename, "wb") as f:
f.write(response.content)
reopen = open(filename, "rb")
django_file = File(reopen)
self.image.save(filename, django_file, save=True)
class LastSeen(models.Model):

View File

@@ -14,6 +14,7 @@ def user(db):
first_name="Regular",
last_name="User",
last_login=timezone.now(),
image="static/img/fpo/user.png",
)
user.set_password("password")
user.save()
@@ -31,6 +32,7 @@ def staff_user(db):
last_name="User",
last_login=timezone.now(),
is_staff=True,
image="static/img/fpo/user.png",
)
user.set_password("password")
user.save()
@@ -49,6 +51,7 @@ def super_user(db):
last_login=timezone.now(),
is_staff=True,
is_superuser=True,
image="static/img/fpo/user.png",
)
user.set_password("password")
user.save()