Merge branch 'LQDJudge:master' into master
This commit is contained in:
commit
3fa6204cf6
76 changed files with 3585 additions and 2578 deletions
12
README.md
12
README.md
|
@ -115,14 +115,20 @@ python3 manage.py runserver 0.0.0.0:8000
|
||||||
|
|
||||||
1. (WSL) có thể tải ứng dụng Terminal trong Windows Store
|
1. (WSL) có thể tải ứng dụng Terminal trong Windows Store
|
||||||
2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery)
|
2. (WSL) mỗi lần mở ubuntu, các bạn cần chạy lệnh sau để mariadb khởi động: `sudo service mysql restart` (tương tự cho một số service khác như memcached, celery)
|
||||||
3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok.
|
3. Sau khi cài đặt, các bạn chỉ cần activate virtual env và chạy lệnh runserver là ok
|
||||||
|
|
||||||
```jsx
|
```jsx
|
||||||
. dmojsite/bin/activate
|
. dmojsite/bin/activate
|
||||||
python3 manage.py runserver
|
python3 manage.py runserver
|
||||||
```
|
```
|
||||||
|
5. Đối với nginx, sau khi config xong theo guide của DMOJ, bạn cần thêm location như sau để sử dụng được tính năng profile image, thay thế `path/to/oj` thành đường dẫn nơi bạn đã clone source code.
|
||||||
|
|
||||||
1. Quy trình dev:
|
```
|
||||||
|
location /profile_images/ {
|
||||||
|
root /path/to/oj;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
6. Quy trình dev:
|
||||||
1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5
|
1. Sau khi thay đổi code thì django tự build lại, các bạn chỉ cần F5
|
||||||
2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi.
|
2. Một số style nằm trong các file .scss. Các bạn cần recompile css thì mới thấy được thay đổi.
|
||||||
|
|
||||||
|
|
20
chat_box/migrations/0013_alter_message_time.py
Normal file
20
chat_box/migrations/0013_alter_message_time.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 01:24
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0012_auto_20230308_1417"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="message",
|
||||||
|
name="time",
|
||||||
|
field=models.DateTimeField(
|
||||||
|
auto_now_add=True, db_index=True, verbose_name="posted time"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
38
chat_box/migrations/0014_userroom_unread_count.py
Normal file
38
chat_box/migrations/0014_userroom_unread_count.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 06:02
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate(apps, schema_editor):
|
||||||
|
UserRoom = apps.get_model("chat_box", "UserRoom")
|
||||||
|
Message = apps.get_model("chat_box", "Message")
|
||||||
|
|
||||||
|
for ur in UserRoom.objects.all():
|
||||||
|
if not ur.room:
|
||||||
|
continue
|
||||||
|
messages = ur.room.message_set
|
||||||
|
last_msg = messages.first()
|
||||||
|
try:
|
||||||
|
if last_msg and last_msg.author != ur.user:
|
||||||
|
ur.unread_count = messages.filter(time__gte=ur.last_seen).count()
|
||||||
|
else:
|
||||||
|
ur.unread_count = 0
|
||||||
|
ur.save()
|
||||||
|
except:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("chat_box", "0013_alter_message_time"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="userroom",
|
||||||
|
name="unread_count",
|
||||||
|
field=models.IntegerField(db_index=True, default=0),
|
||||||
|
),
|
||||||
|
migrations.RunPython(migrate, migrations.RunPython.noop, atomic=True),
|
||||||
|
]
|
|
@ -1,9 +1,10 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import CASCADE
|
from django.db.models import CASCADE, Q
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
from judge.models.profile import Profile
|
from judge.models.profile import Profile
|
||||||
|
from judge.caching import cache_wrapper
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
|
__all__ = ["Message", "Room", "UserRoom", "Ignore"]
|
||||||
|
@ -17,19 +18,28 @@ class Room(models.Model):
|
||||||
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
Profile, related_name="user_two", verbose_name="user 2", on_delete=CASCADE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rc")
|
||||||
def contain(self, profile):
|
def contain(self, profile):
|
||||||
return self.user_one == profile or self.user_two == profile
|
return self.user_one == profile or self.user_two == profile
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rou")
|
||||||
def other_user(self, profile):
|
def other_user(self, profile):
|
||||||
return self.user_one if profile == self.user_two else self.user_two
|
return self.user_one if profile == self.user_two else self.user_two
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rus")
|
||||||
def users(self):
|
def users(self):
|
||||||
return [self.user_one, self.user_two]
|
return [self.user_one, self.user_two]
|
||||||
|
|
||||||
|
@cache_wrapper(prefix="Rlmb")
|
||||||
|
def last_message_body(self):
|
||||||
|
return self.message_set.first().body
|
||||||
|
|
||||||
|
|
||||||
class Message(models.Model):
|
class Message(models.Model):
|
||||||
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
author = models.ForeignKey(Profile, verbose_name=_("user"), on_delete=CASCADE)
|
||||||
time = models.DateTimeField(verbose_name=_("posted time"), auto_now_add=True)
|
time = models.DateTimeField(
|
||||||
|
verbose_name=_("posted time"), auto_now_add=True, db_index=True
|
||||||
|
)
|
||||||
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
body = models.TextField(verbose_name=_("body of comment"), max_length=8192)
|
||||||
hidden = models.BooleanField(verbose_name="is hidden", default=False)
|
hidden = models.BooleanField(verbose_name="is hidden", default=False)
|
||||||
room = models.ForeignKey(
|
room = models.ForeignKey(
|
||||||
|
@ -56,6 +66,7 @@ class UserRoom(models.Model):
|
||||||
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
Room, verbose_name="room id", on_delete=CASCADE, default=None, null=True
|
||||||
)
|
)
|
||||||
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
|
last_seen = models.DateTimeField(verbose_name=_("last seen"), auto_now_add=True)
|
||||||
|
unread_count = models.IntegerField(default=0, db_index=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = ("user", "room")
|
unique_together = ("user", "room")
|
||||||
|
@ -74,11 +85,9 @@ class Ignore(models.Model):
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_ignored(self, current_user, new_friend):
|
def is_ignored(self, current_user, new_friend):
|
||||||
try:
|
try:
|
||||||
return (
|
return current_user.ignored_chat_users.ignored_users.filter(
|
||||||
current_user.ignored_chat_users.get()
|
id=new_friend.id
|
||||||
.ignored_users.filter(id=new_friend.id)
|
).exists()
|
||||||
.exists()
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -89,6 +98,16 @@ class Ignore(models.Model):
|
||||||
except:
|
except:
|
||||||
return Profile.objects.none()
|
return Profile.objects.none()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_ignored_rooms(self, user):
|
||||||
|
try:
|
||||||
|
ignored_users = self.objects.get(user=user).ignored_users.all()
|
||||||
|
return Room.objects.filter(Q(user_one=user) | Q(user_two=user)).filter(
|
||||||
|
Q(user_one__in=ignored_users) | Q(user_two__in=ignored_users)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
return Room.objects.none()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_ignore(self, current_user, friend):
|
def add_ignore(self, current_user, friend):
|
||||||
ignore, created = self.objects.get_or_create(user=current_user)
|
ignore, created = self.objects.get_or_create(user=current_user)
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
from cryptography.fernet import Fernet
|
from cryptography.fernet import Fernet
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db.models import OuterRef, Count, Subquery, IntegerField
|
from django.db.models import OuterRef, Count, Subquery, IntegerField, Q
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
|
|
||||||
from chat_box.models import Ignore, Message, UserRoom
|
from chat_box.models import Ignore, Message, UserRoom, Room
|
||||||
|
|
||||||
secret_key = settings.CHAT_SECRET_KEY
|
secret_key = settings.CHAT_SECRET_KEY
|
||||||
fernet = Fernet(secret_key)
|
fernet = Fernet(secret_key)
|
||||||
|
@ -24,25 +26,22 @@ def decrypt_url(message_encrypted):
|
||||||
return None, None
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
def get_unread_boxes(profile):
|
def encrypt_channel(channel):
|
||||||
ignored_users = Ignore.get_ignored_users(profile)
|
return (
|
||||||
|
hmac.new(
|
||||||
mess = (
|
settings.CHAT_SECRET_KEY.encode(),
|
||||||
Message.objects.filter(room=OuterRef("room"), time__gte=OuterRef("last_seen"))
|
channel.encode(),
|
||||||
.exclude(author=profile)
|
hashlib.sha512,
|
||||||
.exclude(author__in=ignored_users)
|
).hexdigest()[:16]
|
||||||
.order_by()
|
+ "%s" % channel
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_unread_boxes(profile):
|
||||||
|
ignored_rooms = Ignore.get_ignored_rooms(profile)
|
||||||
unread_boxes = (
|
unread_boxes = (
|
||||||
UserRoom.objects.filter(user=profile, room__isnull=False)
|
UserRoom.objects.filter(user=profile, unread_count__gt=0)
|
||||||
.annotate(
|
.exclude(room__in=ignored_rooms)
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
)
|
|
||||||
.filter(unread_count__gte=1)
|
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,8 @@ from django.db.models import (
|
||||||
Exists,
|
Exists,
|
||||||
Count,
|
Count,
|
||||||
IntegerField,
|
IntegerField,
|
||||||
|
F,
|
||||||
|
Max,
|
||||||
)
|
)
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
@ -34,7 +36,7 @@ from judge.jinja2.gravatar import gravatar
|
||||||
from judge.models import Friend
|
from judge.models import Friend
|
||||||
|
|
||||||
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
from chat_box.models import Message, Profile, Room, UserRoom, Ignore
|
||||||
from chat_box.utils import encrypt_url, decrypt_url
|
from chat_box.utils import encrypt_url, decrypt_url, encrypt_channel
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
@ -49,7 +51,8 @@ class ChatView(ListView):
|
||||||
self.room_id = None
|
self.room_id = None
|
||||||
self.room = None
|
self.room = None
|
||||||
self.messages = None
|
self.messages = None
|
||||||
self.page_size = 20
|
self.first_page_size = 20 # only for first request
|
||||||
|
self.follow_up_page_size = 50
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return self.messages
|
return self.messages
|
||||||
|
@ -63,10 +66,12 @@ class ChatView(ListView):
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
request_room = kwargs["room_id"]
|
request_room = kwargs["room_id"]
|
||||||
|
page_size = self.follow_up_page_size
|
||||||
try:
|
try:
|
||||||
last_id = int(request.GET.get("last_id"))
|
last_id = int(request.GET.get("last_id"))
|
||||||
except Exception:
|
except Exception:
|
||||||
last_id = 1e15
|
last_id = 1e15
|
||||||
|
page_size = self.first_page_size
|
||||||
only_messages = request.GET.get("only_messages")
|
only_messages = request.GET.get("only_messages")
|
||||||
|
|
||||||
if request_room:
|
if request_room:
|
||||||
|
@ -80,11 +85,12 @@ class ChatView(ListView):
|
||||||
request_room = None
|
request_room = None
|
||||||
|
|
||||||
self.room_id = request_room
|
self.room_id = request_room
|
||||||
self.messages = Message.objects.filter(
|
self.messages = (
|
||||||
hidden=False, room=self.room_id, id__lt=last_id
|
Message.objects.filter(hidden=False, room=self.room_id, id__lt=last_id)
|
||||||
)[: self.page_size]
|
.select_related("author", "author__user")
|
||||||
|
.defer("author__about", "author__user_script")[:page_size]
|
||||||
|
)
|
||||||
if not only_messages:
|
if not only_messages:
|
||||||
update_last_seen(request, **kwargs)
|
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
|
@ -101,10 +107,14 @@ class ChatView(ListView):
|
||||||
|
|
||||||
context["title"] = self.title
|
context["title"] = self.title
|
||||||
context["last_msg"] = event.last()
|
context["last_msg"] = event.last()
|
||||||
context["status_sections"] = get_status_context(self.request)
|
context["status_sections"] = get_status_context(self.request.profile)
|
||||||
context["room"] = self.room_id
|
context["room"] = self.room_id
|
||||||
context["has_next"] = self.has_next()
|
context["has_next"] = self.has_next()
|
||||||
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
|
context["unread_count_lobby"] = get_unread_count(None, self.request.profile)
|
||||||
|
context["chat_channel"] = encrypt_channel(
|
||||||
|
"chat_" + str(self.request.profile.id)
|
||||||
|
)
|
||||||
|
context["chat_lobby_channel"] = encrypt_channel("chat_lobby")
|
||||||
if self.room:
|
if self.room:
|
||||||
users_room = [self.room.user_one, self.room.user_two]
|
users_room = [self.room.user_one, self.room.user_two]
|
||||||
users_room.remove(self.request.profile)
|
users_room.remove(self.request.profile)
|
||||||
|
@ -187,7 +197,7 @@ def post_message(request):
|
||||||
|
|
||||||
if not room:
|
if not room:
|
||||||
event.post(
|
event.post(
|
||||||
"chat_lobby",
|
encrypt_channel("chat_lobby"),
|
||||||
{
|
{
|
||||||
"type": "lobby",
|
"type": "lobby",
|
||||||
"author_id": request.profile.id,
|
"author_id": request.profile.id,
|
||||||
|
@ -197,9 +207,10 @@ def post_message(request):
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
Room.last_message_body.dirty(room)
|
||||||
for user in room.users():
|
for user in room.users():
|
||||||
event.post(
|
event.post(
|
||||||
"chat_" + str(user.id),
|
encrypt_channel("chat_" + str(user.id)),
|
||||||
{
|
{
|
||||||
"type": "private",
|
"type": "private",
|
||||||
"author_id": request.profile.id,
|
"author_id": request.profile.id,
|
||||||
|
@ -208,6 +219,10 @@ def post_message(request):
|
||||||
"tmp_id": request.POST.get("tmp_id"),
|
"tmp_id": request.POST.get("tmp_id"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if user != request.profile:
|
||||||
|
UserRoom.objects.filter(user=user, room=room).update(
|
||||||
|
unread_count=F("unread_count") + 1
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse(ret)
|
return JsonResponse(ret)
|
||||||
|
|
||||||
|
@ -254,35 +269,33 @@ def update_last_seen(request, **kwargs):
|
||||||
room_id = request.POST.get("room")
|
room_id = request.POST.get("room")
|
||||||
else:
|
else:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile = request.profile
|
profile = request.profile
|
||||||
room = None
|
room = None
|
||||||
if room_id:
|
if room_id:
|
||||||
room = Room.objects.get(id=int(room_id))
|
room = Room.objects.filter(id=int(room_id)).first()
|
||||||
except Room.DoesNotExist:
|
except Room.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
except Exception as e:
|
|
||||||
return HttpResponseBadRequest()
|
|
||||||
|
|
||||||
if room and not room.contain(profile):
|
if room and not room.contain(profile):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room)
|
||||||
user_room.last_seen = timezone.now()
|
user_room.last_seen = timezone.now()
|
||||||
|
user_room.unread_count = 0
|
||||||
user_room.save()
|
user_room.save()
|
||||||
|
|
||||||
return JsonResponse({"msg": "updated"})
|
return JsonResponse({"msg": "updated"})
|
||||||
|
|
||||||
|
|
||||||
def get_online_count():
|
def get_online_count():
|
||||||
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
return Profile.objects.filter(last_access__gte=last_two_minutes).count()
|
return Profile.objects.filter(last_access__gte=last_5_minutes).count()
|
||||||
|
|
||||||
|
|
||||||
def get_user_online_status(user):
|
def get_user_online_status(user):
|
||||||
time_diff = timezone.now() - user.last_access
|
time_diff = timezone.now() - user.last_access
|
||||||
is_online = time_diff <= timezone.timedelta(minutes=2)
|
is_online = time_diff <= timezone.timedelta(minutes=5)
|
||||||
return is_online
|
return is_online
|
||||||
|
|
||||||
|
|
||||||
|
@ -319,47 +332,67 @@ def user_online_status_ajax(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_online_status(request_user, queryset, rooms=None):
|
def get_online_status(profile, other_profile_ids, rooms=None):
|
||||||
if not queryset:
|
if not other_profile_ids:
|
||||||
return None
|
return None
|
||||||
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
|
joined_ids = ",".join([str(id) for id in other_profile_ids])
|
||||||
|
other_profiles = Profile.objects.raw(
|
||||||
|
f"SELECT * from judge_profile where id in ({joined_ids}) order by field(id,{joined_ids})"
|
||||||
|
)
|
||||||
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
if rooms:
|
if rooms:
|
||||||
unread_count = get_unread_count(rooms, request_user)
|
unread_count = get_unread_count(rooms, profile)
|
||||||
count = {}
|
count = {}
|
||||||
|
last_msg = {}
|
||||||
|
room_of_user = {}
|
||||||
for i in unread_count:
|
for i in unread_count:
|
||||||
count[i["other_user"]] = i["unread_count"]
|
room = Room.objects.get(id=i["room"])
|
||||||
|
other_profile = room.other_user(profile)
|
||||||
|
count[other_profile.id] = i["unread_count"]
|
||||||
|
for room in rooms:
|
||||||
|
room = Room.objects.get(id=room)
|
||||||
|
other_profile = room.other_user(profile)
|
||||||
|
last_msg[other_profile.id] = room.last_message_body()
|
||||||
|
room_of_user[other_profile.id] = room.id
|
||||||
|
|
||||||
for user in queryset:
|
for other_profile in other_profiles:
|
||||||
is_online = False
|
is_online = False
|
||||||
if user.last_access >= last_two_minutes:
|
if other_profile.last_access >= last_5_minutes:
|
||||||
is_online = True
|
is_online = True
|
||||||
user_dict = {"user": user, "is_online": is_online}
|
user_dict = {"user": other_profile, "is_online": is_online}
|
||||||
if rooms and user.id in count:
|
if rooms:
|
||||||
user_dict["unread_count"] = count[user.id]
|
user_dict.update(
|
||||||
user_dict["url"] = encrypt_url(request_user.id, user.id)
|
{
|
||||||
|
"unread_count": count.get(other_profile.id),
|
||||||
|
"last_msg": last_msg.get(other_profile.id),
|
||||||
|
"room": room_of_user.get(other_profile.id),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
user_dict["url"] = encrypt_url(profile.id, other_profile.id)
|
||||||
ret.append(user_dict)
|
ret.append(user_dict)
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def get_status_context(request, include_ignored=False):
|
def get_status_context(profile, include_ignored=False):
|
||||||
if include_ignored:
|
if include_ignored:
|
||||||
ignored_users = Profile.objects.none()
|
ignored_users = []
|
||||||
queryset = Profile.objects
|
queryset = Profile.objects
|
||||||
else:
|
else:
|
||||||
ignored_users = Ignore.get_ignored_users(request.profile)
|
ignored_users = list(
|
||||||
|
Ignore.get_ignored_users(profile).values_list("id", flat=True)
|
||||||
|
)
|
||||||
queryset = Profile.objects.exclude(id__in=ignored_users)
|
queryset = Profile.objects.exclude(id__in=ignored_users)
|
||||||
|
|
||||||
last_two_minutes = timezone.now() - timezone.timedelta(minutes=2)
|
last_5_minutes = timezone.now() - timezone.timedelta(minutes=5)
|
||||||
recent_profile = (
|
recent_profile = (
|
||||||
Room.objects.filter(Q(user_one=request.profile) | Q(user_two=request.profile))
|
Room.objects.filter(Q(user_one=profile) | Q(user_two=profile))
|
||||||
.annotate(
|
.annotate(
|
||||||
last_msg_time=Subquery(
|
last_msg_time=Subquery(
|
||||||
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
|
Message.objects.filter(room=OuterRef("pk")).values("time")[:1]
|
||||||
),
|
),
|
||||||
other_user=Case(
|
other_user=Case(
|
||||||
When(user_one=request.profile, then="user_two"),
|
When(user_one=profile, then="user_two"),
|
||||||
default="user_one",
|
default="user_one",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
@ -369,50 +402,36 @@ def get_status_context(request, include_ignored=False):
|
||||||
.values("other_user", "id")[:20]
|
.values("other_user", "id")[:20]
|
||||||
)
|
)
|
||||||
|
|
||||||
recent_profile_id = [str(i["other_user"]) for i in recent_profile]
|
recent_profile_ids = [str(i["other_user"]) for i in recent_profile]
|
||||||
joined_id = ",".join(recent_profile_id)
|
|
||||||
recent_rooms = [int(i["id"]) for i in recent_profile]
|
recent_rooms = [int(i["id"]) for i in recent_profile]
|
||||||
recent_list = None
|
|
||||||
if joined_id:
|
|
||||||
recent_list = Profile.objects.raw(
|
|
||||||
f"SELECT * from judge_profile where id in ({joined_id}) order by field(id,{joined_id})"
|
|
||||||
)
|
|
||||||
friend_list = (
|
|
||||||
Friend.get_friend_profiles(request.profile)
|
|
||||||
.exclude(id__in=recent_profile_id)
|
|
||||||
.exclude(id__in=ignored_users)
|
|
||||||
.order_by("-last_access")
|
|
||||||
)
|
|
||||||
admin_list = (
|
admin_list = (
|
||||||
queryset.filter(display_rank="admin")
|
queryset.filter(display_rank="admin")
|
||||||
.exclude(id__in=friend_list)
|
.exclude(id__in=recent_profile_ids)
|
||||||
.exclude(id__in=recent_profile_id)
|
.values_list("id", flat=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
all_user_status = (
|
all_user_status = (
|
||||||
queryset.filter(display_rank="user", last_access__gte=last_two_minutes)
|
queryset.filter(last_access__gte=last_5_minutes)
|
||||||
.annotate(is_online=Case(default=True, output_field=BooleanField()))
|
.annotate(is_online=Case(default=True, output_field=BooleanField()))
|
||||||
.order_by("-rating")
|
.order_by("-rating")
|
||||||
.exclude(id__in=friend_list)
|
|
||||||
.exclude(id__in=admin_list)
|
.exclude(id__in=admin_list)
|
||||||
.exclude(id__in=recent_profile_id)[:30]
|
.exclude(id__in=recent_profile_ids)
|
||||||
|
.values_list("id", flat=True)[:30]
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"title": "Recent",
|
"title": "Recent",
|
||||||
"user_list": get_online_status(request.profile, recent_list, recent_rooms),
|
"user_list": get_online_status(profile, recent_profile_ids, recent_rooms),
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Following",
|
|
||||||
"user_list": get_online_status(request.profile, friend_list),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Admin",
|
"title": "Admin",
|
||||||
"user_list": get_online_status(request.profile, admin_list),
|
"user_list": get_online_status(profile, admin_list),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Other",
|
"title": "Other",
|
||||||
"user_list": get_online_status(request.profile, all_user_status),
|
"user_list": get_online_status(profile, all_user_status),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -423,7 +442,7 @@ def online_status_ajax(request):
|
||||||
request,
|
request,
|
||||||
"chat/online_status.html",
|
"chat/online_status.html",
|
||||||
{
|
{
|
||||||
"status_sections": get_status_context(request),
|
"status_sections": get_status_context(request.profile),
|
||||||
"unread_count_lobby": get_unread_count(None, request.profile),
|
"unread_count_lobby": get_unread_count(None, request.profile),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -447,7 +466,6 @@ def get_or_create_room(request):
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
request_id, other_id = decrypt_url(decrypted_other_id)
|
request_id, other_id = decrypt_url(decrypted_other_id)
|
||||||
|
|
||||||
if not other_id or not request_id or request_id != request.profile.id:
|
if not other_id or not request_id or request_id != request.profile.id:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
@ -475,48 +493,22 @@ def get_or_create_room(request):
|
||||||
|
|
||||||
def get_unread_count(rooms, user):
|
def get_unread_count(rooms, user):
|
||||||
if rooms:
|
if rooms:
|
||||||
mess = (
|
return UserRoom.objects.filter(
|
||||||
Message.objects.filter(
|
user=user, room__in=rooms, unread_count__gt=0
|
||||||
room=OuterRef("room"), time__gte=OuterRef("last_seen")
|
).values("unread_count", "room")
|
||||||
)
|
|
||||||
.exclude(author=user)
|
|
||||||
.order_by()
|
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
|
||||||
UserRoom.objects.filter(user=user, room__in=rooms)
|
|
||||||
.annotate(
|
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
|
||||||
other_user=Case(
|
|
||||||
When(room__user_one=user, then="room__user_two"),
|
|
||||||
default="room__user_one",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.filter(unread_count__gte=1)
|
|
||||||
.values("other_user", "unread_count")
|
|
||||||
)
|
|
||||||
else: # lobby
|
else: # lobby
|
||||||
mess = (
|
user_room = UserRoom.objects.filter(user=user, room__isnull=True).first()
|
||||||
Message.objects.filter(room__isnull=True, time__gte=OuterRef("last_seen"))
|
if not user_room:
|
||||||
.exclude(author=user)
|
return 0
|
||||||
.order_by()
|
last_seen = user_room.last_seen
|
||||||
.values("room")
|
|
||||||
.annotate(unread_count=Count("pk"))
|
|
||||||
.values("unread_count")
|
|
||||||
)
|
|
||||||
|
|
||||||
res = (
|
res = (
|
||||||
UserRoom.objects.filter(user=user, room__isnull=True)
|
Message.objects.filter(room__isnull=True, time__gte=last_seen)
|
||||||
.annotate(
|
.exclude(author=user)
|
||||||
unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0),
|
.exclude(hidden=True)
|
||||||
)
|
.count()
|
||||||
.values_list("unread_count", flat=True)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return res[0] if len(res) else 0
|
return res
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|
|
@ -477,10 +477,16 @@ ML_OUTPUT_PATH = None
|
||||||
# Use subdomain for organizations
|
# Use subdomain for organizations
|
||||||
USE_SUBDOMAIN = False
|
USE_SUBDOMAIN = False
|
||||||
|
|
||||||
|
# Chat
|
||||||
|
CHAT_SECRET_KEY = "QUdVFsxk6f5-Hd8g9BXv81xMqvIZFRqMl-KbRzztW-U="
|
||||||
|
|
||||||
|
# Nginx
|
||||||
|
META_REMOTE_ADDRESS_KEY = "REMOTE_ADDR"
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
|
with open(os.path.join(os.path.dirname(__file__), "local_settings.py")) as f:
|
||||||
exec(f.read(), globals())
|
exec(f.read(), globals())
|
||||||
except IOError:
|
except IOError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
|
|
||||||
|
|
20
dmoj/urls.py
20
dmoj/urls.py
|
@ -65,6 +65,7 @@ from judge.views import (
|
||||||
internal,
|
internal,
|
||||||
resolver,
|
resolver,
|
||||||
course,
|
course,
|
||||||
|
email,
|
||||||
)
|
)
|
||||||
from judge.views.problem_data import (
|
from judge.views.problem_data import (
|
||||||
ProblemDataView,
|
ProblemDataView,
|
||||||
|
@ -104,19 +105,19 @@ register_patterns = [
|
||||||
# confusing 404.
|
# confusing 404.
|
||||||
url(
|
url(
|
||||||
r"^activate/(?P<activation_key>\w+)/$",
|
r"^activate/(?P<activation_key>\w+)/$",
|
||||||
ActivationView.as_view(title="Activation key invalid"),
|
ActivationView.as_view(title=_("Activation key invalid")),
|
||||||
name="registration_activate",
|
name="registration_activate",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^register/$",
|
r"^register/$",
|
||||||
RegistrationView.as_view(title="Register"),
|
RegistrationView.as_view(title=_("Register")),
|
||||||
name="registration_register",
|
name="registration_register",
|
||||||
),
|
),
|
||||||
url(
|
url(
|
||||||
r"^register/complete/$",
|
r"^register/complete/$",
|
||||||
TitledTemplateView.as_view(
|
TitledTemplateView.as_view(
|
||||||
template_name="registration/registration_complete.html",
|
template_name="registration/registration_complete.html",
|
||||||
title="Registration Completed",
|
title=_("Registration Completed"),
|
||||||
),
|
),
|
||||||
name="registration_complete",
|
name="registration_complete",
|
||||||
),
|
),
|
||||||
|
@ -124,7 +125,7 @@ register_patterns = [
|
||||||
r"^register/closed/$",
|
r"^register/closed/$",
|
||||||
TitledTemplateView.as_view(
|
TitledTemplateView.as_view(
|
||||||
template_name="registration/registration_closed.html",
|
template_name="registration/registration_closed.html",
|
||||||
title="Registration not allowed",
|
title=_("Registration not allowed"),
|
||||||
),
|
),
|
||||||
name="registration_disallowed",
|
name="registration_disallowed",
|
||||||
),
|
),
|
||||||
|
@ -183,6 +184,17 @@ register_patterns = [
|
||||||
),
|
),
|
||||||
name="password_reset_done",
|
name="password_reset_done",
|
||||||
),
|
),
|
||||||
|
url(r"^email/change/$", email.email_change_view, name="email_change"),
|
||||||
|
url(
|
||||||
|
r"^email/change/verify/(?P<uidb64>[0-9A-Za-z]+)-(?P<token>.+)/$",
|
||||||
|
email.verify_email_view,
|
||||||
|
name="email_change_verify",
|
||||||
|
),
|
||||||
|
url(
|
||||||
|
r"^email/change/pending$",
|
||||||
|
email.email_change_pending_view,
|
||||||
|
name="email_change_pending",
|
||||||
|
),
|
||||||
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
|
url(r"^social/error/$", register.social_auth_error, name="social_auth_error"),
|
||||||
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
|
url(r"^2fa/$", totp.TOTPLoginView.as_view(), name="login_2fa"),
|
||||||
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
|
url(r"^2fa/enable/$", totp.TOTPEnableView.as_view(), name="enable_2fa"),
|
||||||
|
|
|
@ -71,7 +71,6 @@ class ContestProblemInlineForm(ModelForm):
|
||||||
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
"hidden_subtasks": TextInput(attrs={"size": "3"}),
|
||||||
"points": TextInput(attrs={"size": "1"}),
|
"points": TextInput(attrs={"size": "1"}),
|
||||||
"order": TextInput(attrs={"size": "1"}),
|
"order": TextInput(attrs={"size": "1"}),
|
||||||
"output_prefix_override": TextInput(attrs={"size": "1"}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,7 +85,7 @@ class ContestProblemInline(admin.TabularInline):
|
||||||
"is_pretested",
|
"is_pretested",
|
||||||
"max_submissions",
|
"max_submissions",
|
||||||
"hidden_subtasks",
|
"hidden_subtasks",
|
||||||
"output_prefix_override",
|
"show_testcases",
|
||||||
"order",
|
"order",
|
||||||
"rejudge_column",
|
"rejudge_column",
|
||||||
)
|
)
|
||||||
|
|
|
@ -5,9 +5,10 @@ from django.db.models.query import QuerySet
|
||||||
import hashlib
|
import hashlib
|
||||||
|
|
||||||
MAX_NUM_CHAR = 15
|
MAX_NUM_CHAR = 15
|
||||||
|
NONE_RESULT = "__None__"
|
||||||
|
|
||||||
|
|
||||||
def cache_wrapper(prefix, timeout=86400):
|
def cache_wrapper(prefix, timeout=None):
|
||||||
def arg_to_str(arg):
|
def arg_to_str(arg):
|
||||||
if hasattr(arg, "id"):
|
if hasattr(arg, "id"):
|
||||||
return str(arg.id)
|
return str(arg.id)
|
||||||
|
@ -31,8 +32,11 @@ def cache_wrapper(prefix, timeout=86400):
|
||||||
cache_key = get_key(func, *args, **kwargs)
|
cache_key = get_key(func, *args, **kwargs)
|
||||||
result = cache.get(cache_key)
|
result = cache.get(cache_key)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
|
if result == NONE_RESULT:
|
||||||
|
result = None
|
||||||
return result
|
return result
|
||||||
|
if result is None:
|
||||||
|
result = NONE_RESULT
|
||||||
result = func(*args, **kwargs)
|
result = func(*args, **kwargs)
|
||||||
cache.set(cache_key, result, timeout)
|
cache.set(cache_key, result, timeout)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -4,4 +4,5 @@ from judge.contest_format.ecoo import ECOOContestFormat
|
||||||
from judge.contest_format.icpc import ICPCContestFormat
|
from judge.contest_format.icpc import ICPCContestFormat
|
||||||
from judge.contest_format.ioi import IOIContestFormat
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
from judge.contest_format.new_ioi import NewIOIContestFormat
|
from judge.contest_format.new_ioi import NewIOIContestFormat
|
||||||
|
from judge.contest_format.ultimate import UltimateContestFormat
|
||||||
from judge.contest_format.registry import choices, formats
|
from judge.contest_format.registry import choices, formats
|
||||||
|
|
55
judge/contest_format/ultimate.py
Normal file
55
judge/contest_format/ultimate.py
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
from django.utils.translation import gettext_lazy
|
||||||
|
|
||||||
|
from judge.contest_format.ioi import IOIContestFormat
|
||||||
|
from judge.contest_format.registry import register_contest_format
|
||||||
|
from django.db.models import Min, OuterRef, Subquery
|
||||||
|
|
||||||
|
# This contest format only counts last submission for each problem.
|
||||||
|
|
||||||
|
|
||||||
|
@register_contest_format("ultimate")
|
||||||
|
class UltimateContestFormat(IOIContestFormat):
|
||||||
|
name = gettext_lazy("Ultimate")
|
||||||
|
|
||||||
|
def update_participation(self, participation):
|
||||||
|
cumtime = 0
|
||||||
|
score = 0
|
||||||
|
format_data = {}
|
||||||
|
|
||||||
|
queryset = participation.submissions
|
||||||
|
if self.contest.freeze_after:
|
||||||
|
queryset = queryset.filter(
|
||||||
|
submission__date__lt=participation.start + self.contest.freeze_after
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = (
|
||||||
|
queryset.values("problem_id")
|
||||||
|
.filter(
|
||||||
|
id=Subquery(
|
||||||
|
queryset.filter(problem_id=OuterRef("problem_id"))
|
||||||
|
.order_by("-id")
|
||||||
|
.values("id")[:1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.values_list("problem_id", "submission__date", "points")
|
||||||
|
)
|
||||||
|
|
||||||
|
for problem_id, time, points in queryset:
|
||||||
|
if self.config["cumtime"]:
|
||||||
|
dt = (time - participation.start).total_seconds()
|
||||||
|
if points:
|
||||||
|
cumtime += dt
|
||||||
|
else:
|
||||||
|
dt = 0
|
||||||
|
format_data[str(problem_id)] = {
|
||||||
|
"time": dt,
|
||||||
|
"points": points,
|
||||||
|
}
|
||||||
|
score += points
|
||||||
|
|
||||||
|
self.handle_frozen_state(participation, format_data)
|
||||||
|
participation.cumtime = max(cumtime, 0)
|
||||||
|
participation.score = round(score, self.contest.points_precision)
|
||||||
|
participation.tiebreaker = 0
|
||||||
|
participation.format_data = format_data
|
||||||
|
participation.save()
|
|
@ -80,6 +80,7 @@ class ProfileForm(ModelForm):
|
||||||
"ace_theme",
|
"ace_theme",
|
||||||
"user_script",
|
"user_script",
|
||||||
"profile_image",
|
"profile_image",
|
||||||
|
"css_background",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"user_script": AceWidget(theme="github"),
|
"user_script": AceWidget(theme="github"),
|
||||||
|
@ -87,6 +88,7 @@ class ProfileForm(ModelForm):
|
||||||
"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,
|
"profile_image": ImageWidget,
|
||||||
|
"css_background": forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
|
||||||
has_math_config = bool(settings.MATHOID_URL)
|
has_math_config = bool(settings.MATHOID_URL)
|
||||||
|
@ -528,19 +530,47 @@ class ContestProblemForm(ModelForm):
|
||||||
"problem",
|
"problem",
|
||||||
"points",
|
"points",
|
||||||
"partial",
|
"partial",
|
||||||
"output_prefix_override",
|
"show_testcases",
|
||||||
"max_submissions",
|
"max_submissions",
|
||||||
)
|
)
|
||||||
widgets = {
|
widgets = {
|
||||||
"problem": HeavySelect2Widget(
|
"problem": HeavySelect2Widget(
|
||||||
data_view="problem_select2", attrs={"style": "width:100%"}
|
data_view="problem_select2", attrs={"style": "width: 100%"}
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ContestProblemModelFormSet(BaseModelFormSet):
|
||||||
|
def is_valid(self):
|
||||||
|
valid = super().is_valid()
|
||||||
|
|
||||||
|
if not valid:
|
||||||
|
return valid
|
||||||
|
|
||||||
|
problems = set()
|
||||||
|
duplicates = []
|
||||||
|
|
||||||
|
for form in self.forms:
|
||||||
|
if form.cleaned_data and not form.cleaned_data.get("DELETE", False):
|
||||||
|
problem = form.cleaned_data.get("problem")
|
||||||
|
if problem in problems:
|
||||||
|
duplicates.append(problem)
|
||||||
|
else:
|
||||||
|
problems.add(problem)
|
||||||
|
|
||||||
|
if duplicates:
|
||||||
|
for form in self.forms:
|
||||||
|
problem = form.cleaned_data.get("problem")
|
||||||
|
if problem in duplicates:
|
||||||
|
form.add_error("problem", _("This problem is duplicated."))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class ContestProblemFormSet(
|
class ContestProblemFormSet(
|
||||||
formset_factory(
|
formset_factory(
|
||||||
ContestProblemForm, formset=BaseModelFormSet, extra=6, can_delete=True
|
ContestProblemForm, formset=ContestProblemModelFormSet, extra=6, can_delete=True
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
model = ContestProblem
|
model = ContestProblem
|
||||||
|
|
18
judge/migrations/0163_email_change.py
Normal file
18
judge/migrations/0163_email_change.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 00:19
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0162_profile_image"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="email_change_pending",
|
||||||
|
field=models.EmailField(blank=True, max_length=254, null=True),
|
||||||
|
),
|
||||||
|
]
|
30
judge/migrations/0164_show_testcase.py
Normal file
30
judge/migrations/0164_show_testcase.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 23:03
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_show_testcases(apps, schema_editor):
|
||||||
|
ContestProblem = apps.get_model("judge", "ContestProblem")
|
||||||
|
|
||||||
|
for c in ContestProblem.objects.all():
|
||||||
|
if c.output_prefix_override == 1:
|
||||||
|
c.show_testcases = True
|
||||||
|
c.save()
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0163_email_change"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="contestproblem",
|
||||||
|
name="show_testcases",
|
||||||
|
field=models.BooleanField(default=False, verbose_name="visible testcases"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(
|
||||||
|
migrate_show_testcases, migrations.RunPython.noop, atomic=True
|
||||||
|
),
|
||||||
|
]
|
17
judge/migrations/0165_drop_output_prefix_override.py
Normal file
17
judge/migrations/0165_drop_output_prefix_override.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-25 23:11
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0164_show_testcase"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name="contestproblem",
|
||||||
|
name="output_prefix_override",
|
||||||
|
),
|
||||||
|
]
|
28
judge/migrations/0166_display_rank_index.py
Normal file
28
judge/migrations/0166_display_rank_index.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-08-28 01:13
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0165_drop_output_prefix_override"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="profile",
|
||||||
|
name="display_rank",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("user", "Normal User"),
|
||||||
|
("setter", "Problem Setter"),
|
||||||
|
("admin", "Admin"),
|
||||||
|
],
|
||||||
|
db_index=True,
|
||||||
|
default="user",
|
||||||
|
max_length=10,
|
||||||
|
verbose_name="display rank",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
32
judge/migrations/0167_ultimate_contest_format.py
Normal file
32
judge/migrations/0167_ultimate_contest_format.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-09-01 00:09
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0166_display_rank_index"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="contest",
|
||||||
|
name="format_name",
|
||||||
|
field=models.CharField(
|
||||||
|
choices=[
|
||||||
|
("atcoder", "AtCoder"),
|
||||||
|
("default", "Default"),
|
||||||
|
("ecoo", "ECOO"),
|
||||||
|
("icpc", "ICPC"),
|
||||||
|
("ioi", "IOI"),
|
||||||
|
("ioi16", "New IOI"),
|
||||||
|
("ultimate", "Ultimate"),
|
||||||
|
],
|
||||||
|
default="default",
|
||||||
|
help_text="The contest format module to use.",
|
||||||
|
max_length=32,
|
||||||
|
verbose_name="contest format",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
24
judge/migrations/0168_css_background.py
Normal file
24
judge/migrations/0168_css_background.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.2.18 on 2023-09-02 00:30
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("judge", "0167_ultimate_contest_format"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="profile",
|
||||||
|
name="css_background",
|
||||||
|
field=models.TextField(
|
||||||
|
blank=True,
|
||||||
|
help_text='CSS custom background properties: url("image_url"), color, etc',
|
||||||
|
max_length=300,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Custom background",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -772,12 +772,9 @@ class ContestProblem(models.Model):
|
||||||
partial = models.BooleanField(default=True, verbose_name=_("partial"))
|
partial = models.BooleanField(default=True, verbose_name=_("partial"))
|
||||||
is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested"))
|
is_pretested = models.BooleanField(default=False, verbose_name=_("is pretested"))
|
||||||
order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
|
order = models.PositiveIntegerField(db_index=True, verbose_name=_("order"))
|
||||||
output_prefix_override = models.IntegerField(
|
show_testcases = models.BooleanField(
|
||||||
help_text=_("0 to not show testcases, 1 to show"),
|
|
||||||
verbose_name=_("visible testcases"),
|
verbose_name=_("visible testcases"),
|
||||||
null=True,
|
default=False,
|
||||||
blank=True,
|
|
||||||
default=0,
|
|
||||||
)
|
)
|
||||||
max_submissions = models.IntegerField(
|
max_submissions = models.IntegerField(
|
||||||
help_text=_(
|
help_text=_(
|
||||||
|
|
|
@ -403,17 +403,17 @@ class Problem(models.Model, PageVotable, Bookmarkable):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Authors, curators, and testers should always have access, so OR at the very end.
|
# Authors, curators, and testers should always have access, so OR at the very end.
|
||||||
filter = Exists(
|
q |= Exists(
|
||||||
Problem.authors.through.objects.filter(
|
Problem.authors.through.objects.filter(
|
||||||
problem=OuterRef("pk"), profile=profile
|
problem=OuterRef("pk"), profile=profile
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
filter |= Exists(
|
q |= Exists(
|
||||||
Problem.curators.through.objects.filter(
|
Problem.curators.through.objects.filter(
|
||||||
problem=OuterRef("pk"), profile=profile
|
problem=OuterRef("pk"), profile=profile
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
filter |= Exists(
|
q |= Exists(
|
||||||
Problem.testers.through.objects.filter(
|
Problem.testers.through.objects.filter(
|
||||||
problem=OuterRef("pk"), profile=profile
|
problem=OuterRef("pk"), profile=profile
|
||||||
)
|
)
|
||||||
|
|
|
@ -109,6 +109,13 @@ class Organization(models.Model):
|
||||||
"Organization membership test must be Profile or primany key"
|
"Organization membership test must be Profile or primany key"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def delete(self, *args, **kwargs):
|
||||||
|
contests = self.contest_set
|
||||||
|
for contest in contests.all():
|
||||||
|
if contest.organizations.count() == 1:
|
||||||
|
contest.delete()
|
||||||
|
super().delete(*args, **kwargs)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
@ -176,6 +183,7 @@ class Profile(models.Model):
|
||||||
("setter", "Problem Setter"),
|
("setter", "Problem Setter"),
|
||||||
("admin", "Admin"),
|
("admin", "Admin"),
|
||||||
),
|
),
|
||||||
|
db_index=True,
|
||||||
)
|
)
|
||||||
mute = models.BooleanField(
|
mute = models.BooleanField(
|
||||||
verbose_name=_("comment mute"),
|
verbose_name=_("comment mute"),
|
||||||
|
@ -237,6 +245,14 @@ class Profile(models.Model):
|
||||||
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)
|
profile_image = models.ImageField(upload_to=profile_image_path, null=True)
|
||||||
|
email_change_pending = models.EmailField(blank=True, null=True)
|
||||||
|
css_background = models.TextField(
|
||||||
|
verbose_name=_("Custom background"),
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text=_('CSS custom background properties: url("image_url"), color, etc'),
|
||||||
|
max_length=300,
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def organization(self):
|
def organization(self):
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import csv
|
import csv
|
||||||
from tempfile import mktemp
|
from tempfile import mktemp
|
||||||
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
@ -41,6 +42,11 @@ def csv_to_dict(csv_file):
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_username(username):
|
||||||
|
match = re.match(r"\w+", username)
|
||||||
|
return match is not None and match.group() == username
|
||||||
|
|
||||||
|
|
||||||
# return result log
|
# return result log
|
||||||
def import_users(users):
|
def import_users(users):
|
||||||
log = ""
|
log = ""
|
||||||
|
@ -48,17 +54,18 @@ def import_users(users):
|
||||||
cur_log = str(i + 1) + ". "
|
cur_log = str(i + 1) + ". "
|
||||||
|
|
||||||
username = row["username"]
|
username = row["username"]
|
||||||
|
if not is_valid_username(username):
|
||||||
|
log += username + ": Invalid username\n"
|
||||||
|
continue
|
||||||
|
|
||||||
cur_log += username + ": "
|
cur_log += username + ": "
|
||||||
|
|
||||||
pwd = row["password"]
|
pwd = row["password"]
|
||||||
|
|
||||||
user, created = User.objects.get_or_create(
|
user, created = User.objects.get_or_create(
|
||||||
username=username,
|
username=username,
|
||||||
defaults={
|
defaults={
|
||||||
"is_active": True,
|
"is_active": True,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
profile, _ = Profile.objects.get_or_create(
|
profile, _ = Profile.objects.get_or_create(
|
||||||
user=user,
|
user=user,
|
||||||
defaults={
|
defaults={
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
from judge.models import Profile
|
from judge.models import Profile
|
||||||
|
|
||||||
|
@ -17,8 +18,8 @@ class LogUserAccessMiddleware(object):
|
||||||
):
|
):
|
||||||
updates = {"last_access": now()}
|
updates = {"last_access": now()}
|
||||||
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
|
# Decided on using REMOTE_ADDR as nginx will translate it to the external IP that hits it.
|
||||||
if request.META.get("REMOTE_ADDR"):
|
if request.META.get(settings.META_REMOTE_ADDRESS_KEY):
|
||||||
updates["ip"] = request.META.get("REMOTE_ADDR")
|
updates["ip"] = request.META.get(settings.META_REMOTE_ADDRESS_KEY)
|
||||||
Profile.objects.filter(user_id=request.user.pk).update(**updates)
|
Profile.objects.filter(user_id=request.user.pk).update(**updates)
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
20
judge/utils/email_render.py
Normal file
20
judge/utils/email_render.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from django.template.loader import render_to_string
|
||||||
|
from django.contrib.sites.shortcuts import get_current_site
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
||||||
|
def render_email_message(request, contexts):
|
||||||
|
current_site = get_current_site(request)
|
||||||
|
email_contexts = {
|
||||||
|
"username": request.user.username,
|
||||||
|
"domain": current_site.domain,
|
||||||
|
"protocol": "https" if request.is_secure() else "http",
|
||||||
|
"site_name": settings.SITE_NAME,
|
||||||
|
"message": None,
|
||||||
|
"title": None,
|
||||||
|
"button_text": "Click here",
|
||||||
|
"url_path": None,
|
||||||
|
}
|
||||||
|
email_contexts.update(contexts)
|
||||||
|
message = render_to_string("general_email.html", email_contexts)
|
||||||
|
return message
|
|
@ -134,7 +134,7 @@ class ContestList(
|
||||||
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView
|
QueryStringSortMixin, DiggPaginatorMixin, TitleMixin, ContestListMixin, ListView
|
||||||
):
|
):
|
||||||
model = Contest
|
model = Contest
|
||||||
paginate_by = 20
|
paginate_by = 10
|
||||||
template_name = "contest/list.html"
|
template_name = "contest/list.html"
|
||||||
title = gettext_lazy("Contests")
|
title = gettext_lazy("Contests")
|
||||||
context_object_name = "past_contests"
|
context_object_name = "past_contests"
|
||||||
|
@ -1357,9 +1357,7 @@ class ContestClarificationAjax(ContestMixin, DetailView):
|
||||||
raise Http404()
|
raise Http404()
|
||||||
|
|
||||||
polling_time = 1 # minute
|
polling_time = 1 # minute
|
||||||
last_one_minute = last_five_minutes = timezone.now() - timezone.timedelta(
|
last_one_minute = timezone.now() - timezone.timedelta(minutes=polling_time)
|
||||||
minutes=polling_time
|
|
||||||
)
|
|
||||||
|
|
||||||
queryset = ContestProblemClarification.objects.filter(
|
queryset = ContestProblemClarification.objects.filter(
|
||||||
problem__in=self.object.contest_problems.all(), date__gte=last_one_minute
|
problem__in=self.object.contest_problems.all(), date__gte=last_one_minute
|
||||||
|
|
121
judge/views/email.py
Normal file
121
judge/views/email.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
from django.contrib.auth.tokens import default_token_generator
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.shortcuts import render, redirect
|
||||||
|
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
|
||||||
|
from django.utils.encoding import force_bytes, force_text
|
||||||
|
from django.conf import settings
|
||||||
|
from django import forms
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.urls import reverse
|
||||||
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.contrib.auth.models import User
|
||||||
|
from django.contrib.auth.hashers import check_password
|
||||||
|
|
||||||
|
from urllib.parse import urlencode, urlunparse, urlparse
|
||||||
|
|
||||||
|
from judge.models import Profile
|
||||||
|
from judge.utils.email_render import render_email_message
|
||||||
|
|
||||||
|
|
||||||
|
class EmailChangeForm(forms.Form):
|
||||||
|
new_email = forms.EmailField(label=_("New Email"))
|
||||||
|
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
self.user = kwargs.pop("user", None)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
def clean_new_email(self):
|
||||||
|
new_email = self.cleaned_data.get("new_email")
|
||||||
|
if User.objects.filter(email=new_email).exists():
|
||||||
|
raise forms.ValidationError(_("An account with this email already exists."))
|
||||||
|
return new_email
|
||||||
|
|
||||||
|
def clean_password(self):
|
||||||
|
password = self.cleaned_data.get("password")
|
||||||
|
if not self.user.check_password(password):
|
||||||
|
raise forms.ValidationError("Invalid password")
|
||||||
|
return password
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def email_change_view(request):
|
||||||
|
form = EmailChangeForm(request.POST or None, user=request.user)
|
||||||
|
|
||||||
|
if request.method == "POST" and form.is_valid():
|
||||||
|
new_email = request.POST.get("new_email")
|
||||||
|
user = request.user
|
||||||
|
profile = request.profile
|
||||||
|
|
||||||
|
# Generate a token for email verification
|
||||||
|
token = default_token_generator.make_token(user)
|
||||||
|
uid = urlsafe_base64_encode(force_bytes(user.pk))
|
||||||
|
|
||||||
|
# Send the email to the user
|
||||||
|
subject = settings.SITE_NAME + " - " + _("Email Change Request")
|
||||||
|
email_contexts = {
|
||||||
|
"message": _(
|
||||||
|
"We have received a request to change your email to this email. Click the button below to change your email:"
|
||||||
|
),
|
||||||
|
"title": _("Email Change"),
|
||||||
|
"button_text": _("Change Email"),
|
||||||
|
"url_path": reverse(
|
||||||
|
"email_change_verify", kwargs={"uidb64": uid, "token": token}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
message = render_email_message(request, email_contexts)
|
||||||
|
send_mail(
|
||||||
|
subject,
|
||||||
|
message,
|
||||||
|
settings.EMAIL_HOST_USER,
|
||||||
|
[new_email],
|
||||||
|
html_message=message,
|
||||||
|
)
|
||||||
|
profile.email_change_pending = new_email
|
||||||
|
profile.save()
|
||||||
|
return redirect("email_change_pending")
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"email_change/email_change.html",
|
||||||
|
{
|
||||||
|
"form": form,
|
||||||
|
"title": _("Change email"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_email_view(request, uidb64, token):
|
||||||
|
try:
|
||||||
|
uid = force_text(urlsafe_base64_decode(uidb64))
|
||||||
|
user = User.objects.get(pk=uid)
|
||||||
|
except (TypeError, ValueError, OverflowError, User.DoesNotExist):
|
||||||
|
user = None
|
||||||
|
if user is not None and default_token_generator.check_token(user, token):
|
||||||
|
profile = Profile.objects.get(user=user)
|
||||||
|
new_email = profile.email_change_pending
|
||||||
|
if new_email and not User.objects.filter(email=new_email).exists():
|
||||||
|
user.email = new_email
|
||||||
|
profile.email_change_pending = None
|
||||||
|
user.save()
|
||||||
|
profile.save()
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"email_change/email_change_success.html",
|
||||||
|
{"title": _("Success"), "user": user},
|
||||||
|
)
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request, "email_change/email_change_failure.html", {"title": _("Invalid")}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def email_change_pending_view(request):
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"email_change/email_change_pending.html",
|
||||||
|
{
|
||||||
|
"title": _("Email change pending"),
|
||||||
|
},
|
||||||
|
)
|
|
@ -3,7 +3,7 @@ import json
|
||||||
|
|
||||||
from django.views.generic import ListView
|
from django.views.generic import ListView
|
||||||
from django.utils.translation import gettext as _, gettext_lazy
|
from django.utils.translation import gettext as _, gettext_lazy
|
||||||
from django.db.models import Count
|
from django.db.models import Count, Q
|
||||||
from django.http import HttpResponseForbidden
|
from django.http import HttpResponseForbidden
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
@ -38,13 +38,19 @@ class InternalProblem(InternalView, ListView):
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_search_query(self):
|
||||||
|
return self.request.GET.get("q") or self.request.POST.get("q")
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
queryset = (
|
queryset = Problem.objects.annotate(
|
||||||
Problem.objects.annotate(vote_count=Count("volunteer_user_votes"))
|
vote_count=Count("volunteer_user_votes")
|
||||||
.filter(vote_count__gte=1)
|
).filter(vote_count__gte=1)
|
||||||
.order_by("-vote_count")
|
query = self.get_search_query()
|
||||||
)
|
if query:
|
||||||
return queryset
|
queryset = queryset.filter(
|
||||||
|
Q(code__icontains=query) | Q(name__icontains=query)
|
||||||
|
)
|
||||||
|
return queryset.order_by("-vote_count")
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super(InternalProblem, self).get_context_data(**kwargs)
|
context = super(InternalProblem, self).get_context_data(**kwargs)
|
||||||
|
@ -52,6 +58,7 @@ class InternalProblem(InternalView, ListView):
|
||||||
context["title"] = self.title
|
context["title"] = self.title
|
||||||
context["page_prefix"] = self.request.path + "?page="
|
context["page_prefix"] = self.request.path + "?page="
|
||||||
context["first_page_href"] = self.request.path
|
context["first_page_href"] = self.request.path
|
||||||
|
context["query"] = self.get_search_query()
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
|
@ -926,8 +926,12 @@ class EditOrganizationContest(
|
||||||
super().post(request, *args, **kwargs)
|
super().post(request, *args, **kwargs)
|
||||||
return HttpResponseRedirect(
|
return HttpResponseRedirect(
|
||||||
reverse(
|
reverse(
|
||||||
"organization_contests",
|
"organization_contest_edit",
|
||||||
args=(self.organization_id, self.organization.slug),
|
args=(
|
||||||
|
self.organization_id,
|
||||||
|
self.organization.slug,
|
||||||
|
self.contest.key,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ from registration.backends.default.views import (
|
||||||
from registration.forms import RegistrationForm
|
from registration.forms import RegistrationForm
|
||||||
from sortedm2m.forms import SortedMultipleChoiceField
|
from sortedm2m.forms import SortedMultipleChoiceField
|
||||||
|
|
||||||
from judge.models import Language, Organization, Profile, TIMEZONE
|
from judge.models import Language, Profile, TIMEZONE
|
||||||
from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget
|
from judge.utils.recaptcha import ReCaptchaField, ReCaptchaWidget
|
||||||
from judge.widgets import Select2MultipleWidget, Select2Widget
|
from judge.widgets import Select2MultipleWidget, Select2Widget
|
||||||
|
|
||||||
|
@ -43,29 +43,10 @@ class CustomRegistrationForm(RegistrationForm):
|
||||||
empty_label=None,
|
empty_label=None,
|
||||||
widget=Select2Widget(attrs={"style": "width:100%"}),
|
widget=Select2Widget(attrs={"style": "width:100%"}),
|
||||||
)
|
)
|
||||||
organizations = SortedMultipleChoiceField(
|
|
||||||
queryset=Organization.objects.filter(is_open=True),
|
|
||||||
label=_("Groups"),
|
|
||||||
required=False,
|
|
||||||
widget=Select2MultipleWidget(attrs={"style": "width:100%"}),
|
|
||||||
)
|
|
||||||
|
|
||||||
if ReCaptchaField is not None:
|
if ReCaptchaField is not None:
|
||||||
captcha = ReCaptchaField(widget=ReCaptchaWidget())
|
captcha = ReCaptchaField(widget=ReCaptchaWidget())
|
||||||
|
|
||||||
def clean_organizations(self):
|
|
||||||
organizations = self.cleaned_data.get("organizations") or []
|
|
||||||
max_orgs = settings.DMOJ_USER_MAX_ORGANIZATION_COUNT
|
|
||||||
|
|
||||||
if sum(org.is_open for org in organizations) > max_orgs:
|
|
||||||
raise forms.ValidationError(
|
|
||||||
_("You may not be part of more than {count} public groups.").format(
|
|
||||||
count=max_orgs
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.cleaned_data["organizations"]
|
|
||||||
|
|
||||||
def clean_email(self):
|
def clean_email(self):
|
||||||
if User.objects.filter(email=self.cleaned_data["email"]).exists():
|
if User.objects.filter(email=self.cleaned_data["email"]).exists():
|
||||||
raise forms.ValidationError(
|
raise forms.ValidationError(
|
||||||
|
@ -116,7 +97,6 @@ class RegistrationView(OldRegistrationView):
|
||||||
cleaned_data = form.cleaned_data
|
cleaned_data = form.cleaned_data
|
||||||
profile.timezone = cleaned_data["timezone"]
|
profile.timezone = cleaned_data["timezone"]
|
||||||
profile.language = cleaned_data["language"]
|
profile.language = cleaned_data["language"]
|
||||||
profile.organizations.add(*cleaned_data["organizations"])
|
|
||||||
profile.save()
|
profile.save()
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
|
@ -259,13 +259,13 @@ class SubmissionStatus(SubmissionDetailBase):
|
||||||
)
|
)
|
||||||
|
|
||||||
contest = submission.contest_or_none
|
contest = submission.contest_or_none
|
||||||
prefix_length = 0
|
show_testcases = False
|
||||||
can_see_testcases = self.access_testcases_in_contest()
|
can_see_testcases = self.access_testcases_in_contest()
|
||||||
|
|
||||||
if contest is not None:
|
if contest is not None:
|
||||||
prefix_length = contest.problem.output_prefix_override or 0
|
show_testcases = contest.problem.show_testcases or False
|
||||||
|
|
||||||
if contest is None or prefix_length > 0 or can_see_testcases:
|
if contest is None or show_testcases or can_see_testcases:
|
||||||
context["cases_data"] = get_cases_data(submission)
|
context["cases_data"] = get_cases_data(submission)
|
||||||
context["can_see_testcases"] = True
|
context["can_see_testcases"] = True
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -16,8 +16,8 @@ class DateTimePickerWidget(forms.DateTimeInput):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def media(self):
|
def media(self):
|
||||||
css_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.min.css"
|
css_url = "/static/datetime-picker/datetimepicker.min.css"
|
||||||
js_url = "https://cdnjs.cloudflare.com/ajax/libs/jquery-datetimepicker/2.5.20/jquery.datetimepicker.full.min.js"
|
js_url = "/static/datetime-picker/datetimepicker.full.min.js"
|
||||||
return forms.Media(
|
return forms.Media(
|
||||||
js=[js_url],
|
js=[js_url],
|
||||||
css={"screen": [css_url]},
|
css={"screen": [css_url]},
|
||||||
|
|
|
@ -47,20 +47,15 @@ else:
|
||||||
"pagedown-extra/Markdown.Extra.js",
|
"pagedown-extra/Markdown.Extra.js",
|
||||||
"pagedown_init.js",
|
"pagedown_init.js",
|
||||||
]
|
]
|
||||||
css = {
|
|
||||||
"all": [
|
|
||||||
"markdown.css",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdminPagedownWidget(PagedownWidget, admin_widgets.AdminTextareaWidget):
|
class AdminPagedownWidget(PagedownWidget, admin_widgets.AdminTextareaWidget):
|
||||||
class Media:
|
class Media:
|
||||||
css = {
|
css = {
|
||||||
"all": [
|
"all": [
|
||||||
|
"markdown.css",
|
||||||
"pagedown_widget.css",
|
"pagedown_widget.css",
|
||||||
"content-description.css",
|
"content-description.css",
|
||||||
"admin/css/pagedown.css",
|
"admin/css/pagedown.css",
|
||||||
"markdown.css",
|
|
||||||
"pagedown.css",
|
"pagedown.css",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -125,7 +120,6 @@ else:
|
||||||
"pygment-github.css",
|
"pygment-github.css",
|
||||||
"table.css",
|
"table.css",
|
||||||
"ranks.css",
|
"ranks.css",
|
||||||
"markdown.css",
|
|
||||||
"dmmd-preview.css",
|
"dmmd-preview.css",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,12 +15,15 @@ msgstr "Thành viên"
|
||||||
msgid "Contests"
|
msgid "Contests"
|
||||||
msgstr "Kỳ thi"
|
msgstr "Kỳ thi"
|
||||||
|
|
||||||
msgid "Groups"
|
msgid "Chat"
|
||||||
msgstr "Nhóm"
|
msgstr ""
|
||||||
|
|
||||||
msgid "About"
|
msgid "About"
|
||||||
msgstr "Giới thiệu"
|
msgstr "Giới thiệu"
|
||||||
|
|
||||||
|
msgid "Groups"
|
||||||
|
msgstr "Nhóm"
|
||||||
|
|
||||||
msgid "Status"
|
msgid "Status"
|
||||||
msgstr "Máy chấm"
|
msgstr "Máy chấm"
|
||||||
|
|
||||||
|
|
|
@ -191,11 +191,19 @@ header {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
& > li > a > span {
|
& > li > span {
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
display: block;
|
display: block;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-top: 2px solid #9c3706;
|
||||||
|
color: black;
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
& > img {
|
& > img {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -232,8 +240,8 @@ header {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 10px;
|
||||||
height: 48px;
|
height: $navbar_height;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
|
@ -377,7 +385,7 @@ hr {
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
margin: 48px auto 1em auto;
|
margin: $navbar_height auto 1em auto;
|
||||||
padding-top: 1em;
|
padding-top: 1em;
|
||||||
|
|
||||||
// Header
|
// Header
|
||||||
|
@ -705,11 +713,11 @@ math {
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-icon {
|
#chat-icon {
|
||||||
color: lightseagreen;
|
color: $theme_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-icon:hover {
|
#chat-icon:hover {
|
||||||
color: darkgreen;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav-lang-icon {
|
#nav-lang-icon {
|
||||||
|
@ -860,6 +868,16 @@ select {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#loading-bar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #993932;
|
||||||
|
width: 0;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 799px) {
|
@media (max-width: 799px) {
|
||||||
#user-links, .anon {
|
#user-links, .anon {
|
||||||
padding-right: 0.5em;
|
padding-right: 0.5em;
|
||||||
|
@ -887,4 +905,56 @@ select {
|
||||||
.view-next-page {
|
.view-next-page {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.colored-text {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bold-text {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.non-italics {
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.margin-label{
|
||||||
|
margin-bottom: 2.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
|
||||||
|
color: $theme_color;
|
||||||
|
opacity: 1; /* Firefox */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-ms-input-placeholder { /* Internet Explorer 10-11 */
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-ms-input-placeholder { /* Microsoft Edge */
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input::placeholder{
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-input-placeholder {
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:-moz-placeholder { /* Firefox 18- */
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-placeholder { /* Firefox 19+ */
|
||||||
|
color: $theme_color;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
|
@ -33,11 +33,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-sidebar-item.active {
|
.left-sidebar-item.active {
|
||||||
color: green;
|
color: white;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
background-color: lightgreen;
|
background-color: $theme_color;
|
||||||
|
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
color: green;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,7 +199,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-sidebar-item.active:hover {
|
.left-sidebar-item.active:hover {
|
||||||
background-color: lightgreen;
|
background-color: $theme_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-icon {
|
.sidebar-icon {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
@import "vars";
|
||||||
|
|
||||||
.chat {
|
.chat {
|
||||||
background: white;
|
background: white;
|
||||||
}
|
}
|
||||||
|
@ -11,15 +13,6 @@
|
||||||
float: right;
|
float: right;
|
||||||
margin-right: 1em;
|
margin-right: 1em;
|
||||||
}
|
}
|
||||||
#emoji-button {
|
|
||||||
position: absolute;
|
|
||||||
right: 1em;
|
|
||||||
font-size: 2em;
|
|
||||||
color: lightgray;
|
|
||||||
}
|
|
||||||
#emoji-button:hover {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
#chat-log {
|
#chat-log {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
padding-top: 2em;
|
padding-top: 2em;
|
||||||
|
@ -39,15 +32,15 @@
|
||||||
#chat-online {
|
#chat-online {
|
||||||
border-right: 1px solid #ccc;
|
border-right: 1px solid #ccc;
|
||||||
padding-bottom: 0 !important;
|
padding-bottom: 0 !important;
|
||||||
min-width: 25%;
|
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
|
font-size: 1.2em;
|
||||||
}
|
}
|
||||||
#chat-online-content {
|
#chat-online-content {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
max-height: calc(100% - 44px);
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
#chat-box {
|
#chat-box {
|
||||||
/*border: 1px solid #ccc;*/
|
/*border: 1px solid #ccc;*/
|
||||||
|
@ -58,37 +51,40 @@
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
border-bottom-left-radius: 0;
|
border-bottom-left-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
height: 75%;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#chat-input {
|
#chat-input {
|
||||||
width: 100%;
|
|
||||||
padding: 0.4em 4em 1em 1.2em;
|
|
||||||
border: 0;
|
|
||||||
color: black;
|
color: black;
|
||||||
border-top-left-radius: 0;
|
border: 2px solid #e4a81c;
|
||||||
border-top-right-radius: 0;
|
|
||||||
height: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
}
|
||||||
#chat-online-content {
|
#chat-online-content {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
.selected-status-row {
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
.status_last_message {
|
||||||
|
color: darkgray;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
@media (min-width: 800px) {
|
||||||
#chat-container {
|
#chat-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: calc(100vh - 3em);;
|
height: calc(100vh - 3em);
|
||||||
border: 1px solid #ccc;
|
border: 1px solid #ccc;
|
||||||
/*border-radius: 0 4px 0 0;*/
|
/*border-radius: 0 4px 0 0;*/
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
#chat-online {
|
#chat-online {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
width: 35%;
|
||||||
}
|
}
|
||||||
.chat-left-panel, .chat-right-panel {
|
#chat-area {
|
||||||
display: block !important;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#chat-input, #chat-log .content-message {
|
#chat-input, #chat-log .content-message {
|
||||||
|
@ -97,14 +93,7 @@
|
||||||
.info-pic {
|
.info-pic {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.info-circle {
|
|
||||||
position: absolute;
|
|
||||||
cx: 12%;
|
|
||||||
cy: 12%;
|
|
||||||
r: 12%;
|
|
||||||
stroke: white;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
|
||||||
.info-name {
|
.info-name {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
|
@ -138,29 +127,30 @@
|
||||||
transition: 1.5s ease-in-out;
|
transition: 1.5s ease-in-out;
|
||||||
}
|
}
|
||||||
.status-pic {
|
.status-pic {
|
||||||
height: 1.3em;
|
height: 32px;
|
||||||
width: 1.3em;
|
width: 32px;
|
||||||
border-radius: 0.3em;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
.status-container {
|
.status-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
.status-circle {
|
.status-circle {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
cx: 18px;
|
cx: 27px;
|
||||||
cy: 18px;
|
cy: 27px;
|
||||||
r: 4.5px;
|
r: 4.5px;
|
||||||
stroke: white;
|
stroke: white;
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
.status-row {
|
.status-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 15px;
|
padding: 15px;
|
||||||
padding: 0.2em 0.2em 0.2em 1em;
|
gap: 0.5em;
|
||||||
border-radius: 4px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.status-row:hover {
|
.status-row:hover {
|
||||||
background: lightgray;
|
background: lightgray;
|
||||||
|
@ -168,6 +158,7 @@
|
||||||
}
|
}
|
||||||
.status-list {
|
.status-list {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
.status-section-title {
|
.status-section-title {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -178,6 +169,8 @@
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
.message-text-other {
|
.message-text-other {
|
||||||
background: #eeeeee;
|
background: #eeeeee;
|
||||||
|
@ -187,7 +180,12 @@
|
||||||
background: rgb(0, 132, 255);
|
background: rgb(0, 132, 255);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.chat-input-icon {
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
.chat-input-icon:hover {
|
||||||
|
background: lightgray;
|
||||||
|
}
|
||||||
.chat {
|
.chat {
|
||||||
.active-span {
|
.active-span {
|
||||||
color: #636363;
|
color: #636363;
|
||||||
|
@ -200,6 +198,7 @@
|
||||||
background-color: darkcyan;
|
background-color: darkcyan;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
padding: 0 0.5em;
|
padding: 0 0.5em;
|
||||||
|
align-self: flex-end;
|
||||||
}
|
}
|
||||||
#setting-content {
|
#setting-content {
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -225,9 +224,6 @@
|
||||||
|
|
||||||
@media (max-width: 799px) {
|
@media (max-width: 799px) {
|
||||||
#chat-area {
|
#chat-area {
|
||||||
height: 500px;
|
height: calc(100vh - 120px);
|
||||||
}
|
|
||||||
#emoji-button {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -385,6 +385,13 @@ function onWindowReady() {
|
||||||
showTooltip(e.trigger, fallbackMessage(e.action));
|
showTooltip(e.trigger, fallbackMessage(e.action));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
$('a').click(function() {
|
||||||
|
$("#loading-bar").show();
|
||||||
|
$("#loading-bar").animate({ width: "100%" }, 1500, function() {
|
||||||
|
$(this).hide();
|
||||||
|
$("#loading-bar").css({ width: 0});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
|
@ -429,4 +436,5 @@ $(function() {
|
||||||
$('html').click(function () {
|
$('html').click(function () {
|
||||||
$nav_list.hide();
|
$nav_list.hide();
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
|
@ -51,7 +51,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
pre.no-border {
|
pre.no-border {
|
||||||
margin-top: -0.7em;
|
margin-top: 0.4em;
|
||||||
padding: 0.5em;
|
padding: 0.5em;
|
||||||
border: none;
|
border: none;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
|
@ -137,7 +137,7 @@
|
||||||
|
|
||||||
.linenos {
|
.linenos {
|
||||||
width: 4%;
|
width: 4%;
|
||||||
|
|
||||||
pre {
|
pre {
|
||||||
color: rgba(0,0,0,.26);
|
color: rgba(0,0,0,.26);
|
||||||
background-color: rgba(0,0,0,.07);
|
background-color: rgba(0,0,0,.07);
|
||||||
|
@ -166,8 +166,8 @@
|
||||||
textarea,
|
textarea,
|
||||||
pre {
|
pre {
|
||||||
-moz-tab-size : 4;
|
-moz-tab-size : 4;
|
||||||
-o-tab-size : 4;
|
-o-tab-size : 4;
|
||||||
tab-size : 4;
|
tab-size : 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,40 @@
|
||||||
@import "vars";
|
@import "vars";
|
||||||
|
|
||||||
|
.list-contest {
|
||||||
|
box-shadow: 0px 4px 8px rgba(4, 83, 67, 0.2), 0px 6px 20px rgba(4, 83, 67, 0.19);
|
||||||
|
border-radius: 15px;
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.info-contest:first-child {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-contest:nth-child(2) {
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-contest {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.participate-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contest-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 150%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#contest-calendar {
|
#contest-calendar {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
File diff suppressed because one or more lines are too long
1
resources/datetime-picker/datetimepicker.full.min.js
vendored
Normal file
1
resources/datetime-picker/datetimepicker.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
resources/datetime-picker/datetimepicker.min.css
vendored
Normal file
1
resources/datetime-picker/datetimepicker.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,3 +1,4 @@
|
||||||
|
@import "vars";
|
||||||
.info-float {
|
.info-float {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 60px;
|
top: 60px;
|
||||||
|
@ -20,6 +21,11 @@
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
border-top: #ccc 1px solid;
|
border-top: #ccc 1px solid;
|
||||||
|
border-top-left-radius: $widget_border_radius;
|
||||||
|
border-top-right-radius: $widget_border_radius;
|
||||||
|
.sub-result {
|
||||||
|
border-top-left-radius: $widget_border_radius;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
|
|
|
@ -150,14 +150,6 @@
|
||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message .content :first-child {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message .content :last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-message .detail {
|
.new-message .detail {
|
||||||
padding: 8px 10px;
|
padding: 8px 10px;
|
||||||
}
|
}
|
||||||
|
@ -174,4 +166,10 @@
|
||||||
padding-left: 0.5em;
|
padding-left: 0.5em;
|
||||||
padding-top: 1.65em;
|
padding-top: 1.65em;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 799px) {
|
||||||
|
.ticket-container {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,9 +4,12 @@ $border_gray: #ccc;
|
||||||
$background_gray: #ededed;
|
$background_gray: #ededed;
|
||||||
$background_light_gray: #fafafa;
|
$background_light_gray: #fafafa;
|
||||||
$announcement_red: #ae0000;
|
$announcement_red: #ae0000;
|
||||||
|
$theme_color: #045343;
|
||||||
|
|
||||||
$base_font_size: 14px;
|
$base_font_size: 14px;
|
||||||
$widget_border_radius: 4px;
|
$widget_border_radius: 4px;
|
||||||
$table_header_rounding: 6px;
|
$table_header_rounding: 6px;
|
||||||
|
|
||||||
$monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
|
$monospace-fonts: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
|
||||||
|
|
||||||
|
$navbar_height: 48px;
|
||||||
|
|
|
@ -30,7 +30,7 @@
|
||||||
.button, button, input[type=submit] {
|
.button, button, input[type=submit] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-clip: padding-box;
|
background-clip: padding-box;
|
||||||
background-color: #fa6400;
|
background-color: $theme_color;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: .25rem;
|
border-radius: .25rem;
|
||||||
box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0;
|
box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0;
|
||||||
|
@ -101,6 +101,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.btn-darkGreen {
|
||||||
|
background: $theme_color;
|
||||||
|
}
|
||||||
|
|
||||||
// class = "unselectable button full small" only appear in online-judge/templates/contest/list.html
|
// class = "unselectable button full small" only appear in online-judge/templates/contest/list.html
|
||||||
// this attribute center buttons in contest list (including "Join", "Virutal Join", "Spectable")
|
// this attribute center buttons in contest list (including "Join", "Virutal Join", "Spectable")
|
||||||
&.unselectable.button.full.small {
|
&.unselectable.button.full.small {
|
||||||
|
@ -113,18 +117,18 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover, button:hover, input[type=submit]:hover {
|
.button:hover, button:hover, input[type=submit]:hover {
|
||||||
background-color: #fb8332;
|
background-color: $theme_color;
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:focus, button:focus, input[type=submit]:focus {
|
.button:focus, button:focus, input[type=submit]:focus {
|
||||||
background-color: #fb8332;
|
background-color: $theme_color;
|
||||||
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:active, button:active, input[type=submit]:hover {
|
.button:active, button:active, input[type=submit]:hover {
|
||||||
background-color: #c85000;
|
background-color: $theme_color;
|
||||||
box-shadow: rgba(0, 0, 0, .06) 0 2px 4px;
|
box-shadow: rgba(0, 0, 0, .06) 0 2px 4px;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
@ -212,17 +216,19 @@ input {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md-typeset .copy-clipboard {
|
.md-typeset .admonition .btn-clipboard,
|
||||||
margin-top: 1.5em;
|
.md-typeset details .btn-clipboard {
|
||||||
|
right: -0.6rem;
|
||||||
|
border-radius: 0 0 0 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Bootstrap-y tabs
|
// Bootstrap-y tabs
|
||||||
.ul_tab_a_active {
|
.ul_tab_a_active {
|
||||||
color: #045343;
|
color: $theme_color;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 3px solid #045343;
|
border-bottom: 3px solid $theme_color;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -269,6 +275,15 @@ input {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 0.1rem; // Adjust the width as needed
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
> li {
|
> li {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -307,11 +322,14 @@ input {
|
||||||
// Bootstrap-y pagination
|
// Bootstrap-y pagination
|
||||||
ul.pagination a:hover {
|
ul.pagination a:hover {
|
||||||
color: #FFF;
|
color: #FFF;
|
||||||
background: rgba(0, 0, 0, 0.55);
|
background: #0aa082;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.pagination {
|
ul.pagination {
|
||||||
display: inline-block;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
display: flex;
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: $widget_border_radius;
|
border-radius: $widget_border_radius;
|
||||||
|
@ -320,33 +338,35 @@ ul.pagination {
|
||||||
li {
|
li {
|
||||||
display: inline;
|
display: inline;
|
||||||
|
|
||||||
&:first-child > {
|
// &:first-child > {
|
||||||
a, span {
|
// a, span {
|
||||||
margin-left: 0;
|
// margin-left: 0;
|
||||||
border-top-left-radius: $widget_border_radius;
|
// border-top-left-radius: $widget_border_radius;
|
||||||
border-bottom-left-radius: $widget_border_radius;
|
// border-bottom-left-radius: $widget_border_radius;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
&:last-child > {
|
// &:last-child > {
|
||||||
a, span {
|
// a, span {
|
||||||
margin-left: 0;
|
// margin-left: 0;
|
||||||
border-top-right-radius: $widget_border_radius;
|
// border-top-right-radius: $widget_border_radius;
|
||||||
border-bottom-right-radius: $widget_border_radius;
|
// border-bottom-right-radius: $widget_border_radius;
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
> {
|
> {
|
||||||
a, span {
|
a, span {
|
||||||
position: relative;
|
position: relative;
|
||||||
float: left;
|
float: left;
|
||||||
padding: 4px 12px;
|
padding: 4px 12px;
|
||||||
|
margin-right: 5px;
|
||||||
|
font-weight: bold;
|
||||||
line-height: 1.42857;
|
line-height: 1.42857;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: #FFF;
|
color: $theme_color;
|
||||||
background-color: $widget_black;
|
background-color: white;
|
||||||
border: 1px solid #505050;
|
border: 2px solid $theme_color;
|
||||||
margin-left: -1px;
|
border-radius: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -354,22 +374,22 @@ ul.pagination {
|
||||||
.disabled-page > {
|
.disabled-page > {
|
||||||
a {
|
a {
|
||||||
color: #888;
|
color: #888;
|
||||||
background-color: $widget_black;
|
background-color: #04534380;
|
||||||
border-color: #282828;
|
border-color: #04534380;
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
span {
|
||||||
color: #888;
|
color: #888;
|
||||||
background-color: $widget_black;
|
background-color: #04534380;
|
||||||
border-color: #505050;
|
border-color: #04534380;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.active-page > {
|
.active-page > {
|
||||||
a {
|
a {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
color: black;
|
color: white;
|
||||||
background-color: #7dc7ff;
|
background-color: $theme_color;
|
||||||
border-color: transparent;
|
border-color: transparent;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
@ -489,7 +509,7 @@ ul.select2-selection__rendered {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.fa {
|
.fa {
|
||||||
background: #045343;
|
background: $theme_color;
|
||||||
color: white;
|
color: white;
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -606,6 +626,14 @@ ul.select2-selection__rendered {
|
||||||
background: gray;
|
background: gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.errorlist {
|
||||||
|
margin: 0px;
|
||||||
|
text-align: right;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0px;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.registration-form {
|
.registration-form {
|
||||||
.sortedm2m-container, .sortedm2m-container p.selector-filter {
|
.sortedm2m-container, .sortedm2m-container p.selector-filter {
|
||||||
width: 300px;
|
width: 300px;
|
||||||
|
@ -682,12 +710,6 @@ ul.select2-selection__rendered {
|
||||||
width: 450px;
|
width: 450px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul.errorlist {
|
|
||||||
margin: 0px;
|
|
||||||
text-align: right;
|
|
||||||
list-style: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.full-textfield {
|
.full-textfield {
|
||||||
padding-top: 0.5em;
|
padding-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,6 +193,15 @@
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
{% if request.profile.css_background %}
|
||||||
|
<style>
|
||||||
|
@media(min-width: 800px) {
|
||||||
|
#page-container {
|
||||||
|
background: {{request.profile.css_background|safe}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<svg width="0" height="0" style="display: block">
|
<svg width="0" height="0" style="display: block">
|
||||||
|
@ -202,7 +211,7 @@
|
||||||
</svg>
|
</svg>
|
||||||
<nav id="navigation" class="unselectable">
|
<nav id="navigation" class="unselectable">
|
||||||
<div id="nav-container">
|
<div id="nav-container">
|
||||||
<a id="navicon" href="javascript:void(0)"><i class="fa fa-bars"></i></a>
|
<span id="navicon"><i class="fa fa-bars"></i></span>
|
||||||
<ul id="nav-list">
|
<ul id="nav-list">
|
||||||
<li class="home-nav-element"><a href="{{ url('home') }}">{% include "site-logo-fragment.html" %}</a></li>
|
<li class="home-nav-element"><a href="{{ url('home') }}">{% include "site-logo-fragment.html" %}</a></li>
|
||||||
<li class="home-nav-element"><span class="nav-divider"></span></li>
|
<li class="home-nav-element"><span class="nav-divider"></span></li>
|
||||||
|
@ -243,9 +252,9 @@
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span title="{{_('Language')}}">
|
<span title="{{_('Language')}}">
|
||||||
<a class="icofont-globe navbar-icon" id="nav-lang-icon" aria-hidden="true">
|
<div class="icofont-globe navbar-icon" id="nav-lang-icon" aria-hidden="true">
|
||||||
<sub class="sub-lang">{{LANGUAGE_CODE}}</sub>
|
<sub class="sub-lang">{{LANGUAGE_CODE}}</sub>
|
||||||
</a>
|
</div>
|
||||||
<div id="lang-dropdown" class="dropdown" role="tooltip">
|
<div id="lang-dropdown" class="dropdown" role="tooltip">
|
||||||
{% for language in language_info_list(LANGUAGES) %}
|
{% for language in language_info_list(LANGUAGES) %}
|
||||||
<div value="{{ language.code }}"
|
<div value="{{ language.code }}"
|
||||||
|
@ -261,14 +270,14 @@
|
||||||
</span>
|
</span>
|
||||||
{% if request.user.is_authenticated %}
|
{% if request.user.is_authenticated %}
|
||||||
<span id="user-links">
|
<span id="user-links">
|
||||||
<ul><li><a href="javascript:void(0)">
|
<ul><li>
|
||||||
<span>
|
<span>
|
||||||
<img src="{{ gravatar(request.profile, 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>
|
||||||
</span>
|
</span>
|
||||||
</a></li></ul>
|
</li></ul>
|
||||||
</span>
|
</span>
|
||||||
<div class="dropdown" id="userlink_dropdown" role="tooptip">
|
<div class="dropdown" id="userlink_dropdown" role="tooptip">
|
||||||
<a href="{{ url('user_page') }}">
|
<a href="{{ url('user_page') }}">
|
||||||
|
@ -316,6 +325,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div id="nav-shadow"></div>
|
<div id="nav-shadow"></div>
|
||||||
</nav>
|
</nav>
|
||||||
|
<div id="loading-bar"></div>
|
||||||
{% if request.in_contest %}
|
{% if request.in_contest %}
|
||||||
<div id="contest-info">
|
<div id="contest-info">
|
||||||
<div id="contest-info-main">
|
<div id="contest-info-main">
|
||||||
|
|
|
@ -7,21 +7,11 @@
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ static('mathjax3_config.js') }}"></script>
|
<script type="text/javascript" src="{{ static('mathjax3_config.js') }}"></script>
|
||||||
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
<script type="text/javascript" src="{{ static('event.js') }}"></script>
|
||||||
<script src="https://unpkg.com/@popperjs/core@2"></script>
|
|
||||||
<script type="module" src="https://unpkg.com/emoji-picker-element@1"></script>
|
<script type="module" src="https://unpkg.com/emoji-picker-element@1"></script>
|
||||||
<script type="text/javascript">
|
{% compress js %}
|
||||||
let message_template = `
|
{% include "chat/chat_js.html" %}
|
||||||
{% with message=message_template %}
|
{% endcompress %}
|
||||||
{% include "chat/message.html" %}
|
|
||||||
{% endwith %}
|
|
||||||
`;
|
|
||||||
let META_HEADER = [
|
|
||||||
"{{_('Recent')}}",
|
|
||||||
"{{_('Following')}}",
|
|
||||||
"{{_('Admin')}}",
|
|
||||||
"{{_('Other')}}",
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
window.limit_time = 24;
|
window.limit_time = 24;
|
||||||
window.room_id = "{{room if room else ''}}";
|
window.room_id = "{{room if room else ''}}";
|
||||||
|
@ -30,75 +20,11 @@
|
||||||
window.lock = false;
|
window.lock = false;
|
||||||
window.lock_click_space = false;
|
window.lock_click_space = false;
|
||||||
window.pushed_messages = new Set();
|
window.pushed_messages = new Set();
|
||||||
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
|
|
||||||
|
|
||||||
function load_next_page(last_id, refresh_html=false) {
|
|
||||||
var param = {
|
|
||||||
'last_id': last_id,
|
|
||||||
'only_messages': true,
|
|
||||||
}
|
|
||||||
$.get("{{ url('chat', '') }}" + window.room_id, param)
|
|
||||||
.fail(function() {
|
|
||||||
console.log("Fail to load page, last_id = " + last_id);
|
|
||||||
})
|
|
||||||
.done(function(data) {
|
|
||||||
if (refresh_html) {
|
|
||||||
$('#chat-log').html('');
|
|
||||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
|
||||||
window.lock = true;
|
|
||||||
}
|
|
||||||
var time = refresh_html ? 0 : 200;
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
$(".has_next").remove();
|
|
||||||
let $chat_box = $('#chat-box');
|
|
||||||
let lastMsgPos = scrollTopOfBottom($chat_box)
|
|
||||||
|
|
||||||
$('#loader').hide();
|
|
||||||
|
|
||||||
if (refresh_html) {
|
|
||||||
$('#chat-log').append(data);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#chat-log').prepend(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
register_time($('.time-with-rel'));
|
|
||||||
merge_authors();
|
|
||||||
|
|
||||||
if (!refresh_html) {
|
|
||||||
$chat_box.scrollTop(scrollTopOfBottom($chat_box) - lastMsgPos);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
|
||||||
}
|
|
||||||
window.lock = false;
|
|
||||||
window.has_next = parseInt($(".has_next").attr("value"));
|
|
||||||
}, time);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollTopOfBottom(container) {
|
|
||||||
return container[0].scrollHeight - container.innerHeight()
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollContainer(container, loader) {
|
|
||||||
container.scroll(function() {
|
|
||||||
if (container.scrollTop() == 0) {
|
|
||||||
if (!window.lock && window.has_next) {
|
|
||||||
loader.show();
|
|
||||||
var message_ids = $('.message').map(function() {
|
|
||||||
return parseInt($(this).attr('message-id'));
|
|
||||||
}).get();
|
|
||||||
load_next_page(Math.min(...message_ids));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
window.load_dynamic_update = function (last_msg) {
|
window.load_dynamic_update = function (last_msg) {
|
||||||
var receiver = new EventReceiver(
|
var receiver = new EventReceiver(
|
||||||
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
"{{ EVENT_DAEMON_LOCATION }}", "{{ EVENT_DAEMON_POLL_LOCATION }}",
|
||||||
['chat_lobby', 'chat_{{request.profile.id}}'], last_msg, function (message) {
|
['{{chat_lobby_channel}}', '{{chat_channel}}'], last_msg, function (message) {
|
||||||
if (window.pushed_messages.has(message.message)) {
|
if (window.pushed_messages.has(message.message)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -115,484 +41,13 @@
|
||||||
|
|
||||||
return receiver;
|
return receiver;
|
||||||
}
|
}
|
||||||
|
let message_template = `
|
||||||
function refresh_status() {
|
{% with message=message_template %}
|
||||||
$.get("{{url('online_status_ajax')}}")
|
{% include "chat/message.html" %}
|
||||||
.fail(function() {
|
{% endwith %}
|
||||||
console.log("Fail to get online status");
|
`;
|
||||||
})
|
|
||||||
.done(function(data) {
|
|
||||||
if (data.status == 403) {
|
|
||||||
console.log("Fail to retrieve data");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#chat-online-list').html(data).find('.toggle').each(function () {
|
|
||||||
register_toggle($(this));
|
|
||||||
});
|
|
||||||
register_click_space();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var data = {
|
|
||||||
'user': window.other_user_id,
|
|
||||||
};
|
|
||||||
|
|
||||||
$.get("{{url('user_online_status_ajax')}}", data)
|
|
||||||
.fail(function() {
|
|
||||||
console.log("Fail to get user online status");
|
|
||||||
})
|
|
||||||
.done(function(data) {
|
|
||||||
$('#chat-info').html(data);
|
|
||||||
register_time($('.time-with-rel'));
|
|
||||||
register_setting();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_message(data) {
|
|
||||||
var $data = $(data);
|
|
||||||
|
|
||||||
$('#chat-log').append($data);
|
|
||||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
|
||||||
register_time($('.time-with-rel'));
|
|
||||||
MathJax.typeset();
|
|
||||||
merge_authors();
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_new_message(message, room, is_self_author) {
|
|
||||||
function callback(update) {
|
|
||||||
if (!document['hidden']) {
|
|
||||||
if (update) update_last_seen();
|
|
||||||
refresh_status();
|
|
||||||
}
|
|
||||||
else if (!is_self_author) {
|
|
||||||
window.unread_message++;
|
|
||||||
document.title = "(" + window.unread_message + ") " + "{{ _('New message(s)') }}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (room == window.room_id) {
|
|
||||||
$.get({
|
|
||||||
url: "{{ url('chat_message_ajax') }}",
|
|
||||||
data: {
|
|
||||||
message: message,
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
add_message(data);
|
|
||||||
callback(true);
|
|
||||||
},
|
|
||||||
error: function (data) {
|
|
||||||
console.log('Could not add new message');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function check_new_message(message, tmp_id, room) {
|
|
||||||
if (room == room_id) {
|
|
||||||
$.get({
|
|
||||||
url: "{{ url('chat_message_ajax') }}",
|
|
||||||
data: {
|
|
||||||
message: message,
|
|
||||||
},
|
|
||||||
success: function (data) {
|
|
||||||
var $body_block = $(data).find('.body-block');
|
|
||||||
if ($('#message-'+tmp_id).length) {
|
|
||||||
$('#message-'+tmp_id).replaceWith(data);
|
|
||||||
}
|
|
||||||
else if ($('#body-block-'+tmp_id).length) {
|
|
||||||
$('#body-block-'+tmp_id).replaceWith($body_block);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
add_new_message(message, room, true);
|
|
||||||
}
|
|
||||||
MathJax.typeset();
|
|
||||||
register_time($('.time-with-rel'));
|
|
||||||
remove_unread_current_user();
|
|
||||||
merge_authors();
|
|
||||||
},
|
|
||||||
error: function (data) {
|
|
||||||
console.log('Fail to check message');
|
|
||||||
var $body = $('#body-block-'+tmp_id + ' p');
|
|
||||||
$body.css('text-decoration', 'line-through');
|
|
||||||
$body.css('text-decoration-color', 'red');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function merge_authors() {
|
|
||||||
var time_limit = 5; // minutes
|
|
||||||
var last = {
|
|
||||||
username: null,
|
|
||||||
time: null,
|
|
||||||
$content: null
|
|
||||||
};
|
|
||||||
$('.body-message').each(function() {
|
|
||||||
var username = $(this).find(".username a").text().trim();
|
|
||||||
var $body = $(this).find(".content-message .body-block");
|
|
||||||
var time = moment($(this).find(".time-with-rel").attr('data-iso'));
|
|
||||||
var $content = $(this).children('.content-message');
|
|
||||||
|
|
||||||
if (username == window.user.name) {
|
|
||||||
$(this).find('.message-text').each(function() {
|
|
||||||
$(this).removeClass('message-text-other').addClass('message-text-myself');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username == last.username && time.diff(last.time, 'minutes') <= time_limit) {
|
|
||||||
last.$content.append($body);
|
|
||||||
$(this).parent().remove();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
last.username = username;
|
|
||||||
last.time = time;
|
|
||||||
last.$content = $content;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function add_message_from_template(body, tmp_id) {
|
|
||||||
var html = message_template;
|
|
||||||
html = html.replaceAll('$body', body).replaceAll('$id', tmp_id);
|
|
||||||
var $html = $(html);
|
|
||||||
$html.find('.time-with-rel').attr('data-iso', (new Date()).toISOString());
|
|
||||||
add_message($html[0].outerHTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
function submit_chat() {
|
|
||||||
{% if last_msg and not request.profile.mute %}
|
|
||||||
if ($("#chat-input").val().trim()) {
|
|
||||||
var body = $('#chat-input').val().trim();
|
|
||||||
// body = body.split('\n').join('\n\n');
|
|
||||||
|
|
||||||
var message = {
|
|
||||||
body: body,
|
|
||||||
room: window.room_id,
|
|
||||||
tmp_id: Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
$('#chat-input').val('');
|
|
||||||
|
|
||||||
add_message_from_template(body, message.tmp_id);
|
|
||||||
|
|
||||||
$.post("{{ url('post_chat_message') }}", message)
|
|
||||||
.fail(function(res) {
|
|
||||||
console.log('Fail to send message');
|
|
||||||
})
|
|
||||||
.done(function(res, status) {
|
|
||||||
$('#empty_msg').hide();
|
|
||||||
$('#chat-input').focus();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
{% endif %}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resize_emoji(element) {
|
|
||||||
var html = element.html();
|
|
||||||
html = html.replace(/(\p{Extended_Pictographic})/ug, `<span class="big-emoji">$1</span>`);
|
|
||||||
element.html(html);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert_char_after_cursor(elem, char) {
|
|
||||||
var val = elem.value;
|
|
||||||
if (typeof elem.selectionStart == "number" && typeof elem.selectionEnd == "number") {
|
|
||||||
var start = elem.selectionStart;
|
|
||||||
var prefix = elem.value.slice(0, start);
|
|
||||||
var prefix_added = prefix + char;
|
|
||||||
var chars = [...val];
|
|
||||||
chars.splice([...prefix].length, 0, char);
|
|
||||||
elem.value = chars.join('');
|
|
||||||
elem.selectionStart = elem.selectionEnd = prefix_added.length;
|
|
||||||
} else if (document.selection && document.selection.createRange) {
|
|
||||||
var range = document.selection.createRange();
|
|
||||||
elem.focus();
|
|
||||||
range.text = char;
|
|
||||||
range.collapse(false);
|
|
||||||
range.select();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function load_room(encrypted_user) {
|
|
||||||
if (window.lock_click_space) return;
|
|
||||||
|
|
||||||
function callback() {
|
|
||||||
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
|
|
||||||
load_next_page(null, true);
|
|
||||||
update_last_seen();
|
|
||||||
refresh_status();
|
|
||||||
$('#chat-input').focus();
|
|
||||||
}
|
|
||||||
window.lock_click_space = true;
|
|
||||||
if (encrypted_user) {
|
|
||||||
$.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`)
|
|
||||||
.done(function(data) {
|
|
||||||
window.room_id = data.room;
|
|
||||||
window.other_user_id = data.other_user_id;
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
.fail(function() {
|
|
||||||
console.log('Fail to get_or_create_room');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
window.room_id = '';
|
|
||||||
window.other_user_id = '';
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
window.lock_click_space = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function register_click_space() {
|
|
||||||
$('.click_space').on('click', function(e) {
|
|
||||||
if ($(this).attr('id') == 'click_space_' + window.other_user_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var other_user = $(this).attr('value');
|
|
||||||
load_room(other_user);
|
|
||||||
});
|
|
||||||
$('#lobby_row').on('click', function(e) {
|
|
||||||
if (window.room_id) {
|
|
||||||
load_room(null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (isMobile) {
|
|
||||||
$('#chat-tab a').click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function update_last_seen() {
|
|
||||||
var data = {
|
|
||||||
room: window.room_id
|
|
||||||
};
|
|
||||||
|
|
||||||
$.post("{{ url('update_last_seen') }}", data)
|
|
||||||
.fail(function(data) {
|
|
||||||
console.log('Fail to update last seen');
|
|
||||||
})
|
|
||||||
.done(function(data) {
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove_unread_current_user() {
|
|
||||||
if (window.other_user_id) {
|
|
||||||
$("#unread-count-" + window.other_user_id).hide();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$('#unread-count-lobby').hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function register_setting() {
|
|
||||||
$('#setting-button').on('click', function() {
|
|
||||||
$('#setting-content').toggle();
|
|
||||||
});
|
|
||||||
$('#setting-content li').on('click', function() {
|
|
||||||
$(this).children('a')[0].click();
|
|
||||||
})
|
|
||||||
$('#setting-content a').on('click', function() {
|
|
||||||
var href = $(this).attr('href');
|
|
||||||
href += '?next=' + window.location.pathname;
|
|
||||||
$(this).attr('href', href);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$(function() {
|
$(function() {
|
||||||
$('#loader').hide();
|
|
||||||
merge_authors();
|
|
||||||
window.has_next = parseInt($(".has_next").attr("value"));
|
|
||||||
|
|
||||||
scrollContainer($('#chat-box'), $('#loader'))
|
|
||||||
|
|
||||||
{% if request.user.is_staff %}
|
|
||||||
$(document).on("click", ".chat_remove", function() {
|
|
||||||
var elt = $(this);
|
|
||||||
$.ajax({
|
|
||||||
url: "{{ url('delete_chat_message') }}",
|
|
||||||
type: 'post',
|
|
||||||
data: {
|
|
||||||
message: elt.attr('value'),
|
|
||||||
},
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(data){
|
|
||||||
var $block = elt.parent();
|
|
||||||
if ($block.parent().find('.body-block').length > 1) {
|
|
||||||
$block.remove();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
elt.closest('li').remove();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: function(data) {
|
|
||||||
console.log('Fail to delete');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
$(document).on("click", ".chat_mute", function() {
|
|
||||||
if (confirm("{{_('Mute this user and delete all messages?')}}")) {
|
|
||||||
var elt = $(this);
|
|
||||||
$.ajax({
|
|
||||||
url: "{{ url('mute_chat_message') }}",
|
|
||||||
type: 'post',
|
|
||||||
data: {
|
|
||||||
message: elt.attr('value'),
|
|
||||||
},
|
|
||||||
dataType: 'json',
|
|
||||||
success: function(data){
|
|
||||||
window.location.reload();
|
|
||||||
},
|
|
||||||
fail: function(data) {
|
|
||||||
console.log('Fail to delete');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
$("#chat-log").show();
|
|
||||||
$("#chat-log").change(function() {
|
|
||||||
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#chat-input').focus();
|
|
||||||
|
|
||||||
$('#chat-input').keydown(function(e) {
|
|
||||||
if (e.keyCode === 13) {
|
|
||||||
if (e.ctrlKey || e.shiftKey) {
|
|
||||||
insert_char_after_cursor(this, "\n");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
e.preventDefault();
|
|
||||||
submit_chat();
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.chat-right-panel').hide();
|
|
||||||
$('#chat-tab').find('a').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('#chat-tab').addClass('active');
|
|
||||||
$('#online-tab').removeClass('active');
|
|
||||||
$('.chat-left-panel').show();
|
|
||||||
$('.chat-right-panel').hide();
|
|
||||||
});
|
|
||||||
$('#online-tab').find('a').click(function (e) {
|
|
||||||
e.preventDefault();
|
|
||||||
$('#online-tab').addClass('active');
|
|
||||||
$('#chat-tab').removeClass('active');
|
|
||||||
$('.chat-left-panel').hide();
|
|
||||||
$('.chat-right-panel').show();
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#refresh-button').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
refresh_status();
|
|
||||||
});
|
|
||||||
|
|
||||||
setInterval(refresh_status, 2 * 60 * 1000);
|
|
||||||
|
|
||||||
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
|
||||||
load_dynamic_update({{last_msg}});
|
load_dynamic_update({{last_msg}});
|
||||||
|
|
||||||
const button = document.querySelector('#emoji-button');
|
|
||||||
const tooltip = document.querySelector('.tooltip');
|
|
||||||
Popper.createPopper(button, tooltip, {
|
|
||||||
placement: 'left-end',
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleEmoji() {
|
|
||||||
tooltip.classList.toggle('shown')
|
|
||||||
}
|
|
||||||
$('#emoji-button').on('click', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
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) {
|
|
||||||
var $chat = $('#chat-input').get(0);
|
|
||||||
insert_char_after_cursor($chat, e.detail.unicode);
|
|
||||||
$chat.focus();
|
|
||||||
})
|
|
||||||
|
|
||||||
register_click_space();
|
|
||||||
|
|
||||||
document.addEventListener('keydown', function(e) {
|
|
||||||
if (e.keyCode === 27 && $('.tooltip').hasClass('shown')) {
|
|
||||||
toggleEmoji();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
$('#search-handle').replaceWith($('<select>').attr({
|
|
||||||
id: 'search-handle',
|
|
||||||
name: 'other',
|
|
||||||
onchange: 'form.submit()'
|
|
||||||
}));
|
|
||||||
|
|
||||||
var in_user_redirect = false;
|
|
||||||
$('#search-handle').select2({
|
|
||||||
placeholder: '{{ _('Search by handle...') }}',
|
|
||||||
ajax: {
|
|
||||||
url: '{{ url('chat_user_search_select2_ajax') }}'
|
|
||||||
},
|
|
||||||
minimumInputLength: 1,
|
|
||||||
escapeMarkup: function (markup) {
|
|
||||||
return markup;
|
|
||||||
},
|
|
||||||
templateResult: function (data, container) {
|
|
||||||
return $('<span>')
|
|
||||||
.append($('<img>', {
|
|
||||||
'class': 'user-search-image', src: data.gravatar_url,
|
|
||||||
width: 24, height: 24
|
|
||||||
}))
|
|
||||||
.append($('<span>', {'class': data.display_rank + ' user-search-name'}).text(data.text))
|
|
||||||
.append($('<a>', {href: '/user/' + data.text, 'class': 'user-redirect'})
|
|
||||||
.append($('<i>', {'class': 'fa fa-mail-forward'}))
|
|
||||||
.mouseover(function () {
|
|
||||||
in_user_redirect = true;
|
|
||||||
}).mouseout(function () {
|
|
||||||
in_user_redirect = false;
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}).on('select2:selecting', function () {
|
|
||||||
return !in_user_redirect;
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#chat-input").on("keyup", function() {
|
|
||||||
$("#chat-input").scrollTop($("#chat-input")[0].scrollHeight);
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/42121565/detecting-class-change-without-setinterval
|
|
||||||
if (typeof(MutationObserver) !== undefined) {
|
|
||||||
var observer = new MutationObserver(function (event) {
|
|
||||||
if (!document['hidden'] && window.unread_message > 0) {
|
|
||||||
update_last_seen();
|
|
||||||
refresh_status();
|
|
||||||
window.unread_message = 0;
|
|
||||||
document.title = "{{_('Chat Box')}}";
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(document.body, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['class'],
|
|
||||||
childList: false,
|
|
||||||
characterData: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
register_setting();
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -615,38 +70,36 @@
|
||||||
|
|
||||||
<div id="chat-container">
|
<div id="chat-container">
|
||||||
<div id="chat-online" class="chat-right-panel sidebox">
|
<div id="chat-online" class="chat-right-panel sidebox">
|
||||||
<h3>
|
|
||||||
<i class="fa fa-users"></i>{{_('Online Users')}}
|
|
||||||
</h3>
|
|
||||||
<div id="chat-online-content">
|
<div id="chat-online-content">
|
||||||
<div id="search-container">
|
<div id="search-container">
|
||||||
<center>
|
<form id="chat-search-form" name="form" action="{{ url('get_or_create_room') }}" method="post">
|
||||||
<form id="search-form" name="form" action="{{ url('get_or_create_room') }}" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input id="search-handle" type="text" name="search"
|
||||||
<input id="search-handle" type="text" name="search"
|
placeholder="{{ _('Search by handle...') }}">
|
||||||
placeholder="{{ _('Search by handle...') }}">
|
</form>
|
||||||
</form>
|
|
||||||
</center>
|
|
||||||
</div>
|
</div>
|
||||||
<div id="chat-online-list">
|
<div id="chat-online-list">
|
||||||
{% include "chat/online_status.html" %}
|
{% include "chat/online_status.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="chat-area" class="chat-left-panel" style="width:100%">
|
<div id="chat-area" class="chat-left-panel">
|
||||||
<div id="chat-info" style="height: 10%">
|
<div id="chat-info">
|
||||||
{% include 'chat/user_online_status.html' %}
|
{% include 'chat/user_online_status.html' %}
|
||||||
</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">
|
||||||
<ul id="chat-log" style="display: none">
|
|
||||||
{% include 'chat/message_list.html' %}
|
{% include 'chat/message_list.html' %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div style="height: 15%">
|
<div id="chat-input-container">
|
||||||
<a id="emoji-button" href="#" title="{{_('Emoji')}}"><i class="icofont-slightly-smile"></i></a>
|
|
||||||
<textarea maxlength="5000" id="chat-input" placeholder="{{_('Enter your message')}}"></textarea>
|
<textarea maxlength="5000" id="chat-input" placeholder="{{_('Enter your message')}}"></textarea>
|
||||||
|
<div class="chat-input-icon" id="emoji-button" href="#" title="{{_('Emoji')}}"><i class="icofont-slightly-smile"></i>
|
||||||
|
</div>
|
||||||
|
<div class="chat-input-icon" id="submit-button">
|
||||||
|
<i class="fa fa-play"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tooltip" role="tooltip">
|
<div class="tooltip" role="tooltip">
|
||||||
<emoji-picker></emoji-picker>
|
<emoji-picker></emoji-picker>
|
||||||
|
|
|
@ -1,132 +1,200 @@
|
||||||
<style>
|
{% compress css %}
|
||||||
footer {
|
<style>
|
||||||
display: none;
|
footer {
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background-color: #d6dee1;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 6px solid transparent;
|
|
||||||
background-clip: content-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb:hover {
|
|
||||||
background-color: #a8bbbf;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-container {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-message img{
|
|
||||||
max-height: 12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:not(.shown) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
resize: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip {
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loader {
|
|
||||||
display: block;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
width: 4%;
|
|
||||||
}
|
|
||||||
.profile-pic {
|
|
||||||
height: 2.6em;
|
|
||||||
width: 2.6em;
|
|
||||||
border-radius: 0.3em;
|
|
||||||
margin-top: 0.1em;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
.body-message {
|
|
||||||
padding-left: 3em;
|
|
||||||
padding-bottom: 0.5em;
|
|
||||||
border-bottom: 1px dotted lightgray;
|
|
||||||
}
|
|
||||||
.user-time {
|
|
||||||
margin-bottom: 0.3em;
|
|
||||||
}
|
|
||||||
.time {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
.clear {
|
|
||||||
clear: both;
|
|
||||||
}
|
|
||||||
.content-message {
|
|
||||||
word-wrap: break-word;
|
|
||||||
}
|
|
||||||
.content-message p {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
#content {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#content-body {
|
|
||||||
padding-bottom: 0;
|
|
||||||
}
|
|
||||||
#page-container {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
.sidebox h3 {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
.body-block {
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 0.05em 0.6em;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
#search-form {
|
|
||||||
float: inherit;
|
|
||||||
}
|
|
||||||
#search-container {
|
|
||||||
margin-bottom: 0.4em;
|
|
||||||
}
|
|
||||||
#setting {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@media (min-width: 800px) {
|
|
||||||
#page-container {
|
|
||||||
position:fixed;
|
|
||||||
overflow:hidden;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 799px) {
|
|
||||||
html, body {
|
|
||||||
max-width: 100%;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
#mobile ul {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.info-pic {
|
|
||||||
margin-left: 0.5em;
|
|
||||||
}
|
|
||||||
.active-span {
|
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</style>
|
#content {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #d6dee1;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
background-clip: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #a8bbbf;
|
||||||
|
}
|
||||||
|
|
||||||
|
#page-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body-message img{
|
||||||
|
max-height: 12em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:not(.shown) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
resize: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
.profile-pic {
|
||||||
|
height: 2.6em;
|
||||||
|
width: 2.6em;
|
||||||
|
border-radius: 0.3em;
|
||||||
|
margin-top: 0.1em;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
.body-message {
|
||||||
|
padding-left: 3em;
|
||||||
|
padding-bottom: 0.5em;
|
||||||
|
}
|
||||||
|
.user-time {
|
||||||
|
margin-bottom: 0.3em;
|
||||||
|
}
|
||||||
|
.time {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
.content-message {
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
.content-message p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#content-body {
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
#page-container {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.sidebox h3 {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.body-block {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.05em 0.6em;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
#search-form {
|
||||||
|
float: inherit;
|
||||||
|
}
|
||||||
|
#search-container {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
#setting {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.status-user {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.wrapline {
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
#chat-input-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
#chat-input {
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||||
|
transition: box-shadow 0.3s ease-in-out;
|
||||||
|
width: 80%;
|
||||||
|
resize: none;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#chat-input:focus {
|
||||||
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.chat-input-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
border: 2px solid;
|
||||||
|
}
|
||||||
|
.chat-input-icon:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
#chat-area {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.info-circle {
|
||||||
|
position: absolute;
|
||||||
|
cx: 5px;
|
||||||
|
cy: 5px;
|
||||||
|
r: 5px;
|
||||||
|
stroke: white;
|
||||||
|
stroke-width: 1;
|
||||||
|
}
|
||||||
|
.info-pic {
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
#chat-info {
|
||||||
|
height: 3em;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 800px) {
|
||||||
|
#page-container {
|
||||||
|
position:fixed;
|
||||||
|
overflow:hidden;
|
||||||
|
}
|
||||||
|
#chat-input-container {
|
||||||
|
padding-left: 5%;
|
||||||
|
}
|
||||||
|
#chat-area {
|
||||||
|
padding-bottom: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 799px) {
|
||||||
|
html, body {
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
#mobile ul {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.info-pic {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
.active-span {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endcompress %}
|
562
templates/chat/chat_js.html
Normal file
562
templates/chat/chat_js.html
Normal file
|
@ -0,0 +1,562 @@
|
||||||
|
<script type="text/javascript">
|
||||||
|
let META_HEADER = [
|
||||||
|
"{{_('Recent')}}",
|
||||||
|
"{{_('Following')}}",
|
||||||
|
"{{_('Admin')}}",
|
||||||
|
"{{_('Other')}}",
|
||||||
|
];
|
||||||
|
let isMobile = window.matchMedia("only screen and (max-width: 799px)").matches;
|
||||||
|
|
||||||
|
function load_next_page(last_id, refresh_html=false) {
|
||||||
|
var param = {
|
||||||
|
'last_id': last_id,
|
||||||
|
'only_messages': true,
|
||||||
|
}
|
||||||
|
$.get("{{ url('chat', '') }}" + window.room_id, param)
|
||||||
|
.fail(function() {
|
||||||
|
console.log("Fail to load page, last_id = " + last_id);
|
||||||
|
})
|
||||||
|
.done(function(data) {
|
||||||
|
if (refresh_html) {
|
||||||
|
$('#chat-log').html('');
|
||||||
|
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||||
|
window.lock = true;
|
||||||
|
}
|
||||||
|
var time = refresh_html ? 0 : 200;
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
$(".has_next").remove();
|
||||||
|
let $chat_box = $('#chat-box');
|
||||||
|
let lastMsgPos = scrollTopOfBottom($chat_box)
|
||||||
|
|
||||||
|
$('#loader').hide();
|
||||||
|
|
||||||
|
if (refresh_html) {
|
||||||
|
$('#chat-log').append(data);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#chat-log').prepend(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
register_time($('.time-with-rel'));
|
||||||
|
merge_authors();
|
||||||
|
|
||||||
|
if (!refresh_html) {
|
||||||
|
$chat_box.scrollTop(scrollTopOfBottom($chat_box) - lastMsgPos);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||||
|
}
|
||||||
|
window.lock = false;
|
||||||
|
window.has_next = parseInt($(".has_next").attr("value"));
|
||||||
|
}, time);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTopOfBottom(container) {
|
||||||
|
return container[0].scrollHeight - container.innerHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollContainer(container, loader) {
|
||||||
|
container.scroll(function() {
|
||||||
|
if (container.scrollTop() == 0) {
|
||||||
|
if (!window.lock && window.has_next) {
|
||||||
|
loader.show();
|
||||||
|
var message_ids = $('.message').map(function() {
|
||||||
|
return parseInt($(this).attr('message-id'));
|
||||||
|
}).get();
|
||||||
|
load_next_page(Math.min(...message_ids));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})}
|
||||||
|
|
||||||
|
function refresh_status() {
|
||||||
|
$.get("{{url('online_status_ajax')}}")
|
||||||
|
.fail(function() {
|
||||||
|
console.log("Fail to get online status");
|
||||||
|
})
|
||||||
|
.done(function(data) {
|
||||||
|
if (data.status == 403) {
|
||||||
|
console.log("Fail to retrieve data");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#chat-online-list').html(data).find('.toggle').each(function () {
|
||||||
|
register_toggle($(this));
|
||||||
|
});
|
||||||
|
register_click_space();
|
||||||
|
color_selected_room();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var data = {
|
||||||
|
'user': window.other_user_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
$.get("{{url('user_online_status_ajax')}}", data)
|
||||||
|
.fail(function() {
|
||||||
|
console.log("Fail to get user online status");
|
||||||
|
})
|
||||||
|
.done(function(data) {
|
||||||
|
$('#chat-info').html(data);
|
||||||
|
register_time($('.time-with-rel'));
|
||||||
|
register_setting();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_message(data) {
|
||||||
|
var $data = $(data);
|
||||||
|
|
||||||
|
$('#chat-log').append($data);
|
||||||
|
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||||
|
register_time($('.time-with-rel'));
|
||||||
|
MathJax.typeset();
|
||||||
|
merge_authors();
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_new_message(message, room, is_self_author) {
|
||||||
|
function callback(update) {
|
||||||
|
if (!document['hidden']) {
|
||||||
|
if (update) update_last_seen();
|
||||||
|
refresh_status();
|
||||||
|
}
|
||||||
|
else if (!is_self_author) {
|
||||||
|
window.unread_message++;
|
||||||
|
document.title = "(" + window.unread_message + ") " + "{{ _('New message(s)') }}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (room == window.room_id) {
|
||||||
|
$.get({
|
||||||
|
url: "{{ url('chat_message_ajax') }}",
|
||||||
|
data: {
|
||||||
|
message: message,
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
add_message(data);
|
||||||
|
callback(true);
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
console.log('Could not add new message');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callback(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function check_new_message(message, tmp_id, room) {
|
||||||
|
if (room == room_id) {
|
||||||
|
$.get({
|
||||||
|
url: "{{ url('chat_message_ajax') }}",
|
||||||
|
data: {
|
||||||
|
message: message,
|
||||||
|
},
|
||||||
|
success: function (data) {
|
||||||
|
var $body_block = $(data).find('.body-block');
|
||||||
|
if ($('#message-'+tmp_id).length) {
|
||||||
|
$('#message-'+tmp_id).replaceWith(data);
|
||||||
|
}
|
||||||
|
else if ($('#body-block-'+tmp_id).length) {
|
||||||
|
$('#body-block-'+tmp_id).replaceWith($body_block);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
add_new_message(message, room, true);
|
||||||
|
}
|
||||||
|
MathJax.typeset();
|
||||||
|
register_time($('.time-with-rel'));
|
||||||
|
remove_unread_current_user();
|
||||||
|
merge_authors();
|
||||||
|
},
|
||||||
|
error: function (data) {
|
||||||
|
console.log('Fail to check message');
|
||||||
|
var $body = $('#body-block-'+tmp_id + ' p');
|
||||||
|
$body.css('text-decoration', 'line-through');
|
||||||
|
$body.css('text-decoration-color', 'red');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge_authors() {
|
||||||
|
var time_limit = 5; // minutes
|
||||||
|
var last = {
|
||||||
|
username: null,
|
||||||
|
time: null,
|
||||||
|
$content: null
|
||||||
|
};
|
||||||
|
$('.body-message').each(function() {
|
||||||
|
var username = $(this).find(".username a").text().trim();
|
||||||
|
var $body = $(this).find(".content-message .body-block");
|
||||||
|
var time = moment($(this).find(".time-with-rel").attr('data-iso'));
|
||||||
|
var $content = $(this).children('.content-message');
|
||||||
|
|
||||||
|
if (username == window.user.name) {
|
||||||
|
$(this).find('.message-text').each(function() {
|
||||||
|
$(this).removeClass('message-text-other').addClass('message-text-myself');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (username == last.username && time.diff(last.time, 'minutes') <= time_limit) {
|
||||||
|
last.$content.append($body);
|
||||||
|
$(this).parent().remove();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
last.username = username;
|
||||||
|
last.time = time;
|
||||||
|
last.$content = $content;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function add_message_from_template(body, tmp_id) {
|
||||||
|
if (window.room_id) {
|
||||||
|
$("#last_msg-" + window.room_id).html(body);
|
||||||
|
}
|
||||||
|
var html = message_template;
|
||||||
|
html = html.replaceAll('$body', body).replaceAll('$id', tmp_id);
|
||||||
|
var $html = $(html);
|
||||||
|
$html.find('.time-with-rel').attr('data-iso', (new Date()).toISOString());
|
||||||
|
add_message($html[0].outerHTML);
|
||||||
|
}
|
||||||
|
|
||||||
|
function submit_chat() {
|
||||||
|
{% if last_msg and not request.profile.mute %}
|
||||||
|
if ($("#chat-input").val().trim()) {
|
||||||
|
$('#chat-input-container').height('auto');
|
||||||
|
var body = $('#chat-input').val().trim();
|
||||||
|
// body = body.split('\n').join('\n\n');
|
||||||
|
|
||||||
|
var message = {
|
||||||
|
body: body,
|
||||||
|
room: window.room_id,
|
||||||
|
tmp_id: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
$('#chat-input').val('');
|
||||||
|
|
||||||
|
add_message_from_template(body, message.tmp_id);
|
||||||
|
|
||||||
|
$.post("{{ url('post_chat_message') }}", message)
|
||||||
|
.fail(function(res) {
|
||||||
|
console.log('Fail to send message');
|
||||||
|
})
|
||||||
|
.done(function(res, status) {
|
||||||
|
$('#empty_msg').hide();
|
||||||
|
$('#chat-input').focus();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resize_emoji(element) {
|
||||||
|
var html = element.html();
|
||||||
|
html = html.replace(/(\p{Extended_Pictographic})/ug, `<span class="big-emoji">$1</span>`);
|
||||||
|
element.html(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
function insert_char_after_cursor(elem, char) {
|
||||||
|
var val = elem.value;
|
||||||
|
if (typeof elem.selectionStart == "number" && typeof elem.selectionEnd == "number") {
|
||||||
|
var start = elem.selectionStart;
|
||||||
|
var prefix = elem.value.slice(0, start);
|
||||||
|
var prefix_added = prefix + char;
|
||||||
|
var chars = [...val];
|
||||||
|
chars.splice([...prefix].length, 0, char);
|
||||||
|
elem.value = chars.join('');
|
||||||
|
elem.selectionStart = elem.selectionEnd = prefix_added.length;
|
||||||
|
} else if (document.selection && document.selection.createRange) {
|
||||||
|
var range = document.selection.createRange();
|
||||||
|
elem.focus();
|
||||||
|
range.text = char;
|
||||||
|
range.collapse(false);
|
||||||
|
range.select();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function color_selected_room() {
|
||||||
|
$(".status-row").removeClass("selected-status-row");
|
||||||
|
$("#click_space_" + window.other_user_id).addClass("selected-status-row");
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_room(encrypted_user) {
|
||||||
|
if (window.lock_click_space) return;
|
||||||
|
|
||||||
|
function callback() {
|
||||||
|
history.replaceState(null, '', "{{url('chat', '')}}" + window.room_id);
|
||||||
|
load_next_page(null, true);
|
||||||
|
update_last_seen();
|
||||||
|
refresh_status();
|
||||||
|
$('#chat-input').focus();
|
||||||
|
}
|
||||||
|
window.lock_click_space = true;
|
||||||
|
if (encrypted_user) {
|
||||||
|
$.get("{{url('get_or_create_room')}}" + `?other=${encrypted_user}`)
|
||||||
|
.done(function(data) {
|
||||||
|
window.room_id = data.room;
|
||||||
|
window.other_user_id = data.other_user_id;
|
||||||
|
color_selected_room();
|
||||||
|
callback();
|
||||||
|
})
|
||||||
|
.fail(function() {
|
||||||
|
console.log('Fail to get_or_create_room');
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
window.room_id = '';
|
||||||
|
window.other_user_id = '';
|
||||||
|
color_selected_room();
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
window.lock_click_space = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function register_click_space() {
|
||||||
|
$('.click_space').on('click', function(e) {
|
||||||
|
if ($(this).attr('id') == 'click_space_' + window.other_user_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var other_user = $(this).attr('value');
|
||||||
|
load_room(other_user);
|
||||||
|
});
|
||||||
|
$('#lobby_row').on('click', function(e) {
|
||||||
|
if (window.room_id) {
|
||||||
|
load_room(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (isMobile) {
|
||||||
|
$('#chat-tab a').click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update_last_seen() {
|
||||||
|
var data = {
|
||||||
|
room: window.room_id
|
||||||
|
};
|
||||||
|
|
||||||
|
$.post("{{ url('update_last_seen') }}", data)
|
||||||
|
.fail(function(data) {
|
||||||
|
console.log('Fail to update last seen');
|
||||||
|
})
|
||||||
|
.done(function(data) {
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function remove_unread_current_user() {
|
||||||
|
if (window.other_user_id) {
|
||||||
|
$("#unread-count-" + window.other_user_id).hide();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$('#unread-count-lobby').hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function register_setting() {
|
||||||
|
$('#setting-button').on('click', function() {
|
||||||
|
$('#setting-content').toggle();
|
||||||
|
});
|
||||||
|
$('#setting-content li').on('click', function() {
|
||||||
|
$(this).children('a')[0].click();
|
||||||
|
})
|
||||||
|
$('#setting-content a').on('click', function() {
|
||||||
|
var href = $(this).attr('href');
|
||||||
|
href += '?next=' + window.location.pathname;
|
||||||
|
$(this).attr('href', href);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
$(function() {
|
||||||
|
$('#loader').hide();
|
||||||
|
update_last_seen();
|
||||||
|
merge_authors();
|
||||||
|
window.has_next = parseInt($(".has_next").attr("value"));
|
||||||
|
|
||||||
|
scrollContainer($('#chat-box'), $('#loader'))
|
||||||
|
|
||||||
|
{% if request.user.is_staff %}
|
||||||
|
$(document).on("click", ".chat_remove", function() {
|
||||||
|
var elt = $(this);
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ url('delete_chat_message') }}",
|
||||||
|
type: 'post',
|
||||||
|
data: {
|
||||||
|
message: elt.attr('value'),
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data){
|
||||||
|
var $block = elt.parent();
|
||||||
|
if ($block.parent().find('.body-block').length > 1) {
|
||||||
|
$block.remove();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
elt.closest('li').remove();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: function(data) {
|
||||||
|
console.log('Fail to delete');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
$(document).on("click", ".chat_mute", function() {
|
||||||
|
if (confirm("{{_('Mute this user and delete all messages?')}}")) {
|
||||||
|
var elt = $(this);
|
||||||
|
$.ajax({
|
||||||
|
url: "{{ url('mute_chat_message') }}",
|
||||||
|
type: 'post',
|
||||||
|
data: {
|
||||||
|
message: elt.attr('value'),
|
||||||
|
},
|
||||||
|
dataType: 'json',
|
||||||
|
success: function(data){
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
fail: function(data) {
|
||||||
|
console.log('Fail to delete');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
$("#chat-log").show();
|
||||||
|
$("#chat-log").change(function() {
|
||||||
|
$('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#chat-input').focus();
|
||||||
|
|
||||||
|
$('#chat-input').keydown(function(e) {
|
||||||
|
if (e.keyCode === 13) {
|
||||||
|
if (e.ctrlKey || e.shiftKey) {
|
||||||
|
insert_char_after_cursor(this, "\n");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
e.preventDefault();
|
||||||
|
submit_chat();
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#chat-tab').find('a').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#chat-tab').addClass('active');
|
||||||
|
$('#online-tab').removeClass('active');
|
||||||
|
$('.chat-left-panel').show();
|
||||||
|
$('.chat-right-panel').hide();
|
||||||
|
});
|
||||||
|
$('#online-tab').find('a').click(function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
$('#online-tab').addClass('active');
|
||||||
|
$('#chat-tab').removeClass('active');
|
||||||
|
$('.chat-left-panel').hide();
|
||||||
|
$('.chat-right-panel').show();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#refresh-button').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
refresh_status();
|
||||||
|
});
|
||||||
|
|
||||||
|
setInterval(refresh_status, 2 * 60 * 1000);
|
||||||
|
|
||||||
|
$('#chat-box').scrollTop($('#chat-box')[0].scrollHeight);
|
||||||
|
|
||||||
|
const button = document.querySelector('#emoji-button');
|
||||||
|
const tooltip = document.querySelector('.tooltip');
|
||||||
|
Popper.createPopper(button, tooltip, {
|
||||||
|
placement: 'left-end',
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleEmoji() {
|
||||||
|
tooltip.classList.toggle('shown')
|
||||||
|
}
|
||||||
|
$('#emoji-button').on('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
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) {
|
||||||
|
var $chat = $('#chat-input').get(0);
|
||||||
|
insert_char_after_cursor($chat, e.detail.unicode);
|
||||||
|
$chat.focus();
|
||||||
|
})
|
||||||
|
|
||||||
|
register_click_space();
|
||||||
|
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.keyCode === 27 && $('.tooltip').hasClass('shown')) {
|
||||||
|
toggleEmoji();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
$('#search-handle').replaceWith($('<select>').attr({
|
||||||
|
id: 'search-handle',
|
||||||
|
name: 'other',
|
||||||
|
onchange: 'form.submit()'
|
||||||
|
}));
|
||||||
|
|
||||||
|
var in_user_redirect = false;
|
||||||
|
$('#search-handle').select2({
|
||||||
|
placeholder: '<i class="fa fa-search"></i> {{ _('Search by handle...') }}',
|
||||||
|
ajax: {
|
||||||
|
url: '{{ url('chat_user_search_select2_ajax') }}'
|
||||||
|
},
|
||||||
|
minimumInputLength: 1,
|
||||||
|
escapeMarkup: function (markup) {
|
||||||
|
return markup;
|
||||||
|
},
|
||||||
|
templateResult: function (data, container) {
|
||||||
|
return $('<span>')
|
||||||
|
.append($('<img>', {
|
||||||
|
'class': 'user-search-image', src: data.gravatar_url,
|
||||||
|
width: 24, height: 24
|
||||||
|
}))
|
||||||
|
.append($('<span>', {'class': data.display_rank + ' user-search-name'}).text(data.text))
|
||||||
|
.append($('<a>', {href: '/user/' + data.text, 'class': 'user-redirect'})
|
||||||
|
.append($('<i>', {'class': 'fa fa-mail-forward'}))
|
||||||
|
.mouseover(function () {
|
||||||
|
in_user_redirect = true;
|
||||||
|
}).mouseout(function () {
|
||||||
|
in_user_redirect = false;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}).on('select2:selecting', function () {
|
||||||
|
return !in_user_redirect;
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#chat-input").on("keyup", function() {
|
||||||
|
$("#chat-input").scrollTop($("#chat-input")[0].scrollHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
// https://stackoverflow.com/questions/42121565/detecting-class-change-without-setinterval
|
||||||
|
if (typeof(MutationObserver) !== undefined) {
|
||||||
|
var observer = new MutationObserver(function (event) {
|
||||||
|
if (!document['hidden'] && window.unread_message > 0) {
|
||||||
|
update_last_seen();
|
||||||
|
refresh_status();
|
||||||
|
window.unread_message = 0;
|
||||||
|
document.title = "{{_('Chat Box')}}";
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(document.body, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['class'],
|
||||||
|
childList: false,
|
||||||
|
characterData: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
register_setting();
|
||||||
|
color_selected_room();
|
||||||
|
});
|
||||||
|
</script>
|
|
@ -27,11 +27,18 @@
|
||||||
fill="{{'green' if user.is_online else 'red'}}"/>
|
fill="{{'green' if user.is_online else 'red'}}"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span style="padding-left:0.3em" class="username {{ user.user.css_class }}">
|
<div class="status-user">
|
||||||
{{ user.user.username }}
|
<span class="username {{ user.user.css_class }} wrapline">
|
||||||
</span>
|
{{ user.user.username }}
|
||||||
<span class="spacer">
|
</span>
|
||||||
<span class="unread-count" id="unread-count-{{user.user.id}}">{{user.unread_count if user.unread_count}}</span>
|
{% if user.last_msg %}
|
||||||
|
<span class="status_last_message wrapline" {% if user.room %}id="last_msg-{{user.room}}"{% endif %}>
|
||||||
|
{{ user.last_msg }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<span class="unread-count" id="unread-count-{{user.user.id}}">
|
||||||
|
{{user.unread_count if user.unread_count}}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{% 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, 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%; transform: rotate(180deg);" >
|
||||||
<circle class="info-circle"
|
<circle class="info-circle"
|
||||||
fill="{{'green' if other_online else 'red'}}"/>
|
fill="{{'green' if other_online else 'red'}}"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
|
@ -5,6 +5,10 @@
|
||||||
|
|
||||||
{% block two_col_media %}
|
{% block two_col_media %}
|
||||||
<style>
|
<style>
|
||||||
|
.non-padding-top {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.content-description ul {
|
.content-description ul {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
@ -109,10 +113,11 @@
|
||||||
|
|
||||||
{% macro contest_head(contest) %}
|
{% macro contest_head(contest) %}
|
||||||
{% spaceless %}
|
{% spaceless %}
|
||||||
<a href="{{ url('contest_view', contest.key) }}" class="contest-list-title">
|
<a href="{{ url('contest_view', contest.key) }}" class="contest-list-title" style="margin-right: 5px;">
|
||||||
{{- contest.name -}}
|
{{- contest.name -}}
|
||||||
</a>
|
</a>
|
||||||
<span class="contest-tags">
|
<br>
|
||||||
|
<div class="contest-tags" style="margin-top: 5px;">
|
||||||
{% if not contest.is_visible %}
|
{% if not contest.is_visible %}
|
||||||
<span class="contest-tag contest-tag-hidden">
|
<span class="contest-tag contest-tag-hidden">
|
||||||
<i class="fa fa-eye-slash"></i> {{ _('hidden') }}
|
<i class="fa fa-eye-slash"></i> {{ _('hidden') }}
|
||||||
|
@ -155,12 +160,12 @@
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</span>
|
</div>
|
||||||
{% endspaceless %}
|
{% endspaceless %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro time_left(contest) %}
|
{% macro time_left(contest, padding_top = true) %}
|
||||||
<div class="time time-left">
|
<div class="time time-left {{ 'non-padding-top' if padding_top == false }}">
|
||||||
{% if contest.time_limit %}
|
{% if contest.time_limit %}
|
||||||
{{ contest.start_time|date(_("M j, Y, G:i")) }} -
|
{{ contest.start_time|date(_("M j, Y, G:i")) }} -
|
||||||
{{ contest.end_time|date(_("M j, Y, G:i")) }}
|
{{ contest.end_time|date(_("M j, Y, G:i")) }}
|
||||||
|
@ -186,21 +191,19 @@
|
||||||
|
|
||||||
{% macro contest_join(contest, request) %}
|
{% macro contest_join(contest, request) %}
|
||||||
{% if not request.in_contest %}
|
{% if not request.in_contest %}
|
||||||
<td>
|
{% if request.profile in contest.authors.all() or request.profile in contest.curators.all() or request.profile in contest.testers.all() %}
|
||||||
{% if request.profile in contest.authors.all() or request.profile in contest.curators.all() or request.profile in contest.testers.all() %}
|
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
||||||
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="submit" class="unselectable button full small"
|
||||||
<input type="submit" class="unselectable button full small"
|
value="{{ _('Spectate') }}">
|
||||||
value="{{ _('Spectate') }}">
|
</form>
|
||||||
</form>
|
{% else %}
|
||||||
{% else %}
|
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
||||||
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
{% csrf_token %}
|
||||||
{% csrf_token %}
|
<input type="submit" class="unselectable button full small join-warning"
|
||||||
<input type="submit" class="unselectable button full small join-warning"
|
value="{{ _('Join') }}">
|
||||||
value="{{ _('Join') }}">
|
</form>
|
||||||
</form>
|
{% endif %}
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
@ -235,88 +238,88 @@
|
||||||
{{ _('Active Contests') }}
|
{{ _('Active Contests') }}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="toggled">
|
<div class="toggled">
|
||||||
<table class="contest-list table striped">
|
|
||||||
<thead>
|
{% for participation in active_participations %}
|
||||||
<tr>
|
{% with contest=participation.contest %}
|
||||||
<th style="width:90%">{{ _('Contest') }}</th>
|
<div class="list-contest">
|
||||||
<th>{{ _('Users') }}</th>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
{% if not request.in_contest %}
|
<div class="contest-title"> {{ _('Contests') }} </div>
|
||||||
<th style="width:15%"></th>
|
{{ contest_head(contest) }}
|
||||||
{% endif %}
|
</div>
|
||||||
</tr>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
</thead>
|
<div class="contest-title"> {{ _('Time') }} </div>
|
||||||
<tbody>
|
<div class="contest-block">
|
||||||
{% for participation in active_participations %}
|
{% if contest.start_time %}
|
||||||
{% with contest=participation.contest %}
|
{% if contest.time_limit %}
|
||||||
<tr>
|
<span class="time">
|
||||||
<td>
|
{% trans countdown=participation.end_time|as_countdown %}Window ends in {{countdown}}{% endtrans %}
|
||||||
<div class="contest-block">
|
</span>
|
||||||
{{ contest_head(contest) }}
|
{% elif contest.time_before_end %}
|
||||||
{% if contest.start_time %}
|
<span class="time">{% trans countdown=contest.end_time|as_countdown %}Ends in {{countdown}}{% endtrans %}</span>
|
||||||
<br>
|
{% endif %}
|
||||||
{% if contest.time_limit %}
|
{{ time_left(contest) }}
|
||||||
<span class="time">
|
{% endif %}
|
||||||
{% trans countdown=participation.end_time|as_countdown %}Window ends in {{countdown}}{% endtrans %}
|
</div>
|
||||||
</span>
|
</div>
|
||||||
{% elif contest.time_before_end %}
|
<div class="info-contest">
|
||||||
<span class="time">{% trans countdown=contest.end_time|as_countdown %}Ends in {{countdown}}{% endtrans %}</span>
|
<div class="contest-title"> {{ _('Format') }} </div>
|
||||||
{% endif %}
|
{{ contest.format.name }}
|
||||||
{{ time_left(contest) }}
|
</div>
|
||||||
{% endif %}
|
<div class="info-contest">
|
||||||
</div>
|
<div class="contest-title"> {{ _('Users') }} </div>
|
||||||
</td>
|
{{ user_count(contest, request.user) }}
|
||||||
<td>
|
</div>
|
||||||
{{ user_count(contest, request.user) }}
|
<div class="participate-button">
|
||||||
</td>
|
{{ contest_join(contest, request) }}
|
||||||
{{ contest_join(contest, request) }}
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<h3 class="toggle open contest-group-header">
|
||||||
|
<i class="fa fa-chevron-right fa-fw"></i>
|
||||||
|
{{ _('Ongoing Contests') }}
|
||||||
|
</h3>
|
||||||
{% if current_contests %}
|
{% if current_contests %}
|
||||||
<h3 class="toggle open contest-group-header">
|
|
||||||
<i class="fa fa-chevron-right fa-fw"></i>
|
|
||||||
{{ _('Ongoing Contests') }}
|
|
||||||
</h3>
|
|
||||||
<div id="ongoing-table" class="toggled">
|
<div id="ongoing-table" class="toggled">
|
||||||
<table class="contest-list table striped">
|
{% for contest in current_contests %}
|
||||||
<thead>
|
<div class="list-contest">
|
||||||
<tr>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
<th style="width:90%">{{ _('Contest') }}</th>
|
<div class="contest-title"> {{ _('Contests') }} </div>
|
||||||
<th>{{ _('Users') }}</th>
|
{{ contest_head(contest) }}
|
||||||
{% if not request.in_contest %}
|
</div>
|
||||||
<th style="width:15%"></th>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
{% endif %}
|
<div class="contest-title"> {{ _('Time') }} </div>
|
||||||
</tr>
|
<div class="contest-block">
|
||||||
</thead>
|
{% if contest.start_time %}
|
||||||
<tbody>
|
{% if contest.time_before_end %}
|
||||||
{% for contest in current_contests %}
|
<span class="time">{% trans countdown=contest.end_time|as_countdown %}Ends in {{countdown}}{% endtrans %}</span>
|
||||||
<tr>
|
{% endif %}
|
||||||
<td>
|
{{ time_left(contest) }}
|
||||||
<div class="contest-block">
|
{% endif %}
|
||||||
{{ contest_head(contest) }}
|
</div>
|
||||||
{% if contest.start_time %}
|
</div>
|
||||||
<br>
|
<div class="info-contest">
|
||||||
{% if contest.time_before_end %}
|
<div class="contest-title"> {{ _('Format') }} </div>
|
||||||
<span class="time">{% trans countdown=contest.end_time|as_countdown %}Ends in {{countdown}}{% endtrans %}</span>
|
{{ contest.format.name }}
|
||||||
{% endif %}
|
</div>
|
||||||
{{ time_left(contest) }}
|
<div class="info-contest">
|
||||||
{% endif %}
|
<div class="contest-title"> {{ _('Users') }} </div>
|
||||||
</div>
|
{{ user_count(contest, request.user) }}
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="participate-button">
|
||||||
{{ user_count(contest, request.user) }}
|
{{ contest_join(contest, request) }}
|
||||||
</td>
|
</div>
|
||||||
{{ contest_join(contest, request) }}
|
</div>
|
||||||
</tr>
|
{% endfor %}
|
||||||
{% endfor %}
|
<br>
|
||||||
</tbody>
|
</div>
|
||||||
</table>
|
{% else %}
|
||||||
|
<div class="toggled">
|
||||||
|
<i> {{ _('There is no ongoing contest at this time.') }} </i>
|
||||||
<br>
|
<br>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -327,90 +330,89 @@
|
||||||
</h3>
|
</h3>
|
||||||
{% if future_contests %}
|
{% if future_contests %}
|
||||||
<div class="toggled">
|
<div class="toggled">
|
||||||
<table class="contest-list table striped">
|
{% for contest in future_contests %}
|
||||||
<thead>
|
<div class="list-contest">
|
||||||
<tr>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
<th>{{ _('Contest') }}</th>
|
<div class="contest-title"> {{ _('Contests') }} </div>
|
||||||
</tr>
|
{{ contest_head(contest) }}
|
||||||
</thead>
|
</div>
|
||||||
<tbody>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
{% for contest in future_contests %}
|
<div class="contest-title"> {{ _('Time') }} </div>
|
||||||
<tr>
|
<div class="contest-block">
|
||||||
<td>
|
{% if contest.start_time %}
|
||||||
<div class="contest-block">
|
{% if contest.time_before_start %}
|
||||||
{{ contest_head(contest) }}
|
<span class="time">{{ _('Starting in %(countdown)s.', countdown=contest.start_time|as_countdown) }}</span>
|
||||||
{% if contest.start_time %}
|
{% endif %}
|
||||||
<br>
|
{{ time_left(contest) }}
|
||||||
{% if contest.time_before_start %}
|
{% endif %}
|
||||||
<span class="time">{{ _('Starting in %(countdown)s.', countdown=contest.start_time|as_countdown) }}</span>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
{{ time_left(contest) }}
|
<div class="info-contest">
|
||||||
{% endif %}
|
<div class="contest-title"> {{ _('Format') }} </div>
|
||||||
</div>
|
{{ contest.format.name }}
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<i>{{ _('There are no scheduled contests at this time.') }}</i>
|
<div class="toggled">
|
||||||
<br>
|
<i>{{ _('There is no scheduled contest at this time.') }}</i>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
<h3 class="toggle open contest-group-header">
|
||||||
|
{{ _('Past Contests') }}
|
||||||
|
</h3>
|
||||||
{% if past_contests %}
|
{% if past_contests %}
|
||||||
<h3 class="toggle open contest-group-header">
|
|
||||||
{{ _('Past Contests') }}
|
|
||||||
</h3>
|
|
||||||
{% if page_obj and page_obj.num_pages > 1 %}
|
{% if page_obj and page_obj.num_pages > 1 %}
|
||||||
<div style="margin-bottom: 4px;">
|
<div style="margin-bottom: 10px;">
|
||||||
{% include "list-pages.html" %}
|
{% include "list-pages.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<table class="contest-list table striped">
|
|
||||||
<thead>
|
{% for contest in past_contests %}
|
||||||
<tr>
|
<div class="list-contest">
|
||||||
<th style="width:90%">
|
<div class="info-contest" style="flex: 1.5">
|
||||||
<a class="contest-list-sort" href="{{ sort_links.name }}">{{ _('Contest') }}{{ sort_order.name }}</a>
|
<div class="contest-title"> {{ _('Contests') }} </div>
|
||||||
</th>
|
{{ contest_head(contest) }}
|
||||||
<th>
|
</div>
|
||||||
<a class="contest-list-sort" href="{{ sort_links.user_count }}">{{ _('Users') }}{{ sort_order.user_count }}</a>
|
<div class="info-contest" style="flex: 1.5">
|
||||||
</th>
|
<div class="contest-title"> {{ _('Time') }} </div>
|
||||||
{% if not request.in_contest %}
|
<div class="contest-block">
|
||||||
<th style="width:15%"></th>
|
{{ time_left(contest, false) }}
|
||||||
{% endif %}
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
</thead>
|
<div class="info-contest">
|
||||||
<tbody>
|
<div class="contest-title"> {{ _('Format') }} </div>
|
||||||
{% for contest in past_contests %}
|
{{ contest.format.name }}
|
||||||
<tr>
|
</div>
|
||||||
<td>
|
<div class="info-contest">
|
||||||
<div class="contest-block">
|
<div class="contest-title"> {{ _('Users') }} </div>
|
||||||
{{ contest_head(contest) }}
|
{{ user_count(contest, request.user) }}
|
||||||
{{ time_left(contest) }}
|
</div>
|
||||||
</div>
|
{% if not request.in_contest %}
|
||||||
</td>
|
<div class="participate-button">
|
||||||
<td>
|
<form action="{{ url('contest_join', contest.key) }}" method="post">
|
||||||
{{ user_count(contest, request.user) }}
|
{% csrf_token %}
|
||||||
</td>
|
<input type="submit" class="unselectable button full small"
|
||||||
{% if not request.in_contest %}
|
value="{{ _('Virtual join') }}">
|
||||||
<td><form action="{{ url('contest_join', contest.key) }}" method="post">
|
</form>
|
||||||
{% csrf_token %}
|
</div>
|
||||||
<input type="submit" class="unselectable button full small"
|
{% endif %}
|
||||||
value="{{ _('Virtual join') }}">
|
</div>
|
||||||
</form>
|
{% endfor %}
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
{% if page_obj and page_obj.num_pages > 1 %}
|
{% if page_obj and page_obj.num_pages > 1 %}
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
{% include "list-pages.html" %}
|
{% include "list-pages.html" %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="toggled">
|
||||||
|
<i> {{ _('There is no past contest.') }} </i>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% if current_contests %}
|
{% if current_contests %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3><i class="fa fa-trophy"></i> {{ _('Ongoing contests') }}</h3>
|
<h3 class="bold-text colored-text"><i class="fa fa-trophy"></i> {{ _('Ongoing contests') }}</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
{% for contest in current_contests %}
|
{% for contest in current_contests %}
|
||||||
<div class="contest">
|
<div class="contest">
|
||||||
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
{% if future_contests %}
|
{% if future_contests %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3><i class="fa fa-trophy"></i>{{ _('Upcoming contests') }}</h3>
|
<h3 class="bold-text colored-text"><i class="fa fa-trophy"></i>{{ _('Upcoming contests') }}</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
{% for contest in future_contests %}
|
{% for contest in future_contests %}
|
||||||
<div class="contest">
|
<div class="contest">
|
||||||
|
|
17
templates/email_change/email_change.html
Normal file
17
templates/email_change/email_change.html
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block media %}
|
||||||
|
<style type="text/css">
|
||||||
|
.errorlist {
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<form action="" method="post" class="form-area">{% csrf_token %}
|
||||||
|
<table border="0">{{ form.as_table() }}</table>
|
||||||
|
<hr>
|
||||||
|
<button style="float:right;" type="submit">{{ _('Verify Email') }}</button>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
4
templates/email_change/email_change_failure.html
Normal file
4
templates/email_change/email_change_failure.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<p>{{ _('Invalid reset link.') }}</p>
|
||||||
|
{% endblock %}
|
6
templates/email_change/email_change_pending.html
Normal file
6
templates/email_change/email_change_pending.html
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
{% if request.profile.email_change_pending %}
|
||||||
|
<p>{{ _('An email was sent to') }} {{request.profile.email_change_pending}}. {{_('If you don\'t see it, kindly check your spam folder as well.')}}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
4
templates/email_change/email_change_success.html
Normal file
4
templates/email_change/email_change_success.html
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block body %}
|
||||||
|
<p>{{ _('Your email was sucessfully changed to') }} {{user.email}}</p>
|
||||||
|
{% endblock %}
|
21
templates/general_email.html
Normal file
21
templates/general_email.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background-color: #f7f7f7;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div style="max-width: 600px; margin: 0 auto; padding: 20px; background-color: #ffffff; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);">
|
||||||
|
<div style="text-align: center; margin-bottom: 20px;">
|
||||||
|
<img src="{{ protocol }}://{{ domain }}{{static('icons/logo.png')}}" alt="{{site_name}}" style="max-width: 150px;">
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 20px; text-align: center;">
|
||||||
|
<h2>{{title}}</h2>
|
||||||
|
<p>{{_('Dear')}} {{username}},</p>
|
||||||
|
<p>{{message}}</p>
|
||||||
|
</div>
|
||||||
|
<div style="text-align: center;">
|
||||||
|
<a href="{{ protocol }}://{{ domain }}{{ url_path }}" style="display: inline-block; padding: 10px 20px; background-color: #007bff; color: #ffffff; text-decoration: none; border-radius: 4px;">{{button_text}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -29,6 +29,11 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
|
<form>
|
||||||
|
<label for="name">{{_('Search')}}:</label>
|
||||||
|
<input type="text" name="q" value="{{query}}">
|
||||||
|
</form>
|
||||||
|
<br>
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
.problems-problem {
|
.problems-problem {
|
||||||
width: 40%;
|
max-width: 60vh;
|
||||||
}
|
}
|
||||||
input[type=number] {
|
input[type=number] {
|
||||||
width: 5em;
|
width: 5em;
|
||||||
|
@ -34,11 +34,10 @@
|
||||||
{% block middle_content %}
|
{% block middle_content %}
|
||||||
<form action="" method="post">
|
<form action="" method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% if form.errors %}
|
{% if form.errors or problems_form.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() }}
|
{{_("Please fix below errors")}}
|
||||||
{{ form.errors }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
|
@ -64,7 +63,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
{% for field in problems_form[0] %}
|
{% for field in problems_form[0] %}
|
||||||
{% if not field.is_hidden %}
|
{% if not field.is_hidden %}
|
||||||
<th>
|
<th class="problems-{{field.name}}">
|
||||||
{{field.label}}
|
{{field.label}}
|
||||||
</th>
|
</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
{% if form.errors %}
|
{% if form.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() }}
|
{{ _("Please fix below errors") }}
|
||||||
{{ form.errors }}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% for field in form %}
|
{% for field in form %}
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
{% if show_preview %}
|
{% if show_preview %}
|
||||||
<div id="{{ postfix|safe }}-preview" data-preview-url="{{ preview_url }}" data-textarea-id="wmd-input-{{ postfix }}"
|
<div id="{{ postfix|safe }}-preview" data-preview-url="{{ preview_url }}" data-textarea-id="wmd-input-{{ postfix }}"
|
||||||
data-timeout="{{ preview_timeout or '' }}" class="wmd-panel wmd-preview dmmd-preview {{ extra_classes }}">
|
data-timeout="{{ preview_timeout or '' }}" class="wmd-panel wmd-preview dmmd-preview {{ extra_classes }}">
|
||||||
<div class="dmmd-preview-update"><i class="fa fa-refresh"></i> Update Preview</div>
|
<div class="dmmd-preview-update"><i class="fa fa-refresh"></i> {{_('Update Preview')}}</div>
|
||||||
<div class="dmmd-preview-content content-description"></div>
|
<div class="dmmd-preview-content content-description"></div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="sidebox">
|
<div class="sidebox">
|
||||||
<h3><i class="fa fa-search"></i>{{ _('Problem search') }}</h3>
|
<h3 class="colored-text"><i class="fa fa-search"></i>{{ _('Problem search') }}</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
<form id="filter-form" name="form" action="" method="get">
|
<form id="filter-form" name="form" action="" method="get">
|
||||||
<div>
|
<div>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if organizations %}
|
{% if organizations %}
|
||||||
<div class="filter-form-group">
|
<div class="filter-form-group">
|
||||||
<label for="type"><i>{{ _('Group') }}</i></label>
|
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Group') }}</i></label>
|
||||||
<select id="search-org" name="orgs" multiple>
|
<select id="search-org" name="orgs" multiple>
|
||||||
{% for org in organizations %}
|
{% for org in organizations %}
|
||||||
<option value="{{ org.id }}"{% if org.id in org_query %} selected{% endif %}>
|
<option value="{{ org.id }}"{% if org.id in org_query %} selected{% endif %}>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="filter-form-group">
|
<div class="filter-form-group">
|
||||||
<label for="type"><i>{{ _('Author') }}</i></label>
|
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Author') }}</i></label>
|
||||||
<select id="search-author" name="authors" multiple>
|
<select id="search-author" name="authors" multiple>
|
||||||
{% for author in all_authors %}
|
{% for author in all_authors %}
|
||||||
<option value="{{ author.id }}"{% if author.id in author_query %} selected{% endif %}>
|
<option value="{{ author.id }}"{% if author.id in author_query %} selected{% endif %}>
|
||||||
|
@ -63,8 +63,20 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{% if show_types %}
|
||||||
|
<div class="filter-form-group">
|
||||||
|
<label class="bold-text margin-label" for="type"><i class="non-italics">{{ _('Problem types') }}</i></label>
|
||||||
|
<select id="types" name="type" multiple>
|
||||||
|
{% for type in problem_types %}
|
||||||
|
<option value="{{ type.id }}"{% if type.id in selected_types %} selected{% endif %}>
|
||||||
|
{{ type.full_name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="filter-form-group">
|
<div class="filter-form-group">
|
||||||
<label for="category"><i>{{ _('Category') }}</i></label>
|
<label class="bold-text margin-label" for="category"><i class="non-italics">{{ _('Category') }}</i></label>
|
||||||
<select id="category" name="category">
|
<select id="category" name="category">
|
||||||
{% if category %}
|
{% if category %}
|
||||||
<option value="" selected="selected">{{ _('All') }}</option>
|
<option value="" selected="selected">{{ _('All') }}</option>
|
||||||
|
@ -78,27 +90,15 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{% if show_types %}
|
|
||||||
<div class="filter-form-group">
|
|
||||||
<label for="type"><i>{{ _('Problem types') }}</i></label>
|
|
||||||
<select id="types" name="type" multiple>
|
|
||||||
{% for type in problem_types %}
|
|
||||||
<option value="{{ type.id }}"{% if type.id in selected_types %} selected{% endif %}>
|
|
||||||
{{ type.full_name }}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if point_values %}
|
{% if point_values %}
|
||||||
<div class="form-label">{{ _('Point range') }}</div>
|
<div style="margin-top: 5px;" class="bold-text margin-label" class="form-label">{{ _('Point range') }}</div>
|
||||||
<div id="point-slider"></div>
|
<div id="point-slider"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input id="point-start" type="hidden" name="point_start" {% if point_start and point_start != point_values.min %}value="{{ point_start }}"{% else %}disabled{% endif %}>
|
<input id="point-start" type="hidden" name="point_start" {% if point_start and point_start != point_values.min %}value="{{ point_start }}"{% else %}disabled{% endif %}>
|
||||||
<input id="point-end" type="hidden" name="point_end" {% if point_end and point_end != point_values.max %}value="{{ point_end }}"{% else %}disabled{% endif %}>
|
<input id="point-end" type="hidden" name="point_end" {% if point_end and point_end != point_values.max %}value="{{ point_end }}"{% else %}disabled{% endif %}>
|
||||||
<div class="form-submit-group">
|
<div class="form-submit-group">
|
||||||
<a id="go" class="button small btn-darkred">{{ _('Go') }}</a>
|
<a id="go" class="button small btn-darkGreen">{{ _('Go') }}</a>
|
||||||
<a id="random" class="button small btn-darkred">{{ _('Random') }}</a>
|
<a id="random" class="button small btn-darkGreen">{{ _('Random') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
|
|
||||||
{% if recent_organizations %}
|
{% if recent_organizations %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3><i class="fa fa-users"></i>{{ _('Recent groups') }}</h3>
|
<h3 class="bold-text colored-text"><i class="fa fa-users"></i>{{ _('Recent groups') }}</h3>
|
||||||
<div class="toggled sidebox-content">
|
<div class="toggled sidebox-content">
|
||||||
{% for organization in recent_organizations %}
|
{% for organization in recent_organizations %}
|
||||||
<a href="{{ url('organization_home', organization.organization.pk, organization.organization.slug) }}" class="organization-row" title="{{organization.organization.about}}">
|
<a href="{{ url('organization_home', organization.organization.pk, organization.organization.slug) }}" class="organization-row" title="{{organization.organization.about}}">
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
<b>Thanks for registering on the {{ site.name }}! We're glad to have you.</b>
|
{% set url_path = "/accounts/activate/" + activation_key %}
|
||||||
<br><br>
|
{% set title = _("Account activation") %}
|
||||||
The last step is activating your account. Please activate your {{ SITE_NAME }} account in the next {{ expiration_days }} days.
|
{% set message = _("Thanks for registering! We're glad to have you. The last step is activating your account. Please activate your account in the next %(expiration_days)d days.", expiration_days=expiration_days) %}
|
||||||
<br><br>
|
{% set username = user.get_username() %}
|
||||||
Please click on the following link to activate your account:
|
{% set button_text = _("Activate") %}
|
||||||
<p style="margin-left:1em">
|
{% set domain = site.domain %}
|
||||||
<a href="http://{{ site.domain }}/accounts/activate/{{ activation_key }}/">http://{{ site.domain }}/accounts/activate/{{ activation_key }}</a>
|
{% set protocol = "http" %}
|
||||||
</p>
|
{% include "general_email.html" %}
|
||||||
|
<br>
|
||||||
Alternatively, you can reply to this message to activate your account.
|
{{_("Alternatively, you can reply to this message to activate your account. Your reply must keep the following text intact for this to work:")}}
|
||||||
Your reply must keep the following text intact for this to work:
|
|
||||||
|
|
||||||
<pre style="margin-left:1em">
|
<pre style="margin-left:1em">
|
||||||
{{ activation_key }}
|
{{ activation_key }}
|
||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
{% if SITE_ADMIN_EMAIL %}
|
{{_("See you soon!")}}
|
||||||
See you soon!
|
|
||||||
<br>
|
|
||||||
If you have problems activating your account, feel free to send us an email at <a href="mailto:{{ SITE_ADMIN_EMAIL }}">{{ SITE_ADMIN_EMAIL }}</a>.
|
|
||||||
{% else %}
|
|
||||||
See you soon!
|
|
||||||
{% endif %}
|
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Activate your {{ SITE_NAME }} account
|
{% trans %}Activate your {{ SITE_NAME }} account{% endtrans %}
|
||||||
|
|
|
@ -1,19 +1,6 @@
|
||||||
<span style="display:block;margin: 0 2px;padding: 1em;border: 3px solid #FFDE05;background-color: #000;border-radius: 6px;font-size: .95em;color: #444;margin-bottom:0.75em">
|
{% set url_path = url('password_reset_confirm', uidb64=uid, token=token) %}
|
||||||
<div style="display:table;margin-left:2em"><img src="https://avatars1.githubusercontent.com/u/6934864?v=3&s=101" style="display:inline;vertical-align: middle">
|
{% set title = _("Password Reset") %}
|
||||||
<h1 style="font-size:4em;display:inline;vertical-align: middle"><a href="//{{ domain }}" style="text-decoration:none;color:gray"><span style="color: #FFDE05">LQD</span>OJ</a>
|
{% set message = _("We have received a request to reset your password. Click the button below to reset your password:") %}
|
||||||
</h1>
|
{% set username = user.get_username() %}
|
||||||
</div>
|
{% set button_text = _("Reset Password") %}
|
||||||
</span>
|
{% include "general_email.html" %}
|
||||||
<div style="display:block;margin: 0 2px;padding: 1em;border: 3px solid #2980B9;background-color: #f8f8f8;border-radius: 6px;font-size: .95em;color: #444;">
|
|
||||||
|
|
||||||
<b>Forgot your password on the {{ site_name }}? Don't worry!</b><br><br>
|
|
||||||
To reset the password for your account "{{ user.get_username() }}", click the below button.
|
|
||||||
<p align="center">
|
|
||||||
<a href="{{ protocol }}://{{ domain }}{{ url('password_reset_confirm', uidb64=uid, token=token) }}" style="cursor: pointer;display:block;text-align: center;padding: 4px 2px 5px;color: white;border: 1px solid #666;border-radius: 1px;background: #2980b9;background: linear-gradient(180deg, #00aee0, #2980b9);text-decoration: none;line-height:2em;font-size:1emm;width:12em;">Reset password</a>
|
|
||||||
</p>
|
|
||||||
{% if SITE_ADMIN_EMAIL %}
|
|
||||||
See you soon! If you have problems resetting your email, feel free to shoot us an email at <a href="mailto:{{ SITE_ADMIN_EMAIL }}">{{ SITE_ADMIN_EMAIL }}</a>
|
|
||||||
{% else %}
|
|
||||||
See you soon!
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
|
@ -1,4 +1,4 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<p>{{ _('You have successfully been registered. An email has been sent to the email address you provided to confirm your registration.') }}</p>
|
<p>{{ _('You have successfully been registered. An email has been sent to the email address you provided to confirm your registration. If you don\'t see it, kindly check your spam folder as well.') }}</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -78,12 +78,6 @@
|
||||||
<div class="block-header">{{ _('Default language') }}</div>
|
<div class="block-header">{{ _('Default language') }}</div>
|
||||||
<span class="fullwidth">{{ form.language }}</span>
|
<span class="fullwidth">{{ form.language }}</span>
|
||||||
|
|
||||||
<div class="block-header">{{ _('Affiliated organizations') }}</div>
|
|
||||||
{{ form.organizations }}
|
|
||||||
{% if form.organizations.errors %}
|
|
||||||
<div class="form-field-error">{{ form.organizations.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if form.captcha %}
|
{% if form.captcha %}
|
||||||
<div style="margin-top: 0.5em">{{ form.captcha }}</div>
|
<div style="margin-top: 0.5em">{{ form.captcha }}</div>
|
||||||
{% if form.captcha.errors %}
|
{% if form.captcha.errors %}
|
||||||
|
@ -92,12 +86,6 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
{% if tos_url %}
|
|
||||||
<span class="tos-section">
|
|
||||||
{{ _('By registering, you agree to our') }}
|
|
||||||
<a href="{{ tos_url }}">{{ _('Terms & Conditions') }}</a>.
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
<button style="float:right;" type="submit">{{ _('Register!') }}</button>
|
<button style="float:right;" type="submit">{{ _('Register!') }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -331,11 +331,11 @@
|
||||||
<div class="right-sidebar">
|
<div class="right-sidebar">
|
||||||
<div class="submission">
|
<div class="submission">
|
||||||
<div class="sidebox">
|
<div class="sidebox">
|
||||||
<h3><i class="fa fa-search"></i>{{ _('Filter submissions') }}</h3>
|
<h3 class="colored-text"><i class="fa fa-search"></i>{{ _('Filter submissions') }}</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
<form id="filter-form" name="form" action="" method="get">
|
<form id="filter-form" name="form" action="" method="get">
|
||||||
<div class="filter-form-group">
|
<div class="filter-form-group">
|
||||||
<label for="status"><i>{{ _('Status') }}</i></label>
|
<label class="bold-text margin-label" for="status"><i class="non-italics" >{{ _('Status') }}</i></label>
|
||||||
<select id="status" name="status" multiple>
|
<select id="status" name="status" multiple>
|
||||||
{% for id, name in all_statuses %}
|
{% for id, name in all_statuses %}
|
||||||
<option {% if id in selected_statuses %}selected{% endif %}
|
<option {% if id in selected_statuses %}selected{% endif %}
|
||||||
|
@ -344,7 +344,7 @@
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-form-group">
|
<div class="filter-form-group">
|
||||||
<label for="language"><i>{{ _('Language') }}</i></label>
|
<label class="bold-text margin-label" for="language"><i class="non-italics">{{ _('Language') }}</i></label>
|
||||||
<select id="language" name="language" multiple>
|
<select id="language" name="language" multiple>
|
||||||
{% for code, name in all_languages %}
|
{% for code, name in all_languages %}
|
||||||
<option {% if code in selected_languages %}selected{% endif %}
|
<option {% if code in selected_languages %}selected{% endif %}
|
||||||
|
@ -352,7 +352,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-submit-group">
|
<div class="form-submit-group" >
|
||||||
<a id="go" onclick="form.submit()" class="button small">{{ _('Go') }}</a>
|
<a id="go" onclick="form.submit()" class="button small">{{ _('Go') }}</a>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -360,12 +360,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebox">
|
<div class="sidebox">
|
||||||
<div id="statistics-table">
|
<div id="statistics-table">
|
||||||
<h3><i class="fa fa-pie-chart"></i>{{ _('Statistics') }}</h3>
|
<h3 class="colored-text"><i class="fa fa-pie-chart"></i>{{ _('Statistics') }}</h3>
|
||||||
<div class="sidebox-content">
|
<div class="sidebox-content">
|
||||||
<div id="status-graph">
|
<div id="status-graph">
|
||||||
<canvas width="230" height="170"></canvas>
|
<canvas width="230" height="170"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<div class="total">
|
<div class="total colored-text bold-text">
|
||||||
{{ _('Total:') }} <span id="total-submission-count"></span>
|
{{ _('Total:') }} <span id="total-submission-count"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,3 @@
|
||||||
{% if submission.contest_or_none %}
|
|
||||||
{% set prefix_length = submission.contest_or_none.problem.output_prefix_override %}
|
|
||||||
{% else %}
|
|
||||||
{% set prefix_length = None %}
|
|
||||||
{% endif %}
|
|
||||||
{% set is_pretest = submission.is_pretested %}
|
{% set is_pretest = submission.is_pretested %}
|
||||||
|
|
||||||
{% if submission.status != 'IE' %}
|
{% if submission.status != 'IE' %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% if top_rated %}
|
{% if top_rated %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3><i class="fa fa-trophy"></i>{{ _('Top Rating') }}</h3>
|
<h3 class="bold-text colored-text"><i class="fa fa-trophy"></i>{{ _('Top Rating') }}</h3>
|
||||||
<div class="sidebox-content" style="padding: 0; border: 0">
|
<div class="sidebox-content" style="padding: 0; border: 0">
|
||||||
<table class="table feed-table">
|
<table class="table feed-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
{% if top_scorer %}
|
{% if top_scorer %}
|
||||||
<div class="blog-sidebox sidebox">
|
<div class="blog-sidebox sidebox">
|
||||||
<h3><i class="fa fa-trophy"></i>{{ _('Top Score') }}</h3>
|
<h3 class="bold-text colored-text"><i class="fa fa-trophy"></i>{{ _('Top Score') }}</h3>
|
||||||
<div class="sidebox-content" style="padding: 0; border: 0">
|
<div class="sidebox-content" style="padding: 0; border: 0">
|
||||||
<table class="table feed-table">
|
<table class="table feed-table">
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
|
@ -47,6 +47,10 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-info tr td {
|
||||||
|
padding-bottom: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
@ -91,7 +95,7 @@
|
||||||
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<table class="block-header grayed">
|
<table class="block-header grayed main-info">
|
||||||
<tr>
|
<tr>
|
||||||
<td> {{ _('Fullname') }}: </td>
|
<td> {{ _('Fullname') }}: </td>
|
||||||
<td> {{ form_user.first_name }} </td>
|
<td> {{ form_user.first_name }} </td>
|
||||||
|
@ -101,8 +105,25 @@
|
||||||
<td> {{ form_user.last_name }} </td>
|
<td> {{ form_user.last_name }} </td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="padding-top: 1em">{{ _('Avatar') }}: </td>
|
<td> {{ _('Password') }}: </td>
|
||||||
<td style="padding-top: 1em">{{ form.profile_image }}</td>
|
<td>
|
||||||
|
<a href="{{ url('password_change') }}">
|
||||||
|
{{ _('Change your password') }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td> {{ _('Email') }}: </td>
|
||||||
|
<td>
|
||||||
|
{{ request.user.email }}
|
||||||
|
<a href="{{ url('email_change') }}">
|
||||||
|
({{ _('Change email') }})
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ _('Avatar') }}: </td>
|
||||||
|
<td>{{ form.profile_image }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -133,32 +154,29 @@
|
||||||
<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="{{ url('password_change') }}" class="inline-header">
|
|
||||||
{{ _('Change your password') }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
</table>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
<br>
|
||||||
<div>
|
<div>
|
||||||
{% if profile.is_totp_enabled %}
|
{% if profile.is_totp_enabled %}
|
||||||
{{ _('Two Factor Authentication is enabled.') }}
|
{{ _('Two Factor Authentication is enabled.') }}
|
||||||
{% if require_staff_2fa and request.user.is_staff %}
|
{% if require_staff_2fa and request.user.is_staff %}
|
||||||
<a id="disable-2fa-button" class="button inline-button">Disable</a>
|
<a id="disable-2fa-button" class="button inline-button">Disable</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url('disable_2fa') }}" class="button inline-button">Disable</a>
|
<a href="{{ url('disable_2fa') }}" class="button inline-button">{{_('Disable')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _('Two Factor Authentication is disabled.') }}
|
{{ _('Two Factor Authentication is disabled.') }}
|
||||||
<a href="{{ url('enable_2fa') }}" class="button inline-button">Enable</a>
|
<a href="{{ url('enable_2fa') }}" class="button inline-button">{{_('Enable')}}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<br><hr>
|
<br><hr>
|
||||||
|
<span class="block-header">{{ _('CSS background') }}:</span>
|
||||||
|
{{ form.css_background }}
|
||||||
|
<div><i>{{form.css_background.help_text}}</i></div>
|
||||||
|
<br><br>
|
||||||
<div class="block-header">{{ _('User-script') }}:</div>
|
<div class="block-header">{{ _('User-script') }}:</div>
|
||||||
{{ form.user_script }}
|
{{ form.user_script }}
|
||||||
<hr>
|
<hr>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue