Add profile image
This commit is contained in:
parent
a22afe0c57
commit
57136d9652
15 changed files with 529 additions and 438 deletions
|
@ -84,6 +84,7 @@ DMOJ_STATS_SUBMISSION_RESULT_COLORS = {
|
||||||
"CE": "#42586d",
|
"CE": "#42586d",
|
||||||
"ERR": "#ffa71c",
|
"ERR": "#ffa71c",
|
||||||
}
|
}
|
||||||
|
DMOJ_PROFILE_IMAGE_ROOT = "profile_images"
|
||||||
|
|
||||||
MARKDOWN_STYLES = {}
|
MARKDOWN_STYLES = {}
|
||||||
MARKDOWN_DEFAULT_STYLE = {}
|
MARKDOWN_DEFAULT_STYLE = {}
|
||||||
|
|
|
@ -50,6 +50,7 @@ from judge.widgets import (
|
||||||
HeavySelect2Widget,
|
HeavySelect2Widget,
|
||||||
Select2MultipleWidget,
|
Select2MultipleWidget,
|
||||||
DateTimePickerWidget,
|
DateTimePickerWidget,
|
||||||
|
ImageWidget,
|
||||||
)
|
)
|
||||||
from judge.tasks import rescore_contest
|
from judge.tasks import rescore_contest
|
||||||
|
|
||||||
|
@ -78,12 +79,14 @@ class ProfileForm(ModelForm):
|
||||||
"language",
|
"language",
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"user_script",
|
"user_script",
|
||||||
|
"profile_image",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"user_script": AceWidget(theme="github"),
|
"user_script": AceWidget(theme="github"),
|
||||||
"timezone": Select2Widget(attrs={"style": "width:200px"}),
|
"timezone": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
"language": Select2Widget(attrs={"style": "width:200px"}),
|
"language": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
|
"ace_theme": Select2Widget(attrs={"style": "width:200px"}),
|
||||||
|
"profile_image": ImageWidget,
|
||||||
}
|
}
|
||||||
|
|
||||||
has_math_config = bool(settings.MATHOID_URL)
|
has_math_config = bool(settings.MATHOID_URL)
|
||||||
|
@ -100,12 +103,22 @@ class ProfileForm(ModelForm):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
user = kwargs.pop("user", None)
|
user = kwargs.pop("user", None)
|
||||||
super(ProfileForm, self).__init__(*args, **kwargs)
|
super(ProfileForm, self).__init__(*args, **kwargs)
|
||||||
|
self.fields["profile_image"].required = False
|
||||||
|
|
||||||
|
def clean_profile_image(self):
|
||||||
|
profile_image = self.cleaned_data.get("profile_image")
|
||||||
|
if profile_image:
|
||||||
|
if profile_image.size > 5 * 1024 * 1024:
|
||||||
|
raise ValidationError(
|
||||||
|
_("File size exceeds the maximum allowed limit of 5MB.")
|
||||||
|
)
|
||||||
|
return profile_image
|
||||||
|
|
||||||
|
|
||||||
def file_size_validator(file):
|
def file_size_validator(file):
|
||||||
limit = 1 * 1024 * 1024
|
limit = 10 * 1024 * 1024
|
||||||
if file.size > limit:
|
if file.size > limit:
|
||||||
raise ValidationError("File too large. Size should not exceed 1MB.")
|
raise ValidationError("File too large. Size should not exceed 10MB.")
|
||||||
|
|
||||||
|
|
||||||
class ProblemSubmitForm(ModelForm):
|
class ProblemSubmitForm(ModelForm):
|
||||||
|
|
|
@ -9,14 +9,14 @@ from . import registry
|
||||||
|
|
||||||
|
|
||||||
@registry.function
|
@registry.function
|
||||||
def gravatar(email, size=80, default=None):
|
def gravatar(profile, size=80, default=None):
|
||||||
if isinstance(email, Profile):
|
assert isinstance(profile, Profile), "profile should be Profile"
|
||||||
if default is None:
|
profile_image = profile.profile_image
|
||||||
default = email.mute
|
if profile_image:
|
||||||
email = email.user.email
|
return profile_image.url
|
||||||
elif isinstance(email, AbstractUser):
|
if default is None:
|
||||||
email = email.email
|
default = profile.mute
|
||||||
|
email = profile.user.email
|
||||||
gravatar_url = (
|
gravatar_url = (
|
||||||
"//www.gravatar.com/avatar/"
|
"//www.gravatar.com/avatar/"
|
||||||
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
+ hashlib.md5(utf8bytes(email.strip().lower())).hexdigest()
|
||||||
|
|
21
judge/migrations/0162_profile_image.py
Normal file
21
judge/migrations/0162_profile_image.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-24 00:50
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import judge.models.profile
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0161_auto_20230803_1536"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="profile_image",
|
||||||
|
field=models.ImageField(
|
||||||
|
null=True, upload_to=judge.models.profile.profile_image_path
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,4 +1,5 @@
|
||||||
from operator import mul
|
from operator import mul
|
||||||
|
import os
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -27,6 +28,12 @@ class EncryptedNullCharField(EncryptedCharField):
|
||||||
return super(EncryptedNullCharField, self).get_prep_value(value)
|
return super(EncryptedNullCharField, self).get_prep_value(value)
|
||||||
|
|
||||||
|
|
||||||
|
def profile_image_path(profile, filename):
|
||||||
|
tail = filename.split(".")[-1]
|
||||||
|
new_filename = f"user_{profile.id}.{tail}"
|
||||||
|
return os.path.join(settings.DMOJ_PROFILE_IMAGE_ROOT, new_filename)
|
||||||
|
|
||||||
|
|
||||||
class Organization(models.Model):
|
class Organization(models.Model):
|
||||||
name = models.CharField(max_length=128, verbose_name=_("organization title"))
|
name = models.CharField(max_length=128, verbose_name=_("organization title"))
|
||||||
slug = models.SlugField(
|
slug = models.SlugField(
|
||||||
|
@ -229,6 +236,7 @@ class Profile(models.Model):
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text=_("Notes for administrators regarding this user."),
|
help_text=_("Notes for administrators regarding this user."),
|
||||||
)
|
)
|
||||||
|
profile_image = models.ImageField(upload_to=profile_image_path, null=True)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def organization(self):
|
def organization(self):
|
||||||
|
|
|
@ -402,12 +402,12 @@ class UserPerformancePointsAjax(UserProblemsPage):
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def edit_profile(request):
|
def edit_profile(request):
|
||||||
profile = Profile.objects.get(user=request.user)
|
profile = request.profile
|
||||||
if profile.mute:
|
|
||||||
raise Http404()
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
form_user = UserForm(request.POST, instance=request.user)
|
form_user = UserForm(request.POST, instance=request.user)
|
||||||
form = ProfileForm(request.POST, instance=profile, user=request.user)
|
form = ProfileForm(
|
||||||
|
request.POST, request.FILES, instance=profile, user=request.user
|
||||||
|
)
|
||||||
if form_user.is_valid() and form.is_valid():
|
if form_user.is_valid() and form.is_valid():
|
||||||
with transaction.atomic(), revisions.create_revision():
|
with transaction.atomic(), revisions.create_revision():
|
||||||
form_user.save()
|
form_user.save()
|
||||||
|
|
|
@ -3,3 +3,4 @@ from judge.widgets.mixins import CompressorWidgetMixin
|
||||||
from judge.widgets.pagedown import *
|
from judge.widgets.pagedown import *
|
||||||
from judge.widgets.select2 import *
|
from judge.widgets.select2 import *
|
||||||
from judge.widgets.datetime import *
|
from judge.widgets.datetime import *
|
||||||
|
from judge.widgets.image import *
|
||||||
|
|
16
judge/widgets/image.py
Normal file
16
judge/widgets/image.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class ImageWidget(forms.ClearableFileInput):
|
||||||
|
template_name = "widgets/image.html"
|
||||||
|
|
||||||
|
def __init__(self, attrs=None, width=80, height=80):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
super().__init__(attrs)
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs=None):
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
context["widget"]["height"] = self.height
|
||||||
|
context["widget"]["width"] = self.height
|
||||||
|
return context
|
File diff suppressed because it is too large
Load diff
|
@ -263,7 +263,7 @@
|
||||||
<span id="user-links">
|
<span id="user-links">
|
||||||
<ul><li><a href="javascript:void(0)">
|
<ul><li><a href="javascript:void(0)">
|
||||||
<span>
|
<span>
|
||||||
<img src="{{ gravatar(request.user, 32) }}" height="24" width="24">{# -#}
|
<img src="{{ gravatar(request.profile, 32) }}" height="24" width="24">{# -#}
|
||||||
<span>
|
<span>
|
||||||
<b class="{{request.profile.css_class}}">{{ request.user.username }}</b>
|
<b class="{{request.profile.css_class}}">{{ request.user.username }}</b>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
</h3>
|
</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
<div class="user-gravatar">
|
<div class="user-gravatar">
|
||||||
<img src="{{ gravatar(request.user, 135) }}"
|
<img src="{{ gravatar(request.profile, 135) }}"
|
||||||
alt="gravatar" width="135px" height="135px">
|
alt="gravatar" width="135px" height="135px">
|
||||||
</div>
|
</div>
|
||||||
<div class="recently-attempted">
|
<div class="recently-attempted">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% if other_user %}
|
{% if other_user %}
|
||||||
<div class="status-container" style="height: 100%">
|
<div class="status-container" style="height: 100%">
|
||||||
<img src="{{ gravatar(other_user.user, 135) }}" class="info-pic">
|
<img src="{{ gravatar(other_user, 135) }}" class="info-pic">
|
||||||
<svg style="position:absolute; height:100%; width: 100%">
|
<svg style="position:absolute; height:100%; width: 100%">
|
||||||
<circle class="info-circle"
|
<circle class="info-circle"
|
||||||
fill="{{'green' if other_online else 'red'}}"/>
|
fill="{{'green' if other_online else 'red'}}"/>
|
||||||
|
|
|
@ -162,7 +162,7 @@
|
||||||
<section class="message new-message">
|
<section class="message new-message">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<a href="{{ url('user_page', request.user.username) }}" class="user">
|
<a href="{{ url('user_page', request.user.username) }}" class="user">
|
||||||
<img src="{{ gravatar(request.user, 135) }}" class="gravatar">
|
<img src="{{ gravatar(request.profile, 135) }}" class="gravatar">
|
||||||
<div class="username {{ request.profile.css_class }}">{{ request.user.username }}</div>
|
<div class="username {{ request.profile.css_class }}">{{ request.user.username }}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -79,11 +79,13 @@
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<div id="center-float">
|
<div id="center-float">
|
||||||
<form id="edit-form" action="" method="post" class="form-area">
|
<form id="edit-form" action="" method="post" class="form-area" enctype="multipart/form-data">
|
||||||
{% if form.errors %}
|
{% if form.errors or form_user.errors %}
|
||||||
<div class="alert alert-danger alert-dismissable">
|
<div class="alert alert-danger alert-dismissable">
|
||||||
<a href="#" class="close">x</a>
|
<a href="#" class="close">x</a>
|
||||||
{{ form.non_field_errors() }}
|
{{ form.errors }}
|
||||||
|
<br>
|
||||||
|
{{ form_user.errors }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -98,6 +100,10 @@
|
||||||
<td> {{ _('School') }}: </td>
|
<td> {{ _('School') }}: </td>
|
||||||
<td> {{ form_user.last_name }} </td>
|
<td> {{ form_user.last_name }} </td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ _('Avatar') }}: </td>
|
||||||
|
<td>{{ form.profile_image }}</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
|
@ -127,12 +133,6 @@
|
||||||
<td><span class="fullwidth">{{ form.math_engine }}</span></td>
|
<td><span class="fullwidth">{{ form.math_engine }}</span></td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr>
|
|
||||||
<td colspan="2">
|
|
||||||
<a href="http://www.gravatar.com/" title="{{ _('Change your avatar') }}"
|
|
||||||
target="_blank" class="inline-header">{{ _('Change your avatar') }}</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="2">
|
<td colspan="2">
|
||||||
<a href="{{ url('password_change') }}" class="inline-header">
|
<a href="{{ url('password_change') }}" class="inline-header">
|
||||||
|
|
13
templates/widgets/image.html
Normal file
13
templates/widgets/image.html
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{% if widget.is_initial %}
|
||||||
|
<div>
|
||||||
|
<a href="{{widget.value.url}}" target=_blank>
|
||||||
|
<img src="{{widget.value.url}}" width="{{widget.width}}" height="{{widget.height}}" style="border-radius: 3px;">
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
{{ widget.input_text }}:
|
||||||
|
{% endif %}
|
||||||
|
<input type="{{ widget.type }}" name="{{ widget.name }}">
|
||||||
|
{% if widget.is_initial %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
Loading…
Reference in a new issue