This commit is contained in:
BaoLe106 2023-08-25 10:19:49 +08:00
commit 78ef14e5ac
28 changed files with 932 additions and 532 deletions

View file

@ -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 = {}

View file

@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from django.contrib.admin.models import LogEntry from django.contrib.admin.models import LogEntry
from django.contrib.auth.models import User
from judge.admin.comments import CommentAdmin from judge.admin.comments import CommentAdmin
from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin from judge.admin.contest import ContestAdmin, ContestParticipationAdmin, ContestTagAdmin
@ -11,7 +12,7 @@ from judge.admin.interface import (
) )
from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin from judge.admin.organization import OrganizationAdmin, OrganizationRequestAdmin
from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin from judge.admin.problem import ProblemAdmin, ProblemPointsVoteAdmin
from judge.admin.profile import ProfileAdmin from judge.admin.profile import ProfileAdmin, UserAdmin
from judge.admin.runtime import JudgeAdmin, LanguageAdmin from judge.admin.runtime import JudgeAdmin, LanguageAdmin
from judge.admin.submission import SubmissionAdmin from judge.admin.submission import SubmissionAdmin
from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin from judge.admin.taxon import ProblemGroupAdmin, ProblemTypeAdmin
@ -66,3 +67,5 @@ admin.site.register(Submission, SubmissionAdmin)
admin.site.register(Ticket, TicketAdmin) admin.site.register(Ticket, TicketAdmin)
admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin) admin.site.register(VolunteerProblemVote, VolunteerProblemVoteAdmin)
admin.site.register(Course) admin.site.register(Course)
admin.site.unregister(User)
admin.site.register(User, UserAdmin)

View file

@ -3,6 +3,7 @@ from django.forms import ModelForm
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext, gettext_lazy as _, ungettext from django.utils.translation import gettext, gettext_lazy as _, ungettext
from reversion.admin import VersionAdmin from reversion.admin import VersionAdmin
from django.contrib.auth.admin import UserAdmin as OldUserAdmin
from django_ace import AceWidget from django_ace import AceWidget
from judge.models import Profile from judge.models import Profile
@ -167,3 +168,38 @@ class ProfileAdmin(VersionAdmin):
"javascript", request.profile.ace_theme "javascript", request.profile.ace_theme
) )
return form return form
class UserAdmin(OldUserAdmin):
# Customize the fieldsets for adding and editing users
fieldsets = (
(None, {"fields": ("username", "password")}),
("Personal Info", {"fields": ("first_name", "last_name", "email")}),
(
"Permissions",
{
"fields": (
"is_active",
"is_staff",
"is_superuser",
"groups",
"user_permissions",
)
},
),
("Important dates", {"fields": ("last_login", "date_joined")}),
)
readonly_fields = ("last_login", "date_joined")
def get_readonly_fields(self, request, obj=None):
fields = self.readonly_fields
if not request.user.is_superuser:
fields += (
"is_staff",
"is_active",
"is_superuser",
"groups",
"user_permissions",
)
return fields

View file

@ -16,7 +16,7 @@ class EventPoster(object):
def _connect(self): def _connect(self):
self._conn = pika.BlockingConnection( self._conn = pika.BlockingConnection(
pika.URLParameters(settings.EVENT_DAEMON_AMQP) pika.URLParameters(settings.EVENT_DAEMON_AMQP),
) )
self._chan = self._conn.channel() self._chan = self._conn.channel()
@ -25,7 +25,7 @@ class EventPoster(object):
id = int(time() * 1000000) id = int(time() * 1000000)
self._chan.basic_publish( self._chan.basic_publish(
self._exchange, self._exchange,
"", "#",
json.dumps({"id": id, "channel": channel, "message": message}), json.dumps({"id": id, "channel": channel, "message": message}),
) )
return id return id

View file

@ -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):
@ -474,6 +487,15 @@ class ContestCloneForm(Form):
max_length=20, max_length=20,
validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))], validators=[RegexValidator("^[a-z0-9]+$", _("Contest id must be ^[a-z0-9]+$"))],
) )
organization = ChoiceField(choices=(), required=True)
def __init__(self, *args, org_choices=(), profile=None, **kwargs):
super(ContestCloneForm, self).__init__(*args, **kwargs)
self.fields["organization"].widget = Select2Widget(
attrs={"style": "width: 100%", "data-placeholder": _("Group")},
)
self.fields["organization"].choices = org_choices
self.profile = profile
def clean_key(self): def clean_key(self):
key = self.cleaned_data["key"] key = self.cleaned_data["key"]
@ -481,6 +503,16 @@ class ContestCloneForm(Form):
raise ValidationError(_("Contest with key already exists.")) raise ValidationError(_("Contest with key already exists."))
return key return key
def clean_organization(self):
organization_id = self.cleaned_data["organization"]
try:
organization = Organization.objects.get(id=organization_id)
except Exception:
raise ValidationError(_("Group doesn't exist."))
if not organization.admins.filter(id=self.profile.id).exists():
raise ValidationError(_("You don't have permission in this group."))
return organization
class ProblemPointsVoteForm(ModelForm): class ProblemPointsVoteForm(ModelForm):
class Meta: class Meta:

View file

@ -9,14 +9,15 @@ from . import registry
@registry.function @registry.function
def gravatar(email, size=80, default=None): def gravatar(profile, size=80, default=None, profile_image=None, email=None):
if isinstance(email, Profile): if profile_image:
return profile_image
if profile and profile.profile_image:
return profile.profile_image.url
if profile:
email = email or profile.user.email
if default is None: if default is None:
default = email.mute default = profile.mute
email = email.user.email
elif isinstance(email, AbstractUser):
email = email.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()

View 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
),
),
]

View file

@ -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):

View file

@ -453,9 +453,19 @@ class ContestClone(
form_class = ContestCloneForm form_class = ContestCloneForm
permission_required = "judge.clone_contest" permission_required = "judge.clone_contest"
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs["org_choices"] = tuple(
Organization.objects.filter(admins=self.request.profile).values_list(
"id", "name"
)
)
kwargs["profile"] = self.request.profile
return kwargs
def form_valid(self, form): def form_valid(self, form):
tags = self.object.tags.all() tags = self.object.tags.all()
organizations = self.object.organizations.all() organization = form.cleaned_data["organization"]
private_contestants = self.object.private_contestants.all() private_contestants = self.object.private_contestants.all()
view_contest_scoreboard = self.object.view_contest_scoreboard.all() view_contest_scoreboard = self.object.view_contest_scoreboard.all()
contest_problems = self.object.contest_problems.all() contest_problems = self.object.contest_problems.all()
@ -469,7 +479,7 @@ class ContestClone(
contest.save() contest.save()
contest.tags.set(tags) contest.tags.set(tags)
contest.organizations.set(organizations) contest.organizations.set([organization])
contest.private_contestants.set(private_contestants) contest.private_contestants.set(private_contestants)
contest.view_contest_scoreboard.set(view_contest_scoreboard) contest.view_contest_scoreboard.set(view_contest_scoreboard)
contest.authors.add(self.request.profile) contest.authors.add(self.request.profile)
@ -480,7 +490,14 @@ class ContestClone(
ContestProblem.objects.bulk_create(contest_problems) ContestProblem.objects.bulk_create(contest_problems)
return HttpResponseRedirect( return HttpResponseRedirect(
reverse("admin:judge_contest_change", args=(contest.id,)) reverse(
"organization_contest_edit",
args=(
organization.id,
organization.slug,
contest.key,
),
)
) )

View file

@ -1,8 +1,11 @@
from urllib.parse import urljoin
from django.db.models import F, Q from django.db.models import F, Q
from django.http import Http404, JsonResponse from django.http import Http404, JsonResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.encoding import smart_text from django.utils.encoding import smart_text
from django.views.generic.list import BaseListView from django.views.generic.list import BaseListView
from django.conf import settings
from chat_box.utils import encrypt_url from chat_box.utils import encrypt_url
@ -54,7 +57,6 @@ class Select2View(BaseListView):
class UserSelect2View(Select2View): class UserSelect2View(Select2View):
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.org_id = kwargs.get("org_id", request.GET.get("org_id", "")) self.org_id = kwargs.get("org_id", request.GET.get("org_id", ""))
print(self.org_id)
return super(UserSelect2View, self).get(request, *args, **kwargs) return super(UserSelect2View, self).get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
@ -100,6 +102,21 @@ class UserSearchSelect2View(BaseListView):
def get_queryset(self): def get_queryset(self):
return _get_user_queryset(self.term) return _get_user_queryset(self.term)
def get_json_result_from_object(self, user_tuple):
pk, username, email, display_rank, profile_image = user_tuple
return {
"text": username,
"id": username,
"gravatar_url": gravatar(
None,
self.gravatar_size,
self.gravatar_default,
self.get_profile_image_url(profile_image),
email,
),
"display_rank": display_rank,
}
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
self.request = request self.request = request
self.kwargs = kwargs self.kwargs = kwargs
@ -108,7 +125,7 @@ class UserSearchSelect2View(BaseListView):
self.gravatar_default = request.GET.get("gravatar_default", None) self.gravatar_default = request.GET.get("gravatar_default", None)
self.object_list = self.get_queryset().values_list( self.object_list = self.get_queryset().values_list(
"pk", "user__username", "user__email", "display_rank" "pk", "user__username", "user__email", "display_rank", "profile_image"
) )
context = self.get_context_data() context = self.get_context_data()
@ -116,15 +133,8 @@ class UserSearchSelect2View(BaseListView):
return JsonResponse( return JsonResponse(
{ {
"results": [ "results": [
{ self.get_json_result_from_object(user_tuple)
"text": username, for user_tuple in context["object_list"]
"id": username,
"gravatar_url": gravatar(
email, self.gravatar_size, self.gravatar_default
),
"display_rank": display_rank,
}
for pk, username, email, display_rank in context["object_list"]
], ],
"more": context["page_obj"].has_next(), "more": context["page_obj"].has_next(),
} }
@ -133,6 +143,11 @@ class UserSearchSelect2View(BaseListView):
def get_name(self, obj): def get_name(self, obj):
return str(obj) return str(obj)
def get_profile_image_url(self, profile_image):
if profile_image:
return urljoin(settings.MEDIA_URL, profile_image)
return None
class ContestUserSearchSelect2View(UserSearchSelect2View): class ContestUserSearchSelect2View(UserSearchSelect2View):
def get_queryset(self): def get_queryset(self):
@ -161,43 +176,20 @@ class AssigneeSelect2View(UserSearchSelect2View):
).distinct() ).distinct()
class ChatUserSearchSelect2View(BaseListView): class ChatUserSearchSelect2View(UserSearchSelect2View):
paginate_by = 20 def get_json_result_from_object(self, user_tuple):
def get_queryset(self): # TODO: add block
return _get_user_queryset(self.term)
def get(self, request, *args, **kwargs):
if not self.request.user.is_authenticated: if not self.request.user.is_authenticated:
raise Http404() raise Http404()
self.request = request pk, username, email, display_rank, profile_image = user_tuple
self.kwargs = kwargs return {
self.term = kwargs.get("term", request.GET.get("term", "")) "text": username,
self.gravatar_size = request.GET.get("gravatar_size", 128) "id": encrypt_url(self.request.profile.id, pk),
self.gravatar_default = request.GET.get("gravatar_default", None) "gravatar_url": gravatar(
None,
self.object_list = self.get_queryset().values_list( self.gravatar_size,
"pk", "user__username", "user__email", "display_rank" self.gravatar_default,
) self.get_profile_image_url(profile_image),
email,
context = self.get_context_data() ),
"display_rank": display_rank,
return JsonResponse( }
{
"results": [
{
"text": username,
"id": encrypt_url(request.profile.id, pk),
"gravatar_url": gravatar(
email, self.gravatar_size, self.gravatar_default
),
"display_rank": display_rank,
}
for pk, username, email, display_rank in context["object_list"]
],
"more": context["page_obj"].has_next(),
}
)
def get_name(self, obj):
return str(obj)

View file

@ -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()

View file

@ -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
View 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

View file

@ -313,6 +313,11 @@
padding: 0.8em 0.2em 0.8em 1em; padding: 0.8em 0.2em 0.8em 1em;
} }
.sidebar-text {
overflow: hidden;
text-overflow: ellipsis;
}
.middle-content, .middle-content,
.blog-sidebar, .blog-sidebar,
.right-sidebar { .right-sidebar {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 542 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Before After
Before After

View file

@ -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>
@ -271,24 +271,38 @@
</a></li></ul> </a></li></ul>
</span> </span>
<div class="dropdown" id="userlink_dropdown" role="tooptip"> <div class="dropdown" id="userlink_dropdown" role="tooptip">
<div class="dropdown-item"><a href="{{ url('user_page') }}">{{ _('Profile') }}</a></div> <a href="{{ url('user_page') }}">
<div class="dropdown-item">{{ _('Profile') }}</div>
</a>
{% if request.user.is_staff or request.user.is_superuser %} {% if request.user.is_staff or request.user.is_superuser %}
<div class="dropdown-item"><a href="{{ url('admin:index') }}">{{ _('Admin') }}</a></div> <a href="{{ url('admin:index') }}">
<div class="dropdown-item">{{ _('Admin') }}</div>
</a>
{% endif %} {% endif %}
{% if request.user.is_superuser %} {% if request.user.is_superuser %}
<div class="dropdown-item"><a href="{{ url('internal_problem') }}">{{ _('Internal') }}</a></div> <a href="{{ url('internal_problem') }}">
<div class="dropdown-item"><a href="{{ url('site_stats') }}">{{ _('Stats') }}</a></div> <div class="dropdown-item">{{ _('Internal') }}</div>
</a>
<a href="{{ url('site_stats') }}">
<div class="dropdown-item">{{ _('Stats') }}</div>
</a>
{% endif %} {% endif %}
<div class="dropdown-item"><a href="{{ url('user_edit_profile') }}">{{ _('Edit profile') }}</a></div> <a href="{{ url('user_edit_profile') }}">
<div class="dropdown-item">{{ _('Edit profile') }}</div>
</a>
{% if request.user.is_impersonate %} {% if request.user.is_impersonate %}
<div class="dropdown-item"><a href="{{ url('impersonate-stop') }}">Stop impersonating</a></div> <a href="{{ url('impersonate-stop') }}">
<div class="dropdown-item">Stop impersonating</div>
</a>
{% else %} {% else %}
<div class="dropdown-item"> <a href="#" id="logout" class="red">
<a href="#" id="logout" class="red">{{ _('Log out') }}</a> <div class="dropdown-item">
<form id="logout-form" action="{{ url('auth_logout') }}" method="POST"> {{ _('Log out') }}
{% csrf_token %} <form id="logout-form" action="{{ url('auth_logout') }}" method="POST">
</form> {% csrf_token %}
</div> </form>
</div>
</a>
{% endif %} {% endif %}
</div> </div>
{% else %} {% else %}

View file

@ -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">

View file

@ -42,8 +42,8 @@
{% block left_sidebar %} {% block left_sidebar %}
<div class="left-sidebar"> <div class="left-sidebar">
{{ make_tab_item('blog', 'fa fa-rss', url('home'), _('News')) }} {{ make_tab_item('blog', 'fa fa-rss', url('home'), _('News')) }}
{{ make_tab_item('comment', 'fa fa-comments', url('comment_feed'), _('Comments')) }} {{ make_tab_item('comment', 'fa fa-comments', url('comment_feed'), _('Comment')) }}
{{ make_tab_item('ticket', 'fa fa-question-circle', url('ticket_feed'), _('Tickets')) }} {{ make_tab_item('ticket', 'fa fa-question-circle', url('ticket_feed'), _('Ticket')) }}
{{ make_tab_item('event', 'fa fa-calendar', '#', _('Events')) }} {{ make_tab_item('event', 'fa fa-calendar', '#', _('Events')) }}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -499,18 +499,28 @@
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight); $('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
load_dynamic_update({{last_msg}}); load_dynamic_update({{last_msg}});
const button = document.querySelector('#emoji-button') const button = document.querySelector('#emoji-button');
const tooltip = document.querySelector('.tooltip') const tooltip = document.querySelector('.tooltip');
Popper.createPopper(button, tooltip) Popper.createPopper(button, tooltip, {
placement: 'left-end',
});
function toggleEmoji() { function toggleEmoji() {
tooltip.classList.toggle('shown') tooltip.classList.toggle('shown')
} }
$('#emoji-button').on('click', function(e) { $('#emoji-button').on('click', function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation();
toggleEmoji(); toggleEmoji();
}); });
// Đóng bảng emoji khi click bất kỳ chỗ nào trên màn hình
document.addEventListener("click", function(e) {
if (!tooltip.contains(e.target)) {
tooltip.classList.remove('shown'); // Ẩn bảng emoji
}
});
$('emoji-picker').on('emoji-click', function(e) { $('emoji-picker').on('emoji-click', function(e) {
var $chat = $('#chat-input').get(0); var $chat = $('#chat-input').get(0);
insert_char_after_cursor($chat, e.detail.unicode); insert_char_after_cursor($chat, e.detail.unicode);
@ -629,7 +639,7 @@
</div> </div>
<div id="chat-box"> <div id="chat-box">
<img src="{{static('loading.gif')}}" id="loader"> <img src="{{static('loading.gif')}}" id="loader" height="2em">
<ul id="chat-log" style="display: none"> <ul id="chat-log" style="display: none">
{% include 'chat/message_list.html' %} {% include 'chat/message_list.html' %}
</ul> </ul>

View file

@ -43,9 +43,8 @@
} }
.tooltip { .tooltip {
left: 120vh !important;
transform: translate(100px, 0) !important;
position: absolute; position: absolute;
z-index: 1000;
} }
#loader { #loader {

View file

@ -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'}}"/>

View file

@ -25,17 +25,32 @@
</style> </style>
{% endblock %} {% endblock %}
{% block js_media %}
<script type="text/javascript">
$(function() {
$("#id_organization").select2();
});
</script>
{% endblock %}
{% block body %} {% block body %}
<form id="contest-clone-panel" action="" method="post" class="form-area"> <form id="contest-clone-panel" action="" method="post" class="form-area">
{% csrf_token %} {% csrf_token %}
{% if form.errors %} {% if form.key.errors %}
<div id="form-errors"> <div id="form-errors">
{{ form.key.errors }} <div>{{ form.key.errors }}</div>
</div> </div>
{% endif %} {% endif %}
<div><label class="inline-header grayed">{{ _('Enter a new key for the cloned contest:') }}</label></div> <div><label class="inline-header grayed">{{ _('Enter a new key for the cloned contest:') }}</label></div>
<div id="contest-key-container"><span class="fullwidth">{{ form.key }}</span></div> <div id="contest-key-container"><span class="fullwidth">{{ form.key }}</span></div>
<div><label class="inline-header grayed">{{ _('Group:') }}</label></div>
{{form.organization}}
{% if form.organization.errors %}
<div id="form-errors">
<div>{{ form.organization.errors }}</div>
</div>
{% endif %}
<hr> <hr>
<button style="float: right;" type="submit">{{ _('Clone!') }}</button> <button style="float: right;" type="submit">{{ _('Clone!') }}</button>
</form> </form>

View file

@ -118,7 +118,7 @@
{% macro make_tab_item(name, fa, url, text) %} {% macro make_tab_item(name, fa, url, text) %}
<div class="left-sidebar-item {% if page_type == name %}active{% endif %}" data-href="{{ url }}" id="{{ name }}-tab"> <div class="left-sidebar-item {% if page_type == name %}active{% endif %}" data-href="{{ url }}" id="{{ name }}-tab">
<span class="sidebar-icon"><i class="{{ fa }}"></i></span> <span class="sidebar-icon"><i class="{{ fa }}"></i></span>
<span>{{ text }}</span> <span class="sidebar-text">{{ text }}</span>
</div> </div>
{% endmacro %} {% endmacro %}

View file

@ -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>

View file

@ -43,9 +43,9 @@
#center-float { #center-float {
position: relative; position: relative;
margin: 0 auto auto -28.5em; width: 100%;
left: 60%; display: flex;
width: 700px; justify-content: center;
} }
</style> </style>
{% endblock %} {% endblock %}
@ -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 style="padding-top: 1em">{{ _('Avatar') }}: </td>
<td style="padding-top: 1em">{{ 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">

View 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 %}

198
websocket/daemon_amqp.js Normal file
View file

@ -0,0 +1,198 @@
var WebSocketServer = require('ws').Server;
var set = require('simplesets').Set;
var queue = require('qu');
var amqp = require('amqp');
var url = require('url');
if (typeof String.prototype.startsWith != 'function') {
String.prototype.startsWith = function (str){
return this.slice(0, str.length) == str;
};
}
const argv = require('yargs')
.demandCommand(3)
.strict()
.usage('Usage: event [options] <amqp url> <exchange> <port>')
.options({
host: {
default: '127.0.0.1',
describe: 'websocket address to listen on'
},
http_host: {
default: '127.0.0.1',
describe: 'http address to listen on'
},
http_port: {
default: null,
describe: 'http port to listen on'
},
max_queue: {
default: 10,
describe: 'queue buffer size'
},
comet_timeout: {
default: 60000,
describe: 'comet long poll timeout'
}
})
.argv;
var followers = new set();
var pollers = new set();
var messages = new queue();
var max_queue = argv.max_queue;
var comet_timeout = argv.comet_timeout;
var rabbitmq = amqp.createConnection({url: argv._[0]});
rabbitmq.on('error', function(e) {
console.log('amqp connection error...', e);
process.exit(1);
});
rabbitmq.on('ready', function () {
rabbitmq.queue('', {exclusive: true}, function (q) {
q.bind(argv._[1], '#');
q.subscribe(function (data) {
message = JSON.parse(data.data.toString('utf8'));
messages.push(message);
if (messages.length > max_queue)
messages.shift();
followers.each(function (client) {
client.got_message(message);
});
pollers.each(function (request) {
request.got_message(message);
});
});
});
});
var wss = new WebSocketServer({host: argv.host, port: parseInt(argv._[2])});
messages.catch_up = function (client) {
this.each(function (message) {
if (message.id > client.last_msg)
client.got_message(message);
});
};
wss.on('connection', function (socket) {
socket.channel = null;
socket.last_msg = 0;
var commands = {
start_msg: function (request) {
socket.last_msg = request.start;
},
set_filter: function (request) {
var filter = {};
if (Array.isArray(request.filter) && request.filter.length > 0 &&
request.filter.every(function (channel, index, array) {
if (typeof channel != 'string')
return false;
filter[channel] = true;
return true;
})) {
socket.filter = filter;
followers.add(socket);
messages.catch_up(socket);
} else {
socket.send(JSON.stringify({
status: 'error',
code: 'invalid-filter',
message: 'invalid filter: ' + request.filter
}));
}
}
};
socket.got_message = function (message) {
if (message.channel in socket.filter)
socket.send(JSON.stringify(message));
socket.last_msg = message.id;
};
socket.on('message', function (request) {
try {
request = JSON.parse(request);
if (typeof request.command !== 'string')
throw {message: 'no command specified'};
} catch (err) {
socket.send(JSON.stringify({
status: 'error',
code: 'syntax-error',
message: err.message
}));
return;
}
request.command = request.command.replace(/-/g, '_');
if (request.command in commands)
commands[request.command](request);
else
socket.send(JSON.stringify({
status: 'error',
code: 'bad-command',
message: 'bad command: ' + request.command
}));
});
socket.on('close', function(code, message) {
followers.remove(socket);
});
});
if (argv.http_port !== null) {
require('http').createServer(function (req, res) {
var parts = url.parse(req.url, true);
if (!parts.pathname.startsWith('/channels/')) {
res.writeHead(404, {'Content-Type': 'text/plain'});
res.end('404 Not Found');
return;
}
var channels = parts.pathname.slice(10).split('|');
if (channels.length == 1 && !channels[0].length) {
res.writeHead(400, {'Content-Type': 'text/plain'});
res.end('400 Bad Request');
return;
}
req.channels = {};
req.last_msg = parseInt(parts.query.last);
if (isNaN(req.last_msg)) req.last_msg = 0;
channels.forEach(function (channel) {
req.channels[channel] = true;
});
req.on('close', function () {
pollers.remove(req);
});
req.got_message = function (message) {
if (message.channel in req.channels) {
res.writeHead(200, {'Content-Type': 'application/json'});
res.end(JSON.stringify(message));
pollers.remove(req);
return true;
}
return false;
};
var got = false;
messages.each(function (message) {
if (!got && message.id > req.last_msg)
got = req.got_message(message);
});
if (!got) {
pollers.add(req);
res.setTimeout(comet_timeout, function () {
pollers.remove(req);
res.writeHead(504, {'Content-Type': 'application/json'});
res.end('{"error": "timeout"}');
});
}
}).listen(argv.http_port, argv.http_host);
}