mirror of
https://github.com/boostorg/website-v2.git
synced 2026-02-27 05:32:08 +00:00
Merge pull request #122 from revsys/121-profile-photos
Underlying code for profile photos
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -143,6 +143,7 @@ cython_debug/
|
||||
.env
|
||||
deployed_static
|
||||
static_deploy
|
||||
media
|
||||
python.log
|
||||
|
||||
# direnv
|
||||
|
||||
@@ -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(
|
||||
|
||||
256
config/urls.py
256
config/urls.py
@@ -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",
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)"""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}),
|
||||
|
||||
28
users/management/commands/photos_test.py
Normal file
28
users/management/commands/photos_test.py
Normal 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
|
||||
18
users/migrations/0007_user_image.py
Normal file
18
users/migrations/0007_user_image.py
Normal 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"),
|
||||
),
|
||||
]
|
||||
@@ -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):
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user