From 2f8ef1b5243fc0478aa6bffc74f920db013e8dc0 Mon Sep 17 00:00:00 2001 From: cuom1999 Date: Sat, 20 Nov 2021 22:23:03 -0600 Subject: [PATCH] Add direct message --- .../migrations/0005_auto_20211011_0714.py | 28 ++ chat_box/migrations/0006_userroom.py | 24 ++ .../migrations/0007_auto_20211112_1255.py | 18 + chat_box/migrations/0008_ignore.py | 23 + chat_box/models.py | 54 +++ chat_box/utils.py | 18 + chat_box/views.py | 344 +++++++++++++-- dmoj/urls.py | 17 +- judge/migrations/0116_auto_20211011_0645.py | 18 + judge/models/profile.py | 8 + judge/views/select2.py | 34 ++ resources/chatbox.scss | 85 +++- resources/widgets.scss | 10 + templates/chat/chat.html | 397 ++++++++++++++---- templates/chat/chat_css.html | 107 ++--- templates/chat/message.html | 4 +- templates/chat/message_list.html | 8 +- templates/chat/online_status.html | 16 +- templates/chat/user_online_status.html | 35 ++ templates/organization/home.html | 13 +- 20 files changed, 1066 insertions(+), 195 deletions(-) create mode 100644 chat_box/migrations/0005_auto_20211011_0714.py create mode 100644 chat_box/migrations/0006_userroom.py create mode 100644 chat_box/migrations/0007_auto_20211112_1255.py create mode 100644 chat_box/migrations/0008_ignore.py create mode 100644 chat_box/utils.py create mode 100644 judge/migrations/0116_auto_20211011_0645.py create mode 100644 templates/chat/user_online_status.html diff --git a/chat_box/migrations/0005_auto_20211011_0714.py b/chat_box/migrations/0005_auto_20211011_0714.py new file mode 100644 index 0000000..eaf26cb --- /dev/null +++ b/chat_box/migrations/0005_auto_20211011_0714.py @@ -0,0 +1,28 @@ +# Generated by Django 2.2.17 on 2021-10-11 00:14 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0116_auto_20211011_0645'), + ('chat_box', '0004_auto_20200505_2336'), + ] + + operations = [ + migrations.CreateModel( + name='Room', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('user_one', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_one', to='judge.Profile', verbose_name='user 1')), + ('user_two', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_two', to='judge.Profile', verbose_name='user 2')), + ], + ), + migrations.AddField( + model_name='message', + name='room', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id'), + ), + ] diff --git a/chat_box/migrations/0006_userroom.py b/chat_box/migrations/0006_userroom.py new file mode 100644 index 0000000..eff219c --- /dev/null +++ b/chat_box/migrations/0006_userroom.py @@ -0,0 +1,24 @@ +# Generated by Django 2.2.17 on 2021-11-12 05:27 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0116_auto_20211011_0645'), + ('chat_box', '0005_auto_20211011_0714'), + ] + + operations = [ + migrations.CreateModel( + name='UserRoom', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('last_seen', models.DateTimeField(verbose_name='last seen')), + ('room', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='chat_box.Room', verbose_name='room id')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='judge.Profile', verbose_name='user')), + ], + ), + ] diff --git a/chat_box/migrations/0007_auto_20211112_1255.py b/chat_box/migrations/0007_auto_20211112_1255.py new file mode 100644 index 0000000..49a39f0 --- /dev/null +++ b/chat_box/migrations/0007_auto_20211112_1255.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-11-12 05:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('chat_box', '0006_userroom'), + ] + + operations = [ + migrations.AlterField( + model_name='userroom', + name='last_seen', + field=models.DateTimeField(auto_now_add=True, verbose_name='last seen'), + ), + ] diff --git a/chat_box/migrations/0008_ignore.py b/chat_box/migrations/0008_ignore.py new file mode 100644 index 0000000..723dd3b --- /dev/null +++ b/chat_box/migrations/0008_ignore.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.17 on 2021-11-18 10:26 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0116_auto_20211011_0645'), + ('chat_box', '0007_auto_20211112_1255'), + ] + + operations = [ + migrations.CreateModel( + name='Ignore', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ignored_users', models.ManyToManyField(to='judge.Profile')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ignored_chat_users', to='judge.Profile', verbose_name='user')), + ], + ), + ] diff --git a/chat_box/models.py b/chat_box/models.py index b1faedb..f867299 100644 --- a/chat_box/models.py +++ b/chat_box/models.py @@ -8,12 +8,23 @@ from judge.models.profile import Profile __all__ = ['Message'] +class Room(models.Model): + user_one = models.ForeignKey(Profile, related_name="user_one", verbose_name='user 1', on_delete=CASCADE) + user_two = models.ForeignKey(Profile, related_name="user_two", verbose_name='user 2', on_delete=CASCADE) + + def contain(self, profile): + return self.user_one == profile or self.user_two == profile + def other_user(self, profile): + return self.user_one if profile == self.user_two else self.user_two + def users(self): + return [self.user_one, self.user_two] class Message(models.Model): author = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE) time = models.DateTimeField(verbose_name=_('posted time'), auto_now_add=True) body = models.TextField(verbose_name=_('body of comment'), max_length=8192) hidden = models.BooleanField(verbose_name='is hidden', default=False) + room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True) def save(self, *args, **kwargs): new_message = self.id @@ -25,3 +36,46 @@ class Message(models.Model): verbose_name = 'message' verbose_name_plural = 'messages' ordering = ('-time',) + +class UserRoom(models.Model): + user = models.ForeignKey(Profile, verbose_name=_('user'), on_delete=CASCADE) + room = models.ForeignKey(Room, verbose_name='room id', on_delete=CASCADE, default=None, null=True) + last_seen = models.DateTimeField(verbose_name=_('last seen'), auto_now_add=True) + + +class Ignore(models.Model): + user = models.ForeignKey(Profile, related_name="ignored_chat_users", verbose_name=_('user'), on_delete=CASCADE) + ignored_users = models.ManyToManyField(Profile) + + @classmethod + def is_ignored(self, current_user, new_friend): + try: + return current_user.ignored_chat_users.get().ignored_users \ + .filter(id=new_friend.id).exists() + except: + return False + + @classmethod + def get_ignored_users(self, user): + return self.objects.get(user=user).ignored_users.all() + + @classmethod + def add_ignore(self, current_user, friend): + ignore, created = self.objects.get_or_create( + user = current_user + ) + ignore.ignored_users.add(friend) + + @classmethod + def remove_ignore(self, current_user, friend): + ignore, created = self.objects.get_or_create( + user = current_user + ) + ignore.ignored_users.remove(friend) + + @classmethod + def toggle_ignore(self, current_user, friend): + if (self.is_ignored(current_user, friend)): + self.remove_ignore(current_user, friend) + else: + self.add_ignore(current_user, friend) \ No newline at end of file diff --git a/chat_box/utils.py b/chat_box/utils.py new file mode 100644 index 0000000..5ebb16e --- /dev/null +++ b/chat_box/utils.py @@ -0,0 +1,18 @@ +from cryptography.fernet import Fernet + +from django.conf import settings + +secret_key = settings.CHAT_SECRET_KEY +fernet = Fernet(secret_key) + +def encrypt_url(creator_id, other_id): + message = str(creator_id) + '_' + str(other_id) + return fernet.encrypt(message.encode()).decode() + +def decrypt_url(message_encrypted): + try: + dec_message = fernet.decrypt(message_encrypted.encode()).decode() + creator_id, other_id = dec_message.split('_') + return int(creator_id), int(other_id) + except Exception as e: + return None, None \ No newline at end of file diff --git a/chat_box/views.py b/chat_box/views.py index 7ebfa24..f553a46 100644 --- a/chat_box/views.py +++ b/chat_box/views.py @@ -1,42 +1,72 @@ from django.utils.translation import gettext as _ from django.views.generic import ListView -from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest +from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, HttpResponsePermanentRedirect, HttpResponseRedirect from django.core.paginator import Paginator from django.core.exceptions import PermissionDenied from django.shortcuts import render from django.forms.models import model_to_dict -from django.db.models import Case, BooleanField +from django.db.models import Case, BooleanField, When, Q, Subquery, OuterRef, Exists, Count, IntegerField +from django.db.models.functions import Coalesce from django.utils import timezone from django.contrib.auth.decorators import login_required +from django.urls import reverse import datetime from judge import event_poster as event from judge.jinja2.gravatar import gravatar -from .models import Message, Profile +from judge.models import Friend + +from chat_box.models import Message, Profile, Room, UserRoom, Ignore +from chat_box.utils import encrypt_url, decrypt_url + import json - + class ChatView(ListView): context_object_name = 'message' template_name = 'chat/chat.html' title = _('Chat Box') - paginate_by = 50 - messages = Message.objects.filter(hidden=False) - paginator = Paginator(messages, paginate_by) + + def __init__(self): + super().__init__() + self.room_id = None + self.room = None + self.paginate_by = 50 + self.messages = None + self.paginator = None def get_queryset(self): return self.messages def get(self, request, *args, **kwargs): + request_room = kwargs['room_id'] page = request.GET.get('page') + + if request_room: + try: + self.room = Room.objects.get(id=request_room) + if not can_access_room(request, self.room): + return HttpResponseBadRequest() + except Room.DoesNotExist: + return HttpResponseBadRequest() + else: + request_room = None + + if request_room != self.room_id or not self.messages: + self.room_id = request_room + self.messages = Message.objects.filter(hidden=False, room=self.room_id) + self.paginator = Paginator(self.messages, self.paginate_by) + if page == None: + update_last_seen(request, **kwargs) return super().get(request, *args, **kwargs) cur_page = self.paginator.get_page(page) return render(request, 'chat/message_list.html', { 'object_list': cur_page.object_list, + 'num_pages': self.paginator.num_pages }) def get_context_data(self, **kwargs): @@ -45,7 +75,22 @@ class ChatView(ListView): context['title'] = self.title context['last_msg'] = event.last() context['status_sections'] = get_status_context(self.request) - context['today'] = timezone.now().strftime("%d-%m-%Y") + context['room'] = self.room_id + context['unread_count_lobby'] = get_unread_count(None, self.request.profile) + if self.room: + users_room = [self.room.user_one, self.room.user_two] + users_room.remove(self.request.profile) + context['other_user'] = users_room[0] + context['other_online'] = get_user_online_status(context['other_user']) + context['is_ignored'] = Ignore.is_ignored(self.request.profile, context['other_user']) + else: + context['online_count'] = get_online_count() + context['message_template'] = { + 'author': self.request.profile, + 'id': '$id', + 'time': timezone.now(), + 'body': '$body' + } return context @@ -73,21 +118,46 @@ def delete_message(request): @login_required def post_message(request): ret = {'msg': 'posted'} + if request.method != 'POST': + return HttpResponseBadRequest() - if request.method == 'GET': - return JsonResponse(ret) + room = None + if request.POST['room']: + room = Room.objects.get(id=request.POST['room']) + + if not can_access_room(request, room) or request.profile.mute: + return HttpResponseBadRequest() new_message = Message(author=request.profile, - body=request.POST['body']) + body=request.POST['body'], + room=room) new_message.save() - event.post('chat', { - 'type': 'new_message', - 'message': new_message.id, - }) - + if not room: + event.post('chat_lobby', { + 'type': 'lobby', + 'author_id': request.profile.id, + 'message': new_message.id, + 'room': 'None', + 'tmp_id': request.POST.get('tmp_id') + }) + else: + for user in room.users(): + event.post('chat_' + str(user.id), { + 'type': 'private', + 'author_id': request.profile.id, + 'message': new_message.id, + 'room': room.id, + 'tmp_id': request.POST.get('tmp_id') + }) + return JsonResponse(ret) + +def can_access_room(request, room): + return not room or room.user_one == request.profile or room.user_two == request.profile + + @login_required def chat_message_ajax(request): if request.method != 'GET': @@ -100,6 +170,9 @@ def chat_message_ajax(request): try: message = Message.objects.filter(hidden=False).get(id=message_id) + room = message.room + if room and not room.contain(request.profile): + return HttpResponse('Unauthorized', status=401) except Message.DoesNotExist: return HttpResponseBadRequest() return render(request, 'chat/message.html', { @@ -107,53 +180,156 @@ def chat_message_ajax(request): }) -def get_user_online_status(): - last_five_minutes = timezone.now()-timezone.timedelta(minutes=5) - return Profile.objects \ - .filter(display_rank='user', - last_access__gte = last_five_minutes)\ - .annotate(is_online=Case(default=True,output_field=BooleanField()))\ - .order_by('-rating') +@login_required +def update_last_seen(request, **kwargs): + if 'room_id' in kwargs: + room_id = kwargs['room_id'] + elif request.method == 'GET': + room_id = request.GET.get('room') + elif request.method == 'POST': + room_id = request.POST.get('room') + else: + return HttpResponseBadRequest() + + try: + profile = request.profile + room = None + if room_id: + room = Room.objects.get(id=int(room_id)) + except Room.DoesNotExist: + return HttpResponseBadRequest() + except Exception as e: + return HttpResponseBadRequest() + + if room and not room.contain(profile): + return HttpResponseBadRequest() + + user_room, _ = UserRoom.objects.get_or_create(user=profile, room=room) + user_room.last_seen = timezone.now() + user_room.save() + + return JsonResponse({'msg': 'updated'}) -def get_admin_online_status(): - all_admin = Profile.objects.filter(display_rank='admin') - last_five_minutes = timezone.now()-timezone.timedelta(minutes=5) +def get_online_count(): + last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) + return Profile.objects.filter(last_access__gte=last_two_minutes).count() + + +def get_user_online_status(user): + time_diff = timezone.now() - user.last_access + is_online = time_diff <= timezone.timedelta(minutes=2) + return is_online + + +def user_online_status_ajax(request): + if request.method != 'GET': + return HttpResponseBadRequest() + + user_id = request.GET.get('user') + + if user_id: + try: + user_id = int(user_id) + user = Profile.objects.get(id=user_id) + except Exception as e: + return HttpResponseBadRequest() + + is_online = get_user_online_status(user) + return render(request, 'chat/user_online_status.html', { + 'other_user': user, + 'other_online': is_online, + 'is_ignored': Ignore.is_ignored(request.profile, user) + }) + else: + return render(request, 'chat/user_online_status.html', { + 'online_count': get_online_count(), + }) + + +def get_online_status(request_user, queryset, rooms=None): + if not queryset: + return None + last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) ret = [] - for admin in all_admin: + if rooms: + unread_count = get_unread_count(rooms, request_user) + count = {} + for i in unread_count: + count[i['other_user']] = i['unread_count'] + + for user in queryset: is_online = False - if (admin.last_access >= last_five_minutes): + if (user.last_access >= last_two_minutes): is_online = True - ret.append({'user': admin, 'is_online': is_online}) - + user_dict = {'user': user, 'is_online': is_online} + if rooms and user.id in count: + user_dict['unread_count'] = count[user.id] + user_dict['url'] = encrypt_url(request_user.id, user.id) + ret.append(user_dict) return ret -def get_status_context(request): - friend_list = request.profile.get_friends() - all_user_status = get_user_online_status() - friend_status = [] - user_status = [] +def get_status_context(request, include_ignored=False): + if include_ignored: + ignored_users = Profile.objects.none() + queryset = Profile.objects + else: + ignored_users = Ignore.get_ignored_users(request.profile) + queryset = Profile.objects.exclude(id__in=ignored_users) - for user in all_user_status: - if user.username in friend_list: - friend_status.append(user) - else: - user_status.append(user) + last_two_minutes = timezone.now()-timezone.timedelta(minutes=2) + recent_profile = Room.objects.filter( + Q(user_one=request.profile) | Q(user_two=request.profile) + ).annotate( + last_msg_time=Subquery( + Message.objects.filter(room=OuterRef('pk')).values('time')[:1] + ), + other_user=Case( + When(user_one=request.profile, then='user_two'), + default='user_one', + ) + ).filter(last_msg_time__isnull=False)\ + .exclude(other_user__in=ignored_users)\ + .order_by('-last_msg_time').values('other_user', 'id')[:20] + recent_profile_id = [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_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 = queryset.filter(display_rank='admin')\ + .exclude(id__in=friend_list).exclude(id__in=recent_profile_id) + all_user_status = queryset\ + .filter(display_rank='user', + last_access__gte = last_two_minutes)\ + .annotate(is_online=Case(default=True,output_field=BooleanField()))\ + .order_by('-rating').exclude(id__in=friend_list).exclude(id__in=admin_list)\ + .exclude(id__in=recent_profile_id)[:30] + return [ { - 'title': 'Admins', - 'user_list': get_admin_online_status(), + 'title': 'Recent', + 'user_list': get_online_status(request.profile, recent_list, recent_rooms), }, { 'title': 'Following', - 'user_list': friend_status, + 'user_list': get_online_status(request.profile, friend_list), }, { - 'title': 'Users', - 'user_list': user_status, + 'title': 'Admins', + 'user_list': get_online_status(request.profile, admin_list), + }, + { + 'title': 'Other', + 'user_list': get_online_status(request.profile, all_user_status), }, ] @@ -162,4 +338,80 @@ def get_status_context(request): def online_status_ajax(request): return render(request, 'chat/online_status.html', { 'status_sections': get_status_context(request), - }) \ No newline at end of file + 'unread_count_lobby': get_unread_count(None, request.profile), + }) + + +@login_required +def get_room(user_one, user_two): + if user_one.id > user_two.id: + user_one, user_two = user_two, user_one + room, created = Room.objects.get_or_create(user_one=user_one, user_two=user_two) + return room + + +@login_required +def get_or_create_room(request): + decrypted_other_id = request.GET.get('other') + request_id, other_id = decrypt_url(decrypted_other_id) + + if not other_id or not request_id or request_id != request.profile.id: + return HttpResponseBadRequest() + + try: + other_user = Profile.objects.get(id=int(other_id)) + except Exception: + return HttpResponseBadRequest() + + user = request.profile + + if not other_user or not user: + return HttpResponseBadRequest() + # TODO: each user can only create <= 300 rooms + room = get_room(other_user, user) + return JsonResponse({'room': room.id, 'other_user_id': other_user.id}) + + +def get_unread_count(rooms, user): + if rooms: + mess = Message.objects.filter(room=OuterRef('room'), + time__gte=OuterRef('last_seen'))\ + .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 + mess = Message.objects.filter(room__isnull=True, + time__gte=OuterRef('last_seen'))\ + .exclude(author=user)\ + .annotate(unread_count=Count('pk')).values('unread_count') + + return UserRoom.objects\ + .filter(user=user, room__isnull=True)\ + .annotate( + unread_count=Coalesce(Subquery(mess, output_field=IntegerField()), 0), + ).values_list('unread_count', flat=True)[0] + + +@login_required +def toggle_ignore(request, **kwargs): + user_id = kwargs['user_id'] + if not user_id: + return HttpResponseBadRequest() + try: + other_user = Profile.objects.get(id=user_id) + except: + return HttpResponseBadRequest() + + Ignore.toggle_ignore(request.profile, other_user) + next_url = request.GET.get('next', '/') + return HttpResponseRedirect(next_url) \ No newline at end of file diff --git a/dmoj/urls.py b/dmoj/urls.py index c5b8875..b063e0d 100644 --- a/dmoj/urls.py +++ b/dmoj/urls.py @@ -1,4 +1,4 @@ -from chat_box.views import ChatView, delete_message, post_message, chat_message_ajax, online_status_ajax +from chat_box.views import * from django.conf import settings from django.conf.urls import include, url @@ -24,8 +24,8 @@ from judge.views import TitledTemplateView, about, api, blog, comment, contests, from judge.views.problem_data import ProblemDataView, ProblemSubmissionDiff, \ problem_data_file, problem_init_view from judge.views.register import ActivationView, RegistrationView -from judge.views.select2 import AssigneeSelect2View, CommentSelect2View, ContestSelect2View, \ - ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ +from judge.views.select2 import AssigneeSelect2View, ChatUserSearchSelect2View, CommentSelect2View, \ + ContestSelect2View, ContestUserSearchSelect2View, OrganizationSelect2View, ProblemSelect2View, TicketUserSelect2View, \ UserSearchSelect2View, UserSelect2View admin.autodiscover() @@ -288,6 +288,7 @@ urlpatterns = [ url(r'^select2/', include([ url(r'^user_search$', UserSearchSelect2View.as_view(), name='user_search_select2_ajax'), + url(r'^user_search_chat$', ChatUserSearchSelect2View.as_view(), name='chat_user_search_select2_ajax'), url(r'^contest_users/(?P\w+)$', ContestUserSearchSelect2View.as_view(), name='contest_user_search_select2_ajax'), url(r'^ticket_user$', TicketUserSelect2View.as_view(), name='ticket_user_select2_ajax'), @@ -373,13 +374,15 @@ urlpatterns = [ url(r'^custom_checker_sample/', about.custom_checker_sample, name='custom_checker_sample'), url(r'^chat/', include([ - url(r'^$', - login_required(ChatView.as_view()), - name='chat'), + url(r'^(?P\d*)$', login_required(ChatView.as_view()), name='chat'), url(r'^delete/$', delete_message, name='delete_chat_message'), url(r'^post/$', post_message, name='post_chat_message'), url(r'^ajax$', chat_message_ajax, name='chat_message_ajax'), - url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax') + url(r'^online_status/ajax$', online_status_ajax, name='online_status_ajax'), + url(r'^get_or_create_room$', get_or_create_room, name='get_or_create_room'), + url(r'^update_last_seen$', update_last_seen, name='update_last_seen'), + url(r'^online_status/user/ajax$', user_online_status_ajax, name='user_online_status_ajax'), + url(r'^toggle_ignore/(?P\d+)$', toggle_ignore, name='toggle_ignore'), ])), url(r'^notifications/', diff --git a/judge/migrations/0116_auto_20211011_0645.py b/judge/migrations/0116_auto_20211011_0645.py new file mode 100644 index 0000000..c067a59 --- /dev/null +++ b/judge/migrations/0116_auto_20211011_0645.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2021-10-10 23:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('judge', '0115_auto_20210525_0222'), + ] + + operations = [ + migrations.AlterField( + model_name='contest', + name='format_name', + field=models.CharField(choices=[('atcoder', 'AtCoder'), ('default', 'Default'), ('ecoo', 'ECOO'), ('icpc', 'ICPC'), ('ioi', 'IOI')], default='default', help_text='The contest format module to use.', max_length=32, verbose_name='contest format'), + ), + ] diff --git a/judge/models/profile.py b/judge/models/profile.py index d374738..e5e2129 100644 --- a/judge/models/profile.py +++ b/judge/models/profile.py @@ -257,5 +257,13 @@ class Friend(models.Model): else: self.make_friend(current_user, new_friend) + @classmethod + def get_friend_profiles(self, current_user): + try: + ret = self.objects.get(current_user=current_user).users.all() + except Friend.DoesNotExist: + ret = [] + return ret + def __str__(self): return str(self.current_user) diff --git a/judge/views/select2.py b/judge/views/select2.py index 65249a5..1a0bece 100644 --- a/judge/views/select2.py +++ b/judge/views/select2.py @@ -4,6 +4,8 @@ from django.shortcuts import get_object_or_404 from django.utils.encoding import smart_text from django.views.generic.list import BaseListView +from chat_box.utils import encrypt_url + from judge.jinja2.gravatar import gravatar from judge.models import Comment, Contest, Organization, Problem, Profile @@ -121,3 +123,35 @@ class AssigneeSelect2View(UserSearchSelect2View): def get_queryset(self): return Profile.objects.filter(assigned_tickets__isnull=False, user__username__icontains=self.term).distinct() + + +class ChatUserSearchSelect2View(BaseListView): + paginate_by = 20 + + def get_queryset(self): # TODO: add block + return _get_user_queryset(self.term) + + def get(self, request, *args, **kwargs): + self.request = request + self.kwargs = kwargs + self.term = kwargs.get('term', request.GET.get('term', '')) + self.gravatar_size = request.GET.get('gravatar_size', 128) + self.gravatar_default = request.GET.get('gravatar_default', None) + + self.object_list = self.get_queryset().values_list('pk', 'user__username', 'user__email', 'display_rank') + + context = self.get_context_data() + + return JsonResponse({ + 'results': [ + { + 'text': username, + 'id': encrypt_url(request.profile.id, pk), + 'gravatar_url': gravatar(email, self.gravatar_size, self.gravatar_default), + 'display_rank': display_rank, + } for pk, username, email, display_rank in context['object_list']], + 'more': context['page_obj'].has_next(), + }) + + def get_name(self, obj): + return str(obj) \ No newline at end of file diff --git a/resources/chatbox.scss b/resources/chatbox.scss index f1a0ec5..6b31bd1 100644 --- a/resources/chatbox.scss +++ b/resources/chatbox.scss @@ -55,8 +55,7 @@ overflow-y: scroll; border-bottom-left-radius: 0; border-bottom-right-radius: 0; - max-height: 85%; - height: auto; + height: 75%; } #chat-input { @@ -90,6 +89,88 @@ display: block !important; } } +#chat-input, #chat-log .content-message { + font-family: "Segoe UI", "Lucida Grande", Arial, sans-serif; +} +.info-pic { + height: 90%; + border-radius: 50%; + padding: 0.05em; + border: 0.1px solid #ccc; + margin-left: 3em; + margin-bottom: 1.5px; +} +.info-circle { + position: absolute; + cx: 86%; + cy: 80%; + r: 6px; + stroke: white; + stroke-width: 1; +} +.info-name { + margin-left: 10px; + font-size: 2em; + font-weight: bold !important; +} +.info-name a { + display: table-caption; +} +#chat-info { + border-bottom: 2px solid darkgray; + display: flex; +} +#refresh-button { + padding: 0; + margin-left: auto; + margin-right: 0.3em; + background: transparent; + border: none; + height: 1.5em; + width: 1.5em; +} +#refresh-button:hover { + background: lightgreen; + -webkit-transform: rotate(360deg); + transform: rotate(360deg); + transition: 1.5s ease-in-out; +} +.status-pic { + height: 1.3em; + width: 1.3em; + border-radius: 0.3em; +} +.status-container { + position: relative; + display: inline-flex; +} +.status-circle { + position: absolute; + bottom: 0; + right: 0; + cx: 18px; + cy: 18px; + r: 4.5px; + stroke: white; + stroke-width: 1; +} +.status-row { + display: flex; + font-size: 15px; + padding: 0.2em 0.2em 0.2em 1em; + border-radius: 4px; +} +.status-row:hover { + background: lightgray; + cursor: pointer; +} +.status-list { + padding: 0; +} +.status-section-title { + cursor: pointer; + margin-top: 0.5em; +} @media (max-width: 799px) { #chat-area { height: 500px; diff --git a/resources/widgets.scss b/resources/widgets.scss index e239eff..07460eb 100644 --- a/resources/widgets.scss +++ b/resources/widgets.scss @@ -522,4 +522,14 @@ details { background: $background_light_gray; padding: 5px 10px; border-radius: 4px; +} + +.control-button { + background: lightgray; + color: black !important; + border: 0; +} + +.control-button:hover { + background: gray; } \ No newline at end of file diff --git a/templates/chat/chat.html b/templates/chat/chat.html index 017a2bb..804bfda 100644 --- a/templates/chat/chat.html +++ b/templates/chat/chat.html @@ -8,24 +8,53 @@ + @@ -314,23 +555,37 @@
- {% include "chat/online_status.html" %} +
+
+
+ +
+
+
+
+ {% include "chat/online_status.html" %} +
-
- -
    - {% include 'chat/message_list.html' %} -
-
-
- - -
- +
+ {% include 'chat/user_online_status.html' %} +
+
+ + + +
+
+ + +
+
{% endblock body %} diff --git a/templates/chat/chat_css.html b/templates/chat/chat_css.html index eda9ffa..1fd2ad4 100644 --- a/templates/chat/chat_css.html +++ b/templates/chat/chat_css.html @@ -2,53 +2,7 @@ #content { margin: -1em 1em 0 0; } - #refresh-button { - padding: 0; - margin-left: auto; - margin-right: 0.3em; - background: transparent; - border: none; - height: 1.5em; - width: 1.5em; - } - #refresh-button:hover { - background: lightgreen; - -webkit-transform: rotate(360deg); - transform: rotate(360deg); - transition: 1.5s ease-in-out; - } - .status-pic { - height: 1.3em; - width: 1.3em; - border-radius: 0.3em; - } - .status-container { - position: relative; - display: inline-flex; - } - .status-circle { - position: absolute; - bottom: 0; - right: 0; - cx: 18px; - cy: 18px; - r: 4.5px; - stroke: white; - stroke-width: 1; - } - .status-row { - display: flex; - margin-bottom: 0.5em; - font-size: 15px; - } - .status-list { - padding: 0; - padding-left: 1em; - } - .status-section-title { - cursor: pointer; - margin-top: 0.5em; - } + ::-webkit-scrollbar { width: 20px; } @@ -143,14 +97,55 @@ .body-block:hover { background: #eee; } - #chat-input, #chat-log .content-message { - font-family: "Apple Color Emoji", "Segoe UI", "Lucida Grande", Arial, sans-serif; + .active-span { + margin-top: 1em; + margin-right: 1em; + color: #636363; } + + .unread-count { + float: right; + color: white; + background-color: darkcyan; + border-radius: 2px; + padding: 0 0.5em; + } + #search-form { + float: inherit; + } + #search-container { + margin-bottom: 0.4em; + } + #setting { + position: relative; + } + #setting-content { + display: none; + position: absolute; + background-color: #f1f1f1; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; + right: 0; + } + #setting-content li { + padding: 12px 16px; + text-decoration: none; + display: block; + color: black; + font-weight: bold; + } + #setting-content li:hover { + background-color: #ddd; + cursor: pointer; + } + + #page-container { + position:fixed; + overflow:hidden; + } + @media (min-width: 800px) { - #page-container { - position:fixed; - overflow:hidden; - } } @media (max-width: 799px) { html, body { @@ -160,5 +155,11 @@ #mobile ul { width: 100%; } + .info-pic { + margin-left: 0.5em; + } + .active-span { + display: none; + } } diff --git a/templates/chat/message.html b/templates/chat/message.html index 5e1dd65..0eec3cb 100644 --- a/templates/chat/message.html +++ b/templates/chat/message.html @@ -6,7 +6,7 @@ -
+
{% if request.user.is_staff %} {{_('Delete')}} diff --git a/templates/chat/message_list.html b/templates/chat/message_list.html index 1f363a8..cf6c823 100644 --- a/templates/chat/message_list.html +++ b/templates/chat/message_list.html @@ -1,8 +1,12 @@ +{% if object_list %} + {% for message in object_list | reverse%} {% include "chat/message.html" %} {% endfor %} - +{% else %} +
{{_('You are connect now. Say something to start the conversation.')}}
+{% endif %} {% if REQUIRE_JAX %} {% include "mathjax-load.html" %} {% endif %} -{% include "comments/math.html" %} +{% include "comments/math.html" %} \ No newline at end of file diff --git a/templates/chat/online_status.html b/templates/chat/online_status.html index 7e6d321..26713d4 100644 --- a/templates/chat/online_status.html +++ b/templates/chat/online_status.html @@ -1,3 +1,14 @@ +
  • +
    + +
    + + {{_('Lobby')}} + + + {{unread_count_lobby if unread_count_lobby}} + +
  • {% for section in status_sections %} {% if section.user_list %}
    @@ -8,7 +19,7 @@
      {% for user in section.user_list %} -
    • +
    • @@ -19,6 +30,9 @@ {{ link_user(user.user) }} + + {{user.unread_count if user.unread_count}} +
    • {% endfor %}
    diff --git a/templates/chat/user_online_status.html b/templates/chat/user_online_status.html new file mode 100644 index 0000000..8118a4d --- /dev/null +++ b/templates/chat/user_online_status.html @@ -0,0 +1,35 @@ +{% if other_user %} +
    + + + + +
    +{% endif %} + + {% if other_user %} +
    {{other_user.user.username}} + {% else%} + {{ _('Lobby') }} + {% endif %} + + +{% if other_user and not other_online %} + {{ relative_time(other_user.last_access, abs=_('Last online on {time}'), rel=_('Online {time}'), format=_('g:i a d/m/Y')) }} +{% endif %} + +{% if other_user %} + + + + +{% else %} +{{online_count}} {{_('users are online')}} +{% endif %} \ No newline at end of file diff --git a/templates/organization/home.html b/templates/organization/home.html index 0a5c31e..55533b6 100644 --- a/templates/organization/home.html +++ b/templates/organization/home.html @@ -2,15 +2,6 @@ {% block media %}