Merge branch 'LQDJudge:master' into master
This commit is contained in:
commit
84aabb9dd5
28 changed files with 932 additions and 532 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 = {}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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()
|
||||||
|
|
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):
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
|
|
|
@ -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
|
@ -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 |
|
@ -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 %}
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -43,9 +43,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip {
|
.tooltip {
|
||||||
left: 120vh !important;
|
|
||||||
transform: translate(100px, 0) !important;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
#loader {
|
#loader {
|
||||||
|
|
|
@ -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'}}"/>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
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 %}
|
198
websocket/daemon_amqp.js
Normal file
198
websocket/daemon_amqp.js
Normal 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);
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue