From ace6faef1791e37ed187b483938eacf27d1fe120 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 18:02:23 +0900 Subject: [PATCH 001/552] chore: update .gitignore --- .gitignore | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 82f9275..2f6a482 100644 --- a/.gitignore +++ b/.gitignore @@ -159,4 +159,12 @@ cython_debug/ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ +.idea/ + +# VS Code +.vscode/ +*.code-workspace + +# Django +**/migrations/* +!**/migrations/__init__.py From f67c85b6256b7c5896baa14a92df923563679fb8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 18:07:53 +0900 Subject: [PATCH 002/552] chore: initialize django project --- src/config/__init__.py | 0 src/config/asgi.py | 16 ++++++ src/config/settings.py | 123 +++++++++++++++++++++++++++++++++++++++++ src/config/urls.py | 22 ++++++++ src/config/wsgi.py | 16 ++++++ src/manage.py | 22 ++++++++ 6 files changed, 199 insertions(+) create mode 100644 src/config/__init__.py create mode 100644 src/config/asgi.py create mode 100644 src/config/settings.py create mode 100644 src/config/urls.py create mode 100644 src/config/wsgi.py create mode 100755 src/manage.py diff --git a/src/config/__init__.py b/src/config/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/config/asgi.py b/src/config/asgi.py new file mode 100644 index 0000000..87078af --- /dev/null +++ b/src/config/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for config project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_asgi_application() diff --git a/src/config/settings.py b/src/config/settings.py new file mode 100644 index 0000000..4bf6cd4 --- /dev/null +++ b/src/config/settings.py @@ -0,0 +1,123 @@ +""" +Django settings for config project. + +Generated by 'django-admin startproject' using Django 4.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/src/config/urls.py b/src/config/urls.py new file mode 100644 index 0000000..425e343 --- /dev/null +++ b/src/config/urls.py @@ -0,0 +1,22 @@ +""" +URL configuration for config project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/src/config/wsgi.py b/src/config/wsgi.py new file mode 100644 index 0000000..a9afbb3 --- /dev/null +++ b/src/config/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for config project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + +application = get_wsgi_application() diff --git a/src/manage.py b/src/manage.py new file mode 100755 index 0000000..d28672e --- /dev/null +++ b/src/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() From f26173c279482831580ede4589b552d691a16242 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 18:08:05 +0900 Subject: [PATCH 003/552] chore: add requirements.txt --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..75207a9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +django +django-cors-headers +djangorestframework From 3dd149920502c3c51c11f62e14d2c5f179e27c76 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 18:10:48 +0900 Subject: [PATCH 004/552] feat(user): initialize app "user" --- src/user/__init__.py | 0 src/user/apps.py | 6 ++++++ src/user/migrations/__init__.py | 0 src/user/models.py | 3 +++ 4 files changed, 9 insertions(+) create mode 100644 src/user/__init__.py create mode 100644 src/user/apps.py create mode 100644 src/user/migrations/__init__.py create mode 100644 src/user/models.py diff --git a/src/user/__init__.py b/src/user/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/user/apps.py b/src/user/apps.py new file mode 100644 index 0000000..578292c --- /dev/null +++ b/src/user/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "user" diff --git a/src/user/migrations/__init__.py b/src/user/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/user/models.py b/src/user/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/user/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. From 70012f9bc2ab775e3daf614f627ca4a9edf77f76 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 18:27:30 +0900 Subject: [PATCH 005/552] feat(user): create "User" model --- src/user/models.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/user/models.py b/src/user/models.py index 71a8362..916639c 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -1,3 +1,28 @@ +from django.contrib import auth from django.db import models -# Create your models here. + +class User(auth.models.User): + REQUIRED_FIELDS = [ + 'email', + 'username', + 'password', + ] + + email = models.EmailField( + unique=True, + null=False, + blank=False, + ) + image = models.ImageField( + upload_to='user_images/', + null=True + ) + boj_id = models.CharField( + max_length=100, # TODO: 추후 조사 필요 + unique=True, + help_text=( + '백준 아이디를 입력해주세요.' + ), + null=True, + ) From ae82527803741f7b9d02eb02cfdd567f80856d6f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 20:46:16 +0900 Subject: [PATCH 006/552] feat(crew): initialize app "crew" --- src/crew/__init__.py | 0 src/crew/apps.py | 6 ++++++ src/crew/migrations/__init__.py | 0 src/crew/models.py | 3 +++ 4 files changed, 9 insertions(+) create mode 100644 src/crew/__init__.py create mode 100644 src/crew/apps.py create mode 100644 src/crew/migrations/__init__.py create mode 100644 src/crew/models.py diff --git a/src/crew/__init__.py b/src/crew/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/crew/apps.py b/src/crew/apps.py new file mode 100644 index 0000000..1485e3e --- /dev/null +++ b/src/crew/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CrewConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "crew" diff --git a/src/crew/migrations/__init__.py b/src/crew/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/crew/models.py b/src/crew/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/crew/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. From 1b7eed35661e134fd14a0ec5641f0e60bc13701f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 23:46:09 +0900 Subject: [PATCH 007/552] feat(crew): add Crew model and related fields --- src/crew/models.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/src/crew/models.py b/src/crew/models.py index 71a8362..41f1159 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -1,3 +1,45 @@ from django.db import models -# Create your models here. +from user.models import User + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '크루 이름을 입력해주세요. (최대 20자)' + ), + ) + emoji = models.CharField( + max_length=1, + help_text=( + '크루 아이콘을 입력해주세요. (이모지)' + ), + null=True, + blank=True, + ) + captain = models.ForeignKey( + User, + on_delete=models.PROTECT, + related_name='crews', + help_text=( + '크루장을 입력해주세요.' + ), + ) + members = models.ManyToManyField( + User, + related_name='crews', + help_text=( + '크루에 속한 유저들을 입력해주세요.' + ), + ) + notice = models.TextField( + help_text=( + '크루 공지를 입력해주세요.' + ), + null=True, + blank=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) From 1525e208902a0cb85520775ea78f147cd8ecd9f8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 23:46:26 +0900 Subject: [PATCH 008/552] feat(crew): Add CrewOpening model and related fields --- src/crew/models.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/crew/models.py b/src/crew/models.py index 41f1159..9ae746e 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -1,3 +1,5 @@ +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator from django.db import models from user.models import User @@ -43,3 +45,69 @@ class Crew(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + +class CrewOpening(models.Model): + crew = models.OneToOneField( + Crew, + on_delete=models.CASCADE, + related_name='opening', + help_text=( + '크루를 입력해주세요.' + ), + unique=True, + ) + max_member = models.IntegerField( + help_text=( + '크루 최대 인원을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 최대 인원 제한 + ], + ) + tags = models.JSONField( + help_text=( + '태그를 입력해주세요.' + ), + validators=[ + # TODO: 태그 형식 검사 + ], + ) + allowed_languages = models.JSONField( + help_text=( + '허용 언어를 입력해주세요. ', + '언어의 아이디를 입력해주세요.' + ), + validators=[ + # TODO: 언어 아이디 검사 + ], + ) + is_boj_user_only = models.BooleanField( + help_text=( + '백준 아이디 필요 여부를 입력해주세요.' + ), + default=False, + ) + min_boj_tier = models.IntegerField( + help_text=( + '최소 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + validators=[ + MinValueValidator(0), + MaxValueValidator(30), + ], + default=0, + ) + max_boj_tier = models.IntegerField( + help_text=( + '최대 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + validators=[ + MinValueValidator(0), + MaxValueValidator(30), + ], + default=30, + ) From 2ffe9e4d542a9d20a07c9278088b8e3f77781a28 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 23:47:09 +0900 Subject: [PATCH 009/552] feat(crew): Add CrewMember model and related fields --- src/crew/models.py | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/crew/models.py b/src/crew/models.py index 9ae746e..524ec26 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -29,13 +29,6 @@ class Crew(models.Model): '크루장을 입력해주세요.' ), ) - members = models.ManyToManyField( - User, - related_name='crews', - help_text=( - '크루에 속한 유저들을 입력해주세요.' - ), - ) notice = models.TextField( help_text=( '크루 공지를 입력해주세요.' @@ -111,3 +104,36 @@ class CrewOpening(models.Model): ], default=30, ) + + +class CrewMemeber(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='members', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crews', + help_text=( + '유저를 입력해주세요.' + ), + ) + is_approved = models.BooleanField( + help_text=( + '가입 승인 여부를 입력해주세요.' + ), + default=False, + ) + approved_at = models.DateTimeField( + help_text=( + '가입 승인 일자를 입력해주세요.' + ), + null=True, + default=None, + ) + created_at = models.DateTimeField(auto_now_add=True) From 1f3cf9954a8defd390a46bbcd4f25b9ca1223264 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 23:51:10 +0900 Subject: [PATCH 010/552] feat(crew): Add CrewActivity model and related fields --- src/crew/models.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/crew/models.py b/src/crew/models.py index 524ec26..9a36346 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -137,3 +137,25 @@ class CrewMemeber(models.Model): default=None, ) created_at = models.DateTimeField(auto_now_add=True) + + +class CrewActivity(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='activities', + help_text=( + '크루를 입력해주세요.' + ), + ) + start_at = models.DateTimeField( + help_text=( + '활동 시작 일자를 입력해주세요.' + ), + ) + end_at = models.DateTimeField( + help_text=( + '활동 종료 일자를 입력해주세요.' + ), + ) + # TODO: Problem 모델 추가 From 5916504783a0241ac5063577ae12e67b8cac8bc9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 2 Jun 2024 23:52:56 +0900 Subject: [PATCH 011/552] feat(problem): initialize app "problem" --- src/problem/__init__.py | 0 src/problem/apps.py | 6 ++++++ src/problem/migrations/__init__.py | 0 src/problem/models.py | 3 +++ 4 files changed, 9 insertions(+) create mode 100644 src/problem/__init__.py create mode 100644 src/problem/apps.py create mode 100644 src/problem/migrations/__init__.py create mode 100644 src/problem/models.py diff --git a/src/problem/__init__.py b/src/problem/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/problem/apps.py b/src/problem/apps.py new file mode 100644 index 0000000..ab8c85e --- /dev/null +++ b/src/problem/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProblemConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "problem" diff --git a/src/problem/migrations/__init__.py b/src/problem/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/problem/models.py b/src/problem/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/problem/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. From 6976202559cba6cfe4a42fa11fd095a0f08db99c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:09:13 +0900 Subject: [PATCH 012/552] feat(problem): Add Problem model and related fields --- src/problem/models.py | 64 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/src/problem/models.py b/src/problem/models.py index 71a8362..ecc873f 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -1,3 +1,65 @@ from django.db import models -# Create your models here. +from user.models import User + + +class ProblemDifficulty(models.IntegerChoices): + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' + + +class Problem(models.Model): + user = models.ForeignKey( + User, + on_delete=models.SET_NULL, + related_name='problems', + help_text=( + '이 문제를 추가한 사용자를 입력해주세요.' + ), + null=True, + ) + title = models.CharField( + max_length=100, + help_text=( + '문제 이름을 입력해주세요.' + ), + blank=False, + ) + link = models.URLField( + help_text=( + '문제 링크를 입력해주세요. (선택)' + ), + blank=True, + ) + description = models.TextField( + help_text=( + '문제 설명을 입력해주세요.' + ), + blank=False, + ) + input_description = models.TextField( + help_text=( + '문제 입력 설명을 입력해주세요.' + ), + blank=True, + ) + output_description = models.TextField( + help_text=( + '문제 출력 설명을 입력해주세요.' + ), + blank=True, + ) + memory_limit = models.IntegerField( + help_text=( + '문제 메모리 제한을 입력해주세요. (바이트 단위)' + ), + ) + time_limit = models.IntegerField( + help_text=( + '문제 시간 제한을 입력해주세요. (밀리 초 단위)' + ), + default=1000, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) From dffa9072c3c69d60e11d3397355dcd93c6b66521 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:11:53 +0900 Subject: [PATCH 013/552] feat(problem): Add ProblemMeta model and related fields --- src/problem/models.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/problem/models.py b/src/problem/models.py index ecc873f..8e1f4b5 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -63,3 +63,34 @@ class Problem(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + +class ProblemMeta(models.Model): + problem = models.OneToOneField( + Problem, + on_delete=models.CASCADE, + related_name='meta', + help_text=( + '문제를 입력해주세요.' + ), + ) + difficulty = models.IntegerField( + help_text=( + '문제 난이도를 입력해주세요.' + ), + choices=ProblemDifficulty.choices, + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + # TODO: tags 필드 추가 + # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + # TODO: 변경 이력을 저장하는 필드 추가 From 2929be59a8a46c7b1b325148240625b78521c285 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:19:22 +0900 Subject: [PATCH 014/552] feat(boj): initialize app "boj" --- src/boj/__init__.py | 0 src/boj/apps.py | 6 ++++++ src/boj/migrations/__init__.py | 0 src/boj/models.py | 3 +++ 4 files changed, 9 insertions(+) create mode 100644 src/boj/__init__.py create mode 100644 src/boj/apps.py create mode 100644 src/boj/migrations/__init__.py create mode 100644 src/boj/models.py diff --git a/src/boj/__init__.py b/src/boj/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/boj/apps.py b/src/boj/apps.py new file mode 100644 index 0000000..025268a --- /dev/null +++ b/src/boj/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BojConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "boj" diff --git a/src/boj/migrations/__init__.py b/src/boj/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/boj/models.py b/src/boj/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/boj/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. From d83a55c24b82bf8a68b8443bb19bd895c0a987e0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:19:54 +0900 Subject: [PATCH 015/552] feat(boj): Add BOJLevel model and choices --- src/boj/models.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/boj/models.py b/src/boj/models.py index 71a8362..9053d46 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -1,3 +1,35 @@ from django.db import models -# Create your models here. + +class BOJLevel(models.IntegerChoices): + U = 0, 'Unrated' + B5 = 1, '브론즈 5' + B4 = 2, '브론즈 4' + B3 = 3, '브론즈 3' + B2 = 4, '브론즈 2' + B1 = 5, '브론즈 1' + S5 = 6, '실버 5' + S4 = 7, '실버 4' + S3 = 8, '실버 3' + S2 = 9, '실버 2' + S1 = 10, '실버 1' + G5 = 11, '골드 5' + G4 = 12, '골드 4' + G3 = 13, '골드 3' + G2 = 14, '골드 2' + G1 = 15, '골드 1' + P5 = 16, '플래티넘 5' + P4 = 17, '플래티넘 4' + P3 = 18, '플래티넘 3' + P2 = 19, '플래티넘 2' + P1 = 20, '플래티넘 1' + D5 = 21, '다이아몬드 5' + D4 = 22, '다이아몬드 4' + D3 = 23, '다이아몬드 3' + D2 = 24, '다이아몬드 2' + D1 = 25, '다이아몬드 1' + R5 = 26, '루비 5' + R4 = 27, '루비 4' + R3 = 28, '루비 3' + R2 = 29, '루비 2' + R1 = 30, '루비 1' From 1ffccfdab5943bfbcd37aedb558e05014494c87d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:21:12 +0900 Subject: [PATCH 016/552] feat(boj): Add BOJUser model and related fields --- src/boj/models.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/boj/models.py b/src/boj/models.py index 9053d46..29b5cf5 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -1,5 +1,7 @@ from django.db import models +from user.models import User + class BOJLevel(models.IntegerChoices): U = 0, 'Unrated' @@ -33,3 +35,40 @@ class BOJLevel(models.IntegerChoices): R3 = 28, '루비 3' R2 = 29, '루비 2' R1 = 30, '루비 1' + + +class BOJUser(models.Model): + user = models.OneToOneField( + User, + on_delete=models.CASCADE, + related_name='boj_user', + help_text=( + '이 사용자와 연결된 사용자를 입력해주세요.' + ), + ) + boj_id = models.CharField( + max_length=100, # TODO: 추후 최대 아이디 길이 조사 필요 + help_text=( + '백준 아이디를 입력해주세요.' + ), + unique=True, + ) + is_verified = models.BooleanField( + default=False, + help_text=( + '이 사용자가 백준 사용자임을 확인했는지 여부를 입력해주세요.' + ), + ) + level = models.IntegerField( + help_text=( + '백준 레벨을 입력해주세요.' + ), + choices=BOJLevel.choices, + default=BOJLevel.U, + ) + updated_at = models.DateTimeField( + help_text=( + '이 사용자의 정보가 최근에 업데이트된 시간을 입력해주세요.' + ), + auto_now=True, + ) From a57e4cf1d57a003d4eb0a94b612d4d7d242d895f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:21:37 +0900 Subject: [PATCH 017/552] refactor(user): Remove boj_id field from User model --- src/user/models.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/user/models.py b/src/user/models.py index 916639c..991bd95 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -18,11 +18,3 @@ class User(auth.models.User): upload_to='user_images/', null=True ) - boj_id = models.CharField( - max_length=100, # TODO: 추후 조사 필요 - unique=True, - help_text=( - '백준 아이디를 입력해주세요.' - ), - null=True, - ) From 86d1e0dbe67e4e3830e3a01553006f3ee5a67ef1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:28:45 +0900 Subject: [PATCH 018/552] feat(crew): Add CrewActivityProblem model and related fields --- src/crew/models.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/crew/models.py b/src/crew/models.py index 9a36346..f874eb6 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -3,6 +3,7 @@ from django.db import models from user.models import User +from problem.models import Problem class Crew(models.Model): @@ -158,4 +159,32 @@ class CrewActivity(models.Model): '활동 종료 일자를 입력해주세요.' ), ) - # TODO: Problem 모델 추가 + + +class CrewActivityProblem(models.Model): + activity = models.ForeignKey( + CrewActivity, + on_delete=models.CASCADE, + related_name='problems', + help_text=( + '활동을 입력해주세요.' + ), + ) + problem = models.ForeignKey( + Problem, + on_delete=models.PROTECT, + related_name='activities', + help_text=( + '문제를 입력해주세요.' + ), + ) + order = models.IntegerField( + help_text=( + '문제 순서를 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 다른 문제 순서와 겹치지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) From b4fc2a008a0d8945659eca1e827cf229b1efcd20 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:30:18 +0900 Subject: [PATCH 019/552] refactor(crew): Update CrewOpening model with BOJLevel choices --- src/crew/models.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/crew/models.py b/src/crew/models.py index f874eb6..47fed86 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -4,6 +4,7 @@ from user.models import User from problem.models import Problem +from boj.models import BOJLevel class Crew(models.Model): @@ -88,11 +89,8 @@ class CrewOpening(models.Model): '최소 백준 레벨을 입력해주세요. ', '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' ), - validators=[ - MinValueValidator(0), - MaxValueValidator(30), - ], - default=0, + choices=BOJLevel.choices, + default=BOJLevel.U, ) max_boj_tier = models.IntegerField( help_text=( @@ -100,10 +98,10 @@ class CrewOpening(models.Model): '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' ), validators=[ - MinValueValidator(0), - MaxValueValidator(30), + # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 ], - default=30, + choices=BOJLevel.choices, + default=BOJLevel.R1, ) From 3bd5d15e9eae475465b44c3694688198a3ec67e5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:57:48 +0900 Subject: [PATCH 020/552] refactor(crew): Rename CrewOpening model to CrewRecruitment --- src/crew/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crew/models.py b/src/crew/models.py index 47fed86..77a6acc 100644 --- a/src/crew/models.py +++ b/src/crew/models.py @@ -42,7 +42,7 @@ class Crew(models.Model): updated_at = models.DateTimeField(auto_now=True) -class CrewOpening(models.Model): +class CrewRecruitment(models.Model): crew = models.OneToOneField( Crew, on_delete=models.CASCADE, From f35eb5fcf72b2546def2fedaf7916385706deb33 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 00:59:32 +0900 Subject: [PATCH 021/552] refactor(crew): Divide models module into submodules crew and activity --- src/crew/models/__init__.py | 2 + src/crew/models/activity.py | 55 ++++++++++++ src/crew/{models.py => models/crew.py} | 118 +++++++------------------ 3 files changed, 90 insertions(+), 85 deletions(-) create mode 100644 src/crew/models/__init__.py create mode 100644 src/crew/models/activity.py rename src/crew/{models.py => models/crew.py} (72%) diff --git a/src/crew/models/__init__.py b/src/crew/models/__init__.py new file mode 100644 index 0000000..e08f984 --- /dev/null +++ b/src/crew/models/__init__.py @@ -0,0 +1,2 @@ +from crew.models.crew import * +from crew.models.activity import * diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py new file mode 100644 index 0000000..2aaabc2 --- /dev/null +++ b/src/crew/models/activity.py @@ -0,0 +1,55 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from problem.models import Problem +from crew.models.crew import Crew + + +class CrewActivity(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='activities', + help_text=( + '크루를 입력해주세요.' + ), + ) + start_at = models.DateTimeField( + help_text=( + '활동 시작 일자를 입력해주세요.' + ), + ) + end_at = models.DateTimeField( + help_text=( + '활동 종료 일자를 입력해주세요.' + ), + ) + + +class CrewActivityProblem(models.Model): + activity = models.ForeignKey( + CrewActivity, + on_delete=models.CASCADE, + related_name='problems', + help_text=( + '활동을 입력해주세요.' + ), + ) + problem = models.ForeignKey( + Problem, + on_delete=models.PROTECT, + related_name='activities', + help_text=( + '문제를 입력해주세요.' + ), + ) + order = models.IntegerField( + help_text=( + '문제 순서를 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 다른 문제 순서와 겹치지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) diff --git a/src/crew/models.py b/src/crew/models/crew.py similarity index 72% rename from src/crew/models.py rename to src/crew/models/crew.py index 77a6acc..fe25f05 100644 --- a/src/crew/models.py +++ b/src/crew/models/crew.py @@ -1,9 +1,7 @@ -from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models from user.models import User -from problem.models import Problem from boj.models import BOJLevel @@ -42,6 +40,39 @@ class Crew(models.Model): updated_at = models.DateTimeField(auto_now=True) +class CrewMemeber(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='members', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crews', + help_text=( + '유저를 입력해주세요.' + ), + ) + is_approved = models.BooleanField( + help_text=( + '가입 승인 여부를 입력해주세요.' + ), + default=False, + ) + approved_at = models.DateTimeField( + help_text=( + '가입 승인 일자를 입력해주세요.' + ), + null=True, + default=None, + ) + created_at = models.DateTimeField(auto_now_add=True) + + class CrewRecruitment(models.Model): crew = models.OneToOneField( Crew, @@ -103,86 +134,3 @@ class CrewRecruitment(models.Model): choices=BOJLevel.choices, default=BOJLevel.R1, ) - - -class CrewMemeber(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='members', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crews', - help_text=( - '유저를 입력해주세요.' - ), - ) - is_approved = models.BooleanField( - help_text=( - '가입 승인 여부를 입력해주세요.' - ), - default=False, - ) - approved_at = models.DateTimeField( - help_text=( - '가입 승인 일자를 입력해주세요.' - ), - null=True, - default=None, - ) - created_at = models.DateTimeField(auto_now_add=True) - - -class CrewActivity(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='activities', - help_text=( - '크루를 입력해주세요.' - ), - ) - start_at = models.DateTimeField( - help_text=( - '활동 시작 일자를 입력해주세요.' - ), - ) - end_at = models.DateTimeField( - help_text=( - '활동 종료 일자를 입력해주세요.' - ), - ) - - -class CrewActivityProblem(models.Model): - activity = models.ForeignKey( - CrewActivity, - on_delete=models.CASCADE, - related_name='problems', - help_text=( - '활동을 입력해주세요.' - ), - ) - problem = models.ForeignKey( - Problem, - on_delete=models.PROTECT, - related_name='activities', - help_text=( - '문제를 입력해주세요.' - ), - ) - order = models.IntegerField( - help_text=( - '문제 순서를 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 다른 문제 순서와 겹치지 않도록 검사 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) From bbd8df0e1c8c3ef7a03b721a2c848903ab3120f0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 01:11:42 +0900 Subject: [PATCH 022/552] feat(crew): Add CrewActivityProblemSubmission model and related fields --- src/crew/models/activity.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index 2aaabc2..ec5098c 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -1,6 +1,7 @@ from django.core.validators import MinValueValidator from django.db import models +from user.models import User from problem.models import Problem from crew.models.crew import Crew @@ -53,3 +54,35 @@ class CrewActivityProblem(models.Model): ], ) created_at = models.DateTimeField(auto_now_add=True) + + +class CrewActivityProblemSubmission(models.Model): + activity_problem = models.ForeignKey( + CrewActivityProblem, + on_delete=models.CASCADE, + related_name='submissions', + help_text=( + '활동 문제를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='submissions', + help_text=( + '유저를 입력해주세요.' + ), + ) + code = models.TextField( + help_text=( + '유저의 코드를 입력해주세요.' + ), + ) + language = models.CharField( + max_length=20, + help_text=( + '유저의 코드 언어를 입력해주세요.' + ), + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) From d6b0429b28322496f2199976337ab456f9a60e0f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 01:11:57 +0900 Subject: [PATCH 023/552] chore(crew): Add TODOs --- src/crew/models/activity.py | 1 + src/crew/models/crew.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index ec5098c..dbbb82b 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -83,6 +83,7 @@ class CrewActivityProblemSubmission(models.Model): help_text=( '유저의 코드 언어를 입력해주세요.' ), + # TODO: 언어 목록 선택지 추가 ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index fe25f05..02e0dbe 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -57,6 +57,7 @@ class CrewMemeber(models.Model): '유저를 입력해주세요.' ), ) + # TODO: 크루 멤버가 선택 가능한 언어 목록 제한 is_approved = models.BooleanField( help_text=( '가입 승인 여부를 입력해주세요.' @@ -101,6 +102,7 @@ class CrewRecruitment(models.Model): ], ) allowed_languages = models.JSONField( + # TODO: 언어 목록 제한을 리크루팅에만 적용할지, 크루에도 적용할지 결정 help_text=( '허용 언어를 입력해주세요. ', '언어의 아이디를 입력해주세요.' From 723f9cafb5ea75adb684ef2b8e8bb506627785d0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 01:14:38 +0900 Subject: [PATCH 024/552] feat(crew): Add CrewActivityProblemSubmissionComment model and related fields --- src/crew/models/activity.py | 44 +++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index dbbb82b..791cc7b 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -87,3 +87,47 @@ class CrewActivityProblemSubmission(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + +class CrewActivityProblemSubmissionComment(models.Model): + submission = models.ForeignKey( + CrewActivityProblemSubmission, + on_delete=models.CASCADE, + related_name='comments', + help_text=( + '제출을 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='comments', + help_text=( + '유저를 입력해주세요.' + ), + ) + content = models.TextField( + help_text=( + '댓글을 입력해주세요.' + ), + ) + line_number_start = models.IntegerField( + help_text=( + '댓글 시작 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + ], + ) + line_number_end = models.IntegerField( + help_text=( + '댓글 종료 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 시작 라인보다 작지 않도록 검사 + # TODO: 코드 라인 수보다 크지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) From 8801e6bd6e1da34109f14c761ee1b65cc8f1c465 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 01:15:39 +0900 Subject: [PATCH 025/552] feat(core): initialize app "core" --- src/core/__init__.py | 0 src/core/apps.py | 6 ++++++ src/core/migrations/__init__.py | 0 src/core/models.py | 3 +++ 4 files changed, 9 insertions(+) create mode 100644 src/core/__init__.py create mode 100644 src/core/apps.py create mode 100644 src/core/migrations/__init__.py create mode 100644 src/core/models.py diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/apps.py b/src/core/apps.py new file mode 100644 index 0000000..c0ce093 --- /dev/null +++ b/src/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "core" diff --git a/src/core/migrations/__init__.py b/src/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/core/models.py b/src/core/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/src/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. From 410c971befbb682425ec216ff7143c399c5d1359 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 02:25:39 +0900 Subject: [PATCH 026/552] feat(core): Add Difficulty and DSA, Language models --- src/core/models.py | 56 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/core/models.py b/src/core/models.py index 71a8362..e54252c 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -1,3 +1,57 @@ from django.db import models -# Create your models here. + +class Difficulty(models.IntegerChoices): + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' + + +class DSA(models.Model): + """Data Structure & Algorithm""" + key = models.CharField( + max_length=20, + unique=True, + help_text=( + '알고리즘 태그 키를 입력해주세요. (최대 20자)' + ), + ) + name_ko = models.CharField( + max_length=20, + unique=True, + help_text=( + '알고리즘 태그 이름(국문)을 입력해주세요. (최대 20자)' + ), + ) + name_en = models.CharField( + max_length=20, + unique=True, + help_text=( + '알고리즘 태그 이름(영문)을 입력해주세요. (최대 20자)' + ), + ) + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + related_name='children', + help_text=( + '부모 알고리즘 태그를 입력해주세요.' + ), + null=True, + ) + is_group = models.BooleanField( + default=False, + help_text=( + '그룹인지 여부를 입력해주세요.' + ), + ) + + +class Language(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 이름을 입력해주세요. (최대 20자)' + ), + ) From 6104332b0b355d6c5fbc348efedf7bf72ecd8130 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 02:46:55 +0900 Subject: [PATCH 027/552] =?UTF-8?q?refactor(crew):=20Core=EC=9D=98=20DSA,?= =?UTF-8?q?=20Difficulty,=20Language=20=EB=A5=BC=20=EB=8B=A4=EB=A5=B8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EC=97=90=EB=8F=84=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/activity.py | 12 +++++++----- src/crew/models/crew.py | 21 +++++++++------------ src/problem/models.py | 18 ++++++++++-------- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index 791cc7b..d6730e9 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -1,9 +1,10 @@ from django.core.validators import MinValueValidator from django.db import models -from user.models import User -from problem.models import Problem +from core.models import Language from crew.models.crew import Crew +from problem.models import Problem +from user.models import User class CrewActivity(models.Model): @@ -78,12 +79,13 @@ class CrewActivityProblemSubmission(models.Model): '유저의 코드를 입력해주세요.' ), ) - language = models.CharField( - max_length=20, + language = models.ForeignKey( + Language, + on_delete=models.PROTECT, + related_name='submissions', help_text=( '유저의 코드 언어를 입력해주세요.' ), - # TODO: 언어 목록 선택지 추가 ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 02e0dbe..545fd58 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -1,8 +1,9 @@ from django.core.validators import MinValueValidator from django.db import models -from user.models import User from boj.models import BOJLevel +from core.models import Language +from user.models import User class Crew(models.Model): @@ -36,6 +37,13 @@ class Crew(models.Model): null=True, blank=True, ) + languages = models.ManyToManyField( + Language, + related_name='crews', + help_text=( + '유저가 사용 가능한 언어를 입력해주세요.' + ), + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -57,7 +65,6 @@ class CrewMemeber(models.Model): '유저를 입력해주세요.' ), ) - # TODO: 크루 멤버가 선택 가능한 언어 목록 제한 is_approved = models.BooleanField( help_text=( '가입 승인 여부를 입력해주세요.' @@ -101,16 +108,6 @@ class CrewRecruitment(models.Model): # TODO: 태그 형식 검사 ], ) - allowed_languages = models.JSONField( - # TODO: 언어 목록 제한을 리크루팅에만 적용할지, 크루에도 적용할지 결정 - help_text=( - '허용 언어를 입력해주세요. ', - '언어의 아이디를 입력해주세요.' - ), - validators=[ - # TODO: 언어 아이디 검사 - ], - ) is_boj_user_only = models.BooleanField( help_text=( '백준 아이디 필요 여부를 입력해주세요.' diff --git a/src/problem/models.py b/src/problem/models.py index 8e1f4b5..fd2aa88 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -1,14 +1,10 @@ from django.db import models +from core.models import Difficulty +from core.models import DSA from user.models import User -class ProblemDifficulty(models.IntegerChoices): - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' - - class Problem(models.Model): user = models.ForeignKey( User, @@ -78,7 +74,14 @@ class ProblemMeta(models.Model): help_text=( '문제 난이도를 입력해주세요.' ), - choices=ProblemDifficulty.choices, + choices=Difficulty.choices, + ) + dsa_tags = models.ManyToManyField( + DSA, + related_name='problems', + help_text=( + '문제의 DSA 태그를 입력해주세요.' + ), ) time_complexity = models.CharField( max_length=100, @@ -91,6 +94,5 @@ class ProblemMeta(models.Model): ], ) created_at = models.DateTimeField(auto_now_add=True) - # TODO: tags 필드 추가 # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 # TODO: 변경 이력을 저장하는 필드 추가 From 6f5c92181ef1e6890919540b7b17cb1a956dfaaf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 03:04:23 +0900 Subject: [PATCH 028/552] feat(crew): Update CrewMember model and add CrewMemberRequest model --- src/crew/models/crew.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 545fd58..130faad 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -65,18 +65,32 @@ class CrewMemeber(models.Model): '유저를 입력해주세요.' ), ) - is_approved = models.BooleanField( + created_at = models.DateTimeField(auto_now_add=True) + + +class CrewMemberRequest(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='requests', help_text=( - '가입 승인 여부를 입력해주세요.' + '크루를 입력해주세요.' ), - default=False, ) - approved_at = models.DateTimeField( + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crew_requests', + help_text=( + '유저를 입력해주세요.' + ), + ) + message = models.TextField( help_text=( - '가입 승인 일자를 입력해주세요.' + '가입 메시지를 입력해주세요.' ), null=True, - default=None, + blank=True, ) created_at = models.DateTimeField(auto_now_add=True) From 6e5515c94c1abd9537c95233e07d80f175138104 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 03:07:50 +0900 Subject: [PATCH 029/552] refactor(problem): Rename ProblemMeta model to ProblemAnalysis --- src/problem/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/problem/models.py b/src/problem/models.py index fd2aa88..3a8a466 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -61,11 +61,11 @@ class Problem(models.Model): updated_at = models.DateTimeField(auto_now=True) -class ProblemMeta(models.Model): +class ProblemAnalysis(models.Model): problem = models.OneToOneField( Problem, on_delete=models.CASCADE, - related_name='meta', + related_name='analysis', help_text=( '문제를 입력해주세요.' ), From 7dfc59ebb3fd7d6553df5ba9565119c235a5a17f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 03:08:14 +0900 Subject: [PATCH 030/552] =?UTF-8?q?feat(problem):=20=ED=95=9C=20Problem?= =?UTF-8?q?=EC=97=90=20=EC=97=AC=EB=9F=AC=EA=B0=9C=EC=9D=98=20ProblemAnaly?= =?UTF-8?q?sis=EB=A5=BC=20=ED=97=88=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/models.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/problem/models.py b/src/problem/models.py index 3a8a466..66fd52e 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -62,7 +62,7 @@ class Problem(models.Model): class ProblemAnalysis(models.Model): - problem = models.OneToOneField( + problem = models.ForeignKey( Problem, on_delete=models.CASCADE, related_name='analysis', @@ -95,4 +95,3 @@ class ProblemAnalysis(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 - # TODO: 변경 이력을 저장하는 필드 추가 From 7bfc0e6cf2afdb44f2bc2269dbe5765fb288c171 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 03:13:22 +0900 Subject: [PATCH 031/552] =?UTF-8?q?refactor(crew):=20CrewRecruitment=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EC=9D=84=20Crew=EC=97=90=20=EB=B3=91?= =?UTF-8?q?=ED=95=A9=EC=8B=9C=ED=82=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/crew.py | 84 ++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 130faad..8033fe7 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -44,6 +44,42 @@ class Crew(models.Model): '유저가 사용 가능한 언어를 입력해주세요.' ), ) + max_member = models.IntegerField( + help_text=( + '크루 최대 인원을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 최대 인원 제한 + ], + ) + is_boj_user_only = models.BooleanField( + help_text=( + '백준 아이디 필요 여부를 입력해주세요.' + ), + default=False, + ) + min_boj_tier = models.IntegerField( + help_text=( + '최소 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + choices=BOJLevel.choices, + null=True, + default=None, + ) + max_boj_tier = models.IntegerField( + help_text=( + '최대 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + validators=[ + # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 + ], + choices=BOJLevel.choices, + null=True, + default=None, + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -92,28 +128,6 @@ class CrewMemberRequest(models.Model): null=True, blank=True, ) - created_at = models.DateTimeField(auto_now_add=True) - - -class CrewRecruitment(models.Model): - crew = models.OneToOneField( - Crew, - on_delete=models.CASCADE, - related_name='opening', - help_text=( - '크루를 입력해주세요.' - ), - unique=True, - ) - max_member = models.IntegerField( - help_text=( - '크루 최대 인원을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 최대 인원 제한 - ], - ) tags = models.JSONField( help_text=( '태그를 입력해주세요.' @@ -122,28 +136,4 @@ class CrewRecruitment(models.Model): # TODO: 태그 형식 검사 ], ) - is_boj_user_only = models.BooleanField( - help_text=( - '백준 아이디 필요 여부를 입력해주세요.' - ), - default=False, - ) - min_boj_tier = models.IntegerField( - help_text=( - '최소 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - choices=BOJLevel.choices, - default=BOJLevel.U, - ) - max_boj_tier = models.IntegerField( - help_text=( - '최대 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - validators=[ - # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 - ], - choices=BOJLevel.choices, - default=BOJLevel.R1, - ) + created_at = models.DateTimeField(auto_now_add=True) From 729a6ba6a53c410fd19a5ddfbcde155ccbf19c20 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:15:20 +0900 Subject: [PATCH 032/552] =?UTF-8?q?fix(crew):=20reverse=20accessor=20name?= =?UTF-8?q?=EC=9D=B4=20=EA=B2=B9=EC=B9=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/crew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 8033fe7..073b360 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -25,7 +25,7 @@ class Crew(models.Model): captain = models.ForeignKey( User, on_delete=models.PROTECT, - related_name='crews', + related_name='crews_as_captain', help_text=( '크루장을 입력해주세요.' ), From af4a9aee7ce362ff121ca15f8c5db02d045f23b2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:57:03 +0900 Subject: [PATCH 033/552] feat(core): Add boj_tag_id field to DSA model --- src/core/models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/models.py b/src/core/models.py index e54252c..1a7e26a 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -9,6 +9,14 @@ class Difficulty(models.IntegerChoices): class DSA(models.Model): """Data Structure & Algorithm""" + boj_tag_id = models.IntegerField( + unique=True, + help_text=( + '백준 태그 ID를 입력해주세요.' + ), + null=True, + default=None, + ) key = models.CharField( max_length=20, unique=True, From bb5f4b77e7c14912cb3129120f5bae4552d1c0e0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:57:13 +0900 Subject: [PATCH 034/552] refactor(user): Update User model to enforce unique and non-null email field --- src/user/models.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/user/models.py b/src/user/models.py index 991bd95..1122946 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -1,20 +1,19 @@ -from django.contrib import auth +from django.contrib.auth.models import User as DjangoUser from django.db import models -class User(auth.models.User): +class User(DjangoUser): REQUIRED_FIELDS = [ 'email', 'username', 'password', ] - - email = models.EmailField( - unique=True, - null=False, - blank=False, - ) image = models.ImageField( upload_to='user_images/', null=True ) + + +User._meta.get_field('email')._unique = True +User._meta.get_field('email').blank = False +User._meta.get_field('email').null = False From 1de4e792e43f62e97022b4dc675bb3bfcd104308 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:57:30 +0900 Subject: [PATCH 035/552] chore: Add Pillow library to requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 75207a9..3b5a3d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ django django-cors-headers djangorestframework +Pillow From 3f5d7ea3adba8c5ab65ea7751a1837534bffb357 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:57:57 +0900 Subject: [PATCH 036/552] chore: Add script to load tags from JSON file and update DSA model --- tools/db_setup.py | 57 + tools/tags.json | 5944 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 6001 insertions(+) create mode 100644 tools/db_setup.py create mode 100644 tools/tags.json diff --git a/tools/db_setup.py b/tools/db_setup.py new file mode 100644 index 0000000..dcd4bca --- /dev/null +++ b/tools/db_setup.py @@ -0,0 +1,57 @@ +""" +Use this like: + +$ python manage.py shell < tools/db_setup.py +""" + +import dataclasses +import json +from typing import List + + +@dataclasses.dataclass +class DisplayName: + language: str + name: str + short: str + + +@dataclasses.dataclass +class Alias: + alias: str + + +@dataclasses.dataclass +class Tag: + key: str + isMeta: bool + bojTagId: int + problemCount: int + displayNames: List[DisplayName] + aliases: List[Alias] + + + +def load_tags(file='tools/tags.json') -> List[Tag]: + with open(file) as f: + raw_tags = json.load(f) + tags = [] + for item in raw_tags['items']: + tag = Tag(**item) + tag.displayNames = [DisplayName(**display_name) for display_name in item["displayNames"]] + tag.aliases = [Alias(**alias) for alias in item["aliases"]] + tags.append(tag) + return tags + + +from django.db.transaction import atomic +from core.models import DSA + + +with atomic(): + for tag in load_tags(): + dsa = DSA.objects.get_or_create(key=tag.key)[0] + dsa.boj_tag_id = tag.bojTagId + dsa.name_ko = next(filter(lambda x: x.language == 'ko', tag.displayNames)).name + dsa.name_en = next(filter(lambda x: x.language == 'en', tag.displayNames)).name + dsa.save() diff --git a/tools/tags.json b/tools/tags.json new file mode 100644 index 0000000..d21c414 --- /dev/null +++ b/tools/tags.json @@ -0,0 +1,5944 @@ +{ + "count": 206, + "items": [ + { + "key": "math", + "isMeta": false, + "bojTagId": 124, + "problemCount": 6212, + "displayNames": [ + { + "language": "ko", + "name": "수학", + "short": "수학" + }, + { + "language": "en", + "name": "mathematics", + "short": "math" + }, + { + "language": "ja", + "name": "数学", + "short": "数学" + } + ], + "aliases": [] + }, + { + "key": "implementation", + "isMeta": false, + "bojTagId": 102, + "problemCount": 5399, + "displayNames": [ + { + "language": "ko", + "name": "구현", + "short": "구현" + }, + { + "language": "en", + "name": "implementation", + "short": "impl" + }, + { + "language": "ja", + "name": "実装", + "short": "impl" + } + ], + "aliases": [] + }, + { + "key": "dp", + "isMeta": false, + "bojTagId": 25, + "problemCount": 3941, + "displayNames": [ + { + "language": "ko", + "name": "다이나믹 프로그래밍", + "short": "다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming", + "short": "dp" + }, + { + "language": "ja", + "name": "動的計画法", + "short": "dp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "동적 계획법" + }, + { + "alias": "다이나믹프로그래밍" + } + ] + }, + { + "key": "data_structures", + "isMeta": false, + "bojTagId": 175, + "problemCount": 3732, + "displayNames": [ + { + "language": "ko", + "name": "자료 구조", + "short": "자료 구조" + }, + { + "language": "en", + "name": "data structures", + "short": "ds" + }, + { + "language": "ja", + "name": "データ構造", + "short": "ds" + } + ], + "aliases": [ + { + "alias": "자료구조" + }, + { + "alias": "자구" + } + ] + }, + { + "key": "graphs", + "isMeta": false, + "bojTagId": 7, + "problemCount": 3600, + "displayNames": [ + { + "language": "ko", + "name": "그래프 이론", + "short": "그래프 이론" + }, + { + "language": "en", + "name": "graph theory", + "short": "graph" + }, + { + "language": "ja", + "name": "グラフ理論", + "short": "グラフ" + } + ], + "aliases": [ + { + "alias": "그래프이론" + }, + { + "alias": "그래프" + } + ] + }, + { + "key": "greedy", + "isMeta": false, + "bojTagId": 33, + "problemCount": 2461, + "displayNames": [ + { + "language": "ko", + "name": "그리디 알고리즘", + "short": "그리디 알고리즘" + }, + { + "language": "en", + "name": "greedy", + "short": "greedy" + }, + { + "language": "ja", + "name": "貪欲法", + "short": "貪欲法" + } + ], + "aliases": [ + { + "alias": "탐욕법" + } + ] + }, + { + "key": "string", + "isMeta": false, + "bojTagId": 158, + "problemCount": 2340, + "displayNames": [ + { + "language": "ko", + "name": "문자열", + "short": "문자열" + }, + { + "language": "en", + "name": "string", + "short": "string" + }, + { + "language": "ja", + "name": "文字列", + "short": "文字列" + } + ], + "aliases": [ + { + "alias": "스트링" + } + ] + }, + { + "key": "bruteforcing", + "isMeta": false, + "bojTagId": 125, + "problemCount": 2132, + "displayNames": [ + { + "language": "ko", + "name": "브루트포스 알고리즘", + "short": "브루트포스 알고리즘" + }, + { + "language": "en", + "name": "bruteforcing", + "short": "bruteforce" + }, + { + "language": "ja", + "name": "全探索", + "short": "全探索" + } + ], + "aliases": [ + { + "alias": "완전탐색" + }, + { + "alias": "완전 탐색" + }, + { + "alias": "브루트포스" + }, + { + "alias": "bruteforce" + }, + { + "alias": "brute force" + }, + { + "alias": "완탐" + } + ] + }, + { + "key": "graph_traversal", + "isMeta": false, + "bojTagId": 11, + "problemCount": 1960, + "displayNames": [ + { + "language": "ko", + "name": "그래프 탐색", + "short": "그래프 탐색" + }, + { + "language": "en", + "name": "graph traversal", + "short": "traversal" + }, + { + "language": "ja", + "name": "グラフの探索", + "short": "横断" + } + ], + "aliases": [ + { + "alias": "bfs" + }, + { + "alias": "dfs" + } + ] + }, + { + "key": "sorting", + "isMeta": false, + "bojTagId": 97, + "problemCount": 1817, + "displayNames": [ + { + "language": "ko", + "name": "정렬", + "short": "정렬" + }, + { + "language": "en", + "name": "sorting", + "short": "sorting" + }, + { + "language": "ja", + "name": "ソート", + "short": "ソート" + } + ], + "aliases": [] + }, + { + "key": "geometry", + "isMeta": false, + "bojTagId": 100, + "problemCount": 1474, + "displayNames": [ + { + "language": "ko", + "name": "기하학", + "short": "기하학" + }, + { + "language": "en", + "name": "geometry", + "short": "geom" + }, + { + "language": "ja", + "name": "幾何学", + "short": "幾何" + } + ], + "aliases": [] + }, + { + "key": "ad_hoc", + "isMeta": false, + "bojTagId": 109, + "problemCount": 1424, + "displayNames": [ + { + "language": "ko", + "name": "애드 혹", + "short": "애드 혹" + }, + { + "language": "en", + "name": "ad-hoc", + "short": "ad-hoc" + }, + { + "language": "ja", + "name": "アドホック", + "short": "アドホック" + } + ], + "aliases": [] + }, + { + "key": "number_theory", + "isMeta": false, + "bojTagId": 95, + "problemCount": 1399, + "displayNames": [ + { + "language": "ko", + "name": "정수론", + "short": "정수론" + }, + { + "language": "en", + "name": "number theory", + "short": "number theory" + }, + { + "language": "ja", + "name": "整数論", + "short": "整数論" + } + ], + "aliases": [] + }, + { + "key": "trees", + "isMeta": false, + "bojTagId": 120, + "problemCount": 1359, + "displayNames": [ + { + "language": "ko", + "name": "트리", + "short": "트리" + }, + { + "language": "en", + "name": "tree", + "short": "tree" + }, + { + "language": "ja", + "name": "木", + "short": "木" + } + ], + "aliases": [ + { + "alias": "trees" + } + ] + }, + { + "key": "segtree", + "isMeta": false, + "bojTagId": 65, + "problemCount": 1275, + "displayNames": [ + { + "language": "ko", + "name": "세그먼트 트리", + "short": "세그먼트 트리" + }, + { + "language": "en", + "name": "segment tree", + "short": "segtree" + }, + { + "language": "ja", + "name": "セグメント木", + "short": "セグ木" + } + ], + "aliases": [ + { + "alias": "구간트리" + }, + { + "alias": "세그트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "binary_search", + "isMeta": false, + "bojTagId": 12, + "problemCount": 1194, + "displayNames": [ + { + "language": "ko", + "name": "이분 탐색", + "short": "이분 탐색" + }, + { + "language": "en", + "name": "binary search", + "short": "binary search" + }, + { + "language": "ja", + "name": "二分探索", + "short": "二分探索" + } + ], + "aliases": [ + { + "alias": "이분탐색" + }, + { + "alias": "이진탐색" + } + ] + }, + { + "key": "arithmetic", + "isMeta": false, + "bojTagId": 121, + "problemCount": 1093, + "displayNames": [ + { + "language": "ko", + "name": "사칙연산", + "short": "사칙연산" + }, + { + "language": "en", + "name": "arithmetic", + "short": "arithmetic" + }, + { + "language": "ja", + "name": "算数", + "short": "算数" + } + ], + "aliases": [ + { + "alias": "덧셈" + }, + { + "alias": "뺄셈" + }, + { + "alias": "곱셈" + }, + { + "alias": "나눗셈" + }, + { + "alias": "더하기" + }, + { + "alias": "빼기" + }, + { + "alias": "곱하기" + }, + { + "alias": "나누기" + } + ] + }, + { + "key": "simulation", + "isMeta": false, + "bojTagId": 141, + "problemCount": 1054, + "displayNames": [ + { + "language": "ko", + "name": "시뮬레이션", + "short": "시뮬레이션" + }, + { + "language": "en", + "name": "simulation", + "short": "simulation" + }, + { + "language": "ja", + "name": "シミュレーション", + "short": "シミュレーション" + } + ], + "aliases": [] + }, + { + "key": "constructive", + "isMeta": false, + "bojTagId": 128, + "problemCount": 970, + "displayNames": [ + { + "language": "ko", + "name": "해 구성하기", + "short": "해 구성하기" + }, + { + "language": "en", + "name": "constructive", + "short": "constructive" + }, + { + "language": "ja", + "name": "構成的", + "short": "構成的" + } + ], + "aliases": [ + { + "alias": "constructive" + }, + { + "alias": "컨스트럭티브" + }, + { + "alias": "구성적" + } + ] + }, + { + "key": "bfs", + "isMeta": false, + "bojTagId": 126, + "problemCount": 962, + "displayNames": [ + { + "language": "ko", + "name": "너비 우선 탐색", + "short": "너비 우선 탐색" + }, + { + "language": "en", + "name": "breadth-first search", + "short": "bfs" + }, + { + "language": "ja", + "name": "幅優先検索", + "short": "bfs" + } + ], + "aliases": [ + { + "alias": "breadthfirst" + }, + { + "alias": "breadth first" + } + ] + }, + { + "key": "prefix_sum", + "isMeta": false, + "bojTagId": 139, + "problemCount": 925, + "displayNames": [ + { + "language": "ko", + "name": "누적 합", + "short": "누적 합" + }, + { + "language": "en", + "name": "prefix sum", + "short": "prefix sum" + }, + { + "language": "ja", + "name": "累積和", + "short": "累積和" + } + ], + "aliases": [ + { + "alias": "구간합" + }, + { + "alias": "부분합" + }, + { + "alias": "rangesum" + } + ] + }, + { + "key": "combinatorics", + "isMeta": false, + "bojTagId": 6, + "problemCount": 879, + "displayNames": [ + { + "language": "ko", + "name": "조합론", + "short": "조합론" + }, + { + "language": "en", + "name": "combinatorics", + "short": "combinatorics" + }, + { + "language": "ja", + "name": "組み合わせ", + "short": "組み合わせ" + } + ], + "aliases": [ + { + "alias": "combination" + }, + { + "alias": "permutation" + }, + { + "alias": "probability" + }, + { + "alias": "확률" + }, + { + "alias": "순열" + } + ] + }, + { + "key": "case_work", + "isMeta": false, + "bojTagId": 137, + "problemCount": 838, + "displayNames": [ + { + "language": "ko", + "name": "많은 조건 분기", + "short": "많은 조건 분기" + }, + { + "language": "en", + "name": "case work", + "short": "case work" + }, + { + "language": "ja", + "name": "ケースワーク", + "short": "ケースワーク" + } + ], + "aliases": [ + { + "alias": "케이스" + }, + { + "alias": "케이스워크" + }, + { + "alias": "케이스 워크" + } + ] + }, + { + "key": "dfs", + "isMeta": false, + "bojTagId": 127, + "problemCount": 795, + "displayNames": [ + { + "language": "ko", + "name": "깊이 우선 탐색", + "short": "깊이 우선 탐색" + }, + { + "language": "en", + "name": "depth-first search", + "short": "dfs" + }, + { + "language": "ja", + "name": "深さ優先探索", + "short": "dfs" + } + ], + "aliases": [ + { + "alias": "depth first" + }, + { + "alias": "depthfirst" + } + ] + }, + { + "key": "shortest_path", + "isMeta": false, + "bojTagId": 215, + "problemCount": 754, + "displayNames": [ + { + "language": "ko", + "name": "최단 경로", + "short": "최단 경로" + }, + { + "language": "en", + "name": "shortest path", + "short": "shortest path" + }, + { + "language": "ja", + "name": "最短経路", + "short": "最短経路" + } + ], + "aliases": [] + }, + { + "key": "bitmask", + "isMeta": false, + "bojTagId": 14, + "problemCount": 704, + "displayNames": [ + { + "language": "ko", + "name": "비트마스킹", + "short": "비트마스킹" + }, + { + "language": "en", + "name": "bitmask", + "short": "bitmask" + }, + { + "language": "ja", + "name": "ビット表現", + "short": "ビット表現" + } + ], + "aliases": [ + { + "alias": "비트필드" + }, + { + "alias": "비트마스크" + } + ] + }, + { + "key": "hash_set", + "isMeta": false, + "bojTagId": 136, + "problemCount": 620, + "displayNames": [ + { + "language": "ko", + "name": "해시를 사용한 집합과 맵", + "short": "해시를 사용한 집합과 맵" + }, + { + "language": "en", + "name": "set / map by hashing", + "short": "hashset" + }, + { + "language": "ja", + "name": "ハッシュ化によるセット・マップ", + "short": "hashset" + } + ], + "aliases": [ + { + "alias": "집합" + }, + { + "alias": "맵" + }, + { + "alias": "셋" + }, + { + "alias": "딕셔너리" + }, + { + "alias": "dictionary" + }, + { + "alias": "map" + }, + { + "alias": "set" + }, + { + "alias": "해싱" + }, + { + "alias": "hashing" + }, + { + "alias": "dict" + } + ] + }, + { + "key": "dijkstra", + "isMeta": false, + "bojTagId": 22, + "problemCount": 572, + "displayNames": [ + { + "language": "ko", + "name": "데이크스트라", + "short": "데이크스트라" + }, + { + "language": "en", + "name": "dijkstra's", + "short": "dijkstra's" + }, + { + "language": "ja", + "name": "ダイクストラ法", + "short": "ダイクストラ法" + } + ], + "aliases": [ + { + "alias": "다익" + }, + { + "alias": "다익스트라" + }, + { + "alias": "데이크스트라" + } + ] + }, + { + "key": "backtracking", + "isMeta": false, + "bojTagId": 5, + "problemCount": 515, + "displayNames": [ + { + "language": "ko", + "name": "백트래킹", + "short": "백트래킹" + }, + { + "language": "en", + "name": "backtracking", + "short": "backtrack" + }, + { + "language": "ja", + "name": "バックトラック法", + "short": "バックトラック" + } + ], + "aliases": [ + { + "alias": "백트래킹" + }, + { + "alias": "퇴각검색" + }, + { + "alias": "퇴각 검색" + } + ] + }, + { + "key": "tree_set", + "isMeta": false, + "bojTagId": 74, + "problemCount": 485, + "displayNames": [ + { + "language": "ko", + "name": "트리를 사용한 집합과 맵", + "short": "트리를 사용한 집합과 맵" + }, + { + "language": "en", + "name": "set / map by trees", + "short": "treeset" + }, + { + "language": "ja", + "name": "木によるセット・マップ", + "short": "treeset" + } + ], + "aliases": [ + { + "alias": "집합" + }, + { + "alias": "맵" + }, + { + "alias": "셋" + }, + { + "alias": "딕셔너리" + }, + { + "alias": "dictionary" + }, + { + "alias": "map" + }, + { + "alias": "set" + }, + { + "alias": "bbst" + }, + { + "alias": "트리" + }, + { + "alias": "tree" + } + ] + }, + { + "key": "sweeping", + "isMeta": false, + "bojTagId": 106, + "problemCount": 465, + "displayNames": [ + { + "language": "ko", + "name": "스위핑", + "short": "스위핑" + }, + { + "language": "en", + "name": "sweeping", + "short": "sweeping" + }, + { + "language": "ja", + "name": "平面走査", + "short": "平面走査" + } + ], + "aliases": [ + { + "alias": "라인 스위핑" + } + ] + }, + { + "key": "disjoint_set", + "isMeta": false, + "bojTagId": 81, + "problemCount": 461, + "displayNames": [ + { + "language": "ko", + "name": "분리 집합", + "short": "분리 집합" + }, + { + "language": "en", + "name": "disjoint set", + "short": "dsu" + }, + { + "language": "ja", + "name": "素集合データ構造", + "short": "素集合データ構造" + } + ], + "aliases": [ + { + "alias": "union" + }, + { + "alias": "find" + }, + { + "alias": "유니온" + }, + { + "alias": "파인드" + }, + { + "alias": "dsu" + } + ] + }, + { + "key": "parsing", + "isMeta": false, + "bojTagId": 96, + "problemCount": 448, + "displayNames": [ + { + "language": "ko", + "name": "파싱", + "short": "파싱" + }, + { + "language": "en", + "name": "parsing", + "short": "parsing" + }, + { + "language": "ja", + "name": "パージング", + "short": "パージング" + } + ], + "aliases": [] + }, + { + "key": "priority_queue", + "isMeta": false, + "bojTagId": 59, + "problemCount": 419, + "displayNames": [ + { + "language": "ko", + "name": "우선순위 큐", + "short": "우선순위 큐" + }, + { + "language": "en", + "name": "priority queue", + "short": "priority queue" + }, + { + "language": "ja", + "name": "優先度付きキュー", + "short": "優先度付きキュー" + } + ], + "aliases": [ + { + "alias": "heap" + }, + { + "alias": "힙" + } + ] + }, + { + "key": "dp_tree", + "isMeta": false, + "bojTagId": 92, + "problemCount": 411, + "displayNames": [ + { + "language": "ko", + "name": "트리에서의 다이나믹 프로그래밍", + "short": "트리에서의 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming on trees", + "short": "tree dp" + }, + { + "language": "ja", + "name": "木上の動的計画法", + "short": "tree dp" + } + ], + "aliases": [ + { + "alias": "트리dp" + } + ] + }, + { + "key": "divide_and_conquer", + "isMeta": false, + "bojTagId": 24, + "problemCount": 406, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복", + "short": "분할 정복" + }, + { + "language": "en", + "name": "divide and conquer", + "short": "d&c" + }, + { + "language": "ja", + "name": "分割統治法", + "short": "分割統治法" + } + ], + "aliases": [ + { + "alias": "dnc" + } + ] + }, + { + "key": "two_pointer", + "isMeta": false, + "bojTagId": 80, + "problemCount": 376, + "displayNames": [ + { + "language": "ko", + "name": "두 포인터", + "short": "두 포인터" + }, + { + "language": "en", + "name": "two-pointer", + "short": "two-pointer" + }, + { + "language": "ja", + "name": "尺取り法", + "short": "尺取り" + } + ], + "aliases": [ + { + "alias": "투포인터" + }, + { + "alias": "인치웜" + }, + { + "alias": "inchworm" + }, + { + "alias": "twopointer" + } + ] + }, + { + "key": "stack", + "isMeta": false, + "bojTagId": 71, + "problemCount": 368, + "displayNames": [ + { + "language": "ko", + "name": "스택", + "short": "스택" + }, + { + "language": "en", + "name": "stack", + "short": "stack" + }, + { + "language": "ja", + "name": "スタック", + "short": "スタック" + } + ], + "aliases": [] + }, + { + "key": "parametric_search", + "isMeta": false, + "bojTagId": 170, + "problemCount": 367, + "displayNames": [ + { + "language": "ko", + "name": "매개 변수 탐색", + "short": "매개 변수 탐색" + }, + { + "language": "en", + "name": "parametric search", + "short": "parametric search" + }, + { + "language": "ja", + "name": "parametric search", + "short": "parametric search" + } + ], + "aliases": [ + { + "alias": "파라메트릭" + } + ] + }, + { + "key": "game_theory", + "isMeta": false, + "bojTagId": 140, + "problemCount": 353, + "displayNames": [ + { + "language": "ko", + "name": "게임 이론", + "short": "게임 이론" + }, + { + "language": "en", + "name": "game theory", + "short": "game theory" + }, + { + "language": "ja", + "name": "ゲーム理論", + "short": "ゲーム" + } + ], + "aliases": [ + { + "alias": "게임이론" + }, + { + "alias": "님" + }, + { + "alias": "nim" + } + ] + }, + { + "key": "flow", + "isMeta": false, + "bojTagId": 45, + "problemCount": 323, + "displayNames": [ + { + "language": "ko", + "name": "최대 유량", + "short": "최대 유량" + }, + { + "language": "en", + "name": "maximum flow", + "short": "flow" + }, + { + "language": "ja", + "name": "最大フロー", + "short": "flow" + } + ], + "aliases": [ + { + "alias": "dinic" + }, + { + "alias": "dinitz" + }, + { + "alias": "ford" + }, + { + "alias": "fulkerson" + }, + { + "alias": "fordfulkerson" + }, + { + "alias": "디닉" + }, + { + "alias": "디니츠" + }, + { + "alias": "포드풀커슨" + }, + { + "alias": "플로우" + } + ] + }, + { + "key": "primality_test", + "isMeta": false, + "bojTagId": 9, + "problemCount": 313, + "displayNames": [ + { + "language": "ko", + "name": "소수 판정", + "short": "소수 판정" + }, + { + "language": "en", + "name": "primality test", + "short": "primality test" + }, + { + "language": "ja", + "name": "素数性テスト", + "short": "素数性テスト" + } + ], + "aliases": [ + { + "alias": "소수" + }, + { + "alias": "소수판별" + }, + { + "alias": "소수판정" + }, + { + "alias": "prime" + } + ] + }, + { + "key": "probability", + "isMeta": false, + "bojTagId": 177, + "problemCount": 299, + "displayNames": [ + { + "language": "ko", + "name": "확률론", + "short": "확률론" + }, + { + "language": "en", + "name": "probability theory", + "short": "probability" + }, + { + "language": "ja", + "name": "確率論", + "short": "確率論" + } + ], + "aliases": [ + { + "alias": "expected value" + }, + { + "alias": "기대값" + }, + { + "alias": "기댓값" + } + ] + }, + { + "key": "lazyprop", + "isMeta": false, + "bojTagId": 66, + "problemCount": 296, + "displayNames": [ + { + "language": "ko", + "name": "느리게 갱신되는 세그먼트 트리", + "short": "느리게 갱신되는 세그먼트 트리" + }, + { + "language": "en", + "name": "segment tree with lazy propagation", + "short": "lazyprop" + }, + { + "language": "ja", + "name": "遅延評価セグメント木", + "short": "遅延評価セグ木" + } + ], + "aliases": [ + { + "alias": "레이지" + }, + { + "alias": "레이지프로퍼게이션" + }, + { + "alias": "레이지프로파게이션" + }, + { + "alias": "구간트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "dp_bitfield", + "isMeta": false, + "bojTagId": 87, + "problemCount": 295, + "displayNames": [ + { + "language": "ko", + "name": "비트필드를 이용한 다이나믹 프로그래밍", + "short": "비트필드를 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using bitfield", + "short": "bitfield dp" + }, + { + "language": "ja", + "name": "ビットを使用した動的計画法", + "short": "ビットdp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "비트마스크" + }, + { + "alias": "비트dp" + } + ] + }, + { + "key": "exponentiation_by_squaring", + "isMeta": false, + "bojTagId": 39, + "problemCount": 273, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복을 이용한 거듭제곱", + "short": "분할 정복을 이용한 거듭제곱" + }, + { + "language": "en", + "name": "exponentiation by squaring", + "short": "exponentiation by squaring" + }, + { + "language": "ja", + "name": "二乗法によるべき乗", + "short": "二乗法によるべき乗" + } + ], + "aliases": [ + { + "alias": "거듭제곱" + }, + { + "alias": "제곱" + }, + { + "alias": "power" + }, + { + "alias": "square" + } + ] + }, + { + "key": "arbitrary_precision", + "isMeta": false, + "bojTagId": 117, + "problemCount": 254, + "displayNames": [ + { + "language": "ko", + "name": "임의 정밀도 / 큰 수 연산", + "short": "임의 정밀도 / 큰 수 연산" + }, + { + "language": "en", + "name": "arbitrary precision / big integers", + "short": "arbitrary precision / big integers" + }, + { + "language": "ja", + "name": "高精度または大きな数の演算", + "short": "高精度または大きな数の演算" + } + ], + "aliases": [ + { + "alias": "빅인티저" + }, + { + "alias": "빅데시멀" + }, + { + "alias": "biginteger" + }, + { + "alias": "bigdecimal" + } + ] + }, + { + "key": "knapsack", + "isMeta": false, + "bojTagId": 148, + "problemCount": 247, + "displayNames": [ + { + "language": "ko", + "name": "배낭 문제", + "short": "배낭 문제" + }, + { + "language": "en", + "name": "knapsack", + "short": "knapsack" + }, + { + "language": "ja", + "name": "ナップサック問題", + "short": "ナップサック" + } + ], + "aliases": [ + { + "alias": "냅색" + } + ] + }, + { + "key": "offline_queries", + "isMeta": false, + "bojTagId": 123, + "problemCount": 246, + "displayNames": [ + { + "language": "ko", + "name": "오프라인 쿼리", + "short": "오프라인 쿼리" + }, + { + "language": "en", + "name": "offline queries", + "short": "offline query" + }, + { + "language": "ja", + "name": "offline queries", + "short": "offline query" + } + ], + "aliases": [ + { + "alias": "offlinequery" + } + ] + }, + { + "key": "recursion", + "isMeta": false, + "bojTagId": 62, + "problemCount": 223, + "displayNames": [ + { + "language": "ko", + "name": "재귀", + "short": "재귀" + }, + { + "language": "en", + "name": "recursion", + "short": "recursion" + }, + { + "language": "ja", + "name": "再帰", + "short": "再帰" + } + ], + "aliases": [] + }, + { + "key": "coordinate_compression", + "isMeta": false, + "bojTagId": 161, + "problemCount": 223, + "displayNames": [ + { + "language": "ko", + "name": "값 / 좌표 압축", + "short": "값 / 좌표 압축" + }, + { + "language": "en", + "name": "value / coordinate compression", + "short": "compression" + }, + { + "language": "ja", + "name": "value / coordinate compression", + "short": "compression" + } + ], + "aliases": [ + { + "alias": "zip" + } + ] + }, + { + "key": "precomputation", + "isMeta": false, + "bojTagId": 172, + "problemCount": 203, + "displayNames": [ + { + "language": "ko", + "name": "런타임 전의 전처리", + "short": "런타임 전의 전처리" + }, + { + "language": "en", + "name": "precomputation", + "short": "precomputation" + }, + { + "language": "ja", + "name": "事前計算", + "short": "事前計算" + } + ], + "aliases": [ + { + "alias": "lookup table" + }, + { + "alias": "db" + }, + { + "alias": "database" + } + ] + }, + { + "key": "mst", + "isMeta": false, + "bojTagId": 49, + "problemCount": 199, + "displayNames": [ + { + "language": "ko", + "name": "최소 스패닝 트리", + "short": "최소 스패닝 트리" + }, + { + "language": "en", + "name": "minimum spanning tree", + "short": "mst" + }, + { + "language": "ja", + "name": "最小全域木", + "short": "最小全域木" + } + ], + "aliases": [] + }, + { + "key": "sieve", + "isMeta": false, + "bojTagId": 67, + "problemCount": 196, + "displayNames": [ + { + "language": "ko", + "name": "에라토스테네스의 체", + "short": "에라토스테네스의 체" + }, + { + "language": "en", + "name": "sieve of eratosthenes", + "short": "eratosthenes" + }, + { + "language": "ja", + "name": "エラトステネスの篩", + "short": "エラトステネス" + } + ], + "aliases": [ + { + "alias": "sieve" + }, + { + "alias": "에라체" + }, + { + "alias": "소수" + }, + { + "alias": "prime" + } + ] + }, + { + "key": "euclidean", + "isMeta": false, + "bojTagId": 26, + "problemCount": 186, + "displayNames": [ + { + "language": "ko", + "name": "유클리드 호제법", + "short": "유클리드 호제법" + }, + { + "language": "en", + "name": "euclidean algorithm", + "short": "euclidean algorithm" + }, + { + "language": "ja", + "name": "ユークリッドの互除法", + "short": "ユークリッドの互除法" + } + ], + "aliases": [ + { + "alias": "유클리드알고리즘" + } + ] + }, + { + "key": "bipartite_matching", + "isMeta": false, + "bojTagId": 13, + "problemCount": 184, + "displayNames": [ + { + "language": "ko", + "name": "이분 매칭", + "short": "이분 매칭" + }, + { + "language": "en", + "name": "bipartite matching", + "short": "bipartite matching" + }, + { + "language": "ja", + "name": "2部マッチング", + "short": "2部マッチング" + } + ], + "aliases": [] + }, + { + "key": "dag", + "isMeta": false, + "bojTagId": 213, + "problemCount": 182, + "displayNames": [ + { + "language": "ko", + "name": "방향 비순환 그래프", + "short": "dag" + }, + { + "language": "en", + "name": "directed acyclic graph", + "short": "dag" + }, + { + "language": "ja", + "name": "有向非巡回グラフ", + "short": "有向非巡回グラフ" + } + ], + "aliases": [] + }, + { + "key": "convex_hull", + "isMeta": false, + "bojTagId": 20, + "problemCount": 178, + "displayNames": [ + { + "language": "ko", + "name": "볼록 껍질", + "short": "볼록 껍질" + }, + { + "language": "en", + "name": "convex hull", + "short": "convex hull" + }, + { + "language": "ja", + "name": "凸包", + "short": "凸包" + } + ], + "aliases": [ + { + "alias": "컨벡스헐" + } + ] + }, + { + "key": "linear_algebra", + "isMeta": false, + "bojTagId": 144, + "problemCount": 174, + "displayNames": [ + { + "language": "ko", + "name": "선형대수학", + "short": "선형대수학" + }, + { + "language": "en", + "name": "linear algebra", + "short": "linear algebra" + }, + { + "language": "ja", + "name": "線形代数", + "short": "線代" + } + ], + "aliases": [ + { + "alias": "선형대수" + } + ] + }, + { + "key": "topological_sorting", + "isMeta": false, + "bojTagId": 78, + "problemCount": 170, + "displayNames": [ + { + "language": "ko", + "name": "위상 정렬", + "short": "위상 정렬" + }, + { + "language": "en", + "name": "topological sorting", + "short": "topological sorting" + }, + { + "language": "ja", + "name": "トポロジカルソート", + "short": "トポロジカルソート" + } + ], + "aliases": [] + }, + { + "key": "floyd_warshall", + "isMeta": false, + "bojTagId": 31, + "problemCount": 165, + "displayNames": [ + { + "language": "ko", + "name": "플로이드–워셜", + "short": "플로이드–워셜" + }, + { + "language": "en", + "name": "floyd–warshall", + "short": "floyd–warshall" + }, + { + "language": "ja", + "name": "ワーシャル–フロイド法", + "short": "ワーシャル–フロイド法" + } + ], + "aliases": [ + { + "alias": "플로이드" + }, + { + "alias": "플로이드와셜" + }, + { + "alias": "플로이드와샬" + } + ] + }, + { + "key": "hashing", + "isMeta": false, + "bojTagId": 8, + "problemCount": 164, + "displayNames": [ + { + "language": "ko", + "name": "해싱", + "short": "해싱" + }, + { + "language": "en", + "name": "hashing", + "short": "hash" + }, + { + "language": "ja", + "name": "ハッシュ化", + "short": "ハッシュ" + } + ], + "aliases": [] + }, + { + "key": "lca", + "isMeta": false, + "bojTagId": 41, + "problemCount": 163, + "displayNames": [ + { + "language": "ko", + "name": "최소 공통 조상", + "short": "최소 공통 조상" + }, + { + "language": "en", + "name": "lowest common ancestor", + "short": "lca" + }, + { + "language": "ja", + "name": "最下位共通祖先", + "short": "lca" + } + ], + "aliases": [] + }, + { + "key": "inclusion_and_exclusion", + "isMeta": false, + "bojTagId": 38, + "problemCount": 152, + "displayNames": [ + { + "language": "ko", + "name": "포함 배제의 원리", + "short": "포함 배제의 원리" + }, + { + "language": "en", + "name": "inclusion and exclusion", + "short": "inclusion and exclusion" + }, + { + "language": "ja", + "name": "包除原理", + "short": "包除原理" + } + ], + "aliases": [] + }, + { + "key": "scc", + "isMeta": false, + "bojTagId": 76, + "problemCount": 147, + "displayNames": [ + { + "language": "ko", + "name": "강한 연결 요소", + "short": "강한 연결 요소" + }, + { + "language": "en", + "name": "strongly connected component", + "short": "scc" + }, + { + "language": "ja", + "name": "強連結", + "short": "強連結" + } + ], + "aliases": [] + }, + { + "key": "randomization", + "isMeta": false, + "bojTagId": 115, + "problemCount": 143, + "displayNames": [ + { + "language": "ko", + "name": "무작위화", + "short": "무작위화" + }, + { + "language": "en", + "name": "randomization", + "short": "randomization" + }, + { + "language": "ja", + "name": "ランダム化", + "short": "ランダム化" + } + ], + "aliases": [ + { + "alias": "랜덤" + } + ] + }, + { + "key": "sparse_table", + "isMeta": false, + "bojTagId": 84, + "problemCount": 136, + "displayNames": [ + { + "language": "ko", + "name": "희소 배열", + "short": "희소 배열" + }, + { + "language": "en", + "name": "sparse table", + "short": "sparse table" + }, + { + "language": "ja", + "name": "sparse table", + "short": "sparse table" + } + ], + "aliases": [ + { + "alias": "스파스어레이" + }, + { + "alias": "sparse table" + } + ] + }, + { + "key": "smaller_to_larger", + "isMeta": false, + "bojTagId": 169, + "problemCount": 128, + "displayNames": [ + { + "language": "ko", + "name": "작은 집합에서 큰 집합으로 합치는 테크닉", + "short": "작은 집합에서 큰 집합으로 합치는 테크닉" + }, + { + "language": "en", + "name": "smaller to larger technique", + "short": "smaller to larger" + }, + { + "language": "ja", + "name": "smaller to larger technique", + "short": "smaller to larger" + } + ], + "aliases": [ + { + "alias": "merge heuristics" + }, + { + "alias": "sack" + }, + { + "alias": "small to large" + }, + { + "alias": "작은거" + }, + { + "alias": "큰거" + } + ] + }, + { + "key": "fft", + "isMeta": false, + "bojTagId": 28, + "problemCount": 126, + "displayNames": [ + { + "language": "ko", + "name": "고속 푸리에 변환", + "short": "고속 푸리에 변환" + }, + { + "language": "en", + "name": "fast fourier transform", + "short": "fft" + }, + { + "language": "ja", + "name": "高速フーリエ変換", + "short": "fft" + } + ], + "aliases": [ + { + "alias": "푸리에변환" + }, + { + "alias": "컨볼루션" + }, + { + "alias": "convolution" + } + ] + }, + { + "key": "trie", + "isMeta": false, + "bojTagId": 79, + "problemCount": 124, + "displayNames": [ + { + "language": "ko", + "name": "트라이", + "short": "트라이" + }, + { + "language": "en", + "name": "trie", + "short": "trie" + }, + { + "language": "ja", + "name": "トライ木", + "short": "トライ" + } + ], + "aliases": [] + }, + { + "key": "deque", + "isMeta": false, + "bojTagId": 73, + "problemCount": 120, + "displayNames": [ + { + "language": "ko", + "name": "덱", + "short": "덱" + }, + { + "language": "en", + "name": "deque", + "short": "deque" + }, + { + "language": "ja", + "name": "両端キュー", + "short": "deque" + } + ], + "aliases": [] + }, + { + "key": "line_intersection", + "isMeta": false, + "bojTagId": 42, + "problemCount": 117, + "displayNames": [ + { + "language": "ko", + "name": "선분 교차 판정", + "short": "선분 교차 판정" + }, + { + "language": "en", + "name": "line segment intersection check", + "short": "line segment intersection check" + }, + { + "language": "ja", + "name": "直線の交点", + "short": "直線の交点" + } + ], + "aliases": [] + }, + { + "key": "mcmf", + "isMeta": false, + "bojTagId": 48, + "problemCount": 116, + "displayNames": [ + { + "language": "ko", + "name": "최소 비용 최대 유량", + "short": "최소 비용 최대 유량" + }, + { + "language": "en", + "name": "minimum cost maximum flow", + "short": "mcmf" + }, + { + "language": "ja", + "name": "最小費用最大流問題", + "short": "mcmf" + } + ], + "aliases": [ + { + "alias": "dinic" + }, + { + "alias": "dinitz" + }, + { + "alias": "ford" + }, + { + "alias": "fulkerson" + }, + { + "alias": "fordfulkerson" + }, + { + "alias": "디닉" + }, + { + "alias": "디니츠" + }, + { + "alias": "포드풀커슨" + } + ] + }, + { + "key": "sqrt_decomposition", + "isMeta": false, + "bojTagId": 130, + "problemCount": 110, + "displayNames": [ + { + "language": "ko", + "name": "제곱근 분할법", + "short": "제곱근 분할법" + }, + { + "language": "en", + "name": "square root decomposition", + "short": "sqrt decomposition" + }, + { + "language": "ja", + "name": "平方分割", + "short": "平方分割" + } + ], + "aliases": [ + { + "alias": "루트분할법" + }, + { + "alias": "평방분할법" + }, + { + "alias": "모" + }, + { + "alias": "mo" + }, + { + "alias": "sqrt" + } + ] + }, + { + "key": "calculus", + "isMeta": false, + "bojTagId": 111, + "problemCount": 107, + "displayNames": [ + { + "language": "ko", + "name": "미적분학", + "short": "미적분학" + }, + { + "language": "en", + "name": "calculus", + "short": "calculus" + }, + { + "language": "ja", + "name": "微積分", + "short": "微積分" + } + ], + "aliases": [ + { + "alias": "미분" + }, + { + "alias": "적분" + } + ] + }, + { + "key": "modular_multiplicative_inverse", + "isMeta": false, + "bojTagId": 164, + "problemCount": 101, + "displayNames": [ + { + "language": "ko", + "name": "모듈로 곱셈 역원", + "short": "모듈로 곱셈 역원" + }, + { + "language": "en", + "name": "modular multiplicative inverse", + "short": "modular multiplicative inverse" + }, + { + "language": "ja", + "name": "モジュラ逆数", + "short": "モジュラ逆数" + } + ], + "aliases": [ + { + "alias": "modinv" + } + ] + }, + { + "key": "cht", + "isMeta": false, + "bojTagId": 89, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "볼록 껍질을 이용한 최적화", + "short": "볼록 껍질을 이용한 최적화" + }, + { + "language": "en", + "name": "convex hull trick", + "short": "cht" + }, + { + "language": "ja", + "name": "convex hull trick", + "short": "cht" + } + ], + "aliases": [ + { + "alias": "컨벡스헐트릭" + }, + { + "alias": "컨벡스헐최적화" + } + ] + }, + { + "key": "heuristics", + "isMeta": false, + "bojTagId": 142, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "휴리스틱", + "short": "휴리스틱" + }, + { + "language": "en", + "name": "heuristics", + "short": "heuristics" + }, + { + "language": "ja", + "name": "ヒューリスティック", + "short": "ヒューリスティック" + } + ], + "aliases": [] + }, + { + "key": "sliding_window", + "isMeta": false, + "bojTagId": 68, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "슬라이딩 윈도우", + "short": "슬라이딩 윈도우" + }, + { + "language": "en", + "name": "sliding window", + "short": "sliding window" + }, + { + "language": "ja", + "name": "スライディングウィンドウ", + "short": "スライディングウィンドウ" + } + ], + "aliases": [ + { + "alias": "슬라이딩윈도" + } + ] + }, + { + "key": "geometry_3d", + "isMeta": false, + "bojTagId": 131, + "problemCount": 98, + "displayNames": [ + { + "language": "ko", + "name": "3차원 기하학", + "short": "3차원 기하학" + }, + { + "language": "en", + "name": "geometry; 3d", + "short": "3d" + }, + { + "language": "ja", + "name": "3次元幾何学", + "short": "3d" + } + ], + "aliases": [] + }, + { + "key": "suffix_array", + "isMeta": false, + "bojTagId": 77, + "problemCount": 97, + "displayNames": [ + { + "language": "ko", + "name": "접미사 배열과 LCP 배열", + "short": "접미사 배열과 LCP 배열" + }, + { + "language": "en", + "name": "suffix array and lcp array", + "short": "suffix array and lcp array" + }, + { + "language": "ja", + "name": "接尾辞配列・LCP配列", + "short": "接尾辞配列・LCP配列" + } + ], + "aliases": [] + }, + { + "key": "centroid", + "isMeta": false, + "bojTagId": 188, + "problemCount": 95, + "displayNames": [ + { + "language": "en", + "name": "centroid", + "short": "centroid" + }, + { + "language": "ko", + "name": "센트로이드", + "short": "센트로이드" + }, + { + "language": "ja", + "name": "centroid", + "short": "centroid" + } + ], + "aliases": [] + }, + { + "key": "euler_tour_technique", + "isMeta": false, + "bojTagId": 150, + "problemCount": 95, + "displayNames": [ + { + "language": "ko", + "name": "오일러 경로 테크닉", + "short": "오일러 경로 테크닉" + }, + { + "language": "en", + "name": "euler tour technique", + "short": "ett" + }, + { + "language": "ja", + "name": "オイラーツアー", + "short": "ett" + } + ], + "aliases": [] + }, + { + "key": "sprague_grundy", + "isMeta": false, + "bojTagId": 70, + "problemCount": 94, + "displayNames": [ + { + "language": "ko", + "name": "스프라그–그런디 정리", + "short": "스프라그–그런디 정리" + }, + { + "language": "en", + "name": "sprague–grundy theorem", + "short": "sprague–grundy thm" + }, + { + "language": "ja", + "name": "sprague–grundy theorem", + "short": "sprague–grundy thm" + } + ], + "aliases": [ + { + "alias": "님버" + }, + { + "alias": "nimber" + } + ] + }, + { + "key": "ternary_search", + "isMeta": false, + "bojTagId": 101, + "problemCount": 92, + "displayNames": [ + { + "language": "ko", + "name": "삼분 탐색", + "short": "삼분 탐색" + }, + { + "language": "en", + "name": "ternary search", + "short": "ternary search" + }, + { + "language": "ja", + "name": "三分探索", + "short": "三分探索" + } + ], + "aliases": [] + }, + { + "key": "mitm", + "isMeta": false, + "bojTagId": 46, + "problemCount": 89, + "displayNames": [ + { + "language": "ko", + "name": "중간에서 만나기", + "short": "중간에서 만나기" + }, + { + "language": "en", + "name": "meet in the middle", + "short": "meet in the middle" + }, + { + "language": "ja", + "name": "半分全列挙", + "short": "半分全列挙" + } + ], + "aliases": [] + }, + { + "key": "bitset", + "isMeta": false, + "bojTagId": 152, + "problemCount": 89, + "displayNames": [ + { + "language": "ko", + "name": "비트 집합", + "short": "비트 집합" + }, + { + "language": "en", + "name": "bit set", + "short": "bit set" + }, + { + "language": "ja", + "name": "bit set", + "short": "bit set" + } + ], + "aliases": [ + { + "alias": "bitset" + }, + { + "alias": "비트셋" + } + ] + }, + { + "key": "pythagoras", + "isMeta": false, + "bojTagId": 60, + "problemCount": 88, + "displayNames": [ + { + "language": "ko", + "name": "피타고라스 정리", + "short": "피타고라스 정리" + }, + { + "language": "en", + "name": "pythagoras theorem", + "short": "pythagoras thm" + }, + { + "language": "ja", + "name": "ピタゴラスの定理", + "short": "ピタゴラス" + } + ], + "aliases": [] + }, + { + "key": "permutation_cycle_decomposition", + "isMeta": false, + "bojTagId": 171, + "problemCount": 87, + "displayNames": [ + { + "language": "ko", + "name": "순열 사이클 분할", + "short": "순열 사이클 분할" + }, + { + "language": "en", + "name": "permutation cycle decomposition", + "short": "permutation cycle decomposition" + }, + { + "language": "ja", + "name": "順列サイクル分解", + "short": "順列サイクル分解" + } + ], + "aliases": [] + }, + { + "key": "lis", + "isMeta": false, + "bojTagId": 43, + "problemCount": 85, + "displayNames": [ + { + "language": "ko", + "name": "가장 긴 증가하는 부분 수열: O(n log n)", + "short": "가장 긴 증가하는 부분 수열: O(n log n)" + }, + { + "language": "en", + "name": "longest increasing sequence in o(n log n)", + "short": "lis in o(n log n)" + }, + { + "language": "ja", + "name": "longest increasing sequence in o(n log n)", + "short": "lis in o(n log n)" + } + ], + "aliases": [] + }, + { + "key": "kmp", + "isMeta": false, + "bojTagId": 40, + "problemCount": 84, + "displayNames": [ + { + "language": "ko", + "name": "KMP", + "short": "KMP" + }, + { + "language": "en", + "name": "knuth–morris–pratt", + "short": "kmp" + }, + { + "language": "ja", + "name": "クヌース–モリス–プラット法", + "short": "kmp" + } + ], + "aliases": [] + }, + { + "key": "gaussian_elimination", + "isMeta": false, + "bojTagId": 32, + "problemCount": 81, + "displayNames": [ + { + "language": "ko", + "name": "가우스 소거법", + "short": "가우스 소거법" + }, + { + "language": "en", + "name": "gaussian elimination", + "short": "gaussian elimination" + }, + { + "language": "ja", + "name": "ガウス消去法", + "short": "ガウス消去法" + } + ], + "aliases": [] + }, + { + "key": "hld", + "isMeta": false, + "bojTagId": 35, + "problemCount": 80, + "displayNames": [ + { + "language": "ko", + "name": "Heavy-light 분할", + "short": "Heavy-light 분할" + }, + { + "language": "en", + "name": "heavy-light decomposition", + "short": "hld" + }, + { + "language": "ja", + "name": "heavy-light decomposition", + "short": "hld" + } + ], + "aliases": [] + }, + { + "key": "centroid_decomposition", + "isMeta": false, + "bojTagId": 18, + "problemCount": 76, + "displayNames": [ + { + "language": "ko", + "name": "센트로이드 분할", + "short": "센트로이드 분할" + }, + { + "language": "en", + "name": "centroid decomposition", + "short": "centroid decomposition" + }, + { + "language": "ja", + "name": "centroid decomposition", + "short": "centroid decomposition" + } + ], + "aliases": [ + { + "alias": "센트로이드" + } + ] + }, + { + "key": "mfmc", + "isMeta": false, + "bojTagId": 167, + "problemCount": 71, + "displayNames": [ + { + "language": "ko", + "name": "최대 유량 최소 컷 정리", + "short": "최대 유량 최소 컷 정리" + }, + { + "language": "en", + "name": "max-flow min-cut theorem", + "short": "mfmc" + }, + { + "language": "ja", + "name": "最大フロー最小カット定理", + "short": "mfmc" + } + ], + "aliases": [] + }, + { + "key": "polygon_area", + "isMeta": false, + "bojTagId": 3, + "problemCount": 71, + "displayNames": [ + { + "language": "ko", + "name": "다각형의 넓이", + "short": "다각형의 넓이" + }, + { + "language": "en", + "name": "area of a polygon", + "short": "area of a polygon" + }, + { + "language": "ja", + "name": "多角形の面積", + "short": "多角形の面積" + } + ], + "aliases": [ + { + "alias": "넓이" + } + ] + }, + { + "key": "queue", + "isMeta": false, + "bojTagId": 72, + "problemCount": 64, + "displayNames": [ + { + "language": "ko", + "name": "큐", + "short": "큐" + }, + { + "language": "en", + "name": "queue", + "short": "queue" + }, + { + "language": "ja", + "name": "キュー", + "short": "キュー" + } + ], + "aliases": [] + }, + { + "key": "physics", + "isMeta": false, + "bojTagId": 116, + "problemCount": 62, + "displayNames": [ + { + "language": "ko", + "name": "물리학", + "short": "물리학" + }, + { + "language": "en", + "name": "physics", + "short": "physics" + }, + { + "language": "ja", + "name": "物理", + "short": "物理" + } + ], + "aliases": [] + }, + { + "key": "flt", + "isMeta": false, + "bojTagId": 29, + "problemCount": 60, + "displayNames": [ + { + "language": "ko", + "name": "페르마의 소정리", + "short": "페르마의 소정리" + }, + { + "language": "en", + "name": "fermat's little theorem", + "short": "fermat's little thm" + }, + { + "language": "ja", + "name": "フェルマーの小定理", + "short": "フェルマー" + } + ], + "aliases": [] + }, + { + "key": "tsp", + "isMeta": false, + "bojTagId": 138, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "외판원 순회 문제", + "short": "외판원 순회 문제" + }, + { + "language": "en", + "name": "travelling salesman problem", + "short": "tsp" + }, + { + "language": "ja", + "name": "巡回セールスマン問題", + "short": "巡回セールスマン" + } + ], + "aliases": [ + { + "alias": "외판원순회" + } + ] + }, + { + "key": "eulerian_path", + "isMeta": false, + "bojTagId": 93, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "오일러 경로", + "short": "오일러 경로" + }, + { + "language": "en", + "name": "eulerian path / circuit", + "short": "eulerian path" + }, + { + "language": "ja", + "name": "eulerian path / circuit", + "short": "eulerian path" + } + ], + "aliases": [ + { + "alias": "eulerian circuit" + }, + { + "alias": "euler tour" + } + ] + }, + { + "key": "linearity_of_expectation", + "isMeta": false, + "bojTagId": 179, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "기댓값의 선형성", + "short": "기댓값의 선형성" + }, + { + "language": "en", + "name": "linearity of expectation", + "short": "linearity of expectation" + }, + { + "language": "ja", + "name": "期待値の線形性", + "short": "期待値の線形性" + } + ], + "aliases": [] + }, + { + "key": "2_sat", + "isMeta": false, + "bojTagId": 1, + "problemCount": 58, + "displayNames": [ + { + "language": "ko", + "name": "2-sat", + "short": "2-sat" + }, + { + "language": "en", + "name": "2-sat", + "short": "2-sat" + }, + { + "language": "ja", + "name": "2-sat", + "short": "2-sat" + } + ], + "aliases": [ + { + "alias": "투셋" + }, + { + "alias": "twosat" + }, + { + "alias": "2sat" + } + ] + }, + { + "key": "articulation", + "isMeta": false, + "bojTagId": 4, + "problemCount": 57, + "displayNames": [ + { + "language": "ko", + "name": "단절점과 단절선", + "short": "단절점과 단절선" + }, + { + "language": "en", + "name": "articulation points and bridges", + "short": "articulation points and bridges" + }, + { + "language": "ja", + "name": "関節点と橋", + "short": "関節点と橋" + } + ], + "aliases": [ + { + "alias": "단절점" + }, + { + "alias": "단절선" + }, + { + "alias": "브리지" + }, + { + "alias": "브릿지" + }, + { + "alias": "bridge" + } + ] + }, + { + "key": "0_1_bfs", + "isMeta": false, + "bojTagId": 176, + "problemCount": 56, + "displayNames": [ + { + "language": "ko", + "name": "0-1 너비 우선 탐색", + "short": "0-1 너비 우선 탐색" + }, + { + "language": "en", + "name": "0-1 bfs", + "short": "0-1 bfs" + }, + { + "language": "ja", + "name": "0-1 bfs", + "short": "0-1 bfs" + } + ], + "aliases": [] + }, + { + "key": "bipartite_graph", + "isMeta": false, + "bojTagId": 197, + "problemCount": 54, + "displayNames": [ + { + "language": "ko", + "name": "이분 그래프", + "short": "이분 그래프" + }, + { + "language": "en", + "name": "bipartite graph", + "short": "bipartite graph" + }, + { + "language": "ja", + "name": "2部グラフ", + "short": "2部グラフ" + } + ], + "aliases": [] + }, + { + "key": "biconnected_component", + "isMeta": false, + "bojTagId": 153, + "problemCount": 48, + "displayNames": [ + { + "language": "ko", + "name": "이중 연결 요소", + "short": "이중 연결 요소" + }, + { + "language": "en", + "name": "biconnected component", + "short": "biconnected component" + }, + { + "language": "ja", + "name": "二重接続コンポーネント", + "short": "二重接続" + } + ], + "aliases": [ + { + "alias": "bcc" + } + ] + }, + { + "key": "pst", + "isMeta": false, + "bojTagId": 55, + "problemCount": 46, + "displayNames": [ + { + "language": "ko", + "name": "퍼시스턴트 세그먼트 트리", + "short": "퍼시스턴트 세그먼트 트리" + }, + { + "language": "en", + "name": "persistent segment tree", + "short": "pst" + }, + { + "language": "ja", + "name": "永続セグメント木", + "short": "永続セグ木" + } + ], + "aliases": [ + { + "alias": "퍼시스턴트구간트리" + }, + { + "alias": "구간트리" + }, + { + "alias": "퍼시스턴트세그트리" + } + ] + }, + { + "key": "crt", + "isMeta": false, + "bojTagId": 19, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "중국인의 나머지 정리", + "short": "중국인의 나머지 정리" + }, + { + "language": "en", + "name": "chinese remainder theorem", + "short": "crt" + }, + { + "language": "ja", + "name": "中国の剰余定理", + "short": "中国の剰余定理" + } + ], + "aliases": [] + }, + { + "key": "linked_list", + "isMeta": false, + "bojTagId": 154, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "연결 리스트", + "short": "연결 리스트" + }, + { + "language": "en", + "name": "linked list", + "short": "ll" + }, + { + "language": "ja", + "name": "連結リスト", + "short": "連結リスト" + } + ], + "aliases": [ + { + "alias": "링크드리스트" + } + ] + }, + { + "key": "pigeonhole_principle", + "isMeta": false, + "bojTagId": 189, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "비둘기집 원리", + "short": "비둘기집" + }, + { + "language": "en", + "name": "pigeonhole principle", + "short": "pigeonhole" + }, + { + "language": "ja", + "name": "鳩の巣原理", + "short": "鳩" + } + ], + "aliases": [] + }, + { + "key": "cactus", + "isMeta": false, + "bojTagId": 143, + "problemCount": 42, + "displayNames": [ + { + "language": "ko", + "name": "선인장", + "short": "선인장" + }, + { + "language": "en", + "name": "cactus", + "short": "cactus" + }, + { + "language": "ja", + "name": "サボテングラフ", + "short": "サボテングラフ" + } + ], + "aliases": [] + }, + { + "key": "bellman_ford", + "isMeta": false, + "bojTagId": 10, + "problemCount": 41, + "displayNames": [ + { + "language": "ko", + "name": "벨만–포드", + "short": "벨만–포드" + }, + { + "language": "en", + "name": "bellman–ford", + "short": "bellman-ford" + }, + { + "language": "ja", + "name": "ベルマンフォード法", + "short": "ベルマンフォード" + } + ], + "aliases": [ + { + "alias": "bellmanford" + }, + { + "alias": "벨만포드" + }, + { + "alias": "spfa" + } + ] + }, + { + "key": "planar_graph", + "isMeta": false, + "bojTagId": 168, + "problemCount": 41, + "displayNames": [ + { + "language": "ko", + "name": "평면 그래프", + "short": "평면 그래프" + }, + { + "language": "en", + "name": "planar graph", + "short": "planar graph" + }, + { + "language": "ja", + "name": "平面グラフ", + "short": "平面グラフ" + } + ], + "aliases": [] + }, + { + "key": "point_in_convex_polygon", + "isMeta": false, + "bojTagId": 56, + "problemCount": 40, + "displayNames": [ + { + "language": "ko", + "name": "볼록 다각형 내부의 점 판정", + "short": "볼록 다각형 내부의 점 판정" + }, + { + "language": "en", + "name": "point in convex polygon check", + "short": "point in convex polygon check" + }, + { + "language": "ja", + "name": "凸多角形の点包含判定", + "short": "凸多角形の点包含判定" + } + ], + "aliases": [] + }, + { + "key": "euler_phi", + "isMeta": false, + "bojTagId": 151, + "problemCount": 38, + "displayNames": [ + { + "language": "ko", + "name": "오일러 피 함수", + "short": "오일러 피 함수" + }, + { + "language": "en", + "name": "euler totient function", + "short": "euler phi function" + }, + { + "language": "ja", + "name": "euler totient function", + "short": "euler phi function" + } + ], + "aliases": [ + { + "alias": "오일러 파이" + }, + { + "alias": "토션트" + }, + { + "alias": "eulerphi" + }, + { + "alias": "euler phi" + } + ] + }, + { + "key": "splay_tree", + "isMeta": false, + "bojTagId": 69, + "problemCount": 37, + "displayNames": [ + { + "language": "ko", + "name": "스플레이 트리", + "short": "스플레이 트리" + }, + { + "language": "en", + "name": "splay tree", + "short": "splay tree" + }, + { + "language": "ja", + "name": "splay tree", + "short": "splay tree" + } + ], + "aliases": [] + }, + { + "key": "pbs", + "isMeta": false, + "bojTagId": 54, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "병렬 이분 탐색", + "short": "병렬 이분 탐색" + }, + { + "language": "en", + "name": "parallel binary search", + "short": "pbs" + }, + { + "language": "ja", + "name": "parallel binary search", + "short": "pbs" + } + ], + "aliases": [] + }, + { + "key": "extended_euclidean", + "isMeta": false, + "bojTagId": 27, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "확장 유클리드 호제법", + "short": "확장 유클리드 호제법" + }, + { + "language": "en", + "name": "extended euclidean algorithm", + "short": "extended euclidean algorithm" + }, + { + "language": "ja", + "name": "拡張ユークリッドの互除法", + "short": "拡張ユークリッド" + } + ], + "aliases": [ + { + "alias": "확장유클리드알고리즘" + }, + { + "alias": "egcd" + } + ] + }, + { + "key": "divide_and_conquer_optimization", + "isMeta": false, + "bojTagId": 91, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복을 사용한 최적화", + "short": "분할 정복을 사용한 최적화" + }, + { + "language": "en", + "name": "divide and conquer optimization", + "short": "d&c optimization" + }, + { + "language": "ja", + "name": "divide and conquer optimization", + "short": "d&c optimization" + } + ], + "aliases": [ + { + "alias": "분할 정복 최적화" + }, + { + "alias": "dnc opt" + } + ] + }, + { + "key": "deque_trick", + "isMeta": false, + "bojTagId": 216, + "problemCount": 34, + "displayNames": [ + { + "language": "ko", + "name": "덱을 이용한 구간 최댓값 트릭", + "short": "덱 트릭" + }, + { + "language": "en", + "name": "deque range maximum trick", + "short": "deque rmq trick" + }, + { + "language": "ja", + "name": "deque range maximum trick", + "short": "deque rmq trick" + } + ], + "aliases": [] + }, + { + "key": "mo", + "isMeta": false, + "bojTagId": 50, + "problemCount": 33, + "displayNames": [ + { + "language": "ko", + "name": "mo's", + "short": "Mo's" + }, + { + "language": "en", + "name": "mo's", + "short": "mo's" + }, + { + "language": "ja", + "name": "mo's", + "short": "mo's" + } + ], + "aliases": [ + { + "alias": "squarerootdecomposition" + }, + { + "alias": "sqrtdecomposition" + }, + { + "alias": "평방분할법" + } + ] + }, + { + "key": "half_plane_intersection", + "isMeta": false, + "bojTagId": 190, + "problemCount": 30, + "displayNames": [ + { + "language": "ko", + "name": "반평면 교집합", + "short": "반평면 교집합" + }, + { + "language": "en", + "name": "half plane intersection", + "short": "hpi" + }, + { + "language": "ja", + "name": "half plane intersection", + "short": "hpi" + } + ], + "aliases": [] + }, + { + "key": "dp_deque", + "isMeta": false, + "bojTagId": 108, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "덱을 이용한 다이나믹 프로그래밍", + "short": "덱을 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using a deque", + "short": "deque dp" + }, + { + "language": "ja", + "name": "両端キューを使用した動的計画法", + "short": "deque dp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "덱dp" + } + ] + }, + { + "key": "aho_corasick", + "isMeta": false, + "bojTagId": 2, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "아호-코라식", + "short": "아호-코라식" + }, + { + "language": "en", + "name": "aho-corasick", + "short": "aho-corasick" + }, + { + "language": "ja", + "name": "アホコラシック", + "short": "アホコラシック" + } + ], + "aliases": [ + { + "alias": "아호코라식" + }, + { + "alias": "ahocorasick" + } + ] + }, + { + "key": "multi_segtree", + "isMeta": false, + "bojTagId": 166, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "다차원 세그먼트 트리", + "short": "다차원 세그먼트 트리" + }, + { + "language": "en", + "name": "multidimensional segment tree", + "short": "multidimensional segtree" + }, + { + "language": "ja", + "name": "multidimensional segment tree", + "short": "multidimensional segtree" + } + ], + "aliases": [ + { + "alias": "구간트리" + }, + { + "alias": "세그트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "rotating_calipers", + "isMeta": false, + "bojTagId": 64, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "회전하는 캘리퍼스", + "short": "회전하는 캘리퍼스" + }, + { + "language": "en", + "name": "rotating calipers", + "short": "rotating calipers" + }, + { + "language": "ja", + "name": "rotating calipers", + "short": "rotating calipers" + } + ], + "aliases": [] + }, + { + "key": "euler_characteristic", + "isMeta": false, + "bojTagId": 119, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "오일러 지표 (χ=V-E+F)", + "short": "오일러 지표" + }, + { + "language": "en", + "name": "euler characteristic (χ=v-e+f)", + "short": "euler characteristic" + }, + { + "language": "ja", + "name": "オイラー特性(χ=v-e+f)", + "short": "オイラー特性" + } + ], + "aliases": [] + }, + { + "key": "regex", + "isMeta": false, + "bojTagId": 63, + "problemCount": 27, + "displayNames": [ + { + "language": "ko", + "name": "정규 표현식", + "short": "정규 표현식" + }, + { + "language": "en", + "name": "regular expression", + "short": "regex" + }, + { + "language": "ja", + "name": "正規表現", + "short": "regex" + } + ], + "aliases": [ + { + "alias": "정규식" + } + ] + }, + { + "key": "slope_trick", + "isMeta": false, + "bojTagId": 157, + "problemCount": 26, + "displayNames": [ + { + "language": "ko", + "name": "함수 개형을 이용한 최적화", + "short": "함수 개형을 이용한 최적화" + }, + { + "language": "en", + "name": "slope trick", + "short": "slope trick" + }, + { + "language": "ja", + "name": "slope trick", + "short": "slope trick" + } + ], + "aliases": [ + { + "alias": "슬로프트릭" + }, + { + "alias": "슬로프 트릭" + } + ] + }, + { + "key": "berlekamp_massey", + "isMeta": false, + "bojTagId": 110, + "problemCount": 25, + "displayNames": [ + { + "language": "ko", + "name": "벌리캠프–매시", + "short": "벌리캠프–매시" + }, + { + "language": "en", + "name": "berlekamp–massey", + "short": "berlekamp–massey" + }, + { + "language": "ja", + "name": "berlekamp–massey", + "short": "berlekamp–massey" + } + ], + "aliases": [ + { + "alias": "벌레캠프" + }, + { + "alias": "벌래캠프" + } + ] + }, + { + "key": "manacher", + "isMeta": false, + "bojTagId": 44, + "problemCount": 24, + "displayNames": [ + { + "language": "ko", + "name": "매내처", + "short": "매내처" + }, + { + "language": "en", + "name": "manacher's", + "short": "manacher's" + }, + { + "language": "ja", + "name": "manacher's", + "short": "manacher's" + } + ], + "aliases": [] + }, + { + "key": "pollard_rho", + "isMeta": false, + "bojTagId": 58, + "problemCount": 23, + "displayNames": [ + { + "language": "ko", + "name": "폴라드 로", + "short": "폴라드 로" + }, + { + "language": "en", + "name": "pollard rho", + "short": "pollard rho" + }, + { + "language": "ja", + "name": "ポラード・ロー素因数分解法", + "short": "ポラード・ロー" + } + ], + "aliases": [] + }, + { + "key": "dp_connection_profile", + "isMeta": false, + "bojTagId": 107, + "problemCount": 23, + "displayNames": [ + { + "language": "ko", + "name": "커넥션 프로파일을 이용한 다이나믹 프로그래밍", + "short": "커넥션 프로파일을 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using connection profile", + "short": "dp using connection profile" + }, + { + "language": "ja", + "name": "dynamic programming using connection profile", + "short": "dp using connection profile" + } + ], + "aliases": [ + { + "alias": "동적계획법" + } + ] + }, + { + "key": "link_cut_tree", + "isMeta": false, + "bojTagId": 98, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "링크/컷 트리", + "short": "링크/컷 트리" + }, + { + "language": "en", + "name": "link/cut tree", + "short": "link/cut tree" + }, + { + "language": "ja", + "name": "link/cut tree", + "short": "link/cut tree" + } + ], + "aliases": [ + { + "alias": "link cut tree" + }, + { + "alias": "linkcuttree" + }, + { + "alias": "링크컷" + } + ] + }, + { + "key": "merge_sort_tree", + "isMeta": false, + "bojTagId": 155, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "머지 소트 트리", + "short": "머지 소트 트리" + }, + { + "language": "en", + "name": "merge sort tree", + "short": "merge sort tree" + }, + { + "language": "ja", + "name": "マージソート木", + "short": "マージソート木" + } + ], + "aliases": [ + { + "alias": "병합정렬트리" + }, + { + "alias": "합병정렬트리" + } + ] + }, + { + "key": "tree_isomorphism", + "isMeta": false, + "bojTagId": 145, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "트리 동형 사상", + "short": "트리 동형 사상" + }, + { + "language": "en", + "name": "tree isomorphism", + "short": "tree isomorphism" + }, + { + "language": "ja", + "name": "木の同型性判定", + "short": "木の同型性判定" + } + ], + "aliases": [ + { + "alias": "graph isomorphism" + }, + { + "alias": "isomorphism" + }, + { + "alias": "topology" + }, + { + "alias": "아이소모피즘" + }, + { + "alias": "위상" + } + ] + }, + { + "key": "simulated_annealing", + "isMeta": false, + "bojTagId": 184, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "담금질 기법", + "short": "담금질 기법" + }, + { + "language": "en", + "name": "simulated annealing", + "short": "simulated annealing" + }, + { + "language": "ja", + "name": "焼き鈍し法", + "short": "焼き鈍し法" + } + ], + "aliases": [] + }, + { + "key": "hall", + "isMeta": false, + "bojTagId": 34, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "홀의 결혼 정리", + "short": "홀의 결혼 정리" + }, + { + "language": "en", + "name": "hall's theorem", + "short": "hall's thm" + }, + { + "language": "ja", + "name": "ホールの定理", + "short": "ホールの定理" + } + ], + "aliases": [] + }, + { + "key": "hungarian", + "isMeta": false, + "bojTagId": 36, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "헝가리안", + "short": "헝가리안" + }, + { + "language": "en", + "name": "hungarian", + "short": "hungarian" + }, + { + "language": "ja", + "name": "hungarian", + "short": "hungarian" + } + ], + "aliases": [ + { + "alias": "헝가리안" + }, + { + "alias": "assignment problem" + }, + { + "alias": "weighted bipartite matching" + } + ] + }, + { + "key": "flood_fill", + "isMeta": false, + "bojTagId": 210, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "플러드 필", + "short": "플러드 필" + }, + { + "language": "en", + "name": "flood-fill", + "short": "ff" + }, + { + "language": "ja", + "name": "flood-fill", + "short": "ff" + } + ], + "aliases": [] + }, + { + "key": "miller_rabin", + "isMeta": false, + "bojTagId": 47, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "밀러–라빈 소수 판별법", + "short": "밀러–라빈 소수 판별법" + }, + { + "language": "en", + "name": "miller–rabin", + "short": "miller–rabin" + }, + { + "language": "ja", + "name": "ミラー–ラビン素数判定法", + "short": "ミラー–ラビン" + } + ], + "aliases": [] + }, + { + "key": "mobius_inversion", + "isMeta": false, + "bojTagId": 51, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "뫼비우스 반전 공식", + "short": "뫼비우스 반전 공식" + }, + { + "language": "en", + "name": "möbius inversion", + "short": "möbius inversion" + }, + { + "language": "ja", + "name": "メビウスの反転公式", + "short": "メビウス" + } + ], + "aliases": [ + { + "alias": "mobius" + } + ] + }, + { + "key": "rabin_karp", + "isMeta": false, + "bojTagId": 61, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "라빈–카프", + "short": "라빈–카프" + }, + { + "language": "en", + "name": "rabin–karp", + "short": "rabin–karp" + }, + { + "language": "ja", + "name": "ラビン-カープ文字列検索", + "short": "ラビン-カープ文字列検索" + } + ], + "aliases": [ + { + "alias": "라빈카프" + } + ] + }, + { + "key": "numerical_analysis", + "isMeta": false, + "bojTagId": 122, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "수치해석", + "short": "수치해석" + }, + { + "language": "en", + "name": "numerical analysis", + "short": "numerical analysis" + }, + { + "language": "ja", + "name": "数値解析", + "short": "数値解析" + } + ], + "aliases": [ + { + "alias": "수학" + } + ] + }, + { + "key": "point_in_non_convex_polygon", + "isMeta": false, + "bojTagId": 57, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "오목 다각형 내부의 점 판정", + "short": "오목 다각형 내부의 점 판정" + }, + { + "language": "en", + "name": "point in non-convex polygon check", + "short": "point in non-convex polygon check" + }, + { + "language": "ja", + "name": "非凸多角形の点包含判定", + "short": "非凸多角形の点包含判定" + } + ], + "aliases": [] + }, + { + "key": "alien", + "isMeta": false, + "bojTagId": 134, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "Aliens 트릭", + "short": "aliens 트릭" + }, + { + "language": "en", + "name": "aliens trick", + "short": "aliens trick" + }, + { + "language": "ja", + "name": "aliens法", + "short": "aliens法" + } + ], + "aliases": [ + { + "alias": "alien's trick" + } + ] + }, + { + "key": "linear_programming", + "isMeta": false, + "bojTagId": 103, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "선형 계획법", + "short": "선형 계획법" + }, + { + "language": "en", + "name": "linear programming", + "short": "lp" + }, + { + "language": "ja", + "name": "線型計画法", + "short": "lp" + } + ], + "aliases": [ + { + "alias": "리니어프로그래밍" + } + ] + }, + { + "key": "generating_function", + "isMeta": false, + "bojTagId": 198, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "생성 함수", + "short": "생성 함수" + }, + { + "language": "en", + "name": "generating function", + "short": "generating function" + }, + { + "language": "ja", + "name": "生成関数", + "short": "生成関数" + } + ], + "aliases": [] + }, + { + "key": "offline_dynamic_connectivity", + "isMeta": false, + "bojTagId": 52, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "오프라인 동적 연결성 판정", + "short": "오프라인 동적 연결성 판정" + }, + { + "language": "en", + "name": "offline dynamic connectivity", + "short": "offline dynamic connectivity" + }, + { + "language": "ja", + "name": "offline dynamic connectivity", + "short": "offline dynamic connectivity" + } + ], + "aliases": [] + }, + { + "key": "statistics", + "isMeta": false, + "bojTagId": 178, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "통계학", + "short": "통계학" + }, + { + "language": "en", + "name": "statistics", + "short": "stats" + }, + { + "language": "ja", + "name": "統計学", + "short": "統計" + } + ], + "aliases": [ + { + "alias": "average" + }, + { + "alias": "평균" + }, + { + "alias": "variance" + }, + { + "alias": "분산" + }, + { + "alias": "표준편차" + }, + { + "alias": "표준 편차" + } + ] + }, + { + "key": "functional_graph", + "isMeta": false, + "bojTagId": 211, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "함수형 그래프", + "short": "함수형 그래프" + }, + { + "language": "en", + "name": "functional graph", + "short": "functional graph" + }, + { + "language": "ja", + "name": "functional graph", + "short": "functional graph" + } + ], + "aliases": [] + }, + { + "key": "dp_sum_over_subsets", + "isMeta": false, + "bojTagId": 207, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "부분집합의 합 다이나믹 프로그래밍", + "short": "부분집합의 합" + }, + { + "language": "en", + "name": "sum over subsets dynamic programming", + "short": "sos dp" + }, + { + "language": "ja", + "name": "sum over subsets dynamic programming", + "short": "sos dp" + } + ], + "aliases": [ + { + "alias": "sos" + } + ] + }, + { + "key": "circulation", + "isMeta": false, + "bojTagId": 191, + "problemCount": 16, + "displayNames": [ + { + "language": "ko", + "name": "서큘레이션", + "short": "서큘레이션" + }, + { + "language": "en", + "name": "circulation", + "short": "circulation" + }, + { + "language": "ja", + "name": "circulation", + "short": "circulation" + } + ], + "aliases": [] + }, + { + "key": "tree_compression", + "isMeta": false, + "bojTagId": 193, + "problemCount": 16, + "displayNames": [ + { + "language": "ko", + "name": "트리 압축", + "short": "트리 압축" + }, + { + "language": "en", + "name": "tree compression", + "short": "tree compression" + }, + { + "language": "ja", + "name": "tree compression", + "short": "tree compression" + } + ], + "aliases": [] + }, + { + "key": "voronoi", + "isMeta": false, + "bojTagId": 82, + "problemCount": 15, + "displayNames": [ + { + "language": "ko", + "name": "보로노이 다이어그램", + "short": "보로노이 다이어그램" + }, + { + "language": "en", + "name": "voronoi diagram", + "short": "voronoi diagram" + }, + { + "language": "ja", + "name": "ボロノイ図", + "short": "ボロノイ図" + } + ], + "aliases": [] + }, + { + "key": "duality", + "isMeta": false, + "bojTagId": 180, + "problemCount": 14, + "displayNames": [ + { + "language": "ko", + "name": "쌍대성", + "short": "쌍대성" + }, + { + "language": "en", + "name": "duality", + "short": "duality" + }, + { + "language": "ja", + "name": "双対性", + "short": "双対性" + } + ], + "aliases": [ + { + "alias": "듀얼리티" + } + ] + }, + { + "key": "dual_graph", + "isMeta": false, + "bojTagId": 181, + "problemCount": 14, + "displayNames": [ + { + "language": "ko", + "name": "쌍대 그래프", + "short": "쌍대 그래프" + }, + { + "language": "en", + "name": "dual graph", + "short": "dual graph" + }, + { + "language": "ja", + "name": "双対グラフ", + "short": "双対グラフ" + } + ], + "aliases": [ + { + "alias": "듀얼 그래프" + } + ] + }, + { + "key": "lucas", + "isMeta": false, + "bojTagId": 113, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "뤼카 정리", + "short": "뤼카 정리" + }, + { + "language": "en", + "name": "lucas theorem", + "short": "lucas thm" + }, + { + "language": "ja", + "name": "lucas theorem", + "short": "lucas thm" + } + ], + "aliases": [] + }, + { + "key": "matroid", + "isMeta": false, + "bojTagId": 104, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "매트로이드", + "short": "매트로이드" + }, + { + "language": "en", + "name": "matroid", + "short": "matroid" + }, + { + "language": "ja", + "name": "マトロイド", + "short": "マトロイド" + } + ], + "aliases": [] + }, + { + "key": "dp_digit", + "isMeta": false, + "bojTagId": 217, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "자릿수를 이용한 다이나믹 프로그래밍", + "short": "자릿수 dp" + }, + { + "language": "en", + "name": "digit dp", + "short": "digit dp" + } + ], + "aliases": [] + }, + { + "key": "kitamasa", + "isMeta": false, + "bojTagId": 112, + "problemCount": 12, + "displayNames": [ + { + "language": "ko", + "name": "키타마사", + "short": "키타마사" + }, + { + "language": "en", + "name": "kitamasa", + "short": "kitamasa" + }, + { + "language": "ja", + "name": "きたまさ法", + "short": "きたまさ法" + } + ], + "aliases": [] + }, + { + "key": "cartesian_tree", + "isMeta": false, + "bojTagId": 206, + "problemCount": 11, + "displayNames": [ + { + "language": "ko", + "name": "데카르트 트리", + "short": "데카르트 트리" + }, + { + "language": "en", + "name": "cartesian tree", + "short": "cartesian tree" + }, + { + "language": "ja", + "name": "デカルト木", + "short": "デカルト木" + } + ], + "aliases": [] + }, + { + "key": "general_matching", + "isMeta": false, + "bojTagId": 15, + "problemCount": 11, + "displayNames": [ + { + "language": "ko", + "name": "일반적인 매칭", + "short": "일반적인 매칭" + }, + { + "language": "en", + "name": "general matching", + "short": "general matching" + }, + { + "language": "ja", + "name": "一般的なマッチング", + "short": "一般的なマッチング" + } + ], + "aliases": [ + { + "alias": "블라썸" + }, + { + "alias": "블러썸" + }, + { + "alias": "블라섬" + }, + { + "alias": "블러섬" + }, + { + "alias": "blossom" + }, + { + "alias": "부합" + } + ] + }, + { + "key": "tree_decomposition", + "isMeta": false, + "bojTagId": 204, + "problemCount": 10, + "displayNames": [ + { + "language": "ko", + "name": "트리 분할", + "short": "트리 분할" + }, + { + "language": "en", + "name": "tree decomposition", + "short": "tree decomposition" + }, + { + "language": "ja", + "name": "tree decomposition", + "short": "tree decomposition" + } + ], + "aliases": [] + }, + { + "key": "burnside", + "isMeta": false, + "bojTagId": 16, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "번사이드 보조정리", + "short": "번사이드 보조정리" + }, + { + "language": "en", + "name": "burnside's lemma", + "short": "burnside's lemma" + }, + { + "language": "ja", + "name": "バーンサイドの補題", + "short": "バーンサイド" + } + ], + "aliases": [] + }, + { + "key": "discrete_log", + "isMeta": false, + "bojTagId": 146, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "이산 로그", + "short": "이산 로그" + }, + { + "language": "en", + "name": "discrete logarithm", + "short": "discrete logarithm" + }, + { + "language": "ja", + "name": "離散対数", + "short": "離散対数" + } + ], + "aliases": [ + { + "alias": "order" + } + ] + }, + { + "key": "geometry_hyper", + "isMeta": false, + "bojTagId": 132, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "4차원 이상의 기하학", + "short": "4차원 이상의 기하학" + }, + { + "language": "en", + "name": "geometry; hyperdimensional", + "short": "hyperdimensional" + }, + { + "language": "ja", + "name": "4次元以上での幾何学", + "short": "hyperdimensional" + } + ], + "aliases": [ + { + "alias": "4차원" + }, + { + "alias": "5차원" + }, + { + "alias": "6차원" + }, + { + "alias": "7차원" + }, + { + "alias": "8차원" + }, + { + "alias": "9차원" + }, + { + "alias": "4d" + }, + { + "alias": "5d" + }, + { + "alias": "6d" + }, + { + "alias": "7d" + }, + { + "alias": "8d" + }, + { + "alias": "9d" + } + ] + }, + { + "key": "bidirectional_search", + "isMeta": false, + "bojTagId": 129, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "양방향 탐색", + "short": "양방향 탐색" + }, + { + "language": "en", + "name": "bidirectional search", + "short": "bidirectional search" + }, + { + "language": "ja", + "name": "bidirectional search", + "short": "bidirectional search" + } + ], + "aliases": [] + }, + { + "key": "min_enclosing_circle", + "isMeta": false, + "bojTagId": 162, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "최소 외접원", + "short": "최소 외접원" + }, + { + "language": "en", + "name": "minimum enclosing circle", + "short": "minimum enclosing circle" + }, + { + "language": "ja", + "name": "最小外接円", + "short": "最小外接円" + } + ], + "aliases": [ + { + "alias": "bounding circle" + }, + { + "alias": "smallest enclosing circle" + }, + { + "alias": "minimum covering circle" + } + ] + }, + { + "key": "z", + "isMeta": false, + "bojTagId": 83, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "z", + "short": "Z" + }, + { + "language": "en", + "name": "z", + "short": "z" + }, + { + "language": "ja", + "name": "z", + "short": "z" + } + ], + "aliases": [] + }, + { + "key": "pick", + "isMeta": false, + "bojTagId": 187, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "픽의 정리", + "short": "픽" + }, + { + "language": "en", + "name": "pick's theorem", + "short": "pick's thm" + }, + { + "language": "ja", + "name": "ピックの定理", + "short": "ピック" + } + ], + "aliases": [] + }, + { + "key": "utf8", + "isMeta": false, + "bojTagId": 199, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "utf-8 입력 처리", + "short": "utf-8" + }, + { + "language": "en", + "name": "utf-8 inputs", + "short": "utf-8" + }, + { + "language": "ja", + "name": "utf-8入力の処理", + "short": "utf-8" + } + ], + "aliases": [] + }, + { + "key": "top_tree", + "isMeta": false, + "bojTagId": 105, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "탑 트리", + "short": "탑 트리" + }, + { + "language": "en", + "name": "top tree", + "short": "top tree" + }, + { + "language": "ja", + "name": "top tree", + "short": "top tree" + } + ], + "aliases": [] + }, + { + "key": "palindrome_tree", + "isMeta": false, + "bojTagId": 53, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "회문 트리", + "short": "회문 트리" + }, + { + "language": "en", + "name": "palindrome tree", + "short": "palindrome tree" + }, + { + "language": "ja", + "name": "palindrome tree", + "short": "palindrome tree" + } + ], + "aliases": [ + { + "alias": "팰린드롬트리" + } + ] + }, + { + "key": "monotone_queue_optimization", + "isMeta": false, + "bojTagId": 165, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "단조 큐를 이용한 최적화", + "short": "단조 큐를 이용한 최적화" + }, + { + "language": "en", + "name": "monotone queue optimization", + "short": "monotone queue optimization" + }, + { + "language": "ja", + "name": "monotone queue optimization", + "short": "monotone queue optimization" + } + ], + "aliases": [] + }, + { + "key": "knuth_x", + "isMeta": false, + "bojTagId": 174, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "크누스 X", + "short": "크누스 X" + }, + { + "language": "en", + "name": "knuth's x", + "short": "x" + }, + { + "language": "ja", + "name": "knuth's x", + "short": "x" + } + ], + "aliases": [] + }, + { + "key": "delaunay", + "isMeta": false, + "bojTagId": 21, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "델로네 삼각분할", + "short": "델로네 삼각분할" + }, + { + "language": "en", + "name": "delaunay triangulation", + "short": "delaunay triangulation" + }, + { + "language": "ja", + "name": "ドロネー三角形分割", + "short": "ドロネー" + } + ], + "aliases": [] + }, + { + "key": "dominator_tree", + "isMeta": false, + "bojTagId": 135, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "도미네이터 트리", + "short": "도미네이터 트리" + }, + { + "language": "en", + "name": "dominator tree", + "short": "dominator tree" + }, + { + "language": "ja", + "name": "dominator tree", + "short": "dominator tree" + } + ], + "aliases": [] + }, + { + "key": "stable_marriage", + "isMeta": false, + "bojTagId": 192, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "안정 결혼 문제", + "short": "안정 결혼" + }, + { + "language": "en", + "name": "stable marriage problem", + "short": "smp" + }, + { + "language": "ja", + "name": "stable marriage problem", + "short": "smp" + } + ], + "aliases": [] + }, + { + "key": "rope", + "isMeta": false, + "bojTagId": 159, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "로프", + "short": "로프" + }, + { + "language": "en", + "name": "rope", + "short": "rope" + }, + { + "language": "ja", + "name": "rope", + "short": "rope" + } + ], + "aliases": [] + }, + { + "key": "bayes", + "isMeta": false, + "bojTagId": 114, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "베이즈 정리", + "short": "베이즈 정리" + }, + { + "language": "en", + "name": "bayes theorem", + "short": "bayes thm" + }, + { + "language": "ja", + "name": "ベイズの定理", + "short": "ベイズ" + } + ], + "aliases": [ + { + "alias": "조건부확률" + } + ] + }, + { + "key": "knuth", + "isMeta": false, + "bojTagId": 90, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "크누스 최적화", + "short": "크누스 최적화" + }, + { + "language": "en", + "name": "knuth optimization", + "short": "knuth" + }, + { + "language": "ja", + "name": "knuth optimization", + "short": "knuth" + } + ], + "aliases": [] + }, + { + "key": "dancing_links", + "isMeta": false, + "bojTagId": 173, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "춤추는 링크", + "short": "춤추는 링크" + }, + { + "language": "en", + "name": "dancing links", + "short": "dancing links" + }, + { + "language": "ja", + "name": "dancing links", + "short": "dancing links" + } + ], + "aliases": [] + }, + { + "key": "degree_sequence", + "isMeta": false, + "bojTagId": 200, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "차수열", + "short": "차수열" + }, + { + "language": "en", + "name": "degree sequence", + "short": "degree sequence" + }, + { + "language": "ja", + "name": "degree sequence", + "short": "degree sequence" + } + ], + "aliases": [] + }, + { + "key": "differential_cryptanalysis", + "isMeta": false, + "bojTagId": 185, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "차분 공격", + "short": "차분 공격" + }, + { + "language": "en", + "name": "differential cryptanalysis", + "short": "differential cryptanalysis" + }, + { + "language": "ja", + "name": "differential cryptanalysis", + "short": "differential cryptanalysis" + } + ], + "aliases": [ + { + "alias": "dc" + } + ] + }, + { + "key": "geometric_boolean_operations", + "isMeta": false, + "bojTagId": 202, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "도형에서의 불 연산", + "short": "도형에서의 불 연산" + }, + { + "language": "en", + "name": "boolean operations on geometric objects", + "short": "geometric boolean operations" + }, + { + "language": "ja", + "name": "図形のブール演算", + "short": "図形のブール演算" + } + ], + "aliases": [ + { + "alias": "병합" + }, + { + "alias": "교집합" + }, + { + "alias": "합집합" + }, + { + "alias": "union" + }, + { + "alias": "intersect" + } + ] + }, + { + "key": "hirschberg", + "isMeta": false, + "bojTagId": 163, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "히르쉬버그", + "short": "히르쉬버그" + }, + { + "language": "en", + "name": "hirschberg's", + "short": "hirschberg's" + }, + { + "language": "ja", + "name": "hirschberg's", + "short": "hirschberg's" + } + ], + "aliases": [ + { + "alias": "hirschburg" + } + ] + }, + { + "key": "suffix_tree", + "isMeta": false, + "bojTagId": 182, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "접미사 트리", + "short": "접미사 트리" + }, + { + "language": "en", + "name": "suffix tree", + "short": "suffix tree" + }, + { + "language": "ja", + "name": "suffix tree", + "short": "suffix tree" + } + ], + "aliases": [] + }, + { + "key": "chordal_graph", + "isMeta": false, + "bojTagId": 201, + "problemCount": 5, + "displayNames": [ + { + "language": "en", + "name": "chordal graph", + "short": "chordal graph" + }, + { + "language": "ko", + "name": "현 그래프", + "short": "현 그래프" + }, + { + "language": "ja", + "name": "弦グラフ", + "short": "弦グラフ" + } + ], + "aliases": [] + }, + { + "key": "discrete_sqrt", + "isMeta": false, + "bojTagId": 147, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "이산 제곱근", + "short": "이산 제곱근" + }, + { + "language": "en", + "name": "discrete square root", + "short": "discrete square root" + }, + { + "language": "ja", + "name": "離散平方根", + "short": "離散平方根" + } + ], + "aliases": [ + { + "alias": "루트" + } + ] + }, + { + "key": "gradient_descent", + "isMeta": false, + "bojTagId": 208, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "경사 하강법", + "short": "경사 하강법" + }, + { + "language": "en", + "name": "gradient descent", + "short": "gradient descent" + }, + { + "language": "ja", + "name": "勾配降下法", + "short": "勾配降下法" + } + ], + "aliases": [] + }, + { + "key": "polynomial_interpolation", + "isMeta": false, + "bojTagId": 209, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "다항식 보간법", + "short": "다항식 보간법" + }, + { + "language": "en", + "name": "polynomial interpolation", + "short": "polynomial interpolation" + }, + { + "language": "ja", + "name": "多項式補間", + "short": "多項式補間" + } + ], + "aliases": [] + }, + { + "key": "lgv", + "isMeta": false, + "bojTagId": 214, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "린드스트롬–게셀–비엔노 보조정리", + "short": "lgv 보조정리" + }, + { + "language": "en", + "name": "lindström–gessel–viennot lemma", + "short": "lgv lemma" + }, + { + "language": "ja", + "name": "lindström–gessel–viennot lemma", + "short": "lgv lemma" + } + ], + "aliases": [] + }, + { + "key": "green", + "isMeta": false, + "bojTagId": 183, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "그린 정리", + "short": "그린" + }, + { + "language": "en", + "name": "green's theorem", + "short": "green's thm" + }, + { + "language": "ja", + "name": "グリーンの定理", + "short": "グリーン" + } + ], + "aliases": [] + }, + { + "key": "directed_mst", + "isMeta": false, + "bojTagId": 23, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "유향 최소 신장 트리", + "short": "유향 최소 신장 트리" + }, + { + "language": "en", + "name": "directed minimum spanning tree", + "short": "dmst" + }, + { + "language": "ja", + "name": "最小全域有向木", + "short": "dmst" + } + ], + "aliases": [ + { + "alias": "유향mst" + } + ] + }, + { + "key": "stoer_wagner", + "isMeta": false, + "bojTagId": 75, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "스토어–바그너", + "short": "스토어–바그너" + }, + { + "language": "en", + "name": "stoer–wagner", + "short": "stoer–wagner" + }, + { + "language": "ja", + "name": "stoer–wagner", + "short": "stoer–wagner" + } + ], + "aliases": [ + { + "alias": "stoer-wagner" + }, + { + "alias": "stoer-karger" + }, + { + "alias": "stoer" + }, + { + "alias": "wagner" + }, + { + "alias": "karger" + }, + { + "alias": "global min cut" + }, + { + "alias": "전역 최소 컷" + } + ] + }, + { + "key": "birthday", + "isMeta": false, + "bojTagId": 203, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "생일 문제", + "short": "생일" + }, + { + "language": "en", + "name": "birthday problem", + "short": "birthday" + }, + { + "language": "ja", + "name": "birthday problem", + "short": "birthday" + } + ], + "aliases": [ + { + "alias": "패러독스" + }, + { + "alias": "파라독스" + }, + { + "alias": "birthday" + } + ] + }, + { + "key": "majority_vote", + "isMeta": false, + "bojTagId": 160, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "보이어–무어 다수결 투표", + "short": "보이어–무어 다수결 투표" + }, + { + "language": "en", + "name": "boyer–moore majority vote", + "short": "majority vote" + }, + { + "language": "ja", + "name": "boyer–moore majority vote", + "short": "majority vote" + } + ], + "aliases": [] + }, + { + "key": "multipoint_evaluation", + "isMeta": false, + "bojTagId": 196, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "다중 대입값 계산", + "short": "다중 계산" + }, + { + "language": "en", + "name": "multipoint evaluation", + "short": "multipoint evaluation" + }, + { + "language": "ja", + "name": "多点評価", + "short": "多点評価" + } + ], + "aliases": [] + }, + { + "key": "lte", + "isMeta": false, + "bojTagId": 212, + "problemCount": 2, + "displayNames": [ + { + "language": "ko", + "name": "지수승강 보조정리", + "short": "지수승강" + }, + { + "language": "en", + "name": "lifting the exponent lemma", + "short": "lte lemma" + }, + { + "language": "ja", + "name": "lifting the exponent lemma", + "short": "lte lemma" + } + ], + "aliases": [] + }, + { + "key": "hackenbush", + "isMeta": false, + "bojTagId": 205, + "problemCount": 2, + "displayNames": [ + { + "language": "ko", + "name": "하켄부시 게임", + "short": "하켄부시 게임" + }, + { + "language": "en", + "name": "hackenbush", + "short": "hackenbush" + }, + { + "language": "ja", + "name": "hackenbush", + "short": "hackenbush" + } + ], + "aliases": [] + }, + { + "key": "rb_tree", + "isMeta": false, + "bojTagId": 94, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "레드-블랙 트리", + "short": "레드-블랙 트리" + }, + { + "language": "en", + "name": "red-black tree", + "short": "rb tree" + }, + { + "language": "ja", + "name": "red-black tree", + "short": "rb tree" + } + ], + "aliases": [ + { + "alias": "rb트리" + } + ] + }, + { + "key": "floor_sum", + "isMeta": false, + "bojTagId": 218, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "유리 등차수열의 내림 합", + "short": "유리 등차수열의 내림 합" + }, + { + "language": "en", + "name": "sum of floor of rational arithmetic sequence", + "short": "floor sum" + } + ], + "aliases": [] + }, + { + "key": "discrete_kth_root", + "isMeta": false, + "bojTagId": 149, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "이산 k제곱근", + "short": "이산 k제곱근" + }, + { + "language": "en", + "name": "discrete k-th root", + "short": "discrete k-th root" + }, + { + "language": "ja", + "name": "離散k平方根", + "short": "離散k平方根" + } + ], + "aliases": [ + { + "alias": "루트" + } + ] + }, + { + "key": "a_star", + "isMeta": false, + "bojTagId": 186, + "problemCount": 0, + "displayNames": [ + { + "language": "ko", + "name": "a*", + "short": "a*" + }, + { + "language": "en", + "name": "a*", + "short": "a*" + }, + { + "language": "ja", + "name": "a*", + "short": "a*" + } + ], + "aliases": [ + { + "alias": "에이" + }, + { + "alias": "에이스타" + } + ] + } + ] +} \ No newline at end of file From 07debbd27254db06fc1e4c09fd8fb297b4bbc1df Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:02:47 +0900 Subject: [PATCH 037/552] feat(config): Add rest_framework to INSTALLED_APPS --- src/config/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/settings.py b/src/config/settings.py index c77c68d..37880af 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -40,6 +40,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "rest_framework", "boj", "core", "crew", From 90d8175c025ee6fc687fdff68224bb855306608a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:16:48 +0900 Subject: [PATCH 038/552] refactor(core): Rename boj_tag_id field to boj_id in DSA model --- src/core/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/models.py b/src/core/models.py index 1a7e26a..111755d 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -9,7 +9,7 @@ class Difficulty(models.IntegerChoices): class DSA(models.Model): """Data Structure & Algorithm""" - boj_tag_id = models.IntegerField( + boj_id = models.IntegerField( unique=True, help_text=( '백준 태그 ID를 입력해주세요.' From e0d0ac987083c666c1c04eb72413504ea0dad53a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:20:00 +0900 Subject: [PATCH 039/552] feat(config): Set API version prefix to "api/v1" --- src/config/urls.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/urls.py b/src/config/urls.py index 1ab69c9..218e9a9 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -19,6 +19,9 @@ from django.contrib import admin from django.urls import path + +API_VERSION_PREFIX = "api/v1" + urlpatterns = [ path("admin/", admin.site.urls), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From 51e4a754ef4dd7ca21c40adede425301efa83afe Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:42:14 +0900 Subject: [PATCH 040/552] =?UTF-8?q?refactor(core,=20boj):=20DSA=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EC=97=90=EC=84=9C=20boj=5Fid=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=EC=9D=84=20BOJTag=20=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/boj/models.py | 20 ++++++++++++++++++++ src/core/models.py | 24 +++++------------------- tools/db_setup.py | 7 ++++++- 3 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/boj/models.py b/src/boj/models.py index 29b5cf5..b7ec13a 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -1,5 +1,6 @@ from django.db import models +from core.models import DSA from user.models import User @@ -72,3 +73,22 @@ class BOJUser(models.Model): ), auto_now=True, ) + + +class BOJTag(models.Model): + tag = models.OneToOneField( + DSA, + on_delete=models.CASCADE, + related_name='boj_tag', + help_text=( + '이 태그와 연결된 알고리즘 태그를 입력해주세요.' + ), + ) + boj_id = models.IntegerField( + unique=True, + help_text=( + '백준 태그 ID를 입력해주세요.' + ), + null=True, + default=None, + ) diff --git a/src/core/models.py b/src/core/models.py index 111755d..7b47451 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -9,13 +9,14 @@ class Difficulty(models.IntegerChoices): class DSA(models.Model): """Data Structure & Algorithm""" - boj_id = models.IntegerField( - unique=True, + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + related_name='children', help_text=( - '백준 태그 ID를 입력해주세요.' + '부모 알고리즘 태그를 입력해주세요.' ), null=True, - default=None, ) key = models.CharField( max_length=20, @@ -38,21 +39,6 @@ class DSA(models.Model): '알고리즘 태그 이름(영문)을 입력해주세요. (최대 20자)' ), ) - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - related_name='children', - help_text=( - '부모 알고리즘 태그를 입력해주세요.' - ), - null=True, - ) - is_group = models.BooleanField( - default=False, - help_text=( - '그룹인지 여부를 입력해주세요.' - ), - ) class Language(models.Model): diff --git a/tools/db_setup.py b/tools/db_setup.py index dcd4bca..1e3d47e 100644 --- a/tools/db_setup.py +++ b/tools/db_setup.py @@ -45,13 +45,18 @@ def load_tags(file='tools/tags.json') -> List[Tag]: from django.db.transaction import atomic + +from boj.models import BOJTag from core.models import DSA with atomic(): for tag in load_tags(): dsa = DSA.objects.get_or_create(key=tag.key)[0] - dsa.boj_tag_id = tag.bojTagId dsa.name_ko = next(filter(lambda x: x.language == 'ko', tag.displayNames)).name dsa.name_en = next(filter(lambda x: x.language == 'en', tag.displayNames)).name dsa.save() + + boj_tag = BOJTag.objects.get_or_create(boj_id=tag.bojTagId)[0] + boj_tag.tag = dsa + boj_tag.save() From 6c3acf6a3b3f2898f409d98373f26af20c325009 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:44:18 +0900 Subject: [PATCH 041/552] refactor(core): Rename model DSA to Tag --- src/boj/models.py | 4 ++-- src/core/models.py | 2 +- src/problem/models.py | 4 ++-- tools/db_setup.py | 19 +++++++++---------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/boj/models.py b/src/boj/models.py index b7ec13a..d2b8dc1 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -1,6 +1,6 @@ from django.db import models -from core.models import DSA +from core.models import Tag from user.models import User @@ -77,7 +77,7 @@ class BOJUser(models.Model): class BOJTag(models.Model): tag = models.OneToOneField( - DSA, + Tag, on_delete=models.CASCADE, related_name='boj_tag', help_text=( diff --git a/src/core/models.py b/src/core/models.py index 7b47451..b0e6e27 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -7,7 +7,7 @@ class Difficulty(models.IntegerChoices): HARD = 3, '어려움' -class DSA(models.Model): +class Tag(models.Model): """Data Structure & Algorithm""" parent = models.ForeignKey( 'self', diff --git a/src/problem/models.py b/src/problem/models.py index 66fd52e..8fa5cd8 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -1,7 +1,7 @@ from django.db import models from core.models import Difficulty -from core.models import DSA +from core.models import Tag from user.models import User @@ -77,7 +77,7 @@ class ProblemAnalysis(models.Model): choices=Difficulty.choices, ) dsa_tags = models.ManyToManyField( - DSA, + Tag, related_name='problems', help_text=( '문제의 DSA 태그를 입력해주세요.' diff --git a/tools/db_setup.py b/tools/db_setup.py index 1e3d47e..e4e6ab8 100644 --- a/tools/db_setup.py +++ b/tools/db_setup.py @@ -31,7 +31,6 @@ class Tag: aliases: List[Alias] - def load_tags(file='tools/tags.json') -> List[Tag]: with open(file) as f: raw_tags = json.load(f) @@ -47,16 +46,16 @@ def load_tags(file='tools/tags.json') -> List[Tag]: from django.db.transaction import atomic from boj.models import BOJTag -from core.models import DSA +from core.models import Tag with atomic(): - for tag in load_tags(): - dsa = DSA.objects.get_or_create(key=tag.key)[0] - dsa.name_ko = next(filter(lambda x: x.language == 'ko', tag.displayNames)).name - dsa.name_en = next(filter(lambda x: x.language == 'en', tag.displayNames)).name - dsa.save() - - boj_tag = BOJTag.objects.get_or_create(boj_id=tag.bojTagId)[0] - boj_tag.tag = dsa + for tag_data in load_tags(): + tag = Tag.objects.get_or_create(key=tag_data.key)[0] + tag.name_ko = next(filter(lambda x: x.language == 'ko', tag_data.displayNames)).name + tag.name_en = next(filter(lambda x: x.language == 'en', tag_data.displayNames)).name + tag.save() + + boj_tag = BOJTag.objects.get_or_create(boj_id=tag_data.bojTagId)[0] + boj_tag.tag = tag boj_tag.save() From 59fb0abd81b4d48284993250ce7b534f8d743270 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 15:58:29 +0900 Subject: [PATCH 042/552] feat(config): Add boj, core, crew, problem, and user apps to INSTALLED_APPS --- src/config/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/settings.py b/src/config/settings.py index 4bf6cd4..eb3a022 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -37,6 +37,11 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "boj", + "core", + "crew", + "problem", + "user", ] MIDDLEWARE = [ From b33cebb7d90789fc4d613568f3942a80d89365df Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 16:46:16 +0900 Subject: [PATCH 043/552] refactor(tools): rename db_setup.py -> setup_db.py --- tools/{db_setup.py => setup_db.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tools/{db_setup.py => setup_db.py} (100%) diff --git a/tools/db_setup.py b/tools/setup_db.py similarity index 100% rename from tools/db_setup.py rename to tools/setup_db.py From 9057214dbf61757518cef096fcf9738f2f12ce33 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 19:50:15 +0900 Subject: [PATCH 044/552] refactor(core): Increase max length for name_ko and name_en fields in Tag model --- src/core/models.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/models.py b/src/core/models.py index b0e6e27..3854fc3 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -17,26 +17,27 @@ class Tag(models.Model): '부모 알고리즘 태그를 입력해주세요.' ), null=True, + blank=True, ) key = models.CharField( - max_length=20, + max_length=50, unique=True, help_text=( '알고리즘 태그 키를 입력해주세요. (최대 20자)' ), ) name_ko = models.CharField( - max_length=20, + max_length=50, unique=True, help_text=( - '알고리즘 태그 이름(국문)을 입력해주세요. (최대 20자)' + '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' ), ) name_en = models.CharField( - max_length=20, + max_length=50, unique=True, help_text=( - '알고리즘 태그 이름(영문)을 입력해주세요. (최대 20자)' + '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' ), ) From 7a6744e6ad25f243a5547c54a10fa5d8303ed162 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 19:50:27 +0900 Subject: [PATCH 045/552] refactor(tools): Update setup_db.py to load tags from JSON file and update Tag and BOJTag models --- tools/setup_db.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/tools/setup_db.py b/tools/setup_db.py index e4e6ab8..54250c1 100644 --- a/tools/setup_db.py +++ b/tools/setup_db.py @@ -10,35 +10,35 @@ @dataclasses.dataclass -class DisplayName: +class DisplayNameJSON: language: str name: str short: str @dataclasses.dataclass -class Alias: +class AliasJSON: alias: str @dataclasses.dataclass -class Tag: +class TagJSON: key: str isMeta: bool bojTagId: int problemCount: int - displayNames: List[DisplayName] - aliases: List[Alias] + displayNames: List[DisplayNameJSON] + aliases: List[AliasJSON] -def load_tags(file='tools/tags.json') -> List[Tag]: +def load_tags(file='../tools/tags.json') -> List[TagJSON]: with open(file) as f: raw_tags = json.load(f) tags = [] for item in raw_tags['items']: - tag = Tag(**item) - tag.displayNames = [DisplayName(**display_name) for display_name in item["displayNames"]] - tag.aliases = [Alias(**alias) for alias in item["aliases"]] + tag = TagJSON(**item) + tag.displayNames = [DisplayNameJSON(**display_name) for display_name in item["displayNames"]] + tag.aliases = [AliasJSON(**alias) for alias in item["aliases"]] tags.append(tag) return tags @@ -49,13 +49,18 @@ def load_tags(file='tools/tags.json') -> List[Tag]: from core.models import Tag +tag_data_list = load_tags() + with atomic(): - for tag_data in load_tags(): + for tag_data in tag_data_list: tag = Tag.objects.get_or_create(key=tag_data.key)[0] tag.name_ko = next(filter(lambda x: x.language == 'ko', tag_data.displayNames)).name tag.name_en = next(filter(lambda x: x.language == 'en', tag_data.displayNames)).name + tag.full_clean() tag.save() - - boj_tag = BOJTag.objects.get_or_create(boj_id=tag_data.bojTagId)[0] - boj_tag.tag = tag + boj_tag = BOJTag.objects.get_or_create( + boj_id=tag_data.bojTagId, + tag=tag, + )[0] + boj_tag.full_clean() boj_tag.save() From 9c9348a9c7b1082f377207bbceed562e0f2ced99 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 19:56:14 +0900 Subject: [PATCH 046/552] feat(core): Add key for Language --- src/core/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/models.py b/src/core/models.py index 3854fc3..89671fa 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -43,6 +43,13 @@ class Tag(models.Model): class Language(models.Model): + key = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 키를 입력해주세요. (최대 20자)' + ), + ) name = models.CharField( max_length=20, unique=True, From dbfe6566502b36b4c56e419e95ef645f653936a3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 20:01:00 +0900 Subject: [PATCH 047/552] feat(core): Add extension field to Language model --- src/core/models.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/models.py b/src/core/models.py index 89671fa..065a627 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -57,3 +57,9 @@ class Language(models.Model): '언어 이름을 입력해주세요. (최대 20자)' ), ) + extension = models.CharField( + max_length=20, + help_text=( + '언어 확장자를 입력해주세요. (최대 20자)' + ), + ) From c073c0a9beb9aaf1fc19c7d79e7969d57cd980af Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 20:02:42 +0900 Subject: [PATCH 048/552] feat(tools): Add languages.json file for storing programming languages data --- tools/languages.json | 46 ++++++++++++++++++++++++++++++++++++++++++++ tools/setup_db.py | 34 +++++++++++++++++++++++++++++--- 2 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tools/languages.json diff --git a/tools/languages.json b/tools/languages.json new file mode 100644 index 0000000..9537e6a --- /dev/null +++ b/tools/languages.json @@ -0,0 +1,46 @@ +{ + "items": [ + { + "key": "nodejs", + "bojId": 17, + "displayName": "Node.js", + "extension": ".js" + }, + { + "key": "kotlin", + "bojId": 69, + "displayName": "Kotlin", + "extension": ".kt" + }, + { + "key": "swift", + "bojId": 74, + "displayName": "Swift", + "extension": ".swift" + }, + { + "key": "cpp", + "bojId": 1001, + "displayName": "C++", + "extension": ".cpp" + }, + { + "key": "java", + "bojId": 1002, + "displayName": "Java", + "extension": ".java" + }, + { + "key": "python", + "bojId": 1003, + "displayName": "Python", + "extension": ".py" + }, + { + "key": "c", + "bojId": 1004, + "displayName": "C", + "extension": ".c" + } + ] +} \ No newline at end of file diff --git a/tools/setup_db.py b/tools/setup_db.py index 54250c1..dba79ee 100644 --- a/tools/setup_db.py +++ b/tools/setup_db.py @@ -31,6 +31,14 @@ class TagJSON: aliases: List[AliasJSON] +@dataclasses.dataclass +class LanguageJSON: + key: str + bojId: int + displayName: str + extension: str + + def load_tags(file='../tools/tags.json') -> List[TagJSON]: with open(file) as f: raw_tags = json.load(f) @@ -43,16 +51,24 @@ def load_tags(file='../tools/tags.json') -> List[TagJSON]: return tags +def load_languages(file='../tools/languages.json') -> List[str]: + with open(file) as f: + raw_languages = json.load(f) + languages = [] + for item in raw_languages['items']: + languages.append(LanguageJSON(**item)) + return languages + + from django.db.transaction import atomic from boj.models import BOJTag +from core.models import Language from core.models import Tag -tag_data_list = load_tags() - with atomic(): - for tag_data in tag_data_list: + for tag_data in load_tags(): tag = Tag.objects.get_or_create(key=tag_data.key)[0] tag.name_ko = next(filter(lambda x: x.language == 'ko', tag_data.displayNames)).name tag.name_en = next(filter(lambda x: x.language == 'en', tag_data.displayNames)).name @@ -64,3 +80,15 @@ def load_tags(file='../tools/tags.json') -> List[TagJSON]: )[0] boj_tag.full_clean() boj_tag.save() + + +with atomic(): + for lang_data in load_languages(): + lang = Language.objects.get_or_create( + pk=lang_data.bojId, + key=lang_data.key + )[0] + lang.name = lang_data.displayName + lang.extension = lang_data.extension + lang.full_clean() + lang.save() From 3f20883420070d65b78d7acd6d619532f1d917c2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 21:14:40 +0900 Subject: [PATCH 049/552] chore(core.serializers): create serializers.py --- src/core/serializers.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/core/serializers.py diff --git a/src/core/serializers.py b/src/core/serializers.py new file mode 100644 index 0000000..5f2b8bd --- /dev/null +++ b/src/core/serializers.py @@ -0,0 +1,3 @@ +from rest_framework.serializers import * + +from .models import * From f0874f604b5da786bf1e7e155c8e19d8e341589e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:17:43 +0900 Subject: [PATCH 050/552] feat(crew.serializers): create `CrewSerializer` --- src/crew/serializers.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/crew/serializers.py b/src/crew/serializers.py index 5f2b8bd..0743ef6 100644 --- a/src/crew/serializers.py +++ b/src/crew/serializers.py @@ -1,3 +1,15 @@ from rest_framework.serializers import * +from core.serializers import LanguageSerializer +from user.serializers import UserSerializer + from .models import * + + +class CrewSerializer(ModelSerializer): + captain = UserSerializer(read_only=True) + languages = LanguageSerializer(many=True, read_only=True) + + class Meta: + model = Crew + fields = '__all__' From dd6a8ca9e0b5aeb3dbb626cecc9a015b5224112b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:03:38 +0900 Subject: [PATCH 051/552] feat(crew.views): create `CrewAPIView.ListCreate` --- src/crew/views.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/crew/views.py b/src/crew/views.py index 66960e9..1277f70 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -1,5 +1,20 @@ from rest_framework.generics import * from rest_framework.permissions import * +from user.models import User + from .models import * from .serializers import * + + +class CrewAPIView: + class ListCreate(ListCreateAPIView): + queryset = Crew.objects.all() + serializer_class = CrewSerializer + permission_classes = [IsAuthenticated] + + def perform_create(self, serializer): + serializer.save(captain=self._get_user()) + + def _get_user(self) -> User: + return User.objects.get(pk=self.request.user.pk) From 6bc59aed31fa82b873bf073d95e717e4c6eb704d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 21:14:49 +0900 Subject: [PATCH 052/552] feat(config.urls): Add API endpoints --- src/config/urls.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/urls.py b/src/config/urls.py index 2158df0..3019bd8 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -22,8 +22,8 @@ path, ) - from core.views import * +from crew.views import * from problem.views import * from user.views import * @@ -41,7 +41,7 @@ path("signout", UserAPIView.SignOut.as_view()), # 로그아웃 기능 구현 ])), path("user/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 사용자 목록 조회 기능 구현 (관리자용) + path("", UserAPIView.List.as_view()), # 사용자 목록 조회 기능 구현 (관리자용) path("/", include([ path("", VIEW_PLACE_HOLDER), # TODO: 사용자 상세 조회+수정 기능 구현 (관리자용) ])), @@ -55,7 +55,7 @@ ])), ])), path("crew/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 전체 크루 목록 조회(관리자용) + 생성 기능 구현 + path("", CrewAPIView.ListCreate.as_view()), # 전체 크루 목록 조회(관리자용) + 생성 path("my", VIEW_PLACE_HOLDER), # TODO: 내가 속한 크루 목록 조회 기능 구현 path("recruiting", VIEW_PLACE_HOLDER), # TODO: 크루원을 모집 중인 크루 목록 조회 기능 구현 path("/", include([ @@ -84,10 +84,10 @@ ])), ])), path("tag/", include([ - path("", TagAPIView.ListCreate.as_view()), # 전체 태그 목록 조회(관리자용) + 생성 기능 구현 + path("", TagAPIView.ListCreate.as_view()), # 전체 태그 목록 조회(관리자용) + 생성 기능 ])), path("language/", include([ - path("", LanguageAPIView.ListCreate.as_view()), # 전체 언어 목록 조회(관리자용) + 생성 기능 구현 + path("", LanguageAPIView.ListCreate.as_view()), # 전체 언어 목록 조회(관리자용) + 생성 기능 ])), ])), ])), From 206aeeb8c964af326c99ee5ea250a3e3cb4ef39d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 21:40:09 +0900 Subject: [PATCH 053/552] feat(config): Update ALLOWED_HOSTS to include 'tle-kr.com' and 'localhost' --- src/config/settings.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/config/settings.py b/src/config/settings.py index eb3a022..5dc5265 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -23,9 +23,12 @@ SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True +DEBUG = False -ALLOWED_HOSTS = [] +ALLOWED_HOSTS = [ + 'tle-kr.com', + 'localhost', +] # Application definition From 27149b672c7a0b8d9acc25a34e0d47fb356afcb8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 22:07:23 +0900 Subject: [PATCH 054/552] feat(core): Register Tag and Language models in admin site --- src/core/admin.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/core/admin.py diff --git a/src/core/admin.py b/src/core/admin.py new file mode 100644 index 0000000..d3b468e --- /dev/null +++ b/src/core/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from core.models import * + + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + pass + + +@admin.register(Language) +class LanguageAdmin(admin.ModelAdmin): + pass From 67c40104722dd7b57a7ae9daa5cb57ca57f9a8ca Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 21:29:37 +0900 Subject: [PATCH 055/552] feat(config): Add STATIC_ROOT --- src/config/settings.py | 3 +++ src/config/urls.py | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/config/settings.py b/src/config/settings.py index 5dc5265..c77c68d 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -125,6 +125,9 @@ STATIC_URL = "static/" +STATIC_ROOT = BASE_DIR / 'staticfiles' + + # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field diff --git a/src/config/urls.py b/src/config/urls.py index 425e343..1ab69c9 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -14,9 +14,11 @@ 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ +from django.conf import settings +from django.conf.urls.static import static from django.contrib import admin from django.urls import path urlpatterns = [ path("admin/", admin.site.urls), -] +] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) From ef390abf56e1b2bd534eac6011404af5404c6583 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 21:40:02 +0900 Subject: [PATCH 056/552] feat(tools): Add scripts for running server and setting up database --- tools/{ => db}/languages.json | 0 tools/{ => db}/setup_db.py | 0 tools/{ => db}/tags.json | 0 tools/runserver.sh | 6 ++++++ tools/setup.sh | 6 ++++++ 5 files changed, 12 insertions(+) rename tools/{ => db}/languages.json (100%) rename tools/{ => db}/setup_db.py (100%) rename tools/{ => db}/tags.json (100%) create mode 100755 tools/runserver.sh create mode 100755 tools/setup.sh diff --git a/tools/languages.json b/tools/db/languages.json similarity index 100% rename from tools/languages.json rename to tools/db/languages.json diff --git a/tools/setup_db.py b/tools/db/setup_db.py similarity index 100% rename from tools/setup_db.py rename to tools/db/setup_db.py diff --git a/tools/tags.json b/tools/db/tags.json similarity index 100% rename from tools/tags.json rename to tools/db/tags.json diff --git a/tools/runserver.sh b/tools/runserver.sh new file mode 100755 index 0000000..0ff1f5e --- /dev/null +++ b/tools/runserver.sh @@ -0,0 +1,6 @@ +#/bin/bash + +REPOSITORY=$(dirname $(dirname $0)) + +cd $REPOSITORY/src +python manage.py runserver 0.0.0.0:80 --insecure > $REPOSITORY/.log 2>&1 & \ No newline at end of file diff --git a/tools/setup.sh b/tools/setup.sh new file mode 100755 index 0000000..e4ab10c --- /dev/null +++ b/tools/setup.sh @@ -0,0 +1,6 @@ +#/bin/bash + +REPOSITORY=$(dirname $(dirname $0)) + +cd $REPOSITORY/src +python manage.py shell < $REPOSITORY/tools/db/setup_db.py From 76a3212ed4e1c199d58d84e612b339d936598b02 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 22:41:09 +0900 Subject: [PATCH 057/552] fix(crew): fix typo (CrewMemeber -> CrewMember) --- src/crew/models/crew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 073b360..244cfff 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -84,7 +84,7 @@ class Crew(models.Model): updated_at = models.DateTimeField(auto_now=True) -class CrewMemeber(models.Model): +class CrewMember(models.Model): crew = models.ForeignKey( Crew, on_delete=models.CASCADE, From 0d2480b1003948416b3789c280b0381772c21e18 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:37:50 +0900 Subject: [PATCH 058/552] chore: Add src/staticfiles/ to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 2f6a482..0bcfb20 100644 --- a/.gitignore +++ b/.gitignore @@ -168,3 +168,4 @@ cython_debug/ # Django **/migrations/* !**/migrations/__init__.py +src/staticfiles/* \ No newline at end of file From db9b1852d879dc359ee08b2541b518c58bba979d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:38:38 +0900 Subject: [PATCH 059/552] feat(user): Allow blank for image field in User model --- src/user/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/user/models.py b/src/user/models.py index 1122946..ef78b4c 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -10,7 +10,8 @@ class User(DjangoUser): ] image = models.ImageField( upload_to='user_images/', - null=True + null=True, + blank=True, ) From a8c41d135bb4e2354b8189230ca02d4bde68e328 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:39:24 +0900 Subject: [PATCH 060/552] =?UTF-8?q?fix(crew):=20max=5Flength=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B8=ED=95=B4=20=EC=9D=B4=EB=AA=A8=EC=A7=80(unicode)?= =?UTF-8?q?=EA=B0=80=20=EB=8B=B4=EA=B8=B0=EC=A7=80=20=EC=95=8A=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/crew.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 244cfff..47292ce 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -15,10 +15,13 @@ class Crew(models.Model): ), ) emoji = models.CharField( - max_length=1, + max_length=2, help_text=( '크루 아이콘을 입력해주세요. (이모지)' ), + validators=[ + # TODO: 이모지 형식 검사 + ], null=True, blank=True, ) From 8316a2c42f16f611d86ccc2798a296e96641e9db Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:40:13 +0900 Subject: [PATCH 061/552] =?UTF-8?q?fix(crew):=20tags=20field=20=EA=B0=80?= =?UTF-8?q?=20Crew=20=EA=B0=80=20=EC=95=84=EB=8B=8C=20CrewMemberRequest=20?= =?UTF-8?q?models=20=EC=97=90=20=EC=86=8D=ED=95=B4=20=EC=9E=88=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20Crew=20=EC=97=90=EA=B2=8C=20=EC=86=8D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/crew.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 47292ce..97ec474 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -83,6 +83,14 @@ class Crew(models.Model): null=True, default=None, ) + tags = models.JSONField( + help_text=( + '태그를 입력해주세요.' + ), + validators=[ + # TODO: 태그 형식 검사 + ], + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) @@ -131,12 +139,4 @@ class CrewMemberRequest(models.Model): null=True, blank=True, ) - tags = models.JSONField( - help_text=( - '태그를 입력해주세요.' - ), - validators=[ - # TODO: 태그 형식 검사 - ], - ) created_at = models.DateTimeField(auto_now_add=True) From e9bb535f9a6dbde9525d89b71fb563d910d74ff7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:41:30 +0900 Subject: [PATCH 062/552] feat(*): Register BOJUser, BOJTag, Crew, CrewMember, CrewMemberRequest, CrewActivity, CrewActivityProblem, CrewActivityProblemSubmission, CrewActivityProblemSubmissionComment, Problem, and ProblemAnalysis models in admin site --- src/boj/admin.py | 13 +++++++++++++ src/crew/admin.py | 38 ++++++++++++++++++++++++++++++++++++++ src/problem/admin.py | 13 +++++++++++++ src/user/admin.py | 8 ++++++++ 4 files changed, 72 insertions(+) create mode 100644 src/boj/admin.py create mode 100644 src/crew/admin.py create mode 100644 src/problem/admin.py create mode 100644 src/user/admin.py diff --git a/src/boj/admin.py b/src/boj/admin.py new file mode 100644 index 0000000..dbc3215 --- /dev/null +++ b/src/boj/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from boj.models import * + + +@admin.register(BOJUser) +class BOJUserAdmin(admin.ModelAdmin): + pass + + +@admin.register(BOJTag) +class BOJTagAdmin(admin.ModelAdmin): + pass diff --git a/src/crew/admin.py b/src/crew/admin.py new file mode 100644 index 0000000..5a163a9 --- /dev/null +++ b/src/crew/admin.py @@ -0,0 +1,38 @@ +from django.contrib import admin + +from crew.models import * + + +@admin.register(Crew) +class CrewAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewMember) +class CrewMemberAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewMemberRequest) +class CrewMemberRequestAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewActivity) +class CrewActivityAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewActivityProblem) +class CrewActivityProblemAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewActivityProblemSubmission) +class CrewActivityProblemSubmissionAdmin(admin.ModelAdmin): + pass + + +@admin.register(CrewActivityProblemSubmissionComment) +class CrewActivityProblemSubmissionCommentAdmin(admin.ModelAdmin): + pass diff --git a/src/problem/admin.py b/src/problem/admin.py new file mode 100644 index 0000000..433cb32 --- /dev/null +++ b/src/problem/admin.py @@ -0,0 +1,13 @@ +from django.contrib import admin + +from problem.models import * + + +@admin.register(Problem) +class ProblemAdmin(admin.ModelAdmin): + pass + + +@admin.register(ProblemAnalysis) +class ProblemAnalysisAdmin(admin.ModelAdmin): + pass diff --git a/src/user/admin.py b/src/user/admin.py new file mode 100644 index 0000000..0decace --- /dev/null +++ b/src/user/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from user.models import * + + +@admin.register(User) +class UserAdmin(admin.ModelAdmin): + pass From f88948ee8968edd5f3147ae1426467527b038bdd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:42:03 +0900 Subject: [PATCH 063/552] =?UTF-8?q?feat(*):=20=EB=AA=A8=EB=8D=B8=EB=93=A4?= =?UTF-8?q?=EC=97=90=20=5F=5Fstr=5F=5F()=EA=B3=BC=20=5F=5Frepr=5F=5F()=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/boj/models.py | 10 ++++++++++ src/core/models.py | 12 ++++++++++++ src/crew/models/activity.py | 25 +++++++++++++++++++++++++ src/crew/models/crew.py | 20 ++++++++++++++++++++ src/problem/models.py | 11 +++++++++++ src/user/models.py | 7 +++++++ 6 files changed, 85 insertions(+) diff --git a/src/boj/models.py b/src/boj/models.py index d2b8dc1..15c7f0e 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -74,6 +74,13 @@ class BOJUser(models.Model): auto_now=True, ) + def __repr__(self) -> str: + return f'[@{self.boj_id} | *{BOJLevel(self.level).label}]' + + def __str__(self) -> str: + verified = 'verified' if self.is_verified else 'not-verified' + return f'{self.pk} : {self.__repr__()} ({verified}) ← {self.user.__repr__()}' + class BOJTag(models.Model): tag = models.OneToOneField( @@ -92,3 +99,6 @@ class BOJTag(models.Model): null=True, default=None, ) + + def __str__(self) -> str: + return f'{self.boj_id} : {self.tag.__repr__()}' diff --git a/src/core/models.py b/src/core/models.py index 065a627..683fb00 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -41,6 +41,12 @@ class Tag(models.Model): ), ) + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name_ko})' + class Language(models.Model): key = models.CharField( @@ -63,3 +69,9 @@ class Language(models.Model): '언어 확장자를 입력해주세요. (최대 20자)' ), ) + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index d6730e9..eef4ccd 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -27,6 +27,12 @@ class CrewActivity(models.Model): ), ) + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + class CrewActivityProblem(models.Model): activity = models.ForeignKey( @@ -56,6 +62,12 @@ class CrewActivityProblem(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + def __repr__(self) -> str: + return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + class CrewActivityProblemSubmission(models.Model): activity_problem = models.ForeignKey( @@ -90,6 +102,12 @@ class CrewActivityProblemSubmission(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def __repr__(self) -> str: + return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + class CrewActivityProblemSubmissionComment(models.Model): submission = models.ForeignKey( @@ -133,3 +151,10 @@ class CrewActivityProblemSubmissionComment(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + line_range = f'L{self.line_number_start}:L{self.line_number_end}' + return f'{self.submission.__repr__()} ← {self.user.__repr__()} {line_range} "{self.content}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 97ec474..da93c41 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -94,6 +94,14 @@ class Crew(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def __repr__(self) -> str: + return f'[{self.emoji} {self.name}]' + + def __str__(self) -> str: + member_count = f'({self.members.count()}/{self.max_member})' + return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' + + class CrewMember(models.Model): crew = models.ForeignKey( @@ -114,6 +122,12 @@ class CrewMember(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + def __repr__(self) -> str: + return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + class CrewMemberRequest(models.Model): crew = models.ForeignKey( @@ -140,3 +154,9 @@ class CrewMemberRequest(models.Model): blank=True, ) created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/problem/models.py b/src/problem/models.py index 8fa5cd8..cf4556a 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -60,6 +60,12 @@ class Problem(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + def __repr__(self) -> str: + return f'[{self.title}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' + class ProblemAnalysis(models.Model): problem = models.ForeignKey( @@ -95,3 +101,8 @@ class ProblemAnalysis(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + + def __str__(self) -> str: + difficulty = Difficulty(self.difficulty).label + tags = ' '.join([f'#{tag.key}' for tag in self.dsa_tags.all()]) + return f'{self.pk} : {self.problem.__repr__()} ← [{difficulty} / {self.time_complexity} / {tags}]' diff --git a/src/user/models.py b/src/user/models.py index ef78b4c..95d220e 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -14,6 +14,13 @@ class User(DjangoUser): blank=True, ) + def __repr__(self) -> str: + return f'[@{self.username}]' + + def __str__(self) -> str: + staff = '(관리자)' if self.is_staff else '' + return f'{self.pk} : {self.__repr__()} {staff}' + User._meta.get_field('email')._unique = True User._meta.get_field('email').blank = False From 2a48f20ca95be4b0ec42e2824b3aa88100ffbea2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:56:14 +0900 Subject: [PATCH 064/552] refactor(config): Update static root path in settings.py --- .gitignore | 2 +- src/config/settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 0bcfb20..538f94d 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,4 @@ cython_debug/ # Django **/migrations/* !**/migrations/__init__.py -src/staticfiles/* \ No newline at end of file +static/ \ No newline at end of file diff --git a/src/config/settings.py b/src/config/settings.py index 37880af..48d2fbf 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -126,7 +126,7 @@ STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / 'staticfiles' +STATIC_ROOT = BASE_DIR / 'static' # Default primary key field type From 746e79de03705f1affe60a325746856c58f517cf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:13:28 +0900 Subject: [PATCH 065/552] feat(config): Add media URL and root path in settings.py, urls.py --- src/config/settings.py | 8 ++++++++ src/config/urls.py | 9 ++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/config/settings.py b/src/config/settings.py index 48d2fbf..48f7ab6 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -129,6 +129,14 @@ STATIC_ROOT = BASE_DIR / 'static' +# Meida files (Images) +# https://docs.djangoproject.com/en/4.2/topics/files/ + +MEDIA_URL = "media/" + +MEDIA_ROOT = BASE_DIR / 'media' + + # Default primary key field type # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field diff --git a/src/config/urls.py b/src/config/urls.py index 218e9a9..e83dfe5 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -24,4 +24,11 @@ urlpatterns = [ path("admin/", admin.site.urls), -] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) +] + +# Static files +urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + +# Media files +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +# TODO: Remove above line in production (미디어 파일은 S3 같은 외부 의존성으로 변경하기) From fbac274bf31aa3a0be3eb45cffb8d153ac1c2216 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:15:22 +0900 Subject: [PATCH 066/552] chore: add media/ directory to .gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 538f94d..f2dc5bc 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,5 @@ cython_debug/ # Django **/migrations/* !**/migrations/__init__.py -static/ \ No newline at end of file +static/ +media/ From 99fc6e227a914108bc7c0130dfd5b3e103286fd7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 3 Jun 2024 23:58:37 +0900 Subject: [PATCH 067/552] feat(crew): Add is_correct and is_help_needed fields to CrewActivityProblemSubmission model --- src/crew/models/activity.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index eef4ccd..b58177e 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -99,6 +99,17 @@ class CrewActivityProblemSubmission(models.Model): '유저의 코드 언어를 입력해주세요.' ), ) + is_correct = models.BooleanField( + help_text=( + '유저의 코드가 정답인지 여부를 입력해주세요.' + ), + ) + is_help_needed = models.BooleanField( + help_text=( + '유저의 코드에 도움이 필요한지 여부를 입력해주세요.' + ), + default=False, + ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From f9bfdc01251f38b892d54b26f747a47da09eb3bb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:09:14 +0900 Subject: [PATCH 068/552] =?UTF-8?q?refactor(boj):=20BOJUser=20=EC=9D=98=20?= =?UTF-8?q?=5F=5Fstr=5F=5F()=20=EC=9D=B4=20TLE=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=99=80=20=EB=B0=B1=EC=A4=80=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=9C=EB=A0=A5=ED=95=98=EB=8A=94=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=EB=A5=BC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/boj/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/boj/models.py b/src/boj/models.py index 15c7f0e..5589bdd 100644 --- a/src/boj/models.py +++ b/src/boj/models.py @@ -79,7 +79,7 @@ def __repr__(self) -> str: def __str__(self) -> str: verified = 'verified' if self.is_verified else 'not-verified' - return f'{self.pk} : {self.__repr__()} ({verified}) ← {self.user.__repr__()}' + return f'{self.pk} : {self.user.__repr__()} ← {self.__repr__()} ({verified})' class BOJTag(models.Model): From 4b78ce0412a3735bac954cbd162024ee91763f24 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:14:56 +0900 Subject: [PATCH 069/552] =?UTF-8?q?chore(user):=20add=20TODO=20(=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=ED=81=AC=EA=B8=B0=20=EC=A0=9C=ED=95=9C,?= =?UTF-8?q?=20=ED=98=95=EC=8B=9D=20=EC=A0=9C=ED=95=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/user/models.py b/src/user/models.py index 95d220e..d343098 100644 --- a/src/user/models.py +++ b/src/user/models.py @@ -12,6 +12,10 @@ class User(DjangoUser): upload_to='user_images/', null=True, blank=True, + validators=[ + # TODO: 이미지 크기 제한 + # TODO: 이미지 확장자 제한 + ] ) def __repr__(self) -> str: From 6172ccffa26a6385f431c0692f932819e9ced2d7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:40:58 +0900 Subject: [PATCH 070/552] feat(crew): Add blank=True to choices fields in Crew model --- src/crew/models/crew.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index da93c41..f7e4f37 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -68,6 +68,7 @@ class Crew(models.Model): '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' ), choices=BOJLevel.choices, + blank=True, null=True, default=None, ) @@ -80,6 +81,7 @@ class Crew(models.Model): # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 ], choices=BOJLevel.choices, + blank=True, null=True, default=None, ) @@ -90,6 +92,7 @@ class Crew(models.Model): validators=[ # TODO: 태그 형식 검사 ], + blank=True, ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From aed65b2bdb2714366c1b8607438c6a4f23fdf60f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:41:06 +0900 Subject: [PATCH 071/552] feat(crew): Add default value for tags field in Crew model --- src/crew/models/crew.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index f7e4f37..8ba857a 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -93,6 +93,7 @@ class Crew(models.Model): # TODO: 태그 형식 검사 ], blank=True, + default=list, ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From f2bcb5152e431b154c1444bf0500974abdf8e812 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 00:56:20 +0900 Subject: [PATCH 072/552] =?UTF-8?q?chore(crew.models):=20add=20TODO:=20?= =?UTF-8?q?=EA=B0=99=EC=9D=80=20=ED=81=AC=EB=A3=A8=EC=97=90=20=EC=97=AC?= =?UTF-8?q?=EB=9F=AC=20=EB=B2=88=20=EA=B0=80=EC=9E=85=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B2=83=EC=9D=84=20=EB=A7=89=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models/crew.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 8ba857a..5982122 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -108,6 +108,7 @@ def __str__(self) -> str: class CrewMember(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 crew = models.ForeignKey( Crew, on_delete=models.CASCADE, @@ -134,6 +135,7 @@ def __str__(self) -> str: class CrewMemberRequest(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 crew = models.ForeignKey( Crew, on_delete=models.CASCADE, From 4fa54f53acdbfdf6a96b539b636f0051a931a7f4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 02:10:50 +0900 Subject: [PATCH 073/552] chore(crew.models): add TODO: logic to prevent multiple submissions for the same problem --- src/crew/models/activity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crew/models/activity.py b/src/crew/models/activity.py index b58177e..957fafb 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models/activity.py @@ -70,6 +70,7 @@ def __str__(self) -> str: class CrewActivityProblemSubmission(models.Model): + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 activity_problem = models.ForeignKey( CrewActivityProblem, on_delete=models.CASCADE, From 390d9264ad8dbac4edb80c3132242fa3b6a79270 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 02:29:51 +0900 Subject: [PATCH 074/552] feat(core.models): add ordering to Tag model --- src/core/models.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/models.py b/src/core/models.py index 683fb00..e58a7a9 100644 --- a/src/core/models.py +++ b/src/core/models.py @@ -41,6 +41,9 @@ class Tag(models.Model): ), ) + class Meta: + ordering = ['key'] + def __repr__(self) -> str: return f'[#{self.key}]' From 50c435fb493b667418d07a313229f3f240a2a0e1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 02:40:45 +0900 Subject: [PATCH 075/552] refactor(problem.models): Rename dsa_tags to tags in ProblemAnalysis model --- src/problem/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/problem/models.py b/src/problem/models.py index cf4556a..9395db0 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -82,7 +82,7 @@ class ProblemAnalysis(models.Model): ), choices=Difficulty.choices, ) - dsa_tags = models.ManyToManyField( + tags = models.ManyToManyField( Tag, related_name='problems', help_text=( @@ -104,5 +104,5 @@ class ProblemAnalysis(models.Model): def __str__(self) -> str: difficulty = Difficulty(self.difficulty).label - tags = ' '.join([f'#{tag.key}' for tag in self.dsa_tags.all()]) + tags = ' '.join([f'#{tag.key}' for tag in self.tags.all()]) return f'{self.pk} : {self.problem.__repr__()} ← [{difficulty} / {self.time_complexity} / {tags}]' From cbbcdf89fa6cb0c23e9968e2d1db089bb434549b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 02:50:01 +0900 Subject: [PATCH 076/552] refactor(problem.models): Modularize Problem and ProblemAnalysis --- src/problem/models/__init__.py | 8 ++++ src/problem/{models.py => models/problem.py} | 43 ------------------- src/problem/models/problem_analysis.py | 45 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 43 deletions(-) create mode 100644 src/problem/models/__init__.py rename src/problem/{models.py => models/problem.py} (55%) create mode 100644 src/problem/models/problem_analysis.py diff --git a/src/problem/models/__init__.py b/src/problem/models/__init__.py new file mode 100644 index 0000000..005bd2a --- /dev/null +++ b/src/problem/models/__init__.py @@ -0,0 +1,8 @@ +from problem.models.problem import * +from problem.models.problem_analysis import * + + +__all__ = [ + 'Problem', + 'ProblemAnalysis', +] diff --git a/src/problem/models.py b/src/problem/models/problem.py similarity index 55% rename from src/problem/models.py rename to src/problem/models/problem.py index 9395db0..d20331c 100644 --- a/src/problem/models.py +++ b/src/problem/models/problem.py @@ -1,7 +1,5 @@ from django.db import models -from core.models import Difficulty -from core.models import Tag from user.models import User @@ -65,44 +63,3 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' - - -class ProblemAnalysis(models.Model): - problem = models.ForeignKey( - Problem, - on_delete=models.CASCADE, - related_name='analysis', - help_text=( - '문제를 입력해주세요.' - ), - ) - difficulty = models.IntegerField( - help_text=( - '문제 난이도를 입력해주세요.' - ), - choices=Difficulty.choices, - ) - tags = models.ManyToManyField( - Tag, - related_name='problems', - help_text=( - '문제의 DSA 태그를 입력해주세요.' - ), - ) - time_complexity = models.CharField( - max_length=100, - help_text=( - '문제 시간 복잡도를 입력해주세요. ', - '예) O(1), O(n), O(n^2), O(V \log E) 등', - ), - validators=[ - # TODO: 시간 복잡도 검증 로직 추가 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 - - def __str__(self) -> str: - difficulty = Difficulty(self.difficulty).label - tags = ' '.join([f'#{tag.key}' for tag in self.tags.all()]) - return f'{self.pk} : {self.problem.__repr__()} ← [{difficulty} / {self.time_complexity} / {tags}]' diff --git a/src/problem/models/problem_analysis.py b/src/problem/models/problem_analysis.py new file mode 100644 index 0000000..49e1d1f --- /dev/null +++ b/src/problem/models/problem_analysis.py @@ -0,0 +1,45 @@ +from django.db import models + +from core.models import * +from problem.models.problem import Problem + + +class ProblemAnalysis(models.Model): + problem = models.ForeignKey( + Problem, + on_delete=models.CASCADE, + related_name='analysis', + help_text=( + '문제를 입력해주세요.' + ), + ) + difficulty = models.IntegerField( + help_text=( + '문제 난이도를 입력해주세요.' + ), + choices=Difficulty.choices, + ) + tags = models.ManyToManyField( + Tag, + related_name='problems', + help_text=( + '문제의 DSA 태그를 입력해주세요.' + ), + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + + def __str__(self) -> str: + difficulty = Difficulty(self.difficulty).label + tags = ' '.join([f'#{tag.key}' for tag in self.tags.all()]) + return f'{self.pk} : {self.problem.__repr__()} ← [{difficulty} / {self.time_complexity} / {tags}]' From 7cc900cde8caf0149798fa6fa8079ec97078a95f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:16:41 +0900 Subject: [PATCH 077/552] chore(crew.serializers): create serializers.py --- src/crew/serializers.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/crew/serializers.py diff --git a/src/crew/serializers.py b/src/crew/serializers.py new file mode 100644 index 0000000..5f2b8bd --- /dev/null +++ b/src/crew/serializers.py @@ -0,0 +1,3 @@ +from rest_framework.serializers import * + +from .models import * From 2d61ade836b965e6e94d20eee855f5b4129f8a2e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 03:18:36 +0900 Subject: [PATCH 078/552] chore(user.serializers): create serializers.py --- src/user/serializers.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/user/serializers.py diff --git a/src/user/serializers.py b/src/user/serializers.py new file mode 100644 index 0000000..5f2b8bd --- /dev/null +++ b/src/user/serializers.py @@ -0,0 +1,3 @@ +from rest_framework.serializers import * + +from .models import * From 29278b57bad877481cf58d5e67fabde3ae7fa14f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 4 Jun 2024 03:21:37 +0900 Subject: [PATCH 079/552] refactor(problem.models): Update ProblemAnalysis model __str__ and __repr__ methods --- src/problem/models/problem_analysis.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/problem/models/problem_analysis.py b/src/problem/models/problem_analysis.py index 49e1d1f..3bc53e3 100644 --- a/src/problem/models/problem_analysis.py +++ b/src/problem/models/problem_analysis.py @@ -39,7 +39,9 @@ class ProblemAnalysis(models.Model): created_at = models.DateTimeField(auto_now_add=True) # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + def __repr__(self) -> str: + tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) + return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' + def __str__(self) -> str: - difficulty = Difficulty(self.difficulty).label - tags = ' '.join([f'#{tag.key}' for tag in self.tags.all()]) - return f'{self.pk} : {self.problem.__repr__()} ← [{difficulty} / {self.time_complexity} / {tags}]' + return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' From 5545bf8de04e7c02f0fb01ffd35319dc5b4a2f8f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:48:29 +0900 Subject: [PATCH 080/552] feat(user.serializers): create `UserSerializer` --- src/user/serializers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/user/serializers.py b/src/user/serializers.py index 5f2b8bd..711b672 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -1,3 +1,17 @@ from rest_framework.serializers import * from .models import * + + +class UserSerializer(ModelSerializer): + + class Meta: + model = User + fields = [ + 'id', + 'image', + 'username', + 'email', + ] + + # TODO: BOJUser Serializer 연결 From 2dc4bb3354592aec18e408d33c2680c570243c78 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:49:11 +0900 Subject: [PATCH 081/552] feat(user.serializers): create `UserSignInSerializer` --- src/user/serializers.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/user/serializers.py b/src/user/serializers.py index 711b672..2075dac 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -15,3 +15,24 @@ class Meta: ] # TODO: BOJUser Serializer 연결 + + +class UserSignInSerializer(ModelSerializer): + email = EmailField() + + class Meta: + model = User + fields = [ + 'id', + 'email', + 'password', + 'image', + 'username', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'image': {'read_only': True}, + 'username': {'read_only': True}, + 'email': {'write_only': True}, + 'password': {'write_only': True}, + } From 9521c6773693bf12d60db19cc3da75dc26db4283 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:26:56 +0900 Subject: [PATCH 082/552] feat(user.serializers): create `UserSignUpSerializer` --- src/user/serializers.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/user/serializers.py b/src/user/serializers.py index 2075dac..24cbafb 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -36,3 +36,23 @@ class Meta: 'email': {'write_only': True}, 'password': {'write_only': True}, } + + +class UserSignUpSerializer(ModelSerializer): + boj_id = CharField(max_length=40, required=False) + + class Meta: + model = User + fields = [ + 'id', + 'boj_id', + 'image', + 'username', + 'email', + 'password', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'boj_id': {'write_only': True}, + 'password': {'write_only': True}, + } From 9b9132c6a3231613ab6dc9c5724218dd14cf5faf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:48:01 +0900 Subject: [PATCH 083/552] feat(user.views): create `UserAPIView.ListCreate` --- src/user/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/user/views.py b/src/user/views.py index 66960e9..38b0ad9 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -3,3 +3,10 @@ from .models import * from .serializers import * + + +class UserAPIView: + class ListCreate(ListCreateAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [IsAdminUser] From af81ecc9619b69794b1e18b6e2f8596ec6249828 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:13:54 +0900 Subject: [PATCH 084/552] feat(user.views): create `UserAPIView.Login` --- src/user/views.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/user/views.py b/src/user/views.py index 38b0ad9..a7df282 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -1,3 +1,8 @@ +from http import HTTPStatus + +from django.contrib.auth import authenticate, login +from rest_framework.request import Request +from rest_framework.response import Response from rest_framework.generics import * from rest_framework.permissions import * @@ -10,3 +15,18 @@ class ListCreate(ListCreateAPIView): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [IsAdminUser] + + + class Login(GenericAPIView): + queryset = User.objects.all() + serializer_class = UserSerializer + permission_classes = [AllowAny] + + def post(self, request: Request): + username = request.data.get('username') + password = request.data.get('password') + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + return Response(UserSerializer(user).data) + return Response(status=HTTPStatus.UNAUTHORIZED) From 1e7210a6c0aa368207537436ff56b35155f477eb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 19 Jun 2024 16:10:51 +0900 Subject: [PATCH 085/552] feat(user.views): create `UserAPIView.Logout` --- src/user/views.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index a7df282..aeced8f 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -1,6 +1,6 @@ from http import HTTPStatus -from django.contrib.auth import authenticate, login +from django.contrib.auth import authenticate, login, logout from rest_framework.request import Request from rest_framework.response import Response from rest_framework.generics import * @@ -30,3 +30,9 @@ def post(self, request: Request): login(request, user) return Response(UserSerializer(user).data) return Response(status=HTTPStatus.UNAUTHORIZED) + + + class Logout(GenericAPIView): + def get(self, request: Request): + logout(request) + return Response(status=HTTPStatus.OK) From 0195657b44f7fe9b14007538965cf3bce8bf5f4f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 19 Jun 2024 16:18:45 +0900 Subject: [PATCH 086/552] =?UTF-8?q?refactor(problem.models):=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC=20=EC=8B=9C=EC=BC=B0=EB=8D=98=20Problem,=20ProblemAna?= =?UTF-8?q?lysis=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20problem.models=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/{models/problem.py => models.py} | 44 ++++++++++++++++++ src/problem/models/__init__.py | 8 ---- src/problem/models/problem_analysis.py | 47 -------------------- 3 files changed, 44 insertions(+), 55 deletions(-) rename src/problem/{models/problem.py => models.py} (56%) delete mode 100644 src/problem/models/__init__.py delete mode 100644 src/problem/models/problem_analysis.py diff --git a/src/problem/models/problem.py b/src/problem/models.py similarity index 56% rename from src/problem/models/problem.py rename to src/problem/models.py index d20331c..8c227c7 100644 --- a/src/problem/models/problem.py +++ b/src/problem/models.py @@ -1,5 +1,6 @@ from django.db import models +from core.models import * from user.models import User @@ -63,3 +64,46 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' + + +class ProblemAnalysis(models.Model): + problem = models.ForeignKey( + Problem, + on_delete=models.CASCADE, + related_name='analysis', + help_text=( + '문제를 입력해주세요.' + ), + ) + difficulty = models.IntegerField( + help_text=( + '문제 난이도를 입력해주세요.' + ), + choices=Difficulty.choices, + ) + tags = models.ManyToManyField( + Tag, + related_name='problems', + help_text=( + '문제의 DSA 태그를 입력해주세요.' + ), + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + + def __repr__(self) -> str: + tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) + return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' diff --git a/src/problem/models/__init__.py b/src/problem/models/__init__.py deleted file mode 100644 index 005bd2a..0000000 --- a/src/problem/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from problem.models.problem import * -from problem.models.problem_analysis import * - - -__all__ = [ - 'Problem', - 'ProblemAnalysis', -] diff --git a/src/problem/models/problem_analysis.py b/src/problem/models/problem_analysis.py deleted file mode 100644 index 3bc53e3..0000000 --- a/src/problem/models/problem_analysis.py +++ /dev/null @@ -1,47 +0,0 @@ -from django.db import models - -from core.models import * -from problem.models.problem import Problem - - -class ProblemAnalysis(models.Model): - problem = models.ForeignKey( - Problem, - on_delete=models.CASCADE, - related_name='analysis', - help_text=( - '문제를 입력해주세요.' - ), - ) - difficulty = models.IntegerField( - help_text=( - '문제 난이도를 입력해주세요.' - ), - choices=Difficulty.choices, - ) - tags = models.ManyToManyField( - Tag, - related_name='problems', - help_text=( - '문제의 DSA 태그를 입력해주세요.' - ), - ) - time_complexity = models.CharField( - max_length=100, - help_text=( - '문제 시간 복잡도를 입력해주세요. ', - '예) O(1), O(n), O(n^2), O(V \log E) 등', - ), - validators=[ - # TODO: 시간 복잡도 검증 로직 추가 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 - - def __repr__(self) -> str: - tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) - return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' From f69cede1b0d996112524b327d1b5c407e9351f0e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 19:59:24 +0900 Subject: [PATCH 087/552] feat(boj.serializers): create `BOJTagSerializer` --- src/boj/serializers.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/boj/serializers.py b/src/boj/serializers.py index b57cd6a..84162db 100644 --- a/src/boj/serializers.py +++ b/src/boj/serializers.py @@ -16,3 +16,14 @@ class Meta: 'level': {'read_only': True}, 'is_verified': {'read_only': True}, } + + +class BOJTagSerializer(ModelSerializer): + class Meta: + model = BOJTag + fields = [ + 'boj_id', + ] + extra_kwargs = { + 'boj_id': {'read_only': True}, + } From 63d14f0936b84dc82f704ba839ec8f6491611c38 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 19 Jun 2024 16:20:52 +0900 Subject: [PATCH 088/552] feat(problem.serializers): create `ProblemSerializer` --- src/problem/serializers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 5f2b8bd..11fa1df 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -1,3 +1,17 @@ from rest_framework.serializers import * +from user.serializers import UserSerializer + from .models import * + + +class ProblemSerializer(ModelSerializer): + user = UserSerializer(read_only=True) + + class Meta: + model = Problem + fields = '__all__' + extra_kwargs = { + 'created_at': {'read_only': True}, + 'updated_at': {'read_only': True}, + } From 999ef931e852c034f795ebf6a2b3a855b2d6ba68 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 19 Jun 2024 16:21:01 +0900 Subject: [PATCH 089/552] feat(problem.serializers): create `ProblemAnalysisSerializer` --- src/problem/serializers.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 11fa1df..4f2007e 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -1,12 +1,26 @@ from rest_framework.serializers import * +from core.serializers import TagSerializer from user.serializers import UserSerializer from .models import * +class ProblemAnalysisSerializer(ModelSerializer): + tags = TagSerializer(many=True, read_only=True) + + class Meta: + model = ProblemAnalysis + fields = '__all__' + extra_kwargs = { + 'created_at': {'read_only': True}, + 'problem': {'write_only': True}, + } + + class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) + analysis = ProblemAnalysisSerializer(many=True, read_only=True) class Meta: model = Problem From bdfb506633799de691977fb86c5450a1343ac8e5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:16:41 +0900 Subject: [PATCH 090/552] refactor(user.views): remove `email` field from `UserSerializer` --- src/user/serializers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/user/serializers.py b/src/user/serializers.py index 24cbafb..cb11621 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -4,14 +4,12 @@ class UserSerializer(ModelSerializer): - class Meta: model = User fields = [ 'id', 'image', 'username', - 'email', ] # TODO: BOJUser Serializer 연결 From 259b409bb217993906d572f18e9d8ad2fe337e16 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:20:50 +0900 Subject: [PATCH 091/552] feat(user.views): create `UserAPIView.SignUp` --- src/user/views.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index aeced8f..621e2d9 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -11,12 +11,23 @@ class UserAPIView: - class ListCreate(ListCreateAPIView): + class List(ListAPIView): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [IsAdminUser] + class SignUp(GenericAPIView): + serializer_class = UserSignUpSerializer + permission_classes = [AllowAny] + + def post(self, request: Request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = User.objects.create_user(**serializer.validated_data) + return Response(UserSerializer(user).data) + + class Login(GenericAPIView): queryset = User.objects.all() serializer_class = UserSerializer From 52f296ccd004a969f81029ffb486d12def931e06 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:22:10 +0900 Subject: [PATCH 092/552] refactor(user.views): rename `UserAPIView.Logout` -> `UserAPIView.SignOut` --- src/user/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index 621e2d9..631a8c4 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -43,7 +43,7 @@ def post(self, request: Request): return Response(status=HTTPStatus.UNAUTHORIZED) - class Logout(GenericAPIView): + class SignOut(GenericAPIView): def get(self, request: Request): logout(request) return Response(status=HTTPStatus.OK) From 211177c53693d6659a02f62769766c106cb7ce65 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:22:31 +0900 Subject: [PATCH 093/552] refactor(user.views): rename `UserAPIView.Login` -> `UserAPIView.SignIn` --- src/user/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index 631a8c4..4e730a7 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -28,7 +28,7 @@ def post(self, request: Request): return Response(UserSerializer(user).data) - class Login(GenericAPIView): + class SignIn(GenericAPIView): queryset = User.objects.all() serializer_class = UserSerializer permission_classes = [AllowAny] From 6b7ac0a8513e718c89e8c0ec6c19bf1856c23340 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 19 Jun 2024 20:46:22 +0900 Subject: [PATCH 094/552] =?UTF-8?q?feat(user.views):=20`UserAPIView.SignIn?= =?UTF-8?q?`=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=ED=95=A0=20=EC=88=98=20=EC=9E=88?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/user/views.py b/src/user/views.py index 4e730a7..cc99b77 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -34,12 +34,22 @@ class SignIn(GenericAPIView): permission_classes = [AllowAny] def post(self, request: Request): - username = request.data.get('username') - password = request.data.get('password') - user = authenticate(request, username=username, password=password) - if user is not None: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + try: + username = User.objects.get(email=email).username + user = authenticate(request, username=username, password=password) + assert user is not None login(request, user) return Response(UserSerializer(user).data) + except User.DoesNotExist: + # TODO: add logger + pass + except AssertionError: + # TODO: add logger + pass return Response(status=HTTPStatus.UNAUTHORIZED) From 968d54ae031ed797832d30fc6a4747ab69557d2f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 18:48:32 +0900 Subject: [PATCH 095/552] feat(config.views): create `NACLExceptionReporter` ... which reports debugging details only to ALLOWED_HOSTS --- src/config/views.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/config/views.py diff --git a/src/config/views.py b/src/config/views.py new file mode 100644 index 0000000..0cf9d35 --- /dev/null +++ b/src/config/views.py @@ -0,0 +1,20 @@ + +from django.conf import settings +from django.views import debug + + +class NACLExceptionReporter(debug.ExceptionReporter): + def __init__(self, request, exc_type, exc_value, tb, is_email=False): + super().__init__(request, exc_type, exc_value, tb, is_email) + + + def get_traceback_data(self) -> dict: + """Return a dictionary containing traceback information.""" + if self._get_domain() in settings.ALLOWED_HOSTS: + return super().get_traceback_frames() + return {} + + + def _get_domain(self): + host = self.request._get_raw_host() + return host.split(':')[0] From 229290280c6a98561ad38247479de75499389440 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:32:12 +0900 Subject: [PATCH 096/552] feat(config.settings): use as DEFAULT_EXCEPTION_REPORTER --- src/config/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/config/settings.py b/src/config/settings.py index 48f7ab6..b15bada 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -141,3 +141,5 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +DEFAULT_EXCEPTION_REPORTER = "config.views.NACLExceptionReporter" From 2bfcb318d792d81a60190e45c51655872f3a7118 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 18:49:13 +0900 Subject: [PATCH 097/552] feat(config.settings): Add 'timelimitexceeded.kr' to ALLOWED_HOSTS --- src/config/settings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/config/settings.py b/src/config/settings.py index b15bada..c1bc43a 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -27,6 +27,7 @@ ALLOWED_HOSTS = [ 'tle-kr.com', + 'timelimitexceeded.kr', 'localhost', ] From 48a00768e6e949a13fd7e1d116980defdd7a64dc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 22:12:32 +0900 Subject: [PATCH 098/552] =?UTF-8?q?feat(problem.models):=20`Problem.memory?= =?UTF-8?q?=5Flimit`=20=EC=9D=98=20=EB=8B=A8=EC=9C=84=EB=A5=BC=20MB?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/problem/models.py b/src/problem/models.py index 8c227c7..615bd1b 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -45,9 +45,9 @@ class Problem(models.Model): ), blank=True, ) - memory_limit = models.IntegerField( + memory_limit = models.FloatField( help_text=( - '문제 메모리 제한을 입력해주세요. (바이트 단위)' + '문제 메모리 제한을 입력해주세요. (MB 단위)' ), ) time_limit = models.IntegerField( From 1ac0613f8dc4d3b6d5f35456f75b90349585a3ea Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 22:12:51 +0900 Subject: [PATCH 099/552] =?UTF-8?q?feat(problem.models):=20`Problem.time?= =?UTF-8?q?=5Flimit`=20=EC=9D=98=20=EB=8B=A8=EC=9C=84=EB=A5=BC=20=EC=B4=88?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/problem/models.py b/src/problem/models.py index 615bd1b..33b809e 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -50,11 +50,11 @@ class Problem(models.Model): '문제 메모리 제한을 입력해주세요. (MB 단위)' ), ) - time_limit = models.IntegerField( + time_limit = models.FloatField( help_text=( - '문제 시간 제한을 입력해주세요. (밀리 초 단위)' + '문제 시간 제한을 입력해주세요. (초 단위)' ), - default=1000, + default=1.0, ) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) From 886bc9dd79fe627e9feed002ea84e30af70ee714 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:18:49 +0900 Subject: [PATCH 100/552] feat(problem.models): change relationship of `Problem` - `ProblemAnalysis`: ForiegnKey -> OneToOne --- src/problem/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/problem/models.py b/src/problem/models.py index 33b809e..159ca9e 100644 --- a/src/problem/models.py +++ b/src/problem/models.py @@ -67,7 +67,7 @@ def __str__(self) -> str: class ProblemAnalysis(models.Model): - problem = models.ForeignKey( + problem = models.OneToOneField( Problem, on_delete=models.CASCADE, related_name='analysis', From 72f5e80a06e99fc3b81043e34dee599e3d1b79de Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 19:17:32 +0900 Subject: [PATCH 101/552] =?UTF-8?q?refactor(crew.models):=20models.py=20?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/{models/activity.py => models.py} | 169 ++++++++++++++++++++- src/crew/models/__init__.py | 2 - src/crew/models/crew.py | 168 -------------------- 3 files changed, 165 insertions(+), 174 deletions(-) rename src/crew/{models/activity.py => models.py} (51%) delete mode 100644 src/crew/models/__init__.py delete mode 100644 src/crew/models/crew.py diff --git a/src/crew/models/activity.py b/src/crew/models.py similarity index 51% rename from src/crew/models/activity.py rename to src/crew/models.py index 957fafb..c0cf2a3 100644 --- a/src/crew/models/activity.py +++ b/src/crew/models.py @@ -1,10 +1,171 @@ from django.core.validators import MinValueValidator from django.db import models -from core.models import Language -from crew.models.crew import Crew -from problem.models import Problem -from user.models import User +from boj.models import * +from core.models import * +from problem.models import * +from user.models import * + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '크루 이름을 입력해주세요. (최대 20자)' + ), + ) + emoji = models.CharField( + max_length=2, + help_text=( + '크루 아이콘을 입력해주세요. (이모지)' + ), + validators=[ + # TODO: 이모지 형식 검사 + ], + null=True, + blank=True, + ) + captain = models.ForeignKey( + User, + on_delete=models.PROTECT, + related_name='crews_as_captain', + help_text=( + '크루장을 입력해주세요.' + ), + ) + notice = models.TextField( + help_text=( + '크루 공지를 입력해주세요.' + ), + null=True, + blank=True, + ) + languages = models.ManyToManyField( + Language, + related_name='crews', + help_text=( + '유저가 사용 가능한 언어를 입력해주세요.' + ), + ) + max_member = models.IntegerField( + help_text=( + '크루 최대 인원을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 최대 인원 제한 + ], + ) + is_boj_user_only = models.BooleanField( + help_text=( + '백준 아이디 필요 여부를 입력해주세요.' + ), + default=False, + ) + min_boj_tier = models.IntegerField( + help_text=( + '최소 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + choices=BOJLevel.choices, + blank=True, + null=True, + default=None, + ) + max_boj_tier = models.IntegerField( + help_text=( + '최대 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + validators=[ + # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 + ], + choices=BOJLevel.choices, + blank=True, + null=True, + default=None, + ) + tags = models.JSONField( + help_text=( + '태그를 입력해주세요.' + ), + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + return f'[{self.emoji} {self.name}]' + + def __str__(self) -> str: + member_count = f'({self.members.count()}/{self.max_member})' + return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' + + +class CrewMember(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='members', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crews', + help_text=( + '유저를 입력해주세요.' + ), + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + + +class CrewMemberRequest(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='requests', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crew_requests', + help_text=( + '유저를 입력해주세요.' + ), + ) + message = models.TextField( + help_text=( + '가입 메시지를 입력해주세요.' + ), + null=True, + blank=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' class CrewActivity(models.Model): diff --git a/src/crew/models/__init__.py b/src/crew/models/__init__.py deleted file mode 100644 index e08f984..0000000 --- a/src/crew/models/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from crew.models.crew import * -from crew.models.activity import * diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py deleted file mode 100644 index 5982122..0000000 --- a/src/crew/models/crew.py +++ /dev/null @@ -1,168 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from boj.models import BOJLevel -from core.models import Language -from user.models import User - - -class Crew(models.Model): - name = models.CharField( - max_length=20, - unique=True, - help_text=( - '크루 이름을 입력해주세요. (최대 20자)' - ), - ) - emoji = models.CharField( - max_length=2, - help_text=( - '크루 아이콘을 입력해주세요. (이모지)' - ), - validators=[ - # TODO: 이모지 형식 검사 - ], - null=True, - blank=True, - ) - captain = models.ForeignKey( - User, - on_delete=models.PROTECT, - related_name='crews_as_captain', - help_text=( - '크루장을 입력해주세요.' - ), - ) - notice = models.TextField( - help_text=( - '크루 공지를 입력해주세요.' - ), - null=True, - blank=True, - ) - languages = models.ManyToManyField( - Language, - related_name='crews', - help_text=( - '유저가 사용 가능한 언어를 입력해주세요.' - ), - ) - max_member = models.IntegerField( - help_text=( - '크루 최대 인원을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 최대 인원 제한 - ], - ) - is_boj_user_only = models.BooleanField( - help_text=( - '백준 아이디 필요 여부를 입력해주세요.' - ), - default=False, - ) - min_boj_tier = models.IntegerField( - help_text=( - '최소 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - max_boj_tier = models.IntegerField( - help_text=( - '최대 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - validators=[ - # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 - ], - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - tags = models.JSONField( - help_text=( - '태그를 입력해주세요.' - ), - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'[{self.emoji} {self.name}]' - - def __str__(self) -> str: - member_count = f'({self.members.count()}/{self.max_member})' - return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' - - - -class CrewMember(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='members', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crews', - help_text=( - '유저를 입력해주세요.' - ), - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewMemberRequest(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='requests', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crew_requests', - help_text=( - '유저를 입력해주세요.' - ), - ) - message = models.TextField( - help_text=( - '가입 메시지를 입력해주세요.' - ), - null=True, - blank=True, - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' From 461a91a393a7b4023a467b082782104379471bd7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:56:31 +0900 Subject: [PATCH 102/552] chore(problem.serializers): create serializers.py --- src/problem/serializers.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/problem/serializers.py diff --git a/src/problem/serializers.py b/src/problem/serializers.py new file mode 100644 index 0000000..5f2b8bd --- /dev/null +++ b/src/problem/serializers.py @@ -0,0 +1,3 @@ +from rest_framework.serializers import * + +from .models import * From 06abe6f9ef6156356110b4898b6667fb53774823 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 19:54:34 +0900 Subject: [PATCH 103/552] chore(boj.serializers): create serializers.py --- src/boj/serializers.py | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/boj/serializers.py diff --git a/src/boj/serializers.py b/src/boj/serializers.py new file mode 100644 index 0000000..6806c80 --- /dev/null +++ b/src/boj/serializers.py @@ -0,0 +1,3 @@ +from rest_framework.serializers import * + +from boj.models import * From c2c6522c9e507a05124c9fa339b5b5db393bfb5e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:13:23 +0900 Subject: [PATCH 104/552] =?UTF-8?q?feat(problem.serializers):=20`ProblemSe?= =?UTF-8?q?rializer.time=5Flimit`=EC=9D=98=20=EB=8B=A8=EC=9C=84=EB=A5=BC?= =?UTF-8?q?=20=EC=B4=88=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 4f2007e..bb83489 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -21,6 +21,7 @@ class Meta: class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) analysis = ProblemAnalysisSerializer(many=True, read_only=True) + time_limit = SerializerMethodField() class Meta: model = Problem @@ -29,3 +30,6 @@ class Meta: 'created_at': {'read_only': True}, 'updated_at': {'read_only': True}, } + + def get_time_limit(self, obj: Problem) -> float: + return obj.time_limit/1000 From 360ab5688bb4aa8337c692780ced4b9c3a8d583e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:13:33 +0900 Subject: [PATCH 105/552] =?UTF-8?q?feat(problem.serializers):=20`ProblemSe?= =?UTF-8?q?rializer.memory=5Flimit`=EC=9D=98=20=EB=8B=A8=EC=9C=84=EB=A5=BC?= =?UTF-8?q?=20MB=20=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/serializers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index bb83489..13ebb13 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -22,6 +22,7 @@ class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) analysis = ProblemAnalysisSerializer(many=True, read_only=True) time_limit = SerializerMethodField() + memory_limit = SerializerMethodField() class Meta: model = Problem @@ -33,3 +34,6 @@ class Meta: def get_time_limit(self, obj: Problem) -> float: return obj.time_limit/1000 + + def get_memory_limit(self, obj: Problem) -> int: + return round(obj.memory_limit/1024/1024) From 76eb510969b50d9714a4302486d98c13a8bb79c4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:20:45 +0900 Subject: [PATCH 106/552] =?UTF-8?q?refactor(user.views):=20`SignIn`?= =?UTF-8?q?=EB=B7=B0=EC=9D=98=20login=20=EB=A1=9C=EC=A7=81=EC=97=90?= =?UTF-8?q?=EC=84=9C=20username=20=EC=B7=A8=EB=93=9D=20=EB=B6=80=EB=B6=84?= =?UTF-8?q?=EC=9D=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/user/views.py b/src/user/views.py index cc99b77..e45d9d4 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -1,6 +1,7 @@ from http import HTTPStatus from django.contrib.auth import authenticate, login, logout +from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response from rest_framework.generics import * @@ -36,21 +37,25 @@ class SignIn(GenericAPIView): def post(self, request: Request): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - email = serializer.validated_data['email'] + + # TODO: authenticate()만 이용하여 email, password로 인증하는 기능 리팩토링 + username = self._get_username(serializer) password = serializer.validated_data['password'] + + user = authenticate(request, username=username, password=password) + if user is None: + return Response(status=HTTPStatus.UNAUTHORIZED) + + login(request, user) + + return Response(UserSerializer(user).data) + + def _get_username(self, serializer): try: - username = User.objects.get(email=email).username - user = authenticate(request, username=username, password=password) - assert user is not None - login(request, user) - return Response(UserSerializer(user).data) + user = User.objects.get(email=serializer.validated_data['email']) except User.DoesNotExist: - # TODO: add logger - pass - except AssertionError: - # TODO: add logger - pass - return Response(status=HTTPStatus.UNAUTHORIZED) + raise AuthenticationFailed + return user.username class SignOut(GenericAPIView): From 5d62b876ef002abd55b0b5652cc832091f18f23e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:21:41 +0900 Subject: [PATCH 107/552] =?UTF-8?q?refactor(user.views):=20`SignUp`?= =?UTF-8?q?=EB=B7=B0=EC=9D=98=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=EC=9D=84=20CreateAPIView=20=EB=A5=BC=20?= =?UTF-8?q?=EC=83=81=EC=86=8D=EB=B0=9B=EC=95=84=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EB=8B=A8=EC=88=9C=ED=99=94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/user/views.py b/src/user/views.py index e45d9d4..f77d871 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -18,15 +18,12 @@ class List(ListAPIView): permission_classes = [IsAdminUser] - class SignUp(GenericAPIView): + class SignUp(CreateAPIView): serializer_class = UserSignUpSerializer permission_classes = [AllowAny] - def post(self, request: Request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - user = User.objects.create_user(**serializer.validated_data) - return Response(UserSerializer(user).data) + def perform_create(self, serializer): + serializer.instance = User.objects.create_user(**serializer.validated_data) class SignIn(GenericAPIView): From fd263214adad4c7c4abf9a1176da57b75c7aca0d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:23:21 +0900 Subject: [PATCH 108/552] chore(crew.views): create views.py --- src/crew/views.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/crew/views.py diff --git a/src/crew/views.py b/src/crew/views.py new file mode 100644 index 0000000..66960e9 --- /dev/null +++ b/src/crew/views.py @@ -0,0 +1,5 @@ +from rest_framework.generics import * +from rest_framework.permissions import * + +from .models import * +from .serializers import * From 2e827f2508fc88898068edf3006202a17daca750 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:29:34 +0900 Subject: [PATCH 109/552] fix(config.views): fix misplaced method --- src/config/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/views.py b/src/config/views.py index 0cf9d35..704692d 100644 --- a/src/config/views.py +++ b/src/config/views.py @@ -11,7 +11,7 @@ def __init__(self, request, exc_type, exc_value, tb, is_email=False): def get_traceback_data(self) -> dict: """Return a dictionary containing traceback information.""" if self._get_domain() in settings.ALLOWED_HOSTS: - return super().get_traceback_frames() + return super().get_traceback_data() return {} From d1fa4b6f38270403a28d1a54339a6a4b404c21df Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:42:29 +0900 Subject: [PATCH 110/552] chore(core.views): create views.py --- src/core/views.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/core/views.py diff --git a/src/core/views.py b/src/core/views.py new file mode 100644 index 0000000..66960e9 --- /dev/null +++ b/src/core/views.py @@ -0,0 +1,5 @@ +from rest_framework.generics import * +from rest_framework.permissions import * + +from .models import * +from .serializers import * From 1fa93b1de461fd8fe8bbab990a034e1487489987 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 01:24:25 +0900 Subject: [PATCH 111/552] feat(config.settings): enable paginator globally --- src/config/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/config/settings.py b/src/config/settings.py index c1bc43a..9fae126 100644 --- a/src/config/settings.py +++ b/src/config/settings.py @@ -144,3 +144,8 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" DEFAULT_EXCEPTION_REPORTER = "config.views.NACLExceptionReporter" + +REST_FRAMEWORK = { + 'PAGE_SIZE': 10, + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', +} From 5fa9dbd853ad95448d8db8b6bb29f1a685bb255e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:43:27 +0900 Subject: [PATCH 112/552] feat(core.serializers): create `TagSerializer` --- src/core/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/serializers.py b/src/core/serializers.py index 5f2b8bd..01e0f9c 100644 --- a/src/core/serializers.py +++ b/src/core/serializers.py @@ -1,3 +1,9 @@ from rest_framework.serializers import * from .models import * + + +class TagSerializer(ModelSerializer): + class Meta: + model = Tag + fields = '__all__' From 699968cda2c3f877ed1cf1e2d7191904ff6c4e1a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:53:58 +0900 Subject: [PATCH 113/552] feat(core.views): create `TagAPIView.ListCreate` --- src/core/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/views.py b/src/core/views.py index 66960e9..5e07272 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -1,5 +1,14 @@ from rest_framework.generics import * from rest_framework.permissions import * +from config.permissions import * + from .models import * from .serializers import * + + +class TagAPIView: + class ListCreate(ListCreateAPIView): + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [IsAdminUser|ReadOnly] From 14e1502786b73b17d2fe044eb1ff0d37510dd821 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:43:47 +0900 Subject: [PATCH 114/552] feat(core.serializers): create `LanguageSerializer` --- src/core/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/serializers.py b/src/core/serializers.py index 01e0f9c..6e21bdc 100644 --- a/src/core/serializers.py +++ b/src/core/serializers.py @@ -7,3 +7,9 @@ class TagSerializer(ModelSerializer): class Meta: model = Tag fields = '__all__' + + +class LanguageSerializer(ModelSerializer): + class Meta: + model = Language + fields = '__all__' From 423d0e16a8024b8d0d0f7dccd628e725d91739f1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:41:05 +0900 Subject: [PATCH 115/552] chore(problem.views): create views.py --- src/problem/views.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/problem/views.py diff --git a/src/problem/views.py b/src/problem/views.py new file mode 100644 index 0000000..66960e9 --- /dev/null +++ b/src/problem/views.py @@ -0,0 +1,5 @@ +from rest_framework.generics import * +from rest_framework.permissions import * + +from .models import * +from .serializers import * From c866558f7be5b7cf06ea36045e0744efc26723c9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:49:37 +0900 Subject: [PATCH 116/552] feat(config.permissions): create `ReadOnly` --- src/config/permissions.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 src/config/permissions.py diff --git a/src/config/permissions.py b/src/config/permissions.py new file mode 100644 index 0000000..65e0dcc --- /dev/null +++ b/src/config/permissions.py @@ -0,0 +1,16 @@ +from rest_framework.permissions import ( + BasePermission, + SAFE_METHODS, +) + + +__all__ = ( + 'ReadOnly', +) + + +class ReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS, + ) From dcb0be6e32f56e3d85802bf7b9c8caeb14f8c356 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 20:56:12 +0900 Subject: [PATCH 117/552] feat(core.views): create `LanguageAPIView.ListCreate` --- src/core/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/core/views.py b/src/core/views.py index 5e07272..ef92768 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -12,3 +12,10 @@ class ListCreate(ListCreateAPIView): queryset = Tag.objects.all() serializer_class = TagSerializer permission_classes = [IsAdminUser|ReadOnly] + + +class LanguageAPIView: + class ListCreate(ListCreateAPIView): + queryset = Language.objects.all() + serializer_class = LanguageSerializer + permission_classes = [IsAdminUser|ReadOnly] From 830cf0825e69f25560884e73a9f37e17bbf94c66 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:19:37 +0900 Subject: [PATCH 118/552] refactor(problem.serializers): update `ProblemAnalysisSerializer` --- src/problem/serializers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 13ebb13..051f37b 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -11,10 +11,16 @@ class ProblemAnalysisSerializer(ModelSerializer): class Meta: model = ProblemAnalysis - fields = '__all__' + fields = [ + 'id', + 'problem', + 'difficulty', + 'tags', + 'time_complexity', + 'created_at', + ] extra_kwargs = { 'created_at': {'read_only': True}, - 'problem': {'write_only': True}, } From 853cd73530b7bae9b05583bd4a02d3e05c8bebb3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:19:48 +0900 Subject: [PATCH 119/552] refactor(problem.serializers): update `ProblemSerializer` --- src/problem/serializers.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 051f37b..7184f50 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -27,19 +27,23 @@ class Meta: class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) analysis = ProblemAnalysisSerializer(many=True, read_only=True) - time_limit = SerializerMethodField() - memory_limit = SerializerMethodField() class Meta: model = Problem - fields = '__all__' + fields = [ + 'id', + 'analysis', + 'title', + 'link', + 'description', + 'input_description', + 'output_description', + 'memory_limit', + 'time_limit', + 'user', + ] extra_kwargs = { - 'created_at': {'read_only': True}, - 'updated_at': {'read_only': True}, + 'id': {'read_only': True}, + 'analysis': {'read_only': True}, + 'user': {'read_only': True}, } - - def get_time_limit(self, obj: Problem) -> float: - return obj.time_limit/1000 - - def get_memory_limit(self, obj: Problem) -> int: - return round(obj.memory_limit/1024/1024) From 8395ed3b7645c600d5320e1c638782fd6dd3bf60 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:32:01 +0900 Subject: [PATCH 120/552] refactor(problem.serializers): update `ProblemAnalysisSerializer` --- src/problem/serializers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 7184f50..62a636a 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -7,7 +7,7 @@ class ProblemAnalysisSerializer(ModelSerializer): - tags = TagSerializer(many=True, read_only=True) + tags = TagSerializer(many=True) class Meta: model = ProblemAnalysis @@ -20,6 +20,8 @@ class Meta: 'created_at', ] extra_kwargs = { + 'id': {'read_only': True}, + 'problem': {'read_only': True}, 'created_at': {'read_only': True}, } From e2055834e72b353a3f4cefdc28333db3185505f2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:47:14 +0900 Subject: [PATCH 121/552] fix(problem.serializers): analysis is not iterable --- src/problem/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/problem/serializers.py b/src/problem/serializers.py index 62a636a..71f8898 100644 --- a/src/problem/serializers.py +++ b/src/problem/serializers.py @@ -28,7 +28,7 @@ class Meta: class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) - analysis = ProblemAnalysisSerializer(many=True, read_only=True) + analysis = ProblemAnalysisSerializer(read_only=True) class Meta: model = Problem From ed5892ad1d2ec362c141869d4f18696596ee167f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 21:42:01 +0900 Subject: [PATCH 122/552] feat(problem.views): create `ProblemAPIView.ListCreate` --- src/problem/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 66960e9..75f169f 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -3,3 +3,10 @@ from .models import * from .serializers import * + + +class ProblemAPIView: + class ListCreate(ListCreateAPIView): + queryset = Problem.objects.all() + serializer_class = ProblemSerializer + permission_classes = [IsAuthenticated] From 3363bf1892cf66f926ec49382f6099de088d7f09 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:09:59 +0900 Subject: [PATCH 123/552] feat(problem.views): create `ProblemAPIView.RetrieveUpdateDestroy` --- src/problem/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 75f169f..7394b6d 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -10,3 +10,9 @@ class ListCreate(ListCreateAPIView): queryset = Problem.objects.all() serializer_class = ProblemSerializer permission_classes = [IsAuthenticated] + + class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): + queryset = Problem.objects.all() + serializer_class = ProblemSerializer + permission_classes = [IsAuthenticatedOrReadOnly] # TODO: 본인만 수정 가능하게 수정 + lookup_url_kwarg = 'id' From c5f91f743abac3a55cfdec78f6233990c42258fb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:24:00 +0900 Subject: [PATCH 124/552] feat(problem.views): create `ProblemAnalysisAPIView.Retrieve` --- src/problem/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 7394b6d..aa921cc 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -16,3 +16,11 @@ class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): serializer_class = ProblemSerializer permission_classes = [IsAuthenticatedOrReadOnly] # TODO: 본인만 수정 가능하게 수정 lookup_url_kwarg = 'id' + + +class ProblemAnalysisAPIView: + class Retrieve(RetrieveAPIView): + queryset = ProblemAnalysis.objects.all() + serializer_class = ProblemAnalysisSerializer + lookup_url_kwarg = 'id' + lookup_field = 'problem__id' From d13ad6880e8691cc9fd35de4a468634f8a7aafe4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:41:20 +0900 Subject: [PATCH 125/552] =?UTF-8?q?refactor(user.views):=20import=20?= =?UTF-8?q?=EB=AC=B8=20=ED=98=95=EC=8B=9D=EC=9D=84=20=ED=95=9C=20=EC=A4=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=97=AC=EB=9F=AC=20=EC=A4=84=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index f77d871..2b9378b 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -1,6 +1,10 @@ from http import HTTPStatus -from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import ( + authenticate, + login, + logout, +) from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response From f1b545634b1f78c8dc1c7af13bbf3a9c57c76645 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:41:56 +0900 Subject: [PATCH 126/552] =?UTF-8?q?refactor(user.views):=20SignInSerialize?= =?UTF-8?q?r=20=EC=9E=98=EB=AA=BB=EB=90=9C=EA=B1=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/user/views.py b/src/user/views.py index 2b9378b..f28e8ce 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -32,7 +32,7 @@ def perform_create(self, serializer): class SignIn(GenericAPIView): queryset = User.objects.all() - serializer_class = UserSerializer + serializer_class = UserSignInSerializer permission_classes = [AllowAny] def post(self, request: Request): @@ -49,7 +49,8 @@ def post(self, request: Request): login(request, user) - return Response(UserSerializer(user).data) + serializer.instance = user + return Response(serializer.data) def _get_username(self, serializer): try: From 353ca4ed88d9064f48df28e48ae581f2c72a0ac6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:43:34 +0900 Subject: [PATCH 127/552] feat(problem.views): create `IsProblemCreator(BasePermission)` --- src/problem/views.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index aa921cc..4ec3fca 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -5,6 +5,15 @@ from .serializers import * +class IsProblemCreator(BasePermission): + def has_object_permission(self, request, view, obj: Problem) -> bool: + return bool( + request.user and + request.user.is_authenticated and + obj.user == request.user + ) + + class ProblemAPIView: class ListCreate(ListCreateAPIView): queryset = Problem.objects.all() From 1e06898ffab6fe2f5eb1bf78ba0b205db3c92683 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:45:30 +0900 Subject: [PATCH 128/552] =?UTF-8?q?feat(problem.views):=20`ProblemAnalysis?= =?UTF-8?q?APIView.Retrieve`=EC=9D=98=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C?= =?UTF-8?q?=ED=95=9C=EC=9D=84=20=EA=B4=80=EB=A6=AC=EC=9E=90/=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=9E=90=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/views.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 4ec3fca..2cb7a5a 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -1,6 +1,8 @@ from rest_framework.generics import * from rest_framework.permissions import * +from config.permissions import ReadOnly + from .models import * from .serializers import * @@ -30,6 +32,7 @@ class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): class ProblemAnalysisAPIView: class Retrieve(RetrieveAPIView): queryset = ProblemAnalysis.objects.all() + permission_classes = [IsAdminUser or (IsProblemCreator and ReadOnly)] serializer_class = ProblemAnalysisSerializer lookup_url_kwarg = 'id' lookup_field = 'problem__id' From feed63ad4338a9ed067203e321b0ee7576dfc6a5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:48:38 +0900 Subject: [PATCH 129/552] =?UTF-8?q?feat(problem.views):=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EA=B0=80=20=EC=95=84=EB=8B=88=EB=A9=B4=20`Pr?= =?UTF-8?q?oblemAPIView.ListCreate`=EC=97=90=EC=84=9C=EB=8A=94=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8=EC=9D=B4=20=EB=93=B1=EB=A1=9D=ED=95=9C=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A7=8C=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/views.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/problem/views.py b/src/problem/views.py index 2cb7a5a..8337bfa 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -18,10 +18,13 @@ def has_object_permission(self, request, view, obj: Problem) -> bool: class ProblemAPIView: class ListCreate(ListCreateAPIView): - queryset = Problem.objects.all() serializer_class = ProblemSerializer permission_classes = [IsAuthenticated] + def get_queryset(self): + return Problem.objects.filter(user=self.request.user) + + class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Problem.objects.all() serializer_class = ProblemSerializer From cb8012d8039e579f36f78f20011b1e0510c7e7ad Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 20 Jun 2024 23:49:10 +0900 Subject: [PATCH 130/552] =?UTF-8?q?feat(problem.views):=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EA=B0=80=20=EC=95=84=EB=8B=88=EB=A9=B4=20`Pr?= =?UTF-8?q?oblemAPIView.RetrieveUpdateDestroy`=EC=97=90=EC=84=9C=EB=8A=94?= =?UTF-8?q?=20=EB=B3=B8=EC=9D=B8=EC=9D=B4=20=EB=93=B1=EB=A1=9D=ED=95=9C=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=EB=A7=8C=20=EC=88=98=EC=A0=95=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/problem/views.py b/src/problem/views.py index 8337bfa..34985e5 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -28,7 +28,7 @@ def get_queryset(self): class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Problem.objects.all() serializer_class = ProblemSerializer - permission_classes = [IsAuthenticatedOrReadOnly] # TODO: 본인만 수정 가능하게 수정 + permission_classes = [IsAdminUser or IsProblemCreator] lookup_url_kwarg = 'id' From 98eadcfc5152d2c256bf35d2e39e157c85587213 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 01:02:56 +0900 Subject: [PATCH 131/552] feat(boj.serializers): create `BOJUserSerializer` --- src/boj/serializers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/boj/serializers.py b/src/boj/serializers.py index 6806c80..b57cd6a 100644 --- a/src/boj/serializers.py +++ b/src/boj/serializers.py @@ -1,3 +1,18 @@ from rest_framework.serializers import * from boj.models import * + + +class BOJUserSerializer(ModelSerializer): + class Meta: + model = BOJUser + fields = [ + 'boj_id', + 'level', + 'is_verified', + ] + extra_kwargs = { + 'boj_id': {'read_only': True}, + 'level': {'read_only': True}, + 'is_verified': {'read_only': True}, + } From e49976814d94d9ae1719ec26d36e9205bfa5760c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:31:02 +0900 Subject: [PATCH 132/552] feat(user.serializers): add field `boj_user` to `UserSignUpSerializer` --- src/user/serializers.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/user/serializers.py b/src/user/serializers.py index cb11621..bf9309d 100644 --- a/src/user/serializers.py +++ b/src/user/serializers.py @@ -1,5 +1,7 @@ from rest_framework.serializers import * +from boj.serializers import BOJUserSerializer + from .models import * @@ -38,12 +40,14 @@ class Meta: class UserSignUpSerializer(ModelSerializer): boj_id = CharField(max_length=40, required=False) + boj_user = BOJUserSerializer(read_only=True) class Meta: model = User fields = [ 'id', 'boj_id', + 'boj_user', 'image', 'username', 'email', @@ -52,5 +56,6 @@ class Meta: extra_kwargs = { 'id': {'read_only': True}, 'boj_id': {'write_only': True}, + 'boj_user': {'read_only': True}, 'password': {'write_only': True}, } From 29edbe5886bca6a1679763c7ab0743749c08e4a4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:31:55 +0900 Subject: [PATCH 133/552] =?UTF-8?q?feat(user.views):=20`UserAPIView.SignUp?= =?UTF-8?q?`=EC=97=90=EC=84=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85?= =?UTF-8?q?=EC=8B=9C=20BOJUser=20=EB=AA=A8=EB=8D=B8=EB=8F=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/user/views.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/user/views.py b/src/user/views.py index f28e8ce..3d80644 100644 --- a/src/user/views.py +++ b/src/user/views.py @@ -5,12 +5,15 @@ login, logout, ) +from django.db.transaction import atomic from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response from rest_framework.generics import * from rest_framework.permissions import * +from boj.models import BOJUser + from .models import * from .serializers import * @@ -27,7 +30,18 @@ class SignUp(CreateAPIView): permission_classes = [AllowAny] def perform_create(self, serializer): - serializer.instance = User.objects.create_user(**serializer.validated_data) + boj_id = None + if 'boj_id' in serializer.validated_data: + boj_id = serializer.validated_data.pop('boj_id') + with atomic(): + user = User.objects.create_user(**serializer.validated_data) + user.save() + if boj_id is not None: + boj_user = BOJUser.objects.create(user=user, boj_id=boj_id) + boj_user.save() + user.boj_user = boj_user + user.save() + serializer.instance = user class SignIn(GenericAPIView): From 8b0774146b37becf977e17a069b727b37aa4ed43 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 00:32:09 +0900 Subject: [PATCH 134/552] fix(problem.views): fix permission grammer --- src/problem/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/problem/views.py b/src/problem/views.py index 34985e5..1d22eac 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -28,14 +28,14 @@ def get_queryset(self): class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Problem.objects.all() serializer_class = ProblemSerializer - permission_classes = [IsAdminUser or IsProblemCreator] + permission_classes = [IsAdminUser | IsProblemCreator] lookup_url_kwarg = 'id' class ProblemAnalysisAPIView: class Retrieve(RetrieveAPIView): queryset = ProblemAnalysis.objects.all() - permission_classes = [IsAdminUser or (IsProblemCreator and ReadOnly)] + permission_classes = [IsAdminUser | (IsProblemCreator & ReadOnly)] serializer_class = ProblemAnalysisSerializer lookup_url_kwarg = 'id' lookup_field = 'problem__id' From 18a130da5553194f8c2715a4bef47ca0d4ba9e71 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 01:05:48 +0900 Subject: [PATCH 135/552] chore(config): create settings_debug.py --- src/config/settings_debug.py | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/config/settings_debug.py diff --git a/src/config/settings_debug.py b/src/config/settings_debug.py new file mode 100644 index 0000000..35a63de --- /dev/null +++ b/src/config/settings_debug.py @@ -0,0 +1,4 @@ +from config.settings import * + + +DEBUG = True From 1dce5f31c50cdb1e5265cfbfc1fc8377319ebcd9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 01:49:13 +0900 Subject: [PATCH 136/552] chore(user.views): create views.py --- src/user/views.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 src/user/views.py diff --git a/src/user/views.py b/src/user/views.py new file mode 100644 index 0000000..66960e9 --- /dev/null +++ b/src/user/views.py @@ -0,0 +1,5 @@ +from rest_framework.generics import * +from rest_framework.permissions import * + +from .models import * +from .serializers import * From 32c28c10a8dac35eb9a0c256583b1b99a2eeb0e4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 06:33:23 +0900 Subject: [PATCH 137/552] chore(config.urls): make draft of overall API endpoints --- src/config/urls.py | 66 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/src/config/urls.py b/src/config/urls.py index e83dfe5..136a805 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -17,13 +17,75 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import path +from django.urls import ( + include, + path, +) -API_VERSION_PREFIX = "api/v1" +VIEW_PLACE_HOLDER = lambda request: NotImplemented + urlpatterns = [ path("admin/", admin.site.urls), + path("api/", include([ + path("v1/", include([ + path("account/", include([ + path("signup", VIEW_PLACE_HOLDER), # TODO: 회원가입 기능 구현 + path("signin", VIEW_PLACE_HOLDER), # TODO: 로그인 기능 구현 + path("signout", VIEW_PLACE_HOLDER), # TODO: 로그아웃 기능 구현 + ])), + path("user/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 사용자 목록 조회 기능 구현 (관리자용) + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 사용자 상세 조회+수정 기능 구현 (관리자용) + ])), + ])), + path("problem/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 전체 문제 목록 조회(관리자용) + 생성 기능 구현 + path("my", VIEW_PLACE_HOLDER), # TODO: 내가 만든 문제 목록 조회 기능 구현 + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 문제 상세 조회 기능 구현 + path("analysis", VIEW_PLACE_HOLDER), # TODO: 문제 분석 조회 기능 구현 + ])), + ])), + path("crew/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 전체 크루 목록 조회(관리자용) + 생성 기능 구현 + path("my", VIEW_PLACE_HOLDER), # TODO: 내가 속한 크루 목록 조회 기능 구현 + path("recruiting", VIEW_PLACE_HOLDER), # TODO: 크루원을 모집 중인 크루 목록 조회 기능 구현 + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 + path("activities", VIEW_PLACE_HOLDER), # TODO: 크루의 활동 회차 목록 조회 기능 구현 + path("problems", VIEW_PLACE_HOLDER), # TODO: 크루에 속한 문제 목록 조회 기능 구현 + path("pending", VIEW_PLACE_HOLDER), # TODO: 크루 가입 대기자 목록 조회 기능 구현 + ])), + ])), + path("activity/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 회차 목록 조회 + 추가(방장만) 기능 구현 + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 회차 상세 조회 기능 구현 + ])), + ])), + path("submission/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 문제에 대한 풀이 제출 + 목록 조회 기능 구현 + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 제출 상세 조회+수정 기능 구현 + ])), + ])), + path("comment/",include([ + path("", VIEW_PLACE_HOLDER), # TODO: 코멘트 목록 조회(관리자용) + 생성 기능 구현 + path("/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 코멘트 상세 조회+수정+삭제 기능 구현 + ])), + ])), + path("tag/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 전체 태그 목록 조회(관리자용) + 생성 기능 구현 + ])), + path("language/", include([ + path("", VIEW_PLACE_HOLDER), # TODO: 전체 언어 목록 조회(관리자용) + 생성 기능 구현 + ])), + ])), + ])), ] # Static files From d1693df64048300068ea4755e56295a074050310 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 06:51:17 +0900 Subject: [PATCH 138/552] feat(config.urls): enable `api/v1/tag/`, `api/v1/language/` --- src/config/urls.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/config/urls.py b/src/config/urls.py index 136a805..8f4771f 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -23,6 +23,9 @@ ) +from core.views import * + + VIEW_PLACE_HOLDER = lambda request: NotImplemented @@ -79,10 +82,10 @@ ])), ])), path("tag/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 전체 태그 목록 조회(관리자용) + 생성 기능 구현 + path("", TagAPIView.ListCreate.as_view()), # 전체 태그 목록 조회(관리자용) + 생성 기능 구현 ])), path("language/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 전체 언어 목록 조회(관리자용) + 생성 기능 구현 + path("", LanguageAPIView.ListCreate.as_view()), # 전체 언어 목록 조회(관리자용) + 생성 기능 구현 ])), ])), ])), From bd412101751b8318620f24f2941311ebd639dfb9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:03:41 +0900 Subject: [PATCH 139/552] =?UTF-8?q?feat(core.views):=20`page=5Fsize=3D250`?= =?UTF-8?q?=20=EC=9C=BC=EB=A1=9C=20Pagination=20=ED=99=9C=EC=84=B1?= =?UTF-8?q?=ED=99=94=20(`TagAPIView`,=20`LanguageAPIView`)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/views.py b/src/core/views.py index ef92768..031ad4f 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -1,4 +1,5 @@ from rest_framework.generics import * +from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import * from config.permissions import * @@ -7,11 +8,17 @@ from .serializers import * +class _PageNumberPagination(PageNumberPagination): + page_size = 250 + page_size_query_param = 'page_size' + + class TagAPIView: class ListCreate(ListCreateAPIView): queryset = Tag.objects.all() serializer_class = TagSerializer permission_classes = [IsAdminUser|ReadOnly] + pagination_class = _PageNumberPagination class LanguageAPIView: @@ -19,3 +26,4 @@ class ListCreate(ListCreateAPIView): queryset = Language.objects.all() serializer_class = LanguageSerializer permission_classes = [IsAdminUser|ReadOnly] + pagination_class = _PageNumberPagination From 9da4b4f7412316833eb50ede7f124375e47c3f95 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:12:29 +0900 Subject: [PATCH 140/552] feat(config.urls): enable `api/v1/account/(signin/signup/signout)` --- src/config/urls.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/config/urls.py b/src/config/urls.py index 8f4771f..a82d0bf 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -24,6 +24,7 @@ from core.views import * +from user.views import * VIEW_PLACE_HOLDER = lambda request: NotImplemented @@ -34,9 +35,9 @@ path("api/", include([ path("v1/", include([ path("account/", include([ - path("signup", VIEW_PLACE_HOLDER), # TODO: 회원가입 기능 구현 - path("signin", VIEW_PLACE_HOLDER), # TODO: 로그인 기능 구현 - path("signout", VIEW_PLACE_HOLDER), # TODO: 로그아웃 기능 구현 + path("signup", UserAPIView.SignUp.as_view()), # 회원가입 기능 구현 + path("signin", UserAPIView.SignIn.as_view()), # 로그인 기능 구현 + path("signout", UserAPIView.SignOut.as_view()), # 로그아웃 기능 구현 ])), path("user/", include([ path("", VIEW_PLACE_HOLDER), # TODO: 사용자 목록 조회 기능 구현 (관리자용) From 9afd179385d1b9f855656cf72014786cdfc63cb8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 21 Jun 2024 01:07:40 +0900 Subject: [PATCH 141/552] =?UTF-8?q?feat(core.views):=20`TagAPIView`,=20`La?= =?UTF-8?q?nguageAPIView`=20=EB=8A=94=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=ED=95=B4=EC=95=BC=EB=A7=8C=20=EC=A1=B0=ED=9A=8C=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5,=20=EC=88=98=EC=A0=95=EC=9D=80=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=EB=A7=8C=20=EA=B0=80=EB=8A=A5=ED=95=98=EA=B2=8C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/views.py b/src/core/views.py index 031ad4f..663279f 100644 --- a/src/core/views.py +++ b/src/core/views.py @@ -2,7 +2,7 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import * -from config.permissions import * +from config.permissions import ReadOnly from .models import * from .serializers import * @@ -17,7 +17,7 @@ class TagAPIView: class ListCreate(ListCreateAPIView): queryset = Tag.objects.all() serializer_class = TagSerializer - permission_classes = [IsAdminUser|ReadOnly] + permission_classes = [IsAdminUser | (IsAuthenticated & ReadOnly)] pagination_class = _PageNumberPagination @@ -25,5 +25,5 @@ class LanguageAPIView: class ListCreate(ListCreateAPIView): queryset = Language.objects.all() serializer_class = LanguageSerializer - permission_classes = [IsAdminUser|ReadOnly] + permission_classes = [IsAdminUser | (IsAuthenticated & ReadOnly)] pagination_class = _PageNumberPagination From 998e3673c1990d43c7a1f0d0cfc4fea426fcc08c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:19:56 +0900 Subject: [PATCH 142/552] feat(problem.views): Add pagination to `ProblemAPIView.ListCreate` --- src/problem/views.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 1d22eac..14b3737 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -1,4 +1,5 @@ from rest_framework.generics import * +from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import * from config.permissions import ReadOnly @@ -7,6 +8,12 @@ from .serializers import * +class _PageNumberPagination(PageNumberPagination): + page_size = 20 + page_size_query_param = 'page_size' + max_page_size = 100 + + class IsProblemCreator(BasePermission): def has_object_permission(self, request, view, obj: Problem) -> bool: return bool( @@ -19,6 +26,7 @@ def has_object_permission(self, request, view, obj: Problem) -> bool: class ProblemAPIView: class ListCreate(ListCreateAPIView): serializer_class = ProblemSerializer + pagination_class = _PageNumberPagination permission_classes = [IsAuthenticated] def get_queryset(self): From 6198d6b327a2aeae887d369ee6065d4102336bc5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:24:54 +0900 Subject: [PATCH 143/552] =?UTF-8?q?feat(problem.views):=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EB=8A=94=20=EB=AA=A8=EB=93=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/problem/views.py b/src/problem/views.py index 14b3737..06f0a1e 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -30,7 +30,11 @@ class ListCreate(ListCreateAPIView): permission_classes = [IsAuthenticated] def get_queryset(self): - return Problem.objects.filter(user=self.request.user) + user = self.request.user + if user.is_staff: + return Problem.objects.all() + # TODO: 공개된 문제도 보여주기 + return Problem.objects.filter(user=user) class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): From 4422311e4c76572cca489ea8b585c2b9d97403d8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:38:07 +0900 Subject: [PATCH 144/552] feat(problem.views): add `ProblemAPIView.MyList` for retrieving user-specific problem list --- src/problem/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/problem/views.py b/src/problem/views.py index 06f0a1e..8523d82 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -37,6 +37,16 @@ def get_queryset(self): return Problem.objects.filter(user=user) + class MyList(ListAPIView): + """내가 만든 문제 목록 조회""" + serializer_class = ProblemSerializer + pagination_class = _PageNumberPagination + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Problem.objects.filter(user=self.request.user) + + class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Problem.objects.all() serializer_class = ProblemSerializer From 46fb78bd081378f7642a295e0d7d5e78f6ee1e1c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:39:28 +0900 Subject: [PATCH 145/552] feat(config.permissions): create `WriteOnly` permission --- src/config/permissions.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/config/permissions.py b/src/config/permissions.py index 65e0dcc..724fd69 100644 --- a/src/config/permissions.py +++ b/src/config/permissions.py @@ -6,6 +6,7 @@ __all__ = ( 'ReadOnly', + 'WriteOnly', ) @@ -14,3 +15,10 @@ def has_permission(self, request, view): return bool( request.method in SAFE_METHODS, ) + + +class WriteOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method == 'POST', + ) From 70652a3e5be65df6132f9ef5b3e52ca2da00ac33 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:46:45 +0900 Subject: [PATCH 146/552] =?UTF-8?q?squash(problem.views):=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90=EB=8A=94=20=EB=AA=A8=EB=93=A0=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=EB=A5=BC=20=EC=A1=B0=ED=9A=8C=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/problem/views.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/problem/views.py b/src/problem/views.py index 8523d82..d8ceb15 100644 --- a/src/problem/views.py +++ b/src/problem/views.py @@ -25,16 +25,20 @@ def has_object_permission(self, request, view, obj: Problem) -> bool: class ProblemAPIView: class ListCreate(ListCreateAPIView): + """전체 문제 목록 조회 + 생성 기능 + + - 관리자는 전체 문제 목록을 조회할 수 있습니다. + - 관리자가 아닌 일반 사용자는 자신이 만든 문제만 조회할 수 있습니다. + """ serializer_class = ProblemSerializer pagination_class = _PageNumberPagination permission_classes = [IsAuthenticated] def get_queryset(self): - user = self.request.user - if user.is_staff: + if self.request.user.is_staff: return Problem.objects.all() - # TODO: 공개된 문제도 보여주기 - return Problem.objects.filter(user=user) + # TODO: 공개된 문제도 보여주도록 기능 추가 + return Problem.objects.filter(user=self.request.user) class MyList(ListAPIView): From 961315fa0d298bad7b922e55e2918ff79c9b630f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 23 Jun 2024 07:47:12 +0900 Subject: [PATCH 147/552] feat(config.urls): Update URLs for problem-related views --- src/config/urls.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/config/urls.py b/src/config/urls.py index a82d0bf..2158df0 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -24,6 +24,7 @@ from core.views import * +from problem.views import * from user.views import * @@ -46,11 +47,11 @@ ])), ])), path("problem/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 전체 문제 목록 조회(관리자용) + 생성 기능 구현 - path("my", VIEW_PLACE_HOLDER), # TODO: 내가 만든 문제 목록 조회 기능 구현 + path("", ProblemAPIView.ListCreate.as_view()), # 전체 문제 목록 조회(관리자용) + 생성 기능 + path("my", ProblemAPIView.MyList.as_view()), # 내가 만든 문제 목록 조회 기능 구현 path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 문제 상세 조회 기능 구현 - path("analysis", VIEW_PLACE_HOLDER), # TODO: 문제 분석 조회 기능 구현 + path("", ProblemAPIView.RetrieveUpdateDestroy.as_view()), # 문제 상세 조회 기능 구현 + path("analysis", ProblemAnalysisAPIView.Retrieve.as_view()), # 문제 분석 조회 기능 구현 ])), ])), path("crew/", include([ From fb4c7aec76a81eb33584b991b5699a7cc4e87512 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 05:50:28 +0900 Subject: [PATCH 148/552] =?UTF-8?q?refactor(crew.views):=20`=5Fget=5Fuser(?= =?UTF-8?q?)`=20=EB=A5=BC=20=EC=A0=84=EC=97=AD=20=ED=95=A8=EC=88=98?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/views.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/crew/views.py b/src/crew/views.py index 1277f70..5172b73 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -1,3 +1,5 @@ +import logging + from rest_framework.generics import * from rest_framework.permissions import * @@ -7,6 +9,23 @@ from .serializers import * +logger = logging.getLogger(__name__) + + +def _get_user(view: GenericAPIView) -> User: + user = view.request.user + try: + return User.objects.get(pk=user.pk) + except User.DoesNotExist: + logger.error(f'User not found. {user.pk}') + logger.error( + f'checking user model... ' + f'expected: {User.__class__} ' + f'actual: {user.__class__}' + ) + return None + + class CrewAPIView: class ListCreate(ListCreateAPIView): queryset = Crew.objects.all() @@ -14,7 +33,4 @@ class ListCreate(ListCreateAPIView): permission_classes = [IsAuthenticated] def perform_create(self, serializer): - serializer.save(captain=self._get_user()) - - def _get_user(self) -> User: - return User.objects.get(pk=self.request.user.pk) + serializer.save(captain=_get_user(self)) From 0feb5608c77a6156f33d3bd9c37f6d3176181133 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 05:51:27 +0900 Subject: [PATCH 149/552] feat(crew.views): create `CrewAPIView.MyList` --- src/crew/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/crew/views.py b/src/crew/views.py index 5172b73..71a2baa 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -34,3 +34,10 @@ class ListCreate(ListCreateAPIView): def perform_create(self, serializer): serializer.save(captain=_get_user(self)) + + class MyList(ListAPIView): + serializer_class = CrewSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return _get_user(self).crews.all() From b3c77f87ba92027221eb124871ba11bbe32c9e7c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 05:51:43 +0900 Subject: [PATCH 150/552] feat(config.urls): use `CrewAPIView.MyList` --- src/config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/urls.py b/src/config/urls.py index 3019bd8..0b8a72f 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -56,7 +56,7 @@ ])), path("crew/", include([ path("", CrewAPIView.ListCreate.as_view()), # 전체 크루 목록 조회(관리자용) + 생성 - path("my", VIEW_PLACE_HOLDER), # TODO: 내가 속한 크루 목록 조회 기능 구현 + path("my", CrewAPIView.MyList.as_view()), # TODO: 내가 속한 크루 목록 조회 기능 구현 path("recruiting", VIEW_PLACE_HOLDER), # TODO: 크루원을 모집 중인 크루 목록 조회 기능 구현 path("/", include([ path("", VIEW_PLACE_HOLDER), # TODO: 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 From 750d4d051c4fd91e3936e48e1642c26959bd83e7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 06:12:28 +0900 Subject: [PATCH 151/552] =?UTF-8?q?refactor(crew.models):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=EC=97=90=20=EC=86=8D=ED=95=9C=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EB=93=A4=EC=9D=84=20=EA=B0=81=20=EA=B0=9C=EB=B3=84=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/models.py | 333 ------------------ src/crew/models/__init__.py | 7 + src/crew/models/crew.py | 106 ++++++ src/crew/models/crew_activity.py | 30 ++ src/crew/models/crew_activity_problem.py | 40 +++ .../crew_activity_problem_submission.py | 57 +++ ...rew_activity_problem_submission_comment.py | 56 +++ src/crew/models/crew_member.py | 31 ++ src/crew/models/crew_member_request.py | 38 ++ 9 files changed, 365 insertions(+), 333 deletions(-) delete mode 100644 src/crew/models.py create mode 100644 src/crew/models/__init__.py create mode 100644 src/crew/models/crew.py create mode 100644 src/crew/models/crew_activity.py create mode 100644 src/crew/models/crew_activity_problem.py create mode 100644 src/crew/models/crew_activity_problem_submission.py create mode 100644 src/crew/models/crew_activity_problem_submission_comment.py create mode 100644 src/crew/models/crew_member.py create mode 100644 src/crew/models/crew_member_request.py diff --git a/src/crew/models.py b/src/crew/models.py deleted file mode 100644 index c0cf2a3..0000000 --- a/src/crew/models.py +++ /dev/null @@ -1,333 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from boj.models import * -from core.models import * -from problem.models import * -from user.models import * - - -class Crew(models.Model): - name = models.CharField( - max_length=20, - unique=True, - help_text=( - '크루 이름을 입력해주세요. (최대 20자)' - ), - ) - emoji = models.CharField( - max_length=2, - help_text=( - '크루 아이콘을 입력해주세요. (이모지)' - ), - validators=[ - # TODO: 이모지 형식 검사 - ], - null=True, - blank=True, - ) - captain = models.ForeignKey( - User, - on_delete=models.PROTECT, - related_name='crews_as_captain', - help_text=( - '크루장을 입력해주세요.' - ), - ) - notice = models.TextField( - help_text=( - '크루 공지를 입력해주세요.' - ), - null=True, - blank=True, - ) - languages = models.ManyToManyField( - Language, - related_name='crews', - help_text=( - '유저가 사용 가능한 언어를 입력해주세요.' - ), - ) - max_member = models.IntegerField( - help_text=( - '크루 최대 인원을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 최대 인원 제한 - ], - ) - is_boj_user_only = models.BooleanField( - help_text=( - '백준 아이디 필요 여부를 입력해주세요.' - ), - default=False, - ) - min_boj_tier = models.IntegerField( - help_text=( - '최소 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - max_boj_tier = models.IntegerField( - help_text=( - '최대 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - validators=[ - # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 - ], - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - tags = models.JSONField( - help_text=( - '태그를 입력해주세요.' - ), - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'[{self.emoji} {self.name}]' - - def __str__(self) -> str: - member_count = f'({self.members.count()}/{self.max_member})' - return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' - - -class CrewMember(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='members', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crews', - help_text=( - '유저를 입력해주세요.' - ), - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewMemberRequest(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='requests', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crew_requests', - help_text=( - '유저를 입력해주세요.' - ), - ) - message = models.TextField( - help_text=( - '가입 메시지를 입력해주세요.' - ), - null=True, - blank=True, - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewActivity(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='activities', - help_text=( - '크루를 입력해주세요.' - ), - ) - start_at = models.DateTimeField( - help_text=( - '활동 시작 일자를 입력해주세요.' - ), - ) - end_at = models.DateTimeField( - help_text=( - '활동 종료 일자를 입력해주세요.' - ), - ) - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewActivityProblem(models.Model): - activity = models.ForeignKey( - CrewActivity, - on_delete=models.CASCADE, - related_name='problems', - help_text=( - '활동을 입력해주세요.' - ), - ) - problem = models.ForeignKey( - Problem, - on_delete=models.PROTECT, - related_name='activities', - help_text=( - '문제를 입력해주세요.' - ), - ) - order = models.IntegerField( - help_text=( - '문제 순서를 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 다른 문제 순서와 겹치지 않도록 검사 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewActivityProblemSubmission(models.Model): - # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 - activity_problem = models.ForeignKey( - CrewActivityProblem, - on_delete=models.CASCADE, - related_name='submissions', - help_text=( - '활동 문제를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='submissions', - help_text=( - '유저를 입력해주세요.' - ), - ) - code = models.TextField( - help_text=( - '유저의 코드를 입력해주세요.' - ), - ) - language = models.ForeignKey( - Language, - on_delete=models.PROTECT, - related_name='submissions', - help_text=( - '유저의 코드 언어를 입력해주세요.' - ), - ) - is_correct = models.BooleanField( - help_text=( - '유저의 코드가 정답인지 여부를 입력해주세요.' - ), - ) - is_help_needed = models.BooleanField( - help_text=( - '유저의 코드에 도움이 필요한지 여부를 입력해주세요.' - ), - default=False, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - - -class CrewActivityProblemSubmissionComment(models.Model): - submission = models.ForeignKey( - CrewActivityProblemSubmission, - on_delete=models.CASCADE, - related_name='comments', - help_text=( - '제출을 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='comments', - help_text=( - '유저를 입력해주세요.' - ), - ) - content = models.TextField( - help_text=( - '댓글을 입력해주세요.' - ), - ) - line_number_start = models.IntegerField( - help_text=( - '댓글 시작 라인을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - ], - ) - line_number_end = models.IntegerField( - help_text=( - '댓글 종료 라인을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 시작 라인보다 작지 않도록 검사 - # TODO: 코드 라인 수보다 크지 않도록 검사 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - line_range = f'L{self.line_number_start}:L{self.line_number_end}' - return f'{self.submission.__repr__()} ← {self.user.__repr__()} {line_range} "{self.content}"' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/__init__.py b/src/crew/models/__init__.py new file mode 100644 index 0000000..af4057c --- /dev/null +++ b/src/crew/models/__init__.py @@ -0,0 +1,7 @@ +from .crew import Crew +from .crew_activity import CrewActivity +from .crew_activity_problem import CrewActivityProblem +from .crew_activity_problem_submission import CrewActivityProblemSubmission +from .crew_activity_problem_submission_comment import CrewActivityProblemSubmissionComment +from .crew_member import CrewMember +from .crew_member_request import CrewMemberRequest diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py new file mode 100644 index 0000000..ea9c28f --- /dev/null +++ b/src/crew/models/crew.py @@ -0,0 +1,106 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from boj.models import BOJLevel +from core.models import Language +from user.models import User + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '크루 이름을 입력해주세요. (최대 20자)' + ), + ) + emoji = models.CharField( + max_length=2, + help_text=( + '크루 아이콘을 입력해주세요. (이모지)' + ), + validators=[ + # TODO: 이모지 형식 검사 + ], + null=True, + blank=True, + ) + captain = models.ForeignKey( + User, + on_delete=models.PROTECT, + related_name='crews_as_captain', + help_text=( + '크루장을 입력해주세요.' + ), + ) + notice = models.TextField( + help_text=( + '크루 공지를 입력해주세요.' + ), + null=True, + blank=True, + ) + languages = models.ManyToManyField( + Language, + related_name='crews', + help_text=( + '유저가 사용 가능한 언어를 입력해주세요.' + ), + ) + max_member = models.IntegerField( + help_text=( + '크루 최대 인원을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 최대 인원 제한 + ], + ) + is_boj_user_only = models.BooleanField( + help_text=( + '백준 아이디 필요 여부를 입력해주세요.' + ), + default=False, + ) + min_boj_tier = models.IntegerField( + help_text=( + '최소 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + choices=BOJLevel.choices, + blank=True, + null=True, + default=None, + ) + max_boj_tier = models.IntegerField( + help_text=( + '최대 백준 레벨을 입력해주세요. ', + '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' + ), + validators=[ + # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 + ], + choices=BOJLevel.choices, + blank=True, + null=True, + default=None, + ) + tags = models.JSONField( + help_text=( + '태그를 입력해주세요.' + ), + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + return f'[{self.emoji} {self.name}]' + + def __str__(self) -> str: + member_count = f'({self.members.count()}/{self.max_member})' + return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' diff --git a/src/crew/models/crew_activity.py b/src/crew/models/crew_activity.py new file mode 100644 index 0000000..5ee178a --- /dev/null +++ b/src/crew/models/crew_activity.py @@ -0,0 +1,30 @@ +from django.db import models + +from crew.models.crew import Crew + + +class CrewActivity(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='activities', + help_text=( + '크루를 입력해주세요.' + ), + ) + start_at = models.DateTimeField( + help_text=( + '활동 시작 일자를 입력해주세요.' + ), + ) + end_at = models.DateTimeField( + help_text=( + '활동 종료 일자를 입력해주세요.' + ), + ) + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew_activity_problem.py b/src/crew/models/crew_activity_problem.py new file mode 100644 index 0000000..4e6d5b1 --- /dev/null +++ b/src/crew/models/crew_activity_problem.py @@ -0,0 +1,40 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from crew.models.crew_activity import CrewActivity +from problem.models import Problem + + +class CrewActivityProblem(models.Model): + activity = models.ForeignKey( + CrewActivity, + on_delete=models.CASCADE, + related_name='problems', + help_text=( + '활동을 입력해주세요.' + ), + ) + problem = models.ForeignKey( + Problem, + on_delete=models.PROTECT, + related_name='activities', + help_text=( + '문제를 입력해주세요.' + ), + ) + order = models.IntegerField( + help_text=( + '문제 순서를 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 다른 문제 순서와 겹치지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew_activity_problem_submission.py b/src/crew/models/crew_activity_problem_submission.py new file mode 100644 index 0000000..26de633 --- /dev/null +++ b/src/crew/models/crew_activity_problem_submission.py @@ -0,0 +1,57 @@ +from django.db import models + +from core.models import Language +from crew.models.crew_activity_problem import CrewActivityProblem +from user.models import User + + +class CrewActivityProblemSubmission(models.Model): + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 + activity_problem = models.ForeignKey( + CrewActivityProblem, + on_delete=models.CASCADE, + related_name='submissions', + help_text=( + '활동 문제를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='submissions', + help_text=( + '유저를 입력해주세요.' + ), + ) + code = models.TextField( + help_text=( + '유저의 코드를 입력해주세요.' + ), + ) + language = models.ForeignKey( + Language, + on_delete=models.PROTECT, + related_name='submissions', + help_text=( + '유저의 코드 언어를 입력해주세요.' + ), + ) + is_correct = models.BooleanField( + help_text=( + '유저의 코드가 정답인지 여부를 입력해주세요.' + ), + ) + is_help_needed = models.BooleanField( + help_text=( + '유저의 코드에 도움이 필요한지 여부를 입력해주세요.' + ), + default=False, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew_activity_problem_submission_comment.py b/src/crew/models/crew_activity_problem_submission_comment.py new file mode 100644 index 0000000..7aa7afb --- /dev/null +++ b/src/crew/models/crew_activity_problem_submission_comment.py @@ -0,0 +1,56 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from crew.models.crew_activity_problem_submission import CrewActivityProblemSubmission +from user.models import User + + +class CrewActivityProblemSubmissionComment(models.Model): + submission = models.ForeignKey( + CrewActivityProblemSubmission, + on_delete=models.CASCADE, + related_name='comments', + help_text=( + '제출을 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='comments', + help_text=( + '유저를 입력해주세요.' + ), + ) + content = models.TextField( + help_text=( + '댓글을 입력해주세요.' + ), + ) + line_number_start = models.IntegerField( + help_text=( + '댓글 시작 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + ], + ) + line_number_end = models.IntegerField( + help_text=( + '댓글 종료 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 시작 라인보다 작지 않도록 검사 + # TODO: 코드 라인 수보다 크지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + line_range = f'L{self.line_number_start}:L{self.line_number_end}' + return f'{self.submission.__repr__()} ← {self.user.__repr__()} {line_range} "{self.content}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew_member.py b/src/crew/models/crew_member.py new file mode 100644 index 0000000..bf0736c --- /dev/null +++ b/src/crew/models/crew_member.py @@ -0,0 +1,31 @@ +from django.db import models + +from crew.models.crew import Crew +from user.models import User + + +class CrewMember(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='members', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crews', + help_text=( + '유저를 입력해주세요.' + ), + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/src/crew/models/crew_member_request.py b/src/crew/models/crew_member_request.py new file mode 100644 index 0000000..b1e77ec --- /dev/null +++ b/src/crew/models/crew_member_request.py @@ -0,0 +1,38 @@ +from django.db import models + +from crew.models.crew import Crew +from user.models import User + + +class CrewMemberRequest(models.Model): + # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='requests', + help_text=( + '크루를 입력해주세요.' + ), + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='crew_requests', + help_text=( + '유저를 입력해주세요.' + ), + ) + message = models.TextField( + help_text=( + '가입 메시지를 입력해주세요.' + ), + null=True, + blank=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' From 456d12791494de353637b18f3fcfd993f720fe93 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 06:13:50 +0900 Subject: [PATCH 152/552] feat(crew.models): add `is_recruiting` field to `Crew` model --- src/crew/models/crew.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index ea9c28f..7e0e9c1 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -56,6 +56,12 @@ class Crew(models.Model): # TODO: 최대 인원 제한 ], ) + is_recruiting = models.BooleanField( + help_text=( + '모집 중 여부를 입력해주세요.' + ), + default=True, + ) is_boj_user_only = models.BooleanField( help_text=( '백준 아이디 필요 여부를 입력해주세요.' From 7097f2b6101dd59db8145d99905037e79ae9cb00 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 13:26:49 +0900 Subject: [PATCH 153/552] feat(crew.views): add `RecruitingList` API view --- src/crew/views.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/crew/views.py b/src/crew/views.py index 71a2baa..50bd8b0 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -41,3 +41,10 @@ class MyList(ListAPIView): def get_queryset(self): return _get_user(self).crews.all() + + class RecruitingList(ListAPIView): + serializer_class = CrewSerializer + permission_classes = [IsAuthenticated] + + def get_queryset(self): + return Crew.objects.filter(is_recruiting=True) From 1b251edb25f4de2cb5c50262a108b4e5f3f25777 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 13:27:03 +0900 Subject: [PATCH 154/552] feat(config.urls): implement recruiting list functionality --- src/config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/urls.py b/src/config/urls.py index 0b8a72f..c24ce93 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -57,7 +57,7 @@ path("crew/", include([ path("", CrewAPIView.ListCreate.as_view()), # 전체 크루 목록 조회(관리자용) + 생성 path("my", CrewAPIView.MyList.as_view()), # TODO: 내가 속한 크루 목록 조회 기능 구현 - path("recruiting", VIEW_PLACE_HOLDER), # TODO: 크루원을 모집 중인 크루 목록 조회 기능 구현 + path("recruiting", CrewAPIView.RecruitingList.as_view()), # 크루원을 모집 중인 크루 목록 조회 기능 구현 path("/", include([ path("", VIEW_PLACE_HOLDER), # TODO: 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 path("activities", VIEW_PLACE_HOLDER), # TODO: 크루의 활동 회차 목록 조회 기능 구현 From bca831515c992b663a99b84901985bbe96c85de1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 13:27:55 +0900 Subject: [PATCH 155/552] feat(crew.views): create `CrewAPIView.RetrieveUpdateDestroy` --- src/crew/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/crew/views.py b/src/crew/views.py index 50bd8b0..69631f1 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -48,3 +48,9 @@ class RecruitingList(ListAPIView): def get_queryset(self): return Crew.objects.filter(is_recruiting=True) + + class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): + queryset = Crew.objects.all() + serializer_class = CrewSerializer + permission_classes = [IsAuthenticated] + lookup_url_kwarg = 'id' From 0d05f7a613512b57463a4745d0e25464382a6747 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 13:28:48 +0900 Subject: [PATCH 156/552] feat(config.urls): update path for crew detail view --- src/config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/urls.py b/src/config/urls.py index c24ce93..4b73f53 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -59,7 +59,7 @@ path("my", CrewAPIView.MyList.as_view()), # TODO: 내가 속한 크루 목록 조회 기능 구현 path("recruiting", CrewAPIView.RecruitingList.as_view()), # 크루원을 모집 중인 크루 목록 조회 기능 구현 path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 + path("", CrewAPIView.RetrieveUpdateDestroy.as_view()), # 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 path("activities", VIEW_PLACE_HOLDER), # TODO: 크루의 활동 회차 목록 조회 기능 구현 path("problems", VIEW_PLACE_HOLDER), # TODO: 크루에 속한 문제 목록 조회 기능 구현 path("pending", VIEW_PLACE_HOLDER), # TODO: 크루 가입 대기자 목록 조회 기능 구현 From 6890e371076c63c7d439ab7d80d78af6c86ede2d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 13:30:00 +0900 Subject: [PATCH 157/552] feat(config.settings): add wildcard to `ALLOWED_HOSTS` for debugging --- src/config/settings_debug.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config/settings_debug.py b/src/config/settings_debug.py index 35a63de..cb3a077 100644 --- a/src/config/settings_debug.py +++ b/src/config/settings_debug.py @@ -1,4 +1,7 @@ from config.settings import * +ALLOWED_HOSTS = ALLOWED_HOSTS + [ + '*', +] DEBUG = True From 4a4d7e0e271d0c98de52b9134e3b5f89be7da106 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:01:42 +0900 Subject: [PATCH 158/552] =?UTF-8?q?refactor(crew.serializers):=20Serialize?= =?UTF-8?q?r=20=ED=81=B4=EB=9E=98=EC=8A=A4=EA=B0=80=20=EB=8B=A4=EB=A3=A8?= =?UTF-8?q?=EB=8A=94=20=EB=AA=A8=EB=8D=B8=20=EB=B3=84=EB=A1=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EC=9D=84=20=EA=B0=96=EB=8F=84=EB=A1=9D=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/serializers/__init__.py | 0 src/crew/{serializers.py => serializers/crew.py} | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 src/crew/serializers/__init__.py rename src/crew/{serializers.py => serializers/crew.py} (58%) diff --git a/src/crew/serializers/__init__.py b/src/crew/serializers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/crew/serializers.py b/src/crew/serializers/crew.py similarity index 58% rename from src/crew/serializers.py rename to src/crew/serializers/crew.py index 0743ef6..861aab3 100644 --- a/src/crew/serializers.py +++ b/src/crew/serializers/crew.py @@ -1,13 +1,13 @@ -from rest_framework.serializers import * +from rest_framework.serializers import ( + ModelSerializer, +) from core.serializers import LanguageSerializer -from user.serializers import UserSerializer - -from .models import * +from crew.models import Crew +from crew.serializers.crew_member import CrewMemberSerializer class CrewSerializer(ModelSerializer): - captain = UserSerializer(read_only=True) languages = LanguageSerializer(many=True, read_only=True) class Meta: From 009e62d8fb4dcf9821244009388ea043a498b035 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:03:02 +0900 Subject: [PATCH 159/552] feat(crew.serializers): create `CrewMemberSerializer` --- src/crew/serializers/crew_member.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 src/crew/serializers/crew_member.py diff --git a/src/crew/serializers/crew_member.py b/src/crew/serializers/crew_member.py new file mode 100644 index 0000000..0c7eee4 --- /dev/null +++ b/src/crew/serializers/crew_member.py @@ -0,0 +1,20 @@ +from rest_framework.serializers import ( + ModelSerializer, + SerializerMethodField, +) + +from crew.models import CrewMember + + +class CrewMemberSerializer(ModelSerializer): + is_host = SerializerMethodField() + + class Meta: + model = CrewMember + fields = ( + 'user', + 'is_host', + ) + + def get_is_host(self, obj: CrewMember) -> bool: + return obj.crew.captain == obj.user From cf7b37fcc2ef5c916a1ca7db804f272635c4c694 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:03:22 +0900 Subject: [PATCH 160/552] feat(crew.serializers): add field `members` to `CrewSerializer` --- src/crew/serializers/crew.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 861aab3..0bd167c 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -8,6 +8,7 @@ class CrewSerializer(ModelSerializer): + members = CrewMemberSerializer(many=True, read_only=True) languages = LanguageSerializer(many=True, read_only=True) class Meta: From 5fd40dd54b74569d8e7eabb0cb16118b4b4e6954 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:05:09 +0900 Subject: [PATCH 161/552] feat(crew.serializers): add `CrewSerializer` and `CrewMemberSerializer` serializers module --- src/crew/serializers/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/crew/serializers/__init__.py b/src/crew/serializers/__init__.py index e69de29..3b89dbc 100644 --- a/src/crew/serializers/__init__.py +++ b/src/crew/serializers/__init__.py @@ -0,0 +1,8 @@ +from crew.serializers.crew import CrewSerializer +from crew.serializers.crew_member import CrewMemberSerializer + + +__all__ = [ + 'CrewSerializer', + 'CrewMemberSerializer', +] From 76108c883ed509641125bf899c3b9315fa0f7617 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:17:19 +0900 Subject: [PATCH 162/552] feat(crew.serializers): use `UserSerializer` for `user` field to `CrewMemberSerializer` --- src/crew/serializers/crew_member.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/crew/serializers/crew_member.py b/src/crew/serializers/crew_member.py index 0c7eee4..c45dc5d 100644 --- a/src/crew/serializers/crew_member.py +++ b/src/crew/serializers/crew_member.py @@ -4,9 +4,11 @@ ) from crew.models import CrewMember +from user.serializers import UserSerializer class CrewMemberSerializer(ModelSerializer): + user = UserSerializer(read_only=True) is_host = SerializerMethodField() class Meta: From 47c7a530c5c5185eccfe748aeba83673d54fa71c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 14:48:44 +0900 Subject: [PATCH 163/552] =?UTF-8?q?feat(crew.serializers):=20`CrewSerializ?= =?UTF-8?q?er`=EC=9D=98=20=ED=95=84=EB=93=9C=20`members`,=20`tags`?= =?UTF-8?q?=EC=97=90=20=EB=B3=B4=EC=97=AC=EC=A4=84=20=EC=A0=95=EB=B3=B4?= =?UTF-8?q?=EB=A5=BC=20=EA=B0=80=EA=B3=B5=ED=95=98=EC=97=AC=20=EC=A0=84?= =?UTF-8?q?=EB=8B=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (tag 정보는 key, name 딕셔너리로 가공하여 전달함) --- src/crew/serializers/crew.py | 58 ++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 0bd167c..3cb50a8 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -1,16 +1,68 @@ from rest_framework.serializers import ( ModelSerializer, + SerializerMethodField, ) +from boj.models import BOJLevel +from core.models import Language from core.serializers import LanguageSerializer from crew.models import Crew from crew.serializers.crew_member import CrewMemberSerializer class CrewSerializer(ModelSerializer): - members = CrewMemberSerializer(many=True, read_only=True) - languages = LanguageSerializer(many=True, read_only=True) + """크루 둘러보기에서 보여줄 정보 + + 크루 참여자가 아니어도 볼 수 있습니다. + """ + + members = SerializerMethodField() + tags = SerializerMethodField() class Meta: model = Crew - fields = '__all__' + fields = [ + 'name', + 'emoji', + 'members', + 'tags', + 'is_recruiting', + ] + + def get_members(self, obj: Crew): + return { + 'count': obj.members.count(), + 'max_count': obj.max_member, + } + + def get_tags(self, obj: Crew): + tags = [] + # Language tags + for language in obj.languages.all(): + language: Language + tags.append({ + 'key': language.key, + 'name': language.name, + }) + if obj.min_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', + }) + if obj.max_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', + }) + if obj.min_boj_tier is None and obj.max_boj_tier is None: + tags.append({ + 'key': None, + 'name': '티어 무관', + }) + # Custom tags + for tag in obj.tags: + tags.append({ + 'key': None, + 'name': tag, + }) + return tags From 2cbb4fe7ad0d8ed7f528ce636c9f41c79e7519c5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 26 Jun 2024 15:07:20 +0900 Subject: [PATCH 164/552] =?UTF-8?q?chore(crew.views):=20=EC=96=B8=EC=96=B4?= =?UTF-8?q?,=20=ED=8B=B0=EC=96=B4=20=EC=A1=B0=EA=B1=B4=20=EB=B3=84=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=EB=A7=81=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=9E=90=EB=8A=94=20TODO=20?= =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crew/views.py b/src/crew/views.py index 69631f1..aad8f77 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -47,6 +47,7 @@ class RecruitingList(ListAPIView): permission_classes = [IsAuthenticated] def get_queryset(self): + # TODO: 언어, 티어 조건에 따라 필터링 return Crew.objects.filter(is_recruiting=True) class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): From 57260b9f332c6be81a27e0fc34f9770b4f8237e0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 01:09:53 +0900 Subject: [PATCH 165/552] refactor(crew.serializers): rename `CrewSerializer` -> `RecruitingCrewSerializer` --- src/crew/serializers/__init__.py | 4 ++-- src/crew/serializers/crew.py | 4 ++-- src/crew/views.py | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/crew/serializers/__init__.py b/src/crew/serializers/__init__.py index 3b89dbc..9b05811 100644 --- a/src/crew/serializers/__init__.py +++ b/src/crew/serializers/__init__.py @@ -1,8 +1,8 @@ -from crew.serializers.crew import CrewSerializer +from crew.serializers.crew import RecruitingCrewSerializer from crew.serializers.crew_member import CrewMemberSerializer __all__ = [ - 'CrewSerializer', + 'RecruitingCrewSerializer', 'CrewMemberSerializer', ] diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 3cb50a8..904b460 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -10,8 +10,8 @@ from crew.serializers.crew_member import CrewMemberSerializer -class CrewSerializer(ModelSerializer): - """크루 둘러보기에서 보여줄 정보 +class RecruitingCrewSerializer(ModelSerializer): + """<크루 둘러보기> 참가자를 모집 중인 크루 정보 크루 참여자가 아니어도 볼 수 있습니다. """ diff --git a/src/crew/views.py b/src/crew/views.py index aad8f77..d7fa972 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -29,21 +29,21 @@ def _get_user(view: GenericAPIView) -> User: class CrewAPIView: class ListCreate(ListCreateAPIView): queryset = Crew.objects.all() - serializer_class = CrewSerializer + serializer_class = RecruitingCrewSerializer permission_classes = [IsAuthenticated] def perform_create(self, serializer): serializer.save(captain=_get_user(self)) class MyList(ListAPIView): - serializer_class = CrewSerializer + serializer_class = RecruitingCrewSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return _get_user(self).crews.all() class RecruitingList(ListAPIView): - serializer_class = CrewSerializer + serializer_class = RecruitingCrewSerializer permission_classes = [IsAuthenticated] def get_queryset(self): @@ -52,6 +52,6 @@ def get_queryset(self): class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Crew.objects.all() - serializer_class = CrewSerializer + serializer_class = RecruitingCrewSerializer permission_classes = [IsAuthenticated] lookup_url_kwarg = 'id' From 9f74efc0633cfa7d173c71d3eb670ae651b2bcad Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 02:50:28 +0900 Subject: [PATCH 166/552] =?UTF-8?q?docs(crew.serializers):=20`RecruitingLi?= =?UTF-8?q?st`=20=ED=81=B4=EB=9E=98=EC=8A=A4=EC=97=90=20=EC=A3=BC=EC=84=9D?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/crew/views.py b/src/crew/views.py index d7fa972..50503a5 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -43,6 +43,7 @@ def get_queryset(self): return _get_user(self).crews.all() class RecruitingList(ListAPIView): + """현재 참가자를 모집 중인 크루의 목록을 반환합니다.""" serializer_class = RecruitingCrewSerializer permission_classes = [IsAuthenticated] From 9064d20bdc23c9f805f1148176ea267409b00177 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 02:51:02 +0900 Subject: [PATCH 167/552] feat(crew.models): add field `name` to `CrewActivity` --- src/crew/models/crew_activity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/crew/models/crew_activity.py b/src/crew/models/crew_activity.py index 5ee178a..83d447f 100644 --- a/src/crew/models/crew_activity.py +++ b/src/crew/models/crew_activity.py @@ -12,6 +12,11 @@ class CrewActivity(models.Model): '크루를 입력해주세요.' ), ) + name = models.TextField( + help_text=( + '활동 이름을 입력해주세요. (예: "1회차")' + ), + ) start_at = models.DateTimeField( help_text=( '활동 시작 일자를 입력해주세요.' From 27e7e132e2c9c975d55a20af74d10b1e912d2f26 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:07:43 +0900 Subject: [PATCH 168/552] feat(crew.serializers): create `MembersMixin` --- src/crew/serializers/crew.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 904b460..a06bccb 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -10,6 +10,17 @@ from crew.serializers.crew_member import CrewMemberSerializer +class MembersMixin: + def get_member_count(self, obj: Crew): + return obj.members.count() + + def get_member_max_count(self, obj: Crew): + return obj.max_member + + def get_members_list(self, obj: Crew): + return CrewMemberSerializer(obj.members.all(), many=True).data + + class RecruitingCrewSerializer(ModelSerializer): """<크루 둘러보기> 참가자를 모집 중인 크루 정보 From bf0ee0b15d0755593f4da94f57720f72d7dd4c05 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:10:20 +0900 Subject: [PATCH 169/552] feat(crew.serializers): create `TagsMixin` --- src/crew/serializers/crew.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index a06bccb..ab8b7e6 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -21,6 +21,40 @@ def get_members_list(self, obj: Crew): return CrewMemberSerializer(obj.members.all(), many=True).data +class TagsMixin: + def get_tags(self, obj: Crew): + tags = [] + # Language tags + for language in obj.languages.all(): + language: Language + tags.append({ + 'key': language.key, + 'name': language.name, + }) + if obj.min_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', + }) + if obj.max_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', + }) + if obj.min_boj_tier is None and obj.max_boj_tier is None: + tags.append({ + 'key': None, + 'name': '티어 무관', + }) + # Custom tags + for tag in obj.tags: + tags.append({ + 'key': None, + 'name': tag, + }) + return tags + + class RecruitingCrewSerializer(ModelSerializer): """<크루 둘러보기> 참가자를 모집 중인 크루 정보 From 35797fc2eb0ab8f6accc5ab7d43c843b8a3d4106 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:11:58 +0900 Subject: [PATCH 170/552] feat(crew.serializers): add method `get_members()` --- src/crew/serializers/crew.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index ab8b7e6..4e7ec55 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -20,6 +20,13 @@ def get_member_max_count(self, obj: Crew): def get_members_list(self, obj: Crew): return CrewMemberSerializer(obj.members.all(), many=True).data + def get_members(self, obj: Crew): + return { + 'count': self.get_member_count(obj), + 'max_count': self.get_member_max_count(obj), + 'items': self.get_members_list(obj), + } + class TagsMixin: def get_tags(self, obj: Crew): From feffdc825305ba3c3d821cf89384a3ef995ed947 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:13:34 +0900 Subject: [PATCH 171/552] refactor(crew.serializers): use Mixins for `get_members()`, `get_tags()` --- src/crew/serializers/crew.py | 40 +----------------------------------- 1 file changed, 1 insertion(+), 39 deletions(-) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 4e7ec55..d62dab8 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -62,7 +62,7 @@ def get_tags(self, obj: Crew): return tags -class RecruitingCrewSerializer(ModelSerializer): +class RecruitingCrewSerializer(ModelSerializer, MembersMixin, TagsMixin): """<크루 둘러보기> 참가자를 모집 중인 크루 정보 크루 참여자가 아니어도 볼 수 있습니다. @@ -80,41 +80,3 @@ class Meta: 'tags', 'is_recruiting', ] - - def get_members(self, obj: Crew): - return { - 'count': obj.members.count(), - 'max_count': obj.max_member, - } - - def get_tags(self, obj: Crew): - tags = [] - # Language tags - for language in obj.languages.all(): - language: Language - tags.append({ - 'key': language.key, - 'name': language.name, - }) - if obj.min_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', - }) - if obj.max_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', - }) - if obj.min_boj_tier is None and obj.max_boj_tier is None: - tags.append({ - 'key': None, - 'name': '티어 무관', - }) - # Custom tags - for tag in obj.tags: - tags.append({ - 'key': None, - 'name': tag, - }) - return tags From ec84ce96d43a6f62890a6ba76fb1309670995652 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:15:08 +0900 Subject: [PATCH 172/552] refactor(crew.serializers): rename `RecruitingCrewSerializer` -> `CrewSerializer` --- src/crew/serializers/__init__.py | 4 ++-- src/crew/serializers/crew.py | 7 ++----- src/crew/views.py | 9 +++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/crew/serializers/__init__.py b/src/crew/serializers/__init__.py index 9b05811..3b89dbc 100644 --- a/src/crew/serializers/__init__.py +++ b/src/crew/serializers/__init__.py @@ -1,8 +1,8 @@ -from crew.serializers.crew import RecruitingCrewSerializer +from crew.serializers.crew import CrewSerializer from crew.serializers.crew_member import CrewMemberSerializer __all__ = [ - 'RecruitingCrewSerializer', + 'CrewSerializer', 'CrewMemberSerializer', ] diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index d62dab8..d3a02b5 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -62,11 +62,8 @@ def get_tags(self, obj: Crew): return tags -class RecruitingCrewSerializer(ModelSerializer, MembersMixin, TagsMixin): - """<크루 둘러보기> 참가자를 모집 중인 크루 정보 - - 크루 참여자가 아니어도 볼 수 있습니다. - """ +class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): + """크루에 대한 공개된 데이터를 다룬다.""" members = SerializerMethodField() tags = SerializerMethodField() diff --git a/src/crew/views.py b/src/crew/views.py index 50503a5..56eda95 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -29,14 +29,15 @@ def _get_user(view: GenericAPIView) -> User: class CrewAPIView: class ListCreate(ListCreateAPIView): queryset = Crew.objects.all() - serializer_class = RecruitingCrewSerializer + serializer_class = CrewSerializer permission_classes = [IsAuthenticated] def perform_create(self, serializer): serializer.save(captain=_get_user(self)) class MyList(ListAPIView): - serializer_class = RecruitingCrewSerializer + """내가 속한 크루의 목록을 반환합니다.""" + serializer_class = CrewSerializer permission_classes = [IsAuthenticated] def get_queryset(self): @@ -44,7 +45,7 @@ def get_queryset(self): class RecruitingList(ListAPIView): """현재 참가자를 모집 중인 크루의 목록을 반환합니다.""" - serializer_class = RecruitingCrewSerializer + serializer_class = CrewSerializer permission_classes = [IsAuthenticated] def get_queryset(self): @@ -53,6 +54,6 @@ def get_queryset(self): class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): queryset = Crew.objects.all() - serializer_class = RecruitingCrewSerializer + serializer_class = CrewSerializer permission_classes = [IsAuthenticated] lookup_url_kwarg = 'id' From 3895364a43e65162094ee0aa70cd2096ff192d1d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:16:35 +0900 Subject: [PATCH 173/552] refactor(crew.serializers): rename `is_host` -> `is_captain` for `CrewMemberSerializer` --- src/crew/serializers/crew_member.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crew/serializers/crew_member.py b/src/crew/serializers/crew_member.py index c45dc5d..2dbf512 100644 --- a/src/crew/serializers/crew_member.py +++ b/src/crew/serializers/crew_member.py @@ -9,14 +9,14 @@ class CrewMemberSerializer(ModelSerializer): user = UserSerializer(read_only=True) - is_host = SerializerMethodField() + is_captain = SerializerMethodField() class Meta: model = CrewMember fields = ( 'user', - 'is_host', + 'is_captain', ) - def get_is_host(self, obj: CrewMember) -> bool: + def get_is_captain(self, obj: CrewMember) -> bool: return obj.crew.captain == obj.user From 090a88899ec02c22fd90685e4075706003aae7b2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:20:20 +0900 Subject: [PATCH 174/552] =?UTF-8?q?chore(crew.serializers):=20=ED=95=84?= =?UTF-8?q?=EB=93=9C=20=EC=88=9C=EC=84=9C=20=EB=B3=80=EA=B2=BD,=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/serializers/crew.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index d3a02b5..64c9294 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -5,7 +5,6 @@ from boj.models import BOJLevel from core.models import Language -from core.serializers import LanguageSerializer from crew.models import Crew from crew.serializers.crew_member import CrewMemberSerializer @@ -71,8 +70,8 @@ class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): class Meta: model = Crew fields = [ - 'name', 'emoji', + 'name', 'members', 'tags', 'is_recruiting', From 3de3f54e205733293be32d690c15d98f1736520d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:20:57 +0900 Subject: [PATCH 175/552] =?UTF-8?q?feat(crew.serializers):=20`TagsMixin`?= =?UTF-8?q?=20=EC=9D=98=20`get=5Ftag()`=20=EB=B0=98=ED=99=98=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9D=84=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=94=95=EC=85=94=EB=84=88=EB=A6=AC=20=ED=98=95?= =?UTF-8?q?=ED=83=9C=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/serializers/crew.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 64c9294..09664e9 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -58,7 +58,10 @@ def get_tags(self, obj: Crew): 'key': None, 'name': tag, }) - return tags + return { + 'count': len(tags), + 'items': tags, + } class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): From ab3794bd640d512360ca7ae7dfbede1e1621bf93 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:23:13 +0900 Subject: [PATCH 176/552] =?UTF-8?q?refactor(crew.serializers):=20Mixin=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EB=93=A4=EC=9D=84=20mixins.py=20=EB=A1=9C=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/serializers/crew.py | 62 +++------------------------------- src/crew/serializers/mixins.py | 59 ++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 58 deletions(-) create mode 100644 src/crew/serializers/mixins.py diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 09664e9..11cbeac 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -3,65 +3,11 @@ SerializerMethodField, ) -from boj.models import BOJLevel -from core.models import Language +from crew.serializers.mixins import ( + MembersMixin, + TagsMixin, +) from crew.models import Crew -from crew.serializers.crew_member import CrewMemberSerializer - - -class MembersMixin: - def get_member_count(self, obj: Crew): - return obj.members.count() - - def get_member_max_count(self, obj: Crew): - return obj.max_member - - def get_members_list(self, obj: Crew): - return CrewMemberSerializer(obj.members.all(), many=True).data - - def get_members(self, obj: Crew): - return { - 'count': self.get_member_count(obj), - 'max_count': self.get_member_max_count(obj), - 'items': self.get_members_list(obj), - } - - -class TagsMixin: - def get_tags(self, obj: Crew): - tags = [] - # Language tags - for language in obj.languages.all(): - language: Language - tags.append({ - 'key': language.key, - 'name': language.name, - }) - if obj.min_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', - }) - if obj.max_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', - }) - if obj.min_boj_tier is None and obj.max_boj_tier is None: - tags.append({ - 'key': None, - 'name': '티어 무관', - }) - # Custom tags - for tag in obj.tags: - tags.append({ - 'key': None, - 'name': tag, - }) - return { - 'count': len(tags), - 'items': tags, - } class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): diff --git a/src/crew/serializers/mixins.py b/src/crew/serializers/mixins.py new file mode 100644 index 0000000..3837255 --- /dev/null +++ b/src/crew/serializers/mixins.py @@ -0,0 +1,59 @@ +from boj.models import BOJLevel +from core.models import Language +from crew.models import Crew +from crew.serializers.crew_member import CrewMemberSerializer + + +class MembersMixin: + def get_member_count(self, obj: Crew): + return obj.members.count() + + def get_member_max_count(self, obj: Crew): + return obj.max_member + + def get_members_list(self, obj: Crew): + return CrewMemberSerializer(obj.members.all(), many=True).data + + def get_members(self, obj: Crew): + return { + 'count': self.get_member_count(obj), + 'max_count': self.get_member_max_count(obj), + 'items': self.get_members_list(obj), + } + + +class TagsMixin: + def get_tags(self, obj: Crew): + tags = [] + # Language tags + for language in obj.languages.all(): + language: Language + tags.append({ + 'key': language.key, + 'name': language.name, + }) + if obj.min_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', + }) + if obj.max_boj_tier is not None: + tags.append({ + 'key': None, + 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', + }) + if obj.min_boj_tier is None and obj.max_boj_tier is None: + tags.append({ + 'key': None, + 'name': '티어 무관', + }) + # Custom tags + for tag in obj.tags: + tags.append({ + 'key': None, + 'name': tag, + }) + return { + 'count': len(tags), + 'items': tags, + } From f442ae37f662f450dc340cb7b9ca2226d1140923 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 03:32:56 +0900 Subject: [PATCH 177/552] =?UTF-8?q?refactor(crew.serializers):=20Mixin=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=ED=8C=A9=ED=84=B0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/serializers/mixins.py | 58 +++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/crew/serializers/mixins.py b/src/crew/serializers/mixins.py index 3837255..5e931cd 100644 --- a/src/crew/serializers/mixins.py +++ b/src/crew/serializers/mixins.py @@ -1,3 +1,7 @@ +from typing import ( + Iterable, +) + from boj.models import BOJLevel from core.models import Language from crew.models import Crew @@ -5,33 +9,41 @@ class MembersMixin: - def get_member_count(self, obj: Crew): + def get_members(self, obj: Crew): + return { + 'count': self._get_member_count(obj), + 'max_count': self._get_member_max_count(obj), + 'items': self._get_members_list(obj), + } + + def _get_member_count(self, obj: Crew): return obj.members.count() - def get_member_max_count(self, obj: Crew): + def _get_member_max_count(self, obj: Crew): return obj.max_member - def get_members_list(self, obj: Crew): + def _get_members_list(self, obj: Crew): return CrewMemberSerializer(obj.members.all(), many=True).data - def get_members(self, obj: Crew): + +class TagsMixin: + def get_tags(self, obj: Crew): + tags = [ + *self._get_language_tags(obj), + *self._get_tier_tags(obj), + *self._get_custom_tags(obj), + ] return { - 'count': self.get_member_count(obj), - 'max_count': self.get_member_max_count(obj), - 'items': self.get_members_list(obj), + 'count': len(tags), + 'items': tags, } + def _get_language_tags(self, obj: Crew): + languages: Iterable[Language] = obj.languages.all() + return [{'key': lang.key, 'name': lang.name} for lang in languages] -class TagsMixin: - def get_tags(self, obj: Crew): + def _get_tier_tags(self, obj: Crew): tags = [] - # Language tags - for language in obj.languages.all(): - language: Language - tags.append({ - 'key': language.key, - 'name': language.name, - }) if obj.min_boj_tier is not None: tags.append({ 'key': None, @@ -47,13 +59,7 @@ def get_tags(self, obj: Crew): 'key': None, 'name': '티어 무관', }) - # Custom tags - for tag in obj.tags: - tags.append({ - 'key': None, - 'name': tag, - }) - return { - 'count': len(tags), - 'items': tags, - } + return tags + + def _get_custom_tags(self, obj: Crew): + return [{'key': None, 'name': tag} for tag in obj.tags] From 4d3941bcd8484fcbd2712c8a04132c1c15ce96c3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 16:18:51 +0900 Subject: [PATCH 178/552] chore(config.urls): remove TODO comment --- src/config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/urls.py b/src/config/urls.py index 4b73f53..8ada45c 100644 --- a/src/config/urls.py +++ b/src/config/urls.py @@ -56,7 +56,7 @@ ])), path("crew/", include([ path("", CrewAPIView.ListCreate.as_view()), # 전체 크루 목록 조회(관리자용) + 생성 - path("my", CrewAPIView.MyList.as_view()), # TODO: 내가 속한 크루 목록 조회 기능 구현 + path("my", CrewAPIView.MyList.as_view()), # 내가 속한 크루 목록 조회 기능 구현 path("recruiting", CrewAPIView.RecruitingList.as_view()), # 크루원을 모집 중인 크루 목록 조회 기능 구현 path("/", include([ path("", CrewAPIView.RetrieveUpdateDestroy.as_view()), # 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 From 36813496950d1df168e48e23fc81dca3bc2b5f49 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 16:25:30 +0900 Subject: [PATCH 179/552] feat(crew.serializers): create `CrewActivitySerializer` --- src/crew/serializers/crew_activity.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/crew/serializers/crew_activity.py diff --git a/src/crew/serializers/crew_activity.py b/src/crew/serializers/crew_activity.py new file mode 100644 index 0000000..e1be6ff --- /dev/null +++ b/src/crew/serializers/crew_activity.py @@ -0,0 +1,23 @@ +from rest_framework.serializers import ( + ModelSerializer, + SerializerMethodField, +) + +from crew.models import CrewActivity + + +class CrewActivitySerializer(ModelSerializer): + date = SerializerMethodField() + + class Meta: + model = CrewActivity + fields = ( + 'name', + 'date', + ) + + def get_date(self, obj: CrewActivity): + return { + 'start_at': obj.start_at, + 'end_at': obj.end_at, + } From ee87057dc30da0607aa5332e2ed73f07d41de25d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 16:28:01 +0900 Subject: [PATCH 180/552] feat(crew.models): add field `is_active` to `Crew` --- src/crew/models/crew.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/crew/models/crew.py b/src/crew/models/crew.py index 7e0e9c1..73798b5 100644 --- a/src/crew/models/crew.py +++ b/src/crew/models/crew.py @@ -62,6 +62,12 @@ class Crew(models.Model): ), default=True, ) + is_active = models.BooleanField( + help_text=( + '활동 중인지 여부를 입력해주세요.' + ), + default=True, + ) is_boj_user_only = models.BooleanField( help_text=( '백준 아이디 필요 여부를 입력해주세요.' From ac470b811f3d89aee7c2db643d5bcb330bac3703 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 16:32:05 +0900 Subject: [PATCH 181/552] feat(crew.serializers): create `CrewDetailSerializer` --- src/crew/serializers/__init__.py | 6 +++++- src/crew/serializers/crew.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/crew/serializers/__init__.py b/src/crew/serializers/__init__.py index 3b89dbc..3520fa7 100644 --- a/src/crew/serializers/__init__.py +++ b/src/crew/serializers/__init__.py @@ -1,8 +1,12 @@ -from crew.serializers.crew import CrewSerializer +from crew.serializers.crew import ( + CrewSerializer, + CrewDetailSerializer, +) from crew.serializers.crew_member import CrewMemberSerializer __all__ = [ 'CrewSerializer', + 'CrewDetailSerializer', 'CrewMemberSerializer', ] diff --git a/src/crew/serializers/crew.py b/src/crew/serializers/crew.py index 11cbeac..c967fee 100644 --- a/src/crew/serializers/crew.py +++ b/src/crew/serializers/crew.py @@ -8,6 +8,7 @@ TagsMixin, ) from crew.models import Crew +from crew.serializers.crew_activity import CrewActivitySerializer class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): @@ -25,3 +26,23 @@ class Meta: 'tags', 'is_recruiting', ] + + +class CrewDetailSerializer(ModelSerializer, MembersMixin, TagsMixin): + """크루에 대한 상세 데이터를 다룬다.""" + + members = SerializerMethodField() + tags = SerializerMethodField() + activities = CrewActivitySerializer(many=True, read_only=True) + + class Meta: + model = Crew + fields = [ + 'emoji', + 'name', + 'members', + 'tags', + 'activities', + 'is_active', + 'is_recruiting', + ] From 2a54179510513b8b24eaad475ff1c8e49e6e3466 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 30 Jun 2024 16:39:28 +0900 Subject: [PATCH 182/552] =?UTF-8?q?feat(crew.views):=20`MyList`=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=8A=94=20=EC=9E=90=EC=8B=A0=EC=9D=B4=20=EA=B0=80?= =?UTF-8?q?=EC=9E=85=ED=95=9C=20=ED=81=AC=EB=A3=A8=EB=A7=8C=20=EB=B3=B4?= =?UTF-8?q?=EC=9D=B4=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/crew/views.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/crew/views.py b/src/crew/views.py index 56eda95..500dd25 100644 --- a/src/crew/views.py +++ b/src/crew/views.py @@ -37,11 +37,12 @@ def perform_create(self, serializer): class MyList(ListAPIView): """내가 속한 크루의 목록을 반환합니다.""" - serializer_class = CrewSerializer + serializer_class = CrewDetailSerializer permission_classes = [IsAuthenticated] def get_queryset(self): - return _get_user(self).crews.all() + user = _get_user(self) + return Crew.objects.filter(members__user=user) class RecruitingList(ListAPIView): """현재 참가자를 모집 중인 크루의 목록을 반환합니다.""" From 16120749a674b12873ce47ed813e7da61ab44b9d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 3 Jul 2024 13:50:26 +0900 Subject: [PATCH 183/552] =?UTF-8?q?docs:=20use-case=20diagram=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/diagrams.drawio | 885 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 885 insertions(+) create mode 100644 docs/diagrams.drawio diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio new file mode 100644 index 0000000..96effc1 --- /dev/null +++ b/docs/diagrams.drawio @@ -0,0 +1,885 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From c86ff64196357ecb22853a94c5df8df73ab046ac Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 02:53:24 +0900 Subject: [PATCH 184/552] chore: rename root dir src -> app --- {src/boj => app/app}/__init__.py | 0 {src/config => app/app}/asgi.py | 2 +- {src/config => app/app}/permissions.py | 0 {src/config => app/app}/settings.py | 8 ++++---- {src/config => app/app}/settings_debug.py | 2 +- {src/config => app/app}/urls.py | 0 {src/config => app/app}/views.py | 0 {src/config => app/app}/wsgi.py | 2 +- {src/boj/migrations => app/boj}/__init__.py | 0 {src => app}/boj/admin.py | 0 {src => app}/boj/apps.py | 0 {src/config => app/boj/migrations}/__init__.py | 0 {src => app}/boj/models.py | 0 {src => app}/boj/serializers.py | 0 {src => app}/core/__init__.py | 0 {src => app}/core/admin.py | 0 {src => app}/core/apps.py | 0 {src => app}/core/migrations/__init__.py | 0 {src => app}/core/models.py | 0 {src => app}/core/serializers.py | 0 {src => app}/core/views.py | 2 +- {src => app}/crew/__init__.py | 0 {src => app}/crew/admin.py | 0 {src => app}/crew/apps.py | 0 {src => app}/crew/migrations/__init__.py | 0 {src => app}/crew/models/__init__.py | 0 {src => app}/crew/models/crew.py | 0 {src => app}/crew/models/crew_activity.py | 0 {src => app}/crew/models/crew_activity_problem.py | 0 .../crew/models/crew_activity_problem_submission.py | 0 .../models/crew_activity_problem_submission_comment.py | 0 {src => app}/crew/models/crew_member.py | 0 {src => app}/crew/models/crew_member_request.py | 0 {src => app}/crew/serializers/__init__.py | 0 {src => app}/crew/serializers/crew.py | 0 {src => app}/crew/serializers/crew_activity.py | 0 {src => app}/crew/serializers/crew_member.py | 0 {src => app}/crew/serializers/mixins.py | 0 {src => app}/crew/views.py | 0 {src => app}/manage.py | 2 +- {src => app}/problem/__init__.py | 0 {src => app}/problem/admin.py | 0 {src => app}/problem/apps.py | 0 {src => app}/problem/migrations/__init__.py | 0 {src => app}/problem/models.py | 0 {src => app}/problem/serializers.py | 0 {src => app}/problem/views.py | 2 +- {src => app}/user/__init__.py | 0 {src => app}/user/admin.py | 0 {src => app}/user/apps.py | 0 {src => app}/user/migrations/__init__.py | 0 {src => app}/user/models.py | 0 {src => app}/user/serializers.py | 0 {src => app}/user/views.py | 0 54 files changed, 10 insertions(+), 10 deletions(-) rename {src/boj => app/app}/__init__.py (100%) rename {src/config => app/app}/asgi.py (82%) rename {src/config => app/app}/permissions.py (100%) rename {src/config => app/app}/settings.py (95%) rename {src/config => app/app}/settings_debug.py (66%) rename {src/config => app/app}/urls.py (100%) rename {src/config => app/app}/views.py (100%) rename {src/config => app/app}/wsgi.py (82%) rename {src/boj/migrations => app/boj}/__init__.py (100%) rename {src => app}/boj/admin.py (100%) rename {src => app}/boj/apps.py (100%) rename {src/config => app/boj/migrations}/__init__.py (100%) rename {src => app}/boj/models.py (100%) rename {src => app}/boj/serializers.py (100%) rename {src => app}/core/__init__.py (100%) rename {src => app}/core/admin.py (100%) rename {src => app}/core/apps.py (100%) rename {src => app}/core/migrations/__init__.py (100%) rename {src => app}/core/models.py (100%) rename {src => app}/core/serializers.py (100%) rename {src => app}/core/views.py (95%) rename {src => app}/crew/__init__.py (100%) rename {src => app}/crew/admin.py (100%) rename {src => app}/crew/apps.py (100%) rename {src => app}/crew/migrations/__init__.py (100%) rename {src => app}/crew/models/__init__.py (100%) rename {src => app}/crew/models/crew.py (100%) rename {src => app}/crew/models/crew_activity.py (100%) rename {src => app}/crew/models/crew_activity_problem.py (100%) rename {src => app}/crew/models/crew_activity_problem_submission.py (100%) rename {src => app}/crew/models/crew_activity_problem_submission_comment.py (100%) rename {src => app}/crew/models/crew_member.py (100%) rename {src => app}/crew/models/crew_member_request.py (100%) rename {src => app}/crew/serializers/__init__.py (100%) rename {src => app}/crew/serializers/crew.py (100%) rename {src => app}/crew/serializers/crew_activity.py (100%) rename {src => app}/crew/serializers/crew_member.py (100%) rename {src => app}/crew/serializers/mixins.py (100%) rename {src => app}/crew/views.py (100%) rename {src => app}/manage.py (89%) rename {src => app}/problem/__init__.py (100%) rename {src => app}/problem/admin.py (100%) rename {src => app}/problem/apps.py (100%) rename {src => app}/problem/migrations/__init__.py (100%) rename {src => app}/problem/models.py (100%) rename {src => app}/problem/serializers.py (100%) rename {src => app}/problem/views.py (98%) rename {src => app}/user/__init__.py (100%) rename {src => app}/user/admin.py (100%) rename {src => app}/user/apps.py (100%) rename {src => app}/user/migrations/__init__.py (100%) rename {src => app}/user/models.py (100%) rename {src => app}/user/serializers.py (100%) rename {src => app}/user/views.py (100%) diff --git a/src/boj/__init__.py b/app/app/__init__.py similarity index 100% rename from src/boj/__init__.py rename to app/app/__init__.py diff --git a/src/config/asgi.py b/app/app/asgi.py similarity index 82% rename from src/config/asgi.py rename to app/app/asgi.py index 87078af..120962e 100644 --- a/src/config/asgi.py +++ b/app/app/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_asgi_application() diff --git a/src/config/permissions.py b/app/app/permissions.py similarity index 100% rename from src/config/permissions.py rename to app/app/permissions.py diff --git a/src/config/settings.py b/app/app/settings.py similarity index 95% rename from src/config/settings.py rename to app/app/settings.py index 9fae126..cbc13ec 100644 --- a/src/config/settings.py +++ b/app/app/settings.py @@ -1,5 +1,5 @@ """ -Django settings for config project. +Django settings for app project. Generated by 'django-admin startproject' using Django 4.2. @@ -59,7 +59,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "config.urls" +ROOT_URLCONF = "app.urls" TEMPLATES = [ { @@ -77,7 +77,7 @@ }, ] -WSGI_APPLICATION = "config.wsgi.application" +WSGI_APPLICATION = "app.wsgi.application" # Database @@ -143,7 +143,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -DEFAULT_EXCEPTION_REPORTER = "config.views.NACLExceptionReporter" +DEFAULT_EXCEPTION_REPORTER = "app.views.NACLExceptionReporter" REST_FRAMEWORK = { 'PAGE_SIZE': 10, diff --git a/src/config/settings_debug.py b/app/app/settings_debug.py similarity index 66% rename from src/config/settings_debug.py rename to app/app/settings_debug.py index cb3a077..4b3015a 100644 --- a/src/config/settings_debug.py +++ b/app/app/settings_debug.py @@ -1,4 +1,4 @@ -from config.settings import * +from app.settings import * ALLOWED_HOSTS = ALLOWED_HOSTS + [ diff --git a/src/config/urls.py b/app/app/urls.py similarity index 100% rename from src/config/urls.py rename to app/app/urls.py diff --git a/src/config/views.py b/app/app/views.py similarity index 100% rename from src/config/views.py rename to app/app/views.py diff --git a/src/config/wsgi.py b/app/app/wsgi.py similarity index 82% rename from src/config/wsgi.py rename to app/app/wsgi.py index a9afbb3..5f42291 100644 --- a/src/config/wsgi.py +++ b/app/app/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") application = get_wsgi_application() diff --git a/src/boj/migrations/__init__.py b/app/boj/__init__.py similarity index 100% rename from src/boj/migrations/__init__.py rename to app/boj/__init__.py diff --git a/src/boj/admin.py b/app/boj/admin.py similarity index 100% rename from src/boj/admin.py rename to app/boj/admin.py diff --git a/src/boj/apps.py b/app/boj/apps.py similarity index 100% rename from src/boj/apps.py rename to app/boj/apps.py diff --git a/src/config/__init__.py b/app/boj/migrations/__init__.py similarity index 100% rename from src/config/__init__.py rename to app/boj/migrations/__init__.py diff --git a/src/boj/models.py b/app/boj/models.py similarity index 100% rename from src/boj/models.py rename to app/boj/models.py diff --git a/src/boj/serializers.py b/app/boj/serializers.py similarity index 100% rename from src/boj/serializers.py rename to app/boj/serializers.py diff --git a/src/core/__init__.py b/app/core/__init__.py similarity index 100% rename from src/core/__init__.py rename to app/core/__init__.py diff --git a/src/core/admin.py b/app/core/admin.py similarity index 100% rename from src/core/admin.py rename to app/core/admin.py diff --git a/src/core/apps.py b/app/core/apps.py similarity index 100% rename from src/core/apps.py rename to app/core/apps.py diff --git a/src/core/migrations/__init__.py b/app/core/migrations/__init__.py similarity index 100% rename from src/core/migrations/__init__.py rename to app/core/migrations/__init__.py diff --git a/src/core/models.py b/app/core/models.py similarity index 100% rename from src/core/models.py rename to app/core/models.py diff --git a/src/core/serializers.py b/app/core/serializers.py similarity index 100% rename from src/core/serializers.py rename to app/core/serializers.py diff --git a/src/core/views.py b/app/core/views.py similarity index 95% rename from src/core/views.py rename to app/core/views.py index 663279f..803e6f3 100644 --- a/src/core/views.py +++ b/app/core/views.py @@ -2,7 +2,7 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import * -from config.permissions import ReadOnly +from app.permissions import ReadOnly from .models import * from .serializers import * diff --git a/src/crew/__init__.py b/app/crew/__init__.py similarity index 100% rename from src/crew/__init__.py rename to app/crew/__init__.py diff --git a/src/crew/admin.py b/app/crew/admin.py similarity index 100% rename from src/crew/admin.py rename to app/crew/admin.py diff --git a/src/crew/apps.py b/app/crew/apps.py similarity index 100% rename from src/crew/apps.py rename to app/crew/apps.py diff --git a/src/crew/migrations/__init__.py b/app/crew/migrations/__init__.py similarity index 100% rename from src/crew/migrations/__init__.py rename to app/crew/migrations/__init__.py diff --git a/src/crew/models/__init__.py b/app/crew/models/__init__.py similarity index 100% rename from src/crew/models/__init__.py rename to app/crew/models/__init__.py diff --git a/src/crew/models/crew.py b/app/crew/models/crew.py similarity index 100% rename from src/crew/models/crew.py rename to app/crew/models/crew.py diff --git a/src/crew/models/crew_activity.py b/app/crew/models/crew_activity.py similarity index 100% rename from src/crew/models/crew_activity.py rename to app/crew/models/crew_activity.py diff --git a/src/crew/models/crew_activity_problem.py b/app/crew/models/crew_activity_problem.py similarity index 100% rename from src/crew/models/crew_activity_problem.py rename to app/crew/models/crew_activity_problem.py diff --git a/src/crew/models/crew_activity_problem_submission.py b/app/crew/models/crew_activity_problem_submission.py similarity index 100% rename from src/crew/models/crew_activity_problem_submission.py rename to app/crew/models/crew_activity_problem_submission.py diff --git a/src/crew/models/crew_activity_problem_submission_comment.py b/app/crew/models/crew_activity_problem_submission_comment.py similarity index 100% rename from src/crew/models/crew_activity_problem_submission_comment.py rename to app/crew/models/crew_activity_problem_submission_comment.py diff --git a/src/crew/models/crew_member.py b/app/crew/models/crew_member.py similarity index 100% rename from src/crew/models/crew_member.py rename to app/crew/models/crew_member.py diff --git a/src/crew/models/crew_member_request.py b/app/crew/models/crew_member_request.py similarity index 100% rename from src/crew/models/crew_member_request.py rename to app/crew/models/crew_member_request.py diff --git a/src/crew/serializers/__init__.py b/app/crew/serializers/__init__.py similarity index 100% rename from src/crew/serializers/__init__.py rename to app/crew/serializers/__init__.py diff --git a/src/crew/serializers/crew.py b/app/crew/serializers/crew.py similarity index 100% rename from src/crew/serializers/crew.py rename to app/crew/serializers/crew.py diff --git a/src/crew/serializers/crew_activity.py b/app/crew/serializers/crew_activity.py similarity index 100% rename from src/crew/serializers/crew_activity.py rename to app/crew/serializers/crew_activity.py diff --git a/src/crew/serializers/crew_member.py b/app/crew/serializers/crew_member.py similarity index 100% rename from src/crew/serializers/crew_member.py rename to app/crew/serializers/crew_member.py diff --git a/src/crew/serializers/mixins.py b/app/crew/serializers/mixins.py similarity index 100% rename from src/crew/serializers/mixins.py rename to app/crew/serializers/mixins.py diff --git a/src/crew/views.py b/app/crew/views.py similarity index 100% rename from src/crew/views.py rename to app/crew/views.py diff --git a/src/manage.py b/app/manage.py similarity index 89% rename from src/manage.py rename to app/manage.py index d28672e..1a64b14 100755 --- a/src/manage.py +++ b/app/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: diff --git a/src/problem/__init__.py b/app/problem/__init__.py similarity index 100% rename from src/problem/__init__.py rename to app/problem/__init__.py diff --git a/src/problem/admin.py b/app/problem/admin.py similarity index 100% rename from src/problem/admin.py rename to app/problem/admin.py diff --git a/src/problem/apps.py b/app/problem/apps.py similarity index 100% rename from src/problem/apps.py rename to app/problem/apps.py diff --git a/src/problem/migrations/__init__.py b/app/problem/migrations/__init__.py similarity index 100% rename from src/problem/migrations/__init__.py rename to app/problem/migrations/__init__.py diff --git a/src/problem/models.py b/app/problem/models.py similarity index 100% rename from src/problem/models.py rename to app/problem/models.py diff --git a/src/problem/serializers.py b/app/problem/serializers.py similarity index 100% rename from src/problem/serializers.py rename to app/problem/serializers.py diff --git a/src/problem/views.py b/app/problem/views.py similarity index 98% rename from src/problem/views.py rename to app/problem/views.py index d8ceb15..a12eedb 100644 --- a/src/problem/views.py +++ b/app/problem/views.py @@ -2,7 +2,7 @@ from rest_framework.pagination import PageNumberPagination from rest_framework.permissions import * -from config.permissions import ReadOnly +from app.permissions import ReadOnly from .models import * from .serializers import * diff --git a/src/user/__init__.py b/app/user/__init__.py similarity index 100% rename from src/user/__init__.py rename to app/user/__init__.py diff --git a/src/user/admin.py b/app/user/admin.py similarity index 100% rename from src/user/admin.py rename to app/user/admin.py diff --git a/src/user/apps.py b/app/user/apps.py similarity index 100% rename from src/user/apps.py rename to app/user/apps.py diff --git a/src/user/migrations/__init__.py b/app/user/migrations/__init__.py similarity index 100% rename from src/user/migrations/__init__.py rename to app/user/migrations/__init__.py diff --git a/src/user/models.py b/app/user/models.py similarity index 100% rename from src/user/models.py rename to app/user/models.py diff --git a/src/user/serializers.py b/app/user/serializers.py similarity index 100% rename from src/user/serializers.py rename to app/user/serializers.py diff --git a/src/user/views.py b/app/user/views.py similarity index 100% rename from src/user/views.py rename to app/user/views.py From 061d73e8ae48508b400d800587d4ad7f91324606 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 03:19:58 +0900 Subject: [PATCH 185/552] =?UTF-8?q?refactor(app.settings):=20settings=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EA=B3=B5=ED=86=B5=EC=A0=81=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=B6=80?= =?UTF-8?q?=EB=B6=84=EC=9D=84=20base.py=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings/__init__.py | 1 + app/app/{settings.py => settings/base.py} | 11 +++-------- app/app/settings/debug.py | 11 +++++++++++ app/app/settings_debug.py | 7 ------- 4 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 app/app/settings/__init__.py rename app/app/{settings.py => settings/base.py} (95%) create mode 100644 app/app/settings/debug.py delete mode 100644 app/app/settings_debug.py diff --git a/app/app/settings/__init__.py b/app/app/settings/__init__.py new file mode 100644 index 0000000..bb7c9d8 --- /dev/null +++ b/app/app/settings/__init__.py @@ -0,0 +1 @@ +from app.settings.base import * diff --git a/app/app/settings.py b/app/app/settings/base.py similarity index 95% rename from app/app/settings.py rename to app/app/settings/base.py index cbc13ec..c029bea 100644 --- a/app/app/settings.py +++ b/app/app/settings/base.py @@ -1,5 +1,5 @@ """ -Django settings for app project. +Django settings for project. Generated by 'django-admin startproject' using Django 4.2. @@ -13,7 +13,7 @@ from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent +BASE_DIR = Path(__file__).resolve().parent.parent.parent # Quick-start development settings - unsuitable for production @@ -28,7 +28,6 @@ ALLOWED_HOSTS = [ 'tle-kr.com', 'timelimitexceeded.kr', - 'localhost', ] @@ -42,11 +41,6 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "boj", - "core", - "crew", - "problem", - "user", ] MIDDLEWARE = [ @@ -143,6 +137,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + DEFAULT_EXCEPTION_REPORTER = "app.views.NACLExceptionReporter" REST_FRAMEWORK = { diff --git a/app/app/settings/debug.py b/app/app/settings/debug.py new file mode 100644 index 0000000..c4bad96 --- /dev/null +++ b/app/app/settings/debug.py @@ -0,0 +1,11 @@ +from app.settings.base import * + + +DEBUG = True + +ALLOWED_HOSTS += [ + '*', +] + +INSTALLED_APPS += [ +] \ No newline at end of file diff --git a/app/app/settings_debug.py b/app/app/settings_debug.py deleted file mode 100644 index 4b3015a..0000000 --- a/app/app/settings_debug.py +++ /dev/null @@ -1,7 +0,0 @@ -from app.settings import * - - -ALLOWED_HOSTS = ALLOWED_HOSTS + [ - '*', -] -DEBUG = True From 856faf7cf8494af369f59315feebcbed874b21e9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 03:35:13 +0900 Subject: [PATCH 186/552] =?UTF-8?q?refactor(app.urls):=20clean=20urls.py?= =?UTF-8?q?=20(=EA=B0=81=20=EB=AA=A8=EB=93=88=EB=B3=84=EB=A1=9C=20urlpatte?= =?UTF-8?q?rns=EB=A5=BC=20=EB=B6=84=EB=A6=AC=ED=95=98=EA=B8=B0=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EC=9E=91=EC=97=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/urls.py | 73 ++----------------------------------------------- 1 file changed, 3 insertions(+), 70 deletions(-) diff --git a/app/app/urls.py b/app/app/urls.py index 8ada45c..2693edd 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -17,79 +17,12 @@ from django.conf import settings from django.conf.urls.static import static from django.contrib import admin -from django.urls import ( - include, - path, -) - -from core.views import * -from crew.views import * -from problem.views import * -from user.views import * - - -VIEW_PLACE_HOLDER = lambda request: NotImplemented +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), - path("api/", include([ - path("v1/", include([ - path("account/", include([ - path("signup", UserAPIView.SignUp.as_view()), # 회원가입 기능 구현 - path("signin", UserAPIView.SignIn.as_view()), # 로그인 기능 구현 - path("signout", UserAPIView.SignOut.as_view()), # 로그아웃 기능 구현 - ])), - path("user/", include([ - path("", UserAPIView.List.as_view()), # 사용자 목록 조회 기능 구현 (관리자용) - path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 사용자 상세 조회+수정 기능 구현 (관리자용) - ])), - ])), - path("problem/", include([ - path("", ProblemAPIView.ListCreate.as_view()), # 전체 문제 목록 조회(관리자용) + 생성 기능 - path("my", ProblemAPIView.MyList.as_view()), # 내가 만든 문제 목록 조회 기능 구현 - path("/", include([ - path("", ProblemAPIView.RetrieveUpdateDestroy.as_view()), # 문제 상세 조회 기능 구현 - path("analysis", ProblemAnalysisAPIView.Retrieve.as_view()), # 문제 분석 조회 기능 구현 - ])), - ])), - path("crew/", include([ - path("", CrewAPIView.ListCreate.as_view()), # 전체 크루 목록 조회(관리자용) + 생성 - path("my", CrewAPIView.MyList.as_view()), # 내가 속한 크루 목록 조회 기능 구현 - path("recruiting", CrewAPIView.RecruitingList.as_view()), # 크루원을 모집 중인 크루 목록 조회 기능 구현 - path("/", include([ - path("", CrewAPIView.RetrieveUpdateDestroy.as_view()), # 크루 상세 조회(공지사항, 크루원 목록, 해결한 문제들의 태그 분포, 이번 주 현황, 모집 시작/종료/옵션, ...)+수정 기능 구현 - path("activities", VIEW_PLACE_HOLDER), # TODO: 크루의 활동 회차 목록 조회 기능 구현 - path("problems", VIEW_PLACE_HOLDER), # TODO: 크루에 속한 문제 목록 조회 기능 구현 - path("pending", VIEW_PLACE_HOLDER), # TODO: 크루 가입 대기자 목록 조회 기능 구현 - ])), - ])), - path("activity/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 회차 목록 조회 + 추가(방장만) 기능 구현 - path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 회차 상세 조회 기능 구현 - ])), - ])), - path("submission/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 크루 활동 문제에 대한 풀이 제출 + 목록 조회 기능 구현 - path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 제출 상세 조회+수정 기능 구현 - ])), - ])), - path("comment/",include([ - path("", VIEW_PLACE_HOLDER), # TODO: 코멘트 목록 조회(관리자용) + 생성 기능 구현 - path("/", include([ - path("", VIEW_PLACE_HOLDER), # TODO: 코멘트 상세 조회+수정+삭제 기능 구현 - ])), - ])), - path("tag/", include([ - path("", TagAPIView.ListCreate.as_view()), # 전체 태그 목록 조회(관리자용) + 생성 기능 - ])), - path("language/", include([ - path("", LanguageAPIView.ListCreate.as_view()), # 전체 언어 목록 조회(관리자용) + 생성 기능 - ])), - ])), + path("api/v1/", include([ ])), ] @@ -97,5 +30,5 @@ urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) # Media files +# TODO: 미디어 파일은 S3 같은 외부 의존성으로 변경하기 urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) -# TODO: Remove above line in production (미디어 파일은 S3 같은 외부 의존성으로 변경하기) From 3fe5fa5579c5db7f46ce6e3aea5a7cdca9de7dfe Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 07:49:25 +0900 Subject: [PATCH 187/552] =?UTF-8?q?refactor(problem):=20`Tag`,=20`Language?= =?UTF-8?q?`,=20`Difficulty`=20=EB=AA=A8=EB=8D=B8=EC=9D=84=20problem?= =?UTF-8?q?=EB=A1=9C=20=EC=98=AE=EA=B9=80=20=EB=93=B1...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rest_framework의 `ViewSet` 클래스를 이용하여 모듈 기능 리팩토링 --- app/app/settings/base.py | 1 + app/app/urls.py | 4 + app/problem/admin.py | 12 ++- app/problem/models/__init__.py | 5 ++ app/problem/models/difficulty.py | 7 ++ app/problem/models/language.py | 30 +++++++ app/problem/{models.py => models/problem.py} | 44 ---------- app/problem/models/problem_analysis.py | 48 ++++++++++ app/problem/models/tag.py | 45 ++++++++++ app/problem/permissions.py | 38 ++++++++ app/problem/serializers/__init__.py | 4 + app/problem/serializers/language.py | 9 ++ .../problem.py} | 26 +----- app/problem/serializers/problem_analysis.py | 24 +++++ app/problem/serializers/tag.py | 9 ++ app/problem/urls.py | 43 +++++++++ app/problem/views.py | 88 ++++++++----------- 17 files changed, 316 insertions(+), 121 deletions(-) create mode 100644 app/problem/models/__init__.py create mode 100644 app/problem/models/difficulty.py create mode 100644 app/problem/models/language.py rename app/problem/{models.py => models/problem.py} (55%) create mode 100644 app/problem/models/problem_analysis.py create mode 100644 app/problem/models/tag.py create mode 100644 app/problem/permissions.py create mode 100644 app/problem/serializers/__init__.py create mode 100644 app/problem/serializers/language.py rename app/problem/{serializers.py => serializers/problem.py} (53%) create mode 100644 app/problem/serializers/problem_analysis.py create mode 100644 app/problem/serializers/tag.py create mode 100644 app/problem/urls.py diff --git a/app/app/settings/base.py b/app/app/settings/base.py index c029bea..d046d88 100644 --- a/app/app/settings/base.py +++ b/app/app/settings/base.py @@ -41,6 +41,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "problem", ] MIDDLEWARE = [ diff --git a/app/app/urls.py b/app/app/urls.py index 2693edd..430de74 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -20,9 +20,13 @@ from django.urls import include, path +import problem.urls + + urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ + path("problem/", include(problem.urls.urlpatterns)), ])), ] diff --git a/app/problem/admin.py b/app/problem/admin.py index 433cb32..a6b2322 100644 --- a/app/problem/admin.py +++ b/app/problem/admin.py @@ -1,6 +1,11 @@ from django.contrib import admin -from problem.models import * +from .models import * + + +@admin.register(Language) +class LanguageAdmin(admin.ModelAdmin): + pass @admin.register(Problem) @@ -11,3 +16,8 @@ class ProblemAdmin(admin.ModelAdmin): @admin.register(ProblemAnalysis) class ProblemAnalysisAdmin(admin.ModelAdmin): pass + + +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): + pass diff --git a/app/problem/models/__init__.py b/app/problem/models/__init__.py new file mode 100644 index 0000000..dd9e460 --- /dev/null +++ b/app/problem/models/__init__.py @@ -0,0 +1,5 @@ +from .problem import Problem +from .problem_analysis import ProblemAnalysis +from .difficulty import Difficulty +from .language import Language +from .tag import Tag diff --git a/app/problem/models/difficulty.py b/app/problem/models/difficulty.py new file mode 100644 index 0000000..e566119 --- /dev/null +++ b/app/problem/models/difficulty.py @@ -0,0 +1,7 @@ +from django.db import models + + +class Difficulty(models.IntegerChoices): + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' diff --git a/app/problem/models/language.py b/app/problem/models/language.py new file mode 100644 index 0000000..4458f53 --- /dev/null +++ b/app/problem/models/language.py @@ -0,0 +1,30 @@ +from django.db import models + + +class Language(models.Model): + key = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 키를 입력해주세요. (최대 20자)' + ), + ) + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 이름을 입력해주세요. (최대 20자)' + ), + ) + extension = models.CharField( + max_length=20, + help_text=( + '언어 확장자를 입력해주세요. (최대 20자)' + ), + ) + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/app/problem/models.py b/app/problem/models/problem.py similarity index 55% rename from app/problem/models.py rename to app/problem/models/problem.py index 159ca9e..de3f34d 100644 --- a/app/problem/models.py +++ b/app/problem/models/problem.py @@ -1,6 +1,5 @@ from django.db import models -from core.models import * from user.models import User @@ -64,46 +63,3 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' - - -class ProblemAnalysis(models.Model): - problem = models.OneToOneField( - Problem, - on_delete=models.CASCADE, - related_name='analysis', - help_text=( - '문제를 입력해주세요.' - ), - ) - difficulty = models.IntegerField( - help_text=( - '문제 난이도를 입력해주세요.' - ), - choices=Difficulty.choices, - ) - tags = models.ManyToManyField( - Tag, - related_name='problems', - help_text=( - '문제의 DSA 태그를 입력해주세요.' - ), - ) - time_complexity = models.CharField( - max_length=100, - help_text=( - '문제 시간 복잡도를 입력해주세요. ', - '예) O(1), O(n), O(n^2), O(V \log E) 등', - ), - validators=[ - # TODO: 시간 복잡도 검증 로직 추가 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 - - def __repr__(self) -> str: - tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) - return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' diff --git a/app/problem/models/problem_analysis.py b/app/problem/models/problem_analysis.py new file mode 100644 index 0000000..da63613 --- /dev/null +++ b/app/problem/models/problem_analysis.py @@ -0,0 +1,48 @@ +from django.db import models + +from .difficulty import Difficulty +from .problem import Problem +from .tag import Tag + + +class ProblemAnalysis(models.Model): + problem = models.OneToOneField( + Problem, + on_delete=models.CASCADE, + related_name='analysis', + help_text=( + '문제를 입력해주세요.' + ), + ) + difficulty = models.IntegerField( + help_text=( + '문제 난이도를 입력해주세요.' + ), + choices=Difficulty.choices, + ) + tags = models.ManyToManyField( + Tag, + related_name='problems', + help_text=( + '문제의 DSA 태그를 입력해주세요.' + ), + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 + + def __repr__(self) -> str: + tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) + return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' diff --git a/app/problem/models/tag.py b/app/problem/models/tag.py new file mode 100644 index 0000000..4e29c4c --- /dev/null +++ b/app/problem/models/tag.py @@ -0,0 +1,45 @@ +from django.db import models + + +class Tag(models.Model): + """Data Structure & Algorithm""" + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + related_name='children', + help_text=( + '부모 알고리즘 태그를 입력해주세요.' + ), + null=True, + blank=True, + ) + key = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 키를 입력해주세요. (최대 20자)' + ), + ) + name_ko = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' + ), + ) + name_en = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' + ), + ) + + class Meta: + ordering = ['key'] + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name_ko})' diff --git a/app/problem/permissions.py b/app/problem/permissions.py new file mode 100644 index 0000000..6516a1c --- /dev/null +++ b/app/problem/permissions.py @@ -0,0 +1,38 @@ +from rest_framework.permissions import ( + BasePermission, + IsAdminUser, + IsAuthenticated, + SAFE_METHODS, +) + +from .models import Problem + + +__all__ = ( + 'IsReadOnly', + 'IsCreateOnly', + 'IsAdminUser', + 'IsAuthenticated', + 'IsProblemCreator', +) + + +class IsReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS, + ) + + +class IsCreateOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method == 'POST', + ) + + +class IsProblemCreator(IsAuthenticated): + def has_object_permission(self, request, view, obj: Problem) -> bool: + return super().has_object_permission(request, view, obj) and bool( + obj.user == request.user + ) diff --git a/app/problem/serializers/__init__.py b/app/problem/serializers/__init__.py new file mode 100644 index 0000000..6dea049 --- /dev/null +++ b/app/problem/serializers/__init__.py @@ -0,0 +1,4 @@ +from .problem import ProblemSerializer +from .problem_analysis import ProblemAnalysisSerializer +from .language import LanguageSerializer +from .tag import TagSerializer diff --git a/app/problem/serializers/language.py b/app/problem/serializers/language.py new file mode 100644 index 0000000..9d8c3d9 --- /dev/null +++ b/app/problem/serializers/language.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer + +from ..models import Language + + +class LanguageSerializer(ModelSerializer): + class Meta: + model = Language + fields = '__all__' diff --git a/app/problem/serializers.py b/app/problem/serializers/problem.py similarity index 53% rename from app/problem/serializers.py rename to app/problem/serializers/problem.py index 71f8898..89e1545 100644 --- a/app/problem/serializers.py +++ b/app/problem/serializers/problem.py @@ -1,29 +1,9 @@ -from rest_framework.serializers import * +from rest_framework.serializers import ModelSerializer -from core.serializers import TagSerializer from user.serializers import UserSerializer -from .models import * - - -class ProblemAnalysisSerializer(ModelSerializer): - tags = TagSerializer(many=True) - - class Meta: - model = ProblemAnalysis - fields = [ - 'id', - 'problem', - 'difficulty', - 'tags', - 'time_complexity', - 'created_at', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'problem': {'read_only': True}, - 'created_at': {'read_only': True}, - } +from ..models import Problem +from ..serializers.problem_analysis import ProblemAnalysisSerializer class ProblemSerializer(ModelSerializer): diff --git a/app/problem/serializers/problem_analysis.py b/app/problem/serializers/problem_analysis.py new file mode 100644 index 0000000..cc14cb1 --- /dev/null +++ b/app/problem/serializers/problem_analysis.py @@ -0,0 +1,24 @@ +from rest_framework.serializers import ModelSerializer + +from ..models import ProblemAnalysis +from ..serializers.tag import TagSerializer + + +class ProblemAnalysisSerializer(ModelSerializer): + tags = TagSerializer(many=True) + + class Meta: + model = ProblemAnalysis + fields = [ + 'id', + 'problem', + 'difficulty', + 'tags', + 'time_complexity', + 'created_at', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'problem': {'read_only': True}, + 'created_at': {'read_only': True}, + } diff --git a/app/problem/serializers/tag.py b/app/problem/serializers/tag.py new file mode 100644 index 0000000..ebf05ec --- /dev/null +++ b/app/problem/serializers/tag.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer + +from ..models import Tag + + +class TagSerializer(ModelSerializer): + class Meta: + model = Tag + fields = '__all__' diff --git a/app/problem/urls.py b/app/problem/urls.py new file mode 100644 index 0000000..99ff8a4 --- /dev/null +++ b/app/problem/urls.py @@ -0,0 +1,43 @@ +from django.urls import include, path + +from .views import * + + +urlpatterns = [ + path("", ProblemViewSet.as_view({ + "get": "list", + "post": "create", + })), + path("my/", ProblemViewSet.as_view({ + "get": "my_list", + })), + path("/", include([ + path("", ProblemViewSet.as_view({ + "get": "retrieve", + "put": "update", + "delete": "destroy", + })), + ])), + path("language/", include([ + path("", LanguageViewSet.as_view({ + "get": "list", + "post": "create", + })), + path("/", LanguageViewSet.as_view({ + "get": "retrieve", + "put": "update", + "delete": "destroy", + })), + ])), + path("tag/", include([ + path("", TagViewSet.as_view({ + "get": "list", + "post": "create", + })), + path("/", TagViewSet.as_view({ + "get": "retrieve", + "put": "update", + "delete": "destroy", + })), + ])), +] diff --git a/app/problem/views.py b/app/problem/views.py index a12eedb..4f4ac6a 100644 --- a/app/problem/views.py +++ b/app/problem/views.py @@ -1,67 +1,49 @@ -from rest_framework.generics import * -from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import * - -from app.permissions import ReadOnly +from rest_framework import viewsets +from rest_framework.request import Request +from rest_framework.response import Response from .models import * from .serializers import * +from .permissions import * -class _PageNumberPagination(PageNumberPagination): - page_size = 20 - page_size_query_param = 'page_size' - max_page_size = 100 - +__all__ = ( + 'TagViewSet', + 'LanguageViewSet', + 'ProblemViewSet', +) -class IsProblemCreator(BasePermission): - def has_object_permission(self, request, view, obj: Problem) -> bool: - return bool( - request.user and - request.user.is_authenticated and - obj.user == request.user - ) +class TagViewSet(viewsets.ModelViewSet): + """문제 태그 목록 조회 + 생성 기능""" + queryset = Tag.objects.all() + serializer_class = TagSerializer + permission_classes = [IsAdminUser | IsReadOnly] -class ProblemAPIView: - class ListCreate(ListCreateAPIView): - """전체 문제 목록 조회 + 생성 기능 - - 관리자는 전체 문제 목록을 조회할 수 있습니다. - - 관리자가 아닌 일반 사용자는 자신이 만든 문제만 조회할 수 있습니다. - """ - serializer_class = ProblemSerializer - pagination_class = _PageNumberPagination - permission_classes = [IsAuthenticated] +class LanguageViewSet(viewsets.ModelViewSet): + """프로그래밍 언어 목록 조회 + 생성 기능""" + queryset = Language.objects.all() + serializer_class = LanguageSerializer + permission_classes = [IsAdminUser | IsReadOnly] - def get_queryset(self): - if self.request.user.is_staff: - return Problem.objects.all() - # TODO: 공개된 문제도 보여주도록 기능 추가 - return Problem.objects.filter(user=self.request.user) +class ProblemViewSet(viewsets.ModelViewSet): + """문제 목록 조회 + 생성 기능 - class MyList(ListAPIView): - """내가 만든 문제 목록 조회""" - serializer_class = ProblemSerializer - pagination_class = _PageNumberPagination - permission_classes = [IsAuthenticated] - - def get_queryset(self): - return Problem.objects.filter(user=self.request.user) + - 관리자는 전체 문제 목록을 조회할 수 있습니다. + - 관리자가 아닌 일반 사용자는 자신이 만든 문제만 조회할 수 있습니다. + """ + serializer_class = ProblemSerializer + permission_classes = [IsAdminUser | IsProblemCreator | IsReadOnly | (IsAuthenticated & IsCreateOnly)] + def get_queryset(self): + if self.request.user.is_staff: + return Problem.objects.all() + return Problem.objects.filter(user=self.request.user) - class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): - queryset = Problem.objects.all() - serializer_class = ProblemSerializer - permission_classes = [IsAdminUser | IsProblemCreator] - lookup_url_kwarg = 'id' - - -class ProblemAnalysisAPIView: - class Retrieve(RetrieveAPIView): - queryset = ProblemAnalysis.objects.all() - permission_classes = [IsAdminUser | (IsProblemCreator & ReadOnly)] - serializer_class = ProblemAnalysisSerializer - lookup_url_kwarg = 'id' - lookup_field = 'problem__id' + def my_list(self, request: Request): + """내가 만든 문제 목록 조회""" + queryset = Problem.objects.filter(user=request.user) + serializer = self.get_serializer(queryset, many=True) + return Response(serializer.data) From 03b503eadf556dd32fe5011ddac73b345a2a3f8c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 09:07:57 +0900 Subject: [PATCH 188/552] refactor(boj): remove app `boj` --- app/boj/__init__.py | 0 app/boj/admin.py | 13 ----- app/boj/apps.py | 6 -- app/boj/migrations/__init__.py | 0 app/boj/models.py | 104 --------------------------------- app/boj/serializers.py | 29 --------- 6 files changed, 152 deletions(-) delete mode 100644 app/boj/__init__.py delete mode 100644 app/boj/admin.py delete mode 100644 app/boj/apps.py delete mode 100644 app/boj/migrations/__init__.py delete mode 100644 app/boj/models.py delete mode 100644 app/boj/serializers.py diff --git a/app/boj/__init__.py b/app/boj/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/boj/admin.py b/app/boj/admin.py deleted file mode 100644 index dbc3215..0000000 --- a/app/boj/admin.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.contrib import admin - -from boj.models import * - - -@admin.register(BOJUser) -class BOJUserAdmin(admin.ModelAdmin): - pass - - -@admin.register(BOJTag) -class BOJTagAdmin(admin.ModelAdmin): - pass diff --git a/app/boj/apps.py b/app/boj/apps.py deleted file mode 100644 index 025268a..0000000 --- a/app/boj/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class BojConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "boj" diff --git a/app/boj/migrations/__init__.py b/app/boj/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/boj/models.py b/app/boj/models.py deleted file mode 100644 index 5589bdd..0000000 --- a/app/boj/models.py +++ /dev/null @@ -1,104 +0,0 @@ -from django.db import models - -from core.models import Tag -from user.models import User - - -class BOJLevel(models.IntegerChoices): - U = 0, 'Unrated' - B5 = 1, '브론즈 5' - B4 = 2, '브론즈 4' - B3 = 3, '브론즈 3' - B2 = 4, '브론즈 2' - B1 = 5, '브론즈 1' - S5 = 6, '실버 5' - S4 = 7, '실버 4' - S3 = 8, '실버 3' - S2 = 9, '실버 2' - S1 = 10, '실버 1' - G5 = 11, '골드 5' - G4 = 12, '골드 4' - G3 = 13, '골드 3' - G2 = 14, '골드 2' - G1 = 15, '골드 1' - P5 = 16, '플래티넘 5' - P4 = 17, '플래티넘 4' - P3 = 18, '플래티넘 3' - P2 = 19, '플래티넘 2' - P1 = 20, '플래티넘 1' - D5 = 21, '다이아몬드 5' - D4 = 22, '다이아몬드 4' - D3 = 23, '다이아몬드 3' - D2 = 24, '다이아몬드 2' - D1 = 25, '다이아몬드 1' - R5 = 26, '루비 5' - R4 = 27, '루비 4' - R3 = 28, '루비 3' - R2 = 29, '루비 2' - R1 = 30, '루비 1' - - -class BOJUser(models.Model): - user = models.OneToOneField( - User, - on_delete=models.CASCADE, - related_name='boj_user', - help_text=( - '이 사용자와 연결된 사용자를 입력해주세요.' - ), - ) - boj_id = models.CharField( - max_length=100, # TODO: 추후 최대 아이디 길이 조사 필요 - help_text=( - '백준 아이디를 입력해주세요.' - ), - unique=True, - ) - is_verified = models.BooleanField( - default=False, - help_text=( - '이 사용자가 백준 사용자임을 확인했는지 여부를 입력해주세요.' - ), - ) - level = models.IntegerField( - help_text=( - '백준 레벨을 입력해주세요.' - ), - choices=BOJLevel.choices, - default=BOJLevel.U, - ) - updated_at = models.DateTimeField( - help_text=( - '이 사용자의 정보가 최근에 업데이트된 시간을 입력해주세요.' - ), - auto_now=True, - ) - - def __repr__(self) -> str: - return f'[@{self.boj_id} | *{BOJLevel(self.level).label}]' - - def __str__(self) -> str: - verified = 'verified' if self.is_verified else 'not-verified' - return f'{self.pk} : {self.user.__repr__()} ← {self.__repr__()} ({verified})' - - -class BOJTag(models.Model): - tag = models.OneToOneField( - Tag, - on_delete=models.CASCADE, - related_name='boj_tag', - help_text=( - '이 태그와 연결된 알고리즘 태그를 입력해주세요.' - ), - ) - boj_id = models.IntegerField( - unique=True, - help_text=( - '백준 태그 ID를 입력해주세요.' - ), - null=True, - default=None, - ) - - def __str__(self) -> str: - return f'{self.boj_id} : {self.tag.__repr__()}' diff --git a/app/boj/serializers.py b/app/boj/serializers.py deleted file mode 100644 index 84162db..0000000 --- a/app/boj/serializers.py +++ /dev/null @@ -1,29 +0,0 @@ -from rest_framework.serializers import * - -from boj.models import * - - -class BOJUserSerializer(ModelSerializer): - class Meta: - model = BOJUser - fields = [ - 'boj_id', - 'level', - 'is_verified', - ] - extra_kwargs = { - 'boj_id': {'read_only': True}, - 'level': {'read_only': True}, - 'is_verified': {'read_only': True}, - } - - -class BOJTagSerializer(ModelSerializer): - class Meta: - model = BOJTag - fields = [ - 'boj_id', - ] - extra_kwargs = { - 'boj_id': {'read_only': True}, - } From 4fb7a2fad9c81641a32479482781c119a37dad9b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 09:08:05 +0900 Subject: [PATCH 189/552] refactor(core): remove app `core` --- app/core/__init__.py | 0 app/core/admin.py | 13 ------ app/core/apps.py | 6 --- app/core/migrations/__init__.py | 0 app/core/models.py | 80 --------------------------------- app/core/serializers.py | 15 ------- app/core/views.py | 29 ------------ 7 files changed, 143 deletions(-) delete mode 100644 app/core/__init__.py delete mode 100644 app/core/admin.py delete mode 100644 app/core/apps.py delete mode 100644 app/core/migrations/__init__.py delete mode 100644 app/core/models.py delete mode 100644 app/core/serializers.py delete mode 100644 app/core/views.py diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/core/admin.py b/app/core/admin.py deleted file mode 100644 index d3b468e..0000000 --- a/app/core/admin.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.contrib import admin - -from core.models import * - - -@admin.register(Tag) -class TagAdmin(admin.ModelAdmin): - pass - - -@admin.register(Language) -class LanguageAdmin(admin.ModelAdmin): - pass diff --git a/app/core/apps.py b/app/core/apps.py deleted file mode 100644 index c0ce093..0000000 --- a/app/core/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CoreConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "core" diff --git a/app/core/migrations/__init__.py b/app/core/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/core/models.py b/app/core/models.py deleted file mode 100644 index e58a7a9..0000000 --- a/app/core/models.py +++ /dev/null @@ -1,80 +0,0 @@ -from django.db import models - - -class Difficulty(models.IntegerChoices): - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' - - -class Tag(models.Model): - """Data Structure & Algorithm""" - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - related_name='children', - help_text=( - '부모 알고리즘 태그를 입력해주세요.' - ), - null=True, - blank=True, - ) - key = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 키를 입력해주세요. (최대 20자)' - ), - ) - name_ko = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' - ), - ) - name_en = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' - ), - ) - - class Meta: - ordering = ['key'] - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name_ko})' - - -class Language(models.Model): - key = models.CharField( - max_length=20, - unique=True, - help_text=( - '언어 키를 입력해주세요. (최대 20자)' - ), - ) - name = models.CharField( - max_length=20, - unique=True, - help_text=( - '언어 이름을 입력해주세요. (최대 20자)' - ), - ) - extension = models.CharField( - max_length=20, - help_text=( - '언어 확장자를 입력해주세요. (최대 20자)' - ), - ) - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/app/core/serializers.py b/app/core/serializers.py deleted file mode 100644 index 6e21bdc..0000000 --- a/app/core/serializers.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework.serializers import * - -from .models import * - - -class TagSerializer(ModelSerializer): - class Meta: - model = Tag - fields = '__all__' - - -class LanguageSerializer(ModelSerializer): - class Meta: - model = Language - fields = '__all__' diff --git a/app/core/views.py b/app/core/views.py deleted file mode 100644 index 803e6f3..0000000 --- a/app/core/views.py +++ /dev/null @@ -1,29 +0,0 @@ -from rest_framework.generics import * -from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import * - -from app.permissions import ReadOnly - -from .models import * -from .serializers import * - - -class _PageNumberPagination(PageNumberPagination): - page_size = 250 - page_size_query_param = 'page_size' - - -class TagAPIView: - class ListCreate(ListCreateAPIView): - queryset = Tag.objects.all() - serializer_class = TagSerializer - permission_classes = [IsAdminUser | (IsAuthenticated & ReadOnly)] - pagination_class = _PageNumberPagination - - -class LanguageAPIView: - class ListCreate(ListCreateAPIView): - queryset = Language.objects.all() - serializer_class = LanguageSerializer - permission_classes = [IsAdminUser | (IsAuthenticated & ReadOnly)] - pagination_class = _PageNumberPagination From c9209c858f001cf5137e3d497b0ad585ff16d9c1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 3 Jul 2024 14:20:52 +0900 Subject: [PATCH 190/552] refactor(account): rename app `user` -> `account` --- app/{user => account}/__init__.py | 0 app/{user => account}/admin.py | 4 ++-- app/{user => account}/apps.py | 4 ++-- app/{user => account}/migrations/__init__.py | 0 app/{user => account}/models.py | 0 app/{user => account}/serializers.py | 0 app/{user => account}/views.py | 0 app/problem/models/problem.py | 2 +- app/problem/serializers/problem.py | 2 +- 9 files changed, 6 insertions(+), 6 deletions(-) rename app/{user => account}/__init__.py (100%) rename app/{user => account}/admin.py (50%) rename app/{user => account}/apps.py (63%) rename app/{user => account}/migrations/__init__.py (100%) rename app/{user => account}/models.py (100%) rename app/{user => account}/serializers.py (100%) rename app/{user => account}/views.py (100%) diff --git a/app/user/__init__.py b/app/account/__init__.py similarity index 100% rename from app/user/__init__.py rename to app/account/__init__.py diff --git a/app/user/admin.py b/app/account/admin.py similarity index 50% rename from app/user/admin.py rename to app/account/admin.py index 0decace..addcd8a 100644 --- a/app/user/admin.py +++ b/app/account/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from user.models import * +from account.models import * @admin.register(User) -class UserAdmin(admin.ModelAdmin): +class AccountAdmin(admin.ModelAdmin): pass diff --git a/app/user/apps.py b/app/account/apps.py similarity index 63% rename from app/user/apps.py rename to app/account/apps.py index 578292c..2c684a9 100644 --- a/app/user/apps.py +++ b/app/account/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class UserConfig(AppConfig): +class AccountConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "user" + name = "account" diff --git a/app/user/migrations/__init__.py b/app/account/migrations/__init__.py similarity index 100% rename from app/user/migrations/__init__.py rename to app/account/migrations/__init__.py diff --git a/app/user/models.py b/app/account/models.py similarity index 100% rename from app/user/models.py rename to app/account/models.py diff --git a/app/user/serializers.py b/app/account/serializers.py similarity index 100% rename from app/user/serializers.py rename to app/account/serializers.py diff --git a/app/user/views.py b/app/account/views.py similarity index 100% rename from app/user/views.py rename to app/account/views.py diff --git a/app/problem/models/problem.py b/app/problem/models/problem.py index de3f34d..f4c9c81 100644 --- a/app/problem/models/problem.py +++ b/app/problem/models/problem.py @@ -1,6 +1,6 @@ from django.db import models -from user.models import User +from account.models import User class Problem(models.Model): diff --git a/app/problem/serializers/problem.py b/app/problem/serializers/problem.py index 89e1545..af06cba 100644 --- a/app/problem/serializers/problem.py +++ b/app/problem/serializers/problem.py @@ -1,6 +1,6 @@ from rest_framework.serializers import ModelSerializer -from user.serializers import UserSerializer +from account.serializers import UserSerializer from ..models import Problem from ..serializers.problem_analysis import ProblemAnalysisSerializer From 2012a68d53817faaa96b189364ce1e89814f670c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 3 Jul 2024 16:00:37 +0900 Subject: [PATCH 191/552] =?UTF-8?q?refactor(account):=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=EB=90=9C=20boj=20=EB=AA=A8=EB=93=88=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20`ViewSet`=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/account/models.py | 7 +++ app/account/serializers.py | 9 +--- app/account/urls.py | 16 ++++++ app/account/views.py | 104 +++++++++++++++++-------------------- app/app/settings/base.py | 1 + app/app/urls.py | 2 + 6 files changed, 75 insertions(+), 64 deletions(-) create mode 100644 app/account/urls.py diff --git a/app/account/models.py b/app/account/models.py index d343098..56299b8 100644 --- a/app/account/models.py +++ b/app/account/models.py @@ -9,6 +9,7 @@ class User(DjangoUser): 'password', ] image = models.ImageField( + help_text='프로필 이미지', upload_to='user_images/', null=True, blank=True, @@ -17,6 +18,12 @@ class User(DjangoUser): # TODO: 이미지 확장자 제한 ] ) + boj_id = models.CharField( + help_text='백준 아이디', + max_length=40, + null=True, + blank=True, + ) def __repr__(self) -> str: return f'[@{self.username}]' diff --git a/app/account/serializers.py b/app/account/serializers.py index bf9309d..c52fb3c 100644 --- a/app/account/serializers.py +++ b/app/account/serializers.py @@ -1,8 +1,6 @@ from rest_framework.serializers import * -from boj.serializers import BOJUserSerializer - -from .models import * +from .models import User class UserSerializer(ModelSerializer): @@ -14,8 +12,6 @@ class Meta: 'username', ] - # TODO: BOJUser Serializer 연결 - class UserSignInSerializer(ModelSerializer): email = EmailField() @@ -40,14 +36,12 @@ class Meta: class UserSignUpSerializer(ModelSerializer): boj_id = CharField(max_length=40, required=False) - boj_user = BOJUserSerializer(read_only=True) class Meta: model = User fields = [ 'id', 'boj_id', - 'boj_user', 'image', 'username', 'email', @@ -56,6 +50,5 @@ class Meta: extra_kwargs = { 'id': {'read_only': True}, 'boj_id': {'write_only': True}, - 'boj_user': {'read_only': True}, 'password': {'write_only': True}, } diff --git a/app/account/urls.py b/app/account/urls.py new file mode 100644 index 0000000..5c976f7 --- /dev/null +++ b/app/account/urls.py @@ -0,0 +1,16 @@ +from django.urls import path + +from .views import UserViewSet + + +urlpatterns = [ + path("signin", UserViewSet.as_view({ + "post": "sign_in", + })), + path("signup", UserViewSet.as_view({ + "post": "sign_up", + })), + path("signout", UserViewSet.as_view({ + "get": "sign_out", + })), +] diff --git a/app/account/views.py b/app/account/views.py index 3d80644..f6b01e8 100644 --- a/app/account/views.py +++ b/app/account/views.py @@ -1,80 +1,72 @@ from http import HTTPStatus from django.contrib.auth import ( - authenticate, + authenticate as django_authenticate, login, logout, ) -from django.db.transaction import atomic +from rest_framework import viewsets from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.generics import * -from rest_framework.permissions import * - -from boj.models import BOJUser - -from .models import * -from .serializers import * +from rest_framework.permissions import AllowAny +from .models import User +from .serializers import ( + UserSerializer, + UserSignInSerializer, + UserSignUpSerializer, +) -class UserAPIView: - class List(ListAPIView): - queryset = User.objects.all() - serializer_class = UserSerializer - permission_classes = [IsAdminUser] +class UserViewSet(viewsets.ModelViewSet): + queryset = User.objects.all() + permission_classes = [AllowAny] - class SignUp(CreateAPIView): - serializer_class = UserSignUpSerializer - permission_classes = [AllowAny] + def get_serializer(self, *args, **kwargs): + if self.action == 'sign_up': + return UserSignUpSerializer(*args, **kwargs) + if self.action == 'sign_in': + return UserSignInSerializer(*args, **kwargs) + return UserSerializer(*args, **kwargs) - def perform_create(self, serializer): - boj_id = None - if 'boj_id' in serializer.validated_data: - boj_id = serializer.validated_data.pop('boj_id') - with atomic(): - user = User.objects.create_user(**serializer.validated_data) - user.save() - if boj_id is not None: - boj_user = BOJUser.objects.create(user=user, boj_id=boj_id) - boj_user.save() - user.boj_user = boj_user - user.save() - serializer.instance = user + def sign_up(self, request: Request): + serializer = UserSignUpSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = User.objects.create_user(**serializer.validated_data) + # TODO: User already exists error handling - class SignIn(GenericAPIView): - queryset = User.objects.all() - serializer_class = UserSignInSerializer - permission_classes = [AllowAny] + serializer.instance = user + return Response(serializer.data) - def post(self, request: Request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) + def sign_in(self, request: Request): + serializer = UserSignInSerializer(data=request.data) + serializer.is_valid(raise_exception=True) - # TODO: authenticate()만 이용하여 email, password로 인증하는 기능 리팩토링 - username = self._get_username(serializer) - password = serializer.validated_data['password'] + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] - user = authenticate(request, username=username, password=password) - if user is None: - return Response(status=HTTPStatus.UNAUTHORIZED) + print(email, password) + user = authenticate(request, email=email, password=password) + if user is None: + raise AuthenticationFailed('Invalid email or password') - login(request, user) + login(request, user) - serializer.instance = user - return Response(serializer.data) + serializer.instance = user + return Response(serializer.data) - def _get_username(self, serializer): - try: - user = User.objects.get(email=serializer.validated_data['email']) - except User.DoesNotExist: - raise AuthenticationFailed - return user.username + def sign_out(self, request: Request): + logout(request) + return Response(status=HTTPStatus.OK) - class SignOut(GenericAPIView): - def get(self, request: Request): - logout(request) - return Response(status=HTTPStatus.OK) +def authenticate(request: Request, email: str, password: str) -> User: + # TODO: User Manager 를 이용해서 authenticate 하도록 모델을 수정한 후, 이 프록시 메소드를 제거할 것. + queryset = User.objects.filter(email=email) + if not queryset.exists(): + return None + username = queryset.first().username + user = django_authenticate(request, username=username, password=password) + return user diff --git a/app/app/settings/base.py b/app/app/settings/base.py index d046d88..43e63d0 100644 --- a/app/app/settings/base.py +++ b/app/app/settings/base.py @@ -41,6 +41,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "account", "problem", ] diff --git a/app/app/urls.py b/app/app/urls.py index 430de74..2d2ea8a 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -20,12 +20,14 @@ from django.urls import include, path +import account.urls import problem.urls urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ + path("account/", include(account.urls.urlpatterns)), path("problem/", include(problem.urls.urlpatterns)), ])), ] From ab0a74d8b8cf7959382c74d210606ac520802c18 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 3 Jul 2024 16:25:14 +0900 Subject: [PATCH 192/552] =?UTF-8?q?feat(account):=20/account/current=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/account/urls.py | 3 +++ app/account/views.py | 13 ++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/account/urls.py b/app/account/urls.py index 5c976f7..2c130bf 100644 --- a/app/account/urls.py +++ b/app/account/urls.py @@ -4,6 +4,9 @@ urlpatterns = [ + path("current", UserViewSet.as_view({ + "get": "current", + })), path("signin", UserViewSet.as_view({ "post": "sign_in", })), diff --git a/app/account/views.py b/app/account/views.py index f6b01e8..a135996 100644 --- a/app/account/views.py +++ b/app/account/views.py @@ -19,7 +19,14 @@ ) -class UserViewSet(viewsets.ModelViewSet): +class UserViewSet(viewsets.GenericViewSet): + """사용자 계정과 관련된 API + + current: 현재 로그인한 사용자 정보 + signup: 사용자 등록(회원가입) + signin: 사용자 로그인 + signout: 사용자 로그아웃 + """ queryset = User.objects.all() permission_classes = [AllowAny] @@ -30,6 +37,10 @@ def get_serializer(self, *args, **kwargs): return UserSignInSerializer(*args, **kwargs) return UserSerializer(*args, **kwargs) + def current(self, request: Request): + serializer = self.get_serializer(instance=request.user) + return Response(serializer.data) + def sign_up(self, request: Request): serializer = UserSignUpSerializer(data=request.data) serializer.is_valid(raise_exception=True) From 1aafaa7b5fb48fb2717fa2c3c2bbf58967a3dd7c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 3 Jul 2024 16:43:37 +0900 Subject: [PATCH 193/552] chore(account.views): add TODOs --- app/account/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/account/views.py b/app/account/views.py index a135996..4becbca 100644 --- a/app/account/views.py +++ b/app/account/views.py @@ -72,6 +72,10 @@ def sign_out(self, request: Request): logout(request) return Response(status=HTTPStatus.OK) + # TODO: 이메일 인증 + + # TODO: 비밀번호 찾기 + def authenticate(request: Request, email: str, password: str) -> User: # TODO: User Manager 를 이용해서 authenticate 하도록 모델을 수정한 후, 이 프록시 메소드를 제거할 것. From 827b7aae5fd4d4746e5f490f7fed53a739fc9e42 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 9 Jul 2024 00:53:05 +0900 Subject: [PATCH 194/552] docs: create use-case-diagram.png --- docs/use-case-diagram.png | Bin 0 -> 446433 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/use-case-diagram.png diff --git a/docs/use-case-diagram.png b/docs/use-case-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..41dd4f625459a40c0ea965f9a4351fbc86d1d151 GIT binary patch literal 446433 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 z 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
    6fiB6+wTRml~q@JhL87 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

    *o<4u5kSY5if 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 JL Date: Sat, 13 Jul 2024 22:08:46 +0900 Subject: [PATCH 195/552] docs(diagrams): add ERD --- docs/diagrams.drawio | 279 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 271 insertions(+), 8 deletions(-) diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio index 96effc1..a431c9a 100644 --- a/docs/diagrams.drawio +++ b/docs/diagrams.drawio @@ -1,6 +1,6 @@ - + @@ -846,11 +846,6 @@ - - - - - @@ -871,15 +866,283 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From b5ef6f918e3194929a27565999ce54a4627b0148 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 1 Jul 2024 08:27:02 +0900 Subject: [PATCH 196/552] feat(problem): add `services`, `signals` for `problem` app --- app/problem/admin.py | 8 ++--- app/problem/models/__init__.py | 4 +-- .../{problem_analysis.py => analysis.py} | 29 ++++++++++++++++--- app/problem/models/problem.py | 13 +++++++++ app/problem/serializers/__init__.py | 2 +- .../{problem_analysis.py => analysis.py} | 8 ++--- app/problem/serializers/problem.py | 4 --- app/problem/services.py | 22 ++++++++++++++ app/problem/signals.py | 28 ++++++++++++++++++ app/problem/urls.py | 3 ++ app/problem/views.py | 10 +++++++ 11 files changed, 112 insertions(+), 19 deletions(-) rename app/problem/models/{problem_analysis.py => analysis.py} (68%) rename app/problem/serializers/{problem_analysis.py => analysis.py} (73%) create mode 100644 app/problem/services.py create mode 100644 app/problem/signals.py diff --git a/app/problem/admin.py b/app/problem/admin.py index a6b2322..bdc8364 100644 --- a/app/problem/admin.py +++ b/app/problem/admin.py @@ -13,11 +13,11 @@ class ProblemAdmin(admin.ModelAdmin): pass -@admin.register(ProblemAnalysis) -class ProblemAnalysisAdmin(admin.ModelAdmin): +@admin.register(Tag) +class TagAdmin(admin.ModelAdmin): pass -@admin.register(Tag) -class TagAdmin(admin.ModelAdmin): +@admin.register(Analysis) +class AnalysisAdmin(admin.ModelAdmin): pass diff --git a/app/problem/models/__init__.py b/app/problem/models/__init__.py index dd9e460..cfb0c7c 100644 --- a/app/problem/models/__init__.py +++ b/app/problem/models/__init__.py @@ -1,5 +1,5 @@ -from .problem import Problem -from .problem_analysis import ProblemAnalysis +from .analysis import Analysis, AnalysisDTO +from .problem import Problem, ProblemDTO from .difficulty import Difficulty from .language import Language from .tag import Tag diff --git a/app/problem/models/problem_analysis.py b/app/problem/models/analysis.py similarity index 68% rename from app/problem/models/problem_analysis.py rename to app/problem/models/analysis.py index da63613..d479f89 100644 --- a/app/problem/models/problem_analysis.py +++ b/app/problem/models/analysis.py @@ -1,11 +1,14 @@ +from dataclasses import dataclass +from typing import List + from django.db import models -from .difficulty import Difficulty -from .problem import Problem -from .tag import Tag +from problem.models.difficulty import Difficulty +from problem.models.problem import Problem +from problem.models.tag import Tag -class ProblemAnalysis(models.Model): +class Analysis(models.Model): problem = models.OneToOneField( Problem, on_delete=models.CASCADE, @@ -37,6 +40,16 @@ class ProblemAnalysis(models.Model): # TODO: 시간 복잡도 검증 로직 추가 ], ) + hint = models.JSONField( + help_text=( + '문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.' + ), + validators=[ + # TODO: 힌트 검증 로직 추가 + ], + blank=False, + default=list, + ) created_at = models.DateTimeField(auto_now_add=True) # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 @@ -46,3 +59,11 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' + + +@dataclass +class AnalysisDTO: + time_complexity: str + difficulty: str + tags: List[str] + hint: List[str] diff --git a/app/problem/models/problem.py b/app/problem/models/problem.py index f4c9c81..0ddc8f0 100644 --- a/app/problem/models/problem.py +++ b/app/problem/models/problem.py @@ -1,3 +1,6 @@ +from dataclasses import dataclass +from typing import List + from django.db import models from account.models import User @@ -63,3 +66,13 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' + + +@dataclass +class ProblemDTO: + title: str + description: str + input_description: str + output_description: str + memory_limit: float + time_limit: float diff --git a/app/problem/serializers/__init__.py b/app/problem/serializers/__init__.py index 6dea049..611edfb 100644 --- a/app/problem/serializers/__init__.py +++ b/app/problem/serializers/__init__.py @@ -1,4 +1,4 @@ from .problem import ProblemSerializer -from .problem_analysis import ProblemAnalysisSerializer from .language import LanguageSerializer from .tag import TagSerializer +from .analysis import AnalysisSerializer diff --git a/app/problem/serializers/problem_analysis.py b/app/problem/serializers/analysis.py similarity index 73% rename from app/problem/serializers/problem_analysis.py rename to app/problem/serializers/analysis.py index cc14cb1..1057b1b 100644 --- a/app/problem/serializers/problem_analysis.py +++ b/app/problem/serializers/analysis.py @@ -1,14 +1,14 @@ from rest_framework.serializers import ModelSerializer -from ..models import ProblemAnalysis -from ..serializers.tag import TagSerializer +from ..models import Analysis +from ..serializers import TagSerializer -class ProblemAnalysisSerializer(ModelSerializer): +class AnalysisSerializer(ModelSerializer): tags = TagSerializer(many=True) class Meta: - model = ProblemAnalysis + model = Analysis fields = [ 'id', 'problem', diff --git a/app/problem/serializers/problem.py b/app/problem/serializers/problem.py index af06cba..3654905 100644 --- a/app/problem/serializers/problem.py +++ b/app/problem/serializers/problem.py @@ -3,18 +3,15 @@ from account.serializers import UserSerializer from ..models import Problem -from ..serializers.problem_analysis import ProblemAnalysisSerializer class ProblemSerializer(ModelSerializer): user = UserSerializer(read_only=True) - analysis = ProblemAnalysisSerializer(read_only=True) class Meta: model = Problem fields = [ 'id', - 'analysis', 'title', 'link', 'description', @@ -26,6 +23,5 @@ class Meta: ] extra_kwargs = { 'id': {'read_only': True}, - 'analysis': {'read_only': True}, 'user': {'read_only': True}, } diff --git a/app/problem/services.py b/app/problem/services.py new file mode 100644 index 0000000..0d7043c --- /dev/null +++ b/app/problem/services.py @@ -0,0 +1,22 @@ +from __future__ import annotations + +from .models import ProblemDTO, AnalysisDTO + + +def get_analyser() -> ProblemAnalyser: + return GPTProblemAnalyser() + + +class ProblemAnalyser: + def analyze(self, problem: ProblemDTO) -> AnalysisDTO: + raise NotImplementedError + + +class GPTProblemAnalyser(ProblemAnalyser): + def analyze(self, problem: ProblemDTO) -> AnalysisDTO: + raise NotImplementedError + + +class GeminiProblemAnalyser(ProblemAnalyser): + def analyze(self, problem: ProblemDTO) -> AnalysisDTO: + raise NotImplementedError diff --git a/app/problem/signals.py b/app/problem/signals.py new file mode 100644 index 0000000..b768ebd --- /dev/null +++ b/app/problem/signals.py @@ -0,0 +1,28 @@ +from django.db import transaction +from django.db.models import signals +from django.dispatch import receiver + +from .models import ( + Problem, + Analysis, +) +from .services import get_analyser + + +@receiver(signals.post_save, sender=Problem) +def problem_on_post_save(sender, instance: Problem, created: bool, **kwargs): + if not created: + return + analyser = get_analyser() + analysis_dto = analyser.analyze(instance) + with transaction.atomic(): + analysis = Analysis.objects.create( + problem=instance, + difficulty=analysis_dto.difficulty, + tags=analysis_dto.tags, + time_complexity=analysis_dto.time_complexity, + hint=analysis_dto.hint, + ) + analysis.save() + instance.analysis = analysis + instance.save() diff --git a/app/problem/urls.py b/app/problem/urls.py index 99ff8a4..f9d6f5f 100644 --- a/app/problem/urls.py +++ b/app/problem/urls.py @@ -17,6 +17,9 @@ "put": "update", "delete": "destroy", })), + path("analysis", AnalysisViewSet.as_view({ + "get": "retrieve", + })), ])), path("language/", include([ path("", LanguageViewSet.as_view({ diff --git a/app/problem/views.py b/app/problem/views.py index 4f4ac6a..cd8348b 100644 --- a/app/problem/views.py +++ b/app/problem/views.py @@ -11,6 +11,7 @@ 'TagViewSet', 'LanguageViewSet', 'ProblemViewSet', + 'AnalysisViewSet', ) @@ -47,3 +48,12 @@ def my_list(self, request: Request): queryset = Problem.objects.filter(user=request.user) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) + + +class AnalysisViewSet(viewsets.ModelViewSet): + """문제 태그 목록 조회 + 생성 기능""" + queryset = Analysis.objects.all() + serializer_class = AnalysisSerializer + permission_classes = [IsAdminUser | IsReadOnly] + lookup_field = 'problem__id' + lookup_url_kwarg = 'pk' From be7916a801a5977828fa6d77a16a1905a8024c97 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 02:06:37 +0900 Subject: [PATCH 197/552] feat(tle): create app tle --- app/tle/__init__.py | 0 app/tle/admin.py | 3 +++ app/tle/apps.py | 6 ++++++ app/tle/migrations/__init__.py | 0 app/tle/tests.py | 3 +++ 5 files changed, 12 insertions(+) create mode 100644 app/tle/__init__.py create mode 100644 app/tle/admin.py create mode 100644 app/tle/apps.py create mode 100644 app/tle/migrations/__init__.py create mode 100644 app/tle/tests.py diff --git a/app/tle/__init__.py b/app/tle/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/admin.py b/app/tle/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/app/tle/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/app/tle/apps.py b/app/tle/apps.py new file mode 100644 index 0000000..dcf2508 --- /dev/null +++ b/app/tle/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TleConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "tle" diff --git a/app/tle/migrations/__init__.py b/app/tle/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/tests.py b/app/tle/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/tle/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. From 3b1cb6cb3847ccf8636132ec4bdeb9683ca8fab9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 02:11:41 +0900 Subject: [PATCH 198/552] REFACTOR: remove app `account`, `crew`, `problem` --- app/account/__init__.py | 0 app/account/admin.py | 8 -- app/account/apps.py | 6 - app/account/migrations/__init__.py | 0 app/account/models.py | 38 ------ app/account/serializers.py | 54 -------- app/account/urls.py | 19 --- app/account/views.py | 87 ------------- app/app/{settings/base.py => settings.py} | 11 +- app/app/settings/__init__.py | 1 - app/app/settings/debug.py | 11 -- app/app/urls.py | 8 -- app/crew/__init__.py | 0 app/crew/admin.py | 38 ------ app/crew/apps.py | 6 - app/crew/migrations/__init__.py | 0 app/crew/models/__init__.py | 7 -- app/crew/models/crew.py | 118 ------------------ app/crew/models/crew_activity.py | 35 ------ app/crew/models/crew_activity_problem.py | 40 ------ .../crew_activity_problem_submission.py | 57 --------- ...rew_activity_problem_submission_comment.py | 56 --------- app/crew/models/crew_member.py | 31 ----- app/crew/models/crew_member_request.py | 38 ------ app/crew/serializers/__init__.py | 12 -- app/crew/serializers/crew.py | 48 ------- app/crew/serializers/crew_activity.py | 23 ---- app/crew/serializers/crew_member.py | 22 ---- app/crew/serializers/mixins.py | 65 ---------- app/crew/views.py | 60 --------- app/problem/__init__.py | 0 app/problem/admin.py | 23 ---- app/problem/apps.py | 6 - app/problem/migrations/__init__.py | 0 app/problem/models/__init__.py | 5 - app/problem/models/analysis.py | 69 ---------- app/problem/models/difficulty.py | 7 -- app/problem/models/language.py | 30 ----- app/problem/models/problem.py | 78 ------------ app/problem/models/tag.py | 45 ------- app/problem/permissions.py | 38 ------ app/problem/serializers/__init__.py | 4 - app/problem/serializers/analysis.py | 24 ---- app/problem/serializers/language.py | 9 -- app/problem/serializers/problem.py | 27 ---- app/problem/serializers/tag.py | 9 -- app/problem/services.py | 22 ---- app/problem/signals.py | 28 ----- app/problem/urls.py | 46 ------- app/problem/views.py | 59 --------- 50 files changed, 8 insertions(+), 1420 deletions(-) delete mode 100644 app/account/__init__.py delete mode 100644 app/account/admin.py delete mode 100644 app/account/apps.py delete mode 100644 app/account/migrations/__init__.py delete mode 100644 app/account/models.py delete mode 100644 app/account/serializers.py delete mode 100644 app/account/urls.py delete mode 100644 app/account/views.py rename app/app/{settings/base.py => settings.py} (97%) delete mode 100644 app/app/settings/__init__.py delete mode 100644 app/app/settings/debug.py delete mode 100644 app/crew/__init__.py delete mode 100644 app/crew/admin.py delete mode 100644 app/crew/apps.py delete mode 100644 app/crew/migrations/__init__.py delete mode 100644 app/crew/models/__init__.py delete mode 100644 app/crew/models/crew.py delete mode 100644 app/crew/models/crew_activity.py delete mode 100644 app/crew/models/crew_activity_problem.py delete mode 100644 app/crew/models/crew_activity_problem_submission.py delete mode 100644 app/crew/models/crew_activity_problem_submission_comment.py delete mode 100644 app/crew/models/crew_member.py delete mode 100644 app/crew/models/crew_member_request.py delete mode 100644 app/crew/serializers/__init__.py delete mode 100644 app/crew/serializers/crew.py delete mode 100644 app/crew/serializers/crew_activity.py delete mode 100644 app/crew/serializers/crew_member.py delete mode 100644 app/crew/serializers/mixins.py delete mode 100644 app/crew/views.py delete mode 100644 app/problem/__init__.py delete mode 100644 app/problem/admin.py delete mode 100644 app/problem/apps.py delete mode 100644 app/problem/migrations/__init__.py delete mode 100644 app/problem/models/__init__.py delete mode 100644 app/problem/models/analysis.py delete mode 100644 app/problem/models/difficulty.py delete mode 100644 app/problem/models/language.py delete mode 100644 app/problem/models/problem.py delete mode 100644 app/problem/models/tag.py delete mode 100644 app/problem/permissions.py delete mode 100644 app/problem/serializers/__init__.py delete mode 100644 app/problem/serializers/analysis.py delete mode 100644 app/problem/serializers/language.py delete mode 100644 app/problem/serializers/problem.py delete mode 100644 app/problem/serializers/tag.py delete mode 100644 app/problem/services.py delete mode 100644 app/problem/signals.py delete mode 100644 app/problem/urls.py delete mode 100644 app/problem/views.py diff --git a/app/account/__init__.py b/app/account/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/account/admin.py b/app/account/admin.py deleted file mode 100644 index addcd8a..0000000 --- a/app/account/admin.py +++ /dev/null @@ -1,8 +0,0 @@ -from django.contrib import admin - -from account.models import * - - -@admin.register(User) -class AccountAdmin(admin.ModelAdmin): - pass diff --git a/app/account/apps.py b/app/account/apps.py deleted file mode 100644 index 2c684a9..0000000 --- a/app/account/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class AccountConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "account" diff --git a/app/account/migrations/__init__.py b/app/account/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/account/models.py b/app/account/models.py deleted file mode 100644 index 56299b8..0000000 --- a/app/account/models.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.contrib.auth.models import User as DjangoUser -from django.db import models - - -class User(DjangoUser): - REQUIRED_FIELDS = [ - 'email', - 'username', - 'password', - ] - image = models.ImageField( - help_text='프로필 이미지', - upload_to='user_images/', - null=True, - blank=True, - validators=[ - # TODO: 이미지 크기 제한 - # TODO: 이미지 확장자 제한 - ] - ) - boj_id = models.CharField( - help_text='백준 아이디', - max_length=40, - null=True, - blank=True, - ) - - def __repr__(self) -> str: - return f'[@{self.username}]' - - def __str__(self) -> str: - staff = '(관리자)' if self.is_staff else '' - return f'{self.pk} : {self.__repr__()} {staff}' - - -User._meta.get_field('email')._unique = True -User._meta.get_field('email').blank = False -User._meta.get_field('email').null = False diff --git a/app/account/serializers.py b/app/account/serializers.py deleted file mode 100644 index c52fb3c..0000000 --- a/app/account/serializers.py +++ /dev/null @@ -1,54 +0,0 @@ -from rest_framework.serializers import * - -from .models import User - - -class UserSerializer(ModelSerializer): - class Meta: - model = User - fields = [ - 'id', - 'image', - 'username', - ] - - -class UserSignInSerializer(ModelSerializer): - email = EmailField() - - class Meta: - model = User - fields = [ - 'id', - 'email', - 'password', - 'image', - 'username', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'image': {'read_only': True}, - 'username': {'read_only': True}, - 'email': {'write_only': True}, - 'password': {'write_only': True}, - } - - -class UserSignUpSerializer(ModelSerializer): - boj_id = CharField(max_length=40, required=False) - - class Meta: - model = User - fields = [ - 'id', - 'boj_id', - 'image', - 'username', - 'email', - 'password', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'boj_id': {'write_only': True}, - 'password': {'write_only': True}, - } diff --git a/app/account/urls.py b/app/account/urls.py deleted file mode 100644 index 2c130bf..0000000 --- a/app/account/urls.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.urls import path - -from .views import UserViewSet - - -urlpatterns = [ - path("current", UserViewSet.as_view({ - "get": "current", - })), - path("signin", UserViewSet.as_view({ - "post": "sign_in", - })), - path("signup", UserViewSet.as_view({ - "post": "sign_up", - })), - path("signout", UserViewSet.as_view({ - "get": "sign_out", - })), -] diff --git a/app/account/views.py b/app/account/views.py deleted file mode 100644 index 4becbca..0000000 --- a/app/account/views.py +++ /dev/null @@ -1,87 +0,0 @@ -from http import HTTPStatus - -from django.contrib.auth import ( - authenticate as django_authenticate, - login, - logout, -) -from rest_framework import viewsets -from rest_framework.exceptions import AuthenticationFailed -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.permissions import AllowAny - -from .models import User -from .serializers import ( - UserSerializer, - UserSignInSerializer, - UserSignUpSerializer, -) - - -class UserViewSet(viewsets.GenericViewSet): - """사용자 계정과 관련된 API - - current: 현재 로그인한 사용자 정보 - signup: 사용자 등록(회원가입) - signin: 사용자 로그인 - signout: 사용자 로그아웃 - """ - queryset = User.objects.all() - permission_classes = [AllowAny] - - def get_serializer(self, *args, **kwargs): - if self.action == 'sign_up': - return UserSignUpSerializer(*args, **kwargs) - if self.action == 'sign_in': - return UserSignInSerializer(*args, **kwargs) - return UserSerializer(*args, **kwargs) - - def current(self, request: Request): - serializer = self.get_serializer(instance=request.user) - return Response(serializer.data) - - def sign_up(self, request: Request): - serializer = UserSignUpSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - user = User.objects.create_user(**serializer.validated_data) - # TODO: User already exists error handling - - serializer.instance = user - return Response(serializer.data) - - def sign_in(self, request: Request): - serializer = UserSignInSerializer(data=request.data) - serializer.is_valid(raise_exception=True) - - email = serializer.validated_data['email'] - password = serializer.validated_data['password'] - - print(email, password) - user = authenticate(request, email=email, password=password) - if user is None: - raise AuthenticationFailed('Invalid email or password') - - login(request, user) - - serializer.instance = user - return Response(serializer.data) - - def sign_out(self, request: Request): - logout(request) - return Response(status=HTTPStatus.OK) - - # TODO: 이메일 인증 - - # TODO: 비밀번호 찾기 - - -def authenticate(request: Request, email: str, password: str) -> User: - # TODO: User Manager 를 이용해서 authenticate 하도록 모델을 수정한 후, 이 프록시 메소드를 제거할 것. - queryset = User.objects.filter(email=email) - if not queryset.exists(): - return None - username = queryset.first().username - user = django_authenticate(request, username=username, password=password) - return user diff --git a/app/app/settings/base.py b/app/app/settings.py similarity index 97% rename from app/app/settings/base.py rename to app/app/settings.py index 43e63d0..abc5a38 100644 --- a/app/app/settings/base.py +++ b/app/app/settings.py @@ -11,6 +11,11 @@ """ from pathlib import Path +import os + + +GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", None) + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent.parent @@ -23,11 +28,12 @@ SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False +DEBUG = True ALLOWED_HOSTS = [ 'tle-kr.com', 'timelimitexceeded.kr', + 'localhost', ] @@ -41,8 +47,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", - "account", - "problem", + "tle", ] MIDDLEWARE = [ diff --git a/app/app/settings/__init__.py b/app/app/settings/__init__.py deleted file mode 100644 index bb7c9d8..0000000 --- a/app/app/settings/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from app.settings.base import * diff --git a/app/app/settings/debug.py b/app/app/settings/debug.py deleted file mode 100644 index c4bad96..0000000 --- a/app/app/settings/debug.py +++ /dev/null @@ -1,11 +0,0 @@ -from app.settings.base import * - - -DEBUG = True - -ALLOWED_HOSTS += [ - '*', -] - -INSTALLED_APPS += [ -] \ No newline at end of file diff --git a/app/app/urls.py b/app/app/urls.py index 2d2ea8a..3518843 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -20,16 +20,8 @@ from django.urls import include, path -import account.urls -import problem.urls - - urlpatterns = [ path("admin/", admin.site.urls), - path("api/v1/", include([ - path("account/", include(account.urls.urlpatterns)), - path("problem/", include(problem.urls.urlpatterns)), - ])), ] # Static files diff --git a/app/crew/__init__.py b/app/crew/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/crew/admin.py b/app/crew/admin.py deleted file mode 100644 index 5a163a9..0000000 --- a/app/crew/admin.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.contrib import admin - -from crew.models import * - - -@admin.register(Crew) -class CrewAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewMember) -class CrewMemberAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewMemberRequest) -class CrewMemberRequestAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewActivity) -class CrewActivityAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewActivityProblem) -class CrewActivityProblemAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewActivityProblemSubmission) -class CrewActivityProblemSubmissionAdmin(admin.ModelAdmin): - pass - - -@admin.register(CrewActivityProblemSubmissionComment) -class CrewActivityProblemSubmissionCommentAdmin(admin.ModelAdmin): - pass diff --git a/app/crew/apps.py b/app/crew/apps.py deleted file mode 100644 index 1485e3e..0000000 --- a/app/crew/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class CrewConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "crew" diff --git a/app/crew/migrations/__init__.py b/app/crew/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/crew/models/__init__.py b/app/crew/models/__init__.py deleted file mode 100644 index af4057c..0000000 --- a/app/crew/models/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from .crew import Crew -from .crew_activity import CrewActivity -from .crew_activity_problem import CrewActivityProblem -from .crew_activity_problem_submission import CrewActivityProblemSubmission -from .crew_activity_problem_submission_comment import CrewActivityProblemSubmissionComment -from .crew_member import CrewMember -from .crew_member_request import CrewMemberRequest diff --git a/app/crew/models/crew.py b/app/crew/models/crew.py deleted file mode 100644 index 73798b5..0000000 --- a/app/crew/models/crew.py +++ /dev/null @@ -1,118 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from boj.models import BOJLevel -from core.models import Language -from user.models import User - - -class Crew(models.Model): - name = models.CharField( - max_length=20, - unique=True, - help_text=( - '크루 이름을 입력해주세요. (최대 20자)' - ), - ) - emoji = models.CharField( - max_length=2, - help_text=( - '크루 아이콘을 입력해주세요. (이모지)' - ), - validators=[ - # TODO: 이모지 형식 검사 - ], - null=True, - blank=True, - ) - captain = models.ForeignKey( - User, - on_delete=models.PROTECT, - related_name='crews_as_captain', - help_text=( - '크루장을 입력해주세요.' - ), - ) - notice = models.TextField( - help_text=( - '크루 공지를 입력해주세요.' - ), - null=True, - blank=True, - ) - languages = models.ManyToManyField( - Language, - related_name='crews', - help_text=( - '유저가 사용 가능한 언어를 입력해주세요.' - ), - ) - max_member = models.IntegerField( - help_text=( - '크루 최대 인원을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 최대 인원 제한 - ], - ) - is_recruiting = models.BooleanField( - help_text=( - '모집 중 여부를 입력해주세요.' - ), - default=True, - ) - is_active = models.BooleanField( - help_text=( - '활동 중인지 여부를 입력해주세요.' - ), - default=True, - ) - is_boj_user_only = models.BooleanField( - help_text=( - '백준 아이디 필요 여부를 입력해주세요.' - ), - default=False, - ) - min_boj_tier = models.IntegerField( - help_text=( - '최소 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - max_boj_tier = models.IntegerField( - help_text=( - '최대 백준 레벨을 입력해주세요. ', - '0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I' - ), - validators=[ - # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 - ], - choices=BOJLevel.choices, - blank=True, - null=True, - default=None, - ) - tags = models.JSONField( - help_text=( - '태그를 입력해주세요.' - ), - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'[{self.emoji} {self.name}]' - - def __str__(self) -> str: - member_count = f'({self.members.count()}/{self.max_member})' - return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' diff --git a/app/crew/models/crew_activity.py b/app/crew/models/crew_activity.py deleted file mode 100644 index 83d447f..0000000 --- a/app/crew/models/crew_activity.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.db import models - -from crew.models.crew import Crew - - -class CrewActivity(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='activities', - help_text=( - '크루를 입력해주세요.' - ), - ) - name = models.TextField( - help_text=( - '활동 이름을 입력해주세요. (예: "1회차")' - ), - ) - start_at = models.DateTimeField( - help_text=( - '활동 시작 일자를 입력해주세요.' - ), - ) - end_at = models.DateTimeField( - help_text=( - '활동 종료 일자를 입력해주세요.' - ), - ) - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/models/crew_activity_problem.py b/app/crew/models/crew_activity_problem.py deleted file mode 100644 index 4e6d5b1..0000000 --- a/app/crew/models/crew_activity_problem.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from crew.models.crew_activity import CrewActivity -from problem.models import Problem - - -class CrewActivityProblem(models.Model): - activity = models.ForeignKey( - CrewActivity, - on_delete=models.CASCADE, - related_name='problems', - help_text=( - '활동을 입력해주세요.' - ), - ) - problem = models.ForeignKey( - Problem, - on_delete=models.PROTECT, - related_name='activities', - help_text=( - '문제를 입력해주세요.' - ), - ) - order = models.IntegerField( - help_text=( - '문제 순서를 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 다른 문제 순서와 겹치지 않도록 검사 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/models/crew_activity_problem_submission.py b/app/crew/models/crew_activity_problem_submission.py deleted file mode 100644 index 26de633..0000000 --- a/app/crew/models/crew_activity_problem_submission.py +++ /dev/null @@ -1,57 +0,0 @@ -from django.db import models - -from core.models import Language -from crew.models.crew_activity_problem import CrewActivityProblem -from user.models import User - - -class CrewActivityProblemSubmission(models.Model): - # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 - activity_problem = models.ForeignKey( - CrewActivityProblem, - on_delete=models.CASCADE, - related_name='submissions', - help_text=( - '활동 문제를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='submissions', - help_text=( - '유저를 입력해주세요.' - ), - ) - code = models.TextField( - help_text=( - '유저의 코드를 입력해주세요.' - ), - ) - language = models.ForeignKey( - Language, - on_delete=models.PROTECT, - related_name='submissions', - help_text=( - '유저의 코드 언어를 입력해주세요.' - ), - ) - is_correct = models.BooleanField( - help_text=( - '유저의 코드가 정답인지 여부를 입력해주세요.' - ), - ) - is_help_needed = models.BooleanField( - help_text=( - '유저의 코드에 도움이 필요한지 여부를 입력해주세요.' - ), - default=False, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/models/crew_activity_problem_submission_comment.py b/app/crew/models/crew_activity_problem_submission_comment.py deleted file mode 100644 index 7aa7afb..0000000 --- a/app/crew/models/crew_activity_problem_submission_comment.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from crew.models.crew_activity_problem_submission import CrewActivityProblemSubmission -from user.models import User - - -class CrewActivityProblemSubmissionComment(models.Model): - submission = models.ForeignKey( - CrewActivityProblemSubmission, - on_delete=models.CASCADE, - related_name='comments', - help_text=( - '제출을 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='comments', - help_text=( - '유저를 입력해주세요.' - ), - ) - content = models.TextField( - help_text=( - '댓글을 입력해주세요.' - ), - ) - line_number_start = models.IntegerField( - help_text=( - '댓글 시작 라인을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - ], - ) - line_number_end = models.IntegerField( - help_text=( - '댓글 종료 라인을 입력해주세요.' - ), - validators=[ - MinValueValidator(1), - # TODO: 시작 라인보다 작지 않도록 검사 - # TODO: 코드 라인 수보다 크지 않도록 검사 - ], - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - line_range = f'L{self.line_number_start}:L{self.line_number_end}' - return f'{self.submission.__repr__()} ← {self.user.__repr__()} {line_range} "{self.content}"' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/models/crew_member.py b/app/crew/models/crew_member.py deleted file mode 100644 index bf0736c..0000000 --- a/app/crew/models/crew_member.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.db import models - -from crew.models.crew import Crew -from user.models import User - - -class CrewMember(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='members', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crews', - help_text=( - '유저를 입력해주세요.' - ), - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/models/crew_member_request.py b/app/crew/models/crew_member_request.py deleted file mode 100644 index b1e77ec..0000000 --- a/app/crew/models/crew_member_request.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.db import models - -from crew.models.crew import Crew -from user.models import User - - -class CrewMemberRequest(models.Model): - # TODO: 같은 크루에 여러 번 가입하는 것을 막기 위한 로직 추가 - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - related_name='requests', - help_text=( - '크루를 입력해주세요.' - ), - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - related_name='crew_requests', - help_text=( - '유저를 입력해주세요.' - ), - ) - message = models.TextField( - help_text=( - '가입 메시지를 입력해주세요.' - ), - null=True, - blank=True, - ) - created_at = models.DateTimeField(auto_now_add=True) - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crew/serializers/__init__.py b/app/crew/serializers/__init__.py deleted file mode 100644 index 3520fa7..0000000 --- a/app/crew/serializers/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from crew.serializers.crew import ( - CrewSerializer, - CrewDetailSerializer, -) -from crew.serializers.crew_member import CrewMemberSerializer - - -__all__ = [ - 'CrewSerializer', - 'CrewDetailSerializer', - 'CrewMemberSerializer', -] diff --git a/app/crew/serializers/crew.py b/app/crew/serializers/crew.py deleted file mode 100644 index c967fee..0000000 --- a/app/crew/serializers/crew.py +++ /dev/null @@ -1,48 +0,0 @@ -from rest_framework.serializers import ( - ModelSerializer, - SerializerMethodField, -) - -from crew.serializers.mixins import ( - MembersMixin, - TagsMixin, -) -from crew.models import Crew -from crew.serializers.crew_activity import CrewActivitySerializer - - -class CrewSerializer(ModelSerializer, MembersMixin, TagsMixin): - """크루에 대한 공개된 데이터를 다룬다.""" - - members = SerializerMethodField() - tags = SerializerMethodField() - - class Meta: - model = Crew - fields = [ - 'emoji', - 'name', - 'members', - 'tags', - 'is_recruiting', - ] - - -class CrewDetailSerializer(ModelSerializer, MembersMixin, TagsMixin): - """크루에 대한 상세 데이터를 다룬다.""" - - members = SerializerMethodField() - tags = SerializerMethodField() - activities = CrewActivitySerializer(many=True, read_only=True) - - class Meta: - model = Crew - fields = [ - 'emoji', - 'name', - 'members', - 'tags', - 'activities', - 'is_active', - 'is_recruiting', - ] diff --git a/app/crew/serializers/crew_activity.py b/app/crew/serializers/crew_activity.py deleted file mode 100644 index e1be6ff..0000000 --- a/app/crew/serializers/crew_activity.py +++ /dev/null @@ -1,23 +0,0 @@ -from rest_framework.serializers import ( - ModelSerializer, - SerializerMethodField, -) - -from crew.models import CrewActivity - - -class CrewActivitySerializer(ModelSerializer): - date = SerializerMethodField() - - class Meta: - model = CrewActivity - fields = ( - 'name', - 'date', - ) - - def get_date(self, obj: CrewActivity): - return { - 'start_at': obj.start_at, - 'end_at': obj.end_at, - } diff --git a/app/crew/serializers/crew_member.py b/app/crew/serializers/crew_member.py deleted file mode 100644 index 2dbf512..0000000 --- a/app/crew/serializers/crew_member.py +++ /dev/null @@ -1,22 +0,0 @@ -from rest_framework.serializers import ( - ModelSerializer, - SerializerMethodField, -) - -from crew.models import CrewMember -from user.serializers import UserSerializer - - -class CrewMemberSerializer(ModelSerializer): - user = UserSerializer(read_only=True) - is_captain = SerializerMethodField() - - class Meta: - model = CrewMember - fields = ( - 'user', - 'is_captain', - ) - - def get_is_captain(self, obj: CrewMember) -> bool: - return obj.crew.captain == obj.user diff --git a/app/crew/serializers/mixins.py b/app/crew/serializers/mixins.py deleted file mode 100644 index 5e931cd..0000000 --- a/app/crew/serializers/mixins.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import ( - Iterable, -) - -from boj.models import BOJLevel -from core.models import Language -from crew.models import Crew -from crew.serializers.crew_member import CrewMemberSerializer - - -class MembersMixin: - def get_members(self, obj: Crew): - return { - 'count': self._get_member_count(obj), - 'max_count': self._get_member_max_count(obj), - 'items': self._get_members_list(obj), - } - - def _get_member_count(self, obj: Crew): - return obj.members.count() - - def _get_member_max_count(self, obj: Crew): - return obj.max_member - - def _get_members_list(self, obj: Crew): - return CrewMemberSerializer(obj.members.all(), many=True).data - - -class TagsMixin: - def get_tags(self, obj: Crew): - tags = [ - *self._get_language_tags(obj), - *self._get_tier_tags(obj), - *self._get_custom_tags(obj), - ] - return { - 'count': len(tags), - 'items': tags, - } - - def _get_language_tags(self, obj: Crew): - languages: Iterable[Language] = obj.languages.all() - return [{'key': lang.key, 'name': lang.name} for lang in languages] - - def _get_tier_tags(self, obj: Crew): - tags = [] - if obj.min_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.min_boj_tier).label} 이상', - }) - if obj.max_boj_tier is not None: - tags.append({ - 'key': None, - 'name': f'{BOJLevel(obj.max_boj_tier).label} 이하', - }) - if obj.min_boj_tier is None and obj.max_boj_tier is None: - tags.append({ - 'key': None, - 'name': '티어 무관', - }) - return tags - - def _get_custom_tags(self, obj: Crew): - return [{'key': None, 'name': tag} for tag in obj.tags] diff --git a/app/crew/views.py b/app/crew/views.py deleted file mode 100644 index 500dd25..0000000 --- a/app/crew/views.py +++ /dev/null @@ -1,60 +0,0 @@ -import logging - -from rest_framework.generics import * -from rest_framework.permissions import * - -from user.models import User - -from .models import * -from .serializers import * - - -logger = logging.getLogger(__name__) - - -def _get_user(view: GenericAPIView) -> User: - user = view.request.user - try: - return User.objects.get(pk=user.pk) - except User.DoesNotExist: - logger.error(f'User not found. {user.pk}') - logger.error( - f'checking user model... ' - f'expected: {User.__class__} ' - f'actual: {user.__class__}' - ) - return None - - -class CrewAPIView: - class ListCreate(ListCreateAPIView): - queryset = Crew.objects.all() - serializer_class = CrewSerializer - permission_classes = [IsAuthenticated] - - def perform_create(self, serializer): - serializer.save(captain=_get_user(self)) - - class MyList(ListAPIView): - """내가 속한 크루의 목록을 반환합니다.""" - serializer_class = CrewDetailSerializer - permission_classes = [IsAuthenticated] - - def get_queryset(self): - user = _get_user(self) - return Crew.objects.filter(members__user=user) - - class RecruitingList(ListAPIView): - """현재 참가자를 모집 중인 크루의 목록을 반환합니다.""" - serializer_class = CrewSerializer - permission_classes = [IsAuthenticated] - - def get_queryset(self): - # TODO: 언어, 티어 조건에 따라 필터링 - return Crew.objects.filter(is_recruiting=True) - - class RetrieveUpdateDestroy(RetrieveUpdateDestroyAPIView): - queryset = Crew.objects.all() - serializer_class = CrewSerializer - permission_classes = [IsAuthenticated] - lookup_url_kwarg = 'id' diff --git a/app/problem/__init__.py b/app/problem/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/problem/admin.py b/app/problem/admin.py deleted file mode 100644 index bdc8364..0000000 --- a/app/problem/admin.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.contrib import admin - -from .models import * - - -@admin.register(Language) -class LanguageAdmin(admin.ModelAdmin): - pass - - -@admin.register(Problem) -class ProblemAdmin(admin.ModelAdmin): - pass - - -@admin.register(Tag) -class TagAdmin(admin.ModelAdmin): - pass - - -@admin.register(Analysis) -class AnalysisAdmin(admin.ModelAdmin): - pass diff --git a/app/problem/apps.py b/app/problem/apps.py deleted file mode 100644 index ab8c85e..0000000 --- a/app/problem/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class ProblemConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "problem" diff --git a/app/problem/migrations/__init__.py b/app/problem/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/problem/models/__init__.py b/app/problem/models/__init__.py deleted file mode 100644 index cfb0c7c..0000000 --- a/app/problem/models/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from .analysis import Analysis, AnalysisDTO -from .problem import Problem, ProblemDTO -from .difficulty import Difficulty -from .language import Language -from .tag import Tag diff --git a/app/problem/models/analysis.py b/app/problem/models/analysis.py deleted file mode 100644 index d479f89..0000000 --- a/app/problem/models/analysis.py +++ /dev/null @@ -1,69 +0,0 @@ -from dataclasses import dataclass -from typing import List - -from django.db import models - -from problem.models.difficulty import Difficulty -from problem.models.problem import Problem -from problem.models.tag import Tag - - -class Analysis(models.Model): - problem = models.OneToOneField( - Problem, - on_delete=models.CASCADE, - related_name='analysis', - help_text=( - '문제를 입력해주세요.' - ), - ) - difficulty = models.IntegerField( - help_text=( - '문제 난이도를 입력해주세요.' - ), - choices=Difficulty.choices, - ) - tags = models.ManyToManyField( - Tag, - related_name='problems', - help_text=( - '문제의 DSA 태그를 입력해주세요.' - ), - ) - time_complexity = models.CharField( - max_length=100, - help_text=( - '문제 시간 복잡도를 입력해주세요. ', - '예) O(1), O(n), O(n^2), O(V \log E) 등', - ), - validators=[ - # TODO: 시간 복잡도 검증 로직 추가 - ], - ) - hint = models.JSONField( - help_text=( - '문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.' - ), - validators=[ - # TODO: 힌트 검증 로직 추가 - ], - blank=False, - default=list, - ) - created_at = models.DateTimeField(auto_now_add=True) - # TODO: 사용자가 추가한 정보인지 확인하는 필드 추가 - - def __repr__(self) -> str: - tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) - return f'[{Difficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' - - -@dataclass -class AnalysisDTO: - time_complexity: str - difficulty: str - tags: List[str] - hint: List[str] diff --git a/app/problem/models/difficulty.py b/app/problem/models/difficulty.py deleted file mode 100644 index e566119..0000000 --- a/app/problem/models/difficulty.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models - - -class Difficulty(models.IntegerChoices): - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' diff --git a/app/problem/models/language.py b/app/problem/models/language.py deleted file mode 100644 index 4458f53..0000000 --- a/app/problem/models/language.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import models - - -class Language(models.Model): - key = models.CharField( - max_length=20, - unique=True, - help_text=( - '언어 키를 입력해주세요. (최대 20자)' - ), - ) - name = models.CharField( - max_length=20, - unique=True, - help_text=( - '언어 이름을 입력해주세요. (최대 20자)' - ), - ) - extension = models.CharField( - max_length=20, - help_text=( - '언어 확장자를 입력해주세요. (최대 20자)' - ), - ) - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/app/problem/models/problem.py b/app/problem/models/problem.py deleted file mode 100644 index 0ddc8f0..0000000 --- a/app/problem/models/problem.py +++ /dev/null @@ -1,78 +0,0 @@ -from dataclasses import dataclass -from typing import List - -from django.db import models - -from account.models import User - - -class Problem(models.Model): - user = models.ForeignKey( - User, - on_delete=models.SET_NULL, - related_name='problems', - help_text=( - '이 문제를 추가한 사용자를 입력해주세요.' - ), - null=True, - ) - title = models.CharField( - max_length=100, - help_text=( - '문제 이름을 입력해주세요.' - ), - blank=False, - ) - link = models.URLField( - help_text=( - '문제 링크를 입력해주세요. (선택)' - ), - blank=True, - ) - description = models.TextField( - help_text=( - '문제 설명을 입력해주세요.' - ), - blank=False, - ) - input_description = models.TextField( - help_text=( - '문제 입력 설명을 입력해주세요.' - ), - blank=True, - ) - output_description = models.TextField( - help_text=( - '문제 출력 설명을 입력해주세요.' - ), - blank=True, - ) - memory_limit = models.FloatField( - help_text=( - '문제 메모리 제한을 입력해주세요. (MB 단위)' - ), - ) - time_limit = models.FloatField( - help_text=( - '문제 시간 제한을 입력해주세요. (초 단위)' - ), - default=1.0, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - def __repr__(self) -> str: - return f'[{self.title}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ← {self.user.__repr__()}' - - -@dataclass -class ProblemDTO: - title: str - description: str - input_description: str - output_description: str - memory_limit: float - time_limit: float diff --git a/app/problem/models/tag.py b/app/problem/models/tag.py deleted file mode 100644 index 4e29c4c..0000000 --- a/app/problem/models/tag.py +++ /dev/null @@ -1,45 +0,0 @@ -from django.db import models - - -class Tag(models.Model): - """Data Structure & Algorithm""" - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - related_name='children', - help_text=( - '부모 알고리즘 태그를 입력해주세요.' - ), - null=True, - blank=True, - ) - key = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 키를 입력해주세요. (최대 20자)' - ), - ) - name_ko = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' - ), - ) - name_en = models.CharField( - max_length=50, - unique=True, - help_text=( - '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' - ), - ) - - class Meta: - ordering = ['key'] - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name_ko})' diff --git a/app/problem/permissions.py b/app/problem/permissions.py deleted file mode 100644 index 6516a1c..0000000 --- a/app/problem/permissions.py +++ /dev/null @@ -1,38 +0,0 @@ -from rest_framework.permissions import ( - BasePermission, - IsAdminUser, - IsAuthenticated, - SAFE_METHODS, -) - -from .models import Problem - - -__all__ = ( - 'IsReadOnly', - 'IsCreateOnly', - 'IsAdminUser', - 'IsAuthenticated', - 'IsProblemCreator', -) - - -class IsReadOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method in SAFE_METHODS, - ) - - -class IsCreateOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method == 'POST', - ) - - -class IsProblemCreator(IsAuthenticated): - def has_object_permission(self, request, view, obj: Problem) -> bool: - return super().has_object_permission(request, view, obj) and bool( - obj.user == request.user - ) diff --git a/app/problem/serializers/__init__.py b/app/problem/serializers/__init__.py deleted file mode 100644 index 611edfb..0000000 --- a/app/problem/serializers/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .problem import ProblemSerializer -from .language import LanguageSerializer -from .tag import TagSerializer -from .analysis import AnalysisSerializer diff --git a/app/problem/serializers/analysis.py b/app/problem/serializers/analysis.py deleted file mode 100644 index 1057b1b..0000000 --- a/app/problem/serializers/analysis.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework.serializers import ModelSerializer - -from ..models import Analysis -from ..serializers import TagSerializer - - -class AnalysisSerializer(ModelSerializer): - tags = TagSerializer(many=True) - - class Meta: - model = Analysis - fields = [ - 'id', - 'problem', - 'difficulty', - 'tags', - 'time_complexity', - 'created_at', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'problem': {'read_only': True}, - 'created_at': {'read_only': True}, - } diff --git a/app/problem/serializers/language.py b/app/problem/serializers/language.py deleted file mode 100644 index 9d8c3d9..0000000 --- a/app/problem/serializers/language.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.serializers import ModelSerializer - -from ..models import Language - - -class LanguageSerializer(ModelSerializer): - class Meta: - model = Language - fields = '__all__' diff --git a/app/problem/serializers/problem.py b/app/problem/serializers/problem.py deleted file mode 100644 index 3654905..0000000 --- a/app/problem/serializers/problem.py +++ /dev/null @@ -1,27 +0,0 @@ -from rest_framework.serializers import ModelSerializer - -from account.serializers import UserSerializer - -from ..models import Problem - - -class ProblemSerializer(ModelSerializer): - user = UserSerializer(read_only=True) - - class Meta: - model = Problem - fields = [ - 'id', - 'title', - 'link', - 'description', - 'input_description', - 'output_description', - 'memory_limit', - 'time_limit', - 'user', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'user': {'read_only': True}, - } diff --git a/app/problem/serializers/tag.py b/app/problem/serializers/tag.py deleted file mode 100644 index ebf05ec..0000000 --- a/app/problem/serializers/tag.py +++ /dev/null @@ -1,9 +0,0 @@ -from rest_framework.serializers import ModelSerializer - -from ..models import Tag - - -class TagSerializer(ModelSerializer): - class Meta: - model = Tag - fields = '__all__' diff --git a/app/problem/services.py b/app/problem/services.py deleted file mode 100644 index 0d7043c..0000000 --- a/app/problem/services.py +++ /dev/null @@ -1,22 +0,0 @@ -from __future__ import annotations - -from .models import ProblemDTO, AnalysisDTO - - -def get_analyser() -> ProblemAnalyser: - return GPTProblemAnalyser() - - -class ProblemAnalyser: - def analyze(self, problem: ProblemDTO) -> AnalysisDTO: - raise NotImplementedError - - -class GPTProblemAnalyser(ProblemAnalyser): - def analyze(self, problem: ProblemDTO) -> AnalysisDTO: - raise NotImplementedError - - -class GeminiProblemAnalyser(ProblemAnalyser): - def analyze(self, problem: ProblemDTO) -> AnalysisDTO: - raise NotImplementedError diff --git a/app/problem/signals.py b/app/problem/signals.py deleted file mode 100644 index b768ebd..0000000 --- a/app/problem/signals.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.db import transaction -from django.db.models import signals -from django.dispatch import receiver - -from .models import ( - Problem, - Analysis, -) -from .services import get_analyser - - -@receiver(signals.post_save, sender=Problem) -def problem_on_post_save(sender, instance: Problem, created: bool, **kwargs): - if not created: - return - analyser = get_analyser() - analysis_dto = analyser.analyze(instance) - with transaction.atomic(): - analysis = Analysis.objects.create( - problem=instance, - difficulty=analysis_dto.difficulty, - tags=analysis_dto.tags, - time_complexity=analysis_dto.time_complexity, - hint=analysis_dto.hint, - ) - analysis.save() - instance.analysis = analysis - instance.save() diff --git a/app/problem/urls.py b/app/problem/urls.py deleted file mode 100644 index f9d6f5f..0000000 --- a/app/problem/urls.py +++ /dev/null @@ -1,46 +0,0 @@ -from django.urls import include, path - -from .views import * - - -urlpatterns = [ - path("", ProblemViewSet.as_view({ - "get": "list", - "post": "create", - })), - path("my/", ProblemViewSet.as_view({ - "get": "my_list", - })), - path("/", include([ - path("", ProblemViewSet.as_view({ - "get": "retrieve", - "put": "update", - "delete": "destroy", - })), - path("analysis", AnalysisViewSet.as_view({ - "get": "retrieve", - })), - ])), - path("language/", include([ - path("", LanguageViewSet.as_view({ - "get": "list", - "post": "create", - })), - path("/", LanguageViewSet.as_view({ - "get": "retrieve", - "put": "update", - "delete": "destroy", - })), - ])), - path("tag/", include([ - path("", TagViewSet.as_view({ - "get": "list", - "post": "create", - })), - path("/", TagViewSet.as_view({ - "get": "retrieve", - "put": "update", - "delete": "destroy", - })), - ])), -] diff --git a/app/problem/views.py b/app/problem/views.py deleted file mode 100644 index cd8348b..0000000 --- a/app/problem/views.py +++ /dev/null @@ -1,59 +0,0 @@ -from rest_framework import viewsets -from rest_framework.request import Request -from rest_framework.response import Response - -from .models import * -from .serializers import * -from .permissions import * - - -__all__ = ( - 'TagViewSet', - 'LanguageViewSet', - 'ProblemViewSet', - 'AnalysisViewSet', -) - - -class TagViewSet(viewsets.ModelViewSet): - """문제 태그 목록 조회 + 생성 기능""" - queryset = Tag.objects.all() - serializer_class = TagSerializer - permission_classes = [IsAdminUser | IsReadOnly] - - -class LanguageViewSet(viewsets.ModelViewSet): - """프로그래밍 언어 목록 조회 + 생성 기능""" - queryset = Language.objects.all() - serializer_class = LanguageSerializer - permission_classes = [IsAdminUser | IsReadOnly] - - -class ProblemViewSet(viewsets.ModelViewSet): - """문제 목록 조회 + 생성 기능 - - - 관리자는 전체 문제 목록을 조회할 수 있습니다. - - 관리자가 아닌 일반 사용자는 자신이 만든 문제만 조회할 수 있습니다. - """ - serializer_class = ProblemSerializer - permission_classes = [IsAdminUser | IsProblemCreator | IsReadOnly | (IsAuthenticated & IsCreateOnly)] - - def get_queryset(self): - if self.request.user.is_staff: - return Problem.objects.all() - return Problem.objects.filter(user=self.request.user) - - def my_list(self, request: Request): - """내가 만든 문제 목록 조회""" - queryset = Problem.objects.filter(user=request.user) - serializer = self.get_serializer(queryset, many=True) - return Response(serializer.data) - - -class AnalysisViewSet(viewsets.ModelViewSet): - """문제 태그 목록 조회 + 생성 기능""" - queryset = Analysis.objects.all() - serializer_class = AnalysisSerializer - permission_classes = [IsAdminUser | IsReadOnly] - lookup_field = 'problem__id' - lookup_url_kwarg = 'pk' From f993f4a78521771fbe645347e099c5e3a6efc783 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 02:16:17 +0900 Subject: [PATCH 199/552] feat(tle.models): create models --- app/tle/models/__init__.py | 38 ++++++++ app/tle/models/crew.py | 111 ++++++++++++++++++++++++ app/tle/models/crew_activity.py | 27 ++++++ app/tle/models/crew_activity_problem.py | 52 +++++++++++ app/tle/models/crew_applicant.py | 89 +++++++++++++++++++ app/tle/models/crew_member.py | 70 +++++++++++++++ app/tle/models/problem.py | 51 +++++++++++ app/tle/models/problem_analysis.py | 57 ++++++++++++ app/tle/models/problem_difficulty.py | 7 ++ app/tle/models/problem_tag.py | 45 ++++++++++ app/tle/models/submission.py | 47 ++++++++++ app/tle/models/submission_comment.py | 54 ++++++++++++ app/tle/models/submission_language.py | 30 +++++++ app/tle/models/user.py | 65 ++++++++++++++ app/tle/models/user_solved_tier.py | 35 ++++++++ 15 files changed, 778 insertions(+) create mode 100644 app/tle/models/__init__.py create mode 100644 app/tle/models/crew.py create mode 100644 app/tle/models/crew_activity.py create mode 100644 app/tle/models/crew_activity_problem.py create mode 100644 app/tle/models/crew_applicant.py create mode 100644 app/tle/models/crew_member.py create mode 100644 app/tle/models/problem.py create mode 100644 app/tle/models/problem_analysis.py create mode 100644 app/tle/models/problem_difficulty.py create mode 100644 app/tle/models/problem_tag.py create mode 100644 app/tle/models/submission.py create mode 100644 app/tle/models/submission_comment.py create mode 100644 app/tle/models/submission_language.py create mode 100644 app/tle/models/user.py create mode 100644 app/tle/models/user_solved_tier.py diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py new file mode 100644 index 0000000..a261ca6 --- /dev/null +++ b/app/tle/models/__init__.py @@ -0,0 +1,38 @@ +from .user import User +from .user_solved_tier import UserSolvedTier + +from .crew import Crew +from .crew_activity import CrewActivity +from .crew_activity_problem import CrewActivityProblem +from .crew_applicant import CrewApplicant +from .crew_member import CrewMember + +from .problem import Problem +from .problem_analysis import ProblemAnalysis +from .problem_difficulty import ProblemDifficulty +from .problem_tag import ProblemTag + +from .submission import Submission +from .submission_comment import SubmissionComment +from .submission_language import SubmissionLanguage + + +__all__ = ( + 'User', + 'UserSolvedTier', + + 'Crew', + 'CrewActivity', + 'CrewActivityProblem', + 'CrewApplicant', + 'CrewMember', + + 'Problem', + 'ProblemAnalysis', + 'ProblemDifficulty', + 'ProblemTag', + + 'Submission', + 'SubmissionComment', + 'SubmissionLanguage', +) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py new file mode 100644 index 0000000..68babcc --- /dev/null +++ b/app/tle/models/crew.py @@ -0,0 +1,111 @@ +from __future__ import annotations +import typing + +from django.core.validators import ( + MinValueValidator, + MaxValueValidator, +) +from django.db import models + +from tle.models.user_solved_tier import UserSolvedTier +from tle.models.submission_language import SubmissionLanguage + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text='크루 이름을 입력해주세요. (최대 20자)', + ) + emoji = models.CharField( + max_length=2, + validators=[ + # TODO: 이모지 형식 검사 + ], + null=True, + blank=True, + help_text='크루 아이콘을 입력해주세요. (이모지)', + ) + max_members = models.IntegerField( + help_text='크루 최대 인원을 입력해주세요.', + validators=[ + MinValueValidator(1), + MaxValueValidator(8), + ], + default=8, + blank=False, + null=False, + ) + notice = models.TextField( + help_text='크루 공지를 입력해주세요.', + null=True, + blank=True, + max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 + ) + is_boj_user_only = models.BooleanField( + help_text='백준 아이디 필요 여부를 입력해주세요.', + default=False, + ) + min_boj_tier = models.IntegerField( + help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', + choices=UserSolvedTier.choices, + blank=True, + null=True, + default=None, + ) + max_boj_tier = models.IntegerField( + help_text='최대 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', + validators=[ + # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 + ], + choices=UserSolvedTier.choices, + blank=True, + null=True, + default=None, + ) + allowed_languages = models.ManyToManyField( + SubmissionLanguage, + related_name='crews', + help_text='유저가 사용 가능한 언어를 입력해주세요.', + ) + custom_tags = models.JSONField( + help_text='태그를 입력해주세요.', + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + is_recruiting = models.BooleanField( + help_text='모집 중 여부를 입력해주세요.', + default=True, + ) + is_active = models.BooleanField( + help_text='활동 중인지 여부를 입력해주세요.', + default=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + @property + def captain(self) -> T_CrewMember: + return self.members.get(is_captain=True) + + class FieldName: + APPLICANTS = 'applicants' + MEMBERS = 'members' + + if typing.TYPE_CHECKING: + from . import ( + CrewApplicant as T_CrewApplicant, + CrewMember as T_CrewMember, + ) + applicants: models.QuerySet[T_CrewApplicant] + members: models.QuerySet[T_CrewMember] + + def __repr__(self) -> str: + return f'[{self.emoji} {self.name}]' + + def __str__(self) -> str: + member_count = f'({self.members.count()}/{self.max_member})' + return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py new file mode 100644 index 0000000..b81aded --- /dev/null +++ b/app/tle/models/crew_activity.py @@ -0,0 +1,27 @@ +from django.db import models + +from tle.models.crew import Crew + + +class CrewActivity(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name='activities', + help_text='크루를 입력해주세요.', + ) + name = models.TextField( + help_text='활동 이름을 입력해주세요. (예: "1회차")', + ) + start_at = models.DateTimeField( + help_text='활동 시작 일자를 입력해주세요.', + ) + end_at = models.DateTimeField( + help_text='활동 종료 일자를 입력해주세요.', + ) + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/models/crew_activity_problem.py b/app/tle/models/crew_activity_problem.py new file mode 100644 index 0000000..c095fa8 --- /dev/null +++ b/app/tle/models/crew_activity_problem.py @@ -0,0 +1,52 @@ +import typing + +from django.core.validators import MinValueValidator +from django.db import models + +from tle.models.crew_activity import CrewActivity +from tle.models.problem import Problem + + +class CrewActivityProblem(models.Model): + activity = models.ForeignKey( + CrewActivity, + on_delete=models.CASCADE, + related_name='problems', + help_text='활동을 입력해주세요.', + ) + problem = models.ForeignKey( + Problem, + on_delete=models.PROTECT, + related_name='activities', + help_text='문제를 입력해주세요.', + ) + order = models.IntegerField( + help_text='문제 순서를 입력해주세요.', + validators=[ + MinValueValidator(1), + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['activity', 'order'], + name='unique_order_per_activity_problem', + ), + ] + + class FieldName: + SUBMISSIONS = 'submissions' + + if typing.TYPE_CHECKING: + from tle.models.submission import ( + Submission as T_Submission, + ) + submissions: models.QuerySet[T_Submission] + + def __repr__(self) -> str: + return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/models/crew_applicant.py b/app/tle/models/crew_applicant.py new file mode 100644 index 0000000..7431056 --- /dev/null +++ b/app/tle/models/crew_applicant.py @@ -0,0 +1,89 @@ +from django.db import ( + models, + transaction, +) +from django.utils import timezone + +from tle.models.user import User +from tle.models.crew import Crew +from tle.models.crew_member import CrewMember + + +class CrewApplicant(models.Model): + crew = models.ForeignKey[Crew]( + Crew, + on_delete=models.CASCADE, + related_name=Crew.FieldName.APPLICANTS, + help_text='크루를 입력해주세요.', + ) + user = models.ForeignKey[User]( + User, + on_delete=models.CASCADE, + related_name=User.FieldName.APPLICANTS, + help_text='유저를 입력해주세요.', + ) + message = models.TextField( + help_text='가입 메시지를 입력해주세요.', + null=True, + blank=True, + ) + is_accepted = models.BooleanField( + default=False, + help_text='수락 여부를 입력해주세요.', + ) + created_at = models.DateTimeField(auto_now_add=True) + reviewed_at = models.DateTimeField( + help_text='리뷰한 시간을 입력해주세요.', + null=True, + blank=True, + default=None, + ) + reviewed_by = models.ForeignKey[User]( + User, + on_delete=models.SET_NULL, + null=True, + blank=True, + default=None, + ) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['crew', 'user'], + name='unique_applicant_per_crew', + ), + ] + + def __repr__(self) -> str: + return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + + def save(self, *args, **kwargs) -> None: + # 같은 크루에 여러 번 가입하는 것을 방지 + if self.crew.members.filter(user=self.user).exists(): + raise ValueError('이미 가입한 크루에 가입 신청을 할 수 없습니다.') + + return super().save(*args, **kwargs) + + def accept(self, commit=True) -> CrewMember: + """크루 가입 신청을 수락합니다.""" + member = CrewMember( + crew=self.crew, + user=self.user, + ) + self.is_accepted = True + self.reviewed_at = timezone.now() + if commit: + with transaction.atomic(): + member.save() + self.save() + return member + + def reject(self, commit=True): + """크루 가입 신청을 거절합니다.""" + self.is_accepted = False + self.reviewed_at = timezone.now() + if commit: + self.save() diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py new file mode 100644 index 0000000..5e5c213 --- /dev/null +++ b/app/tle/models/crew_member.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +from django.db import ( + models, + transaction, +) + +from tle.models.user import User +from tle.models.crew import Crew + + +class CrewMember(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + related_name=Crew.FieldName.MEMBERS, + help_text='크루를 입력해주세요.', + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name=User.FieldName.MEMBERS, + help_text='유저를 입력해주세요.', + ) + is_captain = models.BooleanField( + default=False, + help_text='선장인지 여부를 입력해주세요.', + ) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['crew', 'is_captain'], + name='unique_captain_per_crew' + ), + models.UniqueConstraint( + fields=['crew', 'user'], + name='unique_member_per_crew' + ), + ] + + def __repr__(self) -> str: + return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + + def make_captain(self, commit=True) -> CrewMember: + """전 선장을 직위해제하고, 이 멤버를 새로운 선장으로 임명합니다. + + 전 선장에 대한 엔티티를 반환합니다. + + TODO: 크루장이 탈퇴할 경우 새로운 크루장은 어떻게 선발할 지 검토 + TODO: 크루장이 여러 명일 경우 어떻게 처리할 지 검토 (예외 처리) + """ + def inner(): + former_captain = self.crew.members.get(is_captain=True) + former_captain.is_captain = False + self.is_captain = True + return former_captain + + if not commit: + return inner() + + with transaction.atomic(): + former_captain = inner() + former_captain.save() + self.save() + return former_captain diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py new file mode 100644 index 0000000..d66e5b0 --- /dev/null +++ b/app/tle/models/problem.py @@ -0,0 +1,51 @@ +import typing + +from django.db import models + +from tle.models.user import User + + +class Problem(models.Model): + title = models.CharField( + max_length=100, + help_text='문제 이름을 입력해주세요.', + blank=False, + ) + link = models.URLField( + help_text='문제 링크를 입력해주세요. (선택)', + blank=True, + ) + description = models.TextField( + help_text='문제 설명을 입력해주세요.', + blank=False, + ) + input_description = models.TextField( + help_text='문제 입력 설명을 입력해주세요.', + blank=True, + ) + output_description = models.TextField( + help_text='문제 출력 설명을 입력해주세요.', + blank=True, + ) + memory_limit = models.FloatField( + help_text='문제 메모리 제한을 입력해주세요. (MB 단위)', + ) + time_limit = models.FloatField( + help_text='문제 시간 제한을 입력해주세요. (초 단위)', + default=1.0, + ) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + User, + on_delete=models.SET_NULL, + related_name=User.FieldName.PROBLEMS, + help_text='이 문제를 추가한 사용자를 입력해주세요.', + null=True, + ) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + return f'[{self.title}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ← {self.created_by.__repr__()}' diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py new file mode 100644 index 0000000..a07e381 --- /dev/null +++ b/app/tle/models/problem_analysis.py @@ -0,0 +1,57 @@ +from django.db import models + +from tle.models.problem import Problem +from tle.models.problem_tag import ProblemTag +from tle.models.problem_difficulty import ProblemDifficulty + + +class ProblemAnalysis(models.Model): + problem = models.OneToOneField( + Problem, + on_delete=models.CASCADE, + related_name='analysis', + help_text=( + '문제를 입력해주세요.' + ), + ) + difficulty = models.IntegerField( + help_text=( + '문제 난이도를 입력해주세요.' + ), + choices=ProblemDifficulty.choices, + ) + tags = models.ManyToManyField( + ProblemTag, + related_name='problems', + help_text=( + '문제의 DSA 태그를 입력해주세요.' + ), + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + hint = models.JSONField( + help_text=( + '문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.' + ), + validators=[ + # TODO: 힌트 검증 로직 추가 + ], + blank=False, + default=list, + ) + created_at = models.DateTimeField(auto_now_add=True) + + def __repr__(self) -> str: + tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) + return f'[{ProblemDifficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' diff --git a/app/tle/models/problem_difficulty.py b/app/tle/models/problem_difficulty.py new file mode 100644 index 0000000..9c1b441 --- /dev/null +++ b/app/tle/models/problem_difficulty.py @@ -0,0 +1,7 @@ +from django.db import models + + +class ProblemDifficulty(models.IntegerChoices): + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' diff --git a/app/tle/models/problem_tag.py b/app/tle/models/problem_tag.py new file mode 100644 index 0000000..337fd9b --- /dev/null +++ b/app/tle/models/problem_tag.py @@ -0,0 +1,45 @@ +from django.db import models + + +class ProblemTag(models.Model): + """Data Structure & Algorithm""" + parent = models.ForeignKey( + 'self', + on_delete=models.CASCADE, + related_name='children', + help_text=( + '부모 알고리즘 태그를 입력해주세요.' + ), + null=True, + blank=True, + ) + key = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 키를 입력해주세요. (최대 20자)' + ), + ) + name_ko = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' + ), + ) + name_en = models.CharField( + max_length=50, + unique=True, + help_text=( + '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' + ), + ) + + class Meta: + ordering = ['key'] + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name_ko})' diff --git a/app/tle/models/submission.py b/app/tle/models/submission.py new file mode 100644 index 0000000..fdef98e --- /dev/null +++ b/app/tle/models/submission.py @@ -0,0 +1,47 @@ +from django.db import models + +from tle.models.user import User +from tle.models.crew_activity_problem import CrewActivityProblem +from tle.models.submission_language import SubmissionLanguage + + +class Submission(models.Model): + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 + activity_problem = models.ForeignKey( + CrewActivityProblem, + on_delete=models.PROTECT, + related_name=CrewActivityProblem.FieldName.SUBMISSIONS, + help_text='활동 문제를 입력해주세요.', + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name=User.FieldName.SUBMISSIONS, + help_text='유저를 입력해주세요.', + ) + code = models.TextField( + help_text='유저의 코드를 입력해주세요.', + ) + language = models.ForeignKey( + SubmissionLanguage, + on_delete=models.PROTECT, + help_text='유저의 코드 언어를 입력해주세요.', + ) + is_correct = models.BooleanField( + help_text='유저의 코드가 정답인지 여부를 입력해주세요.', + ) + is_help_needed = models.BooleanField( + help_text='유저의 코드에 도움이 필요한지 여부를 입력해주세요.', + default=False, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class FieldName: + COMMENTS = 'comments' + + def __repr__(self) -> str: + return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/models/submission_comment.py b/app/tle/models/submission_comment.py new file mode 100644 index 0000000..3a7c8ab --- /dev/null +++ b/app/tle/models/submission_comment.py @@ -0,0 +1,54 @@ +from django.core.validators import MinValueValidator +from django.db import models + +from tle.models.user import User +from tle.models.submission import Submission + + +class SubmissionComment(models.Model): + submission = models.ForeignKey( + Submission, + on_delete=models.CASCADE, + related_name=Submission.FieldName.COMMENTS, + help_text='제출을 입력해주세요.', + ) + content = models.TextField( + help_text=( + '댓글을 입력해주세요.' + ), + ) + line_number_start = models.IntegerField( + help_text=( + '댓글 시작 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + ], + ) + line_number_end = models.IntegerField( + help_text=( + '댓글 종료 라인을 입력해주세요.' + ), + validators=[ + MinValueValidator(1), + # TODO: 시작 라인보다 작지 않도록 검사 + # TODO: 코드 라인 수보다 크지 않도록 검사 + ], + ) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + User, + on_delete=models.CASCADE, + related_name='comments', + help_text=( + '유저를 입력해주세요.' + ), + ) + updated_at = models.DateTimeField(auto_now=True) + + def __repr__(self) -> str: + line_range = f'L{self.line_number_start}:L{self.line_number_end}' + return f'{self.submission.__repr__()} ← {self.created_by.__repr__()} {line_range} "{self.content}"' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/models/submission_language.py b/app/tle/models/submission_language.py new file mode 100644 index 0000000..30c3a6a --- /dev/null +++ b/app/tle/models/submission_language.py @@ -0,0 +1,30 @@ +from django.db import models + + +class SubmissionLanguage(models.Model): + key = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 키를 입력해주세요. (최대 20자)' + ), + ) + name = models.CharField( + max_length=20, + unique=True, + help_text=( + '언어 이름을 입력해주세요. (최대 20자)' + ), + ) + extension = models.CharField( + max_length=20, + help_text=( + '언어 확장자를 입력해주세요. (최대 20자)' + ), + ) + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/app/tle/models/user.py b/app/tle/models/user.py new file mode 100644 index 0000000..07d347e --- /dev/null +++ b/app/tle/models/user.py @@ -0,0 +1,65 @@ +import typing + +from django.contrib.auth.models import User as BaseUser +from django.db import models + + +class User(BaseUser): + image = models.ImageField( + help_text='프로필 이미지', + upload_to='user_images/', + null=True, + blank=True, + validators=[ + # TODO: 이미지 크기 제한 + # TODO: 이미지 확장자 제한 + ] + ) + boj_id = models.CharField( + help_text='백준 아이디', + max_length=40, + null=True, + blank=True, + ) + + @property + def crews(self): + for member in self.members: + yield member.crew + + REQUIRED_FIELDS = [ + 'email', + 'username', + 'password', + ] + + class FieldName: + PROBLEMS = 'problems' + APPLICANTS = 'applicants' + MEMBERS = 'members' + SUBMISSIONS = 'submissions' + + if typing.TYPE_CHECKING: + from . import ( + Problem as T_Problem, + Crew as T_Crew, + CrewApplicant as T_CrewApplicant, + CrewMember as T_CrewMember, + Submission as T_Submission, + ) + problems: models.QuerySet[T_Problem] + applicants: models.QuerySet[T_CrewApplicant] + members: models.QuerySet[T_CrewMember] + submissions: models.QuerySet[T_Submission] + + def __repr__(self) -> str: + return f'[@{self.username}]' + + def __str__(self) -> str: + staff = '(관리자)' if self.is_staff else '' + return f'{self.pk} : {self.__repr__()} {staff}' + + +User._meta.get_field('email')._unique = True +User._meta.get_field('email').blank = False +User._meta.get_field('email').null = False diff --git a/app/tle/models/user_solved_tier.py b/app/tle/models/user_solved_tier.py new file mode 100644 index 0000000..9f9decf --- /dev/null +++ b/app/tle/models/user_solved_tier.py @@ -0,0 +1,35 @@ +from django.db import models + + +class UserSolvedTier(models.IntegerChoices): + U = 0, 'Unrated' + B5 = 1, '브론즈 5' + B4 = 2, '브론즈 4' + B3 = 3, '브론즈 3' + B2 = 4, '브론즈 2' + B1 = 5, '브론즈 1' + S5 = 6, '실버 5' + S4 = 7, '실버 4' + S3 = 8, '실버 3' + S2 = 9, '실버 2' + S1 = 10, '실버 1' + G5 = 11, '골드 5' + G4 = 12, '골드 4' + G3 = 13, '골드 3' + G2 = 14, '골드 2' + G1 = 15, '골드 1' + P5 = 16, '플래티넘 5' + P4 = 17, '플래티넘 4' + P3 = 18, '플래티넘 3' + P2 = 19, '플래티넘 2' + P1 = 20, '플래티넘 1' + D5 = 21, '다이아몬드 5' + D4 = 22, '다이아몬드 4' + D3 = 23, '다이아몬드 3' + D2 = 24, '다이아몬드 2' + D1 = 25, '다이아몬드 1' + R5 = 26, '루비 5' + R4 = 27, '루비 4' + R3 = 28, '루비 3' + R2 = 29, '루비 2' + R1 = 30, '루비 1' From abeeb026d18166d68874f10b1567ffd6d4307c5d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 02:16:45 +0900 Subject: [PATCH 200/552] feat(tle.services): create abstract of problem analyser --- app/tle/services/__init__.py | 0 app/tle/services/problem_analyser/__init__.py | 9 +++++++++ app/tle/services/problem_analyser/dto.py | 20 +++++++++++++++++++ .../services/problem_analyser/llm/__init__.py | 0 .../services/problem_analyser/llm/gemini.py | 6 ++++++ app/tle/services/problem_analyser/llm/gpt.py | 6 ++++++ .../problem_analyser/problem_analyser.py | 11 ++++++++++ 7 files changed, 52 insertions(+) create mode 100644 app/tle/services/__init__.py create mode 100644 app/tle/services/problem_analyser/__init__.py create mode 100644 app/tle/services/problem_analyser/dto.py create mode 100644 app/tle/services/problem_analyser/llm/__init__.py create mode 100644 app/tle/services/problem_analyser/llm/gemini.py create mode 100644 app/tle/services/problem_analyser/llm/gpt.py create mode 100644 app/tle/services/problem_analyser/problem_analyser.py diff --git a/app/tle/services/__init__.py b/app/tle/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/services/problem_analyser/__init__.py b/app/tle/services/problem_analyser/__init__.py new file mode 100644 index 0000000..ac1fa7d --- /dev/null +++ b/app/tle/services/problem_analyser/__init__.py @@ -0,0 +1,9 @@ +from .problem_analyser import ProblemAnalyser +from .dto import ProblemDTO, ProblemAnalysisDTO + + +__all__ = ( + 'ProblemAnalyser', + 'ProblemDTO', + 'ProblemAnalysisDTO', +) diff --git a/app/tle/services/problem_analyser/dto.py b/app/tle/services/problem_analyser/dto.py new file mode 100644 index 0000000..27333d6 --- /dev/null +++ b/app/tle/services/problem_analyser/dto.py @@ -0,0 +1,20 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class ProblemDTO: + title: str + description: str + input_description: str + output_description: str + memory_limit: float + time_limit: float + + +@dataclass +class ProblemAnalysisDTO: + time_complexity: str + difficulty: str + tags: List[str] + hint: List[str] diff --git a/app/tle/services/problem_analyser/llm/__init__.py b/app/tle/services/problem_analyser/llm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/services/problem_analyser/llm/gemini.py b/app/tle/services/problem_analyser/llm/gemini.py new file mode 100644 index 0000000..b685e66 --- /dev/null +++ b/app/tle/services/problem_analyser/llm/gemini.py @@ -0,0 +1,6 @@ +from tle.services.problem_analyser import * + + +class GeminiProblemAnalyser(ProblemAnalyser): + def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: + raise NotImplementedError diff --git a/app/tle/services/problem_analyser/llm/gpt.py b/app/tle/services/problem_analyser/llm/gpt.py new file mode 100644 index 0000000..13d239a --- /dev/null +++ b/app/tle/services/problem_analyser/llm/gpt.py @@ -0,0 +1,6 @@ +from tle.services.problem_analyser import * + + +class GPTProblemAnalyser(ProblemAnalyser): + def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: + raise NotImplementedError diff --git a/app/tle/services/problem_analyser/problem_analyser.py b/app/tle/services/problem_analyser/problem_analyser.py new file mode 100644 index 0000000..6068ef9 --- /dev/null +++ b/app/tle/services/problem_analyser/problem_analyser.py @@ -0,0 +1,11 @@ +from tle.services.dto import * + + +class ProblemAnalyser: + """문제를 분석하는 클래스의 추상 클래스입니다. + + 문제 데이터를 받아와 문제의 분석 결과를 반환하는 analyze() 메소드를 구현해야 합니다. + """ + + def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: + raise NotImplementedError From 695e5b0eb175a7c60330e03f45c51dbf350e004d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 02:36:39 +0900 Subject: [PATCH 201/552] feat(tle.admin): register all models to admin --- app/tle/admin.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/tle/admin.py b/app/tle/admin.py index 8c38f3f..e158dd8 100644 --- a/app/tle/admin.py +++ b/app/tle/admin.py @@ -1,3 +1,24 @@ from django.contrib import admin -# Register your models here. +from tle.models import * + + +@admin.register( + User, + + Crew, + CrewActivity, + CrewActivityProblem, + CrewApplicant, + CrewMember, + + Problem, + ProblemAnalysis, + ProblemTag, + + Submission, + SubmissionComment, + SubmissionLanguage, +) +class SuperAdmin(admin.ModelAdmin): + pass From f54ad7d632bb89f4cce9df9f57db61a86afb2c90 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 03:21:30 +0900 Subject: [PATCH 202/552] refactor(app.settings): fix parent path --- app/app/settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/app/settings.py b/app/app/settings.py index abc5a38..ac3af4b 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -18,7 +18,7 @@ # Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent.parent +BASE_DIR = Path(__file__).resolve().parent.parent # Quick-start development settings - unsuitable for production From d70f6472d5387e9f386c69ad11a549587eeb4caa Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 16:14:17 +0900 Subject: [PATCH 203/552] =?UTF-8?q?feat(tle.models):=20`tle.User`=EC=9D=84?= =?UTF-8?q?=20=EC=BB=A4=EC=8A=A4=ED=85=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EB=A1=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 2 + app/tle/admin.py | 22 +++++-- app/tle/models/__init__.py | 34 +++++----- app/tle/models/problem.py | 2 - app/tle/models/submission_comment.py | 21 +++--- app/tle/models/user.py | 95 ++++++++++++++++++++++++---- 6 files changed, 124 insertions(+), 52 deletions(-) diff --git a/app/app/settings.py b/app/app/settings.py index ac3af4b..65c51e0 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -110,6 +110,8 @@ }, ] +AUTH_USER_MODEL = 'tle.User' + # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/app/tle/admin.py b/app/tle/admin.py index e158dd8..9b97433 100644 --- a/app/tle/admin.py +++ b/app/tle/admin.py @@ -1,24 +1,32 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from tle.models import * -@admin.register( - User, +@admin.register(User) +class UserAdmin(BaseUserAdmin): + fieldsets = [ + *BaseUserAdmin.fieldsets, + (None, {'fields': [ + 'profile_image', + 'boj_username', + 'boj_tier', + 'boj_tier_updated_at', + ]}), + ] + +admin.site.register([ Crew, CrewActivity, CrewActivityProblem, CrewApplicant, CrewMember, - Problem, ProblemAnalysis, ProblemTag, - Submission, SubmissionComment, SubmissionLanguage, -) -class SuperAdmin(admin.ModelAdmin): - pass +]) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index a261ca6..3e3e6a7 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,20 +1,20 @@ -from .user import User -from .user_solved_tier import UserSolvedTier - -from .crew import Crew -from .crew_activity import CrewActivity -from .crew_activity_problem import CrewActivityProblem -from .crew_applicant import CrewApplicant -from .crew_member import CrewMember - -from .problem import Problem -from .problem_analysis import ProblemAnalysis -from .problem_difficulty import ProblemDifficulty -from .problem_tag import ProblemTag - -from .submission import Submission -from .submission_comment import SubmissionComment -from .submission_language import SubmissionLanguage +from tle.models.user import User +from tle.models.user_solved_tier import UserSolvedTier + +from tle.models.crew import Crew +from tle.models.crew_activity import CrewActivity +from tle.models.crew_activity_problem import CrewActivityProblem +from tle.models.crew_applicant import CrewApplicant +from tle.models.crew_member import CrewMember + +from tle.models.problem import Problem +from tle.models.problem_analysis import ProblemAnalysis +from tle.models.problem_difficulty import ProblemDifficulty +from tle.models.problem_tag import ProblemTag + +from tle.models.submission import Submission +from tle.models.submission_comment import SubmissionComment +from tle.models.submission_language import SubmissionLanguage __all__ = ( diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index d66e5b0..c02add0 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -1,5 +1,3 @@ -import typing - from django.db import models from tle.models.user import User diff --git a/app/tle/models/submission_comment.py b/app/tle/models/submission_comment.py index 3a7c8ab..c6f2a77 100644 --- a/app/tle/models/submission_comment.py +++ b/app/tle/models/submission_comment.py @@ -13,22 +13,19 @@ class SubmissionComment(models.Model): help_text='제출을 입력해주세요.', ) content = models.TextField( - help_text=( - '댓글을 입력해주세요.' - ), + max_length=1000, + help_text='댓글을 입력해주세요.', + blank=False, + null=False, ) line_number_start = models.IntegerField( - help_text=( - '댓글 시작 라인을 입력해주세요.' - ), + help_text='댓글 시작 라인을 입력해주세요.', validators=[ MinValueValidator(1), ], ) line_number_end = models.IntegerField( - help_text=( - '댓글 종료 라인을 입력해주세요.' - ), + help_text='댓글 종료 라인을 입력해주세요.', validators=[ MinValueValidator(1), # TODO: 시작 라인보다 작지 않도록 검사 @@ -39,10 +36,8 @@ class SubmissionComment(models.Model): created_by = models.ForeignKey( User, on_delete=models.CASCADE, - related_name='comments', - help_text=( - '유저를 입력해주세요.' - ), + related_name=User.FieldName.COMMENTS, + help_text='유저를 입력해주세요.', ) updated_at = models.DateTimeField(auto_now=True) diff --git a/app/tle/models/user.py b/app/tle/models/user.py index 07d347e..c324d8c 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -1,13 +1,49 @@ +from __future__ import annotations import typing -from django.contrib.auth.models import User as BaseUser +from django.contrib.auth.models import ( + AbstractBaseUser, + BaseUserManager, + PermissionsMixin, +) from django.db import models +from django.utils import timezone +from tle.models.user_solved_tier import UserSolvedTier -class User(BaseUser): - image = models.ImageField( + +def get_profile_image_path(instance: User, filename: str) -> str: + return f'user/profile/{instance.pk}/{filename}' + + +class UserManager(BaseUserManager): + model: typing.Callable[..., User] + + def create_user(self, email, username, password=None, **extra_fields): + if not email: + raise ValueError('The Email field must be set') + if not username: + raise ValueError('The Username field must be set') + email = self.normalize_email(email) + user = self.model( + email=email, + username=username, + **extra_fields + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, username, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + return self.create_user(email, username, password, **extra_fields) + + +class User(AbstractBaseUser, PermissionsMixin): + profile_image = models.ImageField( help_text='프로필 이미지', - upload_to='user_images/', + upload_to=get_profile_image_path, null=True, blank=True, validators=[ @@ -15,29 +51,59 @@ class User(BaseUser): # TODO: 이미지 확장자 제한 ] ) - boj_id = models.CharField( + boj_username = models.CharField( help_text='백준 아이디', max_length=40, null=True, blank=True, ) + boj_tier = models.IntegerField( + help_text='백준 티어', + choices=UserSolvedTier.choices, + null=True, + blank=True, + default=None, + ) + boj_tier_updated_at = models.DateTimeField( + help_text='백준 티어 갱신 시각', + null=True, + blank=True, + default=None, + ) + + username = models.CharField( + verbose_name='username', + max_length=30, + unique=True, + ) + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + ) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + first_name = models.TextField(blank=True, null=True, default=None) + last_name = models.TextField(blank=True, null=True, default=None) + date_joined = models.DateTimeField(default=timezone.now) + + objects = UserManager() @property def crews(self): for member in self.members: yield member.crew - REQUIRED_FIELDS = [ - 'email', - 'username', - 'password', - ] + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] class FieldName: PROBLEMS = 'problems' APPLICANTS = 'applicants' MEMBERS = 'members' SUBMISSIONS = 'submissions' + COMMENTS = 'comments' if typing.TYPE_CHECKING: from . import ( @@ -46,11 +112,13 @@ class FieldName: CrewApplicant as T_CrewApplicant, CrewMember as T_CrewMember, Submission as T_Submission, + SubmissionComment as T_SubmissionComment, ) problems: models.QuerySet[T_Problem] applicants: models.QuerySet[T_CrewApplicant] members: models.QuerySet[T_CrewMember] submissions: models.QuerySet[T_Submission] + comments: models.QuerySet[T_SubmissionComment] def __repr__(self) -> str: return f'[@{self.username}]' @@ -59,7 +127,8 @@ def __str__(self) -> str: staff = '(관리자)' if self.is_staff else '' return f'{self.pk} : {self.__repr__()} {staff}' + def has_perm(self, perm, obj=None): + return True -User._meta.get_field('email')._unique = True -User._meta.get_field('email').blank = False -User._meta.get_field('email').null = False + def has_module_perms(self, app_label): + return True From 073d2e06063dc16015d9035215262e92e755699a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 16:51:04 +0900 Subject: [PATCH 204/552] feat(tle.backends): create `UserAuthBackend` --- app/app/settings.py | 4 ++++ app/tle/backends.py | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 app/tle/backends.py diff --git a/app/app/settings.py b/app/app/settings.py index 65c51e0..8bcbe21 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -112,6 +112,10 @@ AUTH_USER_MODEL = 'tle.User' +AUTHENTICATION_BACKENDS = [ + 'tle.backends.UserAuthBackend', +] + # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/app/tle/backends.py b/app/tle/backends.py new file mode 100644 index 0000000..663ed86 --- /dev/null +++ b/app/tle/backends.py @@ -0,0 +1,22 @@ +import logging + +from django.contrib.auth.backends import ModelBackend +from django.http import HttpRequest + +from tle.models import User + + +logger = logging.getLogger(__name__) + + +class UserAuthBackend(ModelBackend): + def authenticate(self, request: HttpRequest, username=None, password=None, **kwargs): + """username 필드지만 email로 인증하도록 오버라이드 되어있음.""" + try: + user = User.objects.get(email=username) + except User.DoesNotExist: + return None + else: + if user.check_password(password): + return user + return None From edaa5e41ed623ac4a42eb4f0a603bb8fffddb922 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 16:51:22 +0900 Subject: [PATCH 205/552] refactor(app.permissions): delete `permissions.py` --- app/app/permissions.py | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 app/app/permissions.py diff --git a/app/app/permissions.py b/app/app/permissions.py deleted file mode 100644 index 724fd69..0000000 --- a/app/app/permissions.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework.permissions import ( - BasePermission, - SAFE_METHODS, -) - - -__all__ = ( - 'ReadOnly', - 'WriteOnly', -) - - -class ReadOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method in SAFE_METHODS, - ) - - -class WriteOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method == 'POST', - ) From b78c44554f2ed94e310e31a3470584f06b55b96a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:02:54 +0900 Subject: [PATCH 206/552] feat(tle.views): create urls.py --- app/app/urls.py | 3 +++ app/tle/views/__init__.py | 0 app/tle/views/urls.py | 7 +++++++ 3 files changed, 10 insertions(+) create mode 100644 app/tle/views/__init__.py create mode 100644 app/tle/views/urls.py diff --git a/app/app/urls.py b/app/app/urls.py index 3518843..20a4bb5 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -19,9 +19,12 @@ from django.contrib import admin from django.urls import include, path +import tle.views.urls + urlpatterns = [ path("admin/", admin.site.urls), + path("api/v1/", include(tle.views.urls.urlpatterns)), ] # Static files diff --git a/app/tle/views/__init__.py b/app/tle/views/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py new file mode 100644 index 0000000..c4e1601 --- /dev/null +++ b/app/tle/views/urls.py @@ -0,0 +1,7 @@ +from django.urls import include, path + +from tle.views.viewsets import * + + +urlpatterns = [ +] From 0bedd9fa7bc267055dc1e5aab86ca1b2370e237d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:03:49 +0900 Subject: [PATCH 207/552] feat(tle.serializers): create `User` related serializers --- app/tle/serializers/user.py | 66 +++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 app/tle/serializers/user.py diff --git a/app/tle/serializers/user.py b/app/tle/serializers/user.py new file mode 100644 index 0000000..c5dd61f --- /dev/null +++ b/app/tle/serializers/user.py @@ -0,0 +1,66 @@ +from rest_framework.serializers import * + +from tle.models import User + + +class BOJ_Mixin: + def get_boj(self, obj: User) -> dict: + return { + 'username': obj.boj_username, + 'tier': obj.boj_tier, + 'tier_updated_at': obj.boj_tier_updated_at, + } + + +USER_SERIALIZER_FIELDS = { + 'id': {'read_only': True}, + 'profile_image': {'read_only': True}, + 'username': {'read_only': True}, + 'boj': {'read_only': True}, +} + + +class UserSerializer(ModelSerializer, BOJ_Mixin): + boj = SerializerMethodField() + + class Meta: + model = User + fields = USER_SERIALIZER_FIELDS.keys() + extra_kwargs = USER_SERIALIZER_FIELDS + + +USER_SIGN_IN_SERIALIZER_FIELDS = { + **USER_SERIALIZER_FIELDS, + 'email': {}, + 'password': {'write_only': True}, +} + + +class UserSignInSerializer(ModelSerializer, BOJ_Mixin): + boj = SerializerMethodField() + + class Meta: + model = User + fields = USER_SIGN_IN_SERIALIZER_FIELDS.keys() + extra_kwargs = USER_SIGN_IN_SERIALIZER_FIELDS + + +USER_SIGN_UP_SERIALIZER_FIELDS = { + 'id': {'read_only': True}, + 'boj': {'read_only': True}, + + 'email': {}, + 'password': {'write_only': True}, + 'profile_image': {}, + 'username': {}, + 'boj_username': {'write_only': True}, +} + + +class UserSignUpSerializer(ModelSerializer): + boj = SerializerMethodField() + + class Meta: + model = User + fields = USER_SIGN_UP_SERIALIZER_FIELDS.keys() + extra_kwargs = USER_SIGN_UP_SERIALIZER_FIELDS From a10409fb320eb6d48c70f071e5f2abc55029ec41 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:05:43 +0900 Subject: [PATCH 208/552] feat(tle.views): create user viewsets --- app/tle/views/urls.py | 6 +++ app/tle/views/viewsets/__init__.py | 1 + app/tle/views/viewsets/user.py | 71 ++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+) create mode 100644 app/tle/views/viewsets/__init__.py create mode 100644 app/tle/views/viewsets/user.py diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index c4e1601..ee4f085 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -4,4 +4,10 @@ urlpatterns = [ + path("account/", include([ + path("signin", UserViewSet.as_view({"post": "sign_in"})), + path("signup", UserViewSet.as_view({"post": "sign_up"})), + path("signout", UserViewSet.as_view({"get": "sign_out"})), + path("current", UserViewSet.as_view({"get": "current"})), + ])), ] diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py new file mode 100644 index 0000000..43c1ce6 --- /dev/null +++ b/app/tle/views/viewsets/__init__.py @@ -0,0 +1 @@ +from tle.views.viewsets.user import UserViewSet diff --git a/app/tle/views/viewsets/user.py b/app/tle/views/viewsets/user.py new file mode 100644 index 0000000..2698392 --- /dev/null +++ b/app/tle/views/viewsets/user.py @@ -0,0 +1,71 @@ +from http import HTTPStatus + +from django.contrib.auth import ( + authenticate, + login, + logout, +) +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from tle.models import User +from tle.serializers import ( + UserSerializer, + UserSignUpSerializer, + UserSignInSerializer, +) +from tle.views.permissions import * + + +class UserViewSet(GenericViewSet): + """사용자 계정과 관련된 API + + current: 현재 로그인한 사용자 정보 + signup: 사용자 등록(회원가입) + signin: 사용자 로그인 + signout: 사용자 로그아웃 + """ + queryset = User.objects.all() + permission_classes = [AllowAny] + + SERIALIZERS = { + 'sign_up': UserSignUpSerializer, + 'sign_in': UserSignInSerializer, + } + + def get_serializer(self, *args, **kwargs): + if self.action in self.__class__.SERIALIZERS: + return self.__class__.SERIALIZERS[self.action](*args, **kwargs) + return UserSerializer(*args, **kwargs) + + def current(self, request: Request): + serializer = self.get_serializer(instance=request.user) + return Response(serializer.data) + + def sign_up(self, request: Request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.instance = User.objects.create_user(**serializer.validated_data) + return Response(serializer.data) + + def sign_in(self, request: Request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + user = authenticate(request, email, password) + if user is None: + raise AuthenticationFailed('Invalid email or password') + login(request, user) + serializer.instance = user + return Response(serializer.data) + + def sign_out(self, request: Request): + logout(request) + return Response(status=HTTPStatus.OK) + + # TODO: 이메일 인증 + + # TODO: 비밀번호 찾기 From cf3ba150bf9a2a30bc88e4b892ef06e7e55c5ce8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:09:54 +0900 Subject: [PATCH 209/552] feat(tle.serializers): create `ProblemSerializer` --- app/tle/serializers/problem.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 app/tle/serializers/problem.py diff --git a/app/tle/serializers/problem.py b/app/tle/serializers/problem.py new file mode 100644 index 0000000..b67e62e --- /dev/null +++ b/app/tle/serializers/problem.py @@ -0,0 +1,28 @@ +from rest_framework.serializers import ModelSerializer + +from tle.models import Problem +from tle.serializers.user import UserSerializer + + +PROBLEM_SERIALIZER_FIELDS = { + 'id': {'read_only': True}, + 'title': {}, + 'link': {}, + 'description': {}, + 'input_description': {}, + 'output_description': {}, + 'memory_limit': {}, + 'time_limit': {}, + 'created_at': {'read_only': True}, + 'created_by': {'read_only': True}, + 'updated_at': {'read_only': True}, +} + + +class ProblemSerializer(ModelSerializer): + created_by = UserSerializer(read_only=True) + + class Meta: + model = Problem + fields = PROBLEM_SERIALIZER_FIELDS.keys() + extra_kwargs = PROBLEM_SERIALIZER_FIELDS From a62df4f2c7d9f16e2631ca06f8432f34254de3b7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:14:20 +0900 Subject: [PATCH 210/552] feat(tle.serializers): add user serializers to `serializer` module --- app/tle/serializers/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 app/tle/serializers/__init__.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py new file mode 100644 index 0000000..c0a0d15 --- /dev/null +++ b/app/tle/serializers/__init__.py @@ -0,0 +1,8 @@ +from tle.serializers.user import * + + +__all__ = ( + 'UserSerializer', + 'UserSignInSerializer', + 'UserSignUpSerializer', +) From d26ff60a6b0ec0f2731ca94ecd301ac7983980cf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 18:55:24 +0900 Subject: [PATCH 211/552] refactor(tools): delete tools/* --- tools/db/languages.json | 46 - tools/db/setup_db.py | 94 - tools/db/tags.json | 5944 --------------------------------------- tools/runserver.sh | 6 - tools/setup.sh | 6 - 5 files changed, 6096 deletions(-) delete mode 100644 tools/db/languages.json delete mode 100644 tools/db/setup_db.py delete mode 100644 tools/db/tags.json delete mode 100755 tools/runserver.sh delete mode 100755 tools/setup.sh diff --git a/tools/db/languages.json b/tools/db/languages.json deleted file mode 100644 index 9537e6a..0000000 --- a/tools/db/languages.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "items": [ - { - "key": "nodejs", - "bojId": 17, - "displayName": "Node.js", - "extension": ".js" - }, - { - "key": "kotlin", - "bojId": 69, - "displayName": "Kotlin", - "extension": ".kt" - }, - { - "key": "swift", - "bojId": 74, - "displayName": "Swift", - "extension": ".swift" - }, - { - "key": "cpp", - "bojId": 1001, - "displayName": "C++", - "extension": ".cpp" - }, - { - "key": "java", - "bojId": 1002, - "displayName": "Java", - "extension": ".java" - }, - { - "key": "python", - "bojId": 1003, - "displayName": "Python", - "extension": ".py" - }, - { - "key": "c", - "bojId": 1004, - "displayName": "C", - "extension": ".c" - } - ] -} \ No newline at end of file diff --git a/tools/db/setup_db.py b/tools/db/setup_db.py deleted file mode 100644 index dba79ee..0000000 --- a/tools/db/setup_db.py +++ /dev/null @@ -1,94 +0,0 @@ -""" -Use this like: - -$ python manage.py shell < tools/db_setup.py -""" - -import dataclasses -import json -from typing import List - - -@dataclasses.dataclass -class DisplayNameJSON: - language: str - name: str - short: str - - -@dataclasses.dataclass -class AliasJSON: - alias: str - - -@dataclasses.dataclass -class TagJSON: - key: str - isMeta: bool - bojTagId: int - problemCount: int - displayNames: List[DisplayNameJSON] - aliases: List[AliasJSON] - - -@dataclasses.dataclass -class LanguageJSON: - key: str - bojId: int - displayName: str - extension: str - - -def load_tags(file='../tools/tags.json') -> List[TagJSON]: - with open(file) as f: - raw_tags = json.load(f) - tags = [] - for item in raw_tags['items']: - tag = TagJSON(**item) - tag.displayNames = [DisplayNameJSON(**display_name) for display_name in item["displayNames"]] - tag.aliases = [AliasJSON(**alias) for alias in item["aliases"]] - tags.append(tag) - return tags - - -def load_languages(file='../tools/languages.json') -> List[str]: - with open(file) as f: - raw_languages = json.load(f) - languages = [] - for item in raw_languages['items']: - languages.append(LanguageJSON(**item)) - return languages - - -from django.db.transaction import atomic - -from boj.models import BOJTag -from core.models import Language -from core.models import Tag - - -with atomic(): - for tag_data in load_tags(): - tag = Tag.objects.get_or_create(key=tag_data.key)[0] - tag.name_ko = next(filter(lambda x: x.language == 'ko', tag_data.displayNames)).name - tag.name_en = next(filter(lambda x: x.language == 'en', tag_data.displayNames)).name - tag.full_clean() - tag.save() - boj_tag = BOJTag.objects.get_or_create( - boj_id=tag_data.bojTagId, - tag=tag, - )[0] - boj_tag.full_clean() - boj_tag.save() - - -with atomic(): - for lang_data in load_languages(): - lang = Language.objects.get_or_create( - pk=lang_data.bojId, - key=lang_data.key - )[0] - lang.name = lang_data.displayName - lang.extension = lang_data.extension - lang.full_clean() - lang.save() diff --git a/tools/db/tags.json b/tools/db/tags.json deleted file mode 100644 index d21c414..0000000 --- a/tools/db/tags.json +++ /dev/null @@ -1,5944 +0,0 @@ -{ - "count": 206, - "items": [ - { - "key": "math", - "isMeta": false, - "bojTagId": 124, - "problemCount": 6212, - "displayNames": [ - { - "language": "ko", - "name": "수학", - "short": "수학" - }, - { - "language": "en", - "name": "mathematics", - "short": "math" - }, - { - "language": "ja", - "name": "数学", - "short": "数学" - } - ], - "aliases": [] - }, - { - "key": "implementation", - "isMeta": false, - "bojTagId": 102, - "problemCount": 5399, - "displayNames": [ - { - "language": "ko", - "name": "구현", - "short": "구현" - }, - { - "language": "en", - "name": "implementation", - "short": "impl" - }, - { - "language": "ja", - "name": "実装", - "short": "impl" - } - ], - "aliases": [] - }, - { - "key": "dp", - "isMeta": false, - "bojTagId": 25, - "problemCount": 3941, - "displayNames": [ - { - "language": "ko", - "name": "다이나믹 프로그래밍", - "short": "다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming", - "short": "dp" - }, - { - "language": "ja", - "name": "動的計画法", - "short": "dp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "동적 계획법" - }, - { - "alias": "다이나믹프로그래밍" - } - ] - }, - { - "key": "data_structures", - "isMeta": false, - "bojTagId": 175, - "problemCount": 3732, - "displayNames": [ - { - "language": "ko", - "name": "자료 구조", - "short": "자료 구조" - }, - { - "language": "en", - "name": "data structures", - "short": "ds" - }, - { - "language": "ja", - "name": "データ構造", - "short": "ds" - } - ], - "aliases": [ - { - "alias": "자료구조" - }, - { - "alias": "자구" - } - ] - }, - { - "key": "graphs", - "isMeta": false, - "bojTagId": 7, - "problemCount": 3600, - "displayNames": [ - { - "language": "ko", - "name": "그래프 이론", - "short": "그래프 이론" - }, - { - "language": "en", - "name": "graph theory", - "short": "graph" - }, - { - "language": "ja", - "name": "グラフ理論", - "short": "グラフ" - } - ], - "aliases": [ - { - "alias": "그래프이론" - }, - { - "alias": "그래프" - } - ] - }, - { - "key": "greedy", - "isMeta": false, - "bojTagId": 33, - "problemCount": 2461, - "displayNames": [ - { - "language": "ko", - "name": "그리디 알고리즘", - "short": "그리디 알고리즘" - }, - { - "language": "en", - "name": "greedy", - "short": "greedy" - }, - { - "language": "ja", - "name": "貪欲法", - "short": "貪欲法" - } - ], - "aliases": [ - { - "alias": "탐욕법" - } - ] - }, - { - "key": "string", - "isMeta": false, - "bojTagId": 158, - "problemCount": 2340, - "displayNames": [ - { - "language": "ko", - "name": "문자열", - "short": "문자열" - }, - { - "language": "en", - "name": "string", - "short": "string" - }, - { - "language": "ja", - "name": "文字列", - "short": "文字列" - } - ], - "aliases": [ - { - "alias": "스트링" - } - ] - }, - { - "key": "bruteforcing", - "isMeta": false, - "bojTagId": 125, - "problemCount": 2132, - "displayNames": [ - { - "language": "ko", - "name": "브루트포스 알고리즘", - "short": "브루트포스 알고리즘" - }, - { - "language": "en", - "name": "bruteforcing", - "short": "bruteforce" - }, - { - "language": "ja", - "name": "全探索", - "short": "全探索" - } - ], - "aliases": [ - { - "alias": "완전탐색" - }, - { - "alias": "완전 탐색" - }, - { - "alias": "브루트포스" - }, - { - "alias": "bruteforce" - }, - { - "alias": "brute force" - }, - { - "alias": "완탐" - } - ] - }, - { - "key": "graph_traversal", - "isMeta": false, - "bojTagId": 11, - "problemCount": 1960, - "displayNames": [ - { - "language": "ko", - "name": "그래프 탐색", - "short": "그래프 탐색" - }, - { - "language": "en", - "name": "graph traversal", - "short": "traversal" - }, - { - "language": "ja", - "name": "グラフの探索", - "short": "横断" - } - ], - "aliases": [ - { - "alias": "bfs" - }, - { - "alias": "dfs" - } - ] - }, - { - "key": "sorting", - "isMeta": false, - "bojTagId": 97, - "problemCount": 1817, - "displayNames": [ - { - "language": "ko", - "name": "정렬", - "short": "정렬" - }, - { - "language": "en", - "name": "sorting", - "short": "sorting" - }, - { - "language": "ja", - "name": "ソート", - "short": "ソート" - } - ], - "aliases": [] - }, - { - "key": "geometry", - "isMeta": false, - "bojTagId": 100, - "problemCount": 1474, - "displayNames": [ - { - "language": "ko", - "name": "기하학", - "short": "기하학" - }, - { - "language": "en", - "name": "geometry", - "short": "geom" - }, - { - "language": "ja", - "name": "幾何学", - "short": "幾何" - } - ], - "aliases": [] - }, - { - "key": "ad_hoc", - "isMeta": false, - "bojTagId": 109, - "problemCount": 1424, - "displayNames": [ - { - "language": "ko", - "name": "애드 혹", - "short": "애드 혹" - }, - { - "language": "en", - "name": "ad-hoc", - "short": "ad-hoc" - }, - { - "language": "ja", - "name": "アドホック", - "short": "アドホック" - } - ], - "aliases": [] - }, - { - "key": "number_theory", - "isMeta": false, - "bojTagId": 95, - "problemCount": 1399, - "displayNames": [ - { - "language": "ko", - "name": "정수론", - "short": "정수론" - }, - { - "language": "en", - "name": "number theory", - "short": "number theory" - }, - { - "language": "ja", - "name": "整数論", - "short": "整数論" - } - ], - "aliases": [] - }, - { - "key": "trees", - "isMeta": false, - "bojTagId": 120, - "problemCount": 1359, - "displayNames": [ - { - "language": "ko", - "name": "트리", - "short": "트리" - }, - { - "language": "en", - "name": "tree", - "short": "tree" - }, - { - "language": "ja", - "name": "木", - "short": "木" - } - ], - "aliases": [ - { - "alias": "trees" - } - ] - }, - { - "key": "segtree", - "isMeta": false, - "bojTagId": 65, - "problemCount": 1275, - "displayNames": [ - { - "language": "ko", - "name": "세그먼트 트리", - "short": "세그먼트 트리" - }, - { - "language": "en", - "name": "segment tree", - "short": "segtree" - }, - { - "language": "ja", - "name": "セグメント木", - "short": "セグ木" - } - ], - "aliases": [ - { - "alias": "구간트리" - }, - { - "alias": "세그트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "binary_search", - "isMeta": false, - "bojTagId": 12, - "problemCount": 1194, - "displayNames": [ - { - "language": "ko", - "name": "이분 탐색", - "short": "이분 탐색" - }, - { - "language": "en", - "name": "binary search", - "short": "binary search" - }, - { - "language": "ja", - "name": "二分探索", - "short": "二分探索" - } - ], - "aliases": [ - { - "alias": "이분탐색" - }, - { - "alias": "이진탐색" - } - ] - }, - { - "key": "arithmetic", - "isMeta": false, - "bojTagId": 121, - "problemCount": 1093, - "displayNames": [ - { - "language": "ko", - "name": "사칙연산", - "short": "사칙연산" - }, - { - "language": "en", - "name": "arithmetic", - "short": "arithmetic" - }, - { - "language": "ja", - "name": "算数", - "short": "算数" - } - ], - "aliases": [ - { - "alias": "덧셈" - }, - { - "alias": "뺄셈" - }, - { - "alias": "곱셈" - }, - { - "alias": "나눗셈" - }, - { - "alias": "더하기" - }, - { - "alias": "빼기" - }, - { - "alias": "곱하기" - }, - { - "alias": "나누기" - } - ] - }, - { - "key": "simulation", - "isMeta": false, - "bojTagId": 141, - "problemCount": 1054, - "displayNames": [ - { - "language": "ko", - "name": "시뮬레이션", - "short": "시뮬레이션" - }, - { - "language": "en", - "name": "simulation", - "short": "simulation" - }, - { - "language": "ja", - "name": "シミュレーション", - "short": "シミュレーション" - } - ], - "aliases": [] - }, - { - "key": "constructive", - "isMeta": false, - "bojTagId": 128, - "problemCount": 970, - "displayNames": [ - { - "language": "ko", - "name": "해 구성하기", - "short": "해 구성하기" - }, - { - "language": "en", - "name": "constructive", - "short": "constructive" - }, - { - "language": "ja", - "name": "構成的", - "short": "構成的" - } - ], - "aliases": [ - { - "alias": "constructive" - }, - { - "alias": "컨스트럭티브" - }, - { - "alias": "구성적" - } - ] - }, - { - "key": "bfs", - "isMeta": false, - "bojTagId": 126, - "problemCount": 962, - "displayNames": [ - { - "language": "ko", - "name": "너비 우선 탐색", - "short": "너비 우선 탐색" - }, - { - "language": "en", - "name": "breadth-first search", - "short": "bfs" - }, - { - "language": "ja", - "name": "幅優先検索", - "short": "bfs" - } - ], - "aliases": [ - { - "alias": "breadthfirst" - }, - { - "alias": "breadth first" - } - ] - }, - { - "key": "prefix_sum", - "isMeta": false, - "bojTagId": 139, - "problemCount": 925, - "displayNames": [ - { - "language": "ko", - "name": "누적 합", - "short": "누적 합" - }, - { - "language": "en", - "name": "prefix sum", - "short": "prefix sum" - }, - { - "language": "ja", - "name": "累積和", - "short": "累積和" - } - ], - "aliases": [ - { - "alias": "구간합" - }, - { - "alias": "부분합" - }, - { - "alias": "rangesum" - } - ] - }, - { - "key": "combinatorics", - "isMeta": false, - "bojTagId": 6, - "problemCount": 879, - "displayNames": [ - { - "language": "ko", - "name": "조합론", - "short": "조합론" - }, - { - "language": "en", - "name": "combinatorics", - "short": "combinatorics" - }, - { - "language": "ja", - "name": "組み合わせ", - "short": "組み合わせ" - } - ], - "aliases": [ - { - "alias": "combination" - }, - { - "alias": "permutation" - }, - { - "alias": "probability" - }, - { - "alias": "확률" - }, - { - "alias": "순열" - } - ] - }, - { - "key": "case_work", - "isMeta": false, - "bojTagId": 137, - "problemCount": 838, - "displayNames": [ - { - "language": "ko", - "name": "많은 조건 분기", - "short": "많은 조건 분기" - }, - { - "language": "en", - "name": "case work", - "short": "case work" - }, - { - "language": "ja", - "name": "ケースワーク", - "short": "ケースワーク" - } - ], - "aliases": [ - { - "alias": "케이스" - }, - { - "alias": "케이스워크" - }, - { - "alias": "케이스 워크" - } - ] - }, - { - "key": "dfs", - "isMeta": false, - "bojTagId": 127, - "problemCount": 795, - "displayNames": [ - { - "language": "ko", - "name": "깊이 우선 탐색", - "short": "깊이 우선 탐색" - }, - { - "language": "en", - "name": "depth-first search", - "short": "dfs" - }, - { - "language": "ja", - "name": "深さ優先探索", - "short": "dfs" - } - ], - "aliases": [ - { - "alias": "depth first" - }, - { - "alias": "depthfirst" - } - ] - }, - { - "key": "shortest_path", - "isMeta": false, - "bojTagId": 215, - "problemCount": 754, - "displayNames": [ - { - "language": "ko", - "name": "최단 경로", - "short": "최단 경로" - }, - { - "language": "en", - "name": "shortest path", - "short": "shortest path" - }, - { - "language": "ja", - "name": "最短経路", - "short": "最短経路" - } - ], - "aliases": [] - }, - { - "key": "bitmask", - "isMeta": false, - "bojTagId": 14, - "problemCount": 704, - "displayNames": [ - { - "language": "ko", - "name": "비트마스킹", - "short": "비트마스킹" - }, - { - "language": "en", - "name": "bitmask", - "short": "bitmask" - }, - { - "language": "ja", - "name": "ビット表現", - "short": "ビット表現" - } - ], - "aliases": [ - { - "alias": "비트필드" - }, - { - "alias": "비트마스크" - } - ] - }, - { - "key": "hash_set", - "isMeta": false, - "bojTagId": 136, - "problemCount": 620, - "displayNames": [ - { - "language": "ko", - "name": "해시를 사용한 집합과 맵", - "short": "해시를 사용한 집합과 맵" - }, - { - "language": "en", - "name": "set / map by hashing", - "short": "hashset" - }, - { - "language": "ja", - "name": "ハッシュ化によるセット・マップ", - "short": "hashset" - } - ], - "aliases": [ - { - "alias": "집합" - }, - { - "alias": "맵" - }, - { - "alias": "셋" - }, - { - "alias": "딕셔너리" - }, - { - "alias": "dictionary" - }, - { - "alias": "map" - }, - { - "alias": "set" - }, - { - "alias": "해싱" - }, - { - "alias": "hashing" - }, - { - "alias": "dict" - } - ] - }, - { - "key": "dijkstra", - "isMeta": false, - "bojTagId": 22, - "problemCount": 572, - "displayNames": [ - { - "language": "ko", - "name": "데이크스트라", - "short": "데이크스트라" - }, - { - "language": "en", - "name": "dijkstra's", - "short": "dijkstra's" - }, - { - "language": "ja", - "name": "ダイクストラ法", - "short": "ダイクストラ法" - } - ], - "aliases": [ - { - "alias": "다익" - }, - { - "alias": "다익스트라" - }, - { - "alias": "데이크스트라" - } - ] - }, - { - "key": "backtracking", - "isMeta": false, - "bojTagId": 5, - "problemCount": 515, - "displayNames": [ - { - "language": "ko", - "name": "백트래킹", - "short": "백트래킹" - }, - { - "language": "en", - "name": "backtracking", - "short": "backtrack" - }, - { - "language": "ja", - "name": "バックトラック法", - "short": "バックトラック" - } - ], - "aliases": [ - { - "alias": "백트래킹" - }, - { - "alias": "퇴각검색" - }, - { - "alias": "퇴각 검색" - } - ] - }, - { - "key": "tree_set", - "isMeta": false, - "bojTagId": 74, - "problemCount": 485, - "displayNames": [ - { - "language": "ko", - "name": "트리를 사용한 집합과 맵", - "short": "트리를 사용한 집합과 맵" - }, - { - "language": "en", - "name": "set / map by trees", - "short": "treeset" - }, - { - "language": "ja", - "name": "木によるセット・マップ", - "short": "treeset" - } - ], - "aliases": [ - { - "alias": "집합" - }, - { - "alias": "맵" - }, - { - "alias": "셋" - }, - { - "alias": "딕셔너리" - }, - { - "alias": "dictionary" - }, - { - "alias": "map" - }, - { - "alias": "set" - }, - { - "alias": "bbst" - }, - { - "alias": "트리" - }, - { - "alias": "tree" - } - ] - }, - { - "key": "sweeping", - "isMeta": false, - "bojTagId": 106, - "problemCount": 465, - "displayNames": [ - { - "language": "ko", - "name": "스위핑", - "short": "스위핑" - }, - { - "language": "en", - "name": "sweeping", - "short": "sweeping" - }, - { - "language": "ja", - "name": "平面走査", - "short": "平面走査" - } - ], - "aliases": [ - { - "alias": "라인 스위핑" - } - ] - }, - { - "key": "disjoint_set", - "isMeta": false, - "bojTagId": 81, - "problemCount": 461, - "displayNames": [ - { - "language": "ko", - "name": "분리 집합", - "short": "분리 집합" - }, - { - "language": "en", - "name": "disjoint set", - "short": "dsu" - }, - { - "language": "ja", - "name": "素集合データ構造", - "short": "素集合データ構造" - } - ], - "aliases": [ - { - "alias": "union" - }, - { - "alias": "find" - }, - { - "alias": "유니온" - }, - { - "alias": "파인드" - }, - { - "alias": "dsu" - } - ] - }, - { - "key": "parsing", - "isMeta": false, - "bojTagId": 96, - "problemCount": 448, - "displayNames": [ - { - "language": "ko", - "name": "파싱", - "short": "파싱" - }, - { - "language": "en", - "name": "parsing", - "short": "parsing" - }, - { - "language": "ja", - "name": "パージング", - "short": "パージング" - } - ], - "aliases": [] - }, - { - "key": "priority_queue", - "isMeta": false, - "bojTagId": 59, - "problemCount": 419, - "displayNames": [ - { - "language": "ko", - "name": "우선순위 큐", - "short": "우선순위 큐" - }, - { - "language": "en", - "name": "priority queue", - "short": "priority queue" - }, - { - "language": "ja", - "name": "優先度付きキュー", - "short": "優先度付きキュー" - } - ], - "aliases": [ - { - "alias": "heap" - }, - { - "alias": "힙" - } - ] - }, - { - "key": "dp_tree", - "isMeta": false, - "bojTagId": 92, - "problemCount": 411, - "displayNames": [ - { - "language": "ko", - "name": "트리에서의 다이나믹 프로그래밍", - "short": "트리에서의 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming on trees", - "short": "tree dp" - }, - { - "language": "ja", - "name": "木上の動的計画法", - "short": "tree dp" - } - ], - "aliases": [ - { - "alias": "트리dp" - } - ] - }, - { - "key": "divide_and_conquer", - "isMeta": false, - "bojTagId": 24, - "problemCount": 406, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복", - "short": "분할 정복" - }, - { - "language": "en", - "name": "divide and conquer", - "short": "d&c" - }, - { - "language": "ja", - "name": "分割統治法", - "short": "分割統治法" - } - ], - "aliases": [ - { - "alias": "dnc" - } - ] - }, - { - "key": "two_pointer", - "isMeta": false, - "bojTagId": 80, - "problemCount": 376, - "displayNames": [ - { - "language": "ko", - "name": "두 포인터", - "short": "두 포인터" - }, - { - "language": "en", - "name": "two-pointer", - "short": "two-pointer" - }, - { - "language": "ja", - "name": "尺取り法", - "short": "尺取り" - } - ], - "aliases": [ - { - "alias": "투포인터" - }, - { - "alias": "인치웜" - }, - { - "alias": "inchworm" - }, - { - "alias": "twopointer" - } - ] - }, - { - "key": "stack", - "isMeta": false, - "bojTagId": 71, - "problemCount": 368, - "displayNames": [ - { - "language": "ko", - "name": "스택", - "short": "스택" - }, - { - "language": "en", - "name": "stack", - "short": "stack" - }, - { - "language": "ja", - "name": "スタック", - "short": "スタック" - } - ], - "aliases": [] - }, - { - "key": "parametric_search", - "isMeta": false, - "bojTagId": 170, - "problemCount": 367, - "displayNames": [ - { - "language": "ko", - "name": "매개 변수 탐색", - "short": "매개 변수 탐색" - }, - { - "language": "en", - "name": "parametric search", - "short": "parametric search" - }, - { - "language": "ja", - "name": "parametric search", - "short": "parametric search" - } - ], - "aliases": [ - { - "alias": "파라메트릭" - } - ] - }, - { - "key": "game_theory", - "isMeta": false, - "bojTagId": 140, - "problemCount": 353, - "displayNames": [ - { - "language": "ko", - "name": "게임 이론", - "short": "게임 이론" - }, - { - "language": "en", - "name": "game theory", - "short": "game theory" - }, - { - "language": "ja", - "name": "ゲーム理論", - "short": "ゲーム" - } - ], - "aliases": [ - { - "alias": "게임이론" - }, - { - "alias": "님" - }, - { - "alias": "nim" - } - ] - }, - { - "key": "flow", - "isMeta": false, - "bojTagId": 45, - "problemCount": 323, - "displayNames": [ - { - "language": "ko", - "name": "최대 유량", - "short": "최대 유량" - }, - { - "language": "en", - "name": "maximum flow", - "short": "flow" - }, - { - "language": "ja", - "name": "最大フロー", - "short": "flow" - } - ], - "aliases": [ - { - "alias": "dinic" - }, - { - "alias": "dinitz" - }, - { - "alias": "ford" - }, - { - "alias": "fulkerson" - }, - { - "alias": "fordfulkerson" - }, - { - "alias": "디닉" - }, - { - "alias": "디니츠" - }, - { - "alias": "포드풀커슨" - }, - { - "alias": "플로우" - } - ] - }, - { - "key": "primality_test", - "isMeta": false, - "bojTagId": 9, - "problemCount": 313, - "displayNames": [ - { - "language": "ko", - "name": "소수 판정", - "short": "소수 판정" - }, - { - "language": "en", - "name": "primality test", - "short": "primality test" - }, - { - "language": "ja", - "name": "素数性テスト", - "short": "素数性テスト" - } - ], - "aliases": [ - { - "alias": "소수" - }, - { - "alias": "소수판별" - }, - { - "alias": "소수판정" - }, - { - "alias": "prime" - } - ] - }, - { - "key": "probability", - "isMeta": false, - "bojTagId": 177, - "problemCount": 299, - "displayNames": [ - { - "language": "ko", - "name": "확률론", - "short": "확률론" - }, - { - "language": "en", - "name": "probability theory", - "short": "probability" - }, - { - "language": "ja", - "name": "確率論", - "short": "確率論" - } - ], - "aliases": [ - { - "alias": "expected value" - }, - { - "alias": "기대값" - }, - { - "alias": "기댓값" - } - ] - }, - { - "key": "lazyprop", - "isMeta": false, - "bojTagId": 66, - "problemCount": 296, - "displayNames": [ - { - "language": "ko", - "name": "느리게 갱신되는 세그먼트 트리", - "short": "느리게 갱신되는 세그먼트 트리" - }, - { - "language": "en", - "name": "segment tree with lazy propagation", - "short": "lazyprop" - }, - { - "language": "ja", - "name": "遅延評価セグメント木", - "short": "遅延評価セグ木" - } - ], - "aliases": [ - { - "alias": "레이지" - }, - { - "alias": "레이지프로퍼게이션" - }, - { - "alias": "레이지프로파게이션" - }, - { - "alias": "구간트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "dp_bitfield", - "isMeta": false, - "bojTagId": 87, - "problemCount": 295, - "displayNames": [ - { - "language": "ko", - "name": "비트필드를 이용한 다이나믹 프로그래밍", - "short": "비트필드를 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using bitfield", - "short": "bitfield dp" - }, - { - "language": "ja", - "name": "ビットを使用した動的計画法", - "short": "ビットdp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "비트마스크" - }, - { - "alias": "비트dp" - } - ] - }, - { - "key": "exponentiation_by_squaring", - "isMeta": false, - "bojTagId": 39, - "problemCount": 273, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복을 이용한 거듭제곱", - "short": "분할 정복을 이용한 거듭제곱" - }, - { - "language": "en", - "name": "exponentiation by squaring", - "short": "exponentiation by squaring" - }, - { - "language": "ja", - "name": "二乗法によるべき乗", - "short": "二乗法によるべき乗" - } - ], - "aliases": [ - { - "alias": "거듭제곱" - }, - { - "alias": "제곱" - }, - { - "alias": "power" - }, - { - "alias": "square" - } - ] - }, - { - "key": "arbitrary_precision", - "isMeta": false, - "bojTagId": 117, - "problemCount": 254, - "displayNames": [ - { - "language": "ko", - "name": "임의 정밀도 / 큰 수 연산", - "short": "임의 정밀도 / 큰 수 연산" - }, - { - "language": "en", - "name": "arbitrary precision / big integers", - "short": "arbitrary precision / big integers" - }, - { - "language": "ja", - "name": "高精度または大きな数の演算", - "short": "高精度または大きな数の演算" - } - ], - "aliases": [ - { - "alias": "빅인티저" - }, - { - "alias": "빅데시멀" - }, - { - "alias": "biginteger" - }, - { - "alias": "bigdecimal" - } - ] - }, - { - "key": "knapsack", - "isMeta": false, - "bojTagId": 148, - "problemCount": 247, - "displayNames": [ - { - "language": "ko", - "name": "배낭 문제", - "short": "배낭 문제" - }, - { - "language": "en", - "name": "knapsack", - "short": "knapsack" - }, - { - "language": "ja", - "name": "ナップサック問題", - "short": "ナップサック" - } - ], - "aliases": [ - { - "alias": "냅색" - } - ] - }, - { - "key": "offline_queries", - "isMeta": false, - "bojTagId": 123, - "problemCount": 246, - "displayNames": [ - { - "language": "ko", - "name": "오프라인 쿼리", - "short": "오프라인 쿼리" - }, - { - "language": "en", - "name": "offline queries", - "short": "offline query" - }, - { - "language": "ja", - "name": "offline queries", - "short": "offline query" - } - ], - "aliases": [ - { - "alias": "offlinequery" - } - ] - }, - { - "key": "recursion", - "isMeta": false, - "bojTagId": 62, - "problemCount": 223, - "displayNames": [ - { - "language": "ko", - "name": "재귀", - "short": "재귀" - }, - { - "language": "en", - "name": "recursion", - "short": "recursion" - }, - { - "language": "ja", - "name": "再帰", - "short": "再帰" - } - ], - "aliases": [] - }, - { - "key": "coordinate_compression", - "isMeta": false, - "bojTagId": 161, - "problemCount": 223, - "displayNames": [ - { - "language": "ko", - "name": "값 / 좌표 압축", - "short": "값 / 좌표 압축" - }, - { - "language": "en", - "name": "value / coordinate compression", - "short": "compression" - }, - { - "language": "ja", - "name": "value / coordinate compression", - "short": "compression" - } - ], - "aliases": [ - { - "alias": "zip" - } - ] - }, - { - "key": "precomputation", - "isMeta": false, - "bojTagId": 172, - "problemCount": 203, - "displayNames": [ - { - "language": "ko", - "name": "런타임 전의 전처리", - "short": "런타임 전의 전처리" - }, - { - "language": "en", - "name": "precomputation", - "short": "precomputation" - }, - { - "language": "ja", - "name": "事前計算", - "short": "事前計算" - } - ], - "aliases": [ - { - "alias": "lookup table" - }, - { - "alias": "db" - }, - { - "alias": "database" - } - ] - }, - { - "key": "mst", - "isMeta": false, - "bojTagId": 49, - "problemCount": 199, - "displayNames": [ - { - "language": "ko", - "name": "최소 스패닝 트리", - "short": "최소 스패닝 트리" - }, - { - "language": "en", - "name": "minimum spanning tree", - "short": "mst" - }, - { - "language": "ja", - "name": "最小全域木", - "short": "最小全域木" - } - ], - "aliases": [] - }, - { - "key": "sieve", - "isMeta": false, - "bojTagId": 67, - "problemCount": 196, - "displayNames": [ - { - "language": "ko", - "name": "에라토스테네스의 체", - "short": "에라토스테네스의 체" - }, - { - "language": "en", - "name": "sieve of eratosthenes", - "short": "eratosthenes" - }, - { - "language": "ja", - "name": "エラトステネスの篩", - "short": "エラトステネス" - } - ], - "aliases": [ - { - "alias": "sieve" - }, - { - "alias": "에라체" - }, - { - "alias": "소수" - }, - { - "alias": "prime" - } - ] - }, - { - "key": "euclidean", - "isMeta": false, - "bojTagId": 26, - "problemCount": 186, - "displayNames": [ - { - "language": "ko", - "name": "유클리드 호제법", - "short": "유클리드 호제법" - }, - { - "language": "en", - "name": "euclidean algorithm", - "short": "euclidean algorithm" - }, - { - "language": "ja", - "name": "ユークリッドの互除法", - "short": "ユークリッドの互除法" - } - ], - "aliases": [ - { - "alias": "유클리드알고리즘" - } - ] - }, - { - "key": "bipartite_matching", - "isMeta": false, - "bojTagId": 13, - "problemCount": 184, - "displayNames": [ - { - "language": "ko", - "name": "이분 매칭", - "short": "이분 매칭" - }, - { - "language": "en", - "name": "bipartite matching", - "short": "bipartite matching" - }, - { - "language": "ja", - "name": "2部マッチング", - "short": "2部マッチング" - } - ], - "aliases": [] - }, - { - "key": "dag", - "isMeta": false, - "bojTagId": 213, - "problemCount": 182, - "displayNames": [ - { - "language": "ko", - "name": "방향 비순환 그래프", - "short": "dag" - }, - { - "language": "en", - "name": "directed acyclic graph", - "short": "dag" - }, - { - "language": "ja", - "name": "有向非巡回グラフ", - "short": "有向非巡回グラフ" - } - ], - "aliases": [] - }, - { - "key": "convex_hull", - "isMeta": false, - "bojTagId": 20, - "problemCount": 178, - "displayNames": [ - { - "language": "ko", - "name": "볼록 껍질", - "short": "볼록 껍질" - }, - { - "language": "en", - "name": "convex hull", - "short": "convex hull" - }, - { - "language": "ja", - "name": "凸包", - "short": "凸包" - } - ], - "aliases": [ - { - "alias": "컨벡스헐" - } - ] - }, - { - "key": "linear_algebra", - "isMeta": false, - "bojTagId": 144, - "problemCount": 174, - "displayNames": [ - { - "language": "ko", - "name": "선형대수학", - "short": "선형대수학" - }, - { - "language": "en", - "name": "linear algebra", - "short": "linear algebra" - }, - { - "language": "ja", - "name": "線形代数", - "short": "線代" - } - ], - "aliases": [ - { - "alias": "선형대수" - } - ] - }, - { - "key": "topological_sorting", - "isMeta": false, - "bojTagId": 78, - "problemCount": 170, - "displayNames": [ - { - "language": "ko", - "name": "위상 정렬", - "short": "위상 정렬" - }, - { - "language": "en", - "name": "topological sorting", - "short": "topological sorting" - }, - { - "language": "ja", - "name": "トポロジカルソート", - "short": "トポロジカルソート" - } - ], - "aliases": [] - }, - { - "key": "floyd_warshall", - "isMeta": false, - "bojTagId": 31, - "problemCount": 165, - "displayNames": [ - { - "language": "ko", - "name": "플로이드–워셜", - "short": "플로이드–워셜" - }, - { - "language": "en", - "name": "floyd–warshall", - "short": "floyd–warshall" - }, - { - "language": "ja", - "name": "ワーシャル–フロイド法", - "short": "ワーシャル–フロイド法" - } - ], - "aliases": [ - { - "alias": "플로이드" - }, - { - "alias": "플로이드와셜" - }, - { - "alias": "플로이드와샬" - } - ] - }, - { - "key": "hashing", - "isMeta": false, - "bojTagId": 8, - "problemCount": 164, - "displayNames": [ - { - "language": "ko", - "name": "해싱", - "short": "해싱" - }, - { - "language": "en", - "name": "hashing", - "short": "hash" - }, - { - "language": "ja", - "name": "ハッシュ化", - "short": "ハッシュ" - } - ], - "aliases": [] - }, - { - "key": "lca", - "isMeta": false, - "bojTagId": 41, - "problemCount": 163, - "displayNames": [ - { - "language": "ko", - "name": "최소 공통 조상", - "short": "최소 공통 조상" - }, - { - "language": "en", - "name": "lowest common ancestor", - "short": "lca" - }, - { - "language": "ja", - "name": "最下位共通祖先", - "short": "lca" - } - ], - "aliases": [] - }, - { - "key": "inclusion_and_exclusion", - "isMeta": false, - "bojTagId": 38, - "problemCount": 152, - "displayNames": [ - { - "language": "ko", - "name": "포함 배제의 원리", - "short": "포함 배제의 원리" - }, - { - "language": "en", - "name": "inclusion and exclusion", - "short": "inclusion and exclusion" - }, - { - "language": "ja", - "name": "包除原理", - "short": "包除原理" - } - ], - "aliases": [] - }, - { - "key": "scc", - "isMeta": false, - "bojTagId": 76, - "problemCount": 147, - "displayNames": [ - { - "language": "ko", - "name": "강한 연결 요소", - "short": "강한 연결 요소" - }, - { - "language": "en", - "name": "strongly connected component", - "short": "scc" - }, - { - "language": "ja", - "name": "強連結", - "short": "強連結" - } - ], - "aliases": [] - }, - { - "key": "randomization", - "isMeta": false, - "bojTagId": 115, - "problemCount": 143, - "displayNames": [ - { - "language": "ko", - "name": "무작위화", - "short": "무작위화" - }, - { - "language": "en", - "name": "randomization", - "short": "randomization" - }, - { - "language": "ja", - "name": "ランダム化", - "short": "ランダム化" - } - ], - "aliases": [ - { - "alias": "랜덤" - } - ] - }, - { - "key": "sparse_table", - "isMeta": false, - "bojTagId": 84, - "problemCount": 136, - "displayNames": [ - { - "language": "ko", - "name": "희소 배열", - "short": "희소 배열" - }, - { - "language": "en", - "name": "sparse table", - "short": "sparse table" - }, - { - "language": "ja", - "name": "sparse table", - "short": "sparse table" - } - ], - "aliases": [ - { - "alias": "스파스어레이" - }, - { - "alias": "sparse table" - } - ] - }, - { - "key": "smaller_to_larger", - "isMeta": false, - "bojTagId": 169, - "problemCount": 128, - "displayNames": [ - { - "language": "ko", - "name": "작은 집합에서 큰 집합으로 합치는 테크닉", - "short": "작은 집합에서 큰 집합으로 합치는 테크닉" - }, - { - "language": "en", - "name": "smaller to larger technique", - "short": "smaller to larger" - }, - { - "language": "ja", - "name": "smaller to larger technique", - "short": "smaller to larger" - } - ], - "aliases": [ - { - "alias": "merge heuristics" - }, - { - "alias": "sack" - }, - { - "alias": "small to large" - }, - { - "alias": "작은거" - }, - { - "alias": "큰거" - } - ] - }, - { - "key": "fft", - "isMeta": false, - "bojTagId": 28, - "problemCount": 126, - "displayNames": [ - { - "language": "ko", - "name": "고속 푸리에 변환", - "short": "고속 푸리에 변환" - }, - { - "language": "en", - "name": "fast fourier transform", - "short": "fft" - }, - { - "language": "ja", - "name": "高速フーリエ変換", - "short": "fft" - } - ], - "aliases": [ - { - "alias": "푸리에변환" - }, - { - "alias": "컨볼루션" - }, - { - "alias": "convolution" - } - ] - }, - { - "key": "trie", - "isMeta": false, - "bojTagId": 79, - "problemCount": 124, - "displayNames": [ - { - "language": "ko", - "name": "트라이", - "short": "트라이" - }, - { - "language": "en", - "name": "trie", - "short": "trie" - }, - { - "language": "ja", - "name": "トライ木", - "short": "トライ" - } - ], - "aliases": [] - }, - { - "key": "deque", - "isMeta": false, - "bojTagId": 73, - "problemCount": 120, - "displayNames": [ - { - "language": "ko", - "name": "덱", - "short": "덱" - }, - { - "language": "en", - "name": "deque", - "short": "deque" - }, - { - "language": "ja", - "name": "両端キュー", - "short": "deque" - } - ], - "aliases": [] - }, - { - "key": "line_intersection", - "isMeta": false, - "bojTagId": 42, - "problemCount": 117, - "displayNames": [ - { - "language": "ko", - "name": "선분 교차 판정", - "short": "선분 교차 판정" - }, - { - "language": "en", - "name": "line segment intersection check", - "short": "line segment intersection check" - }, - { - "language": "ja", - "name": "直線の交点", - "short": "直線の交点" - } - ], - "aliases": [] - }, - { - "key": "mcmf", - "isMeta": false, - "bojTagId": 48, - "problemCount": 116, - "displayNames": [ - { - "language": "ko", - "name": "최소 비용 최대 유량", - "short": "최소 비용 최대 유량" - }, - { - "language": "en", - "name": "minimum cost maximum flow", - "short": "mcmf" - }, - { - "language": "ja", - "name": "最小費用最大流問題", - "short": "mcmf" - } - ], - "aliases": [ - { - "alias": "dinic" - }, - { - "alias": "dinitz" - }, - { - "alias": "ford" - }, - { - "alias": "fulkerson" - }, - { - "alias": "fordfulkerson" - }, - { - "alias": "디닉" - }, - { - "alias": "디니츠" - }, - { - "alias": "포드풀커슨" - } - ] - }, - { - "key": "sqrt_decomposition", - "isMeta": false, - "bojTagId": 130, - "problemCount": 110, - "displayNames": [ - { - "language": "ko", - "name": "제곱근 분할법", - "short": "제곱근 분할법" - }, - { - "language": "en", - "name": "square root decomposition", - "short": "sqrt decomposition" - }, - { - "language": "ja", - "name": "平方分割", - "short": "平方分割" - } - ], - "aliases": [ - { - "alias": "루트분할법" - }, - { - "alias": "평방분할법" - }, - { - "alias": "모" - }, - { - "alias": "mo" - }, - { - "alias": "sqrt" - } - ] - }, - { - "key": "calculus", - "isMeta": false, - "bojTagId": 111, - "problemCount": 107, - "displayNames": [ - { - "language": "ko", - "name": "미적분학", - "short": "미적분학" - }, - { - "language": "en", - "name": "calculus", - "short": "calculus" - }, - { - "language": "ja", - "name": "微積分", - "short": "微積分" - } - ], - "aliases": [ - { - "alias": "미분" - }, - { - "alias": "적분" - } - ] - }, - { - "key": "modular_multiplicative_inverse", - "isMeta": false, - "bojTagId": 164, - "problemCount": 101, - "displayNames": [ - { - "language": "ko", - "name": "모듈로 곱셈 역원", - "short": "모듈로 곱셈 역원" - }, - { - "language": "en", - "name": "modular multiplicative inverse", - "short": "modular multiplicative inverse" - }, - { - "language": "ja", - "name": "モジュラ逆数", - "short": "モジュラ逆数" - } - ], - "aliases": [ - { - "alias": "modinv" - } - ] - }, - { - "key": "cht", - "isMeta": false, - "bojTagId": 89, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "볼록 껍질을 이용한 최적화", - "short": "볼록 껍질을 이용한 최적화" - }, - { - "language": "en", - "name": "convex hull trick", - "short": "cht" - }, - { - "language": "ja", - "name": "convex hull trick", - "short": "cht" - } - ], - "aliases": [ - { - "alias": "컨벡스헐트릭" - }, - { - "alias": "컨벡스헐최적화" - } - ] - }, - { - "key": "heuristics", - "isMeta": false, - "bojTagId": 142, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "휴리스틱", - "short": "휴리스틱" - }, - { - "language": "en", - "name": "heuristics", - "short": "heuristics" - }, - { - "language": "ja", - "name": "ヒューリスティック", - "short": "ヒューリスティック" - } - ], - "aliases": [] - }, - { - "key": "sliding_window", - "isMeta": false, - "bojTagId": 68, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "슬라이딩 윈도우", - "short": "슬라이딩 윈도우" - }, - { - "language": "en", - "name": "sliding window", - "short": "sliding window" - }, - { - "language": "ja", - "name": "スライディングウィンドウ", - "short": "スライディングウィンドウ" - } - ], - "aliases": [ - { - "alias": "슬라이딩윈도" - } - ] - }, - { - "key": "geometry_3d", - "isMeta": false, - "bojTagId": 131, - "problemCount": 98, - "displayNames": [ - { - "language": "ko", - "name": "3차원 기하학", - "short": "3차원 기하학" - }, - { - "language": "en", - "name": "geometry; 3d", - "short": "3d" - }, - { - "language": "ja", - "name": "3次元幾何学", - "short": "3d" - } - ], - "aliases": [] - }, - { - "key": "suffix_array", - "isMeta": false, - "bojTagId": 77, - "problemCount": 97, - "displayNames": [ - { - "language": "ko", - "name": "접미사 배열과 LCP 배열", - "short": "접미사 배열과 LCP 배열" - }, - { - "language": "en", - "name": "suffix array and lcp array", - "short": "suffix array and lcp array" - }, - { - "language": "ja", - "name": "接尾辞配列・LCP配列", - "short": "接尾辞配列・LCP配列" - } - ], - "aliases": [] - }, - { - "key": "centroid", - "isMeta": false, - "bojTagId": 188, - "problemCount": 95, - "displayNames": [ - { - "language": "en", - "name": "centroid", - "short": "centroid" - }, - { - "language": "ko", - "name": "센트로이드", - "short": "센트로이드" - }, - { - "language": "ja", - "name": "centroid", - "short": "centroid" - } - ], - "aliases": [] - }, - { - "key": "euler_tour_technique", - "isMeta": false, - "bojTagId": 150, - "problemCount": 95, - "displayNames": [ - { - "language": "ko", - "name": "오일러 경로 테크닉", - "short": "오일러 경로 테크닉" - }, - { - "language": "en", - "name": "euler tour technique", - "short": "ett" - }, - { - "language": "ja", - "name": "オイラーツアー", - "short": "ett" - } - ], - "aliases": [] - }, - { - "key": "sprague_grundy", - "isMeta": false, - "bojTagId": 70, - "problemCount": 94, - "displayNames": [ - { - "language": "ko", - "name": "스프라그–그런디 정리", - "short": "스프라그–그런디 정리" - }, - { - "language": "en", - "name": "sprague–grundy theorem", - "short": "sprague–grundy thm" - }, - { - "language": "ja", - "name": "sprague–grundy theorem", - "short": "sprague–grundy thm" - } - ], - "aliases": [ - { - "alias": "님버" - }, - { - "alias": "nimber" - } - ] - }, - { - "key": "ternary_search", - "isMeta": false, - "bojTagId": 101, - "problemCount": 92, - "displayNames": [ - { - "language": "ko", - "name": "삼분 탐색", - "short": "삼분 탐색" - }, - { - "language": "en", - "name": "ternary search", - "short": "ternary search" - }, - { - "language": "ja", - "name": "三分探索", - "short": "三分探索" - } - ], - "aliases": [] - }, - { - "key": "mitm", - "isMeta": false, - "bojTagId": 46, - "problemCount": 89, - "displayNames": [ - { - "language": "ko", - "name": "중간에서 만나기", - "short": "중간에서 만나기" - }, - { - "language": "en", - "name": "meet in the middle", - "short": "meet in the middle" - }, - { - "language": "ja", - "name": "半分全列挙", - "short": "半分全列挙" - } - ], - "aliases": [] - }, - { - "key": "bitset", - "isMeta": false, - "bojTagId": 152, - "problemCount": 89, - "displayNames": [ - { - "language": "ko", - "name": "비트 집합", - "short": "비트 집합" - }, - { - "language": "en", - "name": "bit set", - "short": "bit set" - }, - { - "language": "ja", - "name": "bit set", - "short": "bit set" - } - ], - "aliases": [ - { - "alias": "bitset" - }, - { - "alias": "비트셋" - } - ] - }, - { - "key": "pythagoras", - "isMeta": false, - "bojTagId": 60, - "problemCount": 88, - "displayNames": [ - { - "language": "ko", - "name": "피타고라스 정리", - "short": "피타고라스 정리" - }, - { - "language": "en", - "name": "pythagoras theorem", - "short": "pythagoras thm" - }, - { - "language": "ja", - "name": "ピタゴラスの定理", - "short": "ピタゴラス" - } - ], - "aliases": [] - }, - { - "key": "permutation_cycle_decomposition", - "isMeta": false, - "bojTagId": 171, - "problemCount": 87, - "displayNames": [ - { - "language": "ko", - "name": "순열 사이클 분할", - "short": "순열 사이클 분할" - }, - { - "language": "en", - "name": "permutation cycle decomposition", - "short": "permutation cycle decomposition" - }, - { - "language": "ja", - "name": "順列サイクル分解", - "short": "順列サイクル分解" - } - ], - "aliases": [] - }, - { - "key": "lis", - "isMeta": false, - "bojTagId": 43, - "problemCount": 85, - "displayNames": [ - { - "language": "ko", - "name": "가장 긴 증가하는 부분 수열: O(n log n)", - "short": "가장 긴 증가하는 부분 수열: O(n log n)" - }, - { - "language": "en", - "name": "longest increasing sequence in o(n log n)", - "short": "lis in o(n log n)" - }, - { - "language": "ja", - "name": "longest increasing sequence in o(n log n)", - "short": "lis in o(n log n)" - } - ], - "aliases": [] - }, - { - "key": "kmp", - "isMeta": false, - "bojTagId": 40, - "problemCount": 84, - "displayNames": [ - { - "language": "ko", - "name": "KMP", - "short": "KMP" - }, - { - "language": "en", - "name": "knuth–morris–pratt", - "short": "kmp" - }, - { - "language": "ja", - "name": "クヌース–モリス–プラット法", - "short": "kmp" - } - ], - "aliases": [] - }, - { - "key": "gaussian_elimination", - "isMeta": false, - "bojTagId": 32, - "problemCount": 81, - "displayNames": [ - { - "language": "ko", - "name": "가우스 소거법", - "short": "가우스 소거법" - }, - { - "language": "en", - "name": "gaussian elimination", - "short": "gaussian elimination" - }, - { - "language": "ja", - "name": "ガウス消去法", - "short": "ガウス消去法" - } - ], - "aliases": [] - }, - { - "key": "hld", - "isMeta": false, - "bojTagId": 35, - "problemCount": 80, - "displayNames": [ - { - "language": "ko", - "name": "Heavy-light 분할", - "short": "Heavy-light 분할" - }, - { - "language": "en", - "name": "heavy-light decomposition", - "short": "hld" - }, - { - "language": "ja", - "name": "heavy-light decomposition", - "short": "hld" - } - ], - "aliases": [] - }, - { - "key": "centroid_decomposition", - "isMeta": false, - "bojTagId": 18, - "problemCount": 76, - "displayNames": [ - { - "language": "ko", - "name": "센트로이드 분할", - "short": "센트로이드 분할" - }, - { - "language": "en", - "name": "centroid decomposition", - "short": "centroid decomposition" - }, - { - "language": "ja", - "name": "centroid decomposition", - "short": "centroid decomposition" - } - ], - "aliases": [ - { - "alias": "센트로이드" - } - ] - }, - { - "key": "mfmc", - "isMeta": false, - "bojTagId": 167, - "problemCount": 71, - "displayNames": [ - { - "language": "ko", - "name": "최대 유량 최소 컷 정리", - "short": "최대 유량 최소 컷 정리" - }, - { - "language": "en", - "name": "max-flow min-cut theorem", - "short": "mfmc" - }, - { - "language": "ja", - "name": "最大フロー最小カット定理", - "short": "mfmc" - } - ], - "aliases": [] - }, - { - "key": "polygon_area", - "isMeta": false, - "bojTagId": 3, - "problemCount": 71, - "displayNames": [ - { - "language": "ko", - "name": "다각형의 넓이", - "short": "다각형의 넓이" - }, - { - "language": "en", - "name": "area of a polygon", - "short": "area of a polygon" - }, - { - "language": "ja", - "name": "多角形の面積", - "short": "多角形の面積" - } - ], - "aliases": [ - { - "alias": "넓이" - } - ] - }, - { - "key": "queue", - "isMeta": false, - "bojTagId": 72, - "problemCount": 64, - "displayNames": [ - { - "language": "ko", - "name": "큐", - "short": "큐" - }, - { - "language": "en", - "name": "queue", - "short": "queue" - }, - { - "language": "ja", - "name": "キュー", - "short": "キュー" - } - ], - "aliases": [] - }, - { - "key": "physics", - "isMeta": false, - "bojTagId": 116, - "problemCount": 62, - "displayNames": [ - { - "language": "ko", - "name": "물리학", - "short": "물리학" - }, - { - "language": "en", - "name": "physics", - "short": "physics" - }, - { - "language": "ja", - "name": "物理", - "short": "物理" - } - ], - "aliases": [] - }, - { - "key": "flt", - "isMeta": false, - "bojTagId": 29, - "problemCount": 60, - "displayNames": [ - { - "language": "ko", - "name": "페르마의 소정리", - "short": "페르마의 소정리" - }, - { - "language": "en", - "name": "fermat's little theorem", - "short": "fermat's little thm" - }, - { - "language": "ja", - "name": "フェルマーの小定理", - "short": "フェルマー" - } - ], - "aliases": [] - }, - { - "key": "tsp", - "isMeta": false, - "bojTagId": 138, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "외판원 순회 문제", - "short": "외판원 순회 문제" - }, - { - "language": "en", - "name": "travelling salesman problem", - "short": "tsp" - }, - { - "language": "ja", - "name": "巡回セールスマン問題", - "short": "巡回セールスマン" - } - ], - "aliases": [ - { - "alias": "외판원순회" - } - ] - }, - { - "key": "eulerian_path", - "isMeta": false, - "bojTagId": 93, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "오일러 경로", - "short": "오일러 경로" - }, - { - "language": "en", - "name": "eulerian path / circuit", - "short": "eulerian path" - }, - { - "language": "ja", - "name": "eulerian path / circuit", - "short": "eulerian path" - } - ], - "aliases": [ - { - "alias": "eulerian circuit" - }, - { - "alias": "euler tour" - } - ] - }, - { - "key": "linearity_of_expectation", - "isMeta": false, - "bojTagId": 179, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "기댓값의 선형성", - "short": "기댓값의 선형성" - }, - { - "language": "en", - "name": "linearity of expectation", - "short": "linearity of expectation" - }, - { - "language": "ja", - "name": "期待値の線形性", - "short": "期待値の線形性" - } - ], - "aliases": [] - }, - { - "key": "2_sat", - "isMeta": false, - "bojTagId": 1, - "problemCount": 58, - "displayNames": [ - { - "language": "ko", - "name": "2-sat", - "short": "2-sat" - }, - { - "language": "en", - "name": "2-sat", - "short": "2-sat" - }, - { - "language": "ja", - "name": "2-sat", - "short": "2-sat" - } - ], - "aliases": [ - { - "alias": "투셋" - }, - { - "alias": "twosat" - }, - { - "alias": "2sat" - } - ] - }, - { - "key": "articulation", - "isMeta": false, - "bojTagId": 4, - "problemCount": 57, - "displayNames": [ - { - "language": "ko", - "name": "단절점과 단절선", - "short": "단절점과 단절선" - }, - { - "language": "en", - "name": "articulation points and bridges", - "short": "articulation points and bridges" - }, - { - "language": "ja", - "name": "関節点と橋", - "short": "関節点と橋" - } - ], - "aliases": [ - { - "alias": "단절점" - }, - { - "alias": "단절선" - }, - { - "alias": "브리지" - }, - { - "alias": "브릿지" - }, - { - "alias": "bridge" - } - ] - }, - { - "key": "0_1_bfs", - "isMeta": false, - "bojTagId": 176, - "problemCount": 56, - "displayNames": [ - { - "language": "ko", - "name": "0-1 너비 우선 탐색", - "short": "0-1 너비 우선 탐색" - }, - { - "language": "en", - "name": "0-1 bfs", - "short": "0-1 bfs" - }, - { - "language": "ja", - "name": "0-1 bfs", - "short": "0-1 bfs" - } - ], - "aliases": [] - }, - { - "key": "bipartite_graph", - "isMeta": false, - "bojTagId": 197, - "problemCount": 54, - "displayNames": [ - { - "language": "ko", - "name": "이분 그래프", - "short": "이분 그래프" - }, - { - "language": "en", - "name": "bipartite graph", - "short": "bipartite graph" - }, - { - "language": "ja", - "name": "2部グラフ", - "short": "2部グラフ" - } - ], - "aliases": [] - }, - { - "key": "biconnected_component", - "isMeta": false, - "bojTagId": 153, - "problemCount": 48, - "displayNames": [ - { - "language": "ko", - "name": "이중 연결 요소", - "short": "이중 연결 요소" - }, - { - "language": "en", - "name": "biconnected component", - "short": "biconnected component" - }, - { - "language": "ja", - "name": "二重接続コンポーネント", - "short": "二重接続" - } - ], - "aliases": [ - { - "alias": "bcc" - } - ] - }, - { - "key": "pst", - "isMeta": false, - "bojTagId": 55, - "problemCount": 46, - "displayNames": [ - { - "language": "ko", - "name": "퍼시스턴트 세그먼트 트리", - "short": "퍼시스턴트 세그먼트 트리" - }, - { - "language": "en", - "name": "persistent segment tree", - "short": "pst" - }, - { - "language": "ja", - "name": "永続セグメント木", - "short": "永続セグ木" - } - ], - "aliases": [ - { - "alias": "퍼시스턴트구간트리" - }, - { - "alias": "구간트리" - }, - { - "alias": "퍼시스턴트세그트리" - } - ] - }, - { - "key": "crt", - "isMeta": false, - "bojTagId": 19, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "중국인의 나머지 정리", - "short": "중국인의 나머지 정리" - }, - { - "language": "en", - "name": "chinese remainder theorem", - "short": "crt" - }, - { - "language": "ja", - "name": "中国の剰余定理", - "short": "中国の剰余定理" - } - ], - "aliases": [] - }, - { - "key": "linked_list", - "isMeta": false, - "bojTagId": 154, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "연결 리스트", - "short": "연결 리스트" - }, - { - "language": "en", - "name": "linked list", - "short": "ll" - }, - { - "language": "ja", - "name": "連結リスト", - "short": "連結リスト" - } - ], - "aliases": [ - { - "alias": "링크드리스트" - } - ] - }, - { - "key": "pigeonhole_principle", - "isMeta": false, - "bojTagId": 189, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "비둘기집 원리", - "short": "비둘기집" - }, - { - "language": "en", - "name": "pigeonhole principle", - "short": "pigeonhole" - }, - { - "language": "ja", - "name": "鳩の巣原理", - "short": "鳩" - } - ], - "aliases": [] - }, - { - "key": "cactus", - "isMeta": false, - "bojTagId": 143, - "problemCount": 42, - "displayNames": [ - { - "language": "ko", - "name": "선인장", - "short": "선인장" - }, - { - "language": "en", - "name": "cactus", - "short": "cactus" - }, - { - "language": "ja", - "name": "サボテングラフ", - "short": "サボテングラフ" - } - ], - "aliases": [] - }, - { - "key": "bellman_ford", - "isMeta": false, - "bojTagId": 10, - "problemCount": 41, - "displayNames": [ - { - "language": "ko", - "name": "벨만–포드", - "short": "벨만–포드" - }, - { - "language": "en", - "name": "bellman–ford", - "short": "bellman-ford" - }, - { - "language": "ja", - "name": "ベルマンフォード法", - "short": "ベルマンフォード" - } - ], - "aliases": [ - { - "alias": "bellmanford" - }, - { - "alias": "벨만포드" - }, - { - "alias": "spfa" - } - ] - }, - { - "key": "planar_graph", - "isMeta": false, - "bojTagId": 168, - "problemCount": 41, - "displayNames": [ - { - "language": "ko", - "name": "평면 그래프", - "short": "평면 그래프" - }, - { - "language": "en", - "name": "planar graph", - "short": "planar graph" - }, - { - "language": "ja", - "name": "平面グラフ", - "short": "平面グラフ" - } - ], - "aliases": [] - }, - { - "key": "point_in_convex_polygon", - "isMeta": false, - "bojTagId": 56, - "problemCount": 40, - "displayNames": [ - { - "language": "ko", - "name": "볼록 다각형 내부의 점 판정", - "short": "볼록 다각형 내부의 점 판정" - }, - { - "language": "en", - "name": "point in convex polygon check", - "short": "point in convex polygon check" - }, - { - "language": "ja", - "name": "凸多角形の点包含判定", - "short": "凸多角形の点包含判定" - } - ], - "aliases": [] - }, - { - "key": "euler_phi", - "isMeta": false, - "bojTagId": 151, - "problemCount": 38, - "displayNames": [ - { - "language": "ko", - "name": "오일러 피 함수", - "short": "오일러 피 함수" - }, - { - "language": "en", - "name": "euler totient function", - "short": "euler phi function" - }, - { - "language": "ja", - "name": "euler totient function", - "short": "euler phi function" - } - ], - "aliases": [ - { - "alias": "오일러 파이" - }, - { - "alias": "토션트" - }, - { - "alias": "eulerphi" - }, - { - "alias": "euler phi" - } - ] - }, - { - "key": "splay_tree", - "isMeta": false, - "bojTagId": 69, - "problemCount": 37, - "displayNames": [ - { - "language": "ko", - "name": "스플레이 트리", - "short": "스플레이 트리" - }, - { - "language": "en", - "name": "splay tree", - "short": "splay tree" - }, - { - "language": "ja", - "name": "splay tree", - "short": "splay tree" - } - ], - "aliases": [] - }, - { - "key": "pbs", - "isMeta": false, - "bojTagId": 54, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "병렬 이분 탐색", - "short": "병렬 이분 탐색" - }, - { - "language": "en", - "name": "parallel binary search", - "short": "pbs" - }, - { - "language": "ja", - "name": "parallel binary search", - "short": "pbs" - } - ], - "aliases": [] - }, - { - "key": "extended_euclidean", - "isMeta": false, - "bojTagId": 27, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "확장 유클리드 호제법", - "short": "확장 유클리드 호제법" - }, - { - "language": "en", - "name": "extended euclidean algorithm", - "short": "extended euclidean algorithm" - }, - { - "language": "ja", - "name": "拡張ユークリッドの互除法", - "short": "拡張ユークリッド" - } - ], - "aliases": [ - { - "alias": "확장유클리드알고리즘" - }, - { - "alias": "egcd" - } - ] - }, - { - "key": "divide_and_conquer_optimization", - "isMeta": false, - "bojTagId": 91, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복을 사용한 최적화", - "short": "분할 정복을 사용한 최적화" - }, - { - "language": "en", - "name": "divide and conquer optimization", - "short": "d&c optimization" - }, - { - "language": "ja", - "name": "divide and conquer optimization", - "short": "d&c optimization" - } - ], - "aliases": [ - { - "alias": "분할 정복 최적화" - }, - { - "alias": "dnc opt" - } - ] - }, - { - "key": "deque_trick", - "isMeta": false, - "bojTagId": 216, - "problemCount": 34, - "displayNames": [ - { - "language": "ko", - "name": "덱을 이용한 구간 최댓값 트릭", - "short": "덱 트릭" - }, - { - "language": "en", - "name": "deque range maximum trick", - "short": "deque rmq trick" - }, - { - "language": "ja", - "name": "deque range maximum trick", - "short": "deque rmq trick" - } - ], - "aliases": [] - }, - { - "key": "mo", - "isMeta": false, - "bojTagId": 50, - "problemCount": 33, - "displayNames": [ - { - "language": "ko", - "name": "mo's", - "short": "Mo's" - }, - { - "language": "en", - "name": "mo's", - "short": "mo's" - }, - { - "language": "ja", - "name": "mo's", - "short": "mo's" - } - ], - "aliases": [ - { - "alias": "squarerootdecomposition" - }, - { - "alias": "sqrtdecomposition" - }, - { - "alias": "평방분할법" - } - ] - }, - { - "key": "half_plane_intersection", - "isMeta": false, - "bojTagId": 190, - "problemCount": 30, - "displayNames": [ - { - "language": "ko", - "name": "반평면 교집합", - "short": "반평면 교집합" - }, - { - "language": "en", - "name": "half plane intersection", - "short": "hpi" - }, - { - "language": "ja", - "name": "half plane intersection", - "short": "hpi" - } - ], - "aliases": [] - }, - { - "key": "dp_deque", - "isMeta": false, - "bojTagId": 108, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "덱을 이용한 다이나믹 프로그래밍", - "short": "덱을 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using a deque", - "short": "deque dp" - }, - { - "language": "ja", - "name": "両端キューを使用した動的計画法", - "short": "deque dp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "덱dp" - } - ] - }, - { - "key": "aho_corasick", - "isMeta": false, - "bojTagId": 2, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "아호-코라식", - "short": "아호-코라식" - }, - { - "language": "en", - "name": "aho-corasick", - "short": "aho-corasick" - }, - { - "language": "ja", - "name": "アホコラシック", - "short": "アホコラシック" - } - ], - "aliases": [ - { - "alias": "아호코라식" - }, - { - "alias": "ahocorasick" - } - ] - }, - { - "key": "multi_segtree", - "isMeta": false, - "bojTagId": 166, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "다차원 세그먼트 트리", - "short": "다차원 세그먼트 트리" - }, - { - "language": "en", - "name": "multidimensional segment tree", - "short": "multidimensional segtree" - }, - { - "language": "ja", - "name": "multidimensional segment tree", - "short": "multidimensional segtree" - } - ], - "aliases": [ - { - "alias": "구간트리" - }, - { - "alias": "세그트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "rotating_calipers", - "isMeta": false, - "bojTagId": 64, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "회전하는 캘리퍼스", - "short": "회전하는 캘리퍼스" - }, - { - "language": "en", - "name": "rotating calipers", - "short": "rotating calipers" - }, - { - "language": "ja", - "name": "rotating calipers", - "short": "rotating calipers" - } - ], - "aliases": [] - }, - { - "key": "euler_characteristic", - "isMeta": false, - "bojTagId": 119, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "오일러 지표 (χ=V-E+F)", - "short": "오일러 지표" - }, - { - "language": "en", - "name": "euler characteristic (χ=v-e+f)", - "short": "euler characteristic" - }, - { - "language": "ja", - "name": "オイラー特性(χ=v-e+f)", - "short": "オイラー特性" - } - ], - "aliases": [] - }, - { - "key": "regex", - "isMeta": false, - "bojTagId": 63, - "problemCount": 27, - "displayNames": [ - { - "language": "ko", - "name": "정규 표현식", - "short": "정규 표현식" - }, - { - "language": "en", - "name": "regular expression", - "short": "regex" - }, - { - "language": "ja", - "name": "正規表現", - "short": "regex" - } - ], - "aliases": [ - { - "alias": "정규식" - } - ] - }, - { - "key": "slope_trick", - "isMeta": false, - "bojTagId": 157, - "problemCount": 26, - "displayNames": [ - { - "language": "ko", - "name": "함수 개형을 이용한 최적화", - "short": "함수 개형을 이용한 최적화" - }, - { - "language": "en", - "name": "slope trick", - "short": "slope trick" - }, - { - "language": "ja", - "name": "slope trick", - "short": "slope trick" - } - ], - "aliases": [ - { - "alias": "슬로프트릭" - }, - { - "alias": "슬로프 트릭" - } - ] - }, - { - "key": "berlekamp_massey", - "isMeta": false, - "bojTagId": 110, - "problemCount": 25, - "displayNames": [ - { - "language": "ko", - "name": "벌리캠프–매시", - "short": "벌리캠프–매시" - }, - { - "language": "en", - "name": "berlekamp–massey", - "short": "berlekamp–massey" - }, - { - "language": "ja", - "name": "berlekamp–massey", - "short": "berlekamp–massey" - } - ], - "aliases": [ - { - "alias": "벌레캠프" - }, - { - "alias": "벌래캠프" - } - ] - }, - { - "key": "manacher", - "isMeta": false, - "bojTagId": 44, - "problemCount": 24, - "displayNames": [ - { - "language": "ko", - "name": "매내처", - "short": "매내처" - }, - { - "language": "en", - "name": "manacher's", - "short": "manacher's" - }, - { - "language": "ja", - "name": "manacher's", - "short": "manacher's" - } - ], - "aliases": [] - }, - { - "key": "pollard_rho", - "isMeta": false, - "bojTagId": 58, - "problemCount": 23, - "displayNames": [ - { - "language": "ko", - "name": "폴라드 로", - "short": "폴라드 로" - }, - { - "language": "en", - "name": "pollard rho", - "short": "pollard rho" - }, - { - "language": "ja", - "name": "ポラード・ロー素因数分解法", - "short": "ポラード・ロー" - } - ], - "aliases": [] - }, - { - "key": "dp_connection_profile", - "isMeta": false, - "bojTagId": 107, - "problemCount": 23, - "displayNames": [ - { - "language": "ko", - "name": "커넥션 프로파일을 이용한 다이나믹 프로그래밍", - "short": "커넥션 프로파일을 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using connection profile", - "short": "dp using connection profile" - }, - { - "language": "ja", - "name": "dynamic programming using connection profile", - "short": "dp using connection profile" - } - ], - "aliases": [ - { - "alias": "동적계획법" - } - ] - }, - { - "key": "link_cut_tree", - "isMeta": false, - "bojTagId": 98, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "링크/컷 트리", - "short": "링크/컷 트리" - }, - { - "language": "en", - "name": "link/cut tree", - "short": "link/cut tree" - }, - { - "language": "ja", - "name": "link/cut tree", - "short": "link/cut tree" - } - ], - "aliases": [ - { - "alias": "link cut tree" - }, - { - "alias": "linkcuttree" - }, - { - "alias": "링크컷" - } - ] - }, - { - "key": "merge_sort_tree", - "isMeta": false, - "bojTagId": 155, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "머지 소트 트리", - "short": "머지 소트 트리" - }, - { - "language": "en", - "name": "merge sort tree", - "short": "merge sort tree" - }, - { - "language": "ja", - "name": "マージソート木", - "short": "マージソート木" - } - ], - "aliases": [ - { - "alias": "병합정렬트리" - }, - { - "alias": "합병정렬트리" - } - ] - }, - { - "key": "tree_isomorphism", - "isMeta": false, - "bojTagId": 145, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "트리 동형 사상", - "short": "트리 동형 사상" - }, - { - "language": "en", - "name": "tree isomorphism", - "short": "tree isomorphism" - }, - { - "language": "ja", - "name": "木の同型性判定", - "short": "木の同型性判定" - } - ], - "aliases": [ - { - "alias": "graph isomorphism" - }, - { - "alias": "isomorphism" - }, - { - "alias": "topology" - }, - { - "alias": "아이소모피즘" - }, - { - "alias": "위상" - } - ] - }, - { - "key": "simulated_annealing", - "isMeta": false, - "bojTagId": 184, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "담금질 기법", - "short": "담금질 기법" - }, - { - "language": "en", - "name": "simulated annealing", - "short": "simulated annealing" - }, - { - "language": "ja", - "name": "焼き鈍し法", - "short": "焼き鈍し法" - } - ], - "aliases": [] - }, - { - "key": "hall", - "isMeta": false, - "bojTagId": 34, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "홀의 결혼 정리", - "short": "홀의 결혼 정리" - }, - { - "language": "en", - "name": "hall's theorem", - "short": "hall's thm" - }, - { - "language": "ja", - "name": "ホールの定理", - "short": "ホールの定理" - } - ], - "aliases": [] - }, - { - "key": "hungarian", - "isMeta": false, - "bojTagId": 36, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "헝가리안", - "short": "헝가리안" - }, - { - "language": "en", - "name": "hungarian", - "short": "hungarian" - }, - { - "language": "ja", - "name": "hungarian", - "short": "hungarian" - } - ], - "aliases": [ - { - "alias": "헝가리안" - }, - { - "alias": "assignment problem" - }, - { - "alias": "weighted bipartite matching" - } - ] - }, - { - "key": "flood_fill", - "isMeta": false, - "bojTagId": 210, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "플러드 필", - "short": "플러드 필" - }, - { - "language": "en", - "name": "flood-fill", - "short": "ff" - }, - { - "language": "ja", - "name": "flood-fill", - "short": "ff" - } - ], - "aliases": [] - }, - { - "key": "miller_rabin", - "isMeta": false, - "bojTagId": 47, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "밀러–라빈 소수 판별법", - "short": "밀러–라빈 소수 판별법" - }, - { - "language": "en", - "name": "miller–rabin", - "short": "miller–rabin" - }, - { - "language": "ja", - "name": "ミラー–ラビン素数判定法", - "short": "ミラー–ラビン" - } - ], - "aliases": [] - }, - { - "key": "mobius_inversion", - "isMeta": false, - "bojTagId": 51, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "뫼비우스 반전 공식", - "short": "뫼비우스 반전 공식" - }, - { - "language": "en", - "name": "möbius inversion", - "short": "möbius inversion" - }, - { - "language": "ja", - "name": "メビウスの反転公式", - "short": "メビウス" - } - ], - "aliases": [ - { - "alias": "mobius" - } - ] - }, - { - "key": "rabin_karp", - "isMeta": false, - "bojTagId": 61, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "라빈–카프", - "short": "라빈–카프" - }, - { - "language": "en", - "name": "rabin–karp", - "short": "rabin–karp" - }, - { - "language": "ja", - "name": "ラビン-カープ文字列検索", - "short": "ラビン-カープ文字列検索" - } - ], - "aliases": [ - { - "alias": "라빈카프" - } - ] - }, - { - "key": "numerical_analysis", - "isMeta": false, - "bojTagId": 122, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "수치해석", - "short": "수치해석" - }, - { - "language": "en", - "name": "numerical analysis", - "short": "numerical analysis" - }, - { - "language": "ja", - "name": "数値解析", - "short": "数値解析" - } - ], - "aliases": [ - { - "alias": "수학" - } - ] - }, - { - "key": "point_in_non_convex_polygon", - "isMeta": false, - "bojTagId": 57, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "오목 다각형 내부의 점 판정", - "short": "오목 다각형 내부의 점 판정" - }, - { - "language": "en", - "name": "point in non-convex polygon check", - "short": "point in non-convex polygon check" - }, - { - "language": "ja", - "name": "非凸多角形の点包含判定", - "short": "非凸多角形の点包含判定" - } - ], - "aliases": [] - }, - { - "key": "alien", - "isMeta": false, - "bojTagId": 134, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "Aliens 트릭", - "short": "aliens 트릭" - }, - { - "language": "en", - "name": "aliens trick", - "short": "aliens trick" - }, - { - "language": "ja", - "name": "aliens法", - "short": "aliens法" - } - ], - "aliases": [ - { - "alias": "alien's trick" - } - ] - }, - { - "key": "linear_programming", - "isMeta": false, - "bojTagId": 103, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "선형 계획법", - "short": "선형 계획법" - }, - { - "language": "en", - "name": "linear programming", - "short": "lp" - }, - { - "language": "ja", - "name": "線型計画法", - "short": "lp" - } - ], - "aliases": [ - { - "alias": "리니어프로그래밍" - } - ] - }, - { - "key": "generating_function", - "isMeta": false, - "bojTagId": 198, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "생성 함수", - "short": "생성 함수" - }, - { - "language": "en", - "name": "generating function", - "short": "generating function" - }, - { - "language": "ja", - "name": "生成関数", - "short": "生成関数" - } - ], - "aliases": [] - }, - { - "key": "offline_dynamic_connectivity", - "isMeta": false, - "bojTagId": 52, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "오프라인 동적 연결성 판정", - "short": "오프라인 동적 연결성 판정" - }, - { - "language": "en", - "name": "offline dynamic connectivity", - "short": "offline dynamic connectivity" - }, - { - "language": "ja", - "name": "offline dynamic connectivity", - "short": "offline dynamic connectivity" - } - ], - "aliases": [] - }, - { - "key": "statistics", - "isMeta": false, - "bojTagId": 178, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "통계학", - "short": "통계학" - }, - { - "language": "en", - "name": "statistics", - "short": "stats" - }, - { - "language": "ja", - "name": "統計学", - "short": "統計" - } - ], - "aliases": [ - { - "alias": "average" - }, - { - "alias": "평균" - }, - { - "alias": "variance" - }, - { - "alias": "분산" - }, - { - "alias": "표준편차" - }, - { - "alias": "표준 편차" - } - ] - }, - { - "key": "functional_graph", - "isMeta": false, - "bojTagId": 211, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "함수형 그래프", - "short": "함수형 그래프" - }, - { - "language": "en", - "name": "functional graph", - "short": "functional graph" - }, - { - "language": "ja", - "name": "functional graph", - "short": "functional graph" - } - ], - "aliases": [] - }, - { - "key": "dp_sum_over_subsets", - "isMeta": false, - "bojTagId": 207, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "부분집합의 합 다이나믹 프로그래밍", - "short": "부분집합의 합" - }, - { - "language": "en", - "name": "sum over subsets dynamic programming", - "short": "sos dp" - }, - { - "language": "ja", - "name": "sum over subsets dynamic programming", - "short": "sos dp" - } - ], - "aliases": [ - { - "alias": "sos" - } - ] - }, - { - "key": "circulation", - "isMeta": false, - "bojTagId": 191, - "problemCount": 16, - "displayNames": [ - { - "language": "ko", - "name": "서큘레이션", - "short": "서큘레이션" - }, - { - "language": "en", - "name": "circulation", - "short": "circulation" - }, - { - "language": "ja", - "name": "circulation", - "short": "circulation" - } - ], - "aliases": [] - }, - { - "key": "tree_compression", - "isMeta": false, - "bojTagId": 193, - "problemCount": 16, - "displayNames": [ - { - "language": "ko", - "name": "트리 압축", - "short": "트리 압축" - }, - { - "language": "en", - "name": "tree compression", - "short": "tree compression" - }, - { - "language": "ja", - "name": "tree compression", - "short": "tree compression" - } - ], - "aliases": [] - }, - { - "key": "voronoi", - "isMeta": false, - "bojTagId": 82, - "problemCount": 15, - "displayNames": [ - { - "language": "ko", - "name": "보로노이 다이어그램", - "short": "보로노이 다이어그램" - }, - { - "language": "en", - "name": "voronoi diagram", - "short": "voronoi diagram" - }, - { - "language": "ja", - "name": "ボロノイ図", - "short": "ボロノイ図" - } - ], - "aliases": [] - }, - { - "key": "duality", - "isMeta": false, - "bojTagId": 180, - "problemCount": 14, - "displayNames": [ - { - "language": "ko", - "name": "쌍대성", - "short": "쌍대성" - }, - { - "language": "en", - "name": "duality", - "short": "duality" - }, - { - "language": "ja", - "name": "双対性", - "short": "双対性" - } - ], - "aliases": [ - { - "alias": "듀얼리티" - } - ] - }, - { - "key": "dual_graph", - "isMeta": false, - "bojTagId": 181, - "problemCount": 14, - "displayNames": [ - { - "language": "ko", - "name": "쌍대 그래프", - "short": "쌍대 그래프" - }, - { - "language": "en", - "name": "dual graph", - "short": "dual graph" - }, - { - "language": "ja", - "name": "双対グラフ", - "short": "双対グラフ" - } - ], - "aliases": [ - { - "alias": "듀얼 그래프" - } - ] - }, - { - "key": "lucas", - "isMeta": false, - "bojTagId": 113, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "뤼카 정리", - "short": "뤼카 정리" - }, - { - "language": "en", - "name": "lucas theorem", - "short": "lucas thm" - }, - { - "language": "ja", - "name": "lucas theorem", - "short": "lucas thm" - } - ], - "aliases": [] - }, - { - "key": "matroid", - "isMeta": false, - "bojTagId": 104, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "매트로이드", - "short": "매트로이드" - }, - { - "language": "en", - "name": "matroid", - "short": "matroid" - }, - { - "language": "ja", - "name": "マトロイド", - "short": "マトロイド" - } - ], - "aliases": [] - }, - { - "key": "dp_digit", - "isMeta": false, - "bojTagId": 217, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "자릿수를 이용한 다이나믹 프로그래밍", - "short": "자릿수 dp" - }, - { - "language": "en", - "name": "digit dp", - "short": "digit dp" - } - ], - "aliases": [] - }, - { - "key": "kitamasa", - "isMeta": false, - "bojTagId": 112, - "problemCount": 12, - "displayNames": [ - { - "language": "ko", - "name": "키타마사", - "short": "키타마사" - }, - { - "language": "en", - "name": "kitamasa", - "short": "kitamasa" - }, - { - "language": "ja", - "name": "きたまさ法", - "short": "きたまさ法" - } - ], - "aliases": [] - }, - { - "key": "cartesian_tree", - "isMeta": false, - "bojTagId": 206, - "problemCount": 11, - "displayNames": [ - { - "language": "ko", - "name": "데카르트 트리", - "short": "데카르트 트리" - }, - { - "language": "en", - "name": "cartesian tree", - "short": "cartesian tree" - }, - { - "language": "ja", - "name": "デカルト木", - "short": "デカルト木" - } - ], - "aliases": [] - }, - { - "key": "general_matching", - "isMeta": false, - "bojTagId": 15, - "problemCount": 11, - "displayNames": [ - { - "language": "ko", - "name": "일반적인 매칭", - "short": "일반적인 매칭" - }, - { - "language": "en", - "name": "general matching", - "short": "general matching" - }, - { - "language": "ja", - "name": "一般的なマッチング", - "short": "一般的なマッチング" - } - ], - "aliases": [ - { - "alias": "블라썸" - }, - { - "alias": "블러썸" - }, - { - "alias": "블라섬" - }, - { - "alias": "블러섬" - }, - { - "alias": "blossom" - }, - { - "alias": "부합" - } - ] - }, - { - "key": "tree_decomposition", - "isMeta": false, - "bojTagId": 204, - "problemCount": 10, - "displayNames": [ - { - "language": "ko", - "name": "트리 분할", - "short": "트리 분할" - }, - { - "language": "en", - "name": "tree decomposition", - "short": "tree decomposition" - }, - { - "language": "ja", - "name": "tree decomposition", - "short": "tree decomposition" - } - ], - "aliases": [] - }, - { - "key": "burnside", - "isMeta": false, - "bojTagId": 16, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "번사이드 보조정리", - "short": "번사이드 보조정리" - }, - { - "language": "en", - "name": "burnside's lemma", - "short": "burnside's lemma" - }, - { - "language": "ja", - "name": "バーンサイドの補題", - "short": "バーンサイド" - } - ], - "aliases": [] - }, - { - "key": "discrete_log", - "isMeta": false, - "bojTagId": 146, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "이산 로그", - "short": "이산 로그" - }, - { - "language": "en", - "name": "discrete logarithm", - "short": "discrete logarithm" - }, - { - "language": "ja", - "name": "離散対数", - "short": "離散対数" - } - ], - "aliases": [ - { - "alias": "order" - } - ] - }, - { - "key": "geometry_hyper", - "isMeta": false, - "bojTagId": 132, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "4차원 이상의 기하학", - "short": "4차원 이상의 기하학" - }, - { - "language": "en", - "name": "geometry; hyperdimensional", - "short": "hyperdimensional" - }, - { - "language": "ja", - "name": "4次元以上での幾何学", - "short": "hyperdimensional" - } - ], - "aliases": [ - { - "alias": "4차원" - }, - { - "alias": "5차원" - }, - { - "alias": "6차원" - }, - { - "alias": "7차원" - }, - { - "alias": "8차원" - }, - { - "alias": "9차원" - }, - { - "alias": "4d" - }, - { - "alias": "5d" - }, - { - "alias": "6d" - }, - { - "alias": "7d" - }, - { - "alias": "8d" - }, - { - "alias": "9d" - } - ] - }, - { - "key": "bidirectional_search", - "isMeta": false, - "bojTagId": 129, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "양방향 탐색", - "short": "양방향 탐색" - }, - { - "language": "en", - "name": "bidirectional search", - "short": "bidirectional search" - }, - { - "language": "ja", - "name": "bidirectional search", - "short": "bidirectional search" - } - ], - "aliases": [] - }, - { - "key": "min_enclosing_circle", - "isMeta": false, - "bojTagId": 162, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "최소 외접원", - "short": "최소 외접원" - }, - { - "language": "en", - "name": "minimum enclosing circle", - "short": "minimum enclosing circle" - }, - { - "language": "ja", - "name": "最小外接円", - "short": "最小外接円" - } - ], - "aliases": [ - { - "alias": "bounding circle" - }, - { - "alias": "smallest enclosing circle" - }, - { - "alias": "minimum covering circle" - } - ] - }, - { - "key": "z", - "isMeta": false, - "bojTagId": 83, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "z", - "short": "Z" - }, - { - "language": "en", - "name": "z", - "short": "z" - }, - { - "language": "ja", - "name": "z", - "short": "z" - } - ], - "aliases": [] - }, - { - "key": "pick", - "isMeta": false, - "bojTagId": 187, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "픽의 정리", - "short": "픽" - }, - { - "language": "en", - "name": "pick's theorem", - "short": "pick's thm" - }, - { - "language": "ja", - "name": "ピックの定理", - "short": "ピック" - } - ], - "aliases": [] - }, - { - "key": "utf8", - "isMeta": false, - "bojTagId": 199, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "utf-8 입력 처리", - "short": "utf-8" - }, - { - "language": "en", - "name": "utf-8 inputs", - "short": "utf-8" - }, - { - "language": "ja", - "name": "utf-8入力の処理", - "short": "utf-8" - } - ], - "aliases": [] - }, - { - "key": "top_tree", - "isMeta": false, - "bojTagId": 105, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "탑 트리", - "short": "탑 트리" - }, - { - "language": "en", - "name": "top tree", - "short": "top tree" - }, - { - "language": "ja", - "name": "top tree", - "short": "top tree" - } - ], - "aliases": [] - }, - { - "key": "palindrome_tree", - "isMeta": false, - "bojTagId": 53, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "회문 트리", - "short": "회문 트리" - }, - { - "language": "en", - "name": "palindrome tree", - "short": "palindrome tree" - }, - { - "language": "ja", - "name": "palindrome tree", - "short": "palindrome tree" - } - ], - "aliases": [ - { - "alias": "팰린드롬트리" - } - ] - }, - { - "key": "monotone_queue_optimization", - "isMeta": false, - "bojTagId": 165, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "단조 큐를 이용한 최적화", - "short": "단조 큐를 이용한 최적화" - }, - { - "language": "en", - "name": "monotone queue optimization", - "short": "monotone queue optimization" - }, - { - "language": "ja", - "name": "monotone queue optimization", - "short": "monotone queue optimization" - } - ], - "aliases": [] - }, - { - "key": "knuth_x", - "isMeta": false, - "bojTagId": 174, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "크누스 X", - "short": "크누스 X" - }, - { - "language": "en", - "name": "knuth's x", - "short": "x" - }, - { - "language": "ja", - "name": "knuth's x", - "short": "x" - } - ], - "aliases": [] - }, - { - "key": "delaunay", - "isMeta": false, - "bojTagId": 21, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "델로네 삼각분할", - "short": "델로네 삼각분할" - }, - { - "language": "en", - "name": "delaunay triangulation", - "short": "delaunay triangulation" - }, - { - "language": "ja", - "name": "ドロネー三角形分割", - "short": "ドロネー" - } - ], - "aliases": [] - }, - { - "key": "dominator_tree", - "isMeta": false, - "bojTagId": 135, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "도미네이터 트리", - "short": "도미네이터 트리" - }, - { - "language": "en", - "name": "dominator tree", - "short": "dominator tree" - }, - { - "language": "ja", - "name": "dominator tree", - "short": "dominator tree" - } - ], - "aliases": [] - }, - { - "key": "stable_marriage", - "isMeta": false, - "bojTagId": 192, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "안정 결혼 문제", - "short": "안정 결혼" - }, - { - "language": "en", - "name": "stable marriage problem", - "short": "smp" - }, - { - "language": "ja", - "name": "stable marriage problem", - "short": "smp" - } - ], - "aliases": [] - }, - { - "key": "rope", - "isMeta": false, - "bojTagId": 159, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "로프", - "short": "로프" - }, - { - "language": "en", - "name": "rope", - "short": "rope" - }, - { - "language": "ja", - "name": "rope", - "short": "rope" - } - ], - "aliases": [] - }, - { - "key": "bayes", - "isMeta": false, - "bojTagId": 114, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "베이즈 정리", - "short": "베이즈 정리" - }, - { - "language": "en", - "name": "bayes theorem", - "short": "bayes thm" - }, - { - "language": "ja", - "name": "ベイズの定理", - "short": "ベイズ" - } - ], - "aliases": [ - { - "alias": "조건부확률" - } - ] - }, - { - "key": "knuth", - "isMeta": false, - "bojTagId": 90, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "크누스 최적화", - "short": "크누스 최적화" - }, - { - "language": "en", - "name": "knuth optimization", - "short": "knuth" - }, - { - "language": "ja", - "name": "knuth optimization", - "short": "knuth" - } - ], - "aliases": [] - }, - { - "key": "dancing_links", - "isMeta": false, - "bojTagId": 173, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "춤추는 링크", - "short": "춤추는 링크" - }, - { - "language": "en", - "name": "dancing links", - "short": "dancing links" - }, - { - "language": "ja", - "name": "dancing links", - "short": "dancing links" - } - ], - "aliases": [] - }, - { - "key": "degree_sequence", - "isMeta": false, - "bojTagId": 200, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "차수열", - "short": "차수열" - }, - { - "language": "en", - "name": "degree sequence", - "short": "degree sequence" - }, - { - "language": "ja", - "name": "degree sequence", - "short": "degree sequence" - } - ], - "aliases": [] - }, - { - "key": "differential_cryptanalysis", - "isMeta": false, - "bojTagId": 185, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "차분 공격", - "short": "차분 공격" - }, - { - "language": "en", - "name": "differential cryptanalysis", - "short": "differential cryptanalysis" - }, - { - "language": "ja", - "name": "differential cryptanalysis", - "short": "differential cryptanalysis" - } - ], - "aliases": [ - { - "alias": "dc" - } - ] - }, - { - "key": "geometric_boolean_operations", - "isMeta": false, - "bojTagId": 202, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "도형에서의 불 연산", - "short": "도형에서의 불 연산" - }, - { - "language": "en", - "name": "boolean operations on geometric objects", - "short": "geometric boolean operations" - }, - { - "language": "ja", - "name": "図形のブール演算", - "short": "図形のブール演算" - } - ], - "aliases": [ - { - "alias": "병합" - }, - { - "alias": "교집합" - }, - { - "alias": "합집합" - }, - { - "alias": "union" - }, - { - "alias": "intersect" - } - ] - }, - { - "key": "hirschberg", - "isMeta": false, - "bojTagId": 163, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "히르쉬버그", - "short": "히르쉬버그" - }, - { - "language": "en", - "name": "hirschberg's", - "short": "hirschberg's" - }, - { - "language": "ja", - "name": "hirschberg's", - "short": "hirschberg's" - } - ], - "aliases": [ - { - "alias": "hirschburg" - } - ] - }, - { - "key": "suffix_tree", - "isMeta": false, - "bojTagId": 182, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "접미사 트리", - "short": "접미사 트리" - }, - { - "language": "en", - "name": "suffix tree", - "short": "suffix tree" - }, - { - "language": "ja", - "name": "suffix tree", - "short": "suffix tree" - } - ], - "aliases": [] - }, - { - "key": "chordal_graph", - "isMeta": false, - "bojTagId": 201, - "problemCount": 5, - "displayNames": [ - { - "language": "en", - "name": "chordal graph", - "short": "chordal graph" - }, - { - "language": "ko", - "name": "현 그래프", - "short": "현 그래프" - }, - { - "language": "ja", - "name": "弦グラフ", - "short": "弦グラフ" - } - ], - "aliases": [] - }, - { - "key": "discrete_sqrt", - "isMeta": false, - "bojTagId": 147, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "이산 제곱근", - "short": "이산 제곱근" - }, - { - "language": "en", - "name": "discrete square root", - "short": "discrete square root" - }, - { - "language": "ja", - "name": "離散平方根", - "short": "離散平方根" - } - ], - "aliases": [ - { - "alias": "루트" - } - ] - }, - { - "key": "gradient_descent", - "isMeta": false, - "bojTagId": 208, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "경사 하강법", - "short": "경사 하강법" - }, - { - "language": "en", - "name": "gradient descent", - "short": "gradient descent" - }, - { - "language": "ja", - "name": "勾配降下法", - "short": "勾配降下法" - } - ], - "aliases": [] - }, - { - "key": "polynomial_interpolation", - "isMeta": false, - "bojTagId": 209, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "다항식 보간법", - "short": "다항식 보간법" - }, - { - "language": "en", - "name": "polynomial interpolation", - "short": "polynomial interpolation" - }, - { - "language": "ja", - "name": "多項式補間", - "short": "多項式補間" - } - ], - "aliases": [] - }, - { - "key": "lgv", - "isMeta": false, - "bojTagId": 214, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "린드스트롬–게셀–비엔노 보조정리", - "short": "lgv 보조정리" - }, - { - "language": "en", - "name": "lindström–gessel–viennot lemma", - "short": "lgv lemma" - }, - { - "language": "ja", - "name": "lindström–gessel–viennot lemma", - "short": "lgv lemma" - } - ], - "aliases": [] - }, - { - "key": "green", - "isMeta": false, - "bojTagId": 183, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "그린 정리", - "short": "그린" - }, - { - "language": "en", - "name": "green's theorem", - "short": "green's thm" - }, - { - "language": "ja", - "name": "グリーンの定理", - "short": "グリーン" - } - ], - "aliases": [] - }, - { - "key": "directed_mst", - "isMeta": false, - "bojTagId": 23, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "유향 최소 신장 트리", - "short": "유향 최소 신장 트리" - }, - { - "language": "en", - "name": "directed minimum spanning tree", - "short": "dmst" - }, - { - "language": "ja", - "name": "最小全域有向木", - "short": "dmst" - } - ], - "aliases": [ - { - "alias": "유향mst" - } - ] - }, - { - "key": "stoer_wagner", - "isMeta": false, - "bojTagId": 75, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "스토어–바그너", - "short": "스토어–바그너" - }, - { - "language": "en", - "name": "stoer–wagner", - "short": "stoer–wagner" - }, - { - "language": "ja", - "name": "stoer–wagner", - "short": "stoer–wagner" - } - ], - "aliases": [ - { - "alias": "stoer-wagner" - }, - { - "alias": "stoer-karger" - }, - { - "alias": "stoer" - }, - { - "alias": "wagner" - }, - { - "alias": "karger" - }, - { - "alias": "global min cut" - }, - { - "alias": "전역 최소 컷" - } - ] - }, - { - "key": "birthday", - "isMeta": false, - "bojTagId": 203, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "생일 문제", - "short": "생일" - }, - { - "language": "en", - "name": "birthday problem", - "short": "birthday" - }, - { - "language": "ja", - "name": "birthday problem", - "short": "birthday" - } - ], - "aliases": [ - { - "alias": "패러독스" - }, - { - "alias": "파라독스" - }, - { - "alias": "birthday" - } - ] - }, - { - "key": "majority_vote", - "isMeta": false, - "bojTagId": 160, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "보이어–무어 다수결 투표", - "short": "보이어–무어 다수결 투표" - }, - { - "language": "en", - "name": "boyer–moore majority vote", - "short": "majority vote" - }, - { - "language": "ja", - "name": "boyer–moore majority vote", - "short": "majority vote" - } - ], - "aliases": [] - }, - { - "key": "multipoint_evaluation", - "isMeta": false, - "bojTagId": 196, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "다중 대입값 계산", - "short": "다중 계산" - }, - { - "language": "en", - "name": "multipoint evaluation", - "short": "multipoint evaluation" - }, - { - "language": "ja", - "name": "多点評価", - "short": "多点評価" - } - ], - "aliases": [] - }, - { - "key": "lte", - "isMeta": false, - "bojTagId": 212, - "problemCount": 2, - "displayNames": [ - { - "language": "ko", - "name": "지수승강 보조정리", - "short": "지수승강" - }, - { - "language": "en", - "name": "lifting the exponent lemma", - "short": "lte lemma" - }, - { - "language": "ja", - "name": "lifting the exponent lemma", - "short": "lte lemma" - } - ], - "aliases": [] - }, - { - "key": "hackenbush", - "isMeta": false, - "bojTagId": 205, - "problemCount": 2, - "displayNames": [ - { - "language": "ko", - "name": "하켄부시 게임", - "short": "하켄부시 게임" - }, - { - "language": "en", - "name": "hackenbush", - "short": "hackenbush" - }, - { - "language": "ja", - "name": "hackenbush", - "short": "hackenbush" - } - ], - "aliases": [] - }, - { - "key": "rb_tree", - "isMeta": false, - "bojTagId": 94, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "레드-블랙 트리", - "short": "레드-블랙 트리" - }, - { - "language": "en", - "name": "red-black tree", - "short": "rb tree" - }, - { - "language": "ja", - "name": "red-black tree", - "short": "rb tree" - } - ], - "aliases": [ - { - "alias": "rb트리" - } - ] - }, - { - "key": "floor_sum", - "isMeta": false, - "bojTagId": 218, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "유리 등차수열의 내림 합", - "short": "유리 등차수열의 내림 합" - }, - { - "language": "en", - "name": "sum of floor of rational arithmetic sequence", - "short": "floor sum" - } - ], - "aliases": [] - }, - { - "key": "discrete_kth_root", - "isMeta": false, - "bojTagId": 149, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "이산 k제곱근", - "short": "이산 k제곱근" - }, - { - "language": "en", - "name": "discrete k-th root", - "short": "discrete k-th root" - }, - { - "language": "ja", - "name": "離散k平方根", - "short": "離散k平方根" - } - ], - "aliases": [ - { - "alias": "루트" - } - ] - }, - { - "key": "a_star", - "isMeta": false, - "bojTagId": 186, - "problemCount": 0, - "displayNames": [ - { - "language": "ko", - "name": "a*", - "short": "a*" - }, - { - "language": "en", - "name": "a*", - "short": "a*" - }, - { - "language": "ja", - "name": "a*", - "short": "a*" - } - ], - "aliases": [ - { - "alias": "에이" - }, - { - "alias": "에이스타" - } - ] - } - ] -} \ No newline at end of file diff --git a/tools/runserver.sh b/tools/runserver.sh deleted file mode 100755 index 0ff1f5e..0000000 --- a/tools/runserver.sh +++ /dev/null @@ -1,6 +0,0 @@ -#/bin/bash - -REPOSITORY=$(dirname $(dirname $0)) - -cd $REPOSITORY/src -python manage.py runserver 0.0.0.0:80 --insecure > $REPOSITORY/.log 2>&1 & \ No newline at end of file diff --git a/tools/setup.sh b/tools/setup.sh deleted file mode 100755 index e4ab10c..0000000 --- a/tools/setup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#/bin/bash - -REPOSITORY=$(dirname $(dirname $0)) - -cd $REPOSITORY/src -python manage.py shell < $REPOSITORY/tools/db/setup_db.py From 7026b8f1242e613504a99f0cbfea19e9c386e8fa Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 22:03:34 +0900 Subject: [PATCH 212/552] =?UTF-8?q?feat(tle.admin):=20`admin`=EC=9D=98=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=ED=99=94=20=EB=B0=8F=20=EC=9D=BC=EB=B6=80=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EA=B4=80=EB=A6=AC=EC=9E=90=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/admin.py | 32 ---------------------------- app/tle/admin/__init__.py | 26 ++++++++++++++++++++++ app/tle/admin/problem.py | 28 ++++++++++++++++++++++++ app/tle/admin/problem_tag.py | 10 +++++++++ app/tle/admin/submission_language.py | 10 +++++++++ app/tle/admin/user.py | 17 +++++++++++++++ 6 files changed, 91 insertions(+), 32 deletions(-) delete mode 100644 app/tle/admin.py create mode 100644 app/tle/admin/__init__.py create mode 100644 app/tle/admin/problem.py create mode 100644 app/tle/admin/problem_tag.py create mode 100644 app/tle/admin/submission_language.py create mode 100644 app/tle/admin/user.py diff --git a/app/tle/admin.py b/app/tle/admin.py deleted file mode 100644 index 9b97433..0000000 --- a/app/tle/admin.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin as BaseUserAdmin - -from tle.models import * - - -@admin.register(User) -class UserAdmin(BaseUserAdmin): - fieldsets = [ - *BaseUserAdmin.fieldsets, - (None, {'fields': [ - 'profile_image', - 'boj_username', - 'boj_tier', - 'boj_tier_updated_at', - ]}), - ] - - -admin.site.register([ - Crew, - CrewActivity, - CrewActivityProblem, - CrewApplicant, - CrewMember, - Problem, - ProblemAnalysis, - ProblemTag, - Submission, - SubmissionComment, - SubmissionLanguage, -]) diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py new file mode 100644 index 0000000..4bab389 --- /dev/null +++ b/app/tle/admin/__init__.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from tle.admin.user import UserModelAdmin +from tle.admin.problem import ProblemModelAdmin +from tle.admin.problem_tag import ProblemTagModelAdmin +from tle.admin.submission_language import SubmissionLanguageModelAdmin +from tle.models import * + + +__all__ = ( + 'UserModelAdmin', + 'ProblemModelAdmin', + 'ProblemTagModelAdmin', + 'SubmissionLanguageModelAdmin', +) + +admin.site.register([ + Crew, + CrewActivity, + CrewActivityProblem, + CrewApplicant, + CrewMember, + ProblemAnalysis, + Submission, + SubmissionComment, +]) diff --git a/app/tle/admin/problem.py b/app/tle/admin/problem.py new file mode 100644 index 0000000..e8b9218 --- /dev/null +++ b/app/tle/admin/problem.py @@ -0,0 +1,28 @@ +from django.contrib import admin, messages +from django.utils.translation import ngettext + +from tle.models import Problem + + +@admin.register(Problem) +class ProblemModelAdmin(admin.ModelAdmin): + list_display = ['title', 'created_by', 'created_at', 'updated_at'] + list_filter = ['created_by', 'created_at', 'updated_at'] + search_fields = ['title', 'created_by__username'] + ordering = ['-created_at'] + actions = ['set_creator'] + + @admin.action(description="set 'created_by' of selected problems to current user") + def set_creator(self, request, queryset): + user = request.user + updated = queryset.update(created_by=user) + self.message_user( + request, + ngettext( + "%d story was successfully marked as published.", + "%d stories were successfully marked as published.", + updated, + ) + % updated, + messages.SUCCESS, + ) diff --git a/app/tle/admin/problem_tag.py b/app/tle/admin/problem_tag.py new file mode 100644 index 0000000..e1ae6bf --- /dev/null +++ b/app/tle/admin/problem_tag.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from tle.models import ProblemTag + + +@admin.register(ProblemTag) +class ProblemTagModelAdmin(admin.ModelAdmin): + list_display = ['parent', 'key', 'name_ko', 'name_en'] + search_fields = ['parent', 'key', 'name_ko', 'name_en'] + ordering = ['key'] diff --git a/app/tle/admin/submission_language.py b/app/tle/admin/submission_language.py new file mode 100644 index 0000000..bc539f7 --- /dev/null +++ b/app/tle/admin/submission_language.py @@ -0,0 +1,10 @@ +from django.contrib import admin + +from tle.models import SubmissionLanguage + + +@admin.register(SubmissionLanguage) +class SubmissionLanguageModelAdmin(admin.ModelAdmin): + list_display = ['key', 'name', 'extension'] + search_fields = ['key', 'name', 'extension'] + ordering = ['key'] diff --git a/app/tle/admin/user.py b/app/tle/admin/user.py new file mode 100644 index 0000000..6822ee2 --- /dev/null +++ b/app/tle/admin/user.py @@ -0,0 +1,17 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin + +from tle.models import User + + +@admin.register(User) +class UserModelAdmin(UserAdmin): + fieldsets = [ + *UserAdmin.fieldsets, + (None, {'fields': [ + 'profile_image', + 'boj_username', + 'boj_tier', + 'boj_tier_updated_at', + ]}), + ] From 826897fa3e4d0e55e7795c1fd63741da124ffa12 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 22:13:18 +0900 Subject: [PATCH 213/552] refactor(tle.services): rename service `problem_analyser` -> `analysis` --- app/tle/services/{problem_analyser => analysis}/__init__.py | 2 +- .../problem_analyser.py => analysis/analyser.py} | 2 +- app/tle/services/{problem_analyser => analysis}/dto.py | 0 app/tle/services/{problem_analyser => analysis}/llm/__init__.py | 0 app/tle/services/{problem_analyser => analysis}/llm/gemini.py | 2 +- app/tle/services/{problem_analyser => analysis}/llm/gpt.py | 2 +- 6 files changed, 4 insertions(+), 4 deletions(-) rename app/tle/services/{problem_analyser => analysis}/__init__.py (74%) rename app/tle/services/{problem_analyser/problem_analyser.py => analysis/analyser.py} (89%) rename app/tle/services/{problem_analyser => analysis}/dto.py (100%) rename app/tle/services/{problem_analyser => analysis}/llm/__init__.py (100%) rename app/tle/services/{problem_analyser => analysis}/llm/gemini.py (77%) rename app/tle/services/{problem_analyser => analysis}/llm/gpt.py (76%) diff --git a/app/tle/services/problem_analyser/__init__.py b/app/tle/services/analysis/__init__.py similarity index 74% rename from app/tle/services/problem_analyser/__init__.py rename to app/tle/services/analysis/__init__.py index ac1fa7d..68d75e0 100644 --- a/app/tle/services/problem_analyser/__init__.py +++ b/app/tle/services/analysis/__init__.py @@ -1,4 +1,4 @@ -from .problem_analyser import ProblemAnalyser +from .analyser import ProblemAnalyser from .dto import ProblemDTO, ProblemAnalysisDTO diff --git a/app/tle/services/problem_analyser/problem_analyser.py b/app/tle/services/analysis/analyser.py similarity index 89% rename from app/tle/services/problem_analyser/problem_analyser.py rename to app/tle/services/analysis/analyser.py index 6068ef9..0c5aa32 100644 --- a/app/tle/services/problem_analyser/problem_analyser.py +++ b/app/tle/services/analysis/analyser.py @@ -1,4 +1,4 @@ -from tle.services.dto import * +from tle.services.analysis.dto import * class ProblemAnalyser: diff --git a/app/tle/services/problem_analyser/dto.py b/app/tle/services/analysis/dto.py similarity index 100% rename from app/tle/services/problem_analyser/dto.py rename to app/tle/services/analysis/dto.py diff --git a/app/tle/services/problem_analyser/llm/__init__.py b/app/tle/services/analysis/llm/__init__.py similarity index 100% rename from app/tle/services/problem_analyser/llm/__init__.py rename to app/tle/services/analysis/llm/__init__.py diff --git a/app/tle/services/problem_analyser/llm/gemini.py b/app/tle/services/analysis/llm/gemini.py similarity index 77% rename from app/tle/services/problem_analyser/llm/gemini.py rename to app/tle/services/analysis/llm/gemini.py index b685e66..ac3b9d0 100644 --- a/app/tle/services/problem_analyser/llm/gemini.py +++ b/app/tle/services/analysis/llm/gemini.py @@ -1,4 +1,4 @@ -from tle.services.problem_analyser import * +from tle.services.analysis import * class GeminiProblemAnalyser(ProblemAnalyser): diff --git a/app/tle/services/problem_analyser/llm/gpt.py b/app/tle/services/analysis/llm/gpt.py similarity index 76% rename from app/tle/services/problem_analyser/llm/gpt.py rename to app/tle/services/analysis/llm/gpt.py index 13d239a..81012e7 100644 --- a/app/tle/services/problem_analyser/llm/gpt.py +++ b/app/tle/services/analysis/llm/gpt.py @@ -1,4 +1,4 @@ -from tle.services.problem_analyser import * +from tle.services.analysis import * class GPTProblemAnalyser(ProblemAnalyser): From 64cea077e3e09465fac32f4b4d8ec67d366d55cc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 17 Jul 2024 22:15:49 +0900 Subject: [PATCH 214/552] feat(tle.services): create service `data` and `data.parser` --- app/tle/services/data/__init__.py | 0 app/tle/services/data/parser/__init__.py | 2 + app/tle/services/data/parser/base.py | 20 + app/tle/services/data/parser/parsers.py | 43 + app/tle/services/data/parser/runner.py | 42 + app/tle/services/data/raw-languages.json | 46 + app/tle/services/data/raw-problems.json | 81 + app/tle/services/data/raw-tags.json | 5944 ++++++++++++++++++++++ 8 files changed, 6178 insertions(+) create mode 100644 app/tle/services/data/__init__.py create mode 100644 app/tle/services/data/parser/__init__.py create mode 100644 app/tle/services/data/parser/base.py create mode 100644 app/tle/services/data/parser/parsers.py create mode 100644 app/tle/services/data/parser/runner.py create mode 100644 app/tle/services/data/raw-languages.json create mode 100644 app/tle/services/data/raw-problems.json create mode 100644 app/tle/services/data/raw-tags.json diff --git a/app/tle/services/data/__init__.py b/app/tle/services/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/tle/services/data/parser/__init__.py b/app/tle/services/data/parser/__init__.py new file mode 100644 index 0000000..720f234 --- /dev/null +++ b/app/tle/services/data/parser/__init__.py @@ -0,0 +1,2 @@ +from tle.services.data.parser.parsers import ModelParser +from tle.services.data.parser.runner import parse diff --git a/app/tle/services/data/parser/base.py b/app/tle/services/data/parser/base.py new file mode 100644 index 0000000..9f1722a --- /dev/null +++ b/app/tle/services/data/parser/base.py @@ -0,0 +1,20 @@ +import json +import typing + + +T = typing.TypeVar('T') + + +class ModelParser(typing.Generic[T]): + def parse_json(self, file: str, many=False) -> typing.List[T]: + with open(file) as f: + data = json.load(f) + return self.parse(data, many=many) + + def parse(self, data: dict, many=False) -> typing.List[T]: + if not many: + return [self.perform_parse(data)] + return [self.perform_parse(item) for item in data['items']] + + def perform_parse(self, item: dict) -> T: + raise NotImplementedError diff --git a/app/tle/services/data/parser/parsers.py b/app/tle/services/data/parser/parsers.py new file mode 100644 index 0000000..36330db --- /dev/null +++ b/app/tle/services/data/parser/parsers.py @@ -0,0 +1,43 @@ +from datetime import datetime + +from tle.models import * +from tle.services.data.parser.base import ModelParser + + +class ProblemParser(ModelParser[Problem]): + def perform_parse(self, item: dict) -> Problem: + return Problem.objects.create( + title=item['title'], + link=item['link'], + description=item['description'], + input_description=item['input_description'], + output_description=item['output_description'], + time_limit=item['time_limit'], + memory_limit=item['memory_limit'], + created_at=datetime.fromisoformat(item['created_at']), + updated_at=datetime.fromisoformat(item['updated_at']), + ) + + +class ProblemTagParser(ModelParser[ProblemTag]): + def perform_parse(self, item: dict) -> ProblemTag: + return ProblemTag.objects.create( + pk=item['bojTagId'], + parent=None, + key=item['key'], + name_ko=self._find(item['displayNames'], 'ko')['name'], + name_en=self._find(item['displayNames'], 'en')['name'], + ) + + def _find(self, display_names: list[dict], language: str) -> dict: + return next(filter(lambda x: x['language'] == language, display_names)) + + +class SubmissionLanguageParser(ModelParser[SubmissionLanguage]): + def perform_parse(self, item: dict) -> SubmissionLanguage: + return SubmissionLanguage.objects.create( + pk=item['bojId'], + key=item['key'], + name=item['displayName'], + extension=item['extension'], + ) diff --git a/app/tle/services/data/parser/runner.py b/app/tle/services/data/parser/runner.py new file mode 100644 index 0000000..641b49b --- /dev/null +++ b/app/tle/services/data/parser/runner.py @@ -0,0 +1,42 @@ +import typing + +from django.conf import settings + +from tle.models import * +from tle.services.data.parser.parsers import * + + +DATA_DIR = settings.BASE_DIR / 'tle/services/data' + + +T = typing.TypeVar('T') + + +def get_model_parser(model_class: T) -> ModelParser[T]: + PARSERS = { + Problem: ProblemParser, + ProblemTag: ProblemTagParser, + SubmissionLanguage: SubmissionLanguageParser, + } + if model_class not in PARSERS: + raise NotImplementedError( + f'Parser for {model_class} is not implemented') + return PARSERS[model_class]() + + +def get_model_default_json_file(model_class: T) -> str: + FILES = { + Problem: DATA_DIR / 'raw-problems.json', + ProblemTag: DATA_DIR / 'raw-tags.json', + SubmissionLanguage: DATA_DIR / 'raw-languages.json', + } + if model_class not in FILES: + raise NotImplementedError(f'Default JSON file for {model_class} is not implemented') + return FILES[model_class] + + +def parse(model_class: T, file: str = None) -> typing.List[T]: + parser = get_model_parser(model_class) + if file is None: + file = get_model_default_json_file(model_class) + return parser.parse_json(file, many=True) diff --git a/app/tle/services/data/raw-languages.json b/app/tle/services/data/raw-languages.json new file mode 100644 index 0000000..9537e6a --- /dev/null +++ b/app/tle/services/data/raw-languages.json @@ -0,0 +1,46 @@ +{ + "items": [ + { + "key": "nodejs", + "bojId": 17, + "displayName": "Node.js", + "extension": ".js" + }, + { + "key": "kotlin", + "bojId": 69, + "displayName": "Kotlin", + "extension": ".kt" + }, + { + "key": "swift", + "bojId": 74, + "displayName": "Swift", + "extension": ".swift" + }, + { + "key": "cpp", + "bojId": 1001, + "displayName": "C++", + "extension": ".cpp" + }, + { + "key": "java", + "bojId": 1002, + "displayName": "Java", + "extension": ".java" + }, + { + "key": "python", + "bojId": 1003, + "displayName": "Python", + "extension": ".py" + }, + { + "key": "c", + "bojId": 1004, + "displayName": "C", + "extension": ".c" + } + ] +} \ No newline at end of file diff --git a/app/tle/services/data/raw-problems.json b/app/tle/services/data/raw-problems.json new file mode 100644 index 0000000..8d6f000 --- /dev/null +++ b/app/tle/services/data/raw-problems.json @@ -0,0 +1,81 @@ +{ + "items": [ + { + "title": "A+B", + "link": "https://www.acmicpc.net/problem/1000", + "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", + "output_description": "첫째 줄에 A+B를 출력한다.", + "time_limit": 1.0, + "memory_limit": 128.0, + "created_at": "2024-06-03 13:24:06.988383", + "updated_at": "2024-06-03 13:24:06.988446" + }, + { + "title": "피보나치 함수", + "link": "https://www.acmicpc.net/problem/1003", + "description": "다음 소스는 N번째 피보나치 수를 구하는 C++ 함수이다.\r\n\r\nint fibonacci(int n) {\r\n if (n == 0) {\r\n printf(\"0\");\r\n return 0;\r\n } else if (n == 1) {\r\n printf(\"1\");\r\n return 1;\r\n } else {\r\n return fibonacci(n‐1) + fibonacci(n‐2);\r\n }\r\n}\r\nfibonacci(3)을 호출하면 다음과 같은 일이 일어난다.\r\n\r\nfibonacci(3)은 fibonacci(2)와 fibonacci(1) (첫 번째 호출)을 호출한다.\r\nfibonacci(2)는 fibonacci(1) (두 번째 호출)과 fibonacci(0)을 호출한다.\r\n두 번째 호출한 fibonacci(1)은 1을 출력하고 1을 리턴한다.\r\nfibonacci(0)은 0을 출력하고, 0을 리턴한다.\r\nfibonacci(2)는 fibonacci(1)과 fibonacci(0)의 결과를 얻고, 1을 리턴한다.\r\n첫 번째 호출한 fibonacci(1)은 1을 출력하고, 1을 리턴한다.\r\nfibonacci(3)은 fibonacci(2)와 fibonacci(1)의 결과를 얻고, 2를 리턴한다.\r\n1은 2번 출력되고, 0은 1번 출력된다. N이 주어졌을 때, fibonacci(N)을 호출했을 때, 0과 1이 각각 몇 번 출력되는지 구하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 테스트 케이스의 개수 T가 주어진다.\r\n\r\n각 테스트 케이스는 한 줄로 이루어져 있고, N이 주어진다. N은 40보다 작거나 같은 자연수 또는 0이다.", + "output_description": "각 테스트 케이스마다 0이 출력되는 횟수와 1이 출력되는 횟수를 공백으로 구분해서 출력한다.", + "time_limit": 0.25, + "memory_limit": 128.0, + "created_at": "2024-06-03 18:24:21.358190", + "updated_at": "2024-06-03 18:24:21.358210" + }, + { + "title": "평점 변환", + "link": "https://www.acmicpc.net/problem/31799", + "description": "2023학년도까지 대구과고에서는 학생들의 한 학기 동안의 성적에 따라 A+, A0, A-, B+, B0, B-, C+, C0, C-의 아홉 가지 평어 가운데 하나를 부여하였다. 그러나 상대평가 중심의 평어 체제는 학생들 간의 과도한 경쟁을 유도하는 부작용이 있었다. 그래서 2024학년도부터는 B(Beginning), D(Developing), P(Proficient), E(Exceeding)의 네 가지 평어 가운데 하나를 부여하는 방식으로 체제를 바꿀 계획이다. 새로운 평어 체제는 상대평가 기간이 아닌 개인의 성장 과정에 따라 평어가 부여되는 방식이므로 기존 평어 체제의 문제점을 해결할 것으로 기대하고 있다.\r\n\r\n대구과고 학생들은 2023학년도 이전과 2024학년도 이후의 평어 체제가 완전히 달라서 자신의 발전 과정을 정확하게 알기 어려워졌다. 이에 따라 2023학년도 이전의 평어를 새로운 평어 체제에 맞추어 변환하는 공식적인 기준을 발표하였다.\r\n\r\n평어가 C+, C0, C- 가운데 하나이면, 새로운 평어는 B이다.\r\n평어가 B0, B- 가운데 하나이면\r\n첫 학기이거나 이전 학기의 평어가 C+, C0, C- 가운데 하나이면, 새로운 평어는 D이다.\r\n이전 학기의 평어가 A+, A0, A-, B+, B0, B- 가운데 하나이면, 새로운 평어는 B이다.\r\n평어가 A-, B+ 가운데 하나이면\r\n첫 학기이거나 이전 학기의 평어가 B0, B-, C+, C0, C- 가운데 하나이면, 새로운 평어는 P이다.\r\n이전 학기의 평어가 A+, A0, A-, B+ 가운데 하나이면, 새로운 평어는 D이다.\r\n평어가 A0이면\r\n첫 학기이거나 이전 학기의 평어가 A-, B+, B0, B-, C+, C0, C- 가운데 하나이면, 새로운 평어는 E이다.\r\n이전 학기의 평어가 A+, A0 가운데 하나이면, 새로운 평어는 P이다.\r\n평어가 A+이면 새로운 평어는 E이다.\r\n대구과고에 다니는 은성이는 기존 평어 체제로 부여되었던 자신의 $N$학기 동안의 평어를 새로운 평어 체제에 맞게 변환하고 싶다. 하지만 평어 변환 기준이 너무 복잡해 여러분에게 대신 이 일을 맡기려고 한다. $N$학기 동안의 평어가 첫 학기부터 $N$번째 학기까지 순서대로 공백 없이 주어질 때, 새로운 평어 체제에 맞게 변환한 결과를 출력하는 프로그램을 작성하라. 단, 은성이는 A0, B0, C0에서 실수로 0을 생략하여 'A', 'B', 'C'와 같이 적을 때도 있다고 한다.", + "input_description": "첫 번째 줄에 은성이가 대구과고에 다닌 학기의 수 $N$이 주어진다.\r\n\r\n두 번째 줄에 은성이의 $N$학기 동안의 평어를 공백 없이 순서대로 나열한 문자열이 주어진다.\r\n\r\n $1\\le N\\le 200\\,000$", + "output_description": "은성이의 $N$학기 동안의 평어를 새로운 체제에 맞게 변환한 결과를 첫 학기부터 공백 없이 순서대로 나열한 길이 $N$의 문자열을 출력한다.", + "time_limit": 1.0, + "memory_limit": 1024.0, + "created_at": "2024-06-16 13:51:26.551732", + "updated_at": "2024-06-16 13:51:26.551746" + }, + { + "title": "CCW", + "link": "https://www.acmicpc.net/problem/11758", + "description": "2차원 좌표 평면 위에 있는 점 3개 P1, P2, P3가 주어진다. P1, P2, P3를 순서대로 이은 선분이 어떤 방향을 이루고 있는지 구하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 P1의 (x1, y1), 둘째 줄에 P2의 (x2, y2), 셋째 줄에 P3의 (x3, y3)가 주어진다. (-10,000 ≤ x1, y1, x2, y2, x3, y3 ≤ 10,000) 모든 좌표는 정수이다. P1, P2, P3의 좌표는 서로 다르다.", + "output_description": "P1, P2, P3를 순서대로 이은 선분이 반시계 방향을 나타내면 1, 시계 방향이면 -1, 일직선이면 0을 출력한다.", + "time_limit": 1.0, + "memory_limit": 256.0, + "created_at": "2024-06-16 13:54:02.295495", + "updated_at": "2024-06-16 13:54:02.295540" + }, + { + "title": "접두사 찾기", + "link": "https://www.acmicpc.net/problem/14426", + "description": "문자열 S의 접두사란 S의 가장 앞에서부터 부분 문자열을 의미한다. 예를 들어, S = \"codeplus\"의 접두사는 \"code\", \"co\", \"codepl\", \"codeplus\"가 있고, \"plus\", \"s\", \"cude\", \"crud\"는 접두사가 아니다.\r\n\r\n총 N개의 문자열로 이루어진 집합 S가 주어진다.\r\n\r\n입력으로 주어지는 M개의 문자열 중에서 집합 S에 포함되어 있는 문자열 중 적어도 하나의 접두사인 것의 개수를 구하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 문자열의 개수 N과 M (1 ≤ N ≤ 10,000, 1 ≤ M ≤ 10,000)이 주어진다.\r\n\r\n다음 N개의 줄에는 집합 S에 포함되어 있는 문자열이 주어진다.\r\n\r\n다음 M개의 줄에는 검사해야 하는 문자열이 주어진다.\r\n\r\n입력으로 주어지는 문자열은 알파벳 소문자로만 이루어져 있으며, 길이는 500을 넘지 않는다. 집합 S에 같은 문자열이 여러 번 주어지는 경우는 없다.", + "output_description": "첫째 줄에 M개의 문자열 중에 총 몇 개가 포함되어 있는 문자열 중 적어도 하나의 접두사인지 출력한다.", + "time_limit": 1.0, + "memory_limit": 1536.0, + "created_at": "2024-06-16 13:55:49.874302", + "updated_at": "2024-06-16 13:55:49.874324" + }, + { + "title": "벽 부수고 이동하기", + "link": "https://www.acmicpc.net/problem/2206", + "description": "N×M의 행렬로 표현되는 맵이 있다. 맵에서 0은 이동할 수 있는 곳을 나타내고, 1은 이동할 수 없는 벽이 있는 곳을 나타낸다. 당신은 (1, 1)에서 (N, M)의 위치까지 이동하려 하는데, 이때 최단 경로로 이동하려 한다. 최단경로는 맵에서 가장 적은 개수의 칸을 지나는 경로를 말하는데, 이때 시작하는 칸과 끝나는 칸도 포함해서 센다.\r\n\r\n만약에 이동하는 도중에 한 개의 벽을 부수고 이동하는 것이 좀 더 경로가 짧아진다면, 벽을 한 개 까지 부수고 이동하여도 된다.\r\n\r\n한 칸에서 이동할 수 있는 칸은 상하좌우로 인접한 칸이다.\r\n\r\n맵이 주어졌을 때, 최단 경로를 구해 내는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 1,000)이 주어진다. 다음 N개의 줄에 M개의 숫자로 맵이 주어진다. (1, 1)과 (N, M)은 항상 0이라고 가정하자.", + "output_description": "첫째 줄에 최단 거리를 출력한다. 불가능할 때는 -1을 출력한다.", + "time_limit": 2.0, + "memory_limit": 192.0, + "created_at": "2024-06-16 13:56:30.174235", + "updated_at": "2024-06-16 13:56:30.174268" + }, + { + "title": "용액", + "link": "https://www.acmicpc.net/problem/2467", + "description": "KOI 부설 과학연구소에서는 많은 종류의 산성 용액과 알칼리성 용액을 보유하고 있다. 각 용액에는 그 용액의 특성을 나타내는 하나의 정수가 주어져있다. 산성 용액의 특성값은 1부터 1,000,000,000까지의 양의 정수로 나타내고, 알칼리성 용액의 특성값은 -1부터 -1,000,000,000까지의 음의 정수로 나타낸다.\r\n\r\n같은 양의 두 용액을 혼합한 용액의 특성값은 혼합에 사용된 각 용액의 특성값의 합으로 정의한다. 이 연구소에서는 같은 양의 두 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들려고 한다. \r\n\r\n예를 들어, 주어진 용액들의 특성값이 [-99, -2, -1, 4, 98]인 경우에는 특성값이 -99인 용액과 특성값이 98인 용액을 혼합하면 특성값이 -1인 용액을 만들 수 있고, 이 용액의 특성값이 0에 가장 가까운 용액이다. 참고로, 두 종류의 알칼리성 용액만으로나 혹은 두 종류의 산성 용액만으로 특성값이 0에 가장 가까운 혼합 용액을 만드는 경우도 존재할 수 있다.\r\n\r\n산성 용액과 알칼리성 용액의 특성값이 정렬된 순서로 주어졌을 때, 이 중 두 개의 서로 다른 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들어내는 두 용액을 찾는 프로그램을 작성하시오.", + "input_description": "첫째 줄에는 전체 용액의 수 N이 입력된다. N은 2 이상 100,000 이하의 정수이다. 둘째 줄에는 용액의 특성값을 나타내는 N개의 정수가 빈칸을 사이에 두고 오름차순으로 입력되며, 이 수들은 모두 -1,000,000,000 이상 1,000,000,000 이하이다. N개의 용액들의 특성값은 모두 서로 다르고, 산성 용액만으로나 알칼리성 용액만으로 입력이 주어지는 경우도 있을 수 있다.", + "output_description": "첫째 줄에 특성값이 0에 가장 가까운 용액을 만들어내는 두 용액의 특성값을 출력한다. 출력해야 하는 두 용액은 특성값의 오름차순으로 출력한다. 특성값이 0에 가장 가까운 용액을 만들어내는 경우가 두 개 이상일 경우에는 그 중 아무것이나 하나를 출력한다.", + "time_limit": 1.0, + "memory_limit": 128.0, + "created_at": "2024-06-16 13:57:24.447647", + "updated_at": "2024-06-16 13:57:24.447661" + } + ] +} \ No newline at end of file diff --git a/app/tle/services/data/raw-tags.json b/app/tle/services/data/raw-tags.json new file mode 100644 index 0000000..d21c414 --- /dev/null +++ b/app/tle/services/data/raw-tags.json @@ -0,0 +1,5944 @@ +{ + "count": 206, + "items": [ + { + "key": "math", + "isMeta": false, + "bojTagId": 124, + "problemCount": 6212, + "displayNames": [ + { + "language": "ko", + "name": "수학", + "short": "수학" + }, + { + "language": "en", + "name": "mathematics", + "short": "math" + }, + { + "language": "ja", + "name": "数学", + "short": "数学" + } + ], + "aliases": [] + }, + { + "key": "implementation", + "isMeta": false, + "bojTagId": 102, + "problemCount": 5399, + "displayNames": [ + { + "language": "ko", + "name": "구현", + "short": "구현" + }, + { + "language": "en", + "name": "implementation", + "short": "impl" + }, + { + "language": "ja", + "name": "実装", + "short": "impl" + } + ], + "aliases": [] + }, + { + "key": "dp", + "isMeta": false, + "bojTagId": 25, + "problemCount": 3941, + "displayNames": [ + { + "language": "ko", + "name": "다이나믹 프로그래밍", + "short": "다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming", + "short": "dp" + }, + { + "language": "ja", + "name": "動的計画法", + "short": "dp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "동적 계획법" + }, + { + "alias": "다이나믹프로그래밍" + } + ] + }, + { + "key": "data_structures", + "isMeta": false, + "bojTagId": 175, + "problemCount": 3732, + "displayNames": [ + { + "language": "ko", + "name": "자료 구조", + "short": "자료 구조" + }, + { + "language": "en", + "name": "data structures", + "short": "ds" + }, + { + "language": "ja", + "name": "データ構造", + "short": "ds" + } + ], + "aliases": [ + { + "alias": "자료구조" + }, + { + "alias": "자구" + } + ] + }, + { + "key": "graphs", + "isMeta": false, + "bojTagId": 7, + "problemCount": 3600, + "displayNames": [ + { + "language": "ko", + "name": "그래프 이론", + "short": "그래프 이론" + }, + { + "language": "en", + "name": "graph theory", + "short": "graph" + }, + { + "language": "ja", + "name": "グラフ理論", + "short": "グラフ" + } + ], + "aliases": [ + { + "alias": "그래프이론" + }, + { + "alias": "그래프" + } + ] + }, + { + "key": "greedy", + "isMeta": false, + "bojTagId": 33, + "problemCount": 2461, + "displayNames": [ + { + "language": "ko", + "name": "그리디 알고리즘", + "short": "그리디 알고리즘" + }, + { + "language": "en", + "name": "greedy", + "short": "greedy" + }, + { + "language": "ja", + "name": "貪欲法", + "short": "貪欲法" + } + ], + "aliases": [ + { + "alias": "탐욕법" + } + ] + }, + { + "key": "string", + "isMeta": false, + "bojTagId": 158, + "problemCount": 2340, + "displayNames": [ + { + "language": "ko", + "name": "문자열", + "short": "문자열" + }, + { + "language": "en", + "name": "string", + "short": "string" + }, + { + "language": "ja", + "name": "文字列", + "short": "文字列" + } + ], + "aliases": [ + { + "alias": "스트링" + } + ] + }, + { + "key": "bruteforcing", + "isMeta": false, + "bojTagId": 125, + "problemCount": 2132, + "displayNames": [ + { + "language": "ko", + "name": "브루트포스 알고리즘", + "short": "브루트포스 알고리즘" + }, + { + "language": "en", + "name": "bruteforcing", + "short": "bruteforce" + }, + { + "language": "ja", + "name": "全探索", + "short": "全探索" + } + ], + "aliases": [ + { + "alias": "완전탐색" + }, + { + "alias": "완전 탐색" + }, + { + "alias": "브루트포스" + }, + { + "alias": "bruteforce" + }, + { + "alias": "brute force" + }, + { + "alias": "완탐" + } + ] + }, + { + "key": "graph_traversal", + "isMeta": false, + "bojTagId": 11, + "problemCount": 1960, + "displayNames": [ + { + "language": "ko", + "name": "그래프 탐색", + "short": "그래프 탐색" + }, + { + "language": "en", + "name": "graph traversal", + "short": "traversal" + }, + { + "language": "ja", + "name": "グラフの探索", + "short": "横断" + } + ], + "aliases": [ + { + "alias": "bfs" + }, + { + "alias": "dfs" + } + ] + }, + { + "key": "sorting", + "isMeta": false, + "bojTagId": 97, + "problemCount": 1817, + "displayNames": [ + { + "language": "ko", + "name": "정렬", + "short": "정렬" + }, + { + "language": "en", + "name": "sorting", + "short": "sorting" + }, + { + "language": "ja", + "name": "ソート", + "short": "ソート" + } + ], + "aliases": [] + }, + { + "key": "geometry", + "isMeta": false, + "bojTagId": 100, + "problemCount": 1474, + "displayNames": [ + { + "language": "ko", + "name": "기하학", + "short": "기하학" + }, + { + "language": "en", + "name": "geometry", + "short": "geom" + }, + { + "language": "ja", + "name": "幾何学", + "short": "幾何" + } + ], + "aliases": [] + }, + { + "key": "ad_hoc", + "isMeta": false, + "bojTagId": 109, + "problemCount": 1424, + "displayNames": [ + { + "language": "ko", + "name": "애드 혹", + "short": "애드 혹" + }, + { + "language": "en", + "name": "ad-hoc", + "short": "ad-hoc" + }, + { + "language": "ja", + "name": "アドホック", + "short": "アドホック" + } + ], + "aliases": [] + }, + { + "key": "number_theory", + "isMeta": false, + "bojTagId": 95, + "problemCount": 1399, + "displayNames": [ + { + "language": "ko", + "name": "정수론", + "short": "정수론" + }, + { + "language": "en", + "name": "number theory", + "short": "number theory" + }, + { + "language": "ja", + "name": "整数論", + "short": "整数論" + } + ], + "aliases": [] + }, + { + "key": "trees", + "isMeta": false, + "bojTagId": 120, + "problemCount": 1359, + "displayNames": [ + { + "language": "ko", + "name": "트리", + "short": "트리" + }, + { + "language": "en", + "name": "tree", + "short": "tree" + }, + { + "language": "ja", + "name": "木", + "short": "木" + } + ], + "aliases": [ + { + "alias": "trees" + } + ] + }, + { + "key": "segtree", + "isMeta": false, + "bojTagId": 65, + "problemCount": 1275, + "displayNames": [ + { + "language": "ko", + "name": "세그먼트 트리", + "short": "세그먼트 트리" + }, + { + "language": "en", + "name": "segment tree", + "short": "segtree" + }, + { + "language": "ja", + "name": "セグメント木", + "short": "セグ木" + } + ], + "aliases": [ + { + "alias": "구간트리" + }, + { + "alias": "세그트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "binary_search", + "isMeta": false, + "bojTagId": 12, + "problemCount": 1194, + "displayNames": [ + { + "language": "ko", + "name": "이분 탐색", + "short": "이분 탐색" + }, + { + "language": "en", + "name": "binary search", + "short": "binary search" + }, + { + "language": "ja", + "name": "二分探索", + "short": "二分探索" + } + ], + "aliases": [ + { + "alias": "이분탐색" + }, + { + "alias": "이진탐색" + } + ] + }, + { + "key": "arithmetic", + "isMeta": false, + "bojTagId": 121, + "problemCount": 1093, + "displayNames": [ + { + "language": "ko", + "name": "사칙연산", + "short": "사칙연산" + }, + { + "language": "en", + "name": "arithmetic", + "short": "arithmetic" + }, + { + "language": "ja", + "name": "算数", + "short": "算数" + } + ], + "aliases": [ + { + "alias": "덧셈" + }, + { + "alias": "뺄셈" + }, + { + "alias": "곱셈" + }, + { + "alias": "나눗셈" + }, + { + "alias": "더하기" + }, + { + "alias": "빼기" + }, + { + "alias": "곱하기" + }, + { + "alias": "나누기" + } + ] + }, + { + "key": "simulation", + "isMeta": false, + "bojTagId": 141, + "problemCount": 1054, + "displayNames": [ + { + "language": "ko", + "name": "시뮬레이션", + "short": "시뮬레이션" + }, + { + "language": "en", + "name": "simulation", + "short": "simulation" + }, + { + "language": "ja", + "name": "シミュレーション", + "short": "シミュレーション" + } + ], + "aliases": [] + }, + { + "key": "constructive", + "isMeta": false, + "bojTagId": 128, + "problemCount": 970, + "displayNames": [ + { + "language": "ko", + "name": "해 구성하기", + "short": "해 구성하기" + }, + { + "language": "en", + "name": "constructive", + "short": "constructive" + }, + { + "language": "ja", + "name": "構成的", + "short": "構成的" + } + ], + "aliases": [ + { + "alias": "constructive" + }, + { + "alias": "컨스트럭티브" + }, + { + "alias": "구성적" + } + ] + }, + { + "key": "bfs", + "isMeta": false, + "bojTagId": 126, + "problemCount": 962, + "displayNames": [ + { + "language": "ko", + "name": "너비 우선 탐색", + "short": "너비 우선 탐색" + }, + { + "language": "en", + "name": "breadth-first search", + "short": "bfs" + }, + { + "language": "ja", + "name": "幅優先検索", + "short": "bfs" + } + ], + "aliases": [ + { + "alias": "breadthfirst" + }, + { + "alias": "breadth first" + } + ] + }, + { + "key": "prefix_sum", + "isMeta": false, + "bojTagId": 139, + "problemCount": 925, + "displayNames": [ + { + "language": "ko", + "name": "누적 합", + "short": "누적 합" + }, + { + "language": "en", + "name": "prefix sum", + "short": "prefix sum" + }, + { + "language": "ja", + "name": "累積和", + "short": "累積和" + } + ], + "aliases": [ + { + "alias": "구간합" + }, + { + "alias": "부분합" + }, + { + "alias": "rangesum" + } + ] + }, + { + "key": "combinatorics", + "isMeta": false, + "bojTagId": 6, + "problemCount": 879, + "displayNames": [ + { + "language": "ko", + "name": "조합론", + "short": "조합론" + }, + { + "language": "en", + "name": "combinatorics", + "short": "combinatorics" + }, + { + "language": "ja", + "name": "組み合わせ", + "short": "組み合わせ" + } + ], + "aliases": [ + { + "alias": "combination" + }, + { + "alias": "permutation" + }, + { + "alias": "probability" + }, + { + "alias": "확률" + }, + { + "alias": "순열" + } + ] + }, + { + "key": "case_work", + "isMeta": false, + "bojTagId": 137, + "problemCount": 838, + "displayNames": [ + { + "language": "ko", + "name": "많은 조건 분기", + "short": "많은 조건 분기" + }, + { + "language": "en", + "name": "case work", + "short": "case work" + }, + { + "language": "ja", + "name": "ケースワーク", + "short": "ケースワーク" + } + ], + "aliases": [ + { + "alias": "케이스" + }, + { + "alias": "케이스워크" + }, + { + "alias": "케이스 워크" + } + ] + }, + { + "key": "dfs", + "isMeta": false, + "bojTagId": 127, + "problemCount": 795, + "displayNames": [ + { + "language": "ko", + "name": "깊이 우선 탐색", + "short": "깊이 우선 탐색" + }, + { + "language": "en", + "name": "depth-first search", + "short": "dfs" + }, + { + "language": "ja", + "name": "深さ優先探索", + "short": "dfs" + } + ], + "aliases": [ + { + "alias": "depth first" + }, + { + "alias": "depthfirst" + } + ] + }, + { + "key": "shortest_path", + "isMeta": false, + "bojTagId": 215, + "problemCount": 754, + "displayNames": [ + { + "language": "ko", + "name": "최단 경로", + "short": "최단 경로" + }, + { + "language": "en", + "name": "shortest path", + "short": "shortest path" + }, + { + "language": "ja", + "name": "最短経路", + "short": "最短経路" + } + ], + "aliases": [] + }, + { + "key": "bitmask", + "isMeta": false, + "bojTagId": 14, + "problemCount": 704, + "displayNames": [ + { + "language": "ko", + "name": "비트마스킹", + "short": "비트마스킹" + }, + { + "language": "en", + "name": "bitmask", + "short": "bitmask" + }, + { + "language": "ja", + "name": "ビット表現", + "short": "ビット表現" + } + ], + "aliases": [ + { + "alias": "비트필드" + }, + { + "alias": "비트마스크" + } + ] + }, + { + "key": "hash_set", + "isMeta": false, + "bojTagId": 136, + "problemCount": 620, + "displayNames": [ + { + "language": "ko", + "name": "해시를 사용한 집합과 맵", + "short": "해시를 사용한 집합과 맵" + }, + { + "language": "en", + "name": "set / map by hashing", + "short": "hashset" + }, + { + "language": "ja", + "name": "ハッシュ化によるセット・マップ", + "short": "hashset" + } + ], + "aliases": [ + { + "alias": "집합" + }, + { + "alias": "맵" + }, + { + "alias": "셋" + }, + { + "alias": "딕셔너리" + }, + { + "alias": "dictionary" + }, + { + "alias": "map" + }, + { + "alias": "set" + }, + { + "alias": "해싱" + }, + { + "alias": "hashing" + }, + { + "alias": "dict" + } + ] + }, + { + "key": "dijkstra", + "isMeta": false, + "bojTagId": 22, + "problemCount": 572, + "displayNames": [ + { + "language": "ko", + "name": "데이크스트라", + "short": "데이크스트라" + }, + { + "language": "en", + "name": "dijkstra's", + "short": "dijkstra's" + }, + { + "language": "ja", + "name": "ダイクストラ法", + "short": "ダイクストラ法" + } + ], + "aliases": [ + { + "alias": "다익" + }, + { + "alias": "다익스트라" + }, + { + "alias": "데이크스트라" + } + ] + }, + { + "key": "backtracking", + "isMeta": false, + "bojTagId": 5, + "problemCount": 515, + "displayNames": [ + { + "language": "ko", + "name": "백트래킹", + "short": "백트래킹" + }, + { + "language": "en", + "name": "backtracking", + "short": "backtrack" + }, + { + "language": "ja", + "name": "バックトラック法", + "short": "バックトラック" + } + ], + "aliases": [ + { + "alias": "백트래킹" + }, + { + "alias": "퇴각검색" + }, + { + "alias": "퇴각 검색" + } + ] + }, + { + "key": "tree_set", + "isMeta": false, + "bojTagId": 74, + "problemCount": 485, + "displayNames": [ + { + "language": "ko", + "name": "트리를 사용한 집합과 맵", + "short": "트리를 사용한 집합과 맵" + }, + { + "language": "en", + "name": "set / map by trees", + "short": "treeset" + }, + { + "language": "ja", + "name": "木によるセット・マップ", + "short": "treeset" + } + ], + "aliases": [ + { + "alias": "집합" + }, + { + "alias": "맵" + }, + { + "alias": "셋" + }, + { + "alias": "딕셔너리" + }, + { + "alias": "dictionary" + }, + { + "alias": "map" + }, + { + "alias": "set" + }, + { + "alias": "bbst" + }, + { + "alias": "트리" + }, + { + "alias": "tree" + } + ] + }, + { + "key": "sweeping", + "isMeta": false, + "bojTagId": 106, + "problemCount": 465, + "displayNames": [ + { + "language": "ko", + "name": "스위핑", + "short": "스위핑" + }, + { + "language": "en", + "name": "sweeping", + "short": "sweeping" + }, + { + "language": "ja", + "name": "平面走査", + "short": "平面走査" + } + ], + "aliases": [ + { + "alias": "라인 스위핑" + } + ] + }, + { + "key": "disjoint_set", + "isMeta": false, + "bojTagId": 81, + "problemCount": 461, + "displayNames": [ + { + "language": "ko", + "name": "분리 집합", + "short": "분리 집합" + }, + { + "language": "en", + "name": "disjoint set", + "short": "dsu" + }, + { + "language": "ja", + "name": "素集合データ構造", + "short": "素集合データ構造" + } + ], + "aliases": [ + { + "alias": "union" + }, + { + "alias": "find" + }, + { + "alias": "유니온" + }, + { + "alias": "파인드" + }, + { + "alias": "dsu" + } + ] + }, + { + "key": "parsing", + "isMeta": false, + "bojTagId": 96, + "problemCount": 448, + "displayNames": [ + { + "language": "ko", + "name": "파싱", + "short": "파싱" + }, + { + "language": "en", + "name": "parsing", + "short": "parsing" + }, + { + "language": "ja", + "name": "パージング", + "short": "パージング" + } + ], + "aliases": [] + }, + { + "key": "priority_queue", + "isMeta": false, + "bojTagId": 59, + "problemCount": 419, + "displayNames": [ + { + "language": "ko", + "name": "우선순위 큐", + "short": "우선순위 큐" + }, + { + "language": "en", + "name": "priority queue", + "short": "priority queue" + }, + { + "language": "ja", + "name": "優先度付きキュー", + "short": "優先度付きキュー" + } + ], + "aliases": [ + { + "alias": "heap" + }, + { + "alias": "힙" + } + ] + }, + { + "key": "dp_tree", + "isMeta": false, + "bojTagId": 92, + "problemCount": 411, + "displayNames": [ + { + "language": "ko", + "name": "트리에서의 다이나믹 프로그래밍", + "short": "트리에서의 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming on trees", + "short": "tree dp" + }, + { + "language": "ja", + "name": "木上の動的計画法", + "short": "tree dp" + } + ], + "aliases": [ + { + "alias": "트리dp" + } + ] + }, + { + "key": "divide_and_conquer", + "isMeta": false, + "bojTagId": 24, + "problemCount": 406, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복", + "short": "분할 정복" + }, + { + "language": "en", + "name": "divide and conquer", + "short": "d&c" + }, + { + "language": "ja", + "name": "分割統治法", + "short": "分割統治法" + } + ], + "aliases": [ + { + "alias": "dnc" + } + ] + }, + { + "key": "two_pointer", + "isMeta": false, + "bojTagId": 80, + "problemCount": 376, + "displayNames": [ + { + "language": "ko", + "name": "두 포인터", + "short": "두 포인터" + }, + { + "language": "en", + "name": "two-pointer", + "short": "two-pointer" + }, + { + "language": "ja", + "name": "尺取り法", + "short": "尺取り" + } + ], + "aliases": [ + { + "alias": "투포인터" + }, + { + "alias": "인치웜" + }, + { + "alias": "inchworm" + }, + { + "alias": "twopointer" + } + ] + }, + { + "key": "stack", + "isMeta": false, + "bojTagId": 71, + "problemCount": 368, + "displayNames": [ + { + "language": "ko", + "name": "스택", + "short": "스택" + }, + { + "language": "en", + "name": "stack", + "short": "stack" + }, + { + "language": "ja", + "name": "スタック", + "short": "スタック" + } + ], + "aliases": [] + }, + { + "key": "parametric_search", + "isMeta": false, + "bojTagId": 170, + "problemCount": 367, + "displayNames": [ + { + "language": "ko", + "name": "매개 변수 탐색", + "short": "매개 변수 탐색" + }, + { + "language": "en", + "name": "parametric search", + "short": "parametric search" + }, + { + "language": "ja", + "name": "parametric search", + "short": "parametric search" + } + ], + "aliases": [ + { + "alias": "파라메트릭" + } + ] + }, + { + "key": "game_theory", + "isMeta": false, + "bojTagId": 140, + "problemCount": 353, + "displayNames": [ + { + "language": "ko", + "name": "게임 이론", + "short": "게임 이론" + }, + { + "language": "en", + "name": "game theory", + "short": "game theory" + }, + { + "language": "ja", + "name": "ゲーム理論", + "short": "ゲーム" + } + ], + "aliases": [ + { + "alias": "게임이론" + }, + { + "alias": "님" + }, + { + "alias": "nim" + } + ] + }, + { + "key": "flow", + "isMeta": false, + "bojTagId": 45, + "problemCount": 323, + "displayNames": [ + { + "language": "ko", + "name": "최대 유량", + "short": "최대 유량" + }, + { + "language": "en", + "name": "maximum flow", + "short": "flow" + }, + { + "language": "ja", + "name": "最大フロー", + "short": "flow" + } + ], + "aliases": [ + { + "alias": "dinic" + }, + { + "alias": "dinitz" + }, + { + "alias": "ford" + }, + { + "alias": "fulkerson" + }, + { + "alias": "fordfulkerson" + }, + { + "alias": "디닉" + }, + { + "alias": "디니츠" + }, + { + "alias": "포드풀커슨" + }, + { + "alias": "플로우" + } + ] + }, + { + "key": "primality_test", + "isMeta": false, + "bojTagId": 9, + "problemCount": 313, + "displayNames": [ + { + "language": "ko", + "name": "소수 판정", + "short": "소수 판정" + }, + { + "language": "en", + "name": "primality test", + "short": "primality test" + }, + { + "language": "ja", + "name": "素数性テスト", + "short": "素数性テスト" + } + ], + "aliases": [ + { + "alias": "소수" + }, + { + "alias": "소수판별" + }, + { + "alias": "소수판정" + }, + { + "alias": "prime" + } + ] + }, + { + "key": "probability", + "isMeta": false, + "bojTagId": 177, + "problemCount": 299, + "displayNames": [ + { + "language": "ko", + "name": "확률론", + "short": "확률론" + }, + { + "language": "en", + "name": "probability theory", + "short": "probability" + }, + { + "language": "ja", + "name": "確率論", + "short": "確率論" + } + ], + "aliases": [ + { + "alias": "expected value" + }, + { + "alias": "기대값" + }, + { + "alias": "기댓값" + } + ] + }, + { + "key": "lazyprop", + "isMeta": false, + "bojTagId": 66, + "problemCount": 296, + "displayNames": [ + { + "language": "ko", + "name": "느리게 갱신되는 세그먼트 트리", + "short": "느리게 갱신되는 세그먼트 트리" + }, + { + "language": "en", + "name": "segment tree with lazy propagation", + "short": "lazyprop" + }, + { + "language": "ja", + "name": "遅延評価セグメント木", + "short": "遅延評価セグ木" + } + ], + "aliases": [ + { + "alias": "레이지" + }, + { + "alias": "레이지프로퍼게이션" + }, + { + "alias": "레이지프로파게이션" + }, + { + "alias": "구간트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "dp_bitfield", + "isMeta": false, + "bojTagId": 87, + "problemCount": 295, + "displayNames": [ + { + "language": "ko", + "name": "비트필드를 이용한 다이나믹 프로그래밍", + "short": "비트필드를 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using bitfield", + "short": "bitfield dp" + }, + { + "language": "ja", + "name": "ビットを使用した動的計画法", + "short": "ビットdp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "비트마스크" + }, + { + "alias": "비트dp" + } + ] + }, + { + "key": "exponentiation_by_squaring", + "isMeta": false, + "bojTagId": 39, + "problemCount": 273, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복을 이용한 거듭제곱", + "short": "분할 정복을 이용한 거듭제곱" + }, + { + "language": "en", + "name": "exponentiation by squaring", + "short": "exponentiation by squaring" + }, + { + "language": "ja", + "name": "二乗法によるべき乗", + "short": "二乗法によるべき乗" + } + ], + "aliases": [ + { + "alias": "거듭제곱" + }, + { + "alias": "제곱" + }, + { + "alias": "power" + }, + { + "alias": "square" + } + ] + }, + { + "key": "arbitrary_precision", + "isMeta": false, + "bojTagId": 117, + "problemCount": 254, + "displayNames": [ + { + "language": "ko", + "name": "임의 정밀도 / 큰 수 연산", + "short": "임의 정밀도 / 큰 수 연산" + }, + { + "language": "en", + "name": "arbitrary precision / big integers", + "short": "arbitrary precision / big integers" + }, + { + "language": "ja", + "name": "高精度または大きな数の演算", + "short": "高精度または大きな数の演算" + } + ], + "aliases": [ + { + "alias": "빅인티저" + }, + { + "alias": "빅데시멀" + }, + { + "alias": "biginteger" + }, + { + "alias": "bigdecimal" + } + ] + }, + { + "key": "knapsack", + "isMeta": false, + "bojTagId": 148, + "problemCount": 247, + "displayNames": [ + { + "language": "ko", + "name": "배낭 문제", + "short": "배낭 문제" + }, + { + "language": "en", + "name": "knapsack", + "short": "knapsack" + }, + { + "language": "ja", + "name": "ナップサック問題", + "short": "ナップサック" + } + ], + "aliases": [ + { + "alias": "냅색" + } + ] + }, + { + "key": "offline_queries", + "isMeta": false, + "bojTagId": 123, + "problemCount": 246, + "displayNames": [ + { + "language": "ko", + "name": "오프라인 쿼리", + "short": "오프라인 쿼리" + }, + { + "language": "en", + "name": "offline queries", + "short": "offline query" + }, + { + "language": "ja", + "name": "offline queries", + "short": "offline query" + } + ], + "aliases": [ + { + "alias": "offlinequery" + } + ] + }, + { + "key": "recursion", + "isMeta": false, + "bojTagId": 62, + "problemCount": 223, + "displayNames": [ + { + "language": "ko", + "name": "재귀", + "short": "재귀" + }, + { + "language": "en", + "name": "recursion", + "short": "recursion" + }, + { + "language": "ja", + "name": "再帰", + "short": "再帰" + } + ], + "aliases": [] + }, + { + "key": "coordinate_compression", + "isMeta": false, + "bojTagId": 161, + "problemCount": 223, + "displayNames": [ + { + "language": "ko", + "name": "값 / 좌표 압축", + "short": "값 / 좌표 압축" + }, + { + "language": "en", + "name": "value / coordinate compression", + "short": "compression" + }, + { + "language": "ja", + "name": "value / coordinate compression", + "short": "compression" + } + ], + "aliases": [ + { + "alias": "zip" + } + ] + }, + { + "key": "precomputation", + "isMeta": false, + "bojTagId": 172, + "problemCount": 203, + "displayNames": [ + { + "language": "ko", + "name": "런타임 전의 전처리", + "short": "런타임 전의 전처리" + }, + { + "language": "en", + "name": "precomputation", + "short": "precomputation" + }, + { + "language": "ja", + "name": "事前計算", + "short": "事前計算" + } + ], + "aliases": [ + { + "alias": "lookup table" + }, + { + "alias": "db" + }, + { + "alias": "database" + } + ] + }, + { + "key": "mst", + "isMeta": false, + "bojTagId": 49, + "problemCount": 199, + "displayNames": [ + { + "language": "ko", + "name": "최소 스패닝 트리", + "short": "최소 스패닝 트리" + }, + { + "language": "en", + "name": "minimum spanning tree", + "short": "mst" + }, + { + "language": "ja", + "name": "最小全域木", + "short": "最小全域木" + } + ], + "aliases": [] + }, + { + "key": "sieve", + "isMeta": false, + "bojTagId": 67, + "problemCount": 196, + "displayNames": [ + { + "language": "ko", + "name": "에라토스테네스의 체", + "short": "에라토스테네스의 체" + }, + { + "language": "en", + "name": "sieve of eratosthenes", + "short": "eratosthenes" + }, + { + "language": "ja", + "name": "エラトステネスの篩", + "short": "エラトステネス" + } + ], + "aliases": [ + { + "alias": "sieve" + }, + { + "alias": "에라체" + }, + { + "alias": "소수" + }, + { + "alias": "prime" + } + ] + }, + { + "key": "euclidean", + "isMeta": false, + "bojTagId": 26, + "problemCount": 186, + "displayNames": [ + { + "language": "ko", + "name": "유클리드 호제법", + "short": "유클리드 호제법" + }, + { + "language": "en", + "name": "euclidean algorithm", + "short": "euclidean algorithm" + }, + { + "language": "ja", + "name": "ユークリッドの互除法", + "short": "ユークリッドの互除法" + } + ], + "aliases": [ + { + "alias": "유클리드알고리즘" + } + ] + }, + { + "key": "bipartite_matching", + "isMeta": false, + "bojTagId": 13, + "problemCount": 184, + "displayNames": [ + { + "language": "ko", + "name": "이분 매칭", + "short": "이분 매칭" + }, + { + "language": "en", + "name": "bipartite matching", + "short": "bipartite matching" + }, + { + "language": "ja", + "name": "2部マッチング", + "short": "2部マッチング" + } + ], + "aliases": [] + }, + { + "key": "dag", + "isMeta": false, + "bojTagId": 213, + "problemCount": 182, + "displayNames": [ + { + "language": "ko", + "name": "방향 비순환 그래프", + "short": "dag" + }, + { + "language": "en", + "name": "directed acyclic graph", + "short": "dag" + }, + { + "language": "ja", + "name": "有向非巡回グラフ", + "short": "有向非巡回グラフ" + } + ], + "aliases": [] + }, + { + "key": "convex_hull", + "isMeta": false, + "bojTagId": 20, + "problemCount": 178, + "displayNames": [ + { + "language": "ko", + "name": "볼록 껍질", + "short": "볼록 껍질" + }, + { + "language": "en", + "name": "convex hull", + "short": "convex hull" + }, + { + "language": "ja", + "name": "凸包", + "short": "凸包" + } + ], + "aliases": [ + { + "alias": "컨벡스헐" + } + ] + }, + { + "key": "linear_algebra", + "isMeta": false, + "bojTagId": 144, + "problemCount": 174, + "displayNames": [ + { + "language": "ko", + "name": "선형대수학", + "short": "선형대수학" + }, + { + "language": "en", + "name": "linear algebra", + "short": "linear algebra" + }, + { + "language": "ja", + "name": "線形代数", + "short": "線代" + } + ], + "aliases": [ + { + "alias": "선형대수" + } + ] + }, + { + "key": "topological_sorting", + "isMeta": false, + "bojTagId": 78, + "problemCount": 170, + "displayNames": [ + { + "language": "ko", + "name": "위상 정렬", + "short": "위상 정렬" + }, + { + "language": "en", + "name": "topological sorting", + "short": "topological sorting" + }, + { + "language": "ja", + "name": "トポロジカルソート", + "short": "トポロジカルソート" + } + ], + "aliases": [] + }, + { + "key": "floyd_warshall", + "isMeta": false, + "bojTagId": 31, + "problemCount": 165, + "displayNames": [ + { + "language": "ko", + "name": "플로이드–워셜", + "short": "플로이드–워셜" + }, + { + "language": "en", + "name": "floyd–warshall", + "short": "floyd–warshall" + }, + { + "language": "ja", + "name": "ワーシャル–フロイド法", + "short": "ワーシャル–フロイド法" + } + ], + "aliases": [ + { + "alias": "플로이드" + }, + { + "alias": "플로이드와셜" + }, + { + "alias": "플로이드와샬" + } + ] + }, + { + "key": "hashing", + "isMeta": false, + "bojTagId": 8, + "problemCount": 164, + "displayNames": [ + { + "language": "ko", + "name": "해싱", + "short": "해싱" + }, + { + "language": "en", + "name": "hashing", + "short": "hash" + }, + { + "language": "ja", + "name": "ハッシュ化", + "short": "ハッシュ" + } + ], + "aliases": [] + }, + { + "key": "lca", + "isMeta": false, + "bojTagId": 41, + "problemCount": 163, + "displayNames": [ + { + "language": "ko", + "name": "최소 공통 조상", + "short": "최소 공통 조상" + }, + { + "language": "en", + "name": "lowest common ancestor", + "short": "lca" + }, + { + "language": "ja", + "name": "最下位共通祖先", + "short": "lca" + } + ], + "aliases": [] + }, + { + "key": "inclusion_and_exclusion", + "isMeta": false, + "bojTagId": 38, + "problemCount": 152, + "displayNames": [ + { + "language": "ko", + "name": "포함 배제의 원리", + "short": "포함 배제의 원리" + }, + { + "language": "en", + "name": "inclusion and exclusion", + "short": "inclusion and exclusion" + }, + { + "language": "ja", + "name": "包除原理", + "short": "包除原理" + } + ], + "aliases": [] + }, + { + "key": "scc", + "isMeta": false, + "bojTagId": 76, + "problemCount": 147, + "displayNames": [ + { + "language": "ko", + "name": "강한 연결 요소", + "short": "강한 연결 요소" + }, + { + "language": "en", + "name": "strongly connected component", + "short": "scc" + }, + { + "language": "ja", + "name": "強連結", + "short": "強連結" + } + ], + "aliases": [] + }, + { + "key": "randomization", + "isMeta": false, + "bojTagId": 115, + "problemCount": 143, + "displayNames": [ + { + "language": "ko", + "name": "무작위화", + "short": "무작위화" + }, + { + "language": "en", + "name": "randomization", + "short": "randomization" + }, + { + "language": "ja", + "name": "ランダム化", + "short": "ランダム化" + } + ], + "aliases": [ + { + "alias": "랜덤" + } + ] + }, + { + "key": "sparse_table", + "isMeta": false, + "bojTagId": 84, + "problemCount": 136, + "displayNames": [ + { + "language": "ko", + "name": "희소 배열", + "short": "희소 배열" + }, + { + "language": "en", + "name": "sparse table", + "short": "sparse table" + }, + { + "language": "ja", + "name": "sparse table", + "short": "sparse table" + } + ], + "aliases": [ + { + "alias": "스파스어레이" + }, + { + "alias": "sparse table" + } + ] + }, + { + "key": "smaller_to_larger", + "isMeta": false, + "bojTagId": 169, + "problemCount": 128, + "displayNames": [ + { + "language": "ko", + "name": "작은 집합에서 큰 집합으로 합치는 테크닉", + "short": "작은 집합에서 큰 집합으로 합치는 테크닉" + }, + { + "language": "en", + "name": "smaller to larger technique", + "short": "smaller to larger" + }, + { + "language": "ja", + "name": "smaller to larger technique", + "short": "smaller to larger" + } + ], + "aliases": [ + { + "alias": "merge heuristics" + }, + { + "alias": "sack" + }, + { + "alias": "small to large" + }, + { + "alias": "작은거" + }, + { + "alias": "큰거" + } + ] + }, + { + "key": "fft", + "isMeta": false, + "bojTagId": 28, + "problemCount": 126, + "displayNames": [ + { + "language": "ko", + "name": "고속 푸리에 변환", + "short": "고속 푸리에 변환" + }, + { + "language": "en", + "name": "fast fourier transform", + "short": "fft" + }, + { + "language": "ja", + "name": "高速フーリエ変換", + "short": "fft" + } + ], + "aliases": [ + { + "alias": "푸리에변환" + }, + { + "alias": "컨볼루션" + }, + { + "alias": "convolution" + } + ] + }, + { + "key": "trie", + "isMeta": false, + "bojTagId": 79, + "problemCount": 124, + "displayNames": [ + { + "language": "ko", + "name": "트라이", + "short": "트라이" + }, + { + "language": "en", + "name": "trie", + "short": "trie" + }, + { + "language": "ja", + "name": "トライ木", + "short": "トライ" + } + ], + "aliases": [] + }, + { + "key": "deque", + "isMeta": false, + "bojTagId": 73, + "problemCount": 120, + "displayNames": [ + { + "language": "ko", + "name": "덱", + "short": "덱" + }, + { + "language": "en", + "name": "deque", + "short": "deque" + }, + { + "language": "ja", + "name": "両端キュー", + "short": "deque" + } + ], + "aliases": [] + }, + { + "key": "line_intersection", + "isMeta": false, + "bojTagId": 42, + "problemCount": 117, + "displayNames": [ + { + "language": "ko", + "name": "선분 교차 판정", + "short": "선분 교차 판정" + }, + { + "language": "en", + "name": "line segment intersection check", + "short": "line segment intersection check" + }, + { + "language": "ja", + "name": "直線の交点", + "short": "直線の交点" + } + ], + "aliases": [] + }, + { + "key": "mcmf", + "isMeta": false, + "bojTagId": 48, + "problemCount": 116, + "displayNames": [ + { + "language": "ko", + "name": "최소 비용 최대 유량", + "short": "최소 비용 최대 유량" + }, + { + "language": "en", + "name": "minimum cost maximum flow", + "short": "mcmf" + }, + { + "language": "ja", + "name": "最小費用最大流問題", + "short": "mcmf" + } + ], + "aliases": [ + { + "alias": "dinic" + }, + { + "alias": "dinitz" + }, + { + "alias": "ford" + }, + { + "alias": "fulkerson" + }, + { + "alias": "fordfulkerson" + }, + { + "alias": "디닉" + }, + { + "alias": "디니츠" + }, + { + "alias": "포드풀커슨" + } + ] + }, + { + "key": "sqrt_decomposition", + "isMeta": false, + "bojTagId": 130, + "problemCount": 110, + "displayNames": [ + { + "language": "ko", + "name": "제곱근 분할법", + "short": "제곱근 분할법" + }, + { + "language": "en", + "name": "square root decomposition", + "short": "sqrt decomposition" + }, + { + "language": "ja", + "name": "平方分割", + "short": "平方分割" + } + ], + "aliases": [ + { + "alias": "루트분할법" + }, + { + "alias": "평방분할법" + }, + { + "alias": "모" + }, + { + "alias": "mo" + }, + { + "alias": "sqrt" + } + ] + }, + { + "key": "calculus", + "isMeta": false, + "bojTagId": 111, + "problemCount": 107, + "displayNames": [ + { + "language": "ko", + "name": "미적분학", + "short": "미적분학" + }, + { + "language": "en", + "name": "calculus", + "short": "calculus" + }, + { + "language": "ja", + "name": "微積分", + "short": "微積分" + } + ], + "aliases": [ + { + "alias": "미분" + }, + { + "alias": "적분" + } + ] + }, + { + "key": "modular_multiplicative_inverse", + "isMeta": false, + "bojTagId": 164, + "problemCount": 101, + "displayNames": [ + { + "language": "ko", + "name": "모듈로 곱셈 역원", + "short": "모듈로 곱셈 역원" + }, + { + "language": "en", + "name": "modular multiplicative inverse", + "short": "modular multiplicative inverse" + }, + { + "language": "ja", + "name": "モジュラ逆数", + "short": "モジュラ逆数" + } + ], + "aliases": [ + { + "alias": "modinv" + } + ] + }, + { + "key": "cht", + "isMeta": false, + "bojTagId": 89, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "볼록 껍질을 이용한 최적화", + "short": "볼록 껍질을 이용한 최적화" + }, + { + "language": "en", + "name": "convex hull trick", + "short": "cht" + }, + { + "language": "ja", + "name": "convex hull trick", + "short": "cht" + } + ], + "aliases": [ + { + "alias": "컨벡스헐트릭" + }, + { + "alias": "컨벡스헐최적화" + } + ] + }, + { + "key": "heuristics", + "isMeta": false, + "bojTagId": 142, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "휴리스틱", + "short": "휴리스틱" + }, + { + "language": "en", + "name": "heuristics", + "short": "heuristics" + }, + { + "language": "ja", + "name": "ヒューリスティック", + "short": "ヒューリスティック" + } + ], + "aliases": [] + }, + { + "key": "sliding_window", + "isMeta": false, + "bojTagId": 68, + "problemCount": 100, + "displayNames": [ + { + "language": "ko", + "name": "슬라이딩 윈도우", + "short": "슬라이딩 윈도우" + }, + { + "language": "en", + "name": "sliding window", + "short": "sliding window" + }, + { + "language": "ja", + "name": "スライディングウィンドウ", + "short": "スライディングウィンドウ" + } + ], + "aliases": [ + { + "alias": "슬라이딩윈도" + } + ] + }, + { + "key": "geometry_3d", + "isMeta": false, + "bojTagId": 131, + "problemCount": 98, + "displayNames": [ + { + "language": "ko", + "name": "3차원 기하학", + "short": "3차원 기하학" + }, + { + "language": "en", + "name": "geometry; 3d", + "short": "3d" + }, + { + "language": "ja", + "name": "3次元幾何学", + "short": "3d" + } + ], + "aliases": [] + }, + { + "key": "suffix_array", + "isMeta": false, + "bojTagId": 77, + "problemCount": 97, + "displayNames": [ + { + "language": "ko", + "name": "접미사 배열과 LCP 배열", + "short": "접미사 배열과 LCP 배열" + }, + { + "language": "en", + "name": "suffix array and lcp array", + "short": "suffix array and lcp array" + }, + { + "language": "ja", + "name": "接尾辞配列・LCP配列", + "short": "接尾辞配列・LCP配列" + } + ], + "aliases": [] + }, + { + "key": "centroid", + "isMeta": false, + "bojTagId": 188, + "problemCount": 95, + "displayNames": [ + { + "language": "en", + "name": "centroid", + "short": "centroid" + }, + { + "language": "ko", + "name": "센트로이드", + "short": "센트로이드" + }, + { + "language": "ja", + "name": "centroid", + "short": "centroid" + } + ], + "aliases": [] + }, + { + "key": "euler_tour_technique", + "isMeta": false, + "bojTagId": 150, + "problemCount": 95, + "displayNames": [ + { + "language": "ko", + "name": "오일러 경로 테크닉", + "short": "오일러 경로 테크닉" + }, + { + "language": "en", + "name": "euler tour technique", + "short": "ett" + }, + { + "language": "ja", + "name": "オイラーツアー", + "short": "ett" + } + ], + "aliases": [] + }, + { + "key": "sprague_grundy", + "isMeta": false, + "bojTagId": 70, + "problemCount": 94, + "displayNames": [ + { + "language": "ko", + "name": "스프라그–그런디 정리", + "short": "스프라그–그런디 정리" + }, + { + "language": "en", + "name": "sprague–grundy theorem", + "short": "sprague–grundy thm" + }, + { + "language": "ja", + "name": "sprague–grundy theorem", + "short": "sprague–grundy thm" + } + ], + "aliases": [ + { + "alias": "님버" + }, + { + "alias": "nimber" + } + ] + }, + { + "key": "ternary_search", + "isMeta": false, + "bojTagId": 101, + "problemCount": 92, + "displayNames": [ + { + "language": "ko", + "name": "삼분 탐색", + "short": "삼분 탐색" + }, + { + "language": "en", + "name": "ternary search", + "short": "ternary search" + }, + { + "language": "ja", + "name": "三分探索", + "short": "三分探索" + } + ], + "aliases": [] + }, + { + "key": "mitm", + "isMeta": false, + "bojTagId": 46, + "problemCount": 89, + "displayNames": [ + { + "language": "ko", + "name": "중간에서 만나기", + "short": "중간에서 만나기" + }, + { + "language": "en", + "name": "meet in the middle", + "short": "meet in the middle" + }, + { + "language": "ja", + "name": "半分全列挙", + "short": "半分全列挙" + } + ], + "aliases": [] + }, + { + "key": "bitset", + "isMeta": false, + "bojTagId": 152, + "problemCount": 89, + "displayNames": [ + { + "language": "ko", + "name": "비트 집합", + "short": "비트 집합" + }, + { + "language": "en", + "name": "bit set", + "short": "bit set" + }, + { + "language": "ja", + "name": "bit set", + "short": "bit set" + } + ], + "aliases": [ + { + "alias": "bitset" + }, + { + "alias": "비트셋" + } + ] + }, + { + "key": "pythagoras", + "isMeta": false, + "bojTagId": 60, + "problemCount": 88, + "displayNames": [ + { + "language": "ko", + "name": "피타고라스 정리", + "short": "피타고라스 정리" + }, + { + "language": "en", + "name": "pythagoras theorem", + "short": "pythagoras thm" + }, + { + "language": "ja", + "name": "ピタゴラスの定理", + "short": "ピタゴラス" + } + ], + "aliases": [] + }, + { + "key": "permutation_cycle_decomposition", + "isMeta": false, + "bojTagId": 171, + "problemCount": 87, + "displayNames": [ + { + "language": "ko", + "name": "순열 사이클 분할", + "short": "순열 사이클 분할" + }, + { + "language": "en", + "name": "permutation cycle decomposition", + "short": "permutation cycle decomposition" + }, + { + "language": "ja", + "name": "順列サイクル分解", + "short": "順列サイクル分解" + } + ], + "aliases": [] + }, + { + "key": "lis", + "isMeta": false, + "bojTagId": 43, + "problemCount": 85, + "displayNames": [ + { + "language": "ko", + "name": "가장 긴 증가하는 부분 수열: O(n log n)", + "short": "가장 긴 증가하는 부분 수열: O(n log n)" + }, + { + "language": "en", + "name": "longest increasing sequence in o(n log n)", + "short": "lis in o(n log n)" + }, + { + "language": "ja", + "name": "longest increasing sequence in o(n log n)", + "short": "lis in o(n log n)" + } + ], + "aliases": [] + }, + { + "key": "kmp", + "isMeta": false, + "bojTagId": 40, + "problemCount": 84, + "displayNames": [ + { + "language": "ko", + "name": "KMP", + "short": "KMP" + }, + { + "language": "en", + "name": "knuth–morris–pratt", + "short": "kmp" + }, + { + "language": "ja", + "name": "クヌース–モリス–プラット法", + "short": "kmp" + } + ], + "aliases": [] + }, + { + "key": "gaussian_elimination", + "isMeta": false, + "bojTagId": 32, + "problemCount": 81, + "displayNames": [ + { + "language": "ko", + "name": "가우스 소거법", + "short": "가우스 소거법" + }, + { + "language": "en", + "name": "gaussian elimination", + "short": "gaussian elimination" + }, + { + "language": "ja", + "name": "ガウス消去法", + "short": "ガウス消去法" + } + ], + "aliases": [] + }, + { + "key": "hld", + "isMeta": false, + "bojTagId": 35, + "problemCount": 80, + "displayNames": [ + { + "language": "ko", + "name": "Heavy-light 분할", + "short": "Heavy-light 분할" + }, + { + "language": "en", + "name": "heavy-light decomposition", + "short": "hld" + }, + { + "language": "ja", + "name": "heavy-light decomposition", + "short": "hld" + } + ], + "aliases": [] + }, + { + "key": "centroid_decomposition", + "isMeta": false, + "bojTagId": 18, + "problemCount": 76, + "displayNames": [ + { + "language": "ko", + "name": "센트로이드 분할", + "short": "센트로이드 분할" + }, + { + "language": "en", + "name": "centroid decomposition", + "short": "centroid decomposition" + }, + { + "language": "ja", + "name": "centroid decomposition", + "short": "centroid decomposition" + } + ], + "aliases": [ + { + "alias": "센트로이드" + } + ] + }, + { + "key": "mfmc", + "isMeta": false, + "bojTagId": 167, + "problemCount": 71, + "displayNames": [ + { + "language": "ko", + "name": "최대 유량 최소 컷 정리", + "short": "최대 유량 최소 컷 정리" + }, + { + "language": "en", + "name": "max-flow min-cut theorem", + "short": "mfmc" + }, + { + "language": "ja", + "name": "最大フロー最小カット定理", + "short": "mfmc" + } + ], + "aliases": [] + }, + { + "key": "polygon_area", + "isMeta": false, + "bojTagId": 3, + "problemCount": 71, + "displayNames": [ + { + "language": "ko", + "name": "다각형의 넓이", + "short": "다각형의 넓이" + }, + { + "language": "en", + "name": "area of a polygon", + "short": "area of a polygon" + }, + { + "language": "ja", + "name": "多角形の面積", + "short": "多角形の面積" + } + ], + "aliases": [ + { + "alias": "넓이" + } + ] + }, + { + "key": "queue", + "isMeta": false, + "bojTagId": 72, + "problemCount": 64, + "displayNames": [ + { + "language": "ko", + "name": "큐", + "short": "큐" + }, + { + "language": "en", + "name": "queue", + "short": "queue" + }, + { + "language": "ja", + "name": "キュー", + "short": "キュー" + } + ], + "aliases": [] + }, + { + "key": "physics", + "isMeta": false, + "bojTagId": 116, + "problemCount": 62, + "displayNames": [ + { + "language": "ko", + "name": "물리학", + "short": "물리학" + }, + { + "language": "en", + "name": "physics", + "short": "physics" + }, + { + "language": "ja", + "name": "物理", + "short": "物理" + } + ], + "aliases": [] + }, + { + "key": "flt", + "isMeta": false, + "bojTagId": 29, + "problemCount": 60, + "displayNames": [ + { + "language": "ko", + "name": "페르마의 소정리", + "short": "페르마의 소정리" + }, + { + "language": "en", + "name": "fermat's little theorem", + "short": "fermat's little thm" + }, + { + "language": "ja", + "name": "フェルマーの小定理", + "short": "フェルマー" + } + ], + "aliases": [] + }, + { + "key": "tsp", + "isMeta": false, + "bojTagId": 138, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "외판원 순회 문제", + "short": "외판원 순회 문제" + }, + { + "language": "en", + "name": "travelling salesman problem", + "short": "tsp" + }, + { + "language": "ja", + "name": "巡回セールスマン問題", + "short": "巡回セールスマン" + } + ], + "aliases": [ + { + "alias": "외판원순회" + } + ] + }, + { + "key": "eulerian_path", + "isMeta": false, + "bojTagId": 93, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "오일러 경로", + "short": "오일러 경로" + }, + { + "language": "en", + "name": "eulerian path / circuit", + "short": "eulerian path" + }, + { + "language": "ja", + "name": "eulerian path / circuit", + "short": "eulerian path" + } + ], + "aliases": [ + { + "alias": "eulerian circuit" + }, + { + "alias": "euler tour" + } + ] + }, + { + "key": "linearity_of_expectation", + "isMeta": false, + "bojTagId": 179, + "problemCount": 59, + "displayNames": [ + { + "language": "ko", + "name": "기댓값의 선형성", + "short": "기댓값의 선형성" + }, + { + "language": "en", + "name": "linearity of expectation", + "short": "linearity of expectation" + }, + { + "language": "ja", + "name": "期待値の線形性", + "short": "期待値の線形性" + } + ], + "aliases": [] + }, + { + "key": "2_sat", + "isMeta": false, + "bojTagId": 1, + "problemCount": 58, + "displayNames": [ + { + "language": "ko", + "name": "2-sat", + "short": "2-sat" + }, + { + "language": "en", + "name": "2-sat", + "short": "2-sat" + }, + { + "language": "ja", + "name": "2-sat", + "short": "2-sat" + } + ], + "aliases": [ + { + "alias": "투셋" + }, + { + "alias": "twosat" + }, + { + "alias": "2sat" + } + ] + }, + { + "key": "articulation", + "isMeta": false, + "bojTagId": 4, + "problemCount": 57, + "displayNames": [ + { + "language": "ko", + "name": "단절점과 단절선", + "short": "단절점과 단절선" + }, + { + "language": "en", + "name": "articulation points and bridges", + "short": "articulation points and bridges" + }, + { + "language": "ja", + "name": "関節点と橋", + "short": "関節点と橋" + } + ], + "aliases": [ + { + "alias": "단절점" + }, + { + "alias": "단절선" + }, + { + "alias": "브리지" + }, + { + "alias": "브릿지" + }, + { + "alias": "bridge" + } + ] + }, + { + "key": "0_1_bfs", + "isMeta": false, + "bojTagId": 176, + "problemCount": 56, + "displayNames": [ + { + "language": "ko", + "name": "0-1 너비 우선 탐색", + "short": "0-1 너비 우선 탐색" + }, + { + "language": "en", + "name": "0-1 bfs", + "short": "0-1 bfs" + }, + { + "language": "ja", + "name": "0-1 bfs", + "short": "0-1 bfs" + } + ], + "aliases": [] + }, + { + "key": "bipartite_graph", + "isMeta": false, + "bojTagId": 197, + "problemCount": 54, + "displayNames": [ + { + "language": "ko", + "name": "이분 그래프", + "short": "이분 그래프" + }, + { + "language": "en", + "name": "bipartite graph", + "short": "bipartite graph" + }, + { + "language": "ja", + "name": "2部グラフ", + "short": "2部グラフ" + } + ], + "aliases": [] + }, + { + "key": "biconnected_component", + "isMeta": false, + "bojTagId": 153, + "problemCount": 48, + "displayNames": [ + { + "language": "ko", + "name": "이중 연결 요소", + "short": "이중 연결 요소" + }, + { + "language": "en", + "name": "biconnected component", + "short": "biconnected component" + }, + { + "language": "ja", + "name": "二重接続コンポーネント", + "short": "二重接続" + } + ], + "aliases": [ + { + "alias": "bcc" + } + ] + }, + { + "key": "pst", + "isMeta": false, + "bojTagId": 55, + "problemCount": 46, + "displayNames": [ + { + "language": "ko", + "name": "퍼시스턴트 세그먼트 트리", + "short": "퍼시스턴트 세그먼트 트리" + }, + { + "language": "en", + "name": "persistent segment tree", + "short": "pst" + }, + { + "language": "ja", + "name": "永続セグメント木", + "short": "永続セグ木" + } + ], + "aliases": [ + { + "alias": "퍼시스턴트구간트리" + }, + { + "alias": "구간트리" + }, + { + "alias": "퍼시스턴트세그트리" + } + ] + }, + { + "key": "crt", + "isMeta": false, + "bojTagId": 19, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "중국인의 나머지 정리", + "short": "중국인의 나머지 정리" + }, + { + "language": "en", + "name": "chinese remainder theorem", + "short": "crt" + }, + { + "language": "ja", + "name": "中国の剰余定理", + "short": "中国の剰余定理" + } + ], + "aliases": [] + }, + { + "key": "linked_list", + "isMeta": false, + "bojTagId": 154, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "연결 리스트", + "short": "연결 리스트" + }, + { + "language": "en", + "name": "linked list", + "short": "ll" + }, + { + "language": "ja", + "name": "連結リスト", + "short": "連結リスト" + } + ], + "aliases": [ + { + "alias": "링크드리스트" + } + ] + }, + { + "key": "pigeonhole_principle", + "isMeta": false, + "bojTagId": 189, + "problemCount": 43, + "displayNames": [ + { + "language": "ko", + "name": "비둘기집 원리", + "short": "비둘기집" + }, + { + "language": "en", + "name": "pigeonhole principle", + "short": "pigeonhole" + }, + { + "language": "ja", + "name": "鳩の巣原理", + "short": "鳩" + } + ], + "aliases": [] + }, + { + "key": "cactus", + "isMeta": false, + "bojTagId": 143, + "problemCount": 42, + "displayNames": [ + { + "language": "ko", + "name": "선인장", + "short": "선인장" + }, + { + "language": "en", + "name": "cactus", + "short": "cactus" + }, + { + "language": "ja", + "name": "サボテングラフ", + "short": "サボテングラフ" + } + ], + "aliases": [] + }, + { + "key": "bellman_ford", + "isMeta": false, + "bojTagId": 10, + "problemCount": 41, + "displayNames": [ + { + "language": "ko", + "name": "벨만–포드", + "short": "벨만–포드" + }, + { + "language": "en", + "name": "bellman–ford", + "short": "bellman-ford" + }, + { + "language": "ja", + "name": "ベルマンフォード法", + "short": "ベルマンフォード" + } + ], + "aliases": [ + { + "alias": "bellmanford" + }, + { + "alias": "벨만포드" + }, + { + "alias": "spfa" + } + ] + }, + { + "key": "planar_graph", + "isMeta": false, + "bojTagId": 168, + "problemCount": 41, + "displayNames": [ + { + "language": "ko", + "name": "평면 그래프", + "short": "평면 그래프" + }, + { + "language": "en", + "name": "planar graph", + "short": "planar graph" + }, + { + "language": "ja", + "name": "平面グラフ", + "short": "平面グラフ" + } + ], + "aliases": [] + }, + { + "key": "point_in_convex_polygon", + "isMeta": false, + "bojTagId": 56, + "problemCount": 40, + "displayNames": [ + { + "language": "ko", + "name": "볼록 다각형 내부의 점 판정", + "short": "볼록 다각형 내부의 점 판정" + }, + { + "language": "en", + "name": "point in convex polygon check", + "short": "point in convex polygon check" + }, + { + "language": "ja", + "name": "凸多角形の点包含判定", + "short": "凸多角形の点包含判定" + } + ], + "aliases": [] + }, + { + "key": "euler_phi", + "isMeta": false, + "bojTagId": 151, + "problemCount": 38, + "displayNames": [ + { + "language": "ko", + "name": "오일러 피 함수", + "short": "오일러 피 함수" + }, + { + "language": "en", + "name": "euler totient function", + "short": "euler phi function" + }, + { + "language": "ja", + "name": "euler totient function", + "short": "euler phi function" + } + ], + "aliases": [ + { + "alias": "오일러 파이" + }, + { + "alias": "토션트" + }, + { + "alias": "eulerphi" + }, + { + "alias": "euler phi" + } + ] + }, + { + "key": "splay_tree", + "isMeta": false, + "bojTagId": 69, + "problemCount": 37, + "displayNames": [ + { + "language": "ko", + "name": "스플레이 트리", + "short": "스플레이 트리" + }, + { + "language": "en", + "name": "splay tree", + "short": "splay tree" + }, + { + "language": "ja", + "name": "splay tree", + "short": "splay tree" + } + ], + "aliases": [] + }, + { + "key": "pbs", + "isMeta": false, + "bojTagId": 54, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "병렬 이분 탐색", + "short": "병렬 이분 탐색" + }, + { + "language": "en", + "name": "parallel binary search", + "short": "pbs" + }, + { + "language": "ja", + "name": "parallel binary search", + "short": "pbs" + } + ], + "aliases": [] + }, + { + "key": "extended_euclidean", + "isMeta": false, + "bojTagId": 27, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "확장 유클리드 호제법", + "short": "확장 유클리드 호제법" + }, + { + "language": "en", + "name": "extended euclidean algorithm", + "short": "extended euclidean algorithm" + }, + { + "language": "ja", + "name": "拡張ユークリッドの互除法", + "short": "拡張ユークリッド" + } + ], + "aliases": [ + { + "alias": "확장유클리드알고리즘" + }, + { + "alias": "egcd" + } + ] + }, + { + "key": "divide_and_conquer_optimization", + "isMeta": false, + "bojTagId": 91, + "problemCount": 35, + "displayNames": [ + { + "language": "ko", + "name": "분할 정복을 사용한 최적화", + "short": "분할 정복을 사용한 최적화" + }, + { + "language": "en", + "name": "divide and conquer optimization", + "short": "d&c optimization" + }, + { + "language": "ja", + "name": "divide and conquer optimization", + "short": "d&c optimization" + } + ], + "aliases": [ + { + "alias": "분할 정복 최적화" + }, + { + "alias": "dnc opt" + } + ] + }, + { + "key": "deque_trick", + "isMeta": false, + "bojTagId": 216, + "problemCount": 34, + "displayNames": [ + { + "language": "ko", + "name": "덱을 이용한 구간 최댓값 트릭", + "short": "덱 트릭" + }, + { + "language": "en", + "name": "deque range maximum trick", + "short": "deque rmq trick" + }, + { + "language": "ja", + "name": "deque range maximum trick", + "short": "deque rmq trick" + } + ], + "aliases": [] + }, + { + "key": "mo", + "isMeta": false, + "bojTagId": 50, + "problemCount": 33, + "displayNames": [ + { + "language": "ko", + "name": "mo's", + "short": "Mo's" + }, + { + "language": "en", + "name": "mo's", + "short": "mo's" + }, + { + "language": "ja", + "name": "mo's", + "short": "mo's" + } + ], + "aliases": [ + { + "alias": "squarerootdecomposition" + }, + { + "alias": "sqrtdecomposition" + }, + { + "alias": "평방분할법" + } + ] + }, + { + "key": "half_plane_intersection", + "isMeta": false, + "bojTagId": 190, + "problemCount": 30, + "displayNames": [ + { + "language": "ko", + "name": "반평면 교집합", + "short": "반평면 교집합" + }, + { + "language": "en", + "name": "half plane intersection", + "short": "hpi" + }, + { + "language": "ja", + "name": "half plane intersection", + "short": "hpi" + } + ], + "aliases": [] + }, + { + "key": "dp_deque", + "isMeta": false, + "bojTagId": 108, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "덱을 이용한 다이나믹 프로그래밍", + "short": "덱을 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using a deque", + "short": "deque dp" + }, + { + "language": "ja", + "name": "両端キューを使用した動的計画法", + "short": "deque dp" + } + ], + "aliases": [ + { + "alias": "동적계획법" + }, + { + "alias": "덱dp" + } + ] + }, + { + "key": "aho_corasick", + "isMeta": false, + "bojTagId": 2, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "아호-코라식", + "short": "아호-코라식" + }, + { + "language": "en", + "name": "aho-corasick", + "short": "aho-corasick" + }, + { + "language": "ja", + "name": "アホコラシック", + "short": "アホコラシック" + } + ], + "aliases": [ + { + "alias": "아호코라식" + }, + { + "alias": "ahocorasick" + } + ] + }, + { + "key": "multi_segtree", + "isMeta": false, + "bojTagId": 166, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "다차원 세그먼트 트리", + "short": "다차원 세그먼트 트리" + }, + { + "language": "en", + "name": "multidimensional segment tree", + "short": "multidimensional segtree" + }, + { + "language": "ja", + "name": "multidimensional segment tree", + "short": "multidimensional segtree" + } + ], + "aliases": [ + { + "alias": "구간트리" + }, + { + "alias": "세그트리" + }, + { + "alias": "fenwick" + }, + { + "alias": "펜윅" + } + ] + }, + { + "key": "rotating_calipers", + "isMeta": false, + "bojTagId": 64, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "회전하는 캘리퍼스", + "short": "회전하는 캘리퍼스" + }, + { + "language": "en", + "name": "rotating calipers", + "short": "rotating calipers" + }, + { + "language": "ja", + "name": "rotating calipers", + "short": "rotating calipers" + } + ], + "aliases": [] + }, + { + "key": "euler_characteristic", + "isMeta": false, + "bojTagId": 119, + "problemCount": 29, + "displayNames": [ + { + "language": "ko", + "name": "오일러 지표 (χ=V-E+F)", + "short": "오일러 지표" + }, + { + "language": "en", + "name": "euler characteristic (χ=v-e+f)", + "short": "euler characteristic" + }, + { + "language": "ja", + "name": "オイラー特性(χ=v-e+f)", + "short": "オイラー特性" + } + ], + "aliases": [] + }, + { + "key": "regex", + "isMeta": false, + "bojTagId": 63, + "problemCount": 27, + "displayNames": [ + { + "language": "ko", + "name": "정규 표현식", + "short": "정규 표현식" + }, + { + "language": "en", + "name": "regular expression", + "short": "regex" + }, + { + "language": "ja", + "name": "正規表現", + "short": "regex" + } + ], + "aliases": [ + { + "alias": "정규식" + } + ] + }, + { + "key": "slope_trick", + "isMeta": false, + "bojTagId": 157, + "problemCount": 26, + "displayNames": [ + { + "language": "ko", + "name": "함수 개형을 이용한 최적화", + "short": "함수 개형을 이용한 최적화" + }, + { + "language": "en", + "name": "slope trick", + "short": "slope trick" + }, + { + "language": "ja", + "name": "slope trick", + "short": "slope trick" + } + ], + "aliases": [ + { + "alias": "슬로프트릭" + }, + { + "alias": "슬로프 트릭" + } + ] + }, + { + "key": "berlekamp_massey", + "isMeta": false, + "bojTagId": 110, + "problemCount": 25, + "displayNames": [ + { + "language": "ko", + "name": "벌리캠프–매시", + "short": "벌리캠프–매시" + }, + { + "language": "en", + "name": "berlekamp–massey", + "short": "berlekamp–massey" + }, + { + "language": "ja", + "name": "berlekamp–massey", + "short": "berlekamp–massey" + } + ], + "aliases": [ + { + "alias": "벌레캠프" + }, + { + "alias": "벌래캠프" + } + ] + }, + { + "key": "manacher", + "isMeta": false, + "bojTagId": 44, + "problemCount": 24, + "displayNames": [ + { + "language": "ko", + "name": "매내처", + "short": "매내처" + }, + { + "language": "en", + "name": "manacher's", + "short": "manacher's" + }, + { + "language": "ja", + "name": "manacher's", + "short": "manacher's" + } + ], + "aliases": [] + }, + { + "key": "pollard_rho", + "isMeta": false, + "bojTagId": 58, + "problemCount": 23, + "displayNames": [ + { + "language": "ko", + "name": "폴라드 로", + "short": "폴라드 로" + }, + { + "language": "en", + "name": "pollard rho", + "short": "pollard rho" + }, + { + "language": "ja", + "name": "ポラード・ロー素因数分解法", + "short": "ポラード・ロー" + } + ], + "aliases": [] + }, + { + "key": "dp_connection_profile", + "isMeta": false, + "bojTagId": 107, + "problemCount": 23, + "displayNames": [ + { + "language": "ko", + "name": "커넥션 프로파일을 이용한 다이나믹 프로그래밍", + "short": "커넥션 프로파일을 이용한 다이나믹 프로그래밍" + }, + { + "language": "en", + "name": "dynamic programming using connection profile", + "short": "dp using connection profile" + }, + { + "language": "ja", + "name": "dynamic programming using connection profile", + "short": "dp using connection profile" + } + ], + "aliases": [ + { + "alias": "동적계획법" + } + ] + }, + { + "key": "link_cut_tree", + "isMeta": false, + "bojTagId": 98, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "링크/컷 트리", + "short": "링크/컷 트리" + }, + { + "language": "en", + "name": "link/cut tree", + "short": "link/cut tree" + }, + { + "language": "ja", + "name": "link/cut tree", + "short": "link/cut tree" + } + ], + "aliases": [ + { + "alias": "link cut tree" + }, + { + "alias": "linkcuttree" + }, + { + "alias": "링크컷" + } + ] + }, + { + "key": "merge_sort_tree", + "isMeta": false, + "bojTagId": 155, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "머지 소트 트리", + "short": "머지 소트 트리" + }, + { + "language": "en", + "name": "merge sort tree", + "short": "merge sort tree" + }, + { + "language": "ja", + "name": "マージソート木", + "short": "マージソート木" + } + ], + "aliases": [ + { + "alias": "병합정렬트리" + }, + { + "alias": "합병정렬트리" + } + ] + }, + { + "key": "tree_isomorphism", + "isMeta": false, + "bojTagId": 145, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "트리 동형 사상", + "short": "트리 동형 사상" + }, + { + "language": "en", + "name": "tree isomorphism", + "short": "tree isomorphism" + }, + { + "language": "ja", + "name": "木の同型性判定", + "short": "木の同型性判定" + } + ], + "aliases": [ + { + "alias": "graph isomorphism" + }, + { + "alias": "isomorphism" + }, + { + "alias": "topology" + }, + { + "alias": "아이소모피즘" + }, + { + "alias": "위상" + } + ] + }, + { + "key": "simulated_annealing", + "isMeta": false, + "bojTagId": 184, + "problemCount": 22, + "displayNames": [ + { + "language": "ko", + "name": "담금질 기법", + "short": "담금질 기법" + }, + { + "language": "en", + "name": "simulated annealing", + "short": "simulated annealing" + }, + { + "language": "ja", + "name": "焼き鈍し法", + "short": "焼き鈍し法" + } + ], + "aliases": [] + }, + { + "key": "hall", + "isMeta": false, + "bojTagId": 34, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "홀의 결혼 정리", + "short": "홀의 결혼 정리" + }, + { + "language": "en", + "name": "hall's theorem", + "short": "hall's thm" + }, + { + "language": "ja", + "name": "ホールの定理", + "short": "ホールの定理" + } + ], + "aliases": [] + }, + { + "key": "hungarian", + "isMeta": false, + "bojTagId": 36, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "헝가리안", + "short": "헝가리안" + }, + { + "language": "en", + "name": "hungarian", + "short": "hungarian" + }, + { + "language": "ja", + "name": "hungarian", + "short": "hungarian" + } + ], + "aliases": [ + { + "alias": "헝가리안" + }, + { + "alias": "assignment problem" + }, + { + "alias": "weighted bipartite matching" + } + ] + }, + { + "key": "flood_fill", + "isMeta": false, + "bojTagId": 210, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "플러드 필", + "short": "플러드 필" + }, + { + "language": "en", + "name": "flood-fill", + "short": "ff" + }, + { + "language": "ja", + "name": "flood-fill", + "short": "ff" + } + ], + "aliases": [] + }, + { + "key": "miller_rabin", + "isMeta": false, + "bojTagId": 47, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "밀러–라빈 소수 판별법", + "short": "밀러–라빈 소수 판별법" + }, + { + "language": "en", + "name": "miller–rabin", + "short": "miller–rabin" + }, + { + "language": "ja", + "name": "ミラー–ラビン素数判定法", + "short": "ミラー–ラビン" + } + ], + "aliases": [] + }, + { + "key": "mobius_inversion", + "isMeta": false, + "bojTagId": 51, + "problemCount": 20, + "displayNames": [ + { + "language": "ko", + "name": "뫼비우스 반전 공식", + "short": "뫼비우스 반전 공식" + }, + { + "language": "en", + "name": "möbius inversion", + "short": "möbius inversion" + }, + { + "language": "ja", + "name": "メビウスの反転公式", + "short": "メビウス" + } + ], + "aliases": [ + { + "alias": "mobius" + } + ] + }, + { + "key": "rabin_karp", + "isMeta": false, + "bojTagId": 61, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "라빈–카프", + "short": "라빈–카프" + }, + { + "language": "en", + "name": "rabin–karp", + "short": "rabin–karp" + }, + { + "language": "ja", + "name": "ラビン-カープ文字列検索", + "short": "ラビン-カープ文字列検索" + } + ], + "aliases": [ + { + "alias": "라빈카프" + } + ] + }, + { + "key": "numerical_analysis", + "isMeta": false, + "bojTagId": 122, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "수치해석", + "short": "수치해석" + }, + { + "language": "en", + "name": "numerical analysis", + "short": "numerical analysis" + }, + { + "language": "ja", + "name": "数値解析", + "short": "数値解析" + } + ], + "aliases": [ + { + "alias": "수학" + } + ] + }, + { + "key": "point_in_non_convex_polygon", + "isMeta": false, + "bojTagId": 57, + "problemCount": 19, + "displayNames": [ + { + "language": "ko", + "name": "오목 다각형 내부의 점 판정", + "short": "오목 다각형 내부의 점 판정" + }, + { + "language": "en", + "name": "point in non-convex polygon check", + "short": "point in non-convex polygon check" + }, + { + "language": "ja", + "name": "非凸多角形の点包含判定", + "short": "非凸多角形の点包含判定" + } + ], + "aliases": [] + }, + { + "key": "alien", + "isMeta": false, + "bojTagId": 134, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "Aliens 트릭", + "short": "aliens 트릭" + }, + { + "language": "en", + "name": "aliens trick", + "short": "aliens trick" + }, + { + "language": "ja", + "name": "aliens法", + "short": "aliens法" + } + ], + "aliases": [ + { + "alias": "alien's trick" + } + ] + }, + { + "key": "linear_programming", + "isMeta": false, + "bojTagId": 103, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "선형 계획법", + "short": "선형 계획법" + }, + { + "language": "en", + "name": "linear programming", + "short": "lp" + }, + { + "language": "ja", + "name": "線型計画法", + "short": "lp" + } + ], + "aliases": [ + { + "alias": "리니어프로그래밍" + } + ] + }, + { + "key": "generating_function", + "isMeta": false, + "bojTagId": 198, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "생성 함수", + "short": "생성 함수" + }, + { + "language": "en", + "name": "generating function", + "short": "generating function" + }, + { + "language": "ja", + "name": "生成関数", + "short": "生成関数" + } + ], + "aliases": [] + }, + { + "key": "offline_dynamic_connectivity", + "isMeta": false, + "bojTagId": 52, + "problemCount": 18, + "displayNames": [ + { + "language": "ko", + "name": "오프라인 동적 연결성 판정", + "short": "오프라인 동적 연결성 판정" + }, + { + "language": "en", + "name": "offline dynamic connectivity", + "short": "offline dynamic connectivity" + }, + { + "language": "ja", + "name": "offline dynamic connectivity", + "short": "offline dynamic connectivity" + } + ], + "aliases": [] + }, + { + "key": "statistics", + "isMeta": false, + "bojTagId": 178, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "통계학", + "short": "통계학" + }, + { + "language": "en", + "name": "statistics", + "short": "stats" + }, + { + "language": "ja", + "name": "統計学", + "short": "統計" + } + ], + "aliases": [ + { + "alias": "average" + }, + { + "alias": "평균" + }, + { + "alias": "variance" + }, + { + "alias": "분산" + }, + { + "alias": "표준편차" + }, + { + "alias": "표준 편차" + } + ] + }, + { + "key": "functional_graph", + "isMeta": false, + "bojTagId": 211, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "함수형 그래프", + "short": "함수형 그래프" + }, + { + "language": "en", + "name": "functional graph", + "short": "functional graph" + }, + { + "language": "ja", + "name": "functional graph", + "short": "functional graph" + } + ], + "aliases": [] + }, + { + "key": "dp_sum_over_subsets", + "isMeta": false, + "bojTagId": 207, + "problemCount": 17, + "displayNames": [ + { + "language": "ko", + "name": "부분집합의 합 다이나믹 프로그래밍", + "short": "부분집합의 합" + }, + { + "language": "en", + "name": "sum over subsets dynamic programming", + "short": "sos dp" + }, + { + "language": "ja", + "name": "sum over subsets dynamic programming", + "short": "sos dp" + } + ], + "aliases": [ + { + "alias": "sos" + } + ] + }, + { + "key": "circulation", + "isMeta": false, + "bojTagId": 191, + "problemCount": 16, + "displayNames": [ + { + "language": "ko", + "name": "서큘레이션", + "short": "서큘레이션" + }, + { + "language": "en", + "name": "circulation", + "short": "circulation" + }, + { + "language": "ja", + "name": "circulation", + "short": "circulation" + } + ], + "aliases": [] + }, + { + "key": "tree_compression", + "isMeta": false, + "bojTagId": 193, + "problemCount": 16, + "displayNames": [ + { + "language": "ko", + "name": "트리 압축", + "short": "트리 압축" + }, + { + "language": "en", + "name": "tree compression", + "short": "tree compression" + }, + { + "language": "ja", + "name": "tree compression", + "short": "tree compression" + } + ], + "aliases": [] + }, + { + "key": "voronoi", + "isMeta": false, + "bojTagId": 82, + "problemCount": 15, + "displayNames": [ + { + "language": "ko", + "name": "보로노이 다이어그램", + "short": "보로노이 다이어그램" + }, + { + "language": "en", + "name": "voronoi diagram", + "short": "voronoi diagram" + }, + { + "language": "ja", + "name": "ボロノイ図", + "short": "ボロノイ図" + } + ], + "aliases": [] + }, + { + "key": "duality", + "isMeta": false, + "bojTagId": 180, + "problemCount": 14, + "displayNames": [ + { + "language": "ko", + "name": "쌍대성", + "short": "쌍대성" + }, + { + "language": "en", + "name": "duality", + "short": "duality" + }, + { + "language": "ja", + "name": "双対性", + "short": "双対性" + } + ], + "aliases": [ + { + "alias": "듀얼리티" + } + ] + }, + { + "key": "dual_graph", + "isMeta": false, + "bojTagId": 181, + "problemCount": 14, + "displayNames": [ + { + "language": "ko", + "name": "쌍대 그래프", + "short": "쌍대 그래프" + }, + { + "language": "en", + "name": "dual graph", + "short": "dual graph" + }, + { + "language": "ja", + "name": "双対グラフ", + "short": "双対グラフ" + } + ], + "aliases": [ + { + "alias": "듀얼 그래프" + } + ] + }, + { + "key": "lucas", + "isMeta": false, + "bojTagId": 113, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "뤼카 정리", + "short": "뤼카 정리" + }, + { + "language": "en", + "name": "lucas theorem", + "short": "lucas thm" + }, + { + "language": "ja", + "name": "lucas theorem", + "short": "lucas thm" + } + ], + "aliases": [] + }, + { + "key": "matroid", + "isMeta": false, + "bojTagId": 104, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "매트로이드", + "short": "매트로이드" + }, + { + "language": "en", + "name": "matroid", + "short": "matroid" + }, + { + "language": "ja", + "name": "マトロイド", + "short": "マトロイド" + } + ], + "aliases": [] + }, + { + "key": "dp_digit", + "isMeta": false, + "bojTagId": 217, + "problemCount": 13, + "displayNames": [ + { + "language": "ko", + "name": "자릿수를 이용한 다이나믹 프로그래밍", + "short": "자릿수 dp" + }, + { + "language": "en", + "name": "digit dp", + "short": "digit dp" + } + ], + "aliases": [] + }, + { + "key": "kitamasa", + "isMeta": false, + "bojTagId": 112, + "problemCount": 12, + "displayNames": [ + { + "language": "ko", + "name": "키타마사", + "short": "키타마사" + }, + { + "language": "en", + "name": "kitamasa", + "short": "kitamasa" + }, + { + "language": "ja", + "name": "きたまさ法", + "short": "きたまさ法" + } + ], + "aliases": [] + }, + { + "key": "cartesian_tree", + "isMeta": false, + "bojTagId": 206, + "problemCount": 11, + "displayNames": [ + { + "language": "ko", + "name": "데카르트 트리", + "short": "데카르트 트리" + }, + { + "language": "en", + "name": "cartesian tree", + "short": "cartesian tree" + }, + { + "language": "ja", + "name": "デカルト木", + "short": "デカルト木" + } + ], + "aliases": [] + }, + { + "key": "general_matching", + "isMeta": false, + "bojTagId": 15, + "problemCount": 11, + "displayNames": [ + { + "language": "ko", + "name": "일반적인 매칭", + "short": "일반적인 매칭" + }, + { + "language": "en", + "name": "general matching", + "short": "general matching" + }, + { + "language": "ja", + "name": "一般的なマッチング", + "short": "一般的なマッチング" + } + ], + "aliases": [ + { + "alias": "블라썸" + }, + { + "alias": "블러썸" + }, + { + "alias": "블라섬" + }, + { + "alias": "블러섬" + }, + { + "alias": "blossom" + }, + { + "alias": "부합" + } + ] + }, + { + "key": "tree_decomposition", + "isMeta": false, + "bojTagId": 204, + "problemCount": 10, + "displayNames": [ + { + "language": "ko", + "name": "트리 분할", + "short": "트리 분할" + }, + { + "language": "en", + "name": "tree decomposition", + "short": "tree decomposition" + }, + { + "language": "ja", + "name": "tree decomposition", + "short": "tree decomposition" + } + ], + "aliases": [] + }, + { + "key": "burnside", + "isMeta": false, + "bojTagId": 16, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "번사이드 보조정리", + "short": "번사이드 보조정리" + }, + { + "language": "en", + "name": "burnside's lemma", + "short": "burnside's lemma" + }, + { + "language": "ja", + "name": "バーンサイドの補題", + "short": "バーンサイド" + } + ], + "aliases": [] + }, + { + "key": "discrete_log", + "isMeta": false, + "bojTagId": 146, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "이산 로그", + "short": "이산 로그" + }, + { + "language": "en", + "name": "discrete logarithm", + "short": "discrete logarithm" + }, + { + "language": "ja", + "name": "離散対数", + "short": "離散対数" + } + ], + "aliases": [ + { + "alias": "order" + } + ] + }, + { + "key": "geometry_hyper", + "isMeta": false, + "bojTagId": 132, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "4차원 이상의 기하학", + "short": "4차원 이상의 기하학" + }, + { + "language": "en", + "name": "geometry; hyperdimensional", + "short": "hyperdimensional" + }, + { + "language": "ja", + "name": "4次元以上での幾何学", + "short": "hyperdimensional" + } + ], + "aliases": [ + { + "alias": "4차원" + }, + { + "alias": "5차원" + }, + { + "alias": "6차원" + }, + { + "alias": "7차원" + }, + { + "alias": "8차원" + }, + { + "alias": "9차원" + }, + { + "alias": "4d" + }, + { + "alias": "5d" + }, + { + "alias": "6d" + }, + { + "alias": "7d" + }, + { + "alias": "8d" + }, + { + "alias": "9d" + } + ] + }, + { + "key": "bidirectional_search", + "isMeta": false, + "bojTagId": 129, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "양방향 탐색", + "short": "양방향 탐색" + }, + { + "language": "en", + "name": "bidirectional search", + "short": "bidirectional search" + }, + { + "language": "ja", + "name": "bidirectional search", + "short": "bidirectional search" + } + ], + "aliases": [] + }, + { + "key": "min_enclosing_circle", + "isMeta": false, + "bojTagId": 162, + "problemCount": 9, + "displayNames": [ + { + "language": "ko", + "name": "최소 외접원", + "short": "최소 외접원" + }, + { + "language": "en", + "name": "minimum enclosing circle", + "short": "minimum enclosing circle" + }, + { + "language": "ja", + "name": "最小外接円", + "short": "最小外接円" + } + ], + "aliases": [ + { + "alias": "bounding circle" + }, + { + "alias": "smallest enclosing circle" + }, + { + "alias": "minimum covering circle" + } + ] + }, + { + "key": "z", + "isMeta": false, + "bojTagId": 83, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "z", + "short": "Z" + }, + { + "language": "en", + "name": "z", + "short": "z" + }, + { + "language": "ja", + "name": "z", + "short": "z" + } + ], + "aliases": [] + }, + { + "key": "pick", + "isMeta": false, + "bojTagId": 187, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "픽의 정리", + "short": "픽" + }, + { + "language": "en", + "name": "pick's theorem", + "short": "pick's thm" + }, + { + "language": "ja", + "name": "ピックの定理", + "short": "ピック" + } + ], + "aliases": [] + }, + { + "key": "utf8", + "isMeta": false, + "bojTagId": 199, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "utf-8 입력 처리", + "short": "utf-8" + }, + { + "language": "en", + "name": "utf-8 inputs", + "short": "utf-8" + }, + { + "language": "ja", + "name": "utf-8入力の処理", + "short": "utf-8" + } + ], + "aliases": [] + }, + { + "key": "top_tree", + "isMeta": false, + "bojTagId": 105, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "탑 트리", + "short": "탑 트리" + }, + { + "language": "en", + "name": "top tree", + "short": "top tree" + }, + { + "language": "ja", + "name": "top tree", + "short": "top tree" + } + ], + "aliases": [] + }, + { + "key": "palindrome_tree", + "isMeta": false, + "bojTagId": 53, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "회문 트리", + "short": "회문 트리" + }, + { + "language": "en", + "name": "palindrome tree", + "short": "palindrome tree" + }, + { + "language": "ja", + "name": "palindrome tree", + "short": "palindrome tree" + } + ], + "aliases": [ + { + "alias": "팰린드롬트리" + } + ] + }, + { + "key": "monotone_queue_optimization", + "isMeta": false, + "bojTagId": 165, + "problemCount": 8, + "displayNames": [ + { + "language": "ko", + "name": "단조 큐를 이용한 최적화", + "short": "단조 큐를 이용한 최적화" + }, + { + "language": "en", + "name": "monotone queue optimization", + "short": "monotone queue optimization" + }, + { + "language": "ja", + "name": "monotone queue optimization", + "short": "monotone queue optimization" + } + ], + "aliases": [] + }, + { + "key": "knuth_x", + "isMeta": false, + "bojTagId": 174, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "크누스 X", + "short": "크누스 X" + }, + { + "language": "en", + "name": "knuth's x", + "short": "x" + }, + { + "language": "ja", + "name": "knuth's x", + "short": "x" + } + ], + "aliases": [] + }, + { + "key": "delaunay", + "isMeta": false, + "bojTagId": 21, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "델로네 삼각분할", + "short": "델로네 삼각분할" + }, + { + "language": "en", + "name": "delaunay triangulation", + "short": "delaunay triangulation" + }, + { + "language": "ja", + "name": "ドロネー三角形分割", + "short": "ドロネー" + } + ], + "aliases": [] + }, + { + "key": "dominator_tree", + "isMeta": false, + "bojTagId": 135, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "도미네이터 트리", + "short": "도미네이터 트리" + }, + { + "language": "en", + "name": "dominator tree", + "short": "dominator tree" + }, + { + "language": "ja", + "name": "dominator tree", + "short": "dominator tree" + } + ], + "aliases": [] + }, + { + "key": "stable_marriage", + "isMeta": false, + "bojTagId": 192, + "problemCount": 7, + "displayNames": [ + { + "language": "ko", + "name": "안정 결혼 문제", + "short": "안정 결혼" + }, + { + "language": "en", + "name": "stable marriage problem", + "short": "smp" + }, + { + "language": "ja", + "name": "stable marriage problem", + "short": "smp" + } + ], + "aliases": [] + }, + { + "key": "rope", + "isMeta": false, + "bojTagId": 159, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "로프", + "short": "로프" + }, + { + "language": "en", + "name": "rope", + "short": "rope" + }, + { + "language": "ja", + "name": "rope", + "short": "rope" + } + ], + "aliases": [] + }, + { + "key": "bayes", + "isMeta": false, + "bojTagId": 114, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "베이즈 정리", + "short": "베이즈 정리" + }, + { + "language": "en", + "name": "bayes theorem", + "short": "bayes thm" + }, + { + "language": "ja", + "name": "ベイズの定理", + "short": "ベイズ" + } + ], + "aliases": [ + { + "alias": "조건부확률" + } + ] + }, + { + "key": "knuth", + "isMeta": false, + "bojTagId": 90, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "크누스 최적화", + "short": "크누스 최적화" + }, + { + "language": "en", + "name": "knuth optimization", + "short": "knuth" + }, + { + "language": "ja", + "name": "knuth optimization", + "short": "knuth" + } + ], + "aliases": [] + }, + { + "key": "dancing_links", + "isMeta": false, + "bojTagId": 173, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "춤추는 링크", + "short": "춤추는 링크" + }, + { + "language": "en", + "name": "dancing links", + "short": "dancing links" + }, + { + "language": "ja", + "name": "dancing links", + "short": "dancing links" + } + ], + "aliases": [] + }, + { + "key": "degree_sequence", + "isMeta": false, + "bojTagId": 200, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "차수열", + "short": "차수열" + }, + { + "language": "en", + "name": "degree sequence", + "short": "degree sequence" + }, + { + "language": "ja", + "name": "degree sequence", + "short": "degree sequence" + } + ], + "aliases": [] + }, + { + "key": "differential_cryptanalysis", + "isMeta": false, + "bojTagId": 185, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "차분 공격", + "short": "차분 공격" + }, + { + "language": "en", + "name": "differential cryptanalysis", + "short": "differential cryptanalysis" + }, + { + "language": "ja", + "name": "differential cryptanalysis", + "short": "differential cryptanalysis" + } + ], + "aliases": [ + { + "alias": "dc" + } + ] + }, + { + "key": "geometric_boolean_operations", + "isMeta": false, + "bojTagId": 202, + "problemCount": 6, + "displayNames": [ + { + "language": "ko", + "name": "도형에서의 불 연산", + "short": "도형에서의 불 연산" + }, + { + "language": "en", + "name": "boolean operations on geometric objects", + "short": "geometric boolean operations" + }, + { + "language": "ja", + "name": "図形のブール演算", + "short": "図形のブール演算" + } + ], + "aliases": [ + { + "alias": "병합" + }, + { + "alias": "교집합" + }, + { + "alias": "합집합" + }, + { + "alias": "union" + }, + { + "alias": "intersect" + } + ] + }, + { + "key": "hirschberg", + "isMeta": false, + "bojTagId": 163, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "히르쉬버그", + "short": "히르쉬버그" + }, + { + "language": "en", + "name": "hirschberg's", + "short": "hirschberg's" + }, + { + "language": "ja", + "name": "hirschberg's", + "short": "hirschberg's" + } + ], + "aliases": [ + { + "alias": "hirschburg" + } + ] + }, + { + "key": "suffix_tree", + "isMeta": false, + "bojTagId": 182, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "접미사 트리", + "short": "접미사 트리" + }, + { + "language": "en", + "name": "suffix tree", + "short": "suffix tree" + }, + { + "language": "ja", + "name": "suffix tree", + "short": "suffix tree" + } + ], + "aliases": [] + }, + { + "key": "chordal_graph", + "isMeta": false, + "bojTagId": 201, + "problemCount": 5, + "displayNames": [ + { + "language": "en", + "name": "chordal graph", + "short": "chordal graph" + }, + { + "language": "ko", + "name": "현 그래프", + "short": "현 그래프" + }, + { + "language": "ja", + "name": "弦グラフ", + "short": "弦グラフ" + } + ], + "aliases": [] + }, + { + "key": "discrete_sqrt", + "isMeta": false, + "bojTagId": 147, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "이산 제곱근", + "short": "이산 제곱근" + }, + { + "language": "en", + "name": "discrete square root", + "short": "discrete square root" + }, + { + "language": "ja", + "name": "離散平方根", + "short": "離散平方根" + } + ], + "aliases": [ + { + "alias": "루트" + } + ] + }, + { + "key": "gradient_descent", + "isMeta": false, + "bojTagId": 208, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "경사 하강법", + "short": "경사 하강법" + }, + { + "language": "en", + "name": "gradient descent", + "short": "gradient descent" + }, + { + "language": "ja", + "name": "勾配降下法", + "short": "勾配降下法" + } + ], + "aliases": [] + }, + { + "key": "polynomial_interpolation", + "isMeta": false, + "bojTagId": 209, + "problemCount": 5, + "displayNames": [ + { + "language": "ko", + "name": "다항식 보간법", + "short": "다항식 보간법" + }, + { + "language": "en", + "name": "polynomial interpolation", + "short": "polynomial interpolation" + }, + { + "language": "ja", + "name": "多項式補間", + "short": "多項式補間" + } + ], + "aliases": [] + }, + { + "key": "lgv", + "isMeta": false, + "bojTagId": 214, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "린드스트롬–게셀–비엔노 보조정리", + "short": "lgv 보조정리" + }, + { + "language": "en", + "name": "lindström–gessel–viennot lemma", + "short": "lgv lemma" + }, + { + "language": "ja", + "name": "lindström–gessel–viennot lemma", + "short": "lgv lemma" + } + ], + "aliases": [] + }, + { + "key": "green", + "isMeta": false, + "bojTagId": 183, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "그린 정리", + "short": "그린" + }, + { + "language": "en", + "name": "green's theorem", + "short": "green's thm" + }, + { + "language": "ja", + "name": "グリーンの定理", + "short": "グリーン" + } + ], + "aliases": [] + }, + { + "key": "directed_mst", + "isMeta": false, + "bojTagId": 23, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "유향 최소 신장 트리", + "short": "유향 최소 신장 트리" + }, + { + "language": "en", + "name": "directed minimum spanning tree", + "short": "dmst" + }, + { + "language": "ja", + "name": "最小全域有向木", + "short": "dmst" + } + ], + "aliases": [ + { + "alias": "유향mst" + } + ] + }, + { + "key": "stoer_wagner", + "isMeta": false, + "bojTagId": 75, + "problemCount": 4, + "displayNames": [ + { + "language": "ko", + "name": "스토어–바그너", + "short": "스토어–바그너" + }, + { + "language": "en", + "name": "stoer–wagner", + "short": "stoer–wagner" + }, + { + "language": "ja", + "name": "stoer–wagner", + "short": "stoer–wagner" + } + ], + "aliases": [ + { + "alias": "stoer-wagner" + }, + { + "alias": "stoer-karger" + }, + { + "alias": "stoer" + }, + { + "alias": "wagner" + }, + { + "alias": "karger" + }, + { + "alias": "global min cut" + }, + { + "alias": "전역 최소 컷" + } + ] + }, + { + "key": "birthday", + "isMeta": false, + "bojTagId": 203, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "생일 문제", + "short": "생일" + }, + { + "language": "en", + "name": "birthday problem", + "short": "birthday" + }, + { + "language": "ja", + "name": "birthday problem", + "short": "birthday" + } + ], + "aliases": [ + { + "alias": "패러독스" + }, + { + "alias": "파라독스" + }, + { + "alias": "birthday" + } + ] + }, + { + "key": "majority_vote", + "isMeta": false, + "bojTagId": 160, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "보이어–무어 다수결 투표", + "short": "보이어–무어 다수결 투표" + }, + { + "language": "en", + "name": "boyer–moore majority vote", + "short": "majority vote" + }, + { + "language": "ja", + "name": "boyer–moore majority vote", + "short": "majority vote" + } + ], + "aliases": [] + }, + { + "key": "multipoint_evaluation", + "isMeta": false, + "bojTagId": 196, + "problemCount": 3, + "displayNames": [ + { + "language": "ko", + "name": "다중 대입값 계산", + "short": "다중 계산" + }, + { + "language": "en", + "name": "multipoint evaluation", + "short": "multipoint evaluation" + }, + { + "language": "ja", + "name": "多点評価", + "short": "多点評価" + } + ], + "aliases": [] + }, + { + "key": "lte", + "isMeta": false, + "bojTagId": 212, + "problemCount": 2, + "displayNames": [ + { + "language": "ko", + "name": "지수승강 보조정리", + "short": "지수승강" + }, + { + "language": "en", + "name": "lifting the exponent lemma", + "short": "lte lemma" + }, + { + "language": "ja", + "name": "lifting the exponent lemma", + "short": "lte lemma" + } + ], + "aliases": [] + }, + { + "key": "hackenbush", + "isMeta": false, + "bojTagId": 205, + "problemCount": 2, + "displayNames": [ + { + "language": "ko", + "name": "하켄부시 게임", + "short": "하켄부시 게임" + }, + { + "language": "en", + "name": "hackenbush", + "short": "hackenbush" + }, + { + "language": "ja", + "name": "hackenbush", + "short": "hackenbush" + } + ], + "aliases": [] + }, + { + "key": "rb_tree", + "isMeta": false, + "bojTagId": 94, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "레드-블랙 트리", + "short": "레드-블랙 트리" + }, + { + "language": "en", + "name": "red-black tree", + "short": "rb tree" + }, + { + "language": "ja", + "name": "red-black tree", + "short": "rb tree" + } + ], + "aliases": [ + { + "alias": "rb트리" + } + ] + }, + { + "key": "floor_sum", + "isMeta": false, + "bojTagId": 218, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "유리 등차수열의 내림 합", + "short": "유리 등차수열의 내림 합" + }, + { + "language": "en", + "name": "sum of floor of rational arithmetic sequence", + "short": "floor sum" + } + ], + "aliases": [] + }, + { + "key": "discrete_kth_root", + "isMeta": false, + "bojTagId": 149, + "problemCount": 1, + "displayNames": [ + { + "language": "ko", + "name": "이산 k제곱근", + "short": "이산 k제곱근" + }, + { + "language": "en", + "name": "discrete k-th root", + "short": "discrete k-th root" + }, + { + "language": "ja", + "name": "離散k平方根", + "short": "離散k平方根" + } + ], + "aliases": [ + { + "alias": "루트" + } + ] + }, + { + "key": "a_star", + "isMeta": false, + "bojTagId": 186, + "problemCount": 0, + "displayNames": [ + { + "language": "ko", + "name": "a*", + "short": "a*" + }, + { + "language": "en", + "name": "a*", + "short": "a*" + }, + { + "language": "ja", + "name": "a*", + "short": "a*" + } + ], + "aliases": [ + { + "alias": "에이" + }, + { + "alias": "에이스타" + } + ] + } + ] +} \ No newline at end of file From fa245d466d067ee1be1e1ea5dc22cce4e519ce06 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 10:26:30 +0900 Subject: [PATCH 215/552] refactor(tle.models): rename field `date_joined` -> `created_at` and expose `UserManager` --- app/tle/models/__init__.py | 3 ++- app/tle/models/user.py | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index 3e3e6a7..2264f70 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,4 +1,4 @@ -from tle.models.user import User +from tle.models.user import User, UserManager from tle.models.user_solved_tier import UserSolvedTier from tle.models.crew import Crew @@ -19,6 +19,7 @@ __all__ = ( 'User', + 'UserManager', 'UserSolvedTier', 'Crew', diff --git a/app/tle/models/user.py b/app/tle/models/user.py index c324d8c..7de0733 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -86,7 +86,7 @@ class User(AbstractBaseUser, PermissionsMixin): is_superuser = models.BooleanField(default=False) first_name = models.TextField(blank=True, null=True, default=None) last_name = models.TextField(blank=True, null=True, default=None) - date_joined = models.DateTimeField(default=timezone.now) + created_at = models.DateTimeField(default=timezone.now) objects = UserManager() @@ -95,6 +95,10 @@ def crews(self): for member in self.members: yield member.crew + @property + def date_joined(self): + return self.created_at + USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] From db53b2d41c5ed7cac35add2707bc0edf81dccad5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 10:27:37 +0900 Subject: [PATCH 216/552] feat(tle.serializers): create `user_serializers.py` --- app/tle/serializers/__init__.py | 9 +-- app/tle/serializers/user.py | 66 --------------- app/tle/serializers/user_serializer.py | 108 +++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 74 deletions(-) delete mode 100644 app/tle/serializers/user.py create mode 100644 app/tle/serializers/user_serializer.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index c0a0d15..5293e32 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1,8 +1 @@ -from tle.serializers.user import * - - -__all__ = ( - 'UserSerializer', - 'UserSignInSerializer', - 'UserSignUpSerializer', -) +from tle.serializers.user_serializer import * diff --git a/app/tle/serializers/user.py b/app/tle/serializers/user.py deleted file mode 100644 index c5dd61f..0000000 --- a/app/tle/serializers/user.py +++ /dev/null @@ -1,66 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import User - - -class BOJ_Mixin: - def get_boj(self, obj: User) -> dict: - return { - 'username': obj.boj_username, - 'tier': obj.boj_tier, - 'tier_updated_at': obj.boj_tier_updated_at, - } - - -USER_SERIALIZER_FIELDS = { - 'id': {'read_only': True}, - 'profile_image': {'read_only': True}, - 'username': {'read_only': True}, - 'boj': {'read_only': True}, -} - - -class UserSerializer(ModelSerializer, BOJ_Mixin): - boj = SerializerMethodField() - - class Meta: - model = User - fields = USER_SERIALIZER_FIELDS.keys() - extra_kwargs = USER_SERIALIZER_FIELDS - - -USER_SIGN_IN_SERIALIZER_FIELDS = { - **USER_SERIALIZER_FIELDS, - 'email': {}, - 'password': {'write_only': True}, -} - - -class UserSignInSerializer(ModelSerializer, BOJ_Mixin): - boj = SerializerMethodField() - - class Meta: - model = User - fields = USER_SIGN_IN_SERIALIZER_FIELDS.keys() - extra_kwargs = USER_SIGN_IN_SERIALIZER_FIELDS - - -USER_SIGN_UP_SERIALIZER_FIELDS = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - - 'email': {}, - 'password': {'write_only': True}, - 'profile_image': {}, - 'username': {}, - 'boj_username': {'write_only': True}, -} - - -class UserSignUpSerializer(ModelSerializer): - boj = SerializerMethodField() - - class Meta: - model = User - fields = USER_SIGN_UP_SERIALIZER_FIELDS.keys() - extra_kwargs = USER_SIGN_UP_SERIALIZER_FIELDS diff --git a/app/tle/serializers/user_serializer.py b/app/tle/serializers/user_serializer.py new file mode 100644 index 0000000..68fbfb9 --- /dev/null +++ b/app/tle/serializers/user_serializer.py @@ -0,0 +1,108 @@ +from rest_framework.exceptions import PermissionDenied +from rest_framework.serializers import * + +from tle.models import User, UserManager + + +__all__ = ( + 'UserSerializer', + 'UserSignInSerializer', + 'UserMinimalSerializer', +) + + +class UserSerializer(ModelSerializer): + boj = SerializerMethodField(read_only=True) + + class Meta: + model = User + fields = [ + 'id', + 'email', + 'profile_image', + 'username', + 'password', + 'boj_username', + 'boj', + 'created_at', + 'last_login', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'boj': {'read_only': True}, + 'created_at': {'read_only': True}, + 'last_login': {'read_only': True}, + 'password': {'write_only': True}, + 'boj_username': {'write_only': True}, + } + + def get_boj(self, obj: User) -> dict: + return { + 'username': obj.boj_username, + 'profile_url': f'https://boj.kr/{obj.boj_username}', + 'tier': obj.boj_tier, + 'tier_updated_at': obj.boj_tier_updated_at, + } + + def create(self, validated_data): + user_manager: UserManager = User.objects + return user_manager.create_user(**validated_data) + + +class UserSignInSerializer(ModelSerializer): + email = EmailField(write_only=True, validators=None) + boj = SerializerMethodField() + + class Meta: + model = User + fields = [ + 'id', + 'email', + 'profile_image', + 'username', + 'password', + 'boj', + 'created_at', + 'last_login', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'profile_image': {'read_only': True}, + 'username': {'read_only': True}, + 'boj': {'read_only': True}, + 'created_at': {'read_only': True}, + 'last_login': {'read_only': True}, + 'password': {'write_only': True}, + } + + def get_boj(self, obj: User) -> dict: + return { + 'username': obj.boj_username, + 'profile_url': f'https://boj.kr/{obj.boj_username}', + 'tier': obj.boj_tier, + 'tier_updated_at': obj.boj_tier_updated_at, + } + + def create(self, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def update(self, instance, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def save(self, **kwargs): + raise PermissionDenied('Cannot update user through this serializer') + + +class UserMinimalSerializer(ModelSerializer): + class Meta: + model = User + fields = [ + 'id', + 'profile_image', + 'username', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'profile_image': {'read_only': True}, + 'username': {'read_only': True}, + } From 9a9887a05194cb101771d07c11963760442b9324 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 10:43:08 +0900 Subject: [PATCH 217/552] feat(tle.models): add `UserManager.create()` --- app/tle/models/user.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/tle/models/user.py b/app/tle/models/user.py index 7de0733..6d1aca9 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -19,6 +19,9 @@ def get_profile_image_path(instance: User, filename: str) -> str: class UserManager(BaseUserManager): model: typing.Callable[..., User] + def create(self, **kwargs): + return self.create_user(**kwargs) + def create_user(self, email, username, password=None, **extra_fields): if not email: raise ValueError('The Email field must be set') From 60c7417e9626d3e0d264f3e77bfa291a8dfe802e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 10:43:38 +0900 Subject: [PATCH 218/552] refactor(tle.views): rename user.py -> user_viewset.py --- app/tle/views/viewsets/__init__.py | 2 +- .../viewsets/{user.py => user_viewset.py} | 56 ++++++++++--------- 2 files changed, 31 insertions(+), 27 deletions(-) rename app/tle/views/viewsets/{user.py => user_viewset.py} (64%) diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index 43c1ce6..e6d2227 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1 +1 @@ -from tle.views.viewsets.user import UserViewSet +from tle.views.viewsets.user_viewset import UserViewSet diff --git a/app/tle/views/viewsets/user.py b/app/tle/views/viewsets/user_viewset.py similarity index 64% rename from app/tle/views/viewsets/user.py rename to app/tle/views/viewsets/user_viewset.py index 2698392..8a098a8 100644 --- a/app/tle/views/viewsets/user.py +++ b/app/tle/views/viewsets/user_viewset.py @@ -8,14 +8,11 @@ from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import Serializer from rest_framework.viewsets import GenericViewSet from tle.models import User -from tle.serializers import ( - UserSerializer, - UserSignUpSerializer, - UserSignInSerializer, -) +from tle.serializers import * from tle.views.permissions import * @@ -30,36 +27,44 @@ class UserViewSet(GenericViewSet): queryset = User.objects.all() permission_classes = [AllowAny] - SERIALIZERS = { - 'sign_up': UserSignUpSerializer, - 'sign_in': UserSignInSerializer, - } + # Overrides - def get_serializer(self, *args, **kwargs): - if self.action in self.__class__.SERIALIZERS: - return self.__class__.SERIALIZERS[self.action](*args, **kwargs) - return UserSerializer(*args, **kwargs) + def get_serializer_class(self): + if self.action == 'sign_in': + return UserSignInSerializer + else: + return UserSerializer - def current(self, request: Request): - serializer = self.get_serializer(instance=request.user) - return Response(serializer.data) + # Helpers - def sign_up(self, request: Request): + def get_validated_serializer(self, request: Request) -> Serializer: serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - serializer.instance = User.objects.create_user(**serializer.validated_data) - return Response(serializer.data) + return serializer - def sign_in(self, request: Request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) + def authenticate(self, request: Request) -> User: + serializer = self.get_validated_serializer(request) email = serializer.validated_data['email'] password = serializer.validated_data['password'] - user = authenticate(request, email, password) + user = authenticate(request, username=email, password=password) if user is None: raise AuthenticationFailed('Invalid email or password') - login(request, user) - serializer.instance = user + + # Actions + + def current(self, request: Request): + serializer = self.get_serializer(instance=request.user) + return Response(serializer.data) + + def sign_up(self, request: Request): + serializer = self.get_validated_serializer(request) + serializer.save() + return Response(serializer.data) + + def sign_in(self, request: Request): + serializer = self.get_validated_serializer(request) + serializer.instance = self.authenticate(request) + login(request, serializer.instance) return Response(serializer.data) def sign_out(self, request: Request): @@ -67,5 +72,4 @@ def sign_out(self, request: Request): return Response(status=HTTPStatus.OK) # TODO: 이메일 인증 - # TODO: 비밀번호 찾기 From aecae1eecde9d50e16170c19beda04c95c4c2692 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 10:48:45 +0900 Subject: [PATCH 219/552] feat(tle.views): add permissions.py --- app/tle/views/permissions.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/tle/views/permissions.py diff --git a/app/tle/views/permissions.py b/app/tle/views/permissions.py new file mode 100644 index 0000000..1343289 --- /dev/null +++ b/app/tle/views/permissions.py @@ -0,0 +1,23 @@ +from rest_framework.permissions import ( + AllowAny, + BasePermission, + IsAuthenticated, + IsAdminUser, + SAFE_METHODS, +) + + +__all__ = ( + 'AllowAny', + 'IsAuthenticated', + 'IsAdminUser', + + 'IsReadOnly', +) + + +class IsReadOnly(BasePermission): + def has_permission(self, request, view): + return bool( + request.method in SAFE_METHODS, + ) From 85c8b2a33c426a7f3bbb8e7c5fec441b5e2ac5db Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 11:43:36 +0900 Subject: [PATCH 220/552] feat(tle.models): add `get_name()` to `ProblemDifficulty` --- app/tle/models/problem_difficulty.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/tle/models/problem_difficulty.py b/app/tle/models/problem_difficulty.py index 9c1b441..07d26a6 100644 --- a/app/tle/models/problem_difficulty.py +++ b/app/tle/models/problem_difficulty.py @@ -1,7 +1,29 @@ from django.db import models +NAMES = { + 'ko': { + 1: '쉬움', + 2: '보통', + 3: '어려움', + }, + 'en': { + 1: 'EASY', + 2: 'NORMAL', + 3: 'HARD', + }, +} + + class ProblemDifficulty(models.IntegerChoices): EASY = 1, '쉬움' NORMAL = 2, '보통' HARD = 3, '어려움' + + def get_name(self, lang='ko') -> str: + if lang not in NAMES: + raise ValueError( + f'Invalid language: {lang}, ', + f'choose from {NAMES.keys()}' + ) + return NAMES[lang][self.value] From f32dcb700f7b39957ac9dfc4c55f9d9b5dff702a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 11:50:30 +0900 Subject: [PATCH 221/552] refactor(tle.serializers): rename problem.py -> problem_serializer.py --- app/tle/serializers/__init__.py | 1 + app/tle/serializers/problem.py | 28 ----- app/tle/serializers/problem_serializer.py | 140 ++++++++++++++++++++++ 3 files changed, 141 insertions(+), 28 deletions(-) delete mode 100644 app/tle/serializers/problem.py create mode 100644 app/tle/serializers/problem_serializer.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index 5293e32..f211aa7 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1 +1,2 @@ from tle.serializers.user_serializer import * +from tle.serializers.problem_serializer import * diff --git a/app/tle/serializers/problem.py b/app/tle/serializers/problem.py deleted file mode 100644 index b67e62e..0000000 --- a/app/tle/serializers/problem.py +++ /dev/null @@ -1,28 +0,0 @@ -from rest_framework.serializers import ModelSerializer - -from tle.models import Problem -from tle.serializers.user import UserSerializer - - -PROBLEM_SERIALIZER_FIELDS = { - 'id': {'read_only': True}, - 'title': {}, - 'link': {}, - 'description': {}, - 'input_description': {}, - 'output_description': {}, - 'memory_limit': {}, - 'time_limit': {}, - 'created_at': {'read_only': True}, - 'created_by': {'read_only': True}, - 'updated_at': {'read_only': True}, -} - - -class ProblemSerializer(ModelSerializer): - created_by = UserSerializer(read_only=True) - - class Meta: - model = Problem - fields = PROBLEM_SERIALIZER_FIELDS.keys() - extra_kwargs = PROBLEM_SERIALIZER_FIELDS diff --git a/app/tle/serializers/problem_serializer.py b/app/tle/serializers/problem_serializer.py new file mode 100644 index 0000000..6e7cb43 --- /dev/null +++ b/app/tle/serializers/problem_serializer.py @@ -0,0 +1,140 @@ +from rest_framework.serializers import * + +from tle.models import ( + Problem, + ProblemAnalysis, + ProblemDifficulty, + ProblemTag, +) +from tle.serializers.user_serializer import UserMinimalSerializer + + +__all__ = ( + 'ProblemSerializer', + 'ProblemMinimalSerializer', + 'ProblemAnalysisSerializer', + 'ProblemTagSerializer', +) + + +class ProblemTagSerializer(ModelSerializer): + parent = SerializerMethodField() + + class Meta: + model = ProblemTag + fields = [ + 'parent', + 'key', + 'name_ko', + 'name_en', + ] + read_only_fields = ['__all__'] + + def get_parent(self, obj: ProblemTag): + if obj.parent is None: + return None + return ProblemTagSerializer(obj.parent).data + + +class ProblemDifficultySerializer(Serializer): + name_en = SerializerMethodField() + name_ko = SerializerMethodField() + value = SerializerMethodField() + + def get_name_ko(self, choice: int): + return self._get_obj(choice).get_name('ko') + + def get_name_en(self, choice: int): + return self._get_obj(choice).get_name('en') + + def get_value(self, choice: int): + return self._get_obj(choice).value + + def _get_obj(self, choice: int) -> ProblemDifficulty: + return ProblemDifficulty(choice) + + +class ProblemAnalysisSerializer(ModelSerializer): + tags = ProblemTagSerializer(many=True, read_only=True) + difficulty = ProblemDifficultySerializer(read_only=True) + + class Meta: + model = ProblemAnalysis + fields = [ + 'difficulty', + 'tags', + 'time_complexity', + 'hint', + 'created_at', + ] + read_only_fields = ['__all__'] + + +class ProblemMinimalSerializer(ModelSerializer): + created_by = UserMinimalSerializer(read_only=True) + + class Meta: + model = Problem + fields = [ + 'id', + 'title', + 'link', + 'created_at', + 'created_by', + 'updated_at', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'title': {'read_only': True}, + 'link': {'read_only': True}, + 'created_at': {'read_only': True}, + 'created_by': {'read_only': True}, + 'updated_at': {'read_only': True}, + } + + +class ProblemSerializer(ModelSerializer): + analysis = ProblemAnalysisSerializer(read_only=True) + memory_limit_unit = SerializerMethodField() + time_limit_unit = SerializerMethodField() + created_by = UserMinimalSerializer(read_only=True) + + class Meta: + model = Problem + fields = [ + 'id', + 'analysis', + 'title', + 'link', + 'description', + 'input_description', + 'output_description', + 'memory_limit', + 'memory_limit_unit', + 'time_limit', + 'time_limit_unit', + 'created_at', + 'created_by', + 'updated_at', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'anaysis': {'read_only': True}, + 'created_at': {'read_only': True}, + 'created_by': {'read_only': True}, + 'updated_at': {'read_only': True}, + } + + def get_memory_limit_unit(self, obj: Problem): + return { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB", + } + + def get_time_limit_unit(self, obj: Problem): + return { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s", + } From 8bc442bfe04502324ee141f0b6bdbb8698e0ecbf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:05:55 +0900 Subject: [PATCH 222/552] feat(tle.views): add `ProblemViewSet` --- app/tle/views/urls.py | 12 +++++++++++ app/tle/views/viewsets/__init__.py | 1 + app/tle/views/viewsets/problem_viewset.py | 25 +++++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 app/tle/views/viewsets/problem_viewset.py diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index ee4f085..ac3abab 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -10,4 +10,16 @@ path("signout", UserViewSet.as_view({"get": "sign_out"})), path("current", UserViewSet.as_view({"get": "current"})), ])), + path("problem", ProblemViewSet.as_view({ + "post": "create", + "get": "list", + })), + path("problem/", include([ + path("", ProblemViewSet.as_view({ + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + })), + ])), ] diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index e6d2227..ca486df 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1 +1,2 @@ from tle.views.viewsets.user_viewset import UserViewSet +from tle.views.viewsets.problem_viewset import ProblemViewSet diff --git a/app/tle/views/viewsets/problem_viewset.py b/app/tle/views/viewsets/problem_viewset.py new file mode 100644 index 0000000..7e08b06 --- /dev/null +++ b/app/tle/views/viewsets/problem_viewset.py @@ -0,0 +1,25 @@ +from rest_framework.viewsets import ModelViewSet + +from tle.models import Problem +from tle.serializers import * +from tle.views.permissions import * + + +class ProblemViewSet(ModelViewSet): + """문제 태그 목록 조회 + 생성 기능""" + permission_classes = [IsAuthenticated] + lookup_field = 'id' + + # TODO: 내가 만든 문제만 수정할 수 있도록 변경 + + def get_serializer_class(self): + if self.action in ['create', 'list']: + return ProblemMinimalSerializer + return ProblemSerializer + + def get_queryset(self): + user = self.request.user + if user.is_staff: + return Problem.objects.all() + # TODO: 내가 가입한 크루에서 풀어본 문제도 조회할 수 있도록 수정 + return Problem.objects.filter(created_by=user) From dec7cac43972f195c25c4d00652e07574b9b1f91 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:31:28 +0900 Subject: [PATCH 223/552] =?UTF-8?q?fix(tle.models):=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20type=20annotation=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 4 ++-- app/tle/models/problem.py | 11 +++++++++++ app/tle/models/problem_analysis.py | 18 +++++------------- app/tle/models/user.py | 10 +++++----- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 68babcc..4bba1bd 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -100,8 +100,8 @@ class FieldName: CrewApplicant as T_CrewApplicant, CrewMember as T_CrewMember, ) - applicants: models.QuerySet[T_CrewApplicant] - members: models.QuerySet[T_CrewMember] + applicants: models.ManyToManyField[T_CrewApplicant] + members: models.ManyToManyField[T_CrewMember] def __repr__(self) -> str: return f'[{self.emoji} {self.name}]' diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index c02add0..745efc7 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -1,3 +1,5 @@ +import typing + from django.db import models from tle.models.user import User @@ -42,6 +44,15 @@ class Problem(models.Model): ) updated_at = models.DateTimeField(auto_now=True) + class FieldName: + ANALYSIS = 'analysis' + + if typing.TYPE_CHECKING: + from . import ( + ProblemAnalysis as T_ProblemAnalysis, + ) + analysis: models.OneToOneField[T_ProblemAnalysis] + def __repr__(self) -> str: return f'[{self.title}]' diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py index a07e381..c25871f 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/problem_analysis.py @@ -9,23 +9,17 @@ class ProblemAnalysis(models.Model): problem = models.OneToOneField( Problem, on_delete=models.CASCADE, - related_name='analysis', - help_text=( - '문제를 입력해주세요.' - ), + related_name=Problem.FieldName.ANALYSIS, + help_text='문제를 입력해주세요.', ) difficulty = models.IntegerField( - help_text=( - '문제 난이도를 입력해주세요.' - ), + help_text='문제 난이도를 입력해주세요.', choices=ProblemDifficulty.choices, ) tags = models.ManyToManyField( ProblemTag, related_name='problems', - help_text=( - '문제의 DSA 태그를 입력해주세요.' - ), + help_text='문제의 DSA 태그를 입력해주세요.', ) time_complexity = models.CharField( max_length=100, @@ -38,9 +32,7 @@ class ProblemAnalysis(models.Model): ], ) hint = models.JSONField( - help_text=( - '문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.' - ), + help_text='문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.', validators=[ # TODO: 힌트 검증 로직 추가 ], diff --git a/app/tle/models/user.py b/app/tle/models/user.py index 6d1aca9..cdf927d 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -121,11 +121,11 @@ class FieldName: Submission as T_Submission, SubmissionComment as T_SubmissionComment, ) - problems: models.QuerySet[T_Problem] - applicants: models.QuerySet[T_CrewApplicant] - members: models.QuerySet[T_CrewMember] - submissions: models.QuerySet[T_Submission] - comments: models.QuerySet[T_SubmissionComment] + problems: models.ManyToManyField[T_Problem] + applicants: models.ManyToManyField[T_CrewApplicant] + members: models.ManyToManyField[T_CrewMember] + submissions: models.ManyToManyField[T_Submission] + comments: models.ManyToManyField[T_SubmissionComment] def __repr__(self) -> str: return f'[@{self.username}]' From 94e3040a56512911ef5e28a900adeadffb36ec5d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:33:08 +0900 Subject: [PATCH 224/552] =?UTF-8?q?feat(tle.models):=20`ProblemDifficulty`?= =?UTF-8?q?=EC=97=90=20`UNDER=20ANALYASIS`=20=EC=84=A0=ED=83=9D=EC=A7=80?= =?UTF-8?q?=EB=A5=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem_difficulty.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/tle/models/problem_difficulty.py b/app/tle/models/problem_difficulty.py index 07d26a6..1fc9047 100644 --- a/app/tle/models/problem_difficulty.py +++ b/app/tle/models/problem_difficulty.py @@ -3,11 +3,13 @@ NAMES = { 'ko': { + 0: '분석 중', 1: '쉬움', 2: '보통', 3: '어려움', }, 'en': { + 0: 'UNDER ANALYSIS', 1: 'EASY', 2: 'NORMAL', 3: 'HARD', @@ -16,14 +18,18 @@ class ProblemDifficulty(models.IntegerChoices): - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' - - def get_name(self, lang='ko') -> str: + @classmethod + def get_name(cls, value: int, lang='ko') -> str: if lang not in NAMES: raise ValueError( f'Invalid language: {lang}, ', f'choose from {NAMES.keys()}' ) - return NAMES[lang][self.value] + return NAMES[lang][value] + + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' + + def get_name(self, value: int, lang='ko') -> str: + return ProblemDifficulty.get_name(value, lang) From 88fb7b3d1b870897485e42c468874af8a05c223d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:34:09 +0900 Subject: [PATCH 225/552] =?UTF-8?q?feat(tle.serializers):=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=EC=8B=9C=20?= =?UTF-8?q?=EB=82=9C=EC=9D=B4=EB=8F=84=EB=8F=84=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=90=98=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/problem_serializer.py | 25 +++++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/app/tle/serializers/problem_serializer.py b/app/tle/serializers/problem_serializer.py index 6e7cb43..4f44406 100644 --- a/app/tle/serializers/problem_serializer.py +++ b/app/tle/serializers/problem_serializer.py @@ -41,17 +41,14 @@ class ProblemDifficultySerializer(Serializer): name_ko = SerializerMethodField() value = SerializerMethodField() - def get_name_ko(self, choice: int): - return self._get_obj(choice).get_name('ko') + def get_name_ko(self, value: int): + return ProblemDifficulty.get_name(value, 'ko') - def get_name_en(self, choice: int): - return self._get_obj(choice).get_name('en') + def get_name_en(self, value: int): + return ProblemDifficulty.get_name(value, 'en') - def get_value(self, choice: int): - return self._get_obj(choice).value - - def _get_obj(self, choice: int) -> ProblemDifficulty: - return ProblemDifficulty(choice) + def get_value(self, value: int): + return value class ProblemAnalysisSerializer(ModelSerializer): @@ -72,6 +69,7 @@ class Meta: class ProblemMinimalSerializer(ModelSerializer): created_by = UserMinimalSerializer(read_only=True) + difficulty = SerializerMethodField() class Meta: model = Problem @@ -79,6 +77,7 @@ class Meta: 'id', 'title', 'link', + 'difficulty', 'created_at', 'created_by', 'updated_at', @@ -87,11 +86,19 @@ class Meta: 'id': {'read_only': True}, 'title': {'read_only': True}, 'link': {'read_only': True}, + 'difficulty': {'read_only': True}, 'created_at': {'read_only': True}, 'created_by': {'read_only': True}, 'updated_at': {'read_only': True}, } + def get_difficulty(self, obj: Problem): + try: + difficulty = obj.analysis.difficulty + except ProblemAnalysis.DoesNotExist: + difficulty = 0 + return ProblemDifficultySerializer(difficulty).data + class ProblemSerializer(ModelSerializer): analysis = ProblemAnalysisSerializer(read_only=True) From b5d6aac30c6af4718f1b6055d8bb1bbedf4ea88a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:34:20 +0900 Subject: [PATCH 226/552] =?UTF-8?q?refactor(tle.models):=20=EB=AF=B8?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem_difficulty.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/tle/models/problem_difficulty.py b/app/tle/models/problem_difficulty.py index 1fc9047..2e9d2a7 100644 --- a/app/tle/models/problem_difficulty.py +++ b/app/tle/models/problem_difficulty.py @@ -30,6 +30,3 @@ def get_name(cls, value: int, lang='ko') -> str: EASY = 1, '쉬움' NORMAL = 2, '보통' HARD = 3, '어려움' - - def get_name(self, value: int, lang='ko') -> str: - return ProblemDifficulty.get_name(value, lang) From e89ccd40cdc085f9df89a229b9d423a91eec9e7e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 12:41:03 +0900 Subject: [PATCH 227/552] =?UTF-8?q?refactor(tle.models);=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=A0=9C=ED=95=9C,=20=EB=A9=94=EB=AA=A8=EB=A6=AC?= =?UTF-8?q?=20=EC=A0=9C=ED=95=9C=20=EB=8B=A8=EC=9C=84=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=20=EC=B1=85=EC=9E=84=EC=9D=84=20`models`=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem.py | 12 ++++++++++++ app/tle/serializers/problem_serializer.py | 16 ++++------------ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 745efc7..bb1353b 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -53,6 +53,18 @@ class FieldName: ) analysis: models.OneToOneField[T_ProblemAnalysis] + MEMORY_LIMIT_UNIT = { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB", + } + + TIME_LIMIT_UNIT = { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s", + } + def __repr__(self) -> str: return f'[{self.title}]' diff --git a/app/tle/serializers/problem_serializer.py b/app/tle/serializers/problem_serializer.py index 4f44406..9a9354c 100644 --- a/app/tle/serializers/problem_serializer.py +++ b/app/tle/serializers/problem_serializer.py @@ -132,16 +132,8 @@ class Meta: 'updated_at': {'read_only': True}, } - def get_memory_limit_unit(self, obj: Problem): - return { - "name_ko": "메가 바이트", - "name_en": "Mega Bytes", - "abbr": "MB", - } + def get_memory_limit_unit(self, obj): + return Problem.MEMORY_LIMIT_UNIT - def get_time_limit_unit(self, obj: Problem): - return { - "name_ko": "초", - "name_en": "Seconds", - "abbr": "s", - } + def get_time_limit_unit(self, obj): + return Problem.TIME_LIMIT_UNIT From c39c9734bd20c867eb5363735a4fe4a4a9021d1e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 13:23:47 +0900 Subject: [PATCH 228/552] squash for REFACTOR: remove app `account` ... --- app/app/settings.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/app/settings.py b/app/app/settings.py index 8bcbe21..463358c 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -11,10 +11,6 @@ """ from pathlib import Path -import os - - -GOOGLE_API_KEY = os.environ.get("GOOGLE_API_KEY", None) # Build paths inside the project like this: BASE_DIR / 'subdir'. From af7b9a1a85422a5973150b14884e235f35634091 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 13:54:02 +0900 Subject: [PATCH 229/552] =?UTF-8?q?fix(tle.views):=20`UserViewSet.authenti?= =?UTF-8?q?cate()`=EC=9D=98=20=EB=88=84=EB=9D=BD=EB=90=9C=20return=20?= =?UTF-8?q?=EB=AC=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/views/viewsets/user_viewset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/views/viewsets/user_viewset.py b/app/tle/views/viewsets/user_viewset.py index 8a098a8..697b2ea 100644 --- a/app/tle/views/viewsets/user_viewset.py +++ b/app/tle/views/viewsets/user_viewset.py @@ -49,6 +49,7 @@ def authenticate(self, request: Request) -> User: user = authenticate(request, username=email, password=password) if user is None: raise AuthenticationFailed('Invalid email or password') + return user # Actions From 9aeabf586a29eaf1b0c6bab6922bea3e6bdd0c85 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 14:01:24 +0900 Subject: [PATCH 230/552] =?UTF-8?q?chore(app.settings):=20URL=20=EB=92=A4?= =?UTF-8?q?=EC=97=90=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=8A=AC?= =?UTF-8?q?=EB=9E=98=EC=89=AC=EA=B0=80=20=EB=B6=99=EB=8A=94=20=EA=B2=83?= =?UTF-8?q?=EC=9D=84=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/app/settings.py b/app/app/settings.py index 463358c..836ab7c 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -153,3 +153,5 @@ 'PAGE_SIZE': 10, 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', } + +APPEND_SLASH = False From f2f84968588c40a8ccf13f169be9f9f1d945a58f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 14:45:26 +0900 Subject: [PATCH 231/552] refactor(tle.views): redesign urls under `api/v1/problem` --- app/tle/views/urls.py | 20 ++++++++++---------- app/tle/views/viewsets/problem_viewset.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index ac3abab..ab8ed92 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -10,16 +10,16 @@ path("signout", UserViewSet.as_view({"get": "sign_out"})), path("current", UserViewSet.as_view({"get": "current"})), ])), - path("problem", ProblemViewSet.as_view({ - "post": "create", - "get": "list", - })), path("problem/", include([ - path("", ProblemViewSet.as_view({ - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - })), + path("", ProblemViewSet.as_view({"post": "create"})), + path("search", ProblemViewSet.as_view({"get": "list"})), + path("/", include([ + path("detail", ProblemViewSet.as_view({ + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + })) + ])), ])), ] diff --git a/app/tle/views/viewsets/problem_viewset.py b/app/tle/views/viewsets/problem_viewset.py index 7e08b06..8879e76 100644 --- a/app/tle/views/viewsets/problem_viewset.py +++ b/app/tle/views/viewsets/problem_viewset.py @@ -13,7 +13,7 @@ class ProblemViewSet(ModelViewSet): # TODO: 내가 만든 문제만 수정할 수 있도록 변경 def get_serializer_class(self): - if self.action in ['create', 'list']: + if self.action in ['list']: return ProblemMinimalSerializer return ProblemSerializer From 55ce1683e004ed2ddb9da98e5cbf584a4cfc6305 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 14:51:00 +0900 Subject: [PATCH 232/552] refactor(tle.views): redesign urls under `api/v1/account` (renamed `account` to `auth`) --- app/tle/views/urls.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index ab8ed92..aaf9477 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -4,11 +4,10 @@ urlpatterns = [ - path("account/", include([ + path("auth/", include([ path("signin", UserViewSet.as_view({"post": "sign_in"})), path("signup", UserViewSet.as_view({"post": "sign_up"})), path("signout", UserViewSet.as_view({"get": "sign_out"})), - path("current", UserViewSet.as_view({"get": "current"})), ])), path("problem/", include([ path("", ProblemViewSet.as_view({"post": "create"})), From 8cc70f8f2bd346f6cc50e865ec30d72c871804fc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 15:17:50 +0900 Subject: [PATCH 233/552] feat(tle.views): add url `user/current` --- app/tle/views/urls.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index aaf9477..c28ebeb 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -9,6 +9,7 @@ path("signup", UserViewSet.as_view({"post": "sign_up"})), path("signout", UserViewSet.as_view({"get": "sign_out"})), ])), + path("user/current", UserViewSet.as_view({"get": "current"})), path("problem/", include([ path("", ProblemViewSet.as_view({"post": "create"})), path("search", ProblemViewSet.as_view({"get": "list"})), From 729a116c35019f452adcf5be1b1b202c2ab74681 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 15:26:56 +0900 Subject: [PATCH 234/552] =?UTF-8?q?refactor(tle.views):=20REST=20API=20?= =?UTF-8?q?=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=EB=A5=BC=20`auth`?= =?UTF-8?q?,=20`user`=20=EB=91=90=20=EA=B0=9C=EB=A1=9C=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/views/urls.py | 19 +++++-- app/tle/views/viewsets/__init__.py | 1 + app/tle/views/viewsets/auth_viewset.py | 72 ++++++++++++++++++++++++++ app/tle/views/viewsets/user_viewset.py | 68 ++---------------------- 4 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 app/tle/views/viewsets/auth_viewset.py diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index c28ebeb..d2b8c04 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -5,16 +5,27 @@ urlpatterns = [ path("auth/", include([ - path("signin", UserViewSet.as_view({"post": "sign_in"})), - path("signup", UserViewSet.as_view({"post": "sign_up"})), - path("signout", UserViewSet.as_view({"get": "sign_out"})), + path("signin", AuthViewSet.as_view({"post": "sign_in"})), + path("signup", AuthViewSet.as_view({"post": "sign_up"})), + path("signout", AuthViewSet.as_view({"get": "sign_out"})), ])), path("user/current", UserViewSet.as_view({"get": "current"})), + path("user/", include([ + path("search", UserViewSet.as_view({"get": "list"})), + path("/", include([ + path("profile", UserViewSet.as_view({ + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + })), + ])), + ])), path("problem/", include([ path("", ProblemViewSet.as_view({"post": "create"})), path("search", ProblemViewSet.as_view({"get": "list"})), path("/", include([ - path("detail", ProblemViewSet.as_view({ + path("description", ProblemViewSet.as_view({ "get": "retrieve", "put": "update", "patch": "partial_update", diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index ca486df..b518c35 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1,2 +1,3 @@ +from tle.views.viewsets.auth_viewset import AuthViewSet from tle.views.viewsets.user_viewset import UserViewSet from tle.views.viewsets.problem_viewset import ProblemViewSet diff --git a/app/tle/views/viewsets/auth_viewset.py b/app/tle/views/viewsets/auth_viewset.py new file mode 100644 index 0000000..a2a91be --- /dev/null +++ b/app/tle/views/viewsets/auth_viewset.py @@ -0,0 +1,72 @@ +from http import HTTPStatus + +from django.contrib.auth import ( + authenticate, + login, + logout, +) +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.request import Request +from rest_framework.response import Response +from rest_framework.serializers import Serializer +from rest_framework.viewsets import GenericViewSet + +from tle.models import User +from tle.serializers import * +from tle.views.permissions import * + + +class AuthViewSet(GenericViewSet): + """사용자 계정과 관련된 API + + current: 현재 로그인한 사용자 정보 + signup: 사용자 등록(회원가입) + signin: 사용자 로그인 + signout: 사용자 로그아웃 + """ + queryset = User.objects.all() + permission_classes = [AllowAny] + + # Overrides + + def get_serializer_class(self): + if self.action == 'sign_in': + return UserSignInSerializer + else: + return UserSerializer + + # Helpers + + def get_validated_serializer(self, request: Request) -> Serializer: + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + return serializer + + def authenticate(self, request: Request) -> User: + serializer = self.get_validated_serializer(request) + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + user = authenticate(request, username=email, password=password) + if user is None: + raise AuthenticationFailed('Invalid email or password') + return user + + # Actions + + def sign_up(self, request: Request): + serializer = self.get_validated_serializer(request) + serializer.save() + return Response(serializer.data) + + def sign_in(self, request: Request): + serializer = self.get_validated_serializer(request) + serializer.instance = self.authenticate(request) + login(request, serializer.instance) + return Response(serializer.data) + + def sign_out(self, request: Request): + logout(request) + return Response(status=HTTPStatus.OK) + + # TODO: 이메일 인증 + # TODO: 비밀번호 찾기 diff --git a/app/tle/views/viewsets/user_viewset.py b/app/tle/views/viewsets/user_viewset.py index 697b2ea..7554fe0 100644 --- a/app/tle/views/viewsets/user_viewset.py +++ b/app/tle/views/viewsets/user_viewset.py @@ -1,76 +1,18 @@ -from http import HTTPStatus - -from django.contrib.auth import ( - authenticate, - login, - logout, -) -from rest_framework.exceptions import AuthenticationFailed from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import Serializer -from rest_framework.viewsets import GenericViewSet +from rest_framework.viewsets import ModelViewSet from tle.models import User from tle.serializers import * from tle.views.permissions import * -class UserViewSet(GenericViewSet): - """사용자 계정과 관련된 API - - current: 현재 로그인한 사용자 정보 - signup: 사용자 등록(회원가입) - signin: 사용자 로그인 - signout: 사용자 로그아웃 - """ +class UserViewSet(ModelViewSet): queryset = User.objects.all() - permission_classes = [AllowAny] - - # Overrides - - def get_serializer_class(self): - if self.action == 'sign_in': - return UserSignInSerializer - else: - return UserSerializer - - # Helpers - - def get_validated_serializer(self, request: Request) -> Serializer: - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - return serializer - - def authenticate(self, request: Request) -> User: - serializer = self.get_validated_serializer(request) - email = serializer.validated_data['email'] - password = serializer.validated_data['password'] - user = authenticate(request, username=email, password=password) - if user is None: - raise AuthenticationFailed('Invalid email or password') - return user - - # Actions + permission_classes = [IsAdminUser] + serializer_class = UserSerializer + lookup_field = 'id' def current(self, request: Request): serializer = self.get_serializer(instance=request.user) return Response(serializer.data) - - def sign_up(self, request: Request): - serializer = self.get_validated_serializer(request) - serializer.save() - return Response(serializer.data) - - def sign_in(self, request: Request): - serializer = self.get_validated_serializer(request) - serializer.instance = self.authenticate(request) - login(request, serializer.instance) - return Response(serializer.data) - - def sign_out(self, request: Request): - logout(request) - return Response(status=HTTPStatus.OK) - - # TODO: 이메일 인증 - # TODO: 비밀번호 찾기 From 14d2268f054288be209203d1535e09eeaafdc375 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 15:39:31 +0900 Subject: [PATCH 235/552] =?UTF-8?q?feat(tle.views):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20201=20?= =?UTF-8?q?=EC=83=81=ED=83=9C=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/views/viewsets/auth_viewset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/views/viewsets/auth_viewset.py b/app/tle/views/viewsets/auth_viewset.py index a2a91be..0ecb803 100644 --- a/app/tle/views/viewsets/auth_viewset.py +++ b/app/tle/views/viewsets/auth_viewset.py @@ -56,7 +56,7 @@ def authenticate(self, request: Request) -> User: def sign_up(self, request: Request): serializer = self.get_validated_serializer(request) serializer.save() - return Response(serializer.data) + return Response(serializer.data, status=HTTPStatus.CREATED) def sign_in(self, request: Request): serializer = self.get_validated_serializer(request) From 393d7168c620a752306c32b025b31bc62805b09c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 15:43:15 +0900 Subject: [PATCH 236/552] =?UTF-8?q?docs:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20REST=20API=20=EB=AA=85=EC=84=B8=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/REST-API.md | 13 ++++++ docs/api/auth.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 docs/REST-API.md create mode 100644 docs/api/auth.md diff --git a/docs/REST-API.md b/docs/REST-API.md new file mode 100644 index 0000000..43858a0 --- /dev/null +++ b/docs/REST-API.md @@ -0,0 +1,13 @@ +# T.L.E. REST API + +## Base URL + +모든 Endpoint 주소는 Base URL로 아래의 주소를 사용한다. + +```text +http://timelimitexceeded.com/api/v1 +``` + +## Docs + +- [사용자 인증](./api/auth.md) diff --git a/docs/api/auth.md b/docs/api/auth.md new file mode 100644 index 0000000..df9a1bf --- /dev/null +++ b/docs/api/auth.md @@ -0,0 +1,119 @@ +# 사용자 인증 + +## 회원 가입하기 + +### URL + +``` +POST /auth/signup +``` + +### Body Parameters + +지원되는 형식: `multipart/form-data` + +| key | type | required | example | +| ------------- | ------ | -------- | --------------- | +| email | string | yes | "test@test.com" | +| username | string | yes | "test" | +| password | string | yes | "test1234" | +| boj_username | string | yes | "test" | +| profile_image | file | no | "test" | + +### Response Example + +| status | description | +| ------ | -------------------------------------- | +| 201 | 정상적으로 가입됨 | +| 400 | 일부 필드의 정보가 누락되었거나 잘못됨 | + +```json +{ + "id": 1, + "email": "test@test.com", + "profile_image": "http://localhost:8000/media/user/profile/1/b.png", + "username": "test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "tier": 30, + "tier_updated_at": "2024-07-17T07:02:29Z" + }, + "created_at": "2024-07-17T06:53:57Z", + "last_login": "2024-07-18T04:52:42.892169Z" +} +``` + +## 로그인 하기 + +### URL + +``` +POST /auth/signin +``` + +### Body Parameters + +지원되는 형식: `multipart/form-data`, `application/json` + +| key | type | required | example | +| -------- | ------ | -------- | --------------- | +| email | string | yes | "test@test.com" | +| password | string | yes | "test1234" | + +### Response Example + +### Response Example + +| status | description | +| ------ | ----------------------------------------------------------------- | +| 200 | 로그인에 성공함 | +| 400 | 로그인에 실패함 (누락되었거나 잘못된 사용자 이메일 혹은 비밀번호) | + +```json +{ + "id": 1, + "email": "test@test.com", + "profile_image": "http://localhost:8000/media/user/profile/1/b.png", + "username": "test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "tier": 30, + "tier_updated_at": "2024-07-17T07:02:29Z" + }, + "created_at": "2024-07-17T06:53:57Z", + "last_login": "2024-07-18T04:52:42.892169Z" +} +``` + +## 로그아웃 하기 + +### URL + +``` +GET /auth/signout +``` + +### Response Example + +| status | description | +| ------ | ----------------- | +| 200 | 로그아웃에 성공함 | + +```json +{ + "id": 1, + "email": "test@test.com", + "profile_image": "http://localhost:8000/media/user/profile/1/b.png", + "username": "test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "tier": 30, + "tier_updated_at": "2024-07-17T07:02:29Z" + }, + "created_at": "2024-07-17T06:53:57Z", + "last_login": "2024-07-18T04:52:42.892169Z" +} +``` From ce9f5532221c166a2c1772f66032f350c7a12780 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 15:59:46 +0900 Subject: [PATCH 237/552] =?UTF-8?q?docs:=20=EB=AC=B8=EC=A0=9C=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EA=B8=B0=20=EB=B0=8F=20=EB=AA=A9=EB=A1=9D=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=ED=95=98=EA=B8=B0=20REST=20API=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/REST-API.md | 1 + docs/api/problem.md | 104 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 docs/api/problem.md diff --git a/docs/REST-API.md b/docs/REST-API.md index 43858a0..62e6d93 100644 --- a/docs/REST-API.md +++ b/docs/REST-API.md @@ -11,3 +11,4 @@ http://timelimitexceeded.com/api/v1 ## Docs - [사용자 인증](./api/auth.md) +- [문제 관리](./api/problem.md) diff --git a/docs/api/problem.md b/docs/api/problem.md new file mode 100644 index 0000000..efce7e3 --- /dev/null +++ b/docs/api/problem.md @@ -0,0 +1,104 @@ +# 문제 관리 + +## 문제 생성하기 + +### URL + +``` +POST /problem/ +``` + +### Body Parameters + +지원되는 형식: `multipart/form-data` + +| key | type | required | example | description | +| ------------------ | ------ | -------- | --------------------- | ------------ | +| title | string | yes | "A+B" | | +| link | string | yes | "https://boj.kr/1000" | | +| description | string | yes | "A+B를 출력한다." | | +| input_description | string | yes | "첫 번째 줄에..." | | +| output_description | string | yes | "첫 번째 줄에..." | | +| memory_limit | float | yes | 128.0 | MB 단위이다. | +| time_limit | float | yes | 1.0 | 초 단위이다. | + +### Response Example + +| status | description | +| ------ | ----------- | +| 200 | OK | +| 400 | BAD REQUEST | + +```json +{ + "id": 2, + "analysis": null, + "title": "A+B", + "link": "https://www.acmicpc.net/problem/1000", + "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", + "output_description": "첫째 줄에 A+B를 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB" + }, + "time_limit": 1.0, + "time_limit_unit": { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s" + }, + "created_at": "2024-07-17T10:10:55.275762Z", + "created_by": { + "id": 1, + "profile_image": "http://localhost:8000/media/user/profile/1/b.png", + "username": "admin" + }, + "updated_at": "2024-07-17T10:11:06.957807Z" +} +``` + +## 문제 목록 조회하기 (문제검색) + +``` +GET /problem/search? +``` + +### Query Parameters + +> TODO: 추후 수정 예정 + +| key | description | +| --- | ----------- | +| | | + +### Response Example + +```json +{ + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "title": "A+B", + "link": "http://boj.kr/1000", + "difficulty": { + "name_en": "EASY", + "name_ko": "쉬움", + "value": 1 + }, + "created_at": "2024-07-17T09:23:17.876425Z", + "created_by": { + "id": 1, + "profile_image": "http://localhost:8000/media/user/profile/1/b.png", + "username": "admin" + }, + "updated_at": "2024-07-17T09:23:17.876456Z" + } + ] +} +``` From 8c215950478ca0bf4997ca5e5edb6858b29b81ee Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 16:16:32 +0900 Subject: [PATCH 238/552] chore: create `runserver` executable --- app/runserver | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 app/runserver diff --git a/app/runserver b/app/runserver new file mode 100644 index 0000000..1bb0dd8 --- /dev/null +++ b/app/runserver @@ -0,0 +1,6 @@ +#!/bin/bash + +source ../venv/bin/activate +python manage.py makemigrations +python manage.py migrate +python manage.py runserver --insecure 0.0.0.0:80 From 1ceae6fc528b878ff7e671e04b0417c132f53d14 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 18 Jul 2024 16:21:59 +0900 Subject: [PATCH 239/552] =?UTF-8?q?refactor(tle.serializers):=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=AA=A9=EB=A1=9D=20=EB=B3=B4=EA=B8=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/problem_serializer.py | 13 +------------ docs/api/problem.md | 6 ------ 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/app/tle/serializers/problem_serializer.py b/app/tle/serializers/problem_serializer.py index 9a9354c..b4a7b44 100644 --- a/app/tle/serializers/problem_serializer.py +++ b/app/tle/serializers/problem_serializer.py @@ -68,7 +68,6 @@ class Meta: class ProblemMinimalSerializer(ModelSerializer): - created_by = UserMinimalSerializer(read_only=True) difficulty = SerializerMethodField() class Meta: @@ -76,21 +75,11 @@ class Meta: fields = [ 'id', 'title', - 'link', 'difficulty', 'created_at', - 'created_by', 'updated_at', ] - extra_kwargs = { - 'id': {'read_only': True}, - 'title': {'read_only': True}, - 'link': {'read_only': True}, - 'difficulty': {'read_only': True}, - 'created_at': {'read_only': True}, - 'created_by': {'read_only': True}, - 'updated_at': {'read_only': True}, - } + read_only_fields = ['__all__'] def get_difficulty(self, obj: Problem): try: diff --git a/docs/api/problem.md b/docs/api/problem.md index efce7e3..691aff9 100644 --- a/docs/api/problem.md +++ b/docs/api/problem.md @@ -85,18 +85,12 @@ GET /problem/search? { "id": 1, "title": "A+B", - "link": "http://boj.kr/1000", "difficulty": { "name_en": "EASY", "name_ko": "쉬움", "value": 1 }, "created_at": "2024-07-17T09:23:17.876425Z", - "created_by": { - "id": 1, - "profile_image": "http://localhost:8000/media/user/profile/1/b.png", - "username": "admin" - }, "updated_at": "2024-07-17T09:23:17.876456Z" } ] From 4b22f3c837299273ce2b0116169a1e6e43259dee Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 17:58:39 +0900 Subject: [PATCH 240/552] =?UTF-8?q?refactor(tle.serializers):=20problem=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20serializer=EC=9D=98=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/__init__.py | 10 +- app/tle/serializers/problem_analysis.py | 37 +++++++ app/tle/serializers/problem_detail.py | 40 +++++++ app/tle/serializers/problem_difficulty.py | 18 +++ app/tle/serializers/problem_minimal.py | 26 +++++ app/tle/serializers/problem_serializer.py | 128 ---------------------- app/tle/serializers/problem_tag.py | 22 ++++ 7 files changed, 152 insertions(+), 129 deletions(-) create mode 100644 app/tle/serializers/problem_analysis.py create mode 100644 app/tle/serializers/problem_detail.py create mode 100644 app/tle/serializers/problem_difficulty.py create mode 100644 app/tle/serializers/problem_minimal.py delete mode 100644 app/tle/serializers/problem_serializer.py create mode 100644 app/tle/serializers/problem_tag.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index f211aa7..b0b3fe9 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1,2 +1,10 @@ from tle.serializers.user_serializer import * -from tle.serializers.problem_serializer import * + +from tle.serializers.problem_analysis import ProblemAnalysisSerializer +from tle.serializers.problem_detail import ProblemDetailSerializer +from tle.serializers.problem_difficulty import ProblemDifficultySerializer +from tle.serializers.problem_minimal import ProblemMinimalSerializer +from tle.serializers.problem_tag import ProblemTagSerializer + + +ProblemSerializer = ProblemDetailSerializer diff --git a/app/tle/serializers/problem_analysis.py b/app/tle/serializers/problem_analysis.py new file mode 100644 index 0000000..d961b16 --- /dev/null +++ b/app/tle/serializers/problem_analysis.py @@ -0,0 +1,37 @@ +from rest_framework.serializers import * + +from tle.models import ProblemAnalysis +from tle.serializers.problem_tag import ProblemTagSerializer +from tle.serializers.problem_difficulty import ProblemDifficultySerializer + + +class ProblemAnalysisSerializer(ModelSerializer): + tags = ProblemTagSerializer(many=True, read_only=True) + difficulty = ProblemDifficultySerializer(read_only=True) + difficulty_description = SerializerMethodField() + time_complexity_description = SerializerMethodField() + + class Meta: + model = ProblemAnalysis + fields = [ + 'difficulty', + 'difficulty_description', + 'tags', + 'time_complexity', + 'time_complexity_description', + 'hint', + 'created_at', + ] + read_only_fields = ['__all__'] + + def get_difficulty_description(self, obj: ProblemAnalysis): + return ( + "[이 기능은 아직 추가할 예정이 없습니다] " + "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준" + ) + + def get_time_complexity_description(self, obj: ProblemAnalysis): + return ( + "[이 기능은 아직 추가할 예정이 없습니다] " + "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요." + ) diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py new file mode 100644 index 0000000..9edb5a0 --- /dev/null +++ b/app/tle/serializers/problem_detail.py @@ -0,0 +1,40 @@ +from rest_framework.serializers import * + +from tle.models import Problem +from tle.serializers.problem_analysis import ProblemAnalysisSerializer + + +class ProblemDetailSerializer(ModelSerializer): + analysis= ProblemAnalysisSerializer(read_only=True) + memory_limit_unit= SerializerMethodField() + time_limit_unit= SerializerMethodField() + + class Meta: + model= Problem + fields= [ + 'id', + 'analysis', + 'title', + 'link', + 'description', + 'input_description', + 'output_description', + 'memory_limit', + 'memory_limit_unit', + 'time_limit', + 'time_limit_unit', + 'created_at', + 'updated_at', + ] + extra_kwargs= { + 'id': {'read_only': True}, + 'anaysis': {'read_only': True}, + 'created_at': {'read_only': True}, + 'updated_at': {'read_only': True}, + } + + def get_memory_limit_unit(self, obj): + return Problem.MEMORY_LIMIT_UNIT + + def get_time_limit_unit(self, obj): + return Problem.TIME_LIMIT_UNIT diff --git a/app/tle/serializers/problem_difficulty.py b/app/tle/serializers/problem_difficulty.py new file mode 100644 index 0000000..0bdc9c0 --- /dev/null +++ b/app/tle/serializers/problem_difficulty.py @@ -0,0 +1,18 @@ +from rest_framework.serializers import * + +from tle.models import ProblemDifficulty + + +class ProblemDifficultySerializer(Serializer): + name_en = SerializerMethodField() + name_ko = SerializerMethodField() + value = SerializerMethodField() + + def get_name_ko(self, value: int): + return ProblemDifficulty.get_name(value, 'ko') + + def get_name_en(self, value: int): + return ProblemDifficulty.get_name(value, 'en') + + def get_value(self, value: int): + return value diff --git a/app/tle/serializers/problem_minimal.py b/app/tle/serializers/problem_minimal.py new file mode 100644 index 0000000..c75ce97 --- /dev/null +++ b/app/tle/serializers/problem_minimal.py @@ -0,0 +1,26 @@ +from rest_framework.serializers import * + +from tle.models import Problem, ProblemAnalysis +from tle.serializers.problem_difficulty import ProblemDifficultySerializer + + +class ProblemMinimalSerializer(ModelSerializer): + difficulty = SerializerMethodField() + + class Meta: + model = Problem + fields = [ + 'id', + 'title', + 'difficulty', + 'created_at', + 'updated_at', + ] + read_only_fields = ['__all__'] + + def get_difficulty(self, obj: Problem): + try: + difficulty = obj.analysis.difficulty + except ProblemAnalysis.DoesNotExist: + difficulty = 0 + return ProblemDifficultySerializer(difficulty).data diff --git a/app/tle/serializers/problem_serializer.py b/app/tle/serializers/problem_serializer.py deleted file mode 100644 index b4a7b44..0000000 --- a/app/tle/serializers/problem_serializer.py +++ /dev/null @@ -1,128 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import ( - Problem, - ProblemAnalysis, - ProblemDifficulty, - ProblemTag, -) -from tle.serializers.user_serializer import UserMinimalSerializer - - -__all__ = ( - 'ProblemSerializer', - 'ProblemMinimalSerializer', - 'ProblemAnalysisSerializer', - 'ProblemTagSerializer', -) - - -class ProblemTagSerializer(ModelSerializer): - parent = SerializerMethodField() - - class Meta: - model = ProblemTag - fields = [ - 'parent', - 'key', - 'name_ko', - 'name_en', - ] - read_only_fields = ['__all__'] - - def get_parent(self, obj: ProblemTag): - if obj.parent is None: - return None - return ProblemTagSerializer(obj.parent).data - - -class ProblemDifficultySerializer(Serializer): - name_en = SerializerMethodField() - name_ko = SerializerMethodField() - value = SerializerMethodField() - - def get_name_ko(self, value: int): - return ProblemDifficulty.get_name(value, 'ko') - - def get_name_en(self, value: int): - return ProblemDifficulty.get_name(value, 'en') - - def get_value(self, value: int): - return value - - -class ProblemAnalysisSerializer(ModelSerializer): - tags = ProblemTagSerializer(many=True, read_only=True) - difficulty = ProblemDifficultySerializer(read_only=True) - - class Meta: - model = ProblemAnalysis - fields = [ - 'difficulty', - 'tags', - 'time_complexity', - 'hint', - 'created_at', - ] - read_only_fields = ['__all__'] - - -class ProblemMinimalSerializer(ModelSerializer): - difficulty = SerializerMethodField() - - class Meta: - model = Problem - fields = [ - 'id', - 'title', - 'difficulty', - 'created_at', - 'updated_at', - ] - read_only_fields = ['__all__'] - - def get_difficulty(self, obj: Problem): - try: - difficulty = obj.analysis.difficulty - except ProblemAnalysis.DoesNotExist: - difficulty = 0 - return ProblemDifficultySerializer(difficulty).data - - -class ProblemSerializer(ModelSerializer): - analysis = ProblemAnalysisSerializer(read_only=True) - memory_limit_unit = SerializerMethodField() - time_limit_unit = SerializerMethodField() - created_by = UserMinimalSerializer(read_only=True) - - class Meta: - model = Problem - fields = [ - 'id', - 'analysis', - 'title', - 'link', - 'description', - 'input_description', - 'output_description', - 'memory_limit', - 'memory_limit_unit', - 'time_limit', - 'time_limit_unit', - 'created_at', - 'created_by', - 'updated_at', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'anaysis': {'read_only': True}, - 'created_at': {'read_only': True}, - 'created_by': {'read_only': True}, - 'updated_at': {'read_only': True}, - } - - def get_memory_limit_unit(self, obj): - return Problem.MEMORY_LIMIT_UNIT - - def get_time_limit_unit(self, obj): - return Problem.TIME_LIMIT_UNIT diff --git a/app/tle/serializers/problem_tag.py b/app/tle/serializers/problem_tag.py new file mode 100644 index 0000000..5054db5 --- /dev/null +++ b/app/tle/serializers/problem_tag.py @@ -0,0 +1,22 @@ +from rest_framework.serializers import * + +from tle.models import ProblemTag + + +class ProblemTagSerializer(ModelSerializer): + parent = SerializerMethodField() + + class Meta: + model = ProblemTag + fields = [ + 'parent', + 'key', + 'name_ko', + 'name_en', + ] + read_only_fields = ['__all__'] + + def get_parent(self, obj: ProblemTag): + if obj.parent is None: + return None + return ProblemTagSerializer(obj.parent).data From 63f170aee3a2d394ed326ead4d98e0db54b12547 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 18:04:54 +0900 Subject: [PATCH 241/552] =?UTF-8?q?refactor(tle.serializers):=20user=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20serializer=EC=9D=98=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/__init__.py | 20 ++++- app/tle/serializers/user_detail.py | 41 ++++++++++ app/tle/serializers/user_minimal.py | 18 +++++ app/tle/serializers/user_serializer.py | 108 ------------------------- app/tle/serializers/user_sign_in.py | 48 +++++++++++ 5 files changed, 126 insertions(+), 109 deletions(-) create mode 100644 app/tle/serializers/user_detail.py create mode 100644 app/tle/serializers/user_minimal.py delete mode 100644 app/tle/serializers/user_serializer.py create mode 100644 app/tle/serializers/user_sign_in.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index b0b3fe9..894339b 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1,4 +1,6 @@ -from tle.serializers.user_serializer import * +from tle.serializers.user_detail import UserDetailSerializer +from tle.serializers.user_minimal import UserMinimalSerializer +from tle.serializers.user_sign_in import UserSignInSerializer from tle.serializers.problem_analysis import ProblemAnalysisSerializer from tle.serializers.problem_detail import ProblemDetailSerializer @@ -7,4 +9,20 @@ from tle.serializers.problem_tag import ProblemTagSerializer +UserSerializer = UserDetailSerializer ProblemSerializer = ProblemDetailSerializer + + +__all__ = ( + 'UserSerializer', + 'UserDetailSerializer', + 'UserMinimalSerializer', + 'UserSignInSerializer', + + 'ProblemSerializer', + 'ProblemAnalysisSerializer', + 'ProblemDetailSerializer', + 'ProblemDifficultySerializer', + 'ProblemMinimalSerializer', + 'ProblemTagSerializer', +) diff --git a/app/tle/serializers/user_detail.py b/app/tle/serializers/user_detail.py new file mode 100644 index 0000000..dc8f7d8 --- /dev/null +++ b/app/tle/serializers/user_detail.py @@ -0,0 +1,41 @@ +from rest_framework.serializers import * + +from tle.models import User, UserManager + + +class UserDetailSerializer(ModelSerializer): + boj = SerializerMethodField(read_only=True) + + class Meta: + model = User + fields = [ + 'id', + 'email', + 'profile_image', + 'username', + 'password', + 'boj_username', + 'boj', + 'created_at', + 'last_login', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'boj': {'read_only': True}, + 'created_at': {'read_only': True}, + 'last_login': {'read_only': True}, + 'password': {'write_only': True}, + 'boj_username': {'write_only': True}, + } + + def get_boj(self, obj: User) -> dict: + return { + 'username': obj.boj_username, + 'profile_url': f'https://boj.kr/{obj.boj_username}', + 'tier': obj.boj_tier, + 'tier_updated_at': obj.boj_tier_updated_at, + } + + def create(self, validated_data): + user_manager: UserManager = User.objects + return user_manager.create_user(**validated_data) diff --git a/app/tle/serializers/user_minimal.py b/app/tle/serializers/user_minimal.py new file mode 100644 index 0000000..8a8d9ac --- /dev/null +++ b/app/tle/serializers/user_minimal.py @@ -0,0 +1,18 @@ +from rest_framework.serializers import * + +from tle.models import User + + +class UserMinimalSerializer(ModelSerializer): + class Meta: + model = User + fields = [ + 'id', + 'profile_image', + 'username', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'profile_image': {'read_only': True}, + 'username': {'read_only': True}, + } diff --git a/app/tle/serializers/user_serializer.py b/app/tle/serializers/user_serializer.py deleted file mode 100644 index 68fbfb9..0000000 --- a/app/tle/serializers/user_serializer.py +++ /dev/null @@ -1,108 +0,0 @@ -from rest_framework.exceptions import PermissionDenied -from rest_framework.serializers import * - -from tle.models import User, UserManager - - -__all__ = ( - 'UserSerializer', - 'UserSignInSerializer', - 'UserMinimalSerializer', -) - - -class UserSerializer(ModelSerializer): - boj = SerializerMethodField(read_only=True) - - class Meta: - model = User - fields = [ - 'id', - 'email', - 'profile_image', - 'username', - 'password', - 'boj_username', - 'boj', - 'created_at', - 'last_login', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - 'created_at': {'read_only': True}, - 'last_login': {'read_only': True}, - 'password': {'write_only': True}, - 'boj_username': {'write_only': True}, - } - - def get_boj(self, obj: User) -> dict: - return { - 'username': obj.boj_username, - 'profile_url': f'https://boj.kr/{obj.boj_username}', - 'tier': obj.boj_tier, - 'tier_updated_at': obj.boj_tier_updated_at, - } - - def create(self, validated_data): - user_manager: UserManager = User.objects - return user_manager.create_user(**validated_data) - - -class UserSignInSerializer(ModelSerializer): - email = EmailField(write_only=True, validators=None) - boj = SerializerMethodField() - - class Meta: - model = User - fields = [ - 'id', - 'email', - 'profile_image', - 'username', - 'password', - 'boj', - 'created_at', - 'last_login', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'profile_image': {'read_only': True}, - 'username': {'read_only': True}, - 'boj': {'read_only': True}, - 'created_at': {'read_only': True}, - 'last_login': {'read_only': True}, - 'password': {'write_only': True}, - } - - def get_boj(self, obj: User) -> dict: - return { - 'username': obj.boj_username, - 'profile_url': f'https://boj.kr/{obj.boj_username}', - 'tier': obj.boj_tier, - 'tier_updated_at': obj.boj_tier_updated_at, - } - - def create(self, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def update(self, instance, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def save(self, **kwargs): - raise PermissionDenied('Cannot update user through this serializer') - - -class UserMinimalSerializer(ModelSerializer): - class Meta: - model = User - fields = [ - 'id', - 'profile_image', - 'username', - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'profile_image': {'read_only': True}, - 'username': {'read_only': True}, - } diff --git a/app/tle/serializers/user_sign_in.py b/app/tle/serializers/user_sign_in.py new file mode 100644 index 0000000..4c723f6 --- /dev/null +++ b/app/tle/serializers/user_sign_in.py @@ -0,0 +1,48 @@ +from rest_framework.exceptions import PermissionDenied +from rest_framework.serializers import * + +from tle.models import User + + +class UserSignInSerializer(ModelSerializer): + email = EmailField(write_only=True, validators=None) + boj = SerializerMethodField() + + class Meta: + model = User + fields = [ + 'id', + 'email', + 'profile_image', + 'username', + 'password', + 'boj', + 'created_at', + 'last_login', + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'profile_image': {'read_only': True}, + 'username': {'read_only': True}, + 'boj': {'read_only': True}, + 'created_at': {'read_only': True}, + 'last_login': {'read_only': True}, + 'password': {'write_only': True}, + } + + def get_boj(self, obj: User) -> dict: + return { + 'username': obj.boj_username, + 'profile_url': f'https://boj.kr/{obj.boj_username}', + 'tier': obj.boj_tier, + 'tier_updated_at': obj.boj_tier_updated_at, + } + + def create(self, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def update(self, instance, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def save(self, **kwargs): + raise PermissionDenied('Cannot update user through this serializer') From 9320609ae084ce7d7ff29c4a0cc5a98c3ce0fd47 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 18:26:03 +0900 Subject: [PATCH 242/552] refactor(tle.views): update urls to use plurals --- app/tle/views/urls.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index d2b8c04..c6b0a9a 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -9,8 +9,8 @@ path("signup", AuthViewSet.as_view({"post": "sign_up"})), path("signout", AuthViewSet.as_view({"get": "sign_out"})), ])), - path("user/current", UserViewSet.as_view({"get": "current"})), - path("user/", include([ + path("users/current", UserViewSet.as_view({"get": "current"})), + path("users/", include([ path("search", UserViewSet.as_view({"get": "list"})), path("/", include([ path("profile", UserViewSet.as_view({ @@ -21,11 +21,11 @@ })), ])), ])), - path("problem/", include([ + path("problems/", include([ path("", ProblemViewSet.as_view({"post": "create"})), path("search", ProblemViewSet.as_view({"get": "list"})), path("/", include([ - path("description", ProblemViewSet.as_view({ + path("detail", ProblemViewSet.as_view({ "get": "retrieve", "put": "update", "patch": "partial_update", From c23d833f085a8e1cf093b019daa612a5b54c2125 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 18:26:20 +0900 Subject: [PATCH 243/552] =?UTF-8?q?docs:=20/problems/=20API=20=EB=AA=85?= =?UTF-8?q?=EC=84=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/api/problem.md | 197 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 189 insertions(+), 8 deletions(-) diff --git a/docs/api/problem.md b/docs/api/problem.md index 691aff9..4eaad45 100644 --- a/docs/api/problem.md +++ b/docs/api/problem.md @@ -5,12 +5,18 @@ ### URL ``` -POST /problem/ +POST /problems/ ``` +### Permission + +| Not Authenticated | Authenticated | Admin | +| :---------------: | :-----------: | :---: | +| X | O | O | + ### Body Parameters -지원되는 형식: `multipart/form-data` +지원되는 형식: `multipart/form-data`, `application/json` | key | type | required | example | description | | ------------------ | ------ | -------- | --------------------- | ------------ | @@ -28,6 +34,7 @@ POST /problem/ | ------ | ----------- | | 200 | OK | | 400 | BAD REQUEST | +| 403 | FORBIDDEN | ```json { @@ -63,19 +70,29 @@ POST /problem/ ## 문제 목록 조회하기 (문제검색) ``` -GET /problem/search? +GET /problems/search ``` -### Query Parameters +### Permission + +| Not Authenticated | Authenticated | Admin | +| :---------------: | :-----------: | :---: | +| X | O | O | -> TODO: 추후 수정 예정 +### Query Parameters -| key | description | -| --- | ----------- | -| | | +| key | description | example | +| --- | ------------------------------------------------ | ---------------------------- | +| q | 제목의 시작부분이 일치하는 항목을 필터링 합니다. | `/problems/search?q={query}` | ### Response Example +| status | description | +| ------ | ----------- | +| 200 | OK | +| 400 | BAD REQUEST | +| 403 | FORBIDDEN | + ```json { "count": 1, @@ -96,3 +113,167 @@ GET /problem/search? ] } ``` + +## 문제 상세 정보 조회하기 + +``` +GET /problems/{problemId}/detail +``` + +문제 상세 정보를 조회합니다. + +### Permission + +| Not Authenticated | Authenticated | Admin | +| :---------------: | :----------------------------------------: | :---: | +| X | 자신이 만든 문제 / 속한 크루의 문제만 가능 | O | + +### Response Example + +| status | description | +| ------ | ----------- | +| 200 | OK | +| 404 | NOT FOUND | + +#### 문제 분석이 존재하지 않는 경우 + +```json +{ + "id": 1, + "analysis": null, + "title": "A+B", + "link": "https://www.acmicpc.net/problem/1000", + "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", + "output_description": "첫째 줄에 A+B를 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB" + }, + "time_limit": 1.0, + "time_limit_unit": { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s" + }, + "created_at": "2024-07-17T10:10:55.275762Z", + "updated_at": "2024-07-17T10:11:06.957807Z" +} +``` + +#### 문제 분석이 존재하는 경우 + +```json +{ + "id": 1, + "analysis": { + "difficulty": { + "name_en": "EASY", + "name_ko": "쉬움", + "value": 1 + }, + "difficulty_description": "[이 기능은 아직 추가할 예정이 없습니다] 기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준", + "tags": [ + { + "parent": null, + "key": "math", + "name_ko": "수학", + "name_en": "mathematics" + } + ], + "time_complexity": "1", + "time_complexity_description": "[이 기능은 아직 추가할 예정이 없습니다] 선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요.", + "hint": [ + "우선 정수를 입력받는 방법에 대해서 찾아보는게 좋을 것 같아요.", + "정수를 입력받는 방법을 찾았다면, 다음으로는 한 줄에 공백으로 구분된 여러 개의 값을 입력 받는 방법을 찾아보세요." + ], + "created_at": "2024-07-18T02:03:24.987329Z" + }, + "title": "A+B", + "link": "http://boj.kr/1000", + "description": "A+B를 출력한다.", + "input_description": "하나의 줄에 공백을 기준으로 두 정수 A와 B가 주어진다.", + "output_description": "A+B를 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB" + }, + "time_limit": 1.0, + "time_limit_unit": { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s" + }, + "created_at": "2024-07-17T09:23:17.876425Z", + "updated_at": "2024-07-17T09:23:17.876456Z" +} +``` + +## 문제 수정하기 + +``` +PUT /problems/{problemId}/detail +``` + +``` +PATCH /problems/{problemId}/detail +``` + +### Permission + +| Not Authenticated | Authenticated | Admin | +| :---------------: | :---------------------: | :---: | +| X | 자신이 만든 문제만 가능 | O | + +### Body Parameters + +지원되는 형식: `multipart/form-data`, `application/json` + +| key | type | required | example | description | +| ------------------ | ------ | -------- | --------------------- | ------------ | +| title | string | yes | "A+B" | | +| link | string | yes | "https://boj.kr/1000" | | +| description | string | yes | "A+B를 출력한다." | | +| input_description | string | yes | "첫 번째 줄에..." | | +| output_description | string | yes | "첫 번째 줄에..." | | +| memory_limit | float | yes | 128.0 | MB 단위이다. | +| time_limit | float | yes | 1.0 | 초 단위이다. | + +### Response Example + +| status | description | +| ------ | ----------- | +| 200 | OK | +| 400 | BAD REQUEST | +| 403 | FORBIDDEN | +| 404 | NOT FOUND | + +```json +{ + "id": 1, + "analysis": null, + "title": "A+B", + "link": "https://www.acmicpc.net/problem/1000", + "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", + "output_description": "첫째 줄에 A+B를 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": { + "name_ko": "메가 바이트", + "name_en": "Mega Bytes", + "abbr": "MB" + }, + "time_limit": 1.0, + "time_limit_unit": { + "name_ko": "초", + "name_en": "Seconds", + "abbr": "s" + }, + "created_at": "2024-07-17T10:10:55.275762Z", + "updated_at": "2024-07-17T10:11:06.957807Z" +} +``` From 6891609335dd12d590d48e361458766524df50ab Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 19:15:10 +0900 Subject: [PATCH 244/552] refactor(tle.models): rename `is_boj_user_only` -> `is_boj_username_required` --- app/tle/models/crew.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 4bba1bd..212b396 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -1,10 +1,7 @@ from __future__ import annotations import typing -from django.core.validators import ( - MinValueValidator, - MaxValueValidator, -) +from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models from tle.models.user_solved_tier import UserSolvedTier @@ -42,7 +39,20 @@ class Crew(models.Model): blank=True, max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 ) - is_boj_user_only = models.BooleanField( + submittable_languages = models.ManyToManyField[SubmissionLanguage]( + SubmissionLanguage, + related_name='crews', + help_text='유저가 사용 가능한 언어를 입력해주세요.', + ) + custom_tags = models.JSONField( + help_text='태그를 입력해주세요.', + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + is_boj_username_required = models.BooleanField( help_text='백준 아이디 필요 여부를 입력해주세요.', default=False, ) @@ -63,19 +73,6 @@ class Crew(models.Model): null=True, default=None, ) - allowed_languages = models.ManyToManyField( - SubmissionLanguage, - related_name='crews', - help_text='유저가 사용 가능한 언어를 입력해주세요.', - ) - custom_tags = models.JSONField( - help_text='태그를 입력해주세요.', - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) is_recruiting = models.BooleanField( help_text='모집 중 여부를 입력해주세요.', default=True, From 760435e7bf8a2fb1f6b62b26ff236a38cac631f1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 19:15:48 +0900 Subject: [PATCH 245/552] feat(tle.models): add method `Crew.is_joinable(User)` --- app/tle/models/crew.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 212b396..49c0343 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -4,6 +4,7 @@ from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models +from tle.models.user import User from tle.models.user_solved_tier import UserSolvedTier from tle.models.submission_language import SubmissionLanguage @@ -106,3 +107,28 @@ def __repr__(self) -> str: def __str__(self) -> str: member_count = f'({self.members.count()}/{self.max_member})' return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' + + def is_joinable(self, user: User) -> bool: + if not self.is_recruiting: + return False + if self.captain == user: + return False + if self.members.count() >= self.max_members: + return False + if self.members.filter(user=user).exists(): + return False + if self.is_boj_username_required: + if user.boj_username is None: + # TODO: 인증된 BOJ 사용자명이어야 함 + return False + if self.min_boj_tier is not None: + if user.boj_tier is None: + return False + if user.boj_tier < self.min_boj_tier: + return False + if self.max_boj_tier is not None: + if user.boj_tier is None: + return False + if user.boj_tier > self.max_boj_tier: + return False + return True From 667ac628b0f9adeffde523e530ed94186fed7629 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 20:20:52 +0900 Subject: [PATCH 246/552] feat(tle.serializers): add `CrewMemeberSerializer` --- app/tle/serializers/__init__.py | 4 ++++ app/tle/serializers/crew_member.py | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 app/tle/serializers/crew_member.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index 894339b..65490b1 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -8,6 +8,8 @@ from tle.serializers.problem_minimal import ProblemMinimalSerializer from tle.serializers.problem_tag import ProblemTagSerializer +from tle.serializers.crew_member import CrewMemberSerializer + UserSerializer = UserDetailSerializer ProblemSerializer = ProblemDetailSerializer @@ -25,4 +27,6 @@ 'ProblemDifficultySerializer', 'ProblemMinimalSerializer', 'ProblemTagSerializer', + + 'CrewMemberSerializer', ) diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py new file mode 100644 index 0000000..e023958 --- /dev/null +++ b/app/tle/serializers/crew_member.py @@ -0,0 +1,19 @@ +from rest_framework.serializers import * + +from tle.models import CrewMember +from tle.serializers.user_minimal import UserMinimalSerializer + + +class CrewMemberSerializer(ModelSerializer): + user = UserMinimalSerializer(read_only=True) + is_captain = SerializerMethodField() + + class Meta: + model = CrewMember + fields = ( + 'user', + 'is_captain', + ) + + def get_is_captain(self, obj: CrewMember) -> bool: + return obj.crew.captain == obj.user From 3ef7989bd3c72b4c3b9579d49446d0596408bd71 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 20:21:24 +0900 Subject: [PATCH 247/552] feat(tle.serializers): add `CrewRecruitingSerializer` --- app/tle/serializers/__init__.py | 2 + app/tle/serializers/crew_recruiting.py | 74 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 app/tle/serializers/crew_recruiting.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index 65490b1..e7ec667 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -9,6 +9,7 @@ from tle.serializers.problem_tag import ProblemTagSerializer from tle.serializers.crew_member import CrewMemberSerializer +from tle.serializers.crew_recruiting import CrewRecruitingSerializer UserSerializer = UserDetailSerializer @@ -29,4 +30,5 @@ 'ProblemTagSerializer', 'CrewMemberSerializer', + 'CrewRecruitingSerializer', ) diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py new file mode 100644 index 0000000..b7e6c2c --- /dev/null +++ b/app/tle/serializers/crew_recruiting.py @@ -0,0 +1,74 @@ +import typing + +from rest_framework.serializers import * + +from tle.models import Crew, User, UserSolvedTier + + +class CrewRecruitingSerializer(ModelSerializer): + members = SerializerMethodField() + tags = SerializerMethodField() + is_joinable = SerializerMethodField() + + class Meta: + model = Crew + fields = [ + 'name', + 'emoji', + 'members', + 'tags', + 'is_joinable', + ] + + def get_members(self, obj: Crew): + return { + 'count': obj.members.count(), + 'max_count': obj.max_members, + } + + def get_tags(self, obj: Crew): + tags = [ + *self._get_language_tags(obj), + *self._get_tier_tags(obj), + *self._get_custom_tags(obj), + ] + return { + 'count': len(tags), + 'items': tags, + } + + def get_is_joinable(self, obj: Crew): + user = self._get_user() + assert user.is_authenticated + return obj.is_joinable(user) + + def _get_user(self) -> User: + return self.context['request'].user + + def _get_language_tags(self, obj: Crew): + return [ + self._tag_item(lang.key, lang.name) + for lang in obj.submittable_languages.all() + ] + + def _get_tier_tags(self, obj: Crew): + tags = [] + if obj.min_boj_tier is not None: + name = f'{UserSolvedTier(obj.min_boj_tier).label} 이상' + tags.append(self._tag_item(None, name)) + if obj.max_boj_tier is not None: + name = f'{UserSolvedTier(obj.max_boj_tier).label} 이하' + tags.append(self._tag_item(None, name)) + if obj.min_boj_tier is None and obj.max_boj_tier is None: + name = f'티어 무관' + tags.append(self._tag_item(None, name)) + return tags + + def _get_custom_tags(self, obj: Crew): + return [self._tag_item(None, tag) for tag in obj.custom_tags] + + def _tag_item(self, key: typing.Optional[str], name: str): + return { + 'key': key, + 'name': name, + } From 8e9f09b438d08ffaea87fd0afcfe09887caeed22 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 20:34:38 +0900 Subject: [PATCH 248/552] refactor(tle.models): add type hint --- app/tle/models/crew.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 49c0343..aba8a61 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -40,7 +40,7 @@ class Crew(models.Model): blank=True, max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 ) - submittable_languages = models.ManyToManyField[SubmissionLanguage]( + submittable_languages = models.ManyToManyField( SubmissionLanguage, related_name='crews', help_text='유저가 사용 가능한 언어를 입력해주세요.', @@ -100,6 +100,7 @@ class FieldName: ) applicants: models.ManyToManyField[T_CrewApplicant] members: models.ManyToManyField[T_CrewMember] + submittable_languages: models.ManyToManyField[SubmissionLanguage] def __repr__(self) -> str: return f'[{self.emoji} {self.name}]' From b0fedf9c595c0d38a346f4c2f3e44494fe6c4885 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 21:16:39 +0900 Subject: [PATCH 249/552] =?UTF-8?q?fix(tle.models):=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=ED=95=84=EB=93=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index aba8a61..0357ad2 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -106,7 +106,7 @@ def __repr__(self) -> str: return f'[{self.emoji} {self.name}]' def __str__(self) -> str: - member_count = f'({self.members.count()}/{self.max_member})' + member_count = f'({self.members.count()}/{self.max_members})' return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' def is_joinable(self, user: User) -> bool: From 6240a4d04c72069ff3415b086102d363c3d5a632 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 21:49:10 +0900 Subject: [PATCH 250/552] feat(tle.serializers): create `CurrentUserMixin` --- app/tle/serializers/mixins/__init__.py | 6 ++++++ app/tle/serializers/mixins/current_user.py | 8 ++++++++ 2 files changed, 14 insertions(+) create mode 100644 app/tle/serializers/mixins/__init__.py create mode 100644 app/tle/serializers/mixins/current_user.py diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py new file mode 100644 index 0000000..ad18951 --- /dev/null +++ b/app/tle/serializers/mixins/__init__.py @@ -0,0 +1,6 @@ +from tle.serializers.mixins.current_user import CurrentUserMixin + + +__all__ = ( + 'CurrentUserMixin', +) diff --git a/app/tle/serializers/mixins/current_user.py b/app/tle/serializers/mixins/current_user.py new file mode 100644 index 0000000..2697708 --- /dev/null +++ b/app/tle/serializers/mixins/current_user.py @@ -0,0 +1,8 @@ +from rest_framework.serializers import Serializer + +from tle.models import User + + +class CurrentUserMixin: + def current_user(self: Serializer) -> User: + return self.context['request'].user From 38d5d8a77f68b7a3b663e531e8ba1623ce1e5fd3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:30:13 +0900 Subject: [PATCH 251/552] =?UTF-8?q?feat(tle.models):=20`Crew.emoji`=20?= =?UTF-8?q?=EC=97=90=20default=20=EA=B0=92=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 0357ad2..798aec8 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -22,6 +22,7 @@ class Crew(models.Model): ], null=True, blank=True, + default='🚢', help_text='크루 아이콘을 입력해주세요. (이모지)', ) max_members = models.IntegerField( From f639b3e6d837d0fe3b5e5a7212aad40c2eb996eb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:31:59 +0900 Subject: [PATCH 252/552] =?UTF-8?q?feat(tle.models):=20`Crew.get=5Ftags()`?= =?UTF-8?q?=20=EB=A9=94=EC=86=8C=EB=93=9C=EC=99=80=20`CrewTag`=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 798aec8..3447703 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -1,4 +1,5 @@ from __future__ import annotations +import dataclasses import typing from django.core.validators import MinValueValidator, MaxValueValidator @@ -9,6 +10,20 @@ from tle.models.submission_language import SubmissionLanguage +@dataclasses.dataclass +class CrewTag: + key: typing.Optional[str] + name: str + + @classmethod + def from_language(cls, lang: SubmissionLanguage) -> CrewTag: + return CrewTag(key=lang.key, name=lang.name) + + @classmethod + def from_name(cls, name: str) -> CrewTag: + return CrewTag(key=None, name=name) + + class Crew(models.Model): name = models.CharField( max_length=20, @@ -134,3 +149,35 @@ def is_joinable(self, user: User) -> bool: if user.boj_tier > self.max_boj_tier: return False return True + + def get_tags(self) -> typing.List[CrewTag]: + return [ + *map(CrewTag.from_language, self.submittable_languages.all()), + *self._build_tier_tags(), + *map(CrewTag.from_name, self.custom_tags), + ] + + def _build_tier_tags(self) -> typing.List[CrewTag]: + tags = [] + if self.min_boj_tier is None and self.max_boj_tier is None: + tags.append(CrewTag.from_name('티어 무관')) + else: + if self.min_boj_tier is not None: + tags.append(self._build_min_tier_tag()) + if self.max_boj_tier is not None: + tags.append(self._build_max_tier_tag()) + return tags + + def _build_min_tier_tag(self) -> CrewTag: + if UserSolvedTier.get_tier(self.min_boj_tier) == 5: + tier_name = UserSolvedTier.get_rank_name(self.min_boj_tier) + else: + tier_name = UserSolvedTier.get_name(self.min_boj_tier) + return CrewTag.from_name(f'{tier_name} 이상') + + def _build_max_tier_tag(self) -> CrewTag: + if UserSolvedTier.get_tier(self.max_boj_tier) == 1: + tier_name = UserSolvedTier.get_rank_name(self.max_boj_tier) + else: + tier_name = UserSolvedTier.get_name(self.max_boj_tier) + return CrewTag.from_name(f'{tier_name} 이하') From c65dc0e972ae05a088bee3c00ad2b33121124f75 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:33:06 +0900 Subject: [PATCH 253/552] =?UTF-8?q?feat(tle.models):=20`Crew.created=5Fby`?= =?UTF-8?q?=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 3447703..74b95ef 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -99,6 +99,12 @@ class Crew(models.Model): default=True, ) created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + User, + on_delete=models.PROTECT, + null=False, + blank=False, + ) updated_at = models.DateTimeField(auto_now=True) @property From d4dbe6feec32e7db23a98a6d260ebc4f7aef9d9f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:34:02 +0900 Subject: [PATCH 254/552] =?UTF-8?q?feat(tle.models):=20`Crew`=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=8B=9C=EC=97=90=20=EC=BA=A1=ED=8B=B4(`CrewMember`)?= =?UTF-8?q?=EB=8F=84=20=EC=83=9D=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 74b95ef..626aa12 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -3,7 +3,7 @@ import typing from django.core.validators import MinValueValidator, MaxValueValidator -from django.db import models +from django.db import models, transaction from tle.models.user import User from tle.models.user_solved_tier import UserSolvedTier @@ -131,6 +131,17 @@ def __str__(self) -> str: member_count = f'({self.members.count()}/{self.max_members})' return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' + def save(self, *args, **kwargs) -> None: + with transaction.atomic(): + obj = super().save(*args, **kwargs) + if not self.members.filter(user=self.created_by).exists(): + captain = self.members.create( + user=self.created_by, + is_captain=True + ) + captain.save() + return obj + def is_joinable(self, user: User) -> bool: if not self.is_recruiting: return False From 4fc732002c6fbef534959cac9ffdbc480418b37c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:34:41 +0900 Subject: [PATCH 255/552] =?UTF-8?q?feat(tle.models):=20`Crew.is=5Fmember(U?= =?UTF-8?q?ser)`=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 626aa12..b92fdd3 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -142,6 +142,9 @@ def save(self, *args, **kwargs) -> None: captain.save() return obj + def is_member(self, user: User) -> bool: + return self.members.filter(user=user).exists() + def is_joinable(self, user: User) -> bool: if not self.is_recruiting: return False From ab2b44b1f04ba1e8d3e8b9dfc5495e315976b9f1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:35:33 +0900 Subject: [PATCH 256/552] =?UTF-8?q?feat(tle.models):=20`UserSolvedTier`=20?= =?UTF-8?q?=EC=9D=98=20=ED=8B=B0=EC=96=B4,=20=EB=9E=AD=ED=81=AC=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=EC=9D=84=20=EB=A0=8C=EB=8D=94=EB=A7=81=20?= =?UTF-8?q?=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=EB=93=A4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/user_solved_tier.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/app/tle/models/user_solved_tier.py b/app/tle/models/user_solved_tier.py index 9f9decf..88e73a0 100644 --- a/app/tle/models/user_solved_tier.py +++ b/app/tle/models/user_solved_tier.py @@ -1,6 +1,37 @@ from django.db import models +RANK_NAMES = { + 'ko': { + 0: '난이도를 매길 수 없음', + 1: '브론즈', + 2: '실버', + 3: '골드', + 4: '플래티넘', + 5: '다이아몬드', + 6: '루비', + }, + 'en': { + 0: 'Unrated', + 1: 'Bronze', + 2: 'Silver', + 3: 'Gold', + 4: 'Platinum', + 5: 'Diamond', + 6: 'Ruby', + }, +} + +ARABIC_NUMERALS = { + 0: '', + 1: 'I', + 2: 'II', + 3: 'III', + 4: 'IV', + 5: 'V', +} + + class UserSolvedTier(models.IntegerChoices): U = 0, 'Unrated' B5 = 1, '브론즈 5' @@ -33,3 +64,32 @@ class UserSolvedTier(models.IntegerChoices): R3 = 28, '루비 3' R2 = 29, '루비 2' R1 = 30, '루비 1' + + @classmethod + def get_rank(cls, value: int) -> int: + if value == 0: + return 0 + assert 1 <= value <= 30 + return ((value-1) // 5)+1 + + @classmethod + def get_rank_name(cls, value: int, lang='ko') -> str: + assert 0 <= value <= 30 + return RANK_NAMES[lang][cls.get_rank(value)] + + @classmethod + def get_tier(cls, value: int) -> int: + if value == 0: + return 0 + assert 1 <= value <= 30 + return 5 - ((value-1) % 5) + + @classmethod + def get_tier_name(cls, value: int) -> str: + assert 0 <= value <= 30 + return ARABIC_NUMERALS[cls.get_tier(value)] + + @classmethod + def get_name(cls, value: int, lang='ko') -> str: + assert 0 <= value <= 30 + return f'{cls.get_rank_name(value, lang)} {cls.get_tier_name(value)}' From 09e3b2ab80986eaf88a332d2423f2310af5f0fa5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 19 Jul 2024 22:41:22 +0900 Subject: [PATCH 257/552] feat(tle.serializers): add `is_member`, `is_joinable` fields --- app/tle/serializers/crew_recruiting.py | 64 +++++++------------------- 1 file changed, 16 insertions(+), 48 deletions(-) diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index b7e6c2c..af339a2 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -1,25 +1,33 @@ -import typing - from rest_framework.serializers import * -from tle.models import Crew, User, UserSolvedTier +from tle.models import Crew +from tle.serializers.mixins import CurrentUserMixin -class CrewRecruitingSerializer(ModelSerializer): +class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin): + is_joinable = SerializerMethodField() + is_member = SerializerMethodField() members = SerializerMethodField() tags = SerializerMethodField() - is_joinable = SerializerMethodField() class Meta: model = Crew fields = [ 'name', 'emoji', + 'is_recruiting', + 'is_joinable', + 'is_member', 'members', 'tags', - 'is_joinable', ] + def get_is_joinable(self, obj: Crew): + return obj.is_recruiting + + def get_is_member(self, obj: Crew): + return obj.members.filter(user=self.current_user()).exists() + def get_members(self, obj: Crew): return { 'count': obj.members.count(), @@ -27,48 +35,8 @@ def get_members(self, obj: Crew): } def get_tags(self, obj: Crew): - tags = [ - *self._get_language_tags(obj), - *self._get_tier_tags(obj), - *self._get_custom_tags(obj), - ] + tags = obj.get_tags() return { 'count': len(tags), - 'items': tags, - } - - def get_is_joinable(self, obj: Crew): - user = self._get_user() - assert user.is_authenticated - return obj.is_joinable(user) - - def _get_user(self) -> User: - return self.context['request'].user - - def _get_language_tags(self, obj: Crew): - return [ - self._tag_item(lang.key, lang.name) - for lang in obj.submittable_languages.all() - ] - - def _get_tier_tags(self, obj: Crew): - tags = [] - if obj.min_boj_tier is not None: - name = f'{UserSolvedTier(obj.min_boj_tier).label} 이상' - tags.append(self._tag_item(None, name)) - if obj.max_boj_tier is not None: - name = f'{UserSolvedTier(obj.max_boj_tier).label} 이하' - tags.append(self._tag_item(None, name)) - if obj.min_boj_tier is None and obj.max_boj_tier is None: - name = f'티어 무관' - tags.append(self._tag_item(None, name)) - return tags - - def _get_custom_tags(self, obj: Crew): - return [self._tag_item(None, tag) for tag in obj.custom_tags] - - def _tag_item(self, key: typing.Optional[str], name: str): - return { - 'key': key, - 'name': name, + 'items': [{'key': tag.key, 'name': tag.name} for tag in tags], } From d2c44788be7d12959801d1c9602e70b000d05132 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 00:01:46 +0900 Subject: [PATCH 258/552] feat(tle.enums): create `enums` module & `Emoji` --- app/tle/enums/__init__.py | 1 + app/tle/enums/emoji.py | 5038 +++++++++++++++++++++++++++++++++++++ 2 files changed, 5039 insertions(+) create mode 100644 app/tle/enums/__init__.py create mode 100644 app/tle/enums/emoji.py diff --git a/app/tle/enums/__init__.py b/app/tle/enums/__init__.py new file mode 100644 index 0000000..b01f75e --- /dev/null +++ b/app/tle/enums/__init__.py @@ -0,0 +1 @@ +from tle.enums.emoji import Emoji diff --git a/app/tle/enums/emoji.py b/app/tle/enums/emoji.py new file mode 100644 index 0000000..db395c3 --- /dev/null +++ b/app/tle/enums/emoji.py @@ -0,0 +1,5038 @@ +from enum import Enum + + +class Emoji(Enum): + U1F947 = "🥇" # :1st_place_medal: + U1F948 = "🥈" # :2nd_place_medal: + U1F949 = "🥉" # :3rd_place_medal: + U1F18E = "🆎" # :AB_button_(blood_type): + U1F3E7 = "🏧" # :ATM_sign: + U1F170FE0F = "🅰️" # :A_button_(blood_type): + U1F170 = "🅰" # :A_button_(blood_type): + U1F1E61F1EB = "🇦🇫" # :Afghanistan: + U1F1E61F1F1 = "🇦🇱" # :Albania: + U1F1E91F1FF = "🇩🇿" # :Algeria: + U1F1E61F1F8 = "🇦🇸" # :American_Samoa: + U1F1E61F1E9 = "🇦🇩" # :Andorra: + U1F1E61F1F4 = "🇦🇴" # :Angola: + U1F1E61F1EE = "🇦🇮" # :Anguilla: + U1F1E61F1F6 = "🇦🇶" # :Antarctica: + U1F1E61F1EC = "🇦🇬" # :Antigua_&_Barbuda: + U2652 = "♒" # :Aquarius: + U1F1E61F1F7 = "🇦🇷" # :Argentina: + U2648 = "♈" # :Aries: + U1F1E61F1F2 = "🇦🇲" # :Armenia: + U1F1E61F1FC = "🇦🇼" # :Aruba: + U1F1E61F1E8 = "🇦🇨" # :Ascension_Island: + U1F1E61F1FA = "🇦🇺" # :Australia: + U1F1E61F1F9 = "🇦🇹" # :Austria: + U1F1E61F1FF = "🇦🇿" # :Azerbaijan: + U1F519 = "🔙" # :BACK_arrow: + U1F171FE0F = "🅱️" # :B_button_(blood_type): + U1F171 = "🅱" # :B_button_(blood_type): + U1F1E71F1F8 = "🇧🇸" # :Bahamas: + U1F1E71F1ED = "🇧🇭" # :Bahrain: + U1F1E71F1E9 = "🇧🇩" # :Bangladesh: + U1F1E71F1E7 = "🇧🇧" # :Barbados: + U1F1E71F1FE = "🇧🇾" # :Belarus: + U1F1E71F1EA = "🇧🇪" # :Belgium: + U1F1E71F1FF = "🇧🇿" # :Belize: + U1F1E71F1EF = "🇧🇯" # :Benin: + U1F1E71F1F2 = "🇧🇲" # :Bermuda: + U1F1E71F1F9 = "🇧🇹" # :Bhutan: + U1F1E71F1F4 = "🇧🇴" # :Bolivia: + U1F1E71F1E6 = "🇧🇦" # :Bosnia_&_Herzegovina: + U1F1E71F1FC = "🇧🇼" # :Botswana: + U1F1E71F1FB = "🇧🇻" # :Bouvet_Island: + U1F1E71F1F7 = "🇧🇷" # :Brazil: + U1F1EE1F1F4 = "🇮🇴" # :British_Indian_Ocean_Territory: + U1F1FB1F1EC = "🇻🇬" # :British_Virgin_Islands: + U1F1E71F1F3 = "🇧🇳" # :Brunei: + U1F1E71F1EC = "🇧🇬" # :Bulgaria: + U1F1E71F1EB = "🇧🇫" # :Burkina_Faso: + U1F1E71F1EE = "🇧🇮" # :Burundi: + U1F191 = "🆑" # :CL_button: + U1F192 = "🆒" # :COOL_button: + U1F1F01F1ED = "🇰🇭" # :Cambodia: + U1F1E81F1F2 = "🇨🇲" # :Cameroon: + U1F1E81F1E6 = "🇨🇦" # :Canada: + U1F1EE1F1E8 = "🇮🇨" # :Canary_Islands: + U264B = "♋" # :Cancer: + U1F1E81F1FB = "🇨🇻" # :Cape_Verde: + U2651 = "♑" # :Capricorn: + U1F1E71F1F6 = "🇧🇶" # :Caribbean_Netherlands: + U1F1F01F1FE = "🇰🇾" # :Cayman_Islands: + U1F1E81F1EB = "🇨🇫" # :Central_African_Republic: + U1F1EA1F1E6 = "🇪🇦" # :Ceuta_&_Melilla: + U1F1F91F1E9 = "🇹🇩" # :Chad: + U1F1E81F1F1 = "🇨🇱" # :Chile: + U1F1E81F1F3 = "🇨🇳" # :China: + U1F1E81F1FD = "🇨🇽" # :Christmas_Island: + U1F384 = "🎄" # :Christmas_tree: + U1F1E81F1F5 = "🇨🇵" # :Clipperton_Island: + U1F1E81F1E8 = "🇨🇨" # :Cocos_(Keeling)_Islands: + U1F1E81F1F4 = "🇨🇴" # :Colombia: + U1F1F01F1F2 = "🇰🇲" # :Comoros: + U1F1E81F1EC = "🇨🇬" # :Congo-Brazzaville: + U1F1E81F1E9 = "🇨🇩" # :Congo-Kinshasa: + U1F1E81F1F0 = "🇨🇰" # :Cook_Islands: + U1F1E81F1F7 = "🇨🇷" # :Costa_Rica: + U1F1ED1F1F7 = "🇭🇷" # :Croatia: + U1F1E81F1FA = "🇨🇺" # :Cuba: + U1F1E81F1FC = "🇨🇼" # :Curaçao: + U1F1E81F1FE = "🇨🇾" # :Cyprus: + U1F1E81F1FF = "🇨🇿" # :Czechia: + U1F1E81F1EE = "🇨🇮" # :Côte_d’Ivoire: + U1F1E91F1F0 = "🇩🇰" # :Denmark: + U1F1E91F1EC = "🇩🇬" # :Diego_Garcia: + U1F1E91F1EF = "🇩🇯" # :Djibouti: + U1F1E91F1F2 = "🇩🇲" # :Dominica: + U1F1E91F1F4 = "🇩🇴" # :Dominican_Republic: + U1F51A = "🔚" # :END_arrow: + U1F1EA1F1E8 = "🇪🇨" # :Ecuador: + U1F1EA1F1EC = "🇪🇬" # :Egypt: + U1F1F81F1FB = "🇸🇻" # :El_Salvador: + U1F3F4E0067E0062E0065E006EE0067E007F = "🏴󠁧󠁢󠁥󠁮󠁧󠁿" # :England: + U1F1EC1F1F6 = "🇬🇶" # :Equatorial_Guinea: + U1F1EA1F1F7 = "🇪🇷" # :Eritrea: + U1F1EA1F1EA = "🇪🇪" # :Estonia: + U1F1F81F1FF = "🇸🇿" # :Eswatini: + U1F1EA1F1F9 = "🇪🇹" # :Ethiopia: + U1F1EA1F1FA = "🇪🇺" # :European_Union: + U1F193 = "🆓" # :FREE_button: + U1F1EB1F1F0 = "🇫🇰" # :Falkland_Islands: + U1F1EB1F1F4 = "🇫🇴" # :Faroe_Islands: + U1F1EB1F1EF = "🇫🇯" # :Fiji: + U1F1EB1F1EE = "🇫🇮" # :Finland: + U1F1EB1F1F7 = "🇫🇷" # :France: + U1F1EC1F1EB = "🇬🇫" # :French_Guiana: + U1F1F51F1EB = "🇵🇫" # :French_Polynesia: + U1F1F91F1EB = "🇹🇫" # :French_Southern_Territories: + U1F1EC1F1E6 = "🇬🇦" # :Gabon: + U1F1EC1F1F2 = "🇬🇲" # :Gambia: + U264A = "♊" # :Gemini: + U1F1EC1F1EA = "🇬🇪" # :Georgia: + U1F1E91F1EA = "🇩🇪" # :Germany: + U1F1EC1F1ED = "🇬🇭" # :Ghana: + U1F1EC1F1EE = "🇬🇮" # :Gibraltar: + U1F1EC1F1F7 = "🇬🇷" # :Greece: + U1F1EC1F1F1 = "🇬🇱" # :Greenland: + U1F1EC1F1E9 = "🇬🇩" # :Grenada: + U1F1EC1F1F5 = "🇬🇵" # :Guadeloupe: + U1F1EC1F1FA = "🇬🇺" # :Guam: + U1F1EC1F1F9 = "🇬🇹" # :Guatemala: + U1F1EC1F1EC = "🇬🇬" # :Guernsey: + U1F1EC1F1F3 = "🇬🇳" # :Guinea: + U1F1EC1F1FC = "🇬🇼" # :Guinea-Bissau: + U1F1EC1F1FE = "🇬🇾" # :Guyana: + U1F1ED1F1F9 = "🇭🇹" # :Haiti: + U1F1ED1F1F2 = "🇭🇲" # :Heard_&_McDonald_Islands: + U1F1ED1F1F3 = "🇭🇳" # :Honduras: + U1F1ED1F1F0 = "🇭🇰" # :Hong_Kong_SAR_China: + U1F1ED1F1FA = "🇭🇺" # :Hungary: + U1F194 = "🆔" # :ID_button: + U1F1EE1F1F8 = "🇮🇸" # :Iceland: + U1F1EE1F1F3 = "🇮🇳" # :India: + U1F1EE1F1E9 = "🇮🇩" # :Indonesia: + U1F1EE1F1F7 = "🇮🇷" # :Iran: + U1F1EE1F1F6 = "🇮🇶" # :Iraq: + U1F1EE1F1EA = "🇮🇪" # :Ireland: + U1F1EE1F1F2 = "🇮🇲" # :Isle_of_Man: + U1F1EE1F1F1 = "🇮🇱" # :Israel: + U1F1EE1F1F9 = "🇮🇹" # :Italy: + U1F1EF1F1F2 = "🇯🇲" # :Jamaica: + U1F1EF1F1F5 = "🇯🇵" # :Japan: + U1F251 = "🉑" # :Japanese_acceptable_button: + U1F238 = "🈸" # :Japanese_application_button: + U1F250 = "🉐" # :Japanese_bargain_button: + U1F3EF = "🏯" # :Japanese_castle: + U3297FE0F = "㊗️" # :Japanese_congratulations_button: + U3297 = "㊗" # :Japanese_congratulations_button: + U1F239 = "🈹" # :Japanese_discount_button: + U1F38E = "🎎" # :Japanese_dolls: + U1F21A = "🈚" # :Japanese_free_of_charge_button: + U1F201 = "🈁" # :Japanese_here_button: + U1F237FE0F = "🈷️" # :Japanese_monthly_amount_button: + U1F237 = "🈷" # :Japanese_monthly_amount_button: + U1F235 = "🈵" # :Japanese_no_vacancy_button: + U1F236 = "🈶" # :Japanese_not_free_of_charge_button: + U1F23A = "🈺" # :Japanese_open_for_business_button: + U1F234 = "🈴" # :Japanese_passing_grade_button: + U1F3E3 = "🏣" # :Japanese_post_office: + U1F232 = "🈲" # :Japanese_prohibited_button: + U1F22F = "🈯" # :Japanese_reserved_button: + U3299FE0F = "㊙️" # :Japanese_secret_button: + U3299 = "㊙" # :Japanese_secret_button: + U1F202FE0F = "🈂️" # :Japanese_service_charge_button: + U1F202 = "🈂" # :Japanese_service_charge_button: + U1F530 = "🔰" # :Japanese_symbol_for_beginner: + U1F233 = "🈳" # :Japanese_vacancy_button: + U1F1EF1F1EA = "🇯🇪" # :Jersey: + U1F1EF1F1F4 = "🇯🇴" # :Jordan: + U1F1F01F1FF = "🇰🇿" # :Kazakhstan: + U1F1F01F1EA = "🇰🇪" # :Kenya: + U1F1F01F1EE = "🇰🇮" # :Kiribati: + U1F1FD1F1F0 = "🇽🇰" # :Kosovo: + U1F1F01F1FC = "🇰🇼" # :Kuwait: + U1F1F01F1EC = "🇰🇬" # :Kyrgyzstan: + U1F1F11F1E6 = "🇱🇦" # :Laos: + U1F1F11F1FB = "🇱🇻" # :Latvia: + U1F1F11F1E7 = "🇱🇧" # :Lebanon: + U264C = "♌" # :Leo: + U1F1F11F1F8 = "🇱🇸" # :Lesotho: + U1F1F11F1F7 = "🇱🇷" # :Liberia: + U264E = "♎" # :Libra: + U1F1F11F1FE = "🇱🇾" # :Libya: + U1F1F11F1EE = "🇱🇮" # :Liechtenstein: + U1F1F11F1F9 = "🇱🇹" # :Lithuania: + U1F1F11F1FA = "🇱🇺" # :Luxembourg: + U1F1F21F1F4 = "🇲🇴" # :Macao_SAR_China: + U1F1F21F1EC = "🇲🇬" # :Madagascar: + U1F1F21F1FC = "🇲🇼" # :Malawi: + U1F1F21F1FE = "🇲🇾" # :Malaysia: + U1F1F21F1FB = "🇲🇻" # :Maldives: + U1F1F21F1F1 = "🇲🇱" # :Mali: + U1F1F21F1F9 = "🇲🇹" # :Malta: + U1F1F21F1ED = "🇲🇭" # :Marshall_Islands: + U1F1F21F1F6 = "🇲🇶" # :Martinique: + U1F1F21F1F7 = "🇲🇷" # :Mauritania: + U1F1F21F1FA = "🇲🇺" # :Mauritius: + U1F1FE1F1F9 = "🇾🇹" # :Mayotte: + U1F1F21F1FD = "🇲🇽" # :Mexico: + U1F1EB1F1F2 = "🇫🇲" # :Micronesia: + U1F1F21F1E9 = "🇲🇩" # :Moldova: + U1F1F21F1E8 = "🇲🇨" # :Monaco: + U1F1F21F1F3 = "🇲🇳" # :Mongolia: + U1F1F21F1EA = "🇲🇪" # :Montenegro: + U1F1F21F1F8 = "🇲🇸" # :Montserrat: + U1F1F21F1E6 = "🇲🇦" # :Morocco: + U1F1F21F1FF = "🇲🇿" # :Mozambique: + U1F936 = "🤶" # :Mrs._Claus: + U1F9361F3FF = "🤶🏿" # :Mrs._Claus_dark_skin_tone: + U1F9361F3FB = "🤶🏻" # :Mrs._Claus_light_skin_tone: + U1F9361F3FE = "🤶🏾" # :Mrs._Claus_medium-dark_skin_tone: + U1F9361F3FC = "🤶🏼" # :Mrs._Claus_medium-light_skin_tone: + U1F9361F3FD = "🤶🏽" # :Mrs._Claus_medium_skin_tone: + U1F1F21F1F2 = "🇲🇲" # :Myanmar_(Burma): + U1F195 = "🆕" # :NEW_button: + U1F196 = "🆖" # :NG_button: + U1F1F31F1E6 = "🇳🇦" # :Namibia: + U1F1F31F1F7 = "🇳🇷" # :Nauru: + U1F1F31F1F5 = "🇳🇵" # :Nepal: + U1F1F31F1F1 = "🇳🇱" # :Netherlands: + U1F1F31F1E8 = "🇳🇨" # :New_Caledonia: + U1F1F31F1FF = "🇳🇿" # :New_Zealand: + U1F1F31F1EE = "🇳🇮" # :Nicaragua: + U1F1F31F1EA = "🇳🇪" # :Niger: + U1F1F31F1EC = "🇳🇬" # :Nigeria: + U1F1F31F1FA = "🇳🇺" # :Niue: + U1F1F31F1EB = "🇳🇫" # :Norfolk_Island: + U1F1F01F1F5 = "🇰🇵" # :North_Korea: + U1F1F21F1F0 = "🇲🇰" # :North_Macedonia: + U1F1F21F1F5 = "🇲🇵" # :Northern_Mariana_Islands: + U1F1F31F1F4 = "🇳🇴" # :Norway: + U1F197 = "🆗" # :OK_button: + U1F44C = "👌" # :OK_hand: + U1F44C1F3FF = "👌🏿" # :OK_hand_dark_skin_tone: + U1F44C1F3FB = "👌🏻" # :OK_hand_light_skin_tone: + U1F44C1F3FE = "👌🏾" # :OK_hand_medium-dark_skin_tone: + U1F44C1F3FC = "👌🏼" # :OK_hand_medium-light_skin_tone: + U1F44C1F3FD = "👌🏽" # :OK_hand_medium_skin_tone: + U1F51B = "🔛" # :ON!_arrow: + U1F17EFE0F = "🅾️" # :O_button_(blood_type): + U1F17E = "🅾" # :O_button_(blood_type): + U1F1F41F1F2 = "🇴🇲" # :Oman: + U26CE = "⛎" # :Ophiuchus: + U1F17FFE0F = "🅿️" # :P_button: + U1F17F = "🅿" # :P_button: + U1F1F51F1F0 = "🇵🇰" # :Pakistan: + U1F1F51F1FC = "🇵🇼" # :Palau: + U1F1F51F1F8 = "🇵🇸" # :Palestinian_Territories: + U1F1F51F1E6 = "🇵🇦" # :Panama: + U1F1F51F1EC = "🇵🇬" # :Papua_New_Guinea: + U1F1F51F1FE = "🇵🇾" # :Paraguay: + U1F1F51F1EA = "🇵🇪" # :Peru: + U1F1F51F1ED = "🇵🇭" # :Philippines: + U2653 = "♓" # :Pisces: + U1F1F51F1F3 = "🇵🇳" # :Pitcairn_Islands: + U1F1F51F1F1 = "🇵🇱" # :Poland: + U1F1F51F1F9 = "🇵🇹" # :Portugal: + U1F1F51F1F7 = "🇵🇷" # :Puerto_Rico: + U1F1F61F1E6 = "🇶🇦" # :Qatar: + U1F1F71F1F4 = "🇷🇴" # :Romania: + U1F1F71F1FA = "🇷🇺" # :Russia: + U1F1F71F1FC = "🇷🇼" # :Rwanda: + U1F1F71F1EA = "🇷🇪" # :Réunion: + U1F51C = "🔜" # :SOON_arrow: + U1F198 = "🆘" # :SOS_button: + U2650 = "♐" # :Sagittarius: + U1F1FC1F1F8 = "🇼🇸" # :Samoa: + U1F1F81F1F2 = "🇸🇲" # :San_Marino: + U1F385 = "🎅" # :Santa_Claus: + U1F3851F3FF = "🎅🏿" # :Santa_Claus_dark_skin_tone: + U1F3851F3FB = "🎅🏻" # :Santa_Claus_light_skin_tone: + U1F3851F3FE = "🎅🏾" # :Santa_Claus_medium-dark_skin_tone: + U1F3851F3FC = "🎅🏼" # :Santa_Claus_medium-light_skin_tone: + U1F3851F3FD = "🎅🏽" # :Santa_Claus_medium_skin_tone: + U1F1F81F1E6 = "🇸🇦" # :Saudi_Arabia: + U264F = "♏" # :Scorpio: + U1F3F4E0067E0062E0073E0063E0074E007F = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" # :Scotland: + U1F1F81F1F3 = "🇸🇳" # :Senegal: + U1F1F71F1F8 = "🇷🇸" # :Serbia: + U1F1F81F1E8 = "🇸🇨" # :Seychelles: + U1F1F81F1F1 = "🇸🇱" # :Sierra_Leone: + U1F1F81F1EC = "🇸🇬" # :Singapore: + U1F1F81F1FD = "🇸🇽" # :Sint_Maarten: + U1F1F81F1F0 = "🇸🇰" # :Slovakia: + U1F1F81F1EE = "🇸🇮" # :Slovenia: + U1F1F81F1E7 = "🇸🇧" # :Solomon_Islands: + U1F1F81F1F4 = "🇸🇴" # :Somalia: + U1F1FF1F1E6 = "🇿🇦" # :South_Africa: + U1F1EC1F1F8 = "🇬🇸" # :South_Georgia_&_South_Sandwich_Islands: + U1F1F01F1F7 = "🇰🇷" # :South_Korea: + U1F1F81F1F8 = "🇸🇸" # :South_Sudan: + U1F1EA1F1F8 = "🇪🇸" # :Spain: + U1F1F11F1F0 = "🇱🇰" # :Sri_Lanka: + U1F1E71F1F1 = "🇧🇱" # :St._Barthélemy: + U1F1F81F1ED = "🇸🇭" # :St._Helena: + U1F1F01F1F3 = "🇰🇳" # :St._Kitts_&_Nevis: + U1F1F11F1E8 = "🇱🇨" # :St._Lucia: + U1F1F21F1EB = "🇲🇫" # :St._Martin: + U1F1F51F1F2 = "🇵🇲" # :St._Pierre_&_Miquelon: + U1F1FB1F1E8 = "🇻🇨" # :St._Vincent_&_Grenadines: + U1F5FD = "🗽" # :Statue_of_Liberty: + U1F1F81F1E9 = "🇸🇩" # :Sudan: + U1F1F81F1F7 = "🇸🇷" # :Suriname: + U1F1F81F1EF = "🇸🇯" # :Svalbard_&_Jan_Mayen: + U1F1F81F1EA = "🇸🇪" # :Sweden: + U1F1E81F1ED = "🇨🇭" # :Switzerland: + U1F1F81F1FE = "🇸🇾" # :Syria: + U1F1F81F1F9 = "🇸🇹" # :São_Tomé_&_Príncipe: + U1F996 = "🦖" # :T-Rex: + U1F51D = "🔝" # :TOP_arrow: + U1F1F91F1FC = "🇹🇼" # :Taiwan: + U1F1F91F1EF = "🇹🇯" # :Tajikistan: + U1F1F91F1FF = "🇹🇿" # :Tanzania: + U2649 = "♉" # :Taurus: + U1F1F91F1ED = "🇹🇭" # :Thailand: + U1F1F91F1F1 = "🇹🇱" # :Timor-Leste: + U1F1F91F1EC = "🇹🇬" # :Togo: + U1F1F91F1F0 = "🇹🇰" # :Tokelau: + U1F5FC = "🗼" # :Tokyo_tower: + U1F1F91F1F4 = "🇹🇴" # :Tonga: + U1F1F91F1F9 = "🇹🇹" # :Trinidad_&_Tobago: + U1F1F91F1E6 = "🇹🇦" # :Tristan_da_Cunha: + U1F1F91F1F3 = "🇹🇳" # :Tunisia: + U1F1F91F1F2 = "🇹🇲" # :Turkmenistan: + U1F1F91F1E8 = "🇹🇨" # :Turks_&_Caicos_Islands: + U1F1F91F1FB = "🇹🇻" # :Tuvalu: + U1F1F91F1F7 = "🇹🇷" # :Türkiye: + U1F1FA1F1F2 = "🇺🇲" # :U.S._Outlying_Islands: + U1F1FB1F1EE = "🇻🇮" # :U.S._Virgin_Islands: + U1F199 = "🆙" # :UP!_button: + U1F1FA1F1EC = "🇺🇬" # :Uganda: + U1F1FA1F1E6 = "🇺🇦" # :Ukraine: + U1F1E61F1EA = "🇦🇪" # :United_Arab_Emirates: + U1F1EC1F1E7 = "🇬🇧" # :United_Kingdom: + U1F1FA1F1F3 = "🇺🇳" # :United_Nations: + U1F1FA1F1F8 = "🇺🇸" # :United_States: + U1F1FA1F1FE = "🇺🇾" # :Uruguay: + U1F1FA1F1FF = "🇺🇿" # :Uzbekistan: + U1F19A = "🆚" # :VS_button: + U1F1FB1F1FA = "🇻🇺" # :Vanuatu: + U1F1FB1F1E6 = "🇻🇦" # :Vatican_City: + U1F1FB1F1EA = "🇻🇪" # :Venezuela: + U1F1FB1F1F3 = "🇻🇳" # :Vietnam: + U264D = "♍" # :Virgo: + U1F3F4E0067E0062E0077E006CE0073E007F = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" # :Wales: + U1F1FC1F1EB = "🇼🇫" # :Wallis_&_Futuna: + U1F1EA1F1ED = "🇪🇭" # :Western_Sahara: + U1F1FE1F1EA = "🇾🇪" # :Yemen: + U1F4A4 = "💤" # :ZZZ: + U1F1FF1F1F2 = "🇿🇲" # :Zambia: + U1F1FF1F1FC = "🇿🇼" # :Zimbabwe: + U1F9EE = "🧮" # :abacus: + U1FA97 = "🪗" # :accordion: + U1FA79 = "🩹" # :adhesive_bandage: + U1F39FFE0F = "🎟️" # :admission_tickets: + U1F39F = "🎟" # :admission_tickets: + U1F6A1 = "🚡" # :aerial_tramway: + U2708FE0F = "✈️" # :airplane: + U2708 = "✈" # :airplane: + U1F6EC = "🛬" # :airplane_arrival: + U1F6EB = "🛫" # :airplane_departure: + U23F0 = "⏰" # :alarm_clock: + U2697FE0F = "⚗️" # :alembic: + U2697 = "⚗" # :alembic: + U1F47D = "👽" # :alien: + U1F47E = "👾" # :alien_monster: + U1F691 = "🚑" # :ambulance: + U1F3C8 = "🏈" # :american_football: + U1F3FA = "🏺" # :amphora: + U1FAC0 = "🫀" # :anatomical_heart: + U2693 = "⚓" # :anchor: + U1F4A2 = "💢" # :anger_symbol: + U1F620 = "😠" # :angry_face: + U1F47F = "👿" # :angry_face_with_horns: + U1F627 = "😧" # :anguished_face: + U1F41C = "🐜" # :ant: + U1F4F6 = "📶" # :antenna_bars: + U1F630 = "😰" # :anxious_face_with_sweat: + U1F69B = "🚛" # :articulated_lorry: + U1F9D1200D1F3A8 = "🧑‍🎨" # :artist: + U1F9D11F3FF200D1F3A8 = "🧑🏿‍🎨" # :artist_dark_skin_tone: + U1F9D11F3FB200D1F3A8 = "🧑🏻‍🎨" # :artist_light_skin_tone: + U1F9D11F3FE200D1F3A8 = "🧑🏾‍🎨" # :artist_medium-dark_skin_tone: + U1F9D11F3FC200D1F3A8 = "🧑🏼‍🎨" # :artist_medium-light_skin_tone: + U1F9D11F3FD200D1F3A8 = "🧑🏽‍🎨" # :artist_medium_skin_tone: + U1F3A8 = "🎨" # :artist_palette: + U1F632 = "😲" # :astonished_face: + U1F9D1200D1F680 = "🧑‍🚀" # :astronaut: + U1F9D11F3FF200D1F680 = "🧑🏿‍🚀" # :astronaut_dark_skin_tone: + U1F9D11F3FB200D1F680 = "🧑🏻‍🚀" # :astronaut_light_skin_tone: + U1F9D11F3FE200D1F680 = "🧑🏾‍🚀" # :astronaut_medium-dark_skin_tone: + U1F9D11F3FC200D1F680 = "🧑🏼‍🚀" # :astronaut_medium-light_skin_tone: + U1F9D11F3FD200D1F680 = "🧑🏽‍🚀" # :astronaut_medium_skin_tone: + U269BFE0F = "⚛️" # :atom_symbol: + U269B = "⚛" # :atom_symbol: + U1F6FA = "🛺" # :auto_rickshaw: + U1F697 = "🚗" # :automobile: + U1F951 = "🥑" # :avocado: + U1FA93 = "🪓" # :axe: + U1F476 = "👶" # :baby: + U1F47C = "👼" # :baby_angel: + U1F47C1F3FF = "👼🏿" # :baby_angel_dark_skin_tone: + U1F47C1F3FB = "👼🏻" # :baby_angel_light_skin_tone: + U1F47C1F3FE = "👼🏾" # :baby_angel_medium-dark_skin_tone: + U1F47C1F3FC = "👼🏼" # :baby_angel_medium-light_skin_tone: + U1F47C1F3FD = "👼🏽" # :baby_angel_medium_skin_tone: + U1F37C = "🍼" # :baby_bottle: + U1F424 = "🐤" # :baby_chick: + U1F4761F3FF = "👶🏿" # :baby_dark_skin_tone: + U1F4761F3FB = "👶🏻" # :baby_light_skin_tone: + U1F4761F3FE = "👶🏾" # :baby_medium-dark_skin_tone: + U1F4761F3FC = "👶🏼" # :baby_medium-light_skin_tone: + U1F4761F3FD = "👶🏽" # :baby_medium_skin_tone: + U1F6BC = "🚼" # :baby_symbol: + U1F447 = "👇" # :backhand_index_pointing_down: + U1F4471F3FF = "👇🏿" # :backhand_index_pointing_down_dark_skin_tone: + U1F4471F3FB = "👇🏻" # :backhand_index_pointing_down_light_skin_tone: + U1F4471F3FE = "👇🏾" # :backhand_index_pointing_down_medium-dark_skin_tone: + U1F4471F3FC = "👇🏼" # :backhand_index_pointing_down_medium-light_skin_tone: + U1F4471F3FD = "👇🏽" # :backhand_index_pointing_down_medium_skin_tone: + U1F448 = "👈" # :backhand_index_pointing_left: + U1F4481F3FF = "👈🏿" # :backhand_index_pointing_left_dark_skin_tone: + U1F4481F3FB = "👈🏻" # :backhand_index_pointing_left_light_skin_tone: + U1F4481F3FE = "👈🏾" # :backhand_index_pointing_left_medium-dark_skin_tone: + U1F4481F3FC = "👈🏼" # :backhand_index_pointing_left_medium-light_skin_tone: + U1F4481F3FD = "👈🏽" # :backhand_index_pointing_left_medium_skin_tone: + U1F449 = "👉" # :backhand_index_pointing_right: + U1F4491F3FF = "👉🏿" # :backhand_index_pointing_right_dark_skin_tone: + U1F4491F3FB = "👉🏻" # :backhand_index_pointing_right_light_skin_tone: + U1F4491F3FE = "👉🏾" # :backhand_index_pointing_right_medium-dark_skin_tone: + U1F4491F3FC = "👉🏼" # :backhand_index_pointing_right_medium-light_skin_tone: + U1F4491F3FD = "👉🏽" # :backhand_index_pointing_right_medium_skin_tone: + U1F446 = "👆" # :backhand_index_pointing_up: + U1F4461F3FF = "👆🏿" # :backhand_index_pointing_up_dark_skin_tone: + U1F4461F3FB = "👆🏻" # :backhand_index_pointing_up_light_skin_tone: + U1F4461F3FE = "👆🏾" # :backhand_index_pointing_up_medium-dark_skin_tone: + U1F4461F3FC = "👆🏼" # :backhand_index_pointing_up_medium-light_skin_tone: + U1F4461F3FD = "👆🏽" # :backhand_index_pointing_up_medium_skin_tone: + U1F392 = "🎒" # :backpack: + U1F953 = "🥓" # :bacon: + U1F9A1 = "🦡" # :badger: + U1F3F8 = "🏸" # :badminton: + U1F96F = "🥯" # :bagel: + U1F6C4 = "🛄" # :baggage_claim: + U1F956 = "🥖" # :baguette_bread: + U2696FE0F = "⚖️" # :balance_scale: + U2696 = "⚖" # :balance_scale: + U1F9B2 = "🦲" # :bald: + U1FA70 = "🩰" # :ballet_shoes: + U1F388 = "🎈" # :balloon: + U1F5F3FE0F = "🗳️" # :ballot_box_with_ballot: + U1F5F3 = "🗳" # :ballot_box_with_ballot: + U1F34C = "🍌" # :banana: + U1FA95 = "🪕" # :banjo: + U1F3E6 = "🏦" # :bank: + U1F4CA = "📊" # :bar_chart: + U1F488 = "💈" # :barber_pole: + U26BE = "⚾" # :baseball: + U1F9FA = "🧺" # :basket: + U1F3C0 = "🏀" # :basketball: + U1F987 = "🦇" # :bat: + U1F6C1 = "🛁" # :bathtub: + U1F50B = "🔋" # :battery: + U1F3D6FE0F = "🏖️" # :beach_with_umbrella: + U1F3D6 = "🏖" # :beach_with_umbrella: + U1F601 = "😁" # :beaming_face_with_smiling_eyes: + U1FAD8 = "🫘" # :beans: + U1F43B = "🐻" # :bear: + U1F493 = "💓" # :beating_heart: + U1F9AB = "🦫" # :beaver: + U1F6CFFE0F = "🛏️" # :bed: + U1F6CF = "🛏" # :bed: + U1F37A = "🍺" # :beer_mug: + U1FAB2 = "🪲" # :beetle: + U1F514 = "🔔" # :bell: + U1FAD1 = "🫑" # :bell_pepper: + U1F515 = "🔕" # :bell_with_slash: + U1F6CEFE0F = "🛎️" # :bellhop_bell: + U1F6CE = "🛎" # :bellhop_bell: + U1F371 = "🍱" # :bento_box: + U1F9C3 = "🧃" # :beverage_box: + U1F6B2 = "🚲" # :bicycle: + U1F459 = "👙" # :bikini: + U1F9E2 = "🧢" # :billed_cap: + U2623FE0F = "☣️" # :biohazard: + U2623 = "☣" # :biohazard: + U1F426 = "🐦" # :bird: + U1F382 = "🎂" # :birthday_cake: + U1F9AC = "🦬" # :bison: + U1FAE6 = "🫦" # :biting_lip: + U1F426200D2B1B = "🐦‍⬛" # :black_bird: + U1F408200D2B1B = "🐈‍⬛" # :black_cat: + U26AB = "⚫" # :black_circle: + U1F3F4 = "🏴" # :black_flag: + U1F5A4 = "🖤" # :black_heart: + U2B1B = "⬛" # :black_large_square: + U25FE = "◾" # :black_medium-small_square: + U25FCFE0F = "◼️" # :black_medium_square: + U25FC = "◼" # :black_medium_square: + U2712FE0F = "✒️" # :black_nib: + U2712 = "✒" # :black_nib: + U25AAFE0F = "▪️" # :black_small_square: + U25AA = "▪" # :black_small_square: + U1F532 = "🔲" # :black_square_button: + U1F33C = "🌼" # :blossom: + U1F421 = "🐡" # :blowfish: + U1F4D8 = "📘" # :blue_book: + U1F535 = "🔵" # :blue_circle: + U1F499 = "💙" # :blue_heart: + U1F7E6 = "🟦" # :blue_square: + U1FAD0 = "🫐" # :blueberries: + U1F417 = "🐗" # :boar: + U1F4A3 = "💣" # :bomb: + U1F9B4 = "🦴" # :bone: + U1F516 = "🔖" # :bookmark: + U1F4D1 = "📑" # :bookmark_tabs: + U1F4DA = "📚" # :books: + U1FA83 = "🪃" # :boomerang: + U1F37E = "🍾" # :bottle_with_popping_cork: + U1F490 = "💐" # :bouquet: + U1F3F9 = "🏹" # :bow_and_arrow: + U1F963 = "🥣" # :bowl_with_spoon: + U1F3B3 = "🎳" # :bowling: + U1F94A = "🥊" # :boxing_glove: + U1F466 = "👦" # :boy: + U1F4661F3FF = "👦🏿" # :boy_dark_skin_tone: + U1F4661F3FB = "👦🏻" # :boy_light_skin_tone: + U1F4661F3FE = "👦🏾" # :boy_medium-dark_skin_tone: + U1F4661F3FC = "👦🏼" # :boy_medium-light_skin_tone: + U1F4661F3FD = "👦🏽" # :boy_medium_skin_tone: + U1F9E0 = "🧠" # :brain: + U1F35E = "🍞" # :bread: + U1F931 = "🤱" # :breast-feeding: + U1F9311F3FF = "🤱🏿" # :breast-feeding_dark_skin_tone: + U1F9311F3FB = "🤱🏻" # :breast-feeding_light_skin_tone: + U1F9311F3FE = "🤱🏾" # :breast-feeding_medium-dark_skin_tone: + U1F9311F3FC = "🤱🏼" # :breast-feeding_medium-light_skin_tone: + U1F9311F3FD = "🤱🏽" # :breast-feeding_medium_skin_tone: + U1F9F1 = "🧱" # :brick: + U1F309 = "🌉" # :bridge_at_night: + U1F4BC = "💼" # :briefcase: + U1FA72 = "🩲" # :briefs: + U1F506 = "🔆" # :bright_button: + U1F966 = "🥦" # :broccoli: + U26D3FE0F200D1F4A5 = "⛓️‍💥" # :broken_chain: + U26D3200D1F4A5 = "⛓‍💥" # :broken_chain: + U1F494 = "💔" # :broken_heart: + U1F9F9 = "🧹" # :broom: + U1F7E4 = "🟤" # :brown_circle: + U1F90E = "🤎" # :brown_heart: + U1F344200D1F7EB = "🍄‍🟫" # :brown_mushroom: + U1F7EB = "🟫" # :brown_square: + U1F9CB = "🧋" # :bubble_tea: + U1FAE7 = "🫧" # :bubbles: + U1FAA3 = "🪣" # :bucket: + U1F41B = "🐛" # :bug: + U1F3D7FE0F = "🏗️" # :building_construction: + U1F3D7 = "🏗" # :building_construction: + U1F685 = "🚅" # :bullet_train: + U1F3AF = "🎯" # :bullseye: + U1F32F = "🌯" # :burrito: + U1F68C = "🚌" # :bus: + U1F68F = "🚏" # :bus_stop: + U1F464 = "👤" # :bust_in_silhouette: + U1F465 = "👥" # :busts_in_silhouette: + U1F9C8 = "🧈" # :butter: + U1F98B = "🦋" # :butterfly: + U1F335 = "🌵" # :cactus: + U1F4C5 = "📅" # :calendar: + U1F919 = "🤙" # :call_me_hand: + U1F9191F3FF = "🤙🏿" # :call_me_hand_dark_skin_tone: + U1F9191F3FB = "🤙🏻" # :call_me_hand_light_skin_tone: + U1F9191F3FE = "🤙🏾" # :call_me_hand_medium-dark_skin_tone: + U1F9191F3FC = "🤙🏼" # :call_me_hand_medium-light_skin_tone: + U1F9191F3FD = "🤙🏽" # :call_me_hand_medium_skin_tone: + U1F42A = "🐪" # :camel: + U1F4F7 = "📷" # :camera: + U1F4F8 = "📸" # :camera_with_flash: + U1F3D5FE0F = "🏕️" # :camping: + U1F3D5 = "🏕" # :camping: + U1F56FFE0F = "🕯️" # :candle: + U1F56F = "🕯" # :candle: + U1F36C = "🍬" # :candy: + U1F96B = "🥫" # :canned_food: + U1F6F6 = "🛶" # :canoe: + U1F5C3FE0F = "🗃️" # :card_file_box: + U1F5C3 = "🗃" # :card_file_box: + U1F4C7 = "📇" # :card_index: + U1F5C2FE0F = "🗂️" # :card_index_dividers: + U1F5C2 = "🗂" # :card_index_dividers: + U1F3A0 = "🎠" # :carousel_horse: + U1F38F = "🎏" # :carp_streamer: + U1FA9A = "🪚" # :carpentry_saw: + U1F955 = "🥕" # :carrot: + U1F3F0 = "🏰" # :castle: + U1F408 = "🐈" # :cat: + U1F431 = "🐱" # :cat_face: + U1F639 = "😹" # :cat_with_tears_of_joy: + U1F63C = "😼" # :cat_with_wry_smile: + U26D3FE0F = "⛓️" # :chains: + U26D3 = "⛓" # :chains: + U1FA91 = "🪑" # :chair: + U1F4C9 = "📉" # :chart_decreasing: + U1F4C8 = "📈" # :chart_increasing: + U1F4B9 = "💹" # :chart_increasing_with_yen: + U2611FE0F = "☑️" # :check_box_with_check: + U2611 = "☑" # :check_box_with_check: + U2714FE0F = "✔️" # :check_mark: + U2714 = "✔" # :check_mark: + U2705 = "✅" # :check_mark_button: + U1F9C0 = "🧀" # :cheese_wedge: + U1F3C1 = "🏁" # :chequered_flag: + U1F352 = "🍒" # :cherries: + U1F338 = "🌸" # :cherry_blossom: + U265FFE0F = "♟️" # :chess_pawn: + U265F = "♟" # :chess_pawn: + U1F330 = "🌰" # :chestnut: + U1F414 = "🐔" # :chicken: + U1F9D2 = "🧒" # :child: + U1F9D21F3FF = "🧒🏿" # :child_dark_skin_tone: + U1F9D21F3FB = "🧒🏻" # :child_light_skin_tone: + U1F9D21F3FE = "🧒🏾" # :child_medium-dark_skin_tone: + U1F9D21F3FC = "🧒🏼" # :child_medium-light_skin_tone: + U1F9D21F3FD = "🧒🏽" # :child_medium_skin_tone: + U1F6B8 = "🚸" # :children_crossing: + U1F43FFE0F = "🐿️" # :chipmunk: + U1F43F = "🐿" # :chipmunk: + U1F36B = "🍫" # :chocolate_bar: + U1F962 = "🥢" # :chopsticks: + U26EA = "⛪" # :church: + U1F6AC = "🚬" # :cigarette: + U1F3A6 = "🎦" # :cinema: + U24C2FE0F = "Ⓜ️" # :circled_M: + U24C2 = "Ⓜ" # :circled_M: + U1F3AA = "🎪" # :circus_tent: + U1F3D9FE0F = "🏙️" # :cityscape: + U1F3D9 = "🏙" # :cityscape: + U1F306 = "🌆" # :cityscape_at_dusk: + U1F5DCFE0F = "🗜️" # :clamp: + U1F5DC = "🗜" # :clamp: + U1F3AC = "🎬" # :clapper_board: + U1F44F = "👏" # :clapping_hands: + U1F44F1F3FF = "👏🏿" # :clapping_hands_dark_skin_tone: + U1F44F1F3FB = "👏🏻" # :clapping_hands_light_skin_tone: + U1F44F1F3FE = "👏🏾" # :clapping_hands_medium-dark_skin_tone: + U1F44F1F3FC = "👏🏼" # :clapping_hands_medium-light_skin_tone: + U1F44F1F3FD = "👏🏽" # :clapping_hands_medium_skin_tone: + U1F3DBFE0F = "🏛️" # :classical_building: + U1F3DB = "🏛" # :classical_building: + U1F37B = "🍻" # :clinking_beer_mugs: + U1F942 = "🥂" # :clinking_glasses: + U1F4CB = "📋" # :clipboard: + U1F503 = "🔃" # :clockwise_vertical_arrows: + U1F4D5 = "📕" # :closed_book: + U1F4EA = "📪" # :closed_mailbox_with_lowered_flag: + U1F4EB = "📫" # :closed_mailbox_with_raised_flag: + U1F302 = "🌂" # :closed_umbrella: + U2601FE0F = "☁️" # :cloud: + U2601 = "☁" # :cloud: + U1F329FE0F = "🌩️" # :cloud_with_lightning: + U1F329 = "🌩" # :cloud_with_lightning: + U26C8FE0F = "⛈️" # :cloud_with_lightning_and_rain: + U26C8 = "⛈" # :cloud_with_lightning_and_rain: + U1F327FE0F = "🌧️" # :cloud_with_rain: + U1F327 = "🌧" # :cloud_with_rain: + U1F328FE0F = "🌨️" # :cloud_with_snow: + U1F328 = "🌨" # :cloud_with_snow: + U1F921 = "🤡" # :clown_face: + U2663FE0F = "♣️" # :club_suit: + U2663 = "♣" # :club_suit: + U1F45D = "👝" # :clutch_bag: + U1F9E5 = "🧥" # :coat: + U1FAB3 = "🪳" # :cockroach: + U1F378 = "🍸" # :cocktail_glass: + U1F965 = "🥥" # :coconut: + U26B0FE0F = "⚰️" # :coffin: + U26B0 = "⚰" # :coffin: + U1FA99 = "🪙" # :coin: + U1F976 = "🥶" # :cold_face: + U1F4A5 = "💥" # :collision: + U2604FE0F = "☄️" # :comet: + U2604 = "☄" # :comet: + U1F9ED = "🧭" # :compass: + U1F4BD = "💽" # :computer_disk: + U1F5B1FE0F = "🖱️" # :computer_mouse: + U1F5B1 = "🖱" # :computer_mouse: + U1F38A = "🎊" # :confetti_ball: + U1F616 = "😖" # :confounded_face: + U1F615 = "😕" # :confused_face: + U1F6A7 = "🚧" # :construction: + U1F477 = "👷" # :construction_worker: + U1F4771F3FF = "👷🏿" # :construction_worker_dark_skin_tone: + U1F4771F3FB = "👷🏻" # :construction_worker_light_skin_tone: + U1F4771F3FE = "👷🏾" # :construction_worker_medium-dark_skin_tone: + U1F4771F3FC = "👷🏼" # :construction_worker_medium-light_skin_tone: + U1F4771F3FD = "👷🏽" # :construction_worker_medium_skin_tone: + U1F39BFE0F = "🎛️" # :control_knobs: + U1F39B = "🎛" # :control_knobs: + U1F3EA = "🏪" # :convenience_store: + U1F9D1200D1F373 = "🧑‍🍳" # :cook: + U1F9D11F3FF200D1F373 = "🧑🏿‍🍳" # :cook_dark_skin_tone: + U1F9D11F3FB200D1F373 = "🧑🏻‍🍳" # :cook_light_skin_tone: + U1F9D11F3FE200D1F373 = "🧑🏾‍🍳" # :cook_medium-dark_skin_tone: + U1F9D11F3FC200D1F373 = "🧑🏼‍🍳" # :cook_medium-light_skin_tone: + U1F9D11F3FD200D1F373 = "🧑🏽‍🍳" # :cook_medium_skin_tone: + U1F35A = "🍚" # :cooked_rice: + U1F36A = "🍪" # :cookie: + U1F373 = "🍳" # :cooking: + UA9FE0F = "©️" # :copyright: + UA9 = "©" # :copyright: + U1FAB8 = "🪸" # :coral: + U1F6CBFE0F = "🛋️" # :couch_and_lamp: + U1F6CB = "🛋" # :couch_and_lamp: + U1F504 = "🔄" # :counterclockwise_arrows_button: + U1F491 = "💑" # :couple_with_heart: + U1F4911F3FF = "💑🏿" # :couple_with_heart_dark_skin_tone: + U1F4911F3FB = "💑🏻" # :couple_with_heart_light_skin_tone: + U1F468200D2764FE0F200D1F468 = "👨‍❤️‍👨" # :couple_with_heart_man_man: + U1F468200D2764200D1F468 = "👨‍❤‍👨" # :couple_with_heart_man_man: + U1F4681F3FF200D2764FE0F200D1F4681F3FF = "👨🏿‍❤️‍👨🏿" # :couple_with_heart_man_man_dark_skin_tone: + U1F4681F3FF200D2764200D1F4681F3FF = "👨🏿‍❤‍👨🏿" # :couple_with_heart_man_man_dark_skin_tone: + U1F4681F3FF200D2764FE0F200D1F4681F3FB = "👨🏿‍❤️‍👨🏻" # :couple_with_heart_man_man_dark_skin_tone_light_skin_tone: + U1F4681F3FF200D2764200D1F4681F3FB = "👨🏿‍❤‍👨🏻" # :couple_with_heart_man_man_dark_skin_tone_light_skin_tone: + U1F4681F3FF200D2764FE0F200D1F4681F3FE = "👨🏿‍❤️‍👨🏾" # :couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone: + U1F4681F3FF200D2764200D1F4681F3FE = "👨🏿‍❤‍👨🏾" # :couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone: + U1F4681F3FF200D2764FE0F200D1F4681F3FC = "👨🏿‍❤️‍👨🏼" # :couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone: + U1F4681F3FF200D2764200D1F4681F3FC = "👨🏿‍❤‍👨🏼" # :couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone: + U1F4681F3FF200D2764FE0F200D1F4681F3FD = "👨🏿‍❤️‍👨🏽" # :couple_with_heart_man_man_dark_skin_tone_medium_skin_tone: + U1F4681F3FF200D2764200D1F4681F3FD = "👨🏿‍❤‍👨🏽" # :couple_with_heart_man_man_dark_skin_tone_medium_skin_tone: + U1F4681F3FB200D2764FE0F200D1F4681F3FB = "👨🏻‍❤️‍👨🏻" # :couple_with_heart_man_man_light_skin_tone: + U1F4681F3FB200D2764200D1F4681F3FB = "👨🏻‍❤‍👨🏻" # :couple_with_heart_man_man_light_skin_tone: + U1F4681F3FB200D2764FE0F200D1F4681F3FF = "👨🏻‍❤️‍👨🏿" # :couple_with_heart_man_man_light_skin_tone_dark_skin_tone: + U1F4681F3FB200D2764200D1F4681F3FF = "👨🏻‍❤‍👨🏿" # :couple_with_heart_man_man_light_skin_tone_dark_skin_tone: + U1F4681F3FB200D2764FE0F200D1F4681F3FE = "👨🏻‍❤️‍👨🏾" # :couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone: + U1F4681F3FB200D2764200D1F4681F3FE = "👨🏻‍❤‍👨🏾" # :couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone: + U1F4681F3FB200D2764FE0F200D1F4681F3FC = "👨🏻‍❤️‍👨🏼" # :couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone: + U1F4681F3FB200D2764200D1F4681F3FC = "👨🏻‍❤‍👨🏼" # :couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone: + U1F4681F3FB200D2764FE0F200D1F4681F3FD = "👨🏻‍❤️‍👨🏽" # :couple_with_heart_man_man_light_skin_tone_medium_skin_tone: + U1F4681F3FB200D2764200D1F4681F3FD = "👨🏻‍❤‍👨🏽" # :couple_with_heart_man_man_light_skin_tone_medium_skin_tone: + U1F4681F3FE200D2764FE0F200D1F4681F3FE = "👨🏾‍❤️‍👨🏾" # :couple_with_heart_man_man_medium-dark_skin_tone: + U1F4681F3FE200D2764200D1F4681F3FE = "👨🏾‍❤‍👨🏾" # :couple_with_heart_man_man_medium-dark_skin_tone: + U1F4681F3FE200D2764FE0F200D1F4681F3FF = "👨🏾‍❤️‍👨🏿" # :couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone: + U1F4681F3FE200D2764200D1F4681F3FF = "👨🏾‍❤‍👨🏿" # :couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone: + U1F4681F3FE200D2764FE0F200D1F4681F3FB = "👨🏾‍❤️‍👨🏻" # :couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone: + U1F4681F3FE200D2764200D1F4681F3FB = "👨🏾‍❤‍👨🏻" # :couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone: + U1F4681F3FE200D2764FE0F200D1F4681F3FC = "👨🏾‍❤️‍👨🏼" # :couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4681F3FE200D2764200D1F4681F3FC = "👨🏾‍❤‍👨🏼" # :couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4681F3FE200D2764FE0F200D1F4681F3FD = "👨🏾‍❤️‍👨🏽" # :couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone: + U1F4681F3FE200D2764200D1F4681F3FD = "👨🏾‍❤‍👨🏽" # :couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone: + U1F4681F3FC200D2764FE0F200D1F4681F3FC = "👨🏼‍❤️‍👨🏼" # :couple_with_heart_man_man_medium-light_skin_tone: + U1F4681F3FC200D2764200D1F4681F3FC = "👨🏼‍❤‍👨🏼" # :couple_with_heart_man_man_medium-light_skin_tone: + U1F4681F3FC200D2764FE0F200D1F4681F3FF = "👨🏼‍❤️‍👨🏿" # :couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone: + U1F4681F3FC200D2764200D1F4681F3FF = "👨🏼‍❤‍👨🏿" # :couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone: + U1F4681F3FC200D2764FE0F200D1F4681F3FB = "👨🏼‍❤️‍👨🏻" # :couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone: + U1F4681F3FC200D2764200D1F4681F3FB = "👨🏼‍❤‍👨🏻" # :couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone: + U1F4681F3FC200D2764FE0F200D1F4681F3FE = "👨🏼‍❤️‍👨🏾" # :couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4681F3FC200D2764200D1F4681F3FE = "👨🏼‍❤‍👨🏾" # :couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4681F3FC200D2764FE0F200D1F4681F3FD = "👨🏼‍❤️‍👨🏽" # :couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone: + U1F4681F3FC200D2764200D1F4681F3FD = "👨🏼‍❤‍👨🏽" # :couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone: + U1F4681F3FD200D2764FE0F200D1F4681F3FD = "👨🏽‍❤️‍👨🏽" # :couple_with_heart_man_man_medium_skin_tone: + U1F4681F3FD200D2764200D1F4681F3FD = "👨🏽‍❤‍👨🏽" # :couple_with_heart_man_man_medium_skin_tone: + U1F4681F3FD200D2764FE0F200D1F4681F3FF = "👨🏽‍❤️‍👨🏿" # :couple_with_heart_man_man_medium_skin_tone_dark_skin_tone: + U1F4681F3FD200D2764200D1F4681F3FF = "👨🏽‍❤‍👨🏿" # :couple_with_heart_man_man_medium_skin_tone_dark_skin_tone: + U1F4681F3FD200D2764FE0F200D1F4681F3FB = "👨🏽‍❤️‍👨🏻" # :couple_with_heart_man_man_medium_skin_tone_light_skin_tone: + U1F4681F3FD200D2764200D1F4681F3FB = "👨🏽‍❤‍👨🏻" # :couple_with_heart_man_man_medium_skin_tone_light_skin_tone: + U1F4681F3FD200D2764FE0F200D1F4681F3FE = "👨🏽‍❤️‍👨🏾" # :couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone: + U1F4681F3FD200D2764200D1F4681F3FE = "👨🏽‍❤‍👨🏾" # :couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone: + U1F4681F3FD200D2764FE0F200D1F4681F3FC = "👨🏽‍❤️‍👨🏼" # :couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone: + U1F4681F3FD200D2764200D1F4681F3FC = "👨🏽‍❤‍👨🏼" # :couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone: + U1F4911F3FE = "💑🏾" # :couple_with_heart_medium-dark_skin_tone: + U1F4911F3FC = "💑🏼" # :couple_with_heart_medium-light_skin_tone: + U1F4911F3FD = "💑🏽" # :couple_with_heart_medium_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F9D11F3FB = "🧑🏿‍❤️‍🧑🏻" # :couple_with_heart_person_person_dark_skin_tone_light_skin_tone: + U1F9D11F3FF200D2764200D1F9D11F3FB = "🧑🏿‍❤‍🧑🏻" # :couple_with_heart_person_person_dark_skin_tone_light_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F9D11F3FE = "🧑🏿‍❤️‍🧑🏾" # :couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone: + U1F9D11F3FF200D2764200D1F9D11F3FE = "🧑🏿‍❤‍🧑🏾" # :couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F9D11F3FC = "🧑🏿‍❤️‍🧑🏼" # :couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FF200D2764200D1F9D11F3FC = "🧑🏿‍❤‍🧑🏼" # :couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F9D11F3FD = "🧑🏿‍❤️‍🧑🏽" # :couple_with_heart_person_person_dark_skin_tone_medium_skin_tone: + U1F9D11F3FF200D2764200D1F9D11F3FD = "🧑🏿‍❤‍🧑🏽" # :couple_with_heart_person_person_dark_skin_tone_medium_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F9D11F3FF = "🧑🏻‍❤️‍🧑🏿" # :couple_with_heart_person_person_light_skin_tone_dark_skin_tone: + U1F9D11F3FB200D2764200D1F9D11F3FF = "🧑🏻‍❤‍🧑🏿" # :couple_with_heart_person_person_light_skin_tone_dark_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F9D11F3FE = "🧑🏻‍❤️‍🧑🏾" # :couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FB200D2764200D1F9D11F3FE = "🧑🏻‍❤‍🧑🏾" # :couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F9D11F3FC = "🧑🏻‍❤️‍🧑🏼" # :couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone: + U1F9D11F3FB200D2764200D1F9D11F3FC = "🧑🏻‍❤‍🧑🏼" # :couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F9D11F3FD = "🧑🏻‍❤️‍🧑🏽" # :couple_with_heart_person_person_light_skin_tone_medium_skin_tone: + U1F9D11F3FB200D2764200D1F9D11F3FD = "🧑🏻‍❤‍🧑🏽" # :couple_with_heart_person_person_light_skin_tone_medium_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F9D11F3FF = "🧑🏾‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone: + U1F9D11F3FE200D2764200D1F9D11F3FF = "🧑🏾‍❤‍🧑🏿" # :couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F9D11F3FB = "🧑🏾‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone: + U1F9D11F3FE200D2764200D1F9D11F3FB = "🧑🏾‍❤‍🧑🏻" # :couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F9D11F3FC = "🧑🏾‍❤️‍🧑🏼" # :couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FE200D2764200D1F9D11F3FC = "🧑🏾‍❤‍🧑🏼" # :couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F9D11F3FD = "🧑🏾‍❤️‍🧑🏽" # :couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone: + U1F9D11F3FE200D2764200D1F9D11F3FD = "🧑🏾‍❤‍🧑🏽" # :couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F9D11F3FF = "🧑🏼‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone: + U1F9D11F3FC200D2764200D1F9D11F3FF = "🧑🏼‍❤‍🧑🏿" # :couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F9D11F3FB = "🧑🏼‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone: + U1F9D11F3FC200D2764200D1F9D11F3FB = "🧑🏼‍❤‍🧑🏻" # :couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F9D11F3FE = "🧑🏼‍❤️‍🧑🏾" # :couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FC200D2764200D1F9D11F3FE = "🧑🏼‍❤‍🧑🏾" # :couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F9D11F3FD = "🧑🏼‍❤️‍🧑🏽" # :couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone: + U1F9D11F3FC200D2764200D1F9D11F3FD = "🧑🏼‍❤‍🧑🏽" # :couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F9D11F3FF = "🧑🏽‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium_skin_tone_dark_skin_tone: + U1F9D11F3FD200D2764200D1F9D11F3FF = "🧑🏽‍❤‍🧑🏿" # :couple_with_heart_person_person_medium_skin_tone_dark_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F9D11F3FB = "🧑🏽‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium_skin_tone_light_skin_tone: + U1F9D11F3FD200D2764200D1F9D11F3FB = "🧑🏽‍❤‍🧑🏻" # :couple_with_heart_person_person_medium_skin_tone_light_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F9D11F3FE = "🧑🏽‍❤️‍🧑🏾" # :couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone: + U1F9D11F3FD200D2764200D1F9D11F3FE = "🧑🏽‍❤‍🧑🏾" # :couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F9D11F3FC = "🧑🏽‍❤️‍🧑🏼" # :couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone: + U1F9D11F3FD200D2764200D1F9D11F3FC = "🧑🏽‍❤‍🧑🏼" # :couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone: + U1F469200D2764FE0F200D1F468 = "👩‍❤️‍👨" # :couple_with_heart_woman_man: + U1F469200D2764200D1F468 = "👩‍❤‍👨" # :couple_with_heart_woman_man: + U1F4691F3FF200D2764FE0F200D1F4681F3FF = "👩🏿‍❤️‍👨🏿" # :couple_with_heart_woman_man_dark_skin_tone: + U1F4691F3FF200D2764200D1F4681F3FF = "👩🏿‍❤‍👨🏿" # :couple_with_heart_woman_man_dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4681F3FB = "👩🏿‍❤️‍👨🏻" # :couple_with_heart_woman_man_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764200D1F4681F3FB = "👩🏿‍❤‍👨🏻" # :couple_with_heart_woman_man_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4681F3FE = "👩🏿‍❤️‍👨🏾" # :couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764200D1F4681F3FE = "👩🏿‍❤‍👨🏾" # :couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4681F3FC = "👩🏿‍❤️‍👨🏼" # :couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764200D1F4681F3FC = "👩🏿‍❤‍👨🏼" # :couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4681F3FD = "👩🏿‍❤️‍👨🏽" # :couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone: + U1F4691F3FF200D2764200D1F4681F3FD = "👩🏿‍❤‍👨🏽" # :couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4681F3FB = "👩🏻‍❤️‍👨🏻" # :couple_with_heart_woman_man_light_skin_tone: + U1F4691F3FB200D2764200D1F4681F3FB = "👩🏻‍❤‍👨🏻" # :couple_with_heart_woman_man_light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4681F3FF = "👩🏻‍❤️‍👨🏿" # :couple_with_heart_woman_man_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764200D1F4681F3FF = "👩🏻‍❤‍👨🏿" # :couple_with_heart_woman_man_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4681F3FE = "👩🏻‍❤️‍👨🏾" # :couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764200D1F4681F3FE = "👩🏻‍❤‍👨🏾" # :couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4681F3FC = "👩🏻‍❤️‍👨🏼" # :couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764200D1F4681F3FC = "👩🏻‍❤‍👨🏼" # :couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4681F3FD = "👩🏻‍❤️‍👨🏽" # :couple_with_heart_woman_man_light_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764200D1F4681F3FD = "👩🏻‍❤‍👨🏽" # :couple_with_heart_woman_man_light_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4681F3FE = "👩🏾‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium-dark_skin_tone: + U1F4691F3FE200D2764200D1F4681F3FE = "👩🏾‍❤‍👨🏾" # :couple_with_heart_woman_man_medium-dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4681F3FF = "👩🏾‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764200D1F4681F3FF = "👩🏾‍❤‍👨🏿" # :couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4681F3FB = "👩🏾‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764200D1F4681F3FB = "👩🏾‍❤‍👨🏻" # :couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4681F3FC = "👩🏾‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764200D1F4681F3FC = "👩🏾‍❤‍👨🏼" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4681F3FD = "👩🏾‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764200D1F4681F3FD = "👩🏾‍❤‍👨🏽" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4681F3FC = "👩🏼‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium-light_skin_tone: + U1F4691F3FC200D2764200D1F4681F3FC = "👩🏼‍❤‍👨🏼" # :couple_with_heart_woman_man_medium-light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4681F3FF = "👩🏼‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764200D1F4681F3FF = "👩🏼‍❤‍👨🏿" # :couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4681F3FB = "👩🏼‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764200D1F4681F3FB = "👩🏼‍❤‍👨🏻" # :couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4681F3FE = "👩🏼‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764200D1F4681F3FE = "👩🏼‍❤‍👨🏾" # :couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4681F3FD = "👩🏼‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764200D1F4681F3FD = "👩🏼‍❤‍👨🏽" # :couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4681F3FD = "👩🏽‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium_skin_tone: + U1F4691F3FD200D2764200D1F4681F3FD = "👩🏽‍❤‍👨🏽" # :couple_with_heart_woman_man_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4681F3FF = "👩🏽‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764200D1F4681F3FF = "👩🏽‍❤‍👨🏿" # :couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4681F3FB = "👩🏽‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764200D1F4681F3FB = "👩🏽‍❤‍👨🏻" # :couple_with_heart_woman_man_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4681F3FE = "👩🏽‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764200D1F4681F3FE = "👩🏽‍❤‍👨🏾" # :couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4681F3FC = "👩🏽‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone: + U1F4691F3FD200D2764200D1F4681F3FC = "👩🏽‍❤‍👨🏼" # :couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone: + U1F469200D2764FE0F200D1F469 = "👩‍❤️‍👩" # :couple_with_heart_woman_woman: + U1F469200D2764200D1F469 = "👩‍❤‍👩" # :couple_with_heart_woman_woman: + U1F4691F3FF200D2764FE0F200D1F4691F3FF = "👩🏿‍❤️‍👩🏿" # :couple_with_heart_woman_woman_dark_skin_tone: + U1F4691F3FF200D2764200D1F4691F3FF = "👩🏿‍❤‍👩🏿" # :couple_with_heart_woman_woman_dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4691F3FB = "👩🏿‍❤️‍👩🏻" # :couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764200D1F4691F3FB = "👩🏿‍❤‍👩🏻" # :couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4691F3FE = "👩🏿‍❤️‍👩🏾" # :couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764200D1F4691F3FE = "👩🏿‍❤‍👩🏾" # :couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4691F3FC = "👩🏿‍❤️‍👩🏼" # :couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764200D1F4691F3FC = "👩🏿‍❤‍👩🏼" # :couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F4691F3FD = "👩🏿‍❤️‍👩🏽" # :couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone: + U1F4691F3FF200D2764200D1F4691F3FD = "👩🏿‍❤‍👩🏽" # :couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4691F3FB = "👩🏻‍❤️‍👩🏻" # :couple_with_heart_woman_woman_light_skin_tone: + U1F4691F3FB200D2764200D1F4691F3FB = "👩🏻‍❤‍👩🏻" # :couple_with_heart_woman_woman_light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4691F3FF = "👩🏻‍❤️‍👩🏿" # :couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764200D1F4691F3FF = "👩🏻‍❤‍👩🏿" # :couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4691F3FE = "👩🏻‍❤️‍👩🏾" # :couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764200D1F4691F3FE = "👩🏻‍❤‍👩🏾" # :couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4691F3FC = "👩🏻‍❤️‍👩🏼" # :couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764200D1F4691F3FC = "👩🏻‍❤‍👩🏼" # :couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F4691F3FD = "👩🏻‍❤️‍👩🏽" # :couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764200D1F4691F3FD = "👩🏻‍❤‍👩🏽" # :couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4691F3FE = "👩🏾‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium-dark_skin_tone: + U1F4691F3FE200D2764200D1F4691F3FE = "👩🏾‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium-dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4691F3FF = "👩🏾‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764200D1F4691F3FF = "👩🏾‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4691F3FB = "👩🏾‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764200D1F4691F3FB = "👩🏾‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4691F3FC = "👩🏾‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764200D1F4691F3FC = "👩🏾‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F4691F3FD = "👩🏾‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764200D1F4691F3FD = "👩🏾‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4691F3FC = "👩🏼‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium-light_skin_tone: + U1F4691F3FC200D2764200D1F4691F3FC = "👩🏼‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium-light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4691F3FF = "👩🏼‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764200D1F4691F3FF = "👩🏼‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4691F3FB = "👩🏼‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764200D1F4691F3FB = "👩🏼‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4691F3FE = "👩🏼‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764200D1F4691F3FE = "👩🏼‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F4691F3FD = "👩🏼‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764200D1F4691F3FD = "👩🏼‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4691F3FD = "👩🏽‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium_skin_tone: + U1F4691F3FD200D2764200D1F4691F3FD = "👩🏽‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4691F3FF = "👩🏽‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764200D1F4691F3FF = "👩🏽‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4691F3FB = "👩🏽‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764200D1F4691F3FB = "👩🏽‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4691F3FE = "👩🏽‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764200D1F4691F3FE = "👩🏽‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F4691F3FC = "👩🏽‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone: + U1F4691F3FD200D2764200D1F4691F3FC = "👩🏽‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone: + U1F404 = "🐄" # :cow: + U1F42E = "🐮" # :cow_face: + U1F920 = "🤠" # :cowboy_hat_face: + U1F980 = "🦀" # :crab: + U1F58DFE0F = "🖍️" # :crayon: + U1F58D = "🖍" # :crayon: + U1F4B3 = "💳" # :credit_card: + U1F319 = "🌙" # :crescent_moon: + U1F997 = "🦗" # :cricket: + U1F3CF = "🏏" # :cricket_game: + U1F40A = "🐊" # :crocodile: + U1F950 = "🥐" # :croissant: + U274C = "❌" # :cross_mark: + U274E = "❎" # :cross_mark_button: + U1F91E = "🤞" # :crossed_fingers: + U1F91E1F3FF = "🤞🏿" # :crossed_fingers_dark_skin_tone: + U1F91E1F3FB = "🤞🏻" # :crossed_fingers_light_skin_tone: + U1F91E1F3FE = "🤞🏾" # :crossed_fingers_medium-dark_skin_tone: + U1F91E1F3FC = "🤞🏼" # :crossed_fingers_medium-light_skin_tone: + U1F91E1F3FD = "🤞🏽" # :crossed_fingers_medium_skin_tone: + U1F38C = "🎌" # :crossed_flags: + U2694FE0F = "⚔️" # :crossed_swords: + U2694 = "⚔" # :crossed_swords: + U1F451 = "👑" # :crown: + U1FA7C = "🩼" # :crutch: + U1F63F = "😿" # :crying_cat: + U1F622 = "😢" # :crying_face: + U1F52E = "🔮" # :crystal_ball: + U1F952 = "🥒" # :cucumber: + U1F964 = "🥤" # :cup_with_straw: + U1F9C1 = "🧁" # :cupcake: + U1F94C = "🥌" # :curling_stone: + U1F9B1 = "🦱" # :curly_hair: + U27B0 = "➰" # :curly_loop: + U1F4B1 = "💱" # :currency_exchange: + U1F35B = "🍛" # :curry_rice: + U1F36E = "🍮" # :custard: + U1F6C3 = "🛃" # :customs: + U1F969 = "🥩" # :cut_of_meat: + U1F300 = "🌀" # :cyclone: + U1F5E1FE0F = "🗡️" # :dagger: + U1F5E1 = "🗡" # :dagger: + U1F361 = "🍡" # :dango: + U1F3FF = "🏿" # :dark_skin_tone: + U1F4A8 = "💨" # :dashing_away: + U1F9CF200D2642FE0F = "🧏‍♂️" # :deaf_man: + U1F9CF200D2642 = "🧏‍♂" # :deaf_man: + U1F9CF1F3FF200D2642FE0F = "🧏🏿‍♂️" # :deaf_man_dark_skin_tone: + U1F9CF1F3FF200D2642 = "🧏🏿‍♂" # :deaf_man_dark_skin_tone: + U1F9CF1F3FB200D2642FE0F = "🧏🏻‍♂️" # :deaf_man_light_skin_tone: + U1F9CF1F3FB200D2642 = "🧏🏻‍♂" # :deaf_man_light_skin_tone: + U1F9CF1F3FE200D2642FE0F = "🧏🏾‍♂️" # :deaf_man_medium-dark_skin_tone: + U1F9CF1F3FE200D2642 = "🧏🏾‍♂" # :deaf_man_medium-dark_skin_tone: + U1F9CF1F3FC200D2642FE0F = "🧏🏼‍♂️" # :deaf_man_medium-light_skin_tone: + U1F9CF1F3FC200D2642 = "🧏🏼‍♂" # :deaf_man_medium-light_skin_tone: + U1F9CF1F3FD200D2642FE0F = "🧏🏽‍♂️" # :deaf_man_medium_skin_tone: + U1F9CF1F3FD200D2642 = "🧏🏽‍♂" # :deaf_man_medium_skin_tone: + U1F9CF = "🧏" # :deaf_person: + U1F9CF1F3FF = "🧏🏿" # :deaf_person_dark_skin_tone: + U1F9CF1F3FB = "🧏🏻" # :deaf_person_light_skin_tone: + U1F9CF1F3FE = "🧏🏾" # :deaf_person_medium-dark_skin_tone: + U1F9CF1F3FC = "🧏🏼" # :deaf_person_medium-light_skin_tone: + U1F9CF1F3FD = "🧏🏽" # :deaf_person_medium_skin_tone: + U1F9CF200D2640FE0F = "🧏‍♀️" # :deaf_woman: + U1F9CF200D2640 = "🧏‍♀" # :deaf_woman: + U1F9CF1F3FF200D2640FE0F = "🧏🏿‍♀️" # :deaf_woman_dark_skin_tone: + U1F9CF1F3FF200D2640 = "🧏🏿‍♀" # :deaf_woman_dark_skin_tone: + U1F9CF1F3FB200D2640FE0F = "🧏🏻‍♀️" # :deaf_woman_light_skin_tone: + U1F9CF1F3FB200D2640 = "🧏🏻‍♀" # :deaf_woman_light_skin_tone: + U1F9CF1F3FE200D2640FE0F = "🧏🏾‍♀️" # :deaf_woman_medium-dark_skin_tone: + U1F9CF1F3FE200D2640 = "🧏🏾‍♀" # :deaf_woman_medium-dark_skin_tone: + U1F9CF1F3FC200D2640FE0F = "🧏🏼‍♀️" # :deaf_woman_medium-light_skin_tone: + U1F9CF1F3FC200D2640 = "🧏🏼‍♀" # :deaf_woman_medium-light_skin_tone: + U1F9CF1F3FD200D2640FE0F = "🧏🏽‍♀️" # :deaf_woman_medium_skin_tone: + U1F9CF1F3FD200D2640 = "🧏🏽‍♀" # :deaf_woman_medium_skin_tone: + U1F333 = "🌳" # :deciduous_tree: + U1F98C = "🦌" # :deer: + U1F69A = "🚚" # :delivery_truck: + U1F3EC = "🏬" # :department_store: + U1F3DAFE0F = "🏚️" # :derelict_house: + U1F3DA = "🏚" # :derelict_house: + U1F3DCFE0F = "🏜️" # :desert: + U1F3DC = "🏜" # :desert: + U1F3DDFE0F = "🏝️" # :desert_island: + U1F3DD = "🏝" # :desert_island: + U1F5A5FE0F = "🖥️" # :desktop_computer: + U1F5A5 = "🖥" # :desktop_computer: + U1F575FE0F = "🕵️" # :detective: + U1F575 = "🕵" # :detective: + U1F5751F3FF = "🕵🏿" # :detective_dark_skin_tone: + U1F5751F3FB = "🕵🏻" # :detective_light_skin_tone: + U1F5751F3FE = "🕵🏾" # :detective_medium-dark_skin_tone: + U1F5751F3FC = "🕵🏼" # :detective_medium-light_skin_tone: + U1F5751F3FD = "🕵🏽" # :detective_medium_skin_tone: + U2666FE0F = "♦️" # :diamond_suit: + U2666 = "♦" # :diamond_suit: + U1F4A0 = "💠" # :diamond_with_a_dot: + U1F505 = "🔅" # :dim_button: + U1F61E = "😞" # :disappointed_face: + U1F978 = "🥸" # :disguised_face: + U2797 = "➗" # :divide: + U1F93F = "🤿" # :diving_mask: + U1FA94 = "🪔" # :diya_lamp: + U1F4AB = "💫" # :dizzy: + U1F9EC = "🧬" # :dna: + U1F9A4 = "🦤" # :dodo: + U1F415 = "🐕" # :dog: + U1F436 = "🐶" # :dog_face: + U1F4B5 = "💵" # :dollar_banknote: + U1F42C = "🐬" # :dolphin: + U1FACF = "🫏" # :donkey: + U1F6AA = "🚪" # :door: + U1FAE5 = "🫥" # :dotted_line_face: + U1F52F = "🔯" # :dotted_six-pointed_star: + U27BF = "➿" # :double_curly_loop: + U203CFE0F = "‼️" # :double_exclamation_mark: + U203C = "‼" # :double_exclamation_mark: + U1F369 = "🍩" # :doughnut: + U1F54AFE0F = "🕊️" # :dove: + U1F54A = "🕊" # :dove: + U2199FE0F = "↙️" # :down-left_arrow: + U2199 = "↙" # :down-left_arrow: + U2198FE0F = "↘️" # :down-right_arrow: + U2198 = "↘" # :down-right_arrow: + U2B07FE0F = "⬇️" # :down_arrow: + U2B07 = "⬇" # :down_arrow: + U1F613 = "😓" # :downcast_face_with_sweat: + U1F53D = "🔽" # :downwards_button: + U1F409 = "🐉" # :dragon: + U1F432 = "🐲" # :dragon_face: + U1F457 = "👗" # :dress: + U1F924 = "🤤" # :drooling_face: + U1FA78 = "🩸" # :drop_of_blood: + U1F4A7 = "💧" # :droplet: + U1F941 = "🥁" # :drum: + U1F986 = "🦆" # :duck: + U1F95F = "🥟" # :dumpling: + U1F4C0 = "📀" # :dvd: + U1F4E7 = "📧" # :e-mail: + U1F985 = "🦅" # :eagle: + U1F442 = "👂" # :ear: + U1F4421F3FF = "👂🏿" # :ear_dark_skin_tone: + U1F4421F3FB = "👂🏻" # :ear_light_skin_tone: + U1F4421F3FE = "👂🏾" # :ear_medium-dark_skin_tone: + U1F4421F3FC = "👂🏼" # :ear_medium-light_skin_tone: + U1F4421F3FD = "👂🏽" # :ear_medium_skin_tone: + U1F33D = "🌽" # :ear_of_corn: + U1F9BB = "🦻" # :ear_with_hearing_aid: + U1F9BB1F3FF = "🦻🏿" # :ear_with_hearing_aid_dark_skin_tone: + U1F9BB1F3FB = "🦻🏻" # :ear_with_hearing_aid_light_skin_tone: + U1F9BB1F3FE = "🦻🏾" # :ear_with_hearing_aid_medium-dark_skin_tone: + U1F9BB1F3FC = "🦻🏼" # :ear_with_hearing_aid_medium-light_skin_tone: + U1F9BB1F3FD = "🦻🏽" # :ear_with_hearing_aid_medium_skin_tone: + U1F95A = "🥚" # :egg: + U1F346 = "🍆" # :eggplant: + U2734FE0F = "✴️" # :eight-pointed_star: + U2734 = "✴" # :eight-pointed_star: + U2733FE0F = "✳️" # :eight-spoked_asterisk: + U2733 = "✳" # :eight-spoked_asterisk: + U1F563 = "🕣" # :eight-thirty: + U1F557 = "🕗" # :eight_o’clock: + U23CFFE0F = "⏏️" # :eject_button: + U23CF = "⏏" # :eject_button: + U1F50C = "🔌" # :electric_plug: + U1F418 = "🐘" # :elephant: + U1F6D7 = "🛗" # :elevator: + U1F566 = "🕦" # :eleven-thirty: + U1F55A = "🕚" # :eleven_o’clock: + U1F9DD = "🧝" # :elf: + U1F9DD1F3FF = "🧝🏿" # :elf_dark_skin_tone: + U1F9DD1F3FB = "🧝🏻" # :elf_light_skin_tone: + U1F9DD1F3FE = "🧝🏾" # :elf_medium-dark_skin_tone: + U1F9DD1F3FC = "🧝🏼" # :elf_medium-light_skin_tone: + U1F9DD1F3FD = "🧝🏽" # :elf_medium_skin_tone: + U1FAB9 = "🪹" # :empty_nest: + U1F621 = "😡" # :enraged_face: + U2709FE0F = "✉️" # :envelope: + U2709 = "✉" # :envelope: + U1F4E9 = "📩" # :envelope_with_arrow: + U1F4B6 = "💶" # :euro_banknote: + U1F332 = "🌲" # :evergreen_tree: + U1F411 = "🐑" # :ewe: + U2049FE0F = "⁉️" # :exclamation_question_mark: + U2049 = "⁉" # :exclamation_question_mark: + U1F92F = "🤯" # :exploding_head: + U1F611 = "😑" # :expressionless_face: + U1F441FE0F = "👁️" # :eye: + U1F441 = "👁" # :eye: + U1F441FE0F200D1F5E8FE0F = "👁️‍🗨️" # :eye_in_speech_bubble: + U1F441200D1F5E8FE0F = "👁‍🗨️" # :eye_in_speech_bubble: + U1F441FE0F200D1F5E8 = "👁️‍🗨" # :eye_in_speech_bubble: + U1F441200D1F5E8 = "👁‍🗨" # :eye_in_speech_bubble: + U1F440 = "👀" # :eyes: + U1F618 = "😘" # :face_blowing_a_kiss: + U1F62E200D1F4A8 = "😮‍💨" # :face_exhaling: + U1F979 = "🥹" # :face_holding_back_tears: + U1F636200D1F32BFE0F = "😶‍🌫️" # :face_in_clouds: + U1F636200D1F32B = "😶‍🌫" # :face_in_clouds: + U1F60B = "😋" # :face_savoring_food: + U1F631 = "😱" # :face_screaming_in_fear: + U1F92E = "🤮" # :face_vomiting: + U1F635 = "😵" # :face_with_crossed-out_eyes: + U1FAE4 = "🫤" # :face_with_diagonal_mouth: + U1F92D = "🤭" # :face_with_hand_over_mouth: + U1F915 = "🤕" # :face_with_head-bandage: + U1F637 = "😷" # :face_with_medical_mask: + U1F9D0 = "🧐" # :face_with_monocle: + U1FAE2 = "🫢" # :face_with_open_eyes_and_hand_over_mouth: + U1F62E = "😮" # :face_with_open_mouth: + U1FAE3 = "🫣" # :face_with_peeking_eye: + U1F928 = "🤨" # :face_with_raised_eyebrow: + U1F644 = "🙄" # :face_with_rolling_eyes: + U1F635200D1F4AB = "😵‍💫" # :face_with_spiral_eyes: + U1F624 = "😤" # :face_with_steam_from_nose: + U1F92C = "🤬" # :face_with_symbols_on_mouth: + U1F602 = "😂" # :face_with_tears_of_joy: + U1F912 = "🤒" # :face_with_thermometer: + U1F61B = "😛" # :face_with_tongue: + U1F636 = "😶" # :face_without_mouth: + U1F3ED = "🏭" # :factory: + U1F9D1200D1F3ED = "🧑‍🏭" # :factory_worker: + U1F9D11F3FF200D1F3ED = "🧑🏿‍🏭" # :factory_worker_dark_skin_tone: + U1F9D11F3FB200D1F3ED = "🧑🏻‍🏭" # :factory_worker_light_skin_tone: + U1F9D11F3FE200D1F3ED = "🧑🏾‍🏭" # :factory_worker_medium-dark_skin_tone: + U1F9D11F3FC200D1F3ED = "🧑🏼‍🏭" # :factory_worker_medium-light_skin_tone: + U1F9D11F3FD200D1F3ED = "🧑🏽‍🏭" # :factory_worker_medium_skin_tone: + U1F9DA = "🧚" # :fairy: + U1F9DA1F3FF = "🧚🏿" # :fairy_dark_skin_tone: + U1F9DA1F3FB = "🧚🏻" # :fairy_light_skin_tone: + U1F9DA1F3FE = "🧚🏾" # :fairy_medium-dark_skin_tone: + U1F9DA1F3FC = "🧚🏼" # :fairy_medium-light_skin_tone: + U1F9DA1F3FD = "🧚🏽" # :fairy_medium_skin_tone: + U1F9C6 = "🧆" # :falafel: + U1F342 = "🍂" # :fallen_leaf: + U1F46A = "👪" # :family: + U1F9D1200D1F9D1200D1F9D2 = "🧑‍🧑‍🧒" # :family_adult_adult_child: + U1F9D1200D1F9D1200D1F9D2200D1F9D2 = "🧑‍🧑‍🧒‍🧒" # :family_adult_adult_child_child: + U1F9D1200D1F9D2 = "🧑‍🧒" # :family_adult_child: + U1F9D1200D1F9D2200D1F9D2 = "🧑‍🧒‍🧒" # :family_adult_child_child: + U1F468200D1F466 = "👨‍👦" # :family_man_boy: + U1F468200D1F466200D1F466 = "👨‍👦‍👦" # :family_man_boy_boy: + U1F468200D1F467 = "👨‍👧" # :family_man_girl: + U1F468200D1F467200D1F466 = "👨‍👧‍👦" # :family_man_girl_boy: + U1F468200D1F467200D1F467 = "👨‍👧‍👧" # :family_man_girl_girl: + U1F468200D1F468200D1F466 = "👨‍👨‍👦" # :family_man_man_boy: + U1F468200D1F468200D1F466200D1F466 = "👨‍👨‍👦‍👦" # :family_man_man_boy_boy: + U1F468200D1F468200D1F467 = "👨‍👨‍👧" # :family_man_man_girl: + U1F468200D1F468200D1F467200D1F466 = "👨‍👨‍👧‍👦" # :family_man_man_girl_boy: + U1F468200D1F468200D1F467200D1F467 = "👨‍👨‍👧‍👧" # :family_man_man_girl_girl: + U1F468200D1F469200D1F466 = "👨‍👩‍👦" # :family_man_woman_boy: + U1F468200D1F469200D1F466200D1F466 = "👨‍👩‍👦‍👦" # :family_man_woman_boy_boy: + U1F468200D1F469200D1F467 = "👨‍👩‍👧" # :family_man_woman_girl: + U1F468200D1F469200D1F467200D1F466 = "👨‍👩‍👧‍👦" # :family_man_woman_girl_boy: + U1F468200D1F469200D1F467200D1F467 = "👨‍👩‍👧‍👧" # :family_man_woman_girl_girl: + U1F469200D1F466 = "👩‍👦" # :family_woman_boy: + U1F469200D1F466200D1F466 = "👩‍👦‍👦" # :family_woman_boy_boy: + U1F469200D1F467 = "👩‍👧" # :family_woman_girl: + U1F469200D1F467200D1F466 = "👩‍👧‍👦" # :family_woman_girl_boy: + U1F469200D1F467200D1F467 = "👩‍👧‍👧" # :family_woman_girl_girl: + U1F469200D1F469200D1F466 = "👩‍👩‍👦" # :family_woman_woman_boy: + U1F469200D1F469200D1F466200D1F466 = "👩‍👩‍👦‍👦" # :family_woman_woman_boy_boy: + U1F469200D1F469200D1F467 = "👩‍👩‍👧" # :family_woman_woman_girl: + U1F469200D1F469200D1F467200D1F466 = "👩‍👩‍👧‍👦" # :family_woman_woman_girl_boy: + U1F469200D1F469200D1F467200D1F467 = "👩‍👩‍👧‍👧" # :family_woman_woman_girl_girl: + U1F9D1200D1F33E = "🧑‍🌾" # :farmer: + U1F9D11F3FF200D1F33E = "🧑🏿‍🌾" # :farmer_dark_skin_tone: + U1F9D11F3FB200D1F33E = "🧑🏻‍🌾" # :farmer_light_skin_tone: + U1F9D11F3FE200D1F33E = "🧑🏾‍🌾" # :farmer_medium-dark_skin_tone: + U1F9D11F3FC200D1F33E = "🧑🏼‍🌾" # :farmer_medium-light_skin_tone: + U1F9D11F3FD200D1F33E = "🧑🏽‍🌾" # :farmer_medium_skin_tone: + U23E9 = "⏩" # :fast-forward_button: + U23EC = "⏬" # :fast_down_button: + U23EA = "⏪" # :fast_reverse_button: + U23EB = "⏫" # :fast_up_button: + U1F4E0 = "📠" # :fax_machine: + U1F628 = "😨" # :fearful_face: + U1FAB6 = "🪶" # :feather: + U2640FE0F = "♀️" # :female_sign: + U2640 = "♀" # :female_sign: + U1F3A1 = "🎡" # :ferris_wheel: + U26F4FE0F = "⛴️" # :ferry: + U26F4 = "⛴" # :ferry: + U1F3D1 = "🏑" # :field_hockey: + U1F5C4FE0F = "🗄️" # :file_cabinet: + U1F5C4 = "🗄" # :file_cabinet: + U1F4C1 = "📁" # :file_folder: + U1F39EFE0F = "🎞️" # :film_frames: + U1F39E = "🎞" # :film_frames: + U1F4FDFE0F = "📽️" # :film_projector: + U1F4FD = "📽" # :film_projector: + U1F525 = "🔥" # :fire: + U1F692 = "🚒" # :fire_engine: + U1F9EF = "🧯" # :fire_extinguisher: + U1F9E8 = "🧨" # :firecracker: + U1F9D1200D1F692 = "🧑‍🚒" # :firefighter: + U1F9D11F3FF200D1F692 = "🧑🏿‍🚒" # :firefighter_dark_skin_tone: + U1F9D11F3FB200D1F692 = "🧑🏻‍🚒" # :firefighter_light_skin_tone: + U1F9D11F3FE200D1F692 = "🧑🏾‍🚒" # :firefighter_medium-dark_skin_tone: + U1F9D11F3FC200D1F692 = "🧑🏼‍🚒" # :firefighter_medium-light_skin_tone: + U1F9D11F3FD200D1F692 = "🧑🏽‍🚒" # :firefighter_medium_skin_tone: + U1F386 = "🎆" # :fireworks: + U1F313 = "🌓" # :first_quarter_moon: + U1F31B = "🌛" # :first_quarter_moon_face: + U1F41F = "🐟" # :fish: + U1F365 = "🍥" # :fish_cake_with_swirl: + U1F3A3 = "🎣" # :fishing_pole: + U1F560 = "🕠" # :five-thirty: + U1F554 = "🕔" # :five_o’clock: + U26F3 = "⛳" # :flag_in_hole: + U1F9A9 = "🦩" # :flamingo: + U1F526 = "🔦" # :flashlight: + U1F97F = "🥿" # :flat_shoe: + U1FAD3 = "🫓" # :flatbread: + U269CFE0F = "⚜️" # :fleur-de-lis: + U269C = "⚜" # :fleur-de-lis: + U1F4AA = "💪" # :flexed_biceps: + U1F4AA1F3FF = "💪🏿" # :flexed_biceps_dark_skin_tone: + U1F4AA1F3FB = "💪🏻" # :flexed_biceps_light_skin_tone: + U1F4AA1F3FE = "💪🏾" # :flexed_biceps_medium-dark_skin_tone: + U1F4AA1F3FC = "💪🏼" # :flexed_biceps_medium-light_skin_tone: + U1F4AA1F3FD = "💪🏽" # :flexed_biceps_medium_skin_tone: + U1F4BE = "💾" # :floppy_disk: + U1F3B4 = "🎴" # :flower_playing_cards: + U1F633 = "😳" # :flushed_face: + U1FA88 = "🪈" # :flute: + U1FAB0 = "🪰" # :fly: + U1F94F = "🥏" # :flying_disc: + U1F6F8 = "🛸" # :flying_saucer: + U1F32BFE0F = "🌫️" # :fog: + U1F32B = "🌫" # :fog: + U1F301 = "🌁" # :foggy: + U1F64F = "🙏" # :folded_hands: + U1F64F1F3FF = "🙏🏿" # :folded_hands_dark_skin_tone: + U1F64F1F3FB = "🙏🏻" # :folded_hands_light_skin_tone: + U1F64F1F3FE = "🙏🏾" # :folded_hands_medium-dark_skin_tone: + U1F64F1F3FC = "🙏🏼" # :folded_hands_medium-light_skin_tone: + U1F64F1F3FD = "🙏🏽" # :folded_hands_medium_skin_tone: + U1FAAD = "🪭" # :folding_hand_fan: + U1FAD5 = "🫕" # :fondue: + U1F9B6 = "🦶" # :foot: + U1F9B61F3FF = "🦶🏿" # :foot_dark_skin_tone: + U1F9B61F3FB = "🦶🏻" # :foot_light_skin_tone: + U1F9B61F3FE = "🦶🏾" # :foot_medium-dark_skin_tone: + U1F9B61F3FC = "🦶🏼" # :foot_medium-light_skin_tone: + U1F9B61F3FD = "🦶🏽" # :foot_medium_skin_tone: + U1F463 = "👣" # :footprints: + U1F374 = "🍴" # :fork_and_knife: + U1F37DFE0F = "🍽️" # :fork_and_knife_with_plate: + U1F37D = "🍽" # :fork_and_knife_with_plate: + U1F960 = "🥠" # :fortune_cookie: + U26F2 = "⛲" # :fountain: + U1F58BFE0F = "🖋️" # :fountain_pen: + U1F58B = "🖋" # :fountain_pen: + U1F55F = "🕟" # :four-thirty: + U1F340 = "🍀" # :four_leaf_clover: + U1F553 = "🕓" # :four_o’clock: + U1F98A = "🦊" # :fox: + U1F5BCFE0F = "🖼️" # :framed_picture: + U1F5BC = "🖼" # :framed_picture: + U1F35F = "🍟" # :french_fries: + U1F364 = "🍤" # :fried_shrimp: + U1F438 = "🐸" # :frog: + U1F425 = "🐥" # :front-facing_baby_chick: + U2639FE0F = "☹️" # :frowning_face: + U2639 = "☹" # :frowning_face: + U1F626 = "😦" # :frowning_face_with_open_mouth: + U26FD = "⛽" # :fuel_pump: + U1F315 = "🌕" # :full_moon: + U1F31D = "🌝" # :full_moon_face: + U26B1FE0F = "⚱️" # :funeral_urn: + U26B1 = "⚱" # :funeral_urn: + U1F3B2 = "🎲" # :game_die: + U1F9C4 = "🧄" # :garlic: + U2699FE0F = "⚙️" # :gear: + U2699 = "⚙" # :gear: + U1F48E = "💎" # :gem_stone: + U1F9DE = "🧞" # :genie: + U1F47B = "👻" # :ghost: + U1FADA = "🫚" # :ginger_root: + U1F992 = "🦒" # :giraffe: + U1F467 = "👧" # :girl: + U1F4671F3FF = "👧🏿" # :girl_dark_skin_tone: + U1F4671F3FB = "👧🏻" # :girl_light_skin_tone: + U1F4671F3FE = "👧🏾" # :girl_medium-dark_skin_tone: + U1F4671F3FC = "👧🏼" # :girl_medium-light_skin_tone: + U1F4671F3FD = "👧🏽" # :girl_medium_skin_tone: + U1F95B = "🥛" # :glass_of_milk: + U1F453 = "👓" # :glasses: + U1F30E = "🌎" # :globe_showing_Americas: + U1F30F = "🌏" # :globe_showing_Asia-Australia: + U1F30D = "🌍" # :globe_showing_Europe-Africa: + U1F310 = "🌐" # :globe_with_meridians: + U1F9E4 = "🧤" # :gloves: + U1F31F = "🌟" # :glowing_star: + U1F945 = "🥅" # :goal_net: + U1F410 = "🐐" # :goat: + U1F47A = "👺" # :goblin: + U1F97D = "🥽" # :goggles: + U1FABF = "🪿" # :goose: + U1F98D = "🦍" # :gorilla: + U1F393 = "🎓" # :graduation_cap: + U1F347 = "🍇" # :grapes: + U1F34F = "🍏" # :green_apple: + U1F4D7 = "📗" # :green_book: + U1F7E2 = "🟢" # :green_circle: + U1F49A = "💚" # :green_heart: + U1F957 = "🥗" # :green_salad: + U1F7E9 = "🟩" # :green_square: + U1FA76 = "🩶" # :grey_heart: + U1F62C = "😬" # :grimacing_face: + U1F63A = "😺" # :grinning_cat: + U1F638 = "😸" # :grinning_cat_with_smiling_eyes: + U1F600 = "😀" # :grinning_face: + U1F603 = "😃" # :grinning_face_with_big_eyes: + U1F604 = "😄" # :grinning_face_with_smiling_eyes: + U1F605 = "😅" # :grinning_face_with_sweat: + U1F606 = "😆" # :grinning_squinting_face: + U1F497 = "💗" # :growing_heart: + U1F482 = "💂" # :guard: + U1F4821F3FF = "💂🏿" # :guard_dark_skin_tone: + U1F4821F3FB = "💂🏻" # :guard_light_skin_tone: + U1F4821F3FE = "💂🏾" # :guard_medium-dark_skin_tone: + U1F4821F3FC = "💂🏼" # :guard_medium-light_skin_tone: + U1F4821F3FD = "💂🏽" # :guard_medium_skin_tone: + U1F9AE = "🦮" # :guide_dog: + U1F3B8 = "🎸" # :guitar: + U1FAAE = "🪮" # :hair_pick: + U1F354 = "🍔" # :hamburger: + U1F528 = "🔨" # :hammer: + U2692FE0F = "⚒️" # :hammer_and_pick: + U2692 = "⚒" # :hammer_and_pick: + U1F6E0FE0F = "🛠️" # :hammer_and_wrench: + U1F6E0 = "🛠" # :hammer_and_wrench: + U1FAAC = "🪬" # :hamsa: + U1F439 = "🐹" # :hamster: + U1F590FE0F = "🖐️" # :hand_with_fingers_splayed: + U1F590 = "🖐" # :hand_with_fingers_splayed: + U1F5901F3FF = "🖐🏿" # :hand_with_fingers_splayed_dark_skin_tone: + U1F5901F3FB = "🖐🏻" # :hand_with_fingers_splayed_light_skin_tone: + U1F5901F3FE = "🖐🏾" # :hand_with_fingers_splayed_medium-dark_skin_tone: + U1F5901F3FC = "🖐🏼" # :hand_with_fingers_splayed_medium-light_skin_tone: + U1F5901F3FD = "🖐🏽" # :hand_with_fingers_splayed_medium_skin_tone: + U1FAF0 = "🫰" # :hand_with_index_finger_and_thumb_crossed: + U1FAF01F3FF = "🫰🏿" # :hand_with_index_finger_and_thumb_crossed_dark_skin_tone: + U1FAF01F3FB = "🫰🏻" # :hand_with_index_finger_and_thumb_crossed_light_skin_tone: + U1FAF01F3FE = "🫰🏾" # :hand_with_index_finger_and_thumb_crossed_medium-dark_skin_tone: + U1FAF01F3FC = "🫰🏼" # :hand_with_index_finger_and_thumb_crossed_medium-light_skin_tone: + U1FAF01F3FD = "🫰🏽" # :hand_with_index_finger_and_thumb_crossed_medium_skin_tone: + U1F45C = "👜" # :handbag: + U1F91D = "🤝" # :handshake: + U1F91D1F3FF = "🤝🏿" # :handshake_dark_skin_tone: + U1FAF11F3FF200D1FAF21F3FB = "🫱🏿‍🫲🏻" # :handshake_dark_skin_tone_light_skin_tone: + U1FAF11F3FF200D1FAF21F3FE = "🫱🏿‍🫲🏾" # :handshake_dark_skin_tone_medium-dark_skin_tone: + U1FAF11F3FF200D1FAF21F3FC = "🫱🏿‍🫲🏼" # :handshake_dark_skin_tone_medium-light_skin_tone: + U1FAF11F3FF200D1FAF21F3FD = "🫱🏿‍🫲🏽" # :handshake_dark_skin_tone_medium_skin_tone: + U1F91D1F3FB = "🤝🏻" # :handshake_light_skin_tone: + U1FAF11F3FB200D1FAF21F3FF = "🫱🏻‍🫲🏿" # :handshake_light_skin_tone_dark_skin_tone: + U1FAF11F3FB200D1FAF21F3FE = "🫱🏻‍🫲🏾" # :handshake_light_skin_tone_medium-dark_skin_tone: + U1FAF11F3FB200D1FAF21F3FC = "🫱🏻‍🫲🏼" # :handshake_light_skin_tone_medium-light_skin_tone: + U1FAF11F3FB200D1FAF21F3FD = "🫱🏻‍🫲🏽" # :handshake_light_skin_tone_medium_skin_tone: + U1F91D1F3FE = "🤝🏾" # :handshake_medium-dark_skin_tone: + U1FAF11F3FE200D1FAF21F3FF = "🫱🏾‍🫲🏿" # :handshake_medium-dark_skin_tone_dark_skin_tone: + U1FAF11F3FE200D1FAF21F3FB = "🫱🏾‍🫲🏻" # :handshake_medium-dark_skin_tone_light_skin_tone: + U1FAF11F3FE200D1FAF21F3FC = "🫱🏾‍🫲🏼" # :handshake_medium-dark_skin_tone_medium-light_skin_tone: + U1FAF11F3FE200D1FAF21F3FD = "🫱🏾‍🫲🏽" # :handshake_medium-dark_skin_tone_medium_skin_tone: + U1F91D1F3FC = "🤝🏼" # :handshake_medium-light_skin_tone: + U1FAF11F3FC200D1FAF21F3FF = "🫱🏼‍🫲🏿" # :handshake_medium-light_skin_tone_dark_skin_tone: + U1FAF11F3FC200D1FAF21F3FB = "🫱🏼‍🫲🏻" # :handshake_medium-light_skin_tone_light_skin_tone: + U1FAF11F3FC200D1FAF21F3FE = "🫱🏼‍🫲🏾" # :handshake_medium-light_skin_tone_medium-dark_skin_tone: + U1FAF11F3FC200D1FAF21F3FD = "🫱🏼‍🫲🏽" # :handshake_medium-light_skin_tone_medium_skin_tone: + U1F91D1F3FD = "🤝🏽" # :handshake_medium_skin_tone: + U1FAF11F3FD200D1FAF21F3FF = "🫱🏽‍🫲🏿" # :handshake_medium_skin_tone_dark_skin_tone: + U1FAF11F3FD200D1FAF21F3FB = "🫱🏽‍🫲🏻" # :handshake_medium_skin_tone_light_skin_tone: + U1FAF11F3FD200D1FAF21F3FE = "🫱🏽‍🫲🏾" # :handshake_medium_skin_tone_medium-dark_skin_tone: + U1FAF11F3FD200D1FAF21F3FC = "🫱🏽‍🫲🏼" # :handshake_medium_skin_tone_medium-light_skin_tone: + U1F423 = "🐣" # :hatching_chick: + U1F642200D2194FE0F = "🙂‍↔️" # :head_shaking_horizontally: + U1F642200D2194 = "🙂‍↔" # :head_shaking_horizontally: + U1F642200D2195FE0F = "🙂‍↕️" # :head_shaking_vertically: + U1F642200D2195 = "🙂‍↕" # :head_shaking_vertically: + U1F3A7 = "🎧" # :headphone: + U1FAA6 = "🪦" # :headstone: + U1F9D1200D2695FE0F = "🧑‍⚕️" # :health_worker: + U1F9D1200D2695 = "🧑‍⚕" # :health_worker: + U1F9D11F3FF200D2695FE0F = "🧑🏿‍⚕️" # :health_worker_dark_skin_tone: + U1F9D11F3FF200D2695 = "🧑🏿‍⚕" # :health_worker_dark_skin_tone: + U1F9D11F3FB200D2695FE0F = "🧑🏻‍⚕️" # :health_worker_light_skin_tone: + U1F9D11F3FB200D2695 = "🧑🏻‍⚕" # :health_worker_light_skin_tone: + U1F9D11F3FE200D2695FE0F = "🧑🏾‍⚕️" # :health_worker_medium-dark_skin_tone: + U1F9D11F3FE200D2695 = "🧑🏾‍⚕" # :health_worker_medium-dark_skin_tone: + U1F9D11F3FC200D2695FE0F = "🧑🏼‍⚕️" # :health_worker_medium-light_skin_tone: + U1F9D11F3FC200D2695 = "🧑🏼‍⚕" # :health_worker_medium-light_skin_tone: + U1F9D11F3FD200D2695FE0F = "🧑🏽‍⚕️" # :health_worker_medium_skin_tone: + U1F9D11F3FD200D2695 = "🧑🏽‍⚕" # :health_worker_medium_skin_tone: + U1F649 = "🙉" # :hear-no-evil_monkey: + U1F49F = "💟" # :heart_decoration: + U2763FE0F = "❣️" # :heart_exclamation: + U2763 = "❣" # :heart_exclamation: + U1FAF6 = "🫶" # :heart_hands: + U1FAF61F3FF = "🫶🏿" # :heart_hands_dark_skin_tone: + U1FAF61F3FB = "🫶🏻" # :heart_hands_light_skin_tone: + U1FAF61F3FE = "🫶🏾" # :heart_hands_medium-dark_skin_tone: + U1FAF61F3FC = "🫶🏼" # :heart_hands_medium-light_skin_tone: + U1FAF61F3FD = "🫶🏽" # :heart_hands_medium_skin_tone: + U2764FE0F200D1F525 = "❤️‍🔥" # :heart_on_fire: + U2764200D1F525 = "❤‍🔥" # :heart_on_fire: + U2665FE0F = "♥️" # :heart_suit: + U2665 = "♥" # :heart_suit: + U1F498 = "💘" # :heart_with_arrow: + U1F49D = "💝" # :heart_with_ribbon: + U1F4B2 = "💲" # :heavy_dollar_sign: + U1F7F0 = "🟰" # :heavy_equals_sign: + U1F994 = "🦔" # :hedgehog: + U1F681 = "🚁" # :helicopter: + U1F33F = "🌿" # :herb: + U1F33A = "🌺" # :hibiscus: + U1F460 = "👠" # :high-heeled_shoe: + U1F684 = "🚄" # :high-speed_train: + U26A1 = "⚡" # :high_voltage: + U1F97E = "🥾" # :hiking_boot: + U1F6D5 = "🛕" # :hindu_temple: + U1F99B = "🦛" # :hippopotamus: + U1F573FE0F = "🕳️" # :hole: + U1F573 = "🕳" # :hole: + U2B55 = "⭕" # :hollow_red_circle: + U1F36F = "🍯" # :honey_pot: + U1F41D = "🐝" # :honeybee: + U1FA9D = "🪝" # :hook: + U1F6A5 = "🚥" # :horizontal_traffic_light: + U1F40E = "🐎" # :horse: + U1F434 = "🐴" # :horse_face: + U1F3C7 = "🏇" # :horse_racing: + U1F3C71F3FF = "🏇🏿" # :horse_racing_dark_skin_tone: + U1F3C71F3FB = "🏇🏻" # :horse_racing_light_skin_tone: + U1F3C71F3FE = "🏇🏾" # :horse_racing_medium-dark_skin_tone: + U1F3C71F3FC = "🏇🏼" # :horse_racing_medium-light_skin_tone: + U1F3C71F3FD = "🏇🏽" # :horse_racing_medium_skin_tone: + U1F3E5 = "🏥" # :hospital: + U2615 = "☕" # :hot_beverage: + U1F32D = "🌭" # :hot_dog: + U1F975 = "🥵" # :hot_face: + U1F336FE0F = "🌶️" # :hot_pepper: + U1F336 = "🌶" # :hot_pepper: + U2668FE0F = "♨️" # :hot_springs: + U2668 = "♨" # :hot_springs: + U1F3E8 = "🏨" # :hotel: + U231B = "⌛" # :hourglass_done: + U23F3 = "⏳" # :hourglass_not_done: + U1F3E0 = "🏠" # :house: + U1F3E1 = "🏡" # :house_with_garden: + U1F3D8FE0F = "🏘️" # :houses: + U1F3D8 = "🏘" # :houses: + U1F4AF = "💯" # :hundred_points: + U1F62F = "😯" # :hushed_face: + U1F6D6 = "🛖" # :hut: + U1FABB = "🪻" # :hyacinth: + U1F9CA = "🧊" # :ice: + U1F368 = "🍨" # :ice_cream: + U1F3D2 = "🏒" # :ice_hockey: + U26F8FE0F = "⛸️" # :ice_skate: + U26F8 = "⛸" # :ice_skate: + U1FAAA = "🪪" # :identification_card: + U1F4E5 = "📥" # :inbox_tray: + U1F4E8 = "📨" # :incoming_envelope: + U1FAF5 = "🫵" # :index_pointing_at_the_viewer: + U1FAF51F3FF = "🫵🏿" # :index_pointing_at_the_viewer_dark_skin_tone: + U1FAF51F3FB = "🫵🏻" # :index_pointing_at_the_viewer_light_skin_tone: + U1FAF51F3FE = "🫵🏾" # :index_pointing_at_the_viewer_medium-dark_skin_tone: + U1FAF51F3FC = "🫵🏼" # :index_pointing_at_the_viewer_medium-light_skin_tone: + U1FAF51F3FD = "🫵🏽" # :index_pointing_at_the_viewer_medium_skin_tone: + U261DFE0F = "☝️" # :index_pointing_up: + U261D = "☝" # :index_pointing_up: + U261D1F3FF = "☝🏿" # :index_pointing_up_dark_skin_tone: + U261D1F3FB = "☝🏻" # :index_pointing_up_light_skin_tone: + U261D1F3FE = "☝🏾" # :index_pointing_up_medium-dark_skin_tone: + U261D1F3FC = "☝🏼" # :index_pointing_up_medium-light_skin_tone: + U261D1F3FD = "☝🏽" # :index_pointing_up_medium_skin_tone: + U267EFE0F = "♾️" # :infinity: + U267E = "♾" # :infinity: + U2139FE0F = "ℹ️" # :information: + U2139 = "ℹ" # :information: + U1F524 = "🔤" # :input_latin_letters: + U1F521 = "🔡" # :input_latin_lowercase: + U1F520 = "🔠" # :input_latin_uppercase: + U1F522 = "🔢" # :input_numbers: + U1F523 = "🔣" # :input_symbols: + U1F383 = "🎃" # :jack-o-lantern: + U1FAD9 = "🫙" # :jar: + U1F456 = "👖" # :jeans: + U1FABC = "🪼" # :jellyfish: + U1F0CF = "🃏" # :joker: + U1F579FE0F = "🕹️" # :joystick: + U1F579 = "🕹" # :joystick: + U1F9D1200D2696FE0F = "🧑‍⚖️" # :judge: + U1F9D1200D2696 = "🧑‍⚖" # :judge: + U1F9D11F3FF200D2696FE0F = "🧑🏿‍⚖️" # :judge_dark_skin_tone: + U1F9D11F3FF200D2696 = "🧑🏿‍⚖" # :judge_dark_skin_tone: + U1F9D11F3FB200D2696FE0F = "🧑🏻‍⚖️" # :judge_light_skin_tone: + U1F9D11F3FB200D2696 = "🧑🏻‍⚖" # :judge_light_skin_tone: + U1F9D11F3FE200D2696FE0F = "🧑🏾‍⚖️" # :judge_medium-dark_skin_tone: + U1F9D11F3FE200D2696 = "🧑🏾‍⚖" # :judge_medium-dark_skin_tone: + U1F9D11F3FC200D2696FE0F = "🧑🏼‍⚖️" # :judge_medium-light_skin_tone: + U1F9D11F3FC200D2696 = "🧑🏼‍⚖" # :judge_medium-light_skin_tone: + U1F9D11F3FD200D2696FE0F = "🧑🏽‍⚖️" # :judge_medium_skin_tone: + U1F9D11F3FD200D2696 = "🧑🏽‍⚖" # :judge_medium_skin_tone: + U1F54B = "🕋" # :kaaba: + U1F998 = "🦘" # :kangaroo: + U1F511 = "🔑" # :key: + U2328FE0F = "⌨️" # :keyboard: + U2328 = "⌨" # :keyboard: + U23FE0F20E3 = "#️⃣" # :keycap_#: + U2320E3 = "#⃣" # :keycap_#: + U2AFE0F20E3 = "*️⃣" # :keycap_*: + U2A20E3 = "*⃣" # :keycap_*: + U30FE0F20E3 = "0️⃣" # :keycap_0: + U3020E3 = "0⃣" # :keycap_0: + U31FE0F20E3 = "1️⃣" # :keycap_1: + U3120E3 = "1⃣" # :keycap_1: + U1F51F = "🔟" # :keycap_10: + U32FE0F20E3 = "2️⃣" # :keycap_2: + U3220E3 = "2⃣" # :keycap_2: + U33FE0F20E3 = "3️⃣" # :keycap_3: + U3320E3 = "3⃣" # :keycap_3: + U34FE0F20E3 = "4️⃣" # :keycap_4: + U3420E3 = "4⃣" # :keycap_4: + U35FE0F20E3 = "5️⃣" # :keycap_5: + U3520E3 = "5⃣" # :keycap_5: + U36FE0F20E3 = "6️⃣" # :keycap_6: + U3620E3 = "6⃣" # :keycap_6: + U37FE0F20E3 = "7️⃣" # :keycap_7: + U3720E3 = "7⃣" # :keycap_7: + U38FE0F20E3 = "8️⃣" # :keycap_8: + U3820E3 = "8⃣" # :keycap_8: + U39FE0F20E3 = "9️⃣" # :keycap_9: + U3920E3 = "9⃣" # :keycap_9: + U1FAAF = "🪯" # :khanda: + U1F6F4 = "🛴" # :kick_scooter: + U1F458 = "👘" # :kimono: + U1F48F = "💏" # :kiss: + U1F48F1F3FF = "💏🏿" # :kiss_dark_skin_tone: + U1F48F1F3FB = "💏🏻" # :kiss_light_skin_tone: + U1F468200D2764FE0F200D1F48B200D1F468 = "👨‍❤️‍💋‍👨" # :kiss_man_man: + U1F468200D2764200D1F48B200D1F468 = "👨‍❤‍💋‍👨" # :kiss_man_man: + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏿‍❤️‍💋‍👨🏿" # :kiss_man_man_dark_skin_tone: + U1F4681F3FF200D2764200D1F48B200D1F4681F3FF = "👨🏿‍❤‍💋‍👨🏿" # :kiss_man_man_dark_skin_tone: + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏿‍❤️‍💋‍👨🏻" # :kiss_man_man_dark_skin_tone_light_skin_tone: + U1F4681F3FF200D2764200D1F48B200D1F4681F3FB = "👨🏿‍❤‍💋‍👨🏻" # :kiss_man_man_dark_skin_tone_light_skin_tone: + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏿‍❤️‍💋‍👨🏾" # :kiss_man_man_dark_skin_tone_medium-dark_skin_tone: + U1F4681F3FF200D2764200D1F48B200D1F4681F3FE = "👨🏿‍❤‍💋‍👨🏾" # :kiss_man_man_dark_skin_tone_medium-dark_skin_tone: + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏿‍❤️‍💋‍👨🏼" # :kiss_man_man_dark_skin_tone_medium-light_skin_tone: + U1F4681F3FF200D2764200D1F48B200D1F4681F3FC = "👨🏿‍❤‍💋‍👨🏼" # :kiss_man_man_dark_skin_tone_medium-light_skin_tone: + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏿‍❤️‍💋‍👨🏽" # :kiss_man_man_dark_skin_tone_medium_skin_tone: + U1F4681F3FF200D2764200D1F48B200D1F4681F3FD = "👨🏿‍❤‍💋‍👨🏽" # :kiss_man_man_dark_skin_tone_medium_skin_tone: + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏻‍❤️‍💋‍👨🏻" # :kiss_man_man_light_skin_tone: + U1F4681F3FB200D2764200D1F48B200D1F4681F3FB = "👨🏻‍❤‍💋‍👨🏻" # :kiss_man_man_light_skin_tone: + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏻‍❤️‍💋‍👨🏿" # :kiss_man_man_light_skin_tone_dark_skin_tone: + U1F4681F3FB200D2764200D1F48B200D1F4681F3FF = "👨🏻‍❤‍💋‍👨🏿" # :kiss_man_man_light_skin_tone_dark_skin_tone: + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏻‍❤️‍💋‍👨🏾" # :kiss_man_man_light_skin_tone_medium-dark_skin_tone: + U1F4681F3FB200D2764200D1F48B200D1F4681F3FE = "👨🏻‍❤‍💋‍👨🏾" # :kiss_man_man_light_skin_tone_medium-dark_skin_tone: + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏻‍❤️‍💋‍👨🏼" # :kiss_man_man_light_skin_tone_medium-light_skin_tone: + U1F4681F3FB200D2764200D1F48B200D1F4681F3FC = "👨🏻‍❤‍💋‍👨🏼" # :kiss_man_man_light_skin_tone_medium-light_skin_tone: + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏻‍❤️‍💋‍👨🏽" # :kiss_man_man_light_skin_tone_medium_skin_tone: + U1F4681F3FB200D2764200D1F48B200D1F4681F3FD = "👨🏻‍❤‍💋‍👨🏽" # :kiss_man_man_light_skin_tone_medium_skin_tone: + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏾‍❤️‍💋‍👨🏾" # :kiss_man_man_medium-dark_skin_tone: + U1F4681F3FE200D2764200D1F48B200D1F4681F3FE = "👨🏾‍❤‍💋‍👨🏾" # :kiss_man_man_medium-dark_skin_tone: + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏾‍❤️‍💋‍👨🏿" # :kiss_man_man_medium-dark_skin_tone_dark_skin_tone: + U1F4681F3FE200D2764200D1F48B200D1F4681F3FF = "👨🏾‍❤‍💋‍👨🏿" # :kiss_man_man_medium-dark_skin_tone_dark_skin_tone: + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏾‍❤️‍💋‍👨🏻" # :kiss_man_man_medium-dark_skin_tone_light_skin_tone: + U1F4681F3FE200D2764200D1F48B200D1F4681F3FB = "👨🏾‍❤‍💋‍👨🏻" # :kiss_man_man_medium-dark_skin_tone_light_skin_tone: + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏾‍❤️‍💋‍👨🏼" # :kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4681F3FE200D2764200D1F48B200D1F4681F3FC = "👨🏾‍❤‍💋‍👨🏼" # :kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏾‍❤️‍💋‍👨🏽" # :kiss_man_man_medium-dark_skin_tone_medium_skin_tone: + U1F4681F3FE200D2764200D1F48B200D1F4681F3FD = "👨🏾‍❤‍💋‍👨🏽" # :kiss_man_man_medium-dark_skin_tone_medium_skin_tone: + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏼‍❤️‍💋‍👨🏼" # :kiss_man_man_medium-light_skin_tone: + U1F4681F3FC200D2764200D1F48B200D1F4681F3FC = "👨🏼‍❤‍💋‍👨🏼" # :kiss_man_man_medium-light_skin_tone: + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏼‍❤️‍💋‍👨🏿" # :kiss_man_man_medium-light_skin_tone_dark_skin_tone: + U1F4681F3FC200D2764200D1F48B200D1F4681F3FF = "👨🏼‍❤‍💋‍👨🏿" # :kiss_man_man_medium-light_skin_tone_dark_skin_tone: + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏼‍❤️‍💋‍👨🏻" # :kiss_man_man_medium-light_skin_tone_light_skin_tone: + U1F4681F3FC200D2764200D1F48B200D1F4681F3FB = "👨🏼‍❤‍💋‍👨🏻" # :kiss_man_man_medium-light_skin_tone_light_skin_tone: + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏼‍❤️‍💋‍👨🏾" # :kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4681F3FC200D2764200D1F48B200D1F4681F3FE = "👨🏼‍❤‍💋‍👨🏾" # :kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏼‍❤️‍💋‍👨🏽" # :kiss_man_man_medium-light_skin_tone_medium_skin_tone: + U1F4681F3FC200D2764200D1F48B200D1F4681F3FD = "👨🏼‍❤‍💋‍👨🏽" # :kiss_man_man_medium-light_skin_tone_medium_skin_tone: + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏽‍❤️‍💋‍👨🏽" # :kiss_man_man_medium_skin_tone: + U1F4681F3FD200D2764200D1F48B200D1F4681F3FD = "👨🏽‍❤‍💋‍👨🏽" # :kiss_man_man_medium_skin_tone: + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏽‍❤️‍💋‍👨🏿" # :kiss_man_man_medium_skin_tone_dark_skin_tone: + U1F4681F3FD200D2764200D1F48B200D1F4681F3FF = "👨🏽‍❤‍💋‍👨🏿" # :kiss_man_man_medium_skin_tone_dark_skin_tone: + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏽‍❤️‍💋‍👨🏻" # :kiss_man_man_medium_skin_tone_light_skin_tone: + U1F4681F3FD200D2764200D1F48B200D1F4681F3FB = "👨🏽‍❤‍💋‍👨🏻" # :kiss_man_man_medium_skin_tone_light_skin_tone: + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏽‍❤️‍💋‍👨🏾" # :kiss_man_man_medium_skin_tone_medium-dark_skin_tone: + U1F4681F3FD200D2764200D1F48B200D1F4681F3FE = "👨🏽‍❤‍💋‍👨🏾" # :kiss_man_man_medium_skin_tone_medium-dark_skin_tone: + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏽‍❤️‍💋‍👨🏼" # :kiss_man_man_medium_skin_tone_medium-light_skin_tone: + U1F4681F3FD200D2764200D1F48B200D1F4681F3FC = "👨🏽‍❤‍💋‍👨🏼" # :kiss_man_man_medium_skin_tone_medium-light_skin_tone: + U1F48B = "💋" # :kiss_mark: + U1F48F1F3FE = "💏🏾" # :kiss_medium-dark_skin_tone: + U1F48F1F3FC = "💏🏼" # :kiss_medium-light_skin_tone: + U1F48F1F3FD = "💏🏽" # :kiss_medium_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏿‍❤️‍💋‍🧑🏻" # :kiss_person_person_dark_skin_tone_light_skin_tone: + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FB = "🧑🏿‍❤‍💋‍🧑🏻" # :kiss_person_person_dark_skin_tone_light_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏿‍❤️‍💋‍🧑🏾" # :kiss_person_person_dark_skin_tone_medium-dark_skin_tone: + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FE = "🧑🏿‍❤‍💋‍🧑🏾" # :kiss_person_person_dark_skin_tone_medium-dark_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏿‍❤️‍💋‍🧑🏼" # :kiss_person_person_dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FC = "🧑🏿‍❤‍💋‍🧑🏼" # :kiss_person_person_dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏿‍❤️‍💋‍🧑🏽" # :kiss_person_person_dark_skin_tone_medium_skin_tone: + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FD = "🧑🏿‍❤‍💋‍🧑🏽" # :kiss_person_person_dark_skin_tone_medium_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏻‍❤️‍💋‍🧑🏿" # :kiss_person_person_light_skin_tone_dark_skin_tone: + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FF = "🧑🏻‍❤‍💋‍🧑🏿" # :kiss_person_person_light_skin_tone_dark_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏻‍❤️‍💋‍🧑🏾" # :kiss_person_person_light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FE = "🧑🏻‍❤‍💋‍🧑🏾" # :kiss_person_person_light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏻‍❤️‍💋‍🧑🏼" # :kiss_person_person_light_skin_tone_medium-light_skin_tone: + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FC = "🧑🏻‍❤‍💋‍🧑🏼" # :kiss_person_person_light_skin_tone_medium-light_skin_tone: + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏻‍❤️‍💋‍🧑🏽" # :kiss_person_person_light_skin_tone_medium_skin_tone: + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FD = "🧑🏻‍❤‍💋‍🧑🏽" # :kiss_person_person_light_skin_tone_medium_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏾‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium-dark_skin_tone_dark_skin_tone: + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FF = "🧑🏾‍❤‍💋‍🧑🏿" # :kiss_person_person_medium-dark_skin_tone_dark_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏾‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium-dark_skin_tone_light_skin_tone: + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FB = "🧑🏾‍❤‍💋‍🧑🏻" # :kiss_person_person_medium-dark_skin_tone_light_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏾‍❤️‍💋‍🧑🏼" # :kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FC = "🧑🏾‍❤‍💋‍🧑🏼" # :kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏾‍❤️‍💋‍🧑🏽" # :kiss_person_person_medium-dark_skin_tone_medium_skin_tone: + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FD = "🧑🏾‍❤‍💋‍🧑🏽" # :kiss_person_person_medium-dark_skin_tone_medium_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏼‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium-light_skin_tone_dark_skin_tone: + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FF = "🧑🏼‍❤‍💋‍🧑🏿" # :kiss_person_person_medium-light_skin_tone_dark_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏼‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium-light_skin_tone_light_skin_tone: + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FB = "🧑🏼‍❤‍💋‍🧑🏻" # :kiss_person_person_medium-light_skin_tone_light_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏼‍❤️‍💋‍🧑🏾" # :kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FE = "🧑🏼‍❤‍💋‍🧑🏾" # :kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏼‍❤️‍💋‍🧑🏽" # :kiss_person_person_medium-light_skin_tone_medium_skin_tone: + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FD = "🧑🏼‍❤‍💋‍🧑🏽" # :kiss_person_person_medium-light_skin_tone_medium_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏽‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium_skin_tone_dark_skin_tone: + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FF = "🧑🏽‍❤‍💋‍🧑🏿" # :kiss_person_person_medium_skin_tone_dark_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏽‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium_skin_tone_light_skin_tone: + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FB = "🧑🏽‍❤‍💋‍🧑🏻" # :kiss_person_person_medium_skin_tone_light_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏽‍❤️‍💋‍🧑🏾" # :kiss_person_person_medium_skin_tone_medium-dark_skin_tone: + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FE = "🧑🏽‍❤‍💋‍🧑🏾" # :kiss_person_person_medium_skin_tone_medium-dark_skin_tone: + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏽‍❤️‍💋‍🧑🏼" # :kiss_person_person_medium_skin_tone_medium-light_skin_tone: + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FC = "🧑🏽‍❤‍💋‍🧑🏼" # :kiss_person_person_medium_skin_tone_medium-light_skin_tone: + U1F469200D2764FE0F200D1F48B200D1F468 = "👩‍❤️‍💋‍👨" # :kiss_woman_man: + U1F469200D2764200D1F48B200D1F468 = "👩‍❤‍💋‍👨" # :kiss_woman_man: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏿‍❤️‍💋‍👨🏿" # :kiss_woman_man_dark_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4681F3FF = "👩🏿‍❤‍💋‍👨🏿" # :kiss_woman_man_dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏿‍❤️‍💋‍👨🏻" # :kiss_woman_man_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4681F3FB = "👩🏿‍❤‍💋‍👨🏻" # :kiss_woman_man_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏿‍❤️‍💋‍👨🏾" # :kiss_woman_man_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4681F3FE = "👩🏿‍❤‍💋‍👨🏾" # :kiss_woman_man_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏿‍❤️‍💋‍👨🏼" # :kiss_woman_man_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4681F3FC = "👩🏿‍❤‍💋‍👨🏼" # :kiss_woman_man_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏿‍❤️‍💋‍👨🏽" # :kiss_woman_man_dark_skin_tone_medium_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4681F3FD = "👩🏿‍❤‍💋‍👨🏽" # :kiss_woman_man_dark_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏻‍❤️‍💋‍👨🏻" # :kiss_woman_man_light_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4681F3FB = "👩🏻‍❤‍💋‍👨🏻" # :kiss_woman_man_light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏻‍❤️‍💋‍👨🏿" # :kiss_woman_man_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4681F3FF = "👩🏻‍❤‍💋‍👨🏿" # :kiss_woman_man_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏻‍❤️‍💋‍👨🏾" # :kiss_woman_man_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4681F3FE = "👩🏻‍❤‍💋‍👨🏾" # :kiss_woman_man_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏻‍❤️‍💋‍👨🏼" # :kiss_woman_man_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4681F3FC = "👩🏻‍❤‍💋‍👨🏼" # :kiss_woman_man_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏻‍❤️‍💋‍👨🏽" # :kiss_woman_man_light_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4681F3FD = "👩🏻‍❤‍💋‍👨🏽" # :kiss_woman_man_light_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏾‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium-dark_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4681F3FE = "👩🏾‍❤‍💋‍👨🏾" # :kiss_woman_man_medium-dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏾‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4681F3FF = "👩🏾‍❤‍💋‍👨🏿" # :kiss_woman_man_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏾‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4681F3FB = "👩🏾‍❤‍💋‍👨🏻" # :kiss_woman_man_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏾‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4681F3FC = "👩🏾‍❤‍💋‍👨🏼" # :kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏾‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4681F3FD = "👩🏾‍❤‍💋‍👨🏽" # :kiss_woman_man_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏼‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium-light_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4681F3FC = "👩🏼‍❤‍💋‍👨🏼" # :kiss_woman_man_medium-light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏼‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4681F3FF = "👩🏼‍❤‍💋‍👨🏿" # :kiss_woman_man_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏼‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4681F3FB = "👩🏼‍❤‍💋‍👨🏻" # :kiss_woman_man_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏼‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4681F3FE = "👩🏼‍❤‍💋‍👨🏾" # :kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏼‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4681F3FD = "👩🏼‍❤‍💋‍👨🏽" # :kiss_woman_man_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏽‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4681F3FD = "👩🏽‍❤‍💋‍👨🏽" # :kiss_woman_man_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏽‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4681F3FF = "👩🏽‍❤‍💋‍👨🏿" # :kiss_woman_man_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏽‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4681F3FB = "👩🏽‍❤‍💋‍👨🏻" # :kiss_woman_man_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏽‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4681F3FE = "👩🏽‍❤‍💋‍👨🏾" # :kiss_woman_man_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏽‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium_skin_tone_medium-light_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4681F3FC = "👩🏽‍❤‍💋‍👨🏼" # :kiss_woman_man_medium_skin_tone_medium-light_skin_tone: + U1F469200D2764FE0F200D1F48B200D1F469 = "👩‍❤️‍💋‍👩" # :kiss_woman_woman: + U1F469200D2764200D1F48B200D1F469 = "👩‍❤‍💋‍👩" # :kiss_woman_woman: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏿‍❤️‍💋‍👩🏿" # :kiss_woman_woman_dark_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4691F3FF = "👩🏿‍❤‍💋‍👩🏿" # :kiss_woman_woman_dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏿‍❤️‍💋‍👩🏻" # :kiss_woman_woman_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4691F3FB = "👩🏿‍❤‍💋‍👩🏻" # :kiss_woman_woman_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏿‍❤️‍💋‍👩🏾" # :kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4691F3FE = "👩🏿‍❤‍💋‍👩🏾" # :kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏿‍❤️‍💋‍👩🏼" # :kiss_woman_woman_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4691F3FC = "👩🏿‍❤‍💋‍👩🏼" # :kiss_woman_woman_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏿‍❤️‍💋‍👩🏽" # :kiss_woman_woman_dark_skin_tone_medium_skin_tone: + U1F4691F3FF200D2764200D1F48B200D1F4691F3FD = "👩🏿‍❤‍💋‍👩🏽" # :kiss_woman_woman_dark_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏻‍❤️‍💋‍👩🏻" # :kiss_woman_woman_light_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4691F3FB = "👩🏻‍❤‍💋‍👩🏻" # :kiss_woman_woman_light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏻‍❤️‍💋‍👩🏿" # :kiss_woman_woman_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4691F3FF = "👩🏻‍❤‍💋‍👩🏿" # :kiss_woman_woman_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏻‍❤️‍💋‍👩🏾" # :kiss_woman_woman_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4691F3FE = "👩🏻‍❤‍💋‍👩🏾" # :kiss_woman_woman_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏻‍❤️‍💋‍👩🏼" # :kiss_woman_woman_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4691F3FC = "👩🏻‍❤‍💋‍👩🏼" # :kiss_woman_woman_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏻‍❤️‍💋‍👩🏽" # :kiss_woman_woman_light_skin_tone_medium_skin_tone: + U1F4691F3FB200D2764200D1F48B200D1F4691F3FD = "👩🏻‍❤‍💋‍👩🏽" # :kiss_woman_woman_light_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏾‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium-dark_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4691F3FE = "👩🏾‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium-dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏾‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4691F3FF = "👩🏾‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏾‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4691F3FB = "👩🏾‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏾‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4691F3FC = "👩🏾‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏾‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FE200D2764200D1F48B200D1F4691F3FD = "👩🏾‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏼‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium-light_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4691F3FC = "👩🏼‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium-light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏼‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4691F3FF = "👩🏼‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏼‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4691F3FB = "👩🏼‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏼‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4691F3FE = "👩🏼‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏼‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FC200D2764200D1F48B200D1F4691F3FD = "👩🏼‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium-light_skin_tone_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏽‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4691F3FD = "👩🏽‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏽‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4691F3FF = "👩🏽‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏽‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4691F3FB = "👩🏽‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏽‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4691F3FE = "👩🏽‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏽‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium_skin_tone_medium-light_skin_tone: + U1F4691F3FD200D2764200D1F48B200D1F4691F3FC = "👩🏽‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium_skin_tone_medium-light_skin_tone: + U1F63D = "😽" # :kissing_cat: + U1F617 = "😗" # :kissing_face: + U1F61A = "😚" # :kissing_face_with_closed_eyes: + U1F619 = "😙" # :kissing_face_with_smiling_eyes: + U1F52A = "🔪" # :kitchen_knife: + U1FA81 = "🪁" # :kite: + U1F95D = "🥝" # :kiwi_fruit: + U1FAA2 = "🪢" # :knot: + U1F428 = "🐨" # :koala: + U1F97C = "🥼" # :lab_coat: + U1F3F7FE0F = "🏷️" # :label: + U1F3F7 = "🏷" # :label: + U1F94D = "🥍" # :lacrosse: + U1FA9C = "🪜" # :ladder: + U1F41E = "🐞" # :lady_beetle: + U1F4BB = "💻" # :laptop: + U1F537 = "🔷" # :large_blue_diamond: + U1F536 = "🔶" # :large_orange_diamond: + U1F317 = "🌗" # :last_quarter_moon: + U1F31C = "🌜" # :last_quarter_moon_face: + U23EEFE0F = "⏮️" # :last_track_button: + U23EE = "⏮" # :last_track_button: + U271DFE0F = "✝️" # :latin_cross: + U271D = "✝" # :latin_cross: + U1F343 = "🍃" # :leaf_fluttering_in_wind: + U1F96C = "🥬" # :leafy_green: + U1F4D2 = "📒" # :ledger: + U1F91B = "🤛" # :left-facing_fist: + U1F91B1F3FF = "🤛🏿" # :left-facing_fist_dark_skin_tone: + U1F91B1F3FB = "🤛🏻" # :left-facing_fist_light_skin_tone: + U1F91B1F3FE = "🤛🏾" # :left-facing_fist_medium-dark_skin_tone: + U1F91B1F3FC = "🤛🏼" # :left-facing_fist_medium-light_skin_tone: + U1F91B1F3FD = "🤛🏽" # :left-facing_fist_medium_skin_tone: + U2194FE0F = "↔️" # :left-right_arrow: + U2194 = "↔" # :left-right_arrow: + U2B05FE0F = "⬅️" # :left_arrow: + U2B05 = "⬅" # :left_arrow: + U21AAFE0F = "↪️" # :left_arrow_curving_right: + U21AA = "↪" # :left_arrow_curving_right: + U1F6C5 = "🛅" # :left_luggage: + U1F5E8FE0F = "🗨️" # :left_speech_bubble: + U1F5E8 = "🗨" # :left_speech_bubble: + U1FAF2 = "🫲" # :leftwards_hand: + U1FAF21F3FF = "🫲🏿" # :leftwards_hand_dark_skin_tone: + U1FAF21F3FB = "🫲🏻" # :leftwards_hand_light_skin_tone: + U1FAF21F3FE = "🫲🏾" # :leftwards_hand_medium-dark_skin_tone: + U1FAF21F3FC = "🫲🏼" # :leftwards_hand_medium-light_skin_tone: + U1FAF21F3FD = "🫲🏽" # :leftwards_hand_medium_skin_tone: + U1FAF7 = "🫷" # :leftwards_pushing_hand: + U1FAF71F3FF = "🫷🏿" # :leftwards_pushing_hand_dark_skin_tone: + U1FAF71F3FB = "🫷🏻" # :leftwards_pushing_hand_light_skin_tone: + U1FAF71F3FE = "🫷🏾" # :leftwards_pushing_hand_medium-dark_skin_tone: + U1FAF71F3FC = "🫷🏼" # :leftwards_pushing_hand_medium-light_skin_tone: + U1FAF71F3FD = "🫷🏽" # :leftwards_pushing_hand_medium_skin_tone: + U1F9B5 = "🦵" # :leg: + U1F9B51F3FF = "🦵🏿" # :leg_dark_skin_tone: + U1F9B51F3FB = "🦵🏻" # :leg_light_skin_tone: + U1F9B51F3FE = "🦵🏾" # :leg_medium-dark_skin_tone: + U1F9B51F3FC = "🦵🏼" # :leg_medium-light_skin_tone: + U1F9B51F3FD = "🦵🏽" # :leg_medium_skin_tone: + U1F34B = "🍋" # :lemon: + U1F406 = "🐆" # :leopard: + U1F39AFE0F = "🎚️" # :level_slider: + U1F39A = "🎚" # :level_slider: + U1FA75 = "🩵" # :light_blue_heart: + U1F4A1 = "💡" # :light_bulb: + U1F688 = "🚈" # :light_rail: + U1F3FB = "🏻" # :light_skin_tone: + U1F34B200D1F7E9 = "🍋‍🟩" # :lime: + U1F517 = "🔗" # :link: + U1F587FE0F = "🖇️" # :linked_paperclips: + U1F587 = "🖇" # :linked_paperclips: + U1F981 = "🦁" # :lion: + U1F484 = "💄" # :lipstick: + U1F6AE = "🚮" # :litter_in_bin_sign: + U1F98E = "🦎" # :lizard: + U1F999 = "🦙" # :llama: + U1F99E = "🦞" # :lobster: + U1F512 = "🔒" # :locked: + U1F510 = "🔐" # :locked_with_key: + U1F50F = "🔏" # :locked_with_pen: + U1F682 = "🚂" # :locomotive: + U1F36D = "🍭" # :lollipop: + U1FA98 = "🪘" # :long_drum: + U1F9F4 = "🧴" # :lotion_bottle: + U1FAB7 = "🪷" # :lotus: + U1F62D = "😭" # :loudly_crying_face: + U1F4E2 = "📢" # :loudspeaker: + U1F91F = "🤟" # :love-you_gesture: + U1F91F1F3FF = "🤟🏿" # :love-you_gesture_dark_skin_tone: + U1F91F1F3FB = "🤟🏻" # :love-you_gesture_light_skin_tone: + U1F91F1F3FE = "🤟🏾" # :love-you_gesture_medium-dark_skin_tone: + U1F91F1F3FC = "🤟🏼" # :love-you_gesture_medium-light_skin_tone: + U1F91F1F3FD = "🤟🏽" # :love-you_gesture_medium_skin_tone: + U1F3E9 = "🏩" # :love_hotel: + U1F48C = "💌" # :love_letter: + U1FAAB = "🪫" # :low_battery: + U1F9F3 = "🧳" # :luggage: + U1FAC1 = "🫁" # :lungs: + U1F925 = "🤥" # :lying_face: + U1F9D9 = "🧙" # :mage: + U1F9D91F3FF = "🧙🏿" # :mage_dark_skin_tone: + U1F9D91F3FB = "🧙🏻" # :mage_light_skin_tone: + U1F9D91F3FE = "🧙🏾" # :mage_medium-dark_skin_tone: + U1F9D91F3FC = "🧙🏼" # :mage_medium-light_skin_tone: + U1F9D91F3FD = "🧙🏽" # :mage_medium_skin_tone: + U1FA84 = "🪄" # :magic_wand: + U1F9F2 = "🧲" # :magnet: + U1F50D = "🔍" # :magnifying_glass_tilted_left: + U1F50E = "🔎" # :magnifying_glass_tilted_right: + U1F004 = "🀄" # :mahjong_red_dragon: + U2642FE0F = "♂️" # :male_sign: + U2642 = "♂" # :male_sign: + U1F9A3 = "🦣" # :mammoth: + U1F468 = "👨" # :man: + U1F468200D1F3A8 = "👨‍🎨" # :man_artist: + U1F4681F3FF200D1F3A8 = "👨🏿‍🎨" # :man_artist_dark_skin_tone: + U1F4681F3FB200D1F3A8 = "👨🏻‍🎨" # :man_artist_light_skin_tone: + U1F4681F3FE200D1F3A8 = "👨🏾‍🎨" # :man_artist_medium-dark_skin_tone: + U1F4681F3FC200D1F3A8 = "👨🏼‍🎨" # :man_artist_medium-light_skin_tone: + U1F4681F3FD200D1F3A8 = "👨🏽‍🎨" # :man_artist_medium_skin_tone: + U1F468200D1F680 = "👨‍🚀" # :man_astronaut: + U1F4681F3FF200D1F680 = "👨🏿‍🚀" # :man_astronaut_dark_skin_tone: + U1F4681F3FB200D1F680 = "👨🏻‍🚀" # :man_astronaut_light_skin_tone: + U1F4681F3FE200D1F680 = "👨🏾‍🚀" # :man_astronaut_medium-dark_skin_tone: + U1F4681F3FC200D1F680 = "👨🏼‍🚀" # :man_astronaut_medium-light_skin_tone: + U1F4681F3FD200D1F680 = "👨🏽‍🚀" # :man_astronaut_medium_skin_tone: + U1F468200D1F9B2 = "👨‍🦲" # :man_bald: + U1F9D4200D2642FE0F = "🧔‍♂️" # :man_beard: + U1F9D4200D2642 = "🧔‍♂" # :man_beard: + U1F6B4200D2642FE0F = "🚴‍♂️" # :man_biking: + U1F6B4200D2642 = "🚴‍♂" # :man_biking: + U1F6B41F3FF200D2642FE0F = "🚴🏿‍♂️" # :man_biking_dark_skin_tone: + U1F6B41F3FF200D2642 = "🚴🏿‍♂" # :man_biking_dark_skin_tone: + U1F6B41F3FB200D2642FE0F = "🚴🏻‍♂️" # :man_biking_light_skin_tone: + U1F6B41F3FB200D2642 = "🚴🏻‍♂" # :man_biking_light_skin_tone: + U1F6B41F3FE200D2642FE0F = "🚴🏾‍♂️" # :man_biking_medium-dark_skin_tone: + U1F6B41F3FE200D2642 = "🚴🏾‍♂" # :man_biking_medium-dark_skin_tone: + U1F6B41F3FC200D2642FE0F = "🚴🏼‍♂️" # :man_biking_medium-light_skin_tone: + U1F6B41F3FC200D2642 = "🚴🏼‍♂" # :man_biking_medium-light_skin_tone: + U1F6B41F3FD200D2642FE0F = "🚴🏽‍♂️" # :man_biking_medium_skin_tone: + U1F6B41F3FD200D2642 = "🚴🏽‍♂" # :man_biking_medium_skin_tone: + U1F471200D2642FE0F = "👱‍♂️" # :man_blond_hair: + U1F471200D2642 = "👱‍♂" # :man_blond_hair: + U26F9FE0F200D2642FE0F = "⛹️‍♂️" # :man_bouncing_ball: + U26F9200D2642FE0F = "⛹‍♂️" # :man_bouncing_ball: + U26F9FE0F200D2642 = "⛹️‍♂" # :man_bouncing_ball: + U26F9200D2642 = "⛹‍♂" # :man_bouncing_ball: + U26F91F3FF200D2642FE0F = "⛹🏿‍♂️" # :man_bouncing_ball_dark_skin_tone: + U26F91F3FF200D2642 = "⛹🏿‍♂" # :man_bouncing_ball_dark_skin_tone: + U26F91F3FB200D2642FE0F = "⛹🏻‍♂️" # :man_bouncing_ball_light_skin_tone: + U26F91F3FB200D2642 = "⛹🏻‍♂" # :man_bouncing_ball_light_skin_tone: + U26F91F3FE200D2642FE0F = "⛹🏾‍♂️" # :man_bouncing_ball_medium-dark_skin_tone: + U26F91F3FE200D2642 = "⛹🏾‍♂" # :man_bouncing_ball_medium-dark_skin_tone: + U26F91F3FC200D2642FE0F = "⛹🏼‍♂️" # :man_bouncing_ball_medium-light_skin_tone: + U26F91F3FC200D2642 = "⛹🏼‍♂" # :man_bouncing_ball_medium-light_skin_tone: + U26F91F3FD200D2642FE0F = "⛹🏽‍♂️" # :man_bouncing_ball_medium_skin_tone: + U26F91F3FD200D2642 = "⛹🏽‍♂" # :man_bouncing_ball_medium_skin_tone: + U1F647200D2642FE0F = "🙇‍♂️" # :man_bowing: + U1F647200D2642 = "🙇‍♂" # :man_bowing: + U1F6471F3FF200D2642FE0F = "🙇🏿‍♂️" # :man_bowing_dark_skin_tone: + U1F6471F3FF200D2642 = "🙇🏿‍♂" # :man_bowing_dark_skin_tone: + U1F6471F3FB200D2642FE0F = "🙇🏻‍♂️" # :man_bowing_light_skin_tone: + U1F6471F3FB200D2642 = "🙇🏻‍♂" # :man_bowing_light_skin_tone: + U1F6471F3FE200D2642FE0F = "🙇🏾‍♂️" # :man_bowing_medium-dark_skin_tone: + U1F6471F3FE200D2642 = "🙇🏾‍♂" # :man_bowing_medium-dark_skin_tone: + U1F6471F3FC200D2642FE0F = "🙇🏼‍♂️" # :man_bowing_medium-light_skin_tone: + U1F6471F3FC200D2642 = "🙇🏼‍♂" # :man_bowing_medium-light_skin_tone: + U1F6471F3FD200D2642FE0F = "🙇🏽‍♂️" # :man_bowing_medium_skin_tone: + U1F6471F3FD200D2642 = "🙇🏽‍♂" # :man_bowing_medium_skin_tone: + U1F938200D2642FE0F = "🤸‍♂️" # :man_cartwheeling: + U1F938200D2642 = "🤸‍♂" # :man_cartwheeling: + U1F9381F3FF200D2642FE0F = "🤸🏿‍♂️" # :man_cartwheeling_dark_skin_tone: + U1F9381F3FF200D2642 = "🤸🏿‍♂" # :man_cartwheeling_dark_skin_tone: + U1F9381F3FB200D2642FE0F = "🤸🏻‍♂️" # :man_cartwheeling_light_skin_tone: + U1F9381F3FB200D2642 = "🤸🏻‍♂" # :man_cartwheeling_light_skin_tone: + U1F9381F3FE200D2642FE0F = "🤸🏾‍♂️" # :man_cartwheeling_medium-dark_skin_tone: + U1F9381F3FE200D2642 = "🤸🏾‍♂" # :man_cartwheeling_medium-dark_skin_tone: + U1F9381F3FC200D2642FE0F = "🤸🏼‍♂️" # :man_cartwheeling_medium-light_skin_tone: + U1F9381F3FC200D2642 = "🤸🏼‍♂" # :man_cartwheeling_medium-light_skin_tone: + U1F9381F3FD200D2642FE0F = "🤸🏽‍♂️" # :man_cartwheeling_medium_skin_tone: + U1F9381F3FD200D2642 = "🤸🏽‍♂" # :man_cartwheeling_medium_skin_tone: + U1F9D7200D2642FE0F = "🧗‍♂️" # :man_climbing: + U1F9D7200D2642 = "🧗‍♂" # :man_climbing: + U1F9D71F3FF200D2642FE0F = "🧗🏿‍♂️" # :man_climbing_dark_skin_tone: + U1F9D71F3FF200D2642 = "🧗🏿‍♂" # :man_climbing_dark_skin_tone: + U1F9D71F3FB200D2642FE0F = "🧗🏻‍♂️" # :man_climbing_light_skin_tone: + U1F9D71F3FB200D2642 = "🧗🏻‍♂" # :man_climbing_light_skin_tone: + U1F9D71F3FE200D2642FE0F = "🧗🏾‍♂️" # :man_climbing_medium-dark_skin_tone: + U1F9D71F3FE200D2642 = "🧗🏾‍♂" # :man_climbing_medium-dark_skin_tone: + U1F9D71F3FC200D2642FE0F = "🧗🏼‍♂️" # :man_climbing_medium-light_skin_tone: + U1F9D71F3FC200D2642 = "🧗🏼‍♂" # :man_climbing_medium-light_skin_tone: + U1F9D71F3FD200D2642FE0F = "🧗🏽‍♂️" # :man_climbing_medium_skin_tone: + U1F9D71F3FD200D2642 = "🧗🏽‍♂" # :man_climbing_medium_skin_tone: + U1F477200D2642FE0F = "👷‍♂️" # :man_construction_worker: + U1F477200D2642 = "👷‍♂" # :man_construction_worker: + U1F4771F3FF200D2642FE0F = "👷🏿‍♂️" # :man_construction_worker_dark_skin_tone: + U1F4771F3FF200D2642 = "👷🏿‍♂" # :man_construction_worker_dark_skin_tone: + U1F4771F3FB200D2642FE0F = "👷🏻‍♂️" # :man_construction_worker_light_skin_tone: + U1F4771F3FB200D2642 = "👷🏻‍♂" # :man_construction_worker_light_skin_tone: + U1F4771F3FE200D2642FE0F = "👷🏾‍♂️" # :man_construction_worker_medium-dark_skin_tone: + U1F4771F3FE200D2642 = "👷🏾‍♂" # :man_construction_worker_medium-dark_skin_tone: + U1F4771F3FC200D2642FE0F = "👷🏼‍♂️" # :man_construction_worker_medium-light_skin_tone: + U1F4771F3FC200D2642 = "👷🏼‍♂" # :man_construction_worker_medium-light_skin_tone: + U1F4771F3FD200D2642FE0F = "👷🏽‍♂️" # :man_construction_worker_medium_skin_tone: + U1F4771F3FD200D2642 = "👷🏽‍♂" # :man_construction_worker_medium_skin_tone: + U1F468200D1F373 = "👨‍🍳" # :man_cook: + U1F4681F3FF200D1F373 = "👨🏿‍🍳" # :man_cook_dark_skin_tone: + U1F4681F3FB200D1F373 = "👨🏻‍🍳" # :man_cook_light_skin_tone: + U1F4681F3FE200D1F373 = "👨🏾‍🍳" # :man_cook_medium-dark_skin_tone: + U1F4681F3FC200D1F373 = "👨🏼‍🍳" # :man_cook_medium-light_skin_tone: + U1F4681F3FD200D1F373 = "👨🏽‍🍳" # :man_cook_medium_skin_tone: + U1F468200D1F9B1 = "👨‍🦱" # :man_curly_hair: + U1F57A = "🕺" # :man_dancing: + U1F57A1F3FF = "🕺🏿" # :man_dancing_dark_skin_tone: + U1F57A1F3FB = "🕺🏻" # :man_dancing_light_skin_tone: + U1F57A1F3FE = "🕺🏾" # :man_dancing_medium-dark_skin_tone: + U1F57A1F3FC = "🕺🏼" # :man_dancing_medium-light_skin_tone: + U1F57A1F3FD = "🕺🏽" # :man_dancing_medium_skin_tone: + U1F4681F3FF = "👨🏿" # :man_dark_skin_tone: + U1F4681F3FF200D1F9B2 = "👨🏿‍🦲" # :man_dark_skin_tone_bald: + U1F9D41F3FF200D2642FE0F = "🧔🏿‍♂️" # :man_dark_skin_tone_beard: + U1F9D41F3FF200D2642 = "🧔🏿‍♂" # :man_dark_skin_tone_beard: + U1F4711F3FF200D2642FE0F = "👱🏿‍♂️" # :man_dark_skin_tone_blond_hair: + U1F4711F3FF200D2642 = "👱🏿‍♂" # :man_dark_skin_tone_blond_hair: + U1F4681F3FF200D1F9B1 = "👨🏿‍🦱" # :man_dark_skin_tone_curly_hair: + U1F4681F3FF200D1F9B0 = "👨🏿‍🦰" # :man_dark_skin_tone_red_hair: + U1F4681F3FF200D1F9B3 = "👨🏿‍🦳" # :man_dark_skin_tone_white_hair: + U1F575FE0F200D2642FE0F = "🕵️‍♂️" # :man_detective: + U1F575200D2642FE0F = "🕵‍♂️" # :man_detective: + U1F575FE0F200D2642 = "🕵️‍♂" # :man_detective: + U1F575200D2642 = "🕵‍♂" # :man_detective: + U1F5751F3FF200D2642FE0F = "🕵🏿‍♂️" # :man_detective_dark_skin_tone: + U1F5751F3FF200D2642 = "🕵🏿‍♂" # :man_detective_dark_skin_tone: + U1F5751F3FB200D2642FE0F = "🕵🏻‍♂️" # :man_detective_light_skin_tone: + U1F5751F3FB200D2642 = "🕵🏻‍♂" # :man_detective_light_skin_tone: + U1F5751F3FE200D2642FE0F = "🕵🏾‍♂️" # :man_detective_medium-dark_skin_tone: + U1F5751F3FE200D2642 = "🕵🏾‍♂" # :man_detective_medium-dark_skin_tone: + U1F5751F3FC200D2642FE0F = "🕵🏼‍♂️" # :man_detective_medium-light_skin_tone: + U1F5751F3FC200D2642 = "🕵🏼‍♂" # :man_detective_medium-light_skin_tone: + U1F5751F3FD200D2642FE0F = "🕵🏽‍♂️" # :man_detective_medium_skin_tone: + U1F5751F3FD200D2642 = "🕵🏽‍♂" # :man_detective_medium_skin_tone: + U1F9DD200D2642FE0F = "🧝‍♂️" # :man_elf: + U1F9DD200D2642 = "🧝‍♂" # :man_elf: + U1F9DD1F3FF200D2642FE0F = "🧝🏿‍♂️" # :man_elf_dark_skin_tone: + U1F9DD1F3FF200D2642 = "🧝🏿‍♂" # :man_elf_dark_skin_tone: + U1F9DD1F3FB200D2642FE0F = "🧝🏻‍♂️" # :man_elf_light_skin_tone: + U1F9DD1F3FB200D2642 = "🧝🏻‍♂" # :man_elf_light_skin_tone: + U1F9DD1F3FE200D2642FE0F = "🧝🏾‍♂️" # :man_elf_medium-dark_skin_tone: + U1F9DD1F3FE200D2642 = "🧝🏾‍♂" # :man_elf_medium-dark_skin_tone: + U1F9DD1F3FC200D2642FE0F = "🧝🏼‍♂️" # :man_elf_medium-light_skin_tone: + U1F9DD1F3FC200D2642 = "🧝🏼‍♂" # :man_elf_medium-light_skin_tone: + U1F9DD1F3FD200D2642FE0F = "🧝🏽‍♂️" # :man_elf_medium_skin_tone: + U1F9DD1F3FD200D2642 = "🧝🏽‍♂" # :man_elf_medium_skin_tone: + U1F926200D2642FE0F = "🤦‍♂️" # :man_facepalming: + U1F926200D2642 = "🤦‍♂" # :man_facepalming: + U1F9261F3FF200D2642FE0F = "🤦🏿‍♂️" # :man_facepalming_dark_skin_tone: + U1F9261F3FF200D2642 = "🤦🏿‍♂" # :man_facepalming_dark_skin_tone: + U1F9261F3FB200D2642FE0F = "🤦🏻‍♂️" # :man_facepalming_light_skin_tone: + U1F9261F3FB200D2642 = "🤦🏻‍♂" # :man_facepalming_light_skin_tone: + U1F9261F3FE200D2642FE0F = "🤦🏾‍♂️" # :man_facepalming_medium-dark_skin_tone: + U1F9261F3FE200D2642 = "🤦🏾‍♂" # :man_facepalming_medium-dark_skin_tone: + U1F9261F3FC200D2642FE0F = "🤦🏼‍♂️" # :man_facepalming_medium-light_skin_tone: + U1F9261F3FC200D2642 = "🤦🏼‍♂" # :man_facepalming_medium-light_skin_tone: + U1F9261F3FD200D2642FE0F = "🤦🏽‍♂️" # :man_facepalming_medium_skin_tone: + U1F9261F3FD200D2642 = "🤦🏽‍♂" # :man_facepalming_medium_skin_tone: + U1F468200D1F3ED = "👨‍🏭" # :man_factory_worker: + U1F4681F3FF200D1F3ED = "👨🏿‍🏭" # :man_factory_worker_dark_skin_tone: + U1F4681F3FB200D1F3ED = "👨🏻‍🏭" # :man_factory_worker_light_skin_tone: + U1F4681F3FE200D1F3ED = "👨🏾‍🏭" # :man_factory_worker_medium-dark_skin_tone: + U1F4681F3FC200D1F3ED = "👨🏼‍🏭" # :man_factory_worker_medium-light_skin_tone: + U1F4681F3FD200D1F3ED = "👨🏽‍🏭" # :man_factory_worker_medium_skin_tone: + U1F9DA200D2642FE0F = "🧚‍♂️" # :man_fairy: + U1F9DA200D2642 = "🧚‍♂" # :man_fairy: + U1F9DA1F3FF200D2642FE0F = "🧚🏿‍♂️" # :man_fairy_dark_skin_tone: + U1F9DA1F3FF200D2642 = "🧚🏿‍♂" # :man_fairy_dark_skin_tone: + U1F9DA1F3FB200D2642FE0F = "🧚🏻‍♂️" # :man_fairy_light_skin_tone: + U1F9DA1F3FB200D2642 = "🧚🏻‍♂" # :man_fairy_light_skin_tone: + U1F9DA1F3FE200D2642FE0F = "🧚🏾‍♂️" # :man_fairy_medium-dark_skin_tone: + U1F9DA1F3FE200D2642 = "🧚🏾‍♂" # :man_fairy_medium-dark_skin_tone: + U1F9DA1F3FC200D2642FE0F = "🧚🏼‍♂️" # :man_fairy_medium-light_skin_tone: + U1F9DA1F3FC200D2642 = "🧚🏼‍♂" # :man_fairy_medium-light_skin_tone: + U1F9DA1F3FD200D2642FE0F = "🧚🏽‍♂️" # :man_fairy_medium_skin_tone: + U1F9DA1F3FD200D2642 = "🧚🏽‍♂" # :man_fairy_medium_skin_tone: + U1F468200D1F33E = "👨‍🌾" # :man_farmer: + U1F4681F3FF200D1F33E = "👨🏿‍🌾" # :man_farmer_dark_skin_tone: + U1F4681F3FB200D1F33E = "👨🏻‍🌾" # :man_farmer_light_skin_tone: + U1F4681F3FE200D1F33E = "👨🏾‍🌾" # :man_farmer_medium-dark_skin_tone: + U1F4681F3FC200D1F33E = "👨🏼‍🌾" # :man_farmer_medium-light_skin_tone: + U1F4681F3FD200D1F33E = "👨🏽‍🌾" # :man_farmer_medium_skin_tone: + U1F468200D1F37C = "👨‍🍼" # :man_feeding_baby: + U1F4681F3FF200D1F37C = "👨🏿‍🍼" # :man_feeding_baby_dark_skin_tone: + U1F4681F3FB200D1F37C = "👨🏻‍🍼" # :man_feeding_baby_light_skin_tone: + U1F4681F3FE200D1F37C = "👨🏾‍🍼" # :man_feeding_baby_medium-dark_skin_tone: + U1F4681F3FC200D1F37C = "👨🏼‍🍼" # :man_feeding_baby_medium-light_skin_tone: + U1F4681F3FD200D1F37C = "👨🏽‍🍼" # :man_feeding_baby_medium_skin_tone: + U1F468200D1F692 = "👨‍🚒" # :man_firefighter: + U1F4681F3FF200D1F692 = "👨🏿‍🚒" # :man_firefighter_dark_skin_tone: + U1F4681F3FB200D1F692 = "👨🏻‍🚒" # :man_firefighter_light_skin_tone: + U1F4681F3FE200D1F692 = "👨🏾‍🚒" # :man_firefighter_medium-dark_skin_tone: + U1F4681F3FC200D1F692 = "👨🏼‍🚒" # :man_firefighter_medium-light_skin_tone: + U1F4681F3FD200D1F692 = "👨🏽‍🚒" # :man_firefighter_medium_skin_tone: + U1F64D200D2642FE0F = "🙍‍♂️" # :man_frowning: + U1F64D200D2642 = "🙍‍♂" # :man_frowning: + U1F64D1F3FF200D2642FE0F = "🙍🏿‍♂️" # :man_frowning_dark_skin_tone: + U1F64D1F3FF200D2642 = "🙍🏿‍♂" # :man_frowning_dark_skin_tone: + U1F64D1F3FB200D2642FE0F = "🙍🏻‍♂️" # :man_frowning_light_skin_tone: + U1F64D1F3FB200D2642 = "🙍🏻‍♂" # :man_frowning_light_skin_tone: + U1F64D1F3FE200D2642FE0F = "🙍🏾‍♂️" # :man_frowning_medium-dark_skin_tone: + U1F64D1F3FE200D2642 = "🙍🏾‍♂" # :man_frowning_medium-dark_skin_tone: + U1F64D1F3FC200D2642FE0F = "🙍🏼‍♂️" # :man_frowning_medium-light_skin_tone: + U1F64D1F3FC200D2642 = "🙍🏼‍♂" # :man_frowning_medium-light_skin_tone: + U1F64D1F3FD200D2642FE0F = "🙍🏽‍♂️" # :man_frowning_medium_skin_tone: + U1F64D1F3FD200D2642 = "🙍🏽‍♂" # :man_frowning_medium_skin_tone: + U1F9DE200D2642FE0F = "🧞‍♂️" # :man_genie: + U1F9DE200D2642 = "🧞‍♂" # :man_genie: + U1F645200D2642FE0F = "🙅‍♂️" # :man_gesturing_NO: + U1F645200D2642 = "🙅‍♂" # :man_gesturing_NO: + U1F6451F3FF200D2642FE0F = "🙅🏿‍♂️" # :man_gesturing_NO_dark_skin_tone: + U1F6451F3FF200D2642 = "🙅🏿‍♂" # :man_gesturing_NO_dark_skin_tone: + U1F6451F3FB200D2642FE0F = "🙅🏻‍♂️" # :man_gesturing_NO_light_skin_tone: + U1F6451F3FB200D2642 = "🙅🏻‍♂" # :man_gesturing_NO_light_skin_tone: + U1F6451F3FE200D2642FE0F = "🙅🏾‍♂️" # :man_gesturing_NO_medium-dark_skin_tone: + U1F6451F3FE200D2642 = "🙅🏾‍♂" # :man_gesturing_NO_medium-dark_skin_tone: + U1F6451F3FC200D2642FE0F = "🙅🏼‍♂️" # :man_gesturing_NO_medium-light_skin_tone: + U1F6451F3FC200D2642 = "🙅🏼‍♂" # :man_gesturing_NO_medium-light_skin_tone: + U1F6451F3FD200D2642FE0F = "🙅🏽‍♂️" # :man_gesturing_NO_medium_skin_tone: + U1F6451F3FD200D2642 = "🙅🏽‍♂" # :man_gesturing_NO_medium_skin_tone: + U1F646200D2642FE0F = "🙆‍♂️" # :man_gesturing_OK: + U1F646200D2642 = "🙆‍♂" # :man_gesturing_OK: + U1F6461F3FF200D2642FE0F = "🙆🏿‍♂️" # :man_gesturing_OK_dark_skin_tone: + U1F6461F3FF200D2642 = "🙆🏿‍♂" # :man_gesturing_OK_dark_skin_tone: + U1F6461F3FB200D2642FE0F = "🙆🏻‍♂️" # :man_gesturing_OK_light_skin_tone: + U1F6461F3FB200D2642 = "🙆🏻‍♂" # :man_gesturing_OK_light_skin_tone: + U1F6461F3FE200D2642FE0F = "🙆🏾‍♂️" # :man_gesturing_OK_medium-dark_skin_tone: + U1F6461F3FE200D2642 = "🙆🏾‍♂" # :man_gesturing_OK_medium-dark_skin_tone: + U1F6461F3FC200D2642FE0F = "🙆🏼‍♂️" # :man_gesturing_OK_medium-light_skin_tone: + U1F6461F3FC200D2642 = "🙆🏼‍♂" # :man_gesturing_OK_medium-light_skin_tone: + U1F6461F3FD200D2642FE0F = "🙆🏽‍♂️" # :man_gesturing_OK_medium_skin_tone: + U1F6461F3FD200D2642 = "🙆🏽‍♂" # :man_gesturing_OK_medium_skin_tone: + U1F487200D2642FE0F = "💇‍♂️" # :man_getting_haircut: + U1F487200D2642 = "💇‍♂" # :man_getting_haircut: + U1F4871F3FF200D2642FE0F = "💇🏿‍♂️" # :man_getting_haircut_dark_skin_tone: + U1F4871F3FF200D2642 = "💇🏿‍♂" # :man_getting_haircut_dark_skin_tone: + U1F4871F3FB200D2642FE0F = "💇🏻‍♂️" # :man_getting_haircut_light_skin_tone: + U1F4871F3FB200D2642 = "💇🏻‍♂" # :man_getting_haircut_light_skin_tone: + U1F4871F3FE200D2642FE0F = "💇🏾‍♂️" # :man_getting_haircut_medium-dark_skin_tone: + U1F4871F3FE200D2642 = "💇🏾‍♂" # :man_getting_haircut_medium-dark_skin_tone: + U1F4871F3FC200D2642FE0F = "💇🏼‍♂️" # :man_getting_haircut_medium-light_skin_tone: + U1F4871F3FC200D2642 = "💇🏼‍♂" # :man_getting_haircut_medium-light_skin_tone: + U1F4871F3FD200D2642FE0F = "💇🏽‍♂️" # :man_getting_haircut_medium_skin_tone: + U1F4871F3FD200D2642 = "💇🏽‍♂" # :man_getting_haircut_medium_skin_tone: + U1F486200D2642FE0F = "💆‍♂️" # :man_getting_massage: + U1F486200D2642 = "💆‍♂" # :man_getting_massage: + U1F4861F3FF200D2642FE0F = "💆🏿‍♂️" # :man_getting_massage_dark_skin_tone: + U1F4861F3FF200D2642 = "💆🏿‍♂" # :man_getting_massage_dark_skin_tone: + U1F4861F3FB200D2642FE0F = "💆🏻‍♂️" # :man_getting_massage_light_skin_tone: + U1F4861F3FB200D2642 = "💆🏻‍♂" # :man_getting_massage_light_skin_tone: + U1F4861F3FE200D2642FE0F = "💆🏾‍♂️" # :man_getting_massage_medium-dark_skin_tone: + U1F4861F3FE200D2642 = "💆🏾‍♂" # :man_getting_massage_medium-dark_skin_tone: + U1F4861F3FC200D2642FE0F = "💆🏼‍♂️" # :man_getting_massage_medium-light_skin_tone: + U1F4861F3FC200D2642 = "💆🏼‍♂" # :man_getting_massage_medium-light_skin_tone: + U1F4861F3FD200D2642FE0F = "💆🏽‍♂️" # :man_getting_massage_medium_skin_tone: + U1F4861F3FD200D2642 = "💆🏽‍♂" # :man_getting_massage_medium_skin_tone: + U1F3CCFE0F200D2642FE0F = "🏌️‍♂️" # :man_golfing: + U1F3CC200D2642FE0F = "🏌‍♂️" # :man_golfing: + U1F3CCFE0F200D2642 = "🏌️‍♂" # :man_golfing: + U1F3CC200D2642 = "🏌‍♂" # :man_golfing: + U1F3CC1F3FF200D2642FE0F = "🏌🏿‍♂️" # :man_golfing_dark_skin_tone: + U1F3CC1F3FF200D2642 = "🏌🏿‍♂" # :man_golfing_dark_skin_tone: + U1F3CC1F3FB200D2642FE0F = "🏌🏻‍♂️" # :man_golfing_light_skin_tone: + U1F3CC1F3FB200D2642 = "🏌🏻‍♂" # :man_golfing_light_skin_tone: + U1F3CC1F3FE200D2642FE0F = "🏌🏾‍♂️" # :man_golfing_medium-dark_skin_tone: + U1F3CC1F3FE200D2642 = "🏌🏾‍♂" # :man_golfing_medium-dark_skin_tone: + U1F3CC1F3FC200D2642FE0F = "🏌🏼‍♂️" # :man_golfing_medium-light_skin_tone: + U1F3CC1F3FC200D2642 = "🏌🏼‍♂" # :man_golfing_medium-light_skin_tone: + U1F3CC1F3FD200D2642FE0F = "🏌🏽‍♂️" # :man_golfing_medium_skin_tone: + U1F3CC1F3FD200D2642 = "🏌🏽‍♂" # :man_golfing_medium_skin_tone: + U1F482200D2642FE0F = "💂‍♂️" # :man_guard: + U1F482200D2642 = "💂‍♂" # :man_guard: + U1F4821F3FF200D2642FE0F = "💂🏿‍♂️" # :man_guard_dark_skin_tone: + U1F4821F3FF200D2642 = "💂🏿‍♂" # :man_guard_dark_skin_tone: + U1F4821F3FB200D2642FE0F = "💂🏻‍♂️" # :man_guard_light_skin_tone: + U1F4821F3FB200D2642 = "💂🏻‍♂" # :man_guard_light_skin_tone: + U1F4821F3FE200D2642FE0F = "💂🏾‍♂️" # :man_guard_medium-dark_skin_tone: + U1F4821F3FE200D2642 = "💂🏾‍♂" # :man_guard_medium-dark_skin_tone: + U1F4821F3FC200D2642FE0F = "💂🏼‍♂️" # :man_guard_medium-light_skin_tone: + U1F4821F3FC200D2642 = "💂🏼‍♂" # :man_guard_medium-light_skin_tone: + U1F4821F3FD200D2642FE0F = "💂🏽‍♂️" # :man_guard_medium_skin_tone: + U1F4821F3FD200D2642 = "💂🏽‍♂" # :man_guard_medium_skin_tone: + U1F468200D2695FE0F = "👨‍⚕️" # :man_health_worker: + U1F468200D2695 = "👨‍⚕" # :man_health_worker: + U1F4681F3FF200D2695FE0F = "👨🏿‍⚕️" # :man_health_worker_dark_skin_tone: + U1F4681F3FF200D2695 = "👨🏿‍⚕" # :man_health_worker_dark_skin_tone: + U1F4681F3FB200D2695FE0F = "👨🏻‍⚕️" # :man_health_worker_light_skin_tone: + U1F4681F3FB200D2695 = "👨🏻‍⚕" # :man_health_worker_light_skin_tone: + U1F4681F3FE200D2695FE0F = "👨🏾‍⚕️" # :man_health_worker_medium-dark_skin_tone: + U1F4681F3FE200D2695 = "👨🏾‍⚕" # :man_health_worker_medium-dark_skin_tone: + U1F4681F3FC200D2695FE0F = "👨🏼‍⚕️" # :man_health_worker_medium-light_skin_tone: + U1F4681F3FC200D2695 = "👨🏼‍⚕" # :man_health_worker_medium-light_skin_tone: + U1F4681F3FD200D2695FE0F = "👨🏽‍⚕️" # :man_health_worker_medium_skin_tone: + U1F4681F3FD200D2695 = "👨🏽‍⚕" # :man_health_worker_medium_skin_tone: + U1F9D8200D2642FE0F = "🧘‍♂️" # :man_in_lotus_position: + U1F9D8200D2642 = "🧘‍♂" # :man_in_lotus_position: + U1F9D81F3FF200D2642FE0F = "🧘🏿‍♂️" # :man_in_lotus_position_dark_skin_tone: + U1F9D81F3FF200D2642 = "🧘🏿‍♂" # :man_in_lotus_position_dark_skin_tone: + U1F9D81F3FB200D2642FE0F = "🧘🏻‍♂️" # :man_in_lotus_position_light_skin_tone: + U1F9D81F3FB200D2642 = "🧘🏻‍♂" # :man_in_lotus_position_light_skin_tone: + U1F9D81F3FE200D2642FE0F = "🧘🏾‍♂️" # :man_in_lotus_position_medium-dark_skin_tone: + U1F9D81F3FE200D2642 = "🧘🏾‍♂" # :man_in_lotus_position_medium-dark_skin_tone: + U1F9D81F3FC200D2642FE0F = "🧘🏼‍♂️" # :man_in_lotus_position_medium-light_skin_tone: + U1F9D81F3FC200D2642 = "🧘🏼‍♂" # :man_in_lotus_position_medium-light_skin_tone: + U1F9D81F3FD200D2642FE0F = "🧘🏽‍♂️" # :man_in_lotus_position_medium_skin_tone: + U1F9D81F3FD200D2642 = "🧘🏽‍♂" # :man_in_lotus_position_medium_skin_tone: + U1F468200D1F9BD = "👨‍🦽" # :man_in_manual_wheelchair: + U1F4681F3FF200D1F9BD = "👨🏿‍🦽" # :man_in_manual_wheelchair_dark_skin_tone: + U1F468200D1F9BD200D27A1FE0F = "👨‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right: + U1F468200D1F9BD200D27A1 = "👨‍🦽‍➡" # :man_in_manual_wheelchair_facing_right: + U1F4681F3FF200D1F9BD200D27A1FE0F = "👨🏿‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F4681F3FF200D1F9BD200D27A1 = "👨🏿‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F4681F3FB200D1F9BD200D27A1FE0F = "👨🏻‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_light_skin_tone: + U1F4681F3FB200D1F9BD200D27A1 = "👨🏻‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_light_skin_tone: + U1F4681F3FE200D1F9BD200D27A1FE0F = "👨🏾‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F4681F3FE200D1F9BD200D27A1 = "👨🏾‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F4681F3FC200D1F9BD200D27A1FE0F = "👨🏼‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F4681F3FC200D1F9BD200D27A1 = "👨🏼‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F4681F3FD200D1F9BD200D27A1FE0F = "👨🏽‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F4681F3FD200D1F9BD200D27A1 = "👨🏽‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F4681F3FB200D1F9BD = "👨🏻‍🦽" # :man_in_manual_wheelchair_light_skin_tone: + U1F4681F3FE200D1F9BD = "👨🏾‍🦽" # :man_in_manual_wheelchair_medium-dark_skin_tone: + U1F4681F3FC200D1F9BD = "👨🏼‍🦽" # :man_in_manual_wheelchair_medium-light_skin_tone: + U1F4681F3FD200D1F9BD = "👨🏽‍🦽" # :man_in_manual_wheelchair_medium_skin_tone: + U1F468200D1F9BC = "👨‍🦼" # :man_in_motorized_wheelchair: + U1F4681F3FF200D1F9BC = "👨🏿‍🦼" # :man_in_motorized_wheelchair_dark_skin_tone: + U1F468200D1F9BC200D27A1FE0F = "👨‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right: + U1F468200D1F9BC200D27A1 = "👨‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right: + U1F4681F3FF200D1F9BC200D27A1FE0F = "👨🏿‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F4681F3FF200D1F9BC200D27A1 = "👨🏿‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F4681F3FB200D1F9BC200D27A1FE0F = "👨🏻‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F4681F3FB200D1F9BC200D27A1 = "👨🏻‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F4681F3FE200D1F9BC200D27A1FE0F = "👨🏾‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F4681F3FE200D1F9BC200D27A1 = "👨🏾‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F4681F3FC200D1F9BC200D27A1FE0F = "👨🏼‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F4681F3FC200D1F9BC200D27A1 = "👨🏼‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F4681F3FD200D1F9BC200D27A1FE0F = "👨🏽‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F4681F3FD200D1F9BC200D27A1 = "👨🏽‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F4681F3FB200D1F9BC = "👨🏻‍🦼" # :man_in_motorized_wheelchair_light_skin_tone: + U1F4681F3FE200D1F9BC = "👨🏾‍🦼" # :man_in_motorized_wheelchair_medium-dark_skin_tone: + U1F4681F3FC200D1F9BC = "👨🏼‍🦼" # :man_in_motorized_wheelchair_medium-light_skin_tone: + U1F4681F3FD200D1F9BC = "👨🏽‍🦼" # :man_in_motorized_wheelchair_medium_skin_tone: + U1F9D6200D2642FE0F = "🧖‍♂️" # :man_in_steamy_room: + U1F9D6200D2642 = "🧖‍♂" # :man_in_steamy_room: + U1F9D61F3FF200D2642FE0F = "🧖🏿‍♂️" # :man_in_steamy_room_dark_skin_tone: + U1F9D61F3FF200D2642 = "🧖🏿‍♂" # :man_in_steamy_room_dark_skin_tone: + U1F9D61F3FB200D2642FE0F = "🧖🏻‍♂️" # :man_in_steamy_room_light_skin_tone: + U1F9D61F3FB200D2642 = "🧖🏻‍♂" # :man_in_steamy_room_light_skin_tone: + U1F9D61F3FE200D2642FE0F = "🧖🏾‍♂️" # :man_in_steamy_room_medium-dark_skin_tone: + U1F9D61F3FE200D2642 = "🧖🏾‍♂" # :man_in_steamy_room_medium-dark_skin_tone: + U1F9D61F3FC200D2642FE0F = "🧖🏼‍♂️" # :man_in_steamy_room_medium-light_skin_tone: + U1F9D61F3FC200D2642 = "🧖🏼‍♂" # :man_in_steamy_room_medium-light_skin_tone: + U1F9D61F3FD200D2642FE0F = "🧖🏽‍♂️" # :man_in_steamy_room_medium_skin_tone: + U1F9D61F3FD200D2642 = "🧖🏽‍♂" # :man_in_steamy_room_medium_skin_tone: + U1F935200D2642FE0F = "🤵‍♂️" # :man_in_tuxedo: + U1F935200D2642 = "🤵‍♂" # :man_in_tuxedo: + U1F9351F3FF200D2642FE0F = "🤵🏿‍♂️" # :man_in_tuxedo_dark_skin_tone: + U1F9351F3FF200D2642 = "🤵🏿‍♂" # :man_in_tuxedo_dark_skin_tone: + U1F9351F3FB200D2642FE0F = "🤵🏻‍♂️" # :man_in_tuxedo_light_skin_tone: + U1F9351F3FB200D2642 = "🤵🏻‍♂" # :man_in_tuxedo_light_skin_tone: + U1F9351F3FE200D2642FE0F = "🤵🏾‍♂️" # :man_in_tuxedo_medium-dark_skin_tone: + U1F9351F3FE200D2642 = "🤵🏾‍♂" # :man_in_tuxedo_medium-dark_skin_tone: + U1F9351F3FC200D2642FE0F = "🤵🏼‍♂️" # :man_in_tuxedo_medium-light_skin_tone: + U1F9351F3FC200D2642 = "🤵🏼‍♂" # :man_in_tuxedo_medium-light_skin_tone: + U1F9351F3FD200D2642FE0F = "🤵🏽‍♂️" # :man_in_tuxedo_medium_skin_tone: + U1F9351F3FD200D2642 = "🤵🏽‍♂" # :man_in_tuxedo_medium_skin_tone: + U1F468200D2696FE0F = "👨‍⚖️" # :man_judge: + U1F468200D2696 = "👨‍⚖" # :man_judge: + U1F4681F3FF200D2696FE0F = "👨🏿‍⚖️" # :man_judge_dark_skin_tone: + U1F4681F3FF200D2696 = "👨🏿‍⚖" # :man_judge_dark_skin_tone: + U1F4681F3FB200D2696FE0F = "👨🏻‍⚖️" # :man_judge_light_skin_tone: + U1F4681F3FB200D2696 = "👨🏻‍⚖" # :man_judge_light_skin_tone: + U1F4681F3FE200D2696FE0F = "👨🏾‍⚖️" # :man_judge_medium-dark_skin_tone: + U1F4681F3FE200D2696 = "👨🏾‍⚖" # :man_judge_medium-dark_skin_tone: + U1F4681F3FC200D2696FE0F = "👨🏼‍⚖️" # :man_judge_medium-light_skin_tone: + U1F4681F3FC200D2696 = "👨🏼‍⚖" # :man_judge_medium-light_skin_tone: + U1F4681F3FD200D2696FE0F = "👨🏽‍⚖️" # :man_judge_medium_skin_tone: + U1F4681F3FD200D2696 = "👨🏽‍⚖" # :man_judge_medium_skin_tone: + U1F939200D2642FE0F = "🤹‍♂️" # :man_juggling: + U1F939200D2642 = "🤹‍♂" # :man_juggling: + U1F9391F3FF200D2642FE0F = "🤹🏿‍♂️" # :man_juggling_dark_skin_tone: + U1F9391F3FF200D2642 = "🤹🏿‍♂" # :man_juggling_dark_skin_tone: + U1F9391F3FB200D2642FE0F = "🤹🏻‍♂️" # :man_juggling_light_skin_tone: + U1F9391F3FB200D2642 = "🤹🏻‍♂" # :man_juggling_light_skin_tone: + U1F9391F3FE200D2642FE0F = "🤹🏾‍♂️" # :man_juggling_medium-dark_skin_tone: + U1F9391F3FE200D2642 = "🤹🏾‍♂" # :man_juggling_medium-dark_skin_tone: + U1F9391F3FC200D2642FE0F = "🤹🏼‍♂️" # :man_juggling_medium-light_skin_tone: + U1F9391F3FC200D2642 = "🤹🏼‍♂" # :man_juggling_medium-light_skin_tone: + U1F9391F3FD200D2642FE0F = "🤹🏽‍♂️" # :man_juggling_medium_skin_tone: + U1F9391F3FD200D2642 = "🤹🏽‍♂" # :man_juggling_medium_skin_tone: + U1F9CE200D2642FE0F = "🧎‍♂️" # :man_kneeling: + U1F9CE200D2642 = "🧎‍♂" # :man_kneeling: + U1F9CE1F3FF200D2642FE0F = "🧎🏿‍♂️" # :man_kneeling_dark_skin_tone: + U1F9CE1F3FF200D2642 = "🧎🏿‍♂" # :man_kneeling_dark_skin_tone: + U1F9CE200D2642FE0F200D27A1FE0F = "🧎‍♂️‍➡️" # :man_kneeling_facing_right: + U1F9CE200D2642200D27A1FE0F = "🧎‍♂‍➡️" # :man_kneeling_facing_right: + U1F9CE200D2642FE0F200D27A1 = "🧎‍♂️‍➡" # :man_kneeling_facing_right: + U1F9CE200D2642200D27A1 = "🧎‍♂‍➡" # :man_kneeling_facing_right: + U1F9CE1F3FF200D2642FE0F200D27A1FE0F = "🧎🏿‍♂️‍➡️" # :man_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2642200D27A1FE0F = "🧎🏿‍♂‍➡️" # :man_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2642FE0F200D27A1 = "🧎🏿‍♂️‍➡" # :man_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2642200D27A1 = "🧎🏿‍♂‍➡" # :man_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FB200D2642FE0F200D27A1FE0F = "🧎🏻‍♂️‍➡️" # :man_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2642200D27A1FE0F = "🧎🏻‍♂‍➡️" # :man_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2642FE0F200D27A1 = "🧎🏻‍♂️‍➡" # :man_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2642200D27A1 = "🧎🏻‍♂‍➡" # :man_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FE200D2642FE0F200D27A1FE0F = "🧎🏾‍♂️‍➡️" # :man_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2642200D27A1FE0F = "🧎🏾‍♂‍➡️" # :man_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2642FE0F200D27A1 = "🧎🏾‍♂️‍➡" # :man_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2642200D27A1 = "🧎🏾‍♂‍➡" # :man_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FC200D2642FE0F200D27A1FE0F = "🧎🏼‍♂️‍➡️" # :man_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2642200D27A1FE0F = "🧎🏼‍♂‍➡️" # :man_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2642FE0F200D27A1 = "🧎🏼‍♂️‍➡" # :man_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2642200D27A1 = "🧎🏼‍♂‍➡" # :man_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FD200D2642FE0F200D27A1FE0F = "🧎🏽‍♂️‍➡️" # :man_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2642200D27A1FE0F = "🧎🏽‍♂‍➡️" # :man_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2642FE0F200D27A1 = "🧎🏽‍♂️‍➡" # :man_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2642200D27A1 = "🧎🏽‍♂‍➡" # :man_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FB200D2642FE0F = "🧎🏻‍♂️" # :man_kneeling_light_skin_tone: + U1F9CE1F3FB200D2642 = "🧎🏻‍♂" # :man_kneeling_light_skin_tone: + U1F9CE1F3FE200D2642FE0F = "🧎🏾‍♂️" # :man_kneeling_medium-dark_skin_tone: + U1F9CE1F3FE200D2642 = "🧎🏾‍♂" # :man_kneeling_medium-dark_skin_tone: + U1F9CE1F3FC200D2642FE0F = "🧎🏼‍♂️" # :man_kneeling_medium-light_skin_tone: + U1F9CE1F3FC200D2642 = "🧎🏼‍♂" # :man_kneeling_medium-light_skin_tone: + U1F9CE1F3FD200D2642FE0F = "🧎🏽‍♂️" # :man_kneeling_medium_skin_tone: + U1F9CE1F3FD200D2642 = "🧎🏽‍♂" # :man_kneeling_medium_skin_tone: + U1F3CBFE0F200D2642FE0F = "🏋️‍♂️" # :man_lifting_weights: + U1F3CB200D2642FE0F = "🏋‍♂️" # :man_lifting_weights: + U1F3CBFE0F200D2642 = "🏋️‍♂" # :man_lifting_weights: + U1F3CB200D2642 = "🏋‍♂" # :man_lifting_weights: + U1F3CB1F3FF200D2642FE0F = "🏋🏿‍♂️" # :man_lifting_weights_dark_skin_tone: + U1F3CB1F3FF200D2642 = "🏋🏿‍♂" # :man_lifting_weights_dark_skin_tone: + U1F3CB1F3FB200D2642FE0F = "🏋🏻‍♂️" # :man_lifting_weights_light_skin_tone: + U1F3CB1F3FB200D2642 = "🏋🏻‍♂" # :man_lifting_weights_light_skin_tone: + U1F3CB1F3FE200D2642FE0F = "🏋🏾‍♂️" # :man_lifting_weights_medium-dark_skin_tone: + U1F3CB1F3FE200D2642 = "🏋🏾‍♂" # :man_lifting_weights_medium-dark_skin_tone: + U1F3CB1F3FC200D2642FE0F = "🏋🏼‍♂️" # :man_lifting_weights_medium-light_skin_tone: + U1F3CB1F3FC200D2642 = "🏋🏼‍♂" # :man_lifting_weights_medium-light_skin_tone: + U1F3CB1F3FD200D2642FE0F = "🏋🏽‍♂️" # :man_lifting_weights_medium_skin_tone: + U1F3CB1F3FD200D2642 = "🏋🏽‍♂" # :man_lifting_weights_medium_skin_tone: + U1F4681F3FB = "👨🏻" # :man_light_skin_tone: + U1F4681F3FB200D1F9B2 = "👨🏻‍🦲" # :man_light_skin_tone_bald: + U1F9D41F3FB200D2642FE0F = "🧔🏻‍♂️" # :man_light_skin_tone_beard: + U1F9D41F3FB200D2642 = "🧔🏻‍♂" # :man_light_skin_tone_beard: + U1F4711F3FB200D2642FE0F = "👱🏻‍♂️" # :man_light_skin_tone_blond_hair: + U1F4711F3FB200D2642 = "👱🏻‍♂" # :man_light_skin_tone_blond_hair: + U1F4681F3FB200D1F9B1 = "👨🏻‍🦱" # :man_light_skin_tone_curly_hair: + U1F4681F3FB200D1F9B0 = "👨🏻‍🦰" # :man_light_skin_tone_red_hair: + U1F4681F3FB200D1F9B3 = "👨🏻‍🦳" # :man_light_skin_tone_white_hair: + U1F9D9200D2642FE0F = "🧙‍♂️" # :man_mage: + U1F9D9200D2642 = "🧙‍♂" # :man_mage: + U1F9D91F3FF200D2642FE0F = "🧙🏿‍♂️" # :man_mage_dark_skin_tone: + U1F9D91F3FF200D2642 = "🧙🏿‍♂" # :man_mage_dark_skin_tone: + U1F9D91F3FB200D2642FE0F = "🧙🏻‍♂️" # :man_mage_light_skin_tone: + U1F9D91F3FB200D2642 = "🧙🏻‍♂" # :man_mage_light_skin_tone: + U1F9D91F3FE200D2642FE0F = "🧙🏾‍♂️" # :man_mage_medium-dark_skin_tone: + U1F9D91F3FE200D2642 = "🧙🏾‍♂" # :man_mage_medium-dark_skin_tone: + U1F9D91F3FC200D2642FE0F = "🧙🏼‍♂️" # :man_mage_medium-light_skin_tone: + U1F9D91F3FC200D2642 = "🧙🏼‍♂" # :man_mage_medium-light_skin_tone: + U1F9D91F3FD200D2642FE0F = "🧙🏽‍♂️" # :man_mage_medium_skin_tone: + U1F9D91F3FD200D2642 = "🧙🏽‍♂" # :man_mage_medium_skin_tone: + U1F468200D1F527 = "👨‍🔧" # :man_mechanic: + U1F4681F3FF200D1F527 = "👨🏿‍🔧" # :man_mechanic_dark_skin_tone: + U1F4681F3FB200D1F527 = "👨🏻‍🔧" # :man_mechanic_light_skin_tone: + U1F4681F3FE200D1F527 = "👨🏾‍🔧" # :man_mechanic_medium-dark_skin_tone: + U1F4681F3FC200D1F527 = "👨🏼‍🔧" # :man_mechanic_medium-light_skin_tone: + U1F4681F3FD200D1F527 = "👨🏽‍🔧" # :man_mechanic_medium_skin_tone: + U1F4681F3FE = "👨🏾" # :man_medium-dark_skin_tone: + U1F4681F3FE200D1F9B2 = "👨🏾‍🦲" # :man_medium-dark_skin_tone_bald: + U1F9D41F3FE200D2642FE0F = "🧔🏾‍♂️" # :man_medium-dark_skin_tone_beard: + U1F9D41F3FE200D2642 = "🧔🏾‍♂" # :man_medium-dark_skin_tone_beard: + U1F4711F3FE200D2642FE0F = "👱🏾‍♂️" # :man_medium-dark_skin_tone_blond_hair: + U1F4711F3FE200D2642 = "👱🏾‍♂" # :man_medium-dark_skin_tone_blond_hair: + U1F4681F3FE200D1F9B1 = "👨🏾‍🦱" # :man_medium-dark_skin_tone_curly_hair: + U1F4681F3FE200D1F9B0 = "👨🏾‍🦰" # :man_medium-dark_skin_tone_red_hair: + U1F4681F3FE200D1F9B3 = "👨🏾‍🦳" # :man_medium-dark_skin_tone_white_hair: + U1F4681F3FC = "👨🏼" # :man_medium-light_skin_tone: + U1F4681F3FC200D1F9B2 = "👨🏼‍🦲" # :man_medium-light_skin_tone_bald: + U1F9D41F3FC200D2642FE0F = "🧔🏼‍♂️" # :man_medium-light_skin_tone_beard: + U1F9D41F3FC200D2642 = "🧔🏼‍♂" # :man_medium-light_skin_tone_beard: + U1F4711F3FC200D2642FE0F = "👱🏼‍♂️" # :man_medium-light_skin_tone_blond_hair: + U1F4711F3FC200D2642 = "👱🏼‍♂" # :man_medium-light_skin_tone_blond_hair: + U1F4681F3FC200D1F9B1 = "👨🏼‍🦱" # :man_medium-light_skin_tone_curly_hair: + U1F4681F3FC200D1F9B0 = "👨🏼‍🦰" # :man_medium-light_skin_tone_red_hair: + U1F4681F3FC200D1F9B3 = "👨🏼‍🦳" # :man_medium-light_skin_tone_white_hair: + U1F4681F3FD = "👨🏽" # :man_medium_skin_tone: + U1F4681F3FD200D1F9B2 = "👨🏽‍🦲" # :man_medium_skin_tone_bald: + U1F9D41F3FD200D2642FE0F = "🧔🏽‍♂️" # :man_medium_skin_tone_beard: + U1F9D41F3FD200D2642 = "🧔🏽‍♂" # :man_medium_skin_tone_beard: + U1F4711F3FD200D2642FE0F = "👱🏽‍♂️" # :man_medium_skin_tone_blond_hair: + U1F4711F3FD200D2642 = "👱🏽‍♂" # :man_medium_skin_tone_blond_hair: + U1F4681F3FD200D1F9B1 = "👨🏽‍🦱" # :man_medium_skin_tone_curly_hair: + U1F4681F3FD200D1F9B0 = "👨🏽‍🦰" # :man_medium_skin_tone_red_hair: + U1F4681F3FD200D1F9B3 = "👨🏽‍🦳" # :man_medium_skin_tone_white_hair: + U1F6B5200D2642FE0F = "🚵‍♂️" # :man_mountain_biking: + U1F6B5200D2642 = "🚵‍♂" # :man_mountain_biking: + U1F6B51F3FF200D2642FE0F = "🚵🏿‍♂️" # :man_mountain_biking_dark_skin_tone: + U1F6B51F3FF200D2642 = "🚵🏿‍♂" # :man_mountain_biking_dark_skin_tone: + U1F6B51F3FB200D2642FE0F = "🚵🏻‍♂️" # :man_mountain_biking_light_skin_tone: + U1F6B51F3FB200D2642 = "🚵🏻‍♂" # :man_mountain_biking_light_skin_tone: + U1F6B51F3FE200D2642FE0F = "🚵🏾‍♂️" # :man_mountain_biking_medium-dark_skin_tone: + U1F6B51F3FE200D2642 = "🚵🏾‍♂" # :man_mountain_biking_medium-dark_skin_tone: + U1F6B51F3FC200D2642FE0F = "🚵🏼‍♂️" # :man_mountain_biking_medium-light_skin_tone: + U1F6B51F3FC200D2642 = "🚵🏼‍♂" # :man_mountain_biking_medium-light_skin_tone: + U1F6B51F3FD200D2642FE0F = "🚵🏽‍♂️" # :man_mountain_biking_medium_skin_tone: + U1F6B51F3FD200D2642 = "🚵🏽‍♂" # :man_mountain_biking_medium_skin_tone: + U1F468200D1F4BC = "👨‍💼" # :man_office_worker: + U1F4681F3FF200D1F4BC = "👨🏿‍💼" # :man_office_worker_dark_skin_tone: + U1F4681F3FB200D1F4BC = "👨🏻‍💼" # :man_office_worker_light_skin_tone: + U1F4681F3FE200D1F4BC = "👨🏾‍💼" # :man_office_worker_medium-dark_skin_tone: + U1F4681F3FC200D1F4BC = "👨🏼‍💼" # :man_office_worker_medium-light_skin_tone: + U1F4681F3FD200D1F4BC = "👨🏽‍💼" # :man_office_worker_medium_skin_tone: + U1F468200D2708FE0F = "👨‍✈️" # :man_pilot: + U1F468200D2708 = "👨‍✈" # :man_pilot: + U1F4681F3FF200D2708FE0F = "👨🏿‍✈️" # :man_pilot_dark_skin_tone: + U1F4681F3FF200D2708 = "👨🏿‍✈" # :man_pilot_dark_skin_tone: + U1F4681F3FB200D2708FE0F = "👨🏻‍✈️" # :man_pilot_light_skin_tone: + U1F4681F3FB200D2708 = "👨🏻‍✈" # :man_pilot_light_skin_tone: + U1F4681F3FE200D2708FE0F = "👨🏾‍✈️" # :man_pilot_medium-dark_skin_tone: + U1F4681F3FE200D2708 = "👨🏾‍✈" # :man_pilot_medium-dark_skin_tone: + U1F4681F3FC200D2708FE0F = "👨🏼‍✈️" # :man_pilot_medium-light_skin_tone: + U1F4681F3FC200D2708 = "👨🏼‍✈" # :man_pilot_medium-light_skin_tone: + U1F4681F3FD200D2708FE0F = "👨🏽‍✈️" # :man_pilot_medium_skin_tone: + U1F4681F3FD200D2708 = "👨🏽‍✈" # :man_pilot_medium_skin_tone: + U1F93E200D2642FE0F = "🤾‍♂️" # :man_playing_handball: + U1F93E200D2642 = "🤾‍♂" # :man_playing_handball: + U1F93E1F3FF200D2642FE0F = "🤾🏿‍♂️" # :man_playing_handball_dark_skin_tone: + U1F93E1F3FF200D2642 = "🤾🏿‍♂" # :man_playing_handball_dark_skin_tone: + U1F93E1F3FB200D2642FE0F = "🤾🏻‍♂️" # :man_playing_handball_light_skin_tone: + U1F93E1F3FB200D2642 = "🤾🏻‍♂" # :man_playing_handball_light_skin_tone: + U1F93E1F3FE200D2642FE0F = "🤾🏾‍♂️" # :man_playing_handball_medium-dark_skin_tone: + U1F93E1F3FE200D2642 = "🤾🏾‍♂" # :man_playing_handball_medium-dark_skin_tone: + U1F93E1F3FC200D2642FE0F = "🤾🏼‍♂️" # :man_playing_handball_medium-light_skin_tone: + U1F93E1F3FC200D2642 = "🤾🏼‍♂" # :man_playing_handball_medium-light_skin_tone: + U1F93E1F3FD200D2642FE0F = "🤾🏽‍♂️" # :man_playing_handball_medium_skin_tone: + U1F93E1F3FD200D2642 = "🤾🏽‍♂" # :man_playing_handball_medium_skin_tone: + U1F93D200D2642FE0F = "🤽‍♂️" # :man_playing_water_polo: + U1F93D200D2642 = "🤽‍♂" # :man_playing_water_polo: + U1F93D1F3FF200D2642FE0F = "🤽🏿‍♂️" # :man_playing_water_polo_dark_skin_tone: + U1F93D1F3FF200D2642 = "🤽🏿‍♂" # :man_playing_water_polo_dark_skin_tone: + U1F93D1F3FB200D2642FE0F = "🤽🏻‍♂️" # :man_playing_water_polo_light_skin_tone: + U1F93D1F3FB200D2642 = "🤽🏻‍♂" # :man_playing_water_polo_light_skin_tone: + U1F93D1F3FE200D2642FE0F = "🤽🏾‍♂️" # :man_playing_water_polo_medium-dark_skin_tone: + U1F93D1F3FE200D2642 = "🤽🏾‍♂" # :man_playing_water_polo_medium-dark_skin_tone: + U1F93D1F3FC200D2642FE0F = "🤽🏼‍♂️" # :man_playing_water_polo_medium-light_skin_tone: + U1F93D1F3FC200D2642 = "🤽🏼‍♂" # :man_playing_water_polo_medium-light_skin_tone: + U1F93D1F3FD200D2642FE0F = "🤽🏽‍♂️" # :man_playing_water_polo_medium_skin_tone: + U1F93D1F3FD200D2642 = "🤽🏽‍♂" # :man_playing_water_polo_medium_skin_tone: + U1F46E200D2642FE0F = "👮‍♂️" # :man_police_officer: + U1F46E200D2642 = "👮‍♂" # :man_police_officer: + U1F46E1F3FF200D2642FE0F = "👮🏿‍♂️" # :man_police_officer_dark_skin_tone: + U1F46E1F3FF200D2642 = "👮🏿‍♂" # :man_police_officer_dark_skin_tone: + U1F46E1F3FB200D2642FE0F = "👮🏻‍♂️" # :man_police_officer_light_skin_tone: + U1F46E1F3FB200D2642 = "👮🏻‍♂" # :man_police_officer_light_skin_tone: + U1F46E1F3FE200D2642FE0F = "👮🏾‍♂️" # :man_police_officer_medium-dark_skin_tone: + U1F46E1F3FE200D2642 = "👮🏾‍♂" # :man_police_officer_medium-dark_skin_tone: + U1F46E1F3FC200D2642FE0F = "👮🏼‍♂️" # :man_police_officer_medium-light_skin_tone: + U1F46E1F3FC200D2642 = "👮🏼‍♂" # :man_police_officer_medium-light_skin_tone: + U1F46E1F3FD200D2642FE0F = "👮🏽‍♂️" # :man_police_officer_medium_skin_tone: + U1F46E1F3FD200D2642 = "👮🏽‍♂" # :man_police_officer_medium_skin_tone: + U1F64E200D2642FE0F = "🙎‍♂️" # :man_pouting: + U1F64E200D2642 = "🙎‍♂" # :man_pouting: + U1F64E1F3FF200D2642FE0F = "🙎🏿‍♂️" # :man_pouting_dark_skin_tone: + U1F64E1F3FF200D2642 = "🙎🏿‍♂" # :man_pouting_dark_skin_tone: + U1F64E1F3FB200D2642FE0F = "🙎🏻‍♂️" # :man_pouting_light_skin_tone: + U1F64E1F3FB200D2642 = "🙎🏻‍♂" # :man_pouting_light_skin_tone: + U1F64E1F3FE200D2642FE0F = "🙎🏾‍♂️" # :man_pouting_medium-dark_skin_tone: + U1F64E1F3FE200D2642 = "🙎🏾‍♂" # :man_pouting_medium-dark_skin_tone: + U1F64E1F3FC200D2642FE0F = "🙎🏼‍♂️" # :man_pouting_medium-light_skin_tone: + U1F64E1F3FC200D2642 = "🙎🏼‍♂" # :man_pouting_medium-light_skin_tone: + U1F64E1F3FD200D2642FE0F = "🙎🏽‍♂️" # :man_pouting_medium_skin_tone: + U1F64E1F3FD200D2642 = "🙎🏽‍♂" # :man_pouting_medium_skin_tone: + U1F64B200D2642FE0F = "🙋‍♂️" # :man_raising_hand: + U1F64B200D2642 = "🙋‍♂" # :man_raising_hand: + U1F64B1F3FF200D2642FE0F = "🙋🏿‍♂️" # :man_raising_hand_dark_skin_tone: + U1F64B1F3FF200D2642 = "🙋🏿‍♂" # :man_raising_hand_dark_skin_tone: + U1F64B1F3FB200D2642FE0F = "🙋🏻‍♂️" # :man_raising_hand_light_skin_tone: + U1F64B1F3FB200D2642 = "🙋🏻‍♂" # :man_raising_hand_light_skin_tone: + U1F64B1F3FE200D2642FE0F = "🙋🏾‍♂️" # :man_raising_hand_medium-dark_skin_tone: + U1F64B1F3FE200D2642 = "🙋🏾‍♂" # :man_raising_hand_medium-dark_skin_tone: + U1F64B1F3FC200D2642FE0F = "🙋🏼‍♂️" # :man_raising_hand_medium-light_skin_tone: + U1F64B1F3FC200D2642 = "🙋🏼‍♂" # :man_raising_hand_medium-light_skin_tone: + U1F64B1F3FD200D2642FE0F = "🙋🏽‍♂️" # :man_raising_hand_medium_skin_tone: + U1F64B1F3FD200D2642 = "🙋🏽‍♂" # :man_raising_hand_medium_skin_tone: + U1F468200D1F9B0 = "👨‍🦰" # :man_red_hair: + U1F6A3200D2642FE0F = "🚣‍♂️" # :man_rowing_boat: + U1F6A3200D2642 = "🚣‍♂" # :man_rowing_boat: + U1F6A31F3FF200D2642FE0F = "🚣🏿‍♂️" # :man_rowing_boat_dark_skin_tone: + U1F6A31F3FF200D2642 = "🚣🏿‍♂" # :man_rowing_boat_dark_skin_tone: + U1F6A31F3FB200D2642FE0F = "🚣🏻‍♂️" # :man_rowing_boat_light_skin_tone: + U1F6A31F3FB200D2642 = "🚣🏻‍♂" # :man_rowing_boat_light_skin_tone: + U1F6A31F3FE200D2642FE0F = "🚣🏾‍♂️" # :man_rowing_boat_medium-dark_skin_tone: + U1F6A31F3FE200D2642 = "🚣🏾‍♂" # :man_rowing_boat_medium-dark_skin_tone: + U1F6A31F3FC200D2642FE0F = "🚣🏼‍♂️" # :man_rowing_boat_medium-light_skin_tone: + U1F6A31F3FC200D2642 = "🚣🏼‍♂" # :man_rowing_boat_medium-light_skin_tone: + U1F6A31F3FD200D2642FE0F = "🚣🏽‍♂️" # :man_rowing_boat_medium_skin_tone: + U1F6A31F3FD200D2642 = "🚣🏽‍♂" # :man_rowing_boat_medium_skin_tone: + U1F3C3200D2642FE0F = "🏃‍♂️" # :man_running: + U1F3C3200D2642 = "🏃‍♂" # :man_running: + U1F3C31F3FF200D2642FE0F = "🏃🏿‍♂️" # :man_running_dark_skin_tone: + U1F3C31F3FF200D2642 = "🏃🏿‍♂" # :man_running_dark_skin_tone: + U1F3C3200D2642FE0F200D27A1FE0F = "🏃‍♂️‍➡️" # :man_running_facing_right: + U1F3C3200D2642200D27A1FE0F = "🏃‍♂‍➡️" # :man_running_facing_right: + U1F3C3200D2642FE0F200D27A1 = "🏃‍♂️‍➡" # :man_running_facing_right: + U1F3C3200D2642200D27A1 = "🏃‍♂‍➡" # :man_running_facing_right: + U1F3C31F3FF200D2642FE0F200D27A1FE0F = "🏃🏿‍♂️‍➡️" # :man_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2642200D27A1FE0F = "🏃🏿‍♂‍➡️" # :man_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2642FE0F200D27A1 = "🏃🏿‍♂️‍➡" # :man_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2642200D27A1 = "🏃🏿‍♂‍➡" # :man_running_facing_right_dark_skin_tone: + U1F3C31F3FB200D2642FE0F200D27A1FE0F = "🏃🏻‍♂️‍➡️" # :man_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2642200D27A1FE0F = "🏃🏻‍♂‍➡️" # :man_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2642FE0F200D27A1 = "🏃🏻‍♂️‍➡" # :man_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2642200D27A1 = "🏃🏻‍♂‍➡" # :man_running_facing_right_light_skin_tone: + U1F3C31F3FE200D2642FE0F200D27A1FE0F = "🏃🏾‍♂️‍➡️" # :man_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2642200D27A1FE0F = "🏃🏾‍♂‍➡️" # :man_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2642FE0F200D27A1 = "🏃🏾‍♂️‍➡" # :man_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2642200D27A1 = "🏃🏾‍♂‍➡" # :man_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FC200D2642FE0F200D27A1FE0F = "🏃🏼‍♂️‍➡️" # :man_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2642200D27A1FE0F = "🏃🏼‍♂‍➡️" # :man_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2642FE0F200D27A1 = "🏃🏼‍♂️‍➡" # :man_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2642200D27A1 = "🏃🏼‍♂‍➡" # :man_running_facing_right_medium-light_skin_tone: + U1F3C31F3FD200D2642FE0F200D27A1FE0F = "🏃🏽‍♂️‍➡️" # :man_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2642200D27A1FE0F = "🏃🏽‍♂‍➡️" # :man_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2642FE0F200D27A1 = "🏃🏽‍♂️‍➡" # :man_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2642200D27A1 = "🏃🏽‍♂‍➡" # :man_running_facing_right_medium_skin_tone: + U1F3C31F3FB200D2642FE0F = "🏃🏻‍♂️" # :man_running_light_skin_tone: + U1F3C31F3FB200D2642 = "🏃🏻‍♂" # :man_running_light_skin_tone: + U1F3C31F3FE200D2642FE0F = "🏃🏾‍♂️" # :man_running_medium-dark_skin_tone: + U1F3C31F3FE200D2642 = "🏃🏾‍♂" # :man_running_medium-dark_skin_tone: + U1F3C31F3FC200D2642FE0F = "🏃🏼‍♂️" # :man_running_medium-light_skin_tone: + U1F3C31F3FC200D2642 = "🏃🏼‍♂" # :man_running_medium-light_skin_tone: + U1F3C31F3FD200D2642FE0F = "🏃🏽‍♂️" # :man_running_medium_skin_tone: + U1F3C31F3FD200D2642 = "🏃🏽‍♂" # :man_running_medium_skin_tone: + U1F468200D1F52C = "👨‍🔬" # :man_scientist: + U1F4681F3FF200D1F52C = "👨🏿‍🔬" # :man_scientist_dark_skin_tone: + U1F4681F3FB200D1F52C = "👨🏻‍🔬" # :man_scientist_light_skin_tone: + U1F4681F3FE200D1F52C = "👨🏾‍🔬" # :man_scientist_medium-dark_skin_tone: + U1F4681F3FC200D1F52C = "👨🏼‍🔬" # :man_scientist_medium-light_skin_tone: + U1F4681F3FD200D1F52C = "👨🏽‍🔬" # :man_scientist_medium_skin_tone: + U1F937200D2642FE0F = "🤷‍♂️" # :man_shrugging: + U1F937200D2642 = "🤷‍♂" # :man_shrugging: + U1F9371F3FF200D2642FE0F = "🤷🏿‍♂️" # :man_shrugging_dark_skin_tone: + U1F9371F3FF200D2642 = "🤷🏿‍♂" # :man_shrugging_dark_skin_tone: + U1F9371F3FB200D2642FE0F = "🤷🏻‍♂️" # :man_shrugging_light_skin_tone: + U1F9371F3FB200D2642 = "🤷🏻‍♂" # :man_shrugging_light_skin_tone: + U1F9371F3FE200D2642FE0F = "🤷🏾‍♂️" # :man_shrugging_medium-dark_skin_tone: + U1F9371F3FE200D2642 = "🤷🏾‍♂" # :man_shrugging_medium-dark_skin_tone: + U1F9371F3FC200D2642FE0F = "🤷🏼‍♂️" # :man_shrugging_medium-light_skin_tone: + U1F9371F3FC200D2642 = "🤷🏼‍♂" # :man_shrugging_medium-light_skin_tone: + U1F9371F3FD200D2642FE0F = "🤷🏽‍♂️" # :man_shrugging_medium_skin_tone: + U1F9371F3FD200D2642 = "🤷🏽‍♂" # :man_shrugging_medium_skin_tone: + U1F468200D1F3A4 = "👨‍🎤" # :man_singer: + U1F4681F3FF200D1F3A4 = "👨🏿‍🎤" # :man_singer_dark_skin_tone: + U1F4681F3FB200D1F3A4 = "👨🏻‍🎤" # :man_singer_light_skin_tone: + U1F4681F3FE200D1F3A4 = "👨🏾‍🎤" # :man_singer_medium-dark_skin_tone: + U1F4681F3FC200D1F3A4 = "👨🏼‍🎤" # :man_singer_medium-light_skin_tone: + U1F4681F3FD200D1F3A4 = "👨🏽‍🎤" # :man_singer_medium_skin_tone: + U1F9CD200D2642FE0F = "🧍‍♂️" # :man_standing: + U1F9CD200D2642 = "🧍‍♂" # :man_standing: + U1F9CD1F3FF200D2642FE0F = "🧍🏿‍♂️" # :man_standing_dark_skin_tone: + U1F9CD1F3FF200D2642 = "🧍🏿‍♂" # :man_standing_dark_skin_tone: + U1F9CD1F3FB200D2642FE0F = "🧍🏻‍♂️" # :man_standing_light_skin_tone: + U1F9CD1F3FB200D2642 = "🧍🏻‍♂" # :man_standing_light_skin_tone: + U1F9CD1F3FE200D2642FE0F = "🧍🏾‍♂️" # :man_standing_medium-dark_skin_tone: + U1F9CD1F3FE200D2642 = "🧍🏾‍♂" # :man_standing_medium-dark_skin_tone: + U1F9CD1F3FC200D2642FE0F = "🧍🏼‍♂️" # :man_standing_medium-light_skin_tone: + U1F9CD1F3FC200D2642 = "🧍🏼‍♂" # :man_standing_medium-light_skin_tone: + U1F9CD1F3FD200D2642FE0F = "🧍🏽‍♂️" # :man_standing_medium_skin_tone: + U1F9CD1F3FD200D2642 = "🧍🏽‍♂" # :man_standing_medium_skin_tone: + U1F468200D1F393 = "👨‍🎓" # :man_student: + U1F4681F3FF200D1F393 = "👨🏿‍🎓" # :man_student_dark_skin_tone: + U1F4681F3FB200D1F393 = "👨🏻‍🎓" # :man_student_light_skin_tone: + U1F4681F3FE200D1F393 = "👨🏾‍🎓" # :man_student_medium-dark_skin_tone: + U1F4681F3FC200D1F393 = "👨🏼‍🎓" # :man_student_medium-light_skin_tone: + U1F4681F3FD200D1F393 = "👨🏽‍🎓" # :man_student_medium_skin_tone: + U1F9B8200D2642FE0F = "🦸‍♂️" # :man_superhero: + U1F9B8200D2642 = "🦸‍♂" # :man_superhero: + U1F9B81F3FF200D2642FE0F = "🦸🏿‍♂️" # :man_superhero_dark_skin_tone: + U1F9B81F3FF200D2642 = "🦸🏿‍♂" # :man_superhero_dark_skin_tone: + U1F9B81F3FB200D2642FE0F = "🦸🏻‍♂️" # :man_superhero_light_skin_tone: + U1F9B81F3FB200D2642 = "🦸🏻‍♂" # :man_superhero_light_skin_tone: + U1F9B81F3FE200D2642FE0F = "🦸🏾‍♂️" # :man_superhero_medium-dark_skin_tone: + U1F9B81F3FE200D2642 = "🦸🏾‍♂" # :man_superhero_medium-dark_skin_tone: + U1F9B81F3FC200D2642FE0F = "🦸🏼‍♂️" # :man_superhero_medium-light_skin_tone: + U1F9B81F3FC200D2642 = "🦸🏼‍♂" # :man_superhero_medium-light_skin_tone: + U1F9B81F3FD200D2642FE0F = "🦸🏽‍♂️" # :man_superhero_medium_skin_tone: + U1F9B81F3FD200D2642 = "🦸🏽‍♂" # :man_superhero_medium_skin_tone: + U1F9B9200D2642FE0F = "🦹‍♂️" # :man_supervillain: + U1F9B9200D2642 = "🦹‍♂" # :man_supervillain: + U1F9B91F3FF200D2642FE0F = "🦹🏿‍♂️" # :man_supervillain_dark_skin_tone: + U1F9B91F3FF200D2642 = "🦹🏿‍♂" # :man_supervillain_dark_skin_tone: + U1F9B91F3FB200D2642FE0F = "🦹🏻‍♂️" # :man_supervillain_light_skin_tone: + U1F9B91F3FB200D2642 = "🦹🏻‍♂" # :man_supervillain_light_skin_tone: + U1F9B91F3FE200D2642FE0F = "🦹🏾‍♂️" # :man_supervillain_medium-dark_skin_tone: + U1F9B91F3FE200D2642 = "🦹🏾‍♂" # :man_supervillain_medium-dark_skin_tone: + U1F9B91F3FC200D2642FE0F = "🦹🏼‍♂️" # :man_supervillain_medium-light_skin_tone: + U1F9B91F3FC200D2642 = "🦹🏼‍♂" # :man_supervillain_medium-light_skin_tone: + U1F9B91F3FD200D2642FE0F = "🦹🏽‍♂️" # :man_supervillain_medium_skin_tone: + U1F9B91F3FD200D2642 = "🦹🏽‍♂" # :man_supervillain_medium_skin_tone: + U1F3C4200D2642FE0F = "🏄‍♂️" # :man_surfing: + U1F3C4200D2642 = "🏄‍♂" # :man_surfing: + U1F3C41F3FF200D2642FE0F = "🏄🏿‍♂️" # :man_surfing_dark_skin_tone: + U1F3C41F3FF200D2642 = "🏄🏿‍♂" # :man_surfing_dark_skin_tone: + U1F3C41F3FB200D2642FE0F = "🏄🏻‍♂️" # :man_surfing_light_skin_tone: + U1F3C41F3FB200D2642 = "🏄🏻‍♂" # :man_surfing_light_skin_tone: + U1F3C41F3FE200D2642FE0F = "🏄🏾‍♂️" # :man_surfing_medium-dark_skin_tone: + U1F3C41F3FE200D2642 = "🏄🏾‍♂" # :man_surfing_medium-dark_skin_tone: + U1F3C41F3FC200D2642FE0F = "🏄🏼‍♂️" # :man_surfing_medium-light_skin_tone: + U1F3C41F3FC200D2642 = "🏄🏼‍♂" # :man_surfing_medium-light_skin_tone: + U1F3C41F3FD200D2642FE0F = "🏄🏽‍♂️" # :man_surfing_medium_skin_tone: + U1F3C41F3FD200D2642 = "🏄🏽‍♂" # :man_surfing_medium_skin_tone: + U1F3CA200D2642FE0F = "🏊‍♂️" # :man_swimming: + U1F3CA200D2642 = "🏊‍♂" # :man_swimming: + U1F3CA1F3FF200D2642FE0F = "🏊🏿‍♂️" # :man_swimming_dark_skin_tone: + U1F3CA1F3FF200D2642 = "🏊🏿‍♂" # :man_swimming_dark_skin_tone: + U1F3CA1F3FB200D2642FE0F = "🏊🏻‍♂️" # :man_swimming_light_skin_tone: + U1F3CA1F3FB200D2642 = "🏊🏻‍♂" # :man_swimming_light_skin_tone: + U1F3CA1F3FE200D2642FE0F = "🏊🏾‍♂️" # :man_swimming_medium-dark_skin_tone: + U1F3CA1F3FE200D2642 = "🏊🏾‍♂" # :man_swimming_medium-dark_skin_tone: + U1F3CA1F3FC200D2642FE0F = "🏊🏼‍♂️" # :man_swimming_medium-light_skin_tone: + U1F3CA1F3FC200D2642 = "🏊🏼‍♂" # :man_swimming_medium-light_skin_tone: + U1F3CA1F3FD200D2642FE0F = "🏊🏽‍♂️" # :man_swimming_medium_skin_tone: + U1F3CA1F3FD200D2642 = "🏊🏽‍♂" # :man_swimming_medium_skin_tone: + U1F468200D1F3EB = "👨‍🏫" # :man_teacher: + U1F4681F3FF200D1F3EB = "👨🏿‍🏫" # :man_teacher_dark_skin_tone: + U1F4681F3FB200D1F3EB = "👨🏻‍🏫" # :man_teacher_light_skin_tone: + U1F4681F3FE200D1F3EB = "👨🏾‍🏫" # :man_teacher_medium-dark_skin_tone: + U1F4681F3FC200D1F3EB = "👨🏼‍🏫" # :man_teacher_medium-light_skin_tone: + U1F4681F3FD200D1F3EB = "👨🏽‍🏫" # :man_teacher_medium_skin_tone: + U1F468200D1F4BB = "👨‍💻" # :man_technologist: + U1F4681F3FF200D1F4BB = "👨🏿‍💻" # :man_technologist_dark_skin_tone: + U1F4681F3FB200D1F4BB = "👨🏻‍💻" # :man_technologist_light_skin_tone: + U1F4681F3FE200D1F4BB = "👨🏾‍💻" # :man_technologist_medium-dark_skin_tone: + U1F4681F3FC200D1F4BB = "👨🏼‍💻" # :man_technologist_medium-light_skin_tone: + U1F4681F3FD200D1F4BB = "👨🏽‍💻" # :man_technologist_medium_skin_tone: + U1F481200D2642FE0F = "💁‍♂️" # :man_tipping_hand: + U1F481200D2642 = "💁‍♂" # :man_tipping_hand: + U1F4811F3FF200D2642FE0F = "💁🏿‍♂️" # :man_tipping_hand_dark_skin_tone: + U1F4811F3FF200D2642 = "💁🏿‍♂" # :man_tipping_hand_dark_skin_tone: + U1F4811F3FB200D2642FE0F = "💁🏻‍♂️" # :man_tipping_hand_light_skin_tone: + U1F4811F3FB200D2642 = "💁🏻‍♂" # :man_tipping_hand_light_skin_tone: + U1F4811F3FE200D2642FE0F = "💁🏾‍♂️" # :man_tipping_hand_medium-dark_skin_tone: + U1F4811F3FE200D2642 = "💁🏾‍♂" # :man_tipping_hand_medium-dark_skin_tone: + U1F4811F3FC200D2642FE0F = "💁🏼‍♂️" # :man_tipping_hand_medium-light_skin_tone: + U1F4811F3FC200D2642 = "💁🏼‍♂" # :man_tipping_hand_medium-light_skin_tone: + U1F4811F3FD200D2642FE0F = "💁🏽‍♂️" # :man_tipping_hand_medium_skin_tone: + U1F4811F3FD200D2642 = "💁🏽‍♂" # :man_tipping_hand_medium_skin_tone: + U1F9DB200D2642FE0F = "🧛‍♂️" # :man_vampire: + U1F9DB200D2642 = "🧛‍♂" # :man_vampire: + U1F9DB1F3FF200D2642FE0F = "🧛🏿‍♂️" # :man_vampire_dark_skin_tone: + U1F9DB1F3FF200D2642 = "🧛🏿‍♂" # :man_vampire_dark_skin_tone: + U1F9DB1F3FB200D2642FE0F = "🧛🏻‍♂️" # :man_vampire_light_skin_tone: + U1F9DB1F3FB200D2642 = "🧛🏻‍♂" # :man_vampire_light_skin_tone: + U1F9DB1F3FE200D2642FE0F = "🧛🏾‍♂️" # :man_vampire_medium-dark_skin_tone: + U1F9DB1F3FE200D2642 = "🧛🏾‍♂" # :man_vampire_medium-dark_skin_tone: + U1F9DB1F3FC200D2642FE0F = "🧛🏼‍♂️" # :man_vampire_medium-light_skin_tone: + U1F9DB1F3FC200D2642 = "🧛🏼‍♂" # :man_vampire_medium-light_skin_tone: + U1F9DB1F3FD200D2642FE0F = "🧛🏽‍♂️" # :man_vampire_medium_skin_tone: + U1F9DB1F3FD200D2642 = "🧛🏽‍♂" # :man_vampire_medium_skin_tone: + U1F6B6200D2642FE0F = "🚶‍♂️" # :man_walking: + U1F6B6200D2642 = "🚶‍♂" # :man_walking: + U1F6B61F3FF200D2642FE0F = "🚶🏿‍♂️" # :man_walking_dark_skin_tone: + U1F6B61F3FF200D2642 = "🚶🏿‍♂" # :man_walking_dark_skin_tone: + U1F6B6200D2642FE0F200D27A1FE0F = "🚶‍♂️‍➡️" # :man_walking_facing_right: + U1F6B6200D2642200D27A1FE0F = "🚶‍♂‍➡️" # :man_walking_facing_right: + U1F6B6200D2642FE0F200D27A1 = "🚶‍♂️‍➡" # :man_walking_facing_right: + U1F6B6200D2642200D27A1 = "🚶‍♂‍➡" # :man_walking_facing_right: + U1F6B61F3FF200D2642FE0F200D27A1FE0F = "🚶🏿‍♂️‍➡️" # :man_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2642200D27A1FE0F = "🚶🏿‍♂‍➡️" # :man_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2642FE0F200D27A1 = "🚶🏿‍♂️‍➡" # :man_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2642200D27A1 = "🚶🏿‍♂‍➡" # :man_walking_facing_right_dark_skin_tone: + U1F6B61F3FB200D2642FE0F200D27A1FE0F = "🚶🏻‍♂️‍➡️" # :man_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2642200D27A1FE0F = "🚶🏻‍♂‍➡️" # :man_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2642FE0F200D27A1 = "🚶🏻‍♂️‍➡" # :man_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2642200D27A1 = "🚶🏻‍♂‍➡" # :man_walking_facing_right_light_skin_tone: + U1F6B61F3FE200D2642FE0F200D27A1FE0F = "🚶🏾‍♂️‍➡️" # :man_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2642200D27A1FE0F = "🚶🏾‍♂‍➡️" # :man_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2642FE0F200D27A1 = "🚶🏾‍♂️‍➡" # :man_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2642200D27A1 = "🚶🏾‍♂‍➡" # :man_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FC200D2642FE0F200D27A1FE0F = "🚶🏼‍♂️‍➡️" # :man_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2642200D27A1FE0F = "🚶🏼‍♂‍➡️" # :man_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2642FE0F200D27A1 = "🚶🏼‍♂️‍➡" # :man_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2642200D27A1 = "🚶🏼‍♂‍➡" # :man_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FD200D2642FE0F200D27A1FE0F = "🚶🏽‍♂️‍➡️" # :man_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2642200D27A1FE0F = "🚶🏽‍♂‍➡️" # :man_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2642FE0F200D27A1 = "🚶🏽‍♂️‍➡" # :man_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2642200D27A1 = "🚶🏽‍♂‍➡" # :man_walking_facing_right_medium_skin_tone: + U1F6B61F3FB200D2642FE0F = "🚶🏻‍♂️" # :man_walking_light_skin_tone: + U1F6B61F3FB200D2642 = "🚶🏻‍♂" # :man_walking_light_skin_tone: + U1F6B61F3FE200D2642FE0F = "🚶🏾‍♂️" # :man_walking_medium-dark_skin_tone: + U1F6B61F3FE200D2642 = "🚶🏾‍♂" # :man_walking_medium-dark_skin_tone: + U1F6B61F3FC200D2642FE0F = "🚶🏼‍♂️" # :man_walking_medium-light_skin_tone: + U1F6B61F3FC200D2642 = "🚶🏼‍♂" # :man_walking_medium-light_skin_tone: + U1F6B61F3FD200D2642FE0F = "🚶🏽‍♂️" # :man_walking_medium_skin_tone: + U1F6B61F3FD200D2642 = "🚶🏽‍♂" # :man_walking_medium_skin_tone: + U1F473200D2642FE0F = "👳‍♂️" # :man_wearing_turban: + U1F473200D2642 = "👳‍♂" # :man_wearing_turban: + U1F4731F3FF200D2642FE0F = "👳🏿‍♂️" # :man_wearing_turban_dark_skin_tone: + U1F4731F3FF200D2642 = "👳🏿‍♂" # :man_wearing_turban_dark_skin_tone: + U1F4731F3FB200D2642FE0F = "👳🏻‍♂️" # :man_wearing_turban_light_skin_tone: + U1F4731F3FB200D2642 = "👳🏻‍♂" # :man_wearing_turban_light_skin_tone: + U1F4731F3FE200D2642FE0F = "👳🏾‍♂️" # :man_wearing_turban_medium-dark_skin_tone: + U1F4731F3FE200D2642 = "👳🏾‍♂" # :man_wearing_turban_medium-dark_skin_tone: + U1F4731F3FC200D2642FE0F = "👳🏼‍♂️" # :man_wearing_turban_medium-light_skin_tone: + U1F4731F3FC200D2642 = "👳🏼‍♂" # :man_wearing_turban_medium-light_skin_tone: + U1F4731F3FD200D2642FE0F = "👳🏽‍♂️" # :man_wearing_turban_medium_skin_tone: + U1F4731F3FD200D2642 = "👳🏽‍♂" # :man_wearing_turban_medium_skin_tone: + U1F468200D1F9B3 = "👨‍🦳" # :man_white_hair: + U1F470200D2642FE0F = "👰‍♂️" # :man_with_veil: + U1F470200D2642 = "👰‍♂" # :man_with_veil: + U1F4701F3FF200D2642FE0F = "👰🏿‍♂️" # :man_with_veil_dark_skin_tone: + U1F4701F3FF200D2642 = "👰🏿‍♂" # :man_with_veil_dark_skin_tone: + U1F4701F3FB200D2642FE0F = "👰🏻‍♂️" # :man_with_veil_light_skin_tone: + U1F4701F3FB200D2642 = "👰🏻‍♂" # :man_with_veil_light_skin_tone: + U1F4701F3FE200D2642FE0F = "👰🏾‍♂️" # :man_with_veil_medium-dark_skin_tone: + U1F4701F3FE200D2642 = "👰🏾‍♂" # :man_with_veil_medium-dark_skin_tone: + U1F4701F3FC200D2642FE0F = "👰🏼‍♂️" # :man_with_veil_medium-light_skin_tone: + U1F4701F3FC200D2642 = "👰🏼‍♂" # :man_with_veil_medium-light_skin_tone: + U1F4701F3FD200D2642FE0F = "👰🏽‍♂️" # :man_with_veil_medium_skin_tone: + U1F4701F3FD200D2642 = "👰🏽‍♂" # :man_with_veil_medium_skin_tone: + U1F468200D1F9AF = "👨‍🦯" # :man_with_white_cane: + U1F4681F3FF200D1F9AF = "👨🏿‍🦯" # :man_with_white_cane_dark_skin_tone: + U1F468200D1F9AF200D27A1FE0F = "👨‍🦯‍➡️" # :man_with_white_cane_facing_right: + U1F468200D1F9AF200D27A1 = "👨‍🦯‍➡" # :man_with_white_cane_facing_right: + U1F4681F3FF200D1F9AF200D27A1FE0F = "👨🏿‍🦯‍➡️" # :man_with_white_cane_facing_right_dark_skin_tone: + U1F4681F3FF200D1F9AF200D27A1 = "👨🏿‍🦯‍➡" # :man_with_white_cane_facing_right_dark_skin_tone: + U1F4681F3FB200D1F9AF200D27A1FE0F = "👨🏻‍🦯‍➡️" # :man_with_white_cane_facing_right_light_skin_tone: + U1F4681F3FB200D1F9AF200D27A1 = "👨🏻‍🦯‍➡" # :man_with_white_cane_facing_right_light_skin_tone: + U1F4681F3FE200D1F9AF200D27A1FE0F = "👨🏾‍🦯‍➡️" # :man_with_white_cane_facing_right_medium-dark_skin_tone: + U1F4681F3FE200D1F9AF200D27A1 = "👨🏾‍🦯‍➡" # :man_with_white_cane_facing_right_medium-dark_skin_tone: + U1F4681F3FC200D1F9AF200D27A1FE0F = "👨🏼‍🦯‍➡️" # :man_with_white_cane_facing_right_medium-light_skin_tone: + U1F4681F3FC200D1F9AF200D27A1 = "👨🏼‍🦯‍➡" # :man_with_white_cane_facing_right_medium-light_skin_tone: + U1F4681F3FD200D1F9AF200D27A1FE0F = "👨🏽‍🦯‍➡️" # :man_with_white_cane_facing_right_medium_skin_tone: + U1F4681F3FD200D1F9AF200D27A1 = "👨🏽‍🦯‍➡" # :man_with_white_cane_facing_right_medium_skin_tone: + U1F4681F3FB200D1F9AF = "👨🏻‍🦯" # :man_with_white_cane_light_skin_tone: + U1F4681F3FE200D1F9AF = "👨🏾‍🦯" # :man_with_white_cane_medium-dark_skin_tone: + U1F4681F3FC200D1F9AF = "👨🏼‍🦯" # :man_with_white_cane_medium-light_skin_tone: + U1F4681F3FD200D1F9AF = "👨🏽‍🦯" # :man_with_white_cane_medium_skin_tone: + U1F9DF200D2642FE0F = "🧟‍♂️" # :man_zombie: + U1F9DF200D2642 = "🧟‍♂" # :man_zombie: + U1F96D = "🥭" # :mango: + U1F570FE0F = "🕰️" # :mantelpiece_clock: + U1F570 = "🕰" # :mantelpiece_clock: + U1F9BD = "🦽" # :manual_wheelchair: + U1F45E = "👞" # :man’s_shoe: + U1F5FE = "🗾" # :map_of_Japan: + U1F341 = "🍁" # :maple_leaf: + U1FA87 = "🪇" # :maracas: + U1F94B = "🥋" # :martial_arts_uniform: + U1F9C9 = "🧉" # :mate: + U1F356 = "🍖" # :meat_on_bone: + U1F9D1200D1F527 = "🧑‍🔧" # :mechanic: + U1F9D11F3FF200D1F527 = "🧑🏿‍🔧" # :mechanic_dark_skin_tone: + U1F9D11F3FB200D1F527 = "🧑🏻‍🔧" # :mechanic_light_skin_tone: + U1F9D11F3FE200D1F527 = "🧑🏾‍🔧" # :mechanic_medium-dark_skin_tone: + U1F9D11F3FC200D1F527 = "🧑🏼‍🔧" # :mechanic_medium-light_skin_tone: + U1F9D11F3FD200D1F527 = "🧑🏽‍🔧" # :mechanic_medium_skin_tone: + U1F9BE = "🦾" # :mechanical_arm: + U1F9BF = "🦿" # :mechanical_leg: + U2695FE0F = "⚕️" # :medical_symbol: + U2695 = "⚕" # :medical_symbol: + U1F3FE = "🏾" # :medium-dark_skin_tone: + U1F3FC = "🏼" # :medium-light_skin_tone: + U1F3FD = "🏽" # :medium_skin_tone: + U1F4E3 = "📣" # :megaphone: + U1F348 = "🍈" # :melon: + U1FAE0 = "🫠" # :melting_face: + U1F4DD = "📝" # :memo: + U1F46C = "👬" # :men_holding_hands: + U1F46C1F3FF = "👬🏿" # :men_holding_hands_dark_skin_tone: + U1F4681F3FF200D1F91D200D1F4681F3FB = "👨🏿‍🤝‍👨🏻" # :men_holding_hands_dark_skin_tone_light_skin_tone: + U1F4681F3FF200D1F91D200D1F4681F3FE = "👨🏿‍🤝‍👨🏾" # :men_holding_hands_dark_skin_tone_medium-dark_skin_tone: + U1F4681F3FF200D1F91D200D1F4681F3FC = "👨🏿‍🤝‍👨🏼" # :men_holding_hands_dark_skin_tone_medium-light_skin_tone: + U1F4681F3FF200D1F91D200D1F4681F3FD = "👨🏿‍🤝‍👨🏽" # :men_holding_hands_dark_skin_tone_medium_skin_tone: + U1F46C1F3FB = "👬🏻" # :men_holding_hands_light_skin_tone: + U1F4681F3FB200D1F91D200D1F4681F3FF = "👨🏻‍🤝‍👨🏿" # :men_holding_hands_light_skin_tone_dark_skin_tone: + U1F4681F3FB200D1F91D200D1F4681F3FE = "👨🏻‍🤝‍👨🏾" # :men_holding_hands_light_skin_tone_medium-dark_skin_tone: + U1F4681F3FB200D1F91D200D1F4681F3FC = "👨🏻‍🤝‍👨🏼" # :men_holding_hands_light_skin_tone_medium-light_skin_tone: + U1F4681F3FB200D1F91D200D1F4681F3FD = "👨🏻‍🤝‍👨🏽" # :men_holding_hands_light_skin_tone_medium_skin_tone: + U1F46C1F3FE = "👬🏾" # :men_holding_hands_medium-dark_skin_tone: + U1F4681F3FE200D1F91D200D1F4681F3FF = "👨🏾‍🤝‍👨🏿" # :men_holding_hands_medium-dark_skin_tone_dark_skin_tone: + U1F4681F3FE200D1F91D200D1F4681F3FB = "👨🏾‍🤝‍👨🏻" # :men_holding_hands_medium-dark_skin_tone_light_skin_tone: + U1F4681F3FE200D1F91D200D1F4681F3FC = "👨🏾‍🤝‍👨🏼" # :men_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: + U1F4681F3FE200D1F91D200D1F4681F3FD = "👨🏾‍🤝‍👨🏽" # :men_holding_hands_medium-dark_skin_tone_medium_skin_tone: + U1F46C1F3FC = "👬🏼" # :men_holding_hands_medium-light_skin_tone: + U1F4681F3FC200D1F91D200D1F4681F3FF = "👨🏼‍🤝‍👨🏿" # :men_holding_hands_medium-light_skin_tone_dark_skin_tone: + U1F4681F3FC200D1F91D200D1F4681F3FB = "👨🏼‍🤝‍👨🏻" # :men_holding_hands_medium-light_skin_tone_light_skin_tone: + U1F4681F3FC200D1F91D200D1F4681F3FE = "👨🏼‍🤝‍👨🏾" # :men_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: + U1F4681F3FC200D1F91D200D1F4681F3FD = "👨🏼‍🤝‍👨🏽" # :men_holding_hands_medium-light_skin_tone_medium_skin_tone: + U1F46C1F3FD = "👬🏽" # :men_holding_hands_medium_skin_tone: + U1F4681F3FD200D1F91D200D1F4681F3FF = "👨🏽‍🤝‍👨🏿" # :men_holding_hands_medium_skin_tone_dark_skin_tone: + U1F4681F3FD200D1F91D200D1F4681F3FB = "👨🏽‍🤝‍👨🏻" # :men_holding_hands_medium_skin_tone_light_skin_tone: + U1F4681F3FD200D1F91D200D1F4681F3FE = "👨🏽‍🤝‍👨🏾" # :men_holding_hands_medium_skin_tone_medium-dark_skin_tone: + U1F4681F3FD200D1F91D200D1F4681F3FC = "👨🏽‍🤝‍👨🏼" # :men_holding_hands_medium_skin_tone_medium-light_skin_tone: + U1F46F200D2642FE0F = "👯‍♂️" # :men_with_bunny_ears: + U1F46F200D2642 = "👯‍♂" # :men_with_bunny_ears: + U1F93C200D2642FE0F = "🤼‍♂️" # :men_wrestling: + U1F93C200D2642 = "🤼‍♂" # :men_wrestling: + U2764FE0F200D1FA79 = "❤️‍🩹" # :mending_heart: + U2764200D1FA79 = "❤‍🩹" # :mending_heart: + U1F54E = "🕎" # :menorah: + U1F6B9 = "🚹" # :men’s_room: + U1F9DC200D2640FE0F = "🧜‍♀️" # :mermaid: + U1F9DC200D2640 = "🧜‍♀" # :mermaid: + U1F9DC1F3FF200D2640FE0F = "🧜🏿‍♀️" # :mermaid_dark_skin_tone: + U1F9DC1F3FF200D2640 = "🧜🏿‍♀" # :mermaid_dark_skin_tone: + U1F9DC1F3FB200D2640FE0F = "🧜🏻‍♀️" # :mermaid_light_skin_tone: + U1F9DC1F3FB200D2640 = "🧜🏻‍♀" # :mermaid_light_skin_tone: + U1F9DC1F3FE200D2640FE0F = "🧜🏾‍♀️" # :mermaid_medium-dark_skin_tone: + U1F9DC1F3FE200D2640 = "🧜🏾‍♀" # :mermaid_medium-dark_skin_tone: + U1F9DC1F3FC200D2640FE0F = "🧜🏼‍♀️" # :mermaid_medium-light_skin_tone: + U1F9DC1F3FC200D2640 = "🧜🏼‍♀" # :mermaid_medium-light_skin_tone: + U1F9DC1F3FD200D2640FE0F = "🧜🏽‍♀️" # :mermaid_medium_skin_tone: + U1F9DC1F3FD200D2640 = "🧜🏽‍♀" # :mermaid_medium_skin_tone: + U1F9DC200D2642FE0F = "🧜‍♂️" # :merman: + U1F9DC200D2642 = "🧜‍♂" # :merman: + U1F9DC1F3FF200D2642FE0F = "🧜🏿‍♂️" # :merman_dark_skin_tone: + U1F9DC1F3FF200D2642 = "🧜🏿‍♂" # :merman_dark_skin_tone: + U1F9DC1F3FB200D2642FE0F = "🧜🏻‍♂️" # :merman_light_skin_tone: + U1F9DC1F3FB200D2642 = "🧜🏻‍♂" # :merman_light_skin_tone: + U1F9DC1F3FE200D2642FE0F = "🧜🏾‍♂️" # :merman_medium-dark_skin_tone: + U1F9DC1F3FE200D2642 = "🧜🏾‍♂" # :merman_medium-dark_skin_tone: + U1F9DC1F3FC200D2642FE0F = "🧜🏼‍♂️" # :merman_medium-light_skin_tone: + U1F9DC1F3FC200D2642 = "🧜🏼‍♂" # :merman_medium-light_skin_tone: + U1F9DC1F3FD200D2642FE0F = "🧜🏽‍♂️" # :merman_medium_skin_tone: + U1F9DC1F3FD200D2642 = "🧜🏽‍♂" # :merman_medium_skin_tone: + U1F9DC = "🧜" # :merperson: + U1F9DC1F3FF = "🧜🏿" # :merperson_dark_skin_tone: + U1F9DC1F3FB = "🧜🏻" # :merperson_light_skin_tone: + U1F9DC1F3FE = "🧜🏾" # :merperson_medium-dark_skin_tone: + U1F9DC1F3FC = "🧜🏼" # :merperson_medium-light_skin_tone: + U1F9DC1F3FD = "🧜🏽" # :merperson_medium_skin_tone: + U1F687 = "🚇" # :metro: + U1F9A0 = "🦠" # :microbe: + U1F3A4 = "🎤" # :microphone: + U1F52C = "🔬" # :microscope: + U1F595 = "🖕" # :middle_finger: + U1F5951F3FF = "🖕🏿" # :middle_finger_dark_skin_tone: + U1F5951F3FB = "🖕🏻" # :middle_finger_light_skin_tone: + U1F5951F3FE = "🖕🏾" # :middle_finger_medium-dark_skin_tone: + U1F5951F3FC = "🖕🏼" # :middle_finger_medium-light_skin_tone: + U1F5951F3FD = "🖕🏽" # :middle_finger_medium_skin_tone: + U1FA96 = "🪖" # :military_helmet: + U1F396FE0F = "🎖️" # :military_medal: + U1F396 = "🎖" # :military_medal: + U1F30C = "🌌" # :milky_way: + U1F690 = "🚐" # :minibus: + U2796 = "➖" # :minus: + U1FA9E = "🪞" # :mirror: + U1FAA9 = "🪩" # :mirror_ball: + U1F5FF = "🗿" # :moai: + U1F4F1 = "📱" # :mobile_phone: + U1F4F4 = "📴" # :mobile_phone_off: + U1F4F2 = "📲" # :mobile_phone_with_arrow: + U1F911 = "🤑" # :money-mouth_face: + U1F4B0 = "💰" # :money_bag: + U1F4B8 = "💸" # :money_with_wings: + U1F412 = "🐒" # :monkey: + U1F435 = "🐵" # :monkey_face: + U1F69D = "🚝" # :monorail: + U1F96E = "🥮" # :moon_cake: + U1F391 = "🎑" # :moon_viewing_ceremony: + U1FACE = "🫎" # :moose: + U1F54C = "🕌" # :mosque: + U1F99F = "🦟" # :mosquito: + U1F6E5FE0F = "🛥️" # :motor_boat: + U1F6E5 = "🛥" # :motor_boat: + U1F6F5 = "🛵" # :motor_scooter: + U1F3CDFE0F = "🏍️" # :motorcycle: + U1F3CD = "🏍" # :motorcycle: + U1F9BC = "🦼" # :motorized_wheelchair: + U1F6E3FE0F = "🛣️" # :motorway: + U1F6E3 = "🛣" # :motorway: + U1F5FB = "🗻" # :mount_fuji: + U26F0FE0F = "⛰️" # :mountain: + U26F0 = "⛰" # :mountain: + U1F6A0 = "🚠" # :mountain_cableway: + U1F69E = "🚞" # :mountain_railway: + U1F401 = "🐁" # :mouse: + U1F42D = "🐭" # :mouse_face: + U1FAA4 = "🪤" # :mouse_trap: + U1F444 = "👄" # :mouth: + U1F3A5 = "🎥" # :movie_camera: + U2716FE0F = "✖️" # :multiply: + U2716 = "✖" # :multiply: + U1F344 = "🍄" # :mushroom: + U1F3B9 = "🎹" # :musical_keyboard: + U1F3B5 = "🎵" # :musical_note: + U1F3B6 = "🎶" # :musical_notes: + U1F3BC = "🎼" # :musical_score: + U1F507 = "🔇" # :muted_speaker: + U1F9D1200D1F384 = "🧑‍🎄" # :mx_claus: + U1F9D11F3FF200D1F384 = "🧑🏿‍🎄" # :mx_claus_dark_skin_tone: + U1F9D11F3FB200D1F384 = "🧑🏻‍🎄" # :mx_claus_light_skin_tone: + U1F9D11F3FE200D1F384 = "🧑🏾‍🎄" # :mx_claus_medium-dark_skin_tone: + U1F9D11F3FC200D1F384 = "🧑🏼‍🎄" # :mx_claus_medium-light_skin_tone: + U1F9D11F3FD200D1F384 = "🧑🏽‍🎄" # :mx_claus_medium_skin_tone: + U1F485 = "💅" # :nail_polish: + U1F4851F3FF = "💅🏿" # :nail_polish_dark_skin_tone: + U1F4851F3FB = "💅🏻" # :nail_polish_light_skin_tone: + U1F4851F3FE = "💅🏾" # :nail_polish_medium-dark_skin_tone: + U1F4851F3FC = "💅🏼" # :nail_polish_medium-light_skin_tone: + U1F4851F3FD = "💅🏽" # :nail_polish_medium_skin_tone: + U1F4DB = "📛" # :name_badge: + U1F3DEFE0F = "🏞️" # :national_park: + U1F3DE = "🏞" # :national_park: + U1F922 = "🤢" # :nauseated_face: + U1F9FF = "🧿" # :nazar_amulet: + U1F454 = "👔" # :necktie: + U1F913 = "🤓" # :nerd_face: + U1FABA = "🪺" # :nest_with_eggs: + U1FA86 = "🪆" # :nesting_dolls: + U1F610 = "😐" # :neutral_face: + U1F311 = "🌑" # :new_moon: + U1F31A = "🌚" # :new_moon_face: + U1F4F0 = "📰" # :newspaper: + U23EDFE0F = "⏭️" # :next_track_button: + U23ED = "⏭" # :next_track_button: + U1F303 = "🌃" # :night_with_stars: + U1F564 = "🕤" # :nine-thirty: + U1F558 = "🕘" # :nine_o’clock: + U1F977 = "🥷" # :ninja: + U1F9771F3FF = "🥷🏿" # :ninja_dark_skin_tone: + U1F9771F3FB = "🥷🏻" # :ninja_light_skin_tone: + U1F9771F3FE = "🥷🏾" # :ninja_medium-dark_skin_tone: + U1F9771F3FC = "🥷🏼" # :ninja_medium-light_skin_tone: + U1F9771F3FD = "🥷🏽" # :ninja_medium_skin_tone: + U1F6B3 = "🚳" # :no_bicycles: + U26D4 = "⛔" # :no_entry: + U1F6AF = "🚯" # :no_littering: + U1F4F5 = "📵" # :no_mobile_phones: + U1F51E = "🔞" # :no_one_under_eighteen: + U1F6B7 = "🚷" # :no_pedestrians: + U1F6AD = "🚭" # :no_smoking: + U1F6B1 = "🚱" # :non-potable_water: + U1F443 = "👃" # :nose: + U1F4431F3FF = "👃🏿" # :nose_dark_skin_tone: + U1F4431F3FB = "👃🏻" # :nose_light_skin_tone: + U1F4431F3FE = "👃🏾" # :nose_medium-dark_skin_tone: + U1F4431F3FC = "👃🏼" # :nose_medium-light_skin_tone: + U1F4431F3FD = "👃🏽" # :nose_medium_skin_tone: + U1F4D3 = "📓" # :notebook: + U1F4D4 = "📔" # :notebook_with_decorative_cover: + U1F529 = "🔩" # :nut_and_bolt: + U1F419 = "🐙" # :octopus: + U1F362 = "🍢" # :oden: + U1F3E2 = "🏢" # :office_building: + U1F9D1200D1F4BC = "🧑‍💼" # :office_worker: + U1F9D11F3FF200D1F4BC = "🧑🏿‍💼" # :office_worker_dark_skin_tone: + U1F9D11F3FB200D1F4BC = "🧑🏻‍💼" # :office_worker_light_skin_tone: + U1F9D11F3FE200D1F4BC = "🧑🏾‍💼" # :office_worker_medium-dark_skin_tone: + U1F9D11F3FC200D1F4BC = "🧑🏼‍💼" # :office_worker_medium-light_skin_tone: + U1F9D11F3FD200D1F4BC = "🧑🏽‍💼" # :office_worker_medium_skin_tone: + U1F479 = "👹" # :ogre: + U1F6E2FE0F = "🛢️" # :oil_drum: + U1F6E2 = "🛢" # :oil_drum: + U1F5DDFE0F = "🗝️" # :old_key: + U1F5DD = "🗝" # :old_key: + U1F474 = "👴" # :old_man: + U1F4741F3FF = "👴🏿" # :old_man_dark_skin_tone: + U1F4741F3FB = "👴🏻" # :old_man_light_skin_tone: + U1F4741F3FE = "👴🏾" # :old_man_medium-dark_skin_tone: + U1F4741F3FC = "👴🏼" # :old_man_medium-light_skin_tone: + U1F4741F3FD = "👴🏽" # :old_man_medium_skin_tone: + U1F475 = "👵" # :old_woman: + U1F4751F3FF = "👵🏿" # :old_woman_dark_skin_tone: + U1F4751F3FB = "👵🏻" # :old_woman_light_skin_tone: + U1F4751F3FE = "👵🏾" # :old_woman_medium-dark_skin_tone: + U1F4751F3FC = "👵🏼" # :old_woman_medium-light_skin_tone: + U1F4751F3FD = "👵🏽" # :old_woman_medium_skin_tone: + U1F9D3 = "🧓" # :older_person: + U1F9D31F3FF = "🧓🏿" # :older_person_dark_skin_tone: + U1F9D31F3FB = "🧓🏻" # :older_person_light_skin_tone: + U1F9D31F3FE = "🧓🏾" # :older_person_medium-dark_skin_tone: + U1F9D31F3FC = "🧓🏼" # :older_person_medium-light_skin_tone: + U1F9D31F3FD = "🧓🏽" # :older_person_medium_skin_tone: + U1FAD2 = "🫒" # :olive: + U1F549FE0F = "🕉️" # :om: + U1F549 = "🕉" # :om: + U1F698 = "🚘" # :oncoming_automobile: + U1F68D = "🚍" # :oncoming_bus: + U1F44A = "👊" # :oncoming_fist: + U1F44A1F3FF = "👊🏿" # :oncoming_fist_dark_skin_tone: + U1F44A1F3FB = "👊🏻" # :oncoming_fist_light_skin_tone: + U1F44A1F3FE = "👊🏾" # :oncoming_fist_medium-dark_skin_tone: + U1F44A1F3FC = "👊🏼" # :oncoming_fist_medium-light_skin_tone: + U1F44A1F3FD = "👊🏽" # :oncoming_fist_medium_skin_tone: + U1F694 = "🚔" # :oncoming_police_car: + U1F696 = "🚖" # :oncoming_taxi: + U1FA71 = "🩱" # :one-piece_swimsuit: + U1F55C = "🕜" # :one-thirty: + U1F550 = "🕐" # :one_o’clock: + U1F9C5 = "🧅" # :onion: + U1F4D6 = "📖" # :open_book: + U1F4C2 = "📂" # :open_file_folder: + U1F450 = "👐" # :open_hands: + U1F4501F3FF = "👐🏿" # :open_hands_dark_skin_tone: + U1F4501F3FB = "👐🏻" # :open_hands_light_skin_tone: + U1F4501F3FE = "👐🏾" # :open_hands_medium-dark_skin_tone: + U1F4501F3FC = "👐🏼" # :open_hands_medium-light_skin_tone: + U1F4501F3FD = "👐🏽" # :open_hands_medium_skin_tone: + U1F4ED = "📭" # :open_mailbox_with_lowered_flag: + U1F4EC = "📬" # :open_mailbox_with_raised_flag: + U1F4BF = "💿" # :optical_disk: + U1F4D9 = "📙" # :orange_book: + U1F7E0 = "🟠" # :orange_circle: + U1F9E1 = "🧡" # :orange_heart: + U1F7E7 = "🟧" # :orange_square: + U1F9A7 = "🦧" # :orangutan: + U2626FE0F = "☦️" # :orthodox_cross: + U2626 = "☦" # :orthodox_cross: + U1F9A6 = "🦦" # :otter: + U1F4E4 = "📤" # :outbox_tray: + U1F989 = "🦉" # :owl: + U1F402 = "🐂" # :ox: + U1F9AA = "🦪" # :oyster: + U1F4E6 = "📦" # :package: + U1F4C4 = "📄" # :page_facing_up: + U1F4C3 = "📃" # :page_with_curl: + U1F4DF = "📟" # :pager: + U1F58CFE0F = "🖌️" # :paintbrush: + U1F58C = "🖌" # :paintbrush: + U1FAF3 = "🫳" # :palm_down_hand: + U1FAF31F3FF = "🫳🏿" # :palm_down_hand_dark_skin_tone: + U1FAF31F3FB = "🫳🏻" # :palm_down_hand_light_skin_tone: + U1FAF31F3FE = "🫳🏾" # :palm_down_hand_medium-dark_skin_tone: + U1FAF31F3FC = "🫳🏼" # :palm_down_hand_medium-light_skin_tone: + U1FAF31F3FD = "🫳🏽" # :palm_down_hand_medium_skin_tone: + U1F334 = "🌴" # :palm_tree: + U1FAF4 = "🫴" # :palm_up_hand: + U1FAF41F3FF = "🫴🏿" # :palm_up_hand_dark_skin_tone: + U1FAF41F3FB = "🫴🏻" # :palm_up_hand_light_skin_tone: + U1FAF41F3FE = "🫴🏾" # :palm_up_hand_medium-dark_skin_tone: + U1FAF41F3FC = "🫴🏼" # :palm_up_hand_medium-light_skin_tone: + U1FAF41F3FD = "🫴🏽" # :palm_up_hand_medium_skin_tone: + U1F932 = "🤲" # :palms_up_together: + U1F9321F3FF = "🤲🏿" # :palms_up_together_dark_skin_tone: + U1F9321F3FB = "🤲🏻" # :palms_up_together_light_skin_tone: + U1F9321F3FE = "🤲🏾" # :palms_up_together_medium-dark_skin_tone: + U1F9321F3FC = "🤲🏼" # :palms_up_together_medium-light_skin_tone: + U1F9321F3FD = "🤲🏽" # :palms_up_together_medium_skin_tone: + U1F95E = "🥞" # :pancakes: + U1F43C = "🐼" # :panda: + U1F4CE = "📎" # :paperclip: + U1FA82 = "🪂" # :parachute: + U1F99C = "🦜" # :parrot: + U303DFE0F = "〽️" # :part_alternation_mark: + U303D = "〽" # :part_alternation_mark: + U1F389 = "🎉" # :party_popper: + U1F973 = "🥳" # :partying_face: + U1F6F3FE0F = "🛳️" # :passenger_ship: + U1F6F3 = "🛳" # :passenger_ship: + U1F6C2 = "🛂" # :passport_control: + U23F8FE0F = "⏸️" # :pause_button: + U23F8 = "⏸" # :pause_button: + U1F43E = "🐾" # :paw_prints: + U1FADB = "🫛" # :pea_pod: + U262EFE0F = "☮️" # :peace_symbol: + U262E = "☮" # :peace_symbol: + U1F351 = "🍑" # :peach: + U1F99A = "🦚" # :peacock: + U1F95C = "🥜" # :peanuts: + U1F350 = "🍐" # :pear: + U1F58AFE0F = "🖊️" # :pen: + U1F58A = "🖊" # :pen: + U270FFE0F = "✏️" # :pencil: + U270F = "✏" # :pencil: + U1F427 = "🐧" # :penguin: + U1F614 = "😔" # :pensive_face: + U1F9D1200D1F91D200D1F9D1 = "🧑‍🤝‍🧑" # :people_holding_hands: + U1F9D11F3FF200D1F91D200D1F9D11F3FF = "🧑🏿‍🤝‍🧑🏿" # :people_holding_hands_dark_skin_tone: + U1F9D11F3FF200D1F91D200D1F9D11F3FB = "🧑🏿‍🤝‍🧑🏻" # :people_holding_hands_dark_skin_tone_light_skin_tone: + U1F9D11F3FF200D1F91D200D1F9D11F3FE = "🧑🏿‍🤝‍🧑🏾" # :people_holding_hands_dark_skin_tone_medium-dark_skin_tone: + U1F9D11F3FF200D1F91D200D1F9D11F3FC = "🧑🏿‍🤝‍🧑🏼" # :people_holding_hands_dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FF200D1F91D200D1F9D11F3FD = "🧑🏿‍🤝‍🧑🏽" # :people_holding_hands_dark_skin_tone_medium_skin_tone: + U1F9D11F3FB200D1F91D200D1F9D11F3FB = "🧑🏻‍🤝‍🧑🏻" # :people_holding_hands_light_skin_tone: + U1F9D11F3FB200D1F91D200D1F9D11F3FF = "🧑🏻‍🤝‍🧑🏿" # :people_holding_hands_light_skin_tone_dark_skin_tone: + U1F9D11F3FB200D1F91D200D1F9D11F3FE = "🧑🏻‍🤝‍🧑🏾" # :people_holding_hands_light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FB200D1F91D200D1F9D11F3FC = "🧑🏻‍🤝‍🧑🏼" # :people_holding_hands_light_skin_tone_medium-light_skin_tone: + U1F9D11F3FB200D1F91D200D1F9D11F3FD = "🧑🏻‍🤝‍🧑🏽" # :people_holding_hands_light_skin_tone_medium_skin_tone: + U1F9D11F3FE200D1F91D200D1F9D11F3FE = "🧑🏾‍🤝‍🧑🏾" # :people_holding_hands_medium-dark_skin_tone: + U1F9D11F3FE200D1F91D200D1F9D11F3FF = "🧑🏾‍🤝‍🧑🏿" # :people_holding_hands_medium-dark_skin_tone_dark_skin_tone: + U1F9D11F3FE200D1F91D200D1F9D11F3FB = "🧑🏾‍🤝‍🧑🏻" # :people_holding_hands_medium-dark_skin_tone_light_skin_tone: + U1F9D11F3FE200D1F91D200D1F9D11F3FC = "🧑🏾‍🤝‍🧑🏼" # :people_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: + U1F9D11F3FE200D1F91D200D1F9D11F3FD = "🧑🏾‍🤝‍🧑🏽" # :people_holding_hands_medium-dark_skin_tone_medium_skin_tone: + U1F9D11F3FC200D1F91D200D1F9D11F3FC = "🧑🏼‍🤝‍🧑🏼" # :people_holding_hands_medium-light_skin_tone: + U1F9D11F3FC200D1F91D200D1F9D11F3FF = "🧑🏼‍🤝‍🧑🏿" # :people_holding_hands_medium-light_skin_tone_dark_skin_tone: + U1F9D11F3FC200D1F91D200D1F9D11F3FB = "🧑🏼‍🤝‍🧑🏻" # :people_holding_hands_medium-light_skin_tone_light_skin_tone: + U1F9D11F3FC200D1F91D200D1F9D11F3FE = "🧑🏼‍🤝‍🧑🏾" # :people_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: + U1F9D11F3FC200D1F91D200D1F9D11F3FD = "🧑🏼‍🤝‍🧑🏽" # :people_holding_hands_medium-light_skin_tone_medium_skin_tone: + U1F9D11F3FD200D1F91D200D1F9D11F3FD = "🧑🏽‍🤝‍🧑🏽" # :people_holding_hands_medium_skin_tone: + U1F9D11F3FD200D1F91D200D1F9D11F3FF = "🧑🏽‍🤝‍🧑🏿" # :people_holding_hands_medium_skin_tone_dark_skin_tone: + U1F9D11F3FD200D1F91D200D1F9D11F3FB = "🧑🏽‍🤝‍🧑🏻" # :people_holding_hands_medium_skin_tone_light_skin_tone: + U1F9D11F3FD200D1F91D200D1F9D11F3FE = "🧑🏽‍🤝‍🧑🏾" # :people_holding_hands_medium_skin_tone_medium-dark_skin_tone: + U1F9D11F3FD200D1F91D200D1F9D11F3FC = "🧑🏽‍🤝‍🧑🏼" # :people_holding_hands_medium_skin_tone_medium-light_skin_tone: + U1FAC2 = "🫂" # :people_hugging: + U1F46F = "👯" # :people_with_bunny_ears: + U1F93C = "🤼" # :people_wrestling: + U1F3AD = "🎭" # :performing_arts: + U1F623 = "😣" # :persevering_face: + U1F9D1 = "🧑" # :person: + U1F9D1200D1F9B2 = "🧑‍🦲" # :person_bald: + U1F9D4 = "🧔" # :person_beard: + U1F6B4 = "🚴" # :person_biking: + U1F6B41F3FF = "🚴🏿" # :person_biking_dark_skin_tone: + U1F6B41F3FB = "🚴🏻" # :person_biking_light_skin_tone: + U1F6B41F3FE = "🚴🏾" # :person_biking_medium-dark_skin_tone: + U1F6B41F3FC = "🚴🏼" # :person_biking_medium-light_skin_tone: + U1F6B41F3FD = "🚴🏽" # :person_biking_medium_skin_tone: + U1F471 = "👱" # :person_blond_hair: + U26F9FE0F = "⛹️" # :person_bouncing_ball: + U26F9 = "⛹" # :person_bouncing_ball: + U26F91F3FF = "⛹🏿" # :person_bouncing_ball_dark_skin_tone: + U26F91F3FB = "⛹🏻" # :person_bouncing_ball_light_skin_tone: + U26F91F3FE = "⛹🏾" # :person_bouncing_ball_medium-dark_skin_tone: + U26F91F3FC = "⛹🏼" # :person_bouncing_ball_medium-light_skin_tone: + U26F91F3FD = "⛹🏽" # :person_bouncing_ball_medium_skin_tone: + U1F647 = "🙇" # :person_bowing: + U1F6471F3FF = "🙇🏿" # :person_bowing_dark_skin_tone: + U1F6471F3FB = "🙇🏻" # :person_bowing_light_skin_tone: + U1F6471F3FE = "🙇🏾" # :person_bowing_medium-dark_skin_tone: + U1F6471F3FC = "🙇🏼" # :person_bowing_medium-light_skin_tone: + U1F6471F3FD = "🙇🏽" # :person_bowing_medium_skin_tone: + U1F938 = "🤸" # :person_cartwheeling: + U1F9381F3FF = "🤸🏿" # :person_cartwheeling_dark_skin_tone: + U1F9381F3FB = "🤸🏻" # :person_cartwheeling_light_skin_tone: + U1F9381F3FE = "🤸🏾" # :person_cartwheeling_medium-dark_skin_tone: + U1F9381F3FC = "🤸🏼" # :person_cartwheeling_medium-light_skin_tone: + U1F9381F3FD = "🤸🏽" # :person_cartwheeling_medium_skin_tone: + U1F9D7 = "🧗" # :person_climbing: + U1F9D71F3FF = "🧗🏿" # :person_climbing_dark_skin_tone: + U1F9D71F3FB = "🧗🏻" # :person_climbing_light_skin_tone: + U1F9D71F3FE = "🧗🏾" # :person_climbing_medium-dark_skin_tone: + U1F9D71F3FC = "🧗🏼" # :person_climbing_medium-light_skin_tone: + U1F9D71F3FD = "🧗🏽" # :person_climbing_medium_skin_tone: + U1F9D1200D1F9B1 = "🧑‍🦱" # :person_curly_hair: + U1F9D11F3FF = "🧑🏿" # :person_dark_skin_tone: + U1F9D11F3FF200D1F9B2 = "🧑🏿‍🦲" # :person_dark_skin_tone_bald: + U1F9D41F3FF = "🧔🏿" # :person_dark_skin_tone_beard: + U1F4711F3FF = "👱🏿" # :person_dark_skin_tone_blond_hair: + U1F9D11F3FF200D1F9B1 = "🧑🏿‍🦱" # :person_dark_skin_tone_curly_hair: + U1F9D11F3FF200D1F9B0 = "🧑🏿‍🦰" # :person_dark_skin_tone_red_hair: + U1F9D11F3FF200D1F9B3 = "🧑🏿‍🦳" # :person_dark_skin_tone_white_hair: + U1F926 = "🤦" # :person_facepalming: + U1F9261F3FF = "🤦🏿" # :person_facepalming_dark_skin_tone: + U1F9261F3FB = "🤦🏻" # :person_facepalming_light_skin_tone: + U1F9261F3FE = "🤦🏾" # :person_facepalming_medium-dark_skin_tone: + U1F9261F3FC = "🤦🏼" # :person_facepalming_medium-light_skin_tone: + U1F9261F3FD = "🤦🏽" # :person_facepalming_medium_skin_tone: + U1F9D1200D1F37C = "🧑‍🍼" # :person_feeding_baby: + U1F9D11F3FF200D1F37C = "🧑🏿‍🍼" # :person_feeding_baby_dark_skin_tone: + U1F9D11F3FB200D1F37C = "🧑🏻‍🍼" # :person_feeding_baby_light_skin_tone: + U1F9D11F3FE200D1F37C = "🧑🏾‍🍼" # :person_feeding_baby_medium-dark_skin_tone: + U1F9D11F3FC200D1F37C = "🧑🏼‍🍼" # :person_feeding_baby_medium-light_skin_tone: + U1F9D11F3FD200D1F37C = "🧑🏽‍🍼" # :person_feeding_baby_medium_skin_tone: + U1F93A = "🤺" # :person_fencing: + U1F64D = "🙍" # :person_frowning: + U1F64D1F3FF = "🙍🏿" # :person_frowning_dark_skin_tone: + U1F64D1F3FB = "🙍🏻" # :person_frowning_light_skin_tone: + U1F64D1F3FE = "🙍🏾" # :person_frowning_medium-dark_skin_tone: + U1F64D1F3FC = "🙍🏼" # :person_frowning_medium-light_skin_tone: + U1F64D1F3FD = "🙍🏽" # :person_frowning_medium_skin_tone: + U1F645 = "🙅" # :person_gesturing_NO: + U1F6451F3FF = "🙅🏿" # :person_gesturing_NO_dark_skin_tone: + U1F6451F3FB = "🙅🏻" # :person_gesturing_NO_light_skin_tone: + U1F6451F3FE = "🙅🏾" # :person_gesturing_NO_medium-dark_skin_tone: + U1F6451F3FC = "🙅🏼" # :person_gesturing_NO_medium-light_skin_tone: + U1F6451F3FD = "🙅🏽" # :person_gesturing_NO_medium_skin_tone: + U1F646 = "🙆" # :person_gesturing_OK: + U1F6461F3FF = "🙆🏿" # :person_gesturing_OK_dark_skin_tone: + U1F6461F3FB = "🙆🏻" # :person_gesturing_OK_light_skin_tone: + U1F6461F3FE = "🙆🏾" # :person_gesturing_OK_medium-dark_skin_tone: + U1F6461F3FC = "🙆🏼" # :person_gesturing_OK_medium-light_skin_tone: + U1F6461F3FD = "🙆🏽" # :person_gesturing_OK_medium_skin_tone: + U1F487 = "💇" # :person_getting_haircut: + U1F4871F3FF = "💇🏿" # :person_getting_haircut_dark_skin_tone: + U1F4871F3FB = "💇🏻" # :person_getting_haircut_light_skin_tone: + U1F4871F3FE = "💇🏾" # :person_getting_haircut_medium-dark_skin_tone: + U1F4871F3FC = "💇🏼" # :person_getting_haircut_medium-light_skin_tone: + U1F4871F3FD = "💇🏽" # :person_getting_haircut_medium_skin_tone: + U1F486 = "💆" # :person_getting_massage: + U1F4861F3FF = "💆🏿" # :person_getting_massage_dark_skin_tone: + U1F4861F3FB = "💆🏻" # :person_getting_massage_light_skin_tone: + U1F4861F3FE = "💆🏾" # :person_getting_massage_medium-dark_skin_tone: + U1F4861F3FC = "💆🏼" # :person_getting_massage_medium-light_skin_tone: + U1F4861F3FD = "💆🏽" # :person_getting_massage_medium_skin_tone: + U1F3CCFE0F = "🏌️" # :person_golfing: + U1F3CC = "🏌" # :person_golfing: + U1F3CC1F3FF = "🏌🏿" # :person_golfing_dark_skin_tone: + U1F3CC1F3FB = "🏌🏻" # :person_golfing_light_skin_tone: + U1F3CC1F3FE = "🏌🏾" # :person_golfing_medium-dark_skin_tone: + U1F3CC1F3FC = "🏌🏼" # :person_golfing_medium-light_skin_tone: + U1F3CC1F3FD = "🏌🏽" # :person_golfing_medium_skin_tone: + U1F6CC = "🛌" # :person_in_bed: + U1F6CC1F3FF = "🛌🏿" # :person_in_bed_dark_skin_tone: + U1F6CC1F3FB = "🛌🏻" # :person_in_bed_light_skin_tone: + U1F6CC1F3FE = "🛌🏾" # :person_in_bed_medium-dark_skin_tone: + U1F6CC1F3FC = "🛌🏼" # :person_in_bed_medium-light_skin_tone: + U1F6CC1F3FD = "🛌🏽" # :person_in_bed_medium_skin_tone: + U1F9D8 = "🧘" # :person_in_lotus_position: + U1F9D81F3FF = "🧘🏿" # :person_in_lotus_position_dark_skin_tone: + U1F9D81F3FB = "🧘🏻" # :person_in_lotus_position_light_skin_tone: + U1F9D81F3FE = "🧘🏾" # :person_in_lotus_position_medium-dark_skin_tone: + U1F9D81F3FC = "🧘🏼" # :person_in_lotus_position_medium-light_skin_tone: + U1F9D81F3FD = "🧘🏽" # :person_in_lotus_position_medium_skin_tone: + U1F9D1200D1F9BD = "🧑‍🦽" # :person_in_manual_wheelchair: + U1F9D11F3FF200D1F9BD = "🧑🏿‍🦽" # :person_in_manual_wheelchair_dark_skin_tone: + U1F9D1200D1F9BD200D27A1FE0F = "🧑‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right: + U1F9D1200D1F9BD200D27A1 = "🧑‍🦽‍➡" # :person_in_manual_wheelchair_facing_right: + U1F9D11F3FF200D1F9BD200D27A1FE0F = "🧑🏿‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F9D11F3FF200D1F9BD200D27A1 = "🧑🏿‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F9D11F3FB200D1F9BD200D27A1FE0F = "🧑🏻‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_light_skin_tone: + U1F9D11F3FB200D1F9BD200D27A1 = "🧑🏻‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_light_skin_tone: + U1F9D11F3FE200D1F9BD200D27A1FE0F = "🧑🏾‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F9D11F3FE200D1F9BD200D27A1 = "🧑🏾‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F9D11F3FC200D1F9BD200D27A1FE0F = "🧑🏼‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F9D11F3FC200D1F9BD200D27A1 = "🧑🏼‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F9D11F3FD200D1F9BD200D27A1FE0F = "🧑🏽‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F9D11F3FD200D1F9BD200D27A1 = "🧑🏽‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F9D11F3FB200D1F9BD = "🧑🏻‍🦽" # :person_in_manual_wheelchair_light_skin_tone: + U1F9D11F3FE200D1F9BD = "🧑🏾‍🦽" # :person_in_manual_wheelchair_medium-dark_skin_tone: + U1F9D11F3FC200D1F9BD = "🧑🏼‍🦽" # :person_in_manual_wheelchair_medium-light_skin_tone: + U1F9D11F3FD200D1F9BD = "🧑🏽‍🦽" # :person_in_manual_wheelchair_medium_skin_tone: + U1F9D1200D1F9BC = "🧑‍🦼" # :person_in_motorized_wheelchair: + U1F9D11F3FF200D1F9BC = "🧑🏿‍🦼" # :person_in_motorized_wheelchair_dark_skin_tone: + U1F9D1200D1F9BC200D27A1FE0F = "🧑‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right: + U1F9D1200D1F9BC200D27A1 = "🧑‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right: + U1F9D11F3FF200D1F9BC200D27A1FE0F = "🧑🏿‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F9D11F3FF200D1F9BC200D27A1 = "🧑🏿‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F9D11F3FB200D1F9BC200D27A1FE0F = "🧑🏻‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F9D11F3FB200D1F9BC200D27A1 = "🧑🏻‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F9D11F3FE200D1F9BC200D27A1FE0F = "🧑🏾‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F9D11F3FE200D1F9BC200D27A1 = "🧑🏾‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F9D11F3FC200D1F9BC200D27A1FE0F = "🧑🏼‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F9D11F3FC200D1F9BC200D27A1 = "🧑🏼‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F9D11F3FD200D1F9BC200D27A1FE0F = "🧑🏽‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F9D11F3FD200D1F9BC200D27A1 = "🧑🏽‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F9D11F3FB200D1F9BC = "🧑🏻‍🦼" # :person_in_motorized_wheelchair_light_skin_tone: + U1F9D11F3FE200D1F9BC = "🧑🏾‍🦼" # :person_in_motorized_wheelchair_medium-dark_skin_tone: + U1F9D11F3FC200D1F9BC = "🧑🏼‍🦼" # :person_in_motorized_wheelchair_medium-light_skin_tone: + U1F9D11F3FD200D1F9BC = "🧑🏽‍🦼" # :person_in_motorized_wheelchair_medium_skin_tone: + U1F9D6 = "🧖" # :person_in_steamy_room: + U1F9D61F3FF = "🧖🏿" # :person_in_steamy_room_dark_skin_tone: + U1F9D61F3FB = "🧖🏻" # :person_in_steamy_room_light_skin_tone: + U1F9D61F3FE = "🧖🏾" # :person_in_steamy_room_medium-dark_skin_tone: + U1F9D61F3FC = "🧖🏼" # :person_in_steamy_room_medium-light_skin_tone: + U1F9D61F3FD = "🧖🏽" # :person_in_steamy_room_medium_skin_tone: + U1F574FE0F = "🕴️" # :person_in_suit_levitating: + U1F574 = "🕴" # :person_in_suit_levitating: + U1F5741F3FF = "🕴🏿" # :person_in_suit_levitating_dark_skin_tone: + U1F5741F3FB = "🕴🏻" # :person_in_suit_levitating_light_skin_tone: + U1F5741F3FE = "🕴🏾" # :person_in_suit_levitating_medium-dark_skin_tone: + U1F5741F3FC = "🕴🏼" # :person_in_suit_levitating_medium-light_skin_tone: + U1F5741F3FD = "🕴🏽" # :person_in_suit_levitating_medium_skin_tone: + U1F935 = "🤵" # :person_in_tuxedo: + U1F9351F3FF = "🤵🏿" # :person_in_tuxedo_dark_skin_tone: + U1F9351F3FB = "🤵🏻" # :person_in_tuxedo_light_skin_tone: + U1F9351F3FE = "🤵🏾" # :person_in_tuxedo_medium-dark_skin_tone: + U1F9351F3FC = "🤵🏼" # :person_in_tuxedo_medium-light_skin_tone: + U1F9351F3FD = "🤵🏽" # :person_in_tuxedo_medium_skin_tone: + U1F939 = "🤹" # :person_juggling: + U1F9391F3FF = "🤹🏿" # :person_juggling_dark_skin_tone: + U1F9391F3FB = "🤹🏻" # :person_juggling_light_skin_tone: + U1F9391F3FE = "🤹🏾" # :person_juggling_medium-dark_skin_tone: + U1F9391F3FC = "🤹🏼" # :person_juggling_medium-light_skin_tone: + U1F9391F3FD = "🤹🏽" # :person_juggling_medium_skin_tone: + U1F9CE = "🧎" # :person_kneeling: + U1F9CE1F3FF = "🧎🏿" # :person_kneeling_dark_skin_tone: + U1F9CE200D27A1FE0F = "🧎‍➡️" # :person_kneeling_facing_right: + U1F9CE200D27A1 = "🧎‍➡" # :person_kneeling_facing_right: + U1F9CE1F3FF200D27A1FE0F = "🧎🏿‍➡️" # :person_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D27A1 = "🧎🏿‍➡" # :person_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FB200D27A1FE0F = "🧎🏻‍➡️" # :person_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D27A1 = "🧎🏻‍➡" # :person_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FE200D27A1FE0F = "🧎🏾‍➡️" # :person_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D27A1 = "🧎🏾‍➡" # :person_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FC200D27A1FE0F = "🧎🏼‍➡️" # :person_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D27A1 = "🧎🏼‍➡" # :person_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FD200D27A1FE0F = "🧎🏽‍➡️" # :person_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D27A1 = "🧎🏽‍➡" # :person_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FB = "🧎🏻" # :person_kneeling_light_skin_tone: + U1F9CE1F3FE = "🧎🏾" # :person_kneeling_medium-dark_skin_tone: + U1F9CE1F3FC = "🧎🏼" # :person_kneeling_medium-light_skin_tone: + U1F9CE1F3FD = "🧎🏽" # :person_kneeling_medium_skin_tone: + U1F3CBFE0F = "🏋️" # :person_lifting_weights: + U1F3CB = "🏋" # :person_lifting_weights: + U1F3CB1F3FF = "🏋🏿" # :person_lifting_weights_dark_skin_tone: + U1F3CB1F3FB = "🏋🏻" # :person_lifting_weights_light_skin_tone: + U1F3CB1F3FE = "🏋🏾" # :person_lifting_weights_medium-dark_skin_tone: + U1F3CB1F3FC = "🏋🏼" # :person_lifting_weights_medium-light_skin_tone: + U1F3CB1F3FD = "🏋🏽" # :person_lifting_weights_medium_skin_tone: + U1F9D11F3FB = "🧑🏻" # :person_light_skin_tone: + U1F9D11F3FB200D1F9B2 = "🧑🏻‍🦲" # :person_light_skin_tone_bald: + U1F9D41F3FB = "🧔🏻" # :person_light_skin_tone_beard: + U1F4711F3FB = "👱🏻" # :person_light_skin_tone_blond_hair: + U1F9D11F3FB200D1F9B1 = "🧑🏻‍🦱" # :person_light_skin_tone_curly_hair: + U1F9D11F3FB200D1F9B0 = "🧑🏻‍🦰" # :person_light_skin_tone_red_hair: + U1F9D11F3FB200D1F9B3 = "🧑🏻‍🦳" # :person_light_skin_tone_white_hair: + U1F9D11F3FE = "🧑🏾" # :person_medium-dark_skin_tone: + U1F9D11F3FE200D1F9B2 = "🧑🏾‍🦲" # :person_medium-dark_skin_tone_bald: + U1F9D41F3FE = "🧔🏾" # :person_medium-dark_skin_tone_beard: + U1F4711F3FE = "👱🏾" # :person_medium-dark_skin_tone_blond_hair: + U1F9D11F3FE200D1F9B1 = "🧑🏾‍🦱" # :person_medium-dark_skin_tone_curly_hair: + U1F9D11F3FE200D1F9B0 = "🧑🏾‍🦰" # :person_medium-dark_skin_tone_red_hair: + U1F9D11F3FE200D1F9B3 = "🧑🏾‍🦳" # :person_medium-dark_skin_tone_white_hair: + U1F9D11F3FC = "🧑🏼" # :person_medium-light_skin_tone: + U1F9D11F3FC200D1F9B2 = "🧑🏼‍🦲" # :person_medium-light_skin_tone_bald: + U1F9D41F3FC = "🧔🏼" # :person_medium-light_skin_tone_beard: + U1F4711F3FC = "👱🏼" # :person_medium-light_skin_tone_blond_hair: + U1F9D11F3FC200D1F9B1 = "🧑🏼‍🦱" # :person_medium-light_skin_tone_curly_hair: + U1F9D11F3FC200D1F9B0 = "🧑🏼‍🦰" # :person_medium-light_skin_tone_red_hair: + U1F9D11F3FC200D1F9B3 = "🧑🏼‍🦳" # :person_medium-light_skin_tone_white_hair: + U1F9D11F3FD = "🧑🏽" # :person_medium_skin_tone: + U1F9D11F3FD200D1F9B2 = "🧑🏽‍🦲" # :person_medium_skin_tone_bald: + U1F9D41F3FD = "🧔🏽" # :person_medium_skin_tone_beard: + U1F4711F3FD = "👱🏽" # :person_medium_skin_tone_blond_hair: + U1F9D11F3FD200D1F9B1 = "🧑🏽‍🦱" # :person_medium_skin_tone_curly_hair: + U1F9D11F3FD200D1F9B0 = "🧑🏽‍🦰" # :person_medium_skin_tone_red_hair: + U1F9D11F3FD200D1F9B3 = "🧑🏽‍🦳" # :person_medium_skin_tone_white_hair: + U1F6B5 = "🚵" # :person_mountain_biking: + U1F6B51F3FF = "🚵🏿" # :person_mountain_biking_dark_skin_tone: + U1F6B51F3FB = "🚵🏻" # :person_mountain_biking_light_skin_tone: + U1F6B51F3FE = "🚵🏾" # :person_mountain_biking_medium-dark_skin_tone: + U1F6B51F3FC = "🚵🏼" # :person_mountain_biking_medium-light_skin_tone: + U1F6B51F3FD = "🚵🏽" # :person_mountain_biking_medium_skin_tone: + U1F93E = "🤾" # :person_playing_handball: + U1F93E1F3FF = "🤾🏿" # :person_playing_handball_dark_skin_tone: + U1F93E1F3FB = "🤾🏻" # :person_playing_handball_light_skin_tone: + U1F93E1F3FE = "🤾🏾" # :person_playing_handball_medium-dark_skin_tone: + U1F93E1F3FC = "🤾🏼" # :person_playing_handball_medium-light_skin_tone: + U1F93E1F3FD = "🤾🏽" # :person_playing_handball_medium_skin_tone: + U1F93D = "🤽" # :person_playing_water_polo: + U1F93D1F3FF = "🤽🏿" # :person_playing_water_polo_dark_skin_tone: + U1F93D1F3FB = "🤽🏻" # :person_playing_water_polo_light_skin_tone: + U1F93D1F3FE = "🤽🏾" # :person_playing_water_polo_medium-dark_skin_tone: + U1F93D1F3FC = "🤽🏼" # :person_playing_water_polo_medium-light_skin_tone: + U1F93D1F3FD = "🤽🏽" # :person_playing_water_polo_medium_skin_tone: + U1F64E = "🙎" # :person_pouting: + U1F64E1F3FF = "🙎🏿" # :person_pouting_dark_skin_tone: + U1F64E1F3FB = "🙎🏻" # :person_pouting_light_skin_tone: + U1F64E1F3FE = "🙎🏾" # :person_pouting_medium-dark_skin_tone: + U1F64E1F3FC = "🙎🏼" # :person_pouting_medium-light_skin_tone: + U1F64E1F3FD = "🙎🏽" # :person_pouting_medium_skin_tone: + U1F64B = "🙋" # :person_raising_hand: + U1F64B1F3FF = "🙋🏿" # :person_raising_hand_dark_skin_tone: + U1F64B1F3FB = "🙋🏻" # :person_raising_hand_light_skin_tone: + U1F64B1F3FE = "🙋🏾" # :person_raising_hand_medium-dark_skin_tone: + U1F64B1F3FC = "🙋🏼" # :person_raising_hand_medium-light_skin_tone: + U1F64B1F3FD = "🙋🏽" # :person_raising_hand_medium_skin_tone: + U1F9D1200D1F9B0 = "🧑‍🦰" # :person_red_hair: + U1F6A3 = "🚣" # :person_rowing_boat: + U1F6A31F3FF = "🚣🏿" # :person_rowing_boat_dark_skin_tone: + U1F6A31F3FB = "🚣🏻" # :person_rowing_boat_light_skin_tone: + U1F6A31F3FE = "🚣🏾" # :person_rowing_boat_medium-dark_skin_tone: + U1F6A31F3FC = "🚣🏼" # :person_rowing_boat_medium-light_skin_tone: + U1F6A31F3FD = "🚣🏽" # :person_rowing_boat_medium_skin_tone: + U1F3C3 = "🏃" # :person_running: + U1F3C31F3FF = "🏃🏿" # :person_running_dark_skin_tone: + U1F3C3200D27A1FE0F = "🏃‍➡️" # :person_running_facing_right: + U1F3C3200D27A1 = "🏃‍➡" # :person_running_facing_right: + U1F3C31F3FF200D27A1FE0F = "🏃🏿‍➡️" # :person_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D27A1 = "🏃🏿‍➡" # :person_running_facing_right_dark_skin_tone: + U1F3C31F3FB200D27A1FE0F = "🏃🏻‍➡️" # :person_running_facing_right_light_skin_tone: + U1F3C31F3FB200D27A1 = "🏃🏻‍➡" # :person_running_facing_right_light_skin_tone: + U1F3C31F3FE200D27A1FE0F = "🏃🏾‍➡️" # :person_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D27A1 = "🏃🏾‍➡" # :person_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FC200D27A1FE0F = "🏃🏼‍➡️" # :person_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D27A1 = "🏃🏼‍➡" # :person_running_facing_right_medium-light_skin_tone: + U1F3C31F3FD200D27A1FE0F = "🏃🏽‍➡️" # :person_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D27A1 = "🏃🏽‍➡" # :person_running_facing_right_medium_skin_tone: + U1F3C31F3FB = "🏃🏻" # :person_running_light_skin_tone: + U1F3C31F3FE = "🏃🏾" # :person_running_medium-dark_skin_tone: + U1F3C31F3FC = "🏃🏼" # :person_running_medium-light_skin_tone: + U1F3C31F3FD = "🏃🏽" # :person_running_medium_skin_tone: + U1F937 = "🤷" # :person_shrugging: + U1F9371F3FF = "🤷🏿" # :person_shrugging_dark_skin_tone: + U1F9371F3FB = "🤷🏻" # :person_shrugging_light_skin_tone: + U1F9371F3FE = "🤷🏾" # :person_shrugging_medium-dark_skin_tone: + U1F9371F3FC = "🤷🏼" # :person_shrugging_medium-light_skin_tone: + U1F9371F3FD = "🤷🏽" # :person_shrugging_medium_skin_tone: + U1F9CD = "🧍" # :person_standing: + U1F9CD1F3FF = "🧍🏿" # :person_standing_dark_skin_tone: + U1F9CD1F3FB = "🧍🏻" # :person_standing_light_skin_tone: + U1F9CD1F3FE = "🧍🏾" # :person_standing_medium-dark_skin_tone: + U1F9CD1F3FC = "🧍🏼" # :person_standing_medium-light_skin_tone: + U1F9CD1F3FD = "🧍🏽" # :person_standing_medium_skin_tone: + U1F3C4 = "🏄" # :person_surfing: + U1F3C41F3FF = "🏄🏿" # :person_surfing_dark_skin_tone: + U1F3C41F3FB = "🏄🏻" # :person_surfing_light_skin_tone: + U1F3C41F3FE = "🏄🏾" # :person_surfing_medium-dark_skin_tone: + U1F3C41F3FC = "🏄🏼" # :person_surfing_medium-light_skin_tone: + U1F3C41F3FD = "🏄🏽" # :person_surfing_medium_skin_tone: + U1F3CA = "🏊" # :person_swimming: + U1F3CA1F3FF = "🏊🏿" # :person_swimming_dark_skin_tone: + U1F3CA1F3FB = "🏊🏻" # :person_swimming_light_skin_tone: + U1F3CA1F3FE = "🏊🏾" # :person_swimming_medium-dark_skin_tone: + U1F3CA1F3FC = "🏊🏼" # :person_swimming_medium-light_skin_tone: + U1F3CA1F3FD = "🏊🏽" # :person_swimming_medium_skin_tone: + U1F6C0 = "🛀" # :person_taking_bath: + U1F6C01F3FF = "🛀🏿" # :person_taking_bath_dark_skin_tone: + U1F6C01F3FB = "🛀🏻" # :person_taking_bath_light_skin_tone: + U1F6C01F3FE = "🛀🏾" # :person_taking_bath_medium-dark_skin_tone: + U1F6C01F3FC = "🛀🏼" # :person_taking_bath_medium-light_skin_tone: + U1F6C01F3FD = "🛀🏽" # :person_taking_bath_medium_skin_tone: + U1F481 = "💁" # :person_tipping_hand: + U1F4811F3FF = "💁🏿" # :person_tipping_hand_dark_skin_tone: + U1F4811F3FB = "💁🏻" # :person_tipping_hand_light_skin_tone: + U1F4811F3FE = "💁🏾" # :person_tipping_hand_medium-dark_skin_tone: + U1F4811F3FC = "💁🏼" # :person_tipping_hand_medium-light_skin_tone: + U1F4811F3FD = "💁🏽" # :person_tipping_hand_medium_skin_tone: + U1F6B6 = "🚶" # :person_walking: + U1F6B61F3FF = "🚶🏿" # :person_walking_dark_skin_tone: + U1F6B6200D27A1FE0F = "🚶‍➡️" # :person_walking_facing_right: + U1F6B6200D27A1 = "🚶‍➡" # :person_walking_facing_right: + U1F6B61F3FF200D27A1FE0F = "🚶🏿‍➡️" # :person_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D27A1 = "🚶🏿‍➡" # :person_walking_facing_right_dark_skin_tone: + U1F6B61F3FB200D27A1FE0F = "🚶🏻‍➡️" # :person_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D27A1 = "🚶🏻‍➡" # :person_walking_facing_right_light_skin_tone: + U1F6B61F3FE200D27A1FE0F = "🚶🏾‍➡️" # :person_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D27A1 = "🚶🏾‍➡" # :person_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FC200D27A1FE0F = "🚶🏼‍➡️" # :person_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D27A1 = "🚶🏼‍➡" # :person_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FD200D27A1FE0F = "🚶🏽‍➡️" # :person_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D27A1 = "🚶🏽‍➡" # :person_walking_facing_right_medium_skin_tone: + U1F6B61F3FB = "🚶🏻" # :person_walking_light_skin_tone: + U1F6B61F3FE = "🚶🏾" # :person_walking_medium-dark_skin_tone: + U1F6B61F3FC = "🚶🏼" # :person_walking_medium-light_skin_tone: + U1F6B61F3FD = "🚶🏽" # :person_walking_medium_skin_tone: + U1F473 = "👳" # :person_wearing_turban: + U1F4731F3FF = "👳🏿" # :person_wearing_turban_dark_skin_tone: + U1F4731F3FB = "👳🏻" # :person_wearing_turban_light_skin_tone: + U1F4731F3FE = "👳🏾" # :person_wearing_turban_medium-dark_skin_tone: + U1F4731F3FC = "👳🏼" # :person_wearing_turban_medium-light_skin_tone: + U1F4731F3FD = "👳🏽" # :person_wearing_turban_medium_skin_tone: + U1F9D1200D1F9B3 = "🧑‍🦳" # :person_white_hair: + U1FAC5 = "🫅" # :person_with_crown: + U1FAC51F3FF = "🫅🏿" # :person_with_crown_dark_skin_tone: + U1FAC51F3FB = "🫅🏻" # :person_with_crown_light_skin_tone: + U1FAC51F3FE = "🫅🏾" # :person_with_crown_medium-dark_skin_tone: + U1FAC51F3FC = "🫅🏼" # :person_with_crown_medium-light_skin_tone: + U1FAC51F3FD = "🫅🏽" # :person_with_crown_medium_skin_tone: + U1F472 = "👲" # :person_with_skullcap: + U1F4721F3FF = "👲🏿" # :person_with_skullcap_dark_skin_tone: + U1F4721F3FB = "👲🏻" # :person_with_skullcap_light_skin_tone: + U1F4721F3FE = "👲🏾" # :person_with_skullcap_medium-dark_skin_tone: + U1F4721F3FC = "👲🏼" # :person_with_skullcap_medium-light_skin_tone: + U1F4721F3FD = "👲🏽" # :person_with_skullcap_medium_skin_tone: + U1F470 = "👰" # :person_with_veil: + U1F4701F3FF = "👰🏿" # :person_with_veil_dark_skin_tone: + U1F4701F3FB = "👰🏻" # :person_with_veil_light_skin_tone: + U1F4701F3FE = "👰🏾" # :person_with_veil_medium-dark_skin_tone: + U1F4701F3FC = "👰🏼" # :person_with_veil_medium-light_skin_tone: + U1F4701F3FD = "👰🏽" # :person_with_veil_medium_skin_tone: + U1F9D1200D1F9AF = "🧑‍🦯" # :person_with_white_cane: + U1F9D11F3FF200D1F9AF = "🧑🏿‍🦯" # :person_with_white_cane_dark_skin_tone: + U1F9D1200D1F9AF200D27A1FE0F = "🧑‍🦯‍➡️" # :person_with_white_cane_facing_right: + U1F9D1200D1F9AF200D27A1 = "🧑‍🦯‍➡" # :person_with_white_cane_facing_right: + U1F9D11F3FF200D1F9AF200D27A1FE0F = "🧑🏿‍🦯‍➡️" # :person_with_white_cane_facing_right_dark_skin_tone: + U1F9D11F3FF200D1F9AF200D27A1 = "🧑🏿‍🦯‍➡" # :person_with_white_cane_facing_right_dark_skin_tone: + U1F9D11F3FB200D1F9AF200D27A1FE0F = "🧑🏻‍🦯‍➡️" # :person_with_white_cane_facing_right_light_skin_tone: + U1F9D11F3FB200D1F9AF200D27A1 = "🧑🏻‍🦯‍➡" # :person_with_white_cane_facing_right_light_skin_tone: + U1F9D11F3FE200D1F9AF200D27A1FE0F = "🧑🏾‍🦯‍➡️" # :person_with_white_cane_facing_right_medium-dark_skin_tone: + U1F9D11F3FE200D1F9AF200D27A1 = "🧑🏾‍🦯‍➡" # :person_with_white_cane_facing_right_medium-dark_skin_tone: + U1F9D11F3FC200D1F9AF200D27A1FE0F = "🧑🏼‍🦯‍➡️" # :person_with_white_cane_facing_right_medium-light_skin_tone: + U1F9D11F3FC200D1F9AF200D27A1 = "🧑🏼‍🦯‍➡" # :person_with_white_cane_facing_right_medium-light_skin_tone: + U1F9D11F3FD200D1F9AF200D27A1FE0F = "🧑🏽‍🦯‍➡️" # :person_with_white_cane_facing_right_medium_skin_tone: + U1F9D11F3FD200D1F9AF200D27A1 = "🧑🏽‍🦯‍➡" # :person_with_white_cane_facing_right_medium_skin_tone: + U1F9D11F3FB200D1F9AF = "🧑🏻‍🦯" # :person_with_white_cane_light_skin_tone: + U1F9D11F3FE200D1F9AF = "🧑🏾‍🦯" # :person_with_white_cane_medium-dark_skin_tone: + U1F9D11F3FC200D1F9AF = "🧑🏼‍🦯" # :person_with_white_cane_medium-light_skin_tone: + U1F9D11F3FD200D1F9AF = "🧑🏽‍🦯" # :person_with_white_cane_medium_skin_tone: + U1F9EB = "🧫" # :petri_dish: + U1F426200D1F525 = "🐦‍🔥" # :phoenix: + U26CFFE0F = "⛏️" # :pick: + U26CF = "⛏" # :pick: + U1F6FB = "🛻" # :pickup_truck: + U1F967 = "🥧" # :pie: + U1F416 = "🐖" # :pig: + U1F437 = "🐷" # :pig_face: + U1F43D = "🐽" # :pig_nose: + U1F4A9 = "💩" # :pile_of_poo: + U1F48A = "💊" # :pill: + U1F9D1200D2708FE0F = "🧑‍✈️" # :pilot: + U1F9D1200D2708 = "🧑‍✈" # :pilot: + U1F9D11F3FF200D2708FE0F = "🧑🏿‍✈️" # :pilot_dark_skin_tone: + U1F9D11F3FF200D2708 = "🧑🏿‍✈" # :pilot_dark_skin_tone: + U1F9D11F3FB200D2708FE0F = "🧑🏻‍✈️" # :pilot_light_skin_tone: + U1F9D11F3FB200D2708 = "🧑🏻‍✈" # :pilot_light_skin_tone: + U1F9D11F3FE200D2708FE0F = "🧑🏾‍✈️" # :pilot_medium-dark_skin_tone: + U1F9D11F3FE200D2708 = "🧑🏾‍✈" # :pilot_medium-dark_skin_tone: + U1F9D11F3FC200D2708FE0F = "🧑🏼‍✈️" # :pilot_medium-light_skin_tone: + U1F9D11F3FC200D2708 = "🧑🏼‍✈" # :pilot_medium-light_skin_tone: + U1F9D11F3FD200D2708FE0F = "🧑🏽‍✈️" # :pilot_medium_skin_tone: + U1F9D11F3FD200D2708 = "🧑🏽‍✈" # :pilot_medium_skin_tone: + U1F90C = "🤌" # :pinched_fingers: + U1F90C1F3FF = "🤌🏿" # :pinched_fingers_dark_skin_tone: + U1F90C1F3FB = "🤌🏻" # :pinched_fingers_light_skin_tone: + U1F90C1F3FE = "🤌🏾" # :pinched_fingers_medium-dark_skin_tone: + U1F90C1F3FC = "🤌🏼" # :pinched_fingers_medium-light_skin_tone: + U1F90C1F3FD = "🤌🏽" # :pinched_fingers_medium_skin_tone: + U1F90F = "🤏" # :pinching_hand: + U1F90F1F3FF = "🤏🏿" # :pinching_hand_dark_skin_tone: + U1F90F1F3FB = "🤏🏻" # :pinching_hand_light_skin_tone: + U1F90F1F3FE = "🤏🏾" # :pinching_hand_medium-dark_skin_tone: + U1F90F1F3FC = "🤏🏼" # :pinching_hand_medium-light_skin_tone: + U1F90F1F3FD = "🤏🏽" # :pinching_hand_medium_skin_tone: + U1F38D = "🎍" # :pine_decoration: + U1F34D = "🍍" # :pineapple: + U1F3D3 = "🏓" # :ping_pong: + U1FA77 = "🩷" # :pink_heart: + U1F3F4200D2620FE0F = "🏴‍☠️" # :pirate_flag: + U1F3F4200D2620 = "🏴‍☠" # :pirate_flag: + U1F355 = "🍕" # :pizza: + U1FA85 = "🪅" # :piñata: + U1FAA7 = "🪧" # :placard: + U1F6D0 = "🛐" # :place_of_worship: + U25B6FE0F = "▶️" # :play_button: + U25B6 = "▶" # :play_button: + U23EFFE0F = "⏯️" # :play_or_pause_button: + U23EF = "⏯" # :play_or_pause_button: + U1F6DD = "🛝" # :playground_slide: + U1F97A = "🥺" # :pleading_face: + U1FAA0 = "🪠" # :plunger: + U2795 = "➕" # :plus: + U1F43B200D2744FE0F = "🐻‍❄️" # :polar_bear: + U1F43B200D2744 = "🐻‍❄" # :polar_bear: + U1F693 = "🚓" # :police_car: + U1F6A8 = "🚨" # :police_car_light: + U1F46E = "👮" # :police_officer: + U1F46E1F3FF = "👮🏿" # :police_officer_dark_skin_tone: + U1F46E1F3FB = "👮🏻" # :police_officer_light_skin_tone: + U1F46E1F3FE = "👮🏾" # :police_officer_medium-dark_skin_tone: + U1F46E1F3FC = "👮🏼" # :police_officer_medium-light_skin_tone: + U1F46E1F3FD = "👮🏽" # :police_officer_medium_skin_tone: + U1F429 = "🐩" # :poodle: + U1F3B1 = "🎱" # :pool_8_ball: + U1F37F = "🍿" # :popcorn: + U1F3E4 = "🏤" # :post_office: + U1F4EF = "📯" # :postal_horn: + U1F4EE = "📮" # :postbox: + U1F372 = "🍲" # :pot_of_food: + U1F6B0 = "🚰" # :potable_water: + U1F954 = "🥔" # :potato: + U1FAB4 = "🪴" # :potted_plant: + U1F357 = "🍗" # :poultry_leg: + U1F4B7 = "💷" # :pound_banknote: + U1FAD7 = "🫗" # :pouring_liquid: + U1F63E = "😾" # :pouting_cat: + U1F4FF = "📿" # :prayer_beads: + U1FAC3 = "🫃" # :pregnant_man: + U1FAC31F3FF = "🫃🏿" # :pregnant_man_dark_skin_tone: + U1FAC31F3FB = "🫃🏻" # :pregnant_man_light_skin_tone: + U1FAC31F3FE = "🫃🏾" # :pregnant_man_medium-dark_skin_tone: + U1FAC31F3FC = "🫃🏼" # :pregnant_man_medium-light_skin_tone: + U1FAC31F3FD = "🫃🏽" # :pregnant_man_medium_skin_tone: + U1FAC4 = "🫄" # :pregnant_person: + U1FAC41F3FF = "🫄🏿" # :pregnant_person_dark_skin_tone: + U1FAC41F3FB = "🫄🏻" # :pregnant_person_light_skin_tone: + U1FAC41F3FE = "🫄🏾" # :pregnant_person_medium-dark_skin_tone: + U1FAC41F3FC = "🫄🏼" # :pregnant_person_medium-light_skin_tone: + U1FAC41F3FD = "🫄🏽" # :pregnant_person_medium_skin_tone: + U1F930 = "🤰" # :pregnant_woman: + U1F9301F3FF = "🤰🏿" # :pregnant_woman_dark_skin_tone: + U1F9301F3FB = "🤰🏻" # :pregnant_woman_light_skin_tone: + U1F9301F3FE = "🤰🏾" # :pregnant_woman_medium-dark_skin_tone: + U1F9301F3FC = "🤰🏼" # :pregnant_woman_medium-light_skin_tone: + U1F9301F3FD = "🤰🏽" # :pregnant_woman_medium_skin_tone: + U1F968 = "🥨" # :pretzel: + U1F934 = "🤴" # :prince: + U1F9341F3FF = "🤴🏿" # :prince_dark_skin_tone: + U1F9341F3FB = "🤴🏻" # :prince_light_skin_tone: + U1F9341F3FE = "🤴🏾" # :prince_medium-dark_skin_tone: + U1F9341F3FC = "🤴🏼" # :prince_medium-light_skin_tone: + U1F9341F3FD = "🤴🏽" # :prince_medium_skin_tone: + U1F478 = "👸" # :princess: + U1F4781F3FF = "👸🏿" # :princess_dark_skin_tone: + U1F4781F3FB = "👸🏻" # :princess_light_skin_tone: + U1F4781F3FE = "👸🏾" # :princess_medium-dark_skin_tone: + U1F4781F3FC = "👸🏼" # :princess_medium-light_skin_tone: + U1F4781F3FD = "👸🏽" # :princess_medium_skin_tone: + U1F5A8FE0F = "🖨️" # :printer: + U1F5A8 = "🖨" # :printer: + U1F6AB = "🚫" # :prohibited: + U1F7E3 = "🟣" # :purple_circle: + U1F49C = "💜" # :purple_heart: + U1F7EA = "🟪" # :purple_square: + U1F45B = "👛" # :purse: + U1F4CC = "📌" # :pushpin: + U1F9E9 = "🧩" # :puzzle_piece: + U1F407 = "🐇" # :rabbit: + U1F430 = "🐰" # :rabbit_face: + U1F99D = "🦝" # :raccoon: + U1F3CEFE0F = "🏎️" # :racing_car: + U1F3CE = "🏎" # :racing_car: + U1F4FB = "📻" # :radio: + U1F518 = "🔘" # :radio_button: + U2622FE0F = "☢️" # :radioactive: + U2622 = "☢" # :radioactive: + U1F683 = "🚃" # :railway_car: + U1F6E4FE0F = "🛤️" # :railway_track: + U1F6E4 = "🛤" # :railway_track: + U1F308 = "🌈" # :rainbow: + U1F3F3FE0F200D1F308 = "🏳️‍🌈" # :rainbow_flag: + U1F3F3200D1F308 = "🏳‍🌈" # :rainbow_flag: + U1F91A = "🤚" # :raised_back_of_hand: + U1F91A1F3FF = "🤚🏿" # :raised_back_of_hand_dark_skin_tone: + U1F91A1F3FB = "🤚🏻" # :raised_back_of_hand_light_skin_tone: + U1F91A1F3FE = "🤚🏾" # :raised_back_of_hand_medium-dark_skin_tone: + U1F91A1F3FC = "🤚🏼" # :raised_back_of_hand_medium-light_skin_tone: + U1F91A1F3FD = "🤚🏽" # :raised_back_of_hand_medium_skin_tone: + U270A = "✊" # :raised_fist: + U270A1F3FF = "✊🏿" # :raised_fist_dark_skin_tone: + U270A1F3FB = "✊🏻" # :raised_fist_light_skin_tone: + U270A1F3FE = "✊🏾" # :raised_fist_medium-dark_skin_tone: + U270A1F3FC = "✊🏼" # :raised_fist_medium-light_skin_tone: + U270A1F3FD = "✊🏽" # :raised_fist_medium_skin_tone: + U270B = "✋" # :raised_hand: + U270B1F3FF = "✋🏿" # :raised_hand_dark_skin_tone: + U270B1F3FB = "✋🏻" # :raised_hand_light_skin_tone: + U270B1F3FE = "✋🏾" # :raised_hand_medium-dark_skin_tone: + U270B1F3FC = "✋🏼" # :raised_hand_medium-light_skin_tone: + U270B1F3FD = "✋🏽" # :raised_hand_medium_skin_tone: + U1F64C = "🙌" # :raising_hands: + U1F64C1F3FF = "🙌🏿" # :raising_hands_dark_skin_tone: + U1F64C1F3FB = "🙌🏻" # :raising_hands_light_skin_tone: + U1F64C1F3FE = "🙌🏾" # :raising_hands_medium-dark_skin_tone: + U1F64C1F3FC = "🙌🏼" # :raising_hands_medium-light_skin_tone: + U1F64C1F3FD = "🙌🏽" # :raising_hands_medium_skin_tone: + U1F40F = "🐏" # :ram: + U1F400 = "🐀" # :rat: + U1FA92 = "🪒" # :razor: + U1F9FE = "🧾" # :receipt: + U23FAFE0F = "⏺️" # :record_button: + U23FA = "⏺" # :record_button: + U267BFE0F = "♻️" # :recycling_symbol: + U267B = "♻" # :recycling_symbol: + U1F34E = "🍎" # :red_apple: + U1F534 = "🔴" # :red_circle: + U1F9E7 = "🧧" # :red_envelope: + U2757 = "❗" # :red_exclamation_mark: + U1F9B0 = "🦰" # :red_hair: + U2764FE0F = "❤️" # :red_heart: + U2764 = "❤" # :red_heart: + U1F3EE = "🏮" # :red_paper_lantern: + U2753 = "❓" # :red_question_mark: + U1F7E5 = "🟥" # :red_square: + U1F53B = "🔻" # :red_triangle_pointed_down: + U1F53A = "🔺" # :red_triangle_pointed_up: + UAEFE0F = "®️" # :registered: + UAE = "®" # :registered: + U1F60C = "😌" # :relieved_face: + U1F397FE0F = "🎗️" # :reminder_ribbon: + U1F397 = "🎗" # :reminder_ribbon: + U1F501 = "🔁" # :repeat_button: + U1F502 = "🔂" # :repeat_single_button: + U26D1FE0F = "⛑️" # :rescue_worker’s_helmet: + U26D1 = "⛑" # :rescue_worker’s_helmet: + U1F6BB = "🚻" # :restroom: + U25C0FE0F = "◀️" # :reverse_button: + U25C0 = "◀" # :reverse_button: + U1F49E = "💞" # :revolving_hearts: + U1F98F = "🦏" # :rhinoceros: + U1F380 = "🎀" # :ribbon: + U1F359 = "🍙" # :rice_ball: + U1F358 = "🍘" # :rice_cracker: + U1F91C = "🤜" # :right-facing_fist: + U1F91C1F3FF = "🤜🏿" # :right-facing_fist_dark_skin_tone: + U1F91C1F3FB = "🤜🏻" # :right-facing_fist_light_skin_tone: + U1F91C1F3FE = "🤜🏾" # :right-facing_fist_medium-dark_skin_tone: + U1F91C1F3FC = "🤜🏼" # :right-facing_fist_medium-light_skin_tone: + U1F91C1F3FD = "🤜🏽" # :right-facing_fist_medium_skin_tone: + U1F5EFFE0F = "🗯️" # :right_anger_bubble: + U1F5EF = "🗯" # :right_anger_bubble: + U27A1FE0F = "➡️" # :right_arrow: + U27A1 = "➡" # :right_arrow: + U2935FE0F = "⤵️" # :right_arrow_curving_down: + U2935 = "⤵" # :right_arrow_curving_down: + U21A9FE0F = "↩️" # :right_arrow_curving_left: + U21A9 = "↩" # :right_arrow_curving_left: + U2934FE0F = "⤴️" # :right_arrow_curving_up: + U2934 = "⤴" # :right_arrow_curving_up: + U1FAF1 = "🫱" # :rightwards_hand: + U1FAF11F3FF = "🫱🏿" # :rightwards_hand_dark_skin_tone: + U1FAF11F3FB = "🫱🏻" # :rightwards_hand_light_skin_tone: + U1FAF11F3FE = "🫱🏾" # :rightwards_hand_medium-dark_skin_tone: + U1FAF11F3FC = "🫱🏼" # :rightwards_hand_medium-light_skin_tone: + U1FAF11F3FD = "🫱🏽" # :rightwards_hand_medium_skin_tone: + U1FAF8 = "🫸" # :rightwards_pushing_hand: + U1FAF81F3FF = "🫸🏿" # :rightwards_pushing_hand_dark_skin_tone: + U1FAF81F3FB = "🫸🏻" # :rightwards_pushing_hand_light_skin_tone: + U1FAF81F3FE = "🫸🏾" # :rightwards_pushing_hand_medium-dark_skin_tone: + U1FAF81F3FC = "🫸🏼" # :rightwards_pushing_hand_medium-light_skin_tone: + U1FAF81F3FD = "🫸🏽" # :rightwards_pushing_hand_medium_skin_tone: + U1F48D = "💍" # :ring: + U1F6DF = "🛟" # :ring_buoy: + U1FA90 = "🪐" # :ringed_planet: + U1F360 = "🍠" # :roasted_sweet_potato: + U1F916 = "🤖" # :robot: + U1FAA8 = "🪨" # :rock: + U1F680 = "🚀" # :rocket: + U1F9FB = "🧻" # :roll_of_paper: + U1F5DEFE0F = "🗞️" # :rolled-up_newspaper: + U1F5DE = "🗞" # :rolled-up_newspaper: + U1F3A2 = "🎢" # :roller_coaster: + U1F6FC = "🛼" # :roller_skate: + U1F923 = "🤣" # :rolling_on_the_floor_laughing: + U1F413 = "🐓" # :rooster: + U1F339 = "🌹" # :rose: + U1F3F5FE0F = "🏵️" # :rosette: + U1F3F5 = "🏵" # :rosette: + U1F4CD = "📍" # :round_pushpin: + U1F3C9 = "🏉" # :rugby_football: + U1F3BD = "🎽" # :running_shirt: + U1F45F = "👟" # :running_shoe: + U1F625 = "😥" # :sad_but_relieved_face: + U1F9F7 = "🧷" # :safety_pin: + U1F9BA = "🦺" # :safety_vest: + U26F5 = "⛵" # :sailboat: + U1F376 = "🍶" # :sake: + U1F9C2 = "🧂" # :salt: + U1FAE1 = "🫡" # :saluting_face: + U1F96A = "🥪" # :sandwich: + U1F97B = "🥻" # :sari: + U1F6F0FE0F = "🛰️" # :satellite: + U1F6F0 = "🛰" # :satellite: + U1F4E1 = "📡" # :satellite_antenna: + U1F995 = "🦕" # :sauropod: + U1F3B7 = "🎷" # :saxophone: + U1F9E3 = "🧣" # :scarf: + U1F3EB = "🏫" # :school: + U1F9D1200D1F52C = "🧑‍🔬" # :scientist: + U1F9D11F3FF200D1F52C = "🧑🏿‍🔬" # :scientist_dark_skin_tone: + U1F9D11F3FB200D1F52C = "🧑🏻‍🔬" # :scientist_light_skin_tone: + U1F9D11F3FE200D1F52C = "🧑🏾‍🔬" # :scientist_medium-dark_skin_tone: + U1F9D11F3FC200D1F52C = "🧑🏼‍🔬" # :scientist_medium-light_skin_tone: + U1F9D11F3FD200D1F52C = "🧑🏽‍🔬" # :scientist_medium_skin_tone: + U2702FE0F = "✂️" # :scissors: + U2702 = "✂" # :scissors: + U1F982 = "🦂" # :scorpion: + U1FA9B = "🪛" # :screwdriver: + U1F4DC = "📜" # :scroll: + U1F9AD = "🦭" # :seal: + U1F4BA = "💺" # :seat: + U1F648 = "🙈" # :see-no-evil_monkey: + U1F331 = "🌱" # :seedling: + U1F933 = "🤳" # :selfie: + U1F9331F3FF = "🤳🏿" # :selfie_dark_skin_tone: + U1F9331F3FB = "🤳🏻" # :selfie_light_skin_tone: + U1F9331F3FE = "🤳🏾" # :selfie_medium-dark_skin_tone: + U1F9331F3FC = "🤳🏼" # :selfie_medium-light_skin_tone: + U1F9331F3FD = "🤳🏽" # :selfie_medium_skin_tone: + U1F415200D1F9BA = "🐕‍🦺" # :service_dog: + U1F562 = "🕢" # :seven-thirty: + U1F556 = "🕖" # :seven_o’clock: + U1FAA1 = "🪡" # :sewing_needle: + U1FAE8 = "🫨" # :shaking_face: + U1F958 = "🥘" # :shallow_pan_of_food: + U2618FE0F = "☘️" # :shamrock: + U2618 = "☘" # :shamrock: + U1F988 = "🦈" # :shark: + U1F367 = "🍧" # :shaved_ice: + U1F33E = "🌾" # :sheaf_of_rice: + U1F6E1FE0F = "🛡️" # :shield: + U1F6E1 = "🛡" # :shield: + U26E9FE0F = "⛩️" # :shinto_shrine: + U26E9 = "⛩" # :shinto_shrine: + U1F6A2 = "🚢" # :ship: + U1F320 = "🌠" # :shooting_star: + U1F6CDFE0F = "🛍️" # :shopping_bags: + U1F6CD = "🛍" # :shopping_bags: + U1F6D2 = "🛒" # :shopping_cart: + U1F370 = "🍰" # :shortcake: + U1FA73 = "🩳" # :shorts: + U1F6BF = "🚿" # :shower: + U1F990 = "🦐" # :shrimp: + U1F500 = "🔀" # :shuffle_tracks_button: + U1F92B = "🤫" # :shushing_face: + U1F918 = "🤘" # :sign_of_the_horns: + U1F9181F3FF = "🤘🏿" # :sign_of_the_horns_dark_skin_tone: + U1F9181F3FB = "🤘🏻" # :sign_of_the_horns_light_skin_tone: + U1F9181F3FE = "🤘🏾" # :sign_of_the_horns_medium-dark_skin_tone: + U1F9181F3FC = "🤘🏼" # :sign_of_the_horns_medium-light_skin_tone: + U1F9181F3FD = "🤘🏽" # :sign_of_the_horns_medium_skin_tone: + U1F9D1200D1F3A4 = "🧑‍🎤" # :singer: + U1F9D11F3FF200D1F3A4 = "🧑🏿‍🎤" # :singer_dark_skin_tone: + U1F9D11F3FB200D1F3A4 = "🧑🏻‍🎤" # :singer_light_skin_tone: + U1F9D11F3FE200D1F3A4 = "🧑🏾‍🎤" # :singer_medium-dark_skin_tone: + U1F9D11F3FC200D1F3A4 = "🧑🏼‍🎤" # :singer_medium-light_skin_tone: + U1F9D11F3FD200D1F3A4 = "🧑🏽‍🎤" # :singer_medium_skin_tone: + U1F561 = "🕡" # :six-thirty: + U1F555 = "🕕" # :six_o’clock: + U1F6F9 = "🛹" # :skateboard: + U26F7FE0F = "⛷️" # :skier: + U26F7 = "⛷" # :skier: + U1F3BF = "🎿" # :skis: + U1F480 = "💀" # :skull: + U2620FE0F = "☠️" # :skull_and_crossbones: + U2620 = "☠" # :skull_and_crossbones: + U1F9A8 = "🦨" # :skunk: + U1F6F7 = "🛷" # :sled: + U1F634 = "😴" # :sleeping_face: + U1F62A = "😪" # :sleepy_face: + U1F641 = "🙁" # :slightly_frowning_face: + U1F642 = "🙂" # :slightly_smiling_face: + U1F3B0 = "🎰" # :slot_machine: + U1F9A5 = "🦥" # :sloth: + U1F6E9FE0F = "🛩️" # :small_airplane: + U1F6E9 = "🛩" # :small_airplane: + U1F539 = "🔹" # :small_blue_diamond: + U1F538 = "🔸" # :small_orange_diamond: + U1F63B = "😻" # :smiling_cat_with_heart-eyes: + U263AFE0F = "☺️" # :smiling_face: + U263A = "☺" # :smiling_face: + U1F607 = "😇" # :smiling_face_with_halo: + U1F60D = "😍" # :smiling_face_with_heart-eyes: + U1F970 = "🥰" # :smiling_face_with_hearts: + U1F608 = "😈" # :smiling_face_with_horns: + U1F917 = "🤗" # :smiling_face_with_open_hands: + U1F60A = "😊" # :smiling_face_with_smiling_eyes: + U1F60E = "😎" # :smiling_face_with_sunglasses: + U1F972 = "🥲" # :smiling_face_with_tear: + U1F60F = "😏" # :smirking_face: + U1F40C = "🐌" # :snail: + U1F40D = "🐍" # :snake: + U1F927 = "🤧" # :sneezing_face: + U1F3D4FE0F = "🏔️" # :snow-capped_mountain: + U1F3D4 = "🏔" # :snow-capped_mountain: + U1F3C2 = "🏂" # :snowboarder: + U1F3C21F3FF = "🏂🏿" # :snowboarder_dark_skin_tone: + U1F3C21F3FB = "🏂🏻" # :snowboarder_light_skin_tone: + U1F3C21F3FE = "🏂🏾" # :snowboarder_medium-dark_skin_tone: + U1F3C21F3FC = "🏂🏼" # :snowboarder_medium-light_skin_tone: + U1F3C21F3FD = "🏂🏽" # :snowboarder_medium_skin_tone: + U2744FE0F = "❄️" # :snowflake: + U2744 = "❄" # :snowflake: + U2603FE0F = "☃️" # :snowman: + U2603 = "☃" # :snowman: + U26C4 = "⛄" # :snowman_without_snow: + U1F9FC = "🧼" # :soap: + U26BD = "⚽" # :soccer_ball: + U1F9E6 = "🧦" # :socks: + U1F366 = "🍦" # :soft_ice_cream: + U1F94E = "🥎" # :softball: + U2660FE0F = "♠️" # :spade_suit: + U2660 = "♠" # :spade_suit: + U1F35D = "🍝" # :spaghetti: + U2747FE0F = "❇️" # :sparkle: + U2747 = "❇" # :sparkle: + U1F387 = "🎇" # :sparkler: + U2728 = "✨" # :sparkles: + U1F496 = "💖" # :sparkling_heart: + U1F64A = "🙊" # :speak-no-evil_monkey: + U1F50A = "🔊" # :speaker_high_volume: + U1F508 = "🔈" # :speaker_low_volume: + U1F509 = "🔉" # :speaker_medium_volume: + U1F5E3FE0F = "🗣️" # :speaking_head: + U1F5E3 = "🗣" # :speaking_head: + U1F4AC = "💬" # :speech_balloon: + U1F6A4 = "🚤" # :speedboat: + U1F577FE0F = "🕷️" # :spider: + U1F577 = "🕷" # :spider: + U1F578FE0F = "🕸️" # :spider_web: + U1F578 = "🕸" # :spider_web: + U1F5D3FE0F = "🗓️" # :spiral_calendar: + U1F5D3 = "🗓" # :spiral_calendar: + U1F5D2FE0F = "🗒️" # :spiral_notepad: + U1F5D2 = "🗒" # :spiral_notepad: + U1F41A = "🐚" # :spiral_shell: + U1F9FD = "🧽" # :sponge: + U1F944 = "🥄" # :spoon: + U1F699 = "🚙" # :sport_utility_vehicle: + U1F3C5 = "🏅" # :sports_medal: + U1F433 = "🐳" # :spouting_whale: + U1F991 = "🦑" # :squid: + U1F61D = "😝" # :squinting_face_with_tongue: + U1F3DFFE0F = "🏟️" # :stadium: + U1F3DF = "🏟" # :stadium: + U2B50 = "⭐" # :star: + U1F929 = "🤩" # :star-struck: + U262AFE0F = "☪️" # :star_and_crescent: + U262A = "☪" # :star_and_crescent: + U2721FE0F = "✡️" # :star_of_David: + U2721 = "✡" # :star_of_David: + U1F689 = "🚉" # :station: + U1F35C = "🍜" # :steaming_bowl: + U1FA7A = "🩺" # :stethoscope: + U23F9FE0F = "⏹️" # :stop_button: + U23F9 = "⏹" # :stop_button: + U1F6D1 = "🛑" # :stop_sign: + U23F1FE0F = "⏱️" # :stopwatch: + U23F1 = "⏱" # :stopwatch: + U1F4CF = "📏" # :straight_ruler: + U1F353 = "🍓" # :strawberry: + U1F9D1200D1F393 = "🧑‍🎓" # :student: + U1F9D11F3FF200D1F393 = "🧑🏿‍🎓" # :student_dark_skin_tone: + U1F9D11F3FB200D1F393 = "🧑🏻‍🎓" # :student_light_skin_tone: + U1F9D11F3FE200D1F393 = "🧑🏾‍🎓" # :student_medium-dark_skin_tone: + U1F9D11F3FC200D1F393 = "🧑🏼‍🎓" # :student_medium-light_skin_tone: + U1F9D11F3FD200D1F393 = "🧑🏽‍🎓" # :student_medium_skin_tone: + U1F399FE0F = "🎙️" # :studio_microphone: + U1F399 = "🎙" # :studio_microphone: + U1F959 = "🥙" # :stuffed_flatbread: + U2600FE0F = "☀️" # :sun: + U2600 = "☀" # :sun: + U26C5 = "⛅" # :sun_behind_cloud: + U1F325FE0F = "🌥️" # :sun_behind_large_cloud: + U1F325 = "🌥" # :sun_behind_large_cloud: + U1F326FE0F = "🌦️" # :sun_behind_rain_cloud: + U1F326 = "🌦" # :sun_behind_rain_cloud: + U1F324FE0F = "🌤️" # :sun_behind_small_cloud: + U1F324 = "🌤" # :sun_behind_small_cloud: + U1F31E = "🌞" # :sun_with_face: + U1F33B = "🌻" # :sunflower: + U1F576FE0F = "🕶️" # :sunglasses: + U1F576 = "🕶" # :sunglasses: + U1F305 = "🌅" # :sunrise: + U1F304 = "🌄" # :sunrise_over_mountains: + U1F307 = "🌇" # :sunset: + U1F9B8 = "🦸" # :superhero: + U1F9B81F3FF = "🦸🏿" # :superhero_dark_skin_tone: + U1F9B81F3FB = "🦸🏻" # :superhero_light_skin_tone: + U1F9B81F3FE = "🦸🏾" # :superhero_medium-dark_skin_tone: + U1F9B81F3FC = "🦸🏼" # :superhero_medium-light_skin_tone: + U1F9B81F3FD = "🦸🏽" # :superhero_medium_skin_tone: + U1F9B9 = "🦹" # :supervillain: + U1F9B91F3FF = "🦹🏿" # :supervillain_dark_skin_tone: + U1F9B91F3FB = "🦹🏻" # :supervillain_light_skin_tone: + U1F9B91F3FE = "🦹🏾" # :supervillain_medium-dark_skin_tone: + U1F9B91F3FC = "🦹🏼" # :supervillain_medium-light_skin_tone: + U1F9B91F3FD = "🦹🏽" # :supervillain_medium_skin_tone: + U1F363 = "🍣" # :sushi: + U1F69F = "🚟" # :suspension_railway: + U1F9A2 = "🦢" # :swan: + U1F4A6 = "💦" # :sweat_droplets: + U1F54D = "🕍" # :synagogue: + U1F489 = "💉" # :syringe: + U1F455 = "👕" # :t-shirt: + U1F32E = "🌮" # :taco: + U1F961 = "🥡" # :takeout_box: + U1FAD4 = "🫔" # :tamale: + U1F38B = "🎋" # :tanabata_tree: + U1F34A = "🍊" # :tangerine: + U1F695 = "🚕" # :taxi: + U1F9D1200D1F3EB = "🧑‍🏫" # :teacher: + U1F9D11F3FF200D1F3EB = "🧑🏿‍🏫" # :teacher_dark_skin_tone: + U1F9D11F3FB200D1F3EB = "🧑🏻‍🏫" # :teacher_light_skin_tone: + U1F9D11F3FE200D1F3EB = "🧑🏾‍🏫" # :teacher_medium-dark_skin_tone: + U1F9D11F3FC200D1F3EB = "🧑🏼‍🏫" # :teacher_medium-light_skin_tone: + U1F9D11F3FD200D1F3EB = "🧑🏽‍🏫" # :teacher_medium_skin_tone: + U1F375 = "🍵" # :teacup_without_handle: + U1FAD6 = "🫖" # :teapot: + U1F4C6 = "📆" # :tear-off_calendar: + U1F9D1200D1F4BB = "🧑‍💻" # :technologist: + U1F9D11F3FF200D1F4BB = "🧑🏿‍💻" # :technologist_dark_skin_tone: + U1F9D11F3FB200D1F4BB = "🧑🏻‍💻" # :technologist_light_skin_tone: + U1F9D11F3FE200D1F4BB = "🧑🏾‍💻" # :technologist_medium-dark_skin_tone: + U1F9D11F3FC200D1F4BB = "🧑🏼‍💻" # :technologist_medium-light_skin_tone: + U1F9D11F3FD200D1F4BB = "🧑🏽‍💻" # :technologist_medium_skin_tone: + U1F9F8 = "🧸" # :teddy_bear: + U260EFE0F = "☎️" # :telephone: + U260E = "☎" # :telephone: + U1F4DE = "📞" # :telephone_receiver: + U1F52D = "🔭" # :telescope: + U1F4FA = "📺" # :television: + U1F565 = "🕥" # :ten-thirty: + U1F559 = "🕙" # :ten_o’clock: + U1F3BE = "🎾" # :tennis: + U26FA = "⛺" # :tent: + U1F9EA = "🧪" # :test_tube: + U1F321FE0F = "🌡️" # :thermometer: + U1F321 = "🌡" # :thermometer: + U1F914 = "🤔" # :thinking_face: + U1FA74 = "🩴" # :thong_sandal: + U1F4AD = "💭" # :thought_balloon: + U1F9F5 = "🧵" # :thread: + U1F55E = "🕞" # :three-thirty: + U1F552 = "🕒" # :three_o’clock: + U1F44E = "👎" # :thumbs_down: + U1F44E1F3FF = "👎🏿" # :thumbs_down_dark_skin_tone: + U1F44E1F3FB = "👎🏻" # :thumbs_down_light_skin_tone: + U1F44E1F3FE = "👎🏾" # :thumbs_down_medium-dark_skin_tone: + U1F44E1F3FC = "👎🏼" # :thumbs_down_medium-light_skin_tone: + U1F44E1F3FD = "👎🏽" # :thumbs_down_medium_skin_tone: + U1F44D = "👍" # :thumbs_up: + U1F44D1F3FF = "👍🏿" # :thumbs_up_dark_skin_tone: + U1F44D1F3FB = "👍🏻" # :thumbs_up_light_skin_tone: + U1F44D1F3FE = "👍🏾" # :thumbs_up_medium-dark_skin_tone: + U1F44D1F3FC = "👍🏼" # :thumbs_up_medium-light_skin_tone: + U1F44D1F3FD = "👍🏽" # :thumbs_up_medium_skin_tone: + U1F3AB = "🎫" # :ticket: + U1F405 = "🐅" # :tiger: + U1F42F = "🐯" # :tiger_face: + U23F2FE0F = "⏲️" # :timer_clock: + U23F2 = "⏲" # :timer_clock: + U1F62B = "😫" # :tired_face: + U1F6BD = "🚽" # :toilet: + U1F345 = "🍅" # :tomato: + U1F445 = "👅" # :tongue: + U1F9F0 = "🧰" # :toolbox: + U1F9B7 = "🦷" # :tooth: + U1FAA5 = "🪥" # :toothbrush: + U1F3A9 = "🎩" # :top_hat: + U1F32AFE0F = "🌪️" # :tornado: + U1F32A = "🌪" # :tornado: + U1F5B2FE0F = "🖲️" # :trackball: + U1F5B2 = "🖲" # :trackball: + U1F69C = "🚜" # :tractor: + U2122FE0F = "™️" # :trade_mark: + U2122 = "™" # :trade_mark: + U1F686 = "🚆" # :train: + U1F68A = "🚊" # :tram: + U1F68B = "🚋" # :tram_car: + U1F3F3FE0F200D26A7FE0F = "🏳️‍⚧️" # :transgender_flag: + U1F3F3200D26A7FE0F = "🏳‍⚧️" # :transgender_flag: + U1F3F3FE0F200D26A7 = "🏳️‍⚧" # :transgender_flag: + U1F3F3200D26A7 = "🏳‍⚧" # :transgender_flag: + U26A7FE0F = "⚧️" # :transgender_symbol: + U26A7 = "⚧" # :transgender_symbol: + U1F6A9 = "🚩" # :triangular_flag: + U1F4D0 = "📐" # :triangular_ruler: + U1F531 = "🔱" # :trident_emblem: + U1F9CC = "🧌" # :troll: + U1F68E = "🚎" # :trolleybus: + U1F3C6 = "🏆" # :trophy: + U1F379 = "🍹" # :tropical_drink: + U1F420 = "🐠" # :tropical_fish: + U1F3BA = "🎺" # :trumpet: + U1F337 = "🌷" # :tulip: + U1F943 = "🥃" # :tumbler_glass: + U1F983 = "🦃" # :turkey: + U1F422 = "🐢" # :turtle: + U1F567 = "🕧" # :twelve-thirty: + U1F55B = "🕛" # :twelve_o’clock: + U1F42B = "🐫" # :two-hump_camel: + U1F55D = "🕝" # :two-thirty: + U1F495 = "💕" # :two_hearts: + U1F551 = "🕑" # :two_o’clock: + U2602FE0F = "☂️" # :umbrella: + U2602 = "☂" # :umbrella: + U26F1FE0F = "⛱️" # :umbrella_on_ground: + U26F1 = "⛱" # :umbrella_on_ground: + U2614 = "☔" # :umbrella_with_rain_drops: + U1F612 = "😒" # :unamused_face: + U1F984 = "🦄" # :unicorn: + U1F513 = "🔓" # :unlocked: + U2195FE0F = "↕️" # :up-down_arrow: + U2195 = "↕" # :up-down_arrow: + U2196FE0F = "↖️" # :up-left_arrow: + U2196 = "↖" # :up-left_arrow: + U2197FE0F = "↗️" # :up-right_arrow: + U2197 = "↗" # :up-right_arrow: + U2B06FE0F = "⬆️" # :up_arrow: + U2B06 = "⬆" # :up_arrow: + U1F643 = "🙃" # :upside-down_face: + U1F53C = "🔼" # :upwards_button: + U1F9DB = "🧛" # :vampire: + U1F9DB1F3FF = "🧛🏿" # :vampire_dark_skin_tone: + U1F9DB1F3FB = "🧛🏻" # :vampire_light_skin_tone: + U1F9DB1F3FE = "🧛🏾" # :vampire_medium-dark_skin_tone: + U1F9DB1F3FC = "🧛🏼" # :vampire_medium-light_skin_tone: + U1F9DB1F3FD = "🧛🏽" # :vampire_medium_skin_tone: + U1F6A6 = "🚦" # :vertical_traffic_light: + U1F4F3 = "📳" # :vibration_mode: + U270CFE0F = "✌️" # :victory_hand: + U270C = "✌" # :victory_hand: + U270C1F3FF = "✌🏿" # :victory_hand_dark_skin_tone: + U270C1F3FB = "✌🏻" # :victory_hand_light_skin_tone: + U270C1F3FE = "✌🏾" # :victory_hand_medium-dark_skin_tone: + U270C1F3FC = "✌🏼" # :victory_hand_medium-light_skin_tone: + U270C1F3FD = "✌🏽" # :victory_hand_medium_skin_tone: + U1F4F9 = "📹" # :video_camera: + U1F3AE = "🎮" # :video_game: + U1F4FC = "📼" # :videocassette: + U1F3BB = "🎻" # :violin: + U1F30B = "🌋" # :volcano: + U1F3D0 = "🏐" # :volleyball: + U1F596 = "🖖" # :vulcan_salute: + U1F5961F3FF = "🖖🏿" # :vulcan_salute_dark_skin_tone: + U1F5961F3FB = "🖖🏻" # :vulcan_salute_light_skin_tone: + U1F5961F3FE = "🖖🏾" # :vulcan_salute_medium-dark_skin_tone: + U1F5961F3FC = "🖖🏼" # :vulcan_salute_medium-light_skin_tone: + U1F5961F3FD = "🖖🏽" # :vulcan_salute_medium_skin_tone: + U1F9C7 = "🧇" # :waffle: + U1F318 = "🌘" # :waning_crescent_moon: + U1F316 = "🌖" # :waning_gibbous_moon: + U26A0FE0F = "⚠️" # :warning: + U26A0 = "⚠" # :warning: + U1F5D1FE0F = "🗑️" # :wastebasket: + U1F5D1 = "🗑" # :wastebasket: + U231A = "⌚" # :watch: + U1F403 = "🐃" # :water_buffalo: + U1F6BE = "🚾" # :water_closet: + U1F52B = "🔫" # :water_pistol: + U1F30A = "🌊" # :water_wave: + U1F349 = "🍉" # :watermelon: + U1F44B = "👋" # :waving_hand: + U1F44B1F3FF = "👋🏿" # :waving_hand_dark_skin_tone: + U1F44B1F3FB = "👋🏻" # :waving_hand_light_skin_tone: + U1F44B1F3FE = "👋🏾" # :waving_hand_medium-dark_skin_tone: + U1F44B1F3FC = "👋🏼" # :waving_hand_medium-light_skin_tone: + U1F44B1F3FD = "👋🏽" # :waving_hand_medium_skin_tone: + U3030FE0F = "〰️" # :wavy_dash: + U3030 = "〰" # :wavy_dash: + U1F312 = "🌒" # :waxing_crescent_moon: + U1F314 = "🌔" # :waxing_gibbous_moon: + U1F640 = "🙀" # :weary_cat: + U1F629 = "😩" # :weary_face: + U1F492 = "💒" # :wedding: + U1F40B = "🐋" # :whale: + U1F6DE = "🛞" # :wheel: + U2638FE0F = "☸️" # :wheel_of_dharma: + U2638 = "☸" # :wheel_of_dharma: + U267F = "♿" # :wheelchair_symbol: + U1F9AF = "🦯" # :white_cane: + U26AA = "⚪" # :white_circle: + U2755 = "❕" # :white_exclamation_mark: + U1F3F3FE0F = "🏳️" # :white_flag: + U1F3F3 = "🏳" # :white_flag: + U1F4AE = "💮" # :white_flower: + U1F9B3 = "🦳" # :white_hair: + U1F90D = "🤍" # :white_heart: + U2B1C = "⬜" # :white_large_square: + U25FD = "◽" # :white_medium-small_square: + U25FBFE0F = "◻️" # :white_medium_square: + U25FB = "◻" # :white_medium_square: + U2754 = "❔" # :white_question_mark: + U25ABFE0F = "▫️" # :white_small_square: + U25AB = "▫" # :white_small_square: + U1F533 = "🔳" # :white_square_button: + U1F940 = "🥀" # :wilted_flower: + U1F390 = "🎐" # :wind_chime: + U1F32CFE0F = "🌬️" # :wind_face: + U1F32C = "🌬" # :wind_face: + U1FA9F = "🪟" # :window: + U1F377 = "🍷" # :wine_glass: + U1FABD = "🪽" # :wing: + U1F609 = "😉" # :winking_face: + U1F61C = "😜" # :winking_face_with_tongue: + U1F6DC = "🛜" # :wireless: + U1F43A = "🐺" # :wolf: + U1F469 = "👩" # :woman: + U1F46B = "👫" # :woman_and_man_holding_hands: + U1F46B1F3FF = "👫🏿" # :woman_and_man_holding_hands_dark_skin_tone: + U1F4691F3FF200D1F91D200D1F4681F3FB = "👩🏿‍🤝‍👨🏻" # :woman_and_man_holding_hands_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D1F91D200D1F4681F3FE = "👩🏿‍🤝‍👨🏾" # :woman_and_man_holding_hands_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D1F91D200D1F4681F3FC = "👩🏿‍🤝‍👨🏼" # :woman_and_man_holding_hands_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D1F91D200D1F4681F3FD = "👩🏿‍🤝‍👨🏽" # :woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone: + U1F46B1F3FB = "👫🏻" # :woman_and_man_holding_hands_light_skin_tone: + U1F4691F3FB200D1F91D200D1F4681F3FF = "👩🏻‍🤝‍👨🏿" # :woman_and_man_holding_hands_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D1F91D200D1F4681F3FE = "👩🏻‍🤝‍👨🏾" # :woman_and_man_holding_hands_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D1F91D200D1F4681F3FC = "👩🏻‍🤝‍👨🏼" # :woman_and_man_holding_hands_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D1F91D200D1F4681F3FD = "👩🏻‍🤝‍👨🏽" # :woman_and_man_holding_hands_light_skin_tone_medium_skin_tone: + U1F46B1F3FE = "👫🏾" # :woman_and_man_holding_hands_medium-dark_skin_tone: + U1F4691F3FE200D1F91D200D1F4681F3FF = "👩🏾‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D1F91D200D1F4681F3FB = "👩🏾‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D1F91D200D1F4681F3FC = "👩🏾‍🤝‍👨🏼" # :woman_and_man_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D1F91D200D1F4681F3FD = "👩🏾‍🤝‍👨🏽" # :woman_and_man_holding_hands_medium-dark_skin_tone_medium_skin_tone: + U1F46B1F3FC = "👫🏼" # :woman_and_man_holding_hands_medium-light_skin_tone: + U1F4691F3FC200D1F91D200D1F4681F3FF = "👩🏼‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D1F91D200D1F4681F3FB = "👩🏼‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D1F91D200D1F4681F3FE = "👩🏼‍🤝‍👨🏾" # :woman_and_man_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D1F91D200D1F4681F3FD = "👩🏼‍🤝‍👨🏽" # :woman_and_man_holding_hands_medium-light_skin_tone_medium_skin_tone: + U1F46B1F3FD = "👫🏽" # :woman_and_man_holding_hands_medium_skin_tone: + U1F4691F3FD200D1F91D200D1F4681F3FF = "👩🏽‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D1F91D200D1F4681F3FB = "👩🏽‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D1F91D200D1F4681F3FE = "👩🏽‍🤝‍👨🏾" # :woman_and_man_holding_hands_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D1F91D200D1F4681F3FC = "👩🏽‍🤝‍👨🏼" # :woman_and_man_holding_hands_medium_skin_tone_medium-light_skin_tone: + U1F469200D1F3A8 = "👩‍🎨" # :woman_artist: + U1F4691F3FF200D1F3A8 = "👩🏿‍🎨" # :woman_artist_dark_skin_tone: + U1F4691F3FB200D1F3A8 = "👩🏻‍🎨" # :woman_artist_light_skin_tone: + U1F4691F3FE200D1F3A8 = "👩🏾‍🎨" # :woman_artist_medium-dark_skin_tone: + U1F4691F3FC200D1F3A8 = "👩🏼‍🎨" # :woman_artist_medium-light_skin_tone: + U1F4691F3FD200D1F3A8 = "👩🏽‍🎨" # :woman_artist_medium_skin_tone: + U1F469200D1F680 = "👩‍🚀" # :woman_astronaut: + U1F4691F3FF200D1F680 = "👩🏿‍🚀" # :woman_astronaut_dark_skin_tone: + U1F4691F3FB200D1F680 = "👩🏻‍🚀" # :woman_astronaut_light_skin_tone: + U1F4691F3FE200D1F680 = "👩🏾‍🚀" # :woman_astronaut_medium-dark_skin_tone: + U1F4691F3FC200D1F680 = "👩🏼‍🚀" # :woman_astronaut_medium-light_skin_tone: + U1F4691F3FD200D1F680 = "👩🏽‍🚀" # :woman_astronaut_medium_skin_tone: + U1F469200D1F9B2 = "👩‍🦲" # :woman_bald: + U1F9D4200D2640FE0F = "🧔‍♀️" # :woman_beard: + U1F9D4200D2640 = "🧔‍♀" # :woman_beard: + U1F6B4200D2640FE0F = "🚴‍♀️" # :woman_biking: + U1F6B4200D2640 = "🚴‍♀" # :woman_biking: + U1F6B41F3FF200D2640FE0F = "🚴🏿‍♀️" # :woman_biking_dark_skin_tone: + U1F6B41F3FF200D2640 = "🚴🏿‍♀" # :woman_biking_dark_skin_tone: + U1F6B41F3FB200D2640FE0F = "🚴🏻‍♀️" # :woman_biking_light_skin_tone: + U1F6B41F3FB200D2640 = "🚴🏻‍♀" # :woman_biking_light_skin_tone: + U1F6B41F3FE200D2640FE0F = "🚴🏾‍♀️" # :woman_biking_medium-dark_skin_tone: + U1F6B41F3FE200D2640 = "🚴🏾‍♀" # :woman_biking_medium-dark_skin_tone: + U1F6B41F3FC200D2640FE0F = "🚴🏼‍♀️" # :woman_biking_medium-light_skin_tone: + U1F6B41F3FC200D2640 = "🚴🏼‍♀" # :woman_biking_medium-light_skin_tone: + U1F6B41F3FD200D2640FE0F = "🚴🏽‍♀️" # :woman_biking_medium_skin_tone: + U1F6B41F3FD200D2640 = "🚴🏽‍♀" # :woman_biking_medium_skin_tone: + U1F471200D2640FE0F = "👱‍♀️" # :woman_blond_hair: + U1F471200D2640 = "👱‍♀" # :woman_blond_hair: + U26F9FE0F200D2640FE0F = "⛹️‍♀️" # :woman_bouncing_ball: + U26F9200D2640FE0F = "⛹‍♀️" # :woman_bouncing_ball: + U26F9FE0F200D2640 = "⛹️‍♀" # :woman_bouncing_ball: + U26F9200D2640 = "⛹‍♀" # :woman_bouncing_ball: + U26F91F3FF200D2640FE0F = "⛹🏿‍♀️" # :woman_bouncing_ball_dark_skin_tone: + U26F91F3FF200D2640 = "⛹🏿‍♀" # :woman_bouncing_ball_dark_skin_tone: + U26F91F3FB200D2640FE0F = "⛹🏻‍♀️" # :woman_bouncing_ball_light_skin_tone: + U26F91F3FB200D2640 = "⛹🏻‍♀" # :woman_bouncing_ball_light_skin_tone: + U26F91F3FE200D2640FE0F = "⛹🏾‍♀️" # :woman_bouncing_ball_medium-dark_skin_tone: + U26F91F3FE200D2640 = "⛹🏾‍♀" # :woman_bouncing_ball_medium-dark_skin_tone: + U26F91F3FC200D2640FE0F = "⛹🏼‍♀️" # :woman_bouncing_ball_medium-light_skin_tone: + U26F91F3FC200D2640 = "⛹🏼‍♀" # :woman_bouncing_ball_medium-light_skin_tone: + U26F91F3FD200D2640FE0F = "⛹🏽‍♀️" # :woman_bouncing_ball_medium_skin_tone: + U26F91F3FD200D2640 = "⛹🏽‍♀" # :woman_bouncing_ball_medium_skin_tone: + U1F647200D2640FE0F = "🙇‍♀️" # :woman_bowing: + U1F647200D2640 = "🙇‍♀" # :woman_bowing: + U1F6471F3FF200D2640FE0F = "🙇🏿‍♀️" # :woman_bowing_dark_skin_tone: + U1F6471F3FF200D2640 = "🙇🏿‍♀" # :woman_bowing_dark_skin_tone: + U1F6471F3FB200D2640FE0F = "🙇🏻‍♀️" # :woman_bowing_light_skin_tone: + U1F6471F3FB200D2640 = "🙇🏻‍♀" # :woman_bowing_light_skin_tone: + U1F6471F3FE200D2640FE0F = "🙇🏾‍♀️" # :woman_bowing_medium-dark_skin_tone: + U1F6471F3FE200D2640 = "🙇🏾‍♀" # :woman_bowing_medium-dark_skin_tone: + U1F6471F3FC200D2640FE0F = "🙇🏼‍♀️" # :woman_bowing_medium-light_skin_tone: + U1F6471F3FC200D2640 = "🙇🏼‍♀" # :woman_bowing_medium-light_skin_tone: + U1F6471F3FD200D2640FE0F = "🙇🏽‍♀️" # :woman_bowing_medium_skin_tone: + U1F6471F3FD200D2640 = "🙇🏽‍♀" # :woman_bowing_medium_skin_tone: + U1F938200D2640FE0F = "🤸‍♀️" # :woman_cartwheeling: + U1F938200D2640 = "🤸‍♀" # :woman_cartwheeling: + U1F9381F3FF200D2640FE0F = "🤸🏿‍♀️" # :woman_cartwheeling_dark_skin_tone: + U1F9381F3FF200D2640 = "🤸🏿‍♀" # :woman_cartwheeling_dark_skin_tone: + U1F9381F3FB200D2640FE0F = "🤸🏻‍♀️" # :woman_cartwheeling_light_skin_tone: + U1F9381F3FB200D2640 = "🤸🏻‍♀" # :woman_cartwheeling_light_skin_tone: + U1F9381F3FE200D2640FE0F = "🤸🏾‍♀️" # :woman_cartwheeling_medium-dark_skin_tone: + U1F9381F3FE200D2640 = "🤸🏾‍♀" # :woman_cartwheeling_medium-dark_skin_tone: + U1F9381F3FC200D2640FE0F = "🤸🏼‍♀️" # :woman_cartwheeling_medium-light_skin_tone: + U1F9381F3FC200D2640 = "🤸🏼‍♀" # :woman_cartwheeling_medium-light_skin_tone: + U1F9381F3FD200D2640FE0F = "🤸🏽‍♀️" # :woman_cartwheeling_medium_skin_tone: + U1F9381F3FD200D2640 = "🤸🏽‍♀" # :woman_cartwheeling_medium_skin_tone: + U1F9D7200D2640FE0F = "🧗‍♀️" # :woman_climbing: + U1F9D7200D2640 = "🧗‍♀" # :woman_climbing: + U1F9D71F3FF200D2640FE0F = "🧗🏿‍♀️" # :woman_climbing_dark_skin_tone: + U1F9D71F3FF200D2640 = "🧗🏿‍♀" # :woman_climbing_dark_skin_tone: + U1F9D71F3FB200D2640FE0F = "🧗🏻‍♀️" # :woman_climbing_light_skin_tone: + U1F9D71F3FB200D2640 = "🧗🏻‍♀" # :woman_climbing_light_skin_tone: + U1F9D71F3FE200D2640FE0F = "🧗🏾‍♀️" # :woman_climbing_medium-dark_skin_tone: + U1F9D71F3FE200D2640 = "🧗🏾‍♀" # :woman_climbing_medium-dark_skin_tone: + U1F9D71F3FC200D2640FE0F = "🧗🏼‍♀️" # :woman_climbing_medium-light_skin_tone: + U1F9D71F3FC200D2640 = "🧗🏼‍♀" # :woman_climbing_medium-light_skin_tone: + U1F9D71F3FD200D2640FE0F = "🧗🏽‍♀️" # :woman_climbing_medium_skin_tone: + U1F9D71F3FD200D2640 = "🧗🏽‍♀" # :woman_climbing_medium_skin_tone: + U1F477200D2640FE0F = "👷‍♀️" # :woman_construction_worker: + U1F477200D2640 = "👷‍♀" # :woman_construction_worker: + U1F4771F3FF200D2640FE0F = "👷🏿‍♀️" # :woman_construction_worker_dark_skin_tone: + U1F4771F3FF200D2640 = "👷🏿‍♀" # :woman_construction_worker_dark_skin_tone: + U1F4771F3FB200D2640FE0F = "👷🏻‍♀️" # :woman_construction_worker_light_skin_tone: + U1F4771F3FB200D2640 = "👷🏻‍♀" # :woman_construction_worker_light_skin_tone: + U1F4771F3FE200D2640FE0F = "👷🏾‍♀️" # :woman_construction_worker_medium-dark_skin_tone: + U1F4771F3FE200D2640 = "👷🏾‍♀" # :woman_construction_worker_medium-dark_skin_tone: + U1F4771F3FC200D2640FE0F = "👷🏼‍♀️" # :woman_construction_worker_medium-light_skin_tone: + U1F4771F3FC200D2640 = "👷🏼‍♀" # :woman_construction_worker_medium-light_skin_tone: + U1F4771F3FD200D2640FE0F = "👷🏽‍♀️" # :woman_construction_worker_medium_skin_tone: + U1F4771F3FD200D2640 = "👷🏽‍♀" # :woman_construction_worker_medium_skin_tone: + U1F469200D1F373 = "👩‍🍳" # :woman_cook: + U1F4691F3FF200D1F373 = "👩🏿‍🍳" # :woman_cook_dark_skin_tone: + U1F4691F3FB200D1F373 = "👩🏻‍🍳" # :woman_cook_light_skin_tone: + U1F4691F3FE200D1F373 = "👩🏾‍🍳" # :woman_cook_medium-dark_skin_tone: + U1F4691F3FC200D1F373 = "👩🏼‍🍳" # :woman_cook_medium-light_skin_tone: + U1F4691F3FD200D1F373 = "👩🏽‍🍳" # :woman_cook_medium_skin_tone: + U1F469200D1F9B1 = "👩‍🦱" # :woman_curly_hair: + U1F483 = "💃" # :woman_dancing: + U1F4831F3FF = "💃🏿" # :woman_dancing_dark_skin_tone: + U1F4831F3FB = "💃🏻" # :woman_dancing_light_skin_tone: + U1F4831F3FE = "💃🏾" # :woman_dancing_medium-dark_skin_tone: + U1F4831F3FC = "💃🏼" # :woman_dancing_medium-light_skin_tone: + U1F4831F3FD = "💃🏽" # :woman_dancing_medium_skin_tone: + U1F4691F3FF = "👩🏿" # :woman_dark_skin_tone: + U1F4691F3FF200D1F9B2 = "👩🏿‍🦲" # :woman_dark_skin_tone_bald: + U1F9D41F3FF200D2640FE0F = "🧔🏿‍♀️" # :woman_dark_skin_tone_beard: + U1F9D41F3FF200D2640 = "🧔🏿‍♀" # :woman_dark_skin_tone_beard: + U1F4711F3FF200D2640FE0F = "👱🏿‍♀️" # :woman_dark_skin_tone_blond_hair: + U1F4711F3FF200D2640 = "👱🏿‍♀" # :woman_dark_skin_tone_blond_hair: + U1F4691F3FF200D1F9B1 = "👩🏿‍🦱" # :woman_dark_skin_tone_curly_hair: + U1F4691F3FF200D1F9B0 = "👩🏿‍🦰" # :woman_dark_skin_tone_red_hair: + U1F4691F3FF200D1F9B3 = "👩🏿‍🦳" # :woman_dark_skin_tone_white_hair: + U1F575FE0F200D2640FE0F = "🕵️‍♀️" # :woman_detective: + U1F575200D2640FE0F = "🕵‍♀️" # :woman_detective: + U1F575FE0F200D2640 = "🕵️‍♀" # :woman_detective: + U1F575200D2640 = "🕵‍♀" # :woman_detective: + U1F5751F3FF200D2640FE0F = "🕵🏿‍♀️" # :woman_detective_dark_skin_tone: + U1F5751F3FF200D2640 = "🕵🏿‍♀" # :woman_detective_dark_skin_tone: + U1F5751F3FB200D2640FE0F = "🕵🏻‍♀️" # :woman_detective_light_skin_tone: + U1F5751F3FB200D2640 = "🕵🏻‍♀" # :woman_detective_light_skin_tone: + U1F5751F3FE200D2640FE0F = "🕵🏾‍♀️" # :woman_detective_medium-dark_skin_tone: + U1F5751F3FE200D2640 = "🕵🏾‍♀" # :woman_detective_medium-dark_skin_tone: + U1F5751F3FC200D2640FE0F = "🕵🏼‍♀️" # :woman_detective_medium-light_skin_tone: + U1F5751F3FC200D2640 = "🕵🏼‍♀" # :woman_detective_medium-light_skin_tone: + U1F5751F3FD200D2640FE0F = "🕵🏽‍♀️" # :woman_detective_medium_skin_tone: + U1F5751F3FD200D2640 = "🕵🏽‍♀" # :woman_detective_medium_skin_tone: + U1F9DD200D2640FE0F = "🧝‍♀️" # :woman_elf: + U1F9DD200D2640 = "🧝‍♀" # :woman_elf: + U1F9DD1F3FF200D2640FE0F = "🧝🏿‍♀️" # :woman_elf_dark_skin_tone: + U1F9DD1F3FF200D2640 = "🧝🏿‍♀" # :woman_elf_dark_skin_tone: + U1F9DD1F3FB200D2640FE0F = "🧝🏻‍♀️" # :woman_elf_light_skin_tone: + U1F9DD1F3FB200D2640 = "🧝🏻‍♀" # :woman_elf_light_skin_tone: + U1F9DD1F3FE200D2640FE0F = "🧝🏾‍♀️" # :woman_elf_medium-dark_skin_tone: + U1F9DD1F3FE200D2640 = "🧝🏾‍♀" # :woman_elf_medium-dark_skin_tone: + U1F9DD1F3FC200D2640FE0F = "🧝🏼‍♀️" # :woman_elf_medium-light_skin_tone: + U1F9DD1F3FC200D2640 = "🧝🏼‍♀" # :woman_elf_medium-light_skin_tone: + U1F9DD1F3FD200D2640FE0F = "🧝🏽‍♀️" # :woman_elf_medium_skin_tone: + U1F9DD1F3FD200D2640 = "🧝🏽‍♀" # :woman_elf_medium_skin_tone: + U1F926200D2640FE0F = "🤦‍♀️" # :woman_facepalming: + U1F926200D2640 = "🤦‍♀" # :woman_facepalming: + U1F9261F3FF200D2640FE0F = "🤦🏿‍♀️" # :woman_facepalming_dark_skin_tone: + U1F9261F3FF200D2640 = "🤦🏿‍♀" # :woman_facepalming_dark_skin_tone: + U1F9261F3FB200D2640FE0F = "🤦🏻‍♀️" # :woman_facepalming_light_skin_tone: + U1F9261F3FB200D2640 = "🤦🏻‍♀" # :woman_facepalming_light_skin_tone: + U1F9261F3FE200D2640FE0F = "🤦🏾‍♀️" # :woman_facepalming_medium-dark_skin_tone: + U1F9261F3FE200D2640 = "🤦🏾‍♀" # :woman_facepalming_medium-dark_skin_tone: + U1F9261F3FC200D2640FE0F = "🤦🏼‍♀️" # :woman_facepalming_medium-light_skin_tone: + U1F9261F3FC200D2640 = "🤦🏼‍♀" # :woman_facepalming_medium-light_skin_tone: + U1F9261F3FD200D2640FE0F = "🤦🏽‍♀️" # :woman_facepalming_medium_skin_tone: + U1F9261F3FD200D2640 = "🤦🏽‍♀" # :woman_facepalming_medium_skin_tone: + U1F469200D1F3ED = "👩‍🏭" # :woman_factory_worker: + U1F4691F3FF200D1F3ED = "👩🏿‍🏭" # :woman_factory_worker_dark_skin_tone: + U1F4691F3FB200D1F3ED = "👩🏻‍🏭" # :woman_factory_worker_light_skin_tone: + U1F4691F3FE200D1F3ED = "👩🏾‍🏭" # :woman_factory_worker_medium-dark_skin_tone: + U1F4691F3FC200D1F3ED = "👩🏼‍🏭" # :woman_factory_worker_medium-light_skin_tone: + U1F4691F3FD200D1F3ED = "👩🏽‍🏭" # :woman_factory_worker_medium_skin_tone: + U1F9DA200D2640FE0F = "🧚‍♀️" # :woman_fairy: + U1F9DA200D2640 = "🧚‍♀" # :woman_fairy: + U1F9DA1F3FF200D2640FE0F = "🧚🏿‍♀️" # :woman_fairy_dark_skin_tone: + U1F9DA1F3FF200D2640 = "🧚🏿‍♀" # :woman_fairy_dark_skin_tone: + U1F9DA1F3FB200D2640FE0F = "🧚🏻‍♀️" # :woman_fairy_light_skin_tone: + U1F9DA1F3FB200D2640 = "🧚🏻‍♀" # :woman_fairy_light_skin_tone: + U1F9DA1F3FE200D2640FE0F = "🧚🏾‍♀️" # :woman_fairy_medium-dark_skin_tone: + U1F9DA1F3FE200D2640 = "🧚🏾‍♀" # :woman_fairy_medium-dark_skin_tone: + U1F9DA1F3FC200D2640FE0F = "🧚🏼‍♀️" # :woman_fairy_medium-light_skin_tone: + U1F9DA1F3FC200D2640 = "🧚🏼‍♀" # :woman_fairy_medium-light_skin_tone: + U1F9DA1F3FD200D2640FE0F = "🧚🏽‍♀️" # :woman_fairy_medium_skin_tone: + U1F9DA1F3FD200D2640 = "🧚🏽‍♀" # :woman_fairy_medium_skin_tone: + U1F469200D1F33E = "👩‍🌾" # :woman_farmer: + U1F4691F3FF200D1F33E = "👩🏿‍🌾" # :woman_farmer_dark_skin_tone: + U1F4691F3FB200D1F33E = "👩🏻‍🌾" # :woman_farmer_light_skin_tone: + U1F4691F3FE200D1F33E = "👩🏾‍🌾" # :woman_farmer_medium-dark_skin_tone: + U1F4691F3FC200D1F33E = "👩🏼‍🌾" # :woman_farmer_medium-light_skin_tone: + U1F4691F3FD200D1F33E = "👩🏽‍🌾" # :woman_farmer_medium_skin_tone: + U1F469200D1F37C = "👩‍🍼" # :woman_feeding_baby: + U1F4691F3FF200D1F37C = "👩🏿‍🍼" # :woman_feeding_baby_dark_skin_tone: + U1F4691F3FB200D1F37C = "👩🏻‍🍼" # :woman_feeding_baby_light_skin_tone: + U1F4691F3FE200D1F37C = "👩🏾‍🍼" # :woman_feeding_baby_medium-dark_skin_tone: + U1F4691F3FC200D1F37C = "👩🏼‍🍼" # :woman_feeding_baby_medium-light_skin_tone: + U1F4691F3FD200D1F37C = "👩🏽‍🍼" # :woman_feeding_baby_medium_skin_tone: + U1F469200D1F692 = "👩‍🚒" # :woman_firefighter: + U1F4691F3FF200D1F692 = "👩🏿‍🚒" # :woman_firefighter_dark_skin_tone: + U1F4691F3FB200D1F692 = "👩🏻‍🚒" # :woman_firefighter_light_skin_tone: + U1F4691F3FE200D1F692 = "👩🏾‍🚒" # :woman_firefighter_medium-dark_skin_tone: + U1F4691F3FC200D1F692 = "👩🏼‍🚒" # :woman_firefighter_medium-light_skin_tone: + U1F4691F3FD200D1F692 = "👩🏽‍🚒" # :woman_firefighter_medium_skin_tone: + U1F64D200D2640FE0F = "🙍‍♀️" # :woman_frowning: + U1F64D200D2640 = "🙍‍♀" # :woman_frowning: + U1F64D1F3FF200D2640FE0F = "🙍🏿‍♀️" # :woman_frowning_dark_skin_tone: + U1F64D1F3FF200D2640 = "🙍🏿‍♀" # :woman_frowning_dark_skin_tone: + U1F64D1F3FB200D2640FE0F = "🙍🏻‍♀️" # :woman_frowning_light_skin_tone: + U1F64D1F3FB200D2640 = "🙍🏻‍♀" # :woman_frowning_light_skin_tone: + U1F64D1F3FE200D2640FE0F = "🙍🏾‍♀️" # :woman_frowning_medium-dark_skin_tone: + U1F64D1F3FE200D2640 = "🙍🏾‍♀" # :woman_frowning_medium-dark_skin_tone: + U1F64D1F3FC200D2640FE0F = "🙍🏼‍♀️" # :woman_frowning_medium-light_skin_tone: + U1F64D1F3FC200D2640 = "🙍🏼‍♀" # :woman_frowning_medium-light_skin_tone: + U1F64D1F3FD200D2640FE0F = "🙍🏽‍♀️" # :woman_frowning_medium_skin_tone: + U1F64D1F3FD200D2640 = "🙍🏽‍♀" # :woman_frowning_medium_skin_tone: + U1F9DE200D2640FE0F = "🧞‍♀️" # :woman_genie: + U1F9DE200D2640 = "🧞‍♀" # :woman_genie: + U1F645200D2640FE0F = "🙅‍♀️" # :woman_gesturing_NO: + U1F645200D2640 = "🙅‍♀" # :woman_gesturing_NO: + U1F6451F3FF200D2640FE0F = "🙅🏿‍♀️" # :woman_gesturing_NO_dark_skin_tone: + U1F6451F3FF200D2640 = "🙅🏿‍♀" # :woman_gesturing_NO_dark_skin_tone: + U1F6451F3FB200D2640FE0F = "🙅🏻‍♀️" # :woman_gesturing_NO_light_skin_tone: + U1F6451F3FB200D2640 = "🙅🏻‍♀" # :woman_gesturing_NO_light_skin_tone: + U1F6451F3FE200D2640FE0F = "🙅🏾‍♀️" # :woman_gesturing_NO_medium-dark_skin_tone: + U1F6451F3FE200D2640 = "🙅🏾‍♀" # :woman_gesturing_NO_medium-dark_skin_tone: + U1F6451F3FC200D2640FE0F = "🙅🏼‍♀️" # :woman_gesturing_NO_medium-light_skin_tone: + U1F6451F3FC200D2640 = "🙅🏼‍♀" # :woman_gesturing_NO_medium-light_skin_tone: + U1F6451F3FD200D2640FE0F = "🙅🏽‍♀️" # :woman_gesturing_NO_medium_skin_tone: + U1F6451F3FD200D2640 = "🙅🏽‍♀" # :woman_gesturing_NO_medium_skin_tone: + U1F646200D2640FE0F = "🙆‍♀️" # :woman_gesturing_OK: + U1F646200D2640 = "🙆‍♀" # :woman_gesturing_OK: + U1F6461F3FF200D2640FE0F = "🙆🏿‍♀️" # :woman_gesturing_OK_dark_skin_tone: + U1F6461F3FF200D2640 = "🙆🏿‍♀" # :woman_gesturing_OK_dark_skin_tone: + U1F6461F3FB200D2640FE0F = "🙆🏻‍♀️" # :woman_gesturing_OK_light_skin_tone: + U1F6461F3FB200D2640 = "🙆🏻‍♀" # :woman_gesturing_OK_light_skin_tone: + U1F6461F3FE200D2640FE0F = "🙆🏾‍♀️" # :woman_gesturing_OK_medium-dark_skin_tone: + U1F6461F3FE200D2640 = "🙆🏾‍♀" # :woman_gesturing_OK_medium-dark_skin_tone: + U1F6461F3FC200D2640FE0F = "🙆🏼‍♀️" # :woman_gesturing_OK_medium-light_skin_tone: + U1F6461F3FC200D2640 = "🙆🏼‍♀" # :woman_gesturing_OK_medium-light_skin_tone: + U1F6461F3FD200D2640FE0F = "🙆🏽‍♀️" # :woman_gesturing_OK_medium_skin_tone: + U1F6461F3FD200D2640 = "🙆🏽‍♀" # :woman_gesturing_OK_medium_skin_tone: + U1F487200D2640FE0F = "💇‍♀️" # :woman_getting_haircut: + U1F487200D2640 = "💇‍♀" # :woman_getting_haircut: + U1F4871F3FF200D2640FE0F = "💇🏿‍♀️" # :woman_getting_haircut_dark_skin_tone: + U1F4871F3FF200D2640 = "💇🏿‍♀" # :woman_getting_haircut_dark_skin_tone: + U1F4871F3FB200D2640FE0F = "💇🏻‍♀️" # :woman_getting_haircut_light_skin_tone: + U1F4871F3FB200D2640 = "💇🏻‍♀" # :woman_getting_haircut_light_skin_tone: + U1F4871F3FE200D2640FE0F = "💇🏾‍♀️" # :woman_getting_haircut_medium-dark_skin_tone: + U1F4871F3FE200D2640 = "💇🏾‍♀" # :woman_getting_haircut_medium-dark_skin_tone: + U1F4871F3FC200D2640FE0F = "💇🏼‍♀️" # :woman_getting_haircut_medium-light_skin_tone: + U1F4871F3FC200D2640 = "💇🏼‍♀" # :woman_getting_haircut_medium-light_skin_tone: + U1F4871F3FD200D2640FE0F = "💇🏽‍♀️" # :woman_getting_haircut_medium_skin_tone: + U1F4871F3FD200D2640 = "💇🏽‍♀" # :woman_getting_haircut_medium_skin_tone: + U1F486200D2640FE0F = "💆‍♀️" # :woman_getting_massage: + U1F486200D2640 = "💆‍♀" # :woman_getting_massage: + U1F4861F3FF200D2640FE0F = "💆🏿‍♀️" # :woman_getting_massage_dark_skin_tone: + U1F4861F3FF200D2640 = "💆🏿‍♀" # :woman_getting_massage_dark_skin_tone: + U1F4861F3FB200D2640FE0F = "💆🏻‍♀️" # :woman_getting_massage_light_skin_tone: + U1F4861F3FB200D2640 = "💆🏻‍♀" # :woman_getting_massage_light_skin_tone: + U1F4861F3FE200D2640FE0F = "💆🏾‍♀️" # :woman_getting_massage_medium-dark_skin_tone: + U1F4861F3FE200D2640 = "💆🏾‍♀" # :woman_getting_massage_medium-dark_skin_tone: + U1F4861F3FC200D2640FE0F = "💆🏼‍♀️" # :woman_getting_massage_medium-light_skin_tone: + U1F4861F3FC200D2640 = "💆🏼‍♀" # :woman_getting_massage_medium-light_skin_tone: + U1F4861F3FD200D2640FE0F = "💆🏽‍♀️" # :woman_getting_massage_medium_skin_tone: + U1F4861F3FD200D2640 = "💆🏽‍♀" # :woman_getting_massage_medium_skin_tone: + U1F3CCFE0F200D2640FE0F = "🏌️‍♀️" # :woman_golfing: + U1F3CC200D2640FE0F = "🏌‍♀️" # :woman_golfing: + U1F3CCFE0F200D2640 = "🏌️‍♀" # :woman_golfing: + U1F3CC200D2640 = "🏌‍♀" # :woman_golfing: + U1F3CC1F3FF200D2640FE0F = "🏌🏿‍♀️" # :woman_golfing_dark_skin_tone: + U1F3CC1F3FF200D2640 = "🏌🏿‍♀" # :woman_golfing_dark_skin_tone: + U1F3CC1F3FB200D2640FE0F = "🏌🏻‍♀️" # :woman_golfing_light_skin_tone: + U1F3CC1F3FB200D2640 = "🏌🏻‍♀" # :woman_golfing_light_skin_tone: + U1F3CC1F3FE200D2640FE0F = "🏌🏾‍♀️" # :woman_golfing_medium-dark_skin_tone: + U1F3CC1F3FE200D2640 = "🏌🏾‍♀" # :woman_golfing_medium-dark_skin_tone: + U1F3CC1F3FC200D2640FE0F = "🏌🏼‍♀️" # :woman_golfing_medium-light_skin_tone: + U1F3CC1F3FC200D2640 = "🏌🏼‍♀" # :woman_golfing_medium-light_skin_tone: + U1F3CC1F3FD200D2640FE0F = "🏌🏽‍♀️" # :woman_golfing_medium_skin_tone: + U1F3CC1F3FD200D2640 = "🏌🏽‍♀" # :woman_golfing_medium_skin_tone: + U1F482200D2640FE0F = "💂‍♀️" # :woman_guard: + U1F482200D2640 = "💂‍♀" # :woman_guard: + U1F4821F3FF200D2640FE0F = "💂🏿‍♀️" # :woman_guard_dark_skin_tone: + U1F4821F3FF200D2640 = "💂🏿‍♀" # :woman_guard_dark_skin_tone: + U1F4821F3FB200D2640FE0F = "💂🏻‍♀️" # :woman_guard_light_skin_tone: + U1F4821F3FB200D2640 = "💂🏻‍♀" # :woman_guard_light_skin_tone: + U1F4821F3FE200D2640FE0F = "💂🏾‍♀️" # :woman_guard_medium-dark_skin_tone: + U1F4821F3FE200D2640 = "💂🏾‍♀" # :woman_guard_medium-dark_skin_tone: + U1F4821F3FC200D2640FE0F = "💂🏼‍♀️" # :woman_guard_medium-light_skin_tone: + U1F4821F3FC200D2640 = "💂🏼‍♀" # :woman_guard_medium-light_skin_tone: + U1F4821F3FD200D2640FE0F = "💂🏽‍♀️" # :woman_guard_medium_skin_tone: + U1F4821F3FD200D2640 = "💂🏽‍♀" # :woman_guard_medium_skin_tone: + U1F469200D2695FE0F = "👩‍⚕️" # :woman_health_worker: + U1F469200D2695 = "👩‍⚕" # :woman_health_worker: + U1F4691F3FF200D2695FE0F = "👩🏿‍⚕️" # :woman_health_worker_dark_skin_tone: + U1F4691F3FF200D2695 = "👩🏿‍⚕" # :woman_health_worker_dark_skin_tone: + U1F4691F3FB200D2695FE0F = "👩🏻‍⚕️" # :woman_health_worker_light_skin_tone: + U1F4691F3FB200D2695 = "👩🏻‍⚕" # :woman_health_worker_light_skin_tone: + U1F4691F3FE200D2695FE0F = "👩🏾‍⚕️" # :woman_health_worker_medium-dark_skin_tone: + U1F4691F3FE200D2695 = "👩🏾‍⚕" # :woman_health_worker_medium-dark_skin_tone: + U1F4691F3FC200D2695FE0F = "👩🏼‍⚕️" # :woman_health_worker_medium-light_skin_tone: + U1F4691F3FC200D2695 = "👩🏼‍⚕" # :woman_health_worker_medium-light_skin_tone: + U1F4691F3FD200D2695FE0F = "👩🏽‍⚕️" # :woman_health_worker_medium_skin_tone: + U1F4691F3FD200D2695 = "👩🏽‍⚕" # :woman_health_worker_medium_skin_tone: + U1F9D8200D2640FE0F = "🧘‍♀️" # :woman_in_lotus_position: + U1F9D8200D2640 = "🧘‍♀" # :woman_in_lotus_position: + U1F9D81F3FF200D2640FE0F = "🧘🏿‍♀️" # :woman_in_lotus_position_dark_skin_tone: + U1F9D81F3FF200D2640 = "🧘🏿‍♀" # :woman_in_lotus_position_dark_skin_tone: + U1F9D81F3FB200D2640FE0F = "🧘🏻‍♀️" # :woman_in_lotus_position_light_skin_tone: + U1F9D81F3FB200D2640 = "🧘🏻‍♀" # :woman_in_lotus_position_light_skin_tone: + U1F9D81F3FE200D2640FE0F = "🧘🏾‍♀️" # :woman_in_lotus_position_medium-dark_skin_tone: + U1F9D81F3FE200D2640 = "🧘🏾‍♀" # :woman_in_lotus_position_medium-dark_skin_tone: + U1F9D81F3FC200D2640FE0F = "🧘🏼‍♀️" # :woman_in_lotus_position_medium-light_skin_tone: + U1F9D81F3FC200D2640 = "🧘🏼‍♀" # :woman_in_lotus_position_medium-light_skin_tone: + U1F9D81F3FD200D2640FE0F = "🧘🏽‍♀️" # :woman_in_lotus_position_medium_skin_tone: + U1F9D81F3FD200D2640 = "🧘🏽‍♀" # :woman_in_lotus_position_medium_skin_tone: + U1F469200D1F9BD = "👩‍🦽" # :woman_in_manual_wheelchair: + U1F4691F3FF200D1F9BD = "👩🏿‍🦽" # :woman_in_manual_wheelchair_dark_skin_tone: + U1F469200D1F9BD200D27A1FE0F = "👩‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right: + U1F469200D1F9BD200D27A1 = "👩‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right: + U1F4691F3FF200D1F9BD200D27A1FE0F = "👩🏿‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F4691F3FF200D1F9BD200D27A1 = "👩🏿‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_dark_skin_tone: + U1F4691F3FB200D1F9BD200D27A1FE0F = "👩🏻‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_light_skin_tone: + U1F4691F3FB200D1F9BD200D27A1 = "👩🏻‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_light_skin_tone: + U1F4691F3FE200D1F9BD200D27A1FE0F = "👩🏾‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F4691F3FE200D1F9BD200D27A1 = "👩🏾‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone: + U1F4691F3FC200D1F9BD200D27A1FE0F = "👩🏼‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F4691F3FC200D1F9BD200D27A1 = "👩🏼‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium-light_skin_tone: + U1F4691F3FD200D1F9BD200D27A1FE0F = "👩🏽‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F4691F3FD200D1F9BD200D27A1 = "👩🏽‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium_skin_tone: + U1F4691F3FB200D1F9BD = "👩🏻‍🦽" # :woman_in_manual_wheelchair_light_skin_tone: + U1F4691F3FE200D1F9BD = "👩🏾‍🦽" # :woman_in_manual_wheelchair_medium-dark_skin_tone: + U1F4691F3FC200D1F9BD = "👩🏼‍🦽" # :woman_in_manual_wheelchair_medium-light_skin_tone: + U1F4691F3FD200D1F9BD = "👩🏽‍🦽" # :woman_in_manual_wheelchair_medium_skin_tone: + U1F469200D1F9BC = "👩‍🦼" # :woman_in_motorized_wheelchair: + U1F4691F3FF200D1F9BC = "👩🏿‍🦼" # :woman_in_motorized_wheelchair_dark_skin_tone: + U1F469200D1F9BC200D27A1FE0F = "👩‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right: + U1F469200D1F9BC200D27A1 = "👩‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right: + U1F4691F3FF200D1F9BC200D27A1FE0F = "👩🏿‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F4691F3FF200D1F9BC200D27A1 = "👩🏿‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_dark_skin_tone: + U1F4691F3FB200D1F9BC200D27A1FE0F = "👩🏻‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F4691F3FB200D1F9BC200D27A1 = "👩🏻‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_light_skin_tone: + U1F4691F3FE200D1F9BC200D27A1FE0F = "👩🏾‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F4691F3FE200D1F9BC200D27A1 = "👩🏾‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: + U1F4691F3FC200D1F9BC200D27A1FE0F = "👩🏼‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F4691F3FC200D1F9BC200D27A1 = "👩🏼‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone: + U1F4691F3FD200D1F9BC200D27A1FE0F = "👩🏽‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F4691F3FD200D1F9BC200D27A1 = "👩🏽‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium_skin_tone: + U1F4691F3FB200D1F9BC = "👩🏻‍🦼" # :woman_in_motorized_wheelchair_light_skin_tone: + U1F4691F3FE200D1F9BC = "👩🏾‍🦼" # :woman_in_motorized_wheelchair_medium-dark_skin_tone: + U1F4691F3FC200D1F9BC = "👩🏼‍🦼" # :woman_in_motorized_wheelchair_medium-light_skin_tone: + U1F4691F3FD200D1F9BC = "👩🏽‍🦼" # :woman_in_motorized_wheelchair_medium_skin_tone: + U1F9D6200D2640FE0F = "🧖‍♀️" # :woman_in_steamy_room: + U1F9D6200D2640 = "🧖‍♀" # :woman_in_steamy_room: + U1F9D61F3FF200D2640FE0F = "🧖🏿‍♀️" # :woman_in_steamy_room_dark_skin_tone: + U1F9D61F3FF200D2640 = "🧖🏿‍♀" # :woman_in_steamy_room_dark_skin_tone: + U1F9D61F3FB200D2640FE0F = "🧖🏻‍♀️" # :woman_in_steamy_room_light_skin_tone: + U1F9D61F3FB200D2640 = "🧖🏻‍♀" # :woman_in_steamy_room_light_skin_tone: + U1F9D61F3FE200D2640FE0F = "🧖🏾‍♀️" # :woman_in_steamy_room_medium-dark_skin_tone: + U1F9D61F3FE200D2640 = "🧖🏾‍♀" # :woman_in_steamy_room_medium-dark_skin_tone: + U1F9D61F3FC200D2640FE0F = "🧖🏼‍♀️" # :woman_in_steamy_room_medium-light_skin_tone: + U1F9D61F3FC200D2640 = "🧖🏼‍♀" # :woman_in_steamy_room_medium-light_skin_tone: + U1F9D61F3FD200D2640FE0F = "🧖🏽‍♀️" # :woman_in_steamy_room_medium_skin_tone: + U1F9D61F3FD200D2640 = "🧖🏽‍♀" # :woman_in_steamy_room_medium_skin_tone: + U1F935200D2640FE0F = "🤵‍♀️" # :woman_in_tuxedo: + U1F935200D2640 = "🤵‍♀" # :woman_in_tuxedo: + U1F9351F3FF200D2640FE0F = "🤵🏿‍♀️" # :woman_in_tuxedo_dark_skin_tone: + U1F9351F3FF200D2640 = "🤵🏿‍♀" # :woman_in_tuxedo_dark_skin_tone: + U1F9351F3FB200D2640FE0F = "🤵🏻‍♀️" # :woman_in_tuxedo_light_skin_tone: + U1F9351F3FB200D2640 = "🤵🏻‍♀" # :woman_in_tuxedo_light_skin_tone: + U1F9351F3FE200D2640FE0F = "🤵🏾‍♀️" # :woman_in_tuxedo_medium-dark_skin_tone: + U1F9351F3FE200D2640 = "🤵🏾‍♀" # :woman_in_tuxedo_medium-dark_skin_tone: + U1F9351F3FC200D2640FE0F = "🤵🏼‍♀️" # :woman_in_tuxedo_medium-light_skin_tone: + U1F9351F3FC200D2640 = "🤵🏼‍♀" # :woman_in_tuxedo_medium-light_skin_tone: + U1F9351F3FD200D2640FE0F = "🤵🏽‍♀️" # :woman_in_tuxedo_medium_skin_tone: + U1F9351F3FD200D2640 = "🤵🏽‍♀" # :woman_in_tuxedo_medium_skin_tone: + U1F469200D2696FE0F = "👩‍⚖️" # :woman_judge: + U1F469200D2696 = "👩‍⚖" # :woman_judge: + U1F4691F3FF200D2696FE0F = "👩🏿‍⚖️" # :woman_judge_dark_skin_tone: + U1F4691F3FF200D2696 = "👩🏿‍⚖" # :woman_judge_dark_skin_tone: + U1F4691F3FB200D2696FE0F = "👩🏻‍⚖️" # :woman_judge_light_skin_tone: + U1F4691F3FB200D2696 = "👩🏻‍⚖" # :woman_judge_light_skin_tone: + U1F4691F3FE200D2696FE0F = "👩🏾‍⚖️" # :woman_judge_medium-dark_skin_tone: + U1F4691F3FE200D2696 = "👩🏾‍⚖" # :woman_judge_medium-dark_skin_tone: + U1F4691F3FC200D2696FE0F = "👩🏼‍⚖️" # :woman_judge_medium-light_skin_tone: + U1F4691F3FC200D2696 = "👩🏼‍⚖" # :woman_judge_medium-light_skin_tone: + U1F4691F3FD200D2696FE0F = "👩🏽‍⚖️" # :woman_judge_medium_skin_tone: + U1F4691F3FD200D2696 = "👩🏽‍⚖" # :woman_judge_medium_skin_tone: + U1F939200D2640FE0F = "🤹‍♀️" # :woman_juggling: + U1F939200D2640 = "🤹‍♀" # :woman_juggling: + U1F9391F3FF200D2640FE0F = "🤹🏿‍♀️" # :woman_juggling_dark_skin_tone: + U1F9391F3FF200D2640 = "🤹🏿‍♀" # :woman_juggling_dark_skin_tone: + U1F9391F3FB200D2640FE0F = "🤹🏻‍♀️" # :woman_juggling_light_skin_tone: + U1F9391F3FB200D2640 = "🤹🏻‍♀" # :woman_juggling_light_skin_tone: + U1F9391F3FE200D2640FE0F = "🤹🏾‍♀️" # :woman_juggling_medium-dark_skin_tone: + U1F9391F3FE200D2640 = "🤹🏾‍♀" # :woman_juggling_medium-dark_skin_tone: + U1F9391F3FC200D2640FE0F = "🤹🏼‍♀️" # :woman_juggling_medium-light_skin_tone: + U1F9391F3FC200D2640 = "🤹🏼‍♀" # :woman_juggling_medium-light_skin_tone: + U1F9391F3FD200D2640FE0F = "🤹🏽‍♀️" # :woman_juggling_medium_skin_tone: + U1F9391F3FD200D2640 = "🤹🏽‍♀" # :woman_juggling_medium_skin_tone: + U1F9CE200D2640FE0F = "🧎‍♀️" # :woman_kneeling: + U1F9CE200D2640 = "🧎‍♀" # :woman_kneeling: + U1F9CE1F3FF200D2640FE0F = "🧎🏿‍♀️" # :woman_kneeling_dark_skin_tone: + U1F9CE1F3FF200D2640 = "🧎🏿‍♀" # :woman_kneeling_dark_skin_tone: + U1F9CE200D2640FE0F200D27A1FE0F = "🧎‍♀️‍➡️" # :woman_kneeling_facing_right: + U1F9CE200D2640200D27A1FE0F = "🧎‍♀‍➡️" # :woman_kneeling_facing_right: + U1F9CE200D2640FE0F200D27A1 = "🧎‍♀️‍➡" # :woman_kneeling_facing_right: + U1F9CE200D2640200D27A1 = "🧎‍♀‍➡" # :woman_kneeling_facing_right: + U1F9CE1F3FF200D2640FE0F200D27A1FE0F = "🧎🏿‍♀️‍➡️" # :woman_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2640200D27A1FE0F = "🧎🏿‍♀‍➡️" # :woman_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2640FE0F200D27A1 = "🧎🏿‍♀️‍➡" # :woman_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FF200D2640200D27A1 = "🧎🏿‍♀‍➡" # :woman_kneeling_facing_right_dark_skin_tone: + U1F9CE1F3FB200D2640FE0F200D27A1FE0F = "🧎🏻‍♀️‍➡️" # :woman_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2640200D27A1FE0F = "🧎🏻‍♀‍➡️" # :woman_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2640FE0F200D27A1 = "🧎🏻‍♀️‍➡" # :woman_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FB200D2640200D27A1 = "🧎🏻‍♀‍➡" # :woman_kneeling_facing_right_light_skin_tone: + U1F9CE1F3FE200D2640FE0F200D27A1FE0F = "🧎🏾‍♀️‍➡️" # :woman_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2640200D27A1FE0F = "🧎🏾‍♀‍➡️" # :woman_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2640FE0F200D27A1 = "🧎🏾‍♀️‍➡" # :woman_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FE200D2640200D27A1 = "🧎🏾‍♀‍➡" # :woman_kneeling_facing_right_medium-dark_skin_tone: + U1F9CE1F3FC200D2640FE0F200D27A1FE0F = "🧎🏼‍♀️‍➡️" # :woman_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2640200D27A1FE0F = "🧎🏼‍♀‍➡️" # :woman_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2640FE0F200D27A1 = "🧎🏼‍♀️‍➡" # :woman_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FC200D2640200D27A1 = "🧎🏼‍♀‍➡" # :woman_kneeling_facing_right_medium-light_skin_tone: + U1F9CE1F3FD200D2640FE0F200D27A1FE0F = "🧎🏽‍♀️‍➡️" # :woman_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2640200D27A1FE0F = "🧎🏽‍♀‍➡️" # :woman_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2640FE0F200D27A1 = "🧎🏽‍♀️‍➡" # :woman_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FD200D2640200D27A1 = "🧎🏽‍♀‍➡" # :woman_kneeling_facing_right_medium_skin_tone: + U1F9CE1F3FB200D2640FE0F = "🧎🏻‍♀️" # :woman_kneeling_light_skin_tone: + U1F9CE1F3FB200D2640 = "🧎🏻‍♀" # :woman_kneeling_light_skin_tone: + U1F9CE1F3FE200D2640FE0F = "🧎🏾‍♀️" # :woman_kneeling_medium-dark_skin_tone: + U1F9CE1F3FE200D2640 = "🧎🏾‍♀" # :woman_kneeling_medium-dark_skin_tone: + U1F9CE1F3FC200D2640FE0F = "🧎🏼‍♀️" # :woman_kneeling_medium-light_skin_tone: + U1F9CE1F3FC200D2640 = "🧎🏼‍♀" # :woman_kneeling_medium-light_skin_tone: + U1F9CE1F3FD200D2640FE0F = "🧎🏽‍♀️" # :woman_kneeling_medium_skin_tone: + U1F9CE1F3FD200D2640 = "🧎🏽‍♀" # :woman_kneeling_medium_skin_tone: + U1F3CBFE0F200D2640FE0F = "🏋️‍♀️" # :woman_lifting_weights: + U1F3CB200D2640FE0F = "🏋‍♀️" # :woman_lifting_weights: + U1F3CBFE0F200D2640 = "🏋️‍♀" # :woman_lifting_weights: + U1F3CB200D2640 = "🏋‍♀" # :woman_lifting_weights: + U1F3CB1F3FF200D2640FE0F = "🏋🏿‍♀️" # :woman_lifting_weights_dark_skin_tone: + U1F3CB1F3FF200D2640 = "🏋🏿‍♀" # :woman_lifting_weights_dark_skin_tone: + U1F3CB1F3FB200D2640FE0F = "🏋🏻‍♀️" # :woman_lifting_weights_light_skin_tone: + U1F3CB1F3FB200D2640 = "🏋🏻‍♀" # :woman_lifting_weights_light_skin_tone: + U1F3CB1F3FE200D2640FE0F = "🏋🏾‍♀️" # :woman_lifting_weights_medium-dark_skin_tone: + U1F3CB1F3FE200D2640 = "🏋🏾‍♀" # :woman_lifting_weights_medium-dark_skin_tone: + U1F3CB1F3FC200D2640FE0F = "🏋🏼‍♀️" # :woman_lifting_weights_medium-light_skin_tone: + U1F3CB1F3FC200D2640 = "🏋🏼‍♀" # :woman_lifting_weights_medium-light_skin_tone: + U1F3CB1F3FD200D2640FE0F = "🏋🏽‍♀️" # :woman_lifting_weights_medium_skin_tone: + U1F3CB1F3FD200D2640 = "🏋🏽‍♀" # :woman_lifting_weights_medium_skin_tone: + U1F4691F3FB = "👩🏻" # :woman_light_skin_tone: + U1F4691F3FB200D1F9B2 = "👩🏻‍🦲" # :woman_light_skin_tone_bald: + U1F9D41F3FB200D2640FE0F = "🧔🏻‍♀️" # :woman_light_skin_tone_beard: + U1F9D41F3FB200D2640 = "🧔🏻‍♀" # :woman_light_skin_tone_beard: + U1F4711F3FB200D2640FE0F = "👱🏻‍♀️" # :woman_light_skin_tone_blond_hair: + U1F4711F3FB200D2640 = "👱🏻‍♀" # :woman_light_skin_tone_blond_hair: + U1F4691F3FB200D1F9B1 = "👩🏻‍🦱" # :woman_light_skin_tone_curly_hair: + U1F4691F3FB200D1F9B0 = "👩🏻‍🦰" # :woman_light_skin_tone_red_hair: + U1F4691F3FB200D1F9B3 = "👩🏻‍🦳" # :woman_light_skin_tone_white_hair: + U1F9D9200D2640FE0F = "🧙‍♀️" # :woman_mage: + U1F9D9200D2640 = "🧙‍♀" # :woman_mage: + U1F9D91F3FF200D2640FE0F = "🧙🏿‍♀️" # :woman_mage_dark_skin_tone: + U1F9D91F3FF200D2640 = "🧙🏿‍♀" # :woman_mage_dark_skin_tone: + U1F9D91F3FB200D2640FE0F = "🧙🏻‍♀️" # :woman_mage_light_skin_tone: + U1F9D91F3FB200D2640 = "🧙🏻‍♀" # :woman_mage_light_skin_tone: + U1F9D91F3FE200D2640FE0F = "🧙🏾‍♀️" # :woman_mage_medium-dark_skin_tone: + U1F9D91F3FE200D2640 = "🧙🏾‍♀" # :woman_mage_medium-dark_skin_tone: + U1F9D91F3FC200D2640FE0F = "🧙🏼‍♀️" # :woman_mage_medium-light_skin_tone: + U1F9D91F3FC200D2640 = "🧙🏼‍♀" # :woman_mage_medium-light_skin_tone: + U1F9D91F3FD200D2640FE0F = "🧙🏽‍♀️" # :woman_mage_medium_skin_tone: + U1F9D91F3FD200D2640 = "🧙🏽‍♀" # :woman_mage_medium_skin_tone: + U1F469200D1F527 = "👩‍🔧" # :woman_mechanic: + U1F4691F3FF200D1F527 = "👩🏿‍🔧" # :woman_mechanic_dark_skin_tone: + U1F4691F3FB200D1F527 = "👩🏻‍🔧" # :woman_mechanic_light_skin_tone: + U1F4691F3FE200D1F527 = "👩🏾‍🔧" # :woman_mechanic_medium-dark_skin_tone: + U1F4691F3FC200D1F527 = "👩🏼‍🔧" # :woman_mechanic_medium-light_skin_tone: + U1F4691F3FD200D1F527 = "👩🏽‍🔧" # :woman_mechanic_medium_skin_tone: + U1F4691F3FE = "👩🏾" # :woman_medium-dark_skin_tone: + U1F4691F3FE200D1F9B2 = "👩🏾‍🦲" # :woman_medium-dark_skin_tone_bald: + U1F9D41F3FE200D2640FE0F = "🧔🏾‍♀️" # :woman_medium-dark_skin_tone_beard: + U1F9D41F3FE200D2640 = "🧔🏾‍♀" # :woman_medium-dark_skin_tone_beard: + U1F4711F3FE200D2640FE0F = "👱🏾‍♀️" # :woman_medium-dark_skin_tone_blond_hair: + U1F4711F3FE200D2640 = "👱🏾‍♀" # :woman_medium-dark_skin_tone_blond_hair: + U1F4691F3FE200D1F9B1 = "👩🏾‍🦱" # :woman_medium-dark_skin_tone_curly_hair: + U1F4691F3FE200D1F9B0 = "👩🏾‍🦰" # :woman_medium-dark_skin_tone_red_hair: + U1F4691F3FE200D1F9B3 = "👩🏾‍🦳" # :woman_medium-dark_skin_tone_white_hair: + U1F4691F3FC = "👩🏼" # :woman_medium-light_skin_tone: + U1F4691F3FC200D1F9B2 = "👩🏼‍🦲" # :woman_medium-light_skin_tone_bald: + U1F9D41F3FC200D2640FE0F = "🧔🏼‍♀️" # :woman_medium-light_skin_tone_beard: + U1F9D41F3FC200D2640 = "🧔🏼‍♀" # :woman_medium-light_skin_tone_beard: + U1F4711F3FC200D2640FE0F = "👱🏼‍♀️" # :woman_medium-light_skin_tone_blond_hair: + U1F4711F3FC200D2640 = "👱🏼‍♀" # :woman_medium-light_skin_tone_blond_hair: + U1F4691F3FC200D1F9B1 = "👩🏼‍🦱" # :woman_medium-light_skin_tone_curly_hair: + U1F4691F3FC200D1F9B0 = "👩🏼‍🦰" # :woman_medium-light_skin_tone_red_hair: + U1F4691F3FC200D1F9B3 = "👩🏼‍🦳" # :woman_medium-light_skin_tone_white_hair: + U1F4691F3FD = "👩🏽" # :woman_medium_skin_tone: + U1F4691F3FD200D1F9B2 = "👩🏽‍🦲" # :woman_medium_skin_tone_bald: + U1F9D41F3FD200D2640FE0F = "🧔🏽‍♀️" # :woman_medium_skin_tone_beard: + U1F9D41F3FD200D2640 = "🧔🏽‍♀" # :woman_medium_skin_tone_beard: + U1F4711F3FD200D2640FE0F = "👱🏽‍♀️" # :woman_medium_skin_tone_blond_hair: + U1F4711F3FD200D2640 = "👱🏽‍♀" # :woman_medium_skin_tone_blond_hair: + U1F4691F3FD200D1F9B1 = "👩🏽‍🦱" # :woman_medium_skin_tone_curly_hair: + U1F4691F3FD200D1F9B0 = "👩🏽‍🦰" # :woman_medium_skin_tone_red_hair: + U1F4691F3FD200D1F9B3 = "👩🏽‍🦳" # :woman_medium_skin_tone_white_hair: + U1F6B5200D2640FE0F = "🚵‍♀️" # :woman_mountain_biking: + U1F6B5200D2640 = "🚵‍♀" # :woman_mountain_biking: + U1F6B51F3FF200D2640FE0F = "🚵🏿‍♀️" # :woman_mountain_biking_dark_skin_tone: + U1F6B51F3FF200D2640 = "🚵🏿‍♀" # :woman_mountain_biking_dark_skin_tone: + U1F6B51F3FB200D2640FE0F = "🚵🏻‍♀️" # :woman_mountain_biking_light_skin_tone: + U1F6B51F3FB200D2640 = "🚵🏻‍♀" # :woman_mountain_biking_light_skin_tone: + U1F6B51F3FE200D2640FE0F = "🚵🏾‍♀️" # :woman_mountain_biking_medium-dark_skin_tone: + U1F6B51F3FE200D2640 = "🚵🏾‍♀" # :woman_mountain_biking_medium-dark_skin_tone: + U1F6B51F3FC200D2640FE0F = "🚵🏼‍♀️" # :woman_mountain_biking_medium-light_skin_tone: + U1F6B51F3FC200D2640 = "🚵🏼‍♀" # :woman_mountain_biking_medium-light_skin_tone: + U1F6B51F3FD200D2640FE0F = "🚵🏽‍♀️" # :woman_mountain_biking_medium_skin_tone: + U1F6B51F3FD200D2640 = "🚵🏽‍♀" # :woman_mountain_biking_medium_skin_tone: + U1F469200D1F4BC = "👩‍💼" # :woman_office_worker: + U1F4691F3FF200D1F4BC = "👩🏿‍💼" # :woman_office_worker_dark_skin_tone: + U1F4691F3FB200D1F4BC = "👩🏻‍💼" # :woman_office_worker_light_skin_tone: + U1F4691F3FE200D1F4BC = "👩🏾‍💼" # :woman_office_worker_medium-dark_skin_tone: + U1F4691F3FC200D1F4BC = "👩🏼‍💼" # :woman_office_worker_medium-light_skin_tone: + U1F4691F3FD200D1F4BC = "👩🏽‍💼" # :woman_office_worker_medium_skin_tone: + U1F469200D2708FE0F = "👩‍✈️" # :woman_pilot: + U1F469200D2708 = "👩‍✈" # :woman_pilot: + U1F4691F3FF200D2708FE0F = "👩🏿‍✈️" # :woman_pilot_dark_skin_tone: + U1F4691F3FF200D2708 = "👩🏿‍✈" # :woman_pilot_dark_skin_tone: + U1F4691F3FB200D2708FE0F = "👩🏻‍✈️" # :woman_pilot_light_skin_tone: + U1F4691F3FB200D2708 = "👩🏻‍✈" # :woman_pilot_light_skin_tone: + U1F4691F3FE200D2708FE0F = "👩🏾‍✈️" # :woman_pilot_medium-dark_skin_tone: + U1F4691F3FE200D2708 = "👩🏾‍✈" # :woman_pilot_medium-dark_skin_tone: + U1F4691F3FC200D2708FE0F = "👩🏼‍✈️" # :woman_pilot_medium-light_skin_tone: + U1F4691F3FC200D2708 = "👩🏼‍✈" # :woman_pilot_medium-light_skin_tone: + U1F4691F3FD200D2708FE0F = "👩🏽‍✈️" # :woman_pilot_medium_skin_tone: + U1F4691F3FD200D2708 = "👩🏽‍✈" # :woman_pilot_medium_skin_tone: + U1F93E200D2640FE0F = "🤾‍♀️" # :woman_playing_handball: + U1F93E200D2640 = "🤾‍♀" # :woman_playing_handball: + U1F93E1F3FF200D2640FE0F = "🤾🏿‍♀️" # :woman_playing_handball_dark_skin_tone: + U1F93E1F3FF200D2640 = "🤾🏿‍♀" # :woman_playing_handball_dark_skin_tone: + U1F93E1F3FB200D2640FE0F = "🤾🏻‍♀️" # :woman_playing_handball_light_skin_tone: + U1F93E1F3FB200D2640 = "🤾🏻‍♀" # :woman_playing_handball_light_skin_tone: + U1F93E1F3FE200D2640FE0F = "🤾🏾‍♀️" # :woman_playing_handball_medium-dark_skin_tone: + U1F93E1F3FE200D2640 = "🤾🏾‍♀" # :woman_playing_handball_medium-dark_skin_tone: + U1F93E1F3FC200D2640FE0F = "🤾🏼‍♀️" # :woman_playing_handball_medium-light_skin_tone: + U1F93E1F3FC200D2640 = "🤾🏼‍♀" # :woman_playing_handball_medium-light_skin_tone: + U1F93E1F3FD200D2640FE0F = "🤾🏽‍♀️" # :woman_playing_handball_medium_skin_tone: + U1F93E1F3FD200D2640 = "🤾🏽‍♀" # :woman_playing_handball_medium_skin_tone: + U1F93D200D2640FE0F = "🤽‍♀️" # :woman_playing_water_polo: + U1F93D200D2640 = "🤽‍♀" # :woman_playing_water_polo: + U1F93D1F3FF200D2640FE0F = "🤽🏿‍♀️" # :woman_playing_water_polo_dark_skin_tone: + U1F93D1F3FF200D2640 = "🤽🏿‍♀" # :woman_playing_water_polo_dark_skin_tone: + U1F93D1F3FB200D2640FE0F = "🤽🏻‍♀️" # :woman_playing_water_polo_light_skin_tone: + U1F93D1F3FB200D2640 = "🤽🏻‍♀" # :woman_playing_water_polo_light_skin_tone: + U1F93D1F3FE200D2640FE0F = "🤽🏾‍♀️" # :woman_playing_water_polo_medium-dark_skin_tone: + U1F93D1F3FE200D2640 = "🤽🏾‍♀" # :woman_playing_water_polo_medium-dark_skin_tone: + U1F93D1F3FC200D2640FE0F = "🤽🏼‍♀️" # :woman_playing_water_polo_medium-light_skin_tone: + U1F93D1F3FC200D2640 = "🤽🏼‍♀" # :woman_playing_water_polo_medium-light_skin_tone: + U1F93D1F3FD200D2640FE0F = "🤽🏽‍♀️" # :woman_playing_water_polo_medium_skin_tone: + U1F93D1F3FD200D2640 = "🤽🏽‍♀" # :woman_playing_water_polo_medium_skin_tone: + U1F46E200D2640FE0F = "👮‍♀️" # :woman_police_officer: + U1F46E200D2640 = "👮‍♀" # :woman_police_officer: + U1F46E1F3FF200D2640FE0F = "👮🏿‍♀️" # :woman_police_officer_dark_skin_tone: + U1F46E1F3FF200D2640 = "👮🏿‍♀" # :woman_police_officer_dark_skin_tone: + U1F46E1F3FB200D2640FE0F = "👮🏻‍♀️" # :woman_police_officer_light_skin_tone: + U1F46E1F3FB200D2640 = "👮🏻‍♀" # :woman_police_officer_light_skin_tone: + U1F46E1F3FE200D2640FE0F = "👮🏾‍♀️" # :woman_police_officer_medium-dark_skin_tone: + U1F46E1F3FE200D2640 = "👮🏾‍♀" # :woman_police_officer_medium-dark_skin_tone: + U1F46E1F3FC200D2640FE0F = "👮🏼‍♀️" # :woman_police_officer_medium-light_skin_tone: + U1F46E1F3FC200D2640 = "👮🏼‍♀" # :woman_police_officer_medium-light_skin_tone: + U1F46E1F3FD200D2640FE0F = "👮🏽‍♀️" # :woman_police_officer_medium_skin_tone: + U1F46E1F3FD200D2640 = "👮🏽‍♀" # :woman_police_officer_medium_skin_tone: + U1F64E200D2640FE0F = "🙎‍♀️" # :woman_pouting: + U1F64E200D2640 = "🙎‍♀" # :woman_pouting: + U1F64E1F3FF200D2640FE0F = "🙎🏿‍♀️" # :woman_pouting_dark_skin_tone: + U1F64E1F3FF200D2640 = "🙎🏿‍♀" # :woman_pouting_dark_skin_tone: + U1F64E1F3FB200D2640FE0F = "🙎🏻‍♀️" # :woman_pouting_light_skin_tone: + U1F64E1F3FB200D2640 = "🙎🏻‍♀" # :woman_pouting_light_skin_tone: + U1F64E1F3FE200D2640FE0F = "🙎🏾‍♀️" # :woman_pouting_medium-dark_skin_tone: + U1F64E1F3FE200D2640 = "🙎🏾‍♀" # :woman_pouting_medium-dark_skin_tone: + U1F64E1F3FC200D2640FE0F = "🙎🏼‍♀️" # :woman_pouting_medium-light_skin_tone: + U1F64E1F3FC200D2640 = "🙎🏼‍♀" # :woman_pouting_medium-light_skin_tone: + U1F64E1F3FD200D2640FE0F = "🙎🏽‍♀️" # :woman_pouting_medium_skin_tone: + U1F64E1F3FD200D2640 = "🙎🏽‍♀" # :woman_pouting_medium_skin_tone: + U1F64B200D2640FE0F = "🙋‍♀️" # :woman_raising_hand: + U1F64B200D2640 = "🙋‍♀" # :woman_raising_hand: + U1F64B1F3FF200D2640FE0F = "🙋🏿‍♀️" # :woman_raising_hand_dark_skin_tone: + U1F64B1F3FF200D2640 = "🙋🏿‍♀" # :woman_raising_hand_dark_skin_tone: + U1F64B1F3FB200D2640FE0F = "🙋🏻‍♀️" # :woman_raising_hand_light_skin_tone: + U1F64B1F3FB200D2640 = "🙋🏻‍♀" # :woman_raising_hand_light_skin_tone: + U1F64B1F3FE200D2640FE0F = "🙋🏾‍♀️" # :woman_raising_hand_medium-dark_skin_tone: + U1F64B1F3FE200D2640 = "🙋🏾‍♀" # :woman_raising_hand_medium-dark_skin_tone: + U1F64B1F3FC200D2640FE0F = "🙋🏼‍♀️" # :woman_raising_hand_medium-light_skin_tone: + U1F64B1F3FC200D2640 = "🙋🏼‍♀" # :woman_raising_hand_medium-light_skin_tone: + U1F64B1F3FD200D2640FE0F = "🙋🏽‍♀️" # :woman_raising_hand_medium_skin_tone: + U1F64B1F3FD200D2640 = "🙋🏽‍♀" # :woman_raising_hand_medium_skin_tone: + U1F469200D1F9B0 = "👩‍🦰" # :woman_red_hair: + U1F6A3200D2640FE0F = "🚣‍♀️" # :woman_rowing_boat: + U1F6A3200D2640 = "🚣‍♀" # :woman_rowing_boat: + U1F6A31F3FF200D2640FE0F = "🚣🏿‍♀️" # :woman_rowing_boat_dark_skin_tone: + U1F6A31F3FF200D2640 = "🚣🏿‍♀" # :woman_rowing_boat_dark_skin_tone: + U1F6A31F3FB200D2640FE0F = "🚣🏻‍♀️" # :woman_rowing_boat_light_skin_tone: + U1F6A31F3FB200D2640 = "🚣🏻‍♀" # :woman_rowing_boat_light_skin_tone: + U1F6A31F3FE200D2640FE0F = "🚣🏾‍♀️" # :woman_rowing_boat_medium-dark_skin_tone: + U1F6A31F3FE200D2640 = "🚣🏾‍♀" # :woman_rowing_boat_medium-dark_skin_tone: + U1F6A31F3FC200D2640FE0F = "🚣🏼‍♀️" # :woman_rowing_boat_medium-light_skin_tone: + U1F6A31F3FC200D2640 = "🚣🏼‍♀" # :woman_rowing_boat_medium-light_skin_tone: + U1F6A31F3FD200D2640FE0F = "🚣🏽‍♀️" # :woman_rowing_boat_medium_skin_tone: + U1F6A31F3FD200D2640 = "🚣🏽‍♀" # :woman_rowing_boat_medium_skin_tone: + U1F3C3200D2640FE0F = "🏃‍♀️" # :woman_running: + U1F3C3200D2640 = "🏃‍♀" # :woman_running: + U1F3C31F3FF200D2640FE0F = "🏃🏿‍♀️" # :woman_running_dark_skin_tone: + U1F3C31F3FF200D2640 = "🏃🏿‍♀" # :woman_running_dark_skin_tone: + U1F3C3200D2640FE0F200D27A1FE0F = "🏃‍♀️‍➡️" # :woman_running_facing_right: + U1F3C3200D2640200D27A1FE0F = "🏃‍♀‍➡️" # :woman_running_facing_right: + U1F3C3200D2640FE0F200D27A1 = "🏃‍♀️‍➡" # :woman_running_facing_right: + U1F3C3200D2640200D27A1 = "🏃‍♀‍➡" # :woman_running_facing_right: + U1F3C31F3FF200D2640FE0F200D27A1FE0F = "🏃🏿‍♀️‍➡️" # :woman_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2640200D27A1FE0F = "🏃🏿‍♀‍➡️" # :woman_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2640FE0F200D27A1 = "🏃🏿‍♀️‍➡" # :woman_running_facing_right_dark_skin_tone: + U1F3C31F3FF200D2640200D27A1 = "🏃🏿‍♀‍➡" # :woman_running_facing_right_dark_skin_tone: + U1F3C31F3FB200D2640FE0F200D27A1FE0F = "🏃🏻‍♀️‍➡️" # :woman_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2640200D27A1FE0F = "🏃🏻‍♀‍➡️" # :woman_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2640FE0F200D27A1 = "🏃🏻‍♀️‍➡" # :woman_running_facing_right_light_skin_tone: + U1F3C31F3FB200D2640200D27A1 = "🏃🏻‍♀‍➡" # :woman_running_facing_right_light_skin_tone: + U1F3C31F3FE200D2640FE0F200D27A1FE0F = "🏃🏾‍♀️‍➡️" # :woman_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2640200D27A1FE0F = "🏃🏾‍♀‍➡️" # :woman_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2640FE0F200D27A1 = "🏃🏾‍♀️‍➡" # :woman_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FE200D2640200D27A1 = "🏃🏾‍♀‍➡" # :woman_running_facing_right_medium-dark_skin_tone: + U1F3C31F3FC200D2640FE0F200D27A1FE0F = "🏃🏼‍♀️‍➡️" # :woman_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2640200D27A1FE0F = "🏃🏼‍♀‍➡️" # :woman_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2640FE0F200D27A1 = "🏃🏼‍♀️‍➡" # :woman_running_facing_right_medium-light_skin_tone: + U1F3C31F3FC200D2640200D27A1 = "🏃🏼‍♀‍➡" # :woman_running_facing_right_medium-light_skin_tone: + U1F3C31F3FD200D2640FE0F200D27A1FE0F = "🏃🏽‍♀️‍➡️" # :woman_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2640200D27A1FE0F = "🏃🏽‍♀‍➡️" # :woman_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2640FE0F200D27A1 = "🏃🏽‍♀️‍➡" # :woman_running_facing_right_medium_skin_tone: + U1F3C31F3FD200D2640200D27A1 = "🏃🏽‍♀‍➡" # :woman_running_facing_right_medium_skin_tone: + U1F3C31F3FB200D2640FE0F = "🏃🏻‍♀️" # :woman_running_light_skin_tone: + U1F3C31F3FB200D2640 = "🏃🏻‍♀" # :woman_running_light_skin_tone: + U1F3C31F3FE200D2640FE0F = "🏃🏾‍♀️" # :woman_running_medium-dark_skin_tone: + U1F3C31F3FE200D2640 = "🏃🏾‍♀" # :woman_running_medium-dark_skin_tone: + U1F3C31F3FC200D2640FE0F = "🏃🏼‍♀️" # :woman_running_medium-light_skin_tone: + U1F3C31F3FC200D2640 = "🏃🏼‍♀" # :woman_running_medium-light_skin_tone: + U1F3C31F3FD200D2640FE0F = "🏃🏽‍♀️" # :woman_running_medium_skin_tone: + U1F3C31F3FD200D2640 = "🏃🏽‍♀" # :woman_running_medium_skin_tone: + U1F469200D1F52C = "👩‍🔬" # :woman_scientist: + U1F4691F3FF200D1F52C = "👩🏿‍🔬" # :woman_scientist_dark_skin_tone: + U1F4691F3FB200D1F52C = "👩🏻‍🔬" # :woman_scientist_light_skin_tone: + U1F4691F3FE200D1F52C = "👩🏾‍🔬" # :woman_scientist_medium-dark_skin_tone: + U1F4691F3FC200D1F52C = "👩🏼‍🔬" # :woman_scientist_medium-light_skin_tone: + U1F4691F3FD200D1F52C = "👩🏽‍🔬" # :woman_scientist_medium_skin_tone: + U1F937200D2640FE0F = "🤷‍♀️" # :woman_shrugging: + U1F937200D2640 = "🤷‍♀" # :woman_shrugging: + U1F9371F3FF200D2640FE0F = "🤷🏿‍♀️" # :woman_shrugging_dark_skin_tone: + U1F9371F3FF200D2640 = "🤷🏿‍♀" # :woman_shrugging_dark_skin_tone: + U1F9371F3FB200D2640FE0F = "🤷🏻‍♀️" # :woman_shrugging_light_skin_tone: + U1F9371F3FB200D2640 = "🤷🏻‍♀" # :woman_shrugging_light_skin_tone: + U1F9371F3FE200D2640FE0F = "🤷🏾‍♀️" # :woman_shrugging_medium-dark_skin_tone: + U1F9371F3FE200D2640 = "🤷🏾‍♀" # :woman_shrugging_medium-dark_skin_tone: + U1F9371F3FC200D2640FE0F = "🤷🏼‍♀️" # :woman_shrugging_medium-light_skin_tone: + U1F9371F3FC200D2640 = "🤷🏼‍♀" # :woman_shrugging_medium-light_skin_tone: + U1F9371F3FD200D2640FE0F = "🤷🏽‍♀️" # :woman_shrugging_medium_skin_tone: + U1F9371F3FD200D2640 = "🤷🏽‍♀" # :woman_shrugging_medium_skin_tone: + U1F469200D1F3A4 = "👩‍🎤" # :woman_singer: + U1F4691F3FF200D1F3A4 = "👩🏿‍🎤" # :woman_singer_dark_skin_tone: + U1F4691F3FB200D1F3A4 = "👩🏻‍🎤" # :woman_singer_light_skin_tone: + U1F4691F3FE200D1F3A4 = "👩🏾‍🎤" # :woman_singer_medium-dark_skin_tone: + U1F4691F3FC200D1F3A4 = "👩🏼‍🎤" # :woman_singer_medium-light_skin_tone: + U1F4691F3FD200D1F3A4 = "👩🏽‍🎤" # :woman_singer_medium_skin_tone: + U1F9CD200D2640FE0F = "🧍‍♀️" # :woman_standing: + U1F9CD200D2640 = "🧍‍♀" # :woman_standing: + U1F9CD1F3FF200D2640FE0F = "🧍🏿‍♀️" # :woman_standing_dark_skin_tone: + U1F9CD1F3FF200D2640 = "🧍🏿‍♀" # :woman_standing_dark_skin_tone: + U1F9CD1F3FB200D2640FE0F = "🧍🏻‍♀️" # :woman_standing_light_skin_tone: + U1F9CD1F3FB200D2640 = "🧍🏻‍♀" # :woman_standing_light_skin_tone: + U1F9CD1F3FE200D2640FE0F = "🧍🏾‍♀️" # :woman_standing_medium-dark_skin_tone: + U1F9CD1F3FE200D2640 = "🧍🏾‍♀" # :woman_standing_medium-dark_skin_tone: + U1F9CD1F3FC200D2640FE0F = "🧍🏼‍♀️" # :woman_standing_medium-light_skin_tone: + U1F9CD1F3FC200D2640 = "🧍🏼‍♀" # :woman_standing_medium-light_skin_tone: + U1F9CD1F3FD200D2640FE0F = "🧍🏽‍♀️" # :woman_standing_medium_skin_tone: + U1F9CD1F3FD200D2640 = "🧍🏽‍♀" # :woman_standing_medium_skin_tone: + U1F469200D1F393 = "👩‍🎓" # :woman_student: + U1F4691F3FF200D1F393 = "👩🏿‍🎓" # :woman_student_dark_skin_tone: + U1F4691F3FB200D1F393 = "👩🏻‍🎓" # :woman_student_light_skin_tone: + U1F4691F3FE200D1F393 = "👩🏾‍🎓" # :woman_student_medium-dark_skin_tone: + U1F4691F3FC200D1F393 = "👩🏼‍🎓" # :woman_student_medium-light_skin_tone: + U1F4691F3FD200D1F393 = "👩🏽‍🎓" # :woman_student_medium_skin_tone: + U1F9B8200D2640FE0F = "🦸‍♀️" # :woman_superhero: + U1F9B8200D2640 = "🦸‍♀" # :woman_superhero: + U1F9B81F3FF200D2640FE0F = "🦸🏿‍♀️" # :woman_superhero_dark_skin_tone: + U1F9B81F3FF200D2640 = "🦸🏿‍♀" # :woman_superhero_dark_skin_tone: + U1F9B81F3FB200D2640FE0F = "🦸🏻‍♀️" # :woman_superhero_light_skin_tone: + U1F9B81F3FB200D2640 = "🦸🏻‍♀" # :woman_superhero_light_skin_tone: + U1F9B81F3FE200D2640FE0F = "🦸🏾‍♀️" # :woman_superhero_medium-dark_skin_tone: + U1F9B81F3FE200D2640 = "🦸🏾‍♀" # :woman_superhero_medium-dark_skin_tone: + U1F9B81F3FC200D2640FE0F = "🦸🏼‍♀️" # :woman_superhero_medium-light_skin_tone: + U1F9B81F3FC200D2640 = "🦸🏼‍♀" # :woman_superhero_medium-light_skin_tone: + U1F9B81F3FD200D2640FE0F = "🦸🏽‍♀️" # :woman_superhero_medium_skin_tone: + U1F9B81F3FD200D2640 = "🦸🏽‍♀" # :woman_superhero_medium_skin_tone: + U1F9B9200D2640FE0F = "🦹‍♀️" # :woman_supervillain: + U1F9B9200D2640 = "🦹‍♀" # :woman_supervillain: + U1F9B91F3FF200D2640FE0F = "🦹🏿‍♀️" # :woman_supervillain_dark_skin_tone: + U1F9B91F3FF200D2640 = "🦹🏿‍♀" # :woman_supervillain_dark_skin_tone: + U1F9B91F3FB200D2640FE0F = "🦹🏻‍♀️" # :woman_supervillain_light_skin_tone: + U1F9B91F3FB200D2640 = "🦹🏻‍♀" # :woman_supervillain_light_skin_tone: + U1F9B91F3FE200D2640FE0F = "🦹🏾‍♀️" # :woman_supervillain_medium-dark_skin_tone: + U1F9B91F3FE200D2640 = "🦹🏾‍♀" # :woman_supervillain_medium-dark_skin_tone: + U1F9B91F3FC200D2640FE0F = "🦹🏼‍♀️" # :woman_supervillain_medium-light_skin_tone: + U1F9B91F3FC200D2640 = "🦹🏼‍♀" # :woman_supervillain_medium-light_skin_tone: + U1F9B91F3FD200D2640FE0F = "🦹🏽‍♀️" # :woman_supervillain_medium_skin_tone: + U1F9B91F3FD200D2640 = "🦹🏽‍♀" # :woman_supervillain_medium_skin_tone: + U1F3C4200D2640FE0F = "🏄‍♀️" # :woman_surfing: + U1F3C4200D2640 = "🏄‍♀" # :woman_surfing: + U1F3C41F3FF200D2640FE0F = "🏄🏿‍♀️" # :woman_surfing_dark_skin_tone: + U1F3C41F3FF200D2640 = "🏄🏿‍♀" # :woman_surfing_dark_skin_tone: + U1F3C41F3FB200D2640FE0F = "🏄🏻‍♀️" # :woman_surfing_light_skin_tone: + U1F3C41F3FB200D2640 = "🏄🏻‍♀" # :woman_surfing_light_skin_tone: + U1F3C41F3FE200D2640FE0F = "🏄🏾‍♀️" # :woman_surfing_medium-dark_skin_tone: + U1F3C41F3FE200D2640 = "🏄🏾‍♀" # :woman_surfing_medium-dark_skin_tone: + U1F3C41F3FC200D2640FE0F = "🏄🏼‍♀️" # :woman_surfing_medium-light_skin_tone: + U1F3C41F3FC200D2640 = "🏄🏼‍♀" # :woman_surfing_medium-light_skin_tone: + U1F3C41F3FD200D2640FE0F = "🏄🏽‍♀️" # :woman_surfing_medium_skin_tone: + U1F3C41F3FD200D2640 = "🏄🏽‍♀" # :woman_surfing_medium_skin_tone: + U1F3CA200D2640FE0F = "🏊‍♀️" # :woman_swimming: + U1F3CA200D2640 = "🏊‍♀" # :woman_swimming: + U1F3CA1F3FF200D2640FE0F = "🏊🏿‍♀️" # :woman_swimming_dark_skin_tone: + U1F3CA1F3FF200D2640 = "🏊🏿‍♀" # :woman_swimming_dark_skin_tone: + U1F3CA1F3FB200D2640FE0F = "🏊🏻‍♀️" # :woman_swimming_light_skin_tone: + U1F3CA1F3FB200D2640 = "🏊🏻‍♀" # :woman_swimming_light_skin_tone: + U1F3CA1F3FE200D2640FE0F = "🏊🏾‍♀️" # :woman_swimming_medium-dark_skin_tone: + U1F3CA1F3FE200D2640 = "🏊🏾‍♀" # :woman_swimming_medium-dark_skin_tone: + U1F3CA1F3FC200D2640FE0F = "🏊🏼‍♀️" # :woman_swimming_medium-light_skin_tone: + U1F3CA1F3FC200D2640 = "🏊🏼‍♀" # :woman_swimming_medium-light_skin_tone: + U1F3CA1F3FD200D2640FE0F = "🏊🏽‍♀️" # :woman_swimming_medium_skin_tone: + U1F3CA1F3FD200D2640 = "🏊🏽‍♀" # :woman_swimming_medium_skin_tone: + U1F469200D1F3EB = "👩‍🏫" # :woman_teacher: + U1F4691F3FF200D1F3EB = "👩🏿‍🏫" # :woman_teacher_dark_skin_tone: + U1F4691F3FB200D1F3EB = "👩🏻‍🏫" # :woman_teacher_light_skin_tone: + U1F4691F3FE200D1F3EB = "👩🏾‍🏫" # :woman_teacher_medium-dark_skin_tone: + U1F4691F3FC200D1F3EB = "👩🏼‍🏫" # :woman_teacher_medium-light_skin_tone: + U1F4691F3FD200D1F3EB = "👩🏽‍🏫" # :woman_teacher_medium_skin_tone: + U1F469200D1F4BB = "👩‍💻" # :woman_technologist: + U1F4691F3FF200D1F4BB = "👩🏿‍💻" # :woman_technologist_dark_skin_tone: + U1F4691F3FB200D1F4BB = "👩🏻‍💻" # :woman_technologist_light_skin_tone: + U1F4691F3FE200D1F4BB = "👩🏾‍💻" # :woman_technologist_medium-dark_skin_tone: + U1F4691F3FC200D1F4BB = "👩🏼‍💻" # :woman_technologist_medium-light_skin_tone: + U1F4691F3FD200D1F4BB = "👩🏽‍💻" # :woman_technologist_medium_skin_tone: + U1F481200D2640FE0F = "💁‍♀️" # :woman_tipping_hand: + U1F481200D2640 = "💁‍♀" # :woman_tipping_hand: + U1F4811F3FF200D2640FE0F = "💁🏿‍♀️" # :woman_tipping_hand_dark_skin_tone: + U1F4811F3FF200D2640 = "💁🏿‍♀" # :woman_tipping_hand_dark_skin_tone: + U1F4811F3FB200D2640FE0F = "💁🏻‍♀️" # :woman_tipping_hand_light_skin_tone: + U1F4811F3FB200D2640 = "💁🏻‍♀" # :woman_tipping_hand_light_skin_tone: + U1F4811F3FE200D2640FE0F = "💁🏾‍♀️" # :woman_tipping_hand_medium-dark_skin_tone: + U1F4811F3FE200D2640 = "💁🏾‍♀" # :woman_tipping_hand_medium-dark_skin_tone: + U1F4811F3FC200D2640FE0F = "💁🏼‍♀️" # :woman_tipping_hand_medium-light_skin_tone: + U1F4811F3FC200D2640 = "💁🏼‍♀" # :woman_tipping_hand_medium-light_skin_tone: + U1F4811F3FD200D2640FE0F = "💁🏽‍♀️" # :woman_tipping_hand_medium_skin_tone: + U1F4811F3FD200D2640 = "💁🏽‍♀" # :woman_tipping_hand_medium_skin_tone: + U1F9DB200D2640FE0F = "🧛‍♀️" # :woman_vampire: + U1F9DB200D2640 = "🧛‍♀" # :woman_vampire: + U1F9DB1F3FF200D2640FE0F = "🧛🏿‍♀️" # :woman_vampire_dark_skin_tone: + U1F9DB1F3FF200D2640 = "🧛🏿‍♀" # :woman_vampire_dark_skin_tone: + U1F9DB1F3FB200D2640FE0F = "🧛🏻‍♀️" # :woman_vampire_light_skin_tone: + U1F9DB1F3FB200D2640 = "🧛🏻‍♀" # :woman_vampire_light_skin_tone: + U1F9DB1F3FE200D2640FE0F = "🧛🏾‍♀️" # :woman_vampire_medium-dark_skin_tone: + U1F9DB1F3FE200D2640 = "🧛🏾‍♀" # :woman_vampire_medium-dark_skin_tone: + U1F9DB1F3FC200D2640FE0F = "🧛🏼‍♀️" # :woman_vampire_medium-light_skin_tone: + U1F9DB1F3FC200D2640 = "🧛🏼‍♀" # :woman_vampire_medium-light_skin_tone: + U1F9DB1F3FD200D2640FE0F = "🧛🏽‍♀️" # :woman_vampire_medium_skin_tone: + U1F9DB1F3FD200D2640 = "🧛🏽‍♀" # :woman_vampire_medium_skin_tone: + U1F6B6200D2640FE0F = "🚶‍♀️" # :woman_walking: + U1F6B6200D2640 = "🚶‍♀" # :woman_walking: + U1F6B61F3FF200D2640FE0F = "🚶🏿‍♀️" # :woman_walking_dark_skin_tone: + U1F6B61F3FF200D2640 = "🚶🏿‍♀" # :woman_walking_dark_skin_tone: + U1F6B6200D2640FE0F200D27A1FE0F = "🚶‍♀️‍➡️" # :woman_walking_facing_right: + U1F6B6200D2640200D27A1FE0F = "🚶‍♀‍➡️" # :woman_walking_facing_right: + U1F6B6200D2640FE0F200D27A1 = "🚶‍♀️‍➡" # :woman_walking_facing_right: + U1F6B6200D2640200D27A1 = "🚶‍♀‍➡" # :woman_walking_facing_right: + U1F6B61F3FF200D2640FE0F200D27A1FE0F = "🚶🏿‍♀️‍➡️" # :woman_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2640200D27A1FE0F = "🚶🏿‍♀‍➡️" # :woman_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2640FE0F200D27A1 = "🚶🏿‍♀️‍➡" # :woman_walking_facing_right_dark_skin_tone: + U1F6B61F3FF200D2640200D27A1 = "🚶🏿‍♀‍➡" # :woman_walking_facing_right_dark_skin_tone: + U1F6B61F3FB200D2640FE0F200D27A1FE0F = "🚶🏻‍♀️‍➡️" # :woman_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2640200D27A1FE0F = "🚶🏻‍♀‍➡️" # :woman_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2640FE0F200D27A1 = "🚶🏻‍♀️‍➡" # :woman_walking_facing_right_light_skin_tone: + U1F6B61F3FB200D2640200D27A1 = "🚶🏻‍♀‍➡" # :woman_walking_facing_right_light_skin_tone: + U1F6B61F3FE200D2640FE0F200D27A1FE0F = "🚶🏾‍♀️‍➡️" # :woman_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2640200D27A1FE0F = "🚶🏾‍♀‍➡️" # :woman_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2640FE0F200D27A1 = "🚶🏾‍♀️‍➡" # :woman_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FE200D2640200D27A1 = "🚶🏾‍♀‍➡" # :woman_walking_facing_right_medium-dark_skin_tone: + U1F6B61F3FC200D2640FE0F200D27A1FE0F = "🚶🏼‍♀️‍➡️" # :woman_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2640200D27A1FE0F = "🚶🏼‍♀‍➡️" # :woman_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2640FE0F200D27A1 = "🚶🏼‍♀️‍➡" # :woman_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FC200D2640200D27A1 = "🚶🏼‍♀‍➡" # :woman_walking_facing_right_medium-light_skin_tone: + U1F6B61F3FD200D2640FE0F200D27A1FE0F = "🚶🏽‍♀️‍➡️" # :woman_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2640200D27A1FE0F = "🚶🏽‍♀‍➡️" # :woman_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2640FE0F200D27A1 = "🚶🏽‍♀️‍➡" # :woman_walking_facing_right_medium_skin_tone: + U1F6B61F3FD200D2640200D27A1 = "🚶🏽‍♀‍➡" # :woman_walking_facing_right_medium_skin_tone: + U1F6B61F3FB200D2640FE0F = "🚶🏻‍♀️" # :woman_walking_light_skin_tone: + U1F6B61F3FB200D2640 = "🚶🏻‍♀" # :woman_walking_light_skin_tone: + U1F6B61F3FE200D2640FE0F = "🚶🏾‍♀️" # :woman_walking_medium-dark_skin_tone: + U1F6B61F3FE200D2640 = "🚶🏾‍♀" # :woman_walking_medium-dark_skin_tone: + U1F6B61F3FC200D2640FE0F = "🚶🏼‍♀️" # :woman_walking_medium-light_skin_tone: + U1F6B61F3FC200D2640 = "🚶🏼‍♀" # :woman_walking_medium-light_skin_tone: + U1F6B61F3FD200D2640FE0F = "🚶🏽‍♀️" # :woman_walking_medium_skin_tone: + U1F6B61F3FD200D2640 = "🚶🏽‍♀" # :woman_walking_medium_skin_tone: + U1F473200D2640FE0F = "👳‍♀️" # :woman_wearing_turban: + U1F473200D2640 = "👳‍♀" # :woman_wearing_turban: + U1F4731F3FF200D2640FE0F = "👳🏿‍♀️" # :woman_wearing_turban_dark_skin_tone: + U1F4731F3FF200D2640 = "👳🏿‍♀" # :woman_wearing_turban_dark_skin_tone: + U1F4731F3FB200D2640FE0F = "👳🏻‍♀️" # :woman_wearing_turban_light_skin_tone: + U1F4731F3FB200D2640 = "👳🏻‍♀" # :woman_wearing_turban_light_skin_tone: + U1F4731F3FE200D2640FE0F = "👳🏾‍♀️" # :woman_wearing_turban_medium-dark_skin_tone: + U1F4731F3FE200D2640 = "👳🏾‍♀" # :woman_wearing_turban_medium-dark_skin_tone: + U1F4731F3FC200D2640FE0F = "👳🏼‍♀️" # :woman_wearing_turban_medium-light_skin_tone: + U1F4731F3FC200D2640 = "👳🏼‍♀" # :woman_wearing_turban_medium-light_skin_tone: + U1F4731F3FD200D2640FE0F = "👳🏽‍♀️" # :woman_wearing_turban_medium_skin_tone: + U1F4731F3FD200D2640 = "👳🏽‍♀" # :woman_wearing_turban_medium_skin_tone: + U1F469200D1F9B3 = "👩‍🦳" # :woman_white_hair: + U1F9D5 = "🧕" # :woman_with_headscarf: + U1F9D51F3FF = "🧕🏿" # :woman_with_headscarf_dark_skin_tone: + U1F9D51F3FB = "🧕🏻" # :woman_with_headscarf_light_skin_tone: + U1F9D51F3FE = "🧕🏾" # :woman_with_headscarf_medium-dark_skin_tone: + U1F9D51F3FC = "🧕🏼" # :woman_with_headscarf_medium-light_skin_tone: + U1F9D51F3FD = "🧕🏽" # :woman_with_headscarf_medium_skin_tone: + U1F470200D2640FE0F = "👰‍♀️" # :woman_with_veil: + U1F470200D2640 = "👰‍♀" # :woman_with_veil: + U1F4701F3FF200D2640FE0F = "👰🏿‍♀️" # :woman_with_veil_dark_skin_tone: + U1F4701F3FF200D2640 = "👰🏿‍♀" # :woman_with_veil_dark_skin_tone: + U1F4701F3FB200D2640FE0F = "👰🏻‍♀️" # :woman_with_veil_light_skin_tone: + U1F4701F3FB200D2640 = "👰🏻‍♀" # :woman_with_veil_light_skin_tone: + U1F4701F3FE200D2640FE0F = "👰🏾‍♀️" # :woman_with_veil_medium-dark_skin_tone: + U1F4701F3FE200D2640 = "👰🏾‍♀" # :woman_with_veil_medium-dark_skin_tone: + U1F4701F3FC200D2640FE0F = "👰🏼‍♀️" # :woman_with_veil_medium-light_skin_tone: + U1F4701F3FC200D2640 = "👰🏼‍♀" # :woman_with_veil_medium-light_skin_tone: + U1F4701F3FD200D2640FE0F = "👰🏽‍♀️" # :woman_with_veil_medium_skin_tone: + U1F4701F3FD200D2640 = "👰🏽‍♀" # :woman_with_veil_medium_skin_tone: + U1F469200D1F9AF = "👩‍🦯" # :woman_with_white_cane: + U1F4691F3FF200D1F9AF = "👩🏿‍🦯" # :woman_with_white_cane_dark_skin_tone: + U1F469200D1F9AF200D27A1FE0F = "👩‍🦯‍➡️" # :woman_with_white_cane_facing_right: + U1F469200D1F9AF200D27A1 = "👩‍🦯‍➡" # :woman_with_white_cane_facing_right: + U1F4691F3FF200D1F9AF200D27A1FE0F = "👩🏿‍🦯‍➡️" # :woman_with_white_cane_facing_right_dark_skin_tone: + U1F4691F3FF200D1F9AF200D27A1 = "👩🏿‍🦯‍➡" # :woman_with_white_cane_facing_right_dark_skin_tone: + U1F4691F3FB200D1F9AF200D27A1FE0F = "👩🏻‍🦯‍➡️" # :woman_with_white_cane_facing_right_light_skin_tone: + U1F4691F3FB200D1F9AF200D27A1 = "👩🏻‍🦯‍➡" # :woman_with_white_cane_facing_right_light_skin_tone: + U1F4691F3FE200D1F9AF200D27A1FE0F = "👩🏾‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium-dark_skin_tone: + U1F4691F3FE200D1F9AF200D27A1 = "👩🏾‍🦯‍➡" # :woman_with_white_cane_facing_right_medium-dark_skin_tone: + U1F4691F3FC200D1F9AF200D27A1FE0F = "👩🏼‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium-light_skin_tone: + U1F4691F3FC200D1F9AF200D27A1 = "👩🏼‍🦯‍➡" # :woman_with_white_cane_facing_right_medium-light_skin_tone: + U1F4691F3FD200D1F9AF200D27A1FE0F = "👩🏽‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium_skin_tone: + U1F4691F3FD200D1F9AF200D27A1 = "👩🏽‍🦯‍➡" # :woman_with_white_cane_facing_right_medium_skin_tone: + U1F4691F3FB200D1F9AF = "👩🏻‍🦯" # :woman_with_white_cane_light_skin_tone: + U1F4691F3FE200D1F9AF = "👩🏾‍🦯" # :woman_with_white_cane_medium-dark_skin_tone: + U1F4691F3FC200D1F9AF = "👩🏼‍🦯" # :woman_with_white_cane_medium-light_skin_tone: + U1F4691F3FD200D1F9AF = "👩🏽‍🦯" # :woman_with_white_cane_medium_skin_tone: + U1F9DF200D2640FE0F = "🧟‍♀️" # :woman_zombie: + U1F9DF200D2640 = "🧟‍♀" # :woman_zombie: + U1F462 = "👢" # :woman’s_boot: + U1F45A = "👚" # :woman’s_clothes: + U1F452 = "👒" # :woman’s_hat: + U1F461 = "👡" # :woman’s_sandal: + U1F46D = "👭" # :women_holding_hands: + U1F46D1F3FF = "👭🏿" # :women_holding_hands_dark_skin_tone: + U1F4691F3FF200D1F91D200D1F4691F3FB = "👩🏿‍🤝‍👩🏻" # :women_holding_hands_dark_skin_tone_light_skin_tone: + U1F4691F3FF200D1F91D200D1F4691F3FE = "👩🏿‍🤝‍👩🏾" # :women_holding_hands_dark_skin_tone_medium-dark_skin_tone: + U1F4691F3FF200D1F91D200D1F4691F3FC = "👩🏿‍🤝‍👩🏼" # :women_holding_hands_dark_skin_tone_medium-light_skin_tone: + U1F4691F3FF200D1F91D200D1F4691F3FD = "👩🏿‍🤝‍👩🏽" # :women_holding_hands_dark_skin_tone_medium_skin_tone: + U1F46D1F3FB = "👭🏻" # :women_holding_hands_light_skin_tone: + U1F4691F3FB200D1F91D200D1F4691F3FF = "👩🏻‍🤝‍👩🏿" # :women_holding_hands_light_skin_tone_dark_skin_tone: + U1F4691F3FB200D1F91D200D1F4691F3FE = "👩🏻‍🤝‍👩🏾" # :women_holding_hands_light_skin_tone_medium-dark_skin_tone: + U1F4691F3FB200D1F91D200D1F4691F3FC = "👩🏻‍🤝‍👩🏼" # :women_holding_hands_light_skin_tone_medium-light_skin_tone: + U1F4691F3FB200D1F91D200D1F4691F3FD = "👩🏻‍🤝‍👩🏽" # :women_holding_hands_light_skin_tone_medium_skin_tone: + U1F46D1F3FE = "👭🏾" # :women_holding_hands_medium-dark_skin_tone: + U1F4691F3FE200D1F91D200D1F4691F3FF = "👩🏾‍🤝‍👩🏿" # :women_holding_hands_medium-dark_skin_tone_dark_skin_tone: + U1F4691F3FE200D1F91D200D1F4691F3FB = "👩🏾‍🤝‍👩🏻" # :women_holding_hands_medium-dark_skin_tone_light_skin_tone: + U1F4691F3FE200D1F91D200D1F4691F3FC = "👩🏾‍🤝‍👩🏼" # :women_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: + U1F4691F3FE200D1F91D200D1F4691F3FD = "👩🏾‍🤝‍👩🏽" # :women_holding_hands_medium-dark_skin_tone_medium_skin_tone: + U1F46D1F3FC = "👭🏼" # :women_holding_hands_medium-light_skin_tone: + U1F4691F3FC200D1F91D200D1F4691F3FF = "👩🏼‍🤝‍👩🏿" # :women_holding_hands_medium-light_skin_tone_dark_skin_tone: + U1F4691F3FC200D1F91D200D1F4691F3FB = "👩🏼‍🤝‍👩🏻" # :women_holding_hands_medium-light_skin_tone_light_skin_tone: + U1F4691F3FC200D1F91D200D1F4691F3FE = "👩🏼‍🤝‍👩🏾" # :women_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: + U1F4691F3FC200D1F91D200D1F4691F3FD = "👩🏼‍🤝‍👩🏽" # :women_holding_hands_medium-light_skin_tone_medium_skin_tone: + U1F46D1F3FD = "👭🏽" # :women_holding_hands_medium_skin_tone: + U1F4691F3FD200D1F91D200D1F4691F3FF = "👩🏽‍🤝‍👩🏿" # :women_holding_hands_medium_skin_tone_dark_skin_tone: + U1F4691F3FD200D1F91D200D1F4691F3FB = "👩🏽‍🤝‍👩🏻" # :women_holding_hands_medium_skin_tone_light_skin_tone: + U1F4691F3FD200D1F91D200D1F4691F3FE = "👩🏽‍🤝‍👩🏾" # :women_holding_hands_medium_skin_tone_medium-dark_skin_tone: + U1F4691F3FD200D1F91D200D1F4691F3FC = "👩🏽‍🤝‍👩🏼" # :women_holding_hands_medium_skin_tone_medium-light_skin_tone: + U1F46F200D2640FE0F = "👯‍♀️" # :women_with_bunny_ears: + U1F46F200D2640 = "👯‍♀" # :women_with_bunny_ears: + U1F93C200D2640FE0F = "🤼‍♀️" # :women_wrestling: + U1F93C200D2640 = "🤼‍♀" # :women_wrestling: + U1F6BA = "🚺" # :women’s_room: + U1FAB5 = "🪵" # :wood: + U1F974 = "🥴" # :woozy_face: + U1F5FAFE0F = "🗺️" # :world_map: + U1F5FA = "🗺" # :world_map: + U1FAB1 = "🪱" # :worm: + U1F61F = "😟" # :worried_face: + U1F381 = "🎁" # :wrapped_gift: + U1F527 = "🔧" # :wrench: + U270DFE0F = "✍️" # :writing_hand: + U270D = "✍" # :writing_hand: + U270D1F3FF = "✍🏿" # :writing_hand_dark_skin_tone: + U270D1F3FB = "✍🏻" # :writing_hand_light_skin_tone: + U270D1F3FE = "✍🏾" # :writing_hand_medium-dark_skin_tone: + U270D1F3FC = "✍🏼" # :writing_hand_medium-light_skin_tone: + U270D1F3FD = "✍🏽" # :writing_hand_medium_skin_tone: + U1FA7B = "🩻" # :x-ray: + U1F9F6 = "🧶" # :yarn: + U1F971 = "🥱" # :yawning_face: + U1F7E1 = "🟡" # :yellow_circle: + U1F49B = "💛" # :yellow_heart: + U1F7E8 = "🟨" # :yellow_square: + U1F4B4 = "💴" # :yen_banknote: + U262FFE0F = "☯️" # :yin_yang: + U262F = "☯" # :yin_yang: + U1FA80 = "🪀" # :yo-yo: + U1F92A = "🤪" # :zany_face: + U1F993 = "🦓" # :zebra: + U1F910 = "🤐" # :zipper-mouth_face: + U1F9DF = "🧟" # :zombie: + U1F1E61F1FD = "🇦🇽" # :Åland_Islands: From 91fd41ddc175b9f6ff854d90a14b87517f574bbd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 00:03:02 +0900 Subject: [PATCH 259/552] feat(tle.models): create `EmojiValidator` --- app/tle/models/crew.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index b92fdd3..7bd0670 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -2,14 +2,27 @@ import dataclasses import typing +from django.core.exceptions import ValidationError from django.core.validators import MinValueValidator, MaxValueValidator from django.db import models, transaction +from tle.enums import Emoji from tle.models.user import User from tle.models.user_solved_tier import UserSolvedTier from tle.models.submission_language import SubmissionLanguage +class EmojiValidator: + def __init__(self, message: str | None = None) -> None: + self.message = message + + def __call__(self, value) -> None: + try: + Emoji(value) # just checking if it's valid emoji + except ValueError: + raise ValidationError(self.message, params={"value": value}) + + @dataclasses.dataclass class CrewTag: key: typing.Optional[str] From e068afd8da6fe66da1c20460099b8d32926d13ff Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 00:03:49 +0900 Subject: [PATCH 260/552] feat(tle.models): use `EmojiValidator` for validating `Crew.emoji` --- app/tle/models/crew.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 7bd0670..fa81cfc 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -45,11 +45,9 @@ class Crew(models.Model): ) emoji = models.CharField( max_length=2, - validators=[ - # TODO: 이모지 형식 검사 - ], - null=True, - blank=True, + validators=[EmojiValidator(message='이모지 형식이 아닙니다.')], + null=False, + blank=False, default='🚢', help_text='크루 아이콘을 입력해주세요. (이모지)', ) From 233b60fffc12847b3092c5ba6b78d0fbcff52ce8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 00:10:23 +0900 Subject: [PATCH 261/552] refactor(tle.models): add `choices` modules --- app/tle/models/__init__.py | 8 ++++---- app/tle/models/choices/__init__.py | 2 ++ app/tle/models/{ => choices}/problem_difficulty.py | 0 app/tle/models/{ => choices}/user_solved_tier.py | 0 app/tle/models/crew.py | 2 +- app/tle/models/problem_analysis.py | 2 +- app/tle/models/user.py | 2 +- app/tle/serializers/problem_difficulty.py | 2 +- 8 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 app/tle/models/choices/__init__.py rename app/tle/models/{ => choices}/problem_difficulty.py (100%) rename app/tle/models/{ => choices}/user_solved_tier.py (100%) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index 2264f70..0f5ed09 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,5 +1,6 @@ +from tle.models import choices + from tle.models.user import User, UserManager -from tle.models.user_solved_tier import UserSolvedTier from tle.models.crew import Crew from tle.models.crew_activity import CrewActivity @@ -9,7 +10,6 @@ from tle.models.problem import Problem from tle.models.problem_analysis import ProblemAnalysis -from tle.models.problem_difficulty import ProblemDifficulty from tle.models.problem_tag import ProblemTag from tle.models.submission import Submission @@ -18,9 +18,10 @@ __all__ = ( + 'choices', + 'User', 'UserManager', - 'UserSolvedTier', 'Crew', 'CrewActivity', @@ -30,7 +31,6 @@ 'Problem', 'ProblemAnalysis', - 'ProblemDifficulty', 'ProblemTag', 'Submission', diff --git a/app/tle/models/choices/__init__.py b/app/tle/models/choices/__init__.py new file mode 100644 index 0000000..af0c711 --- /dev/null +++ b/app/tle/models/choices/__init__.py @@ -0,0 +1,2 @@ +from tle.models.choices.user_solved_tier import UserSolvedTier +from tle.models.choices.problem_difficulty import ProblemDifficulty diff --git a/app/tle/models/problem_difficulty.py b/app/tle/models/choices/problem_difficulty.py similarity index 100% rename from app/tle/models/problem_difficulty.py rename to app/tle/models/choices/problem_difficulty.py diff --git a/app/tle/models/user_solved_tier.py b/app/tle/models/choices/user_solved_tier.py similarity index 100% rename from app/tle/models/user_solved_tier.py rename to app/tle/models/choices/user_solved_tier.py diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index fa81cfc..406695d 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -7,8 +7,8 @@ from django.db import models, transaction from tle.enums import Emoji +from tle.models.choices import UserSolvedTier from tle.models.user import User -from tle.models.user_solved_tier import UserSolvedTier from tle.models.submission_language import SubmissionLanguage diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py index c25871f..1335f16 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/problem_analysis.py @@ -1,8 +1,8 @@ from django.db import models +from tle.models.choices import ProblemDifficulty from tle.models.problem import Problem from tle.models.problem_tag import ProblemTag -from tle.models.problem_difficulty import ProblemDifficulty class ProblemAnalysis(models.Model): diff --git a/app/tle/models/user.py b/app/tle/models/user.py index cdf927d..dbba8e6 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -9,7 +9,7 @@ from django.db import models from django.utils import timezone -from tle.models.user_solved_tier import UserSolvedTier +from tle.models.choices import UserSolvedTier def get_profile_image_path(instance: User, filename: str) -> str: diff --git a/app/tle/serializers/problem_difficulty.py b/app/tle/serializers/problem_difficulty.py index 0bdc9c0..8675c48 100644 --- a/app/tle/serializers/problem_difficulty.py +++ b/app/tle/serializers/problem_difficulty.py @@ -1,6 +1,6 @@ from rest_framework.serializers import * -from tle.models import ProblemDifficulty +from tle.models.choices import ProblemDifficulty class ProblemDifficultySerializer(Serializer): From 8b6ce61073323d1392c58f4f0eab4f1c13997eee Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 02:52:55 +0900 Subject: [PATCH 262/552] refactor(tle.models.choices): make "en" lang as default, add arabic number option --- app/tle/models/choices/user_solved_tier.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/tle/models/choices/user_solved_tier.py b/app/tle/models/choices/user_solved_tier.py index 88e73a0..895b960 100644 --- a/app/tle/models/choices/user_solved_tier.py +++ b/app/tle/models/choices/user_solved_tier.py @@ -73,7 +73,7 @@ def get_rank(cls, value: int) -> int: return ((value-1) // 5)+1 @classmethod - def get_rank_name(cls, value: int, lang='ko') -> str: + def get_rank_name(cls, value: int, lang='en') -> str: assert 0 <= value <= 30 return RANK_NAMES[lang][cls.get_rank(value)] @@ -85,11 +85,14 @@ def get_tier(cls, value: int) -> int: return 5 - ((value-1) % 5) @classmethod - def get_tier_name(cls, value: int) -> str: + def get_tier_name(cls, value: int, arabic=True) -> str: assert 0 <= value <= 30 - return ARABIC_NUMERALS[cls.get_tier(value)] + tier = cls.get_tier(value) + if arabic: + return ARABIC_NUMERALS[tier] + return str(tier) @classmethod - def get_name(cls, value: int, lang='ko') -> str: + def get_name(cls, value: int, lang='en', arabic=True) -> str: assert 0 <= value <= 30 - return f'{cls.get_rank_name(value, lang)} {cls.get_tier_name(value)}' + return f'{cls.get_rank_name(value, lang=lang)} {cls.get_tier_name(value, arabic=arabic)}' From 03a2ca62fe668954f12352e483e791ea29f9f6e2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 02:55:19 +0900 Subject: [PATCH 263/552] refactor(tle.serializers): create `BojProfileMixin` --- app/tle/serializers/mixins/__init__.py | 2 ++ app/tle/serializers/mixins/boj_profile.py | 19 +++++++++++++++++++ app/tle/serializers/user_detail.py | 10 +++------- app/tle/serializers/user_sign_in.py | 10 +++------- 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 app/tle/serializers/mixins/boj_profile.py diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index ad18951..59afad2 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -1,6 +1,8 @@ from tle.serializers.mixins.current_user import CurrentUserMixin +from tle.serializers.mixins.boj_profile import BojProfileMixin __all__ = ( 'CurrentUserMixin', + 'BojProfileMixin', ) diff --git a/app/tle/serializers/mixins/boj_profile.py b/app/tle/serializers/mixins/boj_profile.py new file mode 100644 index 0000000..571dfff --- /dev/null +++ b/app/tle/serializers/mixins/boj_profile.py @@ -0,0 +1,19 @@ +from rest_framework.serializers import Serializer + +from tle.models import User +from tle.models.choices import UserSolvedTier + + +class BojProfileMixin: + def boj_profile(self: Serializer, user: User) -> dict: + return { + 'username': user.boj_username, + 'profile_url': f'https://boj.kr/{user.boj_username}', + 'level': user.boj_tier, + 'rank': UserSolvedTier.get_rank(user.boj_tier), + 'rank_name_en': UserSolvedTier.get_rank_name(user.boj_tier, lang='en'), + 'rank_name_ko': UserSolvedTier.get_rank_name(user.boj_tier, lang='ko'), + 'tier': UserSolvedTier.get_tier(user.boj_tier), + 'tier_name': UserSolvedTier.get_tier_name(user.boj_tier, arabic=True), + 'tier_updated_at': user.boj_tier_updated_at, + } diff --git a/app/tle/serializers/user_detail.py b/app/tle/serializers/user_detail.py index dc8f7d8..9b16a43 100644 --- a/app/tle/serializers/user_detail.py +++ b/app/tle/serializers/user_detail.py @@ -1,9 +1,10 @@ from rest_framework.serializers import * from tle.models import User, UserManager +from tle.serializers.mixins import BojProfileMixin -class UserDetailSerializer(ModelSerializer): +class UserDetailSerializer(ModelSerializer, BojProfileMixin): boj = SerializerMethodField(read_only=True) class Meta: @@ -29,12 +30,7 @@ class Meta: } def get_boj(self, obj: User) -> dict: - return { - 'username': obj.boj_username, - 'profile_url': f'https://boj.kr/{obj.boj_username}', - 'tier': obj.boj_tier, - 'tier_updated_at': obj.boj_tier_updated_at, - } + return self.boj_profile(obj) def create(self, validated_data): user_manager: UserManager = User.objects diff --git a/app/tle/serializers/user_sign_in.py b/app/tle/serializers/user_sign_in.py index 4c723f6..2c18da1 100644 --- a/app/tle/serializers/user_sign_in.py +++ b/app/tle/serializers/user_sign_in.py @@ -2,9 +2,10 @@ from rest_framework.serializers import * from tle.models import User +from tle.serializers.mixins import BojProfileMixin -class UserSignInSerializer(ModelSerializer): +class UserSignInSerializer(ModelSerializer, BojProfileMixin): email = EmailField(write_only=True, validators=None) boj = SerializerMethodField() @@ -31,12 +32,7 @@ class Meta: } def get_boj(self, obj: User) -> dict: - return { - 'username': obj.boj_username, - 'profile_url': f'https://boj.kr/{obj.boj_username}', - 'tier': obj.boj_tier, - 'tier_updated_at': obj.boj_tier_updated_at, - } + return self.boj_profile(obj) def create(self, validated_data): raise PermissionDenied('Cannot create user through this serializer') From 2fb2805b8952466dd73931d0c179fa938d361877 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 04:34:45 +0900 Subject: [PATCH 264/552] =?UTF-8?q?fix(tle.models):=20Django=20cannot=20se?= =?UTF-8?q?rialize=20`EmojiValidator=20`=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 406695d..09414d5 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -3,7 +3,11 @@ import typing from django.core.exceptions import ValidationError -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import ( + BaseValidator, + MinValueValidator, + MaxValueValidator, +) from django.db import models, transaction from tle.enums import Emoji @@ -12,7 +16,7 @@ from tle.models.submission_language import SubmissionLanguage -class EmojiValidator: +class EmojiValidator(BaseValidator): def __init__(self, message: str | None = None) -> None: self.message = message From f002a9bf2079f60369b034ef792a1834682c9c0b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 04:58:35 +0900 Subject: [PATCH 265/552] refactor(tle.models): rename `UserSolvedTier` -> `BojUserLevel` --- app/tle/models/choices/__init__.py | 2 +- .../{user_solved_tier.py => bol_user_level.py} | 2 +- app/tle/models/crew.py | 18 +++++++++--------- app/tle/models/user.py | 4 ++-- app/tle/serializers/mixins/boj_profile.py | 16 ++++++++-------- 5 files changed, 21 insertions(+), 21 deletions(-) rename app/tle/models/choices/{user_solved_tier.py => bol_user_level.py} (98%) diff --git a/app/tle/models/choices/__init__.py b/app/tle/models/choices/__init__.py index af0c711..7aa7870 100644 --- a/app/tle/models/choices/__init__.py +++ b/app/tle/models/choices/__init__.py @@ -1,2 +1,2 @@ -from tle.models.choices.user_solved_tier import UserSolvedTier +from tle.models.choices.bol_user_level import BojUserLevel from tle.models.choices.problem_difficulty import ProblemDifficulty diff --git a/app/tle/models/choices/user_solved_tier.py b/app/tle/models/choices/bol_user_level.py similarity index 98% rename from app/tle/models/choices/user_solved_tier.py rename to app/tle/models/choices/bol_user_level.py index 895b960..4c55e0a 100644 --- a/app/tle/models/choices/user_solved_tier.py +++ b/app/tle/models/choices/bol_user_level.py @@ -32,7 +32,7 @@ } -class UserSolvedTier(models.IntegerChoices): +class BojUserLevel(models.IntegerChoices): U = 0, 'Unrated' B5 = 1, '브론즈 5' B4 = 2, '브론즈 4' diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 09414d5..2c8a079 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -11,7 +11,7 @@ from django.db import models, transaction from tle.enums import Emoji -from tle.models.choices import UserSolvedTier +from tle.models.choices import BojUserLevel from tle.models.user import User from tle.models.submission_language import SubmissionLanguage @@ -90,7 +90,7 @@ class Crew(models.Model): ) min_boj_tier = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - choices=UserSolvedTier.choices, + choices=BojUserLevel.choices, blank=True, null=True, default=None, @@ -100,7 +100,7 @@ class Crew(models.Model): validators=[ # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 ], - choices=UserSolvedTier.choices, + choices=BojUserLevel.choices, blank=True, null=True, default=None, @@ -204,15 +204,15 @@ def _build_tier_tags(self) -> typing.List[CrewTag]: return tags def _build_min_tier_tag(self) -> CrewTag: - if UserSolvedTier.get_tier(self.min_boj_tier) == 5: - tier_name = UserSolvedTier.get_rank_name(self.min_boj_tier) + if BojUserLevel.get_tier(self.min_boj_tier) == 5: + tier_name = BojUserLevel.get_rank_name(self.min_boj_tier) else: - tier_name = UserSolvedTier.get_name(self.min_boj_tier) + tier_name = BojUserLevel.get_name(self.min_boj_tier) return CrewTag.from_name(f'{tier_name} 이상') def _build_max_tier_tag(self) -> CrewTag: - if UserSolvedTier.get_tier(self.max_boj_tier) == 1: - tier_name = UserSolvedTier.get_rank_name(self.max_boj_tier) + if BojUserLevel.get_tier(self.max_boj_tier) == 1: + tier_name = BojUserLevel.get_rank_name(self.max_boj_tier) else: - tier_name = UserSolvedTier.get_name(self.max_boj_tier) + tier_name = BojUserLevel.get_name(self.max_boj_tier) return CrewTag.from_name(f'{tier_name} 이하') diff --git a/app/tle/models/user.py b/app/tle/models/user.py index dbba8e6..fc4625f 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -9,7 +9,7 @@ from django.db import models from django.utils import timezone -from tle.models.choices import UserSolvedTier +from tle.models.choices import BojUserLevel def get_profile_image_path(instance: User, filename: str) -> str: @@ -62,7 +62,7 @@ class User(AbstractBaseUser, PermissionsMixin): ) boj_tier = models.IntegerField( help_text='백준 티어', - choices=UserSolvedTier.choices, + choices=BojUserLevel.choices, null=True, blank=True, default=None, diff --git a/app/tle/serializers/mixins/boj_profile.py b/app/tle/serializers/mixins/boj_profile.py index 571dfff..7a4d74d 100644 --- a/app/tle/serializers/mixins/boj_profile.py +++ b/app/tle/serializers/mixins/boj_profile.py @@ -1,7 +1,7 @@ from rest_framework.serializers import Serializer from tle.models import User -from tle.models.choices import UserSolvedTier +from tle.models.choices import BojUserLevel class BojProfileMixin: @@ -9,11 +9,11 @@ def boj_profile(self: Serializer, user: User) -> dict: return { 'username': user.boj_username, 'profile_url': f'https://boj.kr/{user.boj_username}', - 'level': user.boj_tier, - 'rank': UserSolvedTier.get_rank(user.boj_tier), - 'rank_name_en': UserSolvedTier.get_rank_name(user.boj_tier, lang='en'), - 'rank_name_ko': UserSolvedTier.get_rank_name(user.boj_tier, lang='ko'), - 'tier': UserSolvedTier.get_tier(user.boj_tier), - 'tier_name': UserSolvedTier.get_tier_name(user.boj_tier, arabic=True), - 'tier_updated_at': user.boj_tier_updated_at, + 'level': user.boj_level, + 'rank': BojUserLevel.get_rank(user.boj_level), + 'rank_name_en': BojUserLevel.get_rank_name(user.boj_level, lang='en'), + 'rank_name_ko': BojUserLevel.get_rank_name(user.boj_level, lang='ko'), + 'tier': BojUserLevel.get_tier(user.boj_level), + 'tier_name': BojUserLevel.get_tier_name(user.boj_level, arabic=True), + 'tier_updated_at': user.boj_level_updated_at, } From adabedaa529054a39e42f61a6dd6892c6235d176 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:02:16 +0900 Subject: [PATCH 266/552] =?UTF-8?q?refactor(tle.models):=20solved.ac=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=88=98=EC=A4=80=EC=9D=84=20tie?= =?UTF-8?q?r=EA=B0=80=20=EC=95=84=EB=8B=8C=20level=20=EB=A1=9C=20=EC=9A=A9?= =?UTF-8?q?=EC=96=B4=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 38 +++++++++++++++++++------------------- app/tle/models/user.py | 4 ++-- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 2c8a079..bdbe4db 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -88,14 +88,14 @@ class Crew(models.Model): help_text='백준 아이디 필요 여부를 입력해주세요.', default=False, ) - min_boj_tier = models.IntegerField( + min_boj_level = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', choices=BojUserLevel.choices, blank=True, null=True, default=None, ) - max_boj_tier = models.IntegerField( + max_boj_level = models.IntegerField( help_text='최대 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', validators=[ # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 @@ -173,15 +173,15 @@ def is_joinable(self, user: User) -> bool: if user.boj_username is None: # TODO: 인증된 BOJ 사용자명이어야 함 return False - if self.min_boj_tier is not None: - if user.boj_tier is None: + if self.min_boj_level is not None: + if user.boj_level is None: return False - if user.boj_tier < self.min_boj_tier: + if user.boj_level < self.min_boj_level: return False - if self.max_boj_tier is not None: - if user.boj_tier is None: + if self.max_boj_level is not None: + if user.boj_level is None: return False - if user.boj_tier > self.max_boj_tier: + if user.boj_level > self.max_boj_level: return False return True @@ -194,25 +194,25 @@ def get_tags(self) -> typing.List[CrewTag]: def _build_tier_tags(self) -> typing.List[CrewTag]: tags = [] - if self.min_boj_tier is None and self.max_boj_tier is None: + if self.min_boj_level is None and self.max_boj_level is None: tags.append(CrewTag.from_name('티어 무관')) else: - if self.min_boj_tier is not None: + if self.min_boj_level is not None: tags.append(self._build_min_tier_tag()) - if self.max_boj_tier is not None: + if self.max_boj_level is not None: tags.append(self._build_max_tier_tag()) return tags def _build_min_tier_tag(self) -> CrewTag: - if BojUserLevel.get_tier(self.min_boj_tier) == 5: - tier_name = BojUserLevel.get_rank_name(self.min_boj_tier) + if BojUserLevel.get_tier(self.min_boj_level) == 5: + level_name = BojUserLevel.get_rank_name(self.min_boj_level) else: - tier_name = BojUserLevel.get_name(self.min_boj_tier) - return CrewTag.from_name(f'{tier_name} 이상') + level_name = BojUserLevel.get_name(self.min_boj_level) + return CrewTag.from_name(f'{level_name} 이상') def _build_max_tier_tag(self) -> CrewTag: - if BojUserLevel.get_tier(self.max_boj_tier) == 1: - tier_name = BojUserLevel.get_rank_name(self.max_boj_tier) + if BojUserLevel.get_tier(self.max_boj_level) == 1: + level_name = BojUserLevel.get_rank_name(self.max_boj_level) else: - tier_name = BojUserLevel.get_name(self.max_boj_tier) - return CrewTag.from_name(f'{tier_name} 이하') + level_name = BojUserLevel.get_name(self.max_boj_level) + return CrewTag.from_name(f'{level_name} 이하') diff --git a/app/tle/models/user.py b/app/tle/models/user.py index fc4625f..15ab5bd 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -60,14 +60,14 @@ class User(AbstractBaseUser, PermissionsMixin): null=True, blank=True, ) - boj_tier = models.IntegerField( + boj_level = models.IntegerField( help_text='백준 티어', choices=BojUserLevel.choices, null=True, blank=True, default=None, ) - boj_tier_updated_at = models.DateTimeField( + boj_level_updated_at = models.DateTimeField( help_text='백준 티어 갱신 시각', null=True, blank=True, From f0069db835366e6386789cc5e960f2256a1398af Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:32:32 +0900 Subject: [PATCH 267/552] feat(tle.views): implement `/crews/recruiting` --- app/tle/views/urls.py | 3 +++ app/tle/views/viewsets/__init__.py | 1 + app/tle/views/viewsets/crew_viewset.py | 23 +++++++++++++++++++++++ 3 files changed, 27 insertions(+) create mode 100644 app/tle/views/viewsets/crew_viewset.py diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index c6b0a9a..06ab421 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -33,4 +33,7 @@ })) ])), ])), + path("crews/", include([ + path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), + ])), ] diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index b518c35..ea1e66a 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1,3 +1,4 @@ from tle.views.viewsets.auth_viewset import AuthViewSet from tle.views.viewsets.user_viewset import UserViewSet from tle.views.viewsets.problem_viewset import ProblemViewSet +from tle.views.viewsets.crew_viewset import CrewViewSet diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py new file mode 100644 index 0000000..e6ccc33 --- /dev/null +++ b/app/tle/views/viewsets/crew_viewset.py @@ -0,0 +1,23 @@ +from rest_framework.viewsets import ModelViewSet + +from tle.models import Crew +from tle.serializers import * +from tle.views.permissions import * + + +class CrewViewSet(ModelViewSet): + """문제 태그 목록 조회 + 생성 기능""" + permission_classes = [IsAuthenticated] + lookup_field = 'id' + + def get_queryset(self): + if self.action in 'list_recruiting': + return Crew.objects.filter(is_recruiting=True) + return Crew.objects.all() + + def get_serializer_class(self): + if self.action in 'list_recruiting': + return CrewRecruitingSerializer + + def list_recruiting(self, request): + return super().list(request) From 548e4439234471e903929fcd8abb8369e5df88a8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:32:50 +0900 Subject: [PATCH 268/552] docs: update diagrams.drawio --- docs/diagrams.drawio | 2423 ++++++++++++++++++++++-------------------- 1 file changed, 1275 insertions(+), 1148 deletions(-) diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio index a431c9a..dbf1144 100644 --- a/docs/diagrams.drawio +++ b/docs/diagrams.drawio @@ -1,1148 +1,1275 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 57dcd4b61bf6d342ed66af94cd84d23c6e2c90d6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:33:13 +0900 Subject: [PATCH 269/552] refactor(tle.views): add `__all__` --- app/tle/views/viewsets/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index ea1e66a..ecc9605 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -2,3 +2,11 @@ from tle.views.viewsets.user_viewset import UserViewSet from tle.views.viewsets.problem_viewset import ProblemViewSet from tle.views.viewsets.crew_viewset import CrewViewSet + + +__all__ = ( + 'AuthViewSet', + 'UserViewSet', + 'ProblemViewSet', + 'CrewViewSet', +) From f56f36fbd9031dcb1bb048177df06f96b896c709 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:47:53 +0900 Subject: [PATCH 270/552] refactor(tle.models): add type hint for `Crew.activities` --- app/tle/models/crew.py | 3 +++ app/tle/models/crew_activity.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index bdbe4db..8cec3e3 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -129,14 +129,17 @@ def captain(self) -> T_CrewMember: class FieldName: APPLICANTS = 'applicants' MEMBERS = 'members' + ACTIVITIES = 'activities' if typing.TYPE_CHECKING: from . import ( CrewApplicant as T_CrewApplicant, CrewMember as T_CrewMember, + CrewActivity as T_CrewActivity, ) applicants: models.ManyToManyField[T_CrewApplicant] members: models.ManyToManyField[T_CrewMember] + activities: models.ManyToManyField[T_CrewActivity] submittable_languages: models.ManyToManyField[SubmissionLanguage] def __repr__(self) -> str: diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index b81aded..9b9e464 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -7,7 +7,7 @@ class CrewActivity(models.Model): crew = models.ForeignKey( Crew, on_delete=models.CASCADE, - related_name='activities', + related_name=Crew.FieldName.ACTIVITIES, help_text='크루를 입력해주세요.', ) name = models.TextField( From c485bf20ac7bccfe543851c0b36484565aa58fdf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 05:48:23 +0900 Subject: [PATCH 271/552] =?UTF-8?q?chore(tle.serializers):=20add=20TODO=20?= =?UTF-8?q?"/crew/recruting"=EC=97=90=20=EA=B2=80=EC=83=89=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_recruiting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index af339a2..9466638 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -21,6 +21,7 @@ class Meta: 'members', 'tags', ] + read_only_fields = ['__all__'] def get_is_joinable(self, obj: Crew): return obj.is_recruiting From 0929e1dcd516d1bec478000fc55b0f943c116d58 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 06:07:35 +0900 Subject: [PATCH 272/552] =?UTF-8?q?feat(tle.models):=20`Problem`=20?= =?UTF-8?q?=EC=9D=98=20=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=A0=AC=20=EC=88=9C?= =?UTF-8?q?=EC=84=9C=EB=A5=BC=20=EC=83=9D=EC=84=B1=EC=9D=BC=EC=9D=98=20?= =?UTF-8?q?=EB=82=B4=EB=A6=BC=EC=B0=A8=EC=88=9C=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index bb1353b..25793b3 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -44,6 +44,9 @@ class Problem(models.Model): ) updated_at = models.DateTimeField(auto_now=True) + class Meta: + ordering = ['-created_at'] + class FieldName: ANALYSIS = 'analysis' From 71a5467f74787402297361b3eb6ec27bce897c75 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 06:08:08 +0900 Subject: [PATCH 273/552] =?UTF-8?q?feat(tle.models):=20`Crew`=20=EC=9D=98?= =?UTF-8?q?=20=EA=B8=B0=EB=B3=B8=20=EC=A0=95=EB=A0=AC=20=EC=88=9C=EC=84=9C?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=A0=95=EC=9D=BC=EC=9D=98=20=EB=82=B4?= =?UTF-8?q?=EB=A6=BC=EC=B0=A8=EC=88=9C=EC=9C=BC=EB=A1=9C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 8cec3e3..d27444c 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -126,6 +126,9 @@ class Crew(models.Model): def captain(self) -> T_CrewMember: return self.members.get(is_captain=True) + class Meta: + ordering = ['-updated_at'] + class FieldName: APPLICANTS = 'applicants' MEMBERS = 'members' From 155e909e834f86a775208fb8ae9e88b072e9eddd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 06:36:59 +0900 Subject: [PATCH 274/552] =?UTF-8?q?feat(tle.models):=20`CrewActivity`=20?= =?UTF-8?q?=EC=9D=98=20=EC=A0=95=EB=A0=AC=20=EC=88=9C=EC=84=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=EC=97=90=20=EA=B8=B0=EC=A4=80=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew_activity.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index 9b9e464..a2727fe 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -20,6 +20,10 @@ class CrewActivity(models.Model): help_text='활동 종료 일자를 입력해주세요.', ) + class Meta: + ordering = ['start_at'] + get_latest_by = ['end_at'] + def __repr__(self) -> str: return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' From 0678b3bb9b058f261e2fa35ceba3f5c87c817402 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 06:37:13 +0900 Subject: [PATCH 275/552] feat(tle.models): add property `CrewActivity.is_ended` --- app/tle/models/crew_activity.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index a2727fe..b2e08df 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -1,4 +1,5 @@ from django.db import models +from django.utils import timezone from tle.models.crew import Crew @@ -24,6 +25,10 @@ class Meta: ordering = ['start_at'] get_latest_by = ['end_at'] + @property + def is_ended(self) -> bool: + return self.end_at < timezone.now() + def __repr__(self) -> str: return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' From 6273f1e4c01fb99fe25045c9098692a595f32ac2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 08:10:28 +0900 Subject: [PATCH 276/552] refactor(tle.models): add ORM type hints, field name mappings --- app/tle/models/crew.py | 48 +++++++++++++-------- app/tle/models/crew_activity.py | 18 +++++++- app/tle/models/crew_activity_problem.py | 28 +++++++----- app/tle/models/crew_applicant.py | 19 ++++++--- app/tle/models/crew_member.py | 16 ++++--- app/tle/models/problem.py | 31 ++++++++++---- app/tle/models/problem_analysis.py | 11 ++++- app/tle/models/problem_tag.py | 7 ++- app/tle/models/submission.py | 19 +++++++-- app/tle/models/submission_comment.py | 17 +++++++- app/tle/models/submission_language.py | 20 +++++---- app/tle/models/user.py | 57 +++++++++++++++---------- 12 files changed, 200 insertions(+), 91 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index d27444c..2db60d1 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -22,7 +22,7 @@ def __init__(self, message: str | None = None) -> None: def __call__(self, value) -> None: try: - Emoji(value) # just checking if it's valid emoji + Emoji(value) # just checking if it's valid emoji except ValueError: raise ValidationError(self.message, params={"value": value}) @@ -73,7 +73,6 @@ class Crew(models.Model): ) submittable_languages = models.ManyToManyField( SubmissionLanguage, - related_name='crews', help_text='유저가 사용 가능한 언어를 입력해주세요.', ) custom_tags = models.JSONField( @@ -122,28 +121,41 @@ class Crew(models.Model): ) updated_at = models.DateTimeField(auto_now=True) - @property - def captain(self) -> T_CrewMember: - return self.members.get(is_captain=True) + if typing.TYPE_CHECKING: + import tle.models as t - class Meta: - ordering = ['-updated_at'] + applicants: models.ManyToManyField[t.CrewApplicant] + members: models.ManyToManyField[t.CrewMember] + activities: models.ManyToManyField[t.CrewActivity] + submittable_languages: models.ManyToManyField[SubmissionLanguage] - class FieldName: + class field_name: + # related fields APPLICANTS = 'applicants' MEMBERS = 'members' ACTIVITIES = 'activities' + SUBMITTABLE_LANGUAGES = 'submittable_languages' + # fields + NAME = 'name' + EMOJI = 'emoji' + MAX_MEMBERS = 'max_members' + NOTICE = 'notice' + CUSTOM_TAGS = 'custom_tags' + IS_BOJ_USERNAME_REQUIRED = 'is_boj_username_required' + MIN_BOJ_LEVEL = 'min_boj_level' + MAX_BOJ_LEVEL = 'max_boj_level' + IS_RECRUITING = 'is_recruiting' + IS_ACTIVE = 'is_active' + CREATED_AT = 'created_at' + CREATED_BY = 'created_by' + UPDATED_AT = 'updated_at' - if typing.TYPE_CHECKING: - from . import ( - CrewApplicant as T_CrewApplicant, - CrewMember as T_CrewMember, - CrewActivity as T_CrewActivity, - ) - applicants: models.ManyToManyField[T_CrewApplicant] - members: models.ManyToManyField[T_CrewMember] - activities: models.ManyToManyField[T_CrewActivity] - submittable_languages: models.ManyToManyField[SubmissionLanguage] + class Meta: + ordering = ['-updated_at'] + + @property + def captain(self) -> t.CrewMember: + return self.members.get(is_captain=True) def __repr__(self) -> str: return f'[{self.emoji} {self.name}]' diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index b2e08df..12ea229 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -1,3 +1,5 @@ +import typing + from django.db import models from django.utils import timezone @@ -8,7 +10,7 @@ class CrewActivity(models.Model): crew = models.ForeignKey( Crew, on_delete=models.CASCADE, - related_name=Crew.FieldName.ACTIVITIES, + related_name=Crew.field_name.ACTIVITIES, help_text='크루를 입력해주세요.', ) name = models.TextField( @@ -21,6 +23,20 @@ class CrewActivity(models.Model): help_text='활동 종료 일자를 입력해주세요.', ) + if typing.TYPE_CHECKING: + import tle.models as t + + problems: models.ManyToOneRel[t.CrewActivityProblem] + + class field_name: + # related fields + PROBLEMS = 'problems' + # fields + CREW = 'crew' + NAME = 'name' + START_AT = 'start_at' + END_AT = 'end_at' + class Meta: ordering = ['start_at'] get_latest_by = ['end_at'] diff --git a/app/tle/models/crew_activity_problem.py b/app/tle/models/crew_activity_problem.py index c095fa8..5ac9b5e 100644 --- a/app/tle/models/crew_activity_problem.py +++ b/app/tle/models/crew_activity_problem.py @@ -11,13 +11,13 @@ class CrewActivityProblem(models.Model): activity = models.ForeignKey( CrewActivity, on_delete=models.CASCADE, - related_name='problems', + related_name=CrewActivity.field_name.PROBLEMS, help_text='활동을 입력해주세요.', ) problem = models.ForeignKey( Problem, on_delete=models.PROTECT, - related_name='activities', + related_name=Problem.field_name.ACTIVITY_PROBLEMS, help_text='문제를 입력해주세요.', ) order = models.IntegerField( @@ -28,6 +28,20 @@ class CrewActivityProblem(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + if typing.TYPE_CHECKING: + import tle.models as t + + submissions: models.ManyToOneRel[t.Submission] + + class field_name: + # related fields + SUBMISSIONS = 'submissions' + # fields + ACTIVITY = 'activity' + PROBLEM = 'problem' + ORDER = 'order' + CREATED_AT = 'created_at' + class Meta: constraints = [ models.UniqueConstraint( @@ -35,15 +49,7 @@ class Meta: name='unique_order_per_activity_problem', ), ] - - class FieldName: - SUBMISSIONS = 'submissions' - - if typing.TYPE_CHECKING: - from tle.models.submission import ( - Submission as T_Submission, - ) - submissions: models.QuerySet[T_Submission] + ordering = ['order'] def __repr__(self) -> str: return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' diff --git a/app/tle/models/crew_applicant.py b/app/tle/models/crew_applicant.py index 7431056..266ee2d 100644 --- a/app/tle/models/crew_applicant.py +++ b/app/tle/models/crew_applicant.py @@ -1,7 +1,4 @@ -from django.db import ( - models, - transaction, -) +from django.db import models, transaction from django.utils import timezone from tle.models.user import User @@ -13,13 +10,13 @@ class CrewApplicant(models.Model): crew = models.ForeignKey[Crew]( Crew, on_delete=models.CASCADE, - related_name=Crew.FieldName.APPLICANTS, + related_name=Crew.field_name.APPLICANTS, help_text='크루를 입력해주세요.', ) user = models.ForeignKey[User]( User, on_delete=models.CASCADE, - related_name=User.FieldName.APPLICANTS, + related_name=User.field_name.APPLICANTS, help_text='유저를 입력해주세요.', ) message = models.TextField( @@ -46,6 +43,15 @@ class CrewApplicant(models.Model): default=None, ) + class field_name: + CREW = 'crew' + USER = 'user' + MESSAGE = 'message' + IS_ACCEPTED = 'is_accepted' + CREATED_AT = 'created_at' + REVIEWED_AT = 'reviewed_at' + REVIEWED_BY = 'reviewed_by' + class Meta: constraints = [ models.UniqueConstraint( @@ -53,6 +59,7 @@ class Meta: name='unique_applicant_per_crew', ), ] + ordering = ['reviewed_by', 'created_at'] def __repr__(self) -> str: return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index 5e5c213..daf3bcf 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -1,9 +1,6 @@ from __future__ import annotations -from django.db import ( - models, - transaction, -) +from django.db import models, transaction from tle.models.user import User from tle.models.crew import Crew @@ -13,13 +10,13 @@ class CrewMember(models.Model): crew = models.ForeignKey( Crew, on_delete=models.CASCADE, - related_name=Crew.FieldName.MEMBERS, + related_name=Crew.field_name.MEMBERS, help_text='크루를 입력해주세요.', ) user = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.FieldName.MEMBERS, + related_name=User.field_name.MEMBERS, help_text='유저를 입력해주세요.', ) is_captain = models.BooleanField( @@ -28,6 +25,12 @@ class CrewMember(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + class field_name: + CREW = 'crew' + USER = 'user' + IS_CAPTAIN = 'is_captain' + CREATED_AT = 'created_at' + class Meta: constraints = [ models.UniqueConstraint( @@ -39,6 +42,7 @@ class Meta: name='unique_member_per_crew' ), ] + ordering = ['is_captain', 'created_at'] def __repr__(self) -> str: return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 25793b3..994ccd5 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -38,23 +38,36 @@ class Problem(models.Model): created_by = models.ForeignKey( User, on_delete=models.SET_NULL, - related_name=User.FieldName.PROBLEMS, + related_name=User.field_name.PROBLEMS, help_text='이 문제를 추가한 사용자를 입력해주세요.', null=True, ) updated_at = models.DateTimeField(auto_now=True) - class Meta: - ordering = ['-created_at'] + if typing.TYPE_CHECKING: + import tle.models as t + + analysis: models.OneToOneField[t.ProblemAnalysis] + activity_problems: models.ManyToOneRel[t.CrewActivityProblem] - class FieldName: + class field_name: + # related fields ANALYSIS = 'analysis' + ACTIVITY_PROBLEMS = 'activity_problems' + # fields + TITLE = 'title' + LINK = 'link' + DESCRIPTION = 'description' + INPUT_DESCRIPTION = 'input_description' + OUTPUT_DESCRIPTION = 'output_description' + MEMORY_LIMIT = 'memory_limit' + TIME_LIMIT = 'time_limit' + CREATED_AT = 'created_at' + CREATED_BY = 'created_by' + UPDATED_AT = 'updated_at' - if typing.TYPE_CHECKING: - from . import ( - ProblemAnalysis as T_ProblemAnalysis, - ) - analysis: models.OneToOneField[T_ProblemAnalysis] + class Meta: + ordering = ['-created_at'] MEMORY_LIMIT_UNIT = { "name_ko": "메가 바이트", diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py index 1335f16..497a83f 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/problem_analysis.py @@ -9,7 +9,7 @@ class ProblemAnalysis(models.Model): problem = models.OneToOneField( Problem, on_delete=models.CASCADE, - related_name=Problem.FieldName.ANALYSIS, + related_name=Problem.field_name.ANALYSIS, help_text='문제를 입력해주세요.', ) difficulty = models.IntegerField( @@ -18,7 +18,6 @@ class ProblemAnalysis(models.Model): ) tags = models.ManyToManyField( ProblemTag, - related_name='problems', help_text='문제의 DSA 태그를 입력해주세요.', ) time_complexity = models.CharField( @@ -41,6 +40,14 @@ class ProblemAnalysis(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) + class field_name: + PROBLEM = 'problem' + DIFFICULTY = 'difficulty' + TAGS = 'tags' + TIME_COMPLEXITY = 'time_complexity' + HINT = 'hint' + CREATED_AT = 'created_at' + def __repr__(self) -> str: tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) return f'[{ProblemDifficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' diff --git a/app/tle/models/problem_tag.py b/app/tle/models/problem_tag.py index 337fd9b..87bcc70 100644 --- a/app/tle/models/problem_tag.py +++ b/app/tle/models/problem_tag.py @@ -2,7 +2,6 @@ class ProblemTag(models.Model): - """Data Structure & Algorithm""" parent = models.ForeignKey( 'self', on_delete=models.CASCADE, @@ -35,6 +34,12 @@ class ProblemTag(models.Model): ), ) + class field_name: + PARENT = 'parent' + KEY = 'key' + NAME_KO = 'name_ko' + NAME_EN = 'name_en' + class Meta: ordering = ['key'] diff --git a/app/tle/models/submission.py b/app/tle/models/submission.py index fdef98e..185f700 100644 --- a/app/tle/models/submission.py +++ b/app/tle/models/submission.py @@ -10,13 +10,13 @@ class Submission(models.Model): activity_problem = models.ForeignKey( CrewActivityProblem, on_delete=models.PROTECT, - related_name=CrewActivityProblem.FieldName.SUBMISSIONS, + related_name=CrewActivityProblem.field_name.SUBMISSIONS, help_text='활동 문제를 입력해주세요.', ) user = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.FieldName.SUBMISSIONS, + related_name=User.field_name.SUBMISSIONS, help_text='유저를 입력해주세요.', ) code = models.TextField( @@ -37,8 +37,21 @@ class Submission(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - class FieldName: + class field_name: + # related fields COMMENTS = 'comments' + # fields + ACTIVITY_PROBLEM = 'activity_problem' + USER = 'user' + CODE = 'code' + LANGUAGE = 'language' + IS_CORRECT = 'is_correct' + IS_HELP_NEEDED = 'is_help_needed' + CREATED_AT = 'created_at' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['created_at'] def __repr__(self) -> str: return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' diff --git a/app/tle/models/submission_comment.py b/app/tle/models/submission_comment.py index c6f2a77..b69e878 100644 --- a/app/tle/models/submission_comment.py +++ b/app/tle/models/submission_comment.py @@ -9,7 +9,7 @@ class SubmissionComment(models.Model): submission = models.ForeignKey( Submission, on_delete=models.CASCADE, - related_name=Submission.FieldName.COMMENTS, + related_name=Submission.field_name.COMMENTS, help_text='제출을 입력해주세요.', ) content = models.TextField( @@ -36,11 +36,24 @@ class SubmissionComment(models.Model): created_by = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.FieldName.COMMENTS, + related_name=User.field_name.COMMENTS, help_text='유저를 입력해주세요.', ) updated_at = models.DateTimeField(auto_now=True) + class field_name: + # fields + SUBMISSION = 'submission' + CONTENT = 'content' + LINE_NUMBER_START = 'line_number_start' + LINE_NUMBER_END = 'line_number_end' + CREATED_AT = 'created_at' + CREATED_BY = 'created_by' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['-created_at'] + def __repr__(self) -> str: line_range = f'L{self.line_number_start}:L{self.line_number_end}' return f'{self.submission.__repr__()} ← {self.created_by.__repr__()} {line_range} "{self.content}"' diff --git a/app/tle/models/submission_language.py b/app/tle/models/submission_language.py index 30c3a6a..86059c7 100644 --- a/app/tle/models/submission_language.py +++ b/app/tle/models/submission_language.py @@ -5,24 +5,26 @@ class SubmissionLanguage(models.Model): key = models.CharField( max_length=20, unique=True, - help_text=( - '언어 키를 입력해주세요. (최대 20자)' - ), + help_text='언어 키를 입력해주세요. (최대 20자)', ) name = models.CharField( max_length=20, unique=True, - help_text=( - '언어 이름을 입력해주세요. (최대 20자)' - ), + help_text='언어 이름을 입력해주세요. (최대 20자)', ) extension = models.CharField( max_length=20, - help_text=( - '언어 확장자를 입력해주세요. (최대 20자)' - ), + help_text='언어 확장자를 입력해주세요. (최대 20자)', ) + class field_name: + KEY = 'key' + NAME = 'name' + EXTENSION = 'extension' + + class Meta: + ordering = ['key'] + def __repr__(self) -> str: return f'[#{self.key}]' diff --git a/app/tle/models/user.py b/app/tle/models/user.py index 15ab5bd..bee48e8 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -11,6 +11,9 @@ from tle.models.choices import BojUserLevel +if typing.TYPE_CHECKING: + import tle.models as t + def get_profile_image_path(instance: User, filename: str) -> str: return f'user/profile/{instance.pk}/{filename}' @@ -44,6 +47,12 @@ def create_superuser(self, email, username, password=None, **extra_fields): class User(AbstractBaseUser, PermissionsMixin): + problems: models.ManyToManyField[t.Problem] + applicants: models.ManyToManyField[t.CrewApplicant] + members: models.ManyToManyField[t.CrewMember] + submissions: models.ManyToManyField[t.Submission] + comments: models.ManyToManyField[t.SubmissionComment] + profile_image = models.ImageField( help_text='프로필 이미지', upload_to=get_profile_image_path, @@ -93,39 +102,41 @@ class User(AbstractBaseUser, PermissionsMixin): objects = UserManager() - @property - def crews(self): - for member in self.members: - yield member.crew - - @property - def date_joined(self): - return self.created_at - USERNAME_FIELD = 'email' REQUIRED_FIELDS = ['username'] - class FieldName: + class field_name: + # related fields PROBLEMS = 'problems' APPLICANTS = 'applicants' MEMBERS = 'members' SUBMISSIONS = 'submissions' COMMENTS = 'comments' + # fields + PROFILE_IMAGE = 'profile_image' + BOJ_USERNAME = 'boj_username' + BOJ_LEVEL = 'boj_level' + BOJ_LEVEL_UPDATED_AT = 'boj_level_updated_at' + USERNAME = 'username' + EMAIL = 'email' + PASSWORD = 'password' + IS_ACTIVE = 'is_active' + IS_STAFF = 'is_staff' + IS_SUPERUSER = 'is_superuser' + FIRST_NAME = 'first_name' + LAST_NAME = 'last_name' + CREATED_AT = 'created_at' + LAST_LOGIN = 'last_login' - if typing.TYPE_CHECKING: - from . import ( - Problem as T_Problem, - Crew as T_Crew, - CrewApplicant as T_CrewApplicant, - CrewMember as T_CrewMember, - Submission as T_Submission, - SubmissionComment as T_SubmissionComment, + def crews(self): + return self.members.values_list( + self.members.model.crew.field.name, + flat=True, ) - problems: models.ManyToManyField[T_Problem] - applicants: models.ManyToManyField[T_CrewApplicant] - members: models.ManyToManyField[T_CrewMember] - submissions: models.ManyToManyField[T_Submission] - comments: models.ManyToManyField[T_SubmissionComment] + + @property + def date_joined(self): + return self.created_at def __repr__(self) -> str: return f'[@{self.username}]' From ff9447b1692f372a816eaa9ace5d3db48d0fe1aa Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 08:12:23 +0900 Subject: [PATCH 277/552] =?UTF-8?q?chore(tle.views):=20add=20TODO=20`/crew?= =?UTF-8?q?s/recruiting'=20=EC=97=90=20=EA=B2=80=EC=83=89=20=EC=98=B5?= =?UTF-8?q?=EC=85=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/views/viewsets/crew_viewset.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py index e6ccc33..5539593 100644 --- a/app/tle/views/viewsets/crew_viewset.py +++ b/app/tle/views/viewsets/crew_viewset.py @@ -20,4 +20,5 @@ def get_serializer_class(self): return CrewRecruitingSerializer def list_recruiting(self, request): + # TODO: 검색 옵션 (사용 언어 / 백준 티어) 제공 return super().list(request) From d343afdcb23ea40366ae4d09a68730110bd0bed8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 10:03:26 +0900 Subject: [PATCH 278/552] refactor(tle.serializers): add ORM type hints, field name mappings --- app/tle/serializers/__init__.py | 4 -- app/tle/serializers/crew_recruiting.py | 6 +- app/tle/serializers/mixins/__init__.py | 4 ++ app/tle/serializers/mixins/analysis_dict.py | 63 +++++++++++++++++++ app/tle/serializers/mixins/difficulty_dict.py | 14 +++++ app/tle/serializers/problem_analysis.py | 37 ----------- app/tle/serializers/problem_detail.py | 45 +++++++------ app/tle/serializers/problem_difficulty.py | 18 ------ app/tle/serializers/problem_minimal.py | 15 +++-- app/tle/serializers/problem_tag.py | 8 +-- app/tle/serializers/user_detail.py | 22 +++---- app/tle/serializers/user_minimal.py | 8 +-- app/tle/serializers/user_sign_in.py | 22 +++---- 13 files changed, 146 insertions(+), 120 deletions(-) create mode 100644 app/tle/serializers/mixins/analysis_dict.py create mode 100644 app/tle/serializers/mixins/difficulty_dict.py delete mode 100644 app/tle/serializers/problem_analysis.py delete mode 100644 app/tle/serializers/problem_difficulty.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index e7ec667..c296a99 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -2,9 +2,7 @@ from tle.serializers.user_minimal import UserMinimalSerializer from tle.serializers.user_sign_in import UserSignInSerializer -from tle.serializers.problem_analysis import ProblemAnalysisSerializer from tle.serializers.problem_detail import ProblemDetailSerializer -from tle.serializers.problem_difficulty import ProblemDifficultySerializer from tle.serializers.problem_minimal import ProblemMinimalSerializer from tle.serializers.problem_tag import ProblemTagSerializer @@ -23,9 +21,7 @@ 'UserSignInSerializer', 'ProblemSerializer', - 'ProblemAnalysisSerializer', 'ProblemDetailSerializer', - 'ProblemDifficultySerializer', 'ProblemMinimalSerializer', 'ProblemTagSerializer', diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index 9466638..657f28c 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -13,9 +13,9 @@ class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin): class Meta: model = Crew fields = [ - 'name', - 'emoji', - 'is_recruiting', + Crew.field_name.NAME, + Crew.field_name.EMOJI, + Crew.field_name.IS_RECRUITING, 'is_joinable', 'is_member', 'members', diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index 59afad2..5e3d66f 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -1,8 +1,12 @@ from tle.serializers.mixins.current_user import CurrentUserMixin from tle.serializers.mixins.boj_profile import BojProfileMixin +from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin +from tle.serializers.mixins.analysis_dict import AnalysisDictMixin __all__ = ( 'CurrentUserMixin', 'BojProfileMixin', + 'DifficultyDictMixin', + 'AnalysisDictMixin', ) diff --git a/app/tle/serializers/mixins/analysis_dict.py b/app/tle/serializers/mixins/analysis_dict.py new file mode 100644 index 0000000..cf63b66 --- /dev/null +++ b/app/tle/serializers/mixins/analysis_dict.py @@ -0,0 +1,63 @@ +import typing + +from rest_framework.serializers import * + +from tle.models import Problem, ProblemAnalysis +from tle.models.choices import ProblemDifficulty +from tle.serializers.problem_tag import ProblemTagSerializer +from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin + + +class AnalysisDictMixin(DifficultyDictMixin): + def analysis_dict(self, problem: Problem) -> typing.Dict: + try: + return self._analysis_dict(problem.analysis) + except ProblemAnalysis.DoesNotExist: + return self._analysis_dict_default() + + def _analysis_dict(self, analysis: ProblemAnalysis): + return { + 'difficulty': { + "name_ko": ProblemDifficulty.get_name(analysis.difficulty, lang='ko'), + "name_en": ProblemDifficulty.get_name(analysis.difficulty, lang='en'), + 'value': analysis.difficulty, + 'description': ( + "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준" + " [이 기능은 추가될 예정이 없습니다]" + ), + }, + 'time_complexity': { + 'value': analysis.time_complexity, + 'description': ( + "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요." + " [이 기능은 추가될 예정이 없습니다]" + ), + }, + 'hint': analysis.hint, + 'tags': ProblemTagSerializer(analysis.tags, many=True).data, + 'is_analyzed': True, + } + + def _analysis_dict_default(self): + default_difficulty = 0 # Under analysis + return { + 'difficulty': { + "name_ko": ProblemDifficulty.get_name(default_difficulty, lang='ko'), + "name_en": ProblemDifficulty.get_name(default_difficulty, lang='en'), + 'value': default_difficulty, + 'description': ( + "AI가 분석을 진행하고 있어요!" + " [이 기능은 추가될 예정이 없습니다]" + ), + }, + 'time_complexity': { + 'value': '', + 'description': ( + "AI가 분석을 진행하고 있어요!" + " [이 기능은 추가될 예정이 없습니다]" + ), + }, + 'hint': [], + 'tags': [], + 'is_analyzed': False, + } diff --git a/app/tle/serializers/mixins/difficulty_dict.py b/app/tle/serializers/mixins/difficulty_dict.py new file mode 100644 index 0000000..5ae3771 --- /dev/null +++ b/app/tle/serializers/mixins/difficulty_dict.py @@ -0,0 +1,14 @@ +import typing + +from rest_framework.serializers import * + +from tle.models.choices import ProblemDifficulty + + +class DifficultyDictMixin: + def difficulty_dict(self, difficulty: ProblemDifficulty) -> typing.Dict: + return { + "name_ko": ProblemDifficulty.get_name(difficulty.value, lang='ko'), + "name_en": ProblemDifficulty.get_name(difficulty.value, lang='en'), + "value": difficulty.value, + } diff --git a/app/tle/serializers/problem_analysis.py b/app/tle/serializers/problem_analysis.py deleted file mode 100644 index d961b16..0000000 --- a/app/tle/serializers/problem_analysis.py +++ /dev/null @@ -1,37 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import ProblemAnalysis -from tle.serializers.problem_tag import ProblemTagSerializer -from tle.serializers.problem_difficulty import ProblemDifficultySerializer - - -class ProblemAnalysisSerializer(ModelSerializer): - tags = ProblemTagSerializer(many=True, read_only=True) - difficulty = ProblemDifficultySerializer(read_only=True) - difficulty_description = SerializerMethodField() - time_complexity_description = SerializerMethodField() - - class Meta: - model = ProblemAnalysis - fields = [ - 'difficulty', - 'difficulty_description', - 'tags', - 'time_complexity', - 'time_complexity_description', - 'hint', - 'created_at', - ] - read_only_fields = ['__all__'] - - def get_difficulty_description(self, obj: ProblemAnalysis): - return ( - "[이 기능은 아직 추가할 예정이 없습니다] " - "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준" - ) - - def get_time_complexity_description(self, obj: ProblemAnalysis): - return ( - "[이 기능은 아직 추가할 예정이 없습니다] " - "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요." - ) diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py index 9edb5a0..b4278a7 100644 --- a/app/tle/serializers/problem_detail.py +++ b/app/tle/serializers/problem_detail.py @@ -1,38 +1,43 @@ from rest_framework.serializers import * from tle.models import Problem -from tle.serializers.problem_analysis import ProblemAnalysisSerializer +from tle.serializers.mixins import AnalysisDictMixin -class ProblemDetailSerializer(ModelSerializer): - analysis= ProblemAnalysisSerializer(read_only=True) - memory_limit_unit= SerializerMethodField() - time_limit_unit= SerializerMethodField() +class ProblemDetailSerializer(ModelSerializer, AnalysisDictMixin): + analysis = SerializerMethodField() + memory_limit_unit = SerializerMethodField() + time_limit_unit = SerializerMethodField() class Meta: - model= Problem - fields= [ + model = Problem + fields = [ 'id', 'analysis', - 'title', - 'link', - 'description', - 'input_description', - 'output_description', - 'memory_limit', + Problem.field_name.TITLE, + Problem.field_name.LINK, + Problem.field_name.DESCRIPTION, + Problem.field_name.INPUT_DESCRIPTION, + Problem.field_name.OUTPUT_DESCRIPTION, + Problem.field_name.MEMORY_LIMIT, 'memory_limit_unit', - 'time_limit', + Problem.field_name.TIME_LIMIT, 'time_limit_unit', - 'created_at', - 'updated_at', + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, ] - extra_kwargs= { + extra_kwargs = { 'id': {'read_only': True}, - 'anaysis': {'read_only': True}, - 'created_at': {'read_only': True}, - 'updated_at': {'read_only': True}, + Problem.field_name.ANALYSIS: {'read_only': True}, + 'memory_limit_unit': {'read_only': True}, + 'time_limit_unit': {'read_only': True}, + Problem.field_name.CREATED_AT: {'read_only': True}, + Problem.field_name.UPDATED_AT: {'read_only': True}, } + def get_analysis(self, obj: Problem): + return self.analysis_dict(obj) + def get_memory_limit_unit(self, obj): return Problem.MEMORY_LIMIT_UNIT diff --git a/app/tle/serializers/problem_difficulty.py b/app/tle/serializers/problem_difficulty.py deleted file mode 100644 index 8675c48..0000000 --- a/app/tle/serializers/problem_difficulty.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.serializers import * - -from tle.models.choices import ProblemDifficulty - - -class ProblemDifficultySerializer(Serializer): - name_en = SerializerMethodField() - name_ko = SerializerMethodField() - value = SerializerMethodField() - - def get_name_ko(self, value: int): - return ProblemDifficulty.get_name(value, 'ko') - - def get_name_en(self, value: int): - return ProblemDifficulty.get_name(value, 'en') - - def get_value(self, value: int): - return value diff --git a/app/tle/serializers/problem_minimal.py b/app/tle/serializers/problem_minimal.py index c75ce97..5b8ea7c 100644 --- a/app/tle/serializers/problem_minimal.py +++ b/app/tle/serializers/problem_minimal.py @@ -1,26 +1,25 @@ from rest_framework.serializers import * from tle.models import Problem, ProblemAnalysis -from tle.serializers.problem_difficulty import ProblemDifficultySerializer +from tle.serializers.mixins import DifficultyDictMixin -class ProblemMinimalSerializer(ModelSerializer): +class ProblemMinimalSerializer(ModelSerializer, DifficultyDictMixin): difficulty = SerializerMethodField() class Meta: model = Problem fields = [ 'id', - 'title', + Problem.field_name.TITLE, 'difficulty', - 'created_at', - 'updated_at', + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, ] read_only_fields = ['__all__'] def get_difficulty(self, obj: Problem): try: - difficulty = obj.analysis.difficulty + return self.difficulty_dict(obj.analysis.difficulty) except ProblemAnalysis.DoesNotExist: - difficulty = 0 - return ProblemDifficultySerializer(difficulty).data + return self.difficulty_dict(0) diff --git a/app/tle/serializers/problem_tag.py b/app/tle/serializers/problem_tag.py index 5054db5..7753407 100644 --- a/app/tle/serializers/problem_tag.py +++ b/app/tle/serializers/problem_tag.py @@ -9,10 +9,10 @@ class ProblemTagSerializer(ModelSerializer): class Meta: model = ProblemTag fields = [ - 'parent', - 'key', - 'name_ko', - 'name_en', + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ProblemTag.field_name.PARENT, ] read_only_fields = ['__all__'] diff --git a/app/tle/serializers/user_detail.py b/app/tle/serializers/user_detail.py index 9b16a43..9bf38a9 100644 --- a/app/tle/serializers/user_detail.py +++ b/app/tle/serializers/user_detail.py @@ -11,22 +11,22 @@ class Meta: model = User fields = [ 'id', - 'email', - 'profile_image', - 'username', - 'password', - 'boj_username', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, + User.field_name.BOJ_USERNAME, 'boj', - 'created_at', - 'last_login', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, ] extra_kwargs = { 'id': {'read_only': True}, 'boj': {'read_only': True}, - 'created_at': {'read_only': True}, - 'last_login': {'read_only': True}, - 'password': {'write_only': True}, - 'boj_username': {'write_only': True}, + User.field_name.CREATED_AT: {'read_only': True}, + User.field_name.LAST_LOGIN: {'read_only': True}, + User.field_name.PASSWORD: {'write_only': True}, + User.field_name.BOJ_USERNAME: {'write_only': True}, } def get_boj(self, obj: User) -> dict: diff --git a/app/tle/serializers/user_minimal.py b/app/tle/serializers/user_minimal.py index 8a8d9ac..134e5e1 100644 --- a/app/tle/serializers/user_minimal.py +++ b/app/tle/serializers/user_minimal.py @@ -8,11 +8,11 @@ class Meta: model = User fields = [ 'id', - 'profile_image', - 'username', + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, ] extra_kwargs = { 'id': {'read_only': True}, - 'profile_image': {'read_only': True}, - 'username': {'read_only': True}, + User.field_name.PROFILE_IMAGE: {'read_only': True}, + User.field_name.USERNAME: {'read_only': True}, } diff --git a/app/tle/serializers/user_sign_in.py b/app/tle/serializers/user_sign_in.py index 2c18da1..bcf2f73 100644 --- a/app/tle/serializers/user_sign_in.py +++ b/app/tle/serializers/user_sign_in.py @@ -13,22 +13,22 @@ class Meta: model = User fields = [ 'id', - 'email', - 'profile_image', - 'username', - 'password', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, 'boj', - 'created_at', - 'last_login', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, ] extra_kwargs = { 'id': {'read_only': True}, - 'profile_image': {'read_only': True}, - 'username': {'read_only': True}, 'boj': {'read_only': True}, - 'created_at': {'read_only': True}, - 'last_login': {'read_only': True}, - 'password': {'write_only': True}, + User.field_name.PROFILE_IMAGE: {'read_only': True}, + User.field_name.USERNAME: {'read_only': True}, + User.field_name.CREATED_AT: {'read_only': True}, + User.field_name.LAST_LOGIN: {'read_only': True}, + User.field_name.PASSWORD: {'write_only': True}, } def get_boj(self, obj: User) -> dict: From 5ec8be0e656be62bee739a59c8375329814eb141 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 20 Jul 2024 10:05:41 +0900 Subject: [PATCH 279/552] =?UTF-8?q?refactor(tle.models):=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84/=EB=A9=94=EB=AA=A8=EB=A6=AC=20=EC=A0=9C=ED=95=9C=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EC=97=90=20=EB=8B=A8=EC=9C=84=EB=A5=BC=20?= =?UTF-8?q?=EB=AA=85=EC=8B=9C=20(=EC=B4=88,=20=EB=A9=94=EA=B0=80=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem.py | 8 ++++---- app/tle/serializers/problem_detail.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 994ccd5..fb09c7b 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -27,10 +27,10 @@ class Problem(models.Model): help_text='문제 출력 설명을 입력해주세요.', blank=True, ) - memory_limit = models.FloatField( + memory_limit_megabyte = models.FloatField( help_text='문제 메모리 제한을 입력해주세요. (MB 단위)', ) - time_limit = models.FloatField( + time_limit_second = models.FloatField( help_text='문제 시간 제한을 입력해주세요. (초 단위)', default=1.0, ) @@ -60,8 +60,8 @@ class field_name: DESCRIPTION = 'description' INPUT_DESCRIPTION = 'input_description' OUTPUT_DESCRIPTION = 'output_description' - MEMORY_LIMIT = 'memory_limit' - TIME_LIMIT = 'time_limit' + MEMORY_LIMIT_MEGABYTE = 'memory_limit_megabyte' + TIME_LIMIT_SECOND = 'time_limit_second' CREATED_AT = 'created_at' CREATED_BY = 'created_by' UPDATED_AT = 'updated_at' diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py index b4278a7..62b0223 100644 --- a/app/tle/serializers/problem_detail.py +++ b/app/tle/serializers/problem_detail.py @@ -19,9 +19,9 @@ class Meta: Problem.field_name.DESCRIPTION, Problem.field_name.INPUT_DESCRIPTION, Problem.field_name.OUTPUT_DESCRIPTION, - Problem.field_name.MEMORY_LIMIT, + Problem.field_name.MEMORY_LIMIT_MEGABYTE, 'memory_limit_unit', - Problem.field_name.TIME_LIMIT, + Problem.field_name.TIME_LIMIT_SECOND, 'time_limit_unit', Problem.field_name.CREATED_AT, Problem.field_name.UPDATED_AT, From efb5ebff707d088c23fb0b920fb52705c616b5c8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 02:08:49 +0900 Subject: [PATCH 280/552] feat(tle.enums): create `Unit` --- app/tle/enums/__init__.py | 7 +++++++ app/tle/enums/unit.py | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 app/tle/enums/unit.py diff --git a/app/tle/enums/__init__.py b/app/tle/enums/__init__.py index b01f75e..edf09c9 100644 --- a/app/tle/enums/__init__.py +++ b/app/tle/enums/__init__.py @@ -1 +1,8 @@ from tle.enums.emoji import Emoji +from tle.enums.unit import Unit + + +__all__ = ( + 'Emoji', + 'Unit', +) diff --git a/app/tle/enums/unit.py b/app/tle/enums/unit.py new file mode 100644 index 0000000..2457e40 --- /dev/null +++ b/app/tle/enums/unit.py @@ -0,0 +1,21 @@ +from typing import NamedTuple + + +_Unit = NamedTuple('_Unit', [ + ('name_ko', str), + ('name_en', str), + ('abbr', str), +]) + + +class Unit: + MEGA_BYTE = _Unit( + name_ko="메가 바이트", + name_en="Mega Bytes", + abbr="MB", + ) + SECOND = _Unit( + name_ko="초", + name_en="Seconds", + abbr="s", + ) From abf488d3b3982b8924b1818d98941bb3bb271802 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 02:09:46 +0900 Subject: [PATCH 281/552] =?UTF-8?q?refactor(tle.serializers):=20`ProblemDe?= =?UTF-8?q?tailSerializer`=EC=9D=98=20`memory`,=20`time=5Flimit`=20?= =?UTF-8?q?=EC=9E=AC=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/problem.py | 12 --------- app/tle/serializers/problem_detail.py | 35 ++++++++++++++++++--------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index fb09c7b..9a34f0e 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -69,18 +69,6 @@ class field_name: class Meta: ordering = ['-created_at'] - MEMORY_LIMIT_UNIT = { - "name_ko": "메가 바이트", - "name_en": "Mega Bytes", - "abbr": "MB", - } - - TIME_LIMIT_UNIT = { - "name_ko": "초", - "name_en": "Seconds", - "abbr": "s", - } - def __repr__(self) -> str: return f'[{self.title}]' diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py index 62b0223..e32b7b8 100644 --- a/app/tle/serializers/problem_detail.py +++ b/app/tle/serializers/problem_detail.py @@ -1,45 +1,58 @@ from rest_framework.serializers import * +from tle.enums import Unit from tle.models import Problem from tle.serializers.mixins import AnalysisDictMixin class ProblemDetailSerializer(ModelSerializer, AnalysisDictMixin): analysis = SerializerMethodField() - memory_limit_unit = SerializerMethodField() - time_limit_unit = SerializerMethodField() + memory_limit = SerializerMethodField() + time_limit = SerializerMethodField() class Meta: model = Problem fields = [ 'id', 'analysis', + 'memory_limit', + 'time_limit', Problem.field_name.TITLE, Problem.field_name.LINK, Problem.field_name.DESCRIPTION, Problem.field_name.INPUT_DESCRIPTION, Problem.field_name.OUTPUT_DESCRIPTION, Problem.field_name.MEMORY_LIMIT_MEGABYTE, - 'memory_limit_unit', Problem.field_name.TIME_LIMIT_SECOND, - 'time_limit_unit', Problem.field_name.CREATED_AT, Problem.field_name.UPDATED_AT, ] extra_kwargs = { 'id': {'read_only': True}, - Problem.field_name.ANALYSIS: {'read_only': True}, - 'memory_limit_unit': {'read_only': True}, - 'time_limit_unit': {'read_only': True}, + 'analysis': {'read_only': True}, + 'memory_limit': {'read_only': True}, + 'time_limit': {'read_only': True}, Problem.field_name.CREATED_AT: {'read_only': True}, Problem.field_name.UPDATED_AT: {'read_only': True}, + Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, + Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, } def get_analysis(self, obj: Problem): return self.analysis_dict(obj) - def get_memory_limit_unit(self, obj): - return Problem.MEMORY_LIMIT_UNIT + def get_memory_limit(self, obj: Problem): + return { + "value": obj.memory_limit_megabyte, + "name_ko": Unit.MEGA_BYTE.name_ko, + "name_en": Unit.MEGA_BYTE.name_en, + "abbr": Unit.MEGA_BYTE.abbr, + } - def get_time_limit_unit(self, obj): - return Problem.TIME_LIMIT_UNIT + def get_time_limit(self, obj: Problem): + return { + "value": obj.time_limit_second, + "name_ko": Unit.SECOND.name_ko, + "name_en": Unit.SECOND.name_en, + "abbr": Unit.SECOND.abbr, + } From 0f9258e3997d88420096a741739230e05cbe615e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 02:47:37 +0900 Subject: [PATCH 282/552] =?UTF-8?q?refactor(tle.serializers):=20`Crew`=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EC=9D=98=20=ED=83=9C=EA=B7=B8=20=EB=A0=8C?= =?UTF-8?q?=EB=8D=94=EB=A7=81=20=EB=B6=80=EB=B6=84=EC=9D=84=20serializer?= =?UTF-8?q?=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EC=9C=84=EC=9E=84.=20?= =?UTF-8?q?=ED=95=B4=EB=8B=B9=20=EA=B3=BC=EC=A0=95=EC=97=90=EC=84=9C=20`Ta?= =?UTF-8?q?gListMixin`=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 46 --------------- app/tle/serializers/crew_recruiting.py | 14 ++--- app/tle/serializers/mixins/__init__.py | 2 + app/tle/serializers/mixins/tag_list.py | 77 ++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 55 deletions(-) create mode 100644 app/tle/serializers/mixins/tag_list.py diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 2db60d1..b5eff90 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -27,20 +27,6 @@ def __call__(self, value) -> None: raise ValidationError(self.message, params={"value": value}) -@dataclasses.dataclass -class CrewTag: - key: typing.Optional[str] - name: str - - @classmethod - def from_language(cls, lang: SubmissionLanguage) -> CrewTag: - return CrewTag(key=lang.key, name=lang.name) - - @classmethod - def from_name(cls, name: str) -> CrewTag: - return CrewTag(key=None, name=name) - - class Crew(models.Model): name = models.CharField( max_length=20, @@ -202,35 +188,3 @@ def is_joinable(self, user: User) -> bool: if user.boj_level > self.max_boj_level: return False return True - - def get_tags(self) -> typing.List[CrewTag]: - return [ - *map(CrewTag.from_language, self.submittable_languages.all()), - *self._build_tier_tags(), - *map(CrewTag.from_name, self.custom_tags), - ] - - def _build_tier_tags(self) -> typing.List[CrewTag]: - tags = [] - if self.min_boj_level is None and self.max_boj_level is None: - tags.append(CrewTag.from_name('티어 무관')) - else: - if self.min_boj_level is not None: - tags.append(self._build_min_tier_tag()) - if self.max_boj_level is not None: - tags.append(self._build_max_tier_tag()) - return tags - - def _build_min_tier_tag(self) -> CrewTag: - if BojUserLevel.get_tier(self.min_boj_level) == 5: - level_name = BojUserLevel.get_rank_name(self.min_boj_level) - else: - level_name = BojUserLevel.get_name(self.min_boj_level) - return CrewTag.from_name(f'{level_name} 이상') - - def _build_max_tier_tag(self) -> CrewTag: - if BojUserLevel.get_tier(self.max_boj_level) == 1: - level_name = BojUserLevel.get_rank_name(self.max_boj_level) - else: - level_name = BojUserLevel.get_name(self.max_boj_level) - return CrewTag.from_name(f'{level_name} 이하') diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index 657f28c..865d48b 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -1,10 +1,10 @@ from rest_framework.serializers import * from tle.models import Crew -from tle.serializers.mixins import CurrentUserMixin +from tle.serializers.mixins import CurrentUserMixin, TagListMixin -class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin): +class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin, TagListMixin): is_joinable = SerializerMethodField() is_member = SerializerMethodField() members = SerializerMethodField() @@ -24,10 +24,10 @@ class Meta: read_only_fields = ['__all__'] def get_is_joinable(self, obj: Crew): - return obj.is_recruiting + return obj.is_joinable(self.current_user()) def get_is_member(self, obj: Crew): - return obj.members.filter(user=self.current_user()).exists() + return obj.is_member(self.current_user()) def get_members(self, obj: Crew): return { @@ -36,8 +36,4 @@ def get_members(self, obj: Crew): } def get_tags(self, obj: Crew): - tags = obj.get_tags() - return { - 'count': len(tags), - 'items': [{'key': tag.key, 'name': tag.name} for tag in tags], - } + return self.tag_list(obj) diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index 5e3d66f..520c2b4 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -2,6 +2,7 @@ from tle.serializers.mixins.boj_profile import BojProfileMixin from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin from tle.serializers.mixins.analysis_dict import AnalysisDictMixin +from tle.serializers.mixins.tag_list import TagListMixin __all__ = ( @@ -9,4 +10,5 @@ 'BojProfileMixin', 'DifficultyDictMixin', 'AnalysisDictMixin', + 'TagListMixin', ) diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py new file mode 100644 index 0000000..4bbbb07 --- /dev/null +++ b/app/tle/serializers/mixins/tag_list.py @@ -0,0 +1,77 @@ +import dataclasses +import typing + +from rest_framework.serializers import * + +from tle.models import Crew +from tle.models.choices import BojUserLevel + + +@dataclasses.dataclass +class TagDict: + key: str + name: str + + +class TagListMixin: + def tag_list(self, crew: Crew) -> typing.Dict: + """크루의 태그들을 key와 name으로 나열하여 반환한다. + + 반환 예시: + + ```python + { + "count": 2, + "items": [ + { "key": "c", "name": "C" }, + { "key": None, "name": "티어 무관" } + ] + } + ``` + """ + # 태그의 나열 순서는 리스트에 선언한 순서를 따름. + tags: typing.List[TagDict] = [ + *self._get_language_tags(crew), + *self._get_boj_level_tags(crew), + *self._get_custom_tags(crew), + ] + return { + 'count': len(tags), + 'items': [dataclasses.asdict(tag) for tag in tags], + } + + def _get_language_tags(self, crew: Crew) -> typing.Iterable[TagDict]: + for lang in crew.submittable_languages.all(): + yield TagDict(key=lang.key, name=lang.name) + + def _get_boj_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: + if crew.min_boj_level is not None: + yield self._get_boj_level_bound_tag(crew.min_boj_level, 5, "이상") + if crew.max_boj_level is not None: + yield self._get_boj_level_bound_tag(crew.max_boj_level, 1, "이하") + if crew.min_boj_level is None and crew.max_boj_level is None: + yield TagDict(key=None, name="티어 무관") + + def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, lang='ko', arabic=False) -> TagDict: + """level에 대한 백준 난이도 태그를 반환한다. + + bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 + 티어(1,2,3,4,5)를 의미한다. + + bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. + + 만약 level의 티어가 bound_tier와 + 같다면 랭크만 출력하고, + 같지않다면 랭크와 티어 모두 출력한다. + + 메시지의 마지막에는 bound_msg를 출력한다. + """ + if BojUserLevel.get_tier(level) == bound_tier: + level_name = BojUserLevel.get_rank_name(level, lang=lang) + else: + level_name = BojUserLevel.get_name(level, lang=lang, arabic=arabic) + return TagDict(key=None, name=f'{level_name} {bound_msg}') + + def _get_custom_tags(self, crew: Crew) -> typing.Iterable[TagDict]: + for tag in crew.custom_tags: + yield TagDict(key=None, name=tag) From 214f85552d28d47c91372fcdd51464b478c9a6dc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 02:48:02 +0900 Subject: [PATCH 283/552] =?UTF-8?q?fix(tle.admin):=20`User`=20=EA=B0=9D?= =?UTF-8?q?=EC=B2=B4=EB=A5=BC=20=EC=88=98=EC=A0=95=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=97=86=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=A0=95=EC=A0=95=20?= =?UTF-8?q?(=ED=95=84=EB=93=9C=EB=AA=85=20=EB=B6=88=EC=9D=BC=EC=B9=98=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/admin/user.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/tle/admin/user.py b/app/tle/admin/user.py index 6822ee2..ff242cd 100644 --- a/app/tle/admin/user.py +++ b/app/tle/admin/user.py @@ -7,11 +7,17 @@ @admin.register(User) class UserModelAdmin(UserAdmin): fieldsets = [ - *UserAdmin.fieldsets, (None, {'fields': [ - 'profile_image', - 'boj_username', - 'boj_tier', - 'boj_tier_updated_at', + User.field_name.EMAIL, + User.field_name.USERNAME, + User.field_name.PASSWORD, + User.field_name.PROFILE_IMAGE, + User.field_name.BOJ_USERNAME, + User.field_name.BOJ_LEVEL, + User.field_name.BOJ_LEVEL_UPDATED_AT, + User.field_name.IS_ACTIVE, + User.field_name.IS_STAFF, + User.field_name.IS_SUPERUSER, + User.field_name.CREATED_AT, ]}), ] From 93ef84c2dd58f04d91f8e2e79759bc4bbf8110eb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 03:29:03 +0900 Subject: [PATCH 284/552] =?UTF-8?q?refactor(tle.models):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20unimport?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index b5eff90..615e2fb 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -1,5 +1,4 @@ from __future__ import annotations -import dataclasses import typing from django.core.exceptions import ValidationError From bc0f7fc9b3e9d200b950a23d3bd2c326777d5192 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 05:54:10 +0900 Subject: [PATCH 285/552] feat(tle): impl `/crew/joined` --- app/tle/models/crew.py | 4 ++ app/tle/models/crew_activity.py | 25 +++++++++- app/tle/models/user.py | 6 --- app/tle/serializers/__init__.py | 2 + app/tle/serializers/crew_joined.py | 65 ++++++++++++++++++++++++++ app/tle/views/urls.py | 1 + app/tle/views/viewsets/crew_viewset.py | 22 ++++++++- 7 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 app/tle/serializers/crew_joined.py diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 615e2fb..245c3c3 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -138,6 +138,10 @@ class field_name: class Meta: ordering = ['-updated_at'] + @classmethod + def of_user(cls, user: User) -> models.QuerySet[Crew]: + return cls.objects.filter(members__user=user) + @property def captain(self) -> t.CrewMember: return self.members.get(is_captain=True) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index 12ea229..838c98f 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -41,10 +41,33 @@ class Meta: ordering = ['start_at'] get_latest_by = ['end_at'] - @property + @classmethod + def opened_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: + """활동 시작 전이거나 종료된 활동을 제외한 활동 목록을 반환합니다.""" + return cls.objects.filter(crew=crew, start_at__lte=timezone.now(), end_at__gte=timezone.now()) + + @classmethod + def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: + """종료된 활동 목록을 반환합니다.""" + return cls.objects.filter(crew=crew, end_at__gt=timezone.now()) + + def is_open(self) -> bool: + """활동이 진행 중인지 여부를 반환합니다.""" + return self.start_at <= timezone.now() <= self.end_at + def is_ended(self) -> bool: + """활동이 종료되었는지 여부를 반환합니다.""" return self.end_at < timezone.now() + def nth(self) -> int: + """활동의 회차 번호를 반환합니다. + + 이 값은 1부터 시작합니다. + 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 + 더한 값을 반환하므로, 고정된 값이 아닙니다. + """ + return self.crew.activities.filter(start_at__lte=self.start_at).count() + def __repr__(self) -> str: return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' diff --git a/app/tle/models/user.py b/app/tle/models/user.py index bee48e8..fa9c802 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -128,12 +128,6 @@ class field_name: CREATED_AT = 'created_at' LAST_LOGIN = 'last_login' - def crews(self): - return self.members.values_list( - self.members.model.crew.field.name, - flat=True, - ) - @property def date_joined(self): return self.created_at diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index c296a99..c5b790d 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -8,6 +8,7 @@ from tle.serializers.crew_member import CrewMemberSerializer from tle.serializers.crew_recruiting import CrewRecruitingSerializer +from tle.serializers.crew_joined import CrewJoinedSerializer UserSerializer = UserDetailSerializer @@ -27,4 +28,5 @@ 'CrewMemberSerializer', 'CrewRecruitingSerializer', + 'CrewJoinedSerializer', ) diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py new file mode 100644 index 0000000..faa0636 --- /dev/null +++ b/app/tle/serializers/crew_joined.py @@ -0,0 +1,65 @@ +from dataclasses import dataclass, asdict +from datetime import date +from typing import Optional + +from rest_framework.serializers import * + +from tle.models import Crew, CrewActivity + + +@dataclass +class ActivityDict: + name: str + nth: Optional[int] = None + in_progress: bool = False + start_at: Optional[date] = None + end_at: Optional[date] = None + + +class CrewJoinedSerializer(ModelSerializer): + activities = SerializerMethodField() + + class Meta: + model = Crew + fields = [ + Crew.field_name.EMOJI, + Crew.field_name.NAME, + 'activities', + Crew.field_name.IS_ACTIVE, + ] + read_only_fields = ['__all__'] + + def get_activities(self, crew: Crew) -> dict: + return { + "count": crew.activities.count(), + "recent": self.get_recent_activity(crew), + } + + def get_recent_activity(self, crew: Crew) -> dict: + if not crew.is_active: + activity_dict = ActivityDict( + name='활동 종료', + ) + elif (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): + activity = opened_activities.earliest() + activity_dict = ActivityDict( + name=activity.name, + nth=activity.nth(), + start_at=activity.start_at.date(), + end_at=activity.end_at.date(), + in_progress=activity.is_open(), + ) + elif (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): + activity = closed_activities.latest() + activity_dict = ActivityDict( + nth=activity.nth(), + name=activity.name, + start_at=activity.start_at.date(), + end_at=activity.end_at.date(), + in_progress=activity.is_open(), + ) + else: + activity_dict = ActivityDict( + name='등록된 활동 없음', + ) + return asdict(activity_dict) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index 06ab421..7e2a6c5 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -35,5 +35,6 @@ ])), path("crews/", include([ path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), + path("joined", CrewViewSet.as_view({"get": "list_joined"})), ])), ] diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py index 5539593..1a26aee 100644 --- a/app/tle/views/viewsets/crew_viewset.py +++ b/app/tle/views/viewsets/crew_viewset.py @@ -1,24 +1,42 @@ -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import ViewSet, ModelViewSet +from rest_framework.permissions import BasePermission +from rest_framework.request import Request from tle.models import Crew from tle.serializers import * from tle.views.permissions import * +class CrewPermission(BasePermission): + def has_permission(self, request: Request, view: ViewSet): + if view.action == 'list_recruiting': + # 모든 사용자에게 공개 + return True + if view.action == 'list_joined': + return request.user.is_authenticated + + class CrewViewSet(ModelViewSet): """문제 태그 목록 조회 + 생성 기능""" - permission_classes = [IsAuthenticated] lookup_field = 'id' + permission_classes = [CrewPermission] def get_queryset(self): if self.action in 'list_recruiting': return Crew.objects.filter(is_recruiting=True) + if self.action in 'list_joined': + return Crew.of_user(self.request.user) return Crew.objects.all() def get_serializer_class(self): if self.action in 'list_recruiting': return CrewRecruitingSerializer + if self.action in 'list_joined': + return CrewJoinedSerializer def list_recruiting(self, request): # TODO: 검색 옵션 (사용 언어 / 백준 티어) 제공 return super().list(request) + + def list_joined(self, request): + return super().list(request) From 59375b14f9b7c311e6cef1674720f9c5307b6e26 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:02:30 +0900 Subject: [PATCH 286/552] refactor(tle.models): refactor type hints --- app/tle/models/crew.py | 19 ++++++++++--------- app/tle/models/crew_activity.py | 12 +++++++----- app/tle/models/crew_activity_problem.py | 11 ++++++----- app/tle/models/problem.py | 13 +++++++------ app/tle/models/problem_tag.py | 9 +++++++++ app/tle/models/submission.py | 8 ++++++++ app/tle/models/user.py | 12 ++++++------ 7 files changed, 53 insertions(+), 31 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 245c3c3..e04dd3b 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -14,6 +14,9 @@ from tle.models.user import User from tle.models.submission_language import SubmissionLanguage +if typing.TYPE_CHECKING: + import tle.models as _T + class EmojiValidator(BaseValidator): def __init__(self, message: str | None = None) -> None: @@ -27,6 +30,12 @@ def __call__(self, value) -> None: class Crew(models.Model): + if typing.TYPE_CHECKING: + applicants: models.ManyToManyField[_T.CrewApplicant] + members: models.ManyToManyField[_T.CrewMember] + activities: models.ManyToManyField[_T.CrewActivity] + submittable_languages: models.ManyToManyField[SubmissionLanguage] + name = models.CharField( max_length=20, unique=True, @@ -106,14 +115,6 @@ class Crew(models.Model): ) updated_at = models.DateTimeField(auto_now=True) - if typing.TYPE_CHECKING: - import tle.models as t - - applicants: models.ManyToManyField[t.CrewApplicant] - members: models.ManyToManyField[t.CrewMember] - activities: models.ManyToManyField[t.CrewActivity] - submittable_languages: models.ManyToManyField[SubmissionLanguage] - class field_name: # related fields APPLICANTS = 'applicants' @@ -143,7 +144,7 @@ def of_user(cls, user: User) -> models.QuerySet[Crew]: return cls.objects.filter(members__user=user) @property - def captain(self) -> t.CrewMember: + def captain(self) -> _T.CrewMember: return self.members.get(is_captain=True) def __repr__(self) -> str: diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index 838c98f..6c17c00 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -1,3 +1,4 @@ +from __future__ import annotations import typing from django.db import models @@ -5,8 +6,14 @@ from tle.models.crew import Crew +if typing.TYPE_CHECKING: + import tle.models as _T + class CrewActivity(models.Model): + if typing.TYPE_CHECKING: + problems: models.ManyToOneRel[_T.CrewActivityProblem] + crew = models.ForeignKey( Crew, on_delete=models.CASCADE, @@ -23,11 +30,6 @@ class CrewActivity(models.Model): help_text='활동 종료 일자를 입력해주세요.', ) - if typing.TYPE_CHECKING: - import tle.models as t - - problems: models.ManyToOneRel[t.CrewActivityProblem] - class field_name: # related fields PROBLEMS = 'problems' diff --git a/app/tle/models/crew_activity_problem.py b/app/tle/models/crew_activity_problem.py index 5ac9b5e..a45babd 100644 --- a/app/tle/models/crew_activity_problem.py +++ b/app/tle/models/crew_activity_problem.py @@ -6,8 +6,14 @@ from tle.models.crew_activity import CrewActivity from tle.models.problem import Problem +if typing.TYPE_CHECKING: + import tle.models as _T + class CrewActivityProblem(models.Model): + if typing.TYPE_CHECKING: + submissions: models.ManyToOneRel[_T.Submission] + activity = models.ForeignKey( CrewActivity, on_delete=models.CASCADE, @@ -28,11 +34,6 @@ class CrewActivityProblem(models.Model): ) created_at = models.DateTimeField(auto_now_add=True) - if typing.TYPE_CHECKING: - import tle.models as t - - submissions: models.ManyToOneRel[t.Submission] - class field_name: # related fields SUBMISSIONS = 'submissions' diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 9a34f0e..4964cca 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -4,8 +4,15 @@ from tle.models.user import User +if typing.TYPE_CHECKING: + import tle.models as _T + class Problem(models.Model): + if typing.TYPE_CHECKING: + analysis: models.OneToOneField[_T.ProblemAnalysis] + activity_problems: models.ManyToOneRel[_T.CrewActivityProblem] + title = models.CharField( max_length=100, help_text='문제 이름을 입력해주세요.', @@ -44,12 +51,6 @@ class Problem(models.Model): ) updated_at = models.DateTimeField(auto_now=True) - if typing.TYPE_CHECKING: - import tle.models as t - - analysis: models.OneToOneField[t.ProblemAnalysis] - activity_problems: models.ManyToOneRel[t.CrewActivityProblem] - class field_name: # related fields ANALYSIS = 'analysis' diff --git a/app/tle/models/problem_tag.py b/app/tle/models/problem_tag.py index 87bcc70..616d70c 100644 --- a/app/tle/models/problem_tag.py +++ b/app/tle/models/problem_tag.py @@ -1,7 +1,16 @@ +import typing + from django.db import models +if typing.TYPE_CHECKING: + import tle.models as _T + class ProblemTag(models.Model): + if typing.TYPE_CHECKING: + parent: models.ManyToManyField[_T.ProblemTag] + children: models.ManyToManyField[_T.ProblemTag] + parent = models.ForeignKey( 'self', on_delete=models.CASCADE, diff --git a/app/tle/models/submission.py b/app/tle/models/submission.py index 185f700..62b5d6b 100644 --- a/app/tle/models/submission.py +++ b/app/tle/models/submission.py @@ -1,11 +1,19 @@ +import typing + from django.db import models from tle.models.user import User from tle.models.crew_activity_problem import CrewActivityProblem from tle.models.submission_language import SubmissionLanguage +if typing.TYPE_CHECKING: + import tle.models as _T + class Submission(models.Model): + if typing.TYPE_CHECKING: + comments: models.ManyToManyField[_T.SubmissionComment] + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 activity_problem = models.ForeignKey( CrewActivityProblem, diff --git a/app/tle/models/user.py b/app/tle/models/user.py index fa9c802..dd1445f 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -12,7 +12,7 @@ from tle.models.choices import BojUserLevel if typing.TYPE_CHECKING: - import tle.models as t + import tle.models as _T def get_profile_image_path(instance: User, filename: str) -> str: @@ -47,11 +47,11 @@ def create_superuser(self, email, username, password=None, **extra_fields): class User(AbstractBaseUser, PermissionsMixin): - problems: models.ManyToManyField[t.Problem] - applicants: models.ManyToManyField[t.CrewApplicant] - members: models.ManyToManyField[t.CrewMember] - submissions: models.ManyToManyField[t.Submission] - comments: models.ManyToManyField[t.SubmissionComment] + problems: models.ManyToManyField[_T.Problem] + applicants: models.ManyToManyField[_T.CrewApplicant] + members: models.ManyToManyField[_T.CrewMember] + submissions: models.ManyToManyField[_T.Submission] + comments: models.ManyToManyField[_T.SubmissionComment] profile_image = models.ImageField( help_text='프로필 이미지', From 13d29e35ea07d6bf5bca314209e055c4f0ff0a76 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:05:03 +0900 Subject: [PATCH 287/552] =?UTF-8?q?refactor(tle.models):=20=EC=86=94?= =?UTF-8?q?=EB=B8=8C=EB=93=9C=EC=9D=98=20=EB=B8=8C=EC=8B=A4=EA=B3=A8..=20?= =?UTF-8?q?=EC=9A=A9=EC=96=B4=EB=A5=BC=20rank=20->=20division=20=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/choices/bol_user_level.py | 10 +++++----- app/tle/serializers/mixins/boj_profile.py | 6 +++--- app/tle/serializers/mixins/tag_list.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/app/tle/models/choices/bol_user_level.py b/app/tle/models/choices/bol_user_level.py index 4c55e0a..f992b57 100644 --- a/app/tle/models/choices/bol_user_level.py +++ b/app/tle/models/choices/bol_user_level.py @@ -1,7 +1,7 @@ from django.db import models -RANK_NAMES = { +DIVISION_NAMES = { 'ko': { 0: '난이도를 매길 수 없음', 1: '브론즈', @@ -66,16 +66,16 @@ class BojUserLevel(models.IntegerChoices): R1 = 30, '루비 1' @classmethod - def get_rank(cls, value: int) -> int: + def get_division(cls, value: int) -> int: if value == 0: return 0 assert 1 <= value <= 30 return ((value-1) // 5)+1 @classmethod - def get_rank_name(cls, value: int, lang='en') -> str: + def get_division_name(cls, value: int, lang='en') -> str: assert 0 <= value <= 30 - return RANK_NAMES[lang][cls.get_rank(value)] + return DIVISION_NAMES[lang][cls.get_division(value)] @classmethod def get_tier(cls, value: int) -> int: @@ -95,4 +95,4 @@ def get_tier_name(cls, value: int, arabic=True) -> str: @classmethod def get_name(cls, value: int, lang='en', arabic=True) -> str: assert 0 <= value <= 30 - return f'{cls.get_rank_name(value, lang=lang)} {cls.get_tier_name(value, arabic=arabic)}' + return f'{cls.get_division_name(value, lang=lang)} {cls.get_tier_name(value, arabic=arabic)}' diff --git a/app/tle/serializers/mixins/boj_profile.py b/app/tle/serializers/mixins/boj_profile.py index 7a4d74d..ff8b005 100644 --- a/app/tle/serializers/mixins/boj_profile.py +++ b/app/tle/serializers/mixins/boj_profile.py @@ -10,9 +10,9 @@ def boj_profile(self: Serializer, user: User) -> dict: 'username': user.boj_username, 'profile_url': f'https://boj.kr/{user.boj_username}', 'level': user.boj_level, - 'rank': BojUserLevel.get_rank(user.boj_level), - 'rank_name_en': BojUserLevel.get_rank_name(user.boj_level, lang='en'), - 'rank_name_ko': BojUserLevel.get_rank_name(user.boj_level, lang='ko'), + 'division': BojUserLevel.get_division(user.boj_level), + 'division_name_en': BojUserLevel.get_division_name(user.boj_level, lang='en'), + 'division_name_ko': BojUserLevel.get_division_name(user.boj_level, lang='ko'), 'tier': BojUserLevel.get_tier(user.boj_level), 'tier_name': BojUserLevel.get_tier_name(user.boj_level, arabic=True), 'tier_updated_at': user.boj_level_updated_at, diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py index 4bbbb07..53d3370 100644 --- a/app/tle/serializers/mixins/tag_list.py +++ b/app/tle/serializers/mixins/tag_list.py @@ -67,7 +67,7 @@ def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, 메시지의 마지막에는 bound_msg를 출력한다. """ if BojUserLevel.get_tier(level) == bound_tier: - level_name = BojUserLevel.get_rank_name(level, lang=lang) + level_name = BojUserLevel.get_division_name(level, lang=lang) else: level_name = BojUserLevel.get_name(level, lang=lang, arabic=arabic) return TagDict(key=None, name=f'{level_name} {bound_msg}') From 90fe757a70b9b6173f80d3748253cdddcfbf788c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:14:14 +0900 Subject: [PATCH 288/552] =?UTF-8?q?feat(app.settings):=20timezone=20?= =?UTF-8?q?=EC=9D=84=20=ED=95=9C=EA=B5=AD=20=EC=84=9C=EC=9A=B8=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EC=9C=BC=EB=A1=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/app/settings.py b/app/app/settings.py index 836ab7c..b675d73 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -118,11 +118,11 @@ LANGUAGE_CODE = "en-us" -TIME_ZONE = "UTC" +TIME_ZONE = 'Asia/Seoul' USE_I18N = True -USE_TZ = True +USE_TZ = False # Static files (CSS, JavaScript, Images) From db91c71add1f17089fdeb950835b3177a652ed56 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:24:30 +0900 Subject: [PATCH 289/552] =?UTF-8?q?fix(tle.models.CrewActivity):=20?= =?UTF-8?q?=EC=9E=98=EB=AA=BB=EB=90=9C=20=EB=82=A0=EC=A7=9C=20=EB=B9=84?= =?UTF-8?q?=EA=B5=90=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew_activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index 6c17c00..d76dc84 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -51,7 +51,7 @@ def opened_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: @classmethod def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: """종료된 활동 목록을 반환합니다.""" - return cls.objects.filter(crew=crew, end_at__gt=timezone.now()) + return cls.objects.filter(crew=crew, end_at__lt=timezone.now()) def is_open(self) -> bool: """활동이 진행 중인지 여부를 반환합니다.""" From 1df867cfaa89128b54ec5839040688b34b9a77ab Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:25:41 +0900 Subject: [PATCH 290/552] =?UTF-8?q?feat(tle.serializers):=20`CrewJoinedSer?= =?UTF-8?q?ializer`=20=EC=97=90=EC=84=9C=20=EB=82=A0=EC=A7=9C=EB=A7=8C=20?= =?UTF-8?q?=EC=9E=88=EB=8A=94=20=ED=98=95=EC=8B=9D=EA=B3=BC=20=EC=A0=84?= =?UTF-8?q?=EC=B2=B4=20=EC=8B=9C=EA=B0=84=20=EB=AA=A8=EB=91=90=EB=A5=BC=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EB=8F=84=EB=A1=9D=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_joined.py | 39 +++++++++++++++--------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index faa0636..826982a 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -12,8 +12,23 @@ class ActivityDict: name: str nth: Optional[int] = None in_progress: bool = False + # TODO: 프론트에게 날짜만 쓸지 시간도 쓸지 물어보기 start_at: Optional[date] = None end_at: Optional[date] = None + date_start_at: Optional[date] = None + date_end_at: Optional[date] = None + + @classmethod + def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': + return cls( + name=activity.name, + nth=activity.nth(), + date_start_at=activity.start_at.date(), + date_end_at=activity.end_at.date(), + start_at=activity.start_at, + end_at=activity.end_at, + in_progress=activity.is_open(), + ) class CrewJoinedSerializer(ModelSerializer): @@ -37,29 +52,13 @@ def get_activities(self, crew: Crew) -> dict: def get_recent_activity(self, crew: Crew) -> dict: if not crew.is_active: - activity_dict = ActivityDict( - name='활동 종료', - ) + activity_dict = ActivityDict(name='활동 종료') elif (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): activity = opened_activities.earliest() - activity_dict = ActivityDict( - name=activity.name, - nth=activity.nth(), - start_at=activity.start_at.date(), - end_at=activity.end_at.date(), - in_progress=activity.is_open(), - ) + activity_dict = ActivityDict.from_activity(activity) elif (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): activity = closed_activities.latest() - activity_dict = ActivityDict( - nth=activity.nth(), - name=activity.name, - start_at=activity.start_at.date(), - end_at=activity.end_at.date(), - in_progress=activity.is_open(), - ) + activity_dict = ActivityDict.from_activity(activity) else: - activity_dict = ActivityDict( - name='등록된 활동 없음', - ) + activity_dict = ActivityDict(name='등록된 활동 없음') return asdict(activity_dict) From fa7b244c536b5c3e8b334698aa9819ab2dd7fce1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:30:28 +0900 Subject: [PATCH 291/552] refactor(tle.models): rename field of `Crew`: `emoji` -> `icon` --- app/tle/models/crew.py | 6 +++--- app/tle/models/crew_member.py | 2 +- app/tle/serializers/crew_joined.py | 2 +- app/tle/serializers/crew_recruiting.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index e04dd3b..1c22e64 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -41,7 +41,7 @@ class Crew(models.Model): unique=True, help_text='크루 이름을 입력해주세요. (최대 20자)', ) - emoji = models.CharField( + icon = models.CharField( max_length=2, validators=[EmojiValidator(message='이모지 형식이 아닙니다.')], null=False, @@ -123,7 +123,7 @@ class field_name: SUBMITTABLE_LANGUAGES = 'submittable_languages' # fields NAME = 'name' - EMOJI = 'emoji' + ICON = 'icon' MAX_MEMBERS = 'max_members' NOTICE = 'notice' CUSTOM_TAGS = 'custom_tags' @@ -148,7 +148,7 @@ def captain(self) -> _T.CrewMember: return self.members.get(is_captain=True) def __repr__(self) -> str: - return f'[{self.emoji} {self.name}]' + return f'[{self.icon} {self.name}]' def __str__(self) -> str: member_count = f'({self.members.count()}/{self.max_members})' diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index daf3bcf..6183528 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -45,7 +45,7 @@ class Meta: ordering = ['is_captain', 'created_at'] def __repr__(self) -> str: - return f'[{self.crew.emoji} {self.crew.name}] ← [@{self.user.username}]' + return f'[{self.crew.icon} {self.crew.name}] ← [@{self.user.username}]' def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index 826982a..f68d0d6 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -37,7 +37,7 @@ class CrewJoinedSerializer(ModelSerializer): class Meta: model = Crew fields = [ - Crew.field_name.EMOJI, + Crew.field_name.ICON, Crew.field_name.NAME, 'activities', Crew.field_name.IS_ACTIVE, diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index 865d48b..d1200a0 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -13,8 +13,8 @@ class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin, TagListMixin): class Meta: model = Crew fields = [ + Crew.field_name.ICON, Crew.field_name.NAME, - Crew.field_name.EMOJI, Crew.field_name.IS_RECRUITING, 'is_joinable', 'is_member', From 0af58d44f5b21f232afd7192be1e5b0fede05505 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:36:07 +0900 Subject: [PATCH 292/552] =?UTF-8?q?refactor(tle.serializers):=20`CrewJoine?= =?UTF-8?q?dSerializer`=EC=9D=98=20=ED=95=84=EB=93=9C=EB=AA=85=20`in=5Fpro?= =?UTF-8?q?gress`=20=EC=9D=B4=20=EB=AA=A8=ED=98=B8=ED=95=9C=20=EA=B2=83=20?= =?UTF-8?q?=EA=B0=99=EC=95=84=20`is=5Fopen`=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_joined.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index f68d0d6..7d16fd0 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -11,7 +11,7 @@ class ActivityDict: name: str nth: Optional[int] = None - in_progress: bool = False + is_open: bool = False # 제출 가능 여부 # TODO: 프론트에게 날짜만 쓸지 시간도 쓸지 물어보기 start_at: Optional[date] = None end_at: Optional[date] = None @@ -20,14 +20,14 @@ class ActivityDict: @classmethod def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': - return cls( + return ActivityDict( name=activity.name, nth=activity.nth(), + is_open=activity.is_open(), date_start_at=activity.start_at.date(), date_end_at=activity.end_at.date(), start_at=activity.start_at, end_at=activity.end_at, - in_progress=activity.is_open(), ) From a16a59c42219925bde665c80beed21235959c045 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 06:44:28 +0900 Subject: [PATCH 293/552] =?UTF-8?q?feat(tle.models):=20=ED=81=AC=EB=A3=A8?= =?UTF-8?q?=EC=97=90=20=EB=B0=B1=EC=A4=80=20=EC=95=84=EC=9D=B4=EB=94=94?= =?UTF-8?q?=EB=8A=94=20=EA=B8=B0=EB=B3=B8=EC=A0=81=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 1c22e64..42236ad 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -79,7 +79,7 @@ class Crew(models.Model): ) is_boj_username_required = models.BooleanField( help_text='백준 아이디 필요 여부를 입력해주세요.', - default=False, + default=True, ) min_boj_level = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', From 4b7bcfa5362b8013e12fe2ce65ac8ba94b944c37 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 07:35:57 +0900 Subject: [PATCH 294/552] =?UTF-8?q?refactor(tle.models):=20`CrewMember.is?= =?UTF-8?q?=5Fcaptain`=20=ED=95=84=EB=93=9C=EB=A5=BC=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=98=EA=B3=A0,=20`Crew.created=5Fby`=EB=A5=BC=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EA=B8=B0=EB=8A=A5=EC=9C=BC=EB=A1=9C=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 6 +---- app/tle/models/crew_member.py | 39 ++++++------------------------ app/tle/serializers/crew_member.py | 2 +- 3 files changed, 9 insertions(+), 38 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 42236ad..63c74c0 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -143,16 +143,12 @@ class Meta: def of_user(cls, user: User) -> models.QuerySet[Crew]: return cls.objects.filter(members__user=user) - @property - def captain(self) -> _T.CrewMember: - return self.members.get(is_captain=True) - def __repr__(self) -> str: return f'[{self.icon} {self.name}]' def __str__(self) -> str: member_count = f'({self.members.count()}/{self.max_members})' - return f'{self.pk} : {self.__repr__()} {member_count} ← {self.captain.__repr__()}' + return f'{self.pk} : {repr(self)} {member_count} ← {repr(self.created_by)}' def save(self, *args, **kwargs) -> None: with transaction.atomic(): diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index 6183528..3696c91 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -19,30 +19,25 @@ class CrewMember(models.Model): related_name=User.field_name.MEMBERS, help_text='유저를 입력해주세요.', ) - is_captain = models.BooleanField( - default=False, - help_text='선장인지 여부를 입력해주세요.', - ) created_at = models.DateTimeField(auto_now_add=True) class field_name: CREW = 'crew' USER = 'user' - IS_CAPTAIN = 'is_captain' CREATED_AT = 'created_at' class Meta: constraints = [ - models.UniqueConstraint( - fields=['crew', 'is_captain'], - name='unique_captain_per_crew' - ), models.UniqueConstraint( fields=['crew', 'user'], name='unique_member_per_crew' ), ] - ordering = ['is_captain', 'created_at'] + ordering = ['created_at'] + + @classmethod + def captain_of(cls, crew: Crew) -> CrewMember: + return cls.objects.get(crew=crew, user=crew.created_by) def __repr__(self) -> str: return f'[{self.crew.icon} {self.crew.name}] ← [@{self.user.username}]' @@ -50,25 +45,5 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' - def make_captain(self, commit=True) -> CrewMember: - """전 선장을 직위해제하고, 이 멤버를 새로운 선장으로 임명합니다. - - 전 선장에 대한 엔티티를 반환합니다. - - TODO: 크루장이 탈퇴할 경우 새로운 크루장은 어떻게 선발할 지 검토 - TODO: 크루장이 여러 명일 경우 어떻게 처리할 지 검토 (예외 처리) - """ - def inner(): - former_captain = self.crew.members.get(is_captain=True) - former_captain.is_captain = False - self.is_captain = True - return former_captain - - if not commit: - return inner() - - with transaction.atomic(): - former_captain = inner() - former_captain.save() - self.save() - return former_captain + def is_captain(self) -> bool: + return self.crew.created_by == self.user diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py index e023958..7720bd8 100644 --- a/app/tle/serializers/crew_member.py +++ b/app/tle/serializers/crew_member.py @@ -16,4 +16,4 @@ class Meta: ) def get_is_captain(self, obj: CrewMember) -> bool: - return obj.crew.captain == obj.user + return obj.is_captain() From 6eebb2af38b02958cfb93fee92e96d27b5f96b89 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 08:06:57 +0900 Subject: [PATCH 295/552] refactor(tle.models): rename `is_open()` -> `is_opened()` of `CrewActivity` --- app/tle/models/crew_activity.py | 2 +- app/tle/serializers/crew_joined.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index d76dc84..d95f4ae 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -53,7 +53,7 @@ def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: """종료된 활동 목록을 반환합니다.""" return cls.objects.filter(crew=crew, end_at__lt=timezone.now()) - def is_open(self) -> bool: + def is_opened(self) -> bool: """활동이 진행 중인지 여부를 반환합니다.""" return self.start_at <= timezone.now() <= self.end_at diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index 7d16fd0..004bd28 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -23,7 +23,7 @@ def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': return ActivityDict( name=activity.name, nth=activity.nth(), - is_open=activity.is_open(), + is_open=activity.is_opened(), date_start_at=activity.start_at.date(), date_end_at=activity.end_at.date(), start_at=activity.start_at, From dc5e0024b4ac46970caddf797e371781b636327d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 08:11:30 +0900 Subject: [PATCH 296/552] refactor(tle.models): rename `is_ended()` -> `is_closed()` of `CrewActivity` --- app/tle/models/crew_activity.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index d95f4ae..9b0b778 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -57,7 +57,7 @@ def is_opened(self) -> bool: """활동이 진행 중인지 여부를 반환합니다.""" return self.start_at <= timezone.now() <= self.end_at - def is_ended(self) -> bool: + def is_closed(self) -> bool: """활동이 종료되었는지 여부를 반환합니다.""" return self.end_at < timezone.now() From bb8926ace703a6178d91c0b219e8e59080c2e6ce Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 09:18:20 +0900 Subject: [PATCH 297/552] feat(tle.models): add `Crew.of_user_as_captain(User)` class method --- app/tle/models/crew.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 63c74c0..53c91f8 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -143,8 +143,9 @@ class Meta: def of_user(cls, user: User) -> models.QuerySet[Crew]: return cls.objects.filter(members__user=user) - def __repr__(self) -> str: - return f'[{self.icon} {self.name}]' + @classmethod + def of_user_as_captain(cls, user: User) -> models.QuerySet[Crew]: + return cls.objects.filter(created_by=user) def __str__(self) -> str: member_count = f'({self.members.count()}/{self.max_members})' From 7ceb32229e85be6eaf150d3f944b9c3fa837e954 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 09:18:50 +0900 Subject: [PATCH 298/552] feat(tle.admin): enhance admin table views --- app/tle/admin/__init__.py | 202 ++++++++++++++++++++++++--- app/tle/admin/problem.py | 28 ---- app/tle/admin/problem_tag.py | 10 -- app/tle/admin/submission_language.py | 10 -- app/tle/admin/user.py | 23 --- app/tle/models/crew.py | 3 +- app/tle/models/crew_activity.py | 10 +- app/tle/models/crew_member.py | 10 +- app/tle/models/problem.py | 5 +- app/tle/models/problem_analysis.py | 7 - app/tle/models/user.py | 6 +- 11 files changed, 196 insertions(+), 118 deletions(-) delete mode 100644 app/tle/admin/problem.py delete mode 100644 app/tle/admin/problem_tag.py delete mode 100644 app/tle/admin/submission_language.py delete mode 100644 app/tle/admin/user.py diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index 4bab389..f575973 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -1,26 +1,196 @@ -from django.contrib import admin +from textwrap import shorten -from tle.admin.user import UserModelAdmin -from tle.admin.problem import ProblemModelAdmin -from tle.admin.problem_tag import ProblemTagModelAdmin -from tle.admin.submission_language import SubmissionLanguageModelAdmin -from tle.models import * +from django.contrib import admin, messages +from django.contrib.auth.admin import UserAdmin +from django.db.models import QuerySet +from django.utils.translation import ngettext +from tle.models import * -__all__ = ( - 'UserModelAdmin', - 'ProblemModelAdmin', - 'ProblemTagModelAdmin', - 'SubmissionLanguageModelAdmin', -) admin.site.register([ - Crew, - CrewActivity, CrewActivityProblem, CrewApplicant, - CrewMember, - ProblemAnalysis, Submission, SubmissionComment, ]) + + +@admin.register(User) +class UserModelAdmin(UserAdmin): + list_display = [ + User.field_name.USERNAME, + User.field_name.EMAIL, + User.field_name.BOJ_USERNAME, + User.field_name.BOJ_LEVEL, + 'get_crews', + User.field_name.IS_ACTIVE, + User.field_name.IS_STAFF, + User.field_name.IS_SUPERUSER, + User.field_name.CREATED_AT, + User.field_name.BOJ_LEVEL_UPDATED_AT, + ] + + @admin.display(description='captains / members') + def get_crews(self, user: User) -> str: + return f'{Crew.of_user_as_captain(user).count()} / {Crew.of_user(user).count()}' + + +@admin.register(Problem) +class ProblemModelAdmin(admin.ModelAdmin): + list_display = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY, + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, + ] + search_fields = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + ] + ordering = ['-'+Problem.field_name.CREATED_AT] + actions = ['set_creator'] + + @admin.action(description="set admin(you) as creator for selected problems") + def set_creator(self, request, queryset: QuerySet[Problem]): + updated = queryset.update(created_by=request.user) + self.message_user( + request, + ngettext( + "%d problem was successfully updated.", + "%d problems were successfully updated.", + updated, + ) + % updated, + messages.SUCCESS, + ) + + +@admin.register(ProblemAnalysis) +class ProblemAnalysisModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysis.field_name.PROBLEM, + ProblemAnalysis.field_name.DIFFICULTY, + 'get_timecomplexity', + 'get_tags', + 'get_hint', + ProblemAnalysis.field_name.CREATED_AT, + ] + search_fields = [ + ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ProblemAnalysis.field_name.TIME_COMPLEXITY, + ] + ordering = ['-'+ProblemAnalysis.field_name.CREATED_AT] + + @admin.display(description='Big-O') + def get_timecomplexity(self, obj: ProblemAnalysis) -> str: + return f'O({obj.time_complexity})' + + @admin.display(description='Tags') + def get_tags(self, obj: ProblemAnalysis) -> str: + def get_tag_keys(): + for tag in obj.tags.all(): + yield f'#{tag.key}' + return ' '.join(get_tag_keys()) + + @admin.display(description='Hint (Steps, Verbose)') + def get_hint(self, obj: ProblemAnalysis) -> str: + return len(obj.hint), shorten(', '.join(obj.hint), width=32) + + +@admin.register(ProblemTag) +class ProblemTagModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ProblemTag.field_name.PARENT, + ] + search_fields = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ] + ordering = [ProblemTag.field_name.KEY] + + +@admin.register(Crew) +class CrewModelAdmin(admin.ModelAdmin): + list_display = [ + 'get_display_name', + 'get_members', + 'get_applicants', + 'get_activities', + Crew.field_name.IS_ACTIVE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.CREATED_BY, + Crew.field_name.CREATED_AT, + Crew.field_name.UPDATED_AT, + ] + search_fields = [ + Crew.field_name.NAME, + Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + Crew.field_name.ICON, + ] + + @admin.display(description='Display Name') + def get_display_name(self, obj: Crew) -> str: + return f'{obj.icon} {obj.name}' + + @admin.display(description='Activities') + def get_activities(self, obj: Crew) -> str: + return obj.activities.count() + + @admin.display(description='Members') + def get_members(self, obj: Crew) -> str: + return f'{obj.members.count()} / {obj.max_members}' + + @admin.display(description='Applicants') + def get_applicants(self, obj: Crew) -> str: + return obj.applicants.count() + + +@admin.register(CrewMember) +class CrewMemberModelAdmin(admin.ModelAdmin): + list_display = [ + CrewMember.field_name.USER, + 'is_captain', + CrewMember.field_name.CREW, + CrewMember.field_name.CREATED_AT, + ] + search_fields = [ + CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, + CrewMember.field_name.USER+'__'+User.field_name.USERNAME, + ] + ordering = [CrewMember.field_name.CREW] + + +@admin.register(CrewActivity) +class CrewActivityModelAdmin(admin.ModelAdmin): + list_display = [ + CrewActivity.field_name.CREW, + CrewActivity.field_name.NAME, + CrewActivity.field_name.START_AT, + CrewActivity.field_name.END_AT, + 'nth', + 'is_opened', + 'is_closed', + ] + search_fields = [ + CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, + CrewActivity.field_name.NAME, + ] + + +@admin.register(SubmissionLanguage) +class SubmissionLanguageModelAdmin(admin.ModelAdmin): + list_display = [ + SubmissionLanguage.field_name.KEY, + SubmissionLanguage.field_name.NAME, + SubmissionLanguage.field_name.EXTENSION, + ] + search_fields = [ + SubmissionLanguage.field_name.KEY, + SubmissionLanguage.field_name.NAME, + SubmissionLanguage.field_name.EXTENSION, + ] diff --git a/app/tle/admin/problem.py b/app/tle/admin/problem.py deleted file mode 100644 index e8b9218..0000000 --- a/app/tle/admin/problem.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.contrib import admin, messages -from django.utils.translation import ngettext - -from tle.models import Problem - - -@admin.register(Problem) -class ProblemModelAdmin(admin.ModelAdmin): - list_display = ['title', 'created_by', 'created_at', 'updated_at'] - list_filter = ['created_by', 'created_at', 'updated_at'] - search_fields = ['title', 'created_by__username'] - ordering = ['-created_at'] - actions = ['set_creator'] - - @admin.action(description="set 'created_by' of selected problems to current user") - def set_creator(self, request, queryset): - user = request.user - updated = queryset.update(created_by=user) - self.message_user( - request, - ngettext( - "%d story was successfully marked as published.", - "%d stories were successfully marked as published.", - updated, - ) - % updated, - messages.SUCCESS, - ) diff --git a/app/tle/admin/problem_tag.py b/app/tle/admin/problem_tag.py deleted file mode 100644 index e1ae6bf..0000000 --- a/app/tle/admin/problem_tag.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from tle.models import ProblemTag - - -@admin.register(ProblemTag) -class ProblemTagModelAdmin(admin.ModelAdmin): - list_display = ['parent', 'key', 'name_ko', 'name_en'] - search_fields = ['parent', 'key', 'name_ko', 'name_en'] - ordering = ['key'] diff --git a/app/tle/admin/submission_language.py b/app/tle/admin/submission_language.py deleted file mode 100644 index bc539f7..0000000 --- a/app/tle/admin/submission_language.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.contrib import admin - -from tle.models import SubmissionLanguage - - -@admin.register(SubmissionLanguage) -class SubmissionLanguageModelAdmin(admin.ModelAdmin): - list_display = ['key', 'name', 'extension'] - search_fields = ['key', 'name', 'extension'] - ordering = ['key'] diff --git a/app/tle/admin/user.py b/app/tle/admin/user.py deleted file mode 100644 index ff242cd..0000000 --- a/app/tle/admin/user.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.contrib import admin -from django.contrib.auth.admin import UserAdmin - -from tle.models import User - - -@admin.register(User) -class UserModelAdmin(UserAdmin): - fieldsets = [ - (None, {'fields': [ - User.field_name.EMAIL, - User.field_name.USERNAME, - User.field_name.PASSWORD, - User.field_name.PROFILE_IMAGE, - User.field_name.BOJ_USERNAME, - User.field_name.BOJ_LEVEL, - User.field_name.BOJ_LEVEL_UPDATED_AT, - User.field_name.IS_ACTIVE, - User.field_name.IS_STAFF, - User.field_name.IS_SUPERUSER, - User.field_name.CREATED_AT, - ]}), - ] diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 53c91f8..09aae88 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -148,8 +148,7 @@ def of_user_as_captain(cls, user: User) -> models.QuerySet[Crew]: return cls.objects.filter(created_by=user) def __str__(self) -> str: - member_count = f'({self.members.count()}/{self.max_members})' - return f'{self.pk} : {repr(self)} {member_count} ← {repr(self.created_by)}' + return f'[{self.pk} : {self.icon} "{self.name}"] ({self.members.count()}/{self.max_members})' def save(self, *args, **kwargs) -> None: with transaction.atomic(): diff --git a/app/tle/models/crew_activity.py b/app/tle/models/crew_activity.py index 9b0b778..5b4a569 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/crew_activity.py @@ -1,6 +1,7 @@ from __future__ import annotations import typing +from django.contrib import admin from django.db import models from django.utils import timezone @@ -53,14 +54,17 @@ def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: """종료된 활동 목록을 반환합니다.""" return cls.objects.filter(crew=crew, end_at__lt=timezone.now()) + @admin.display(boolean=True, description='Is Opend') def is_opened(self) -> bool: """활동이 진행 중인지 여부를 반환합니다.""" return self.start_at <= timezone.now() <= self.end_at + @admin.display(boolean=True, description='Is Closed') def is_closed(self) -> bool: """활동이 종료되었는지 여부를 반환합니다.""" return self.end_at < timezone.now() + @admin.display(description='Nth') def nth(self) -> int: """활동의 회차 번호를 반환합니다. @@ -69,9 +73,3 @@ def nth(self) -> int: 더한 값을 반환하므로, 고정된 값이 아닙니다. """ return self.crew.activities.filter(start_at__lte=self.start_at).count() - - def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← [{self.start_at.date()} ~ {self.end_at.date()}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index 3696c91..e85835e 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -1,6 +1,7 @@ from __future__ import annotations -from django.db import models, transaction +from django.contrib import admin +from django.db import models from tle.models.user import User from tle.models.crew import Crew @@ -39,11 +40,6 @@ class Meta: def captain_of(cls, crew: Crew) -> CrewMember: return cls.objects.get(crew=crew, user=crew.created_by) - def __repr__(self) -> str: - return f'[{self.crew.icon} {self.crew.name}] ← [@{self.user.username}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' - + @admin.display(boolean=True, description='Is Captain') def is_captain(self) -> bool: return self.crew.created_by == self.user diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 4964cca..3348bca 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -70,8 +70,5 @@ class field_name: class Meta: ordering = ['-created_at'] - def __repr__(self) -> str: - return f'[{self.title}]' - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ← {self.created_by.__repr__()}' + return f'[{self.pk} : {self.title}]' diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py index 497a83f..61dbcfe 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/problem_analysis.py @@ -47,10 +47,3 @@ class field_name: TIME_COMPLEXITY = 'time_complexity' HINT = 'hint' CREATED_AT = 'created_at' - - def __repr__(self) -> str: - tags = ' '.join(f'#{tag.key}' for tag in self.tags.all()) - return f'[{ProblemDifficulty(self.difficulty).label} / {self.time_complexity} / {tags}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.problem.__repr__()} ← {self.__repr__()}' diff --git a/app/tle/models/user.py b/app/tle/models/user.py index dd1445f..f4d08e9 100644 --- a/app/tle/models/user.py +++ b/app/tle/models/user.py @@ -132,12 +132,8 @@ class field_name: def date_joined(self): return self.created_at - def __repr__(self) -> str: - return f'[@{self.username}]' - def __str__(self) -> str: - staff = '(관리자)' if self.is_staff else '' - return f'{self.pk} : {self.__repr__()} {staff}' + return f'[{self.pk} : "{self.username}"]' + (' (관리자)' if self.is_staff else '') def has_perm(self, perm, obj=None): return True From f37931658d0dc5bd8bb1f2ce329349c50321ff7d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 09:26:14 +0900 Subject: [PATCH 299/552] =?UTF-8?q?refactor(tle.serializers):=20`CrewJoine?= =?UTF-8?q?dSerializer`=20=EC=97=90=EC=84=9C=20=ED=99=9C=EB=8F=99=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EC=9D=80=20datetime=20=EC=9C=BC=EB=A1=9C=20=EC=A3=BC?= =?UTF-8?q?=EB=8A=94=EA=B2=83=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20(da?= =?UTF-8?q?te=EB=A7=8C=20=EC=A3=BC=EB=8A=94=20=EA=B2=83=EC=9D=B4=20?= =?UTF-8?q?=EC=95=84=EB=8B=88=EB=8B=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_joined.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index 004bd28..7e37272 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -9,14 +9,11 @@ @dataclass class ActivityDict: - name: str nth: Optional[int] = None - is_open: bool = False # 제출 가능 여부 - # TODO: 프론트에게 날짜만 쓸지 시간도 쓸지 물어보기 + name: str = '' start_at: Optional[date] = None end_at: Optional[date] = None - date_start_at: Optional[date] = None - date_end_at: Optional[date] = None + is_open: bool = False # 제출 가능 여부 @classmethod def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': @@ -24,8 +21,6 @@ def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': name=activity.name, nth=activity.nth(), is_open=activity.is_opened(), - date_start_at=activity.start_at.date(), - date_end_at=activity.end_at.date(), start_at=activity.start_at, end_at=activity.end_at, ) From dafe0be2faa148102a1b02626a5011530b903655 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 09:34:36 +0900 Subject: [PATCH 300/552] =?UTF-8?q?refactor(tle.serializers):=20problem,?= =?UTF-8?q?=20crew=5Fmember=20=EC=B6=9C=EB=A0=A5=ED=98=95=EC=8B=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_member.py | 3 ++- app/tle/serializers/problem_detail.py | 16 ++++++++++------ app/tle/serializers/problem_tag.py | 8 -------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py index 7720bd8..d07acc7 100644 --- a/app/tle/serializers/crew_member.py +++ b/app/tle/serializers/crew_member.py @@ -11,8 +11,9 @@ class CrewMemberSerializer(ModelSerializer): class Meta: model = CrewMember fields = ( - 'user', + CrewMember.field_name.USER, 'is_captain', + CrewMember.field_name.CREATED_AT, ) def get_is_captain(self, obj: CrewMember) -> bool: diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py index e32b7b8..de2383c 100644 --- a/app/tle/serializers/problem_detail.py +++ b/app/tle/serializers/problem_detail.py @@ -44,15 +44,19 @@ def get_analysis(self, obj: Problem): def get_memory_limit(self, obj: Problem): return { "value": obj.memory_limit_megabyte, - "name_ko": Unit.MEGA_BYTE.name_ko, - "name_en": Unit.MEGA_BYTE.name_en, - "abbr": Unit.MEGA_BYTE.abbr, + "unit": { + "name_ko": Unit.MEGA_BYTE.name_ko, + "name_en": Unit.MEGA_BYTE.name_en, + "abbr": Unit.MEGA_BYTE.abbr, + }, } def get_time_limit(self, obj: Problem): return { "value": obj.time_limit_second, - "name_ko": Unit.SECOND.name_ko, - "name_en": Unit.SECOND.name_en, - "abbr": Unit.SECOND.abbr, + "unit": { + "name_ko": Unit.SECOND.name_ko, + "name_en": Unit.SECOND.name_en, + "abbr": Unit.SECOND.abbr, + }, } diff --git a/app/tle/serializers/problem_tag.py b/app/tle/serializers/problem_tag.py index 7753407..99a0976 100644 --- a/app/tle/serializers/problem_tag.py +++ b/app/tle/serializers/problem_tag.py @@ -4,19 +4,11 @@ class ProblemTagSerializer(ModelSerializer): - parent = SerializerMethodField() - class Meta: model = ProblemTag fields = [ ProblemTag.field_name.KEY, ProblemTag.field_name.NAME_KO, ProblemTag.field_name.NAME_EN, - ProblemTag.field_name.PARENT, ] read_only_fields = ['__all__'] - - def get_parent(self, obj: ProblemTag): - if obj.parent is None: - return None - return ProblemTagSerializer(obj.parent).data From 959d610502d2ce7501d5624f29e7eaa5380a44c9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 09:37:03 +0900 Subject: [PATCH 301/552] fix(tle.models): fix `Crew.is_joinable(User)` --- app/tle/models/crew.py | 5 ++++- app/tle/models/crew_member.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index 09aae88..e345eec 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -164,10 +164,13 @@ def save(self, *args, **kwargs) -> None: def is_member(self, user: User) -> bool: return self.members.filter(user=user).exists() + def is_captain(self, user: User) -> bool: + return self.created_by == user + def is_joinable(self, user: User) -> bool: if not self.is_recruiting: return False - if self.captain == user: + if self.is_captain(user): return False if self.members.count() >= self.max_members: return False diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index e85835e..1b2bf19 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -42,4 +42,4 @@ def captain_of(cls, crew: Crew) -> CrewMember: @admin.display(boolean=True, description='Is Captain') def is_captain(self) -> bool: - return self.crew.created_by == self.user + return self.crew.is_captain(self.user) From 0532134d3ead1e1cda66035091f98426ff1cdd95 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 21 Jul 2024 14:27:47 +0900 Subject: [PATCH 302/552] =?UTF-8?q?feat(tle.serializers):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EA=B0=80=EC=9E=85=EC=8B=A0=EC=B2=AD=EC=97=90=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=ED=95=98=EA=B8=B0=20=EC=9C=84=ED=95=9C=20?= =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91=ED=9A=8C=EC=B0=A8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20`CrewJoinedSerializer`=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_joined.py | 43 ++----------------- app/tle/serializers/crew_recruiting.py | 19 +++++++- app/tle/serializers/mixins/__init__.py | 2 + app/tle/serializers/mixins/recent_activity.py | 41 ++++++++++++++++++ 4 files changed, 64 insertions(+), 41 deletions(-) create mode 100644 app/tle/serializers/mixins/recent_activity.py diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py index 7e37272..7b4dd26 100644 --- a/app/tle/serializers/crew_joined.py +++ b/app/tle/serializers/crew_joined.py @@ -1,32 +1,10 @@ -from dataclasses import dataclass, asdict -from datetime import date -from typing import Optional - from rest_framework.serializers import * -from tle.models import Crew, CrewActivity - - -@dataclass -class ActivityDict: - nth: Optional[int] = None - name: str = '' - start_at: Optional[date] = None - end_at: Optional[date] = None - is_open: bool = False # 제출 가능 여부 +from tle.models import Crew +from tle.serializers.mixins import RecentActivityMixin - @classmethod - def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': - return ActivityDict( - name=activity.name, - nth=activity.nth(), - is_open=activity.is_opened(), - start_at=activity.start_at, - end_at=activity.end_at, - ) - -class CrewJoinedSerializer(ModelSerializer): +class CrewJoinedSerializer(ModelSerializer, RecentActivityMixin): activities = SerializerMethodField() class Meta: @@ -42,18 +20,5 @@ class Meta: def get_activities(self, crew: Crew) -> dict: return { "count": crew.activities.count(), - "recent": self.get_recent_activity(crew), + "recent": self.recent_activity(crew), } - - def get_recent_activity(self, crew: Crew) -> dict: - if not crew.is_active: - activity_dict = ActivityDict(name='활동 종료') - elif (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): - activity = opened_activities.earliest() - activity_dict = ActivityDict.from_activity(activity) - elif (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): - activity = closed_activities.latest() - activity_dict = ActivityDict.from_activity(activity) - else: - activity_dict = ActivityDict(name='등록된 활동 없음') - return asdict(activity_dict) diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index d1200a0..a4799a1 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -1,12 +1,20 @@ from rest_framework.serializers import * from tle.models import Crew -from tle.serializers.mixins import CurrentUserMixin, TagListMixin +from tle.serializers.mixins import ( + CurrentUserMixin, + TagListMixin, + RecentActivityMixin, +) -class CrewRecruitingSerializer(ModelSerializer, CurrentUserMixin, TagListMixin): +class CrewRecruitingSerializer(CurrentUserMixin, + TagListMixin, + RecentActivityMixin, + ModelSerializer): is_joinable = SerializerMethodField() is_member = SerializerMethodField() + activities = SerializerMethodField() members = SerializerMethodField() tags = SerializerMethodField() @@ -18,6 +26,7 @@ class Meta: Crew.field_name.IS_RECRUITING, 'is_joinable', 'is_member', + 'activities', 'members', 'tags', ] @@ -29,6 +38,12 @@ def get_is_joinable(self, obj: Crew): def get_is_member(self, obj: Crew): return obj.is_member(self.current_user()) + def get_activities(self, crew: Crew) -> dict: + return { + "count": crew.activities.count(), + "recent": self.recent_activity(crew), + } + def get_members(self, obj: Crew): return { 'count': obj.members.count(), diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index 520c2b4..755fa82 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -3,6 +3,7 @@ from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin from tle.serializers.mixins.analysis_dict import AnalysisDictMixin from tle.serializers.mixins.tag_list import TagListMixin +from tle.serializers.mixins.recent_activity import RecentActivityMixin __all__ = ( @@ -10,5 +11,6 @@ 'BojProfileMixin', 'DifficultyDictMixin', 'AnalysisDictMixin', + 'RecentActivityMixin', 'TagListMixin', ) diff --git a/app/tle/serializers/mixins/recent_activity.py b/app/tle/serializers/mixins/recent_activity.py new file mode 100644 index 0000000..f7d4f58 --- /dev/null +++ b/app/tle/serializers/mixins/recent_activity.py @@ -0,0 +1,41 @@ +from datetime import date +from dataclasses import dataclass, asdict +from typing import Optional + +from rest_framework.serializers import * + +from tle.models import Crew, CrewActivity + + +@dataclass +class ActivityDict: + nth: Optional[int] = None + name: str = '' + start_at: Optional[date] = None + end_at: Optional[date] = None + is_open: bool = False # 제출 가능 여부 + + @classmethod + def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': + return ActivityDict( + name=activity.name, + nth=activity.nth(), + is_open=activity.is_opened(), + start_at=activity.start_at, + end_at=activity.end_at, + ) + + +class RecentActivityMixin: + def recent_activity(self, crew: Crew) -> dict: + if not crew.is_active: + activity_dict = ActivityDict(name='활동 종료') + elif (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): + activity = opened_activities.earliest() + activity_dict = ActivityDict.from_activity(activity) + elif (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): + activity = closed_activities.latest() + activity_dict = ActivityDict.from_activity(activity) + else: + activity_dict = ActivityDict(name='등록된 활동 없음') + return asdict(activity_dict) From 098d7a9bbf7f6c34622b558fc2a8d57f00969021 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 24 Jul 2024 14:57:52 +0900 Subject: [PATCH 303/552] =?UTF-8?q?feat(tle.serializers):=20=EC=96=B8?= =?UTF-8?q?=EC=96=B4/=EC=82=AC=EC=9A=A9=EC=9E=90=EB=A0=88=EB=B2=A8?= =?UTF-8?q?=EC=A0=9C=ED=95=9C/=EC=BB=A4=EC=8A=A4=ED=85=80=20=ED=83=9C?= =?UTF-8?q?=EA=B7=B8=EB=A5=BC=20=EA=B5=AC=EB=B6=84=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20`type`=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/mixins/tag_list.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py index 53d3370..166cbc9 100644 --- a/app/tle/serializers/mixins/tag_list.py +++ b/app/tle/serializers/mixins/tag_list.py @@ -1,4 +1,5 @@ import dataclasses +import enum import typing from rest_framework.serializers import * @@ -7,10 +8,17 @@ from tle.models.choices import BojUserLevel +class TagType(enum.Enum): + LANGUAGE = 'language' + LEVEL = 'level' + CUSTOM = 'custom' + + @dataclasses.dataclass class TagDict: key: str name: str + type: TagType class TagListMixin: @@ -42,7 +50,7 @@ def tag_list(self, crew: Crew) -> typing.Dict: def _get_language_tags(self, crew: Crew) -> typing.Iterable[TagDict]: for lang in crew.submittable_languages.all(): - yield TagDict(key=lang.key, name=lang.name) + yield TagDict(key=lang.key, name=lang.name, type=TagType.LANGUAGE.value) def _get_boj_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: if crew.min_boj_level is not None: @@ -50,7 +58,7 @@ def _get_boj_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: if crew.max_boj_level is not None: yield self._get_boj_level_bound_tag(crew.max_boj_level, 1, "이하") if crew.min_boj_level is None and crew.max_boj_level is None: - yield TagDict(key=None, name="티어 무관") + yield TagDict(key=None, name="티어 무관", type=TagType.LEVEL.value) def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, lang='ko', arabic=False) -> TagDict: """level에 대한 백준 난이도 태그를 반환한다. @@ -70,8 +78,8 @@ def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, level_name = BojUserLevel.get_division_name(level, lang=lang) else: level_name = BojUserLevel.get_name(level, lang=lang, arabic=arabic) - return TagDict(key=None, name=f'{level_name} {bound_msg}') + return TagDict(key=None, name=f'{level_name} {bound_msg}', type=TagType.LEVEL.value) def _get_custom_tags(self, crew: Crew) -> typing.Iterable[TagDict]: for tag in crew.custom_tags: - yield TagDict(key=None, name=tag) + yield TagDict(key=None, name=tag, type=TagType.CUSTOM.value) From 51c354fe5f6b48315438f7b1cfe681d652c679fe Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 24 Jul 2024 15:38:09 +0900 Subject: [PATCH 304/552] =?UTF-8?q?fix(tle.serializers):=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=ED=83=80=EC=9E=85=20=ED=9E=8C=ED=8A=B8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/choices/problem_difficulty.py | 1 + app/tle/serializers/problem_minimal.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/tle/models/choices/problem_difficulty.py b/app/tle/models/choices/problem_difficulty.py index 2e9d2a7..ba39828 100644 --- a/app/tle/models/choices/problem_difficulty.py +++ b/app/tle/models/choices/problem_difficulty.py @@ -27,6 +27,7 @@ def get_name(cls, value: int, lang='ko') -> str: ) return NAMES[lang][value] + UNDER_ANALYSIS = 0, '분석 중' EASY = 1, '쉬움' NORMAL = 2, '보통' HARD = 3, '어려움' diff --git a/app/tle/serializers/problem_minimal.py b/app/tle/serializers/problem_minimal.py index 5b8ea7c..f5244b7 100644 --- a/app/tle/serializers/problem_minimal.py +++ b/app/tle/serializers/problem_minimal.py @@ -1,6 +1,7 @@ from rest_framework.serializers import * from tle.models import Problem, ProblemAnalysis +from tle.models.choices import ProblemDifficulty from tle.serializers.mixins import DifficultyDictMixin @@ -20,6 +21,6 @@ class Meta: def get_difficulty(self, obj: Problem): try: - return self.difficulty_dict(obj.analysis.difficulty) + return self.difficulty_dict(ProblemDifficulty(obj.analysis.difficulty)) except ProblemAnalysis.DoesNotExist: - return self.difficulty_dict(0) + return self.difficulty_dict(ProblemDifficulty(0)) From e48e5981ebd02493f56654a91c891b473d391faf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 24 Jul 2024 15:50:14 +0900 Subject: [PATCH 305/552] =?UTF-8?q?feat(tle.views):=20=EB=82=98=EC=9D=98?= =?UTF-8?q?=20=ED=81=AC=EB=A3=A8=20=EB=AA=A9=EB=A1=9D=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=EC=9D=B4=20=EC=A2=85=EB=A3=8C=EB=90=9C=20?= =?UTF-8?q?=ED=81=AC=EB=A3=A8=EB=A5=BC=20=EB=AA=A9=EB=A1=9D=EC=9D=98=20?= =?UTF-8?q?=EB=A7=88=EC=A7=80=EB=A7=89=EC=97=90=20=EB=B0=B0=EC=B9=98=20#1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/views/viewsets/crew_viewset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py index 1a26aee..9ad7478 100644 --- a/app/tle/views/viewsets/crew_viewset.py +++ b/app/tle/views/viewsets/crew_viewset.py @@ -25,7 +25,7 @@ def get_queryset(self): if self.action in 'list_recruiting': return Crew.objects.filter(is_recruiting=True) if self.action in 'list_joined': - return Crew.of_user(self.request.user) + return Crew.of_user(self.request.user).order_by('-'+Crew.field_name.IS_ACTIVE) return Crew.objects.all() def get_serializer_class(self): From 78347d173b035aa0bbf205b4c17df19e42b2cf70 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 24 Jul 2024 17:01:54 +0900 Subject: [PATCH 306/552] feat(tle.views): change url `/crews/joined` -> `/crews/my` --- app/tle/views/urls.py | 2 +- app/tle/views/viewsets/crew_viewset.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index 7e2a6c5..8042623 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -35,6 +35,6 @@ ])), path("crews/", include([ path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), - path("joined", CrewViewSet.as_view({"get": "list_joined"})), + path("my", CrewViewSet.as_view({"get": "list_my"})), ])), ] diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py index 9ad7478..99ea21c 100644 --- a/app/tle/views/viewsets/crew_viewset.py +++ b/app/tle/views/viewsets/crew_viewset.py @@ -12,7 +12,7 @@ def has_permission(self, request: Request, view: ViewSet): if view.action == 'list_recruiting': # 모든 사용자에게 공개 return True - if view.action == 'list_joined': + if view.action == 'list_my': return request.user.is_authenticated @@ -24,19 +24,19 @@ class CrewViewSet(ModelViewSet): def get_queryset(self): if self.action in 'list_recruiting': return Crew.objects.filter(is_recruiting=True) - if self.action in 'list_joined': + if self.action in 'list_my': return Crew.of_user(self.request.user).order_by('-'+Crew.field_name.IS_ACTIVE) return Crew.objects.all() def get_serializer_class(self): if self.action in 'list_recruiting': return CrewRecruitingSerializer - if self.action in 'list_joined': + if self.action in 'list_my': return CrewJoinedSerializer def list_recruiting(self, request): # TODO: 검색 옵션 (사용 언어 / 백준 티어) 제공 return super().list(request) - def list_joined(self, request): + def list_my(self, request): return super().list(request) From 4966bae4160585b4682d843f9f1ff39ec5aa215c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 24 Jul 2024 14:58:45 +0900 Subject: [PATCH 307/552] =?UTF-8?q?feat:=20=ED=81=AC=EB=A3=A8=EC=97=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8B=9C=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=A0=88=EB=B2=A8=20=EC=A0=9C=ED=95=9C=20=EC=A4=91=20=EC=83=81?= =?UTF-8?q?=ED=95=9C=EC=9D=84=20=EC=A0=9C=EA=B1=B0=ED=95=A8=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/admin/__init__.py | 22 ++++++++++------ app/tle/models/crew.py | 35 ++++---------------------- app/tle/serializers/mixins/tag_list.py | 12 ++++----- 3 files changed, 24 insertions(+), 45 deletions(-) diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index f575973..9782dd7 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -118,14 +118,13 @@ class ProblemTagModelAdmin(admin.ModelAdmin): class CrewModelAdmin(admin.ModelAdmin): list_display = [ 'get_display_name', + 'get_captain', 'get_members', 'get_applicants', 'get_activities', Crew.field_name.IS_ACTIVE, Crew.field_name.IS_RECRUITING, - Crew.field_name.CREATED_BY, Crew.field_name.CREATED_AT, - Crew.field_name.UPDATED_AT, ] search_fields = [ Crew.field_name.NAME, @@ -135,11 +134,11 @@ class CrewModelAdmin(admin.ModelAdmin): @admin.display(description='Display Name') def get_display_name(self, obj: Crew) -> str: - return f'{obj.icon} {obj.name}' + return obj.get_display_name() - @admin.display(description='Activities') - def get_activities(self, obj: Crew) -> str: - return obj.activities.count() + @admin.display(description='Captain') + def get_captain(self, obj: Crew) -> str: + return obj.get_captain() @admin.display(description='Members') def get_members(self, obj: Crew) -> str: @@ -149,20 +148,27 @@ def get_members(self, obj: Crew) -> str: def get_applicants(self, obj: Crew) -> str: return obj.applicants.count() + @admin.display(description='Activities') + def get_activities(self, obj: Crew) -> str: + return obj.activities.count() + @admin.register(CrewMember) class CrewMemberModelAdmin(admin.ModelAdmin): list_display = [ CrewMember.field_name.USER, - 'is_captain', CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, CrewMember.field_name.CREATED_AT, ] search_fields = [ CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, CrewMember.field_name.USER+'__'+User.field_name.USERNAME, ] - ordering = [CrewMember.field_name.CREW] + ordering = [ + CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, + ] @admin.register(CrewActivity) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index e345eec..f36f795 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -77,10 +77,6 @@ class Crew(models.Model): blank=True, default=list, ) - is_boj_username_required = models.BooleanField( - help_text='백준 아이디 필요 여부를 입력해주세요.', - default=True, - ) min_boj_level = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', choices=BojUserLevel.choices, @@ -88,16 +84,6 @@ class Crew(models.Model): null=True, default=None, ) - max_boj_level = models.IntegerField( - help_text='최대 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - validators=[ - # TODO: 최대 레벨이 최소 레벨보다 높은지 검사 - ], - choices=BojUserLevel.choices, - blank=True, - null=True, - default=None, - ) is_recruiting = models.BooleanField( help_text='모집 중 여부를 입력해주세요.', default=True, @@ -127,9 +113,7 @@ class field_name: MAX_MEMBERS = 'max_members' NOTICE = 'notice' CUSTOM_TAGS = 'custom_tags' - IS_BOJ_USERNAME_REQUIRED = 'is_boj_username_required' MIN_BOJ_LEVEL = 'min_boj_level' - MAX_BOJ_LEVEL = 'max_boj_level' IS_RECRUITING = 'is_recruiting' IS_ACTIVE = 'is_active' CREATED_AT = 'created_at' @@ -176,18 +160,9 @@ def is_joinable(self, user: User) -> bool: return False if self.members.filter(user=user).exists(): return False - if self.is_boj_username_required: - if user.boj_username is None: - # TODO: 인증된 BOJ 사용자명이어야 함 - return False - if self.min_boj_level is not None: - if user.boj_level is None: - return False - if user.boj_level < self.min_boj_level: - return False - if self.max_boj_level is not None: - if user.boj_level is None: - return False - if user.boj_level > self.max_boj_level: - return False + if self.min_boj_level is not None: + return bool( + (user.boj_level is not None) and + (user.boj_level >= self.min_boj_level) + ) return True diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py index 166cbc9..75a669c 100644 --- a/app/tle/serializers/mixins/tag_list.py +++ b/app/tle/serializers/mixins/tag_list.py @@ -40,7 +40,7 @@ def tag_list(self, crew: Crew) -> typing.Dict: # 태그의 나열 순서는 리스트에 선언한 순서를 따름. tags: typing.List[TagDict] = [ *self._get_language_tags(crew), - *self._get_boj_level_tags(crew), + *self._get_level_tags(crew), *self._get_custom_tags(crew), ] return { @@ -52,13 +52,11 @@ def _get_language_tags(self, crew: Crew) -> typing.Iterable[TagDict]: for lang in crew.submittable_languages.all(): yield TagDict(key=lang.key, name=lang.name, type=TagType.LANGUAGE.value) - def _get_boj_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: - if crew.min_boj_level is not None: - yield self._get_boj_level_bound_tag(crew.min_boj_level, 5, "이상") - if crew.max_boj_level is not None: - yield self._get_boj_level_bound_tag(crew.max_boj_level, 1, "이하") - if crew.min_boj_level is None and crew.max_boj_level is None: + def _get_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: + if crew.min_boj_level is None: yield TagDict(key=None, name="티어 무관", type=TagType.LEVEL.value) + else: + yield self._get_boj_level_bound_tag(crew.min_boj_level, 5, "이상") def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, lang='ko', arabic=False) -> TagDict: """level에 대한 백준 난이도 태그를 반환한다. From 788e574bd5c3d68da55df51ae422e66fae568b2e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 25 Jul 2024 13:10:28 +0900 Subject: [PATCH 308/552] =?UTF-8?q?refactor(tle.models):=20captain=20?= =?UTF-8?q?=EC=9D=B8=EC=A7=80=20=EC=97=AC=EB=B6=80=EB=A5=BC=20Crew=20?= =?UTF-8?q?=EA=B0=80=20=EC=95=84=EB=8B=8C=20CrewMember=EA=B0=80=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/crew.py | 36 +++++++++++++++++++----------- app/tle/models/crew_member.py | 15 ++++++++----- app/tle/serializers/crew_member.py | 7 ++---- 3 files changed, 34 insertions(+), 24 deletions(-) diff --git a/app/tle/models/crew.py b/app/tle/models/crew.py index f36f795..4c3b3da 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/crew.py @@ -136,29 +136,39 @@ def __str__(self) -> str: def save(self, *args, **kwargs) -> None: with transaction.atomic(): - obj = super().save(*args, **kwargs) - if not self.members.filter(user=self.created_by).exists(): - captain = self.members.create( - user=self.created_by, - is_captain=True - ) + super().save(*args, **kwargs) + self.set_captain(self.created_by) + + def get_display_name(self) -> str: + return f'{self.icon} {self.name}' + + def get_captain(self) -> User: + return self.members.get(is_captain=True).user + + def set_captain(self, user: User) -> None: + assert isinstance(user, User) + with transaction.atomic(): + self.members.filter(is_captain=True).update(is_captain=False) + try: + captain = self.members.get(user=user) + captain.is_captain = True + except self.members.model.DoesNotExist: + captain = self.members.create(user=user, is_captain=True) + finally: captain.save() - return obj + + def is_captain(self, user: User) -> bool: + return self.members.filter(user=user, is_captain=True).exists() def is_member(self, user: User) -> bool: return self.members.filter(user=user).exists() - def is_captain(self, user: User) -> bool: - return self.created_by == user - def is_joinable(self, user: User) -> bool: if not self.is_recruiting: return False - if self.is_captain(user): - return False if self.members.count() >= self.max_members: return False - if self.members.filter(user=user).exists(): + if self.is_member(user): return False if self.min_boj_level is not None: return bool( diff --git a/app/tle/models/crew_member.py b/app/tle/models/crew_member.py index 1b2bf19..75d7d53 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/crew_member.py @@ -1,6 +1,5 @@ from __future__ import annotations -from django.contrib import admin from django.db import models from tle.models.user import User @@ -20,11 +19,16 @@ class CrewMember(models.Model): related_name=User.field_name.MEMBERS, help_text='유저를 입력해주세요.', ) + is_captain = models.BooleanField( + default=False, + help_text='크루장 여부', + ) created_at = models.DateTimeField(auto_now_add=True) class field_name: CREW = 'crew' USER = 'user' + IS_CAPTAIN = 'is_captain' CREATED_AT = 'created_at' class Meta: @@ -38,8 +42,7 @@ class Meta: @classmethod def captain_of(cls, crew: Crew) -> CrewMember: - return cls.objects.get(crew=crew, user=crew.created_by) - - @admin.display(boolean=True, description='Is Captain') - def is_captain(self) -> bool: - return self.crew.is_captain(self.user) + return cls.objects.get(**{ + CrewMember.field_name.CREW: crew, + CrewMember.field_name.IS_CAPTAIN: True, + }) diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py index d07acc7..242064a 100644 --- a/app/tle/serializers/crew_member.py +++ b/app/tle/serializers/crew_member.py @@ -6,15 +6,12 @@ class CrewMemberSerializer(ModelSerializer): user = UserMinimalSerializer(read_only=True) - is_captain = SerializerMethodField() class Meta: model = CrewMember fields = ( CrewMember.field_name.USER, - 'is_captain', + CrewMember.field_name.IS_CAPTAIN, CrewMember.field_name.CREATED_AT, ) - - def get_is_captain(self, obj: CrewMember) -> bool: - return obj.is_captain() + read_only_fields = '__all__' From fd7de9a85b1c81adc31c890001340207477e7902 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 25 Jul 2024 13:11:05 +0900 Subject: [PATCH 309/552] =?UTF-8?q?feat(tle.serializers):=20`CrewRecruitin?= =?UTF-8?q?gSerializer`=20=EC=97=90=EC=84=9C=20=ED=81=AC=EB=A3=A8=EA=B0=80?= =?UTF-8?q?=20=ED=99=9C=EB=8F=99=EC=A4=91=EC=9D=B8=EC=A7=80=20=EC=97=AC?= =?UTF-8?q?=EB=B6=80=EB=8F=84=20=EB=B3=B4=EC=9D=B4=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/serializers/crew_recruiting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py index a4799a1..4e3d854 100644 --- a/app/tle/serializers/crew_recruiting.py +++ b/app/tle/serializers/crew_recruiting.py @@ -23,6 +23,7 @@ class Meta: fields = [ Crew.field_name.ICON, Crew.field_name.NAME, + Crew.field_name.IS_ACTIVE, Crew.field_name.IS_RECRUITING, 'is_joinable', 'is_member', From 0ae2b50ccb132988ca6a9bcbe8fa660afbeb070a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 25 Jul 2024 13:24:23 +0900 Subject: [PATCH 310/552] feat(tle.serializers): add `CrewDetailSerializer` --- app/tle/serializers/__init__.py | 2 + app/tle/serializers/crew_detail.py | 101 +++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 app/tle/serializers/crew_detail.py diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index c5b790d..421f3fe 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -6,6 +6,7 @@ from tle.serializers.problem_minimal import ProblemMinimalSerializer from tle.serializers.problem_tag import ProblemTagSerializer +from tle.serializers.crew_detail import CrewDetailSerializer from tle.serializers.crew_member import CrewMemberSerializer from tle.serializers.crew_recruiting import CrewRecruitingSerializer from tle.serializers.crew_joined import CrewJoinedSerializer @@ -26,6 +27,7 @@ 'ProblemMinimalSerializer', 'ProblemTagSerializer', + 'CrewDetailSerializer', 'CrewMemberSerializer', 'CrewRecruitingSerializer', 'CrewJoinedSerializer', diff --git a/app/tle/serializers/crew_detail.py b/app/tle/serializers/crew_detail.py new file mode 100644 index 0000000..24d300d --- /dev/null +++ b/app/tle/serializers/crew_detail.py @@ -0,0 +1,101 @@ +from django.db.models import QuerySet +from django.db.transaction import atomic +from rest_framework.serializers import * + +from tle.models import Crew, SubmissionLanguage +from tle.serializers.crew_member import CrewMemberSerializer +from tle.serializers.user_minimal import UserMinimalSerializer +from tle.serializers.mixins import CurrentUserMixin, TagListMixin + + +class CrewDetailSerializer(ModelSerializer, CurrentUserMixin, TagListMixin): + is_member = SerializerMethodField() + members = SerializerMethodField() + languages = JSONField(help_text='사용 가능한 언어 목록 (언어 key의 배열)') + tags = SerializerMethodField() + created_by = UserMinimalSerializer(read_only=True) + + class Meta: + model = Crew + fields = [ + 'id', + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.MAX_MEMBERS, + 'members', + 'is_member', + Crew.field_name.MIN_BOJ_LEVEL, + 'languages', + Crew.field_name.CUSTOM_TAGS, + 'tags', + Crew.field_name.NOTICE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.IS_ACTIVE, + Crew.field_name.CREATED_AT, + Crew.field_name.CREATED_BY, + Crew.field_name.UPDATED_AT, + ] + read_only_fields = [ + 'id', + 'tags', + 'members', + Crew.field_name.CREATED_AT, + Crew.field_name.CREATED_BY, + Crew.field_name.UPDATED_AT, + ] + extra_kwargs = { + Crew.field_name.MAX_MEMBERS: {'write_only': True}, + Crew.field_name.MIN_BOJ_LEVEL: {'write_only': True}, + 'languages': {'write_only': True}, + Crew.field_name.CUSTOM_TAGS: {'write_only': True}, + } + + def create(self, validated_data): + return super().create(validated_data) + + def get_is_member(self, obj: Crew): + return obj.is_member(self.current_user()) + + def get_members(self, obj: Crew): + return { + 'count': obj.members.count(), + 'max_count': obj.max_members, + 'items': CrewMemberSerializer(obj.members.all(), many=True).data, + } + + def get_tags(self, obj: Crew): + return self.tag_list(obj) + + def validate_languages(self, value) -> QuerySet[SubmissionLanguage]: + """언어 정보를 언어 키의 배열로 받고, 이를 SubmissionLanguage의 QuerySet으로 변환한다.""" + # 언어 정보는 문자열의 배열로 받는다. + if not isinstance(value, list): + raise ValidationError('Languages must be a list of strings') + for lang in value: + if not isinstance(lang, str): + raise ValidationError('Languages must be a list of strings') + # 최소 한 개 이상의 언어가 있어야 한다. + if len(value) == 0: + raise ValidationError('At least one language must be specified') + for lang in value: + if not SubmissionLanguage.objects.filter(**{ + SubmissionLanguage.field_name.KEY: lang, + }).exists(): + raise ValidationError(f'Invalid language key "{lang}"') + # 언어 키의 배열을 SubmissionLanguage의 QuerySet으로 변환한다. + return SubmissionLanguage.objects.filter(**{ + SubmissionLanguage.field_name.KEY + '__in': value, + }) + + def save(self, **kwargs): + crew: Crew + languages: QuerySet[SubmissionLanguage] + languages = self.validated_data.pop('languages') + with atomic(): + crew = super().save(**{ + **kwargs, + Crew.field_name.CREATED_BY: self.current_user(), + }) + crew.submittable_languages.set(languages) + crew.save() + return crew \ No newline at end of file From 0de0d8134724a7811220cca0db1f455014b321ab Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 25 Jul 2024 13:26:25 +0900 Subject: [PATCH 311/552] feat(tle.views): add `/crews/`, and `/crews/detail` --- app/tle/views/urls.py | 9 +++++++++ app/tle/views/viewsets/crew_viewset.py | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index 8042623..a655bcb 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -34,7 +34,16 @@ ])), ])), path("crews/", include([ + path("", CrewViewSet.as_view({"post": "create"})), path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), path("my", CrewViewSet.as_view({"get": "list_my"})), + path("/", include([ + path("detail", CrewViewSet.as_view({ + "get": "retrieve", + "put": "update", + "patch": "partial_update", + "delete": "destroy", + })) + ])), ])), ] diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py index 99ea21c..3f943c3 100644 --- a/app/tle/views/viewsets/crew_viewset.py +++ b/app/tle/views/viewsets/crew_viewset.py @@ -12,8 +12,7 @@ def has_permission(self, request: Request, view: ViewSet): if view.action == 'list_recruiting': # 모든 사용자에게 공개 return True - if view.action == 'list_my': - return request.user.is_authenticated + return request.user.is_authenticated class CrewViewSet(ModelViewSet): @@ -33,6 +32,7 @@ def get_serializer_class(self): return CrewRecruitingSerializer if self.action in 'list_my': return CrewJoinedSerializer + return CrewDetailSerializer def list_recruiting(self, request): # TODO: 검색 옵션 (사용 언어 / 백준 티어) 제공 From 481b624a735fa760b56cd80f10e175483ef70c95 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 25 Jul 2024 13:35:23 +0900 Subject: [PATCH 312/552] feat(tle.models): create `ProblemAnalysisQueue` --- app/tle/admin/__init__.py | 40 ++++++++++++++++++++- app/tle/models/__init__.py | 2 ++ app/tle/models/problem.py | 7 ++++ app/tle/models/problem_analysis.py | 3 ++ app/tle/models/problem_analysis_queue.py | 46 ++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 app/tle/models/problem_analysis_queue.py diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index 9782dd7..3327f13 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -49,7 +49,10 @@ class ProblemModelAdmin(admin.ModelAdmin): Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, ] ordering = ['-'+Problem.field_name.CREATED_AT] - actions = ['set_creator'] + actions = [ + 'set_creator', + 'add_to_analysis_queue', + ] @admin.action(description="set admin(you) as creator for selected problems") def set_creator(self, request, queryset: QuerySet[Problem]): @@ -65,6 +68,20 @@ def set_creator(self, request, queryset: QuerySet[Problem]): messages.SUCCESS, ) + @admin.action(description="add selected problems to analysis queue") + def add_to_analysis_queue(self, request, queryset: QuerySet[Problem]): + ProblemAnalysisQueue.extend(queryset) + self.message_user( + request, + ngettext( + "%d problem was successfully added to analysis queue.", + "%d problems were successfully added to analysis queue.", + queryset.count(), + ) + % queryset.count(), + messages.SUCCESS, + ) + @admin.register(ProblemAnalysis) class ProblemAnalysisModelAdmin(admin.ModelAdmin): @@ -98,6 +115,27 @@ def get_hint(self, obj: ProblemAnalysis) -> str: return len(obj.hint), shorten(', '.join(obj.hint), width=32) +@admin.register(ProblemAnalysisQueue) +class ProblemAnalysisQueueModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysisQueue.field_name.PROBLEM, + ProblemAnalysisQueue.field_name.ANALYSIS, + ProblemAnalysisQueue.field_name.IS_ANALYZING, + 'get_is_analyzed', + ProblemAnalysisQueue.field_name.CREATED_AT, + ] + search_fields = [ + ProblemAnalysisQueue.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ] + ordering = [ + ProblemAnalysisQueue.field_name.CREATED_AT, + ] + + @admin.display(description='Is Analyzed', boolean=True) + def get_is_analyzed(self, obj: ProblemAnalysisQueue) -> bool: + return obj.analysis is not None + + @admin.register(ProblemTag) class ProblemTagModelAdmin(admin.ModelAdmin): list_display = [ diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index 0f5ed09..f28090b 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -10,6 +10,7 @@ from tle.models.problem import Problem from tle.models.problem_analysis import ProblemAnalysis +from tle.models.problem_analysis_queue import ProblemAnalysisQueue from tle.models.problem_tag import ProblemTag from tle.models.submission import Submission @@ -31,6 +32,7 @@ 'Problem', 'ProblemAnalysis', + 'ProblemAnalysisQueue', 'ProblemTag', 'Submission', diff --git a/app/tle/models/problem.py b/app/tle/models/problem.py index 3348bca..6cc88db 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/problem.py @@ -11,6 +11,7 @@ class Problem(models.Model): if typing.TYPE_CHECKING: analysis: models.OneToOneField[_T.ProblemAnalysis] + analysis_queue: models.ManyToManyField[_T.ProblemAnalysisQueue] activity_problems: models.ManyToOneRel[_T.CrewActivityProblem] title = models.CharField( @@ -54,6 +55,7 @@ class Problem(models.Model): class field_name: # related fields ANALYSIS = 'analysis' + ANALYSIS_QUEUE = 'analysis_queue' ACTIVITY_PROBLEMS = 'activity_problems' # fields TITLE = 'title' @@ -72,3 +74,8 @@ class Meta: def __str__(self) -> str: return f'[{self.pk} : {self.title}]' + + def save(self, *args, **kwargs) -> None: + from tle.models import ProblemAnalysisQueue + super().save(*args, **kwargs) + ProblemAnalysisQueue.append(self) diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/problem_analysis.py index 61dbcfe..2ab1e74 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/problem_analysis.py @@ -47,3 +47,6 @@ class field_name: TIME_COMPLEXITY = 'time_complexity' HINT = 'hint' CREATED_AT = 'created_at' + + def __str__(self): + return f'[Analyse of {self.problem}]' \ No newline at end of file diff --git a/app/tle/models/problem_analysis_queue.py b/app/tle/models/problem_analysis_queue.py new file mode 100644 index 0000000..120090f --- /dev/null +++ b/app/tle/models/problem_analysis_queue.py @@ -0,0 +1,46 @@ +from django.db import models + +from tle.models.problem import Problem +from tle.models.problem_analysis import ProblemAnalysis + + +class ProblemAnalysisQueue(models.Model): + problem = models.ForeignKey( + Problem, + on_delete=models.CASCADE, + related_name=Problem.field_name.ANALYSIS_QUEUE, + help_text='문제를 입력해주세요.', + ) + analysis = models.OneToOneField( + ProblemAnalysis, + on_delete=models.SET_NULL, + blank=True, + null=True, + help_text='문제 분석 결과를 입력해주세요.', + ) + is_analyzing = models.BooleanField( + default=False, + help_text='문제 분석이 완료되었는지 여부를 입력해주세요.', + ) + created_at = models.DateTimeField(auto_now_add=True) + + class field_name: + PROBLEM = 'problem' + ANALYSIS = 'analysis' + IS_ANALYZING = 'is_analyzing' + CREATED_AT = 'created_at' + + class Meta: + ordering = ['created_at'] + + @classmethod + def append(cls, problem: Problem): + return cls.objects.create(**{ + cls.field_name.PROBLEM: problem, + }) + + @classmethod + def extend(cls, problems: models.QuerySet[Problem]): + # TODO: Do bulk_create() + for problem in problems: + cls.append(problem) From 37ff20ea4421ac15528d3f5e5ffb7bd526896dc0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 26 Jul 2024 04:43:01 +0900 Subject: [PATCH 313/552] =?UTF-8?q?refactor(tle.models.dao):=20orm=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=EB=93=A4=EC=9D=84=20dao=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/tle/models/__init__.py | 18 +-------- app/tle/models/dao/__init__.py | 37 +++++++++++++++++++ app/tle/models/{ => dao}/crew.py | 4 +- app/tle/models/{ => dao}/crew_activity.py | 2 +- .../models/{ => dao}/crew_activity_problem.py | 4 +- app/tle/models/{ => dao}/crew_applicant.py | 6 +-- app/tle/models/{ => dao}/crew_member.py | 4 +- app/tle/models/{ => dao}/problem.py | 2 +- app/tle/models/{ => dao}/problem_analysis.py | 4 +- .../{ => dao}/problem_analysis_queue.py | 4 +- app/tle/models/{ => dao}/problem_tag.py | 0 app/tle/models/{ => dao}/submission.py | 6 +-- .../models/{ => dao}/submission_comment.py | 4 +- .../models/{ => dao}/submission_language.py | 0 app/tle/models/{ => dao}/user.py | 0 15 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 app/tle/models/dao/__init__.py rename app/tle/models/{ => dao}/crew.py (98%) rename app/tle/models/{ => dao}/crew_activity.py (98%) rename app/tle/models/{ => dao}/crew_activity_problem.py (94%) rename app/tle/models/{ => dao}/crew_applicant.py (95%) rename app/tle/models/{ => dao}/crew_member.py (94%) rename app/tle/models/{ => dao}/problem.py (98%) rename app/tle/models/{ => dao}/problem_analysis.py (94%) rename app/tle/models/{ => dao}/problem_analysis_queue.py (92%) rename app/tle/models/{ => dao}/problem_tag.py (100%) rename app/tle/models/{ => dao}/submission.py (92%) rename app/tle/models/{ => dao}/submission_comment.py (95%) rename app/tle/models/{ => dao}/submission_language.py (100%) rename app/tle/models/{ => dao}/user.py (100%) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index f28090b..bbe2968 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,21 +1,5 @@ from tle.models import choices - -from tle.models.user import User, UserManager - -from tle.models.crew import Crew -from tle.models.crew_activity import CrewActivity -from tle.models.crew_activity_problem import CrewActivityProblem -from tle.models.crew_applicant import CrewApplicant -from tle.models.crew_member import CrewMember - -from tle.models.problem import Problem -from tle.models.problem_analysis import ProblemAnalysis -from tle.models.problem_analysis_queue import ProblemAnalysisQueue -from tle.models.problem_tag import ProblemTag - -from tle.models.submission import Submission -from tle.models.submission_comment import SubmissionComment -from tle.models.submission_language import SubmissionLanguage +from tle.models.dao import * __all__ = ( diff --git a/app/tle/models/dao/__init__.py b/app/tle/models/dao/__init__.py new file mode 100644 index 0000000..c242882 --- /dev/null +++ b/app/tle/models/dao/__init__.py @@ -0,0 +1,37 @@ +from tle.models.dao.user import User, UserManager + +from tle.models.dao.crew import Crew +from tle.models.dao.crew_activity import CrewActivity +from tle.models.dao.crew_activity_problem import CrewActivityProblem +from tle.models.dao.crew_applicant import CrewApplicant +from tle.models.dao.crew_member import CrewMember + +from tle.models.dao.problem import Problem +from tle.models.dao.problem_analysis import ProblemAnalysis +from tle.models.dao.problem_analysis_queue import ProblemAnalysisQueue +from tle.models.dao.problem_tag import ProblemTag + +from tle.models.dao.submission import Submission +from tle.models.dao.submission_comment import SubmissionComment +from tle.models.dao.submission_language import SubmissionLanguage + + +__all__ = ( + 'User', + 'UserManager', + + 'Crew', + 'CrewActivity', + 'CrewActivityProblem', + 'CrewApplicant', + 'CrewMember', + + 'Problem', + 'ProblemAnalysis', + 'ProblemAnalysisQueue', + 'ProblemTag', + + 'Submission', + 'SubmissionComment', + 'SubmissionLanguage', +) diff --git a/app/tle/models/crew.py b/app/tle/models/dao/crew.py similarity index 98% rename from app/tle/models/crew.py rename to app/tle/models/dao/crew.py index 4c3b3da..e8bee5e 100644 --- a/app/tle/models/crew.py +++ b/app/tle/models/dao/crew.py @@ -11,8 +11,8 @@ from tle.enums import Emoji from tle.models.choices import BojUserLevel -from tle.models.user import User -from tle.models.submission_language import SubmissionLanguage +from tle.models.dao.user import User +from tle.models.dao.submission_language import SubmissionLanguage if typing.TYPE_CHECKING: import tle.models as _T diff --git a/app/tle/models/crew_activity.py b/app/tle/models/dao/crew_activity.py similarity index 98% rename from app/tle/models/crew_activity.py rename to app/tle/models/dao/crew_activity.py index 5b4a569..10a3aa1 100644 --- a/app/tle/models/crew_activity.py +++ b/app/tle/models/dao/crew_activity.py @@ -5,7 +5,7 @@ from django.db import models from django.utils import timezone -from tle.models.crew import Crew +from tle.models.dao.crew import Crew if typing.TYPE_CHECKING: import tle.models as _T diff --git a/app/tle/models/crew_activity_problem.py b/app/tle/models/dao/crew_activity_problem.py similarity index 94% rename from app/tle/models/crew_activity_problem.py rename to app/tle/models/dao/crew_activity_problem.py index a45babd..f9ed9d1 100644 --- a/app/tle/models/crew_activity_problem.py +++ b/app/tle/models/dao/crew_activity_problem.py @@ -3,8 +3,8 @@ from django.core.validators import MinValueValidator from django.db import models -from tle.models.crew_activity import CrewActivity -from tle.models.problem import Problem +from tle.models.dao.crew_activity import CrewActivity +from tle.models.dao.problem import Problem if typing.TYPE_CHECKING: import tle.models as _T diff --git a/app/tle/models/crew_applicant.py b/app/tle/models/dao/crew_applicant.py similarity index 95% rename from app/tle/models/crew_applicant.py rename to app/tle/models/dao/crew_applicant.py index 266ee2d..c2a1ad1 100644 --- a/app/tle/models/crew_applicant.py +++ b/app/tle/models/dao/crew_applicant.py @@ -1,9 +1,9 @@ from django.db import models, transaction from django.utils import timezone -from tle.models.user import User -from tle.models.crew import Crew -from tle.models.crew_member import CrewMember +from tle.models.dao.user import User +from tle.models.dao.crew import Crew +from tle.models.dao.crew_member import CrewMember class CrewApplicant(models.Model): diff --git a/app/tle/models/crew_member.py b/app/tle/models/dao/crew_member.py similarity index 94% rename from app/tle/models/crew_member.py rename to app/tle/models/dao/crew_member.py index 75d7d53..2b29174 100644 --- a/app/tle/models/crew_member.py +++ b/app/tle/models/dao/crew_member.py @@ -2,8 +2,8 @@ from django.db import models -from tle.models.user import User -from tle.models.crew import Crew +from tle.models.dao.user import User +from tle.models.dao.crew import Crew class CrewMember(models.Model): diff --git a/app/tle/models/problem.py b/app/tle/models/dao/problem.py similarity index 98% rename from app/tle/models/problem.py rename to app/tle/models/dao/problem.py index 6cc88db..b42cf70 100644 --- a/app/tle/models/problem.py +++ b/app/tle/models/dao/problem.py @@ -2,7 +2,7 @@ from django.db import models -from tle.models.user import User +from tle.models.dao.user import User if typing.TYPE_CHECKING: import tle.models as _T diff --git a/app/tle/models/problem_analysis.py b/app/tle/models/dao/problem_analysis.py similarity index 94% rename from app/tle/models/problem_analysis.py rename to app/tle/models/dao/problem_analysis.py index 2ab1e74..3d534e6 100644 --- a/app/tle/models/problem_analysis.py +++ b/app/tle/models/dao/problem_analysis.py @@ -1,8 +1,8 @@ from django.db import models from tle.models.choices import ProblemDifficulty -from tle.models.problem import Problem -from tle.models.problem_tag import ProblemTag +from tle.models.dao.problem import Problem +from tle.models.dao.problem_tag import ProblemTag class ProblemAnalysis(models.Model): diff --git a/app/tle/models/problem_analysis_queue.py b/app/tle/models/dao/problem_analysis_queue.py similarity index 92% rename from app/tle/models/problem_analysis_queue.py rename to app/tle/models/dao/problem_analysis_queue.py index 120090f..2fc8bb0 100644 --- a/app/tle/models/problem_analysis_queue.py +++ b/app/tle/models/dao/problem_analysis_queue.py @@ -1,7 +1,7 @@ from django.db import models -from tle.models.problem import Problem -from tle.models.problem_analysis import ProblemAnalysis +from tle.models.dao.problem import Problem +from tle.models.dao.problem_analysis import ProblemAnalysis class ProblemAnalysisQueue(models.Model): diff --git a/app/tle/models/problem_tag.py b/app/tle/models/dao/problem_tag.py similarity index 100% rename from app/tle/models/problem_tag.py rename to app/tle/models/dao/problem_tag.py diff --git a/app/tle/models/submission.py b/app/tle/models/dao/submission.py similarity index 92% rename from app/tle/models/submission.py rename to app/tle/models/dao/submission.py index 62b5d6b..49416dd 100644 --- a/app/tle/models/submission.py +++ b/app/tle/models/dao/submission.py @@ -2,9 +2,9 @@ from django.db import models -from tle.models.user import User -from tle.models.crew_activity_problem import CrewActivityProblem -from tle.models.submission_language import SubmissionLanguage +from tle.models.dao.user import User +from tle.models.dao.crew_activity_problem import CrewActivityProblem +from tle.models.dao.submission_language import SubmissionLanguage if typing.TYPE_CHECKING: import tle.models as _T diff --git a/app/tle/models/submission_comment.py b/app/tle/models/dao/submission_comment.py similarity index 95% rename from app/tle/models/submission_comment.py rename to app/tle/models/dao/submission_comment.py index b69e878..a66de85 100644 --- a/app/tle/models/submission_comment.py +++ b/app/tle/models/dao/submission_comment.py @@ -1,8 +1,8 @@ from django.core.validators import MinValueValidator from django.db import models -from tle.models.user import User -from tle.models.submission import Submission +from tle.models.dao.user import User +from tle.models.dao.submission import Submission class SubmissionComment(models.Model): diff --git a/app/tle/models/submission_language.py b/app/tle/models/dao/submission_language.py similarity index 100% rename from app/tle/models/submission_language.py rename to app/tle/models/dao/submission_language.py diff --git a/app/tle/models/user.py b/app/tle/models/dao/user.py similarity index 100% rename from app/tle/models/user.py rename to app/tle/models/dao/user.py From 7198d1a26ff3bdeb7eeb3cc4fe6836219126f4b4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 26 Jul 2024 21:47:10 +0900 Subject: [PATCH 314/552] =?UTF-8?q?refactor:=20`User`=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20`users`=20=EC=95=B1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 5 +- app/app/urls.py | 12 ++- app/tle/admin/__init__.py | 21 +---- app/tle/models/__init__.py | 3 - app/tle/models/choices/__init__.py | 1 - app/tle/models/choices/bol_user_level.py | 98 -------------------- app/tle/models/dao/__init__.py | 5 - app/tle/models/dao/crew.py | 21 +---- app/tle/models/dao/crew_activity.py | 10 -- app/tle/models/dao/crew_activity_problem.py | 8 -- app/tle/models/dao/crew_applicant.py | 5 +- app/tle/models/dao/crew_member.py | 4 +- app/tle/models/dao/problem.py | 13 +-- app/tle/models/dao/problem_analysis.py | 1 - app/tle/models/dao/problem_analysis_queue.py | 1 - app/tle/models/dao/problem_tag.py | 17 +--- app/tle/models/dao/submission.py | 15 +-- app/tle/models/dao/submission_comment.py | 5 +- app/tle/serializers/__init__.py | 10 -- app/tle/serializers/crew_detail.py | 2 +- app/tle/serializers/crew_member.py | 2 +- app/tle/serializers/mixins/__init__.py | 2 - app/tle/serializers/mixins/boj_profile.py | 19 ---- app/tle/serializers/mixins/current_user.py | 2 +- app/tle/serializers/mixins/tag_list.py | 8 +- app/tle/serializers/user_detail.py | 37 -------- app/tle/serializers/user_minimal.py | 18 ---- app/tle/serializers/user_sign_in.py | 44 --------- app/tle/views/urls.py | 17 ---- app/tle/views/viewsets/__init__.py | 4 - app/tle/views/viewsets/auth_viewset.py | 72 -------------- app/tle/views/viewsets/user_viewset.py | 18 ---- app/users/__init__.py | 0 app/users/admin.py | 33 +++++++ app/users/apps.py | 6 ++ app/{tle => users}/backends.py | 2 +- app/users/migrations/__init__.py | 0 app/users/models/__init__.py | 10 ++ app/{tle/models/dao => users/models}/user.py | 59 ++---------- app/users/models/user_boj_level.py | 65 +++++++++++++ app/users/models/user_manager.py | 30 ++++++ app/users/serializers/__init__.py | 73 +++++++++++++++ app/users/serializers/fields.py | 21 +++++ app/users/serializers/mixins.py | 21 +++++ app/users/views.py | 83 +++++++++++++++++ 45 files changed, 382 insertions(+), 521 deletions(-) delete mode 100644 app/tle/models/choices/bol_user_level.py delete mode 100644 app/tle/serializers/mixins/boj_profile.py delete mode 100644 app/tle/serializers/user_detail.py delete mode 100644 app/tle/serializers/user_minimal.py delete mode 100644 app/tle/serializers/user_sign_in.py delete mode 100644 app/tle/views/viewsets/auth_viewset.py delete mode 100644 app/tle/views/viewsets/user_viewset.py create mode 100644 app/users/__init__.py create mode 100644 app/users/admin.py create mode 100644 app/users/apps.py rename app/{tle => users}/backends.py (95%) create mode 100644 app/users/migrations/__init__.py create mode 100644 app/users/models/__init__.py rename app/{tle/models/dao => users/models}/user.py (57%) create mode 100644 app/users/models/user_boj_level.py create mode 100644 app/users/models/user_manager.py create mode 100644 app/users/serializers/__init__.py create mode 100644 app/users/serializers/fields.py create mode 100644 app/users/serializers/mixins.py create mode 100644 app/users/views.py diff --git a/app/app/settings.py b/app/app/settings.py index b675d73..6681790 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -43,6 +43,7 @@ "django.contrib.messages", "django.contrib.staticfiles", "rest_framework", + "users", "tle", ] @@ -106,10 +107,10 @@ }, ] -AUTH_USER_MODEL = 'tle.User' +AUTH_USER_MODEL = 'users.User' AUTHENTICATION_BACKENDS = [ - 'tle.backends.UserAuthBackend', + 'users.backends.UserAuthBackend', ] diff --git a/app/app/urls.py b/app/app/urls.py index 20a4bb5..6e77a25 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -19,12 +19,20 @@ from django.contrib import admin from django.urls import include, path +from users.views import SignIn, SignUp, SignOut, CurrentUser import tle.views.urls - urlpatterns = [ path("admin/", admin.site.urls), - path("api/v1/", include(tle.views.urls.urlpatterns)), + path("api/v1/", include([ + path("auth/", include([ + path("signin", SignIn.as_view()), + path("signup", SignUp.as_view()), + path("signout", SignOut.as_view()), + ])), + path("users/current", CurrentUser.as_view()), + *tle.views.urls.urlpatterns, + ])), ] # Static files diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index 3327f13..7c3a83c 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -1,10 +1,10 @@ from textwrap import shorten from django.contrib import admin, messages -from django.contrib.auth.admin import UserAdmin from django.db.models import QuerySet from django.utils.translation import ngettext +from users.models import User from tle.models import * @@ -16,25 +16,6 @@ ]) -@admin.register(User) -class UserModelAdmin(UserAdmin): - list_display = [ - User.field_name.USERNAME, - User.field_name.EMAIL, - User.field_name.BOJ_USERNAME, - User.field_name.BOJ_LEVEL, - 'get_crews', - User.field_name.IS_ACTIVE, - User.field_name.IS_STAFF, - User.field_name.IS_SUPERUSER, - User.field_name.CREATED_AT, - User.field_name.BOJ_LEVEL_UPDATED_AT, - ] - - @admin.display(description='captains / members') - def get_crews(self, user: User) -> str: - return f'{Crew.of_user_as_captain(user).count()} / {Crew.of_user(user).count()}' - @admin.register(Problem) class ProblemModelAdmin(admin.ModelAdmin): diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index bbe2968..956d408 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -5,9 +5,6 @@ __all__ = ( 'choices', - 'User', - 'UserManager', - 'Crew', 'CrewActivity', 'CrewActivityProblem', diff --git a/app/tle/models/choices/__init__.py b/app/tle/models/choices/__init__.py index 7aa7870..f3399d3 100644 --- a/app/tle/models/choices/__init__.py +++ b/app/tle/models/choices/__init__.py @@ -1,2 +1 @@ -from tle.models.choices.bol_user_level import BojUserLevel from tle.models.choices.problem_difficulty import ProblemDifficulty diff --git a/app/tle/models/choices/bol_user_level.py b/app/tle/models/choices/bol_user_level.py deleted file mode 100644 index f992b57..0000000 --- a/app/tle/models/choices/bol_user_level.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.db import models - - -DIVISION_NAMES = { - 'ko': { - 0: '난이도를 매길 수 없음', - 1: '브론즈', - 2: '실버', - 3: '골드', - 4: '플래티넘', - 5: '다이아몬드', - 6: '루비', - }, - 'en': { - 0: 'Unrated', - 1: 'Bronze', - 2: 'Silver', - 3: 'Gold', - 4: 'Platinum', - 5: 'Diamond', - 6: 'Ruby', - }, -} - -ARABIC_NUMERALS = { - 0: '', - 1: 'I', - 2: 'II', - 3: 'III', - 4: 'IV', - 5: 'V', -} - - -class BojUserLevel(models.IntegerChoices): - U = 0, 'Unrated' - B5 = 1, '브론즈 5' - B4 = 2, '브론즈 4' - B3 = 3, '브론즈 3' - B2 = 4, '브론즈 2' - B1 = 5, '브론즈 1' - S5 = 6, '실버 5' - S4 = 7, '실버 4' - S3 = 8, '실버 3' - S2 = 9, '실버 2' - S1 = 10, '실버 1' - G5 = 11, '골드 5' - G4 = 12, '골드 4' - G3 = 13, '골드 3' - G2 = 14, '골드 2' - G1 = 15, '골드 1' - P5 = 16, '플래티넘 5' - P4 = 17, '플래티넘 4' - P3 = 18, '플래티넘 3' - P2 = 19, '플래티넘 2' - P1 = 20, '플래티넘 1' - D5 = 21, '다이아몬드 5' - D4 = 22, '다이아몬드 4' - D3 = 23, '다이아몬드 3' - D2 = 24, '다이아몬드 2' - D1 = 25, '다이아몬드 1' - R5 = 26, '루비 5' - R4 = 27, '루비 4' - R3 = 28, '루비 3' - R2 = 29, '루비 2' - R1 = 30, '루비 1' - - @classmethod - def get_division(cls, value: int) -> int: - if value == 0: - return 0 - assert 1 <= value <= 30 - return ((value-1) // 5)+1 - - @classmethod - def get_division_name(cls, value: int, lang='en') -> str: - assert 0 <= value <= 30 - return DIVISION_NAMES[lang][cls.get_division(value)] - - @classmethod - def get_tier(cls, value: int) -> int: - if value == 0: - return 0 - assert 1 <= value <= 30 - return 5 - ((value-1) % 5) - - @classmethod - def get_tier_name(cls, value: int, arabic=True) -> str: - assert 0 <= value <= 30 - tier = cls.get_tier(value) - if arabic: - return ARABIC_NUMERALS[tier] - return str(tier) - - @classmethod - def get_name(cls, value: int, lang='en', arabic=True) -> str: - assert 0 <= value <= 30 - return f'{cls.get_division_name(value, lang=lang)} {cls.get_tier_name(value, arabic=arabic)}' diff --git a/app/tle/models/dao/__init__.py b/app/tle/models/dao/__init__.py index c242882..0233eea 100644 --- a/app/tle/models/dao/__init__.py +++ b/app/tle/models/dao/__init__.py @@ -1,5 +1,3 @@ -from tle.models.dao.user import User, UserManager - from tle.models.dao.crew import Crew from tle.models.dao.crew_activity import CrewActivity from tle.models.dao.crew_activity_problem import CrewActivityProblem @@ -17,9 +15,6 @@ __all__ = ( - 'User', - 'UserManager', - 'Crew', 'CrewActivity', 'CrewActivityProblem', diff --git a/app/tle/models/dao/crew.py b/app/tle/models/dao/crew.py index e8bee5e..1eccce1 100644 --- a/app/tle/models/dao/crew.py +++ b/app/tle/models/dao/crew.py @@ -1,5 +1,4 @@ from __future__ import annotations -import typing from django.core.exceptions import ValidationError from django.core.validators import ( @@ -9,14 +8,10 @@ ) from django.db import models, transaction +from users.models import User, UserBojLevel from tle.enums import Emoji -from tle.models.choices import BojUserLevel -from tle.models.dao.user import User from tle.models.dao.submission_language import SubmissionLanguage -if typing.TYPE_CHECKING: - import tle.models as _T - class EmojiValidator(BaseValidator): def __init__(self, message: str | None = None) -> None: @@ -30,12 +25,6 @@ def __call__(self, value) -> None: class Crew(models.Model): - if typing.TYPE_CHECKING: - applicants: models.ManyToManyField[_T.CrewApplicant] - members: models.ManyToManyField[_T.CrewMember] - activities: models.ManyToManyField[_T.CrewActivity] - submittable_languages: models.ManyToManyField[SubmissionLanguage] - name = models.CharField( max_length=20, unique=True, @@ -79,7 +68,7 @@ class Crew(models.Model): ) min_boj_level = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - choices=BojUserLevel.choices, + choices=UserBojLevel.choices, blank=True, null=True, default=None, @@ -102,12 +91,6 @@ class Crew(models.Model): updated_at = models.DateTimeField(auto_now=True) class field_name: - # related fields - APPLICANTS = 'applicants' - MEMBERS = 'members' - ACTIVITIES = 'activities' - SUBMITTABLE_LANGUAGES = 'submittable_languages' - # fields NAME = 'name' ICON = 'icon' MAX_MEMBERS = 'max_members' diff --git a/app/tle/models/dao/crew_activity.py b/app/tle/models/dao/crew_activity.py index 10a3aa1..f9963d4 100644 --- a/app/tle/models/dao/crew_activity.py +++ b/app/tle/models/dao/crew_activity.py @@ -7,18 +7,11 @@ from tle.models.dao.crew import Crew -if typing.TYPE_CHECKING: - import tle.models as _T - class CrewActivity(models.Model): - if typing.TYPE_CHECKING: - problems: models.ManyToOneRel[_T.CrewActivityProblem] - crew = models.ForeignKey( Crew, on_delete=models.CASCADE, - related_name=Crew.field_name.ACTIVITIES, help_text='크루를 입력해주세요.', ) name = models.TextField( @@ -32,9 +25,6 @@ class CrewActivity(models.Model): ) class field_name: - # related fields - PROBLEMS = 'problems' - # fields CREW = 'crew' NAME = 'name' START_AT = 'start_at' diff --git a/app/tle/models/dao/crew_activity_problem.py b/app/tle/models/dao/crew_activity_problem.py index f9ed9d1..6eadd97 100644 --- a/app/tle/models/dao/crew_activity_problem.py +++ b/app/tle/models/dao/crew_activity_problem.py @@ -6,24 +6,16 @@ from tle.models.dao.crew_activity import CrewActivity from tle.models.dao.problem import Problem -if typing.TYPE_CHECKING: - import tle.models as _T - class CrewActivityProblem(models.Model): - if typing.TYPE_CHECKING: - submissions: models.ManyToOneRel[_T.Submission] - activity = models.ForeignKey( CrewActivity, on_delete=models.CASCADE, - related_name=CrewActivity.field_name.PROBLEMS, help_text='활동을 입력해주세요.', ) problem = models.ForeignKey( Problem, on_delete=models.PROTECT, - related_name=Problem.field_name.ACTIVITY_PROBLEMS, help_text='문제를 입력해주세요.', ) order = models.IntegerField( diff --git a/app/tle/models/dao/crew_applicant.py b/app/tle/models/dao/crew_applicant.py index c2a1ad1..ff3bbaa 100644 --- a/app/tle/models/dao/crew_applicant.py +++ b/app/tle/models/dao/crew_applicant.py @@ -1,7 +1,7 @@ from django.db import models, transaction from django.utils import timezone -from tle.models.dao.user import User +from users.models import User from tle.models.dao.crew import Crew from tle.models.dao.crew_member import CrewMember @@ -10,13 +10,11 @@ class CrewApplicant(models.Model): crew = models.ForeignKey[Crew]( Crew, on_delete=models.CASCADE, - related_name=Crew.field_name.APPLICANTS, help_text='크루를 입력해주세요.', ) user = models.ForeignKey[User]( User, on_delete=models.CASCADE, - related_name=User.field_name.APPLICANTS, help_text='유저를 입력해주세요.', ) message = models.TextField( @@ -37,6 +35,7 @@ class CrewApplicant(models.Model): ) reviewed_by = models.ForeignKey[User]( User, + related_name='reviewed_applicants', on_delete=models.SET_NULL, null=True, blank=True, diff --git a/app/tle/models/dao/crew_member.py b/app/tle/models/dao/crew_member.py index 2b29174..91be94f 100644 --- a/app/tle/models/dao/crew_member.py +++ b/app/tle/models/dao/crew_member.py @@ -2,7 +2,7 @@ from django.db import models -from tle.models.dao.user import User +from users.models import User from tle.models.dao.crew import Crew @@ -10,13 +10,11 @@ class CrewMember(models.Model): crew = models.ForeignKey( Crew, on_delete=models.CASCADE, - related_name=Crew.field_name.MEMBERS, help_text='크루를 입력해주세요.', ) user = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.field_name.MEMBERS, help_text='유저를 입력해주세요.', ) is_captain = models.BooleanField( diff --git a/app/tle/models/dao/problem.py b/app/tle/models/dao/problem.py index b42cf70..2e1f8e5 100644 --- a/app/tle/models/dao/problem.py +++ b/app/tle/models/dao/problem.py @@ -2,18 +2,13 @@ from django.db import models -from tle.models.dao.user import User +from users.models import User if typing.TYPE_CHECKING: import tle.models as _T class Problem(models.Model): - if typing.TYPE_CHECKING: - analysis: models.OneToOneField[_T.ProblemAnalysis] - analysis_queue: models.ManyToManyField[_T.ProblemAnalysisQueue] - activity_problems: models.ManyToOneRel[_T.CrewActivityProblem] - title = models.CharField( max_length=100, help_text='문제 이름을 입력해주세요.', @@ -46,18 +41,12 @@ class Problem(models.Model): created_by = models.ForeignKey( User, on_delete=models.SET_NULL, - related_name=User.field_name.PROBLEMS, help_text='이 문제를 추가한 사용자를 입력해주세요.', null=True, ) updated_at = models.DateTimeField(auto_now=True) class field_name: - # related fields - ANALYSIS = 'analysis' - ANALYSIS_QUEUE = 'analysis_queue' - ACTIVITY_PROBLEMS = 'activity_problems' - # fields TITLE = 'title' LINK = 'link' DESCRIPTION = 'description' diff --git a/app/tle/models/dao/problem_analysis.py b/app/tle/models/dao/problem_analysis.py index 3d534e6..1b40a3b 100644 --- a/app/tle/models/dao/problem_analysis.py +++ b/app/tle/models/dao/problem_analysis.py @@ -9,7 +9,6 @@ class ProblemAnalysis(models.Model): problem = models.OneToOneField( Problem, on_delete=models.CASCADE, - related_name=Problem.field_name.ANALYSIS, help_text='문제를 입력해주세요.', ) difficulty = models.IntegerField( diff --git a/app/tle/models/dao/problem_analysis_queue.py b/app/tle/models/dao/problem_analysis_queue.py index 2fc8bb0..6003063 100644 --- a/app/tle/models/dao/problem_analysis_queue.py +++ b/app/tle/models/dao/problem_analysis_queue.py @@ -8,7 +8,6 @@ class ProblemAnalysisQueue(models.Model): problem = models.ForeignKey( Problem, on_delete=models.CASCADE, - related_name=Problem.field_name.ANALYSIS_QUEUE, help_text='문제를 입력해주세요.', ) analysis = models.OneToOneField( diff --git a/app/tle/models/dao/problem_tag.py b/app/tle/models/dao/problem_tag.py index 616d70c..6253a12 100644 --- a/app/tle/models/dao/problem_tag.py +++ b/app/tle/models/dao/problem_tag.py @@ -14,33 +14,24 @@ class ProblemTag(models.Model): parent = models.ForeignKey( 'self', on_delete=models.CASCADE, - related_name='children', - help_text=( - '부모 알고리즘 태그를 입력해주세요.' - ), + help_text='부모 알고리즘 태그를 입력해주세요.', null=True, blank=True, ) key = models.CharField( max_length=50, unique=True, - help_text=( - '알고리즘 태그 키를 입력해주세요. (최대 20자)' - ), + help_text='알고리즘 태그 키를 입력해주세요. (최대 20자)', ) name_ko = models.CharField( max_length=50, unique=True, - help_text=( - '알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)' - ), + help_text='알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)', ) name_en = models.CharField( max_length=50, unique=True, - help_text=( - '알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)' - ), + help_text='알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)', ) class field_name: diff --git a/app/tle/models/dao/submission.py b/app/tle/models/dao/submission.py index 49416dd..ebe0f76 100644 --- a/app/tle/models/dao/submission.py +++ b/app/tle/models/dao/submission.py @@ -1,30 +1,20 @@ -import typing - from django.db import models -from tle.models.dao.user import User +from users.models import User from tle.models.dao.crew_activity_problem import CrewActivityProblem from tle.models.dao.submission_language import SubmissionLanguage -if typing.TYPE_CHECKING: - import tle.models as _T - class Submission(models.Model): - if typing.TYPE_CHECKING: - comments: models.ManyToManyField[_T.SubmissionComment] - # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 activity_problem = models.ForeignKey( CrewActivityProblem, on_delete=models.PROTECT, - related_name=CrewActivityProblem.field_name.SUBMISSIONS, help_text='활동 문제를 입력해주세요.', ) user = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.field_name.SUBMISSIONS, help_text='유저를 입력해주세요.', ) code = models.TextField( @@ -46,9 +36,6 @@ class Submission(models.Model): updated_at = models.DateTimeField(auto_now=True) class field_name: - # related fields - COMMENTS = 'comments' - # fields ACTIVITY_PROBLEM = 'activity_problem' USER = 'user' CODE = 'code' diff --git a/app/tle/models/dao/submission_comment.py b/app/tle/models/dao/submission_comment.py index a66de85..06fcb6b 100644 --- a/app/tle/models/dao/submission_comment.py +++ b/app/tle/models/dao/submission_comment.py @@ -1,7 +1,7 @@ from django.core.validators import MinValueValidator from django.db import models -from tle.models.dao.user import User +from users.models import User from tle.models.dao.submission import Submission @@ -9,7 +9,6 @@ class SubmissionComment(models.Model): submission = models.ForeignKey( Submission, on_delete=models.CASCADE, - related_name=Submission.field_name.COMMENTS, help_text='제출을 입력해주세요.', ) content = models.TextField( @@ -36,13 +35,11 @@ class SubmissionComment(models.Model): created_by = models.ForeignKey( User, on_delete=models.CASCADE, - related_name=User.field_name.COMMENTS, help_text='유저를 입력해주세요.', ) updated_at = models.DateTimeField(auto_now=True) class field_name: - # fields SUBMISSION = 'submission' CONTENT = 'content' LINE_NUMBER_START = 'line_number_start' diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index 421f3fe..82754a5 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1,7 +1,3 @@ -from tle.serializers.user_detail import UserDetailSerializer -from tle.serializers.user_minimal import UserMinimalSerializer -from tle.serializers.user_sign_in import UserSignInSerializer - from tle.serializers.problem_detail import ProblemDetailSerializer from tle.serializers.problem_minimal import ProblemMinimalSerializer from tle.serializers.problem_tag import ProblemTagSerializer @@ -12,16 +8,10 @@ from tle.serializers.crew_joined import CrewJoinedSerializer -UserSerializer = UserDetailSerializer ProblemSerializer = ProblemDetailSerializer __all__ = ( - 'UserSerializer', - 'UserDetailSerializer', - 'UserMinimalSerializer', - 'UserSignInSerializer', - 'ProblemSerializer', 'ProblemDetailSerializer', 'ProblemMinimalSerializer', diff --git a/app/tle/serializers/crew_detail.py b/app/tle/serializers/crew_detail.py index 24d300d..7b6d0ea 100644 --- a/app/tle/serializers/crew_detail.py +++ b/app/tle/serializers/crew_detail.py @@ -2,9 +2,9 @@ from django.db.transaction import atomic from rest_framework.serializers import * +from users.serializers import UserMinimalSerializer from tle.models import Crew, SubmissionLanguage from tle.serializers.crew_member import CrewMemberSerializer -from tle.serializers.user_minimal import UserMinimalSerializer from tle.serializers.mixins import CurrentUserMixin, TagListMixin diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py index 242064a..def4adc 100644 --- a/app/tle/serializers/crew_member.py +++ b/app/tle/serializers/crew_member.py @@ -1,7 +1,7 @@ from rest_framework.serializers import * +from users.serializers import UserMinimalSerializer from tle.models import CrewMember -from tle.serializers.user_minimal import UserMinimalSerializer class CrewMemberSerializer(ModelSerializer): diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index 755fa82..e04cf82 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -1,5 +1,4 @@ from tle.serializers.mixins.current_user import CurrentUserMixin -from tle.serializers.mixins.boj_profile import BojProfileMixin from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin from tle.serializers.mixins.analysis_dict import AnalysisDictMixin from tle.serializers.mixins.tag_list import TagListMixin @@ -8,7 +7,6 @@ __all__ = ( 'CurrentUserMixin', - 'BojProfileMixin', 'DifficultyDictMixin', 'AnalysisDictMixin', 'RecentActivityMixin', diff --git a/app/tle/serializers/mixins/boj_profile.py b/app/tle/serializers/mixins/boj_profile.py deleted file mode 100644 index ff8b005..0000000 --- a/app/tle/serializers/mixins/boj_profile.py +++ /dev/null @@ -1,19 +0,0 @@ -from rest_framework.serializers import Serializer - -from tle.models import User -from tle.models.choices import BojUserLevel - - -class BojProfileMixin: - def boj_profile(self: Serializer, user: User) -> dict: - return { - 'username': user.boj_username, - 'profile_url': f'https://boj.kr/{user.boj_username}', - 'level': user.boj_level, - 'division': BojUserLevel.get_division(user.boj_level), - 'division_name_en': BojUserLevel.get_division_name(user.boj_level, lang='en'), - 'division_name_ko': BojUserLevel.get_division_name(user.boj_level, lang='ko'), - 'tier': BojUserLevel.get_tier(user.boj_level), - 'tier_name': BojUserLevel.get_tier_name(user.boj_level, arabic=True), - 'tier_updated_at': user.boj_level_updated_at, - } diff --git a/app/tle/serializers/mixins/current_user.py b/app/tle/serializers/mixins/current_user.py index 2697708..8d314c2 100644 --- a/app/tle/serializers/mixins/current_user.py +++ b/app/tle/serializers/mixins/current_user.py @@ -1,6 +1,6 @@ from rest_framework.serializers import Serializer -from tle.models import User +from users.models import User class CurrentUserMixin: diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py index 75a669c..426f001 100644 --- a/app/tle/serializers/mixins/tag_list.py +++ b/app/tle/serializers/mixins/tag_list.py @@ -4,8 +4,8 @@ from rest_framework.serializers import * +from users.models import UserBojLevel from tle.models import Crew -from tle.models.choices import BojUserLevel class TagType(enum.Enum): @@ -72,10 +72,10 @@ def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, 메시지의 마지막에는 bound_msg를 출력한다. """ - if BojUserLevel.get_tier(level) == bound_tier: - level_name = BojUserLevel.get_division_name(level, lang=lang) + if UserBojLevel.get_tier(level) == bound_tier: + level_name = UserBojLevel.get_division_name(level, lang=lang) else: - level_name = BojUserLevel.get_name(level, lang=lang, arabic=arabic) + level_name = UserBojLevel.get_name(level, lang=lang, arabic=arabic) return TagDict(key=None, name=f'{level_name} {bound_msg}', type=TagType.LEVEL.value) def _get_custom_tags(self, crew: Crew) -> typing.Iterable[TagDict]: diff --git a/app/tle/serializers/user_detail.py b/app/tle/serializers/user_detail.py deleted file mode 100644 index 9bf38a9..0000000 --- a/app/tle/serializers/user_detail.py +++ /dev/null @@ -1,37 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import User, UserManager -from tle.serializers.mixins import BojProfileMixin - - -class UserDetailSerializer(ModelSerializer, BojProfileMixin): - boj = SerializerMethodField(read_only=True) - - class Meta: - model = User - fields = [ - 'id', - User.field_name.EMAIL, - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - User.field_name.PASSWORD, - User.field_name.BOJ_USERNAME, - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - User.field_name.CREATED_AT: {'read_only': True}, - User.field_name.LAST_LOGIN: {'read_only': True}, - User.field_name.PASSWORD: {'write_only': True}, - User.field_name.BOJ_USERNAME: {'write_only': True}, - } - - def get_boj(self, obj: User) -> dict: - return self.boj_profile(obj) - - def create(self, validated_data): - user_manager: UserManager = User.objects - return user_manager.create_user(**validated_data) diff --git a/app/tle/serializers/user_minimal.py b/app/tle/serializers/user_minimal.py deleted file mode 100644 index 134e5e1..0000000 --- a/app/tle/serializers/user_minimal.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import User - - -class UserMinimalSerializer(ModelSerializer): - class Meta: - model = User - fields = [ - 'id', - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - ] - extra_kwargs = { - 'id': {'read_only': True}, - User.field_name.PROFILE_IMAGE: {'read_only': True}, - User.field_name.USERNAME: {'read_only': True}, - } diff --git a/app/tle/serializers/user_sign_in.py b/app/tle/serializers/user_sign_in.py deleted file mode 100644 index bcf2f73..0000000 --- a/app/tle/serializers/user_sign_in.py +++ /dev/null @@ -1,44 +0,0 @@ -from rest_framework.exceptions import PermissionDenied -from rest_framework.serializers import * - -from tle.models import User -from tle.serializers.mixins import BojProfileMixin - - -class UserSignInSerializer(ModelSerializer, BojProfileMixin): - email = EmailField(write_only=True, validators=None) - boj = SerializerMethodField() - - class Meta: - model = User - fields = [ - 'id', - User.field_name.EMAIL, - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - User.field_name.PASSWORD, - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - User.field_name.PROFILE_IMAGE: {'read_only': True}, - User.field_name.USERNAME: {'read_only': True}, - User.field_name.CREATED_AT: {'read_only': True}, - User.field_name.LAST_LOGIN: {'read_only': True}, - User.field_name.PASSWORD: {'write_only': True}, - } - - def get_boj(self, obj: User) -> dict: - return self.boj_profile(obj) - - def create(self, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def update(self, instance, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def save(self, **kwargs): - raise PermissionDenied('Cannot update user through this serializer') diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index a655bcb..3a505a1 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -4,23 +4,6 @@ urlpatterns = [ - path("auth/", include([ - path("signin", AuthViewSet.as_view({"post": "sign_in"})), - path("signup", AuthViewSet.as_view({"post": "sign_up"})), - path("signout", AuthViewSet.as_view({"get": "sign_out"})), - ])), - path("users/current", UserViewSet.as_view({"get": "current"})), - path("users/", include([ - path("search", UserViewSet.as_view({"get": "list"})), - path("/", include([ - path("profile", UserViewSet.as_view({ - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - })), - ])), - ])), path("problems/", include([ path("", ProblemViewSet.as_view({"post": "create"})), path("search", ProblemViewSet.as_view({"get": "list"})), diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index ecc9605..3469036 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1,12 +1,8 @@ -from tle.views.viewsets.auth_viewset import AuthViewSet -from tle.views.viewsets.user_viewset import UserViewSet from tle.views.viewsets.problem_viewset import ProblemViewSet from tle.views.viewsets.crew_viewset import CrewViewSet __all__ = ( - 'AuthViewSet', - 'UserViewSet', 'ProblemViewSet', 'CrewViewSet', ) diff --git a/app/tle/views/viewsets/auth_viewset.py b/app/tle/views/viewsets/auth_viewset.py deleted file mode 100644 index 0ecb803..0000000 --- a/app/tle/views/viewsets/auth_viewset.py +++ /dev/null @@ -1,72 +0,0 @@ -from http import HTTPStatus - -from django.contrib.auth import ( - authenticate, - login, - logout, -) -from rest_framework.exceptions import AuthenticationFailed -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.serializers import Serializer -from rest_framework.viewsets import GenericViewSet - -from tle.models import User -from tle.serializers import * -from tle.views.permissions import * - - -class AuthViewSet(GenericViewSet): - """사용자 계정과 관련된 API - - current: 현재 로그인한 사용자 정보 - signup: 사용자 등록(회원가입) - signin: 사용자 로그인 - signout: 사용자 로그아웃 - """ - queryset = User.objects.all() - permission_classes = [AllowAny] - - # Overrides - - def get_serializer_class(self): - if self.action == 'sign_in': - return UserSignInSerializer - else: - return UserSerializer - - # Helpers - - def get_validated_serializer(self, request: Request) -> Serializer: - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - return serializer - - def authenticate(self, request: Request) -> User: - serializer = self.get_validated_serializer(request) - email = serializer.validated_data['email'] - password = serializer.validated_data['password'] - user = authenticate(request, username=email, password=password) - if user is None: - raise AuthenticationFailed('Invalid email or password') - return user - - # Actions - - def sign_up(self, request: Request): - serializer = self.get_validated_serializer(request) - serializer.save() - return Response(serializer.data, status=HTTPStatus.CREATED) - - def sign_in(self, request: Request): - serializer = self.get_validated_serializer(request) - serializer.instance = self.authenticate(request) - login(request, serializer.instance) - return Response(serializer.data) - - def sign_out(self, request: Request): - logout(request) - return Response(status=HTTPStatus.OK) - - # TODO: 이메일 인증 - # TODO: 비밀번호 찾기 diff --git a/app/tle/views/viewsets/user_viewset.py b/app/tle/views/viewsets/user_viewset.py deleted file mode 100644 index 7554fe0..0000000 --- a/app/tle/views/viewsets/user_viewset.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.request import Request -from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet - -from tle.models import User -from tle.serializers import * -from tle.views.permissions import * - - -class UserViewSet(ModelViewSet): - queryset = User.objects.all() - permission_classes = [IsAdminUser] - serializer_class = UserSerializer - lookup_field = 'id' - - def current(self, request: Request): - serializer = self.get_serializer(instance=request.user) - return Response(serializer.data) diff --git a/app/users/__init__.py b/app/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/users/admin.py b/app/users/admin.py new file mode 100644 index 0000000..ecc3e53 --- /dev/null +++ b/app/users/admin.py @@ -0,0 +1,33 @@ +from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as BaseUserAdmin + +from users.models import User +from tle.models import CrewMember + + +@admin.register(User) +class UserAdmin(BaseUserAdmin): + fieldsets = None + list_display = [ + User.field_name.USERNAME, + User.field_name.EMAIL, + User.field_name.BOJ_USERNAME, + User.field_name.BOJ_LEVEL, + 'get_crews', + User.field_name.IS_ACTIVE, + User.field_name.IS_STAFF, + User.field_name.IS_SUPERUSER, + User.field_name.CREATED_AT, + User.field_name.BOJ_LEVEL_UPDATED_AT, + ] + + @admin.display(description='captains / members') + def get_crews(self, user: User) -> str: + n_captains = CrewMember.objects.filter(**{ + CrewMember.field_name.USER: user, + CrewMember.field_name.IS_CAPTAIN: True, + }).count() + n_members = CrewMember.objects.filter(**{ + CrewMember.field_name.USER: user, + }).count() + return f'{n_captains} / {n_members}' diff --git a/app/users/apps.py b/app/users/apps.py new file mode 100644 index 0000000..88f7b17 --- /dev/null +++ b/app/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" diff --git a/app/tle/backends.py b/app/users/backends.py similarity index 95% rename from app/tle/backends.py rename to app/users/backends.py index 663ed86..8c1c90e 100644 --- a/app/tle/backends.py +++ b/app/users/backends.py @@ -3,7 +3,7 @@ from django.contrib.auth.backends import ModelBackend from django.http import HttpRequest -from tle.models import User +from users.models import User logger = logging.getLogger(__name__) diff --git a/app/users/migrations/__init__.py b/app/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py new file mode 100644 index 0000000..2efb280 --- /dev/null +++ b/app/users/models/__init__.py @@ -0,0 +1,10 @@ +from users.models.user import User +from users.models.user_manager import UserManager +from users.models.user_boj_level import UserBojLevel + + +__all__ = ( + 'User', + 'UserManager', + 'UserBojLevel', +) diff --git a/app/tle/models/dao/user.py b/app/users/models/user.py similarity index 57% rename from app/tle/models/dao/user.py rename to app/users/models/user.py index f4d08e9..2d6d005 100644 --- a/app/tle/models/dao/user.py +++ b/app/users/models/user.py @@ -1,58 +1,18 @@ from __future__ import annotations -import typing -from django.contrib.auth.models import ( - AbstractBaseUser, - BaseUserManager, - PermissionsMixin, -) +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin from django.db import models from django.utils import timezone -from tle.models.choices import BojUserLevel +from users.models.user_manager import UserManager +from users.models.user_boj_level import UserBojLevel -if typing.TYPE_CHECKING: - import tle.models as _T - -def get_profile_image_path(instance: User, filename: str) -> str: - return f'user/profile/{instance.pk}/{filename}' - - -class UserManager(BaseUserManager): - model: typing.Callable[..., User] - - def create(self, **kwargs): - return self.create_user(**kwargs) - - def create_user(self, email, username, password=None, **extra_fields): - if not email: - raise ValueError('The Email field must be set') - if not username: - raise ValueError('The Username field must be set') - email = self.normalize_email(email) - user = self.model( - email=email, - username=username, - **extra_fields - ) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, username, password=None, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - return self.create_user(email, username, password, **extra_fields) +def get_profile_image_path(user: User, filename: str) -> str: + return f'user/profile/{user.pk}/{filename}' class User(AbstractBaseUser, PermissionsMixin): - problems: models.ManyToManyField[_T.Problem] - applicants: models.ManyToManyField[_T.CrewApplicant] - members: models.ManyToManyField[_T.CrewMember] - submissions: models.ManyToManyField[_T.Submission] - comments: models.ManyToManyField[_T.SubmissionComment] - profile_image = models.ImageField( help_text='프로필 이미지', upload_to=get_profile_image_path, @@ -71,7 +31,7 @@ class User(AbstractBaseUser, PermissionsMixin): ) boj_level = models.IntegerField( help_text='백준 티어', - choices=BojUserLevel.choices, + choices=UserBojLevel.choices, null=True, blank=True, default=None, @@ -106,13 +66,6 @@ class User(AbstractBaseUser, PermissionsMixin): REQUIRED_FIELDS = ['username'] class field_name: - # related fields - PROBLEMS = 'problems' - APPLICANTS = 'applicants' - MEMBERS = 'members' - SUBMISSIONS = 'submissions' - COMMENTS = 'comments' - # fields PROFILE_IMAGE = 'profile_image' BOJ_USERNAME = 'boj_username' BOJ_LEVEL = 'boj_level' diff --git a/app/users/models/user_boj_level.py b/app/users/models/user_boj_level.py new file mode 100644 index 0000000..27d5b32 --- /dev/null +++ b/app/users/models/user_boj_level.py @@ -0,0 +1,65 @@ +from django.db import models + + +DIVISION_NAMES = { + 'ko': ['난이도를 매길 수 없음', '브론즈', '실버', '골드', '플래티넘', '다이아몬드', '루비'], + 'en': ['Unrated', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Ruby'], +} +ARABIC_NUMERALS = ['', 'I', 'II', 'III', 'IV', 'V'] + + +class UserBojLevel(models.IntegerChoices): + U = 0, 'Unrated' + B5 = 1, '브론즈 5' + B4 = 2, '브론즈 4' + B3 = 3, '브론즈 3' + B2 = 4, '브론즈 2' + B1 = 5, '브론즈 1' + S5 = 6, '실버 5' + S4 = 7, '실버 4' + S3 = 8, '실버 3' + S2 = 9, '실버 2' + S1 = 10, '실버 1' + G5 = 11, '골드 5' + G4 = 12, '골드 4' + G3 = 13, '골드 3' + G2 = 14, '골드 2' + G1 = 15, '골드 1' + P5 = 16, '플래티넘 5' + P4 = 17, '플래티넘 4' + P3 = 18, '플래티넘 3' + P2 = 19, '플래티넘 2' + P1 = 20, '플래티넘 1' + D5 = 21, '다이아몬드 5' + D4 = 22, '다이아몬드 4' + D3 = 23, '다이아몬드 3' + D2 = 24, '다이아몬드 2' + D1 = 25, '다이아몬드 1' + R5 = 26, '루비 5' + R4 = 27, '루비 4' + R3 = 28, '루비 3' + R2 = 29, '루비 2' + R1 = 30, '루비 1' + M = 31, '마스터' + + def get_division(self) -> int: + if self == self.U: + return 0 + return ((self.value-1) // 5)+1 + + def get_division_name(self, lang='en') -> str: + return DIVISION_NAMES[lang][self.get_division()] + + def get_tier(self) -> int: + if self == self.U: + return 0 + return 5 - ((self.value-1) % 5) + + def get_tier_name(self, arabic=True) -> str: + tier = self.get_tier() + if arabic: + return ARABIC_NUMERALS[tier] + return str(tier) + + def get_name(self, lang='en', arabic=True) -> str: + return f'{self.get_division_name(lang=lang)} {self.get_tier_name(arabic=arabic)}' diff --git a/app/users/models/user_manager.py b/app/users/models/user_manager.py new file mode 100644 index 0000000..a490bb6 --- /dev/null +++ b/app/users/models/user_manager.py @@ -0,0 +1,30 @@ +import typing + +from django.contrib.auth.models import AbstractUser, BaseUserManager + + +class UserManager(BaseUserManager): + model: typing.Callable[..., AbstractUser] + + def create(self, **kwargs): + return self.create_user(**kwargs) + + def create_user(self, email, username, password=None, **extra_fields): + if not email: + raise ValueError('The Email field must be set') + if not username: + raise ValueError('The Username field must be set') + email = self.normalize_email(email) + user = self.model( + email=email, + username=username, + **extra_fields + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, username, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + return self.create_user(email, username, password, **extra_fields) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py new file mode 100644 index 0000000..e295d46 --- /dev/null +++ b/app/users/serializers/__init__.py @@ -0,0 +1,73 @@ +from rest_framework.serializers import * + +from users.models import User +from users.serializers.fields import UserBojField +from users.serializers.mixins import ReadOnlySerializerMixin + + +class UserSignInSerializer(ModelSerializer, ReadOnlySerializerMixin): + email = EmailField(write_only=True, validators=None) + boj = UserBojField(read_only=True) + + class Meta: + model = User + fields = [ + 'id', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'boj': {'read_only': True}, + User.field_name.PROFILE_IMAGE: {'read_only': True}, + User.field_name.USERNAME: {'read_only': True}, + User.field_name.CREATED_AT: {'read_only': True}, + User.field_name.LAST_LOGIN: {'read_only': True}, + User.field_name.PASSWORD: {'write_only': True}, + } + + +class UserDetailSerializer(ModelSerializer, ReadOnlySerializerMixin): + boj = UserBojField(read_only=True) + + class Meta: + model = User + fields = [ + 'id', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, + User.field_name.BOJ_USERNAME, + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + ] + extra_kwargs = { + 'id': {'read_only': True}, + 'boj': {'read_only': True}, + User.field_name.CREATED_AT: {'read_only': True}, + User.field_name.LAST_LOGIN: {'read_only': True}, + User.field_name.PASSWORD: {'write_only': True}, + User.field_name.BOJ_USERNAME: {'write_only': True}, + } + + +class UserMinimalSerializer(ModelSerializer, ReadOnlySerializerMixin): + class Meta: + model = User + fields = [ + 'id', + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + ] + extra_kwargs = { + 'id': {'read_only': True}, + User.field_name.PROFILE_IMAGE: {'read_only': True}, + User.field_name.USERNAME: {'read_only': True}, + } diff --git a/app/users/serializers/fields.py b/app/users/serializers/fields.py new file mode 100644 index 0000000..289d832 --- /dev/null +++ b/app/users/serializers/fields.py @@ -0,0 +1,21 @@ +from users.models import User, UserBojLevel +from users.serializers.mixins import ReadOnlyField + + +class UserBojField(ReadOnlyField): + def to_representation(self, user: User): + if user.boj_username is None: + user_boj_level = UserBojLevel.U + else: + user_boj_level = UserBojLevel(user.boj_level) + return { + 'username': user.boj_username, + 'profile_url': f'https://boj.kr/{user.boj_username}', + 'level': user_boj_level.value, + 'division': user_boj_level.get_division(), + 'division_name_en': user_boj_level.get_division_name(lang='en'), + 'division_name_ko': user_boj_level.get_division_name(lang='ko'), + 'tier': user_boj_level.get_tier(), + 'tier_name': user_boj_level.get_tier_name(arabic=True), + 'tier_updated_at': user.boj_level_updated_at, + } diff --git a/app/users/serializers/mixins.py b/app/users/serializers/mixins.py new file mode 100644 index 0000000..de7ad99 --- /dev/null +++ b/app/users/serializers/mixins.py @@ -0,0 +1,21 @@ +from rest_framework.exceptions import PermissionDenied +from rest_framework.serializers import Field + + +class ReadOnlySerializerMixin: + def create(self, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def update(self, instance, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def save(self, **kwargs): + raise PermissionDenied('Cannot update user through this serializer') + + +class ReadOnlyField(Field): + def get_attribute(self, instance): + return instance + + def to_internal_value(self, data): + raise PermissionDenied('This field is read-only') diff --git a/app/users/views.py b/app/users/views.py new file mode 100644 index 0000000..a054714 --- /dev/null +++ b/app/users/views.py @@ -0,0 +1,83 @@ +from typing import Callable + +from django.contrib.auth import authenticate, login, logout +from rest_framework import ( + mixins, + permissions, + status, +) +from rest_framework.exceptions import AuthenticationFailed +from rest_framework.generics import GenericAPIView +from rest_framework.response import Response +from rest_framework.serializers import Serializer + +from users.models import User, UserManager +from users.serializers import UserDetailSerializer, UserSignInSerializer + + +class SignUp(mixins.CreateModelMixin, + GenericAPIView): + """사용자 등록(회원가입) API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserDetailSerializer + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + def perform_create(self, serializer: Serializer): + user_manager: UserManager = User.objects + user = user_manager.create_user(**serializer.validated_data) + serializer.instance = user + + +class SignIn(mixins.RetrieveModelMixin, + GenericAPIView): + """사용자 로그인 API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserSignInSerializer + + get_serializer: Callable[..., Serializer] + + def get_object(self) -> User: + serializer = self.get_serializer(data=self.request.data) + serializer.is_valid(raise_exception=True) + # 사용자 인증을 위한 email과 password를 추출 + email = serializer.validated_data['email'] + password = serializer.validated_data['password'] + # 사용자 인증 + user = authenticate(self.request, username=email, password=password) + # 사용자 인증 실패 시 예외 발생 + if user is None: + raise AuthenticationFailed('Invalid email or password') + # 사용자 인증 성공 시 (세션) 로그인 + login(self.request, user) + return user + + def post(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + +class SignOut(GenericAPIView): + """사용자 로그아웃 API""" + + permission_classes = [permissions.IsAuthenticated] + + def get(self, request, *args, **kwargs): + logout(request) + return Response(status=status.HTTP_204_NO_CONTENT) + + +class CurrentUser(mixins.RetrieveModelMixin, + GenericAPIView): + """현재 로그인한 사용자 정보 API""" + + permission_classes = [permissions.IsAuthenticated] + serializer_class = UserDetailSerializer + + def get_object(self) -> User: + return self.request.user + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) From 32598c49df04eacfffb02f7b0048d21de0b9c404 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 26 Jul 2024 23:51:38 +0900 Subject: [PATCH 315/552] =?UTF-8?q?refactor:=20`Problem`=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20`problems`=20?= =?UTF-8?q?=EC=95=B1=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 1 + app/app/urls.py | 6 + .../analysis/llm => problems}/__init__.py | 0 app/problems/admin.py | 154 ++++++++++++++++++ app/problems/apps.py | 6 + .../enums/unit.py => problems/constants.py} | 0 app/problems/migrations/__init__.py | 0 app/problems/models/__init__.py | 19 +++ .../analysis => problems/models}/dto.py | 0 .../models/dao => problems/models}/problem.py | 8 +- .../models}/problem_analysis.py | 9 +- .../models}/problem_analysis_queue.py | 16 +- .../models}/problem_difficulty.py | 29 +--- .../dao => problems/models}/problem_tag.py | 9 - app/problems/serializers/__init__.py | 70 ++++++++ app/problems/serializers/fields.py | 88 ++++++++++ app/problems/serializers/mixins.py | 35 ++++ app/problems/services/__init__.py | 0 app/problems/services/analysis/__init__.py | 44 +++++ .../services/analysis/analyser.py | 5 +- .../services/analysis/llm/gemini.py | 6 +- .../services/analysis/llm/gpt.py | 6 +- app/problems/views.py | 58 +++++++ app/tle/admin/__init__.py | 124 +------------- app/tle/enums/__init__.py | 2 - app/tle/models/__init__.py | 8 - app/tle/models/choices/__init__.py | 1 - app/tle/models/dao/__init__.py | 10 -- app/tle/models/dao/crew_activity_problem.py | 4 +- app/tle/serializers/__init__.py | 12 -- app/tle/serializers/mixins/__init__.py | 4 - app/tle/serializers/mixins/analysis_dict.py | 63 ------- app/tle/serializers/mixins/difficulty_dict.py | 14 -- app/tle/serializers/problem_detail.py | 62 ------- app/tle/serializers/problem_minimal.py | 26 --- app/tle/serializers/problem_tag.py | 14 -- app/tle/services/analysis/__init__.py | 9 - app/tle/views/urls.py | 12 -- app/tle/views/viewsets/__init__.py | 2 - app/tle/views/viewsets/problem_viewset.py | 25 --- 40 files changed, 515 insertions(+), 446 deletions(-) rename app/{tle/services/analysis/llm => problems}/__init__.py (100%) create mode 100644 app/problems/admin.py create mode 100644 app/problems/apps.py rename app/{tle/enums/unit.py => problems/constants.py} (100%) create mode 100644 app/problems/migrations/__init__.py create mode 100644 app/problems/models/__init__.py rename app/{tle/services/analysis => problems/models}/dto.py (100%) rename app/{tle/models/dao => problems/models}/problem.py (92%) rename app/{tle/models/dao => problems/models}/problem_analysis.py (85%) rename app/{tle/models/dao => problems/models}/problem_analysis_queue.py (66%) rename app/{tle/models/choices => problems/models}/problem_difficulty.py (53%) rename app/{tle/models/dao => problems/models}/problem_tag.py (84%) create mode 100644 app/problems/serializers/__init__.py create mode 100644 app/problems/serializers/fields.py create mode 100644 app/problems/serializers/mixins.py create mode 100644 app/problems/services/__init__.py create mode 100644 app/problems/services/analysis/__init__.py rename app/{tle => problems}/services/analysis/analyser.py (81%) rename app/{tle => problems}/services/analysis/llm/gemini.py (56%) rename app/{tle => problems}/services/analysis/llm/gpt.py (56%) create mode 100644 app/problems/views.py delete mode 100644 app/tle/models/choices/__init__.py delete mode 100644 app/tle/serializers/mixins/analysis_dict.py delete mode 100644 app/tle/serializers/mixins/difficulty_dict.py delete mode 100644 app/tle/serializers/problem_detail.py delete mode 100644 app/tle/serializers/problem_minimal.py delete mode 100644 app/tle/serializers/problem_tag.py delete mode 100644 app/tle/services/analysis/__init__.py delete mode 100644 app/tle/views/viewsets/problem_viewset.py diff --git a/app/app/settings.py b/app/app/settings.py index 6681790..5feb821 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -44,6 +44,7 @@ "django.contrib.staticfiles", "rest_framework", "users", + "problems", "tle", ] diff --git a/app/app/urls.py b/app/app/urls.py index 6e77a25..18365ae 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -20,6 +20,7 @@ from django.urls import include, path from users.views import SignIn, SignUp, SignOut, CurrentUser +from problems.views import ProblemCreate, ProblemDetail, ProblemSearch import tle.views.urls urlpatterns = [ @@ -31,6 +32,11 @@ path("signout", SignOut.as_view()), ])), path("users/current", CurrentUser.as_view()), + path("problems/", include([ + path("", ProblemCreate.as_view()), + path("search", ProblemSearch.as_view()), + path("/detail", ProblemDetail.as_view()), + ])), *tle.views.urls.urlpatterns, ])), ] diff --git a/app/tle/services/analysis/llm/__init__.py b/app/problems/__init__.py similarity index 100% rename from app/tle/services/analysis/llm/__init__.py rename to app/problems/__init__.py diff --git a/app/problems/admin.py b/app/problems/admin.py new file mode 100644 index 0000000..165fbbc --- /dev/null +++ b/app/problems/admin.py @@ -0,0 +1,154 @@ +from textwrap import shorten + +from django.contrib import admin, messages +from django.db.models import QuerySet +from django.utils.translation import ngettext + +from problems.models import ( + Problem, + ProblemAnalysis, + ProblemAnalysisQueue, + ProblemTag, +) +from problems.services.analysis import AnalysingService +from users.models import User + + +@admin.register(Problem) +class ProblemModelAdmin(admin.ModelAdmin): + list_display = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY, + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, + ] + search_fields = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + ] + ordering = ['-'+Problem.field_name.CREATED_AT] + actions = [ + 'analyze', + 'add_to_analysis_queue', + 'set_creator', + ] + + @admin.action(description="Set admin(you) as creator for selected problems") + def set_creator(self, request, queryset: QuerySet[Problem]): + updated = queryset.update(**{ + Problem.field_name.CREATED_BY: request.user, + }) + self.message_user( + request, + ngettext( + "%d problem was successfully updated.", + "%d problems were successfully updated.", + updated, + ) + % updated, + messages.SUCCESS, + ) + + @admin.action(description="Add selected problems to analysis queue") + def add_to_analysis_queue(self, request, queryset: QuerySet[Problem]): + for problem in queryset: + ProblemAnalysisQueue.objects.create(**{ + ProblemAnalysisQueue.field_name.PROBLEM: problem, + }) + self.message_user( + request, + ngettext( + "%d problem was successfully added to analysis queue.", + "%d problems were successfully added to analysis queue.", + queryset.count(), + ) + % queryset.count(), + messages.SUCCESS, + ) + + @admin.action(description="Analyze selected problems") + def analyze(self, request, queryset: QuerySet[Problem]): + analysing_service = AnalysingService.get_instance() + for problem in queryset: + analysis = analysing_service.analyze(problem) + analysis.save() + self.message_user( + request, + ngettext( + "%d problem was successfully analyzed.", + "%d problems were successfully analyzed.", + queryset.count(), + ) + % queryset.count(), + messages.SUCCESS, + ) + + +@admin.register(ProblemAnalysis) +class ProblemAnalysisModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysis.field_name.PROBLEM, + ProblemAnalysis.field_name.DIFFICULTY, + 'get_timecomplexity', + 'get_tags', + 'get_hint', + ProblemAnalysis.field_name.CREATED_AT, + ] + search_fields = [ + ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ProblemAnalysis.field_name.TIME_COMPLEXITY, + ] + ordering = ['-'+ProblemAnalysis.field_name.CREATED_AT] + + @admin.display(description='Big-O') + def get_timecomplexity(self, obj: ProblemAnalysis) -> str: + return f'O({obj.time_complexity})' + + @admin.display(description='Tags') + def get_tags(self, obj: ProblemAnalysis) -> str: + def get_tag_keys(): + for tag in obj.tags.all(): + yield f'#{tag.key}' + return ' '.join(get_tag_keys()) + + @admin.display(description='Hint (Steps, Verbose)') + def get_hint(self, obj: ProblemAnalysis) -> str: + hints_in_a_row = ', '.join(obj.hint) + return len(obj.hint), shorten(hints_in_a_row, width=32) + + +@admin.register(ProblemAnalysisQueue) +class ProblemAnalysisQueueModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysisQueue.field_name.PROBLEM, + ProblemAnalysisQueue.field_name.ANALYSIS, + ProblemAnalysisQueue.field_name.IS_ANALYZING, + 'get_is_analyzed', + ProblemAnalysisQueue.field_name.CREATED_AT, + ] + search_fields = [ + ProblemAnalysisQueue.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ] + ordering = [ + ProblemAnalysisQueue.field_name.CREATED_AT, + ] + + @admin.display(description='Is analyzed', boolean=True) + def get_is_analyzed(self, obj: ProblemAnalysisQueue) -> bool: + return obj.analysis is not None + + +@admin.register(ProblemTag) +class ProblemTagModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ProblemTag.field_name.PARENT, + ] + search_fields = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ] + ordering = [ProblemTag.field_name.KEY] diff --git a/app/problems/apps.py b/app/problems/apps.py new file mode 100644 index 0000000..f35f38e --- /dev/null +++ b/app/problems/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ProblemsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "problems" diff --git a/app/tle/enums/unit.py b/app/problems/constants.py similarity index 100% rename from app/tle/enums/unit.py rename to app/problems/constants.py diff --git a/app/problems/migrations/__init__.py b/app/problems/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py new file mode 100644 index 0000000..49c29a6 --- /dev/null +++ b/app/problems/models/__init__.py @@ -0,0 +1,19 @@ +from problems.models.dto import ProblemDTO, ProblemAnalysisDTO + +from problems.models.problem import Problem +from problems.models.problem_analysis import ProblemAnalysis +from problems.models.problem_analysis_queue import ProblemAnalysisQueue +from problems.models.problem_difficulty import ProblemDifficulty +from problems.models.problem_tag import ProblemTag + + +__all__ = ( + 'ProblemDTO', + 'ProblemAnalysisDTO', + + 'Problem', + 'ProblemAnalysis', + 'ProblemAnalysisQueue', + 'ProblemDifficulty', + 'ProblemTag', +) diff --git a/app/tle/services/analysis/dto.py b/app/problems/models/dto.py similarity index 100% rename from app/tle/services/analysis/dto.py rename to app/problems/models/dto.py diff --git a/app/tle/models/dao/problem.py b/app/problems/models/problem.py similarity index 92% rename from app/tle/models/dao/problem.py rename to app/problems/models/problem.py index 2e1f8e5..6483cec 100644 --- a/app/tle/models/dao/problem.py +++ b/app/problems/models/problem.py @@ -1,12 +1,7 @@ -import typing - from django.db import models from users.models import User -if typing.TYPE_CHECKING: - import tle.models as _T - class Problem(models.Model): title = models.CharField( @@ -65,6 +60,5 @@ def __str__(self) -> str: return f'[{self.pk} : {self.title}]' def save(self, *args, **kwargs) -> None: - from tle.models import ProblemAnalysisQueue super().save(*args, **kwargs) - ProblemAnalysisQueue.append(self) + # TODO: Add to ProblemAnalysisQueue diff --git a/app/tle/models/dao/problem_analysis.py b/app/problems/models/problem_analysis.py similarity index 85% rename from app/tle/models/dao/problem_analysis.py rename to app/problems/models/problem_analysis.py index 1b40a3b..de00a28 100644 --- a/app/tle/models/dao/problem_analysis.py +++ b/app/problems/models/problem_analysis.py @@ -1,8 +1,8 @@ from django.db import models -from tle.models.choices import ProblemDifficulty -from tle.models.dao.problem import Problem -from tle.models.dao.problem_tag import ProblemTag +from problems.models.problem import Problem +from problems.models.problem_difficulty import ProblemDifficulty +from problems.models.problem_tag import ProblemTag class ProblemAnalysis(models.Model): @@ -47,5 +47,8 @@ class field_name: HINT = 'hint' CREATED_AT = 'created_at' + class Meta: + verbose_name_plural = 'Problem Analyses' + def __str__(self): return f'[Analyse of {self.problem}]' \ No newline at end of file diff --git a/app/tle/models/dao/problem_analysis_queue.py b/app/problems/models/problem_analysis_queue.py similarity index 66% rename from app/tle/models/dao/problem_analysis_queue.py rename to app/problems/models/problem_analysis_queue.py index 6003063..86cca69 100644 --- a/app/tle/models/dao/problem_analysis_queue.py +++ b/app/problems/models/problem_analysis_queue.py @@ -1,7 +1,7 @@ from django.db import models -from tle.models.dao.problem import Problem -from tle.models.dao.problem_analysis import ProblemAnalysis +from problems.models.problem import Problem +from problems.models.problem_analysis import ProblemAnalysis class ProblemAnalysisQueue(models.Model): @@ -31,15 +31,3 @@ class field_name: class Meta: ordering = ['created_at'] - - @classmethod - def append(cls, problem: Problem): - return cls.objects.create(**{ - cls.field_name.PROBLEM: problem, - }) - - @classmethod - def extend(cls, problems: models.QuerySet[Problem]): - # TODO: Do bulk_create() - for problem in problems: - cls.append(problem) diff --git a/app/tle/models/choices/problem_difficulty.py b/app/problems/models/problem_difficulty.py similarity index 53% rename from app/tle/models/choices/problem_difficulty.py rename to app/problems/models/problem_difficulty.py index ba39828..6db9fe6 100644 --- a/app/tle/models/choices/problem_difficulty.py +++ b/app/problems/models/problem_difficulty.py @@ -2,32 +2,21 @@ NAMES = { - 'ko': { - 0: '분석 중', - 1: '쉬움', - 2: '보통', - 3: '어려움', - }, - 'en': { - 0: 'UNDER ANALYSIS', - 1: 'EASY', - 2: 'NORMAL', - 3: 'HARD', - }, + 'ko': ['분석 중', '쉬움', '보통', '어려움'], + 'en': ['UNDER ANALYSIS', 'EASY', 'NORMAL', 'HARD'], } class ProblemDifficulty(models.IntegerChoices): - @classmethod - def get_name(cls, value: int, lang='ko') -> str: + UNDER_ANALYSIS = 0, '분석 중' + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' + + def get_name(self, lang='ko') -> str: if lang not in NAMES: raise ValueError( f'Invalid language: {lang}, ', f'choose from {NAMES.keys()}' ) - return NAMES[lang][value] - - UNDER_ANALYSIS = 0, '분석 중' - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' + return NAMES[lang][self.value] diff --git a/app/tle/models/dao/problem_tag.py b/app/problems/models/problem_tag.py similarity index 84% rename from app/tle/models/dao/problem_tag.py rename to app/problems/models/problem_tag.py index 6253a12..2f49f3e 100644 --- a/app/tle/models/dao/problem_tag.py +++ b/app/problems/models/problem_tag.py @@ -1,16 +1,7 @@ -import typing - from django.db import models -if typing.TYPE_CHECKING: - import tle.models as _T - class ProblemTag(models.Model): - if typing.TYPE_CHECKING: - parent: models.ManyToManyField[_T.ProblemTag] - children: models.ManyToManyField[_T.ProblemTag] - parent = models.ForeignKey( 'self', on_delete=models.CASCADE, diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py new file mode 100644 index 0000000..a0ebf52 --- /dev/null +++ b/app/problems/serializers/__init__.py @@ -0,0 +1,70 @@ +from rest_framework.serializers import CurrentUserDefault, ModelSerializer + +from problems.models import Problem +from problems.serializers.fields import ( + AnalysisField, + MemoryLimitField, + TimeLimitField, + DifficultyField, +) +from problems.serializers.mixins import ReadOnlySerializerMixin +from users.serializers import UserMinimalSerializer + + +class ProblemDetailSerializer(ModelSerializer): + analysis = AnalysisField(read_only=True) + memory_limit = MemoryLimitField(read_only=True) + time_limit = TimeLimitField(read_only=True) + created_by = UserMinimalSerializer(read_only=True) + + class Meta: + model = Problem + fields = [ + 'id', + 'analysis', + 'memory_limit', + 'time_limit', + Problem.field_name.TITLE, + Problem.field_name.LINK, + Problem.field_name.DESCRIPTION, + Problem.field_name.INPUT_DESCRIPTION, + Problem.field_name.OUTPUT_DESCRIPTION, + Problem.field_name.MEMORY_LIMIT_MEGABYTE, + Problem.field_name.TIME_LIMIT_SECOND, + Problem.field_name.CREATED_AT, + Problem.field_name.CREATED_BY, + Problem.field_name.UPDATED_AT, + ] + read_only_fields = [ + 'id', + 'analysis', + 'memory_limit', + 'time_limit', + Problem.field_name.CREATED_AT, + Problem.field_name.CREATED_BY, + Problem.field_name.UPDATED_AT, + ] + extra_kwargs = { + Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, + Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, + } + + def create(self, validated_data): + validated_data[Problem.field_name.CREATED_BY] = CurrentUserDefault()( + self) + return super().create(validated_data) + + +class ProblemMinimalSerializer(ModelSerializer, ReadOnlySerializerMixin): + difficulty = DifficultyField(read_only=True) + + class Meta: + model = Problem + fields = [ + 'id', + Problem.field_name.TITLE, + 'difficulty', + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, + ] + read_only_fields = ['__all__'] diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py new file mode 100644 index 0000000..6e18886 --- /dev/null +++ b/app/problems/serializers/fields.py @@ -0,0 +1,88 @@ +from rest_framework.serializers import ModelSerializer + +from problems.constants import Unit +from problems.models import Problem, ProblemDifficulty, ProblemTag +from problems.serializers.mixins import ReadOnlyFieldMixin, AnalysisMixin + + +class MemoryLimitField(ReadOnlyFieldMixin): + def to_representation(self, problem: Problem): + return { + "value": problem.memory_limit_megabyte, + "unit": { + "name_ko": Unit.MEGA_BYTE.name_ko, + "name_en": Unit.MEGA_BYTE.name_en, + "abbr": Unit.MEGA_BYTE.abbr, + }, + } + + +class TimeLimitField(ReadOnlyFieldMixin): + def to_representation(self, problem: Problem): + return { + "value": problem.time_limit_second, + "unit": { + "name_ko": Unit.SECOND.name_ko, + "name_en": Unit.SECOND.name_en, + "abbr": Unit.SECOND.abbr, + }, + } + + +class DifficultyField(ReadOnlyFieldMixin, AnalysisMixin): + def to_representation(self, problem: Problem): + if (analysis := self.get_analysis(problem)) is None: + difficulty = ProblemDifficulty.UNDER_ANALYSIS + else: + difficulty = ProblemDifficulty(analysis.difficulty) + return { + "name_ko": difficulty.get_name(lang='ko'), + "name_en": difficulty.get_name(lang='en'), + 'value': difficulty.value, + } + + +class AnalysisField(ReadOnlyFieldMixin, AnalysisMixin): + def to_representation(self, problem: Problem): + if (analysis := self.get_analysis(problem)) is None: + difficulty = ProblemDifficulty.UNDER_ANALYSIS + difficulty_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" + time_complexity = '' + time_complexity_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" + hint = [] + tags = [] + is_analyzed = False + else: + difficulty = ProblemDifficulty(analysis.difficulty) + difficulty_description = "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" + time_complexity = analysis.time_complexity + time_complexity_description = "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" + hint = analysis.hint + tags = ProblemTagSerializer(analysis.tags, many=True).data + is_analyzed = True + return { + 'difficulty': { + "name_ko": difficulty.get_name(lang='ko'), + "name_en": difficulty.get_name(lang='en'), + 'value': difficulty.value, + 'description': difficulty_description, + }, + 'time_complexity': { + 'value': time_complexity, + 'description': time_complexity_description, + }, + 'hint': hint, + 'tags': tags, + 'is_analyzed': is_analyzed, + } + + +class ProblemTagSerializer(ModelSerializer): + class Meta: + model = ProblemTag + fields = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ] + read_only_fields = ['__all__'] diff --git a/app/problems/serializers/mixins.py b/app/problems/serializers/mixins.py new file mode 100644 index 0000000..f161fba --- /dev/null +++ b/app/problems/serializers/mixins.py @@ -0,0 +1,35 @@ +from typing import Optional + +from rest_framework.exceptions import PermissionDenied +from rest_framework.serializers import Field + +from problems.models import Problem, ProblemAnalysis + + +class ReadOnlySerializerMixin: + def create(self, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def update(self, instance, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def save(self, **kwargs): + raise PermissionDenied('Cannot update user through this serializer') + + +class ReadOnlyFieldMixin(Field): + def get_attribute(self, instance): + return instance + + def to_internal_value(self, data): + raise PermissionDenied('This field is read-only') + + +class AnalysisMixin: + def get_analysis(self, problem: Problem) -> Optional[ProblemAnalysis]: + try: + return ProblemAnalysis.objects.filter(**{ + ProblemAnalysis.field_name.PROBLEM: problem, + }).last() + except ProblemAnalysis.DoesNotExist: + return None diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/services/analysis/__init__.py b/app/problems/services/analysis/__init__.py new file mode 100644 index 0000000..cfc978a --- /dev/null +++ b/app/problems/services/analysis/__init__.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from problems.models import Problem, ProblemAnalysis, ProblemDTO +from problems.services.analysis.analyser import ProblemAnalyser +from problems.services.analysis.llm.gemini import GeminiProblemAnalyser + + +__all__ = ( + 'ProblemAnalyser', + 'get_analyser', +) + + +class AnalysingService: + instance = None + analyzer_class = GeminiProblemAnalyser + + @classmethod + def get_instance(cls) -> AnalysingService: + if not cls.instance: + cls.instance = cls() + return cls.instance + + def get_analyzer(self) -> ProblemAnalyser: + return self.analyzer_class() + + def analyze(self, problem: Problem) -> ProblemAnalysis: + problem_dto = ProblemDTO( + title=problem.title, + description=problem.description, + input_description=problem.input_description, + output_description=problem.output_description, + memory_limit_megabyte=problem.memory_limit_megabyte, + time_limit_second=problem.time_limit_second, + ) + analyzer = self.get_analyzer() + analysis_dto = analyzer.analyze(problem_dto) + return ProblemAnalysis(**{ + ProblemAnalysis.field_name.PROBLEM: problem, + ProblemAnalysis.field_name.DIFFICULTY: analysis_dto.difficulty, + ProblemAnalysis.field_name.TAGS: analysis_dto.tags, + ProblemAnalysis.field_name.TIME_COMPLEXITY: analysis_dto.time_complexity, + ProblemAnalysis.field_name.HINT: analysis_dto.hint, + }) diff --git a/app/tle/services/analysis/analyser.py b/app/problems/services/analysis/analyser.py similarity index 81% rename from app/tle/services/analysis/analyser.py rename to app/problems/services/analysis/analyser.py index 0c5aa32..1110321 100644 --- a/app/tle/services/analysis/analyser.py +++ b/app/problems/services/analysis/analyser.py @@ -1,4 +1,7 @@ -from tle.services.analysis.dto import * +from problems.models import ( + ProblemDTO, + ProblemAnalysisDTO, +) class ProblemAnalyser: diff --git a/app/tle/services/analysis/llm/gemini.py b/app/problems/services/analysis/llm/gemini.py similarity index 56% rename from app/tle/services/analysis/llm/gemini.py rename to app/problems/services/analysis/llm/gemini.py index ac3b9d0..74f0b0d 100644 --- a/app/tle/services/analysis/llm/gemini.py +++ b/app/problems/services/analysis/llm/gemini.py @@ -1,4 +1,8 @@ -from tle.services.analysis import * +from problems.services.analysis.analyser import ( + ProblemAnalyser, + ProblemDTO, + ProblemAnalysisDTO, +) class GeminiProblemAnalyser(ProblemAnalyser): diff --git a/app/tle/services/analysis/llm/gpt.py b/app/problems/services/analysis/llm/gpt.py similarity index 56% rename from app/tle/services/analysis/llm/gpt.py rename to app/problems/services/analysis/llm/gpt.py index 81012e7..22e2cbf 100644 --- a/app/tle/services/analysis/llm/gpt.py +++ b/app/problems/services/analysis/llm/gpt.py @@ -1,4 +1,8 @@ -from tle.services.analysis import * +from problems.services.analysis.analyser import ( + ProblemAnalyser, + ProblemDTO, + ProblemAnalysisDTO, +) class GPTProblemAnalyser(ProblemAnalyser): diff --git a/app/problems/views.py b/app/problems/views.py new file mode 100644 index 0000000..673a55b --- /dev/null +++ b/app/problems/views.py @@ -0,0 +1,58 @@ +from rest_framework import mixins +from rest_framework import permissions +from rest_framework.generics import GenericAPIView + +from problems.models import Problem +from problems.serializers import ProblemDetailSerializer, ProblemMinimalSerializer + + +class ProblemCreate(mixins.CreateModelMixin, + GenericAPIView): + """문제 생성 API""" + + queryset = Problem.objects.all() + permission_classes = [permissions.IsAuthenticated] + serializer_class = ProblemDetailSerializer + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + +class ProblemDetail(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericAPIView): + """문제 상세 조회, 수정, 삭제 API""" + + queryset = Problem.objects.all() + permission_classes = [permissions.IsAuthenticated] + serializer_class = ProblemDetailSerializer + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class ProblemSearch(mixins.ListModelMixin, + GenericAPIView): + """문제 검색 API""" + + permission_classes = [permissions.IsAuthenticated] + serializer_class = ProblemMinimalSerializer + + def get_queryset(self): + return Problem.objects.filter(**{ + Problem.field_name.CREATED_BY: self.request.user, + }) + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index 7c3a83c..3743d1b 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -1,8 +1,4 @@ -from textwrap import shorten - -from django.contrib import admin, messages -from django.db.models import QuerySet -from django.utils.translation import ngettext +from django.contrib import admin from users.models import User from tle.models import * @@ -15,124 +11,6 @@ SubmissionComment, ]) - - -@admin.register(Problem) -class ProblemModelAdmin(admin.ModelAdmin): - list_display = [ - Problem.field_name.TITLE, - Problem.field_name.CREATED_BY, - Problem.field_name.CREATED_AT, - Problem.field_name.UPDATED_AT, - ] - search_fields = [ - Problem.field_name.TITLE, - Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - ] - ordering = ['-'+Problem.field_name.CREATED_AT] - actions = [ - 'set_creator', - 'add_to_analysis_queue', - ] - - @admin.action(description="set admin(you) as creator for selected problems") - def set_creator(self, request, queryset: QuerySet[Problem]): - updated = queryset.update(created_by=request.user) - self.message_user( - request, - ngettext( - "%d problem was successfully updated.", - "%d problems were successfully updated.", - updated, - ) - % updated, - messages.SUCCESS, - ) - - @admin.action(description="add selected problems to analysis queue") - def add_to_analysis_queue(self, request, queryset: QuerySet[Problem]): - ProblemAnalysisQueue.extend(queryset) - self.message_user( - request, - ngettext( - "%d problem was successfully added to analysis queue.", - "%d problems were successfully added to analysis queue.", - queryset.count(), - ) - % queryset.count(), - messages.SUCCESS, - ) - - -@admin.register(ProblemAnalysis) -class ProblemAnalysisModelAdmin(admin.ModelAdmin): - list_display = [ - ProblemAnalysis.field_name.PROBLEM, - ProblemAnalysis.field_name.DIFFICULTY, - 'get_timecomplexity', - 'get_tags', - 'get_hint', - ProblemAnalysis.field_name.CREATED_AT, - ] - search_fields = [ - ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, - ProblemAnalysis.field_name.TIME_COMPLEXITY, - ] - ordering = ['-'+ProblemAnalysis.field_name.CREATED_AT] - - @admin.display(description='Big-O') - def get_timecomplexity(self, obj: ProblemAnalysis) -> str: - return f'O({obj.time_complexity})' - - @admin.display(description='Tags') - def get_tags(self, obj: ProblemAnalysis) -> str: - def get_tag_keys(): - for tag in obj.tags.all(): - yield f'#{tag.key}' - return ' '.join(get_tag_keys()) - - @admin.display(description='Hint (Steps, Verbose)') - def get_hint(self, obj: ProblemAnalysis) -> str: - return len(obj.hint), shorten(', '.join(obj.hint), width=32) - - -@admin.register(ProblemAnalysisQueue) -class ProblemAnalysisQueueModelAdmin(admin.ModelAdmin): - list_display = [ - ProblemAnalysisQueue.field_name.PROBLEM, - ProblemAnalysisQueue.field_name.ANALYSIS, - ProblemAnalysisQueue.field_name.IS_ANALYZING, - 'get_is_analyzed', - ProblemAnalysisQueue.field_name.CREATED_AT, - ] - search_fields = [ - ProblemAnalysisQueue.field_name.PROBLEM+'__'+Problem.field_name.TITLE, - ] - ordering = [ - ProblemAnalysisQueue.field_name.CREATED_AT, - ] - - @admin.display(description='Is Analyzed', boolean=True) - def get_is_analyzed(self, obj: ProblemAnalysisQueue) -> bool: - return obj.analysis is not None - - -@admin.register(ProblemTag) -class ProblemTagModelAdmin(admin.ModelAdmin): - list_display = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, - ProblemTag.field_name.PARENT, - ] - search_fields = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, - ] - ordering = [ProblemTag.field_name.KEY] - - @admin.register(Crew) class CrewModelAdmin(admin.ModelAdmin): list_display = [ diff --git a/app/tle/enums/__init__.py b/app/tle/enums/__init__.py index edf09c9..9514643 100644 --- a/app/tle/enums/__init__.py +++ b/app/tle/enums/__init__.py @@ -1,8 +1,6 @@ from tle.enums.emoji import Emoji -from tle.enums.unit import Unit __all__ = ( 'Emoji', - 'Unit', ) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index 956d408..4319c7a 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,21 +1,13 @@ -from tle.models import choices from tle.models.dao import * __all__ = ( - 'choices', - 'Crew', 'CrewActivity', 'CrewActivityProblem', 'CrewApplicant', 'CrewMember', - 'Problem', - 'ProblemAnalysis', - 'ProblemAnalysisQueue', - 'ProblemTag', - 'Submission', 'SubmissionComment', 'SubmissionLanguage', diff --git a/app/tle/models/choices/__init__.py b/app/tle/models/choices/__init__.py deleted file mode 100644 index f3399d3..0000000 --- a/app/tle/models/choices/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from tle.models.choices.problem_difficulty import ProblemDifficulty diff --git a/app/tle/models/dao/__init__.py b/app/tle/models/dao/__init__.py index 0233eea..12e2763 100644 --- a/app/tle/models/dao/__init__.py +++ b/app/tle/models/dao/__init__.py @@ -4,11 +4,6 @@ from tle.models.dao.crew_applicant import CrewApplicant from tle.models.dao.crew_member import CrewMember -from tle.models.dao.problem import Problem -from tle.models.dao.problem_analysis import ProblemAnalysis -from tle.models.dao.problem_analysis_queue import ProblemAnalysisQueue -from tle.models.dao.problem_tag import ProblemTag - from tle.models.dao.submission import Submission from tle.models.dao.submission_comment import SubmissionComment from tle.models.dao.submission_language import SubmissionLanguage @@ -21,11 +16,6 @@ 'CrewApplicant', 'CrewMember', - 'Problem', - 'ProblemAnalysis', - 'ProblemAnalysisQueue', - 'ProblemTag', - 'Submission', 'SubmissionComment', 'SubmissionLanguage', diff --git a/app/tle/models/dao/crew_activity_problem.py b/app/tle/models/dao/crew_activity_problem.py index 6eadd97..86cdb6c 100644 --- a/app/tle/models/dao/crew_activity_problem.py +++ b/app/tle/models/dao/crew_activity_problem.py @@ -1,10 +1,8 @@ -import typing - from django.core.validators import MinValueValidator from django.db import models from tle.models.dao.crew_activity import CrewActivity -from tle.models.dao.problem import Problem +from problems.models.problem import Problem class CrewActivityProblem(models.Model): diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py index 82754a5..7259b1d 100644 --- a/app/tle/serializers/__init__.py +++ b/app/tle/serializers/__init__.py @@ -1,22 +1,10 @@ -from tle.serializers.problem_detail import ProblemDetailSerializer -from tle.serializers.problem_minimal import ProblemMinimalSerializer -from tle.serializers.problem_tag import ProblemTagSerializer - from tle.serializers.crew_detail import CrewDetailSerializer from tle.serializers.crew_member import CrewMemberSerializer from tle.serializers.crew_recruiting import CrewRecruitingSerializer from tle.serializers.crew_joined import CrewJoinedSerializer -ProblemSerializer = ProblemDetailSerializer - - __all__ = ( - 'ProblemSerializer', - 'ProblemDetailSerializer', - 'ProblemMinimalSerializer', - 'ProblemTagSerializer', - 'CrewDetailSerializer', 'CrewMemberSerializer', 'CrewRecruitingSerializer', diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py index e04cf82..4018ad2 100644 --- a/app/tle/serializers/mixins/__init__.py +++ b/app/tle/serializers/mixins/__init__.py @@ -1,14 +1,10 @@ from tle.serializers.mixins.current_user import CurrentUserMixin -from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin -from tle.serializers.mixins.analysis_dict import AnalysisDictMixin from tle.serializers.mixins.tag_list import TagListMixin from tle.serializers.mixins.recent_activity import RecentActivityMixin __all__ = ( 'CurrentUserMixin', - 'DifficultyDictMixin', - 'AnalysisDictMixin', 'RecentActivityMixin', 'TagListMixin', ) diff --git a/app/tle/serializers/mixins/analysis_dict.py b/app/tle/serializers/mixins/analysis_dict.py deleted file mode 100644 index cf63b66..0000000 --- a/app/tle/serializers/mixins/analysis_dict.py +++ /dev/null @@ -1,63 +0,0 @@ -import typing - -from rest_framework.serializers import * - -from tle.models import Problem, ProblemAnalysis -from tle.models.choices import ProblemDifficulty -from tle.serializers.problem_tag import ProblemTagSerializer -from tle.serializers.mixins.difficulty_dict import DifficultyDictMixin - - -class AnalysisDictMixin(DifficultyDictMixin): - def analysis_dict(self, problem: Problem) -> typing.Dict: - try: - return self._analysis_dict(problem.analysis) - except ProblemAnalysis.DoesNotExist: - return self._analysis_dict_default() - - def _analysis_dict(self, analysis: ProblemAnalysis): - return { - 'difficulty': { - "name_ko": ProblemDifficulty.get_name(analysis.difficulty, lang='ko'), - "name_en": ProblemDifficulty.get_name(analysis.difficulty, lang='en'), - 'value': analysis.difficulty, - 'description': ( - "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준" - " [이 기능은 추가될 예정이 없습니다]" - ), - }, - 'time_complexity': { - 'value': analysis.time_complexity, - 'description': ( - "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요." - " [이 기능은 추가될 예정이 없습니다]" - ), - }, - 'hint': analysis.hint, - 'tags': ProblemTagSerializer(analysis.tags, many=True).data, - 'is_analyzed': True, - } - - def _analysis_dict_default(self): - default_difficulty = 0 # Under analysis - return { - 'difficulty': { - "name_ko": ProblemDifficulty.get_name(default_difficulty, lang='ko'), - "name_en": ProblemDifficulty.get_name(default_difficulty, lang='en'), - 'value': default_difficulty, - 'description': ( - "AI가 분석을 진행하고 있어요!" - " [이 기능은 추가될 예정이 없습니다]" - ), - }, - 'time_complexity': { - 'value': '', - 'description': ( - "AI가 분석을 진행하고 있어요!" - " [이 기능은 추가될 예정이 없습니다]" - ), - }, - 'hint': [], - 'tags': [], - 'is_analyzed': False, - } diff --git a/app/tle/serializers/mixins/difficulty_dict.py b/app/tle/serializers/mixins/difficulty_dict.py deleted file mode 100644 index 5ae3771..0000000 --- a/app/tle/serializers/mixins/difficulty_dict.py +++ /dev/null @@ -1,14 +0,0 @@ -import typing - -from rest_framework.serializers import * - -from tle.models.choices import ProblemDifficulty - - -class DifficultyDictMixin: - def difficulty_dict(self, difficulty: ProblemDifficulty) -> typing.Dict: - return { - "name_ko": ProblemDifficulty.get_name(difficulty.value, lang='ko'), - "name_en": ProblemDifficulty.get_name(difficulty.value, lang='en'), - "value": difficulty.value, - } diff --git a/app/tle/serializers/problem_detail.py b/app/tle/serializers/problem_detail.py deleted file mode 100644 index de2383c..0000000 --- a/app/tle/serializers/problem_detail.py +++ /dev/null @@ -1,62 +0,0 @@ -from rest_framework.serializers import * - -from tle.enums import Unit -from tle.models import Problem -from tle.serializers.mixins import AnalysisDictMixin - - -class ProblemDetailSerializer(ModelSerializer, AnalysisDictMixin): - analysis = SerializerMethodField() - memory_limit = SerializerMethodField() - time_limit = SerializerMethodField() - - class Meta: - model = Problem - fields = [ - 'id', - 'analysis', - 'memory_limit', - 'time_limit', - Problem.field_name.TITLE, - Problem.field_name.LINK, - Problem.field_name.DESCRIPTION, - Problem.field_name.INPUT_DESCRIPTION, - Problem.field_name.OUTPUT_DESCRIPTION, - Problem.field_name.MEMORY_LIMIT_MEGABYTE, - Problem.field_name.TIME_LIMIT_SECOND, - Problem.field_name.CREATED_AT, - Problem.field_name.UPDATED_AT, - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'analysis': {'read_only': True}, - 'memory_limit': {'read_only': True}, - 'time_limit': {'read_only': True}, - Problem.field_name.CREATED_AT: {'read_only': True}, - Problem.field_name.UPDATED_AT: {'read_only': True}, - Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, - Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, - } - - def get_analysis(self, obj: Problem): - return self.analysis_dict(obj) - - def get_memory_limit(self, obj: Problem): - return { - "value": obj.memory_limit_megabyte, - "unit": { - "name_ko": Unit.MEGA_BYTE.name_ko, - "name_en": Unit.MEGA_BYTE.name_en, - "abbr": Unit.MEGA_BYTE.abbr, - }, - } - - def get_time_limit(self, obj: Problem): - return { - "value": obj.time_limit_second, - "unit": { - "name_ko": Unit.SECOND.name_ko, - "name_en": Unit.SECOND.name_en, - "abbr": Unit.SECOND.abbr, - }, - } diff --git a/app/tle/serializers/problem_minimal.py b/app/tle/serializers/problem_minimal.py deleted file mode 100644 index f5244b7..0000000 --- a/app/tle/serializers/problem_minimal.py +++ /dev/null @@ -1,26 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import Problem, ProblemAnalysis -from tle.models.choices import ProblemDifficulty -from tle.serializers.mixins import DifficultyDictMixin - - -class ProblemMinimalSerializer(ModelSerializer, DifficultyDictMixin): - difficulty = SerializerMethodField() - - class Meta: - model = Problem - fields = [ - 'id', - Problem.field_name.TITLE, - 'difficulty', - Problem.field_name.CREATED_AT, - Problem.field_name.UPDATED_AT, - ] - read_only_fields = ['__all__'] - - def get_difficulty(self, obj: Problem): - try: - return self.difficulty_dict(ProblemDifficulty(obj.analysis.difficulty)) - except ProblemAnalysis.DoesNotExist: - return self.difficulty_dict(ProblemDifficulty(0)) diff --git a/app/tle/serializers/problem_tag.py b/app/tle/serializers/problem_tag.py deleted file mode 100644 index 99a0976..0000000 --- a/app/tle/serializers/problem_tag.py +++ /dev/null @@ -1,14 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import ProblemTag - - -class ProblemTagSerializer(ModelSerializer): - class Meta: - model = ProblemTag - fields = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, - ] - read_only_fields = ['__all__'] diff --git a/app/tle/services/analysis/__init__.py b/app/tle/services/analysis/__init__.py deleted file mode 100644 index 68d75e0..0000000 --- a/app/tle/services/analysis/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -from .analyser import ProblemAnalyser -from .dto import ProblemDTO, ProblemAnalysisDTO - - -__all__ = ( - 'ProblemAnalyser', - 'ProblemDTO', - 'ProblemAnalysisDTO', -) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py index 3a505a1..89c9104 100644 --- a/app/tle/views/urls.py +++ b/app/tle/views/urls.py @@ -4,18 +4,6 @@ urlpatterns = [ - path("problems/", include([ - path("", ProblemViewSet.as_view({"post": "create"})), - path("search", ProblemViewSet.as_view({"get": "list"})), - path("/", include([ - path("detail", ProblemViewSet.as_view({ - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - })) - ])), - ])), path("crews/", include([ path("", CrewViewSet.as_view({"post": "create"})), path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py index 3469036..cab8f79 100644 --- a/app/tle/views/viewsets/__init__.py +++ b/app/tle/views/viewsets/__init__.py @@ -1,8 +1,6 @@ -from tle.views.viewsets.problem_viewset import ProblemViewSet from tle.views.viewsets.crew_viewset import CrewViewSet __all__ = ( - 'ProblemViewSet', 'CrewViewSet', ) diff --git a/app/tle/views/viewsets/problem_viewset.py b/app/tle/views/viewsets/problem_viewset.py deleted file mode 100644 index 8879e76..0000000 --- a/app/tle/views/viewsets/problem_viewset.py +++ /dev/null @@ -1,25 +0,0 @@ -from rest_framework.viewsets import ModelViewSet - -from tle.models import Problem -from tle.serializers import * -from tle.views.permissions import * - - -class ProblemViewSet(ModelViewSet): - """문제 태그 목록 조회 + 생성 기능""" - permission_classes = [IsAuthenticated] - lookup_field = 'id' - - # TODO: 내가 만든 문제만 수정할 수 있도록 변경 - - def get_serializer_class(self): - if self.action in ['list']: - return ProblemMinimalSerializer - return ProblemSerializer - - def get_queryset(self): - user = self.request.user - if user.is_staff: - return Problem.objects.all() - # TODO: 내가 가입한 크루에서 풀어본 문제도 조회할 수 있도록 수정 - return Problem.objects.filter(created_by=user) From 40a00920c945c3bac0f34c6d29169b4e23e9fd31 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:12:01 +0900 Subject: [PATCH 316/552] =?UTF-8?q?refactor:=20`Crew`=20=EA=B4=80=EB=A0=A8?= =?UTF-8?q?=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20`crews`=20=EC=95=B1=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 1 + app/app/urls.py | 10 +- app/crews/__init__.py | 14 ++ app/crews/admin.py | 117 ++++++++++++ app/crews/apps.py | 6 + app/crews/enums/__init__.py | 8 + app/{tle => crews}/enums/emoji.py | 0 app/crews/enums/programming_language.py | 29 +++ .../views => crews/migrations}/__init__.py | 0 app/crews/models/__init__.py | 20 ++ app/crews/models/choices.py | 15 ++ app/crews/models/crew.py | 87 +++++++++ .../dao => crews/models}/crew_activity.py | 3 +- .../models}/crew_activity_problem.py | 4 +- .../dao => crews/models}/crew_applicant.py | 5 +- .../dao => crews/models}/crew_member.py | 11 +- app/crews/models/crew_submittable_language.py | 25 +++ app/crews/serializers/__init__.py | 119 ++++++++++++ app/crews/serializers/fields.py | 174 ++++++++++++++++++ app/crews/serializers/mixins.py | 18 ++ app/crews/services.py | 66 +++++++ app/crews/validators.py | 15 ++ app/crews/views.py | 79 ++++++++ app/tle/admin/__init__.py | 90 --------- app/tle/enums/__init__.py | 6 - app/tle/models/__init__.py | 10 +- app/tle/models/dao/__init__.py | 22 --- app/tle/models/dao/crew.py | 161 ---------------- app/tle/models/dao/submission.py | 5 +- app/tle/models/dao/submission_language.py | 32 ---- app/tle/serializers/__init__.py | 12 -- app/tle/serializers/crew_detail.py | 101 ---------- app/tle/serializers/crew_joined.py | 24 --- app/tle/serializers/crew_member.py | 17 -- app/tle/serializers/crew_recruiting.py | 55 ------ app/tle/serializers/mixins/__init__.py | 10 - app/tle/serializers/mixins/current_user.py | 8 - app/tle/serializers/mixins/recent_activity.py | 41 ----- app/tle/serializers/mixins/tag_list.py | 83 --------- app/tle/views/permissions.py | 23 --- app/tle/views/urls.py | 20 -- app/tle/views/viewsets/__init__.py | 6 - app/tle/views/viewsets/crew_viewset.py | 42 ----- app/users/admin.py | 2 +- 44 files changed, 811 insertions(+), 785 deletions(-) create mode 100644 app/crews/__init__.py create mode 100644 app/crews/admin.py create mode 100644 app/crews/apps.py create mode 100644 app/crews/enums/__init__.py rename app/{tle => crews}/enums/emoji.py (100%) create mode 100644 app/crews/enums/programming_language.py rename app/{tle/views => crews/migrations}/__init__.py (100%) create mode 100644 app/crews/models/__init__.py create mode 100644 app/crews/models/choices.py create mode 100644 app/crews/models/crew.py rename app/{tle/models/dao => crews/models}/crew_activity.py (97%) rename app/{tle/models/dao => crews/models}/crew_activity_problem.py (89%) rename app/{tle/models/dao => crews/models}/crew_applicant.py (96%) rename app/{tle/models/dao => crews/models}/crew_member.py (75%) create mode 100644 app/crews/models/crew_submittable_language.py create mode 100644 app/crews/serializers/__init__.py create mode 100644 app/crews/serializers/fields.py create mode 100644 app/crews/serializers/mixins.py create mode 100644 app/crews/services.py create mode 100644 app/crews/validators.py create mode 100644 app/crews/views.py delete mode 100644 app/tle/enums/__init__.py delete mode 100644 app/tle/models/dao/__init__.py delete mode 100644 app/tle/models/dao/crew.py delete mode 100644 app/tle/models/dao/submission_language.py delete mode 100644 app/tle/serializers/__init__.py delete mode 100644 app/tle/serializers/crew_detail.py delete mode 100644 app/tle/serializers/crew_joined.py delete mode 100644 app/tle/serializers/crew_member.py delete mode 100644 app/tle/serializers/crew_recruiting.py delete mode 100644 app/tle/serializers/mixins/__init__.py delete mode 100644 app/tle/serializers/mixins/current_user.py delete mode 100644 app/tle/serializers/mixins/recent_activity.py delete mode 100644 app/tle/serializers/mixins/tag_list.py delete mode 100644 app/tle/views/permissions.py delete mode 100644 app/tle/views/urls.py delete mode 100644 app/tle/views/viewsets/__init__.py delete mode 100644 app/tle/views/viewsets/crew_viewset.py diff --git a/app/app/settings.py b/app/app/settings.py index 5feb821..44c8391 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -45,6 +45,7 @@ "rest_framework", "users", "problems", + "crews", "tle", ] diff --git a/app/app/urls.py b/app/app/urls.py index 18365ae..ffcb3df 100644 --- a/app/app/urls.py +++ b/app/app/urls.py @@ -21,7 +21,8 @@ from users.views import SignIn, SignUp, SignOut, CurrentUser from problems.views import ProblemCreate, ProblemDetail, ProblemSearch -import tle.views.urls +from crews.views import CrewCreate, CrewDetail, CrewRecruiting, CrewJoined + urlpatterns = [ path("admin/", admin.site.urls), @@ -37,7 +38,12 @@ path("search", ProblemSearch.as_view()), path("/detail", ProblemDetail.as_view()), ])), - *tle.views.urls.urlpatterns, + path("crews/", include([ + path("", CrewCreate.as_view()), + path("recruiting", CrewRecruiting.as_view()), + path("my", CrewJoined.as_view()), + path("/detail", CrewDetail.as_view()), + ])), ])), ] diff --git a/app/crews/__init__.py b/app/crews/__init__.py new file mode 100644 index 0000000..2fc51dd --- /dev/null +++ b/app/crews/__init__.py @@ -0,0 +1,14 @@ +from enum import Enum + + +class ProgrammingLanguage(Enum): + PYTHON = 'python' + JAVASCRIPT = 'javascript' + TYPESCRIPT = 'typescript' + JAVA = 'java' + CSHARP = 'csharp' + RUBY = 'ruby' + PHP = 'php' + GO = 'go' + SWIFT = 'swift' + KOTLIN = 'kotlin' \ No newline at end of file diff --git a/app/crews/admin.py b/app/crews/admin.py new file mode 100644 index 0000000..9fe56ce --- /dev/null +++ b/app/crews/admin.py @@ -0,0 +1,117 @@ +from django.contrib import admin + +from crews.models import ( + Crew, + CrewActivity, + CrewActivityProblem, + CrewApplicant, + CrewMember, + CrewSubmittableLanguage, +) +from users.models import User + + +admin.site.register([ + CrewActivityProblem, + CrewApplicant, +]) + + +@admin.register(Crew) +class CrewModelAdmin(admin.ModelAdmin): + list_display = [ + 'get_display_name', + 'get_captain', + 'get_members', + 'get_applicants', + 'get_activities', + Crew.field_name.IS_ACTIVE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.CREATED_AT, + ] + search_fields = [ + Crew.field_name.NAME, + Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + Crew.field_name.ICON, + ] + + @admin.display(description='Display Name') + def get_display_name(self, crew: Crew): + return f'{crew.icon} {crew.name}' + + @admin.display(description='Captain') + def get_captain(self, obj: Crew): + return CrewMember.objects.get(**{ + CrewMember.field_name.CREW: obj, + CrewMember.field_name.IS_CAPTAIN: True, + }) + + @admin.display(description='Members') + def get_members(self, crew: Crew): + members_count = CrewMember.objects.filter(**{ + CrewMember.field_name.CREW: crew, + }).count() + return f'{members_count} / {crew.max_members}' + + @admin.display(description='Applicants') + def get_applicants(self, obj: Crew): + return CrewApplicant.objects.filter(**{ + CrewApplicant.field_name.CREW: obj, + }).count() + + @admin.display(description='Activities') + def get_activities(self, obj: Crew): + return CrewActivity.objects.filter(**{ + CrewActivity.field_name.CREW: obj, + }).count() + + +@admin.register(CrewMember) +class CrewMemberModelAdmin(admin.ModelAdmin): + list_display = [ + CrewMember.field_name.USER, + CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, + CrewMember.field_name.CREATED_AT, + ] + search_fields = [ + CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, + CrewMember.field_name.USER+'__'+User.field_name.USERNAME, + ] + ordering = [ + CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, + ] + + +@admin.register(CrewActivity) +class CrewActivityModelAdmin(admin.ModelAdmin): + list_display = [ + CrewActivity.field_name.CREW, + CrewActivity.field_name.NAME, + CrewActivity.field_name.START_AT, + CrewActivity.field_name.END_AT, + 'nth', + 'is_opened', + 'is_closed', + ] + search_fields = [ + CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, + CrewActivity.field_name.NAME, + ] + + +@admin.register(CrewSubmittableLanguage) +class CrewSubmittableLanguageModelAdmin(admin.ModelAdmin): + list_display = [ + CrewSubmittableLanguage.field_name.CREW, + CrewSubmittableLanguage.field_name.LANGUAGE, + ] + search_fields = [ + CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, + CrewSubmittableLanguage.field_name.LANGUAGE, + ] + ordering = [ + CrewSubmittableLanguage.field_name.CREW, + CrewSubmittableLanguage.field_name.LANGUAGE, + ] diff --git a/app/crews/apps.py b/app/crews/apps.py new file mode 100644 index 0000000..1e88651 --- /dev/null +++ b/app/crews/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CrewsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "crews" diff --git a/app/crews/enums/__init__.py b/app/crews/enums/__init__.py new file mode 100644 index 0000000..a922ffb --- /dev/null +++ b/app/crews/enums/__init__.py @@ -0,0 +1,8 @@ +from crews.enums.emoji import Emoji +from crews.enums.programming_language import ProgrammingLanguage + + +__all__ = ( + 'Emoji', + 'ProgrammingLanguage', +) diff --git a/app/tle/enums/emoji.py b/app/crews/enums/emoji.py similarity index 100% rename from app/tle/enums/emoji.py rename to app/crews/enums/emoji.py diff --git a/app/crews/enums/programming_language.py b/app/crews/enums/programming_language.py new file mode 100644 index 0000000..c3bc45e --- /dev/null +++ b/app/crews/enums/programming_language.py @@ -0,0 +1,29 @@ +from dataclasses import dataclass +from enum import Enum + + +@dataclass +class _ProgrammingLanguage: + key: str + name: str + extension: str + + def to_choice(self): + return self.key, self.name + + +class ProgrammingLanguage(Enum): + # TLE에서 허용중인 언어 + NODE_JS = _ProgrammingLanguage('nodejs', 'Node.js', '.js') + KOTLIN = _ProgrammingLanguage('kotlin', 'Kotlin', '.kt') + SWIFT = _ProgrammingLanguage('swift', 'Swift', '.swift') + CPP = _ProgrammingLanguage('cpp', 'C++', '.cpp') + JAVA = _ProgrammingLanguage('java', 'Java', '.java') + PYTHON = _ProgrammingLanguage('python', 'Python', '.py') + C = _ProgrammingLanguage('c', 'C', '.c') + + # 아직 지원하지 않는 언어 + JAVASCRIPT = _ProgrammingLanguage('javascript', 'JavaScript', '.js') + CSHARP = _ProgrammingLanguage('csharp', 'C#', '.cs') + RUBY = _ProgrammingLanguage('ruby', 'Ruby', '.rb') + PHP = _ProgrammingLanguage('php', 'PHP', '.php') diff --git a/app/tle/views/__init__.py b/app/crews/migrations/__init__.py similarity index 100% rename from app/tle/views/__init__.py rename to app/crews/migrations/__init__.py diff --git a/app/crews/models/__init__.py b/app/crews/models/__init__.py new file mode 100644 index 0000000..d8a60d2 --- /dev/null +++ b/app/crews/models/__init__.py @@ -0,0 +1,20 @@ +from crews.models.crew import Crew +from crews.models.crew_activity import CrewActivity +from crews.models.crew_activity_problem import CrewActivityProblem +from crews.models.crew_applicant import CrewApplicant +from crews.models.crew_member import CrewMember +from crews.models.crew_submittable_language import CrewSubmittableLanguage + +from crews.models.choices import ProgrammingLanguageChoices + + +__all__ = ( + 'Crew', + 'CrewActivity', + 'CrewActivityProblem', + 'CrewApplicant', + 'CrewMember', + 'CrewSubmittableLanguage', + + 'ProgrammingLanguageChoices', +) diff --git a/app/crews/models/choices.py b/app/crews/models/choices.py new file mode 100644 index 0000000..27edfd7 --- /dev/null +++ b/app/crews/models/choices.py @@ -0,0 +1,15 @@ +from django.db import models + +from crews.enums import ProgrammingLanguage + + +class ProgrammingLanguageChoices(models.TextChoices): + """크루에서 사용 가능한 언어""" + + NODE_JS = ProgrammingLanguage.NODE_JS.value.to_choice() + KOTLIN = ProgrammingLanguage.KOTLIN.value.to_choice() + SWIFT = ProgrammingLanguage.SWIFT.value.to_choice() + CPP = ProgrammingLanguage.CPP.value.to_choice() + JAVA = ProgrammingLanguage.JAVA.value.to_choice() + PYTHON = ProgrammingLanguage.PYTHON.value.to_choice() + C = ProgrammingLanguage.C.value.to_choice() diff --git a/app/crews/models/crew.py b/app/crews/models/crew.py new file mode 100644 index 0000000..15458c5 --- /dev/null +++ b/app/crews/models/crew.py @@ -0,0 +1,87 @@ +from django.core.validators import MinValueValidator, MaxValueValidator +from django.db import models + +from crews.validators import EmojiValidator +from users.models import User, UserBojLevel + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text='크루 이름을 입력해주세요. (최대 20자)', + ) + icon = models.CharField( + max_length=2, + validators=[EmojiValidator(message='이모지 형식이 아닙니다.')], + null=False, + blank=False, + default='🚢', + help_text='크루 아이콘을 입력해주세요. (이모지)', + ) + max_members = models.IntegerField( + help_text='크루 최대 인원을 입력해주세요.', + validators=[ + MinValueValidator(1), + MaxValueValidator(8), + ], + default=8, + blank=False, + null=False, + ) + notice = models.TextField( + help_text='크루 공지를 입력해주세요.', + null=True, + blank=True, + max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 + ) + custom_tags = models.JSONField( + help_text='태그를 입력해주세요.', + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + min_boj_level = models.IntegerField( + help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', + choices=UserBojLevel.choices, + blank=True, + null=True, + default=None, + ) + is_recruiting = models.BooleanField( + help_text='모집 중 여부를 입력해주세요.', + default=True, + ) + is_active = models.BooleanField( + help_text='활동 중인지 여부를 입력해주세요.', + default=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + User, + on_delete=models.PROTECT, + null=False, + blank=False, + ) + updated_at = models.DateTimeField(auto_now=True) + + class field_name: + NAME = 'name' + ICON = 'icon' + MAX_MEMBERS = 'max_members' + NOTICE = 'notice' + CUSTOM_TAGS = 'custom_tags' + MIN_BOJ_LEVEL = 'min_boj_level' + IS_RECRUITING = 'is_recruiting' + IS_ACTIVE = 'is_active' + CREATED_AT = 'created_at' + CREATED_BY = 'created_by' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['-updated_at'] + + def __str__(self) -> str: + return f'[{self.pk} : {self.icon} "{self.name}"]' diff --git a/app/tle/models/dao/crew_activity.py b/app/crews/models/crew_activity.py similarity index 97% rename from app/tle/models/dao/crew_activity.py rename to app/crews/models/crew_activity.py index f9963d4..0005819 100644 --- a/app/tle/models/dao/crew_activity.py +++ b/app/crews/models/crew_activity.py @@ -1,11 +1,10 @@ from __future__ import annotations -import typing from django.contrib import admin from django.db import models from django.utils import timezone -from tle.models.dao.crew import Crew +from crews.models.crew import Crew class CrewActivity(models.Model): diff --git a/app/tle/models/dao/crew_activity_problem.py b/app/crews/models/crew_activity_problem.py similarity index 89% rename from app/tle/models/dao/crew_activity_problem.py rename to app/crews/models/crew_activity_problem.py index 86cdb6c..19891d0 100644 --- a/app/tle/models/dao/crew_activity_problem.py +++ b/app/crews/models/crew_activity_problem.py @@ -1,7 +1,7 @@ from django.core.validators import MinValueValidator from django.db import models -from tle.models.dao.crew_activity import CrewActivity +from crews.models.crew_activity import CrewActivity from problems.models.problem import Problem @@ -22,7 +22,6 @@ class CrewActivityProblem(models.Model): MinValueValidator(1), ], ) - created_at = models.DateTimeField(auto_now_add=True) class field_name: # related fields @@ -31,7 +30,6 @@ class field_name: ACTIVITY = 'activity' PROBLEM = 'problem' ORDER = 'order' - CREATED_AT = 'created_at' class Meta: constraints = [ diff --git a/app/tle/models/dao/crew_applicant.py b/app/crews/models/crew_applicant.py similarity index 96% rename from app/tle/models/dao/crew_applicant.py rename to app/crews/models/crew_applicant.py index ff3bbaa..6c2391a 100644 --- a/app/tle/models/dao/crew_applicant.py +++ b/app/crews/models/crew_applicant.py @@ -2,8 +2,8 @@ from django.utils import timezone from users.models import User -from tle.models.dao.crew import Crew -from tle.models.dao.crew_member import CrewMember +from crews.models.crew import Crew +from crews.models.crew_member import CrewMember class CrewApplicant(models.Model): @@ -70,7 +70,6 @@ def save(self, *args, **kwargs) -> None: # 같은 크루에 여러 번 가입하는 것을 방지 if self.crew.members.filter(user=self.user).exists(): raise ValueError('이미 가입한 크루에 가입 신청을 할 수 없습니다.') - return super().save(*args, **kwargs) def accept(self, commit=True) -> CrewMember: diff --git a/app/tle/models/dao/crew_member.py b/app/crews/models/crew_member.py similarity index 75% rename from app/tle/models/dao/crew_member.py rename to app/crews/models/crew_member.py index 91be94f..3186bb3 100644 --- a/app/tle/models/dao/crew_member.py +++ b/app/crews/models/crew_member.py @@ -1,9 +1,7 @@ -from __future__ import annotations - from django.db import models from users.models import User -from tle.models.dao.crew import Crew +from crews.models.crew import Crew class CrewMember(models.Model): @@ -37,10 +35,3 @@ class Meta: ), ] ordering = ['created_at'] - - @classmethod - def captain_of(cls, crew: Crew) -> CrewMember: - return cls.objects.get(**{ - CrewMember.field_name.CREW: crew, - CrewMember.field_name.IS_CAPTAIN: True, - }) diff --git a/app/crews/models/crew_submittable_language.py b/app/crews/models/crew_submittable_language.py new file mode 100644 index 0000000..6bd5059 --- /dev/null +++ b/app/crews/models/crew_submittable_language.py @@ -0,0 +1,25 @@ +from django.db import models + +from crews.models.crew import Crew +from crews.models.choices import ProgrammingLanguageChoices + + +class CrewSubmittableLanguage(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + ) + language = models.TextField( + choices=ProgrammingLanguageChoices.choices, + help_text='언어 키를 입력해주세요. (최대 20자)', + ) + + class field_name: + CREW = 'crew' + LANGUAGE = 'language' + + class Meta: + ordering = ['crew'] + + def __str__(self) -> str: + return f'[{self.pk} : #{self.language}]' diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py new file mode 100644 index 0000000..cb2317a --- /dev/null +++ b/app/crews/serializers/__init__.py @@ -0,0 +1,119 @@ +from django.db.transaction import atomic +from rest_framework.serializers import ( + ModelSerializer, + MultipleChoiceField, +) + +from crews.models import ( + Crew, + CrewSubmittableLanguage, + ProgrammingLanguageChoices, +) +from crews.serializers.fields import ( + MembersField, + MemberCountField, + IsMemberField, + IsJoinableField, + TagsField, + RecentActivityField, +) +from crews.serializers.mixins import ( + CurrentUserMixin, + ReadOnlySerializerMixin, +) +from crews.services import set_crew_submittable_languages +from users.serializers import UserMinimalSerializer + + +class CrewDetailSerializer(CurrentUserMixin, + ModelSerializer): + is_member = IsMemberField() + members = MemberCountField() + tags = TagsField() + languages = MultipleChoiceField(choices=ProgrammingLanguageChoices.choices) + created_by = UserMinimalSerializer(read_only=True) + + class Meta: + model = Crew + fields = [ + 'id', + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.MAX_MEMBERS, + 'members', + 'is_member', + 'languages', + 'tags', + Crew.field_name.MIN_BOJ_LEVEL, + Crew.field_name.CUSTOM_TAGS, + Crew.field_name.NOTICE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.IS_ACTIVE, + Crew.field_name.CREATED_AT, + Crew.field_name.CREATED_BY, + Crew.field_name.UPDATED_AT, + ] + read_only_fields = [ + 'id', + 'tags', + 'members', + Crew.field_name.CREATED_AT, + Crew.field_name.CREATED_BY, + Crew.field_name.UPDATED_AT, + ] + extra_kwargs = { + Crew.field_name.MAX_MEMBERS: {'write_only': True}, + Crew.field_name.MIN_BOJ_LEVEL: {'write_only': True}, + 'languages': {'write_only': True}, + Crew.field_name.CUSTOM_TAGS: {'write_only': True}, + } + + def save(self, **kwargs): + languages = self.validated_data.pop('languages') + with atomic(): + crew: Crew = super().save(**kwargs) + set_crew_submittable_languages(crew, languages) + return crew + + def create(self, validated_data): + validated_data[Crew.field_name.CREATED_BY] = self.current_user() + return super().create(validated_data) + + +class CrewRecruitingSerializer(ReadOnlySerializerMixin, + ModelSerializer): + is_joinable = IsJoinableField() + is_member = IsMemberField() + activities = RecentActivityField() + members = MembersField() + tags = TagsField() + + class Meta: + model = Crew + fields = [ + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.IS_ACTIVE, + Crew.field_name.IS_RECRUITING, + 'is_joinable', + 'is_member', + 'activities', + 'members', + 'tags', + ] + read_only_fields = ['__all__'] + + +class CrewJoinedSerializer(ReadOnlySerializerMixin, + ModelSerializer): + activities = RecentActivityField() + + class Meta: + model = Crew + fields = [ + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.IS_ACTIVE, + 'activities', + ] + read_only_fields = ['__all__'] diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py new file mode 100644 index 0000000..905ea59 --- /dev/null +++ b/app/crews/serializers/fields.py @@ -0,0 +1,174 @@ +from dataclasses import asdict, dataclass +from datetime import date +from enum import Enum +from typing import Iterable, List, Optional + +from rest_framework.serializers import ReadOnlyField + +from crews.models import Crew, CrewActivity +from crews.serializers.mixins import CurrentUserMixin +from crews.services import get_members, is_member, is_joinable +from users.models import UserBojLevel + + +class MemberCountField(ReadOnlyField): + def to_representation(self, crew: Crew): + members = get_members(crew) + return { + 'count': members.count(), + 'max_count': crew.max_members, + } + + +class MembersField(ReadOnlyField): + def to_representation(self, crew: Crew): + members = get_members(crew) + return { + 'count': members.count(), + 'max_count': crew.max_members, + 'items': [{ + "user_id": member.user.pk, + "username": member.user.username, + "profile_image": member.user.profile_image, + "is_captain": member.is_captain, + "created_at": member.created_at, + } for member in members], + } + + +class IsMemberField(CurrentUserMixin, + ReadOnlyField): + def to_representation(self, crew: Crew): + user = self.current_user() + return is_member(crew, user) + + +class IsJoinableField(CurrentUserMixin, + ReadOnlyField): + def to_representation(self, crew: Crew): + user = self.current_user() + return is_joinable(crew, user) + + +class TagType(Enum): + LANGUAGE = 'language' + LEVEL = 'level' + CUSTOM = 'custom' + + +@dataclass +class TagDict: + key: str + name: str + type: TagType + + +class TagsField(ReadOnlyField): + def to_representation(self, crew: Crew): + # 태그의 나열 순서는 리스트에 선언한 순서를 따름. + tags: List[TagDict] = [ + *self.get_language_tags(crew), + *self.get_level_tags(crew), + *self.get_custom_tags(crew), + ] + return { + 'count': len(tags), + 'items': [asdict(tag) for tag in tags], + } + + def get_custom_tags(self, crew: Crew) -> Iterable[TagDict]: + for tag in crew.custom_tags: + yield TagDict( + key=None, + name=tag, + type=TagType.CUSTOM.value, + ) + + def get_language_tags(self, crew: Crew) -> Iterable[TagDict]: + for lang in crew.submittable_languages.all(): + yield TagDict( + key=lang.key, + name=lang.name, + type=TagType.LANGUAGE.value, + ) + + def get_level_tags(self, crew: Crew) -> Iterable[TagDict]: + yield TagDict( + key=None, + name=self.get_boj_level_bounded_name( + level=UserBojLevel(crew.min_boj_level), + ), + type=TagType.LEVEL.value, + ) + + def get_boj_level_bounded_name(self, + level: Optional[UserBojLevel], + bound_tier: int = 5, + bound_msg: str = "이상", + default_msg: str = "티어 무관", + lang='ko', + arabic=False) -> str: + """level에 대한 백준 난이도 태그 이름을 반환한다. + + bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 + 티어(1,2,3,4,5)를 의미한다. + + bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. + + 만약 level의 티어가 bound_tier와 + 같다면 랭크만 출력하고, + 같지않다면 랭크와 티어 모두 출력한다. + + 메시지의 마지막에는 bound_msg를 출력한다. + """ + if level is None: + return default_msg + if level.get_tier() == bound_tier: + return level.get_division_name(lang=lang) + ' ' + bound_msg + else: + return level.get_name(lang=lang, arabic=arabic) + ' ' + bound_msg + + +@dataclass +class ActivityDict: + nth: Optional[int] = None + name: str = '' + start_at: Optional[date] = None + end_at: Optional[date] = None + is_open: bool = False # 제출 가능 여부 + + @classmethod + def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': + return ActivityDict( + name=activity.name, + nth=activity.nth(), + is_open=activity.is_opened(), + start_at=activity.start_at, + end_at=activity.end_at, + ) + + +class RecentActivityField(ReadOnlyField): + def to_representation(self, crew: Crew): + activities = CrewActivity.objects.filter(**{ + CrewActivity.field_name.CREW: crew, + }) + return { + 'count': activities.count(), + "recent": asdict(self.get_recent_activity(crew)), + } + + def get_recent_activity(self, crew: Crew) -> ActivityDict: + # 활동 종료 여부가 최우선 순위 + if not crew.is_active: + return ActivityDict(name='활동 종료') + # 활동 중이라면, 현재 진행 중인 활동 중 가장 오래된 것을 우선적으로 표시 + if (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): + activity = opened_activities.earliest() + return ActivityDict.from_activity(activity) + # 현재 진행 중인 활동이 없다면, 가장 최근 활동을 표시 + if (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): + activity = closed_activities.latest() + return ActivityDict.from_activity(activity) + # 활동 중이나, 등록된 활동이 없다면 '등록된 활동 없음'을 표시 + return ActivityDict(name='등록된 활동 없음') diff --git a/app/crews/serializers/mixins.py b/app/crews/serializers/mixins.py new file mode 100644 index 0000000..be3fd40 --- /dev/null +++ b/app/crews/serializers/mixins.py @@ -0,0 +1,18 @@ +from rest_framework.exceptions import PermissionDenied +from rest_framework.serializers import CurrentUserDefault, Field + + +class ReadOnlySerializerMixin: + def create(self, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def update(self, instance, validated_data): + raise PermissionDenied('Cannot create user through this serializer') + + def save(self, **kwargs): + raise PermissionDenied('Cannot update user through this serializer') + + +class CurrentUserMixin(Field): + def current_user(self): + return CurrentUserDefault()(self) diff --git a/app/crews/services.py b/app/crews/services.py new file mode 100644 index 0000000..803e4b4 --- /dev/null +++ b/app/crews/services.py @@ -0,0 +1,66 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from crews.models import ( + Crew, + CrewMember, + CrewSubmittableLanguage, + ProgrammingLanguageChoices, +) +from users.models import User + + +@receiver(post_save, sender=Crew) +def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): + """크루 생성 시 선장을 자동으로 생성합니다.""" + if created: + CrewMember.objects.create(**{ + CrewMember.field_name.CREW: instance, + CrewMember.field_name.USER: instance.created_by, + CrewMember.field_name.IS_CAPTAIN: True, + }) + + +def set_crew_submittable_languages(crew: Crew, languages: list): + """크루의 제출 가능 언어를 설정합니다.""" + for language in languages: + for choice in ProgrammingLanguageChoices.choices: + if language == choice[0]: + raise ValueError('Invalid language') + CrewSubmittableLanguage.objects.filter(**{ + CrewSubmittableLanguage.field_name.CREW: crew, + }).delete() + CrewSubmittableLanguage.objects.bulk_create([ + CrewSubmittableLanguage(**{ + CrewSubmittableLanguage.field_name.CREW: crew, + CrewSubmittableLanguage.field_name.LANGUAGE: language, + }) for language in languages + ]) + + +def get_members(crew: Crew): + return CrewMember.objects.filter(**{ + CrewMember.field_name.CREW: crew, + }) + + +def is_member(crew: Crew, user: User) -> bool: + return CrewMember.objects.filter(**{ + CrewMember.field_name.CREW: crew, + CrewMember.field_name.USER: user, + }).exists() + + +def is_joinable(crew: Crew, user: User) -> bool: + if not crew.is_recruiting: + return False + if get_members(crew).count() >= crew.max_members: + return False + if is_member(crew, user): + return False + if crew.min_boj_level is not None: + return bool( + (user.boj_level is not None) and + (user.boj_level >= crew.min_boj_level) + ) + return True diff --git a/app/crews/validators.py b/app/crews/validators.py new file mode 100644 index 0000000..e02a884 --- /dev/null +++ b/app/crews/validators.py @@ -0,0 +1,15 @@ +from django.core.exceptions import ValidationError +from django.core.validators import BaseValidator + +from crews.enums import Emoji + + +class EmojiValidator(BaseValidator): + def __init__(self, message: str = None) -> None: + self.message = message + + def __call__(self, value) -> None: + try: + Emoji(value) # just checking if it's valid emoji + except ValueError: + raise ValidationError(self.message, params={"value": value}) diff --git a/app/crews/views.py b/app/crews/views.py new file mode 100644 index 0000000..46fd440 --- /dev/null +++ b/app/crews/views.py @@ -0,0 +1,79 @@ +from rest_framework import mixins +from rest_framework import permissions +from rest_framework.generics import GenericAPIView + +from crews.models import Crew, CrewMember +from crews.serializers import ( + CrewDetailSerializer, + CrewRecruitingSerializer, + CrewJoinedSerializer, +) + + +class CrewCreate(mixins.CreateModelMixin, + GenericAPIView): + """크루 생성 API""" + + queryset = Crew.objects.all() + permission_classes = [permissions.IsAuthenticated] + serializer_class = CrewDetailSerializer + + def post(self, request, *args, **kwargs): + return self.create(request, *args, **kwargs) + + + +class CrewDetail(mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.DestroyModelMixin, + GenericAPIView): + """크루 상세 조회, 수정, 삭제 API""" + + queryset = Crew.objects.all() + permission_classes = [permissions.IsAuthenticated] + serializer_class = CrewDetailSerializer + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def put(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + def patch(self, request, *args, **kwargs): + return self.partial_update(request, *args, **kwargs) + + def delete(self, request, *args, **kwargs): + return self.destroy(request, *args, **kwargs) + + +class CrewRecruiting(mixins.ListModelMixin, + GenericAPIView): + """모집 중인 크루 목록 조회 API""" + + queryset = Crew.objects.filter(**{Crew.field_name.IS_RECRUITING: True}) + permission_classes = [permissions.AllowAny] + serializer_class = CrewRecruitingSerializer + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + +class CrewJoined(mixins.ListModelMixin, + GenericAPIView): + """가입한 크루 목록 조회 API""" + + permission_classes = [permissions.IsAuthenticated] + serializer_class = CrewJoinedSerializer + + def get_queryset(self): + # 현재 사용자가 속한 크루만 반환 + crews = CrewMember.objects.filter(**{ + CrewMember.field_name.USER: self.request.user, + }).values_list(CrewMember.field_name.CREW) + queryset = Crew.objects.filter(pk__in=crews) + # 활동 종료된 크루는 뒤로 가도록 정렬 + return queryset.order_by('-'+Crew.field_name.IS_ACTIVE) + + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) diff --git a/app/tle/admin/__init__.py b/app/tle/admin/__init__.py index 3743d1b..a372c9f 100644 --- a/app/tle/admin/__init__.py +++ b/app/tle/admin/__init__.py @@ -1,99 +1,9 @@ from django.contrib import admin -from users.models import User from tle.models import * admin.site.register([ - CrewActivityProblem, - CrewApplicant, Submission, SubmissionComment, ]) - -@admin.register(Crew) -class CrewModelAdmin(admin.ModelAdmin): - list_display = [ - 'get_display_name', - 'get_captain', - 'get_members', - 'get_applicants', - 'get_activities', - Crew.field_name.IS_ACTIVE, - Crew.field_name.IS_RECRUITING, - Crew.field_name.CREATED_AT, - ] - search_fields = [ - Crew.field_name.NAME, - Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - Crew.field_name.ICON, - ] - - @admin.display(description='Display Name') - def get_display_name(self, obj: Crew) -> str: - return obj.get_display_name() - - @admin.display(description='Captain') - def get_captain(self, obj: Crew) -> str: - return obj.get_captain() - - @admin.display(description='Members') - def get_members(self, obj: Crew) -> str: - return f'{obj.members.count()} / {obj.max_members}' - - @admin.display(description='Applicants') - def get_applicants(self, obj: Crew) -> str: - return obj.applicants.count() - - @admin.display(description='Activities') - def get_activities(self, obj: Crew) -> str: - return obj.activities.count() - - -@admin.register(CrewMember) -class CrewMemberModelAdmin(admin.ModelAdmin): - list_display = [ - CrewMember.field_name.USER, - CrewMember.field_name.CREW, - CrewMember.field_name.IS_CAPTAIN, - CrewMember.field_name.CREATED_AT, - ] - search_fields = [ - CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, - CrewMember.field_name.USER+'__'+User.field_name.USERNAME, - ] - ordering = [ - CrewMember.field_name.CREW, - CrewMember.field_name.IS_CAPTAIN, - ] - - -@admin.register(CrewActivity) -class CrewActivityModelAdmin(admin.ModelAdmin): - list_display = [ - CrewActivity.field_name.CREW, - CrewActivity.field_name.NAME, - CrewActivity.field_name.START_AT, - CrewActivity.field_name.END_AT, - 'nth', - 'is_opened', - 'is_closed', - ] - search_fields = [ - CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, - CrewActivity.field_name.NAME, - ] - - -@admin.register(SubmissionLanguage) -class SubmissionLanguageModelAdmin(admin.ModelAdmin): - list_display = [ - SubmissionLanguage.field_name.KEY, - SubmissionLanguage.field_name.NAME, - SubmissionLanguage.field_name.EXTENSION, - ] - search_fields = [ - SubmissionLanguage.field_name.KEY, - SubmissionLanguage.field_name.NAME, - SubmissionLanguage.field_name.EXTENSION, - ] diff --git a/app/tle/enums/__init__.py b/app/tle/enums/__init__.py deleted file mode 100644 index 9514643..0000000 --- a/app/tle/enums/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from tle.enums.emoji import Emoji - - -__all__ = ( - 'Emoji', -) diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py index 4319c7a..e0b6cfa 100644 --- a/app/tle/models/__init__.py +++ b/app/tle/models/__init__.py @@ -1,14 +1,8 @@ -from tle.models.dao import * +from tle.models.dao.submission import Submission +from tle.models.dao.submission_comment import SubmissionComment __all__ = ( - 'Crew', - 'CrewActivity', - 'CrewActivityProblem', - 'CrewApplicant', - 'CrewMember', - 'Submission', 'SubmissionComment', - 'SubmissionLanguage', ) diff --git a/app/tle/models/dao/__init__.py b/app/tle/models/dao/__init__.py deleted file mode 100644 index 12e2763..0000000 --- a/app/tle/models/dao/__init__.py +++ /dev/null @@ -1,22 +0,0 @@ -from tle.models.dao.crew import Crew -from tle.models.dao.crew_activity import CrewActivity -from tle.models.dao.crew_activity_problem import CrewActivityProblem -from tle.models.dao.crew_applicant import CrewApplicant -from tle.models.dao.crew_member import CrewMember - -from tle.models.dao.submission import Submission -from tle.models.dao.submission_comment import SubmissionComment -from tle.models.dao.submission_language import SubmissionLanguage - - -__all__ = ( - 'Crew', - 'CrewActivity', - 'CrewActivityProblem', - 'CrewApplicant', - 'CrewMember', - - 'Submission', - 'SubmissionComment', - 'SubmissionLanguage', -) diff --git a/app/tle/models/dao/crew.py b/app/tle/models/dao/crew.py deleted file mode 100644 index 1eccce1..0000000 --- a/app/tle/models/dao/crew.py +++ /dev/null @@ -1,161 +0,0 @@ -from __future__ import annotations - -from django.core.exceptions import ValidationError -from django.core.validators import ( - BaseValidator, - MinValueValidator, - MaxValueValidator, -) -from django.db import models, transaction - -from users.models import User, UserBojLevel -from tle.enums import Emoji -from tle.models.dao.submission_language import SubmissionLanguage - - -class EmojiValidator(BaseValidator): - def __init__(self, message: str | None = None) -> None: - self.message = message - - def __call__(self, value) -> None: - try: - Emoji(value) # just checking if it's valid emoji - except ValueError: - raise ValidationError(self.message, params={"value": value}) - - -class Crew(models.Model): - name = models.CharField( - max_length=20, - unique=True, - help_text='크루 이름을 입력해주세요. (최대 20자)', - ) - icon = models.CharField( - max_length=2, - validators=[EmojiValidator(message='이모지 형식이 아닙니다.')], - null=False, - blank=False, - default='🚢', - help_text='크루 아이콘을 입력해주세요. (이모지)', - ) - max_members = models.IntegerField( - help_text='크루 최대 인원을 입력해주세요.', - validators=[ - MinValueValidator(1), - MaxValueValidator(8), - ], - default=8, - blank=False, - null=False, - ) - notice = models.TextField( - help_text='크루 공지를 입력해주세요.', - null=True, - blank=True, - max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 - ) - submittable_languages = models.ManyToManyField( - SubmissionLanguage, - help_text='유저가 사용 가능한 언어를 입력해주세요.', - ) - custom_tags = models.JSONField( - help_text='태그를 입력해주세요.', - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) - min_boj_level = models.IntegerField( - help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - choices=UserBojLevel.choices, - blank=True, - null=True, - default=None, - ) - is_recruiting = models.BooleanField( - help_text='모집 중 여부를 입력해주세요.', - default=True, - ) - is_active = models.BooleanField( - help_text='활동 중인지 여부를 입력해주세요.', - default=True, - ) - created_at = models.DateTimeField(auto_now_add=True) - created_by = models.ForeignKey( - User, - on_delete=models.PROTECT, - null=False, - blank=False, - ) - updated_at = models.DateTimeField(auto_now=True) - - class field_name: - NAME = 'name' - ICON = 'icon' - MAX_MEMBERS = 'max_members' - NOTICE = 'notice' - CUSTOM_TAGS = 'custom_tags' - MIN_BOJ_LEVEL = 'min_boj_level' - IS_RECRUITING = 'is_recruiting' - IS_ACTIVE = 'is_active' - CREATED_AT = 'created_at' - CREATED_BY = 'created_by' - UPDATED_AT = 'updated_at' - - class Meta: - ordering = ['-updated_at'] - - @classmethod - def of_user(cls, user: User) -> models.QuerySet[Crew]: - return cls.objects.filter(members__user=user) - - @classmethod - def of_user_as_captain(cls, user: User) -> models.QuerySet[Crew]: - return cls.objects.filter(created_by=user) - - def __str__(self) -> str: - return f'[{self.pk} : {self.icon} "{self.name}"] ({self.members.count()}/{self.max_members})' - - def save(self, *args, **kwargs) -> None: - with transaction.atomic(): - super().save(*args, **kwargs) - self.set_captain(self.created_by) - - def get_display_name(self) -> str: - return f'{self.icon} {self.name}' - - def get_captain(self) -> User: - return self.members.get(is_captain=True).user - - def set_captain(self, user: User) -> None: - assert isinstance(user, User) - with transaction.atomic(): - self.members.filter(is_captain=True).update(is_captain=False) - try: - captain = self.members.get(user=user) - captain.is_captain = True - except self.members.model.DoesNotExist: - captain = self.members.create(user=user, is_captain=True) - finally: - captain.save() - - def is_captain(self, user: User) -> bool: - return self.members.filter(user=user, is_captain=True).exists() - - def is_member(self, user: User) -> bool: - return self.members.filter(user=user).exists() - - def is_joinable(self, user: User) -> bool: - if not self.is_recruiting: - return False - if self.members.count() >= self.max_members: - return False - if self.is_member(user): - return False - if self.min_boj_level is not None: - return bool( - (user.boj_level is not None) and - (user.boj_level >= self.min_boj_level) - ) - return True diff --git a/app/tle/models/dao/submission.py b/app/tle/models/dao/submission.py index ebe0f76..1573fe6 100644 --- a/app/tle/models/dao/submission.py +++ b/app/tle/models/dao/submission.py @@ -1,8 +1,7 @@ from django.db import models +from crews.models import CrewActivityProblem, CrewSubmittableLanguage from users.models import User -from tle.models.dao.crew_activity_problem import CrewActivityProblem -from tle.models.dao.submission_language import SubmissionLanguage class Submission(models.Model): @@ -21,7 +20,7 @@ class Submission(models.Model): help_text='유저의 코드를 입력해주세요.', ) language = models.ForeignKey( - SubmissionLanguage, + CrewSubmittableLanguage, on_delete=models.PROTECT, help_text='유저의 코드 언어를 입력해주세요.', ) diff --git a/app/tle/models/dao/submission_language.py b/app/tle/models/dao/submission_language.py deleted file mode 100644 index 86059c7..0000000 --- a/app/tle/models/dao/submission_language.py +++ /dev/null @@ -1,32 +0,0 @@ -from django.db import models - - -class SubmissionLanguage(models.Model): - key = models.CharField( - max_length=20, - unique=True, - help_text='언어 키를 입력해주세요. (최대 20자)', - ) - name = models.CharField( - max_length=20, - unique=True, - help_text='언어 이름을 입력해주세요. (최대 20자)', - ) - extension = models.CharField( - max_length=20, - help_text='언어 확장자를 입력해주세요. (최대 20자)', - ) - - class field_name: - KEY = 'key' - NAME = 'name' - EXTENSION = 'extension' - - class Meta: - ordering = ['key'] - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name})' diff --git a/app/tle/serializers/__init__.py b/app/tle/serializers/__init__.py deleted file mode 100644 index 7259b1d..0000000 --- a/app/tle/serializers/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -from tle.serializers.crew_detail import CrewDetailSerializer -from tle.serializers.crew_member import CrewMemberSerializer -from tle.serializers.crew_recruiting import CrewRecruitingSerializer -from tle.serializers.crew_joined import CrewJoinedSerializer - - -__all__ = ( - 'CrewDetailSerializer', - 'CrewMemberSerializer', - 'CrewRecruitingSerializer', - 'CrewJoinedSerializer', -) diff --git a/app/tle/serializers/crew_detail.py b/app/tle/serializers/crew_detail.py deleted file mode 100644 index 7b6d0ea..0000000 --- a/app/tle/serializers/crew_detail.py +++ /dev/null @@ -1,101 +0,0 @@ -from django.db.models import QuerySet -from django.db.transaction import atomic -from rest_framework.serializers import * - -from users.serializers import UserMinimalSerializer -from tle.models import Crew, SubmissionLanguage -from tle.serializers.crew_member import CrewMemberSerializer -from tle.serializers.mixins import CurrentUserMixin, TagListMixin - - -class CrewDetailSerializer(ModelSerializer, CurrentUserMixin, TagListMixin): - is_member = SerializerMethodField() - members = SerializerMethodField() - languages = JSONField(help_text='사용 가능한 언어 목록 (언어 key의 배열)') - tags = SerializerMethodField() - created_by = UserMinimalSerializer(read_only=True) - - class Meta: - model = Crew - fields = [ - 'id', - Crew.field_name.ICON, - Crew.field_name.NAME, - Crew.field_name.MAX_MEMBERS, - 'members', - 'is_member', - Crew.field_name.MIN_BOJ_LEVEL, - 'languages', - Crew.field_name.CUSTOM_TAGS, - 'tags', - Crew.field_name.NOTICE, - Crew.field_name.IS_RECRUITING, - Crew.field_name.IS_ACTIVE, - Crew.field_name.CREATED_AT, - Crew.field_name.CREATED_BY, - Crew.field_name.UPDATED_AT, - ] - read_only_fields = [ - 'id', - 'tags', - 'members', - Crew.field_name.CREATED_AT, - Crew.field_name.CREATED_BY, - Crew.field_name.UPDATED_AT, - ] - extra_kwargs = { - Crew.field_name.MAX_MEMBERS: {'write_only': True}, - Crew.field_name.MIN_BOJ_LEVEL: {'write_only': True}, - 'languages': {'write_only': True}, - Crew.field_name.CUSTOM_TAGS: {'write_only': True}, - } - - def create(self, validated_data): - return super().create(validated_data) - - def get_is_member(self, obj: Crew): - return obj.is_member(self.current_user()) - - def get_members(self, obj: Crew): - return { - 'count': obj.members.count(), - 'max_count': obj.max_members, - 'items': CrewMemberSerializer(obj.members.all(), many=True).data, - } - - def get_tags(self, obj: Crew): - return self.tag_list(obj) - - def validate_languages(self, value) -> QuerySet[SubmissionLanguage]: - """언어 정보를 언어 키의 배열로 받고, 이를 SubmissionLanguage의 QuerySet으로 변환한다.""" - # 언어 정보는 문자열의 배열로 받는다. - if not isinstance(value, list): - raise ValidationError('Languages must be a list of strings') - for lang in value: - if not isinstance(lang, str): - raise ValidationError('Languages must be a list of strings') - # 최소 한 개 이상의 언어가 있어야 한다. - if len(value) == 0: - raise ValidationError('At least one language must be specified') - for lang in value: - if not SubmissionLanguage.objects.filter(**{ - SubmissionLanguage.field_name.KEY: lang, - }).exists(): - raise ValidationError(f'Invalid language key "{lang}"') - # 언어 키의 배열을 SubmissionLanguage의 QuerySet으로 변환한다. - return SubmissionLanguage.objects.filter(**{ - SubmissionLanguage.field_name.KEY + '__in': value, - }) - - def save(self, **kwargs): - crew: Crew - languages: QuerySet[SubmissionLanguage] - languages = self.validated_data.pop('languages') - with atomic(): - crew = super().save(**{ - **kwargs, - Crew.field_name.CREATED_BY: self.current_user(), - }) - crew.submittable_languages.set(languages) - crew.save() - return crew \ No newline at end of file diff --git a/app/tle/serializers/crew_joined.py b/app/tle/serializers/crew_joined.py deleted file mode 100644 index 7b4dd26..0000000 --- a/app/tle/serializers/crew_joined.py +++ /dev/null @@ -1,24 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import Crew -from tle.serializers.mixins import RecentActivityMixin - - -class CrewJoinedSerializer(ModelSerializer, RecentActivityMixin): - activities = SerializerMethodField() - - class Meta: - model = Crew - fields = [ - Crew.field_name.ICON, - Crew.field_name.NAME, - 'activities', - Crew.field_name.IS_ACTIVE, - ] - read_only_fields = ['__all__'] - - def get_activities(self, crew: Crew) -> dict: - return { - "count": crew.activities.count(), - "recent": self.recent_activity(crew), - } diff --git a/app/tle/serializers/crew_member.py b/app/tle/serializers/crew_member.py deleted file mode 100644 index def4adc..0000000 --- a/app/tle/serializers/crew_member.py +++ /dev/null @@ -1,17 +0,0 @@ -from rest_framework.serializers import * - -from users.serializers import UserMinimalSerializer -from tle.models import CrewMember - - -class CrewMemberSerializer(ModelSerializer): - user = UserMinimalSerializer(read_only=True) - - class Meta: - model = CrewMember - fields = ( - CrewMember.field_name.USER, - CrewMember.field_name.IS_CAPTAIN, - CrewMember.field_name.CREATED_AT, - ) - read_only_fields = '__all__' diff --git a/app/tle/serializers/crew_recruiting.py b/app/tle/serializers/crew_recruiting.py deleted file mode 100644 index 4e3d854..0000000 --- a/app/tle/serializers/crew_recruiting.py +++ /dev/null @@ -1,55 +0,0 @@ -from rest_framework.serializers import * - -from tle.models import Crew -from tle.serializers.mixins import ( - CurrentUserMixin, - TagListMixin, - RecentActivityMixin, -) - - -class CrewRecruitingSerializer(CurrentUserMixin, - TagListMixin, - RecentActivityMixin, - ModelSerializer): - is_joinable = SerializerMethodField() - is_member = SerializerMethodField() - activities = SerializerMethodField() - members = SerializerMethodField() - tags = SerializerMethodField() - - class Meta: - model = Crew - fields = [ - Crew.field_name.ICON, - Crew.field_name.NAME, - Crew.field_name.IS_ACTIVE, - Crew.field_name.IS_RECRUITING, - 'is_joinable', - 'is_member', - 'activities', - 'members', - 'tags', - ] - read_only_fields = ['__all__'] - - def get_is_joinable(self, obj: Crew): - return obj.is_joinable(self.current_user()) - - def get_is_member(self, obj: Crew): - return obj.is_member(self.current_user()) - - def get_activities(self, crew: Crew) -> dict: - return { - "count": crew.activities.count(), - "recent": self.recent_activity(crew), - } - - def get_members(self, obj: Crew): - return { - 'count': obj.members.count(), - 'max_count': obj.max_members, - } - - def get_tags(self, obj: Crew): - return self.tag_list(obj) diff --git a/app/tle/serializers/mixins/__init__.py b/app/tle/serializers/mixins/__init__.py deleted file mode 100644 index 4018ad2..0000000 --- a/app/tle/serializers/mixins/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from tle.serializers.mixins.current_user import CurrentUserMixin -from tle.serializers.mixins.tag_list import TagListMixin -from tle.serializers.mixins.recent_activity import RecentActivityMixin - - -__all__ = ( - 'CurrentUserMixin', - 'RecentActivityMixin', - 'TagListMixin', -) diff --git a/app/tle/serializers/mixins/current_user.py b/app/tle/serializers/mixins/current_user.py deleted file mode 100644 index 8d314c2..0000000 --- a/app/tle/serializers/mixins/current_user.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework.serializers import Serializer - -from users.models import User - - -class CurrentUserMixin: - def current_user(self: Serializer) -> User: - return self.context['request'].user diff --git a/app/tle/serializers/mixins/recent_activity.py b/app/tle/serializers/mixins/recent_activity.py deleted file mode 100644 index f7d4f58..0000000 --- a/app/tle/serializers/mixins/recent_activity.py +++ /dev/null @@ -1,41 +0,0 @@ -from datetime import date -from dataclasses import dataclass, asdict -from typing import Optional - -from rest_framework.serializers import * - -from tle.models import Crew, CrewActivity - - -@dataclass -class ActivityDict: - nth: Optional[int] = None - name: str = '' - start_at: Optional[date] = None - end_at: Optional[date] = None - is_open: bool = False # 제출 가능 여부 - - @classmethod - def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': - return ActivityDict( - name=activity.name, - nth=activity.nth(), - is_open=activity.is_opened(), - start_at=activity.start_at, - end_at=activity.end_at, - ) - - -class RecentActivityMixin: - def recent_activity(self, crew: Crew) -> dict: - if not crew.is_active: - activity_dict = ActivityDict(name='활동 종료') - elif (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): - activity = opened_activities.earliest() - activity_dict = ActivityDict.from_activity(activity) - elif (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): - activity = closed_activities.latest() - activity_dict = ActivityDict.from_activity(activity) - else: - activity_dict = ActivityDict(name='등록된 활동 없음') - return asdict(activity_dict) diff --git a/app/tle/serializers/mixins/tag_list.py b/app/tle/serializers/mixins/tag_list.py deleted file mode 100644 index 426f001..0000000 --- a/app/tle/serializers/mixins/tag_list.py +++ /dev/null @@ -1,83 +0,0 @@ -import dataclasses -import enum -import typing - -from rest_framework.serializers import * - -from users.models import UserBojLevel -from tle.models import Crew - - -class TagType(enum.Enum): - LANGUAGE = 'language' - LEVEL = 'level' - CUSTOM = 'custom' - - -@dataclasses.dataclass -class TagDict: - key: str - name: str - type: TagType - - -class TagListMixin: - def tag_list(self, crew: Crew) -> typing.Dict: - """크루의 태그들을 key와 name으로 나열하여 반환한다. - - 반환 예시: - - ```python - { - "count": 2, - "items": [ - { "key": "c", "name": "C" }, - { "key": None, "name": "티어 무관" } - ] - } - ``` - """ - # 태그의 나열 순서는 리스트에 선언한 순서를 따름. - tags: typing.List[TagDict] = [ - *self._get_language_tags(crew), - *self._get_level_tags(crew), - *self._get_custom_tags(crew), - ] - return { - 'count': len(tags), - 'items': [dataclasses.asdict(tag) for tag in tags], - } - - def _get_language_tags(self, crew: Crew) -> typing.Iterable[TagDict]: - for lang in crew.submittable_languages.all(): - yield TagDict(key=lang.key, name=lang.name, type=TagType.LANGUAGE.value) - - def _get_level_tags(self, crew: Crew) -> typing.Iterable[TagDict]: - if crew.min_boj_level is None: - yield TagDict(key=None, name="티어 무관", type=TagType.LEVEL.value) - else: - yield self._get_boj_level_bound_tag(crew.min_boj_level, 5, "이상") - - def _get_boj_level_bound_tag(self, level: int, bound_tier: int, bound_msg: str, lang='ko', arabic=False) -> TagDict: - """level에 대한 백준 난이도 태그를 반환한다. - - bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 - 티어(1,2,3,4,5)를 의미한다. - - bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. - - 만약 level의 티어가 bound_tier와 - 같다면 랭크만 출력하고, - 같지않다면 랭크와 티어 모두 출력한다. - - 메시지의 마지막에는 bound_msg를 출력한다. - """ - if UserBojLevel.get_tier(level) == bound_tier: - level_name = UserBojLevel.get_division_name(level, lang=lang) - else: - level_name = UserBojLevel.get_name(level, lang=lang, arabic=arabic) - return TagDict(key=None, name=f'{level_name} {bound_msg}', type=TagType.LEVEL.value) - - def _get_custom_tags(self, crew: Crew) -> typing.Iterable[TagDict]: - for tag in crew.custom_tags: - yield TagDict(key=None, name=tag, type=TagType.CUSTOM.value) diff --git a/app/tle/views/permissions.py b/app/tle/views/permissions.py deleted file mode 100644 index 1343289..0000000 --- a/app/tle/views/permissions.py +++ /dev/null @@ -1,23 +0,0 @@ -from rest_framework.permissions import ( - AllowAny, - BasePermission, - IsAuthenticated, - IsAdminUser, - SAFE_METHODS, -) - - -__all__ = ( - 'AllowAny', - 'IsAuthenticated', - 'IsAdminUser', - - 'IsReadOnly', -) - - -class IsReadOnly(BasePermission): - def has_permission(self, request, view): - return bool( - request.method in SAFE_METHODS, - ) diff --git a/app/tle/views/urls.py b/app/tle/views/urls.py deleted file mode 100644 index 89c9104..0000000 --- a/app/tle/views/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.urls import include, path - -from tle.views.viewsets import * - - -urlpatterns = [ - path("crews/", include([ - path("", CrewViewSet.as_view({"post": "create"})), - path("recruiting", CrewViewSet.as_view({"get": "list_recruiting"})), - path("my", CrewViewSet.as_view({"get": "list_my"})), - path("/", include([ - path("detail", CrewViewSet.as_view({ - "get": "retrieve", - "put": "update", - "patch": "partial_update", - "delete": "destroy", - })) - ])), - ])), -] diff --git a/app/tle/views/viewsets/__init__.py b/app/tle/views/viewsets/__init__.py deleted file mode 100644 index cab8f79..0000000 --- a/app/tle/views/viewsets/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from tle.views.viewsets.crew_viewset import CrewViewSet - - -__all__ = ( - 'CrewViewSet', -) diff --git a/app/tle/views/viewsets/crew_viewset.py b/app/tle/views/viewsets/crew_viewset.py deleted file mode 100644 index 3f943c3..0000000 --- a/app/tle/views/viewsets/crew_viewset.py +++ /dev/null @@ -1,42 +0,0 @@ -from rest_framework.viewsets import ViewSet, ModelViewSet -from rest_framework.permissions import BasePermission -from rest_framework.request import Request - -from tle.models import Crew -from tle.serializers import * -from tle.views.permissions import * - - -class CrewPermission(BasePermission): - def has_permission(self, request: Request, view: ViewSet): - if view.action == 'list_recruiting': - # 모든 사용자에게 공개 - return True - return request.user.is_authenticated - - -class CrewViewSet(ModelViewSet): - """문제 태그 목록 조회 + 생성 기능""" - lookup_field = 'id' - permission_classes = [CrewPermission] - - def get_queryset(self): - if self.action in 'list_recruiting': - return Crew.objects.filter(is_recruiting=True) - if self.action in 'list_my': - return Crew.of_user(self.request.user).order_by('-'+Crew.field_name.IS_ACTIVE) - return Crew.objects.all() - - def get_serializer_class(self): - if self.action in 'list_recruiting': - return CrewRecruitingSerializer - if self.action in 'list_my': - return CrewJoinedSerializer - return CrewDetailSerializer - - def list_recruiting(self, request): - # TODO: 검색 옵션 (사용 언어 / 백준 티어) 제공 - return super().list(request) - - def list_my(self, request): - return super().list(request) diff --git a/app/users/admin.py b/app/users/admin.py index ecc3e53..da02934 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -2,7 +2,7 @@ from django.contrib.auth.admin import UserAdmin as BaseUserAdmin from users.models import User -from tle.models import CrewMember +from crews.models import CrewMember @admin.register(User) From 5268732b0aa43c36f41ae566cf46e75c3f7648fe Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:22:47 +0900 Subject: [PATCH 317/552] =?UTF-8?q?refactor:=20`Submission`=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EA=B8=B0=EB=8A=A5=EC=9D=84=20`submissions`=20?= =?UTF-8?q?=EC=95=B1=EC=9C=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(tle=20?= =?UTF-8?q?=EC=95=B1=20=EC=82=AD=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/app/settings.py | 2 +- app/{tle => submissions}/__init__.py | 0 .../__init__.py => submissions/admin.py} | 2 +- app/{tle => submissions}/apps.py | 4 +- .../migrations/__init__.py | 0 app/submissions/models/__init__.py | 8 + .../dao => submissions/models}/submission.py | 16 +- .../models}/submission_comment.py | 8 +- app/submissions/views.py | 3 + app/tle/models/__init__.py | 8 - app/tle/services/__init__.py | 0 app/tle/services/data/__init__.py | 0 app/tle/services/data/parser/__init__.py | 2 - app/tle/services/data/parser/base.py | 20 - app/tle/services/data/parser/parsers.py | 43 - app/tle/services/data/parser/runner.py | 42 - app/tle/services/data/raw-languages.json | 46 - app/tle/services/data/raw-problems.json | 81 - app/tle/services/data/raw-tags.json | 5944 ----------------- app/tle/tests.py | 3 - 20 files changed, 23 insertions(+), 6209 deletions(-) rename app/{tle => submissions}/__init__.py (100%) rename app/{tle/admin/__init__.py => submissions/admin.py} (75%) rename app/{tle => submissions}/apps.py (60%) rename app/{tle => submissions}/migrations/__init__.py (100%) create mode 100644 app/submissions/models/__init__.py rename app/{tle/models/dao => submissions/models}/submission.py (75%) rename app/{tle/models/dao => submissions/models}/submission_comment.py (83%) create mode 100644 app/submissions/views.py delete mode 100644 app/tle/models/__init__.py delete mode 100644 app/tle/services/__init__.py delete mode 100644 app/tle/services/data/__init__.py delete mode 100644 app/tle/services/data/parser/__init__.py delete mode 100644 app/tle/services/data/parser/base.py delete mode 100644 app/tle/services/data/parser/parsers.py delete mode 100644 app/tle/services/data/parser/runner.py delete mode 100644 app/tle/services/data/raw-languages.json delete mode 100644 app/tle/services/data/raw-problems.json delete mode 100644 app/tle/services/data/raw-tags.json delete mode 100644 app/tle/tests.py diff --git a/app/app/settings.py b/app/app/settings.py index 44c8391..ea7da7d 100644 --- a/app/app/settings.py +++ b/app/app/settings.py @@ -46,7 +46,7 @@ "users", "problems", "crews", - "tle", + "submissions", ] MIDDLEWARE = [ diff --git a/app/tle/__init__.py b/app/submissions/__init__.py similarity index 100% rename from app/tle/__init__.py rename to app/submissions/__init__.py diff --git a/app/tle/admin/__init__.py b/app/submissions/admin.py similarity index 75% rename from app/tle/admin/__init__.py rename to app/submissions/admin.py index a372c9f..5a5005b 100644 --- a/app/tle/admin/__init__.py +++ b/app/submissions/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from tle.models import * +from submissions.models import * admin.site.register([ diff --git a/app/tle/apps.py b/app/submissions/apps.py similarity index 60% rename from app/tle/apps.py rename to app/submissions/apps.py index dcf2508..1ffc7b6 100644 --- a/app/tle/apps.py +++ b/app/submissions/apps.py @@ -1,6 +1,6 @@ from django.apps import AppConfig -class TleConfig(AppConfig): +class SubmissionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "tle" + name = "submissions" diff --git a/app/tle/migrations/__init__.py b/app/submissions/migrations/__init__.py similarity index 100% rename from app/tle/migrations/__init__.py rename to app/submissions/migrations/__init__.py diff --git a/app/submissions/models/__init__.py b/app/submissions/models/__init__.py new file mode 100644 index 0000000..f8dcd36 --- /dev/null +++ b/app/submissions/models/__init__.py @@ -0,0 +1,8 @@ +from submissions.models.submission import Submission +from submissions.models.submission_comment import SubmissionComment + + +__all__ = ( + 'Submission', + 'SubmissionComment', +) diff --git a/app/tle/models/dao/submission.py b/app/submissions/models/submission.py similarity index 75% rename from app/tle/models/dao/submission.py rename to app/submissions/models/submission.py index 1573fe6..879d4ba 100644 --- a/app/tle/models/dao/submission.py +++ b/app/submissions/models/submission.py @@ -1,12 +1,12 @@ from django.db import models -from crews.models import CrewActivityProblem, CrewSubmittableLanguage +from crews.models import CrewActivityProblem, ProgrammingLanguageChoices from users.models import User class Submission(models.Model): # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 - activity_problem = models.ForeignKey( + problem = models.ForeignKey( CrewActivityProblem, on_delete=models.PROTECT, help_text='활동 문제를 입력해주세요.', @@ -19,9 +19,8 @@ class Submission(models.Model): code = models.TextField( help_text='유저의 코드를 입력해주세요.', ) - language = models.ForeignKey( - CrewSubmittableLanguage, - on_delete=models.PROTECT, + language = models.TextField( + choices=ProgrammingLanguageChoices.choices, help_text='유저의 코드 언어를 입력해주세요.', ) is_correct = models.BooleanField( @@ -35,7 +34,7 @@ class Submission(models.Model): updated_at = models.DateTimeField(auto_now=True) class field_name: - ACTIVITY_PROBLEM = 'activity_problem' + PROBLEM = 'problem' USER = 'user' CODE = 'code' LANGUAGE = 'language' @@ -47,8 +46,5 @@ class field_name: class Meta: ordering = ['created_at'] - def __repr__(self) -> str: - return f'{self.activity_problem.__repr__()} ← {self.user.__repr__()} ({self.language.name})' - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' + return f'[{self.pk} : {self.problem} ← {self.user}]' diff --git a/app/tle/models/dao/submission_comment.py b/app/submissions/models/submission_comment.py similarity index 83% rename from app/tle/models/dao/submission_comment.py rename to app/submissions/models/submission_comment.py index 06fcb6b..7b094b4 100644 --- a/app/tle/models/dao/submission_comment.py +++ b/app/submissions/models/submission_comment.py @@ -1,8 +1,8 @@ from django.core.validators import MinValueValidator from django.db import models +from submissions.models.submission import Submission from users.models import User -from tle.models.dao.submission import Submission class SubmissionComment(models.Model): @@ -51,9 +51,5 @@ class field_name: class Meta: ordering = ['-created_at'] - def __repr__(self) -> str: - line_range = f'L{self.line_number_start}:L{self.line_number_end}' - return f'{self.submission.__repr__()} ← {self.created_by.__repr__()} {line_range} "{self.content}"' - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' + return f'[{self.pk} : {self.submission} ← {self.created_by}] L{self.line_number_start}:L{self.line_number_end}' diff --git a/app/submissions/views.py b/app/submissions/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/app/submissions/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/app/tle/models/__init__.py b/app/tle/models/__init__.py deleted file mode 100644 index e0b6cfa..0000000 --- a/app/tle/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from tle.models.dao.submission import Submission -from tle.models.dao.submission_comment import SubmissionComment - - -__all__ = ( - 'Submission', - 'SubmissionComment', -) diff --git a/app/tle/services/__init__.py b/app/tle/services/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tle/services/data/__init__.py b/app/tle/services/data/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/tle/services/data/parser/__init__.py b/app/tle/services/data/parser/__init__.py deleted file mode 100644 index 720f234..0000000 --- a/app/tle/services/data/parser/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from tle.services.data.parser.parsers import ModelParser -from tle.services.data.parser.runner import parse diff --git a/app/tle/services/data/parser/base.py b/app/tle/services/data/parser/base.py deleted file mode 100644 index 9f1722a..0000000 --- a/app/tle/services/data/parser/base.py +++ /dev/null @@ -1,20 +0,0 @@ -import json -import typing - - -T = typing.TypeVar('T') - - -class ModelParser(typing.Generic[T]): - def parse_json(self, file: str, many=False) -> typing.List[T]: - with open(file) as f: - data = json.load(f) - return self.parse(data, many=many) - - def parse(self, data: dict, many=False) -> typing.List[T]: - if not many: - return [self.perform_parse(data)] - return [self.perform_parse(item) for item in data['items']] - - def perform_parse(self, item: dict) -> T: - raise NotImplementedError diff --git a/app/tle/services/data/parser/parsers.py b/app/tle/services/data/parser/parsers.py deleted file mode 100644 index 36330db..0000000 --- a/app/tle/services/data/parser/parsers.py +++ /dev/null @@ -1,43 +0,0 @@ -from datetime import datetime - -from tle.models import * -from tle.services.data.parser.base import ModelParser - - -class ProblemParser(ModelParser[Problem]): - def perform_parse(self, item: dict) -> Problem: - return Problem.objects.create( - title=item['title'], - link=item['link'], - description=item['description'], - input_description=item['input_description'], - output_description=item['output_description'], - time_limit=item['time_limit'], - memory_limit=item['memory_limit'], - created_at=datetime.fromisoformat(item['created_at']), - updated_at=datetime.fromisoformat(item['updated_at']), - ) - - -class ProblemTagParser(ModelParser[ProblemTag]): - def perform_parse(self, item: dict) -> ProblemTag: - return ProblemTag.objects.create( - pk=item['bojTagId'], - parent=None, - key=item['key'], - name_ko=self._find(item['displayNames'], 'ko')['name'], - name_en=self._find(item['displayNames'], 'en')['name'], - ) - - def _find(self, display_names: list[dict], language: str) -> dict: - return next(filter(lambda x: x['language'] == language, display_names)) - - -class SubmissionLanguageParser(ModelParser[SubmissionLanguage]): - def perform_parse(self, item: dict) -> SubmissionLanguage: - return SubmissionLanguage.objects.create( - pk=item['bojId'], - key=item['key'], - name=item['displayName'], - extension=item['extension'], - ) diff --git a/app/tle/services/data/parser/runner.py b/app/tle/services/data/parser/runner.py deleted file mode 100644 index 641b49b..0000000 --- a/app/tle/services/data/parser/runner.py +++ /dev/null @@ -1,42 +0,0 @@ -import typing - -from django.conf import settings - -from tle.models import * -from tle.services.data.parser.parsers import * - - -DATA_DIR = settings.BASE_DIR / 'tle/services/data' - - -T = typing.TypeVar('T') - - -def get_model_parser(model_class: T) -> ModelParser[T]: - PARSERS = { - Problem: ProblemParser, - ProblemTag: ProblemTagParser, - SubmissionLanguage: SubmissionLanguageParser, - } - if model_class not in PARSERS: - raise NotImplementedError( - f'Parser for {model_class} is not implemented') - return PARSERS[model_class]() - - -def get_model_default_json_file(model_class: T) -> str: - FILES = { - Problem: DATA_DIR / 'raw-problems.json', - ProblemTag: DATA_DIR / 'raw-tags.json', - SubmissionLanguage: DATA_DIR / 'raw-languages.json', - } - if model_class not in FILES: - raise NotImplementedError(f'Default JSON file for {model_class} is not implemented') - return FILES[model_class] - - -def parse(model_class: T, file: str = None) -> typing.List[T]: - parser = get_model_parser(model_class) - if file is None: - file = get_model_default_json_file(model_class) - return parser.parse_json(file, many=True) diff --git a/app/tle/services/data/raw-languages.json b/app/tle/services/data/raw-languages.json deleted file mode 100644 index 9537e6a..0000000 --- a/app/tle/services/data/raw-languages.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "items": [ - { - "key": "nodejs", - "bojId": 17, - "displayName": "Node.js", - "extension": ".js" - }, - { - "key": "kotlin", - "bojId": 69, - "displayName": "Kotlin", - "extension": ".kt" - }, - { - "key": "swift", - "bojId": 74, - "displayName": "Swift", - "extension": ".swift" - }, - { - "key": "cpp", - "bojId": 1001, - "displayName": "C++", - "extension": ".cpp" - }, - { - "key": "java", - "bojId": 1002, - "displayName": "Java", - "extension": ".java" - }, - { - "key": "python", - "bojId": 1003, - "displayName": "Python", - "extension": ".py" - }, - { - "key": "c", - "bojId": 1004, - "displayName": "C", - "extension": ".c" - } - ] -} \ No newline at end of file diff --git a/app/tle/services/data/raw-problems.json b/app/tle/services/data/raw-problems.json deleted file mode 100644 index 8d6f000..0000000 --- a/app/tle/services/data/raw-problems.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "items": [ - { - "title": "A+B", - "link": "https://www.acmicpc.net/problem/1000", - "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", - "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", - "output_description": "첫째 줄에 A+B를 출력한다.", - "time_limit": 1.0, - "memory_limit": 128.0, - "created_at": "2024-06-03 13:24:06.988383", - "updated_at": "2024-06-03 13:24:06.988446" - }, - { - "title": "피보나치 함수", - "link": "https://www.acmicpc.net/problem/1003", - "description": "다음 소스는 N번째 피보나치 수를 구하는 C++ 함수이다.\r\n\r\nint fibonacci(int n) {\r\n if (n == 0) {\r\n printf(\"0\");\r\n return 0;\r\n } else if (n == 1) {\r\n printf(\"1\");\r\n return 1;\r\n } else {\r\n return fibonacci(n‐1) + fibonacci(n‐2);\r\n }\r\n}\r\nfibonacci(3)을 호출하면 다음과 같은 일이 일어난다.\r\n\r\nfibonacci(3)은 fibonacci(2)와 fibonacci(1) (첫 번째 호출)을 호출한다.\r\nfibonacci(2)는 fibonacci(1) (두 번째 호출)과 fibonacci(0)을 호출한다.\r\n두 번째 호출한 fibonacci(1)은 1을 출력하고 1을 리턴한다.\r\nfibonacci(0)은 0을 출력하고, 0을 리턴한다.\r\nfibonacci(2)는 fibonacci(1)과 fibonacci(0)의 결과를 얻고, 1을 리턴한다.\r\n첫 번째 호출한 fibonacci(1)은 1을 출력하고, 1을 리턴한다.\r\nfibonacci(3)은 fibonacci(2)와 fibonacci(1)의 결과를 얻고, 2를 리턴한다.\r\n1은 2번 출력되고, 0은 1번 출력된다. N이 주어졌을 때, fibonacci(N)을 호출했을 때, 0과 1이 각각 몇 번 출력되는지 구하는 프로그램을 작성하시오.", - "input_description": "첫째 줄에 테스트 케이스의 개수 T가 주어진다.\r\n\r\n각 테스트 케이스는 한 줄로 이루어져 있고, N이 주어진다. N은 40보다 작거나 같은 자연수 또는 0이다.", - "output_description": "각 테스트 케이스마다 0이 출력되는 횟수와 1이 출력되는 횟수를 공백으로 구분해서 출력한다.", - "time_limit": 0.25, - "memory_limit": 128.0, - "created_at": "2024-06-03 18:24:21.358190", - "updated_at": "2024-06-03 18:24:21.358210" - }, - { - "title": "평점 변환", - "link": "https://www.acmicpc.net/problem/31799", - "description": "2023학년도까지 대구과고에서는 학생들의 한 학기 동안의 성적에 따라 A+, A0, A-, B+, B0, B-, C+, C0, C-의 아홉 가지 평어 가운데 하나를 부여하였다. 그러나 상대평가 중심의 평어 체제는 학생들 간의 과도한 경쟁을 유도하는 부작용이 있었다. 그래서 2024학년도부터는 B(Beginning), D(Developing), P(Proficient), E(Exceeding)의 네 가지 평어 가운데 하나를 부여하는 방식으로 체제를 바꿀 계획이다. 새로운 평어 체제는 상대평가 기간이 아닌 개인의 성장 과정에 따라 평어가 부여되는 방식이므로 기존 평어 체제의 문제점을 해결할 것으로 기대하고 있다.\r\n\r\n대구과고 학생들은 2023학년도 이전과 2024학년도 이후의 평어 체제가 완전히 달라서 자신의 발전 과정을 정확하게 알기 어려워졌다. 이에 따라 2023학년도 이전의 평어를 새로운 평어 체제에 맞추어 변환하는 공식적인 기준을 발표하였다.\r\n\r\n평어가 C+, C0, C- 가운데 하나이면, 새로운 평어는 B이다.\r\n평어가 B0, B- 가운데 하나이면\r\n첫 학기이거나 이전 학기의 평어가 C+, C0, C- 가운데 하나이면, 새로운 평어는 D이다.\r\n이전 학기의 평어가 A+, A0, A-, B+, B0, B- 가운데 하나이면, 새로운 평어는 B이다.\r\n평어가 A-, B+ 가운데 하나이면\r\n첫 학기이거나 이전 학기의 평어가 B0, B-, C+, C0, C- 가운데 하나이면, 새로운 평어는 P이다.\r\n이전 학기의 평어가 A+, A0, A-, B+ 가운데 하나이면, 새로운 평어는 D이다.\r\n평어가 A0이면\r\n첫 학기이거나 이전 학기의 평어가 A-, B+, B0, B-, C+, C0, C- 가운데 하나이면, 새로운 평어는 E이다.\r\n이전 학기의 평어가 A+, A0 가운데 하나이면, 새로운 평어는 P이다.\r\n평어가 A+이면 새로운 평어는 E이다.\r\n대구과고에 다니는 은성이는 기존 평어 체제로 부여되었던 자신의 $N$학기 동안의 평어를 새로운 평어 체제에 맞게 변환하고 싶다. 하지만 평어 변환 기준이 너무 복잡해 여러분에게 대신 이 일을 맡기려고 한다. $N$학기 동안의 평어가 첫 학기부터 $N$번째 학기까지 순서대로 공백 없이 주어질 때, 새로운 평어 체제에 맞게 변환한 결과를 출력하는 프로그램을 작성하라. 단, 은성이는 A0, B0, C0에서 실수로 0을 생략하여 'A', 'B', 'C'와 같이 적을 때도 있다고 한다.", - "input_description": "첫 번째 줄에 은성이가 대구과고에 다닌 학기의 수 $N$이 주어진다.\r\n\r\n두 번째 줄에 은성이의 $N$학기 동안의 평어를 공백 없이 순서대로 나열한 문자열이 주어진다.\r\n\r\n $1\\le N\\le 200\\,000$", - "output_description": "은성이의 $N$학기 동안의 평어를 새로운 체제에 맞게 변환한 결과를 첫 학기부터 공백 없이 순서대로 나열한 길이 $N$의 문자열을 출력한다.", - "time_limit": 1.0, - "memory_limit": 1024.0, - "created_at": "2024-06-16 13:51:26.551732", - "updated_at": "2024-06-16 13:51:26.551746" - }, - { - "title": "CCW", - "link": "https://www.acmicpc.net/problem/11758", - "description": "2차원 좌표 평면 위에 있는 점 3개 P1, P2, P3가 주어진다. P1, P2, P3를 순서대로 이은 선분이 어떤 방향을 이루고 있는지 구하는 프로그램을 작성하시오.", - "input_description": "첫째 줄에 P1의 (x1, y1), 둘째 줄에 P2의 (x2, y2), 셋째 줄에 P3의 (x3, y3)가 주어진다. (-10,000 ≤ x1, y1, x2, y2, x3, y3 ≤ 10,000) 모든 좌표는 정수이다. P1, P2, P3의 좌표는 서로 다르다.", - "output_description": "P1, P2, P3를 순서대로 이은 선분이 반시계 방향을 나타내면 1, 시계 방향이면 -1, 일직선이면 0을 출력한다.", - "time_limit": 1.0, - "memory_limit": 256.0, - "created_at": "2024-06-16 13:54:02.295495", - "updated_at": "2024-06-16 13:54:02.295540" - }, - { - "title": "접두사 찾기", - "link": "https://www.acmicpc.net/problem/14426", - "description": "문자열 S의 접두사란 S의 가장 앞에서부터 부분 문자열을 의미한다. 예를 들어, S = \"codeplus\"의 접두사는 \"code\", \"co\", \"codepl\", \"codeplus\"가 있고, \"plus\", \"s\", \"cude\", \"crud\"는 접두사가 아니다.\r\n\r\n총 N개의 문자열로 이루어진 집합 S가 주어진다.\r\n\r\n입력으로 주어지는 M개의 문자열 중에서 집합 S에 포함되어 있는 문자열 중 적어도 하나의 접두사인 것의 개수를 구하는 프로그램을 작성하시오.", - "input_description": "첫째 줄에 문자열의 개수 N과 M (1 ≤ N ≤ 10,000, 1 ≤ M ≤ 10,000)이 주어진다.\r\n\r\n다음 N개의 줄에는 집합 S에 포함되어 있는 문자열이 주어진다.\r\n\r\n다음 M개의 줄에는 검사해야 하는 문자열이 주어진다.\r\n\r\n입력으로 주어지는 문자열은 알파벳 소문자로만 이루어져 있으며, 길이는 500을 넘지 않는다. 집합 S에 같은 문자열이 여러 번 주어지는 경우는 없다.", - "output_description": "첫째 줄에 M개의 문자열 중에 총 몇 개가 포함되어 있는 문자열 중 적어도 하나의 접두사인지 출력한다.", - "time_limit": 1.0, - "memory_limit": 1536.0, - "created_at": "2024-06-16 13:55:49.874302", - "updated_at": "2024-06-16 13:55:49.874324" - }, - { - "title": "벽 부수고 이동하기", - "link": "https://www.acmicpc.net/problem/2206", - "description": "N×M의 행렬로 표현되는 맵이 있다. 맵에서 0은 이동할 수 있는 곳을 나타내고, 1은 이동할 수 없는 벽이 있는 곳을 나타낸다. 당신은 (1, 1)에서 (N, M)의 위치까지 이동하려 하는데, 이때 최단 경로로 이동하려 한다. 최단경로는 맵에서 가장 적은 개수의 칸을 지나는 경로를 말하는데, 이때 시작하는 칸과 끝나는 칸도 포함해서 센다.\r\n\r\n만약에 이동하는 도중에 한 개의 벽을 부수고 이동하는 것이 좀 더 경로가 짧아진다면, 벽을 한 개 까지 부수고 이동하여도 된다.\r\n\r\n한 칸에서 이동할 수 있는 칸은 상하좌우로 인접한 칸이다.\r\n\r\n맵이 주어졌을 때, 최단 경로를 구해 내는 프로그램을 작성하시오.", - "input_description": "첫째 줄에 N(1 ≤ N ≤ 1,000), M(1 ≤ M ≤ 1,000)이 주어진다. 다음 N개의 줄에 M개의 숫자로 맵이 주어진다. (1, 1)과 (N, M)은 항상 0이라고 가정하자.", - "output_description": "첫째 줄에 최단 거리를 출력한다. 불가능할 때는 -1을 출력한다.", - "time_limit": 2.0, - "memory_limit": 192.0, - "created_at": "2024-06-16 13:56:30.174235", - "updated_at": "2024-06-16 13:56:30.174268" - }, - { - "title": "용액", - "link": "https://www.acmicpc.net/problem/2467", - "description": "KOI 부설 과학연구소에서는 많은 종류의 산성 용액과 알칼리성 용액을 보유하고 있다. 각 용액에는 그 용액의 특성을 나타내는 하나의 정수가 주어져있다. 산성 용액의 특성값은 1부터 1,000,000,000까지의 양의 정수로 나타내고, 알칼리성 용액의 특성값은 -1부터 -1,000,000,000까지의 음의 정수로 나타낸다.\r\n\r\n같은 양의 두 용액을 혼합한 용액의 특성값은 혼합에 사용된 각 용액의 특성값의 합으로 정의한다. 이 연구소에서는 같은 양의 두 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들려고 한다. \r\n\r\n예를 들어, 주어진 용액들의 특성값이 [-99, -2, -1, 4, 98]인 경우에는 특성값이 -99인 용액과 특성값이 98인 용액을 혼합하면 특성값이 -1인 용액을 만들 수 있고, 이 용액의 특성값이 0에 가장 가까운 용액이다. 참고로, 두 종류의 알칼리성 용액만으로나 혹은 두 종류의 산성 용액만으로 특성값이 0에 가장 가까운 혼합 용액을 만드는 경우도 존재할 수 있다.\r\n\r\n산성 용액과 알칼리성 용액의 특성값이 정렬된 순서로 주어졌을 때, 이 중 두 개의 서로 다른 용액을 혼합하여 특성값이 0에 가장 가까운 용액을 만들어내는 두 용액을 찾는 프로그램을 작성하시오.", - "input_description": "첫째 줄에는 전체 용액의 수 N이 입력된다. N은 2 이상 100,000 이하의 정수이다. 둘째 줄에는 용액의 특성값을 나타내는 N개의 정수가 빈칸을 사이에 두고 오름차순으로 입력되며, 이 수들은 모두 -1,000,000,000 이상 1,000,000,000 이하이다. N개의 용액들의 특성값은 모두 서로 다르고, 산성 용액만으로나 알칼리성 용액만으로 입력이 주어지는 경우도 있을 수 있다.", - "output_description": "첫째 줄에 특성값이 0에 가장 가까운 용액을 만들어내는 두 용액의 특성값을 출력한다. 출력해야 하는 두 용액은 특성값의 오름차순으로 출력한다. 특성값이 0에 가장 가까운 용액을 만들어내는 경우가 두 개 이상일 경우에는 그 중 아무것이나 하나를 출력한다.", - "time_limit": 1.0, - "memory_limit": 128.0, - "created_at": "2024-06-16 13:57:24.447647", - "updated_at": "2024-06-16 13:57:24.447661" - } - ] -} \ No newline at end of file diff --git a/app/tle/services/data/raw-tags.json b/app/tle/services/data/raw-tags.json deleted file mode 100644 index d21c414..0000000 --- a/app/tle/services/data/raw-tags.json +++ /dev/null @@ -1,5944 +0,0 @@ -{ - "count": 206, - "items": [ - { - "key": "math", - "isMeta": false, - "bojTagId": 124, - "problemCount": 6212, - "displayNames": [ - { - "language": "ko", - "name": "수학", - "short": "수학" - }, - { - "language": "en", - "name": "mathematics", - "short": "math" - }, - { - "language": "ja", - "name": "数学", - "short": "数学" - } - ], - "aliases": [] - }, - { - "key": "implementation", - "isMeta": false, - "bojTagId": 102, - "problemCount": 5399, - "displayNames": [ - { - "language": "ko", - "name": "구현", - "short": "구현" - }, - { - "language": "en", - "name": "implementation", - "short": "impl" - }, - { - "language": "ja", - "name": "実装", - "short": "impl" - } - ], - "aliases": [] - }, - { - "key": "dp", - "isMeta": false, - "bojTagId": 25, - "problemCount": 3941, - "displayNames": [ - { - "language": "ko", - "name": "다이나믹 프로그래밍", - "short": "다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming", - "short": "dp" - }, - { - "language": "ja", - "name": "動的計画法", - "short": "dp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "동적 계획법" - }, - { - "alias": "다이나믹프로그래밍" - } - ] - }, - { - "key": "data_structures", - "isMeta": false, - "bojTagId": 175, - "problemCount": 3732, - "displayNames": [ - { - "language": "ko", - "name": "자료 구조", - "short": "자료 구조" - }, - { - "language": "en", - "name": "data structures", - "short": "ds" - }, - { - "language": "ja", - "name": "データ構造", - "short": "ds" - } - ], - "aliases": [ - { - "alias": "자료구조" - }, - { - "alias": "자구" - } - ] - }, - { - "key": "graphs", - "isMeta": false, - "bojTagId": 7, - "problemCount": 3600, - "displayNames": [ - { - "language": "ko", - "name": "그래프 이론", - "short": "그래프 이론" - }, - { - "language": "en", - "name": "graph theory", - "short": "graph" - }, - { - "language": "ja", - "name": "グラフ理論", - "short": "グラフ" - } - ], - "aliases": [ - { - "alias": "그래프이론" - }, - { - "alias": "그래프" - } - ] - }, - { - "key": "greedy", - "isMeta": false, - "bojTagId": 33, - "problemCount": 2461, - "displayNames": [ - { - "language": "ko", - "name": "그리디 알고리즘", - "short": "그리디 알고리즘" - }, - { - "language": "en", - "name": "greedy", - "short": "greedy" - }, - { - "language": "ja", - "name": "貪欲法", - "short": "貪欲法" - } - ], - "aliases": [ - { - "alias": "탐욕법" - } - ] - }, - { - "key": "string", - "isMeta": false, - "bojTagId": 158, - "problemCount": 2340, - "displayNames": [ - { - "language": "ko", - "name": "문자열", - "short": "문자열" - }, - { - "language": "en", - "name": "string", - "short": "string" - }, - { - "language": "ja", - "name": "文字列", - "short": "文字列" - } - ], - "aliases": [ - { - "alias": "스트링" - } - ] - }, - { - "key": "bruteforcing", - "isMeta": false, - "bojTagId": 125, - "problemCount": 2132, - "displayNames": [ - { - "language": "ko", - "name": "브루트포스 알고리즘", - "short": "브루트포스 알고리즘" - }, - { - "language": "en", - "name": "bruteforcing", - "short": "bruteforce" - }, - { - "language": "ja", - "name": "全探索", - "short": "全探索" - } - ], - "aliases": [ - { - "alias": "완전탐색" - }, - { - "alias": "완전 탐색" - }, - { - "alias": "브루트포스" - }, - { - "alias": "bruteforce" - }, - { - "alias": "brute force" - }, - { - "alias": "완탐" - } - ] - }, - { - "key": "graph_traversal", - "isMeta": false, - "bojTagId": 11, - "problemCount": 1960, - "displayNames": [ - { - "language": "ko", - "name": "그래프 탐색", - "short": "그래프 탐색" - }, - { - "language": "en", - "name": "graph traversal", - "short": "traversal" - }, - { - "language": "ja", - "name": "グラフの探索", - "short": "横断" - } - ], - "aliases": [ - { - "alias": "bfs" - }, - { - "alias": "dfs" - } - ] - }, - { - "key": "sorting", - "isMeta": false, - "bojTagId": 97, - "problemCount": 1817, - "displayNames": [ - { - "language": "ko", - "name": "정렬", - "short": "정렬" - }, - { - "language": "en", - "name": "sorting", - "short": "sorting" - }, - { - "language": "ja", - "name": "ソート", - "short": "ソート" - } - ], - "aliases": [] - }, - { - "key": "geometry", - "isMeta": false, - "bojTagId": 100, - "problemCount": 1474, - "displayNames": [ - { - "language": "ko", - "name": "기하학", - "short": "기하학" - }, - { - "language": "en", - "name": "geometry", - "short": "geom" - }, - { - "language": "ja", - "name": "幾何学", - "short": "幾何" - } - ], - "aliases": [] - }, - { - "key": "ad_hoc", - "isMeta": false, - "bojTagId": 109, - "problemCount": 1424, - "displayNames": [ - { - "language": "ko", - "name": "애드 혹", - "short": "애드 혹" - }, - { - "language": "en", - "name": "ad-hoc", - "short": "ad-hoc" - }, - { - "language": "ja", - "name": "アドホック", - "short": "アドホック" - } - ], - "aliases": [] - }, - { - "key": "number_theory", - "isMeta": false, - "bojTagId": 95, - "problemCount": 1399, - "displayNames": [ - { - "language": "ko", - "name": "정수론", - "short": "정수론" - }, - { - "language": "en", - "name": "number theory", - "short": "number theory" - }, - { - "language": "ja", - "name": "整数論", - "short": "整数論" - } - ], - "aliases": [] - }, - { - "key": "trees", - "isMeta": false, - "bojTagId": 120, - "problemCount": 1359, - "displayNames": [ - { - "language": "ko", - "name": "트리", - "short": "트리" - }, - { - "language": "en", - "name": "tree", - "short": "tree" - }, - { - "language": "ja", - "name": "木", - "short": "木" - } - ], - "aliases": [ - { - "alias": "trees" - } - ] - }, - { - "key": "segtree", - "isMeta": false, - "bojTagId": 65, - "problemCount": 1275, - "displayNames": [ - { - "language": "ko", - "name": "세그먼트 트리", - "short": "세그먼트 트리" - }, - { - "language": "en", - "name": "segment tree", - "short": "segtree" - }, - { - "language": "ja", - "name": "セグメント木", - "short": "セグ木" - } - ], - "aliases": [ - { - "alias": "구간트리" - }, - { - "alias": "세그트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "binary_search", - "isMeta": false, - "bojTagId": 12, - "problemCount": 1194, - "displayNames": [ - { - "language": "ko", - "name": "이분 탐색", - "short": "이분 탐색" - }, - { - "language": "en", - "name": "binary search", - "short": "binary search" - }, - { - "language": "ja", - "name": "二分探索", - "short": "二分探索" - } - ], - "aliases": [ - { - "alias": "이분탐색" - }, - { - "alias": "이진탐색" - } - ] - }, - { - "key": "arithmetic", - "isMeta": false, - "bojTagId": 121, - "problemCount": 1093, - "displayNames": [ - { - "language": "ko", - "name": "사칙연산", - "short": "사칙연산" - }, - { - "language": "en", - "name": "arithmetic", - "short": "arithmetic" - }, - { - "language": "ja", - "name": "算数", - "short": "算数" - } - ], - "aliases": [ - { - "alias": "덧셈" - }, - { - "alias": "뺄셈" - }, - { - "alias": "곱셈" - }, - { - "alias": "나눗셈" - }, - { - "alias": "더하기" - }, - { - "alias": "빼기" - }, - { - "alias": "곱하기" - }, - { - "alias": "나누기" - } - ] - }, - { - "key": "simulation", - "isMeta": false, - "bojTagId": 141, - "problemCount": 1054, - "displayNames": [ - { - "language": "ko", - "name": "시뮬레이션", - "short": "시뮬레이션" - }, - { - "language": "en", - "name": "simulation", - "short": "simulation" - }, - { - "language": "ja", - "name": "シミュレーション", - "short": "シミュレーション" - } - ], - "aliases": [] - }, - { - "key": "constructive", - "isMeta": false, - "bojTagId": 128, - "problemCount": 970, - "displayNames": [ - { - "language": "ko", - "name": "해 구성하기", - "short": "해 구성하기" - }, - { - "language": "en", - "name": "constructive", - "short": "constructive" - }, - { - "language": "ja", - "name": "構成的", - "short": "構成的" - } - ], - "aliases": [ - { - "alias": "constructive" - }, - { - "alias": "컨스트럭티브" - }, - { - "alias": "구성적" - } - ] - }, - { - "key": "bfs", - "isMeta": false, - "bojTagId": 126, - "problemCount": 962, - "displayNames": [ - { - "language": "ko", - "name": "너비 우선 탐색", - "short": "너비 우선 탐색" - }, - { - "language": "en", - "name": "breadth-first search", - "short": "bfs" - }, - { - "language": "ja", - "name": "幅優先検索", - "short": "bfs" - } - ], - "aliases": [ - { - "alias": "breadthfirst" - }, - { - "alias": "breadth first" - } - ] - }, - { - "key": "prefix_sum", - "isMeta": false, - "bojTagId": 139, - "problemCount": 925, - "displayNames": [ - { - "language": "ko", - "name": "누적 합", - "short": "누적 합" - }, - { - "language": "en", - "name": "prefix sum", - "short": "prefix sum" - }, - { - "language": "ja", - "name": "累積和", - "short": "累積和" - } - ], - "aliases": [ - { - "alias": "구간합" - }, - { - "alias": "부분합" - }, - { - "alias": "rangesum" - } - ] - }, - { - "key": "combinatorics", - "isMeta": false, - "bojTagId": 6, - "problemCount": 879, - "displayNames": [ - { - "language": "ko", - "name": "조합론", - "short": "조합론" - }, - { - "language": "en", - "name": "combinatorics", - "short": "combinatorics" - }, - { - "language": "ja", - "name": "組み合わせ", - "short": "組み合わせ" - } - ], - "aliases": [ - { - "alias": "combination" - }, - { - "alias": "permutation" - }, - { - "alias": "probability" - }, - { - "alias": "확률" - }, - { - "alias": "순열" - } - ] - }, - { - "key": "case_work", - "isMeta": false, - "bojTagId": 137, - "problemCount": 838, - "displayNames": [ - { - "language": "ko", - "name": "많은 조건 분기", - "short": "많은 조건 분기" - }, - { - "language": "en", - "name": "case work", - "short": "case work" - }, - { - "language": "ja", - "name": "ケースワーク", - "short": "ケースワーク" - } - ], - "aliases": [ - { - "alias": "케이스" - }, - { - "alias": "케이스워크" - }, - { - "alias": "케이스 워크" - } - ] - }, - { - "key": "dfs", - "isMeta": false, - "bojTagId": 127, - "problemCount": 795, - "displayNames": [ - { - "language": "ko", - "name": "깊이 우선 탐색", - "short": "깊이 우선 탐색" - }, - { - "language": "en", - "name": "depth-first search", - "short": "dfs" - }, - { - "language": "ja", - "name": "深さ優先探索", - "short": "dfs" - } - ], - "aliases": [ - { - "alias": "depth first" - }, - { - "alias": "depthfirst" - } - ] - }, - { - "key": "shortest_path", - "isMeta": false, - "bojTagId": 215, - "problemCount": 754, - "displayNames": [ - { - "language": "ko", - "name": "최단 경로", - "short": "최단 경로" - }, - { - "language": "en", - "name": "shortest path", - "short": "shortest path" - }, - { - "language": "ja", - "name": "最短経路", - "short": "最短経路" - } - ], - "aliases": [] - }, - { - "key": "bitmask", - "isMeta": false, - "bojTagId": 14, - "problemCount": 704, - "displayNames": [ - { - "language": "ko", - "name": "비트마스킹", - "short": "비트마스킹" - }, - { - "language": "en", - "name": "bitmask", - "short": "bitmask" - }, - { - "language": "ja", - "name": "ビット表現", - "short": "ビット表現" - } - ], - "aliases": [ - { - "alias": "비트필드" - }, - { - "alias": "비트마스크" - } - ] - }, - { - "key": "hash_set", - "isMeta": false, - "bojTagId": 136, - "problemCount": 620, - "displayNames": [ - { - "language": "ko", - "name": "해시를 사용한 집합과 맵", - "short": "해시를 사용한 집합과 맵" - }, - { - "language": "en", - "name": "set / map by hashing", - "short": "hashset" - }, - { - "language": "ja", - "name": "ハッシュ化によるセット・マップ", - "short": "hashset" - } - ], - "aliases": [ - { - "alias": "집합" - }, - { - "alias": "맵" - }, - { - "alias": "셋" - }, - { - "alias": "딕셔너리" - }, - { - "alias": "dictionary" - }, - { - "alias": "map" - }, - { - "alias": "set" - }, - { - "alias": "해싱" - }, - { - "alias": "hashing" - }, - { - "alias": "dict" - } - ] - }, - { - "key": "dijkstra", - "isMeta": false, - "bojTagId": 22, - "problemCount": 572, - "displayNames": [ - { - "language": "ko", - "name": "데이크스트라", - "short": "데이크스트라" - }, - { - "language": "en", - "name": "dijkstra's", - "short": "dijkstra's" - }, - { - "language": "ja", - "name": "ダイクストラ法", - "short": "ダイクストラ法" - } - ], - "aliases": [ - { - "alias": "다익" - }, - { - "alias": "다익스트라" - }, - { - "alias": "데이크스트라" - } - ] - }, - { - "key": "backtracking", - "isMeta": false, - "bojTagId": 5, - "problemCount": 515, - "displayNames": [ - { - "language": "ko", - "name": "백트래킹", - "short": "백트래킹" - }, - { - "language": "en", - "name": "backtracking", - "short": "backtrack" - }, - { - "language": "ja", - "name": "バックトラック法", - "short": "バックトラック" - } - ], - "aliases": [ - { - "alias": "백트래킹" - }, - { - "alias": "퇴각검색" - }, - { - "alias": "퇴각 검색" - } - ] - }, - { - "key": "tree_set", - "isMeta": false, - "bojTagId": 74, - "problemCount": 485, - "displayNames": [ - { - "language": "ko", - "name": "트리를 사용한 집합과 맵", - "short": "트리를 사용한 집합과 맵" - }, - { - "language": "en", - "name": "set / map by trees", - "short": "treeset" - }, - { - "language": "ja", - "name": "木によるセット・マップ", - "short": "treeset" - } - ], - "aliases": [ - { - "alias": "집합" - }, - { - "alias": "맵" - }, - { - "alias": "셋" - }, - { - "alias": "딕셔너리" - }, - { - "alias": "dictionary" - }, - { - "alias": "map" - }, - { - "alias": "set" - }, - { - "alias": "bbst" - }, - { - "alias": "트리" - }, - { - "alias": "tree" - } - ] - }, - { - "key": "sweeping", - "isMeta": false, - "bojTagId": 106, - "problemCount": 465, - "displayNames": [ - { - "language": "ko", - "name": "스위핑", - "short": "스위핑" - }, - { - "language": "en", - "name": "sweeping", - "short": "sweeping" - }, - { - "language": "ja", - "name": "平面走査", - "short": "平面走査" - } - ], - "aliases": [ - { - "alias": "라인 스위핑" - } - ] - }, - { - "key": "disjoint_set", - "isMeta": false, - "bojTagId": 81, - "problemCount": 461, - "displayNames": [ - { - "language": "ko", - "name": "분리 집합", - "short": "분리 집합" - }, - { - "language": "en", - "name": "disjoint set", - "short": "dsu" - }, - { - "language": "ja", - "name": "素集合データ構造", - "short": "素集合データ構造" - } - ], - "aliases": [ - { - "alias": "union" - }, - { - "alias": "find" - }, - { - "alias": "유니온" - }, - { - "alias": "파인드" - }, - { - "alias": "dsu" - } - ] - }, - { - "key": "parsing", - "isMeta": false, - "bojTagId": 96, - "problemCount": 448, - "displayNames": [ - { - "language": "ko", - "name": "파싱", - "short": "파싱" - }, - { - "language": "en", - "name": "parsing", - "short": "parsing" - }, - { - "language": "ja", - "name": "パージング", - "short": "パージング" - } - ], - "aliases": [] - }, - { - "key": "priority_queue", - "isMeta": false, - "bojTagId": 59, - "problemCount": 419, - "displayNames": [ - { - "language": "ko", - "name": "우선순위 큐", - "short": "우선순위 큐" - }, - { - "language": "en", - "name": "priority queue", - "short": "priority queue" - }, - { - "language": "ja", - "name": "優先度付きキュー", - "short": "優先度付きキュー" - } - ], - "aliases": [ - { - "alias": "heap" - }, - { - "alias": "힙" - } - ] - }, - { - "key": "dp_tree", - "isMeta": false, - "bojTagId": 92, - "problemCount": 411, - "displayNames": [ - { - "language": "ko", - "name": "트리에서의 다이나믹 프로그래밍", - "short": "트리에서의 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming on trees", - "short": "tree dp" - }, - { - "language": "ja", - "name": "木上の動的計画法", - "short": "tree dp" - } - ], - "aliases": [ - { - "alias": "트리dp" - } - ] - }, - { - "key": "divide_and_conquer", - "isMeta": false, - "bojTagId": 24, - "problemCount": 406, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복", - "short": "분할 정복" - }, - { - "language": "en", - "name": "divide and conquer", - "short": "d&c" - }, - { - "language": "ja", - "name": "分割統治法", - "short": "分割統治法" - } - ], - "aliases": [ - { - "alias": "dnc" - } - ] - }, - { - "key": "two_pointer", - "isMeta": false, - "bojTagId": 80, - "problemCount": 376, - "displayNames": [ - { - "language": "ko", - "name": "두 포인터", - "short": "두 포인터" - }, - { - "language": "en", - "name": "two-pointer", - "short": "two-pointer" - }, - { - "language": "ja", - "name": "尺取り法", - "short": "尺取り" - } - ], - "aliases": [ - { - "alias": "투포인터" - }, - { - "alias": "인치웜" - }, - { - "alias": "inchworm" - }, - { - "alias": "twopointer" - } - ] - }, - { - "key": "stack", - "isMeta": false, - "bojTagId": 71, - "problemCount": 368, - "displayNames": [ - { - "language": "ko", - "name": "스택", - "short": "스택" - }, - { - "language": "en", - "name": "stack", - "short": "stack" - }, - { - "language": "ja", - "name": "スタック", - "short": "スタック" - } - ], - "aliases": [] - }, - { - "key": "parametric_search", - "isMeta": false, - "bojTagId": 170, - "problemCount": 367, - "displayNames": [ - { - "language": "ko", - "name": "매개 변수 탐색", - "short": "매개 변수 탐색" - }, - { - "language": "en", - "name": "parametric search", - "short": "parametric search" - }, - { - "language": "ja", - "name": "parametric search", - "short": "parametric search" - } - ], - "aliases": [ - { - "alias": "파라메트릭" - } - ] - }, - { - "key": "game_theory", - "isMeta": false, - "bojTagId": 140, - "problemCount": 353, - "displayNames": [ - { - "language": "ko", - "name": "게임 이론", - "short": "게임 이론" - }, - { - "language": "en", - "name": "game theory", - "short": "game theory" - }, - { - "language": "ja", - "name": "ゲーム理論", - "short": "ゲーム" - } - ], - "aliases": [ - { - "alias": "게임이론" - }, - { - "alias": "님" - }, - { - "alias": "nim" - } - ] - }, - { - "key": "flow", - "isMeta": false, - "bojTagId": 45, - "problemCount": 323, - "displayNames": [ - { - "language": "ko", - "name": "최대 유량", - "short": "최대 유량" - }, - { - "language": "en", - "name": "maximum flow", - "short": "flow" - }, - { - "language": "ja", - "name": "最大フロー", - "short": "flow" - } - ], - "aliases": [ - { - "alias": "dinic" - }, - { - "alias": "dinitz" - }, - { - "alias": "ford" - }, - { - "alias": "fulkerson" - }, - { - "alias": "fordfulkerson" - }, - { - "alias": "디닉" - }, - { - "alias": "디니츠" - }, - { - "alias": "포드풀커슨" - }, - { - "alias": "플로우" - } - ] - }, - { - "key": "primality_test", - "isMeta": false, - "bojTagId": 9, - "problemCount": 313, - "displayNames": [ - { - "language": "ko", - "name": "소수 판정", - "short": "소수 판정" - }, - { - "language": "en", - "name": "primality test", - "short": "primality test" - }, - { - "language": "ja", - "name": "素数性テスト", - "short": "素数性テスト" - } - ], - "aliases": [ - { - "alias": "소수" - }, - { - "alias": "소수판별" - }, - { - "alias": "소수판정" - }, - { - "alias": "prime" - } - ] - }, - { - "key": "probability", - "isMeta": false, - "bojTagId": 177, - "problemCount": 299, - "displayNames": [ - { - "language": "ko", - "name": "확률론", - "short": "확률론" - }, - { - "language": "en", - "name": "probability theory", - "short": "probability" - }, - { - "language": "ja", - "name": "確率論", - "short": "確率論" - } - ], - "aliases": [ - { - "alias": "expected value" - }, - { - "alias": "기대값" - }, - { - "alias": "기댓값" - } - ] - }, - { - "key": "lazyprop", - "isMeta": false, - "bojTagId": 66, - "problemCount": 296, - "displayNames": [ - { - "language": "ko", - "name": "느리게 갱신되는 세그먼트 트리", - "short": "느리게 갱신되는 세그먼트 트리" - }, - { - "language": "en", - "name": "segment tree with lazy propagation", - "short": "lazyprop" - }, - { - "language": "ja", - "name": "遅延評価セグメント木", - "short": "遅延評価セグ木" - } - ], - "aliases": [ - { - "alias": "레이지" - }, - { - "alias": "레이지프로퍼게이션" - }, - { - "alias": "레이지프로파게이션" - }, - { - "alias": "구간트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "dp_bitfield", - "isMeta": false, - "bojTagId": 87, - "problemCount": 295, - "displayNames": [ - { - "language": "ko", - "name": "비트필드를 이용한 다이나믹 프로그래밍", - "short": "비트필드를 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using bitfield", - "short": "bitfield dp" - }, - { - "language": "ja", - "name": "ビットを使用した動的計画法", - "short": "ビットdp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "비트마스크" - }, - { - "alias": "비트dp" - } - ] - }, - { - "key": "exponentiation_by_squaring", - "isMeta": false, - "bojTagId": 39, - "problemCount": 273, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복을 이용한 거듭제곱", - "short": "분할 정복을 이용한 거듭제곱" - }, - { - "language": "en", - "name": "exponentiation by squaring", - "short": "exponentiation by squaring" - }, - { - "language": "ja", - "name": "二乗法によるべき乗", - "short": "二乗法によるべき乗" - } - ], - "aliases": [ - { - "alias": "거듭제곱" - }, - { - "alias": "제곱" - }, - { - "alias": "power" - }, - { - "alias": "square" - } - ] - }, - { - "key": "arbitrary_precision", - "isMeta": false, - "bojTagId": 117, - "problemCount": 254, - "displayNames": [ - { - "language": "ko", - "name": "임의 정밀도 / 큰 수 연산", - "short": "임의 정밀도 / 큰 수 연산" - }, - { - "language": "en", - "name": "arbitrary precision / big integers", - "short": "arbitrary precision / big integers" - }, - { - "language": "ja", - "name": "高精度または大きな数の演算", - "short": "高精度または大きな数の演算" - } - ], - "aliases": [ - { - "alias": "빅인티저" - }, - { - "alias": "빅데시멀" - }, - { - "alias": "biginteger" - }, - { - "alias": "bigdecimal" - } - ] - }, - { - "key": "knapsack", - "isMeta": false, - "bojTagId": 148, - "problemCount": 247, - "displayNames": [ - { - "language": "ko", - "name": "배낭 문제", - "short": "배낭 문제" - }, - { - "language": "en", - "name": "knapsack", - "short": "knapsack" - }, - { - "language": "ja", - "name": "ナップサック問題", - "short": "ナップサック" - } - ], - "aliases": [ - { - "alias": "냅색" - } - ] - }, - { - "key": "offline_queries", - "isMeta": false, - "bojTagId": 123, - "problemCount": 246, - "displayNames": [ - { - "language": "ko", - "name": "오프라인 쿼리", - "short": "오프라인 쿼리" - }, - { - "language": "en", - "name": "offline queries", - "short": "offline query" - }, - { - "language": "ja", - "name": "offline queries", - "short": "offline query" - } - ], - "aliases": [ - { - "alias": "offlinequery" - } - ] - }, - { - "key": "recursion", - "isMeta": false, - "bojTagId": 62, - "problemCount": 223, - "displayNames": [ - { - "language": "ko", - "name": "재귀", - "short": "재귀" - }, - { - "language": "en", - "name": "recursion", - "short": "recursion" - }, - { - "language": "ja", - "name": "再帰", - "short": "再帰" - } - ], - "aliases": [] - }, - { - "key": "coordinate_compression", - "isMeta": false, - "bojTagId": 161, - "problemCount": 223, - "displayNames": [ - { - "language": "ko", - "name": "값 / 좌표 압축", - "short": "값 / 좌표 압축" - }, - { - "language": "en", - "name": "value / coordinate compression", - "short": "compression" - }, - { - "language": "ja", - "name": "value / coordinate compression", - "short": "compression" - } - ], - "aliases": [ - { - "alias": "zip" - } - ] - }, - { - "key": "precomputation", - "isMeta": false, - "bojTagId": 172, - "problemCount": 203, - "displayNames": [ - { - "language": "ko", - "name": "런타임 전의 전처리", - "short": "런타임 전의 전처리" - }, - { - "language": "en", - "name": "precomputation", - "short": "precomputation" - }, - { - "language": "ja", - "name": "事前計算", - "short": "事前計算" - } - ], - "aliases": [ - { - "alias": "lookup table" - }, - { - "alias": "db" - }, - { - "alias": "database" - } - ] - }, - { - "key": "mst", - "isMeta": false, - "bojTagId": 49, - "problemCount": 199, - "displayNames": [ - { - "language": "ko", - "name": "최소 스패닝 트리", - "short": "최소 스패닝 트리" - }, - { - "language": "en", - "name": "minimum spanning tree", - "short": "mst" - }, - { - "language": "ja", - "name": "最小全域木", - "short": "最小全域木" - } - ], - "aliases": [] - }, - { - "key": "sieve", - "isMeta": false, - "bojTagId": 67, - "problemCount": 196, - "displayNames": [ - { - "language": "ko", - "name": "에라토스테네스의 체", - "short": "에라토스테네스의 체" - }, - { - "language": "en", - "name": "sieve of eratosthenes", - "short": "eratosthenes" - }, - { - "language": "ja", - "name": "エラトステネスの篩", - "short": "エラトステネス" - } - ], - "aliases": [ - { - "alias": "sieve" - }, - { - "alias": "에라체" - }, - { - "alias": "소수" - }, - { - "alias": "prime" - } - ] - }, - { - "key": "euclidean", - "isMeta": false, - "bojTagId": 26, - "problemCount": 186, - "displayNames": [ - { - "language": "ko", - "name": "유클리드 호제법", - "short": "유클리드 호제법" - }, - { - "language": "en", - "name": "euclidean algorithm", - "short": "euclidean algorithm" - }, - { - "language": "ja", - "name": "ユークリッドの互除法", - "short": "ユークリッドの互除法" - } - ], - "aliases": [ - { - "alias": "유클리드알고리즘" - } - ] - }, - { - "key": "bipartite_matching", - "isMeta": false, - "bojTagId": 13, - "problemCount": 184, - "displayNames": [ - { - "language": "ko", - "name": "이분 매칭", - "short": "이분 매칭" - }, - { - "language": "en", - "name": "bipartite matching", - "short": "bipartite matching" - }, - { - "language": "ja", - "name": "2部マッチング", - "short": "2部マッチング" - } - ], - "aliases": [] - }, - { - "key": "dag", - "isMeta": false, - "bojTagId": 213, - "problemCount": 182, - "displayNames": [ - { - "language": "ko", - "name": "방향 비순환 그래프", - "short": "dag" - }, - { - "language": "en", - "name": "directed acyclic graph", - "short": "dag" - }, - { - "language": "ja", - "name": "有向非巡回グラフ", - "short": "有向非巡回グラフ" - } - ], - "aliases": [] - }, - { - "key": "convex_hull", - "isMeta": false, - "bojTagId": 20, - "problemCount": 178, - "displayNames": [ - { - "language": "ko", - "name": "볼록 껍질", - "short": "볼록 껍질" - }, - { - "language": "en", - "name": "convex hull", - "short": "convex hull" - }, - { - "language": "ja", - "name": "凸包", - "short": "凸包" - } - ], - "aliases": [ - { - "alias": "컨벡스헐" - } - ] - }, - { - "key": "linear_algebra", - "isMeta": false, - "bojTagId": 144, - "problemCount": 174, - "displayNames": [ - { - "language": "ko", - "name": "선형대수학", - "short": "선형대수학" - }, - { - "language": "en", - "name": "linear algebra", - "short": "linear algebra" - }, - { - "language": "ja", - "name": "線形代数", - "short": "線代" - } - ], - "aliases": [ - { - "alias": "선형대수" - } - ] - }, - { - "key": "topological_sorting", - "isMeta": false, - "bojTagId": 78, - "problemCount": 170, - "displayNames": [ - { - "language": "ko", - "name": "위상 정렬", - "short": "위상 정렬" - }, - { - "language": "en", - "name": "topological sorting", - "short": "topological sorting" - }, - { - "language": "ja", - "name": "トポロジカルソート", - "short": "トポロジカルソート" - } - ], - "aliases": [] - }, - { - "key": "floyd_warshall", - "isMeta": false, - "bojTagId": 31, - "problemCount": 165, - "displayNames": [ - { - "language": "ko", - "name": "플로이드–워셜", - "short": "플로이드–워셜" - }, - { - "language": "en", - "name": "floyd–warshall", - "short": "floyd–warshall" - }, - { - "language": "ja", - "name": "ワーシャル–フロイド法", - "short": "ワーシャル–フロイド法" - } - ], - "aliases": [ - { - "alias": "플로이드" - }, - { - "alias": "플로이드와셜" - }, - { - "alias": "플로이드와샬" - } - ] - }, - { - "key": "hashing", - "isMeta": false, - "bojTagId": 8, - "problemCount": 164, - "displayNames": [ - { - "language": "ko", - "name": "해싱", - "short": "해싱" - }, - { - "language": "en", - "name": "hashing", - "short": "hash" - }, - { - "language": "ja", - "name": "ハッシュ化", - "short": "ハッシュ" - } - ], - "aliases": [] - }, - { - "key": "lca", - "isMeta": false, - "bojTagId": 41, - "problemCount": 163, - "displayNames": [ - { - "language": "ko", - "name": "최소 공통 조상", - "short": "최소 공통 조상" - }, - { - "language": "en", - "name": "lowest common ancestor", - "short": "lca" - }, - { - "language": "ja", - "name": "最下位共通祖先", - "short": "lca" - } - ], - "aliases": [] - }, - { - "key": "inclusion_and_exclusion", - "isMeta": false, - "bojTagId": 38, - "problemCount": 152, - "displayNames": [ - { - "language": "ko", - "name": "포함 배제의 원리", - "short": "포함 배제의 원리" - }, - { - "language": "en", - "name": "inclusion and exclusion", - "short": "inclusion and exclusion" - }, - { - "language": "ja", - "name": "包除原理", - "short": "包除原理" - } - ], - "aliases": [] - }, - { - "key": "scc", - "isMeta": false, - "bojTagId": 76, - "problemCount": 147, - "displayNames": [ - { - "language": "ko", - "name": "강한 연결 요소", - "short": "강한 연결 요소" - }, - { - "language": "en", - "name": "strongly connected component", - "short": "scc" - }, - { - "language": "ja", - "name": "強連結", - "short": "強連結" - } - ], - "aliases": [] - }, - { - "key": "randomization", - "isMeta": false, - "bojTagId": 115, - "problemCount": 143, - "displayNames": [ - { - "language": "ko", - "name": "무작위화", - "short": "무작위화" - }, - { - "language": "en", - "name": "randomization", - "short": "randomization" - }, - { - "language": "ja", - "name": "ランダム化", - "short": "ランダム化" - } - ], - "aliases": [ - { - "alias": "랜덤" - } - ] - }, - { - "key": "sparse_table", - "isMeta": false, - "bojTagId": 84, - "problemCount": 136, - "displayNames": [ - { - "language": "ko", - "name": "희소 배열", - "short": "희소 배열" - }, - { - "language": "en", - "name": "sparse table", - "short": "sparse table" - }, - { - "language": "ja", - "name": "sparse table", - "short": "sparse table" - } - ], - "aliases": [ - { - "alias": "스파스어레이" - }, - { - "alias": "sparse table" - } - ] - }, - { - "key": "smaller_to_larger", - "isMeta": false, - "bojTagId": 169, - "problemCount": 128, - "displayNames": [ - { - "language": "ko", - "name": "작은 집합에서 큰 집합으로 합치는 테크닉", - "short": "작은 집합에서 큰 집합으로 합치는 테크닉" - }, - { - "language": "en", - "name": "smaller to larger technique", - "short": "smaller to larger" - }, - { - "language": "ja", - "name": "smaller to larger technique", - "short": "smaller to larger" - } - ], - "aliases": [ - { - "alias": "merge heuristics" - }, - { - "alias": "sack" - }, - { - "alias": "small to large" - }, - { - "alias": "작은거" - }, - { - "alias": "큰거" - } - ] - }, - { - "key": "fft", - "isMeta": false, - "bojTagId": 28, - "problemCount": 126, - "displayNames": [ - { - "language": "ko", - "name": "고속 푸리에 변환", - "short": "고속 푸리에 변환" - }, - { - "language": "en", - "name": "fast fourier transform", - "short": "fft" - }, - { - "language": "ja", - "name": "高速フーリエ変換", - "short": "fft" - } - ], - "aliases": [ - { - "alias": "푸리에변환" - }, - { - "alias": "컨볼루션" - }, - { - "alias": "convolution" - } - ] - }, - { - "key": "trie", - "isMeta": false, - "bojTagId": 79, - "problemCount": 124, - "displayNames": [ - { - "language": "ko", - "name": "트라이", - "short": "트라이" - }, - { - "language": "en", - "name": "trie", - "short": "trie" - }, - { - "language": "ja", - "name": "トライ木", - "short": "トライ" - } - ], - "aliases": [] - }, - { - "key": "deque", - "isMeta": false, - "bojTagId": 73, - "problemCount": 120, - "displayNames": [ - { - "language": "ko", - "name": "덱", - "short": "덱" - }, - { - "language": "en", - "name": "deque", - "short": "deque" - }, - { - "language": "ja", - "name": "両端キュー", - "short": "deque" - } - ], - "aliases": [] - }, - { - "key": "line_intersection", - "isMeta": false, - "bojTagId": 42, - "problemCount": 117, - "displayNames": [ - { - "language": "ko", - "name": "선분 교차 판정", - "short": "선분 교차 판정" - }, - { - "language": "en", - "name": "line segment intersection check", - "short": "line segment intersection check" - }, - { - "language": "ja", - "name": "直線の交点", - "short": "直線の交点" - } - ], - "aliases": [] - }, - { - "key": "mcmf", - "isMeta": false, - "bojTagId": 48, - "problemCount": 116, - "displayNames": [ - { - "language": "ko", - "name": "최소 비용 최대 유량", - "short": "최소 비용 최대 유량" - }, - { - "language": "en", - "name": "minimum cost maximum flow", - "short": "mcmf" - }, - { - "language": "ja", - "name": "最小費用最大流問題", - "short": "mcmf" - } - ], - "aliases": [ - { - "alias": "dinic" - }, - { - "alias": "dinitz" - }, - { - "alias": "ford" - }, - { - "alias": "fulkerson" - }, - { - "alias": "fordfulkerson" - }, - { - "alias": "디닉" - }, - { - "alias": "디니츠" - }, - { - "alias": "포드풀커슨" - } - ] - }, - { - "key": "sqrt_decomposition", - "isMeta": false, - "bojTagId": 130, - "problemCount": 110, - "displayNames": [ - { - "language": "ko", - "name": "제곱근 분할법", - "short": "제곱근 분할법" - }, - { - "language": "en", - "name": "square root decomposition", - "short": "sqrt decomposition" - }, - { - "language": "ja", - "name": "平方分割", - "short": "平方分割" - } - ], - "aliases": [ - { - "alias": "루트분할법" - }, - { - "alias": "평방분할법" - }, - { - "alias": "모" - }, - { - "alias": "mo" - }, - { - "alias": "sqrt" - } - ] - }, - { - "key": "calculus", - "isMeta": false, - "bojTagId": 111, - "problemCount": 107, - "displayNames": [ - { - "language": "ko", - "name": "미적분학", - "short": "미적분학" - }, - { - "language": "en", - "name": "calculus", - "short": "calculus" - }, - { - "language": "ja", - "name": "微積分", - "short": "微積分" - } - ], - "aliases": [ - { - "alias": "미분" - }, - { - "alias": "적분" - } - ] - }, - { - "key": "modular_multiplicative_inverse", - "isMeta": false, - "bojTagId": 164, - "problemCount": 101, - "displayNames": [ - { - "language": "ko", - "name": "모듈로 곱셈 역원", - "short": "모듈로 곱셈 역원" - }, - { - "language": "en", - "name": "modular multiplicative inverse", - "short": "modular multiplicative inverse" - }, - { - "language": "ja", - "name": "モジュラ逆数", - "short": "モジュラ逆数" - } - ], - "aliases": [ - { - "alias": "modinv" - } - ] - }, - { - "key": "cht", - "isMeta": false, - "bojTagId": 89, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "볼록 껍질을 이용한 최적화", - "short": "볼록 껍질을 이용한 최적화" - }, - { - "language": "en", - "name": "convex hull trick", - "short": "cht" - }, - { - "language": "ja", - "name": "convex hull trick", - "short": "cht" - } - ], - "aliases": [ - { - "alias": "컨벡스헐트릭" - }, - { - "alias": "컨벡스헐최적화" - } - ] - }, - { - "key": "heuristics", - "isMeta": false, - "bojTagId": 142, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "휴리스틱", - "short": "휴리스틱" - }, - { - "language": "en", - "name": "heuristics", - "short": "heuristics" - }, - { - "language": "ja", - "name": "ヒューリスティック", - "short": "ヒューリスティック" - } - ], - "aliases": [] - }, - { - "key": "sliding_window", - "isMeta": false, - "bojTagId": 68, - "problemCount": 100, - "displayNames": [ - { - "language": "ko", - "name": "슬라이딩 윈도우", - "short": "슬라이딩 윈도우" - }, - { - "language": "en", - "name": "sliding window", - "short": "sliding window" - }, - { - "language": "ja", - "name": "スライディングウィンドウ", - "short": "スライディングウィンドウ" - } - ], - "aliases": [ - { - "alias": "슬라이딩윈도" - } - ] - }, - { - "key": "geometry_3d", - "isMeta": false, - "bojTagId": 131, - "problemCount": 98, - "displayNames": [ - { - "language": "ko", - "name": "3차원 기하학", - "short": "3차원 기하학" - }, - { - "language": "en", - "name": "geometry; 3d", - "short": "3d" - }, - { - "language": "ja", - "name": "3次元幾何学", - "short": "3d" - } - ], - "aliases": [] - }, - { - "key": "suffix_array", - "isMeta": false, - "bojTagId": 77, - "problemCount": 97, - "displayNames": [ - { - "language": "ko", - "name": "접미사 배열과 LCP 배열", - "short": "접미사 배열과 LCP 배열" - }, - { - "language": "en", - "name": "suffix array and lcp array", - "short": "suffix array and lcp array" - }, - { - "language": "ja", - "name": "接尾辞配列・LCP配列", - "short": "接尾辞配列・LCP配列" - } - ], - "aliases": [] - }, - { - "key": "centroid", - "isMeta": false, - "bojTagId": 188, - "problemCount": 95, - "displayNames": [ - { - "language": "en", - "name": "centroid", - "short": "centroid" - }, - { - "language": "ko", - "name": "센트로이드", - "short": "센트로이드" - }, - { - "language": "ja", - "name": "centroid", - "short": "centroid" - } - ], - "aliases": [] - }, - { - "key": "euler_tour_technique", - "isMeta": false, - "bojTagId": 150, - "problemCount": 95, - "displayNames": [ - { - "language": "ko", - "name": "오일러 경로 테크닉", - "short": "오일러 경로 테크닉" - }, - { - "language": "en", - "name": "euler tour technique", - "short": "ett" - }, - { - "language": "ja", - "name": "オイラーツアー", - "short": "ett" - } - ], - "aliases": [] - }, - { - "key": "sprague_grundy", - "isMeta": false, - "bojTagId": 70, - "problemCount": 94, - "displayNames": [ - { - "language": "ko", - "name": "스프라그–그런디 정리", - "short": "스프라그–그런디 정리" - }, - { - "language": "en", - "name": "sprague–grundy theorem", - "short": "sprague–grundy thm" - }, - { - "language": "ja", - "name": "sprague–grundy theorem", - "short": "sprague–grundy thm" - } - ], - "aliases": [ - { - "alias": "님버" - }, - { - "alias": "nimber" - } - ] - }, - { - "key": "ternary_search", - "isMeta": false, - "bojTagId": 101, - "problemCount": 92, - "displayNames": [ - { - "language": "ko", - "name": "삼분 탐색", - "short": "삼분 탐색" - }, - { - "language": "en", - "name": "ternary search", - "short": "ternary search" - }, - { - "language": "ja", - "name": "三分探索", - "short": "三分探索" - } - ], - "aliases": [] - }, - { - "key": "mitm", - "isMeta": false, - "bojTagId": 46, - "problemCount": 89, - "displayNames": [ - { - "language": "ko", - "name": "중간에서 만나기", - "short": "중간에서 만나기" - }, - { - "language": "en", - "name": "meet in the middle", - "short": "meet in the middle" - }, - { - "language": "ja", - "name": "半分全列挙", - "short": "半分全列挙" - } - ], - "aliases": [] - }, - { - "key": "bitset", - "isMeta": false, - "bojTagId": 152, - "problemCount": 89, - "displayNames": [ - { - "language": "ko", - "name": "비트 집합", - "short": "비트 집합" - }, - { - "language": "en", - "name": "bit set", - "short": "bit set" - }, - { - "language": "ja", - "name": "bit set", - "short": "bit set" - } - ], - "aliases": [ - { - "alias": "bitset" - }, - { - "alias": "비트셋" - } - ] - }, - { - "key": "pythagoras", - "isMeta": false, - "bojTagId": 60, - "problemCount": 88, - "displayNames": [ - { - "language": "ko", - "name": "피타고라스 정리", - "short": "피타고라스 정리" - }, - { - "language": "en", - "name": "pythagoras theorem", - "short": "pythagoras thm" - }, - { - "language": "ja", - "name": "ピタゴラスの定理", - "short": "ピタゴラス" - } - ], - "aliases": [] - }, - { - "key": "permutation_cycle_decomposition", - "isMeta": false, - "bojTagId": 171, - "problemCount": 87, - "displayNames": [ - { - "language": "ko", - "name": "순열 사이클 분할", - "short": "순열 사이클 분할" - }, - { - "language": "en", - "name": "permutation cycle decomposition", - "short": "permutation cycle decomposition" - }, - { - "language": "ja", - "name": "順列サイクル分解", - "short": "順列サイクル分解" - } - ], - "aliases": [] - }, - { - "key": "lis", - "isMeta": false, - "bojTagId": 43, - "problemCount": 85, - "displayNames": [ - { - "language": "ko", - "name": "가장 긴 증가하는 부분 수열: O(n log n)", - "short": "가장 긴 증가하는 부분 수열: O(n log n)" - }, - { - "language": "en", - "name": "longest increasing sequence in o(n log n)", - "short": "lis in o(n log n)" - }, - { - "language": "ja", - "name": "longest increasing sequence in o(n log n)", - "short": "lis in o(n log n)" - } - ], - "aliases": [] - }, - { - "key": "kmp", - "isMeta": false, - "bojTagId": 40, - "problemCount": 84, - "displayNames": [ - { - "language": "ko", - "name": "KMP", - "short": "KMP" - }, - { - "language": "en", - "name": "knuth–morris–pratt", - "short": "kmp" - }, - { - "language": "ja", - "name": "クヌース–モリス–プラット法", - "short": "kmp" - } - ], - "aliases": [] - }, - { - "key": "gaussian_elimination", - "isMeta": false, - "bojTagId": 32, - "problemCount": 81, - "displayNames": [ - { - "language": "ko", - "name": "가우스 소거법", - "short": "가우스 소거법" - }, - { - "language": "en", - "name": "gaussian elimination", - "short": "gaussian elimination" - }, - { - "language": "ja", - "name": "ガウス消去法", - "short": "ガウス消去法" - } - ], - "aliases": [] - }, - { - "key": "hld", - "isMeta": false, - "bojTagId": 35, - "problemCount": 80, - "displayNames": [ - { - "language": "ko", - "name": "Heavy-light 분할", - "short": "Heavy-light 분할" - }, - { - "language": "en", - "name": "heavy-light decomposition", - "short": "hld" - }, - { - "language": "ja", - "name": "heavy-light decomposition", - "short": "hld" - } - ], - "aliases": [] - }, - { - "key": "centroid_decomposition", - "isMeta": false, - "bojTagId": 18, - "problemCount": 76, - "displayNames": [ - { - "language": "ko", - "name": "센트로이드 분할", - "short": "센트로이드 분할" - }, - { - "language": "en", - "name": "centroid decomposition", - "short": "centroid decomposition" - }, - { - "language": "ja", - "name": "centroid decomposition", - "short": "centroid decomposition" - } - ], - "aliases": [ - { - "alias": "센트로이드" - } - ] - }, - { - "key": "mfmc", - "isMeta": false, - "bojTagId": 167, - "problemCount": 71, - "displayNames": [ - { - "language": "ko", - "name": "최대 유량 최소 컷 정리", - "short": "최대 유량 최소 컷 정리" - }, - { - "language": "en", - "name": "max-flow min-cut theorem", - "short": "mfmc" - }, - { - "language": "ja", - "name": "最大フロー最小カット定理", - "short": "mfmc" - } - ], - "aliases": [] - }, - { - "key": "polygon_area", - "isMeta": false, - "bojTagId": 3, - "problemCount": 71, - "displayNames": [ - { - "language": "ko", - "name": "다각형의 넓이", - "short": "다각형의 넓이" - }, - { - "language": "en", - "name": "area of a polygon", - "short": "area of a polygon" - }, - { - "language": "ja", - "name": "多角形の面積", - "short": "多角形の面積" - } - ], - "aliases": [ - { - "alias": "넓이" - } - ] - }, - { - "key": "queue", - "isMeta": false, - "bojTagId": 72, - "problemCount": 64, - "displayNames": [ - { - "language": "ko", - "name": "큐", - "short": "큐" - }, - { - "language": "en", - "name": "queue", - "short": "queue" - }, - { - "language": "ja", - "name": "キュー", - "short": "キュー" - } - ], - "aliases": [] - }, - { - "key": "physics", - "isMeta": false, - "bojTagId": 116, - "problemCount": 62, - "displayNames": [ - { - "language": "ko", - "name": "물리학", - "short": "물리학" - }, - { - "language": "en", - "name": "physics", - "short": "physics" - }, - { - "language": "ja", - "name": "物理", - "short": "物理" - } - ], - "aliases": [] - }, - { - "key": "flt", - "isMeta": false, - "bojTagId": 29, - "problemCount": 60, - "displayNames": [ - { - "language": "ko", - "name": "페르마의 소정리", - "short": "페르마의 소정리" - }, - { - "language": "en", - "name": "fermat's little theorem", - "short": "fermat's little thm" - }, - { - "language": "ja", - "name": "フェルマーの小定理", - "short": "フェルマー" - } - ], - "aliases": [] - }, - { - "key": "tsp", - "isMeta": false, - "bojTagId": 138, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "외판원 순회 문제", - "short": "외판원 순회 문제" - }, - { - "language": "en", - "name": "travelling salesman problem", - "short": "tsp" - }, - { - "language": "ja", - "name": "巡回セールスマン問題", - "short": "巡回セールスマン" - } - ], - "aliases": [ - { - "alias": "외판원순회" - } - ] - }, - { - "key": "eulerian_path", - "isMeta": false, - "bojTagId": 93, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "오일러 경로", - "short": "오일러 경로" - }, - { - "language": "en", - "name": "eulerian path / circuit", - "short": "eulerian path" - }, - { - "language": "ja", - "name": "eulerian path / circuit", - "short": "eulerian path" - } - ], - "aliases": [ - { - "alias": "eulerian circuit" - }, - { - "alias": "euler tour" - } - ] - }, - { - "key": "linearity_of_expectation", - "isMeta": false, - "bojTagId": 179, - "problemCount": 59, - "displayNames": [ - { - "language": "ko", - "name": "기댓값의 선형성", - "short": "기댓값의 선형성" - }, - { - "language": "en", - "name": "linearity of expectation", - "short": "linearity of expectation" - }, - { - "language": "ja", - "name": "期待値の線形性", - "short": "期待値の線形性" - } - ], - "aliases": [] - }, - { - "key": "2_sat", - "isMeta": false, - "bojTagId": 1, - "problemCount": 58, - "displayNames": [ - { - "language": "ko", - "name": "2-sat", - "short": "2-sat" - }, - { - "language": "en", - "name": "2-sat", - "short": "2-sat" - }, - { - "language": "ja", - "name": "2-sat", - "short": "2-sat" - } - ], - "aliases": [ - { - "alias": "투셋" - }, - { - "alias": "twosat" - }, - { - "alias": "2sat" - } - ] - }, - { - "key": "articulation", - "isMeta": false, - "bojTagId": 4, - "problemCount": 57, - "displayNames": [ - { - "language": "ko", - "name": "단절점과 단절선", - "short": "단절점과 단절선" - }, - { - "language": "en", - "name": "articulation points and bridges", - "short": "articulation points and bridges" - }, - { - "language": "ja", - "name": "関節点と橋", - "short": "関節点と橋" - } - ], - "aliases": [ - { - "alias": "단절점" - }, - { - "alias": "단절선" - }, - { - "alias": "브리지" - }, - { - "alias": "브릿지" - }, - { - "alias": "bridge" - } - ] - }, - { - "key": "0_1_bfs", - "isMeta": false, - "bojTagId": 176, - "problemCount": 56, - "displayNames": [ - { - "language": "ko", - "name": "0-1 너비 우선 탐색", - "short": "0-1 너비 우선 탐색" - }, - { - "language": "en", - "name": "0-1 bfs", - "short": "0-1 bfs" - }, - { - "language": "ja", - "name": "0-1 bfs", - "short": "0-1 bfs" - } - ], - "aliases": [] - }, - { - "key": "bipartite_graph", - "isMeta": false, - "bojTagId": 197, - "problemCount": 54, - "displayNames": [ - { - "language": "ko", - "name": "이분 그래프", - "short": "이분 그래프" - }, - { - "language": "en", - "name": "bipartite graph", - "short": "bipartite graph" - }, - { - "language": "ja", - "name": "2部グラフ", - "short": "2部グラフ" - } - ], - "aliases": [] - }, - { - "key": "biconnected_component", - "isMeta": false, - "bojTagId": 153, - "problemCount": 48, - "displayNames": [ - { - "language": "ko", - "name": "이중 연결 요소", - "short": "이중 연결 요소" - }, - { - "language": "en", - "name": "biconnected component", - "short": "biconnected component" - }, - { - "language": "ja", - "name": "二重接続コンポーネント", - "short": "二重接続" - } - ], - "aliases": [ - { - "alias": "bcc" - } - ] - }, - { - "key": "pst", - "isMeta": false, - "bojTagId": 55, - "problemCount": 46, - "displayNames": [ - { - "language": "ko", - "name": "퍼시스턴트 세그먼트 트리", - "short": "퍼시스턴트 세그먼트 트리" - }, - { - "language": "en", - "name": "persistent segment tree", - "short": "pst" - }, - { - "language": "ja", - "name": "永続セグメント木", - "short": "永続セグ木" - } - ], - "aliases": [ - { - "alias": "퍼시스턴트구간트리" - }, - { - "alias": "구간트리" - }, - { - "alias": "퍼시스턴트세그트리" - } - ] - }, - { - "key": "crt", - "isMeta": false, - "bojTagId": 19, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "중국인의 나머지 정리", - "short": "중국인의 나머지 정리" - }, - { - "language": "en", - "name": "chinese remainder theorem", - "short": "crt" - }, - { - "language": "ja", - "name": "中国の剰余定理", - "short": "中国の剰余定理" - } - ], - "aliases": [] - }, - { - "key": "linked_list", - "isMeta": false, - "bojTagId": 154, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "연결 리스트", - "short": "연결 리스트" - }, - { - "language": "en", - "name": "linked list", - "short": "ll" - }, - { - "language": "ja", - "name": "連結リスト", - "short": "連結リスト" - } - ], - "aliases": [ - { - "alias": "링크드리스트" - } - ] - }, - { - "key": "pigeonhole_principle", - "isMeta": false, - "bojTagId": 189, - "problemCount": 43, - "displayNames": [ - { - "language": "ko", - "name": "비둘기집 원리", - "short": "비둘기집" - }, - { - "language": "en", - "name": "pigeonhole principle", - "short": "pigeonhole" - }, - { - "language": "ja", - "name": "鳩の巣原理", - "short": "鳩" - } - ], - "aliases": [] - }, - { - "key": "cactus", - "isMeta": false, - "bojTagId": 143, - "problemCount": 42, - "displayNames": [ - { - "language": "ko", - "name": "선인장", - "short": "선인장" - }, - { - "language": "en", - "name": "cactus", - "short": "cactus" - }, - { - "language": "ja", - "name": "サボテングラフ", - "short": "サボテングラフ" - } - ], - "aliases": [] - }, - { - "key": "bellman_ford", - "isMeta": false, - "bojTagId": 10, - "problemCount": 41, - "displayNames": [ - { - "language": "ko", - "name": "벨만–포드", - "short": "벨만–포드" - }, - { - "language": "en", - "name": "bellman–ford", - "short": "bellman-ford" - }, - { - "language": "ja", - "name": "ベルマンフォード法", - "short": "ベルマンフォード" - } - ], - "aliases": [ - { - "alias": "bellmanford" - }, - { - "alias": "벨만포드" - }, - { - "alias": "spfa" - } - ] - }, - { - "key": "planar_graph", - "isMeta": false, - "bojTagId": 168, - "problemCount": 41, - "displayNames": [ - { - "language": "ko", - "name": "평면 그래프", - "short": "평면 그래프" - }, - { - "language": "en", - "name": "planar graph", - "short": "planar graph" - }, - { - "language": "ja", - "name": "平面グラフ", - "short": "平面グラフ" - } - ], - "aliases": [] - }, - { - "key": "point_in_convex_polygon", - "isMeta": false, - "bojTagId": 56, - "problemCount": 40, - "displayNames": [ - { - "language": "ko", - "name": "볼록 다각형 내부의 점 판정", - "short": "볼록 다각형 내부의 점 판정" - }, - { - "language": "en", - "name": "point in convex polygon check", - "short": "point in convex polygon check" - }, - { - "language": "ja", - "name": "凸多角形の点包含判定", - "short": "凸多角形の点包含判定" - } - ], - "aliases": [] - }, - { - "key": "euler_phi", - "isMeta": false, - "bojTagId": 151, - "problemCount": 38, - "displayNames": [ - { - "language": "ko", - "name": "오일러 피 함수", - "short": "오일러 피 함수" - }, - { - "language": "en", - "name": "euler totient function", - "short": "euler phi function" - }, - { - "language": "ja", - "name": "euler totient function", - "short": "euler phi function" - } - ], - "aliases": [ - { - "alias": "오일러 파이" - }, - { - "alias": "토션트" - }, - { - "alias": "eulerphi" - }, - { - "alias": "euler phi" - } - ] - }, - { - "key": "splay_tree", - "isMeta": false, - "bojTagId": 69, - "problemCount": 37, - "displayNames": [ - { - "language": "ko", - "name": "스플레이 트리", - "short": "스플레이 트리" - }, - { - "language": "en", - "name": "splay tree", - "short": "splay tree" - }, - { - "language": "ja", - "name": "splay tree", - "short": "splay tree" - } - ], - "aliases": [] - }, - { - "key": "pbs", - "isMeta": false, - "bojTagId": 54, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "병렬 이분 탐색", - "short": "병렬 이분 탐색" - }, - { - "language": "en", - "name": "parallel binary search", - "short": "pbs" - }, - { - "language": "ja", - "name": "parallel binary search", - "short": "pbs" - } - ], - "aliases": [] - }, - { - "key": "extended_euclidean", - "isMeta": false, - "bojTagId": 27, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "확장 유클리드 호제법", - "short": "확장 유클리드 호제법" - }, - { - "language": "en", - "name": "extended euclidean algorithm", - "short": "extended euclidean algorithm" - }, - { - "language": "ja", - "name": "拡張ユークリッドの互除法", - "short": "拡張ユークリッド" - } - ], - "aliases": [ - { - "alias": "확장유클리드알고리즘" - }, - { - "alias": "egcd" - } - ] - }, - { - "key": "divide_and_conquer_optimization", - "isMeta": false, - "bojTagId": 91, - "problemCount": 35, - "displayNames": [ - { - "language": "ko", - "name": "분할 정복을 사용한 최적화", - "short": "분할 정복을 사용한 최적화" - }, - { - "language": "en", - "name": "divide and conquer optimization", - "short": "d&c optimization" - }, - { - "language": "ja", - "name": "divide and conquer optimization", - "short": "d&c optimization" - } - ], - "aliases": [ - { - "alias": "분할 정복 최적화" - }, - { - "alias": "dnc opt" - } - ] - }, - { - "key": "deque_trick", - "isMeta": false, - "bojTagId": 216, - "problemCount": 34, - "displayNames": [ - { - "language": "ko", - "name": "덱을 이용한 구간 최댓값 트릭", - "short": "덱 트릭" - }, - { - "language": "en", - "name": "deque range maximum trick", - "short": "deque rmq trick" - }, - { - "language": "ja", - "name": "deque range maximum trick", - "short": "deque rmq trick" - } - ], - "aliases": [] - }, - { - "key": "mo", - "isMeta": false, - "bojTagId": 50, - "problemCount": 33, - "displayNames": [ - { - "language": "ko", - "name": "mo's", - "short": "Mo's" - }, - { - "language": "en", - "name": "mo's", - "short": "mo's" - }, - { - "language": "ja", - "name": "mo's", - "short": "mo's" - } - ], - "aliases": [ - { - "alias": "squarerootdecomposition" - }, - { - "alias": "sqrtdecomposition" - }, - { - "alias": "평방분할법" - } - ] - }, - { - "key": "half_plane_intersection", - "isMeta": false, - "bojTagId": 190, - "problemCount": 30, - "displayNames": [ - { - "language": "ko", - "name": "반평면 교집합", - "short": "반평면 교집합" - }, - { - "language": "en", - "name": "half plane intersection", - "short": "hpi" - }, - { - "language": "ja", - "name": "half plane intersection", - "short": "hpi" - } - ], - "aliases": [] - }, - { - "key": "dp_deque", - "isMeta": false, - "bojTagId": 108, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "덱을 이용한 다이나믹 프로그래밍", - "short": "덱을 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using a deque", - "short": "deque dp" - }, - { - "language": "ja", - "name": "両端キューを使用した動的計画法", - "short": "deque dp" - } - ], - "aliases": [ - { - "alias": "동적계획법" - }, - { - "alias": "덱dp" - } - ] - }, - { - "key": "aho_corasick", - "isMeta": false, - "bojTagId": 2, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "아호-코라식", - "short": "아호-코라식" - }, - { - "language": "en", - "name": "aho-corasick", - "short": "aho-corasick" - }, - { - "language": "ja", - "name": "アホコラシック", - "short": "アホコラシック" - } - ], - "aliases": [ - { - "alias": "아호코라식" - }, - { - "alias": "ahocorasick" - } - ] - }, - { - "key": "multi_segtree", - "isMeta": false, - "bojTagId": 166, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "다차원 세그먼트 트리", - "short": "다차원 세그먼트 트리" - }, - { - "language": "en", - "name": "multidimensional segment tree", - "short": "multidimensional segtree" - }, - { - "language": "ja", - "name": "multidimensional segment tree", - "short": "multidimensional segtree" - } - ], - "aliases": [ - { - "alias": "구간트리" - }, - { - "alias": "세그트리" - }, - { - "alias": "fenwick" - }, - { - "alias": "펜윅" - } - ] - }, - { - "key": "rotating_calipers", - "isMeta": false, - "bojTagId": 64, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "회전하는 캘리퍼스", - "short": "회전하는 캘리퍼스" - }, - { - "language": "en", - "name": "rotating calipers", - "short": "rotating calipers" - }, - { - "language": "ja", - "name": "rotating calipers", - "short": "rotating calipers" - } - ], - "aliases": [] - }, - { - "key": "euler_characteristic", - "isMeta": false, - "bojTagId": 119, - "problemCount": 29, - "displayNames": [ - { - "language": "ko", - "name": "오일러 지표 (χ=V-E+F)", - "short": "오일러 지표" - }, - { - "language": "en", - "name": "euler characteristic (χ=v-e+f)", - "short": "euler characteristic" - }, - { - "language": "ja", - "name": "オイラー特性(χ=v-e+f)", - "short": "オイラー特性" - } - ], - "aliases": [] - }, - { - "key": "regex", - "isMeta": false, - "bojTagId": 63, - "problemCount": 27, - "displayNames": [ - { - "language": "ko", - "name": "정규 표현식", - "short": "정규 표현식" - }, - { - "language": "en", - "name": "regular expression", - "short": "regex" - }, - { - "language": "ja", - "name": "正規表現", - "short": "regex" - } - ], - "aliases": [ - { - "alias": "정규식" - } - ] - }, - { - "key": "slope_trick", - "isMeta": false, - "bojTagId": 157, - "problemCount": 26, - "displayNames": [ - { - "language": "ko", - "name": "함수 개형을 이용한 최적화", - "short": "함수 개형을 이용한 최적화" - }, - { - "language": "en", - "name": "slope trick", - "short": "slope trick" - }, - { - "language": "ja", - "name": "slope trick", - "short": "slope trick" - } - ], - "aliases": [ - { - "alias": "슬로프트릭" - }, - { - "alias": "슬로프 트릭" - } - ] - }, - { - "key": "berlekamp_massey", - "isMeta": false, - "bojTagId": 110, - "problemCount": 25, - "displayNames": [ - { - "language": "ko", - "name": "벌리캠프–매시", - "short": "벌리캠프–매시" - }, - { - "language": "en", - "name": "berlekamp–massey", - "short": "berlekamp–massey" - }, - { - "language": "ja", - "name": "berlekamp–massey", - "short": "berlekamp–massey" - } - ], - "aliases": [ - { - "alias": "벌레캠프" - }, - { - "alias": "벌래캠프" - } - ] - }, - { - "key": "manacher", - "isMeta": false, - "bojTagId": 44, - "problemCount": 24, - "displayNames": [ - { - "language": "ko", - "name": "매내처", - "short": "매내처" - }, - { - "language": "en", - "name": "manacher's", - "short": "manacher's" - }, - { - "language": "ja", - "name": "manacher's", - "short": "manacher's" - } - ], - "aliases": [] - }, - { - "key": "pollard_rho", - "isMeta": false, - "bojTagId": 58, - "problemCount": 23, - "displayNames": [ - { - "language": "ko", - "name": "폴라드 로", - "short": "폴라드 로" - }, - { - "language": "en", - "name": "pollard rho", - "short": "pollard rho" - }, - { - "language": "ja", - "name": "ポラード・ロー素因数分解法", - "short": "ポラード・ロー" - } - ], - "aliases": [] - }, - { - "key": "dp_connection_profile", - "isMeta": false, - "bojTagId": 107, - "problemCount": 23, - "displayNames": [ - { - "language": "ko", - "name": "커넥션 프로파일을 이용한 다이나믹 프로그래밍", - "short": "커넥션 프로파일을 이용한 다이나믹 프로그래밍" - }, - { - "language": "en", - "name": "dynamic programming using connection profile", - "short": "dp using connection profile" - }, - { - "language": "ja", - "name": "dynamic programming using connection profile", - "short": "dp using connection profile" - } - ], - "aliases": [ - { - "alias": "동적계획법" - } - ] - }, - { - "key": "link_cut_tree", - "isMeta": false, - "bojTagId": 98, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "링크/컷 트리", - "short": "링크/컷 트리" - }, - { - "language": "en", - "name": "link/cut tree", - "short": "link/cut tree" - }, - { - "language": "ja", - "name": "link/cut tree", - "short": "link/cut tree" - } - ], - "aliases": [ - { - "alias": "link cut tree" - }, - { - "alias": "linkcuttree" - }, - { - "alias": "링크컷" - } - ] - }, - { - "key": "merge_sort_tree", - "isMeta": false, - "bojTagId": 155, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "머지 소트 트리", - "short": "머지 소트 트리" - }, - { - "language": "en", - "name": "merge sort tree", - "short": "merge sort tree" - }, - { - "language": "ja", - "name": "マージソート木", - "short": "マージソート木" - } - ], - "aliases": [ - { - "alias": "병합정렬트리" - }, - { - "alias": "합병정렬트리" - } - ] - }, - { - "key": "tree_isomorphism", - "isMeta": false, - "bojTagId": 145, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "트리 동형 사상", - "short": "트리 동형 사상" - }, - { - "language": "en", - "name": "tree isomorphism", - "short": "tree isomorphism" - }, - { - "language": "ja", - "name": "木の同型性判定", - "short": "木の同型性判定" - } - ], - "aliases": [ - { - "alias": "graph isomorphism" - }, - { - "alias": "isomorphism" - }, - { - "alias": "topology" - }, - { - "alias": "아이소모피즘" - }, - { - "alias": "위상" - } - ] - }, - { - "key": "simulated_annealing", - "isMeta": false, - "bojTagId": 184, - "problemCount": 22, - "displayNames": [ - { - "language": "ko", - "name": "담금질 기법", - "short": "담금질 기법" - }, - { - "language": "en", - "name": "simulated annealing", - "short": "simulated annealing" - }, - { - "language": "ja", - "name": "焼き鈍し法", - "short": "焼き鈍し法" - } - ], - "aliases": [] - }, - { - "key": "hall", - "isMeta": false, - "bojTagId": 34, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "홀의 결혼 정리", - "short": "홀의 결혼 정리" - }, - { - "language": "en", - "name": "hall's theorem", - "short": "hall's thm" - }, - { - "language": "ja", - "name": "ホールの定理", - "short": "ホールの定理" - } - ], - "aliases": [] - }, - { - "key": "hungarian", - "isMeta": false, - "bojTagId": 36, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "헝가리안", - "short": "헝가리안" - }, - { - "language": "en", - "name": "hungarian", - "short": "hungarian" - }, - { - "language": "ja", - "name": "hungarian", - "short": "hungarian" - } - ], - "aliases": [ - { - "alias": "헝가리안" - }, - { - "alias": "assignment problem" - }, - { - "alias": "weighted bipartite matching" - } - ] - }, - { - "key": "flood_fill", - "isMeta": false, - "bojTagId": 210, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "플러드 필", - "short": "플러드 필" - }, - { - "language": "en", - "name": "flood-fill", - "short": "ff" - }, - { - "language": "ja", - "name": "flood-fill", - "short": "ff" - } - ], - "aliases": [] - }, - { - "key": "miller_rabin", - "isMeta": false, - "bojTagId": 47, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "밀러–라빈 소수 판별법", - "short": "밀러–라빈 소수 판별법" - }, - { - "language": "en", - "name": "miller–rabin", - "short": "miller–rabin" - }, - { - "language": "ja", - "name": "ミラー–ラビン素数判定法", - "short": "ミラー–ラビン" - } - ], - "aliases": [] - }, - { - "key": "mobius_inversion", - "isMeta": false, - "bojTagId": 51, - "problemCount": 20, - "displayNames": [ - { - "language": "ko", - "name": "뫼비우스 반전 공식", - "short": "뫼비우스 반전 공식" - }, - { - "language": "en", - "name": "möbius inversion", - "short": "möbius inversion" - }, - { - "language": "ja", - "name": "メビウスの反転公式", - "short": "メビウス" - } - ], - "aliases": [ - { - "alias": "mobius" - } - ] - }, - { - "key": "rabin_karp", - "isMeta": false, - "bojTagId": 61, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "라빈–카프", - "short": "라빈–카프" - }, - { - "language": "en", - "name": "rabin–karp", - "short": "rabin–karp" - }, - { - "language": "ja", - "name": "ラビン-カープ文字列検索", - "short": "ラビン-カープ文字列検索" - } - ], - "aliases": [ - { - "alias": "라빈카프" - } - ] - }, - { - "key": "numerical_analysis", - "isMeta": false, - "bojTagId": 122, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "수치해석", - "short": "수치해석" - }, - { - "language": "en", - "name": "numerical analysis", - "short": "numerical analysis" - }, - { - "language": "ja", - "name": "数値解析", - "short": "数値解析" - } - ], - "aliases": [ - { - "alias": "수학" - } - ] - }, - { - "key": "point_in_non_convex_polygon", - "isMeta": false, - "bojTagId": 57, - "problemCount": 19, - "displayNames": [ - { - "language": "ko", - "name": "오목 다각형 내부의 점 판정", - "short": "오목 다각형 내부의 점 판정" - }, - { - "language": "en", - "name": "point in non-convex polygon check", - "short": "point in non-convex polygon check" - }, - { - "language": "ja", - "name": "非凸多角形の点包含判定", - "short": "非凸多角形の点包含判定" - } - ], - "aliases": [] - }, - { - "key": "alien", - "isMeta": false, - "bojTagId": 134, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "Aliens 트릭", - "short": "aliens 트릭" - }, - { - "language": "en", - "name": "aliens trick", - "short": "aliens trick" - }, - { - "language": "ja", - "name": "aliens法", - "short": "aliens法" - } - ], - "aliases": [ - { - "alias": "alien's trick" - } - ] - }, - { - "key": "linear_programming", - "isMeta": false, - "bojTagId": 103, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "선형 계획법", - "short": "선형 계획법" - }, - { - "language": "en", - "name": "linear programming", - "short": "lp" - }, - { - "language": "ja", - "name": "線型計画法", - "short": "lp" - } - ], - "aliases": [ - { - "alias": "리니어프로그래밍" - } - ] - }, - { - "key": "generating_function", - "isMeta": false, - "bojTagId": 198, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "생성 함수", - "short": "생성 함수" - }, - { - "language": "en", - "name": "generating function", - "short": "generating function" - }, - { - "language": "ja", - "name": "生成関数", - "short": "生成関数" - } - ], - "aliases": [] - }, - { - "key": "offline_dynamic_connectivity", - "isMeta": false, - "bojTagId": 52, - "problemCount": 18, - "displayNames": [ - { - "language": "ko", - "name": "오프라인 동적 연결성 판정", - "short": "오프라인 동적 연결성 판정" - }, - { - "language": "en", - "name": "offline dynamic connectivity", - "short": "offline dynamic connectivity" - }, - { - "language": "ja", - "name": "offline dynamic connectivity", - "short": "offline dynamic connectivity" - } - ], - "aliases": [] - }, - { - "key": "statistics", - "isMeta": false, - "bojTagId": 178, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "통계학", - "short": "통계학" - }, - { - "language": "en", - "name": "statistics", - "short": "stats" - }, - { - "language": "ja", - "name": "統計学", - "short": "統計" - } - ], - "aliases": [ - { - "alias": "average" - }, - { - "alias": "평균" - }, - { - "alias": "variance" - }, - { - "alias": "분산" - }, - { - "alias": "표준편차" - }, - { - "alias": "표준 편차" - } - ] - }, - { - "key": "functional_graph", - "isMeta": false, - "bojTagId": 211, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "함수형 그래프", - "short": "함수형 그래프" - }, - { - "language": "en", - "name": "functional graph", - "short": "functional graph" - }, - { - "language": "ja", - "name": "functional graph", - "short": "functional graph" - } - ], - "aliases": [] - }, - { - "key": "dp_sum_over_subsets", - "isMeta": false, - "bojTagId": 207, - "problemCount": 17, - "displayNames": [ - { - "language": "ko", - "name": "부분집합의 합 다이나믹 프로그래밍", - "short": "부분집합의 합" - }, - { - "language": "en", - "name": "sum over subsets dynamic programming", - "short": "sos dp" - }, - { - "language": "ja", - "name": "sum over subsets dynamic programming", - "short": "sos dp" - } - ], - "aliases": [ - { - "alias": "sos" - } - ] - }, - { - "key": "circulation", - "isMeta": false, - "bojTagId": 191, - "problemCount": 16, - "displayNames": [ - { - "language": "ko", - "name": "서큘레이션", - "short": "서큘레이션" - }, - { - "language": "en", - "name": "circulation", - "short": "circulation" - }, - { - "language": "ja", - "name": "circulation", - "short": "circulation" - } - ], - "aliases": [] - }, - { - "key": "tree_compression", - "isMeta": false, - "bojTagId": 193, - "problemCount": 16, - "displayNames": [ - { - "language": "ko", - "name": "트리 압축", - "short": "트리 압축" - }, - { - "language": "en", - "name": "tree compression", - "short": "tree compression" - }, - { - "language": "ja", - "name": "tree compression", - "short": "tree compression" - } - ], - "aliases": [] - }, - { - "key": "voronoi", - "isMeta": false, - "bojTagId": 82, - "problemCount": 15, - "displayNames": [ - { - "language": "ko", - "name": "보로노이 다이어그램", - "short": "보로노이 다이어그램" - }, - { - "language": "en", - "name": "voronoi diagram", - "short": "voronoi diagram" - }, - { - "language": "ja", - "name": "ボロノイ図", - "short": "ボロノイ図" - } - ], - "aliases": [] - }, - { - "key": "duality", - "isMeta": false, - "bojTagId": 180, - "problemCount": 14, - "displayNames": [ - { - "language": "ko", - "name": "쌍대성", - "short": "쌍대성" - }, - { - "language": "en", - "name": "duality", - "short": "duality" - }, - { - "language": "ja", - "name": "双対性", - "short": "双対性" - } - ], - "aliases": [ - { - "alias": "듀얼리티" - } - ] - }, - { - "key": "dual_graph", - "isMeta": false, - "bojTagId": 181, - "problemCount": 14, - "displayNames": [ - { - "language": "ko", - "name": "쌍대 그래프", - "short": "쌍대 그래프" - }, - { - "language": "en", - "name": "dual graph", - "short": "dual graph" - }, - { - "language": "ja", - "name": "双対グラフ", - "short": "双対グラフ" - } - ], - "aliases": [ - { - "alias": "듀얼 그래프" - } - ] - }, - { - "key": "lucas", - "isMeta": false, - "bojTagId": 113, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "뤼카 정리", - "short": "뤼카 정리" - }, - { - "language": "en", - "name": "lucas theorem", - "short": "lucas thm" - }, - { - "language": "ja", - "name": "lucas theorem", - "short": "lucas thm" - } - ], - "aliases": [] - }, - { - "key": "matroid", - "isMeta": false, - "bojTagId": 104, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "매트로이드", - "short": "매트로이드" - }, - { - "language": "en", - "name": "matroid", - "short": "matroid" - }, - { - "language": "ja", - "name": "マトロイド", - "short": "マトロイド" - } - ], - "aliases": [] - }, - { - "key": "dp_digit", - "isMeta": false, - "bojTagId": 217, - "problemCount": 13, - "displayNames": [ - { - "language": "ko", - "name": "자릿수를 이용한 다이나믹 프로그래밍", - "short": "자릿수 dp" - }, - { - "language": "en", - "name": "digit dp", - "short": "digit dp" - } - ], - "aliases": [] - }, - { - "key": "kitamasa", - "isMeta": false, - "bojTagId": 112, - "problemCount": 12, - "displayNames": [ - { - "language": "ko", - "name": "키타마사", - "short": "키타마사" - }, - { - "language": "en", - "name": "kitamasa", - "short": "kitamasa" - }, - { - "language": "ja", - "name": "きたまさ法", - "short": "きたまさ法" - } - ], - "aliases": [] - }, - { - "key": "cartesian_tree", - "isMeta": false, - "bojTagId": 206, - "problemCount": 11, - "displayNames": [ - { - "language": "ko", - "name": "데카르트 트리", - "short": "데카르트 트리" - }, - { - "language": "en", - "name": "cartesian tree", - "short": "cartesian tree" - }, - { - "language": "ja", - "name": "デカルト木", - "short": "デカルト木" - } - ], - "aliases": [] - }, - { - "key": "general_matching", - "isMeta": false, - "bojTagId": 15, - "problemCount": 11, - "displayNames": [ - { - "language": "ko", - "name": "일반적인 매칭", - "short": "일반적인 매칭" - }, - { - "language": "en", - "name": "general matching", - "short": "general matching" - }, - { - "language": "ja", - "name": "一般的なマッチング", - "short": "一般的なマッチング" - } - ], - "aliases": [ - { - "alias": "블라썸" - }, - { - "alias": "블러썸" - }, - { - "alias": "블라섬" - }, - { - "alias": "블러섬" - }, - { - "alias": "blossom" - }, - { - "alias": "부합" - } - ] - }, - { - "key": "tree_decomposition", - "isMeta": false, - "bojTagId": 204, - "problemCount": 10, - "displayNames": [ - { - "language": "ko", - "name": "트리 분할", - "short": "트리 분할" - }, - { - "language": "en", - "name": "tree decomposition", - "short": "tree decomposition" - }, - { - "language": "ja", - "name": "tree decomposition", - "short": "tree decomposition" - } - ], - "aliases": [] - }, - { - "key": "burnside", - "isMeta": false, - "bojTagId": 16, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "번사이드 보조정리", - "short": "번사이드 보조정리" - }, - { - "language": "en", - "name": "burnside's lemma", - "short": "burnside's lemma" - }, - { - "language": "ja", - "name": "バーンサイドの補題", - "short": "バーンサイド" - } - ], - "aliases": [] - }, - { - "key": "discrete_log", - "isMeta": false, - "bojTagId": 146, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "이산 로그", - "short": "이산 로그" - }, - { - "language": "en", - "name": "discrete logarithm", - "short": "discrete logarithm" - }, - { - "language": "ja", - "name": "離散対数", - "short": "離散対数" - } - ], - "aliases": [ - { - "alias": "order" - } - ] - }, - { - "key": "geometry_hyper", - "isMeta": false, - "bojTagId": 132, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "4차원 이상의 기하학", - "short": "4차원 이상의 기하학" - }, - { - "language": "en", - "name": "geometry; hyperdimensional", - "short": "hyperdimensional" - }, - { - "language": "ja", - "name": "4次元以上での幾何学", - "short": "hyperdimensional" - } - ], - "aliases": [ - { - "alias": "4차원" - }, - { - "alias": "5차원" - }, - { - "alias": "6차원" - }, - { - "alias": "7차원" - }, - { - "alias": "8차원" - }, - { - "alias": "9차원" - }, - { - "alias": "4d" - }, - { - "alias": "5d" - }, - { - "alias": "6d" - }, - { - "alias": "7d" - }, - { - "alias": "8d" - }, - { - "alias": "9d" - } - ] - }, - { - "key": "bidirectional_search", - "isMeta": false, - "bojTagId": 129, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "양방향 탐색", - "short": "양방향 탐색" - }, - { - "language": "en", - "name": "bidirectional search", - "short": "bidirectional search" - }, - { - "language": "ja", - "name": "bidirectional search", - "short": "bidirectional search" - } - ], - "aliases": [] - }, - { - "key": "min_enclosing_circle", - "isMeta": false, - "bojTagId": 162, - "problemCount": 9, - "displayNames": [ - { - "language": "ko", - "name": "최소 외접원", - "short": "최소 외접원" - }, - { - "language": "en", - "name": "minimum enclosing circle", - "short": "minimum enclosing circle" - }, - { - "language": "ja", - "name": "最小外接円", - "short": "最小外接円" - } - ], - "aliases": [ - { - "alias": "bounding circle" - }, - { - "alias": "smallest enclosing circle" - }, - { - "alias": "minimum covering circle" - } - ] - }, - { - "key": "z", - "isMeta": false, - "bojTagId": 83, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "z", - "short": "Z" - }, - { - "language": "en", - "name": "z", - "short": "z" - }, - { - "language": "ja", - "name": "z", - "short": "z" - } - ], - "aliases": [] - }, - { - "key": "pick", - "isMeta": false, - "bojTagId": 187, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "픽의 정리", - "short": "픽" - }, - { - "language": "en", - "name": "pick's theorem", - "short": "pick's thm" - }, - { - "language": "ja", - "name": "ピックの定理", - "short": "ピック" - } - ], - "aliases": [] - }, - { - "key": "utf8", - "isMeta": false, - "bojTagId": 199, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "utf-8 입력 처리", - "short": "utf-8" - }, - { - "language": "en", - "name": "utf-8 inputs", - "short": "utf-8" - }, - { - "language": "ja", - "name": "utf-8入力の処理", - "short": "utf-8" - } - ], - "aliases": [] - }, - { - "key": "top_tree", - "isMeta": false, - "bojTagId": 105, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "탑 트리", - "short": "탑 트리" - }, - { - "language": "en", - "name": "top tree", - "short": "top tree" - }, - { - "language": "ja", - "name": "top tree", - "short": "top tree" - } - ], - "aliases": [] - }, - { - "key": "palindrome_tree", - "isMeta": false, - "bojTagId": 53, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "회문 트리", - "short": "회문 트리" - }, - { - "language": "en", - "name": "palindrome tree", - "short": "palindrome tree" - }, - { - "language": "ja", - "name": "palindrome tree", - "short": "palindrome tree" - } - ], - "aliases": [ - { - "alias": "팰린드롬트리" - } - ] - }, - { - "key": "monotone_queue_optimization", - "isMeta": false, - "bojTagId": 165, - "problemCount": 8, - "displayNames": [ - { - "language": "ko", - "name": "단조 큐를 이용한 최적화", - "short": "단조 큐를 이용한 최적화" - }, - { - "language": "en", - "name": "monotone queue optimization", - "short": "monotone queue optimization" - }, - { - "language": "ja", - "name": "monotone queue optimization", - "short": "monotone queue optimization" - } - ], - "aliases": [] - }, - { - "key": "knuth_x", - "isMeta": false, - "bojTagId": 174, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "크누스 X", - "short": "크누스 X" - }, - { - "language": "en", - "name": "knuth's x", - "short": "x" - }, - { - "language": "ja", - "name": "knuth's x", - "short": "x" - } - ], - "aliases": [] - }, - { - "key": "delaunay", - "isMeta": false, - "bojTagId": 21, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "델로네 삼각분할", - "short": "델로네 삼각분할" - }, - { - "language": "en", - "name": "delaunay triangulation", - "short": "delaunay triangulation" - }, - { - "language": "ja", - "name": "ドロネー三角形分割", - "short": "ドロネー" - } - ], - "aliases": [] - }, - { - "key": "dominator_tree", - "isMeta": false, - "bojTagId": 135, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "도미네이터 트리", - "short": "도미네이터 트리" - }, - { - "language": "en", - "name": "dominator tree", - "short": "dominator tree" - }, - { - "language": "ja", - "name": "dominator tree", - "short": "dominator tree" - } - ], - "aliases": [] - }, - { - "key": "stable_marriage", - "isMeta": false, - "bojTagId": 192, - "problemCount": 7, - "displayNames": [ - { - "language": "ko", - "name": "안정 결혼 문제", - "short": "안정 결혼" - }, - { - "language": "en", - "name": "stable marriage problem", - "short": "smp" - }, - { - "language": "ja", - "name": "stable marriage problem", - "short": "smp" - } - ], - "aliases": [] - }, - { - "key": "rope", - "isMeta": false, - "bojTagId": 159, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "로프", - "short": "로프" - }, - { - "language": "en", - "name": "rope", - "short": "rope" - }, - { - "language": "ja", - "name": "rope", - "short": "rope" - } - ], - "aliases": [] - }, - { - "key": "bayes", - "isMeta": false, - "bojTagId": 114, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "베이즈 정리", - "short": "베이즈 정리" - }, - { - "language": "en", - "name": "bayes theorem", - "short": "bayes thm" - }, - { - "language": "ja", - "name": "ベイズの定理", - "short": "ベイズ" - } - ], - "aliases": [ - { - "alias": "조건부확률" - } - ] - }, - { - "key": "knuth", - "isMeta": false, - "bojTagId": 90, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "크누스 최적화", - "short": "크누스 최적화" - }, - { - "language": "en", - "name": "knuth optimization", - "short": "knuth" - }, - { - "language": "ja", - "name": "knuth optimization", - "short": "knuth" - } - ], - "aliases": [] - }, - { - "key": "dancing_links", - "isMeta": false, - "bojTagId": 173, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "춤추는 링크", - "short": "춤추는 링크" - }, - { - "language": "en", - "name": "dancing links", - "short": "dancing links" - }, - { - "language": "ja", - "name": "dancing links", - "short": "dancing links" - } - ], - "aliases": [] - }, - { - "key": "degree_sequence", - "isMeta": false, - "bojTagId": 200, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "차수열", - "short": "차수열" - }, - { - "language": "en", - "name": "degree sequence", - "short": "degree sequence" - }, - { - "language": "ja", - "name": "degree sequence", - "short": "degree sequence" - } - ], - "aliases": [] - }, - { - "key": "differential_cryptanalysis", - "isMeta": false, - "bojTagId": 185, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "차분 공격", - "short": "차분 공격" - }, - { - "language": "en", - "name": "differential cryptanalysis", - "short": "differential cryptanalysis" - }, - { - "language": "ja", - "name": "differential cryptanalysis", - "short": "differential cryptanalysis" - } - ], - "aliases": [ - { - "alias": "dc" - } - ] - }, - { - "key": "geometric_boolean_operations", - "isMeta": false, - "bojTagId": 202, - "problemCount": 6, - "displayNames": [ - { - "language": "ko", - "name": "도형에서의 불 연산", - "short": "도형에서의 불 연산" - }, - { - "language": "en", - "name": "boolean operations on geometric objects", - "short": "geometric boolean operations" - }, - { - "language": "ja", - "name": "図形のブール演算", - "short": "図形のブール演算" - } - ], - "aliases": [ - { - "alias": "병합" - }, - { - "alias": "교집합" - }, - { - "alias": "합집합" - }, - { - "alias": "union" - }, - { - "alias": "intersect" - } - ] - }, - { - "key": "hirschberg", - "isMeta": false, - "bojTagId": 163, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "히르쉬버그", - "short": "히르쉬버그" - }, - { - "language": "en", - "name": "hirschberg's", - "short": "hirschberg's" - }, - { - "language": "ja", - "name": "hirschberg's", - "short": "hirschberg's" - } - ], - "aliases": [ - { - "alias": "hirschburg" - } - ] - }, - { - "key": "suffix_tree", - "isMeta": false, - "bojTagId": 182, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "접미사 트리", - "short": "접미사 트리" - }, - { - "language": "en", - "name": "suffix tree", - "short": "suffix tree" - }, - { - "language": "ja", - "name": "suffix tree", - "short": "suffix tree" - } - ], - "aliases": [] - }, - { - "key": "chordal_graph", - "isMeta": false, - "bojTagId": 201, - "problemCount": 5, - "displayNames": [ - { - "language": "en", - "name": "chordal graph", - "short": "chordal graph" - }, - { - "language": "ko", - "name": "현 그래프", - "short": "현 그래프" - }, - { - "language": "ja", - "name": "弦グラフ", - "short": "弦グラフ" - } - ], - "aliases": [] - }, - { - "key": "discrete_sqrt", - "isMeta": false, - "bojTagId": 147, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "이산 제곱근", - "short": "이산 제곱근" - }, - { - "language": "en", - "name": "discrete square root", - "short": "discrete square root" - }, - { - "language": "ja", - "name": "離散平方根", - "short": "離散平方根" - } - ], - "aliases": [ - { - "alias": "루트" - } - ] - }, - { - "key": "gradient_descent", - "isMeta": false, - "bojTagId": 208, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "경사 하강법", - "short": "경사 하강법" - }, - { - "language": "en", - "name": "gradient descent", - "short": "gradient descent" - }, - { - "language": "ja", - "name": "勾配降下法", - "short": "勾配降下法" - } - ], - "aliases": [] - }, - { - "key": "polynomial_interpolation", - "isMeta": false, - "bojTagId": 209, - "problemCount": 5, - "displayNames": [ - { - "language": "ko", - "name": "다항식 보간법", - "short": "다항식 보간법" - }, - { - "language": "en", - "name": "polynomial interpolation", - "short": "polynomial interpolation" - }, - { - "language": "ja", - "name": "多項式補間", - "short": "多項式補間" - } - ], - "aliases": [] - }, - { - "key": "lgv", - "isMeta": false, - "bojTagId": 214, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "린드스트롬–게셀–비엔노 보조정리", - "short": "lgv 보조정리" - }, - { - "language": "en", - "name": "lindström–gessel–viennot lemma", - "short": "lgv lemma" - }, - { - "language": "ja", - "name": "lindström–gessel–viennot lemma", - "short": "lgv lemma" - } - ], - "aliases": [] - }, - { - "key": "green", - "isMeta": false, - "bojTagId": 183, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "그린 정리", - "short": "그린" - }, - { - "language": "en", - "name": "green's theorem", - "short": "green's thm" - }, - { - "language": "ja", - "name": "グリーンの定理", - "short": "グリーン" - } - ], - "aliases": [] - }, - { - "key": "directed_mst", - "isMeta": false, - "bojTagId": 23, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "유향 최소 신장 트리", - "short": "유향 최소 신장 트리" - }, - { - "language": "en", - "name": "directed minimum spanning tree", - "short": "dmst" - }, - { - "language": "ja", - "name": "最小全域有向木", - "short": "dmst" - } - ], - "aliases": [ - { - "alias": "유향mst" - } - ] - }, - { - "key": "stoer_wagner", - "isMeta": false, - "bojTagId": 75, - "problemCount": 4, - "displayNames": [ - { - "language": "ko", - "name": "스토어–바그너", - "short": "스토어–바그너" - }, - { - "language": "en", - "name": "stoer–wagner", - "short": "stoer–wagner" - }, - { - "language": "ja", - "name": "stoer–wagner", - "short": "stoer–wagner" - } - ], - "aliases": [ - { - "alias": "stoer-wagner" - }, - { - "alias": "stoer-karger" - }, - { - "alias": "stoer" - }, - { - "alias": "wagner" - }, - { - "alias": "karger" - }, - { - "alias": "global min cut" - }, - { - "alias": "전역 최소 컷" - } - ] - }, - { - "key": "birthday", - "isMeta": false, - "bojTagId": 203, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "생일 문제", - "short": "생일" - }, - { - "language": "en", - "name": "birthday problem", - "short": "birthday" - }, - { - "language": "ja", - "name": "birthday problem", - "short": "birthday" - } - ], - "aliases": [ - { - "alias": "패러독스" - }, - { - "alias": "파라독스" - }, - { - "alias": "birthday" - } - ] - }, - { - "key": "majority_vote", - "isMeta": false, - "bojTagId": 160, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "보이어–무어 다수결 투표", - "short": "보이어–무어 다수결 투표" - }, - { - "language": "en", - "name": "boyer–moore majority vote", - "short": "majority vote" - }, - { - "language": "ja", - "name": "boyer–moore majority vote", - "short": "majority vote" - } - ], - "aliases": [] - }, - { - "key": "multipoint_evaluation", - "isMeta": false, - "bojTagId": 196, - "problemCount": 3, - "displayNames": [ - { - "language": "ko", - "name": "다중 대입값 계산", - "short": "다중 계산" - }, - { - "language": "en", - "name": "multipoint evaluation", - "short": "multipoint evaluation" - }, - { - "language": "ja", - "name": "多点評価", - "short": "多点評価" - } - ], - "aliases": [] - }, - { - "key": "lte", - "isMeta": false, - "bojTagId": 212, - "problemCount": 2, - "displayNames": [ - { - "language": "ko", - "name": "지수승강 보조정리", - "short": "지수승강" - }, - { - "language": "en", - "name": "lifting the exponent lemma", - "short": "lte lemma" - }, - { - "language": "ja", - "name": "lifting the exponent lemma", - "short": "lte lemma" - } - ], - "aliases": [] - }, - { - "key": "hackenbush", - "isMeta": false, - "bojTagId": 205, - "problemCount": 2, - "displayNames": [ - { - "language": "ko", - "name": "하켄부시 게임", - "short": "하켄부시 게임" - }, - { - "language": "en", - "name": "hackenbush", - "short": "hackenbush" - }, - { - "language": "ja", - "name": "hackenbush", - "short": "hackenbush" - } - ], - "aliases": [] - }, - { - "key": "rb_tree", - "isMeta": false, - "bojTagId": 94, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "레드-블랙 트리", - "short": "레드-블랙 트리" - }, - { - "language": "en", - "name": "red-black tree", - "short": "rb tree" - }, - { - "language": "ja", - "name": "red-black tree", - "short": "rb tree" - } - ], - "aliases": [ - { - "alias": "rb트리" - } - ] - }, - { - "key": "floor_sum", - "isMeta": false, - "bojTagId": 218, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "유리 등차수열의 내림 합", - "short": "유리 등차수열의 내림 합" - }, - { - "language": "en", - "name": "sum of floor of rational arithmetic sequence", - "short": "floor sum" - } - ], - "aliases": [] - }, - { - "key": "discrete_kth_root", - "isMeta": false, - "bojTagId": 149, - "problemCount": 1, - "displayNames": [ - { - "language": "ko", - "name": "이산 k제곱근", - "short": "이산 k제곱근" - }, - { - "language": "en", - "name": "discrete k-th root", - "short": "discrete k-th root" - }, - { - "language": "ja", - "name": "離散k平方根", - "short": "離散k平方根" - } - ], - "aliases": [ - { - "alias": "루트" - } - ] - }, - { - "key": "a_star", - "isMeta": false, - "bojTagId": 186, - "problemCount": 0, - "displayNames": [ - { - "language": "ko", - "name": "a*", - "short": "a*" - }, - { - "language": "en", - "name": "a*", - "short": "a*" - }, - { - "language": "ja", - "name": "a*", - "short": "a*" - } - ], - "aliases": [ - { - "alias": "에이" - }, - { - "alias": "에이스타" - } - ] - } - ] -} \ No newline at end of file diff --git a/app/tle/tests.py b/app/tle/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/tle/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From 743593d0e86c0d9cc7716e5bea644462b5fa2bfa Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:25:34 +0900 Subject: [PATCH 318/552] =?UTF-8?q?refactor:=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=93=B1=EC=9D=84=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8D=98=20`ap?= =?UTF-8?q?p`=20=EC=95=B1=EC=9D=84=20`config`=EB=A1=9C=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/{app => config}/__init__.py | 0 app/{app => config}/asgi.py | 2 +- app/{app/views.py => config/reporters.py} | 0 app/{app => config}/settings.py | 6 +++--- app/{app => config}/urls.py | 0 app/{app => config}/wsgi.py | 2 +- app/manage.py | 2 +- 7 files changed, 6 insertions(+), 6 deletions(-) rename app/{app => config}/__init__.py (100%) rename app/{app => config}/asgi.py (82%) rename app/{app/views.py => config/reporters.py} (100%) rename app/{app => config}/settings.py (96%) rename app/{app => config}/urls.py (100%) rename app/{app => config}/wsgi.py (82%) diff --git a/app/app/__init__.py b/app/config/__init__.py similarity index 100% rename from app/app/__init__.py rename to app/config/__init__.py diff --git a/app/app/asgi.py b/app/config/asgi.py similarity index 82% rename from app/app/asgi.py rename to app/config/asgi.py index 120962e..87078af 100644 --- a/app/app/asgi.py +++ b/app/config/asgi.py @@ -11,6 +11,6 @@ from django.core.asgi import get_asgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_asgi_application() diff --git a/app/app/views.py b/app/config/reporters.py similarity index 100% rename from app/app/views.py rename to app/config/reporters.py diff --git a/app/app/settings.py b/app/config/settings.py similarity index 96% rename from app/app/settings.py rename to app/config/settings.py index ea7da7d..5fc67c4 100644 --- a/app/app/settings.py +++ b/app/config/settings.py @@ -59,7 +59,7 @@ "django.middleware.clickjacking.XFrameOptionsMiddleware", ] -ROOT_URLCONF = "app.urls" +ROOT_URLCONF = "config.urls" TEMPLATES = [ { @@ -77,7 +77,7 @@ }, ] -WSGI_APPLICATION = "app.wsgi.application" +WSGI_APPLICATION = "config.wsgi.application" # Database @@ -150,7 +150,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -DEFAULT_EXCEPTION_REPORTER = "app.views.NACLExceptionReporter" +DEFAULT_EXCEPTION_REPORTER = "config.reporters.NACLExceptionReporter" REST_FRAMEWORK = { 'PAGE_SIZE': 10, diff --git a/app/app/urls.py b/app/config/urls.py similarity index 100% rename from app/app/urls.py rename to app/config/urls.py diff --git a/app/app/wsgi.py b/app/config/wsgi.py similarity index 82% rename from app/app/wsgi.py rename to app/config/wsgi.py index 5f42291..a9afbb3 100644 --- a/app/app/wsgi.py +++ b/app/config/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") application = get_wsgi_application() diff --git a/app/manage.py b/app/manage.py index 1a64b14..d28672e 100755 --- a/app/manage.py +++ b/app/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") try: from django.core.management import execute_from_command_line except ImportError as exc: From 4bc5bc1f0296ff09d933771b50fdb005e5ef23c3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:29:10 +0900 Subject: [PATCH 319/552] =?UTF-8?q?refactor(config.settings.py):=20media,?= =?UTF-8?q?=20static=20root=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 ++-- app/config/settings.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index f2dc5bc..50a85c2 100644 --- a/.gitignore +++ b/.gitignore @@ -168,5 +168,5 @@ cython_debug/ # Django **/migrations/* !**/migrations/__init__.py -static/ -media/ +.static/ +.media/ diff --git a/app/config/settings.py b/app/config/settings.py index 5fc67c4..209d7ac 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -133,7 +133,7 @@ STATIC_URL = "static/" -STATIC_ROOT = BASE_DIR / 'static' +STATIC_ROOT = BASE_DIR / '.static' # Meida files (Images) @@ -141,7 +141,7 @@ MEDIA_URL = "media/" -MEDIA_ROOT = BASE_DIR / 'media' +MEDIA_ROOT = BASE_DIR / '.media' # Default primary key field type From 2dab698bbed28380d51475445150d2043ac74a19 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:32:27 +0900 Subject: [PATCH 320/552] refactor(users.models): rename `UserBojLevel` -> `UserBojLevelChoices` --- app/crews/models/crew.py | 4 ++-- app/crews/serializers/fields.py | 6 +++--- app/users/models/__init__.py | 6 ++++-- app/users/models/{user_boj_level.py => choices.py} | 2 +- app/users/models/user.py | 4 ++-- app/users/serializers/fields.py | 6 +++--- 6 files changed, 15 insertions(+), 13 deletions(-) rename app/users/models/{user_boj_level.py => choices.py} (97%) diff --git a/app/crews/models/crew.py b/app/crews/models/crew.py index 15458c5..048543e 100644 --- a/app/crews/models/crew.py +++ b/app/crews/models/crew.py @@ -2,7 +2,7 @@ from django.db import models from crews.validators import EmojiValidator -from users.models import User, UserBojLevel +from users.models import User, UserBojLevelChoices class Crew(models.Model): @@ -45,7 +45,7 @@ class Crew(models.Model): ) min_boj_level = models.IntegerField( help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - choices=UserBojLevel.choices, + choices=UserBojLevelChoices.choices, blank=True, null=True, default=None, diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index 905ea59..eb81a4b 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -8,7 +8,7 @@ from crews.models import Crew, CrewActivity from crews.serializers.mixins import CurrentUserMixin from crews.services import get_members, is_member, is_joinable -from users.models import UserBojLevel +from users.models import UserBojLevelChoices class MemberCountField(ReadOnlyField): @@ -96,13 +96,13 @@ def get_level_tags(self, crew: Crew) -> Iterable[TagDict]: yield TagDict( key=None, name=self.get_boj_level_bounded_name( - level=UserBojLevel(crew.min_boj_level), + level=UserBojLevelChoices(crew.min_boj_level), ), type=TagType.LEVEL.value, ) def get_boj_level_bounded_name(self, - level: Optional[UserBojLevel], + level: Optional[UserBojLevelChoices], bound_tier: int = 5, bound_msg: str = "이상", default_msg: str = "티어 무관", diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py index 2efb280..fe392a9 100644 --- a/app/users/models/__init__.py +++ b/app/users/models/__init__.py @@ -1,10 +1,12 @@ from users.models.user import User from users.models.user_manager import UserManager -from users.models.user_boj_level import UserBojLevel + +from users.models.choices import UserBojLevelChoices __all__ = ( 'User', 'UserManager', - 'UserBojLevel', + + 'UserBojLevelChoices', ) diff --git a/app/users/models/user_boj_level.py b/app/users/models/choices.py similarity index 97% rename from app/users/models/user_boj_level.py rename to app/users/models/choices.py index 27d5b32..137d498 100644 --- a/app/users/models/user_boj_level.py +++ b/app/users/models/choices.py @@ -8,7 +8,7 @@ ARABIC_NUMERALS = ['', 'I', 'II', 'III', 'IV', 'V'] -class UserBojLevel(models.IntegerChoices): +class UserBojLevelChoices(models.IntegerChoices): U = 0, 'Unrated' B5 = 1, '브론즈 5' B4 = 2, '브론즈 4' diff --git a/app/users/models/user.py b/app/users/models/user.py index 2d6d005..632ce5c 100644 --- a/app/users/models/user.py +++ b/app/users/models/user.py @@ -4,8 +4,8 @@ from django.db import models from django.utils import timezone +from users.models.choices import UserBojLevelChoices from users.models.user_manager import UserManager -from users.models.user_boj_level import UserBojLevel def get_profile_image_path(user: User, filename: str) -> str: @@ -31,7 +31,7 @@ class User(AbstractBaseUser, PermissionsMixin): ) boj_level = models.IntegerField( help_text='백준 티어', - choices=UserBojLevel.choices, + choices=UserBojLevelChoices.choices, null=True, blank=True, default=None, diff --git a/app/users/serializers/fields.py b/app/users/serializers/fields.py index 289d832..270e64c 100644 --- a/app/users/serializers/fields.py +++ b/app/users/serializers/fields.py @@ -1,13 +1,13 @@ -from users.models import User, UserBojLevel +from users.models import User, UserBojLevelChoices from users.serializers.mixins import ReadOnlyField class UserBojField(ReadOnlyField): def to_representation(self, user: User): if user.boj_username is None: - user_boj_level = UserBojLevel.U + user_boj_level = UserBojLevelChoices.U else: - user_boj_level = UserBojLevel(user.boj_level) + user_boj_level = UserBojLevelChoices(user.boj_level) return { 'username': user.boj_username, 'profile_url': f'https://boj.kr/{user.boj_username}', From bef2f0abd9019c40c27a382f8eb17c0427259783 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 27 Jul 2024 06:34:35 +0900 Subject: [PATCH 321/552] refactor(problems.models): rename `ProblemDifficulty` -> `ProblemDifficultyChoices` --- app/problems/models/__init__.py | 6 ++++-- .../models/{problem_difficulty.py => choices.py} | 2 +- app/problems/models/problem_analysis.py | 4 ++-- app/problems/serializers/fields.py | 10 +++++----- 4 files changed, 12 insertions(+), 10 deletions(-) rename app/problems/models/{problem_difficulty.py => choices.py} (90%) diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py index 49c29a6..844b869 100644 --- a/app/problems/models/__init__.py +++ b/app/problems/models/__init__.py @@ -3,9 +3,10 @@ from problems.models.problem import Problem from problems.models.problem_analysis import ProblemAnalysis from problems.models.problem_analysis_queue import ProblemAnalysisQueue -from problems.models.problem_difficulty import ProblemDifficulty from problems.models.problem_tag import ProblemTag +from problems.models.choices import ProblemDifficultyChoices + __all__ = ( 'ProblemDTO', @@ -14,6 +15,7 @@ 'Problem', 'ProblemAnalysis', 'ProblemAnalysisQueue', - 'ProblemDifficulty', 'ProblemTag', + + 'ProblemDifficultyChoices', ) diff --git a/app/problems/models/problem_difficulty.py b/app/problems/models/choices.py similarity index 90% rename from app/problems/models/problem_difficulty.py rename to app/problems/models/choices.py index 6db9fe6..fae8cec 100644 --- a/app/problems/models/problem_difficulty.py +++ b/app/problems/models/choices.py @@ -7,7 +7,7 @@ } -class ProblemDifficulty(models.IntegerChoices): +class ProblemDifficultyChoices(models.IntegerChoices): UNDER_ANALYSIS = 0, '분석 중' EASY = 1, '쉬움' NORMAL = 2, '보통' diff --git a/app/problems/models/problem_analysis.py b/app/problems/models/problem_analysis.py index de00a28..2954221 100644 --- a/app/problems/models/problem_analysis.py +++ b/app/problems/models/problem_analysis.py @@ -1,7 +1,7 @@ from django.db import models +from problems.models.choices import ProblemDifficultyChoices from problems.models.problem import Problem -from problems.models.problem_difficulty import ProblemDifficulty from problems.models.problem_tag import ProblemTag @@ -13,7 +13,7 @@ class ProblemAnalysis(models.Model): ) difficulty = models.IntegerField( help_text='문제 난이도를 입력해주세요.', - choices=ProblemDifficulty.choices, + choices=ProblemDifficultyChoices.choices, ) tags = models.ManyToManyField( ProblemTag, diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index 6e18886..3947048 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -1,7 +1,7 @@ from rest_framework.serializers import ModelSerializer from problems.constants import Unit -from problems.models import Problem, ProblemDifficulty, ProblemTag +from problems.models import Problem, ProblemDifficultyChoices, ProblemTag from problems.serializers.mixins import ReadOnlyFieldMixin, AnalysisMixin @@ -32,9 +32,9 @@ def to_representation(self, problem: Problem): class DifficultyField(ReadOnlyFieldMixin, AnalysisMixin): def to_representation(self, problem: Problem): if (analysis := self.get_analysis(problem)) is None: - difficulty = ProblemDifficulty.UNDER_ANALYSIS + difficulty = ProblemDifficultyChoices.UNDER_ANALYSIS else: - difficulty = ProblemDifficulty(analysis.difficulty) + difficulty = ProblemDifficultyChoices(analysis.difficulty) return { "name_ko": difficulty.get_name(lang='ko'), "name_en": difficulty.get_name(lang='en'), @@ -45,7 +45,7 @@ def to_representation(self, problem: Problem): class AnalysisField(ReadOnlyFieldMixin, AnalysisMixin): def to_representation(self, problem: Problem): if (analysis := self.get_analysis(problem)) is None: - difficulty = ProblemDifficulty.UNDER_ANALYSIS + difficulty = ProblemDifficultyChoices.UNDER_ANALYSIS difficulty_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" time_complexity = '' time_complexity_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" @@ -53,7 +53,7 @@ def to_representation(self, problem: Problem): tags = [] is_analyzed = False else: - difficulty = ProblemDifficulty(analysis.difficulty) + difficulty = ProblemDifficultyChoices(analysis.difficulty) difficulty_description = "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" time_complexity = analysis.time_complexity time_complexity_description = "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" From 08be52d4075e4f9bf2fef8e6f5fcf17461c8443a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 30 Jul 2024 04:17:37 +0900 Subject: [PATCH 322/552] =?UTF-8?q?docs:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=ED=94=8C=EB=A1=9C=EC=9A=B0=20=EC=B0=A8?= =?UTF-8?q?=ED=8A=B8=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/diagrams.drawio | 233 ++++++++++++++++++++++++++++++++----------- 1 file changed, 173 insertions(+), 60 deletions(-) diff --git a/docs/diagrams.drawio b/docs/diagrams.drawio index dbf1144..6736b6b 100644 --- a/docs/diagrams.drawio +++ b/docs/diagrams.drawio @@ -1,6 +1,6 @@ - + - + @@ -761,7 +761,7 @@ - + @@ -796,207 +796,207 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -1272,4 +1272,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From c24f759386096e4aaec68d8fa9998b919771b494 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 30 Jul 2024 04:47:44 +0900 Subject: [PATCH 323/552] feat(users.models): create `UserEmailVerification` model #3 --- app/users/models/__init__.py | 6 +-- app/users/models/user_email_verification.py | 55 +++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 app/users/models/user_email_verification.py diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py index fe392a9..2fec29e 100644 --- a/app/users/models/__init__.py +++ b/app/users/models/__init__.py @@ -1,12 +1,12 @@ +from users.models.choices import UserBojLevelChoices from users.models.user import User +from users.models.user_email_verification import UserEmailVerification from users.models.user_manager import UserManager -from users.models.choices import UserBojLevelChoices - __all__ = ( 'User', + 'UserEmailVerification', 'UserManager', - 'UserBojLevelChoices', ) diff --git a/app/users/models/user_email_verification.py b/app/users/models/user_email_verification.py new file mode 100644 index 0000000..8f0171a --- /dev/null +++ b/app/users/models/user_email_verification.py @@ -0,0 +1,55 @@ +from __future__ import annotations +from datetime import timedelta +from hashlib import sha256 +from random import randint + +from django.db import models +from django.utils import timezone + + +N_CODES = 6 + +A, Z = ord('A'), ord('Z') + + +def default_verification_code_factory() -> str: + def code(length=N_CODES): + for _ in range(length): + yield chr(randint(A, Z)) + return ''.join(code()) + + +def default_verification_token_factory() -> str: + return sha256(default_verification_code_factory()).hexdigest() + + +def default_expires_at_factory(): + return timezone.now() + timedelta(minutes=5) + + +class UserEmailVerification(models.Model): + email = models.EmailField( + help_text='이메일 주소', + primary_key=True, + ) + verification_code = models.CharField( + help_text='인증 코드', + max_length=6, + default=default_verification_code_factory, + ) + verification_token = models.TextField( + help_text='인증 토큰', + default=default_verification_token_factory, + ) + expires_at = models.DateTimeField(default=default_expires_at_factory) + created_at = models.DateTimeField(auto_now_add=True) + + class field_name: + EMAIL = 'email' + VERIFICATION_CODE = 'verification_code' + VERIFICATION_TOKEN = 'verification_token' + EXPIRES_AT = 'expires_at' + CREATED_AT = 'created_at' + + def is_expired(self) -> bool: + return self.expires_at < timezone.now() From 917fa9f2d92912b6d453f36e9bb64467580f0045 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 31 Jul 2024 11:07:17 +0900 Subject: [PATCH 324/552] =?UTF-8?q?feat(users):=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8B=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EA=B8=B0=EB=8A=A5=20=EB=BC=88=EB=8C=80=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 7 +- app/users/models/user_email_verification.py | 9 +-- app/users/serializers/__init__.py | 61 ++++++++++++++-- app/users/services/__init__.py | 8 +++ app/users/services/authentication.py | 46 ++++++++++++ app/users/services/verification.py | 37 ++++++++++ app/users/views.py | 79 ++++++++++++++++----- 7 files changed, 219 insertions(+), 28 deletions(-) create mode 100644 app/users/services/__init__.py create mode 100644 app/users/services/authentication.py create mode 100644 app/users/services/verification.py diff --git a/app/config/urls.py b/app/config/urls.py index ffcb3df..0331790 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -19,11 +19,10 @@ from django.contrib import admin from django.urls import include, path -from users.views import SignIn, SignUp, SignOut, CurrentUser +from users.views import * from problems.views import ProblemCreate, ProblemDetail, ProblemSearch from crews.views import CrewCreate, CrewDetail, CrewRecruiting, CrewJoined - urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ @@ -32,6 +31,10 @@ path("signup", SignUp.as_view()), path("signout", SignOut.as_view()), ])), + path("verification", include([ + path("email/send", SendVerificationCode.as_view()), + path("email/validate", ValidateVerificationCode.as_view()), + ])), path("users/current", CurrentUser.as_view()), path("problems/", include([ path("", ProblemCreate.as_view()), diff --git a/app/users/models/user_email_verification.py b/app/users/models/user_email_verification.py index 8f0171a..b4f8480 100644 --- a/app/users/models/user_email_verification.py +++ b/app/users/models/user_email_verification.py @@ -32,14 +32,15 @@ class UserEmailVerification(models.Model): help_text='이메일 주소', primary_key=True, ) - verification_code = models.CharField( + verification_code = models.TextField( help_text='인증 코드', - max_length=6, - default=default_verification_code_factory, + null=False, + blank=False, ) verification_token = models.TextField( help_text='인증 토큰', - default=default_verification_token_factory, + null=True, + blank=True, ) expires_at = models.DateTimeField(default=default_expires_at_factory) created_at = models.DateTimeField(auto_now_add=True) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index e295d46..a133878 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -5,6 +5,16 @@ from users.serializers.mixins import ReadOnlySerializerMixin +__all__ = ( + 'UserSignInSerializer', + 'UserSignUpSerializer', + 'UserDetailSerializer', + 'UserMinimalSerializer', + 'UserEmailSerializer', + 'UserEmailVerificationCodeSerializer', +) + + class UserSignInSerializer(ModelSerializer, ReadOnlySerializerMixin): email = EmailField(write_only=True, validators=None) boj = UserBojField(read_only=True) @@ -32,6 +42,37 @@ class Meta: } +class UserSignUpSerializer(ModelSerializer, ReadOnlySerializerMixin): + boj = UserBojField(read_only=True) + verification_token = CharField(write_only=True) + + class Meta: + model = User + fields = [ + 'id', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, + User.field_name.BOJ_USERNAME, + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + 'verification_token', + ] + read_only_fields = [ + 'id', + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + ] + extra_kwargs = { + User.field_name.PASSWORD: {'write_only': True}, + User.field_name.BOJ_USERNAME: {'write_only': True}, + 'verification_token': {'write_only': True}, + } + + class UserDetailSerializer(ModelSerializer, ReadOnlySerializerMixin): boj = UserBojField(read_only=True) @@ -48,11 +89,13 @@ class Meta: User.field_name.CREATED_AT, User.field_name.LAST_LOGIN, ] + read_only_fields = [ + 'id', + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + ] extra_kwargs = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - User.field_name.CREATED_AT: {'read_only': True}, - User.field_name.LAST_LOGIN: {'read_only': True}, User.field_name.PASSWORD: {'write_only': True}, User.field_name.BOJ_USERNAME: {'write_only': True}, } @@ -71,3 +114,13 @@ class Meta: User.field_name.PROFILE_IMAGE: {'read_only': True}, User.field_name.USERNAME: {'read_only': True}, } + + +class UserEmailSerializer(Serializer): + email = EmailField() + + +class UserEmailVerificationCodeSerializer(Serializer, ReadOnlySerializerMixin): + email = EmailField(write_only=True) + code = CharField(write_only=True) + token = CharField(read_only=True) diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py new file mode 100644 index 0000000..e0a3720 --- /dev/null +++ b/app/users/services/__init__.py @@ -0,0 +1,8 @@ +from users.services.authentication import AuthenticationService +from users.services.verification import VerificationService + + +__all__ = ( + 'AuthenticationService', + 'VerificationService', +) diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py new file mode 100644 index 0000000..487c1c3 --- /dev/null +++ b/app/users/services/authentication.py @@ -0,0 +1,46 @@ +"""인증과 관련된 서비스들입니다. + +사용자 로그인, 회원가입, 로그아웃 로직을 담고 있습니다. +""" +from django.contrib.auth import authenticate, login, logout +from rest_framework.exceptions import AuthenticationFailed, ValidationError +from rest_framework.request import Request + +from users.models import User, UserEmailVerification, UserManager + + +class AuthenticationService: + @staticmethod + def sign_up(email: str, username: str, password: str, **extra_fields) -> User: + """회원가입 + + 이메일 인증 토큰이 필요합니다. + 인증에 실패할 경우 ValidationError를 발생시킵니다.""" + user_manager: UserManager = User.objects + verification_token = extra_fields.pop('verification_token', None) + # 이메일 주소 인증 + if not UserEmailVerification.objects.filter(**{ + UserEmailVerification.field_name.EMAIL: email, + UserEmailVerification.field_name.VERIFICATION_TOKEN: verification_token + }).exists(): + raise ValidationError('Email is not verified.') + return user_manager.create_user(email, username, password, **extra_fields) + + @staticmethod + def sign_in(request: Request, email: str, password: str) -> User: + """로그인 + + 사용자 인증에 실패할 경우 AuthenticationFailed를 발생시킵니다.""" + # 사용자 인증 + user = authenticate(request, username=email, password=password) + # 사용자 인증 실패 시 예외 발생 + if user is None: + raise AuthenticationFailed('Invalid email or password') + # 사용자 인증 성공 시 (세션) 로그인 + login(request, user) + return user + + @staticmethod + def sign_out(request: Request): + """로그아웃""" + logout(request) diff --git a/app/users/services/verification.py b/app/users/services/verification.py new file mode 100644 index 0000000..1028541 --- /dev/null +++ b/app/users/services/verification.py @@ -0,0 +1,37 @@ +"""이 서비스는 사용자 확인 절차를 수행하는 데 필요한 기능을 제공합니다. + +사용자 확인 절차는 다음과 같습니다: +1. 사용자가 이메일 주소를 입력합니다. +2. 서버는 해당 이메일 주소로 인증 코드를 전송합니다. +3. 사용자는 인증 코드를 입력합니다. +4. 서버는 인증 코드를 확인합니다. +5. 서버는 인증 코드를 확인한 사용자에게 인증 토큰을 전송합니다. +6. 사용자는 회원가입 절차에서 인증 토큰을 입력합니다. +7. 서버는 인증 토큰을 확인하여 사용자를 확인합니다. +""" + + +class VerificationService: + @staticmethod + def is_verified(email: str) -> bool: + pass + + @staticmethod + def get_verification_code(email: str) -> str: + pass + + @staticmethod + def send_verification_code(email: str, verification_code: str) -> str: + pass + + @staticmethod + def get_verification_token(email: str, verification_code: str) -> str: + pass + + @staticmethod + def validate_verification_code(email: str, verification_code: str) -> None: + pass + + @staticmethod + def validate_verification_token(email: str, verification_token: str) -> None: + pass diff --git a/app/users/views.py b/app/users/views.py index a054714..82a419c 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,18 +1,29 @@ from typing import Callable -from django.contrib.auth import authenticate, login, logout from rest_framework import ( mixins, permissions, status, ) -from rest_framework.exceptions import AuthenticationFailed +from rest_framework.exceptions import ValidationError from rest_framework.generics import GenericAPIView +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import Serializer -from users.models import User, UserManager -from users.serializers import UserDetailSerializer, UserSignInSerializer +from users.models import User +from users.serializers import * +from users.services import * + + +__all__ = ( + 'SignUp', + 'SignIn', + 'SignOut', + 'CurrentUser', + 'SendVerificationCode', + 'ValidateVerificationCode', +) class SignUp(mixins.CreateModelMixin, @@ -26,8 +37,9 @@ def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def perform_create(self, serializer: Serializer): - user_manager: UserManager = User.objects - user = user_manager.create_user(**serializer.validated_data) + token = serializer.validated_data.pop('verification_token') + VerificationService.validate_verification_token(token) + user = AuthenticationService.sign_up(**serializer.validated_data) serializer.instance = user @@ -43,17 +55,11 @@ class SignIn(mixins.RetrieveModelMixin, def get_object(self) -> User: serializer = self.get_serializer(data=self.request.data) serializer.is_valid(raise_exception=True) - # 사용자 인증을 위한 email과 password를 추출 - email = serializer.validated_data['email'] - password = serializer.validated_data['password'] - # 사용자 인증 - user = authenticate(self.request, username=email, password=password) - # 사용자 인증 실패 시 예외 발생 - if user is None: - raise AuthenticationFailed('Invalid email or password') - # 사용자 인증 성공 시 (세션) 로그인 - login(self.request, user) - return user + return AuthenticationService.sign_in( + request=self.request, + email=serializer.validated_data['email'], + password=serializer.validated_data['password'], + ) def post(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) @@ -65,7 +71,7 @@ class SignOut(GenericAPIView): permission_classes = [permissions.IsAuthenticated] def get(self, request, *args, **kwargs): - logout(request) + AuthenticationService.sign_out(request) return Response(status=status.HTTP_204_NO_CONTENT) @@ -81,3 +87,40 @@ def get_object(self) -> User: def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) + + +class SendVerificationCode(GenericAPIView): + """이메일 인증 요청 API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserEmailSerializer + + get_serializer: Callable[..., Serializer] + + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + if VerificationService.is_verified(email): + raise ValidationError('Email is already verified.') + code = VerificationService.get_verification_code(email) + VerificationService.send_verification_code(email, code) + return Response(status=status.HTTP_200_OK) + + +class ValidateVerificationCode(GenericAPIView): + """이메일 인증 코드 검증 API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserEmailVerificationCodeSerializer + + get_serializer: Callable[..., Serializer] + + def post(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + code = serializer.validated_data['code'] + token = VerificationService.get_verification_token(email, code) + serializer.validated_data['token'] = token + return Response(serializer.data, status=status.HTTP_200_OK) From 6fc678afc42884f9c555b48909ead845c2f5c371 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 31 Jul 2024 11:29:19 +0900 Subject: [PATCH 325/552] =?UTF-8?q?feat(users):=20`VerificationService`=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20#3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 5 +- app/users/models/user_email_verification.py | 18 ----- app/users/services/verification.py | 78 +++++++++++++++++++-- app/users/views.py | 20 +++--- 4 files changed, 81 insertions(+), 40 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index 0331790..a2f4d93 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -30,10 +30,7 @@ path("signin", SignIn.as_view()), path("signup", SignUp.as_view()), path("signout", SignOut.as_view()), - ])), - path("verification", include([ - path("email/send", SendVerificationCode.as_view()), - path("email/validate", ValidateVerificationCode.as_view()), + path("verification", EmailVerification.as_view()), ])), path("users/current", CurrentUser.as_view()), path("problems/", include([ diff --git a/app/users/models/user_email_verification.py b/app/users/models/user_email_verification.py index b4f8480..ce795b7 100644 --- a/app/users/models/user_email_verification.py +++ b/app/users/models/user_email_verification.py @@ -1,28 +1,10 @@ from __future__ import annotations from datetime import timedelta -from hashlib import sha256 -from random import randint from django.db import models from django.utils import timezone -N_CODES = 6 - -A, Z = ord('A'), ord('Z') - - -def default_verification_code_factory() -> str: - def code(length=N_CODES): - for _ in range(length): - yield chr(randint(A, Z)) - return ''.join(code()) - - -def default_verification_token_factory() -> str: - return sha256(default_verification_code_factory()).hexdigest() - - def default_expires_at_factory(): return timezone.now() + timedelta(minutes=5) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index 1028541..7ea8edb 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -9,29 +9,95 @@ 6. 사용자는 회원가입 절차에서 인증 토큰을 입력합니다. 7. 서버는 인증 토큰을 확인하여 사용자를 확인합니다. """ +from hashlib import sha256 +from random import randint + +from django.core.mail import send_mail +from rest_framework.exceptions import ValidationError + +from users.models import User, UserEmailVerification class VerificationService: @staticmethod def is_verified(email: str) -> bool: - pass + return User.objects.filter(**{ + User.field_name.EMAIL: email, + }).exists() @staticmethod def get_verification_code(email: str) -> str: - pass + if has_verification_code(email): + if not (obj := get_verification_object(email)).is_expired(): + return obj.verification_code + else: + obj.delete() + verification_code = generate_verification_code() + create_verification_object(email, verification_code) + return verification_code @staticmethod def send_verification_code(email: str, verification_code: str) -> str: - pass + send_mail( + subject='[Time Limit Exceeded] 이메일 주소 인증 코드', + message=f'인증 코드: {verification_code}', + from_email=None, + recipient_list=[email], + fail_silently=False, + ) @staticmethod def get_verification_token(email: str, verification_code: str) -> str: - pass + VerificationService.validate_verification_code( + email, verification_code) + verification_token = generate_verification_token() + obj = get_verification_object(email) + obj.verification_token = verification_token + obj.save() + return verification_token @staticmethod def validate_verification_code(email: str, verification_code: str) -> None: - pass + if not has_verification_code(email): + raise ValidationError('Verification code does not exist.') + obj = get_verification_object(email) + if obj.is_expired(): + raise ValidationError('Verification code is expired.') + if obj.verification_code != verification_code: + raise ValidationError('Verification code is invalid.') @staticmethod def validate_verification_token(email: str, verification_token: str) -> None: - pass + if not has_verification_code(email): + raise ValidationError('Verification token does not exist.') + obj = get_verification_object(email) + if obj.verification_token != verification_token: + raise ValidationError('Verification token is invalid.') + + +def has_verification_code(email: str) -> bool: + return UserEmailVerification.objects.filter(**{ + UserEmailVerification.field_name.EMAIL: email + }).exists() + + +def create_verification_object(email: str, verification_code: str) -> UserEmailVerification: + return UserEmailVerification.objects.create(**{ + UserEmailVerification.field_name.EMAIL: email, + UserEmailVerification.field_name.VERIFICATION_CODE: verification_code + }) + + +def get_verification_object(email: str) -> UserEmailVerification: + return UserEmailVerification.objects.get(**{ + UserEmailVerification.field_name.EMAIL: email + }) + + +def generate_verification_code(length: int = 6) -> str: + return ''.join(chr(randint(ord('A'), ord('Z'))) for _ in range(length)) + + +def generate_verification_token() -> str: + seed = generate_verification_code() # TODO: Use better seed + return sha256(seed.encode()).hexdigest() diff --git a/app/users/views.py b/app/users/views.py index 82a419c..d467b80 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -21,8 +21,7 @@ 'SignIn', 'SignOut', 'CurrentUser', - 'SendVerificationCode', - 'ValidateVerificationCode', + 'EmailVerification', ) @@ -89,7 +88,7 @@ def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) -class SendVerificationCode(GenericAPIView): +class EmailVerification(GenericAPIView): """이메일 인증 요청 API""" permission_classes = [permissions.AllowAny] @@ -97,6 +96,12 @@ class SendVerificationCode(GenericAPIView): get_serializer: Callable[..., Serializer] + def get_serializer_class(self): + if self.request.method == 'GET': + return UserEmailSerializer + if self.request.method == 'POST': + return UserEmailVerificationCodeSerializer + def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) @@ -107,15 +112,6 @@ def get(self, request: Request, *args, **kwargs): VerificationService.send_verification_code(email, code) return Response(status=status.HTTP_200_OK) - -class ValidateVerificationCode(GenericAPIView): - """이메일 인증 코드 검증 API""" - - permission_classes = [permissions.AllowAny] - serializer_class = UserEmailVerificationCodeSerializer - - get_serializer: Callable[..., Serializer] - def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) From de8000fb44c5cf891d184ba7dbf6ce44e403767d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 12:59:16 +0900 Subject: [PATCH 326/552] =?UTF-8?q?refactor:=20url=20=EC=9E=84=ED=8F=AC?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 63 ++++++++++++++-------------------------------- 1 file changed, 19 insertions(+), 44 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index a2f4d93..602e8bc 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -1,55 +1,30 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.conf import settings from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path -from users.views import * -from problems.views import ProblemCreate, ProblemDetail, ProblemSearch -from crews.views import CrewCreate, CrewDetail, CrewRecruiting, CrewJoined +import crews.views +import problems.views +import users.views + urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ - path("auth/", include([ - path("signin", SignIn.as_view()), - path("signup", SignUp.as_view()), - path("signout", SignOut.as_view()), - path("verification", EmailVerification.as_view()), - ])), - path("users/current", CurrentUser.as_view()), - path("problems/", include([ - path("", ProblemCreate.as_view()), - path("search", ProblemSearch.as_view()), - path("/detail", ProblemDetail.as_view()), - ])), - path("crews/", include([ - path("", CrewCreate.as_view()), - path("recruiting", CrewRecruiting.as_view()), - path("my", CrewJoined.as_view()), - path("/detail", CrewDetail.as_view()), - ])), + path("auth/signin", users.views.SignInAPIView.as_view()), + path("auth/signup", users.views.SignUpAPIView.as_view()), + path("auth/signout", users.views.SignOutAPIView.as_view()), + path("auth/verification/code", users.views.EmailVerificationCodeAPIView.as_view()), + path("auth/verification/token", users.views.EmailVerificationTokenAPIView.as_view()), + path("crews/", crews.views.CrewCreate.as_view()), + path("crews/recruiting", crews.views.CrewRecruiting.as_view()), + path("crews/my", crews.views.CrewJoined.as_view()), + path("crews//detail", crews.views.CrewDetail.as_view()), + path("problems/", problems.views.ProblemCreate.as_view()), + path("problems/search", problems.views.ProblemSearch.as_view()), + path("problems//detail", problems.views.ProblemDetail.as_view()), + path("users/current", users.views.CurrentUserAPIView.as_view()), ])), + *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] - -# Static files -urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - -# Media files -# TODO: 미디어 파일은 S3 같은 외부 의존성으로 변경하기 -urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) From 2b643c2d570cce9c9ab9e750059feca70f95c521 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 13:01:12 +0900 Subject: [PATCH 327/552] refactor: rename **/views.py -> **/views/__init__.py --- app/crews/{views.py => views/__init__.py} | 0 app/problems/{views.py => views/__init__.py} | 0 app/users/{views.py => views/__init__.py} | 84 +++++++++----------- 3 files changed, 38 insertions(+), 46 deletions(-) rename app/crews/{views.py => views/__init__.py} (100%) rename app/problems/{views.py => views/__init__.py} (100%) rename app/users/{views.py => views/__init__.py} (72%) diff --git a/app/crews/views.py b/app/crews/views/__init__.py similarity index 100% rename from app/crews/views.py rename to app/crews/views/__init__.py diff --git a/app/problems/views.py b/app/problems/views/__init__.py similarity index 100% rename from app/problems/views.py rename to app/problems/views/__init__.py diff --git a/app/users/views.py b/app/users/views/__init__.py similarity index 72% rename from app/users/views.py rename to app/users/views/__init__.py index d467b80..5727b77 100644 --- a/app/users/views.py +++ b/app/users/views/__init__.py @@ -1,15 +1,13 @@ from typing import Callable -from rest_framework import ( - mixins, - permissions, - status, -) +from rest_framework import generics +from rest_framework import permissions +from rest_framework import status from rest_framework.exceptions import ValidationError -from rest_framework.generics import GenericAPIView from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import Serializer +from rest_framework.views import APIView from users.models import User from users.serializers import * @@ -17,33 +15,15 @@ __all__ = ( - 'SignUp', - 'SignIn', - 'SignOut', - 'CurrentUser', + 'SignUpAPIView', + 'SignInAPIView', + 'SignOutAPIView', + 'CurrentUserAPIView', 'EmailVerification', ) -class SignUp(mixins.CreateModelMixin, - GenericAPIView): - """사용자 등록(회원가입) API""" - - permission_classes = [permissions.AllowAny] - serializer_class = UserDetailSerializer - - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) - - def perform_create(self, serializer: Serializer): - token = serializer.validated_data.pop('verification_token') - VerificationService.validate_verification_token(token) - user = AuthenticationService.sign_up(**serializer.validated_data) - serializer.instance = user - - -class SignIn(mixins.RetrieveModelMixin, - GenericAPIView): +class SignInAPIView(generics.RetrieveAPIView): """사용자 로그인 API""" permission_classes = [permissions.AllowAny] @@ -64,7 +44,7 @@ def post(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) -class SignOut(GenericAPIView): +class SignOutAPIView(APIView): """사용자 로그아웃 API""" permission_classes = [permissions.IsAuthenticated] @@ -74,8 +54,20 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class CurrentUser(mixins.RetrieveModelMixin, - GenericAPIView): +class SignUpAPIView(generics.CreateAPIView): + """사용자 등록(회원가입) API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserDetailSerializer + + def perform_create(self, serializer: Serializer): + token = serializer.validated_data.pop('verification_token') + VerificationService.validate_verification_token(token) + user = AuthenticationService.sign_up(**serializer.validated_data) + serializer.instance = user + + +class CurrentUserAPIView(generics.RetrieveAPIView): """현재 로그인한 사용자 정보 API""" permission_classes = [permissions.IsAuthenticated] @@ -84,33 +76,33 @@ class CurrentUser(mixins.RetrieveModelMixin, def get_object(self) -> User: return self.request.user - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - -class EmailVerification(GenericAPIView): - """이메일 인증 요청 API""" +class EmailVerificationCodeAPIView(generics.GenericAPIView): + """이메일 인증 코드 전송 API""" permission_classes = [permissions.AllowAny] serializer_class = UserEmailSerializer get_serializer: Callable[..., Serializer] - def get_serializer_class(self): - if self.request.method == 'GET': - return UserEmailSerializer - if self.request.method == 'POST': - return UserEmailVerificationCodeSerializer - - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) + def post(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] if VerificationService.is_verified(email): raise ValidationError('Email is already verified.') code = VerificationService.get_verification_code(email) VerificationService.send_verification_code(email, code) - return Response(status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_200_OK) + + +class EmailVerificationTokenAPIView(generics.GenericAPIView): + """이메일 인증 토큰 발급 API""" + + permission_classes = [permissions.AllowAny] + serializer_class = UserEmailVerificationCodeSerializer + + get_serializer: Callable[..., Serializer] def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) From 81875eadd56248abffa6ee66e2466e2aae7922cd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 13:33:17 +0900 Subject: [PATCH 328/552] test: add user signin api test --- app/users/tests/__init__.py | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 app/users/tests/__init__.py diff --git a/app/users/tests/__init__.py b/app/users/tests/__init__.py new file mode 100644 index 0000000..fa5099d --- /dev/null +++ b/app/users/tests/__init__.py @@ -0,0 +1,68 @@ +from django.utils import timezone +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status + +from users.models import User, BojLevelChoices + + +class SignInTest(TestCase): + def setUp(self): + self.maxDiff = None + self.client = APIClient() + self.url = '/api/v1/auth/signin' + self.now = timezone.now() + self.user = User.objects.create(**{ + User.field_name.EMAIL: 'email@example.com', + User.field_name.USERNAME: 'username', + User.field_name.PASSWORD: 'password', + User.field_name.BOJ_USERNAME: 'boj_username', + User.field_name.BOJ_LEVEL: BojLevelChoices.S1, + User.field_name.BOJ_LEVEL_UPDATED_AT: self.now, + }) + + def test_returns_200(self): + res = self.client.post(self.url, { + 'email': 'email@example.com', + 'password': 'password', + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_returns_400(self): + res = self.client.post(self.url, { + 'username': 'username', + 'password': 'password', + }) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_returns_403(self): + res = self.client.post(self.url, { + 'email': 'email@example.com', + 'password': 'password2', + }) + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + + def test_200_response(self): + res = self.client.post(self.url, { + 'email': 'email@example.com', + 'password': 'password', + }) + user = User.objects.get(pk=1) + self.assertJSONEqual(res.content, { + 'id': 1, + 'boj': { + 'username': 'boj_username', + 'profile_url': 'https://boj.kr/boj_username', + 'level': 10, + 'division': 2, + 'division_name_en': 'Silver', + 'division_name_ko': '실버', + 'tier': 1, + 'tier_name': 'I', + 'tier_updated_at': user.boj_level_updated_at.isoformat(), + }, + 'profile_image': None, + 'username': 'username', + 'created_at': user.created_at.isoformat(), + 'last_login': user.last_login.isoformat(), + }) From f791884f6558a89051ad3288dad1ad7ea6cab3dc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 17:04:58 +0900 Subject: [PATCH 329/552] feat: use swagger --- app/config/settings.py | 1 + app/config/urls.py | 17 +++++++++++++++++ requirements.txt | 1 + 3 files changed, 19 insertions(+) diff --git a/app/config/settings.py b/app/config/settings.py index 209d7ac..6373903 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -42,6 +42,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "drf_yasg", "rest_framework", "users", "problems", diff --git a/app/config/urls.py b/app/config/urls.py index 602e8bc..43a8e6f 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -2,12 +2,27 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import include, path +from drf_yasg import openapi +from drf_yasg.views import get_schema_view +from rest_framework import permissions import crews.views import problems.views import users.views +schema_view = get_schema_view( + info=openapi.Info( + title="Time Limit Exceeded API Server", + default_version='1.0.0', + description="", + contact=openapi.Contact(email="202115064@sangmyung.kr"), + ), + public=True, + permission_classes=[permissions.IsAdminUser], +) + + urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ @@ -25,6 +40,8 @@ path("problems//detail", problems.views.ProblemDetail.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), ])), + path(r'swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), + path(r'swagger(?P\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'), *static(settings.STATIC_URL, document_root=settings.STATIC_ROOT), *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] diff --git a/requirements.txt b/requirements.txt index 3b5a3d5..e85ab90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ django django-cors-headers djangorestframework Pillow +drf-yasg From 5c1cc6a5911511e16cf32e84e5935b36a5336db4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 17:06:03 +0900 Subject: [PATCH 330/552] tmp --- app/users/serializers/__init__.py | 52 +++++++++++++++++++++++-- app/users/views/__init__.py | 64 +++++++++++++++++++------------ 2 files changed, 89 insertions(+), 27 deletions(-) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index a133878..8c03a64 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -1,3 +1,4 @@ +from rest_framework import serializers from rest_framework.serializers import * from users.models import User @@ -11,13 +12,58 @@ 'UserDetailSerializer', 'UserMinimalSerializer', 'UserEmailSerializer', - 'UserEmailVerificationCodeSerializer', + 'EmailVerificationCodeSerializer', ) +class EmailSerializer(serializers.Serializer): + email = EmailField() + + +class EmailVerificationCodeSerializer(serializers.Serializer): + email = EmailField(write_only=True) + code = CharField(write_only=True) + token = CharField(read_only=True) + + +class SignInSerializer(serializers.Serializer): + email = EmailField() + password = CharField() + + +class SignUpSerializer(serializers.ModelSerializer): + verification_token = CharField() + + class Meta: + model = User + fields = [ + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.PASSWORD, + User.field_name.BOJ_USERNAME, + 'verification_token', + ] + + +class UserSerializer(serializers.ModelSerializer): + boj = UserBojField() + + class Meta: + model = User + fields = [ + 'id', + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + 'boj', + User.field_name.CREATED_AT, + User.field_name.LAST_LOGIN, + ] + + class UserSignInSerializer(ModelSerializer, ReadOnlySerializerMixin): email = EmailField(write_only=True, validators=None) - boj = UserBojField(read_only=True) class Meta: model = User @@ -120,7 +166,7 @@ class UserEmailSerializer(Serializer): email = EmailField() -class UserEmailVerificationCodeSerializer(Serializer, ReadOnlySerializerMixin): +class EmailVerificationCodeSerializer(Serializer, ReadOnlySerializerMixin): email = EmailField(write_only=True) code = CharField(write_only=True) token = CharField(read_only=True) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 5727b77..2b45dac 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -1,8 +1,8 @@ from typing import Callable -from rest_framework import generics -from rest_framework import permissions -from rest_framework import status +from drf_yasg import openapi +from drf_yasg.utils import swagger_auto_schema +from rest_framework import generics, mixins, permissions, status from rest_framework.exceptions import ValidationError from rest_framework.request import Request from rest_framework.response import Response @@ -10,6 +10,7 @@ from rest_framework.views import APIView from users.models import User +from users import serializers from users.serializers import * from users.services import * @@ -23,12 +24,12 @@ ) -class SignInAPIView(generics.RetrieveAPIView): +class SignInAPIView(mixins.RetrieveModelMixin, + generics.GenericAPIView): """사용자 로그인 API""" permission_classes = [permissions.AllowAny] - serializer_class = UserSignInSerializer - + serializer_class = serializers.SignInSerializer get_serializer: Callable[..., Serializer] def get_object(self) -> User: @@ -40,26 +41,26 @@ def get_object(self) -> User: password=serializer.validated_data['password'], ) + @swagger_auto_schema(responses={ + status.HTTP_200_OK: '로그인 성공', + status.HTTP_401_UNAUTHORIZED: '로그인 실패', + }) def post(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - -class SignOutAPIView(APIView): - """사용자 로그아웃 API""" - - permission_classes = [permissions.IsAuthenticated] - - def get(self, request, *args, **kwargs): - AuthenticationService.sign_out(request) - return Response(status=status.HTTP_204_NO_CONTENT) + instance = self.get_object() + serializer = serializers.UserSerializer(instance) + return Response(serializer.data) class SignUpAPIView(generics.CreateAPIView): """사용자 등록(회원가입) API""" permission_classes = [permissions.AllowAny] - serializer_class = UserDetailSerializer + serializer_class = serializers.SignUpSerializer + @swagger_auto_schema(responses={ + status.HTTP_201_CREATED: '회원가입 성공', + status.HTTP_400_BAD_REQUEST: '잘못 입력한 값이 존재', + }) def perform_create(self, serializer: Serializer): token = serializer.validated_data.pop('verification_token') VerificationService.validate_verification_token(token) @@ -67,11 +68,24 @@ def perform_create(self, serializer: Serializer): serializer.instance = user +class SignOutAPIView(APIView): + """사용자 로그아웃 API""" + + permission_classes = [permissions.IsAuthenticated] + + @swagger_auto_schema(responses={ + status.HTTP_204_NO_CONTENT: '로그아웃 성공', + }) + def get(self, request, *args, **kwargs): + AuthenticationService.sign_out(request) + return Response(status=status.HTTP_204_NO_CONTENT) + + class CurrentUserAPIView(generics.RetrieveAPIView): """현재 로그인한 사용자 정보 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = UserDetailSerializer + serializer_class = serializers.UserSerializer def get_object(self) -> User: return self.request.user @@ -81,10 +95,13 @@ class EmailVerificationCodeAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" permission_classes = [permissions.AllowAny] - serializer_class = UserEmailSerializer - + serializer_class = serializers.EmailSerializer get_serializer: Callable[..., Serializer] + @swagger_auto_schema(responses={ + status.HTTP_201_CREATED: '회원가입 성공', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + }) def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -93,15 +110,14 @@ def post(self, request: Request, *args, **kwargs): raise ValidationError('Email is already verified.') code = VerificationService.get_verification_code(email) VerificationService.send_verification_code(email, code) - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(serializer.data, status=status.HTTP_201_CREATED) class EmailVerificationTokenAPIView(generics.GenericAPIView): """이메일 인증 토큰 발급 API""" permission_classes = [permissions.AllowAny] - serializer_class = UserEmailVerificationCodeSerializer - + serializer_class = serializers.EmailVerificationCodeSerializer get_serializer: Callable[..., Serializer] def post(self, request: Request, *args, **kwargs): From ec602215085027f4ed9f958fc2fce233d025a62f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 17:29:56 +0900 Subject: [PATCH 331/552] =?UTF-8?q?fix:=20CORS=20=ED=97=88=EC=9A=A9=20(?= =?UTF-8?q?=EB=82=98=EC=A4=91=EC=97=90=20FE=EB=A5=BC=20tle.kr=EC=97=90=20?= =?UTF-8?q?=EC=98=AC=EB=A6=B0=EB=8B=A4=EB=A9=B4=20=EC=9D=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=8B=A4=EC=8B=9C=20=ED=95=B4=EC=A0=9C=ED=95=A0=20=EA=B2=83)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/config/settings.py b/app/config/settings.py index 6373903..d64f3e1 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -32,6 +32,11 @@ 'localhost', ] +# CORS + +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True + # Application definition @@ -42,6 +47,7 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "corsheaders", "drf_yasg", "rest_framework", "users", @@ -51,10 +57,10 @@ ] MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", From 960d03046551d0fc3af18f2e5234eb84a7998f68 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 19:11:11 +0900 Subject: [PATCH 332/552] =?UTF-8?q?refactor(users):=20=EB=B6=88=ED=95=84?= =?UTF-8?q?=EC=9A=94=ED=95=9C=20Serializer=EB=93=A4=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 4 +- app/users/serializers/__init__.py | 111 ++------------------- app/users/services/__init__.py | 12 ++- app/users/services/authentication.py | 66 ++++++------- app/users/services/verification.py | 138 +++++++++++++++------------ app/users/views/__init__.py | 37 ++++--- 6 files changed, 143 insertions(+), 225 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index 43a8e6f..c402808 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -29,8 +29,8 @@ path("auth/signin", users.views.SignInAPIView.as_view()), path("auth/signup", users.views.SignUpAPIView.as_view()), path("auth/signout", users.views.SignOutAPIView.as_view()), - path("auth/verification/code", users.views.EmailVerificationCodeAPIView.as_view()), - path("auth/verification/token", users.views.EmailVerificationTokenAPIView.as_view()), + path("auth/verification/code", users.views.EmailCodeAPIView.as_view()), + path("auth/verification/token", users.views.EmailTokenAPIView.as_view()), path("crews/", crews.views.CrewCreate.as_view()), path("crews/recruiting", crews.views.CrewRecruiting.as_view()), path("crews/my", crews.views.CrewJoined.as_view()), diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index 8c03a64..220142f 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -12,7 +12,7 @@ 'UserDetailSerializer', 'UserMinimalSerializer', 'UserEmailSerializer', - 'EmailVerificationCodeSerializer', + 'EmailCodeSerializer', ) @@ -20,10 +20,14 @@ class EmailSerializer(serializers.Serializer): email = EmailField() -class EmailVerificationCodeSerializer(serializers.Serializer): - email = EmailField(write_only=True) - code = CharField(write_only=True) - token = CharField(read_only=True) +class EmailCodeSerializer(serializers.Serializer): + email = EmailField() + code = CharField() + + +class EmailTokenSerializer(serializers.Serializer): + email = EmailField() + token = CharField() class SignInSerializer(serializers.Serializer): @@ -47,24 +51,6 @@ class Meta: class UserSerializer(serializers.ModelSerializer): - boj = UserBojField() - - class Meta: - model = User - fields = [ - 'id', - User.field_name.EMAIL, - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - - -class UserSignInSerializer(ModelSerializer, ReadOnlySerializerMixin): - email = EmailField(write_only=True, validators=None) - class Meta: model = User fields = [ @@ -72,79 +58,10 @@ class Meta: User.field_name.EMAIL, User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, - User.field_name.PASSWORD, - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - extra_kwargs = { - 'id': {'read_only': True}, - 'boj': {'read_only': True}, - User.field_name.PROFILE_IMAGE: {'read_only': True}, - User.field_name.USERNAME: {'read_only': True}, - User.field_name.CREATED_AT: {'read_only': True}, - User.field_name.LAST_LOGIN: {'read_only': True}, - User.field_name.PASSWORD: {'write_only': True}, - } - - -class UserSignUpSerializer(ModelSerializer, ReadOnlySerializerMixin): - boj = UserBojField(read_only=True) - verification_token = CharField(write_only=True) - - class Meta: - model = User - fields = [ - 'id', - User.field_name.EMAIL, - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - User.field_name.PASSWORD, User.field_name.BOJ_USERNAME, - 'boj', User.field_name.CREATED_AT, User.field_name.LAST_LOGIN, - 'verification_token', ] - read_only_fields = [ - 'id', - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - extra_kwargs = { - User.field_name.PASSWORD: {'write_only': True}, - User.field_name.BOJ_USERNAME: {'write_only': True}, - 'verification_token': {'write_only': True}, - } - - -class UserDetailSerializer(ModelSerializer, ReadOnlySerializerMixin): - boj = UserBojField(read_only=True) - - class Meta: - model = User - fields = [ - 'id', - User.field_name.EMAIL, - User.field_name.PROFILE_IMAGE, - User.field_name.USERNAME, - User.field_name.PASSWORD, - User.field_name.BOJ_USERNAME, - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - read_only_fields = [ - 'id', - 'boj', - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, - ] - extra_kwargs = { - User.field_name.PASSWORD: {'write_only': True}, - User.field_name.BOJ_USERNAME: {'write_only': True}, - } class UserMinimalSerializer(ModelSerializer, ReadOnlySerializerMixin): @@ -160,13 +77,3 @@ class Meta: User.field_name.PROFILE_IMAGE: {'read_only': True}, User.field_name.USERNAME: {'read_only': True}, } - - -class UserEmailSerializer(Serializer): - email = EmailField() - - -class EmailVerificationCodeSerializer(Serializer, ReadOnlySerializerMixin): - email = EmailField(write_only=True) - code = CharField(write_only=True) - token = CharField(read_only=True) diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py index e0a3720..747e1a2 100644 --- a/app/users/services/__init__.py +++ b/app/users/services/__init__.py @@ -1,8 +1,12 @@ -from users.services.authentication import AuthenticationService -from users.services.verification import VerificationService +from users.services.authentication import sign_in, sign_up, sign_out +from users.services.verification import send_verification_code, get_verification_token, verify_token __all__ = ( - 'AuthenticationService', - 'VerificationService', + 'sign_in', + 'sign_up', + 'sign_out', + 'send_verification_code', + 'get_verification_token', + 'verify_token', ) diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py index 487c1c3..1b5687f 100644 --- a/app/users/services/authentication.py +++ b/app/users/services/authentication.py @@ -9,38 +9,34 @@ from users.models import User, UserEmailVerification, UserManager -class AuthenticationService: - @staticmethod - def sign_up(email: str, username: str, password: str, **extra_fields) -> User: - """회원가입 - - 이메일 인증 토큰이 필요합니다. - 인증에 실패할 경우 ValidationError를 발생시킵니다.""" - user_manager: UserManager = User.objects - verification_token = extra_fields.pop('verification_token', None) - # 이메일 주소 인증 - if not UserEmailVerification.objects.filter(**{ - UserEmailVerification.field_name.EMAIL: email, - UserEmailVerification.field_name.VERIFICATION_TOKEN: verification_token - }).exists(): - raise ValidationError('Email is not verified.') - return user_manager.create_user(email, username, password, **extra_fields) - - @staticmethod - def sign_in(request: Request, email: str, password: str) -> User: - """로그인 - - 사용자 인증에 실패할 경우 AuthenticationFailed를 발생시킵니다.""" - # 사용자 인증 - user = authenticate(request, username=email, password=password) - # 사용자 인증 실패 시 예외 발생 - if user is None: - raise AuthenticationFailed('Invalid email or password') - # 사용자 인증 성공 시 (세션) 로그인 - login(request, user) - return user - - @staticmethod - def sign_out(request: Request): - """로그아웃""" - logout(request) +def sign_up(email: str, username: str, password: str, **extra_fields) -> User: + """회원가입 + + 이메일 인증 토큰이 필요합니다. + 인증에 실패할 경우 ValidationError를 발생시킵니다.""" + user_manager: UserManager = User.objects + verification_token = extra_fields.pop('verification_token', None) + # 이메일 주소 인증 + if not UserEmailVerification.objects.filter(**{ + UserEmailVerification.field_name.EMAIL: email, + UserEmailVerification.field_name.VERIFICATION_TOKEN: verification_token + }).exists(): + raise ValidationError('Email is not verified.') + return user_manager.create_user(email, username, password, **extra_fields) + +def sign_in(request: Request, email: str, password: str) -> User: + """로그인 + + 사용자 인증에 실패할 경우 AuthenticationFailed를 발생시킵니다.""" + # 사용자 인증 + user = authenticate(request, username=email, password=password) + # 사용자 인증 실패 시 예외 발생 + if user is None: + raise AuthenticationFailed('Invalid email or password') + # 사용자 인증 성공 시 (세션) 로그인 + login(request, user) + return user + +def sign_out(request: Request): + """로그아웃""" + logout(request) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index 7ea8edb..ad9959a 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -18,86 +18,98 @@ from users.models import User, UserEmailVerification -class VerificationService: - @staticmethod - def is_verified(email: str) -> bool: - return User.objects.filter(**{ - User.field_name.EMAIL: email, - }).exists() - - @staticmethod - def get_verification_code(email: str) -> str: - if has_verification_code(email): - if not (obj := get_verification_object(email)).is_expired(): - return obj.verification_code - else: - obj.delete() - verification_code = generate_verification_code() - create_verification_object(email, verification_code) - return verification_code - - @staticmethod - def send_verification_code(email: str, verification_code: str) -> str: - send_mail( - subject='[Time Limit Exceeded] 이메일 주소 인증 코드', - message=f'인증 코드: {verification_code}', - from_email=None, - recipient_list=[email], - fail_silently=False, - ) - - @staticmethod - def get_verification_token(email: str, verification_code: str) -> str: - VerificationService.validate_verification_code( - email, verification_code) - verification_token = generate_verification_token() - obj = get_verification_object(email) - obj.verification_token = verification_token - obj.save() - return verification_token - - @staticmethod - def validate_verification_code(email: str, verification_code: str) -> None: - if not has_verification_code(email): - raise ValidationError('Verification code does not exist.') - obj = get_verification_object(email) - if obj.is_expired(): - raise ValidationError('Verification code is expired.') - if obj.verification_code != verification_code: - raise ValidationError('Verification code is invalid.') - - @staticmethod - def validate_verification_token(email: str, verification_token: str) -> None: - if not has_verification_code(email): - raise ValidationError('Verification token does not exist.') - obj = get_verification_object(email) - if obj.verification_token != verification_token: - raise ValidationError('Verification token is invalid.') - - -def has_verification_code(email: str) -> bool: +def send_verification_code(email: str) -> None: + if _is_verified(email): + raise ValidationError('Email is already verified.') + code = _get_verification_code(email) + _send_verification_code(email, code) + + +def get_verification_token(email: str, verification_code: str) -> str: + _validate_verification_code(email, verification_code) + return _get_verification_token(email, verification_code) + + +def verify_token(email: str, verification_token: str) -> None: + _validate_verification_token(email, verification_token) + + +def _is_verified(email: str) -> bool: + return User.objects.filter(**{ + User.field_name.EMAIL: email, + }).exists() + + +def _get_verification_code(email: str) -> str: + if _has_verification_code(email): + if not (obj := _get_verification_object(email)).is_expired(): + return obj.verification_code + else: + obj.delete() + verification_code = _generate_verification_code() + _create_verification_object(email, verification_code) + return verification_code + + +def _send_verification_code(email: str, verification_code: str) -> str: + send_mail( + subject='[Time Limit Exceeded] 이메일 주소 인증 코드', + message=f'인증 코드: {verification_code}', + from_email=None, + recipient_list=[email], + fail_silently=False, + ) + + +def _get_verification_token(email: str) -> str: + verification_token = _generate_verification_token() + obj = _get_verification_object(email) + obj.verification_token = verification_token + obj.save() + return verification_token + + +def _validate_verification_code(email: str, verification_code: str) -> None: + if not _has_verification_code(email): + raise ValidationError('Verification code does not exist.') + obj = _get_verification_object(email) + if obj.is_expired(): + raise ValidationError('Verification code is expired.') + if obj.verification_code != verification_code: + raise ValidationError('Verification code is invalid.') + + +def _validate_verification_token(email: str, verification_token: str) -> None: + if not _has_verification_code(email): + raise ValidationError('Verification token does not exist.') + obj = _get_verification_object(email) + if obj.verification_token != verification_token: + raise ValidationError('Verification token is invalid.') + + +def _has_verification_code(email: str) -> bool: return UserEmailVerification.objects.filter(**{ UserEmailVerification.field_name.EMAIL: email }).exists() -def create_verification_object(email: str, verification_code: str) -> UserEmailVerification: +def _create_verification_object(email: str, verification_code: str) -> UserEmailVerification: return UserEmailVerification.objects.create(**{ UserEmailVerification.field_name.EMAIL: email, UserEmailVerification.field_name.VERIFICATION_CODE: verification_code }) -def get_verification_object(email: str) -> UserEmailVerification: +def _get_verification_object(email: str) -> UserEmailVerification: return UserEmailVerification.objects.get(**{ UserEmailVerification.field_name.EMAIL: email }) -def generate_verification_code(length: int = 6) -> str: +def _generate_verification_code(length: int = 6) -> str: return ''.join(chr(randint(ord('A'), ord('Z'))) for _ in range(length)) -def generate_verification_token() -> str: - seed = generate_verification_code() # TODO: Use better seed +def _generate_verification_token() -> str: + seed = _generate_verification_code() # TODO: Use better seed return sha256(seed.encode()).hexdigest() diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 2b45dac..df4c285 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -1,6 +1,5 @@ from typing import Callable -from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema from rest_framework import generics, mixins, permissions, status from rest_framework.exceptions import ValidationError @@ -9,10 +8,8 @@ from rest_framework.serializers import Serializer from rest_framework.views import APIView +from users import serializers, services from users.models import User -from users import serializers -from users.serializers import * -from users.services import * __all__ = ( @@ -35,7 +32,7 @@ class SignInAPIView(mixins.RetrieveModelMixin, def get_object(self) -> User: serializer = self.get_serializer(data=self.request.data) serializer.is_valid(raise_exception=True) - return AuthenticationService.sign_in( + return services.sign_in( request=self.request, email=serializer.validated_data['email'], password=serializer.validated_data['password'], @@ -63,8 +60,8 @@ class SignUpAPIView(generics.CreateAPIView): }) def perform_create(self, serializer: Serializer): token = serializer.validated_data.pop('verification_token') - VerificationService.validate_verification_token(token) - user = AuthenticationService.sign_up(**serializer.validated_data) + services.verify_token(token) + user = services.sign_up(**serializer.validated_data) serializer.instance = user @@ -77,7 +74,7 @@ class SignOutAPIView(APIView): status.HTTP_204_NO_CONTENT: '로그아웃 성공', }) def get(self, request, *args, **kwargs): - AuthenticationService.sign_out(request) + services.sign_out(request) return Response(status=status.HTTP_204_NO_CONTENT) @@ -91,7 +88,7 @@ def get_object(self) -> User: return self.request.user -class EmailVerificationCodeAPIView(generics.GenericAPIView): +class EmailCodeAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" permission_classes = [permissions.AllowAny] @@ -99,32 +96,34 @@ class EmailVerificationCodeAPIView(generics.GenericAPIView): get_serializer: Callable[..., Serializer] @swagger_auto_schema(responses={ - status.HTTP_201_CREATED: '회원가입 성공', + status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', }) def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] - if VerificationService.is_verified(email): - raise ValidationError('Email is already verified.') - code = VerificationService.get_verification_code(email) - VerificationService.send_verification_code(email, code) + services.send_verification_code(email) return Response(serializer.data, status=status.HTTP_201_CREATED) -class EmailVerificationTokenAPIView(generics.GenericAPIView): +class EmailTokenAPIView(generics.GenericAPIView): """이메일 인증 토큰 발급 API""" permission_classes = [permissions.AllowAny] - serializer_class = serializers.EmailVerificationCodeSerializer + serializer_class = serializers.EmailCodeSerializer get_serializer: Callable[..., Serializer] + @swagger_auto_schema(responses={ + status.HTTP_201_CREATED: '이메일 토큰 발급 성공', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일, 잘못된 인증 코드 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + }) def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] code = serializer.validated_data['code'] - token = VerificationService.get_verification_token(email, code) - serializer.validated_data['token'] = token - return Response(serializer.data, status=status.HTTP_200_OK) + return Response(data={ + 'email': email, + 'token': services.get_verification_token(email, code), + }, status=status.HTTP_201_CREATED) From 65bda3f2edff60f242aeb691b5ab8ebd4ebec3ae Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 19:51:06 +0900 Subject: [PATCH 333/552] =?UTF-8?q?feat(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=EC=9D=B4=20=EC=82=AC=EC=9A=A9=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=EB=A7=8C=20=EA=B2=80=EC=82=AC=ED=95=98=EB=8A=94=20API?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 4 +-- app/users/services/__init__.py | 14 ++++++-- app/users/services/verification.py | 4 +++ app/users/views/__init__.py | 51 ++++++++++++++++++++---------- 4 files changed, 52 insertions(+), 21 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index c402808..669f084 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -29,8 +29,8 @@ path("auth/signin", users.views.SignInAPIView.as_view()), path("auth/signup", users.views.SignUpAPIView.as_view()), path("auth/signout", users.views.SignOutAPIView.as_view()), - path("auth/verification/code", users.views.EmailCodeAPIView.as_view()), - path("auth/verification/token", users.views.EmailTokenAPIView.as_view()), + path("auth/email/check", users.views.EmailCheckAPIView.as_view()), + path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), path("crews/", crews.views.CrewCreate.as_view()), path("crews/recruiting", crews.views.CrewRecruiting.as_view()), path("crews/my", crews.views.CrewJoined.as_view()), diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py index 747e1a2..709794c 100644 --- a/app/users/services/__init__.py +++ b/app/users/services/__init__.py @@ -1,5 +1,14 @@ -from users.services.authentication import sign_in, sign_up, sign_out -from users.services.verification import send_verification_code, get_verification_token, verify_token +from users.services.authentication import ( + sign_in, + sign_up, + sign_out, +) +from users.services.verification import ( + send_verification_code, + get_verification_token, + verify_token, + is_usable, +) __all__ = ( @@ -9,4 +18,5 @@ 'send_verification_code', 'get_verification_token', 'verify_token', + 'is_usable', ) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index ad9959a..1e51969 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -18,6 +18,10 @@ from users.models import User, UserEmailVerification +def is_usable(email: str) -> bool: + return _is_verified(email) + + def send_verification_code(email: str) -> None: if _is_verified(email): raise ValidationError('Email is already verified.') diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index df4c285..5e8c6c4 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -1,8 +1,7 @@ from typing import Callable from drf_yasg.utils import swagger_auto_schema -from rest_framework import generics, mixins, permissions, status -from rest_framework.exceptions import ValidationError +from rest_framework import generics, mixins, permissions, status, throttling from rest_framework.request import Request from rest_framework.response import Response from rest_framework.serializers import Serializer @@ -88,35 +87,53 @@ def get_object(self) -> User: return self.request.user -class EmailCodeAPIView(generics.GenericAPIView): - """이메일 인증 코드 전송 API""" - +class EmailCheckAPIView(generics.GenericAPIView): + """이메일이 사용가능한지 검사 API""" permission_classes = [permissions.AllowAny] serializer_class = serializers.EmailSerializer get_serializer: Callable[..., Serializer] @swagger_auto_schema(responses={ - status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + status.HTTP_200_OK: '검사를 수행했을 경우, 사용가능 여부를 Boolean으로 반환함.', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 형식.', }) - def post(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] - services.send_verification_code(email) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(data={ + "email": email, + "is_usable": services.is_usable(email), + }, status=status.HTTP_200_OK) -class EmailTokenAPIView(generics.GenericAPIView): - """이메일 인증 토큰 발급 API""" +class EmailVerifyAPIView(generics.GenericAPIView): + """이메일 인증 코드 전송 API""" permission_classes = [permissions.AllowAny] - serializer_class = serializers.EmailCodeSerializer get_serializer: Callable[..., Serializer] + def get_serializer_class(self): + if self.request.method == 'GET': + return serializers.EmailSerializer + if self.request.method == 'POST': + return serializers.EmailCodeSerializer + raise ValueError + + @swagger_auto_schema(responses={ + status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + }) + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + email = serializer.validated_data['email'] + services.send_verification_code(email) + return Response(serializer.data, status=status.HTTP_201_CREATED) + @swagger_auto_schema(responses={ - status.HTTP_201_CREATED: '이메일 토큰 발급 성공', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일, 잘못된 인증 코드 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + status.HTTP_200_OK: '이메일 인증 성공. 새로운 이메일 인증 토큰을 발급받는다. 이 때 받은 토큰은 회원 가입시 이메일 소유 증명을 위해 제출해야한다.', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 올바르지 않은 인증번호.', }) def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -126,4 +143,4 @@ def post(self, request: Request, *args, **kwargs): return Response(data={ 'email': email, 'token': services.get_verification_token(email, code), - }, status=status.HTTP_201_CREATED) + }, status=status.HTTP_200_OK) From 8a3980b0033acb55a8dacf4561a91dc0224548b4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 19:57:14 +0900 Subject: [PATCH 334/552] =?UTF-8?q?refactor:=20import=EB=AC=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index 220142f..72bd5ca 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -1,8 +1,6 @@ from rest_framework import serializers -from rest_framework.serializers import * from users.models import User -from users.serializers.fields import UserBojField from users.serializers.mixins import ReadOnlySerializerMixin @@ -17,26 +15,26 @@ class EmailSerializer(serializers.Serializer): - email = EmailField() + email = serializers.EmailField() class EmailCodeSerializer(serializers.Serializer): - email = EmailField() - code = CharField() + email = serializers.EmailField() + code = serializers.CharField() class EmailTokenSerializer(serializers.Serializer): - email = EmailField() - token = CharField() + email = serializers.EmailField() + token = serializers.CharField() class SignInSerializer(serializers.Serializer): - email = EmailField() - password = CharField() + email = serializers.EmailField() + password = serializers.CharField() class SignUpSerializer(serializers.ModelSerializer): - verification_token = CharField() + verification_token = serializers.CharField() class Meta: model = User @@ -64,7 +62,7 @@ class Meta: ] -class UserMinimalSerializer(ModelSerializer, ReadOnlySerializerMixin): +class UserMinimalSerializer(serializers.ModelSerializer, ReadOnlySerializerMixin): class Meta: model = User fields = [ From 0d94c44ae142a731e8e12cd177c34c1992ab7c18 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 20:02:22 +0900 Subject: [PATCH 335/552] =?UTF-8?q?refactor(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=EC=9D=B4=20=EC=82=AC=EC=9A=A9=EC=A4=91=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=EB=A7=8C=20=EA=B2=80=EC=82=AC=ED=95=98=EB=8A=94=20API?= =?UTF-8?q?=20Swagger=20=EB=AA=85=EC=84=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 5e8c6c4..b331a62 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -110,6 +110,7 @@ def get(self, request: Request, *args, **kwargs): class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" + pagination_class = None permission_classes = [permissions.AllowAny] get_serializer: Callable[..., Serializer] @@ -120,10 +121,14 @@ def get_serializer_class(self): return serializers.EmailCodeSerializer raise ValueError - @swagger_auto_schema(responses={ - status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', - }) + @swagger_auto_schema( + operation_description="이메일을 입력하면, 입력된 이메일 주소로 인증 코드를 발송합니다. 해당 코드를 동일한 주소의 POST 요청으로 전달하면 회원가입시 사용할 수 있는 인증 토큰을 반환합니다.", + query_serializer=serializers.EmailSerializer, + responses={ + status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', + }, + ) def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) From 0da1e81d3b4c0dc5f31e3f437ffcaa395ff68b3b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 20:22:17 +0900 Subject: [PATCH 336/552] =?UTF-8?q?feat(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=EC=9D=84=20=EC=82=AC=EC=9A=A9=ED=95=A0=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8A=94=EC=A7=80=20=EA=B2=80=EC=82=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 5 ----- app/config/urls.py | 1 + app/users/serializers/__init__.py | 4 ++++ app/users/services/__init__.py | 6 ++++-- app/users/services/verification.py | 8 ++++++- app/users/views/__init__.py | 34 ++++++++++++++++++++++++------ 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index d64f3e1..f872ce2 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -159,9 +159,4 @@ DEFAULT_EXCEPTION_REPORTER = "config.reporters.NACLExceptionReporter" -REST_FRAMEWORK = { - 'PAGE_SIZE': 10, - 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', -} - APPEND_SLASH = False diff --git a/app/config/urls.py b/app/config/urls.py index 669f084..1a3cbea 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -29,6 +29,7 @@ path("auth/signin", users.views.SignInAPIView.as_view()), path("auth/signup", users.views.SignUpAPIView.as_view()), path("auth/signout", users.views.SignOutAPIView.as_view()), + path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), path("auth/email/check", users.views.EmailCheckAPIView.as_view()), path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), path("crews/", crews.views.CrewCreate.as_view()), diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index 72bd5ca..02384da 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -48,6 +48,10 @@ class Meta: ] +class UsernameSerializer(serializers.Serializer): + username = serializers.CharField() + + class UserSerializer(serializers.ModelSerializer): class Meta: model = User diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py index 709794c..8caab0a 100644 --- a/app/users/services/__init__.py +++ b/app/users/services/__init__.py @@ -7,7 +7,8 @@ send_verification_code, get_verification_token, verify_token, - is_usable, + is_email_usable, + is_username_usable, ) @@ -18,5 +19,6 @@ 'send_verification_code', 'get_verification_token', 'verify_token', - 'is_usable', + 'is_email_usable', + 'is_username_usable', ) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index 1e51969..4783516 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -18,10 +18,16 @@ from users.models import User, UserEmailVerification -def is_usable(email: str) -> bool: +def is_email_usable(email: str) -> bool: return _is_verified(email) +def is_username_usable(username: str) -> bool: + return User.objects.filter(**{ + User.field_name.USERNAME: username, + }).exists() + + def send_verification_code(email: str) -> None: if _is_verified(email): raise ValidationError('Email is already verified.') diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index b331a62..c194f73 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -87,8 +87,29 @@ def get_object(self) -> User: return self.request.user +class UsernameCheckAPIView(generics.GenericAPIView): + """이메일이 사용가능한지 검사 API""" + permission_classes = [permissions.AllowAny] + serializer_class = serializers.UsernameSerializer + get_serializer: Callable[..., Serializer] + + @swagger_auto_schema(responses={ + status.HTTP_200_OK: '사용자명이 사용가능한지 검사에 성공.', + status.HTTP_400_BAD_REQUEST: '잘못된 데이터 형식을 입력했을 경우.', + }) + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + username = serializer.validated_data['username'] + return Response(data={ + "username": username, + "is_usable": services.is_email_usable(username), + }, status=status.HTTP_200_OK) + + class EmailCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" + permission_classes = [permissions.AllowAny] serializer_class = serializers.EmailSerializer get_serializer: Callable[..., Serializer] @@ -103,14 +124,13 @@ def get(self, request: Request, *args, **kwargs): email = serializer.validated_data['email'] return Response(data={ "email": email, - "is_usable": services.is_usable(email), + "is_usable": services.is_email_usable(email), }, status=status.HTTP_200_OK) class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" - pagination_class = None permission_classes = [permissions.AllowAny] get_serializer: Callable[..., Serializer] @@ -136,10 +156,12 @@ def get(self, request: Request, *args, **kwargs): services.send_verification_code(email) return Response(serializer.data, status=status.HTTP_201_CREATED) - @swagger_auto_schema(responses={ - status.HTTP_200_OK: '이메일 인증 성공. 새로운 이메일 인증 토큰을 발급받는다. 이 때 받은 토큰은 회원 가입시 이메일 소유 증명을 위해 제출해야한다.', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 올바르지 않은 인증번호.', - }) + @swagger_auto_schema( + responses={ + status.HTTP_200_OK: '이메일 인증 성공. 새로운 이메일 인증 토큰을 발급받는다. 이 때 받은 토큰은 회원 가입시 이메일 소유 증명을 위해 제출해야한다.', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 올바르지 않은 인증번호.', + }, + ) def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) From 4129b6a89296240b4024616983c82b1fa8ac204a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 20:33:43 +0900 Subject: [PATCH 337/552] =?UTF-8?q?fix(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC/=EC=82=AC=EC=9A=A9=EC=9E=90=EB=AA=85=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EA=B0=80=EB=8A=A5=20=EC=97=AC=EB=B6=80=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=EB=8C=80=EB=A1=9C=20=EB=8B=AC=EC=95=84=EB=91=94=20?= =?UTF-8?q?=EA=B2=83=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/services/verification.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index 4783516..8144807 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -19,11 +19,13 @@ def is_email_usable(email: str) -> bool: - return _is_verified(email) + return not User.objects.filter(**{ + User.field_name.EMAIL: email, + }).exists() def is_username_usable(username: str) -> bool: - return User.objects.filter(**{ + return not User.objects.filter(**{ User.field_name.USERNAME: username, }).exists() From 23b05d9ca35f1c1cc22f83609e4394e080ad8bf1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 20:34:12 +0900 Subject: [PATCH 338/552] =?UTF-8?q?feat(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=EB=B2=88=ED=98=B8=20=EB=B0=9C?= =?UTF-8?q?=EC=86=A1=EC=97=90=20=EB=8C=80=ED=95=B4=20throttling=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20(=EB=B6=84=EB=8B=B9=201=ED=9A=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index c194f73..e6bf895 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -128,9 +128,15 @@ def get(self, request: Request, *args, **kwargs): }, status=status.HTTP_200_OK) + +class EmailVerifyThrottle(throttling.AnonRateThrottle): + THROTTLE_RATES = '1/min' + + class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" + throttle_classes = [EmailVerifyThrottle] permission_classes = [permissions.AllowAny] get_serializer: Callable[..., Serializer] From b54587376674553c5329d0b3fda3d47fc221b4be Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 7 Aug 2024 21:00:05 +0900 Subject: [PATCH 339/552] =?UTF-8?q?feat(users):=20UserEmailVerification=20?= =?UTF-8?q?=EC=9D=84=20=EA=B4=80=EB=A6=AC=EC=9E=90=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=EC=97=90=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/admin.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/users/admin.py b/app/users/admin.py index da02934..32632bb 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -1,10 +1,15 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from users.models import User +from users.models import User, UserEmailVerification from crews.models import CrewMember +admin.site.register([ + UserEmailVerification, +]) + + @admin.register(User) class UserAdmin(BaseUserAdmin): fieldsets = None From 7c8df8739b4ac3d519661ec60adbd467acb33e64 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 00:53:45 +0900 Subject: [PATCH 340/552] =?UTF-8?q?feat(submissions):=20=EB=AA=A9=EC=97=85?= =?UTF-8?q?=20=EC=BD=94=EB=93=9C=EB=A6=AC=EB=B7=B0=20API=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 1 + app/submissions/serializers/__init__.py | 17 ++++++ app/submissions/views.py | 69 ++++++++++++++++++++++++- 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 app/submissions/serializers/__init__.py diff --git a/app/config/urls.py b/app/config/urls.py index 1a3cbea..3c0f884 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -40,6 +40,7 @@ path("problems/search", problems.views.ProblemSearch.as_view()), path("problems//detail", problems.views.ProblemDetail.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), + path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), path(r'swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path(r'swagger(?P\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'), diff --git a/app/submissions/serializers/__init__.py b/app/submissions/serializers/__init__.py new file mode 100644 index 0000000..f5d91f6 --- /dev/null +++ b/app/submissions/serializers/__init__.py @@ -0,0 +1,17 @@ +from rest_framework import serializers + +from submissions.models import Submission + + +class SubmissionSerializer(serializers.ModelField): + class Meta: + model = Submission + fields = [ + 'id', + Submission.field_name.USER, + Submission.field_name.CODE, + Submission.field_name.LANGUAGE, + Submission.field_name.IS_CORRECT, + Submission.field_name.IS_HELP_NEEDED, + Submission.field_name.CREATED_AT, + ] diff --git a/app/submissions/views.py b/app/submissions/views.py index 91ea44a..8cdaff5 100644 --- a/app/submissions/views.py +++ b/app/submissions/views.py @@ -1,3 +1,68 @@ -from django.shortcuts import render +from typing import Callable -# Create your views here. +from drf_yasg.utils import swagger_auto_schema +from django.utils import timezone +from rest_framework import generics, permissions, status +from rest_framework.serializers import Serializer +from rest_framework.response import Response + +from submissions import serializers + + +class CreateCodeReview(generics.RetrieveAPIView): + permission_classes = [permissions.AllowAny] + serializer_class = serializers.SubmissionSerializer + get_serializer: Callable[..., Serializer] + + lookup_field = 'id' + + @swagger_auto_schema( + responses={ + status.HTTP_200_OK: 'OK', + status.HTTP_404_NOT_FOUND: 'Not Found', + }, + ) + def get(self, request, *args, **kwargs): + return Response( + data={ + 'id': 1, + 'code': """# 이동3-2\n\n\nimport math\n\n\nMAX_K = math.ceil(math.log(1e9, 3))\n\n\nK_POW = [1]\nfor i in range(1, MAX_K):\n K_POW.append(3*K_POW[i-1])\n\n\ndef solve(x: int, y: int, k=0) -> bool:\n\n if k >= MAX_K:\n return False\n if x == 0 and y == 0:\n return True\n coords = [\n (x-K_POW[k], y),\n (x, y-K_POW[k]),\n (x+K_POW[k], y),\n (x, y+K_POW[k]),\n ]\n for x, y in coords:\n if k+1 < MAX_K and not (0 < abs(x) < K_POW[k+1] and 0 < abs(y) < K_POW[k+1]):\n if solve(x, y, k+1):\n return True\n return False\n\n\nif __name__ == "__main__":\n X, Y = map(int, input().split())\n print('1' if solve(X, Y) else '0')\n""", + 'language': 'python', + 'is_correct': False, + 'is_help_needed': True, + "created_by": { + "id": 2, + "username": "hi", + "profile_image": "https://picsum.photos/250/250", + }, + 'created_at': timezone.now(), + 'comments': { + 'count': 1, + 'items': [ + { + "id": 1, + "line_start": 1, + "line_end": 3, + "content": "이 라인이 조금 이상해요.", + "created_by": { + "id": 2, + "username": "hi", + "profile_image": "https://picsum.photos/250/250", + }, + }, + { + "id": 2, + "line_start": 2, + "line_end": 3, + "content": "ㄹㅇ.", + "created_by": { + "id": 3, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + }, + ] + }, + }, + status=status.HTTP_200_OK, + ) \ No newline at end of file From af55faae73354a6ca94361a5c6f000b7a2c246b9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 00:54:19 +0900 Subject: [PATCH 341/552] =?UTF-8?q?feat(config):=20swagger=20api=20?= =?UTF-8?q?=EA=B3=B5=EA=B0=9C=20=EB=B2=94=EC=9C=84=EB=A5=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC=EC=9E=90->=EB=AA=A8=EB=91=90=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/config/urls.py b/app/config/urls.py index 3c0f884..72db6ec 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -9,6 +9,7 @@ import crews.views import problems.views import users.views +import submissions.views schema_view = get_schema_view( @@ -19,7 +20,7 @@ contact=openapi.Contact(email="202115064@sangmyung.kr"), ), public=True, - permission_classes=[permissions.IsAdminUser], + permission_classes=[permissions.AllowAny], ) From 127558b14382113030e551dc1ce8ed91d8ade9c4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 00:55:09 +0900 Subject: [PATCH 342/552] =?UTF-8?q?feat(user):=20UsernameCheckAPI=20?= =?UTF-8?q?=EC=8A=A4=EC=9B=A8=EA=B1=B0=20=EC=A7=80=EC=9B=90=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index e6bf895..8f6768c 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -93,10 +93,13 @@ class UsernameCheckAPIView(generics.GenericAPIView): serializer_class = serializers.UsernameSerializer get_serializer: Callable[..., Serializer] - @swagger_auto_schema(responses={ - status.HTTP_200_OK: '사용자명이 사용가능한지 검사에 성공.', - status.HTTP_400_BAD_REQUEST: '잘못된 데이터 형식을 입력했을 경우.', - }) + @swagger_auto_schema( + query_serializer=serializers.UsernameSerializer, + responses={ + status.HTTP_200_OK: '사용자명이 사용가능한지 검사에 성공.', + status.HTTP_400_BAD_REQUEST: '잘못된 데이터 형식을 입력했을 경우.', + }, + ) def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) From c01651cffbde561079691c1ed945e6672f2a41da Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 01:03:14 +0900 Subject: [PATCH 343/552] =?UTF-8?q?fix(users.services):=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=ED=95=A8=EC=88=98=20=ED=98=B8=EC=B6=9C=20?= =?UTF-8?q?=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/services/verification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/services/verification.py b/app/users/services/verification.py index 8144807..fe0a7da 100644 --- a/app/users/services/verification.py +++ b/app/users/services/verification.py @@ -39,7 +39,7 @@ def send_verification_code(email: str) -> None: def get_verification_token(email: str, verification_code: str) -> str: _validate_verification_code(email, verification_code) - return _get_verification_token(email, verification_code) + return _get_verification_token(email) def verify_token(email: str, verification_token: str) -> None: From 92197f9a49305cb0eb8dc84ffa222cf52212949a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 01:03:53 +0900 Subject: [PATCH 344/552] =?UTF-8?q?fix(users.views):=20EmailVerifyAPI?= =?UTF-8?q?=EC=9D=98=20Throttling=20=EB=B9=84=ED=99=9C=EC=84=B1=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 8f6768c..c41076a 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -139,7 +139,7 @@ class EmailVerifyThrottle(throttling.AnonRateThrottle): class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" - throttle_classes = [EmailVerifyThrottle] + throttle_classes = [] permission_classes = [permissions.AllowAny] get_serializer: Callable[..., Serializer] From 11dd585ee3b8764a8dbd6aef7038d1e06b448e91 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 01:50:08 +0900 Subject: [PATCH 345/552] =?UTF-8?q?feat(crews.views):=20=EB=8C=80=EC=89=AC?= =?UTF-8?q?=EB=B3=B4=EB=93=9C=20=EB=AA=A9=EC=97=85=20API=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 1 + app/crews/serializers/__init__.py | 12 + app/crews/views/__init__.py | 415 ++++++++++++++++++++++++++++++ 3 files changed, 428 insertions(+) diff --git a/app/config/urls.py b/app/config/urls.py index 72db6ec..2fd2f36 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -37,6 +37,7 @@ path("crews/recruiting", crews.views.CrewRecruiting.as_view()), path("crews/my", crews.views.CrewJoined.as_view()), path("crews//detail", crews.views.CrewDetail.as_view()), + path("crews//dashboard", crews.views.CrewDashboard.as_view()), path("problems/", problems.views.ProblemCreate.as_view()), path("problems/search", problems.views.ProblemSearch.as_view()), path("problems//detail", problems.views.ProblemDetail.as_view()), diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index cb2317a..007f0e6 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -1,4 +1,5 @@ from django.db.transaction import atomic +from rest_framework import serializers from rest_framework.serializers import ( ModelSerializer, MultipleChoiceField, @@ -117,3 +118,14 @@ class Meta: 'activities', ] read_only_fields = ['__all__'] + + +class CrewDashboardSerializer(serializers.Serializer): + id = serializers.CharField() + icon = serializers.CharField() + name = serializers.CharField() + activity = serializers.CharField() + members = serializers.CharField() + member_submissions = serializers.CharField() + tags = serializers.CharField() + statistics = serializers.CharField() diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 46fd440..4d6fe40 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,8 +1,14 @@ +from drf_yasg.utils import swagger_auto_schema +from django.utils import timezone +from rest_framework import generics from rest_framework import mixins from rest_framework import permissions +from rest_framework import status from rest_framework.generics import GenericAPIView +from rest_framework.response import Response from crews.models import Crew, CrewMember +from crews import serializers from crews.serializers import ( CrewDetailSerializer, CrewRecruitingSerializer, @@ -77,3 +83,412 @@ def get_queryset(self): def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) + + +class CrewDashboard(generics.RetrieveAPIView): + """가입한 크루 목록 조회 API""" + + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewDashboardSerializer + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + return Response( + data={ + 'id': 1, + 'icon': '😀', + 'name': '코딩메리호', + 'activity': { + 'recent': { + 'id': 1, + 'name': '9회차', + 'problems': { + 'count': 8, + 'items': [ + { + 'problem_number': 1, + 'is_solved': False, + }, + { + 'problem_number': 2, + 'is_solved': False, + }, + { + 'problem_number': 3, + 'is_solved': True, + }, + { + 'problem_number': 4, + 'is_solved': True, + }, + { + 'problem_number': 5, + 'is_solved': True, + }, + { + 'problem_number': 6, + 'is_solved': True, + }, + { + 'problem_number': 7, + 'is_solved': True, + }, + { + 'problem_number': 8, + 'is_solved': True, + }, + ], + }, + }, + }, + 'members': { + 'count': 3, + 'max_count': 8, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + "is_captain": True, + }, + { + "id": 3, + "username": "hello", + "profile_image": "https://picsum.photos/250/250", + "is_captain": False, + }, + { + "id": 2, + "username": "hi", + "profile_image": "https://picsum.photos/250/250", + "is_captain": False, + }, + ], + }, + 'member_submissions': { + 'count': 2, + 'items': [ + { + 'user_id': 1, + "username": "hey", + "submissions": { + 'count': 7, + 'items': [ + { + 'problem_number': 1, + 'submission_id': 1, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 2, + 'submission_id': 2, + 'is_correct': False, + 'is_help_needed': True, + }, + { + 'problem_number': 4, + 'submission_id': 3, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 5, + 'submission_id': 4, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 6, + 'submission_id': 5, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 7, + 'submission_id': 6, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 8, + 'submission_id': 7, + 'is_correct': True, + 'is_help_needed': False, + }, + ], + }, + }, + { + 'user_id': 2, + "username": "leeyuuuuuuum", + "submissions": { + 'count': 6, + 'items': [ + { + 'problem_number': 1, + 'submission_id': 8, + 'is_correct': False, + 'is_help_needed': True, + }, + { + 'problem_number': 2, + 'submission_id': 9, + 'is_correct': True, + 'is_help_needed': True, + }, + { + 'problem_number': 4, + 'submission_id': 10, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 5, + 'submission_id': 11, + 'is_correct': False, + 'is_help_needed': False, + }, + { + 'problem_number': 7, + 'submission_id': 12, + 'is_correct': True, + 'is_help_needed': False, + }, + { + 'problem_number': 8, + 'submission_id': 13, + 'is_correct': True, + 'is_help_needed': False, + }, + ], + }, + } + ], + }, + 'tags': { + 'count': 3, + 'items': [ + { + 'key': 'python', + 'name': 'Python', + 'type': 'language', + }, + { + 'key': None, + 'name': '실버 이상', + 'type': 'level', + }, + { + 'key': None, + 'name': '우하하', + 'type': 'custom', + }, + ], + }, + 'statistics': { + 'difficulty': [ + { + 'difficulty': 1, + 'problem_count': 3, + 'ratio': .375, + }, + { + 'difficulty': 2, + 'problem_count': 3, + 'ratio': .375, + }, + { + 'difficulty': 3, + 'problem_count': 2, + 'ratio': .25, + } + ], + 'tags': [ + { + 'label': { + 'en': 'mathematics', + 'ko': '수학' + }, + 'problem_count': 20, + 'ratio': .392, + }, + { + 'label': { + 'en': 'implementation', + 'ko': '구현' + }, + 'problem_count': 20, + 'ratio': .392, + }, + { + 'label': { + 'en': 'graph', + 'ko': '그래프 이론' + }, + 'problem_count': 5, + 'ratio': .098, + }, + { + 'label': { + 'en': 'dynamic programming', + 'ko': '다이나믹 프로그래밍' + }, + 'problem_count': 4, + 'ratio': .078, + }, + { + 'label': { + 'en': 'data structures', + 'ko': '자료구조' + }, + 'problem_count': 4, + 'ratio': .078, + }, + ], + }, + 'reviews': { + 'count': 2, + 'items': [ + { + 'problem_number': 1, + 'problem_title': 'A+B', + 'submission_id': 1, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 3, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + { + "id": 3, + "username": "hello", + "profile_image": "https://picsum.photos/250/250", + }, + { + "id": 2, + "username": "hi", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 2, + 'problem_title': 'C+D', + 'submission_id': 2, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 3, + 'problem_title': '임시제목', + 'submission_id': 3, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 4, + 'problem_title': '임시제목', + 'submission_id': 4, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 5, + 'problem_title': '임시제목', + 'submission_id': 5, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 6, + 'problem_title': '임시제목', + 'submission_id': 6, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 7, + 'problem_title': '임시제목', + 'submission_id': 7, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + { + 'problem_number': 8, + 'problem_title': '임시제목', + 'submission_id': 8, + 'submission_created_at': timezone.now(), + 'reviewers': { + 'count': 1, + 'items': [ + { + "id": 4, + "username": "hey", + "profile_image": "https://picsum.photos/250/250", + }, + ], + }, + }, + ], + }, + }, + ) \ No newline at end of file From f72a13403af12e1b168e4f766fcc0e21628b3f9c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 10 Aug 2024 01:56:45 +0900 Subject: [PATCH 346/552] =?UTF-8?q?fix(users.views):=20SignUpAPI=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=EB=90=9C=20=EA=B2=83=EC=9D=84=20=EA=B3=A0?= =?UTF-8?q?=EC=B9=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index c41076a..88df5e3 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -58,8 +58,9 @@ class SignUpAPIView(generics.CreateAPIView): status.HTTP_400_BAD_REQUEST: '잘못 입력한 값이 존재', }) def perform_create(self, serializer: Serializer): + email = serializer.validated_data['email'] token = serializer.validated_data.pop('verification_token') - services.verify_token(token) + services.verify_token(email, token) user = services.sign_up(**serializer.validated_data) serializer.instance = user From f7b8c5b186d170f18d230da46152078f8f16a475 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 13:47:29 +0900 Subject: [PATCH 347/552] =?UTF-8?q?feat(users):=20JWT=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 17 +++++++++++++++++ app/users/services/__init__.py | 2 ++ app/users/services/authentication.py | 6 ++++++ app/users/views/__init__.py | 23 +++++++++++------------ requirements.txt | 1 + 5 files changed, 37 insertions(+), 12 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index f872ce2..b58e0b0 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -10,6 +10,7 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ """ +from datetime import timedelta from pathlib import Path @@ -47,9 +48,12 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", + "corsheaders", "drf_yasg", "rest_framework", + 'rest_framework_simplejwt', + "users", "problems", "crews", @@ -122,6 +126,19 @@ 'users.backends.UserAuthBackend', ] +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=60), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_MAX_LIFETIME': timedelta(days=200), +} + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + ), +} + # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py index 8caab0a..c32dca9 100644 --- a/app/users/services/__init__.py +++ b/app/users/services/__init__.py @@ -2,6 +2,7 @@ sign_in, sign_up, sign_out, + get_user_jwt, ) from users.services.verification import ( send_verification_code, @@ -16,6 +17,7 @@ 'sign_in', 'sign_up', 'sign_out', + 'get_user_jwt', 'send_verification_code', 'get_verification_token', 'verify_token', diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py index 1b5687f..d2adbde 100644 --- a/app/users/services/authentication.py +++ b/app/users/services/authentication.py @@ -5,6 +5,7 @@ from django.contrib.auth import authenticate, login, logout from rest_framework.exceptions import AuthenticationFailed, ValidationError from rest_framework.request import Request +from rest_framework_simplejwt.tokens import RefreshToken from users.models import User, UserEmailVerification, UserManager @@ -40,3 +41,8 @@ def sign_in(request: Request, email: str, password: str) -> User: def sign_out(request: Request): """로그아웃""" logout(request) + +def get_user_jwt(user: User) -> str: + refresh_token: RefreshToken + refresh_token = RefreshToken.for_user(user) + return str(refresh_token.access_token) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 88df5e3..6bb84c3 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -28,23 +28,23 @@ class SignInAPIView(mixins.RetrieveModelMixin, serializer_class = serializers.SignInSerializer get_serializer: Callable[..., Serializer] - def get_object(self) -> User: + @swagger_auto_schema(responses={ + status.HTTP_200_OK: '로그인 성공', + status.HTTP_401_UNAUTHORIZED: '로그인 실패', + }) + def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=self.request.data) serializer.is_valid(raise_exception=True) - return services.sign_in( + user = services.sign_in( request=self.request, email=serializer.validated_data['email'], password=serializer.validated_data['password'], ) - - @swagger_auto_schema(responses={ - status.HTTP_200_OK: '로그인 성공', - status.HTTP_401_UNAUTHORIZED: '로그인 실패', - }) - def post(self, request, *args, **kwargs): - instance = self.get_object() - serializer = serializers.UserSerializer(instance) - return Response(serializer.data) + token = services.get_user_jwt(user) + return Response(data={ + **serializers.UserSerializer(user).data, + 'token': token, + }) class SignUpAPIView(generics.CreateAPIView): @@ -132,7 +132,6 @@ def get(self, request: Request, *args, **kwargs): }, status=status.HTTP_200_OK) - class EmailVerifyThrottle(throttling.AnonRateThrottle): THROTTLE_RATES = '1/min' diff --git a/requirements.txt b/requirements.txt index e85ab90..5e4e5d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ django django-cors-headers djangorestframework +djangorestframework-simplejwt Pillow drf-yasg From a92e05d73650c68edbbc1482161d174298a20fd5 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 13:57:13 +0900 Subject: [PATCH 348/552] chore: code formatting --- app/users/views/__init__.py | 46 +++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 6bb84c3..b047cf3 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -41,10 +41,13 @@ def post(self, request, *args, **kwargs): password=serializer.validated_data['password'], ) token = services.get_user_jwt(user) - return Response(data={ - **serializers.UserSerializer(user).data, - 'token': token, - }) + return Response( + data={ + **serializers.UserSerializer(user).data, + 'token': token, + }, + status=status.HTTP_200_OK, + ) class SignUpAPIView(generics.CreateAPIView): @@ -105,10 +108,13 @@ def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) username = serializer.validated_data['username'] - return Response(data={ - "username": username, - "is_usable": services.is_email_usable(username), - }, status=status.HTTP_200_OK) + return Response( + data={ + "username": username, + "is_usable": services.is_email_usable(username), + }, + status=status.HTTP_200_OK, + ) class EmailCheckAPIView(generics.GenericAPIView): @@ -126,10 +132,13 @@ def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] - return Response(data={ - "email": email, - "is_usable": services.is_email_usable(email), - }, status=status.HTTP_200_OK) + return Response( + data={ + "email": email, + "is_usable": services.is_email_usable(email), + }, + status=status.HTTP_200_OK, + ) class EmailVerifyThrottle(throttling.AnonRateThrottle): @@ -163,7 +172,7 @@ def get(self, request: Request, *args, **kwargs): serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] services.send_verification_code(email) - return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(data=serializer.data, status=status.HTTP_201_CREATED) @swagger_auto_schema( responses={ @@ -176,7 +185,10 @@ def post(self, request: Request, *args, **kwargs): serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] code = serializer.validated_data['code'] - return Response(data={ - 'email': email, - 'token': services.get_verification_token(email, code), - }, status=status.HTTP_200_OK) + return Response( + data={ + 'email': email, + 'token': services.get_verification_token(email, code), + }, + status=status.HTTP_200_OK, + ) From acf52990c4f6b2e35b835c64ca6e6878923271cc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 14:27:49 +0900 Subject: [PATCH 349/552] =?UTF-8?q?fix(users):=202=EC=A4=91=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EB=A1=9C=20=EC=9D=B8=ED=95=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=A4=ED=8C=A8=20=EC=98=A4=EB=A5=98=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/services/authentication.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py index d2adbde..45e8b18 100644 --- a/app/users/services/authentication.py +++ b/app/users/services/authentication.py @@ -16,13 +16,6 @@ def sign_up(email: str, username: str, password: str, **extra_fields) -> User: 이메일 인증 토큰이 필요합니다. 인증에 실패할 경우 ValidationError를 발생시킵니다.""" user_manager: UserManager = User.objects - verification_token = extra_fields.pop('verification_token', None) - # 이메일 주소 인증 - if not UserEmailVerification.objects.filter(**{ - UserEmailVerification.field_name.EMAIL: email, - UserEmailVerification.field_name.VERIFICATION_TOKEN: verification_token - }).exists(): - raise ValidationError('Email is not verified.') return user_manager.create_user(email, username, password, **extra_fields) def sign_in(request: Request, email: str, password: str) -> User: From 7f66f0036f174a535a7357f4a174983c0cdc8e14 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 14:28:17 +0900 Subject: [PATCH 350/552] =?UTF-8?q?feat(config):=20jwt=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=99=B8=EC=97=90=EB=8F=84=20session=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=EC=9D=84=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/config/settings.py b/app/config/settings.py index b58e0b0..5ef6797 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -136,6 +136,8 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', ), } From 562408b55142b6e8417ec33bd230190c685bd68d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 15:09:18 +0900 Subject: [PATCH 351/552] =?UTF-8?q?fix(users):=20access=20token=EC=9D=B4?= =?UTF-8?q?=20not=20serializable=20=ED=95=9C=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers/__init__.py | 2 +- app/users/services/authentication.py | 6 ++---- app/users/views/__init__.py | 5 +++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index 02384da..b37c816 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -34,7 +34,7 @@ class SignInSerializer(serializers.Serializer): class SignUpSerializer(serializers.ModelSerializer): - verification_token = serializers.CharField() + verification_token = serializers.CharField(read_only=True) class Meta: model = User diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py index 45e8b18..47b1d97 100644 --- a/app/users/services/authentication.py +++ b/app/users/services/authentication.py @@ -35,7 +35,5 @@ def sign_out(request: Request): """로그아웃""" logout(request) -def get_user_jwt(user: User) -> str: - refresh_token: RefreshToken - refresh_token = RefreshToken.for_user(user) - return str(refresh_token.access_token) +def get_user_jwt(user: User) -> RefreshToken: + return RefreshToken.for_user(user) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index b047cf3..ca0939f 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -44,7 +44,8 @@ def post(self, request, *args, **kwargs): return Response( data={ **serializers.UserSerializer(user).data, - 'token': token, + 'access_token': str(token.access_token), + 'refresh_token': str(token.token), }, status=status.HTTP_200_OK, ) @@ -85,7 +86,7 @@ class CurrentUserAPIView(generics.RetrieveAPIView): """현재 로그인한 사용자 정보 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.UserSerializer + serializer_class = serializers.SignUpSerializer def get_object(self) -> User: return self.request.user From efafdd3c49a527599ccf7c42884e1299dfa66033 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 15:24:36 +0900 Subject: [PATCH 352/552] =?UTF-8?q?fix(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=20=EA=B2=80=EC=82=AC=20API=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index ca0939f..57f01f2 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -112,7 +112,7 @@ def get(self, request: Request, *args, **kwargs): return Response( data={ "username": username, - "is_usable": services.is_email_usable(username), + "is_usable": services.is_username_usable(username), }, status=status.HTTP_200_OK, ) From f6f05bb914a8d9a2e4e3fe72a1a6d9d090b6d784 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 15:47:17 +0900 Subject: [PATCH 353/552] =?UTF-8?q?feat:=20swagger=20email=20check?= =?UTF-8?q?=EC=97=90=20query=20parameter=20=EC=A7=80=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 57f01f2..2ea0cdc 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -125,10 +125,13 @@ class EmailCheckAPIView(generics.GenericAPIView): serializer_class = serializers.EmailSerializer get_serializer: Callable[..., Serializer] - @swagger_auto_schema(responses={ - status.HTTP_200_OK: '검사를 수행했을 경우, 사용가능 여부를 Boolean으로 반환함.', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 형식.', - }) + @swagger_auto_schema( + query_serializer=serializers.EmailSerializer, + responses={ + status.HTTP_200_OK: '검사를 수행했을 경우, 사용가능 여부를 Boolean으로 반환함.', + status.HTTP_400_BAD_REQUEST: '잘못된 이메일 형식.', + }, + ) def get(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.query_params) serializer.is_valid(raise_exception=True) From 5c0227a7329ac5143badcac23220591c8ab1027f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 15:55:59 +0900 Subject: [PATCH 354/552] =?UTF-8?q?feat(users):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=EC=8B=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=EC=9D=B4=20=ED=95=84=EC=9A=94=EC=97=86=EB=8A=94=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EC=9D=80=20=EC=9E=98=EB=AA=BB=EB=90=9C=20Bearer=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=9E=85=EB=A0=A5=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=EC=82=AC=EC=9A=A9=20=EA=B0=80=EB=8A=A5=ED=95=98?= =?UTF-8?q?=EA=B2=8C=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 2ea0cdc..62567db 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -24,6 +24,7 @@ class SignInAPIView(mixins.RetrieveModelMixin, generics.GenericAPIView): """사용자 로그인 API""" + authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignInSerializer get_serializer: Callable[..., Serializer] @@ -54,6 +55,7 @@ def post(self, request, *args, **kwargs): class SignUpAPIView(generics.CreateAPIView): """사용자 등록(회원가입) API""" + authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignUpSerializer @@ -94,6 +96,8 @@ def get_object(self) -> User: class UsernameCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" + + authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.UsernameSerializer get_serializer: Callable[..., Serializer] @@ -121,6 +125,7 @@ def get(self, request: Request, *args, **kwargs): class EmailCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" + authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.EmailSerializer get_serializer: Callable[..., Serializer] @@ -152,6 +157,7 @@ class EmailVerifyThrottle(throttling.AnonRateThrottle): class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" + authentication_classes = [] throttle_classes = [] permission_classes = [permissions.AllowAny] get_serializer: Callable[..., Serializer] From 2c393e09affc7fab357f8fe07a5506a41666c4ae Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 15:57:49 +0900 Subject: [PATCH 355/552] =?UTF-8?q?fix(users):=20Response=EC=97=90=20verif?= =?UTF-8?q?ication=20token=EA=B3=BC=20password=EA=B0=80=20=EB=85=B8?= =?UTF-8?q?=EC=B6=9C=EB=90=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers/__init__.py | 2 +- app/users/views/__init__.py | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/users/serializers/__init__.py b/app/users/serializers/__init__.py index b37c816..02384da 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers/__init__.py @@ -34,7 +34,7 @@ class SignInSerializer(serializers.Serializer): class SignUpSerializer(serializers.ModelSerializer): - verification_token = serializers.CharField(read_only=True) + verification_token = serializers.CharField() class Meta: model = User diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 62567db..95ea38b 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -52,23 +52,29 @@ def post(self, request, *args, **kwargs): ) -class SignUpAPIView(generics.CreateAPIView): +class SignUpAPIView(generics.GenericAPIView): """사용자 등록(회원가입) API""" authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignUpSerializer + get_serializer: Callable[..., Serializer] @swagger_auto_schema(responses={ status.HTTP_201_CREATED: '회원가입 성공', status.HTTP_400_BAD_REQUEST: '잘못 입력한 값이 존재', }) - def perform_create(self, serializer: Serializer): + def post(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) email = serializer.validated_data['email'] token = serializer.validated_data.pop('verification_token') services.verify_token(email, token) user = services.sign_up(**serializer.validated_data) - serializer.instance = user + return Response( + data=serializers.UserSerializer(instance=user).data, + status=status.HTTP_201_CREATED, + ) class SignOutAPIView(APIView): From 6139fb740fdd8aff3faec6a75bf2d3c93e18f824 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 14 Aug 2024 16:53:08 +0900 Subject: [PATCH 356/552] =?UTF-8?q?feat(crews):=20CrewActivityProblem?= =?UTF-8?q?=EC=97=90=20crew=20=ED=95=84=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models/crew_activity_problem.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/crews/models/crew_activity_problem.py b/app/crews/models/crew_activity_problem.py index 19891d0..bace4b7 100644 --- a/app/crews/models/crew_activity_problem.py +++ b/app/crews/models/crew_activity_problem.py @@ -1,11 +1,18 @@ from django.core.validators import MinValueValidator from django.db import models +from crews.models.crew import Crew from crews.models.crew_activity import CrewActivity from problems.models.problem import Problem class CrewActivityProblem(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + blank=False, + null=False, + ) activity = models.ForeignKey( CrewActivity, on_delete=models.CASCADE, @@ -27,6 +34,7 @@ class field_name: # related fields SUBMISSIONS = 'submissions' # fields + CREW = 'crew' ACTIVITY = 'activity' PROBLEM = 'problem' ORDER = 'order' From 4efa1e9dcb8fee095457dd3e2fb40c9ecdf3e902 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 02:47:30 +0900 Subject: [PATCH 357/552] =?UTF-8?q?chore:=20drf-yasg=20(swagger)=20login?= =?UTF-8?q?=20url=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/config/settings.py b/app/config/settings.py index 5ef6797..72fcab4 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -179,3 +179,7 @@ DEFAULT_EXCEPTION_REPORTER = "config.reporters.NACLExceptionReporter" APPEND_SLASH = False + +# Swagger Settings (DRf-YASG) + +LOGIN_URL = "/api/v1/auth/signin" From 37f8efadeb7396accb2fde07961b06e83952e368 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 12:23:07 +0900 Subject: [PATCH 358/552] =?UTF-8?q?fix(crews):=20crew=20=EB=AA=A9=EB=A1=9D?= =?UTF-8?q?=20=EC=A1=B0=ED=9A=8C=20=ED=95=84=EB=93=9C=20=EC=9D=BC=EB=B6=80?= =?UTF-8?q?=EA=B0=80=20=EB=88=84=EB=9D=BD=EB=90=98=EB=8A=94=20=EA=B2=83=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(Serializer=EC=9D=98=20Field=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 9 +- app/crews/dto/__init__.py | 37 ++ app/crews/enums/__init__.py | 37 +- app/crews/enums/programming_language.py | 29 -- app/crews/models/choices.py | 16 +- app/crews/models/crew_member.py | 13 + app/crews/serializers/__init__.py | 175 ++++----- app/crews/serializers/fields.py | 278 ++++++-------- app/crews/serializers/mixins.py | 18 - app/crews/services.py | 66 ---- app/crews/services/__init__.py | 159 ++++++++ app/crews/utils.py | 4 + app/crews/views/__init__.py | 491 ++---------------------- 13 files changed, 482 insertions(+), 850 deletions(-) create mode 100644 app/crews/dto/__init__.py delete mode 100644 app/crews/enums/programming_language.py delete mode 100644 app/crews/serializers/mixins.py delete mode 100644 app/crews/services.py create mode 100644 app/crews/services/__init__.py create mode 100644 app/crews/utils.py diff --git a/app/config/urls.py b/app/config/urls.py index 2fd2f36..be79d3a 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -33,11 +33,10 @@ path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), path("auth/email/check", users.views.EmailCheckAPIView.as_view()), path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), - path("crews/", crews.views.CrewCreate.as_view()), - path("crews/recruiting", crews.views.CrewRecruiting.as_view()), - path("crews/my", crews.views.CrewJoined.as_view()), - path("crews//detail", crews.views.CrewDetail.as_view()), - path("crews//dashboard", crews.views.CrewDashboard.as_view()), + path("crews/", crews.views.CrewCreateAPIView.as_view()), + path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), + path("crews/my", crews.views.MyCrewAPIView.as_view()), + path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("problems/", problems.views.ProblemCreate.as_view()), path("problems/search", problems.views.ProblemSearch.as_view()), path("problems//detail", problems.views.ProblemDetail.as_view()), diff --git a/app/crews/dto/__init__.py b/app/crews/dto/__init__.py new file mode 100644 index 0000000..89a27e4 --- /dev/null +++ b/app/crews/dto/__init__.py @@ -0,0 +1,37 @@ +from collections import Counter +from dataclasses import dataclass +from dataclasses import field +from typing import Counter +from typing import List + + +from crews.enums import CrewTagType + + +@dataclass +class ProblemTag: + key: str + name_ko: str + name_en: str + + def __hash__(self) -> int: + return self.key + + +@dataclass +class ProblemStatistic: + sample_count: int = field(default=0) + difficulty: Counter[int] = field(default_factory=Counter) + tags: Counter[ProblemTag] = field(default_factory=Counter) + + +@dataclass +class CrewTag: + key: str + name: str + type: CrewTagType + + +@dataclass +class CrewProblem: + id: int diff --git a/app/crews/enums/__init__.py b/app/crews/enums/__init__.py index a922ffb..90aaa56 100644 --- a/app/crews/enums/__init__.py +++ b/app/crews/enums/__init__.py @@ -1,8 +1,43 @@ +from dataclasses import dataclass +from enum import Enum + from crews.enums.emoji import Emoji -from crews.enums.programming_language import ProgrammingLanguage __all__ = ( + 'CrewTagType', 'Emoji', 'ProgrammingLanguage', ) + + +class CrewTagType(Enum): + LANGUAGE = 'language' + LEVEL = 'level' + CUSTOM = 'custom' + + +class ProgrammingLanguage(Enum): + @dataclass + class Item: + key: str + name: str + extension: str + + # TLE에서 허용중인 언어 + NODE_JS = Item('nodejs', 'Node.js', '.js') + KOTLIN = Item('kotlin', 'Kotlin', '.kt') + SWIFT = Item('swift', 'Swift', '.swift') + CPP = Item('cpp', 'C++', '.cpp') + JAVA = Item('java', 'Java', '.java') + PYTHON = Item('python', 'Python', '.py') + C = Item('c', 'C', '.c') + + # 아직 지원하지 않는 언어 + JAVASCRIPT = Item('javascript', 'JavaScript', '.js') + CSHARP = Item('csharp', 'C#', '.cs') + RUBY = Item('ruby', 'Ruby', '.rb') + PHP = Item('php', 'PHP', '.php') + + def to_choice(self): + return self.value.key, self.value.name diff --git a/app/crews/enums/programming_language.py b/app/crews/enums/programming_language.py deleted file mode 100644 index c3bc45e..0000000 --- a/app/crews/enums/programming_language.py +++ /dev/null @@ -1,29 +0,0 @@ -from dataclasses import dataclass -from enum import Enum - - -@dataclass -class _ProgrammingLanguage: - key: str - name: str - extension: str - - def to_choice(self): - return self.key, self.name - - -class ProgrammingLanguage(Enum): - # TLE에서 허용중인 언어 - NODE_JS = _ProgrammingLanguage('nodejs', 'Node.js', '.js') - KOTLIN = _ProgrammingLanguage('kotlin', 'Kotlin', '.kt') - SWIFT = _ProgrammingLanguage('swift', 'Swift', '.swift') - CPP = _ProgrammingLanguage('cpp', 'C++', '.cpp') - JAVA = _ProgrammingLanguage('java', 'Java', '.java') - PYTHON = _ProgrammingLanguage('python', 'Python', '.py') - C = _ProgrammingLanguage('c', 'C', '.c') - - # 아직 지원하지 않는 언어 - JAVASCRIPT = _ProgrammingLanguage('javascript', 'JavaScript', '.js') - CSHARP = _ProgrammingLanguage('csharp', 'C#', '.cs') - RUBY = _ProgrammingLanguage('ruby', 'Ruby', '.rb') - PHP = _ProgrammingLanguage('php', 'PHP', '.php') diff --git a/app/crews/models/choices.py b/app/crews/models/choices.py index 27edfd7..2d1f5ee 100644 --- a/app/crews/models/choices.py +++ b/app/crews/models/choices.py @@ -1,15 +1,15 @@ from django.db import models -from crews.enums import ProgrammingLanguage +from crews import enums class ProgrammingLanguageChoices(models.TextChoices): """크루에서 사용 가능한 언어""" - NODE_JS = ProgrammingLanguage.NODE_JS.value.to_choice() - KOTLIN = ProgrammingLanguage.KOTLIN.value.to_choice() - SWIFT = ProgrammingLanguage.SWIFT.value.to_choice() - CPP = ProgrammingLanguage.CPP.value.to_choice() - JAVA = ProgrammingLanguage.JAVA.value.to_choice() - PYTHON = ProgrammingLanguage.PYTHON.value.to_choice() - C = ProgrammingLanguage.C.value.to_choice() + NODE_JS = enums.ProgrammingLanguage.NODE_JS.to_choice() + KOTLIN = enums.ProgrammingLanguage.KOTLIN.to_choice() + SWIFT = enums.ProgrammingLanguage.SWIFT.to_choice() + CPP = enums.ProgrammingLanguage.CPP.to_choice() + JAVA = enums.ProgrammingLanguage.JAVA.to_choice() + PYTHON = enums.ProgrammingLanguage.PYTHON.to_choice() + C = enums.ProgrammingLanguage.C.to_choice() diff --git a/app/crews/models/crew_member.py b/app/crews/models/crew_member.py index 3186bb3..62b97ae 100644 --- a/app/crews/models/crew_member.py +++ b/app/crews/models/crew_member.py @@ -1,4 +1,6 @@ from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver from users.models import User from crews.models.crew import Crew @@ -35,3 +37,14 @@ class Meta: ), ] ordering = ['created_at'] + + +@receiver(post_save, sender=Crew) +def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): + """크루 생성 시 선장을 자동으로 생성합니다.""" + if created: + CrewMember.objects.create(**{ + CrewMember.field_name.CREW: instance, + CrewMember.field_name.USER: instance.created_by, + CrewMember.field_name.IS_CAPTAIN: True, + }) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index 007f0e6..f5527ea 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -1,131 +1,88 @@ -from django.db.transaction import atomic from rest_framework import serializers -from rest_framework.serializers import ( - ModelSerializer, - MultipleChoiceField, -) - -from crews.models import ( - Crew, - CrewSubmittableLanguage, - ProgrammingLanguageChoices, -) -from crews.serializers.fields import ( - MembersField, - MemberCountField, - IsMemberField, - IsJoinableField, - TagsField, - RecentActivityField, -) -from crews.serializers.mixins import ( - CurrentUserMixin, - ReadOnlySerializerMixin, -) -from crews.services import set_crew_submittable_languages -from users.serializers import UserMinimalSerializer - - -class CrewDetailSerializer(CurrentUserMixin, - ModelSerializer): - is_member = IsMemberField() - members = MemberCountField() - tags = TagsField() - languages = MultipleChoiceField(choices=ProgrammingLanguageChoices.choices) - created_by = UserMinimalSerializer(read_only=True) + +from crews import models +from crews.serializers import fields + + +PK = 'id' + + +class RecruitingCrewSerializer(serializers.ModelSerializer): + """크루 목록""" + + is_joinable = fields.CrewIsJoinableField() + members = fields.CrewMemberCountField() + tags = fields.CrewTagsField() + latest_activity = fields.LatestActivityField() class Meta: - model = Crew + model = models.Crew fields = [ - 'id', - Crew.field_name.ICON, - Crew.field_name.NAME, - Crew.field_name.MAX_MEMBERS, + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.IS_ACTIVE, + 'is_joinable', 'members', - 'is_member', - 'languages', 'tags', - Crew.field_name.MIN_BOJ_LEVEL, - Crew.field_name.CUSTOM_TAGS, - Crew.field_name.NOTICE, - Crew.field_name.IS_RECRUITING, - Crew.field_name.IS_ACTIVE, - Crew.field_name.CREATED_AT, - Crew.field_name.CREATED_BY, - Crew.field_name.UPDATED_AT, + 'latest_activity', ] - read_only_fields = [ - 'id', - 'tags', - 'members', - Crew.field_name.CREATED_AT, - Crew.field_name.CREATED_BY, - Crew.field_name.UPDATED_AT, - ] - extra_kwargs = { - Crew.field_name.MAX_MEMBERS: {'write_only': True}, - Crew.field_name.MIN_BOJ_LEVEL: {'write_only': True}, - 'languages': {'write_only': True}, - Crew.field_name.CUSTOM_TAGS: {'write_only': True}, - } - - def save(self, **kwargs): - languages = self.validated_data.pop('languages') - with atomic(): - crew: Crew = super().save(**kwargs) - set_crew_submittable_languages(crew, languages) - return crew - - def create(self, validated_data): - validated_data[Crew.field_name.CREATED_BY] = self.current_user() - return super().create(validated_data) - - -class CrewRecruitingSerializer(ReadOnlySerializerMixin, - ModelSerializer): - is_joinable = IsJoinableField() - is_member = IsMemberField() - activities = RecentActivityField() - members = MembersField() - tags = TagsField() + read_only_fields = ['__all__'] + + +class MyCrewSerializer(serializers.ModelSerializer): + "나의 참여 크루" + + latest_activity = fields.LatestActivityField() class Meta: - model = Crew + model = models.Crew fields = [ - Crew.field_name.ICON, - Crew.field_name.NAME, - Crew.field_name.IS_ACTIVE, - Crew.field_name.IS_RECRUITING, - 'is_joinable', - 'is_member', - 'activities', - 'members', - 'tags', + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + 'latest_activity', ] read_only_fields = ['__all__'] -class CrewJoinedSerializer(ReadOnlySerializerMixin, - ModelSerializer): - activities = RecentActivityField() +class MyCrewDashboardSerializer(serializers.ModelSerializer): + """크루 대시보드 + + - 공지사항 + - 크루 태그 + - 나의 동료 + - 크루가 풀이한 문제 + - 풀이한 문제의 난이도 + """ + + tags = fields.CrewTagsField() + members = fields.CrewMembersField() + statistics = fields.ProblemStatisticsField() + activities = fields.CrewActivitiesField() class Meta: - model = Crew + model = models.Crew fields = [ - Crew.field_name.ICON, - Crew.field_name.NAME, - Crew.field_name.IS_ACTIVE, + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.NOTICE, + 'tags', + 'members', + 'statistics', 'activities', ] read_only_fields = ['__all__'] -class CrewDashboardSerializer(serializers.Serializer): - id = serializers.CharField() - icon = serializers.CharField() - name = serializers.CharField() - activity = serializers.CharField() - members = serializers.CharField() - member_submissions = serializers.CharField() - tags = serializers.CharField() - statistics = serializers.CharField() +class MyCrewDashboardAcitivySerializer(serializers.ModelSerializer): + problems = fields.CrewAcitivityProblemsField() + + class Meta: + model = models.CrewActivity + fields = [ + PK, + 'problems', + ] + read_only_fields = ['__all__'] diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index eb81a4b..1497d82 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -1,174 +1,148 @@ -from dataclasses import asdict, dataclass -from datetime import date -from enum import Enum -from typing import Iterable, List, Optional +from rest_framework import serializers -from rest_framework.serializers import ReadOnlyField +from crews import models +from crews import services +from crews import utils +from users.models import User -from crews.models import Crew, CrewActivity -from crews.serializers.mixins import CurrentUserMixin -from crews.services import get_members, is_member, is_joinable -from users.models import UserBojLevelChoices +class LatestActivityField(serializers.SerializerMethodField): + """마지막 활동회차""" -class MemberCountField(ReadOnlyField): - def to_representation(self, crew: Crew): - members = get_members(crew) - return { - 'count': members.count(), - 'max_count': crew.max_members, - } + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + if not crew.is_active: + return { + "name": "활동 종료", + "date_start_at": None, + "date_end_at": None, + } + queryset = services.crew_activities_queryset(crew) + try: + activity = queryset.latest() + except models.CrewActivity.DoesNotExist: + return { + "name": "등록된 활동 없음", + "date_start_at": None, + "date_end_at": None, + } + else: + return { + "name": f"{queryset.count()}회차", + "date_start_at": activity.start_at, + "date_end_at": activity.end_at, + } -class MembersField(ReadOnlyField): - def to_representation(self, crew: Crew): - members = get_members(crew) - return { - 'count': members.count(), - 'max_count': crew.max_members, - 'items': [{ - "user_id": member.user.pk, +class CrewMembersField(serializers.SerializerMethodField): + """나의 동료""" + + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + queryset = models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, + }) + image_field = serializers.ImageField() + return [ + { "username": member.user.username, - "profile_image": member.user.profile_image, + "profile_image": image_field.to_representation(member.user.profile_image), "is_captain": member.is_captain, - "created_at": member.created_at, - } for member in members], - } + } + for member in queryset + ] -class IsMemberField(CurrentUserMixin, - ReadOnlyField): - def to_representation(self, crew: Crew): - user = self.current_user() - return is_member(crew, user) +class CrewMemberCountField(serializers.SerializerMethodField): + """크루 인원""" + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + return { + "count": services.crew_member_count(crew), + "max_count": crew.max_members, + } -class IsJoinableField(CurrentUserMixin, - ReadOnlyField): - def to_representation(self, crew: Crew): - user = self.current_user() - return is_joinable(crew, user) +class CrewTagsField(serializers.SerializerMethodField): + """크루 태그""" -class TagType(Enum): - LANGUAGE = 'language' - LEVEL = 'level' - CUSTOM = 'custom' + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + return [ + { + 'key': tag.key, + 'name': tag.name, + 'type': tag.type.value, + } + for tag in services.crew_tags(crew) + ] -@dataclass -class TagDict: - key: str - name: str - type: TagType +class CrewIsJoinableField(serializers.SerializerMethodField): + def to_representation(self, crew: models.Crew): + user = serializers.CurrentUserDefault()(self) + assert isinstance(crew, models.Crew) + assert isinstance(user, User) + return services.crew_is_joinable(crew, user) + + +class CrewIsMemberField(serializers.SerializerMethodField): + def to_representation(self, crew: models.Crew): + user = serializers.CurrentUserDefault()(self) + assert isinstance(crew, models.Crew) + assert isinstance(user, User) + return services.crew_is_member(crew, user) + + +class CrewActivitiesField(serializers.SerializerMethodField): + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + queryset = services.crew_activities_queryset(crew) + return [ + { + 'id': activity.pk, + 'name': f'{n}회차', + } + for n, activity in enumerate(queryset, start=1) + ] -class TagsField(ReadOnlyField): - def to_representation(self, crew: Crew): - # 태그의 나열 순서는 리스트에 선언한 순서를 따름. - tags: List[TagDict] = [ - *self.get_language_tags(crew), - *self.get_level_tags(crew), - *self.get_custom_tags(crew), +class CrewAcitivityProblemsField(serializers.SerializerMethodField): + def to_representation(self, activity: models.CrewActivity): + assert isinstance(activity, models.CrewActivity) + queryset = models.CrewActivityProblem.objects.filter(**{ + models.CrewActivityProblem.field_name.ACTIVITY: activity, + }) + return [ + { + 'is_solved': problem, + } + for problem in queryset ] - return { - 'count': len(tags), - 'items': [asdict(tag) for tag in tags], - } - def get_custom_tags(self, crew: Crew) -> Iterable[TagDict]: - for tag in crew.custom_tags: - yield TagDict( - key=None, - name=tag, - type=TagType.CUSTOM.value, - ) - - def get_language_tags(self, crew: Crew) -> Iterable[TagDict]: - for lang in crew.submittable_languages.all(): - yield TagDict( - key=lang.key, - name=lang.name, - type=TagType.LANGUAGE.value, - ) - - def get_level_tags(self, crew: Crew) -> Iterable[TagDict]: - yield TagDict( - key=None, - name=self.get_boj_level_bounded_name( - level=UserBojLevelChoices(crew.min_boj_level), - ), - type=TagType.LEVEL.value, - ) - - def get_boj_level_bounded_name(self, - level: Optional[UserBojLevelChoices], - bound_tier: int = 5, - bound_msg: str = "이상", - default_msg: str = "티어 무관", - lang='ko', - arabic=False) -> str: - """level에 대한 백준 난이도 태그 이름을 반환한다. - - bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 - 티어(1,2,3,4,5)를 의미한다. - - bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. - - 만약 level의 티어가 bound_tier와 - 같다면 랭크만 출력하고, - 같지않다면 랭크와 티어 모두 출력한다. - - 메시지의 마지막에는 bound_msg를 출력한다. - """ - if level is None: - return default_msg - if level.get_tier() == bound_tier: - return level.get_division_name(lang=lang) + ' ' + bound_msg - else: - return level.get_name(lang=lang, arabic=arabic) + ' ' + bound_msg - - -@dataclass -class ActivityDict: - nth: Optional[int] = None - name: str = '' - start_at: Optional[date] = None - end_at: Optional[date] = None - is_open: bool = False # 제출 가능 여부 - - @classmethod - def from_activity(cls, activity: CrewActivity) -> 'ActivityDict': - return ActivityDict( - name=activity.name, - nth=activity.nth(), - is_open=activity.is_opened(), - start_at=activity.start_at, - end_at=activity.end_at, - ) - - -class RecentActivityField(ReadOnlyField): - def to_representation(self, crew: Crew): - activities = CrewActivity.objects.filter(**{ - CrewActivity.field_name.CREW: crew, - }) + +class ProblemStatisticsField(serializers.SerializerMethodField): + def to_representation(self, crew: models.Crew): + statistics = services.problem_statistics(crew) return { - 'count': activities.count(), - "recent": asdict(self.get_recent_activity(crew)), + 'difficulty': [ + { + 'difficulty': difficulty, + 'problem_count': count, + 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), + } + for difficulty, count in statistics.difficulty.items() + ], + 'tags': [ + { + 'label': { + 'ko': tag.name_ko, + 'en': tag.name_en, + }, + 'problem_count': count, + 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), + } + for tag, count in statistics.tags.items() + ], } - - def get_recent_activity(self, crew: Crew) -> ActivityDict: - # 활동 종료 여부가 최우선 순위 - if not crew.is_active: - return ActivityDict(name='활동 종료') - # 활동 중이라면, 현재 진행 중인 활동 중 가장 오래된 것을 우선적으로 표시 - if (opened_activities := CrewActivity.opened_of_crew(crew)).exists(): - activity = opened_activities.earliest() - return ActivityDict.from_activity(activity) - # 현재 진행 중인 활동이 없다면, 가장 최근 활동을 표시 - if (closed_activities := CrewActivity.closed_of_crew(crew)).exists(): - activity = closed_activities.latest() - return ActivityDict.from_activity(activity) - # 활동 중이나, 등록된 활동이 없다면 '등록된 활동 없음'을 표시 - return ActivityDict(name='등록된 활동 없음') diff --git a/app/crews/serializers/mixins.py b/app/crews/serializers/mixins.py deleted file mode 100644 index be3fd40..0000000 --- a/app/crews/serializers/mixins.py +++ /dev/null @@ -1,18 +0,0 @@ -from rest_framework.exceptions import PermissionDenied -from rest_framework.serializers import CurrentUserDefault, Field - - -class ReadOnlySerializerMixin: - def create(self, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def update(self, instance, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def save(self, **kwargs): - raise PermissionDenied('Cannot update user through this serializer') - - -class CurrentUserMixin(Field): - def current_user(self): - return CurrentUserDefault()(self) diff --git a/app/crews/services.py b/app/crews/services.py deleted file mode 100644 index 803e4b4..0000000 --- a/app/crews/services.py +++ /dev/null @@ -1,66 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from crews.models import ( - Crew, - CrewMember, - CrewSubmittableLanguage, - ProgrammingLanguageChoices, -) -from users.models import User - - -@receiver(post_save, sender=Crew) -def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): - """크루 생성 시 선장을 자동으로 생성합니다.""" - if created: - CrewMember.objects.create(**{ - CrewMember.field_name.CREW: instance, - CrewMember.field_name.USER: instance.created_by, - CrewMember.field_name.IS_CAPTAIN: True, - }) - - -def set_crew_submittable_languages(crew: Crew, languages: list): - """크루의 제출 가능 언어를 설정합니다.""" - for language in languages: - for choice in ProgrammingLanguageChoices.choices: - if language == choice[0]: - raise ValueError('Invalid language') - CrewSubmittableLanguage.objects.filter(**{ - CrewSubmittableLanguage.field_name.CREW: crew, - }).delete() - CrewSubmittableLanguage.objects.bulk_create([ - CrewSubmittableLanguage(**{ - CrewSubmittableLanguage.field_name.CREW: crew, - CrewSubmittableLanguage.field_name.LANGUAGE: language, - }) for language in languages - ]) - - -def get_members(crew: Crew): - return CrewMember.objects.filter(**{ - CrewMember.field_name.CREW: crew, - }) - - -def is_member(crew: Crew, user: User) -> bool: - return CrewMember.objects.filter(**{ - CrewMember.field_name.CREW: crew, - CrewMember.field_name.USER: user, - }).exists() - - -def is_joinable(crew: Crew, user: User) -> bool: - if not crew.is_recruiting: - return False - if get_members(crew).count() >= crew.max_members: - return False - if is_member(crew, user): - return False - if crew.min_boj_level is not None: - return bool( - (user.boj_level is not None) and - (user.boj_level >= crew.min_boj_level) - ) - return True diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py new file mode 100644 index 0000000..2b6cecf --- /dev/null +++ b/app/crews/services/__init__.py @@ -0,0 +1,159 @@ +from typing import Iterable +from typing import List +from typing import Optional + +from django.db.models import QuerySet +from django.utils import timezone + +from crews import dto +from crews import models +from problems.models import ProblemAnalysis +from problems.models import ProblemDifficultyChoices +from users.models import User +from users.models import UserBojLevelChoices + + +def problem_statistics(crew: models.Crew) -> dto.ProblemStatistic: + statistics = dto.ProblemStatistic() + queryset = models.CrewActivityProblem.objects.filter(**{ + models.CrewActivityProblem.field_name.CREW: crew, + }) + for activity_problem in queryset: + statistics.sample_count += 1 + try: + analysis = ProblemAnalysis.objects.filter(**{ + ProblemAnalysis.field_name.PROBLEM: activity_problem.problem, + }).latest() + except ProblemAnalysis.DoesNotExist: + statistics.difficulty[ProblemDifficultyChoices.UNDER_ANALYSIS.value] += 1 + else: + statistics.difficulty[analysis.difficulty] += 1 + for tag in analysis.tags: + problem_tag = dto.ProblemTag( + key=tag.key, + name_ko=tag.name_ko, + name_en=tag.name_en, + ) + statistics.tags[problem_tag] += 1 + return statistics + + +def crew_of_user_queryset(include_user: Optional[User] = None, + exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: + """특정 사용자가 속하거나 속하지 않은 크루 목록을 조회하는 쿼리를 반환한다.""" + queryset = models.Crew.objects + if include_user is not None: + queryset = queryset.filter(pk__in=models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: include_user, + }).values_list(models.CrewMember.field_name.CREW)) + if exclude_user is not None: + queryset = queryset.exclude(pk__in=models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: exclude_user, + }).values_list(models.CrewMember.field_name.CREW)) + return queryset + + +def crew_activities_queryset(crew: models.Crew, exclude_future=True) -> QuerySet[models.CrewActivity]: + """ + exclude_future: 아직 공개되지 않은 활동도 포함할 지 여부. + """ + kwargs = { + models.CrewActivity.field_name.CREW: crew, + } + if exclude_future: + kwargs[models.CrewActivity.field_name.START_AT + '__lte'] = timezone.now() + return models.CrewActivity.objects.filter(**kwargs).order_by(models.CrewActivity.field_name.START_AT) + + +def crew_tags(crew: models.Crew) -> List[dto.CrewTag]: + # 태그의 나열 순서는 리스트에 선언한 순서를 따름. + return [ + *_get_language_tags(crew), + *_get_level_tags(crew), + *_get_custom_tags(crew), + ] + + +def _get_language_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: + queryset = models.CrewSubmittableLanguage.objects.filter(**{ + models.CrewSubmittableLanguage.field_name.CREW: crew, + }) + for language in queryset.all(): + yield dto.CrewTag( + key=language.key, + name=language.name, + type=dto.CrewTagType.LANGUAGE, + ) + + +def _get_level_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: + yield dto.CrewTag( + key=None, + name=_get_bounded_level_name(UserBojLevelChoices(crew.min_boj_level)), + type=dto.CrewTagType.LEVEL, + ) + + +def _get_custom_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: + for tag in crew.custom_tags: + yield dto.CrewTag( + key=None, + name=tag, + type=dto.CrewTagType.CUSTOM, + ) + + +def _get_bounded_level_name(level: Optional[UserBojLevelChoices], + bound_tier: int = 5, + bound_msg: str = "이상", + default_msg: str = "티어 무관", + lang='ko', + arabic=False) -> str: + """level에 대한 백준 난이도 태그 이름을 반환한다. + + bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 + 티어(1,2,3,4,5)를 의미한다. + + bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. + + 만약 level의 티어가 bound_tier와 + 같다면 랭크만 출력하고, + 같지않다면 랭크와 티어 모두 출력한다. + + 메시지의 마지막에는 bound_msg를 출력한다. + """ + if level is None: + return default_msg + assert isinstance(level, UserBojLevelChoices) + if level.get_tier() == bound_tier: + return f'{level.get_division_name(lang=lang)} {bound_msg}' + else: + return f'{level.get_name(lang=lang, arabic=arabic)} {bound_msg}' + + +def crew_member_count(crew: models.Crew) -> int: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, + }).count() + + +def crew_is_joinable(crew: models.Crew, user: User) -> bool: + if not crew.is_recruiting: + return False + if crew_member_count(crew) >= crew.max_members: + return False + if crew_is_member(crew, user): + return False + if crew.min_boj_level is not None: + return bool( + (user.boj_level is not None) and + (user.boj_level >= crew.min_boj_level) + ) + return True + + +def crew_is_member(crew: models.Crew, user: User) -> bool: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, + models.CrewMember.field_name.USER: user, + }).exists() diff --git a/app/crews/utils.py b/app/crews/utils.py new file mode 100644 index 0000000..8dbdf4d --- /dev/null +++ b/app/crews/utils.py @@ -0,0 +1,4 @@ +def divide_by_zero_handler(numerator: float, denominator: float, default=0) -> float: + if denominator == 0: + return default + return numerator / denominator diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 4d6fe40..9512f39 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,494 +1,61 @@ from drf_yasg.utils import swagger_auto_schema from django.utils import timezone from rest_framework import generics -from rest_framework import mixins from rest_framework import permissions from rest_framework import status -from rest_framework.generics import GenericAPIView from rest_framework.response import Response -from crews.models import Crew, CrewMember +from crews import models from crews import serializers -from crews.serializers import ( - CrewDetailSerializer, - CrewRecruitingSerializer, - CrewJoinedSerializer, -) +from crews import services -class CrewCreate(mixins.CreateModelMixin, - GenericAPIView): +class CrewCreateAPIView(generics.CreateAPIView): """크루 생성 API""" - queryset = Crew.objects.all() + queryset = models.Crew.objects.all() permission_classes = [permissions.IsAuthenticated] - serializer_class = CrewDetailSerializer + serializer_class = serializers.RecruitingCrewSerializer - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) +class RecruitingCrewListAPIView(generics.ListAPIView): + """크루 목록""" - -class CrewDetail(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - GenericAPIView): - """크루 상세 조회, 수정, 삭제 API""" - - queryset = Crew.objects.all() - permission_classes = [permissions.IsAuthenticated] - serializer_class = CrewDetailSerializer - lookup_field = 'id' - - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) - - def patch(self, request, *args, **kwargs): - return self.partial_update(request, *args, **kwargs) - - def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) - - -class CrewRecruiting(mixins.ListModelMixin, - GenericAPIView): - """모집 중인 크루 목록 조회 API""" - - queryset = Crew.objects.filter(**{Crew.field_name.IS_RECRUITING: True}) permission_classes = [permissions.AllowAny] - serializer_class = CrewRecruitingSerializer + serializer_class = serializers.RecruitingCrewSerializer - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) + def get_queryset(self): + # 본인이 속한 크루는 제외. + queryset = services.crew_of_user_queryset( + exclude_user=self.request.user, + ) + return queryset.filter(**{ + models.Crew.field_name.IS_RECRUITING: True, + }) -class CrewJoined(mixins.ListModelMixin, - GenericAPIView): - """가입한 크루 목록 조회 API""" +class MyCrewAPIView(generics.ListAPIView): + """나의 참여 크루""" permission_classes = [permissions.IsAuthenticated] - serializer_class = CrewJoinedSerializer + serializer_class = serializers.MyCrewSerializer def get_queryset(self): - # 현재 사용자가 속한 크루만 반환 - crews = CrewMember.objects.filter(**{ - CrewMember.field_name.USER: self.request.user, - }).values_list(CrewMember.field_name.CREW) - queryset = Crew.objects.filter(pk__in=crews) + queryset = services.crew_of_user_queryset( + include_user=self.request.user, + ) # 활동 종료된 크루는 뒤로 가도록 정렬 - return queryset.order_by('-'+Crew.field_name.IS_ACTIVE) - - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) + return queryset.order_by('-'+models.Crew.field_name.IS_ACTIVE) -class CrewDashboard(generics.RetrieveAPIView): +class CrewDashboardAPIView(generics.RetrieveAPIView): """가입한 크루 목록 조회 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewDashboardSerializer + serializer_class = serializers.MyCrewDashboardSerializer lookup_field = 'id' - def get(self, request, *args, **kwargs): - return Response( - data={ - 'id': 1, - 'icon': '😀', - 'name': '코딩메리호', - 'activity': { - 'recent': { - 'id': 1, - 'name': '9회차', - 'problems': { - 'count': 8, - 'items': [ - { - 'problem_number': 1, - 'is_solved': False, - }, - { - 'problem_number': 2, - 'is_solved': False, - }, - { - 'problem_number': 3, - 'is_solved': True, - }, - { - 'problem_number': 4, - 'is_solved': True, - }, - { - 'problem_number': 5, - 'is_solved': True, - }, - { - 'problem_number': 6, - 'is_solved': True, - }, - { - 'problem_number': 7, - 'is_solved': True, - }, - { - 'problem_number': 8, - 'is_solved': True, - }, - ], - }, - }, - }, - 'members': { - 'count': 3, - 'max_count': 8, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - "is_captain": True, - }, - { - "id": 3, - "username": "hello", - "profile_image": "https://picsum.photos/250/250", - "is_captain": False, - }, - { - "id": 2, - "username": "hi", - "profile_image": "https://picsum.photos/250/250", - "is_captain": False, - }, - ], - }, - 'member_submissions': { - 'count': 2, - 'items': [ - { - 'user_id': 1, - "username": "hey", - "submissions": { - 'count': 7, - 'items': [ - { - 'problem_number': 1, - 'submission_id': 1, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 2, - 'submission_id': 2, - 'is_correct': False, - 'is_help_needed': True, - }, - { - 'problem_number': 4, - 'submission_id': 3, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 5, - 'submission_id': 4, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 6, - 'submission_id': 5, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 7, - 'submission_id': 6, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 8, - 'submission_id': 7, - 'is_correct': True, - 'is_help_needed': False, - }, - ], - }, - }, - { - 'user_id': 2, - "username": "leeyuuuuuuum", - "submissions": { - 'count': 6, - 'items': [ - { - 'problem_number': 1, - 'submission_id': 8, - 'is_correct': False, - 'is_help_needed': True, - }, - { - 'problem_number': 2, - 'submission_id': 9, - 'is_correct': True, - 'is_help_needed': True, - }, - { - 'problem_number': 4, - 'submission_id': 10, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 5, - 'submission_id': 11, - 'is_correct': False, - 'is_help_needed': False, - }, - { - 'problem_number': 7, - 'submission_id': 12, - 'is_correct': True, - 'is_help_needed': False, - }, - { - 'problem_number': 8, - 'submission_id': 13, - 'is_correct': True, - 'is_help_needed': False, - }, - ], - }, - } - ], - }, - 'tags': { - 'count': 3, - 'items': [ - { - 'key': 'python', - 'name': 'Python', - 'type': 'language', - }, - { - 'key': None, - 'name': '실버 이상', - 'type': 'level', - }, - { - 'key': None, - 'name': '우하하', - 'type': 'custom', - }, - ], - }, - 'statistics': { - 'difficulty': [ - { - 'difficulty': 1, - 'problem_count': 3, - 'ratio': .375, - }, - { - 'difficulty': 2, - 'problem_count': 3, - 'ratio': .375, - }, - { - 'difficulty': 3, - 'problem_count': 2, - 'ratio': .25, - } - ], - 'tags': [ - { - 'label': { - 'en': 'mathematics', - 'ko': '수학' - }, - 'problem_count': 20, - 'ratio': .392, - }, - { - 'label': { - 'en': 'implementation', - 'ko': '구현' - }, - 'problem_count': 20, - 'ratio': .392, - }, - { - 'label': { - 'en': 'graph', - 'ko': '그래프 이론' - }, - 'problem_count': 5, - 'ratio': .098, - }, - { - 'label': { - 'en': 'dynamic programming', - 'ko': '다이나믹 프로그래밍' - }, - 'problem_count': 4, - 'ratio': .078, - }, - { - 'label': { - 'en': 'data structures', - 'ko': '자료구조' - }, - 'problem_count': 4, - 'ratio': .078, - }, - ], - }, - 'reviews': { - 'count': 2, - 'items': [ - { - 'problem_number': 1, - 'problem_title': 'A+B', - 'submission_id': 1, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 3, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - { - "id": 3, - "username": "hello", - "profile_image": "https://picsum.photos/250/250", - }, - { - "id": 2, - "username": "hi", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 2, - 'problem_title': 'C+D', - 'submission_id': 2, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 3, - 'problem_title': '임시제목', - 'submission_id': 3, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 4, - 'problem_title': '임시제목', - 'submission_id': 4, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 5, - 'problem_title': '임시제목', - 'submission_id': 5, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 6, - 'problem_title': '임시제목', - 'submission_id': 6, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 7, - 'problem_title': '임시제목', - 'submission_id': 7, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - { - 'problem_number': 8, - 'problem_title': '임시제목', - 'submission_id': 8, - 'submission_created_at': timezone.now(), - 'reviewers': { - 'count': 1, - 'items': [ - { - "id": 4, - "username": "hey", - "profile_image": "https://picsum.photos/250/250", - }, - ], - }, - }, - ], - }, - }, - ) \ No newline at end of file + def get_queryset(self): + return services.crew_of_user_queryset( + include_user=self.request.user, + ) From 2aef0a978485e9af5d5f3516482da79f841a9ae6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 12:30:21 +0900 Subject: [PATCH 359/552] refactor(problems): add typehint --- app/problems/models/problem_analysis.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/problems/models/problem_analysis.py b/app/problems/models/problem_analysis.py index 2954221..5da2910 100644 --- a/app/problems/models/problem_analysis.py +++ b/app/problems/models/problem_analysis.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from django.db import models from problems.models.choices import ProblemDifficultyChoices @@ -6,6 +8,9 @@ class ProblemAnalysis(models.Model): + if TYPE_CHECKING: + tags: models.ManyToManyField[ProblemTag] + problem = models.OneToOneField( Problem, on_delete=models.CASCADE, From 8b23f5844626e352093f96fb2d07b7861cd7b9f3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 12:34:24 +0900 Subject: [PATCH 360/552] chore: untrack custom scripts --- .gitignore | 2 ++ app/runserver | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 app/runserver diff --git a/.gitignore b/.gitignore index 50a85c2..138a61e 100644 --- a/.gitignore +++ b/.gitignore @@ -170,3 +170,5 @@ cython_debug/ !**/migrations/__init__.py .static/ .media/ +sync_db +runserver diff --git a/app/runserver b/app/runserver deleted file mode 100644 index 1bb0dd8..0000000 --- a/app/runserver +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash - -source ../venv/bin/activate -python manage.py makemigrations -python manage.py migrate -python manage.py runserver --insecure 0.0.0.0:80 From e287bc0e12c7efc2f00f259b9b92a8cc5df1f929 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 15:04:08 +0900 Subject: [PATCH 361/552] =?UTF-8?q?refactor(problems):=20TagRelation=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/admin.py | 144 +++++++----------- app/problems/dto/__init__.py | 23 +++ app/problems/models/__init__.py | 17 +-- app/problems/models/dto.py | 20 --- app/problems/models/problem_analysis.py | 14 +- app/problems/models/problem_analysis_queue.py | 33 ---- app/problems/models/problem_analysis_tag.py | 27 ++++ app/problems/models/problem_tag.py | 8 - app/problems/models/problem_tag_relation.py | 23 +++ app/problems/serializers/fields.py | 84 +++++----- app/problems/serializers/mixins.py | 35 ----- app/problems/services/__init__.py | 28 ++++ app/problems/services/analysis/__init__.py | 26 +--- app/problems/services/analysis/analyser.py | 7 +- 14 files changed, 208 insertions(+), 281 deletions(-) create mode 100644 app/problems/dto/__init__.py delete mode 100644 app/problems/models/dto.py delete mode 100644 app/problems/models/problem_analysis_queue.py create mode 100644 app/problems/models/problem_analysis_tag.py create mode 100644 app/problems/models/problem_tag_relation.py delete mode 100644 app/problems/serializers/mixins.py diff --git a/app/problems/admin.py b/app/problems/admin.py index 165fbbc..1fe7607 100644 --- a/app/problems/admin.py +++ b/app/problems/admin.py @@ -4,29 +4,23 @@ from django.db.models import QuerySet from django.utils.translation import ngettext -from problems.models import ( - Problem, - ProblemAnalysis, - ProblemAnalysisQueue, - ProblemTag, -) -from problems.services.analysis import AnalysingService +from problems import models from users.models import User -@admin.register(Problem) +@admin.register(models.Problem) class ProblemModelAdmin(admin.ModelAdmin): list_display = [ - Problem.field_name.TITLE, - Problem.field_name.CREATED_BY, - Problem.field_name.CREATED_AT, - Problem.field_name.UPDATED_AT, + models.Problem.field_name.TITLE, + models.Problem.field_name.CREATED_BY, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.UPDATED_AT, ] search_fields = [ - Problem.field_name.TITLE, - Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + models.Problem.field_name.TITLE, + models.Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, ] - ordering = ['-'+Problem.field_name.CREATED_AT] + ordering = ['-'+models.Problem.field_name.CREATED_AT] actions = [ 'analyze', 'add_to_analysis_queue', @@ -34,9 +28,9 @@ class ProblemModelAdmin(admin.ModelAdmin): ] @admin.action(description="Set admin(you) as creator for selected problems") - def set_creator(self, request, queryset: QuerySet[Problem]): + def set_creator(self, request, queryset: QuerySet[models.Problem]): updated = queryset.update(**{ - Problem.field_name.CREATED_BY: request.user, + models.Problem.field_name.CREATED_BY: request.user, }) self.message_user( request, @@ -49,106 +43,78 @@ def set_creator(self, request, queryset: QuerySet[Problem]): messages.SUCCESS, ) - @admin.action(description="Add selected problems to analysis queue") - def add_to_analysis_queue(self, request, queryset: QuerySet[Problem]): - for problem in queryset: - ProblemAnalysisQueue.objects.create(**{ - ProblemAnalysisQueue.field_name.PROBLEM: problem, - }) - self.message_user( - request, - ngettext( - "%d problem was successfully added to analysis queue.", - "%d problems were successfully added to analysis queue.", - queryset.count(), - ) - % queryset.count(), - messages.SUCCESS, - ) - - @admin.action(description="Analyze selected problems") - def analyze(self, request, queryset: QuerySet[Problem]): - analysing_service = AnalysingService.get_instance() - for problem in queryset: - analysis = analysing_service.analyze(problem) - analysis.save() - self.message_user( - request, - ngettext( - "%d problem was successfully analyzed.", - "%d problems were successfully analyzed.", - queryset.count(), - ) - % queryset.count(), - messages.SUCCESS, - ) - -@admin.register(ProblemAnalysis) +@admin.register(models.ProblemAnalysis) class ProblemAnalysisModelAdmin(admin.ModelAdmin): list_display = [ - ProblemAnalysis.field_name.PROBLEM, - ProblemAnalysis.field_name.DIFFICULTY, + models.ProblemAnalysis.field_name.PROBLEM, + models.ProblemAnalysis.field_name.DIFFICULTY, 'get_timecomplexity', 'get_tags', 'get_hint', - ProblemAnalysis.field_name.CREATED_AT, + models.ProblemAnalysis.field_name.CREATED_AT, ] search_fields = [ - ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, - ProblemAnalysis.field_name.TIME_COMPLEXITY, + models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, + models.ProblemAnalysis.field_name.TIME_COMPLEXITY, ] - ordering = ['-'+ProblemAnalysis.field_name.CREATED_AT] + ordering = ['-'+models.ProblemAnalysis.field_name.CREATED_AT] @admin.display(description='Big-O') - def get_timecomplexity(self, obj: ProblemAnalysis) -> str: + def get_timecomplexity(self, obj: models.ProblemAnalysis) -> str: return f'O({obj.time_complexity})' @admin.display(description='Tags') - def get_tags(self, obj: ProblemAnalysis) -> str: - def get_tag_keys(): - for tag in obj.tags.all(): - yield f'#{tag.key}' - return ' '.join(get_tag_keys()) + def get_tags(self, analysis: models.ProblemAnalysis) -> str: + tags = models.ProblemAnalysisTag.objects.filter(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, + }).values_list(models.ProblemAnalysisTag.field_name.TAG, flat=True) + return ' '.join(tag.key for tag in tags) @admin.display(description='Hint (Steps, Verbose)') - def get_hint(self, obj: ProblemAnalysis) -> str: + def get_hint(self, obj: models.ProblemAnalysis) -> str: hints_in_a_row = ', '.join(obj.hint) return len(obj.hint), shorten(hints_in_a_row, width=32) -@admin.register(ProblemAnalysisQueue) -class ProblemAnalysisQueueModelAdmin(admin.ModelAdmin): +@admin.register(models.ProblemAnalysisTag) +class ProblemAnalysisTagModelAdmin(admin.ModelAdmin): list_display = [ - ProblemAnalysisQueue.field_name.PROBLEM, - ProblemAnalysisQueue.field_name.ANALYSIS, - ProblemAnalysisQueue.field_name.IS_ANALYZING, - 'get_is_analyzed', - ProblemAnalysisQueue.field_name.CREATED_AT, + models.ProblemAnalysisTag.field_name.ANALYSIS, + models.ProblemAnalysisTag.field_name.TAG, ] search_fields = [ - ProblemAnalysisQueue.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + models.ProblemAnalysisTag.field_name.ANALYSIS+'__'+models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.NAME_KO, + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.NAME_EN, ] - ordering = [ - ProblemAnalysisQueue.field_name.CREATED_AT, - ] - - @admin.display(description='Is analyzed', boolean=True) - def get_is_analyzed(self, obj: ProblemAnalysisQueue) -> bool: - return obj.analysis is not None + ordering = [models.ProblemAnalysisTag.field_name.ANALYSIS] -@admin.register(ProblemTag) +@admin.register(models.ProblemTag) class ProblemTagModelAdmin(admin.ModelAdmin): list_display = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, - ProblemTag.field_name.PARENT, + models.ProblemTag.field_name.KEY, + models.ProblemTag.field_name.NAME_KO, + models.ProblemTag.field_name.NAME_EN, + ] + search_fields = [ + models.ProblemTag.field_name.KEY, + models.ProblemTag.field_name.NAME_KO, + models.ProblemTag.field_name.NAME_EN, + ] + ordering = [models.ProblemTag.field_name.KEY] + + +@admin.register(models.ProblemTagRelation) +class ProblemTagRelationModelAdmin(admin.ModelAdmin): + list_display = [ + models.ProblemTagRelation.field_name.PARENT, + models.ProblemTagRelation.field_name.CHILD, ] search_fields = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, + models.ProblemTagRelation.field_name.PARENT, + models.ProblemTagRelation.field_name.CHILD, ] - ordering = [ProblemTag.field_name.KEY] + ordering = [models.ProblemTagRelation.field_name.PARENT] diff --git a/app/problems/dto/__init__.py b/app/problems/dto/__init__.py new file mode 100644 index 0000000..689e80e --- /dev/null +++ b/app/problems/dto/__init__.py @@ -0,0 +1,23 @@ +from dataclasses import dataclass +from dataclasses import field +from typing import Tuple + +from problems import models + + +@dataclass +class ProblemDTO: + title: str + description: str + input_description: str + output_description: str + memory_limit: float + time_limit: float + + +@dataclass(frozen=True) +class ProblemAnalysisDTO: + time_complexity: str + difficulty: models.ProblemDifficultyChoices + tags: Tuple[str] = field(default_factory=tuple) + hint: Tuple[str] = field(default_factory=tuple) diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py index 844b869..9923136 100644 --- a/app/problems/models/__init__.py +++ b/app/problems/models/__init__.py @@ -1,21 +1,16 @@ -from problems.models.dto import ProblemDTO, ProblemAnalysisDTO - +from problems.models.choices import ProblemDifficultyChoices from problems.models.problem import Problem from problems.models.problem_analysis import ProblemAnalysis -from problems.models.problem_analysis_queue import ProblemAnalysisQueue +from problems.models.problem_analysis_tag import ProblemAnalysisTag from problems.models.problem_tag import ProblemTag - -from problems.models.choices import ProblemDifficultyChoices +from problems.models.problem_tag_relation import ProblemTagRelation __all__ = ( - 'ProblemDTO', - 'ProblemAnalysisDTO', - 'Problem', 'ProblemAnalysis', - 'ProblemAnalysisQueue', - 'ProblemTag', - + 'ProblemAnalysisTag', 'ProblemDifficultyChoices', + 'ProblemTag', + 'ProblemTagRelation', ) diff --git a/app/problems/models/dto.py b/app/problems/models/dto.py deleted file mode 100644 index 27333d6..0000000 --- a/app/problems/models/dto.py +++ /dev/null @@ -1,20 +0,0 @@ -from dataclasses import dataclass -from typing import List - - -@dataclass -class ProblemDTO: - title: str - description: str - input_description: str - output_description: str - memory_limit: float - time_limit: float - - -@dataclass -class ProblemAnalysisDTO: - time_complexity: str - difficulty: str - tags: List[str] - hint: List[str] diff --git a/app/problems/models/problem_analysis.py b/app/problems/models/problem_analysis.py index 5da2910..f8ac924 100644 --- a/app/problems/models/problem_analysis.py +++ b/app/problems/models/problem_analysis.py @@ -1,16 +1,10 @@ -from typing import TYPE_CHECKING - from django.db import models from problems.models.choices import ProblemDifficultyChoices from problems.models.problem import Problem -from problems.models.problem_tag import ProblemTag class ProblemAnalysis(models.Model): - if TYPE_CHECKING: - tags: models.ManyToManyField[ProblemTag] - problem = models.OneToOneField( Problem, on_delete=models.CASCADE, @@ -20,10 +14,6 @@ class ProblemAnalysis(models.Model): help_text='문제 난이도를 입력해주세요.', choices=ProblemDifficultyChoices.choices, ) - tags = models.ManyToManyField( - ProblemTag, - help_text='문제의 DSA 태그를 입력해주세요.', - ) time_complexity = models.CharField( max_length=100, help_text=( @@ -53,7 +43,9 @@ class field_name: CREATED_AT = 'created_at' class Meta: - verbose_name_plural = 'Problem Analyses' + verbose_name_plural = 'Problem analyses' + ordering = ['-created_at'] + get_latest_by = ['created_at'] def __str__(self): return f'[Analyse of {self.problem}]' \ No newline at end of file diff --git a/app/problems/models/problem_analysis_queue.py b/app/problems/models/problem_analysis_queue.py deleted file mode 100644 index 86cca69..0000000 --- a/app/problems/models/problem_analysis_queue.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db import models - -from problems.models.problem import Problem -from problems.models.problem_analysis import ProblemAnalysis - - -class ProblemAnalysisQueue(models.Model): - problem = models.ForeignKey( - Problem, - on_delete=models.CASCADE, - help_text='문제를 입력해주세요.', - ) - analysis = models.OneToOneField( - ProblemAnalysis, - on_delete=models.SET_NULL, - blank=True, - null=True, - help_text='문제 분석 결과를 입력해주세요.', - ) - is_analyzing = models.BooleanField( - default=False, - help_text='문제 분석이 완료되었는지 여부를 입력해주세요.', - ) - created_at = models.DateTimeField(auto_now_add=True) - - class field_name: - PROBLEM = 'problem' - ANALYSIS = 'analysis' - IS_ANALYZING = 'is_analyzing' - CREATED_AT = 'created_at' - - class Meta: - ordering = ['created_at'] diff --git a/app/problems/models/problem_analysis_tag.py b/app/problems/models/problem_analysis_tag.py new file mode 100644 index 0000000..5c8a347 --- /dev/null +++ b/app/problems/models/problem_analysis_tag.py @@ -0,0 +1,27 @@ +from django.db import models + +from problems.models.problem_analysis import ProblemAnalysis +from problems.models.problem_tag import ProblemTag + + +class ProblemAnalysisTag(models.Model): + analysis = models.ForeignKey( + ProblemAnalysis, + on_delete=models.CASCADE, + null=False, + blank=False, + ) + tag = models.ForeignKey( + ProblemTag, + on_delete=models.PROTECT, + null=False, + blank=False, + help_text='문제의 DSA 태그를 입력해주세요.', + ) + + class field_name: + ANALYSIS = 'analysis' + TAG = 'tag' + + def __str__(self): + return f'{self.analysis.problem} #{self.tag}' diff --git a/app/problems/models/problem_tag.py b/app/problems/models/problem_tag.py index 2f49f3e..a6cef71 100644 --- a/app/problems/models/problem_tag.py +++ b/app/problems/models/problem_tag.py @@ -2,13 +2,6 @@ class ProblemTag(models.Model): - parent = models.ForeignKey( - 'self', - on_delete=models.CASCADE, - help_text='부모 알고리즘 태그를 입력해주세요.', - null=True, - blank=True, - ) key = models.CharField( max_length=50, unique=True, @@ -26,7 +19,6 @@ class ProblemTag(models.Model): ) class field_name: - PARENT = 'parent' KEY = 'key' NAME_KO = 'name_ko' NAME_EN = 'name_en' diff --git a/app/problems/models/problem_tag_relation.py b/app/problems/models/problem_tag_relation.py new file mode 100644 index 0000000..53238b6 --- /dev/null +++ b/app/problems/models/problem_tag_relation.py @@ -0,0 +1,23 @@ +from django.db import models + +from problems.models.problem_tag import ProblemTag + + +class ProblemTagRelation(models.Model): + parent = models.ForeignKey( + ProblemTag, + on_delete=models.CASCADE, + related_name='parent' + ) + child = models.ForeignKey( + ProblemTag, + on_delete=models.CASCADE, + related_name='child' + ) + + class field_name: + PARENT = 'parent' + CHILD = 'child' + + def __str__(self) -> str: + return f'{self.pk} : #{self.parent.key} <- #{self.child.key}' diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index 3947048..544ca27 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -1,12 +1,13 @@ -from rest_framework.serializers import ModelSerializer +from rest_framework import serializers from problems.constants import Unit -from problems.models import Problem, ProblemDifficultyChoices, ProblemTag -from problems.serializers.mixins import ReadOnlyFieldMixin, AnalysisMixin +from problems import models +from problems import services -class MemoryLimitField(ReadOnlyFieldMixin): - def to_representation(self, problem: Problem): +class MemoryLimitField(serializers.SerializerMethodField): + def to_representation(self, problem: models.Problem): + assert isinstance(problem, models.Problem) return { "value": problem.memory_limit_megabyte, "unit": { @@ -17,8 +18,9 @@ def to_representation(self, problem: Problem): } -class TimeLimitField(ReadOnlyFieldMixin): - def to_representation(self, problem: Problem): +class TimeLimitField(serializers.SerializerMethodField): + def to_representation(self, problem: models.Problem): + assert isinstance(problem, models.Problem) return { "value": problem.time_limit_second, "unit": { @@ -29,60 +31,50 @@ def to_representation(self, problem: Problem): } -class DifficultyField(ReadOnlyFieldMixin, AnalysisMixin): - def to_representation(self, problem: Problem): - if (analysis := self.get_analysis(problem)) is None: - difficulty = ProblemDifficultyChoices.UNDER_ANALYSIS - else: - difficulty = ProblemDifficultyChoices(analysis.difficulty) +class DifficultyField(serializers.SerializerMethodField): + def to_representation(self, problem: models.Problem): + assert isinstance(problem, models.Problem) + analysis = services.get_analysis(problem) return { - "name_ko": difficulty.get_name(lang='ko'), - "name_en": difficulty.get_name(lang='en'), - 'value': difficulty.value, + "name_ko": analysis.difficulty.get_name(lang='ko'), + "name_en": analysis.difficulty.get_name(lang='en'), + 'value': analysis.difficulty.value, } -class AnalysisField(ReadOnlyFieldMixin, AnalysisMixin): - def to_representation(self, problem: Problem): - if (analysis := self.get_analysis(problem)) is None: - difficulty = ProblemDifficultyChoices.UNDER_ANALYSIS +class AnalysisField(serializers.SerializerMethodField): + def to_representation(self, problem: models.Problem): + assert isinstance(problem, models.Problem) + analysis = services.get_analysis(problem) + is_analyzed = analysis.difficulty != models.ProblemDifficultyChoices.UNDER_ANALYSIS + tags_queryset = models.ProblemTag.objects.filter(**{ + models.ProblemTag.field_name.KEY+'__in': analysis.tags, + }) + if not is_analyzed: difficulty_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - time_complexity = '' time_complexity_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - hint = [] - tags = [] - is_analyzed = False else: - difficulty = ProblemDifficultyChoices(analysis.difficulty) difficulty_description = "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" - time_complexity = analysis.time_complexity time_complexity_description = "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" - hint = analysis.hint - tags = ProblemTagSerializer(analysis.tags, many=True).data - is_analyzed = True return { 'difficulty': { - "name_ko": difficulty.get_name(lang='ko'), - "name_en": difficulty.get_name(lang='en'), - 'value': difficulty.value, + "name_ko": analysis.difficulty.get_name(lang='ko'), + "name_en": analysis.difficulty.get_name(lang='en'), + 'value': analysis.difficulty.value, 'description': difficulty_description, }, 'time_complexity': { - 'value': time_complexity, + 'value': analysis.time_complexity, 'description': time_complexity_description, }, - 'hint': hint, - 'tags': tags, + 'hint': analysis.hint, + 'tags': [ + { + 'key': tag.key, + 'name_en': tag.name_en, + 'name_ko': tag.name_ko, + } + for tag in tags_queryset + ], 'is_analyzed': is_analyzed, } - - -class ProblemTagSerializer(ModelSerializer): - class Meta: - model = ProblemTag - fields = [ - ProblemTag.field_name.KEY, - ProblemTag.field_name.NAME_KO, - ProblemTag.field_name.NAME_EN, - ] - read_only_fields = ['__all__'] diff --git a/app/problems/serializers/mixins.py b/app/problems/serializers/mixins.py deleted file mode 100644 index f161fba..0000000 --- a/app/problems/serializers/mixins.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Optional - -from rest_framework.exceptions import PermissionDenied -from rest_framework.serializers import Field - -from problems.models import Problem, ProblemAnalysis - - -class ReadOnlySerializerMixin: - def create(self, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def update(self, instance, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def save(self, **kwargs): - raise PermissionDenied('Cannot update user through this serializer') - - -class ReadOnlyFieldMixin(Field): - def get_attribute(self, instance): - return instance - - def to_internal_value(self, data): - raise PermissionDenied('This field is read-only') - - -class AnalysisMixin: - def get_analysis(self, problem: Problem) -> Optional[ProblemAnalysis]: - try: - return ProblemAnalysis.objects.filter(**{ - ProblemAnalysis.field_name.PROBLEM: problem, - }).last() - except ProblemAnalysis.DoesNotExist: - return None diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py index e69de29..4d22485 100644 --- a/app/problems/services/__init__.py +++ b/app/problems/services/__init__.py @@ -0,0 +1,28 @@ +from problems import dto +from problems import models + + +def get_analysis(problem: models.Problem) -> dto.ProblemAnalysisDTO: + queryset = models.ProblemAnalysis.objects.filter(**{ + models.ProblemAnalysis.field_name.PROBLEM: problem + }) + try: + analysis = queryset.latest() + tag_keys = models.ProblemAnalysisTag.objects.filter(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, + }).values_list( + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, + flat=True, + ) + except models.ProblemAnalysis.DoesNotExist: + return dto.ProblemAnalysisDTO( + time_complexity='', + difficulty=models.ProblemDifficultyChoices.UNDER_ANALYSIS, + ) + else: + return dto.ProblemAnalysisDTO( + time_complexity=analysis.time_complexity, + difficulty=models.ProblemDifficultyChoices(analysis.difficulty), + hint=tuple(analysis.hint), + tags=tuple(tag_keys), + ) diff --git a/app/problems/services/analysis/__init__.py b/app/problems/services/analysis/__init__.py index cfc978a..0602a0f 100644 --- a/app/problems/services/analysis/__init__.py +++ b/app/problems/services/analysis/__init__.py @@ -1,15 +1,10 @@ from __future__ import annotations -from problems.models import Problem, ProblemAnalysis, ProblemDTO +from problems import dto from problems.services.analysis.analyser import ProblemAnalyser from problems.services.analysis.llm.gemini import GeminiProblemAnalyser -__all__ = ( - 'ProblemAnalyser', - 'get_analyser', -) - class AnalysingService: instance = None @@ -24,21 +19,6 @@ def get_instance(cls) -> AnalysingService: def get_analyzer(self) -> ProblemAnalyser: return self.analyzer_class() - def analyze(self, problem: Problem) -> ProblemAnalysis: - problem_dto = ProblemDTO( - title=problem.title, - description=problem.description, - input_description=problem.input_description, - output_description=problem.output_description, - memory_limit_megabyte=problem.memory_limit_megabyte, - time_limit_second=problem.time_limit_second, - ) + def analyze(self, problem: dto.ProblemDTO) -> dto.ProblemAnalysisDTO: analyzer = self.get_analyzer() - analysis_dto = analyzer.analyze(problem_dto) - return ProblemAnalysis(**{ - ProblemAnalysis.field_name.PROBLEM: problem, - ProblemAnalysis.field_name.DIFFICULTY: analysis_dto.difficulty, - ProblemAnalysis.field_name.TAGS: analysis_dto.tags, - ProblemAnalysis.field_name.TIME_COMPLEXITY: analysis_dto.time_complexity, - ProblemAnalysis.field_name.HINT: analysis_dto.hint, - }) + return analyzer.analyze(problem) diff --git a/app/problems/services/analysis/analyser.py b/app/problems/services/analysis/analyser.py index 1110321..e3e01a2 100644 --- a/app/problems/services/analysis/analyser.py +++ b/app/problems/services/analysis/analyser.py @@ -1,7 +1,4 @@ -from problems.models import ( - ProblemDTO, - ProblemAnalysisDTO, -) +from problems import dto class ProblemAnalyser: @@ -10,5 +7,5 @@ class ProblemAnalyser: 문제 데이터를 받아와 문제의 분석 결과를 반환하는 analyze() 메소드를 구현해야 합니다. """ - def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: + def analyze(self, problem: dto.ProblemDTO) -> dto.ProblemAnalysisDTO: raise NotImplementedError From 6cd6c9907ead9a8e7a8ae0bb0ebeba3fb8b18b33 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 15:15:59 +0900 Subject: [PATCH 362/552] =?UTF-8?q?feat(crews):=20=EC=96=B8=EC=96=B4?= =?UTF-8?q?=EC=97=90=20=EC=9E=90=EB=B0=94=EC=8A=A4=ED=81=AC=EB=A6=BD?= =?UTF-8?q?=ED=8A=B8=EA=B0=80=20=EB=88=84=EB=9D=BD=EB=90=9C=20=EA=B2=83=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models/choices.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/crews/models/choices.py b/app/crews/models/choices.py index 2d1f5ee..da3f1ce 100644 --- a/app/crews/models/choices.py +++ b/app/crews/models/choices.py @@ -11,5 +11,6 @@ class ProgrammingLanguageChoices(models.TextChoices): SWIFT = enums.ProgrammingLanguage.SWIFT.to_choice() CPP = enums.ProgrammingLanguage.CPP.to_choice() JAVA = enums.ProgrammingLanguage.JAVA.to_choice() + JAVASCRIPT = enums.ProgrammingLanguage.JAVASCRIPT.to_choice() PYTHON = enums.ProgrammingLanguage.PYTHON.to_choice() C = enums.ProgrammingLanguage.C.to_choice() From a67c0d5d1529204d78f420b65b3f766304861e98 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 15:21:21 +0900 Subject: [PATCH 363/552] =?UTF-8?q?fix(crews):=20`=5Fget=5Flanguage=5Ftags?= =?UTF-8?q?()`=EC=9D=98=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/services/__init__.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index 2b6cecf..3db8af5 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -6,6 +6,7 @@ from django.utils import timezone from crews import dto +from crews import enums from crews import models from problems.models import ProblemAnalysis from problems.models import ProblemDifficultyChoices @@ -14,6 +15,7 @@ def problem_statistics(crew: models.Crew) -> dto.ProblemStatistic: + assert isinstance(crew, models.Crew) statistics = dto.ProblemStatistic() queryset = models.CrewActivityProblem.objects.filter(**{ models.CrewActivityProblem.field_name.CREW: crew, @@ -75,14 +77,15 @@ def crew_tags(crew: models.Crew) -> List[dto.CrewTag]: def _get_language_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: - queryset = models.CrewSubmittableLanguage.objects.filter(**{ + submittable_languages = models.CrewSubmittableLanguage.objects.filter(**{ models.CrewSubmittableLanguage.field_name.CREW: crew, }) - for language in queryset.all(): + for submittable_language in submittable_languages.all(): + programming_language = models.ProgrammingLanguageChoices(submittable_language) yield dto.CrewTag( - key=language.key, - name=language.name, - type=dto.CrewTagType.LANGUAGE, + key=programming_language.key, + name=programming_language.name, + type=enums.CrewTagType.LANGUAGE, ) From b2c1441bf06b2417759fc4ff2b48bce370588b69 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 15:33:39 +0900 Subject: [PATCH 364/552] refactor(problems): refactor imports --- app/config/urls.py | 6 +-- app/problems/serializers/__init__.py | 67 +++++++++++++--------------- app/problems/serializers/fields.py | 2 +- app/problems/views/__init__.py | 50 ++++++--------------- 4 files changed, 47 insertions(+), 78 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index be79d3a..f87b047 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -37,9 +37,9 @@ path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), path("crews/my", crews.views.MyCrewAPIView.as_view()), path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), - path("problems/", problems.views.ProblemCreate.as_view()), - path("problems/search", problems.views.ProblemSearch.as_view()), - path("problems//detail", problems.views.ProblemDetail.as_view()), + path("problems/", problems.views.ProblemCreateAPIView.as_view()), + path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), + path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py index a0ebf52..dc6f494 100644 --- a/app/problems/serializers/__init__.py +++ b/app/problems/serializers/__init__.py @@ -1,70 +1,63 @@ -from rest_framework.serializers import CurrentUserDefault, ModelSerializer +from rest_framework import serializers -from problems.models import Problem -from problems.serializers.fields import ( - AnalysisField, - MemoryLimitField, - TimeLimitField, - DifficultyField, -) -from problems.serializers.mixins import ReadOnlySerializerMixin +from problems import models +from problems.serializers import fields from users.serializers import UserMinimalSerializer -class ProblemDetailSerializer(ModelSerializer): - analysis = AnalysisField(read_only=True) - memory_limit = MemoryLimitField(read_only=True) - time_limit = TimeLimitField(read_only=True) +class ProblemDetailSerializer(serializers.ModelSerializer): + analysis = fields.AnalysisField(read_only=True) + memory_limit = fields.MemoryLimitField(read_only=True) + time_limit = fields.TimeLimitField(read_only=True) created_by = UserMinimalSerializer(read_only=True) class Meta: - model = Problem + model = models.Problem fields = [ 'id', 'analysis', 'memory_limit', 'time_limit', - Problem.field_name.TITLE, - Problem.field_name.LINK, - Problem.field_name.DESCRIPTION, - Problem.field_name.INPUT_DESCRIPTION, - Problem.field_name.OUTPUT_DESCRIPTION, - Problem.field_name.MEMORY_LIMIT_MEGABYTE, - Problem.field_name.TIME_LIMIT_SECOND, - Problem.field_name.CREATED_AT, - Problem.field_name.CREATED_BY, - Problem.field_name.UPDATED_AT, + models.Problem.field_name.TITLE, + models.Problem.field_name.LINK, + models.Problem.field_name.DESCRIPTION, + models.Problem.field_name.INPUT_DESCRIPTION, + models.Problem.field_name.OUTPUT_DESCRIPTION, + models.Problem.field_name.MEMORY_LIMIT_MEGABYTE, + models.Problem.field_name.TIME_LIMIT_SECOND, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.CREATED_BY, + models.Problem.field_name.UPDATED_AT, ] read_only_fields = [ 'id', 'analysis', 'memory_limit', 'time_limit', - Problem.field_name.CREATED_AT, - Problem.field_name.CREATED_BY, - Problem.field_name.UPDATED_AT, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.CREATED_BY, + models.Problem.field_name.UPDATED_AT, ] extra_kwargs = { - Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, - Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, + models.Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, + models.Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, } def create(self, validated_data): - validated_data[Problem.field_name.CREATED_BY] = CurrentUserDefault()( - self) + validated_data[models.Problem.field_name.CREATED_BY] = serializers.CurrentUserDefault()(self) return super().create(validated_data) -class ProblemMinimalSerializer(ModelSerializer, ReadOnlySerializerMixin): - difficulty = DifficultyField(read_only=True) +class ProblemMinimalSerializer(serializers.ModelSerializer): + difficulty = fields.DifficultyField(read_only=True) class Meta: - model = Problem + model = models.Problem fields = [ 'id', - Problem.field_name.TITLE, + models.Problem.field_name.TITLE, 'difficulty', - Problem.field_name.CREATED_AT, - Problem.field_name.UPDATED_AT, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.UPDATED_AT, ] read_only_fields = ['__all__'] diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index 544ca27..0a8c33d 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -1,8 +1,8 @@ from rest_framework import serializers -from problems.constants import Unit from problems import models from problems import services +from problems.constants import Unit class MemoryLimitField(serializers.SerializerMethodField): diff --git a/app/problems/views/__init__.py b/app/problems/views/__init__.py index 673a55b..7852f4a 100644 --- a/app/problems/views/__init__.py +++ b/app/problems/views/__init__.py @@ -1,58 +1,34 @@ -from rest_framework import mixins +from rest_framework import generics from rest_framework import permissions -from rest_framework.generics import GenericAPIView -from problems.models import Problem -from problems.serializers import ProblemDetailSerializer, ProblemMinimalSerializer +from problems import models +from problems import serializers -class ProblemCreate(mixins.CreateModelMixin, - GenericAPIView): +class ProblemCreateAPIView(generics.CreateAPIView): """문제 생성 API""" - queryset = Problem.objects.all() + queryset = models.Problem.objects.all() permission_classes = [permissions.IsAuthenticated] - serializer_class = ProblemDetailSerializer + serializer_class = serializers.ProblemDetailSerializer - def post(self, request, *args, **kwargs): - return self.create(request, *args, **kwargs) - -class ProblemDetail(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - GenericAPIView): +class ProblemDetailRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): """문제 상세 조회, 수정, 삭제 API""" - queryset = Problem.objects.all() + queryset = models.Problem.objects.all() permission_classes = [permissions.IsAuthenticated] - serializer_class = ProblemDetailSerializer + serializer_class = serializers.ProblemDetailSerializer lookup_field = 'id' - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def put(self, request, *args, **kwargs): - return self.update(request, *args, **kwargs) - - def patch(self, request, *args, **kwargs): - return self.partial_update(request, *args, **kwargs) - def delete(self, request, *args, **kwargs): - return self.destroy(request, *args, **kwargs) - - -class ProblemSearch(mixins.ListModelMixin, - GenericAPIView): +class ProblemSearchListAPIView(generics.ListAPIView): """문제 검색 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = ProblemMinimalSerializer + serializer_class = serializers.ProblemMinimalSerializer def get_queryset(self): - return Problem.objects.filter(**{ - Problem.field_name.CREATED_BY: self.request.user, + return models.Problem.objects.filter(**{ + models.Problem.field_name.CREATED_BY: self.request.user, }) - - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) From a6d34a70ad175be62eafda1ccbf86dfd492d4790 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 16:43:27 +0900 Subject: [PATCH 365/552] =?UTF-8?q?refactor(crews):=20service=20=EB=B0=8F?= =?UTF-8?q?=20enums=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 2 + app/crews/admin.py | 115 ++++---- app/crews/dto/__init__.py | 29 ++- app/crews/enums/__init__.py | 36 +-- app/crews/models/__init__.py | 4 - app/crews/models/choices.py | 16 -- app/crews/models/crew_activity.py | 20 -- app/crews/models/crew_activity_submission.py | 51 ++++ app/crews/models/crew_submittable_language.py | 4 +- app/crews/serializers/__init__.py | 9 +- app/crews/serializers/fields.py | 64 ++--- app/crews/services/__init__.py | 246 +++++++++--------- app/crews/views/__init__.py | 42 ++- app/submissions/models/submission.py | 3 +- 14 files changed, 359 insertions(+), 282 deletions(-) delete mode 100644 app/crews/models/choices.py create mode 100644 app/crews/models/crew_activity_submission.py diff --git a/app/config/urls.py b/app/config/urls.py index f87b047..de54cdd 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -37,6 +37,8 @@ path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), path("crews/my", crews.views.MyCrewAPIView.as_view()), path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), + path("crews//statistics", crews.views.CrewStatisticsAPIView.as_view()), + path("crews//activity", crews.views.CrewDashboardAPIView.as_view()), path("problems/", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), diff --git a/app/crews/admin.py b/app/crews/admin.py index 9fe56ce..245c675 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -1,23 +1,18 @@ from django.contrib import admin +from django.utils import timezone -from crews.models import ( - Crew, - CrewActivity, - CrewActivityProblem, - CrewApplicant, - CrewMember, - CrewSubmittableLanguage, -) +from crews import models +from crews import services from users.models import User admin.site.register([ - CrewActivityProblem, - CrewApplicant, + models.CrewActivityProblem, + models.CrewApplicant, ]) -@admin.register(Crew) +@admin.register(models.Crew) class CrewModelAdmin(admin.ModelAdmin): list_display = [ 'get_display_name', @@ -25,93 +20,105 @@ class CrewModelAdmin(admin.ModelAdmin): 'get_members', 'get_applicants', 'get_activities', - Crew.field_name.IS_ACTIVE, - Crew.field_name.IS_RECRUITING, - Crew.field_name.CREATED_AT, + models.Crew.field_name.IS_ACTIVE, + models.Crew.field_name.IS_RECRUITING, + models.Crew.field_name.CREATED_AT, ] search_fields = [ - Crew.field_name.NAME, - Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + models.Crew.field_name.ICON, ] @admin.display(description='Display Name') - def get_display_name(self, crew: Crew): + def get_display_name(self, crew: models.Crew): return f'{crew.icon} {crew.name}' @admin.display(description='Captain') - def get_captain(self, obj: Crew): - return CrewMember.objects.get(**{ - CrewMember.field_name.CREW: obj, - CrewMember.field_name.IS_CAPTAIN: True, + def get_captain(self, obj: models.Crew): + return models.CrewMember.objects.get(**{ + models.CrewMember.field_name.CREW: obj, + models.CrewMember.field_name.IS_CAPTAIN: True, }) @admin.display(description='Members') - def get_members(self, crew: Crew): - members_count = CrewMember.objects.filter(**{ - CrewMember.field_name.CREW: crew, + def get_members(self, crew: models.Crew): + members_count = models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, }).count() return f'{members_count} / {crew.max_members}' @admin.display(description='Applicants') - def get_applicants(self, obj: Crew): - return CrewApplicant.objects.filter(**{ - CrewApplicant.field_name.CREW: obj, + def get_applicants(self, obj: models.Crew): + return models.CrewApplicant.objects.filter(**{ + models.CrewApplicant.field_name.CREW: obj, }).count() @admin.display(description='Activities') - def get_activities(self, obj: Crew): - return CrewActivity.objects.filter(**{ - CrewActivity.field_name.CREW: obj, + def get_activities(self, obj: models.Crew): + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: obj, }).count() -@admin.register(CrewMember) +@admin.register(models.CrewMember) class CrewMemberModelAdmin(admin.ModelAdmin): list_display = [ - CrewMember.field_name.USER, - CrewMember.field_name.CREW, - CrewMember.field_name.IS_CAPTAIN, - CrewMember.field_name.CREATED_AT, + models.CrewMember.field_name.USER, + models.CrewMember.field_name.CREW, + models.CrewMember.field_name.IS_CAPTAIN, + models.CrewMember.field_name.CREATED_AT, ] search_fields = [ - CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, - CrewMember.field_name.USER+'__'+User.field_name.USERNAME, + models.CrewMember.field_name.CREW+'__'+models.Crew.field_name.NAME, + models.CrewMember.field_name.USER+'__'+User.field_name.USERNAME, ] ordering = [ - CrewMember.field_name.CREW, - CrewMember.field_name.IS_CAPTAIN, + models.CrewMember.field_name.CREW, + models.CrewMember.field_name.IS_CAPTAIN, ] -@admin.register(CrewActivity) +@admin.register(models.CrewActivity) class CrewActivityModelAdmin(admin.ModelAdmin): list_display = [ - CrewActivity.field_name.CREW, - CrewActivity.field_name.NAME, - CrewActivity.field_name.START_AT, - CrewActivity.field_name.END_AT, + models.CrewActivity.field_name.CREW, + models.CrewActivity.field_name.NAME, + models.CrewActivity.field_name.START_AT, + models.CrewActivity.field_name.END_AT, 'nth', 'is_opened', 'is_closed', ] search_fields = [ - CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, - CrewActivity.field_name.NAME, + models.CrewActivity.field_name.CREW+'__'+models.Crew.field_name.NAME, + models.CrewActivity.field_name.NAME, ] + @admin.display(boolean=True, description='Is Opened') + def is_opened(self, obj: models.CrewActivity) -> bool: + return services.crew_acitivity.is_opened(obj) -@admin.register(CrewSubmittableLanguage) + @admin.display(boolean=True, description='Is Closed') + def is_closed(self, obj: models.CrewActivity) -> bool: + return services.crew_acitivity.is_closed(obj) + + @admin.display(description='N-th') + def nth(self, obj: models.CrewActivity) -> int: + return services.crew_acitivity.number(obj) + + +@admin.register(models.CrewSubmittableLanguage) class CrewSubmittableLanguageModelAdmin(admin.ModelAdmin): list_display = [ - CrewSubmittableLanguage.field_name.CREW, - CrewSubmittableLanguage.field_name.LANGUAGE, + models.CrewSubmittableLanguage.field_name.CREW, + models.CrewSubmittableLanguage.field_name.LANGUAGE, ] search_fields = [ - CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, - CrewSubmittableLanguage.field_name.LANGUAGE, + models.CrewActivity.field_name.CREW+'__'+models.Crew.field_name.NAME, + models.CrewSubmittableLanguage.field_name.LANGUAGE, ] ordering = [ - CrewSubmittableLanguage.field_name.CREW, - CrewSubmittableLanguage.field_name.LANGUAGE, + models.CrewSubmittableLanguage.field_name.CREW, + models.CrewSubmittableLanguage.field_name.LANGUAGE, ] diff --git a/app/crews/dto/__init__.py b/app/crews/dto/__init__.py index 89a27e4..0c0f75a 100644 --- a/app/crews/dto/__init__.py +++ b/app/crews/dto/__init__.py @@ -1,11 +1,11 @@ from collections import Counter from dataclasses import dataclass from dataclasses import field -from typing import Counter +from datetime import datetime from typing import List - -from crews.enums import CrewTagType +from crews import enums +from problems.models import ProblemDifficultyChoices @dataclass @@ -29,9 +29,28 @@ class ProblemStatistic: class CrewTag: key: str name: str - type: CrewTagType + type: enums.CrewTagType @dataclass class CrewProblem: - id: int + problem_number: int + problem_id: int + problem_title: str + problem_difficulty: ProblemDifficultyChoices + is_submitted: bool + last_submitted_date: datetime + + +@dataclass +class SubmissionGraphNode: + problem_number: int + submitted_at: datetime + is_accepted: bool # 정답인지 여부 + + +@dataclass +class SubmissionGraph: + user_username: str + user_profile_image: str + submissions: List[SubmissionGraphNode] diff --git a/app/crews/enums/__init__.py b/app/crews/enums/__init__.py index 90aaa56..d52fb9f 100644 --- a/app/crews/enums/__init__.py +++ b/app/crews/enums/__init__.py @@ -1,6 +1,7 @@ -from dataclasses import dataclass from enum import Enum +from django.db import models + from crews.enums.emoji import Emoji @@ -17,27 +18,18 @@ class CrewTagType(Enum): CUSTOM = 'custom' -class ProgrammingLanguage(Enum): - @dataclass - class Item: - key: str - name: str - extension: str - +class ProgrammingLanguageChoices(models.TextChoices): # TLE에서 허용중인 언어 - NODE_JS = Item('nodejs', 'Node.js', '.js') - KOTLIN = Item('kotlin', 'Kotlin', '.kt') - SWIFT = Item('swift', 'Swift', '.swift') - CPP = Item('cpp', 'C++', '.cpp') - JAVA = Item('java', 'Java', '.java') - PYTHON = Item('python', 'Python', '.py') - C = Item('c', 'C', '.c') + NODE_JS = 'nodejs', 'Node.js' + KOTLIN = 'kotlin', 'Kotlin' + SWIFT = 'swift', 'Swift' + CPP = 'cpp', 'C++' + JAVA = 'java', 'Java' + PYTHON = 'python', 'Python' + C = 'c', 'C' + JAVASCRIPT = 'javascript', 'JavaScript' # 아직 지원하지 않는 언어 - JAVASCRIPT = Item('javascript', 'JavaScript', '.js') - CSHARP = Item('csharp', 'C#', '.cs') - RUBY = Item('ruby', 'Ruby', '.rb') - PHP = Item('php', 'PHP', '.php') - - def to_choice(self): - return self.value.key, self.value.name + CSHARP = 'csharp', 'C#' + RUBY = 'ruby', 'Ruby' + PHP = 'php', 'PHP' diff --git a/app/crews/models/__init__.py b/app/crews/models/__init__.py index d8a60d2..ad85f05 100644 --- a/app/crews/models/__init__.py +++ b/app/crews/models/__init__.py @@ -5,8 +5,6 @@ from crews.models.crew_member import CrewMember from crews.models.crew_submittable_language import CrewSubmittableLanguage -from crews.models.choices import ProgrammingLanguageChoices - __all__ = ( 'Crew', @@ -15,6 +13,4 @@ 'CrewApplicant', 'CrewMember', 'CrewSubmittableLanguage', - - 'ProgrammingLanguageChoices', ) diff --git a/app/crews/models/choices.py b/app/crews/models/choices.py deleted file mode 100644 index da3f1ce..0000000 --- a/app/crews/models/choices.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.db import models - -from crews import enums - - -class ProgrammingLanguageChoices(models.TextChoices): - """크루에서 사용 가능한 언어""" - - NODE_JS = enums.ProgrammingLanguage.NODE_JS.to_choice() - KOTLIN = enums.ProgrammingLanguage.KOTLIN.to_choice() - SWIFT = enums.ProgrammingLanguage.SWIFT.to_choice() - CPP = enums.ProgrammingLanguage.CPP.to_choice() - JAVA = enums.ProgrammingLanguage.JAVA.to_choice() - JAVASCRIPT = enums.ProgrammingLanguage.JAVASCRIPT.to_choice() - PYTHON = enums.ProgrammingLanguage.PYTHON.to_choice() - C = enums.ProgrammingLanguage.C.to_choice() diff --git a/app/crews/models/crew_activity.py b/app/crews/models/crew_activity.py index 0005819..3d2e299 100644 --- a/app/crews/models/crew_activity.py +++ b/app/crews/models/crew_activity.py @@ -42,23 +42,3 @@ def opened_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: """종료된 활동 목록을 반환합니다.""" return cls.objects.filter(crew=crew, end_at__lt=timezone.now()) - - @admin.display(boolean=True, description='Is Opend') - def is_opened(self) -> bool: - """활동이 진행 중인지 여부를 반환합니다.""" - return self.start_at <= timezone.now() <= self.end_at - - @admin.display(boolean=True, description='Is Closed') - def is_closed(self) -> bool: - """활동이 종료되었는지 여부를 반환합니다.""" - return self.end_at < timezone.now() - - @admin.display(description='Nth') - def nth(self) -> int: - """활동의 회차 번호를 반환합니다. - - 이 값은 1부터 시작합니다. - 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 - 더한 값을 반환하므로, 고정된 값이 아닙니다. - """ - return self.crew.activities.filter(start_at__lte=self.start_at).count() diff --git a/app/crews/models/crew_activity_submission.py b/app/crews/models/crew_activity_submission.py new file mode 100644 index 0000000..42c5fb4 --- /dev/null +++ b/app/crews/models/crew_activity_submission.py @@ -0,0 +1,51 @@ +from django.db import models + +from crews import enums +from crews.models.crew_activity_problem import CrewActivityProblem +from users.models import User + + +class CrewActivitySubmission(models.Model): + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 + problem = models.ForeignKey( + CrewActivityProblem, + on_delete=models.PROTECT, + help_text='활동 문제를 입력해주세요.', + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text='유저를 입력해주세요.', + ) + code = models.TextField( + help_text='유저의 코드를 입력해주세요.', + ) + language = models.TextField( + choices=enums.ProgrammingLanguageChoices.choices, + help_text='유저의 코드 언어를 입력해주세요.', + ) + is_correct = models.BooleanField( + help_text='유저의 코드가 정답인지 여부를 입력해주세요.', + ) + is_help_needed = models.BooleanField( + help_text='유저의 코드에 도움이 필요한지 여부를 입력해주세요.', + default=False, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class field_name: + PROBLEM = 'problem' + USER = 'user' + CODE = 'code' + LANGUAGE = 'language' + IS_CORRECT = 'is_correct' + IS_HELP_NEEDED = 'is_help_needed' + CREATED_AT = 'created_at' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['created_at'] + + def __str__(self) -> str: + return f'[{self.pk} : {self.problem} ← {self.user}]' diff --git a/app/crews/models/crew_submittable_language.py b/app/crews/models/crew_submittable_language.py index 6bd5059..c74dc2f 100644 --- a/app/crews/models/crew_submittable_language.py +++ b/app/crews/models/crew_submittable_language.py @@ -1,7 +1,7 @@ from django.db import models +from crews import enums from crews.models.crew import Crew -from crews.models.choices import ProgrammingLanguageChoices class CrewSubmittableLanguage(models.Model): @@ -10,7 +10,7 @@ class CrewSubmittableLanguage(models.Model): on_delete=models.CASCADE, ) language = models.TextField( - choices=ProgrammingLanguageChoices.choices, + choices=enums.ProgrammingLanguageChoices.choices, help_text='언어 키를 입력해주세요. (최대 20자)', ) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index f5527ea..a1cf781 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -46,7 +46,7 @@ class Meta: read_only_fields = ['__all__'] -class MyCrewDashboardSerializer(serializers.ModelSerializer): +class CrewDashboardSerializer(serializers.ModelSerializer): """크루 대시보드 - 공지사항 @@ -58,7 +58,6 @@ class MyCrewDashboardSerializer(serializers.ModelSerializer): tags = fields.CrewTagsField() members = fields.CrewMembersField() - statistics = fields.ProblemStatisticsField() activities = fields.CrewActivitiesField() class Meta: @@ -70,12 +69,16 @@ class Meta: models.Crew.field_name.NOTICE, 'tags', 'members', - 'statistics', 'activities', ] read_only_fields = ['__all__'] +class CrewStatisticsSerializer(serializers.Serializer): + difficulty = fields.ProblemStatisticsDifficultyField() + tags = fields.ProblemStatisticsTagsField() + + class MyCrewDashboardAcitivySerializer(serializers.ModelSerializer): problems = fields.CrewAcitivityProblemsField() diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index 1497d82..0b92d9b 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from crews import dto from crews import models from crews import services from crews import utils @@ -17,7 +18,7 @@ def to_representation(self, crew: models.Crew): "date_start_at": None, "date_end_at": None, } - queryset = services.crew_activities_queryset(crew) + queryset = services.crew_acitivity.of_crew(crew) try: activity = queryset.latest() except models.CrewActivity.DoesNotExist: @@ -59,7 +60,7 @@ class CrewMemberCountField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) return { - "count": services.crew_member_count(crew), + "count": services.crew.member_count(crew), "max_count": crew.max_members, } @@ -75,7 +76,7 @@ def to_representation(self, crew: models.Crew): 'name': tag.name, 'type': tag.type.value, } - for tag in services.crew_tags(crew) + for tag in services.crew.tags(crew) ] @@ -84,7 +85,7 @@ def to_representation(self, crew: models.Crew): user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) assert isinstance(user, User) - return services.crew_is_joinable(crew, user) + return services.crew.is_joinable(crew, user) class CrewIsMemberField(serializers.SerializerMethodField): @@ -92,13 +93,13 @@ def to_representation(self, crew: models.Crew): user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) assert isinstance(user, User) - return services.crew_is_member(crew, user) + return services.crew.is_member(crew, user) class CrewActivitiesField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - queryset = services.crew_activities_queryset(crew) + queryset = services.crew_acitivity.of_crew(crew) return [ { 'id': activity.pk, @@ -122,27 +123,30 @@ def to_representation(self, activity: models.CrewActivity): ] -class ProblemStatisticsField(serializers.SerializerMethodField): - def to_representation(self, crew: models.Crew): - statistics = services.problem_statistics(crew) - return { - 'difficulty': [ - { - 'difficulty': difficulty, - 'problem_count': count, - 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), - } - for difficulty, count in statistics.difficulty.items() - ], - 'tags': [ - { - 'label': { - 'ko': tag.name_ko, - 'en': tag.name_en, - }, - 'problem_count': count, - 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), - } - for tag, count in statistics.tags.items() - ], - } +class ProblemStatisticsDifficultyField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatistic): + assert isinstance(statistics, dto.ProblemStatistic) + return [ + { + 'difficulty': difficulty, + 'problem_count': count, + 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), + } + for difficulty, count in statistics.difficulty.items() + ] + + +class ProblemStatisticsTagsField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatistic): + assert isinstance(statistics, dto.ProblemStatistic) + return [ + { + 'label': { + 'ko': tag.name_ko, + 'en': tag.name_en, + }, + 'problem_count': count, + 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), + } + for tag, count in statistics.tags.items() + ] diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index 3db8af5..4cad22d 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -40,123 +40,137 @@ def problem_statistics(crew: models.Crew) -> dto.ProblemStatistic: return statistics -def crew_of_user_queryset(include_user: Optional[User] = None, - exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: - """특정 사용자가 속하거나 속하지 않은 크루 목록을 조회하는 쿼리를 반환한다.""" - queryset = models.Crew.objects - if include_user is not None: - queryset = queryset.filter(pk__in=models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: include_user, - }).values_list(models.CrewMember.field_name.CREW)) - if exclude_user is not None: - queryset = queryset.exclude(pk__in=models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: exclude_user, - }).values_list(models.CrewMember.field_name.CREW)) - return queryset - - -def crew_activities_queryset(crew: models.Crew, exclude_future=True) -> QuerySet[models.CrewActivity]: - """ - exclude_future: 아직 공개되지 않은 활동도 포함할 지 여부. - """ - kwargs = { - models.CrewActivity.field_name.CREW: crew, - } - if exclude_future: - kwargs[models.CrewActivity.field_name.START_AT + '__lte'] = timezone.now() - return models.CrewActivity.objects.filter(**kwargs).order_by(models.CrewActivity.field_name.START_AT) - - -def crew_tags(crew: models.Crew) -> List[dto.CrewTag]: - # 태그의 나열 순서는 리스트에 선언한 순서를 따름. - return [ - *_get_language_tags(crew), - *_get_level_tags(crew), - *_get_custom_tags(crew), - ] - - -def _get_language_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: - submittable_languages = models.CrewSubmittableLanguage.objects.filter(**{ - models.CrewSubmittableLanguage.field_name.CREW: crew, - }) - for submittable_language in submittable_languages.all(): - programming_language = models.ProgrammingLanguageChoices(submittable_language) - yield dto.CrewTag( - key=programming_language.key, - name=programming_language.name, - type=enums.CrewTagType.LANGUAGE, - ) - - -def _get_level_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: - yield dto.CrewTag( - key=None, - name=_get_bounded_level_name(UserBojLevelChoices(crew.min_boj_level)), - type=dto.CrewTagType.LEVEL, - ) - - -def _get_custom_tags(crew: models.Crew) -> Iterable[dto.CrewTag]: - for tag in crew.custom_tags: +class crew: + @staticmethod + def of_user_queryset(include_user: Optional[User] = None, + exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: + """특정 사용자가 속하거나 속하지 않은 크루 목록을 조회하는 쿼리를 반환한다.""" + queryset = models.Crew.objects + if include_user is not None: + queryset = queryset.filter(pk__in=models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: include_user, + }).values_list(models.CrewMember.field_name.CREW)) + if exclude_user is not None: + queryset = queryset.exclude(pk__in=models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: exclude_user, + }).values_list(models.CrewMember.field_name.CREW)) + return queryset + + @classmethod + def tags(cls, crew: models.Crew) -> List[dto.CrewTag]: + # 태그의 나열 순서는 리스트에 선언한 순서를 따름. + return [ + *cls._get_language_tags(crew), + *cls._get_level_tags(crew), + *cls._get_custom_tags(crew), + ] + + @classmethod + def _get_language_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: + submittable_languages = models.CrewSubmittableLanguage.objects.filter(**{ + models.CrewSubmittableLanguage.field_name.CREW: crew, + }) + for submittable_language in submittable_languages.all(): + programming_language = enums.ProgrammingLanguageChoices( + submittable_language.language) + yield dto.CrewTag( + key=programming_language.value, + name=programming_language.label, + type=enums.CrewTagType.LANGUAGE, + ) + + @classmethod + def _get_level_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: + if crew.min_boj_level is not None: + min_boj_level = UserBojLevelChoices(crew.min_boj_level) + else: + min_boj_level = UserBojLevelChoices.U + # 보여질 문구를 결정 + if min_boj_level == UserBojLevelChoices.U: + name = '티어 무관' + elif min_boj_level.get_tier() == 5: + name = f"{min_boj_level.get_division_name(lang='ko')} 이상" + else: + name = f"{min_boj_level.get_name(lang='ko', arabic=False)} 이상" yield dto.CrewTag( key=None, - name=tag, - type=dto.CrewTagType.CUSTOM, - ) - - -def _get_bounded_level_name(level: Optional[UserBojLevelChoices], - bound_tier: int = 5, - bound_msg: str = "이상", - default_msg: str = "티어 무관", - lang='ko', - arabic=False) -> str: - """level에 대한 백준 난이도 태그 이름을 반환한다. - - bound_tier는 해당 랭크(브론즈,실버,...)를 모두 아우르는 마지막 - 티어(1,2,3,4,5)를 의미한다. - - bound_msg는 "이상", 혹은 "이하"를 나타내는 제한 메시지이다. - - 만약 level의 티어가 bound_tier와 - 같다면 랭크만 출력하고, - 같지않다면 랭크와 티어 모두 출력한다. - - 메시지의 마지막에는 bound_msg를 출력한다. - """ - if level is None: - return default_msg - assert isinstance(level, UserBojLevelChoices) - if level.get_tier() == bound_tier: - return f'{level.get_division_name(lang=lang)} {bound_msg}' - else: - return f'{level.get_name(lang=lang, arabic=arabic)} {bound_msg}' - - -def crew_member_count(crew: models.Crew) -> int: - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - }).count() - - -def crew_is_joinable(crew: models.Crew, user: User) -> bool: - if not crew.is_recruiting: - return False - if crew_member_count(crew) >= crew.max_members: - return False - if crew_is_member(crew, user): - return False - if crew.min_boj_level is not None: - return bool( - (user.boj_level is not None) and - (user.boj_level >= crew.min_boj_level) + name=name, + type=enums.CrewTagType.LEVEL, ) - return True - -def crew_is_member(crew: models.Crew, user: User) -> bool: - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - models.CrewMember.field_name.USER: user, - }).exists() + @classmethod + def _get_custom_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: + for tag in crew.custom_tags: + yield dto.CrewTag( + key=None, + name=tag, + type=enums.CrewTagType.CUSTOM, + ) + + @staticmethod + def member_count(crew: models.Crew) -> int: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, + }).count() + + @classmethod + def is_joinable(cls, crew: models.Crew, user: User) -> bool: + if not crew.is_recruiting: + return False + if cls.member_count(crew) >= crew.max_members: + return False + if cls.is_member(crew, user): + return False + if crew.min_boj_level is not None: + return bool( + (user.boj_level is not None) and + (user.boj_level >= crew.min_boj_level) + ) + return True + + @staticmethod + def is_member(crew: models.Crew, user: User) -> bool: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: crew, + models.CrewMember.field_name.USER: user, + }).exists() + + +class crew_acitivity: + @staticmethod + def of_crew(crew: models.Crew, exclude_future=True) -> QuerySet[models.CrewActivity]: + """ + exclude_future: 아직 공개되지 않은 활동도 포함할 지 여부. + """ + kwargs = { + models.CrewActivity.field_name.CREW: crew, + } + if exclude_future: + kwargs[models.CrewActivity.field_name.START_AT + '__lte'] = timezone.now() + return models.CrewActivity.objects.filter(**kwargs).order_by(models.CrewActivity.field_name.START_AT) + + @staticmethod + def is_opened(activity: models.CrewActivity) -> bool: + """활동이 진행 중인지 여부를 반환합니다.""" + assert isinstance(activity, models.CrewActivity) + return activity.start_at <= timezone.now() <= activity.end_at + + @staticmethod + def is_closed(activity: models.CrewActivity) -> bool: + """활동이 종료되었는지 여부를 반환합니다.""" + assert isinstance(activity, models.CrewActivity) + return activity.end_at < timezone.now() + + @staticmethod + def number(activity: models.CrewActivity) -> int: + assert isinstance(activity, models.CrewActivity) + """활동의 회차 번호를 반환합니다. + + 이 값은 1부터 시작합니다. + 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 + 더한 값을 반환하므로, 고정된 값이 아닙니다. + """ + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: activity.crew, + models.CrewActivity.field_name.START_AT+'__lte': activity.start_at, + }).count() diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 9512f39..19b8353 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,10 +1,7 @@ -from drf_yasg.utils import swagger_auto_schema -from django.utils import timezone from rest_framework import generics from rest_framework import permissions -from rest_framework import status -from rest_framework.response import Response +from crews import dto from crews import models from crews import serializers from crews import services @@ -26,7 +23,7 @@ class RecruitingCrewListAPIView(generics.ListAPIView): def get_queryset(self): # 본인이 속한 크루는 제외. - queryset = services.crew_of_user_queryset( + queryset = services.crew.of_user_queryset( exclude_user=self.request.user, ) return queryset.filter(**{ @@ -41,7 +38,7 @@ class MyCrewAPIView(generics.ListAPIView): serializer_class = serializers.MyCrewSerializer def get_queryset(self): - queryset = services.crew_of_user_queryset( + queryset = services.crew.of_user_queryset( include_user=self.request.user, ) # 활동 종료된 크루는 뒤로 가도록 정렬 @@ -49,13 +46,40 @@ def get_queryset(self): class CrewDashboardAPIView(generics.RetrieveAPIView): - """가입한 크루 목록 조회 API""" + """크루 대시보드 홈 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.MyCrewDashboardSerializer + serializer_class = serializers.CrewDashboardSerializer lookup_field = 'id' def get_queryset(self): - return services.crew_of_user_queryset( + return services.crew.of_user_queryset( + include_user=self.request.user, + ) + + +class CrewStatisticsAPIView(generics.RetrieveAPIView): + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewStatisticsSerializer + lookup_field = 'id' + + def get_queryset(self): + return services.crew.of_user_queryset( + include_user=self.request.user, + ) + + def get_object(self) -> dto.ProblemStatistic: + return services.problem_statistics(crew=super().get_object()) + + +class CrewActivityAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 - 회차 API""" + + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewDashboardSerializer + lookup_field = 'id' + + def get_queryset(self): + return services.crew.of_user_queryset( include_user=self.request.user, ) diff --git a/app/submissions/models/submission.py b/app/submissions/models/submission.py index 879d4ba..681d237 100644 --- a/app/submissions/models/submission.py +++ b/app/submissions/models/submission.py @@ -1,6 +1,7 @@ from django.db import models -from crews.models import CrewActivityProblem, ProgrammingLanguageChoices +from crews.enums import ProgrammingLanguageChoices +from crews.models import CrewActivityProblem from users.models import User From 4dcc699ec8f8d6dbc9942c0aacaff55778894898 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 17:23:52 +0900 Subject: [PATCH 366/552] =?UTF-8?q?refactor(crews):=20Emoji=20=EB=A5=BC=20?= =?UTF-8?q?TextChoice=EB=A1=9C=20=EC=A7=80=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/enums/__init__.py | 9 +- app/crews/enums/emoji.py | 10072 ++++++++++---------- app/crews/models/crew.py | 15 +- app/crews/models/crew_activity.py | 11 +- app/crews/models/crew_activity_problem.py | 4 + app/crews/validators.py | 15 - 6 files changed, 5051 insertions(+), 5075 deletions(-) delete mode 100644 app/crews/validators.py diff --git a/app/crews/enums/__init__.py b/app/crews/enums/__init__.py index d52fb9f..84a8b2e 100644 --- a/app/crews/enums/__init__.py +++ b/app/crews/enums/__init__.py @@ -2,14 +2,7 @@ from django.db import models -from crews.enums.emoji import Emoji - - -__all__ = ( - 'CrewTagType', - 'Emoji', - 'ProgrammingLanguage', -) +from crews.enums.emoji import EmojiChoices class CrewTagType(Enum): diff --git a/app/crews/enums/emoji.py b/app/crews/enums/emoji.py index db395c3..fb25cd5 100644 --- a/app/crews/enums/emoji.py +++ b/app/crews/enums/emoji.py @@ -1,5038 +1,5038 @@ -from enum import Enum +from django.db.models import TextChoices -class Emoji(Enum): - U1F947 = "🥇" # :1st_place_medal: - U1F948 = "🥈" # :2nd_place_medal: - U1F949 = "🥉" # :3rd_place_medal: - U1F18E = "🆎" # :AB_button_(blood_type): - U1F3E7 = "🏧" # :ATM_sign: - U1F170FE0F = "🅰️" # :A_button_(blood_type): - U1F170 = "🅰" # :A_button_(blood_type): - U1F1E61F1EB = "🇦🇫" # :Afghanistan: - U1F1E61F1F1 = "🇦🇱" # :Albania: - U1F1E91F1FF = "🇩🇿" # :Algeria: - U1F1E61F1F8 = "🇦🇸" # :American_Samoa: - U1F1E61F1E9 = "🇦🇩" # :Andorra: - U1F1E61F1F4 = "🇦🇴" # :Angola: - U1F1E61F1EE = "🇦🇮" # :Anguilla: - U1F1E61F1F6 = "🇦🇶" # :Antarctica: - U1F1E61F1EC = "🇦🇬" # :Antigua_&_Barbuda: - U2652 = "♒" # :Aquarius: - U1F1E61F1F7 = "🇦🇷" # :Argentina: - U2648 = "♈" # :Aries: - U1F1E61F1F2 = "🇦🇲" # :Armenia: - U1F1E61F1FC = "🇦🇼" # :Aruba: - U1F1E61F1E8 = "🇦🇨" # :Ascension_Island: - U1F1E61F1FA = "🇦🇺" # :Australia: - U1F1E61F1F9 = "🇦🇹" # :Austria: - U1F1E61F1FF = "🇦🇿" # :Azerbaijan: - U1F519 = "🔙" # :BACK_arrow: - U1F171FE0F = "🅱️" # :B_button_(blood_type): - U1F171 = "🅱" # :B_button_(blood_type): - U1F1E71F1F8 = "🇧🇸" # :Bahamas: - U1F1E71F1ED = "🇧🇭" # :Bahrain: - U1F1E71F1E9 = "🇧🇩" # :Bangladesh: - U1F1E71F1E7 = "🇧🇧" # :Barbados: - U1F1E71F1FE = "🇧🇾" # :Belarus: - U1F1E71F1EA = "🇧🇪" # :Belgium: - U1F1E71F1FF = "🇧🇿" # :Belize: - U1F1E71F1EF = "🇧🇯" # :Benin: - U1F1E71F1F2 = "🇧🇲" # :Bermuda: - U1F1E71F1F9 = "🇧🇹" # :Bhutan: - U1F1E71F1F4 = "🇧🇴" # :Bolivia: - U1F1E71F1E6 = "🇧🇦" # :Bosnia_&_Herzegovina: - U1F1E71F1FC = "🇧🇼" # :Botswana: - U1F1E71F1FB = "🇧🇻" # :Bouvet_Island: - U1F1E71F1F7 = "🇧🇷" # :Brazil: - U1F1EE1F1F4 = "🇮🇴" # :British_Indian_Ocean_Territory: - U1F1FB1F1EC = "🇻🇬" # :British_Virgin_Islands: - U1F1E71F1F3 = "🇧🇳" # :Brunei: - U1F1E71F1EC = "🇧🇬" # :Bulgaria: - U1F1E71F1EB = "🇧🇫" # :Burkina_Faso: - U1F1E71F1EE = "🇧🇮" # :Burundi: - U1F191 = "🆑" # :CL_button: - U1F192 = "🆒" # :COOL_button: - U1F1F01F1ED = "🇰🇭" # :Cambodia: - U1F1E81F1F2 = "🇨🇲" # :Cameroon: - U1F1E81F1E6 = "🇨🇦" # :Canada: - U1F1EE1F1E8 = "🇮🇨" # :Canary_Islands: - U264B = "♋" # :Cancer: - U1F1E81F1FB = "🇨🇻" # :Cape_Verde: - U2651 = "♑" # :Capricorn: - U1F1E71F1F6 = "🇧🇶" # :Caribbean_Netherlands: - U1F1F01F1FE = "🇰🇾" # :Cayman_Islands: - U1F1E81F1EB = "🇨🇫" # :Central_African_Republic: - U1F1EA1F1E6 = "🇪🇦" # :Ceuta_&_Melilla: - U1F1F91F1E9 = "🇹🇩" # :Chad: - U1F1E81F1F1 = "🇨🇱" # :Chile: - U1F1E81F1F3 = "🇨🇳" # :China: - U1F1E81F1FD = "🇨🇽" # :Christmas_Island: - U1F384 = "🎄" # :Christmas_tree: - U1F1E81F1F5 = "🇨🇵" # :Clipperton_Island: - U1F1E81F1E8 = "🇨🇨" # :Cocos_(Keeling)_Islands: - U1F1E81F1F4 = "🇨🇴" # :Colombia: - U1F1F01F1F2 = "🇰🇲" # :Comoros: - U1F1E81F1EC = "🇨🇬" # :Congo-Brazzaville: - U1F1E81F1E9 = "🇨🇩" # :Congo-Kinshasa: - U1F1E81F1F0 = "🇨🇰" # :Cook_Islands: - U1F1E81F1F7 = "🇨🇷" # :Costa_Rica: - U1F1ED1F1F7 = "🇭🇷" # :Croatia: - U1F1E81F1FA = "🇨🇺" # :Cuba: - U1F1E81F1FC = "🇨🇼" # :Curaçao: - U1F1E81F1FE = "🇨🇾" # :Cyprus: - U1F1E81F1FF = "🇨🇿" # :Czechia: - U1F1E81F1EE = "🇨🇮" # :Côte_d’Ivoire: - U1F1E91F1F0 = "🇩🇰" # :Denmark: - U1F1E91F1EC = "🇩🇬" # :Diego_Garcia: - U1F1E91F1EF = "🇩🇯" # :Djibouti: - U1F1E91F1F2 = "🇩🇲" # :Dominica: - U1F1E91F1F4 = "🇩🇴" # :Dominican_Republic: - U1F51A = "🔚" # :END_arrow: - U1F1EA1F1E8 = "🇪🇨" # :Ecuador: - U1F1EA1F1EC = "🇪🇬" # :Egypt: - U1F1F81F1FB = "🇸🇻" # :El_Salvador: - U1F3F4E0067E0062E0065E006EE0067E007F = "🏴󠁧󠁢󠁥󠁮󠁧󠁿" # :England: - U1F1EC1F1F6 = "🇬🇶" # :Equatorial_Guinea: - U1F1EA1F1F7 = "🇪🇷" # :Eritrea: - U1F1EA1F1EA = "🇪🇪" # :Estonia: - U1F1F81F1FF = "🇸🇿" # :Eswatini: - U1F1EA1F1F9 = "🇪🇹" # :Ethiopia: - U1F1EA1F1FA = "🇪🇺" # :European_Union: - U1F193 = "🆓" # :FREE_button: - U1F1EB1F1F0 = "🇫🇰" # :Falkland_Islands: - U1F1EB1F1F4 = "🇫🇴" # :Faroe_Islands: - U1F1EB1F1EF = "🇫🇯" # :Fiji: - U1F1EB1F1EE = "🇫🇮" # :Finland: - U1F1EB1F1F7 = "🇫🇷" # :France: - U1F1EC1F1EB = "🇬🇫" # :French_Guiana: - U1F1F51F1EB = "🇵🇫" # :French_Polynesia: - U1F1F91F1EB = "🇹🇫" # :French_Southern_Territories: - U1F1EC1F1E6 = "🇬🇦" # :Gabon: - U1F1EC1F1F2 = "🇬🇲" # :Gambia: - U264A = "♊" # :Gemini: - U1F1EC1F1EA = "🇬🇪" # :Georgia: - U1F1E91F1EA = "🇩🇪" # :Germany: - U1F1EC1F1ED = "🇬🇭" # :Ghana: - U1F1EC1F1EE = "🇬🇮" # :Gibraltar: - U1F1EC1F1F7 = "🇬🇷" # :Greece: - U1F1EC1F1F1 = "🇬🇱" # :Greenland: - U1F1EC1F1E9 = "🇬🇩" # :Grenada: - U1F1EC1F1F5 = "🇬🇵" # :Guadeloupe: - U1F1EC1F1FA = "🇬🇺" # :Guam: - U1F1EC1F1F9 = "🇬🇹" # :Guatemala: - U1F1EC1F1EC = "🇬🇬" # :Guernsey: - U1F1EC1F1F3 = "🇬🇳" # :Guinea: - U1F1EC1F1FC = "🇬🇼" # :Guinea-Bissau: - U1F1EC1F1FE = "🇬🇾" # :Guyana: - U1F1ED1F1F9 = "🇭🇹" # :Haiti: - U1F1ED1F1F2 = "🇭🇲" # :Heard_&_McDonald_Islands: - U1F1ED1F1F3 = "🇭🇳" # :Honduras: - U1F1ED1F1F0 = "🇭🇰" # :Hong_Kong_SAR_China: - U1F1ED1F1FA = "🇭🇺" # :Hungary: - U1F194 = "🆔" # :ID_button: - U1F1EE1F1F8 = "🇮🇸" # :Iceland: - U1F1EE1F1F3 = "🇮🇳" # :India: - U1F1EE1F1E9 = "🇮🇩" # :Indonesia: - U1F1EE1F1F7 = "🇮🇷" # :Iran: - U1F1EE1F1F6 = "🇮🇶" # :Iraq: - U1F1EE1F1EA = "🇮🇪" # :Ireland: - U1F1EE1F1F2 = "🇮🇲" # :Isle_of_Man: - U1F1EE1F1F1 = "🇮🇱" # :Israel: - U1F1EE1F1F9 = "🇮🇹" # :Italy: - U1F1EF1F1F2 = "🇯🇲" # :Jamaica: - U1F1EF1F1F5 = "🇯🇵" # :Japan: - U1F251 = "🉑" # :Japanese_acceptable_button: - U1F238 = "🈸" # :Japanese_application_button: - U1F250 = "🉐" # :Japanese_bargain_button: - U1F3EF = "🏯" # :Japanese_castle: - U3297FE0F = "㊗️" # :Japanese_congratulations_button: - U3297 = "㊗" # :Japanese_congratulations_button: - U1F239 = "🈹" # :Japanese_discount_button: - U1F38E = "🎎" # :Japanese_dolls: - U1F21A = "🈚" # :Japanese_free_of_charge_button: - U1F201 = "🈁" # :Japanese_here_button: - U1F237FE0F = "🈷️" # :Japanese_monthly_amount_button: - U1F237 = "🈷" # :Japanese_monthly_amount_button: - U1F235 = "🈵" # :Japanese_no_vacancy_button: - U1F236 = "🈶" # :Japanese_not_free_of_charge_button: - U1F23A = "🈺" # :Japanese_open_for_business_button: - U1F234 = "🈴" # :Japanese_passing_grade_button: - U1F3E3 = "🏣" # :Japanese_post_office: - U1F232 = "🈲" # :Japanese_prohibited_button: - U1F22F = "🈯" # :Japanese_reserved_button: - U3299FE0F = "㊙️" # :Japanese_secret_button: - U3299 = "㊙" # :Japanese_secret_button: - U1F202FE0F = "🈂️" # :Japanese_service_charge_button: - U1F202 = "🈂" # :Japanese_service_charge_button: - U1F530 = "🔰" # :Japanese_symbol_for_beginner: - U1F233 = "🈳" # :Japanese_vacancy_button: - U1F1EF1F1EA = "🇯🇪" # :Jersey: - U1F1EF1F1F4 = "🇯🇴" # :Jordan: - U1F1F01F1FF = "🇰🇿" # :Kazakhstan: - U1F1F01F1EA = "🇰🇪" # :Kenya: - U1F1F01F1EE = "🇰🇮" # :Kiribati: - U1F1FD1F1F0 = "🇽🇰" # :Kosovo: - U1F1F01F1FC = "🇰🇼" # :Kuwait: - U1F1F01F1EC = "🇰🇬" # :Kyrgyzstan: - U1F1F11F1E6 = "🇱🇦" # :Laos: - U1F1F11F1FB = "🇱🇻" # :Latvia: - U1F1F11F1E7 = "🇱🇧" # :Lebanon: - U264C = "♌" # :Leo: - U1F1F11F1F8 = "🇱🇸" # :Lesotho: - U1F1F11F1F7 = "🇱🇷" # :Liberia: - U264E = "♎" # :Libra: - U1F1F11F1FE = "🇱🇾" # :Libya: - U1F1F11F1EE = "🇱🇮" # :Liechtenstein: - U1F1F11F1F9 = "🇱🇹" # :Lithuania: - U1F1F11F1FA = "🇱🇺" # :Luxembourg: - U1F1F21F1F4 = "🇲🇴" # :Macao_SAR_China: - U1F1F21F1EC = "🇲🇬" # :Madagascar: - U1F1F21F1FC = "🇲🇼" # :Malawi: - U1F1F21F1FE = "🇲🇾" # :Malaysia: - U1F1F21F1FB = "🇲🇻" # :Maldives: - U1F1F21F1F1 = "🇲🇱" # :Mali: - U1F1F21F1F9 = "🇲🇹" # :Malta: - U1F1F21F1ED = "🇲🇭" # :Marshall_Islands: - U1F1F21F1F6 = "🇲🇶" # :Martinique: - U1F1F21F1F7 = "🇲🇷" # :Mauritania: - U1F1F21F1FA = "🇲🇺" # :Mauritius: - U1F1FE1F1F9 = "🇾🇹" # :Mayotte: - U1F1F21F1FD = "🇲🇽" # :Mexico: - U1F1EB1F1F2 = "🇫🇲" # :Micronesia: - U1F1F21F1E9 = "🇲🇩" # :Moldova: - U1F1F21F1E8 = "🇲🇨" # :Monaco: - U1F1F21F1F3 = "🇲🇳" # :Mongolia: - U1F1F21F1EA = "🇲🇪" # :Montenegro: - U1F1F21F1F8 = "🇲🇸" # :Montserrat: - U1F1F21F1E6 = "🇲🇦" # :Morocco: - U1F1F21F1FF = "🇲🇿" # :Mozambique: - U1F936 = "🤶" # :Mrs._Claus: - U1F9361F3FF = "🤶🏿" # :Mrs._Claus_dark_skin_tone: - U1F9361F3FB = "🤶🏻" # :Mrs._Claus_light_skin_tone: - U1F9361F3FE = "🤶🏾" # :Mrs._Claus_medium-dark_skin_tone: - U1F9361F3FC = "🤶🏼" # :Mrs._Claus_medium-light_skin_tone: - U1F9361F3FD = "🤶🏽" # :Mrs._Claus_medium_skin_tone: - U1F1F21F1F2 = "🇲🇲" # :Myanmar_(Burma): - U1F195 = "🆕" # :NEW_button: - U1F196 = "🆖" # :NG_button: - U1F1F31F1E6 = "🇳🇦" # :Namibia: - U1F1F31F1F7 = "🇳🇷" # :Nauru: - U1F1F31F1F5 = "🇳🇵" # :Nepal: - U1F1F31F1F1 = "🇳🇱" # :Netherlands: - U1F1F31F1E8 = "🇳🇨" # :New_Caledonia: - U1F1F31F1FF = "🇳🇿" # :New_Zealand: - U1F1F31F1EE = "🇳🇮" # :Nicaragua: - U1F1F31F1EA = "🇳🇪" # :Niger: - U1F1F31F1EC = "🇳🇬" # :Nigeria: - U1F1F31F1FA = "🇳🇺" # :Niue: - U1F1F31F1EB = "🇳🇫" # :Norfolk_Island: - U1F1F01F1F5 = "🇰🇵" # :North_Korea: - U1F1F21F1F0 = "🇲🇰" # :North_Macedonia: - U1F1F21F1F5 = "🇲🇵" # :Northern_Mariana_Islands: - U1F1F31F1F4 = "🇳🇴" # :Norway: - U1F197 = "🆗" # :OK_button: - U1F44C = "👌" # :OK_hand: - U1F44C1F3FF = "👌🏿" # :OK_hand_dark_skin_tone: - U1F44C1F3FB = "👌🏻" # :OK_hand_light_skin_tone: - U1F44C1F3FE = "👌🏾" # :OK_hand_medium-dark_skin_tone: - U1F44C1F3FC = "👌🏼" # :OK_hand_medium-light_skin_tone: - U1F44C1F3FD = "👌🏽" # :OK_hand_medium_skin_tone: - U1F51B = "🔛" # :ON!_arrow: - U1F17EFE0F = "🅾️" # :O_button_(blood_type): - U1F17E = "🅾" # :O_button_(blood_type): - U1F1F41F1F2 = "🇴🇲" # :Oman: - U26CE = "⛎" # :Ophiuchus: - U1F17FFE0F = "🅿️" # :P_button: - U1F17F = "🅿" # :P_button: - U1F1F51F1F0 = "🇵🇰" # :Pakistan: - U1F1F51F1FC = "🇵🇼" # :Palau: - U1F1F51F1F8 = "🇵🇸" # :Palestinian_Territories: - U1F1F51F1E6 = "🇵🇦" # :Panama: - U1F1F51F1EC = "🇵🇬" # :Papua_New_Guinea: - U1F1F51F1FE = "🇵🇾" # :Paraguay: - U1F1F51F1EA = "🇵🇪" # :Peru: - U1F1F51F1ED = "🇵🇭" # :Philippines: - U2653 = "♓" # :Pisces: - U1F1F51F1F3 = "🇵🇳" # :Pitcairn_Islands: - U1F1F51F1F1 = "🇵🇱" # :Poland: - U1F1F51F1F9 = "🇵🇹" # :Portugal: - U1F1F51F1F7 = "🇵🇷" # :Puerto_Rico: - U1F1F61F1E6 = "🇶🇦" # :Qatar: - U1F1F71F1F4 = "🇷🇴" # :Romania: - U1F1F71F1FA = "🇷🇺" # :Russia: - U1F1F71F1FC = "🇷🇼" # :Rwanda: - U1F1F71F1EA = "🇷🇪" # :Réunion: - U1F51C = "🔜" # :SOON_arrow: - U1F198 = "🆘" # :SOS_button: - U2650 = "♐" # :Sagittarius: - U1F1FC1F1F8 = "🇼🇸" # :Samoa: - U1F1F81F1F2 = "🇸🇲" # :San_Marino: - U1F385 = "🎅" # :Santa_Claus: - U1F3851F3FF = "🎅🏿" # :Santa_Claus_dark_skin_tone: - U1F3851F3FB = "🎅🏻" # :Santa_Claus_light_skin_tone: - U1F3851F3FE = "🎅🏾" # :Santa_Claus_medium-dark_skin_tone: - U1F3851F3FC = "🎅🏼" # :Santa_Claus_medium-light_skin_tone: - U1F3851F3FD = "🎅🏽" # :Santa_Claus_medium_skin_tone: - U1F1F81F1E6 = "🇸🇦" # :Saudi_Arabia: - U264F = "♏" # :Scorpio: - U1F3F4E0067E0062E0073E0063E0074E007F = "🏴󠁧󠁢󠁳󠁣󠁴󠁿" # :Scotland: - U1F1F81F1F3 = "🇸🇳" # :Senegal: - U1F1F71F1F8 = "🇷🇸" # :Serbia: - U1F1F81F1E8 = "🇸🇨" # :Seychelles: - U1F1F81F1F1 = "🇸🇱" # :Sierra_Leone: - U1F1F81F1EC = "🇸🇬" # :Singapore: - U1F1F81F1FD = "🇸🇽" # :Sint_Maarten: - U1F1F81F1F0 = "🇸🇰" # :Slovakia: - U1F1F81F1EE = "🇸🇮" # :Slovenia: - U1F1F81F1E7 = "🇸🇧" # :Solomon_Islands: - U1F1F81F1F4 = "🇸🇴" # :Somalia: - U1F1FF1F1E6 = "🇿🇦" # :South_Africa: - U1F1EC1F1F8 = "🇬🇸" # :South_Georgia_&_South_Sandwich_Islands: - U1F1F01F1F7 = "🇰🇷" # :South_Korea: - U1F1F81F1F8 = "🇸🇸" # :South_Sudan: - U1F1EA1F1F8 = "🇪🇸" # :Spain: - U1F1F11F1F0 = "🇱🇰" # :Sri_Lanka: - U1F1E71F1F1 = "🇧🇱" # :St._Barthélemy: - U1F1F81F1ED = "🇸🇭" # :St._Helena: - U1F1F01F1F3 = "🇰🇳" # :St._Kitts_&_Nevis: - U1F1F11F1E8 = "🇱🇨" # :St._Lucia: - U1F1F21F1EB = "🇲🇫" # :St._Martin: - U1F1F51F1F2 = "🇵🇲" # :St._Pierre_&_Miquelon: - U1F1FB1F1E8 = "🇻🇨" # :St._Vincent_&_Grenadines: - U1F5FD = "🗽" # :Statue_of_Liberty: - U1F1F81F1E9 = "🇸🇩" # :Sudan: - U1F1F81F1F7 = "🇸🇷" # :Suriname: - U1F1F81F1EF = "🇸🇯" # :Svalbard_&_Jan_Mayen: - U1F1F81F1EA = "🇸🇪" # :Sweden: - U1F1E81F1ED = "🇨🇭" # :Switzerland: - U1F1F81F1FE = "🇸🇾" # :Syria: - U1F1F81F1F9 = "🇸🇹" # :São_Tomé_&_Príncipe: - U1F996 = "🦖" # :T-Rex: - U1F51D = "🔝" # :TOP_arrow: - U1F1F91F1FC = "🇹🇼" # :Taiwan: - U1F1F91F1EF = "🇹🇯" # :Tajikistan: - U1F1F91F1FF = "🇹🇿" # :Tanzania: - U2649 = "♉" # :Taurus: - U1F1F91F1ED = "🇹🇭" # :Thailand: - U1F1F91F1F1 = "🇹🇱" # :Timor-Leste: - U1F1F91F1EC = "🇹🇬" # :Togo: - U1F1F91F1F0 = "🇹🇰" # :Tokelau: - U1F5FC = "🗼" # :Tokyo_tower: - U1F1F91F1F4 = "🇹🇴" # :Tonga: - U1F1F91F1F9 = "🇹🇹" # :Trinidad_&_Tobago: - U1F1F91F1E6 = "🇹🇦" # :Tristan_da_Cunha: - U1F1F91F1F3 = "🇹🇳" # :Tunisia: - U1F1F91F1F2 = "🇹🇲" # :Turkmenistan: - U1F1F91F1E8 = "🇹🇨" # :Turks_&_Caicos_Islands: - U1F1F91F1FB = "🇹🇻" # :Tuvalu: - U1F1F91F1F7 = "🇹🇷" # :Türkiye: - U1F1FA1F1F2 = "🇺🇲" # :U.S._Outlying_Islands: - U1F1FB1F1EE = "🇻🇮" # :U.S._Virgin_Islands: - U1F199 = "🆙" # :UP!_button: - U1F1FA1F1EC = "🇺🇬" # :Uganda: - U1F1FA1F1E6 = "🇺🇦" # :Ukraine: - U1F1E61F1EA = "🇦🇪" # :United_Arab_Emirates: - U1F1EC1F1E7 = "🇬🇧" # :United_Kingdom: - U1F1FA1F1F3 = "🇺🇳" # :United_Nations: - U1F1FA1F1F8 = "🇺🇸" # :United_States: - U1F1FA1F1FE = "🇺🇾" # :Uruguay: - U1F1FA1F1FF = "🇺🇿" # :Uzbekistan: - U1F19A = "🆚" # :VS_button: - U1F1FB1F1FA = "🇻🇺" # :Vanuatu: - U1F1FB1F1E6 = "🇻🇦" # :Vatican_City: - U1F1FB1F1EA = "🇻🇪" # :Venezuela: - U1F1FB1F1F3 = "🇻🇳" # :Vietnam: - U264D = "♍" # :Virgo: - U1F3F4E0067E0062E0077E006CE0073E007F = "🏴󠁧󠁢󠁷󠁬󠁳󠁿" # :Wales: - U1F1FC1F1EB = "🇼🇫" # :Wallis_&_Futuna: - U1F1EA1F1ED = "🇪🇭" # :Western_Sahara: - U1F1FE1F1EA = "🇾🇪" # :Yemen: - U1F4A4 = "💤" # :ZZZ: - U1F1FF1F1F2 = "🇿🇲" # :Zambia: - U1F1FF1F1FC = "🇿🇼" # :Zimbabwe: - U1F9EE = "🧮" # :abacus: - U1FA97 = "🪗" # :accordion: - U1FA79 = "🩹" # :adhesive_bandage: - U1F39FFE0F = "🎟️" # :admission_tickets: - U1F39F = "🎟" # :admission_tickets: - U1F6A1 = "🚡" # :aerial_tramway: - U2708FE0F = "✈️" # :airplane: - U2708 = "✈" # :airplane: - U1F6EC = "🛬" # :airplane_arrival: - U1F6EB = "🛫" # :airplane_departure: - U23F0 = "⏰" # :alarm_clock: - U2697FE0F = "⚗️" # :alembic: - U2697 = "⚗" # :alembic: - U1F47D = "👽" # :alien: - U1F47E = "👾" # :alien_monster: - U1F691 = "🚑" # :ambulance: - U1F3C8 = "🏈" # :american_football: - U1F3FA = "🏺" # :amphora: - U1FAC0 = "🫀" # :anatomical_heart: - U2693 = "⚓" # :anchor: - U1F4A2 = "💢" # :anger_symbol: - U1F620 = "😠" # :angry_face: - U1F47F = "👿" # :angry_face_with_horns: - U1F627 = "😧" # :anguished_face: - U1F41C = "🐜" # :ant: - U1F4F6 = "📶" # :antenna_bars: - U1F630 = "😰" # :anxious_face_with_sweat: - U1F69B = "🚛" # :articulated_lorry: - U1F9D1200D1F3A8 = "🧑‍🎨" # :artist: - U1F9D11F3FF200D1F3A8 = "🧑🏿‍🎨" # :artist_dark_skin_tone: - U1F9D11F3FB200D1F3A8 = "🧑🏻‍🎨" # :artist_light_skin_tone: - U1F9D11F3FE200D1F3A8 = "🧑🏾‍🎨" # :artist_medium-dark_skin_tone: - U1F9D11F3FC200D1F3A8 = "🧑🏼‍🎨" # :artist_medium-light_skin_tone: - U1F9D11F3FD200D1F3A8 = "🧑🏽‍🎨" # :artist_medium_skin_tone: - U1F3A8 = "🎨" # :artist_palette: - U1F632 = "😲" # :astonished_face: - U1F9D1200D1F680 = "🧑‍🚀" # :astronaut: - U1F9D11F3FF200D1F680 = "🧑🏿‍🚀" # :astronaut_dark_skin_tone: - U1F9D11F3FB200D1F680 = "🧑🏻‍🚀" # :astronaut_light_skin_tone: - U1F9D11F3FE200D1F680 = "🧑🏾‍🚀" # :astronaut_medium-dark_skin_tone: - U1F9D11F3FC200D1F680 = "🧑🏼‍🚀" # :astronaut_medium-light_skin_tone: - U1F9D11F3FD200D1F680 = "🧑🏽‍🚀" # :astronaut_medium_skin_tone: - U269BFE0F = "⚛️" # :atom_symbol: - U269B = "⚛" # :atom_symbol: - U1F6FA = "🛺" # :auto_rickshaw: - U1F697 = "🚗" # :automobile: - U1F951 = "🥑" # :avocado: - U1FA93 = "🪓" # :axe: - U1F476 = "👶" # :baby: - U1F47C = "👼" # :baby_angel: - U1F47C1F3FF = "👼🏿" # :baby_angel_dark_skin_tone: - U1F47C1F3FB = "👼🏻" # :baby_angel_light_skin_tone: - U1F47C1F3FE = "👼🏾" # :baby_angel_medium-dark_skin_tone: - U1F47C1F3FC = "👼🏼" # :baby_angel_medium-light_skin_tone: - U1F47C1F3FD = "👼🏽" # :baby_angel_medium_skin_tone: - U1F37C = "🍼" # :baby_bottle: - U1F424 = "🐤" # :baby_chick: - U1F4761F3FF = "👶🏿" # :baby_dark_skin_tone: - U1F4761F3FB = "👶🏻" # :baby_light_skin_tone: - U1F4761F3FE = "👶🏾" # :baby_medium-dark_skin_tone: - U1F4761F3FC = "👶🏼" # :baby_medium-light_skin_tone: - U1F4761F3FD = "👶🏽" # :baby_medium_skin_tone: - U1F6BC = "🚼" # :baby_symbol: - U1F447 = "👇" # :backhand_index_pointing_down: - U1F4471F3FF = "👇🏿" # :backhand_index_pointing_down_dark_skin_tone: - U1F4471F3FB = "👇🏻" # :backhand_index_pointing_down_light_skin_tone: - U1F4471F3FE = "👇🏾" # :backhand_index_pointing_down_medium-dark_skin_tone: - U1F4471F3FC = "👇🏼" # :backhand_index_pointing_down_medium-light_skin_tone: - U1F4471F3FD = "👇🏽" # :backhand_index_pointing_down_medium_skin_tone: - U1F448 = "👈" # :backhand_index_pointing_left: - U1F4481F3FF = "👈🏿" # :backhand_index_pointing_left_dark_skin_tone: - U1F4481F3FB = "👈🏻" # :backhand_index_pointing_left_light_skin_tone: - U1F4481F3FE = "👈🏾" # :backhand_index_pointing_left_medium-dark_skin_tone: - U1F4481F3FC = "👈🏼" # :backhand_index_pointing_left_medium-light_skin_tone: - U1F4481F3FD = "👈🏽" # :backhand_index_pointing_left_medium_skin_tone: - U1F449 = "👉" # :backhand_index_pointing_right: - U1F4491F3FF = "👉🏿" # :backhand_index_pointing_right_dark_skin_tone: - U1F4491F3FB = "👉🏻" # :backhand_index_pointing_right_light_skin_tone: - U1F4491F3FE = "👉🏾" # :backhand_index_pointing_right_medium-dark_skin_tone: - U1F4491F3FC = "👉🏼" # :backhand_index_pointing_right_medium-light_skin_tone: - U1F4491F3FD = "👉🏽" # :backhand_index_pointing_right_medium_skin_tone: - U1F446 = "👆" # :backhand_index_pointing_up: - U1F4461F3FF = "👆🏿" # :backhand_index_pointing_up_dark_skin_tone: - U1F4461F3FB = "👆🏻" # :backhand_index_pointing_up_light_skin_tone: - U1F4461F3FE = "👆🏾" # :backhand_index_pointing_up_medium-dark_skin_tone: - U1F4461F3FC = "👆🏼" # :backhand_index_pointing_up_medium-light_skin_tone: - U1F4461F3FD = "👆🏽" # :backhand_index_pointing_up_medium_skin_tone: - U1F392 = "🎒" # :backpack: - U1F953 = "🥓" # :bacon: - U1F9A1 = "🦡" # :badger: - U1F3F8 = "🏸" # :badminton: - U1F96F = "🥯" # :bagel: - U1F6C4 = "🛄" # :baggage_claim: - U1F956 = "🥖" # :baguette_bread: - U2696FE0F = "⚖️" # :balance_scale: - U2696 = "⚖" # :balance_scale: - U1F9B2 = "🦲" # :bald: - U1FA70 = "🩰" # :ballet_shoes: - U1F388 = "🎈" # :balloon: - U1F5F3FE0F = "🗳️" # :ballot_box_with_ballot: - U1F5F3 = "🗳" # :ballot_box_with_ballot: - U1F34C = "🍌" # :banana: - U1FA95 = "🪕" # :banjo: - U1F3E6 = "🏦" # :bank: - U1F4CA = "📊" # :bar_chart: - U1F488 = "💈" # :barber_pole: - U26BE = "⚾" # :baseball: - U1F9FA = "🧺" # :basket: - U1F3C0 = "🏀" # :basketball: - U1F987 = "🦇" # :bat: - U1F6C1 = "🛁" # :bathtub: - U1F50B = "🔋" # :battery: - U1F3D6FE0F = "🏖️" # :beach_with_umbrella: - U1F3D6 = "🏖" # :beach_with_umbrella: - U1F601 = "😁" # :beaming_face_with_smiling_eyes: - U1FAD8 = "🫘" # :beans: - U1F43B = "🐻" # :bear: - U1F493 = "💓" # :beating_heart: - U1F9AB = "🦫" # :beaver: - U1F6CFFE0F = "🛏️" # :bed: - U1F6CF = "🛏" # :bed: - U1F37A = "🍺" # :beer_mug: - U1FAB2 = "🪲" # :beetle: - U1F514 = "🔔" # :bell: - U1FAD1 = "🫑" # :bell_pepper: - U1F515 = "🔕" # :bell_with_slash: - U1F6CEFE0F = "🛎️" # :bellhop_bell: - U1F6CE = "🛎" # :bellhop_bell: - U1F371 = "🍱" # :bento_box: - U1F9C3 = "🧃" # :beverage_box: - U1F6B2 = "🚲" # :bicycle: - U1F459 = "👙" # :bikini: - U1F9E2 = "🧢" # :billed_cap: - U2623FE0F = "☣️" # :biohazard: - U2623 = "☣" # :biohazard: - U1F426 = "🐦" # :bird: - U1F382 = "🎂" # :birthday_cake: - U1F9AC = "🦬" # :bison: - U1FAE6 = "🫦" # :biting_lip: - U1F426200D2B1B = "🐦‍⬛" # :black_bird: - U1F408200D2B1B = "🐈‍⬛" # :black_cat: - U26AB = "⚫" # :black_circle: - U1F3F4 = "🏴" # :black_flag: - U1F5A4 = "🖤" # :black_heart: - U2B1B = "⬛" # :black_large_square: - U25FE = "◾" # :black_medium-small_square: - U25FCFE0F = "◼️" # :black_medium_square: - U25FC = "◼" # :black_medium_square: - U2712FE0F = "✒️" # :black_nib: - U2712 = "✒" # :black_nib: - U25AAFE0F = "▪️" # :black_small_square: - U25AA = "▪" # :black_small_square: - U1F532 = "🔲" # :black_square_button: - U1F33C = "🌼" # :blossom: - U1F421 = "🐡" # :blowfish: - U1F4D8 = "📘" # :blue_book: - U1F535 = "🔵" # :blue_circle: - U1F499 = "💙" # :blue_heart: - U1F7E6 = "🟦" # :blue_square: - U1FAD0 = "🫐" # :blueberries: - U1F417 = "🐗" # :boar: - U1F4A3 = "💣" # :bomb: - U1F9B4 = "🦴" # :bone: - U1F516 = "🔖" # :bookmark: - U1F4D1 = "📑" # :bookmark_tabs: - U1F4DA = "📚" # :books: - U1FA83 = "🪃" # :boomerang: - U1F37E = "🍾" # :bottle_with_popping_cork: - U1F490 = "💐" # :bouquet: - U1F3F9 = "🏹" # :bow_and_arrow: - U1F963 = "🥣" # :bowl_with_spoon: - U1F3B3 = "🎳" # :bowling: - U1F94A = "🥊" # :boxing_glove: - U1F466 = "👦" # :boy: - U1F4661F3FF = "👦🏿" # :boy_dark_skin_tone: - U1F4661F3FB = "👦🏻" # :boy_light_skin_tone: - U1F4661F3FE = "👦🏾" # :boy_medium-dark_skin_tone: - U1F4661F3FC = "👦🏼" # :boy_medium-light_skin_tone: - U1F4661F3FD = "👦🏽" # :boy_medium_skin_tone: - U1F9E0 = "🧠" # :brain: - U1F35E = "🍞" # :bread: - U1F931 = "🤱" # :breast-feeding: - U1F9311F3FF = "🤱🏿" # :breast-feeding_dark_skin_tone: - U1F9311F3FB = "🤱🏻" # :breast-feeding_light_skin_tone: - U1F9311F3FE = "🤱🏾" # :breast-feeding_medium-dark_skin_tone: - U1F9311F3FC = "🤱🏼" # :breast-feeding_medium-light_skin_tone: - U1F9311F3FD = "🤱🏽" # :breast-feeding_medium_skin_tone: - U1F9F1 = "🧱" # :brick: - U1F309 = "🌉" # :bridge_at_night: - U1F4BC = "💼" # :briefcase: - U1FA72 = "🩲" # :briefs: - U1F506 = "🔆" # :bright_button: - U1F966 = "🥦" # :broccoli: - U26D3FE0F200D1F4A5 = "⛓️‍💥" # :broken_chain: - U26D3200D1F4A5 = "⛓‍💥" # :broken_chain: - U1F494 = "💔" # :broken_heart: - U1F9F9 = "🧹" # :broom: - U1F7E4 = "🟤" # :brown_circle: - U1F90E = "🤎" # :brown_heart: - U1F344200D1F7EB = "🍄‍🟫" # :brown_mushroom: - U1F7EB = "🟫" # :brown_square: - U1F9CB = "🧋" # :bubble_tea: - U1FAE7 = "🫧" # :bubbles: - U1FAA3 = "🪣" # :bucket: - U1F41B = "🐛" # :bug: - U1F3D7FE0F = "🏗️" # :building_construction: - U1F3D7 = "🏗" # :building_construction: - U1F685 = "🚅" # :bullet_train: - U1F3AF = "🎯" # :bullseye: - U1F32F = "🌯" # :burrito: - U1F68C = "🚌" # :bus: - U1F68F = "🚏" # :bus_stop: - U1F464 = "👤" # :bust_in_silhouette: - U1F465 = "👥" # :busts_in_silhouette: - U1F9C8 = "🧈" # :butter: - U1F98B = "🦋" # :butterfly: - U1F335 = "🌵" # :cactus: - U1F4C5 = "📅" # :calendar: - U1F919 = "🤙" # :call_me_hand: - U1F9191F3FF = "🤙🏿" # :call_me_hand_dark_skin_tone: - U1F9191F3FB = "🤙🏻" # :call_me_hand_light_skin_tone: - U1F9191F3FE = "🤙🏾" # :call_me_hand_medium-dark_skin_tone: - U1F9191F3FC = "🤙🏼" # :call_me_hand_medium-light_skin_tone: - U1F9191F3FD = "🤙🏽" # :call_me_hand_medium_skin_tone: - U1F42A = "🐪" # :camel: - U1F4F7 = "📷" # :camera: - U1F4F8 = "📸" # :camera_with_flash: - U1F3D5FE0F = "🏕️" # :camping: - U1F3D5 = "🏕" # :camping: - U1F56FFE0F = "🕯️" # :candle: - U1F56F = "🕯" # :candle: - U1F36C = "🍬" # :candy: - U1F96B = "🥫" # :canned_food: - U1F6F6 = "🛶" # :canoe: - U1F5C3FE0F = "🗃️" # :card_file_box: - U1F5C3 = "🗃" # :card_file_box: - U1F4C7 = "📇" # :card_index: - U1F5C2FE0F = "🗂️" # :card_index_dividers: - U1F5C2 = "🗂" # :card_index_dividers: - U1F3A0 = "🎠" # :carousel_horse: - U1F38F = "🎏" # :carp_streamer: - U1FA9A = "🪚" # :carpentry_saw: - U1F955 = "🥕" # :carrot: - U1F3F0 = "🏰" # :castle: - U1F408 = "🐈" # :cat: - U1F431 = "🐱" # :cat_face: - U1F639 = "😹" # :cat_with_tears_of_joy: - U1F63C = "😼" # :cat_with_wry_smile: - U26D3FE0F = "⛓️" # :chains: - U26D3 = "⛓" # :chains: - U1FA91 = "🪑" # :chair: - U1F4C9 = "📉" # :chart_decreasing: - U1F4C8 = "📈" # :chart_increasing: - U1F4B9 = "💹" # :chart_increasing_with_yen: - U2611FE0F = "☑️" # :check_box_with_check: - U2611 = "☑" # :check_box_with_check: - U2714FE0F = "✔️" # :check_mark: - U2714 = "✔" # :check_mark: - U2705 = "✅" # :check_mark_button: - U1F9C0 = "🧀" # :cheese_wedge: - U1F3C1 = "🏁" # :chequered_flag: - U1F352 = "🍒" # :cherries: - U1F338 = "🌸" # :cherry_blossom: - U265FFE0F = "♟️" # :chess_pawn: - U265F = "♟" # :chess_pawn: - U1F330 = "🌰" # :chestnut: - U1F414 = "🐔" # :chicken: - U1F9D2 = "🧒" # :child: - U1F9D21F3FF = "🧒🏿" # :child_dark_skin_tone: - U1F9D21F3FB = "🧒🏻" # :child_light_skin_tone: - U1F9D21F3FE = "🧒🏾" # :child_medium-dark_skin_tone: - U1F9D21F3FC = "🧒🏼" # :child_medium-light_skin_tone: - U1F9D21F3FD = "🧒🏽" # :child_medium_skin_tone: - U1F6B8 = "🚸" # :children_crossing: - U1F43FFE0F = "🐿️" # :chipmunk: - U1F43F = "🐿" # :chipmunk: - U1F36B = "🍫" # :chocolate_bar: - U1F962 = "🥢" # :chopsticks: - U26EA = "⛪" # :church: - U1F6AC = "🚬" # :cigarette: - U1F3A6 = "🎦" # :cinema: - U24C2FE0F = "Ⓜ️" # :circled_M: - U24C2 = "Ⓜ" # :circled_M: - U1F3AA = "🎪" # :circus_tent: - U1F3D9FE0F = "🏙️" # :cityscape: - U1F3D9 = "🏙" # :cityscape: - U1F306 = "🌆" # :cityscape_at_dusk: - U1F5DCFE0F = "🗜️" # :clamp: - U1F5DC = "🗜" # :clamp: - U1F3AC = "🎬" # :clapper_board: - U1F44F = "👏" # :clapping_hands: - U1F44F1F3FF = "👏🏿" # :clapping_hands_dark_skin_tone: - U1F44F1F3FB = "👏🏻" # :clapping_hands_light_skin_tone: - U1F44F1F3FE = "👏🏾" # :clapping_hands_medium-dark_skin_tone: - U1F44F1F3FC = "👏🏼" # :clapping_hands_medium-light_skin_tone: - U1F44F1F3FD = "👏🏽" # :clapping_hands_medium_skin_tone: - U1F3DBFE0F = "🏛️" # :classical_building: - U1F3DB = "🏛" # :classical_building: - U1F37B = "🍻" # :clinking_beer_mugs: - U1F942 = "🥂" # :clinking_glasses: - U1F4CB = "📋" # :clipboard: - U1F503 = "🔃" # :clockwise_vertical_arrows: - U1F4D5 = "📕" # :closed_book: - U1F4EA = "📪" # :closed_mailbox_with_lowered_flag: - U1F4EB = "📫" # :closed_mailbox_with_raised_flag: - U1F302 = "🌂" # :closed_umbrella: - U2601FE0F = "☁️" # :cloud: - U2601 = "☁" # :cloud: - U1F329FE0F = "🌩️" # :cloud_with_lightning: - U1F329 = "🌩" # :cloud_with_lightning: - U26C8FE0F = "⛈️" # :cloud_with_lightning_and_rain: - U26C8 = "⛈" # :cloud_with_lightning_and_rain: - U1F327FE0F = "🌧️" # :cloud_with_rain: - U1F327 = "🌧" # :cloud_with_rain: - U1F328FE0F = "🌨️" # :cloud_with_snow: - U1F328 = "🌨" # :cloud_with_snow: - U1F921 = "🤡" # :clown_face: - U2663FE0F = "♣️" # :club_suit: - U2663 = "♣" # :club_suit: - U1F45D = "👝" # :clutch_bag: - U1F9E5 = "🧥" # :coat: - U1FAB3 = "🪳" # :cockroach: - U1F378 = "🍸" # :cocktail_glass: - U1F965 = "🥥" # :coconut: - U26B0FE0F = "⚰️" # :coffin: - U26B0 = "⚰" # :coffin: - U1FA99 = "🪙" # :coin: - U1F976 = "🥶" # :cold_face: - U1F4A5 = "💥" # :collision: - U2604FE0F = "☄️" # :comet: - U2604 = "☄" # :comet: - U1F9ED = "🧭" # :compass: - U1F4BD = "💽" # :computer_disk: - U1F5B1FE0F = "🖱️" # :computer_mouse: - U1F5B1 = "🖱" # :computer_mouse: - U1F38A = "🎊" # :confetti_ball: - U1F616 = "😖" # :confounded_face: - U1F615 = "😕" # :confused_face: - U1F6A7 = "🚧" # :construction: - U1F477 = "👷" # :construction_worker: - U1F4771F3FF = "👷🏿" # :construction_worker_dark_skin_tone: - U1F4771F3FB = "👷🏻" # :construction_worker_light_skin_tone: - U1F4771F3FE = "👷🏾" # :construction_worker_medium-dark_skin_tone: - U1F4771F3FC = "👷🏼" # :construction_worker_medium-light_skin_tone: - U1F4771F3FD = "👷🏽" # :construction_worker_medium_skin_tone: - U1F39BFE0F = "🎛️" # :control_knobs: - U1F39B = "🎛" # :control_knobs: - U1F3EA = "🏪" # :convenience_store: - U1F9D1200D1F373 = "🧑‍🍳" # :cook: - U1F9D11F3FF200D1F373 = "🧑🏿‍🍳" # :cook_dark_skin_tone: - U1F9D11F3FB200D1F373 = "🧑🏻‍🍳" # :cook_light_skin_tone: - U1F9D11F3FE200D1F373 = "🧑🏾‍🍳" # :cook_medium-dark_skin_tone: - U1F9D11F3FC200D1F373 = "🧑🏼‍🍳" # :cook_medium-light_skin_tone: - U1F9D11F3FD200D1F373 = "🧑🏽‍🍳" # :cook_medium_skin_tone: - U1F35A = "🍚" # :cooked_rice: - U1F36A = "🍪" # :cookie: - U1F373 = "🍳" # :cooking: - UA9FE0F = "©️" # :copyright: - UA9 = "©" # :copyright: - U1FAB8 = "🪸" # :coral: - U1F6CBFE0F = "🛋️" # :couch_and_lamp: - U1F6CB = "🛋" # :couch_and_lamp: - U1F504 = "🔄" # :counterclockwise_arrows_button: - U1F491 = "💑" # :couple_with_heart: - U1F4911F3FF = "💑🏿" # :couple_with_heart_dark_skin_tone: - U1F4911F3FB = "💑🏻" # :couple_with_heart_light_skin_tone: - U1F468200D2764FE0F200D1F468 = "👨‍❤️‍👨" # :couple_with_heart_man_man: - U1F468200D2764200D1F468 = "👨‍❤‍👨" # :couple_with_heart_man_man: - U1F4681F3FF200D2764FE0F200D1F4681F3FF = "👨🏿‍❤️‍👨🏿" # :couple_with_heart_man_man_dark_skin_tone: - U1F4681F3FF200D2764200D1F4681F3FF = "👨🏿‍❤‍👨🏿" # :couple_with_heart_man_man_dark_skin_tone: - U1F4681F3FF200D2764FE0F200D1F4681F3FB = "👨🏿‍❤️‍👨🏻" # :couple_with_heart_man_man_dark_skin_tone_light_skin_tone: - U1F4681F3FF200D2764200D1F4681F3FB = "👨🏿‍❤‍👨🏻" # :couple_with_heart_man_man_dark_skin_tone_light_skin_tone: - U1F4681F3FF200D2764FE0F200D1F4681F3FE = "👨🏿‍❤️‍👨🏾" # :couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone: - U1F4681F3FF200D2764200D1F4681F3FE = "👨🏿‍❤‍👨🏾" # :couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone: - U1F4681F3FF200D2764FE0F200D1F4681F3FC = "👨🏿‍❤️‍👨🏼" # :couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone: - U1F4681F3FF200D2764200D1F4681F3FC = "👨🏿‍❤‍👨🏼" # :couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone: - U1F4681F3FF200D2764FE0F200D1F4681F3FD = "👨🏿‍❤️‍👨🏽" # :couple_with_heart_man_man_dark_skin_tone_medium_skin_tone: - U1F4681F3FF200D2764200D1F4681F3FD = "👨🏿‍❤‍👨🏽" # :couple_with_heart_man_man_dark_skin_tone_medium_skin_tone: - U1F4681F3FB200D2764FE0F200D1F4681F3FB = "👨🏻‍❤️‍👨🏻" # :couple_with_heart_man_man_light_skin_tone: - U1F4681F3FB200D2764200D1F4681F3FB = "👨🏻‍❤‍👨🏻" # :couple_with_heart_man_man_light_skin_tone: - U1F4681F3FB200D2764FE0F200D1F4681F3FF = "👨🏻‍❤️‍👨🏿" # :couple_with_heart_man_man_light_skin_tone_dark_skin_tone: - U1F4681F3FB200D2764200D1F4681F3FF = "👨🏻‍❤‍👨🏿" # :couple_with_heart_man_man_light_skin_tone_dark_skin_tone: - U1F4681F3FB200D2764FE0F200D1F4681F3FE = "👨🏻‍❤️‍👨🏾" # :couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone: - U1F4681F3FB200D2764200D1F4681F3FE = "👨🏻‍❤‍👨🏾" # :couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone: - U1F4681F3FB200D2764FE0F200D1F4681F3FC = "👨🏻‍❤️‍👨🏼" # :couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone: - U1F4681F3FB200D2764200D1F4681F3FC = "👨🏻‍❤‍👨🏼" # :couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone: - U1F4681F3FB200D2764FE0F200D1F4681F3FD = "👨🏻‍❤️‍👨🏽" # :couple_with_heart_man_man_light_skin_tone_medium_skin_tone: - U1F4681F3FB200D2764200D1F4681F3FD = "👨🏻‍❤‍👨🏽" # :couple_with_heart_man_man_light_skin_tone_medium_skin_tone: - U1F4681F3FE200D2764FE0F200D1F4681F3FE = "👨🏾‍❤️‍👨🏾" # :couple_with_heart_man_man_medium-dark_skin_tone: - U1F4681F3FE200D2764200D1F4681F3FE = "👨🏾‍❤‍👨🏾" # :couple_with_heart_man_man_medium-dark_skin_tone: - U1F4681F3FE200D2764FE0F200D1F4681F3FF = "👨🏾‍❤️‍👨🏿" # :couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone: - U1F4681F3FE200D2764200D1F4681F3FF = "👨🏾‍❤‍👨🏿" # :couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone: - U1F4681F3FE200D2764FE0F200D1F4681F3FB = "👨🏾‍❤️‍👨🏻" # :couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone: - U1F4681F3FE200D2764200D1F4681F3FB = "👨🏾‍❤‍👨🏻" # :couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone: - U1F4681F3FE200D2764FE0F200D1F4681F3FC = "👨🏾‍❤️‍👨🏼" # :couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4681F3FE200D2764200D1F4681F3FC = "👨🏾‍❤‍👨🏼" # :couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4681F3FE200D2764FE0F200D1F4681F3FD = "👨🏾‍❤️‍👨🏽" # :couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone: - U1F4681F3FE200D2764200D1F4681F3FD = "👨🏾‍❤‍👨🏽" # :couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone: - U1F4681F3FC200D2764FE0F200D1F4681F3FC = "👨🏼‍❤️‍👨🏼" # :couple_with_heart_man_man_medium-light_skin_tone: - U1F4681F3FC200D2764200D1F4681F3FC = "👨🏼‍❤‍👨🏼" # :couple_with_heart_man_man_medium-light_skin_tone: - U1F4681F3FC200D2764FE0F200D1F4681F3FF = "👨🏼‍❤️‍👨🏿" # :couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone: - U1F4681F3FC200D2764200D1F4681F3FF = "👨🏼‍❤‍👨🏿" # :couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone: - U1F4681F3FC200D2764FE0F200D1F4681F3FB = "👨🏼‍❤️‍👨🏻" # :couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone: - U1F4681F3FC200D2764200D1F4681F3FB = "👨🏼‍❤‍👨🏻" # :couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone: - U1F4681F3FC200D2764FE0F200D1F4681F3FE = "👨🏼‍❤️‍👨🏾" # :couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4681F3FC200D2764200D1F4681F3FE = "👨🏼‍❤‍👨🏾" # :couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4681F3FC200D2764FE0F200D1F4681F3FD = "👨🏼‍❤️‍👨🏽" # :couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone: - U1F4681F3FC200D2764200D1F4681F3FD = "👨🏼‍❤‍👨🏽" # :couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone: - U1F4681F3FD200D2764FE0F200D1F4681F3FD = "👨🏽‍❤️‍👨🏽" # :couple_with_heart_man_man_medium_skin_tone: - U1F4681F3FD200D2764200D1F4681F3FD = "👨🏽‍❤‍👨🏽" # :couple_with_heart_man_man_medium_skin_tone: - U1F4681F3FD200D2764FE0F200D1F4681F3FF = "👨🏽‍❤️‍👨🏿" # :couple_with_heart_man_man_medium_skin_tone_dark_skin_tone: - U1F4681F3FD200D2764200D1F4681F3FF = "👨🏽‍❤‍👨🏿" # :couple_with_heart_man_man_medium_skin_tone_dark_skin_tone: - U1F4681F3FD200D2764FE0F200D1F4681F3FB = "👨🏽‍❤️‍👨🏻" # :couple_with_heart_man_man_medium_skin_tone_light_skin_tone: - U1F4681F3FD200D2764200D1F4681F3FB = "👨🏽‍❤‍👨🏻" # :couple_with_heart_man_man_medium_skin_tone_light_skin_tone: - U1F4681F3FD200D2764FE0F200D1F4681F3FE = "👨🏽‍❤️‍👨🏾" # :couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone: - U1F4681F3FD200D2764200D1F4681F3FE = "👨🏽‍❤‍👨🏾" # :couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone: - U1F4681F3FD200D2764FE0F200D1F4681F3FC = "👨🏽‍❤️‍👨🏼" # :couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone: - U1F4681F3FD200D2764200D1F4681F3FC = "👨🏽‍❤‍👨🏼" # :couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone: - U1F4911F3FE = "💑🏾" # :couple_with_heart_medium-dark_skin_tone: - U1F4911F3FC = "💑🏼" # :couple_with_heart_medium-light_skin_tone: - U1F4911F3FD = "💑🏽" # :couple_with_heart_medium_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F9D11F3FB = "🧑🏿‍❤️‍🧑🏻" # :couple_with_heart_person_person_dark_skin_tone_light_skin_tone: - U1F9D11F3FF200D2764200D1F9D11F3FB = "🧑🏿‍❤‍🧑🏻" # :couple_with_heart_person_person_dark_skin_tone_light_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F9D11F3FE = "🧑🏿‍❤️‍🧑🏾" # :couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone: - U1F9D11F3FF200D2764200D1F9D11F3FE = "🧑🏿‍❤‍🧑🏾" # :couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F9D11F3FC = "🧑🏿‍❤️‍🧑🏼" # :couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FF200D2764200D1F9D11F3FC = "🧑🏿‍❤‍🧑🏼" # :couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F9D11F3FD = "🧑🏿‍❤️‍🧑🏽" # :couple_with_heart_person_person_dark_skin_tone_medium_skin_tone: - U1F9D11F3FF200D2764200D1F9D11F3FD = "🧑🏿‍❤‍🧑🏽" # :couple_with_heart_person_person_dark_skin_tone_medium_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F9D11F3FF = "🧑🏻‍❤️‍🧑🏿" # :couple_with_heart_person_person_light_skin_tone_dark_skin_tone: - U1F9D11F3FB200D2764200D1F9D11F3FF = "🧑🏻‍❤‍🧑🏿" # :couple_with_heart_person_person_light_skin_tone_dark_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F9D11F3FE = "🧑🏻‍❤️‍🧑🏾" # :couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FB200D2764200D1F9D11F3FE = "🧑🏻‍❤‍🧑🏾" # :couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F9D11F3FC = "🧑🏻‍❤️‍🧑🏼" # :couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone: - U1F9D11F3FB200D2764200D1F9D11F3FC = "🧑🏻‍❤‍🧑🏼" # :couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F9D11F3FD = "🧑🏻‍❤️‍🧑🏽" # :couple_with_heart_person_person_light_skin_tone_medium_skin_tone: - U1F9D11F3FB200D2764200D1F9D11F3FD = "🧑🏻‍❤‍🧑🏽" # :couple_with_heart_person_person_light_skin_tone_medium_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F9D11F3FF = "🧑🏾‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone: - U1F9D11F3FE200D2764200D1F9D11F3FF = "🧑🏾‍❤‍🧑🏿" # :couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F9D11F3FB = "🧑🏾‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone: - U1F9D11F3FE200D2764200D1F9D11F3FB = "🧑🏾‍❤‍🧑🏻" # :couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F9D11F3FC = "🧑🏾‍❤️‍🧑🏼" # :couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FE200D2764200D1F9D11F3FC = "🧑🏾‍❤‍🧑🏼" # :couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F9D11F3FD = "🧑🏾‍❤️‍🧑🏽" # :couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone: - U1F9D11F3FE200D2764200D1F9D11F3FD = "🧑🏾‍❤‍🧑🏽" # :couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F9D11F3FF = "🧑🏼‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone: - U1F9D11F3FC200D2764200D1F9D11F3FF = "🧑🏼‍❤‍🧑🏿" # :couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F9D11F3FB = "🧑🏼‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone: - U1F9D11F3FC200D2764200D1F9D11F3FB = "🧑🏼‍❤‍🧑🏻" # :couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F9D11F3FE = "🧑🏼‍❤️‍🧑🏾" # :couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FC200D2764200D1F9D11F3FE = "🧑🏼‍❤‍🧑🏾" # :couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F9D11F3FD = "🧑🏼‍❤️‍🧑🏽" # :couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone: - U1F9D11F3FC200D2764200D1F9D11F3FD = "🧑🏼‍❤‍🧑🏽" # :couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F9D11F3FF = "🧑🏽‍❤️‍🧑🏿" # :couple_with_heart_person_person_medium_skin_tone_dark_skin_tone: - U1F9D11F3FD200D2764200D1F9D11F3FF = "🧑🏽‍❤‍🧑🏿" # :couple_with_heart_person_person_medium_skin_tone_dark_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F9D11F3FB = "🧑🏽‍❤️‍🧑🏻" # :couple_with_heart_person_person_medium_skin_tone_light_skin_tone: - U1F9D11F3FD200D2764200D1F9D11F3FB = "🧑🏽‍❤‍🧑🏻" # :couple_with_heart_person_person_medium_skin_tone_light_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F9D11F3FE = "🧑🏽‍❤️‍🧑🏾" # :couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone: - U1F9D11F3FD200D2764200D1F9D11F3FE = "🧑🏽‍❤‍🧑🏾" # :couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F9D11F3FC = "🧑🏽‍❤️‍🧑🏼" # :couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone: - U1F9D11F3FD200D2764200D1F9D11F3FC = "🧑🏽‍❤‍🧑🏼" # :couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone: - U1F469200D2764FE0F200D1F468 = "👩‍❤️‍👨" # :couple_with_heart_woman_man: - U1F469200D2764200D1F468 = "👩‍❤‍👨" # :couple_with_heart_woman_man: - U1F4691F3FF200D2764FE0F200D1F4681F3FF = "👩🏿‍❤️‍👨🏿" # :couple_with_heart_woman_man_dark_skin_tone: - U1F4691F3FF200D2764200D1F4681F3FF = "👩🏿‍❤‍👨🏿" # :couple_with_heart_woman_man_dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4681F3FB = "👩🏿‍❤️‍👨🏻" # :couple_with_heart_woman_man_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764200D1F4681F3FB = "👩🏿‍❤‍👨🏻" # :couple_with_heart_woman_man_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4681F3FE = "👩🏿‍❤️‍👨🏾" # :couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764200D1F4681F3FE = "👩🏿‍❤‍👨🏾" # :couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4681F3FC = "👩🏿‍❤️‍👨🏼" # :couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764200D1F4681F3FC = "👩🏿‍❤‍👨🏼" # :couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4681F3FD = "👩🏿‍❤️‍👨🏽" # :couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone: - U1F4691F3FF200D2764200D1F4681F3FD = "👩🏿‍❤‍👨🏽" # :couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4681F3FB = "👩🏻‍❤️‍👨🏻" # :couple_with_heart_woman_man_light_skin_tone: - U1F4691F3FB200D2764200D1F4681F3FB = "👩🏻‍❤‍👨🏻" # :couple_with_heart_woman_man_light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4681F3FF = "👩🏻‍❤️‍👨🏿" # :couple_with_heart_woman_man_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764200D1F4681F3FF = "👩🏻‍❤‍👨🏿" # :couple_with_heart_woman_man_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4681F3FE = "👩🏻‍❤️‍👨🏾" # :couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764200D1F4681F3FE = "👩🏻‍❤‍👨🏾" # :couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4681F3FC = "👩🏻‍❤️‍👨🏼" # :couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764200D1F4681F3FC = "👩🏻‍❤‍👨🏼" # :couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4681F3FD = "👩🏻‍❤️‍👨🏽" # :couple_with_heart_woman_man_light_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764200D1F4681F3FD = "👩🏻‍❤‍👨🏽" # :couple_with_heart_woman_man_light_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4681F3FE = "👩🏾‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium-dark_skin_tone: - U1F4691F3FE200D2764200D1F4681F3FE = "👩🏾‍❤‍👨🏾" # :couple_with_heart_woman_man_medium-dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4681F3FF = "👩🏾‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764200D1F4681F3FF = "👩🏾‍❤‍👨🏿" # :couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4681F3FB = "👩🏾‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764200D1F4681F3FB = "👩🏾‍❤‍👨🏻" # :couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4681F3FC = "👩🏾‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764200D1F4681F3FC = "👩🏾‍❤‍👨🏼" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4681F3FD = "👩🏾‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764200D1F4681F3FD = "👩🏾‍❤‍👨🏽" # :couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4681F3FC = "👩🏼‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium-light_skin_tone: - U1F4691F3FC200D2764200D1F4681F3FC = "👩🏼‍❤‍👨🏼" # :couple_with_heart_woman_man_medium-light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4681F3FF = "👩🏼‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764200D1F4681F3FF = "👩🏼‍❤‍👨🏿" # :couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4681F3FB = "👩🏼‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764200D1F4681F3FB = "👩🏼‍❤‍👨🏻" # :couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4681F3FE = "👩🏼‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764200D1F4681F3FE = "👩🏼‍❤‍👨🏾" # :couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4681F3FD = "👩🏼‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764200D1F4681F3FD = "👩🏼‍❤‍👨🏽" # :couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4681F3FD = "👩🏽‍❤️‍👨🏽" # :couple_with_heart_woman_man_medium_skin_tone: - U1F4691F3FD200D2764200D1F4681F3FD = "👩🏽‍❤‍👨🏽" # :couple_with_heart_woman_man_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4681F3FF = "👩🏽‍❤️‍👨🏿" # :couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764200D1F4681F3FF = "👩🏽‍❤‍👨🏿" # :couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4681F3FB = "👩🏽‍❤️‍👨🏻" # :couple_with_heart_woman_man_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764200D1F4681F3FB = "👩🏽‍❤‍👨🏻" # :couple_with_heart_woman_man_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4681F3FE = "👩🏽‍❤️‍👨🏾" # :couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764200D1F4681F3FE = "👩🏽‍❤‍👨🏾" # :couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4681F3FC = "👩🏽‍❤️‍👨🏼" # :couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone: - U1F4691F3FD200D2764200D1F4681F3FC = "👩🏽‍❤‍👨🏼" # :couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone: - U1F469200D2764FE0F200D1F469 = "👩‍❤️‍👩" # :couple_with_heart_woman_woman: - U1F469200D2764200D1F469 = "👩‍❤‍👩" # :couple_with_heart_woman_woman: - U1F4691F3FF200D2764FE0F200D1F4691F3FF = "👩🏿‍❤️‍👩🏿" # :couple_with_heart_woman_woman_dark_skin_tone: - U1F4691F3FF200D2764200D1F4691F3FF = "👩🏿‍❤‍👩🏿" # :couple_with_heart_woman_woman_dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4691F3FB = "👩🏿‍❤️‍👩🏻" # :couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764200D1F4691F3FB = "👩🏿‍❤‍👩🏻" # :couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4691F3FE = "👩🏿‍❤️‍👩🏾" # :couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764200D1F4691F3FE = "👩🏿‍❤‍👩🏾" # :couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4691F3FC = "👩🏿‍❤️‍👩🏼" # :couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764200D1F4691F3FC = "👩🏿‍❤‍👩🏼" # :couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F4691F3FD = "👩🏿‍❤️‍👩🏽" # :couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone: - U1F4691F3FF200D2764200D1F4691F3FD = "👩🏿‍❤‍👩🏽" # :couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4691F3FB = "👩🏻‍❤️‍👩🏻" # :couple_with_heart_woman_woman_light_skin_tone: - U1F4691F3FB200D2764200D1F4691F3FB = "👩🏻‍❤‍👩🏻" # :couple_with_heart_woman_woman_light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4691F3FF = "👩🏻‍❤️‍👩🏿" # :couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764200D1F4691F3FF = "👩🏻‍❤‍👩🏿" # :couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4691F3FE = "👩🏻‍❤️‍👩🏾" # :couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764200D1F4691F3FE = "👩🏻‍❤‍👩🏾" # :couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4691F3FC = "👩🏻‍❤️‍👩🏼" # :couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764200D1F4691F3FC = "👩🏻‍❤‍👩🏼" # :couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F4691F3FD = "👩🏻‍❤️‍👩🏽" # :couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764200D1F4691F3FD = "👩🏻‍❤‍👩🏽" # :couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4691F3FE = "👩🏾‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium-dark_skin_tone: - U1F4691F3FE200D2764200D1F4691F3FE = "👩🏾‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium-dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4691F3FF = "👩🏾‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764200D1F4691F3FF = "👩🏾‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4691F3FB = "👩🏾‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764200D1F4691F3FB = "👩🏾‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4691F3FC = "👩🏾‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764200D1F4691F3FC = "👩🏾‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F4691F3FD = "👩🏾‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764200D1F4691F3FD = "👩🏾‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4691F3FC = "👩🏼‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium-light_skin_tone: - U1F4691F3FC200D2764200D1F4691F3FC = "👩🏼‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium-light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4691F3FF = "👩🏼‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764200D1F4691F3FF = "👩🏼‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4691F3FB = "👩🏼‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764200D1F4691F3FB = "👩🏼‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4691F3FE = "👩🏼‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764200D1F4691F3FE = "👩🏼‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F4691F3FD = "👩🏼‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764200D1F4691F3FD = "👩🏼‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4691F3FD = "👩🏽‍❤️‍👩🏽" # :couple_with_heart_woman_woman_medium_skin_tone: - U1F4691F3FD200D2764200D1F4691F3FD = "👩🏽‍❤‍👩🏽" # :couple_with_heart_woman_woman_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4691F3FF = "👩🏽‍❤️‍👩🏿" # :couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764200D1F4691F3FF = "👩🏽‍❤‍👩🏿" # :couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4691F3FB = "👩🏽‍❤️‍👩🏻" # :couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764200D1F4691F3FB = "👩🏽‍❤‍👩🏻" # :couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4691F3FE = "👩🏽‍❤️‍👩🏾" # :couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764200D1F4691F3FE = "👩🏽‍❤‍👩🏾" # :couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F4691F3FC = "👩🏽‍❤️‍👩🏼" # :couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone: - U1F4691F3FD200D2764200D1F4691F3FC = "👩🏽‍❤‍👩🏼" # :couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone: - U1F404 = "🐄" # :cow: - U1F42E = "🐮" # :cow_face: - U1F920 = "🤠" # :cowboy_hat_face: - U1F980 = "🦀" # :crab: - U1F58DFE0F = "🖍️" # :crayon: - U1F58D = "🖍" # :crayon: - U1F4B3 = "💳" # :credit_card: - U1F319 = "🌙" # :crescent_moon: - U1F997 = "🦗" # :cricket: - U1F3CF = "🏏" # :cricket_game: - U1F40A = "🐊" # :crocodile: - U1F950 = "🥐" # :croissant: - U274C = "❌" # :cross_mark: - U274E = "❎" # :cross_mark_button: - U1F91E = "🤞" # :crossed_fingers: - U1F91E1F3FF = "🤞🏿" # :crossed_fingers_dark_skin_tone: - U1F91E1F3FB = "🤞🏻" # :crossed_fingers_light_skin_tone: - U1F91E1F3FE = "🤞🏾" # :crossed_fingers_medium-dark_skin_tone: - U1F91E1F3FC = "🤞🏼" # :crossed_fingers_medium-light_skin_tone: - U1F91E1F3FD = "🤞🏽" # :crossed_fingers_medium_skin_tone: - U1F38C = "🎌" # :crossed_flags: - U2694FE0F = "⚔️" # :crossed_swords: - U2694 = "⚔" # :crossed_swords: - U1F451 = "👑" # :crown: - U1FA7C = "🩼" # :crutch: - U1F63F = "😿" # :crying_cat: - U1F622 = "😢" # :crying_face: - U1F52E = "🔮" # :crystal_ball: - U1F952 = "🥒" # :cucumber: - U1F964 = "🥤" # :cup_with_straw: - U1F9C1 = "🧁" # :cupcake: - U1F94C = "🥌" # :curling_stone: - U1F9B1 = "🦱" # :curly_hair: - U27B0 = "➰" # :curly_loop: - U1F4B1 = "💱" # :currency_exchange: - U1F35B = "🍛" # :curry_rice: - U1F36E = "🍮" # :custard: - U1F6C3 = "🛃" # :customs: - U1F969 = "🥩" # :cut_of_meat: - U1F300 = "🌀" # :cyclone: - U1F5E1FE0F = "🗡️" # :dagger: - U1F5E1 = "🗡" # :dagger: - U1F361 = "🍡" # :dango: - U1F3FF = "🏿" # :dark_skin_tone: - U1F4A8 = "💨" # :dashing_away: - U1F9CF200D2642FE0F = "🧏‍♂️" # :deaf_man: - U1F9CF200D2642 = "🧏‍♂" # :deaf_man: - U1F9CF1F3FF200D2642FE0F = "🧏🏿‍♂️" # :deaf_man_dark_skin_tone: - U1F9CF1F3FF200D2642 = "🧏🏿‍♂" # :deaf_man_dark_skin_tone: - U1F9CF1F3FB200D2642FE0F = "🧏🏻‍♂️" # :deaf_man_light_skin_tone: - U1F9CF1F3FB200D2642 = "🧏🏻‍♂" # :deaf_man_light_skin_tone: - U1F9CF1F3FE200D2642FE0F = "🧏🏾‍♂️" # :deaf_man_medium-dark_skin_tone: - U1F9CF1F3FE200D2642 = "🧏🏾‍♂" # :deaf_man_medium-dark_skin_tone: - U1F9CF1F3FC200D2642FE0F = "🧏🏼‍♂️" # :deaf_man_medium-light_skin_tone: - U1F9CF1F3FC200D2642 = "🧏🏼‍♂" # :deaf_man_medium-light_skin_tone: - U1F9CF1F3FD200D2642FE0F = "🧏🏽‍♂️" # :deaf_man_medium_skin_tone: - U1F9CF1F3FD200D2642 = "🧏🏽‍♂" # :deaf_man_medium_skin_tone: - U1F9CF = "🧏" # :deaf_person: - U1F9CF1F3FF = "🧏🏿" # :deaf_person_dark_skin_tone: - U1F9CF1F3FB = "🧏🏻" # :deaf_person_light_skin_tone: - U1F9CF1F3FE = "🧏🏾" # :deaf_person_medium-dark_skin_tone: - U1F9CF1F3FC = "🧏🏼" # :deaf_person_medium-light_skin_tone: - U1F9CF1F3FD = "🧏🏽" # :deaf_person_medium_skin_tone: - U1F9CF200D2640FE0F = "🧏‍♀️" # :deaf_woman: - U1F9CF200D2640 = "🧏‍♀" # :deaf_woman: - U1F9CF1F3FF200D2640FE0F = "🧏🏿‍♀️" # :deaf_woman_dark_skin_tone: - U1F9CF1F3FF200D2640 = "🧏🏿‍♀" # :deaf_woman_dark_skin_tone: - U1F9CF1F3FB200D2640FE0F = "🧏🏻‍♀️" # :deaf_woman_light_skin_tone: - U1F9CF1F3FB200D2640 = "🧏🏻‍♀" # :deaf_woman_light_skin_tone: - U1F9CF1F3FE200D2640FE0F = "🧏🏾‍♀️" # :deaf_woman_medium-dark_skin_tone: - U1F9CF1F3FE200D2640 = "🧏🏾‍♀" # :deaf_woman_medium-dark_skin_tone: - U1F9CF1F3FC200D2640FE0F = "🧏🏼‍♀️" # :deaf_woman_medium-light_skin_tone: - U1F9CF1F3FC200D2640 = "🧏🏼‍♀" # :deaf_woman_medium-light_skin_tone: - U1F9CF1F3FD200D2640FE0F = "🧏🏽‍♀️" # :deaf_woman_medium_skin_tone: - U1F9CF1F3FD200D2640 = "🧏🏽‍♀" # :deaf_woman_medium_skin_tone: - U1F333 = "🌳" # :deciduous_tree: - U1F98C = "🦌" # :deer: - U1F69A = "🚚" # :delivery_truck: - U1F3EC = "🏬" # :department_store: - U1F3DAFE0F = "🏚️" # :derelict_house: - U1F3DA = "🏚" # :derelict_house: - U1F3DCFE0F = "🏜️" # :desert: - U1F3DC = "🏜" # :desert: - U1F3DDFE0F = "🏝️" # :desert_island: - U1F3DD = "🏝" # :desert_island: - U1F5A5FE0F = "🖥️" # :desktop_computer: - U1F5A5 = "🖥" # :desktop_computer: - U1F575FE0F = "🕵️" # :detective: - U1F575 = "🕵" # :detective: - U1F5751F3FF = "🕵🏿" # :detective_dark_skin_tone: - U1F5751F3FB = "🕵🏻" # :detective_light_skin_tone: - U1F5751F3FE = "🕵🏾" # :detective_medium-dark_skin_tone: - U1F5751F3FC = "🕵🏼" # :detective_medium-light_skin_tone: - U1F5751F3FD = "🕵🏽" # :detective_medium_skin_tone: - U2666FE0F = "♦️" # :diamond_suit: - U2666 = "♦" # :diamond_suit: - U1F4A0 = "💠" # :diamond_with_a_dot: - U1F505 = "🔅" # :dim_button: - U1F61E = "😞" # :disappointed_face: - U1F978 = "🥸" # :disguised_face: - U2797 = "➗" # :divide: - U1F93F = "🤿" # :diving_mask: - U1FA94 = "🪔" # :diya_lamp: - U1F4AB = "💫" # :dizzy: - U1F9EC = "🧬" # :dna: - U1F9A4 = "🦤" # :dodo: - U1F415 = "🐕" # :dog: - U1F436 = "🐶" # :dog_face: - U1F4B5 = "💵" # :dollar_banknote: - U1F42C = "🐬" # :dolphin: - U1FACF = "🫏" # :donkey: - U1F6AA = "🚪" # :door: - U1FAE5 = "🫥" # :dotted_line_face: - U1F52F = "🔯" # :dotted_six-pointed_star: - U27BF = "➿" # :double_curly_loop: - U203CFE0F = "‼️" # :double_exclamation_mark: - U203C = "‼" # :double_exclamation_mark: - U1F369 = "🍩" # :doughnut: - U1F54AFE0F = "🕊️" # :dove: - U1F54A = "🕊" # :dove: - U2199FE0F = "↙️" # :down-left_arrow: - U2199 = "↙" # :down-left_arrow: - U2198FE0F = "↘️" # :down-right_arrow: - U2198 = "↘" # :down-right_arrow: - U2B07FE0F = "⬇️" # :down_arrow: - U2B07 = "⬇" # :down_arrow: - U1F613 = "😓" # :downcast_face_with_sweat: - U1F53D = "🔽" # :downwards_button: - U1F409 = "🐉" # :dragon: - U1F432 = "🐲" # :dragon_face: - U1F457 = "👗" # :dress: - U1F924 = "🤤" # :drooling_face: - U1FA78 = "🩸" # :drop_of_blood: - U1F4A7 = "💧" # :droplet: - U1F941 = "🥁" # :drum: - U1F986 = "🦆" # :duck: - U1F95F = "🥟" # :dumpling: - U1F4C0 = "📀" # :dvd: - U1F4E7 = "📧" # :e-mail: - U1F985 = "🦅" # :eagle: - U1F442 = "👂" # :ear: - U1F4421F3FF = "👂🏿" # :ear_dark_skin_tone: - U1F4421F3FB = "👂🏻" # :ear_light_skin_tone: - U1F4421F3FE = "👂🏾" # :ear_medium-dark_skin_tone: - U1F4421F3FC = "👂🏼" # :ear_medium-light_skin_tone: - U1F4421F3FD = "👂🏽" # :ear_medium_skin_tone: - U1F33D = "🌽" # :ear_of_corn: - U1F9BB = "🦻" # :ear_with_hearing_aid: - U1F9BB1F3FF = "🦻🏿" # :ear_with_hearing_aid_dark_skin_tone: - U1F9BB1F3FB = "🦻🏻" # :ear_with_hearing_aid_light_skin_tone: - U1F9BB1F3FE = "🦻🏾" # :ear_with_hearing_aid_medium-dark_skin_tone: - U1F9BB1F3FC = "🦻🏼" # :ear_with_hearing_aid_medium-light_skin_tone: - U1F9BB1F3FD = "🦻🏽" # :ear_with_hearing_aid_medium_skin_tone: - U1F95A = "🥚" # :egg: - U1F346 = "🍆" # :eggplant: - U2734FE0F = "✴️" # :eight-pointed_star: - U2734 = "✴" # :eight-pointed_star: - U2733FE0F = "✳️" # :eight-spoked_asterisk: - U2733 = "✳" # :eight-spoked_asterisk: - U1F563 = "🕣" # :eight-thirty: - U1F557 = "🕗" # :eight_o’clock: - U23CFFE0F = "⏏️" # :eject_button: - U23CF = "⏏" # :eject_button: - U1F50C = "🔌" # :electric_plug: - U1F418 = "🐘" # :elephant: - U1F6D7 = "🛗" # :elevator: - U1F566 = "🕦" # :eleven-thirty: - U1F55A = "🕚" # :eleven_o’clock: - U1F9DD = "🧝" # :elf: - U1F9DD1F3FF = "🧝🏿" # :elf_dark_skin_tone: - U1F9DD1F3FB = "🧝🏻" # :elf_light_skin_tone: - U1F9DD1F3FE = "🧝🏾" # :elf_medium-dark_skin_tone: - U1F9DD1F3FC = "🧝🏼" # :elf_medium-light_skin_tone: - U1F9DD1F3FD = "🧝🏽" # :elf_medium_skin_tone: - U1FAB9 = "🪹" # :empty_nest: - U1F621 = "😡" # :enraged_face: - U2709FE0F = "✉️" # :envelope: - U2709 = "✉" # :envelope: - U1F4E9 = "📩" # :envelope_with_arrow: - U1F4B6 = "💶" # :euro_banknote: - U1F332 = "🌲" # :evergreen_tree: - U1F411 = "🐑" # :ewe: - U2049FE0F = "⁉️" # :exclamation_question_mark: - U2049 = "⁉" # :exclamation_question_mark: - U1F92F = "🤯" # :exploding_head: - U1F611 = "😑" # :expressionless_face: - U1F441FE0F = "👁️" # :eye: - U1F441 = "👁" # :eye: - U1F441FE0F200D1F5E8FE0F = "👁️‍🗨️" # :eye_in_speech_bubble: - U1F441200D1F5E8FE0F = "👁‍🗨️" # :eye_in_speech_bubble: - U1F441FE0F200D1F5E8 = "👁️‍🗨" # :eye_in_speech_bubble: - U1F441200D1F5E8 = "👁‍🗨" # :eye_in_speech_bubble: - U1F440 = "👀" # :eyes: - U1F618 = "😘" # :face_blowing_a_kiss: - U1F62E200D1F4A8 = "😮‍💨" # :face_exhaling: - U1F979 = "🥹" # :face_holding_back_tears: - U1F636200D1F32BFE0F = "😶‍🌫️" # :face_in_clouds: - U1F636200D1F32B = "😶‍🌫" # :face_in_clouds: - U1F60B = "😋" # :face_savoring_food: - U1F631 = "😱" # :face_screaming_in_fear: - U1F92E = "🤮" # :face_vomiting: - U1F635 = "😵" # :face_with_crossed-out_eyes: - U1FAE4 = "🫤" # :face_with_diagonal_mouth: - U1F92D = "🤭" # :face_with_hand_over_mouth: - U1F915 = "🤕" # :face_with_head-bandage: - U1F637 = "😷" # :face_with_medical_mask: - U1F9D0 = "🧐" # :face_with_monocle: - U1FAE2 = "🫢" # :face_with_open_eyes_and_hand_over_mouth: - U1F62E = "😮" # :face_with_open_mouth: - U1FAE3 = "🫣" # :face_with_peeking_eye: - U1F928 = "🤨" # :face_with_raised_eyebrow: - U1F644 = "🙄" # :face_with_rolling_eyes: - U1F635200D1F4AB = "😵‍💫" # :face_with_spiral_eyes: - U1F624 = "😤" # :face_with_steam_from_nose: - U1F92C = "🤬" # :face_with_symbols_on_mouth: - U1F602 = "😂" # :face_with_tears_of_joy: - U1F912 = "🤒" # :face_with_thermometer: - U1F61B = "😛" # :face_with_tongue: - U1F636 = "😶" # :face_without_mouth: - U1F3ED = "🏭" # :factory: - U1F9D1200D1F3ED = "🧑‍🏭" # :factory_worker: - U1F9D11F3FF200D1F3ED = "🧑🏿‍🏭" # :factory_worker_dark_skin_tone: - U1F9D11F3FB200D1F3ED = "🧑🏻‍🏭" # :factory_worker_light_skin_tone: - U1F9D11F3FE200D1F3ED = "🧑🏾‍🏭" # :factory_worker_medium-dark_skin_tone: - U1F9D11F3FC200D1F3ED = "🧑🏼‍🏭" # :factory_worker_medium-light_skin_tone: - U1F9D11F3FD200D1F3ED = "🧑🏽‍🏭" # :factory_worker_medium_skin_tone: - U1F9DA = "🧚" # :fairy: - U1F9DA1F3FF = "🧚🏿" # :fairy_dark_skin_tone: - U1F9DA1F3FB = "🧚🏻" # :fairy_light_skin_tone: - U1F9DA1F3FE = "🧚🏾" # :fairy_medium-dark_skin_tone: - U1F9DA1F3FC = "🧚🏼" # :fairy_medium-light_skin_tone: - U1F9DA1F3FD = "🧚🏽" # :fairy_medium_skin_tone: - U1F9C6 = "🧆" # :falafel: - U1F342 = "🍂" # :fallen_leaf: - U1F46A = "👪" # :family: - U1F9D1200D1F9D1200D1F9D2 = "🧑‍🧑‍🧒" # :family_adult_adult_child: - U1F9D1200D1F9D1200D1F9D2200D1F9D2 = "🧑‍🧑‍🧒‍🧒" # :family_adult_adult_child_child: - U1F9D1200D1F9D2 = "🧑‍🧒" # :family_adult_child: - U1F9D1200D1F9D2200D1F9D2 = "🧑‍🧒‍🧒" # :family_adult_child_child: - U1F468200D1F466 = "👨‍👦" # :family_man_boy: - U1F468200D1F466200D1F466 = "👨‍👦‍👦" # :family_man_boy_boy: - U1F468200D1F467 = "👨‍👧" # :family_man_girl: - U1F468200D1F467200D1F466 = "👨‍👧‍👦" # :family_man_girl_boy: - U1F468200D1F467200D1F467 = "👨‍👧‍👧" # :family_man_girl_girl: - U1F468200D1F468200D1F466 = "👨‍👨‍👦" # :family_man_man_boy: - U1F468200D1F468200D1F466200D1F466 = "👨‍👨‍👦‍👦" # :family_man_man_boy_boy: - U1F468200D1F468200D1F467 = "👨‍👨‍👧" # :family_man_man_girl: - U1F468200D1F468200D1F467200D1F466 = "👨‍👨‍👧‍👦" # :family_man_man_girl_boy: - U1F468200D1F468200D1F467200D1F467 = "👨‍👨‍👧‍👧" # :family_man_man_girl_girl: - U1F468200D1F469200D1F466 = "👨‍👩‍👦" # :family_man_woman_boy: - U1F468200D1F469200D1F466200D1F466 = "👨‍👩‍👦‍👦" # :family_man_woman_boy_boy: - U1F468200D1F469200D1F467 = "👨‍👩‍👧" # :family_man_woman_girl: - U1F468200D1F469200D1F467200D1F466 = "👨‍👩‍👧‍👦" # :family_man_woman_girl_boy: - U1F468200D1F469200D1F467200D1F467 = "👨‍👩‍👧‍👧" # :family_man_woman_girl_girl: - U1F469200D1F466 = "👩‍👦" # :family_woman_boy: - U1F469200D1F466200D1F466 = "👩‍👦‍👦" # :family_woman_boy_boy: - U1F469200D1F467 = "👩‍👧" # :family_woman_girl: - U1F469200D1F467200D1F466 = "👩‍👧‍👦" # :family_woman_girl_boy: - U1F469200D1F467200D1F467 = "👩‍👧‍👧" # :family_woman_girl_girl: - U1F469200D1F469200D1F466 = "👩‍👩‍👦" # :family_woman_woman_boy: - U1F469200D1F469200D1F466200D1F466 = "👩‍👩‍👦‍👦" # :family_woman_woman_boy_boy: - U1F469200D1F469200D1F467 = "👩‍👩‍👧" # :family_woman_woman_girl: - U1F469200D1F469200D1F467200D1F466 = "👩‍👩‍👧‍👦" # :family_woman_woman_girl_boy: - U1F469200D1F469200D1F467200D1F467 = "👩‍👩‍👧‍👧" # :family_woman_woman_girl_girl: - U1F9D1200D1F33E = "🧑‍🌾" # :farmer: - U1F9D11F3FF200D1F33E = "🧑🏿‍🌾" # :farmer_dark_skin_tone: - U1F9D11F3FB200D1F33E = "🧑🏻‍🌾" # :farmer_light_skin_tone: - U1F9D11F3FE200D1F33E = "🧑🏾‍🌾" # :farmer_medium-dark_skin_tone: - U1F9D11F3FC200D1F33E = "🧑🏼‍🌾" # :farmer_medium-light_skin_tone: - U1F9D11F3FD200D1F33E = "🧑🏽‍🌾" # :farmer_medium_skin_tone: - U23E9 = "⏩" # :fast-forward_button: - U23EC = "⏬" # :fast_down_button: - U23EA = "⏪" # :fast_reverse_button: - U23EB = "⏫" # :fast_up_button: - U1F4E0 = "📠" # :fax_machine: - U1F628 = "😨" # :fearful_face: - U1FAB6 = "🪶" # :feather: - U2640FE0F = "♀️" # :female_sign: - U2640 = "♀" # :female_sign: - U1F3A1 = "🎡" # :ferris_wheel: - U26F4FE0F = "⛴️" # :ferry: - U26F4 = "⛴" # :ferry: - U1F3D1 = "🏑" # :field_hockey: - U1F5C4FE0F = "🗄️" # :file_cabinet: - U1F5C4 = "🗄" # :file_cabinet: - U1F4C1 = "📁" # :file_folder: - U1F39EFE0F = "🎞️" # :film_frames: - U1F39E = "🎞" # :film_frames: - U1F4FDFE0F = "📽️" # :film_projector: - U1F4FD = "📽" # :film_projector: - U1F525 = "🔥" # :fire: - U1F692 = "🚒" # :fire_engine: - U1F9EF = "🧯" # :fire_extinguisher: - U1F9E8 = "🧨" # :firecracker: - U1F9D1200D1F692 = "🧑‍🚒" # :firefighter: - U1F9D11F3FF200D1F692 = "🧑🏿‍🚒" # :firefighter_dark_skin_tone: - U1F9D11F3FB200D1F692 = "🧑🏻‍🚒" # :firefighter_light_skin_tone: - U1F9D11F3FE200D1F692 = "🧑🏾‍🚒" # :firefighter_medium-dark_skin_tone: - U1F9D11F3FC200D1F692 = "🧑🏼‍🚒" # :firefighter_medium-light_skin_tone: - U1F9D11F3FD200D1F692 = "🧑🏽‍🚒" # :firefighter_medium_skin_tone: - U1F386 = "🎆" # :fireworks: - U1F313 = "🌓" # :first_quarter_moon: - U1F31B = "🌛" # :first_quarter_moon_face: - U1F41F = "🐟" # :fish: - U1F365 = "🍥" # :fish_cake_with_swirl: - U1F3A3 = "🎣" # :fishing_pole: - U1F560 = "🕠" # :five-thirty: - U1F554 = "🕔" # :five_o’clock: - U26F3 = "⛳" # :flag_in_hole: - U1F9A9 = "🦩" # :flamingo: - U1F526 = "🔦" # :flashlight: - U1F97F = "🥿" # :flat_shoe: - U1FAD3 = "🫓" # :flatbread: - U269CFE0F = "⚜️" # :fleur-de-lis: - U269C = "⚜" # :fleur-de-lis: - U1F4AA = "💪" # :flexed_biceps: - U1F4AA1F3FF = "💪🏿" # :flexed_biceps_dark_skin_tone: - U1F4AA1F3FB = "💪🏻" # :flexed_biceps_light_skin_tone: - U1F4AA1F3FE = "💪🏾" # :flexed_biceps_medium-dark_skin_tone: - U1F4AA1F3FC = "💪🏼" # :flexed_biceps_medium-light_skin_tone: - U1F4AA1F3FD = "💪🏽" # :flexed_biceps_medium_skin_tone: - U1F4BE = "💾" # :floppy_disk: - U1F3B4 = "🎴" # :flower_playing_cards: - U1F633 = "😳" # :flushed_face: - U1FA88 = "🪈" # :flute: - U1FAB0 = "🪰" # :fly: - U1F94F = "🥏" # :flying_disc: - U1F6F8 = "🛸" # :flying_saucer: - U1F32BFE0F = "🌫️" # :fog: - U1F32B = "🌫" # :fog: - U1F301 = "🌁" # :foggy: - U1F64F = "🙏" # :folded_hands: - U1F64F1F3FF = "🙏🏿" # :folded_hands_dark_skin_tone: - U1F64F1F3FB = "🙏🏻" # :folded_hands_light_skin_tone: - U1F64F1F3FE = "🙏🏾" # :folded_hands_medium-dark_skin_tone: - U1F64F1F3FC = "🙏🏼" # :folded_hands_medium-light_skin_tone: - U1F64F1F3FD = "🙏🏽" # :folded_hands_medium_skin_tone: - U1FAAD = "🪭" # :folding_hand_fan: - U1FAD5 = "🫕" # :fondue: - U1F9B6 = "🦶" # :foot: - U1F9B61F3FF = "🦶🏿" # :foot_dark_skin_tone: - U1F9B61F3FB = "🦶🏻" # :foot_light_skin_tone: - U1F9B61F3FE = "🦶🏾" # :foot_medium-dark_skin_tone: - U1F9B61F3FC = "🦶🏼" # :foot_medium-light_skin_tone: - U1F9B61F3FD = "🦶🏽" # :foot_medium_skin_tone: - U1F463 = "👣" # :footprints: - U1F374 = "🍴" # :fork_and_knife: - U1F37DFE0F = "🍽️" # :fork_and_knife_with_plate: - U1F37D = "🍽" # :fork_and_knife_with_plate: - U1F960 = "🥠" # :fortune_cookie: - U26F2 = "⛲" # :fountain: - U1F58BFE0F = "🖋️" # :fountain_pen: - U1F58B = "🖋" # :fountain_pen: - U1F55F = "🕟" # :four-thirty: - U1F340 = "🍀" # :four_leaf_clover: - U1F553 = "🕓" # :four_o’clock: - U1F98A = "🦊" # :fox: - U1F5BCFE0F = "🖼️" # :framed_picture: - U1F5BC = "🖼" # :framed_picture: - U1F35F = "🍟" # :french_fries: - U1F364 = "🍤" # :fried_shrimp: - U1F438 = "🐸" # :frog: - U1F425 = "🐥" # :front-facing_baby_chick: - U2639FE0F = "☹️" # :frowning_face: - U2639 = "☹" # :frowning_face: - U1F626 = "😦" # :frowning_face_with_open_mouth: - U26FD = "⛽" # :fuel_pump: - U1F315 = "🌕" # :full_moon: - U1F31D = "🌝" # :full_moon_face: - U26B1FE0F = "⚱️" # :funeral_urn: - U26B1 = "⚱" # :funeral_urn: - U1F3B2 = "🎲" # :game_die: - U1F9C4 = "🧄" # :garlic: - U2699FE0F = "⚙️" # :gear: - U2699 = "⚙" # :gear: - U1F48E = "💎" # :gem_stone: - U1F9DE = "🧞" # :genie: - U1F47B = "👻" # :ghost: - U1FADA = "🫚" # :ginger_root: - U1F992 = "🦒" # :giraffe: - U1F467 = "👧" # :girl: - U1F4671F3FF = "👧🏿" # :girl_dark_skin_tone: - U1F4671F3FB = "👧🏻" # :girl_light_skin_tone: - U1F4671F3FE = "👧🏾" # :girl_medium-dark_skin_tone: - U1F4671F3FC = "👧🏼" # :girl_medium-light_skin_tone: - U1F4671F3FD = "👧🏽" # :girl_medium_skin_tone: - U1F95B = "🥛" # :glass_of_milk: - U1F453 = "👓" # :glasses: - U1F30E = "🌎" # :globe_showing_Americas: - U1F30F = "🌏" # :globe_showing_Asia-Australia: - U1F30D = "🌍" # :globe_showing_Europe-Africa: - U1F310 = "🌐" # :globe_with_meridians: - U1F9E4 = "🧤" # :gloves: - U1F31F = "🌟" # :glowing_star: - U1F945 = "🥅" # :goal_net: - U1F410 = "🐐" # :goat: - U1F47A = "👺" # :goblin: - U1F97D = "🥽" # :goggles: - U1FABF = "🪿" # :goose: - U1F98D = "🦍" # :gorilla: - U1F393 = "🎓" # :graduation_cap: - U1F347 = "🍇" # :grapes: - U1F34F = "🍏" # :green_apple: - U1F4D7 = "📗" # :green_book: - U1F7E2 = "🟢" # :green_circle: - U1F49A = "💚" # :green_heart: - U1F957 = "🥗" # :green_salad: - U1F7E9 = "🟩" # :green_square: - U1FA76 = "🩶" # :grey_heart: - U1F62C = "😬" # :grimacing_face: - U1F63A = "😺" # :grinning_cat: - U1F638 = "😸" # :grinning_cat_with_smiling_eyes: - U1F600 = "😀" # :grinning_face: - U1F603 = "😃" # :grinning_face_with_big_eyes: - U1F604 = "😄" # :grinning_face_with_smiling_eyes: - U1F605 = "😅" # :grinning_face_with_sweat: - U1F606 = "😆" # :grinning_squinting_face: - U1F497 = "💗" # :growing_heart: - U1F482 = "💂" # :guard: - U1F4821F3FF = "💂🏿" # :guard_dark_skin_tone: - U1F4821F3FB = "💂🏻" # :guard_light_skin_tone: - U1F4821F3FE = "💂🏾" # :guard_medium-dark_skin_tone: - U1F4821F3FC = "💂🏼" # :guard_medium-light_skin_tone: - U1F4821F3FD = "💂🏽" # :guard_medium_skin_tone: - U1F9AE = "🦮" # :guide_dog: - U1F3B8 = "🎸" # :guitar: - U1FAAE = "🪮" # :hair_pick: - U1F354 = "🍔" # :hamburger: - U1F528 = "🔨" # :hammer: - U2692FE0F = "⚒️" # :hammer_and_pick: - U2692 = "⚒" # :hammer_and_pick: - U1F6E0FE0F = "🛠️" # :hammer_and_wrench: - U1F6E0 = "🛠" # :hammer_and_wrench: - U1FAAC = "🪬" # :hamsa: - U1F439 = "🐹" # :hamster: - U1F590FE0F = "🖐️" # :hand_with_fingers_splayed: - U1F590 = "🖐" # :hand_with_fingers_splayed: - U1F5901F3FF = "🖐🏿" # :hand_with_fingers_splayed_dark_skin_tone: - U1F5901F3FB = "🖐🏻" # :hand_with_fingers_splayed_light_skin_tone: - U1F5901F3FE = "🖐🏾" # :hand_with_fingers_splayed_medium-dark_skin_tone: - U1F5901F3FC = "🖐🏼" # :hand_with_fingers_splayed_medium-light_skin_tone: - U1F5901F3FD = "🖐🏽" # :hand_with_fingers_splayed_medium_skin_tone: - U1FAF0 = "🫰" # :hand_with_index_finger_and_thumb_crossed: - U1FAF01F3FF = "🫰🏿" # :hand_with_index_finger_and_thumb_crossed_dark_skin_tone: - U1FAF01F3FB = "🫰🏻" # :hand_with_index_finger_and_thumb_crossed_light_skin_tone: - U1FAF01F3FE = "🫰🏾" # :hand_with_index_finger_and_thumb_crossed_medium-dark_skin_tone: - U1FAF01F3FC = "🫰🏼" # :hand_with_index_finger_and_thumb_crossed_medium-light_skin_tone: - U1FAF01F3FD = "🫰🏽" # :hand_with_index_finger_and_thumb_crossed_medium_skin_tone: - U1F45C = "👜" # :handbag: - U1F91D = "🤝" # :handshake: - U1F91D1F3FF = "🤝🏿" # :handshake_dark_skin_tone: - U1FAF11F3FF200D1FAF21F3FB = "🫱🏿‍🫲🏻" # :handshake_dark_skin_tone_light_skin_tone: - U1FAF11F3FF200D1FAF21F3FE = "🫱🏿‍🫲🏾" # :handshake_dark_skin_tone_medium-dark_skin_tone: - U1FAF11F3FF200D1FAF21F3FC = "🫱🏿‍🫲🏼" # :handshake_dark_skin_tone_medium-light_skin_tone: - U1FAF11F3FF200D1FAF21F3FD = "🫱🏿‍🫲🏽" # :handshake_dark_skin_tone_medium_skin_tone: - U1F91D1F3FB = "🤝🏻" # :handshake_light_skin_tone: - U1FAF11F3FB200D1FAF21F3FF = "🫱🏻‍🫲🏿" # :handshake_light_skin_tone_dark_skin_tone: - U1FAF11F3FB200D1FAF21F3FE = "🫱🏻‍🫲🏾" # :handshake_light_skin_tone_medium-dark_skin_tone: - U1FAF11F3FB200D1FAF21F3FC = "🫱🏻‍🫲🏼" # :handshake_light_skin_tone_medium-light_skin_tone: - U1FAF11F3FB200D1FAF21F3FD = "🫱🏻‍🫲🏽" # :handshake_light_skin_tone_medium_skin_tone: - U1F91D1F3FE = "🤝🏾" # :handshake_medium-dark_skin_tone: - U1FAF11F3FE200D1FAF21F3FF = "🫱🏾‍🫲🏿" # :handshake_medium-dark_skin_tone_dark_skin_tone: - U1FAF11F3FE200D1FAF21F3FB = "🫱🏾‍🫲🏻" # :handshake_medium-dark_skin_tone_light_skin_tone: - U1FAF11F3FE200D1FAF21F3FC = "🫱🏾‍🫲🏼" # :handshake_medium-dark_skin_tone_medium-light_skin_tone: - U1FAF11F3FE200D1FAF21F3FD = "🫱🏾‍🫲🏽" # :handshake_medium-dark_skin_tone_medium_skin_tone: - U1F91D1F3FC = "🤝🏼" # :handshake_medium-light_skin_tone: - U1FAF11F3FC200D1FAF21F3FF = "🫱🏼‍🫲🏿" # :handshake_medium-light_skin_tone_dark_skin_tone: - U1FAF11F3FC200D1FAF21F3FB = "🫱🏼‍🫲🏻" # :handshake_medium-light_skin_tone_light_skin_tone: - U1FAF11F3FC200D1FAF21F3FE = "🫱🏼‍🫲🏾" # :handshake_medium-light_skin_tone_medium-dark_skin_tone: - U1FAF11F3FC200D1FAF21F3FD = "🫱🏼‍🫲🏽" # :handshake_medium-light_skin_tone_medium_skin_tone: - U1F91D1F3FD = "🤝🏽" # :handshake_medium_skin_tone: - U1FAF11F3FD200D1FAF21F3FF = "🫱🏽‍🫲🏿" # :handshake_medium_skin_tone_dark_skin_tone: - U1FAF11F3FD200D1FAF21F3FB = "🫱🏽‍🫲🏻" # :handshake_medium_skin_tone_light_skin_tone: - U1FAF11F3FD200D1FAF21F3FE = "🫱🏽‍🫲🏾" # :handshake_medium_skin_tone_medium-dark_skin_tone: - U1FAF11F3FD200D1FAF21F3FC = "🫱🏽‍🫲🏼" # :handshake_medium_skin_tone_medium-light_skin_tone: - U1F423 = "🐣" # :hatching_chick: - U1F642200D2194FE0F = "🙂‍↔️" # :head_shaking_horizontally: - U1F642200D2194 = "🙂‍↔" # :head_shaking_horizontally: - U1F642200D2195FE0F = "🙂‍↕️" # :head_shaking_vertically: - U1F642200D2195 = "🙂‍↕" # :head_shaking_vertically: - U1F3A7 = "🎧" # :headphone: - U1FAA6 = "🪦" # :headstone: - U1F9D1200D2695FE0F = "🧑‍⚕️" # :health_worker: - U1F9D1200D2695 = "🧑‍⚕" # :health_worker: - U1F9D11F3FF200D2695FE0F = "🧑🏿‍⚕️" # :health_worker_dark_skin_tone: - U1F9D11F3FF200D2695 = "🧑🏿‍⚕" # :health_worker_dark_skin_tone: - U1F9D11F3FB200D2695FE0F = "🧑🏻‍⚕️" # :health_worker_light_skin_tone: - U1F9D11F3FB200D2695 = "🧑🏻‍⚕" # :health_worker_light_skin_tone: - U1F9D11F3FE200D2695FE0F = "🧑🏾‍⚕️" # :health_worker_medium-dark_skin_tone: - U1F9D11F3FE200D2695 = "🧑🏾‍⚕" # :health_worker_medium-dark_skin_tone: - U1F9D11F3FC200D2695FE0F = "🧑🏼‍⚕️" # :health_worker_medium-light_skin_tone: - U1F9D11F3FC200D2695 = "🧑🏼‍⚕" # :health_worker_medium-light_skin_tone: - U1F9D11F3FD200D2695FE0F = "🧑🏽‍⚕️" # :health_worker_medium_skin_tone: - U1F9D11F3FD200D2695 = "🧑🏽‍⚕" # :health_worker_medium_skin_tone: - U1F649 = "🙉" # :hear-no-evil_monkey: - U1F49F = "💟" # :heart_decoration: - U2763FE0F = "❣️" # :heart_exclamation: - U2763 = "❣" # :heart_exclamation: - U1FAF6 = "🫶" # :heart_hands: - U1FAF61F3FF = "🫶🏿" # :heart_hands_dark_skin_tone: - U1FAF61F3FB = "🫶🏻" # :heart_hands_light_skin_tone: - U1FAF61F3FE = "🫶🏾" # :heart_hands_medium-dark_skin_tone: - U1FAF61F3FC = "🫶🏼" # :heart_hands_medium-light_skin_tone: - U1FAF61F3FD = "🫶🏽" # :heart_hands_medium_skin_tone: - U2764FE0F200D1F525 = "❤️‍🔥" # :heart_on_fire: - U2764200D1F525 = "❤‍🔥" # :heart_on_fire: - U2665FE0F = "♥️" # :heart_suit: - U2665 = "♥" # :heart_suit: - U1F498 = "💘" # :heart_with_arrow: - U1F49D = "💝" # :heart_with_ribbon: - U1F4B2 = "💲" # :heavy_dollar_sign: - U1F7F0 = "🟰" # :heavy_equals_sign: - U1F994 = "🦔" # :hedgehog: - U1F681 = "🚁" # :helicopter: - U1F33F = "🌿" # :herb: - U1F33A = "🌺" # :hibiscus: - U1F460 = "👠" # :high-heeled_shoe: - U1F684 = "🚄" # :high-speed_train: - U26A1 = "⚡" # :high_voltage: - U1F97E = "🥾" # :hiking_boot: - U1F6D5 = "🛕" # :hindu_temple: - U1F99B = "🦛" # :hippopotamus: - U1F573FE0F = "🕳️" # :hole: - U1F573 = "🕳" # :hole: - U2B55 = "⭕" # :hollow_red_circle: - U1F36F = "🍯" # :honey_pot: - U1F41D = "🐝" # :honeybee: - U1FA9D = "🪝" # :hook: - U1F6A5 = "🚥" # :horizontal_traffic_light: - U1F40E = "🐎" # :horse: - U1F434 = "🐴" # :horse_face: - U1F3C7 = "🏇" # :horse_racing: - U1F3C71F3FF = "🏇🏿" # :horse_racing_dark_skin_tone: - U1F3C71F3FB = "🏇🏻" # :horse_racing_light_skin_tone: - U1F3C71F3FE = "🏇🏾" # :horse_racing_medium-dark_skin_tone: - U1F3C71F3FC = "🏇🏼" # :horse_racing_medium-light_skin_tone: - U1F3C71F3FD = "🏇🏽" # :horse_racing_medium_skin_tone: - U1F3E5 = "🏥" # :hospital: - U2615 = "☕" # :hot_beverage: - U1F32D = "🌭" # :hot_dog: - U1F975 = "🥵" # :hot_face: - U1F336FE0F = "🌶️" # :hot_pepper: - U1F336 = "🌶" # :hot_pepper: - U2668FE0F = "♨️" # :hot_springs: - U2668 = "♨" # :hot_springs: - U1F3E8 = "🏨" # :hotel: - U231B = "⌛" # :hourglass_done: - U23F3 = "⏳" # :hourglass_not_done: - U1F3E0 = "🏠" # :house: - U1F3E1 = "🏡" # :house_with_garden: - U1F3D8FE0F = "🏘️" # :houses: - U1F3D8 = "🏘" # :houses: - U1F4AF = "💯" # :hundred_points: - U1F62F = "😯" # :hushed_face: - U1F6D6 = "🛖" # :hut: - U1FABB = "🪻" # :hyacinth: - U1F9CA = "🧊" # :ice: - U1F368 = "🍨" # :ice_cream: - U1F3D2 = "🏒" # :ice_hockey: - U26F8FE0F = "⛸️" # :ice_skate: - U26F8 = "⛸" # :ice_skate: - U1FAAA = "🪪" # :identification_card: - U1F4E5 = "📥" # :inbox_tray: - U1F4E8 = "📨" # :incoming_envelope: - U1FAF5 = "🫵" # :index_pointing_at_the_viewer: - U1FAF51F3FF = "🫵🏿" # :index_pointing_at_the_viewer_dark_skin_tone: - U1FAF51F3FB = "🫵🏻" # :index_pointing_at_the_viewer_light_skin_tone: - U1FAF51F3FE = "🫵🏾" # :index_pointing_at_the_viewer_medium-dark_skin_tone: - U1FAF51F3FC = "🫵🏼" # :index_pointing_at_the_viewer_medium-light_skin_tone: - U1FAF51F3FD = "🫵🏽" # :index_pointing_at_the_viewer_medium_skin_tone: - U261DFE0F = "☝️" # :index_pointing_up: - U261D = "☝" # :index_pointing_up: - U261D1F3FF = "☝🏿" # :index_pointing_up_dark_skin_tone: - U261D1F3FB = "☝🏻" # :index_pointing_up_light_skin_tone: - U261D1F3FE = "☝🏾" # :index_pointing_up_medium-dark_skin_tone: - U261D1F3FC = "☝🏼" # :index_pointing_up_medium-light_skin_tone: - U261D1F3FD = "☝🏽" # :index_pointing_up_medium_skin_tone: - U267EFE0F = "♾️" # :infinity: - U267E = "♾" # :infinity: - U2139FE0F = "ℹ️" # :information: - U2139 = "ℹ" # :information: - U1F524 = "🔤" # :input_latin_letters: - U1F521 = "🔡" # :input_latin_lowercase: - U1F520 = "🔠" # :input_latin_uppercase: - U1F522 = "🔢" # :input_numbers: - U1F523 = "🔣" # :input_symbols: - U1F383 = "🎃" # :jack-o-lantern: - U1FAD9 = "🫙" # :jar: - U1F456 = "👖" # :jeans: - U1FABC = "🪼" # :jellyfish: - U1F0CF = "🃏" # :joker: - U1F579FE0F = "🕹️" # :joystick: - U1F579 = "🕹" # :joystick: - U1F9D1200D2696FE0F = "🧑‍⚖️" # :judge: - U1F9D1200D2696 = "🧑‍⚖" # :judge: - U1F9D11F3FF200D2696FE0F = "🧑🏿‍⚖️" # :judge_dark_skin_tone: - U1F9D11F3FF200D2696 = "🧑🏿‍⚖" # :judge_dark_skin_tone: - U1F9D11F3FB200D2696FE0F = "🧑🏻‍⚖️" # :judge_light_skin_tone: - U1F9D11F3FB200D2696 = "🧑🏻‍⚖" # :judge_light_skin_tone: - U1F9D11F3FE200D2696FE0F = "🧑🏾‍⚖️" # :judge_medium-dark_skin_tone: - U1F9D11F3FE200D2696 = "🧑🏾‍⚖" # :judge_medium-dark_skin_tone: - U1F9D11F3FC200D2696FE0F = "🧑🏼‍⚖️" # :judge_medium-light_skin_tone: - U1F9D11F3FC200D2696 = "🧑🏼‍⚖" # :judge_medium-light_skin_tone: - U1F9D11F3FD200D2696FE0F = "🧑🏽‍⚖️" # :judge_medium_skin_tone: - U1F9D11F3FD200D2696 = "🧑🏽‍⚖" # :judge_medium_skin_tone: - U1F54B = "🕋" # :kaaba: - U1F998 = "🦘" # :kangaroo: - U1F511 = "🔑" # :key: - U2328FE0F = "⌨️" # :keyboard: - U2328 = "⌨" # :keyboard: - U23FE0F20E3 = "#️⃣" # :keycap_#: - U2320E3 = "#⃣" # :keycap_#: - U2AFE0F20E3 = "*️⃣" # :keycap_*: - U2A20E3 = "*⃣" # :keycap_*: - U30FE0F20E3 = "0️⃣" # :keycap_0: - U3020E3 = "0⃣" # :keycap_0: - U31FE0F20E3 = "1️⃣" # :keycap_1: - U3120E3 = "1⃣" # :keycap_1: - U1F51F = "🔟" # :keycap_10: - U32FE0F20E3 = "2️⃣" # :keycap_2: - U3220E3 = "2⃣" # :keycap_2: - U33FE0F20E3 = "3️⃣" # :keycap_3: - U3320E3 = "3⃣" # :keycap_3: - U34FE0F20E3 = "4️⃣" # :keycap_4: - U3420E3 = "4⃣" # :keycap_4: - U35FE0F20E3 = "5️⃣" # :keycap_5: - U3520E3 = "5⃣" # :keycap_5: - U36FE0F20E3 = "6️⃣" # :keycap_6: - U3620E3 = "6⃣" # :keycap_6: - U37FE0F20E3 = "7️⃣" # :keycap_7: - U3720E3 = "7⃣" # :keycap_7: - U38FE0F20E3 = "8️⃣" # :keycap_8: - U3820E3 = "8⃣" # :keycap_8: - U39FE0F20E3 = "9️⃣" # :keycap_9: - U3920E3 = "9⃣" # :keycap_9: - U1FAAF = "🪯" # :khanda: - U1F6F4 = "🛴" # :kick_scooter: - U1F458 = "👘" # :kimono: - U1F48F = "💏" # :kiss: - U1F48F1F3FF = "💏🏿" # :kiss_dark_skin_tone: - U1F48F1F3FB = "💏🏻" # :kiss_light_skin_tone: - U1F468200D2764FE0F200D1F48B200D1F468 = "👨‍❤️‍💋‍👨" # :kiss_man_man: - U1F468200D2764200D1F48B200D1F468 = "👨‍❤‍💋‍👨" # :kiss_man_man: - U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏿‍❤️‍💋‍👨🏿" # :kiss_man_man_dark_skin_tone: - U1F4681F3FF200D2764200D1F48B200D1F4681F3FF = "👨🏿‍❤‍💋‍👨🏿" # :kiss_man_man_dark_skin_tone: - U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏿‍❤️‍💋‍👨🏻" # :kiss_man_man_dark_skin_tone_light_skin_tone: - U1F4681F3FF200D2764200D1F48B200D1F4681F3FB = "👨🏿‍❤‍💋‍👨🏻" # :kiss_man_man_dark_skin_tone_light_skin_tone: - U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏿‍❤️‍💋‍👨🏾" # :kiss_man_man_dark_skin_tone_medium-dark_skin_tone: - U1F4681F3FF200D2764200D1F48B200D1F4681F3FE = "👨🏿‍❤‍💋‍👨🏾" # :kiss_man_man_dark_skin_tone_medium-dark_skin_tone: - U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏿‍❤️‍💋‍👨🏼" # :kiss_man_man_dark_skin_tone_medium-light_skin_tone: - U1F4681F3FF200D2764200D1F48B200D1F4681F3FC = "👨🏿‍❤‍💋‍👨🏼" # :kiss_man_man_dark_skin_tone_medium-light_skin_tone: - U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏿‍❤️‍💋‍👨🏽" # :kiss_man_man_dark_skin_tone_medium_skin_tone: - U1F4681F3FF200D2764200D1F48B200D1F4681F3FD = "👨🏿‍❤‍💋‍👨🏽" # :kiss_man_man_dark_skin_tone_medium_skin_tone: - U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏻‍❤️‍💋‍👨🏻" # :kiss_man_man_light_skin_tone: - U1F4681F3FB200D2764200D1F48B200D1F4681F3FB = "👨🏻‍❤‍💋‍👨🏻" # :kiss_man_man_light_skin_tone: - U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏻‍❤️‍💋‍👨🏿" # :kiss_man_man_light_skin_tone_dark_skin_tone: - U1F4681F3FB200D2764200D1F48B200D1F4681F3FF = "👨🏻‍❤‍💋‍👨🏿" # :kiss_man_man_light_skin_tone_dark_skin_tone: - U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏻‍❤️‍💋‍👨🏾" # :kiss_man_man_light_skin_tone_medium-dark_skin_tone: - U1F4681F3FB200D2764200D1F48B200D1F4681F3FE = "👨🏻‍❤‍💋‍👨🏾" # :kiss_man_man_light_skin_tone_medium-dark_skin_tone: - U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏻‍❤️‍💋‍👨🏼" # :kiss_man_man_light_skin_tone_medium-light_skin_tone: - U1F4681F3FB200D2764200D1F48B200D1F4681F3FC = "👨🏻‍❤‍💋‍👨🏼" # :kiss_man_man_light_skin_tone_medium-light_skin_tone: - U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏻‍❤️‍💋‍👨🏽" # :kiss_man_man_light_skin_tone_medium_skin_tone: - U1F4681F3FB200D2764200D1F48B200D1F4681F3FD = "👨🏻‍❤‍💋‍👨🏽" # :kiss_man_man_light_skin_tone_medium_skin_tone: - U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏾‍❤️‍💋‍👨🏾" # :kiss_man_man_medium-dark_skin_tone: - U1F4681F3FE200D2764200D1F48B200D1F4681F3FE = "👨🏾‍❤‍💋‍👨🏾" # :kiss_man_man_medium-dark_skin_tone: - U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏾‍❤️‍💋‍👨🏿" # :kiss_man_man_medium-dark_skin_tone_dark_skin_tone: - U1F4681F3FE200D2764200D1F48B200D1F4681F3FF = "👨🏾‍❤‍💋‍👨🏿" # :kiss_man_man_medium-dark_skin_tone_dark_skin_tone: - U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏾‍❤️‍💋‍👨🏻" # :kiss_man_man_medium-dark_skin_tone_light_skin_tone: - U1F4681F3FE200D2764200D1F48B200D1F4681F3FB = "👨🏾‍❤‍💋‍👨🏻" # :kiss_man_man_medium-dark_skin_tone_light_skin_tone: - U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏾‍❤️‍💋‍👨🏼" # :kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4681F3FE200D2764200D1F48B200D1F4681F3FC = "👨🏾‍❤‍💋‍👨🏼" # :kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏾‍❤️‍💋‍👨🏽" # :kiss_man_man_medium-dark_skin_tone_medium_skin_tone: - U1F4681F3FE200D2764200D1F48B200D1F4681F3FD = "👨🏾‍❤‍💋‍👨🏽" # :kiss_man_man_medium-dark_skin_tone_medium_skin_tone: - U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏼‍❤️‍💋‍👨🏼" # :kiss_man_man_medium-light_skin_tone: - U1F4681F3FC200D2764200D1F48B200D1F4681F3FC = "👨🏼‍❤‍💋‍👨🏼" # :kiss_man_man_medium-light_skin_tone: - U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏼‍❤️‍💋‍👨🏿" # :kiss_man_man_medium-light_skin_tone_dark_skin_tone: - U1F4681F3FC200D2764200D1F48B200D1F4681F3FF = "👨🏼‍❤‍💋‍👨🏿" # :kiss_man_man_medium-light_skin_tone_dark_skin_tone: - U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏼‍❤️‍💋‍👨🏻" # :kiss_man_man_medium-light_skin_tone_light_skin_tone: - U1F4681F3FC200D2764200D1F48B200D1F4681F3FB = "👨🏼‍❤‍💋‍👨🏻" # :kiss_man_man_medium-light_skin_tone_light_skin_tone: - U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏼‍❤️‍💋‍👨🏾" # :kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4681F3FC200D2764200D1F48B200D1F4681F3FE = "👨🏼‍❤‍💋‍👨🏾" # :kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏼‍❤️‍💋‍👨🏽" # :kiss_man_man_medium-light_skin_tone_medium_skin_tone: - U1F4681F3FC200D2764200D1F48B200D1F4681F3FD = "👨🏼‍❤‍💋‍👨🏽" # :kiss_man_man_medium-light_skin_tone_medium_skin_tone: - U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏽‍❤️‍💋‍👨🏽" # :kiss_man_man_medium_skin_tone: - U1F4681F3FD200D2764200D1F48B200D1F4681F3FD = "👨🏽‍❤‍💋‍👨🏽" # :kiss_man_man_medium_skin_tone: - U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏽‍❤️‍💋‍👨🏿" # :kiss_man_man_medium_skin_tone_dark_skin_tone: - U1F4681F3FD200D2764200D1F48B200D1F4681F3FF = "👨🏽‍❤‍💋‍👨🏿" # :kiss_man_man_medium_skin_tone_dark_skin_tone: - U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏽‍❤️‍💋‍👨🏻" # :kiss_man_man_medium_skin_tone_light_skin_tone: - U1F4681F3FD200D2764200D1F48B200D1F4681F3FB = "👨🏽‍❤‍💋‍👨🏻" # :kiss_man_man_medium_skin_tone_light_skin_tone: - U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏽‍❤️‍💋‍👨🏾" # :kiss_man_man_medium_skin_tone_medium-dark_skin_tone: - U1F4681F3FD200D2764200D1F48B200D1F4681F3FE = "👨🏽‍❤‍💋‍👨🏾" # :kiss_man_man_medium_skin_tone_medium-dark_skin_tone: - U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏽‍❤️‍💋‍👨🏼" # :kiss_man_man_medium_skin_tone_medium-light_skin_tone: - U1F4681F3FD200D2764200D1F48B200D1F4681F3FC = "👨🏽‍❤‍💋‍👨🏼" # :kiss_man_man_medium_skin_tone_medium-light_skin_tone: - U1F48B = "💋" # :kiss_mark: - U1F48F1F3FE = "💏🏾" # :kiss_medium-dark_skin_tone: - U1F48F1F3FC = "💏🏼" # :kiss_medium-light_skin_tone: - U1F48F1F3FD = "💏🏽" # :kiss_medium_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏿‍❤️‍💋‍🧑🏻" # :kiss_person_person_dark_skin_tone_light_skin_tone: - U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FB = "🧑🏿‍❤‍💋‍🧑🏻" # :kiss_person_person_dark_skin_tone_light_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏿‍❤️‍💋‍🧑🏾" # :kiss_person_person_dark_skin_tone_medium-dark_skin_tone: - U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FE = "🧑🏿‍❤‍💋‍🧑🏾" # :kiss_person_person_dark_skin_tone_medium-dark_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏿‍❤️‍💋‍🧑🏼" # :kiss_person_person_dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FC = "🧑🏿‍❤‍💋‍🧑🏼" # :kiss_person_person_dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏿‍❤️‍💋‍🧑🏽" # :kiss_person_person_dark_skin_tone_medium_skin_tone: - U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FD = "🧑🏿‍❤‍💋‍🧑🏽" # :kiss_person_person_dark_skin_tone_medium_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏻‍❤️‍💋‍🧑🏿" # :kiss_person_person_light_skin_tone_dark_skin_tone: - U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FF = "🧑🏻‍❤‍💋‍🧑🏿" # :kiss_person_person_light_skin_tone_dark_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏻‍❤️‍💋‍🧑🏾" # :kiss_person_person_light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FE = "🧑🏻‍❤‍💋‍🧑🏾" # :kiss_person_person_light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏻‍❤️‍💋‍🧑🏼" # :kiss_person_person_light_skin_tone_medium-light_skin_tone: - U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FC = "🧑🏻‍❤‍💋‍🧑🏼" # :kiss_person_person_light_skin_tone_medium-light_skin_tone: - U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏻‍❤️‍💋‍🧑🏽" # :kiss_person_person_light_skin_tone_medium_skin_tone: - U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FD = "🧑🏻‍❤‍💋‍🧑🏽" # :kiss_person_person_light_skin_tone_medium_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏾‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium-dark_skin_tone_dark_skin_tone: - U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FF = "🧑🏾‍❤‍💋‍🧑🏿" # :kiss_person_person_medium-dark_skin_tone_dark_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏾‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium-dark_skin_tone_light_skin_tone: - U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FB = "🧑🏾‍❤‍💋‍🧑🏻" # :kiss_person_person_medium-dark_skin_tone_light_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏾‍❤️‍💋‍🧑🏼" # :kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FC = "🧑🏾‍❤‍💋‍🧑🏼" # :kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏾‍❤️‍💋‍🧑🏽" # :kiss_person_person_medium-dark_skin_tone_medium_skin_tone: - U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FD = "🧑🏾‍❤‍💋‍🧑🏽" # :kiss_person_person_medium-dark_skin_tone_medium_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏼‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium-light_skin_tone_dark_skin_tone: - U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FF = "🧑🏼‍❤‍💋‍🧑🏿" # :kiss_person_person_medium-light_skin_tone_dark_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏼‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium-light_skin_tone_light_skin_tone: - U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FB = "🧑🏼‍❤‍💋‍🧑🏻" # :kiss_person_person_medium-light_skin_tone_light_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏼‍❤️‍💋‍🧑🏾" # :kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FE = "🧑🏼‍❤‍💋‍🧑🏾" # :kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏼‍❤️‍💋‍🧑🏽" # :kiss_person_person_medium-light_skin_tone_medium_skin_tone: - U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FD = "🧑🏼‍❤‍💋‍🧑🏽" # :kiss_person_person_medium-light_skin_tone_medium_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏽‍❤️‍💋‍🧑🏿" # :kiss_person_person_medium_skin_tone_dark_skin_tone: - U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FF = "🧑🏽‍❤‍💋‍🧑🏿" # :kiss_person_person_medium_skin_tone_dark_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏽‍❤️‍💋‍🧑🏻" # :kiss_person_person_medium_skin_tone_light_skin_tone: - U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FB = "🧑🏽‍❤‍💋‍🧑🏻" # :kiss_person_person_medium_skin_tone_light_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏽‍❤️‍💋‍🧑🏾" # :kiss_person_person_medium_skin_tone_medium-dark_skin_tone: - U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FE = "🧑🏽‍❤‍💋‍🧑🏾" # :kiss_person_person_medium_skin_tone_medium-dark_skin_tone: - U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏽‍❤️‍💋‍🧑🏼" # :kiss_person_person_medium_skin_tone_medium-light_skin_tone: - U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FC = "🧑🏽‍❤‍💋‍🧑🏼" # :kiss_person_person_medium_skin_tone_medium-light_skin_tone: - U1F469200D2764FE0F200D1F48B200D1F468 = "👩‍❤️‍💋‍👨" # :kiss_woman_man: - U1F469200D2764200D1F48B200D1F468 = "👩‍❤‍💋‍👨" # :kiss_woman_man: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏿‍❤️‍💋‍👨🏿" # :kiss_woman_man_dark_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4681F3FF = "👩🏿‍❤‍💋‍👨🏿" # :kiss_woman_man_dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏿‍❤️‍💋‍👨🏻" # :kiss_woman_man_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4681F3FB = "👩🏿‍❤‍💋‍👨🏻" # :kiss_woman_man_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏿‍❤️‍💋‍👨🏾" # :kiss_woman_man_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4681F3FE = "👩🏿‍❤‍💋‍👨🏾" # :kiss_woman_man_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏿‍❤️‍💋‍👨🏼" # :kiss_woman_man_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4681F3FC = "👩🏿‍❤‍💋‍👨🏼" # :kiss_woman_man_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏿‍❤️‍💋‍👨🏽" # :kiss_woman_man_dark_skin_tone_medium_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4681F3FD = "👩🏿‍❤‍💋‍👨🏽" # :kiss_woman_man_dark_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏻‍❤️‍💋‍👨🏻" # :kiss_woman_man_light_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4681F3FB = "👩🏻‍❤‍💋‍👨🏻" # :kiss_woman_man_light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏻‍❤️‍💋‍👨🏿" # :kiss_woman_man_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4681F3FF = "👩🏻‍❤‍💋‍👨🏿" # :kiss_woman_man_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏻‍❤️‍💋‍👨🏾" # :kiss_woman_man_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4681F3FE = "👩🏻‍❤‍💋‍👨🏾" # :kiss_woman_man_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏻‍❤️‍💋‍👨🏼" # :kiss_woman_man_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4681F3FC = "👩🏻‍❤‍💋‍👨🏼" # :kiss_woman_man_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏻‍❤️‍💋‍👨🏽" # :kiss_woman_man_light_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4681F3FD = "👩🏻‍❤‍💋‍👨🏽" # :kiss_woman_man_light_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏾‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium-dark_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4681F3FE = "👩🏾‍❤‍💋‍👨🏾" # :kiss_woman_man_medium-dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏾‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4681F3FF = "👩🏾‍❤‍💋‍👨🏿" # :kiss_woman_man_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏾‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4681F3FB = "👩🏾‍❤‍💋‍👨🏻" # :kiss_woman_man_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏾‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4681F3FC = "👩🏾‍❤‍💋‍👨🏼" # :kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏾‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4681F3FD = "👩🏾‍❤‍💋‍👨🏽" # :kiss_woman_man_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏼‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium-light_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4681F3FC = "👩🏼‍❤‍💋‍👨🏼" # :kiss_woman_man_medium-light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏼‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4681F3FF = "👩🏼‍❤‍💋‍👨🏿" # :kiss_woman_man_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏼‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4681F3FB = "👩🏼‍❤‍💋‍👨🏻" # :kiss_woman_man_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏼‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4681F3FE = "👩🏼‍❤‍💋‍👨🏾" # :kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏼‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4681F3FD = "👩🏼‍❤‍💋‍👨🏽" # :kiss_woman_man_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏽‍❤️‍💋‍👨🏽" # :kiss_woman_man_medium_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4681F3FD = "👩🏽‍❤‍💋‍👨🏽" # :kiss_woman_man_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏽‍❤️‍💋‍👨🏿" # :kiss_woman_man_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4681F3FF = "👩🏽‍❤‍💋‍👨🏿" # :kiss_woman_man_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏽‍❤️‍💋‍👨🏻" # :kiss_woman_man_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4681F3FB = "👩🏽‍❤‍💋‍👨🏻" # :kiss_woman_man_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏽‍❤️‍💋‍👨🏾" # :kiss_woman_man_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4681F3FE = "👩🏽‍❤‍💋‍👨🏾" # :kiss_woman_man_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏽‍❤️‍💋‍👨🏼" # :kiss_woman_man_medium_skin_tone_medium-light_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4681F3FC = "👩🏽‍❤‍💋‍👨🏼" # :kiss_woman_man_medium_skin_tone_medium-light_skin_tone: - U1F469200D2764FE0F200D1F48B200D1F469 = "👩‍❤️‍💋‍👩" # :kiss_woman_woman: - U1F469200D2764200D1F48B200D1F469 = "👩‍❤‍💋‍👩" # :kiss_woman_woman: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏿‍❤️‍💋‍👩🏿" # :kiss_woman_woman_dark_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4691F3FF = "👩🏿‍❤‍💋‍👩🏿" # :kiss_woman_woman_dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏿‍❤️‍💋‍👩🏻" # :kiss_woman_woman_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4691F3FB = "👩🏿‍❤‍💋‍👩🏻" # :kiss_woman_woman_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏿‍❤️‍💋‍👩🏾" # :kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4691F3FE = "👩🏿‍❤‍💋‍👩🏾" # :kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏿‍❤️‍💋‍👩🏼" # :kiss_woman_woman_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4691F3FC = "👩🏿‍❤‍💋‍👩🏼" # :kiss_woman_woman_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏿‍❤️‍💋‍👩🏽" # :kiss_woman_woman_dark_skin_tone_medium_skin_tone: - U1F4691F3FF200D2764200D1F48B200D1F4691F3FD = "👩🏿‍❤‍💋‍👩🏽" # :kiss_woman_woman_dark_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏻‍❤️‍💋‍👩🏻" # :kiss_woman_woman_light_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4691F3FB = "👩🏻‍❤‍💋‍👩🏻" # :kiss_woman_woman_light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏻‍❤️‍💋‍👩🏿" # :kiss_woman_woman_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4691F3FF = "👩🏻‍❤‍💋‍👩🏿" # :kiss_woman_woman_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏻‍❤️‍💋‍👩🏾" # :kiss_woman_woman_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4691F3FE = "👩🏻‍❤‍💋‍👩🏾" # :kiss_woman_woman_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏻‍❤️‍💋‍👩🏼" # :kiss_woman_woman_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4691F3FC = "👩🏻‍❤‍💋‍👩🏼" # :kiss_woman_woman_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏻‍❤️‍💋‍👩🏽" # :kiss_woman_woman_light_skin_tone_medium_skin_tone: - U1F4691F3FB200D2764200D1F48B200D1F4691F3FD = "👩🏻‍❤‍💋‍👩🏽" # :kiss_woman_woman_light_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏾‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium-dark_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4691F3FE = "👩🏾‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium-dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏾‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4691F3FF = "👩🏾‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏾‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4691F3FB = "👩🏾‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏾‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4691F3FC = "👩🏾‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏾‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FE200D2764200D1F48B200D1F4691F3FD = "👩🏾‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏼‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium-light_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4691F3FC = "👩🏼‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium-light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏼‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4691F3FF = "👩🏼‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏼‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4691F3FB = "👩🏼‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏼‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4691F3FE = "👩🏼‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏼‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FC200D2764200D1F48B200D1F4691F3FD = "👩🏼‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium-light_skin_tone_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏽‍❤️‍💋‍👩🏽" # :kiss_woman_woman_medium_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4691F3FD = "👩🏽‍❤‍💋‍👩🏽" # :kiss_woman_woman_medium_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏽‍❤️‍💋‍👩🏿" # :kiss_woman_woman_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4691F3FF = "👩🏽‍❤‍💋‍👩🏿" # :kiss_woman_woman_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏽‍❤️‍💋‍👩🏻" # :kiss_woman_woman_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4691F3FB = "👩🏽‍❤‍💋‍👩🏻" # :kiss_woman_woman_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏽‍❤️‍💋‍👩🏾" # :kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4691F3FE = "👩🏽‍❤‍💋‍👩🏾" # :kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏽‍❤️‍💋‍👩🏼" # :kiss_woman_woman_medium_skin_tone_medium-light_skin_tone: - U1F4691F3FD200D2764200D1F48B200D1F4691F3FC = "👩🏽‍❤‍💋‍👩🏼" # :kiss_woman_woman_medium_skin_tone_medium-light_skin_tone: - U1F63D = "😽" # :kissing_cat: - U1F617 = "😗" # :kissing_face: - U1F61A = "😚" # :kissing_face_with_closed_eyes: - U1F619 = "😙" # :kissing_face_with_smiling_eyes: - U1F52A = "🔪" # :kitchen_knife: - U1FA81 = "🪁" # :kite: - U1F95D = "🥝" # :kiwi_fruit: - U1FAA2 = "🪢" # :knot: - U1F428 = "🐨" # :koala: - U1F97C = "🥼" # :lab_coat: - U1F3F7FE0F = "🏷️" # :label: - U1F3F7 = "🏷" # :label: - U1F94D = "🥍" # :lacrosse: - U1FA9C = "🪜" # :ladder: - U1F41E = "🐞" # :lady_beetle: - U1F4BB = "💻" # :laptop: - U1F537 = "🔷" # :large_blue_diamond: - U1F536 = "🔶" # :large_orange_diamond: - U1F317 = "🌗" # :last_quarter_moon: - U1F31C = "🌜" # :last_quarter_moon_face: - U23EEFE0F = "⏮️" # :last_track_button: - U23EE = "⏮" # :last_track_button: - U271DFE0F = "✝️" # :latin_cross: - U271D = "✝" # :latin_cross: - U1F343 = "🍃" # :leaf_fluttering_in_wind: - U1F96C = "🥬" # :leafy_green: - U1F4D2 = "📒" # :ledger: - U1F91B = "🤛" # :left-facing_fist: - U1F91B1F3FF = "🤛🏿" # :left-facing_fist_dark_skin_tone: - U1F91B1F3FB = "🤛🏻" # :left-facing_fist_light_skin_tone: - U1F91B1F3FE = "🤛🏾" # :left-facing_fist_medium-dark_skin_tone: - U1F91B1F3FC = "🤛🏼" # :left-facing_fist_medium-light_skin_tone: - U1F91B1F3FD = "🤛🏽" # :left-facing_fist_medium_skin_tone: - U2194FE0F = "↔️" # :left-right_arrow: - U2194 = "↔" # :left-right_arrow: - U2B05FE0F = "⬅️" # :left_arrow: - U2B05 = "⬅" # :left_arrow: - U21AAFE0F = "↪️" # :left_arrow_curving_right: - U21AA = "↪" # :left_arrow_curving_right: - U1F6C5 = "🛅" # :left_luggage: - U1F5E8FE0F = "🗨️" # :left_speech_bubble: - U1F5E8 = "🗨" # :left_speech_bubble: - U1FAF2 = "🫲" # :leftwards_hand: - U1FAF21F3FF = "🫲🏿" # :leftwards_hand_dark_skin_tone: - U1FAF21F3FB = "🫲🏻" # :leftwards_hand_light_skin_tone: - U1FAF21F3FE = "🫲🏾" # :leftwards_hand_medium-dark_skin_tone: - U1FAF21F3FC = "🫲🏼" # :leftwards_hand_medium-light_skin_tone: - U1FAF21F3FD = "🫲🏽" # :leftwards_hand_medium_skin_tone: - U1FAF7 = "🫷" # :leftwards_pushing_hand: - U1FAF71F3FF = "🫷🏿" # :leftwards_pushing_hand_dark_skin_tone: - U1FAF71F3FB = "🫷🏻" # :leftwards_pushing_hand_light_skin_tone: - U1FAF71F3FE = "🫷🏾" # :leftwards_pushing_hand_medium-dark_skin_tone: - U1FAF71F3FC = "🫷🏼" # :leftwards_pushing_hand_medium-light_skin_tone: - U1FAF71F3FD = "🫷🏽" # :leftwards_pushing_hand_medium_skin_tone: - U1F9B5 = "🦵" # :leg: - U1F9B51F3FF = "🦵🏿" # :leg_dark_skin_tone: - U1F9B51F3FB = "🦵🏻" # :leg_light_skin_tone: - U1F9B51F3FE = "🦵🏾" # :leg_medium-dark_skin_tone: - U1F9B51F3FC = "🦵🏼" # :leg_medium-light_skin_tone: - U1F9B51F3FD = "🦵🏽" # :leg_medium_skin_tone: - U1F34B = "🍋" # :lemon: - U1F406 = "🐆" # :leopard: - U1F39AFE0F = "🎚️" # :level_slider: - U1F39A = "🎚" # :level_slider: - U1FA75 = "🩵" # :light_blue_heart: - U1F4A1 = "💡" # :light_bulb: - U1F688 = "🚈" # :light_rail: - U1F3FB = "🏻" # :light_skin_tone: - U1F34B200D1F7E9 = "🍋‍🟩" # :lime: - U1F517 = "🔗" # :link: - U1F587FE0F = "🖇️" # :linked_paperclips: - U1F587 = "🖇" # :linked_paperclips: - U1F981 = "🦁" # :lion: - U1F484 = "💄" # :lipstick: - U1F6AE = "🚮" # :litter_in_bin_sign: - U1F98E = "🦎" # :lizard: - U1F999 = "🦙" # :llama: - U1F99E = "🦞" # :lobster: - U1F512 = "🔒" # :locked: - U1F510 = "🔐" # :locked_with_key: - U1F50F = "🔏" # :locked_with_pen: - U1F682 = "🚂" # :locomotive: - U1F36D = "🍭" # :lollipop: - U1FA98 = "🪘" # :long_drum: - U1F9F4 = "🧴" # :lotion_bottle: - U1FAB7 = "🪷" # :lotus: - U1F62D = "😭" # :loudly_crying_face: - U1F4E2 = "📢" # :loudspeaker: - U1F91F = "🤟" # :love-you_gesture: - U1F91F1F3FF = "🤟🏿" # :love-you_gesture_dark_skin_tone: - U1F91F1F3FB = "🤟🏻" # :love-you_gesture_light_skin_tone: - U1F91F1F3FE = "🤟🏾" # :love-you_gesture_medium-dark_skin_tone: - U1F91F1F3FC = "🤟🏼" # :love-you_gesture_medium-light_skin_tone: - U1F91F1F3FD = "🤟🏽" # :love-you_gesture_medium_skin_tone: - U1F3E9 = "🏩" # :love_hotel: - U1F48C = "💌" # :love_letter: - U1FAAB = "🪫" # :low_battery: - U1F9F3 = "🧳" # :luggage: - U1FAC1 = "🫁" # :lungs: - U1F925 = "🤥" # :lying_face: - U1F9D9 = "🧙" # :mage: - U1F9D91F3FF = "🧙🏿" # :mage_dark_skin_tone: - U1F9D91F3FB = "🧙🏻" # :mage_light_skin_tone: - U1F9D91F3FE = "🧙🏾" # :mage_medium-dark_skin_tone: - U1F9D91F3FC = "🧙🏼" # :mage_medium-light_skin_tone: - U1F9D91F3FD = "🧙🏽" # :mage_medium_skin_tone: - U1FA84 = "🪄" # :magic_wand: - U1F9F2 = "🧲" # :magnet: - U1F50D = "🔍" # :magnifying_glass_tilted_left: - U1F50E = "🔎" # :magnifying_glass_tilted_right: - U1F004 = "🀄" # :mahjong_red_dragon: - U2642FE0F = "♂️" # :male_sign: - U2642 = "♂" # :male_sign: - U1F9A3 = "🦣" # :mammoth: - U1F468 = "👨" # :man: - U1F468200D1F3A8 = "👨‍🎨" # :man_artist: - U1F4681F3FF200D1F3A8 = "👨🏿‍🎨" # :man_artist_dark_skin_tone: - U1F4681F3FB200D1F3A8 = "👨🏻‍🎨" # :man_artist_light_skin_tone: - U1F4681F3FE200D1F3A8 = "👨🏾‍🎨" # :man_artist_medium-dark_skin_tone: - U1F4681F3FC200D1F3A8 = "👨🏼‍🎨" # :man_artist_medium-light_skin_tone: - U1F4681F3FD200D1F3A8 = "👨🏽‍🎨" # :man_artist_medium_skin_tone: - U1F468200D1F680 = "👨‍🚀" # :man_astronaut: - U1F4681F3FF200D1F680 = "👨🏿‍🚀" # :man_astronaut_dark_skin_tone: - U1F4681F3FB200D1F680 = "👨🏻‍🚀" # :man_astronaut_light_skin_tone: - U1F4681F3FE200D1F680 = "👨🏾‍🚀" # :man_astronaut_medium-dark_skin_tone: - U1F4681F3FC200D1F680 = "👨🏼‍🚀" # :man_astronaut_medium-light_skin_tone: - U1F4681F3FD200D1F680 = "👨🏽‍🚀" # :man_astronaut_medium_skin_tone: - U1F468200D1F9B2 = "👨‍🦲" # :man_bald: - U1F9D4200D2642FE0F = "🧔‍♂️" # :man_beard: - U1F9D4200D2642 = "🧔‍♂" # :man_beard: - U1F6B4200D2642FE0F = "🚴‍♂️" # :man_biking: - U1F6B4200D2642 = "🚴‍♂" # :man_biking: - U1F6B41F3FF200D2642FE0F = "🚴🏿‍♂️" # :man_biking_dark_skin_tone: - U1F6B41F3FF200D2642 = "🚴🏿‍♂" # :man_biking_dark_skin_tone: - U1F6B41F3FB200D2642FE0F = "🚴🏻‍♂️" # :man_biking_light_skin_tone: - U1F6B41F3FB200D2642 = "🚴🏻‍♂" # :man_biking_light_skin_tone: - U1F6B41F3FE200D2642FE0F = "🚴🏾‍♂️" # :man_biking_medium-dark_skin_tone: - U1F6B41F3FE200D2642 = "🚴🏾‍♂" # :man_biking_medium-dark_skin_tone: - U1F6B41F3FC200D2642FE0F = "🚴🏼‍♂️" # :man_biking_medium-light_skin_tone: - U1F6B41F3FC200D2642 = "🚴🏼‍♂" # :man_biking_medium-light_skin_tone: - U1F6B41F3FD200D2642FE0F = "🚴🏽‍♂️" # :man_biking_medium_skin_tone: - U1F6B41F3FD200D2642 = "🚴🏽‍♂" # :man_biking_medium_skin_tone: - U1F471200D2642FE0F = "👱‍♂️" # :man_blond_hair: - U1F471200D2642 = "👱‍♂" # :man_blond_hair: - U26F9FE0F200D2642FE0F = "⛹️‍♂️" # :man_bouncing_ball: - U26F9200D2642FE0F = "⛹‍♂️" # :man_bouncing_ball: - U26F9FE0F200D2642 = "⛹️‍♂" # :man_bouncing_ball: - U26F9200D2642 = "⛹‍♂" # :man_bouncing_ball: - U26F91F3FF200D2642FE0F = "⛹🏿‍♂️" # :man_bouncing_ball_dark_skin_tone: - U26F91F3FF200D2642 = "⛹🏿‍♂" # :man_bouncing_ball_dark_skin_tone: - U26F91F3FB200D2642FE0F = "⛹🏻‍♂️" # :man_bouncing_ball_light_skin_tone: - U26F91F3FB200D2642 = "⛹🏻‍♂" # :man_bouncing_ball_light_skin_tone: - U26F91F3FE200D2642FE0F = "⛹🏾‍♂️" # :man_bouncing_ball_medium-dark_skin_tone: - U26F91F3FE200D2642 = "⛹🏾‍♂" # :man_bouncing_ball_medium-dark_skin_tone: - U26F91F3FC200D2642FE0F = "⛹🏼‍♂️" # :man_bouncing_ball_medium-light_skin_tone: - U26F91F3FC200D2642 = "⛹🏼‍♂" # :man_bouncing_ball_medium-light_skin_tone: - U26F91F3FD200D2642FE0F = "⛹🏽‍♂️" # :man_bouncing_ball_medium_skin_tone: - U26F91F3FD200D2642 = "⛹🏽‍♂" # :man_bouncing_ball_medium_skin_tone: - U1F647200D2642FE0F = "🙇‍♂️" # :man_bowing: - U1F647200D2642 = "🙇‍♂" # :man_bowing: - U1F6471F3FF200D2642FE0F = "🙇🏿‍♂️" # :man_bowing_dark_skin_tone: - U1F6471F3FF200D2642 = "🙇🏿‍♂" # :man_bowing_dark_skin_tone: - U1F6471F3FB200D2642FE0F = "🙇🏻‍♂️" # :man_bowing_light_skin_tone: - U1F6471F3FB200D2642 = "🙇🏻‍♂" # :man_bowing_light_skin_tone: - U1F6471F3FE200D2642FE0F = "🙇🏾‍♂️" # :man_bowing_medium-dark_skin_tone: - U1F6471F3FE200D2642 = "🙇🏾‍♂" # :man_bowing_medium-dark_skin_tone: - U1F6471F3FC200D2642FE0F = "🙇🏼‍♂️" # :man_bowing_medium-light_skin_tone: - U1F6471F3FC200D2642 = "🙇🏼‍♂" # :man_bowing_medium-light_skin_tone: - U1F6471F3FD200D2642FE0F = "🙇🏽‍♂️" # :man_bowing_medium_skin_tone: - U1F6471F3FD200D2642 = "🙇🏽‍♂" # :man_bowing_medium_skin_tone: - U1F938200D2642FE0F = "🤸‍♂️" # :man_cartwheeling: - U1F938200D2642 = "🤸‍♂" # :man_cartwheeling: - U1F9381F3FF200D2642FE0F = "🤸🏿‍♂️" # :man_cartwheeling_dark_skin_tone: - U1F9381F3FF200D2642 = "🤸🏿‍♂" # :man_cartwheeling_dark_skin_tone: - U1F9381F3FB200D2642FE0F = "🤸🏻‍♂️" # :man_cartwheeling_light_skin_tone: - U1F9381F3FB200D2642 = "🤸🏻‍♂" # :man_cartwheeling_light_skin_tone: - U1F9381F3FE200D2642FE0F = "🤸🏾‍♂️" # :man_cartwheeling_medium-dark_skin_tone: - U1F9381F3FE200D2642 = "🤸🏾‍♂" # :man_cartwheeling_medium-dark_skin_tone: - U1F9381F3FC200D2642FE0F = "🤸🏼‍♂️" # :man_cartwheeling_medium-light_skin_tone: - U1F9381F3FC200D2642 = "🤸🏼‍♂" # :man_cartwheeling_medium-light_skin_tone: - U1F9381F3FD200D2642FE0F = "🤸🏽‍♂️" # :man_cartwheeling_medium_skin_tone: - U1F9381F3FD200D2642 = "🤸🏽‍♂" # :man_cartwheeling_medium_skin_tone: - U1F9D7200D2642FE0F = "🧗‍♂️" # :man_climbing: - U1F9D7200D2642 = "🧗‍♂" # :man_climbing: - U1F9D71F3FF200D2642FE0F = "🧗🏿‍♂️" # :man_climbing_dark_skin_tone: - U1F9D71F3FF200D2642 = "🧗🏿‍♂" # :man_climbing_dark_skin_tone: - U1F9D71F3FB200D2642FE0F = "🧗🏻‍♂️" # :man_climbing_light_skin_tone: - U1F9D71F3FB200D2642 = "🧗🏻‍♂" # :man_climbing_light_skin_tone: - U1F9D71F3FE200D2642FE0F = "🧗🏾‍♂️" # :man_climbing_medium-dark_skin_tone: - U1F9D71F3FE200D2642 = "🧗🏾‍♂" # :man_climbing_medium-dark_skin_tone: - U1F9D71F3FC200D2642FE0F = "🧗🏼‍♂️" # :man_climbing_medium-light_skin_tone: - U1F9D71F3FC200D2642 = "🧗🏼‍♂" # :man_climbing_medium-light_skin_tone: - U1F9D71F3FD200D2642FE0F = "🧗🏽‍♂️" # :man_climbing_medium_skin_tone: - U1F9D71F3FD200D2642 = "🧗🏽‍♂" # :man_climbing_medium_skin_tone: - U1F477200D2642FE0F = "👷‍♂️" # :man_construction_worker: - U1F477200D2642 = "👷‍♂" # :man_construction_worker: - U1F4771F3FF200D2642FE0F = "👷🏿‍♂️" # :man_construction_worker_dark_skin_tone: - U1F4771F3FF200D2642 = "👷🏿‍♂" # :man_construction_worker_dark_skin_tone: - U1F4771F3FB200D2642FE0F = "👷🏻‍♂️" # :man_construction_worker_light_skin_tone: - U1F4771F3FB200D2642 = "👷🏻‍♂" # :man_construction_worker_light_skin_tone: - U1F4771F3FE200D2642FE0F = "👷🏾‍♂️" # :man_construction_worker_medium-dark_skin_tone: - U1F4771F3FE200D2642 = "👷🏾‍♂" # :man_construction_worker_medium-dark_skin_tone: - U1F4771F3FC200D2642FE0F = "👷🏼‍♂️" # :man_construction_worker_medium-light_skin_tone: - U1F4771F3FC200D2642 = "👷🏼‍♂" # :man_construction_worker_medium-light_skin_tone: - U1F4771F3FD200D2642FE0F = "👷🏽‍♂️" # :man_construction_worker_medium_skin_tone: - U1F4771F3FD200D2642 = "👷🏽‍♂" # :man_construction_worker_medium_skin_tone: - U1F468200D1F373 = "👨‍🍳" # :man_cook: - U1F4681F3FF200D1F373 = "👨🏿‍🍳" # :man_cook_dark_skin_tone: - U1F4681F3FB200D1F373 = "👨🏻‍🍳" # :man_cook_light_skin_tone: - U1F4681F3FE200D1F373 = "👨🏾‍🍳" # :man_cook_medium-dark_skin_tone: - U1F4681F3FC200D1F373 = "👨🏼‍🍳" # :man_cook_medium-light_skin_tone: - U1F4681F3FD200D1F373 = "👨🏽‍🍳" # :man_cook_medium_skin_tone: - U1F468200D1F9B1 = "👨‍🦱" # :man_curly_hair: - U1F57A = "🕺" # :man_dancing: - U1F57A1F3FF = "🕺🏿" # :man_dancing_dark_skin_tone: - U1F57A1F3FB = "🕺🏻" # :man_dancing_light_skin_tone: - U1F57A1F3FE = "🕺🏾" # :man_dancing_medium-dark_skin_tone: - U1F57A1F3FC = "🕺🏼" # :man_dancing_medium-light_skin_tone: - U1F57A1F3FD = "🕺🏽" # :man_dancing_medium_skin_tone: - U1F4681F3FF = "👨🏿" # :man_dark_skin_tone: - U1F4681F3FF200D1F9B2 = "👨🏿‍🦲" # :man_dark_skin_tone_bald: - U1F9D41F3FF200D2642FE0F = "🧔🏿‍♂️" # :man_dark_skin_tone_beard: - U1F9D41F3FF200D2642 = "🧔🏿‍♂" # :man_dark_skin_tone_beard: - U1F4711F3FF200D2642FE0F = "👱🏿‍♂️" # :man_dark_skin_tone_blond_hair: - U1F4711F3FF200D2642 = "👱🏿‍♂" # :man_dark_skin_tone_blond_hair: - U1F4681F3FF200D1F9B1 = "👨🏿‍🦱" # :man_dark_skin_tone_curly_hair: - U1F4681F3FF200D1F9B0 = "👨🏿‍🦰" # :man_dark_skin_tone_red_hair: - U1F4681F3FF200D1F9B3 = "👨🏿‍🦳" # :man_dark_skin_tone_white_hair: - U1F575FE0F200D2642FE0F = "🕵️‍♂️" # :man_detective: - U1F575200D2642FE0F = "🕵‍♂️" # :man_detective: - U1F575FE0F200D2642 = "🕵️‍♂" # :man_detective: - U1F575200D2642 = "🕵‍♂" # :man_detective: - U1F5751F3FF200D2642FE0F = "🕵🏿‍♂️" # :man_detective_dark_skin_tone: - U1F5751F3FF200D2642 = "🕵🏿‍♂" # :man_detective_dark_skin_tone: - U1F5751F3FB200D2642FE0F = "🕵🏻‍♂️" # :man_detective_light_skin_tone: - U1F5751F3FB200D2642 = "🕵🏻‍♂" # :man_detective_light_skin_tone: - U1F5751F3FE200D2642FE0F = "🕵🏾‍♂️" # :man_detective_medium-dark_skin_tone: - U1F5751F3FE200D2642 = "🕵🏾‍♂" # :man_detective_medium-dark_skin_tone: - U1F5751F3FC200D2642FE0F = "🕵🏼‍♂️" # :man_detective_medium-light_skin_tone: - U1F5751F3FC200D2642 = "🕵🏼‍♂" # :man_detective_medium-light_skin_tone: - U1F5751F3FD200D2642FE0F = "🕵🏽‍♂️" # :man_detective_medium_skin_tone: - U1F5751F3FD200D2642 = "🕵🏽‍♂" # :man_detective_medium_skin_tone: - U1F9DD200D2642FE0F = "🧝‍♂️" # :man_elf: - U1F9DD200D2642 = "🧝‍♂" # :man_elf: - U1F9DD1F3FF200D2642FE0F = "🧝🏿‍♂️" # :man_elf_dark_skin_tone: - U1F9DD1F3FF200D2642 = "🧝🏿‍♂" # :man_elf_dark_skin_tone: - U1F9DD1F3FB200D2642FE0F = "🧝🏻‍♂️" # :man_elf_light_skin_tone: - U1F9DD1F3FB200D2642 = "🧝🏻‍♂" # :man_elf_light_skin_tone: - U1F9DD1F3FE200D2642FE0F = "🧝🏾‍♂️" # :man_elf_medium-dark_skin_tone: - U1F9DD1F3FE200D2642 = "🧝🏾‍♂" # :man_elf_medium-dark_skin_tone: - U1F9DD1F3FC200D2642FE0F = "🧝🏼‍♂️" # :man_elf_medium-light_skin_tone: - U1F9DD1F3FC200D2642 = "🧝🏼‍♂" # :man_elf_medium-light_skin_tone: - U1F9DD1F3FD200D2642FE0F = "🧝🏽‍♂️" # :man_elf_medium_skin_tone: - U1F9DD1F3FD200D2642 = "🧝🏽‍♂" # :man_elf_medium_skin_tone: - U1F926200D2642FE0F = "🤦‍♂️" # :man_facepalming: - U1F926200D2642 = "🤦‍♂" # :man_facepalming: - U1F9261F3FF200D2642FE0F = "🤦🏿‍♂️" # :man_facepalming_dark_skin_tone: - U1F9261F3FF200D2642 = "🤦🏿‍♂" # :man_facepalming_dark_skin_tone: - U1F9261F3FB200D2642FE0F = "🤦🏻‍♂️" # :man_facepalming_light_skin_tone: - U1F9261F3FB200D2642 = "🤦🏻‍♂" # :man_facepalming_light_skin_tone: - U1F9261F3FE200D2642FE0F = "🤦🏾‍♂️" # :man_facepalming_medium-dark_skin_tone: - U1F9261F3FE200D2642 = "🤦🏾‍♂" # :man_facepalming_medium-dark_skin_tone: - U1F9261F3FC200D2642FE0F = "🤦🏼‍♂️" # :man_facepalming_medium-light_skin_tone: - U1F9261F3FC200D2642 = "🤦🏼‍♂" # :man_facepalming_medium-light_skin_tone: - U1F9261F3FD200D2642FE0F = "🤦🏽‍♂️" # :man_facepalming_medium_skin_tone: - U1F9261F3FD200D2642 = "🤦🏽‍♂" # :man_facepalming_medium_skin_tone: - U1F468200D1F3ED = "👨‍🏭" # :man_factory_worker: - U1F4681F3FF200D1F3ED = "👨🏿‍🏭" # :man_factory_worker_dark_skin_tone: - U1F4681F3FB200D1F3ED = "👨🏻‍🏭" # :man_factory_worker_light_skin_tone: - U1F4681F3FE200D1F3ED = "👨🏾‍🏭" # :man_factory_worker_medium-dark_skin_tone: - U1F4681F3FC200D1F3ED = "👨🏼‍🏭" # :man_factory_worker_medium-light_skin_tone: - U1F4681F3FD200D1F3ED = "👨🏽‍🏭" # :man_factory_worker_medium_skin_tone: - U1F9DA200D2642FE0F = "🧚‍♂️" # :man_fairy: - U1F9DA200D2642 = "🧚‍♂" # :man_fairy: - U1F9DA1F3FF200D2642FE0F = "🧚🏿‍♂️" # :man_fairy_dark_skin_tone: - U1F9DA1F3FF200D2642 = "🧚🏿‍♂" # :man_fairy_dark_skin_tone: - U1F9DA1F3FB200D2642FE0F = "🧚🏻‍♂️" # :man_fairy_light_skin_tone: - U1F9DA1F3FB200D2642 = "🧚🏻‍♂" # :man_fairy_light_skin_tone: - U1F9DA1F3FE200D2642FE0F = "🧚🏾‍♂️" # :man_fairy_medium-dark_skin_tone: - U1F9DA1F3FE200D2642 = "🧚🏾‍♂" # :man_fairy_medium-dark_skin_tone: - U1F9DA1F3FC200D2642FE0F = "🧚🏼‍♂️" # :man_fairy_medium-light_skin_tone: - U1F9DA1F3FC200D2642 = "🧚🏼‍♂" # :man_fairy_medium-light_skin_tone: - U1F9DA1F3FD200D2642FE0F = "🧚🏽‍♂️" # :man_fairy_medium_skin_tone: - U1F9DA1F3FD200D2642 = "🧚🏽‍♂" # :man_fairy_medium_skin_tone: - U1F468200D1F33E = "👨‍🌾" # :man_farmer: - U1F4681F3FF200D1F33E = "👨🏿‍🌾" # :man_farmer_dark_skin_tone: - U1F4681F3FB200D1F33E = "👨🏻‍🌾" # :man_farmer_light_skin_tone: - U1F4681F3FE200D1F33E = "👨🏾‍🌾" # :man_farmer_medium-dark_skin_tone: - U1F4681F3FC200D1F33E = "👨🏼‍🌾" # :man_farmer_medium-light_skin_tone: - U1F4681F3FD200D1F33E = "👨🏽‍🌾" # :man_farmer_medium_skin_tone: - U1F468200D1F37C = "👨‍🍼" # :man_feeding_baby: - U1F4681F3FF200D1F37C = "👨🏿‍🍼" # :man_feeding_baby_dark_skin_tone: - U1F4681F3FB200D1F37C = "👨🏻‍🍼" # :man_feeding_baby_light_skin_tone: - U1F4681F3FE200D1F37C = "👨🏾‍🍼" # :man_feeding_baby_medium-dark_skin_tone: - U1F4681F3FC200D1F37C = "👨🏼‍🍼" # :man_feeding_baby_medium-light_skin_tone: - U1F4681F3FD200D1F37C = "👨🏽‍🍼" # :man_feeding_baby_medium_skin_tone: - U1F468200D1F692 = "👨‍🚒" # :man_firefighter: - U1F4681F3FF200D1F692 = "👨🏿‍🚒" # :man_firefighter_dark_skin_tone: - U1F4681F3FB200D1F692 = "👨🏻‍🚒" # :man_firefighter_light_skin_tone: - U1F4681F3FE200D1F692 = "👨🏾‍🚒" # :man_firefighter_medium-dark_skin_tone: - U1F4681F3FC200D1F692 = "👨🏼‍🚒" # :man_firefighter_medium-light_skin_tone: - U1F4681F3FD200D1F692 = "👨🏽‍🚒" # :man_firefighter_medium_skin_tone: - U1F64D200D2642FE0F = "🙍‍♂️" # :man_frowning: - U1F64D200D2642 = "🙍‍♂" # :man_frowning: - U1F64D1F3FF200D2642FE0F = "🙍🏿‍♂️" # :man_frowning_dark_skin_tone: - U1F64D1F3FF200D2642 = "🙍🏿‍♂" # :man_frowning_dark_skin_tone: - U1F64D1F3FB200D2642FE0F = "🙍🏻‍♂️" # :man_frowning_light_skin_tone: - U1F64D1F3FB200D2642 = "🙍🏻‍♂" # :man_frowning_light_skin_tone: - U1F64D1F3FE200D2642FE0F = "🙍🏾‍♂️" # :man_frowning_medium-dark_skin_tone: - U1F64D1F3FE200D2642 = "🙍🏾‍♂" # :man_frowning_medium-dark_skin_tone: - U1F64D1F3FC200D2642FE0F = "🙍🏼‍♂️" # :man_frowning_medium-light_skin_tone: - U1F64D1F3FC200D2642 = "🙍🏼‍♂" # :man_frowning_medium-light_skin_tone: - U1F64D1F3FD200D2642FE0F = "🙍🏽‍♂️" # :man_frowning_medium_skin_tone: - U1F64D1F3FD200D2642 = "🙍🏽‍♂" # :man_frowning_medium_skin_tone: - U1F9DE200D2642FE0F = "🧞‍♂️" # :man_genie: - U1F9DE200D2642 = "🧞‍♂" # :man_genie: - U1F645200D2642FE0F = "🙅‍♂️" # :man_gesturing_NO: - U1F645200D2642 = "🙅‍♂" # :man_gesturing_NO: - U1F6451F3FF200D2642FE0F = "🙅🏿‍♂️" # :man_gesturing_NO_dark_skin_tone: - U1F6451F3FF200D2642 = "🙅🏿‍♂" # :man_gesturing_NO_dark_skin_tone: - U1F6451F3FB200D2642FE0F = "🙅🏻‍♂️" # :man_gesturing_NO_light_skin_tone: - U1F6451F3FB200D2642 = "🙅🏻‍♂" # :man_gesturing_NO_light_skin_tone: - U1F6451F3FE200D2642FE0F = "🙅🏾‍♂️" # :man_gesturing_NO_medium-dark_skin_tone: - U1F6451F3FE200D2642 = "🙅🏾‍♂" # :man_gesturing_NO_medium-dark_skin_tone: - U1F6451F3FC200D2642FE0F = "🙅🏼‍♂️" # :man_gesturing_NO_medium-light_skin_tone: - U1F6451F3FC200D2642 = "🙅🏼‍♂" # :man_gesturing_NO_medium-light_skin_tone: - U1F6451F3FD200D2642FE0F = "🙅🏽‍♂️" # :man_gesturing_NO_medium_skin_tone: - U1F6451F3FD200D2642 = "🙅🏽‍♂" # :man_gesturing_NO_medium_skin_tone: - U1F646200D2642FE0F = "🙆‍♂️" # :man_gesturing_OK: - U1F646200D2642 = "🙆‍♂" # :man_gesturing_OK: - U1F6461F3FF200D2642FE0F = "🙆🏿‍♂️" # :man_gesturing_OK_dark_skin_tone: - U1F6461F3FF200D2642 = "🙆🏿‍♂" # :man_gesturing_OK_dark_skin_tone: - U1F6461F3FB200D2642FE0F = "🙆🏻‍♂️" # :man_gesturing_OK_light_skin_tone: - U1F6461F3FB200D2642 = "🙆🏻‍♂" # :man_gesturing_OK_light_skin_tone: - U1F6461F3FE200D2642FE0F = "🙆🏾‍♂️" # :man_gesturing_OK_medium-dark_skin_tone: - U1F6461F3FE200D2642 = "🙆🏾‍♂" # :man_gesturing_OK_medium-dark_skin_tone: - U1F6461F3FC200D2642FE0F = "🙆🏼‍♂️" # :man_gesturing_OK_medium-light_skin_tone: - U1F6461F3FC200D2642 = "🙆🏼‍♂" # :man_gesturing_OK_medium-light_skin_tone: - U1F6461F3FD200D2642FE0F = "🙆🏽‍♂️" # :man_gesturing_OK_medium_skin_tone: - U1F6461F3FD200D2642 = "🙆🏽‍♂" # :man_gesturing_OK_medium_skin_tone: - U1F487200D2642FE0F = "💇‍♂️" # :man_getting_haircut: - U1F487200D2642 = "💇‍♂" # :man_getting_haircut: - U1F4871F3FF200D2642FE0F = "💇🏿‍♂️" # :man_getting_haircut_dark_skin_tone: - U1F4871F3FF200D2642 = "💇🏿‍♂" # :man_getting_haircut_dark_skin_tone: - U1F4871F3FB200D2642FE0F = "💇🏻‍♂️" # :man_getting_haircut_light_skin_tone: - U1F4871F3FB200D2642 = "💇🏻‍♂" # :man_getting_haircut_light_skin_tone: - U1F4871F3FE200D2642FE0F = "💇🏾‍♂️" # :man_getting_haircut_medium-dark_skin_tone: - U1F4871F3FE200D2642 = "💇🏾‍♂" # :man_getting_haircut_medium-dark_skin_tone: - U1F4871F3FC200D2642FE0F = "💇🏼‍♂️" # :man_getting_haircut_medium-light_skin_tone: - U1F4871F3FC200D2642 = "💇🏼‍♂" # :man_getting_haircut_medium-light_skin_tone: - U1F4871F3FD200D2642FE0F = "💇🏽‍♂️" # :man_getting_haircut_medium_skin_tone: - U1F4871F3FD200D2642 = "💇🏽‍♂" # :man_getting_haircut_medium_skin_tone: - U1F486200D2642FE0F = "💆‍♂️" # :man_getting_massage: - U1F486200D2642 = "💆‍♂" # :man_getting_massage: - U1F4861F3FF200D2642FE0F = "💆🏿‍♂️" # :man_getting_massage_dark_skin_tone: - U1F4861F3FF200D2642 = "💆🏿‍♂" # :man_getting_massage_dark_skin_tone: - U1F4861F3FB200D2642FE0F = "💆🏻‍♂️" # :man_getting_massage_light_skin_tone: - U1F4861F3FB200D2642 = "💆🏻‍♂" # :man_getting_massage_light_skin_tone: - U1F4861F3FE200D2642FE0F = "💆🏾‍♂️" # :man_getting_massage_medium-dark_skin_tone: - U1F4861F3FE200D2642 = "💆🏾‍♂" # :man_getting_massage_medium-dark_skin_tone: - U1F4861F3FC200D2642FE0F = "💆🏼‍♂️" # :man_getting_massage_medium-light_skin_tone: - U1F4861F3FC200D2642 = "💆🏼‍♂" # :man_getting_massage_medium-light_skin_tone: - U1F4861F3FD200D2642FE0F = "💆🏽‍♂️" # :man_getting_massage_medium_skin_tone: - U1F4861F3FD200D2642 = "💆🏽‍♂" # :man_getting_massage_medium_skin_tone: - U1F3CCFE0F200D2642FE0F = "🏌️‍♂️" # :man_golfing: - U1F3CC200D2642FE0F = "🏌‍♂️" # :man_golfing: - U1F3CCFE0F200D2642 = "🏌️‍♂" # :man_golfing: - U1F3CC200D2642 = "🏌‍♂" # :man_golfing: - U1F3CC1F3FF200D2642FE0F = "🏌🏿‍♂️" # :man_golfing_dark_skin_tone: - U1F3CC1F3FF200D2642 = "🏌🏿‍♂" # :man_golfing_dark_skin_tone: - U1F3CC1F3FB200D2642FE0F = "🏌🏻‍♂️" # :man_golfing_light_skin_tone: - U1F3CC1F3FB200D2642 = "🏌🏻‍♂" # :man_golfing_light_skin_tone: - U1F3CC1F3FE200D2642FE0F = "🏌🏾‍♂️" # :man_golfing_medium-dark_skin_tone: - U1F3CC1F3FE200D2642 = "🏌🏾‍♂" # :man_golfing_medium-dark_skin_tone: - U1F3CC1F3FC200D2642FE0F = "🏌🏼‍♂️" # :man_golfing_medium-light_skin_tone: - U1F3CC1F3FC200D2642 = "🏌🏼‍♂" # :man_golfing_medium-light_skin_tone: - U1F3CC1F3FD200D2642FE0F = "🏌🏽‍♂️" # :man_golfing_medium_skin_tone: - U1F3CC1F3FD200D2642 = "🏌🏽‍♂" # :man_golfing_medium_skin_tone: - U1F482200D2642FE0F = "💂‍♂️" # :man_guard: - U1F482200D2642 = "💂‍♂" # :man_guard: - U1F4821F3FF200D2642FE0F = "💂🏿‍♂️" # :man_guard_dark_skin_tone: - U1F4821F3FF200D2642 = "💂🏿‍♂" # :man_guard_dark_skin_tone: - U1F4821F3FB200D2642FE0F = "💂🏻‍♂️" # :man_guard_light_skin_tone: - U1F4821F3FB200D2642 = "💂🏻‍♂" # :man_guard_light_skin_tone: - U1F4821F3FE200D2642FE0F = "💂🏾‍♂️" # :man_guard_medium-dark_skin_tone: - U1F4821F3FE200D2642 = "💂🏾‍♂" # :man_guard_medium-dark_skin_tone: - U1F4821F3FC200D2642FE0F = "💂🏼‍♂️" # :man_guard_medium-light_skin_tone: - U1F4821F3FC200D2642 = "💂🏼‍♂" # :man_guard_medium-light_skin_tone: - U1F4821F3FD200D2642FE0F = "💂🏽‍♂️" # :man_guard_medium_skin_tone: - U1F4821F3FD200D2642 = "💂🏽‍♂" # :man_guard_medium_skin_tone: - U1F468200D2695FE0F = "👨‍⚕️" # :man_health_worker: - U1F468200D2695 = "👨‍⚕" # :man_health_worker: - U1F4681F3FF200D2695FE0F = "👨🏿‍⚕️" # :man_health_worker_dark_skin_tone: - U1F4681F3FF200D2695 = "👨🏿‍⚕" # :man_health_worker_dark_skin_tone: - U1F4681F3FB200D2695FE0F = "👨🏻‍⚕️" # :man_health_worker_light_skin_tone: - U1F4681F3FB200D2695 = "👨🏻‍⚕" # :man_health_worker_light_skin_tone: - U1F4681F3FE200D2695FE0F = "👨🏾‍⚕️" # :man_health_worker_medium-dark_skin_tone: - U1F4681F3FE200D2695 = "👨🏾‍⚕" # :man_health_worker_medium-dark_skin_tone: - U1F4681F3FC200D2695FE0F = "👨🏼‍⚕️" # :man_health_worker_medium-light_skin_tone: - U1F4681F3FC200D2695 = "👨🏼‍⚕" # :man_health_worker_medium-light_skin_tone: - U1F4681F3FD200D2695FE0F = "👨🏽‍⚕️" # :man_health_worker_medium_skin_tone: - U1F4681F3FD200D2695 = "👨🏽‍⚕" # :man_health_worker_medium_skin_tone: - U1F9D8200D2642FE0F = "🧘‍♂️" # :man_in_lotus_position: - U1F9D8200D2642 = "🧘‍♂" # :man_in_lotus_position: - U1F9D81F3FF200D2642FE0F = "🧘🏿‍♂️" # :man_in_lotus_position_dark_skin_tone: - U1F9D81F3FF200D2642 = "🧘🏿‍♂" # :man_in_lotus_position_dark_skin_tone: - U1F9D81F3FB200D2642FE0F = "🧘🏻‍♂️" # :man_in_lotus_position_light_skin_tone: - U1F9D81F3FB200D2642 = "🧘🏻‍♂" # :man_in_lotus_position_light_skin_tone: - U1F9D81F3FE200D2642FE0F = "🧘🏾‍♂️" # :man_in_lotus_position_medium-dark_skin_tone: - U1F9D81F3FE200D2642 = "🧘🏾‍♂" # :man_in_lotus_position_medium-dark_skin_tone: - U1F9D81F3FC200D2642FE0F = "🧘🏼‍♂️" # :man_in_lotus_position_medium-light_skin_tone: - U1F9D81F3FC200D2642 = "🧘🏼‍♂" # :man_in_lotus_position_medium-light_skin_tone: - U1F9D81F3FD200D2642FE0F = "🧘🏽‍♂️" # :man_in_lotus_position_medium_skin_tone: - U1F9D81F3FD200D2642 = "🧘🏽‍♂" # :man_in_lotus_position_medium_skin_tone: - U1F468200D1F9BD = "👨‍🦽" # :man_in_manual_wheelchair: - U1F4681F3FF200D1F9BD = "👨🏿‍🦽" # :man_in_manual_wheelchair_dark_skin_tone: - U1F468200D1F9BD200D27A1FE0F = "👨‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right: - U1F468200D1F9BD200D27A1 = "👨‍🦽‍➡" # :man_in_manual_wheelchair_facing_right: - U1F4681F3FF200D1F9BD200D27A1FE0F = "👨🏿‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F4681F3FF200D1F9BD200D27A1 = "👨🏿‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F4681F3FB200D1F9BD200D27A1FE0F = "👨🏻‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_light_skin_tone: - U1F4681F3FB200D1F9BD200D27A1 = "👨🏻‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_light_skin_tone: - U1F4681F3FE200D1F9BD200D27A1FE0F = "👨🏾‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F4681F3FE200D1F9BD200D27A1 = "👨🏾‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F4681F3FC200D1F9BD200D27A1FE0F = "👨🏼‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F4681F3FC200D1F9BD200D27A1 = "👨🏼‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F4681F3FD200D1F9BD200D27A1FE0F = "👨🏽‍🦽‍➡️" # :man_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F4681F3FD200D1F9BD200D27A1 = "👨🏽‍🦽‍➡" # :man_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F4681F3FB200D1F9BD = "👨🏻‍🦽" # :man_in_manual_wheelchair_light_skin_tone: - U1F4681F3FE200D1F9BD = "👨🏾‍🦽" # :man_in_manual_wheelchair_medium-dark_skin_tone: - U1F4681F3FC200D1F9BD = "👨🏼‍🦽" # :man_in_manual_wheelchair_medium-light_skin_tone: - U1F4681F3FD200D1F9BD = "👨🏽‍🦽" # :man_in_manual_wheelchair_medium_skin_tone: - U1F468200D1F9BC = "👨‍🦼" # :man_in_motorized_wheelchair: - U1F4681F3FF200D1F9BC = "👨🏿‍🦼" # :man_in_motorized_wheelchair_dark_skin_tone: - U1F468200D1F9BC200D27A1FE0F = "👨‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right: - U1F468200D1F9BC200D27A1 = "👨‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right: - U1F4681F3FF200D1F9BC200D27A1FE0F = "👨🏿‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F4681F3FF200D1F9BC200D27A1 = "👨🏿‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F4681F3FB200D1F9BC200D27A1FE0F = "👨🏻‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F4681F3FB200D1F9BC200D27A1 = "👨🏻‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F4681F3FE200D1F9BC200D27A1FE0F = "👨🏾‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F4681F3FE200D1F9BC200D27A1 = "👨🏾‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F4681F3FC200D1F9BC200D27A1FE0F = "👨🏼‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F4681F3FC200D1F9BC200D27A1 = "👨🏼‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F4681F3FD200D1F9BC200D27A1FE0F = "👨🏽‍🦼‍➡️" # :man_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F4681F3FD200D1F9BC200D27A1 = "👨🏽‍🦼‍➡" # :man_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F4681F3FB200D1F9BC = "👨🏻‍🦼" # :man_in_motorized_wheelchair_light_skin_tone: - U1F4681F3FE200D1F9BC = "👨🏾‍🦼" # :man_in_motorized_wheelchair_medium-dark_skin_tone: - U1F4681F3FC200D1F9BC = "👨🏼‍🦼" # :man_in_motorized_wheelchair_medium-light_skin_tone: - U1F4681F3FD200D1F9BC = "👨🏽‍🦼" # :man_in_motorized_wheelchair_medium_skin_tone: - U1F9D6200D2642FE0F = "🧖‍♂️" # :man_in_steamy_room: - U1F9D6200D2642 = "🧖‍♂" # :man_in_steamy_room: - U1F9D61F3FF200D2642FE0F = "🧖🏿‍♂️" # :man_in_steamy_room_dark_skin_tone: - U1F9D61F3FF200D2642 = "🧖🏿‍♂" # :man_in_steamy_room_dark_skin_tone: - U1F9D61F3FB200D2642FE0F = "🧖🏻‍♂️" # :man_in_steamy_room_light_skin_tone: - U1F9D61F3FB200D2642 = "🧖🏻‍♂" # :man_in_steamy_room_light_skin_tone: - U1F9D61F3FE200D2642FE0F = "🧖🏾‍♂️" # :man_in_steamy_room_medium-dark_skin_tone: - U1F9D61F3FE200D2642 = "🧖🏾‍♂" # :man_in_steamy_room_medium-dark_skin_tone: - U1F9D61F3FC200D2642FE0F = "🧖🏼‍♂️" # :man_in_steamy_room_medium-light_skin_tone: - U1F9D61F3FC200D2642 = "🧖🏼‍♂" # :man_in_steamy_room_medium-light_skin_tone: - U1F9D61F3FD200D2642FE0F = "🧖🏽‍♂️" # :man_in_steamy_room_medium_skin_tone: - U1F9D61F3FD200D2642 = "🧖🏽‍♂" # :man_in_steamy_room_medium_skin_tone: - U1F935200D2642FE0F = "🤵‍♂️" # :man_in_tuxedo: - U1F935200D2642 = "🤵‍♂" # :man_in_tuxedo: - U1F9351F3FF200D2642FE0F = "🤵🏿‍♂️" # :man_in_tuxedo_dark_skin_tone: - U1F9351F3FF200D2642 = "🤵🏿‍♂" # :man_in_tuxedo_dark_skin_tone: - U1F9351F3FB200D2642FE0F = "🤵🏻‍♂️" # :man_in_tuxedo_light_skin_tone: - U1F9351F3FB200D2642 = "🤵🏻‍♂" # :man_in_tuxedo_light_skin_tone: - U1F9351F3FE200D2642FE0F = "🤵🏾‍♂️" # :man_in_tuxedo_medium-dark_skin_tone: - U1F9351F3FE200D2642 = "🤵🏾‍♂" # :man_in_tuxedo_medium-dark_skin_tone: - U1F9351F3FC200D2642FE0F = "🤵🏼‍♂️" # :man_in_tuxedo_medium-light_skin_tone: - U1F9351F3FC200D2642 = "🤵🏼‍♂" # :man_in_tuxedo_medium-light_skin_tone: - U1F9351F3FD200D2642FE0F = "🤵🏽‍♂️" # :man_in_tuxedo_medium_skin_tone: - U1F9351F3FD200D2642 = "🤵🏽‍♂" # :man_in_tuxedo_medium_skin_tone: - U1F468200D2696FE0F = "👨‍⚖️" # :man_judge: - U1F468200D2696 = "👨‍⚖" # :man_judge: - U1F4681F3FF200D2696FE0F = "👨🏿‍⚖️" # :man_judge_dark_skin_tone: - U1F4681F3FF200D2696 = "👨🏿‍⚖" # :man_judge_dark_skin_tone: - U1F4681F3FB200D2696FE0F = "👨🏻‍⚖️" # :man_judge_light_skin_tone: - U1F4681F3FB200D2696 = "👨🏻‍⚖" # :man_judge_light_skin_tone: - U1F4681F3FE200D2696FE0F = "👨🏾‍⚖️" # :man_judge_medium-dark_skin_tone: - U1F4681F3FE200D2696 = "👨🏾‍⚖" # :man_judge_medium-dark_skin_tone: - U1F4681F3FC200D2696FE0F = "👨🏼‍⚖️" # :man_judge_medium-light_skin_tone: - U1F4681F3FC200D2696 = "👨🏼‍⚖" # :man_judge_medium-light_skin_tone: - U1F4681F3FD200D2696FE0F = "👨🏽‍⚖️" # :man_judge_medium_skin_tone: - U1F4681F3FD200D2696 = "👨🏽‍⚖" # :man_judge_medium_skin_tone: - U1F939200D2642FE0F = "🤹‍♂️" # :man_juggling: - U1F939200D2642 = "🤹‍♂" # :man_juggling: - U1F9391F3FF200D2642FE0F = "🤹🏿‍♂️" # :man_juggling_dark_skin_tone: - U1F9391F3FF200D2642 = "🤹🏿‍♂" # :man_juggling_dark_skin_tone: - U1F9391F3FB200D2642FE0F = "🤹🏻‍♂️" # :man_juggling_light_skin_tone: - U1F9391F3FB200D2642 = "🤹🏻‍♂" # :man_juggling_light_skin_tone: - U1F9391F3FE200D2642FE0F = "🤹🏾‍♂️" # :man_juggling_medium-dark_skin_tone: - U1F9391F3FE200D2642 = "🤹🏾‍♂" # :man_juggling_medium-dark_skin_tone: - U1F9391F3FC200D2642FE0F = "🤹🏼‍♂️" # :man_juggling_medium-light_skin_tone: - U1F9391F3FC200D2642 = "🤹🏼‍♂" # :man_juggling_medium-light_skin_tone: - U1F9391F3FD200D2642FE0F = "🤹🏽‍♂️" # :man_juggling_medium_skin_tone: - U1F9391F3FD200D2642 = "🤹🏽‍♂" # :man_juggling_medium_skin_tone: - U1F9CE200D2642FE0F = "🧎‍♂️" # :man_kneeling: - U1F9CE200D2642 = "🧎‍♂" # :man_kneeling: - U1F9CE1F3FF200D2642FE0F = "🧎🏿‍♂️" # :man_kneeling_dark_skin_tone: - U1F9CE1F3FF200D2642 = "🧎🏿‍♂" # :man_kneeling_dark_skin_tone: - U1F9CE200D2642FE0F200D27A1FE0F = "🧎‍♂️‍➡️" # :man_kneeling_facing_right: - U1F9CE200D2642200D27A1FE0F = "🧎‍♂‍➡️" # :man_kneeling_facing_right: - U1F9CE200D2642FE0F200D27A1 = "🧎‍♂️‍➡" # :man_kneeling_facing_right: - U1F9CE200D2642200D27A1 = "🧎‍♂‍➡" # :man_kneeling_facing_right: - U1F9CE1F3FF200D2642FE0F200D27A1FE0F = "🧎🏿‍♂️‍➡️" # :man_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2642200D27A1FE0F = "🧎🏿‍♂‍➡️" # :man_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2642FE0F200D27A1 = "🧎🏿‍♂️‍➡" # :man_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2642200D27A1 = "🧎🏿‍♂‍➡" # :man_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FB200D2642FE0F200D27A1FE0F = "🧎🏻‍♂️‍➡️" # :man_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2642200D27A1FE0F = "🧎🏻‍♂‍➡️" # :man_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2642FE0F200D27A1 = "🧎🏻‍♂️‍➡" # :man_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2642200D27A1 = "🧎🏻‍♂‍➡" # :man_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FE200D2642FE0F200D27A1FE0F = "🧎🏾‍♂️‍➡️" # :man_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2642200D27A1FE0F = "🧎🏾‍♂‍➡️" # :man_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2642FE0F200D27A1 = "🧎🏾‍♂️‍➡" # :man_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2642200D27A1 = "🧎🏾‍♂‍➡" # :man_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FC200D2642FE0F200D27A1FE0F = "🧎🏼‍♂️‍➡️" # :man_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2642200D27A1FE0F = "🧎🏼‍♂‍➡️" # :man_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2642FE0F200D27A1 = "🧎🏼‍♂️‍➡" # :man_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2642200D27A1 = "🧎🏼‍♂‍➡" # :man_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FD200D2642FE0F200D27A1FE0F = "🧎🏽‍♂️‍➡️" # :man_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2642200D27A1FE0F = "🧎🏽‍♂‍➡️" # :man_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2642FE0F200D27A1 = "🧎🏽‍♂️‍➡" # :man_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2642200D27A1 = "🧎🏽‍♂‍➡" # :man_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FB200D2642FE0F = "🧎🏻‍♂️" # :man_kneeling_light_skin_tone: - U1F9CE1F3FB200D2642 = "🧎🏻‍♂" # :man_kneeling_light_skin_tone: - U1F9CE1F3FE200D2642FE0F = "🧎🏾‍♂️" # :man_kneeling_medium-dark_skin_tone: - U1F9CE1F3FE200D2642 = "🧎🏾‍♂" # :man_kneeling_medium-dark_skin_tone: - U1F9CE1F3FC200D2642FE0F = "🧎🏼‍♂️" # :man_kneeling_medium-light_skin_tone: - U1F9CE1F3FC200D2642 = "🧎🏼‍♂" # :man_kneeling_medium-light_skin_tone: - U1F9CE1F3FD200D2642FE0F = "🧎🏽‍♂️" # :man_kneeling_medium_skin_tone: - U1F9CE1F3FD200D2642 = "🧎🏽‍♂" # :man_kneeling_medium_skin_tone: - U1F3CBFE0F200D2642FE0F = "🏋️‍♂️" # :man_lifting_weights: - U1F3CB200D2642FE0F = "🏋‍♂️" # :man_lifting_weights: - U1F3CBFE0F200D2642 = "🏋️‍♂" # :man_lifting_weights: - U1F3CB200D2642 = "🏋‍♂" # :man_lifting_weights: - U1F3CB1F3FF200D2642FE0F = "🏋🏿‍♂️" # :man_lifting_weights_dark_skin_tone: - U1F3CB1F3FF200D2642 = "🏋🏿‍♂" # :man_lifting_weights_dark_skin_tone: - U1F3CB1F3FB200D2642FE0F = "🏋🏻‍♂️" # :man_lifting_weights_light_skin_tone: - U1F3CB1F3FB200D2642 = "🏋🏻‍♂" # :man_lifting_weights_light_skin_tone: - U1F3CB1F3FE200D2642FE0F = "🏋🏾‍♂️" # :man_lifting_weights_medium-dark_skin_tone: - U1F3CB1F3FE200D2642 = "🏋🏾‍♂" # :man_lifting_weights_medium-dark_skin_tone: - U1F3CB1F3FC200D2642FE0F = "🏋🏼‍♂️" # :man_lifting_weights_medium-light_skin_tone: - U1F3CB1F3FC200D2642 = "🏋🏼‍♂" # :man_lifting_weights_medium-light_skin_tone: - U1F3CB1F3FD200D2642FE0F = "🏋🏽‍♂️" # :man_lifting_weights_medium_skin_tone: - U1F3CB1F3FD200D2642 = "🏋🏽‍♂" # :man_lifting_weights_medium_skin_tone: - U1F4681F3FB = "👨🏻" # :man_light_skin_tone: - U1F4681F3FB200D1F9B2 = "👨🏻‍🦲" # :man_light_skin_tone_bald: - U1F9D41F3FB200D2642FE0F = "🧔🏻‍♂️" # :man_light_skin_tone_beard: - U1F9D41F3FB200D2642 = "🧔🏻‍♂" # :man_light_skin_tone_beard: - U1F4711F3FB200D2642FE0F = "👱🏻‍♂️" # :man_light_skin_tone_blond_hair: - U1F4711F3FB200D2642 = "👱🏻‍♂" # :man_light_skin_tone_blond_hair: - U1F4681F3FB200D1F9B1 = "👨🏻‍🦱" # :man_light_skin_tone_curly_hair: - U1F4681F3FB200D1F9B0 = "👨🏻‍🦰" # :man_light_skin_tone_red_hair: - U1F4681F3FB200D1F9B3 = "👨🏻‍🦳" # :man_light_skin_tone_white_hair: - U1F9D9200D2642FE0F = "🧙‍♂️" # :man_mage: - U1F9D9200D2642 = "🧙‍♂" # :man_mage: - U1F9D91F3FF200D2642FE0F = "🧙🏿‍♂️" # :man_mage_dark_skin_tone: - U1F9D91F3FF200D2642 = "🧙🏿‍♂" # :man_mage_dark_skin_tone: - U1F9D91F3FB200D2642FE0F = "🧙🏻‍♂️" # :man_mage_light_skin_tone: - U1F9D91F3FB200D2642 = "🧙🏻‍♂" # :man_mage_light_skin_tone: - U1F9D91F3FE200D2642FE0F = "🧙🏾‍♂️" # :man_mage_medium-dark_skin_tone: - U1F9D91F3FE200D2642 = "🧙🏾‍♂" # :man_mage_medium-dark_skin_tone: - U1F9D91F3FC200D2642FE0F = "🧙🏼‍♂️" # :man_mage_medium-light_skin_tone: - U1F9D91F3FC200D2642 = "🧙🏼‍♂" # :man_mage_medium-light_skin_tone: - U1F9D91F3FD200D2642FE0F = "🧙🏽‍♂️" # :man_mage_medium_skin_tone: - U1F9D91F3FD200D2642 = "🧙🏽‍♂" # :man_mage_medium_skin_tone: - U1F468200D1F527 = "👨‍🔧" # :man_mechanic: - U1F4681F3FF200D1F527 = "👨🏿‍🔧" # :man_mechanic_dark_skin_tone: - U1F4681F3FB200D1F527 = "👨🏻‍🔧" # :man_mechanic_light_skin_tone: - U1F4681F3FE200D1F527 = "👨🏾‍🔧" # :man_mechanic_medium-dark_skin_tone: - U1F4681F3FC200D1F527 = "👨🏼‍🔧" # :man_mechanic_medium-light_skin_tone: - U1F4681F3FD200D1F527 = "👨🏽‍🔧" # :man_mechanic_medium_skin_tone: - U1F4681F3FE = "👨🏾" # :man_medium-dark_skin_tone: - U1F4681F3FE200D1F9B2 = "👨🏾‍🦲" # :man_medium-dark_skin_tone_bald: - U1F9D41F3FE200D2642FE0F = "🧔🏾‍♂️" # :man_medium-dark_skin_tone_beard: - U1F9D41F3FE200D2642 = "🧔🏾‍♂" # :man_medium-dark_skin_tone_beard: - U1F4711F3FE200D2642FE0F = "👱🏾‍♂️" # :man_medium-dark_skin_tone_blond_hair: - U1F4711F3FE200D2642 = "👱🏾‍♂" # :man_medium-dark_skin_tone_blond_hair: - U1F4681F3FE200D1F9B1 = "👨🏾‍🦱" # :man_medium-dark_skin_tone_curly_hair: - U1F4681F3FE200D1F9B0 = "👨🏾‍🦰" # :man_medium-dark_skin_tone_red_hair: - U1F4681F3FE200D1F9B3 = "👨🏾‍🦳" # :man_medium-dark_skin_tone_white_hair: - U1F4681F3FC = "👨🏼" # :man_medium-light_skin_tone: - U1F4681F3FC200D1F9B2 = "👨🏼‍🦲" # :man_medium-light_skin_tone_bald: - U1F9D41F3FC200D2642FE0F = "🧔🏼‍♂️" # :man_medium-light_skin_tone_beard: - U1F9D41F3FC200D2642 = "🧔🏼‍♂" # :man_medium-light_skin_tone_beard: - U1F4711F3FC200D2642FE0F = "👱🏼‍♂️" # :man_medium-light_skin_tone_blond_hair: - U1F4711F3FC200D2642 = "👱🏼‍♂" # :man_medium-light_skin_tone_blond_hair: - U1F4681F3FC200D1F9B1 = "👨🏼‍🦱" # :man_medium-light_skin_tone_curly_hair: - U1F4681F3FC200D1F9B0 = "👨🏼‍🦰" # :man_medium-light_skin_tone_red_hair: - U1F4681F3FC200D1F9B3 = "👨🏼‍🦳" # :man_medium-light_skin_tone_white_hair: - U1F4681F3FD = "👨🏽" # :man_medium_skin_tone: - U1F4681F3FD200D1F9B2 = "👨🏽‍🦲" # :man_medium_skin_tone_bald: - U1F9D41F3FD200D2642FE0F = "🧔🏽‍♂️" # :man_medium_skin_tone_beard: - U1F9D41F3FD200D2642 = "🧔🏽‍♂" # :man_medium_skin_tone_beard: - U1F4711F3FD200D2642FE0F = "👱🏽‍♂️" # :man_medium_skin_tone_blond_hair: - U1F4711F3FD200D2642 = "👱🏽‍♂" # :man_medium_skin_tone_blond_hair: - U1F4681F3FD200D1F9B1 = "👨🏽‍🦱" # :man_medium_skin_tone_curly_hair: - U1F4681F3FD200D1F9B0 = "👨🏽‍🦰" # :man_medium_skin_tone_red_hair: - U1F4681F3FD200D1F9B3 = "👨🏽‍🦳" # :man_medium_skin_tone_white_hair: - U1F6B5200D2642FE0F = "🚵‍♂️" # :man_mountain_biking: - U1F6B5200D2642 = "🚵‍♂" # :man_mountain_biking: - U1F6B51F3FF200D2642FE0F = "🚵🏿‍♂️" # :man_mountain_biking_dark_skin_tone: - U1F6B51F3FF200D2642 = "🚵🏿‍♂" # :man_mountain_biking_dark_skin_tone: - U1F6B51F3FB200D2642FE0F = "🚵🏻‍♂️" # :man_mountain_biking_light_skin_tone: - U1F6B51F3FB200D2642 = "🚵🏻‍♂" # :man_mountain_biking_light_skin_tone: - U1F6B51F3FE200D2642FE0F = "🚵🏾‍♂️" # :man_mountain_biking_medium-dark_skin_tone: - U1F6B51F3FE200D2642 = "🚵🏾‍♂" # :man_mountain_biking_medium-dark_skin_tone: - U1F6B51F3FC200D2642FE0F = "🚵🏼‍♂️" # :man_mountain_biking_medium-light_skin_tone: - U1F6B51F3FC200D2642 = "🚵🏼‍♂" # :man_mountain_biking_medium-light_skin_tone: - U1F6B51F3FD200D2642FE0F = "🚵🏽‍♂️" # :man_mountain_biking_medium_skin_tone: - U1F6B51F3FD200D2642 = "🚵🏽‍♂" # :man_mountain_biking_medium_skin_tone: - U1F468200D1F4BC = "👨‍💼" # :man_office_worker: - U1F4681F3FF200D1F4BC = "👨🏿‍💼" # :man_office_worker_dark_skin_tone: - U1F4681F3FB200D1F4BC = "👨🏻‍💼" # :man_office_worker_light_skin_tone: - U1F4681F3FE200D1F4BC = "👨🏾‍💼" # :man_office_worker_medium-dark_skin_tone: - U1F4681F3FC200D1F4BC = "👨🏼‍💼" # :man_office_worker_medium-light_skin_tone: - U1F4681F3FD200D1F4BC = "👨🏽‍💼" # :man_office_worker_medium_skin_tone: - U1F468200D2708FE0F = "👨‍✈️" # :man_pilot: - U1F468200D2708 = "👨‍✈" # :man_pilot: - U1F4681F3FF200D2708FE0F = "👨🏿‍✈️" # :man_pilot_dark_skin_tone: - U1F4681F3FF200D2708 = "👨🏿‍✈" # :man_pilot_dark_skin_tone: - U1F4681F3FB200D2708FE0F = "👨🏻‍✈️" # :man_pilot_light_skin_tone: - U1F4681F3FB200D2708 = "👨🏻‍✈" # :man_pilot_light_skin_tone: - U1F4681F3FE200D2708FE0F = "👨🏾‍✈️" # :man_pilot_medium-dark_skin_tone: - U1F4681F3FE200D2708 = "👨🏾‍✈" # :man_pilot_medium-dark_skin_tone: - U1F4681F3FC200D2708FE0F = "👨🏼‍✈️" # :man_pilot_medium-light_skin_tone: - U1F4681F3FC200D2708 = "👨🏼‍✈" # :man_pilot_medium-light_skin_tone: - U1F4681F3FD200D2708FE0F = "👨🏽‍✈️" # :man_pilot_medium_skin_tone: - U1F4681F3FD200D2708 = "👨🏽‍✈" # :man_pilot_medium_skin_tone: - U1F93E200D2642FE0F = "🤾‍♂️" # :man_playing_handball: - U1F93E200D2642 = "🤾‍♂" # :man_playing_handball: - U1F93E1F3FF200D2642FE0F = "🤾🏿‍♂️" # :man_playing_handball_dark_skin_tone: - U1F93E1F3FF200D2642 = "🤾🏿‍♂" # :man_playing_handball_dark_skin_tone: - U1F93E1F3FB200D2642FE0F = "🤾🏻‍♂️" # :man_playing_handball_light_skin_tone: - U1F93E1F3FB200D2642 = "🤾🏻‍♂" # :man_playing_handball_light_skin_tone: - U1F93E1F3FE200D2642FE0F = "🤾🏾‍♂️" # :man_playing_handball_medium-dark_skin_tone: - U1F93E1F3FE200D2642 = "🤾🏾‍♂" # :man_playing_handball_medium-dark_skin_tone: - U1F93E1F3FC200D2642FE0F = "🤾🏼‍♂️" # :man_playing_handball_medium-light_skin_tone: - U1F93E1F3FC200D2642 = "🤾🏼‍♂" # :man_playing_handball_medium-light_skin_tone: - U1F93E1F3FD200D2642FE0F = "🤾🏽‍♂️" # :man_playing_handball_medium_skin_tone: - U1F93E1F3FD200D2642 = "🤾🏽‍♂" # :man_playing_handball_medium_skin_tone: - U1F93D200D2642FE0F = "🤽‍♂️" # :man_playing_water_polo: - U1F93D200D2642 = "🤽‍♂" # :man_playing_water_polo: - U1F93D1F3FF200D2642FE0F = "🤽🏿‍♂️" # :man_playing_water_polo_dark_skin_tone: - U1F93D1F3FF200D2642 = "🤽🏿‍♂" # :man_playing_water_polo_dark_skin_tone: - U1F93D1F3FB200D2642FE0F = "🤽🏻‍♂️" # :man_playing_water_polo_light_skin_tone: - U1F93D1F3FB200D2642 = "🤽🏻‍♂" # :man_playing_water_polo_light_skin_tone: - U1F93D1F3FE200D2642FE0F = "🤽🏾‍♂️" # :man_playing_water_polo_medium-dark_skin_tone: - U1F93D1F3FE200D2642 = "🤽🏾‍♂" # :man_playing_water_polo_medium-dark_skin_tone: - U1F93D1F3FC200D2642FE0F = "🤽🏼‍♂️" # :man_playing_water_polo_medium-light_skin_tone: - U1F93D1F3FC200D2642 = "🤽🏼‍♂" # :man_playing_water_polo_medium-light_skin_tone: - U1F93D1F3FD200D2642FE0F = "🤽🏽‍♂️" # :man_playing_water_polo_medium_skin_tone: - U1F93D1F3FD200D2642 = "🤽🏽‍♂" # :man_playing_water_polo_medium_skin_tone: - U1F46E200D2642FE0F = "👮‍♂️" # :man_police_officer: - U1F46E200D2642 = "👮‍♂" # :man_police_officer: - U1F46E1F3FF200D2642FE0F = "👮🏿‍♂️" # :man_police_officer_dark_skin_tone: - U1F46E1F3FF200D2642 = "👮🏿‍♂" # :man_police_officer_dark_skin_tone: - U1F46E1F3FB200D2642FE0F = "👮🏻‍♂️" # :man_police_officer_light_skin_tone: - U1F46E1F3FB200D2642 = "👮🏻‍♂" # :man_police_officer_light_skin_tone: - U1F46E1F3FE200D2642FE0F = "👮🏾‍♂️" # :man_police_officer_medium-dark_skin_tone: - U1F46E1F3FE200D2642 = "👮🏾‍♂" # :man_police_officer_medium-dark_skin_tone: - U1F46E1F3FC200D2642FE0F = "👮🏼‍♂️" # :man_police_officer_medium-light_skin_tone: - U1F46E1F3FC200D2642 = "👮🏼‍♂" # :man_police_officer_medium-light_skin_tone: - U1F46E1F3FD200D2642FE0F = "👮🏽‍♂️" # :man_police_officer_medium_skin_tone: - U1F46E1F3FD200D2642 = "👮🏽‍♂" # :man_police_officer_medium_skin_tone: - U1F64E200D2642FE0F = "🙎‍♂️" # :man_pouting: - U1F64E200D2642 = "🙎‍♂" # :man_pouting: - U1F64E1F3FF200D2642FE0F = "🙎🏿‍♂️" # :man_pouting_dark_skin_tone: - U1F64E1F3FF200D2642 = "🙎🏿‍♂" # :man_pouting_dark_skin_tone: - U1F64E1F3FB200D2642FE0F = "🙎🏻‍♂️" # :man_pouting_light_skin_tone: - U1F64E1F3FB200D2642 = "🙎🏻‍♂" # :man_pouting_light_skin_tone: - U1F64E1F3FE200D2642FE0F = "🙎🏾‍♂️" # :man_pouting_medium-dark_skin_tone: - U1F64E1F3FE200D2642 = "🙎🏾‍♂" # :man_pouting_medium-dark_skin_tone: - U1F64E1F3FC200D2642FE0F = "🙎🏼‍♂️" # :man_pouting_medium-light_skin_tone: - U1F64E1F3FC200D2642 = "🙎🏼‍♂" # :man_pouting_medium-light_skin_tone: - U1F64E1F3FD200D2642FE0F = "🙎🏽‍♂️" # :man_pouting_medium_skin_tone: - U1F64E1F3FD200D2642 = "🙎🏽‍♂" # :man_pouting_medium_skin_tone: - U1F64B200D2642FE0F = "🙋‍♂️" # :man_raising_hand: - U1F64B200D2642 = "🙋‍♂" # :man_raising_hand: - U1F64B1F3FF200D2642FE0F = "🙋🏿‍♂️" # :man_raising_hand_dark_skin_tone: - U1F64B1F3FF200D2642 = "🙋🏿‍♂" # :man_raising_hand_dark_skin_tone: - U1F64B1F3FB200D2642FE0F = "🙋🏻‍♂️" # :man_raising_hand_light_skin_tone: - U1F64B1F3FB200D2642 = "🙋🏻‍♂" # :man_raising_hand_light_skin_tone: - U1F64B1F3FE200D2642FE0F = "🙋🏾‍♂️" # :man_raising_hand_medium-dark_skin_tone: - U1F64B1F3FE200D2642 = "🙋🏾‍♂" # :man_raising_hand_medium-dark_skin_tone: - U1F64B1F3FC200D2642FE0F = "🙋🏼‍♂️" # :man_raising_hand_medium-light_skin_tone: - U1F64B1F3FC200D2642 = "🙋🏼‍♂" # :man_raising_hand_medium-light_skin_tone: - U1F64B1F3FD200D2642FE0F = "🙋🏽‍♂️" # :man_raising_hand_medium_skin_tone: - U1F64B1F3FD200D2642 = "🙋🏽‍♂" # :man_raising_hand_medium_skin_tone: - U1F468200D1F9B0 = "👨‍🦰" # :man_red_hair: - U1F6A3200D2642FE0F = "🚣‍♂️" # :man_rowing_boat: - U1F6A3200D2642 = "🚣‍♂" # :man_rowing_boat: - U1F6A31F3FF200D2642FE0F = "🚣🏿‍♂️" # :man_rowing_boat_dark_skin_tone: - U1F6A31F3FF200D2642 = "🚣🏿‍♂" # :man_rowing_boat_dark_skin_tone: - U1F6A31F3FB200D2642FE0F = "🚣🏻‍♂️" # :man_rowing_boat_light_skin_tone: - U1F6A31F3FB200D2642 = "🚣🏻‍♂" # :man_rowing_boat_light_skin_tone: - U1F6A31F3FE200D2642FE0F = "🚣🏾‍♂️" # :man_rowing_boat_medium-dark_skin_tone: - U1F6A31F3FE200D2642 = "🚣🏾‍♂" # :man_rowing_boat_medium-dark_skin_tone: - U1F6A31F3FC200D2642FE0F = "🚣🏼‍♂️" # :man_rowing_boat_medium-light_skin_tone: - U1F6A31F3FC200D2642 = "🚣🏼‍♂" # :man_rowing_boat_medium-light_skin_tone: - U1F6A31F3FD200D2642FE0F = "🚣🏽‍♂️" # :man_rowing_boat_medium_skin_tone: - U1F6A31F3FD200D2642 = "🚣🏽‍♂" # :man_rowing_boat_medium_skin_tone: - U1F3C3200D2642FE0F = "🏃‍♂️" # :man_running: - U1F3C3200D2642 = "🏃‍♂" # :man_running: - U1F3C31F3FF200D2642FE0F = "🏃🏿‍♂️" # :man_running_dark_skin_tone: - U1F3C31F3FF200D2642 = "🏃🏿‍♂" # :man_running_dark_skin_tone: - U1F3C3200D2642FE0F200D27A1FE0F = "🏃‍♂️‍➡️" # :man_running_facing_right: - U1F3C3200D2642200D27A1FE0F = "🏃‍♂‍➡️" # :man_running_facing_right: - U1F3C3200D2642FE0F200D27A1 = "🏃‍♂️‍➡" # :man_running_facing_right: - U1F3C3200D2642200D27A1 = "🏃‍♂‍➡" # :man_running_facing_right: - U1F3C31F3FF200D2642FE0F200D27A1FE0F = "🏃🏿‍♂️‍➡️" # :man_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2642200D27A1FE0F = "🏃🏿‍♂‍➡️" # :man_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2642FE0F200D27A1 = "🏃🏿‍♂️‍➡" # :man_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2642200D27A1 = "🏃🏿‍♂‍➡" # :man_running_facing_right_dark_skin_tone: - U1F3C31F3FB200D2642FE0F200D27A1FE0F = "🏃🏻‍♂️‍➡️" # :man_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2642200D27A1FE0F = "🏃🏻‍♂‍➡️" # :man_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2642FE0F200D27A1 = "🏃🏻‍♂️‍➡" # :man_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2642200D27A1 = "🏃🏻‍♂‍➡" # :man_running_facing_right_light_skin_tone: - U1F3C31F3FE200D2642FE0F200D27A1FE0F = "🏃🏾‍♂️‍➡️" # :man_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2642200D27A1FE0F = "🏃🏾‍♂‍➡️" # :man_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2642FE0F200D27A1 = "🏃🏾‍♂️‍➡" # :man_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2642200D27A1 = "🏃🏾‍♂‍➡" # :man_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FC200D2642FE0F200D27A1FE0F = "🏃🏼‍♂️‍➡️" # :man_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2642200D27A1FE0F = "🏃🏼‍♂‍➡️" # :man_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2642FE0F200D27A1 = "🏃🏼‍♂️‍➡" # :man_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2642200D27A1 = "🏃🏼‍♂‍➡" # :man_running_facing_right_medium-light_skin_tone: - U1F3C31F3FD200D2642FE0F200D27A1FE0F = "🏃🏽‍♂️‍➡️" # :man_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2642200D27A1FE0F = "🏃🏽‍♂‍➡️" # :man_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2642FE0F200D27A1 = "🏃🏽‍♂️‍➡" # :man_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2642200D27A1 = "🏃🏽‍♂‍➡" # :man_running_facing_right_medium_skin_tone: - U1F3C31F3FB200D2642FE0F = "🏃🏻‍♂️" # :man_running_light_skin_tone: - U1F3C31F3FB200D2642 = "🏃🏻‍♂" # :man_running_light_skin_tone: - U1F3C31F3FE200D2642FE0F = "🏃🏾‍♂️" # :man_running_medium-dark_skin_tone: - U1F3C31F3FE200D2642 = "🏃🏾‍♂" # :man_running_medium-dark_skin_tone: - U1F3C31F3FC200D2642FE0F = "🏃🏼‍♂️" # :man_running_medium-light_skin_tone: - U1F3C31F3FC200D2642 = "🏃🏼‍♂" # :man_running_medium-light_skin_tone: - U1F3C31F3FD200D2642FE0F = "🏃🏽‍♂️" # :man_running_medium_skin_tone: - U1F3C31F3FD200D2642 = "🏃🏽‍♂" # :man_running_medium_skin_tone: - U1F468200D1F52C = "👨‍🔬" # :man_scientist: - U1F4681F3FF200D1F52C = "👨🏿‍🔬" # :man_scientist_dark_skin_tone: - U1F4681F3FB200D1F52C = "👨🏻‍🔬" # :man_scientist_light_skin_tone: - U1F4681F3FE200D1F52C = "👨🏾‍🔬" # :man_scientist_medium-dark_skin_tone: - U1F4681F3FC200D1F52C = "👨🏼‍🔬" # :man_scientist_medium-light_skin_tone: - U1F4681F3FD200D1F52C = "👨🏽‍🔬" # :man_scientist_medium_skin_tone: - U1F937200D2642FE0F = "🤷‍♂️" # :man_shrugging: - U1F937200D2642 = "🤷‍♂" # :man_shrugging: - U1F9371F3FF200D2642FE0F = "🤷🏿‍♂️" # :man_shrugging_dark_skin_tone: - U1F9371F3FF200D2642 = "🤷🏿‍♂" # :man_shrugging_dark_skin_tone: - U1F9371F3FB200D2642FE0F = "🤷🏻‍♂️" # :man_shrugging_light_skin_tone: - U1F9371F3FB200D2642 = "🤷🏻‍♂" # :man_shrugging_light_skin_tone: - U1F9371F3FE200D2642FE0F = "🤷🏾‍♂️" # :man_shrugging_medium-dark_skin_tone: - U1F9371F3FE200D2642 = "🤷🏾‍♂" # :man_shrugging_medium-dark_skin_tone: - U1F9371F3FC200D2642FE0F = "🤷🏼‍♂️" # :man_shrugging_medium-light_skin_tone: - U1F9371F3FC200D2642 = "🤷🏼‍♂" # :man_shrugging_medium-light_skin_tone: - U1F9371F3FD200D2642FE0F = "🤷🏽‍♂️" # :man_shrugging_medium_skin_tone: - U1F9371F3FD200D2642 = "🤷🏽‍♂" # :man_shrugging_medium_skin_tone: - U1F468200D1F3A4 = "👨‍🎤" # :man_singer: - U1F4681F3FF200D1F3A4 = "👨🏿‍🎤" # :man_singer_dark_skin_tone: - U1F4681F3FB200D1F3A4 = "👨🏻‍🎤" # :man_singer_light_skin_tone: - U1F4681F3FE200D1F3A4 = "👨🏾‍🎤" # :man_singer_medium-dark_skin_tone: - U1F4681F3FC200D1F3A4 = "👨🏼‍🎤" # :man_singer_medium-light_skin_tone: - U1F4681F3FD200D1F3A4 = "👨🏽‍🎤" # :man_singer_medium_skin_tone: - U1F9CD200D2642FE0F = "🧍‍♂️" # :man_standing: - U1F9CD200D2642 = "🧍‍♂" # :man_standing: - U1F9CD1F3FF200D2642FE0F = "🧍🏿‍♂️" # :man_standing_dark_skin_tone: - U1F9CD1F3FF200D2642 = "🧍🏿‍♂" # :man_standing_dark_skin_tone: - U1F9CD1F3FB200D2642FE0F = "🧍🏻‍♂️" # :man_standing_light_skin_tone: - U1F9CD1F3FB200D2642 = "🧍🏻‍♂" # :man_standing_light_skin_tone: - U1F9CD1F3FE200D2642FE0F = "🧍🏾‍♂️" # :man_standing_medium-dark_skin_tone: - U1F9CD1F3FE200D2642 = "🧍🏾‍♂" # :man_standing_medium-dark_skin_tone: - U1F9CD1F3FC200D2642FE0F = "🧍🏼‍♂️" # :man_standing_medium-light_skin_tone: - U1F9CD1F3FC200D2642 = "🧍🏼‍♂" # :man_standing_medium-light_skin_tone: - U1F9CD1F3FD200D2642FE0F = "🧍🏽‍♂️" # :man_standing_medium_skin_tone: - U1F9CD1F3FD200D2642 = "🧍🏽‍♂" # :man_standing_medium_skin_tone: - U1F468200D1F393 = "👨‍🎓" # :man_student: - U1F4681F3FF200D1F393 = "👨🏿‍🎓" # :man_student_dark_skin_tone: - U1F4681F3FB200D1F393 = "👨🏻‍🎓" # :man_student_light_skin_tone: - U1F4681F3FE200D1F393 = "👨🏾‍🎓" # :man_student_medium-dark_skin_tone: - U1F4681F3FC200D1F393 = "👨🏼‍🎓" # :man_student_medium-light_skin_tone: - U1F4681F3FD200D1F393 = "👨🏽‍🎓" # :man_student_medium_skin_tone: - U1F9B8200D2642FE0F = "🦸‍♂️" # :man_superhero: - U1F9B8200D2642 = "🦸‍♂" # :man_superhero: - U1F9B81F3FF200D2642FE0F = "🦸🏿‍♂️" # :man_superhero_dark_skin_tone: - U1F9B81F3FF200D2642 = "🦸🏿‍♂" # :man_superhero_dark_skin_tone: - U1F9B81F3FB200D2642FE0F = "🦸🏻‍♂️" # :man_superhero_light_skin_tone: - U1F9B81F3FB200D2642 = "🦸🏻‍♂" # :man_superhero_light_skin_tone: - U1F9B81F3FE200D2642FE0F = "🦸🏾‍♂️" # :man_superhero_medium-dark_skin_tone: - U1F9B81F3FE200D2642 = "🦸🏾‍♂" # :man_superhero_medium-dark_skin_tone: - U1F9B81F3FC200D2642FE0F = "🦸🏼‍♂️" # :man_superhero_medium-light_skin_tone: - U1F9B81F3FC200D2642 = "🦸🏼‍♂" # :man_superhero_medium-light_skin_tone: - U1F9B81F3FD200D2642FE0F = "🦸🏽‍♂️" # :man_superhero_medium_skin_tone: - U1F9B81F3FD200D2642 = "🦸🏽‍♂" # :man_superhero_medium_skin_tone: - U1F9B9200D2642FE0F = "🦹‍♂️" # :man_supervillain: - U1F9B9200D2642 = "🦹‍♂" # :man_supervillain: - U1F9B91F3FF200D2642FE0F = "🦹🏿‍♂️" # :man_supervillain_dark_skin_tone: - U1F9B91F3FF200D2642 = "🦹🏿‍♂" # :man_supervillain_dark_skin_tone: - U1F9B91F3FB200D2642FE0F = "🦹🏻‍♂️" # :man_supervillain_light_skin_tone: - U1F9B91F3FB200D2642 = "🦹🏻‍♂" # :man_supervillain_light_skin_tone: - U1F9B91F3FE200D2642FE0F = "🦹🏾‍♂️" # :man_supervillain_medium-dark_skin_tone: - U1F9B91F3FE200D2642 = "🦹🏾‍♂" # :man_supervillain_medium-dark_skin_tone: - U1F9B91F3FC200D2642FE0F = "🦹🏼‍♂️" # :man_supervillain_medium-light_skin_tone: - U1F9B91F3FC200D2642 = "🦹🏼‍♂" # :man_supervillain_medium-light_skin_tone: - U1F9B91F3FD200D2642FE0F = "🦹🏽‍♂️" # :man_supervillain_medium_skin_tone: - U1F9B91F3FD200D2642 = "🦹🏽‍♂" # :man_supervillain_medium_skin_tone: - U1F3C4200D2642FE0F = "🏄‍♂️" # :man_surfing: - U1F3C4200D2642 = "🏄‍♂" # :man_surfing: - U1F3C41F3FF200D2642FE0F = "🏄🏿‍♂️" # :man_surfing_dark_skin_tone: - U1F3C41F3FF200D2642 = "🏄🏿‍♂" # :man_surfing_dark_skin_tone: - U1F3C41F3FB200D2642FE0F = "🏄🏻‍♂️" # :man_surfing_light_skin_tone: - U1F3C41F3FB200D2642 = "🏄🏻‍♂" # :man_surfing_light_skin_tone: - U1F3C41F3FE200D2642FE0F = "🏄🏾‍♂️" # :man_surfing_medium-dark_skin_tone: - U1F3C41F3FE200D2642 = "🏄🏾‍♂" # :man_surfing_medium-dark_skin_tone: - U1F3C41F3FC200D2642FE0F = "🏄🏼‍♂️" # :man_surfing_medium-light_skin_tone: - U1F3C41F3FC200D2642 = "🏄🏼‍♂" # :man_surfing_medium-light_skin_tone: - U1F3C41F3FD200D2642FE0F = "🏄🏽‍♂️" # :man_surfing_medium_skin_tone: - U1F3C41F3FD200D2642 = "🏄🏽‍♂" # :man_surfing_medium_skin_tone: - U1F3CA200D2642FE0F = "🏊‍♂️" # :man_swimming: - U1F3CA200D2642 = "🏊‍♂" # :man_swimming: - U1F3CA1F3FF200D2642FE0F = "🏊🏿‍♂️" # :man_swimming_dark_skin_tone: - U1F3CA1F3FF200D2642 = "🏊🏿‍♂" # :man_swimming_dark_skin_tone: - U1F3CA1F3FB200D2642FE0F = "🏊🏻‍♂️" # :man_swimming_light_skin_tone: - U1F3CA1F3FB200D2642 = "🏊🏻‍♂" # :man_swimming_light_skin_tone: - U1F3CA1F3FE200D2642FE0F = "🏊🏾‍♂️" # :man_swimming_medium-dark_skin_tone: - U1F3CA1F3FE200D2642 = "🏊🏾‍♂" # :man_swimming_medium-dark_skin_tone: - U1F3CA1F3FC200D2642FE0F = "🏊🏼‍♂️" # :man_swimming_medium-light_skin_tone: - U1F3CA1F3FC200D2642 = "🏊🏼‍♂" # :man_swimming_medium-light_skin_tone: - U1F3CA1F3FD200D2642FE0F = "🏊🏽‍♂️" # :man_swimming_medium_skin_tone: - U1F3CA1F3FD200D2642 = "🏊🏽‍♂" # :man_swimming_medium_skin_tone: - U1F468200D1F3EB = "👨‍🏫" # :man_teacher: - U1F4681F3FF200D1F3EB = "👨🏿‍🏫" # :man_teacher_dark_skin_tone: - U1F4681F3FB200D1F3EB = "👨🏻‍🏫" # :man_teacher_light_skin_tone: - U1F4681F3FE200D1F3EB = "👨🏾‍🏫" # :man_teacher_medium-dark_skin_tone: - U1F4681F3FC200D1F3EB = "👨🏼‍🏫" # :man_teacher_medium-light_skin_tone: - U1F4681F3FD200D1F3EB = "👨🏽‍🏫" # :man_teacher_medium_skin_tone: - U1F468200D1F4BB = "👨‍💻" # :man_technologist: - U1F4681F3FF200D1F4BB = "👨🏿‍💻" # :man_technologist_dark_skin_tone: - U1F4681F3FB200D1F4BB = "👨🏻‍💻" # :man_technologist_light_skin_tone: - U1F4681F3FE200D1F4BB = "👨🏾‍💻" # :man_technologist_medium-dark_skin_tone: - U1F4681F3FC200D1F4BB = "👨🏼‍💻" # :man_technologist_medium-light_skin_tone: - U1F4681F3FD200D1F4BB = "👨🏽‍💻" # :man_technologist_medium_skin_tone: - U1F481200D2642FE0F = "💁‍♂️" # :man_tipping_hand: - U1F481200D2642 = "💁‍♂" # :man_tipping_hand: - U1F4811F3FF200D2642FE0F = "💁🏿‍♂️" # :man_tipping_hand_dark_skin_tone: - U1F4811F3FF200D2642 = "💁🏿‍♂" # :man_tipping_hand_dark_skin_tone: - U1F4811F3FB200D2642FE0F = "💁🏻‍♂️" # :man_tipping_hand_light_skin_tone: - U1F4811F3FB200D2642 = "💁🏻‍♂" # :man_tipping_hand_light_skin_tone: - U1F4811F3FE200D2642FE0F = "💁🏾‍♂️" # :man_tipping_hand_medium-dark_skin_tone: - U1F4811F3FE200D2642 = "💁🏾‍♂" # :man_tipping_hand_medium-dark_skin_tone: - U1F4811F3FC200D2642FE0F = "💁🏼‍♂️" # :man_tipping_hand_medium-light_skin_tone: - U1F4811F3FC200D2642 = "💁🏼‍♂" # :man_tipping_hand_medium-light_skin_tone: - U1F4811F3FD200D2642FE0F = "💁🏽‍♂️" # :man_tipping_hand_medium_skin_tone: - U1F4811F3FD200D2642 = "💁🏽‍♂" # :man_tipping_hand_medium_skin_tone: - U1F9DB200D2642FE0F = "🧛‍♂️" # :man_vampire: - U1F9DB200D2642 = "🧛‍♂" # :man_vampire: - U1F9DB1F3FF200D2642FE0F = "🧛🏿‍♂️" # :man_vampire_dark_skin_tone: - U1F9DB1F3FF200D2642 = "🧛🏿‍♂" # :man_vampire_dark_skin_tone: - U1F9DB1F3FB200D2642FE0F = "🧛🏻‍♂️" # :man_vampire_light_skin_tone: - U1F9DB1F3FB200D2642 = "🧛🏻‍♂" # :man_vampire_light_skin_tone: - U1F9DB1F3FE200D2642FE0F = "🧛🏾‍♂️" # :man_vampire_medium-dark_skin_tone: - U1F9DB1F3FE200D2642 = "🧛🏾‍♂" # :man_vampire_medium-dark_skin_tone: - U1F9DB1F3FC200D2642FE0F = "🧛🏼‍♂️" # :man_vampire_medium-light_skin_tone: - U1F9DB1F3FC200D2642 = "🧛🏼‍♂" # :man_vampire_medium-light_skin_tone: - U1F9DB1F3FD200D2642FE0F = "🧛🏽‍♂️" # :man_vampire_medium_skin_tone: - U1F9DB1F3FD200D2642 = "🧛🏽‍♂" # :man_vampire_medium_skin_tone: - U1F6B6200D2642FE0F = "🚶‍♂️" # :man_walking: - U1F6B6200D2642 = "🚶‍♂" # :man_walking: - U1F6B61F3FF200D2642FE0F = "🚶🏿‍♂️" # :man_walking_dark_skin_tone: - U1F6B61F3FF200D2642 = "🚶🏿‍♂" # :man_walking_dark_skin_tone: - U1F6B6200D2642FE0F200D27A1FE0F = "🚶‍♂️‍➡️" # :man_walking_facing_right: - U1F6B6200D2642200D27A1FE0F = "🚶‍♂‍➡️" # :man_walking_facing_right: - U1F6B6200D2642FE0F200D27A1 = "🚶‍♂️‍➡" # :man_walking_facing_right: - U1F6B6200D2642200D27A1 = "🚶‍♂‍➡" # :man_walking_facing_right: - U1F6B61F3FF200D2642FE0F200D27A1FE0F = "🚶🏿‍♂️‍➡️" # :man_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2642200D27A1FE0F = "🚶🏿‍♂‍➡️" # :man_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2642FE0F200D27A1 = "🚶🏿‍♂️‍➡" # :man_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2642200D27A1 = "🚶🏿‍♂‍➡" # :man_walking_facing_right_dark_skin_tone: - U1F6B61F3FB200D2642FE0F200D27A1FE0F = "🚶🏻‍♂️‍➡️" # :man_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2642200D27A1FE0F = "🚶🏻‍♂‍➡️" # :man_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2642FE0F200D27A1 = "🚶🏻‍♂️‍➡" # :man_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2642200D27A1 = "🚶🏻‍♂‍➡" # :man_walking_facing_right_light_skin_tone: - U1F6B61F3FE200D2642FE0F200D27A1FE0F = "🚶🏾‍♂️‍➡️" # :man_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2642200D27A1FE0F = "🚶🏾‍♂‍➡️" # :man_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2642FE0F200D27A1 = "🚶🏾‍♂️‍➡" # :man_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2642200D27A1 = "🚶🏾‍♂‍➡" # :man_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FC200D2642FE0F200D27A1FE0F = "🚶🏼‍♂️‍➡️" # :man_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2642200D27A1FE0F = "🚶🏼‍♂‍➡️" # :man_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2642FE0F200D27A1 = "🚶🏼‍♂️‍➡" # :man_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2642200D27A1 = "🚶🏼‍♂‍➡" # :man_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FD200D2642FE0F200D27A1FE0F = "🚶🏽‍♂️‍➡️" # :man_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2642200D27A1FE0F = "🚶🏽‍♂‍➡️" # :man_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2642FE0F200D27A1 = "🚶🏽‍♂️‍➡" # :man_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2642200D27A1 = "🚶🏽‍♂‍➡" # :man_walking_facing_right_medium_skin_tone: - U1F6B61F3FB200D2642FE0F = "🚶🏻‍♂️" # :man_walking_light_skin_tone: - U1F6B61F3FB200D2642 = "🚶🏻‍♂" # :man_walking_light_skin_tone: - U1F6B61F3FE200D2642FE0F = "🚶🏾‍♂️" # :man_walking_medium-dark_skin_tone: - U1F6B61F3FE200D2642 = "🚶🏾‍♂" # :man_walking_medium-dark_skin_tone: - U1F6B61F3FC200D2642FE0F = "🚶🏼‍♂️" # :man_walking_medium-light_skin_tone: - U1F6B61F3FC200D2642 = "🚶🏼‍♂" # :man_walking_medium-light_skin_tone: - U1F6B61F3FD200D2642FE0F = "🚶🏽‍♂️" # :man_walking_medium_skin_tone: - U1F6B61F3FD200D2642 = "🚶🏽‍♂" # :man_walking_medium_skin_tone: - U1F473200D2642FE0F = "👳‍♂️" # :man_wearing_turban: - U1F473200D2642 = "👳‍♂" # :man_wearing_turban: - U1F4731F3FF200D2642FE0F = "👳🏿‍♂️" # :man_wearing_turban_dark_skin_tone: - U1F4731F3FF200D2642 = "👳🏿‍♂" # :man_wearing_turban_dark_skin_tone: - U1F4731F3FB200D2642FE0F = "👳🏻‍♂️" # :man_wearing_turban_light_skin_tone: - U1F4731F3FB200D2642 = "👳🏻‍♂" # :man_wearing_turban_light_skin_tone: - U1F4731F3FE200D2642FE0F = "👳🏾‍♂️" # :man_wearing_turban_medium-dark_skin_tone: - U1F4731F3FE200D2642 = "👳🏾‍♂" # :man_wearing_turban_medium-dark_skin_tone: - U1F4731F3FC200D2642FE0F = "👳🏼‍♂️" # :man_wearing_turban_medium-light_skin_tone: - U1F4731F3FC200D2642 = "👳🏼‍♂" # :man_wearing_turban_medium-light_skin_tone: - U1F4731F3FD200D2642FE0F = "👳🏽‍♂️" # :man_wearing_turban_medium_skin_tone: - U1F4731F3FD200D2642 = "👳🏽‍♂" # :man_wearing_turban_medium_skin_tone: - U1F468200D1F9B3 = "👨‍🦳" # :man_white_hair: - U1F470200D2642FE0F = "👰‍♂️" # :man_with_veil: - U1F470200D2642 = "👰‍♂" # :man_with_veil: - U1F4701F3FF200D2642FE0F = "👰🏿‍♂️" # :man_with_veil_dark_skin_tone: - U1F4701F3FF200D2642 = "👰🏿‍♂" # :man_with_veil_dark_skin_tone: - U1F4701F3FB200D2642FE0F = "👰🏻‍♂️" # :man_with_veil_light_skin_tone: - U1F4701F3FB200D2642 = "👰🏻‍♂" # :man_with_veil_light_skin_tone: - U1F4701F3FE200D2642FE0F = "👰🏾‍♂️" # :man_with_veil_medium-dark_skin_tone: - U1F4701F3FE200D2642 = "👰🏾‍♂" # :man_with_veil_medium-dark_skin_tone: - U1F4701F3FC200D2642FE0F = "👰🏼‍♂️" # :man_with_veil_medium-light_skin_tone: - U1F4701F3FC200D2642 = "👰🏼‍♂" # :man_with_veil_medium-light_skin_tone: - U1F4701F3FD200D2642FE0F = "👰🏽‍♂️" # :man_with_veil_medium_skin_tone: - U1F4701F3FD200D2642 = "👰🏽‍♂" # :man_with_veil_medium_skin_tone: - U1F468200D1F9AF = "👨‍🦯" # :man_with_white_cane: - U1F4681F3FF200D1F9AF = "👨🏿‍🦯" # :man_with_white_cane_dark_skin_tone: - U1F468200D1F9AF200D27A1FE0F = "👨‍🦯‍➡️" # :man_with_white_cane_facing_right: - U1F468200D1F9AF200D27A1 = "👨‍🦯‍➡" # :man_with_white_cane_facing_right: - U1F4681F3FF200D1F9AF200D27A1FE0F = "👨🏿‍🦯‍➡️" # :man_with_white_cane_facing_right_dark_skin_tone: - U1F4681F3FF200D1F9AF200D27A1 = "👨🏿‍🦯‍➡" # :man_with_white_cane_facing_right_dark_skin_tone: - U1F4681F3FB200D1F9AF200D27A1FE0F = "👨🏻‍🦯‍➡️" # :man_with_white_cane_facing_right_light_skin_tone: - U1F4681F3FB200D1F9AF200D27A1 = "👨🏻‍🦯‍➡" # :man_with_white_cane_facing_right_light_skin_tone: - U1F4681F3FE200D1F9AF200D27A1FE0F = "👨🏾‍🦯‍➡️" # :man_with_white_cane_facing_right_medium-dark_skin_tone: - U1F4681F3FE200D1F9AF200D27A1 = "👨🏾‍🦯‍➡" # :man_with_white_cane_facing_right_medium-dark_skin_tone: - U1F4681F3FC200D1F9AF200D27A1FE0F = "👨🏼‍🦯‍➡️" # :man_with_white_cane_facing_right_medium-light_skin_tone: - U1F4681F3FC200D1F9AF200D27A1 = "👨🏼‍🦯‍➡" # :man_with_white_cane_facing_right_medium-light_skin_tone: - U1F4681F3FD200D1F9AF200D27A1FE0F = "👨🏽‍🦯‍➡️" # :man_with_white_cane_facing_right_medium_skin_tone: - U1F4681F3FD200D1F9AF200D27A1 = "👨🏽‍🦯‍➡" # :man_with_white_cane_facing_right_medium_skin_tone: - U1F4681F3FB200D1F9AF = "👨🏻‍🦯" # :man_with_white_cane_light_skin_tone: - U1F4681F3FE200D1F9AF = "👨🏾‍🦯" # :man_with_white_cane_medium-dark_skin_tone: - U1F4681F3FC200D1F9AF = "👨🏼‍🦯" # :man_with_white_cane_medium-light_skin_tone: - U1F4681F3FD200D1F9AF = "👨🏽‍🦯" # :man_with_white_cane_medium_skin_tone: - U1F9DF200D2642FE0F = "🧟‍♂️" # :man_zombie: - U1F9DF200D2642 = "🧟‍♂" # :man_zombie: - U1F96D = "🥭" # :mango: - U1F570FE0F = "🕰️" # :mantelpiece_clock: - U1F570 = "🕰" # :mantelpiece_clock: - U1F9BD = "🦽" # :manual_wheelchair: - U1F45E = "👞" # :man’s_shoe: - U1F5FE = "🗾" # :map_of_Japan: - U1F341 = "🍁" # :maple_leaf: - U1FA87 = "🪇" # :maracas: - U1F94B = "🥋" # :martial_arts_uniform: - U1F9C9 = "🧉" # :mate: - U1F356 = "🍖" # :meat_on_bone: - U1F9D1200D1F527 = "🧑‍🔧" # :mechanic: - U1F9D11F3FF200D1F527 = "🧑🏿‍🔧" # :mechanic_dark_skin_tone: - U1F9D11F3FB200D1F527 = "🧑🏻‍🔧" # :mechanic_light_skin_tone: - U1F9D11F3FE200D1F527 = "🧑🏾‍🔧" # :mechanic_medium-dark_skin_tone: - U1F9D11F3FC200D1F527 = "🧑🏼‍🔧" # :mechanic_medium-light_skin_tone: - U1F9D11F3FD200D1F527 = "🧑🏽‍🔧" # :mechanic_medium_skin_tone: - U1F9BE = "🦾" # :mechanical_arm: - U1F9BF = "🦿" # :mechanical_leg: - U2695FE0F = "⚕️" # :medical_symbol: - U2695 = "⚕" # :medical_symbol: - U1F3FE = "🏾" # :medium-dark_skin_tone: - U1F3FC = "🏼" # :medium-light_skin_tone: - U1F3FD = "🏽" # :medium_skin_tone: - U1F4E3 = "📣" # :megaphone: - U1F348 = "🍈" # :melon: - U1FAE0 = "🫠" # :melting_face: - U1F4DD = "📝" # :memo: - U1F46C = "👬" # :men_holding_hands: - U1F46C1F3FF = "👬🏿" # :men_holding_hands_dark_skin_tone: - U1F4681F3FF200D1F91D200D1F4681F3FB = "👨🏿‍🤝‍👨🏻" # :men_holding_hands_dark_skin_tone_light_skin_tone: - U1F4681F3FF200D1F91D200D1F4681F3FE = "👨🏿‍🤝‍👨🏾" # :men_holding_hands_dark_skin_tone_medium-dark_skin_tone: - U1F4681F3FF200D1F91D200D1F4681F3FC = "👨🏿‍🤝‍👨🏼" # :men_holding_hands_dark_skin_tone_medium-light_skin_tone: - U1F4681F3FF200D1F91D200D1F4681F3FD = "👨🏿‍🤝‍👨🏽" # :men_holding_hands_dark_skin_tone_medium_skin_tone: - U1F46C1F3FB = "👬🏻" # :men_holding_hands_light_skin_tone: - U1F4681F3FB200D1F91D200D1F4681F3FF = "👨🏻‍🤝‍👨🏿" # :men_holding_hands_light_skin_tone_dark_skin_tone: - U1F4681F3FB200D1F91D200D1F4681F3FE = "👨🏻‍🤝‍👨🏾" # :men_holding_hands_light_skin_tone_medium-dark_skin_tone: - U1F4681F3FB200D1F91D200D1F4681F3FC = "👨🏻‍🤝‍👨🏼" # :men_holding_hands_light_skin_tone_medium-light_skin_tone: - U1F4681F3FB200D1F91D200D1F4681F3FD = "👨🏻‍🤝‍👨🏽" # :men_holding_hands_light_skin_tone_medium_skin_tone: - U1F46C1F3FE = "👬🏾" # :men_holding_hands_medium-dark_skin_tone: - U1F4681F3FE200D1F91D200D1F4681F3FF = "👨🏾‍🤝‍👨🏿" # :men_holding_hands_medium-dark_skin_tone_dark_skin_tone: - U1F4681F3FE200D1F91D200D1F4681F3FB = "👨🏾‍🤝‍👨🏻" # :men_holding_hands_medium-dark_skin_tone_light_skin_tone: - U1F4681F3FE200D1F91D200D1F4681F3FC = "👨🏾‍🤝‍👨🏼" # :men_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: - U1F4681F3FE200D1F91D200D1F4681F3FD = "👨🏾‍🤝‍👨🏽" # :men_holding_hands_medium-dark_skin_tone_medium_skin_tone: - U1F46C1F3FC = "👬🏼" # :men_holding_hands_medium-light_skin_tone: - U1F4681F3FC200D1F91D200D1F4681F3FF = "👨🏼‍🤝‍👨🏿" # :men_holding_hands_medium-light_skin_tone_dark_skin_tone: - U1F4681F3FC200D1F91D200D1F4681F3FB = "👨🏼‍🤝‍👨🏻" # :men_holding_hands_medium-light_skin_tone_light_skin_tone: - U1F4681F3FC200D1F91D200D1F4681F3FE = "👨🏼‍🤝‍👨🏾" # :men_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: - U1F4681F3FC200D1F91D200D1F4681F3FD = "👨🏼‍🤝‍👨🏽" # :men_holding_hands_medium-light_skin_tone_medium_skin_tone: - U1F46C1F3FD = "👬🏽" # :men_holding_hands_medium_skin_tone: - U1F4681F3FD200D1F91D200D1F4681F3FF = "👨🏽‍🤝‍👨🏿" # :men_holding_hands_medium_skin_tone_dark_skin_tone: - U1F4681F3FD200D1F91D200D1F4681F3FB = "👨🏽‍🤝‍👨🏻" # :men_holding_hands_medium_skin_tone_light_skin_tone: - U1F4681F3FD200D1F91D200D1F4681F3FE = "👨🏽‍🤝‍👨🏾" # :men_holding_hands_medium_skin_tone_medium-dark_skin_tone: - U1F4681F3FD200D1F91D200D1F4681F3FC = "👨🏽‍🤝‍👨🏼" # :men_holding_hands_medium_skin_tone_medium-light_skin_tone: - U1F46F200D2642FE0F = "👯‍♂️" # :men_with_bunny_ears: - U1F46F200D2642 = "👯‍♂" # :men_with_bunny_ears: - U1F93C200D2642FE0F = "🤼‍♂️" # :men_wrestling: - U1F93C200D2642 = "🤼‍♂" # :men_wrestling: - U2764FE0F200D1FA79 = "❤️‍🩹" # :mending_heart: - U2764200D1FA79 = "❤‍🩹" # :mending_heart: - U1F54E = "🕎" # :menorah: - U1F6B9 = "🚹" # :men’s_room: - U1F9DC200D2640FE0F = "🧜‍♀️" # :mermaid: - U1F9DC200D2640 = "🧜‍♀" # :mermaid: - U1F9DC1F3FF200D2640FE0F = "🧜🏿‍♀️" # :mermaid_dark_skin_tone: - U1F9DC1F3FF200D2640 = "🧜🏿‍♀" # :mermaid_dark_skin_tone: - U1F9DC1F3FB200D2640FE0F = "🧜🏻‍♀️" # :mermaid_light_skin_tone: - U1F9DC1F3FB200D2640 = "🧜🏻‍♀" # :mermaid_light_skin_tone: - U1F9DC1F3FE200D2640FE0F = "🧜🏾‍♀️" # :mermaid_medium-dark_skin_tone: - U1F9DC1F3FE200D2640 = "🧜🏾‍♀" # :mermaid_medium-dark_skin_tone: - U1F9DC1F3FC200D2640FE0F = "🧜🏼‍♀️" # :mermaid_medium-light_skin_tone: - U1F9DC1F3FC200D2640 = "🧜🏼‍♀" # :mermaid_medium-light_skin_tone: - U1F9DC1F3FD200D2640FE0F = "🧜🏽‍♀️" # :mermaid_medium_skin_tone: - U1F9DC1F3FD200D2640 = "🧜🏽‍♀" # :mermaid_medium_skin_tone: - U1F9DC200D2642FE0F = "🧜‍♂️" # :merman: - U1F9DC200D2642 = "🧜‍♂" # :merman: - U1F9DC1F3FF200D2642FE0F = "🧜🏿‍♂️" # :merman_dark_skin_tone: - U1F9DC1F3FF200D2642 = "🧜🏿‍♂" # :merman_dark_skin_tone: - U1F9DC1F3FB200D2642FE0F = "🧜🏻‍♂️" # :merman_light_skin_tone: - U1F9DC1F3FB200D2642 = "🧜🏻‍♂" # :merman_light_skin_tone: - U1F9DC1F3FE200D2642FE0F = "🧜🏾‍♂️" # :merman_medium-dark_skin_tone: - U1F9DC1F3FE200D2642 = "🧜🏾‍♂" # :merman_medium-dark_skin_tone: - U1F9DC1F3FC200D2642FE0F = "🧜🏼‍♂️" # :merman_medium-light_skin_tone: - U1F9DC1F3FC200D2642 = "🧜🏼‍♂" # :merman_medium-light_skin_tone: - U1F9DC1F3FD200D2642FE0F = "🧜🏽‍♂️" # :merman_medium_skin_tone: - U1F9DC1F3FD200D2642 = "🧜🏽‍♂" # :merman_medium_skin_tone: - U1F9DC = "🧜" # :merperson: - U1F9DC1F3FF = "🧜🏿" # :merperson_dark_skin_tone: - U1F9DC1F3FB = "🧜🏻" # :merperson_light_skin_tone: - U1F9DC1F3FE = "🧜🏾" # :merperson_medium-dark_skin_tone: - U1F9DC1F3FC = "🧜🏼" # :merperson_medium-light_skin_tone: - U1F9DC1F3FD = "🧜🏽" # :merperson_medium_skin_tone: - U1F687 = "🚇" # :metro: - U1F9A0 = "🦠" # :microbe: - U1F3A4 = "🎤" # :microphone: - U1F52C = "🔬" # :microscope: - U1F595 = "🖕" # :middle_finger: - U1F5951F3FF = "🖕🏿" # :middle_finger_dark_skin_tone: - U1F5951F3FB = "🖕🏻" # :middle_finger_light_skin_tone: - U1F5951F3FE = "🖕🏾" # :middle_finger_medium-dark_skin_tone: - U1F5951F3FC = "🖕🏼" # :middle_finger_medium-light_skin_tone: - U1F5951F3FD = "🖕🏽" # :middle_finger_medium_skin_tone: - U1FA96 = "🪖" # :military_helmet: - U1F396FE0F = "🎖️" # :military_medal: - U1F396 = "🎖" # :military_medal: - U1F30C = "🌌" # :milky_way: - U1F690 = "🚐" # :minibus: - U2796 = "➖" # :minus: - U1FA9E = "🪞" # :mirror: - U1FAA9 = "🪩" # :mirror_ball: - U1F5FF = "🗿" # :moai: - U1F4F1 = "📱" # :mobile_phone: - U1F4F4 = "📴" # :mobile_phone_off: - U1F4F2 = "📲" # :mobile_phone_with_arrow: - U1F911 = "🤑" # :money-mouth_face: - U1F4B0 = "💰" # :money_bag: - U1F4B8 = "💸" # :money_with_wings: - U1F412 = "🐒" # :monkey: - U1F435 = "🐵" # :monkey_face: - U1F69D = "🚝" # :monorail: - U1F96E = "🥮" # :moon_cake: - U1F391 = "🎑" # :moon_viewing_ceremony: - U1FACE = "🫎" # :moose: - U1F54C = "🕌" # :mosque: - U1F99F = "🦟" # :mosquito: - U1F6E5FE0F = "🛥️" # :motor_boat: - U1F6E5 = "🛥" # :motor_boat: - U1F6F5 = "🛵" # :motor_scooter: - U1F3CDFE0F = "🏍️" # :motorcycle: - U1F3CD = "🏍" # :motorcycle: - U1F9BC = "🦼" # :motorized_wheelchair: - U1F6E3FE0F = "🛣️" # :motorway: - U1F6E3 = "🛣" # :motorway: - U1F5FB = "🗻" # :mount_fuji: - U26F0FE0F = "⛰️" # :mountain: - U26F0 = "⛰" # :mountain: - U1F6A0 = "🚠" # :mountain_cableway: - U1F69E = "🚞" # :mountain_railway: - U1F401 = "🐁" # :mouse: - U1F42D = "🐭" # :mouse_face: - U1FAA4 = "🪤" # :mouse_trap: - U1F444 = "👄" # :mouth: - U1F3A5 = "🎥" # :movie_camera: - U2716FE0F = "✖️" # :multiply: - U2716 = "✖" # :multiply: - U1F344 = "🍄" # :mushroom: - U1F3B9 = "🎹" # :musical_keyboard: - U1F3B5 = "🎵" # :musical_note: - U1F3B6 = "🎶" # :musical_notes: - U1F3BC = "🎼" # :musical_score: - U1F507 = "🔇" # :muted_speaker: - U1F9D1200D1F384 = "🧑‍🎄" # :mx_claus: - U1F9D11F3FF200D1F384 = "🧑🏿‍🎄" # :mx_claus_dark_skin_tone: - U1F9D11F3FB200D1F384 = "🧑🏻‍🎄" # :mx_claus_light_skin_tone: - U1F9D11F3FE200D1F384 = "🧑🏾‍🎄" # :mx_claus_medium-dark_skin_tone: - U1F9D11F3FC200D1F384 = "🧑🏼‍🎄" # :mx_claus_medium-light_skin_tone: - U1F9D11F3FD200D1F384 = "🧑🏽‍🎄" # :mx_claus_medium_skin_tone: - U1F485 = "💅" # :nail_polish: - U1F4851F3FF = "💅🏿" # :nail_polish_dark_skin_tone: - U1F4851F3FB = "💅🏻" # :nail_polish_light_skin_tone: - U1F4851F3FE = "💅🏾" # :nail_polish_medium-dark_skin_tone: - U1F4851F3FC = "💅🏼" # :nail_polish_medium-light_skin_tone: - U1F4851F3FD = "💅🏽" # :nail_polish_medium_skin_tone: - U1F4DB = "📛" # :name_badge: - U1F3DEFE0F = "🏞️" # :national_park: - U1F3DE = "🏞" # :national_park: - U1F922 = "🤢" # :nauseated_face: - U1F9FF = "🧿" # :nazar_amulet: - U1F454 = "👔" # :necktie: - U1F913 = "🤓" # :nerd_face: - U1FABA = "🪺" # :nest_with_eggs: - U1FA86 = "🪆" # :nesting_dolls: - U1F610 = "😐" # :neutral_face: - U1F311 = "🌑" # :new_moon: - U1F31A = "🌚" # :new_moon_face: - U1F4F0 = "📰" # :newspaper: - U23EDFE0F = "⏭️" # :next_track_button: - U23ED = "⏭" # :next_track_button: - U1F303 = "🌃" # :night_with_stars: - U1F564 = "🕤" # :nine-thirty: - U1F558 = "🕘" # :nine_o’clock: - U1F977 = "🥷" # :ninja: - U1F9771F3FF = "🥷🏿" # :ninja_dark_skin_tone: - U1F9771F3FB = "🥷🏻" # :ninja_light_skin_tone: - U1F9771F3FE = "🥷🏾" # :ninja_medium-dark_skin_tone: - U1F9771F3FC = "🥷🏼" # :ninja_medium-light_skin_tone: - U1F9771F3FD = "🥷🏽" # :ninja_medium_skin_tone: - U1F6B3 = "🚳" # :no_bicycles: - U26D4 = "⛔" # :no_entry: - U1F6AF = "🚯" # :no_littering: - U1F4F5 = "📵" # :no_mobile_phones: - U1F51E = "🔞" # :no_one_under_eighteen: - U1F6B7 = "🚷" # :no_pedestrians: - U1F6AD = "🚭" # :no_smoking: - U1F6B1 = "🚱" # :non-potable_water: - U1F443 = "👃" # :nose: - U1F4431F3FF = "👃🏿" # :nose_dark_skin_tone: - U1F4431F3FB = "👃🏻" # :nose_light_skin_tone: - U1F4431F3FE = "👃🏾" # :nose_medium-dark_skin_tone: - U1F4431F3FC = "👃🏼" # :nose_medium-light_skin_tone: - U1F4431F3FD = "👃🏽" # :nose_medium_skin_tone: - U1F4D3 = "📓" # :notebook: - U1F4D4 = "📔" # :notebook_with_decorative_cover: - U1F529 = "🔩" # :nut_and_bolt: - U1F419 = "🐙" # :octopus: - U1F362 = "🍢" # :oden: - U1F3E2 = "🏢" # :office_building: - U1F9D1200D1F4BC = "🧑‍💼" # :office_worker: - U1F9D11F3FF200D1F4BC = "🧑🏿‍💼" # :office_worker_dark_skin_tone: - U1F9D11F3FB200D1F4BC = "🧑🏻‍💼" # :office_worker_light_skin_tone: - U1F9D11F3FE200D1F4BC = "🧑🏾‍💼" # :office_worker_medium-dark_skin_tone: - U1F9D11F3FC200D1F4BC = "🧑🏼‍💼" # :office_worker_medium-light_skin_tone: - U1F9D11F3FD200D1F4BC = "🧑🏽‍💼" # :office_worker_medium_skin_tone: - U1F479 = "👹" # :ogre: - U1F6E2FE0F = "🛢️" # :oil_drum: - U1F6E2 = "🛢" # :oil_drum: - U1F5DDFE0F = "🗝️" # :old_key: - U1F5DD = "🗝" # :old_key: - U1F474 = "👴" # :old_man: - U1F4741F3FF = "👴🏿" # :old_man_dark_skin_tone: - U1F4741F3FB = "👴🏻" # :old_man_light_skin_tone: - U1F4741F3FE = "👴🏾" # :old_man_medium-dark_skin_tone: - U1F4741F3FC = "👴🏼" # :old_man_medium-light_skin_tone: - U1F4741F3FD = "👴🏽" # :old_man_medium_skin_tone: - U1F475 = "👵" # :old_woman: - U1F4751F3FF = "👵🏿" # :old_woman_dark_skin_tone: - U1F4751F3FB = "👵🏻" # :old_woman_light_skin_tone: - U1F4751F3FE = "👵🏾" # :old_woman_medium-dark_skin_tone: - U1F4751F3FC = "👵🏼" # :old_woman_medium-light_skin_tone: - U1F4751F3FD = "👵🏽" # :old_woman_medium_skin_tone: - U1F9D3 = "🧓" # :older_person: - U1F9D31F3FF = "🧓🏿" # :older_person_dark_skin_tone: - U1F9D31F3FB = "🧓🏻" # :older_person_light_skin_tone: - U1F9D31F3FE = "🧓🏾" # :older_person_medium-dark_skin_tone: - U1F9D31F3FC = "🧓🏼" # :older_person_medium-light_skin_tone: - U1F9D31F3FD = "🧓🏽" # :older_person_medium_skin_tone: - U1FAD2 = "🫒" # :olive: - U1F549FE0F = "🕉️" # :om: - U1F549 = "🕉" # :om: - U1F698 = "🚘" # :oncoming_automobile: - U1F68D = "🚍" # :oncoming_bus: - U1F44A = "👊" # :oncoming_fist: - U1F44A1F3FF = "👊🏿" # :oncoming_fist_dark_skin_tone: - U1F44A1F3FB = "👊🏻" # :oncoming_fist_light_skin_tone: - U1F44A1F3FE = "👊🏾" # :oncoming_fist_medium-dark_skin_tone: - U1F44A1F3FC = "👊🏼" # :oncoming_fist_medium-light_skin_tone: - U1F44A1F3FD = "👊🏽" # :oncoming_fist_medium_skin_tone: - U1F694 = "🚔" # :oncoming_police_car: - U1F696 = "🚖" # :oncoming_taxi: - U1FA71 = "🩱" # :one-piece_swimsuit: - U1F55C = "🕜" # :one-thirty: - U1F550 = "🕐" # :one_o’clock: - U1F9C5 = "🧅" # :onion: - U1F4D6 = "📖" # :open_book: - U1F4C2 = "📂" # :open_file_folder: - U1F450 = "👐" # :open_hands: - U1F4501F3FF = "👐🏿" # :open_hands_dark_skin_tone: - U1F4501F3FB = "👐🏻" # :open_hands_light_skin_tone: - U1F4501F3FE = "👐🏾" # :open_hands_medium-dark_skin_tone: - U1F4501F3FC = "👐🏼" # :open_hands_medium-light_skin_tone: - U1F4501F3FD = "👐🏽" # :open_hands_medium_skin_tone: - U1F4ED = "📭" # :open_mailbox_with_lowered_flag: - U1F4EC = "📬" # :open_mailbox_with_raised_flag: - U1F4BF = "💿" # :optical_disk: - U1F4D9 = "📙" # :orange_book: - U1F7E0 = "🟠" # :orange_circle: - U1F9E1 = "🧡" # :orange_heart: - U1F7E7 = "🟧" # :orange_square: - U1F9A7 = "🦧" # :orangutan: - U2626FE0F = "☦️" # :orthodox_cross: - U2626 = "☦" # :orthodox_cross: - U1F9A6 = "🦦" # :otter: - U1F4E4 = "📤" # :outbox_tray: - U1F989 = "🦉" # :owl: - U1F402 = "🐂" # :ox: - U1F9AA = "🦪" # :oyster: - U1F4E6 = "📦" # :package: - U1F4C4 = "📄" # :page_facing_up: - U1F4C3 = "📃" # :page_with_curl: - U1F4DF = "📟" # :pager: - U1F58CFE0F = "🖌️" # :paintbrush: - U1F58C = "🖌" # :paintbrush: - U1FAF3 = "🫳" # :palm_down_hand: - U1FAF31F3FF = "🫳🏿" # :palm_down_hand_dark_skin_tone: - U1FAF31F3FB = "🫳🏻" # :palm_down_hand_light_skin_tone: - U1FAF31F3FE = "🫳🏾" # :palm_down_hand_medium-dark_skin_tone: - U1FAF31F3FC = "🫳🏼" # :palm_down_hand_medium-light_skin_tone: - U1FAF31F3FD = "🫳🏽" # :palm_down_hand_medium_skin_tone: - U1F334 = "🌴" # :palm_tree: - U1FAF4 = "🫴" # :palm_up_hand: - U1FAF41F3FF = "🫴🏿" # :palm_up_hand_dark_skin_tone: - U1FAF41F3FB = "🫴🏻" # :palm_up_hand_light_skin_tone: - U1FAF41F3FE = "🫴🏾" # :palm_up_hand_medium-dark_skin_tone: - U1FAF41F3FC = "🫴🏼" # :palm_up_hand_medium-light_skin_tone: - U1FAF41F3FD = "🫴🏽" # :palm_up_hand_medium_skin_tone: - U1F932 = "🤲" # :palms_up_together: - U1F9321F3FF = "🤲🏿" # :palms_up_together_dark_skin_tone: - U1F9321F3FB = "🤲🏻" # :palms_up_together_light_skin_tone: - U1F9321F3FE = "🤲🏾" # :palms_up_together_medium-dark_skin_tone: - U1F9321F3FC = "🤲🏼" # :palms_up_together_medium-light_skin_tone: - U1F9321F3FD = "🤲🏽" # :palms_up_together_medium_skin_tone: - U1F95E = "🥞" # :pancakes: - U1F43C = "🐼" # :panda: - U1F4CE = "📎" # :paperclip: - U1FA82 = "🪂" # :parachute: - U1F99C = "🦜" # :parrot: - U303DFE0F = "〽️" # :part_alternation_mark: - U303D = "〽" # :part_alternation_mark: - U1F389 = "🎉" # :party_popper: - U1F973 = "🥳" # :partying_face: - U1F6F3FE0F = "🛳️" # :passenger_ship: - U1F6F3 = "🛳" # :passenger_ship: - U1F6C2 = "🛂" # :passport_control: - U23F8FE0F = "⏸️" # :pause_button: - U23F8 = "⏸" # :pause_button: - U1F43E = "🐾" # :paw_prints: - U1FADB = "🫛" # :pea_pod: - U262EFE0F = "☮️" # :peace_symbol: - U262E = "☮" # :peace_symbol: - U1F351 = "🍑" # :peach: - U1F99A = "🦚" # :peacock: - U1F95C = "🥜" # :peanuts: - U1F350 = "🍐" # :pear: - U1F58AFE0F = "🖊️" # :pen: - U1F58A = "🖊" # :pen: - U270FFE0F = "✏️" # :pencil: - U270F = "✏" # :pencil: - U1F427 = "🐧" # :penguin: - U1F614 = "😔" # :pensive_face: - U1F9D1200D1F91D200D1F9D1 = "🧑‍🤝‍🧑" # :people_holding_hands: - U1F9D11F3FF200D1F91D200D1F9D11F3FF = "🧑🏿‍🤝‍🧑🏿" # :people_holding_hands_dark_skin_tone: - U1F9D11F3FF200D1F91D200D1F9D11F3FB = "🧑🏿‍🤝‍🧑🏻" # :people_holding_hands_dark_skin_tone_light_skin_tone: - U1F9D11F3FF200D1F91D200D1F9D11F3FE = "🧑🏿‍🤝‍🧑🏾" # :people_holding_hands_dark_skin_tone_medium-dark_skin_tone: - U1F9D11F3FF200D1F91D200D1F9D11F3FC = "🧑🏿‍🤝‍🧑🏼" # :people_holding_hands_dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FF200D1F91D200D1F9D11F3FD = "🧑🏿‍🤝‍🧑🏽" # :people_holding_hands_dark_skin_tone_medium_skin_tone: - U1F9D11F3FB200D1F91D200D1F9D11F3FB = "🧑🏻‍🤝‍🧑🏻" # :people_holding_hands_light_skin_tone: - U1F9D11F3FB200D1F91D200D1F9D11F3FF = "🧑🏻‍🤝‍🧑🏿" # :people_holding_hands_light_skin_tone_dark_skin_tone: - U1F9D11F3FB200D1F91D200D1F9D11F3FE = "🧑🏻‍🤝‍🧑🏾" # :people_holding_hands_light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FB200D1F91D200D1F9D11F3FC = "🧑🏻‍🤝‍🧑🏼" # :people_holding_hands_light_skin_tone_medium-light_skin_tone: - U1F9D11F3FB200D1F91D200D1F9D11F3FD = "🧑🏻‍🤝‍🧑🏽" # :people_holding_hands_light_skin_tone_medium_skin_tone: - U1F9D11F3FE200D1F91D200D1F9D11F3FE = "🧑🏾‍🤝‍🧑🏾" # :people_holding_hands_medium-dark_skin_tone: - U1F9D11F3FE200D1F91D200D1F9D11F3FF = "🧑🏾‍🤝‍🧑🏿" # :people_holding_hands_medium-dark_skin_tone_dark_skin_tone: - U1F9D11F3FE200D1F91D200D1F9D11F3FB = "🧑🏾‍🤝‍🧑🏻" # :people_holding_hands_medium-dark_skin_tone_light_skin_tone: - U1F9D11F3FE200D1F91D200D1F9D11F3FC = "🧑🏾‍🤝‍🧑🏼" # :people_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: - U1F9D11F3FE200D1F91D200D1F9D11F3FD = "🧑🏾‍🤝‍🧑🏽" # :people_holding_hands_medium-dark_skin_tone_medium_skin_tone: - U1F9D11F3FC200D1F91D200D1F9D11F3FC = "🧑🏼‍🤝‍🧑🏼" # :people_holding_hands_medium-light_skin_tone: - U1F9D11F3FC200D1F91D200D1F9D11F3FF = "🧑🏼‍🤝‍🧑🏿" # :people_holding_hands_medium-light_skin_tone_dark_skin_tone: - U1F9D11F3FC200D1F91D200D1F9D11F3FB = "🧑🏼‍🤝‍🧑🏻" # :people_holding_hands_medium-light_skin_tone_light_skin_tone: - U1F9D11F3FC200D1F91D200D1F9D11F3FE = "🧑🏼‍🤝‍🧑🏾" # :people_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: - U1F9D11F3FC200D1F91D200D1F9D11F3FD = "🧑🏼‍🤝‍🧑🏽" # :people_holding_hands_medium-light_skin_tone_medium_skin_tone: - U1F9D11F3FD200D1F91D200D1F9D11F3FD = "🧑🏽‍🤝‍🧑🏽" # :people_holding_hands_medium_skin_tone: - U1F9D11F3FD200D1F91D200D1F9D11F3FF = "🧑🏽‍🤝‍🧑🏿" # :people_holding_hands_medium_skin_tone_dark_skin_tone: - U1F9D11F3FD200D1F91D200D1F9D11F3FB = "🧑🏽‍🤝‍🧑🏻" # :people_holding_hands_medium_skin_tone_light_skin_tone: - U1F9D11F3FD200D1F91D200D1F9D11F3FE = "🧑🏽‍🤝‍🧑🏾" # :people_holding_hands_medium_skin_tone_medium-dark_skin_tone: - U1F9D11F3FD200D1F91D200D1F9D11F3FC = "🧑🏽‍🤝‍🧑🏼" # :people_holding_hands_medium_skin_tone_medium-light_skin_tone: - U1FAC2 = "🫂" # :people_hugging: - U1F46F = "👯" # :people_with_bunny_ears: - U1F93C = "🤼" # :people_wrestling: - U1F3AD = "🎭" # :performing_arts: - U1F623 = "😣" # :persevering_face: - U1F9D1 = "🧑" # :person: - U1F9D1200D1F9B2 = "🧑‍🦲" # :person_bald: - U1F9D4 = "🧔" # :person_beard: - U1F6B4 = "🚴" # :person_biking: - U1F6B41F3FF = "🚴🏿" # :person_biking_dark_skin_tone: - U1F6B41F3FB = "🚴🏻" # :person_biking_light_skin_tone: - U1F6B41F3FE = "🚴🏾" # :person_biking_medium-dark_skin_tone: - U1F6B41F3FC = "🚴🏼" # :person_biking_medium-light_skin_tone: - U1F6B41F3FD = "🚴🏽" # :person_biking_medium_skin_tone: - U1F471 = "👱" # :person_blond_hair: - U26F9FE0F = "⛹️" # :person_bouncing_ball: - U26F9 = "⛹" # :person_bouncing_ball: - U26F91F3FF = "⛹🏿" # :person_bouncing_ball_dark_skin_tone: - U26F91F3FB = "⛹🏻" # :person_bouncing_ball_light_skin_tone: - U26F91F3FE = "⛹🏾" # :person_bouncing_ball_medium-dark_skin_tone: - U26F91F3FC = "⛹🏼" # :person_bouncing_ball_medium-light_skin_tone: - U26F91F3FD = "⛹🏽" # :person_bouncing_ball_medium_skin_tone: - U1F647 = "🙇" # :person_bowing: - U1F6471F3FF = "🙇🏿" # :person_bowing_dark_skin_tone: - U1F6471F3FB = "🙇🏻" # :person_bowing_light_skin_tone: - U1F6471F3FE = "🙇🏾" # :person_bowing_medium-dark_skin_tone: - U1F6471F3FC = "🙇🏼" # :person_bowing_medium-light_skin_tone: - U1F6471F3FD = "🙇🏽" # :person_bowing_medium_skin_tone: - U1F938 = "🤸" # :person_cartwheeling: - U1F9381F3FF = "🤸🏿" # :person_cartwheeling_dark_skin_tone: - U1F9381F3FB = "🤸🏻" # :person_cartwheeling_light_skin_tone: - U1F9381F3FE = "🤸🏾" # :person_cartwheeling_medium-dark_skin_tone: - U1F9381F3FC = "🤸🏼" # :person_cartwheeling_medium-light_skin_tone: - U1F9381F3FD = "🤸🏽" # :person_cartwheeling_medium_skin_tone: - U1F9D7 = "🧗" # :person_climbing: - U1F9D71F3FF = "🧗🏿" # :person_climbing_dark_skin_tone: - U1F9D71F3FB = "🧗🏻" # :person_climbing_light_skin_tone: - U1F9D71F3FE = "🧗🏾" # :person_climbing_medium-dark_skin_tone: - U1F9D71F3FC = "🧗🏼" # :person_climbing_medium-light_skin_tone: - U1F9D71F3FD = "🧗🏽" # :person_climbing_medium_skin_tone: - U1F9D1200D1F9B1 = "🧑‍🦱" # :person_curly_hair: - U1F9D11F3FF = "🧑🏿" # :person_dark_skin_tone: - U1F9D11F3FF200D1F9B2 = "🧑🏿‍🦲" # :person_dark_skin_tone_bald: - U1F9D41F3FF = "🧔🏿" # :person_dark_skin_tone_beard: - U1F4711F3FF = "👱🏿" # :person_dark_skin_tone_blond_hair: - U1F9D11F3FF200D1F9B1 = "🧑🏿‍🦱" # :person_dark_skin_tone_curly_hair: - U1F9D11F3FF200D1F9B0 = "🧑🏿‍🦰" # :person_dark_skin_tone_red_hair: - U1F9D11F3FF200D1F9B3 = "🧑🏿‍🦳" # :person_dark_skin_tone_white_hair: - U1F926 = "🤦" # :person_facepalming: - U1F9261F3FF = "🤦🏿" # :person_facepalming_dark_skin_tone: - U1F9261F3FB = "🤦🏻" # :person_facepalming_light_skin_tone: - U1F9261F3FE = "🤦🏾" # :person_facepalming_medium-dark_skin_tone: - U1F9261F3FC = "🤦🏼" # :person_facepalming_medium-light_skin_tone: - U1F9261F3FD = "🤦🏽" # :person_facepalming_medium_skin_tone: - U1F9D1200D1F37C = "🧑‍🍼" # :person_feeding_baby: - U1F9D11F3FF200D1F37C = "🧑🏿‍🍼" # :person_feeding_baby_dark_skin_tone: - U1F9D11F3FB200D1F37C = "🧑🏻‍🍼" # :person_feeding_baby_light_skin_tone: - U1F9D11F3FE200D1F37C = "🧑🏾‍🍼" # :person_feeding_baby_medium-dark_skin_tone: - U1F9D11F3FC200D1F37C = "🧑🏼‍🍼" # :person_feeding_baby_medium-light_skin_tone: - U1F9D11F3FD200D1F37C = "🧑🏽‍🍼" # :person_feeding_baby_medium_skin_tone: - U1F93A = "🤺" # :person_fencing: - U1F64D = "🙍" # :person_frowning: - U1F64D1F3FF = "🙍🏿" # :person_frowning_dark_skin_tone: - U1F64D1F3FB = "🙍🏻" # :person_frowning_light_skin_tone: - U1F64D1F3FE = "🙍🏾" # :person_frowning_medium-dark_skin_tone: - U1F64D1F3FC = "🙍🏼" # :person_frowning_medium-light_skin_tone: - U1F64D1F3FD = "🙍🏽" # :person_frowning_medium_skin_tone: - U1F645 = "🙅" # :person_gesturing_NO: - U1F6451F3FF = "🙅🏿" # :person_gesturing_NO_dark_skin_tone: - U1F6451F3FB = "🙅🏻" # :person_gesturing_NO_light_skin_tone: - U1F6451F3FE = "🙅🏾" # :person_gesturing_NO_medium-dark_skin_tone: - U1F6451F3FC = "🙅🏼" # :person_gesturing_NO_medium-light_skin_tone: - U1F6451F3FD = "🙅🏽" # :person_gesturing_NO_medium_skin_tone: - U1F646 = "🙆" # :person_gesturing_OK: - U1F6461F3FF = "🙆🏿" # :person_gesturing_OK_dark_skin_tone: - U1F6461F3FB = "🙆🏻" # :person_gesturing_OK_light_skin_tone: - U1F6461F3FE = "🙆🏾" # :person_gesturing_OK_medium-dark_skin_tone: - U1F6461F3FC = "🙆🏼" # :person_gesturing_OK_medium-light_skin_tone: - U1F6461F3FD = "🙆🏽" # :person_gesturing_OK_medium_skin_tone: - U1F487 = "💇" # :person_getting_haircut: - U1F4871F3FF = "💇🏿" # :person_getting_haircut_dark_skin_tone: - U1F4871F3FB = "💇🏻" # :person_getting_haircut_light_skin_tone: - U1F4871F3FE = "💇🏾" # :person_getting_haircut_medium-dark_skin_tone: - U1F4871F3FC = "💇🏼" # :person_getting_haircut_medium-light_skin_tone: - U1F4871F3FD = "💇🏽" # :person_getting_haircut_medium_skin_tone: - U1F486 = "💆" # :person_getting_massage: - U1F4861F3FF = "💆🏿" # :person_getting_massage_dark_skin_tone: - U1F4861F3FB = "💆🏻" # :person_getting_massage_light_skin_tone: - U1F4861F3FE = "💆🏾" # :person_getting_massage_medium-dark_skin_tone: - U1F4861F3FC = "💆🏼" # :person_getting_massage_medium-light_skin_tone: - U1F4861F3FD = "💆🏽" # :person_getting_massage_medium_skin_tone: - U1F3CCFE0F = "🏌️" # :person_golfing: - U1F3CC = "🏌" # :person_golfing: - U1F3CC1F3FF = "🏌🏿" # :person_golfing_dark_skin_tone: - U1F3CC1F3FB = "🏌🏻" # :person_golfing_light_skin_tone: - U1F3CC1F3FE = "🏌🏾" # :person_golfing_medium-dark_skin_tone: - U1F3CC1F3FC = "🏌🏼" # :person_golfing_medium-light_skin_tone: - U1F3CC1F3FD = "🏌🏽" # :person_golfing_medium_skin_tone: - U1F6CC = "🛌" # :person_in_bed: - U1F6CC1F3FF = "🛌🏿" # :person_in_bed_dark_skin_tone: - U1F6CC1F3FB = "🛌🏻" # :person_in_bed_light_skin_tone: - U1F6CC1F3FE = "🛌🏾" # :person_in_bed_medium-dark_skin_tone: - U1F6CC1F3FC = "🛌🏼" # :person_in_bed_medium-light_skin_tone: - U1F6CC1F3FD = "🛌🏽" # :person_in_bed_medium_skin_tone: - U1F9D8 = "🧘" # :person_in_lotus_position: - U1F9D81F3FF = "🧘🏿" # :person_in_lotus_position_dark_skin_tone: - U1F9D81F3FB = "🧘🏻" # :person_in_lotus_position_light_skin_tone: - U1F9D81F3FE = "🧘🏾" # :person_in_lotus_position_medium-dark_skin_tone: - U1F9D81F3FC = "🧘🏼" # :person_in_lotus_position_medium-light_skin_tone: - U1F9D81F3FD = "🧘🏽" # :person_in_lotus_position_medium_skin_tone: - U1F9D1200D1F9BD = "🧑‍🦽" # :person_in_manual_wheelchair: - U1F9D11F3FF200D1F9BD = "🧑🏿‍🦽" # :person_in_manual_wheelchair_dark_skin_tone: - U1F9D1200D1F9BD200D27A1FE0F = "🧑‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right: - U1F9D1200D1F9BD200D27A1 = "🧑‍🦽‍➡" # :person_in_manual_wheelchair_facing_right: - U1F9D11F3FF200D1F9BD200D27A1FE0F = "🧑🏿‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F9D11F3FF200D1F9BD200D27A1 = "🧑🏿‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F9D11F3FB200D1F9BD200D27A1FE0F = "🧑🏻‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_light_skin_tone: - U1F9D11F3FB200D1F9BD200D27A1 = "🧑🏻‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_light_skin_tone: - U1F9D11F3FE200D1F9BD200D27A1FE0F = "🧑🏾‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F9D11F3FE200D1F9BD200D27A1 = "🧑🏾‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F9D11F3FC200D1F9BD200D27A1FE0F = "🧑🏼‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F9D11F3FC200D1F9BD200D27A1 = "🧑🏼‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F9D11F3FD200D1F9BD200D27A1FE0F = "🧑🏽‍🦽‍➡️" # :person_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F9D11F3FD200D1F9BD200D27A1 = "🧑🏽‍🦽‍➡" # :person_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F9D11F3FB200D1F9BD = "🧑🏻‍🦽" # :person_in_manual_wheelchair_light_skin_tone: - U1F9D11F3FE200D1F9BD = "🧑🏾‍🦽" # :person_in_manual_wheelchair_medium-dark_skin_tone: - U1F9D11F3FC200D1F9BD = "🧑🏼‍🦽" # :person_in_manual_wheelchair_medium-light_skin_tone: - U1F9D11F3FD200D1F9BD = "🧑🏽‍🦽" # :person_in_manual_wheelchair_medium_skin_tone: - U1F9D1200D1F9BC = "🧑‍🦼" # :person_in_motorized_wheelchair: - U1F9D11F3FF200D1F9BC = "🧑🏿‍🦼" # :person_in_motorized_wheelchair_dark_skin_tone: - U1F9D1200D1F9BC200D27A1FE0F = "🧑‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right: - U1F9D1200D1F9BC200D27A1 = "🧑‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right: - U1F9D11F3FF200D1F9BC200D27A1FE0F = "🧑🏿‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F9D11F3FF200D1F9BC200D27A1 = "🧑🏿‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F9D11F3FB200D1F9BC200D27A1FE0F = "🧑🏻‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F9D11F3FB200D1F9BC200D27A1 = "🧑🏻‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F9D11F3FE200D1F9BC200D27A1FE0F = "🧑🏾‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F9D11F3FE200D1F9BC200D27A1 = "🧑🏾‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F9D11F3FC200D1F9BC200D27A1FE0F = "🧑🏼‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F9D11F3FC200D1F9BC200D27A1 = "🧑🏼‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F9D11F3FD200D1F9BC200D27A1FE0F = "🧑🏽‍🦼‍➡️" # :person_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F9D11F3FD200D1F9BC200D27A1 = "🧑🏽‍🦼‍➡" # :person_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F9D11F3FB200D1F9BC = "🧑🏻‍🦼" # :person_in_motorized_wheelchair_light_skin_tone: - U1F9D11F3FE200D1F9BC = "🧑🏾‍🦼" # :person_in_motorized_wheelchair_medium-dark_skin_tone: - U1F9D11F3FC200D1F9BC = "🧑🏼‍🦼" # :person_in_motorized_wheelchair_medium-light_skin_tone: - U1F9D11F3FD200D1F9BC = "🧑🏽‍🦼" # :person_in_motorized_wheelchair_medium_skin_tone: - U1F9D6 = "🧖" # :person_in_steamy_room: - U1F9D61F3FF = "🧖🏿" # :person_in_steamy_room_dark_skin_tone: - U1F9D61F3FB = "🧖🏻" # :person_in_steamy_room_light_skin_tone: - U1F9D61F3FE = "🧖🏾" # :person_in_steamy_room_medium-dark_skin_tone: - U1F9D61F3FC = "🧖🏼" # :person_in_steamy_room_medium-light_skin_tone: - U1F9D61F3FD = "🧖🏽" # :person_in_steamy_room_medium_skin_tone: - U1F574FE0F = "🕴️" # :person_in_suit_levitating: - U1F574 = "🕴" # :person_in_suit_levitating: - U1F5741F3FF = "🕴🏿" # :person_in_suit_levitating_dark_skin_tone: - U1F5741F3FB = "🕴🏻" # :person_in_suit_levitating_light_skin_tone: - U1F5741F3FE = "🕴🏾" # :person_in_suit_levitating_medium-dark_skin_tone: - U1F5741F3FC = "🕴🏼" # :person_in_suit_levitating_medium-light_skin_tone: - U1F5741F3FD = "🕴🏽" # :person_in_suit_levitating_medium_skin_tone: - U1F935 = "🤵" # :person_in_tuxedo: - U1F9351F3FF = "🤵🏿" # :person_in_tuxedo_dark_skin_tone: - U1F9351F3FB = "🤵🏻" # :person_in_tuxedo_light_skin_tone: - U1F9351F3FE = "🤵🏾" # :person_in_tuxedo_medium-dark_skin_tone: - U1F9351F3FC = "🤵🏼" # :person_in_tuxedo_medium-light_skin_tone: - U1F9351F3FD = "🤵🏽" # :person_in_tuxedo_medium_skin_tone: - U1F939 = "🤹" # :person_juggling: - U1F9391F3FF = "🤹🏿" # :person_juggling_dark_skin_tone: - U1F9391F3FB = "🤹🏻" # :person_juggling_light_skin_tone: - U1F9391F3FE = "🤹🏾" # :person_juggling_medium-dark_skin_tone: - U1F9391F3FC = "🤹🏼" # :person_juggling_medium-light_skin_tone: - U1F9391F3FD = "🤹🏽" # :person_juggling_medium_skin_tone: - U1F9CE = "🧎" # :person_kneeling: - U1F9CE1F3FF = "🧎🏿" # :person_kneeling_dark_skin_tone: - U1F9CE200D27A1FE0F = "🧎‍➡️" # :person_kneeling_facing_right: - U1F9CE200D27A1 = "🧎‍➡" # :person_kneeling_facing_right: - U1F9CE1F3FF200D27A1FE0F = "🧎🏿‍➡️" # :person_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D27A1 = "🧎🏿‍➡" # :person_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FB200D27A1FE0F = "🧎🏻‍➡️" # :person_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D27A1 = "🧎🏻‍➡" # :person_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FE200D27A1FE0F = "🧎🏾‍➡️" # :person_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D27A1 = "🧎🏾‍➡" # :person_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FC200D27A1FE0F = "🧎🏼‍➡️" # :person_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D27A1 = "🧎🏼‍➡" # :person_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FD200D27A1FE0F = "🧎🏽‍➡️" # :person_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D27A1 = "🧎🏽‍➡" # :person_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FB = "🧎🏻" # :person_kneeling_light_skin_tone: - U1F9CE1F3FE = "🧎🏾" # :person_kneeling_medium-dark_skin_tone: - U1F9CE1F3FC = "🧎🏼" # :person_kneeling_medium-light_skin_tone: - U1F9CE1F3FD = "🧎🏽" # :person_kneeling_medium_skin_tone: - U1F3CBFE0F = "🏋️" # :person_lifting_weights: - U1F3CB = "🏋" # :person_lifting_weights: - U1F3CB1F3FF = "🏋🏿" # :person_lifting_weights_dark_skin_tone: - U1F3CB1F3FB = "🏋🏻" # :person_lifting_weights_light_skin_tone: - U1F3CB1F3FE = "🏋🏾" # :person_lifting_weights_medium-dark_skin_tone: - U1F3CB1F3FC = "🏋🏼" # :person_lifting_weights_medium-light_skin_tone: - U1F3CB1F3FD = "🏋🏽" # :person_lifting_weights_medium_skin_tone: - U1F9D11F3FB = "🧑🏻" # :person_light_skin_tone: - U1F9D11F3FB200D1F9B2 = "🧑🏻‍🦲" # :person_light_skin_tone_bald: - U1F9D41F3FB = "🧔🏻" # :person_light_skin_tone_beard: - U1F4711F3FB = "👱🏻" # :person_light_skin_tone_blond_hair: - U1F9D11F3FB200D1F9B1 = "🧑🏻‍🦱" # :person_light_skin_tone_curly_hair: - U1F9D11F3FB200D1F9B0 = "🧑🏻‍🦰" # :person_light_skin_tone_red_hair: - U1F9D11F3FB200D1F9B3 = "🧑🏻‍🦳" # :person_light_skin_tone_white_hair: - U1F9D11F3FE = "🧑🏾" # :person_medium-dark_skin_tone: - U1F9D11F3FE200D1F9B2 = "🧑🏾‍🦲" # :person_medium-dark_skin_tone_bald: - U1F9D41F3FE = "🧔🏾" # :person_medium-dark_skin_tone_beard: - U1F4711F3FE = "👱🏾" # :person_medium-dark_skin_tone_blond_hair: - U1F9D11F3FE200D1F9B1 = "🧑🏾‍🦱" # :person_medium-dark_skin_tone_curly_hair: - U1F9D11F3FE200D1F9B0 = "🧑🏾‍🦰" # :person_medium-dark_skin_tone_red_hair: - U1F9D11F3FE200D1F9B3 = "🧑🏾‍🦳" # :person_medium-dark_skin_tone_white_hair: - U1F9D11F3FC = "🧑🏼" # :person_medium-light_skin_tone: - U1F9D11F3FC200D1F9B2 = "🧑🏼‍🦲" # :person_medium-light_skin_tone_bald: - U1F9D41F3FC = "🧔🏼" # :person_medium-light_skin_tone_beard: - U1F4711F3FC = "👱🏼" # :person_medium-light_skin_tone_blond_hair: - U1F9D11F3FC200D1F9B1 = "🧑🏼‍🦱" # :person_medium-light_skin_tone_curly_hair: - U1F9D11F3FC200D1F9B0 = "🧑🏼‍🦰" # :person_medium-light_skin_tone_red_hair: - U1F9D11F3FC200D1F9B3 = "🧑🏼‍🦳" # :person_medium-light_skin_tone_white_hair: - U1F9D11F3FD = "🧑🏽" # :person_medium_skin_tone: - U1F9D11F3FD200D1F9B2 = "🧑🏽‍🦲" # :person_medium_skin_tone_bald: - U1F9D41F3FD = "🧔🏽" # :person_medium_skin_tone_beard: - U1F4711F3FD = "👱🏽" # :person_medium_skin_tone_blond_hair: - U1F9D11F3FD200D1F9B1 = "🧑🏽‍🦱" # :person_medium_skin_tone_curly_hair: - U1F9D11F3FD200D1F9B0 = "🧑🏽‍🦰" # :person_medium_skin_tone_red_hair: - U1F9D11F3FD200D1F9B3 = "🧑🏽‍🦳" # :person_medium_skin_tone_white_hair: - U1F6B5 = "🚵" # :person_mountain_biking: - U1F6B51F3FF = "🚵🏿" # :person_mountain_biking_dark_skin_tone: - U1F6B51F3FB = "🚵🏻" # :person_mountain_biking_light_skin_tone: - U1F6B51F3FE = "🚵🏾" # :person_mountain_biking_medium-dark_skin_tone: - U1F6B51F3FC = "🚵🏼" # :person_mountain_biking_medium-light_skin_tone: - U1F6B51F3FD = "🚵🏽" # :person_mountain_biking_medium_skin_tone: - U1F93E = "🤾" # :person_playing_handball: - U1F93E1F3FF = "🤾🏿" # :person_playing_handball_dark_skin_tone: - U1F93E1F3FB = "🤾🏻" # :person_playing_handball_light_skin_tone: - U1F93E1F3FE = "🤾🏾" # :person_playing_handball_medium-dark_skin_tone: - U1F93E1F3FC = "🤾🏼" # :person_playing_handball_medium-light_skin_tone: - U1F93E1F3FD = "🤾🏽" # :person_playing_handball_medium_skin_tone: - U1F93D = "🤽" # :person_playing_water_polo: - U1F93D1F3FF = "🤽🏿" # :person_playing_water_polo_dark_skin_tone: - U1F93D1F3FB = "🤽🏻" # :person_playing_water_polo_light_skin_tone: - U1F93D1F3FE = "🤽🏾" # :person_playing_water_polo_medium-dark_skin_tone: - U1F93D1F3FC = "🤽🏼" # :person_playing_water_polo_medium-light_skin_tone: - U1F93D1F3FD = "🤽🏽" # :person_playing_water_polo_medium_skin_tone: - U1F64E = "🙎" # :person_pouting: - U1F64E1F3FF = "🙎🏿" # :person_pouting_dark_skin_tone: - U1F64E1F3FB = "🙎🏻" # :person_pouting_light_skin_tone: - U1F64E1F3FE = "🙎🏾" # :person_pouting_medium-dark_skin_tone: - U1F64E1F3FC = "🙎🏼" # :person_pouting_medium-light_skin_tone: - U1F64E1F3FD = "🙎🏽" # :person_pouting_medium_skin_tone: - U1F64B = "🙋" # :person_raising_hand: - U1F64B1F3FF = "🙋🏿" # :person_raising_hand_dark_skin_tone: - U1F64B1F3FB = "🙋🏻" # :person_raising_hand_light_skin_tone: - U1F64B1F3FE = "🙋🏾" # :person_raising_hand_medium-dark_skin_tone: - U1F64B1F3FC = "🙋🏼" # :person_raising_hand_medium-light_skin_tone: - U1F64B1F3FD = "🙋🏽" # :person_raising_hand_medium_skin_tone: - U1F9D1200D1F9B0 = "🧑‍🦰" # :person_red_hair: - U1F6A3 = "🚣" # :person_rowing_boat: - U1F6A31F3FF = "🚣🏿" # :person_rowing_boat_dark_skin_tone: - U1F6A31F3FB = "🚣🏻" # :person_rowing_boat_light_skin_tone: - U1F6A31F3FE = "🚣🏾" # :person_rowing_boat_medium-dark_skin_tone: - U1F6A31F3FC = "🚣🏼" # :person_rowing_boat_medium-light_skin_tone: - U1F6A31F3FD = "🚣🏽" # :person_rowing_boat_medium_skin_tone: - U1F3C3 = "🏃" # :person_running: - U1F3C31F3FF = "🏃🏿" # :person_running_dark_skin_tone: - U1F3C3200D27A1FE0F = "🏃‍➡️" # :person_running_facing_right: - U1F3C3200D27A1 = "🏃‍➡" # :person_running_facing_right: - U1F3C31F3FF200D27A1FE0F = "🏃🏿‍➡️" # :person_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D27A1 = "🏃🏿‍➡" # :person_running_facing_right_dark_skin_tone: - U1F3C31F3FB200D27A1FE0F = "🏃🏻‍➡️" # :person_running_facing_right_light_skin_tone: - U1F3C31F3FB200D27A1 = "🏃🏻‍➡" # :person_running_facing_right_light_skin_tone: - U1F3C31F3FE200D27A1FE0F = "🏃🏾‍➡️" # :person_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D27A1 = "🏃🏾‍➡" # :person_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FC200D27A1FE0F = "🏃🏼‍➡️" # :person_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D27A1 = "🏃🏼‍➡" # :person_running_facing_right_medium-light_skin_tone: - U1F3C31F3FD200D27A1FE0F = "🏃🏽‍➡️" # :person_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D27A1 = "🏃🏽‍➡" # :person_running_facing_right_medium_skin_tone: - U1F3C31F3FB = "🏃🏻" # :person_running_light_skin_tone: - U1F3C31F3FE = "🏃🏾" # :person_running_medium-dark_skin_tone: - U1F3C31F3FC = "🏃🏼" # :person_running_medium-light_skin_tone: - U1F3C31F3FD = "🏃🏽" # :person_running_medium_skin_tone: - U1F937 = "🤷" # :person_shrugging: - U1F9371F3FF = "🤷🏿" # :person_shrugging_dark_skin_tone: - U1F9371F3FB = "🤷🏻" # :person_shrugging_light_skin_tone: - U1F9371F3FE = "🤷🏾" # :person_shrugging_medium-dark_skin_tone: - U1F9371F3FC = "🤷🏼" # :person_shrugging_medium-light_skin_tone: - U1F9371F3FD = "🤷🏽" # :person_shrugging_medium_skin_tone: - U1F9CD = "🧍" # :person_standing: - U1F9CD1F3FF = "🧍🏿" # :person_standing_dark_skin_tone: - U1F9CD1F3FB = "🧍🏻" # :person_standing_light_skin_tone: - U1F9CD1F3FE = "🧍🏾" # :person_standing_medium-dark_skin_tone: - U1F9CD1F3FC = "🧍🏼" # :person_standing_medium-light_skin_tone: - U1F9CD1F3FD = "🧍🏽" # :person_standing_medium_skin_tone: - U1F3C4 = "🏄" # :person_surfing: - U1F3C41F3FF = "🏄🏿" # :person_surfing_dark_skin_tone: - U1F3C41F3FB = "🏄🏻" # :person_surfing_light_skin_tone: - U1F3C41F3FE = "🏄🏾" # :person_surfing_medium-dark_skin_tone: - U1F3C41F3FC = "🏄🏼" # :person_surfing_medium-light_skin_tone: - U1F3C41F3FD = "🏄🏽" # :person_surfing_medium_skin_tone: - U1F3CA = "🏊" # :person_swimming: - U1F3CA1F3FF = "🏊🏿" # :person_swimming_dark_skin_tone: - U1F3CA1F3FB = "🏊🏻" # :person_swimming_light_skin_tone: - U1F3CA1F3FE = "🏊🏾" # :person_swimming_medium-dark_skin_tone: - U1F3CA1F3FC = "🏊🏼" # :person_swimming_medium-light_skin_tone: - U1F3CA1F3FD = "🏊🏽" # :person_swimming_medium_skin_tone: - U1F6C0 = "🛀" # :person_taking_bath: - U1F6C01F3FF = "🛀🏿" # :person_taking_bath_dark_skin_tone: - U1F6C01F3FB = "🛀🏻" # :person_taking_bath_light_skin_tone: - U1F6C01F3FE = "🛀🏾" # :person_taking_bath_medium-dark_skin_tone: - U1F6C01F3FC = "🛀🏼" # :person_taking_bath_medium-light_skin_tone: - U1F6C01F3FD = "🛀🏽" # :person_taking_bath_medium_skin_tone: - U1F481 = "💁" # :person_tipping_hand: - U1F4811F3FF = "💁🏿" # :person_tipping_hand_dark_skin_tone: - U1F4811F3FB = "💁🏻" # :person_tipping_hand_light_skin_tone: - U1F4811F3FE = "💁🏾" # :person_tipping_hand_medium-dark_skin_tone: - U1F4811F3FC = "💁🏼" # :person_tipping_hand_medium-light_skin_tone: - U1F4811F3FD = "💁🏽" # :person_tipping_hand_medium_skin_tone: - U1F6B6 = "🚶" # :person_walking: - U1F6B61F3FF = "🚶🏿" # :person_walking_dark_skin_tone: - U1F6B6200D27A1FE0F = "🚶‍➡️" # :person_walking_facing_right: - U1F6B6200D27A1 = "🚶‍➡" # :person_walking_facing_right: - U1F6B61F3FF200D27A1FE0F = "🚶🏿‍➡️" # :person_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D27A1 = "🚶🏿‍➡" # :person_walking_facing_right_dark_skin_tone: - U1F6B61F3FB200D27A1FE0F = "🚶🏻‍➡️" # :person_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D27A1 = "🚶🏻‍➡" # :person_walking_facing_right_light_skin_tone: - U1F6B61F3FE200D27A1FE0F = "🚶🏾‍➡️" # :person_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D27A1 = "🚶🏾‍➡" # :person_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FC200D27A1FE0F = "🚶🏼‍➡️" # :person_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D27A1 = "🚶🏼‍➡" # :person_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FD200D27A1FE0F = "🚶🏽‍➡️" # :person_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D27A1 = "🚶🏽‍➡" # :person_walking_facing_right_medium_skin_tone: - U1F6B61F3FB = "🚶🏻" # :person_walking_light_skin_tone: - U1F6B61F3FE = "🚶🏾" # :person_walking_medium-dark_skin_tone: - U1F6B61F3FC = "🚶🏼" # :person_walking_medium-light_skin_tone: - U1F6B61F3FD = "🚶🏽" # :person_walking_medium_skin_tone: - U1F473 = "👳" # :person_wearing_turban: - U1F4731F3FF = "👳🏿" # :person_wearing_turban_dark_skin_tone: - U1F4731F3FB = "👳🏻" # :person_wearing_turban_light_skin_tone: - U1F4731F3FE = "👳🏾" # :person_wearing_turban_medium-dark_skin_tone: - U1F4731F3FC = "👳🏼" # :person_wearing_turban_medium-light_skin_tone: - U1F4731F3FD = "👳🏽" # :person_wearing_turban_medium_skin_tone: - U1F9D1200D1F9B3 = "🧑‍🦳" # :person_white_hair: - U1FAC5 = "🫅" # :person_with_crown: - U1FAC51F3FF = "🫅🏿" # :person_with_crown_dark_skin_tone: - U1FAC51F3FB = "🫅🏻" # :person_with_crown_light_skin_tone: - U1FAC51F3FE = "🫅🏾" # :person_with_crown_medium-dark_skin_tone: - U1FAC51F3FC = "🫅🏼" # :person_with_crown_medium-light_skin_tone: - U1FAC51F3FD = "🫅🏽" # :person_with_crown_medium_skin_tone: - U1F472 = "👲" # :person_with_skullcap: - U1F4721F3FF = "👲🏿" # :person_with_skullcap_dark_skin_tone: - U1F4721F3FB = "👲🏻" # :person_with_skullcap_light_skin_tone: - U1F4721F3FE = "👲🏾" # :person_with_skullcap_medium-dark_skin_tone: - U1F4721F3FC = "👲🏼" # :person_with_skullcap_medium-light_skin_tone: - U1F4721F3FD = "👲🏽" # :person_with_skullcap_medium_skin_tone: - U1F470 = "👰" # :person_with_veil: - U1F4701F3FF = "👰🏿" # :person_with_veil_dark_skin_tone: - U1F4701F3FB = "👰🏻" # :person_with_veil_light_skin_tone: - U1F4701F3FE = "👰🏾" # :person_with_veil_medium-dark_skin_tone: - U1F4701F3FC = "👰🏼" # :person_with_veil_medium-light_skin_tone: - U1F4701F3FD = "👰🏽" # :person_with_veil_medium_skin_tone: - U1F9D1200D1F9AF = "🧑‍🦯" # :person_with_white_cane: - U1F9D11F3FF200D1F9AF = "🧑🏿‍🦯" # :person_with_white_cane_dark_skin_tone: - U1F9D1200D1F9AF200D27A1FE0F = "🧑‍🦯‍➡️" # :person_with_white_cane_facing_right: - U1F9D1200D1F9AF200D27A1 = "🧑‍🦯‍➡" # :person_with_white_cane_facing_right: - U1F9D11F3FF200D1F9AF200D27A1FE0F = "🧑🏿‍🦯‍➡️" # :person_with_white_cane_facing_right_dark_skin_tone: - U1F9D11F3FF200D1F9AF200D27A1 = "🧑🏿‍🦯‍➡" # :person_with_white_cane_facing_right_dark_skin_tone: - U1F9D11F3FB200D1F9AF200D27A1FE0F = "🧑🏻‍🦯‍➡️" # :person_with_white_cane_facing_right_light_skin_tone: - U1F9D11F3FB200D1F9AF200D27A1 = "🧑🏻‍🦯‍➡" # :person_with_white_cane_facing_right_light_skin_tone: - U1F9D11F3FE200D1F9AF200D27A1FE0F = "🧑🏾‍🦯‍➡️" # :person_with_white_cane_facing_right_medium-dark_skin_tone: - U1F9D11F3FE200D1F9AF200D27A1 = "🧑🏾‍🦯‍➡" # :person_with_white_cane_facing_right_medium-dark_skin_tone: - U1F9D11F3FC200D1F9AF200D27A1FE0F = "🧑🏼‍🦯‍➡️" # :person_with_white_cane_facing_right_medium-light_skin_tone: - U1F9D11F3FC200D1F9AF200D27A1 = "🧑🏼‍🦯‍➡" # :person_with_white_cane_facing_right_medium-light_skin_tone: - U1F9D11F3FD200D1F9AF200D27A1FE0F = "🧑🏽‍🦯‍➡️" # :person_with_white_cane_facing_right_medium_skin_tone: - U1F9D11F3FD200D1F9AF200D27A1 = "🧑🏽‍🦯‍➡" # :person_with_white_cane_facing_right_medium_skin_tone: - U1F9D11F3FB200D1F9AF = "🧑🏻‍🦯" # :person_with_white_cane_light_skin_tone: - U1F9D11F3FE200D1F9AF = "🧑🏾‍🦯" # :person_with_white_cane_medium-dark_skin_tone: - U1F9D11F3FC200D1F9AF = "🧑🏼‍🦯" # :person_with_white_cane_medium-light_skin_tone: - U1F9D11F3FD200D1F9AF = "🧑🏽‍🦯" # :person_with_white_cane_medium_skin_tone: - U1F9EB = "🧫" # :petri_dish: - U1F426200D1F525 = "🐦‍🔥" # :phoenix: - U26CFFE0F = "⛏️" # :pick: - U26CF = "⛏" # :pick: - U1F6FB = "🛻" # :pickup_truck: - U1F967 = "🥧" # :pie: - U1F416 = "🐖" # :pig: - U1F437 = "🐷" # :pig_face: - U1F43D = "🐽" # :pig_nose: - U1F4A9 = "💩" # :pile_of_poo: - U1F48A = "💊" # :pill: - U1F9D1200D2708FE0F = "🧑‍✈️" # :pilot: - U1F9D1200D2708 = "🧑‍✈" # :pilot: - U1F9D11F3FF200D2708FE0F = "🧑🏿‍✈️" # :pilot_dark_skin_tone: - U1F9D11F3FF200D2708 = "🧑🏿‍✈" # :pilot_dark_skin_tone: - U1F9D11F3FB200D2708FE0F = "🧑🏻‍✈️" # :pilot_light_skin_tone: - U1F9D11F3FB200D2708 = "🧑🏻‍✈" # :pilot_light_skin_tone: - U1F9D11F3FE200D2708FE0F = "🧑🏾‍✈️" # :pilot_medium-dark_skin_tone: - U1F9D11F3FE200D2708 = "🧑🏾‍✈" # :pilot_medium-dark_skin_tone: - U1F9D11F3FC200D2708FE0F = "🧑🏼‍✈️" # :pilot_medium-light_skin_tone: - U1F9D11F3FC200D2708 = "🧑🏼‍✈" # :pilot_medium-light_skin_tone: - U1F9D11F3FD200D2708FE0F = "🧑🏽‍✈️" # :pilot_medium_skin_tone: - U1F9D11F3FD200D2708 = "🧑🏽‍✈" # :pilot_medium_skin_tone: - U1F90C = "🤌" # :pinched_fingers: - U1F90C1F3FF = "🤌🏿" # :pinched_fingers_dark_skin_tone: - U1F90C1F3FB = "🤌🏻" # :pinched_fingers_light_skin_tone: - U1F90C1F3FE = "🤌🏾" # :pinched_fingers_medium-dark_skin_tone: - U1F90C1F3FC = "🤌🏼" # :pinched_fingers_medium-light_skin_tone: - U1F90C1F3FD = "🤌🏽" # :pinched_fingers_medium_skin_tone: - U1F90F = "🤏" # :pinching_hand: - U1F90F1F3FF = "🤏🏿" # :pinching_hand_dark_skin_tone: - U1F90F1F3FB = "🤏🏻" # :pinching_hand_light_skin_tone: - U1F90F1F3FE = "🤏🏾" # :pinching_hand_medium-dark_skin_tone: - U1F90F1F3FC = "🤏🏼" # :pinching_hand_medium-light_skin_tone: - U1F90F1F3FD = "🤏🏽" # :pinching_hand_medium_skin_tone: - U1F38D = "🎍" # :pine_decoration: - U1F34D = "🍍" # :pineapple: - U1F3D3 = "🏓" # :ping_pong: - U1FA77 = "🩷" # :pink_heart: - U1F3F4200D2620FE0F = "🏴‍☠️" # :pirate_flag: - U1F3F4200D2620 = "🏴‍☠" # :pirate_flag: - U1F355 = "🍕" # :pizza: - U1FA85 = "🪅" # :piñata: - U1FAA7 = "🪧" # :placard: - U1F6D0 = "🛐" # :place_of_worship: - U25B6FE0F = "▶️" # :play_button: - U25B6 = "▶" # :play_button: - U23EFFE0F = "⏯️" # :play_or_pause_button: - U23EF = "⏯" # :play_or_pause_button: - U1F6DD = "🛝" # :playground_slide: - U1F97A = "🥺" # :pleading_face: - U1FAA0 = "🪠" # :plunger: - U2795 = "➕" # :plus: - U1F43B200D2744FE0F = "🐻‍❄️" # :polar_bear: - U1F43B200D2744 = "🐻‍❄" # :polar_bear: - U1F693 = "🚓" # :police_car: - U1F6A8 = "🚨" # :police_car_light: - U1F46E = "👮" # :police_officer: - U1F46E1F3FF = "👮🏿" # :police_officer_dark_skin_tone: - U1F46E1F3FB = "👮🏻" # :police_officer_light_skin_tone: - U1F46E1F3FE = "👮🏾" # :police_officer_medium-dark_skin_tone: - U1F46E1F3FC = "👮🏼" # :police_officer_medium-light_skin_tone: - U1F46E1F3FD = "👮🏽" # :police_officer_medium_skin_tone: - U1F429 = "🐩" # :poodle: - U1F3B1 = "🎱" # :pool_8_ball: - U1F37F = "🍿" # :popcorn: - U1F3E4 = "🏤" # :post_office: - U1F4EF = "📯" # :postal_horn: - U1F4EE = "📮" # :postbox: - U1F372 = "🍲" # :pot_of_food: - U1F6B0 = "🚰" # :potable_water: - U1F954 = "🥔" # :potato: - U1FAB4 = "🪴" # :potted_plant: - U1F357 = "🍗" # :poultry_leg: - U1F4B7 = "💷" # :pound_banknote: - U1FAD7 = "🫗" # :pouring_liquid: - U1F63E = "😾" # :pouting_cat: - U1F4FF = "📿" # :prayer_beads: - U1FAC3 = "🫃" # :pregnant_man: - U1FAC31F3FF = "🫃🏿" # :pregnant_man_dark_skin_tone: - U1FAC31F3FB = "🫃🏻" # :pregnant_man_light_skin_tone: - U1FAC31F3FE = "🫃🏾" # :pregnant_man_medium-dark_skin_tone: - U1FAC31F3FC = "🫃🏼" # :pregnant_man_medium-light_skin_tone: - U1FAC31F3FD = "🫃🏽" # :pregnant_man_medium_skin_tone: - U1FAC4 = "🫄" # :pregnant_person: - U1FAC41F3FF = "🫄🏿" # :pregnant_person_dark_skin_tone: - U1FAC41F3FB = "🫄🏻" # :pregnant_person_light_skin_tone: - U1FAC41F3FE = "🫄🏾" # :pregnant_person_medium-dark_skin_tone: - U1FAC41F3FC = "🫄🏼" # :pregnant_person_medium-light_skin_tone: - U1FAC41F3FD = "🫄🏽" # :pregnant_person_medium_skin_tone: - U1F930 = "🤰" # :pregnant_woman: - U1F9301F3FF = "🤰🏿" # :pregnant_woman_dark_skin_tone: - U1F9301F3FB = "🤰🏻" # :pregnant_woman_light_skin_tone: - U1F9301F3FE = "🤰🏾" # :pregnant_woman_medium-dark_skin_tone: - U1F9301F3FC = "🤰🏼" # :pregnant_woman_medium-light_skin_tone: - U1F9301F3FD = "🤰🏽" # :pregnant_woman_medium_skin_tone: - U1F968 = "🥨" # :pretzel: - U1F934 = "🤴" # :prince: - U1F9341F3FF = "🤴🏿" # :prince_dark_skin_tone: - U1F9341F3FB = "🤴🏻" # :prince_light_skin_tone: - U1F9341F3FE = "🤴🏾" # :prince_medium-dark_skin_tone: - U1F9341F3FC = "🤴🏼" # :prince_medium-light_skin_tone: - U1F9341F3FD = "🤴🏽" # :prince_medium_skin_tone: - U1F478 = "👸" # :princess: - U1F4781F3FF = "👸🏿" # :princess_dark_skin_tone: - U1F4781F3FB = "👸🏻" # :princess_light_skin_tone: - U1F4781F3FE = "👸🏾" # :princess_medium-dark_skin_tone: - U1F4781F3FC = "👸🏼" # :princess_medium-light_skin_tone: - U1F4781F3FD = "👸🏽" # :princess_medium_skin_tone: - U1F5A8FE0F = "🖨️" # :printer: - U1F5A8 = "🖨" # :printer: - U1F6AB = "🚫" # :prohibited: - U1F7E3 = "🟣" # :purple_circle: - U1F49C = "💜" # :purple_heart: - U1F7EA = "🟪" # :purple_square: - U1F45B = "👛" # :purse: - U1F4CC = "📌" # :pushpin: - U1F9E9 = "🧩" # :puzzle_piece: - U1F407 = "🐇" # :rabbit: - U1F430 = "🐰" # :rabbit_face: - U1F99D = "🦝" # :raccoon: - U1F3CEFE0F = "🏎️" # :racing_car: - U1F3CE = "🏎" # :racing_car: - U1F4FB = "📻" # :radio: - U1F518 = "🔘" # :radio_button: - U2622FE0F = "☢️" # :radioactive: - U2622 = "☢" # :radioactive: - U1F683 = "🚃" # :railway_car: - U1F6E4FE0F = "🛤️" # :railway_track: - U1F6E4 = "🛤" # :railway_track: - U1F308 = "🌈" # :rainbow: - U1F3F3FE0F200D1F308 = "🏳️‍🌈" # :rainbow_flag: - U1F3F3200D1F308 = "🏳‍🌈" # :rainbow_flag: - U1F91A = "🤚" # :raised_back_of_hand: - U1F91A1F3FF = "🤚🏿" # :raised_back_of_hand_dark_skin_tone: - U1F91A1F3FB = "🤚🏻" # :raised_back_of_hand_light_skin_tone: - U1F91A1F3FE = "🤚🏾" # :raised_back_of_hand_medium-dark_skin_tone: - U1F91A1F3FC = "🤚🏼" # :raised_back_of_hand_medium-light_skin_tone: - U1F91A1F3FD = "🤚🏽" # :raised_back_of_hand_medium_skin_tone: - U270A = "✊" # :raised_fist: - U270A1F3FF = "✊🏿" # :raised_fist_dark_skin_tone: - U270A1F3FB = "✊🏻" # :raised_fist_light_skin_tone: - U270A1F3FE = "✊🏾" # :raised_fist_medium-dark_skin_tone: - U270A1F3FC = "✊🏼" # :raised_fist_medium-light_skin_tone: - U270A1F3FD = "✊🏽" # :raised_fist_medium_skin_tone: - U270B = "✋" # :raised_hand: - U270B1F3FF = "✋🏿" # :raised_hand_dark_skin_tone: - U270B1F3FB = "✋🏻" # :raised_hand_light_skin_tone: - U270B1F3FE = "✋🏾" # :raised_hand_medium-dark_skin_tone: - U270B1F3FC = "✋🏼" # :raised_hand_medium-light_skin_tone: - U270B1F3FD = "✋🏽" # :raised_hand_medium_skin_tone: - U1F64C = "🙌" # :raising_hands: - U1F64C1F3FF = "🙌🏿" # :raising_hands_dark_skin_tone: - U1F64C1F3FB = "🙌🏻" # :raising_hands_light_skin_tone: - U1F64C1F3FE = "🙌🏾" # :raising_hands_medium-dark_skin_tone: - U1F64C1F3FC = "🙌🏼" # :raising_hands_medium-light_skin_tone: - U1F64C1F3FD = "🙌🏽" # :raising_hands_medium_skin_tone: - U1F40F = "🐏" # :ram: - U1F400 = "🐀" # :rat: - U1FA92 = "🪒" # :razor: - U1F9FE = "🧾" # :receipt: - U23FAFE0F = "⏺️" # :record_button: - U23FA = "⏺" # :record_button: - U267BFE0F = "♻️" # :recycling_symbol: - U267B = "♻" # :recycling_symbol: - U1F34E = "🍎" # :red_apple: - U1F534 = "🔴" # :red_circle: - U1F9E7 = "🧧" # :red_envelope: - U2757 = "❗" # :red_exclamation_mark: - U1F9B0 = "🦰" # :red_hair: - U2764FE0F = "❤️" # :red_heart: - U2764 = "❤" # :red_heart: - U1F3EE = "🏮" # :red_paper_lantern: - U2753 = "❓" # :red_question_mark: - U1F7E5 = "🟥" # :red_square: - U1F53B = "🔻" # :red_triangle_pointed_down: - U1F53A = "🔺" # :red_triangle_pointed_up: - UAEFE0F = "®️" # :registered: - UAE = "®" # :registered: - U1F60C = "😌" # :relieved_face: - U1F397FE0F = "🎗️" # :reminder_ribbon: - U1F397 = "🎗" # :reminder_ribbon: - U1F501 = "🔁" # :repeat_button: - U1F502 = "🔂" # :repeat_single_button: - U26D1FE0F = "⛑️" # :rescue_worker’s_helmet: - U26D1 = "⛑" # :rescue_worker’s_helmet: - U1F6BB = "🚻" # :restroom: - U25C0FE0F = "◀️" # :reverse_button: - U25C0 = "◀" # :reverse_button: - U1F49E = "💞" # :revolving_hearts: - U1F98F = "🦏" # :rhinoceros: - U1F380 = "🎀" # :ribbon: - U1F359 = "🍙" # :rice_ball: - U1F358 = "🍘" # :rice_cracker: - U1F91C = "🤜" # :right-facing_fist: - U1F91C1F3FF = "🤜🏿" # :right-facing_fist_dark_skin_tone: - U1F91C1F3FB = "🤜🏻" # :right-facing_fist_light_skin_tone: - U1F91C1F3FE = "🤜🏾" # :right-facing_fist_medium-dark_skin_tone: - U1F91C1F3FC = "🤜🏼" # :right-facing_fist_medium-light_skin_tone: - U1F91C1F3FD = "🤜🏽" # :right-facing_fist_medium_skin_tone: - U1F5EFFE0F = "🗯️" # :right_anger_bubble: - U1F5EF = "🗯" # :right_anger_bubble: - U27A1FE0F = "➡️" # :right_arrow: - U27A1 = "➡" # :right_arrow: - U2935FE0F = "⤵️" # :right_arrow_curving_down: - U2935 = "⤵" # :right_arrow_curving_down: - U21A9FE0F = "↩️" # :right_arrow_curving_left: - U21A9 = "↩" # :right_arrow_curving_left: - U2934FE0F = "⤴️" # :right_arrow_curving_up: - U2934 = "⤴" # :right_arrow_curving_up: - U1FAF1 = "🫱" # :rightwards_hand: - U1FAF11F3FF = "🫱🏿" # :rightwards_hand_dark_skin_tone: - U1FAF11F3FB = "🫱🏻" # :rightwards_hand_light_skin_tone: - U1FAF11F3FE = "🫱🏾" # :rightwards_hand_medium-dark_skin_tone: - U1FAF11F3FC = "🫱🏼" # :rightwards_hand_medium-light_skin_tone: - U1FAF11F3FD = "🫱🏽" # :rightwards_hand_medium_skin_tone: - U1FAF8 = "🫸" # :rightwards_pushing_hand: - U1FAF81F3FF = "🫸🏿" # :rightwards_pushing_hand_dark_skin_tone: - U1FAF81F3FB = "🫸🏻" # :rightwards_pushing_hand_light_skin_tone: - U1FAF81F3FE = "🫸🏾" # :rightwards_pushing_hand_medium-dark_skin_tone: - U1FAF81F3FC = "🫸🏼" # :rightwards_pushing_hand_medium-light_skin_tone: - U1FAF81F3FD = "🫸🏽" # :rightwards_pushing_hand_medium_skin_tone: - U1F48D = "💍" # :ring: - U1F6DF = "🛟" # :ring_buoy: - U1FA90 = "🪐" # :ringed_planet: - U1F360 = "🍠" # :roasted_sweet_potato: - U1F916 = "🤖" # :robot: - U1FAA8 = "🪨" # :rock: - U1F680 = "🚀" # :rocket: - U1F9FB = "🧻" # :roll_of_paper: - U1F5DEFE0F = "🗞️" # :rolled-up_newspaper: - U1F5DE = "🗞" # :rolled-up_newspaper: - U1F3A2 = "🎢" # :roller_coaster: - U1F6FC = "🛼" # :roller_skate: - U1F923 = "🤣" # :rolling_on_the_floor_laughing: - U1F413 = "🐓" # :rooster: - U1F339 = "🌹" # :rose: - U1F3F5FE0F = "🏵️" # :rosette: - U1F3F5 = "🏵" # :rosette: - U1F4CD = "📍" # :round_pushpin: - U1F3C9 = "🏉" # :rugby_football: - U1F3BD = "🎽" # :running_shirt: - U1F45F = "👟" # :running_shoe: - U1F625 = "😥" # :sad_but_relieved_face: - U1F9F7 = "🧷" # :safety_pin: - U1F9BA = "🦺" # :safety_vest: - U26F5 = "⛵" # :sailboat: - U1F376 = "🍶" # :sake: - U1F9C2 = "🧂" # :salt: - U1FAE1 = "🫡" # :saluting_face: - U1F96A = "🥪" # :sandwich: - U1F97B = "🥻" # :sari: - U1F6F0FE0F = "🛰️" # :satellite: - U1F6F0 = "🛰" # :satellite: - U1F4E1 = "📡" # :satellite_antenna: - U1F995 = "🦕" # :sauropod: - U1F3B7 = "🎷" # :saxophone: - U1F9E3 = "🧣" # :scarf: - U1F3EB = "🏫" # :school: - U1F9D1200D1F52C = "🧑‍🔬" # :scientist: - U1F9D11F3FF200D1F52C = "🧑🏿‍🔬" # :scientist_dark_skin_tone: - U1F9D11F3FB200D1F52C = "🧑🏻‍🔬" # :scientist_light_skin_tone: - U1F9D11F3FE200D1F52C = "🧑🏾‍🔬" # :scientist_medium-dark_skin_tone: - U1F9D11F3FC200D1F52C = "🧑🏼‍🔬" # :scientist_medium-light_skin_tone: - U1F9D11F3FD200D1F52C = "🧑🏽‍🔬" # :scientist_medium_skin_tone: - U2702FE0F = "✂️" # :scissors: - U2702 = "✂" # :scissors: - U1F982 = "🦂" # :scorpion: - U1FA9B = "🪛" # :screwdriver: - U1F4DC = "📜" # :scroll: - U1F9AD = "🦭" # :seal: - U1F4BA = "💺" # :seat: - U1F648 = "🙈" # :see-no-evil_monkey: - U1F331 = "🌱" # :seedling: - U1F933 = "🤳" # :selfie: - U1F9331F3FF = "🤳🏿" # :selfie_dark_skin_tone: - U1F9331F3FB = "🤳🏻" # :selfie_light_skin_tone: - U1F9331F3FE = "🤳🏾" # :selfie_medium-dark_skin_tone: - U1F9331F3FC = "🤳🏼" # :selfie_medium-light_skin_tone: - U1F9331F3FD = "🤳🏽" # :selfie_medium_skin_tone: - U1F415200D1F9BA = "🐕‍🦺" # :service_dog: - U1F562 = "🕢" # :seven-thirty: - U1F556 = "🕖" # :seven_o’clock: - U1FAA1 = "🪡" # :sewing_needle: - U1FAE8 = "🫨" # :shaking_face: - U1F958 = "🥘" # :shallow_pan_of_food: - U2618FE0F = "☘️" # :shamrock: - U2618 = "☘" # :shamrock: - U1F988 = "🦈" # :shark: - U1F367 = "🍧" # :shaved_ice: - U1F33E = "🌾" # :sheaf_of_rice: - U1F6E1FE0F = "🛡️" # :shield: - U1F6E1 = "🛡" # :shield: - U26E9FE0F = "⛩️" # :shinto_shrine: - U26E9 = "⛩" # :shinto_shrine: - U1F6A2 = "🚢" # :ship: - U1F320 = "🌠" # :shooting_star: - U1F6CDFE0F = "🛍️" # :shopping_bags: - U1F6CD = "🛍" # :shopping_bags: - U1F6D2 = "🛒" # :shopping_cart: - U1F370 = "🍰" # :shortcake: - U1FA73 = "🩳" # :shorts: - U1F6BF = "🚿" # :shower: - U1F990 = "🦐" # :shrimp: - U1F500 = "🔀" # :shuffle_tracks_button: - U1F92B = "🤫" # :shushing_face: - U1F918 = "🤘" # :sign_of_the_horns: - U1F9181F3FF = "🤘🏿" # :sign_of_the_horns_dark_skin_tone: - U1F9181F3FB = "🤘🏻" # :sign_of_the_horns_light_skin_tone: - U1F9181F3FE = "🤘🏾" # :sign_of_the_horns_medium-dark_skin_tone: - U1F9181F3FC = "🤘🏼" # :sign_of_the_horns_medium-light_skin_tone: - U1F9181F3FD = "🤘🏽" # :sign_of_the_horns_medium_skin_tone: - U1F9D1200D1F3A4 = "🧑‍🎤" # :singer: - U1F9D11F3FF200D1F3A4 = "🧑🏿‍🎤" # :singer_dark_skin_tone: - U1F9D11F3FB200D1F3A4 = "🧑🏻‍🎤" # :singer_light_skin_tone: - U1F9D11F3FE200D1F3A4 = "🧑🏾‍🎤" # :singer_medium-dark_skin_tone: - U1F9D11F3FC200D1F3A4 = "🧑🏼‍🎤" # :singer_medium-light_skin_tone: - U1F9D11F3FD200D1F3A4 = "🧑🏽‍🎤" # :singer_medium_skin_tone: - U1F561 = "🕡" # :six-thirty: - U1F555 = "🕕" # :six_o’clock: - U1F6F9 = "🛹" # :skateboard: - U26F7FE0F = "⛷️" # :skier: - U26F7 = "⛷" # :skier: - U1F3BF = "🎿" # :skis: - U1F480 = "💀" # :skull: - U2620FE0F = "☠️" # :skull_and_crossbones: - U2620 = "☠" # :skull_and_crossbones: - U1F9A8 = "🦨" # :skunk: - U1F6F7 = "🛷" # :sled: - U1F634 = "😴" # :sleeping_face: - U1F62A = "😪" # :sleepy_face: - U1F641 = "🙁" # :slightly_frowning_face: - U1F642 = "🙂" # :slightly_smiling_face: - U1F3B0 = "🎰" # :slot_machine: - U1F9A5 = "🦥" # :sloth: - U1F6E9FE0F = "🛩️" # :small_airplane: - U1F6E9 = "🛩" # :small_airplane: - U1F539 = "🔹" # :small_blue_diamond: - U1F538 = "🔸" # :small_orange_diamond: - U1F63B = "😻" # :smiling_cat_with_heart-eyes: - U263AFE0F = "☺️" # :smiling_face: - U263A = "☺" # :smiling_face: - U1F607 = "😇" # :smiling_face_with_halo: - U1F60D = "😍" # :smiling_face_with_heart-eyes: - U1F970 = "🥰" # :smiling_face_with_hearts: - U1F608 = "😈" # :smiling_face_with_horns: - U1F917 = "🤗" # :smiling_face_with_open_hands: - U1F60A = "😊" # :smiling_face_with_smiling_eyes: - U1F60E = "😎" # :smiling_face_with_sunglasses: - U1F972 = "🥲" # :smiling_face_with_tear: - U1F60F = "😏" # :smirking_face: - U1F40C = "🐌" # :snail: - U1F40D = "🐍" # :snake: - U1F927 = "🤧" # :sneezing_face: - U1F3D4FE0F = "🏔️" # :snow-capped_mountain: - U1F3D4 = "🏔" # :snow-capped_mountain: - U1F3C2 = "🏂" # :snowboarder: - U1F3C21F3FF = "🏂🏿" # :snowboarder_dark_skin_tone: - U1F3C21F3FB = "🏂🏻" # :snowboarder_light_skin_tone: - U1F3C21F3FE = "🏂🏾" # :snowboarder_medium-dark_skin_tone: - U1F3C21F3FC = "🏂🏼" # :snowboarder_medium-light_skin_tone: - U1F3C21F3FD = "🏂🏽" # :snowboarder_medium_skin_tone: - U2744FE0F = "❄️" # :snowflake: - U2744 = "❄" # :snowflake: - U2603FE0F = "☃️" # :snowman: - U2603 = "☃" # :snowman: - U26C4 = "⛄" # :snowman_without_snow: - U1F9FC = "🧼" # :soap: - U26BD = "⚽" # :soccer_ball: - U1F9E6 = "🧦" # :socks: - U1F366 = "🍦" # :soft_ice_cream: - U1F94E = "🥎" # :softball: - U2660FE0F = "♠️" # :spade_suit: - U2660 = "♠" # :spade_suit: - U1F35D = "🍝" # :spaghetti: - U2747FE0F = "❇️" # :sparkle: - U2747 = "❇" # :sparkle: - U1F387 = "🎇" # :sparkler: - U2728 = "✨" # :sparkles: - U1F496 = "💖" # :sparkling_heart: - U1F64A = "🙊" # :speak-no-evil_monkey: - U1F50A = "🔊" # :speaker_high_volume: - U1F508 = "🔈" # :speaker_low_volume: - U1F509 = "🔉" # :speaker_medium_volume: - U1F5E3FE0F = "🗣️" # :speaking_head: - U1F5E3 = "🗣" # :speaking_head: - U1F4AC = "💬" # :speech_balloon: - U1F6A4 = "🚤" # :speedboat: - U1F577FE0F = "🕷️" # :spider: - U1F577 = "🕷" # :spider: - U1F578FE0F = "🕸️" # :spider_web: - U1F578 = "🕸" # :spider_web: - U1F5D3FE0F = "🗓️" # :spiral_calendar: - U1F5D3 = "🗓" # :spiral_calendar: - U1F5D2FE0F = "🗒️" # :spiral_notepad: - U1F5D2 = "🗒" # :spiral_notepad: - U1F41A = "🐚" # :spiral_shell: - U1F9FD = "🧽" # :sponge: - U1F944 = "🥄" # :spoon: - U1F699 = "🚙" # :sport_utility_vehicle: - U1F3C5 = "🏅" # :sports_medal: - U1F433 = "🐳" # :spouting_whale: - U1F991 = "🦑" # :squid: - U1F61D = "😝" # :squinting_face_with_tongue: - U1F3DFFE0F = "🏟️" # :stadium: - U1F3DF = "🏟" # :stadium: - U2B50 = "⭐" # :star: - U1F929 = "🤩" # :star-struck: - U262AFE0F = "☪️" # :star_and_crescent: - U262A = "☪" # :star_and_crescent: - U2721FE0F = "✡️" # :star_of_David: - U2721 = "✡" # :star_of_David: - U1F689 = "🚉" # :station: - U1F35C = "🍜" # :steaming_bowl: - U1FA7A = "🩺" # :stethoscope: - U23F9FE0F = "⏹️" # :stop_button: - U23F9 = "⏹" # :stop_button: - U1F6D1 = "🛑" # :stop_sign: - U23F1FE0F = "⏱️" # :stopwatch: - U23F1 = "⏱" # :stopwatch: - U1F4CF = "📏" # :straight_ruler: - U1F353 = "🍓" # :strawberry: - U1F9D1200D1F393 = "🧑‍🎓" # :student: - U1F9D11F3FF200D1F393 = "🧑🏿‍🎓" # :student_dark_skin_tone: - U1F9D11F3FB200D1F393 = "🧑🏻‍🎓" # :student_light_skin_tone: - U1F9D11F3FE200D1F393 = "🧑🏾‍🎓" # :student_medium-dark_skin_tone: - U1F9D11F3FC200D1F393 = "🧑🏼‍🎓" # :student_medium-light_skin_tone: - U1F9D11F3FD200D1F393 = "🧑🏽‍🎓" # :student_medium_skin_tone: - U1F399FE0F = "🎙️" # :studio_microphone: - U1F399 = "🎙" # :studio_microphone: - U1F959 = "🥙" # :stuffed_flatbread: - U2600FE0F = "☀️" # :sun: - U2600 = "☀" # :sun: - U26C5 = "⛅" # :sun_behind_cloud: - U1F325FE0F = "🌥️" # :sun_behind_large_cloud: - U1F325 = "🌥" # :sun_behind_large_cloud: - U1F326FE0F = "🌦️" # :sun_behind_rain_cloud: - U1F326 = "🌦" # :sun_behind_rain_cloud: - U1F324FE0F = "🌤️" # :sun_behind_small_cloud: - U1F324 = "🌤" # :sun_behind_small_cloud: - U1F31E = "🌞" # :sun_with_face: - U1F33B = "🌻" # :sunflower: - U1F576FE0F = "🕶️" # :sunglasses: - U1F576 = "🕶" # :sunglasses: - U1F305 = "🌅" # :sunrise: - U1F304 = "🌄" # :sunrise_over_mountains: - U1F307 = "🌇" # :sunset: - U1F9B8 = "🦸" # :superhero: - U1F9B81F3FF = "🦸🏿" # :superhero_dark_skin_tone: - U1F9B81F3FB = "🦸🏻" # :superhero_light_skin_tone: - U1F9B81F3FE = "🦸🏾" # :superhero_medium-dark_skin_tone: - U1F9B81F3FC = "🦸🏼" # :superhero_medium-light_skin_tone: - U1F9B81F3FD = "🦸🏽" # :superhero_medium_skin_tone: - U1F9B9 = "🦹" # :supervillain: - U1F9B91F3FF = "🦹🏿" # :supervillain_dark_skin_tone: - U1F9B91F3FB = "🦹🏻" # :supervillain_light_skin_tone: - U1F9B91F3FE = "🦹🏾" # :supervillain_medium-dark_skin_tone: - U1F9B91F3FC = "🦹🏼" # :supervillain_medium-light_skin_tone: - U1F9B91F3FD = "🦹🏽" # :supervillain_medium_skin_tone: - U1F363 = "🍣" # :sushi: - U1F69F = "🚟" # :suspension_railway: - U1F9A2 = "🦢" # :swan: - U1F4A6 = "💦" # :sweat_droplets: - U1F54D = "🕍" # :synagogue: - U1F489 = "💉" # :syringe: - U1F455 = "👕" # :t-shirt: - U1F32E = "🌮" # :taco: - U1F961 = "🥡" # :takeout_box: - U1FAD4 = "🫔" # :tamale: - U1F38B = "🎋" # :tanabata_tree: - U1F34A = "🍊" # :tangerine: - U1F695 = "🚕" # :taxi: - U1F9D1200D1F3EB = "🧑‍🏫" # :teacher: - U1F9D11F3FF200D1F3EB = "🧑🏿‍🏫" # :teacher_dark_skin_tone: - U1F9D11F3FB200D1F3EB = "🧑🏻‍🏫" # :teacher_light_skin_tone: - U1F9D11F3FE200D1F3EB = "🧑🏾‍🏫" # :teacher_medium-dark_skin_tone: - U1F9D11F3FC200D1F3EB = "🧑🏼‍🏫" # :teacher_medium-light_skin_tone: - U1F9D11F3FD200D1F3EB = "🧑🏽‍🏫" # :teacher_medium_skin_tone: - U1F375 = "🍵" # :teacup_without_handle: - U1FAD6 = "🫖" # :teapot: - U1F4C6 = "📆" # :tear-off_calendar: - U1F9D1200D1F4BB = "🧑‍💻" # :technologist: - U1F9D11F3FF200D1F4BB = "🧑🏿‍💻" # :technologist_dark_skin_tone: - U1F9D11F3FB200D1F4BB = "🧑🏻‍💻" # :technologist_light_skin_tone: - U1F9D11F3FE200D1F4BB = "🧑🏾‍💻" # :technologist_medium-dark_skin_tone: - U1F9D11F3FC200D1F4BB = "🧑🏼‍💻" # :technologist_medium-light_skin_tone: - U1F9D11F3FD200D1F4BB = "🧑🏽‍💻" # :technologist_medium_skin_tone: - U1F9F8 = "🧸" # :teddy_bear: - U260EFE0F = "☎️" # :telephone: - U260E = "☎" # :telephone: - U1F4DE = "📞" # :telephone_receiver: - U1F52D = "🔭" # :telescope: - U1F4FA = "📺" # :television: - U1F565 = "🕥" # :ten-thirty: - U1F559 = "🕙" # :ten_o’clock: - U1F3BE = "🎾" # :tennis: - U26FA = "⛺" # :tent: - U1F9EA = "🧪" # :test_tube: - U1F321FE0F = "🌡️" # :thermometer: - U1F321 = "🌡" # :thermometer: - U1F914 = "🤔" # :thinking_face: - U1FA74 = "🩴" # :thong_sandal: - U1F4AD = "💭" # :thought_balloon: - U1F9F5 = "🧵" # :thread: - U1F55E = "🕞" # :three-thirty: - U1F552 = "🕒" # :three_o’clock: - U1F44E = "👎" # :thumbs_down: - U1F44E1F3FF = "👎🏿" # :thumbs_down_dark_skin_tone: - U1F44E1F3FB = "👎🏻" # :thumbs_down_light_skin_tone: - U1F44E1F3FE = "👎🏾" # :thumbs_down_medium-dark_skin_tone: - U1F44E1F3FC = "👎🏼" # :thumbs_down_medium-light_skin_tone: - U1F44E1F3FD = "👎🏽" # :thumbs_down_medium_skin_tone: - U1F44D = "👍" # :thumbs_up: - U1F44D1F3FF = "👍🏿" # :thumbs_up_dark_skin_tone: - U1F44D1F3FB = "👍🏻" # :thumbs_up_light_skin_tone: - U1F44D1F3FE = "👍🏾" # :thumbs_up_medium-dark_skin_tone: - U1F44D1F3FC = "👍🏼" # :thumbs_up_medium-light_skin_tone: - U1F44D1F3FD = "👍🏽" # :thumbs_up_medium_skin_tone: - U1F3AB = "🎫" # :ticket: - U1F405 = "🐅" # :tiger: - U1F42F = "🐯" # :tiger_face: - U23F2FE0F = "⏲️" # :timer_clock: - U23F2 = "⏲" # :timer_clock: - U1F62B = "😫" # :tired_face: - U1F6BD = "🚽" # :toilet: - U1F345 = "🍅" # :tomato: - U1F445 = "👅" # :tongue: - U1F9F0 = "🧰" # :toolbox: - U1F9B7 = "🦷" # :tooth: - U1FAA5 = "🪥" # :toothbrush: - U1F3A9 = "🎩" # :top_hat: - U1F32AFE0F = "🌪️" # :tornado: - U1F32A = "🌪" # :tornado: - U1F5B2FE0F = "🖲️" # :trackball: - U1F5B2 = "🖲" # :trackball: - U1F69C = "🚜" # :tractor: - U2122FE0F = "™️" # :trade_mark: - U2122 = "™" # :trade_mark: - U1F686 = "🚆" # :train: - U1F68A = "🚊" # :tram: - U1F68B = "🚋" # :tram_car: - U1F3F3FE0F200D26A7FE0F = "🏳️‍⚧️" # :transgender_flag: - U1F3F3200D26A7FE0F = "🏳‍⚧️" # :transgender_flag: - U1F3F3FE0F200D26A7 = "🏳️‍⚧" # :transgender_flag: - U1F3F3200D26A7 = "🏳‍⚧" # :transgender_flag: - U26A7FE0F = "⚧️" # :transgender_symbol: - U26A7 = "⚧" # :transgender_symbol: - U1F6A9 = "🚩" # :triangular_flag: - U1F4D0 = "📐" # :triangular_ruler: - U1F531 = "🔱" # :trident_emblem: - U1F9CC = "🧌" # :troll: - U1F68E = "🚎" # :trolleybus: - U1F3C6 = "🏆" # :trophy: - U1F379 = "🍹" # :tropical_drink: - U1F420 = "🐠" # :tropical_fish: - U1F3BA = "🎺" # :trumpet: - U1F337 = "🌷" # :tulip: - U1F943 = "🥃" # :tumbler_glass: - U1F983 = "🦃" # :turkey: - U1F422 = "🐢" # :turtle: - U1F567 = "🕧" # :twelve-thirty: - U1F55B = "🕛" # :twelve_o’clock: - U1F42B = "🐫" # :two-hump_camel: - U1F55D = "🕝" # :two-thirty: - U1F495 = "💕" # :two_hearts: - U1F551 = "🕑" # :two_o’clock: - U2602FE0F = "☂️" # :umbrella: - U2602 = "☂" # :umbrella: - U26F1FE0F = "⛱️" # :umbrella_on_ground: - U26F1 = "⛱" # :umbrella_on_ground: - U2614 = "☔" # :umbrella_with_rain_drops: - U1F612 = "😒" # :unamused_face: - U1F984 = "🦄" # :unicorn: - U1F513 = "🔓" # :unlocked: - U2195FE0F = "↕️" # :up-down_arrow: - U2195 = "↕" # :up-down_arrow: - U2196FE0F = "↖️" # :up-left_arrow: - U2196 = "↖" # :up-left_arrow: - U2197FE0F = "↗️" # :up-right_arrow: - U2197 = "↗" # :up-right_arrow: - U2B06FE0F = "⬆️" # :up_arrow: - U2B06 = "⬆" # :up_arrow: - U1F643 = "🙃" # :upside-down_face: - U1F53C = "🔼" # :upwards_button: - U1F9DB = "🧛" # :vampire: - U1F9DB1F3FF = "🧛🏿" # :vampire_dark_skin_tone: - U1F9DB1F3FB = "🧛🏻" # :vampire_light_skin_tone: - U1F9DB1F3FE = "🧛🏾" # :vampire_medium-dark_skin_tone: - U1F9DB1F3FC = "🧛🏼" # :vampire_medium-light_skin_tone: - U1F9DB1F3FD = "🧛🏽" # :vampire_medium_skin_tone: - U1F6A6 = "🚦" # :vertical_traffic_light: - U1F4F3 = "📳" # :vibration_mode: - U270CFE0F = "✌️" # :victory_hand: - U270C = "✌" # :victory_hand: - U270C1F3FF = "✌🏿" # :victory_hand_dark_skin_tone: - U270C1F3FB = "✌🏻" # :victory_hand_light_skin_tone: - U270C1F3FE = "✌🏾" # :victory_hand_medium-dark_skin_tone: - U270C1F3FC = "✌🏼" # :victory_hand_medium-light_skin_tone: - U270C1F3FD = "✌🏽" # :victory_hand_medium_skin_tone: - U1F4F9 = "📹" # :video_camera: - U1F3AE = "🎮" # :video_game: - U1F4FC = "📼" # :videocassette: - U1F3BB = "🎻" # :violin: - U1F30B = "🌋" # :volcano: - U1F3D0 = "🏐" # :volleyball: - U1F596 = "🖖" # :vulcan_salute: - U1F5961F3FF = "🖖🏿" # :vulcan_salute_dark_skin_tone: - U1F5961F3FB = "🖖🏻" # :vulcan_salute_light_skin_tone: - U1F5961F3FE = "🖖🏾" # :vulcan_salute_medium-dark_skin_tone: - U1F5961F3FC = "🖖🏼" # :vulcan_salute_medium-light_skin_tone: - U1F5961F3FD = "🖖🏽" # :vulcan_salute_medium_skin_tone: - U1F9C7 = "🧇" # :waffle: - U1F318 = "🌘" # :waning_crescent_moon: - U1F316 = "🌖" # :waning_gibbous_moon: - U26A0FE0F = "⚠️" # :warning: - U26A0 = "⚠" # :warning: - U1F5D1FE0F = "🗑️" # :wastebasket: - U1F5D1 = "🗑" # :wastebasket: - U231A = "⌚" # :watch: - U1F403 = "🐃" # :water_buffalo: - U1F6BE = "🚾" # :water_closet: - U1F52B = "🔫" # :water_pistol: - U1F30A = "🌊" # :water_wave: - U1F349 = "🍉" # :watermelon: - U1F44B = "👋" # :waving_hand: - U1F44B1F3FF = "👋🏿" # :waving_hand_dark_skin_tone: - U1F44B1F3FB = "👋🏻" # :waving_hand_light_skin_tone: - U1F44B1F3FE = "👋🏾" # :waving_hand_medium-dark_skin_tone: - U1F44B1F3FC = "👋🏼" # :waving_hand_medium-light_skin_tone: - U1F44B1F3FD = "👋🏽" # :waving_hand_medium_skin_tone: - U3030FE0F = "〰️" # :wavy_dash: - U3030 = "〰" # :wavy_dash: - U1F312 = "🌒" # :waxing_crescent_moon: - U1F314 = "🌔" # :waxing_gibbous_moon: - U1F640 = "🙀" # :weary_cat: - U1F629 = "😩" # :weary_face: - U1F492 = "💒" # :wedding: - U1F40B = "🐋" # :whale: - U1F6DE = "🛞" # :wheel: - U2638FE0F = "☸️" # :wheel_of_dharma: - U2638 = "☸" # :wheel_of_dharma: - U267F = "♿" # :wheelchair_symbol: - U1F9AF = "🦯" # :white_cane: - U26AA = "⚪" # :white_circle: - U2755 = "❕" # :white_exclamation_mark: - U1F3F3FE0F = "🏳️" # :white_flag: - U1F3F3 = "🏳" # :white_flag: - U1F4AE = "💮" # :white_flower: - U1F9B3 = "🦳" # :white_hair: - U1F90D = "🤍" # :white_heart: - U2B1C = "⬜" # :white_large_square: - U25FD = "◽" # :white_medium-small_square: - U25FBFE0F = "◻️" # :white_medium_square: - U25FB = "◻" # :white_medium_square: - U2754 = "❔" # :white_question_mark: - U25ABFE0F = "▫️" # :white_small_square: - U25AB = "▫" # :white_small_square: - U1F533 = "🔳" # :white_square_button: - U1F940 = "🥀" # :wilted_flower: - U1F390 = "🎐" # :wind_chime: - U1F32CFE0F = "🌬️" # :wind_face: - U1F32C = "🌬" # :wind_face: - U1FA9F = "🪟" # :window: - U1F377 = "🍷" # :wine_glass: - U1FABD = "🪽" # :wing: - U1F609 = "😉" # :winking_face: - U1F61C = "😜" # :winking_face_with_tongue: - U1F6DC = "🛜" # :wireless: - U1F43A = "🐺" # :wolf: - U1F469 = "👩" # :woman: - U1F46B = "👫" # :woman_and_man_holding_hands: - U1F46B1F3FF = "👫🏿" # :woman_and_man_holding_hands_dark_skin_tone: - U1F4691F3FF200D1F91D200D1F4681F3FB = "👩🏿‍🤝‍👨🏻" # :woman_and_man_holding_hands_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D1F91D200D1F4681F3FE = "👩🏿‍🤝‍👨🏾" # :woman_and_man_holding_hands_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D1F91D200D1F4681F3FC = "👩🏿‍🤝‍👨🏼" # :woman_and_man_holding_hands_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D1F91D200D1F4681F3FD = "👩🏿‍🤝‍👨🏽" # :woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone: - U1F46B1F3FB = "👫🏻" # :woman_and_man_holding_hands_light_skin_tone: - U1F4691F3FB200D1F91D200D1F4681F3FF = "👩🏻‍🤝‍👨🏿" # :woman_and_man_holding_hands_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D1F91D200D1F4681F3FE = "👩🏻‍🤝‍👨🏾" # :woman_and_man_holding_hands_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D1F91D200D1F4681F3FC = "👩🏻‍🤝‍👨🏼" # :woman_and_man_holding_hands_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D1F91D200D1F4681F3FD = "👩🏻‍🤝‍👨🏽" # :woman_and_man_holding_hands_light_skin_tone_medium_skin_tone: - U1F46B1F3FE = "👫🏾" # :woman_and_man_holding_hands_medium-dark_skin_tone: - U1F4691F3FE200D1F91D200D1F4681F3FF = "👩🏾‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D1F91D200D1F4681F3FB = "👩🏾‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D1F91D200D1F4681F3FC = "👩🏾‍🤝‍👨🏼" # :woman_and_man_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D1F91D200D1F4681F3FD = "👩🏾‍🤝‍👨🏽" # :woman_and_man_holding_hands_medium-dark_skin_tone_medium_skin_tone: - U1F46B1F3FC = "👫🏼" # :woman_and_man_holding_hands_medium-light_skin_tone: - U1F4691F3FC200D1F91D200D1F4681F3FF = "👩🏼‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D1F91D200D1F4681F3FB = "👩🏼‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D1F91D200D1F4681F3FE = "👩🏼‍🤝‍👨🏾" # :woman_and_man_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D1F91D200D1F4681F3FD = "👩🏼‍🤝‍👨🏽" # :woman_and_man_holding_hands_medium-light_skin_tone_medium_skin_tone: - U1F46B1F3FD = "👫🏽" # :woman_and_man_holding_hands_medium_skin_tone: - U1F4691F3FD200D1F91D200D1F4681F3FF = "👩🏽‍🤝‍👨🏿" # :woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D1F91D200D1F4681F3FB = "👩🏽‍🤝‍👨🏻" # :woman_and_man_holding_hands_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D1F91D200D1F4681F3FE = "👩🏽‍🤝‍👨🏾" # :woman_and_man_holding_hands_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D1F91D200D1F4681F3FC = "👩🏽‍🤝‍👨🏼" # :woman_and_man_holding_hands_medium_skin_tone_medium-light_skin_tone: - U1F469200D1F3A8 = "👩‍🎨" # :woman_artist: - U1F4691F3FF200D1F3A8 = "👩🏿‍🎨" # :woman_artist_dark_skin_tone: - U1F4691F3FB200D1F3A8 = "👩🏻‍🎨" # :woman_artist_light_skin_tone: - U1F4691F3FE200D1F3A8 = "👩🏾‍🎨" # :woman_artist_medium-dark_skin_tone: - U1F4691F3FC200D1F3A8 = "👩🏼‍🎨" # :woman_artist_medium-light_skin_tone: - U1F4691F3FD200D1F3A8 = "👩🏽‍🎨" # :woman_artist_medium_skin_tone: - U1F469200D1F680 = "👩‍🚀" # :woman_astronaut: - U1F4691F3FF200D1F680 = "👩🏿‍🚀" # :woman_astronaut_dark_skin_tone: - U1F4691F3FB200D1F680 = "👩🏻‍🚀" # :woman_astronaut_light_skin_tone: - U1F4691F3FE200D1F680 = "👩🏾‍🚀" # :woman_astronaut_medium-dark_skin_tone: - U1F4691F3FC200D1F680 = "👩🏼‍🚀" # :woman_astronaut_medium-light_skin_tone: - U1F4691F3FD200D1F680 = "👩🏽‍🚀" # :woman_astronaut_medium_skin_tone: - U1F469200D1F9B2 = "👩‍🦲" # :woman_bald: - U1F9D4200D2640FE0F = "🧔‍♀️" # :woman_beard: - U1F9D4200D2640 = "🧔‍♀" # :woman_beard: - U1F6B4200D2640FE0F = "🚴‍♀️" # :woman_biking: - U1F6B4200D2640 = "🚴‍♀" # :woman_biking: - U1F6B41F3FF200D2640FE0F = "🚴🏿‍♀️" # :woman_biking_dark_skin_tone: - U1F6B41F3FF200D2640 = "🚴🏿‍♀" # :woman_biking_dark_skin_tone: - U1F6B41F3FB200D2640FE0F = "🚴🏻‍♀️" # :woman_biking_light_skin_tone: - U1F6B41F3FB200D2640 = "🚴🏻‍♀" # :woman_biking_light_skin_tone: - U1F6B41F3FE200D2640FE0F = "🚴🏾‍♀️" # :woman_biking_medium-dark_skin_tone: - U1F6B41F3FE200D2640 = "🚴🏾‍♀" # :woman_biking_medium-dark_skin_tone: - U1F6B41F3FC200D2640FE0F = "🚴🏼‍♀️" # :woman_biking_medium-light_skin_tone: - U1F6B41F3FC200D2640 = "🚴🏼‍♀" # :woman_biking_medium-light_skin_tone: - U1F6B41F3FD200D2640FE0F = "🚴🏽‍♀️" # :woman_biking_medium_skin_tone: - U1F6B41F3FD200D2640 = "🚴🏽‍♀" # :woman_biking_medium_skin_tone: - U1F471200D2640FE0F = "👱‍♀️" # :woman_blond_hair: - U1F471200D2640 = "👱‍♀" # :woman_blond_hair: - U26F9FE0F200D2640FE0F = "⛹️‍♀️" # :woman_bouncing_ball: - U26F9200D2640FE0F = "⛹‍♀️" # :woman_bouncing_ball: - U26F9FE0F200D2640 = "⛹️‍♀" # :woman_bouncing_ball: - U26F9200D2640 = "⛹‍♀" # :woman_bouncing_ball: - U26F91F3FF200D2640FE0F = "⛹🏿‍♀️" # :woman_bouncing_ball_dark_skin_tone: - U26F91F3FF200D2640 = "⛹🏿‍♀" # :woman_bouncing_ball_dark_skin_tone: - U26F91F3FB200D2640FE0F = "⛹🏻‍♀️" # :woman_bouncing_ball_light_skin_tone: - U26F91F3FB200D2640 = "⛹🏻‍♀" # :woman_bouncing_ball_light_skin_tone: - U26F91F3FE200D2640FE0F = "⛹🏾‍♀️" # :woman_bouncing_ball_medium-dark_skin_tone: - U26F91F3FE200D2640 = "⛹🏾‍♀" # :woman_bouncing_ball_medium-dark_skin_tone: - U26F91F3FC200D2640FE0F = "⛹🏼‍♀️" # :woman_bouncing_ball_medium-light_skin_tone: - U26F91F3FC200D2640 = "⛹🏼‍♀" # :woman_bouncing_ball_medium-light_skin_tone: - U26F91F3FD200D2640FE0F = "⛹🏽‍♀️" # :woman_bouncing_ball_medium_skin_tone: - U26F91F3FD200D2640 = "⛹🏽‍♀" # :woman_bouncing_ball_medium_skin_tone: - U1F647200D2640FE0F = "🙇‍♀️" # :woman_bowing: - U1F647200D2640 = "🙇‍♀" # :woman_bowing: - U1F6471F3FF200D2640FE0F = "🙇🏿‍♀️" # :woman_bowing_dark_skin_tone: - U1F6471F3FF200D2640 = "🙇🏿‍♀" # :woman_bowing_dark_skin_tone: - U1F6471F3FB200D2640FE0F = "🙇🏻‍♀️" # :woman_bowing_light_skin_tone: - U1F6471F3FB200D2640 = "🙇🏻‍♀" # :woman_bowing_light_skin_tone: - U1F6471F3FE200D2640FE0F = "🙇🏾‍♀️" # :woman_bowing_medium-dark_skin_tone: - U1F6471F3FE200D2640 = "🙇🏾‍♀" # :woman_bowing_medium-dark_skin_tone: - U1F6471F3FC200D2640FE0F = "🙇🏼‍♀️" # :woman_bowing_medium-light_skin_tone: - U1F6471F3FC200D2640 = "🙇🏼‍♀" # :woman_bowing_medium-light_skin_tone: - U1F6471F3FD200D2640FE0F = "🙇🏽‍♀️" # :woman_bowing_medium_skin_tone: - U1F6471F3FD200D2640 = "🙇🏽‍♀" # :woman_bowing_medium_skin_tone: - U1F938200D2640FE0F = "🤸‍♀️" # :woman_cartwheeling: - U1F938200D2640 = "🤸‍♀" # :woman_cartwheeling: - U1F9381F3FF200D2640FE0F = "🤸🏿‍♀️" # :woman_cartwheeling_dark_skin_tone: - U1F9381F3FF200D2640 = "🤸🏿‍♀" # :woman_cartwheeling_dark_skin_tone: - U1F9381F3FB200D2640FE0F = "🤸🏻‍♀️" # :woman_cartwheeling_light_skin_tone: - U1F9381F3FB200D2640 = "🤸🏻‍♀" # :woman_cartwheeling_light_skin_tone: - U1F9381F3FE200D2640FE0F = "🤸🏾‍♀️" # :woman_cartwheeling_medium-dark_skin_tone: - U1F9381F3FE200D2640 = "🤸🏾‍♀" # :woman_cartwheeling_medium-dark_skin_tone: - U1F9381F3FC200D2640FE0F = "🤸🏼‍♀️" # :woman_cartwheeling_medium-light_skin_tone: - U1F9381F3FC200D2640 = "🤸🏼‍♀" # :woman_cartwheeling_medium-light_skin_tone: - U1F9381F3FD200D2640FE0F = "🤸🏽‍♀️" # :woman_cartwheeling_medium_skin_tone: - U1F9381F3FD200D2640 = "🤸🏽‍♀" # :woman_cartwheeling_medium_skin_tone: - U1F9D7200D2640FE0F = "🧗‍♀️" # :woman_climbing: - U1F9D7200D2640 = "🧗‍♀" # :woman_climbing: - U1F9D71F3FF200D2640FE0F = "🧗🏿‍♀️" # :woman_climbing_dark_skin_tone: - U1F9D71F3FF200D2640 = "🧗🏿‍♀" # :woman_climbing_dark_skin_tone: - U1F9D71F3FB200D2640FE0F = "🧗🏻‍♀️" # :woman_climbing_light_skin_tone: - U1F9D71F3FB200D2640 = "🧗🏻‍♀" # :woman_climbing_light_skin_tone: - U1F9D71F3FE200D2640FE0F = "🧗🏾‍♀️" # :woman_climbing_medium-dark_skin_tone: - U1F9D71F3FE200D2640 = "🧗🏾‍♀" # :woman_climbing_medium-dark_skin_tone: - U1F9D71F3FC200D2640FE0F = "🧗🏼‍♀️" # :woman_climbing_medium-light_skin_tone: - U1F9D71F3FC200D2640 = "🧗🏼‍♀" # :woman_climbing_medium-light_skin_tone: - U1F9D71F3FD200D2640FE0F = "🧗🏽‍♀️" # :woman_climbing_medium_skin_tone: - U1F9D71F3FD200D2640 = "🧗🏽‍♀" # :woman_climbing_medium_skin_tone: - U1F477200D2640FE0F = "👷‍♀️" # :woman_construction_worker: - U1F477200D2640 = "👷‍♀" # :woman_construction_worker: - U1F4771F3FF200D2640FE0F = "👷🏿‍♀️" # :woman_construction_worker_dark_skin_tone: - U1F4771F3FF200D2640 = "👷🏿‍♀" # :woman_construction_worker_dark_skin_tone: - U1F4771F3FB200D2640FE0F = "👷🏻‍♀️" # :woman_construction_worker_light_skin_tone: - U1F4771F3FB200D2640 = "👷🏻‍♀" # :woman_construction_worker_light_skin_tone: - U1F4771F3FE200D2640FE0F = "👷🏾‍♀️" # :woman_construction_worker_medium-dark_skin_tone: - U1F4771F3FE200D2640 = "👷🏾‍♀" # :woman_construction_worker_medium-dark_skin_tone: - U1F4771F3FC200D2640FE0F = "👷🏼‍♀️" # :woman_construction_worker_medium-light_skin_tone: - U1F4771F3FC200D2640 = "👷🏼‍♀" # :woman_construction_worker_medium-light_skin_tone: - U1F4771F3FD200D2640FE0F = "👷🏽‍♀️" # :woman_construction_worker_medium_skin_tone: - U1F4771F3FD200D2640 = "👷🏽‍♀" # :woman_construction_worker_medium_skin_tone: - U1F469200D1F373 = "👩‍🍳" # :woman_cook: - U1F4691F3FF200D1F373 = "👩🏿‍🍳" # :woman_cook_dark_skin_tone: - U1F4691F3FB200D1F373 = "👩🏻‍🍳" # :woman_cook_light_skin_tone: - U1F4691F3FE200D1F373 = "👩🏾‍🍳" # :woman_cook_medium-dark_skin_tone: - U1F4691F3FC200D1F373 = "👩🏼‍🍳" # :woman_cook_medium-light_skin_tone: - U1F4691F3FD200D1F373 = "👩🏽‍🍳" # :woman_cook_medium_skin_tone: - U1F469200D1F9B1 = "👩‍🦱" # :woman_curly_hair: - U1F483 = "💃" # :woman_dancing: - U1F4831F3FF = "💃🏿" # :woman_dancing_dark_skin_tone: - U1F4831F3FB = "💃🏻" # :woman_dancing_light_skin_tone: - U1F4831F3FE = "💃🏾" # :woman_dancing_medium-dark_skin_tone: - U1F4831F3FC = "💃🏼" # :woman_dancing_medium-light_skin_tone: - U1F4831F3FD = "💃🏽" # :woman_dancing_medium_skin_tone: - U1F4691F3FF = "👩🏿" # :woman_dark_skin_tone: - U1F4691F3FF200D1F9B2 = "👩🏿‍🦲" # :woman_dark_skin_tone_bald: - U1F9D41F3FF200D2640FE0F = "🧔🏿‍♀️" # :woman_dark_skin_tone_beard: - U1F9D41F3FF200D2640 = "🧔🏿‍♀" # :woman_dark_skin_tone_beard: - U1F4711F3FF200D2640FE0F = "👱🏿‍♀️" # :woman_dark_skin_tone_blond_hair: - U1F4711F3FF200D2640 = "👱🏿‍♀" # :woman_dark_skin_tone_blond_hair: - U1F4691F3FF200D1F9B1 = "👩🏿‍🦱" # :woman_dark_skin_tone_curly_hair: - U1F4691F3FF200D1F9B0 = "👩🏿‍🦰" # :woman_dark_skin_tone_red_hair: - U1F4691F3FF200D1F9B3 = "👩🏿‍🦳" # :woman_dark_skin_tone_white_hair: - U1F575FE0F200D2640FE0F = "🕵️‍♀️" # :woman_detective: - U1F575200D2640FE0F = "🕵‍♀️" # :woman_detective: - U1F575FE0F200D2640 = "🕵️‍♀" # :woman_detective: - U1F575200D2640 = "🕵‍♀" # :woman_detective: - U1F5751F3FF200D2640FE0F = "🕵🏿‍♀️" # :woman_detective_dark_skin_tone: - U1F5751F3FF200D2640 = "🕵🏿‍♀" # :woman_detective_dark_skin_tone: - U1F5751F3FB200D2640FE0F = "🕵🏻‍♀️" # :woman_detective_light_skin_tone: - U1F5751F3FB200D2640 = "🕵🏻‍♀" # :woman_detective_light_skin_tone: - U1F5751F3FE200D2640FE0F = "🕵🏾‍♀️" # :woman_detective_medium-dark_skin_tone: - U1F5751F3FE200D2640 = "🕵🏾‍♀" # :woman_detective_medium-dark_skin_tone: - U1F5751F3FC200D2640FE0F = "🕵🏼‍♀️" # :woman_detective_medium-light_skin_tone: - U1F5751F3FC200D2640 = "🕵🏼‍♀" # :woman_detective_medium-light_skin_tone: - U1F5751F3FD200D2640FE0F = "🕵🏽‍♀️" # :woman_detective_medium_skin_tone: - U1F5751F3FD200D2640 = "🕵🏽‍♀" # :woman_detective_medium_skin_tone: - U1F9DD200D2640FE0F = "🧝‍♀️" # :woman_elf: - U1F9DD200D2640 = "🧝‍♀" # :woman_elf: - U1F9DD1F3FF200D2640FE0F = "🧝🏿‍♀️" # :woman_elf_dark_skin_tone: - U1F9DD1F3FF200D2640 = "🧝🏿‍♀" # :woman_elf_dark_skin_tone: - U1F9DD1F3FB200D2640FE0F = "🧝🏻‍♀️" # :woman_elf_light_skin_tone: - U1F9DD1F3FB200D2640 = "🧝🏻‍♀" # :woman_elf_light_skin_tone: - U1F9DD1F3FE200D2640FE0F = "🧝🏾‍♀️" # :woman_elf_medium-dark_skin_tone: - U1F9DD1F3FE200D2640 = "🧝🏾‍♀" # :woman_elf_medium-dark_skin_tone: - U1F9DD1F3FC200D2640FE0F = "🧝🏼‍♀️" # :woman_elf_medium-light_skin_tone: - U1F9DD1F3FC200D2640 = "🧝🏼‍♀" # :woman_elf_medium-light_skin_tone: - U1F9DD1F3FD200D2640FE0F = "🧝🏽‍♀️" # :woman_elf_medium_skin_tone: - U1F9DD1F3FD200D2640 = "🧝🏽‍♀" # :woman_elf_medium_skin_tone: - U1F926200D2640FE0F = "🤦‍♀️" # :woman_facepalming: - U1F926200D2640 = "🤦‍♀" # :woman_facepalming: - U1F9261F3FF200D2640FE0F = "🤦🏿‍♀️" # :woman_facepalming_dark_skin_tone: - U1F9261F3FF200D2640 = "🤦🏿‍♀" # :woman_facepalming_dark_skin_tone: - U1F9261F3FB200D2640FE0F = "🤦🏻‍♀️" # :woman_facepalming_light_skin_tone: - U1F9261F3FB200D2640 = "🤦🏻‍♀" # :woman_facepalming_light_skin_tone: - U1F9261F3FE200D2640FE0F = "🤦🏾‍♀️" # :woman_facepalming_medium-dark_skin_tone: - U1F9261F3FE200D2640 = "🤦🏾‍♀" # :woman_facepalming_medium-dark_skin_tone: - U1F9261F3FC200D2640FE0F = "🤦🏼‍♀️" # :woman_facepalming_medium-light_skin_tone: - U1F9261F3FC200D2640 = "🤦🏼‍♀" # :woman_facepalming_medium-light_skin_tone: - U1F9261F3FD200D2640FE0F = "🤦🏽‍♀️" # :woman_facepalming_medium_skin_tone: - U1F9261F3FD200D2640 = "🤦🏽‍♀" # :woman_facepalming_medium_skin_tone: - U1F469200D1F3ED = "👩‍🏭" # :woman_factory_worker: - U1F4691F3FF200D1F3ED = "👩🏿‍🏭" # :woman_factory_worker_dark_skin_tone: - U1F4691F3FB200D1F3ED = "👩🏻‍🏭" # :woman_factory_worker_light_skin_tone: - U1F4691F3FE200D1F3ED = "👩🏾‍🏭" # :woman_factory_worker_medium-dark_skin_tone: - U1F4691F3FC200D1F3ED = "👩🏼‍🏭" # :woman_factory_worker_medium-light_skin_tone: - U1F4691F3FD200D1F3ED = "👩🏽‍🏭" # :woman_factory_worker_medium_skin_tone: - U1F9DA200D2640FE0F = "🧚‍♀️" # :woman_fairy: - U1F9DA200D2640 = "🧚‍♀" # :woman_fairy: - U1F9DA1F3FF200D2640FE0F = "🧚🏿‍♀️" # :woman_fairy_dark_skin_tone: - U1F9DA1F3FF200D2640 = "🧚🏿‍♀" # :woman_fairy_dark_skin_tone: - U1F9DA1F3FB200D2640FE0F = "🧚🏻‍♀️" # :woman_fairy_light_skin_tone: - U1F9DA1F3FB200D2640 = "🧚🏻‍♀" # :woman_fairy_light_skin_tone: - U1F9DA1F3FE200D2640FE0F = "🧚🏾‍♀️" # :woman_fairy_medium-dark_skin_tone: - U1F9DA1F3FE200D2640 = "🧚🏾‍♀" # :woman_fairy_medium-dark_skin_tone: - U1F9DA1F3FC200D2640FE0F = "🧚🏼‍♀️" # :woman_fairy_medium-light_skin_tone: - U1F9DA1F3FC200D2640 = "🧚🏼‍♀" # :woman_fairy_medium-light_skin_tone: - U1F9DA1F3FD200D2640FE0F = "🧚🏽‍♀️" # :woman_fairy_medium_skin_tone: - U1F9DA1F3FD200D2640 = "🧚🏽‍♀" # :woman_fairy_medium_skin_tone: - U1F469200D1F33E = "👩‍🌾" # :woman_farmer: - U1F4691F3FF200D1F33E = "👩🏿‍🌾" # :woman_farmer_dark_skin_tone: - U1F4691F3FB200D1F33E = "👩🏻‍🌾" # :woman_farmer_light_skin_tone: - U1F4691F3FE200D1F33E = "👩🏾‍🌾" # :woman_farmer_medium-dark_skin_tone: - U1F4691F3FC200D1F33E = "👩🏼‍🌾" # :woman_farmer_medium-light_skin_tone: - U1F4691F3FD200D1F33E = "👩🏽‍🌾" # :woman_farmer_medium_skin_tone: - U1F469200D1F37C = "👩‍🍼" # :woman_feeding_baby: - U1F4691F3FF200D1F37C = "👩🏿‍🍼" # :woman_feeding_baby_dark_skin_tone: - U1F4691F3FB200D1F37C = "👩🏻‍🍼" # :woman_feeding_baby_light_skin_tone: - U1F4691F3FE200D1F37C = "👩🏾‍🍼" # :woman_feeding_baby_medium-dark_skin_tone: - U1F4691F3FC200D1F37C = "👩🏼‍🍼" # :woman_feeding_baby_medium-light_skin_tone: - U1F4691F3FD200D1F37C = "👩🏽‍🍼" # :woman_feeding_baby_medium_skin_tone: - U1F469200D1F692 = "👩‍🚒" # :woman_firefighter: - U1F4691F3FF200D1F692 = "👩🏿‍🚒" # :woman_firefighter_dark_skin_tone: - U1F4691F3FB200D1F692 = "👩🏻‍🚒" # :woman_firefighter_light_skin_tone: - U1F4691F3FE200D1F692 = "👩🏾‍🚒" # :woman_firefighter_medium-dark_skin_tone: - U1F4691F3FC200D1F692 = "👩🏼‍🚒" # :woman_firefighter_medium-light_skin_tone: - U1F4691F3FD200D1F692 = "👩🏽‍🚒" # :woman_firefighter_medium_skin_tone: - U1F64D200D2640FE0F = "🙍‍♀️" # :woman_frowning: - U1F64D200D2640 = "🙍‍♀" # :woman_frowning: - U1F64D1F3FF200D2640FE0F = "🙍🏿‍♀️" # :woman_frowning_dark_skin_tone: - U1F64D1F3FF200D2640 = "🙍🏿‍♀" # :woman_frowning_dark_skin_tone: - U1F64D1F3FB200D2640FE0F = "🙍🏻‍♀️" # :woman_frowning_light_skin_tone: - U1F64D1F3FB200D2640 = "🙍🏻‍♀" # :woman_frowning_light_skin_tone: - U1F64D1F3FE200D2640FE0F = "🙍🏾‍♀️" # :woman_frowning_medium-dark_skin_tone: - U1F64D1F3FE200D2640 = "🙍🏾‍♀" # :woman_frowning_medium-dark_skin_tone: - U1F64D1F3FC200D2640FE0F = "🙍🏼‍♀️" # :woman_frowning_medium-light_skin_tone: - U1F64D1F3FC200D2640 = "🙍🏼‍♀" # :woman_frowning_medium-light_skin_tone: - U1F64D1F3FD200D2640FE0F = "🙍🏽‍♀️" # :woman_frowning_medium_skin_tone: - U1F64D1F3FD200D2640 = "🙍🏽‍♀" # :woman_frowning_medium_skin_tone: - U1F9DE200D2640FE0F = "🧞‍♀️" # :woman_genie: - U1F9DE200D2640 = "🧞‍♀" # :woman_genie: - U1F645200D2640FE0F = "🙅‍♀️" # :woman_gesturing_NO: - U1F645200D2640 = "🙅‍♀" # :woman_gesturing_NO: - U1F6451F3FF200D2640FE0F = "🙅🏿‍♀️" # :woman_gesturing_NO_dark_skin_tone: - U1F6451F3FF200D2640 = "🙅🏿‍♀" # :woman_gesturing_NO_dark_skin_tone: - U1F6451F3FB200D2640FE0F = "🙅🏻‍♀️" # :woman_gesturing_NO_light_skin_tone: - U1F6451F3FB200D2640 = "🙅🏻‍♀" # :woman_gesturing_NO_light_skin_tone: - U1F6451F3FE200D2640FE0F = "🙅🏾‍♀️" # :woman_gesturing_NO_medium-dark_skin_tone: - U1F6451F3FE200D2640 = "🙅🏾‍♀" # :woman_gesturing_NO_medium-dark_skin_tone: - U1F6451F3FC200D2640FE0F = "🙅🏼‍♀️" # :woman_gesturing_NO_medium-light_skin_tone: - U1F6451F3FC200D2640 = "🙅🏼‍♀" # :woman_gesturing_NO_medium-light_skin_tone: - U1F6451F3FD200D2640FE0F = "🙅🏽‍♀️" # :woman_gesturing_NO_medium_skin_tone: - U1F6451F3FD200D2640 = "🙅🏽‍♀" # :woman_gesturing_NO_medium_skin_tone: - U1F646200D2640FE0F = "🙆‍♀️" # :woman_gesturing_OK: - U1F646200D2640 = "🙆‍♀" # :woman_gesturing_OK: - U1F6461F3FF200D2640FE0F = "🙆🏿‍♀️" # :woman_gesturing_OK_dark_skin_tone: - U1F6461F3FF200D2640 = "🙆🏿‍♀" # :woman_gesturing_OK_dark_skin_tone: - U1F6461F3FB200D2640FE0F = "🙆🏻‍♀️" # :woman_gesturing_OK_light_skin_tone: - U1F6461F3FB200D2640 = "🙆🏻‍♀" # :woman_gesturing_OK_light_skin_tone: - U1F6461F3FE200D2640FE0F = "🙆🏾‍♀️" # :woman_gesturing_OK_medium-dark_skin_tone: - U1F6461F3FE200D2640 = "🙆🏾‍♀" # :woman_gesturing_OK_medium-dark_skin_tone: - U1F6461F3FC200D2640FE0F = "🙆🏼‍♀️" # :woman_gesturing_OK_medium-light_skin_tone: - U1F6461F3FC200D2640 = "🙆🏼‍♀" # :woman_gesturing_OK_medium-light_skin_tone: - U1F6461F3FD200D2640FE0F = "🙆🏽‍♀️" # :woman_gesturing_OK_medium_skin_tone: - U1F6461F3FD200D2640 = "🙆🏽‍♀" # :woman_gesturing_OK_medium_skin_tone: - U1F487200D2640FE0F = "💇‍♀️" # :woman_getting_haircut: - U1F487200D2640 = "💇‍♀" # :woman_getting_haircut: - U1F4871F3FF200D2640FE0F = "💇🏿‍♀️" # :woman_getting_haircut_dark_skin_tone: - U1F4871F3FF200D2640 = "💇🏿‍♀" # :woman_getting_haircut_dark_skin_tone: - U1F4871F3FB200D2640FE0F = "💇🏻‍♀️" # :woman_getting_haircut_light_skin_tone: - U1F4871F3FB200D2640 = "💇🏻‍♀" # :woman_getting_haircut_light_skin_tone: - U1F4871F3FE200D2640FE0F = "💇🏾‍♀️" # :woman_getting_haircut_medium-dark_skin_tone: - U1F4871F3FE200D2640 = "💇🏾‍♀" # :woman_getting_haircut_medium-dark_skin_tone: - U1F4871F3FC200D2640FE0F = "💇🏼‍♀️" # :woman_getting_haircut_medium-light_skin_tone: - U1F4871F3FC200D2640 = "💇🏼‍♀" # :woman_getting_haircut_medium-light_skin_tone: - U1F4871F3FD200D2640FE0F = "💇🏽‍♀️" # :woman_getting_haircut_medium_skin_tone: - U1F4871F3FD200D2640 = "💇🏽‍♀" # :woman_getting_haircut_medium_skin_tone: - U1F486200D2640FE0F = "💆‍♀️" # :woman_getting_massage: - U1F486200D2640 = "💆‍♀" # :woman_getting_massage: - U1F4861F3FF200D2640FE0F = "💆🏿‍♀️" # :woman_getting_massage_dark_skin_tone: - U1F4861F3FF200D2640 = "💆🏿‍♀" # :woman_getting_massage_dark_skin_tone: - U1F4861F3FB200D2640FE0F = "💆🏻‍♀️" # :woman_getting_massage_light_skin_tone: - U1F4861F3FB200D2640 = "💆🏻‍♀" # :woman_getting_massage_light_skin_tone: - U1F4861F3FE200D2640FE0F = "💆🏾‍♀️" # :woman_getting_massage_medium-dark_skin_tone: - U1F4861F3FE200D2640 = "💆🏾‍♀" # :woman_getting_massage_medium-dark_skin_tone: - U1F4861F3FC200D2640FE0F = "💆🏼‍♀️" # :woman_getting_massage_medium-light_skin_tone: - U1F4861F3FC200D2640 = "💆🏼‍♀" # :woman_getting_massage_medium-light_skin_tone: - U1F4861F3FD200D2640FE0F = "💆🏽‍♀️" # :woman_getting_massage_medium_skin_tone: - U1F4861F3FD200D2640 = "💆🏽‍♀" # :woman_getting_massage_medium_skin_tone: - U1F3CCFE0F200D2640FE0F = "🏌️‍♀️" # :woman_golfing: - U1F3CC200D2640FE0F = "🏌‍♀️" # :woman_golfing: - U1F3CCFE0F200D2640 = "🏌️‍♀" # :woman_golfing: - U1F3CC200D2640 = "🏌‍♀" # :woman_golfing: - U1F3CC1F3FF200D2640FE0F = "🏌🏿‍♀️" # :woman_golfing_dark_skin_tone: - U1F3CC1F3FF200D2640 = "🏌🏿‍♀" # :woman_golfing_dark_skin_tone: - U1F3CC1F3FB200D2640FE0F = "🏌🏻‍♀️" # :woman_golfing_light_skin_tone: - U1F3CC1F3FB200D2640 = "🏌🏻‍♀" # :woman_golfing_light_skin_tone: - U1F3CC1F3FE200D2640FE0F = "🏌🏾‍♀️" # :woman_golfing_medium-dark_skin_tone: - U1F3CC1F3FE200D2640 = "🏌🏾‍♀" # :woman_golfing_medium-dark_skin_tone: - U1F3CC1F3FC200D2640FE0F = "🏌🏼‍♀️" # :woman_golfing_medium-light_skin_tone: - U1F3CC1F3FC200D2640 = "🏌🏼‍♀" # :woman_golfing_medium-light_skin_tone: - U1F3CC1F3FD200D2640FE0F = "🏌🏽‍♀️" # :woman_golfing_medium_skin_tone: - U1F3CC1F3FD200D2640 = "🏌🏽‍♀" # :woman_golfing_medium_skin_tone: - U1F482200D2640FE0F = "💂‍♀️" # :woman_guard: - U1F482200D2640 = "💂‍♀" # :woman_guard: - U1F4821F3FF200D2640FE0F = "💂🏿‍♀️" # :woman_guard_dark_skin_tone: - U1F4821F3FF200D2640 = "💂🏿‍♀" # :woman_guard_dark_skin_tone: - U1F4821F3FB200D2640FE0F = "💂🏻‍♀️" # :woman_guard_light_skin_tone: - U1F4821F3FB200D2640 = "💂🏻‍♀" # :woman_guard_light_skin_tone: - U1F4821F3FE200D2640FE0F = "💂🏾‍♀️" # :woman_guard_medium-dark_skin_tone: - U1F4821F3FE200D2640 = "💂🏾‍♀" # :woman_guard_medium-dark_skin_tone: - U1F4821F3FC200D2640FE0F = "💂🏼‍♀️" # :woman_guard_medium-light_skin_tone: - U1F4821F3FC200D2640 = "💂🏼‍♀" # :woman_guard_medium-light_skin_tone: - U1F4821F3FD200D2640FE0F = "💂🏽‍♀️" # :woman_guard_medium_skin_tone: - U1F4821F3FD200D2640 = "💂🏽‍♀" # :woman_guard_medium_skin_tone: - U1F469200D2695FE0F = "👩‍⚕️" # :woman_health_worker: - U1F469200D2695 = "👩‍⚕" # :woman_health_worker: - U1F4691F3FF200D2695FE0F = "👩🏿‍⚕️" # :woman_health_worker_dark_skin_tone: - U1F4691F3FF200D2695 = "👩🏿‍⚕" # :woman_health_worker_dark_skin_tone: - U1F4691F3FB200D2695FE0F = "👩🏻‍⚕️" # :woman_health_worker_light_skin_tone: - U1F4691F3FB200D2695 = "👩🏻‍⚕" # :woman_health_worker_light_skin_tone: - U1F4691F3FE200D2695FE0F = "👩🏾‍⚕️" # :woman_health_worker_medium-dark_skin_tone: - U1F4691F3FE200D2695 = "👩🏾‍⚕" # :woman_health_worker_medium-dark_skin_tone: - U1F4691F3FC200D2695FE0F = "👩🏼‍⚕️" # :woman_health_worker_medium-light_skin_tone: - U1F4691F3FC200D2695 = "👩🏼‍⚕" # :woman_health_worker_medium-light_skin_tone: - U1F4691F3FD200D2695FE0F = "👩🏽‍⚕️" # :woman_health_worker_medium_skin_tone: - U1F4691F3FD200D2695 = "👩🏽‍⚕" # :woman_health_worker_medium_skin_tone: - U1F9D8200D2640FE0F = "🧘‍♀️" # :woman_in_lotus_position: - U1F9D8200D2640 = "🧘‍♀" # :woman_in_lotus_position: - U1F9D81F3FF200D2640FE0F = "🧘🏿‍♀️" # :woman_in_lotus_position_dark_skin_tone: - U1F9D81F3FF200D2640 = "🧘🏿‍♀" # :woman_in_lotus_position_dark_skin_tone: - U1F9D81F3FB200D2640FE0F = "🧘🏻‍♀️" # :woman_in_lotus_position_light_skin_tone: - U1F9D81F3FB200D2640 = "🧘🏻‍♀" # :woman_in_lotus_position_light_skin_tone: - U1F9D81F3FE200D2640FE0F = "🧘🏾‍♀️" # :woman_in_lotus_position_medium-dark_skin_tone: - U1F9D81F3FE200D2640 = "🧘🏾‍♀" # :woman_in_lotus_position_medium-dark_skin_tone: - U1F9D81F3FC200D2640FE0F = "🧘🏼‍♀️" # :woman_in_lotus_position_medium-light_skin_tone: - U1F9D81F3FC200D2640 = "🧘🏼‍♀" # :woman_in_lotus_position_medium-light_skin_tone: - U1F9D81F3FD200D2640FE0F = "🧘🏽‍♀️" # :woman_in_lotus_position_medium_skin_tone: - U1F9D81F3FD200D2640 = "🧘🏽‍♀" # :woman_in_lotus_position_medium_skin_tone: - U1F469200D1F9BD = "👩‍🦽" # :woman_in_manual_wheelchair: - U1F4691F3FF200D1F9BD = "👩🏿‍🦽" # :woman_in_manual_wheelchair_dark_skin_tone: - U1F469200D1F9BD200D27A1FE0F = "👩‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right: - U1F469200D1F9BD200D27A1 = "👩‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right: - U1F4691F3FF200D1F9BD200D27A1FE0F = "👩🏿‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F4691F3FF200D1F9BD200D27A1 = "👩🏿‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_dark_skin_tone: - U1F4691F3FB200D1F9BD200D27A1FE0F = "👩🏻‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_light_skin_tone: - U1F4691F3FB200D1F9BD200D27A1 = "👩🏻‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_light_skin_tone: - U1F4691F3FE200D1F9BD200D27A1FE0F = "👩🏾‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F4691F3FE200D1F9BD200D27A1 = "👩🏾‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone: - U1F4691F3FC200D1F9BD200D27A1FE0F = "👩🏼‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F4691F3FC200D1F9BD200D27A1 = "👩🏼‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium-light_skin_tone: - U1F4691F3FD200D1F9BD200D27A1FE0F = "👩🏽‍🦽‍➡️" # :woman_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F4691F3FD200D1F9BD200D27A1 = "👩🏽‍🦽‍➡" # :woman_in_manual_wheelchair_facing_right_medium_skin_tone: - U1F4691F3FB200D1F9BD = "👩🏻‍🦽" # :woman_in_manual_wheelchair_light_skin_tone: - U1F4691F3FE200D1F9BD = "👩🏾‍🦽" # :woman_in_manual_wheelchair_medium-dark_skin_tone: - U1F4691F3FC200D1F9BD = "👩🏼‍🦽" # :woman_in_manual_wheelchair_medium-light_skin_tone: - U1F4691F3FD200D1F9BD = "👩🏽‍🦽" # :woman_in_manual_wheelchair_medium_skin_tone: - U1F469200D1F9BC = "👩‍🦼" # :woman_in_motorized_wheelchair: - U1F4691F3FF200D1F9BC = "👩🏿‍🦼" # :woman_in_motorized_wheelchair_dark_skin_tone: - U1F469200D1F9BC200D27A1FE0F = "👩‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right: - U1F469200D1F9BC200D27A1 = "👩‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right: - U1F4691F3FF200D1F9BC200D27A1FE0F = "👩🏿‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F4691F3FF200D1F9BC200D27A1 = "👩🏿‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_dark_skin_tone: - U1F4691F3FB200D1F9BC200D27A1FE0F = "👩🏻‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F4691F3FB200D1F9BC200D27A1 = "👩🏻‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_light_skin_tone: - U1F4691F3FE200D1F9BC200D27A1FE0F = "👩🏾‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F4691F3FE200D1F9BC200D27A1 = "👩🏾‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone: - U1F4691F3FC200D1F9BC200D27A1FE0F = "👩🏼‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F4691F3FC200D1F9BC200D27A1 = "👩🏼‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone: - U1F4691F3FD200D1F9BC200D27A1FE0F = "👩🏽‍🦼‍➡️" # :woman_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F4691F3FD200D1F9BC200D27A1 = "👩🏽‍🦼‍➡" # :woman_in_motorized_wheelchair_facing_right_medium_skin_tone: - U1F4691F3FB200D1F9BC = "👩🏻‍🦼" # :woman_in_motorized_wheelchair_light_skin_tone: - U1F4691F3FE200D1F9BC = "👩🏾‍🦼" # :woman_in_motorized_wheelchair_medium-dark_skin_tone: - U1F4691F3FC200D1F9BC = "👩🏼‍🦼" # :woman_in_motorized_wheelchair_medium-light_skin_tone: - U1F4691F3FD200D1F9BC = "👩🏽‍🦼" # :woman_in_motorized_wheelchair_medium_skin_tone: - U1F9D6200D2640FE0F = "🧖‍♀️" # :woman_in_steamy_room: - U1F9D6200D2640 = "🧖‍♀" # :woman_in_steamy_room: - U1F9D61F3FF200D2640FE0F = "🧖🏿‍♀️" # :woman_in_steamy_room_dark_skin_tone: - U1F9D61F3FF200D2640 = "🧖🏿‍♀" # :woman_in_steamy_room_dark_skin_tone: - U1F9D61F3FB200D2640FE0F = "🧖🏻‍♀️" # :woman_in_steamy_room_light_skin_tone: - U1F9D61F3FB200D2640 = "🧖🏻‍♀" # :woman_in_steamy_room_light_skin_tone: - U1F9D61F3FE200D2640FE0F = "🧖🏾‍♀️" # :woman_in_steamy_room_medium-dark_skin_tone: - U1F9D61F3FE200D2640 = "🧖🏾‍♀" # :woman_in_steamy_room_medium-dark_skin_tone: - U1F9D61F3FC200D2640FE0F = "🧖🏼‍♀️" # :woman_in_steamy_room_medium-light_skin_tone: - U1F9D61F3FC200D2640 = "🧖🏼‍♀" # :woman_in_steamy_room_medium-light_skin_tone: - U1F9D61F3FD200D2640FE0F = "🧖🏽‍♀️" # :woman_in_steamy_room_medium_skin_tone: - U1F9D61F3FD200D2640 = "🧖🏽‍♀" # :woman_in_steamy_room_medium_skin_tone: - U1F935200D2640FE0F = "🤵‍♀️" # :woman_in_tuxedo: - U1F935200D2640 = "🤵‍♀" # :woman_in_tuxedo: - U1F9351F3FF200D2640FE0F = "🤵🏿‍♀️" # :woman_in_tuxedo_dark_skin_tone: - U1F9351F3FF200D2640 = "🤵🏿‍♀" # :woman_in_tuxedo_dark_skin_tone: - U1F9351F3FB200D2640FE0F = "🤵🏻‍♀️" # :woman_in_tuxedo_light_skin_tone: - U1F9351F3FB200D2640 = "🤵🏻‍♀" # :woman_in_tuxedo_light_skin_tone: - U1F9351F3FE200D2640FE0F = "🤵🏾‍♀️" # :woman_in_tuxedo_medium-dark_skin_tone: - U1F9351F3FE200D2640 = "🤵🏾‍♀" # :woman_in_tuxedo_medium-dark_skin_tone: - U1F9351F3FC200D2640FE0F = "🤵🏼‍♀️" # :woman_in_tuxedo_medium-light_skin_tone: - U1F9351F3FC200D2640 = "🤵🏼‍♀" # :woman_in_tuxedo_medium-light_skin_tone: - U1F9351F3FD200D2640FE0F = "🤵🏽‍♀️" # :woman_in_tuxedo_medium_skin_tone: - U1F9351F3FD200D2640 = "🤵🏽‍♀" # :woman_in_tuxedo_medium_skin_tone: - U1F469200D2696FE0F = "👩‍⚖️" # :woman_judge: - U1F469200D2696 = "👩‍⚖" # :woman_judge: - U1F4691F3FF200D2696FE0F = "👩🏿‍⚖️" # :woman_judge_dark_skin_tone: - U1F4691F3FF200D2696 = "👩🏿‍⚖" # :woman_judge_dark_skin_tone: - U1F4691F3FB200D2696FE0F = "👩🏻‍⚖️" # :woman_judge_light_skin_tone: - U1F4691F3FB200D2696 = "👩🏻‍⚖" # :woman_judge_light_skin_tone: - U1F4691F3FE200D2696FE0F = "👩🏾‍⚖️" # :woman_judge_medium-dark_skin_tone: - U1F4691F3FE200D2696 = "👩🏾‍⚖" # :woman_judge_medium-dark_skin_tone: - U1F4691F3FC200D2696FE0F = "👩🏼‍⚖️" # :woman_judge_medium-light_skin_tone: - U1F4691F3FC200D2696 = "👩🏼‍⚖" # :woman_judge_medium-light_skin_tone: - U1F4691F3FD200D2696FE0F = "👩🏽‍⚖️" # :woman_judge_medium_skin_tone: - U1F4691F3FD200D2696 = "👩🏽‍⚖" # :woman_judge_medium_skin_tone: - U1F939200D2640FE0F = "🤹‍♀️" # :woman_juggling: - U1F939200D2640 = "🤹‍♀" # :woman_juggling: - U1F9391F3FF200D2640FE0F = "🤹🏿‍♀️" # :woman_juggling_dark_skin_tone: - U1F9391F3FF200D2640 = "🤹🏿‍♀" # :woman_juggling_dark_skin_tone: - U1F9391F3FB200D2640FE0F = "🤹🏻‍♀️" # :woman_juggling_light_skin_tone: - U1F9391F3FB200D2640 = "🤹🏻‍♀" # :woman_juggling_light_skin_tone: - U1F9391F3FE200D2640FE0F = "🤹🏾‍♀️" # :woman_juggling_medium-dark_skin_tone: - U1F9391F3FE200D2640 = "🤹🏾‍♀" # :woman_juggling_medium-dark_skin_tone: - U1F9391F3FC200D2640FE0F = "🤹🏼‍♀️" # :woman_juggling_medium-light_skin_tone: - U1F9391F3FC200D2640 = "🤹🏼‍♀" # :woman_juggling_medium-light_skin_tone: - U1F9391F3FD200D2640FE0F = "🤹🏽‍♀️" # :woman_juggling_medium_skin_tone: - U1F9391F3FD200D2640 = "🤹🏽‍♀" # :woman_juggling_medium_skin_tone: - U1F9CE200D2640FE0F = "🧎‍♀️" # :woman_kneeling: - U1F9CE200D2640 = "🧎‍♀" # :woman_kneeling: - U1F9CE1F3FF200D2640FE0F = "🧎🏿‍♀️" # :woman_kneeling_dark_skin_tone: - U1F9CE1F3FF200D2640 = "🧎🏿‍♀" # :woman_kneeling_dark_skin_tone: - U1F9CE200D2640FE0F200D27A1FE0F = "🧎‍♀️‍➡️" # :woman_kneeling_facing_right: - U1F9CE200D2640200D27A1FE0F = "🧎‍♀‍➡️" # :woman_kneeling_facing_right: - U1F9CE200D2640FE0F200D27A1 = "🧎‍♀️‍➡" # :woman_kneeling_facing_right: - U1F9CE200D2640200D27A1 = "🧎‍♀‍➡" # :woman_kneeling_facing_right: - U1F9CE1F3FF200D2640FE0F200D27A1FE0F = "🧎🏿‍♀️‍➡️" # :woman_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2640200D27A1FE0F = "🧎🏿‍♀‍➡️" # :woman_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2640FE0F200D27A1 = "🧎🏿‍♀️‍➡" # :woman_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FF200D2640200D27A1 = "🧎🏿‍♀‍➡" # :woman_kneeling_facing_right_dark_skin_tone: - U1F9CE1F3FB200D2640FE0F200D27A1FE0F = "🧎🏻‍♀️‍➡️" # :woman_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2640200D27A1FE0F = "🧎🏻‍♀‍➡️" # :woman_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2640FE0F200D27A1 = "🧎🏻‍♀️‍➡" # :woman_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FB200D2640200D27A1 = "🧎🏻‍♀‍➡" # :woman_kneeling_facing_right_light_skin_tone: - U1F9CE1F3FE200D2640FE0F200D27A1FE0F = "🧎🏾‍♀️‍➡️" # :woman_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2640200D27A1FE0F = "🧎🏾‍♀‍➡️" # :woman_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2640FE0F200D27A1 = "🧎🏾‍♀️‍➡" # :woman_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FE200D2640200D27A1 = "🧎🏾‍♀‍➡" # :woman_kneeling_facing_right_medium-dark_skin_tone: - U1F9CE1F3FC200D2640FE0F200D27A1FE0F = "🧎🏼‍♀️‍➡️" # :woman_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2640200D27A1FE0F = "🧎🏼‍♀‍➡️" # :woman_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2640FE0F200D27A1 = "🧎🏼‍♀️‍➡" # :woman_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FC200D2640200D27A1 = "🧎🏼‍♀‍➡" # :woman_kneeling_facing_right_medium-light_skin_tone: - U1F9CE1F3FD200D2640FE0F200D27A1FE0F = "🧎🏽‍♀️‍➡️" # :woman_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2640200D27A1FE0F = "🧎🏽‍♀‍➡️" # :woman_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2640FE0F200D27A1 = "🧎🏽‍♀️‍➡" # :woman_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FD200D2640200D27A1 = "🧎🏽‍♀‍➡" # :woman_kneeling_facing_right_medium_skin_tone: - U1F9CE1F3FB200D2640FE0F = "🧎🏻‍♀️" # :woman_kneeling_light_skin_tone: - U1F9CE1F3FB200D2640 = "🧎🏻‍♀" # :woman_kneeling_light_skin_tone: - U1F9CE1F3FE200D2640FE0F = "🧎🏾‍♀️" # :woman_kneeling_medium-dark_skin_tone: - U1F9CE1F3FE200D2640 = "🧎🏾‍♀" # :woman_kneeling_medium-dark_skin_tone: - U1F9CE1F3FC200D2640FE0F = "🧎🏼‍♀️" # :woman_kneeling_medium-light_skin_tone: - U1F9CE1F3FC200D2640 = "🧎🏼‍♀" # :woman_kneeling_medium-light_skin_tone: - U1F9CE1F3FD200D2640FE0F = "🧎🏽‍♀️" # :woman_kneeling_medium_skin_tone: - U1F9CE1F3FD200D2640 = "🧎🏽‍♀" # :woman_kneeling_medium_skin_tone: - U1F3CBFE0F200D2640FE0F = "🏋️‍♀️" # :woman_lifting_weights: - U1F3CB200D2640FE0F = "🏋‍♀️" # :woman_lifting_weights: - U1F3CBFE0F200D2640 = "🏋️‍♀" # :woman_lifting_weights: - U1F3CB200D2640 = "🏋‍♀" # :woman_lifting_weights: - U1F3CB1F3FF200D2640FE0F = "🏋🏿‍♀️" # :woman_lifting_weights_dark_skin_tone: - U1F3CB1F3FF200D2640 = "🏋🏿‍♀" # :woman_lifting_weights_dark_skin_tone: - U1F3CB1F3FB200D2640FE0F = "🏋🏻‍♀️" # :woman_lifting_weights_light_skin_tone: - U1F3CB1F3FB200D2640 = "🏋🏻‍♀" # :woman_lifting_weights_light_skin_tone: - U1F3CB1F3FE200D2640FE0F = "🏋🏾‍♀️" # :woman_lifting_weights_medium-dark_skin_tone: - U1F3CB1F3FE200D2640 = "🏋🏾‍♀" # :woman_lifting_weights_medium-dark_skin_tone: - U1F3CB1F3FC200D2640FE0F = "🏋🏼‍♀️" # :woman_lifting_weights_medium-light_skin_tone: - U1F3CB1F3FC200D2640 = "🏋🏼‍♀" # :woman_lifting_weights_medium-light_skin_tone: - U1F3CB1F3FD200D2640FE0F = "🏋🏽‍♀️" # :woman_lifting_weights_medium_skin_tone: - U1F3CB1F3FD200D2640 = "🏋🏽‍♀" # :woman_lifting_weights_medium_skin_tone: - U1F4691F3FB = "👩🏻" # :woman_light_skin_tone: - U1F4691F3FB200D1F9B2 = "👩🏻‍🦲" # :woman_light_skin_tone_bald: - U1F9D41F3FB200D2640FE0F = "🧔🏻‍♀️" # :woman_light_skin_tone_beard: - U1F9D41F3FB200D2640 = "🧔🏻‍♀" # :woman_light_skin_tone_beard: - U1F4711F3FB200D2640FE0F = "👱🏻‍♀️" # :woman_light_skin_tone_blond_hair: - U1F4711F3FB200D2640 = "👱🏻‍♀" # :woman_light_skin_tone_blond_hair: - U1F4691F3FB200D1F9B1 = "👩🏻‍🦱" # :woman_light_skin_tone_curly_hair: - U1F4691F3FB200D1F9B0 = "👩🏻‍🦰" # :woman_light_skin_tone_red_hair: - U1F4691F3FB200D1F9B3 = "👩🏻‍🦳" # :woman_light_skin_tone_white_hair: - U1F9D9200D2640FE0F = "🧙‍♀️" # :woman_mage: - U1F9D9200D2640 = "🧙‍♀" # :woman_mage: - U1F9D91F3FF200D2640FE0F = "🧙🏿‍♀️" # :woman_mage_dark_skin_tone: - U1F9D91F3FF200D2640 = "🧙🏿‍♀" # :woman_mage_dark_skin_tone: - U1F9D91F3FB200D2640FE0F = "🧙🏻‍♀️" # :woman_mage_light_skin_tone: - U1F9D91F3FB200D2640 = "🧙🏻‍♀" # :woman_mage_light_skin_tone: - U1F9D91F3FE200D2640FE0F = "🧙🏾‍♀️" # :woman_mage_medium-dark_skin_tone: - U1F9D91F3FE200D2640 = "🧙🏾‍♀" # :woman_mage_medium-dark_skin_tone: - U1F9D91F3FC200D2640FE0F = "🧙🏼‍♀️" # :woman_mage_medium-light_skin_tone: - U1F9D91F3FC200D2640 = "🧙🏼‍♀" # :woman_mage_medium-light_skin_tone: - U1F9D91F3FD200D2640FE0F = "🧙🏽‍♀️" # :woman_mage_medium_skin_tone: - U1F9D91F3FD200D2640 = "🧙🏽‍♀" # :woman_mage_medium_skin_tone: - U1F469200D1F527 = "👩‍🔧" # :woman_mechanic: - U1F4691F3FF200D1F527 = "👩🏿‍🔧" # :woman_mechanic_dark_skin_tone: - U1F4691F3FB200D1F527 = "👩🏻‍🔧" # :woman_mechanic_light_skin_tone: - U1F4691F3FE200D1F527 = "👩🏾‍🔧" # :woman_mechanic_medium-dark_skin_tone: - U1F4691F3FC200D1F527 = "👩🏼‍🔧" # :woman_mechanic_medium-light_skin_tone: - U1F4691F3FD200D1F527 = "👩🏽‍🔧" # :woman_mechanic_medium_skin_tone: - U1F4691F3FE = "👩🏾" # :woman_medium-dark_skin_tone: - U1F4691F3FE200D1F9B2 = "👩🏾‍🦲" # :woman_medium-dark_skin_tone_bald: - U1F9D41F3FE200D2640FE0F = "🧔🏾‍♀️" # :woman_medium-dark_skin_tone_beard: - U1F9D41F3FE200D2640 = "🧔🏾‍♀" # :woman_medium-dark_skin_tone_beard: - U1F4711F3FE200D2640FE0F = "👱🏾‍♀️" # :woman_medium-dark_skin_tone_blond_hair: - U1F4711F3FE200D2640 = "👱🏾‍♀" # :woman_medium-dark_skin_tone_blond_hair: - U1F4691F3FE200D1F9B1 = "👩🏾‍🦱" # :woman_medium-dark_skin_tone_curly_hair: - U1F4691F3FE200D1F9B0 = "👩🏾‍🦰" # :woman_medium-dark_skin_tone_red_hair: - U1F4691F3FE200D1F9B3 = "👩🏾‍🦳" # :woman_medium-dark_skin_tone_white_hair: - U1F4691F3FC = "👩🏼" # :woman_medium-light_skin_tone: - U1F4691F3FC200D1F9B2 = "👩🏼‍🦲" # :woman_medium-light_skin_tone_bald: - U1F9D41F3FC200D2640FE0F = "🧔🏼‍♀️" # :woman_medium-light_skin_tone_beard: - U1F9D41F3FC200D2640 = "🧔🏼‍♀" # :woman_medium-light_skin_tone_beard: - U1F4711F3FC200D2640FE0F = "👱🏼‍♀️" # :woman_medium-light_skin_tone_blond_hair: - U1F4711F3FC200D2640 = "👱🏼‍♀" # :woman_medium-light_skin_tone_blond_hair: - U1F4691F3FC200D1F9B1 = "👩🏼‍🦱" # :woman_medium-light_skin_tone_curly_hair: - U1F4691F3FC200D1F9B0 = "👩🏼‍🦰" # :woman_medium-light_skin_tone_red_hair: - U1F4691F3FC200D1F9B3 = "👩🏼‍🦳" # :woman_medium-light_skin_tone_white_hair: - U1F4691F3FD = "👩🏽" # :woman_medium_skin_tone: - U1F4691F3FD200D1F9B2 = "👩🏽‍🦲" # :woman_medium_skin_tone_bald: - U1F9D41F3FD200D2640FE0F = "🧔🏽‍♀️" # :woman_medium_skin_tone_beard: - U1F9D41F3FD200D2640 = "🧔🏽‍♀" # :woman_medium_skin_tone_beard: - U1F4711F3FD200D2640FE0F = "👱🏽‍♀️" # :woman_medium_skin_tone_blond_hair: - U1F4711F3FD200D2640 = "👱🏽‍♀" # :woman_medium_skin_tone_blond_hair: - U1F4691F3FD200D1F9B1 = "👩🏽‍🦱" # :woman_medium_skin_tone_curly_hair: - U1F4691F3FD200D1F9B0 = "👩🏽‍🦰" # :woman_medium_skin_tone_red_hair: - U1F4691F3FD200D1F9B3 = "👩🏽‍🦳" # :woman_medium_skin_tone_white_hair: - U1F6B5200D2640FE0F = "🚵‍♀️" # :woman_mountain_biking: - U1F6B5200D2640 = "🚵‍♀" # :woman_mountain_biking: - U1F6B51F3FF200D2640FE0F = "🚵🏿‍♀️" # :woman_mountain_biking_dark_skin_tone: - U1F6B51F3FF200D2640 = "🚵🏿‍♀" # :woman_mountain_biking_dark_skin_tone: - U1F6B51F3FB200D2640FE0F = "🚵🏻‍♀️" # :woman_mountain_biking_light_skin_tone: - U1F6B51F3FB200D2640 = "🚵🏻‍♀" # :woman_mountain_biking_light_skin_tone: - U1F6B51F3FE200D2640FE0F = "🚵🏾‍♀️" # :woman_mountain_biking_medium-dark_skin_tone: - U1F6B51F3FE200D2640 = "🚵🏾‍♀" # :woman_mountain_biking_medium-dark_skin_tone: - U1F6B51F3FC200D2640FE0F = "🚵🏼‍♀️" # :woman_mountain_biking_medium-light_skin_tone: - U1F6B51F3FC200D2640 = "🚵🏼‍♀" # :woman_mountain_biking_medium-light_skin_tone: - U1F6B51F3FD200D2640FE0F = "🚵🏽‍♀️" # :woman_mountain_biking_medium_skin_tone: - U1F6B51F3FD200D2640 = "🚵🏽‍♀" # :woman_mountain_biking_medium_skin_tone: - U1F469200D1F4BC = "👩‍💼" # :woman_office_worker: - U1F4691F3FF200D1F4BC = "👩🏿‍💼" # :woman_office_worker_dark_skin_tone: - U1F4691F3FB200D1F4BC = "👩🏻‍💼" # :woman_office_worker_light_skin_tone: - U1F4691F3FE200D1F4BC = "👩🏾‍💼" # :woman_office_worker_medium-dark_skin_tone: - U1F4691F3FC200D1F4BC = "👩🏼‍💼" # :woman_office_worker_medium-light_skin_tone: - U1F4691F3FD200D1F4BC = "👩🏽‍💼" # :woman_office_worker_medium_skin_tone: - U1F469200D2708FE0F = "👩‍✈️" # :woman_pilot: - U1F469200D2708 = "👩‍✈" # :woman_pilot: - U1F4691F3FF200D2708FE0F = "👩🏿‍✈️" # :woman_pilot_dark_skin_tone: - U1F4691F3FF200D2708 = "👩🏿‍✈" # :woman_pilot_dark_skin_tone: - U1F4691F3FB200D2708FE0F = "👩🏻‍✈️" # :woman_pilot_light_skin_tone: - U1F4691F3FB200D2708 = "👩🏻‍✈" # :woman_pilot_light_skin_tone: - U1F4691F3FE200D2708FE0F = "👩🏾‍✈️" # :woman_pilot_medium-dark_skin_tone: - U1F4691F3FE200D2708 = "👩🏾‍✈" # :woman_pilot_medium-dark_skin_tone: - U1F4691F3FC200D2708FE0F = "👩🏼‍✈️" # :woman_pilot_medium-light_skin_tone: - U1F4691F3FC200D2708 = "👩🏼‍✈" # :woman_pilot_medium-light_skin_tone: - U1F4691F3FD200D2708FE0F = "👩🏽‍✈️" # :woman_pilot_medium_skin_tone: - U1F4691F3FD200D2708 = "👩🏽‍✈" # :woman_pilot_medium_skin_tone: - U1F93E200D2640FE0F = "🤾‍♀️" # :woman_playing_handball: - U1F93E200D2640 = "🤾‍♀" # :woman_playing_handball: - U1F93E1F3FF200D2640FE0F = "🤾🏿‍♀️" # :woman_playing_handball_dark_skin_tone: - U1F93E1F3FF200D2640 = "🤾🏿‍♀" # :woman_playing_handball_dark_skin_tone: - U1F93E1F3FB200D2640FE0F = "🤾🏻‍♀️" # :woman_playing_handball_light_skin_tone: - U1F93E1F3FB200D2640 = "🤾🏻‍♀" # :woman_playing_handball_light_skin_tone: - U1F93E1F3FE200D2640FE0F = "🤾🏾‍♀️" # :woman_playing_handball_medium-dark_skin_tone: - U1F93E1F3FE200D2640 = "🤾🏾‍♀" # :woman_playing_handball_medium-dark_skin_tone: - U1F93E1F3FC200D2640FE0F = "🤾🏼‍♀️" # :woman_playing_handball_medium-light_skin_tone: - U1F93E1F3FC200D2640 = "🤾🏼‍♀" # :woman_playing_handball_medium-light_skin_tone: - U1F93E1F3FD200D2640FE0F = "🤾🏽‍♀️" # :woman_playing_handball_medium_skin_tone: - U1F93E1F3FD200D2640 = "🤾🏽‍♀" # :woman_playing_handball_medium_skin_tone: - U1F93D200D2640FE0F = "🤽‍♀️" # :woman_playing_water_polo: - U1F93D200D2640 = "🤽‍♀" # :woman_playing_water_polo: - U1F93D1F3FF200D2640FE0F = "🤽🏿‍♀️" # :woman_playing_water_polo_dark_skin_tone: - U1F93D1F3FF200D2640 = "🤽🏿‍♀" # :woman_playing_water_polo_dark_skin_tone: - U1F93D1F3FB200D2640FE0F = "🤽🏻‍♀️" # :woman_playing_water_polo_light_skin_tone: - U1F93D1F3FB200D2640 = "🤽🏻‍♀" # :woman_playing_water_polo_light_skin_tone: - U1F93D1F3FE200D2640FE0F = "🤽🏾‍♀️" # :woman_playing_water_polo_medium-dark_skin_tone: - U1F93D1F3FE200D2640 = "🤽🏾‍♀" # :woman_playing_water_polo_medium-dark_skin_tone: - U1F93D1F3FC200D2640FE0F = "🤽🏼‍♀️" # :woman_playing_water_polo_medium-light_skin_tone: - U1F93D1F3FC200D2640 = "🤽🏼‍♀" # :woman_playing_water_polo_medium-light_skin_tone: - U1F93D1F3FD200D2640FE0F = "🤽🏽‍♀️" # :woman_playing_water_polo_medium_skin_tone: - U1F93D1F3FD200D2640 = "🤽🏽‍♀" # :woman_playing_water_polo_medium_skin_tone: - U1F46E200D2640FE0F = "👮‍♀️" # :woman_police_officer: - U1F46E200D2640 = "👮‍♀" # :woman_police_officer: - U1F46E1F3FF200D2640FE0F = "👮🏿‍♀️" # :woman_police_officer_dark_skin_tone: - U1F46E1F3FF200D2640 = "👮🏿‍♀" # :woman_police_officer_dark_skin_tone: - U1F46E1F3FB200D2640FE0F = "👮🏻‍♀️" # :woman_police_officer_light_skin_tone: - U1F46E1F3FB200D2640 = "👮🏻‍♀" # :woman_police_officer_light_skin_tone: - U1F46E1F3FE200D2640FE0F = "👮🏾‍♀️" # :woman_police_officer_medium-dark_skin_tone: - U1F46E1F3FE200D2640 = "👮🏾‍♀" # :woman_police_officer_medium-dark_skin_tone: - U1F46E1F3FC200D2640FE0F = "👮🏼‍♀️" # :woman_police_officer_medium-light_skin_tone: - U1F46E1F3FC200D2640 = "👮🏼‍♀" # :woman_police_officer_medium-light_skin_tone: - U1F46E1F3FD200D2640FE0F = "👮🏽‍♀️" # :woman_police_officer_medium_skin_tone: - U1F46E1F3FD200D2640 = "👮🏽‍♀" # :woman_police_officer_medium_skin_tone: - U1F64E200D2640FE0F = "🙎‍♀️" # :woman_pouting: - U1F64E200D2640 = "🙎‍♀" # :woman_pouting: - U1F64E1F3FF200D2640FE0F = "🙎🏿‍♀️" # :woman_pouting_dark_skin_tone: - U1F64E1F3FF200D2640 = "🙎🏿‍♀" # :woman_pouting_dark_skin_tone: - U1F64E1F3FB200D2640FE0F = "🙎🏻‍♀️" # :woman_pouting_light_skin_tone: - U1F64E1F3FB200D2640 = "🙎🏻‍♀" # :woman_pouting_light_skin_tone: - U1F64E1F3FE200D2640FE0F = "🙎🏾‍♀️" # :woman_pouting_medium-dark_skin_tone: - U1F64E1F3FE200D2640 = "🙎🏾‍♀" # :woman_pouting_medium-dark_skin_tone: - U1F64E1F3FC200D2640FE0F = "🙎🏼‍♀️" # :woman_pouting_medium-light_skin_tone: - U1F64E1F3FC200D2640 = "🙎🏼‍♀" # :woman_pouting_medium-light_skin_tone: - U1F64E1F3FD200D2640FE0F = "🙎🏽‍♀️" # :woman_pouting_medium_skin_tone: - U1F64E1F3FD200D2640 = "🙎🏽‍♀" # :woman_pouting_medium_skin_tone: - U1F64B200D2640FE0F = "🙋‍♀️" # :woman_raising_hand: - U1F64B200D2640 = "🙋‍♀" # :woman_raising_hand: - U1F64B1F3FF200D2640FE0F = "🙋🏿‍♀️" # :woman_raising_hand_dark_skin_tone: - U1F64B1F3FF200D2640 = "🙋🏿‍♀" # :woman_raising_hand_dark_skin_tone: - U1F64B1F3FB200D2640FE0F = "🙋🏻‍♀️" # :woman_raising_hand_light_skin_tone: - U1F64B1F3FB200D2640 = "🙋🏻‍♀" # :woman_raising_hand_light_skin_tone: - U1F64B1F3FE200D2640FE0F = "🙋🏾‍♀️" # :woman_raising_hand_medium-dark_skin_tone: - U1F64B1F3FE200D2640 = "🙋🏾‍♀" # :woman_raising_hand_medium-dark_skin_tone: - U1F64B1F3FC200D2640FE0F = "🙋🏼‍♀️" # :woman_raising_hand_medium-light_skin_tone: - U1F64B1F3FC200D2640 = "🙋🏼‍♀" # :woman_raising_hand_medium-light_skin_tone: - U1F64B1F3FD200D2640FE0F = "🙋🏽‍♀️" # :woman_raising_hand_medium_skin_tone: - U1F64B1F3FD200D2640 = "🙋🏽‍♀" # :woman_raising_hand_medium_skin_tone: - U1F469200D1F9B0 = "👩‍🦰" # :woman_red_hair: - U1F6A3200D2640FE0F = "🚣‍♀️" # :woman_rowing_boat: - U1F6A3200D2640 = "🚣‍♀" # :woman_rowing_boat: - U1F6A31F3FF200D2640FE0F = "🚣🏿‍♀️" # :woman_rowing_boat_dark_skin_tone: - U1F6A31F3FF200D2640 = "🚣🏿‍♀" # :woman_rowing_boat_dark_skin_tone: - U1F6A31F3FB200D2640FE0F = "🚣🏻‍♀️" # :woman_rowing_boat_light_skin_tone: - U1F6A31F3FB200D2640 = "🚣🏻‍♀" # :woman_rowing_boat_light_skin_tone: - U1F6A31F3FE200D2640FE0F = "🚣🏾‍♀️" # :woman_rowing_boat_medium-dark_skin_tone: - U1F6A31F3FE200D2640 = "🚣🏾‍♀" # :woman_rowing_boat_medium-dark_skin_tone: - U1F6A31F3FC200D2640FE0F = "🚣🏼‍♀️" # :woman_rowing_boat_medium-light_skin_tone: - U1F6A31F3FC200D2640 = "🚣🏼‍♀" # :woman_rowing_boat_medium-light_skin_tone: - U1F6A31F3FD200D2640FE0F = "🚣🏽‍♀️" # :woman_rowing_boat_medium_skin_tone: - U1F6A31F3FD200D2640 = "🚣🏽‍♀" # :woman_rowing_boat_medium_skin_tone: - U1F3C3200D2640FE0F = "🏃‍♀️" # :woman_running: - U1F3C3200D2640 = "🏃‍♀" # :woman_running: - U1F3C31F3FF200D2640FE0F = "🏃🏿‍♀️" # :woman_running_dark_skin_tone: - U1F3C31F3FF200D2640 = "🏃🏿‍♀" # :woman_running_dark_skin_tone: - U1F3C3200D2640FE0F200D27A1FE0F = "🏃‍♀️‍➡️" # :woman_running_facing_right: - U1F3C3200D2640200D27A1FE0F = "🏃‍♀‍➡️" # :woman_running_facing_right: - U1F3C3200D2640FE0F200D27A1 = "🏃‍♀️‍➡" # :woman_running_facing_right: - U1F3C3200D2640200D27A1 = "🏃‍♀‍➡" # :woman_running_facing_right: - U1F3C31F3FF200D2640FE0F200D27A1FE0F = "🏃🏿‍♀️‍➡️" # :woman_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2640200D27A1FE0F = "🏃🏿‍♀‍➡️" # :woman_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2640FE0F200D27A1 = "🏃🏿‍♀️‍➡" # :woman_running_facing_right_dark_skin_tone: - U1F3C31F3FF200D2640200D27A1 = "🏃🏿‍♀‍➡" # :woman_running_facing_right_dark_skin_tone: - U1F3C31F3FB200D2640FE0F200D27A1FE0F = "🏃🏻‍♀️‍➡️" # :woman_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2640200D27A1FE0F = "🏃🏻‍♀‍➡️" # :woman_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2640FE0F200D27A1 = "🏃🏻‍♀️‍➡" # :woman_running_facing_right_light_skin_tone: - U1F3C31F3FB200D2640200D27A1 = "🏃🏻‍♀‍➡" # :woman_running_facing_right_light_skin_tone: - U1F3C31F3FE200D2640FE0F200D27A1FE0F = "🏃🏾‍♀️‍➡️" # :woman_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2640200D27A1FE0F = "🏃🏾‍♀‍➡️" # :woman_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2640FE0F200D27A1 = "🏃🏾‍♀️‍➡" # :woman_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FE200D2640200D27A1 = "🏃🏾‍♀‍➡" # :woman_running_facing_right_medium-dark_skin_tone: - U1F3C31F3FC200D2640FE0F200D27A1FE0F = "🏃🏼‍♀️‍➡️" # :woman_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2640200D27A1FE0F = "🏃🏼‍♀‍➡️" # :woman_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2640FE0F200D27A1 = "🏃🏼‍♀️‍➡" # :woman_running_facing_right_medium-light_skin_tone: - U1F3C31F3FC200D2640200D27A1 = "🏃🏼‍♀‍➡" # :woman_running_facing_right_medium-light_skin_tone: - U1F3C31F3FD200D2640FE0F200D27A1FE0F = "🏃🏽‍♀️‍➡️" # :woman_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2640200D27A1FE0F = "🏃🏽‍♀‍➡️" # :woman_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2640FE0F200D27A1 = "🏃🏽‍♀️‍➡" # :woman_running_facing_right_medium_skin_tone: - U1F3C31F3FD200D2640200D27A1 = "🏃🏽‍♀‍➡" # :woman_running_facing_right_medium_skin_tone: - U1F3C31F3FB200D2640FE0F = "🏃🏻‍♀️" # :woman_running_light_skin_tone: - U1F3C31F3FB200D2640 = "🏃🏻‍♀" # :woman_running_light_skin_tone: - U1F3C31F3FE200D2640FE0F = "🏃🏾‍♀️" # :woman_running_medium-dark_skin_tone: - U1F3C31F3FE200D2640 = "🏃🏾‍♀" # :woman_running_medium-dark_skin_tone: - U1F3C31F3FC200D2640FE0F = "🏃🏼‍♀️" # :woman_running_medium-light_skin_tone: - U1F3C31F3FC200D2640 = "🏃🏼‍♀" # :woman_running_medium-light_skin_tone: - U1F3C31F3FD200D2640FE0F = "🏃🏽‍♀️" # :woman_running_medium_skin_tone: - U1F3C31F3FD200D2640 = "🏃🏽‍♀" # :woman_running_medium_skin_tone: - U1F469200D1F52C = "👩‍🔬" # :woman_scientist: - U1F4691F3FF200D1F52C = "👩🏿‍🔬" # :woman_scientist_dark_skin_tone: - U1F4691F3FB200D1F52C = "👩🏻‍🔬" # :woman_scientist_light_skin_tone: - U1F4691F3FE200D1F52C = "👩🏾‍🔬" # :woman_scientist_medium-dark_skin_tone: - U1F4691F3FC200D1F52C = "👩🏼‍🔬" # :woman_scientist_medium-light_skin_tone: - U1F4691F3FD200D1F52C = "👩🏽‍🔬" # :woman_scientist_medium_skin_tone: - U1F937200D2640FE0F = "🤷‍♀️" # :woman_shrugging: - U1F937200D2640 = "🤷‍♀" # :woman_shrugging: - U1F9371F3FF200D2640FE0F = "🤷🏿‍♀️" # :woman_shrugging_dark_skin_tone: - U1F9371F3FF200D2640 = "🤷🏿‍♀" # :woman_shrugging_dark_skin_tone: - U1F9371F3FB200D2640FE0F = "🤷🏻‍♀️" # :woman_shrugging_light_skin_tone: - U1F9371F3FB200D2640 = "🤷🏻‍♀" # :woman_shrugging_light_skin_tone: - U1F9371F3FE200D2640FE0F = "🤷🏾‍♀️" # :woman_shrugging_medium-dark_skin_tone: - U1F9371F3FE200D2640 = "🤷🏾‍♀" # :woman_shrugging_medium-dark_skin_tone: - U1F9371F3FC200D2640FE0F = "🤷🏼‍♀️" # :woman_shrugging_medium-light_skin_tone: - U1F9371F3FC200D2640 = "🤷🏼‍♀" # :woman_shrugging_medium-light_skin_tone: - U1F9371F3FD200D2640FE0F = "🤷🏽‍♀️" # :woman_shrugging_medium_skin_tone: - U1F9371F3FD200D2640 = "🤷🏽‍♀" # :woman_shrugging_medium_skin_tone: - U1F469200D1F3A4 = "👩‍🎤" # :woman_singer: - U1F4691F3FF200D1F3A4 = "👩🏿‍🎤" # :woman_singer_dark_skin_tone: - U1F4691F3FB200D1F3A4 = "👩🏻‍🎤" # :woman_singer_light_skin_tone: - U1F4691F3FE200D1F3A4 = "👩🏾‍🎤" # :woman_singer_medium-dark_skin_tone: - U1F4691F3FC200D1F3A4 = "👩🏼‍🎤" # :woman_singer_medium-light_skin_tone: - U1F4691F3FD200D1F3A4 = "👩🏽‍🎤" # :woman_singer_medium_skin_tone: - U1F9CD200D2640FE0F = "🧍‍♀️" # :woman_standing: - U1F9CD200D2640 = "🧍‍♀" # :woman_standing: - U1F9CD1F3FF200D2640FE0F = "🧍🏿‍♀️" # :woman_standing_dark_skin_tone: - U1F9CD1F3FF200D2640 = "🧍🏿‍♀" # :woman_standing_dark_skin_tone: - U1F9CD1F3FB200D2640FE0F = "🧍🏻‍♀️" # :woman_standing_light_skin_tone: - U1F9CD1F3FB200D2640 = "🧍🏻‍♀" # :woman_standing_light_skin_tone: - U1F9CD1F3FE200D2640FE0F = "🧍🏾‍♀️" # :woman_standing_medium-dark_skin_tone: - U1F9CD1F3FE200D2640 = "🧍🏾‍♀" # :woman_standing_medium-dark_skin_tone: - U1F9CD1F3FC200D2640FE0F = "🧍🏼‍♀️" # :woman_standing_medium-light_skin_tone: - U1F9CD1F3FC200D2640 = "🧍🏼‍♀" # :woman_standing_medium-light_skin_tone: - U1F9CD1F3FD200D2640FE0F = "🧍🏽‍♀️" # :woman_standing_medium_skin_tone: - U1F9CD1F3FD200D2640 = "🧍🏽‍♀" # :woman_standing_medium_skin_tone: - U1F469200D1F393 = "👩‍🎓" # :woman_student: - U1F4691F3FF200D1F393 = "👩🏿‍🎓" # :woman_student_dark_skin_tone: - U1F4691F3FB200D1F393 = "👩🏻‍🎓" # :woman_student_light_skin_tone: - U1F4691F3FE200D1F393 = "👩🏾‍🎓" # :woman_student_medium-dark_skin_tone: - U1F4691F3FC200D1F393 = "👩🏼‍🎓" # :woman_student_medium-light_skin_tone: - U1F4691F3FD200D1F393 = "👩🏽‍🎓" # :woman_student_medium_skin_tone: - U1F9B8200D2640FE0F = "🦸‍♀️" # :woman_superhero: - U1F9B8200D2640 = "🦸‍♀" # :woman_superhero: - U1F9B81F3FF200D2640FE0F = "🦸🏿‍♀️" # :woman_superhero_dark_skin_tone: - U1F9B81F3FF200D2640 = "🦸🏿‍♀" # :woman_superhero_dark_skin_tone: - U1F9B81F3FB200D2640FE0F = "🦸🏻‍♀️" # :woman_superhero_light_skin_tone: - U1F9B81F3FB200D2640 = "🦸🏻‍♀" # :woman_superhero_light_skin_tone: - U1F9B81F3FE200D2640FE0F = "🦸🏾‍♀️" # :woman_superhero_medium-dark_skin_tone: - U1F9B81F3FE200D2640 = "🦸🏾‍♀" # :woman_superhero_medium-dark_skin_tone: - U1F9B81F3FC200D2640FE0F = "🦸🏼‍♀️" # :woman_superhero_medium-light_skin_tone: - U1F9B81F3FC200D2640 = "🦸🏼‍♀" # :woman_superhero_medium-light_skin_tone: - U1F9B81F3FD200D2640FE0F = "🦸🏽‍♀️" # :woman_superhero_medium_skin_tone: - U1F9B81F3FD200D2640 = "🦸🏽‍♀" # :woman_superhero_medium_skin_tone: - U1F9B9200D2640FE0F = "🦹‍♀️" # :woman_supervillain: - U1F9B9200D2640 = "🦹‍♀" # :woman_supervillain: - U1F9B91F3FF200D2640FE0F = "🦹🏿‍♀️" # :woman_supervillain_dark_skin_tone: - U1F9B91F3FF200D2640 = "🦹🏿‍♀" # :woman_supervillain_dark_skin_tone: - U1F9B91F3FB200D2640FE0F = "🦹🏻‍♀️" # :woman_supervillain_light_skin_tone: - U1F9B91F3FB200D2640 = "🦹🏻‍♀" # :woman_supervillain_light_skin_tone: - U1F9B91F3FE200D2640FE0F = "🦹🏾‍♀️" # :woman_supervillain_medium-dark_skin_tone: - U1F9B91F3FE200D2640 = "🦹🏾‍♀" # :woman_supervillain_medium-dark_skin_tone: - U1F9B91F3FC200D2640FE0F = "🦹🏼‍♀️" # :woman_supervillain_medium-light_skin_tone: - U1F9B91F3FC200D2640 = "🦹🏼‍♀" # :woman_supervillain_medium-light_skin_tone: - U1F9B91F3FD200D2640FE0F = "🦹🏽‍♀️" # :woman_supervillain_medium_skin_tone: - U1F9B91F3FD200D2640 = "🦹🏽‍♀" # :woman_supervillain_medium_skin_tone: - U1F3C4200D2640FE0F = "🏄‍♀️" # :woman_surfing: - U1F3C4200D2640 = "🏄‍♀" # :woman_surfing: - U1F3C41F3FF200D2640FE0F = "🏄🏿‍♀️" # :woman_surfing_dark_skin_tone: - U1F3C41F3FF200D2640 = "🏄🏿‍♀" # :woman_surfing_dark_skin_tone: - U1F3C41F3FB200D2640FE0F = "🏄🏻‍♀️" # :woman_surfing_light_skin_tone: - U1F3C41F3FB200D2640 = "🏄🏻‍♀" # :woman_surfing_light_skin_tone: - U1F3C41F3FE200D2640FE0F = "🏄🏾‍♀️" # :woman_surfing_medium-dark_skin_tone: - U1F3C41F3FE200D2640 = "🏄🏾‍♀" # :woman_surfing_medium-dark_skin_tone: - U1F3C41F3FC200D2640FE0F = "🏄🏼‍♀️" # :woman_surfing_medium-light_skin_tone: - U1F3C41F3FC200D2640 = "🏄🏼‍♀" # :woman_surfing_medium-light_skin_tone: - U1F3C41F3FD200D2640FE0F = "🏄🏽‍♀️" # :woman_surfing_medium_skin_tone: - U1F3C41F3FD200D2640 = "🏄🏽‍♀" # :woman_surfing_medium_skin_tone: - U1F3CA200D2640FE0F = "🏊‍♀️" # :woman_swimming: - U1F3CA200D2640 = "🏊‍♀" # :woman_swimming: - U1F3CA1F3FF200D2640FE0F = "🏊🏿‍♀️" # :woman_swimming_dark_skin_tone: - U1F3CA1F3FF200D2640 = "🏊🏿‍♀" # :woman_swimming_dark_skin_tone: - U1F3CA1F3FB200D2640FE0F = "🏊🏻‍♀️" # :woman_swimming_light_skin_tone: - U1F3CA1F3FB200D2640 = "🏊🏻‍♀" # :woman_swimming_light_skin_tone: - U1F3CA1F3FE200D2640FE0F = "🏊🏾‍♀️" # :woman_swimming_medium-dark_skin_tone: - U1F3CA1F3FE200D2640 = "🏊🏾‍♀" # :woman_swimming_medium-dark_skin_tone: - U1F3CA1F3FC200D2640FE0F = "🏊🏼‍♀️" # :woman_swimming_medium-light_skin_tone: - U1F3CA1F3FC200D2640 = "🏊🏼‍♀" # :woman_swimming_medium-light_skin_tone: - U1F3CA1F3FD200D2640FE0F = "🏊🏽‍♀️" # :woman_swimming_medium_skin_tone: - U1F3CA1F3FD200D2640 = "🏊🏽‍♀" # :woman_swimming_medium_skin_tone: - U1F469200D1F3EB = "👩‍🏫" # :woman_teacher: - U1F4691F3FF200D1F3EB = "👩🏿‍🏫" # :woman_teacher_dark_skin_tone: - U1F4691F3FB200D1F3EB = "👩🏻‍🏫" # :woman_teacher_light_skin_tone: - U1F4691F3FE200D1F3EB = "👩🏾‍🏫" # :woman_teacher_medium-dark_skin_tone: - U1F4691F3FC200D1F3EB = "👩🏼‍🏫" # :woman_teacher_medium-light_skin_tone: - U1F4691F3FD200D1F3EB = "👩🏽‍🏫" # :woman_teacher_medium_skin_tone: - U1F469200D1F4BB = "👩‍💻" # :woman_technologist: - U1F4691F3FF200D1F4BB = "👩🏿‍💻" # :woman_technologist_dark_skin_tone: - U1F4691F3FB200D1F4BB = "👩🏻‍💻" # :woman_technologist_light_skin_tone: - U1F4691F3FE200D1F4BB = "👩🏾‍💻" # :woman_technologist_medium-dark_skin_tone: - U1F4691F3FC200D1F4BB = "👩🏼‍💻" # :woman_technologist_medium-light_skin_tone: - U1F4691F3FD200D1F4BB = "👩🏽‍💻" # :woman_technologist_medium_skin_tone: - U1F481200D2640FE0F = "💁‍♀️" # :woman_tipping_hand: - U1F481200D2640 = "💁‍♀" # :woman_tipping_hand: - U1F4811F3FF200D2640FE0F = "💁🏿‍♀️" # :woman_tipping_hand_dark_skin_tone: - U1F4811F3FF200D2640 = "💁🏿‍♀" # :woman_tipping_hand_dark_skin_tone: - U1F4811F3FB200D2640FE0F = "💁🏻‍♀️" # :woman_tipping_hand_light_skin_tone: - U1F4811F3FB200D2640 = "💁🏻‍♀" # :woman_tipping_hand_light_skin_tone: - U1F4811F3FE200D2640FE0F = "💁🏾‍♀️" # :woman_tipping_hand_medium-dark_skin_tone: - U1F4811F3FE200D2640 = "💁🏾‍♀" # :woman_tipping_hand_medium-dark_skin_tone: - U1F4811F3FC200D2640FE0F = "💁🏼‍♀️" # :woman_tipping_hand_medium-light_skin_tone: - U1F4811F3FC200D2640 = "💁🏼‍♀" # :woman_tipping_hand_medium-light_skin_tone: - U1F4811F3FD200D2640FE0F = "💁🏽‍♀️" # :woman_tipping_hand_medium_skin_tone: - U1F4811F3FD200D2640 = "💁🏽‍♀" # :woman_tipping_hand_medium_skin_tone: - U1F9DB200D2640FE0F = "🧛‍♀️" # :woman_vampire: - U1F9DB200D2640 = "🧛‍♀" # :woman_vampire: - U1F9DB1F3FF200D2640FE0F = "🧛🏿‍♀️" # :woman_vampire_dark_skin_tone: - U1F9DB1F3FF200D2640 = "🧛🏿‍♀" # :woman_vampire_dark_skin_tone: - U1F9DB1F3FB200D2640FE0F = "🧛🏻‍♀️" # :woman_vampire_light_skin_tone: - U1F9DB1F3FB200D2640 = "🧛🏻‍♀" # :woman_vampire_light_skin_tone: - U1F9DB1F3FE200D2640FE0F = "🧛🏾‍♀️" # :woman_vampire_medium-dark_skin_tone: - U1F9DB1F3FE200D2640 = "🧛🏾‍♀" # :woman_vampire_medium-dark_skin_tone: - U1F9DB1F3FC200D2640FE0F = "🧛🏼‍♀️" # :woman_vampire_medium-light_skin_tone: - U1F9DB1F3FC200D2640 = "🧛🏼‍♀" # :woman_vampire_medium-light_skin_tone: - U1F9DB1F3FD200D2640FE0F = "🧛🏽‍♀️" # :woman_vampire_medium_skin_tone: - U1F9DB1F3FD200D2640 = "🧛🏽‍♀" # :woman_vampire_medium_skin_tone: - U1F6B6200D2640FE0F = "🚶‍♀️" # :woman_walking: - U1F6B6200D2640 = "🚶‍♀" # :woman_walking: - U1F6B61F3FF200D2640FE0F = "🚶🏿‍♀️" # :woman_walking_dark_skin_tone: - U1F6B61F3FF200D2640 = "🚶🏿‍♀" # :woman_walking_dark_skin_tone: - U1F6B6200D2640FE0F200D27A1FE0F = "🚶‍♀️‍➡️" # :woman_walking_facing_right: - U1F6B6200D2640200D27A1FE0F = "🚶‍♀‍➡️" # :woman_walking_facing_right: - U1F6B6200D2640FE0F200D27A1 = "🚶‍♀️‍➡" # :woman_walking_facing_right: - U1F6B6200D2640200D27A1 = "🚶‍♀‍➡" # :woman_walking_facing_right: - U1F6B61F3FF200D2640FE0F200D27A1FE0F = "🚶🏿‍♀️‍➡️" # :woman_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2640200D27A1FE0F = "🚶🏿‍♀‍➡️" # :woman_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2640FE0F200D27A1 = "🚶🏿‍♀️‍➡" # :woman_walking_facing_right_dark_skin_tone: - U1F6B61F3FF200D2640200D27A1 = "🚶🏿‍♀‍➡" # :woman_walking_facing_right_dark_skin_tone: - U1F6B61F3FB200D2640FE0F200D27A1FE0F = "🚶🏻‍♀️‍➡️" # :woman_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2640200D27A1FE0F = "🚶🏻‍♀‍➡️" # :woman_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2640FE0F200D27A1 = "🚶🏻‍♀️‍➡" # :woman_walking_facing_right_light_skin_tone: - U1F6B61F3FB200D2640200D27A1 = "🚶🏻‍♀‍➡" # :woman_walking_facing_right_light_skin_tone: - U1F6B61F3FE200D2640FE0F200D27A1FE0F = "🚶🏾‍♀️‍➡️" # :woman_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2640200D27A1FE0F = "🚶🏾‍♀‍➡️" # :woman_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2640FE0F200D27A1 = "🚶🏾‍♀️‍➡" # :woman_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FE200D2640200D27A1 = "🚶🏾‍♀‍➡" # :woman_walking_facing_right_medium-dark_skin_tone: - U1F6B61F3FC200D2640FE0F200D27A1FE0F = "🚶🏼‍♀️‍➡️" # :woman_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2640200D27A1FE0F = "🚶🏼‍♀‍➡️" # :woman_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2640FE0F200D27A1 = "🚶🏼‍♀️‍➡" # :woman_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FC200D2640200D27A1 = "🚶🏼‍♀‍➡" # :woman_walking_facing_right_medium-light_skin_tone: - U1F6B61F3FD200D2640FE0F200D27A1FE0F = "🚶🏽‍♀️‍➡️" # :woman_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2640200D27A1FE0F = "🚶🏽‍♀‍➡️" # :woman_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2640FE0F200D27A1 = "🚶🏽‍♀️‍➡" # :woman_walking_facing_right_medium_skin_tone: - U1F6B61F3FD200D2640200D27A1 = "🚶🏽‍♀‍➡" # :woman_walking_facing_right_medium_skin_tone: - U1F6B61F3FB200D2640FE0F = "🚶🏻‍♀️" # :woman_walking_light_skin_tone: - U1F6B61F3FB200D2640 = "🚶🏻‍♀" # :woman_walking_light_skin_tone: - U1F6B61F3FE200D2640FE0F = "🚶🏾‍♀️" # :woman_walking_medium-dark_skin_tone: - U1F6B61F3FE200D2640 = "🚶🏾‍♀" # :woman_walking_medium-dark_skin_tone: - U1F6B61F3FC200D2640FE0F = "🚶🏼‍♀️" # :woman_walking_medium-light_skin_tone: - U1F6B61F3FC200D2640 = "🚶🏼‍♀" # :woman_walking_medium-light_skin_tone: - U1F6B61F3FD200D2640FE0F = "🚶🏽‍♀️" # :woman_walking_medium_skin_tone: - U1F6B61F3FD200D2640 = "🚶🏽‍♀" # :woman_walking_medium_skin_tone: - U1F473200D2640FE0F = "👳‍♀️" # :woman_wearing_turban: - U1F473200D2640 = "👳‍♀" # :woman_wearing_turban: - U1F4731F3FF200D2640FE0F = "👳🏿‍♀️" # :woman_wearing_turban_dark_skin_tone: - U1F4731F3FF200D2640 = "👳🏿‍♀" # :woman_wearing_turban_dark_skin_tone: - U1F4731F3FB200D2640FE0F = "👳🏻‍♀️" # :woman_wearing_turban_light_skin_tone: - U1F4731F3FB200D2640 = "👳🏻‍♀" # :woman_wearing_turban_light_skin_tone: - U1F4731F3FE200D2640FE0F = "👳🏾‍♀️" # :woman_wearing_turban_medium-dark_skin_tone: - U1F4731F3FE200D2640 = "👳🏾‍♀" # :woman_wearing_turban_medium-dark_skin_tone: - U1F4731F3FC200D2640FE0F = "👳🏼‍♀️" # :woman_wearing_turban_medium-light_skin_tone: - U1F4731F3FC200D2640 = "👳🏼‍♀" # :woman_wearing_turban_medium-light_skin_tone: - U1F4731F3FD200D2640FE0F = "👳🏽‍♀️" # :woman_wearing_turban_medium_skin_tone: - U1F4731F3FD200D2640 = "👳🏽‍♀" # :woman_wearing_turban_medium_skin_tone: - U1F469200D1F9B3 = "👩‍🦳" # :woman_white_hair: - U1F9D5 = "🧕" # :woman_with_headscarf: - U1F9D51F3FF = "🧕🏿" # :woman_with_headscarf_dark_skin_tone: - U1F9D51F3FB = "🧕🏻" # :woman_with_headscarf_light_skin_tone: - U1F9D51F3FE = "🧕🏾" # :woman_with_headscarf_medium-dark_skin_tone: - U1F9D51F3FC = "🧕🏼" # :woman_with_headscarf_medium-light_skin_tone: - U1F9D51F3FD = "🧕🏽" # :woman_with_headscarf_medium_skin_tone: - U1F470200D2640FE0F = "👰‍♀️" # :woman_with_veil: - U1F470200D2640 = "👰‍♀" # :woman_with_veil: - U1F4701F3FF200D2640FE0F = "👰🏿‍♀️" # :woman_with_veil_dark_skin_tone: - U1F4701F3FF200D2640 = "👰🏿‍♀" # :woman_with_veil_dark_skin_tone: - U1F4701F3FB200D2640FE0F = "👰🏻‍♀️" # :woman_with_veil_light_skin_tone: - U1F4701F3FB200D2640 = "👰🏻‍♀" # :woman_with_veil_light_skin_tone: - U1F4701F3FE200D2640FE0F = "👰🏾‍♀️" # :woman_with_veil_medium-dark_skin_tone: - U1F4701F3FE200D2640 = "👰🏾‍♀" # :woman_with_veil_medium-dark_skin_tone: - U1F4701F3FC200D2640FE0F = "👰🏼‍♀️" # :woman_with_veil_medium-light_skin_tone: - U1F4701F3FC200D2640 = "👰🏼‍♀" # :woman_with_veil_medium-light_skin_tone: - U1F4701F3FD200D2640FE0F = "👰🏽‍♀️" # :woman_with_veil_medium_skin_tone: - U1F4701F3FD200D2640 = "👰🏽‍♀" # :woman_with_veil_medium_skin_tone: - U1F469200D1F9AF = "👩‍🦯" # :woman_with_white_cane: - U1F4691F3FF200D1F9AF = "👩🏿‍🦯" # :woman_with_white_cane_dark_skin_tone: - U1F469200D1F9AF200D27A1FE0F = "👩‍🦯‍➡️" # :woman_with_white_cane_facing_right: - U1F469200D1F9AF200D27A1 = "👩‍🦯‍➡" # :woman_with_white_cane_facing_right: - U1F4691F3FF200D1F9AF200D27A1FE0F = "👩🏿‍🦯‍➡️" # :woman_with_white_cane_facing_right_dark_skin_tone: - U1F4691F3FF200D1F9AF200D27A1 = "👩🏿‍🦯‍➡" # :woman_with_white_cane_facing_right_dark_skin_tone: - U1F4691F3FB200D1F9AF200D27A1FE0F = "👩🏻‍🦯‍➡️" # :woman_with_white_cane_facing_right_light_skin_tone: - U1F4691F3FB200D1F9AF200D27A1 = "👩🏻‍🦯‍➡" # :woman_with_white_cane_facing_right_light_skin_tone: - U1F4691F3FE200D1F9AF200D27A1FE0F = "👩🏾‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium-dark_skin_tone: - U1F4691F3FE200D1F9AF200D27A1 = "👩🏾‍🦯‍➡" # :woman_with_white_cane_facing_right_medium-dark_skin_tone: - U1F4691F3FC200D1F9AF200D27A1FE0F = "👩🏼‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium-light_skin_tone: - U1F4691F3FC200D1F9AF200D27A1 = "👩🏼‍🦯‍➡" # :woman_with_white_cane_facing_right_medium-light_skin_tone: - U1F4691F3FD200D1F9AF200D27A1FE0F = "👩🏽‍🦯‍➡️" # :woman_with_white_cane_facing_right_medium_skin_tone: - U1F4691F3FD200D1F9AF200D27A1 = "👩🏽‍🦯‍➡" # :woman_with_white_cane_facing_right_medium_skin_tone: - U1F4691F3FB200D1F9AF = "👩🏻‍🦯" # :woman_with_white_cane_light_skin_tone: - U1F4691F3FE200D1F9AF = "👩🏾‍🦯" # :woman_with_white_cane_medium-dark_skin_tone: - U1F4691F3FC200D1F9AF = "👩🏼‍🦯" # :woman_with_white_cane_medium-light_skin_tone: - U1F4691F3FD200D1F9AF = "👩🏽‍🦯" # :woman_with_white_cane_medium_skin_tone: - U1F9DF200D2640FE0F = "🧟‍♀️" # :woman_zombie: - U1F9DF200D2640 = "🧟‍♀" # :woman_zombie: - U1F462 = "👢" # :woman’s_boot: - U1F45A = "👚" # :woman’s_clothes: - U1F452 = "👒" # :woman’s_hat: - U1F461 = "👡" # :woman’s_sandal: - U1F46D = "👭" # :women_holding_hands: - U1F46D1F3FF = "👭🏿" # :women_holding_hands_dark_skin_tone: - U1F4691F3FF200D1F91D200D1F4691F3FB = "👩🏿‍🤝‍👩🏻" # :women_holding_hands_dark_skin_tone_light_skin_tone: - U1F4691F3FF200D1F91D200D1F4691F3FE = "👩🏿‍🤝‍👩🏾" # :women_holding_hands_dark_skin_tone_medium-dark_skin_tone: - U1F4691F3FF200D1F91D200D1F4691F3FC = "👩🏿‍🤝‍👩🏼" # :women_holding_hands_dark_skin_tone_medium-light_skin_tone: - U1F4691F3FF200D1F91D200D1F4691F3FD = "👩🏿‍🤝‍👩🏽" # :women_holding_hands_dark_skin_tone_medium_skin_tone: - U1F46D1F3FB = "👭🏻" # :women_holding_hands_light_skin_tone: - U1F4691F3FB200D1F91D200D1F4691F3FF = "👩🏻‍🤝‍👩🏿" # :women_holding_hands_light_skin_tone_dark_skin_tone: - U1F4691F3FB200D1F91D200D1F4691F3FE = "👩🏻‍🤝‍👩🏾" # :women_holding_hands_light_skin_tone_medium-dark_skin_tone: - U1F4691F3FB200D1F91D200D1F4691F3FC = "👩🏻‍🤝‍👩🏼" # :women_holding_hands_light_skin_tone_medium-light_skin_tone: - U1F4691F3FB200D1F91D200D1F4691F3FD = "👩🏻‍🤝‍👩🏽" # :women_holding_hands_light_skin_tone_medium_skin_tone: - U1F46D1F3FE = "👭🏾" # :women_holding_hands_medium-dark_skin_tone: - U1F4691F3FE200D1F91D200D1F4691F3FF = "👩🏾‍🤝‍👩🏿" # :women_holding_hands_medium-dark_skin_tone_dark_skin_tone: - U1F4691F3FE200D1F91D200D1F4691F3FB = "👩🏾‍🤝‍👩🏻" # :women_holding_hands_medium-dark_skin_tone_light_skin_tone: - U1F4691F3FE200D1F91D200D1F4691F3FC = "👩🏾‍🤝‍👩🏼" # :women_holding_hands_medium-dark_skin_tone_medium-light_skin_tone: - U1F4691F3FE200D1F91D200D1F4691F3FD = "👩🏾‍🤝‍👩🏽" # :women_holding_hands_medium-dark_skin_tone_medium_skin_tone: - U1F46D1F3FC = "👭🏼" # :women_holding_hands_medium-light_skin_tone: - U1F4691F3FC200D1F91D200D1F4691F3FF = "👩🏼‍🤝‍👩🏿" # :women_holding_hands_medium-light_skin_tone_dark_skin_tone: - U1F4691F3FC200D1F91D200D1F4691F3FB = "👩🏼‍🤝‍👩🏻" # :women_holding_hands_medium-light_skin_tone_light_skin_tone: - U1F4691F3FC200D1F91D200D1F4691F3FE = "👩🏼‍🤝‍👩🏾" # :women_holding_hands_medium-light_skin_tone_medium-dark_skin_tone: - U1F4691F3FC200D1F91D200D1F4691F3FD = "👩🏼‍🤝‍👩🏽" # :women_holding_hands_medium-light_skin_tone_medium_skin_tone: - U1F46D1F3FD = "👭🏽" # :women_holding_hands_medium_skin_tone: - U1F4691F3FD200D1F91D200D1F4691F3FF = "👩🏽‍🤝‍👩🏿" # :women_holding_hands_medium_skin_tone_dark_skin_tone: - U1F4691F3FD200D1F91D200D1F4691F3FB = "👩🏽‍🤝‍👩🏻" # :women_holding_hands_medium_skin_tone_light_skin_tone: - U1F4691F3FD200D1F91D200D1F4691F3FE = "👩🏽‍🤝‍👩🏾" # :women_holding_hands_medium_skin_tone_medium-dark_skin_tone: - U1F4691F3FD200D1F91D200D1F4691F3FC = "👩🏽‍🤝‍👩🏼" # :women_holding_hands_medium_skin_tone_medium-light_skin_tone: - U1F46F200D2640FE0F = "👯‍♀️" # :women_with_bunny_ears: - U1F46F200D2640 = "👯‍♀" # :women_with_bunny_ears: - U1F93C200D2640FE0F = "🤼‍♀️" # :women_wrestling: - U1F93C200D2640 = "🤼‍♀" # :women_wrestling: - U1F6BA = "🚺" # :women’s_room: - U1FAB5 = "🪵" # :wood: - U1F974 = "🥴" # :woozy_face: - U1F5FAFE0F = "🗺️" # :world_map: - U1F5FA = "🗺" # :world_map: - U1FAB1 = "🪱" # :worm: - U1F61F = "😟" # :worried_face: - U1F381 = "🎁" # :wrapped_gift: - U1F527 = "🔧" # :wrench: - U270DFE0F = "✍️" # :writing_hand: - U270D = "✍" # :writing_hand: - U270D1F3FF = "✍🏿" # :writing_hand_dark_skin_tone: - U270D1F3FB = "✍🏻" # :writing_hand_light_skin_tone: - U270D1F3FE = "✍🏾" # :writing_hand_medium-dark_skin_tone: - U270D1F3FC = "✍🏼" # :writing_hand_medium-light_skin_tone: - U270D1F3FD = "✍🏽" # :writing_hand_medium_skin_tone: - U1FA7B = "🩻" # :x-ray: - U1F9F6 = "🧶" # :yarn: - U1F971 = "🥱" # :yawning_face: - U1F7E1 = "🟡" # :yellow_circle: - U1F49B = "💛" # :yellow_heart: - U1F7E8 = "🟨" # :yellow_square: - U1F4B4 = "💴" # :yen_banknote: - U262FFE0F = "☯️" # :yin_yang: - U262F = "☯" # :yin_yang: - U1FA80 = "🪀" # :yo-yo: - U1F92A = "🤪" # :zany_face: - U1F993 = "🦓" # :zebra: - U1F910 = "🤐" # :zipper-mouth_face: - U1F9DF = "🧟" # :zombie: - U1F1E61F1FD = "🇦🇽" # :Åland_Islands: +class EmojiChoices(TextChoices): + U1F947 = "🥇", "🥇 (:1st_place_medal:)" + U1F948 = "🥈", "🥈 (:2nd_place_medal:)" + U1F949 = "🥉", "🥉 (:3rd_place_medal:)" + U1F18E = "🆎", "🆎 (:AB_button_(blood_type):)" + U1F3E7 = "🏧", "🏧 (:ATM_sign:)" + U1F170FE0F = "🅰️", "🅰️ (:A_button_(blood_type):)" + U1F170 = "🅰", "🅰 (:A_button_(blood_type):)" + U1F1E61F1EB = "🇦🇫", "🇦🇫 (:Afghanistan:)" + U1F1E61F1F1 = "🇦🇱", "🇦🇱 (:Albania:)" + U1F1E91F1FF = "🇩🇿", "🇩🇿 (:Algeria:)" + U1F1E61F1F8 = "🇦🇸", "🇦🇸 (:American_Samoa:)" + U1F1E61F1E9 = "🇦🇩", "🇦🇩 (:Andorra:)" + U1F1E61F1F4 = "🇦🇴", "🇦🇴 (:Angola:)" + U1F1E61F1EE = "🇦🇮", "🇦🇮 (:Anguilla:)" + U1F1E61F1F6 = "🇦🇶", "🇦🇶 (:Antarctica:)" + U1F1E61F1EC = "🇦🇬", "🇦🇬 (:Antigua_&_Barbuda:)" + U2652 = "♒", "♒ (:Aquarius:)" + U1F1E61F1F7 = "🇦🇷", "🇦🇷 (:Argentina:)" + U2648 = "♈", "♈ (:Aries:)" + U1F1E61F1F2 = "🇦🇲", "🇦🇲 (:Armenia:)" + U1F1E61F1FC = "🇦🇼", "🇦🇼 (:Aruba:)" + U1F1E61F1E8 = "🇦🇨", "🇦🇨 (:Ascension_Island:)" + U1F1E61F1FA = "🇦🇺", "🇦🇺 (:Australia:)" + U1F1E61F1F9 = "🇦🇹", "🇦🇹 (:Austria:)" + U1F1E61F1FF = "🇦🇿", "🇦🇿 (:Azerbaijan:)" + U1F519 = "🔙", "🔙 (:BACK_arrow:)" + U1F171FE0F = "🅱️", "🅱️ (:B_button_(blood_type):)" + U1F171 = "🅱", "🅱 (:B_button_(blood_type):)" + U1F1E71F1F8 = "🇧🇸", "🇧🇸 (:Bahamas:)" + U1F1E71F1ED = "🇧🇭", "🇧🇭 (:Bahrain:)" + U1F1E71F1E9 = "🇧🇩", "🇧🇩 (:Bangladesh:)" + U1F1E71F1E7 = "🇧🇧", "🇧🇧 (:Barbados:)" + U1F1E71F1FE = "🇧🇾", "🇧🇾 (:Belarus:)" + U1F1E71F1EA = "🇧🇪", "🇧🇪 (:Belgium:)" + U1F1E71F1FF = "🇧🇿", "🇧🇿 (:Belize:)" + U1F1E71F1EF = "🇧🇯", "🇧🇯 (:Benin:)" + U1F1E71F1F2 = "🇧🇲", "🇧🇲 (:Bermuda:)" + U1F1E71F1F9 = "🇧🇹", "🇧🇹 (:Bhutan:)" + U1F1E71F1F4 = "🇧🇴", "🇧🇴 (:Bolivia:)" + U1F1E71F1E6 = "🇧🇦", "🇧🇦 (:Bosnia_&_Herzegovina:)" + U1F1E71F1FC = "🇧🇼", "🇧🇼 (:Botswana:)" + U1F1E71F1FB = "🇧🇻", "🇧🇻 (:Bouvet_Island:)" + U1F1E71F1F7 = "🇧🇷", "🇧🇷 (:Brazil:)" + U1F1EE1F1F4 = "🇮🇴", "🇮🇴 (:British_Indian_Ocean_Territory:)" + U1F1FB1F1EC = "🇻🇬", "🇻🇬 (:British_Virgin_Islands:)" + U1F1E71F1F3 = "🇧🇳", "🇧🇳 (:Brunei:)" + U1F1E71F1EC = "🇧🇬", "🇧🇬 (:Bulgaria:)" + U1F1E71F1EB = "🇧🇫", "🇧🇫 (:Burkina_Faso:)" + U1F1E71F1EE = "🇧🇮", "🇧🇮 (:Burundi:)" + U1F191 = "🆑", "🆑 (:CL_button:)" + U1F192 = "🆒", "🆒 (:COOL_button:)" + U1F1F01F1ED = "🇰🇭", "🇰🇭 (:Cambodia:)" + U1F1E81F1F2 = "🇨🇲", "🇨🇲 (:Cameroon:)" + U1F1E81F1E6 = "🇨🇦", "🇨🇦 (:Canada:)" + U1F1EE1F1E8 = "🇮🇨", "🇮🇨 (:Canary_Islands:)" + U264B = "♋", "♋ (:Cancer:)" + U1F1E81F1FB = "🇨🇻", "🇨🇻 (:Cape_Verde:)" + U2651 = "♑", "♑ (:Capricorn:)" + U1F1E71F1F6 = "🇧🇶", "🇧🇶 (:Caribbean_Netherlands:)" + U1F1F01F1FE = "🇰🇾", "🇰🇾 (:Cayman_Islands:)" + U1F1E81F1EB = "🇨🇫", "🇨🇫 (:Central_African_Republic:)" + U1F1EA1F1E6 = "🇪🇦", "🇪🇦 (:Ceuta_&_Melilla:)" + U1F1F91F1E9 = "🇹🇩", "🇹🇩 (:Chad:)" + U1F1E81F1F1 = "🇨🇱", "🇨🇱 (:Chile:)" + U1F1E81F1F3 = "🇨🇳", "🇨🇳 (:China:)" + U1F1E81F1FD = "🇨🇽", "🇨🇽 (:Christmas_Island:)" + U1F384 = "🎄", "🎄 (:Christmas_tree:)" + U1F1E81F1F5 = "🇨🇵", "🇨🇵 (:Clipperton_Island:)" + U1F1E81F1E8 = "🇨🇨", "🇨🇨 (:Cocos_(Keeling)_Islands:)" + U1F1E81F1F4 = "🇨🇴", "🇨🇴 (:Colombia:)" + U1F1F01F1F2 = "🇰🇲", "🇰🇲 (:Comoros:)" + U1F1E81F1EC = "🇨🇬", "🇨🇬 (:Congo-Brazzaville:)" + U1F1E81F1E9 = "🇨🇩", "🇨🇩 (:Congo-Kinshasa:)" + U1F1E81F1F0 = "🇨🇰", "🇨🇰 (:Cook_Islands:)" + U1F1E81F1F7 = "🇨🇷", "🇨🇷 (:Costa_Rica:)" + U1F1ED1F1F7 = "🇭🇷", "🇭🇷 (:Croatia:)" + U1F1E81F1FA = "🇨🇺", "🇨🇺 (:Cuba:)" + U1F1E81F1FC = "🇨🇼", "🇨🇼 (:Curaçao:)" + U1F1E81F1FE = "🇨🇾", "🇨🇾 (:Cyprus:)" + U1F1E81F1FF = "🇨🇿", "🇨🇿 (:Czechia:)" + U1F1E81F1EE = "🇨🇮", "🇨🇮 (:Côte_d’Ivoire:)" + U1F1E91F1F0 = "🇩🇰", "🇩🇰 (:Denmark:)" + U1F1E91F1EC = "🇩🇬", "🇩🇬 (:Diego_Garcia:)" + U1F1E91F1EF = "🇩🇯", "🇩🇯 (:Djibouti:)" + U1F1E91F1F2 = "🇩🇲", "🇩🇲 (:Dominica:)" + U1F1E91F1F4 = "🇩🇴", "🇩🇴 (:Dominican_Republic:)" + U1F51A = "🔚", "🔚 (:END_arrow:)" + U1F1EA1F1E8 = "🇪🇨", "🇪🇨 (:Ecuador:)" + U1F1EA1F1EC = "🇪🇬", "🇪🇬 (:Egypt:)" + U1F1F81F1FB = "🇸🇻", "🇸🇻 (:El_Salvador:)" + U1F3F4E0067E0062E0065E006EE0067E007F = "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "🏴󠁧󠁢󠁥󠁮󠁧󠁿 (:England:)" + U1F1EC1F1F6 = "🇬🇶", "🇬🇶 (:Equatorial_Guinea:)" + U1F1EA1F1F7 = "🇪🇷", "🇪🇷 (:Eritrea:)" + U1F1EA1F1EA = "🇪🇪", "🇪🇪 (:Estonia:)" + U1F1F81F1FF = "🇸🇿", "🇸🇿 (:Eswatini:)" + U1F1EA1F1F9 = "🇪🇹", "🇪🇹 (:Ethiopia:)" + U1F1EA1F1FA = "🇪🇺", "🇪🇺 (:European_Union:)" + U1F193 = "🆓", "🆓 (:FREE_button:)" + U1F1EB1F1F0 = "🇫🇰", "🇫🇰 (:Falkland_Islands:)" + U1F1EB1F1F4 = "🇫🇴", "🇫🇴 (:Faroe_Islands:)" + U1F1EB1F1EF = "🇫🇯", "🇫🇯 (:Fiji:)" + U1F1EB1F1EE = "🇫🇮", "🇫🇮 (:Finland:)" + U1F1EB1F1F7 = "🇫🇷", "🇫🇷 (:France:)" + U1F1EC1F1EB = "🇬🇫", "🇬🇫 (:French_Guiana:)" + U1F1F51F1EB = "🇵🇫", "🇵🇫 (:French_Polynesia:)" + U1F1F91F1EB = "🇹🇫", "🇹🇫 (:French_Southern_Territories:)" + U1F1EC1F1E6 = "🇬🇦", "🇬🇦 (:Gabon:)" + U1F1EC1F1F2 = "🇬🇲", "🇬🇲 (:Gambia:)" + U264A = "♊", "♊ (:Gemini:)" + U1F1EC1F1EA = "🇬🇪", "🇬🇪 (:Georgia:)" + U1F1E91F1EA = "🇩🇪", "🇩🇪 (:Germany:)" + U1F1EC1F1ED = "🇬🇭", "🇬🇭 (:Ghana:)" + U1F1EC1F1EE = "🇬🇮", "🇬🇮 (:Gibraltar:)" + U1F1EC1F1F7 = "🇬🇷", "🇬🇷 (:Greece:)" + U1F1EC1F1F1 = "🇬🇱", "🇬🇱 (:Greenland:)" + U1F1EC1F1E9 = "🇬🇩", "🇬🇩 (:Grenada:)" + U1F1EC1F1F5 = "🇬🇵", "🇬🇵 (:Guadeloupe:)" + U1F1EC1F1FA = "🇬🇺", "🇬🇺 (:Guam:)" + U1F1EC1F1F9 = "🇬🇹", "🇬🇹 (:Guatemala:)" + U1F1EC1F1EC = "🇬🇬", "🇬🇬 (:Guernsey:)" + U1F1EC1F1F3 = "🇬🇳", "🇬🇳 (:Guinea:)" + U1F1EC1F1FC = "🇬🇼", "🇬🇼 (:Guinea-Bissau:)" + U1F1EC1F1FE = "🇬🇾", "🇬🇾 (:Guyana:)" + U1F1ED1F1F9 = "🇭🇹", "🇭🇹 (:Haiti:)" + U1F1ED1F1F2 = "🇭🇲", "🇭🇲 (:Heard_&_McDonald_Islands:)" + U1F1ED1F1F3 = "🇭🇳", "🇭🇳 (:Honduras:)" + U1F1ED1F1F0 = "🇭🇰", "🇭🇰 (:Hong_Kong_SAR_China:)" + U1F1ED1F1FA = "🇭🇺", "🇭🇺 (:Hungary:)" + U1F194 = "🆔", "🆔 (:ID_button:)" + U1F1EE1F1F8 = "🇮🇸", "🇮🇸 (:Iceland:)" + U1F1EE1F1F3 = "🇮🇳", "🇮🇳 (:India:)" + U1F1EE1F1E9 = "🇮🇩", "🇮🇩 (:Indonesia:)" + U1F1EE1F1F7 = "🇮🇷", "🇮🇷 (:Iran:)" + U1F1EE1F1F6 = "🇮🇶", "🇮🇶 (:Iraq:)" + U1F1EE1F1EA = "🇮🇪", "🇮🇪 (:Ireland:)" + U1F1EE1F1F2 = "🇮🇲", "🇮🇲 (:Isle_of_Man:)" + U1F1EE1F1F1 = "🇮🇱", "🇮🇱 (:Israel:)" + U1F1EE1F1F9 = "🇮🇹", "🇮🇹 (:Italy:)" + U1F1EF1F1F2 = "🇯🇲", "🇯🇲 (:Jamaica:)" + U1F1EF1F1F5 = "🇯🇵", "🇯🇵 (:Japan:)" + U1F251 = "🉑", "🉑 (:Japanese_acceptable_button:)" + U1F238 = "🈸", "🈸 (:Japanese_application_button:)" + U1F250 = "🉐", "🉐 (:Japanese_bargain_button:)" + U1F3EF = "🏯", "🏯 (:Japanese_castle:)" + U3297FE0F = "㊗️", "㊗️ (:Japanese_congratulations_button:)" + U3297 = "㊗", "㊗ (:Japanese_congratulations_button:)" + U1F239 = "🈹", "🈹 (:Japanese_discount_button:)" + U1F38E = "🎎", "🎎 (:Japanese_dolls:)" + U1F21A = "🈚", "🈚 (:Japanese_free_of_charge_button:)" + U1F201 = "🈁", "🈁 (:Japanese_here_button:)" + U1F237FE0F = "🈷️", "🈷️ (:Japanese_monthly_amount_button:)" + U1F237 = "🈷", "🈷 (:Japanese_monthly_amount_button:)" + U1F235 = "🈵", "🈵 (:Japanese_no_vacancy_button:)" + U1F236 = "🈶", "🈶 (:Japanese_not_free_of_charge_button:)" + U1F23A = "🈺", "🈺 (:Japanese_open_for_business_button:)" + U1F234 = "🈴", "🈴 (:Japanese_passing_grade_button:)" + U1F3E3 = "🏣", "🏣 (:Japanese_post_office:)" + U1F232 = "🈲", "🈲 (:Japanese_prohibited_button:)" + U1F22F = "🈯", "🈯 (:Japanese_reserved_button:)" + U3299FE0F = "㊙️", "㊙️ (:Japanese_secret_button:)" + U3299 = "㊙", "㊙ (:Japanese_secret_button:)" + U1F202FE0F = "🈂️", "🈂️ (:Japanese_service_charge_button:)" + U1F202 = "🈂", "🈂 (:Japanese_service_charge_button:)" + U1F530 = "🔰", "🔰 (:Japanese_symbol_for_beginner:)" + U1F233 = "🈳", "🈳 (:Japanese_vacancy_button:)" + U1F1EF1F1EA = "🇯🇪", "🇯🇪 (:Jersey:)" + U1F1EF1F1F4 = "🇯🇴", "🇯🇴 (:Jordan:)" + U1F1F01F1FF = "🇰🇿", "🇰🇿 (:Kazakhstan:)" + U1F1F01F1EA = "🇰🇪", "🇰🇪 (:Kenya:)" + U1F1F01F1EE = "🇰🇮", "🇰🇮 (:Kiribati:)" + U1F1FD1F1F0 = "🇽🇰", "🇽🇰 (:Kosovo:)" + U1F1F01F1FC = "🇰🇼", "🇰🇼 (:Kuwait:)" + U1F1F01F1EC = "🇰🇬", "🇰🇬 (:Kyrgyzstan:)" + U1F1F11F1E6 = "🇱🇦", "🇱🇦 (:Laos:)" + U1F1F11F1FB = "🇱🇻", "🇱🇻 (:Latvia:)" + U1F1F11F1E7 = "🇱🇧", "🇱🇧 (:Lebanon:)" + U264C = "♌", "♌ (:Leo:)" + U1F1F11F1F8 = "🇱🇸", "🇱🇸 (:Lesotho:)" + U1F1F11F1F7 = "🇱🇷", "🇱🇷 (:Liberia:)" + U264E = "♎", "♎ (:Libra:)" + U1F1F11F1FE = "🇱🇾", "🇱🇾 (:Libya:)" + U1F1F11F1EE = "🇱🇮", "🇱🇮 (:Liechtenstein:)" + U1F1F11F1F9 = "🇱🇹", "🇱🇹 (:Lithuania:)" + U1F1F11F1FA = "🇱🇺", "🇱🇺 (:Luxembourg:)" + U1F1F21F1F4 = "🇲🇴", "🇲🇴 (:Macao_SAR_China:)" + U1F1F21F1EC = "🇲🇬", "🇲🇬 (:Madagascar:)" + U1F1F21F1FC = "🇲🇼", "🇲🇼 (:Malawi:)" + U1F1F21F1FE = "🇲🇾", "🇲🇾 (:Malaysia:)" + U1F1F21F1FB = "🇲🇻", "🇲🇻 (:Maldives:)" + U1F1F21F1F1 = "🇲🇱", "🇲🇱 (:Mali:)" + U1F1F21F1F9 = "🇲🇹", "🇲🇹 (:Malta:)" + U1F1F21F1ED = "🇲🇭", "🇲🇭 (:Marshall_Islands:)" + U1F1F21F1F6 = "🇲🇶", "🇲🇶 (:Martinique:)" + U1F1F21F1F7 = "🇲🇷", "🇲🇷 (:Mauritania:)" + U1F1F21F1FA = "🇲🇺", "🇲🇺 (:Mauritius:)" + U1F1FE1F1F9 = "🇾🇹", "🇾🇹 (:Mayotte:)" + U1F1F21F1FD = "🇲🇽", "🇲🇽 (:Mexico:)" + U1F1EB1F1F2 = "🇫🇲", "🇫🇲 (:Micronesia:)" + U1F1F21F1E9 = "🇲🇩", "🇲🇩 (:Moldova:)" + U1F1F21F1E8 = "🇲🇨", "🇲🇨 (:Monaco:)" + U1F1F21F1F3 = "🇲🇳", "🇲🇳 (:Mongolia:)" + U1F1F21F1EA = "🇲🇪", "🇲🇪 (:Montenegro:)" + U1F1F21F1F8 = "🇲🇸", "🇲🇸 (:Montserrat:)" + U1F1F21F1E6 = "🇲🇦", "🇲🇦 (:Morocco:)" + U1F1F21F1FF = "🇲🇿", "🇲🇿 (:Mozambique:)" + U1F936 = "🤶", "🤶 (:Mrs._Claus:)" + U1F9361F3FF = "🤶🏿", "🤶🏿 (:Mrs._Claus_dark_skin_tone:)" + U1F9361F3FB = "🤶🏻", "🤶🏻 (:Mrs._Claus_light_skin_tone:)" + U1F9361F3FE = "🤶🏾", "🤶🏾 (:Mrs._Claus_medium-dark_skin_tone:)" + U1F9361F3FC = "🤶🏼", "🤶🏼 (:Mrs._Claus_medium-light_skin_tone:)" + U1F9361F3FD = "🤶🏽", "🤶🏽 (:Mrs._Claus_medium_skin_tone:)" + U1F1F21F1F2 = "🇲🇲", "🇲🇲 (:Myanmar_(Burma):)" + U1F195 = "🆕", "🆕 (:NEW_button:)" + U1F196 = "🆖", "🆖 (:NG_button:)" + U1F1F31F1E6 = "🇳🇦", "🇳🇦 (:Namibia:)" + U1F1F31F1F7 = "🇳🇷", "🇳🇷 (:Nauru:)" + U1F1F31F1F5 = "🇳🇵", "🇳🇵 (:Nepal:)" + U1F1F31F1F1 = "🇳🇱", "🇳🇱 (:Netherlands:)" + U1F1F31F1E8 = "🇳🇨", "🇳🇨 (:New_Caledonia:)" + U1F1F31F1FF = "🇳🇿", "🇳🇿 (:New_Zealand:)" + U1F1F31F1EE = "🇳🇮", "🇳🇮 (:Nicaragua:)" + U1F1F31F1EA = "🇳🇪", "🇳🇪 (:Niger:)" + U1F1F31F1EC = "🇳🇬", "🇳🇬 (:Nigeria:)" + U1F1F31F1FA = "🇳🇺", "🇳🇺 (:Niue:)" + U1F1F31F1EB = "🇳🇫", "🇳🇫 (:Norfolk_Island:)" + U1F1F01F1F5 = "🇰🇵", "🇰🇵 (:North_Korea:)" + U1F1F21F1F0 = "🇲🇰", "🇲🇰 (:North_Macedonia:)" + U1F1F21F1F5 = "🇲🇵", "🇲🇵 (:Northern_Mariana_Islands:)" + U1F1F31F1F4 = "🇳🇴", "🇳🇴 (:Norway:)" + U1F197 = "🆗", "🆗 (:OK_button:)" + U1F44C = "👌", "👌 (:OK_hand:)" + U1F44C1F3FF = "👌🏿", "👌🏿 (:OK_hand_dark_skin_tone:)" + U1F44C1F3FB = "👌🏻", "👌🏻 (:OK_hand_light_skin_tone:)" + U1F44C1F3FE = "👌🏾", "👌🏾 (:OK_hand_medium-dark_skin_tone:)" + U1F44C1F3FC = "👌🏼", "👌🏼 (:OK_hand_medium-light_skin_tone:)" + U1F44C1F3FD = "👌🏽", "👌🏽 (:OK_hand_medium_skin_tone:)" + U1F51B = "🔛", "🔛 (:ON!_arrow:)" + U1F17EFE0F = "🅾️", "🅾️ (:O_button_(blood_type):)" + U1F17E = "🅾", "🅾 (:O_button_(blood_type):)" + U1F1F41F1F2 = "🇴🇲", "🇴🇲 (:Oman:)" + U26CE = "⛎", "⛎ (:Ophiuchus:)" + U1F17FFE0F = "🅿️", "🅿️ (:P_button:)" + U1F17F = "🅿", "🅿 (:P_button:)" + U1F1F51F1F0 = "🇵🇰", "🇵🇰 (:Pakistan:)" + U1F1F51F1FC = "🇵🇼", "🇵🇼 (:Palau:)" + U1F1F51F1F8 = "🇵🇸", "🇵🇸 (:Palestinian_Territories:)" + U1F1F51F1E6 = "🇵🇦", "🇵🇦 (:Panama:)" + U1F1F51F1EC = "🇵🇬", "🇵🇬 (:Papua_New_Guinea:)" + U1F1F51F1FE = "🇵🇾", "🇵🇾 (:Paraguay:)" + U1F1F51F1EA = "🇵🇪", "🇵🇪 (:Peru:)" + U1F1F51F1ED = "🇵🇭", "🇵🇭 (:Philippines:)" + U2653 = "♓", "♓ (:Pisces:)" + U1F1F51F1F3 = "🇵🇳", "🇵🇳 (:Pitcairn_Islands:)" + U1F1F51F1F1 = "🇵🇱", "🇵🇱 (:Poland:)" + U1F1F51F1F9 = "🇵🇹", "🇵🇹 (:Portugal:)" + U1F1F51F1F7 = "🇵🇷", "🇵🇷 (:Puerto_Rico:)" + U1F1F61F1E6 = "🇶🇦", "🇶🇦 (:Qatar:)" + U1F1F71F1F4 = "🇷🇴", "🇷🇴 (:Romania:)" + U1F1F71F1FA = "🇷🇺", "🇷🇺 (:Russia:)" + U1F1F71F1FC = "🇷🇼", "🇷🇼 (:Rwanda:)" + U1F1F71F1EA = "🇷🇪", "🇷🇪 (:Réunion:)" + U1F51C = "🔜", "🔜 (:SOON_arrow:)" + U1F198 = "🆘", "🆘 (:SOS_button:)" + U2650 = "♐", "♐ (:Sagittarius:)" + U1F1FC1F1F8 = "🇼🇸", "🇼🇸 (:Samoa:)" + U1F1F81F1F2 = "🇸🇲", "🇸🇲 (:San_Marino:)" + U1F385 = "🎅", "🎅 (:Santa_Claus:)" + U1F3851F3FF = "🎅🏿", "🎅🏿 (:Santa_Claus_dark_skin_tone:)" + U1F3851F3FB = "🎅🏻", "🎅🏻 (:Santa_Claus_light_skin_tone:)" + U1F3851F3FE = "🎅🏾", "🎅🏾 (:Santa_Claus_medium-dark_skin_tone:)" + U1F3851F3FC = "🎅🏼", "🎅🏼 (:Santa_Claus_medium-light_skin_tone:)" + U1F3851F3FD = "🎅🏽", "🎅🏽 (:Santa_Claus_medium_skin_tone:)" + U1F1F81F1E6 = "🇸🇦", "🇸🇦 (:Saudi_Arabia:)" + U264F = "♏", "♏ (:Scorpio:)" + U1F3F4E0067E0062E0073E0063E0074E007F = "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "🏴󠁧󠁢󠁳󠁣󠁴󠁿 (:Scotland:)" + U1F1F81F1F3 = "🇸🇳", "🇸🇳 (:Senegal:)" + U1F1F71F1F8 = "🇷🇸", "🇷🇸 (:Serbia:)" + U1F1F81F1E8 = "🇸🇨", "🇸🇨 (:Seychelles:)" + U1F1F81F1F1 = "🇸🇱", "🇸🇱 (:Sierra_Leone:)" + U1F1F81F1EC = "🇸🇬", "🇸🇬 (:Singapore:)" + U1F1F81F1FD = "🇸🇽", "🇸🇽 (:Sint_Maarten:)" + U1F1F81F1F0 = "🇸🇰", "🇸🇰 (:Slovakia:)" + U1F1F81F1EE = "🇸🇮", "🇸🇮 (:Slovenia:)" + U1F1F81F1E7 = "🇸🇧", "🇸🇧 (:Solomon_Islands:)" + U1F1F81F1F4 = "🇸🇴", "🇸🇴 (:Somalia:)" + U1F1FF1F1E6 = "🇿🇦", "🇿🇦 (:South_Africa:)" + U1F1EC1F1F8 = "🇬🇸", "🇬🇸 (:South_Georgia_&_South_Sandwich_Islands:)" + U1F1F01F1F7 = "🇰🇷", "🇰🇷 (:South_Korea:)" + U1F1F81F1F8 = "🇸🇸", "🇸🇸 (:South_Sudan:)" + U1F1EA1F1F8 = "🇪🇸", "🇪🇸 (:Spain:)" + U1F1F11F1F0 = "🇱🇰", "🇱🇰 (:Sri_Lanka:)" + U1F1E71F1F1 = "🇧🇱", "🇧🇱 (:St._Barthélemy:)" + U1F1F81F1ED = "🇸🇭", "🇸🇭 (:St._Helena:)" + U1F1F01F1F3 = "🇰🇳", "🇰🇳 (:St._Kitts_&_Nevis:)" + U1F1F11F1E8 = "🇱🇨", "🇱🇨 (:St._Lucia:)" + U1F1F21F1EB = "🇲🇫", "🇲🇫 (:St._Martin:)" + U1F1F51F1F2 = "🇵🇲", "🇵🇲 (:St._Pierre_&_Miquelon:)" + U1F1FB1F1E8 = "🇻🇨", "🇻🇨 (:St._Vincent_&_Grenadines:)" + U1F5FD = "🗽", "🗽 (:Statue_of_Liberty:)" + U1F1F81F1E9 = "🇸🇩", "🇸🇩 (:Sudan:)" + U1F1F81F1F7 = "🇸🇷", "🇸🇷 (:Suriname:)" + U1F1F81F1EF = "🇸🇯", "🇸🇯 (:Svalbard_&_Jan_Mayen:)" + U1F1F81F1EA = "🇸🇪", "🇸🇪 (:Sweden:)" + U1F1E81F1ED = "🇨🇭", "🇨🇭 (:Switzerland:)" + U1F1F81F1FE = "🇸🇾", "🇸🇾 (:Syria:)" + U1F1F81F1F9 = "🇸🇹", "🇸🇹 (:São_Tomé_&_Príncipe:)" + U1F996 = "🦖", "🦖 (:T-Rex:)" + U1F51D = "🔝", "🔝 (:TOP_arrow:)" + U1F1F91F1FC = "🇹🇼", "🇹🇼 (:Taiwan:)" + U1F1F91F1EF = "🇹🇯", "🇹🇯 (:Tajikistan:)" + U1F1F91F1FF = "🇹🇿", "🇹🇿 (:Tanzania:)" + U2649 = "♉", "♉ (:Taurus:)" + U1F1F91F1ED = "🇹🇭", "🇹🇭 (:Thailand:)" + U1F1F91F1F1 = "🇹🇱", "🇹🇱 (:Timor-Leste:)" + U1F1F91F1EC = "🇹🇬", "🇹🇬 (:Togo:)" + U1F1F91F1F0 = "🇹🇰", "🇹🇰 (:Tokelau:)" + U1F5FC = "🗼", "🗼 (:Tokyo_tower:)" + U1F1F91F1F4 = "🇹🇴", "🇹🇴 (:Tonga:)" + U1F1F91F1F9 = "🇹🇹", "🇹🇹 (:Trinidad_&_Tobago:)" + U1F1F91F1E6 = "🇹🇦", "🇹🇦 (:Tristan_da_Cunha:)" + U1F1F91F1F3 = "🇹🇳", "🇹🇳 (:Tunisia:)" + U1F1F91F1F2 = "🇹🇲", "🇹🇲 (:Turkmenistan:)" + U1F1F91F1E8 = "🇹🇨", "🇹🇨 (:Turks_&_Caicos_Islands:)" + U1F1F91F1FB = "🇹🇻", "🇹🇻 (:Tuvalu:)" + U1F1F91F1F7 = "🇹🇷", "🇹🇷 (:Türkiye:)" + U1F1FA1F1F2 = "🇺🇲", "🇺🇲 (:U.S._Outlying_Islands:)" + U1F1FB1F1EE = "🇻🇮", "🇻🇮 (:U.S._Virgin_Islands:)" + U1F199 = "🆙", "🆙 (:UP!_button:)" + U1F1FA1F1EC = "🇺🇬", "🇺🇬 (:Uganda:)" + U1F1FA1F1E6 = "🇺🇦", "🇺🇦 (:Ukraine:)" + U1F1E61F1EA = "🇦🇪", "🇦🇪 (:United_Arab_Emirates:)" + U1F1EC1F1E7 = "🇬🇧", "🇬🇧 (:United_Kingdom:)" + U1F1FA1F1F3 = "🇺🇳", "🇺🇳 (:United_Nations:)" + U1F1FA1F1F8 = "🇺🇸", "🇺🇸 (:United_States:)" + U1F1FA1F1FE = "🇺🇾", "🇺🇾 (:Uruguay:)" + U1F1FA1F1FF = "🇺🇿", "🇺🇿 (:Uzbekistan:)" + U1F19A = "🆚", "🆚 (:VS_button:)" + U1F1FB1F1FA = "🇻🇺", "🇻🇺 (:Vanuatu:)" + U1F1FB1F1E6 = "🇻🇦", "🇻🇦 (:Vatican_City:)" + U1F1FB1F1EA = "🇻🇪", "🇻🇪 (:Venezuela:)" + U1F1FB1F1F3 = "🇻🇳", "🇻🇳 (:Vietnam:)" + U264D = "♍", "♍ (:Virgo:)" + U1F3F4E0067E0062E0077E006CE0073E007F = "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "🏴󠁧󠁢󠁷󠁬󠁳󠁿 (:Wales:)" + U1F1FC1F1EB = "🇼🇫", "🇼🇫 (:Wallis_&_Futuna:)" + U1F1EA1F1ED = "🇪🇭", "🇪🇭 (:Western_Sahara:)" + U1F1FE1F1EA = "🇾🇪", "🇾🇪 (:Yemen:)" + U1F4A4 = "💤", "💤 (:ZZZ:)" + U1F1FF1F1F2 = "🇿🇲", "🇿🇲 (:Zambia:)" + U1F1FF1F1FC = "🇿🇼", "🇿🇼 (:Zimbabwe:)" + U1F9EE = "🧮", "🧮 (:abacus:)" + U1FA97 = "🪗", "🪗 (:accordion:)" + U1FA79 = "🩹", "🩹 (:adhesive_bandage:)" + U1F39FFE0F = "🎟️", "🎟️ (:admission_tickets:)" + U1F39F = "🎟", "🎟 (:admission_tickets:)" + U1F6A1 = "🚡", "🚡 (:aerial_tramway:)" + U2708FE0F = "✈️", "✈️ (:airplane:)" + U2708 = "✈", "✈ (:airplane:)" + U1F6EC = "🛬", "🛬 (:airplane_arrival:)" + U1F6EB = "🛫", "🛫 (:airplane_departure:)" + U23F0 = "⏰", "⏰ (:alarm_clock:)" + U2697FE0F = "⚗️", "⚗️ (:alembic:)" + U2697 = "⚗", "⚗ (:alembic:)" + U1F47D = "👽", "👽 (:alien:)" + U1F47E = "👾", "👾 (:alien_monster:)" + U1F691 = "🚑", "🚑 (:ambulance:)" + U1F3C8 = "🏈", "🏈 (:american_football:)" + U1F3FA = "🏺", "🏺 (:amphora:)" + U1FAC0 = "🫀", "🫀 (:anatomical_heart:)" + U2693 = "⚓", "⚓ (:anchor:)" + U1F4A2 = "💢", "💢 (:anger_symbol:)" + U1F620 = "😠", "😠 (:angry_face:)" + U1F47F = "👿", "👿 (:angry_face_with_horns:)" + U1F627 = "😧", "😧 (:anguished_face:)" + U1F41C = "🐜", "🐜 (:ant:)" + U1F4F6 = "📶", "📶 (:antenna_bars:)" + U1F630 = "😰", "😰 (:anxious_face_with_sweat:)" + U1F69B = "🚛", "🚛 (:articulated_lorry:)" + U1F9D1200D1F3A8 = "🧑‍🎨", "🧑‍🎨 (:artist:)" + U1F9D11F3FF200D1F3A8 = "🧑🏿‍🎨", "🧑🏿‍🎨 (:artist_dark_skin_tone:)" + U1F9D11F3FB200D1F3A8 = "🧑🏻‍🎨", "🧑🏻‍🎨 (:artist_light_skin_tone:)" + U1F9D11F3FE200D1F3A8 = "🧑🏾‍🎨", "🧑🏾‍🎨 (:artist_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F3A8 = "🧑🏼‍🎨", "🧑🏼‍🎨 (:artist_medium-light_skin_tone:)" + U1F9D11F3FD200D1F3A8 = "🧑🏽‍🎨", "🧑🏽‍🎨 (:artist_medium_skin_tone:)" + U1F3A8 = "🎨", "🎨 (:artist_palette:)" + U1F632 = "😲", "😲 (:astonished_face:)" + U1F9D1200D1F680 = "🧑‍🚀", "🧑‍🚀 (:astronaut:)" + U1F9D11F3FF200D1F680 = "🧑🏿‍🚀", "🧑🏿‍🚀 (:astronaut_dark_skin_tone:)" + U1F9D11F3FB200D1F680 = "🧑🏻‍🚀", "🧑🏻‍🚀 (:astronaut_light_skin_tone:)" + U1F9D11F3FE200D1F680 = "🧑🏾‍🚀", "🧑🏾‍🚀 (:astronaut_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F680 = "🧑🏼‍🚀", "🧑🏼‍🚀 (:astronaut_medium-light_skin_tone:)" + U1F9D11F3FD200D1F680 = "🧑🏽‍🚀", "🧑🏽‍🚀 (:astronaut_medium_skin_tone:)" + U269BFE0F = "⚛️", "⚛️ (:atom_symbol:)" + U269B = "⚛", "⚛ (:atom_symbol:)" + U1F6FA = "🛺", "🛺 (:auto_rickshaw:)" + U1F697 = "🚗", "🚗 (:automobile:)" + U1F951 = "🥑", "🥑 (:avocado:)" + U1FA93 = "🪓", "🪓 (:axe:)" + U1F476 = "👶", "👶 (:baby:)" + U1F47C = "👼", "👼 (:baby_angel:)" + U1F47C1F3FF = "👼🏿", "👼🏿 (:baby_angel_dark_skin_tone:)" + U1F47C1F3FB = "👼🏻", "👼🏻 (:baby_angel_light_skin_tone:)" + U1F47C1F3FE = "👼🏾", "👼🏾 (:baby_angel_medium-dark_skin_tone:)" + U1F47C1F3FC = "👼🏼", "👼🏼 (:baby_angel_medium-light_skin_tone:)" + U1F47C1F3FD = "👼🏽", "👼🏽 (:baby_angel_medium_skin_tone:)" + U1F37C = "🍼", "🍼 (:baby_bottle:)" + U1F424 = "🐤", "🐤 (:baby_chick:)" + U1F4761F3FF = "👶🏿", "👶🏿 (:baby_dark_skin_tone:)" + U1F4761F3FB = "👶🏻", "👶🏻 (:baby_light_skin_tone:)" + U1F4761F3FE = "👶🏾", "👶🏾 (:baby_medium-dark_skin_tone:)" + U1F4761F3FC = "👶🏼", "👶🏼 (:baby_medium-light_skin_tone:)" + U1F4761F3FD = "👶🏽", "👶🏽 (:baby_medium_skin_tone:)" + U1F6BC = "🚼", "🚼 (:baby_symbol:)" + U1F447 = "👇", "👇 (:backhand_index_pointing_down:)" + U1F4471F3FF = "👇🏿", "👇🏿 (:backhand_index_pointing_down_dark_skin_tone:)" + U1F4471F3FB = "👇🏻", "👇🏻 (:backhand_index_pointing_down_light_skin_tone:)" + U1F4471F3FE = "👇🏾", "👇🏾 (:backhand_index_pointing_down_medium-dark_skin_tone:)" + U1F4471F3FC = "👇🏼", "👇🏼 (:backhand_index_pointing_down_medium-light_skin_tone:)" + U1F4471F3FD = "👇🏽", "👇🏽 (:backhand_index_pointing_down_medium_skin_tone:)" + U1F448 = "👈", "👈 (:backhand_index_pointing_left:)" + U1F4481F3FF = "👈🏿", "👈🏿 (:backhand_index_pointing_left_dark_skin_tone:)" + U1F4481F3FB = "👈🏻", "👈🏻 (:backhand_index_pointing_left_light_skin_tone:)" + U1F4481F3FE = "👈🏾", "👈🏾 (:backhand_index_pointing_left_medium-dark_skin_tone:)" + U1F4481F3FC = "👈🏼", "👈🏼 (:backhand_index_pointing_left_medium-light_skin_tone:)" + U1F4481F3FD = "👈🏽", "👈🏽 (:backhand_index_pointing_left_medium_skin_tone:)" + U1F449 = "👉", "👉 (:backhand_index_pointing_right:)" + U1F4491F3FF = "👉🏿", "👉🏿 (:backhand_index_pointing_right_dark_skin_tone:)" + U1F4491F3FB = "👉🏻", "👉🏻 (:backhand_index_pointing_right_light_skin_tone:)" + U1F4491F3FE = "👉🏾", "👉🏾 (:backhand_index_pointing_right_medium-dark_skin_tone:)" + U1F4491F3FC = "👉🏼", "👉🏼 (:backhand_index_pointing_right_medium-light_skin_tone:)" + U1F4491F3FD = "👉🏽", "👉🏽 (:backhand_index_pointing_right_medium_skin_tone:)" + U1F446 = "👆", "👆 (:backhand_index_pointing_up:)" + U1F4461F3FF = "👆🏿", "👆🏿 (:backhand_index_pointing_up_dark_skin_tone:)" + U1F4461F3FB = "👆🏻", "👆🏻 (:backhand_index_pointing_up_light_skin_tone:)" + U1F4461F3FE = "👆🏾", "👆🏾 (:backhand_index_pointing_up_medium-dark_skin_tone:)" + U1F4461F3FC = "👆🏼", "👆🏼 (:backhand_index_pointing_up_medium-light_skin_tone:)" + U1F4461F3FD = "👆🏽", "👆🏽 (:backhand_index_pointing_up_medium_skin_tone:)" + U1F392 = "🎒", "🎒 (:backpack:)" + U1F953 = "🥓", "🥓 (:bacon:)" + U1F9A1 = "🦡", "🦡 (:badger:)" + U1F3F8 = "🏸", "🏸 (:badminton:)" + U1F96F = "🥯", "🥯 (:bagel:)" + U1F6C4 = "🛄", "🛄 (:baggage_claim:)" + U1F956 = "🥖", "🥖 (:baguette_bread:)" + U2696FE0F = "⚖️", "⚖️ (:balance_scale:)" + U2696 = "⚖", "⚖ (:balance_scale:)" + U1F9B2 = "🦲", "🦲 (:bald:)" + U1FA70 = "🩰", "🩰 (:ballet_shoes:)" + U1F388 = "🎈", "🎈 (:balloon:)" + U1F5F3FE0F = "🗳️", "🗳️ (:ballot_box_with_ballot:)" + U1F5F3 = "🗳", "🗳 (:ballot_box_with_ballot:)" + U1F34C = "🍌", "🍌 (:banana:)" + U1FA95 = "🪕", "🪕 (:banjo:)" + U1F3E6 = "🏦", "🏦 (:bank:)" + U1F4CA = "📊", "📊 (:bar_chart:)" + U1F488 = "💈", "💈 (:barber_pole:)" + U26BE = "⚾", "⚾ (:baseball:)" + U1F9FA = "🧺", "🧺 (:basket:)" + U1F3C0 = "🏀", "🏀 (:basketball:)" + U1F987 = "🦇", "🦇 (:bat:)" + U1F6C1 = "🛁", "🛁 (:bathtub:)" + U1F50B = "🔋", "🔋 (:battery:)" + U1F3D6FE0F = "🏖️", "🏖️ (:beach_with_umbrella:)" + U1F3D6 = "🏖", "🏖 (:beach_with_umbrella:)" + U1F601 = "😁", "😁 (:beaming_face_with_smiling_eyes:)" + U1FAD8 = "🫘", "🫘 (:beans:)" + U1F43B = "🐻", "🐻 (:bear:)" + U1F493 = "💓", "💓 (:beating_heart:)" + U1F9AB = "🦫", "🦫 (:beaver:)" + U1F6CFFE0F = "🛏️", "🛏️ (:bed:)" + U1F6CF = "🛏", "🛏 (:bed:)" + U1F37A = "🍺", "🍺 (:beer_mug:)" + U1FAB2 = "🪲", "🪲 (:beetle:)" + U1F514 = "🔔", "🔔 (:bell:)" + U1FAD1 = "🫑", "🫑 (:bell_pepper:)" + U1F515 = "🔕", "🔕 (:bell_with_slash:)" + U1F6CEFE0F = "🛎️", "🛎️ (:bellhop_bell:)" + U1F6CE = "🛎", "🛎 (:bellhop_bell:)" + U1F371 = "🍱", "🍱 (:bento_box:)" + U1F9C3 = "🧃", "🧃 (:beverage_box:)" + U1F6B2 = "🚲", "🚲 (:bicycle:)" + U1F459 = "👙", "👙 (:bikini:)" + U1F9E2 = "🧢", "🧢 (:billed_cap:)" + U2623FE0F = "☣️", "☣️ (:biohazard:)" + U2623 = "☣", "☣ (:biohazard:)" + U1F426 = "🐦", "🐦 (:bird:)" + U1F382 = "🎂", "🎂 (:birthday_cake:)" + U1F9AC = "🦬", "🦬 (:bison:)" + U1FAE6 = "🫦", "🫦 (:biting_lip:)" + U1F426200D2B1B = "🐦‍⬛", "🐦‍⬛ (:black_bird:)" + U1F408200D2B1B = "🐈‍⬛", "🐈‍⬛ (:black_cat:)" + U26AB = "⚫", "⚫ (:black_circle:)" + U1F3F4 = "🏴", "🏴 (:black_flag:)" + U1F5A4 = "🖤", "🖤 (:black_heart:)" + U2B1B = "⬛", "⬛ (:black_large_square:)" + U25FE = "◾", "◾ (:black_medium-small_square:)" + U25FCFE0F = "◼️", "◼️ (:black_medium_square:)" + U25FC = "◼", "◼ (:black_medium_square:)" + U2712FE0F = "✒️", "✒️ (:black_nib:)" + U2712 = "✒", "✒ (:black_nib:)" + U25AAFE0F = "▪️", "▪️ (:black_small_square:)" + U25AA = "▪", "▪ (:black_small_square:)" + U1F532 = "🔲", "🔲 (:black_square_button:)" + U1F33C = "🌼", "🌼 (:blossom:)" + U1F421 = "🐡", "🐡 (:blowfish:)" + U1F4D8 = "📘", "📘 (:blue_book:)" + U1F535 = "🔵", "🔵 (:blue_circle:)" + U1F499 = "💙", "💙 (:blue_heart:)" + U1F7E6 = "🟦", "🟦 (:blue_square:)" + U1FAD0 = "🫐", "🫐 (:blueberries:)" + U1F417 = "🐗", "🐗 (:boar:)" + U1F4A3 = "💣", "💣 (:bomb:)" + U1F9B4 = "🦴", "🦴 (:bone:)" + U1F516 = "🔖", "🔖 (:bookmark:)" + U1F4D1 = "📑", "📑 (:bookmark_tabs:)" + U1F4DA = "📚", "📚 (:books:)" + U1FA83 = "🪃", "🪃 (:boomerang:)" + U1F37E = "🍾", "🍾 (:bottle_with_popping_cork:)" + U1F490 = "💐", "💐 (:bouquet:)" + U1F3F9 = "🏹", "🏹 (:bow_and_arrow:)" + U1F963 = "🥣", "🥣 (:bowl_with_spoon:)" + U1F3B3 = "🎳", "🎳 (:bowling:)" + U1F94A = "🥊", "🥊 (:boxing_glove:)" + U1F466 = "👦", "👦 (:boy:)" + U1F4661F3FF = "👦🏿", "👦🏿 (:boy_dark_skin_tone:)" + U1F4661F3FB = "👦🏻", "👦🏻 (:boy_light_skin_tone:)" + U1F4661F3FE = "👦🏾", "👦🏾 (:boy_medium-dark_skin_tone:)" + U1F4661F3FC = "👦🏼", "👦🏼 (:boy_medium-light_skin_tone:)" + U1F4661F3FD = "👦🏽", "👦🏽 (:boy_medium_skin_tone:)" + U1F9E0 = "🧠", "🧠 (:brain:)" + U1F35E = "🍞", "🍞 (:bread:)" + U1F931 = "🤱", "🤱 (:breast-feeding:)" + U1F9311F3FF = "🤱🏿", "🤱🏿 (:breast-feeding_dark_skin_tone:)" + U1F9311F3FB = "🤱🏻", "🤱🏻 (:breast-feeding_light_skin_tone:)" + U1F9311F3FE = "🤱🏾", "🤱🏾 (:breast-feeding_medium-dark_skin_tone:)" + U1F9311F3FC = "🤱🏼", "🤱🏼 (:breast-feeding_medium-light_skin_tone:)" + U1F9311F3FD = "🤱🏽", "🤱🏽 (:breast-feeding_medium_skin_tone:)" + U1F9F1 = "🧱", "🧱 (:brick:)" + U1F309 = "🌉", "🌉 (:bridge_at_night:)" + U1F4BC = "💼", "💼 (:briefcase:)" + U1FA72 = "🩲", "🩲 (:briefs:)" + U1F506 = "🔆", "🔆 (:bright_button:)" + U1F966 = "🥦", "🥦 (:broccoli:)" + U26D3FE0F200D1F4A5 = "⛓️‍💥", "⛓️‍💥 (:broken_chain:)" + U26D3200D1F4A5 = "⛓‍💥", "⛓‍💥 (:broken_chain:)" + U1F494 = "💔", "💔 (:broken_heart:)" + U1F9F9 = "🧹", "🧹 (:broom:)" + U1F7E4 = "🟤", "🟤 (:brown_circle:)" + U1F90E = "🤎", "🤎 (:brown_heart:)" + U1F344200D1F7EB = "🍄‍🟫", "🍄‍🟫 (:brown_mushroom:)" + U1F7EB = "🟫", "🟫 (:brown_square:)" + U1F9CB = "🧋", "🧋 (:bubble_tea:)" + U1FAE7 = "🫧", "🫧 (:bubbles:)" + U1FAA3 = "🪣", "🪣 (:bucket:)" + U1F41B = "🐛", "🐛 (:bug:)" + U1F3D7FE0F = "🏗️", "🏗️ (:building_construction:)" + U1F3D7 = "🏗", "🏗 (:building_construction:)" + U1F685 = "🚅", "🚅 (:bullet_train:)" + U1F3AF = "🎯", "🎯 (:bullseye:)" + U1F32F = "🌯", "🌯 (:burrito:)" + U1F68C = "🚌", "🚌 (:bus:)" + U1F68F = "🚏", "🚏 (:bus_stop:)" + U1F464 = "👤", "👤 (:bust_in_silhouette:)" + U1F465 = "👥", "👥 (:busts_in_silhouette:)" + U1F9C8 = "🧈", "🧈 (:butter:)" + U1F98B = "🦋", "🦋 (:butterfly:)" + U1F335 = "🌵", "🌵 (:cactus:)" + U1F4C5 = "📅", "📅 (:calendar:)" + U1F919 = "🤙", "🤙 (:call_me_hand:)" + U1F9191F3FF = "🤙🏿", "🤙🏿 (:call_me_hand_dark_skin_tone:)" + U1F9191F3FB = "🤙🏻", "🤙🏻 (:call_me_hand_light_skin_tone:)" + U1F9191F3FE = "🤙🏾", "🤙🏾 (:call_me_hand_medium-dark_skin_tone:)" + U1F9191F3FC = "🤙🏼", "🤙🏼 (:call_me_hand_medium-light_skin_tone:)" + U1F9191F3FD = "🤙🏽", "🤙🏽 (:call_me_hand_medium_skin_tone:)" + U1F42A = "🐪", "🐪 (:camel:)" + U1F4F7 = "📷", "📷 (:camera:)" + U1F4F8 = "📸", "📸 (:camera_with_flash:)" + U1F3D5FE0F = "🏕️", "🏕️ (:camping:)" + U1F3D5 = "🏕", "🏕 (:camping:)" + U1F56FFE0F = "🕯️", "🕯️ (:candle:)" + U1F56F = "🕯", "🕯 (:candle:)" + U1F36C = "🍬", "🍬 (:candy:)" + U1F96B = "🥫", "🥫 (:canned_food:)" + U1F6F6 = "🛶", "🛶 (:canoe:)" + U1F5C3FE0F = "🗃️", "🗃️ (:card_file_box:)" + U1F5C3 = "🗃", "🗃 (:card_file_box:)" + U1F4C7 = "📇", "📇 (:card_index:)" + U1F5C2FE0F = "🗂️", "🗂️ (:card_index_dividers:)" + U1F5C2 = "🗂", "🗂 (:card_index_dividers:)" + U1F3A0 = "🎠", "🎠 (:carousel_horse:)" + U1F38F = "🎏", "🎏 (:carp_streamer:)" + U1FA9A = "🪚", "🪚 (:carpentry_saw:)" + U1F955 = "🥕", "🥕 (:carrot:)" + U1F3F0 = "🏰", "🏰 (:castle:)" + U1F408 = "🐈", "🐈 (:cat:)" + U1F431 = "🐱", "🐱 (:cat_face:)" + U1F639 = "😹", "😹 (:cat_with_tears_of_joy:)" + U1F63C = "😼", "😼 (:cat_with_wry_smile:)" + U26D3FE0F = "⛓️", "⛓️ (:chains:)" + U26D3 = "⛓", "⛓ (:chains:)" + U1FA91 = "🪑", "🪑 (:chair:)" + U1F4C9 = "📉", "📉 (:chart_decreasing:)" + U1F4C8 = "📈", "📈 (:chart_increasing:)" + U1F4B9 = "💹", "💹 (:chart_increasing_with_yen:)" + U2611FE0F = "☑️", "☑️ (:check_box_with_check:)" + U2611 = "☑", "☑ (:check_box_with_check:)" + U2714FE0F = "✔️", "✔️ (:check_mark:)" + U2714 = "✔", "✔ (:check_mark:)" + U2705 = "✅", "✅ (:check_mark_button:)" + U1F9C0 = "🧀", "🧀 (:cheese_wedge:)" + U1F3C1 = "🏁", "🏁 (:chequered_flag:)" + U1F352 = "🍒", "🍒 (:cherries:)" + U1F338 = "🌸", "🌸 (:cherry_blossom:)" + U265FFE0F = "♟️", "♟️ (:chess_pawn:)" + U265F = "♟", "♟ (:chess_pawn:)" + U1F330 = "🌰", "🌰 (:chestnut:)" + U1F414 = "🐔", "🐔 (:chicken:)" + U1F9D2 = "🧒", "🧒 (:child:)" + U1F9D21F3FF = "🧒🏿", "🧒🏿 (:child_dark_skin_tone:)" + U1F9D21F3FB = "🧒🏻", "🧒🏻 (:child_light_skin_tone:)" + U1F9D21F3FE = "🧒🏾", "🧒🏾 (:child_medium-dark_skin_tone:)" + U1F9D21F3FC = "🧒🏼", "🧒🏼 (:child_medium-light_skin_tone:)" + U1F9D21F3FD = "🧒🏽", "🧒🏽 (:child_medium_skin_tone:)" + U1F6B8 = "🚸", "🚸 (:children_crossing:)" + U1F43FFE0F = "🐿️", "🐿️ (:chipmunk:)" + U1F43F = "🐿", "🐿 (:chipmunk:)" + U1F36B = "🍫", "🍫 (:chocolate_bar:)" + U1F962 = "🥢", "🥢 (:chopsticks:)" + U26EA = "⛪", "⛪ (:church:)" + U1F6AC = "🚬", "🚬 (:cigarette:)" + U1F3A6 = "🎦", "🎦 (:cinema:)" + U24C2FE0F = "Ⓜ️", "Ⓜ️ (:circled_M:)" + U24C2 = "Ⓜ", "Ⓜ (:circled_M:)" + U1F3AA = "🎪", "🎪 (:circus_tent:)" + U1F3D9FE0F = "🏙️", "🏙️ (:cityscape:)" + U1F3D9 = "🏙", "🏙 (:cityscape:)" + U1F306 = "🌆", "🌆 (:cityscape_at_dusk:)" + U1F5DCFE0F = "🗜️", "🗜️ (:clamp:)" + U1F5DC = "🗜", "🗜 (:clamp:)" + U1F3AC = "🎬", "🎬 (:clapper_board:)" + U1F44F = "👏", "👏 (:clapping_hands:)" + U1F44F1F3FF = "👏🏿", "👏🏿 (:clapping_hands_dark_skin_tone:)" + U1F44F1F3FB = "👏🏻", "👏🏻 (:clapping_hands_light_skin_tone:)" + U1F44F1F3FE = "👏🏾", "👏🏾 (:clapping_hands_medium-dark_skin_tone:)" + U1F44F1F3FC = "👏🏼", "👏🏼 (:clapping_hands_medium-light_skin_tone:)" + U1F44F1F3FD = "👏🏽", "👏🏽 (:clapping_hands_medium_skin_tone:)" + U1F3DBFE0F = "🏛️", "🏛️ (:classical_building:)" + U1F3DB = "🏛", "🏛 (:classical_building:)" + U1F37B = "🍻", "🍻 (:clinking_beer_mugs:)" + U1F942 = "🥂", "🥂 (:clinking_glasses:)" + U1F4CB = "📋", "📋 (:clipboard:)" + U1F503 = "🔃", "🔃 (:clockwise_vertical_arrows:)" + U1F4D5 = "📕", "📕 (:closed_book:)" + U1F4EA = "📪", "📪 (:closed_mailbox_with_lowered_flag:)" + U1F4EB = "📫", "📫 (:closed_mailbox_with_raised_flag:)" + U1F302 = "🌂", "🌂 (:closed_umbrella:)" + U2601FE0F = "☁️", "☁️ (:cloud:)" + U2601 = "☁", "☁ (:cloud:)" + U1F329FE0F = "🌩️", "🌩️ (:cloud_with_lightning:)" + U1F329 = "🌩", "🌩 (:cloud_with_lightning:)" + U26C8FE0F = "⛈️", "⛈️ (:cloud_with_lightning_and_rain:)" + U26C8 = "⛈", "⛈ (:cloud_with_lightning_and_rain:)" + U1F327FE0F = "🌧️", "🌧️ (:cloud_with_rain:)" + U1F327 = "🌧", "🌧 (:cloud_with_rain:)" + U1F328FE0F = "🌨️", "🌨️ (:cloud_with_snow:)" + U1F328 = "🌨", "🌨 (:cloud_with_snow:)" + U1F921 = "🤡", "🤡 (:clown_face:)" + U2663FE0F = "♣️", "♣️ (:club_suit:)" + U2663 = "♣", "♣ (:club_suit:)" + U1F45D = "👝", "👝 (:clutch_bag:)" + U1F9E5 = "🧥", "🧥 (:coat:)" + U1FAB3 = "🪳", "🪳 (:cockroach:)" + U1F378 = "🍸", "🍸 (:cocktail_glass:)" + U1F965 = "🥥", "🥥 (:coconut:)" + U26B0FE0F = "⚰️", "⚰️ (:coffin:)" + U26B0 = "⚰", "⚰ (:coffin:)" + U1FA99 = "🪙", "🪙 (:coin:)" + U1F976 = "🥶", "🥶 (:cold_face:)" + U1F4A5 = "💥", "💥 (:collision:)" + U2604FE0F = "☄️", "☄️ (:comet:)" + U2604 = "☄", "☄ (:comet:)" + U1F9ED = "🧭", "🧭 (:compass:)" + U1F4BD = "💽", "💽 (:computer_disk:)" + U1F5B1FE0F = "🖱️", "🖱️ (:computer_mouse:)" + U1F5B1 = "🖱", "🖱 (:computer_mouse:)" + U1F38A = "🎊", "🎊 (:confetti_ball:)" + U1F616 = "😖", "😖 (:confounded_face:)" + U1F615 = "😕", "😕 (:confused_face:)" + U1F6A7 = "🚧", "🚧 (:construction:)" + U1F477 = "👷", "👷 (:construction_worker:)" + U1F4771F3FF = "👷🏿", "👷🏿 (:construction_worker_dark_skin_tone:)" + U1F4771F3FB = "👷🏻", "👷🏻 (:construction_worker_light_skin_tone:)" + U1F4771F3FE = "👷🏾", "👷🏾 (:construction_worker_medium-dark_skin_tone:)" + U1F4771F3FC = "👷🏼", "👷🏼 (:construction_worker_medium-light_skin_tone:)" + U1F4771F3FD = "👷🏽", "👷🏽 (:construction_worker_medium_skin_tone:)" + U1F39BFE0F = "🎛️", "🎛️ (:control_knobs:)" + U1F39B = "🎛", "🎛 (:control_knobs:)" + U1F3EA = "🏪", "🏪 (:convenience_store:)" + U1F9D1200D1F373 = "🧑‍🍳", "🧑‍🍳 (:cook:)" + U1F9D11F3FF200D1F373 = "🧑🏿‍🍳", "🧑🏿‍🍳 (:cook_dark_skin_tone:)" + U1F9D11F3FB200D1F373 = "🧑🏻‍🍳", "🧑🏻‍🍳 (:cook_light_skin_tone:)" + U1F9D11F3FE200D1F373 = "🧑🏾‍🍳", "🧑🏾‍🍳 (:cook_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F373 = "🧑🏼‍🍳", "🧑🏼‍🍳 (:cook_medium-light_skin_tone:)" + U1F9D11F3FD200D1F373 = "🧑🏽‍🍳", "🧑🏽‍🍳 (:cook_medium_skin_tone:)" + U1F35A = "🍚", "🍚 (:cooked_rice:)" + U1F36A = "🍪", "🍪 (:cookie:)" + U1F373 = "🍳", "🍳 (:cooking:)" + UA9FE0F = "©️", "©️ (:copyright:)" + UA9 = "©", "© (:copyright:)" + U1FAB8 = "🪸", "🪸 (:coral:)" + U1F6CBFE0F = "🛋️", "🛋️ (:couch_and_lamp:)" + U1F6CB = "🛋", "🛋 (:couch_and_lamp:)" + U1F504 = "🔄", "🔄 (:counterclockwise_arrows_button:)" + U1F491 = "💑", "💑 (:couple_with_heart:)" + U1F4911F3FF = "💑🏿", "💑🏿 (:couple_with_heart_dark_skin_tone:)" + U1F4911F3FB = "💑🏻", "💑🏻 (:couple_with_heart_light_skin_tone:)" + U1F468200D2764FE0F200D1F468 = "👨‍❤️‍👨", "👨‍❤️‍👨 (:couple_with_heart_man_man:)" + U1F468200D2764200D1F468 = "👨‍❤‍👨", "👨‍❤‍👨 (:couple_with_heart_man_man:)" + U1F4681F3FF200D2764FE0F200D1F4681F3FF = "👨🏿‍❤️‍👨🏿", "👨🏿‍❤️‍👨🏿 (:couple_with_heart_man_man_dark_skin_tone:)" + U1F4681F3FF200D2764200D1F4681F3FF = "👨🏿‍❤‍👨🏿", "👨🏿‍❤‍👨🏿 (:couple_with_heart_man_man_dark_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F4681F3FB = "👨🏿‍❤️‍👨🏻", "👨🏿‍❤️‍👨🏻 (:couple_with_heart_man_man_dark_skin_tone_light_skin_tone:)" + U1F4681F3FF200D2764200D1F4681F3FB = "👨🏿‍❤‍👨🏻", "👨🏿‍❤‍👨🏻 (:couple_with_heart_man_man_dark_skin_tone_light_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F4681F3FE = "👨🏿‍❤️‍👨🏾", "👨🏿‍❤️‍👨🏾 (:couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FF200D2764200D1F4681F3FE = "👨🏿‍❤‍👨🏾", "👨🏿‍❤‍👨🏾 (:couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F4681F3FC = "👨🏿‍❤️‍👨🏼", "👨🏿‍❤️‍👨🏼 (:couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FF200D2764200D1F4681F3FC = "👨🏿‍❤‍👨🏼", "👨🏿‍❤‍👨🏼 (:couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F4681F3FD = "👨🏿‍❤️‍👨🏽", "👨🏿‍❤️‍👨🏽 (:couple_with_heart_man_man_dark_skin_tone_medium_skin_tone:)" + U1F4681F3FF200D2764200D1F4681F3FD = "👨🏿‍❤‍👨🏽", "👨🏿‍❤‍👨🏽 (:couple_with_heart_man_man_dark_skin_tone_medium_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F4681F3FB = "👨🏻‍❤️‍👨🏻", "👨🏻‍❤️‍👨🏻 (:couple_with_heart_man_man_light_skin_tone:)" + U1F4681F3FB200D2764200D1F4681F3FB = "👨🏻‍❤‍👨🏻", "👨🏻‍❤‍👨🏻 (:couple_with_heart_man_man_light_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F4681F3FF = "👨🏻‍❤️‍👨🏿", "👨🏻‍❤️‍👨🏿 (:couple_with_heart_man_man_light_skin_tone_dark_skin_tone:)" + U1F4681F3FB200D2764200D1F4681F3FF = "👨🏻‍❤‍👨🏿", "👨🏻‍❤‍👨🏿 (:couple_with_heart_man_man_light_skin_tone_dark_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F4681F3FE = "👨🏻‍❤️‍👨🏾", "👨🏻‍❤️‍👨🏾 (:couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FB200D2764200D1F4681F3FE = "👨🏻‍❤‍👨🏾", "👨🏻‍❤‍👨🏾 (:couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F4681F3FC = "👨🏻‍❤️‍👨🏼", "👨🏻‍❤️‍👨🏼 (:couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone:)" + U1F4681F3FB200D2764200D1F4681F3FC = "👨🏻‍❤‍👨🏼", "👨🏻‍❤‍👨🏼 (:couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F4681F3FD = "👨🏻‍❤️‍👨🏽", "👨🏻‍❤️‍👨🏽 (:couple_with_heart_man_man_light_skin_tone_medium_skin_tone:)" + U1F4681F3FB200D2764200D1F4681F3FD = "👨🏻‍❤‍👨🏽", "👨🏻‍❤‍👨🏽 (:couple_with_heart_man_man_light_skin_tone_medium_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F4681F3FE = "👨🏾‍❤️‍👨🏾", "👨🏾‍❤️‍👨🏾 (:couple_with_heart_man_man_medium-dark_skin_tone:)" + U1F4681F3FE200D2764200D1F4681F3FE = "👨🏾‍❤‍👨🏾", "👨🏾‍❤‍👨🏾 (:couple_with_heart_man_man_medium-dark_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F4681F3FF = "👨🏾‍❤️‍👨🏿", "👨🏾‍❤️‍👨🏿 (:couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4681F3FE200D2764200D1F4681F3FF = "👨🏾‍❤‍👨🏿", "👨🏾‍❤‍👨🏿 (:couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F4681F3FB = "👨🏾‍❤️‍👨🏻", "👨🏾‍❤️‍👨🏻 (:couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4681F3FE200D2764200D1F4681F3FB = "👨🏾‍❤‍👨🏻", "👨🏾‍❤‍👨🏻 (:couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F4681F3FC = "👨🏾‍❤️‍👨🏼", "👨🏾‍❤️‍👨🏼 (:couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FE200D2764200D1F4681F3FC = "👨🏾‍❤‍👨🏼", "👨🏾‍❤‍👨🏼 (:couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F4681F3FD = "👨🏾‍❤️‍👨🏽", "👨🏾‍❤️‍👨🏽 (:couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4681F3FE200D2764200D1F4681F3FD = "👨🏾‍❤‍👨🏽", "👨🏾‍❤‍👨🏽 (:couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F4681F3FC = "👨🏼‍❤️‍👨🏼", "👨🏼‍❤️‍👨🏼 (:couple_with_heart_man_man_medium-light_skin_tone:)" + U1F4681F3FC200D2764200D1F4681F3FC = "👨🏼‍❤‍👨🏼", "👨🏼‍❤‍👨🏼 (:couple_with_heart_man_man_medium-light_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F4681F3FF = "👨🏼‍❤️‍👨🏿", "👨🏼‍❤️‍👨🏿 (:couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4681F3FC200D2764200D1F4681F3FF = "👨🏼‍❤‍👨🏿", "👨🏼‍❤‍👨🏿 (:couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F4681F3FB = "👨🏼‍❤️‍👨🏻", "👨🏼‍❤️‍👨🏻 (:couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone:)" + U1F4681F3FC200D2764200D1F4681F3FB = "👨🏼‍❤‍👨🏻", "👨🏼‍❤‍👨🏻 (:couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F4681F3FE = "👨🏼‍❤️‍👨🏾", "👨🏼‍❤️‍👨🏾 (:couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FC200D2764200D1F4681F3FE = "👨🏼‍❤‍👨🏾", "👨🏼‍❤‍👨🏾 (:couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F4681F3FD = "👨🏼‍❤️‍👨🏽", "👨🏼‍❤️‍👨🏽 (:couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4681F3FC200D2764200D1F4681F3FD = "👨🏼‍❤‍👨🏽", "👨🏼‍❤‍👨🏽 (:couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F4681F3FD = "👨🏽‍❤️‍👨🏽", "👨🏽‍❤️‍👨🏽 (:couple_with_heart_man_man_medium_skin_tone:)" + U1F4681F3FD200D2764200D1F4681F3FD = "👨🏽‍❤‍👨🏽", "👨🏽‍❤‍👨🏽 (:couple_with_heart_man_man_medium_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F4681F3FF = "👨🏽‍❤️‍👨🏿", "👨🏽‍❤️‍👨🏿 (:couple_with_heart_man_man_medium_skin_tone_dark_skin_tone:)" + U1F4681F3FD200D2764200D1F4681F3FF = "👨🏽‍❤‍👨🏿", "👨🏽‍❤‍👨🏿 (:couple_with_heart_man_man_medium_skin_tone_dark_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F4681F3FB = "👨🏽‍❤️‍👨🏻", "👨🏽‍❤️‍👨🏻 (:couple_with_heart_man_man_medium_skin_tone_light_skin_tone:)" + U1F4681F3FD200D2764200D1F4681F3FB = "👨🏽‍❤‍👨🏻", "👨🏽‍❤‍👨🏻 (:couple_with_heart_man_man_medium_skin_tone_light_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F4681F3FE = "👨🏽‍❤️‍👨🏾", "👨🏽‍❤️‍👨🏾 (:couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FD200D2764200D1F4681F3FE = "👨🏽‍❤‍👨🏾", "👨🏽‍❤‍👨🏾 (:couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F4681F3FC = "👨🏽‍❤️‍👨🏼", "👨🏽‍❤️‍👨🏼 (:couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone:)" + U1F4681F3FD200D2764200D1F4681F3FC = "👨🏽‍❤‍👨🏼", "👨🏽‍❤‍👨🏼 (:couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone:)" + U1F4911F3FE = "💑🏾", "💑🏾 (:couple_with_heart_medium-dark_skin_tone:)" + U1F4911F3FC = "💑🏼", "💑🏼 (:couple_with_heart_medium-light_skin_tone:)" + U1F4911F3FD = "💑🏽", "💑🏽 (:couple_with_heart_medium_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F9D11F3FB = "🧑🏿‍❤️‍🧑🏻", "🧑🏿‍❤️‍🧑🏻 (:couple_with_heart_person_person_dark_skin_tone_light_skin_tone:)" + U1F9D11F3FF200D2764200D1F9D11F3FB = "🧑🏿‍❤‍🧑🏻", "🧑🏿‍❤‍🧑🏻 (:couple_with_heart_person_person_dark_skin_tone_light_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F9D11F3FE = "🧑🏿‍❤️‍🧑🏾", "🧑🏿‍❤️‍🧑🏾 (:couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FF200D2764200D1F9D11F3FE = "🧑🏿‍❤‍🧑🏾", "🧑🏿‍❤‍🧑🏾 (:couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F9D11F3FC = "🧑🏿‍❤️‍🧑🏼", "🧑🏿‍❤️‍🧑🏼 (:couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FF200D2764200D1F9D11F3FC = "🧑🏿‍❤‍🧑🏼", "🧑🏿‍❤‍🧑🏼 (:couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F9D11F3FD = "🧑🏿‍❤️‍🧑🏽", "🧑🏿‍❤️‍🧑🏽 (:couple_with_heart_person_person_dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FF200D2764200D1F9D11F3FD = "🧑🏿‍❤‍🧑🏽", "🧑🏿‍❤‍🧑🏽 (:couple_with_heart_person_person_dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F9D11F3FF = "🧑🏻‍❤️‍🧑🏿", "🧑🏻‍❤️‍🧑🏿 (:couple_with_heart_person_person_light_skin_tone_dark_skin_tone:)" + U1F9D11F3FB200D2764200D1F9D11F3FF = "🧑🏻‍❤‍🧑🏿", "🧑🏻‍❤‍🧑🏿 (:couple_with_heart_person_person_light_skin_tone_dark_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F9D11F3FE = "🧑🏻‍❤️‍🧑🏾", "🧑🏻‍❤️‍🧑🏾 (:couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FB200D2764200D1F9D11F3FE = "🧑🏻‍❤‍🧑🏾", "🧑🏻‍❤‍🧑🏾 (:couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F9D11F3FC = "🧑🏻‍❤️‍🧑🏼", "🧑🏻‍❤️‍🧑🏼 (:couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FB200D2764200D1F9D11F3FC = "🧑🏻‍❤‍🧑🏼", "🧑🏻‍❤‍🧑🏼 (:couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F9D11F3FD = "🧑🏻‍❤️‍🧑🏽", "🧑🏻‍❤️‍🧑🏽 (:couple_with_heart_person_person_light_skin_tone_medium_skin_tone:)" + U1F9D11F3FB200D2764200D1F9D11F3FD = "🧑🏻‍❤‍🧑🏽", "🧑🏻‍❤‍🧑🏽 (:couple_with_heart_person_person_light_skin_tone_medium_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F9D11F3FF = "🧑🏾‍❤️‍🧑🏿", "🧑🏾‍❤️‍🧑🏿 (:couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone:)" + U1F9D11F3FE200D2764200D1F9D11F3FF = "🧑🏾‍❤‍🧑🏿", "🧑🏾‍❤‍🧑🏿 (:couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F9D11F3FB = "🧑🏾‍❤️‍🧑🏻", "🧑🏾‍❤️‍🧑🏻 (:couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone:)" + U1F9D11F3FE200D2764200D1F9D11F3FB = "🧑🏾‍❤‍🧑🏻", "🧑🏾‍❤‍🧑🏻 (:couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F9D11F3FC = "🧑🏾‍❤️‍🧑🏼", "🧑🏾‍❤️‍🧑🏼 (:couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FE200D2764200D1F9D11F3FC = "🧑🏾‍❤‍🧑🏼", "🧑🏾‍❤‍🧑🏼 (:couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F9D11F3FD = "🧑🏾‍❤️‍🧑🏽", "🧑🏾‍❤️‍🧑🏽 (:couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FE200D2764200D1F9D11F3FD = "🧑🏾‍❤‍🧑🏽", "🧑🏾‍❤‍🧑🏽 (:couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F9D11F3FF = "🧑🏼‍❤️‍🧑🏿", "🧑🏼‍❤️‍🧑🏿 (:couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone:)" + U1F9D11F3FC200D2764200D1F9D11F3FF = "🧑🏼‍❤‍🧑🏿", "🧑🏼‍❤‍🧑🏿 (:couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F9D11F3FB = "🧑🏼‍❤️‍🧑🏻", "🧑🏼‍❤️‍🧑🏻 (:couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone:)" + U1F9D11F3FC200D2764200D1F9D11F3FB = "🧑🏼‍❤‍🧑🏻", "🧑🏼‍❤‍🧑🏻 (:couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F9D11F3FE = "🧑🏼‍❤️‍🧑🏾", "🧑🏼‍❤️‍🧑🏾 (:couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FC200D2764200D1F9D11F3FE = "🧑🏼‍❤‍🧑🏾", "🧑🏼‍❤‍🧑🏾 (:couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F9D11F3FD = "🧑🏼‍❤️‍🧑🏽", "🧑🏼‍❤️‍🧑🏽 (:couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone:)" + U1F9D11F3FC200D2764200D1F9D11F3FD = "🧑🏼‍❤‍🧑🏽", "🧑🏼‍❤‍🧑🏽 (:couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F9D11F3FF = "🧑🏽‍❤️‍🧑🏿", "🧑🏽‍❤️‍🧑🏿 (:couple_with_heart_person_person_medium_skin_tone_dark_skin_tone:)" + U1F9D11F3FD200D2764200D1F9D11F3FF = "🧑🏽‍❤‍🧑🏿", "🧑🏽‍❤‍🧑🏿 (:couple_with_heart_person_person_medium_skin_tone_dark_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F9D11F3FB = "🧑🏽‍❤️‍🧑🏻", "🧑🏽‍❤️‍🧑🏻 (:couple_with_heart_person_person_medium_skin_tone_light_skin_tone:)" + U1F9D11F3FD200D2764200D1F9D11F3FB = "🧑🏽‍❤‍🧑🏻", "🧑🏽‍❤‍🧑🏻 (:couple_with_heart_person_person_medium_skin_tone_light_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F9D11F3FE = "🧑🏽‍❤️‍🧑🏾", "🧑🏽‍❤️‍🧑🏾 (:couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FD200D2764200D1F9D11F3FE = "🧑🏽‍❤‍🧑🏾", "🧑🏽‍❤‍🧑🏾 (:couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F9D11F3FC = "🧑🏽‍❤️‍🧑🏼", "🧑🏽‍❤️‍🧑🏼 (:couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FD200D2764200D1F9D11F3FC = "🧑🏽‍❤‍🧑🏼", "🧑🏽‍❤‍🧑🏼 (:couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone:)" + U1F469200D2764FE0F200D1F468 = "👩‍❤️‍👨", "👩‍❤️‍👨 (:couple_with_heart_woman_man:)" + U1F469200D2764200D1F468 = "👩‍❤‍👨", "👩‍❤‍👨 (:couple_with_heart_woman_man:)" + U1F4691F3FF200D2764FE0F200D1F4681F3FF = "👩🏿‍❤️‍👨🏿", "👩🏿‍❤️‍👨🏿 (:couple_with_heart_woman_man_dark_skin_tone:)" + U1F4691F3FF200D2764200D1F4681F3FF = "👩🏿‍❤‍👨🏿", "👩🏿‍❤‍👨🏿 (:couple_with_heart_woman_man_dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4681F3FB = "👩🏿‍❤️‍👨🏻", "👩🏿‍❤️‍👨🏻 (:couple_with_heart_woman_man_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764200D1F4681F3FB = "👩🏿‍❤‍👨🏻", "👩🏿‍❤‍👨🏻 (:couple_with_heart_woman_man_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4681F3FE = "👩🏿‍❤️‍👨🏾", "👩🏿‍❤️‍👨🏾 (:couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764200D1F4681F3FE = "👩🏿‍❤‍👨🏾", "👩🏿‍❤‍👨🏾 (:couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4681F3FC = "👩🏿‍❤️‍👨🏼", "👩🏿‍❤️‍👨🏼 (:couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764200D1F4681F3FC = "👩🏿‍❤‍👨🏼", "👩🏿‍❤‍👨🏼 (:couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4681F3FD = "👩🏿‍❤️‍👨🏽", "👩🏿‍❤️‍👨🏽 (:couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FF200D2764200D1F4681F3FD = "👩🏿‍❤‍👨🏽", "👩🏿‍❤‍👨🏽 (:couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4681F3FB = "👩🏻‍❤️‍👨🏻", "👩🏻‍❤️‍👨🏻 (:couple_with_heart_woman_man_light_skin_tone:)" + U1F4691F3FB200D2764200D1F4681F3FB = "👩🏻‍❤‍👨🏻", "👩🏻‍❤‍👨🏻 (:couple_with_heart_woman_man_light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4681F3FF = "👩🏻‍❤️‍👨🏿", "👩🏻‍❤️‍👨🏿 (:couple_with_heart_woman_man_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764200D1F4681F3FF = "👩🏻‍❤‍👨🏿", "👩🏻‍❤‍👨🏿 (:couple_with_heart_woman_man_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4681F3FE = "👩🏻‍❤️‍👨🏾", "👩🏻‍❤️‍👨🏾 (:couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764200D1F4681F3FE = "👩🏻‍❤‍👨🏾", "👩🏻‍❤‍👨🏾 (:couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4681F3FC = "👩🏻‍❤️‍👨🏼", "👩🏻‍❤️‍👨🏼 (:couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764200D1F4681F3FC = "👩🏻‍❤‍👨🏼", "👩🏻‍❤‍👨🏼 (:couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4681F3FD = "👩🏻‍❤️‍👨🏽", "👩🏻‍❤️‍👨🏽 (:couple_with_heart_woman_man_light_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764200D1F4681F3FD = "👩🏻‍❤‍👨🏽", "👩🏻‍❤‍👨🏽 (:couple_with_heart_woman_man_light_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4681F3FE = "👩🏾‍❤️‍👨🏾", "👩🏾‍❤️‍👨🏾 (:couple_with_heart_woman_man_medium-dark_skin_tone:)" + U1F4691F3FE200D2764200D1F4681F3FE = "👩🏾‍❤‍👨🏾", "👩🏾‍❤‍👨🏾 (:couple_with_heart_woman_man_medium-dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4681F3FF = "👩🏾‍❤️‍👨🏿", "👩🏾‍❤️‍👨🏿 (:couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764200D1F4681F3FF = "👩🏾‍❤‍👨🏿", "👩🏾‍❤‍👨🏿 (:couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4681F3FB = "👩🏾‍❤️‍👨🏻", "👩🏾‍❤️‍👨🏻 (:couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764200D1F4681F3FB = "👩🏾‍❤‍👨🏻", "👩🏾‍❤‍👨🏻 (:couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4681F3FC = "👩🏾‍❤️‍👨🏼", "👩🏾‍❤️‍👨🏼 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764200D1F4681F3FC = "👩🏾‍❤‍👨🏼", "👩🏾‍❤‍👨🏼 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4681F3FD = "👩🏾‍❤️‍👨🏽", "👩🏾‍❤️‍👨🏽 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764200D1F4681F3FD = "👩🏾‍❤‍👨🏽", "👩🏾‍❤‍👨🏽 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4681F3FC = "👩🏼‍❤️‍👨🏼", "👩🏼‍❤️‍👨🏼 (:couple_with_heart_woman_man_medium-light_skin_tone:)" + U1F4691F3FC200D2764200D1F4681F3FC = "👩🏼‍❤‍👨🏼", "👩🏼‍❤‍👨🏼 (:couple_with_heart_woman_man_medium-light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4681F3FF = "👩🏼‍❤️‍👨🏿", "👩🏼‍❤️‍👨🏿 (:couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764200D1F4681F3FF = "👩🏼‍❤‍👨🏿", "👩🏼‍❤‍👨🏿 (:couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4681F3FB = "👩🏼‍❤️‍👨🏻", "👩🏼‍❤️‍👨🏻 (:couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764200D1F4681F3FB = "👩🏼‍❤‍👨🏻", "👩🏼‍❤‍👨🏻 (:couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4681F3FE = "👩🏼‍❤️‍👨🏾", "👩🏼‍❤️‍👨🏾 (:couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764200D1F4681F3FE = "👩🏼‍❤‍👨🏾", "👩🏼‍❤‍👨🏾 (:couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4681F3FD = "👩🏼‍❤️‍👨🏽", "👩🏼‍❤️‍👨🏽 (:couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764200D1F4681F3FD = "👩🏼‍❤‍👨🏽", "👩🏼‍❤‍👨🏽 (:couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4681F3FD = "👩🏽‍❤️‍👨🏽", "👩🏽‍❤️‍👨🏽 (:couple_with_heart_woman_man_medium_skin_tone:)" + U1F4691F3FD200D2764200D1F4681F3FD = "👩🏽‍❤‍👨🏽", "👩🏽‍❤‍👨🏽 (:couple_with_heart_woman_man_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4681F3FF = "👩🏽‍❤️‍👨🏿", "👩🏽‍❤️‍👨🏿 (:couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764200D1F4681F3FF = "👩🏽‍❤‍👨🏿", "👩🏽‍❤‍👨🏿 (:couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4681F3FB = "👩🏽‍❤️‍👨🏻", "👩🏽‍❤️‍👨🏻 (:couple_with_heart_woman_man_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764200D1F4681F3FB = "👩🏽‍❤‍👨🏻", "👩🏽‍❤‍👨🏻 (:couple_with_heart_woman_man_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4681F3FE = "👩🏽‍❤️‍👨🏾", "👩🏽‍❤️‍👨🏾 (:couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764200D1F4681F3FE = "👩🏽‍❤‍👨🏾", "👩🏽‍❤‍👨🏾 (:couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4681F3FC = "👩🏽‍❤️‍👨🏼", "👩🏽‍❤️‍👨🏼 (:couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone:)" + U1F4691F3FD200D2764200D1F4681F3FC = "👩🏽‍❤‍👨🏼", "👩🏽‍❤‍👨🏼 (:couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone:)" + U1F469200D2764FE0F200D1F469 = "👩‍❤️‍👩", "👩‍❤️‍👩 (:couple_with_heart_woman_woman:)" + U1F469200D2764200D1F469 = "👩‍❤‍👩", "👩‍❤‍👩 (:couple_with_heart_woman_woman:)" + U1F4691F3FF200D2764FE0F200D1F4691F3FF = "👩🏿‍❤️‍👩🏿", "👩🏿‍❤️‍👩🏿 (:couple_with_heart_woman_woman_dark_skin_tone:)" + U1F4691F3FF200D2764200D1F4691F3FF = "👩🏿‍❤‍👩🏿", "👩🏿‍❤‍👩🏿 (:couple_with_heart_woman_woman_dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4691F3FB = "👩🏿‍❤️‍👩🏻", "👩🏿‍❤️‍👩🏻 (:couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764200D1F4691F3FB = "👩🏿‍❤‍👩🏻", "👩🏿‍❤‍👩🏻 (:couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4691F3FE = "👩🏿‍❤️‍👩🏾", "👩🏿‍❤️‍👩🏾 (:couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764200D1F4691F3FE = "👩🏿‍❤‍👩🏾", "👩🏿‍❤‍👩🏾 (:couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4691F3FC = "👩🏿‍❤️‍👩🏼", "👩🏿‍❤️‍👩🏼 (:couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764200D1F4691F3FC = "👩🏿‍❤‍👩🏼", "👩🏿‍❤‍👩🏼 (:couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F4691F3FD = "👩🏿‍❤️‍👩🏽", "👩🏿‍❤️‍👩🏽 (:couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FF200D2764200D1F4691F3FD = "👩🏿‍❤‍👩🏽", "👩🏿‍❤‍👩🏽 (:couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4691F3FB = "👩🏻‍❤️‍👩🏻", "👩🏻‍❤️‍👩🏻 (:couple_with_heart_woman_woman_light_skin_tone:)" + U1F4691F3FB200D2764200D1F4691F3FB = "👩🏻‍❤‍👩🏻", "👩🏻‍❤‍👩🏻 (:couple_with_heart_woman_woman_light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4691F3FF = "👩🏻‍❤️‍👩🏿", "👩🏻‍❤️‍👩🏿 (:couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764200D1F4691F3FF = "👩🏻‍❤‍👩🏿", "👩🏻‍❤‍👩🏿 (:couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4691F3FE = "👩🏻‍❤️‍👩🏾", "👩🏻‍❤️‍👩🏾 (:couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764200D1F4691F3FE = "👩🏻‍❤‍👩🏾", "👩🏻‍❤‍👩🏾 (:couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4691F3FC = "👩🏻‍❤️‍👩🏼", "👩🏻‍❤️‍👩🏼 (:couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764200D1F4691F3FC = "👩🏻‍❤‍👩🏼", "👩🏻‍❤‍👩🏼 (:couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F4691F3FD = "👩🏻‍❤️‍👩🏽", "👩🏻‍❤️‍👩🏽 (:couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764200D1F4691F3FD = "👩🏻‍❤‍👩🏽", "👩🏻‍❤‍👩🏽 (:couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4691F3FE = "👩🏾‍❤️‍👩🏾", "👩🏾‍❤️‍👩🏾 (:couple_with_heart_woman_woman_medium-dark_skin_tone:)" + U1F4691F3FE200D2764200D1F4691F3FE = "👩🏾‍❤‍👩🏾", "👩🏾‍❤‍👩🏾 (:couple_with_heart_woman_woman_medium-dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4691F3FF = "👩🏾‍❤️‍👩🏿", "👩🏾‍❤️‍👩🏿 (:couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764200D1F4691F3FF = "👩🏾‍❤‍👩🏿", "👩🏾‍❤‍👩🏿 (:couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4691F3FB = "👩🏾‍❤️‍👩🏻", "👩🏾‍❤️‍👩🏻 (:couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764200D1F4691F3FB = "👩🏾‍❤‍👩🏻", "👩🏾‍❤‍👩🏻 (:couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4691F3FC = "👩🏾‍❤️‍👩🏼", "👩🏾‍❤️‍👩🏼 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764200D1F4691F3FC = "👩🏾‍❤‍👩🏼", "👩🏾‍❤‍👩🏼 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F4691F3FD = "👩🏾‍❤️‍👩🏽", "👩🏾‍❤️‍👩🏽 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764200D1F4691F3FD = "👩🏾‍❤‍👩🏽", "👩🏾‍❤‍👩🏽 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4691F3FC = "👩🏼‍❤️‍👩🏼", "👩🏼‍❤️‍👩🏼 (:couple_with_heart_woman_woman_medium-light_skin_tone:)" + U1F4691F3FC200D2764200D1F4691F3FC = "👩🏼‍❤‍👩🏼", "👩🏼‍❤‍👩🏼 (:couple_with_heart_woman_woman_medium-light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4691F3FF = "👩🏼‍❤️‍👩🏿", "👩🏼‍❤️‍👩🏿 (:couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764200D1F4691F3FF = "👩🏼‍❤‍👩🏿", "👩🏼‍❤‍👩🏿 (:couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4691F3FB = "👩🏼‍❤️‍👩🏻", "👩🏼‍❤️‍👩🏻 (:couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764200D1F4691F3FB = "👩🏼‍❤‍👩🏻", "👩🏼‍❤‍👩🏻 (:couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4691F3FE = "👩🏼‍❤️‍👩🏾", "👩🏼‍❤️‍👩🏾 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764200D1F4691F3FE = "👩🏼‍❤‍👩🏾", "👩🏼‍❤‍👩🏾 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F4691F3FD = "👩🏼‍❤️‍👩🏽", "👩🏼‍❤️‍👩🏽 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764200D1F4691F3FD = "👩🏼‍❤‍👩🏽", "👩🏼‍❤‍👩🏽 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4691F3FD = "👩🏽‍❤️‍👩🏽", "👩🏽‍❤️‍👩🏽 (:couple_with_heart_woman_woman_medium_skin_tone:)" + U1F4691F3FD200D2764200D1F4691F3FD = "👩🏽‍❤‍👩🏽", "👩🏽‍❤‍👩🏽 (:couple_with_heart_woman_woman_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4691F3FF = "👩🏽‍❤️‍👩🏿", "👩🏽‍❤️‍👩🏿 (:couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764200D1F4691F3FF = "👩🏽‍❤‍👩🏿", "👩🏽‍❤‍👩🏿 (:couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4691F3FB = "👩🏽‍❤️‍👩🏻", "👩🏽‍❤️‍👩🏻 (:couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764200D1F4691F3FB = "👩🏽‍❤‍👩🏻", "👩🏽‍❤‍👩🏻 (:couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4691F3FE = "👩🏽‍❤️‍👩🏾", "👩🏽‍❤️‍👩🏾 (:couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764200D1F4691F3FE = "👩🏽‍❤‍👩🏾", "👩🏽‍❤‍👩🏾 (:couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F4691F3FC = "👩🏽‍❤️‍👩🏼", "👩🏽‍❤️‍👩🏼 (:couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone:)" + U1F4691F3FD200D2764200D1F4691F3FC = "👩🏽‍❤‍👩🏼", "👩🏽‍❤‍👩🏼 (:couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone:)" + U1F404 = "🐄", "🐄 (:cow:)" + U1F42E = "🐮", "🐮 (:cow_face:)" + U1F920 = "🤠", "🤠 (:cowboy_hat_face:)" + U1F980 = "🦀", "🦀 (:crab:)" + U1F58DFE0F = "🖍️", "🖍️ (:crayon:)" + U1F58D = "🖍", "🖍 (:crayon:)" + U1F4B3 = "💳", "💳 (:credit_card:)" + U1F319 = "🌙", "🌙 (:crescent_moon:)" + U1F997 = "🦗", "🦗 (:cricket:)" + U1F3CF = "🏏", "🏏 (:cricket_game:)" + U1F40A = "🐊", "🐊 (:crocodile:)" + U1F950 = "🥐", "🥐 (:croissant:)" + U274C = "❌", "❌ (:cross_mark:)" + U274E = "❎", "❎ (:cross_mark_button:)" + U1F91E = "🤞", "🤞 (:crossed_fingers:)" + U1F91E1F3FF = "🤞🏿", "🤞🏿 (:crossed_fingers_dark_skin_tone:)" + U1F91E1F3FB = "🤞🏻", "🤞🏻 (:crossed_fingers_light_skin_tone:)" + U1F91E1F3FE = "🤞🏾", "🤞🏾 (:crossed_fingers_medium-dark_skin_tone:)" + U1F91E1F3FC = "🤞🏼", "🤞🏼 (:crossed_fingers_medium-light_skin_tone:)" + U1F91E1F3FD = "🤞🏽", "🤞🏽 (:crossed_fingers_medium_skin_tone:)" + U1F38C = "🎌", "🎌 (:crossed_flags:)" + U2694FE0F = "⚔️", "⚔️ (:crossed_swords:)" + U2694 = "⚔", "⚔ (:crossed_swords:)" + U1F451 = "👑", "👑 (:crown:)" + U1FA7C = "🩼", "🩼 (:crutch:)" + U1F63F = "😿", "😿 (:crying_cat:)" + U1F622 = "😢", "😢 (:crying_face:)" + U1F52E = "🔮", "🔮 (:crystal_ball:)" + U1F952 = "🥒", "🥒 (:cucumber:)" + U1F964 = "🥤", "🥤 (:cup_with_straw:)" + U1F9C1 = "🧁", "🧁 (:cupcake:)" + U1F94C = "🥌", "🥌 (:curling_stone:)" + U1F9B1 = "🦱", "🦱 (:curly_hair:)" + U27B0 = "➰", "➰ (:curly_loop:)" + U1F4B1 = "💱", "💱 (:currency_exchange:)" + U1F35B = "🍛", "🍛 (:curry_rice:)" + U1F36E = "🍮", "🍮 (:custard:)" + U1F6C3 = "🛃", "🛃 (:customs:)" + U1F969 = "🥩", "🥩 (:cut_of_meat:)" + U1F300 = "🌀", "🌀 (:cyclone:)" + U1F5E1FE0F = "🗡️", "🗡️ (:dagger:)" + U1F5E1 = "🗡", "🗡 (:dagger:)" + U1F361 = "🍡", "🍡 (:dango:)" + U1F3FF = "🏿", "🏿 (:dark_skin_tone:)" + U1F4A8 = "💨", "💨 (:dashing_away:)" + U1F9CF200D2642FE0F = "🧏‍♂️", "🧏‍♂️ (:deaf_man:)" + U1F9CF200D2642 = "🧏‍♂", "🧏‍♂ (:deaf_man:)" + U1F9CF1F3FF200D2642FE0F = "🧏🏿‍♂️", "🧏🏿‍♂️ (:deaf_man_dark_skin_tone:)" + U1F9CF1F3FF200D2642 = "🧏🏿‍♂", "🧏🏿‍♂ (:deaf_man_dark_skin_tone:)" + U1F9CF1F3FB200D2642FE0F = "🧏🏻‍♂️", "🧏🏻‍♂️ (:deaf_man_light_skin_tone:)" + U1F9CF1F3FB200D2642 = "🧏🏻‍♂", "🧏🏻‍♂ (:deaf_man_light_skin_tone:)" + U1F9CF1F3FE200D2642FE0F = "🧏🏾‍♂️", "🧏🏾‍♂️ (:deaf_man_medium-dark_skin_tone:)" + U1F9CF1F3FE200D2642 = "🧏🏾‍♂", "🧏🏾‍♂ (:deaf_man_medium-dark_skin_tone:)" + U1F9CF1F3FC200D2642FE0F = "🧏🏼‍♂️", "🧏🏼‍♂️ (:deaf_man_medium-light_skin_tone:)" + U1F9CF1F3FC200D2642 = "🧏🏼‍♂", "🧏🏼‍♂ (:deaf_man_medium-light_skin_tone:)" + U1F9CF1F3FD200D2642FE0F = "🧏🏽‍♂️", "🧏🏽‍♂️ (:deaf_man_medium_skin_tone:)" + U1F9CF1F3FD200D2642 = "🧏🏽‍♂", "🧏🏽‍♂ (:deaf_man_medium_skin_tone:)" + U1F9CF = "🧏", "🧏 (:deaf_person:)" + U1F9CF1F3FF = "🧏🏿", "🧏🏿 (:deaf_person_dark_skin_tone:)" + U1F9CF1F3FB = "🧏🏻", "🧏🏻 (:deaf_person_light_skin_tone:)" + U1F9CF1F3FE = "🧏🏾", "🧏🏾 (:deaf_person_medium-dark_skin_tone:)" + U1F9CF1F3FC = "🧏🏼", "🧏🏼 (:deaf_person_medium-light_skin_tone:)" + U1F9CF1F3FD = "🧏🏽", "🧏🏽 (:deaf_person_medium_skin_tone:)" + U1F9CF200D2640FE0F = "🧏‍♀️", "🧏‍♀️ (:deaf_woman:)" + U1F9CF200D2640 = "🧏‍♀", "🧏‍♀ (:deaf_woman:)" + U1F9CF1F3FF200D2640FE0F = "🧏🏿‍♀️", "🧏🏿‍♀️ (:deaf_woman_dark_skin_tone:)" + U1F9CF1F3FF200D2640 = "🧏🏿‍♀", "🧏🏿‍♀ (:deaf_woman_dark_skin_tone:)" + U1F9CF1F3FB200D2640FE0F = "🧏🏻‍♀️", "🧏🏻‍♀️ (:deaf_woman_light_skin_tone:)" + U1F9CF1F3FB200D2640 = "🧏🏻‍♀", "🧏🏻‍♀ (:deaf_woman_light_skin_tone:)" + U1F9CF1F3FE200D2640FE0F = "🧏🏾‍♀️", "🧏🏾‍♀️ (:deaf_woman_medium-dark_skin_tone:)" + U1F9CF1F3FE200D2640 = "🧏🏾‍♀", "🧏🏾‍♀ (:deaf_woman_medium-dark_skin_tone:)" + U1F9CF1F3FC200D2640FE0F = "🧏🏼‍♀️", "🧏🏼‍♀️ (:deaf_woman_medium-light_skin_tone:)" + U1F9CF1F3FC200D2640 = "🧏🏼‍♀", "🧏🏼‍♀ (:deaf_woman_medium-light_skin_tone:)" + U1F9CF1F3FD200D2640FE0F = "🧏🏽‍♀️", "🧏🏽‍♀️ (:deaf_woman_medium_skin_tone:)" + U1F9CF1F3FD200D2640 = "🧏🏽‍♀", "🧏🏽‍♀ (:deaf_woman_medium_skin_tone:)" + U1F333 = "🌳", "🌳 (:deciduous_tree:)" + U1F98C = "🦌", "🦌 (:deer:)" + U1F69A = "🚚", "🚚 (:delivery_truck:)" + U1F3EC = "🏬", "🏬 (:department_store:)" + U1F3DAFE0F = "🏚️", "🏚️ (:derelict_house:)" + U1F3DA = "🏚", "🏚 (:derelict_house:)" + U1F3DCFE0F = "🏜️", "🏜️ (:desert:)" + U1F3DC = "🏜", "🏜 (:desert:)" + U1F3DDFE0F = "🏝️", "🏝️ (:desert_island:)" + U1F3DD = "🏝", "🏝 (:desert_island:)" + U1F5A5FE0F = "🖥️", "🖥️ (:desktop_computer:)" + U1F5A5 = "🖥", "🖥 (:desktop_computer:)" + U1F575FE0F = "🕵️", "🕵️ (:detective:)" + U1F575 = "🕵", "🕵 (:detective:)" + U1F5751F3FF = "🕵🏿", "🕵🏿 (:detective_dark_skin_tone:)" + U1F5751F3FB = "🕵🏻", "🕵🏻 (:detective_light_skin_tone:)" + U1F5751F3FE = "🕵🏾", "🕵🏾 (:detective_medium-dark_skin_tone:)" + U1F5751F3FC = "🕵🏼", "🕵🏼 (:detective_medium-light_skin_tone:)" + U1F5751F3FD = "🕵🏽", "🕵🏽 (:detective_medium_skin_tone:)" + U2666FE0F = "♦️", "♦️ (:diamond_suit:)" + U2666 = "♦", "♦ (:diamond_suit:)" + U1F4A0 = "💠", "💠 (:diamond_with_a_dot:)" + U1F505 = "🔅", "🔅 (:dim_button:)" + U1F61E = "😞", "😞 (:disappointed_face:)" + U1F978 = "🥸", "🥸 (:disguised_face:)" + U2797 = "➗", "➗ (:divide:)" + U1F93F = "🤿", "🤿 (:diving_mask:)" + U1FA94 = "🪔", "🪔 (:diya_lamp:)" + U1F4AB = "💫", "💫 (:dizzy:)" + U1F9EC = "🧬", "🧬 (:dna:)" + U1F9A4 = "🦤", "🦤 (:dodo:)" + U1F415 = "🐕", "🐕 (:dog:)" + U1F436 = "🐶", "🐶 (:dog_face:)" + U1F4B5 = "💵", "💵 (:dollar_banknote:)" + U1F42C = "🐬", "🐬 (:dolphin:)" + U1FACF = "🫏", "🫏 (:donkey:)" + U1F6AA = "🚪", "🚪 (:door:)" + U1FAE5 = "🫥", "🫥 (:dotted_line_face:)" + U1F52F = "🔯", "🔯 (:dotted_six-pointed_star:)" + U27BF = "➿", "➿ (:double_curly_loop:)" + U203CFE0F = "‼️", "‼️ (:double_exclamation_mark:)" + U203C = "‼", "‼ (:double_exclamation_mark:)" + U1F369 = "🍩", "🍩 (:doughnut:)" + U1F54AFE0F = "🕊️", "🕊️ (:dove:)" + U1F54A = "🕊", "🕊 (:dove:)" + U2199FE0F = "↙️", "↙️ (:down-left_arrow:)" + U2199 = "↙", "↙ (:down-left_arrow:)" + U2198FE0F = "↘️", "↘️ (:down-right_arrow:)" + U2198 = "↘", "↘ (:down-right_arrow:)" + U2B07FE0F = "⬇️", "⬇️ (:down_arrow:)" + U2B07 = "⬇", "⬇ (:down_arrow:)" + U1F613 = "😓", "😓 (:downcast_face_with_sweat:)" + U1F53D = "🔽", "🔽 (:downwards_button:)" + U1F409 = "🐉", "🐉 (:dragon:)" + U1F432 = "🐲", "🐲 (:dragon_face:)" + U1F457 = "👗", "👗 (:dress:)" + U1F924 = "🤤", "🤤 (:drooling_face:)" + U1FA78 = "🩸", "🩸 (:drop_of_blood:)" + U1F4A7 = "💧", "💧 (:droplet:)" + U1F941 = "🥁", "🥁 (:drum:)" + U1F986 = "🦆", "🦆 (:duck:)" + U1F95F = "🥟", "🥟 (:dumpling:)" + U1F4C0 = "📀", "📀 (:dvd:)" + U1F4E7 = "📧", "📧 (:e-mail:)" + U1F985 = "🦅", "🦅 (:eagle:)" + U1F442 = "👂", "👂 (:ear:)" + U1F4421F3FF = "👂🏿", "👂🏿 (:ear_dark_skin_tone:)" + U1F4421F3FB = "👂🏻", "👂🏻 (:ear_light_skin_tone:)" + U1F4421F3FE = "👂🏾", "👂🏾 (:ear_medium-dark_skin_tone:)" + U1F4421F3FC = "👂🏼", "👂🏼 (:ear_medium-light_skin_tone:)" + U1F4421F3FD = "👂🏽", "👂🏽 (:ear_medium_skin_tone:)" + U1F33D = "🌽", "🌽 (:ear_of_corn:)" + U1F9BB = "🦻", "🦻 (:ear_with_hearing_aid:)" + U1F9BB1F3FF = "🦻🏿", "🦻🏿 (:ear_with_hearing_aid_dark_skin_tone:)" + U1F9BB1F3FB = "🦻🏻", "🦻🏻 (:ear_with_hearing_aid_light_skin_tone:)" + U1F9BB1F3FE = "🦻🏾", "🦻🏾 (:ear_with_hearing_aid_medium-dark_skin_tone:)" + U1F9BB1F3FC = "🦻🏼", "🦻🏼 (:ear_with_hearing_aid_medium-light_skin_tone:)" + U1F9BB1F3FD = "🦻🏽", "🦻🏽 (:ear_with_hearing_aid_medium_skin_tone:)" + U1F95A = "🥚", "🥚 (:egg:)" + U1F346 = "🍆", "🍆 (:eggplant:)" + U2734FE0F = "✴️", "✴️ (:eight-pointed_star:)" + U2734 = "✴", "✴ (:eight-pointed_star:)" + U2733FE0F = "✳️", "✳️ (:eight-spoked_asterisk:)" + U2733 = "✳", "✳ (:eight-spoked_asterisk:)" + U1F563 = "🕣", "🕣 (:eight-thirty:)" + U1F557 = "🕗", "🕗 (:eight_o’clock:)" + U23CFFE0F = "⏏️", "⏏️ (:eject_button:)" + U23CF = "⏏", "⏏ (:eject_button:)" + U1F50C = "🔌", "🔌 (:electric_plug:)" + U1F418 = "🐘", "🐘 (:elephant:)" + U1F6D7 = "🛗", "🛗 (:elevator:)" + U1F566 = "🕦", "🕦 (:eleven-thirty:)" + U1F55A = "🕚", "🕚 (:eleven_o’clock:)" + U1F9DD = "🧝", "🧝 (:elf:)" + U1F9DD1F3FF = "🧝🏿", "🧝🏿 (:elf_dark_skin_tone:)" + U1F9DD1F3FB = "🧝🏻", "🧝🏻 (:elf_light_skin_tone:)" + U1F9DD1F3FE = "🧝🏾", "🧝🏾 (:elf_medium-dark_skin_tone:)" + U1F9DD1F3FC = "🧝🏼", "🧝🏼 (:elf_medium-light_skin_tone:)" + U1F9DD1F3FD = "🧝🏽", "🧝🏽 (:elf_medium_skin_tone:)" + U1FAB9 = "🪹", "🪹 (:empty_nest:)" + U1F621 = "😡", "😡 (:enraged_face:)" + U2709FE0F = "✉️", "✉️ (:envelope:)" + U2709 = "✉", "✉ (:envelope:)" + U1F4E9 = "📩", "📩 (:envelope_with_arrow:)" + U1F4B6 = "💶", "💶 (:euro_banknote:)" + U1F332 = "🌲", "🌲 (:evergreen_tree:)" + U1F411 = "🐑", "🐑 (:ewe:)" + U2049FE0F = "⁉️", "⁉️ (:exclamation_question_mark:)" + U2049 = "⁉", "⁉ (:exclamation_question_mark:)" + U1F92F = "🤯", "🤯 (:exploding_head:)" + U1F611 = "😑", "😑 (:expressionless_face:)" + U1F441FE0F = "👁️", "👁️ (:eye:)" + U1F441 = "👁", "👁 (:eye:)" + U1F441FE0F200D1F5E8FE0F = "👁️‍🗨️", "👁️‍🗨️ (:eye_in_speech_bubble:)" + U1F441200D1F5E8FE0F = "👁‍🗨️", "👁‍🗨️ (:eye_in_speech_bubble:)" + U1F441FE0F200D1F5E8 = "👁️‍🗨", "👁️‍🗨 (:eye_in_speech_bubble:)" + U1F441200D1F5E8 = "👁‍🗨", "👁‍🗨 (:eye_in_speech_bubble:)" + U1F440 = "👀", "👀 (:eyes:)" + U1F618 = "😘", "😘 (:face_blowing_a_kiss:)" + U1F62E200D1F4A8 = "😮‍💨", "😮‍💨 (:face_exhaling:)" + U1F979 = "🥹", "🥹 (:face_holding_back_tears:)" + U1F636200D1F32BFE0F = "😶‍🌫️", "😶‍🌫️ (:face_in_clouds:)" + U1F636200D1F32B = "😶‍🌫", "😶‍🌫 (:face_in_clouds:)" + U1F60B = "😋", "😋 (:face_savoring_food:)" + U1F631 = "😱", "😱 (:face_screaming_in_fear:)" + U1F92E = "🤮", "🤮 (:face_vomiting:)" + U1F635 = "😵", "😵 (:face_with_crossed-out_eyes:)" + U1FAE4 = "🫤", "🫤 (:face_with_diagonal_mouth:)" + U1F92D = "🤭", "🤭 (:face_with_hand_over_mouth:)" + U1F915 = "🤕", "🤕 (:face_with_head-bandage:)" + U1F637 = "😷", "😷 (:face_with_medical_mask:)" + U1F9D0 = "🧐", "🧐 (:face_with_monocle:)" + U1FAE2 = "🫢", "🫢 (:face_with_open_eyes_and_hand_over_mouth:)" + U1F62E = "😮", "😮 (:face_with_open_mouth:)" + U1FAE3 = "🫣", "🫣 (:face_with_peeking_eye:)" + U1F928 = "🤨", "🤨 (:face_with_raised_eyebrow:)" + U1F644 = "🙄", "🙄 (:face_with_rolling_eyes:)" + U1F635200D1F4AB = "😵‍💫", "😵‍💫 (:face_with_spiral_eyes:)" + U1F624 = "😤", "😤 (:face_with_steam_from_nose:)" + U1F92C = "🤬", "🤬 (:face_with_symbols_on_mouth:)" + U1F602 = "😂", "😂 (:face_with_tears_of_joy:)" + U1F912 = "🤒", "🤒 (:face_with_thermometer:)" + U1F61B = "😛", "😛 (:face_with_tongue:)" + U1F636 = "😶", "😶 (:face_without_mouth:)" + U1F3ED = "🏭", "🏭 (:factory:)" + U1F9D1200D1F3ED = "🧑‍🏭", "🧑‍🏭 (:factory_worker:)" + U1F9D11F3FF200D1F3ED = "🧑🏿‍🏭", "🧑🏿‍🏭 (:factory_worker_dark_skin_tone:)" + U1F9D11F3FB200D1F3ED = "🧑🏻‍🏭", "🧑🏻‍🏭 (:factory_worker_light_skin_tone:)" + U1F9D11F3FE200D1F3ED = "🧑🏾‍🏭", "🧑🏾‍🏭 (:factory_worker_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F3ED = "🧑🏼‍🏭", "🧑🏼‍🏭 (:factory_worker_medium-light_skin_tone:)" + U1F9D11F3FD200D1F3ED = "🧑🏽‍🏭", "🧑🏽‍🏭 (:factory_worker_medium_skin_tone:)" + U1F9DA = "🧚", "🧚 (:fairy:)" + U1F9DA1F3FF = "🧚🏿", "🧚🏿 (:fairy_dark_skin_tone:)" + U1F9DA1F3FB = "🧚🏻", "🧚🏻 (:fairy_light_skin_tone:)" + U1F9DA1F3FE = "🧚🏾", "🧚🏾 (:fairy_medium-dark_skin_tone:)" + U1F9DA1F3FC = "🧚🏼", "🧚🏼 (:fairy_medium-light_skin_tone:)" + U1F9DA1F3FD = "🧚🏽", "🧚🏽 (:fairy_medium_skin_tone:)" + U1F9C6 = "🧆", "🧆 (:falafel:)" + U1F342 = "🍂", "🍂 (:fallen_leaf:)" + U1F46A = "👪", "👪 (:family:)" + U1F9D1200D1F9D1200D1F9D2 = "🧑‍🧑‍🧒", "🧑‍🧑‍🧒 (:family_adult_adult_child:)" + U1F9D1200D1F9D1200D1F9D2200D1F9D2 = "🧑‍🧑‍🧒‍🧒", "🧑‍🧑‍🧒‍🧒 (:family_adult_adult_child_child:)" + U1F9D1200D1F9D2 = "🧑‍🧒", "🧑‍🧒 (:family_adult_child:)" + U1F9D1200D1F9D2200D1F9D2 = "🧑‍🧒‍🧒", "🧑‍🧒‍🧒 (:family_adult_child_child:)" + U1F468200D1F466 = "👨‍👦", "👨‍👦 (:family_man_boy:)" + U1F468200D1F466200D1F466 = "👨‍👦‍👦", "👨‍👦‍👦 (:family_man_boy_boy:)" + U1F468200D1F467 = "👨‍👧", "👨‍👧 (:family_man_girl:)" + U1F468200D1F467200D1F466 = "👨‍👧‍👦", "👨‍👧‍👦 (:family_man_girl_boy:)" + U1F468200D1F467200D1F467 = "👨‍👧‍👧", "👨‍👧‍👧 (:family_man_girl_girl:)" + U1F468200D1F468200D1F466 = "👨‍👨‍👦", "👨‍👨‍👦 (:family_man_man_boy:)" + U1F468200D1F468200D1F466200D1F466 = "👨‍👨‍👦‍👦", "👨‍👨‍👦‍👦 (:family_man_man_boy_boy:)" + U1F468200D1F468200D1F467 = "👨‍👨‍👧", "👨‍👨‍👧 (:family_man_man_girl:)" + U1F468200D1F468200D1F467200D1F466 = "👨‍👨‍👧‍👦", "👨‍👨‍👧‍👦 (:family_man_man_girl_boy:)" + U1F468200D1F468200D1F467200D1F467 = "👨‍👨‍👧‍👧", "👨‍👨‍👧‍👧 (:family_man_man_girl_girl:)" + U1F468200D1F469200D1F466 = "👨‍👩‍👦", "👨‍👩‍👦 (:family_man_woman_boy:)" + U1F468200D1F469200D1F466200D1F466 = "👨‍👩‍👦‍👦", "👨‍👩‍👦‍👦 (:family_man_woman_boy_boy:)" + U1F468200D1F469200D1F467 = "👨‍👩‍👧", "👨‍👩‍👧 (:family_man_woman_girl:)" + U1F468200D1F469200D1F467200D1F466 = "👨‍👩‍👧‍👦", "👨‍👩‍👧‍👦 (:family_man_woman_girl_boy:)" + U1F468200D1F469200D1F467200D1F467 = "👨‍👩‍👧‍👧", "👨‍👩‍👧‍👧 (:family_man_woman_girl_girl:)" + U1F469200D1F466 = "👩‍👦", "👩‍👦 (:family_woman_boy:)" + U1F469200D1F466200D1F466 = "👩‍👦‍👦", "👩‍👦‍👦 (:family_woman_boy_boy:)" + U1F469200D1F467 = "👩‍👧", "👩‍👧 (:family_woman_girl:)" + U1F469200D1F467200D1F466 = "👩‍👧‍👦", "👩‍👧‍👦 (:family_woman_girl_boy:)" + U1F469200D1F467200D1F467 = "👩‍👧‍👧", "👩‍👧‍👧 (:family_woman_girl_girl:)" + U1F469200D1F469200D1F466 = "👩‍👩‍👦", "👩‍👩‍👦 (:family_woman_woman_boy:)" + U1F469200D1F469200D1F466200D1F466 = "👩‍👩‍👦‍👦", "👩‍👩‍👦‍👦 (:family_woman_woman_boy_boy:)" + U1F469200D1F469200D1F467 = "👩‍👩‍👧", "👩‍👩‍👧 (:family_woman_woman_girl:)" + U1F469200D1F469200D1F467200D1F466 = "👩‍👩‍👧‍👦", "👩‍👩‍👧‍👦 (:family_woman_woman_girl_boy:)" + U1F469200D1F469200D1F467200D1F467 = "👩‍👩‍👧‍👧", "👩‍👩‍👧‍👧 (:family_woman_woman_girl_girl:)" + U1F9D1200D1F33E = "🧑‍🌾", "🧑‍🌾 (:farmer:)" + U1F9D11F3FF200D1F33E = "🧑🏿‍🌾", "🧑🏿‍🌾 (:farmer_dark_skin_tone:)" + U1F9D11F3FB200D1F33E = "🧑🏻‍🌾", "🧑🏻‍🌾 (:farmer_light_skin_tone:)" + U1F9D11F3FE200D1F33E = "🧑🏾‍🌾", "🧑🏾‍🌾 (:farmer_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F33E = "🧑🏼‍🌾", "🧑🏼‍🌾 (:farmer_medium-light_skin_tone:)" + U1F9D11F3FD200D1F33E = "🧑🏽‍🌾", "🧑🏽‍🌾 (:farmer_medium_skin_tone:)" + U23E9 = "⏩", "⏩ (:fast-forward_button:)" + U23EC = "⏬", "⏬ (:fast_down_button:)" + U23EA = "⏪", "⏪ (:fast_reverse_button:)" + U23EB = "⏫", "⏫ (:fast_up_button:)" + U1F4E0 = "📠", "📠 (:fax_machine:)" + U1F628 = "😨", "😨 (:fearful_face:)" + U1FAB6 = "🪶", "🪶 (:feather:)" + U2640FE0F = "♀️", "♀️ (:female_sign:)" + U2640 = "♀", "♀ (:female_sign:)" + U1F3A1 = "🎡", "🎡 (:ferris_wheel:)" + U26F4FE0F = "⛴️", "⛴️ (:ferry:)" + U26F4 = "⛴", "⛴ (:ferry:)" + U1F3D1 = "🏑", "🏑 (:field_hockey:)" + U1F5C4FE0F = "🗄️", "🗄️ (:file_cabinet:)" + U1F5C4 = "🗄", "🗄 (:file_cabinet:)" + U1F4C1 = "📁", "📁 (:file_folder:)" + U1F39EFE0F = "🎞️", "🎞️ (:film_frames:)" + U1F39E = "🎞", "🎞 (:film_frames:)" + U1F4FDFE0F = "📽️", "📽️ (:film_projector:)" + U1F4FD = "📽", "📽 (:film_projector:)" + U1F525 = "🔥", "🔥 (:fire:)" + U1F692 = "🚒", "🚒 (:fire_engine:)" + U1F9EF = "🧯", "🧯 (:fire_extinguisher:)" + U1F9E8 = "🧨", "🧨 (:firecracker:)" + U1F9D1200D1F692 = "🧑‍🚒", "🧑‍🚒 (:firefighter:)" + U1F9D11F3FF200D1F692 = "🧑🏿‍🚒", "🧑🏿‍🚒 (:firefighter_dark_skin_tone:)" + U1F9D11F3FB200D1F692 = "🧑🏻‍🚒", "🧑🏻‍🚒 (:firefighter_light_skin_tone:)" + U1F9D11F3FE200D1F692 = "🧑🏾‍🚒", "🧑🏾‍🚒 (:firefighter_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F692 = "🧑🏼‍🚒", "🧑🏼‍🚒 (:firefighter_medium-light_skin_tone:)" + U1F9D11F3FD200D1F692 = "🧑🏽‍🚒", "🧑🏽‍🚒 (:firefighter_medium_skin_tone:)" + U1F386 = "🎆", "🎆 (:fireworks:)" + U1F313 = "🌓", "🌓 (:first_quarter_moon:)" + U1F31B = "🌛", "🌛 (:first_quarter_moon_face:)" + U1F41F = "🐟", "🐟 (:fish:)" + U1F365 = "🍥", "🍥 (:fish_cake_with_swirl:)" + U1F3A3 = "🎣", "🎣 (:fishing_pole:)" + U1F560 = "🕠", "🕠 (:five-thirty:)" + U1F554 = "🕔", "🕔 (:five_o’clock:)" + U26F3 = "⛳", "⛳ (:flag_in_hole:)" + U1F9A9 = "🦩", "🦩 (:flamingo:)" + U1F526 = "🔦", "🔦 (:flashlight:)" + U1F97F = "🥿", "🥿 (:flat_shoe:)" + U1FAD3 = "🫓", "🫓 (:flatbread:)" + U269CFE0F = "⚜️", "⚜️ (:fleur-de-lis:)" + U269C = "⚜", "⚜ (:fleur-de-lis:)" + U1F4AA = "💪", "💪 (:flexed_biceps:)" + U1F4AA1F3FF = "💪🏿", "💪🏿 (:flexed_biceps_dark_skin_tone:)" + U1F4AA1F3FB = "💪🏻", "💪🏻 (:flexed_biceps_light_skin_tone:)" + U1F4AA1F3FE = "💪🏾", "💪🏾 (:flexed_biceps_medium-dark_skin_tone:)" + U1F4AA1F3FC = "💪🏼", "💪🏼 (:flexed_biceps_medium-light_skin_tone:)" + U1F4AA1F3FD = "💪🏽", "💪🏽 (:flexed_biceps_medium_skin_tone:)" + U1F4BE = "💾", "💾 (:floppy_disk:)" + U1F3B4 = "🎴", "🎴 (:flower_playing_cards:)" + U1F633 = "😳", "😳 (:flushed_face:)" + U1FA88 = "🪈", "🪈 (:flute:)" + U1FAB0 = "🪰", "🪰 (:fly:)" + U1F94F = "🥏", "🥏 (:flying_disc:)" + U1F6F8 = "🛸", "🛸 (:flying_saucer:)" + U1F32BFE0F = "🌫️", "🌫️ (:fog:)" + U1F32B = "🌫", "🌫 (:fog:)" + U1F301 = "🌁", "🌁 (:foggy:)" + U1F64F = "🙏", "🙏 (:folded_hands:)" + U1F64F1F3FF = "🙏🏿", "🙏🏿 (:folded_hands_dark_skin_tone:)" + U1F64F1F3FB = "🙏🏻", "🙏🏻 (:folded_hands_light_skin_tone:)" + U1F64F1F3FE = "🙏🏾", "🙏🏾 (:folded_hands_medium-dark_skin_tone:)" + U1F64F1F3FC = "🙏🏼", "🙏🏼 (:folded_hands_medium-light_skin_tone:)" + U1F64F1F3FD = "🙏🏽", "🙏🏽 (:folded_hands_medium_skin_tone:)" + U1FAAD = "🪭", "🪭 (:folding_hand_fan:)" + U1FAD5 = "🫕", "🫕 (:fondue:)" + U1F9B6 = "🦶", "🦶 (:foot:)" + U1F9B61F3FF = "🦶🏿", "🦶🏿 (:foot_dark_skin_tone:)" + U1F9B61F3FB = "🦶🏻", "🦶🏻 (:foot_light_skin_tone:)" + U1F9B61F3FE = "🦶🏾", "🦶🏾 (:foot_medium-dark_skin_tone:)" + U1F9B61F3FC = "🦶🏼", "🦶🏼 (:foot_medium-light_skin_tone:)" + U1F9B61F3FD = "🦶🏽", "🦶🏽 (:foot_medium_skin_tone:)" + U1F463 = "👣", "👣 (:footprints:)" + U1F374 = "🍴", "🍴 (:fork_and_knife:)" + U1F37DFE0F = "🍽️", "🍽️ (:fork_and_knife_with_plate:)" + U1F37D = "🍽", "🍽 (:fork_and_knife_with_plate:)" + U1F960 = "🥠", "🥠 (:fortune_cookie:)" + U26F2 = "⛲", "⛲ (:fountain:)" + U1F58BFE0F = "🖋️", "🖋️ (:fountain_pen:)" + U1F58B = "🖋", "🖋 (:fountain_pen:)" + U1F55F = "🕟", "🕟 (:four-thirty:)" + U1F340 = "🍀", "🍀 (:four_leaf_clover:)" + U1F553 = "🕓", "🕓 (:four_o’clock:)" + U1F98A = "🦊", "🦊 (:fox:)" + U1F5BCFE0F = "🖼️", "🖼️ (:framed_picture:)" + U1F5BC = "🖼", "🖼 (:framed_picture:)" + U1F35F = "🍟", "🍟 (:french_fries:)" + U1F364 = "🍤", "🍤 (:fried_shrimp:)" + U1F438 = "🐸", "🐸 (:frog:)" + U1F425 = "🐥", "🐥 (:front-facing_baby_chick:)" + U2639FE0F = "☹️", "☹️ (:frowning_face:)" + U2639 = "☹", "☹ (:frowning_face:)" + U1F626 = "😦", "😦 (:frowning_face_with_open_mouth:)" + U26FD = "⛽", "⛽ (:fuel_pump:)" + U1F315 = "🌕", "🌕 (:full_moon:)" + U1F31D = "🌝", "🌝 (:full_moon_face:)" + U26B1FE0F = "⚱️", "⚱️ (:funeral_urn:)" + U26B1 = "⚱", "⚱ (:funeral_urn:)" + U1F3B2 = "🎲", "🎲 (:game_die:)" + U1F9C4 = "🧄", "🧄 (:garlic:)" + U2699FE0F = "⚙️", "⚙️ (:gear:)" + U2699 = "⚙", "⚙ (:gear:)" + U1F48E = "💎", "💎 (:gem_stone:)" + U1F9DE = "🧞", "🧞 (:genie:)" + U1F47B = "👻", "👻 (:ghost:)" + U1FADA = "🫚", "🫚 (:ginger_root:)" + U1F992 = "🦒", "🦒 (:giraffe:)" + U1F467 = "👧", "👧 (:girl:)" + U1F4671F3FF = "👧🏿", "👧🏿 (:girl_dark_skin_tone:)" + U1F4671F3FB = "👧🏻", "👧🏻 (:girl_light_skin_tone:)" + U1F4671F3FE = "👧🏾", "👧🏾 (:girl_medium-dark_skin_tone:)" + U1F4671F3FC = "👧🏼", "👧🏼 (:girl_medium-light_skin_tone:)" + U1F4671F3FD = "👧🏽", "👧🏽 (:girl_medium_skin_tone:)" + U1F95B = "🥛", "🥛 (:glass_of_milk:)" + U1F453 = "👓", "👓 (:glasses:)" + U1F30E = "🌎", "🌎 (:globe_showing_Americas:)" + U1F30F = "🌏", "🌏 (:globe_showing_Asia-Australia:)" + U1F30D = "🌍", "🌍 (:globe_showing_Europe-Africa:)" + U1F310 = "🌐", "🌐 (:globe_with_meridians:)" + U1F9E4 = "🧤", "🧤 (:gloves:)" + U1F31F = "🌟", "🌟 (:glowing_star:)" + U1F945 = "🥅", "🥅 (:goal_net:)" + U1F410 = "🐐", "🐐 (:goat:)" + U1F47A = "👺", "👺 (:goblin:)" + U1F97D = "🥽", "🥽 (:goggles:)" + U1FABF = "🪿", "🪿 (:goose:)" + U1F98D = "🦍", "🦍 (:gorilla:)" + U1F393 = "🎓", "🎓 (:graduation_cap:)" + U1F347 = "🍇", "🍇 (:grapes:)" + U1F34F = "🍏", "🍏 (:green_apple:)" + U1F4D7 = "📗", "📗 (:green_book:)" + U1F7E2 = "🟢", "🟢 (:green_circle:)" + U1F49A = "💚", "💚 (:green_heart:)" + U1F957 = "🥗", "🥗 (:green_salad:)" + U1F7E9 = "🟩", "🟩 (:green_square:)" + U1FA76 = "🩶", "🩶 (:grey_heart:)" + U1F62C = "😬", "😬 (:grimacing_face:)" + U1F63A = "😺", "😺 (:grinning_cat:)" + U1F638 = "😸", "😸 (:grinning_cat_with_smiling_eyes:)" + U1F600 = "😀", "😀 (:grinning_face:)" + U1F603 = "😃", "😃 (:grinning_face_with_big_eyes:)" + U1F604 = "😄", "😄 (:grinning_face_with_smiling_eyes:)" + U1F605 = "😅", "😅 (:grinning_face_with_sweat:)" + U1F606 = "😆", "😆 (:grinning_squinting_face:)" + U1F497 = "💗", "💗 (:growing_heart:)" + U1F482 = "💂", "💂 (:guard:)" + U1F4821F3FF = "💂🏿", "💂🏿 (:guard_dark_skin_tone:)" + U1F4821F3FB = "💂🏻", "💂🏻 (:guard_light_skin_tone:)" + U1F4821F3FE = "💂🏾", "💂🏾 (:guard_medium-dark_skin_tone:)" + U1F4821F3FC = "💂🏼", "💂🏼 (:guard_medium-light_skin_tone:)" + U1F4821F3FD = "💂🏽", "💂🏽 (:guard_medium_skin_tone:)" + U1F9AE = "🦮", "🦮 (:guide_dog:)" + U1F3B8 = "🎸", "🎸 (:guitar:)" + U1FAAE = "🪮", "🪮 (:hair_pick:)" + U1F354 = "🍔", "🍔 (:hamburger:)" + U1F528 = "🔨", "🔨 (:hammer:)" + U2692FE0F = "⚒️", "⚒️ (:hammer_and_pick:)" + U2692 = "⚒", "⚒ (:hammer_and_pick:)" + U1F6E0FE0F = "🛠️", "🛠️ (:hammer_and_wrench:)" + U1F6E0 = "🛠", "🛠 (:hammer_and_wrench:)" + U1FAAC = "🪬", "🪬 (:hamsa:)" + U1F439 = "🐹", "🐹 (:hamster:)" + U1F590FE0F = "🖐️", "🖐️ (:hand_with_fingers_splayed:)" + U1F590 = "🖐", "🖐 (:hand_with_fingers_splayed:)" + U1F5901F3FF = "🖐🏿", "🖐🏿 (:hand_with_fingers_splayed_dark_skin_tone:)" + U1F5901F3FB = "🖐🏻", "🖐🏻 (:hand_with_fingers_splayed_light_skin_tone:)" + U1F5901F3FE = "🖐🏾", "🖐🏾 (:hand_with_fingers_splayed_medium-dark_skin_tone:)" + U1F5901F3FC = "🖐🏼", "🖐🏼 (:hand_with_fingers_splayed_medium-light_skin_tone:)" + U1F5901F3FD = "🖐🏽", "🖐🏽 (:hand_with_fingers_splayed_medium_skin_tone:)" + U1FAF0 = "🫰", "🫰 (:hand_with_index_finger_and_thumb_crossed:)" + U1FAF01F3FF = "🫰🏿", "🫰🏿 (:hand_with_index_finger_and_thumb_crossed_dark_skin_tone:)" + U1FAF01F3FB = "🫰🏻", "🫰🏻 (:hand_with_index_finger_and_thumb_crossed_light_skin_tone:)" + U1FAF01F3FE = "🫰🏾", "🫰🏾 (:hand_with_index_finger_and_thumb_crossed_medium-dark_skin_tone:)" + U1FAF01F3FC = "🫰🏼", "🫰🏼 (:hand_with_index_finger_and_thumb_crossed_medium-light_skin_tone:)" + U1FAF01F3FD = "🫰🏽", "🫰🏽 (:hand_with_index_finger_and_thumb_crossed_medium_skin_tone:)" + U1F45C = "👜", "👜 (:handbag:)" + U1F91D = "🤝", "🤝 (:handshake:)" + U1F91D1F3FF = "🤝🏿", "🤝🏿 (:handshake_dark_skin_tone:)" + U1FAF11F3FF200D1FAF21F3FB = "🫱🏿‍🫲🏻", "🫱🏿‍🫲🏻 (:handshake_dark_skin_tone_light_skin_tone:)" + U1FAF11F3FF200D1FAF21F3FE = "🫱🏿‍🫲🏾", "🫱🏿‍🫲🏾 (:handshake_dark_skin_tone_medium-dark_skin_tone:)" + U1FAF11F3FF200D1FAF21F3FC = "🫱🏿‍🫲🏼", "🫱🏿‍🫲🏼 (:handshake_dark_skin_tone_medium-light_skin_tone:)" + U1FAF11F3FF200D1FAF21F3FD = "🫱🏿‍🫲🏽", "🫱🏿‍🫲🏽 (:handshake_dark_skin_tone_medium_skin_tone:)" + U1F91D1F3FB = "🤝🏻", "🤝🏻 (:handshake_light_skin_tone:)" + U1FAF11F3FB200D1FAF21F3FF = "🫱🏻‍🫲🏿", "🫱🏻‍🫲🏿 (:handshake_light_skin_tone_dark_skin_tone:)" + U1FAF11F3FB200D1FAF21F3FE = "🫱🏻‍🫲🏾", "🫱🏻‍🫲🏾 (:handshake_light_skin_tone_medium-dark_skin_tone:)" + U1FAF11F3FB200D1FAF21F3FC = "🫱🏻‍🫲🏼", "🫱🏻‍🫲🏼 (:handshake_light_skin_tone_medium-light_skin_tone:)" + U1FAF11F3FB200D1FAF21F3FD = "🫱🏻‍🫲🏽", "🫱🏻‍🫲🏽 (:handshake_light_skin_tone_medium_skin_tone:)" + U1F91D1F3FE = "🤝🏾", "🤝🏾 (:handshake_medium-dark_skin_tone:)" + U1FAF11F3FE200D1FAF21F3FF = "🫱🏾‍🫲🏿", "🫱🏾‍🫲🏿 (:handshake_medium-dark_skin_tone_dark_skin_tone:)" + U1FAF11F3FE200D1FAF21F3FB = "🫱🏾‍🫲🏻", "🫱🏾‍🫲🏻 (:handshake_medium-dark_skin_tone_light_skin_tone:)" + U1FAF11F3FE200D1FAF21F3FC = "🫱🏾‍🫲🏼", "🫱🏾‍🫲🏼 (:handshake_medium-dark_skin_tone_medium-light_skin_tone:)" + U1FAF11F3FE200D1FAF21F3FD = "🫱🏾‍🫲🏽", "🫱🏾‍🫲🏽 (:handshake_medium-dark_skin_tone_medium_skin_tone:)" + U1F91D1F3FC = "🤝🏼", "🤝🏼 (:handshake_medium-light_skin_tone:)" + U1FAF11F3FC200D1FAF21F3FF = "🫱🏼‍🫲🏿", "🫱🏼‍🫲🏿 (:handshake_medium-light_skin_tone_dark_skin_tone:)" + U1FAF11F3FC200D1FAF21F3FB = "🫱🏼‍🫲🏻", "🫱🏼‍🫲🏻 (:handshake_medium-light_skin_tone_light_skin_tone:)" + U1FAF11F3FC200D1FAF21F3FE = "🫱🏼‍🫲🏾", "🫱🏼‍🫲🏾 (:handshake_medium-light_skin_tone_medium-dark_skin_tone:)" + U1FAF11F3FC200D1FAF21F3FD = "🫱🏼‍🫲🏽", "🫱🏼‍🫲🏽 (:handshake_medium-light_skin_tone_medium_skin_tone:)" + U1F91D1F3FD = "🤝🏽", "🤝🏽 (:handshake_medium_skin_tone:)" + U1FAF11F3FD200D1FAF21F3FF = "🫱🏽‍🫲🏿", "🫱🏽‍🫲🏿 (:handshake_medium_skin_tone_dark_skin_tone:)" + U1FAF11F3FD200D1FAF21F3FB = "🫱🏽‍🫲🏻", "🫱🏽‍🫲🏻 (:handshake_medium_skin_tone_light_skin_tone:)" + U1FAF11F3FD200D1FAF21F3FE = "🫱🏽‍🫲🏾", "🫱🏽‍🫲🏾 (:handshake_medium_skin_tone_medium-dark_skin_tone:)" + U1FAF11F3FD200D1FAF21F3FC = "🫱🏽‍🫲🏼", "🫱🏽‍🫲🏼 (:handshake_medium_skin_tone_medium-light_skin_tone:)" + U1F423 = "🐣", "🐣 (:hatching_chick:)" + U1F642200D2194FE0F = "🙂‍↔️", "🙂‍↔️ (:head_shaking_horizontally:)" + U1F642200D2194 = "🙂‍↔", "🙂‍↔ (:head_shaking_horizontally:)" + U1F642200D2195FE0F = "🙂‍↕️", "🙂‍↕️ (:head_shaking_vertically:)" + U1F642200D2195 = "🙂‍↕", "🙂‍↕ (:head_shaking_vertically:)" + U1F3A7 = "🎧", "🎧 (:headphone:)" + U1FAA6 = "🪦", "🪦 (:headstone:)" + U1F9D1200D2695FE0F = "🧑‍⚕️", "🧑‍⚕️ (:health_worker:)" + U1F9D1200D2695 = "🧑‍⚕", "🧑‍⚕ (:health_worker:)" + U1F9D11F3FF200D2695FE0F = "🧑🏿‍⚕️", "🧑🏿‍⚕️ (:health_worker_dark_skin_tone:)" + U1F9D11F3FF200D2695 = "🧑🏿‍⚕", "🧑🏿‍⚕ (:health_worker_dark_skin_tone:)" + U1F9D11F3FB200D2695FE0F = "🧑🏻‍⚕️", "🧑🏻‍⚕️ (:health_worker_light_skin_tone:)" + U1F9D11F3FB200D2695 = "🧑🏻‍⚕", "🧑🏻‍⚕ (:health_worker_light_skin_tone:)" + U1F9D11F3FE200D2695FE0F = "🧑🏾‍⚕️", "🧑🏾‍⚕️ (:health_worker_medium-dark_skin_tone:)" + U1F9D11F3FE200D2695 = "🧑🏾‍⚕", "🧑🏾‍⚕ (:health_worker_medium-dark_skin_tone:)" + U1F9D11F3FC200D2695FE0F = "🧑🏼‍⚕️", "🧑🏼‍⚕️ (:health_worker_medium-light_skin_tone:)" + U1F9D11F3FC200D2695 = "🧑🏼‍⚕", "🧑🏼‍⚕ (:health_worker_medium-light_skin_tone:)" + U1F9D11F3FD200D2695FE0F = "🧑🏽‍⚕️", "🧑🏽‍⚕️ (:health_worker_medium_skin_tone:)" + U1F9D11F3FD200D2695 = "🧑🏽‍⚕", "🧑🏽‍⚕ (:health_worker_medium_skin_tone:)" + U1F649 = "🙉", "🙉 (:hear-no-evil_monkey:)" + U1F49F = "💟", "💟 (:heart_decoration:)" + U2763FE0F = "❣️", "❣️ (:heart_exclamation:)" + U2763 = "❣", "❣ (:heart_exclamation:)" + U1FAF6 = "🫶", "🫶 (:heart_hands:)" + U1FAF61F3FF = "🫶🏿", "🫶🏿 (:heart_hands_dark_skin_tone:)" + U1FAF61F3FB = "🫶🏻", "🫶🏻 (:heart_hands_light_skin_tone:)" + U1FAF61F3FE = "🫶🏾", "🫶🏾 (:heart_hands_medium-dark_skin_tone:)" + U1FAF61F3FC = "🫶🏼", "🫶🏼 (:heart_hands_medium-light_skin_tone:)" + U1FAF61F3FD = "🫶🏽", "🫶🏽 (:heart_hands_medium_skin_tone:)" + U2764FE0F200D1F525 = "❤️‍🔥", "❤️‍🔥 (:heart_on_fire:)" + U2764200D1F525 = "❤‍🔥", "❤‍🔥 (:heart_on_fire:)" + U2665FE0F = "♥️", "♥️ (:heart_suit:)" + U2665 = "♥", "♥ (:heart_suit:)" + U1F498 = "💘", "💘 (:heart_with_arrow:)" + U1F49D = "💝", "💝 (:heart_with_ribbon:)" + U1F4B2 = "💲", "💲 (:heavy_dollar_sign:)" + U1F7F0 = "🟰", "🟰 (:heavy_equals_sign:)" + U1F994 = "🦔", "🦔 (:hedgehog:)" + U1F681 = "🚁", "🚁 (:helicopter:)" + U1F33F = "🌿", "🌿 (:herb:)" + U1F33A = "🌺", "🌺 (:hibiscus:)" + U1F460 = "👠", "👠 (:high-heeled_shoe:)" + U1F684 = "🚄", "🚄 (:high-speed_train:)" + U26A1 = "⚡", "⚡ (:high_voltage:)" + U1F97E = "🥾", "🥾 (:hiking_boot:)" + U1F6D5 = "🛕", "🛕 (:hindu_temple:)" + U1F99B = "🦛", "🦛 (:hippopotamus:)" + U1F573FE0F = "🕳️", "🕳️ (:hole:)" + U1F573 = "🕳", "🕳 (:hole:)" + U2B55 = "⭕", "⭕ (:hollow_red_circle:)" + U1F36F = "🍯", "🍯 (:honey_pot:)" + U1F41D = "🐝", "🐝 (:honeybee:)" + U1FA9D = "🪝", "🪝 (:hook:)" + U1F6A5 = "🚥", "🚥 (:horizontal_traffic_light:)" + U1F40E = "🐎", "🐎 (:horse:)" + U1F434 = "🐴", "🐴 (:horse_face:)" + U1F3C7 = "🏇", "🏇 (:horse_racing:)" + U1F3C71F3FF = "🏇🏿", "🏇🏿 (:horse_racing_dark_skin_tone:)" + U1F3C71F3FB = "🏇🏻", "🏇🏻 (:horse_racing_light_skin_tone:)" + U1F3C71F3FE = "🏇🏾", "🏇🏾 (:horse_racing_medium-dark_skin_tone:)" + U1F3C71F3FC = "🏇🏼", "🏇🏼 (:horse_racing_medium-light_skin_tone:)" + U1F3C71F3FD = "🏇🏽", "🏇🏽 (:horse_racing_medium_skin_tone:)" + U1F3E5 = "🏥", "🏥 (:hospital:)" + U2615 = "☕", "☕ (:hot_beverage:)" + U1F32D = "🌭", "🌭 (:hot_dog:)" + U1F975 = "🥵", "🥵 (:hot_face:)" + U1F336FE0F = "🌶️", "🌶️ (:hot_pepper:)" + U1F336 = "🌶", "🌶 (:hot_pepper:)" + U2668FE0F = "♨️", "♨️ (:hot_springs:)" + U2668 = "♨", "♨ (:hot_springs:)" + U1F3E8 = "🏨", "🏨 (:hotel:)" + U231B = "⌛", "⌛ (:hourglass_done:)" + U23F3 = "⏳", "⏳ (:hourglass_not_done:)" + U1F3E0 = "🏠", "🏠 (:house:)" + U1F3E1 = "🏡", "🏡 (:house_with_garden:)" + U1F3D8FE0F = "🏘️", "🏘️ (:houses:)" + U1F3D8 = "🏘", "🏘 (:houses:)" + U1F4AF = "💯", "💯 (:hundred_points:)" + U1F62F = "😯", "😯 (:hushed_face:)" + U1F6D6 = "🛖", "🛖 (:hut:)" + U1FABB = "🪻", "🪻 (:hyacinth:)" + U1F9CA = "🧊", "🧊 (:ice:)" + U1F368 = "🍨", "🍨 (:ice_cream:)" + U1F3D2 = "🏒", "🏒 (:ice_hockey:)" + U26F8FE0F = "⛸️", "⛸️ (:ice_skate:)" + U26F8 = "⛸", "⛸ (:ice_skate:)" + U1FAAA = "🪪", "🪪 (:identification_card:)" + U1F4E5 = "📥", "📥 (:inbox_tray:)" + U1F4E8 = "📨", "📨 (:incoming_envelope:)" + U1FAF5 = "🫵", "🫵 (:index_pointing_at_the_viewer:)" + U1FAF51F3FF = "🫵🏿", "🫵🏿 (:index_pointing_at_the_viewer_dark_skin_tone:)" + U1FAF51F3FB = "🫵🏻", "🫵🏻 (:index_pointing_at_the_viewer_light_skin_tone:)" + U1FAF51F3FE = "🫵🏾", "🫵🏾 (:index_pointing_at_the_viewer_medium-dark_skin_tone:)" + U1FAF51F3FC = "🫵🏼", "🫵🏼 (:index_pointing_at_the_viewer_medium-light_skin_tone:)" + U1FAF51F3FD = "🫵🏽", "🫵🏽 (:index_pointing_at_the_viewer_medium_skin_tone:)" + U261DFE0F = "☝️", "☝️ (:index_pointing_up:)" + U261D = "☝", "☝ (:index_pointing_up:)" + U261D1F3FF = "☝🏿", "☝🏿 (:index_pointing_up_dark_skin_tone:)" + U261D1F3FB = "☝🏻", "☝🏻 (:index_pointing_up_light_skin_tone:)" + U261D1F3FE = "☝🏾", "☝🏾 (:index_pointing_up_medium-dark_skin_tone:)" + U261D1F3FC = "☝🏼", "☝🏼 (:index_pointing_up_medium-light_skin_tone:)" + U261D1F3FD = "☝🏽", "☝🏽 (:index_pointing_up_medium_skin_tone:)" + U267EFE0F = "♾️", "♾️ (:infinity:)" + U267E = "♾", "♾ (:infinity:)" + U2139FE0F = "ℹ️", "ℹ️ (:information:)" + U2139 = "ℹ", "ℹ (:information:)" + U1F524 = "🔤", "🔤 (:input_latin_letters:)" + U1F521 = "🔡", "🔡 (:input_latin_lowercase:)" + U1F520 = "🔠", "🔠 (:input_latin_uppercase:)" + U1F522 = "🔢", "🔢 (:input_numbers:)" + U1F523 = "🔣", "🔣 (:input_symbols:)" + U1F383 = "🎃", "🎃 (:jack-o-lantern:)" + U1FAD9 = "🫙", "🫙 (:jar:)" + U1F456 = "👖", "👖 (:jeans:)" + U1FABC = "🪼", "🪼 (:jellyfish:)" + U1F0CF = "🃏", "🃏 (:joker:)" + U1F579FE0F = "🕹️", "🕹️ (:joystick:)" + U1F579 = "🕹", "🕹 (:joystick:)" + U1F9D1200D2696FE0F = "🧑‍⚖️", "🧑‍⚖️ (:judge:)" + U1F9D1200D2696 = "🧑‍⚖", "🧑‍⚖ (:judge:)" + U1F9D11F3FF200D2696FE0F = "🧑🏿‍⚖️", "🧑🏿‍⚖️ (:judge_dark_skin_tone:)" + U1F9D11F3FF200D2696 = "🧑🏿‍⚖", "🧑🏿‍⚖ (:judge_dark_skin_tone:)" + U1F9D11F3FB200D2696FE0F = "🧑🏻‍⚖️", "🧑🏻‍⚖️ (:judge_light_skin_tone:)" + U1F9D11F3FB200D2696 = "🧑🏻‍⚖", "🧑🏻‍⚖ (:judge_light_skin_tone:)" + U1F9D11F3FE200D2696FE0F = "🧑🏾‍⚖️", "🧑🏾‍⚖️ (:judge_medium-dark_skin_tone:)" + U1F9D11F3FE200D2696 = "🧑🏾‍⚖", "🧑🏾‍⚖ (:judge_medium-dark_skin_tone:)" + U1F9D11F3FC200D2696FE0F = "🧑🏼‍⚖️", "🧑🏼‍⚖️ (:judge_medium-light_skin_tone:)" + U1F9D11F3FC200D2696 = "🧑🏼‍⚖", "🧑🏼‍⚖ (:judge_medium-light_skin_tone:)" + U1F9D11F3FD200D2696FE0F = "🧑🏽‍⚖️", "🧑🏽‍⚖️ (:judge_medium_skin_tone:)" + U1F9D11F3FD200D2696 = "🧑🏽‍⚖", "🧑🏽‍⚖ (:judge_medium_skin_tone:)" + U1F54B = "🕋", "🕋 (:kaaba:)" + U1F998 = "🦘", "🦘 (:kangaroo:)" + U1F511 = "🔑", "🔑 (:key:)" + U2328FE0F = "⌨️", "⌨️ (:keyboard:)" + U2328 = "⌨", "⌨ (:keyboard:)" + U23FE0F20E3 = "#️⃣", "#️⃣ (:keycap_#:)" + U2320E3 = "#⃣", "#⃣ (:keycap_#:)" + U2AFE0F20E3 = "*️⃣", "*️⃣ (:keycap_*:)" + U2A20E3 = "*⃣", "*⃣ (:keycap_*:)" + U30FE0F20E3 = "0️⃣", "0️⃣ (:keycap_0:)" + U3020E3 = "0⃣", "0⃣ (:keycap_0:)" + U31FE0F20E3 = "1️⃣", "1️⃣ (:keycap_1:)" + U3120E3 = "1⃣", "1⃣ (:keycap_1:)" + U1F51F = "🔟", "🔟 (:keycap_10:)" + U32FE0F20E3 = "2️⃣", "2️⃣ (:keycap_2:)" + U3220E3 = "2⃣", "2⃣ (:keycap_2:)" + U33FE0F20E3 = "3️⃣", "3️⃣ (:keycap_3:)" + U3320E3 = "3⃣", "3⃣ (:keycap_3:)" + U34FE0F20E3 = "4️⃣", "4️⃣ (:keycap_4:)" + U3420E3 = "4⃣", "4⃣ (:keycap_4:)" + U35FE0F20E3 = "5️⃣", "5️⃣ (:keycap_5:)" + U3520E3 = "5⃣", "5⃣ (:keycap_5:)" + U36FE0F20E3 = "6️⃣", "6️⃣ (:keycap_6:)" + U3620E3 = "6⃣", "6⃣ (:keycap_6:)" + U37FE0F20E3 = "7️⃣", "7️⃣ (:keycap_7:)" + U3720E3 = "7⃣", "7⃣ (:keycap_7:)" + U38FE0F20E3 = "8️⃣", "8️⃣ (:keycap_8:)" + U3820E3 = "8⃣", "8⃣ (:keycap_8:)" + U39FE0F20E3 = "9️⃣", "9️⃣ (:keycap_9:)" + U3920E3 = "9⃣", "9⃣ (:keycap_9:)" + U1FAAF = "🪯", "🪯 (:khanda:)" + U1F6F4 = "🛴", "🛴 (:kick_scooter:)" + U1F458 = "👘", "👘 (:kimono:)" + U1F48F = "💏", "💏 (:kiss:)" + U1F48F1F3FF = "💏🏿", "💏🏿 (:kiss_dark_skin_tone:)" + U1F48F1F3FB = "💏🏻", "💏🏻 (:kiss_light_skin_tone:)" + U1F468200D2764FE0F200D1F48B200D1F468 = "👨‍❤️‍💋‍👨", "👨‍❤️‍💋‍👨 (:kiss_man_man:)" + U1F468200D2764200D1F48B200D1F468 = "👨‍❤‍💋‍👨", "👨‍❤‍💋‍👨 (:kiss_man_man:)" + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏿‍❤️‍💋‍👨🏿", "👨🏿‍❤️‍💋‍👨🏿 (:kiss_man_man_dark_skin_tone:)" + U1F4681F3FF200D2764200D1F48B200D1F4681F3FF = "👨🏿‍❤‍💋‍👨🏿", "👨🏿‍❤‍💋‍👨🏿 (:kiss_man_man_dark_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏿‍❤️‍💋‍👨🏻", "👨🏿‍❤️‍💋‍👨🏻 (:kiss_man_man_dark_skin_tone_light_skin_tone:)" + U1F4681F3FF200D2764200D1F48B200D1F4681F3FB = "👨🏿‍❤‍💋‍👨🏻", "👨🏿‍❤‍💋‍👨🏻 (:kiss_man_man_dark_skin_tone_light_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏿‍❤️‍💋‍👨🏾", "👨🏿‍❤️‍💋‍👨🏾 (:kiss_man_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FF200D2764200D1F48B200D1F4681F3FE = "👨🏿‍❤‍💋‍👨🏾", "👨🏿‍❤‍💋‍👨🏾 (:kiss_man_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏿‍❤️‍💋‍👨🏼", "👨🏿‍❤️‍💋‍👨🏼 (:kiss_man_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FF200D2764200D1F48B200D1F4681F3FC = "👨🏿‍❤‍💋‍👨🏼", "👨🏿‍❤‍💋‍👨🏼 (:kiss_man_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏿‍❤️‍💋‍👨🏽", "👨🏿‍❤️‍💋‍👨🏽 (:kiss_man_man_dark_skin_tone_medium_skin_tone:)" + U1F4681F3FF200D2764200D1F48B200D1F4681F3FD = "👨🏿‍❤‍💋‍👨🏽", "👨🏿‍❤‍💋‍👨🏽 (:kiss_man_man_dark_skin_tone_medium_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏻‍❤️‍💋‍👨🏻", "👨🏻‍❤️‍💋‍👨🏻 (:kiss_man_man_light_skin_tone:)" + U1F4681F3FB200D2764200D1F48B200D1F4681F3FB = "👨🏻‍❤‍💋‍👨🏻", "👨🏻‍❤‍💋‍👨🏻 (:kiss_man_man_light_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏻‍❤️‍💋‍👨🏿", "👨🏻‍❤️‍💋‍👨🏿 (:kiss_man_man_light_skin_tone_dark_skin_tone:)" + U1F4681F3FB200D2764200D1F48B200D1F4681F3FF = "👨🏻‍❤‍💋‍👨🏿", "👨🏻‍❤‍💋‍👨🏿 (:kiss_man_man_light_skin_tone_dark_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏻‍❤️‍💋‍👨🏾", "👨🏻‍❤️‍💋‍👨🏾 (:kiss_man_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FB200D2764200D1F48B200D1F4681F3FE = "👨🏻‍❤‍💋‍👨🏾", "👨🏻‍❤‍💋‍👨🏾 (:kiss_man_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏻‍❤️‍💋‍👨🏼", "👨🏻‍❤️‍💋‍👨🏼 (:kiss_man_man_light_skin_tone_medium-light_skin_tone:)" + U1F4681F3FB200D2764200D1F48B200D1F4681F3FC = "👨🏻‍❤‍💋‍👨🏼", "👨🏻‍❤‍💋‍👨🏼 (:kiss_man_man_light_skin_tone_medium-light_skin_tone:)" + U1F4681F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏻‍❤️‍💋‍👨🏽", "👨🏻‍❤️‍💋‍👨🏽 (:kiss_man_man_light_skin_tone_medium_skin_tone:)" + U1F4681F3FB200D2764200D1F48B200D1F4681F3FD = "👨🏻‍❤‍💋‍👨🏽", "👨🏻‍❤‍💋‍👨🏽 (:kiss_man_man_light_skin_tone_medium_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏾‍❤️‍💋‍👨🏾", "👨🏾‍❤️‍💋‍👨🏾 (:kiss_man_man_medium-dark_skin_tone:)" + U1F4681F3FE200D2764200D1F48B200D1F4681F3FE = "👨🏾‍❤‍💋‍👨🏾", "👨🏾‍❤‍💋‍👨🏾 (:kiss_man_man_medium-dark_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏾‍❤️‍💋‍👨🏿", "👨🏾‍❤️‍💋‍👨🏿 (:kiss_man_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4681F3FE200D2764200D1F48B200D1F4681F3FF = "👨🏾‍❤‍💋‍👨🏿", "👨🏾‍❤‍💋‍👨🏿 (:kiss_man_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏾‍❤️‍💋‍👨🏻", "👨🏾‍❤️‍💋‍👨🏻 (:kiss_man_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4681F3FE200D2764200D1F48B200D1F4681F3FB = "👨🏾‍❤‍💋‍👨🏻", "👨🏾‍❤‍💋‍👨🏻 (:kiss_man_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏾‍❤️‍💋‍👨🏼", "👨🏾‍❤️‍💋‍👨🏼 (:kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FE200D2764200D1F48B200D1F4681F3FC = "👨🏾‍❤‍💋‍👨🏼", "👨🏾‍❤‍💋‍👨🏼 (:kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏾‍❤️‍💋‍👨🏽", "👨🏾‍❤️‍💋‍👨🏽 (:kiss_man_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4681F3FE200D2764200D1F48B200D1F4681F3FD = "👨🏾‍❤‍💋‍👨🏽", "👨🏾‍❤‍💋‍👨🏽 (:kiss_man_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏼‍❤️‍💋‍👨🏼", "👨🏼‍❤️‍💋‍👨🏼 (:kiss_man_man_medium-light_skin_tone:)" + U1F4681F3FC200D2764200D1F48B200D1F4681F3FC = "👨🏼‍❤‍💋‍👨🏼", "👨🏼‍❤‍💋‍👨🏼 (:kiss_man_man_medium-light_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏼‍❤️‍💋‍👨🏿", "👨🏼‍❤️‍💋‍👨🏿 (:kiss_man_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4681F3FC200D2764200D1F48B200D1F4681F3FF = "👨🏼‍❤‍💋‍👨🏿", "👨🏼‍❤‍💋‍👨🏿 (:kiss_man_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏼‍❤️‍💋‍👨🏻", "👨🏼‍❤️‍💋‍👨🏻 (:kiss_man_man_medium-light_skin_tone_light_skin_tone:)" + U1F4681F3FC200D2764200D1F48B200D1F4681F3FB = "👨🏼‍❤‍💋‍👨🏻", "👨🏼‍❤‍💋‍👨🏻 (:kiss_man_man_medium-light_skin_tone_light_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏼‍❤️‍💋‍👨🏾", "👨🏼‍❤️‍💋‍👨🏾 (:kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FC200D2764200D1F48B200D1F4681F3FE = "👨🏼‍❤‍💋‍👨🏾", "👨🏼‍❤‍💋‍👨🏾 (:kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏼‍❤️‍💋‍👨🏽", "👨🏼‍❤️‍💋‍👨🏽 (:kiss_man_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4681F3FC200D2764200D1F48B200D1F4681F3FD = "👨🏼‍❤‍💋‍👨🏽", "👨🏼‍❤‍💋‍👨🏽 (:kiss_man_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👨🏽‍❤️‍💋‍👨🏽", "👨🏽‍❤️‍💋‍👨🏽 (:kiss_man_man_medium_skin_tone:)" + U1F4681F3FD200D2764200D1F48B200D1F4681F3FD = "👨🏽‍❤‍💋‍👨🏽", "👨🏽‍❤‍💋‍👨🏽 (:kiss_man_man_medium_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👨🏽‍❤️‍💋‍👨🏿", "👨🏽‍❤️‍💋‍👨🏿 (:kiss_man_man_medium_skin_tone_dark_skin_tone:)" + U1F4681F3FD200D2764200D1F48B200D1F4681F3FF = "👨🏽‍❤‍💋‍👨🏿", "👨🏽‍❤‍💋‍👨🏿 (:kiss_man_man_medium_skin_tone_dark_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👨🏽‍❤️‍💋‍👨🏻", "👨🏽‍❤️‍💋‍👨🏻 (:kiss_man_man_medium_skin_tone_light_skin_tone:)" + U1F4681F3FD200D2764200D1F48B200D1F4681F3FB = "👨🏽‍❤‍💋‍👨🏻", "👨🏽‍❤‍💋‍👨🏻 (:kiss_man_man_medium_skin_tone_light_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👨🏽‍❤️‍💋‍👨🏾", "👨🏽‍❤️‍💋‍👨🏾 (:kiss_man_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FD200D2764200D1F48B200D1F4681F3FE = "👨🏽‍❤‍💋‍👨🏾", "👨🏽‍❤‍💋‍👨🏾 (:kiss_man_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👨🏽‍❤️‍💋‍👨🏼", "👨🏽‍❤️‍💋‍👨🏼 (:kiss_man_man_medium_skin_tone_medium-light_skin_tone:)" + U1F4681F3FD200D2764200D1F48B200D1F4681F3FC = "👨🏽‍❤‍💋‍👨🏼", "👨🏽‍❤‍💋‍👨🏼 (:kiss_man_man_medium_skin_tone_medium-light_skin_tone:)" + U1F48B = "💋", "💋 (:kiss_mark:)" + U1F48F1F3FE = "💏🏾", "💏🏾 (:kiss_medium-dark_skin_tone:)" + U1F48F1F3FC = "💏🏼", "💏🏼 (:kiss_medium-light_skin_tone:)" + U1F48F1F3FD = "💏🏽", "💏🏽 (:kiss_medium_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏿‍❤️‍💋‍🧑🏻", "🧑🏿‍❤️‍💋‍🧑🏻 (:kiss_person_person_dark_skin_tone_light_skin_tone:)" + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FB = "🧑🏿‍❤‍💋‍🧑🏻", "🧑🏿‍❤‍💋‍🧑🏻 (:kiss_person_person_dark_skin_tone_light_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏿‍❤️‍💋‍🧑🏾", "🧑🏿‍❤️‍💋‍🧑🏾 (:kiss_person_person_dark_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FE = "🧑🏿‍❤‍💋‍🧑🏾", "🧑🏿‍❤‍💋‍🧑🏾 (:kiss_person_person_dark_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏿‍❤️‍💋‍🧑🏼", "🧑🏿‍❤️‍💋‍🧑🏼 (:kiss_person_person_dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FC = "🧑🏿‍❤‍💋‍🧑🏼", "🧑🏿‍❤‍💋‍🧑🏼 (:kiss_person_person_dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FF200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏿‍❤️‍💋‍🧑🏽", "🧑🏿‍❤️‍💋‍🧑🏽 (:kiss_person_person_dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FF200D2764200D1F48B200D1F9D11F3FD = "🧑🏿‍❤‍💋‍🧑🏽", "🧑🏿‍❤‍💋‍🧑🏽 (:kiss_person_person_dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏻‍❤️‍💋‍🧑🏿", "🧑🏻‍❤️‍💋‍🧑🏿 (:kiss_person_person_light_skin_tone_dark_skin_tone:)" + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FF = "🧑🏻‍❤‍💋‍🧑🏿", "🧑🏻‍❤‍💋‍🧑🏿 (:kiss_person_person_light_skin_tone_dark_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏻‍❤️‍💋‍🧑🏾", "🧑🏻‍❤️‍💋‍🧑🏾 (:kiss_person_person_light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FE = "🧑🏻‍❤‍💋‍🧑🏾", "🧑🏻‍❤‍💋‍🧑🏾 (:kiss_person_person_light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏻‍❤️‍💋‍🧑🏼", "🧑🏻‍❤️‍💋‍🧑🏼 (:kiss_person_person_light_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FC = "🧑🏻‍❤‍💋‍🧑🏼", "🧑🏻‍❤‍💋‍🧑🏼 (:kiss_person_person_light_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FB200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏻‍❤️‍💋‍🧑🏽", "🧑🏻‍❤️‍💋‍🧑🏽 (:kiss_person_person_light_skin_tone_medium_skin_tone:)" + U1F9D11F3FB200D2764200D1F48B200D1F9D11F3FD = "🧑🏻‍❤‍💋‍🧑🏽", "🧑🏻‍❤‍💋‍🧑🏽 (:kiss_person_person_light_skin_tone_medium_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏾‍❤️‍💋‍🧑🏿", "🧑🏾‍❤️‍💋‍🧑🏿 (:kiss_person_person_medium-dark_skin_tone_dark_skin_tone:)" + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FF = "🧑🏾‍❤‍💋‍🧑🏿", "🧑🏾‍❤‍💋‍🧑🏿 (:kiss_person_person_medium-dark_skin_tone_dark_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏾‍❤️‍💋‍🧑🏻", "🧑🏾‍❤️‍💋‍🧑🏻 (:kiss_person_person_medium-dark_skin_tone_light_skin_tone:)" + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FB = "🧑🏾‍❤‍💋‍🧑🏻", "🧑🏾‍❤‍💋‍🧑🏻 (:kiss_person_person_medium-dark_skin_tone_light_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏾‍❤️‍💋‍🧑🏼", "🧑🏾‍❤️‍💋‍🧑🏼 (:kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FC = "🧑🏾‍❤‍💋‍🧑🏼", "🧑🏾‍❤‍💋‍🧑🏼 (:kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FE200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏾‍❤️‍💋‍🧑🏽", "🧑🏾‍❤️‍💋‍🧑🏽 (:kiss_person_person_medium-dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FE200D2764200D1F48B200D1F9D11F3FD = "🧑🏾‍❤‍💋‍🧑🏽", "🧑🏾‍❤‍💋‍🧑🏽 (:kiss_person_person_medium-dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏼‍❤️‍💋‍🧑🏿", "🧑🏼‍❤️‍💋‍🧑🏿 (:kiss_person_person_medium-light_skin_tone_dark_skin_tone:)" + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FF = "🧑🏼‍❤‍💋‍🧑🏿", "🧑🏼‍❤‍💋‍🧑🏿 (:kiss_person_person_medium-light_skin_tone_dark_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏼‍❤️‍💋‍🧑🏻", "🧑🏼‍❤️‍💋‍🧑🏻 (:kiss_person_person_medium-light_skin_tone_light_skin_tone:)" + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FB = "🧑🏼‍❤‍💋‍🧑🏻", "🧑🏼‍❤‍💋‍🧑🏻 (:kiss_person_person_medium-light_skin_tone_light_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏼‍❤️‍💋‍🧑🏾", "🧑🏼‍❤️‍💋‍🧑🏾 (:kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FE = "🧑🏼‍❤‍💋‍🧑🏾", "🧑🏼‍❤‍💋‍🧑🏾 (:kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FC200D2764FE0F200D1F48B200D1F9D11F3FD = "🧑🏼‍❤️‍💋‍🧑🏽", "🧑🏼‍❤️‍💋‍🧑🏽 (:kiss_person_person_medium-light_skin_tone_medium_skin_tone:)" + U1F9D11F3FC200D2764200D1F48B200D1F9D11F3FD = "🧑🏼‍❤‍💋‍🧑🏽", "🧑🏼‍❤‍💋‍🧑🏽 (:kiss_person_person_medium-light_skin_tone_medium_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FF = "🧑🏽‍❤️‍💋‍🧑🏿", "🧑🏽‍❤️‍💋‍🧑🏿 (:kiss_person_person_medium_skin_tone_dark_skin_tone:)" + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FF = "🧑🏽‍❤‍💋‍🧑🏿", "🧑🏽‍❤‍💋‍🧑🏿 (:kiss_person_person_medium_skin_tone_dark_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FB = "🧑🏽‍❤️‍💋‍🧑🏻", "🧑🏽‍❤️‍💋‍🧑🏻 (:kiss_person_person_medium_skin_tone_light_skin_tone:)" + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FB = "🧑🏽‍❤‍💋‍🧑🏻", "🧑🏽‍❤‍💋‍🧑🏻 (:kiss_person_person_medium_skin_tone_light_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FE = "🧑🏽‍❤️‍💋‍🧑🏾", "🧑🏽‍❤️‍💋‍🧑🏾 (:kiss_person_person_medium_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FE = "🧑🏽‍❤‍💋‍🧑🏾", "🧑🏽‍❤‍💋‍🧑🏾 (:kiss_person_person_medium_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FD200D2764FE0F200D1F48B200D1F9D11F3FC = "🧑🏽‍❤️‍💋‍🧑🏼", "🧑🏽‍❤️‍💋‍🧑🏼 (:kiss_person_person_medium_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FD200D2764200D1F48B200D1F9D11F3FC = "🧑🏽‍❤‍💋‍🧑🏼", "🧑🏽‍❤‍💋‍🧑🏼 (:kiss_person_person_medium_skin_tone_medium-light_skin_tone:)" + U1F469200D2764FE0F200D1F48B200D1F468 = "👩‍❤️‍💋‍👨", "👩‍❤️‍💋‍👨 (:kiss_woman_man:)" + U1F469200D2764200D1F48B200D1F468 = "👩‍❤‍💋‍👨", "👩‍❤‍💋‍👨 (:kiss_woman_man:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏿‍❤️‍💋‍👨🏿", "👩🏿‍❤️‍💋‍👨🏿 (:kiss_woman_man_dark_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4681F3FF = "👩🏿‍❤‍💋‍👨🏿", "👩🏿‍❤‍💋‍👨🏿 (:kiss_woman_man_dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏿‍❤️‍💋‍👨🏻", "👩🏿‍❤️‍💋‍👨🏻 (:kiss_woman_man_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4681F3FB = "👩🏿‍❤‍💋‍👨🏻", "👩🏿‍❤‍💋‍👨🏻 (:kiss_woman_man_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏿‍❤️‍💋‍👨🏾", "👩🏿‍❤️‍💋‍👨🏾 (:kiss_woman_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4681F3FE = "👩🏿‍❤‍💋‍👨🏾", "👩🏿‍❤‍💋‍👨🏾 (:kiss_woman_man_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏿‍❤️‍💋‍👨🏼", "👩🏿‍❤️‍💋‍👨🏼 (:kiss_woman_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4681F3FC = "👩🏿‍❤‍💋‍👨🏼", "👩🏿‍❤‍💋‍👨🏼 (:kiss_woman_man_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏿‍❤️‍💋‍👨🏽", "👩🏿‍❤️‍💋‍👨🏽 (:kiss_woman_man_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4681F3FD = "👩🏿‍❤‍💋‍👨🏽", "👩🏿‍❤‍💋‍👨🏽 (:kiss_woman_man_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏻‍❤️‍💋‍👨🏻", "👩🏻‍❤️‍💋‍👨🏻 (:kiss_woman_man_light_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4681F3FB = "👩🏻‍❤‍💋‍👨🏻", "👩🏻‍❤‍💋‍👨🏻 (:kiss_woman_man_light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏻‍❤️‍💋‍👨🏿", "👩🏻‍❤️‍💋‍👨🏿 (:kiss_woman_man_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4681F3FF = "👩🏻‍❤‍💋‍👨🏿", "👩🏻‍❤‍💋‍👨🏿 (:kiss_woman_man_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏻‍❤️‍💋‍👨🏾", "👩🏻‍❤️‍💋‍👨🏾 (:kiss_woman_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4681F3FE = "👩🏻‍❤‍💋‍👨🏾", "👩🏻‍❤‍💋‍👨🏾 (:kiss_woman_man_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏻‍❤️‍💋‍👨🏼", "👩🏻‍❤️‍💋‍👨🏼 (:kiss_woman_man_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4681F3FC = "👩🏻‍❤‍💋‍👨🏼", "👩🏻‍❤‍💋‍👨🏼 (:kiss_woman_man_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏻‍❤️‍💋‍👨🏽", "👩🏻‍❤️‍💋‍👨🏽 (:kiss_woman_man_light_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4681F3FD = "👩🏻‍❤‍💋‍👨🏽", "👩🏻‍❤‍💋‍👨🏽 (:kiss_woman_man_light_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏾‍❤️‍💋‍👨🏾", "👩🏾‍❤️‍💋‍👨🏾 (:kiss_woman_man_medium-dark_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4681F3FE = "👩🏾‍❤‍💋‍👨🏾", "👩🏾‍❤‍💋‍👨🏾 (:kiss_woman_man_medium-dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏾‍❤️‍💋‍👨🏿", "👩🏾‍❤️‍💋‍👨🏿 (:kiss_woman_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4681F3FF = "👩🏾‍❤‍💋‍👨🏿", "👩🏾‍❤‍💋‍👨🏿 (:kiss_woman_man_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏾‍❤️‍💋‍👨🏻", "👩🏾‍❤️‍💋‍👨🏻 (:kiss_woman_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4681F3FB = "👩🏾‍❤‍💋‍👨🏻", "👩🏾‍❤‍💋‍👨🏻 (:kiss_woman_man_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏾‍❤️‍💋‍👨🏼", "👩🏾‍❤️‍💋‍👨🏼 (:kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4681F3FC = "👩🏾‍❤‍💋‍👨🏼", "👩🏾‍❤‍💋‍👨🏼 (:kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏾‍❤️‍💋‍👨🏽", "👩🏾‍❤️‍💋‍👨🏽 (:kiss_woman_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4681F3FD = "👩🏾‍❤‍💋‍👨🏽", "👩🏾‍❤‍💋‍👨🏽 (:kiss_woman_man_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏼‍❤️‍💋‍👨🏼", "👩🏼‍❤️‍💋‍👨🏼 (:kiss_woman_man_medium-light_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4681F3FC = "👩🏼‍❤‍💋‍👨🏼", "👩🏼‍❤‍💋‍👨🏼 (:kiss_woman_man_medium-light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏼‍❤️‍💋‍👨🏿", "👩🏼‍❤️‍💋‍👨🏿 (:kiss_woman_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4681F3FF = "👩🏼‍❤‍💋‍👨🏿", "👩🏼‍❤‍💋‍👨🏿 (:kiss_woman_man_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏼‍❤️‍💋‍👨🏻", "👩🏼‍❤️‍💋‍👨🏻 (:kiss_woman_man_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4681F3FB = "👩🏼‍❤‍💋‍👨🏻", "👩🏼‍❤‍💋‍👨🏻 (:kiss_woman_man_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏼‍❤️‍💋‍👨🏾", "👩🏼‍❤️‍💋‍👨🏾 (:kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4681F3FE = "👩🏼‍❤‍💋‍👨🏾", "👩🏼‍❤‍💋‍👨🏾 (:kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏼‍❤️‍💋‍👨🏽", "👩🏼‍❤️‍💋‍👨🏽 (:kiss_woman_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4681F3FD = "👩🏼‍❤‍💋‍👨🏽", "👩🏼‍❤‍💋‍👨🏽 (:kiss_woman_man_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FD = "👩🏽‍❤️‍💋‍👨🏽", "👩🏽‍❤️‍💋‍👨🏽 (:kiss_woman_man_medium_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4681F3FD = "👩🏽‍❤‍💋‍👨🏽", "👩🏽‍❤‍💋‍👨🏽 (:kiss_woman_man_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FF = "👩🏽‍❤️‍💋‍👨🏿", "👩🏽‍❤️‍💋‍👨🏿 (:kiss_woman_man_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4681F3FF = "👩🏽‍❤‍💋‍👨🏿", "👩🏽‍❤‍💋‍👨🏿 (:kiss_woman_man_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FB = "👩🏽‍❤️‍💋‍👨🏻", "👩🏽‍❤️‍💋‍👨🏻 (:kiss_woman_man_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4681F3FB = "👩🏽‍❤‍💋‍👨🏻", "👩🏽‍❤‍💋‍👨🏻 (:kiss_woman_man_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FE = "👩🏽‍❤️‍💋‍👨🏾", "👩🏽‍❤️‍💋‍👨🏾 (:kiss_woman_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4681F3FE = "👩🏽‍❤‍💋‍👨🏾", "👩🏽‍❤‍💋‍👨🏾 (:kiss_woman_man_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4681F3FC = "👩🏽‍❤️‍💋‍👨🏼", "👩🏽‍❤️‍💋‍👨🏼 (:kiss_woman_man_medium_skin_tone_medium-light_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4681F3FC = "👩🏽‍❤‍💋‍👨🏼", "👩🏽‍❤‍💋‍👨🏼 (:kiss_woman_man_medium_skin_tone_medium-light_skin_tone:)" + U1F469200D2764FE0F200D1F48B200D1F469 = "👩‍❤️‍💋‍👩", "👩‍❤️‍💋‍👩 (:kiss_woman_woman:)" + U1F469200D2764200D1F48B200D1F469 = "👩‍❤‍💋‍👩", "👩‍❤‍💋‍👩 (:kiss_woman_woman:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏿‍❤️‍💋‍👩🏿", "👩🏿‍❤️‍💋‍👩🏿 (:kiss_woman_woman_dark_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4691F3FF = "👩🏿‍❤‍💋‍👩🏿", "👩🏿‍❤‍💋‍👩🏿 (:kiss_woman_woman_dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏿‍❤️‍💋‍👩🏻", "👩🏿‍❤️‍💋‍👩🏻 (:kiss_woman_woman_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4691F3FB = "👩🏿‍❤‍💋‍👩🏻", "👩🏿‍❤‍💋‍👩🏻 (:kiss_woman_woman_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏿‍❤️‍💋‍👩🏾", "👩🏿‍❤️‍💋‍👩🏾 (:kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4691F3FE = "👩🏿‍❤‍💋‍👩🏾", "👩🏿‍❤‍💋‍👩🏾 (:kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏿‍❤️‍💋‍👩🏼", "👩🏿‍❤️‍💋‍👩🏼 (:kiss_woman_woman_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4691F3FC = "👩🏿‍❤‍💋‍👩🏼", "👩🏿‍❤‍💋‍👩🏼 (:kiss_woman_woman_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏿‍❤️‍💋‍👩🏽", "👩🏿‍❤️‍💋‍👩🏽 (:kiss_woman_woman_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FF200D2764200D1F48B200D1F4691F3FD = "👩🏿‍❤‍💋‍👩🏽", "👩🏿‍❤‍💋‍👩🏽 (:kiss_woman_woman_dark_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏻‍❤️‍💋‍👩🏻", "👩🏻‍❤️‍💋‍👩🏻 (:kiss_woman_woman_light_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4691F3FB = "👩🏻‍❤‍💋‍👩🏻", "👩🏻‍❤‍💋‍👩🏻 (:kiss_woman_woman_light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏻‍❤️‍💋‍👩🏿", "👩🏻‍❤️‍💋‍👩🏿 (:kiss_woman_woman_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4691F3FF = "👩🏻‍❤‍💋‍👩🏿", "👩🏻‍❤‍💋‍👩🏿 (:kiss_woman_woman_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏻‍❤️‍💋‍👩🏾", "👩🏻‍❤️‍💋‍👩🏾 (:kiss_woman_woman_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4691F3FE = "👩🏻‍❤‍💋‍👩🏾", "👩🏻‍❤‍💋‍👩🏾 (:kiss_woman_woman_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏻‍❤️‍💋‍👩🏼", "👩🏻‍❤️‍💋‍👩🏼 (:kiss_woman_woman_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4691F3FC = "👩🏻‍❤‍💋‍👩🏼", "👩🏻‍❤‍💋‍👩🏼 (:kiss_woman_woman_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏻‍❤️‍💋‍👩🏽", "👩🏻‍❤️‍💋‍👩🏽 (:kiss_woman_woman_light_skin_tone_medium_skin_tone:)" + U1F4691F3FB200D2764200D1F48B200D1F4691F3FD = "👩🏻‍❤‍💋‍👩🏽", "👩🏻‍❤‍💋‍👩🏽 (:kiss_woman_woman_light_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏾‍❤️‍💋‍👩🏾", "👩🏾‍❤️‍💋‍👩🏾 (:kiss_woman_woman_medium-dark_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4691F3FE = "👩🏾‍❤‍💋‍👩🏾", "👩🏾‍❤‍💋‍👩🏾 (:kiss_woman_woman_medium-dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏾‍❤️‍💋‍👩🏿", "👩🏾‍❤️‍💋‍👩🏿 (:kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4691F3FF = "👩🏾‍❤‍💋‍👩🏿", "👩🏾‍❤‍💋‍👩🏿 (:kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏾‍❤️‍💋‍👩🏻", "👩🏾‍❤️‍💋‍👩🏻 (:kiss_woman_woman_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4691F3FB = "👩🏾‍❤‍💋‍👩🏻", "👩🏾‍❤‍💋‍👩🏻 (:kiss_woman_woman_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏾‍❤️‍💋‍👩🏼", "👩🏾‍❤️‍💋‍👩🏼 (:kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4691F3FC = "👩🏾‍❤‍💋‍👩🏼", "👩🏾‍❤‍💋‍👩🏼 (:kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏾‍❤️‍💋‍👩🏽", "👩🏾‍❤️‍💋‍👩🏽 (:kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FE200D2764200D1F48B200D1F4691F3FD = "👩🏾‍❤‍💋‍👩🏽", "👩🏾‍❤‍💋‍👩🏽 (:kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏼‍❤️‍💋‍👩🏼", "👩🏼‍❤️‍💋‍👩🏼 (:kiss_woman_woman_medium-light_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4691F3FC = "👩🏼‍❤‍💋‍👩🏼", "👩🏼‍❤‍💋‍👩🏼 (:kiss_woman_woman_medium-light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏼‍❤️‍💋‍👩🏿", "👩🏼‍❤️‍💋‍👩🏿 (:kiss_woman_woman_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4691F3FF = "👩🏼‍❤‍💋‍👩🏿", "👩🏼‍❤‍💋‍👩🏿 (:kiss_woman_woman_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏼‍❤️‍💋‍👩🏻", "👩🏼‍❤️‍💋‍👩🏻 (:kiss_woman_woman_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4691F3FB = "👩🏼‍❤‍💋‍👩🏻", "👩🏼‍❤‍💋‍👩🏻 (:kiss_woman_woman_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏼‍❤️‍💋‍👩🏾", "👩🏼‍❤️‍💋‍👩🏾 (:kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4691F3FE = "👩🏼‍❤‍💋‍👩🏾", "👩🏼‍❤‍💋‍👩🏾 (:kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏼‍❤️‍💋‍👩🏽", "👩🏼‍❤️‍💋‍👩🏽 (:kiss_woman_woman_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FC200D2764200D1F48B200D1F4691F3FD = "👩🏼‍❤‍💋‍👩🏽", "👩🏼‍❤‍💋‍👩🏽 (:kiss_woman_woman_medium-light_skin_tone_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FD = "👩🏽‍❤️‍💋‍👩🏽", "👩🏽‍❤️‍💋‍👩🏽 (:kiss_woman_woman_medium_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4691F3FD = "👩🏽‍❤‍💋‍👩🏽", "👩🏽‍❤‍💋‍👩🏽 (:kiss_woman_woman_medium_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FF = "👩🏽‍❤️‍💋‍👩🏿", "👩🏽‍❤️‍💋‍👩🏿 (:kiss_woman_woman_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4691F3FF = "👩🏽‍❤‍💋‍👩🏿", "👩🏽‍❤‍💋‍👩🏿 (:kiss_woman_woman_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FB = "👩🏽‍❤️‍💋‍👩🏻", "👩🏽‍❤️‍💋‍👩🏻 (:kiss_woman_woman_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4691F3FB = "👩🏽‍❤‍💋‍👩🏻", "👩🏽‍❤‍💋‍👩🏻 (:kiss_woman_woman_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FE = "👩🏽‍❤️‍💋‍👩🏾", "👩🏽‍❤️‍💋‍👩🏾 (:kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4691F3FE = "👩🏽‍❤‍💋‍👩🏾", "👩🏽‍❤‍💋‍👩🏾 (:kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D2764FE0F200D1F48B200D1F4691F3FC = "👩🏽‍❤️‍💋‍👩🏼", "👩🏽‍❤️‍💋‍👩🏼 (:kiss_woman_woman_medium_skin_tone_medium-light_skin_tone:)" + U1F4691F3FD200D2764200D1F48B200D1F4691F3FC = "👩🏽‍❤‍💋‍👩🏼", "👩🏽‍❤‍💋‍👩🏼 (:kiss_woman_woman_medium_skin_tone_medium-light_skin_tone:)" + U1F63D = "😽", "😽 (:kissing_cat:)" + U1F617 = "😗", "😗 (:kissing_face:)" + U1F61A = "😚", "😚 (:kissing_face_with_closed_eyes:)" + U1F619 = "😙", "😙 (:kissing_face_with_smiling_eyes:)" + U1F52A = "🔪", "🔪 (:kitchen_knife:)" + U1FA81 = "🪁", "🪁 (:kite:)" + U1F95D = "🥝", "🥝 (:kiwi_fruit:)" + U1FAA2 = "🪢", "🪢 (:knot:)" + U1F428 = "🐨", "🐨 (:koala:)" + U1F97C = "🥼", "🥼 (:lab_coat:)" + U1F3F7FE0F = "🏷️", "🏷️ (:label:)" + U1F3F7 = "🏷", "🏷 (:label:)" + U1F94D = "🥍", "🥍 (:lacrosse:)" + U1FA9C = "🪜", "🪜 (:ladder:)" + U1F41E = "🐞", "🐞 (:lady_beetle:)" + U1F4BB = "💻", "💻 (:laptop:)" + U1F537 = "🔷", "🔷 (:large_blue_diamond:)" + U1F536 = "🔶", "🔶 (:large_orange_diamond:)" + U1F317 = "🌗", "🌗 (:last_quarter_moon:)" + U1F31C = "🌜", "🌜 (:last_quarter_moon_face:)" + U23EEFE0F = "⏮️", "⏮️ (:last_track_button:)" + U23EE = "⏮", "⏮ (:last_track_button:)" + U271DFE0F = "✝️", "✝️ (:latin_cross:)" + U271D = "✝", "✝ (:latin_cross:)" + U1F343 = "🍃", "🍃 (:leaf_fluttering_in_wind:)" + U1F96C = "🥬", "🥬 (:leafy_green:)" + U1F4D2 = "📒", "📒 (:ledger:)" + U1F91B = "🤛", "🤛 (:left-facing_fist:)" + U1F91B1F3FF = "🤛🏿", "🤛🏿 (:left-facing_fist_dark_skin_tone:)" + U1F91B1F3FB = "🤛🏻", "🤛🏻 (:left-facing_fist_light_skin_tone:)" + U1F91B1F3FE = "🤛🏾", "🤛🏾 (:left-facing_fist_medium-dark_skin_tone:)" + U1F91B1F3FC = "🤛🏼", "🤛🏼 (:left-facing_fist_medium-light_skin_tone:)" + U1F91B1F3FD = "🤛🏽", "🤛🏽 (:left-facing_fist_medium_skin_tone:)" + U2194FE0F = "↔️", "↔️ (:left-right_arrow:)" + U2194 = "↔", "↔ (:left-right_arrow:)" + U2B05FE0F = "⬅️", "⬅️ (:left_arrow:)" + U2B05 = "⬅", "⬅ (:left_arrow:)" + U21AAFE0F = "↪️", "↪️ (:left_arrow_curving_right:)" + U21AA = "↪", "↪ (:left_arrow_curving_right:)" + U1F6C5 = "🛅", "🛅 (:left_luggage:)" + U1F5E8FE0F = "🗨️", "🗨️ (:left_speech_bubble:)" + U1F5E8 = "🗨", "🗨 (:left_speech_bubble:)" + U1FAF2 = "🫲", "🫲 (:leftwards_hand:)" + U1FAF21F3FF = "🫲🏿", "🫲🏿 (:leftwards_hand_dark_skin_tone:)" + U1FAF21F3FB = "🫲🏻", "🫲🏻 (:leftwards_hand_light_skin_tone:)" + U1FAF21F3FE = "🫲🏾", "🫲🏾 (:leftwards_hand_medium-dark_skin_tone:)" + U1FAF21F3FC = "🫲🏼", "🫲🏼 (:leftwards_hand_medium-light_skin_tone:)" + U1FAF21F3FD = "🫲🏽", "🫲🏽 (:leftwards_hand_medium_skin_tone:)" + U1FAF7 = "🫷", "🫷 (:leftwards_pushing_hand:)" + U1FAF71F3FF = "🫷🏿", "🫷🏿 (:leftwards_pushing_hand_dark_skin_tone:)" + U1FAF71F3FB = "🫷🏻", "🫷🏻 (:leftwards_pushing_hand_light_skin_tone:)" + U1FAF71F3FE = "🫷🏾", "🫷🏾 (:leftwards_pushing_hand_medium-dark_skin_tone:)" + U1FAF71F3FC = "🫷🏼", "🫷🏼 (:leftwards_pushing_hand_medium-light_skin_tone:)" + U1FAF71F3FD = "🫷🏽", "🫷🏽 (:leftwards_pushing_hand_medium_skin_tone:)" + U1F9B5 = "🦵", "🦵 (:leg:)" + U1F9B51F3FF = "🦵🏿", "🦵🏿 (:leg_dark_skin_tone:)" + U1F9B51F3FB = "🦵🏻", "🦵🏻 (:leg_light_skin_tone:)" + U1F9B51F3FE = "🦵🏾", "🦵🏾 (:leg_medium-dark_skin_tone:)" + U1F9B51F3FC = "🦵🏼", "🦵🏼 (:leg_medium-light_skin_tone:)" + U1F9B51F3FD = "🦵🏽", "🦵🏽 (:leg_medium_skin_tone:)" + U1F34B = "🍋", "🍋 (:lemon:)" + U1F406 = "🐆", "🐆 (:leopard:)" + U1F39AFE0F = "🎚️", "🎚️ (:level_slider:)" + U1F39A = "🎚", "🎚 (:level_slider:)" + U1FA75 = "🩵", "🩵 (:light_blue_heart:)" + U1F4A1 = "💡", "💡 (:light_bulb:)" + U1F688 = "🚈", "🚈 (:light_rail:)" + U1F3FB = "🏻", "🏻 (:light_skin_tone:)" + U1F34B200D1F7E9 = "🍋‍🟩", "🍋‍🟩 (:lime:)" + U1F517 = "🔗", "🔗 (:link:)" + U1F587FE0F = "🖇️", "🖇️ (:linked_paperclips:)" + U1F587 = "🖇", "🖇 (:linked_paperclips:)" + U1F981 = "🦁", "🦁 (:lion:)" + U1F484 = "💄", "💄 (:lipstick:)" + U1F6AE = "🚮", "🚮 (:litter_in_bin_sign:)" + U1F98E = "🦎", "🦎 (:lizard:)" + U1F999 = "🦙", "🦙 (:llama:)" + U1F99E = "🦞", "🦞 (:lobster:)" + U1F512 = "🔒", "🔒 (:locked:)" + U1F510 = "🔐", "🔐 (:locked_with_key:)" + U1F50F = "🔏", "🔏 (:locked_with_pen:)" + U1F682 = "🚂", "🚂 (:locomotive:)" + U1F36D = "🍭", "🍭 (:lollipop:)" + U1FA98 = "🪘", "🪘 (:long_drum:)" + U1F9F4 = "🧴", "🧴 (:lotion_bottle:)" + U1FAB7 = "🪷", "🪷 (:lotus:)" + U1F62D = "😭", "😭 (:loudly_crying_face:)" + U1F4E2 = "📢", "📢 (:loudspeaker:)" + U1F91F = "🤟", "🤟 (:love-you_gesture:)" + U1F91F1F3FF = "🤟🏿", "🤟🏿 (:love-you_gesture_dark_skin_tone:)" + U1F91F1F3FB = "🤟🏻", "🤟🏻 (:love-you_gesture_light_skin_tone:)" + U1F91F1F3FE = "🤟🏾", "🤟🏾 (:love-you_gesture_medium-dark_skin_tone:)" + U1F91F1F3FC = "🤟🏼", "🤟🏼 (:love-you_gesture_medium-light_skin_tone:)" + U1F91F1F3FD = "🤟🏽", "🤟🏽 (:love-you_gesture_medium_skin_tone:)" + U1F3E9 = "🏩", "🏩 (:love_hotel:)" + U1F48C = "💌", "💌 (:love_letter:)" + U1FAAB = "🪫", "🪫 (:low_battery:)" + U1F9F3 = "🧳", "🧳 (:luggage:)" + U1FAC1 = "🫁", "🫁 (:lungs:)" + U1F925 = "🤥", "🤥 (:lying_face:)" + U1F9D9 = "🧙", "🧙 (:mage:)" + U1F9D91F3FF = "🧙🏿", "🧙🏿 (:mage_dark_skin_tone:)" + U1F9D91F3FB = "🧙🏻", "🧙🏻 (:mage_light_skin_tone:)" + U1F9D91F3FE = "🧙🏾", "🧙🏾 (:mage_medium-dark_skin_tone:)" + U1F9D91F3FC = "🧙🏼", "🧙🏼 (:mage_medium-light_skin_tone:)" + U1F9D91F3FD = "🧙🏽", "🧙🏽 (:mage_medium_skin_tone:)" + U1FA84 = "🪄", "🪄 (:magic_wand:)" + U1F9F2 = "🧲", "🧲 (:magnet:)" + U1F50D = "🔍", "🔍 (:magnifying_glass_tilted_left:)" + U1F50E = "🔎", "🔎 (:magnifying_glass_tilted_right:)" + U1F004 = "🀄", "🀄 (:mahjong_red_dragon:)" + U2642FE0F = "♂️", "♂️ (:male_sign:)" + U2642 = "♂", "♂ (:male_sign:)" + U1F9A3 = "🦣", "🦣 (:mammoth:)" + U1F468 = "👨", "👨 (:man:)" + U1F468200D1F3A8 = "👨‍🎨", "👨‍🎨 (:man_artist:)" + U1F4681F3FF200D1F3A8 = "👨🏿‍🎨", "👨🏿‍🎨 (:man_artist_dark_skin_tone:)" + U1F4681F3FB200D1F3A8 = "👨🏻‍🎨", "👨🏻‍🎨 (:man_artist_light_skin_tone:)" + U1F4681F3FE200D1F3A8 = "👨🏾‍🎨", "👨🏾‍🎨 (:man_artist_medium-dark_skin_tone:)" + U1F4681F3FC200D1F3A8 = "👨🏼‍🎨", "👨🏼‍🎨 (:man_artist_medium-light_skin_tone:)" + U1F4681F3FD200D1F3A8 = "👨🏽‍🎨", "👨🏽‍🎨 (:man_artist_medium_skin_tone:)" + U1F468200D1F680 = "👨‍🚀", "👨‍🚀 (:man_astronaut:)" + U1F4681F3FF200D1F680 = "👨🏿‍🚀", "👨🏿‍🚀 (:man_astronaut_dark_skin_tone:)" + U1F4681F3FB200D1F680 = "👨🏻‍🚀", "👨🏻‍🚀 (:man_astronaut_light_skin_tone:)" + U1F4681F3FE200D1F680 = "👨🏾‍🚀", "👨🏾‍🚀 (:man_astronaut_medium-dark_skin_tone:)" + U1F4681F3FC200D1F680 = "👨🏼‍🚀", "👨🏼‍🚀 (:man_astronaut_medium-light_skin_tone:)" + U1F4681F3FD200D1F680 = "👨🏽‍🚀", "👨🏽‍🚀 (:man_astronaut_medium_skin_tone:)" + U1F468200D1F9B2 = "👨‍🦲", "👨‍🦲 (:man_bald:)" + U1F9D4200D2642FE0F = "🧔‍♂️", "🧔‍♂️ (:man_beard:)" + U1F9D4200D2642 = "🧔‍♂", "🧔‍♂ (:man_beard:)" + U1F6B4200D2642FE0F = "🚴‍♂️", "🚴‍♂️ (:man_biking:)" + U1F6B4200D2642 = "🚴‍♂", "🚴‍♂ (:man_biking:)" + U1F6B41F3FF200D2642FE0F = "🚴🏿‍♂️", "🚴🏿‍♂️ (:man_biking_dark_skin_tone:)" + U1F6B41F3FF200D2642 = "🚴🏿‍♂", "🚴🏿‍♂ (:man_biking_dark_skin_tone:)" + U1F6B41F3FB200D2642FE0F = "🚴🏻‍♂️", "🚴🏻‍♂️ (:man_biking_light_skin_tone:)" + U1F6B41F3FB200D2642 = "🚴🏻‍♂", "🚴🏻‍♂ (:man_biking_light_skin_tone:)" + U1F6B41F3FE200D2642FE0F = "🚴🏾‍♂️", "🚴🏾‍♂️ (:man_biking_medium-dark_skin_tone:)" + U1F6B41F3FE200D2642 = "🚴🏾‍♂", "🚴🏾‍♂ (:man_biking_medium-dark_skin_tone:)" + U1F6B41F3FC200D2642FE0F = "🚴🏼‍♂️", "🚴🏼‍♂️ (:man_biking_medium-light_skin_tone:)" + U1F6B41F3FC200D2642 = "🚴🏼‍♂", "🚴🏼‍♂ (:man_biking_medium-light_skin_tone:)" + U1F6B41F3FD200D2642FE0F = "🚴🏽‍♂️", "🚴🏽‍♂️ (:man_biking_medium_skin_tone:)" + U1F6B41F3FD200D2642 = "🚴🏽‍♂", "🚴🏽‍♂ (:man_biking_medium_skin_tone:)" + U1F471200D2642FE0F = "👱‍♂️", "👱‍♂️ (:man_blond_hair:)" + U1F471200D2642 = "👱‍♂", "👱‍♂ (:man_blond_hair:)" + U26F9FE0F200D2642FE0F = "⛹️‍♂️", "⛹️‍♂️ (:man_bouncing_ball:)" + U26F9200D2642FE0F = "⛹‍♂️", "⛹‍♂️ (:man_bouncing_ball:)" + U26F9FE0F200D2642 = "⛹️‍♂", "⛹️‍♂ (:man_bouncing_ball:)" + U26F9200D2642 = "⛹‍♂", "⛹‍♂ (:man_bouncing_ball:)" + U26F91F3FF200D2642FE0F = "⛹🏿‍♂️", "⛹🏿‍♂️ (:man_bouncing_ball_dark_skin_tone:)" + U26F91F3FF200D2642 = "⛹🏿‍♂", "⛹🏿‍♂ (:man_bouncing_ball_dark_skin_tone:)" + U26F91F3FB200D2642FE0F = "⛹🏻‍♂️", "⛹🏻‍♂️ (:man_bouncing_ball_light_skin_tone:)" + U26F91F3FB200D2642 = "⛹🏻‍♂", "⛹🏻‍♂ (:man_bouncing_ball_light_skin_tone:)" + U26F91F3FE200D2642FE0F = "⛹🏾‍♂️", "⛹🏾‍♂️ (:man_bouncing_ball_medium-dark_skin_tone:)" + U26F91F3FE200D2642 = "⛹🏾‍♂", "⛹🏾‍♂ (:man_bouncing_ball_medium-dark_skin_tone:)" + U26F91F3FC200D2642FE0F = "⛹🏼‍♂️", "⛹🏼‍♂️ (:man_bouncing_ball_medium-light_skin_tone:)" + U26F91F3FC200D2642 = "⛹🏼‍♂", "⛹🏼‍♂ (:man_bouncing_ball_medium-light_skin_tone:)" + U26F91F3FD200D2642FE0F = "⛹🏽‍♂️", "⛹🏽‍♂️ (:man_bouncing_ball_medium_skin_tone:)" + U26F91F3FD200D2642 = "⛹🏽‍♂", "⛹🏽‍♂ (:man_bouncing_ball_medium_skin_tone:)" + U1F647200D2642FE0F = "🙇‍♂️", "🙇‍♂️ (:man_bowing:)" + U1F647200D2642 = "🙇‍♂", "🙇‍♂ (:man_bowing:)" + U1F6471F3FF200D2642FE0F = "🙇🏿‍♂️", "🙇🏿‍♂️ (:man_bowing_dark_skin_tone:)" + U1F6471F3FF200D2642 = "🙇🏿‍♂", "🙇🏿‍♂ (:man_bowing_dark_skin_tone:)" + U1F6471F3FB200D2642FE0F = "🙇🏻‍♂️", "🙇🏻‍♂️ (:man_bowing_light_skin_tone:)" + U1F6471F3FB200D2642 = "🙇🏻‍♂", "🙇🏻‍♂ (:man_bowing_light_skin_tone:)" + U1F6471F3FE200D2642FE0F = "🙇🏾‍♂️", "🙇🏾‍♂️ (:man_bowing_medium-dark_skin_tone:)" + U1F6471F3FE200D2642 = "🙇🏾‍♂", "🙇🏾‍♂ (:man_bowing_medium-dark_skin_tone:)" + U1F6471F3FC200D2642FE0F = "🙇🏼‍♂️", "🙇🏼‍♂️ (:man_bowing_medium-light_skin_tone:)" + U1F6471F3FC200D2642 = "🙇🏼‍♂", "🙇🏼‍♂ (:man_bowing_medium-light_skin_tone:)" + U1F6471F3FD200D2642FE0F = "🙇🏽‍♂️", "🙇🏽‍♂️ (:man_bowing_medium_skin_tone:)" + U1F6471F3FD200D2642 = "🙇🏽‍♂", "🙇🏽‍♂ (:man_bowing_medium_skin_tone:)" + U1F938200D2642FE0F = "🤸‍♂️", "🤸‍♂️ (:man_cartwheeling:)" + U1F938200D2642 = "🤸‍♂", "🤸‍♂ (:man_cartwheeling:)" + U1F9381F3FF200D2642FE0F = "🤸🏿‍♂️", "🤸🏿‍♂️ (:man_cartwheeling_dark_skin_tone:)" + U1F9381F3FF200D2642 = "🤸🏿‍♂", "🤸🏿‍♂ (:man_cartwheeling_dark_skin_tone:)" + U1F9381F3FB200D2642FE0F = "🤸🏻‍♂️", "🤸🏻‍♂️ (:man_cartwheeling_light_skin_tone:)" + U1F9381F3FB200D2642 = "🤸🏻‍♂", "🤸🏻‍♂ (:man_cartwheeling_light_skin_tone:)" + U1F9381F3FE200D2642FE0F = "🤸🏾‍♂️", "🤸🏾‍♂️ (:man_cartwheeling_medium-dark_skin_tone:)" + U1F9381F3FE200D2642 = "🤸🏾‍♂", "🤸🏾‍♂ (:man_cartwheeling_medium-dark_skin_tone:)" + U1F9381F3FC200D2642FE0F = "🤸🏼‍♂️", "🤸🏼‍♂️ (:man_cartwheeling_medium-light_skin_tone:)" + U1F9381F3FC200D2642 = "🤸🏼‍♂", "🤸🏼‍♂ (:man_cartwheeling_medium-light_skin_tone:)" + U1F9381F3FD200D2642FE0F = "🤸🏽‍♂️", "🤸🏽‍♂️ (:man_cartwheeling_medium_skin_tone:)" + U1F9381F3FD200D2642 = "🤸🏽‍♂", "🤸🏽‍♂ (:man_cartwheeling_medium_skin_tone:)" + U1F9D7200D2642FE0F = "🧗‍♂️", "🧗‍♂️ (:man_climbing:)" + U1F9D7200D2642 = "🧗‍♂", "🧗‍♂ (:man_climbing:)" + U1F9D71F3FF200D2642FE0F = "🧗🏿‍♂️", "🧗🏿‍♂️ (:man_climbing_dark_skin_tone:)" + U1F9D71F3FF200D2642 = "🧗🏿‍♂", "🧗🏿‍♂ (:man_climbing_dark_skin_tone:)" + U1F9D71F3FB200D2642FE0F = "🧗🏻‍♂️", "🧗🏻‍♂️ (:man_climbing_light_skin_tone:)" + U1F9D71F3FB200D2642 = "🧗🏻‍♂", "🧗🏻‍♂ (:man_climbing_light_skin_tone:)" + U1F9D71F3FE200D2642FE0F = "🧗🏾‍♂️", "🧗🏾‍♂️ (:man_climbing_medium-dark_skin_tone:)" + U1F9D71F3FE200D2642 = "🧗🏾‍♂", "🧗🏾‍♂ (:man_climbing_medium-dark_skin_tone:)" + U1F9D71F3FC200D2642FE0F = "🧗🏼‍♂️", "🧗🏼‍♂️ (:man_climbing_medium-light_skin_tone:)" + U1F9D71F3FC200D2642 = "🧗🏼‍♂", "🧗🏼‍♂ (:man_climbing_medium-light_skin_tone:)" + U1F9D71F3FD200D2642FE0F = "🧗🏽‍♂️", "🧗🏽‍♂️ (:man_climbing_medium_skin_tone:)" + U1F9D71F3FD200D2642 = "🧗🏽‍♂", "🧗🏽‍♂ (:man_climbing_medium_skin_tone:)" + U1F477200D2642FE0F = "👷‍♂️", "👷‍♂️ (:man_construction_worker:)" + U1F477200D2642 = "👷‍♂", "👷‍♂ (:man_construction_worker:)" + U1F4771F3FF200D2642FE0F = "👷🏿‍♂️", "👷🏿‍♂️ (:man_construction_worker_dark_skin_tone:)" + U1F4771F3FF200D2642 = "👷🏿‍♂", "👷🏿‍♂ (:man_construction_worker_dark_skin_tone:)" + U1F4771F3FB200D2642FE0F = "👷🏻‍♂️", "👷🏻‍♂️ (:man_construction_worker_light_skin_tone:)" + U1F4771F3FB200D2642 = "👷🏻‍♂", "👷🏻‍♂ (:man_construction_worker_light_skin_tone:)" + U1F4771F3FE200D2642FE0F = "👷🏾‍♂️", "👷🏾‍♂️ (:man_construction_worker_medium-dark_skin_tone:)" + U1F4771F3FE200D2642 = "👷🏾‍♂", "👷🏾‍♂ (:man_construction_worker_medium-dark_skin_tone:)" + U1F4771F3FC200D2642FE0F = "👷🏼‍♂️", "👷🏼‍♂️ (:man_construction_worker_medium-light_skin_tone:)" + U1F4771F3FC200D2642 = "👷🏼‍♂", "👷🏼‍♂ (:man_construction_worker_medium-light_skin_tone:)" + U1F4771F3FD200D2642FE0F = "👷🏽‍♂️", "👷🏽‍♂️ (:man_construction_worker_medium_skin_tone:)" + U1F4771F3FD200D2642 = "👷🏽‍♂", "👷🏽‍♂ (:man_construction_worker_medium_skin_tone:)" + U1F468200D1F373 = "👨‍🍳", "👨‍🍳 (:man_cook:)" + U1F4681F3FF200D1F373 = "👨🏿‍🍳", "👨🏿‍🍳 (:man_cook_dark_skin_tone:)" + U1F4681F3FB200D1F373 = "👨🏻‍🍳", "👨🏻‍🍳 (:man_cook_light_skin_tone:)" + U1F4681F3FE200D1F373 = "👨🏾‍🍳", "👨🏾‍🍳 (:man_cook_medium-dark_skin_tone:)" + U1F4681F3FC200D1F373 = "👨🏼‍🍳", "👨🏼‍🍳 (:man_cook_medium-light_skin_tone:)" + U1F4681F3FD200D1F373 = "👨🏽‍🍳", "👨🏽‍🍳 (:man_cook_medium_skin_tone:)" + U1F468200D1F9B1 = "👨‍🦱", "👨‍🦱 (:man_curly_hair:)" + U1F57A = "🕺", "🕺 (:man_dancing:)" + U1F57A1F3FF = "🕺🏿", "🕺🏿 (:man_dancing_dark_skin_tone:)" + U1F57A1F3FB = "🕺🏻", "🕺🏻 (:man_dancing_light_skin_tone:)" + U1F57A1F3FE = "🕺🏾", "🕺🏾 (:man_dancing_medium-dark_skin_tone:)" + U1F57A1F3FC = "🕺🏼", "🕺🏼 (:man_dancing_medium-light_skin_tone:)" + U1F57A1F3FD = "🕺🏽", "🕺🏽 (:man_dancing_medium_skin_tone:)" + U1F4681F3FF = "👨🏿", "👨🏿 (:man_dark_skin_tone:)" + U1F4681F3FF200D1F9B2 = "👨🏿‍🦲", "👨🏿‍🦲 (:man_dark_skin_tone_bald:)" + U1F9D41F3FF200D2642FE0F = "🧔🏿‍♂️", "🧔🏿‍♂️ (:man_dark_skin_tone_beard:)" + U1F9D41F3FF200D2642 = "🧔🏿‍♂", "🧔🏿‍♂ (:man_dark_skin_tone_beard:)" + U1F4711F3FF200D2642FE0F = "👱🏿‍♂️", "👱🏿‍♂️ (:man_dark_skin_tone_blond_hair:)" + U1F4711F3FF200D2642 = "👱🏿‍♂", "👱🏿‍♂ (:man_dark_skin_tone_blond_hair:)" + U1F4681F3FF200D1F9B1 = "👨🏿‍🦱", "👨🏿‍🦱 (:man_dark_skin_tone_curly_hair:)" + U1F4681F3FF200D1F9B0 = "👨🏿‍🦰", "👨🏿‍🦰 (:man_dark_skin_tone_red_hair:)" + U1F4681F3FF200D1F9B3 = "👨🏿‍🦳", "👨🏿‍🦳 (:man_dark_skin_tone_white_hair:)" + U1F575FE0F200D2642FE0F = "🕵️‍♂️", "🕵️‍♂️ (:man_detective:)" + U1F575200D2642FE0F = "🕵‍♂️", "🕵‍♂️ (:man_detective:)" + U1F575FE0F200D2642 = "🕵️‍♂", "🕵️‍♂ (:man_detective:)" + U1F575200D2642 = "🕵‍♂", "🕵‍♂ (:man_detective:)" + U1F5751F3FF200D2642FE0F = "🕵🏿‍♂️", "🕵🏿‍♂️ (:man_detective_dark_skin_tone:)" + U1F5751F3FF200D2642 = "🕵🏿‍♂", "🕵🏿‍♂ (:man_detective_dark_skin_tone:)" + U1F5751F3FB200D2642FE0F = "🕵🏻‍♂️", "🕵🏻‍♂️ (:man_detective_light_skin_tone:)" + U1F5751F3FB200D2642 = "🕵🏻‍♂", "🕵🏻‍♂ (:man_detective_light_skin_tone:)" + U1F5751F3FE200D2642FE0F = "🕵🏾‍♂️", "🕵🏾‍♂️ (:man_detective_medium-dark_skin_tone:)" + U1F5751F3FE200D2642 = "🕵🏾‍♂", "🕵🏾‍♂ (:man_detective_medium-dark_skin_tone:)" + U1F5751F3FC200D2642FE0F = "🕵🏼‍♂️", "🕵🏼‍♂️ (:man_detective_medium-light_skin_tone:)" + U1F5751F3FC200D2642 = "🕵🏼‍♂", "🕵🏼‍♂ (:man_detective_medium-light_skin_tone:)" + U1F5751F3FD200D2642FE0F = "🕵🏽‍♂️", "🕵🏽‍♂️ (:man_detective_medium_skin_tone:)" + U1F5751F3FD200D2642 = "🕵🏽‍♂", "🕵🏽‍♂ (:man_detective_medium_skin_tone:)" + U1F9DD200D2642FE0F = "🧝‍♂️", "🧝‍♂️ (:man_elf:)" + U1F9DD200D2642 = "🧝‍♂", "🧝‍♂ (:man_elf:)" + U1F9DD1F3FF200D2642FE0F = "🧝🏿‍♂️", "🧝🏿‍♂️ (:man_elf_dark_skin_tone:)" + U1F9DD1F3FF200D2642 = "🧝🏿‍♂", "🧝🏿‍♂ (:man_elf_dark_skin_tone:)" + U1F9DD1F3FB200D2642FE0F = "🧝🏻‍♂️", "🧝🏻‍♂️ (:man_elf_light_skin_tone:)" + U1F9DD1F3FB200D2642 = "🧝🏻‍♂", "🧝🏻‍♂ (:man_elf_light_skin_tone:)" + U1F9DD1F3FE200D2642FE0F = "🧝🏾‍♂️", "🧝🏾‍♂️ (:man_elf_medium-dark_skin_tone:)" + U1F9DD1F3FE200D2642 = "🧝🏾‍♂", "🧝🏾‍♂ (:man_elf_medium-dark_skin_tone:)" + U1F9DD1F3FC200D2642FE0F = "🧝🏼‍♂️", "🧝🏼‍♂️ (:man_elf_medium-light_skin_tone:)" + U1F9DD1F3FC200D2642 = "🧝🏼‍♂", "🧝🏼‍♂ (:man_elf_medium-light_skin_tone:)" + U1F9DD1F3FD200D2642FE0F = "🧝🏽‍♂️", "🧝🏽‍♂️ (:man_elf_medium_skin_tone:)" + U1F9DD1F3FD200D2642 = "🧝🏽‍♂", "🧝🏽‍♂ (:man_elf_medium_skin_tone:)" + U1F926200D2642FE0F = "🤦‍♂️", "🤦‍♂️ (:man_facepalming:)" + U1F926200D2642 = "🤦‍♂", "🤦‍♂ (:man_facepalming:)" + U1F9261F3FF200D2642FE0F = "🤦🏿‍♂️", "🤦🏿‍♂️ (:man_facepalming_dark_skin_tone:)" + U1F9261F3FF200D2642 = "🤦🏿‍♂", "🤦🏿‍♂ (:man_facepalming_dark_skin_tone:)" + U1F9261F3FB200D2642FE0F = "🤦🏻‍♂️", "🤦🏻‍♂️ (:man_facepalming_light_skin_tone:)" + U1F9261F3FB200D2642 = "🤦🏻‍♂", "🤦🏻‍♂ (:man_facepalming_light_skin_tone:)" + U1F9261F3FE200D2642FE0F = "🤦🏾‍♂️", "🤦🏾‍♂️ (:man_facepalming_medium-dark_skin_tone:)" + U1F9261F3FE200D2642 = "🤦🏾‍♂", "🤦🏾‍♂ (:man_facepalming_medium-dark_skin_tone:)" + U1F9261F3FC200D2642FE0F = "🤦🏼‍♂️", "🤦🏼‍♂️ (:man_facepalming_medium-light_skin_tone:)" + U1F9261F3FC200D2642 = "🤦🏼‍♂", "🤦🏼‍♂ (:man_facepalming_medium-light_skin_tone:)" + U1F9261F3FD200D2642FE0F = "🤦🏽‍♂️", "🤦🏽‍♂️ (:man_facepalming_medium_skin_tone:)" + U1F9261F3FD200D2642 = "🤦🏽‍♂", "🤦🏽‍♂ (:man_facepalming_medium_skin_tone:)" + U1F468200D1F3ED = "👨‍🏭", "👨‍🏭 (:man_factory_worker:)" + U1F4681F3FF200D1F3ED = "👨🏿‍🏭", "👨🏿‍🏭 (:man_factory_worker_dark_skin_tone:)" + U1F4681F3FB200D1F3ED = "👨🏻‍🏭", "👨🏻‍🏭 (:man_factory_worker_light_skin_tone:)" + U1F4681F3FE200D1F3ED = "👨🏾‍🏭", "👨🏾‍🏭 (:man_factory_worker_medium-dark_skin_tone:)" + U1F4681F3FC200D1F3ED = "👨🏼‍🏭", "👨🏼‍🏭 (:man_factory_worker_medium-light_skin_tone:)" + U1F4681F3FD200D1F3ED = "👨🏽‍🏭", "👨🏽‍🏭 (:man_factory_worker_medium_skin_tone:)" + U1F9DA200D2642FE0F = "🧚‍♂️", "🧚‍♂️ (:man_fairy:)" + U1F9DA200D2642 = "🧚‍♂", "🧚‍♂ (:man_fairy:)" + U1F9DA1F3FF200D2642FE0F = "🧚🏿‍♂️", "🧚🏿‍♂️ (:man_fairy_dark_skin_tone:)" + U1F9DA1F3FF200D2642 = "🧚🏿‍♂", "🧚🏿‍♂ (:man_fairy_dark_skin_tone:)" + U1F9DA1F3FB200D2642FE0F = "🧚🏻‍♂️", "🧚🏻‍♂️ (:man_fairy_light_skin_tone:)" + U1F9DA1F3FB200D2642 = "🧚🏻‍♂", "🧚🏻‍♂ (:man_fairy_light_skin_tone:)" + U1F9DA1F3FE200D2642FE0F = "🧚🏾‍♂️", "🧚🏾‍♂️ (:man_fairy_medium-dark_skin_tone:)" + U1F9DA1F3FE200D2642 = "🧚🏾‍♂", "🧚🏾‍♂ (:man_fairy_medium-dark_skin_tone:)" + U1F9DA1F3FC200D2642FE0F = "🧚🏼‍♂️", "🧚🏼‍♂️ (:man_fairy_medium-light_skin_tone:)" + U1F9DA1F3FC200D2642 = "🧚🏼‍♂", "🧚🏼‍♂ (:man_fairy_medium-light_skin_tone:)" + U1F9DA1F3FD200D2642FE0F = "🧚🏽‍♂️", "🧚🏽‍♂️ (:man_fairy_medium_skin_tone:)" + U1F9DA1F3FD200D2642 = "🧚🏽‍♂", "🧚🏽‍♂ (:man_fairy_medium_skin_tone:)" + U1F468200D1F33E = "👨‍🌾", "👨‍🌾 (:man_farmer:)" + U1F4681F3FF200D1F33E = "👨🏿‍🌾", "👨🏿‍🌾 (:man_farmer_dark_skin_tone:)" + U1F4681F3FB200D1F33E = "👨🏻‍🌾", "👨🏻‍🌾 (:man_farmer_light_skin_tone:)" + U1F4681F3FE200D1F33E = "👨🏾‍🌾", "👨🏾‍🌾 (:man_farmer_medium-dark_skin_tone:)" + U1F4681F3FC200D1F33E = "👨🏼‍🌾", "👨🏼‍🌾 (:man_farmer_medium-light_skin_tone:)" + U1F4681F3FD200D1F33E = "👨🏽‍🌾", "👨🏽‍🌾 (:man_farmer_medium_skin_tone:)" + U1F468200D1F37C = "👨‍🍼", "👨‍🍼 (:man_feeding_baby:)" + U1F4681F3FF200D1F37C = "👨🏿‍🍼", "👨🏿‍🍼 (:man_feeding_baby_dark_skin_tone:)" + U1F4681F3FB200D1F37C = "👨🏻‍🍼", "👨🏻‍🍼 (:man_feeding_baby_light_skin_tone:)" + U1F4681F3FE200D1F37C = "👨🏾‍🍼", "👨🏾‍🍼 (:man_feeding_baby_medium-dark_skin_tone:)" + U1F4681F3FC200D1F37C = "👨🏼‍🍼", "👨🏼‍🍼 (:man_feeding_baby_medium-light_skin_tone:)" + U1F4681F3FD200D1F37C = "👨🏽‍🍼", "👨🏽‍🍼 (:man_feeding_baby_medium_skin_tone:)" + U1F468200D1F692 = "👨‍🚒", "👨‍🚒 (:man_firefighter:)" + U1F4681F3FF200D1F692 = "👨🏿‍🚒", "👨🏿‍🚒 (:man_firefighter_dark_skin_tone:)" + U1F4681F3FB200D1F692 = "👨🏻‍🚒", "👨🏻‍🚒 (:man_firefighter_light_skin_tone:)" + U1F4681F3FE200D1F692 = "👨🏾‍🚒", "👨🏾‍🚒 (:man_firefighter_medium-dark_skin_tone:)" + U1F4681F3FC200D1F692 = "👨🏼‍🚒", "👨🏼‍🚒 (:man_firefighter_medium-light_skin_tone:)" + U1F4681F3FD200D1F692 = "👨🏽‍🚒", "👨🏽‍🚒 (:man_firefighter_medium_skin_tone:)" + U1F64D200D2642FE0F = "🙍‍♂️", "🙍‍♂️ (:man_frowning:)" + U1F64D200D2642 = "🙍‍♂", "🙍‍♂ (:man_frowning:)" + U1F64D1F3FF200D2642FE0F = "🙍🏿‍♂️", "🙍🏿‍♂️ (:man_frowning_dark_skin_tone:)" + U1F64D1F3FF200D2642 = "🙍🏿‍♂", "🙍🏿‍♂ (:man_frowning_dark_skin_tone:)" + U1F64D1F3FB200D2642FE0F = "🙍🏻‍♂️", "🙍🏻‍♂️ (:man_frowning_light_skin_tone:)" + U1F64D1F3FB200D2642 = "🙍🏻‍♂", "🙍🏻‍♂ (:man_frowning_light_skin_tone:)" + U1F64D1F3FE200D2642FE0F = "🙍🏾‍♂️", "🙍🏾‍♂️ (:man_frowning_medium-dark_skin_tone:)" + U1F64D1F3FE200D2642 = "🙍🏾‍♂", "🙍🏾‍♂ (:man_frowning_medium-dark_skin_tone:)" + U1F64D1F3FC200D2642FE0F = "🙍🏼‍♂️", "🙍🏼‍♂️ (:man_frowning_medium-light_skin_tone:)" + U1F64D1F3FC200D2642 = "🙍🏼‍♂", "🙍🏼‍♂ (:man_frowning_medium-light_skin_tone:)" + U1F64D1F3FD200D2642FE0F = "🙍🏽‍♂️", "🙍🏽‍♂️ (:man_frowning_medium_skin_tone:)" + U1F64D1F3FD200D2642 = "🙍🏽‍♂", "🙍🏽‍♂ (:man_frowning_medium_skin_tone:)" + U1F9DE200D2642FE0F = "🧞‍♂️", "🧞‍♂️ (:man_genie:)" + U1F9DE200D2642 = "🧞‍♂", "🧞‍♂ (:man_genie:)" + U1F645200D2642FE0F = "🙅‍♂️", "🙅‍♂️ (:man_gesturing_NO:)" + U1F645200D2642 = "🙅‍♂", "🙅‍♂ (:man_gesturing_NO:)" + U1F6451F3FF200D2642FE0F = "🙅🏿‍♂️", "🙅🏿‍♂️ (:man_gesturing_NO_dark_skin_tone:)" + U1F6451F3FF200D2642 = "🙅🏿‍♂", "🙅🏿‍♂ (:man_gesturing_NO_dark_skin_tone:)" + U1F6451F3FB200D2642FE0F = "🙅🏻‍♂️", "🙅🏻‍♂️ (:man_gesturing_NO_light_skin_tone:)" + U1F6451F3FB200D2642 = "🙅🏻‍♂", "🙅🏻‍♂ (:man_gesturing_NO_light_skin_tone:)" + U1F6451F3FE200D2642FE0F = "🙅🏾‍♂️", "🙅🏾‍♂️ (:man_gesturing_NO_medium-dark_skin_tone:)" + U1F6451F3FE200D2642 = "🙅🏾‍♂", "🙅🏾‍♂ (:man_gesturing_NO_medium-dark_skin_tone:)" + U1F6451F3FC200D2642FE0F = "🙅🏼‍♂️", "🙅🏼‍♂️ (:man_gesturing_NO_medium-light_skin_tone:)" + U1F6451F3FC200D2642 = "🙅🏼‍♂", "🙅🏼‍♂ (:man_gesturing_NO_medium-light_skin_tone:)" + U1F6451F3FD200D2642FE0F = "🙅🏽‍♂️", "🙅🏽‍♂️ (:man_gesturing_NO_medium_skin_tone:)" + U1F6451F3FD200D2642 = "🙅🏽‍♂", "🙅🏽‍♂ (:man_gesturing_NO_medium_skin_tone:)" + U1F646200D2642FE0F = "🙆‍♂️", "🙆‍♂️ (:man_gesturing_OK:)" + U1F646200D2642 = "🙆‍♂", "🙆‍♂ (:man_gesturing_OK:)" + U1F6461F3FF200D2642FE0F = "🙆🏿‍♂️", "🙆🏿‍♂️ (:man_gesturing_OK_dark_skin_tone:)" + U1F6461F3FF200D2642 = "🙆🏿‍♂", "🙆🏿‍♂ (:man_gesturing_OK_dark_skin_tone:)" + U1F6461F3FB200D2642FE0F = "🙆🏻‍♂️", "🙆🏻‍♂️ (:man_gesturing_OK_light_skin_tone:)" + U1F6461F3FB200D2642 = "🙆🏻‍♂", "🙆🏻‍♂ (:man_gesturing_OK_light_skin_tone:)" + U1F6461F3FE200D2642FE0F = "🙆🏾‍♂️", "🙆🏾‍♂️ (:man_gesturing_OK_medium-dark_skin_tone:)" + U1F6461F3FE200D2642 = "🙆🏾‍♂", "🙆🏾‍♂ (:man_gesturing_OK_medium-dark_skin_tone:)" + U1F6461F3FC200D2642FE0F = "🙆🏼‍♂️", "🙆🏼‍♂️ (:man_gesturing_OK_medium-light_skin_tone:)" + U1F6461F3FC200D2642 = "🙆🏼‍♂", "🙆🏼‍♂ (:man_gesturing_OK_medium-light_skin_tone:)" + U1F6461F3FD200D2642FE0F = "🙆🏽‍♂️", "🙆🏽‍♂️ (:man_gesturing_OK_medium_skin_tone:)" + U1F6461F3FD200D2642 = "🙆🏽‍♂", "🙆🏽‍♂ (:man_gesturing_OK_medium_skin_tone:)" + U1F487200D2642FE0F = "💇‍♂️", "💇‍♂️ (:man_getting_haircut:)" + U1F487200D2642 = "💇‍♂", "💇‍♂ (:man_getting_haircut:)" + U1F4871F3FF200D2642FE0F = "💇🏿‍♂️", "💇🏿‍♂️ (:man_getting_haircut_dark_skin_tone:)" + U1F4871F3FF200D2642 = "💇🏿‍♂", "💇🏿‍♂ (:man_getting_haircut_dark_skin_tone:)" + U1F4871F3FB200D2642FE0F = "💇🏻‍♂️", "💇🏻‍♂️ (:man_getting_haircut_light_skin_tone:)" + U1F4871F3FB200D2642 = "💇🏻‍♂", "💇🏻‍♂ (:man_getting_haircut_light_skin_tone:)" + U1F4871F3FE200D2642FE0F = "💇🏾‍♂️", "💇🏾‍♂️ (:man_getting_haircut_medium-dark_skin_tone:)" + U1F4871F3FE200D2642 = "💇🏾‍♂", "💇🏾‍♂ (:man_getting_haircut_medium-dark_skin_tone:)" + U1F4871F3FC200D2642FE0F = "💇🏼‍♂️", "💇🏼‍♂️ (:man_getting_haircut_medium-light_skin_tone:)" + U1F4871F3FC200D2642 = "💇🏼‍♂", "💇🏼‍♂ (:man_getting_haircut_medium-light_skin_tone:)" + U1F4871F3FD200D2642FE0F = "💇🏽‍♂️", "💇🏽‍♂️ (:man_getting_haircut_medium_skin_tone:)" + U1F4871F3FD200D2642 = "💇🏽‍♂", "💇🏽‍♂ (:man_getting_haircut_medium_skin_tone:)" + U1F486200D2642FE0F = "💆‍♂️", "💆‍♂️ (:man_getting_massage:)" + U1F486200D2642 = "💆‍♂", "💆‍♂ (:man_getting_massage:)" + U1F4861F3FF200D2642FE0F = "💆🏿‍♂️", "💆🏿‍♂️ (:man_getting_massage_dark_skin_tone:)" + U1F4861F3FF200D2642 = "💆🏿‍♂", "💆🏿‍♂ (:man_getting_massage_dark_skin_tone:)" + U1F4861F3FB200D2642FE0F = "💆🏻‍♂️", "💆🏻‍♂️ (:man_getting_massage_light_skin_tone:)" + U1F4861F3FB200D2642 = "💆🏻‍♂", "💆🏻‍♂ (:man_getting_massage_light_skin_tone:)" + U1F4861F3FE200D2642FE0F = "💆🏾‍♂️", "💆🏾‍♂️ (:man_getting_massage_medium-dark_skin_tone:)" + U1F4861F3FE200D2642 = "💆🏾‍♂", "💆🏾‍♂ (:man_getting_massage_medium-dark_skin_tone:)" + U1F4861F3FC200D2642FE0F = "💆🏼‍♂️", "💆🏼‍♂️ (:man_getting_massage_medium-light_skin_tone:)" + U1F4861F3FC200D2642 = "💆🏼‍♂", "💆🏼‍♂ (:man_getting_massage_medium-light_skin_tone:)" + U1F4861F3FD200D2642FE0F = "💆🏽‍♂️", "💆🏽‍♂️ (:man_getting_massage_medium_skin_tone:)" + U1F4861F3FD200D2642 = "💆🏽‍♂", "💆🏽‍♂ (:man_getting_massage_medium_skin_tone:)" + U1F3CCFE0F200D2642FE0F = "🏌️‍♂️", "🏌️‍♂️ (:man_golfing:)" + U1F3CC200D2642FE0F = "🏌‍♂️", "🏌‍♂️ (:man_golfing:)" + U1F3CCFE0F200D2642 = "🏌️‍♂", "🏌️‍♂ (:man_golfing:)" + U1F3CC200D2642 = "🏌‍♂", "🏌‍♂ (:man_golfing:)" + U1F3CC1F3FF200D2642FE0F = "🏌🏿‍♂️", "🏌🏿‍♂️ (:man_golfing_dark_skin_tone:)" + U1F3CC1F3FF200D2642 = "🏌🏿‍♂", "🏌🏿‍♂ (:man_golfing_dark_skin_tone:)" + U1F3CC1F3FB200D2642FE0F = "🏌🏻‍♂️", "🏌🏻‍♂️ (:man_golfing_light_skin_tone:)" + U1F3CC1F3FB200D2642 = "🏌🏻‍♂", "🏌🏻‍♂ (:man_golfing_light_skin_tone:)" + U1F3CC1F3FE200D2642FE0F = "🏌🏾‍♂️", "🏌🏾‍♂️ (:man_golfing_medium-dark_skin_tone:)" + U1F3CC1F3FE200D2642 = "🏌🏾‍♂", "🏌🏾‍♂ (:man_golfing_medium-dark_skin_tone:)" + U1F3CC1F3FC200D2642FE0F = "🏌🏼‍♂️", "🏌🏼‍♂️ (:man_golfing_medium-light_skin_tone:)" + U1F3CC1F3FC200D2642 = "🏌🏼‍♂", "🏌🏼‍♂ (:man_golfing_medium-light_skin_tone:)" + U1F3CC1F3FD200D2642FE0F = "🏌🏽‍♂️", "🏌🏽‍♂️ (:man_golfing_medium_skin_tone:)" + U1F3CC1F3FD200D2642 = "🏌🏽‍♂", "🏌🏽‍♂ (:man_golfing_medium_skin_tone:)" + U1F482200D2642FE0F = "💂‍♂️", "💂‍♂️ (:man_guard:)" + U1F482200D2642 = "💂‍♂", "💂‍♂ (:man_guard:)" + U1F4821F3FF200D2642FE0F = "💂🏿‍♂️", "💂🏿‍♂️ (:man_guard_dark_skin_tone:)" + U1F4821F3FF200D2642 = "💂🏿‍♂", "💂🏿‍♂ (:man_guard_dark_skin_tone:)" + U1F4821F3FB200D2642FE0F = "💂🏻‍♂️", "💂🏻‍♂️ (:man_guard_light_skin_tone:)" + U1F4821F3FB200D2642 = "💂🏻‍♂", "💂🏻‍♂ (:man_guard_light_skin_tone:)" + U1F4821F3FE200D2642FE0F = "💂🏾‍♂️", "💂🏾‍♂️ (:man_guard_medium-dark_skin_tone:)" + U1F4821F3FE200D2642 = "💂🏾‍♂", "💂🏾‍♂ (:man_guard_medium-dark_skin_tone:)" + U1F4821F3FC200D2642FE0F = "💂🏼‍♂️", "💂🏼‍♂️ (:man_guard_medium-light_skin_tone:)" + U1F4821F3FC200D2642 = "💂🏼‍♂", "💂🏼‍♂ (:man_guard_medium-light_skin_tone:)" + U1F4821F3FD200D2642FE0F = "💂🏽‍♂️", "💂🏽‍♂️ (:man_guard_medium_skin_tone:)" + U1F4821F3FD200D2642 = "💂🏽‍♂", "💂🏽‍♂ (:man_guard_medium_skin_tone:)" + U1F468200D2695FE0F = "👨‍⚕️", "👨‍⚕️ (:man_health_worker:)" + U1F468200D2695 = "👨‍⚕", "👨‍⚕ (:man_health_worker:)" + U1F4681F3FF200D2695FE0F = "👨🏿‍⚕️", "👨🏿‍⚕️ (:man_health_worker_dark_skin_tone:)" + U1F4681F3FF200D2695 = "👨🏿‍⚕", "👨🏿‍⚕ (:man_health_worker_dark_skin_tone:)" + U1F4681F3FB200D2695FE0F = "👨🏻‍⚕️", "👨🏻‍⚕️ (:man_health_worker_light_skin_tone:)" + U1F4681F3FB200D2695 = "👨🏻‍⚕", "👨🏻‍⚕ (:man_health_worker_light_skin_tone:)" + U1F4681F3FE200D2695FE0F = "👨🏾‍⚕️", "👨🏾‍⚕️ (:man_health_worker_medium-dark_skin_tone:)" + U1F4681F3FE200D2695 = "👨🏾‍⚕", "👨🏾‍⚕ (:man_health_worker_medium-dark_skin_tone:)" + U1F4681F3FC200D2695FE0F = "👨🏼‍⚕️", "👨🏼‍⚕️ (:man_health_worker_medium-light_skin_tone:)" + U1F4681F3FC200D2695 = "👨🏼‍⚕", "👨🏼‍⚕ (:man_health_worker_medium-light_skin_tone:)" + U1F4681F3FD200D2695FE0F = "👨🏽‍⚕️", "👨🏽‍⚕️ (:man_health_worker_medium_skin_tone:)" + U1F4681F3FD200D2695 = "👨🏽‍⚕", "👨🏽‍⚕ (:man_health_worker_medium_skin_tone:)" + U1F9D8200D2642FE0F = "🧘‍♂️", "🧘‍♂️ (:man_in_lotus_position:)" + U1F9D8200D2642 = "🧘‍♂", "🧘‍♂ (:man_in_lotus_position:)" + U1F9D81F3FF200D2642FE0F = "🧘🏿‍♂️", "🧘🏿‍♂️ (:man_in_lotus_position_dark_skin_tone:)" + U1F9D81F3FF200D2642 = "🧘🏿‍♂", "🧘🏿‍♂ (:man_in_lotus_position_dark_skin_tone:)" + U1F9D81F3FB200D2642FE0F = "🧘🏻‍♂️", "🧘🏻‍♂️ (:man_in_lotus_position_light_skin_tone:)" + U1F9D81F3FB200D2642 = "🧘🏻‍♂", "🧘🏻‍♂ (:man_in_lotus_position_light_skin_tone:)" + U1F9D81F3FE200D2642FE0F = "🧘🏾‍♂️", "🧘🏾‍♂️ (:man_in_lotus_position_medium-dark_skin_tone:)" + U1F9D81F3FE200D2642 = "🧘🏾‍♂", "🧘🏾‍♂ (:man_in_lotus_position_medium-dark_skin_tone:)" + U1F9D81F3FC200D2642FE0F = "🧘🏼‍♂️", "🧘🏼‍♂️ (:man_in_lotus_position_medium-light_skin_tone:)" + U1F9D81F3FC200D2642 = "🧘🏼‍♂", "🧘🏼‍♂ (:man_in_lotus_position_medium-light_skin_tone:)" + U1F9D81F3FD200D2642FE0F = "🧘🏽‍♂️", "🧘🏽‍♂️ (:man_in_lotus_position_medium_skin_tone:)" + U1F9D81F3FD200D2642 = "🧘🏽‍♂", "🧘🏽‍♂ (:man_in_lotus_position_medium_skin_tone:)" + U1F468200D1F9BD = "👨‍🦽", "👨‍🦽 (:man_in_manual_wheelchair:)" + U1F4681F3FF200D1F9BD = "👨🏿‍🦽", "👨🏿‍🦽 (:man_in_manual_wheelchair_dark_skin_tone:)" + U1F468200D1F9BD200D27A1FE0F = "👨‍🦽‍➡️", "👨‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right:)" + U1F468200D1F9BD200D27A1 = "👨‍🦽‍➡", "👨‍🦽‍➡ (:man_in_manual_wheelchair_facing_right:)" + U1F4681F3FF200D1F9BD200D27A1FE0F = "👨🏿‍🦽‍➡️", "👨🏿‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F4681F3FF200D1F9BD200D27A1 = "👨🏿‍🦽‍➡", "👨🏿‍🦽‍➡ (:man_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F4681F3FB200D1F9BD200D27A1FE0F = "👨🏻‍🦽‍➡️", "👨🏻‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F4681F3FB200D1F9BD200D27A1 = "👨🏻‍🦽‍➡", "👨🏻‍🦽‍➡ (:man_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F4681F3FE200D1F9BD200D27A1FE0F = "👨🏾‍🦽‍➡️", "👨🏾‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4681F3FE200D1F9BD200D27A1 = "👨🏾‍🦽‍➡", "👨🏾‍🦽‍➡ (:man_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9BD200D27A1FE0F = "👨🏼‍🦽‍➡️", "👨🏼‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4681F3FC200D1F9BD200D27A1 = "👨🏼‍🦽‍➡", "👨🏼‍🦽‍➡ (:man_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4681F3FD200D1F9BD200D27A1FE0F = "👨🏽‍🦽‍➡️", "👨🏽‍🦽‍➡️ (:man_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F4681F3FD200D1F9BD200D27A1 = "👨🏽‍🦽‍➡", "👨🏽‍🦽‍➡ (:man_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F4681F3FB200D1F9BD = "👨🏻‍🦽", "👨🏻‍🦽 (:man_in_manual_wheelchair_light_skin_tone:)" + U1F4681F3FE200D1F9BD = "👨🏾‍🦽", "👨🏾‍🦽 (:man_in_manual_wheelchair_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9BD = "👨🏼‍🦽", "👨🏼‍🦽 (:man_in_manual_wheelchair_medium-light_skin_tone:)" + U1F4681F3FD200D1F9BD = "👨🏽‍🦽", "👨🏽‍🦽 (:man_in_manual_wheelchair_medium_skin_tone:)" + U1F468200D1F9BC = "👨‍🦼", "👨‍🦼 (:man_in_motorized_wheelchair:)" + U1F4681F3FF200D1F9BC = "👨🏿‍🦼", "👨🏿‍🦼 (:man_in_motorized_wheelchair_dark_skin_tone:)" + U1F468200D1F9BC200D27A1FE0F = "👨‍🦼‍➡️", "👨‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right:)" + U1F468200D1F9BC200D27A1 = "👨‍🦼‍➡", "👨‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right:)" + U1F4681F3FF200D1F9BC200D27A1FE0F = "👨🏿‍🦼‍➡️", "👨🏿‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F4681F3FF200D1F9BC200D27A1 = "👨🏿‍🦼‍➡", "👨🏿‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F4681F3FB200D1F9BC200D27A1FE0F = "👨🏻‍🦼‍➡️", "👨🏻‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F4681F3FB200D1F9BC200D27A1 = "👨🏻‍🦼‍➡", "👨🏻‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F4681F3FE200D1F9BC200D27A1FE0F = "👨🏾‍🦼‍➡️", "👨🏾‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4681F3FE200D1F9BC200D27A1 = "👨🏾‍🦼‍➡", "👨🏾‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9BC200D27A1FE0F = "👨🏼‍🦼‍➡️", "👨🏼‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4681F3FC200D1F9BC200D27A1 = "👨🏼‍🦼‍➡", "👨🏼‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4681F3FD200D1F9BC200D27A1FE0F = "👨🏽‍🦼‍➡️", "👨🏽‍🦼‍➡️ (:man_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F4681F3FD200D1F9BC200D27A1 = "👨🏽‍🦼‍➡", "👨🏽‍🦼‍➡ (:man_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F4681F3FB200D1F9BC = "👨🏻‍🦼", "👨🏻‍🦼 (:man_in_motorized_wheelchair_light_skin_tone:)" + U1F4681F3FE200D1F9BC = "👨🏾‍🦼", "👨🏾‍🦼 (:man_in_motorized_wheelchair_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9BC = "👨🏼‍🦼", "👨🏼‍🦼 (:man_in_motorized_wheelchair_medium-light_skin_tone:)" + U1F4681F3FD200D1F9BC = "👨🏽‍🦼", "👨🏽‍🦼 (:man_in_motorized_wheelchair_medium_skin_tone:)" + U1F9D6200D2642FE0F = "🧖‍♂️", "🧖‍♂️ (:man_in_steamy_room:)" + U1F9D6200D2642 = "🧖‍♂", "🧖‍♂ (:man_in_steamy_room:)" + U1F9D61F3FF200D2642FE0F = "🧖🏿‍♂️", "🧖🏿‍♂️ (:man_in_steamy_room_dark_skin_tone:)" + U1F9D61F3FF200D2642 = "🧖🏿‍♂", "🧖🏿‍♂ (:man_in_steamy_room_dark_skin_tone:)" + U1F9D61F3FB200D2642FE0F = "🧖🏻‍♂️", "🧖🏻‍♂️ (:man_in_steamy_room_light_skin_tone:)" + U1F9D61F3FB200D2642 = "🧖🏻‍♂", "🧖🏻‍♂ (:man_in_steamy_room_light_skin_tone:)" + U1F9D61F3FE200D2642FE0F = "🧖🏾‍♂️", "🧖🏾‍♂️ (:man_in_steamy_room_medium-dark_skin_tone:)" + U1F9D61F3FE200D2642 = "🧖🏾‍♂", "🧖🏾‍♂ (:man_in_steamy_room_medium-dark_skin_tone:)" + U1F9D61F3FC200D2642FE0F = "🧖🏼‍♂️", "🧖🏼‍♂️ (:man_in_steamy_room_medium-light_skin_tone:)" + U1F9D61F3FC200D2642 = "🧖🏼‍♂", "🧖🏼‍♂ (:man_in_steamy_room_medium-light_skin_tone:)" + U1F9D61F3FD200D2642FE0F = "🧖🏽‍♂️", "🧖🏽‍♂️ (:man_in_steamy_room_medium_skin_tone:)" + U1F9D61F3FD200D2642 = "🧖🏽‍♂", "🧖🏽‍♂ (:man_in_steamy_room_medium_skin_tone:)" + U1F935200D2642FE0F = "🤵‍♂️", "🤵‍♂️ (:man_in_tuxedo:)" + U1F935200D2642 = "🤵‍♂", "🤵‍♂ (:man_in_tuxedo:)" + U1F9351F3FF200D2642FE0F = "🤵🏿‍♂️", "🤵🏿‍♂️ (:man_in_tuxedo_dark_skin_tone:)" + U1F9351F3FF200D2642 = "🤵🏿‍♂", "🤵🏿‍♂ (:man_in_tuxedo_dark_skin_tone:)" + U1F9351F3FB200D2642FE0F = "🤵🏻‍♂️", "🤵🏻‍♂️ (:man_in_tuxedo_light_skin_tone:)" + U1F9351F3FB200D2642 = "🤵🏻‍♂", "🤵🏻‍♂ (:man_in_tuxedo_light_skin_tone:)" + U1F9351F3FE200D2642FE0F = "🤵🏾‍♂️", "🤵🏾‍♂️ (:man_in_tuxedo_medium-dark_skin_tone:)" + U1F9351F3FE200D2642 = "🤵🏾‍♂", "🤵🏾‍♂ (:man_in_tuxedo_medium-dark_skin_tone:)" + U1F9351F3FC200D2642FE0F = "🤵🏼‍♂️", "🤵🏼‍♂️ (:man_in_tuxedo_medium-light_skin_tone:)" + U1F9351F3FC200D2642 = "🤵🏼‍♂", "🤵🏼‍♂ (:man_in_tuxedo_medium-light_skin_tone:)" + U1F9351F3FD200D2642FE0F = "🤵🏽‍♂️", "🤵🏽‍♂️ (:man_in_tuxedo_medium_skin_tone:)" + U1F9351F3FD200D2642 = "🤵🏽‍♂", "🤵🏽‍♂ (:man_in_tuxedo_medium_skin_tone:)" + U1F468200D2696FE0F = "👨‍⚖️", "👨‍⚖️ (:man_judge:)" + U1F468200D2696 = "👨‍⚖", "👨‍⚖ (:man_judge:)" + U1F4681F3FF200D2696FE0F = "👨🏿‍⚖️", "👨🏿‍⚖️ (:man_judge_dark_skin_tone:)" + U1F4681F3FF200D2696 = "👨🏿‍⚖", "👨🏿‍⚖ (:man_judge_dark_skin_tone:)" + U1F4681F3FB200D2696FE0F = "👨🏻‍⚖️", "👨🏻‍⚖️ (:man_judge_light_skin_tone:)" + U1F4681F3FB200D2696 = "👨🏻‍⚖", "👨🏻‍⚖ (:man_judge_light_skin_tone:)" + U1F4681F3FE200D2696FE0F = "👨🏾‍⚖️", "👨🏾‍⚖️ (:man_judge_medium-dark_skin_tone:)" + U1F4681F3FE200D2696 = "👨🏾‍⚖", "👨🏾‍⚖ (:man_judge_medium-dark_skin_tone:)" + U1F4681F3FC200D2696FE0F = "👨🏼‍⚖️", "👨🏼‍⚖️ (:man_judge_medium-light_skin_tone:)" + U1F4681F3FC200D2696 = "👨🏼‍⚖", "👨🏼‍⚖ (:man_judge_medium-light_skin_tone:)" + U1F4681F3FD200D2696FE0F = "👨🏽‍⚖️", "👨🏽‍⚖️ (:man_judge_medium_skin_tone:)" + U1F4681F3FD200D2696 = "👨🏽‍⚖", "👨🏽‍⚖ (:man_judge_medium_skin_tone:)" + U1F939200D2642FE0F = "🤹‍♂️", "🤹‍♂️ (:man_juggling:)" + U1F939200D2642 = "🤹‍♂", "🤹‍♂ (:man_juggling:)" + U1F9391F3FF200D2642FE0F = "🤹🏿‍♂️", "🤹🏿‍♂️ (:man_juggling_dark_skin_tone:)" + U1F9391F3FF200D2642 = "🤹🏿‍♂", "🤹🏿‍♂ (:man_juggling_dark_skin_tone:)" + U1F9391F3FB200D2642FE0F = "🤹🏻‍♂️", "🤹🏻‍♂️ (:man_juggling_light_skin_tone:)" + U1F9391F3FB200D2642 = "🤹🏻‍♂", "🤹🏻‍♂ (:man_juggling_light_skin_tone:)" + U1F9391F3FE200D2642FE0F = "🤹🏾‍♂️", "🤹🏾‍♂️ (:man_juggling_medium-dark_skin_tone:)" + U1F9391F3FE200D2642 = "🤹🏾‍♂", "🤹🏾‍♂ (:man_juggling_medium-dark_skin_tone:)" + U1F9391F3FC200D2642FE0F = "🤹🏼‍♂️", "🤹🏼‍♂️ (:man_juggling_medium-light_skin_tone:)" + U1F9391F3FC200D2642 = "🤹🏼‍♂", "🤹🏼‍♂ (:man_juggling_medium-light_skin_tone:)" + U1F9391F3FD200D2642FE0F = "🤹🏽‍♂️", "🤹🏽‍♂️ (:man_juggling_medium_skin_tone:)" + U1F9391F3FD200D2642 = "🤹🏽‍♂", "🤹🏽‍♂ (:man_juggling_medium_skin_tone:)" + U1F9CE200D2642FE0F = "🧎‍♂️", "🧎‍♂️ (:man_kneeling:)" + U1F9CE200D2642 = "🧎‍♂", "🧎‍♂ (:man_kneeling:)" + U1F9CE1F3FF200D2642FE0F = "🧎🏿‍♂️", "🧎🏿‍♂️ (:man_kneeling_dark_skin_tone:)" + U1F9CE1F3FF200D2642 = "🧎🏿‍♂", "🧎🏿‍♂ (:man_kneeling_dark_skin_tone:)" + U1F9CE200D2642FE0F200D27A1FE0F = "🧎‍♂️‍➡️", "🧎‍♂️‍➡️ (:man_kneeling_facing_right:)" + U1F9CE200D2642200D27A1FE0F = "🧎‍♂‍➡️", "🧎‍♂‍➡️ (:man_kneeling_facing_right:)" + U1F9CE200D2642FE0F200D27A1 = "🧎‍♂️‍➡", "🧎‍♂️‍➡ (:man_kneeling_facing_right:)" + U1F9CE200D2642200D27A1 = "🧎‍♂‍➡", "🧎‍♂‍➡ (:man_kneeling_facing_right:)" + U1F9CE1F3FF200D2642FE0F200D27A1FE0F = "🧎🏿‍♂️‍➡️", "🧎🏿‍♂️‍➡️ (:man_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2642200D27A1FE0F = "🧎🏿‍♂‍➡️", "🧎🏿‍♂‍➡️ (:man_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2642FE0F200D27A1 = "🧎🏿‍♂️‍➡", "🧎🏿‍♂️‍➡ (:man_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2642200D27A1 = "🧎🏿‍♂‍➡", "🧎🏿‍♂‍➡ (:man_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FB200D2642FE0F200D27A1FE0F = "🧎🏻‍♂️‍➡️", "🧎🏻‍♂️‍➡️ (:man_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2642200D27A1FE0F = "🧎🏻‍♂‍➡️", "🧎🏻‍♂‍➡️ (:man_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2642FE0F200D27A1 = "🧎🏻‍♂️‍➡", "🧎🏻‍♂️‍➡ (:man_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2642200D27A1 = "🧎🏻‍♂‍➡", "🧎🏻‍♂‍➡ (:man_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FE200D2642FE0F200D27A1FE0F = "🧎🏾‍♂️‍➡️", "🧎🏾‍♂️‍➡️ (:man_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2642200D27A1FE0F = "🧎🏾‍♂‍➡️", "🧎🏾‍♂‍➡️ (:man_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2642FE0F200D27A1 = "🧎🏾‍♂️‍➡", "🧎🏾‍♂️‍➡ (:man_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2642200D27A1 = "🧎🏾‍♂‍➡", "🧎🏾‍♂‍➡ (:man_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FC200D2642FE0F200D27A1FE0F = "🧎🏼‍♂️‍➡️", "🧎🏼‍♂️‍➡️ (:man_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2642200D27A1FE0F = "🧎🏼‍♂‍➡️", "🧎🏼‍♂‍➡️ (:man_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2642FE0F200D27A1 = "🧎🏼‍♂️‍➡", "🧎🏼‍♂️‍➡ (:man_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2642200D27A1 = "🧎🏼‍♂‍➡", "🧎🏼‍♂‍➡ (:man_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FD200D2642FE0F200D27A1FE0F = "🧎🏽‍♂️‍➡️", "🧎🏽‍♂️‍➡️ (:man_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2642200D27A1FE0F = "🧎🏽‍♂‍➡️", "🧎🏽‍♂‍➡️ (:man_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2642FE0F200D27A1 = "🧎🏽‍♂️‍➡", "🧎🏽‍♂️‍➡ (:man_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2642200D27A1 = "🧎🏽‍♂‍➡", "🧎🏽‍♂‍➡ (:man_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FB200D2642FE0F = "🧎🏻‍♂️", "🧎🏻‍♂️ (:man_kneeling_light_skin_tone:)" + U1F9CE1F3FB200D2642 = "🧎🏻‍♂", "🧎🏻‍♂ (:man_kneeling_light_skin_tone:)" + U1F9CE1F3FE200D2642FE0F = "🧎🏾‍♂️", "🧎🏾‍♂️ (:man_kneeling_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2642 = "🧎🏾‍♂", "🧎🏾‍♂ (:man_kneeling_medium-dark_skin_tone:)" + U1F9CE1F3FC200D2642FE0F = "🧎🏼‍♂️", "🧎🏼‍♂️ (:man_kneeling_medium-light_skin_tone:)" + U1F9CE1F3FC200D2642 = "🧎🏼‍♂", "🧎🏼‍♂ (:man_kneeling_medium-light_skin_tone:)" + U1F9CE1F3FD200D2642FE0F = "🧎🏽‍♂️", "🧎🏽‍♂️ (:man_kneeling_medium_skin_tone:)" + U1F9CE1F3FD200D2642 = "🧎🏽‍♂", "🧎🏽‍♂ (:man_kneeling_medium_skin_tone:)" + U1F3CBFE0F200D2642FE0F = "🏋️‍♂️", "🏋️‍♂️ (:man_lifting_weights:)" + U1F3CB200D2642FE0F = "🏋‍♂️", "🏋‍♂️ (:man_lifting_weights:)" + U1F3CBFE0F200D2642 = "🏋️‍♂", "🏋️‍♂ (:man_lifting_weights:)" + U1F3CB200D2642 = "🏋‍♂", "🏋‍♂ (:man_lifting_weights:)" + U1F3CB1F3FF200D2642FE0F = "🏋🏿‍♂️", "🏋🏿‍♂️ (:man_lifting_weights_dark_skin_tone:)" + U1F3CB1F3FF200D2642 = "🏋🏿‍♂", "🏋🏿‍♂ (:man_lifting_weights_dark_skin_tone:)" + U1F3CB1F3FB200D2642FE0F = "🏋🏻‍♂️", "🏋🏻‍♂️ (:man_lifting_weights_light_skin_tone:)" + U1F3CB1F3FB200D2642 = "🏋🏻‍♂", "🏋🏻‍♂ (:man_lifting_weights_light_skin_tone:)" + U1F3CB1F3FE200D2642FE0F = "🏋🏾‍♂️", "🏋🏾‍♂️ (:man_lifting_weights_medium-dark_skin_tone:)" + U1F3CB1F3FE200D2642 = "🏋🏾‍♂", "🏋🏾‍♂ (:man_lifting_weights_medium-dark_skin_tone:)" + U1F3CB1F3FC200D2642FE0F = "🏋🏼‍♂️", "🏋🏼‍♂️ (:man_lifting_weights_medium-light_skin_tone:)" + U1F3CB1F3FC200D2642 = "🏋🏼‍♂", "🏋🏼‍♂ (:man_lifting_weights_medium-light_skin_tone:)" + U1F3CB1F3FD200D2642FE0F = "🏋🏽‍♂️", "🏋🏽‍♂️ (:man_lifting_weights_medium_skin_tone:)" + U1F3CB1F3FD200D2642 = "🏋🏽‍♂", "🏋🏽‍♂ (:man_lifting_weights_medium_skin_tone:)" + U1F4681F3FB = "👨🏻", "👨🏻 (:man_light_skin_tone:)" + U1F4681F3FB200D1F9B2 = "👨🏻‍🦲", "👨🏻‍🦲 (:man_light_skin_tone_bald:)" + U1F9D41F3FB200D2642FE0F = "🧔🏻‍♂️", "🧔🏻‍♂️ (:man_light_skin_tone_beard:)" + U1F9D41F3FB200D2642 = "🧔🏻‍♂", "🧔🏻‍♂ (:man_light_skin_tone_beard:)" + U1F4711F3FB200D2642FE0F = "👱🏻‍♂️", "👱🏻‍♂️ (:man_light_skin_tone_blond_hair:)" + U1F4711F3FB200D2642 = "👱🏻‍♂", "👱🏻‍♂ (:man_light_skin_tone_blond_hair:)" + U1F4681F3FB200D1F9B1 = "👨🏻‍🦱", "👨🏻‍🦱 (:man_light_skin_tone_curly_hair:)" + U1F4681F3FB200D1F9B0 = "👨🏻‍🦰", "👨🏻‍🦰 (:man_light_skin_tone_red_hair:)" + U1F4681F3FB200D1F9B3 = "👨🏻‍🦳", "👨🏻‍🦳 (:man_light_skin_tone_white_hair:)" + U1F9D9200D2642FE0F = "🧙‍♂️", "🧙‍♂️ (:man_mage:)" + U1F9D9200D2642 = "🧙‍♂", "🧙‍♂ (:man_mage:)" + U1F9D91F3FF200D2642FE0F = "🧙🏿‍♂️", "🧙🏿‍♂️ (:man_mage_dark_skin_tone:)" + U1F9D91F3FF200D2642 = "🧙🏿‍♂", "🧙🏿‍♂ (:man_mage_dark_skin_tone:)" + U1F9D91F3FB200D2642FE0F = "🧙🏻‍♂️", "🧙🏻‍♂️ (:man_mage_light_skin_tone:)" + U1F9D91F3FB200D2642 = "🧙🏻‍♂", "🧙🏻‍♂ (:man_mage_light_skin_tone:)" + U1F9D91F3FE200D2642FE0F = "🧙🏾‍♂️", "🧙🏾‍♂️ (:man_mage_medium-dark_skin_tone:)" + U1F9D91F3FE200D2642 = "🧙🏾‍♂", "🧙🏾‍♂ (:man_mage_medium-dark_skin_tone:)" + U1F9D91F3FC200D2642FE0F = "🧙🏼‍♂️", "🧙🏼‍♂️ (:man_mage_medium-light_skin_tone:)" + U1F9D91F3FC200D2642 = "🧙🏼‍♂", "🧙🏼‍♂ (:man_mage_medium-light_skin_tone:)" + U1F9D91F3FD200D2642FE0F = "🧙🏽‍♂️", "🧙🏽‍♂️ (:man_mage_medium_skin_tone:)" + U1F9D91F3FD200D2642 = "🧙🏽‍♂", "🧙🏽‍♂ (:man_mage_medium_skin_tone:)" + U1F468200D1F527 = "👨‍🔧", "👨‍🔧 (:man_mechanic:)" + U1F4681F3FF200D1F527 = "👨🏿‍🔧", "👨🏿‍🔧 (:man_mechanic_dark_skin_tone:)" + U1F4681F3FB200D1F527 = "👨🏻‍🔧", "👨🏻‍🔧 (:man_mechanic_light_skin_tone:)" + U1F4681F3FE200D1F527 = "👨🏾‍🔧", "👨🏾‍🔧 (:man_mechanic_medium-dark_skin_tone:)" + U1F4681F3FC200D1F527 = "👨🏼‍🔧", "👨🏼‍🔧 (:man_mechanic_medium-light_skin_tone:)" + U1F4681F3FD200D1F527 = "👨🏽‍🔧", "👨🏽‍🔧 (:man_mechanic_medium_skin_tone:)" + U1F4681F3FE = "👨🏾", "👨🏾 (:man_medium-dark_skin_tone:)" + U1F4681F3FE200D1F9B2 = "👨🏾‍🦲", "👨🏾‍🦲 (:man_medium-dark_skin_tone_bald:)" + U1F9D41F3FE200D2642FE0F = "🧔🏾‍♂️", "🧔🏾‍♂️ (:man_medium-dark_skin_tone_beard:)" + U1F9D41F3FE200D2642 = "🧔🏾‍♂", "🧔🏾‍♂ (:man_medium-dark_skin_tone_beard:)" + U1F4711F3FE200D2642FE0F = "👱🏾‍♂️", "👱🏾‍♂️ (:man_medium-dark_skin_tone_blond_hair:)" + U1F4711F3FE200D2642 = "👱🏾‍♂", "👱🏾‍♂ (:man_medium-dark_skin_tone_blond_hair:)" + U1F4681F3FE200D1F9B1 = "👨🏾‍🦱", "👨🏾‍🦱 (:man_medium-dark_skin_tone_curly_hair:)" + U1F4681F3FE200D1F9B0 = "👨🏾‍🦰", "👨🏾‍🦰 (:man_medium-dark_skin_tone_red_hair:)" + U1F4681F3FE200D1F9B3 = "👨🏾‍🦳", "👨🏾‍🦳 (:man_medium-dark_skin_tone_white_hair:)" + U1F4681F3FC = "👨🏼", "👨🏼 (:man_medium-light_skin_tone:)" + U1F4681F3FC200D1F9B2 = "👨🏼‍🦲", "👨🏼‍🦲 (:man_medium-light_skin_tone_bald:)" + U1F9D41F3FC200D2642FE0F = "🧔🏼‍♂️", "🧔🏼‍♂️ (:man_medium-light_skin_tone_beard:)" + U1F9D41F3FC200D2642 = "🧔🏼‍♂", "🧔🏼‍♂ (:man_medium-light_skin_tone_beard:)" + U1F4711F3FC200D2642FE0F = "👱🏼‍♂️", "👱🏼‍♂️ (:man_medium-light_skin_tone_blond_hair:)" + U1F4711F3FC200D2642 = "👱🏼‍♂", "👱🏼‍♂ (:man_medium-light_skin_tone_blond_hair:)" + U1F4681F3FC200D1F9B1 = "👨🏼‍🦱", "👨🏼‍🦱 (:man_medium-light_skin_tone_curly_hair:)" + U1F4681F3FC200D1F9B0 = "👨🏼‍🦰", "👨🏼‍🦰 (:man_medium-light_skin_tone_red_hair:)" + U1F4681F3FC200D1F9B3 = "👨🏼‍🦳", "👨🏼‍🦳 (:man_medium-light_skin_tone_white_hair:)" + U1F4681F3FD = "👨🏽", "👨🏽 (:man_medium_skin_tone:)" + U1F4681F3FD200D1F9B2 = "👨🏽‍🦲", "👨🏽‍🦲 (:man_medium_skin_tone_bald:)" + U1F9D41F3FD200D2642FE0F = "🧔🏽‍♂️", "🧔🏽‍♂️ (:man_medium_skin_tone_beard:)" + U1F9D41F3FD200D2642 = "🧔🏽‍♂", "🧔🏽‍♂ (:man_medium_skin_tone_beard:)" + U1F4711F3FD200D2642FE0F = "👱🏽‍♂️", "👱🏽‍♂️ (:man_medium_skin_tone_blond_hair:)" + U1F4711F3FD200D2642 = "👱🏽‍♂", "👱🏽‍♂ (:man_medium_skin_tone_blond_hair:)" + U1F4681F3FD200D1F9B1 = "👨🏽‍🦱", "👨🏽‍🦱 (:man_medium_skin_tone_curly_hair:)" + U1F4681F3FD200D1F9B0 = "👨🏽‍🦰", "👨🏽‍🦰 (:man_medium_skin_tone_red_hair:)" + U1F4681F3FD200D1F9B3 = "👨🏽‍🦳", "👨🏽‍🦳 (:man_medium_skin_tone_white_hair:)" + U1F6B5200D2642FE0F = "🚵‍♂️", "🚵‍♂️ (:man_mountain_biking:)" + U1F6B5200D2642 = "🚵‍♂", "🚵‍♂ (:man_mountain_biking:)" + U1F6B51F3FF200D2642FE0F = "🚵🏿‍♂️", "🚵🏿‍♂️ (:man_mountain_biking_dark_skin_tone:)" + U1F6B51F3FF200D2642 = "🚵🏿‍♂", "🚵🏿‍♂ (:man_mountain_biking_dark_skin_tone:)" + U1F6B51F3FB200D2642FE0F = "🚵🏻‍♂️", "🚵🏻‍♂️ (:man_mountain_biking_light_skin_tone:)" + U1F6B51F3FB200D2642 = "🚵🏻‍♂", "🚵🏻‍♂ (:man_mountain_biking_light_skin_tone:)" + U1F6B51F3FE200D2642FE0F = "🚵🏾‍♂️", "🚵🏾‍♂️ (:man_mountain_biking_medium-dark_skin_tone:)" + U1F6B51F3FE200D2642 = "🚵🏾‍♂", "🚵🏾‍♂ (:man_mountain_biking_medium-dark_skin_tone:)" + U1F6B51F3FC200D2642FE0F = "🚵🏼‍♂️", "🚵🏼‍♂️ (:man_mountain_biking_medium-light_skin_tone:)" + U1F6B51F3FC200D2642 = "🚵🏼‍♂", "🚵🏼‍♂ (:man_mountain_biking_medium-light_skin_tone:)" + U1F6B51F3FD200D2642FE0F = "🚵🏽‍♂️", "🚵🏽‍♂️ (:man_mountain_biking_medium_skin_tone:)" + U1F6B51F3FD200D2642 = "🚵🏽‍♂", "🚵🏽‍♂ (:man_mountain_biking_medium_skin_tone:)" + U1F468200D1F4BC = "👨‍💼", "👨‍💼 (:man_office_worker:)" + U1F4681F3FF200D1F4BC = "👨🏿‍💼", "👨🏿‍💼 (:man_office_worker_dark_skin_tone:)" + U1F4681F3FB200D1F4BC = "👨🏻‍💼", "👨🏻‍💼 (:man_office_worker_light_skin_tone:)" + U1F4681F3FE200D1F4BC = "👨🏾‍💼", "👨🏾‍💼 (:man_office_worker_medium-dark_skin_tone:)" + U1F4681F3FC200D1F4BC = "👨🏼‍💼", "👨🏼‍💼 (:man_office_worker_medium-light_skin_tone:)" + U1F4681F3FD200D1F4BC = "👨🏽‍💼", "👨🏽‍💼 (:man_office_worker_medium_skin_tone:)" + U1F468200D2708FE0F = "👨‍✈️", "👨‍✈️ (:man_pilot:)" + U1F468200D2708 = "👨‍✈", "👨‍✈ (:man_pilot:)" + U1F4681F3FF200D2708FE0F = "👨🏿‍✈️", "👨🏿‍✈️ (:man_pilot_dark_skin_tone:)" + U1F4681F3FF200D2708 = "👨🏿‍✈", "👨🏿‍✈ (:man_pilot_dark_skin_tone:)" + U1F4681F3FB200D2708FE0F = "👨🏻‍✈️", "👨🏻‍✈️ (:man_pilot_light_skin_tone:)" + U1F4681F3FB200D2708 = "👨🏻‍✈", "👨🏻‍✈ (:man_pilot_light_skin_tone:)" + U1F4681F3FE200D2708FE0F = "👨🏾‍✈️", "👨🏾‍✈️ (:man_pilot_medium-dark_skin_tone:)" + U1F4681F3FE200D2708 = "👨🏾‍✈", "👨🏾‍✈ (:man_pilot_medium-dark_skin_tone:)" + U1F4681F3FC200D2708FE0F = "👨🏼‍✈️", "👨🏼‍✈️ (:man_pilot_medium-light_skin_tone:)" + U1F4681F3FC200D2708 = "👨🏼‍✈", "👨🏼‍✈ (:man_pilot_medium-light_skin_tone:)" + U1F4681F3FD200D2708FE0F = "👨🏽‍✈️", "👨🏽‍✈️ (:man_pilot_medium_skin_tone:)" + U1F4681F3FD200D2708 = "👨🏽‍✈", "👨🏽‍✈ (:man_pilot_medium_skin_tone:)" + U1F93E200D2642FE0F = "🤾‍♂️", "🤾‍♂️ (:man_playing_handball:)" + U1F93E200D2642 = "🤾‍♂", "🤾‍♂ (:man_playing_handball:)" + U1F93E1F3FF200D2642FE0F = "🤾🏿‍♂️", "🤾🏿‍♂️ (:man_playing_handball_dark_skin_tone:)" + U1F93E1F3FF200D2642 = "🤾🏿‍♂", "🤾🏿‍♂ (:man_playing_handball_dark_skin_tone:)" + U1F93E1F3FB200D2642FE0F = "🤾🏻‍♂️", "🤾🏻‍♂️ (:man_playing_handball_light_skin_tone:)" + U1F93E1F3FB200D2642 = "🤾🏻‍♂", "🤾🏻‍♂ (:man_playing_handball_light_skin_tone:)" + U1F93E1F3FE200D2642FE0F = "🤾🏾‍♂️", "🤾🏾‍♂️ (:man_playing_handball_medium-dark_skin_tone:)" + U1F93E1F3FE200D2642 = "🤾🏾‍♂", "🤾🏾‍♂ (:man_playing_handball_medium-dark_skin_tone:)" + U1F93E1F3FC200D2642FE0F = "🤾🏼‍♂️", "🤾🏼‍♂️ (:man_playing_handball_medium-light_skin_tone:)" + U1F93E1F3FC200D2642 = "🤾🏼‍♂", "🤾🏼‍♂ (:man_playing_handball_medium-light_skin_tone:)" + U1F93E1F3FD200D2642FE0F = "🤾🏽‍♂️", "🤾🏽‍♂️ (:man_playing_handball_medium_skin_tone:)" + U1F93E1F3FD200D2642 = "🤾🏽‍♂", "🤾🏽‍♂ (:man_playing_handball_medium_skin_tone:)" + U1F93D200D2642FE0F = "🤽‍♂️", "🤽‍♂️ (:man_playing_water_polo:)" + U1F93D200D2642 = "🤽‍♂", "🤽‍♂ (:man_playing_water_polo:)" + U1F93D1F3FF200D2642FE0F = "🤽🏿‍♂️", "🤽🏿‍♂️ (:man_playing_water_polo_dark_skin_tone:)" + U1F93D1F3FF200D2642 = "🤽🏿‍♂", "🤽🏿‍♂ (:man_playing_water_polo_dark_skin_tone:)" + U1F93D1F3FB200D2642FE0F = "🤽🏻‍♂️", "🤽🏻‍♂️ (:man_playing_water_polo_light_skin_tone:)" + U1F93D1F3FB200D2642 = "🤽🏻‍♂", "🤽🏻‍♂ (:man_playing_water_polo_light_skin_tone:)" + U1F93D1F3FE200D2642FE0F = "🤽🏾‍♂️", "🤽🏾‍♂️ (:man_playing_water_polo_medium-dark_skin_tone:)" + U1F93D1F3FE200D2642 = "🤽🏾‍♂", "🤽🏾‍♂ (:man_playing_water_polo_medium-dark_skin_tone:)" + U1F93D1F3FC200D2642FE0F = "🤽🏼‍♂️", "🤽🏼‍♂️ (:man_playing_water_polo_medium-light_skin_tone:)" + U1F93D1F3FC200D2642 = "🤽🏼‍♂", "🤽🏼‍♂ (:man_playing_water_polo_medium-light_skin_tone:)" + U1F93D1F3FD200D2642FE0F = "🤽🏽‍♂️", "🤽🏽‍♂️ (:man_playing_water_polo_medium_skin_tone:)" + U1F93D1F3FD200D2642 = "🤽🏽‍♂", "🤽🏽‍♂ (:man_playing_water_polo_medium_skin_tone:)" + U1F46E200D2642FE0F = "👮‍♂️", "👮‍♂️ (:man_police_officer:)" + U1F46E200D2642 = "👮‍♂", "👮‍♂ (:man_police_officer:)" + U1F46E1F3FF200D2642FE0F = "👮🏿‍♂️", "👮🏿‍♂️ (:man_police_officer_dark_skin_tone:)" + U1F46E1F3FF200D2642 = "👮🏿‍♂", "👮🏿‍♂ (:man_police_officer_dark_skin_tone:)" + U1F46E1F3FB200D2642FE0F = "👮🏻‍♂️", "👮🏻‍♂️ (:man_police_officer_light_skin_tone:)" + U1F46E1F3FB200D2642 = "👮🏻‍♂", "👮🏻‍♂ (:man_police_officer_light_skin_tone:)" + U1F46E1F3FE200D2642FE0F = "👮🏾‍♂️", "👮🏾‍♂️ (:man_police_officer_medium-dark_skin_tone:)" + U1F46E1F3FE200D2642 = "👮🏾‍♂", "👮🏾‍♂ (:man_police_officer_medium-dark_skin_tone:)" + U1F46E1F3FC200D2642FE0F = "👮🏼‍♂️", "👮🏼‍♂️ (:man_police_officer_medium-light_skin_tone:)" + U1F46E1F3FC200D2642 = "👮🏼‍♂", "👮🏼‍♂ (:man_police_officer_medium-light_skin_tone:)" + U1F46E1F3FD200D2642FE0F = "👮🏽‍♂️", "👮🏽‍♂️ (:man_police_officer_medium_skin_tone:)" + U1F46E1F3FD200D2642 = "👮🏽‍♂", "👮🏽‍♂ (:man_police_officer_medium_skin_tone:)" + U1F64E200D2642FE0F = "🙎‍♂️", "🙎‍♂️ (:man_pouting:)" + U1F64E200D2642 = "🙎‍♂", "🙎‍♂ (:man_pouting:)" + U1F64E1F3FF200D2642FE0F = "🙎🏿‍♂️", "🙎🏿‍♂️ (:man_pouting_dark_skin_tone:)" + U1F64E1F3FF200D2642 = "🙎🏿‍♂", "🙎🏿‍♂ (:man_pouting_dark_skin_tone:)" + U1F64E1F3FB200D2642FE0F = "🙎🏻‍♂️", "🙎🏻‍♂️ (:man_pouting_light_skin_tone:)" + U1F64E1F3FB200D2642 = "🙎🏻‍♂", "🙎🏻‍♂ (:man_pouting_light_skin_tone:)" + U1F64E1F3FE200D2642FE0F = "🙎🏾‍♂️", "🙎🏾‍♂️ (:man_pouting_medium-dark_skin_tone:)" + U1F64E1F3FE200D2642 = "🙎🏾‍♂", "🙎🏾‍♂ (:man_pouting_medium-dark_skin_tone:)" + U1F64E1F3FC200D2642FE0F = "🙎🏼‍♂️", "🙎🏼‍♂️ (:man_pouting_medium-light_skin_tone:)" + U1F64E1F3FC200D2642 = "🙎🏼‍♂", "🙎🏼‍♂ (:man_pouting_medium-light_skin_tone:)" + U1F64E1F3FD200D2642FE0F = "🙎🏽‍♂️", "🙎🏽‍♂️ (:man_pouting_medium_skin_tone:)" + U1F64E1F3FD200D2642 = "🙎🏽‍♂", "🙎🏽‍♂ (:man_pouting_medium_skin_tone:)" + U1F64B200D2642FE0F = "🙋‍♂️", "🙋‍♂️ (:man_raising_hand:)" + U1F64B200D2642 = "🙋‍♂", "🙋‍♂ (:man_raising_hand:)" + U1F64B1F3FF200D2642FE0F = "🙋🏿‍♂️", "🙋🏿‍♂️ (:man_raising_hand_dark_skin_tone:)" + U1F64B1F3FF200D2642 = "🙋🏿‍♂", "🙋🏿‍♂ (:man_raising_hand_dark_skin_tone:)" + U1F64B1F3FB200D2642FE0F = "🙋🏻‍♂️", "🙋🏻‍♂️ (:man_raising_hand_light_skin_tone:)" + U1F64B1F3FB200D2642 = "🙋🏻‍♂", "🙋🏻‍♂ (:man_raising_hand_light_skin_tone:)" + U1F64B1F3FE200D2642FE0F = "🙋🏾‍♂️", "🙋🏾‍♂️ (:man_raising_hand_medium-dark_skin_tone:)" + U1F64B1F3FE200D2642 = "🙋🏾‍♂", "🙋🏾‍♂ (:man_raising_hand_medium-dark_skin_tone:)" + U1F64B1F3FC200D2642FE0F = "🙋🏼‍♂️", "🙋🏼‍♂️ (:man_raising_hand_medium-light_skin_tone:)" + U1F64B1F3FC200D2642 = "🙋🏼‍♂", "🙋🏼‍♂ (:man_raising_hand_medium-light_skin_tone:)" + U1F64B1F3FD200D2642FE0F = "🙋🏽‍♂️", "🙋🏽‍♂️ (:man_raising_hand_medium_skin_tone:)" + U1F64B1F3FD200D2642 = "🙋🏽‍♂", "🙋🏽‍♂ (:man_raising_hand_medium_skin_tone:)" + U1F468200D1F9B0 = "👨‍🦰", "👨‍🦰 (:man_red_hair:)" + U1F6A3200D2642FE0F = "🚣‍♂️", "🚣‍♂️ (:man_rowing_boat:)" + U1F6A3200D2642 = "🚣‍♂", "🚣‍♂ (:man_rowing_boat:)" + U1F6A31F3FF200D2642FE0F = "🚣🏿‍♂️", "🚣🏿‍♂️ (:man_rowing_boat_dark_skin_tone:)" + U1F6A31F3FF200D2642 = "🚣🏿‍♂", "🚣🏿‍♂ (:man_rowing_boat_dark_skin_tone:)" + U1F6A31F3FB200D2642FE0F = "🚣🏻‍♂️", "🚣🏻‍♂️ (:man_rowing_boat_light_skin_tone:)" + U1F6A31F3FB200D2642 = "🚣🏻‍♂", "🚣🏻‍♂ (:man_rowing_boat_light_skin_tone:)" + U1F6A31F3FE200D2642FE0F = "🚣🏾‍♂️", "🚣🏾‍♂️ (:man_rowing_boat_medium-dark_skin_tone:)" + U1F6A31F3FE200D2642 = "🚣🏾‍♂", "🚣🏾‍♂ (:man_rowing_boat_medium-dark_skin_tone:)" + U1F6A31F3FC200D2642FE0F = "🚣🏼‍♂️", "🚣🏼‍♂️ (:man_rowing_boat_medium-light_skin_tone:)" + U1F6A31F3FC200D2642 = "🚣🏼‍♂", "🚣🏼‍♂ (:man_rowing_boat_medium-light_skin_tone:)" + U1F6A31F3FD200D2642FE0F = "🚣🏽‍♂️", "🚣🏽‍♂️ (:man_rowing_boat_medium_skin_tone:)" + U1F6A31F3FD200D2642 = "🚣🏽‍♂", "🚣🏽‍♂ (:man_rowing_boat_medium_skin_tone:)" + U1F3C3200D2642FE0F = "🏃‍♂️", "🏃‍♂️ (:man_running:)" + U1F3C3200D2642 = "🏃‍♂", "🏃‍♂ (:man_running:)" + U1F3C31F3FF200D2642FE0F = "🏃🏿‍♂️", "🏃🏿‍♂️ (:man_running_dark_skin_tone:)" + U1F3C31F3FF200D2642 = "🏃🏿‍♂", "🏃🏿‍♂ (:man_running_dark_skin_tone:)" + U1F3C3200D2642FE0F200D27A1FE0F = "🏃‍♂️‍➡️", "🏃‍♂️‍➡️ (:man_running_facing_right:)" + U1F3C3200D2642200D27A1FE0F = "🏃‍♂‍➡️", "🏃‍♂‍➡️ (:man_running_facing_right:)" + U1F3C3200D2642FE0F200D27A1 = "🏃‍♂️‍➡", "🏃‍♂️‍➡ (:man_running_facing_right:)" + U1F3C3200D2642200D27A1 = "🏃‍♂‍➡", "🏃‍♂‍➡ (:man_running_facing_right:)" + U1F3C31F3FF200D2642FE0F200D27A1FE0F = "🏃🏿‍♂️‍➡️", "🏃🏿‍♂️‍➡️ (:man_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2642200D27A1FE0F = "🏃🏿‍♂‍➡️", "🏃🏿‍♂‍➡️ (:man_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2642FE0F200D27A1 = "🏃🏿‍♂️‍➡", "🏃🏿‍♂️‍➡ (:man_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2642200D27A1 = "🏃🏿‍♂‍➡", "🏃🏿‍♂‍➡ (:man_running_facing_right_dark_skin_tone:)" + U1F3C31F3FB200D2642FE0F200D27A1FE0F = "🏃🏻‍♂️‍➡️", "🏃🏻‍♂️‍➡️ (:man_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2642200D27A1FE0F = "🏃🏻‍♂‍➡️", "🏃🏻‍♂‍➡️ (:man_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2642FE0F200D27A1 = "🏃🏻‍♂️‍➡", "🏃🏻‍♂️‍➡ (:man_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2642200D27A1 = "🏃🏻‍♂‍➡", "🏃🏻‍♂‍➡ (:man_running_facing_right_light_skin_tone:)" + U1F3C31F3FE200D2642FE0F200D27A1FE0F = "🏃🏾‍♂️‍➡️", "🏃🏾‍♂️‍➡️ (:man_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2642200D27A1FE0F = "🏃🏾‍♂‍➡️", "🏃🏾‍♂‍➡️ (:man_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2642FE0F200D27A1 = "🏃🏾‍♂️‍➡", "🏃🏾‍♂️‍➡ (:man_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2642200D27A1 = "🏃🏾‍♂‍➡", "🏃🏾‍♂‍➡ (:man_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FC200D2642FE0F200D27A1FE0F = "🏃🏼‍♂️‍➡️", "🏃🏼‍♂️‍➡️ (:man_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2642200D27A1FE0F = "🏃🏼‍♂‍➡️", "🏃🏼‍♂‍➡️ (:man_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2642FE0F200D27A1 = "🏃🏼‍♂️‍➡", "🏃🏼‍♂️‍➡ (:man_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2642200D27A1 = "🏃🏼‍♂‍➡", "🏃🏼‍♂‍➡ (:man_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FD200D2642FE0F200D27A1FE0F = "🏃🏽‍♂️‍➡️", "🏃🏽‍♂️‍➡️ (:man_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2642200D27A1FE0F = "🏃🏽‍♂‍➡️", "🏃🏽‍♂‍➡️ (:man_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2642FE0F200D27A1 = "🏃🏽‍♂️‍➡", "🏃🏽‍♂️‍➡ (:man_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2642200D27A1 = "🏃🏽‍♂‍➡", "🏃🏽‍♂‍➡ (:man_running_facing_right_medium_skin_tone:)" + U1F3C31F3FB200D2642FE0F = "🏃🏻‍♂️", "🏃🏻‍♂️ (:man_running_light_skin_tone:)" + U1F3C31F3FB200D2642 = "🏃🏻‍♂", "🏃🏻‍♂ (:man_running_light_skin_tone:)" + U1F3C31F3FE200D2642FE0F = "🏃🏾‍♂️", "🏃🏾‍♂️ (:man_running_medium-dark_skin_tone:)" + U1F3C31F3FE200D2642 = "🏃🏾‍♂", "🏃🏾‍♂ (:man_running_medium-dark_skin_tone:)" + U1F3C31F3FC200D2642FE0F = "🏃🏼‍♂️", "🏃🏼‍♂️ (:man_running_medium-light_skin_tone:)" + U1F3C31F3FC200D2642 = "🏃🏼‍♂", "🏃🏼‍♂ (:man_running_medium-light_skin_tone:)" + U1F3C31F3FD200D2642FE0F = "🏃🏽‍♂️", "🏃🏽‍♂️ (:man_running_medium_skin_tone:)" + U1F3C31F3FD200D2642 = "🏃🏽‍♂", "🏃🏽‍♂ (:man_running_medium_skin_tone:)" + U1F468200D1F52C = "👨‍🔬", "👨‍🔬 (:man_scientist:)" + U1F4681F3FF200D1F52C = "👨🏿‍🔬", "👨🏿‍🔬 (:man_scientist_dark_skin_tone:)" + U1F4681F3FB200D1F52C = "👨🏻‍🔬", "👨🏻‍🔬 (:man_scientist_light_skin_tone:)" + U1F4681F3FE200D1F52C = "👨🏾‍🔬", "👨🏾‍🔬 (:man_scientist_medium-dark_skin_tone:)" + U1F4681F3FC200D1F52C = "👨🏼‍🔬", "👨🏼‍🔬 (:man_scientist_medium-light_skin_tone:)" + U1F4681F3FD200D1F52C = "👨🏽‍🔬", "👨🏽‍🔬 (:man_scientist_medium_skin_tone:)" + U1F937200D2642FE0F = "🤷‍♂️", "🤷‍♂️ (:man_shrugging:)" + U1F937200D2642 = "🤷‍♂", "🤷‍♂ (:man_shrugging:)" + U1F9371F3FF200D2642FE0F = "🤷🏿‍♂️", "🤷🏿‍♂️ (:man_shrugging_dark_skin_tone:)" + U1F9371F3FF200D2642 = "🤷🏿‍♂", "🤷🏿‍♂ (:man_shrugging_dark_skin_tone:)" + U1F9371F3FB200D2642FE0F = "🤷🏻‍♂️", "🤷🏻‍♂️ (:man_shrugging_light_skin_tone:)" + U1F9371F3FB200D2642 = "🤷🏻‍♂", "🤷🏻‍♂ (:man_shrugging_light_skin_tone:)" + U1F9371F3FE200D2642FE0F = "🤷🏾‍♂️", "🤷🏾‍♂️ (:man_shrugging_medium-dark_skin_tone:)" + U1F9371F3FE200D2642 = "🤷🏾‍♂", "🤷🏾‍♂ (:man_shrugging_medium-dark_skin_tone:)" + U1F9371F3FC200D2642FE0F = "🤷🏼‍♂️", "🤷🏼‍♂️ (:man_shrugging_medium-light_skin_tone:)" + U1F9371F3FC200D2642 = "🤷🏼‍♂", "🤷🏼‍♂ (:man_shrugging_medium-light_skin_tone:)" + U1F9371F3FD200D2642FE0F = "🤷🏽‍♂️", "🤷🏽‍♂️ (:man_shrugging_medium_skin_tone:)" + U1F9371F3FD200D2642 = "🤷🏽‍♂", "🤷🏽‍♂ (:man_shrugging_medium_skin_tone:)" + U1F468200D1F3A4 = "👨‍🎤", "👨‍🎤 (:man_singer:)" + U1F4681F3FF200D1F3A4 = "👨🏿‍🎤", "👨🏿‍🎤 (:man_singer_dark_skin_tone:)" + U1F4681F3FB200D1F3A4 = "👨🏻‍🎤", "👨🏻‍🎤 (:man_singer_light_skin_tone:)" + U1F4681F3FE200D1F3A4 = "👨🏾‍🎤", "👨🏾‍🎤 (:man_singer_medium-dark_skin_tone:)" + U1F4681F3FC200D1F3A4 = "👨🏼‍🎤", "👨🏼‍🎤 (:man_singer_medium-light_skin_tone:)" + U1F4681F3FD200D1F3A4 = "👨🏽‍🎤", "👨🏽‍🎤 (:man_singer_medium_skin_tone:)" + U1F9CD200D2642FE0F = "🧍‍♂️", "🧍‍♂️ (:man_standing:)" + U1F9CD200D2642 = "🧍‍♂", "🧍‍♂ (:man_standing:)" + U1F9CD1F3FF200D2642FE0F = "🧍🏿‍♂️", "🧍🏿‍♂️ (:man_standing_dark_skin_tone:)" + U1F9CD1F3FF200D2642 = "🧍🏿‍♂", "🧍🏿‍♂ (:man_standing_dark_skin_tone:)" + U1F9CD1F3FB200D2642FE0F = "🧍🏻‍♂️", "🧍🏻‍♂️ (:man_standing_light_skin_tone:)" + U1F9CD1F3FB200D2642 = "🧍🏻‍♂", "🧍🏻‍♂ (:man_standing_light_skin_tone:)" + U1F9CD1F3FE200D2642FE0F = "🧍🏾‍♂️", "🧍🏾‍♂️ (:man_standing_medium-dark_skin_tone:)" + U1F9CD1F3FE200D2642 = "🧍🏾‍♂", "🧍🏾‍♂ (:man_standing_medium-dark_skin_tone:)" + U1F9CD1F3FC200D2642FE0F = "🧍🏼‍♂️", "🧍🏼‍♂️ (:man_standing_medium-light_skin_tone:)" + U1F9CD1F3FC200D2642 = "🧍🏼‍♂", "🧍🏼‍♂ (:man_standing_medium-light_skin_tone:)" + U1F9CD1F3FD200D2642FE0F = "🧍🏽‍♂️", "🧍🏽‍♂️ (:man_standing_medium_skin_tone:)" + U1F9CD1F3FD200D2642 = "🧍🏽‍♂", "🧍🏽‍♂ (:man_standing_medium_skin_tone:)" + U1F468200D1F393 = "👨‍🎓", "👨‍🎓 (:man_student:)" + U1F4681F3FF200D1F393 = "👨🏿‍🎓", "👨🏿‍🎓 (:man_student_dark_skin_tone:)" + U1F4681F3FB200D1F393 = "👨🏻‍🎓", "👨🏻‍🎓 (:man_student_light_skin_tone:)" + U1F4681F3FE200D1F393 = "👨🏾‍🎓", "👨🏾‍🎓 (:man_student_medium-dark_skin_tone:)" + U1F4681F3FC200D1F393 = "👨🏼‍🎓", "👨🏼‍🎓 (:man_student_medium-light_skin_tone:)" + U1F4681F3FD200D1F393 = "👨🏽‍🎓", "👨🏽‍🎓 (:man_student_medium_skin_tone:)" + U1F9B8200D2642FE0F = "🦸‍♂️", "🦸‍♂️ (:man_superhero:)" + U1F9B8200D2642 = "🦸‍♂", "🦸‍♂ (:man_superhero:)" + U1F9B81F3FF200D2642FE0F = "🦸🏿‍♂️", "🦸🏿‍♂️ (:man_superhero_dark_skin_tone:)" + U1F9B81F3FF200D2642 = "🦸🏿‍♂", "🦸🏿‍♂ (:man_superhero_dark_skin_tone:)" + U1F9B81F3FB200D2642FE0F = "🦸🏻‍♂️", "🦸🏻‍♂️ (:man_superhero_light_skin_tone:)" + U1F9B81F3FB200D2642 = "🦸🏻‍♂", "🦸🏻‍♂ (:man_superhero_light_skin_tone:)" + U1F9B81F3FE200D2642FE0F = "🦸🏾‍♂️", "🦸🏾‍♂️ (:man_superhero_medium-dark_skin_tone:)" + U1F9B81F3FE200D2642 = "🦸🏾‍♂", "🦸🏾‍♂ (:man_superhero_medium-dark_skin_tone:)" + U1F9B81F3FC200D2642FE0F = "🦸🏼‍♂️", "🦸🏼‍♂️ (:man_superhero_medium-light_skin_tone:)" + U1F9B81F3FC200D2642 = "🦸🏼‍♂", "🦸🏼‍♂ (:man_superhero_medium-light_skin_tone:)" + U1F9B81F3FD200D2642FE0F = "🦸🏽‍♂️", "🦸🏽‍♂️ (:man_superhero_medium_skin_tone:)" + U1F9B81F3FD200D2642 = "🦸🏽‍♂", "🦸🏽‍♂ (:man_superhero_medium_skin_tone:)" + U1F9B9200D2642FE0F = "🦹‍♂️", "🦹‍♂️ (:man_supervillain:)" + U1F9B9200D2642 = "🦹‍♂", "🦹‍♂ (:man_supervillain:)" + U1F9B91F3FF200D2642FE0F = "🦹🏿‍♂️", "🦹🏿‍♂️ (:man_supervillain_dark_skin_tone:)" + U1F9B91F3FF200D2642 = "🦹🏿‍♂", "🦹🏿‍♂ (:man_supervillain_dark_skin_tone:)" + U1F9B91F3FB200D2642FE0F = "🦹🏻‍♂️", "🦹🏻‍♂️ (:man_supervillain_light_skin_tone:)" + U1F9B91F3FB200D2642 = "🦹🏻‍♂", "🦹🏻‍♂ (:man_supervillain_light_skin_tone:)" + U1F9B91F3FE200D2642FE0F = "🦹🏾‍♂️", "🦹🏾‍♂️ (:man_supervillain_medium-dark_skin_tone:)" + U1F9B91F3FE200D2642 = "🦹🏾‍♂", "🦹🏾‍♂ (:man_supervillain_medium-dark_skin_tone:)" + U1F9B91F3FC200D2642FE0F = "🦹🏼‍♂️", "🦹🏼‍♂️ (:man_supervillain_medium-light_skin_tone:)" + U1F9B91F3FC200D2642 = "🦹🏼‍♂", "🦹🏼‍♂ (:man_supervillain_medium-light_skin_tone:)" + U1F9B91F3FD200D2642FE0F = "🦹🏽‍♂️", "🦹🏽‍♂️ (:man_supervillain_medium_skin_tone:)" + U1F9B91F3FD200D2642 = "🦹🏽‍♂", "🦹🏽‍♂ (:man_supervillain_medium_skin_tone:)" + U1F3C4200D2642FE0F = "🏄‍♂️", "🏄‍♂️ (:man_surfing:)" + U1F3C4200D2642 = "🏄‍♂", "🏄‍♂ (:man_surfing:)" + U1F3C41F3FF200D2642FE0F = "🏄🏿‍♂️", "🏄🏿‍♂️ (:man_surfing_dark_skin_tone:)" + U1F3C41F3FF200D2642 = "🏄🏿‍♂", "🏄🏿‍♂ (:man_surfing_dark_skin_tone:)" + U1F3C41F3FB200D2642FE0F = "🏄🏻‍♂️", "🏄🏻‍♂️ (:man_surfing_light_skin_tone:)" + U1F3C41F3FB200D2642 = "🏄🏻‍♂", "🏄🏻‍♂ (:man_surfing_light_skin_tone:)" + U1F3C41F3FE200D2642FE0F = "🏄🏾‍♂️", "🏄🏾‍♂️ (:man_surfing_medium-dark_skin_tone:)" + U1F3C41F3FE200D2642 = "🏄🏾‍♂", "🏄🏾‍♂ (:man_surfing_medium-dark_skin_tone:)" + U1F3C41F3FC200D2642FE0F = "🏄🏼‍♂️", "🏄🏼‍♂️ (:man_surfing_medium-light_skin_tone:)" + U1F3C41F3FC200D2642 = "🏄🏼‍♂", "🏄🏼‍♂ (:man_surfing_medium-light_skin_tone:)" + U1F3C41F3FD200D2642FE0F = "🏄🏽‍♂️", "🏄🏽‍♂️ (:man_surfing_medium_skin_tone:)" + U1F3C41F3FD200D2642 = "🏄🏽‍♂", "🏄🏽‍♂ (:man_surfing_medium_skin_tone:)" + U1F3CA200D2642FE0F = "🏊‍♂️", "🏊‍♂️ (:man_swimming:)" + U1F3CA200D2642 = "🏊‍♂", "🏊‍♂ (:man_swimming:)" + U1F3CA1F3FF200D2642FE0F = "🏊🏿‍♂️", "🏊🏿‍♂️ (:man_swimming_dark_skin_tone:)" + U1F3CA1F3FF200D2642 = "🏊🏿‍♂", "🏊🏿‍♂ (:man_swimming_dark_skin_tone:)" + U1F3CA1F3FB200D2642FE0F = "🏊🏻‍♂️", "🏊🏻‍♂️ (:man_swimming_light_skin_tone:)" + U1F3CA1F3FB200D2642 = "🏊🏻‍♂", "🏊🏻‍♂ (:man_swimming_light_skin_tone:)" + U1F3CA1F3FE200D2642FE0F = "🏊🏾‍♂️", "🏊🏾‍♂️ (:man_swimming_medium-dark_skin_tone:)" + U1F3CA1F3FE200D2642 = "🏊🏾‍♂", "🏊🏾‍♂ (:man_swimming_medium-dark_skin_tone:)" + U1F3CA1F3FC200D2642FE0F = "🏊🏼‍♂️", "🏊🏼‍♂️ (:man_swimming_medium-light_skin_tone:)" + U1F3CA1F3FC200D2642 = "🏊🏼‍♂", "🏊🏼‍♂ (:man_swimming_medium-light_skin_tone:)" + U1F3CA1F3FD200D2642FE0F = "🏊🏽‍♂️", "🏊🏽‍♂️ (:man_swimming_medium_skin_tone:)" + U1F3CA1F3FD200D2642 = "🏊🏽‍♂", "🏊🏽‍♂ (:man_swimming_medium_skin_tone:)" + U1F468200D1F3EB = "👨‍🏫", "👨‍🏫 (:man_teacher:)" + U1F4681F3FF200D1F3EB = "👨🏿‍🏫", "👨🏿‍🏫 (:man_teacher_dark_skin_tone:)" + U1F4681F3FB200D1F3EB = "👨🏻‍🏫", "👨🏻‍🏫 (:man_teacher_light_skin_tone:)" + U1F4681F3FE200D1F3EB = "👨🏾‍🏫", "👨🏾‍🏫 (:man_teacher_medium-dark_skin_tone:)" + U1F4681F3FC200D1F3EB = "👨🏼‍🏫", "👨🏼‍🏫 (:man_teacher_medium-light_skin_tone:)" + U1F4681F3FD200D1F3EB = "👨🏽‍🏫", "👨🏽‍🏫 (:man_teacher_medium_skin_tone:)" + U1F468200D1F4BB = "👨‍💻", "👨‍💻 (:man_technologist:)" + U1F4681F3FF200D1F4BB = "👨🏿‍💻", "👨🏿‍💻 (:man_technologist_dark_skin_tone:)" + U1F4681F3FB200D1F4BB = "👨🏻‍💻", "👨🏻‍💻 (:man_technologist_light_skin_tone:)" + U1F4681F3FE200D1F4BB = "👨🏾‍💻", "👨🏾‍💻 (:man_technologist_medium-dark_skin_tone:)" + U1F4681F3FC200D1F4BB = "👨🏼‍💻", "👨🏼‍💻 (:man_technologist_medium-light_skin_tone:)" + U1F4681F3FD200D1F4BB = "👨🏽‍💻", "👨🏽‍💻 (:man_technologist_medium_skin_tone:)" + U1F481200D2642FE0F = "💁‍♂️", "💁‍♂️ (:man_tipping_hand:)" + U1F481200D2642 = "💁‍♂", "💁‍♂ (:man_tipping_hand:)" + U1F4811F3FF200D2642FE0F = "💁🏿‍♂️", "💁🏿‍♂️ (:man_tipping_hand_dark_skin_tone:)" + U1F4811F3FF200D2642 = "💁🏿‍♂", "💁🏿‍♂ (:man_tipping_hand_dark_skin_tone:)" + U1F4811F3FB200D2642FE0F = "💁🏻‍♂️", "💁🏻‍♂️ (:man_tipping_hand_light_skin_tone:)" + U1F4811F3FB200D2642 = "💁🏻‍♂", "💁🏻‍♂ (:man_tipping_hand_light_skin_tone:)" + U1F4811F3FE200D2642FE0F = "💁🏾‍♂️", "💁🏾‍♂️ (:man_tipping_hand_medium-dark_skin_tone:)" + U1F4811F3FE200D2642 = "💁🏾‍♂", "💁🏾‍♂ (:man_tipping_hand_medium-dark_skin_tone:)" + U1F4811F3FC200D2642FE0F = "💁🏼‍♂️", "💁🏼‍♂️ (:man_tipping_hand_medium-light_skin_tone:)" + U1F4811F3FC200D2642 = "💁🏼‍♂", "💁🏼‍♂ (:man_tipping_hand_medium-light_skin_tone:)" + U1F4811F3FD200D2642FE0F = "💁🏽‍♂️", "💁🏽‍♂️ (:man_tipping_hand_medium_skin_tone:)" + U1F4811F3FD200D2642 = "💁🏽‍♂", "💁🏽‍♂ (:man_tipping_hand_medium_skin_tone:)" + U1F9DB200D2642FE0F = "🧛‍♂️", "🧛‍♂️ (:man_vampire:)" + U1F9DB200D2642 = "🧛‍♂", "🧛‍♂ (:man_vampire:)" + U1F9DB1F3FF200D2642FE0F = "🧛🏿‍♂️", "🧛🏿‍♂️ (:man_vampire_dark_skin_tone:)" + U1F9DB1F3FF200D2642 = "🧛🏿‍♂", "🧛🏿‍♂ (:man_vampire_dark_skin_tone:)" + U1F9DB1F3FB200D2642FE0F = "🧛🏻‍♂️", "🧛🏻‍♂️ (:man_vampire_light_skin_tone:)" + U1F9DB1F3FB200D2642 = "🧛🏻‍♂", "🧛🏻‍♂ (:man_vampire_light_skin_tone:)" + U1F9DB1F3FE200D2642FE0F = "🧛🏾‍♂️", "🧛🏾‍♂️ (:man_vampire_medium-dark_skin_tone:)" + U1F9DB1F3FE200D2642 = "🧛🏾‍♂", "🧛🏾‍♂ (:man_vampire_medium-dark_skin_tone:)" + U1F9DB1F3FC200D2642FE0F = "🧛🏼‍♂️", "🧛🏼‍♂️ (:man_vampire_medium-light_skin_tone:)" + U1F9DB1F3FC200D2642 = "🧛🏼‍♂", "🧛🏼‍♂ (:man_vampire_medium-light_skin_tone:)" + U1F9DB1F3FD200D2642FE0F = "🧛🏽‍♂️", "🧛🏽‍♂️ (:man_vampire_medium_skin_tone:)" + U1F9DB1F3FD200D2642 = "🧛🏽‍♂", "🧛🏽‍♂ (:man_vampire_medium_skin_tone:)" + U1F6B6200D2642FE0F = "🚶‍♂️", "🚶‍♂️ (:man_walking:)" + U1F6B6200D2642 = "🚶‍♂", "🚶‍♂ (:man_walking:)" + U1F6B61F3FF200D2642FE0F = "🚶🏿‍♂️", "🚶🏿‍♂️ (:man_walking_dark_skin_tone:)" + U1F6B61F3FF200D2642 = "🚶🏿‍♂", "🚶🏿‍♂ (:man_walking_dark_skin_tone:)" + U1F6B6200D2642FE0F200D27A1FE0F = "🚶‍♂️‍➡️", "🚶‍♂️‍➡️ (:man_walking_facing_right:)" + U1F6B6200D2642200D27A1FE0F = "🚶‍♂‍➡️", "🚶‍♂‍➡️ (:man_walking_facing_right:)" + U1F6B6200D2642FE0F200D27A1 = "🚶‍♂️‍➡", "🚶‍♂️‍➡ (:man_walking_facing_right:)" + U1F6B6200D2642200D27A1 = "🚶‍♂‍➡", "🚶‍♂‍➡ (:man_walking_facing_right:)" + U1F6B61F3FF200D2642FE0F200D27A1FE0F = "🚶🏿‍♂️‍➡️", "🚶🏿‍♂️‍➡️ (:man_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2642200D27A1FE0F = "🚶🏿‍♂‍➡️", "🚶🏿‍♂‍➡️ (:man_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2642FE0F200D27A1 = "🚶🏿‍♂️‍➡", "🚶🏿‍♂️‍➡ (:man_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2642200D27A1 = "🚶🏿‍♂‍➡", "🚶🏿‍♂‍➡ (:man_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FB200D2642FE0F200D27A1FE0F = "🚶🏻‍♂️‍➡️", "🚶🏻‍♂️‍➡️ (:man_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2642200D27A1FE0F = "🚶🏻‍♂‍➡️", "🚶🏻‍♂‍➡️ (:man_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2642FE0F200D27A1 = "🚶🏻‍♂️‍➡", "🚶🏻‍♂️‍➡ (:man_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2642200D27A1 = "🚶🏻‍♂‍➡", "🚶🏻‍♂‍➡ (:man_walking_facing_right_light_skin_tone:)" + U1F6B61F3FE200D2642FE0F200D27A1FE0F = "🚶🏾‍♂️‍➡️", "🚶🏾‍♂️‍➡️ (:man_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2642200D27A1FE0F = "🚶🏾‍♂‍➡️", "🚶🏾‍♂‍➡️ (:man_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2642FE0F200D27A1 = "🚶🏾‍♂️‍➡", "🚶🏾‍♂️‍➡ (:man_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2642200D27A1 = "🚶🏾‍♂‍➡", "🚶🏾‍♂‍➡ (:man_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FC200D2642FE0F200D27A1FE0F = "🚶🏼‍♂️‍➡️", "🚶🏼‍♂️‍➡️ (:man_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2642200D27A1FE0F = "🚶🏼‍♂‍➡️", "🚶🏼‍♂‍➡️ (:man_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2642FE0F200D27A1 = "🚶🏼‍♂️‍➡", "🚶🏼‍♂️‍➡ (:man_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2642200D27A1 = "🚶🏼‍♂‍➡", "🚶🏼‍♂‍➡ (:man_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FD200D2642FE0F200D27A1FE0F = "🚶🏽‍♂️‍➡️", "🚶🏽‍♂️‍➡️ (:man_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2642200D27A1FE0F = "🚶🏽‍♂‍➡️", "🚶🏽‍♂‍➡️ (:man_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2642FE0F200D27A1 = "🚶🏽‍♂️‍➡", "🚶🏽‍♂️‍➡ (:man_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2642200D27A1 = "🚶🏽‍♂‍➡", "🚶🏽‍♂‍➡ (:man_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FB200D2642FE0F = "🚶🏻‍♂️", "🚶🏻‍♂️ (:man_walking_light_skin_tone:)" + U1F6B61F3FB200D2642 = "🚶🏻‍♂", "🚶🏻‍♂ (:man_walking_light_skin_tone:)" + U1F6B61F3FE200D2642FE0F = "🚶🏾‍♂️", "🚶🏾‍♂️ (:man_walking_medium-dark_skin_tone:)" + U1F6B61F3FE200D2642 = "🚶🏾‍♂", "🚶🏾‍♂ (:man_walking_medium-dark_skin_tone:)" + U1F6B61F3FC200D2642FE0F = "🚶🏼‍♂️", "🚶🏼‍♂️ (:man_walking_medium-light_skin_tone:)" + U1F6B61F3FC200D2642 = "🚶🏼‍♂", "🚶🏼‍♂ (:man_walking_medium-light_skin_tone:)" + U1F6B61F3FD200D2642FE0F = "🚶🏽‍♂️", "🚶🏽‍♂️ (:man_walking_medium_skin_tone:)" + U1F6B61F3FD200D2642 = "🚶🏽‍♂", "🚶🏽‍♂ (:man_walking_medium_skin_tone:)" + U1F473200D2642FE0F = "👳‍♂️", "👳‍♂️ (:man_wearing_turban:)" + U1F473200D2642 = "👳‍♂", "👳‍♂ (:man_wearing_turban:)" + U1F4731F3FF200D2642FE0F = "👳🏿‍♂️", "👳🏿‍♂️ (:man_wearing_turban_dark_skin_tone:)" + U1F4731F3FF200D2642 = "👳🏿‍♂", "👳🏿‍♂ (:man_wearing_turban_dark_skin_tone:)" + U1F4731F3FB200D2642FE0F = "👳🏻‍♂️", "👳🏻‍♂️ (:man_wearing_turban_light_skin_tone:)" + U1F4731F3FB200D2642 = "👳🏻‍♂", "👳🏻‍♂ (:man_wearing_turban_light_skin_tone:)" + U1F4731F3FE200D2642FE0F = "👳🏾‍♂️", "👳🏾‍♂️ (:man_wearing_turban_medium-dark_skin_tone:)" + U1F4731F3FE200D2642 = "👳🏾‍♂", "👳🏾‍♂ (:man_wearing_turban_medium-dark_skin_tone:)" + U1F4731F3FC200D2642FE0F = "👳🏼‍♂️", "👳🏼‍♂️ (:man_wearing_turban_medium-light_skin_tone:)" + U1F4731F3FC200D2642 = "👳🏼‍♂", "👳🏼‍♂ (:man_wearing_turban_medium-light_skin_tone:)" + U1F4731F3FD200D2642FE0F = "👳🏽‍♂️", "👳🏽‍♂️ (:man_wearing_turban_medium_skin_tone:)" + U1F4731F3FD200D2642 = "👳🏽‍♂", "👳🏽‍♂ (:man_wearing_turban_medium_skin_tone:)" + U1F468200D1F9B3 = "👨‍🦳", "👨‍🦳 (:man_white_hair:)" + U1F470200D2642FE0F = "👰‍♂️", "👰‍♂️ (:man_with_veil:)" + U1F470200D2642 = "👰‍♂", "👰‍♂ (:man_with_veil:)" + U1F4701F3FF200D2642FE0F = "👰🏿‍♂️", "👰🏿‍♂️ (:man_with_veil_dark_skin_tone:)" + U1F4701F3FF200D2642 = "👰🏿‍♂", "👰🏿‍♂ (:man_with_veil_dark_skin_tone:)" + U1F4701F3FB200D2642FE0F = "👰🏻‍♂️", "👰🏻‍♂️ (:man_with_veil_light_skin_tone:)" + U1F4701F3FB200D2642 = "👰🏻‍♂", "👰🏻‍♂ (:man_with_veil_light_skin_tone:)" + U1F4701F3FE200D2642FE0F = "👰🏾‍♂️", "👰🏾‍♂️ (:man_with_veil_medium-dark_skin_tone:)" + U1F4701F3FE200D2642 = "👰🏾‍♂", "👰🏾‍♂ (:man_with_veil_medium-dark_skin_tone:)" + U1F4701F3FC200D2642FE0F = "👰🏼‍♂️", "👰🏼‍♂️ (:man_with_veil_medium-light_skin_tone:)" + U1F4701F3FC200D2642 = "👰🏼‍♂", "👰🏼‍♂ (:man_with_veil_medium-light_skin_tone:)" + U1F4701F3FD200D2642FE0F = "👰🏽‍♂️", "👰🏽‍♂️ (:man_with_veil_medium_skin_tone:)" + U1F4701F3FD200D2642 = "👰🏽‍♂", "👰🏽‍♂ (:man_with_veil_medium_skin_tone:)" + U1F468200D1F9AF = "👨‍🦯", "👨‍🦯 (:man_with_white_cane:)" + U1F4681F3FF200D1F9AF = "👨🏿‍🦯", "👨🏿‍🦯 (:man_with_white_cane_dark_skin_tone:)" + U1F468200D1F9AF200D27A1FE0F = "👨‍🦯‍➡️", "👨‍🦯‍➡️ (:man_with_white_cane_facing_right:)" + U1F468200D1F9AF200D27A1 = "👨‍🦯‍➡", "👨‍🦯‍➡ (:man_with_white_cane_facing_right:)" + U1F4681F3FF200D1F9AF200D27A1FE0F = "👨🏿‍🦯‍➡️", "👨🏿‍🦯‍➡️ (:man_with_white_cane_facing_right_dark_skin_tone:)" + U1F4681F3FF200D1F9AF200D27A1 = "👨🏿‍🦯‍➡", "👨🏿‍🦯‍➡ (:man_with_white_cane_facing_right_dark_skin_tone:)" + U1F4681F3FB200D1F9AF200D27A1FE0F = "👨🏻‍🦯‍➡️", "👨🏻‍🦯‍➡️ (:man_with_white_cane_facing_right_light_skin_tone:)" + U1F4681F3FB200D1F9AF200D27A1 = "👨🏻‍🦯‍➡", "👨🏻‍🦯‍➡ (:man_with_white_cane_facing_right_light_skin_tone:)" + U1F4681F3FE200D1F9AF200D27A1FE0F = "👨🏾‍🦯‍➡️", "👨🏾‍🦯‍➡️ (:man_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F4681F3FE200D1F9AF200D27A1 = "👨🏾‍🦯‍➡", "👨🏾‍🦯‍➡ (:man_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9AF200D27A1FE0F = "👨🏼‍🦯‍➡️", "👨🏼‍🦯‍➡️ (:man_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F4681F3FC200D1F9AF200D27A1 = "👨🏼‍🦯‍➡", "👨🏼‍🦯‍➡ (:man_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F4681F3FD200D1F9AF200D27A1FE0F = "👨🏽‍🦯‍➡️", "👨🏽‍🦯‍➡️ (:man_with_white_cane_facing_right_medium_skin_tone:)" + U1F4681F3FD200D1F9AF200D27A1 = "👨🏽‍🦯‍➡", "👨🏽‍🦯‍➡ (:man_with_white_cane_facing_right_medium_skin_tone:)" + U1F4681F3FB200D1F9AF = "👨🏻‍🦯", "👨🏻‍🦯 (:man_with_white_cane_light_skin_tone:)" + U1F4681F3FE200D1F9AF = "👨🏾‍🦯", "👨🏾‍🦯 (:man_with_white_cane_medium-dark_skin_tone:)" + U1F4681F3FC200D1F9AF = "👨🏼‍🦯", "👨🏼‍🦯 (:man_with_white_cane_medium-light_skin_tone:)" + U1F4681F3FD200D1F9AF = "👨🏽‍🦯", "👨🏽‍🦯 (:man_with_white_cane_medium_skin_tone:)" + U1F9DF200D2642FE0F = "🧟‍♂️", "🧟‍♂️ (:man_zombie:)" + U1F9DF200D2642 = "🧟‍♂", "🧟‍♂ (:man_zombie:)" + U1F96D = "🥭", "🥭 (:mango:)" + U1F570FE0F = "🕰️", "🕰️ (:mantelpiece_clock:)" + U1F570 = "🕰", "🕰 (:mantelpiece_clock:)" + U1F9BD = "🦽", "🦽 (:manual_wheelchair:)" + U1F45E = "👞", "👞 (:man’s_shoe:)" + U1F5FE = "🗾", "🗾 (:map_of_Japan:)" + U1F341 = "🍁", "🍁 (:maple_leaf:)" + U1FA87 = "🪇", "🪇 (:maracas:)" + U1F94B = "🥋", "🥋 (:martial_arts_uniform:)" + U1F9C9 = "🧉", "🧉 (:mate:)" + U1F356 = "🍖", "🍖 (:meat_on_bone:)" + U1F9D1200D1F527 = "🧑‍🔧", "🧑‍🔧 (:mechanic:)" + U1F9D11F3FF200D1F527 = "🧑🏿‍🔧", "🧑🏿‍🔧 (:mechanic_dark_skin_tone:)" + U1F9D11F3FB200D1F527 = "🧑🏻‍🔧", "🧑🏻‍🔧 (:mechanic_light_skin_tone:)" + U1F9D11F3FE200D1F527 = "🧑🏾‍🔧", "🧑🏾‍🔧 (:mechanic_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F527 = "🧑🏼‍🔧", "🧑🏼‍🔧 (:mechanic_medium-light_skin_tone:)" + U1F9D11F3FD200D1F527 = "🧑🏽‍🔧", "🧑🏽‍🔧 (:mechanic_medium_skin_tone:)" + U1F9BE = "🦾", "🦾 (:mechanical_arm:)" + U1F9BF = "🦿", "🦿 (:mechanical_leg:)" + U2695FE0F = "⚕️", "⚕️ (:medical_symbol:)" + U2695 = "⚕", "⚕ (:medical_symbol:)" + U1F3FE = "🏾", "🏾 (:medium-dark_skin_tone:)" + U1F3FC = "🏼", "🏼 (:medium-light_skin_tone:)" + U1F3FD = "🏽", "🏽 (:medium_skin_tone:)" + U1F4E3 = "📣", "📣 (:megaphone:)" + U1F348 = "🍈", "🍈 (:melon:)" + U1FAE0 = "🫠", "🫠 (:melting_face:)" + U1F4DD = "📝", "📝 (:memo:)" + U1F46C = "👬", "👬 (:men_holding_hands:)" + U1F46C1F3FF = "👬🏿", "👬🏿 (:men_holding_hands_dark_skin_tone:)" + U1F4681F3FF200D1F91D200D1F4681F3FB = "👨🏿‍🤝‍👨🏻", "👨🏿‍🤝‍👨🏻 (:men_holding_hands_dark_skin_tone_light_skin_tone:)" + U1F4681F3FF200D1F91D200D1F4681F3FE = "👨🏿‍🤝‍👨🏾", "👨🏿‍🤝‍👨🏾 (:men_holding_hands_dark_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FF200D1F91D200D1F4681F3FC = "👨🏿‍🤝‍👨🏼", "👨🏿‍🤝‍👨🏼 (:men_holding_hands_dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FF200D1F91D200D1F4681F3FD = "👨🏿‍🤝‍👨🏽", "👨🏿‍🤝‍👨🏽 (:men_holding_hands_dark_skin_tone_medium_skin_tone:)" + U1F46C1F3FB = "👬🏻", "👬🏻 (:men_holding_hands_light_skin_tone:)" + U1F4681F3FB200D1F91D200D1F4681F3FF = "👨🏻‍🤝‍👨🏿", "👨🏻‍🤝‍👨🏿 (:men_holding_hands_light_skin_tone_dark_skin_tone:)" + U1F4681F3FB200D1F91D200D1F4681F3FE = "👨🏻‍🤝‍👨🏾", "👨🏻‍🤝‍👨🏾 (:men_holding_hands_light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FB200D1F91D200D1F4681F3FC = "👨🏻‍🤝‍👨🏼", "👨🏻‍🤝‍👨🏼 (:men_holding_hands_light_skin_tone_medium-light_skin_tone:)" + U1F4681F3FB200D1F91D200D1F4681F3FD = "👨🏻‍🤝‍👨🏽", "👨🏻‍🤝‍👨🏽 (:men_holding_hands_light_skin_tone_medium_skin_tone:)" + U1F46C1F3FE = "👬🏾", "👬🏾 (:men_holding_hands_medium-dark_skin_tone:)" + U1F4681F3FE200D1F91D200D1F4681F3FF = "👨🏾‍🤝‍👨🏿", "👨🏾‍🤝‍👨🏿 (:men_holding_hands_medium-dark_skin_tone_dark_skin_tone:)" + U1F4681F3FE200D1F91D200D1F4681F3FB = "👨🏾‍🤝‍👨🏻", "👨🏾‍🤝‍👨🏻 (:men_holding_hands_medium-dark_skin_tone_light_skin_tone:)" + U1F4681F3FE200D1F91D200D1F4681F3FC = "👨🏾‍🤝‍👨🏼", "👨🏾‍🤝‍👨🏼 (:men_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4681F3FE200D1F91D200D1F4681F3FD = "👨🏾‍🤝‍👨🏽", "👨🏾‍🤝‍👨🏽 (:men_holding_hands_medium-dark_skin_tone_medium_skin_tone:)" + U1F46C1F3FC = "👬🏼", "👬🏼 (:men_holding_hands_medium-light_skin_tone:)" + U1F4681F3FC200D1F91D200D1F4681F3FF = "👨🏼‍🤝‍👨🏿", "👨🏼‍🤝‍👨🏿 (:men_holding_hands_medium-light_skin_tone_dark_skin_tone:)" + U1F4681F3FC200D1F91D200D1F4681F3FB = "👨🏼‍🤝‍👨🏻", "👨🏼‍🤝‍👨🏻 (:men_holding_hands_medium-light_skin_tone_light_skin_tone:)" + U1F4681F3FC200D1F91D200D1F4681F3FE = "👨🏼‍🤝‍👨🏾", "👨🏼‍🤝‍👨🏾 (:men_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FC200D1F91D200D1F4681F3FD = "👨🏼‍🤝‍👨🏽", "👨🏼‍🤝‍👨🏽 (:men_holding_hands_medium-light_skin_tone_medium_skin_tone:)" + U1F46C1F3FD = "👬🏽", "👬🏽 (:men_holding_hands_medium_skin_tone:)" + U1F4681F3FD200D1F91D200D1F4681F3FF = "👨🏽‍🤝‍👨🏿", "👨🏽‍🤝‍👨🏿 (:men_holding_hands_medium_skin_tone_dark_skin_tone:)" + U1F4681F3FD200D1F91D200D1F4681F3FB = "👨🏽‍🤝‍👨🏻", "👨🏽‍🤝‍👨🏻 (:men_holding_hands_medium_skin_tone_light_skin_tone:)" + U1F4681F3FD200D1F91D200D1F4681F3FE = "👨🏽‍🤝‍👨🏾", "👨🏽‍🤝‍👨🏾 (:men_holding_hands_medium_skin_tone_medium-dark_skin_tone:)" + U1F4681F3FD200D1F91D200D1F4681F3FC = "👨🏽‍🤝‍👨🏼", "👨🏽‍🤝‍👨🏼 (:men_holding_hands_medium_skin_tone_medium-light_skin_tone:)" + U1F46F200D2642FE0F = "👯‍♂️", "👯‍♂️ (:men_with_bunny_ears:)" + U1F46F200D2642 = "👯‍♂", "👯‍♂ (:men_with_bunny_ears:)" + U1F93C200D2642FE0F = "🤼‍♂️", "🤼‍♂️ (:men_wrestling:)" + U1F93C200D2642 = "🤼‍♂", "🤼‍♂ (:men_wrestling:)" + U2764FE0F200D1FA79 = "❤️‍🩹", "❤️‍🩹 (:mending_heart:)" + U2764200D1FA79 = "❤‍🩹", "❤‍🩹 (:mending_heart:)" + U1F54E = "🕎", "🕎 (:menorah:)" + U1F6B9 = "🚹", "🚹 (:men’s_room:)" + U1F9DC200D2640FE0F = "🧜‍♀️", "🧜‍♀️ (:mermaid:)" + U1F9DC200D2640 = "🧜‍♀", "🧜‍♀ (:mermaid:)" + U1F9DC1F3FF200D2640FE0F = "🧜🏿‍♀️", "🧜🏿‍♀️ (:mermaid_dark_skin_tone:)" + U1F9DC1F3FF200D2640 = "🧜🏿‍♀", "🧜🏿‍♀ (:mermaid_dark_skin_tone:)" + U1F9DC1F3FB200D2640FE0F = "🧜🏻‍♀️", "🧜🏻‍♀️ (:mermaid_light_skin_tone:)" + U1F9DC1F3FB200D2640 = "🧜🏻‍♀", "🧜🏻‍♀ (:mermaid_light_skin_tone:)" + U1F9DC1F3FE200D2640FE0F = "🧜🏾‍♀️", "🧜🏾‍♀️ (:mermaid_medium-dark_skin_tone:)" + U1F9DC1F3FE200D2640 = "🧜🏾‍♀", "🧜🏾‍♀ (:mermaid_medium-dark_skin_tone:)" + U1F9DC1F3FC200D2640FE0F = "🧜🏼‍♀️", "🧜🏼‍♀️ (:mermaid_medium-light_skin_tone:)" + U1F9DC1F3FC200D2640 = "🧜🏼‍♀", "🧜🏼‍♀ (:mermaid_medium-light_skin_tone:)" + U1F9DC1F3FD200D2640FE0F = "🧜🏽‍♀️", "🧜🏽‍♀️ (:mermaid_medium_skin_tone:)" + U1F9DC1F3FD200D2640 = "🧜🏽‍♀", "🧜🏽‍♀ (:mermaid_medium_skin_tone:)" + U1F9DC200D2642FE0F = "🧜‍♂️", "🧜‍♂️ (:merman:)" + U1F9DC200D2642 = "🧜‍♂", "🧜‍♂ (:merman:)" + U1F9DC1F3FF200D2642FE0F = "🧜🏿‍♂️", "🧜🏿‍♂️ (:merman_dark_skin_tone:)" + U1F9DC1F3FF200D2642 = "🧜🏿‍♂", "🧜🏿‍♂ (:merman_dark_skin_tone:)" + U1F9DC1F3FB200D2642FE0F = "🧜🏻‍♂️", "🧜🏻‍♂️ (:merman_light_skin_tone:)" + U1F9DC1F3FB200D2642 = "🧜🏻‍♂", "🧜🏻‍♂ (:merman_light_skin_tone:)" + U1F9DC1F3FE200D2642FE0F = "🧜🏾‍♂️", "🧜🏾‍♂️ (:merman_medium-dark_skin_tone:)" + U1F9DC1F3FE200D2642 = "🧜🏾‍♂", "🧜🏾‍♂ (:merman_medium-dark_skin_tone:)" + U1F9DC1F3FC200D2642FE0F = "🧜🏼‍♂️", "🧜🏼‍♂️ (:merman_medium-light_skin_tone:)" + U1F9DC1F3FC200D2642 = "🧜🏼‍♂", "🧜🏼‍♂ (:merman_medium-light_skin_tone:)" + U1F9DC1F3FD200D2642FE0F = "🧜🏽‍♂️", "🧜🏽‍♂️ (:merman_medium_skin_tone:)" + U1F9DC1F3FD200D2642 = "🧜🏽‍♂", "🧜🏽‍♂ (:merman_medium_skin_tone:)" + U1F9DC = "🧜", "🧜 (:merperson:)" + U1F9DC1F3FF = "🧜🏿", "🧜🏿 (:merperson_dark_skin_tone:)" + U1F9DC1F3FB = "🧜🏻", "🧜🏻 (:merperson_light_skin_tone:)" + U1F9DC1F3FE = "🧜🏾", "🧜🏾 (:merperson_medium-dark_skin_tone:)" + U1F9DC1F3FC = "🧜🏼", "🧜🏼 (:merperson_medium-light_skin_tone:)" + U1F9DC1F3FD = "🧜🏽", "🧜🏽 (:merperson_medium_skin_tone:)" + U1F687 = "🚇", "🚇 (:metro:)" + U1F9A0 = "🦠", "🦠 (:microbe:)" + U1F3A4 = "🎤", "🎤 (:microphone:)" + U1F52C = "🔬", "🔬 (:microscope:)" + U1F595 = "🖕", "🖕 (:middle_finger:)" + U1F5951F3FF = "🖕🏿", "🖕🏿 (:middle_finger_dark_skin_tone:)" + U1F5951F3FB = "🖕🏻", "🖕🏻 (:middle_finger_light_skin_tone:)" + U1F5951F3FE = "🖕🏾", "🖕🏾 (:middle_finger_medium-dark_skin_tone:)" + U1F5951F3FC = "🖕🏼", "🖕🏼 (:middle_finger_medium-light_skin_tone:)" + U1F5951F3FD = "🖕🏽", "🖕🏽 (:middle_finger_medium_skin_tone:)" + U1FA96 = "🪖", "🪖 (:military_helmet:)" + U1F396FE0F = "🎖️", "🎖️ (:military_medal:)" + U1F396 = "🎖", "🎖 (:military_medal:)" + U1F30C = "🌌", "🌌 (:milky_way:)" + U1F690 = "🚐", "🚐 (:minibus:)" + U2796 = "➖", "➖ (:minus:)" + U1FA9E = "🪞", "🪞 (:mirror:)" + U1FAA9 = "🪩", "🪩 (:mirror_ball:)" + U1F5FF = "🗿", "🗿 (:moai:)" + U1F4F1 = "📱", "📱 (:mobile_phone:)" + U1F4F4 = "📴", "📴 (:mobile_phone_off:)" + U1F4F2 = "📲", "📲 (:mobile_phone_with_arrow:)" + U1F911 = "🤑", "🤑 (:money-mouth_face:)" + U1F4B0 = "💰", "💰 (:money_bag:)" + U1F4B8 = "💸", "💸 (:money_with_wings:)" + U1F412 = "🐒", "🐒 (:monkey:)" + U1F435 = "🐵", "🐵 (:monkey_face:)" + U1F69D = "🚝", "🚝 (:monorail:)" + U1F96E = "🥮", "🥮 (:moon_cake:)" + U1F391 = "🎑", "🎑 (:moon_viewing_ceremony:)" + U1FACE = "🫎", "🫎 (:moose:)" + U1F54C = "🕌", "🕌 (:mosque:)" + U1F99F = "🦟", "🦟 (:mosquito:)" + U1F6E5FE0F = "🛥️", "🛥️ (:motor_boat:)" + U1F6E5 = "🛥", "🛥 (:motor_boat:)" + U1F6F5 = "🛵", "🛵 (:motor_scooter:)" + U1F3CDFE0F = "🏍️", "🏍️ (:motorcycle:)" + U1F3CD = "🏍", "🏍 (:motorcycle:)" + U1F9BC = "🦼", "🦼 (:motorized_wheelchair:)" + U1F6E3FE0F = "🛣️", "🛣️ (:motorway:)" + U1F6E3 = "🛣", "🛣 (:motorway:)" + U1F5FB = "🗻", "🗻 (:mount_fuji:)" + U26F0FE0F = "⛰️", "⛰️ (:mountain:)" + U26F0 = "⛰", "⛰ (:mountain:)" + U1F6A0 = "🚠", "🚠 (:mountain_cableway:)" + U1F69E = "🚞", "🚞 (:mountain_railway:)" + U1F401 = "🐁", "🐁 (:mouse:)" + U1F42D = "🐭", "🐭 (:mouse_face:)" + U1FAA4 = "🪤", "🪤 (:mouse_trap:)" + U1F444 = "👄", "👄 (:mouth:)" + U1F3A5 = "🎥", "🎥 (:movie_camera:)" + U2716FE0F = "✖️", "✖️ (:multiply:)" + U2716 = "✖", "✖ (:multiply:)" + U1F344 = "🍄", "🍄 (:mushroom:)" + U1F3B9 = "🎹", "🎹 (:musical_keyboard:)" + U1F3B5 = "🎵", "🎵 (:musical_note:)" + U1F3B6 = "🎶", "🎶 (:musical_notes:)" + U1F3BC = "🎼", "🎼 (:musical_score:)" + U1F507 = "🔇", "🔇 (:muted_speaker:)" + U1F9D1200D1F384 = "🧑‍🎄", "🧑‍🎄 (:mx_claus:)" + U1F9D11F3FF200D1F384 = "🧑🏿‍🎄", "🧑🏿‍🎄 (:mx_claus_dark_skin_tone:)" + U1F9D11F3FB200D1F384 = "🧑🏻‍🎄", "🧑🏻‍🎄 (:mx_claus_light_skin_tone:)" + U1F9D11F3FE200D1F384 = "🧑🏾‍🎄", "🧑🏾‍🎄 (:mx_claus_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F384 = "🧑🏼‍🎄", "🧑🏼‍🎄 (:mx_claus_medium-light_skin_tone:)" + U1F9D11F3FD200D1F384 = "🧑🏽‍🎄", "🧑🏽‍🎄 (:mx_claus_medium_skin_tone:)" + U1F485 = "💅", "💅 (:nail_polish:)" + U1F4851F3FF = "💅🏿", "💅🏿 (:nail_polish_dark_skin_tone:)" + U1F4851F3FB = "💅🏻", "💅🏻 (:nail_polish_light_skin_tone:)" + U1F4851F3FE = "💅🏾", "💅🏾 (:nail_polish_medium-dark_skin_tone:)" + U1F4851F3FC = "💅🏼", "💅🏼 (:nail_polish_medium-light_skin_tone:)" + U1F4851F3FD = "💅🏽", "💅🏽 (:nail_polish_medium_skin_tone:)" + U1F4DB = "📛", "📛 (:name_badge:)" + U1F3DEFE0F = "🏞️", "🏞️ (:national_park:)" + U1F3DE = "🏞", "🏞 (:national_park:)" + U1F922 = "🤢", "🤢 (:nauseated_face:)" + U1F9FF = "🧿", "🧿 (:nazar_amulet:)" + U1F454 = "👔", "👔 (:necktie:)" + U1F913 = "🤓", "🤓 (:nerd_face:)" + U1FABA = "🪺", "🪺 (:nest_with_eggs:)" + U1FA86 = "🪆", "🪆 (:nesting_dolls:)" + U1F610 = "😐", "😐 (:neutral_face:)" + U1F311 = "🌑", "🌑 (:new_moon:)" + U1F31A = "🌚", "🌚 (:new_moon_face:)" + U1F4F0 = "📰", "📰 (:newspaper:)" + U23EDFE0F = "⏭️", "⏭️ (:next_track_button:)" + U23ED = "⏭", "⏭ (:next_track_button:)" + U1F303 = "🌃", "🌃 (:night_with_stars:)" + U1F564 = "🕤", "🕤 (:nine-thirty:)" + U1F558 = "🕘", "🕘 (:nine_o’clock:)" + U1F977 = "🥷", "🥷 (:ninja:)" + U1F9771F3FF = "🥷🏿", "🥷🏿 (:ninja_dark_skin_tone:)" + U1F9771F3FB = "🥷🏻", "🥷🏻 (:ninja_light_skin_tone:)" + U1F9771F3FE = "🥷🏾", "🥷🏾 (:ninja_medium-dark_skin_tone:)" + U1F9771F3FC = "🥷🏼", "🥷🏼 (:ninja_medium-light_skin_tone:)" + U1F9771F3FD = "🥷🏽", "🥷🏽 (:ninja_medium_skin_tone:)" + U1F6B3 = "🚳", "🚳 (:no_bicycles:)" + U26D4 = "⛔", "⛔ (:no_entry:)" + U1F6AF = "🚯", "🚯 (:no_littering:)" + U1F4F5 = "📵", "📵 (:no_mobile_phones:)" + U1F51E = "🔞", "🔞 (:no_one_under_eighteen:)" + U1F6B7 = "🚷", "🚷 (:no_pedestrians:)" + U1F6AD = "🚭", "🚭 (:no_smoking:)" + U1F6B1 = "🚱", "🚱 (:non-potable_water:)" + U1F443 = "👃", "👃 (:nose:)" + U1F4431F3FF = "👃🏿", "👃🏿 (:nose_dark_skin_tone:)" + U1F4431F3FB = "👃🏻", "👃🏻 (:nose_light_skin_tone:)" + U1F4431F3FE = "👃🏾", "👃🏾 (:nose_medium-dark_skin_tone:)" + U1F4431F3FC = "👃🏼", "👃🏼 (:nose_medium-light_skin_tone:)" + U1F4431F3FD = "👃🏽", "👃🏽 (:nose_medium_skin_tone:)" + U1F4D3 = "📓", "📓 (:notebook:)" + U1F4D4 = "📔", "📔 (:notebook_with_decorative_cover:)" + U1F529 = "🔩", "🔩 (:nut_and_bolt:)" + U1F419 = "🐙", "🐙 (:octopus:)" + U1F362 = "🍢", "🍢 (:oden:)" + U1F3E2 = "🏢", "🏢 (:office_building:)" + U1F9D1200D1F4BC = "🧑‍💼", "🧑‍💼 (:office_worker:)" + U1F9D11F3FF200D1F4BC = "🧑🏿‍💼", "🧑🏿‍💼 (:office_worker_dark_skin_tone:)" + U1F9D11F3FB200D1F4BC = "🧑🏻‍💼", "🧑🏻‍💼 (:office_worker_light_skin_tone:)" + U1F9D11F3FE200D1F4BC = "🧑🏾‍💼", "🧑🏾‍💼 (:office_worker_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F4BC = "🧑🏼‍💼", "🧑🏼‍💼 (:office_worker_medium-light_skin_tone:)" + U1F9D11F3FD200D1F4BC = "🧑🏽‍💼", "🧑🏽‍💼 (:office_worker_medium_skin_tone:)" + U1F479 = "👹", "👹 (:ogre:)" + U1F6E2FE0F = "🛢️", "🛢️ (:oil_drum:)" + U1F6E2 = "🛢", "🛢 (:oil_drum:)" + U1F5DDFE0F = "🗝️", "🗝️ (:old_key:)" + U1F5DD = "🗝", "🗝 (:old_key:)" + U1F474 = "👴", "👴 (:old_man:)" + U1F4741F3FF = "👴🏿", "👴🏿 (:old_man_dark_skin_tone:)" + U1F4741F3FB = "👴🏻", "👴🏻 (:old_man_light_skin_tone:)" + U1F4741F3FE = "👴🏾", "👴🏾 (:old_man_medium-dark_skin_tone:)" + U1F4741F3FC = "👴🏼", "👴🏼 (:old_man_medium-light_skin_tone:)" + U1F4741F3FD = "👴🏽", "👴🏽 (:old_man_medium_skin_tone:)" + U1F475 = "👵", "👵 (:old_woman:)" + U1F4751F3FF = "👵🏿", "👵🏿 (:old_woman_dark_skin_tone:)" + U1F4751F3FB = "👵🏻", "👵🏻 (:old_woman_light_skin_tone:)" + U1F4751F3FE = "👵🏾", "👵🏾 (:old_woman_medium-dark_skin_tone:)" + U1F4751F3FC = "👵🏼", "👵🏼 (:old_woman_medium-light_skin_tone:)" + U1F4751F3FD = "👵🏽", "👵🏽 (:old_woman_medium_skin_tone:)" + U1F9D3 = "🧓", "🧓 (:older_person:)" + U1F9D31F3FF = "🧓🏿", "🧓🏿 (:older_person_dark_skin_tone:)" + U1F9D31F3FB = "🧓🏻", "🧓🏻 (:older_person_light_skin_tone:)" + U1F9D31F3FE = "🧓🏾", "🧓🏾 (:older_person_medium-dark_skin_tone:)" + U1F9D31F3FC = "🧓🏼", "🧓🏼 (:older_person_medium-light_skin_tone:)" + U1F9D31F3FD = "🧓🏽", "🧓🏽 (:older_person_medium_skin_tone:)" + U1FAD2 = "🫒", "🫒 (:olive:)" + U1F549FE0F = "🕉️", "🕉️ (:om:)" + U1F549 = "🕉", "🕉 (:om:)" + U1F698 = "🚘", "🚘 (:oncoming_automobile:)" + U1F68D = "🚍", "🚍 (:oncoming_bus:)" + U1F44A = "👊", "👊 (:oncoming_fist:)" + U1F44A1F3FF = "👊🏿", "👊🏿 (:oncoming_fist_dark_skin_tone:)" + U1F44A1F3FB = "👊🏻", "👊🏻 (:oncoming_fist_light_skin_tone:)" + U1F44A1F3FE = "👊🏾", "👊🏾 (:oncoming_fist_medium-dark_skin_tone:)" + U1F44A1F3FC = "👊🏼", "👊🏼 (:oncoming_fist_medium-light_skin_tone:)" + U1F44A1F3FD = "👊🏽", "👊🏽 (:oncoming_fist_medium_skin_tone:)" + U1F694 = "🚔", "🚔 (:oncoming_police_car:)" + U1F696 = "🚖", "🚖 (:oncoming_taxi:)" + U1FA71 = "🩱", "🩱 (:one-piece_swimsuit:)" + U1F55C = "🕜", "🕜 (:one-thirty:)" + U1F550 = "🕐", "🕐 (:one_o’clock:)" + U1F9C5 = "🧅", "🧅 (:onion:)" + U1F4D6 = "📖", "📖 (:open_book:)" + U1F4C2 = "📂", "📂 (:open_file_folder:)" + U1F450 = "👐", "👐 (:open_hands:)" + U1F4501F3FF = "👐🏿", "👐🏿 (:open_hands_dark_skin_tone:)" + U1F4501F3FB = "👐🏻", "👐🏻 (:open_hands_light_skin_tone:)" + U1F4501F3FE = "👐🏾", "👐🏾 (:open_hands_medium-dark_skin_tone:)" + U1F4501F3FC = "👐🏼", "👐🏼 (:open_hands_medium-light_skin_tone:)" + U1F4501F3FD = "👐🏽", "👐🏽 (:open_hands_medium_skin_tone:)" + U1F4ED = "📭", "📭 (:open_mailbox_with_lowered_flag:)" + U1F4EC = "📬", "📬 (:open_mailbox_with_raised_flag:)" + U1F4BF = "💿", "💿 (:optical_disk:)" + U1F4D9 = "📙", "📙 (:orange_book:)" + U1F7E0 = "🟠", "🟠 (:orange_circle:)" + U1F9E1 = "🧡", "🧡 (:orange_heart:)" + U1F7E7 = "🟧", "🟧 (:orange_square:)" + U1F9A7 = "🦧", "🦧 (:orangutan:)" + U2626FE0F = "☦️", "☦️ (:orthodox_cross:)" + U2626 = "☦", "☦ (:orthodox_cross:)" + U1F9A6 = "🦦", "🦦 (:otter:)" + U1F4E4 = "📤", "📤 (:outbox_tray:)" + U1F989 = "🦉", "🦉 (:owl:)" + U1F402 = "🐂", "🐂 (:ox:)" + U1F9AA = "🦪", "🦪 (:oyster:)" + U1F4E6 = "📦", "📦 (:package:)" + U1F4C4 = "📄", "📄 (:page_facing_up:)" + U1F4C3 = "📃", "📃 (:page_with_curl:)" + U1F4DF = "📟", "📟 (:pager:)" + U1F58CFE0F = "🖌️", "🖌️ (:paintbrush:)" + U1F58C = "🖌", "🖌 (:paintbrush:)" + U1FAF3 = "🫳", "🫳 (:palm_down_hand:)" + U1FAF31F3FF = "🫳🏿", "🫳🏿 (:palm_down_hand_dark_skin_tone:)" + U1FAF31F3FB = "🫳🏻", "🫳🏻 (:palm_down_hand_light_skin_tone:)" + U1FAF31F3FE = "🫳🏾", "🫳🏾 (:palm_down_hand_medium-dark_skin_tone:)" + U1FAF31F3FC = "🫳🏼", "🫳🏼 (:palm_down_hand_medium-light_skin_tone:)" + U1FAF31F3FD = "🫳🏽", "🫳🏽 (:palm_down_hand_medium_skin_tone:)" + U1F334 = "🌴", "🌴 (:palm_tree:)" + U1FAF4 = "🫴", "🫴 (:palm_up_hand:)" + U1FAF41F3FF = "🫴🏿", "🫴🏿 (:palm_up_hand_dark_skin_tone:)" + U1FAF41F3FB = "🫴🏻", "🫴🏻 (:palm_up_hand_light_skin_tone:)" + U1FAF41F3FE = "🫴🏾", "🫴🏾 (:palm_up_hand_medium-dark_skin_tone:)" + U1FAF41F3FC = "🫴🏼", "🫴🏼 (:palm_up_hand_medium-light_skin_tone:)" + U1FAF41F3FD = "🫴🏽", "🫴🏽 (:palm_up_hand_medium_skin_tone:)" + U1F932 = "🤲", "🤲 (:palms_up_together:)" + U1F9321F3FF = "🤲🏿", "🤲🏿 (:palms_up_together_dark_skin_tone:)" + U1F9321F3FB = "🤲🏻", "🤲🏻 (:palms_up_together_light_skin_tone:)" + U1F9321F3FE = "🤲🏾", "🤲🏾 (:palms_up_together_medium-dark_skin_tone:)" + U1F9321F3FC = "🤲🏼", "🤲🏼 (:palms_up_together_medium-light_skin_tone:)" + U1F9321F3FD = "🤲🏽", "🤲🏽 (:palms_up_together_medium_skin_tone:)" + U1F95E = "🥞", "🥞 (:pancakes:)" + U1F43C = "🐼", "🐼 (:panda:)" + U1F4CE = "📎", "📎 (:paperclip:)" + U1FA82 = "🪂", "🪂 (:parachute:)" + U1F99C = "🦜", "🦜 (:parrot:)" + U303DFE0F = "〽️", "〽️ (:part_alternation_mark:)" + U303D = "〽", "〽 (:part_alternation_mark:)" + U1F389 = "🎉", "🎉 (:party_popper:)" + U1F973 = "🥳", "🥳 (:partying_face:)" + U1F6F3FE0F = "🛳️", "🛳️ (:passenger_ship:)" + U1F6F3 = "🛳", "🛳 (:passenger_ship:)" + U1F6C2 = "🛂", "🛂 (:passport_control:)" + U23F8FE0F = "⏸️", "⏸️ (:pause_button:)" + U23F8 = "⏸", "⏸ (:pause_button:)" + U1F43E = "🐾", "🐾 (:paw_prints:)" + U1FADB = "🫛", "🫛 (:pea_pod:)" + U262EFE0F = "☮️", "☮️ (:peace_symbol:)" + U262E = "☮", "☮ (:peace_symbol:)" + U1F351 = "🍑", "🍑 (:peach:)" + U1F99A = "🦚", "🦚 (:peacock:)" + U1F95C = "🥜", "🥜 (:peanuts:)" + U1F350 = "🍐", "🍐 (:pear:)" + U1F58AFE0F = "🖊️", "🖊️ (:pen:)" + U1F58A = "🖊", "🖊 (:pen:)" + U270FFE0F = "✏️", "✏️ (:pencil:)" + U270F = "✏", "✏ (:pencil:)" + U1F427 = "🐧", "🐧 (:penguin:)" + U1F614 = "😔", "😔 (:pensive_face:)" + U1F9D1200D1F91D200D1F9D1 = "🧑‍🤝‍🧑", "🧑‍🤝‍🧑 (:people_holding_hands:)" + U1F9D11F3FF200D1F91D200D1F9D11F3FF = "🧑🏿‍🤝‍🧑🏿", "🧑🏿‍🤝‍🧑🏿 (:people_holding_hands_dark_skin_tone:)" + U1F9D11F3FF200D1F91D200D1F9D11F3FB = "🧑🏿‍🤝‍🧑🏻", "🧑🏿‍🤝‍🧑🏻 (:people_holding_hands_dark_skin_tone_light_skin_tone:)" + U1F9D11F3FF200D1F91D200D1F9D11F3FE = "🧑🏿‍🤝‍🧑🏾", "🧑🏿‍🤝‍🧑🏾 (:people_holding_hands_dark_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FF200D1F91D200D1F9D11F3FC = "🧑🏿‍🤝‍🧑🏼", "🧑🏿‍🤝‍🧑🏼 (:people_holding_hands_dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FF200D1F91D200D1F9D11F3FD = "🧑🏿‍🤝‍🧑🏽", "🧑🏿‍🤝‍🧑🏽 (:people_holding_hands_dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FB200D1F91D200D1F9D11F3FB = "🧑🏻‍🤝‍🧑🏻", "🧑🏻‍🤝‍🧑🏻 (:people_holding_hands_light_skin_tone:)" + U1F9D11F3FB200D1F91D200D1F9D11F3FF = "🧑🏻‍🤝‍🧑🏿", "🧑🏻‍🤝‍🧑🏿 (:people_holding_hands_light_skin_tone_dark_skin_tone:)" + U1F9D11F3FB200D1F91D200D1F9D11F3FE = "🧑🏻‍🤝‍🧑🏾", "🧑🏻‍🤝‍🧑🏾 (:people_holding_hands_light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FB200D1F91D200D1F9D11F3FC = "🧑🏻‍🤝‍🧑🏼", "🧑🏻‍🤝‍🧑🏼 (:people_holding_hands_light_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FB200D1F91D200D1F9D11F3FD = "🧑🏻‍🤝‍🧑🏽", "🧑🏻‍🤝‍🧑🏽 (:people_holding_hands_light_skin_tone_medium_skin_tone:)" + U1F9D11F3FE200D1F91D200D1F9D11F3FE = "🧑🏾‍🤝‍🧑🏾", "🧑🏾‍🤝‍🧑🏾 (:people_holding_hands_medium-dark_skin_tone:)" + U1F9D11F3FE200D1F91D200D1F9D11F3FF = "🧑🏾‍🤝‍🧑🏿", "🧑🏾‍🤝‍🧑🏿 (:people_holding_hands_medium-dark_skin_tone_dark_skin_tone:)" + U1F9D11F3FE200D1F91D200D1F9D11F3FB = "🧑🏾‍🤝‍🧑🏻", "🧑🏾‍🤝‍🧑🏻 (:people_holding_hands_medium-dark_skin_tone_light_skin_tone:)" + U1F9D11F3FE200D1F91D200D1F9D11F3FC = "🧑🏾‍🤝‍🧑🏼", "🧑🏾‍🤝‍🧑🏼 (:people_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F9D11F3FE200D1F91D200D1F9D11F3FD = "🧑🏾‍🤝‍🧑🏽", "🧑🏾‍🤝‍🧑🏽 (:people_holding_hands_medium-dark_skin_tone_medium_skin_tone:)" + U1F9D11F3FC200D1F91D200D1F9D11F3FC = "🧑🏼‍🤝‍🧑🏼", "🧑🏼‍🤝‍🧑🏼 (:people_holding_hands_medium-light_skin_tone:)" + U1F9D11F3FC200D1F91D200D1F9D11F3FF = "🧑🏼‍🤝‍🧑🏿", "🧑🏼‍🤝‍🧑🏿 (:people_holding_hands_medium-light_skin_tone_dark_skin_tone:)" + U1F9D11F3FC200D1F91D200D1F9D11F3FB = "🧑🏼‍🤝‍🧑🏻", "🧑🏼‍🤝‍🧑🏻 (:people_holding_hands_medium-light_skin_tone_light_skin_tone:)" + U1F9D11F3FC200D1F91D200D1F9D11F3FE = "🧑🏼‍🤝‍🧑🏾", "🧑🏼‍🤝‍🧑🏾 (:people_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F91D200D1F9D11F3FD = "🧑🏼‍🤝‍🧑🏽", "🧑🏼‍🤝‍🧑🏽 (:people_holding_hands_medium-light_skin_tone_medium_skin_tone:)" + U1F9D11F3FD200D1F91D200D1F9D11F3FD = "🧑🏽‍🤝‍🧑🏽", "🧑🏽‍🤝‍🧑🏽 (:people_holding_hands_medium_skin_tone:)" + U1F9D11F3FD200D1F91D200D1F9D11F3FF = "🧑🏽‍🤝‍🧑🏿", "🧑🏽‍🤝‍🧑🏿 (:people_holding_hands_medium_skin_tone_dark_skin_tone:)" + U1F9D11F3FD200D1F91D200D1F9D11F3FB = "🧑🏽‍🤝‍🧑🏻", "🧑🏽‍🤝‍🧑🏻 (:people_holding_hands_medium_skin_tone_light_skin_tone:)" + U1F9D11F3FD200D1F91D200D1F9D11F3FE = "🧑🏽‍🤝‍🧑🏾", "🧑🏽‍🤝‍🧑🏾 (:people_holding_hands_medium_skin_tone_medium-dark_skin_tone:)" + U1F9D11F3FD200D1F91D200D1F9D11F3FC = "🧑🏽‍🤝‍🧑🏼", "🧑🏽‍🤝‍🧑🏼 (:people_holding_hands_medium_skin_tone_medium-light_skin_tone:)" + U1FAC2 = "🫂", "🫂 (:people_hugging:)" + U1F46F = "👯", "👯 (:people_with_bunny_ears:)" + U1F93C = "🤼", "🤼 (:people_wrestling:)" + U1F3AD = "🎭", "🎭 (:performing_arts:)" + U1F623 = "😣", "😣 (:persevering_face:)" + U1F9D1 = "🧑", "🧑 (:person:)" + U1F9D1200D1F9B2 = "🧑‍🦲", "🧑‍🦲 (:person_bald:)" + U1F9D4 = "🧔", "🧔 (:person_beard:)" + U1F6B4 = "🚴", "🚴 (:person_biking:)" + U1F6B41F3FF = "🚴🏿", "🚴🏿 (:person_biking_dark_skin_tone:)" + U1F6B41F3FB = "🚴🏻", "🚴🏻 (:person_biking_light_skin_tone:)" + U1F6B41F3FE = "🚴🏾", "🚴🏾 (:person_biking_medium-dark_skin_tone:)" + U1F6B41F3FC = "🚴🏼", "🚴🏼 (:person_biking_medium-light_skin_tone:)" + U1F6B41F3FD = "🚴🏽", "🚴🏽 (:person_biking_medium_skin_tone:)" + U1F471 = "👱", "👱 (:person_blond_hair:)" + U26F9FE0F = "⛹️", "⛹️ (:person_bouncing_ball:)" + U26F9 = "⛹", "⛹ (:person_bouncing_ball:)" + U26F91F3FF = "⛹🏿", "⛹🏿 (:person_bouncing_ball_dark_skin_tone:)" + U26F91F3FB = "⛹🏻", "⛹🏻 (:person_bouncing_ball_light_skin_tone:)" + U26F91F3FE = "⛹🏾", "⛹🏾 (:person_bouncing_ball_medium-dark_skin_tone:)" + U26F91F3FC = "⛹🏼", "⛹🏼 (:person_bouncing_ball_medium-light_skin_tone:)" + U26F91F3FD = "⛹🏽", "⛹🏽 (:person_bouncing_ball_medium_skin_tone:)" + U1F647 = "🙇", "🙇 (:person_bowing:)" + U1F6471F3FF = "🙇🏿", "🙇🏿 (:person_bowing_dark_skin_tone:)" + U1F6471F3FB = "🙇🏻", "🙇🏻 (:person_bowing_light_skin_tone:)" + U1F6471F3FE = "🙇🏾", "🙇🏾 (:person_bowing_medium-dark_skin_tone:)" + U1F6471F3FC = "🙇🏼", "🙇🏼 (:person_bowing_medium-light_skin_tone:)" + U1F6471F3FD = "🙇🏽", "🙇🏽 (:person_bowing_medium_skin_tone:)" + U1F938 = "🤸", "🤸 (:person_cartwheeling:)" + U1F9381F3FF = "🤸🏿", "🤸🏿 (:person_cartwheeling_dark_skin_tone:)" + U1F9381F3FB = "🤸🏻", "🤸🏻 (:person_cartwheeling_light_skin_tone:)" + U1F9381F3FE = "🤸🏾", "🤸🏾 (:person_cartwheeling_medium-dark_skin_tone:)" + U1F9381F3FC = "🤸🏼", "🤸🏼 (:person_cartwheeling_medium-light_skin_tone:)" + U1F9381F3FD = "🤸🏽", "🤸🏽 (:person_cartwheeling_medium_skin_tone:)" + U1F9D7 = "🧗", "🧗 (:person_climbing:)" + U1F9D71F3FF = "🧗🏿", "🧗🏿 (:person_climbing_dark_skin_tone:)" + U1F9D71F3FB = "🧗🏻", "🧗🏻 (:person_climbing_light_skin_tone:)" + U1F9D71F3FE = "🧗🏾", "🧗🏾 (:person_climbing_medium-dark_skin_tone:)" + U1F9D71F3FC = "🧗🏼", "🧗🏼 (:person_climbing_medium-light_skin_tone:)" + U1F9D71F3FD = "🧗🏽", "🧗🏽 (:person_climbing_medium_skin_tone:)" + U1F9D1200D1F9B1 = "🧑‍🦱", "🧑‍🦱 (:person_curly_hair:)" + U1F9D11F3FF = "🧑🏿", "🧑🏿 (:person_dark_skin_tone:)" + U1F9D11F3FF200D1F9B2 = "🧑🏿‍🦲", "🧑🏿‍🦲 (:person_dark_skin_tone_bald:)" + U1F9D41F3FF = "🧔🏿", "🧔🏿 (:person_dark_skin_tone_beard:)" + U1F4711F3FF = "👱🏿", "👱🏿 (:person_dark_skin_tone_blond_hair:)" + U1F9D11F3FF200D1F9B1 = "🧑🏿‍🦱", "🧑🏿‍🦱 (:person_dark_skin_tone_curly_hair:)" + U1F9D11F3FF200D1F9B0 = "🧑🏿‍🦰", "🧑🏿‍🦰 (:person_dark_skin_tone_red_hair:)" + U1F9D11F3FF200D1F9B3 = "🧑🏿‍🦳", "🧑🏿‍🦳 (:person_dark_skin_tone_white_hair:)" + U1F926 = "🤦", "🤦 (:person_facepalming:)" + U1F9261F3FF = "🤦🏿", "🤦🏿 (:person_facepalming_dark_skin_tone:)" + U1F9261F3FB = "🤦🏻", "🤦🏻 (:person_facepalming_light_skin_tone:)" + U1F9261F3FE = "🤦🏾", "🤦🏾 (:person_facepalming_medium-dark_skin_tone:)" + U1F9261F3FC = "🤦🏼", "🤦🏼 (:person_facepalming_medium-light_skin_tone:)" + U1F9261F3FD = "🤦🏽", "🤦🏽 (:person_facepalming_medium_skin_tone:)" + U1F9D1200D1F37C = "🧑‍🍼", "🧑‍🍼 (:person_feeding_baby:)" + U1F9D11F3FF200D1F37C = "🧑🏿‍🍼", "🧑🏿‍🍼 (:person_feeding_baby_dark_skin_tone:)" + U1F9D11F3FB200D1F37C = "🧑🏻‍🍼", "🧑🏻‍🍼 (:person_feeding_baby_light_skin_tone:)" + U1F9D11F3FE200D1F37C = "🧑🏾‍🍼", "🧑🏾‍🍼 (:person_feeding_baby_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F37C = "🧑🏼‍🍼", "🧑🏼‍🍼 (:person_feeding_baby_medium-light_skin_tone:)" + U1F9D11F3FD200D1F37C = "🧑🏽‍🍼", "🧑🏽‍🍼 (:person_feeding_baby_medium_skin_tone:)" + U1F93A = "🤺", "🤺 (:person_fencing:)" + U1F64D = "🙍", "🙍 (:person_frowning:)" + U1F64D1F3FF = "🙍🏿", "🙍🏿 (:person_frowning_dark_skin_tone:)" + U1F64D1F3FB = "🙍🏻", "🙍🏻 (:person_frowning_light_skin_tone:)" + U1F64D1F3FE = "🙍🏾", "🙍🏾 (:person_frowning_medium-dark_skin_tone:)" + U1F64D1F3FC = "🙍🏼", "🙍🏼 (:person_frowning_medium-light_skin_tone:)" + U1F64D1F3FD = "🙍🏽", "🙍🏽 (:person_frowning_medium_skin_tone:)" + U1F645 = "🙅", "🙅 (:person_gesturing_NO:)" + U1F6451F3FF = "🙅🏿", "🙅🏿 (:person_gesturing_NO_dark_skin_tone:)" + U1F6451F3FB = "🙅🏻", "🙅🏻 (:person_gesturing_NO_light_skin_tone:)" + U1F6451F3FE = "🙅🏾", "🙅🏾 (:person_gesturing_NO_medium-dark_skin_tone:)" + U1F6451F3FC = "🙅🏼", "🙅🏼 (:person_gesturing_NO_medium-light_skin_tone:)" + U1F6451F3FD = "🙅🏽", "🙅🏽 (:person_gesturing_NO_medium_skin_tone:)" + U1F646 = "🙆", "🙆 (:person_gesturing_OK:)" + U1F6461F3FF = "🙆🏿", "🙆🏿 (:person_gesturing_OK_dark_skin_tone:)" + U1F6461F3FB = "🙆🏻", "🙆🏻 (:person_gesturing_OK_light_skin_tone:)" + U1F6461F3FE = "🙆🏾", "🙆🏾 (:person_gesturing_OK_medium-dark_skin_tone:)" + U1F6461F3FC = "🙆🏼", "🙆🏼 (:person_gesturing_OK_medium-light_skin_tone:)" + U1F6461F3FD = "🙆🏽", "🙆🏽 (:person_gesturing_OK_medium_skin_tone:)" + U1F487 = "💇", "💇 (:person_getting_haircut:)" + U1F4871F3FF = "💇🏿", "💇🏿 (:person_getting_haircut_dark_skin_tone:)" + U1F4871F3FB = "💇🏻", "💇🏻 (:person_getting_haircut_light_skin_tone:)" + U1F4871F3FE = "💇🏾", "💇🏾 (:person_getting_haircut_medium-dark_skin_tone:)" + U1F4871F3FC = "💇🏼", "💇🏼 (:person_getting_haircut_medium-light_skin_tone:)" + U1F4871F3FD = "💇🏽", "💇🏽 (:person_getting_haircut_medium_skin_tone:)" + U1F486 = "💆", "💆 (:person_getting_massage:)" + U1F4861F3FF = "💆🏿", "💆🏿 (:person_getting_massage_dark_skin_tone:)" + U1F4861F3FB = "💆🏻", "💆🏻 (:person_getting_massage_light_skin_tone:)" + U1F4861F3FE = "💆🏾", "💆🏾 (:person_getting_massage_medium-dark_skin_tone:)" + U1F4861F3FC = "💆🏼", "💆🏼 (:person_getting_massage_medium-light_skin_tone:)" + U1F4861F3FD = "💆🏽", "💆🏽 (:person_getting_massage_medium_skin_tone:)" + U1F3CCFE0F = "🏌️", "🏌️ (:person_golfing:)" + U1F3CC = "🏌", "🏌 (:person_golfing:)" + U1F3CC1F3FF = "🏌🏿", "🏌🏿 (:person_golfing_dark_skin_tone:)" + U1F3CC1F3FB = "🏌🏻", "🏌🏻 (:person_golfing_light_skin_tone:)" + U1F3CC1F3FE = "🏌🏾", "🏌🏾 (:person_golfing_medium-dark_skin_tone:)" + U1F3CC1F3FC = "🏌🏼", "🏌🏼 (:person_golfing_medium-light_skin_tone:)" + U1F3CC1F3FD = "🏌🏽", "🏌🏽 (:person_golfing_medium_skin_tone:)" + U1F6CC = "🛌", "🛌 (:person_in_bed:)" + U1F6CC1F3FF = "🛌🏿", "🛌🏿 (:person_in_bed_dark_skin_tone:)" + U1F6CC1F3FB = "🛌🏻", "🛌🏻 (:person_in_bed_light_skin_tone:)" + U1F6CC1F3FE = "🛌🏾", "🛌🏾 (:person_in_bed_medium-dark_skin_tone:)" + U1F6CC1F3FC = "🛌🏼", "🛌🏼 (:person_in_bed_medium-light_skin_tone:)" + U1F6CC1F3FD = "🛌🏽", "🛌🏽 (:person_in_bed_medium_skin_tone:)" + U1F9D8 = "🧘", "🧘 (:person_in_lotus_position:)" + U1F9D81F3FF = "🧘🏿", "🧘🏿 (:person_in_lotus_position_dark_skin_tone:)" + U1F9D81F3FB = "🧘🏻", "🧘🏻 (:person_in_lotus_position_light_skin_tone:)" + U1F9D81F3FE = "🧘🏾", "🧘🏾 (:person_in_lotus_position_medium-dark_skin_tone:)" + U1F9D81F3FC = "🧘🏼", "🧘🏼 (:person_in_lotus_position_medium-light_skin_tone:)" + U1F9D81F3FD = "🧘🏽", "🧘🏽 (:person_in_lotus_position_medium_skin_tone:)" + U1F9D1200D1F9BD = "🧑‍🦽", "🧑‍🦽 (:person_in_manual_wheelchair:)" + U1F9D11F3FF200D1F9BD = "🧑🏿‍🦽", "🧑🏿‍🦽 (:person_in_manual_wheelchair_dark_skin_tone:)" + U1F9D1200D1F9BD200D27A1FE0F = "🧑‍🦽‍➡️", "🧑‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right:)" + U1F9D1200D1F9BD200D27A1 = "🧑‍🦽‍➡", "🧑‍🦽‍➡ (:person_in_manual_wheelchair_facing_right:)" + U1F9D11F3FF200D1F9BD200D27A1FE0F = "🧑🏿‍🦽‍➡️", "🧑🏿‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F9D11F3FF200D1F9BD200D27A1 = "🧑🏿‍🦽‍➡", "🧑🏿‍🦽‍➡ (:person_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F9D11F3FB200D1F9BD200D27A1FE0F = "🧑🏻‍🦽‍➡️", "🧑🏻‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F9D11F3FB200D1F9BD200D27A1 = "🧑🏻‍🦽‍➡", "🧑🏻‍🦽‍➡ (:person_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F9D11F3FE200D1F9BD200D27A1FE0F = "🧑🏾‍🦽‍➡️", "🧑🏾‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FE200D1F9BD200D27A1 = "🧑🏾‍🦽‍➡", "🧑🏾‍🦽‍➡ (:person_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9BD200D27A1FE0F = "🧑🏼‍🦽‍➡️", "🧑🏼‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F9D11F3FC200D1F9BD200D27A1 = "🧑🏼‍🦽‍➡", "🧑🏼‍🦽‍➡ (:person_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9BD200D27A1FE0F = "🧑🏽‍🦽‍➡️", "🧑🏽‍🦽‍➡️ (:person_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F9D11F3FD200D1F9BD200D27A1 = "🧑🏽‍🦽‍➡", "🧑🏽‍🦽‍➡ (:person_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F9D11F3FB200D1F9BD = "🧑🏻‍🦽", "🧑🏻‍🦽 (:person_in_manual_wheelchair_light_skin_tone:)" + U1F9D11F3FE200D1F9BD = "🧑🏾‍🦽", "🧑🏾‍🦽 (:person_in_manual_wheelchair_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9BD = "🧑🏼‍🦽", "🧑🏼‍🦽 (:person_in_manual_wheelchair_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9BD = "🧑🏽‍🦽", "🧑🏽‍🦽 (:person_in_manual_wheelchair_medium_skin_tone:)" + U1F9D1200D1F9BC = "🧑‍🦼", "🧑‍🦼 (:person_in_motorized_wheelchair:)" + U1F9D11F3FF200D1F9BC = "🧑🏿‍🦼", "🧑🏿‍🦼 (:person_in_motorized_wheelchair_dark_skin_tone:)" + U1F9D1200D1F9BC200D27A1FE0F = "🧑‍🦼‍➡️", "🧑‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right:)" + U1F9D1200D1F9BC200D27A1 = "🧑‍🦼‍➡", "🧑‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right:)" + U1F9D11F3FF200D1F9BC200D27A1FE0F = "🧑🏿‍🦼‍➡️", "🧑🏿‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F9D11F3FF200D1F9BC200D27A1 = "🧑🏿‍🦼‍➡", "🧑🏿‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F9D11F3FB200D1F9BC200D27A1FE0F = "🧑🏻‍🦼‍➡️", "🧑🏻‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F9D11F3FB200D1F9BC200D27A1 = "🧑🏻‍🦼‍➡", "🧑🏻‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F9D11F3FE200D1F9BC200D27A1FE0F = "🧑🏾‍🦼‍➡️", "🧑🏾‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FE200D1F9BC200D27A1 = "🧑🏾‍🦼‍➡", "🧑🏾‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9BC200D27A1FE0F = "🧑🏼‍🦼‍➡️", "🧑🏼‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F9D11F3FC200D1F9BC200D27A1 = "🧑🏼‍🦼‍➡", "🧑🏼‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9BC200D27A1FE0F = "🧑🏽‍🦼‍➡️", "🧑🏽‍🦼‍➡️ (:person_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F9D11F3FD200D1F9BC200D27A1 = "🧑🏽‍🦼‍➡", "🧑🏽‍🦼‍➡ (:person_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F9D11F3FB200D1F9BC = "🧑🏻‍🦼", "🧑🏻‍🦼 (:person_in_motorized_wheelchair_light_skin_tone:)" + U1F9D11F3FE200D1F9BC = "🧑🏾‍🦼", "🧑🏾‍🦼 (:person_in_motorized_wheelchair_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9BC = "🧑🏼‍🦼", "🧑🏼‍🦼 (:person_in_motorized_wheelchair_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9BC = "🧑🏽‍🦼", "🧑🏽‍🦼 (:person_in_motorized_wheelchair_medium_skin_tone:)" + U1F9D6 = "🧖", "🧖 (:person_in_steamy_room:)" + U1F9D61F3FF = "🧖🏿", "🧖🏿 (:person_in_steamy_room_dark_skin_tone:)" + U1F9D61F3FB = "🧖🏻", "🧖🏻 (:person_in_steamy_room_light_skin_tone:)" + U1F9D61F3FE = "🧖🏾", "🧖🏾 (:person_in_steamy_room_medium-dark_skin_tone:)" + U1F9D61F3FC = "🧖🏼", "🧖🏼 (:person_in_steamy_room_medium-light_skin_tone:)" + U1F9D61F3FD = "🧖🏽", "🧖🏽 (:person_in_steamy_room_medium_skin_tone:)" + U1F574FE0F = "🕴️", "🕴️ (:person_in_suit_levitating:)" + U1F574 = "🕴", "🕴 (:person_in_suit_levitating:)" + U1F5741F3FF = "🕴🏿", "🕴🏿 (:person_in_suit_levitating_dark_skin_tone:)" + U1F5741F3FB = "🕴🏻", "🕴🏻 (:person_in_suit_levitating_light_skin_tone:)" + U1F5741F3FE = "🕴🏾", "🕴🏾 (:person_in_suit_levitating_medium-dark_skin_tone:)" + U1F5741F3FC = "🕴🏼", "🕴🏼 (:person_in_suit_levitating_medium-light_skin_tone:)" + U1F5741F3FD = "🕴🏽", "🕴🏽 (:person_in_suit_levitating_medium_skin_tone:)" + U1F935 = "🤵", "🤵 (:person_in_tuxedo:)" + U1F9351F3FF = "🤵🏿", "🤵🏿 (:person_in_tuxedo_dark_skin_tone:)" + U1F9351F3FB = "🤵🏻", "🤵🏻 (:person_in_tuxedo_light_skin_tone:)" + U1F9351F3FE = "🤵🏾", "🤵🏾 (:person_in_tuxedo_medium-dark_skin_tone:)" + U1F9351F3FC = "🤵🏼", "🤵🏼 (:person_in_tuxedo_medium-light_skin_tone:)" + U1F9351F3FD = "🤵🏽", "🤵🏽 (:person_in_tuxedo_medium_skin_tone:)" + U1F939 = "🤹", "🤹 (:person_juggling:)" + U1F9391F3FF = "🤹🏿", "🤹🏿 (:person_juggling_dark_skin_tone:)" + U1F9391F3FB = "🤹🏻", "🤹🏻 (:person_juggling_light_skin_tone:)" + U1F9391F3FE = "🤹🏾", "🤹🏾 (:person_juggling_medium-dark_skin_tone:)" + U1F9391F3FC = "🤹🏼", "🤹🏼 (:person_juggling_medium-light_skin_tone:)" + U1F9391F3FD = "🤹🏽", "🤹🏽 (:person_juggling_medium_skin_tone:)" + U1F9CE = "🧎", "🧎 (:person_kneeling:)" + U1F9CE1F3FF = "🧎🏿", "🧎🏿 (:person_kneeling_dark_skin_tone:)" + U1F9CE200D27A1FE0F = "🧎‍➡️", "🧎‍➡️ (:person_kneeling_facing_right:)" + U1F9CE200D27A1 = "🧎‍➡", "🧎‍➡ (:person_kneeling_facing_right:)" + U1F9CE1F3FF200D27A1FE0F = "🧎🏿‍➡️", "🧎🏿‍➡️ (:person_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D27A1 = "🧎🏿‍➡", "🧎🏿‍➡ (:person_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FB200D27A1FE0F = "🧎🏻‍➡️", "🧎🏻‍➡️ (:person_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D27A1 = "🧎🏻‍➡", "🧎🏻‍➡ (:person_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FE200D27A1FE0F = "🧎🏾‍➡️", "🧎🏾‍➡️ (:person_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D27A1 = "🧎🏾‍➡", "🧎🏾‍➡ (:person_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FC200D27A1FE0F = "🧎🏼‍➡️", "🧎🏼‍➡️ (:person_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D27A1 = "🧎🏼‍➡", "🧎🏼‍➡ (:person_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FD200D27A1FE0F = "🧎🏽‍➡️", "🧎🏽‍➡️ (:person_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D27A1 = "🧎🏽‍➡", "🧎🏽‍➡ (:person_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FB = "🧎🏻", "🧎🏻 (:person_kneeling_light_skin_tone:)" + U1F9CE1F3FE = "🧎🏾", "🧎🏾 (:person_kneeling_medium-dark_skin_tone:)" + U1F9CE1F3FC = "🧎🏼", "🧎🏼 (:person_kneeling_medium-light_skin_tone:)" + U1F9CE1F3FD = "🧎🏽", "🧎🏽 (:person_kneeling_medium_skin_tone:)" + U1F3CBFE0F = "🏋️", "🏋️ (:person_lifting_weights:)" + U1F3CB = "🏋", "🏋 (:person_lifting_weights:)" + U1F3CB1F3FF = "🏋🏿", "🏋🏿 (:person_lifting_weights_dark_skin_tone:)" + U1F3CB1F3FB = "🏋🏻", "🏋🏻 (:person_lifting_weights_light_skin_tone:)" + U1F3CB1F3FE = "🏋🏾", "🏋🏾 (:person_lifting_weights_medium-dark_skin_tone:)" + U1F3CB1F3FC = "🏋🏼", "🏋🏼 (:person_lifting_weights_medium-light_skin_tone:)" + U1F3CB1F3FD = "🏋🏽", "🏋🏽 (:person_lifting_weights_medium_skin_tone:)" + U1F9D11F3FB = "🧑🏻", "🧑🏻 (:person_light_skin_tone:)" + U1F9D11F3FB200D1F9B2 = "🧑🏻‍🦲", "🧑🏻‍🦲 (:person_light_skin_tone_bald:)" + U1F9D41F3FB = "🧔🏻", "🧔🏻 (:person_light_skin_tone_beard:)" + U1F4711F3FB = "👱🏻", "👱🏻 (:person_light_skin_tone_blond_hair:)" + U1F9D11F3FB200D1F9B1 = "🧑🏻‍🦱", "🧑🏻‍🦱 (:person_light_skin_tone_curly_hair:)" + U1F9D11F3FB200D1F9B0 = "🧑🏻‍🦰", "🧑🏻‍🦰 (:person_light_skin_tone_red_hair:)" + U1F9D11F3FB200D1F9B3 = "🧑🏻‍🦳", "🧑🏻‍🦳 (:person_light_skin_tone_white_hair:)" + U1F9D11F3FE = "🧑🏾", "🧑🏾 (:person_medium-dark_skin_tone:)" + U1F9D11F3FE200D1F9B2 = "🧑🏾‍🦲", "🧑🏾‍🦲 (:person_medium-dark_skin_tone_bald:)" + U1F9D41F3FE = "🧔🏾", "🧔🏾 (:person_medium-dark_skin_tone_beard:)" + U1F4711F3FE = "👱🏾", "👱🏾 (:person_medium-dark_skin_tone_blond_hair:)" + U1F9D11F3FE200D1F9B1 = "🧑🏾‍🦱", "🧑🏾‍🦱 (:person_medium-dark_skin_tone_curly_hair:)" + U1F9D11F3FE200D1F9B0 = "🧑🏾‍🦰", "🧑🏾‍🦰 (:person_medium-dark_skin_tone_red_hair:)" + U1F9D11F3FE200D1F9B3 = "🧑🏾‍🦳", "🧑🏾‍🦳 (:person_medium-dark_skin_tone_white_hair:)" + U1F9D11F3FC = "🧑🏼", "🧑🏼 (:person_medium-light_skin_tone:)" + U1F9D11F3FC200D1F9B2 = "🧑🏼‍🦲", "🧑🏼‍🦲 (:person_medium-light_skin_tone_bald:)" + U1F9D41F3FC = "🧔🏼", "🧔🏼 (:person_medium-light_skin_tone_beard:)" + U1F4711F3FC = "👱🏼", "👱🏼 (:person_medium-light_skin_tone_blond_hair:)" + U1F9D11F3FC200D1F9B1 = "🧑🏼‍🦱", "🧑🏼‍🦱 (:person_medium-light_skin_tone_curly_hair:)" + U1F9D11F3FC200D1F9B0 = "🧑🏼‍🦰", "🧑🏼‍🦰 (:person_medium-light_skin_tone_red_hair:)" + U1F9D11F3FC200D1F9B3 = "🧑🏼‍🦳", "🧑🏼‍🦳 (:person_medium-light_skin_tone_white_hair:)" + U1F9D11F3FD = "🧑🏽", "🧑🏽 (:person_medium_skin_tone:)" + U1F9D11F3FD200D1F9B2 = "🧑🏽‍🦲", "🧑🏽‍🦲 (:person_medium_skin_tone_bald:)" + U1F9D41F3FD = "🧔🏽", "🧔🏽 (:person_medium_skin_tone_beard:)" + U1F4711F3FD = "👱🏽", "👱🏽 (:person_medium_skin_tone_blond_hair:)" + U1F9D11F3FD200D1F9B1 = "🧑🏽‍🦱", "🧑🏽‍🦱 (:person_medium_skin_tone_curly_hair:)" + U1F9D11F3FD200D1F9B0 = "🧑🏽‍🦰", "🧑🏽‍🦰 (:person_medium_skin_tone_red_hair:)" + U1F9D11F3FD200D1F9B3 = "🧑🏽‍🦳", "🧑🏽‍🦳 (:person_medium_skin_tone_white_hair:)" + U1F6B5 = "🚵", "🚵 (:person_mountain_biking:)" + U1F6B51F3FF = "🚵🏿", "🚵🏿 (:person_mountain_biking_dark_skin_tone:)" + U1F6B51F3FB = "🚵🏻", "🚵🏻 (:person_mountain_biking_light_skin_tone:)" + U1F6B51F3FE = "🚵🏾", "🚵🏾 (:person_mountain_biking_medium-dark_skin_tone:)" + U1F6B51F3FC = "🚵🏼", "🚵🏼 (:person_mountain_biking_medium-light_skin_tone:)" + U1F6B51F3FD = "🚵🏽", "🚵🏽 (:person_mountain_biking_medium_skin_tone:)" + U1F93E = "🤾", "🤾 (:person_playing_handball:)" + U1F93E1F3FF = "🤾🏿", "🤾🏿 (:person_playing_handball_dark_skin_tone:)" + U1F93E1F3FB = "🤾🏻", "🤾🏻 (:person_playing_handball_light_skin_tone:)" + U1F93E1F3FE = "🤾🏾", "🤾🏾 (:person_playing_handball_medium-dark_skin_tone:)" + U1F93E1F3FC = "🤾🏼", "🤾🏼 (:person_playing_handball_medium-light_skin_tone:)" + U1F93E1F3FD = "🤾🏽", "🤾🏽 (:person_playing_handball_medium_skin_tone:)" + U1F93D = "🤽", "🤽 (:person_playing_water_polo:)" + U1F93D1F3FF = "🤽🏿", "🤽🏿 (:person_playing_water_polo_dark_skin_tone:)" + U1F93D1F3FB = "🤽🏻", "🤽🏻 (:person_playing_water_polo_light_skin_tone:)" + U1F93D1F3FE = "🤽🏾", "🤽🏾 (:person_playing_water_polo_medium-dark_skin_tone:)" + U1F93D1F3FC = "🤽🏼", "🤽🏼 (:person_playing_water_polo_medium-light_skin_tone:)" + U1F93D1F3FD = "🤽🏽", "🤽🏽 (:person_playing_water_polo_medium_skin_tone:)" + U1F64E = "🙎", "🙎 (:person_pouting:)" + U1F64E1F3FF = "🙎🏿", "🙎🏿 (:person_pouting_dark_skin_tone:)" + U1F64E1F3FB = "🙎🏻", "🙎🏻 (:person_pouting_light_skin_tone:)" + U1F64E1F3FE = "🙎🏾", "🙎🏾 (:person_pouting_medium-dark_skin_tone:)" + U1F64E1F3FC = "🙎🏼", "🙎🏼 (:person_pouting_medium-light_skin_tone:)" + U1F64E1F3FD = "🙎🏽", "🙎🏽 (:person_pouting_medium_skin_tone:)" + U1F64B = "🙋", "🙋 (:person_raising_hand:)" + U1F64B1F3FF = "🙋🏿", "🙋🏿 (:person_raising_hand_dark_skin_tone:)" + U1F64B1F3FB = "🙋🏻", "🙋🏻 (:person_raising_hand_light_skin_tone:)" + U1F64B1F3FE = "🙋🏾", "🙋🏾 (:person_raising_hand_medium-dark_skin_tone:)" + U1F64B1F3FC = "🙋🏼", "🙋🏼 (:person_raising_hand_medium-light_skin_tone:)" + U1F64B1F3FD = "🙋🏽", "🙋🏽 (:person_raising_hand_medium_skin_tone:)" + U1F9D1200D1F9B0 = "🧑‍🦰", "🧑‍🦰 (:person_red_hair:)" + U1F6A3 = "🚣", "🚣 (:person_rowing_boat:)" + U1F6A31F3FF = "🚣🏿", "🚣🏿 (:person_rowing_boat_dark_skin_tone:)" + U1F6A31F3FB = "🚣🏻", "🚣🏻 (:person_rowing_boat_light_skin_tone:)" + U1F6A31F3FE = "🚣🏾", "🚣🏾 (:person_rowing_boat_medium-dark_skin_tone:)" + U1F6A31F3FC = "🚣🏼", "🚣🏼 (:person_rowing_boat_medium-light_skin_tone:)" + U1F6A31F3FD = "🚣🏽", "🚣🏽 (:person_rowing_boat_medium_skin_tone:)" + U1F3C3 = "🏃", "🏃 (:person_running:)" + U1F3C31F3FF = "🏃🏿", "🏃🏿 (:person_running_dark_skin_tone:)" + U1F3C3200D27A1FE0F = "🏃‍➡️", "🏃‍➡️ (:person_running_facing_right:)" + U1F3C3200D27A1 = "🏃‍➡", "🏃‍➡ (:person_running_facing_right:)" + U1F3C31F3FF200D27A1FE0F = "🏃🏿‍➡️", "🏃🏿‍➡️ (:person_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D27A1 = "🏃🏿‍➡", "🏃🏿‍➡ (:person_running_facing_right_dark_skin_tone:)" + U1F3C31F3FB200D27A1FE0F = "🏃🏻‍➡️", "🏃🏻‍➡️ (:person_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D27A1 = "🏃🏻‍➡", "🏃🏻‍➡ (:person_running_facing_right_light_skin_tone:)" + U1F3C31F3FE200D27A1FE0F = "🏃🏾‍➡️", "🏃🏾‍➡️ (:person_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D27A1 = "🏃🏾‍➡", "🏃🏾‍➡ (:person_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FC200D27A1FE0F = "🏃🏼‍➡️", "🏃🏼‍➡️ (:person_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D27A1 = "🏃🏼‍➡", "🏃🏼‍➡ (:person_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FD200D27A1FE0F = "🏃🏽‍➡️", "🏃🏽‍➡️ (:person_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D27A1 = "🏃🏽‍➡", "🏃🏽‍➡ (:person_running_facing_right_medium_skin_tone:)" + U1F3C31F3FB = "🏃🏻", "🏃🏻 (:person_running_light_skin_tone:)" + U1F3C31F3FE = "🏃🏾", "🏃🏾 (:person_running_medium-dark_skin_tone:)" + U1F3C31F3FC = "🏃🏼", "🏃🏼 (:person_running_medium-light_skin_tone:)" + U1F3C31F3FD = "🏃🏽", "🏃🏽 (:person_running_medium_skin_tone:)" + U1F937 = "🤷", "🤷 (:person_shrugging:)" + U1F9371F3FF = "🤷🏿", "🤷🏿 (:person_shrugging_dark_skin_tone:)" + U1F9371F3FB = "🤷🏻", "🤷🏻 (:person_shrugging_light_skin_tone:)" + U1F9371F3FE = "🤷🏾", "🤷🏾 (:person_shrugging_medium-dark_skin_tone:)" + U1F9371F3FC = "🤷🏼", "🤷🏼 (:person_shrugging_medium-light_skin_tone:)" + U1F9371F3FD = "🤷🏽", "🤷🏽 (:person_shrugging_medium_skin_tone:)" + U1F9CD = "🧍", "🧍 (:person_standing:)" + U1F9CD1F3FF = "🧍🏿", "🧍🏿 (:person_standing_dark_skin_tone:)" + U1F9CD1F3FB = "🧍🏻", "🧍🏻 (:person_standing_light_skin_tone:)" + U1F9CD1F3FE = "🧍🏾", "🧍🏾 (:person_standing_medium-dark_skin_tone:)" + U1F9CD1F3FC = "🧍🏼", "🧍🏼 (:person_standing_medium-light_skin_tone:)" + U1F9CD1F3FD = "🧍🏽", "🧍🏽 (:person_standing_medium_skin_tone:)" + U1F3C4 = "🏄", "🏄 (:person_surfing:)" + U1F3C41F3FF = "🏄🏿", "🏄🏿 (:person_surfing_dark_skin_tone:)" + U1F3C41F3FB = "🏄🏻", "🏄🏻 (:person_surfing_light_skin_tone:)" + U1F3C41F3FE = "🏄🏾", "🏄🏾 (:person_surfing_medium-dark_skin_tone:)" + U1F3C41F3FC = "🏄🏼", "🏄🏼 (:person_surfing_medium-light_skin_tone:)" + U1F3C41F3FD = "🏄🏽", "🏄🏽 (:person_surfing_medium_skin_tone:)" + U1F3CA = "🏊", "🏊 (:person_swimming:)" + U1F3CA1F3FF = "🏊🏿", "🏊🏿 (:person_swimming_dark_skin_tone:)" + U1F3CA1F3FB = "🏊🏻", "🏊🏻 (:person_swimming_light_skin_tone:)" + U1F3CA1F3FE = "🏊🏾", "🏊🏾 (:person_swimming_medium-dark_skin_tone:)" + U1F3CA1F3FC = "🏊🏼", "🏊🏼 (:person_swimming_medium-light_skin_tone:)" + U1F3CA1F3FD = "🏊🏽", "🏊🏽 (:person_swimming_medium_skin_tone:)" + U1F6C0 = "🛀", "🛀 (:person_taking_bath:)" + U1F6C01F3FF = "🛀🏿", "🛀🏿 (:person_taking_bath_dark_skin_tone:)" + U1F6C01F3FB = "🛀🏻", "🛀🏻 (:person_taking_bath_light_skin_tone:)" + U1F6C01F3FE = "🛀🏾", "🛀🏾 (:person_taking_bath_medium-dark_skin_tone:)" + U1F6C01F3FC = "🛀🏼", "🛀🏼 (:person_taking_bath_medium-light_skin_tone:)" + U1F6C01F3FD = "🛀🏽", "🛀🏽 (:person_taking_bath_medium_skin_tone:)" + U1F481 = "💁", "💁 (:person_tipping_hand:)" + U1F4811F3FF = "💁🏿", "💁🏿 (:person_tipping_hand_dark_skin_tone:)" + U1F4811F3FB = "💁🏻", "💁🏻 (:person_tipping_hand_light_skin_tone:)" + U1F4811F3FE = "💁🏾", "💁🏾 (:person_tipping_hand_medium-dark_skin_tone:)" + U1F4811F3FC = "💁🏼", "💁🏼 (:person_tipping_hand_medium-light_skin_tone:)" + U1F4811F3FD = "💁🏽", "💁🏽 (:person_tipping_hand_medium_skin_tone:)" + U1F6B6 = "🚶", "🚶 (:person_walking:)" + U1F6B61F3FF = "🚶🏿", "🚶🏿 (:person_walking_dark_skin_tone:)" + U1F6B6200D27A1FE0F = "🚶‍➡️", "🚶‍➡️ (:person_walking_facing_right:)" + U1F6B6200D27A1 = "🚶‍➡", "🚶‍➡ (:person_walking_facing_right:)" + U1F6B61F3FF200D27A1FE0F = "🚶🏿‍➡️", "🚶🏿‍➡️ (:person_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D27A1 = "🚶🏿‍➡", "🚶🏿‍➡ (:person_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FB200D27A1FE0F = "🚶🏻‍➡️", "🚶🏻‍➡️ (:person_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D27A1 = "🚶🏻‍➡", "🚶🏻‍➡ (:person_walking_facing_right_light_skin_tone:)" + U1F6B61F3FE200D27A1FE0F = "🚶🏾‍➡️", "🚶🏾‍➡️ (:person_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D27A1 = "🚶🏾‍➡", "🚶🏾‍➡ (:person_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FC200D27A1FE0F = "🚶🏼‍➡️", "🚶🏼‍➡️ (:person_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D27A1 = "🚶🏼‍➡", "🚶🏼‍➡ (:person_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FD200D27A1FE0F = "🚶🏽‍➡️", "🚶🏽‍➡️ (:person_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D27A1 = "🚶🏽‍➡", "🚶🏽‍➡ (:person_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FB = "🚶🏻", "🚶🏻 (:person_walking_light_skin_tone:)" + U1F6B61F3FE = "🚶🏾", "🚶🏾 (:person_walking_medium-dark_skin_tone:)" + U1F6B61F3FC = "🚶🏼", "🚶🏼 (:person_walking_medium-light_skin_tone:)" + U1F6B61F3FD = "🚶🏽", "🚶🏽 (:person_walking_medium_skin_tone:)" + U1F473 = "👳", "👳 (:person_wearing_turban:)" + U1F4731F3FF = "👳🏿", "👳🏿 (:person_wearing_turban_dark_skin_tone:)" + U1F4731F3FB = "👳🏻", "👳🏻 (:person_wearing_turban_light_skin_tone:)" + U1F4731F3FE = "👳🏾", "👳🏾 (:person_wearing_turban_medium-dark_skin_tone:)" + U1F4731F3FC = "👳🏼", "👳🏼 (:person_wearing_turban_medium-light_skin_tone:)" + U1F4731F3FD = "👳🏽", "👳🏽 (:person_wearing_turban_medium_skin_tone:)" + U1F9D1200D1F9B3 = "🧑‍🦳", "🧑‍🦳 (:person_white_hair:)" + U1FAC5 = "🫅", "🫅 (:person_with_crown:)" + U1FAC51F3FF = "🫅🏿", "🫅🏿 (:person_with_crown_dark_skin_tone:)" + U1FAC51F3FB = "🫅🏻", "🫅🏻 (:person_with_crown_light_skin_tone:)" + U1FAC51F3FE = "🫅🏾", "🫅🏾 (:person_with_crown_medium-dark_skin_tone:)" + U1FAC51F3FC = "🫅🏼", "🫅🏼 (:person_with_crown_medium-light_skin_tone:)" + U1FAC51F3FD = "🫅🏽", "🫅🏽 (:person_with_crown_medium_skin_tone:)" + U1F472 = "👲", "👲 (:person_with_skullcap:)" + U1F4721F3FF = "👲🏿", "👲🏿 (:person_with_skullcap_dark_skin_tone:)" + U1F4721F3FB = "👲🏻", "👲🏻 (:person_with_skullcap_light_skin_tone:)" + U1F4721F3FE = "👲🏾", "👲🏾 (:person_with_skullcap_medium-dark_skin_tone:)" + U1F4721F3FC = "👲🏼", "👲🏼 (:person_with_skullcap_medium-light_skin_tone:)" + U1F4721F3FD = "👲🏽", "👲🏽 (:person_with_skullcap_medium_skin_tone:)" + U1F470 = "👰", "👰 (:person_with_veil:)" + U1F4701F3FF = "👰🏿", "👰🏿 (:person_with_veil_dark_skin_tone:)" + U1F4701F3FB = "👰🏻", "👰🏻 (:person_with_veil_light_skin_tone:)" + U1F4701F3FE = "👰🏾", "👰🏾 (:person_with_veil_medium-dark_skin_tone:)" + U1F4701F3FC = "👰🏼", "👰🏼 (:person_with_veil_medium-light_skin_tone:)" + U1F4701F3FD = "👰🏽", "👰🏽 (:person_with_veil_medium_skin_tone:)" + U1F9D1200D1F9AF = "🧑‍🦯", "🧑‍🦯 (:person_with_white_cane:)" + U1F9D11F3FF200D1F9AF = "🧑🏿‍🦯", "🧑🏿‍🦯 (:person_with_white_cane_dark_skin_tone:)" + U1F9D1200D1F9AF200D27A1FE0F = "🧑‍🦯‍➡️", "🧑‍🦯‍➡️ (:person_with_white_cane_facing_right:)" + U1F9D1200D1F9AF200D27A1 = "🧑‍🦯‍➡", "🧑‍🦯‍➡ (:person_with_white_cane_facing_right:)" + U1F9D11F3FF200D1F9AF200D27A1FE0F = "🧑🏿‍🦯‍➡️", "🧑🏿‍🦯‍➡️ (:person_with_white_cane_facing_right_dark_skin_tone:)" + U1F9D11F3FF200D1F9AF200D27A1 = "🧑🏿‍🦯‍➡", "🧑🏿‍🦯‍➡ (:person_with_white_cane_facing_right_dark_skin_tone:)" + U1F9D11F3FB200D1F9AF200D27A1FE0F = "🧑🏻‍🦯‍➡️", "🧑🏻‍🦯‍➡️ (:person_with_white_cane_facing_right_light_skin_tone:)" + U1F9D11F3FB200D1F9AF200D27A1 = "🧑🏻‍🦯‍➡", "🧑🏻‍🦯‍➡ (:person_with_white_cane_facing_right_light_skin_tone:)" + U1F9D11F3FE200D1F9AF200D27A1FE0F = "🧑🏾‍🦯‍➡️", "🧑🏾‍🦯‍➡️ (:person_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FE200D1F9AF200D27A1 = "🧑🏾‍🦯‍➡", "🧑🏾‍🦯‍➡ (:person_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9AF200D27A1FE0F = "🧑🏼‍🦯‍➡️", "🧑🏼‍🦯‍➡️ (:person_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F9D11F3FC200D1F9AF200D27A1 = "🧑🏼‍🦯‍➡", "🧑🏼‍🦯‍➡ (:person_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9AF200D27A1FE0F = "🧑🏽‍🦯‍➡️", "🧑🏽‍🦯‍➡️ (:person_with_white_cane_facing_right_medium_skin_tone:)" + U1F9D11F3FD200D1F9AF200D27A1 = "🧑🏽‍🦯‍➡", "🧑🏽‍🦯‍➡ (:person_with_white_cane_facing_right_medium_skin_tone:)" + U1F9D11F3FB200D1F9AF = "🧑🏻‍🦯", "🧑🏻‍🦯 (:person_with_white_cane_light_skin_tone:)" + U1F9D11F3FE200D1F9AF = "🧑🏾‍🦯", "🧑🏾‍🦯 (:person_with_white_cane_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F9AF = "🧑🏼‍🦯", "🧑🏼‍🦯 (:person_with_white_cane_medium-light_skin_tone:)" + U1F9D11F3FD200D1F9AF = "🧑🏽‍🦯", "🧑🏽‍🦯 (:person_with_white_cane_medium_skin_tone:)" + U1F9EB = "🧫", "🧫 (:petri_dish:)" + U1F426200D1F525 = "🐦‍🔥", "🐦‍🔥 (:phoenix:)" + U26CFFE0F = "⛏️", "⛏️ (:pick:)" + U26CF = "⛏", "⛏ (:pick:)" + U1F6FB = "🛻", "🛻 (:pickup_truck:)" + U1F967 = "🥧", "🥧 (:pie:)" + U1F416 = "🐖", "🐖 (:pig:)" + U1F437 = "🐷", "🐷 (:pig_face:)" + U1F43D = "🐽", "🐽 (:pig_nose:)" + U1F4A9 = "💩", "💩 (:pile_of_poo:)" + U1F48A = "💊", "💊 (:pill:)" + U1F9D1200D2708FE0F = "🧑‍✈️", "🧑‍✈️ (:pilot:)" + U1F9D1200D2708 = "🧑‍✈", "🧑‍✈ (:pilot:)" + U1F9D11F3FF200D2708FE0F = "🧑🏿‍✈️", "🧑🏿‍✈️ (:pilot_dark_skin_tone:)" + U1F9D11F3FF200D2708 = "🧑🏿‍✈", "🧑🏿‍✈ (:pilot_dark_skin_tone:)" + U1F9D11F3FB200D2708FE0F = "🧑🏻‍✈️", "🧑🏻‍✈️ (:pilot_light_skin_tone:)" + U1F9D11F3FB200D2708 = "🧑🏻‍✈", "🧑🏻‍✈ (:pilot_light_skin_tone:)" + U1F9D11F3FE200D2708FE0F = "🧑🏾‍✈️", "🧑🏾‍✈️ (:pilot_medium-dark_skin_tone:)" + U1F9D11F3FE200D2708 = "🧑🏾‍✈", "🧑🏾‍✈ (:pilot_medium-dark_skin_tone:)" + U1F9D11F3FC200D2708FE0F = "🧑🏼‍✈️", "🧑🏼‍✈️ (:pilot_medium-light_skin_tone:)" + U1F9D11F3FC200D2708 = "🧑🏼‍✈", "🧑🏼‍✈ (:pilot_medium-light_skin_tone:)" + U1F9D11F3FD200D2708FE0F = "🧑🏽‍✈️", "🧑🏽‍✈️ (:pilot_medium_skin_tone:)" + U1F9D11F3FD200D2708 = "🧑🏽‍✈", "🧑🏽‍✈ (:pilot_medium_skin_tone:)" + U1F90C = "🤌", "🤌 (:pinched_fingers:)" + U1F90C1F3FF = "🤌🏿", "🤌🏿 (:pinched_fingers_dark_skin_tone:)" + U1F90C1F3FB = "🤌🏻", "🤌🏻 (:pinched_fingers_light_skin_tone:)" + U1F90C1F3FE = "🤌🏾", "🤌🏾 (:pinched_fingers_medium-dark_skin_tone:)" + U1F90C1F3FC = "🤌🏼", "🤌🏼 (:pinched_fingers_medium-light_skin_tone:)" + U1F90C1F3FD = "🤌🏽", "🤌🏽 (:pinched_fingers_medium_skin_tone:)" + U1F90F = "🤏", "🤏 (:pinching_hand:)" + U1F90F1F3FF = "🤏🏿", "🤏🏿 (:pinching_hand_dark_skin_tone:)" + U1F90F1F3FB = "🤏🏻", "🤏🏻 (:pinching_hand_light_skin_tone:)" + U1F90F1F3FE = "🤏🏾", "🤏🏾 (:pinching_hand_medium-dark_skin_tone:)" + U1F90F1F3FC = "🤏🏼", "🤏🏼 (:pinching_hand_medium-light_skin_tone:)" + U1F90F1F3FD = "🤏🏽", "🤏🏽 (:pinching_hand_medium_skin_tone:)" + U1F38D = "🎍", "🎍 (:pine_decoration:)" + U1F34D = "🍍", "🍍 (:pineapple:)" + U1F3D3 = "🏓", "🏓 (:ping_pong:)" + U1FA77 = "🩷", "🩷 (:pink_heart:)" + U1F3F4200D2620FE0F = "🏴‍☠️", "🏴‍☠️ (:pirate_flag:)" + U1F3F4200D2620 = "🏴‍☠", "🏴‍☠ (:pirate_flag:)" + U1F355 = "🍕", "🍕 (:pizza:)" + U1FA85 = "🪅", "🪅 (:piñata:)" + U1FAA7 = "🪧", "🪧 (:placard:)" + U1F6D0 = "🛐", "🛐 (:place_of_worship:)" + U25B6FE0F = "▶️", "▶️ (:play_button:)" + U25B6 = "▶", "▶ (:play_button:)" + U23EFFE0F = "⏯️", "⏯️ (:play_or_pause_button:)" + U23EF = "⏯", "⏯ (:play_or_pause_button:)" + U1F6DD = "🛝", "🛝 (:playground_slide:)" + U1F97A = "🥺", "🥺 (:pleading_face:)" + U1FAA0 = "🪠", "🪠 (:plunger:)" + U2795 = "➕", "➕ (:plus:)" + U1F43B200D2744FE0F = "🐻‍❄️", "🐻‍❄️ (:polar_bear:)" + U1F43B200D2744 = "🐻‍❄", "🐻‍❄ (:polar_bear:)" + U1F693 = "🚓", "🚓 (:police_car:)" + U1F6A8 = "🚨", "🚨 (:police_car_light:)" + U1F46E = "👮", "👮 (:police_officer:)" + U1F46E1F3FF = "👮🏿", "👮🏿 (:police_officer_dark_skin_tone:)" + U1F46E1F3FB = "👮🏻", "👮🏻 (:police_officer_light_skin_tone:)" + U1F46E1F3FE = "👮🏾", "👮🏾 (:police_officer_medium-dark_skin_tone:)" + U1F46E1F3FC = "👮🏼", "👮🏼 (:police_officer_medium-light_skin_tone:)" + U1F46E1F3FD = "👮🏽", "👮🏽 (:police_officer_medium_skin_tone:)" + U1F429 = "🐩", "🐩 (:poodle:)" + U1F3B1 = "🎱", "🎱 (:pool_8_ball:)" + U1F37F = "🍿", "🍿 (:popcorn:)" + U1F3E4 = "🏤", "🏤 (:post_office:)" + U1F4EF = "📯", "📯 (:postal_horn:)" + U1F4EE = "📮", "📮 (:postbox:)" + U1F372 = "🍲", "🍲 (:pot_of_food:)" + U1F6B0 = "🚰", "🚰 (:potable_water:)" + U1F954 = "🥔", "🥔 (:potato:)" + U1FAB4 = "🪴", "🪴 (:potted_plant:)" + U1F357 = "🍗", "🍗 (:poultry_leg:)" + U1F4B7 = "💷", "💷 (:pound_banknote:)" + U1FAD7 = "🫗", "🫗 (:pouring_liquid:)" + U1F63E = "😾", "😾 (:pouting_cat:)" + U1F4FF = "📿", "📿 (:prayer_beads:)" + U1FAC3 = "🫃", "🫃 (:pregnant_man:)" + U1FAC31F3FF = "🫃🏿", "🫃🏿 (:pregnant_man_dark_skin_tone:)" + U1FAC31F3FB = "🫃🏻", "🫃🏻 (:pregnant_man_light_skin_tone:)" + U1FAC31F3FE = "🫃🏾", "🫃🏾 (:pregnant_man_medium-dark_skin_tone:)" + U1FAC31F3FC = "🫃🏼", "🫃🏼 (:pregnant_man_medium-light_skin_tone:)" + U1FAC31F3FD = "🫃🏽", "🫃🏽 (:pregnant_man_medium_skin_tone:)" + U1FAC4 = "🫄", "🫄 (:pregnant_person:)" + U1FAC41F3FF = "🫄🏿", "🫄🏿 (:pregnant_person_dark_skin_tone:)" + U1FAC41F3FB = "🫄🏻", "🫄🏻 (:pregnant_person_light_skin_tone:)" + U1FAC41F3FE = "🫄🏾", "🫄🏾 (:pregnant_person_medium-dark_skin_tone:)" + U1FAC41F3FC = "🫄🏼", "🫄🏼 (:pregnant_person_medium-light_skin_tone:)" + U1FAC41F3FD = "🫄🏽", "🫄🏽 (:pregnant_person_medium_skin_tone:)" + U1F930 = "🤰", "🤰 (:pregnant_woman:)" + U1F9301F3FF = "🤰🏿", "🤰🏿 (:pregnant_woman_dark_skin_tone:)" + U1F9301F3FB = "🤰🏻", "🤰🏻 (:pregnant_woman_light_skin_tone:)" + U1F9301F3FE = "🤰🏾", "🤰🏾 (:pregnant_woman_medium-dark_skin_tone:)" + U1F9301F3FC = "🤰🏼", "🤰🏼 (:pregnant_woman_medium-light_skin_tone:)" + U1F9301F3FD = "🤰🏽", "🤰🏽 (:pregnant_woman_medium_skin_tone:)" + U1F968 = "🥨", "🥨 (:pretzel:)" + U1F934 = "🤴", "🤴 (:prince:)" + U1F9341F3FF = "🤴🏿", "🤴🏿 (:prince_dark_skin_tone:)" + U1F9341F3FB = "🤴🏻", "🤴🏻 (:prince_light_skin_tone:)" + U1F9341F3FE = "🤴🏾", "🤴🏾 (:prince_medium-dark_skin_tone:)" + U1F9341F3FC = "🤴🏼", "🤴🏼 (:prince_medium-light_skin_tone:)" + U1F9341F3FD = "🤴🏽", "🤴🏽 (:prince_medium_skin_tone:)" + U1F478 = "👸", "👸 (:princess:)" + U1F4781F3FF = "👸🏿", "👸🏿 (:princess_dark_skin_tone:)" + U1F4781F3FB = "👸🏻", "👸🏻 (:princess_light_skin_tone:)" + U1F4781F3FE = "👸🏾", "👸🏾 (:princess_medium-dark_skin_tone:)" + U1F4781F3FC = "👸🏼", "👸🏼 (:princess_medium-light_skin_tone:)" + U1F4781F3FD = "👸🏽", "👸🏽 (:princess_medium_skin_tone:)" + U1F5A8FE0F = "🖨️", "🖨️ (:printer:)" + U1F5A8 = "🖨", "🖨 (:printer:)" + U1F6AB = "🚫", "🚫 (:prohibited:)" + U1F7E3 = "🟣", "🟣 (:purple_circle:)" + U1F49C = "💜", "💜 (:purple_heart:)" + U1F7EA = "🟪", "🟪 (:purple_square:)" + U1F45B = "👛", "👛 (:purse:)" + U1F4CC = "📌", "📌 (:pushpin:)" + U1F9E9 = "🧩", "🧩 (:puzzle_piece:)" + U1F407 = "🐇", "🐇 (:rabbit:)" + U1F430 = "🐰", "🐰 (:rabbit_face:)" + U1F99D = "🦝", "🦝 (:raccoon:)" + U1F3CEFE0F = "🏎️", "🏎️ (:racing_car:)" + U1F3CE = "🏎", "🏎 (:racing_car:)" + U1F4FB = "📻", "📻 (:radio:)" + U1F518 = "🔘", "🔘 (:radio_button:)" + U2622FE0F = "☢️", "☢️ (:radioactive:)" + U2622 = "☢", "☢ (:radioactive:)" + U1F683 = "🚃", "🚃 (:railway_car:)" + U1F6E4FE0F = "🛤️", "🛤️ (:railway_track:)" + U1F6E4 = "🛤", "🛤 (:railway_track:)" + U1F308 = "🌈", "🌈 (:rainbow:)" + U1F3F3FE0F200D1F308 = "🏳️‍🌈", "🏳️‍🌈 (:rainbow_flag:)" + U1F3F3200D1F308 = "🏳‍🌈", "🏳‍🌈 (:rainbow_flag:)" + U1F91A = "🤚", "🤚 (:raised_back_of_hand:)" + U1F91A1F3FF = "🤚🏿", "🤚🏿 (:raised_back_of_hand_dark_skin_tone:)" + U1F91A1F3FB = "🤚🏻", "🤚🏻 (:raised_back_of_hand_light_skin_tone:)" + U1F91A1F3FE = "🤚🏾", "🤚🏾 (:raised_back_of_hand_medium-dark_skin_tone:)" + U1F91A1F3FC = "🤚🏼", "🤚🏼 (:raised_back_of_hand_medium-light_skin_tone:)" + U1F91A1F3FD = "🤚🏽", "🤚🏽 (:raised_back_of_hand_medium_skin_tone:)" + U270A = "✊", "✊ (:raised_fist:)" + U270A1F3FF = "✊🏿", "✊🏿 (:raised_fist_dark_skin_tone:)" + U270A1F3FB = "✊🏻", "✊🏻 (:raised_fist_light_skin_tone:)" + U270A1F3FE = "✊🏾", "✊🏾 (:raised_fist_medium-dark_skin_tone:)" + U270A1F3FC = "✊🏼", "✊🏼 (:raised_fist_medium-light_skin_tone:)" + U270A1F3FD = "✊🏽", "✊🏽 (:raised_fist_medium_skin_tone:)" + U270B = "✋", "✋ (:raised_hand:)" + U270B1F3FF = "✋🏿", "✋🏿 (:raised_hand_dark_skin_tone:)" + U270B1F3FB = "✋🏻", "✋🏻 (:raised_hand_light_skin_tone:)" + U270B1F3FE = "✋🏾", "✋🏾 (:raised_hand_medium-dark_skin_tone:)" + U270B1F3FC = "✋🏼", "✋🏼 (:raised_hand_medium-light_skin_tone:)" + U270B1F3FD = "✋🏽", "✋🏽 (:raised_hand_medium_skin_tone:)" + U1F64C = "🙌", "🙌 (:raising_hands:)" + U1F64C1F3FF = "🙌🏿", "🙌🏿 (:raising_hands_dark_skin_tone:)" + U1F64C1F3FB = "🙌🏻", "🙌🏻 (:raising_hands_light_skin_tone:)" + U1F64C1F3FE = "🙌🏾", "🙌🏾 (:raising_hands_medium-dark_skin_tone:)" + U1F64C1F3FC = "🙌🏼", "🙌🏼 (:raising_hands_medium-light_skin_tone:)" + U1F64C1F3FD = "🙌🏽", "🙌🏽 (:raising_hands_medium_skin_tone:)" + U1F40F = "🐏", "🐏 (:ram:)" + U1F400 = "🐀", "🐀 (:rat:)" + U1FA92 = "🪒", "🪒 (:razor:)" + U1F9FE = "🧾", "🧾 (:receipt:)" + U23FAFE0F = "⏺️", "⏺️ (:record_button:)" + U23FA = "⏺", "⏺ (:record_button:)" + U267BFE0F = "♻️", "♻️ (:recycling_symbol:)" + U267B = "♻", "♻ (:recycling_symbol:)" + U1F34E = "🍎", "🍎 (:red_apple:)" + U1F534 = "🔴", "🔴 (:red_circle:)" + U1F9E7 = "🧧", "🧧 (:red_envelope:)" + U2757 = "❗", "❗ (:red_exclamation_mark:)" + U1F9B0 = "🦰", "🦰 (:red_hair:)" + U2764FE0F = "❤️", "❤️ (:red_heart:)" + U2764 = "❤", "❤ (:red_heart:)" + U1F3EE = "🏮", "🏮 (:red_paper_lantern:)" + U2753 = "❓", "❓ (:red_question_mark:)" + U1F7E5 = "🟥", "🟥 (:red_square:)" + U1F53B = "🔻", "🔻 (:red_triangle_pointed_down:)" + U1F53A = "🔺", "🔺 (:red_triangle_pointed_up:)" + UAEFE0F = "®️", "®️ (:registered:)" + UAE = "®", "® (:registered:)" + U1F60C = "😌", "😌 (:relieved_face:)" + U1F397FE0F = "🎗️", "🎗️ (:reminder_ribbon:)" + U1F397 = "🎗", "🎗 (:reminder_ribbon:)" + U1F501 = "🔁", "🔁 (:repeat_button:)" + U1F502 = "🔂", "🔂 (:repeat_single_button:)" + U26D1FE0F = "⛑️", "⛑️ (:rescue_worker’s_helmet:)" + U26D1 = "⛑", "⛑ (:rescue_worker’s_helmet:)" + U1F6BB = "🚻", "🚻 (:restroom:)" + U25C0FE0F = "◀️", "◀️ (:reverse_button:)" + U25C0 = "◀", "◀ (:reverse_button:)" + U1F49E = "💞", "💞 (:revolving_hearts:)" + U1F98F = "🦏", "🦏 (:rhinoceros:)" + U1F380 = "🎀", "🎀 (:ribbon:)" + U1F359 = "🍙", "🍙 (:rice_ball:)" + U1F358 = "🍘", "🍘 (:rice_cracker:)" + U1F91C = "🤜", "🤜 (:right-facing_fist:)" + U1F91C1F3FF = "🤜🏿", "🤜🏿 (:right-facing_fist_dark_skin_tone:)" + U1F91C1F3FB = "🤜🏻", "🤜🏻 (:right-facing_fist_light_skin_tone:)" + U1F91C1F3FE = "🤜🏾", "🤜🏾 (:right-facing_fist_medium-dark_skin_tone:)" + U1F91C1F3FC = "🤜🏼", "🤜🏼 (:right-facing_fist_medium-light_skin_tone:)" + U1F91C1F3FD = "🤜🏽", "🤜🏽 (:right-facing_fist_medium_skin_tone:)" + U1F5EFFE0F = "🗯️", "🗯️ (:right_anger_bubble:)" + U1F5EF = "🗯", "🗯 (:right_anger_bubble:)" + U27A1FE0F = "➡️", "➡️ (:right_arrow:)" + U27A1 = "➡", "➡ (:right_arrow:)" + U2935FE0F = "⤵️", "⤵️ (:right_arrow_curving_down:)" + U2935 = "⤵", "⤵ (:right_arrow_curving_down:)" + U21A9FE0F = "↩️", "↩️ (:right_arrow_curving_left:)" + U21A9 = "↩", "↩ (:right_arrow_curving_left:)" + U2934FE0F = "⤴️", "⤴️ (:right_arrow_curving_up:)" + U2934 = "⤴", "⤴ (:right_arrow_curving_up:)" + U1FAF1 = "🫱", "🫱 (:rightwards_hand:)" + U1FAF11F3FF = "🫱🏿", "🫱🏿 (:rightwards_hand_dark_skin_tone:)" + U1FAF11F3FB = "🫱🏻", "🫱🏻 (:rightwards_hand_light_skin_tone:)" + U1FAF11F3FE = "🫱🏾", "🫱🏾 (:rightwards_hand_medium-dark_skin_tone:)" + U1FAF11F3FC = "🫱🏼", "🫱🏼 (:rightwards_hand_medium-light_skin_tone:)" + U1FAF11F3FD = "🫱🏽", "🫱🏽 (:rightwards_hand_medium_skin_tone:)" + U1FAF8 = "🫸", "🫸 (:rightwards_pushing_hand:)" + U1FAF81F3FF = "🫸🏿", "🫸🏿 (:rightwards_pushing_hand_dark_skin_tone:)" + U1FAF81F3FB = "🫸🏻", "🫸🏻 (:rightwards_pushing_hand_light_skin_tone:)" + U1FAF81F3FE = "🫸🏾", "🫸🏾 (:rightwards_pushing_hand_medium-dark_skin_tone:)" + U1FAF81F3FC = "🫸🏼", "🫸🏼 (:rightwards_pushing_hand_medium-light_skin_tone:)" + U1FAF81F3FD = "🫸🏽", "🫸🏽 (:rightwards_pushing_hand_medium_skin_tone:)" + U1F48D = "💍", "💍 (:ring:)" + U1F6DF = "🛟", "🛟 (:ring_buoy:)" + U1FA90 = "🪐", "🪐 (:ringed_planet:)" + U1F360 = "🍠", "🍠 (:roasted_sweet_potato:)" + U1F916 = "🤖", "🤖 (:robot:)" + U1FAA8 = "🪨", "🪨 (:rock:)" + U1F680 = "🚀", "🚀 (:rocket:)" + U1F9FB = "🧻", "🧻 (:roll_of_paper:)" + U1F5DEFE0F = "🗞️", "🗞️ (:rolled-up_newspaper:)" + U1F5DE = "🗞", "🗞 (:rolled-up_newspaper:)" + U1F3A2 = "🎢", "🎢 (:roller_coaster:)" + U1F6FC = "🛼", "🛼 (:roller_skate:)" + U1F923 = "🤣", "🤣 (:rolling_on_the_floor_laughing:)" + U1F413 = "🐓", "🐓 (:rooster:)" + U1F339 = "🌹", "🌹 (:rose:)" + U1F3F5FE0F = "🏵️", "🏵️ (:rosette:)" + U1F3F5 = "🏵", "🏵 (:rosette:)" + U1F4CD = "📍", "📍 (:round_pushpin:)" + U1F3C9 = "🏉", "🏉 (:rugby_football:)" + U1F3BD = "🎽", "🎽 (:running_shirt:)" + U1F45F = "👟", "👟 (:running_shoe:)" + U1F625 = "😥", "😥 (:sad_but_relieved_face:)" + U1F9F7 = "🧷", "🧷 (:safety_pin:)" + U1F9BA = "🦺", "🦺 (:safety_vest:)" + U26F5 = "⛵", "⛵ (:sailboat:)" + U1F376 = "🍶", "🍶 (:sake:)" + U1F9C2 = "🧂", "🧂 (:salt:)" + U1FAE1 = "🫡", "🫡 (:saluting_face:)" + U1F96A = "🥪", "🥪 (:sandwich:)" + U1F97B = "🥻", "🥻 (:sari:)" + U1F6F0FE0F = "🛰️", "🛰️ (:satellite:)" + U1F6F0 = "🛰", "🛰 (:satellite:)" + U1F4E1 = "📡", "📡 (:satellite_antenna:)" + U1F995 = "🦕", "🦕 (:sauropod:)" + U1F3B7 = "🎷", "🎷 (:saxophone:)" + U1F9E3 = "🧣", "🧣 (:scarf:)" + U1F3EB = "🏫", "🏫 (:school:)" + U1F9D1200D1F52C = "🧑‍🔬", "🧑‍🔬 (:scientist:)" + U1F9D11F3FF200D1F52C = "🧑🏿‍🔬", "🧑🏿‍🔬 (:scientist_dark_skin_tone:)" + U1F9D11F3FB200D1F52C = "🧑🏻‍🔬", "🧑🏻‍🔬 (:scientist_light_skin_tone:)" + U1F9D11F3FE200D1F52C = "🧑🏾‍🔬", "🧑🏾‍🔬 (:scientist_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F52C = "🧑🏼‍🔬", "🧑🏼‍🔬 (:scientist_medium-light_skin_tone:)" + U1F9D11F3FD200D1F52C = "🧑🏽‍🔬", "🧑🏽‍🔬 (:scientist_medium_skin_tone:)" + U2702FE0F = "✂️", "✂️ (:scissors:)" + U2702 = "✂", "✂ (:scissors:)" + U1F982 = "🦂", "🦂 (:scorpion:)" + U1FA9B = "🪛", "🪛 (:screwdriver:)" + U1F4DC = "📜", "📜 (:scroll:)" + U1F9AD = "🦭", "🦭 (:seal:)" + U1F4BA = "💺", "💺 (:seat:)" + U1F648 = "🙈", "🙈 (:see-no-evil_monkey:)" + U1F331 = "🌱", "🌱 (:seedling:)" + U1F933 = "🤳", "🤳 (:selfie:)" + U1F9331F3FF = "🤳🏿", "🤳🏿 (:selfie_dark_skin_tone:)" + U1F9331F3FB = "🤳🏻", "🤳🏻 (:selfie_light_skin_tone:)" + U1F9331F3FE = "🤳🏾", "🤳🏾 (:selfie_medium-dark_skin_tone:)" + U1F9331F3FC = "🤳🏼", "🤳🏼 (:selfie_medium-light_skin_tone:)" + U1F9331F3FD = "🤳🏽", "🤳🏽 (:selfie_medium_skin_tone:)" + U1F415200D1F9BA = "🐕‍🦺", "🐕‍🦺 (:service_dog:)" + U1F562 = "🕢", "🕢 (:seven-thirty:)" + U1F556 = "🕖", "🕖 (:seven_o’clock:)" + U1FAA1 = "🪡", "🪡 (:sewing_needle:)" + U1FAE8 = "🫨", "🫨 (:shaking_face:)" + U1F958 = "🥘", "🥘 (:shallow_pan_of_food:)" + U2618FE0F = "☘️", "☘️ (:shamrock:)" + U2618 = "☘", "☘ (:shamrock:)" + U1F988 = "🦈", "🦈 (:shark:)" + U1F367 = "🍧", "🍧 (:shaved_ice:)" + U1F33E = "🌾", "🌾 (:sheaf_of_rice:)" + U1F6E1FE0F = "🛡️", "🛡️ (:shield:)" + U1F6E1 = "🛡", "🛡 (:shield:)" + U26E9FE0F = "⛩️", "⛩️ (:shinto_shrine:)" + U26E9 = "⛩", "⛩ (:shinto_shrine:)" + U1F6A2 = "🚢", "🚢 (:ship:)" + U1F320 = "🌠", "🌠 (:shooting_star:)" + U1F6CDFE0F = "🛍️", "🛍️ (:shopping_bags:)" + U1F6CD = "🛍", "🛍 (:shopping_bags:)" + U1F6D2 = "🛒", "🛒 (:shopping_cart:)" + U1F370 = "🍰", "🍰 (:shortcake:)" + U1FA73 = "🩳", "🩳 (:shorts:)" + U1F6BF = "🚿", "🚿 (:shower:)" + U1F990 = "🦐", "🦐 (:shrimp:)" + U1F500 = "🔀", "🔀 (:shuffle_tracks_button:)" + U1F92B = "🤫", "🤫 (:shushing_face:)" + U1F918 = "🤘", "🤘 (:sign_of_the_horns:)" + U1F9181F3FF = "🤘🏿", "🤘🏿 (:sign_of_the_horns_dark_skin_tone:)" + U1F9181F3FB = "🤘🏻", "🤘🏻 (:sign_of_the_horns_light_skin_tone:)" + U1F9181F3FE = "🤘🏾", "🤘🏾 (:sign_of_the_horns_medium-dark_skin_tone:)" + U1F9181F3FC = "🤘🏼", "🤘🏼 (:sign_of_the_horns_medium-light_skin_tone:)" + U1F9181F3FD = "🤘🏽", "🤘🏽 (:sign_of_the_horns_medium_skin_tone:)" + U1F9D1200D1F3A4 = "🧑‍🎤", "🧑‍🎤 (:singer:)" + U1F9D11F3FF200D1F3A4 = "🧑🏿‍🎤", "🧑🏿‍🎤 (:singer_dark_skin_tone:)" + U1F9D11F3FB200D1F3A4 = "🧑🏻‍🎤", "🧑🏻‍🎤 (:singer_light_skin_tone:)" + U1F9D11F3FE200D1F3A4 = "🧑🏾‍🎤", "🧑🏾‍🎤 (:singer_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F3A4 = "🧑🏼‍🎤", "🧑🏼‍🎤 (:singer_medium-light_skin_tone:)" + U1F9D11F3FD200D1F3A4 = "🧑🏽‍🎤", "🧑🏽‍🎤 (:singer_medium_skin_tone:)" + U1F561 = "🕡", "🕡 (:six-thirty:)" + U1F555 = "🕕", "🕕 (:six_o’clock:)" + U1F6F9 = "🛹", "🛹 (:skateboard:)" + U26F7FE0F = "⛷️", "⛷️ (:skier:)" + U26F7 = "⛷", "⛷ (:skier:)" + U1F3BF = "🎿", "🎿 (:skis:)" + U1F480 = "💀", "💀 (:skull:)" + U2620FE0F = "☠️", "☠️ (:skull_and_crossbones:)" + U2620 = "☠", "☠ (:skull_and_crossbones:)" + U1F9A8 = "🦨", "🦨 (:skunk:)" + U1F6F7 = "🛷", "🛷 (:sled:)" + U1F634 = "😴", "😴 (:sleeping_face:)" + U1F62A = "😪", "😪 (:sleepy_face:)" + U1F641 = "🙁", "🙁 (:slightly_frowning_face:)" + U1F642 = "🙂", "🙂 (:slightly_smiling_face:)" + U1F3B0 = "🎰", "🎰 (:slot_machine:)" + U1F9A5 = "🦥", "🦥 (:sloth:)" + U1F6E9FE0F = "🛩️", "🛩️ (:small_airplane:)" + U1F6E9 = "🛩", "🛩 (:small_airplane:)" + U1F539 = "🔹", "🔹 (:small_blue_diamond:)" + U1F538 = "🔸", "🔸 (:small_orange_diamond:)" + U1F63B = "😻", "😻 (:smiling_cat_with_heart-eyes:)" + U263AFE0F = "☺️", "☺️ (:smiling_face:)" + U263A = "☺", "☺ (:smiling_face:)" + U1F607 = "😇", "😇 (:smiling_face_with_halo:)" + U1F60D = "😍", "😍 (:smiling_face_with_heart-eyes:)" + U1F970 = "🥰", "🥰 (:smiling_face_with_hearts:)" + U1F608 = "😈", "😈 (:smiling_face_with_horns:)" + U1F917 = "🤗", "🤗 (:smiling_face_with_open_hands:)" + U1F60A = "😊", "😊 (:smiling_face_with_smiling_eyes:)" + U1F60E = "😎", "😎 (:smiling_face_with_sunglasses:)" + U1F972 = "🥲", "🥲 (:smiling_face_with_tear:)" + U1F60F = "😏", "😏 (:smirking_face:)" + U1F40C = "🐌", "🐌 (:snail:)" + U1F40D = "🐍", "🐍 (:snake:)" + U1F927 = "🤧", "🤧 (:sneezing_face:)" + U1F3D4FE0F = "🏔️", "🏔️ (:snow-capped_mountain:)" + U1F3D4 = "🏔", "🏔 (:snow-capped_mountain:)" + U1F3C2 = "🏂", "🏂 (:snowboarder:)" + U1F3C21F3FF = "🏂🏿", "🏂🏿 (:snowboarder_dark_skin_tone:)" + U1F3C21F3FB = "🏂🏻", "🏂🏻 (:snowboarder_light_skin_tone:)" + U1F3C21F3FE = "🏂🏾", "🏂🏾 (:snowboarder_medium-dark_skin_tone:)" + U1F3C21F3FC = "🏂🏼", "🏂🏼 (:snowboarder_medium-light_skin_tone:)" + U1F3C21F3FD = "🏂🏽", "🏂🏽 (:snowboarder_medium_skin_tone:)" + U2744FE0F = "❄️", "❄️ (:snowflake:)" + U2744 = "❄", "❄ (:snowflake:)" + U2603FE0F = "☃️", "☃️ (:snowman:)" + U2603 = "☃", "☃ (:snowman:)" + U26C4 = "⛄", "⛄ (:snowman_without_snow:)" + U1F9FC = "🧼", "🧼 (:soap:)" + U26BD = "⚽", "⚽ (:soccer_ball:)" + U1F9E6 = "🧦", "🧦 (:socks:)" + U1F366 = "🍦", "🍦 (:soft_ice_cream:)" + U1F94E = "🥎", "🥎 (:softball:)" + U2660FE0F = "♠️", "♠️ (:spade_suit:)" + U2660 = "♠", "♠ (:spade_suit:)" + U1F35D = "🍝", "🍝 (:spaghetti:)" + U2747FE0F = "❇️", "❇️ (:sparkle:)" + U2747 = "❇", "❇ (:sparkle:)" + U1F387 = "🎇", "🎇 (:sparkler:)" + U2728 = "✨", "✨ (:sparkles:)" + U1F496 = "💖", "💖 (:sparkling_heart:)" + U1F64A = "🙊", "🙊 (:speak-no-evil_monkey:)" + U1F50A = "🔊", "🔊 (:speaker_high_volume:)" + U1F508 = "🔈", "🔈 (:speaker_low_volume:)" + U1F509 = "🔉", "🔉 (:speaker_medium_volume:)" + U1F5E3FE0F = "🗣️", "🗣️ (:speaking_head:)" + U1F5E3 = "🗣", "🗣 (:speaking_head:)" + U1F4AC = "💬", "💬 (:speech_balloon:)" + U1F6A4 = "🚤", "🚤 (:speedboat:)" + U1F577FE0F = "🕷️", "🕷️ (:spider:)" + U1F577 = "🕷", "🕷 (:spider:)" + U1F578FE0F = "🕸️", "🕸️ (:spider_web:)" + U1F578 = "🕸", "🕸 (:spider_web:)" + U1F5D3FE0F = "🗓️", "🗓️ (:spiral_calendar:)" + U1F5D3 = "🗓", "🗓 (:spiral_calendar:)" + U1F5D2FE0F = "🗒️", "🗒️ (:spiral_notepad:)" + U1F5D2 = "🗒", "🗒 (:spiral_notepad:)" + U1F41A = "🐚", "🐚 (:spiral_shell:)" + U1F9FD = "🧽", "🧽 (:sponge:)" + U1F944 = "🥄", "🥄 (:spoon:)" + U1F699 = "🚙", "🚙 (:sport_utility_vehicle:)" + U1F3C5 = "🏅", "🏅 (:sports_medal:)" + U1F433 = "🐳", "🐳 (:spouting_whale:)" + U1F991 = "🦑", "🦑 (:squid:)" + U1F61D = "😝", "😝 (:squinting_face_with_tongue:)" + U1F3DFFE0F = "🏟️", "🏟️ (:stadium:)" + U1F3DF = "🏟", "🏟 (:stadium:)" + U2B50 = "⭐", "⭐ (:star:)" + U1F929 = "🤩", "🤩 (:star-struck:)" + U262AFE0F = "☪️", "☪️ (:star_and_crescent:)" + U262A = "☪", "☪ (:star_and_crescent:)" + U2721FE0F = "✡️", "✡️ (:star_of_David:)" + U2721 = "✡", "✡ (:star_of_David:)" + U1F689 = "🚉", "🚉 (:station:)" + U1F35C = "🍜", "🍜 (:steaming_bowl:)" + U1FA7A = "🩺", "🩺 (:stethoscope:)" + U23F9FE0F = "⏹️", "⏹️ (:stop_button:)" + U23F9 = "⏹", "⏹ (:stop_button:)" + U1F6D1 = "🛑", "🛑 (:stop_sign:)" + U23F1FE0F = "⏱️", "⏱️ (:stopwatch:)" + U23F1 = "⏱", "⏱ (:stopwatch:)" + U1F4CF = "📏", "📏 (:straight_ruler:)" + U1F353 = "🍓", "🍓 (:strawberry:)" + U1F9D1200D1F393 = "🧑‍🎓", "🧑‍🎓 (:student:)" + U1F9D11F3FF200D1F393 = "🧑🏿‍🎓", "🧑🏿‍🎓 (:student_dark_skin_tone:)" + U1F9D11F3FB200D1F393 = "🧑🏻‍🎓", "🧑🏻‍🎓 (:student_light_skin_tone:)" + U1F9D11F3FE200D1F393 = "🧑🏾‍🎓", "🧑🏾‍🎓 (:student_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F393 = "🧑🏼‍🎓", "🧑🏼‍🎓 (:student_medium-light_skin_tone:)" + U1F9D11F3FD200D1F393 = "🧑🏽‍🎓", "🧑🏽‍🎓 (:student_medium_skin_tone:)" + U1F399FE0F = "🎙️", "🎙️ (:studio_microphone:)" + U1F399 = "🎙", "🎙 (:studio_microphone:)" + U1F959 = "🥙", "🥙 (:stuffed_flatbread:)" + U2600FE0F = "☀️", "☀️ (:sun:)" + U2600 = "☀", "☀ (:sun:)" + U26C5 = "⛅", "⛅ (:sun_behind_cloud:)" + U1F325FE0F = "🌥️", "🌥️ (:sun_behind_large_cloud:)" + U1F325 = "🌥", "🌥 (:sun_behind_large_cloud:)" + U1F326FE0F = "🌦️", "🌦️ (:sun_behind_rain_cloud:)" + U1F326 = "🌦", "🌦 (:sun_behind_rain_cloud:)" + U1F324FE0F = "🌤️", "🌤️ (:sun_behind_small_cloud:)" + U1F324 = "🌤", "🌤 (:sun_behind_small_cloud:)" + U1F31E = "🌞", "🌞 (:sun_with_face:)" + U1F33B = "🌻", "🌻 (:sunflower:)" + U1F576FE0F = "🕶️", "🕶️ (:sunglasses:)" + U1F576 = "🕶", "🕶 (:sunglasses:)" + U1F305 = "🌅", "🌅 (:sunrise:)" + U1F304 = "🌄", "🌄 (:sunrise_over_mountains:)" + U1F307 = "🌇", "🌇 (:sunset:)" + U1F9B8 = "🦸", "🦸 (:superhero:)" + U1F9B81F3FF = "🦸🏿", "🦸🏿 (:superhero_dark_skin_tone:)" + U1F9B81F3FB = "🦸🏻", "🦸🏻 (:superhero_light_skin_tone:)" + U1F9B81F3FE = "🦸🏾", "🦸🏾 (:superhero_medium-dark_skin_tone:)" + U1F9B81F3FC = "🦸🏼", "🦸🏼 (:superhero_medium-light_skin_tone:)" + U1F9B81F3FD = "🦸🏽", "🦸🏽 (:superhero_medium_skin_tone:)" + U1F9B9 = "🦹", "🦹 (:supervillain:)" + U1F9B91F3FF = "🦹🏿", "🦹🏿 (:supervillain_dark_skin_tone:)" + U1F9B91F3FB = "🦹🏻", "🦹🏻 (:supervillain_light_skin_tone:)" + U1F9B91F3FE = "🦹🏾", "🦹🏾 (:supervillain_medium-dark_skin_tone:)" + U1F9B91F3FC = "🦹🏼", "🦹🏼 (:supervillain_medium-light_skin_tone:)" + U1F9B91F3FD = "🦹🏽", "🦹🏽 (:supervillain_medium_skin_tone:)" + U1F363 = "🍣", "🍣 (:sushi:)" + U1F69F = "🚟", "🚟 (:suspension_railway:)" + U1F9A2 = "🦢", "🦢 (:swan:)" + U1F4A6 = "💦", "💦 (:sweat_droplets:)" + U1F54D = "🕍", "🕍 (:synagogue:)" + U1F489 = "💉", "💉 (:syringe:)" + U1F455 = "👕", "👕 (:t-shirt:)" + U1F32E = "🌮", "🌮 (:taco:)" + U1F961 = "🥡", "🥡 (:takeout_box:)" + U1FAD4 = "🫔", "🫔 (:tamale:)" + U1F38B = "🎋", "🎋 (:tanabata_tree:)" + U1F34A = "🍊", "🍊 (:tangerine:)" + U1F695 = "🚕", "🚕 (:taxi:)" + U1F9D1200D1F3EB = "🧑‍🏫", "🧑‍🏫 (:teacher:)" + U1F9D11F3FF200D1F3EB = "🧑🏿‍🏫", "🧑🏿‍🏫 (:teacher_dark_skin_tone:)" + U1F9D11F3FB200D1F3EB = "🧑🏻‍🏫", "🧑🏻‍🏫 (:teacher_light_skin_tone:)" + U1F9D11F3FE200D1F3EB = "🧑🏾‍🏫", "🧑🏾‍🏫 (:teacher_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F3EB = "🧑🏼‍🏫", "🧑🏼‍🏫 (:teacher_medium-light_skin_tone:)" + U1F9D11F3FD200D1F3EB = "🧑🏽‍🏫", "🧑🏽‍🏫 (:teacher_medium_skin_tone:)" + U1F375 = "🍵", "🍵 (:teacup_without_handle:)" + U1FAD6 = "🫖", "🫖 (:teapot:)" + U1F4C6 = "📆", "📆 (:tear-off_calendar:)" + U1F9D1200D1F4BB = "🧑‍💻", "🧑‍💻 (:technologist:)" + U1F9D11F3FF200D1F4BB = "🧑🏿‍💻", "🧑🏿‍💻 (:technologist_dark_skin_tone:)" + U1F9D11F3FB200D1F4BB = "🧑🏻‍💻", "🧑🏻‍💻 (:technologist_light_skin_tone:)" + U1F9D11F3FE200D1F4BB = "🧑🏾‍💻", "🧑🏾‍💻 (:technologist_medium-dark_skin_tone:)" + U1F9D11F3FC200D1F4BB = "🧑🏼‍💻", "🧑🏼‍💻 (:technologist_medium-light_skin_tone:)" + U1F9D11F3FD200D1F4BB = "🧑🏽‍💻", "🧑🏽‍💻 (:technologist_medium_skin_tone:)" + U1F9F8 = "🧸", "🧸 (:teddy_bear:)" + U260EFE0F = "☎️", "☎️ (:telephone:)" + U260E = "☎", "☎ (:telephone:)" + U1F4DE = "📞", "📞 (:telephone_receiver:)" + U1F52D = "🔭", "🔭 (:telescope:)" + U1F4FA = "📺", "📺 (:television:)" + U1F565 = "🕥", "🕥 (:ten-thirty:)" + U1F559 = "🕙", "🕙 (:ten_o’clock:)" + U1F3BE = "🎾", "🎾 (:tennis:)" + U26FA = "⛺", "⛺ (:tent:)" + U1F9EA = "🧪", "🧪 (:test_tube:)" + U1F321FE0F = "🌡️", "🌡️ (:thermometer:)" + U1F321 = "🌡", "🌡 (:thermometer:)" + U1F914 = "🤔", "🤔 (:thinking_face:)" + U1FA74 = "🩴", "🩴 (:thong_sandal:)" + U1F4AD = "💭", "💭 (:thought_balloon:)" + U1F9F5 = "🧵", "🧵 (:thread:)" + U1F55E = "🕞", "🕞 (:three-thirty:)" + U1F552 = "🕒", "🕒 (:three_o’clock:)" + U1F44E = "👎", "👎 (:thumbs_down:)" + U1F44E1F3FF = "👎🏿", "👎🏿 (:thumbs_down_dark_skin_tone:)" + U1F44E1F3FB = "👎🏻", "👎🏻 (:thumbs_down_light_skin_tone:)" + U1F44E1F3FE = "👎🏾", "👎🏾 (:thumbs_down_medium-dark_skin_tone:)" + U1F44E1F3FC = "👎🏼", "👎🏼 (:thumbs_down_medium-light_skin_tone:)" + U1F44E1F3FD = "👎🏽", "👎🏽 (:thumbs_down_medium_skin_tone:)" + U1F44D = "👍", "👍 (:thumbs_up:)" + U1F44D1F3FF = "👍🏿", "👍🏿 (:thumbs_up_dark_skin_tone:)" + U1F44D1F3FB = "👍🏻", "👍🏻 (:thumbs_up_light_skin_tone:)" + U1F44D1F3FE = "👍🏾", "👍🏾 (:thumbs_up_medium-dark_skin_tone:)" + U1F44D1F3FC = "👍🏼", "👍🏼 (:thumbs_up_medium-light_skin_tone:)" + U1F44D1F3FD = "👍🏽", "👍🏽 (:thumbs_up_medium_skin_tone:)" + U1F3AB = "🎫", "🎫 (:ticket:)" + U1F405 = "🐅", "🐅 (:tiger:)" + U1F42F = "🐯", "🐯 (:tiger_face:)" + U23F2FE0F = "⏲️", "⏲️ (:timer_clock:)" + U23F2 = "⏲", "⏲ (:timer_clock:)" + U1F62B = "😫", "😫 (:tired_face:)" + U1F6BD = "🚽", "🚽 (:toilet:)" + U1F345 = "🍅", "🍅 (:tomato:)" + U1F445 = "👅", "👅 (:tongue:)" + U1F9F0 = "🧰", "🧰 (:toolbox:)" + U1F9B7 = "🦷", "🦷 (:tooth:)" + U1FAA5 = "🪥", "🪥 (:toothbrush:)" + U1F3A9 = "🎩", "🎩 (:top_hat:)" + U1F32AFE0F = "🌪️", "🌪️ (:tornado:)" + U1F32A = "🌪", "🌪 (:tornado:)" + U1F5B2FE0F = "🖲️", "🖲️ (:trackball:)" + U1F5B2 = "🖲", "🖲 (:trackball:)" + U1F69C = "🚜", "🚜 (:tractor:)" + U2122FE0F = "™️", "™️ (:trade_mark:)" + U2122 = "™", "™ (:trade_mark:)" + U1F686 = "🚆", "🚆 (:train:)" + U1F68A = "🚊", "🚊 (:tram:)" + U1F68B = "🚋", "🚋 (:tram_car:)" + U1F3F3FE0F200D26A7FE0F = "🏳️‍⚧️", "🏳️‍⚧️ (:transgender_flag:)" + U1F3F3200D26A7FE0F = "🏳‍⚧️", "🏳‍⚧️ (:transgender_flag:)" + U1F3F3FE0F200D26A7 = "🏳️‍⚧", "🏳️‍⚧ (:transgender_flag:)" + U1F3F3200D26A7 = "🏳‍⚧", "🏳‍⚧ (:transgender_flag:)" + U26A7FE0F = "⚧️", "⚧️ (:transgender_symbol:)" + U26A7 = "⚧", "⚧ (:transgender_symbol:)" + U1F6A9 = "🚩", "🚩 (:triangular_flag:)" + U1F4D0 = "📐", "📐 (:triangular_ruler:)" + U1F531 = "🔱", "🔱 (:trident_emblem:)" + U1F9CC = "🧌", "🧌 (:troll:)" + U1F68E = "🚎", "🚎 (:trolleybus:)" + U1F3C6 = "🏆", "🏆 (:trophy:)" + U1F379 = "🍹", "🍹 (:tropical_drink:)" + U1F420 = "🐠", "🐠 (:tropical_fish:)" + U1F3BA = "🎺", "🎺 (:trumpet:)" + U1F337 = "🌷", "🌷 (:tulip:)" + U1F943 = "🥃", "🥃 (:tumbler_glass:)" + U1F983 = "🦃", "🦃 (:turkey:)" + U1F422 = "🐢", "🐢 (:turtle:)" + U1F567 = "🕧", "🕧 (:twelve-thirty:)" + U1F55B = "🕛", "🕛 (:twelve_o’clock:)" + U1F42B = "🐫", "🐫 (:two-hump_camel:)" + U1F55D = "🕝", "🕝 (:two-thirty:)" + U1F495 = "💕", "💕 (:two_hearts:)" + U1F551 = "🕑", "🕑 (:two_o’clock:)" + U2602FE0F = "☂️", "☂️ (:umbrella:)" + U2602 = "☂", "☂ (:umbrella:)" + U26F1FE0F = "⛱️", "⛱️ (:umbrella_on_ground:)" + U26F1 = "⛱", "⛱ (:umbrella_on_ground:)" + U2614 = "☔", "☔ (:umbrella_with_rain_drops:)" + U1F612 = "😒", "😒 (:unamused_face:)" + U1F984 = "🦄", "🦄 (:unicorn:)" + U1F513 = "🔓", "🔓 (:unlocked:)" + U2195FE0F = "↕️", "↕️ (:up-down_arrow:)" + U2195 = "↕", "↕ (:up-down_arrow:)" + U2196FE0F = "↖️", "↖️ (:up-left_arrow:)" + U2196 = "↖", "↖ (:up-left_arrow:)" + U2197FE0F = "↗️", "↗️ (:up-right_arrow:)" + U2197 = "↗", "↗ (:up-right_arrow:)" + U2B06FE0F = "⬆️", "⬆️ (:up_arrow:)" + U2B06 = "⬆", "⬆ (:up_arrow:)" + U1F643 = "🙃", "🙃 (:upside-down_face:)" + U1F53C = "🔼", "🔼 (:upwards_button:)" + U1F9DB = "🧛", "🧛 (:vampire:)" + U1F9DB1F3FF = "🧛🏿", "🧛🏿 (:vampire_dark_skin_tone:)" + U1F9DB1F3FB = "🧛🏻", "🧛🏻 (:vampire_light_skin_tone:)" + U1F9DB1F3FE = "🧛🏾", "🧛🏾 (:vampire_medium-dark_skin_tone:)" + U1F9DB1F3FC = "🧛🏼", "🧛🏼 (:vampire_medium-light_skin_tone:)" + U1F9DB1F3FD = "🧛🏽", "🧛🏽 (:vampire_medium_skin_tone:)" + U1F6A6 = "🚦", "🚦 (:vertical_traffic_light:)" + U1F4F3 = "📳", "📳 (:vibration_mode:)" + U270CFE0F = "✌️", "✌️ (:victory_hand:)" + U270C = "✌", "✌ (:victory_hand:)" + U270C1F3FF = "✌🏿", "✌🏿 (:victory_hand_dark_skin_tone:)" + U270C1F3FB = "✌🏻", "✌🏻 (:victory_hand_light_skin_tone:)" + U270C1F3FE = "✌🏾", "✌🏾 (:victory_hand_medium-dark_skin_tone:)" + U270C1F3FC = "✌🏼", "✌🏼 (:victory_hand_medium-light_skin_tone:)" + U270C1F3FD = "✌🏽", "✌🏽 (:victory_hand_medium_skin_tone:)" + U1F4F9 = "📹", "📹 (:video_camera:)" + U1F3AE = "🎮", "🎮 (:video_game:)" + U1F4FC = "📼", "📼 (:videocassette:)" + U1F3BB = "🎻", "🎻 (:violin:)" + U1F30B = "🌋", "🌋 (:volcano:)" + U1F3D0 = "🏐", "🏐 (:volleyball:)" + U1F596 = "🖖", "🖖 (:vulcan_salute:)" + U1F5961F3FF = "🖖🏿", "🖖🏿 (:vulcan_salute_dark_skin_tone:)" + U1F5961F3FB = "🖖🏻", "🖖🏻 (:vulcan_salute_light_skin_tone:)" + U1F5961F3FE = "🖖🏾", "🖖🏾 (:vulcan_salute_medium-dark_skin_tone:)" + U1F5961F3FC = "🖖🏼", "🖖🏼 (:vulcan_salute_medium-light_skin_tone:)" + U1F5961F3FD = "🖖🏽", "🖖🏽 (:vulcan_salute_medium_skin_tone:)" + U1F9C7 = "🧇", "🧇 (:waffle:)" + U1F318 = "🌘", "🌘 (:waning_crescent_moon:)" + U1F316 = "🌖", "🌖 (:waning_gibbous_moon:)" + U26A0FE0F = "⚠️", "⚠️ (:warning:)" + U26A0 = "⚠", "⚠ (:warning:)" + U1F5D1FE0F = "🗑️", "🗑️ (:wastebasket:)" + U1F5D1 = "🗑", "🗑 (:wastebasket:)" + U231A = "⌚", "⌚ (:watch:)" + U1F403 = "🐃", "🐃 (:water_buffalo:)" + U1F6BE = "🚾", "🚾 (:water_closet:)" + U1F52B = "🔫", "🔫 (:water_pistol:)" + U1F30A = "🌊", "🌊 (:water_wave:)" + U1F349 = "🍉", "🍉 (:watermelon:)" + U1F44B = "👋", "👋 (:waving_hand:)" + U1F44B1F3FF = "👋🏿", "👋🏿 (:waving_hand_dark_skin_tone:)" + U1F44B1F3FB = "👋🏻", "👋🏻 (:waving_hand_light_skin_tone:)" + U1F44B1F3FE = "👋🏾", "👋🏾 (:waving_hand_medium-dark_skin_tone:)" + U1F44B1F3FC = "👋🏼", "👋🏼 (:waving_hand_medium-light_skin_tone:)" + U1F44B1F3FD = "👋🏽", "👋🏽 (:waving_hand_medium_skin_tone:)" + U3030FE0F = "〰️", "〰️ (:wavy_dash:)" + U3030 = "〰", "〰 (:wavy_dash:)" + U1F312 = "🌒", "🌒 (:waxing_crescent_moon:)" + U1F314 = "🌔", "🌔 (:waxing_gibbous_moon:)" + U1F640 = "🙀", "🙀 (:weary_cat:)" + U1F629 = "😩", "😩 (:weary_face:)" + U1F492 = "💒", "💒 (:wedding:)" + U1F40B = "🐋", "🐋 (:whale:)" + U1F6DE = "🛞", "🛞 (:wheel:)" + U2638FE0F = "☸️", "☸️ (:wheel_of_dharma:)" + U2638 = "☸", "☸ (:wheel_of_dharma:)" + U267F = "♿", "♿ (:wheelchair_symbol:)" + U1F9AF = "🦯", "🦯 (:white_cane:)" + U26AA = "⚪", "⚪ (:white_circle:)" + U2755 = "❕", "❕ (:white_exclamation_mark:)" + U1F3F3FE0F = "🏳️", "🏳️ (:white_flag:)" + U1F3F3 = "🏳", "🏳 (:white_flag:)" + U1F4AE = "💮", "💮 (:white_flower:)" + U1F9B3 = "🦳", "🦳 (:white_hair:)" + U1F90D = "🤍", "🤍 (:white_heart:)" + U2B1C = "⬜", "⬜ (:white_large_square:)" + U25FD = "◽", "◽ (:white_medium-small_square:)" + U25FBFE0F = "◻️", "◻️ (:white_medium_square:)" + U25FB = "◻", "◻ (:white_medium_square:)" + U2754 = "❔", "❔ (:white_question_mark:)" + U25ABFE0F = "▫️", "▫️ (:white_small_square:)" + U25AB = "▫", "▫ (:white_small_square:)" + U1F533 = "🔳", "🔳 (:white_square_button:)" + U1F940 = "🥀", "🥀 (:wilted_flower:)" + U1F390 = "🎐", "🎐 (:wind_chime:)" + U1F32CFE0F = "🌬️", "🌬️ (:wind_face:)" + U1F32C = "🌬", "🌬 (:wind_face:)" + U1FA9F = "🪟", "🪟 (:window:)" + U1F377 = "🍷", "🍷 (:wine_glass:)" + U1FABD = "🪽", "🪽 (:wing:)" + U1F609 = "😉", "😉 (:winking_face:)" + U1F61C = "😜", "😜 (:winking_face_with_tongue:)" + U1F6DC = "🛜", "🛜 (:wireless:)" + U1F43A = "🐺", "🐺 (:wolf:)" + U1F469 = "👩", "👩 (:woman:)" + U1F46B = "👫", "👫 (:woman_and_man_holding_hands:)" + U1F46B1F3FF = "👫🏿", "👫🏿 (:woman_and_man_holding_hands_dark_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4681F3FB = "👩🏿‍🤝‍👨🏻", "👩🏿‍🤝‍👨🏻 (:woman_and_man_holding_hands_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4681F3FE = "👩🏿‍🤝‍👨🏾", "👩🏿‍🤝‍👨🏾 (:woman_and_man_holding_hands_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4681F3FC = "👩🏿‍🤝‍👨🏼", "👩🏿‍🤝‍👨🏼 (:woman_and_man_holding_hands_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4681F3FD = "👩🏿‍🤝‍👨🏽", "👩🏿‍🤝‍👨🏽 (:woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone:)" + U1F46B1F3FB = "👫🏻", "👫🏻 (:woman_and_man_holding_hands_light_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4681F3FF = "👩🏻‍🤝‍👨🏿", "👩🏻‍🤝‍👨🏿 (:woman_and_man_holding_hands_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4681F3FE = "👩🏻‍🤝‍👨🏾", "👩🏻‍🤝‍👨🏾 (:woman_and_man_holding_hands_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4681F3FC = "👩🏻‍🤝‍👨🏼", "👩🏻‍🤝‍👨🏼 (:woman_and_man_holding_hands_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4681F3FD = "👩🏻‍🤝‍👨🏽", "👩🏻‍🤝‍👨🏽 (:woman_and_man_holding_hands_light_skin_tone_medium_skin_tone:)" + U1F46B1F3FE = "👫🏾", "👫🏾 (:woman_and_man_holding_hands_medium-dark_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4681F3FF = "👩🏾‍🤝‍👨🏿", "👩🏾‍🤝‍👨🏿 (:woman_and_man_holding_hands_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4681F3FB = "👩🏾‍🤝‍👨🏻", "👩🏾‍🤝‍👨🏻 (:woman_and_man_holding_hands_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4681F3FC = "👩🏾‍🤝‍👨🏼", "👩🏾‍🤝‍👨🏼 (:woman_and_man_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4681F3FD = "👩🏾‍🤝‍👨🏽", "👩🏾‍🤝‍👨🏽 (:woman_and_man_holding_hands_medium-dark_skin_tone_medium_skin_tone:)" + U1F46B1F3FC = "👫🏼", "👫🏼 (:woman_and_man_holding_hands_medium-light_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4681F3FF = "👩🏼‍🤝‍👨🏿", "👩🏼‍🤝‍👨🏿 (:woman_and_man_holding_hands_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4681F3FB = "👩🏼‍🤝‍👨🏻", "👩🏼‍🤝‍👨🏻 (:woman_and_man_holding_hands_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4681F3FE = "👩🏼‍🤝‍👨🏾", "👩🏼‍🤝‍👨🏾 (:woman_and_man_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4681F3FD = "👩🏼‍🤝‍👨🏽", "👩🏼‍🤝‍👨🏽 (:woman_and_man_holding_hands_medium-light_skin_tone_medium_skin_tone:)" + U1F46B1F3FD = "👫🏽", "👫🏽 (:woman_and_man_holding_hands_medium_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4681F3FF = "👩🏽‍🤝‍👨🏿", "👩🏽‍🤝‍👨🏿 (:woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4681F3FB = "👩🏽‍🤝‍👨🏻", "👩🏽‍🤝‍👨🏻 (:woman_and_man_holding_hands_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4681F3FE = "👩🏽‍🤝‍👨🏾", "👩🏽‍🤝‍👨🏾 (:woman_and_man_holding_hands_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4681F3FC = "👩🏽‍🤝‍👨🏼", "👩🏽‍🤝‍👨🏼 (:woman_and_man_holding_hands_medium_skin_tone_medium-light_skin_tone:)" + U1F469200D1F3A8 = "👩‍🎨", "👩‍🎨 (:woman_artist:)" + U1F4691F3FF200D1F3A8 = "👩🏿‍🎨", "👩🏿‍🎨 (:woman_artist_dark_skin_tone:)" + U1F4691F3FB200D1F3A8 = "👩🏻‍🎨", "👩🏻‍🎨 (:woman_artist_light_skin_tone:)" + U1F4691F3FE200D1F3A8 = "👩🏾‍🎨", "👩🏾‍🎨 (:woman_artist_medium-dark_skin_tone:)" + U1F4691F3FC200D1F3A8 = "👩🏼‍🎨", "👩🏼‍🎨 (:woman_artist_medium-light_skin_tone:)" + U1F4691F3FD200D1F3A8 = "👩🏽‍🎨", "👩🏽‍🎨 (:woman_artist_medium_skin_tone:)" + U1F469200D1F680 = "👩‍🚀", "👩‍🚀 (:woman_astronaut:)" + U1F4691F3FF200D1F680 = "👩🏿‍🚀", "👩🏿‍🚀 (:woman_astronaut_dark_skin_tone:)" + U1F4691F3FB200D1F680 = "👩🏻‍🚀", "👩🏻‍🚀 (:woman_astronaut_light_skin_tone:)" + U1F4691F3FE200D1F680 = "👩🏾‍🚀", "👩🏾‍🚀 (:woman_astronaut_medium-dark_skin_tone:)" + U1F4691F3FC200D1F680 = "👩🏼‍🚀", "👩🏼‍🚀 (:woman_astronaut_medium-light_skin_tone:)" + U1F4691F3FD200D1F680 = "👩🏽‍🚀", "👩🏽‍🚀 (:woman_astronaut_medium_skin_tone:)" + U1F469200D1F9B2 = "👩‍🦲", "👩‍🦲 (:woman_bald:)" + U1F9D4200D2640FE0F = "🧔‍♀️", "🧔‍♀️ (:woman_beard:)" + U1F9D4200D2640 = "🧔‍♀", "🧔‍♀ (:woman_beard:)" + U1F6B4200D2640FE0F = "🚴‍♀️", "🚴‍♀️ (:woman_biking:)" + U1F6B4200D2640 = "🚴‍♀", "🚴‍♀ (:woman_biking:)" + U1F6B41F3FF200D2640FE0F = "🚴🏿‍♀️", "🚴🏿‍♀️ (:woman_biking_dark_skin_tone:)" + U1F6B41F3FF200D2640 = "🚴🏿‍♀", "🚴🏿‍♀ (:woman_biking_dark_skin_tone:)" + U1F6B41F3FB200D2640FE0F = "🚴🏻‍♀️", "🚴🏻‍♀️ (:woman_biking_light_skin_tone:)" + U1F6B41F3FB200D2640 = "🚴🏻‍♀", "🚴🏻‍♀ (:woman_biking_light_skin_tone:)" + U1F6B41F3FE200D2640FE0F = "🚴🏾‍♀️", "🚴🏾‍♀️ (:woman_biking_medium-dark_skin_tone:)" + U1F6B41F3FE200D2640 = "🚴🏾‍♀", "🚴🏾‍♀ (:woman_biking_medium-dark_skin_tone:)" + U1F6B41F3FC200D2640FE0F = "🚴🏼‍♀️", "🚴🏼‍♀️ (:woman_biking_medium-light_skin_tone:)" + U1F6B41F3FC200D2640 = "🚴🏼‍♀", "🚴🏼‍♀ (:woman_biking_medium-light_skin_tone:)" + U1F6B41F3FD200D2640FE0F = "🚴🏽‍♀️", "🚴🏽‍♀️ (:woman_biking_medium_skin_tone:)" + U1F6B41F3FD200D2640 = "🚴🏽‍♀", "🚴🏽‍♀ (:woman_biking_medium_skin_tone:)" + U1F471200D2640FE0F = "👱‍♀️", "👱‍♀️ (:woman_blond_hair:)" + U1F471200D2640 = "👱‍♀", "👱‍♀ (:woman_blond_hair:)" + U26F9FE0F200D2640FE0F = "⛹️‍♀️", "⛹️‍♀️ (:woman_bouncing_ball:)" + U26F9200D2640FE0F = "⛹‍♀️", "⛹‍♀️ (:woman_bouncing_ball:)" + U26F9FE0F200D2640 = "⛹️‍♀", "⛹️‍♀ (:woman_bouncing_ball:)" + U26F9200D2640 = "⛹‍♀", "⛹‍♀ (:woman_bouncing_ball:)" + U26F91F3FF200D2640FE0F = "⛹🏿‍♀️", "⛹🏿‍♀️ (:woman_bouncing_ball_dark_skin_tone:)" + U26F91F3FF200D2640 = "⛹🏿‍♀", "⛹🏿‍♀ (:woman_bouncing_ball_dark_skin_tone:)" + U26F91F3FB200D2640FE0F = "⛹🏻‍♀️", "⛹🏻‍♀️ (:woman_bouncing_ball_light_skin_tone:)" + U26F91F3FB200D2640 = "⛹🏻‍♀", "⛹🏻‍♀ (:woman_bouncing_ball_light_skin_tone:)" + U26F91F3FE200D2640FE0F = "⛹🏾‍♀️", "⛹🏾‍♀️ (:woman_bouncing_ball_medium-dark_skin_tone:)" + U26F91F3FE200D2640 = "⛹🏾‍♀", "⛹🏾‍♀ (:woman_bouncing_ball_medium-dark_skin_tone:)" + U26F91F3FC200D2640FE0F = "⛹🏼‍♀️", "⛹🏼‍♀️ (:woman_bouncing_ball_medium-light_skin_tone:)" + U26F91F3FC200D2640 = "⛹🏼‍♀", "⛹🏼‍♀ (:woman_bouncing_ball_medium-light_skin_tone:)" + U26F91F3FD200D2640FE0F = "⛹🏽‍♀️", "⛹🏽‍♀️ (:woman_bouncing_ball_medium_skin_tone:)" + U26F91F3FD200D2640 = "⛹🏽‍♀", "⛹🏽‍♀ (:woman_bouncing_ball_medium_skin_tone:)" + U1F647200D2640FE0F = "🙇‍♀️", "🙇‍♀️ (:woman_bowing:)" + U1F647200D2640 = "🙇‍♀", "🙇‍♀ (:woman_bowing:)" + U1F6471F3FF200D2640FE0F = "🙇🏿‍♀️", "🙇🏿‍♀️ (:woman_bowing_dark_skin_tone:)" + U1F6471F3FF200D2640 = "🙇🏿‍♀", "🙇🏿‍♀ (:woman_bowing_dark_skin_tone:)" + U1F6471F3FB200D2640FE0F = "🙇🏻‍♀️", "🙇🏻‍♀️ (:woman_bowing_light_skin_tone:)" + U1F6471F3FB200D2640 = "🙇🏻‍♀", "🙇🏻‍♀ (:woman_bowing_light_skin_tone:)" + U1F6471F3FE200D2640FE0F = "🙇🏾‍♀️", "🙇🏾‍♀️ (:woman_bowing_medium-dark_skin_tone:)" + U1F6471F3FE200D2640 = "🙇🏾‍♀", "🙇🏾‍♀ (:woman_bowing_medium-dark_skin_tone:)" + U1F6471F3FC200D2640FE0F = "🙇🏼‍♀️", "🙇🏼‍♀️ (:woman_bowing_medium-light_skin_tone:)" + U1F6471F3FC200D2640 = "🙇🏼‍♀", "🙇🏼‍♀ (:woman_bowing_medium-light_skin_tone:)" + U1F6471F3FD200D2640FE0F = "🙇🏽‍♀️", "🙇🏽‍♀️ (:woman_bowing_medium_skin_tone:)" + U1F6471F3FD200D2640 = "🙇🏽‍♀", "🙇🏽‍♀ (:woman_bowing_medium_skin_tone:)" + U1F938200D2640FE0F = "🤸‍♀️", "🤸‍♀️ (:woman_cartwheeling:)" + U1F938200D2640 = "🤸‍♀", "🤸‍♀ (:woman_cartwheeling:)" + U1F9381F3FF200D2640FE0F = "🤸🏿‍♀️", "🤸🏿‍♀️ (:woman_cartwheeling_dark_skin_tone:)" + U1F9381F3FF200D2640 = "🤸🏿‍♀", "🤸🏿‍♀ (:woman_cartwheeling_dark_skin_tone:)" + U1F9381F3FB200D2640FE0F = "🤸🏻‍♀️", "🤸🏻‍♀️ (:woman_cartwheeling_light_skin_tone:)" + U1F9381F3FB200D2640 = "🤸🏻‍♀", "🤸🏻‍♀ (:woman_cartwheeling_light_skin_tone:)" + U1F9381F3FE200D2640FE0F = "🤸🏾‍♀️", "🤸🏾‍♀️ (:woman_cartwheeling_medium-dark_skin_tone:)" + U1F9381F3FE200D2640 = "🤸🏾‍♀", "🤸🏾‍♀ (:woman_cartwheeling_medium-dark_skin_tone:)" + U1F9381F3FC200D2640FE0F = "🤸🏼‍♀️", "🤸🏼‍♀️ (:woman_cartwheeling_medium-light_skin_tone:)" + U1F9381F3FC200D2640 = "🤸🏼‍♀", "🤸🏼‍♀ (:woman_cartwheeling_medium-light_skin_tone:)" + U1F9381F3FD200D2640FE0F = "🤸🏽‍♀️", "🤸🏽‍♀️ (:woman_cartwheeling_medium_skin_tone:)" + U1F9381F3FD200D2640 = "🤸🏽‍♀", "🤸🏽‍♀ (:woman_cartwheeling_medium_skin_tone:)" + U1F9D7200D2640FE0F = "🧗‍♀️", "🧗‍♀️ (:woman_climbing:)" + U1F9D7200D2640 = "🧗‍♀", "🧗‍♀ (:woman_climbing:)" + U1F9D71F3FF200D2640FE0F = "🧗🏿‍♀️", "🧗🏿‍♀️ (:woman_climbing_dark_skin_tone:)" + U1F9D71F3FF200D2640 = "🧗🏿‍♀", "🧗🏿‍♀ (:woman_climbing_dark_skin_tone:)" + U1F9D71F3FB200D2640FE0F = "🧗🏻‍♀️", "🧗🏻‍♀️ (:woman_climbing_light_skin_tone:)" + U1F9D71F3FB200D2640 = "🧗🏻‍♀", "🧗🏻‍♀ (:woman_climbing_light_skin_tone:)" + U1F9D71F3FE200D2640FE0F = "🧗🏾‍♀️", "🧗🏾‍♀️ (:woman_climbing_medium-dark_skin_tone:)" + U1F9D71F3FE200D2640 = "🧗🏾‍♀", "🧗🏾‍♀ (:woman_climbing_medium-dark_skin_tone:)" + U1F9D71F3FC200D2640FE0F = "🧗🏼‍♀️", "🧗🏼‍♀️ (:woman_climbing_medium-light_skin_tone:)" + U1F9D71F3FC200D2640 = "🧗🏼‍♀", "🧗🏼‍♀ (:woman_climbing_medium-light_skin_tone:)" + U1F9D71F3FD200D2640FE0F = "🧗🏽‍♀️", "🧗🏽‍♀️ (:woman_climbing_medium_skin_tone:)" + U1F9D71F3FD200D2640 = "🧗🏽‍♀", "🧗🏽‍♀ (:woman_climbing_medium_skin_tone:)" + U1F477200D2640FE0F = "👷‍♀️", "👷‍♀️ (:woman_construction_worker:)" + U1F477200D2640 = "👷‍♀", "👷‍♀ (:woman_construction_worker:)" + U1F4771F3FF200D2640FE0F = "👷🏿‍♀️", "👷🏿‍♀️ (:woman_construction_worker_dark_skin_tone:)" + U1F4771F3FF200D2640 = "👷🏿‍♀", "👷🏿‍♀ (:woman_construction_worker_dark_skin_tone:)" + U1F4771F3FB200D2640FE0F = "👷🏻‍♀️", "👷🏻‍♀️ (:woman_construction_worker_light_skin_tone:)" + U1F4771F3FB200D2640 = "👷🏻‍♀", "👷🏻‍♀ (:woman_construction_worker_light_skin_tone:)" + U1F4771F3FE200D2640FE0F = "👷🏾‍♀️", "👷🏾‍♀️ (:woman_construction_worker_medium-dark_skin_tone:)" + U1F4771F3FE200D2640 = "👷🏾‍♀", "👷🏾‍♀ (:woman_construction_worker_medium-dark_skin_tone:)" + U1F4771F3FC200D2640FE0F = "👷🏼‍♀️", "👷🏼‍♀️ (:woman_construction_worker_medium-light_skin_tone:)" + U1F4771F3FC200D2640 = "👷🏼‍♀", "👷🏼‍♀ (:woman_construction_worker_medium-light_skin_tone:)" + U1F4771F3FD200D2640FE0F = "👷🏽‍♀️", "👷🏽‍♀️ (:woman_construction_worker_medium_skin_tone:)" + U1F4771F3FD200D2640 = "👷🏽‍♀", "👷🏽‍♀ (:woman_construction_worker_medium_skin_tone:)" + U1F469200D1F373 = "👩‍🍳", "👩‍🍳 (:woman_cook:)" + U1F4691F3FF200D1F373 = "👩🏿‍🍳", "👩🏿‍🍳 (:woman_cook_dark_skin_tone:)" + U1F4691F3FB200D1F373 = "👩🏻‍🍳", "👩🏻‍🍳 (:woman_cook_light_skin_tone:)" + U1F4691F3FE200D1F373 = "👩🏾‍🍳", "👩🏾‍🍳 (:woman_cook_medium-dark_skin_tone:)" + U1F4691F3FC200D1F373 = "👩🏼‍🍳", "👩🏼‍🍳 (:woman_cook_medium-light_skin_tone:)" + U1F4691F3FD200D1F373 = "👩🏽‍🍳", "👩🏽‍🍳 (:woman_cook_medium_skin_tone:)" + U1F469200D1F9B1 = "👩‍🦱", "👩‍🦱 (:woman_curly_hair:)" + U1F483 = "💃", "💃 (:woman_dancing:)" + U1F4831F3FF = "💃🏿", "💃🏿 (:woman_dancing_dark_skin_tone:)" + U1F4831F3FB = "💃🏻", "💃🏻 (:woman_dancing_light_skin_tone:)" + U1F4831F3FE = "💃🏾", "💃🏾 (:woman_dancing_medium-dark_skin_tone:)" + U1F4831F3FC = "💃🏼", "💃🏼 (:woman_dancing_medium-light_skin_tone:)" + U1F4831F3FD = "💃🏽", "💃🏽 (:woman_dancing_medium_skin_tone:)" + U1F4691F3FF = "👩🏿", "👩🏿 (:woman_dark_skin_tone:)" + U1F4691F3FF200D1F9B2 = "👩🏿‍🦲", "👩🏿‍🦲 (:woman_dark_skin_tone_bald:)" + U1F9D41F3FF200D2640FE0F = "🧔🏿‍♀️", "🧔🏿‍♀️ (:woman_dark_skin_tone_beard:)" + U1F9D41F3FF200D2640 = "🧔🏿‍♀", "🧔🏿‍♀ (:woman_dark_skin_tone_beard:)" + U1F4711F3FF200D2640FE0F = "👱🏿‍♀️", "👱🏿‍♀️ (:woman_dark_skin_tone_blond_hair:)" + U1F4711F3FF200D2640 = "👱🏿‍♀", "👱🏿‍♀ (:woman_dark_skin_tone_blond_hair:)" + U1F4691F3FF200D1F9B1 = "👩🏿‍🦱", "👩🏿‍🦱 (:woman_dark_skin_tone_curly_hair:)" + U1F4691F3FF200D1F9B0 = "👩🏿‍🦰", "👩🏿‍🦰 (:woman_dark_skin_tone_red_hair:)" + U1F4691F3FF200D1F9B3 = "👩🏿‍🦳", "👩🏿‍🦳 (:woman_dark_skin_tone_white_hair:)" + U1F575FE0F200D2640FE0F = "🕵️‍♀️", "🕵️‍♀️ (:woman_detective:)" + U1F575200D2640FE0F = "🕵‍♀️", "🕵‍♀️ (:woman_detective:)" + U1F575FE0F200D2640 = "🕵️‍♀", "🕵️‍♀ (:woman_detective:)" + U1F575200D2640 = "🕵‍♀", "🕵‍♀ (:woman_detective:)" + U1F5751F3FF200D2640FE0F = "🕵🏿‍♀️", "🕵🏿‍♀️ (:woman_detective_dark_skin_tone:)" + U1F5751F3FF200D2640 = "🕵🏿‍♀", "🕵🏿‍♀ (:woman_detective_dark_skin_tone:)" + U1F5751F3FB200D2640FE0F = "🕵🏻‍♀️", "🕵🏻‍♀️ (:woman_detective_light_skin_tone:)" + U1F5751F3FB200D2640 = "🕵🏻‍♀", "🕵🏻‍♀ (:woman_detective_light_skin_tone:)" + U1F5751F3FE200D2640FE0F = "🕵🏾‍♀️", "🕵🏾‍♀️ (:woman_detective_medium-dark_skin_tone:)" + U1F5751F3FE200D2640 = "🕵🏾‍♀", "🕵🏾‍♀ (:woman_detective_medium-dark_skin_tone:)" + U1F5751F3FC200D2640FE0F = "🕵🏼‍♀️", "🕵🏼‍♀️ (:woman_detective_medium-light_skin_tone:)" + U1F5751F3FC200D2640 = "🕵🏼‍♀", "🕵🏼‍♀ (:woman_detective_medium-light_skin_tone:)" + U1F5751F3FD200D2640FE0F = "🕵🏽‍♀️", "🕵🏽‍♀️ (:woman_detective_medium_skin_tone:)" + U1F5751F3FD200D2640 = "🕵🏽‍♀", "🕵🏽‍♀ (:woman_detective_medium_skin_tone:)" + U1F9DD200D2640FE0F = "🧝‍♀️", "🧝‍♀️ (:woman_elf:)" + U1F9DD200D2640 = "🧝‍♀", "🧝‍♀ (:woman_elf:)" + U1F9DD1F3FF200D2640FE0F = "🧝🏿‍♀️", "🧝🏿‍♀️ (:woman_elf_dark_skin_tone:)" + U1F9DD1F3FF200D2640 = "🧝🏿‍♀", "🧝🏿‍♀ (:woman_elf_dark_skin_tone:)" + U1F9DD1F3FB200D2640FE0F = "🧝🏻‍♀️", "🧝🏻‍♀️ (:woman_elf_light_skin_tone:)" + U1F9DD1F3FB200D2640 = "🧝🏻‍♀", "🧝🏻‍♀ (:woman_elf_light_skin_tone:)" + U1F9DD1F3FE200D2640FE0F = "🧝🏾‍♀️", "🧝🏾‍♀️ (:woman_elf_medium-dark_skin_tone:)" + U1F9DD1F3FE200D2640 = "🧝🏾‍♀", "🧝🏾‍♀ (:woman_elf_medium-dark_skin_tone:)" + U1F9DD1F3FC200D2640FE0F = "🧝🏼‍♀️", "🧝🏼‍♀️ (:woman_elf_medium-light_skin_tone:)" + U1F9DD1F3FC200D2640 = "🧝🏼‍♀", "🧝🏼‍♀ (:woman_elf_medium-light_skin_tone:)" + U1F9DD1F3FD200D2640FE0F = "🧝🏽‍♀️", "🧝🏽‍♀️ (:woman_elf_medium_skin_tone:)" + U1F9DD1F3FD200D2640 = "🧝🏽‍♀", "🧝🏽‍♀ (:woman_elf_medium_skin_tone:)" + U1F926200D2640FE0F = "🤦‍♀️", "🤦‍♀️ (:woman_facepalming:)" + U1F926200D2640 = "🤦‍♀", "🤦‍♀ (:woman_facepalming:)" + U1F9261F3FF200D2640FE0F = "🤦🏿‍♀️", "🤦🏿‍♀️ (:woman_facepalming_dark_skin_tone:)" + U1F9261F3FF200D2640 = "🤦🏿‍♀", "🤦🏿‍♀ (:woman_facepalming_dark_skin_tone:)" + U1F9261F3FB200D2640FE0F = "🤦🏻‍♀️", "🤦🏻‍♀️ (:woman_facepalming_light_skin_tone:)" + U1F9261F3FB200D2640 = "🤦🏻‍♀", "🤦🏻‍♀ (:woman_facepalming_light_skin_tone:)" + U1F9261F3FE200D2640FE0F = "🤦🏾‍♀️", "🤦🏾‍♀️ (:woman_facepalming_medium-dark_skin_tone:)" + U1F9261F3FE200D2640 = "🤦🏾‍♀", "🤦🏾‍♀ (:woman_facepalming_medium-dark_skin_tone:)" + U1F9261F3FC200D2640FE0F = "🤦🏼‍♀️", "🤦🏼‍♀️ (:woman_facepalming_medium-light_skin_tone:)" + U1F9261F3FC200D2640 = "🤦🏼‍♀", "🤦🏼‍♀ (:woman_facepalming_medium-light_skin_tone:)" + U1F9261F3FD200D2640FE0F = "🤦🏽‍♀️", "🤦🏽‍♀️ (:woman_facepalming_medium_skin_tone:)" + U1F9261F3FD200D2640 = "🤦🏽‍♀", "🤦🏽‍♀ (:woman_facepalming_medium_skin_tone:)" + U1F469200D1F3ED = "👩‍🏭", "👩‍🏭 (:woman_factory_worker:)" + U1F4691F3FF200D1F3ED = "👩🏿‍🏭", "👩🏿‍🏭 (:woman_factory_worker_dark_skin_tone:)" + U1F4691F3FB200D1F3ED = "👩🏻‍🏭", "👩🏻‍🏭 (:woman_factory_worker_light_skin_tone:)" + U1F4691F3FE200D1F3ED = "👩🏾‍🏭", "👩🏾‍🏭 (:woman_factory_worker_medium-dark_skin_tone:)" + U1F4691F3FC200D1F3ED = "👩🏼‍🏭", "👩🏼‍🏭 (:woman_factory_worker_medium-light_skin_tone:)" + U1F4691F3FD200D1F3ED = "👩🏽‍🏭", "👩🏽‍🏭 (:woman_factory_worker_medium_skin_tone:)" + U1F9DA200D2640FE0F = "🧚‍♀️", "🧚‍♀️ (:woman_fairy:)" + U1F9DA200D2640 = "🧚‍♀", "🧚‍♀ (:woman_fairy:)" + U1F9DA1F3FF200D2640FE0F = "🧚🏿‍♀️", "🧚🏿‍♀️ (:woman_fairy_dark_skin_tone:)" + U1F9DA1F3FF200D2640 = "🧚🏿‍♀", "🧚🏿‍♀ (:woman_fairy_dark_skin_tone:)" + U1F9DA1F3FB200D2640FE0F = "🧚🏻‍♀️", "🧚🏻‍♀️ (:woman_fairy_light_skin_tone:)" + U1F9DA1F3FB200D2640 = "🧚🏻‍♀", "🧚🏻‍♀ (:woman_fairy_light_skin_tone:)" + U1F9DA1F3FE200D2640FE0F = "🧚🏾‍♀️", "🧚🏾‍♀️ (:woman_fairy_medium-dark_skin_tone:)" + U1F9DA1F3FE200D2640 = "🧚🏾‍♀", "🧚🏾‍♀ (:woman_fairy_medium-dark_skin_tone:)" + U1F9DA1F3FC200D2640FE0F = "🧚🏼‍♀️", "🧚🏼‍♀️ (:woman_fairy_medium-light_skin_tone:)" + U1F9DA1F3FC200D2640 = "🧚🏼‍♀", "🧚🏼‍♀ (:woman_fairy_medium-light_skin_tone:)" + U1F9DA1F3FD200D2640FE0F = "🧚🏽‍♀️", "🧚🏽‍♀️ (:woman_fairy_medium_skin_tone:)" + U1F9DA1F3FD200D2640 = "🧚🏽‍♀", "🧚🏽‍♀ (:woman_fairy_medium_skin_tone:)" + U1F469200D1F33E = "👩‍🌾", "👩‍🌾 (:woman_farmer:)" + U1F4691F3FF200D1F33E = "👩🏿‍🌾", "👩🏿‍🌾 (:woman_farmer_dark_skin_tone:)" + U1F4691F3FB200D1F33E = "👩🏻‍🌾", "👩🏻‍🌾 (:woman_farmer_light_skin_tone:)" + U1F4691F3FE200D1F33E = "👩🏾‍🌾", "👩🏾‍🌾 (:woman_farmer_medium-dark_skin_tone:)" + U1F4691F3FC200D1F33E = "👩🏼‍🌾", "👩🏼‍🌾 (:woman_farmer_medium-light_skin_tone:)" + U1F4691F3FD200D1F33E = "👩🏽‍🌾", "👩🏽‍🌾 (:woman_farmer_medium_skin_tone:)" + U1F469200D1F37C = "👩‍🍼", "👩‍🍼 (:woman_feeding_baby:)" + U1F4691F3FF200D1F37C = "👩🏿‍🍼", "👩🏿‍🍼 (:woman_feeding_baby_dark_skin_tone:)" + U1F4691F3FB200D1F37C = "👩🏻‍🍼", "👩🏻‍🍼 (:woman_feeding_baby_light_skin_tone:)" + U1F4691F3FE200D1F37C = "👩🏾‍🍼", "👩🏾‍🍼 (:woman_feeding_baby_medium-dark_skin_tone:)" + U1F4691F3FC200D1F37C = "👩🏼‍🍼", "👩🏼‍🍼 (:woman_feeding_baby_medium-light_skin_tone:)" + U1F4691F3FD200D1F37C = "👩🏽‍🍼", "👩🏽‍🍼 (:woman_feeding_baby_medium_skin_tone:)" + U1F469200D1F692 = "👩‍🚒", "👩‍🚒 (:woman_firefighter:)" + U1F4691F3FF200D1F692 = "👩🏿‍🚒", "👩🏿‍🚒 (:woman_firefighter_dark_skin_tone:)" + U1F4691F3FB200D1F692 = "👩🏻‍🚒", "👩🏻‍🚒 (:woman_firefighter_light_skin_tone:)" + U1F4691F3FE200D1F692 = "👩🏾‍🚒", "👩🏾‍🚒 (:woman_firefighter_medium-dark_skin_tone:)" + U1F4691F3FC200D1F692 = "👩🏼‍🚒", "👩🏼‍🚒 (:woman_firefighter_medium-light_skin_tone:)" + U1F4691F3FD200D1F692 = "👩🏽‍🚒", "👩🏽‍🚒 (:woman_firefighter_medium_skin_tone:)" + U1F64D200D2640FE0F = "🙍‍♀️", "🙍‍♀️ (:woman_frowning:)" + U1F64D200D2640 = "🙍‍♀", "🙍‍♀ (:woman_frowning:)" + U1F64D1F3FF200D2640FE0F = "🙍🏿‍♀️", "🙍🏿‍♀️ (:woman_frowning_dark_skin_tone:)" + U1F64D1F3FF200D2640 = "🙍🏿‍♀", "🙍🏿‍♀ (:woman_frowning_dark_skin_tone:)" + U1F64D1F3FB200D2640FE0F = "🙍🏻‍♀️", "🙍🏻‍♀️ (:woman_frowning_light_skin_tone:)" + U1F64D1F3FB200D2640 = "🙍🏻‍♀", "🙍🏻‍♀ (:woman_frowning_light_skin_tone:)" + U1F64D1F3FE200D2640FE0F = "🙍🏾‍♀️", "🙍🏾‍♀️ (:woman_frowning_medium-dark_skin_tone:)" + U1F64D1F3FE200D2640 = "🙍🏾‍♀", "🙍🏾‍♀ (:woman_frowning_medium-dark_skin_tone:)" + U1F64D1F3FC200D2640FE0F = "🙍🏼‍♀️", "🙍🏼‍♀️ (:woman_frowning_medium-light_skin_tone:)" + U1F64D1F3FC200D2640 = "🙍🏼‍♀", "🙍🏼‍♀ (:woman_frowning_medium-light_skin_tone:)" + U1F64D1F3FD200D2640FE0F = "🙍🏽‍♀️", "🙍🏽‍♀️ (:woman_frowning_medium_skin_tone:)" + U1F64D1F3FD200D2640 = "🙍🏽‍♀", "🙍🏽‍♀ (:woman_frowning_medium_skin_tone:)" + U1F9DE200D2640FE0F = "🧞‍♀️", "🧞‍♀️ (:woman_genie:)" + U1F9DE200D2640 = "🧞‍♀", "🧞‍♀ (:woman_genie:)" + U1F645200D2640FE0F = "🙅‍♀️", "🙅‍♀️ (:woman_gesturing_NO:)" + U1F645200D2640 = "🙅‍♀", "🙅‍♀ (:woman_gesturing_NO:)" + U1F6451F3FF200D2640FE0F = "🙅🏿‍♀️", "🙅🏿‍♀️ (:woman_gesturing_NO_dark_skin_tone:)" + U1F6451F3FF200D2640 = "🙅🏿‍♀", "🙅🏿‍♀ (:woman_gesturing_NO_dark_skin_tone:)" + U1F6451F3FB200D2640FE0F = "🙅🏻‍♀️", "🙅🏻‍♀️ (:woman_gesturing_NO_light_skin_tone:)" + U1F6451F3FB200D2640 = "🙅🏻‍♀", "🙅🏻‍♀ (:woman_gesturing_NO_light_skin_tone:)" + U1F6451F3FE200D2640FE0F = "🙅🏾‍♀️", "🙅🏾‍♀️ (:woman_gesturing_NO_medium-dark_skin_tone:)" + U1F6451F3FE200D2640 = "🙅🏾‍♀", "🙅🏾‍♀ (:woman_gesturing_NO_medium-dark_skin_tone:)" + U1F6451F3FC200D2640FE0F = "🙅🏼‍♀️", "🙅🏼‍♀️ (:woman_gesturing_NO_medium-light_skin_tone:)" + U1F6451F3FC200D2640 = "🙅🏼‍♀", "🙅🏼‍♀ (:woman_gesturing_NO_medium-light_skin_tone:)" + U1F6451F3FD200D2640FE0F = "🙅🏽‍♀️", "🙅🏽‍♀️ (:woman_gesturing_NO_medium_skin_tone:)" + U1F6451F3FD200D2640 = "🙅🏽‍♀", "🙅🏽‍♀ (:woman_gesturing_NO_medium_skin_tone:)" + U1F646200D2640FE0F = "🙆‍♀️", "🙆‍♀️ (:woman_gesturing_OK:)" + U1F646200D2640 = "🙆‍♀", "🙆‍♀ (:woman_gesturing_OK:)" + U1F6461F3FF200D2640FE0F = "🙆🏿‍♀️", "🙆🏿‍♀️ (:woman_gesturing_OK_dark_skin_tone:)" + U1F6461F3FF200D2640 = "🙆🏿‍♀", "🙆🏿‍♀ (:woman_gesturing_OK_dark_skin_tone:)" + U1F6461F3FB200D2640FE0F = "🙆🏻‍♀️", "🙆🏻‍♀️ (:woman_gesturing_OK_light_skin_tone:)" + U1F6461F3FB200D2640 = "🙆🏻‍♀", "🙆🏻‍♀ (:woman_gesturing_OK_light_skin_tone:)" + U1F6461F3FE200D2640FE0F = "🙆🏾‍♀️", "🙆🏾‍♀️ (:woman_gesturing_OK_medium-dark_skin_tone:)" + U1F6461F3FE200D2640 = "🙆🏾‍♀", "🙆🏾‍♀ (:woman_gesturing_OK_medium-dark_skin_tone:)" + U1F6461F3FC200D2640FE0F = "🙆🏼‍♀️", "🙆🏼‍♀️ (:woman_gesturing_OK_medium-light_skin_tone:)" + U1F6461F3FC200D2640 = "🙆🏼‍♀", "🙆🏼‍♀ (:woman_gesturing_OK_medium-light_skin_tone:)" + U1F6461F3FD200D2640FE0F = "🙆🏽‍♀️", "🙆🏽‍♀️ (:woman_gesturing_OK_medium_skin_tone:)" + U1F6461F3FD200D2640 = "🙆🏽‍♀", "🙆🏽‍♀ (:woman_gesturing_OK_medium_skin_tone:)" + U1F487200D2640FE0F = "💇‍♀️", "💇‍♀️ (:woman_getting_haircut:)" + U1F487200D2640 = "💇‍♀", "💇‍♀ (:woman_getting_haircut:)" + U1F4871F3FF200D2640FE0F = "💇🏿‍♀️", "💇🏿‍♀️ (:woman_getting_haircut_dark_skin_tone:)" + U1F4871F3FF200D2640 = "💇🏿‍♀", "💇🏿‍♀ (:woman_getting_haircut_dark_skin_tone:)" + U1F4871F3FB200D2640FE0F = "💇🏻‍♀️", "💇🏻‍♀️ (:woman_getting_haircut_light_skin_tone:)" + U1F4871F3FB200D2640 = "💇🏻‍♀", "💇🏻‍♀ (:woman_getting_haircut_light_skin_tone:)" + U1F4871F3FE200D2640FE0F = "💇🏾‍♀️", "💇🏾‍♀️ (:woman_getting_haircut_medium-dark_skin_tone:)" + U1F4871F3FE200D2640 = "💇🏾‍♀", "💇🏾‍♀ (:woman_getting_haircut_medium-dark_skin_tone:)" + U1F4871F3FC200D2640FE0F = "💇🏼‍♀️", "💇🏼‍♀️ (:woman_getting_haircut_medium-light_skin_tone:)" + U1F4871F3FC200D2640 = "💇🏼‍♀", "💇🏼‍♀ (:woman_getting_haircut_medium-light_skin_tone:)" + U1F4871F3FD200D2640FE0F = "💇🏽‍♀️", "💇🏽‍♀️ (:woman_getting_haircut_medium_skin_tone:)" + U1F4871F3FD200D2640 = "💇🏽‍♀", "💇🏽‍♀ (:woman_getting_haircut_medium_skin_tone:)" + U1F486200D2640FE0F = "💆‍♀️", "💆‍♀️ (:woman_getting_massage:)" + U1F486200D2640 = "💆‍♀", "💆‍♀ (:woman_getting_massage:)" + U1F4861F3FF200D2640FE0F = "💆🏿‍♀️", "💆🏿‍♀️ (:woman_getting_massage_dark_skin_tone:)" + U1F4861F3FF200D2640 = "💆🏿‍♀", "💆🏿‍♀ (:woman_getting_massage_dark_skin_tone:)" + U1F4861F3FB200D2640FE0F = "💆🏻‍♀️", "💆🏻‍♀️ (:woman_getting_massage_light_skin_tone:)" + U1F4861F3FB200D2640 = "💆🏻‍♀", "💆🏻‍♀ (:woman_getting_massage_light_skin_tone:)" + U1F4861F3FE200D2640FE0F = "💆🏾‍♀️", "💆🏾‍♀️ (:woman_getting_massage_medium-dark_skin_tone:)" + U1F4861F3FE200D2640 = "💆🏾‍♀", "💆🏾‍♀ (:woman_getting_massage_medium-dark_skin_tone:)" + U1F4861F3FC200D2640FE0F = "💆🏼‍♀️", "💆🏼‍♀️ (:woman_getting_massage_medium-light_skin_tone:)" + U1F4861F3FC200D2640 = "💆🏼‍♀", "💆🏼‍♀ (:woman_getting_massage_medium-light_skin_tone:)" + U1F4861F3FD200D2640FE0F = "💆🏽‍♀️", "💆🏽‍♀️ (:woman_getting_massage_medium_skin_tone:)" + U1F4861F3FD200D2640 = "💆🏽‍♀", "💆🏽‍♀ (:woman_getting_massage_medium_skin_tone:)" + U1F3CCFE0F200D2640FE0F = "🏌️‍♀️", "🏌️‍♀️ (:woman_golfing:)" + U1F3CC200D2640FE0F = "🏌‍♀️", "🏌‍♀️ (:woman_golfing:)" + U1F3CCFE0F200D2640 = "🏌️‍♀", "🏌️‍♀ (:woman_golfing:)" + U1F3CC200D2640 = "🏌‍♀", "🏌‍♀ (:woman_golfing:)" + U1F3CC1F3FF200D2640FE0F = "🏌🏿‍♀️", "🏌🏿‍♀️ (:woman_golfing_dark_skin_tone:)" + U1F3CC1F3FF200D2640 = "🏌🏿‍♀", "🏌🏿‍♀ (:woman_golfing_dark_skin_tone:)" + U1F3CC1F3FB200D2640FE0F = "🏌🏻‍♀️", "🏌🏻‍♀️ (:woman_golfing_light_skin_tone:)" + U1F3CC1F3FB200D2640 = "🏌🏻‍♀", "🏌🏻‍♀ (:woman_golfing_light_skin_tone:)" + U1F3CC1F3FE200D2640FE0F = "🏌🏾‍♀️", "🏌🏾‍♀️ (:woman_golfing_medium-dark_skin_tone:)" + U1F3CC1F3FE200D2640 = "🏌🏾‍♀", "🏌🏾‍♀ (:woman_golfing_medium-dark_skin_tone:)" + U1F3CC1F3FC200D2640FE0F = "🏌🏼‍♀️", "🏌🏼‍♀️ (:woman_golfing_medium-light_skin_tone:)" + U1F3CC1F3FC200D2640 = "🏌🏼‍♀", "🏌🏼‍♀ (:woman_golfing_medium-light_skin_tone:)" + U1F3CC1F3FD200D2640FE0F = "🏌🏽‍♀️", "🏌🏽‍♀️ (:woman_golfing_medium_skin_tone:)" + U1F3CC1F3FD200D2640 = "🏌🏽‍♀", "🏌🏽‍♀ (:woman_golfing_medium_skin_tone:)" + U1F482200D2640FE0F = "💂‍♀️", "💂‍♀️ (:woman_guard:)" + U1F482200D2640 = "💂‍♀", "💂‍♀ (:woman_guard:)" + U1F4821F3FF200D2640FE0F = "💂🏿‍♀️", "💂🏿‍♀️ (:woman_guard_dark_skin_tone:)" + U1F4821F3FF200D2640 = "💂🏿‍♀", "💂🏿‍♀ (:woman_guard_dark_skin_tone:)" + U1F4821F3FB200D2640FE0F = "💂🏻‍♀️", "💂🏻‍♀️ (:woman_guard_light_skin_tone:)" + U1F4821F3FB200D2640 = "💂🏻‍♀", "💂🏻‍♀ (:woman_guard_light_skin_tone:)" + U1F4821F3FE200D2640FE0F = "💂🏾‍♀️", "💂🏾‍♀️ (:woman_guard_medium-dark_skin_tone:)" + U1F4821F3FE200D2640 = "💂🏾‍♀", "💂🏾‍♀ (:woman_guard_medium-dark_skin_tone:)" + U1F4821F3FC200D2640FE0F = "💂🏼‍♀️", "💂🏼‍♀️ (:woman_guard_medium-light_skin_tone:)" + U1F4821F3FC200D2640 = "💂🏼‍♀", "💂🏼‍♀ (:woman_guard_medium-light_skin_tone:)" + U1F4821F3FD200D2640FE0F = "💂🏽‍♀️", "💂🏽‍♀️ (:woman_guard_medium_skin_tone:)" + U1F4821F3FD200D2640 = "💂🏽‍♀", "💂🏽‍♀ (:woman_guard_medium_skin_tone:)" + U1F469200D2695FE0F = "👩‍⚕️", "👩‍⚕️ (:woman_health_worker:)" + U1F469200D2695 = "👩‍⚕", "👩‍⚕ (:woman_health_worker:)" + U1F4691F3FF200D2695FE0F = "👩🏿‍⚕️", "👩🏿‍⚕️ (:woman_health_worker_dark_skin_tone:)" + U1F4691F3FF200D2695 = "👩🏿‍⚕", "👩🏿‍⚕ (:woman_health_worker_dark_skin_tone:)" + U1F4691F3FB200D2695FE0F = "👩🏻‍⚕️", "👩🏻‍⚕️ (:woman_health_worker_light_skin_tone:)" + U1F4691F3FB200D2695 = "👩🏻‍⚕", "👩🏻‍⚕ (:woman_health_worker_light_skin_tone:)" + U1F4691F3FE200D2695FE0F = "👩🏾‍⚕️", "👩🏾‍⚕️ (:woman_health_worker_medium-dark_skin_tone:)" + U1F4691F3FE200D2695 = "👩🏾‍⚕", "👩🏾‍⚕ (:woman_health_worker_medium-dark_skin_tone:)" + U1F4691F3FC200D2695FE0F = "👩🏼‍⚕️", "👩🏼‍⚕️ (:woman_health_worker_medium-light_skin_tone:)" + U1F4691F3FC200D2695 = "👩🏼‍⚕", "👩🏼‍⚕ (:woman_health_worker_medium-light_skin_tone:)" + U1F4691F3FD200D2695FE0F = "👩🏽‍⚕️", "👩🏽‍⚕️ (:woman_health_worker_medium_skin_tone:)" + U1F4691F3FD200D2695 = "👩🏽‍⚕", "👩🏽‍⚕ (:woman_health_worker_medium_skin_tone:)" + U1F9D8200D2640FE0F = "🧘‍♀️", "🧘‍♀️ (:woman_in_lotus_position:)" + U1F9D8200D2640 = "🧘‍♀", "🧘‍♀ (:woman_in_lotus_position:)" + U1F9D81F3FF200D2640FE0F = "🧘🏿‍♀️", "🧘🏿‍♀️ (:woman_in_lotus_position_dark_skin_tone:)" + U1F9D81F3FF200D2640 = "🧘🏿‍♀", "🧘🏿‍♀ (:woman_in_lotus_position_dark_skin_tone:)" + U1F9D81F3FB200D2640FE0F = "🧘🏻‍♀️", "🧘🏻‍♀️ (:woman_in_lotus_position_light_skin_tone:)" + U1F9D81F3FB200D2640 = "🧘🏻‍♀", "🧘🏻‍♀ (:woman_in_lotus_position_light_skin_tone:)" + U1F9D81F3FE200D2640FE0F = "🧘🏾‍♀️", "🧘🏾‍♀️ (:woman_in_lotus_position_medium-dark_skin_tone:)" + U1F9D81F3FE200D2640 = "🧘🏾‍♀", "🧘🏾‍♀ (:woman_in_lotus_position_medium-dark_skin_tone:)" + U1F9D81F3FC200D2640FE0F = "🧘🏼‍♀️", "🧘🏼‍♀️ (:woman_in_lotus_position_medium-light_skin_tone:)" + U1F9D81F3FC200D2640 = "🧘🏼‍♀", "🧘🏼‍♀ (:woman_in_lotus_position_medium-light_skin_tone:)" + U1F9D81F3FD200D2640FE0F = "🧘🏽‍♀️", "🧘🏽‍♀️ (:woman_in_lotus_position_medium_skin_tone:)" + U1F9D81F3FD200D2640 = "🧘🏽‍♀", "🧘🏽‍♀ (:woman_in_lotus_position_medium_skin_tone:)" + U1F469200D1F9BD = "👩‍🦽", "👩‍🦽 (:woman_in_manual_wheelchair:)" + U1F4691F3FF200D1F9BD = "👩🏿‍🦽", "👩🏿‍🦽 (:woman_in_manual_wheelchair_dark_skin_tone:)" + U1F469200D1F9BD200D27A1FE0F = "👩‍🦽‍➡️", "👩‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right:)" + U1F469200D1F9BD200D27A1 = "👩‍🦽‍➡", "👩‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right:)" + U1F4691F3FF200D1F9BD200D27A1FE0F = "👩🏿‍🦽‍➡️", "👩🏿‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F4691F3FF200D1F9BD200D27A1 = "👩🏿‍🦽‍➡", "👩🏿‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right_dark_skin_tone:)" + U1F4691F3FB200D1F9BD200D27A1FE0F = "👩🏻‍🦽‍➡️", "👩🏻‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F4691F3FB200D1F9BD200D27A1 = "👩🏻‍🦽‍➡", "👩🏻‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right_light_skin_tone:)" + U1F4691F3FE200D1F9BD200D27A1FE0F = "👩🏾‍🦽‍➡️", "👩🏾‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4691F3FE200D1F9BD200D27A1 = "👩🏾‍🦽‍➡", "👩🏾‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9BD200D27A1FE0F = "👩🏼‍🦽‍➡️", "👩🏼‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4691F3FC200D1F9BD200D27A1 = "👩🏼‍🦽‍➡", "👩🏼‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4691F3FD200D1F9BD200D27A1FE0F = "👩🏽‍🦽‍➡️", "👩🏽‍🦽‍➡️ (:woman_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F4691F3FD200D1F9BD200D27A1 = "👩🏽‍🦽‍➡", "👩🏽‍🦽‍➡ (:woman_in_manual_wheelchair_facing_right_medium_skin_tone:)" + U1F4691F3FB200D1F9BD = "👩🏻‍🦽", "👩🏻‍🦽 (:woman_in_manual_wheelchair_light_skin_tone:)" + U1F4691F3FE200D1F9BD = "👩🏾‍🦽", "👩🏾‍🦽 (:woman_in_manual_wheelchair_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9BD = "👩🏼‍🦽", "👩🏼‍🦽 (:woman_in_manual_wheelchair_medium-light_skin_tone:)" + U1F4691F3FD200D1F9BD = "👩🏽‍🦽", "👩🏽‍🦽 (:woman_in_manual_wheelchair_medium_skin_tone:)" + U1F469200D1F9BC = "👩‍🦼", "👩‍🦼 (:woman_in_motorized_wheelchair:)" + U1F4691F3FF200D1F9BC = "👩🏿‍🦼", "👩🏿‍🦼 (:woman_in_motorized_wheelchair_dark_skin_tone:)" + U1F469200D1F9BC200D27A1FE0F = "👩‍🦼‍➡️", "👩‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right:)" + U1F469200D1F9BC200D27A1 = "👩‍🦼‍➡", "👩‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right:)" + U1F4691F3FF200D1F9BC200D27A1FE0F = "👩🏿‍🦼‍➡️", "👩🏿‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F4691F3FF200D1F9BC200D27A1 = "👩🏿‍🦼‍➡", "👩🏿‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right_dark_skin_tone:)" + U1F4691F3FB200D1F9BC200D27A1FE0F = "👩🏻‍🦼‍➡️", "👩🏻‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F4691F3FB200D1F9BC200D27A1 = "👩🏻‍🦼‍➡", "👩🏻‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right_light_skin_tone:)" + U1F4691F3FE200D1F9BC200D27A1FE0F = "👩🏾‍🦼‍➡️", "👩🏾‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4691F3FE200D1F9BC200D27A1 = "👩🏾‍🦼‍➡", "👩🏾‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9BC200D27A1FE0F = "👩🏼‍🦼‍➡️", "👩🏼‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4691F3FC200D1F9BC200D27A1 = "👩🏼‍🦼‍➡", "👩🏼‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)" + U1F4691F3FD200D1F9BC200D27A1FE0F = "👩🏽‍🦼‍➡️", "👩🏽‍🦼‍➡️ (:woman_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F4691F3FD200D1F9BC200D27A1 = "👩🏽‍🦼‍➡", "👩🏽‍🦼‍➡ (:woman_in_motorized_wheelchair_facing_right_medium_skin_tone:)" + U1F4691F3FB200D1F9BC = "👩🏻‍🦼", "👩🏻‍🦼 (:woman_in_motorized_wheelchair_light_skin_tone:)" + U1F4691F3FE200D1F9BC = "👩🏾‍🦼", "👩🏾‍🦼 (:woman_in_motorized_wheelchair_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9BC = "👩🏼‍🦼", "👩🏼‍🦼 (:woman_in_motorized_wheelchair_medium-light_skin_tone:)" + U1F4691F3FD200D1F9BC = "👩🏽‍🦼", "👩🏽‍🦼 (:woman_in_motorized_wheelchair_medium_skin_tone:)" + U1F9D6200D2640FE0F = "🧖‍♀️", "🧖‍♀️ (:woman_in_steamy_room:)" + U1F9D6200D2640 = "🧖‍♀", "🧖‍♀ (:woman_in_steamy_room:)" + U1F9D61F3FF200D2640FE0F = "🧖🏿‍♀️", "🧖🏿‍♀️ (:woman_in_steamy_room_dark_skin_tone:)" + U1F9D61F3FF200D2640 = "🧖🏿‍♀", "🧖🏿‍♀ (:woman_in_steamy_room_dark_skin_tone:)" + U1F9D61F3FB200D2640FE0F = "🧖🏻‍♀️", "🧖🏻‍♀️ (:woman_in_steamy_room_light_skin_tone:)" + U1F9D61F3FB200D2640 = "🧖🏻‍♀", "🧖🏻‍♀ (:woman_in_steamy_room_light_skin_tone:)" + U1F9D61F3FE200D2640FE0F = "🧖🏾‍♀️", "🧖🏾‍♀️ (:woman_in_steamy_room_medium-dark_skin_tone:)" + U1F9D61F3FE200D2640 = "🧖🏾‍♀", "🧖🏾‍♀ (:woman_in_steamy_room_medium-dark_skin_tone:)" + U1F9D61F3FC200D2640FE0F = "🧖🏼‍♀️", "🧖🏼‍♀️ (:woman_in_steamy_room_medium-light_skin_tone:)" + U1F9D61F3FC200D2640 = "🧖🏼‍♀", "🧖🏼‍♀ (:woman_in_steamy_room_medium-light_skin_tone:)" + U1F9D61F3FD200D2640FE0F = "🧖🏽‍♀️", "🧖🏽‍♀️ (:woman_in_steamy_room_medium_skin_tone:)" + U1F9D61F3FD200D2640 = "🧖🏽‍♀", "🧖🏽‍♀ (:woman_in_steamy_room_medium_skin_tone:)" + U1F935200D2640FE0F = "🤵‍♀️", "🤵‍♀️ (:woman_in_tuxedo:)" + U1F935200D2640 = "🤵‍♀", "🤵‍♀ (:woman_in_tuxedo:)" + U1F9351F3FF200D2640FE0F = "🤵🏿‍♀️", "🤵🏿‍♀️ (:woman_in_tuxedo_dark_skin_tone:)" + U1F9351F3FF200D2640 = "🤵🏿‍♀", "🤵🏿‍♀ (:woman_in_tuxedo_dark_skin_tone:)" + U1F9351F3FB200D2640FE0F = "🤵🏻‍♀️", "🤵🏻‍♀️ (:woman_in_tuxedo_light_skin_tone:)" + U1F9351F3FB200D2640 = "🤵🏻‍♀", "🤵🏻‍♀ (:woman_in_tuxedo_light_skin_tone:)" + U1F9351F3FE200D2640FE0F = "🤵🏾‍♀️", "🤵🏾‍♀️ (:woman_in_tuxedo_medium-dark_skin_tone:)" + U1F9351F3FE200D2640 = "🤵🏾‍♀", "🤵🏾‍♀ (:woman_in_tuxedo_medium-dark_skin_tone:)" + U1F9351F3FC200D2640FE0F = "🤵🏼‍♀️", "🤵🏼‍♀️ (:woman_in_tuxedo_medium-light_skin_tone:)" + U1F9351F3FC200D2640 = "🤵🏼‍♀", "🤵🏼‍♀ (:woman_in_tuxedo_medium-light_skin_tone:)" + U1F9351F3FD200D2640FE0F = "🤵🏽‍♀️", "🤵🏽‍♀️ (:woman_in_tuxedo_medium_skin_tone:)" + U1F9351F3FD200D2640 = "🤵🏽‍♀", "🤵🏽‍♀ (:woman_in_tuxedo_medium_skin_tone:)" + U1F469200D2696FE0F = "👩‍⚖️", "👩‍⚖️ (:woman_judge:)" + U1F469200D2696 = "👩‍⚖", "👩‍⚖ (:woman_judge:)" + U1F4691F3FF200D2696FE0F = "👩🏿‍⚖️", "👩🏿‍⚖️ (:woman_judge_dark_skin_tone:)" + U1F4691F3FF200D2696 = "👩🏿‍⚖", "👩🏿‍⚖ (:woman_judge_dark_skin_tone:)" + U1F4691F3FB200D2696FE0F = "👩🏻‍⚖️", "👩🏻‍⚖️ (:woman_judge_light_skin_tone:)" + U1F4691F3FB200D2696 = "👩🏻‍⚖", "👩🏻‍⚖ (:woman_judge_light_skin_tone:)" + U1F4691F3FE200D2696FE0F = "👩🏾‍⚖️", "👩🏾‍⚖️ (:woman_judge_medium-dark_skin_tone:)" + U1F4691F3FE200D2696 = "👩🏾‍⚖", "👩🏾‍⚖ (:woman_judge_medium-dark_skin_tone:)" + U1F4691F3FC200D2696FE0F = "👩🏼‍⚖️", "👩🏼‍⚖️ (:woman_judge_medium-light_skin_tone:)" + U1F4691F3FC200D2696 = "👩🏼‍⚖", "👩🏼‍⚖ (:woman_judge_medium-light_skin_tone:)" + U1F4691F3FD200D2696FE0F = "👩🏽‍⚖️", "👩🏽‍⚖️ (:woman_judge_medium_skin_tone:)" + U1F4691F3FD200D2696 = "👩🏽‍⚖", "👩🏽‍⚖ (:woman_judge_medium_skin_tone:)" + U1F939200D2640FE0F = "🤹‍♀️", "🤹‍♀️ (:woman_juggling:)" + U1F939200D2640 = "🤹‍♀", "🤹‍♀ (:woman_juggling:)" + U1F9391F3FF200D2640FE0F = "🤹🏿‍♀️", "🤹🏿‍♀️ (:woman_juggling_dark_skin_tone:)" + U1F9391F3FF200D2640 = "🤹🏿‍♀", "🤹🏿‍♀ (:woman_juggling_dark_skin_tone:)" + U1F9391F3FB200D2640FE0F = "🤹🏻‍♀️", "🤹🏻‍♀️ (:woman_juggling_light_skin_tone:)" + U1F9391F3FB200D2640 = "🤹🏻‍♀", "🤹🏻‍♀ (:woman_juggling_light_skin_tone:)" + U1F9391F3FE200D2640FE0F = "🤹🏾‍♀️", "🤹🏾‍♀️ (:woman_juggling_medium-dark_skin_tone:)" + U1F9391F3FE200D2640 = "🤹🏾‍♀", "🤹🏾‍♀ (:woman_juggling_medium-dark_skin_tone:)" + U1F9391F3FC200D2640FE0F = "🤹🏼‍♀️", "🤹🏼‍♀️ (:woman_juggling_medium-light_skin_tone:)" + U1F9391F3FC200D2640 = "🤹🏼‍♀", "🤹🏼‍♀ (:woman_juggling_medium-light_skin_tone:)" + U1F9391F3FD200D2640FE0F = "🤹🏽‍♀️", "🤹🏽‍♀️ (:woman_juggling_medium_skin_tone:)" + U1F9391F3FD200D2640 = "🤹🏽‍♀", "🤹🏽‍♀ (:woman_juggling_medium_skin_tone:)" + U1F9CE200D2640FE0F = "🧎‍♀️", "🧎‍♀️ (:woman_kneeling:)" + U1F9CE200D2640 = "🧎‍♀", "🧎‍♀ (:woman_kneeling:)" + U1F9CE1F3FF200D2640FE0F = "🧎🏿‍♀️", "🧎🏿‍♀️ (:woman_kneeling_dark_skin_tone:)" + U1F9CE1F3FF200D2640 = "🧎🏿‍♀", "🧎🏿‍♀ (:woman_kneeling_dark_skin_tone:)" + U1F9CE200D2640FE0F200D27A1FE0F = "🧎‍♀️‍➡️", "🧎‍♀️‍➡️ (:woman_kneeling_facing_right:)" + U1F9CE200D2640200D27A1FE0F = "🧎‍♀‍➡️", "🧎‍♀‍➡️ (:woman_kneeling_facing_right:)" + U1F9CE200D2640FE0F200D27A1 = "🧎‍♀️‍➡", "🧎‍♀️‍➡ (:woman_kneeling_facing_right:)" + U1F9CE200D2640200D27A1 = "🧎‍♀‍➡", "🧎‍♀‍➡ (:woman_kneeling_facing_right:)" + U1F9CE1F3FF200D2640FE0F200D27A1FE0F = "🧎🏿‍♀️‍➡️", "🧎🏿‍♀️‍➡️ (:woman_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2640200D27A1FE0F = "🧎🏿‍♀‍➡️", "🧎🏿‍♀‍➡️ (:woman_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2640FE0F200D27A1 = "🧎🏿‍♀️‍➡", "🧎🏿‍♀️‍➡ (:woman_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FF200D2640200D27A1 = "🧎🏿‍♀‍➡", "🧎🏿‍♀‍➡ (:woman_kneeling_facing_right_dark_skin_tone:)" + U1F9CE1F3FB200D2640FE0F200D27A1FE0F = "🧎🏻‍♀️‍➡️", "🧎🏻‍♀️‍➡️ (:woman_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2640200D27A1FE0F = "🧎🏻‍♀‍➡️", "🧎🏻‍♀‍➡️ (:woman_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2640FE0F200D27A1 = "🧎🏻‍♀️‍➡", "🧎🏻‍♀️‍➡ (:woman_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FB200D2640200D27A1 = "🧎🏻‍♀‍➡", "🧎🏻‍♀‍➡ (:woman_kneeling_facing_right_light_skin_tone:)" + U1F9CE1F3FE200D2640FE0F200D27A1FE0F = "🧎🏾‍♀️‍➡️", "🧎🏾‍♀️‍➡️ (:woman_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2640200D27A1FE0F = "🧎🏾‍♀‍➡️", "🧎🏾‍♀‍➡️ (:woman_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2640FE0F200D27A1 = "🧎🏾‍♀️‍➡", "🧎🏾‍♀️‍➡ (:woman_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2640200D27A1 = "🧎🏾‍♀‍➡", "🧎🏾‍♀‍➡ (:woman_kneeling_facing_right_medium-dark_skin_tone:)" + U1F9CE1F3FC200D2640FE0F200D27A1FE0F = "🧎🏼‍♀️‍➡️", "🧎🏼‍♀️‍➡️ (:woman_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2640200D27A1FE0F = "🧎🏼‍♀‍➡️", "🧎🏼‍♀‍➡️ (:woman_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2640FE0F200D27A1 = "🧎🏼‍♀️‍➡", "🧎🏼‍♀️‍➡ (:woman_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FC200D2640200D27A1 = "🧎🏼‍♀‍➡", "🧎🏼‍♀‍➡ (:woman_kneeling_facing_right_medium-light_skin_tone:)" + U1F9CE1F3FD200D2640FE0F200D27A1FE0F = "🧎🏽‍♀️‍➡️", "🧎🏽‍♀️‍➡️ (:woman_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2640200D27A1FE0F = "🧎🏽‍♀‍➡️", "🧎🏽‍♀‍➡️ (:woman_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2640FE0F200D27A1 = "🧎🏽‍♀️‍➡", "🧎🏽‍♀️‍➡ (:woman_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FD200D2640200D27A1 = "🧎🏽‍♀‍➡", "🧎🏽‍♀‍➡ (:woman_kneeling_facing_right_medium_skin_tone:)" + U1F9CE1F3FB200D2640FE0F = "🧎🏻‍♀️", "🧎🏻‍♀️ (:woman_kneeling_light_skin_tone:)" + U1F9CE1F3FB200D2640 = "🧎🏻‍♀", "🧎🏻‍♀ (:woman_kneeling_light_skin_tone:)" + U1F9CE1F3FE200D2640FE0F = "🧎🏾‍♀️", "🧎🏾‍♀️ (:woman_kneeling_medium-dark_skin_tone:)" + U1F9CE1F3FE200D2640 = "🧎🏾‍♀", "🧎🏾‍♀ (:woman_kneeling_medium-dark_skin_tone:)" + U1F9CE1F3FC200D2640FE0F = "🧎🏼‍♀️", "🧎🏼‍♀️ (:woman_kneeling_medium-light_skin_tone:)" + U1F9CE1F3FC200D2640 = "🧎🏼‍♀", "🧎🏼‍♀ (:woman_kneeling_medium-light_skin_tone:)" + U1F9CE1F3FD200D2640FE0F = "🧎🏽‍♀️", "🧎🏽‍♀️ (:woman_kneeling_medium_skin_tone:)" + U1F9CE1F3FD200D2640 = "🧎🏽‍♀", "🧎🏽‍♀ (:woman_kneeling_medium_skin_tone:)" + U1F3CBFE0F200D2640FE0F = "🏋️‍♀️", "🏋️‍♀️ (:woman_lifting_weights:)" + U1F3CB200D2640FE0F = "🏋‍♀️", "🏋‍♀️ (:woman_lifting_weights:)" + U1F3CBFE0F200D2640 = "🏋️‍♀", "🏋️‍♀ (:woman_lifting_weights:)" + U1F3CB200D2640 = "🏋‍♀", "🏋‍♀ (:woman_lifting_weights:)" + U1F3CB1F3FF200D2640FE0F = "🏋🏿‍♀️", "🏋🏿‍♀️ (:woman_lifting_weights_dark_skin_tone:)" + U1F3CB1F3FF200D2640 = "🏋🏿‍♀", "🏋🏿‍♀ (:woman_lifting_weights_dark_skin_tone:)" + U1F3CB1F3FB200D2640FE0F = "🏋🏻‍♀️", "🏋🏻‍♀️ (:woman_lifting_weights_light_skin_tone:)" + U1F3CB1F3FB200D2640 = "🏋🏻‍♀", "🏋🏻‍♀ (:woman_lifting_weights_light_skin_tone:)" + U1F3CB1F3FE200D2640FE0F = "🏋🏾‍♀️", "🏋🏾‍♀️ (:woman_lifting_weights_medium-dark_skin_tone:)" + U1F3CB1F3FE200D2640 = "🏋🏾‍♀", "🏋🏾‍♀ (:woman_lifting_weights_medium-dark_skin_tone:)" + U1F3CB1F3FC200D2640FE0F = "🏋🏼‍♀️", "🏋🏼‍♀️ (:woman_lifting_weights_medium-light_skin_tone:)" + U1F3CB1F3FC200D2640 = "🏋🏼‍♀", "🏋🏼‍♀ (:woman_lifting_weights_medium-light_skin_tone:)" + U1F3CB1F3FD200D2640FE0F = "🏋🏽‍♀️", "🏋🏽‍♀️ (:woman_lifting_weights_medium_skin_tone:)" + U1F3CB1F3FD200D2640 = "🏋🏽‍♀", "🏋🏽‍♀ (:woman_lifting_weights_medium_skin_tone:)" + U1F4691F3FB = "👩🏻", "👩🏻 (:woman_light_skin_tone:)" + U1F4691F3FB200D1F9B2 = "👩🏻‍🦲", "👩🏻‍🦲 (:woman_light_skin_tone_bald:)" + U1F9D41F3FB200D2640FE0F = "🧔🏻‍♀️", "🧔🏻‍♀️ (:woman_light_skin_tone_beard:)" + U1F9D41F3FB200D2640 = "🧔🏻‍♀", "🧔🏻‍♀ (:woman_light_skin_tone_beard:)" + U1F4711F3FB200D2640FE0F = "👱🏻‍♀️", "👱🏻‍♀️ (:woman_light_skin_tone_blond_hair:)" + U1F4711F3FB200D2640 = "👱🏻‍♀", "👱🏻‍♀ (:woman_light_skin_tone_blond_hair:)" + U1F4691F3FB200D1F9B1 = "👩🏻‍🦱", "👩🏻‍🦱 (:woman_light_skin_tone_curly_hair:)" + U1F4691F3FB200D1F9B0 = "👩🏻‍🦰", "👩🏻‍🦰 (:woman_light_skin_tone_red_hair:)" + U1F4691F3FB200D1F9B3 = "👩🏻‍🦳", "👩🏻‍🦳 (:woman_light_skin_tone_white_hair:)" + U1F9D9200D2640FE0F = "🧙‍♀️", "🧙‍♀️ (:woman_mage:)" + U1F9D9200D2640 = "🧙‍♀", "🧙‍♀ (:woman_mage:)" + U1F9D91F3FF200D2640FE0F = "🧙🏿‍♀️", "🧙🏿‍♀️ (:woman_mage_dark_skin_tone:)" + U1F9D91F3FF200D2640 = "🧙🏿‍♀", "🧙🏿‍♀ (:woman_mage_dark_skin_tone:)" + U1F9D91F3FB200D2640FE0F = "🧙🏻‍♀️", "🧙🏻‍♀️ (:woman_mage_light_skin_tone:)" + U1F9D91F3FB200D2640 = "🧙🏻‍♀", "🧙🏻‍♀ (:woman_mage_light_skin_tone:)" + U1F9D91F3FE200D2640FE0F = "🧙🏾‍♀️", "🧙🏾‍♀️ (:woman_mage_medium-dark_skin_tone:)" + U1F9D91F3FE200D2640 = "🧙🏾‍♀", "🧙🏾‍♀ (:woman_mage_medium-dark_skin_tone:)" + U1F9D91F3FC200D2640FE0F = "🧙🏼‍♀️", "🧙🏼‍♀️ (:woman_mage_medium-light_skin_tone:)" + U1F9D91F3FC200D2640 = "🧙🏼‍♀", "🧙🏼‍♀ (:woman_mage_medium-light_skin_tone:)" + U1F9D91F3FD200D2640FE0F = "🧙🏽‍♀️", "🧙🏽‍♀️ (:woman_mage_medium_skin_tone:)" + U1F9D91F3FD200D2640 = "🧙🏽‍♀", "🧙🏽‍♀ (:woman_mage_medium_skin_tone:)" + U1F469200D1F527 = "👩‍🔧", "👩‍🔧 (:woman_mechanic:)" + U1F4691F3FF200D1F527 = "👩🏿‍🔧", "👩🏿‍🔧 (:woman_mechanic_dark_skin_tone:)" + U1F4691F3FB200D1F527 = "👩🏻‍🔧", "👩🏻‍🔧 (:woman_mechanic_light_skin_tone:)" + U1F4691F3FE200D1F527 = "👩🏾‍🔧", "👩🏾‍🔧 (:woman_mechanic_medium-dark_skin_tone:)" + U1F4691F3FC200D1F527 = "👩🏼‍🔧", "👩🏼‍🔧 (:woman_mechanic_medium-light_skin_tone:)" + U1F4691F3FD200D1F527 = "👩🏽‍🔧", "👩🏽‍🔧 (:woman_mechanic_medium_skin_tone:)" + U1F4691F3FE = "👩🏾", "👩🏾 (:woman_medium-dark_skin_tone:)" + U1F4691F3FE200D1F9B2 = "👩🏾‍🦲", "👩🏾‍🦲 (:woman_medium-dark_skin_tone_bald:)" + U1F9D41F3FE200D2640FE0F = "🧔🏾‍♀️", "🧔🏾‍♀️ (:woman_medium-dark_skin_tone_beard:)" + U1F9D41F3FE200D2640 = "🧔🏾‍♀", "🧔🏾‍♀ (:woman_medium-dark_skin_tone_beard:)" + U1F4711F3FE200D2640FE0F = "👱🏾‍♀️", "👱🏾‍♀️ (:woman_medium-dark_skin_tone_blond_hair:)" + U1F4711F3FE200D2640 = "👱🏾‍♀", "👱🏾‍♀ (:woman_medium-dark_skin_tone_blond_hair:)" + U1F4691F3FE200D1F9B1 = "👩🏾‍🦱", "👩🏾‍🦱 (:woman_medium-dark_skin_tone_curly_hair:)" + U1F4691F3FE200D1F9B0 = "👩🏾‍🦰", "👩🏾‍🦰 (:woman_medium-dark_skin_tone_red_hair:)" + U1F4691F3FE200D1F9B3 = "👩🏾‍🦳", "👩🏾‍🦳 (:woman_medium-dark_skin_tone_white_hair:)" + U1F4691F3FC = "👩🏼", "👩🏼 (:woman_medium-light_skin_tone:)" + U1F4691F3FC200D1F9B2 = "👩🏼‍🦲", "👩🏼‍🦲 (:woman_medium-light_skin_tone_bald:)" + U1F9D41F3FC200D2640FE0F = "🧔🏼‍♀️", "🧔🏼‍♀️ (:woman_medium-light_skin_tone_beard:)" + U1F9D41F3FC200D2640 = "🧔🏼‍♀", "🧔🏼‍♀ (:woman_medium-light_skin_tone_beard:)" + U1F4711F3FC200D2640FE0F = "👱🏼‍♀️", "👱🏼‍♀️ (:woman_medium-light_skin_tone_blond_hair:)" + U1F4711F3FC200D2640 = "👱🏼‍♀", "👱🏼‍♀ (:woman_medium-light_skin_tone_blond_hair:)" + U1F4691F3FC200D1F9B1 = "👩🏼‍🦱", "👩🏼‍🦱 (:woman_medium-light_skin_tone_curly_hair:)" + U1F4691F3FC200D1F9B0 = "👩🏼‍🦰", "👩🏼‍🦰 (:woman_medium-light_skin_tone_red_hair:)" + U1F4691F3FC200D1F9B3 = "👩🏼‍🦳", "👩🏼‍🦳 (:woman_medium-light_skin_tone_white_hair:)" + U1F4691F3FD = "👩🏽", "👩🏽 (:woman_medium_skin_tone:)" + U1F4691F3FD200D1F9B2 = "👩🏽‍🦲", "👩🏽‍🦲 (:woman_medium_skin_tone_bald:)" + U1F9D41F3FD200D2640FE0F = "🧔🏽‍♀️", "🧔🏽‍♀️ (:woman_medium_skin_tone_beard:)" + U1F9D41F3FD200D2640 = "🧔🏽‍♀", "🧔🏽‍♀ (:woman_medium_skin_tone_beard:)" + U1F4711F3FD200D2640FE0F = "👱🏽‍♀️", "👱🏽‍♀️ (:woman_medium_skin_tone_blond_hair:)" + U1F4711F3FD200D2640 = "👱🏽‍♀", "👱🏽‍♀ (:woman_medium_skin_tone_blond_hair:)" + U1F4691F3FD200D1F9B1 = "👩🏽‍🦱", "👩🏽‍🦱 (:woman_medium_skin_tone_curly_hair:)" + U1F4691F3FD200D1F9B0 = "👩🏽‍🦰", "👩🏽‍🦰 (:woman_medium_skin_tone_red_hair:)" + U1F4691F3FD200D1F9B3 = "👩🏽‍🦳", "👩🏽‍🦳 (:woman_medium_skin_tone_white_hair:)" + U1F6B5200D2640FE0F = "🚵‍♀️", "🚵‍♀️ (:woman_mountain_biking:)" + U1F6B5200D2640 = "🚵‍♀", "🚵‍♀ (:woman_mountain_biking:)" + U1F6B51F3FF200D2640FE0F = "🚵🏿‍♀️", "🚵🏿‍♀️ (:woman_mountain_biking_dark_skin_tone:)" + U1F6B51F3FF200D2640 = "🚵🏿‍♀", "🚵🏿‍♀ (:woman_mountain_biking_dark_skin_tone:)" + U1F6B51F3FB200D2640FE0F = "🚵🏻‍♀️", "🚵🏻‍♀️ (:woman_mountain_biking_light_skin_tone:)" + U1F6B51F3FB200D2640 = "🚵🏻‍♀", "🚵🏻‍♀ (:woman_mountain_biking_light_skin_tone:)" + U1F6B51F3FE200D2640FE0F = "🚵🏾‍♀️", "🚵🏾‍♀️ (:woman_mountain_biking_medium-dark_skin_tone:)" + U1F6B51F3FE200D2640 = "🚵🏾‍♀", "🚵🏾‍♀ (:woman_mountain_biking_medium-dark_skin_tone:)" + U1F6B51F3FC200D2640FE0F = "🚵🏼‍♀️", "🚵🏼‍♀️ (:woman_mountain_biking_medium-light_skin_tone:)" + U1F6B51F3FC200D2640 = "🚵🏼‍♀", "🚵🏼‍♀ (:woman_mountain_biking_medium-light_skin_tone:)" + U1F6B51F3FD200D2640FE0F = "🚵🏽‍♀️", "🚵🏽‍♀️ (:woman_mountain_biking_medium_skin_tone:)" + U1F6B51F3FD200D2640 = "🚵🏽‍♀", "🚵🏽‍♀ (:woman_mountain_biking_medium_skin_tone:)" + U1F469200D1F4BC = "👩‍💼", "👩‍💼 (:woman_office_worker:)" + U1F4691F3FF200D1F4BC = "👩🏿‍💼", "👩🏿‍💼 (:woman_office_worker_dark_skin_tone:)" + U1F4691F3FB200D1F4BC = "👩🏻‍💼", "👩🏻‍💼 (:woman_office_worker_light_skin_tone:)" + U1F4691F3FE200D1F4BC = "👩🏾‍💼", "👩🏾‍💼 (:woman_office_worker_medium-dark_skin_tone:)" + U1F4691F3FC200D1F4BC = "👩🏼‍💼", "👩🏼‍💼 (:woman_office_worker_medium-light_skin_tone:)" + U1F4691F3FD200D1F4BC = "👩🏽‍💼", "👩🏽‍💼 (:woman_office_worker_medium_skin_tone:)" + U1F469200D2708FE0F = "👩‍✈️", "👩‍✈️ (:woman_pilot:)" + U1F469200D2708 = "👩‍✈", "👩‍✈ (:woman_pilot:)" + U1F4691F3FF200D2708FE0F = "👩🏿‍✈️", "👩🏿‍✈️ (:woman_pilot_dark_skin_tone:)" + U1F4691F3FF200D2708 = "👩🏿‍✈", "👩🏿‍✈ (:woman_pilot_dark_skin_tone:)" + U1F4691F3FB200D2708FE0F = "👩🏻‍✈️", "👩🏻‍✈️ (:woman_pilot_light_skin_tone:)" + U1F4691F3FB200D2708 = "👩🏻‍✈", "👩🏻‍✈ (:woman_pilot_light_skin_tone:)" + U1F4691F3FE200D2708FE0F = "👩🏾‍✈️", "👩🏾‍✈️ (:woman_pilot_medium-dark_skin_tone:)" + U1F4691F3FE200D2708 = "👩🏾‍✈", "👩🏾‍✈ (:woman_pilot_medium-dark_skin_tone:)" + U1F4691F3FC200D2708FE0F = "👩🏼‍✈️", "👩🏼‍✈️ (:woman_pilot_medium-light_skin_tone:)" + U1F4691F3FC200D2708 = "👩🏼‍✈", "👩🏼‍✈ (:woman_pilot_medium-light_skin_tone:)" + U1F4691F3FD200D2708FE0F = "👩🏽‍✈️", "👩🏽‍✈️ (:woman_pilot_medium_skin_tone:)" + U1F4691F3FD200D2708 = "👩🏽‍✈", "👩🏽‍✈ (:woman_pilot_medium_skin_tone:)" + U1F93E200D2640FE0F = "🤾‍♀️", "🤾‍♀️ (:woman_playing_handball:)" + U1F93E200D2640 = "🤾‍♀", "🤾‍♀ (:woman_playing_handball:)" + U1F93E1F3FF200D2640FE0F = "🤾🏿‍♀️", "🤾🏿‍♀️ (:woman_playing_handball_dark_skin_tone:)" + U1F93E1F3FF200D2640 = "🤾🏿‍♀", "🤾🏿‍♀ (:woman_playing_handball_dark_skin_tone:)" + U1F93E1F3FB200D2640FE0F = "🤾🏻‍♀️", "🤾🏻‍♀️ (:woman_playing_handball_light_skin_tone:)" + U1F93E1F3FB200D2640 = "🤾🏻‍♀", "🤾🏻‍♀ (:woman_playing_handball_light_skin_tone:)" + U1F93E1F3FE200D2640FE0F = "🤾🏾‍♀️", "🤾🏾‍♀️ (:woman_playing_handball_medium-dark_skin_tone:)" + U1F93E1F3FE200D2640 = "🤾🏾‍♀", "🤾🏾‍♀ (:woman_playing_handball_medium-dark_skin_tone:)" + U1F93E1F3FC200D2640FE0F = "🤾🏼‍♀️", "🤾🏼‍♀️ (:woman_playing_handball_medium-light_skin_tone:)" + U1F93E1F3FC200D2640 = "🤾🏼‍♀", "🤾🏼‍♀ (:woman_playing_handball_medium-light_skin_tone:)" + U1F93E1F3FD200D2640FE0F = "🤾🏽‍♀️", "🤾🏽‍♀️ (:woman_playing_handball_medium_skin_tone:)" + U1F93E1F3FD200D2640 = "🤾🏽‍♀", "🤾🏽‍♀ (:woman_playing_handball_medium_skin_tone:)" + U1F93D200D2640FE0F = "🤽‍♀️", "🤽‍♀️ (:woman_playing_water_polo:)" + U1F93D200D2640 = "🤽‍♀", "🤽‍♀ (:woman_playing_water_polo:)" + U1F93D1F3FF200D2640FE0F = "🤽🏿‍♀️", "🤽🏿‍♀️ (:woman_playing_water_polo_dark_skin_tone:)" + U1F93D1F3FF200D2640 = "🤽🏿‍♀", "🤽🏿‍♀ (:woman_playing_water_polo_dark_skin_tone:)" + U1F93D1F3FB200D2640FE0F = "🤽🏻‍♀️", "🤽🏻‍♀️ (:woman_playing_water_polo_light_skin_tone:)" + U1F93D1F3FB200D2640 = "🤽🏻‍♀", "🤽🏻‍♀ (:woman_playing_water_polo_light_skin_tone:)" + U1F93D1F3FE200D2640FE0F = "🤽🏾‍♀️", "🤽🏾‍♀️ (:woman_playing_water_polo_medium-dark_skin_tone:)" + U1F93D1F3FE200D2640 = "🤽🏾‍♀", "🤽🏾‍♀ (:woman_playing_water_polo_medium-dark_skin_tone:)" + U1F93D1F3FC200D2640FE0F = "🤽🏼‍♀️", "🤽🏼‍♀️ (:woman_playing_water_polo_medium-light_skin_tone:)" + U1F93D1F3FC200D2640 = "🤽🏼‍♀", "🤽🏼‍♀ (:woman_playing_water_polo_medium-light_skin_tone:)" + U1F93D1F3FD200D2640FE0F = "🤽🏽‍♀️", "🤽🏽‍♀️ (:woman_playing_water_polo_medium_skin_tone:)" + U1F93D1F3FD200D2640 = "🤽🏽‍♀", "🤽🏽‍♀ (:woman_playing_water_polo_medium_skin_tone:)" + U1F46E200D2640FE0F = "👮‍♀️", "👮‍♀️ (:woman_police_officer:)" + U1F46E200D2640 = "👮‍♀", "👮‍♀ (:woman_police_officer:)" + U1F46E1F3FF200D2640FE0F = "👮🏿‍♀️", "👮🏿‍♀️ (:woman_police_officer_dark_skin_tone:)" + U1F46E1F3FF200D2640 = "👮🏿‍♀", "👮🏿‍♀ (:woman_police_officer_dark_skin_tone:)" + U1F46E1F3FB200D2640FE0F = "👮🏻‍♀️", "👮🏻‍♀️ (:woman_police_officer_light_skin_tone:)" + U1F46E1F3FB200D2640 = "👮🏻‍♀", "👮🏻‍♀ (:woman_police_officer_light_skin_tone:)" + U1F46E1F3FE200D2640FE0F = "👮🏾‍♀️", "👮🏾‍♀️ (:woman_police_officer_medium-dark_skin_tone:)" + U1F46E1F3FE200D2640 = "👮🏾‍♀", "👮🏾‍♀ (:woman_police_officer_medium-dark_skin_tone:)" + U1F46E1F3FC200D2640FE0F = "👮🏼‍♀️", "👮🏼‍♀️ (:woman_police_officer_medium-light_skin_tone:)" + U1F46E1F3FC200D2640 = "👮🏼‍♀", "👮🏼‍♀ (:woman_police_officer_medium-light_skin_tone:)" + U1F46E1F3FD200D2640FE0F = "👮🏽‍♀️", "👮🏽‍♀️ (:woman_police_officer_medium_skin_tone:)" + U1F46E1F3FD200D2640 = "👮🏽‍♀", "👮🏽‍♀ (:woman_police_officer_medium_skin_tone:)" + U1F64E200D2640FE0F = "🙎‍♀️", "🙎‍♀️ (:woman_pouting:)" + U1F64E200D2640 = "🙎‍♀", "🙎‍♀ (:woman_pouting:)" + U1F64E1F3FF200D2640FE0F = "🙎🏿‍♀️", "🙎🏿‍♀️ (:woman_pouting_dark_skin_tone:)" + U1F64E1F3FF200D2640 = "🙎🏿‍♀", "🙎🏿‍♀ (:woman_pouting_dark_skin_tone:)" + U1F64E1F3FB200D2640FE0F = "🙎🏻‍♀️", "🙎🏻‍♀️ (:woman_pouting_light_skin_tone:)" + U1F64E1F3FB200D2640 = "🙎🏻‍♀", "🙎🏻‍♀ (:woman_pouting_light_skin_tone:)" + U1F64E1F3FE200D2640FE0F = "🙎🏾‍♀️", "🙎🏾‍♀️ (:woman_pouting_medium-dark_skin_tone:)" + U1F64E1F3FE200D2640 = "🙎🏾‍♀", "🙎🏾‍♀ (:woman_pouting_medium-dark_skin_tone:)" + U1F64E1F3FC200D2640FE0F = "🙎🏼‍♀️", "🙎🏼‍♀️ (:woman_pouting_medium-light_skin_tone:)" + U1F64E1F3FC200D2640 = "🙎🏼‍♀", "🙎🏼‍♀ (:woman_pouting_medium-light_skin_tone:)" + U1F64E1F3FD200D2640FE0F = "🙎🏽‍♀️", "🙎🏽‍♀️ (:woman_pouting_medium_skin_tone:)" + U1F64E1F3FD200D2640 = "🙎🏽‍♀", "🙎🏽‍♀ (:woman_pouting_medium_skin_tone:)" + U1F64B200D2640FE0F = "🙋‍♀️", "🙋‍♀️ (:woman_raising_hand:)" + U1F64B200D2640 = "🙋‍♀", "🙋‍♀ (:woman_raising_hand:)" + U1F64B1F3FF200D2640FE0F = "🙋🏿‍♀️", "🙋🏿‍♀️ (:woman_raising_hand_dark_skin_tone:)" + U1F64B1F3FF200D2640 = "🙋🏿‍♀", "🙋🏿‍♀ (:woman_raising_hand_dark_skin_tone:)" + U1F64B1F3FB200D2640FE0F = "🙋🏻‍♀️", "🙋🏻‍♀️ (:woman_raising_hand_light_skin_tone:)" + U1F64B1F3FB200D2640 = "🙋🏻‍♀", "🙋🏻‍♀ (:woman_raising_hand_light_skin_tone:)" + U1F64B1F3FE200D2640FE0F = "🙋🏾‍♀️", "🙋🏾‍♀️ (:woman_raising_hand_medium-dark_skin_tone:)" + U1F64B1F3FE200D2640 = "🙋🏾‍♀", "🙋🏾‍♀ (:woman_raising_hand_medium-dark_skin_tone:)" + U1F64B1F3FC200D2640FE0F = "🙋🏼‍♀️", "🙋🏼‍♀️ (:woman_raising_hand_medium-light_skin_tone:)" + U1F64B1F3FC200D2640 = "🙋🏼‍♀", "🙋🏼‍♀ (:woman_raising_hand_medium-light_skin_tone:)" + U1F64B1F3FD200D2640FE0F = "🙋🏽‍♀️", "🙋🏽‍♀️ (:woman_raising_hand_medium_skin_tone:)" + U1F64B1F3FD200D2640 = "🙋🏽‍♀", "🙋🏽‍♀ (:woman_raising_hand_medium_skin_tone:)" + U1F469200D1F9B0 = "👩‍🦰", "👩‍🦰 (:woman_red_hair:)" + U1F6A3200D2640FE0F = "🚣‍♀️", "🚣‍♀️ (:woman_rowing_boat:)" + U1F6A3200D2640 = "🚣‍♀", "🚣‍♀ (:woman_rowing_boat:)" + U1F6A31F3FF200D2640FE0F = "🚣🏿‍♀️", "🚣🏿‍♀️ (:woman_rowing_boat_dark_skin_tone:)" + U1F6A31F3FF200D2640 = "🚣🏿‍♀", "🚣🏿‍♀ (:woman_rowing_boat_dark_skin_tone:)" + U1F6A31F3FB200D2640FE0F = "🚣🏻‍♀️", "🚣🏻‍♀️ (:woman_rowing_boat_light_skin_tone:)" + U1F6A31F3FB200D2640 = "🚣🏻‍♀", "🚣🏻‍♀ (:woman_rowing_boat_light_skin_tone:)" + U1F6A31F3FE200D2640FE0F = "🚣🏾‍♀️", "🚣🏾‍♀️ (:woman_rowing_boat_medium-dark_skin_tone:)" + U1F6A31F3FE200D2640 = "🚣🏾‍♀", "🚣🏾‍♀ (:woman_rowing_boat_medium-dark_skin_tone:)" + U1F6A31F3FC200D2640FE0F = "🚣🏼‍♀️", "🚣🏼‍♀️ (:woman_rowing_boat_medium-light_skin_tone:)" + U1F6A31F3FC200D2640 = "🚣🏼‍♀", "🚣🏼‍♀ (:woman_rowing_boat_medium-light_skin_tone:)" + U1F6A31F3FD200D2640FE0F = "🚣🏽‍♀️", "🚣🏽‍♀️ (:woman_rowing_boat_medium_skin_tone:)" + U1F6A31F3FD200D2640 = "🚣🏽‍♀", "🚣🏽‍♀ (:woman_rowing_boat_medium_skin_tone:)" + U1F3C3200D2640FE0F = "🏃‍♀️", "🏃‍♀️ (:woman_running:)" + U1F3C3200D2640 = "🏃‍♀", "🏃‍♀ (:woman_running:)" + U1F3C31F3FF200D2640FE0F = "🏃🏿‍♀️", "🏃🏿‍♀️ (:woman_running_dark_skin_tone:)" + U1F3C31F3FF200D2640 = "🏃🏿‍♀", "🏃🏿‍♀ (:woman_running_dark_skin_tone:)" + U1F3C3200D2640FE0F200D27A1FE0F = "🏃‍♀️‍➡️", "🏃‍♀️‍➡️ (:woman_running_facing_right:)" + U1F3C3200D2640200D27A1FE0F = "🏃‍♀‍➡️", "🏃‍♀‍➡️ (:woman_running_facing_right:)" + U1F3C3200D2640FE0F200D27A1 = "🏃‍♀️‍➡", "🏃‍♀️‍➡ (:woman_running_facing_right:)" + U1F3C3200D2640200D27A1 = "🏃‍♀‍➡", "🏃‍♀‍➡ (:woman_running_facing_right:)" + U1F3C31F3FF200D2640FE0F200D27A1FE0F = "🏃🏿‍♀️‍➡️", "🏃🏿‍♀️‍➡️ (:woman_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2640200D27A1FE0F = "🏃🏿‍♀‍➡️", "🏃🏿‍♀‍➡️ (:woman_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2640FE0F200D27A1 = "🏃🏿‍♀️‍➡", "🏃🏿‍♀️‍➡ (:woman_running_facing_right_dark_skin_tone:)" + U1F3C31F3FF200D2640200D27A1 = "🏃🏿‍♀‍➡", "🏃🏿‍♀‍➡ (:woman_running_facing_right_dark_skin_tone:)" + U1F3C31F3FB200D2640FE0F200D27A1FE0F = "🏃🏻‍♀️‍➡️", "🏃🏻‍♀️‍➡️ (:woman_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2640200D27A1FE0F = "🏃🏻‍♀‍➡️", "🏃🏻‍♀‍➡️ (:woman_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2640FE0F200D27A1 = "🏃🏻‍♀️‍➡", "🏃🏻‍♀️‍➡ (:woman_running_facing_right_light_skin_tone:)" + U1F3C31F3FB200D2640200D27A1 = "🏃🏻‍♀‍➡", "🏃🏻‍♀‍➡ (:woman_running_facing_right_light_skin_tone:)" + U1F3C31F3FE200D2640FE0F200D27A1FE0F = "🏃🏾‍♀️‍➡️", "🏃🏾‍♀️‍➡️ (:woman_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2640200D27A1FE0F = "🏃🏾‍♀‍➡️", "🏃🏾‍♀‍➡️ (:woman_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2640FE0F200D27A1 = "🏃🏾‍♀️‍➡", "🏃🏾‍♀️‍➡ (:woman_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FE200D2640200D27A1 = "🏃🏾‍♀‍➡", "🏃🏾‍♀‍➡ (:woman_running_facing_right_medium-dark_skin_tone:)" + U1F3C31F3FC200D2640FE0F200D27A1FE0F = "🏃🏼‍♀️‍➡️", "🏃🏼‍♀️‍➡️ (:woman_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2640200D27A1FE0F = "🏃🏼‍♀‍➡️", "🏃🏼‍♀‍➡️ (:woman_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2640FE0F200D27A1 = "🏃🏼‍♀️‍➡", "🏃🏼‍♀️‍➡ (:woman_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FC200D2640200D27A1 = "🏃🏼‍♀‍➡", "🏃🏼‍♀‍➡ (:woman_running_facing_right_medium-light_skin_tone:)" + U1F3C31F3FD200D2640FE0F200D27A1FE0F = "🏃🏽‍♀️‍➡️", "🏃🏽‍♀️‍➡️ (:woman_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2640200D27A1FE0F = "🏃🏽‍♀‍➡️", "🏃🏽‍♀‍➡️ (:woman_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2640FE0F200D27A1 = "🏃🏽‍♀️‍➡", "🏃🏽‍♀️‍➡ (:woman_running_facing_right_medium_skin_tone:)" + U1F3C31F3FD200D2640200D27A1 = "🏃🏽‍♀‍➡", "🏃🏽‍♀‍➡ (:woman_running_facing_right_medium_skin_tone:)" + U1F3C31F3FB200D2640FE0F = "🏃🏻‍♀️", "🏃🏻‍♀️ (:woman_running_light_skin_tone:)" + U1F3C31F3FB200D2640 = "🏃🏻‍♀", "🏃🏻‍♀ (:woman_running_light_skin_tone:)" + U1F3C31F3FE200D2640FE0F = "🏃🏾‍♀️", "🏃🏾‍♀️ (:woman_running_medium-dark_skin_tone:)" + U1F3C31F3FE200D2640 = "🏃🏾‍♀", "🏃🏾‍♀ (:woman_running_medium-dark_skin_tone:)" + U1F3C31F3FC200D2640FE0F = "🏃🏼‍♀️", "🏃🏼‍♀️ (:woman_running_medium-light_skin_tone:)" + U1F3C31F3FC200D2640 = "🏃🏼‍♀", "🏃🏼‍♀ (:woman_running_medium-light_skin_tone:)" + U1F3C31F3FD200D2640FE0F = "🏃🏽‍♀️", "🏃🏽‍♀️ (:woman_running_medium_skin_tone:)" + U1F3C31F3FD200D2640 = "🏃🏽‍♀", "🏃🏽‍♀ (:woman_running_medium_skin_tone:)" + U1F469200D1F52C = "👩‍🔬", "👩‍🔬 (:woman_scientist:)" + U1F4691F3FF200D1F52C = "👩🏿‍🔬", "👩🏿‍🔬 (:woman_scientist_dark_skin_tone:)" + U1F4691F3FB200D1F52C = "👩🏻‍🔬", "👩🏻‍🔬 (:woman_scientist_light_skin_tone:)" + U1F4691F3FE200D1F52C = "👩🏾‍🔬", "👩🏾‍🔬 (:woman_scientist_medium-dark_skin_tone:)" + U1F4691F3FC200D1F52C = "👩🏼‍🔬", "👩🏼‍🔬 (:woman_scientist_medium-light_skin_tone:)" + U1F4691F3FD200D1F52C = "👩🏽‍🔬", "👩🏽‍🔬 (:woman_scientist_medium_skin_tone:)" + U1F937200D2640FE0F = "🤷‍♀️", "🤷‍♀️ (:woman_shrugging:)" + U1F937200D2640 = "🤷‍♀", "🤷‍♀ (:woman_shrugging:)" + U1F9371F3FF200D2640FE0F = "🤷🏿‍♀️", "🤷🏿‍♀️ (:woman_shrugging_dark_skin_tone:)" + U1F9371F3FF200D2640 = "🤷🏿‍♀", "🤷🏿‍♀ (:woman_shrugging_dark_skin_tone:)" + U1F9371F3FB200D2640FE0F = "🤷🏻‍♀️", "🤷🏻‍♀️ (:woman_shrugging_light_skin_tone:)" + U1F9371F3FB200D2640 = "🤷🏻‍♀", "🤷🏻‍♀ (:woman_shrugging_light_skin_tone:)" + U1F9371F3FE200D2640FE0F = "🤷🏾‍♀️", "🤷🏾‍♀️ (:woman_shrugging_medium-dark_skin_tone:)" + U1F9371F3FE200D2640 = "🤷🏾‍♀", "🤷🏾‍♀ (:woman_shrugging_medium-dark_skin_tone:)" + U1F9371F3FC200D2640FE0F = "🤷🏼‍♀️", "🤷🏼‍♀️ (:woman_shrugging_medium-light_skin_tone:)" + U1F9371F3FC200D2640 = "🤷🏼‍♀", "🤷🏼‍♀ (:woman_shrugging_medium-light_skin_tone:)" + U1F9371F3FD200D2640FE0F = "🤷🏽‍♀️", "🤷🏽‍♀️ (:woman_shrugging_medium_skin_tone:)" + U1F9371F3FD200D2640 = "🤷🏽‍♀", "🤷🏽‍♀ (:woman_shrugging_medium_skin_tone:)" + U1F469200D1F3A4 = "👩‍🎤", "👩‍🎤 (:woman_singer:)" + U1F4691F3FF200D1F3A4 = "👩🏿‍🎤", "👩🏿‍🎤 (:woman_singer_dark_skin_tone:)" + U1F4691F3FB200D1F3A4 = "👩🏻‍🎤", "👩🏻‍🎤 (:woman_singer_light_skin_tone:)" + U1F4691F3FE200D1F3A4 = "👩🏾‍🎤", "👩🏾‍🎤 (:woman_singer_medium-dark_skin_tone:)" + U1F4691F3FC200D1F3A4 = "👩🏼‍🎤", "👩🏼‍🎤 (:woman_singer_medium-light_skin_tone:)" + U1F4691F3FD200D1F3A4 = "👩🏽‍🎤", "👩🏽‍🎤 (:woman_singer_medium_skin_tone:)" + U1F9CD200D2640FE0F = "🧍‍♀️", "🧍‍♀️ (:woman_standing:)" + U1F9CD200D2640 = "🧍‍♀", "🧍‍♀ (:woman_standing:)" + U1F9CD1F3FF200D2640FE0F = "🧍🏿‍♀️", "🧍🏿‍♀️ (:woman_standing_dark_skin_tone:)" + U1F9CD1F3FF200D2640 = "🧍🏿‍♀", "🧍🏿‍♀ (:woman_standing_dark_skin_tone:)" + U1F9CD1F3FB200D2640FE0F = "🧍🏻‍♀️", "🧍🏻‍♀️ (:woman_standing_light_skin_tone:)" + U1F9CD1F3FB200D2640 = "🧍🏻‍♀", "🧍🏻‍♀ (:woman_standing_light_skin_tone:)" + U1F9CD1F3FE200D2640FE0F = "🧍🏾‍♀️", "🧍🏾‍♀️ (:woman_standing_medium-dark_skin_tone:)" + U1F9CD1F3FE200D2640 = "🧍🏾‍♀", "🧍🏾‍♀ (:woman_standing_medium-dark_skin_tone:)" + U1F9CD1F3FC200D2640FE0F = "🧍🏼‍♀️", "🧍🏼‍♀️ (:woman_standing_medium-light_skin_tone:)" + U1F9CD1F3FC200D2640 = "🧍🏼‍♀", "🧍🏼‍♀ (:woman_standing_medium-light_skin_tone:)" + U1F9CD1F3FD200D2640FE0F = "🧍🏽‍♀️", "🧍🏽‍♀️ (:woman_standing_medium_skin_tone:)" + U1F9CD1F3FD200D2640 = "🧍🏽‍♀", "🧍🏽‍♀ (:woman_standing_medium_skin_tone:)" + U1F469200D1F393 = "👩‍🎓", "👩‍🎓 (:woman_student:)" + U1F4691F3FF200D1F393 = "👩🏿‍🎓", "👩🏿‍🎓 (:woman_student_dark_skin_tone:)" + U1F4691F3FB200D1F393 = "👩🏻‍🎓", "👩🏻‍🎓 (:woman_student_light_skin_tone:)" + U1F4691F3FE200D1F393 = "👩🏾‍🎓", "👩🏾‍🎓 (:woman_student_medium-dark_skin_tone:)" + U1F4691F3FC200D1F393 = "👩🏼‍🎓", "👩🏼‍🎓 (:woman_student_medium-light_skin_tone:)" + U1F4691F3FD200D1F393 = "👩🏽‍🎓", "👩🏽‍🎓 (:woman_student_medium_skin_tone:)" + U1F9B8200D2640FE0F = "🦸‍♀️", "🦸‍♀️ (:woman_superhero:)" + U1F9B8200D2640 = "🦸‍♀", "🦸‍♀ (:woman_superhero:)" + U1F9B81F3FF200D2640FE0F = "🦸🏿‍♀️", "🦸🏿‍♀️ (:woman_superhero_dark_skin_tone:)" + U1F9B81F3FF200D2640 = "🦸🏿‍♀", "🦸🏿‍♀ (:woman_superhero_dark_skin_tone:)" + U1F9B81F3FB200D2640FE0F = "🦸🏻‍♀️", "🦸🏻‍♀️ (:woman_superhero_light_skin_tone:)" + U1F9B81F3FB200D2640 = "🦸🏻‍♀", "🦸🏻‍♀ (:woman_superhero_light_skin_tone:)" + U1F9B81F3FE200D2640FE0F = "🦸🏾‍♀️", "🦸🏾‍♀️ (:woman_superhero_medium-dark_skin_tone:)" + U1F9B81F3FE200D2640 = "🦸🏾‍♀", "🦸🏾‍♀ (:woman_superhero_medium-dark_skin_tone:)" + U1F9B81F3FC200D2640FE0F = "🦸🏼‍♀️", "🦸🏼‍♀️ (:woman_superhero_medium-light_skin_tone:)" + U1F9B81F3FC200D2640 = "🦸🏼‍♀", "🦸🏼‍♀ (:woman_superhero_medium-light_skin_tone:)" + U1F9B81F3FD200D2640FE0F = "🦸🏽‍♀️", "🦸🏽‍♀️ (:woman_superhero_medium_skin_tone:)" + U1F9B81F3FD200D2640 = "🦸🏽‍♀", "🦸🏽‍♀ (:woman_superhero_medium_skin_tone:)" + U1F9B9200D2640FE0F = "🦹‍♀️", "🦹‍♀️ (:woman_supervillain:)" + U1F9B9200D2640 = "🦹‍♀", "🦹‍♀ (:woman_supervillain:)" + U1F9B91F3FF200D2640FE0F = "🦹🏿‍♀️", "🦹🏿‍♀️ (:woman_supervillain_dark_skin_tone:)" + U1F9B91F3FF200D2640 = "🦹🏿‍♀", "🦹🏿‍♀ (:woman_supervillain_dark_skin_tone:)" + U1F9B91F3FB200D2640FE0F = "🦹🏻‍♀️", "🦹🏻‍♀️ (:woman_supervillain_light_skin_tone:)" + U1F9B91F3FB200D2640 = "🦹🏻‍♀", "🦹🏻‍♀ (:woman_supervillain_light_skin_tone:)" + U1F9B91F3FE200D2640FE0F = "🦹🏾‍♀️", "🦹🏾‍♀️ (:woman_supervillain_medium-dark_skin_tone:)" + U1F9B91F3FE200D2640 = "🦹🏾‍♀", "🦹🏾‍♀ (:woman_supervillain_medium-dark_skin_tone:)" + U1F9B91F3FC200D2640FE0F = "🦹🏼‍♀️", "🦹🏼‍♀️ (:woman_supervillain_medium-light_skin_tone:)" + U1F9B91F3FC200D2640 = "🦹🏼‍♀", "🦹🏼‍♀ (:woman_supervillain_medium-light_skin_tone:)" + U1F9B91F3FD200D2640FE0F = "🦹🏽‍♀️", "🦹🏽‍♀️ (:woman_supervillain_medium_skin_tone:)" + U1F9B91F3FD200D2640 = "🦹🏽‍♀", "🦹🏽‍♀ (:woman_supervillain_medium_skin_tone:)" + U1F3C4200D2640FE0F = "🏄‍♀️", "🏄‍♀️ (:woman_surfing:)" + U1F3C4200D2640 = "🏄‍♀", "🏄‍♀ (:woman_surfing:)" + U1F3C41F3FF200D2640FE0F = "🏄🏿‍♀️", "🏄🏿‍♀️ (:woman_surfing_dark_skin_tone:)" + U1F3C41F3FF200D2640 = "🏄🏿‍♀", "🏄🏿‍♀ (:woman_surfing_dark_skin_tone:)" + U1F3C41F3FB200D2640FE0F = "🏄🏻‍♀️", "🏄🏻‍♀️ (:woman_surfing_light_skin_tone:)" + U1F3C41F3FB200D2640 = "🏄🏻‍♀", "🏄🏻‍♀ (:woman_surfing_light_skin_tone:)" + U1F3C41F3FE200D2640FE0F = "🏄🏾‍♀️", "🏄🏾‍♀️ (:woman_surfing_medium-dark_skin_tone:)" + U1F3C41F3FE200D2640 = "🏄🏾‍♀", "🏄🏾‍♀ (:woman_surfing_medium-dark_skin_tone:)" + U1F3C41F3FC200D2640FE0F = "🏄🏼‍♀️", "🏄🏼‍♀️ (:woman_surfing_medium-light_skin_tone:)" + U1F3C41F3FC200D2640 = "🏄🏼‍♀", "🏄🏼‍♀ (:woman_surfing_medium-light_skin_tone:)" + U1F3C41F3FD200D2640FE0F = "🏄🏽‍♀️", "🏄🏽‍♀️ (:woman_surfing_medium_skin_tone:)" + U1F3C41F3FD200D2640 = "🏄🏽‍♀", "🏄🏽‍♀ (:woman_surfing_medium_skin_tone:)" + U1F3CA200D2640FE0F = "🏊‍♀️", "🏊‍♀️ (:woman_swimming:)" + U1F3CA200D2640 = "🏊‍♀", "🏊‍♀ (:woman_swimming:)" + U1F3CA1F3FF200D2640FE0F = "🏊🏿‍♀️", "🏊🏿‍♀️ (:woman_swimming_dark_skin_tone:)" + U1F3CA1F3FF200D2640 = "🏊🏿‍♀", "🏊🏿‍♀ (:woman_swimming_dark_skin_tone:)" + U1F3CA1F3FB200D2640FE0F = "🏊🏻‍♀️", "🏊🏻‍♀️ (:woman_swimming_light_skin_tone:)" + U1F3CA1F3FB200D2640 = "🏊🏻‍♀", "🏊🏻‍♀ (:woman_swimming_light_skin_tone:)" + U1F3CA1F3FE200D2640FE0F = "🏊🏾‍♀️", "🏊🏾‍♀️ (:woman_swimming_medium-dark_skin_tone:)" + U1F3CA1F3FE200D2640 = "🏊🏾‍♀", "🏊🏾‍♀ (:woman_swimming_medium-dark_skin_tone:)" + U1F3CA1F3FC200D2640FE0F = "🏊🏼‍♀️", "🏊🏼‍♀️ (:woman_swimming_medium-light_skin_tone:)" + U1F3CA1F3FC200D2640 = "🏊🏼‍♀", "🏊🏼‍♀ (:woman_swimming_medium-light_skin_tone:)" + U1F3CA1F3FD200D2640FE0F = "🏊🏽‍♀️", "🏊🏽‍♀️ (:woman_swimming_medium_skin_tone:)" + U1F3CA1F3FD200D2640 = "🏊🏽‍♀", "🏊🏽‍♀ (:woman_swimming_medium_skin_tone:)" + U1F469200D1F3EB = "👩‍🏫", "👩‍🏫 (:woman_teacher:)" + U1F4691F3FF200D1F3EB = "👩🏿‍🏫", "👩🏿‍🏫 (:woman_teacher_dark_skin_tone:)" + U1F4691F3FB200D1F3EB = "👩🏻‍🏫", "👩🏻‍🏫 (:woman_teacher_light_skin_tone:)" + U1F4691F3FE200D1F3EB = "👩🏾‍🏫", "👩🏾‍🏫 (:woman_teacher_medium-dark_skin_tone:)" + U1F4691F3FC200D1F3EB = "👩🏼‍🏫", "👩🏼‍🏫 (:woman_teacher_medium-light_skin_tone:)" + U1F4691F3FD200D1F3EB = "👩🏽‍🏫", "👩🏽‍🏫 (:woman_teacher_medium_skin_tone:)" + U1F469200D1F4BB = "👩‍💻", "👩‍💻 (:woman_technologist:)" + U1F4691F3FF200D1F4BB = "👩🏿‍💻", "👩🏿‍💻 (:woman_technologist_dark_skin_tone:)" + U1F4691F3FB200D1F4BB = "👩🏻‍💻", "👩🏻‍💻 (:woman_technologist_light_skin_tone:)" + U1F4691F3FE200D1F4BB = "👩🏾‍💻", "👩🏾‍💻 (:woman_technologist_medium-dark_skin_tone:)" + U1F4691F3FC200D1F4BB = "👩🏼‍💻", "👩🏼‍💻 (:woman_technologist_medium-light_skin_tone:)" + U1F4691F3FD200D1F4BB = "👩🏽‍💻", "👩🏽‍💻 (:woman_technologist_medium_skin_tone:)" + U1F481200D2640FE0F = "💁‍♀️", "💁‍♀️ (:woman_tipping_hand:)" + U1F481200D2640 = "💁‍♀", "💁‍♀ (:woman_tipping_hand:)" + U1F4811F3FF200D2640FE0F = "💁🏿‍♀️", "💁🏿‍♀️ (:woman_tipping_hand_dark_skin_tone:)" + U1F4811F3FF200D2640 = "💁🏿‍♀", "💁🏿‍♀ (:woman_tipping_hand_dark_skin_tone:)" + U1F4811F3FB200D2640FE0F = "💁🏻‍♀️", "💁🏻‍♀️ (:woman_tipping_hand_light_skin_tone:)" + U1F4811F3FB200D2640 = "💁🏻‍♀", "💁🏻‍♀ (:woman_tipping_hand_light_skin_tone:)" + U1F4811F3FE200D2640FE0F = "💁🏾‍♀️", "💁🏾‍♀️ (:woman_tipping_hand_medium-dark_skin_tone:)" + U1F4811F3FE200D2640 = "💁🏾‍♀", "💁🏾‍♀ (:woman_tipping_hand_medium-dark_skin_tone:)" + U1F4811F3FC200D2640FE0F = "💁🏼‍♀️", "💁🏼‍♀️ (:woman_tipping_hand_medium-light_skin_tone:)" + U1F4811F3FC200D2640 = "💁🏼‍♀", "💁🏼‍♀ (:woman_tipping_hand_medium-light_skin_tone:)" + U1F4811F3FD200D2640FE0F = "💁🏽‍♀️", "💁🏽‍♀️ (:woman_tipping_hand_medium_skin_tone:)" + U1F4811F3FD200D2640 = "💁🏽‍♀", "💁🏽‍♀ (:woman_tipping_hand_medium_skin_tone:)" + U1F9DB200D2640FE0F = "🧛‍♀️", "🧛‍♀️ (:woman_vampire:)" + U1F9DB200D2640 = "🧛‍♀", "🧛‍♀ (:woman_vampire:)" + U1F9DB1F3FF200D2640FE0F = "🧛🏿‍♀️", "🧛🏿‍♀️ (:woman_vampire_dark_skin_tone:)" + U1F9DB1F3FF200D2640 = "🧛🏿‍♀", "🧛🏿‍♀ (:woman_vampire_dark_skin_tone:)" + U1F9DB1F3FB200D2640FE0F = "🧛🏻‍♀️", "🧛🏻‍♀️ (:woman_vampire_light_skin_tone:)" + U1F9DB1F3FB200D2640 = "🧛🏻‍♀", "🧛🏻‍♀ (:woman_vampire_light_skin_tone:)" + U1F9DB1F3FE200D2640FE0F = "🧛🏾‍♀️", "🧛🏾‍♀️ (:woman_vampire_medium-dark_skin_tone:)" + U1F9DB1F3FE200D2640 = "🧛🏾‍♀", "🧛🏾‍♀ (:woman_vampire_medium-dark_skin_tone:)" + U1F9DB1F3FC200D2640FE0F = "🧛🏼‍♀️", "🧛🏼‍♀️ (:woman_vampire_medium-light_skin_tone:)" + U1F9DB1F3FC200D2640 = "🧛🏼‍♀", "🧛🏼‍♀ (:woman_vampire_medium-light_skin_tone:)" + U1F9DB1F3FD200D2640FE0F = "🧛🏽‍♀️", "🧛🏽‍♀️ (:woman_vampire_medium_skin_tone:)" + U1F9DB1F3FD200D2640 = "🧛🏽‍♀", "🧛🏽‍♀ (:woman_vampire_medium_skin_tone:)" + U1F6B6200D2640FE0F = "🚶‍♀️", "🚶‍♀️ (:woman_walking:)" + U1F6B6200D2640 = "🚶‍♀", "🚶‍♀ (:woman_walking:)" + U1F6B61F3FF200D2640FE0F = "🚶🏿‍♀️", "🚶🏿‍♀️ (:woman_walking_dark_skin_tone:)" + U1F6B61F3FF200D2640 = "🚶🏿‍♀", "🚶🏿‍♀ (:woman_walking_dark_skin_tone:)" + U1F6B6200D2640FE0F200D27A1FE0F = "🚶‍♀️‍➡️", "🚶‍♀️‍➡️ (:woman_walking_facing_right:)" + U1F6B6200D2640200D27A1FE0F = "🚶‍♀‍➡️", "🚶‍♀‍➡️ (:woman_walking_facing_right:)" + U1F6B6200D2640FE0F200D27A1 = "🚶‍♀️‍➡", "🚶‍♀️‍➡ (:woman_walking_facing_right:)" + U1F6B6200D2640200D27A1 = "🚶‍♀‍➡", "🚶‍♀‍➡ (:woman_walking_facing_right:)" + U1F6B61F3FF200D2640FE0F200D27A1FE0F = "🚶🏿‍♀️‍➡️", "🚶🏿‍♀️‍➡️ (:woman_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2640200D27A1FE0F = "🚶🏿‍♀‍➡️", "🚶🏿‍♀‍➡️ (:woman_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2640FE0F200D27A1 = "🚶🏿‍♀️‍➡", "🚶🏿‍♀️‍➡ (:woman_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FF200D2640200D27A1 = "🚶🏿‍♀‍➡", "🚶🏿‍♀‍➡ (:woman_walking_facing_right_dark_skin_tone:)" + U1F6B61F3FB200D2640FE0F200D27A1FE0F = "🚶🏻‍♀️‍➡️", "🚶🏻‍♀️‍➡️ (:woman_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2640200D27A1FE0F = "🚶🏻‍♀‍➡️", "🚶🏻‍♀‍➡️ (:woman_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2640FE0F200D27A1 = "🚶🏻‍♀️‍➡", "🚶🏻‍♀️‍➡ (:woman_walking_facing_right_light_skin_tone:)" + U1F6B61F3FB200D2640200D27A1 = "🚶🏻‍♀‍➡", "🚶🏻‍♀‍➡ (:woman_walking_facing_right_light_skin_tone:)" + U1F6B61F3FE200D2640FE0F200D27A1FE0F = "🚶🏾‍♀️‍➡️", "🚶🏾‍♀️‍➡️ (:woman_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2640200D27A1FE0F = "🚶🏾‍♀‍➡️", "🚶🏾‍♀‍➡️ (:woman_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2640FE0F200D27A1 = "🚶🏾‍♀️‍➡", "🚶🏾‍♀️‍➡ (:woman_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FE200D2640200D27A1 = "🚶🏾‍♀‍➡", "🚶🏾‍♀‍➡ (:woman_walking_facing_right_medium-dark_skin_tone:)" + U1F6B61F3FC200D2640FE0F200D27A1FE0F = "🚶🏼‍♀️‍➡️", "🚶🏼‍♀️‍➡️ (:woman_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2640200D27A1FE0F = "🚶🏼‍♀‍➡️", "🚶🏼‍♀‍➡️ (:woman_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2640FE0F200D27A1 = "🚶🏼‍♀️‍➡", "🚶🏼‍♀️‍➡ (:woman_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FC200D2640200D27A1 = "🚶🏼‍♀‍➡", "🚶🏼‍♀‍➡ (:woman_walking_facing_right_medium-light_skin_tone:)" + U1F6B61F3FD200D2640FE0F200D27A1FE0F = "🚶🏽‍♀️‍➡️", "🚶🏽‍♀️‍➡️ (:woman_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2640200D27A1FE0F = "🚶🏽‍♀‍➡️", "🚶🏽‍♀‍➡️ (:woman_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2640FE0F200D27A1 = "🚶🏽‍♀️‍➡", "🚶🏽‍♀️‍➡ (:woman_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FD200D2640200D27A1 = "🚶🏽‍♀‍➡", "🚶🏽‍♀‍➡ (:woman_walking_facing_right_medium_skin_tone:)" + U1F6B61F3FB200D2640FE0F = "🚶🏻‍♀️", "🚶🏻‍♀️ (:woman_walking_light_skin_tone:)" + U1F6B61F3FB200D2640 = "🚶🏻‍♀", "🚶🏻‍♀ (:woman_walking_light_skin_tone:)" + U1F6B61F3FE200D2640FE0F = "🚶🏾‍♀️", "🚶🏾‍♀️ (:woman_walking_medium-dark_skin_tone:)" + U1F6B61F3FE200D2640 = "🚶🏾‍♀", "🚶🏾‍♀ (:woman_walking_medium-dark_skin_tone:)" + U1F6B61F3FC200D2640FE0F = "🚶🏼‍♀️", "🚶🏼‍♀️ (:woman_walking_medium-light_skin_tone:)" + U1F6B61F3FC200D2640 = "🚶🏼‍♀", "🚶🏼‍♀ (:woman_walking_medium-light_skin_tone:)" + U1F6B61F3FD200D2640FE0F = "🚶🏽‍♀️", "🚶🏽‍♀️ (:woman_walking_medium_skin_tone:)" + U1F6B61F3FD200D2640 = "🚶🏽‍♀", "🚶🏽‍♀ (:woman_walking_medium_skin_tone:)" + U1F473200D2640FE0F = "👳‍♀️", "👳‍♀️ (:woman_wearing_turban:)" + U1F473200D2640 = "👳‍♀", "👳‍♀ (:woman_wearing_turban:)" + U1F4731F3FF200D2640FE0F = "👳🏿‍♀️", "👳🏿‍♀️ (:woman_wearing_turban_dark_skin_tone:)" + U1F4731F3FF200D2640 = "👳🏿‍♀", "👳🏿‍♀ (:woman_wearing_turban_dark_skin_tone:)" + U1F4731F3FB200D2640FE0F = "👳🏻‍♀️", "👳🏻‍♀️ (:woman_wearing_turban_light_skin_tone:)" + U1F4731F3FB200D2640 = "👳🏻‍♀", "👳🏻‍♀ (:woman_wearing_turban_light_skin_tone:)" + U1F4731F3FE200D2640FE0F = "👳🏾‍♀️", "👳🏾‍♀️ (:woman_wearing_turban_medium-dark_skin_tone:)" + U1F4731F3FE200D2640 = "👳🏾‍♀", "👳🏾‍♀ (:woman_wearing_turban_medium-dark_skin_tone:)" + U1F4731F3FC200D2640FE0F = "👳🏼‍♀️", "👳🏼‍♀️ (:woman_wearing_turban_medium-light_skin_tone:)" + U1F4731F3FC200D2640 = "👳🏼‍♀", "👳🏼‍♀ (:woman_wearing_turban_medium-light_skin_tone:)" + U1F4731F3FD200D2640FE0F = "👳🏽‍♀️", "👳🏽‍♀️ (:woman_wearing_turban_medium_skin_tone:)" + U1F4731F3FD200D2640 = "👳🏽‍♀", "👳🏽‍♀ (:woman_wearing_turban_medium_skin_tone:)" + U1F469200D1F9B3 = "👩‍🦳", "👩‍🦳 (:woman_white_hair:)" + U1F9D5 = "🧕", "🧕 (:woman_with_headscarf:)" + U1F9D51F3FF = "🧕🏿", "🧕🏿 (:woman_with_headscarf_dark_skin_tone:)" + U1F9D51F3FB = "🧕🏻", "🧕🏻 (:woman_with_headscarf_light_skin_tone:)" + U1F9D51F3FE = "🧕🏾", "🧕🏾 (:woman_with_headscarf_medium-dark_skin_tone:)" + U1F9D51F3FC = "🧕🏼", "🧕🏼 (:woman_with_headscarf_medium-light_skin_tone:)" + U1F9D51F3FD = "🧕🏽", "🧕🏽 (:woman_with_headscarf_medium_skin_tone:)" + U1F470200D2640FE0F = "👰‍♀️", "👰‍♀️ (:woman_with_veil:)" + U1F470200D2640 = "👰‍♀", "👰‍♀ (:woman_with_veil:)" + U1F4701F3FF200D2640FE0F = "👰🏿‍♀️", "👰🏿‍♀️ (:woman_with_veil_dark_skin_tone:)" + U1F4701F3FF200D2640 = "👰🏿‍♀", "👰🏿‍♀ (:woman_with_veil_dark_skin_tone:)" + U1F4701F3FB200D2640FE0F = "👰🏻‍♀️", "👰🏻‍♀️ (:woman_with_veil_light_skin_tone:)" + U1F4701F3FB200D2640 = "👰🏻‍♀", "👰🏻‍♀ (:woman_with_veil_light_skin_tone:)" + U1F4701F3FE200D2640FE0F = "👰🏾‍♀️", "👰🏾‍♀️ (:woman_with_veil_medium-dark_skin_tone:)" + U1F4701F3FE200D2640 = "👰🏾‍♀", "👰🏾‍♀ (:woman_with_veil_medium-dark_skin_tone:)" + U1F4701F3FC200D2640FE0F = "👰🏼‍♀️", "👰🏼‍♀️ (:woman_with_veil_medium-light_skin_tone:)" + U1F4701F3FC200D2640 = "👰🏼‍♀", "👰🏼‍♀ (:woman_with_veil_medium-light_skin_tone:)" + U1F4701F3FD200D2640FE0F = "👰🏽‍♀️", "👰🏽‍♀️ (:woman_with_veil_medium_skin_tone:)" + U1F4701F3FD200D2640 = "👰🏽‍♀", "👰🏽‍♀ (:woman_with_veil_medium_skin_tone:)" + U1F469200D1F9AF = "👩‍🦯", "👩‍🦯 (:woman_with_white_cane:)" + U1F4691F3FF200D1F9AF = "👩🏿‍🦯", "👩🏿‍🦯 (:woman_with_white_cane_dark_skin_tone:)" + U1F469200D1F9AF200D27A1FE0F = "👩‍🦯‍➡️", "👩‍🦯‍➡️ (:woman_with_white_cane_facing_right:)" + U1F469200D1F9AF200D27A1 = "👩‍🦯‍➡", "👩‍🦯‍➡ (:woman_with_white_cane_facing_right:)" + U1F4691F3FF200D1F9AF200D27A1FE0F = "👩🏿‍🦯‍➡️", "👩🏿‍🦯‍➡️ (:woman_with_white_cane_facing_right_dark_skin_tone:)" + U1F4691F3FF200D1F9AF200D27A1 = "👩🏿‍🦯‍➡", "👩🏿‍🦯‍➡ (:woman_with_white_cane_facing_right_dark_skin_tone:)" + U1F4691F3FB200D1F9AF200D27A1FE0F = "👩🏻‍🦯‍➡️", "👩🏻‍🦯‍➡️ (:woman_with_white_cane_facing_right_light_skin_tone:)" + U1F4691F3FB200D1F9AF200D27A1 = "👩🏻‍🦯‍➡", "👩🏻‍🦯‍➡ (:woman_with_white_cane_facing_right_light_skin_tone:)" + U1F4691F3FE200D1F9AF200D27A1FE0F = "👩🏾‍🦯‍➡️", "👩🏾‍🦯‍➡️ (:woman_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F4691F3FE200D1F9AF200D27A1 = "👩🏾‍🦯‍➡", "👩🏾‍🦯‍➡ (:woman_with_white_cane_facing_right_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9AF200D27A1FE0F = "👩🏼‍🦯‍➡️", "👩🏼‍🦯‍➡️ (:woman_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F4691F3FC200D1F9AF200D27A1 = "👩🏼‍🦯‍➡", "👩🏼‍🦯‍➡ (:woman_with_white_cane_facing_right_medium-light_skin_tone:)" + U1F4691F3FD200D1F9AF200D27A1FE0F = "👩🏽‍🦯‍➡️", "👩🏽‍🦯‍➡️ (:woman_with_white_cane_facing_right_medium_skin_tone:)" + U1F4691F3FD200D1F9AF200D27A1 = "👩🏽‍🦯‍➡", "👩🏽‍🦯‍➡ (:woman_with_white_cane_facing_right_medium_skin_tone:)" + U1F4691F3FB200D1F9AF = "👩🏻‍🦯", "👩🏻‍🦯 (:woman_with_white_cane_light_skin_tone:)" + U1F4691F3FE200D1F9AF = "👩🏾‍🦯", "👩🏾‍🦯 (:woman_with_white_cane_medium-dark_skin_tone:)" + U1F4691F3FC200D1F9AF = "👩🏼‍🦯", "👩🏼‍🦯 (:woman_with_white_cane_medium-light_skin_tone:)" + U1F4691F3FD200D1F9AF = "👩🏽‍🦯", "👩🏽‍🦯 (:woman_with_white_cane_medium_skin_tone:)" + U1F9DF200D2640FE0F = "🧟‍♀️", "🧟‍♀️ (:woman_zombie:)" + U1F9DF200D2640 = "🧟‍♀", "🧟‍♀ (:woman_zombie:)" + U1F462 = "👢", "👢 (:woman’s_boot:)" + U1F45A = "👚", "👚 (:woman’s_clothes:)" + U1F452 = "👒", "👒 (:woman’s_hat:)" + U1F461 = "👡", "👡 (:woman’s_sandal:)" + U1F46D = "👭", "👭 (:women_holding_hands:)" + U1F46D1F3FF = "👭🏿", "👭🏿 (:women_holding_hands_dark_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4691F3FB = "👩🏿‍🤝‍👩🏻", "👩🏿‍🤝‍👩🏻 (:women_holding_hands_dark_skin_tone_light_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4691F3FE = "👩🏿‍🤝‍👩🏾", "👩🏿‍🤝‍👩🏾 (:women_holding_hands_dark_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4691F3FC = "👩🏿‍🤝‍👩🏼", "👩🏿‍🤝‍👩🏼 (:women_holding_hands_dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FF200D1F91D200D1F4691F3FD = "👩🏿‍🤝‍👩🏽", "👩🏿‍🤝‍👩🏽 (:women_holding_hands_dark_skin_tone_medium_skin_tone:)" + U1F46D1F3FB = "👭🏻", "👭🏻 (:women_holding_hands_light_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4691F3FF = "👩🏻‍🤝‍👩🏿", "👩🏻‍🤝‍👩🏿 (:women_holding_hands_light_skin_tone_dark_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4691F3FE = "👩🏻‍🤝‍👩🏾", "👩🏻‍🤝‍👩🏾 (:women_holding_hands_light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4691F3FC = "👩🏻‍🤝‍👩🏼", "👩🏻‍🤝‍👩🏼 (:women_holding_hands_light_skin_tone_medium-light_skin_tone:)" + U1F4691F3FB200D1F91D200D1F4691F3FD = "👩🏻‍🤝‍👩🏽", "👩🏻‍🤝‍👩🏽 (:women_holding_hands_light_skin_tone_medium_skin_tone:)" + U1F46D1F3FE = "👭🏾", "👭🏾 (:women_holding_hands_medium-dark_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4691F3FF = "👩🏾‍🤝‍👩🏿", "👩🏾‍🤝‍👩🏿 (:women_holding_hands_medium-dark_skin_tone_dark_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4691F3FB = "👩🏾‍🤝‍👩🏻", "👩🏾‍🤝‍👩🏻 (:women_holding_hands_medium-dark_skin_tone_light_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4691F3FC = "👩🏾‍🤝‍👩🏼", "👩🏾‍🤝‍👩🏼 (:women_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)" + U1F4691F3FE200D1F91D200D1F4691F3FD = "👩🏾‍🤝‍👩🏽", "👩🏾‍🤝‍👩🏽 (:women_holding_hands_medium-dark_skin_tone_medium_skin_tone:)" + U1F46D1F3FC = "👭🏼", "👭🏼 (:women_holding_hands_medium-light_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4691F3FF = "👩🏼‍🤝‍👩🏿", "👩🏼‍🤝‍👩🏿 (:women_holding_hands_medium-light_skin_tone_dark_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4691F3FB = "👩🏼‍🤝‍👩🏻", "👩🏼‍🤝‍👩🏻 (:women_holding_hands_medium-light_skin_tone_light_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4691F3FE = "👩🏼‍🤝‍👩🏾", "👩🏼‍🤝‍👩🏾 (:women_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FC200D1F91D200D1F4691F3FD = "👩🏼‍🤝‍👩🏽", "👩🏼‍🤝‍👩🏽 (:women_holding_hands_medium-light_skin_tone_medium_skin_tone:)" + U1F46D1F3FD = "👭🏽", "👭🏽 (:women_holding_hands_medium_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4691F3FF = "👩🏽‍🤝‍👩🏿", "👩🏽‍🤝‍👩🏿 (:women_holding_hands_medium_skin_tone_dark_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4691F3FB = "👩🏽‍🤝‍👩🏻", "👩🏽‍🤝‍👩🏻 (:women_holding_hands_medium_skin_tone_light_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4691F3FE = "👩🏽‍🤝‍👩🏾", "👩🏽‍🤝‍👩🏾 (:women_holding_hands_medium_skin_tone_medium-dark_skin_tone:)" + U1F4691F3FD200D1F91D200D1F4691F3FC = "👩🏽‍🤝‍👩🏼", "👩🏽‍🤝‍👩🏼 (:women_holding_hands_medium_skin_tone_medium-light_skin_tone:)" + U1F46F200D2640FE0F = "👯‍♀️", "👯‍♀️ (:women_with_bunny_ears:)" + U1F46F200D2640 = "👯‍♀", "👯‍♀ (:women_with_bunny_ears:)" + U1F93C200D2640FE0F = "🤼‍♀️", "🤼‍♀️ (:women_wrestling:)" + U1F93C200D2640 = "🤼‍♀", "🤼‍♀ (:women_wrestling:)" + U1F6BA = "🚺", "🚺 (:women’s_room:)" + U1FAB5 = "🪵", "🪵 (:wood:)" + U1F974 = "🥴", "🥴 (:woozy_face:)" + U1F5FAFE0F = "🗺️", "🗺️ (:world_map:)" + U1F5FA = "🗺", "🗺 (:world_map:)" + U1FAB1 = "🪱", "🪱 (:worm:)" + U1F61F = "😟", "😟 (:worried_face:)" + U1F381 = "🎁", "🎁 (:wrapped_gift:)" + U1F527 = "🔧", "🔧 (:wrench:)" + U270DFE0F = "✍️", "✍️ (:writing_hand:)" + U270D = "✍", "✍ (:writing_hand:)" + U270D1F3FF = "✍🏿", "✍🏿 (:writing_hand_dark_skin_tone:)" + U270D1F3FB = "✍🏻", "✍🏻 (:writing_hand_light_skin_tone:)" + U270D1F3FE = "✍🏾", "✍🏾 (:writing_hand_medium-dark_skin_tone:)" + U270D1F3FC = "✍🏼", "✍🏼 (:writing_hand_medium-light_skin_tone:)" + U270D1F3FD = "✍🏽", "✍🏽 (:writing_hand_medium_skin_tone:)" + U1FA7B = "🩻", "🩻 (:x-ray:)" + U1F9F6 = "🧶", "🧶 (:yarn:)" + U1F971 = "🥱", "🥱 (:yawning_face:)" + U1F7E1 = "🟡", "🟡 (:yellow_circle:)" + U1F49B = "💛", "💛 (:yellow_heart:)" + U1F7E8 = "🟨", "🟨 (:yellow_square:)" + U1F4B4 = "💴", "💴 (:yen_banknote:)" + U262FFE0F = "☯️", "☯️ (:yin_yang:)" + U262F = "☯", "☯ (:yin_yang:)" + U1FA80 = "🪀", "🪀 (:yo-yo:)" + U1F92A = "🤪", "🤪 (:zany_face:)" + U1F993 = "🦓", "🦓 (:zebra:)" + U1F910 = "🤐", "🤐 (:zipper-mouth_face:)" + U1F9DF = "🧟", "🧟 (:zombie:)" + U1F1E61F1FD = "🇦🇽", "🇦🇽 (:Åland_Islands:)" diff --git a/app/crews/models/crew.py b/app/crews/models/crew.py index 048543e..b7ae8fd 100644 --- a/app/crews/models/crew.py +++ b/app/crews/models/crew.py @@ -1,8 +1,10 @@ -from django.core.validators import MinValueValidator, MaxValueValidator +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator from django.db import models -from crews.validators import EmojiValidator -from users.models import User, UserBojLevelChoices +from crews import enums +from users.models import User +from users.models import UserBojLevelChoices class Crew(models.Model): @@ -11,12 +13,11 @@ class Crew(models.Model): unique=True, help_text='크루 이름을 입력해주세요. (최대 20자)', ) - icon = models.CharField( - max_length=2, - validators=[EmojiValidator(message='이모지 형식이 아닙니다.')], + icon = models.TextField( + choices=enums.EmojiChoices.choices, null=False, blank=False, - default='🚢', + default=enums.EmojiChoices.U1F6A2, # :ship: help_text='크루 아이콘을 입력해주세요. (이모지)', ) max_members = models.IntegerField( diff --git a/app/crews/models/crew_activity.py b/app/crews/models/crew_activity.py index 3d2e299..4e1a7dd 100644 --- a/app/crews/models/crew_activity.py +++ b/app/crews/models/crew_activity.py @@ -33,12 +33,5 @@ class Meta: ordering = ['start_at'] get_latest_by = ['end_at'] - @classmethod - def opened_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: - """활동 시작 전이거나 종료된 활동을 제외한 활동 목록을 반환합니다.""" - return cls.objects.filter(crew=crew, start_at__lte=timezone.now(), end_at__gte=timezone.now()) - - @classmethod - def closed_of_crew(cls, crew: Crew) -> models.QuerySet[CrewActivity]: - """종료된 활동 목록을 반환합니다.""" - return cls.objects.filter(crew=crew, end_at__lt=timezone.now()) + def __str__(self) -> str: + return f"[{self.pk}: {self.name} ({self.start_at.date()} ~ {self.end_at.date()})]" diff --git a/app/crews/models/crew_activity_problem.py b/app/crews/models/crew_activity_problem.py index bace4b7..23faa98 100644 --- a/app/crews/models/crew_activity_problem.py +++ b/app/crews/models/crew_activity_problem.py @@ -48,6 +48,10 @@ class Meta: ] ordering = ['order'] + def save(self, *args, **kwargs) -> None: + assert self.crew == self.activity.crew + return super().save(*args, **kwargs) + def __repr__(self) -> str: return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' diff --git a/app/crews/validators.py b/app/crews/validators.py deleted file mode 100644 index e02a884..0000000 --- a/app/crews/validators.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.core.exceptions import ValidationError -from django.core.validators import BaseValidator - -from crews.enums import Emoji - - -class EmojiValidator(BaseValidator): - def __init__(self, message: str = None) -> None: - self.message = message - - def __call__(self, value) -> None: - try: - Emoji(value) # just checking if it's valid emoji - except ValueError: - raise ValidationError(self.message, params={"value": value}) From e5164b5fa93bb7e2a735595dbce74dc196d0f69c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 19:30:35 +0900 Subject: [PATCH 367/552] =?UTF-8?q?feat(crews):=20=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=8B=A0=EC=B2=AD=20=EC=8A=B9=EC=9D=B8/=EA=B1=B0=EC=A0=88=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 3 ++ app/crews/admin.py | 30 ++++++++++- app/crews/models/crew_applicant.py | 50 +++++++----------- app/crews/serializers/__init__.py | 21 ++++++++ app/crews/services/__init__.py | 81 +++++++++++++++++++++++++++++- app/crews/views/__init__.py | 41 +++++++++++++++ 6 files changed, 190 insertions(+), 36 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index de54cdd..533a02a 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -39,6 +39,9 @@ path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crews//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crews//activity", crews.views.CrewDashboardAPIView.as_view()), + path("crews//apply", crews.views.CrewApplicantCreateAPIView.as_view()), + path("crews/applications//accept", crews.views.AcceptApplicationAPIView.as_view()), + path("crews/applications//reject", crews.views.RejectApplicationAPIView.as_view()), path("problems/", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), diff --git a/app/crews/admin.py b/app/crews/admin.py index 245c675..c3fe63e 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -1,5 +1,6 @@ from django.contrib import admin -from django.utils import timezone +from django.db.models import QuerySet +from django.http.request import HttpRequest from crews import models from crews import services @@ -8,7 +9,6 @@ admin.site.register([ models.CrewActivityProblem, - models.CrewApplicant, ]) @@ -122,3 +122,29 @@ class CrewSubmittableLanguageModelAdmin(admin.ModelAdmin): models.CrewSubmittableLanguage.field_name.CREW, models.CrewSubmittableLanguage.field_name.LANGUAGE, ] + + +@admin.register(models.CrewApplicant) +class CrewApplicantModelAdmin(admin.ModelAdmin): + list_display = [ + models.CrewApplicant.field_name.CREW, + models.CrewApplicant.field_name.USER, + models.CrewApplicant.field_name.IS_ACCEPTED, + models.CrewApplicant.field_name.REVIEWED_BY, + ] + actions = [ + 'accept', + 'reject', + ] + + @admin.action(description="Accept user") + def accept(self, request: HttpRequest, queryset: QuerySet[models.CrewApplicant]): + for applicant in queryset: + services.crew_applicant.accept(applicant, request.user) + services.crew_applicant.notify_accepted(applicant) + + @admin.action(description="Reject user") + def reject(self, request: HttpRequest, queryset: QuerySet[models.CrewApplicant]): + for applicant in queryset: + services.crew_applicant.reject(applicant, request.user) + services.crew_applicant.notify_rejected(applicant) diff --git a/app/crews/models/crew_applicant.py b/app/crews/models/crew_applicant.py index 6c2391a..db8ac72 100644 --- a/app/crews/models/crew_applicant.py +++ b/app/crews/models/crew_applicant.py @@ -1,5 +1,4 @@ -from django.db import models, transaction -from django.utils import timezone +from django.db import models from users.models import User from crews.models.crew import Crew @@ -52,12 +51,6 @@ class field_name: REVIEWED_BY = 'reviewed_by' class Meta: - constraints = [ - models.UniqueConstraint( - fields=['crew', 'user'], - name='unique_applicant_per_crew', - ), - ] ordering = ['reviewed_by', 'created_at'] def __repr__(self) -> str: @@ -67,28 +60,19 @@ def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' def save(self, *args, **kwargs) -> None: - # 같은 크루에 여러 번 가입하는 것을 방지 - if self.crew.members.filter(user=self.user).exists(): - raise ValueError('이미 가입한 크루에 가입 신청을 할 수 없습니다.') - return super().save(*args, **kwargs) - - def accept(self, commit=True) -> CrewMember: - """크루 가입 신청을 수락합니다.""" - member = CrewMember( - crew=self.crew, - user=self.user, - ) - self.is_accepted = True - self.reviewed_at = timezone.now() - if commit: - with transaction.atomic(): - member.save() - self.save() - return member - - def reject(self, commit=True): - """크루 가입 신청을 거절합니다.""" - self.is_accepted = False - self.reviewed_at = timezone.now() - if commit: - self.save() + try: + # 같은 크루에 여러 번 가입하는 것을 방지 + assert not CrewMember.objects.filter(**{ + CrewMember.field_name.CREW: self.crew, + CrewMember.field_name.USER: self.user, + }).exclude(pk=self.pk).exists(), '이미 가입한 크루에 가입 신청을 할 수 없습니다.' + # 아직 검토되지 않은 신청이 있으면 가입 불가 + assert not CrewApplicant.objects.filter(**{ + CrewApplicant.field_name.CREW: self.crew, + CrewApplicant.field_name.USER: self.user, + CrewApplicant.field_name.REVIEWED_BY: None, + }).exclude(pk=self.pk).exists(), '크루에 아직 검토되지 않은 지원 이력이 있습니다.' + except AssertionError as e: + raise ValueError from e + else: + return super().save(*args, **kwargs) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index a1cf781..f789233 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -2,6 +2,7 @@ from crews import models from crews.serializers import fields +from users.serializers import UserMinimalSerializer PK = 'id' @@ -89,3 +90,23 @@ class Meta: 'problems', ] read_only_fields = ['__all__'] + + +class CrewApplicationSerializer(serializers.ModelSerializer): + user = UserMinimalSerializer(read_only=True) + + class Meta: + model = models.CrewApplicant + fields = [ + PK, + models.CrewApplicant.field_name.CREW, + models.CrewApplicant.field_name.MESSAGE, + models.CrewApplicant.field_name.USER, + models.CrewApplicant.field_name.IS_ACCEPTED, + models.CrewApplicant.field_name.CREATED_AT, + ] + read_only_fields = [ + models.CrewApplicant.field_name.CREW, + models.CrewApplicant.field_name.IS_ACCEPTED, + models.CrewApplicant.field_name.CREATED_AT, + ] diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index 4cad22d..6ec3f59 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -1,8 +1,11 @@ +from textwrap import dedent from typing import Iterable from typing import List from typing import Optional +from django.core.mail import send_mail from django.db.models import QuerySet +from django.db.transaction import atomic from django.utils import timezone from crews import dto @@ -146,7 +149,8 @@ def of_crew(crew: models.Crew, exclude_future=True) -> QuerySet[models.CrewActiv models.CrewActivity.field_name.CREW: crew, } if exclude_future: - kwargs[models.CrewActivity.field_name.START_AT + '__lte'] = timezone.now() + kwargs[models.CrewActivity.field_name.START_AT + + '__lte'] = timezone.now() return models.CrewActivity.objects.filter(**kwargs).order_by(models.CrewActivity.field_name.START_AT) @staticmethod @@ -174,3 +178,78 @@ def number(activity: models.CrewActivity) -> int: models.CrewActivity.field_name.CREW: activity.crew, models.CrewActivity.field_name.START_AT+'__lte': activity.start_at, }).count() + + +class crew_applicant: + @staticmethod + def notify_captain(applicant: models.CrewApplicant): + assert isinstance(applicant, models.CrewApplicant) + captain = models.CrewMember.objects.get(**{ + models.CrewMember.field_name.CREW: applicant.crew, + models.CrewMember.field_name.IS_CAPTAIN: True, + }) + send_mail( + subject='[Time Limit Exceeded] 새로운 크루 가입 신청', + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 새로운 가입 신청이 왔어요! + + 지원자의 메시지: + ``` + {applicant.message} + ``` + + 수락하시려면 [여기]를 클릭해주세요. + """), + from_email=None, + recipient_list=[captain.user.email], + fail_silently=False, + ) + + @staticmethod + def notify_accepted(applicant: models.CrewApplicant): + assert isinstance(applicant, models.CrewApplicant) + assert applicant.is_accepted + send_mail( + subject=f"""[Time Limit Exceeded] 크루 가입 신청이 승인되었습니다""", + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 가입하신 것을 축하해요! + + [여기]를 눌러 크루 대시보드로 바로가기 + """), + from_email=None, + recipient_list=[applicant.user.email], + fail_silently=False, + ) + + @staticmethod + def notify_rejected(applicant: models.CrewApplicant): + assert isinstance(applicant, models.CrewApplicant) + assert not applicant.is_accepted + send_mail( + subject=f"""[Time Limit Exceeded] 크루 가입 신청이 거절되었습니다""", + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 아쉽게도 가입하지 못했어요! + """), + from_email=None, + recipient_list=[applicant.user.email], + fail_silently=False, + ) + + @staticmethod + def accept(applicant: models.CrewApplicant, reviewed_by: User): + with atomic(): + applicant.is_accepted = True + applicant.reviewed_at = timezone.now() + applicant.reviewed_by = reviewed_by + applicant.save() + models.CrewMember.objects.create(**{ + models.CrewApplicant.field_name.CREW: applicant.crew, + models.CrewApplicant.field_name.USER: applicant.user, + }) + + @staticmethod + def reject(applicant: models.CrewApplicant, reviewed_by: User): + applicant.is_accepted = False + applicant.reviewed_at = timezone.now() + applicant.reviewed_by = reviewed_by + applicant.save() diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 19b8353..e3ffa1c 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,5 +1,8 @@ +from drf_yasg.utils import swagger_auto_schema from rest_framework import generics from rest_framework import permissions +from rest_framework import status +from rest_framework.response import Response from crews import dto from crews import models @@ -83,3 +86,41 @@ def get_queryset(self): return services.crew.of_user_queryset( include_user=self.request.user, ) + + +class CrewApplicantCreateAPIView(generics.CreateAPIView): + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewApplicationSerializer + lookup_field = 'id' + + def perform_create(self, serializer: serializers.CrewApplicationSerializer): + instance = serializer.save(**{ + models.CrewApplicant.field_name.CREW: self.get_object(), + models.CrewApplicant.field_name.USER: self.request.user, + }) + services.crew_applicant.notify_captain(instance) + + +class AcceptApplicationAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant.objects + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + instance: models.CrewApplicant = self.get_object() + services.crew_applicant.accept(instance, self.request.user) + services.crew_applicant.notify_accepted(instance) + return Response(status=status.HTTP_200_OK) + + +class RejectApplicationAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant.objects + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + instance: models.CrewApplicant = self.get_object() + services.crew_applicant.reject(instance, self.request.user) + services.crew_applicant.notify_rejected(instance) + return Response(status=status.HTTP_200_OK) From ed4823bb2eb70af31c6da2c9ce14a53a2b5d1b8e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 20:06:59 +0900 Subject: [PATCH 368/552] feat(crews): add CrewCreateSerializer --- app/crews/serializers/__init__.py | 20 ++++++++++++++++++++ app/crews/services/__init__.py | 26 ++++++++++++++++++++++++-- app/crews/views/__init__.py | 22 ++++++++++++++++------ 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index f789233..a57153f 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from crews import enums from crews import models from crews.serializers import fields from users.serializers import UserMinimalSerializer @@ -8,6 +9,25 @@ PK = 'id' +class CrewCreateSerializer(serializers.ModelSerializer): + languages = serializers.MultipleChoiceField(choices=enums.ProgrammingLanguageChoices.choices) + + class Meta: + model = models.Crew + fields = [ + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.MAX_MEMBERS, + 'languages', + models.Crew.field_name.MIN_BOJ_LEVEL, + models.Crew.field_name.CUSTOM_TAGS, + models.Crew.field_name.NOTICE, + models.Crew.field_name.IS_RECRUITING, + models.Crew.field_name.IS_ACTIVE, + ] + read_only_fields = ['__all__'] + + class RecruitingCrewSerializer(serializers.ModelSerializer): """크루 목록""" diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index 6ec3f59..b1b6277 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -7,6 +7,7 @@ from django.db.models import QuerySet from django.db.transaction import atomic from django.utils import timezone +from rest_framework import exceptions from crews import dto from crews import enums @@ -45,8 +46,8 @@ def problem_statistics(crew: models.Crew) -> dto.ProblemStatistic: class crew: @staticmethod - def of_user_queryset(include_user: Optional[User] = None, - exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: + def of_user(include_user: Optional[User] = None, + exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: """특정 사용자가 속하거나 속하지 않은 크루 목록을 조회하는 쿼리를 반환한다.""" queryset = models.Crew.objects if include_user is not None: @@ -61,6 +62,7 @@ def of_user_queryset(include_user: Optional[User] = None, @classmethod def tags(cls, crew: models.Crew) -> List[dto.CrewTag]: + assert isinstance(crew, models.Crew) # 태그의 나열 순서는 리스트에 선언한 순서를 따름. return [ *cls._get_language_tags(crew), @@ -70,6 +72,7 @@ def tags(cls, crew: models.Crew) -> List[dto.CrewTag]: @classmethod def _get_language_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: + assert isinstance(crew, models.Crew) submittable_languages = models.CrewSubmittableLanguage.objects.filter(**{ models.CrewSubmittableLanguage.field_name.CREW: crew, }) @@ -112,12 +115,15 @@ def _get_custom_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: @staticmethod def member_count(crew: models.Crew) -> int: + assert isinstance(crew, models.Crew) return models.CrewMember.objects.filter(**{ models.CrewMember.field_name.CREW: crew, }).count() @classmethod def is_joinable(cls, crew: models.Crew, user: User) -> bool: + assert isinstance(crew, models.Crew) + assert isinstance(user, User) if not crew.is_recruiting: return False if cls.member_count(crew) >= crew.max_members: @@ -133,11 +139,27 @@ def is_joinable(cls, crew: models.Crew, user: User) -> bool: @staticmethod def is_member(crew: models.Crew, user: User) -> bool: + assert isinstance(crew, models.Crew) + assert isinstance(user, User) return models.CrewMember.objects.filter(**{ models.CrewMember.field_name.CREW: crew, models.CrewMember.field_name.USER: user, }).exists() + @staticmethod + def set_submittable_languages(crew: models.Crew, languages: List[str]): + assert isinstance(crew, models.Crew) + assert isinstance(languages, list) + entities = [] + for lang in languages: + if not isinstance(lang, str) or lang not in enums.ProgrammingLanguageChoices: + raise exceptions.ValidationError(f'{lang}은 선택 가능한 언어가 아닙니다.') + entities.append(models.CrewSubmittableLanguage(**{ + models.CrewSubmittableLanguage.field_name.CREW: crew, + models.CrewSubmittableLanguage.field_name.LANGUAGE: lang, + })) + models.CrewSubmittableLanguage.objects.bulk_create(entities) + class crew_acitivity: @staticmethod diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index e3ffa1c..7f3ec5f 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,4 +1,5 @@ from drf_yasg.utils import swagger_auto_schema +from django.db.transaction import atomic from rest_framework import generics from rest_framework import permissions from rest_framework import status @@ -15,7 +16,16 @@ class CrewCreateAPIView(generics.CreateAPIView): queryset = models.Crew.objects.all() permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.RecruitingCrewSerializer + serializer_class = serializers.CrewCreateSerializer + + def perform_create(self, serializer: serializers.CrewCreateSerializer): + languages = serializer.validated_data.pop('languages') + with atomic(): + crew = serializer.save(**{ + models.Crew.field_name.CREATED_BY: self.request.user, + }) + services.crew.set_submittable_languages(crew, languages) + return crew class RecruitingCrewListAPIView(generics.ListAPIView): @@ -26,7 +36,7 @@ class RecruitingCrewListAPIView(generics.ListAPIView): def get_queryset(self): # 본인이 속한 크루는 제외. - queryset = services.crew.of_user_queryset( + queryset = services.crew.of_user( exclude_user=self.request.user, ) return queryset.filter(**{ @@ -41,7 +51,7 @@ class MyCrewAPIView(generics.ListAPIView): serializer_class = serializers.MyCrewSerializer def get_queryset(self): - queryset = services.crew.of_user_queryset( + queryset = services.crew.of_user( include_user=self.request.user, ) # 활동 종료된 크루는 뒤로 가도록 정렬 @@ -56,7 +66,7 @@ class CrewDashboardAPIView(generics.RetrieveAPIView): lookup_field = 'id' def get_queryset(self): - return services.crew.of_user_queryset( + return services.crew.of_user( include_user=self.request.user, ) @@ -67,7 +77,7 @@ class CrewStatisticsAPIView(generics.RetrieveAPIView): lookup_field = 'id' def get_queryset(self): - return services.crew.of_user_queryset( + return services.crew.of_user( include_user=self.request.user, ) @@ -83,7 +93,7 @@ class CrewActivityAPIView(generics.RetrieveAPIView): lookup_field = 'id' def get_queryset(self): - return services.crew.of_user_queryset( + return services.crew.of_user( include_user=self.request.user, ) From 41ba011a3ee6cc01498ca7c91a896df64df8a6ef Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 16 Aug 2024 20:20:09 +0900 Subject: [PATCH 369/552] =?UTF-8?q?fix(crews):=20CrewCreateAPIView=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EC=8B=9C=20languages=20=EC=86=8D=EC=84=B1?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EB=B0=9C=EC=83=9D=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers/__init__.py | 5 +++++ app/crews/services/__init__.py | 2 +- app/crews/views/__init__.py | 10 +++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index a57153f..2faa517 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -27,6 +27,11 @@ class Meta: ] read_only_fields = ['__all__'] + def save(self, **kwargs): + if 'langauges' in self.validated_data: + self.validated_data.pop('languages') + return super().save(**kwargs) + class RecruitingCrewSerializer(serializers.ModelSerializer): """크루 목록""" diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index b1b6277..aa08e24 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -149,7 +149,7 @@ def is_member(crew: models.Crew, user: User) -> bool: @staticmethod def set_submittable_languages(crew: models.Crew, languages: List[str]): assert isinstance(crew, models.Crew) - assert isinstance(languages, list) + assert isinstance(languages, Iterable) entities = [] for lang in languages: if not isinstance(lang, str) or lang not in enums.ProgrammingLanguageChoices: diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 7f3ec5f..ea8b85c 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -19,7 +19,7 @@ class CrewCreateAPIView(generics.CreateAPIView): serializer_class = serializers.CrewCreateSerializer def perform_create(self, serializer: serializers.CrewCreateSerializer): - languages = serializer.validated_data.pop('languages') + languages = [*serializer.validated_data.pop('languages')] with atomic(): crew = serializer.save(**{ models.Crew.field_name.CREATED_BY: self.request.user, @@ -27,6 +27,14 @@ def perform_create(self, serializer: serializers.CrewCreateSerializer): services.crew.set_submittable_languages(crew, languages) return crew + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + serializer = serializers.MyCrewSerializer(instance=instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + class RecruitingCrewListAPIView(generics.ListAPIView): """크루 목록""" From 89ae231efb314daf172096d293d64fa289d2aff1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 19 Aug 2024 21:32:51 +0900 Subject: [PATCH 370/552] chore(config): apply `TimedRotatingFileHandler` to `django.server` logger --- app/config/settings.py | 54 +++++++++++++++++++++++++++++++++++++++++- app/config/utils.py | 9 +++++++ app/logs/.gitkeep | 0 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 app/config/utils.py create mode 100644 app/logs/.gitkeep diff --git a/app/config/settings.py b/app/config/settings.py index 72fcab4..b6d3ef5 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -136,7 +136,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), } @@ -183,3 +183,55 @@ # Swagger Settings (DRf-YASG) LOGIN_URL = "/api/v1/auth/signin" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": { + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + "require_debug_true": { + "()": "django.utils.log.RequireDebugTrue", + }, + }, + "formatters": { + "django.server": { + "()": "config.utils.ColorlessServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + } + }, + "handlers": { + "console": { + "level": "INFO", + "filters": ["require_debug_true"], + 'class': 'logging.FileHandler', + 'filename': 'logs/console.log', + }, + "django.server": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/django.server.log', + 'when': 'D', + "formatter": "django.server", + }, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + 'class': 'logging.FileHandler', + 'filename': 'logs/mail_admins.log', + }, + }, + "loggers": { + "django": { + "handlers": ["console", "mail_admins"], + "level": "INFO", + }, + "django.server": { + "handlers": ["django.server"], + "level": "INFO", + "propagate": False, + }, + }, +} diff --git a/app/config/utils.py b/app/config/utils.py new file mode 100644 index 0000000..ee70b27 --- /dev/null +++ b/app/config/utils.py @@ -0,0 +1,9 @@ +from typing import Any +from django.core.management.color import no_style +from django.utils.log import ServerFormatter + + +class ColorlessServerFormatter(ServerFormatter): + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self.style = no_style() diff --git a/app/logs/.gitkeep b/app/logs/.gitkeep new file mode 100644 index 0000000..e69de29 From ad913d6670336eb802776b6e0abd20209c4252c1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 19 Aug 2024 21:41:53 +0900 Subject: [PATCH 371/552] =?UTF-8?q?refactor(crews):=20application=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EB=B7=B0=EB=A5=BC=20=EB=B3=84=EA=B0=9C?= =?UTF-8?q?=EC=9D=98=20=ED=8C=8C=EC=9D=BC=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20?= =?UTF-8?q?+=20=EB=B7=B0=20=ED=81=B4=EB=9E=98=EC=8A=A4=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 6 ++--- app/crews/views/__init__.py | 41 +++------------------------- app/crews/views/applicantions.py | 46 ++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 41 deletions(-) create mode 100644 app/crews/views/applicantions.py diff --git a/app/config/urls.py b/app/config/urls.py index 533a02a..fccb3e1 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -39,9 +39,9 @@ path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crews//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crews//activity", crews.views.CrewDashboardAPIView.as_view()), - path("crews//apply", crews.views.CrewApplicantCreateAPIView.as_view()), - path("crews/applications//accept", crews.views.AcceptApplicationAPIView.as_view()), - path("crews/applications//reject", crews.views.RejectApplicationAPIView.as_view()), + path("crews//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), + path("crews/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), + path("crews/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("problems/", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index ea8b85c..5806504 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -9,6 +9,9 @@ from crews import models from crews import serializers from crews import services +from crews.views.applicantions import CrewApplicantionCreateAPIView +from crews.views.applicantions import CrewApplicantionAcceptAPIView +from crews.views.applicantions import CrewApplicantionRejectAPIView class CrewCreateAPIView(generics.CreateAPIView): @@ -104,41 +107,3 @@ def get_queryset(self): return services.crew.of_user( include_user=self.request.user, ) - - -class CrewApplicantCreateAPIView(generics.CreateAPIView): - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewApplicationSerializer - lookup_field = 'id' - - def perform_create(self, serializer: serializers.CrewApplicationSerializer): - instance = serializer.save(**{ - models.CrewApplicant.field_name.CREW: self.get_object(), - models.CrewApplicant.field_name.USER: self.request.user, - }) - services.crew_applicant.notify_captain(instance) - - -class AcceptApplicationAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant.objects - permission_classes = [permissions.IsAuthenticated] - lookup_field = 'id' - - def get(self, request, *args, **kwargs): - instance: models.CrewApplicant = self.get_object() - services.crew_applicant.accept(instance, self.request.user) - services.crew_applicant.notify_accepted(instance) - return Response(status=status.HTTP_200_OK) - - -class RejectApplicationAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant.objects - permission_classes = [permissions.IsAuthenticated] - lookup_field = 'id' - - def get(self, request, *args, **kwargs): - instance: models.CrewApplicant = self.get_object() - services.crew_applicant.reject(instance, self.request.user) - services.crew_applicant.notify_rejected(instance) - return Response(status=status.HTTP_200_OK) diff --git a/app/crews/views/applicantions.py b/app/crews/views/applicantions.py new file mode 100644 index 0000000..e2a9777 --- /dev/null +++ b/app/crews/views/applicantions.py @@ -0,0 +1,46 @@ +from rest_framework import generics +from rest_framework import permissions +from rest_framework import status +from rest_framework.response import Response + +from crews import models +from crews import serializers +from crews import services + + +class CrewApplicantionCreateAPIView(generics.CreateAPIView): + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewApplicationSerializer + lookup_field = 'id' + + def perform_create(self, serializer: serializers.CrewApplicationSerializer): + instance = serializer.save(**{ + models.CrewApplicant.field_name.CREW: self.get_object(), + models.CrewApplicant.field_name.USER: self.request.user, + }) + services.crew_applicant.notify_captain(instance) + + +class CrewApplicantionAcceptAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant.objects + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + instance: models.CrewApplicant = self.get_object() + services.crew_applicant.accept(instance, self.request.user) + services.crew_applicant.notify_accepted(instance) + return Response(status=status.HTTP_200_OK) + + +class CrewApplicantionRejectAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant.objects + permission_classes = [permissions.IsAuthenticated] + lookup_field = 'id' + + def get(self, request, *args, **kwargs): + instance: models.CrewApplicant = self.get_object() + services.crew_applicant.reject(instance, self.request.user) + services.crew_applicant.notify_rejected(instance) + return Response(status=status.HTTP_200_OK) From 6e704d6e08b1e252048a051442e5bfc414c84a08 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 20 Aug 2024 04:42:54 +0900 Subject: [PATCH 372/552] =?UTF-8?q?refactor(config):=20reporters.py?= =?UTF-8?q?=EC=9D=98=20=EB=82=B4=EC=9A=A9=EC=9D=84=20utils.py=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/reporters.py | 20 -------------------- app/config/settings.py | 2 +- app/config/utils.py | 18 ++++++++++++++++++ 3 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 app/config/reporters.py diff --git a/app/config/reporters.py b/app/config/reporters.py deleted file mode 100644 index 704692d..0000000 --- a/app/config/reporters.py +++ /dev/null @@ -1,20 +0,0 @@ - -from django.conf import settings -from django.views import debug - - -class NACLExceptionReporter(debug.ExceptionReporter): - def __init__(self, request, exc_type, exc_value, tb, is_email=False): - super().__init__(request, exc_type, exc_value, tb, is_email) - - - def get_traceback_data(self) -> dict: - """Return a dictionary containing traceback information.""" - if self._get_domain() in settings.ALLOWED_HOSTS: - return super().get_traceback_data() - return {} - - - def _get_domain(self): - host = self.request._get_raw_host() - return host.split(':')[0] diff --git a/app/config/settings.py b/app/config/settings.py index b6d3ef5..1156edf 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -176,7 +176,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" -DEFAULT_EXCEPTION_REPORTER = "config.reporters.NACLExceptionReporter" +DEFAULT_EXCEPTION_REPORTER = "config.utils.NACLExceptionReporter" APPEND_SLASH = False diff --git a/app/config/utils.py b/app/config/utils.py index ee70b27..27b81d6 100644 --- a/app/config/utils.py +++ b/app/config/utils.py @@ -1,9 +1,27 @@ from typing import Any + +from django.conf import settings from django.core.management.color import no_style from django.utils.log import ServerFormatter +from django.views import debug class ColorlessServerFormatter(ServerFormatter): def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.style = no_style() + + +class NACLExceptionReporter(debug.ExceptionReporter): + def __init__(self, request, exc_type, exc_value, tb, is_email=False): + super().__init__(request, exc_type, exc_value, tb, is_email) + + def get_traceback_data(self) -> dict: + """Return a dictionary containing traceback information.""" + if self._get_domain() in settings.ALLOWED_HOSTS: + return super().get_traceback_data() + return {} + + def _get_domain(self): + host = self.request._get_raw_host() + return host.split(':')[0] From 7088e8224fd30ded5c59995bd05ac7b0ebcbe604 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 20 Aug 2024 05:57:11 +0900 Subject: [PATCH 373/552] =?UTF-8?q?refactor(config):=20DisallowedHost=20?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20logging=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 19 ++++++++++++++++++- app/config/utils.py | 24 ++++++++++++++++++------ 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index 1156edf..5ec351b 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -30,7 +30,6 @@ ALLOWED_HOSTS = [ 'tle-kr.com', 'timelimitexceeded.kr', - 'localhost', ] # CORS @@ -196,6 +195,11 @@ }, }, "formatters": { + "standard": { + "()": "config.utils.ColorlessServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + }, "django.server": { "()": "config.utils.ColorlessServerFormatter", "format": "[{server_time}] {message}", @@ -208,6 +212,7 @@ "filters": ["require_debug_true"], 'class': 'logging.FileHandler', 'filename': 'logs/console.log', + "formatter": "standard", }, "django.server": { "level": "INFO", @@ -222,6 +227,13 @@ 'class': 'logging.FileHandler', 'filename': 'logs/mail_admins.log', }, + "django.security.DisallowedHost": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/django.security.DisallowedHost.log', + 'when': 'D', + "formatter": "standard", + }, }, "loggers": { "django": { @@ -233,5 +245,10 @@ "level": "INFO", "propagate": False, }, + 'django.security.DisallowedHost': { + 'handlers': ['django.security.DisallowedHost'], + "level": "DEBUG", + 'propagate': False, + }, }, } diff --git a/app/config/utils.py b/app/config/utils.py index 27b81d6..55209c6 100644 --- a/app/config/utils.py +++ b/app/config/utils.py @@ -1,3 +1,4 @@ +import logging from typing import Any from django.conf import settings @@ -14,14 +15,25 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: class NACLExceptionReporter(debug.ExceptionReporter): def __init__(self, request, exc_type, exc_value, tb, is_email=False): + self.logger = logging.getLogger('django.security.DisallowedHost') super().__init__(request, exc_type, exc_value, tb, is_email) def get_traceback_data(self) -> dict: """Return a dictionary containing traceback information.""" - if self._get_domain() in settings.ALLOWED_HOSTS: - return super().get_traceback_data() - return {} + if self._get_host() not in settings.ALLOWED_HOSTS: + # IP, 혹은 AWS 도메인 주소 패턴을 이용하여 접속을 시도하는 악성 봇들에게 + # 환경변수나 기타 정보가 노출되지 않도록 함. + self.logger.info(f'Illegal access detected from client "{self._get_client_ip_addr()}" to host "{self._get_host()}"') + return {} + return super().get_traceback_data() - def _get_domain(self): - host = self.request._get_raw_host() - return host.split(':')[0] + def _get_host(self) -> str: + return self.request._get_raw_host().split(':')[0] + + def _get_client_ip_addr(self) -> str: + x_forwarded_for = self.request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip_addr = x_forwarded_for.split(',')[0] + else: + ip_addr = self.request.META.get('REMOTE_ADDR') + return ip_addr From 3328c6344791b1a69681d8fc06fbd5367baf5a1e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 20 Aug 2024 06:43:46 +0900 Subject: [PATCH 374/552] feat(notifications): create app notifications --- app/notifications/__init__.py | 0 app/notifications/apps.py | 6 +++ app/notifications/migrations/__init__.py | 0 app/notifications/services.py | 66 ++++++++++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100644 app/notifications/__init__.py create mode 100644 app/notifications/apps.py create mode 100644 app/notifications/migrations/__init__.py create mode 100644 app/notifications/services.py diff --git a/app/notifications/__init__.py b/app/notifications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/notifications/apps.py b/app/notifications/apps.py new file mode 100644 index 0000000..3a08476 --- /dev/null +++ b/app/notifications/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NotificationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "notifications" diff --git a/app/notifications/migrations/__init__.py b/app/notifications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/notifications/services.py b/app/notifications/services.py new file mode 100644 index 0000000..d69239c --- /dev/null +++ b/app/notifications/services.py @@ -0,0 +1,66 @@ +import logging +from textwrap import dedent + +from django.core.mail import send_mail + +import crews.models +import users.models + + +SUBJECT_PREFIX = '[Time Limit Exceeded]' + +LOGGER = logging.getLogger('mail_admins') + + +def notify_crew_application_requested(applicant: crews.models.CrewApplicant): + assert isinstance(applicant, crews.models.CrewApplicant) + send_mail( + subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청', + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 새로운 가입 신청이 왔어요! + + 지원자: {applicant.user.username} + 지원자의 백준 아이디(레벨): {applicant.user.boj_username} ({users.models.UserBojLevelChoices(applicant.user.boj_level).get_name(lang='ko', arabic=False)}) + + 지원자의 메시지: + ``` + {applicant.message} + ``` + + 수락하시려면 [여기]를 클릭해주세요. + """), + recipient_list=[applicant.crew.created_by.email], + from_email=None, + fail_silently=False, + ) + LOGGER.info(f'MAIL crew.application.requested {applicant.crew.created_by.email}') + + +def notify_crew_application_accepted(applicant: crews.models.CrewApplicant): + assert isinstance(applicant, crews.models.CrewApplicant) + send_mail( + subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 승인되었습니다', + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 가입하신 것을 축하해요! + + [여기]를 눌러 크루 대시보드로 바로가기 + """), + recipient_list=[applicant.user.email], + from_email=None, + fail_silently=False, + ) + LOGGER.info(f'MAIL crew.application.accepted {applicant.user.email}') + + +def notify_crew_application_rejected(applicant: crews.models.CrewApplicant): + assert isinstance(applicant, crews.models.CrewApplicant) + send_mail( + subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 거절되었습니다', + message=dedent(f""" + [{applicant.crew.icon} {applicant.crew.name}]에 아쉽게도 가입하지 못했어요! + """), + recipient_list=[applicant.user.email], + from_email=None, + fail_silently=False, + ) + LOGGER.info(f'MAIL crew.application.rejected {applicant.user.email}') From c0e8685f60f70b4d6c0f3e20628e065166445736 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 20:12:19 +0900 Subject: [PATCH 375/552] =?UTF-8?q?chore:=20console=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EB=A1=9C=EA=B9=85=EC=9D=80=20stream=EA=B3=BC=20fil?= =?UTF-8?q?e=20=EB=AA=A8=EB=91=90=EC=97=90=20=EC=B6=9C=EB=A0=A5=EB=90=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 8 +++++++- app/config/utils.py | 14 +++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index 5ec351b..544c72e 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -210,10 +210,16 @@ "console": { "level": "INFO", "filters": ["require_debug_true"], - 'class': 'logging.FileHandler', + 'class': 'config.utils.FileAndStreamHandler', 'filename': 'logs/console.log', "formatter": "standard", }, + "django.mail": { + "level": "ERROR", + "filters": ["require_debug_true"], + 'class': 'config.utils.FileAndStreamHandler', + 'filename': 'logs/django.mail.log', + }, "django.server": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', diff --git a/app/config/utils.py b/app/config/utils.py index 55209c6..704f39e 100644 --- a/app/config/utils.py +++ b/app/config/utils.py @@ -1,4 +1,5 @@ import logging +from sys import stderr from typing import Any from django.conf import settings @@ -13,6 +14,15 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self.style = no_style() +class FileAndStreamHandler(logging.FileHandler): + """기본적으로 logging.FileHandler와 동일하나, STDERR에도 동일한 로그를 같이 출력해주는 Handler이다.""" + def emit(self, record: logging.LogRecord) -> None: + self.stream, stream = stderr, self.stream + super().emit(record) + self.stream = stream + super().emit(record) + + class NACLExceptionReporter(debug.ExceptionReporter): def __init__(self, request, exc_type, exc_value, tb, is_email=False): self.logger = logging.getLogger('django.security.DisallowedHost') @@ -23,7 +33,9 @@ def get_traceback_data(self) -> dict: if self._get_host() not in settings.ALLOWED_HOSTS: # IP, 혹은 AWS 도메인 주소 패턴을 이용하여 접속을 시도하는 악성 봇들에게 # 환경변수나 기타 정보가 노출되지 않도록 함. - self.logger.info(f'Illegal access detected from client "{self._get_client_ip_addr()}" to host "{self._get_host()}"') + self.logger.info( + f'Illegal access detected from client "{self._get_client_ip_addr()}" to host "{self._get_host()}"' + ) return {} return super().get_traceback_data() From aa75d806c0ad72ccd3a8e1c0d5ea502bbf7db2e1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 20:16:40 +0900 Subject: [PATCH 376/552] refactor(notifications): change logger for notification to `django.mail` --- app/notifications/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/notifications/services.py b/app/notifications/services.py index d69239c..1bfdd5e 100644 --- a/app/notifications/services.py +++ b/app/notifications/services.py @@ -9,7 +9,7 @@ SUBJECT_PREFIX = '[Time Limit Exceeded]' -LOGGER = logging.getLogger('mail_admins') +LOGGER = logging.getLogger('django.mail') def notify_crew_application_requested(applicant: crews.models.CrewApplicant): From 6a2278ca35eac5fb2c48d020ee167f4ad0b34fca Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 22:04:03 +0900 Subject: [PATCH 377/552] =?UTF-8?q?refactor(problems):=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EA=B3=BC=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EC=9E=91?= =?UTF-8?q?=EC=97=85=EC=9D=80=20services=EB=A1=9C=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/serializers/fields.py | 38 +++----- app/problems/services/__init__.py | 142 ++++++++++++++++++++++++----- 2 files changed, 131 insertions(+), 49 deletions(-) diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index 0a8c33d..dd3caa5 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -34,47 +34,37 @@ def to_representation(self, problem: models.Problem): class DifficultyField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) - analysis = services.get_analysis(problem) + service = services.ProblemAnalysisService.from_problem(problem) return { - "name_ko": analysis.difficulty.get_name(lang='ko'), - "name_en": analysis.difficulty.get_name(lang='en'), - 'value': analysis.difficulty.value, + "name_ko": service.difficulty().get_name(lang='ko'), + "name_en": service.difficulty().get_name(lang='en'), + 'value': service.difficulty().value, } class AnalysisField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) - analysis = services.get_analysis(problem) - is_analyzed = analysis.difficulty != models.ProblemDifficultyChoices.UNDER_ANALYSIS - tags_queryset = models.ProblemTag.objects.filter(**{ - models.ProblemTag.field_name.KEY+'__in': analysis.tags, - }) - if not is_analyzed: - difficulty_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - time_complexity_description = "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - else: - difficulty_description = "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" - time_complexity_description = "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" + service = services.ProblemAnalysisService.from_problem(problem) return { 'difficulty': { - "name_ko": analysis.difficulty.get_name(lang='ko'), - "name_en": analysis.difficulty.get_name(lang='en'), - 'value': analysis.difficulty.value, - 'description': difficulty_description, + "name_ko": service.difficulty().get_name(lang='ko'), + "name_en": service.difficulty().get_name(lang='en'), + 'value': service.difficulty().value, + 'description': service.difficulty_description(), }, 'time_complexity': { - 'value': analysis.time_complexity, - 'description': time_complexity_description, + 'value': service.time_complexity(), + 'description': service.time_complexity_description(), }, - 'hint': analysis.hint, + 'hints': service.hints(), 'tags': [ { 'key': tag.key, 'name_en': tag.name_en, 'name_ko': tag.name_ko, } - for tag in tags_queryset + for tag in service.tags() ], - 'is_analyzed': is_analyzed, + 'is_analyzed': service.is_analyzed(), } diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py index 4d22485..5b717c7 100644 --- a/app/problems/services/__init__.py +++ b/app/problems/services/__init__.py @@ -1,28 +1,120 @@ -from problems import dto +from __future__ import annotations + +from typing import List +from typing import Optional + +from django.db.models import QuerySet + from problems import models -def get_analysis(problem: models.Problem) -> dto.ProblemAnalysisDTO: - queryset = models.ProblemAnalysis.objects.filter(**{ - models.ProblemAnalysis.field_name.PROBLEM: problem - }) - try: - analysis = queryset.latest() - tag_keys = models.ProblemAnalysisTag.objects.filter(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, - }).values_list( - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, - flat=True, - ) - except models.ProblemAnalysis.DoesNotExist: - return dto.ProblemAnalysisDTO( - time_complexity='', - difficulty=models.ProblemDifficultyChoices.UNDER_ANALYSIS, - ) - else: - return dto.ProblemAnalysisDTO( - time_complexity=analysis.time_complexity, - difficulty=models.ProblemDifficultyChoices(analysis.difficulty), - hint=tuple(analysis.hint), - tags=tuple(tag_keys), - ) +class ProblemService: + def __init__(self, instance: models.Problem) -> None: + assert isinstance(instance, models.Problem) + self.instance = instance + + def analyses(self) -> QuerySet[models.ProblemAnalysis]: + return models.ProblemAnalysis.objects.filter(**{ + models.ProblemAnalysis.field_name.PROBLEM: self.instance, + }) + + def analysis(self) -> Optional[models.ProblemAnalysis]: + if not self.is_analyzed(): + return None + return self.analyses().latest() + + def is_analyzed(self) -> bool: + return self.analyses().exists() + + def analyze(self): + # TODO + pass + + +class ProblemAnalysisService: + @staticmethod + def from_problem(problem: models.Problem) -> ProblemAnalysisService: + analysis = ProblemService(problem).analysis() + return ProblemAnalysisService(analysis) + + def __init__(self, instance: Optional[models.ProblemAnalysis] = None) -> None: + if instance is None: + self._strategy = UnanalyzedProblemAnalysisService() + else: + self._strategy = AnalyzedProblemAnalysisService(instance) + + def is_analyzed(self) -> bool: + return self._strategy.is_analyzed() + + def difficulty(self) -> models.ProblemDifficultyChoices: + return self._strategy.difficulty() + + def difficulty_description(self) -> str: + return self._strategy.difficulty_description() + + def time_complexity(self) -> str: + return self._strategy.time_complexity() + + def time_complexity_description(self) -> str: + return self._strategy.time_complexity_description() + + def tags(self) -> List[models.ProblemTag]: + return self._strategy.tags() + + def hints(self) -> List[str]: + return self._strategy.hints() + + +class UnanalyzedProblemAnalysisService(ProblemAnalysisService): + def __init__(self, *args, **kwargs) -> None: + return + + def is_analyzed(self) -> bool: + return False + + def difficulty(self) -> models.ProblemDifficultyChoices: + return models.ProblemDifficultyChoices.UNDER_ANALYSIS + + def time_complexity(self) -> str: + return '' + + def tags(self) -> List[models.ProblemTag]: + return [] + + def hints(self) -> List[str]: + return [] + + def difficulty_description(self) -> str: + return "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" + + def time_complexity_description(self) -> str: + return "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" + + +class AnalyzedProblemAnalysisService(ProblemAnalysisService): + def __init__(self, instance: models.ProblemAnalysis) -> None: + assert isinstance(instance, models.ProblemAnalysis) + self.instance = instance + + def is_analyzed(self) -> bool: + return True + + def difficulty(self) -> models.ProblemDifficultyChoices: + return models.ProblemDifficultyChoices(self.instance.difficulty) + + def time_complexity(self) -> str: + return self.instance.time_complexity + + def tags(self) -> List[models.ProblemTag]: + return models.ProblemAnalysisTag.objects.filter(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: self.instance, + }).values_list(models.ProblemAnalysisTag.field_name.TAG, flat=True) + + def hints(self) -> List[str]: + return self.instance.hint + + def difficulty_description(self) -> str: + return "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" + + def time_complexity_description(self) -> str: + return "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" From 1ea90051832b7c74dab04fbd22e15eabdb2cf240 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 22:04:23 +0900 Subject: [PATCH 378/552] =?UTF-8?q?fix(users):=20current=20user=20api?= =?UTF-8?q?=EC=97=90=20=EC=98=AC=EB=B0=94=EB=A5=B8=20serializer=20?= =?UTF-8?q?=ED=95=A0=EB=8B=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/views/__init__.py b/app/users/views/__init__.py index 95ea38b..d25138d 100644 --- a/app/users/views/__init__.py +++ b/app/users/views/__init__.py @@ -94,7 +94,7 @@ class CurrentUserAPIView(generics.RetrieveAPIView): """현재 로그인한 사용자 정보 API""" permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.SignUpSerializer + serializer_class = serializers.UserSerializer def get_object(self) -> User: return self.request.user From cc5296236a7509a819d6065aa0408374eb3aac3f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 22:59:37 +0900 Subject: [PATCH 379/552] =?UTF-8?q?refactor(crews):=20=EB=A7=8E=EC=9D=80?= =?UTF-8?q?=20=EC=B1=85=EC=9E=84=EB=93=A4=EC=9D=84=20=EC=A0=81=EC=A0=88?= =?UTF-8?q?=ED=95=9C=20=EB=AA=A8=EB=93=88=EC=97=90=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 16 +- app/crews/admin.py | 34 ++- app/crews/dto/__init__.py | 11 + app/crews/models/__init__.py | 10 - app/crews/permissions.py | 41 +++ app/crews/serializers/__init__.py | 22 +- app/crews/serializers/fields.py | 35 +-- app/crews/services/__init__.py | 280 +----------------- app/crews/services/crew_activity_service.py | 81 +++++ .../services/crew_application_service.py | 41 +++ app/crews/services/crew_service.py | 229 ++++++++++++++ app/crews/tests/__init__.py | 89 ++++++ app/crews/views/__init__.py | 118 +------- app/crews/views/applicantions.py | 46 --- app/crews/views/crew_activity_views.py | 15 + app/crews/views/crew_applicantions_views.py | 65 ++++ app/crews/views/crew_views.py | 74 +++++ 17 files changed, 723 insertions(+), 484 deletions(-) create mode 100644 app/crews/permissions.py create mode 100644 app/crews/services/crew_activity_service.py create mode 100644 app/crews/services/crew_application_service.py create mode 100644 app/crews/services/crew_service.py create mode 100644 app/crews/tests/__init__.py delete mode 100644 app/crews/views/applicantions.py create mode 100644 app/crews/views/crew_activity_views.py create mode 100644 app/crews/views/crew_applicantions_views.py create mode 100644 app/crews/views/crew_views.py diff --git a/app/config/urls.py b/app/config/urls.py index fccb3e1..baba207 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -33,15 +33,15 @@ path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), path("auth/email/check", users.views.EmailCheckAPIView.as_view()), path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), - path("crews/", crews.views.CrewCreateAPIView.as_view()), + path("crews/my", crews.views.MyCrewListAPIView.as_view()), path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), - path("crews/my", crews.views.MyCrewAPIView.as_view()), - path("crews//dashboard", crews.views.CrewDashboardAPIView.as_view()), - path("crews//statistics", crews.views.CrewStatisticsAPIView.as_view()), - path("crews//activity", crews.views.CrewDashboardAPIView.as_view()), - path("crews//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), - path("crews/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), - path("crews/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), + path("crew/", crews.views.CrewCreateAPIView.as_view()), + path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), + path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), + path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), + path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), + path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), + path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), path("problems/", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), diff --git a/app/crews/admin.py b/app/crews/admin.py index c3fe63e..b6dd3cd 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -15,6 +15,7 @@ @admin.register(models.Crew) class CrewModelAdmin(admin.ModelAdmin): list_display = [ + 'id', 'get_display_name', 'get_captain', 'get_members', @@ -87,25 +88,34 @@ class CrewActivityModelAdmin(admin.ModelAdmin): models.CrewActivity.field_name.START_AT, models.CrewActivity.field_name.END_AT, 'nth', - 'is_opened', - 'is_closed', + 'is_in_progress', + 'has_started', + 'has_ended', ] search_fields = [ models.CrewActivity.field_name.CREW+'__'+models.Crew.field_name.NAME, models.CrewActivity.field_name.NAME, ] - @admin.display(boolean=True, description='Is Opened') - def is_opened(self, obj: models.CrewActivity) -> bool: - return services.crew_acitivity.is_opened(obj) - - @admin.display(boolean=True, description='Is Closed') - def is_closed(self, obj: models.CrewActivity) -> bool: - return services.crew_acitivity.is_closed(obj) - - @admin.display(description='N-th') + @admin.display(description='회차 번호') def nth(self, obj: models.CrewActivity) -> int: - return services.crew_acitivity.number(obj) + service = services.CrewActivityService(obj) + return service.nth() + + @admin.display(boolean=True, description='진행 중') + def is_in_progress(self, obj: models.CrewActivity) -> bool: + service = services.CrewActivityService(obj) + return service.is_in_progress() + + @admin.display(boolean=True, description='시작 됨') + def has_started(self, obj: models.CrewActivity) -> bool: + service = services.CrewActivityService(obj) + return service.has_started() + + @admin.display(boolean=True, description='종료 됨') + def has_ended(self, obj: models.CrewActivity) -> bool: + service = services.CrewActivityService(obj) + return service.has_ended() @admin.register(models.CrewSubmittableLanguage) diff --git a/app/crews/dto/__init__.py b/app/crews/dto/__init__.py index 0c0f75a..e09e128 100644 --- a/app/crews/dto/__init__.py +++ b/app/crews/dto/__init__.py @@ -42,6 +42,17 @@ class CrewProblem: last_submitted_date: datetime +@dataclass +class CrewActivity: + activity_id: int + name: str + start_at: datetime + end_at: datetime + is_in_progress: bool + has_started: bool + has_ended: bool + + @dataclass class SubmissionGraphNode: problem_number: int diff --git a/app/crews/models/__init__.py b/app/crews/models/__init__.py index ad85f05..3a24052 100644 --- a/app/crews/models/__init__.py +++ b/app/crews/models/__init__.py @@ -4,13 +4,3 @@ from crews.models.crew_applicant import CrewApplicant from crews.models.crew_member import CrewMember from crews.models.crew_submittable_language import CrewSubmittableLanguage - - -__all__ = ( - 'Crew', - 'CrewActivity', - 'CrewActivityProblem', - 'CrewApplicant', - 'CrewMember', - 'CrewSubmittableLanguage', -) diff --git a/app/crews/permissions.py b/app/crews/permissions.py new file mode 100644 index 0000000..bac5291 --- /dev/null +++ b/app/crews/permissions.py @@ -0,0 +1,41 @@ +from typing import Union + +from rest_framework import exceptions +from rest_framework import permissions +from rest_framework.request import Request + +from crews import models +from crews import services + + +class IsJoinable(permissions.BasePermission): + def has_object_permission(self, request: Request, view, crew: models.Crew): + assert isinstance(crew, models.Crew) + service = services.CrewService(crew) + try: + service.validate_applicant(request.user, raises_exception=True) + except exceptions.ValidationError as exception: + detail = f"크루에 가입할 수 없습니다. ({exception.args[0]})" + raise exceptions.PermissionDenied(detail) + return True + + +class IsMember(permissions.BasePermission): + def has_object_permission(self, request: Request, view, obj: Union[models.Crew, models.CrewActivity]): + if isinstance(obj, models.Crew): + crew = obj + elif isinstance(obj, models.CrewActivity): + crew = obj.crew + else: + raise ValueError('백엔드의 실책으로 보이는 오류') + service = services.CrewService(crew) + if not service.is_member(request.user): + raise exceptions.PermissionDenied('크루 멤버가 아닙니다.') + return True + + +class IsCaptain(permissions.BasePermission): + def has_object_permission(self, request: Request, view, application: models.CrewApplicant) -> bool: + assert isinstance(application, models.CrewApplicant) + service = services.CrewService(application.crew) + return service.is_captain(request.user) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index 2faa517..97e9998 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -2,6 +2,7 @@ from crews import enums from crews import models +from crews import services from crews.serializers import fields from users.serializers import UserMinimalSerializer @@ -9,6 +10,10 @@ PK = 'id' +class NoInputSerializer(serializers.Serializer): + pass + + class CrewCreateSerializer(serializers.ModelSerializer): languages = serializers.MultipleChoiceField(choices=enums.ProgrammingLanguageChoices.choices) @@ -25,12 +30,14 @@ class Meta: models.Crew.field_name.IS_RECRUITING, models.Crew.field_name.IS_ACTIVE, ] - read_only_fields = ['__all__'] + extra_kwargs = { + models.Crew.field_name.CUSTOM_TAGS: { + 'default': list, + } + } def save(self, **kwargs): - if 'langauges' in self.validated_data: - self.validated_data.pop('languages') - return super().save(**kwargs) + return services.CrewService.create(**self.validated_data) class RecruitingCrewSerializer(serializers.ModelSerializer): @@ -67,6 +74,7 @@ class Meta: PK, models.Crew.field_name.ICON, models.Crew.field_name.NAME, + models.Crew.field_name.IS_ACTIVE, 'latest_activity', ] read_only_fields = ['__all__'] @@ -105,7 +113,7 @@ class CrewStatisticsSerializer(serializers.Serializer): tags = fields.ProblemStatisticsTagsField() -class MyCrewDashboardAcitivySerializer(serializers.ModelSerializer): +class CrewActivityDashboardSerializer(serializers.ModelSerializer): problems = fields.CrewAcitivityProblemsField() class Meta: @@ -135,3 +143,7 @@ class Meta: models.CrewApplicant.field_name.IS_ACCEPTED, models.CrewApplicant.field_name.CREATED_AT, ] + + +class CrewApplicationCreateSerializer(serializers.Serializer): + message = serializers.CharField() diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index 0b92d9b..d140099 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -18,9 +18,8 @@ def to_representation(self, crew: models.Crew): "date_start_at": None, "date_end_at": None, } - queryset = services.crew_acitivity.of_crew(crew) try: - activity = queryset.latest() + service = services.CrewActivityService.last_started(crew) except models.CrewActivity.DoesNotExist: return { "name": "등록된 활동 없음", @@ -29,9 +28,9 @@ def to_representation(self, crew: models.Crew): } else: return { - "name": f"{queryset.count()}회차", - "date_start_at": activity.start_at, - "date_end_at": activity.end_at, + "name": f"{service.nth()}회차", + "date_start_at": service.instance.start_at, + "date_end_at": service.instance.end_at, } @@ -40,9 +39,7 @@ class CrewMembersField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - queryset = models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - }) + service = services.CrewService(crew) image_field = serializers.ImageField() return [ { @@ -50,7 +47,7 @@ def to_representation(self, crew: models.Crew): "profile_image": image_field.to_representation(member.user.profile_image), "is_captain": member.is_captain, } - for member in queryset + for member in service.query_members() ] @@ -59,8 +56,9 @@ class CrewMemberCountField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) + service = services.CrewService(crew) return { - "count": services.crew.member_count(crew), + "count": service.query_members().count(), "max_count": crew.max_members, } @@ -70,13 +68,14 @@ class CrewTagsField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) + service = services.CrewService(crew) return [ { 'key': tag.key, 'name': tag.name, 'type': tag.type.value, } - for tag in services.crew.tags(crew) + for tag in service.tags() ] @@ -85,7 +84,8 @@ def to_representation(self, crew: models.Crew): user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) assert isinstance(user, User) - return services.crew.is_joinable(crew, user) + service = services.CrewService(crew) + return service.validate_applicant(user) class CrewIsMemberField(serializers.SerializerMethodField): @@ -93,19 +93,20 @@ def to_representation(self, crew: models.Crew): user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) assert isinstance(user, User) - return services.crew.is_member(crew, user) + service = services.CrewService(crew) + return service.is_member(user) class CrewActivitiesField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - queryset = services.crew_acitivity.of_crew(crew) + service = services.CrewService(crew) return [ { - 'id': activity.pk, - 'name': f'{n}회차', + 'activity_id': activity.activity_id, + 'name': activity.name, } - for n, activity in enumerate(queryset, start=1) + for activity in service.activities() ] diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index aa08e24..99f93da 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -1,277 +1,3 @@ -from textwrap import dedent -from typing import Iterable -from typing import List -from typing import Optional - -from django.core.mail import send_mail -from django.db.models import QuerySet -from django.db.transaction import atomic -from django.utils import timezone -from rest_framework import exceptions - -from crews import dto -from crews import enums -from crews import models -from problems.models import ProblemAnalysis -from problems.models import ProblemDifficultyChoices -from users.models import User -from users.models import UserBojLevelChoices - - -def problem_statistics(crew: models.Crew) -> dto.ProblemStatistic: - assert isinstance(crew, models.Crew) - statistics = dto.ProblemStatistic() - queryset = models.CrewActivityProblem.objects.filter(**{ - models.CrewActivityProblem.field_name.CREW: crew, - }) - for activity_problem in queryset: - statistics.sample_count += 1 - try: - analysis = ProblemAnalysis.objects.filter(**{ - ProblemAnalysis.field_name.PROBLEM: activity_problem.problem, - }).latest() - except ProblemAnalysis.DoesNotExist: - statistics.difficulty[ProblemDifficultyChoices.UNDER_ANALYSIS.value] += 1 - else: - statistics.difficulty[analysis.difficulty] += 1 - for tag in analysis.tags: - problem_tag = dto.ProblemTag( - key=tag.key, - name_ko=tag.name_ko, - name_en=tag.name_en, - ) - statistics.tags[problem_tag] += 1 - return statistics - - -class crew: - @staticmethod - def of_user(include_user: Optional[User] = None, - exclude_user: Optional[User] = None) -> QuerySet[models.Crew]: - """특정 사용자가 속하거나 속하지 않은 크루 목록을 조회하는 쿼리를 반환한다.""" - queryset = models.Crew.objects - if include_user is not None: - queryset = queryset.filter(pk__in=models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: include_user, - }).values_list(models.CrewMember.field_name.CREW)) - if exclude_user is not None: - queryset = queryset.exclude(pk__in=models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: exclude_user, - }).values_list(models.CrewMember.field_name.CREW)) - return queryset - - @classmethod - def tags(cls, crew: models.Crew) -> List[dto.CrewTag]: - assert isinstance(crew, models.Crew) - # 태그의 나열 순서는 리스트에 선언한 순서를 따름. - return [ - *cls._get_language_tags(crew), - *cls._get_level_tags(crew), - *cls._get_custom_tags(crew), - ] - - @classmethod - def _get_language_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: - assert isinstance(crew, models.Crew) - submittable_languages = models.CrewSubmittableLanguage.objects.filter(**{ - models.CrewSubmittableLanguage.field_name.CREW: crew, - }) - for submittable_language in submittable_languages.all(): - programming_language = enums.ProgrammingLanguageChoices( - submittable_language.language) - yield dto.CrewTag( - key=programming_language.value, - name=programming_language.label, - type=enums.CrewTagType.LANGUAGE, - ) - - @classmethod - def _get_level_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: - if crew.min_boj_level is not None: - min_boj_level = UserBojLevelChoices(crew.min_boj_level) - else: - min_boj_level = UserBojLevelChoices.U - # 보여질 문구를 결정 - if min_boj_level == UserBojLevelChoices.U: - name = '티어 무관' - elif min_boj_level.get_tier() == 5: - name = f"{min_boj_level.get_division_name(lang='ko')} 이상" - else: - name = f"{min_boj_level.get_name(lang='ko', arabic=False)} 이상" - yield dto.CrewTag( - key=None, - name=name, - type=enums.CrewTagType.LEVEL, - ) - - @classmethod - def _get_custom_tags(cls, crew: models.Crew) -> Iterable[dto.CrewTag]: - for tag in crew.custom_tags: - yield dto.CrewTag( - key=None, - name=tag, - type=enums.CrewTagType.CUSTOM, - ) - - @staticmethod - def member_count(crew: models.Crew) -> int: - assert isinstance(crew, models.Crew) - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - }).count() - - @classmethod - def is_joinable(cls, crew: models.Crew, user: User) -> bool: - assert isinstance(crew, models.Crew) - assert isinstance(user, User) - if not crew.is_recruiting: - return False - if cls.member_count(crew) >= crew.max_members: - return False - if cls.is_member(crew, user): - return False - if crew.min_boj_level is not None: - return bool( - (user.boj_level is not None) and - (user.boj_level >= crew.min_boj_level) - ) - return True - - @staticmethod - def is_member(crew: models.Crew, user: User) -> bool: - assert isinstance(crew, models.Crew) - assert isinstance(user, User) - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - models.CrewMember.field_name.USER: user, - }).exists() - - @staticmethod - def set_submittable_languages(crew: models.Crew, languages: List[str]): - assert isinstance(crew, models.Crew) - assert isinstance(languages, Iterable) - entities = [] - for lang in languages: - if not isinstance(lang, str) or lang not in enums.ProgrammingLanguageChoices: - raise exceptions.ValidationError(f'{lang}은 선택 가능한 언어가 아닙니다.') - entities.append(models.CrewSubmittableLanguage(**{ - models.CrewSubmittableLanguage.field_name.CREW: crew, - models.CrewSubmittableLanguage.field_name.LANGUAGE: lang, - })) - models.CrewSubmittableLanguage.objects.bulk_create(entities) - - -class crew_acitivity: - @staticmethod - def of_crew(crew: models.Crew, exclude_future=True) -> QuerySet[models.CrewActivity]: - """ - exclude_future: 아직 공개되지 않은 활동도 포함할 지 여부. - """ - kwargs = { - models.CrewActivity.field_name.CREW: crew, - } - if exclude_future: - kwargs[models.CrewActivity.field_name.START_AT + - '__lte'] = timezone.now() - return models.CrewActivity.objects.filter(**kwargs).order_by(models.CrewActivity.field_name.START_AT) - - @staticmethod - def is_opened(activity: models.CrewActivity) -> bool: - """활동이 진행 중인지 여부를 반환합니다.""" - assert isinstance(activity, models.CrewActivity) - return activity.start_at <= timezone.now() <= activity.end_at - - @staticmethod - def is_closed(activity: models.CrewActivity) -> bool: - """활동이 종료되었는지 여부를 반환합니다.""" - assert isinstance(activity, models.CrewActivity) - return activity.end_at < timezone.now() - - @staticmethod - def number(activity: models.CrewActivity) -> int: - assert isinstance(activity, models.CrewActivity) - """활동의 회차 번호를 반환합니다. - - 이 값은 1부터 시작합니다. - 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 - 더한 값을 반환하므로, 고정된 값이 아닙니다. - """ - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: activity.crew, - models.CrewActivity.field_name.START_AT+'__lte': activity.start_at, - }).count() - - -class crew_applicant: - @staticmethod - def notify_captain(applicant: models.CrewApplicant): - assert isinstance(applicant, models.CrewApplicant) - captain = models.CrewMember.objects.get(**{ - models.CrewMember.field_name.CREW: applicant.crew, - models.CrewMember.field_name.IS_CAPTAIN: True, - }) - send_mail( - subject='[Time Limit Exceeded] 새로운 크루 가입 신청', - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 새로운 가입 신청이 왔어요! - - 지원자의 메시지: - ``` - {applicant.message} - ``` - - 수락하시려면 [여기]를 클릭해주세요. - """), - from_email=None, - recipient_list=[captain.user.email], - fail_silently=False, - ) - - @staticmethod - def notify_accepted(applicant: models.CrewApplicant): - assert isinstance(applicant, models.CrewApplicant) - assert applicant.is_accepted - send_mail( - subject=f"""[Time Limit Exceeded] 크루 가입 신청이 승인되었습니다""", - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 가입하신 것을 축하해요! - - [여기]를 눌러 크루 대시보드로 바로가기 - """), - from_email=None, - recipient_list=[applicant.user.email], - fail_silently=False, - ) - - @staticmethod - def notify_rejected(applicant: models.CrewApplicant): - assert isinstance(applicant, models.CrewApplicant) - assert not applicant.is_accepted - send_mail( - subject=f"""[Time Limit Exceeded] 크루 가입 신청이 거절되었습니다""", - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 아쉽게도 가입하지 못했어요! - """), - from_email=None, - recipient_list=[applicant.user.email], - fail_silently=False, - ) - - @staticmethod - def accept(applicant: models.CrewApplicant, reviewed_by: User): - with atomic(): - applicant.is_accepted = True - applicant.reviewed_at = timezone.now() - applicant.reviewed_by = reviewed_by - applicant.save() - models.CrewMember.objects.create(**{ - models.CrewApplicant.field_name.CREW: applicant.crew, - models.CrewApplicant.field_name.USER: applicant.user, - }) - - @staticmethod - def reject(applicant: models.CrewApplicant, reviewed_by: User): - applicant.is_accepted = False - applicant.reviewed_at = timezone.now() - applicant.reviewed_by = reviewed_by - applicant.save() +from crews.services.crew_service import CrewService +from crews.services.crew_activity_service import CrewActivityService +from crews.services.crew_application_service import CrewApplicantionService diff --git a/app/crews/services/crew_activity_service.py b/app/crews/services/crew_activity_service.py new file mode 100644 index 0000000..ec1c650 --- /dev/null +++ b/app/crews/services/crew_activity_service.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from django.db.models import QuerySet +from django.utils import timezone + +from crews import models + + +class CrewActivityService: + @staticmethod + def query_all(crew: models.Crew, order_by=[models.CrewActivity.field_name.START_AT]) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: crew, + }).order_by(*order_by) + + @staticmethod + def query_in_progress(crew: models.Crew) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: crew, + models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), + models.CrewActivity.field_name.END_AT + '__gt': timezone.now(), + }) + + @staticmethod + def query_has_started(crew: models.Crew) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: crew, + models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), + }) + + @staticmethod + def query_has_ended(crew: models.Crew) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: crew, + models.CrewActivity.field_name.END_AT + '__lt': timezone.now(), + }) + + @staticmethod + def last_started(crew: models.Crew) -> CrewActivityService: + """ + 주의: CrewActivity.DoesNotExist를 발생시킬 수도 있습니다. + """ + instance = models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: crew, + models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), + }).latest() + return CrewActivityService(instance) + + def __init__(self, instance: models.CrewActivity) -> None: + assert isinstance(instance, models.CrewActivity) + self.instance = instance + + def query_previous(self) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: self.instance.crew, + models.CrewActivity.field_name.START_AT+'__lt': self.instance.start_at, + }) + + def nth(self) -> int: + """활동의 회차 번호를 반환합니다. + + 이 값은 1부터 시작합니다. + 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 + 더한 값을 반환하므로, 고정된 값이 아닙니다. + + 느린 연산입니다. + 한 번에 여러 회차 번호들을 조회하기 위해 이 함수를 사용하는 것은 권장하지 않습니다. + """ + return self.query_previous().count()+1 + + def is_in_progress(self) -> bool: + """활동이 진행 중인지 여부를 반환합니다.""" + return self.has_started() and not self.has_ended() + + def has_started(self) -> bool: + """활동이 열린적이 있는지 여부를 반환합니다..""" + return self.instance.start_at <= timezone.now() + + def has_ended(self) -> bool: + """활동이 종료되었는지 여부를 반환합니다.""" + return self.instance.end_at < timezone.now() diff --git a/app/crews/services/crew_application_service.py b/app/crews/services/crew_application_service.py new file mode 100644 index 0000000..1179ec2 --- /dev/null +++ b/app/crews/services/crew_application_service.py @@ -0,0 +1,41 @@ +from django.db.transaction import atomic +from django.utils import timezone + +import notifications.services +import users.models +from crews import models + + +class CrewApplicantionService: + @staticmethod + def create(crew: models.Crew, user: users.models.User, message: str) -> models.CrewApplicant: + instance = models.CrewApplicant(**{ + models.CrewApplicant.field_name.CREW: crew, + models.CrewApplicant.field_name.USER: user, + models.CrewApplicant.field_name.MESSAGE: message, + }) + instance.save() + notifications.services.notify_crew_application_requested(instance) + + def __init__(self, instance: models.CrewApplicant): + assert isinstance(instance, models.CrewApplicant) + self.instance = instance + + def reject(self, reviewed_by: users.models.User): + self.instance.is_accepted = False + self.instance.reviewed_by = reviewed_by + self.instance.reviewed_at = timezone.now() + self.instance.save() + notifications.services.notify_crew_application_rejected(self.instance) + + def accept(self, reviewed_by: users.models.User): + self.instance.is_accepted = True + self.instance.reviewed_by = reviewed_by + self.instance.reviewed_at = timezone.now() + with atomic(): + self.instance.save() + models.CrewMember.objects.create(**{ + models.CrewApplicant.field_name.CREW: self.instance.crew, + models.CrewApplicant.field_name.USER: self.instance.user, + }) + notifications.services.notify_crew_application_accepted(self.instance) diff --git a/app/crews/services/crew_service.py b/app/crews/services/crew_service.py new file mode 100644 index 0000000..2694a96 --- /dev/null +++ b/app/crews/services/crew_service.py @@ -0,0 +1,229 @@ +from typing import List +from typing import Iterable + +from django.db.models import QuerySet +from django.db.transaction import atomic +from rest_framework import exceptions + +from crews import dto +from crews import enums +from crews import models +from crews.services.crew_activity_service import CrewActivityService +from problems.models import Problem +from problems.services import ProblemService +from users.models import User +from users.models import UserBojLevelChoices + + +class CrewService: + @staticmethod + def create(languages: List[str] = [], **fields) -> models.Crew: + with atomic(): + crew = models.Crew.objects.create(**fields) + service = CrewService(crew) + service.save_languages(languages) + + @staticmethod + def query_as_member(user: User) -> QuerySet[models.Crew]: + """자신이 멤버로 있는 크루를 조회하는 쿼리를 반환""" + # TODO: Query 최적화 필요 + crew_ids = CrewService._crew_ids_as_member(user) + return models.Crew.objects.filter(pk__in=crew_ids) + + @staticmethod + def query_recruiting(user: User) -> QuerySet[models.Crew]: + """자신이 멤버로 있지 않으면서 크루원을 모집중인 크루를 조회하는 쿼리를 반환""" + crew_ids = CrewService._crew_ids_as_member(user) + return models.Crew.objects.filter(**{ + models.Crew.field_name.IS_RECRUITING: True, + }).exclude(pk__in=crew_ids) + + @staticmethod + def _crew_ids_as_member(user: User) -> List[int]: + if not user.is_authenticated: + return [] + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: user, + }).values_list(models.CrewMember.field_name.CREW) + + def __init__(self, instance: models.Crew) -> None: + assert isinstance(instance, models.Crew) + self.instance = instance + + def query_members(self) -> QuerySet[models.CrewMember]: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: self.instance, + }) + + def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: + return models.CrewSubmittableLanguage.objects.filter(**{ + models.CrewSubmittableLanguage.field_name.CREW: self.instance, + }) + + def query_activities(self) -> QuerySet[models.CrewActivity]: + return CrewActivityService.query_all(self.instance) + + def statistics(self) -> dto.ProblemStatistic: + stat = dto.ProblemStatistic() + for problem in self.problems(): + service = ProblemService(problem) + for tag in service.tags(): + problem_tag = dto.ProblemTag( + key=tag.key, + name_ko=tag.name_ko, + name_en=tag.name_en, + ) + stat.tags[problem_tag] += 1 + stat.difficulty[service.difficulty()] += 1 + stat.sample_count += 1 + return stat + + def activities(self) -> List[dto.CrewActivity]: + activities = [] + queryset = CrewActivityService.query_all(self.instance) + for nth, entity in enumerate(queryset, start=1): + service = CrewActivityService(entity) + # TODO: 회차 이름을 모델 생성과 함께 고정 + activities.append(dto.CrewActivity( + activity_id=service.instance.pk, + name=f'{nth}회차', + start_at=service.instance.start_at, + end_at=service.instance.end_at, + is_in_progress=service.is_in_progress(), + has_started=service.has_started(), + has_ended=service.has_ended(), + )) + return activities + + def captain(self) -> models.CrewMember: + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: self.instance, + models.CrewMember.field_name.IS_CAPTAIN: True, + }) + + def problems(self) -> List[Problem]: + return models.CrewActivityProblem.objects.filter(**{ + models.CrewActivityProblem.field_name.CREW: self.instance, + }).values_list(models.CrewActivityProblem.field_name.PROBLEM, flat=True) + + def languages(self) -> List[enums.ProgrammingLanguageChoices]: + languages = [] + for submittable_language in self.query_languages(): + language = enums.ProgrammingLanguageChoices(submittable_language.language) + languages.append(language) + return languages + + def save_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> List[models.CrewSubmittableLanguage]: + assert isinstance(languages, list) + self.delete_languages() + entities = [] + for language in languages: + self._validate_language(language) + entity = models.CrewSubmittableLanguage(**{ + models.CrewSubmittableLanguage.field_name.CREW: self.instance, + models.CrewSubmittableLanguage.field_name.LANGUAGE: language, + }) + entities.append(entity) + return models.CrewSubmittableLanguage.objects.bulk_create(entities) + + def _validate_language(self, language: str): + assert isinstance(language, str) or isinstance( + language, enums.ProgrammingLanguageChoices) + if language not in enums.ProgrammingLanguageChoices: + raise exceptions.ValidationError(f'{language}은 선택 가능한 언어가 아닙니다.') + + def delete_languages(self): + self.query_languages().delete() + + def min_boj_level(self) -> UserBojLevelChoices: + if self.instance.min_boj_level is None: + return UserBojLevelChoices.U + return UserBojLevelChoices(self.instance.min_boj_level) + + def is_captain(self, user: User) -> bool: + assert isinstance(user, User) + return self.captain().user == user + + def is_member(self, user: User) -> bool: + assert isinstance(user, User) + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.CREW: self.instance, + models.CrewMember.field_name.USER: user, + }).exists() + + def validate_applicant(self, applicant: User, raises_exception=False) -> bool: + assert isinstance(applicant, User) + try: + self._validate_applicant_boj_level(applicant) + except exceptions.ValidationError as exception: + if not raises_exception: + return False + raise exception + return True + + def _validate_applicant(self, applicant: User): + if not self.instance.is_recruiting: + raise exceptions.ValidationError('크루가 현재 크루원을 모집하고 있지 않습니다.') + if self.query_members().count() >= self.instance.max_members: + raise exceptions.ValidationError('크루의 최대 정원을 초과하였습니다.') + if self.is_member(applicant): + raise exceptions.ValidationError('이미 가입한 크루입니다.') + self._validate_applicant_boj_level(applicant) + + def validate_applicant_boj_level(self, applicant: User, raises_exception=False) -> bool: + assert isinstance(applicant, User) + try: + self._validate_applicant_boj_level(applicant) + except exceptions.ValidationError as exception: + if not raises_exception: + return False + raise exception + return True + + def _validate_applicant_boj_level(self, applicant: User): + if self.instance.min_boj_level is None: + return + if applicant.boj_level is None: + raise exceptions.ValidationError('사용자의 백준 레벨을 가져올 수 없습니다.') + if applicant.boj_level < self.instance.min_boj_level: + raise exceptions.ValidationError('최소 백준 레벨 요구조건을 달성하지 못하였습니다.') + + def tags(self) -> List[dto.CrewTag]: + # 태그의 나열 순서는 리스트에 선언한 순서를 따름. + return [ + *self._get_language_tags(), + *self._get_min_level_tags(), + *self._get_custom_tags(), + ] + + def _get_language_tags(self) -> Iterable[dto.CrewTag]: + for language in self.languages(): + yield dto.CrewTag( + key=language.value, + name=language.label, + type=enums.CrewTagType.LANGUAGE, + ) + + def _get_min_level_tags(self) -> Iterable[dto.CrewTag]: + yield dto.CrewTag( + key=None, + name=self._get_min_level_tag_name(), + type=enums.CrewTagType.LEVEL, + ) + + def _get_min_level_tag_name(self) -> str: + min_level = self.min_boj_level() + if min_level == UserBojLevelChoices.U: + return '티어 무관' + elif min_level.get_tier() == 5: + return f"{min_level.get_division_name(lang='ko')} 이상" + else: + return f"{min_level.get_name(lang='ko', arabic=False)} 이상" + + def _get_custom_tags(self) -> Iterable[dto.CrewTag]: + for tag in self.instance.custom_tags: + yield dto.CrewTag( + key=None, + name=tag, + type=enums.CrewTagType.CUSTOM, + ) diff --git a/app/crews/tests/__init__.py b/app/crews/tests/__init__.py new file mode 100644 index 0000000..61d9c5e --- /dev/null +++ b/app/crews/tests/__init__.py @@ -0,0 +1,89 @@ +from django.test import TestCase +from rest_framework.test import APIClient +from rest_framework import status + +from crews.models import Crew +from users.models import User, BojLevelChoices + + +class CrewRecruitingTest(TestCase): + def setUp(self): + self.maxDiff = None + self.client = APIClient() + self.url = '/api/v1/crews/recruiting' + self.user = User.objects.create(**{ + User.field_name.EMAIL: 'email@example.com', + User.field_name.USERNAME: 'username', + User.field_name.PASSWORD: 'password', + User.field_name.BOJ_USERNAME: 'boj_username', + User.field_name.BOJ_LEVEL: BojLevelChoices.S1, + }) + self.crew = Crew.objects.create(**{ + Crew.field_name.NAME: '크루명', + Crew.field_name.ICON: '😀', + Crew.field_name.MAX_MEMBERS: 4, + Crew.field_name.NOTICE: '공지', + Crew.field_name.CUSTOM_TAGS: ["태그1", "태그2",], + Crew.field_name.MIN_BOJ_LEVEL: BojLevelChoices.G5, + Crew.field_name.IS_RECRUITING: True, + Crew.field_name.IS_ACTIVE: True, + Crew.field_name.CREATED_BY: self.user, + }) + + def test_returns_200(self): + res = self.client.get(self.url) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_200_response(self): + res = self.client.get(self.url) + self.assertJSONEqual(res.content, { + 'count': 1, + 'next': None, + 'previous': None, + 'results': [ + { + 'id': self.crew.pk, + 'icon': self.crew.icon, + 'name': self.crew.name, + 'is_active': self.crew.is_active, + 'is_member': False, + 'is_recruiting': self.crew.is_recruiting, + 'is_joinable': False, + 'activities': { + 'count': 0, + 'recent': { + 'nth': None, + 'name': '등록된 활동 없음', + 'start_at': None, + 'end_at': None, + 'is_open': False, + } + }, + 'members': { + 'count': 1, + 'max_count': self.crew.max_members, + 'items': [], + }, + 'tags': { + 'count': 3, + 'items': [ + { + 'key': None, + 'name': '골드 5 이상', + 'type': 'level', + }, + { + 'key': None, + 'name': '태그1', + 'type': 'custom', + }, + { + 'key': None, + 'name': '태그2', + 'type': 'custom', + }, + ] + }, + } + ], + }) diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index 5806504..c833409 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -1,109 +1,9 @@ -from drf_yasg.utils import swagger_auto_schema -from django.db.transaction import atomic -from rest_framework import generics -from rest_framework import permissions -from rest_framework import status -from rest_framework.response import Response - -from crews import dto -from crews import models -from crews import serializers -from crews import services -from crews.views.applicantions import CrewApplicantionCreateAPIView -from crews.views.applicantions import CrewApplicantionAcceptAPIView -from crews.views.applicantions import CrewApplicantionRejectAPIView - - -class CrewCreateAPIView(generics.CreateAPIView): - """크루 생성 API""" - - queryset = models.Crew.objects.all() - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewCreateSerializer - - def perform_create(self, serializer: serializers.CrewCreateSerializer): - languages = [*serializer.validated_data.pop('languages')] - with atomic(): - crew = serializer.save(**{ - models.Crew.field_name.CREATED_BY: self.request.user, - }) - services.crew.set_submittable_languages(crew, languages) - return crew - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = self.perform_create(serializer) - serializer = serializers.MyCrewSerializer(instance=instance) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - -class RecruitingCrewListAPIView(generics.ListAPIView): - """크루 목록""" - - permission_classes = [permissions.AllowAny] - serializer_class = serializers.RecruitingCrewSerializer - - def get_queryset(self): - # 본인이 속한 크루는 제외. - queryset = services.crew.of_user( - exclude_user=self.request.user, - ) - return queryset.filter(**{ - models.Crew.field_name.IS_RECRUITING: True, - }) - - -class MyCrewAPIView(generics.ListAPIView): - """나의 참여 크루""" - - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.MyCrewSerializer - - def get_queryset(self): - queryset = services.crew.of_user( - include_user=self.request.user, - ) - # 활동 종료된 크루는 뒤로 가도록 정렬 - return queryset.order_by('-'+models.Crew.field_name.IS_ACTIVE) - - -class CrewDashboardAPIView(generics.RetrieveAPIView): - """크루 대시보드 홈 API""" - - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewDashboardSerializer - lookup_field = 'id' - - def get_queryset(self): - return services.crew.of_user( - include_user=self.request.user, - ) - - -class CrewStatisticsAPIView(generics.RetrieveAPIView): - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewStatisticsSerializer - lookup_field = 'id' - - def get_queryset(self): - return services.crew.of_user( - include_user=self.request.user, - ) - - def get_object(self) -> dto.ProblemStatistic: - return services.problem_statistics(crew=super().get_object()) - - -class CrewActivityAPIView(generics.RetrieveAPIView): - """크루 대시보드 홈 - 회차 API""" - - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewDashboardSerializer - lookup_field = 'id' - - def get_queryset(self): - return services.crew.of_user( - include_user=self.request.user, - ) +from crews.views.crew_views import CrewCreateAPIView +from crews.views.crew_views import MyCrewListAPIView +from crews.views.crew_views import RecruitingCrewListAPIView +from crews.views.crew_views import CrewStatisticsAPIView +from crews.views.crew_views import CrewDashboardAPIView +from crews.views.crew_applicantions_views import CrewApplicantionCreateAPIView +from crews.views.crew_applicantions_views import CrewApplicantionAcceptAPIView +from crews.views.crew_applicantions_views import CrewApplicantionRejectAPIView +from crews.views.crew_activity_views import CrewActivityRetrieveAPIView diff --git a/app/crews/views/applicantions.py b/app/crews/views/applicantions.py deleted file mode 100644 index e2a9777..0000000 --- a/app/crews/views/applicantions.py +++ /dev/null @@ -1,46 +0,0 @@ -from rest_framework import generics -from rest_framework import permissions -from rest_framework import status -from rest_framework.response import Response - -from crews import models -from crews import serializers -from crews import services - - -class CrewApplicantionCreateAPIView(generics.CreateAPIView): - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewApplicationSerializer - lookup_field = 'id' - - def perform_create(self, serializer: serializers.CrewApplicationSerializer): - instance = serializer.save(**{ - models.CrewApplicant.field_name.CREW: self.get_object(), - models.CrewApplicant.field_name.USER: self.request.user, - }) - services.crew_applicant.notify_captain(instance) - - -class CrewApplicantionAcceptAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant.objects - permission_classes = [permissions.IsAuthenticated] - lookup_field = 'id' - - def get(self, request, *args, **kwargs): - instance: models.CrewApplicant = self.get_object() - services.crew_applicant.accept(instance, self.request.user) - services.crew_applicant.notify_accepted(instance) - return Response(status=status.HTTP_200_OK) - - -class CrewApplicantionRejectAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant.objects - permission_classes = [permissions.IsAuthenticated] - lookup_field = 'id' - - def get(self, request, *args, **kwargs): - instance: models.CrewApplicant = self.get_object() - services.crew_applicant.reject(instance, self.request.user) - services.crew_applicant.notify_rejected(instance) - return Response(status=status.HTTP_200_OK) diff --git a/app/crews/views/crew_activity_views.py b/app/crews/views/crew_activity_views.py new file mode 100644 index 0000000..70e669b --- /dev/null +++ b/app/crews/views/crew_activity_views.py @@ -0,0 +1,15 @@ +from rest_framework import generics +from rest_framework.permissions import IsAuthenticated + +from crews import models +from crews import permissions +from crews import serializers + + +class CrewActivityRetrieveAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 - 회차 API""" + queryset = models.CrewActivity + permission_classes = [IsAuthenticated & permissions.IsMember] + serializer_class = serializers.CrewActivityDashboardSerializer + lookup_field = 'id' + lookup_url_kwarg = 'activity_id' diff --git a/app/crews/views/crew_applicantions_views.py b/app/crews/views/crew_applicantions_views.py new file mode 100644 index 0000000..4385ced --- /dev/null +++ b/app/crews/views/crew_applicantions_views.py @@ -0,0 +1,65 @@ +from rest_framework import generics +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.request import Request +from rest_framework.response import Response + +from crews import models +from crews import permissions +from crews import serializers +from crews import services + + +class CrewApplicantionCreateAPIView(generics.CreateAPIView): + queryset = models.Crew + permission_classes = [IsAuthenticated & permissions.IsJoinable] + serializer_class = serializers.CrewApplicationCreateSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def create(self, request: Request, *args, **kwargs): + # input serializer + serializer = self.get_serializer(data=request.data) + instance = services.CrewApplicantionService.create( + crew=self.get_object(), + user=request.user, + message=serializer.validated_data['message'], + ) + # output serializer + serializer = serializers.CrewApplicationSerializer(instance) + headers = self.get_success_headers(serializer.data) + return Response( + data=serializer.data, + status=status.HTTP_201_CREATED, + headers=headers, + ) + + +class CrewApplicantionAcceptAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant + permission_classes = [IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'applicantion_id' + + def put(self, request: Request, *args, **kwargs): + instance = self.get_object() + service = services.CrewApplicantionService(instance) + service.accept(reviewed_by=request.user) + serializer = serializers.CrewApplicationSerializer(instance) + return Response(serializer.data) + + +class CrewApplicantionRejectAPIView(generics.GenericAPIView): + queryset = models.CrewApplicant + permission_classes = [IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'applicantion_id' + + def put(self, request: Request, *args, **kwargs): + instance = self.get_object() + service = services.CrewApplicantionService(instance) + service.reject(reviewed_by=request.user) + serializer = serializers.CrewApplicationSerializer(instance) + return Response(serializer.data) diff --git a/app/crews/views/crew_views.py b/app/crews/views/crew_views.py new file mode 100644 index 0000000..da37c8a --- /dev/null +++ b/app/crews/views/crew_views.py @@ -0,0 +1,74 @@ +from rest_framework import generics +from rest_framework import status +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + +from crews import dto +from crews import models +from crews import permissions +from crews import serializers +from crews import services + + +class CrewCreateAPIView(generics.CreateAPIView): + """크루 생성 API""" + queryset = models.Crew.objects.all() + permission_classes = [IsAuthenticated] + serializer_class = serializers.CrewCreateSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + serializer = serializers.MyCrewSerializer(instance=instance) + return Response( + data=serializer.data, + status=status.HTTP_201_CREATED, + headers=self.get_success_headers(serializer.data), + ) + + +class RecruitingCrewListAPIView(generics.ListAPIView): + """크루 목록""" + permission_classes = [AllowAny] + serializer_class = serializers.RecruitingCrewSerializer + + def get_queryset(self): + return services.CrewService.query_recruiting(self.request.user) + + +class MyCrewListAPIView(generics.ListAPIView): + """나의 참여 크루""" + permission_classes = [IsAuthenticated & permissions.IsMember] + serializer_class = serializers.MyCrewSerializer + + def get_queryset(self): + queryset = services.CrewService.query_as_member(self.request.user) + # 활동 종료된 크루는 뒤로 가도록 정렬 + return queryset.order_by('-'+models.Crew.field_name.IS_ACTIVE) + + +class CrewDashboardAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 API""" + queryset = models.Crew + permission_classes = [IsAuthenticated & permissions.IsMember] + serializer_class = serializers.CrewDashboardSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def get_queryset(self): + return services.CrewService.query_as_member(self.request.user) + + +class CrewStatisticsAPIView(generics.RetrieveAPIView): + queryset = models.Crew + permission_classes = [IsAuthenticated & permissions.IsMember] + serializer_class = serializers.CrewStatisticsSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def get_object(self) -> dto.ProblemStatistic: + crew = super().get_object() + service = services.CrewService(crew) + return service.statistics() From 1b7fbace245d7002b86fed17954392d17a3400a7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 21 Aug 2024 23:11:04 +0900 Subject: [PATCH 380/552] =?UTF-8?q?refactor(problems):=20problem=EC=9D=98?= =?UTF-8?q?=20api=20url=EB=8F=84=20crew=EC=99=80=20=EB=8F=99=EC=9D=BC?= =?UTF-8?q?=ED=95=98=EA=B2=8C=20=EB=B3=B5=EC=88=98/=EB=8B=A8=EC=88=98?= =?UTF-8?q?=ED=98=95=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index baba207..cba260b 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -42,9 +42,9 @@ path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), - path("problems/", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), - path("problems//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), + path("problem/", problems.views.ProblemCreateAPIView.as_view()), + path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), From c38d99aef3ae8e009165a540deb1067526c75210 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 02:58:55 +0900 Subject: [PATCH 381/552] refactor(problems): remove trailing slash for all url endpoints --- app/config/urls.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index cba260b..a6532cb 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -35,15 +35,15 @@ path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), path("crews/my", crews.views.MyCrewListAPIView.as_view()), path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), - path("crew/", crews.views.CrewCreateAPIView.as_view()), + path("crew", crews.views.CrewCreateAPIView.as_view()), path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), + path("problem", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), - path("problem/", problems.views.ProblemCreateAPIView.as_view()), path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), From d5f949260ab1a1e92eae07e2561f9aaccc70111a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 03:07:31 +0900 Subject: [PATCH 382/552] =?UTF-8?q?chore(config):=20swagger=20login=20?= =?UTF-8?q?=EB=B2=84=ED=8A=BC=EC=9D=98=20url=20=EB=A7=A4=ED=95=91=EC=9D=84?= =?UTF-8?q?=20/api/v1/auth/signin,=20signout=EC=9C=BC=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/config/settings.py b/app/config/settings.py index 544c72e..b926bfe 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -181,7 +181,11 @@ # Swagger Settings (DRf-YASG) -LOGIN_URL = "/api/v1/auth/signin" +SWAGGER_SETTINGS = { + "LOGIN_URL": "/api/v1/auth/signin", + "LOGOUT_URL": "/api/v1/auth/signout", +} + LOGGING = { "version": 1, From 36a17adb838b7336c590bb9b484cbf610d5ce678 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 03:18:59 +0900 Subject: [PATCH 383/552] refactor(crews): change model name CrewApplicant -> CrewApplication --- app/config/urls.py | 4 ++-- app/crews/admin.py | 18 +++++++------- app/crews/models/__init__.py | 2 +- ...{crew_applicant.py => crew_application.py} | 20 ++++++++-------- app/crews/permissions.py | 4 ++-- app/crews/serializers/__init__.py | 18 +++++++------- .../services/crew_application_service.py | 18 +++++++------- app/crews/views/crew_applicantions_views.py | 8 +++---- app/notifications/services.py | 24 +++++++++---------- 9 files changed, 58 insertions(+), 58 deletions(-) rename app/crews/models/{crew_applicant.py => crew_application.py} (78%) diff --git a/app/config/urls.py b/app/config/urls.py index a6532cb..c595df1 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -39,8 +39,8 @@ path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), - path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), - path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), + path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), + path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), path("problem", problems.views.ProblemCreateAPIView.as_view()), path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), diff --git a/app/crews/admin.py b/app/crews/admin.py index b6dd3cd..c904526 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -51,8 +51,8 @@ def get_members(self, crew: models.Crew): @admin.display(description='Applicants') def get_applicants(self, obj: models.Crew): - return models.CrewApplicant.objects.filter(**{ - models.CrewApplicant.field_name.CREW: obj, + return models.CrewApplication.objects.filter(**{ + models.CrewApplication.field_name.CREW: obj, }).count() @admin.display(description='Activities') @@ -134,13 +134,13 @@ class CrewSubmittableLanguageModelAdmin(admin.ModelAdmin): ] -@admin.register(models.CrewApplicant) +@admin.register(models.CrewApplication) class CrewApplicantModelAdmin(admin.ModelAdmin): list_display = [ - models.CrewApplicant.field_name.CREW, - models.CrewApplicant.field_name.USER, - models.CrewApplicant.field_name.IS_ACCEPTED, - models.CrewApplicant.field_name.REVIEWED_BY, + models.CrewApplication.field_name.CREW, + models.CrewApplication.field_name.APPLICANT, + models.CrewApplication.field_name.IS_ACCEPTED, + models.CrewApplication.field_name.REVIEWED_BY, ] actions = [ 'accept', @@ -148,13 +148,13 @@ class CrewApplicantModelAdmin(admin.ModelAdmin): ] @admin.action(description="Accept user") - def accept(self, request: HttpRequest, queryset: QuerySet[models.CrewApplicant]): + def accept(self, request: HttpRequest, queryset: QuerySet[models.CrewApplication]): for applicant in queryset: services.crew_applicant.accept(applicant, request.user) services.crew_applicant.notify_accepted(applicant) @admin.action(description="Reject user") - def reject(self, request: HttpRequest, queryset: QuerySet[models.CrewApplicant]): + def reject(self, request: HttpRequest, queryset: QuerySet[models.CrewApplication]): for applicant in queryset: services.crew_applicant.reject(applicant, request.user) services.crew_applicant.notify_rejected(applicant) diff --git a/app/crews/models/__init__.py b/app/crews/models/__init__.py index 3a24052..92a9a49 100644 --- a/app/crews/models/__init__.py +++ b/app/crews/models/__init__.py @@ -1,6 +1,6 @@ from crews.models.crew import Crew from crews.models.crew_activity import CrewActivity from crews.models.crew_activity_problem import CrewActivityProblem -from crews.models.crew_applicant import CrewApplicant +from crews.models.crew_application import CrewApplication from crews.models.crew_member import CrewMember from crews.models.crew_submittable_language import CrewSubmittableLanguage diff --git a/app/crews/models/crew_applicant.py b/app/crews/models/crew_application.py similarity index 78% rename from app/crews/models/crew_applicant.py rename to app/crews/models/crew_application.py index db8ac72..a4bba18 100644 --- a/app/crews/models/crew_applicant.py +++ b/app/crews/models/crew_application.py @@ -5,16 +5,16 @@ from crews.models.crew_member import CrewMember -class CrewApplicant(models.Model): +class CrewApplication(models.Model): crew = models.ForeignKey[Crew]( Crew, on_delete=models.CASCADE, help_text='크루를 입력해주세요.', ) - user = models.ForeignKey[User]( + applicant = models.ForeignKey[User]( User, on_delete=models.CASCADE, - help_text='유저를 입력해주세요.', + help_text='지원자를 입력해주세요.', ) message = models.TextField( help_text='가입 메시지를 입력해주세요.', @@ -43,7 +43,7 @@ class CrewApplicant(models.Model): class field_name: CREW = 'crew' - USER = 'user' + APPLICANT = 'applicant' MESSAGE = 'message' IS_ACCEPTED = 'is_accepted' CREATED_AT = 'created_at' @@ -54,7 +54,7 @@ class Meta: ordering = ['reviewed_by', 'created_at'] def __repr__(self) -> str: - return f'{self.crew.__repr__()} ← {self.user.__repr__()} : "{self.message}"' + return f'{self.crew.__repr__()} ← {self.applicant.__repr__()} : "{self.message}"' def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' @@ -64,13 +64,13 @@ def save(self, *args, **kwargs) -> None: # 같은 크루에 여러 번 가입하는 것을 방지 assert not CrewMember.objects.filter(**{ CrewMember.field_name.CREW: self.crew, - CrewMember.field_name.USER: self.user, + CrewMember.field_name.USER: self.applicant, }).exclude(pk=self.pk).exists(), '이미 가입한 크루에 가입 신청을 할 수 없습니다.' # 아직 검토되지 않은 신청이 있으면 가입 불가 - assert not CrewApplicant.objects.filter(**{ - CrewApplicant.field_name.CREW: self.crew, - CrewApplicant.field_name.USER: self.user, - CrewApplicant.field_name.REVIEWED_BY: None, + assert not CrewApplication.objects.filter(**{ + CrewApplication.field_name.CREW: self.crew, + CrewApplication.field_name.APPLICANT: self.applicant, + CrewApplication.field_name.REVIEWED_BY: None, }).exclude(pk=self.pk).exists(), '크루에 아직 검토되지 않은 지원 이력이 있습니다.' except AssertionError as e: raise ValueError from e diff --git a/app/crews/permissions.py b/app/crews/permissions.py index bac5291..403d9e0 100644 --- a/app/crews/permissions.py +++ b/app/crews/permissions.py @@ -35,7 +35,7 @@ def has_object_permission(self, request: Request, view, obj: Union[models.Crew, class IsCaptain(permissions.BasePermission): - def has_object_permission(self, request: Request, view, application: models.CrewApplicant) -> bool: - assert isinstance(application, models.CrewApplicant) + def has_object_permission(self, request: Request, view, application: models.CrewApplication) -> bool: + assert isinstance(application, models.CrewApplication) service = services.CrewService(application.crew) return service.is_captain(request.user) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index 97e9998..073e412 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -129,19 +129,19 @@ class CrewApplicationSerializer(serializers.ModelSerializer): user = UserMinimalSerializer(read_only=True) class Meta: - model = models.CrewApplicant + model = models.CrewApplication fields = [ PK, - models.CrewApplicant.field_name.CREW, - models.CrewApplicant.field_name.MESSAGE, - models.CrewApplicant.field_name.USER, - models.CrewApplicant.field_name.IS_ACCEPTED, - models.CrewApplicant.field_name.CREATED_AT, + models.CrewApplication.field_name.CREW, + models.CrewApplication.field_name.MESSAGE, + models.CrewApplication.field_name.APPLICANT, + models.CrewApplication.field_name.IS_ACCEPTED, + models.CrewApplication.field_name.CREATED_AT, ] read_only_fields = [ - models.CrewApplicant.field_name.CREW, - models.CrewApplicant.field_name.IS_ACCEPTED, - models.CrewApplicant.field_name.CREATED_AT, + models.CrewApplication.field_name.CREW, + models.CrewApplication.field_name.IS_ACCEPTED, + models.CrewApplication.field_name.CREATED_AT, ] diff --git a/app/crews/services/crew_application_service.py b/app/crews/services/crew_application_service.py index 1179ec2..f021b94 100644 --- a/app/crews/services/crew_application_service.py +++ b/app/crews/services/crew_application_service.py @@ -8,17 +8,17 @@ class CrewApplicantionService: @staticmethod - def create(crew: models.Crew, user: users.models.User, message: str) -> models.CrewApplicant: - instance = models.CrewApplicant(**{ - models.CrewApplicant.field_name.CREW: crew, - models.CrewApplicant.field_name.USER: user, - models.CrewApplicant.field_name.MESSAGE: message, + def create(crew: models.Crew, user: users.models.User, message: str) -> models.CrewApplication: + instance = models.CrewApplication(**{ + models.CrewApplication.field_name.CREW: crew, + models.CrewApplication.field_name.APPLICANT: user, + models.CrewApplication.field_name.MESSAGE: message, }) instance.save() notifications.services.notify_crew_application_requested(instance) - def __init__(self, instance: models.CrewApplicant): - assert isinstance(instance, models.CrewApplicant) + def __init__(self, instance: models.CrewApplication): + assert isinstance(instance, models.CrewApplication) self.instance = instance def reject(self, reviewed_by: users.models.User): @@ -35,7 +35,7 @@ def accept(self, reviewed_by: users.models.User): with atomic(): self.instance.save() models.CrewMember.objects.create(**{ - models.CrewApplicant.field_name.CREW: self.instance.crew, - models.CrewApplicant.field_name.USER: self.instance.user, + models.CrewApplication.field_name.CREW: self.instance.crew, + models.CrewApplication.field_name.APPLICANT: self.instance.applicant, }) notifications.services.notify_crew_application_accepted(self.instance) diff --git a/app/crews/views/crew_applicantions_views.py b/app/crews/views/crew_applicantions_views.py index 4385ced..e38779f 100644 --- a/app/crews/views/crew_applicantions_views.py +++ b/app/crews/views/crew_applicantions_views.py @@ -36,11 +36,11 @@ def create(self, request: Request, *args, **kwargs): class CrewApplicantionAcceptAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant + queryset = models.CrewApplication permission_classes = [IsAuthenticated & permissions.IsCaptain] serializer_class = serializers.NoInputSerializer lookup_field = 'id' - lookup_url_kwarg = 'applicantion_id' + lookup_url_kwarg = 'application_id' def put(self, request: Request, *args, **kwargs): instance = self.get_object() @@ -51,11 +51,11 @@ def put(self, request: Request, *args, **kwargs): class CrewApplicantionRejectAPIView(generics.GenericAPIView): - queryset = models.CrewApplicant + queryset = models.CrewApplication permission_classes = [IsAuthenticated & permissions.IsCaptain] serializer_class = serializers.NoInputSerializer lookup_field = 'id' - lookup_url_kwarg = 'applicantion_id' + lookup_url_kwarg = 'application_id' def put(self, request: Request, *args, **kwargs): instance = self.get_object() diff --git a/app/notifications/services.py b/app/notifications/services.py index 1bfdd5e..82690fb 100644 --- a/app/notifications/services.py +++ b/app/notifications/services.py @@ -12,15 +12,15 @@ LOGGER = logging.getLogger('django.mail') -def notify_crew_application_requested(applicant: crews.models.CrewApplicant): - assert isinstance(applicant, crews.models.CrewApplicant) +def notify_crew_application_requested(applicant: crews.models.CrewApplication): + assert isinstance(applicant, crews.models.CrewApplication) send_mail( subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청', message=dedent(f""" [{applicant.crew.icon} {applicant.crew.name}]에 새로운 가입 신청이 왔어요! - 지원자: {applicant.user.username} - 지원자의 백준 아이디(레벨): {applicant.user.boj_username} ({users.models.UserBojLevelChoices(applicant.user.boj_level).get_name(lang='ko', arabic=False)}) + 지원자: {applicant.applicant.username} + 지원자의 백준 아이디(레벨): {applicant.applicant.boj_username} ({users.models.UserBojLevelChoices(applicant.applicant.boj_level).get_name(lang='ko', arabic=False)}) 지원자의 메시지: ``` @@ -36,8 +36,8 @@ def notify_crew_application_requested(applicant: crews.models.CrewApplicant): LOGGER.info(f'MAIL crew.application.requested {applicant.crew.created_by.email}') -def notify_crew_application_accepted(applicant: crews.models.CrewApplicant): - assert isinstance(applicant, crews.models.CrewApplicant) +def notify_crew_application_accepted(applicant: crews.models.CrewApplication): + assert isinstance(applicant, crews.models.CrewApplication) send_mail( subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 승인되었습니다', message=dedent(f""" @@ -45,22 +45,22 @@ def notify_crew_application_accepted(applicant: crews.models.CrewApplicant): [여기]를 눌러 크루 대시보드로 바로가기 """), - recipient_list=[applicant.user.email], + recipient_list=[applicant.applicant.email], from_email=None, fail_silently=False, ) - LOGGER.info(f'MAIL crew.application.accepted {applicant.user.email}') + LOGGER.info(f'MAIL crew.application.accepted {applicant.applicant.email}') -def notify_crew_application_rejected(applicant: crews.models.CrewApplicant): - assert isinstance(applicant, crews.models.CrewApplicant) +def notify_crew_application_rejected(applicant: crews.models.CrewApplication): + assert isinstance(applicant, crews.models.CrewApplication) send_mail( subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 거절되었습니다', message=dedent(f""" [{applicant.crew.icon} {applicant.crew.name}]에 아쉽게도 가입하지 못했어요! """), - recipient_list=[applicant.user.email], + recipient_list=[applicant.applicant.email], from_email=None, fail_silently=False, ) - LOGGER.info(f'MAIL crew.application.rejected {applicant.user.email}') + LOGGER.info(f'MAIL crew.application.rejected {applicant.applicant.email}') From 680156a46388c17bea2aebd70f00edffb3ad6e84 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 05:08:27 +0900 Subject: [PATCH 384/552] =?UTF-8?q?fix(crews.services):=20captain()=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=EA=B0=80=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EC=9D=B4=20=EC=95=84=EB=8B=8C=20queryset=EC=9D=84=20=EB=B0=98?= =?UTF-8?q?=ED=99=98=ED=95=98=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/services/crew_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/crews/services/crew_service.py b/app/crews/services/crew_service.py index 2694a96..e8a2bce 100644 --- a/app/crews/services/crew_service.py +++ b/app/crews/services/crew_service.py @@ -99,7 +99,7 @@ def captain(self) -> models.CrewMember: return models.CrewMember.objects.filter(**{ models.CrewMember.field_name.CREW: self.instance, models.CrewMember.field_name.IS_CAPTAIN: True, - }) + }).get() def problems(self) -> List[Problem]: return models.CrewActivityProblem.objects.filter(**{ From 5e0f9e1b219e96588bb59fd63efaae27119be54a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 05:09:06 +0900 Subject: [PATCH 385/552] =?UTF-8?q?feat(crews.services):=20CrewService?= =?UTF-8?q?=EC=97=90=20problems,=20applications=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=BF=BC=EB=A6=AC=20=EC=B6=94=EA=B0=80,=20displayname()=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/services/crew_service.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/crews/services/crew_service.py b/app/crews/services/crew_service.py index e8a2bce..2e6329c 100644 --- a/app/crews/services/crew_service.py +++ b/app/crews/services/crew_service.py @@ -60,9 +60,19 @@ def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: models.CrewSubmittableLanguage.field_name.CREW: self.instance, }) + def query_applications(self) -> QuerySet[models.CrewApplication]: + return models.CrewApplication.objects.filter(**{ + models.CrewApplication.field_name.CREW: self.instance, + }) + def query_activities(self) -> QuerySet[models.CrewActivity]: return CrewActivityService.query_all(self.instance) + def query_problems(self) -> QuerySet[models.CrewActivityProblem]: + return models.CrewActivityProblem.objects.filter(**{ + models.CrewActivityProblem.field_name.CREW: self.instance, + }) + def statistics(self) -> dto.ProblemStatistic: stat = dto.ProblemStatistic() for problem in self.problems(): @@ -106,6 +116,9 @@ def problems(self) -> List[Problem]: models.CrewActivityProblem.field_name.CREW: self.instance, }).values_list(models.CrewActivityProblem.field_name.PROBLEM, flat=True) + def display_name(self) -> str: + return f'{self.instance.icon} {self.instance.name}' + def languages(self) -> List[enums.ProgrammingLanguageChoices]: languages = [] for submittable_language in self.query_languages(): From d02d1d5a9d5e05cf1acef07b4623b9f607d71084 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 22 Aug 2024 05:09:54 +0900 Subject: [PATCH 386/552] =?UTF-8?q?refactor(crews.models):=20CrewApplicati?= =?UTF-8?q?on=20=EC=97=90=20is=5Fpending=20=EC=86=8D=EC=84=B1=EC=9D=84=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=98=EC=97=AC=20=EA=B8=B0=EC=A1=B4=20?= =?UTF-8?q?=EC=88=98=EB=9D=BD/=EA=B1=B0=EC=A0=88=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EB=8B=A8?= =?UTF-8?q?=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models/crew_application.py | 30 ++++++++----------- .../services/crew_application_service.py | 14 +++++---- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/app/crews/models/crew_application.py b/app/crews/models/crew_application.py index a4bba18..c5f6bc5 100644 --- a/app/crews/models/crew_application.py +++ b/app/crews/models/crew_application.py @@ -21,6 +21,10 @@ class CrewApplication(models.Model): null=True, blank=True, ) + is_pending = models.BooleanField( + default=True, + help_text="아직 수락/거절 되지 않았다면 True.", + ) is_accepted = models.BooleanField( default=False, help_text='수락 여부를 입력해주세요.', @@ -45,12 +49,20 @@ class field_name: CREW = 'crew' APPLICANT = 'applicant' MESSAGE = 'message' + IS_PENDING = 'is_pending' IS_ACCEPTED = 'is_accepted' CREATED_AT = 'created_at' REVIEWED_AT = 'reviewed_at' REVIEWED_BY = 'reviewed_by' class Meta: + constraints = [ + models.UniqueConstraint( + fields=['crew', 'applicant'], + condition=models.Q(is_pending=True), + name='unique_pending_application', + ), + ] ordering = ['reviewed_by', 'created_at'] def __repr__(self) -> str: @@ -58,21 +70,3 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' - - def save(self, *args, **kwargs) -> None: - try: - # 같은 크루에 여러 번 가입하는 것을 방지 - assert not CrewMember.objects.filter(**{ - CrewMember.field_name.CREW: self.crew, - CrewMember.field_name.USER: self.applicant, - }).exclude(pk=self.pk).exists(), '이미 가입한 크루에 가입 신청을 할 수 없습니다.' - # 아직 검토되지 않은 신청이 있으면 가입 불가 - assert not CrewApplication.objects.filter(**{ - CrewApplication.field_name.CREW: self.crew, - CrewApplication.field_name.APPLICANT: self.applicant, - CrewApplication.field_name.REVIEWED_BY: None, - }).exclude(pk=self.pk).exists(), '크루에 아직 검토되지 않은 지원 이력이 있습니다.' - except AssertionError as e: - raise ValueError from e - else: - return super().save(*args, **kwargs) diff --git a/app/crews/services/crew_application_service.py b/app/crews/services/crew_application_service.py index f021b94..210621d 100644 --- a/app/crews/services/crew_application_service.py +++ b/app/crews/services/crew_application_service.py @@ -22,16 +22,12 @@ def __init__(self, instance: models.CrewApplication): self.instance = instance def reject(self, reviewed_by: users.models.User): - self.instance.is_accepted = False - self.instance.reviewed_by = reviewed_by - self.instance.reviewed_at = timezone.now() + self._review(reviewed_by, accept=False) self.instance.save() notifications.services.notify_crew_application_rejected(self.instance) def accept(self, reviewed_by: users.models.User): - self.instance.is_accepted = True - self.instance.reviewed_by = reviewed_by - self.instance.reviewed_at = timezone.now() + self._review(reviewed_by, accept=True) with atomic(): self.instance.save() models.CrewMember.objects.create(**{ @@ -39,3 +35,9 @@ def accept(self, reviewed_by: users.models.User): models.CrewApplication.field_name.APPLICANT: self.instance.applicant, }) notifications.services.notify_crew_application_accepted(self.instance) + + def _review(self, by: users.models.User, accept: bool): + self.instance.is_pending = False + self.instance.is_accepted = accept + self.instance.reviewed_by = by + self.instance.reviewed_at = timezone.now() From c125ca58737f967997c1b6c00a2ec41ae9b8bc54 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 23 Aug 2024 17:33:02 +0900 Subject: [PATCH 387/552] =?UTF-8?q?feat(boj):=20=EB=B0=B1=EA=B7=B8?= =?UTF-8?q?=EB=9D=BC=EC=9A=B4=EB=93=9C=EC=97=90=EC=84=9C=20=EB=B0=B1?= =?UTF-8?q?=EC=A4=80=20=EA=B3=84=EC=A0=95=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + app/boj/__init__.py | 0 app/boj/admin.py | 34 ++++++++++++++++++ app/boj/apps.py | 6 ++++ app/boj/dto.py | 9 +++++ app/boj/enums.py | 65 ++++++++++++++++++++++++++++++++++ app/boj/migrations/__init__.py | 0 app/boj/models.py | 46 ++++++++++++++++++++++++ app/boj/services.py | 54 ++++++++++++++++++++++++++++ app/config/settings.py | 2 ++ requirements.txt | 1 + 11 files changed, 218 insertions(+) create mode 100644 app/boj/__init__.py create mode 100644 app/boj/admin.py create mode 100644 app/boj/apps.py create mode 100644 app/boj/dto.py create mode 100644 app/boj/enums.py create mode 100644 app/boj/migrations/__init__.py create mode 100644 app/boj/models.py create mode 100644 app/boj/services.py diff --git a/.gitignore b/.gitignore index 138a61e..989d7c5 100644 --- a/.gitignore +++ b/.gitignore @@ -56,6 +56,7 @@ cover/ *.pot # Django stuff: +*.log.* *.log local_settings.py db.sqlite3 diff --git a/app/boj/__init__.py b/app/boj/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/boj/admin.py b/app/boj/admin.py new file mode 100644 index 0000000..deccee8 --- /dev/null +++ b/app/boj/admin.py @@ -0,0 +1,34 @@ +from django.contrib import admin +from django.db.models import QuerySet +from django.http.request import HttpRequest + +from boj import models +from boj import services + + +@admin.register(models.BOJUser) +class BOJUserModelAdmin(admin.ModelAdmin): + list_display = [ + models.BOJUser.field_name.USERNAME, + models.BOJUser.field_name.LEVEL, + models.BOJUser.field_name.RATING, + models.BOJUser.field_name.UPDATED_AT, + ] + actions = [ + 'fetch', + ] + + @admin.action(description="Fetch data from solved.ac API of selected BOJ users.") + def fetch(self, request: HttpRequest, queryset: QuerySet[models.BOJUser]): + for obj in queryset: + services.fetch(obj.username) + + +@admin.register(models.BOJUserSnapshot) +class BOJUserSnapshotModelAdmin(admin.ModelAdmin): + list_display = [ + models.BOJUserSnapshot.field_name.USER, + models.BOJUserSnapshot.field_name.LEVEL, + models.BOJUserSnapshot.field_name.RATING, + models.BOJUserSnapshot.field_name.CREATED_AT, + ] diff --git a/app/boj/apps.py b/app/boj/apps.py new file mode 100644 index 0000000..025268a --- /dev/null +++ b/app/boj/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BojConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "boj" diff --git a/app/boj/dto.py b/app/boj/dto.py new file mode 100644 index 0000000..784559e --- /dev/null +++ b/app/boj/dto.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + +from boj import enums + + +@dataclass(frozen=True) +class BOJUserData: + level: enums.BOJLevel + rating: int diff --git a/app/boj/enums.py b/app/boj/enums.py new file mode 100644 index 0000000..3ba08c3 --- /dev/null +++ b/app/boj/enums.py @@ -0,0 +1,65 @@ +from django.db.models import IntegerChoices + + +DIVISION_NAMES = { + 'ko': ['난이도를 매길 수 없음', '브론즈', '실버', '골드', '플래티넘', '다이아몬드', '루비'], + 'en': ['Unrated', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Ruby'], +} +ARABIC_NUMERALS = ['', 'I', 'II', 'III', 'IV', 'V'] + + +class BOJLevel(IntegerChoices): + U = 0, 'Unrated' + B5 = 1, '브론즈 5' + B4 = 2, '브론즈 4' + B3 = 3, '브론즈 3' + B2 = 4, '브론즈 2' + B1 = 5, '브론즈 1' + S5 = 6, '실버 5' + S4 = 7, '실버 4' + S3 = 8, '실버 3' + S2 = 9, '실버 2' + S1 = 10, '실버 1' + G5 = 11, '골드 5' + G4 = 12, '골드 4' + G3 = 13, '골드 3' + G2 = 14, '골드 2' + G1 = 15, '골드 1' + P5 = 16, '플래티넘 5' + P4 = 17, '플래티넘 4' + P3 = 18, '플래티넘 3' + P2 = 19, '플래티넘 2' + P1 = 20, '플래티넘 1' + D5 = 21, '다이아몬드 5' + D4 = 22, '다이아몬드 4' + D3 = 23, '다이아몬드 3' + D2 = 24, '다이아몬드 2' + D1 = 25, '다이아몬드 1' + R5 = 26, '루비 5' + R4 = 27, '루비 4' + R3 = 28, '루비 3' + R2 = 29, '루비 2' + R1 = 30, '루비 1' + M = 31, '마스터' + + def get_division(self) -> int: + if self == self.U: + return 0 + return ((self.value-1) // 5)+1 + + def get_division_name(self, lang='en') -> str: + return DIVISION_NAMES[lang][self.get_division()] + + def get_tier(self) -> int: + if self == self.U: + return 0 + return 5 - ((self.value-1) % 5) + + def get_tier_name(self, arabic=True) -> str: + tier = self.get_tier() + if arabic: + return ARABIC_NUMERALS[tier] + return str(tier) + + def get_name(self, lang='en', arabic=True) -> str: + return f'{self.get_division_name(lang=lang)} {self.get_tier_name(arabic=arabic)}' diff --git a/app/boj/migrations/__init__.py b/app/boj/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/boj/models.py b/app/boj/models.py new file mode 100644 index 0000000..6e28140 --- /dev/null +++ b/app/boj/models.py @@ -0,0 +1,46 @@ +from __future__ import annotations + +from django.db import models + +from boj import enums + + +class BOJUser(models.Model): + username = models.TextField( + help_text='백준 아이디', + max_length=40, + unique=True, + ) + level = models.IntegerField( + choices=enums.BOJLevel.choices, + default=enums.BOJLevel.U, + ) + rating = models.IntegerField( + default=0, + ) + updated_at = models.DateTimeField(auto_now_add=True) + + class field_name: + USERNAME = 'username' + LEVEL = 'level' + RATING = 'rating' + UPDATED_AT = 'updated_at' + + def __str__(self) -> str: + return f'{self.username}' + + +class BOJUserSnapshot(models.Model): + user = models.ForeignKey( + BOJUser, + on_delete=models.CASCADE, + ) + level = models.IntegerField(choices=enums.BOJLevel.choices) + rating = models.IntegerField() + created_at = models.DateTimeField() + + class field_name: + USER = 'user' + LEVEL = 'level' + RATING = 'rating' + CREATED_AT = 'created_at' diff --git a/app/boj/services.py b/app/boj/services.py new file mode 100644 index 0000000..cc58b8a --- /dev/null +++ b/app/boj/services.py @@ -0,0 +1,54 @@ +import logging + +from django.utils import timezone +import background_task +import requests + +from boj import dto +from boj import enums +from boj import models + + +logger = logging.getLogger('tle.boj') + + +def get_object(username: str) -> models.BOJUser: + obj, created = models.BOJUser.objects.get_or_create(**{ + models.BOJUser.field_name.USERNAME: username, + }) + return obj + + +def snapshot(obj: models.BOJUser) -> models.BOJUserSnapshot: + return models.BOJUserSnapshot(**{ + models.BOJUserSnapshot.field_name.USER: obj, + models.BOJUserSnapshot.field_name.LEVEL: obj.level, + models.BOJUserSnapshot.field_name.RATING: obj.rating, + models.BOJUserSnapshot.field_name.CREATED_AT: obj.updated_at, + }) + + +@background_task.background() +def fetch(username: str) -> None: + data = fetch_data(username) + obj = get_object(username) + obj.level = data.level.value + obj.rating = data.rating + obj.updated_at = timezone.now() + obj.save() + snapshot(obj).save() + + +def fetch_data(username: str) -> dto.BOJUserData: + url = f'https://solved.ac/api/v3/user/show?handle={username}' + data = requests.get(url).json() + try: + tier = data['tier'] + rating = data['rating'] + except AssertionError: + # Solved.ac API 관련 문제일 가능성이 높다. + logger.warning( + '"https://solved.ac/api/v3/user/show"로 부터 데이터를 파싱해오는 것에 실패했습니다.' + ) + else: + return dto.BOJUserData(level=enums.BOJLevel(tier), rating=rating) diff --git a/app/config/settings.py b/app/config/settings.py index b926bfe..ecf6c05 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -50,9 +50,11 @@ "corsheaders", "drf_yasg", + 'background_task', "rest_framework", 'rest_framework_simplejwt', + "boj", "users", "problems", "crews", diff --git a/requirements.txt b/requirements.txt index 5e4e5d5..7c47084 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ django +django-background-tasks django-cors-headers djangorestframework djangorestframework-simplejwt From e0ea2e973adb6f6aa98df1a86ea18cfb7bf8e043 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 08:21:33 +0900 Subject: [PATCH 388/552] =?UTF-8?q?feat(problems):=20LLM(Gemini)=EC=9D=84?= =?UTF-8?q?=20=EC=9D=B4=EC=9A=A9=ED=95=9C=20=EB=AC=B8=EC=A0=9C=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80=20=EB=B0=8F?= =?UTF-8?q?=20=EC=9D=B4=EB=A5=BC=20=EC=9C=84=ED=95=9C=20=EB=A6=AC=ED=8C=A9?= =?UTF-8?q?=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/dto/__init__.py | 4 +- app/problems/admin.py | 120 --------------- app/problems/admin/__init__.py | 5 + app/problems/admin/problem.py | 30 ++++ app/problems/admin/problem_analysis.py | 42 +++++ app/problems/admin/problem_analysis_tag.py | 21 +++ app/problems/admin/problem_tag.py | 18 +++ app/problems/admin/problem_tag_relation.py | 16 ++ app/problems/constants.py | 21 --- app/problems/{dto/__init__.py => dto.py} | 8 +- app/problems/{models/choices.py => enums.py} | 18 ++- app/problems/models/__init__.py | 11 -- app/problems/models/problem.py | 23 ++- app/problems/models/problem_analysis.py | 6 +- app/problems/serializers/__init__.py | 8 +- app/problems/serializers/fields.py | 18 +-- app/problems/services/__init__.py | 121 +-------------- app/problems/services/analysers/__init__.py | 6 + .../analyser.py => analysers/base.py} | 2 +- .../services/analysers/gemini/__init__.py | 1 + .../services/analysers/gemini/analyser.py | 66 ++++++++ .../services/analysers/gemini/parsers.py | 62 ++++++++ .../services/analysers/gemini/prompts.py | 112 ++++++++++++++ .../services/analysers/gpt/__init__.py | 0 .../services/analysers/gpt/analyser.py | 15 ++ app/problems/services/analysis/__init__.py | 24 --- app/problems/services/analysis/llm/gemini.py | 10 -- app/problems/services/analysis/llm/gpt.py | 10 -- app/problems/services/base.py | 50 ++++++ app/problems/services/concrete.py | 144 ++++++++++++++++++ requirements.txt | 4 + 31 files changed, 651 insertions(+), 345 deletions(-) delete mode 100644 app/problems/admin.py create mode 100644 app/problems/admin/__init__.py create mode 100644 app/problems/admin/problem.py create mode 100644 app/problems/admin/problem_analysis.py create mode 100644 app/problems/admin/problem_analysis_tag.py create mode 100644 app/problems/admin/problem_tag.py create mode 100644 app/problems/admin/problem_tag_relation.py delete mode 100644 app/problems/constants.py rename app/problems/{dto/__init__.py => dto.py} (70%) rename app/problems/{models/choices.py => enums.py} (54%) create mode 100644 app/problems/services/analysers/__init__.py rename app/problems/services/{analysis/analyser.py => analysers/base.py} (93%) create mode 100644 app/problems/services/analysers/gemini/__init__.py create mode 100644 app/problems/services/analysers/gemini/analyser.py create mode 100644 app/problems/services/analysers/gemini/parsers.py create mode 100644 app/problems/services/analysers/gemini/prompts.py create mode 100644 app/problems/services/analysers/gpt/__init__.py create mode 100644 app/problems/services/analysers/gpt/analyser.py delete mode 100644 app/problems/services/analysis/__init__.py delete mode 100644 app/problems/services/analysis/llm/gemini.py delete mode 100644 app/problems/services/analysis/llm/gpt.py create mode 100644 app/problems/services/base.py create mode 100644 app/problems/services/concrete.py diff --git a/app/crews/dto/__init__.py b/app/crews/dto/__init__.py index e09e128..a646f49 100644 --- a/app/crews/dto/__init__.py +++ b/app/crews/dto/__init__.py @@ -5,7 +5,7 @@ from typing import List from crews import enums -from problems.models import ProblemDifficultyChoices +from problems.enums import ProblemDifficulty @dataclass @@ -37,7 +37,7 @@ class CrewProblem: problem_number: int problem_id: int problem_title: str - problem_difficulty: ProblemDifficultyChoices + problem_difficulty: ProblemDifficulty is_submitted: bool last_submitted_date: datetime diff --git a/app/problems/admin.py b/app/problems/admin.py deleted file mode 100644 index 1fe7607..0000000 --- a/app/problems/admin.py +++ /dev/null @@ -1,120 +0,0 @@ -from textwrap import shorten - -from django.contrib import admin, messages -from django.db.models import QuerySet -from django.utils.translation import ngettext - -from problems import models -from users.models import User - - -@admin.register(models.Problem) -class ProblemModelAdmin(admin.ModelAdmin): - list_display = [ - models.Problem.field_name.TITLE, - models.Problem.field_name.CREATED_BY, - models.Problem.field_name.CREATED_AT, - models.Problem.field_name.UPDATED_AT, - ] - search_fields = [ - models.Problem.field_name.TITLE, - models.Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - ] - ordering = ['-'+models.Problem.field_name.CREATED_AT] - actions = [ - 'analyze', - 'add_to_analysis_queue', - 'set_creator', - ] - - @admin.action(description="Set admin(you) as creator for selected problems") - def set_creator(self, request, queryset: QuerySet[models.Problem]): - updated = queryset.update(**{ - models.Problem.field_name.CREATED_BY: request.user, - }) - self.message_user( - request, - ngettext( - "%d problem was successfully updated.", - "%d problems were successfully updated.", - updated, - ) - % updated, - messages.SUCCESS, - ) - - -@admin.register(models.ProblemAnalysis) -class ProblemAnalysisModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemAnalysis.field_name.PROBLEM, - models.ProblemAnalysis.field_name.DIFFICULTY, - 'get_timecomplexity', - 'get_tags', - 'get_hint', - models.ProblemAnalysis.field_name.CREATED_AT, - ] - search_fields = [ - models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, - models.ProblemAnalysis.field_name.TIME_COMPLEXITY, - ] - ordering = ['-'+models.ProblemAnalysis.field_name.CREATED_AT] - - @admin.display(description='Big-O') - def get_timecomplexity(self, obj: models.ProblemAnalysis) -> str: - return f'O({obj.time_complexity})' - - @admin.display(description='Tags') - def get_tags(self, analysis: models.ProblemAnalysis) -> str: - tags = models.ProblemAnalysisTag.objects.filter(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, - }).values_list(models.ProblemAnalysisTag.field_name.TAG, flat=True) - return ' '.join(tag.key for tag in tags) - - @admin.display(description='Hint (Steps, Verbose)') - def get_hint(self, obj: models.ProblemAnalysis) -> str: - hints_in_a_row = ', '.join(obj.hint) - return len(obj.hint), shorten(hints_in_a_row, width=32) - - -@admin.register(models.ProblemAnalysisTag) -class ProblemAnalysisTagModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemAnalysisTag.field_name.ANALYSIS, - models.ProblemAnalysisTag.field_name.TAG, - ] - search_fields = [ - models.ProblemAnalysisTag.field_name.ANALYSIS+'__'+models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.NAME_KO, - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.NAME_EN, - ] - ordering = [models.ProblemAnalysisTag.field_name.ANALYSIS] - - -@admin.register(models.ProblemTag) -class ProblemTagModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemTag.field_name.KEY, - models.ProblemTag.field_name.NAME_KO, - models.ProblemTag.field_name.NAME_EN, - ] - search_fields = [ - models.ProblemTag.field_name.KEY, - models.ProblemTag.field_name.NAME_KO, - models.ProblemTag.field_name.NAME_EN, - ] - ordering = [models.ProblemTag.field_name.KEY] - - -@admin.register(models.ProblemTagRelation) -class ProblemTagRelationModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemTagRelation.field_name.PARENT, - models.ProblemTagRelation.field_name.CHILD, - ] - search_fields = [ - models.ProblemTagRelation.field_name.PARENT, - models.ProblemTagRelation.field_name.CHILD, - ] - ordering = [models.ProblemTagRelation.field_name.PARENT] diff --git a/app/problems/admin/__init__.py b/app/problems/admin/__init__.py new file mode 100644 index 0000000..6502a2c --- /dev/null +++ b/app/problems/admin/__init__.py @@ -0,0 +1,5 @@ +from problems.admin.problem import ProblemModelAdmin +from problems.admin.problem_analysis import ProblemAnalysisModelAdmin +from problems.admin.problem_analysis_tag import ProblemAnalysisTagModelAdmin +from problems.admin.problem_tag import ProblemTagModelAdmin +from problems.admin.problem_tag_relation import ProblemTagRelationModelAdmin diff --git a/app/problems/admin/problem.py b/app/problems/admin/problem.py new file mode 100644 index 0000000..7959da5 --- /dev/null +++ b/app/problems/admin/problem.py @@ -0,0 +1,30 @@ +from django.contrib import admin +from django.db.models import QuerySet + +from problems import models +from problems import services +from users.models import User + + +@admin.register(models.Problem) +class ProblemModelAdmin(admin.ModelAdmin): + list_display = [ + models.Problem.field_name.TITLE, + models.Problem.field_name.CREATED_BY, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.UPDATED_AT, + ] + search_fields = [ + models.Problem.field_name.TITLE, + models.Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + ] + ordering = ['-'+models.Problem.field_name.CREATED_AT] + actions = [ + 'analyze', + ] + + @admin.action(description="Analyze selected problems.") + def analyze(self, request, queryset: QuerySet[models.Problem]): + for obj in queryset: + service = services.get_problem_service(obj) + service.analyze() diff --git a/app/problems/admin/problem_analysis.py b/app/problems/admin/problem_analysis.py new file mode 100644 index 0000000..9992c2b --- /dev/null +++ b/app/problems/admin/problem_analysis.py @@ -0,0 +1,42 @@ +from textwrap import shorten + +from django.contrib import admin + +from problems import models + + +@admin.register(models.ProblemAnalysis) +class ProblemAnalysisModelAdmin(admin.ModelAdmin): + list_display = [ + models.ProblemAnalysis.field_name.PROBLEM, + models.ProblemAnalysis.field_name.DIFFICULTY, + 'get_time_complexity', + 'get_tags', + 'get_hint_steps', + models.ProblemAnalysis.field_name.CREATED_AT, + ] + search_fields = [ + models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, + models.ProblemAnalysis.field_name.TIME_COMPLEXITY, + ] + ordering = ['-'+models.ProblemAnalysis.field_name.CREATED_AT] + + @admin.display(description='Time complexity') + def get_time_complexity(self, obj: models.ProblemAnalysis) -> str: + return f'O({obj.time_complexity})' + + @admin.display(description='Tags') + def get_tags(self, analysis: models.ProblemAnalysis) -> str: + tag_keys = models.ProblemAnalysisTag.objects.filter(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, + }).select_related( + models.ProblemAnalysisTag.field_name.TAG, + ).values_list( + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, + flat=True, + ) + return ', '.join(tag_keys) + + @admin.display(description='Hint steps') + def get_hint_steps(self, obj: models.ProblemAnalysis) -> int: + return len(obj.hint) diff --git a/app/problems/admin/problem_analysis_tag.py b/app/problems/admin/problem_analysis_tag.py new file mode 100644 index 0000000..b8619e3 --- /dev/null +++ b/app/problems/admin/problem_analysis_tag.py @@ -0,0 +1,21 @@ +from django.contrib import admin + +from problems import models + + +@admin.register(models.ProblemAnalysisTag) +class ProblemAnalysisTagModelAdmin(admin.ModelAdmin): + list_display = [ + models.ProblemAnalysisTag.field_name.ANALYSIS, + models.ProblemAnalysisTag.field_name.TAG, + ] + search_fields = [ + models.ProblemAnalysisTag.field_name.ANALYSIS+'__' + + models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, + models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, + models.ProblemAnalysisTag.field_name.TAG + + '__'+models.ProblemTag.field_name.NAME_KO, + models.ProblemAnalysisTag.field_name.TAG + + '__'+models.ProblemTag.field_name.NAME_EN, + ] + ordering = [models.ProblemAnalysisTag.field_name.ANALYSIS] diff --git a/app/problems/admin/problem_tag.py b/app/problems/admin/problem_tag.py new file mode 100644 index 0000000..a6c2819 --- /dev/null +++ b/app/problems/admin/problem_tag.py @@ -0,0 +1,18 @@ +from django.contrib import admin + +from problems import models + + +@admin.register(models.ProblemTag) +class ProblemTagModelAdmin(admin.ModelAdmin): + list_display = [ + models.ProblemTag.field_name.KEY, + models.ProblemTag.field_name.NAME_KO, + models.ProblemTag.field_name.NAME_EN, + ] + search_fields = [ + models.ProblemTag.field_name.KEY, + models.ProblemTag.field_name.NAME_KO, + models.ProblemTag.field_name.NAME_EN, + ] + ordering = [models.ProblemTag.field_name.KEY] diff --git a/app/problems/admin/problem_tag_relation.py b/app/problems/admin/problem_tag_relation.py new file mode 100644 index 0000000..e1e1eb0 --- /dev/null +++ b/app/problems/admin/problem_tag_relation.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from problems import models + + +@admin.register(models.ProblemTagRelation) +class ProblemTagRelationModelAdmin(admin.ModelAdmin): + list_display = [ + models.ProblemTagRelation.field_name.PARENT, + models.ProblemTagRelation.field_name.CHILD, + ] + search_fields = [ + models.ProblemTagRelation.field_name.PARENT, + models.ProblemTagRelation.field_name.CHILD, + ] + ordering = [models.ProblemTagRelation.field_name.PARENT] diff --git a/app/problems/constants.py b/app/problems/constants.py deleted file mode 100644 index 2457e40..0000000 --- a/app/problems/constants.py +++ /dev/null @@ -1,21 +0,0 @@ -from typing import NamedTuple - - -_Unit = NamedTuple('_Unit', [ - ('name_ko', str), - ('name_en', str), - ('abbr', str), -]) - - -class Unit: - MEGA_BYTE = _Unit( - name_ko="메가 바이트", - name_en="Mega Bytes", - abbr="MB", - ) - SECOND = _Unit( - name_ko="초", - name_en="Seconds", - abbr="s", - ) diff --git a/app/problems/dto/__init__.py b/app/problems/dto.py similarity index 70% rename from app/problems/dto/__init__.py rename to app/problems/dto.py index 689e80e..f0d787f 100644 --- a/app/problems/dto/__init__.py +++ b/app/problems/dto.py @@ -2,7 +2,7 @@ from dataclasses import field from typing import Tuple -from problems import models +from problems import enums @dataclass @@ -15,9 +15,9 @@ class ProblemDTO: time_limit: float -@dataclass(frozen=True) +@dataclass class ProblemAnalysisDTO: time_complexity: str - difficulty: models.ProblemDifficultyChoices + difficulty: enums.ProblemDifficulty tags: Tuple[str] = field(default_factory=tuple) - hint: Tuple[str] = field(default_factory=tuple) + hints: Tuple[str] = field(default_factory=tuple) diff --git a/app/problems/models/choices.py b/app/problems/enums.py similarity index 54% rename from app/problems/models/choices.py rename to app/problems/enums.py index fae8cec..06232e1 100644 --- a/app/problems/models/choices.py +++ b/app/problems/enums.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from django.db import models @@ -7,7 +9,16 @@ } -class ProblemDifficultyChoices(models.IntegerChoices): +class ProblemDifficulty(models.IntegerChoices): + @staticmethod + def from_label(label: str) -> ProblemDifficulty: + return { + 'EASY': ProblemDifficulty.EASY, + 'NORMAL': ProblemDifficulty.NORMAL, + 'HARD': ProblemDifficulty.HARD, + }[label] + + UNDER_ANALYSIS = 0, '분석 중' EASY = 1, '쉬움' NORMAL = 2, '보통' @@ -20,3 +31,8 @@ def get_name(self, lang='ko') -> str: f'choose from {NAMES.keys()}' ) return NAMES[lang][self.value] + + +class Unit(models.TextChoices): + MEGA_BYTE = 'MB', "메가 바이트" + SECOND = 's', "초" diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py index 9923136..e90e4f5 100644 --- a/app/problems/models/__init__.py +++ b/app/problems/models/__init__.py @@ -1,16 +1,5 @@ -from problems.models.choices import ProblemDifficultyChoices from problems.models.problem import Problem from problems.models.problem_analysis import ProblemAnalysis from problems.models.problem_analysis_tag import ProblemAnalysisTag from problems.models.problem_tag import ProblemTag from problems.models.problem_tag_relation import ProblemTagRelation - - -__all__ = ( - 'Problem', - 'ProblemAnalysis', - 'ProblemAnalysisTag', - 'ProblemDifficultyChoices', - 'ProblemTag', - 'ProblemTagRelation', -) diff --git a/app/problems/models/problem.py b/app/problems/models/problem.py index 6483cec..da94cb1 100644 --- a/app/problems/models/problem.py +++ b/app/problems/models/problem.py @@ -1,5 +1,6 @@ from django.db import models +from problems import enums from users.models import User @@ -25,13 +26,21 @@ class Problem(models.Model): help_text='문제 출력 설명을 입력해주세요.', blank=True, ) - memory_limit_megabyte = models.FloatField( + memory_limit = models.FloatField( help_text='문제 메모리 제한을 입력해주세요. (MB 단위)', ) - time_limit_second = models.FloatField( + memory_limit_unit = models.TextField( + choices=enums.Unit.choices, + default=enums.Unit.MEGA_BYTE, + ) + time_limit = models.FloatField( help_text='문제 시간 제한을 입력해주세요. (초 단위)', default=1.0, ) + time_limit_unit = models.TextField( + choices=enums.Unit.choices, + default=enums.Unit.SECOND, + ) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( User, @@ -47,8 +56,10 @@ class field_name: DESCRIPTION = 'description' INPUT_DESCRIPTION = 'input_description' OUTPUT_DESCRIPTION = 'output_description' - MEMORY_LIMIT_MEGABYTE = 'memory_limit_megabyte' - TIME_LIMIT_SECOND = 'time_limit_second' + MEMORY_LIMIT = 'memory_limit' + MEMORY_LIMIT_UNIT = 'memory_limit_unit' + TIME_LIMIT = 'time_limit' + TIME_LIMIT_UNIT = 'time_limit_unit' CREATED_AT = 'created_at' CREATED_BY = 'created_by' UPDATED_AT = 'updated_at' @@ -58,7 +69,3 @@ class Meta: def __str__(self) -> str: return f'[{self.pk} : {self.title}]' - - def save(self, *args, **kwargs) -> None: - super().save(*args, **kwargs) - # TODO: Add to ProblemAnalysisQueue diff --git a/app/problems/models/problem_analysis.py b/app/problems/models/problem_analysis.py index f8ac924..66d05f0 100644 --- a/app/problems/models/problem_analysis.py +++ b/app/problems/models/problem_analysis.py @@ -1,18 +1,18 @@ from django.db import models -from problems.models.choices import ProblemDifficultyChoices +from problems import enums from problems.models.problem import Problem class ProblemAnalysis(models.Model): - problem = models.OneToOneField( + problem = models.ForeignKey( Problem, on_delete=models.CASCADE, help_text='문제를 입력해주세요.', ) difficulty = models.IntegerField( help_text='문제 난이도를 입력해주세요.', - choices=ProblemDifficultyChoices.choices, + choices=enums.ProblemDifficulty.choices, ) time_complexity = models.CharField( max_length=100, diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py index dc6f494..ce5bfc9 100644 --- a/app/problems/serializers/__init__.py +++ b/app/problems/serializers/__init__.py @@ -23,8 +23,8 @@ class Meta: models.Problem.field_name.DESCRIPTION, models.Problem.field_name.INPUT_DESCRIPTION, models.Problem.field_name.OUTPUT_DESCRIPTION, - models.Problem.field_name.MEMORY_LIMIT_MEGABYTE, - models.Problem.field_name.TIME_LIMIT_SECOND, + models.Problem.field_name.MEMORY_LIMIT, + models.Problem.field_name.TIME_LIMIT, models.Problem.field_name.CREATED_AT, models.Problem.field_name.CREATED_BY, models.Problem.field_name.UPDATED_AT, @@ -39,8 +39,8 @@ class Meta: models.Problem.field_name.UPDATED_AT, ] extra_kwargs = { - models.Problem.field_name.MEMORY_LIMIT_MEGABYTE: {'write_only': True}, - models.Problem.field_name.TIME_LIMIT_SECOND: {'write_only': True}, + models.Problem.field_name.MEMORY_LIMIT: {'write_only': True}, + models.Problem.field_name.TIME_LIMIT: {'write_only': True}, } def create(self, validated_data): diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index dd3caa5..d7840d3 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -1,19 +1,19 @@ from rest_framework import serializers +from problems import enums from problems import models from problems import services -from problems.constants import Unit class MemoryLimitField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) + unit = enums.Unit(problem.memory_limit_unit) return { - "value": problem.memory_limit_megabyte, + "value": problem.memory_limit, "unit": { - "name_ko": Unit.MEGA_BYTE.name_ko, - "name_en": Unit.MEGA_BYTE.name_en, - "abbr": Unit.MEGA_BYTE.abbr, + "name": unit.label, + "abbr": unit.value, }, } @@ -21,12 +21,12 @@ def to_representation(self, problem: models.Problem): class TimeLimitField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) + unit = enums.Unit(problem.time_limit_unit) return { - "value": problem.time_limit_second, + "value": problem.time_limit, "unit": { - "name_ko": Unit.SECOND.name_ko, - "name_en": Unit.SECOND.name_en, - "abbr": Unit.SECOND.abbr, + "name": unit.label, + "abbr": unit.value, }, } diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py index 5b717c7..e119e9b 100644 --- a/app/problems/services/__init__.py +++ b/app/problems/services/__init__.py @@ -1,120 +1,7 @@ -from __future__ import annotations - -from typing import List -from typing import Optional - -from django.db.models import QuerySet - from problems import models +from problems.services.base import ProblemService +from problems.services.concrete import ConcreteProblemService -class ProblemService: - def __init__(self, instance: models.Problem) -> None: - assert isinstance(instance, models.Problem) - self.instance = instance - - def analyses(self) -> QuerySet[models.ProblemAnalysis]: - return models.ProblemAnalysis.objects.filter(**{ - models.ProblemAnalysis.field_name.PROBLEM: self.instance, - }) - - def analysis(self) -> Optional[models.ProblemAnalysis]: - if not self.is_analyzed(): - return None - return self.analyses().latest() - - def is_analyzed(self) -> bool: - return self.analyses().exists() - - def analyze(self): - # TODO - pass - - -class ProblemAnalysisService: - @staticmethod - def from_problem(problem: models.Problem) -> ProblemAnalysisService: - analysis = ProblemService(problem).analysis() - return ProblemAnalysisService(analysis) - - def __init__(self, instance: Optional[models.ProblemAnalysis] = None) -> None: - if instance is None: - self._strategy = UnanalyzedProblemAnalysisService() - else: - self._strategy = AnalyzedProblemAnalysisService(instance) - - def is_analyzed(self) -> bool: - return self._strategy.is_analyzed() - - def difficulty(self) -> models.ProblemDifficultyChoices: - return self._strategy.difficulty() - - def difficulty_description(self) -> str: - return self._strategy.difficulty_description() - - def time_complexity(self) -> str: - return self._strategy.time_complexity() - - def time_complexity_description(self) -> str: - return self._strategy.time_complexity_description() - - def tags(self) -> List[models.ProblemTag]: - return self._strategy.tags() - - def hints(self) -> List[str]: - return self._strategy.hints() - - -class UnanalyzedProblemAnalysisService(ProblemAnalysisService): - def __init__(self, *args, **kwargs) -> None: - return - - def is_analyzed(self) -> bool: - return False - - def difficulty(self) -> models.ProblemDifficultyChoices: - return models.ProblemDifficultyChoices.UNDER_ANALYSIS - - def time_complexity(self) -> str: - return '' - - def tags(self) -> List[models.ProblemTag]: - return [] - - def hints(self) -> List[str]: - return [] - - def difficulty_description(self) -> str: - return "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - - def time_complexity_description(self) -> str: - return "AI가 분석을 진행하고 있어요! [이 기능은 추가될 예정이 없습니다]" - - -class AnalyzedProblemAnalysisService(ProblemAnalysisService): - def __init__(self, instance: models.ProblemAnalysis) -> None: - assert isinstance(instance, models.ProblemAnalysis) - self.instance = instance - - def is_analyzed(self) -> bool: - return True - - def difficulty(self) -> models.ProblemDifficultyChoices: - return models.ProblemDifficultyChoices(self.instance.difficulty) - - def time_complexity(self) -> str: - return self.instance.time_complexity - - def tags(self) -> List[models.ProblemTag]: - return models.ProblemAnalysisTag.objects.filter(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: self.instance, - }).values_list(models.ProblemAnalysisTag.field_name.TAG, flat=True) - - def hints(self) -> List[str]: - return self.instance.hint - - def difficulty_description(self) -> str: - return "기초적인 계산적 사고와 프로그래밍 문법만 있어도 해결 가능한 수준 [이 기능은 추가될 예정이 없습니다]" - - def time_complexity_description(self) -> str: - return "선형시간에 풀이가 가능한 문제. N의 크기에 주의하세요. [이 기능은 추가될 예정이 없습니다]" +def get_problem_service(problem: models.Problem) -> ProblemService: + return ConcreteProblemService(problem) diff --git a/app/problems/services/analysers/__init__.py b/app/problems/services/analysers/__init__.py new file mode 100644 index 0000000..945cb81 --- /dev/null +++ b/app/problems/services/analysers/__init__.py @@ -0,0 +1,6 @@ +from problems.services.analysers.base import ProblemAnalyzer +from problems.services.analysers.gemini import GeminiProblemAnalyzer + + +def get_analyzer() -> ProblemAnalyzer: + return GeminiProblemAnalyzer.get_instance() diff --git a/app/problems/services/analysis/analyser.py b/app/problems/services/analysers/base.py similarity index 93% rename from app/problems/services/analysis/analyser.py rename to app/problems/services/analysers/base.py index e3e01a2..63e1c21 100644 --- a/app/problems/services/analysis/analyser.py +++ b/app/problems/services/analysers/base.py @@ -1,7 +1,7 @@ from problems import dto -class ProblemAnalyser: +class ProblemAnalyzer: """문제를 분석하는 클래스의 추상 클래스입니다. 문제 데이터를 받아와 문제의 분석 결과를 반환하는 analyze() 메소드를 구현해야 합니다. diff --git a/app/problems/services/analysers/gemini/__init__.py b/app/problems/services/analysers/gemini/__init__.py new file mode 100644 index 0000000..1a49c8b --- /dev/null +++ b/app/problems/services/analysers/gemini/__init__.py @@ -0,0 +1 @@ +from problems.services.analysers.gemini.analyser import GeminiProblemAnalyzer diff --git a/app/problems/services/analysers/gemini/analyser.py b/app/problems/services/analysers/gemini/analyser.py new file mode 100644 index 0000000..cf1f1e7 --- /dev/null +++ b/app/problems/services/analysers/gemini/analyser.py @@ -0,0 +1,66 @@ +from logging import getLogger + +from django.conf import settings +from google import generativeai as genai + +from problems.dto import ProblemDTO +from problems.dto import ProblemAnalysisDTO +from problems.services.analysers.base import ProblemAnalyzer +from problems.services.analysers.gemini import prompts +from problems.services.analysers.gemini import parsers + + +# TODO: replace log channel +logger = getLogger('django.server') + + +class GeminiProblemAnalyzer(ProblemAnalyzer): + _instance = None + + @classmethod + def get_instance(cls) -> ProblemAnalyzer: + if cls._instance is None: + cls._instance = GeminiProblemAnalyzer() + return cls._instance + + def __init__(self) -> None: + genai.configure(api_key=settings.GEMINI_API_KEY) + self.model = genai.GenerativeModel( + model_name="gemini-1.5-flash", + generation_config={ + "temperature": 1, + "top_p": 0.95, + "top_k": 64, + "max_output_tokens": 8192, + "response_mime_type": "text/plain", + }, + ) + + def analyze(self, problem_dto: ProblemDTO) -> ProblemAnalysisDTO: + chat = self.model.start_chat(history=[]) + + analysis_dto = ProblemAnalysisDTO( + difficulty=None, + time_complexity=None, + tags=None, + hints=None, + ) + + prompt = prompts.get_tags_prompt(problem_dto, analysis_dto) + assistant_message = chat.send_message(content=prompt).text + analysis_dto.tags = parsers.parse_tags(assistant_message) + + prompt = prompts.get_difficulty_prompt(problem_dto, analysis_dto) + assistant_message = chat.send_message(content=prompt).text + analysis_dto.difficulty = parsers.parse_difficulty(assistant_message) + + prompt = prompts.get_time_complexity_prompt(problem_dto, analysis_dto) + assistant_message = chat.send_message(content=prompt).text + analysis_dto.time_complexity = parsers.parse_time_complexity( + assistant_message) + + prompt = prompts.get_hints_prompt(problem_dto, analysis_dto) + assistant_message = chat.send_message(content=prompt).text + analysis_dto.hints = parsers.parse_hints(assistant_message) + + return analysis_dto diff --git a/app/problems/services/analysers/gemini/parsers.py b/app/problems/services/analysers/gemini/parsers.py new file mode 100644 index 0000000..669a76e --- /dev/null +++ b/app/problems/services/analysers/gemini/parsers.py @@ -0,0 +1,62 @@ +from functools import cache +from logging import getLogger +from typing import List +import re + +from sympy import latex +from sympy.parsing.latex import parse_latex + +from problems.models import ProblemTag + + +logger = getLogger('django.server') + + +@cache +def get_valid_tags() -> List[str]: + """실제로 존재하는 태그의 목록을 가져온다. (solved.ac 태그 기준) + + 비싼 연산이므로 캐시하여 사용한다. + """ + return [*ProblemTag.objects.values_list(ProblemTag.field_name.KEY, flat=True)] + + +def parse_difficulty(assistant_message: str) -> str: + return assistant_message.strip().split('\n')[0] + + +def parse_tags(assistant_message: str) -> List[str]: + tags = [] + # 1. 실제로 존재하는 태그의 목록을 가져온다. (solved.ac 태그 기준) + valid_tags = get_valid_tags() + # 2. + tokens = assistant_message.split(',') + logger.info(f'ANALYSER TAG {len(tokens)} 개의 후보로 시작. ({", ".join(tokens)})') + regex = re.compile(r"^[a-zA-Z0-9_]+$") + for token in tokens: + token = token.strip().lower().replace(" ", "_") + if regex.match(token) and (token in valid_tags): + tags.append(token) + logger.info(f'... 태그가 선택됨: "{token}"') + else: + logger.info(f'... 태그가 선택되지 않음: "{token}"') + logger.info(f'ANALYSER TAG {len(tags)} 개의 후보로 종료. ({", ".join(tags)})') + return tags + + +def parse_time_complexity(assistant_message: str) -> str: + match = re.search(r"\$O\((.*?)\)\$", assistant_message) + if match: + complexity = match.group(1) + try: + parsed_expr = parse_latex(complexity) + latex_str = latex(parsed_expr) + return f"{latex_str}" + except Exception as e: + return f"Invalid LaTeX expression: {str(e)}" + else: + return "No complexity found" + + +def parse_hints(assistant_message: str) -> List[str]: + return re.sub(r"##.*", "", assistant_message).strip() diff --git a/app/problems/services/analysers/gemini/prompts.py b/app/problems/services/analysers/gemini/prompts.py new file mode 100644 index 0000000..4ac6cb2 --- /dev/null +++ b/app/problems/services/analysers/gemini/prompts.py @@ -0,0 +1,112 @@ +from textwrap import dedent + +from problems.dto import ProblemDTO +from problems.dto import ProblemAnalysisDTO + + +def get_difficulty_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: + sys_msg = dedent(""" + You are a helpful, respectful and honest assistant. Always answer clearly as possible. + First line of your answer must be one of these words "EASY", "NORMAL", "HARD". + From 3rd lines, you can reason why you have chosen the difficulty. + + Your goal is to classify the difficulty of user given problem when it is solved in most efficient way. + + Here is how you determine the difficulties. + + "EASY": For beginners in programming, it's not difficult if you have a basic level of computational thinking and understand programming syntax. May require cultivating a bit more logical thinking to solve. Problems which should be tagged as string, implementation, sorting, hash, greedy, binary search, graph, and queue should be considered "EASY". + + "NORMAL": Advanced level for intermediate programmers. Tend to require a more advanced approach to solve. Algorithms, Hashing, Dynamic Programming, Mathematical reasoning, Graph Traversal, and so on... + + "HARD": Highly challenging level for advanced programmers. If you know special algorithms regardless of practical skills, give it a try. These problems are suitable for high-level programming competitions, requiring proficiency and speed. Dynamic Programming, Binary Search, Segment Tree, Priority Queue, ... + """) + usr_msg = get_problem_message(problem_dto) + return build_prompt(sys_msg, usr_msg) + + +def get_tags_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: + sys_msg = dedent(""" + You are a helpful, respectful and honest assistant. Always answer clearly as possible. + You must answer in English. + + Please ensure that your responses are academically accurate. + You must not explain details but only answer the tags. + + I will give you some algorithm or data structure tags separated by lines. + You must only give the algorithmic tag analysis listed in Solved.ac. + Your goal is to select multiple tags (separated by commas) of the algorithm or data structure below that can best describe the user given problem. + Please analyze the tags accurately. + + Tags cannot appear at all in tag analysis. You must classify accurately, and these tags must be those listed in solved.ac. + Make sure that accurate tags are output, not filtered values. + And if there are multiple tags, be sure to print them all. + """) + usr_msg = get_problem_message(problem_dto) + return build_prompt(sys_msg, usr_msg) + + +def get_time_complexity_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: + sys_msg = dedent(""" + You are a helpful, respectful and honest time-complexity analyst. Always answer clearly as possible. + Analysts don't converse in natural language as people do. They communicate using intricate equations and symbols, typically preferring the syntax of the LaTeX language. + Please ensure that your responses are academically accurate. + Your goal is to find out the short answer about Big-O notated time complexity that can best describe user given problem. + + This is important: + The first line of your answer must be a single LaTeX syntax wrapped by `$` marks, which is the Big-O notated time complexity. + Starting from the third lines, you can shortly brief how you have approached to the answer. + """) + usr_msg = get_problem_message(problem_dto) + return build_prompt(sys_msg, usr_msg) + + +def get_hints_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: + assert analysis_dto.tags is not None + assert analysis_dto.difficulty is not None + assert analysis_dto.time_complexity is not None + sys_msg = dedent(""" + You are a helpful, respectful, and honest assistant. Always answer as clearly as possible. + Please ensure that your responses are academically accurate. + Your goal is to provide step-by-step hints to help the user solve the given problem. + + I will give you a problem statement, including tags, difficulty, and time complexity. + Your task is to provide a series of hints to approach and solve the problem. Code should never be included when outputting hints. Your response should be in Korean. + """) + usr_msg = dedent(f""" + Problem Statement: + {problem_dto.description} + + Tags: {', '.join(analysis_dto.tags)} + Difficulty: {analysis_dto.difficulty} + Time Complexity: {analysis_dto.time_complexity} + """) + return build_prompt(sys_msg, usr_msg) + + +def get_problem_message(problem_dto: ProblemDTO) -> str: + return dedent(f""" + 제목 + {problem_dto.title} + + 메모리 제한 + {problem_dto.memory_limit} MB + + 시간 제한 + {problem_dto.time_limit} 초 + + 문제 + {problem_dto.description} + + 입력 + {problem_dto.input_description} + + 출력 + {problem_dto.output_description} + """) + + +def build_prompt(sys_msg: str, usr_msg: str) -> str: + return dedent(f""" + {sys_msg} + {usr_msg} + """) diff --git a/app/problems/services/analysers/gpt/__init__.py b/app/problems/services/analysers/gpt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/services/analysers/gpt/analyser.py b/app/problems/services/analysers/gpt/analyser.py new file mode 100644 index 0000000..bd3121f --- /dev/null +++ b/app/problems/services/analysers/gpt/analyser.py @@ -0,0 +1,15 @@ +from problems import dto +from problems.services.analysers.base import ProblemAnalyzer + + +class GPTProblemAnalyzer(ProblemAnalyzer): + _instance = None + + @classmethod + def get_instance(cls) -> ProblemAnalyzer: + if cls._instance is None: + cls._instance = GPTProblemAnalyzer() + return cls._instance + + def analyze(self, problem: dto.ProblemDTO) -> dto.ProblemAnalysisDTO: + raise NotImplementedError diff --git a/app/problems/services/analysis/__init__.py b/app/problems/services/analysis/__init__.py deleted file mode 100644 index 0602a0f..0000000 --- a/app/problems/services/analysis/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -from __future__ import annotations - -from problems import dto -from problems.services.analysis.analyser import ProblemAnalyser -from problems.services.analysis.llm.gemini import GeminiProblemAnalyser - - - -class AnalysingService: - instance = None - analyzer_class = GeminiProblemAnalyser - - @classmethod - def get_instance(cls) -> AnalysingService: - if not cls.instance: - cls.instance = cls() - return cls.instance - - def get_analyzer(self) -> ProblemAnalyser: - return self.analyzer_class() - - def analyze(self, problem: dto.ProblemDTO) -> dto.ProblemAnalysisDTO: - analyzer = self.get_analyzer() - return analyzer.analyze(problem) diff --git a/app/problems/services/analysis/llm/gemini.py b/app/problems/services/analysis/llm/gemini.py deleted file mode 100644 index 74f0b0d..0000000 --- a/app/problems/services/analysis/llm/gemini.py +++ /dev/null @@ -1,10 +0,0 @@ -from problems.services.analysis.analyser import ( - ProblemAnalyser, - ProblemDTO, - ProblemAnalysisDTO, -) - - -class GeminiProblemAnalyser(ProblemAnalyser): - def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: - raise NotImplementedError diff --git a/app/problems/services/analysis/llm/gpt.py b/app/problems/services/analysis/llm/gpt.py deleted file mode 100644 index 22e2cbf..0000000 --- a/app/problems/services/analysis/llm/gpt.py +++ /dev/null @@ -1,10 +0,0 @@ -from problems.services.analysis.analyser import ( - ProblemAnalyser, - ProblemDTO, - ProblemAnalysisDTO, -) - - -class GPTProblemAnalyser(ProblemAnalyser): - def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: - raise NotImplementedError diff --git a/app/problems/services/base.py b/app/problems/services/base.py new file mode 100644 index 0000000..4915e04 --- /dev/null +++ b/app/problems/services/base.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +from typing import List +from typing import Optional + +from django.db.models import QuerySet + +from problems import enums +from problems import models + + +class ProblemService: + def __init__(self, instance: models.Problem) -> None: + assert isinstance(instance, models.Problem) + self.instance = instance + + def query_analyses(self) -> QuerySet[models.ProblemAnalysis]: + raise NotImplementedError + + def query_tags(self) -> QuerySet[models.ProblemAnalysisTag]: + raise NotImplementedError + + def get_analysis(self) -> Optional[models.ProblemAnalysis]: + """ + raises: + ProblemAnalysis.DoesNotExists + """ + raise NotImplementedError + + def is_analyzed(self) -> bool: + raise NotImplementedError + + def analyze(self) -> None: + """문제를 분석합니다. + + 오래 걸리는 작업인 만큼 본 함수는 분석 작업을 예약만 하고, + 실제 분석은 비동기적으로 진행됩니다.""" + raise NotImplementedError + + def difficulty(self) -> enums.ProblemDifficulty: + raise NotImplementedError + + def time_complexity(self) -> str: + raise NotImplementedError + + def tags(self) -> List[str]: + raise NotImplementedError + + def hints(self) -> List[str]: + raise NotImplementedError diff --git a/app/problems/services/concrete.py b/app/problems/services/concrete.py new file mode 100644 index 0000000..ee3a1eb --- /dev/null +++ b/app/problems/services/concrete.py @@ -0,0 +1,144 @@ +from logging import getLogger +from typing import List +from typing import Optional + +from background_task import background +from django.db.models import QuerySet +from django.db.transaction import atomic + +from problems import dto +from problems import enums +from problems import models +from problems.services.base import ProblemService +from problems.services.analysers import ProblemAnalyzer +from problems.services.analysers import GeminiProblemAnalyzer + + +logger = getLogger('django.server') + + +class ConcreteProblemService(ProblemService): + def __init__(self, instance: models.Problem) -> None: + super().__init__(instance) + self._analysis = None + + def query_analyses(self) -> QuerySet[models.ProblemAnalysis]: + return models.ProblemAnalysis.objects.filter(**{ + models.ProblemAnalysis.field_name.PROBLEM: self.instance, + }) + + def query_analysis_tags(self) -> QuerySet[models.ProblemAnalysisTag]: + if not self.is_analyzed(): + return models.ProblemAnalysisTag.objects.none() + else: + return models.ProblemAnalysisTag.objects.filter(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: self.get_analysis(), + }) + + def query_tags(self) -> QuerySet[models.ProblemTag]: + return models.ProblemTag.objects.filter(**{ + models.ProblemTag.field_name.KEY+'__in': self.tags(), + }) + + def get_analysis(self) -> Optional[models.ProblemAnalysis]: + try: + self._analysis = self.query_analyses().latest() + except models.ProblemAnalysis.DoesNotExist: + self._analysis = None + finally: + return self._analysis + + def is_analyzed(self) -> bool: + return self.get_analysis() is not None + + def analyze(self) -> None: + schedule_analyze(self.instance.pk) + + def difficulty(self) -> enums.ProblemDifficulty: + if (analysis := self.get_analysis()) is None: + return enums.ProblemDifficulty.UNDER_ANALYSIS + return enums.ProblemDifficulty(analysis.difficulty) + + def time_complexity(self) -> str: + if (analysis := self.get_analysis()) is None: + return '?' + return analysis.time_complexity + + def tags(self) -> List[str]: + return self.query_analysis_tags().select_related( + models.ProblemAnalysisTag.field_name.TAG, + ).values_list(models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY) + + def hints(self) -> List[str]: + if (analysis := self.get_analysis()) is None: + return [] + return analysis.hint + + +@background +def schedule_analyze(problem_id: int): + logger.info(f'PK={problem_id} 문제의 분석 준비중.') + problem = get_problem(problem_id) + problem_dto = get_problem_dto(problem) + problem_repr = f'PK={problem_id} ({problem_dto.title})' + logger.info('문제 분석기를 불러오는 중.') + analyzer = get_analyzer() + logger.info(f'{problem_repr} 문제의 분석 시작.') + analysis_dto = analyzer.analyze(problem_dto) + logger.info(f'{problem_repr} 문제의 분석 완료.') + logger.info(f'{problem_repr} 문제의 분석 결과를 데이터베이스에 저장하는 중.') + save_analysis(problem, analysis_dto) + logger.info(f'{problem_repr} 문제의 분석 결과를 데이터베이스에 저장 완료.') + + +def get_analyzer() -> ProblemAnalyzer: + return GeminiProblemAnalyzer.get_instance() + + +def get_problem(problem_id: int) -> models.Problem: + try: + return models.Problem.objects.get(pk=problem_id) + except models.Problem.DoesNotExist: + logger.warning(f'id가 {problem_id}인 문제를 찾을 수 없습니다.') + + +def get_problem_dto(problem: models.Problem) -> dto.ProblemDTO: + return dto.ProblemDTO( + title=problem.title, + description=problem.description, + input_description=problem.input_description, + output_description=problem.output_description, + memory_limit=problem.memory_limit, + time_limit=problem.time_limit, + ) + + +def save_analysis(problem: models.Problem, analysis_dto: dto.ProblemAnalysisDTO) -> models.ProblemAnalysis: + analysis = models.ProblemAnalysis(**{ + models.ProblemAnalysis.field_name.PROBLEM: problem, + models.ProblemAnalysis.field_name.TIME_COMPLEXITY: analysis_dto.time_complexity, + models.ProblemAnalysis.field_name.DIFFICULTY: enums.ProblemDifficulty.from_label(analysis_dto.difficulty), + models.ProblemAnalysis.field_name.HINT: analysis_dto.hints, + }) + analysis_tags = [] + for tag_key in analysis_dto.tags: + try: + tag = models.ProblemTag.objects.get(**{ + models.ProblemTag.field_name.KEY: tag_key, + }) + except models.ProblemTag.DoesNotExist: + logger.warn(f'문제 분석 결과에 알 수 없는 태그 [{tag_key}] 가 포함된 것을 발견하였습니다.') + tag = models.ProblemTag.objects.create(**{ + models.ProblemTag.field_name.KEY: tag_key, + models.ProblemTag.field_name.NAME_KO: f'존재하지 않는 태그: [{tag_key}]', + models.ProblemTag.field_name.NAME_EN: f'존재하지 않는 태그: [{tag_key}]', + }) + finally: + analysis_tag = models.ProblemAnalysisTag(**{ + models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, + models.ProblemAnalysisTag.field_name.TAG: tag, + }) + analysis_tags.append(analysis_tag) + with atomic(): + analysis.save() + models.ProblemAnalysisTag.objects.bulk_create(analysis_tags) diff --git a/requirements.txt b/requirements.txt index 7c47084..68bb70e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,7 @@ djangorestframework djangorestframework-simplejwt Pillow drf-yasg +# MODEL DEPENDENCY +google-generativeai +sympy +antlr4-python3-runtime==4.11 # sympy dependency for latex parsing From ac89f91b64eaeb8a231fdc377e8e9fdc509c77fb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 08:46:36 +0900 Subject: [PATCH 389/552] =?UTF-8?q?feat(problems):=20=EB=AC=B8=EC=A0=9C?= =?UTF-8?q?=EA=B0=80=20=EB=93=B1=EB=A1=9D=EB=90=A0=20=EB=95=8C=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=20=EC=9E=91=EC=97=85=EC=9D=84=20=EC=9E=90=EB=8F=99?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=98=88=EC=95=BD=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/models/__init__.py | 1 + app/problems/models/signals.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 app/problems/models/signals.py diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py index e90e4f5..2adf4d4 100644 --- a/app/problems/models/__init__.py +++ b/app/problems/models/__init__.py @@ -3,3 +3,4 @@ from problems.models.problem_analysis_tag import ProblemAnalysisTag from problems.models.problem_tag import ProblemTag from problems.models.problem_tag_relation import ProblemTagRelation +from problems.models import signals diff --git a/app/problems/models/signals.py b/app/problems/models/signals.py new file mode 100644 index 0000000..e379fad --- /dev/null +++ b/app/problems/models/signals.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from problems import models +from problems import services + + +@receiver(post_save, sender=models.Problem) +def schedule_analyze(sender, instance: models.Problem, **kwargs): + service = services.get_problem_service(instance) + service.analyze() From 36a5cd660906219e6f29b428bf1f467dbd354e08 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 09:28:46 +0900 Subject: [PATCH 390/552] =?UTF-8?q?refactor(problems):=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=EA=B8=B0=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=98=81?= =?UTF-8?q?=EB=AC=B8=20=EB=AA=85=EC=B9=AD=EC=9D=84=20=EB=AF=B8=EA=B5=AD?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC=20analysers=20?= =?UTF-8?q?->=20analyzers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/services/analysers/__init__.py | 6 ------ app/problems/services/analysers/gemini/__init__.py | 1 - app/problems/services/analyzers/__init__.py | 6 ++++++ app/problems/services/{analysers => analyzers}/base.py | 0 app/problems/services/analyzers/gemini/__init__.py | 1 + .../gemini/analyser.py => analyzers/gemini/analyzer.py} | 6 +++--- .../services/{analysers => analyzers}/gemini/parsers.py | 0 .../services/{analysers => analyzers}/gemini/prompts.py | 0 .../services/{analysers => analyzers}/gpt/__init__.py | 0 .../gpt/analyser.py => analyzers/gpt/analyzer.py} | 2 +- app/problems/services/concrete.py | 4 ++-- 11 files changed, 13 insertions(+), 13 deletions(-) delete mode 100644 app/problems/services/analysers/__init__.py delete mode 100644 app/problems/services/analysers/gemini/__init__.py create mode 100644 app/problems/services/analyzers/__init__.py rename app/problems/services/{analysers => analyzers}/base.py (100%) create mode 100644 app/problems/services/analyzers/gemini/__init__.py rename app/problems/services/{analysers/gemini/analyser.py => analyzers/gemini/analyzer.py} (92%) rename app/problems/services/{analysers => analyzers}/gemini/parsers.py (100%) rename app/problems/services/{analysers => analyzers}/gemini/prompts.py (100%) rename app/problems/services/{analysers => analyzers}/gpt/__init__.py (100%) rename app/problems/services/{analysers/gpt/analyser.py => analyzers/gpt/analyzer.py} (86%) diff --git a/app/problems/services/analysers/__init__.py b/app/problems/services/analysers/__init__.py deleted file mode 100644 index 945cb81..0000000 --- a/app/problems/services/analysers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from problems.services.analysers.base import ProblemAnalyzer -from problems.services.analysers.gemini import GeminiProblemAnalyzer - - -def get_analyzer() -> ProblemAnalyzer: - return GeminiProblemAnalyzer.get_instance() diff --git a/app/problems/services/analysers/gemini/__init__.py b/app/problems/services/analysers/gemini/__init__.py deleted file mode 100644 index 1a49c8b..0000000 --- a/app/problems/services/analysers/gemini/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from problems.services.analysers.gemini.analyser import GeminiProblemAnalyzer diff --git a/app/problems/services/analyzers/__init__.py b/app/problems/services/analyzers/__init__.py new file mode 100644 index 0000000..ac6290f --- /dev/null +++ b/app/problems/services/analyzers/__init__.py @@ -0,0 +1,6 @@ +from problems.services.analyzers.base import ProblemAnalyzer +from problems.services.analyzers.gemini import GeminiProblemAnalyzer + + +def get_analyzer() -> ProblemAnalyzer: + return GeminiProblemAnalyzer.get_instance() diff --git a/app/problems/services/analysers/base.py b/app/problems/services/analyzers/base.py similarity index 100% rename from app/problems/services/analysers/base.py rename to app/problems/services/analyzers/base.py diff --git a/app/problems/services/analyzers/gemini/__init__.py b/app/problems/services/analyzers/gemini/__init__.py new file mode 100644 index 0000000..c6f0f8a --- /dev/null +++ b/app/problems/services/analyzers/gemini/__init__.py @@ -0,0 +1 @@ +from problems.services.analyzers.gemini.analyzer import GeminiProblemAnalyzer diff --git a/app/problems/services/analysers/gemini/analyser.py b/app/problems/services/analyzers/gemini/analyzer.py similarity index 92% rename from app/problems/services/analysers/gemini/analyser.py rename to app/problems/services/analyzers/gemini/analyzer.py index cf1f1e7..da44b39 100644 --- a/app/problems/services/analysers/gemini/analyser.py +++ b/app/problems/services/analyzers/gemini/analyzer.py @@ -5,9 +5,9 @@ from problems.dto import ProblemDTO from problems.dto import ProblemAnalysisDTO -from problems.services.analysers.base import ProblemAnalyzer -from problems.services.analysers.gemini import prompts -from problems.services.analysers.gemini import parsers +from problems.services.analyzers.base import ProblemAnalyzer +from problems.services.analyzers.gemini import prompts +from problems.services.analyzers.gemini import parsers # TODO: replace log channel diff --git a/app/problems/services/analysers/gemini/parsers.py b/app/problems/services/analyzers/gemini/parsers.py similarity index 100% rename from app/problems/services/analysers/gemini/parsers.py rename to app/problems/services/analyzers/gemini/parsers.py diff --git a/app/problems/services/analysers/gemini/prompts.py b/app/problems/services/analyzers/gemini/prompts.py similarity index 100% rename from app/problems/services/analysers/gemini/prompts.py rename to app/problems/services/analyzers/gemini/prompts.py diff --git a/app/problems/services/analysers/gpt/__init__.py b/app/problems/services/analyzers/gpt/__init__.py similarity index 100% rename from app/problems/services/analysers/gpt/__init__.py rename to app/problems/services/analyzers/gpt/__init__.py diff --git a/app/problems/services/analysers/gpt/analyser.py b/app/problems/services/analyzers/gpt/analyzer.py similarity index 86% rename from app/problems/services/analysers/gpt/analyser.py rename to app/problems/services/analyzers/gpt/analyzer.py index bd3121f..640663f 100644 --- a/app/problems/services/analysers/gpt/analyser.py +++ b/app/problems/services/analyzers/gpt/analyzer.py @@ -1,5 +1,5 @@ from problems import dto -from problems.services.analysers.base import ProblemAnalyzer +from problems.services.analyzers.base import ProblemAnalyzer class GPTProblemAnalyzer(ProblemAnalyzer): diff --git a/app/problems/services/concrete.py b/app/problems/services/concrete.py index ee3a1eb..92eb6d9 100644 --- a/app/problems/services/concrete.py +++ b/app/problems/services/concrete.py @@ -10,8 +10,8 @@ from problems import enums from problems import models from problems.services.base import ProblemService -from problems.services.analysers import ProblemAnalyzer -from problems.services.analysers import GeminiProblemAnalyzer +from problems.services.analyzers import ProblemAnalyzer +from problems.services.analyzers import GeminiProblemAnalyzer logger = getLogger('django.server') From 68bae01bc7fa7c2f960d59fffcbfca65a27d5bc8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 09:29:27 +0900 Subject: [PATCH 391/552] =?UTF-8?q?refactor(problems):=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EB=B6=84=EC=84=9D=20=EA=B4=80=EB=A0=A8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=EB=93=A4=EC=9D=98=20logging=20=EC=B1=84=EB=84=90?= =?UTF-8?q?=EC=9D=84=20'problems.analyzers'=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 11 +++++++++++ .../services/analyzers/gemini/analyzer.py | 17 ++++++++--------- .../services/analyzers/gemini/parsers.py | 2 +- app/problems/services/concrete.py | 2 +- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/app/config/settings.py b/app/config/settings.py index ecf6c05..1c9a1f3 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -233,6 +233,13 @@ 'when': 'D', "formatter": "django.server", }, + "problems": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/problems.log', + 'when': 'D', + "formatter": "standard", + }, "mail_admins": { "level": "ERROR", "filters": ["require_debug_false"], @@ -262,5 +269,9 @@ "level": "DEBUG", 'propagate': False, }, + "problems": { + "handlers": ["problems"], + "level": "INFO", + }, }, } diff --git a/app/problems/services/analyzers/gemini/analyzer.py b/app/problems/services/analyzers/gemini/analyzer.py index da44b39..82836db 100644 --- a/app/problems/services/analyzers/gemini/analyzer.py +++ b/app/problems/services/analyzers/gemini/analyzer.py @@ -10,8 +10,7 @@ from problems.services.analyzers.gemini import parsers -# TODO: replace log channel -logger = getLogger('django.server') +logger = getLogger('problems.analyzers.gemini.analyzer') class GeminiProblemAnalyzer(ProblemAnalyzer): @@ -37,6 +36,7 @@ def __init__(self) -> None: ) def analyze(self, problem_dto: ProblemDTO) -> ProblemAnalysisDTO: + logger.info(f'"{problem_dto.title}" 문제의 분석 시작.') chat = self.model.start_chat(history=[]) analysis_dto = ProblemAnalysisDTO( @@ -45,22 +45,21 @@ def analyze(self, problem_dto: ProblemDTO) -> ProblemAnalysisDTO: tags=None, hints=None, ) - + logger.info(f'... 태그 분석 중...') prompt = prompts.get_tags_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.tags = parsers.parse_tags(assistant_message) - + logger.info(f'... 난이도 분석 중...') prompt = prompts.get_difficulty_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.difficulty = parsers.parse_difficulty(assistant_message) - + logger.info(f'... 시간 복잡도 분석 중...') prompt = prompts.get_time_complexity_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text - analysis_dto.time_complexity = parsers.parse_time_complexity( - assistant_message) - + analysis_dto.time_complexity = parsers.parse_time_complexity(assistant_message) + logger.info(f'... 힌트 분석 중...') prompt = prompts.get_hints_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.hints = parsers.parse_hints(assistant_message) - + logger.info(f'"{problem_dto.title}" 문제의 분석 완료.') return analysis_dto diff --git a/app/problems/services/analyzers/gemini/parsers.py b/app/problems/services/analyzers/gemini/parsers.py index 669a76e..b00945f 100644 --- a/app/problems/services/analyzers/gemini/parsers.py +++ b/app/problems/services/analyzers/gemini/parsers.py @@ -9,7 +9,7 @@ from problems.models import ProblemTag -logger = getLogger('django.server') +logger = getLogger('problems.analyzers.gemini.parsers') @cache diff --git a/app/problems/services/concrete.py b/app/problems/services/concrete.py index 92eb6d9..7450875 100644 --- a/app/problems/services/concrete.py +++ b/app/problems/services/concrete.py @@ -14,7 +14,7 @@ from problems.services.analyzers import GeminiProblemAnalyzer -logger = getLogger('django.server') +logger = getLogger('problems.services') class ConcreteProblemService(ProblemService): From 727520b80c6c3659da4500cb2f1d55815606a17d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 10:15:48 +0900 Subject: [PATCH 392/552] =?UTF-8?q?chore(problems):=20background=20tasks?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=95=9C=20=EB=B2=88=EC=97=90=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=97=90=20=EC=B2=98=EB=A6=AC=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=EC=9D=98=20=EC=88=98=EB=A5=BC=201=EA=B0=9C?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/config/settings.py b/app/config/settings.py index 1c9a1f3..9b56a5a 100644 --- a/app/config/settings.py +++ b/app/config/settings.py @@ -275,3 +275,8 @@ }, }, } + + +#Django Background Tasks + +BACKGROUND_TASK_ASYNC_THREADS = 1 From 8e8042e097240a2c91dd2e0a72309e13b8684d99 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 10:16:17 +0900 Subject: [PATCH 393/552] =?UTF-8?q?chore(problems):=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=20=ED=91=9C=EA=B8=B0=EB=90=9C=20=ED=83=80=EC=9E=85=20=ED=9E=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/services/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/problems/services/base.py b/app/problems/services/base.py index 4915e04..49d4ccd 100644 --- a/app/problems/services/base.py +++ b/app/problems/services/base.py @@ -17,7 +17,7 @@ def __init__(self, instance: models.Problem) -> None: def query_analyses(self) -> QuerySet[models.ProblemAnalysis]: raise NotImplementedError - def query_tags(self) -> QuerySet[models.ProblemAnalysisTag]: + def query_tags(self) -> QuerySet[models.ProblemTag]: raise NotImplementedError def get_analysis(self) -> Optional[models.ProblemAnalysis]: From e1a89e9f10ee61c2ff293d55830ac528ba7f273d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 10:16:43 +0900 Subject: [PATCH 394/552] =?UTF-8?q?fix(problems):=20problems.services=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20=EA=B5=AC=EC=A1=B0=EC=9D=98=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=9C=BC=EB=A1=9C=20=EC=83=9D=EA=B8=B4=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=ED=95=AD=20=EC=A0=84=ED=8C=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/serializers/fields.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index d7840d3..d5019ff 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -34,7 +34,7 @@ def to_representation(self, problem: models.Problem): class DifficultyField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) - service = services.ProblemAnalysisService.from_problem(problem) + service = services.get_problem_service(problem) return { "name_ko": service.difficulty().get_name(lang='ko'), "name_en": service.difficulty().get_name(lang='en'), @@ -45,7 +45,7 @@ def to_representation(self, problem: models.Problem): class AnalysisField(serializers.SerializerMethodField): def to_representation(self, problem: models.Problem): assert isinstance(problem, models.Problem) - service = services.ProblemAnalysisService.from_problem(problem) + service = services.get_problem_service(problem) return { 'difficulty': { "name_ko": service.difficulty().get_name(lang='ko'), @@ -64,7 +64,7 @@ def to_representation(self, problem: models.Problem): 'name_en': tag.name_en, 'name_ko': tag.name_ko, } - for tag in service.tags() + for tag in service.query_tags() ], 'is_analyzed': service.is_analyzed(), } From c584062499fcbf5210e703a16d427c48dc3cf89a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 10:17:00 +0900 Subject: [PATCH 395/552] =?UTF-8?q?feat(boj):=20BOJProblem=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/boj/models.py b/app/boj/models.py index 6e28140..fed1250 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -44,3 +44,14 @@ class field_name: LEVEL = 'level' RATING = 'rating' CREATED_AT = 'created_at' + + +class BOJProblem(models.Model): + title = models.TextField() + description = models.TextField() + input_description = models.TextField() + output_description = models.TextField() + memory_limit = models.FloatField() + time_limit = models.FloatField() + tags = models.JSONField(default=list) + level = models.IntegerField(choices=enums.BOJLevel.choices) From 74251d354c9cf841514506e465a2fead40a74632 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 10:35:00 +0900 Subject: [PATCH 396/552] =?UTF-8?q?refactor(config.settings):=20settings.p?= =?UTF-8?q?y=20=EB=8C=80=EC=8B=A0=20settings.base.py,=20settings.local.py?= =?UTF-8?q?=20=EB=A5=BC=20=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/.gitignore | 2 + app/config/settings/__init__.py | 0 app/config/settings/base/__init__.py | 194 +++++++++++++++++++++++++++ app/config/settings/base/logging.py | 86 ++++++++++++ app/manage.py | 2 +- 5 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 app/config/settings/.gitignore create mode 100644 app/config/settings/__init__.py create mode 100644 app/config/settings/base/__init__.py create mode 100644 app/config/settings/base/logging.py diff --git a/app/config/settings/.gitignore b/app/config/settings/.gitignore new file mode 100644 index 0000000..49379ca --- /dev/null +++ b/app/config/settings/.gitignore @@ -0,0 +1,2 @@ +local.py +production.py diff --git a/app/config/settings/__init__.py b/app/config/settings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py new file mode 100644 index 0000000..07e20fb --- /dev/null +++ b/app/config/settings/base/__init__.py @@ -0,0 +1,194 @@ +""" +Django settings for project. + +Generated by 'django-admin startproject' using Django 4.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from datetime import timedelta +from pathlib import Path + +from config.settings.base.logging import * + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = [] + + +# CORS + +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + + "corsheaders", + "drf_yasg", + 'background_task', + "rest_framework", + 'rest_framework_simplejwt', + + "boj", + "users", + "problems", + "crews", + "submissions", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +AUTH_USER_MODEL = 'users.User' + +AUTHENTICATION_BACKENDS = [ + 'users.backends.UserAuthBackend', +] + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=60), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_MAX_LIFETIME': timedelta(days=200), +} + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ), +} + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = 'Asia/Seoul' + +USE_I18N = True + +USE_TZ = False + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +STATIC_ROOT = BASE_DIR / '.static' + + +# Meida files (Images) +# https://docs.djangoproject.com/en/4.2/topics/files/ + +MEDIA_URL = "media/" + +MEDIA_ROOT = BASE_DIR / '.media' + + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +DEFAULT_EXCEPTION_REPORTER = "config.utils.NACLExceptionReporter" + +APPEND_SLASH = False + +# Swagger Settings (DRf-YASG) + +SWAGGER_SETTINGS = { + "LOGIN_URL": "/api/v1/auth/signin", + "LOGOUT_URL": "/api/v1/auth/signout", +} + + +#Django Background Tasks + +BACKGROUND_TASK_ASYNC_THREADS = 1 diff --git a/app/config/settings/base/logging.py b/app/config/settings/base/logging.py new file mode 100644 index 0000000..51dfe85 --- /dev/null +++ b/app/config/settings/base/logging.py @@ -0,0 +1,86 @@ +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "filters": { + "require_debug_false": { + "()": "django.utils.log.RequireDebugFalse", + }, + "require_debug_true": { + "()": "django.utils.log.RequireDebugTrue", + }, + }, + "formatters": { + "standard": { + "()": "config.utils.ColorlessServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + }, + "django.server": { + "()": "config.utils.ColorlessServerFormatter", + "format": "[{server_time}] {message}", + "style": "{", + } + }, + "handlers": { + "console": { + "level": "INFO", + "filters": ["require_debug_true"], + 'class': 'config.utils.FileAndStreamHandler', + 'filename': 'logs/console.log', + "formatter": "standard", + }, + "django.mail": { + "level": "ERROR", + "filters": ["require_debug_true"], + 'class': 'config.utils.FileAndStreamHandler', + 'filename': 'logs/django.mail.log', + }, + "django.server": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/django.server.log', + 'when': 'D', + "formatter": "django.server", + }, + "problems": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/problems.log', + 'when': 'D', + "formatter": "standard", + }, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + 'class': 'logging.FileHandler', + 'filename': 'logs/mail_admins.log', + }, + "django.security.DisallowedHost": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': 'logs/django.security.DisallowedHost.log', + 'when': 'D', + "formatter": "standard", + }, + }, + "loggers": { + "django": { + "handlers": ["console", "mail_admins"], + "level": "INFO", + }, + "django.server": { + "handlers": ["django.server"], + "level": "INFO", + "propagate": False, + }, + 'django.security.DisallowedHost': { + 'handlers': ['django.security.DisallowedHost'], + "level": "DEBUG", + 'propagate': False, + }, + "problems": { + "handlers": ["problems"], + "level": "INFO", + }, + }, +} diff --git a/app/manage.py b/app/manage.py index d28672e..6326a7c 100755 --- a/app/manage.py +++ b/app/manage.py @@ -6,7 +6,7 @@ def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") try: from django.core.management import execute_from_command_line except ImportError as exc: From e56f9fc8d6dc03d2a8b70d481055f92cb8a1c340 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 19:44:24 +0900 Subject: [PATCH 397/552] =?UTF-8?q?feat(boj):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90(User)=EC=83=9D=EC=84=B1=20=EC=8B=9C=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=EC=9C=BC=EB=A1=9C=20BOJUser=EB=8F=84=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EB=90=98=EB=8F=84=EB=A1=9D=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/apps.py | 3 +++ app/boj/signals/__init__.py | 0 app/boj/signals/handlers.py | 16 ++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 app/boj/signals/__init__.py create mode 100644 app/boj/signals/handlers.py diff --git a/app/boj/apps.py b/app/boj/apps.py index 025268a..2e7bd37 100644 --- a/app/boj/apps.py +++ b/app/boj/apps.py @@ -4,3 +4,6 @@ class BojConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "boj" + + def ready(self) -> None: + import boj.signals.handlers diff --git a/app/boj/signals/__init__.py b/app/boj/signals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/boj/signals/handlers.py b/app/boj/signals/handlers.py new file mode 100644 index 0000000..af939d7 --- /dev/null +++ b/app/boj/signals/handlers.py @@ -0,0 +1,16 @@ +from django.db.models.signals import pre_save +from django.dispatch import receiver + +from boj import models +from boj import services +from users.models import User + + +@receiver(pre_save, sender=User) +def auto_wire_boj_user(sender, user: User, **kwargs): + assert user.boj_username + boj_user, created = models.BOJUser.objects.get_or_create(**{ + models.BOJUser.field_name.USERNAME: user.boj_username + }) + if created: + services.fetch(boj_user.username) From aed891377199c5404bee7c8c74b04880b9058aa1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 19:48:35 +0900 Subject: [PATCH 398/552] feat(problems): API endpoint rename: /problems/search -> /problems --- app/config/urls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/config/urls.py b/app/config/urls.py index c595df1..1067208 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -42,8 +42,8 @@ path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), + path("problems", problems.views.ProblemSearchListAPIView.as_view()), path("problem", problems.views.ProblemCreateAPIView.as_view()), - path("problems/search", problems.views.ProblemSearchListAPIView.as_view()), path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), path("users/current", users.views.CurrentUserAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), From 04af1abd0b703840b64518b1ab5645697a76a779 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 19:55:08 +0900 Subject: [PATCH 399/552] =?UTF-8?q?fix(problems):=20=EB=82=9C=EC=9D=B4?= =?UTF-8?q?=EB=8F=84,=20=EC=8B=9C=EA=B0=84=EB=B3=B5=EC=9E=A1=EB=8F=84=20?= =?UTF-8?q?=EC=84=A4=EB=AA=85=EC=9D=80=20=EA=B8=B0=EB=8A=A5=20=EC=9A=94?= =?UTF-8?q?=EA=B5=AC=EC=82=AC=ED=95=AD=EC=97=90=20=EC=97=86=EC=9C=BC?= =?UTF-8?q?=EB=AF=80=EB=A1=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/serializers/fields.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index d5019ff..48dabe9 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -51,11 +51,9 @@ def to_representation(self, problem: models.Problem): "name_ko": service.difficulty().get_name(lang='ko'), "name_en": service.difficulty().get_name(lang='en'), 'value': service.difficulty().value, - 'description': service.difficulty_description(), }, 'time_complexity': { 'value': service.time_complexity(), - 'description': service.time_complexity_description(), }, 'hints': service.hints(), 'tags': [ From d62c6adf121ec1932b6a3f1d428fa96135616035 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 20:27:58 +0900 Subject: [PATCH 400/552] =?UTF-8?q?fix(problems):=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20API=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/serializers/__init__.py | 14 ++++++++++++++ app/problems/views/__init__.py | 19 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py index ce5bfc9..774aa64 100644 --- a/app/problems/serializers/__init__.py +++ b/app/problems/serializers/__init__.py @@ -5,6 +5,20 @@ from users.serializers import UserMinimalSerializer +class ProblemCreateSerializer(serializers.ModelSerializer): + class Meta: + model = models.Problem + fields = [ + models.Problem.field_name.TITLE, + models.Problem.field_name.LINK, + models.Problem.field_name.DESCRIPTION, + models.Problem.field_name.INPUT_DESCRIPTION, + models.Problem.field_name.OUTPUT_DESCRIPTION, + models.Problem.field_name.MEMORY_LIMIT, + models.Problem.field_name.TIME_LIMIT, + ] + + class ProblemDetailSerializer(serializers.ModelSerializer): analysis = fields.AnalysisField(read_only=True) memory_limit = fields.MemoryLimitField(read_only=True) diff --git a/app/problems/views/__init__.py b/app/problems/views/__init__.py index 7852f4a..5283cdc 100644 --- a/app/problems/views/__init__.py +++ b/app/problems/views/__init__.py @@ -1,5 +1,7 @@ from rest_framework import generics from rest_framework import permissions +from rest_framework import status +from rest_framework.response import Response from problems import models from problems import serializers @@ -10,7 +12,22 @@ class ProblemCreateAPIView(generics.CreateAPIView): queryset = models.Problem.objects.all() permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.ProblemDetailSerializer + serializer_class = serializers.ProblemCreateSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.validated_data[models.Problem.field_name.CREATED_BY] = request.user + self.perform_create(serializer) + serializer = serializers.ProblemDetailSerializer( + instance=serializer.instance, + ) + headers = self.get_success_headers(serializer.data) + return Response( + data=serializer.data, + status=status.HTTP_201_CREATED, + headers=headers, + ) class ProblemDetailRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView): From 7cb6a103054a0b1472cab6faf86a82455a09829a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 20:34:00 +0900 Subject: [PATCH 401/552] =?UTF-8?q?refactor(problems):=20views=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EC=9D=80=20=EB=8A=94=20=EA=B7=9C=EB=AA=A8=EA=B0=80=20?= =?UTF-8?q?=EC=9E=91=EC=95=84=20views.py=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/{views/__init__.py => views.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/problems/{views/__init__.py => views.py} (100%) diff --git a/app/problems/views/__init__.py b/app/problems/views.py similarity index 100% rename from app/problems/views/__init__.py rename to app/problems/views.py From b25c803f7802062adefc078b7da23658c6cdca71 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 21:15:25 +0900 Subject: [PATCH 402/552] =?UTF-8?q?fix(users):=20signal=20=ED=95=B8?= =?UTF-8?q?=EB=93=A4=EB=9F=AC=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=A4=ED=8C=A8=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95=20(kwargs=20=EC=9D=B4?= =?UTF-8?q?=EB=A6=84=20=EB=A7=A4=ED=95=91=20=EB=AC=B8=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit login 시 last_login 시간이 save 되면서 pre_save signal을 트리거 하는 모양. --- app/boj/signals/handlers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/boj/signals/handlers.py b/app/boj/signals/handlers.py index af939d7..df07328 100644 --- a/app/boj/signals/handlers.py +++ b/app/boj/signals/handlers.py @@ -7,10 +7,10 @@ @receiver(pre_save, sender=User) -def auto_wire_boj_user(sender, user: User, **kwargs): - assert user.boj_username +def auto_wire_boj_user(sender, instance: User, **kwargs): + assert instance.boj_username boj_user, created = models.BOJUser.objects.get_or_create(**{ - models.BOJUser.field_name.USERNAME: user.boj_username + models.BOJUser.field_name.USERNAME: instance.boj_username }) if created: services.fetch(boj_user.username) From 00f5ba33684608ed3e69e01dec419ea1612d20c4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 21:36:43 +0900 Subject: [PATCH 403/552] =?UTF-8?q?refactor(boj.services):=20BOJUserServic?= =?UTF-8?q?e=20=EA=B0=9D=EC=B2=B4=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EC=97=AC=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=A0=9C=EA=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/services.py | 53 ++++++++++++++++++++++--------------- app/boj/signals/handlers.py | 3 ++- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/app/boj/services.py b/app/boj/services.py index cc58b8a..bf0b937 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -1,7 +1,9 @@ +from __future__ import annotations + import logging +from background_task import background from django.utils import timezone -import background_task import requests from boj import dto @@ -12,31 +14,38 @@ logger = logging.getLogger('tle.boj') -def get_object(username: str) -> models.BOJUser: - obj, created = models.BOJUser.objects.get_or_create(**{ - models.BOJUser.field_name.USERNAME: username, - }) - return obj +def get_boj_user_service(boj_username: str) -> BOJUserService: + return BOJUserService(boj_username) + + +class BOJUserService: + def __init__(self, username: str): + self.username = username + self.instance, created = models.BOJUser.objects.get_or_create(**{ + models.BOJUser.field_name.USERNAME: username, + }) + def update(self) -> None: + update_boj_user(self.username) -def snapshot(obj: models.BOJUser) -> models.BOJUserSnapshot: - return models.BOJUserSnapshot(**{ - models.BOJUserSnapshot.field_name.USER: obj, - models.BOJUserSnapshot.field_name.LEVEL: obj.level, - models.BOJUserSnapshot.field_name.RATING: obj.rating, - models.BOJUserSnapshot.field_name.CREATED_AT: obj.updated_at, - }) + def create_snapshot(self) -> models.BOJUserSnapshot: + return models.BOJUserSnapshot(**{ + models.BOJUserSnapshot.field_name.USER: self.instance, + models.BOJUserSnapshot.field_name.LEVEL: self.instance.level, + models.BOJUserSnapshot.field_name.RATING: self.instance.rating, + models.BOJUserSnapshot.field_name.CREATED_AT: self.instance.updated_at, + }) -@background_task.background() -def fetch(username: str) -> None: - data = fetch_data(username) - obj = get_object(username) - obj.level = data.level.value - obj.rating = data.rating - obj.updated_at = timezone.now() - obj.save() - snapshot(obj).save() +@background +def update_boj_user(boj_username: str): + data = fetch_data(boj_username) + service = get_boj_user_service(boj_username) + service.instance.level = data.level.value + service.instance.rating = data.rating + service.instance.updated_at = timezone.now() + service.instance.save() + service.create_snapshot() def fetch_data(username: str) -> dto.BOJUserData: diff --git a/app/boj/signals/handlers.py b/app/boj/signals/handlers.py index df07328..fb6a77a 100644 --- a/app/boj/signals/handlers.py +++ b/app/boj/signals/handlers.py @@ -13,4 +13,5 @@ def auto_wire_boj_user(sender, instance: User, **kwargs): models.BOJUser.field_name.USERNAME: instance.boj_username }) if created: - services.fetch(boj_user.username) + service = services.get_boj_user_service(instance.username) + service.update() From ae8cc5e35c7758bcbecc7d8e6b9c38987ba9308f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 21:38:18 +0900 Subject: [PATCH 404/552] =?UTF-8?q?feat(boj.serializers):=20BOJUserSeriali?= =?UTF-8?q?zer=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/serializers.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/boj/serializers.py diff --git a/app/boj/serializers.py b/app/boj/serializers.py new file mode 100644 index 0000000..d3e3f1f --- /dev/null +++ b/app/boj/serializers.py @@ -0,0 +1,35 @@ +from rest_framework import serializers + +from boj.enums import BOJLevel +from boj.models import BOJUser + + +class BOJLevelField(serializers.SerializerMethodField): + def to_representation(self, instance: BOJUser): + assert isinstance(instance, BOJUser) + level = BOJLevel(instance.level) + return { + 'value': level.value, + 'name': level.get_name(lang='ko', arabic=False), + } + + +class BOJProfileUrlField(serializers.SerializerMethodField): + def to_representation(self, instance: BOJUser): + assert isinstance(instance, BOJUser) + return f'https://boj.kr/{instance.username}' + + +class BOJUserSerializer(serializers.ModelSerializer): + level = BOJLevelField() + profile_url = BOJProfileUrlField() + + class Meta: + model = BOJUser + fields = [ + BOJUser.field_name.USERNAME, + 'profile_url', + 'level', + BOJUser.field_name.RATING, + BOJUser.field_name.UPDATED_AT, + ] From fc9ed894a76368d84c59a306853942c13af2e497 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 21:55:53 +0900 Subject: [PATCH 405/552] =?UTF-8?q?refactor(users):=20User=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=97=90=EC=84=9C=20boj=5Fusername=EC=9D=84=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8=ED=95=9C=20boj=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=86=8D=EC=84=B1=EB=93=A4=EC=9D=84=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit boj 모듈에서 관리하도록 책임을 위임 --- app/users/admin.py | 2 - app/users/models/__init__.py | 10 +-- app/users/models/choices.py | 65 ------------------- app/users/models/user.py | 40 ++++-------- .../__init__.py => serializers.py} | 32 ++++----- app/users/serializers/fields.py | 21 ------ app/users/serializers/mixins.py | 21 ------ app/users/{views/__init__.py => views.py} | 29 ++++----- 8 files changed, 37 insertions(+), 183 deletions(-) delete mode 100644 app/users/models/choices.py rename app/users/{serializers/__init__.py => serializers.py} (67%) delete mode 100644 app/users/serializers/fields.py delete mode 100644 app/users/serializers/mixins.py rename app/users/{views/__init__.py => views.py} (92%) diff --git a/app/users/admin.py b/app/users/admin.py index 32632bb..229a085 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -17,13 +17,11 @@ class UserAdmin(BaseUserAdmin): User.field_name.USERNAME, User.field_name.EMAIL, User.field_name.BOJ_USERNAME, - User.field_name.BOJ_LEVEL, 'get_crews', User.field_name.IS_ACTIVE, User.field_name.IS_STAFF, User.field_name.IS_SUPERUSER, User.field_name.CREATED_AT, - User.field_name.BOJ_LEVEL_UPDATED_AT, ] @admin.display(description='captains / members') diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py index 2fec29e..ea19936 100644 --- a/app/users/models/__init__.py +++ b/app/users/models/__init__.py @@ -1,12 +1,4 @@ -from users.models.choices import UserBojLevelChoices +from boj.enums import BOJLevel as UserBojLevelChoices from users.models.user import User from users.models.user_email_verification import UserEmailVerification from users.models.user_manager import UserManager - - -__all__ = ( - 'User', - 'UserEmailVerification', - 'UserManager', - 'UserBojLevelChoices', -) diff --git a/app/users/models/choices.py b/app/users/models/choices.py deleted file mode 100644 index 137d498..0000000 --- a/app/users/models/choices.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.db import models - - -DIVISION_NAMES = { - 'ko': ['난이도를 매길 수 없음', '브론즈', '실버', '골드', '플래티넘', '다이아몬드', '루비'], - 'en': ['Unrated', 'Bronze', 'Silver', 'Gold', 'Platinum', 'Diamond', 'Ruby'], -} -ARABIC_NUMERALS = ['', 'I', 'II', 'III', 'IV', 'V'] - - -class UserBojLevelChoices(models.IntegerChoices): - U = 0, 'Unrated' - B5 = 1, '브론즈 5' - B4 = 2, '브론즈 4' - B3 = 3, '브론즈 3' - B2 = 4, '브론즈 2' - B1 = 5, '브론즈 1' - S5 = 6, '실버 5' - S4 = 7, '실버 4' - S3 = 8, '실버 3' - S2 = 9, '실버 2' - S1 = 10, '실버 1' - G5 = 11, '골드 5' - G4 = 12, '골드 4' - G3 = 13, '골드 3' - G2 = 14, '골드 2' - G1 = 15, '골드 1' - P5 = 16, '플래티넘 5' - P4 = 17, '플래티넘 4' - P3 = 18, '플래티넘 3' - P2 = 19, '플래티넘 2' - P1 = 20, '플래티넘 1' - D5 = 21, '다이아몬드 5' - D4 = 22, '다이아몬드 4' - D3 = 23, '다이아몬드 3' - D2 = 24, '다이아몬드 2' - D1 = 25, '다이아몬드 1' - R5 = 26, '루비 5' - R4 = 27, '루비 4' - R3 = 28, '루비 3' - R2 = 29, '루비 2' - R1 = 30, '루비 1' - M = 31, '마스터' - - def get_division(self) -> int: - if self == self.U: - return 0 - return ((self.value-1) // 5)+1 - - def get_division_name(self, lang='en') -> str: - return DIVISION_NAMES[lang][self.get_division()] - - def get_tier(self) -> int: - if self == self.U: - return 0 - return 5 - ((self.value-1) % 5) - - def get_tier_name(self, arabic=True) -> str: - tier = self.get_tier() - if arabic: - return ARABIC_NUMERALS[tier] - return str(tier) - - def get_name(self, lang='en', arabic=True) -> str: - return f'{self.get_division_name(lang=lang)} {self.get_tier_name(arabic=arabic)}' diff --git a/app/users/models/user.py b/app/users/models/user.py index 632ce5c..791cbb7 100644 --- a/app/users/models/user.py +++ b/app/users/models/user.py @@ -1,10 +1,10 @@ from __future__ import annotations -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.contrib.auth.models import AbstractBaseUser +from django.contrib.auth.models import PermissionsMixin from django.db import models from django.utils import timezone -from users.models.choices import UserBojLevelChoices from users.models.user_manager import UserManager @@ -13,6 +13,16 @@ def get_profile_image_path(user: User, filename: str) -> str: class User(AbstractBaseUser, PermissionsMixin): + username = models.CharField( + verbose_name='username', + max_length=30, + unique=True, + ) + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + ) profile_image = models.ImageField( help_text='프로필 이미지', upload_to=get_profile_image_path, @@ -29,30 +39,6 @@ class User(AbstractBaseUser, PermissionsMixin): null=True, blank=True, ) - boj_level = models.IntegerField( - help_text='백준 티어', - choices=UserBojLevelChoices.choices, - null=True, - blank=True, - default=None, - ) - boj_level_updated_at = models.DateTimeField( - help_text='백준 티어 갱신 시각', - null=True, - blank=True, - default=None, - ) - - username = models.CharField( - verbose_name='username', - max_length=30, - unique=True, - ) - email = models.EmailField( - verbose_name='email address', - max_length=255, - unique=True, - ) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) @@ -68,8 +54,6 @@ class User(AbstractBaseUser, PermissionsMixin): class field_name: PROFILE_IMAGE = 'profile_image' BOJ_USERNAME = 'boj_username' - BOJ_LEVEL = 'boj_level' - BOJ_LEVEL_UPDATED_AT = 'boj_level_updated_at' USERNAME = 'username' EMAIL = 'email' PASSWORD = 'password' diff --git a/app/users/serializers/__init__.py b/app/users/serializers.py similarity index 67% rename from app/users/serializers/__init__.py rename to app/users/serializers.py index 02384da..42f0735 100644 --- a/app/users/serializers/__init__.py +++ b/app/users/serializers.py @@ -1,17 +1,15 @@ from rest_framework import serializers +from boj.serializers import BOJUserSerializer +from boj.services import get_boj_user_service from users.models import User -from users.serializers.mixins import ReadOnlySerializerMixin -__all__ = ( - 'UserSignInSerializer', - 'UserSignUpSerializer', - 'UserDetailSerializer', - 'UserMinimalSerializer', - 'UserEmailSerializer', - 'EmailCodeSerializer', -) +class UserBOJField(serializers.SerializerMethodField): + def to_representation(self, instance: User): + service = get_boj_user_service(instance.boj_username) + serializer = BOJUserSerializer(instance=service.instance) + return serializer.data class EmailSerializer(serializers.Serializer): @@ -53,20 +51,20 @@ class UsernameSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): + boj = UserBOJField() + class Meta: model = User fields = [ 'id', - User.field_name.EMAIL, User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, - User.field_name.BOJ_USERNAME, - User.field_name.CREATED_AT, - User.field_name.LAST_LOGIN, + 'boj', ] + read_only_fields = ['__all__'] -class UserMinimalSerializer(serializers.ModelSerializer, ReadOnlySerializerMixin): +class UserMinimalSerializer(serializers.ModelSerializer): class Meta: model = User fields = [ @@ -74,8 +72,4 @@ class Meta: User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, ] - extra_kwargs = { - 'id': {'read_only': True}, - User.field_name.PROFILE_IMAGE: {'read_only': True}, - User.field_name.USERNAME: {'read_only': True}, - } + read_only_fields = ['__all__'] diff --git a/app/users/serializers/fields.py b/app/users/serializers/fields.py deleted file mode 100644 index 270e64c..0000000 --- a/app/users/serializers/fields.py +++ /dev/null @@ -1,21 +0,0 @@ -from users.models import User, UserBojLevelChoices -from users.serializers.mixins import ReadOnlyField - - -class UserBojField(ReadOnlyField): - def to_representation(self, user: User): - if user.boj_username is None: - user_boj_level = UserBojLevelChoices.U - else: - user_boj_level = UserBojLevelChoices(user.boj_level) - return { - 'username': user.boj_username, - 'profile_url': f'https://boj.kr/{user.boj_username}', - 'level': user_boj_level.value, - 'division': user_boj_level.get_division(), - 'division_name_en': user_boj_level.get_division_name(lang='en'), - 'division_name_ko': user_boj_level.get_division_name(lang='ko'), - 'tier': user_boj_level.get_tier(), - 'tier_name': user_boj_level.get_tier_name(arabic=True), - 'tier_updated_at': user.boj_level_updated_at, - } diff --git a/app/users/serializers/mixins.py b/app/users/serializers/mixins.py deleted file mode 100644 index de7ad99..0000000 --- a/app/users/serializers/mixins.py +++ /dev/null @@ -1,21 +0,0 @@ -from rest_framework.exceptions import PermissionDenied -from rest_framework.serializers import Field - - -class ReadOnlySerializerMixin: - def create(self, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def update(self, instance, validated_data): - raise PermissionDenied('Cannot create user through this serializer') - - def save(self, **kwargs): - raise PermissionDenied('Cannot update user through this serializer') - - -class ReadOnlyField(Field): - def get_attribute(self, instance): - return instance - - def to_internal_value(self, data): - raise PermissionDenied('This field is read-only') diff --git a/app/users/views/__init__.py b/app/users/views.py similarity index 92% rename from app/users/views/__init__.py rename to app/users/views.py index d25138d..51c42eb 100644 --- a/app/users/views/__init__.py +++ b/app/users/views.py @@ -7,7 +7,8 @@ from rest_framework.serializers import Serializer from rest_framework.views import APIView -from users import serializers, services +from users import serializers +from users import services from users.models import User @@ -23,7 +24,6 @@ class SignInAPIView(mixins.RetrieveModelMixin, generics.GenericAPIView): """사용자 로그인 API""" - authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignInSerializer @@ -52,34 +52,30 @@ def post(self, request, *args, **kwargs): ) -class SignUpAPIView(generics.GenericAPIView): +class SignUpAPIView(generics.CreateAPIView): """사용자 등록(회원가입) API""" - authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignUpSerializer get_serializer: Callable[..., Serializer] - @swagger_auto_schema(responses={ - status.HTTP_201_CREATED: '회원가입 성공', - status.HTTP_400_BAD_REQUEST: '잘못 입력한 값이 존재', - }) - def post(self, request, *args, **kwargs): + def create(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + serializer = serializers.UserSerializer(instance=serializer.instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer: serializers.SignUpSerializer): email = serializer.validated_data['email'] token = serializer.validated_data.pop('verification_token') services.verify_token(email, token) - user = services.sign_up(**serializer.validated_data) - return Response( - data=serializers.UserSerializer(instance=user).data, - status=status.HTTP_201_CREATED, - ) + serializer.instance = services.sign_up(**serializer.validated_data) class SignOutAPIView(APIView): """사용자 로그아웃 API""" - permission_classes = [permissions.IsAuthenticated] @swagger_auto_schema(responses={ @@ -92,7 +88,6 @@ def get(self, request, *args, **kwargs): class CurrentUserAPIView(generics.RetrieveAPIView): """현재 로그인한 사용자 정보 API""" - permission_classes = [permissions.IsAuthenticated] serializer_class = serializers.UserSerializer @@ -102,7 +97,6 @@ def get_object(self) -> User: class UsernameCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" - authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.UsernameSerializer @@ -162,7 +156,6 @@ class EmailVerifyThrottle(throttling.AnonRateThrottle): class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" - authentication_classes = [] throttle_classes = [] permission_classes = [permissions.AllowAny] From a8536d74c5d15013254f28ac5bb6b6aa0bb45074 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 22:02:31 +0900 Subject: [PATCH 406/552] =?UTF-8?q?fix(users.models):=20boj=5Fusername=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=A5=BC=20=ED=95=84=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EC=A7=80=EC=A0=95=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models/user.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/users/models/user.py b/app/users/models/user.py index 791cbb7..774b83f 100644 --- a/app/users/models/user.py +++ b/app/users/models/user.py @@ -36,8 +36,6 @@ class User(AbstractBaseUser, PermissionsMixin): boj_username = models.CharField( help_text='백준 아이디', max_length=40, - null=True, - blank=True, ) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) From 030e632eb42238b5e6c0379accb48ed467075744 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 22:03:26 +0900 Subject: [PATCH 407/552] =?UTF-8?q?feat(boj.enums):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=A0=88=EB=B2=A8=EC=9D=B4=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20'=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EB=B6=88=EB=9F=AC=EC=98=A4=EC=A7=80=20?= =?UTF-8?q?=EB=AA=BB=ED=95=A8'=20=EB=AC=B8=EA=B5=AC=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/enums.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/boj/enums.py b/app/boj/enums.py index 3ba08c3..9c46d90 100644 --- a/app/boj/enums.py +++ b/app/boj/enums.py @@ -62,4 +62,6 @@ def get_tier_name(self, arabic=True) -> str: return str(tier) def get_name(self, lang='en', arabic=True) -> str: + if self.value == 0: + return '사용자 정보를 불러오지 못함' return f'{self.get_division_name(lang=lang)} {self.get_tier_name(arabic=arabic)}' From 20b60ba5a752f6baf2828945614443112363bb65 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 22:42:36 +0900 Subject: [PATCH 408/552] feat(users.serializers): create `UserUpdateSerializer` --- app/users/serializers.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 42f0735..0ef366a 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -1,5 +1,7 @@ from rest_framework import serializers + +from boj.enums import BOJLevel from boj.serializers import BOJUserSerializer from boj.services import get_boj_user_service from users.models import User @@ -7,9 +9,20 @@ class UserBOJField(serializers.SerializerMethodField): def to_representation(self, instance: User): + assert isinstance(instance, User) + service = get_boj_user_service(instance.boj_username) + return BOJUserSerializer(service.instance).data + + +class UserBOJLevelNameField(serializers.SerializerMethodField): + def to_representation(self, instance: User): + assert isinstance(instance, User) service = get_boj_user_service(instance.boj_username) - serializer = BOJUserSerializer(instance=service.instance) - return serializer.data + level = BOJLevel(service.instance.level) + return { + 'value': level.value, + 'name': level.name, + } class EmailSerializer(serializers.Serializer): @@ -57,13 +70,27 @@ class Meta: model = User fields = [ 'id', - User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, + User.field_name.PROFILE_IMAGE, 'boj', ] read_only_fields = ['__all__'] +class UserUpdateSerializer(serializers.ModelSerializer): + level = UserBOJLevelNameField() + + class Meta: + model = User + fields = [ + User.field_name.EMAIL, + User.field_name.PROFILE_IMAGE, + User.field_name.USERNAME, + User.field_name.BOJ_USERNAME, + 'level', + ] + + class UserMinimalSerializer(serializers.ModelSerializer): class Meta: model = User From 18e2b04b032e61a49976ce8975fb743e8831e8bc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 22:29:17 +0900 Subject: [PATCH 409/552] =?UTF-8?q?feat(users.views):=20=EB=82=B4=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EA=B4=80=EB=A6=AC=20API=EB=A5=BC=20/user/?= =?UTF-8?q?manage=20=EB=A1=9C=20=EA=B3=B5=EA=B0=9C=20=EB=B0=8F=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 2 +- app/users/permissions.py | 2 ++ app/users/serializers.py | 10 +++++- app/users/views.py | 74 +++++++++++++++------------------------- 4 files changed, 39 insertions(+), 49 deletions(-) create mode 100644 app/users/permissions.py diff --git a/app/config/urls.py b/app/config/urls.py index 1067208..3720657 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -45,7 +45,7 @@ path("problems", problems.views.ProblemSearchListAPIView.as_view()), path("problem", problems.views.ProblemCreateAPIView.as_view()), path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), - path("users/current", users.views.CurrentUserAPIView.as_view()), + path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), path(r'swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), diff --git a/app/users/permissions.py b/app/users/permissions.py new file mode 100644 index 0000000..b98d65d --- /dev/null +++ b/app/users/permissions.py @@ -0,0 +1,2 @@ +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated diff --git a/app/users/serializers.py b/app/users/serializers.py index 0ef366a..6e30014 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -1,6 +1,5 @@ from rest_framework import serializers - from boj.enums import BOJLevel from boj.serializers import BOJUserSerializer from boj.services import get_boj_user_service @@ -84,11 +83,20 @@ class Meta: model = User fields = [ User.field_name.EMAIL, + User.field_name.PASSWORD, User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, User.field_name.BOJ_USERNAME, 'level', ] + extra_kwargs = { + User.field_name.EMAIL: { + 'read_only': True, + }, + User.field_name.PASSWORD: { + 'write_only': True, + } + } class UserMinimalSerializer(serializers.ModelSerializer): diff --git a/app/users/views.py b/app/users/views.py index 51c42eb..3a22195 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,55 +1,39 @@ -from typing import Callable - from drf_yasg.utils import swagger_auto_schema -from rest_framework import generics, mixins, permissions, status, throttling +from rest_framework import generics +from rest_framework import status +from rest_framework import throttling from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import Serializer -from rest_framework.views import APIView +from users import models +from users import permissions from users import serializers from users import services -from users.models import User - - -__all__ = ( - 'SignUpAPIView', - 'SignInAPIView', - 'SignOutAPIView', - 'CurrentUserAPIView', - 'EmailVerification', -) -class SignInAPIView(mixins.RetrieveModelMixin, - generics.GenericAPIView): +class SignInAPIView(generics.RetrieveAPIView): """사용자 로그인 API""" authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignInSerializer - get_serializer: Callable[..., Serializer] - @swagger_auto_schema(responses={ - status.HTTP_200_OK: '로그인 성공', - status.HTTP_401_UNAUTHORIZED: '로그인 실패', - }) - def post(self, request, *args, **kwargs): + def get_object(self) -> models.User: serializer = self.get_serializer(data=self.request.data) serializer.is_valid(raise_exception=True) - user = services.sign_in( + return services.sign_in( request=self.request, email=serializer.validated_data['email'], password=serializer.validated_data['password'], ) - token = services.get_user_jwt(user) - return Response( - data={ - **serializers.UserSerializer(user).data, - 'access_token': str(token.access_token), - 'refresh_token': str(token.token), - }, - status=status.HTTP_200_OK, - ) + + def retrieve(self, request: Request, *args, **kwargs): + instance = self.get_object() + token = services.get_user_jwt(instance) + return Response({ + **serializers.UserSerializer(instance).data, + 'access_token': str(token.access_token), + 'refresh_token': str(token.token), + }) class SignUpAPIView(generics.CreateAPIView): @@ -57,7 +41,6 @@ class SignUpAPIView(generics.CreateAPIView): authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignUpSerializer - get_serializer: Callable[..., Serializer] def create(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -74,7 +57,7 @@ def perform_create(self, serializer: serializers.SignUpSerializer): serializer.instance = services.sign_up(**serializer.validated_data) -class SignOutAPIView(APIView): +class SignOutAPIView(generics.GenericAPIView): """사용자 로그아웃 API""" permission_classes = [permissions.IsAuthenticated] @@ -86,21 +69,11 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class CurrentUserAPIView(generics.RetrieveAPIView): - """현재 로그인한 사용자 정보 API""" - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.UserSerializer - - def get_object(self) -> User: - return self.request.user - - class UsernameCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.UsernameSerializer - get_serializer: Callable[..., Serializer] @swagger_auto_schema( query_serializer=serializers.UsernameSerializer, @@ -128,7 +101,6 @@ class EmailCheckAPIView(generics.GenericAPIView): authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.EmailSerializer - get_serializer: Callable[..., Serializer] @swagger_auto_schema( query_serializer=serializers.EmailSerializer, @@ -159,7 +131,6 @@ class EmailVerifyAPIView(generics.GenericAPIView): authentication_classes = [] throttle_classes = [] permission_classes = [permissions.AllowAny] - get_serializer: Callable[..., Serializer] def get_serializer_class(self): if self.request.method == 'GET': @@ -201,3 +172,12 @@ def post(self, request: Request, *args, **kwargs): }, status=status.HTTP_200_OK, ) + + +class CurrentUserRetrieveUpdateAPIView(generics.RetrieveUpdateAPIView): + """현재 로그인한 사용자 정보를 조회/수정하는 API""" + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.UserUpdateSerializer + + def get_object(self) -> models.User: + return self.request.user From 212222fa49bb354aa7a5d51f13eb2e9f582eb69a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:04:22 +0900 Subject: [PATCH 410/552] =?UTF-8?q?feat(crews):=20=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20API=EC=97=90=20=EB=B3=B8?= =?UTF-8?q?=EC=9D=B8=EC=9D=B4=20=ED=81=AC=EB=A3=A8=EC=9E=A5=EC=9D=B8?= =?UTF-8?q?=EC=A7=80=20=EC=97=AC=EB=B6=80=20=EC=B6=9C=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers/__init__.py | 2 ++ app/crews/serializers/fields.py | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index 073e412..6ee9773 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -93,6 +93,7 @@ class CrewDashboardSerializer(serializers.ModelSerializer): tags = fields.CrewTagsField() members = fields.CrewMembersField() activities = fields.CrewActivitiesField() + is_captain = fields.IsCrewCaptainField() class Meta: model = models.Crew @@ -101,6 +102,7 @@ class Meta: models.Crew.field_name.ICON, models.Crew.field_name.NAME, models.Crew.field_name.NOTICE, + 'is_captain', 'tags', 'members', 'activities', diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index d140099..157d712 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -34,6 +34,14 @@ def to_representation(self, crew: models.Crew): } +class IsCrewCaptainField(serializers.SerializerMethodField): + def to_representation(self, crew: models.Crew): + assert isinstance(crew, models.Crew) + user = serializers.CurrentUserDefault()(self) + service = services.CrewService(crew) + return service.is_captain(user) + + class CrewMembersField(serializers.SerializerMethodField): """나의 동료""" From b1a9f303bf29ab9a7a8e030f2425c76e6b929eec Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:08:34 +0900 Subject: [PATCH 411/552] =?UTF-8?q?fix(users.serializers):=20=EB=B0=B1?= =?UTF-8?q?=EC=A4=80=20=EB=A0=88=EB=B2=A8=EC=9D=98=20=ED=91=9C=EC=8B=9C?= =?UTF-8?q?=EB=AA=85=EC=9D=B4=20=EC=9E=98=EB=AA=BB=20=EC=A0=84=EC=86=A1?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 6e30014..c941707 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -20,7 +20,7 @@ def to_representation(self, instance: User): level = BOJLevel(service.instance.level) return { 'value': level.value, - 'name': level.name, + 'name': level.get_name(), } From bcc1c83f449d2aa8b53384b746edfd38adfb089a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:16:55 +0900 Subject: [PATCH 412/552] =?UTF-8?q?fix(boj):=20=EB=B0=B1=EC=A4=80=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EC=A0=9C=EB=8C=80=EB=A1=9C=20=EB=B6=88=EB=9F=AC=EC=98=A4?= =?UTF-8?q?=EC=A7=80=20=EB=AA=BB=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(=EC=82=AC=EC=9A=A9=EC=9E=90=EB=AA=85?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EA=B0=92?= =?UTF-8?q?=EC=9D=84=20=EC=A0=84=EB=8B=AC=ED=95=98=EC=98=80=EC=9D=8C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/signals/handlers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/boj/signals/handlers.py b/app/boj/signals/handlers.py index fb6a77a..beb17b9 100644 --- a/app/boj/signals/handlers.py +++ b/app/boj/signals/handlers.py @@ -13,5 +13,5 @@ def auto_wire_boj_user(sender, instance: User, **kwargs): models.BOJUser.field_name.USERNAME: instance.boj_username }) if created: - service = services.get_boj_user_service(instance.username) + service = services.get_boj_user_service(instance.boj_username) service.update() From 777ce664d09ca02e5c6dc407cfd92b46571e6c65 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:19:56 +0900 Subject: [PATCH 413/552] =?UTF-8?q?fix(boj.admin):=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EC=95=A1=EC=85=98=EC=9D=98=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B6=80=EB=B6=84=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/admin.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/boj/admin.py b/app/boj/admin.py index deccee8..39e33c6 100644 --- a/app/boj/admin.py +++ b/app/boj/admin.py @@ -15,13 +15,14 @@ class BOJUserModelAdmin(admin.ModelAdmin): models.BOJUser.field_name.UPDATED_AT, ] actions = [ - 'fetch', + 'update', ] - @admin.action(description="Fetch data from solved.ac API of selected BOJ users.") - def fetch(self, request: HttpRequest, queryset: QuerySet[models.BOJUser]): + @admin.action(description="Update selected BOJ user data. (via solved.ac API)") + def update(self, request: HttpRequest, queryset: QuerySet[models.BOJUser]): for obj in queryset: - services.fetch(obj.username) + service = services.get_boj_user_service(obj.username) + service.update() @admin.register(models.BOJUserSnapshot) From 4e7b1063ab56d8e42b97b7930fc3159aef934e1d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:25:36 +0900 Subject: [PATCH 414/552] =?UTF-8?q?feat(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EA=B4=80=EB=A6=AC=20API=EC=97=90=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=EB=90=98=EB=8A=94=20=EB=B0=B1=EC=A4=80=20=EB=A0=88?= =?UTF-8?q?=EB=B2=A8=EC=9D=84=20=ED=95=9C=EA=B5=AD=EC=96=B4=EB=A1=9C=20?= =?UTF-8?q?=ED=91=9C=EC=8B=9C=EB=90=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index c941707..2a71715 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -20,7 +20,7 @@ def to_representation(self, instance: User): level = BOJLevel(service.instance.level) return { 'value': level.value, - 'name': level.get_name(), + 'name': level.get_name(lang='ko', arabic=False), } From d0cbed09b8d767a957de9f28e54f2f3544dfb4f3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:34:08 +0900 Subject: [PATCH 415/552] =?UTF-8?q?fix(crews.services):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EB=8C=80=EC=89=AC=EB=B3=B4=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8A=94=20500=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/services/crew_service.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/crews/services/crew_service.py b/app/crews/services/crew_service.py index 2e6329c..5ad4ff8 100644 --- a/app/crews/services/crew_service.py +++ b/app/crews/services/crew_service.py @@ -5,6 +5,7 @@ from django.db.transaction import atomic from rest_framework import exceptions +from boj.services import get_boj_user_service from crews import dto from crews import enums from crews import models @@ -196,9 +197,8 @@ def validate_applicant_boj_level(self, applicant: User, raises_exception=False) def _validate_applicant_boj_level(self, applicant: User): if self.instance.min_boj_level is None: return - if applicant.boj_level is None: - raise exceptions.ValidationError('사용자의 백준 레벨을 가져올 수 없습니다.') - if applicant.boj_level < self.instance.min_boj_level: + service = get_boj_user_service(applicant.boj_username) + if service.instance.level < self.instance.min_boj_level: raise exceptions.ValidationError('최소 백준 레벨 요구조건을 달성하지 못하였습니다.') def tags(self) -> List[dto.CrewTag]: From 87dacac20cbde080992516c5d47e6923f935e5bd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 26 Aug 2024 23:57:58 +0900 Subject: [PATCH 416/552] =?UTF-8?q?feat(crews.views):=20=ED=81=AC=EB=A3=A8?= =?UTF-8?q?=EC=97=90=20=EB=8F=84=EC=B0=A9=ED=95=9C=20=EC=8B=A0=EC=B2=AD=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20API=EC=9D=98=20Mock=20=EB=B2=84=EC=A0=84?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 6 ++- app/crews/views/__init__.py | 1 + app/crews/views/crew_views.py | 71 +++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 2 deletions(-) diff --git a/app/config/urls.py b/app/config/urls.py index 3720657..3d4c357 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -39,8 +39,10 @@ path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), - path("crew/applications//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), - path("crew/applications//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), + path("crew//applications", crews.views.CrewApplicationsListAPIView.as_view()), + path("crew/applications/my", crews.views.CrewApplicantionRejectAPIView.as_view()), + path("crew/application//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), + path("crew/application//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), path("problems", problems.views.ProblemSearchListAPIView.as_view()), path("problem", problems.views.ProblemCreateAPIView.as_view()), diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py index c833409..83e787b 100644 --- a/app/crews/views/__init__.py +++ b/app/crews/views/__init__.py @@ -3,6 +3,7 @@ from crews.views.crew_views import RecruitingCrewListAPIView from crews.views.crew_views import CrewStatisticsAPIView from crews.views.crew_views import CrewDashboardAPIView +from crews.views.crew_views import CrewApplicationsListAPIView from crews.views.crew_applicantions_views import CrewApplicantionCreateAPIView from crews.views.crew_applicantions_views import CrewApplicantionAcceptAPIView from crews.views.crew_applicantions_views import CrewApplicantionRejectAPIView diff --git a/app/crews/views/crew_views.py b/app/crews/views/crew_views.py index da37c8a..31ca4da 100644 --- a/app/crews/views/crew_views.py +++ b/app/crews/views/crew_views.py @@ -72,3 +72,74 @@ def get_object(self) -> dto.ProblemStatistic: crew = super().get_object() service = services.CrewService(crew) return service.statistics() + + +class CrewApplicationsListAPIView(generics.ListAPIView): + queryset = models.Crew + permission_classes = [IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.CrewStatisticsSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def get_object(self) -> dto.ProblemStatistic: + crew = super().get_object() + service = services.CrewService(crew) + return service.statistics() + + def list(self, request, *args, **kwargs): + return Response({ + 'count': 2, + 'results': [ + { + "created_at": "2024-08-26T14:34:47", + "message": "저 진짜 열심히 할 자신 어쩌구...", + "is_pending": False, + "is_accepted": True, + "applicant": { + "user_id": 1, + "username": "인간 닉네임", + "profile_image": "https://picsum.photos/250/250", + "boj": { + "level": { + "value": 1, + "name": "골드 4", + } + } + }, + }, + { + "created_at": "2024-08-26T14:34:47", + "message": "저 진짜 열심히 할 자신 어쩌구...", + "is_pending": False, + "is_accepted": False, + "applicant": { + "user_id": 1, + "username": "인간 닉네임", + "profile_image": "https://picsum.photos/250/250", + "boj": { + "level": { + "value": 1, + "name": "골드 4", + } + } + }, + }, + { + "created_at": "2024-08-26T14:34:47", + "message": "저 진짜 열심히 할 자신 어쩌구...", + "is_pending": True, + "is_accepted": False, + "applicant": { + "user_id": 1, + "username": "인간 닉네임", + "profile_image": "https://picsum.photos/250/250", + "boj": { + "level": { + "value": 1, + "name": "골드 4", + } + } + }, + }, + ], + }) From 44d6aae2e70bc35071486bc05f878068bf938d93 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 00:12:30 +0900 Subject: [PATCH 417/552] feat(boj.services): add `BOJUserService.level()` --- app/boj/services.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/boj/services.py b/app/boj/services.py index bf0b937..757c98a 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -36,6 +36,9 @@ def create_snapshot(self) -> models.BOJUserSnapshot: models.BOJUserSnapshot.field_name.CREATED_AT: self.instance.updated_at, }) + def level(self) -> enums.BOJLevel: + return enums.BOJLevel(self.instance.level) + @background def update_boj_user(boj_username: str): From e7f21db03a6231a3155f2c76fb80725c37445b34 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 00:13:44 +0900 Subject: [PATCH 418/552] feat(crews.serializers): add `CrewApplicationApplicantField` --- app/crews/serializers/fields.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index 157d712..859fe34 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from boj.services import get_boj_user_service from crews import dto from crews import models from crews import services @@ -159,3 +160,21 @@ def to_representation(self, statistics: dto.ProblemStatistic): } for tag, count in statistics.tags.items() ] + + +class CrewApplicationApplicantField(serializers.SerializerMethodField): + def to_representation(self, instance: models.CrewApplication): + assert isinstance(instance, models.CrewApplication) + service = get_boj_user_service(instance.applicant.boj_username) + level = service.level() + return { + "user_id": instance.applicant.pk, + "username": instance.applicant.username, + "profile_image": instance.applicant.profile_image, + "boj": { + "level": { + "value": level.value, + "name": level.get_name(lang="ko", arabic=False), + } + } + } From 2504e594e335619c8d2f326da8b1f12ce8f24e70 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 00:28:29 +0900 Subject: [PATCH 419/552] =?UTF-8?q?feat(crews.views):=20=ED=81=AC=EB=A3=A8?= =?UTF-8?q?=EC=97=90=20=EC=A7=80=EC=9B=90=ED=95=9C=20=EC=A7=80=EC=9B=90?= =?UTF-8?q?=EC=9E=90=20=EB=AA=A9=EB=A1=9D=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers/__init__.py | 14 ++-- app/crews/serializers/fields.py | 2 +- app/crews/views/crew_applicantions_views.py | 6 +- app/crews/views/crew_views.py | 74 +++------------------ 4 files changed, 20 insertions(+), 76 deletions(-) diff --git a/app/crews/serializers/__init__.py b/app/crews/serializers/__init__.py index 6ee9773..24bf9e7 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializers/__init__.py @@ -127,24 +127,20 @@ class Meta: read_only_fields = ['__all__'] -class CrewApplicationSerializer(serializers.ModelSerializer): - user = UserMinimalSerializer(read_only=True) +class CrewApplicationAboutApplicantSerializer(serializers.ModelSerializer): + applicant = fields.CrewApplicationApplicantField() class Meta: model = models.CrewApplication fields = [ PK, - models.CrewApplication.field_name.CREW, models.CrewApplication.field_name.MESSAGE, - models.CrewApplication.field_name.APPLICANT, - models.CrewApplication.field_name.IS_ACCEPTED, - models.CrewApplication.field_name.CREATED_AT, - ] - read_only_fields = [ - models.CrewApplication.field_name.CREW, + models.CrewApplication.field_name.IS_PENDING, models.CrewApplication.field_name.IS_ACCEPTED, models.CrewApplication.field_name.CREATED_AT, + 'applicant', ] + read_only_fields = ['__all__'] class CrewApplicationCreateSerializer(serializers.Serializer): diff --git a/app/crews/serializers/fields.py b/app/crews/serializers/fields.py index 859fe34..c665d67 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializers/fields.py @@ -170,7 +170,7 @@ def to_representation(self, instance: models.CrewApplication): return { "user_id": instance.applicant.pk, "username": instance.applicant.username, - "profile_image": instance.applicant.profile_image, + "profile_image": instance.applicant.profile_image.url, "boj": { "level": { "value": level.value, diff --git a/app/crews/views/crew_applicantions_views.py b/app/crews/views/crew_applicantions_views.py index e38779f..36b97fc 100644 --- a/app/crews/views/crew_applicantions_views.py +++ b/app/crews/views/crew_applicantions_views.py @@ -26,7 +26,7 @@ def create(self, request: Request, *args, **kwargs): message=serializer.validated_data['message'], ) # output serializer - serializer = serializers.CrewApplicationSerializer(instance) + serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) headers = self.get_success_headers(serializer.data) return Response( data=serializer.data, @@ -46,7 +46,7 @@ def put(self, request: Request, *args, **kwargs): instance = self.get_object() service = services.CrewApplicantionService(instance) service.accept(reviewed_by=request.user) - serializer = serializers.CrewApplicationSerializer(instance) + serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) return Response(serializer.data) @@ -61,5 +61,5 @@ def put(self, request: Request, *args, **kwargs): instance = self.get_object() service = services.CrewApplicantionService(instance) service.reject(reviewed_by=request.user) - serializer = serializers.CrewApplicationSerializer(instance) + serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) return Response(serializer.data) diff --git a/app/crews/views/crew_views.py b/app/crews/views/crew_views.py index 31ca4da..93478f4 100644 --- a/app/crews/views/crew_views.py +++ b/app/crews/views/crew_views.py @@ -1,3 +1,5 @@ +from django.shortcuts import get_object_or_404 +from rest_framework import exceptions from rest_framework import generics from rest_framework import status from rest_framework.permissions import AllowAny @@ -77,69 +79,15 @@ def get_object(self) -> dto.ProblemStatistic: class CrewApplicationsListAPIView(generics.ListAPIView): queryset = models.Crew permission_classes = [IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.CrewStatisticsSerializer - lookup_field = 'id' + serializer_class = serializers.CrewApplicationAboutApplicantSerializer lookup_url_kwarg = 'crew_id' - def get_object(self) -> dto.ProblemStatistic: - crew = super().get_object() - service = services.CrewService(crew) - return service.statistics() - - def list(self, request, *args, **kwargs): - return Response({ - 'count': 2, - 'results': [ - { - "created_at": "2024-08-26T14:34:47", - "message": "저 진짜 열심히 할 자신 어쩌구...", - "is_pending": False, - "is_accepted": True, - "applicant": { - "user_id": 1, - "username": "인간 닉네임", - "profile_image": "https://picsum.photos/250/250", - "boj": { - "level": { - "value": 1, - "name": "골드 4", - } - } - }, - }, - { - "created_at": "2024-08-26T14:34:47", - "message": "저 진짜 열심히 할 자신 어쩌구...", - "is_pending": False, - "is_accepted": False, - "applicant": { - "user_id": 1, - "username": "인간 닉네임", - "profile_image": "https://picsum.photos/250/250", - "boj": { - "level": { - "value": 1, - "name": "골드 4", - } - } - }, - }, - { - "created_at": "2024-08-26T14:34:47", - "message": "저 진짜 열심히 할 자신 어쩌구...", - "is_pending": True, - "is_accepted": False, - "applicant": { - "user_id": 1, - "username": "인간 닉네임", - "profile_image": "https://picsum.photos/250/250", - "boj": { - "level": { - "value": 1, - "name": "골드 4", - } - } - }, - }, - ], + def get_queryset(self): + crew_id = self.kwargs[self.lookup_url_kwarg] + try: + crew = models.Crew.objects.get(pk=crew_id) + except models.Crew.DoesNotExist: + raise exceptions.NotFound + return models.CrewApplication.objects.filter(**{ + models.CrewApplication.field_name.CREW: crew, }) From f84fb28dd3d668af84e8b46b7a91d4cccf415b97 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 00:32:11 +0900 Subject: [PATCH 420/552] =?UTF-8?q?chore(crews.admin):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EC=A7=80=EC=9B=90=EC=9D=98=20=EA=B2=80=ED=86=A0?= =?UTF-8?q?=EC=A4=91=20=EC=97=AC=EB=B6=80=EB=A5=BC=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=20=EC=B6=9C?= =?UTF-8?q?=EB=A0=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/admin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/crews/admin.py b/app/crews/admin.py index c904526..3acda70 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -140,6 +140,7 @@ class CrewApplicantModelAdmin(admin.ModelAdmin): models.CrewApplication.field_name.CREW, models.CrewApplication.field_name.APPLICANT, models.CrewApplication.field_name.IS_ACCEPTED, + models.CrewApplication.field_name.IS_PENDING, models.CrewApplication.field_name.REVIEWED_BY, ] actions = [ From a0da542dd16d40735f726c57c08a51d789ab2c48 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 01:37:56 +0900 Subject: [PATCH 421/552] feat(problems.dto): add ProblemTagDTO --- app/problems/dto.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/problems/dto.py b/app/problems/dto.py index f0d787f..6bb3b3b 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -21,3 +21,13 @@ class ProblemAnalysisDTO: difficulty: enums.ProblemDifficulty tags: Tuple[str] = field(default_factory=tuple) hints: Tuple[str] = field(default_factory=tuple) + + +@dataclass +class ProblemTagDTO: + key: str + name_ko: str + name_en: str + + def __hash__(self) -> int: + return self.key From 10bbb20853526e3e1b81795c4456611f8e9a1807 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 01:38:11 +0900 Subject: [PATCH 422/552] feat(problems.dto): add ProblemStatisticDTO --- app/problems/dto.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/problems/dto.py b/app/problems/dto.py index 6bb3b3b..ad6bede 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -1,8 +1,12 @@ +from __future__ import annotations + +from collections import Counter from dataclasses import dataclass from dataclasses import field from typing import Tuple from problems import enums +from problems import models @dataclass @@ -31,3 +35,10 @@ class ProblemTagDTO: def __hash__(self) -> int: return self.key + + +@dataclass +class ProblemStatisticDTO: + sample_count: int = field(default=0) + difficulty: Counter[int] = field(default_factory=Counter) + tags: Counter[ProblemTagDTO] = field(default_factory=Counter) From 289d121812825f49ecdc8f2a7e3613e4fd693e7e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 02:01:25 +0900 Subject: [PATCH 423/552] feat(problems.dto): add `id` property to `ProblemDTO` --- app/problems/dto.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/problems/dto.py b/app/problems/dto.py index ad6bede..227a5ba 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -11,6 +11,7 @@ @dataclass class ProblemDTO: + id: int title: str description: str input_description: str @@ -18,6 +19,9 @@ class ProblemDTO: memory_limit: float time_limit: float + def __str__(self) -> str: + return f'' + @dataclass class ProblemAnalysisDTO: From afe45b761b7a35abb6d3cd4f635fa76be74c0a31 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 02:01:40 +0900 Subject: [PATCH 424/552] feat(problems.dto): add models to dtos converters --- app/problems/dto.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/problems/dto.py b/app/problems/dto.py index 227a5ba..5afa6e1 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -22,6 +22,18 @@ class ProblemDTO: def __str__(self) -> str: return f'' + @classmethod + def from_model(self, instance: models.Problem) -> ProblemDTO: + return ProblemDTO( + id=instance.pk, + title=instance.title, + description=instance.description, + input_description=instance.input_description, + output_description=instance.output_description, + memory_limit=instance.memory_limit, + time_limit=instance.time_limit, + ) + @dataclass class ProblemAnalysisDTO: @@ -40,6 +52,14 @@ class ProblemTagDTO: def __hash__(self) -> int: return self.key + @classmethod + def from_model(self, instance: models.ProblemTag) -> ProblemTagDTO: + return ProblemTagDTO( + key=instance.key, + name_ko=instance.name_ko, + name_en=instance.name_en, + ) + @dataclass class ProblemStatisticDTO: From 468bf85e99dc205362de334df2bffad382108860 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 02:02:39 +0900 Subject: [PATCH 425/552] =?UTF-8?q?refactor(problems.services):=20dto?= =?UTF-8?q?=EC=97=90=20=EC=9C=84=EC=9E=84=EB=90=9C=20=EB=AA=A8=EB=8D=B8-DT?= =?UTF-8?q?O=20=EB=B3=80=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/services/concrete.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/problems/services/concrete.py b/app/problems/services/concrete.py index 7450875..8b23825 100644 --- a/app/problems/services/concrete.py +++ b/app/problems/services/concrete.py @@ -79,16 +79,15 @@ def hints(self) -> List[str]: def schedule_analyze(problem_id: int): logger.info(f'PK={problem_id} 문제의 분석 준비중.') problem = get_problem(problem_id) - problem_dto = get_problem_dto(problem) - problem_repr = f'PK={problem_id} ({problem_dto.title})' + problem_dto = dto.ProblemDTO.from_model(problem) logger.info('문제 분석기를 불러오는 중.') analyzer = get_analyzer() - logger.info(f'{problem_repr} 문제의 분석 시작.') + logger.info(f'{problem_dto} 문제의 분석 시작.') analysis_dto = analyzer.analyze(problem_dto) - logger.info(f'{problem_repr} 문제의 분석 완료.') - logger.info(f'{problem_repr} 문제의 분석 결과를 데이터베이스에 저장하는 중.') + logger.info(f'{problem_dto} 문제의 분석 완료.') + logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장하는 중.') save_analysis(problem, analysis_dto) - logger.info(f'{problem_repr} 문제의 분석 결과를 데이터베이스에 저장 완료.') + logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장 완료.') def get_analyzer() -> ProblemAnalyzer: @@ -102,17 +101,6 @@ def get_problem(problem_id: int) -> models.Problem: logger.warning(f'id가 {problem_id}인 문제를 찾을 수 없습니다.') -def get_problem_dto(problem: models.Problem) -> dto.ProblemDTO: - return dto.ProblemDTO( - title=problem.title, - description=problem.description, - input_description=problem.input_description, - output_description=problem.output_description, - memory_limit=problem.memory_limit, - time_limit=problem.time_limit, - ) - - def save_analysis(problem: models.Problem, analysis_dto: dto.ProblemAnalysisDTO) -> models.ProblemAnalysis: analysis = models.ProblemAnalysis(**{ models.ProblemAnalysis.field_name.PROBLEM: problem, From a7309c6e0eb9f37be14e9d1de52aef0c2611c1f1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 02:02:59 +0900 Subject: [PATCH 426/552] =?UTF-8?q?refactor(problems.services):=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=AA=A9=EB=A1=9D=EC=9D=84=20=EA=B1=B4?= =?UTF-8?q?=EB=84=A4=EB=A9=B4=20=ED=83=9C=EA=B7=B8=20=EB=B6=84=ED=8F=AC?= =?UTF-8?q?=EC=99=80=20=EB=82=9C=EC=9D=B4=EB=8F=84=20=EB=B6=84=ED=8F=AC?= =?UTF-8?q?=EB=A5=BC=20=EB=B6=84=EC=84=9D=ED=95=B4=EC=A3=BC=EB=8A=94=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/services/__init__.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py index e119e9b..a7dddf2 100644 --- a/app/problems/services/__init__.py +++ b/app/problems/services/__init__.py @@ -1,3 +1,6 @@ +from django.db.models import QuerySet + +from problems import dto from problems import models from problems.services.base import ProblemService from problems.services.concrete import ConcreteProblemService @@ -5,3 +8,14 @@ def get_problem_service(problem: models.Problem) -> ProblemService: return ConcreteProblemService(problem) + + +def get_problems_statistics(queryset: QuerySet[models.Problem]) -> dto.ProblemStatisticDTO: + stat = dto.ProblemStatisticDTO() + for problem in queryset: + service = get_problem_service(problem) + for tag in service.query_tags(): + stat.tags[dto.ProblemTagDTO.from_model(tag)] += 1 + stat.difficulty[service.difficulty()] += 1 + stat.sample_count += 1 + return stat From ffadb7e35c5f555a090b908f0f2e6788081f6807 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 03:50:42 +0900 Subject: [PATCH 427/552] =?UTF-8?q?fix(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=B9=84=EB=B0=80=EB=B2=88=ED=98=B8=20=EC=97=85?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=8A=B8=EC=8B=9C=20=EC=95=94=ED=98=B8?= =?UTF-8?q?=ED=99=94=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=EC=9D=B4=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9=20=EC=95=88=EB=90=98=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/users/serializers.py b/app/users/serializers.py index 2a71715..f3a4038 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -98,6 +98,12 @@ class Meta: } } + def save(self, **kwargs): + instance: User = super().save(**kwargs) + if User.field_name.PASSWORD in self.validated_data: + instance.set_password(self.validated_data[User.field_name.PASSWORD]) + instance.save() + class UserMinimalSerializer(serializers.ModelSerializer): class Meta: From 754b31183173b55f8134d85711b65f65de80fffe Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 04:16:38 +0900 Subject: [PATCH 428/552] =?UTF-8?q?fix(users.views):=20signin=20api?= =?UTF-8?q?=EC=9D=98=20method=EA=B0=80=20post=EC=97=90=EC=84=9C=20get?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B0=94=EB=80=8C=EC=96=B4=20=EB=B2=84?= =?UTF-8?q?=EB=A6=B0=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/app/users/views.py b/app/users/views.py index 3a22195..2c06660 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -11,26 +11,24 @@ from users import services -class SignInAPIView(generics.RetrieveAPIView): +class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] permission_classes = [permissions.AllowAny] serializer_class = serializers.SignInSerializer - def get_object(self) -> models.User: - serializer = self.get_serializer(data=self.request.data) + def post(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - return services.sign_in( + user = services.sign_in( request=self.request, email=serializer.validated_data['email'], password=serializer.validated_data['password'], ) - - def retrieve(self, request: Request, *args, **kwargs): - instance = self.get_object() - token = services.get_user_jwt(instance) + token = services.get_user_jwt(user) + serializer = serializers.UserSerializer(instance=user) return Response({ - **serializers.UserSerializer(instance).data, + **serializer.data, 'access_token': str(token.access_token), 'refresh_token': str(token.token), }) From 4bff0197019576817dc9bd66c3acb04fcc311ee6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 05:07:24 +0900 Subject: [PATCH 429/552] feat(problems.serializers): add ProblemStatisticsSerializer --- app/problems/serializers/__init__.py | 10 +++++++ app/problems/serializers/fields.py | 40 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py index 774aa64..676f2c2 100644 --- a/app/problems/serializers/__init__.py +++ b/app/problems/serializers/__init__.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from problems import dto from problems import models from problems.serializers import fields from users.serializers import UserMinimalSerializer @@ -75,3 +76,12 @@ class Meta: models.Problem.field_name.UPDATED_AT, ] read_only_fields = ['__all__'] + + +class ProblemStatisticSerializer(serializers.Serializer): + difficulty = fields.ProblemStatisticsDifficultyField() + tags = fields.ProblemStatisticsTagsField() + + def __init__(self, instance: dto.ProblemStatisticDTO, **kwargs): + assert isinstance(instance, dto.ProblemStatisticDTO) + super().__init__(instance, **kwargs) diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py index 48dabe9..23c5b83 100644 --- a/app/problems/serializers/fields.py +++ b/app/problems/serializers/fields.py @@ -1,5 +1,6 @@ from rest_framework import serializers +from problems import dto from problems import enums from problems import models from problems import services @@ -66,3 +67,42 @@ def to_representation(self, problem: models.Problem): ], 'is_analyzed': service.is_analyzed(), } + + +class ProblemStatisticsDifficultyField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatisticDTO): + assert isinstance(statistics, dto.ProblemStatisticDTO) + try: + ratio_denominator = 1 / statistics.sample_count + except ZeroDivisionError: + ratio_denominator = 0 + finally: + return [ + { + 'difficulty': difficulty, + 'problem_count': count, + 'ratio': count * ratio_denominator, + } + for difficulty, count in statistics.difficulty.items() + ] + + +class ProblemStatisticsTagsField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatisticDTO): + assert isinstance(statistics, dto.ProblemStatisticDTO) + try: + ratio_denominator = 1 / statistics.sample_count + except ZeroDivisionError: + ratio_denominator = 0 + finally: + return [ + { + 'label': { + 'ko': tag.name_ko, + 'en': tag.name_en, + }, + 'problem_count': count, + 'ratio': count * ratio_denominator, + } + for tag, count in statistics.tags.items() + ] From 421f8385606d7d4c0393b38016de64c40c331499 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 05:10:25 +0900 Subject: [PATCH 430/552] refactor(crews.dto): rename dto/__init__.py -> dto.py --- app/crews/{dto/__init__.py => dto.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/crews/{dto/__init__.py => dto.py} (100%) diff --git a/app/crews/dto/__init__.py b/app/crews/dto.py similarity index 100% rename from app/crews/dto/__init__.py rename to app/crews/dto.py From ec79aca6feaebe5d412ac504855c629b98419a62 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 05:16:12 +0900 Subject: [PATCH 431/552] =?UTF-8?q?feat(crews.services):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4=EC=9D=98=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/services/__init__.py | 26 ++++++- app/crews/services/base.py | 127 +++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 app/crews/services/base.py diff --git a/app/crews/services/__init__.py b/app/crews/services/__init__.py index 99f93da..2d00252 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/services/__init__.py @@ -1,3 +1,23 @@ -from crews.services.crew_service import CrewService -from crews.services.crew_activity_service import CrewActivityService -from crews.services.crew_application_service import CrewApplicantionService +from crews.services.base import UserCrewService +from crews.services.base import CrewService +from crews.services.base import CrewActivityService +from crews.services.base import CrewApplicantionService + +from crews import models +from users.models import User + + +def get_user_crew_service(user: User) -> UserCrewService: + return UserCrewService(user) + + +def get_crew_service(crew: models.Crew) -> CrewService: + return CrewService(crew) + + +def get_crew_activity_service(crew_activity: models.CrewActivity) -> CrewActivityService: + return CrewActivityService(crew_activity) + + +def get_crew_application_service(crew_application: models.CrewApplication) -> CrewApplicantionService: + return CrewApplicantionService(crew_application) diff --git a/app/crews/services/base.py b/app/crews/services/base.py new file mode 100644 index 0000000..bf9ac21 --- /dev/null +++ b/app/crews/services/base.py @@ -0,0 +1,127 @@ +from typing import List + +from django.db.models import QuerySet + +from boj.enums import BOJLevel +from crews import dto +from crews import enums +from crews import models +from problems.dto import ProblemStatisticDTO +from problems.models import Problem +from users.models import User + + +class UserCrewService: + def __init__(self, instance: User) -> None: + assert isinstance(instance, User) + self.instance = instance + + def query_crews_joined(self) -> QuerySet[models.Crew]: + """자신이 멤버로 있는 크루를 조회하는 쿼리를 반환""" + ... + + def query_crews_recruiting(self) -> QuerySet[models.Crew]: + """자신이 멤버로 있지 않으면서 크루원을 모집중인 크루를 조회하는 쿼리를 반환""" + ... + + +class CrewService: + def __init__(self, instance: models.Crew) -> None: + assert isinstance(instance, models.Crew) + self.instance = instance + + def query_members(self) -> QuerySet[models.CrewMember]: + ... + + def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: + ... + + def query_applications(self) -> QuerySet[models.CrewApplication]: + ... + + def query_activities(self) -> QuerySet[models.CrewActivity]: + ... + + def query_activities_published(self) -> QuerySet[models.CrewActivity]: + """크루원들에게 공개된 활동들""" + ... + + def query_problems(self) -> QuerySet[Problem]: + ... + + def query_captain(self) -> QuerySet[models.CrewMember]: + ... + + def statistics(self) -> ProblemStatisticDTO: + """대쉬보드에 사용되는 크루가 풀이해온 문제 통계""" + ... + + def display_name(self) -> str: + ... + + def tags(self) -> List[dto.CrewTag]: + ... + + def languages(self) -> List[enums.ProgrammingLanguageChoices]: + ... + + def min_boj_level(self) -> BOJLevel: + ... + + def is_captain(self, user: User) -> bool: + ... + + def is_member(self, user: User) -> bool: + ... + + def set_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> None: + ... + + def apply(self, applicant: User, message: str) -> models.CrewApplication: + """지원자가 자격요건을 갖추지 못했다면 ValidationError를 발생시킬 수 있다.""" + ... + + +class CrewActivityService: + def __init__(self, instance: models.CrewActivity) -> None: + assert isinstance(instance, models.CrewActivity) + self.instance = instance + + def query_previous_activities(self) -> QuerySet[models.CrewActivity]: + ... + + def nth(self) -> int: + """활동의 회차 번호를 반환합니다. + + 이 값은 1부터 시작합니다. + 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 + 더한 값을 반환하므로, 고정된 값이 아닙니다. + + 느린 연산입니다. + 한 번에 여러 회차 번호들을 조회하기 위해 이 함수를 사용하는 것은 권장하지 않습니다. + """ + ... + + def is_in_progress(self) -> bool: + """활동이 진행 중인지 여부를 반환합니다.""" + ... + + def has_started(self) -> bool: + """활동이 열린적이 있는지 여부를 반환합니다..""" + ... + + def has_ended(self) -> bool: + """활동이 종료되었는지 여부를 반환합니다.""" + ... + + +class CrewApplicantionService: + def __init__(self, instance: models.CrewApplication): + assert isinstance(instance, models.CrewApplication) + self.instance = instance + + def reject(self, reviewed_by: User): + ... + + def accept(self, reviewed_by: User): + ... From f80bdb87584302ec0f9fa30f1eeb5e4f4147194e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 05:22:24 +0900 Subject: [PATCH 432/552] =?UTF-8?q?refactor(crews.permissions):=20permissi?= =?UTF-8?q?ons.py=EC=97=90=20DRF=EC=97=90=EC=84=9C=20=EC=A0=9C=EA=B3=B5?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B6=8C=ED=95=9C=EB=8F=84=20=ED=8F=AC?= =?UTF-8?q?=ED=95=A8=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/permissions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/crews/permissions.py b/app/crews/permissions.py index 403d9e0..afb8877 100644 --- a/app/crews/permissions.py +++ b/app/crews/permissions.py @@ -1,14 +1,16 @@ from typing import Union from rest_framework import exceptions -from rest_framework import permissions +from rest_framework.permissions import BasePermission +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from crews import models from crews import services -class IsJoinable(permissions.BasePermission): +class IsJoinable(BasePermission): def has_object_permission(self, request: Request, view, crew: models.Crew): assert isinstance(crew, models.Crew) service = services.CrewService(crew) @@ -20,7 +22,7 @@ def has_object_permission(self, request: Request, view, crew: models.Crew): return True -class IsMember(permissions.BasePermission): +class IsMember(BasePermission): def has_object_permission(self, request: Request, view, obj: Union[models.Crew, models.CrewActivity]): if isinstance(obj, models.Crew): crew = obj @@ -34,7 +36,7 @@ def has_object_permission(self, request: Request, view, obj: Union[models.Crew, return True -class IsCaptain(permissions.BasePermission): +class IsCaptain(BasePermission): def has_object_permission(self, request: Request, view, application: models.CrewApplication) -> bool: assert isinstance(application, models.CrewApplication) service = services.CrewService(application.crew) From b3aaa730a2495b74ef038271ab14120ba36b7df8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 07:56:11 +0900 Subject: [PATCH 433/552] =?UTF-8?q?refactor(crews.views):=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EB=A1=9C=20=EB=82=98=EB=88=84=EC=97=88=EB=8D=98=20vie?= =?UTF-8?q?ws=20=EB=AA=A8=EB=93=88=EC=9D=84=20=ED=95=9C=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=EB=B3=B5=EA=B5=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 4 +- app/crews/views.py | 156 ++++++++++++++++++++ app/crews/views/__init__.py | 10 -- app/crews/views/crew_activity_views.py | 15 -- app/crews/views/crew_applicantions_views.py | 65 -------- app/crews/views/crew_views.py | 93 ------------ 6 files changed, 158 insertions(+), 185 deletions(-) create mode 100644 app/crews/views.py delete mode 100644 app/crews/views/__init__.py delete mode 100644 app/crews/views/crew_activity_views.py delete mode 100644 app/crews/views/crew_applicantions_views.py delete mode 100644 app/crews/views/crew_views.py diff --git a/app/config/urls.py b/app/config/urls.py index 3d4c357..ad24571 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -39,11 +39,11 @@ path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), - path("crew//applications", crews.views.CrewApplicationsListAPIView.as_view()), + path("crew//applications", crews.views.CrewApplicationListAPIView.as_view()), path("crew/applications/my", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/application//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), path("crew/application//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), - path("crew/activities//dashboard", crews.views.CrewActivityRetrieveAPIView.as_view()), + path("crew/activities//dashboard", crews.views.CrewDashboardActivityAPIView.as_view()), path("problems", problems.views.ProblemSearchListAPIView.as_view()), path("problem", problems.views.ProblemCreateAPIView.as_view()), path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), diff --git a/app/crews/views.py b/app/crews/views.py new file mode 100644 index 0000000..99d6b0b --- /dev/null +++ b/app/crews/views.py @@ -0,0 +1,156 @@ +from django.http.request import HttpRequest +from rest_framework import generics +from rest_framework import status +from rest_framework.response import Response + +from crews import models +from crews import permissions +from crews import serializers +from crews import services +from problems.dto import ProblemStatisticDTO +from problems.serializers import ProblemStatisticSerializer + + +# Crew List API Views + +class RecruitingCrewListAPIView(generics.ListAPIView): + """크루 목록""" + permission_classes = [permissions.AllowAny] + serializer_class = serializers.RecruitingCrewSerializer + + def get_queryset(self): + service = services.get_user_crew_service(self.request.user) + return service.query_crews_recruiting() + + +class MyCrewListAPIView(generics.ListAPIView): + """나의 참여 크루""" + permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + serializer_class = serializers.MyCrewSerializer + + def get_queryset(self): + service = services.get_user_crew_service(self.request.user) + return service.query_crews_joined() + + +# Crew API Views + +class CrewCreateAPIView(generics.CreateAPIView): + """크루 생성 API""" + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated] + serializer_class = serializers.CrewCreateSerializer + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + instance = self.perform_create(serializer) + serializer = serializers.MyCrewSerializer(instance=instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + +class CrewDashboardAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 API""" + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + serializer_class = serializers.CrewDashboardSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + +class CrewStatisticsAPIView(generics.RetrieveAPIView): + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + serializer_class = ProblemStatisticSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def get_object(self) -> ProblemStatisticDTO: + instance = super().get_object() + service = services.get_crew_service(instance) + return service.statistics() + + +# Crew Activity API Views + +class CrewDashboardActivityAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 - 회차 별 API""" + queryset = models.CrewActivity + permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + serializer_class = serializers.CrewActivityDashboardSerializer + lookup_field = 'id' + lookup_url_kwarg = 'activity_id' + + +# Crew Application API Views + +class CrewApplicationListAPIView(generics.RetrieveAPIView): + """[크루/관리/크루 멤버 관리] 크루 가입 신청 현황 API""" + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.CrewApplicationAboutApplicantSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def retrieve(self, request, *args, **kwargs): + instance = self.get_object() + service = services.get_crew_service(instance) + queryset = service.query_applications() + page = self.paginate_queryset(queryset) + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + +class CrewApplicantionCreateAPIView(generics.CreateAPIView): + """크루 가입 신청 API""" + queryset = models.Crew + permission_classes = [permissions.IsAuthenticated & permissions.IsJoinable] + serializer_class = serializers.CrewApplicationCreateSerializer + lookup_field = 'id' + lookup_url_kwarg = 'crew_id' + + def create(self, request: HttpRequest, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + serializer = serializers.CrewApplicationSerializer(serializer.instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer: serializers.CrewApplicationCreateSerializer): + message = serializer.validated_data['message'] + service = services.get_crew_service(crew=self.get_object()) + serializer.instance = service.apply(self.request.user, message) + + +class CrewApplicantionAcceptAPIView(generics.GenericAPIView): + """크루 가입 수락 API""" + queryset = models.CrewApplication + permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'application_id' + + def put(self, request: HttpRequest, *args, **kwargs): + instance = self.get_object() + service = services.get_crew_application_service(instance) + service.accept(reviewed_by=request.user) + serializer = serializers.CrewApplicationSerializer(instance) + return Response(serializer.data) + + +class CrewApplicantionRejectAPIView(generics.GenericAPIView): + """크루 가입 거부 API""" + queryset = models.CrewApplication + permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'application_id' + + def put(self, request: HttpRequest, *args, **kwargs): + instance = self.get_object() + service = services.get_crew_application_service(instance) + service.reject(reviewed_by=request.user) + serializer = serializers.CrewApplicationSerializer(instance) + return Response(serializer.data) diff --git a/app/crews/views/__init__.py b/app/crews/views/__init__.py deleted file mode 100644 index 83e787b..0000000 --- a/app/crews/views/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -from crews.views.crew_views import CrewCreateAPIView -from crews.views.crew_views import MyCrewListAPIView -from crews.views.crew_views import RecruitingCrewListAPIView -from crews.views.crew_views import CrewStatisticsAPIView -from crews.views.crew_views import CrewDashboardAPIView -from crews.views.crew_views import CrewApplicationsListAPIView -from crews.views.crew_applicantions_views import CrewApplicantionCreateAPIView -from crews.views.crew_applicantions_views import CrewApplicantionAcceptAPIView -from crews.views.crew_applicantions_views import CrewApplicantionRejectAPIView -from crews.views.crew_activity_views import CrewActivityRetrieveAPIView diff --git a/app/crews/views/crew_activity_views.py b/app/crews/views/crew_activity_views.py deleted file mode 100644 index 70e669b..0000000 --- a/app/crews/views/crew_activity_views.py +++ /dev/null @@ -1,15 +0,0 @@ -from rest_framework import generics -from rest_framework.permissions import IsAuthenticated - -from crews import models -from crews import permissions -from crews import serializers - - -class CrewActivityRetrieveAPIView(generics.RetrieveAPIView): - """크루 대시보드 홈 - 회차 API""" - queryset = models.CrewActivity - permission_classes = [IsAuthenticated & permissions.IsMember] - serializer_class = serializers.CrewActivityDashboardSerializer - lookup_field = 'id' - lookup_url_kwarg = 'activity_id' diff --git a/app/crews/views/crew_applicantions_views.py b/app/crews/views/crew_applicantions_views.py deleted file mode 100644 index 36b97fc..0000000 --- a/app/crews/views/crew_applicantions_views.py +++ /dev/null @@ -1,65 +0,0 @@ -from rest_framework import generics -from rest_framework import status -from rest_framework.permissions import IsAuthenticated -from rest_framework.request import Request -from rest_framework.response import Response - -from crews import models -from crews import permissions -from crews import serializers -from crews import services - - -class CrewApplicantionCreateAPIView(generics.CreateAPIView): - queryset = models.Crew - permission_classes = [IsAuthenticated & permissions.IsJoinable] - serializer_class = serializers.CrewApplicationCreateSerializer - lookup_field = 'id' - lookup_url_kwarg = 'crew_id' - - def create(self, request: Request, *args, **kwargs): - # input serializer - serializer = self.get_serializer(data=request.data) - instance = services.CrewApplicantionService.create( - crew=self.get_object(), - user=request.user, - message=serializer.validated_data['message'], - ) - # output serializer - serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) - headers = self.get_success_headers(serializer.data) - return Response( - data=serializer.data, - status=status.HTTP_201_CREATED, - headers=headers, - ) - - -class CrewApplicantionAcceptAPIView(generics.GenericAPIView): - queryset = models.CrewApplication - permission_classes = [IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.NoInputSerializer - lookup_field = 'id' - lookup_url_kwarg = 'application_id' - - def put(self, request: Request, *args, **kwargs): - instance = self.get_object() - service = services.CrewApplicantionService(instance) - service.accept(reviewed_by=request.user) - serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) - return Response(serializer.data) - - -class CrewApplicantionRejectAPIView(generics.GenericAPIView): - queryset = models.CrewApplication - permission_classes = [IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.NoInputSerializer - lookup_field = 'id' - lookup_url_kwarg = 'application_id' - - def put(self, request: Request, *args, **kwargs): - instance = self.get_object() - service = services.CrewApplicantionService(instance) - service.reject(reviewed_by=request.user) - serializer = serializers.CrewApplicationAboutApplicantSerializer(instance) - return Response(serializer.data) diff --git a/app/crews/views/crew_views.py b/app/crews/views/crew_views.py deleted file mode 100644 index 93478f4..0000000 --- a/app/crews/views/crew_views.py +++ /dev/null @@ -1,93 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework import exceptions -from rest_framework import generics -from rest_framework import status -from rest_framework.permissions import AllowAny -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response - -from crews import dto -from crews import models -from crews import permissions -from crews import serializers -from crews import services - - -class CrewCreateAPIView(generics.CreateAPIView): - """크루 생성 API""" - queryset = models.Crew.objects.all() - permission_classes = [IsAuthenticated] - serializer_class = serializers.CrewCreateSerializer - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = self.perform_create(serializer) - serializer = serializers.MyCrewSerializer(instance=instance) - return Response( - data=serializer.data, - status=status.HTTP_201_CREATED, - headers=self.get_success_headers(serializer.data), - ) - - -class RecruitingCrewListAPIView(generics.ListAPIView): - """크루 목록""" - permission_classes = [AllowAny] - serializer_class = serializers.RecruitingCrewSerializer - - def get_queryset(self): - return services.CrewService.query_recruiting(self.request.user) - - -class MyCrewListAPIView(generics.ListAPIView): - """나의 참여 크루""" - permission_classes = [IsAuthenticated & permissions.IsMember] - serializer_class = serializers.MyCrewSerializer - - def get_queryset(self): - queryset = services.CrewService.query_as_member(self.request.user) - # 활동 종료된 크루는 뒤로 가도록 정렬 - return queryset.order_by('-'+models.Crew.field_name.IS_ACTIVE) - - -class CrewDashboardAPIView(generics.RetrieveAPIView): - """크루 대시보드 홈 API""" - queryset = models.Crew - permission_classes = [IsAuthenticated & permissions.IsMember] - serializer_class = serializers.CrewDashboardSerializer - lookup_field = 'id' - lookup_url_kwarg = 'crew_id' - - def get_queryset(self): - return services.CrewService.query_as_member(self.request.user) - - -class CrewStatisticsAPIView(generics.RetrieveAPIView): - queryset = models.Crew - permission_classes = [IsAuthenticated & permissions.IsMember] - serializer_class = serializers.CrewStatisticsSerializer - lookup_field = 'id' - lookup_url_kwarg = 'crew_id' - - def get_object(self) -> dto.ProblemStatistic: - crew = super().get_object() - service = services.CrewService(crew) - return service.statistics() - - -class CrewApplicationsListAPIView(generics.ListAPIView): - queryset = models.Crew - permission_classes = [IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.CrewApplicationAboutApplicantSerializer - lookup_url_kwarg = 'crew_id' - - def get_queryset(self): - crew_id = self.kwargs[self.lookup_url_kwarg] - try: - crew = models.Crew.objects.get(pk=crew_id) - except models.Crew.DoesNotExist: - raise exceptions.NotFound - return models.CrewApplication.objects.filter(**{ - models.CrewApplication.field_name.CREW: crew, - }) From 2988fc9a0ffddb7ff25a12ee18b1496e32ca16c7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 23:52:44 +0900 Subject: [PATCH 434/552] =?UTF-8?q?refactor(problems.urls):=20problems=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20url=EB=93=A4=EC=9D=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 6 ++---- app/problems/urls.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 app/problems/urls.py diff --git a/app/config/urls.py b/app/config/urls.py index ad24571..85df618 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -7,7 +7,7 @@ from rest_framework import permissions import crews.views -import problems.views +import problems.urls import users.views import submissions.views @@ -44,9 +44,7 @@ path("crew/application//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), path("crew/application//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), path("crew/activities//dashboard", crews.views.CrewDashboardActivityAPIView.as_view()), - path("problems", problems.views.ProblemSearchListAPIView.as_view()), - path("problem", problems.views.ProblemCreateAPIView.as_view()), - path("problem//detail", problems.views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), + *problems.urls.urlpatterns, path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), diff --git a/app/problems/urls.py b/app/problems/urls.py new file mode 100644 index 0000000..d5f3458 --- /dev/null +++ b/app/problems/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from problems import views + + +urlpatterns = [ + path("problems", views.ProblemSearchListAPIView.as_view()), + path("problem", views.ProblemCreateAPIView.as_view()), + path("problem//detail", views.ProblemDetailRetrieveUpdateDestroyAPIView.as_view()), +] From d931380b6366b83f404889251a84616eff27dfab Mon Sep 17 00:00:00 2001 From: Hepheir Date: Tue, 27 Aug 2024 23:55:41 +0900 Subject: [PATCH 435/552] =?UTF-8?q?refactor(crews.urls):=20crews=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20url=EB=93=A4=EC=9D=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 14 ++------------ app/crews/urls.py | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 app/crews/urls.py diff --git a/app/config/urls.py b/app/config/urls.py index 85df618..89fe547 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -6,7 +6,7 @@ from drf_yasg.views import get_schema_view from rest_framework import permissions -import crews.views +import crews.urls import problems.urls import users.views import submissions.views @@ -33,17 +33,7 @@ path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), path("auth/email/check", users.views.EmailCheckAPIView.as_view()), path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), - path("crews/my", crews.views.MyCrewListAPIView.as_view()), - path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), - path("crew", crews.views.CrewCreateAPIView.as_view()), - path("crew//dashboard", crews.views.CrewDashboardAPIView.as_view()), - path("crew//statistics", crews.views.CrewStatisticsAPIView.as_view()), - path("crew//apply", crews.views.CrewApplicantionCreateAPIView.as_view()), - path("crew//applications", crews.views.CrewApplicationListAPIView.as_view()), - path("crew/applications/my", crews.views.CrewApplicantionRejectAPIView.as_view()), - path("crew/application//accept", crews.views.CrewApplicantionAcceptAPIView.as_view()), - path("crew/application//reject", crews.views.CrewApplicantionRejectAPIView.as_view()), - path("crew/activities//dashboard", crews.views.CrewDashboardActivityAPIView.as_view()), + *crews.urls.urlpatterns, *problems.urls.urlpatterns, path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), path("submissions/", submissions.views.CreateCodeReview.as_view()), diff --git a/app/crews/urls.py b/app/crews/urls.py new file mode 100644 index 0000000..d4eaf11 --- /dev/null +++ b/app/crews/urls.py @@ -0,0 +1,25 @@ +from django.urls import include +from django.urls import path + +from crews import views + + +urlpatterns = [ + path("crews/my", views.MyCrewListAPIView.as_view()), + path("crews/recruiting", views.RecruitingCrewListAPIView.as_view()), + path("crew", views.CrewCreateAPIView.as_view()), + path("crew/", include([ + path("/dashboard", views.CrewDashboardAPIView.as_view()), + path("/statistics", views.CrewStatisticsAPIView.as_view()), + path("/applications", views.CrewApplicationListAPIView.as_view()), + path("/apply", views.CrewApplicantionCreateAPIView.as_view()), + ])), + path("crew/applications/my", views.CrewApplicantionRejectAPIView.as_view()), + path("crew/application/", include([ + path("/accept", views.CrewApplicantionAcceptAPIView.as_view()), + path("/reject", views.CrewApplicantionRejectAPIView.as_view()), + ])), + path("crew/activities/", include([ + path("/dashboard", views.CrewDashboardActivityAPIView.as_view()), + ])), +] From e1aeb3abe5594e7c920fb1ae3e67a66df30cc4db Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 17:58:09 +0900 Subject: [PATCH 436/552] =?UTF-8?q?refactor(crews.enums):=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=EC=97=90=EC=84=9C=20=EB=8B=A8=EC=9D=BC=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/{enums/emoji.py => enums.py} | 29 ++++++++++++++++++++++++-- app/crews/enums/__init__.py | 28 ------------------------- 2 files changed, 27 insertions(+), 30 deletions(-) rename app/crews/{enums/emoji.py => enums.py} (99%) delete mode 100644 app/crews/enums/__init__.py diff --git a/app/crews/enums/emoji.py b/app/crews/enums.py similarity index 99% rename from app/crews/enums/emoji.py rename to app/crews/enums.py index fb25cd5..b61e32a 100644 --- a/app/crews/enums/emoji.py +++ b/app/crews/enums.py @@ -1,7 +1,32 @@ -from django.db.models import TextChoices +from enum import Enum +from django.db import models -class EmojiChoices(TextChoices): + +class CrewTagType(Enum): + LANGUAGE = 'language' + LEVEL = 'level' + CUSTOM = 'custom' + + +class ProgrammingLanguageChoices(models.TextChoices): + # TLE에서 허용중인 언어 + NODE_JS = 'nodejs', 'Node.js' + KOTLIN = 'kotlin', 'Kotlin' + SWIFT = 'swift', 'Swift' + CPP = 'cpp', 'C++' + JAVA = 'java', 'Java' + PYTHON = 'python', 'Python' + C = 'c', 'C' + JAVASCRIPT = 'javascript', 'JavaScript' + + # 아직 지원하지 않는 언어 + CSHARP = 'csharp', 'C#' + RUBY = 'ruby', 'Ruby' + PHP = 'php', 'PHP' + + +class EmojiChoices(models.TextChoices): U1F947 = "🥇", "🥇 (:1st_place_medal:)" U1F948 = "🥈", "🥈 (:2nd_place_medal:)" U1F949 = "🥉", "🥉 (:3rd_place_medal:)" diff --git a/app/crews/enums/__init__.py b/app/crews/enums/__init__.py deleted file mode 100644 index 84a8b2e..0000000 --- a/app/crews/enums/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -from enum import Enum - -from django.db import models - -from crews.enums.emoji import EmojiChoices - - -class CrewTagType(Enum): - LANGUAGE = 'language' - LEVEL = 'level' - CUSTOM = 'custom' - - -class ProgrammingLanguageChoices(models.TextChoices): - # TLE에서 허용중인 언어 - NODE_JS = 'nodejs', 'Node.js' - KOTLIN = 'kotlin', 'Kotlin' - SWIFT = 'swift', 'Swift' - CPP = 'cpp', 'C++' - JAVA = 'java', 'Java' - PYTHON = 'python', 'Python' - C = 'c', 'C' - JAVASCRIPT = 'javascript', 'JavaScript' - - # 아직 지원하지 않는 언어 - CSHARP = 'csharp', 'C#' - RUBY = 'ruby', 'Ruby' - PHP = 'php', 'PHP' From 593c346995e477c505aae992bcbdcfcc79bb69c1 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 19:12:11 +0900 Subject: [PATCH 437/552] feat(boj.models): create BOJUserManager --- app/boj/models.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index fed1250..37f6abc 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -1,8 +1,19 @@ from __future__ import annotations +from typing import Union + from django.db import models -from boj import enums +from boj.enums import BOJLevel +from users.models import User + + +class BOJUserManager(models.BaseManager): + def user(self, user: User) -> _BOJUserManager: + return self.filter(**{BOJUser.field_name.USERNAME: user.boj_username}) + + def username(self, username: str) -> BOJUser: + return self.get_or_create(**{BOJUser.field_name.USERNAME: username})[0] class BOJUser(models.Model): @@ -12,14 +23,16 @@ class BOJUser(models.Model): unique=True, ) level = models.IntegerField( - choices=enums.BOJLevel.choices, - default=enums.BOJLevel.U, + choices=BOJLevel.choices, + default=BOJLevel.U, ) rating = models.IntegerField( default=0, ) updated_at = models.DateTimeField(auto_now_add=True) + objects: _BOJUserManager = BOJUserManager() + class field_name: USERNAME = 'username' LEVEL = 'level' @@ -35,7 +48,7 @@ class BOJUserSnapshot(models.Model): BOJUser, on_delete=models.CASCADE, ) - level = models.IntegerField(choices=enums.BOJLevel.choices) + level = models.IntegerField(choices=BOJLevel.choices) rating = models.IntegerField() created_at = models.DateTimeField() @@ -54,4 +67,7 @@ class BOJProblem(models.Model): memory_limit = models.FloatField() time_limit = models.FloatField() tags = models.JSONField(default=list) - level = models.IntegerField(choices=enums.BOJLevel.choices) + level = models.IntegerField(choices=BOJLevel.choices) + + +_BOJUserManager = Union[BOJUserManager, models.BaseManager[BOJUser]] From ee5377cbf99905b798f57812c3433dc205fca900 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 19:38:16 +0900 Subject: [PATCH 438/552] feat(boj.models): create BOJUserSnapshotManager --- app/boj/models.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/boj/models.py b/app/boj/models.py index 37f6abc..24bbab0 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -43,6 +43,16 @@ def __str__(self) -> str: return f'{self.username}' +class BOJUserSnapshotManager(models.BaseManager): + def create_snapshot_of(self, boj_user: BOJUser) -> BOJUserSnapshot: + return self.create(**{ + BOJUserSnapshot.field_name.USER: boj_user, + BOJUserSnapshot.field_name.LEVEL: boj_user.level, + BOJUserSnapshot.field_name.RATING: boj_user.rating, + BOJUserSnapshot.field_name.CREATED_AT: boj_user.updated_at, + }) + + class BOJUserSnapshot(models.Model): user = models.ForeignKey( BOJUser, @@ -52,6 +62,8 @@ class BOJUserSnapshot(models.Model): rating = models.IntegerField() created_at = models.DateTimeField() + objects: _BOJUserSnapshotManager = BOJUserSnapshotManager() + class field_name: USER = 'user' LEVEL = 'level' @@ -71,3 +83,5 @@ class BOJProblem(models.Model): _BOJUserManager = Union[BOJUserManager, models.BaseManager[BOJUser]] +_BOJUserSnapshotManager = Union[BOJUserSnapshotManager, + models.BaseManager[BOJUserSnapshot]] From e6ac69a7861c3ed543f0d13d35c794a288a22053 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 19:39:01 +0900 Subject: [PATCH 439/552] =?UTF-8?q?refactor(boj.signals):=20services.py?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=9C=EA=B1=B0=ED=95=98=EA=B3=A0=20signals?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=97=94=ED=8B=B0=ED=8B=B0=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=EC=8B=9C=20=EC=9D=B4=EB=B2=A4=ED=8A=B8=EB=A5=BC=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/admin.py | 5 ++- app/boj/apps.py | 2 +- app/boj/dto.py | 9 ----- app/boj/services.py | 66 ------------------------------------- app/boj/signals.py | 43 ++++++++++++++++++++++++ app/boj/signals/__init__.py | 0 app/boj/signals/handlers.py | 17 ---------- 7 files changed, 46 insertions(+), 96 deletions(-) delete mode 100644 app/boj/dto.py delete mode 100644 app/boj/services.py create mode 100644 app/boj/signals.py delete mode 100644 app/boj/signals/__init__.py delete mode 100644 app/boj/signals/handlers.py diff --git a/app/boj/admin.py b/app/boj/admin.py index 39e33c6..a683fe4 100644 --- a/app/boj/admin.py +++ b/app/boj/admin.py @@ -3,7 +3,7 @@ from django.http.request import HttpRequest from boj import models -from boj import services +from boj.signals import update_boj_user_data @admin.register(models.BOJUser) @@ -21,8 +21,7 @@ class BOJUserModelAdmin(admin.ModelAdmin): @admin.action(description="Update selected BOJ user data. (via solved.ac API)") def update(self, request: HttpRequest, queryset: QuerySet[models.BOJUser]): for obj in queryset: - service = services.get_boj_user_service(obj.username) - service.update() + update_boj_user_data(obj.username) @admin.register(models.BOJUserSnapshot) diff --git a/app/boj/apps.py b/app/boj/apps.py index 2e7bd37..cfe94d6 100644 --- a/app/boj/apps.py +++ b/app/boj/apps.py @@ -6,4 +6,4 @@ class BojConfig(AppConfig): name = "boj" def ready(self) -> None: - import boj.signals.handlers + import boj.signals diff --git a/app/boj/dto.py b/app/boj/dto.py deleted file mode 100644 index 784559e..0000000 --- a/app/boj/dto.py +++ /dev/null @@ -1,9 +0,0 @@ -from dataclasses import dataclass - -from boj import enums - - -@dataclass(frozen=True) -class BOJUserData: - level: enums.BOJLevel - rating: int diff --git a/app/boj/services.py b/app/boj/services.py deleted file mode 100644 index 757c98a..0000000 --- a/app/boj/services.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import annotations - -import logging - -from background_task import background -from django.utils import timezone -import requests - -from boj import dto -from boj import enums -from boj import models - - -logger = logging.getLogger('tle.boj') - - -def get_boj_user_service(boj_username: str) -> BOJUserService: - return BOJUserService(boj_username) - - -class BOJUserService: - def __init__(self, username: str): - self.username = username - self.instance, created = models.BOJUser.objects.get_or_create(**{ - models.BOJUser.field_name.USERNAME: username, - }) - - def update(self) -> None: - update_boj_user(self.username) - - def create_snapshot(self) -> models.BOJUserSnapshot: - return models.BOJUserSnapshot(**{ - models.BOJUserSnapshot.field_name.USER: self.instance, - models.BOJUserSnapshot.field_name.LEVEL: self.instance.level, - models.BOJUserSnapshot.field_name.RATING: self.instance.rating, - models.BOJUserSnapshot.field_name.CREATED_AT: self.instance.updated_at, - }) - - def level(self) -> enums.BOJLevel: - return enums.BOJLevel(self.instance.level) - - -@background -def update_boj_user(boj_username: str): - data = fetch_data(boj_username) - service = get_boj_user_service(boj_username) - service.instance.level = data.level.value - service.instance.rating = data.rating - service.instance.updated_at = timezone.now() - service.instance.save() - service.create_snapshot() - - -def fetch_data(username: str) -> dto.BOJUserData: - url = f'https://solved.ac/api/v3/user/show?handle={username}' - data = requests.get(url).json() - try: - tier = data['tier'] - rating = data['rating'] - except AssertionError: - # Solved.ac API 관련 문제일 가능성이 높다. - logger.warning( - '"https://solved.ac/api/v3/user/show"로 부터 데이터를 파싱해오는 것에 실패했습니다.' - ) - else: - return dto.BOJUserData(level=enums.BOJLevel(tier), rating=rating) diff --git a/app/boj/signals.py b/app/boj/signals.py new file mode 100644 index 0000000..0b75609 --- /dev/null +++ b/app/boj/signals.py @@ -0,0 +1,43 @@ +from logging import getLogger + +from background_task import background +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils import timezone +import requests + +from boj.models import BOJUser +from boj.models import BOJUserSnapshot +from users.models import User + + +logger = getLogger('django.server') + + +@receiver(post_save, sender=User) +def auto_create_boj_user(sender, instance: User, created: bool, **kwargs): + if created and not BOJUser.objects.user(instance).exists(): + BOJUser.objects.create( + **{BOJUser.field_name.USERNAME: instance.boj_username}) + + +@receiver(post_save, sender=BOJUser) +def auto_create_boj_user(sender, instance: BOJUser, created: bool, **kwargs): + if created: + update_boj_user_data(instance.username) + + +@background +def update_boj_user_data(username: str): + instance = BOJUser.objects.username(username) + url = f'https://solved.ac/api/v3/user/show?handle={username}' + data = requests.get(url).json() + try: + instance.level = data['tier'] + instance.rating = data['rating'] + instance.updated_at = timezone.now() + instance.save() + BOJUserSnapshot.objects.create_snapshot_of(instance) + except AssertionError: + # Solved.ac API 관련 문제일 가능성이 높다. + logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') diff --git a/app/boj/signals/__init__.py b/app/boj/signals/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/boj/signals/handlers.py b/app/boj/signals/handlers.py deleted file mode 100644 index beb17b9..0000000 --- a/app/boj/signals/handlers.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db.models.signals import pre_save -from django.dispatch import receiver - -from boj import models -from boj import services -from users.models import User - - -@receiver(pre_save, sender=User) -def auto_wire_boj_user(sender, instance: User, **kwargs): - assert instance.boj_username - boj_user, created = models.BOJUser.objects.get_or_create(**{ - models.BOJUser.field_name.USERNAME: instance.boj_username - }) - if created: - service = services.get_boj_user_service(instance.boj_username) - service.update() From a548905998e42531d5aad3ade13d7f61de393879 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 20:28:10 +0900 Subject: [PATCH 440/552] =?UTF-8?q?fix(boj.models):=20Model=EC=9D=98=20Man?= =?UTF-8?q?ager=EC=9D=98=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=9E=84?= =?UTF-8?q?=ED=8F=AC=ED=8A=B8=EB=A5=BC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index 24bbab0..e1ff254 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -3,12 +3,13 @@ from typing import Union from django.db import models +from django.db.models import Manager from boj.enums import BOJLevel from users.models import User -class BOJUserManager(models.BaseManager): +class BOJUserManager(Manager): def user(self, user: User) -> _BOJUserManager: return self.filter(**{BOJUser.field_name.USERNAME: user.boj_username}) @@ -43,7 +44,7 @@ def __str__(self) -> str: return f'{self.username}' -class BOJUserSnapshotManager(models.BaseManager): +class BOJUserSnapshotManager(Manager): def create_snapshot_of(self, boj_user: BOJUser) -> BOJUserSnapshot: return self.create(**{ BOJUserSnapshot.field_name.USER: boj_user, @@ -82,6 +83,6 @@ class BOJProblem(models.Model): level = models.IntegerField(choices=BOJLevel.choices) -_BOJUserManager = Union[BOJUserManager, models.BaseManager[BOJUser]] +_BOJUserManager = Union[BOJUserManager, Manager[BOJUser]] _BOJUserSnapshotManager = Union[BOJUserSnapshotManager, - models.BaseManager[BOJUserSnapshot]] + Manager[BOJUserSnapshot]] From b6ad7babef2c3c96c83073939947901723ec7fff Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:03:47 +0900 Subject: [PATCH 441/552] refactor(boj.models): update BOJUserManager --- app/boj/models.py | 11 +++++++---- app/boj/signals.py | 8 +++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index e1ff254..2015d8a 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -10,11 +10,14 @@ class BOJUserManager(Manager): - def user(self, user: User) -> _BOJUserManager: - return self.filter(**{BOJUser.field_name.USERNAME: user.boj_username}) + def username(self, username: str) -> _BOJUserManager: + return self.filter(**{BOJUser.field_name.USERNAME: username}) - def username(self, username: str) -> BOJUser: - return self.get_or_create(**{BOJUser.field_name.USERNAME: username})[0] + def get_by_user(self, user: User) -> BOJUser: + return self.username(user.boj_username).get_or_create()[0] + + def get_by_username(self, username: str) -> BOJUser: + return self.username(username).get_or_create()[0] class BOJUser(models.Model): diff --git a/app/boj/signals.py b/app/boj/signals.py index 0b75609..cfcf93d 100644 --- a/app/boj/signals.py +++ b/app/boj/signals.py @@ -16,20 +16,18 @@ @receiver(post_save, sender=User) def auto_create_boj_user(sender, instance: User, created: bool, **kwargs): - if created and not BOJUser.objects.user(instance).exists(): - BOJUser.objects.create( - **{BOJUser.field_name.USERNAME: instance.boj_username}) + update_boj_user_data(instance.username) @receiver(post_save, sender=BOJUser) -def auto_create_boj_user(sender, instance: BOJUser, created: bool, **kwargs): +def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): if created: update_boj_user_data(instance.username) @background def update_boj_user_data(username: str): - instance = BOJUser.objects.username(username) + instance = BOJUser.objects.get_by_username(username) url = f'https://solved.ac/api/v3/user/show?handle={username}' data = requests.get(url).json() try: From 9c0800b5eed5e0718fc9ab07686a8bf23d2dafd6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:06:58 +0900 Subject: [PATCH 442/552] =?UTF-8?q?refactor(boj.services):=20signals.py?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=8B=9C=EA=B7=B8=EB=84=90=EC=9D=84=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=ED=95=98=EC=A7=80=20=EC=95=8A=EC=9C=BC?= =?UTF-8?q?=EB=AF=80=EB=A1=9C=20services=EB=A1=9C=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/admin.py | 27 ++++++++++++++------------- app/boj/apps.py | 2 +- app/boj/{signals.py => services.py} | 0 3 files changed, 15 insertions(+), 14 deletions(-) rename app/boj/{signals.py => services.py} (100%) diff --git a/app/boj/admin.py b/app/boj/admin.py index a683fe4..58b54d1 100644 --- a/app/boj/admin.py +++ b/app/boj/admin.py @@ -2,33 +2,34 @@ from django.db.models import QuerySet from django.http.request import HttpRequest -from boj import models -from boj.signals import update_boj_user_data +from boj.models import BOJUser +from boj.models import BOJUserSnapshot +from boj.services import update_boj_user_data -@admin.register(models.BOJUser) +@admin.register(BOJUser) class BOJUserModelAdmin(admin.ModelAdmin): list_display = [ - models.BOJUser.field_name.USERNAME, - models.BOJUser.field_name.LEVEL, - models.BOJUser.field_name.RATING, - models.BOJUser.field_name.UPDATED_AT, + BOJUser.field_name.USERNAME, + BOJUser.field_name.LEVEL, + BOJUser.field_name.RATING, + BOJUser.field_name.UPDATED_AT, ] actions = [ 'update', ] @admin.action(description="Update selected BOJ user data. (via solved.ac API)") - def update(self, request: HttpRequest, queryset: QuerySet[models.BOJUser]): + def update(self, request: HttpRequest, queryset: QuerySet[BOJUser]): for obj in queryset: update_boj_user_data(obj.username) -@admin.register(models.BOJUserSnapshot) +@admin.register(BOJUserSnapshot) class BOJUserSnapshotModelAdmin(admin.ModelAdmin): list_display = [ - models.BOJUserSnapshot.field_name.USER, - models.BOJUserSnapshot.field_name.LEVEL, - models.BOJUserSnapshot.field_name.RATING, - models.BOJUserSnapshot.field_name.CREATED_AT, + BOJUserSnapshot.field_name.USER, + BOJUserSnapshot.field_name.LEVEL, + BOJUserSnapshot.field_name.RATING, + BOJUserSnapshot.field_name.CREATED_AT, ] diff --git a/app/boj/apps.py b/app/boj/apps.py index cfe94d6..209829a 100644 --- a/app/boj/apps.py +++ b/app/boj/apps.py @@ -6,4 +6,4 @@ class BojConfig(AppConfig): name = "boj" def ready(self) -> None: - import boj.signals + import boj.services diff --git a/app/boj/signals.py b/app/boj/services.py similarity index 100% rename from app/boj/signals.py rename to app/boj/services.py From 182bf999728d146eb0b858038bd5ab8a8935addd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:27:42 +0900 Subject: [PATCH 443/552] chore(config.settings): remove unused file --- app/config/settings.py | 282 ----------------------------------------- 1 file changed, 282 deletions(-) delete mode 100644 app/config/settings.py diff --git a/app/config/settings.py b/app/config/settings.py deleted file mode 100644 index 9b56a5a..0000000 --- a/app/config/settings.py +++ /dev/null @@ -1,282 +0,0 @@ -""" -Django settings for project. - -Generated by 'django-admin startproject' using Django 4.2. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" - -from datetime import timedelta -from pathlib import Path - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [ - 'tle-kr.com', - 'timelimitexceeded.kr', -] - -# CORS - -CORS_ORIGIN_ALLOW_ALL = True -CORS_ALLOW_CREDENTIALS = True - - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - - "corsheaders", - "drf_yasg", - 'background_task', - "rest_framework", - 'rest_framework_simplejwt', - - "boj", - "users", - "problems", - "crews", - "submissions", -] - -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "config.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "config.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -AUTH_USER_MODEL = 'users.User' - -AUTHENTICATION_BACKENDS = [ - 'users.backends.UserAuthBackend', -] - -SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=60), - 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=100), - 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=100), - 'SLIDING_TOKEN_REFRESH_MAX_LIFETIME': timedelta(days=200), -} - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication', - ), -} - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = 'Asia/Seoul' - -USE_I18N = True - -USE_TZ = False - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = "static/" - -STATIC_ROOT = BASE_DIR / '.static' - - -# Meida files (Images) -# https://docs.djangoproject.com/en/4.2/topics/files/ - -MEDIA_URL = "media/" - -MEDIA_ROOT = BASE_DIR / '.media' - - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - - -DEFAULT_EXCEPTION_REPORTER = "config.utils.NACLExceptionReporter" - -APPEND_SLASH = False - -# Swagger Settings (DRf-YASG) - -SWAGGER_SETTINGS = { - "LOGIN_URL": "/api/v1/auth/signin", - "LOGOUT_URL": "/api/v1/auth/signout", -} - - -LOGGING = { - "version": 1, - "disable_existing_loggers": False, - "filters": { - "require_debug_false": { - "()": "django.utils.log.RequireDebugFalse", - }, - "require_debug_true": { - "()": "django.utils.log.RequireDebugTrue", - }, - }, - "formatters": { - "standard": { - "()": "config.utils.ColorlessServerFormatter", - "format": "[{server_time}] {message}", - "style": "{", - }, - "django.server": { - "()": "config.utils.ColorlessServerFormatter", - "format": "[{server_time}] {message}", - "style": "{", - } - }, - "handlers": { - "console": { - "level": "INFO", - "filters": ["require_debug_true"], - 'class': 'config.utils.FileAndStreamHandler', - 'filename': 'logs/console.log', - "formatter": "standard", - }, - "django.mail": { - "level": "ERROR", - "filters": ["require_debug_true"], - 'class': 'config.utils.FileAndStreamHandler', - 'filename': 'logs/django.mail.log', - }, - "django.server": { - "level": "INFO", - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/django.server.log', - 'when': 'D', - "formatter": "django.server", - }, - "problems": { - "level": "INFO", - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/problems.log', - 'when': 'D', - "formatter": "standard", - }, - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - 'class': 'logging.FileHandler', - 'filename': 'logs/mail_admins.log', - }, - "django.security.DisallowedHost": { - "level": "INFO", - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/django.security.DisallowedHost.log', - 'when': 'D', - "formatter": "standard", - }, - }, - "loggers": { - "django": { - "handlers": ["console", "mail_admins"], - "level": "INFO", - }, - "django.server": { - "handlers": ["django.server"], - "level": "INFO", - "propagate": False, - }, - 'django.security.DisallowedHost': { - 'handlers': ['django.security.DisallowedHost'], - "level": "DEBUG", - 'propagate': False, - }, - "problems": { - "handlers": ["problems"], - "level": "INFO", - }, - }, -} - - -#Django Background Tasks - -BACKGROUND_TASK_ASYNC_THREADS = 1 From 3d3a938c82f787e171e0ac127c6b6e0a0d7026c4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:32:13 +0900 Subject: [PATCH 444/552] =?UTF-8?q?refactor(users.urls):=20users=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20url=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/urls.py | 14 ++------------ app/users/urls.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 app/users/urls.py diff --git a/app/config/urls.py b/app/config/urls.py index 89fe547..2ea3025 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -6,10 +6,8 @@ from drf_yasg.views import get_schema_view from rest_framework import permissions -import crews.urls import problems.urls -import users.views -import submissions.views +import users.urls schema_view = get_schema_view( @@ -27,16 +25,8 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ - path("auth/signin", users.views.SignInAPIView.as_view()), - path("auth/signup", users.views.SignUpAPIView.as_view()), - path("auth/signout", users.views.SignOutAPIView.as_view()), - path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), - path("auth/email/check", users.views.EmailCheckAPIView.as_view()), - path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), - *crews.urls.urlpatterns, + *users.urls.urlpatterns, *problems.urls.urlpatterns, - path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), - path("submissions/", submissions.views.CreateCodeReview.as_view()), ])), path(r'swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path(r'swagger(?P\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'), diff --git a/app/users/urls.py b/app/users/urls.py new file mode 100644 index 0000000..6c3221d --- /dev/null +++ b/app/users/urls.py @@ -0,0 +1,14 @@ +from django.urls import path + +import users.views + + +urlpatterns = [ + path("auth/signin", users.views.SignInAPIView.as_view()), + path("auth/signup", users.views.SignUpAPIView.as_view()), + path("auth/signout", users.views.SignOutAPIView.as_view()), + path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), + path("auth/email/check", users.views.EmailCheckAPIView.as_view()), + path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), + path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), +] From 04dfd598b9c6a147b4c8c105517a2f45e6bc3fa2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:37:02 +0900 Subject: [PATCH 445/552] =?UTF-8?q?refactor(users.admins):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EA=B0=80=20=EC=86=8C=EC=86=8D=EB=90=9C=20?= =?UTF-8?q?=ED=81=AC=EB=A3=A8=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=B3=B4?= =?UTF-8?q?=EC=97=AC=EC=A3=BC=EB=8A=94=20=EC=97=B4=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?(=EC=9D=98=EC=A1=B4=EC=84=B1=EC=9D=84=20=EC=A4=84=EC=9D=B4?= =?UTF-8?q?=EA=B8=B0=20=EC=9C=84=ED=95=B4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/admin.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/app/users/admin.py b/app/users/admin.py index 229a085..cba8205 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from users.models import User, UserEmailVerification -from crews.models import CrewMember +from users.models import User +from users.models import UserEmailVerification admin.site.register([ @@ -17,20 +17,8 @@ class UserAdmin(BaseUserAdmin): User.field_name.USERNAME, User.field_name.EMAIL, User.field_name.BOJ_USERNAME, - 'get_crews', User.field_name.IS_ACTIVE, User.field_name.IS_STAFF, User.field_name.IS_SUPERUSER, User.field_name.CREATED_AT, ] - - @admin.display(description='captains / members') - def get_crews(self, user: User) -> str: - n_captains = CrewMember.objects.filter(**{ - CrewMember.field_name.USER: user, - CrewMember.field_name.IS_CAPTAIN: True, - }).count() - n_members = CrewMember.objects.filter(**{ - CrewMember.field_name.USER: user, - }).count() - return f'{n_captains} / {n_members}' From d3a0633fbcacf72cd8733c62a0b2cb55b55777b3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:37:08 +0900 Subject: [PATCH 446/552] =?UTF-8?q?refactor(config.settings):=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EC=A4=91=EC=9D=B8=20crews,=20sub?= =?UTF-8?q?missions=20=EC=95=B1=EC=9D=84=20=EC=84=A4=EC=B9=98=20=ED=95=B4?= =?UTF-8?q?=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py index 07e20fb..c9cc959 100644 --- a/app/config/settings/base/__init__.py +++ b/app/config/settings/base/__init__.py @@ -57,8 +57,6 @@ "boj", "users", "problems", - "crews", - "submissions", ] MIDDLEWARE = [ From 02368a80e8888de639974b4d04d203605935800b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Wed, 28 Aug 2024 23:46:14 +0900 Subject: [PATCH 447/552] =?UTF-8?q?refactor(users.urls):=20users=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=20url=EB=93=A4=20=EC=A4=91=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20path=EB=93=A4=EC=9D=84=20=EA=B7=B8=EB=A3=B9?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/urls.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/users/urls.py b/app/users/urls.py index 6c3221d..57938bd 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -1,14 +1,19 @@ +from django.urls import include from django.urls import path import users.views urlpatterns = [ - path("auth/signin", users.views.SignInAPIView.as_view()), - path("auth/signup", users.views.SignUpAPIView.as_view()), - path("auth/signout", users.views.SignOutAPIView.as_view()), - path("auth/username/check", users.views.UsernameCheckAPIView.as_view()), - path("auth/email/check", users.views.EmailCheckAPIView.as_view()), - path("auth/email/verify", users.views.EmailVerifyAPIView.as_view()), - path("user/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), + path("auth", include([ + path("/signin", users.views.SignInAPIView.as_view()), + path("/signup", users.views.SignUpAPIView.as_view()), + path("/signout", users.views.SignOutAPIView.as_view()), + path("/username/check", users.views.UsernameCheckAPIView.as_view()), + path("/email/check", users.views.EmailCheckAPIView.as_view()), + path("/email/verify", users.views.EmailVerifyAPIView.as_view()), + ])), + path("user", include([ + path("/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), + ])), ] From 58a3af28b84d594688020e92e5caf0c9c114b510 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 29 Aug 2024 00:05:59 +0900 Subject: [PATCH 448/552] =?UTF-8?q?refactor(users.serializers):=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EB=B0=B1=EC=A4=80=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=A0=95=EB=B3=B4=EB=8A=94=20BOJField=20=ED=98=95?= =?UTF-8?q?=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index f3a4038..1f14051 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -1,27 +1,20 @@ from rest_framework import serializers -from boj.enums import BOJLevel +from boj.models import BOJUser from boj.serializers import BOJUserSerializer -from boj.services import get_boj_user_service from users.models import User -class UserBOJField(serializers.SerializerMethodField): - def to_representation(self, instance: User): - assert isinstance(instance, User) - service = get_boj_user_service(instance.boj_username) - return BOJUserSerializer(service.instance).data +PK = 'id' + +class BOJField(serializers.SerializerMethodField): + def to_representation(self, value: BOJUser): + return BOJUserSerializer(value).data -class UserBOJLevelNameField(serializers.SerializerMethodField): - def to_representation(self, instance: User): + def get_attribute(self, instance) -> BOJUser: assert isinstance(instance, User) - service = get_boj_user_service(instance.boj_username) - level = BOJLevel(service.instance.level) - return { - 'value': level.value, - 'name': level.get_name(lang='ko', arabic=False), - } + return BOJUser.objects.get_by_user(instance) class EmailSerializer(serializers.Serializer): @@ -40,7 +33,7 @@ class EmailTokenSerializer(serializers.Serializer): class SignInSerializer(serializers.Serializer): email = serializers.EmailField() - password = serializers.CharField() + password = serializers.CharField(style={'input_type': 'password'}) class SignUpSerializer(serializers.ModelSerializer): @@ -63,12 +56,12 @@ class UsernameSerializer(serializers.Serializer): class UserSerializer(serializers.ModelSerializer): - boj = UserBOJField() + boj = BOJField() class Meta: model = User fields = [ - 'id', + PK, User.field_name.USERNAME, User.field_name.PROFILE_IMAGE, 'boj', @@ -77,7 +70,7 @@ class Meta: class UserUpdateSerializer(serializers.ModelSerializer): - level = UserBOJLevelNameField() + boj = BOJField() class Meta: model = User @@ -87,7 +80,7 @@ class Meta: User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, User.field_name.BOJ_USERNAME, - 'level', + 'boj', ] extra_kwargs = { User.field_name.EMAIL: { @@ -95,6 +88,10 @@ class Meta: }, User.field_name.PASSWORD: { 'write_only': True, + 'style': {'input_type': 'password'}, + }, + User.field_name.BOJ_USERNAME: { + 'write_only': True, } } @@ -109,7 +106,7 @@ class UserMinimalSerializer(serializers.ModelSerializer): class Meta: model = User fields = [ - 'id', + PK, User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, ] From defa574a8a3b6755b7bd3c9a40bf2c45275ce125 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 29 Aug 2024 00:06:26 +0900 Subject: [PATCH 449/552] =?UTF-8?q?refactor(boj.serializers):=20DRF=20seri?= =?UTF-8?q?alizer=EC=9D=98=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=AA=A9=EC=A0=81=EC=97=90=20=EB=A7=9E=EA=B2=8C=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=B6=84=ED=95=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/serializers.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/app/boj/serializers.py b/app/boj/serializers.py index d3e3f1f..6b937ee 100644 --- a/app/boj/serializers.py +++ b/app/boj/serializers.py @@ -5,19 +5,24 @@ class BOJLevelField(serializers.SerializerMethodField): - def to_representation(self, instance: BOJUser): - assert isinstance(instance, BOJUser) - level = BOJLevel(instance.level) + def to_representation(self, boj_level: BOJLevel): return { - 'value': level.value, - 'name': level.get_name(lang='ko', arabic=False), + 'value': boj_level.value, + 'name': boj_level.get_name(lang='ko', arabic=False), } + def get_attribute(self, instance: BOJUser) -> BOJLevel: + assert isinstance(instance, BOJUser) + return BOJLevel(instance.level) + class BOJProfileUrlField(serializers.SerializerMethodField): - def to_representation(self, instance: BOJUser): + def to_representation(self, username: str): + return f'https://boj.kr/{username}' + + def get_attribute(self, instance: BOJUser) -> str: assert isinstance(instance, BOJUser) - return f'https://boj.kr/{instance.username}' + return instance.username class BOJUserSerializer(serializers.ModelSerializer): From ed93c4a9d67d8d2eee5f3e751a0ff7f8a0500499 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 29 Aug 2024 05:23:09 +0900 Subject: [PATCH 450/552] =?UTF-8?q?refactor(problems.analyses):=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EB=B6=84=EC=84=9D=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EB=AA=A8=EB=93=88=EB=93=A4=EC=9D=84=20problems=20=EC=95=B1=20?= =?UTF-8?q?=EC=95=88=EC=97=90=20=EC=84=9C=EB=B8=8C=20=EC=95=B1=20analyses?= =?UTF-8?q?=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/__init__.py | 1 + app/problems/admin.py | 29 +++ app/problems/admin/__init__.py | 5 - app/problems/admin/problem.py | 30 --- app/problems/admin/problem_analysis.py | 42 ---- app/problems/admin/problem_analysis_tag.py | 21 -- app/problems/admin/problem_tag.py | 18 -- app/problems/admin/problem_tag_relation.py | 16 -- .../analyzers/gpt => analyses}/__init__.py | 0 app/problems/analyses/admin.py | 87 ++++++++ app/problems/analyses/apps.py | 9 + app/problems/analyses/dto.py | 26 +++ app/problems/analyses/enums.py | 33 +++ app/problems/analyses/migrations/__init__.py | 0 app/problems/analyses/models.py | 198 ++++++++++++++++++ app/problems/analyses/serializers.py | 86 ++++++++ app/problems/analyses/services.py | 11 + app/problems/analyses/tests.py | 3 + app/problems/analyzers/__init__.py | 48 +++++ app/problems/{services => }/analyzers/base.py | 5 +- app/problems/analyzers/gemini/__init__.py | 1 + .../analyzers/gemini/analyzer.py | 22 +- .../analyzers/gemini/parsers.py | 2 +- .../analyzers/gemini/prompts.py | 4 +- app/problems/analyzers/gpt/__init__.py | 0 .../{services => }/analyzers/gpt/analyzer.py | 2 +- app/problems/dto.py | 42 +--- app/problems/enums.py | 30 --- app/problems/{models/problem.py => models.py} | 22 +- app/problems/models/__init__.py | 6 - app/problems/models/problem_analysis.py | 51 ----- app/problems/models/problem_analysis_tag.py | 27 --- app/problems/models/problem_tag.py | 33 --- app/problems/models/problem_tag_relation.py | 23 -- app/problems/models/signals.py | 11 - app/problems/serializers.py | 183 ++++++++++++++++ app/problems/serializers/__init__.py | 87 -------- app/problems/serializers/fields.py | 108 ---------- app/problems/services/__init__.py | 21 -- app/problems/services/analyzers/__init__.py | 6 - .../services/analyzers/gemini/__init__.py | 1 - app/problems/services/base.py | 50 ----- app/problems/services/concrete.py | 132 ------------ 43 files changed, 754 insertions(+), 778 deletions(-) create mode 100644 app/problems/admin.py delete mode 100644 app/problems/admin/__init__.py delete mode 100644 app/problems/admin/problem.py delete mode 100644 app/problems/admin/problem_analysis.py delete mode 100644 app/problems/admin/problem_analysis_tag.py delete mode 100644 app/problems/admin/problem_tag.py delete mode 100644 app/problems/admin/problem_tag_relation.py rename app/problems/{services/analyzers/gpt => analyses}/__init__.py (100%) create mode 100644 app/problems/analyses/admin.py create mode 100644 app/problems/analyses/apps.py create mode 100644 app/problems/analyses/dto.py create mode 100644 app/problems/analyses/enums.py create mode 100644 app/problems/analyses/migrations/__init__.py create mode 100644 app/problems/analyses/models.py create mode 100644 app/problems/analyses/serializers.py create mode 100644 app/problems/analyses/services.py create mode 100644 app/problems/analyses/tests.py create mode 100644 app/problems/analyzers/__init__.py rename app/problems/{services => }/analyzers/base.py (62%) create mode 100644 app/problems/analyzers/gemini/__init__.py rename app/problems/{services => }/analyzers/gemini/analyzer.py (89%) rename app/problems/{services => }/analyzers/gemini/parsers.py (97%) rename app/problems/{services => }/analyzers/gemini/prompts.py (98%) create mode 100644 app/problems/analyzers/gpt/__init__.py rename app/problems/{services => }/analyzers/gpt/analyzer.py (86%) rename app/problems/{models/problem.py => models.py} (78%) delete mode 100644 app/problems/models/__init__.py delete mode 100644 app/problems/models/problem_analysis.py delete mode 100644 app/problems/models/problem_analysis_tag.py delete mode 100644 app/problems/models/problem_tag.py delete mode 100644 app/problems/models/problem_tag_relation.py delete mode 100644 app/problems/models/signals.py create mode 100644 app/problems/serializers.py delete mode 100644 app/problems/serializers/__init__.py delete mode 100644 app/problems/serializers/fields.py delete mode 100644 app/problems/services/__init__.py delete mode 100644 app/problems/services/analyzers/__init__.py delete mode 100644 app/problems/services/analyzers/gemini/__init__.py delete mode 100644 app/problems/services/base.py delete mode 100644 app/problems/services/concrete.py diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py index c9cc959..48c5883 100644 --- a/app/config/settings/base/__init__.py +++ b/app/config/settings/base/__init__.py @@ -57,6 +57,7 @@ "boj", "users", "problems", + "problems.analyses", ] MIDDLEWARE = [ diff --git a/app/problems/admin.py b/app/problems/admin.py new file mode 100644 index 0000000..9090707 --- /dev/null +++ b/app/problems/admin.py @@ -0,0 +1,29 @@ +from django.contrib import admin +from django.db.models import QuerySet + +from problems.models import Problem +from problems.analyzers import schedule_analyze +from users.models import User + + +@admin.register(Problem) +class ProblemModelAdmin(admin.ModelAdmin): + list_display = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY, + Problem.field_name.CREATED_AT, + Problem.field_name.UPDATED_AT, + ] + search_fields = [ + Problem.field_name.TITLE, + Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + ] + ordering = ['-'+Problem.field_name.CREATED_AT] + actions = [ + 'analyze', + ] + + @admin.action(description="Analyze selected problems.") + def analyze(self, request, queryset: QuerySet[Problem]): + for obj in queryset: + schedule_analyze(obj.pk) diff --git a/app/problems/admin/__init__.py b/app/problems/admin/__init__.py deleted file mode 100644 index 6502a2c..0000000 --- a/app/problems/admin/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from problems.admin.problem import ProblemModelAdmin -from problems.admin.problem_analysis import ProblemAnalysisModelAdmin -from problems.admin.problem_analysis_tag import ProblemAnalysisTagModelAdmin -from problems.admin.problem_tag import ProblemTagModelAdmin -from problems.admin.problem_tag_relation import ProblemTagRelationModelAdmin diff --git a/app/problems/admin/problem.py b/app/problems/admin/problem.py deleted file mode 100644 index 7959da5..0000000 --- a/app/problems/admin/problem.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.contrib import admin -from django.db.models import QuerySet - -from problems import models -from problems import services -from users.models import User - - -@admin.register(models.Problem) -class ProblemModelAdmin(admin.ModelAdmin): - list_display = [ - models.Problem.field_name.TITLE, - models.Problem.field_name.CREATED_BY, - models.Problem.field_name.CREATED_AT, - models.Problem.field_name.UPDATED_AT, - ] - search_fields = [ - models.Problem.field_name.TITLE, - models.Problem.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - ] - ordering = ['-'+models.Problem.field_name.CREATED_AT] - actions = [ - 'analyze', - ] - - @admin.action(description="Analyze selected problems.") - def analyze(self, request, queryset: QuerySet[models.Problem]): - for obj in queryset: - service = services.get_problem_service(obj) - service.analyze() diff --git a/app/problems/admin/problem_analysis.py b/app/problems/admin/problem_analysis.py deleted file mode 100644 index 9992c2b..0000000 --- a/app/problems/admin/problem_analysis.py +++ /dev/null @@ -1,42 +0,0 @@ -from textwrap import shorten - -from django.contrib import admin - -from problems import models - - -@admin.register(models.ProblemAnalysis) -class ProblemAnalysisModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemAnalysis.field_name.PROBLEM, - models.ProblemAnalysis.field_name.DIFFICULTY, - 'get_time_complexity', - 'get_tags', - 'get_hint_steps', - models.ProblemAnalysis.field_name.CREATED_AT, - ] - search_fields = [ - models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, - models.ProblemAnalysis.field_name.TIME_COMPLEXITY, - ] - ordering = ['-'+models.ProblemAnalysis.field_name.CREATED_AT] - - @admin.display(description='Time complexity') - def get_time_complexity(self, obj: models.ProblemAnalysis) -> str: - return f'O({obj.time_complexity})' - - @admin.display(description='Tags') - def get_tags(self, analysis: models.ProblemAnalysis) -> str: - tag_keys = models.ProblemAnalysisTag.objects.filter(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, - }).select_related( - models.ProblemAnalysisTag.field_name.TAG, - ).values_list( - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, - flat=True, - ) - return ', '.join(tag_keys) - - @admin.display(description='Hint steps') - def get_hint_steps(self, obj: models.ProblemAnalysis) -> int: - return len(obj.hint) diff --git a/app/problems/admin/problem_analysis_tag.py b/app/problems/admin/problem_analysis_tag.py deleted file mode 100644 index b8619e3..0000000 --- a/app/problems/admin/problem_analysis_tag.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.contrib import admin - -from problems import models - - -@admin.register(models.ProblemAnalysisTag) -class ProblemAnalysisTagModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemAnalysisTag.field_name.ANALYSIS, - models.ProblemAnalysisTag.field_name.TAG, - ] - search_fields = [ - models.ProblemAnalysisTag.field_name.ANALYSIS+'__' + - models.ProblemAnalysis.field_name.PROBLEM+'__'+models.Problem.field_name.TITLE, - models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY, - models.ProblemAnalysisTag.field_name.TAG + - '__'+models.ProblemTag.field_name.NAME_KO, - models.ProblemAnalysisTag.field_name.TAG + - '__'+models.ProblemTag.field_name.NAME_EN, - ] - ordering = [models.ProblemAnalysisTag.field_name.ANALYSIS] diff --git a/app/problems/admin/problem_tag.py b/app/problems/admin/problem_tag.py deleted file mode 100644 index a6c2819..0000000 --- a/app/problems/admin/problem_tag.py +++ /dev/null @@ -1,18 +0,0 @@ -from django.contrib import admin - -from problems import models - - -@admin.register(models.ProblemTag) -class ProblemTagModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemTag.field_name.KEY, - models.ProblemTag.field_name.NAME_KO, - models.ProblemTag.field_name.NAME_EN, - ] - search_fields = [ - models.ProblemTag.field_name.KEY, - models.ProblemTag.field_name.NAME_KO, - models.ProblemTag.field_name.NAME_EN, - ] - ordering = [models.ProblemTag.field_name.KEY] diff --git a/app/problems/admin/problem_tag_relation.py b/app/problems/admin/problem_tag_relation.py deleted file mode 100644 index e1e1eb0..0000000 --- a/app/problems/admin/problem_tag_relation.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.contrib import admin - -from problems import models - - -@admin.register(models.ProblemTagRelation) -class ProblemTagRelationModelAdmin(admin.ModelAdmin): - list_display = [ - models.ProblemTagRelation.field_name.PARENT, - models.ProblemTagRelation.field_name.CHILD, - ] - search_fields = [ - models.ProblemTagRelation.field_name.PARENT, - models.ProblemTagRelation.field_name.CHILD, - ] - ordering = [models.ProblemTagRelation.field_name.PARENT] diff --git a/app/problems/services/analyzers/gpt/__init__.py b/app/problems/analyses/__init__.py similarity index 100% rename from app/problems/services/analyzers/gpt/__init__.py rename to app/problems/analyses/__init__.py diff --git a/app/problems/analyses/admin.py b/app/problems/analyses/admin.py new file mode 100644 index 0000000..b8adb53 --- /dev/null +++ b/app/problems/analyses/admin.py @@ -0,0 +1,87 @@ +from django.contrib import admin + +from problems.models import Problem +from problems.analyses.models import ProblemAnalysis +from problems.analyses.models import ProblemAnalysisTag +from problems.analyses.models import ProblemTag +from problems.analyses.models import ProblemTagRelation + + +@admin.register(ProblemAnalysis) +class ProblemAnalysisModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysis.field_name.PROBLEM, + ProblemAnalysis.field_name.DIFFICULTY, + 'get_time_complexity', + 'get_tags', + 'get_hint_steps', + ProblemAnalysis.field_name.CREATED_AT, + ] + search_fields = [ + ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ProblemAnalysis.field_name.TIME_COMPLEXITY, + ] + ordering = ['-'+ProblemAnalysis.field_name.CREATED_AT] + + @admin.display(description='Time complexity') + def get_time_complexity(self, obj: ProblemAnalysis) -> str: + return f'O({obj.time_complexity})' + + @admin.display(description='Tags') + def get_tags(self, analysis: ProblemAnalysis) -> str: + tag_keys = ProblemAnalysisTag.objects.filter(**{ + ProblemAnalysisTag.field_name.ANALYSIS: analysis, + }).select_related( + ProblemAnalysisTag.field_name.TAG, + ).values_list( + ProblemAnalysisTag.field_name.TAG+'__'+ProblemTag.field_name.KEY, + flat=True, + ) + return ', '.join(tag_keys) + + @admin.display(description='Hint steps') + def get_hint_steps(self, obj: ProblemAnalysis) -> int: + return len(obj.hint) + + +@admin.register(ProblemAnalysisTag) +class ProblemAnalysisTagModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemAnalysisTag.field_name.ANALYSIS, + ProblemAnalysisTag.field_name.TAG, + ] + search_fields = [ + ProblemAnalysisTag.field_name.ANALYSIS+'__'+ProblemAnalysis.field_name.PROBLEM+'__'+Problem.field_name.TITLE, + ProblemAnalysisTag.field_name.TAG+'__'+ProblemTag.field_name.KEY, + ProblemAnalysisTag.field_name.TAG+'__'+ProblemTag.field_name.NAME_KO, + ProblemAnalysisTag.field_name.TAG+'__'+ProblemTag.field_name.NAME_EN, + ] + ordering = [ProblemAnalysisTag.field_name.ANALYSIS] + + +@admin.register(ProblemTag) +class ProblemTagModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ] + search_fields = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_KO, + ProblemTag.field_name.NAME_EN, + ] + ordering = [ProblemTag.field_name.KEY] + + +@admin.register(ProblemTagRelation) +class ProblemTagRelationModelAdmin(admin.ModelAdmin): + list_display = [ + ProblemTagRelation.field_name.PARENT, + ProblemTagRelation.field_name.CHILD, + ] + search_fields = [ + ProblemTagRelation.field_name.PARENT, + ProblemTagRelation.field_name.CHILD, + ] + ordering = [ProblemTagRelation.field_name.PARENT] diff --git a/app/problems/analyses/apps.py b/app/problems/analyses/apps.py new file mode 100644 index 0000000..41ef4f2 --- /dev/null +++ b/app/problems/analyses/apps.py @@ -0,0 +1,9 @@ +from django.apps import AppConfig + + +class AnalysesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "problems.analyses" + + def ready(self) -> None: + import problems.analyses.services diff --git a/app/problems/analyses/dto.py b/app/problems/analyses/dto.py new file mode 100644 index 0000000..1d3b3b1 --- /dev/null +++ b/app/problems/analyses/dto.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses import field +from typing import Tuple + +from problems.analyses.enums import ProblemDifficulty + + +@dataclass +class ProblemAnalysisDTO: + problem_id: int + time_complexity: str + difficulty: ProblemDifficulty + tags: Tuple[str] = field(default_factory=tuple) + hints: Tuple[str] = field(default_factory=tuple) + + +@dataclass +class ProblemTagDTO: + key: str + name_ko: str + name_en: str + + def __hash__(self) -> int: + return self.key diff --git a/app/problems/analyses/enums.py b/app/problems/analyses/enums.py new file mode 100644 index 0000000..d97ed39 --- /dev/null +++ b/app/problems/analyses/enums.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from django.db import models + + +NAMES = { + 'ko': ['분석 중', '쉬움', '보통', '어려움'], + 'en': ['UNDER ANALYSIS', 'EASY', 'NORMAL', 'HARD'], +} + + +class ProblemDifficulty(models.IntegerChoices): + @staticmethod + def from_label(label: str) -> ProblemDifficulty: + return { + 'EASY': ProblemDifficulty.EASY, + 'NORMAL': ProblemDifficulty.NORMAL, + 'HARD': ProblemDifficulty.HARD, + }[label] + + + UNDER_ANALYSIS = 0, '분석 중' + EASY = 1, '쉬움' + NORMAL = 2, '보통' + HARD = 3, '어려움' + + def get_name(self, lang='ko') -> str: + if lang not in NAMES: + raise ValueError( + f'Invalid language: {lang}, ', + f'choose from {NAMES.keys()}' + ) + return NAMES[lang][self.value] diff --git a/app/problems/analyses/migrations/__init__.py b/app/problems/analyses/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/analyses/models.py b/app/problems/analyses/models.py new file mode 100644 index 0000000..2a25e62 --- /dev/null +++ b/app/problems/analyses/models.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +from typing import Union + +from django.db import models +from django.db.transaction import atomic + +from problems.analyses.dto import ProblemAnalysisDTO +from problems.analyses.dto import ProblemTagDTO +from problems.analyses.enums import ProblemDifficulty +from problems.models import Problem + + +class ProblemTagManager(models.Manager): + def get_by_key(self, key: str) -> ProblemTag: + return self.get(**{ProblemTag.field_name.KEY: key}) + + +class ProblemTag(models.Model): + key = models.CharField( + max_length=50, + unique=True, + help_text='알고리즘 태그 키를 입력해주세요. (최대 20자)', + ) + name_ko = models.CharField( + max_length=50, + unique=True, + help_text='알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)', + ) + name_en = models.CharField( + max_length=50, + unique=True, + help_text='알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)', + ) + + objects: _ProblemTagManager = ProblemTagManager() + + class field_name: + KEY = 'key' + NAME_KO = 'name_ko' + NAME_EN = 'name_en' + + class Meta: + ordering = ['key'] + + def __repr__(self) -> str: + return f'[#{self.key}]' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()} ({self.name_ko})' + + def as_dto(self) -> ProblemTagDTO: + return ProblemTagDTO( + key=self.key, + name_ko=self.name_ko, + name_en=self.name_en, + ) + + +class ProblemTagRelation(models.Model): + parent = models.ForeignKey( + ProblemTag, + on_delete=models.CASCADE, + related_name='parent' + ) + child = models.ForeignKey( + ProblemTag, + on_delete=models.CASCADE, + related_name='child' + ) + + class field_name: + PARENT = 'parent' + CHILD = 'child' + + def __str__(self) -> str: + return f'{self.pk} : #{self.parent.key} <- #{self.child.key}' + + +class ProblemAnalysisManager(models.Manager): + def problem(self, problem: Problem) -> _ProblemAnalysisManager: + return self.filter(**{ProblemAnalysis.field_name.PROBLEM: problem}) + + def get_by_problem(self, problem: Problem) -> ProblemAnalysis: + return self.problem(problem).latest() + + def create_from_dto(self, analysis_dto: ProblemAnalysisDTO) -> ProblemAnalysis: + analysis = ProblemAnalysis(**{ + ProblemAnalysis.field_name.PROBLEM: analysis_dto.problem_id, + ProblemAnalysis.field_name.TIME_COMPLEXITY: analysis_dto.time_complexity, + ProblemAnalysis.field_name.DIFFICULTY: ProblemDifficulty.from_label(analysis_dto.difficulty), + ProblemAnalysis.field_name.HINT: analysis_dto.hints, + }) + analysis_tags = [] + for tag_key in analysis_dto.tags: + tag = ProblemTag.objects.get_by_key(tag_key) + analysis_tag = ProblemAnalysisTag(**{ + ProblemAnalysisTag.field_name.ANALYSIS: analysis, + ProblemAnalysisTag.field_name.TAG: tag, + }) + analysis_tags.append(analysis_tag) + with atomic(): + analysis.save() + ProblemAnalysisTag.objects.bulk_create(analysis_tags) + + +class ProblemAnalysis(models.Model): + problem = models.ForeignKey( + Problem, + on_delete=models.CASCADE, + help_text='문제를 입력해주세요.', + ) + difficulty = models.IntegerField( + help_text='문제 난이도를 입력해주세요.', + choices=ProblemDifficulty.choices, + ) + time_complexity = models.CharField( + max_length=100, + help_text=( + '문제 시간 복잡도를 입력해주세요. ', + '예) O(1), O(n), O(n^2), O(V \log E) 등', + ), + validators=[ + # TODO: 시간 복잡도 검증 로직 추가 + ], + ) + hint = models.JSONField( + help_text='문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.', + validators=[ + # TODO: 힌트 검증 로직 추가 + ], + blank=False, + default=list, + ) + created_at = models.DateTimeField(auto_now_add=True) + + objects: _ProblemAnalysisManager = ProblemAnalysisManager() + + class field_name: + PROBLEM = 'problem' + DIFFICULTY = 'difficulty' + TAGS = 'tags' + TIME_COMPLEXITY = 'time_complexity' + HINT = 'hint' + CREATED_AT = 'created_at' + + class Meta: + verbose_name_plural = 'Problem analyses' + ordering = ['-created_at'] + get_latest_by = ['created_at'] + + def __str__(self): + return f'[Analyse of {self.problem}]' + + +class ProblemAnalysisTagManager(models.Manager): + def problem(self, problem: Problem) -> _ProblemAnalysisTagManager: + try: + analysis = ProblemAnalysis.objects.get_by_problem(problem) + except ProblemAnalysis.DoesNotExist: + return ProblemAnalysisTag.objects.none() + else: + return self.analysis(analysis) + + def analysis(self, analysis: ProblemAnalysis) -> _ProblemAnalysisTagManager: + return self.filter(**{ProblemAnalysisTag.field_name.ANALYSIS: analysis}) + + +class ProblemAnalysisTag(models.Model): + analysis = models.ForeignKey( + ProblemAnalysis, + on_delete=models.CASCADE, + null=False, + blank=False, + ) + tag = models.ForeignKey( + ProblemTag, + on_delete=models.PROTECT, + null=False, + blank=False, + help_text='문제의 DSA 태그를 입력해주세요.', + ) + + objects: _ProblemAnalysisTagManager = ProblemAnalysisTagManager() + + class field_name: + ANALYSIS = 'analysis' + TAG = 'tag' + + def __str__(self): + return f'{self.analysis.problem} #{self.tag}' + + +_ProblemTagManager = Union[ProblemTagManager, models.Manager[ProblemTag]] +_ProblemAnalysisManager = Union[ProblemAnalysisManager, + models.Manager[ProblemAnalysis]] +_ProblemAnalysisTagManager = Union[ProblemAnalysisTagManager, + models.Manager[ProblemAnalysisTag]] diff --git a/app/problems/analyses/serializers.py b/app/problems/analyses/serializers.py new file mode 100644 index 0000000..b5af272 --- /dev/null +++ b/app/problems/analyses/serializers.py @@ -0,0 +1,86 @@ +from typing import List + +from django.db.models import QuerySet +from rest_framework import serializers + +from problems.analyses.enums import ProblemDifficulty +from problems.analyses.models import ProblemAnalysis +from problems.analyses.models import ProblemAnalysisTag +from problems.analyses.models import ProblemTag + + +PK = 'id' + + +class ProblemAnalysisDifficultyField(serializers.SerializerMethodField): + def to_representation(self, difficulty: ProblemDifficulty): + return { + "name_ko": difficulty.get_name(lang='ko'), + "name_en": difficulty.get_name(lang='en'), + 'value': difficulty.value, + } + + def get_attribute(self, instance: ProblemAnalysis) -> ProblemDifficulty: + assert isinstance(instance, ProblemAnalysis) + return ProblemDifficulty(instance.difficulty) + + +class ProblemAnalysisTimeComplexityField(serializers.SerializerMethodField): + def to_representation(self, value: str): + return { + 'value': value, + } + + def get_attribute(self, instance: ProblemAnalysis): + assert isinstance(instance, ProblemAnalysis) + return instance.time_complexity + + +class ProblemAnalysisTagsField(serializers.SerializerMethodField): + def to_representation(self, tags: QuerySet[ProblemTag]): + return ProblemTagSerializer(tags, many=True).data + + def get_attribute(self, instance: ProblemAnalysis): + assert isinstance(instance, ProblemAnalysis) + tag_ids = ProblemAnalysisTag.objects.analysis(instance).values_list(ProblemAnalysisTag.field_name.TAG, flat=True) + return ProblemTag.objects.filter(pk__in=tag_ids) + + +class ProblemAnalysisHintsField(serializers.SerializerMethodField): + def to_representation(self, hints: List[str]): + return hints + + def get_attribute(self, instance: ProblemAnalysis) -> List[str]: + assert isinstance(instance, ProblemAnalysis) + if isinstance(instance.hint, list): + return instance.hint + return [instance.hint] + + +class ProblemAnalysisSerializer(serializers.ModelSerializer): + difficulty = ProblemAnalysisDifficultyField() + time_complexity = ProblemAnalysisTimeComplexityField() + tags = ProblemAnalysisTagsField() + hints = ProblemAnalysisHintsField() + + class Meta: + model = ProblemAnalysis + fields = [ + 'difficulty', + 'time_complexity', + 'tags', + 'hints', + ProblemAnalysis.field_name.CREATED_AT, + ] + read_only_fields = ['__all__'] + + +class ProblemTagSerializer(serializers.ModelSerializer): + class Meta: + model = ProblemTag + fields = [ + ProblemTag.field_name.KEY, + ProblemTag.field_name.NAME_EN, + ProblemTag.field_name.NAME_KO, + ] + read_only_fields = ['__all__'] diff --git a/app/problems/analyses/services.py b/app/problems/analyses/services.py new file mode 100644 index 0000000..aa448d3 --- /dev/null +++ b/app/problems/analyses/services.py @@ -0,0 +1,11 @@ +from django.db.models.signals import post_save +from django.dispatch import receiver + +from problems.models import Problem +from problems.analyzers import schedule_analyze + + +@receiver(post_save, sender=Problem) +def auto_analyze(sender, instance: Problem, created: bool, **kwargs): + if created: + schedule_analyze(instance.pk) diff --git a/app/problems/analyses/tests.py b/app/problems/analyses/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/app/problems/analyses/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/app/problems/analyzers/__init__.py b/app/problems/analyzers/__init__.py new file mode 100644 index 0000000..00e01b1 --- /dev/null +++ b/app/problems/analyzers/__init__.py @@ -0,0 +1,48 @@ +from logging import getLogger + +from background_task import background + + +from problems.analyses.dto import ProblemAnalysisDTO +from problems.analyses.models import ProblemAnalysis +from problems.analyses.models import ProblemTag +from problems.analyzers.base import ProblemAnalyzer +from problems.analyzers.gemini import GeminiProblemAnalyzer +from problems.dto import ProblemDTO +from problems.models import Problem + + +logger = getLogger('problems.analyzers') + + +def get_analyzer() -> ProblemAnalyzer: + return GeminiProblemAnalyzer.get_instance() + + +@background +def schedule_analyze(problem_id: int): + logger.info(f'PK={problem_id} 문제의 분석 준비중.') + problem = Problem.objects.get(pk=problem_id) + problem_dto = ProblemDTO.from_model(problem) + logger.info('문제 분석기를 불러오는 중.') + analyzer = get_analyzer() + logger.info(f'{problem_dto} 문제의 분석 시작.') + analysis_dto = analyzer.analyze(problem_dto) + logger.info(f'{problem_dto} 문제의 분석 완료.') + logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장하는 중.') + validate_analysis_dto_tags(analysis_dto) + ProblemAnalysis.objects.create_from_dto(analysis_dto) + logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장 완료.') + + +def validate_analysis_dto_tags(analysis_dto: ProblemAnalysisDTO): + for tag_key in analysis_dto.tags: + try: + ProblemTag.objects.get_by_key(tag_key) + except ProblemTag.DoesNotExist: + logger.warn(f'문제 분석 결과에 알 수 없는 태그 [{tag_key}] 가 포함된 것을 발견하였습니다.') + ProblemTag.objects.create(**{ + ProblemTag.field_name.KEY: tag_key, + ProblemTag.field_name.NAME_KO: f'존재하지 않는 태그: [{tag_key}]', + ProblemTag.field_name.NAME_EN: f'존재하지 않는 태그: [{tag_key}]', + }) diff --git a/app/problems/services/analyzers/base.py b/app/problems/analyzers/base.py similarity index 62% rename from app/problems/services/analyzers/base.py rename to app/problems/analyzers/base.py index 63e1c21..c9e74dd 100644 --- a/app/problems/services/analyzers/base.py +++ b/app/problems/analyzers/base.py @@ -1,4 +1,5 @@ -from problems import dto +from problems.dto import ProblemDTO +from problems.analyses.dto import ProblemAnalysisDTO class ProblemAnalyzer: @@ -7,5 +8,5 @@ class ProblemAnalyzer: 문제 데이터를 받아와 문제의 분석 결과를 반환하는 analyze() 메소드를 구현해야 합니다. """ - def analyze(self, problem: dto.ProblemDTO) -> dto.ProblemAnalysisDTO: + def analyze(self, problem: ProblemDTO) -> ProblemAnalysisDTO: raise NotImplementedError diff --git a/app/problems/analyzers/gemini/__init__.py b/app/problems/analyzers/gemini/__init__.py new file mode 100644 index 0000000..8187e73 --- /dev/null +++ b/app/problems/analyzers/gemini/__init__.py @@ -0,0 +1 @@ +from problems.analyzers.gemini.analyzer import GeminiProblemAnalyzer diff --git a/app/problems/services/analyzers/gemini/analyzer.py b/app/problems/analyzers/gemini/analyzer.py similarity index 89% rename from app/problems/services/analyzers/gemini/analyzer.py rename to app/problems/analyzers/gemini/analyzer.py index 82836db..97e33cb 100644 --- a/app/problems/services/analyzers/gemini/analyzer.py +++ b/app/problems/analyzers/gemini/analyzer.py @@ -3,11 +3,11 @@ from django.conf import settings from google import generativeai as genai -from problems.dto import ProblemDTO -from problems.dto import ProblemAnalysisDTO -from problems.services.analyzers.base import ProblemAnalyzer -from problems.services.analyzers.gemini import prompts -from problems.services.analyzers.gemini import parsers +from problems.analyzers.base import ProblemAnalyzer +from problems.analyzers.base import ProblemDTO +from problems.analyzers.base import ProblemAnalysisDTO +from problems.analyzers.gemini import prompts +from problems.analyzers.gemini import parsers logger = getLogger('problems.analyzers.gemini.analyzer') @@ -36,30 +36,36 @@ def __init__(self) -> None: ) def analyze(self, problem_dto: ProblemDTO) -> ProblemAnalysisDTO: - logger.info(f'"{problem_dto.title}" 문제의 분석 시작.') - chat = self.model.start_chat(history=[]) - analysis_dto = ProblemAnalysisDTO( + problem_id=problem_dto.id, difficulty=None, time_complexity=None, tags=None, hints=None, ) + + logger.info(f'"{problem_dto.title}" 문제의 분석 시작.') + chat = self.model.start_chat(history=[]) + logger.info(f'... 태그 분석 중...') prompt = prompts.get_tags_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.tags = parsers.parse_tags(assistant_message) + logger.info(f'... 난이도 분석 중...') prompt = prompts.get_difficulty_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.difficulty = parsers.parse_difficulty(assistant_message) + logger.info(f'... 시간 복잡도 분석 중...') prompt = prompts.get_time_complexity_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.time_complexity = parsers.parse_time_complexity(assistant_message) + logger.info(f'... 힌트 분석 중...') prompt = prompts.get_hints_prompt(problem_dto, analysis_dto) assistant_message = chat.send_message(content=prompt).text analysis_dto.hints = parsers.parse_hints(assistant_message) + logger.info(f'"{problem_dto.title}" 문제의 분석 완료.') return analysis_dto diff --git a/app/problems/services/analyzers/gemini/parsers.py b/app/problems/analyzers/gemini/parsers.py similarity index 97% rename from app/problems/services/analyzers/gemini/parsers.py rename to app/problems/analyzers/gemini/parsers.py index b00945f..5159383 100644 --- a/app/problems/services/analyzers/gemini/parsers.py +++ b/app/problems/analyzers/gemini/parsers.py @@ -6,7 +6,7 @@ from sympy import latex from sympy.parsing.latex import parse_latex -from problems.models import ProblemTag +from problems.analyses.models import ProblemTag logger = getLogger('problems.analyzers.gemini.parsers') diff --git a/app/problems/services/analyzers/gemini/prompts.py b/app/problems/analyzers/gemini/prompts.py similarity index 98% rename from app/problems/services/analyzers/gemini/prompts.py rename to app/problems/analyzers/gemini/prompts.py index 4ac6cb2..f36eb0f 100644 --- a/app/problems/services/analyzers/gemini/prompts.py +++ b/app/problems/analyzers/gemini/prompts.py @@ -1,7 +1,7 @@ from textwrap import dedent -from problems.dto import ProblemDTO -from problems.dto import ProblemAnalysisDTO +from problems.analyzers.base import ProblemDTO +from problems.analyzers.base import ProblemAnalysisDTO def get_difficulty_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: diff --git a/app/problems/analyzers/gpt/__init__.py b/app/problems/analyzers/gpt/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/services/analyzers/gpt/analyzer.py b/app/problems/analyzers/gpt/analyzer.py similarity index 86% rename from app/problems/services/analyzers/gpt/analyzer.py rename to app/problems/analyzers/gpt/analyzer.py index 640663f..2f481ba 100644 --- a/app/problems/services/analyzers/gpt/analyzer.py +++ b/app/problems/analyzers/gpt/analyzer.py @@ -1,5 +1,5 @@ from problems import dto -from problems.services.analyzers.base import ProblemAnalyzer +from problems.analyzers.base import ProblemAnalyzer class GPTProblemAnalyzer(ProblemAnalyzer): diff --git a/app/problems/dto.py b/app/problems/dto.py index 5afa6e1..4bd367b 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -3,10 +3,8 @@ from collections import Counter from dataclasses import dataclass from dataclasses import field -from typing import Tuple -from problems import enums -from problems import models +from problems.analyses.dto import ProblemTagDTO @dataclass @@ -22,44 +20,6 @@ class ProblemDTO: def __str__(self) -> str: return f'' - @classmethod - def from_model(self, instance: models.Problem) -> ProblemDTO: - return ProblemDTO( - id=instance.pk, - title=instance.title, - description=instance.description, - input_description=instance.input_description, - output_description=instance.output_description, - memory_limit=instance.memory_limit, - time_limit=instance.time_limit, - ) - - -@dataclass -class ProblemAnalysisDTO: - time_complexity: str - difficulty: enums.ProblemDifficulty - tags: Tuple[str] = field(default_factory=tuple) - hints: Tuple[str] = field(default_factory=tuple) - - -@dataclass -class ProblemTagDTO: - key: str - name_ko: str - name_en: str - - def __hash__(self) -> int: - return self.key - - @classmethod - def from_model(self, instance: models.ProblemTag) -> ProblemTagDTO: - return ProblemTagDTO( - key=instance.key, - name_ko=instance.name_ko, - name_en=instance.name_en, - ) - @dataclass class ProblemStatisticDTO: diff --git a/app/problems/enums.py b/app/problems/enums.py index 06232e1..1821a3c 100644 --- a/app/problems/enums.py +++ b/app/problems/enums.py @@ -3,36 +3,6 @@ from django.db import models -NAMES = { - 'ko': ['분석 중', '쉬움', '보통', '어려움'], - 'en': ['UNDER ANALYSIS', 'EASY', 'NORMAL', 'HARD'], -} - - -class ProblemDifficulty(models.IntegerChoices): - @staticmethod - def from_label(label: str) -> ProblemDifficulty: - return { - 'EASY': ProblemDifficulty.EASY, - 'NORMAL': ProblemDifficulty.NORMAL, - 'HARD': ProblemDifficulty.HARD, - }[label] - - - UNDER_ANALYSIS = 0, '분석 중' - EASY = 1, '쉬움' - NORMAL = 2, '보통' - HARD = 3, '어려움' - - def get_name(self, lang='ko') -> str: - if lang not in NAMES: - raise ValueError( - f'Invalid language: {lang}, ', - f'choose from {NAMES.keys()}' - ) - return NAMES[lang][self.value] - - class Unit(models.TextChoices): MEGA_BYTE = 'MB', "메가 바이트" SECOND = 's', "초" diff --git a/app/problems/models/problem.py b/app/problems/models.py similarity index 78% rename from app/problems/models/problem.py rename to app/problems/models.py index da94cb1..b4dbc88 100644 --- a/app/problems/models/problem.py +++ b/app/problems/models.py @@ -1,6 +1,7 @@ from django.db import models -from problems import enums +from problems.dto import ProblemDTO +from problems.enums import Unit from users.models import User @@ -30,16 +31,16 @@ class Problem(models.Model): help_text='문제 메모리 제한을 입력해주세요. (MB 단위)', ) memory_limit_unit = models.TextField( - choices=enums.Unit.choices, - default=enums.Unit.MEGA_BYTE, + choices=Unit.choices, + default=Unit.MEGA_BYTE, ) time_limit = models.FloatField( help_text='문제 시간 제한을 입력해주세요. (초 단위)', default=1.0, ) time_limit_unit = models.TextField( - choices=enums.Unit.choices, - default=enums.Unit.SECOND, + choices=Unit.choices, + default=Unit.SECOND, ) created_at = models.DateTimeField(auto_now_add=True) created_by = models.ForeignKey( @@ -69,3 +70,14 @@ class Meta: def __str__(self) -> str: return f'[{self.pk} : {self.title}]' + + def as_dto(self) -> ProblemDTO: + return ProblemDTO( + id=self.pk, + title=self.title, + description=self.description, + input_description=self.input_description, + output_description=self.output_description, + memory_limit=self.memory_limit, + time_limit=self.time_limit, + ) diff --git a/app/problems/models/__init__.py b/app/problems/models/__init__.py deleted file mode 100644 index 2adf4d4..0000000 --- a/app/problems/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from problems.models.problem import Problem -from problems.models.problem_analysis import ProblemAnalysis -from problems.models.problem_analysis_tag import ProblemAnalysisTag -from problems.models.problem_tag import ProblemTag -from problems.models.problem_tag_relation import ProblemTagRelation -from problems.models import signals diff --git a/app/problems/models/problem_analysis.py b/app/problems/models/problem_analysis.py deleted file mode 100644 index 66d05f0..0000000 --- a/app/problems/models/problem_analysis.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import models - -from problems import enums -from problems.models.problem import Problem - - -class ProblemAnalysis(models.Model): - problem = models.ForeignKey( - Problem, - on_delete=models.CASCADE, - help_text='문제를 입력해주세요.', - ) - difficulty = models.IntegerField( - help_text='문제 난이도를 입력해주세요.', - choices=enums.ProblemDifficulty.choices, - ) - time_complexity = models.CharField( - max_length=100, - help_text=( - '문제 시간 복잡도를 입력해주세요. ', - '예) O(1), O(n), O(n^2), O(V \log E) 등', - ), - validators=[ - # TODO: 시간 복잡도 검증 로직 추가 - ], - ) - hint = models.JSONField( - help_text='문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요.', - validators=[ - # TODO: 힌트 검증 로직 추가 - ], - blank=False, - default=list, - ) - created_at = models.DateTimeField(auto_now_add=True) - - class field_name: - PROBLEM = 'problem' - DIFFICULTY = 'difficulty' - TAGS = 'tags' - TIME_COMPLEXITY = 'time_complexity' - HINT = 'hint' - CREATED_AT = 'created_at' - - class Meta: - verbose_name_plural = 'Problem analyses' - ordering = ['-created_at'] - get_latest_by = ['created_at'] - - def __str__(self): - return f'[Analyse of {self.problem}]' \ No newline at end of file diff --git a/app/problems/models/problem_analysis_tag.py b/app/problems/models/problem_analysis_tag.py deleted file mode 100644 index 5c8a347..0000000 --- a/app/problems/models/problem_analysis_tag.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db import models - -from problems.models.problem_analysis import ProblemAnalysis -from problems.models.problem_tag import ProblemTag - - -class ProblemAnalysisTag(models.Model): - analysis = models.ForeignKey( - ProblemAnalysis, - on_delete=models.CASCADE, - null=False, - blank=False, - ) - tag = models.ForeignKey( - ProblemTag, - on_delete=models.PROTECT, - null=False, - blank=False, - help_text='문제의 DSA 태그를 입력해주세요.', - ) - - class field_name: - ANALYSIS = 'analysis' - TAG = 'tag' - - def __str__(self): - return f'{self.analysis.problem} #{self.tag}' diff --git a/app/problems/models/problem_tag.py b/app/problems/models/problem_tag.py deleted file mode 100644 index a6cef71..0000000 --- a/app/problems/models/problem_tag.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db import models - - -class ProblemTag(models.Model): - key = models.CharField( - max_length=50, - unique=True, - help_text='알고리즘 태그 키를 입력해주세요. (최대 20자)', - ) - name_ko = models.CharField( - max_length=50, - unique=True, - help_text='알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)', - ) - name_en = models.CharField( - max_length=50, - unique=True, - help_text='알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)', - ) - - class field_name: - KEY = 'key' - NAME_KO = 'name_ko' - NAME_EN = 'name_en' - - class Meta: - ordering = ['key'] - - def __repr__(self) -> str: - return f'[#{self.key}]' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()} ({self.name_ko})' diff --git a/app/problems/models/problem_tag_relation.py b/app/problems/models/problem_tag_relation.py deleted file mode 100644 index 53238b6..0000000 --- a/app/problems/models/problem_tag_relation.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.db import models - -from problems.models.problem_tag import ProblemTag - - -class ProblemTagRelation(models.Model): - parent = models.ForeignKey( - ProblemTag, - on_delete=models.CASCADE, - related_name='parent' - ) - child = models.ForeignKey( - ProblemTag, - on_delete=models.CASCADE, - related_name='child' - ) - - class field_name: - PARENT = 'parent' - CHILD = 'child' - - def __str__(self) -> str: - return f'{self.pk} : #{self.parent.key} <- #{self.child.key}' diff --git a/app/problems/models/signals.py b/app/problems/models/signals.py deleted file mode 100644 index e379fad..0000000 --- a/app/problems/models/signals.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from problems import models -from problems import services - - -@receiver(post_save, sender=models.Problem) -def schedule_analyze(sender, instance: models.Problem, **kwargs): - service = services.get_problem_service(instance) - service.analyze() diff --git a/app/problems/serializers.py b/app/problems/serializers.py new file mode 100644 index 0000000..7295c48 --- /dev/null +++ b/app/problems/serializers.py @@ -0,0 +1,183 @@ +from typing import Optional + +from rest_framework import serializers + +from problems import dto +from problems import models +from problems.analyses.enums import ProblemDifficulty +from problems.analyses.models import ProblemAnalysis +from problems.analyses.serializers import ProblemAnalysisSerializer +from problems.analyses.serializers import ProblemAnalysisDifficultyField +from problems.dto import ProblemStatisticDTO +from problems.enums import Unit +from problems.models import Problem +from users.serializers import UserMinimalSerializer + + +PK = 'id' + + +class AnalysisSerializer(ProblemAnalysisSerializer): + def to_representation(self, analysis: Optional[ProblemAnalysis]): + if analysis is None: + return { + 'is_analyzed': False, + } + else: + return { + 'is_analyzed': True, + **super().to_representation(analysis), + } + + def get_attribute(self, instance: Problem) -> Optional[ProblemAnalysis]: + assert isinstance(instance, Problem) + try: + return ProblemAnalysis.objects.get_by_problem(instance) + except ProblemAnalysis.DoesNotExist: + return None + + +class ProblemLimitsField(serializers.SerializerMethodField): + def to_representation(self, problem: models.Problem): + assert isinstance(problem, models.Problem) + return { + "memory": { + "value": problem.memory_limit, + "unit": UnitSerializer(Unit(problem.memory_limit_unit)).data, + }, + "time_limit": { + "value": problem.time_limit, + "unit": UnitSerializer(Unit(problem.time_limit_unit)).data, + }, + } + + +class ProblemDifficultyField(ProblemAnalysisDifficultyField): + def get_attribute(self, instance: Problem) -> ProblemDifficulty: + try: + analysis = ProblemAnalysis.objects.get_by_problem(instance) + except ProblemAnalysis.DoesNotExist: + return ProblemDifficulty.UNDER_ANALYSIS + else: + return ProblemDifficulty(analysis.difficulty) + + +class ProblemStatisticsDifficultyField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatisticDTO): + assert isinstance(statistics, dto.ProblemStatisticDTO) + try: + ratio_denominator = 1 / statistics.sample_count + except ZeroDivisionError: + ratio_denominator = 0 + finally: + return [ + { + 'difficulty': difficulty, + 'problem_count': count, + 'ratio': count * ratio_denominator, + } + for difficulty, count in statistics.difficulty.items() + ] + + +class ProblemStatisticsTagsField(serializers.SerializerMethodField): + def to_representation(self, statistics: dto.ProblemStatisticDTO): + assert isinstance(statistics, dto.ProblemStatisticDTO) + try: + ratio_denominator = 1 / statistics.sample_count + except ZeroDivisionError: + ratio_denominator = 0 + finally: + return [ + { + 'label': { + 'ko': tag.name_ko, + 'en': tag.name_en, + }, + 'problem_count': count, + 'ratio': count * ratio_denominator, + } + for tag, count in statistics.tags.items() + ] + + +class ProblemCreateSerializer(serializers.ModelSerializer): + class Meta: + model = models.Problem + fields = [ + models.Problem.field_name.TITLE, + models.Problem.field_name.LINK, + models.Problem.field_name.DESCRIPTION, + models.Problem.field_name.INPUT_DESCRIPTION, + models.Problem.field_name.OUTPUT_DESCRIPTION, + models.Problem.field_name.MEMORY_LIMIT, + models.Problem.field_name.TIME_LIMIT, + ] + + +class ProblemDetailSerializer(serializers.ModelSerializer): + analysis = AnalysisSerializer() + limits = ProblemLimitsField() + created_by = UserMinimalSerializer() + + class Meta: + model = models.Problem + fields = [ + PK, + models.Problem.field_name.TITLE, + models.Problem.field_name.LINK, + 'limits', + models.Problem.field_name.DESCRIPTION, + models.Problem.field_name.INPUT_DESCRIPTION, + models.Problem.field_name.OUTPUT_DESCRIPTION, + models.Problem.field_name.MEMORY_LIMIT, + models.Problem.field_name.TIME_LIMIT, + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.UPDATED_AT, + 'created_by', + 'analysis', + ] + extra_kwargs = { + PK: {'read_only': True}, + models.Problem.field_name.MEMORY_LIMIT: {'write_only': True}, + models.Problem.field_name.TIME_LIMIT: {'write_only': True}, + models.Problem.field_name.CREATED_AT: {'read_only': True}, + models.Problem.field_name.UPDATED_AT: {'read_only': True}, + 'created_by': { + 'read_only': True, + 'default': serializers.CurrentUserDefault(), + } + } + + +class ProblemMinimalSerializer(serializers.ModelSerializer): + difficulty = ProblemDifficultyField() + + class Meta: + model = models.Problem + fields = [ + 'id', + models.Problem.field_name.TITLE, + 'difficulty', + models.Problem.field_name.CREATED_AT, + models.Problem.field_name.UPDATED_AT, + ] + read_only_fields = ['__all__'] + + +class ProblemStatisticSerializer(serializers.Serializer): + difficulty = ProblemStatisticsDifficultyField() + tags = ProblemStatisticsTagsField() + + def __init__(self, instance: ProblemStatisticDTO, **kwargs): + assert isinstance(instance, ProblemStatisticDTO) + super().__init__(instance, **kwargs) + + +class UnitSerializer(serializers.Serializer): + def to_representation(self, unit: Unit): + assert isinstance(unit, Unit) + return { + 'name': unit.label, + 'value': unit.value, + } diff --git a/app/problems/serializers/__init__.py b/app/problems/serializers/__init__.py deleted file mode 100644 index 676f2c2..0000000 --- a/app/problems/serializers/__init__.py +++ /dev/null @@ -1,87 +0,0 @@ -from rest_framework import serializers - -from problems import dto -from problems import models -from problems.serializers import fields -from users.serializers import UserMinimalSerializer - - -class ProblemCreateSerializer(serializers.ModelSerializer): - class Meta: - model = models.Problem - fields = [ - models.Problem.field_name.TITLE, - models.Problem.field_name.LINK, - models.Problem.field_name.DESCRIPTION, - models.Problem.field_name.INPUT_DESCRIPTION, - models.Problem.field_name.OUTPUT_DESCRIPTION, - models.Problem.field_name.MEMORY_LIMIT, - models.Problem.field_name.TIME_LIMIT, - ] - - -class ProblemDetailSerializer(serializers.ModelSerializer): - analysis = fields.AnalysisField(read_only=True) - memory_limit = fields.MemoryLimitField(read_only=True) - time_limit = fields.TimeLimitField(read_only=True) - created_by = UserMinimalSerializer(read_only=True) - - class Meta: - model = models.Problem - fields = [ - 'id', - 'analysis', - 'memory_limit', - 'time_limit', - models.Problem.field_name.TITLE, - models.Problem.field_name.LINK, - models.Problem.field_name.DESCRIPTION, - models.Problem.field_name.INPUT_DESCRIPTION, - models.Problem.field_name.OUTPUT_DESCRIPTION, - models.Problem.field_name.MEMORY_LIMIT, - models.Problem.field_name.TIME_LIMIT, - models.Problem.field_name.CREATED_AT, - models.Problem.field_name.CREATED_BY, - models.Problem.field_name.UPDATED_AT, - ] - read_only_fields = [ - 'id', - 'analysis', - 'memory_limit', - 'time_limit', - models.Problem.field_name.CREATED_AT, - models.Problem.field_name.CREATED_BY, - models.Problem.field_name.UPDATED_AT, - ] - extra_kwargs = { - models.Problem.field_name.MEMORY_LIMIT: {'write_only': True}, - models.Problem.field_name.TIME_LIMIT: {'write_only': True}, - } - - def create(self, validated_data): - validated_data[models.Problem.field_name.CREATED_BY] = serializers.CurrentUserDefault()(self) - return super().create(validated_data) - - -class ProblemMinimalSerializer(serializers.ModelSerializer): - difficulty = fields.DifficultyField(read_only=True) - - class Meta: - model = models.Problem - fields = [ - 'id', - models.Problem.field_name.TITLE, - 'difficulty', - models.Problem.field_name.CREATED_AT, - models.Problem.field_name.UPDATED_AT, - ] - read_only_fields = ['__all__'] - - -class ProblemStatisticSerializer(serializers.Serializer): - difficulty = fields.ProblemStatisticsDifficultyField() - tags = fields.ProblemStatisticsTagsField() - - def __init__(self, instance: dto.ProblemStatisticDTO, **kwargs): - assert isinstance(instance, dto.ProblemStatisticDTO) - super().__init__(instance, **kwargs) diff --git a/app/problems/serializers/fields.py b/app/problems/serializers/fields.py deleted file mode 100644 index 23c5b83..0000000 --- a/app/problems/serializers/fields.py +++ /dev/null @@ -1,108 +0,0 @@ -from rest_framework import serializers - -from problems import dto -from problems import enums -from problems import models -from problems import services - - -class MemoryLimitField(serializers.SerializerMethodField): - def to_representation(self, problem: models.Problem): - assert isinstance(problem, models.Problem) - unit = enums.Unit(problem.memory_limit_unit) - return { - "value": problem.memory_limit, - "unit": { - "name": unit.label, - "abbr": unit.value, - }, - } - - -class TimeLimitField(serializers.SerializerMethodField): - def to_representation(self, problem: models.Problem): - assert isinstance(problem, models.Problem) - unit = enums.Unit(problem.time_limit_unit) - return { - "value": problem.time_limit, - "unit": { - "name": unit.label, - "abbr": unit.value, - }, - } - - -class DifficultyField(serializers.SerializerMethodField): - def to_representation(self, problem: models.Problem): - assert isinstance(problem, models.Problem) - service = services.get_problem_service(problem) - return { - "name_ko": service.difficulty().get_name(lang='ko'), - "name_en": service.difficulty().get_name(lang='en'), - 'value': service.difficulty().value, - } - - -class AnalysisField(serializers.SerializerMethodField): - def to_representation(self, problem: models.Problem): - assert isinstance(problem, models.Problem) - service = services.get_problem_service(problem) - return { - 'difficulty': { - "name_ko": service.difficulty().get_name(lang='ko'), - "name_en": service.difficulty().get_name(lang='en'), - 'value': service.difficulty().value, - }, - 'time_complexity': { - 'value': service.time_complexity(), - }, - 'hints': service.hints(), - 'tags': [ - { - 'key': tag.key, - 'name_en': tag.name_en, - 'name_ko': tag.name_ko, - } - for tag in service.query_tags() - ], - 'is_analyzed': service.is_analyzed(), - } - - -class ProblemStatisticsDifficultyField(serializers.SerializerMethodField): - def to_representation(self, statistics: dto.ProblemStatisticDTO): - assert isinstance(statistics, dto.ProblemStatisticDTO) - try: - ratio_denominator = 1 / statistics.sample_count - except ZeroDivisionError: - ratio_denominator = 0 - finally: - return [ - { - 'difficulty': difficulty, - 'problem_count': count, - 'ratio': count * ratio_denominator, - } - for difficulty, count in statistics.difficulty.items() - ] - - -class ProblemStatisticsTagsField(serializers.SerializerMethodField): - def to_representation(self, statistics: dto.ProblemStatisticDTO): - assert isinstance(statistics, dto.ProblemStatisticDTO) - try: - ratio_denominator = 1 / statistics.sample_count - except ZeroDivisionError: - ratio_denominator = 0 - finally: - return [ - { - 'label': { - 'ko': tag.name_ko, - 'en': tag.name_en, - }, - 'problem_count': count, - 'ratio': count * ratio_denominator, - } - for tag, count in statistics.tags.items() - ] diff --git a/app/problems/services/__init__.py b/app/problems/services/__init__.py deleted file mode 100644 index a7dddf2..0000000 --- a/app/problems/services/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.db.models import QuerySet - -from problems import dto -from problems import models -from problems.services.base import ProblemService -from problems.services.concrete import ConcreteProblemService - - -def get_problem_service(problem: models.Problem) -> ProblemService: - return ConcreteProblemService(problem) - - -def get_problems_statistics(queryset: QuerySet[models.Problem]) -> dto.ProblemStatisticDTO: - stat = dto.ProblemStatisticDTO() - for problem in queryset: - service = get_problem_service(problem) - for tag in service.query_tags(): - stat.tags[dto.ProblemTagDTO.from_model(tag)] += 1 - stat.difficulty[service.difficulty()] += 1 - stat.sample_count += 1 - return stat diff --git a/app/problems/services/analyzers/__init__.py b/app/problems/services/analyzers/__init__.py deleted file mode 100644 index ac6290f..0000000 --- a/app/problems/services/analyzers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from problems.services.analyzers.base import ProblemAnalyzer -from problems.services.analyzers.gemini import GeminiProblemAnalyzer - - -def get_analyzer() -> ProblemAnalyzer: - return GeminiProblemAnalyzer.get_instance() diff --git a/app/problems/services/analyzers/gemini/__init__.py b/app/problems/services/analyzers/gemini/__init__.py deleted file mode 100644 index c6f0f8a..0000000 --- a/app/problems/services/analyzers/gemini/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from problems.services.analyzers.gemini.analyzer import GeminiProblemAnalyzer diff --git a/app/problems/services/base.py b/app/problems/services/base.py deleted file mode 100644 index 49d4ccd..0000000 --- a/app/problems/services/base.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -from typing import List -from typing import Optional - -from django.db.models import QuerySet - -from problems import enums -from problems import models - - -class ProblemService: - def __init__(self, instance: models.Problem) -> None: - assert isinstance(instance, models.Problem) - self.instance = instance - - def query_analyses(self) -> QuerySet[models.ProblemAnalysis]: - raise NotImplementedError - - def query_tags(self) -> QuerySet[models.ProblemTag]: - raise NotImplementedError - - def get_analysis(self) -> Optional[models.ProblemAnalysis]: - """ - raises: - ProblemAnalysis.DoesNotExists - """ - raise NotImplementedError - - def is_analyzed(self) -> bool: - raise NotImplementedError - - def analyze(self) -> None: - """문제를 분석합니다. - - 오래 걸리는 작업인 만큼 본 함수는 분석 작업을 예약만 하고, - 실제 분석은 비동기적으로 진행됩니다.""" - raise NotImplementedError - - def difficulty(self) -> enums.ProblemDifficulty: - raise NotImplementedError - - def time_complexity(self) -> str: - raise NotImplementedError - - def tags(self) -> List[str]: - raise NotImplementedError - - def hints(self) -> List[str]: - raise NotImplementedError diff --git a/app/problems/services/concrete.py b/app/problems/services/concrete.py deleted file mode 100644 index 8b23825..0000000 --- a/app/problems/services/concrete.py +++ /dev/null @@ -1,132 +0,0 @@ -from logging import getLogger -from typing import List -from typing import Optional - -from background_task import background -from django.db.models import QuerySet -from django.db.transaction import atomic - -from problems import dto -from problems import enums -from problems import models -from problems.services.base import ProblemService -from problems.services.analyzers import ProblemAnalyzer -from problems.services.analyzers import GeminiProblemAnalyzer - - -logger = getLogger('problems.services') - - -class ConcreteProblemService(ProblemService): - def __init__(self, instance: models.Problem) -> None: - super().__init__(instance) - self._analysis = None - - def query_analyses(self) -> QuerySet[models.ProblemAnalysis]: - return models.ProblemAnalysis.objects.filter(**{ - models.ProblemAnalysis.field_name.PROBLEM: self.instance, - }) - - def query_analysis_tags(self) -> QuerySet[models.ProblemAnalysisTag]: - if not self.is_analyzed(): - return models.ProblemAnalysisTag.objects.none() - else: - return models.ProblemAnalysisTag.objects.filter(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: self.get_analysis(), - }) - - def query_tags(self) -> QuerySet[models.ProblemTag]: - return models.ProblemTag.objects.filter(**{ - models.ProblemTag.field_name.KEY+'__in': self.tags(), - }) - - def get_analysis(self) -> Optional[models.ProblemAnalysis]: - try: - self._analysis = self.query_analyses().latest() - except models.ProblemAnalysis.DoesNotExist: - self._analysis = None - finally: - return self._analysis - - def is_analyzed(self) -> bool: - return self.get_analysis() is not None - - def analyze(self) -> None: - schedule_analyze(self.instance.pk) - - def difficulty(self) -> enums.ProblemDifficulty: - if (analysis := self.get_analysis()) is None: - return enums.ProblemDifficulty.UNDER_ANALYSIS - return enums.ProblemDifficulty(analysis.difficulty) - - def time_complexity(self) -> str: - if (analysis := self.get_analysis()) is None: - return '?' - return analysis.time_complexity - - def tags(self) -> List[str]: - return self.query_analysis_tags().select_related( - models.ProblemAnalysisTag.field_name.TAG, - ).values_list(models.ProblemAnalysisTag.field_name.TAG+'__'+models.ProblemTag.field_name.KEY) - - def hints(self) -> List[str]: - if (analysis := self.get_analysis()) is None: - return [] - return analysis.hint - - -@background -def schedule_analyze(problem_id: int): - logger.info(f'PK={problem_id} 문제의 분석 준비중.') - problem = get_problem(problem_id) - problem_dto = dto.ProblemDTO.from_model(problem) - logger.info('문제 분석기를 불러오는 중.') - analyzer = get_analyzer() - logger.info(f'{problem_dto} 문제의 분석 시작.') - analysis_dto = analyzer.analyze(problem_dto) - logger.info(f'{problem_dto} 문제의 분석 완료.') - logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장하는 중.') - save_analysis(problem, analysis_dto) - logger.info(f'{problem_dto} 문제의 분석 결과를 데이터베이스에 저장 완료.') - - -def get_analyzer() -> ProblemAnalyzer: - return GeminiProblemAnalyzer.get_instance() - - -def get_problem(problem_id: int) -> models.Problem: - try: - return models.Problem.objects.get(pk=problem_id) - except models.Problem.DoesNotExist: - logger.warning(f'id가 {problem_id}인 문제를 찾을 수 없습니다.') - - -def save_analysis(problem: models.Problem, analysis_dto: dto.ProblemAnalysisDTO) -> models.ProblemAnalysis: - analysis = models.ProblemAnalysis(**{ - models.ProblemAnalysis.field_name.PROBLEM: problem, - models.ProblemAnalysis.field_name.TIME_COMPLEXITY: analysis_dto.time_complexity, - models.ProblemAnalysis.field_name.DIFFICULTY: enums.ProblemDifficulty.from_label(analysis_dto.difficulty), - models.ProblemAnalysis.field_name.HINT: analysis_dto.hints, - }) - analysis_tags = [] - for tag_key in analysis_dto.tags: - try: - tag = models.ProblemTag.objects.get(**{ - models.ProblemTag.field_name.KEY: tag_key, - }) - except models.ProblemTag.DoesNotExist: - logger.warn(f'문제 분석 결과에 알 수 없는 태그 [{tag_key}] 가 포함된 것을 발견하였습니다.') - tag = models.ProblemTag.objects.create(**{ - models.ProblemTag.field_name.KEY: tag_key, - models.ProblemTag.field_name.NAME_KO: f'존재하지 않는 태그: [{tag_key}]', - models.ProblemTag.field_name.NAME_EN: f'존재하지 않는 태그: [{tag_key}]', - }) - finally: - analysis_tag = models.ProblemAnalysisTag(**{ - models.ProblemAnalysisTag.field_name.ANALYSIS: analysis, - models.ProblemAnalysisTag.field_name.TAG: tag, - }) - analysis_tags.append(analysis_tag) - with atomic(): - analysis.save() - models.ProblemAnalysisTag.objects.bulk_create(analysis_tags) From a4cbb26599fc8f6885be135817885ff0881f71dd Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 29 Aug 2024 22:47:36 +0900 Subject: [PATCH 451/552] chore(problems.dto): remove unncessary `__futre__` import --- app/problems/dto.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/problems/dto.py b/app/problems/dto.py index 4bd367b..a445f06 100644 --- a/app/problems/dto.py +++ b/app/problems/dto.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from collections import Counter from dataclasses import dataclass from dataclasses import field From 60be5b0600f4963a066804af46a7eeb41f96ef28 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Thu, 29 Aug 2024 22:56:13 +0900 Subject: [PATCH 452/552] feat(problems.statistics): add `create_statistics()` --- app/problems/statistics.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/problems/statistics.py diff --git a/app/problems/statistics.py b/app/problems/statistics.py new file mode 100644 index 0000000..856bdfb --- /dev/null +++ b/app/problems/statistics.py @@ -0,0 +1,22 @@ +from typing import Iterable + +from problems.models import Problem +from problems.analyses.enums import ProblemDifficulty +from problems.analyses.models import ProblemAnalysis +from problems.analyses.models import ProblemAnalysisTag +from problems.dto import ProblemStatisticDTO + + +def create_statistics(problems: Iterable[Problem]) -> ProblemStatisticDTO: + stat = ProblemStatisticDTO() + for problem in problems: + stat.sample_count += 1 + try: + analysis = ProblemAnalysis.objects.get_by_problem(problem) + except ProblemAnalysis.DoesNotExist: + stat.difficulty[ProblemDifficulty.UNDER_ANALYSIS] += 1 + else: + stat.difficulty[ProblemDifficulty(analysis.difficulty)] += 1 + for analysis_tag in ProblemAnalysisTag.objects.analysis(analysis).select_related(ProblemAnalysisTag.field_name.TAG): + stat.tags[analysis_tag.tag.as_dto()] += 1 + return stat From f75d2e51ff77e22f7e6ce2b00c9270e9867a1d5c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 02:40:01 +0900 Subject: [PATCH 453/552] =?UTF-8?q?refactor(crews):=20activities,=20applic?= =?UTF-8?q?ations=20=EB=AA=A8=EB=93=88=EB=A1=9C=20=EB=B6=84=EB=A6=AC=20(?= =?UTF-8?q?=EB=8C=80=EA=B7=9C=EB=AA=A8=20=EB=A6=AC=ED=8C=A9=ED=86=A0?= =?UTF-8?q?=EB=A7=81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/__init__.py | 3 + app/config/urls.py | 2 + app/crews/activities/__init__.py | 0 app/crews/activities/admin.py | 48 +++ app/crews/activities/apps.py | 7 + app/crews/activities/dto.py | 41 +++ app/crews/activities/migrations/__init__.py | 0 app/crews/activities/models.py | 189 ++++++++++++ app/crews/activities/serializers.py | 31 ++ app/crews/activities/views.py | 14 + app/crews/admin.py | 155 +++------- app/crews/applications/__init__.py | 0 app/crews/applications/admin.py | 32 ++ app/crews/applications/apps.py | 7 + app/crews/applications/migrations/__init__.py | 0 .../models.py} | 21 +- app/crews/applications/permissions.py | 10 + app/crews/applications/serializers.py | 61 ++++ app/crews/applications/services.py | 132 +++++++++ app/crews/applications/signals.py | 4 + app/crews/applications/views.py | 96 ++++++ app/crews/dto.py | 63 +--- app/crews/models.py | 276 ++++++++++++++++++ app/crews/models/__init__.py | 6 - app/crews/models/crew.py | 88 ------ app/crews/models/crew_activity.py | 37 --- app/crews/models/crew_activity_problem.py | 59 ---- app/crews/models/crew_activity_submission.py | 51 ---- app/crews/models/crew_member.py | 50 ---- app/crews/models/crew_submittable_language.py | 25 -- app/crews/permissions.py | 42 +-- app/crews/serializers.py | 246 ++++++++++++++++ .../__init__ copy.py} | 46 ++- app/crews/serializersaaa/__init__.py | 33 +++ .../{serializers => serializersaaa}/fields.py | 67 ++--- .../services/crew_application_service.py | 43 --- app/crews/{services => servicesa}/__init__.py | 14 +- app/crews/{services => servicesa}/base.py | 47 +-- app/crews/servicesa/concrete.py | 189 ++++++++++++ .../crew_activity_service.py | 34 --- .../{services => servicesa}/crew_service.py | 4 +- app/crews/servicesa/dto.py | 67 +++++ app/crews/tests/__init__.py | 89 ------ app/crews/urls.py | 33 ++- app/crews/utils.py | 4 - app/crews/views.py | 159 +++------- 46 files changed, 1673 insertions(+), 952 deletions(-) create mode 100644 app/crews/activities/__init__.py create mode 100644 app/crews/activities/admin.py create mode 100644 app/crews/activities/apps.py create mode 100644 app/crews/activities/dto.py create mode 100644 app/crews/activities/migrations/__init__.py create mode 100644 app/crews/activities/models.py create mode 100644 app/crews/activities/serializers.py create mode 100644 app/crews/activities/views.py create mode 100644 app/crews/applications/__init__.py create mode 100644 app/crews/applications/admin.py create mode 100644 app/crews/applications/apps.py create mode 100644 app/crews/applications/migrations/__init__.py rename app/crews/{models/crew_application.py => applications/models.py} (77%) create mode 100644 app/crews/applications/permissions.py create mode 100644 app/crews/applications/serializers.py create mode 100644 app/crews/applications/services.py create mode 100644 app/crews/applications/signals.py create mode 100644 app/crews/applications/views.py create mode 100644 app/crews/models.py delete mode 100644 app/crews/models/__init__.py delete mode 100644 app/crews/models/crew.py delete mode 100644 app/crews/models/crew_activity.py delete mode 100644 app/crews/models/crew_activity_problem.py delete mode 100644 app/crews/models/crew_activity_submission.py delete mode 100644 app/crews/models/crew_member.py delete mode 100644 app/crews/models/crew_submittable_language.py create mode 100644 app/crews/serializers.py rename app/crews/{serializers/__init__.py => serializersaaa/__init__ copy.py} (75%) create mode 100644 app/crews/serializersaaa/__init__.py rename app/crews/{serializers => serializersaaa}/fields.py (70%) delete mode 100644 app/crews/services/crew_application_service.py rename app/crews/{services => servicesa}/__init__.py (54%) rename app/crews/{services => servicesa}/base.py (61%) create mode 100644 app/crews/servicesa/concrete.py rename app/crews/{services => servicesa}/crew_activity_service.py (56%) rename app/crews/{services => servicesa}/crew_service.py (98%) create mode 100644 app/crews/servicesa/dto.py delete mode 100644 app/crews/tests/__init__.py delete mode 100644 app/crews/utils.py diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py index 48c5883..03b0a5a 100644 --- a/app/config/settings/base/__init__.py +++ b/app/config/settings/base/__init__.py @@ -55,6 +55,9 @@ 'rest_framework_simplejwt', "boj", + "crews", + "crews.activities", + "crews.applications", "users", "problems", "problems.analyses", diff --git a/app/config/urls.py b/app/config/urls.py index 2ea3025..3c62116 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -6,6 +6,7 @@ from drf_yasg.views import get_schema_view from rest_framework import permissions +import crews.urls import problems.urls import users.urls @@ -25,6 +26,7 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ + *crews.urls.urlpatterns, *users.urls.urlpatterns, *problems.urls.urlpatterns, ])), diff --git a/app/crews/activities/__init__.py b/app/crews/activities/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crews/activities/admin.py b/app/crews/activities/admin.py new file mode 100644 index 0000000..2de99d9 --- /dev/null +++ b/app/crews/activities/admin.py @@ -0,0 +1,48 @@ +from django.contrib import admin + +from crews.models import Crew +from crews.activities.models import CrewActivity +from crews.activities.models import CrewActivityProblem +from crews.activities.models import CrewActivitySubmission + + +admin.site.register([ + CrewActivityProblem, + CrewActivitySubmission, +]) + + +@admin.register(CrewActivity) +class CrewActivityModelAdmin(admin.ModelAdmin): + list_display = [ + CrewActivity.field_name.CREW, + CrewActivity.field_name.NAME, + CrewActivity.field_name.START_AT, + CrewActivity.field_name.END_AT, + 'nth', + 'is_in_progress', + 'has_started', + 'has_ended', + ] + search_fields = [ + CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, + CrewActivity.field_name.NAME, + ] + + @admin.display(description='회차 번호') + def nth(self, obj: CrewActivity) -> int: + for nth, activity in enumerate(CrewActivity.objects.filter(crew=obj.crew), start=1): + if activity == obj: + return nth + + @admin.display(boolean=True, description='진행 중') + def is_in_progress(self, obj: CrewActivity) -> bool: + return obj.is_in_progress() + + @admin.display(boolean=True, description='시작 됨') + def has_started(self, obj: CrewActivity) -> bool: + return obj.has_started() + + @admin.display(boolean=True, description='종료 됨') + def has_ended(self, obj: CrewActivity) -> bool: + return obj.has_ended() diff --git a/app/crews/activities/apps.py b/app/crews/activities/apps.py new file mode 100644 index 0000000..9362c54 --- /dev/null +++ b/app/crews/activities/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CrewActivitiesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "crews.activities" + verbose_name = "Crew activities" diff --git a/app/crews/activities/dto.py b/app/crews/activities/dto.py new file mode 100644 index 0000000..335f9aa --- /dev/null +++ b/app/crews/activities/dto.py @@ -0,0 +1,41 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import List + +from crews import enums +from problems.analyses.enums import ProblemDifficulty + + +@dataclass +class CrewProblem: + problem_number: int + problem_id: int + problem_title: str + problem_difficulty: ProblemDifficulty + is_submitted: bool + last_submitted_date: datetime + + +@dataclass +class CrewActivity: + activity_id: int + name: str + start_at: datetime + end_at: datetime + is_in_progress: bool + has_started: bool + has_ended: bool + + +@dataclass +class SubmissionGraphNode: + problem_number: int + submitted_at: datetime + is_accepted: bool # 정답인지 여부 + + +@dataclass +class SubmissionGraph: + user_username: str + user_profile_image: str + submissions: List[SubmissionGraphNode] diff --git a/app/crews/activities/migrations/__init__.py b/app/crews/activities/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crews/activities/models.py b/app/crews/activities/models.py new file mode 100644 index 0000000..51e80b1 --- /dev/null +++ b/app/crews/activities/models.py @@ -0,0 +1,189 @@ +from __future__ import annotations + +from typing import Union + +from django.core.validators import MinValueValidator +from django.db import models +from django.utils import timezone + +from crews.enums import ProgrammingLanguageChoices +from crews.models import Crew +from problems.models import Problem +from users.models import User + + +class CrewActivityManager(models.Manager): + def filter(self, + crew: Crew = None, + *args, + **kwargs) -> models.QuerySet[CrewActivity]: + if crew is not None: + assert isinstance(crew, Crew) + kwargs[CrewActivity.field_name.CREW] = crew + return super().filter(*args, **kwargs) + + def crew(self, crew: Crew) -> _CrewActivityManager: + return self.filter(**{CrewActivity.field_name.CREW: crew}) + + def in_progress(self) -> _CrewActivityManager: + return self.filter(**{ + CrewActivity.field_name.START_AT + '__lte': timezone.now(), + CrewActivity.field_name.END_AT + '__gt': timezone.now(), + }) + + def has_started(self) -> _CrewActivityManager: + return self.filter(**{ + CrewActivity.field_name.START_AT + '__lte': timezone.now(), + }) + + +class CrewActivity(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + help_text='크루를 입력해주세요.', + ) + name = models.TextField( + help_text='활동 이름을 입력해주세요. (예: "1회차")', + ) + start_at = models.DateTimeField( + help_text='활동 시작 일자를 입력해주세요.', + ) + end_at = models.DateTimeField( + help_text='활동 종료 일자를 입력해주세요.', + ) + + objects: _CrewActivityManager = CrewActivityManager() + + class field_name: + CREW = 'crew' + NAME = 'name' + START_AT = 'start_at' + END_AT = 'end_at' + + class Meta: + ordering = ['start_at'] + get_latest_by = ['end_at'] + + def __str__(self) -> str: + return f'[{self.pk}: "{self.name}"@"{self.crew.display_name()}" ({self.start_at.date()} ~ {self.end_at.date()})]' + + def is_in_progress(self) -> bool: + return self.has_started() and not self.has_ended() + + def has_started(self) -> bool: + return self.start_at <= timezone.now() + + def has_ended(self) -> bool: + return self.end_at < timezone.now() + + +class CrewActivityProblemManager(models.Manager): + def crew(self, crew: Crew) -> _CrewActivityProblemManager: + return self.filter(**{CrewActivityProblem.field_name.CREW: crew}) + + +class CrewActivityProblem(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + blank=False, + null=False, + ) + activity = models.ForeignKey( + CrewActivity, + on_delete=models.CASCADE, + help_text='활동을 입력해주세요.', + ) + problem = models.ForeignKey( + Problem, + on_delete=models.PROTECT, + help_text='문제를 입력해주세요.', + ) + order = models.IntegerField( + help_text='문제 순서를 입력해주세요.', + validators=[ + MinValueValidator(1), + ], + ) + + objects: _CrewActivityProblemManager = CrewActivityProblemManager() + + class field_name: + # related fields + SUBMISSIONS = 'submissions' + # fields + CREW = 'crew' + ACTIVITY = 'activity' + PROBLEM = 'problem' + ORDER = 'order' + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['activity', 'order'], + name='unique_order_per_activity_problem', + ), + ] + ordering = ['order'] + + def save(self, *args, **kwargs) -> None: + assert self.crew == self.activity.crew + return super().save(*args, **kwargs) + + def __repr__(self) -> str: + return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' + + def __str__(self) -> str: + return f'{self.pk} : {self.__repr__()}' + + +class CrewActivitySubmission(models.Model): + # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 + problem = models.ForeignKey( + CrewActivityProblem, + on_delete=models.PROTECT, + help_text='활동 문제를 입력해주세요.', + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text='유저를 입력해주세요.', + ) + code = models.TextField( + help_text='유저의 코드를 입력해주세요.', + ) + language = models.TextField( + choices=ProgrammingLanguageChoices.choices, + help_text='유저의 코드 언어를 입력해주세요.', + ) + is_correct = models.BooleanField( + help_text='유저의 코드가 정답인지 여부를 입력해주세요.', + ) + is_help_needed = models.BooleanField( + help_text='유저의 코드에 도움이 필요한지 여부를 입력해주세요.', + default=False, + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class field_name: + PROBLEM = 'problem' + USER = 'user' + CODE = 'code' + LANGUAGE = 'language' + IS_CORRECT = 'is_correct' + IS_HELP_NEEDED = 'is_help_needed' + CREATED_AT = 'created_at' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['created_at'] + + def __str__(self) -> str: + return f'[{self.pk} : {self.problem} ← {self.user}]' + + +_CrewActivityManager = Union[CrewActivityManager, models.Manager[CrewActivity]] +_CrewActivityProblemManager = Union[CrewActivityProblemManager, + models.Manager[CrewActivityProblem]] diff --git a/app/crews/activities/serializers.py b/app/crews/activities/serializers.py new file mode 100644 index 0000000..d53a28b --- /dev/null +++ b/app/crews/activities/serializers.py @@ -0,0 +1,31 @@ +from rest_framework import serializers + +from crews.activities.models import CrewActivity + + +PK = 'id' + + +class CrewActivitySerializer(serializers.ModelSerializer): + class Meta: + model = CrewActivity + fields = [ + CrewActivity.field_name.NAME, + CrewActivity.field_name.START_AT, + CrewActivity.field_name.END_AT, + ] + read_only_fields = ['__all__'] + + +# class CrewActivityProblemSerializer(serializers.ModelSerializer): +# problems = fields.CrewAcitivityProblemsField() + +# class Meta: +# model = CrewActivity +# fields = [ +# CrewActivity.field_name.NAME, +# CrewActivity.field_name.START_AT, +# CrewActivity.field_name.END_AT, +# 'problems', +# ] +# read_only_fields = ['__all__'] diff --git a/app/crews/activities/views.py b/app/crews/activities/views.py new file mode 100644 index 0000000..6e2be4c --- /dev/null +++ b/app/crews/activities/views.py @@ -0,0 +1,14 @@ +from rest_framework import generics + +from crews.activities import models +from crews import permissions +from crews import serializersaaa + + +class CrewDashboardActivityAPIView(generics.RetrieveAPIView): + """크루 대시보드 홈 - 회차 별 API""" + queryset = models.CrewActivity + permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + serializer_class = serializersaaa.CrewActivityDashboardSerializer + lookup_field = 'id' + lookup_url_kwarg = 'activity_id' diff --git a/app/crews/admin.py b/app/crews/admin.py index 3acda70..22bfca6 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -1,18 +1,14 @@ from django.contrib import admin -from django.db.models import QuerySet -from django.http.request import HttpRequest -from crews import models -from crews import services +from crews.activities.models import CrewActivity +from crews.applications.models import CrewApplication +from crews.models import Crew +from crews.models import CrewMember +from crews.models import CrewSubmittableLanguage from users.models import User -admin.site.register([ - models.CrewActivityProblem, -]) - - -@admin.register(models.Crew) +@admin.register(Crew) class CrewModelAdmin(admin.ModelAdmin): list_display = [ 'id', @@ -21,141 +17,66 @@ class CrewModelAdmin(admin.ModelAdmin): 'get_members', 'get_applicants', 'get_activities', - models.Crew.field_name.IS_ACTIVE, - models.Crew.field_name.IS_RECRUITING, - models.Crew.field_name.CREATED_AT, + Crew.field_name.IS_ACTIVE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.CREATED_AT, ] search_fields = [ - models.Crew.field_name.NAME, - models.Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, - models.Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.CREATED_BY+'__'+User.field_name.USERNAME, + Crew.field_name.ICON, ] @admin.display(description='Display Name') - def get_display_name(self, crew: models.Crew): - return f'{crew.icon} {crew.name}' + def get_display_name(self, obj: Crew): + return obj.display_name() @admin.display(description='Captain') - def get_captain(self, obj: models.Crew): - return models.CrewMember.objects.get(**{ - models.CrewMember.field_name.CREW: obj, - models.CrewMember.field_name.IS_CAPTAIN: True, - }) + def get_captain(self, obj: Crew): + return CrewMember.objects.filter(crew=obj, is_captain=True).get() @admin.display(description='Members') - def get_members(self, crew: models.Crew): - members_count = models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: crew, - }).count() - return f'{members_count} / {crew.max_members}' + def get_members(self, obj: Crew): + return f'{CrewMember.objects.filter(crew=obj).count()} / {obj.max_members}' @admin.display(description='Applicants') - def get_applicants(self, obj: models.Crew): - return models.CrewApplication.objects.filter(**{ - models.CrewApplication.field_name.CREW: obj, - }).count() + def get_applicants(self, obj: Crew): + return CrewApplication.objects.crew(obj).count() @admin.display(description='Activities') - def get_activities(self, obj: models.Crew): - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: obj, - }).count() + def get_activities(self, obj: Crew): + return CrewActivity.objects.crew(obj).count() -@admin.register(models.CrewMember) +@admin.register(CrewMember) class CrewMemberModelAdmin(admin.ModelAdmin): list_display = [ - models.CrewMember.field_name.USER, - models.CrewMember.field_name.CREW, - models.CrewMember.field_name.IS_CAPTAIN, - models.CrewMember.field_name.CREATED_AT, + CrewMember.field_name.USER, + CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, + CrewMember.field_name.CREATED_AT, ] search_fields = [ - models.CrewMember.field_name.CREW+'__'+models.Crew.field_name.NAME, - models.CrewMember.field_name.USER+'__'+User.field_name.USERNAME, + CrewMember.field_name.CREW+'__'+Crew.field_name.NAME, + CrewMember.field_name.USER+'__'+User.field_name.USERNAME, ] ordering = [ - models.CrewMember.field_name.CREW, - models.CrewMember.field_name.IS_CAPTAIN, - ] - - -@admin.register(models.CrewActivity) -class CrewActivityModelAdmin(admin.ModelAdmin): - list_display = [ - models.CrewActivity.field_name.CREW, - models.CrewActivity.field_name.NAME, - models.CrewActivity.field_name.START_AT, - models.CrewActivity.field_name.END_AT, - 'nth', - 'is_in_progress', - 'has_started', - 'has_ended', - ] - search_fields = [ - models.CrewActivity.field_name.CREW+'__'+models.Crew.field_name.NAME, - models.CrewActivity.field_name.NAME, + CrewMember.field_name.CREW, + CrewMember.field_name.IS_CAPTAIN, ] - @admin.display(description='회차 번호') - def nth(self, obj: models.CrewActivity) -> int: - service = services.CrewActivityService(obj) - return service.nth() - - @admin.display(boolean=True, description='진행 중') - def is_in_progress(self, obj: models.CrewActivity) -> bool: - service = services.CrewActivityService(obj) - return service.is_in_progress() - - @admin.display(boolean=True, description='시작 됨') - def has_started(self, obj: models.CrewActivity) -> bool: - service = services.CrewActivityService(obj) - return service.has_started() - @admin.display(boolean=True, description='종료 됨') - def has_ended(self, obj: models.CrewActivity) -> bool: - service = services.CrewActivityService(obj) - return service.has_ended() - - -@admin.register(models.CrewSubmittableLanguage) +@admin.register(CrewSubmittableLanguage) class CrewSubmittableLanguageModelAdmin(admin.ModelAdmin): list_display = [ - models.CrewSubmittableLanguage.field_name.CREW, - models.CrewSubmittableLanguage.field_name.LANGUAGE, + CrewSubmittableLanguage.field_name.CREW, + CrewSubmittableLanguage.field_name.LANGUAGE, ] search_fields = [ - models.CrewActivity.field_name.CREW+'__'+models.Crew.field_name.NAME, - models.CrewSubmittableLanguage.field_name.LANGUAGE, + CrewActivity.field_name.CREW+'__'+Crew.field_name.NAME, + CrewSubmittableLanguage.field_name.LANGUAGE, ] ordering = [ - models.CrewSubmittableLanguage.field_name.CREW, - models.CrewSubmittableLanguage.field_name.LANGUAGE, + CrewSubmittableLanguage.field_name.CREW, + CrewSubmittableLanguage.field_name.LANGUAGE, ] - - -@admin.register(models.CrewApplication) -class CrewApplicantModelAdmin(admin.ModelAdmin): - list_display = [ - models.CrewApplication.field_name.CREW, - models.CrewApplication.field_name.APPLICANT, - models.CrewApplication.field_name.IS_ACCEPTED, - models.CrewApplication.field_name.IS_PENDING, - models.CrewApplication.field_name.REVIEWED_BY, - ] - actions = [ - 'accept', - 'reject', - ] - - @admin.action(description="Accept user") - def accept(self, request: HttpRequest, queryset: QuerySet[models.CrewApplication]): - for applicant in queryset: - services.crew_applicant.accept(applicant, request.user) - services.crew_applicant.notify_accepted(applicant) - - @admin.action(description="Reject user") - def reject(self, request: HttpRequest, queryset: QuerySet[models.CrewApplication]): - for applicant in queryset: - services.crew_applicant.reject(applicant, request.user) - services.crew_applicant.notify_rejected(applicant) diff --git a/app/crews/applications/__init__.py b/app/crews/applications/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crews/applications/admin.py b/app/crews/applications/admin.py new file mode 100644 index 0000000..c2f18c1 --- /dev/null +++ b/app/crews/applications/admin.py @@ -0,0 +1,32 @@ +from django.contrib import admin +from django.db.models import QuerySet +from django.http.request import HttpRequest + +from crews.applications.models import CrewApplication +from crews.applications.services import accept +from crews.applications.services import reject + + +@admin.register(CrewApplication) +class CrewApplicantModelAdmin(admin.ModelAdmin): + list_display = [ + CrewApplication.field_name.CREW, + CrewApplication.field_name.APPLICANT, + CrewApplication.field_name.IS_ACCEPTED, + CrewApplication.field_name.IS_PENDING, + CrewApplication.field_name.REVIEWED_BY, + ] + actions = [ + 'accept', + 'reject', + ] + + @admin.action(description="Accept user") + def accept(self, request: HttpRequest, queryset: QuerySet[CrewApplication]): + for applicant in queryset: + accept(applicant, request.user) + + @admin.action(description="Reject user") + def reject(self, request: HttpRequest, queryset: QuerySet[CrewApplication]): + for applicant in queryset: + reject(applicant, request.user) diff --git a/app/crews/applications/apps.py b/app/crews/applications/apps.py new file mode 100644 index 0000000..2f45887 --- /dev/null +++ b/app/crews/applications/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class CrewApplicationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "crews.applications" + verbose_name = "Crew applications" diff --git a/app/crews/applications/migrations/__init__.py b/app/crews/applications/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/crews/models/crew_application.py b/app/crews/applications/models.py similarity index 77% rename from app/crews/models/crew_application.py rename to app/crews/applications/models.py index c5f6bc5..7ee4588 100644 --- a/app/crews/models/crew_application.py +++ b/app/crews/applications/models.py @@ -1,8 +1,19 @@ +from __future__ import annotations + +from typing import Union + from django.db import models +from crews.models import Crew from users.models import User -from crews.models.crew import Crew -from crews.models.crew_member import CrewMember + + +class CrewApplicationManager(models.Manager): + def crew(self, crew: Crew) -> _CrewApplicationManager: + return self.filter(**{CrewApplication.field_name.CREW: crew}) + + def applicant(self, user: User) -> _CrewApplicationManager: + return self.filter(**{CrewApplication.field_name.APPLICANT: user}) class CrewApplication(models.Model): @@ -45,6 +56,8 @@ class CrewApplication(models.Model): default=None, ) + objects: _CrewApplicationManager = CrewApplicationManager() + class field_name: CREW = 'crew' APPLICANT = 'applicant' @@ -70,3 +83,7 @@ def __repr__(self) -> str: def __str__(self) -> str: return f'{self.pk} : {self.__repr__()}' + + +_CrewApplicationManager = Union[CrewApplicationManager, + models.Manager[CrewApplication]] diff --git a/app/crews/applications/permissions.py b/app/crews/applications/permissions.py new file mode 100644 index 0000000..e852a79 --- /dev/null +++ b/app/crews/applications/permissions.py @@ -0,0 +1,10 @@ +from rest_framework.request import Request + +from crews.applications.models import CrewApplication +from crews.permissions import IsCaptain as _IsCaptain + + +class IsCaptain(_IsCaptain): + def has_object_permission(self, request: Request, view, obj: CrewApplication): + assert isinstance(obj, CrewApplication) + return super().has_object_permission(request, view, obj.crew) diff --git a/app/crews/applications/serializers.py b/app/crews/applications/serializers.py new file mode 100644 index 0000000..f0b8418 --- /dev/null +++ b/app/crews/applications/serializers.py @@ -0,0 +1,61 @@ +from rest_framework import serializers + +from crews.applications.models import CrewApplication + + +PK = 'id' + + +class NoInputSerializer(serializers.Serializer): + pass + + +class CrewApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = CrewApplication + read_only_fields = ['__all__'] + + +class CrewApplicationCreateSerializer(serializers.ModelSerializer): + message = serializers.CharField() + + class Meta: + model = CrewApplication + fields = [ + CrewApplication.field_name.MESSAGE, + ] + read_only_fields = ['__all__'] + + +## TEMP + +# class CrewApplicationAboutApplicantSerializer(serializers.ModelSerializer): +# applicant = fields.CrewApplicationApplicantField() + +# class Meta: +# model = models.CrewApplication +# fields = [ +# PK, +# models.CrewApplication.field_name.MESSAGE, +# models.CrewApplication.field_name.IS_PENDING, +# models.CrewApplication.field_name.IS_ACCEPTED, +# models.CrewApplication.field_name.CREATED_AT, +# 'applicant', +# ] +# read_only_fields = ['__all__'] + + +# class CrewApplicationSerializer(serializers.ModelSerializer): +# class Meta: +# model = models.CrewApplication + + +# class CrewApplicationCreateSerializer(serializers.ModelSerializer): +# message = serializers.CharField() + +# class Meta: +# model = models.CrewApplication +# fields = [ +# models.CrewApplication.field_name.MESSAGE, +# ] +# read_only_fields = ['__all__'] \ No newline at end of file diff --git a/app/crews/applications/services.py b/app/crews/applications/services.py new file mode 100644 index 0000000..5b2c6a1 --- /dev/null +++ b/app/crews/applications/services.py @@ -0,0 +1,132 @@ +from textwrap import dedent + +from background_task import background +from django.core.mail import send_mail +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from boj.enums import BOJLevel +from boj.models import BOJUser +from crews.applications.models import CrewApplication +from crews.applications.signals import reviewed +from crews.models import Crew +from crews.models import CrewMember +from users.models import User + + +def apply(crew: Crew, applicant: User, message: str) -> CrewApplication: + is_valid_applicant(crew, applicant, raise_exception=True) + return CrewApplication.objects.create(**{ + CrewApplication.field_name.CREW: crew, + CrewApplication.field_name.APPLICANT: applicant, + CrewApplication.field_name.MESSAGE: message, + }) + + +def is_valid_applicant(crew: Crew, applicant: User, raise_exception=False) -> bool: + try: + boj_user = BOJUser.objects.get_by_user(applicant) + assert crew.is_recruiting, ( + "'크루가 현재 크루원을 모집하고 있지 않습니다." + ) + assert CrewMember.objects.crew(crew).count() < crew.max_members, ( + "크루의 최대 정원을 초과하였습니다." + ) + assert not CrewMember.objects.crew(crew).user(applicant).exists(), ( + "이미 가입한 크루입니다." + ) + assert (crew.min_boj_level is None) or (crew.min_boj_level <= boj_user.level), ( + "최소 백준 티어 요구조건을 달성하지 못하였습니다." + ) + except AssertionError as exception: + if raise_exception: + raise ValidationError from exception + return False + else: + return True + + +def accept(instance: CrewApplication, reviewed_by: User): + review(instance, reviewed_by, accept=True) + + +def reject(instance: CrewApplication, reviewed_by: User): + review(instance, reviewed_by, accept=False) + + +def review(instance: CrewApplication, reviewed_by: User, accept: bool): + instance.is_pending = False + instance.is_accepted = accept + instance.reviewed_by = reviewed_by + instance.reviewed_at = timezone.now() + instance.save() + reviewed.send(sender=CrewApplication, instance=instance) + + +@receiver(post_save, sender=CrewApplication) +def notify_on_applied(sender, instance: CrewApplication, created: bool, **kwargs): + if created: + notify_application_recieved(instance) + + +@receiver(reviewed, sender=CrewApplication) +def notify_on_reviewed(sender, instance: CrewApplication, **kwargs): + assert not instance.is_pending + if instance.is_accepted: + notify_application_accepted(instance) + else: + notify_application_rejected(instance) + + +def notify_application_recieved(instance: CrewApplication): + captain = CrewMember.objects.crew(instance.crew).captains().get() + boj_user = BOJUser.objects.get(username=instance.applicant.boj_username) + subject = '[Time Limit Exceeded] 새로운 크루 가입 신청이 도착했습니다' + message = dedent(f""" + [{instance.crew.icon} {instance.crew.name}]에 새로운 가입 신청이 왔어요! + + 지원자: {instance.applicant.username} + 지원자의 백준 아이디(레벨): {boj_user.username} ({BOJLevel(boj_user.level).get_name(lang='ko', arabic=False)}) + + 지원자의 메시지: + ``` + {instance.message} + ``` + + 수락하시려면 [여기]를 클릭해주세요. + """) + recipient = captain.user.email + _schedule_mail(subject, message, recipient) + + +def notify_application_accepted(instance: CrewApplication): + subject = '[Time Limit Exceeded] 새로운 크루 가입 신청이 승인되었습니다' + message = dedent(f""" + [{instance.crew.icon} {instance.crew.name}]에 가입하신 것을 축하해요! + + [여기]를 눌러 크루 대시보드로 바로가기 + """) + recipient = instance.applicant.email + _schedule_mail(subject, message, recipient) + + +def notify_application_rejected(instance: CrewApplication): + subject = '[Time Limit Exceeded] 새로운 크루 가입 신청이 거절되었습니다' + message = dedent(f""" + [{instance.crew.icon} {instance.crew.name}]에 아쉽게도 가입하지 못했어요. + """) + recipient = instance.applicant.email + _schedule_mail(subject, message, recipient) + + +@background +def _schedule_mail(subject: str, message: str, recipient: str) -> None: + send_mail( + subject=subject, + message=message, + recipient_list=[recipient], + from_email=None, + fail_silently=False, + ) diff --git a/app/crews/applications/signals.py b/app/crews/applications/signals.py new file mode 100644 index 0000000..ad2f18c --- /dev/null +++ b/app/crews/applications/signals.py @@ -0,0 +1,4 @@ +from django.db.models.signals import ModelSignal + + +reviewed = ModelSignal(use_caching=True) diff --git a/app/crews/applications/views.py b/app/crews/applications/views.py new file mode 100644 index 0000000..c0a5fbe --- /dev/null +++ b/app/crews/applications/views.py @@ -0,0 +1,96 @@ +from django.http.request import HttpRequest +from rest_framework import generics +from rest_framework import status +from rest_framework.exceptions import NotFound +from rest_framework.exceptions import ValidationError +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated + +from crews.applications.models import CrewApplication +from crews.applications.permissions import IsCaptain +from crews.applications.services import review +from crews.applications import serializers +from crews.models import Crew +from crews import servicesa + + +class CrewApplicationForCrewListAPIView(generics.ListAPIView): + """[크루/관리/크루 멤버 관리] 크루 가입 신청 현황 API""" + permission_classes = [IsAuthenticated] + serializer_class = serializers.CrewApplicationSerializer + lookup_url_kwarg = 'crew_id' + + def get_queryset(self): + return CrewApplication.objects.crew(self.get_crew()) + + def get_crew(self) -> Crew: + crew_id = self.kwargs[self.lookup_url_kwarg] + try: + return Crew.objects.as_captain(self.request.user).get(pk=crew_id) + except Crew.DoesNotExist: + raise ValidationError("크루가 존재하지 않거나, 권한이 없습니다.") + + +class CrewApplicationForUserListAPIView(generics.ListAPIView): + permission_classes = [IsAuthenticated] + serializer_class = serializers.CrewApplicationSerializer + + def get_queryset(self): + return CrewApplication.objects.applicant(self.request.user) + + +class CrewApplicantionCreateAPIView(generics.CreateAPIView): + """크루 가입 신청 API""" + queryset = CrewApplication + permission_classes = [IsAuthenticated] + serializer_class = serializers.CrewApplicationCreateSerializer + lookup_url_kwarg = 'crew_id' + + def create(self, request: HttpRequest, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + self.perform_create(serializer) + serializer = serializers.CrewApplicationSerializer(serializer.instance) + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + + def perform_create(self, serializer: serializers.CrewApplicationCreateSerializer): + crew_id = self.kwargs[self.lookup_url_kwarg] + try: + crew = Crew.objects.get(pk=crew_id) + except Crew.DoesNotExist: + raise NotFound("크루가 존재하지 않습니다.") + + message = serializer.validated_data['message'] + service = servicesa.get_crew_service(crew=self.get_object()) + serializer.instance = service.apply(self.request.user, message) + + +class CrewApplicantionAcceptAPIView(generics.UpdateAPIView): + """크루 가입 수락 API""" + queryset = CrewApplication + permission_classes = [IsAuthenticated & IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'application_id' + + def get_serializer(self, instance, *args, **kwargs) -> serializers.CrewApplicationSerializer: + return serializers.CrewApplicationSerializer(instance) + + def perform_update(self, serializer: serializers.CrewApplicationSerializer): + review(serializer.instance, self.request.user, accept=True) + + +class CrewApplicantionRejectAPIView(generics.UpdateAPIView): + """크루 가입 거부 API""" + queryset = CrewApplication + permission_classes = [IsAuthenticated & IsCaptain] + serializer_class = serializers.NoInputSerializer + lookup_field = 'id' + lookup_url_kwarg = 'application_id' + + def get_serializer(self, instance, *args, **kwargs) -> serializers.CrewApplicationSerializer: + return serializers.CrewApplicationSerializer(instance) + + def perform_update(self, serializer: serializers.CrewApplicationSerializer): + review(serializer.instance, self.request.user, accept=False) diff --git a/app/crews/dto.py b/app/crews/dto.py index a646f49..9651e24 100644 --- a/app/crews/dto.py +++ b/app/crews/dto.py @@ -1,67 +1,10 @@ -from collections import Counter from dataclasses import dataclass -from dataclasses import field -from datetime import datetime -from typing import List -from crews import enums -from problems.enums import ProblemDifficulty +from crews.enums import CrewTagType @dataclass -class ProblemTag: - key: str - name_ko: str - name_en: str - - def __hash__(self) -> int: - return self.key - - -@dataclass -class ProblemStatistic: - sample_count: int = field(default=0) - difficulty: Counter[int] = field(default_factory=Counter) - tags: Counter[ProblemTag] = field(default_factory=Counter) - - -@dataclass -class CrewTag: +class CrewTagDTO: key: str name: str - type: enums.CrewTagType - - -@dataclass -class CrewProblem: - problem_number: int - problem_id: int - problem_title: str - problem_difficulty: ProblemDifficulty - is_submitted: bool - last_submitted_date: datetime - - -@dataclass -class CrewActivity: - activity_id: int - name: str - start_at: datetime - end_at: datetime - is_in_progress: bool - has_started: bool - has_ended: bool - - -@dataclass -class SubmissionGraphNode: - problem_number: int - submitted_at: datetime - is_accepted: bool # 정답인지 여부 - - -@dataclass -class SubmissionGraph: - user_username: str - user_profile_image: str - submissions: List[SubmissionGraphNode] + type: CrewTagType diff --git a/app/crews/models.py b/app/crews/models.py new file mode 100644 index 0000000..2b9d1f8 --- /dev/null +++ b/app/crews/models.py @@ -0,0 +1,276 @@ +from __future__ import annotations + +from typing import List +from typing import Union + +from django.core.validators import MaxValueValidator +from django.core.validators import MinValueValidator +from django.db import models +from django.dispatch import receiver + +from boj.enums import BOJLevel +from crews.dto import CrewTagDTO +from crews.enums import CrewTagType +from crews.enums import EmojiChoices +from crews.enums import ProgrammingLanguageChoices +from users.models import User + + +class CrewManager(models.Manager): + def as_captain(self, user: User) -> _CrewManager: + return self.filter(pk__in=self._ids_as_captain(user)) + + def as_member(self, user: User) -> _CrewManager: + return self.filter(pk__in=self._ids_as_member(user)) + + def not_as_member(self, user: User) -> _CrewManager: + return self.exclude(pk__in=self._ids_as_member(user)) + + def recruiting(self) -> _CrewManager: + return self.filter(**{Crew.field_name.IS_RECRUITING: True}) + + def _ids_as_captain(self, user: User) -> List[int]: + return CrewMember.objects.user(user).captains().values_list(CrewMember.field_name.CREW, flat=True) + + def _ids_as_member(self, user: User) -> List[int]: + return CrewMember.objects.user(user).values_list(CrewMember.field_name.CREW, flat=True) + + +class Crew(models.Model): + name = models.CharField( + max_length=20, + unique=True, + help_text='크루 이름을 입력해주세요. (최대 20자)', + ) + icon = models.TextField( + choices=EmojiChoices.choices, + null=False, + blank=False, + default=EmojiChoices.U1F6A2, # :ship: + help_text='크루 아이콘을 입력해주세요. (이모지)', + ) + max_members = models.IntegerField( + help_text='크루 최대 인원을 입력해주세요.', + validators=[ + MinValueValidator(1), + MaxValueValidator(8), + ], + default=8, + blank=False, + null=False, + ) + notice = models.TextField( + help_text='크루 공지를 입력해주세요.', + null=True, + blank=True, + max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 + ) + custom_tags = models.JSONField( + help_text='태그를 입력해주세요.', + validators=[ + # TODO: 태그 형식 검사 + ], + blank=True, + default=list, + ) + min_boj_level = models.IntegerField( + help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', + choices=BOJLevel.choices, + default=BOJLevel.U, + ) + is_recruiting = models.BooleanField( + help_text='모집 중 여부를 입력해주세요.', + default=True, + ) + is_active = models.BooleanField( + help_text='활동 중인지 여부를 입력해주세요.', + default=True, + ) + created_at = models.DateTimeField(auto_now_add=True) + created_by = models.ForeignKey( + User, + on_delete=models.PROTECT, + null=False, + blank=False, + ) + updated_at = models.DateTimeField(auto_now=True) + + objects: _CrewManager = CrewManager() + + class field_name: + NAME = 'name' + ICON = 'icon' + MAX_MEMBERS = 'max_members' + NOTICE = 'notice' + CUSTOM_TAGS = 'custom_tags' + MIN_BOJ_LEVEL = 'min_boj_level' + IS_RECRUITING = 'is_recruiting' + IS_ACTIVE = 'is_active' + CREATED_AT = 'created_at' + CREATED_BY = 'created_by' + UPDATED_AT = 'updated_at' + + class Meta: + ordering = ['-updated_at'] + + def __str__(self) -> str: + return f'[{self.pk} : {self.icon} "{self.name}"]' + + def display_name(self) -> str: + return f'{self.icon} {self.name}' + + def tags(self) -> List[CrewTagDTO]: + tags = [] + # 사용 가능 언어 + for language_value in CrewSubmittableLanguage.objects.crew(self).values_list(CrewSubmittableLanguage.field_name.LANGUAGE, flat=True): + language = ProgrammingLanguageChoices(language_value) + tag_dto = CrewTagDTO( + key=language.value, + name=language.label, + type=CrewTagType.LANGUAGE, + ) + tags.append(tag_dto) + # 백준 최소 요구 티어 + min_level = BOJLevel(self.min_boj_level) + if min_level == BOJLevel.U: + tag_name = '티어 무관' + elif min_level.get_tier() == 5: + tag_name = f"{min_level.get_division_name(lang='ko')} 이상" + else: + tag_name = f"{min_level.get_name(lang='ko', arabic=False)} 이상" + tag_dto = CrewTagDTO(key=None, name=tag_name, type=CrewTagType.LEVEL) + tags.append(tag_dto) + # 커스텀 태그 + for tag_name in self.custom_tags: + tag_dto = CrewTagDTO(key=None, name=tag_name, type=CrewTagType.CUSTOM) + tags.append(tag_dto) + return tags + + +@receiver(models.signals.post_save, sender=Crew) +def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): + """크루 생성 시 선장을 자동으로 생성합니다.""" + if created: + CrewMember.objects.create(**{ + CrewMember.field_name.CREW: instance, + CrewMember.field_name.USER: instance.created_by, + CrewMember.field_name.IS_CAPTAIN: True, + }) + + +class CrewMemberManager(models.Manager): + def filter(self, + user: User = None, + crew: Crew = None, + is_captain: bool = None, + *args, **kwargs) -> models.QuerySet[CrewMember]: + if user is not None: + kwargs[CrewMember.field_name.USER] = user + if crew is not None: + kwargs[CrewMember.field_name.CREW] = crew + if is_captain is not None: + kwargs[CrewMember.field_name.IS_CAPTAIN] = is_captain + return super().filter(*args, **kwargs) + + def get_captain(self, crew: Crew) -> CrewMember: + return self.filter(crew=crew, is_captain=True).get() + + # TODO: 아래의 메소드들이 체인이 가능한지 검사. + + def captains(self) -> _CrewMemberManager: + return self.filter(**{CrewMember.field_name.IS_CAPTAIN: True}) + + def crew(self, crew: Crew) -> _CrewMemberManager: + return self.filter(**{CrewMember.field_name.CREW: crew}) + + def user(self, user: User) -> _CrewMemberManager: + return self.filter(**{CrewMember.field_name.USER: user}) + + +class CrewMember(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + help_text='크루를 입력해주세요.', + ) + user = models.ForeignKey( + User, + on_delete=models.CASCADE, + help_text='유저를 입력해주세요.', + ) + is_captain = models.BooleanField( + default=False, + help_text='크루장 여부', + ) + created_at = models.DateTimeField(auto_now_add=True) + + objects: _CrewMemberManager = CrewMemberManager() + + class field_name: + CREW = 'crew' + USER = 'user' + IS_CAPTAIN = 'is_captain' + CREATED_AT = 'created_at' + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['crew', 'user'], + name='unique_member_per_crew' + ), + ] + ordering = ['created_at'] + + def __str__(self) -> str: + return f'[{self.pk} : "{self.user.username}"@"{self.crew.display_name()}"]' + + +class CrewSubmittableLanguageManager(models.Manager): + def crew(self, crew: Crew) -> _CrewSubmittableLanguageManager: + return self.filter(**{CrewSubmittableLanguage.field_name.CREW: crew}) + + def bulk_create_from_languages(self, crew: Crew, languages: List[Union[str, ProgrammingLanguageChoices]]) -> List[CrewSubmittableLanguage]: + assert isinstance(crew, Crew) + entities = [] + for language in languages: + if isinstance(language, str): + language = ProgrammingLanguageChoices(language) + elif isinstance(language, ProgrammingLanguageChoices): + pass + else: + raise ValueError(f'{language}은 선택 가능한 언어가 아닙니다.') + entity = CrewSubmittableLanguage(**{ + CrewSubmittableLanguage.field_name.CREW: self.instance, + CrewSubmittableLanguage.field_name.LANGUAGE: language, + }) + entities.append(entity) + return CrewSubmittableLanguage.objects.bulk_create(entities) + + +class CrewSubmittableLanguage(models.Model): + crew = models.ForeignKey( + Crew, + on_delete=models.CASCADE, + ) + language = models.TextField( + choices=ProgrammingLanguageChoices.choices, + help_text='언어 키를 입력해주세요. (최대 20자)', + ) + + objects: _CrewSubmittableLanguageManager = CrewSubmittableLanguageManager() + + class field_name: + CREW = 'crew' + LANGUAGE = 'language' + + class Meta: + ordering = ['crew'] + + def __str__(self) -> str: + return f'[{self.pk} : #{self.language}]' + + +_CrewManager = Union[CrewManager, models.Manager[Crew]] +_CrewMemberManager = Union[CrewMemberManager, models.Manager[CrewMember]] +_CrewSubmittableLanguageManager = Union[CrewSubmittableLanguageManager, + models.Manager[CrewSubmittableLanguage]] diff --git a/app/crews/models/__init__.py b/app/crews/models/__init__.py deleted file mode 100644 index 92a9a49..0000000 --- a/app/crews/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from crews.models.crew import Crew -from crews.models.crew_activity import CrewActivity -from crews.models.crew_activity_problem import CrewActivityProblem -from crews.models.crew_application import CrewApplication -from crews.models.crew_member import CrewMember -from crews.models.crew_submittable_language import CrewSubmittableLanguage diff --git a/app/crews/models/crew.py b/app/crews/models/crew.py deleted file mode 100644 index b7ae8fd..0000000 --- a/app/crews/models/crew.py +++ /dev/null @@ -1,88 +0,0 @@ -from django.core.validators import MaxValueValidator -from django.core.validators import MinValueValidator -from django.db import models - -from crews import enums -from users.models import User -from users.models import UserBojLevelChoices - - -class Crew(models.Model): - name = models.CharField( - max_length=20, - unique=True, - help_text='크루 이름을 입력해주세요. (최대 20자)', - ) - icon = models.TextField( - choices=enums.EmojiChoices.choices, - null=False, - blank=False, - default=enums.EmojiChoices.U1F6A2, # :ship: - help_text='크루 아이콘을 입력해주세요. (이모지)', - ) - max_members = models.IntegerField( - help_text='크루 최대 인원을 입력해주세요.', - validators=[ - MinValueValidator(1), - MaxValueValidator(8), - ], - default=8, - blank=False, - null=False, - ) - notice = models.TextField( - help_text='크루 공지를 입력해주세요.', - null=True, - blank=True, - max_length=500, # TODO: 최대 길이 제한이 적정한지 검토 - ) - custom_tags = models.JSONField( - help_text='태그를 입력해주세요.', - validators=[ - # TODO: 태그 형식 검사 - ], - blank=True, - default=list, - ) - min_boj_level = models.IntegerField( - help_text='최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I', - choices=UserBojLevelChoices.choices, - blank=True, - null=True, - default=None, - ) - is_recruiting = models.BooleanField( - help_text='모집 중 여부를 입력해주세요.', - default=True, - ) - is_active = models.BooleanField( - help_text='활동 중인지 여부를 입력해주세요.', - default=True, - ) - created_at = models.DateTimeField(auto_now_add=True) - created_by = models.ForeignKey( - User, - on_delete=models.PROTECT, - null=False, - blank=False, - ) - updated_at = models.DateTimeField(auto_now=True) - - class field_name: - NAME = 'name' - ICON = 'icon' - MAX_MEMBERS = 'max_members' - NOTICE = 'notice' - CUSTOM_TAGS = 'custom_tags' - MIN_BOJ_LEVEL = 'min_boj_level' - IS_RECRUITING = 'is_recruiting' - IS_ACTIVE = 'is_active' - CREATED_AT = 'created_at' - CREATED_BY = 'created_by' - UPDATED_AT = 'updated_at' - - class Meta: - ordering = ['-updated_at'] - - def __str__(self) -> str: - return f'[{self.pk} : {self.icon} "{self.name}"]' diff --git a/app/crews/models/crew_activity.py b/app/crews/models/crew_activity.py deleted file mode 100644 index 4e1a7dd..0000000 --- a/app/crews/models/crew_activity.py +++ /dev/null @@ -1,37 +0,0 @@ -from __future__ import annotations - -from django.contrib import admin -from django.db import models -from django.utils import timezone - -from crews.models.crew import Crew - - -class CrewActivity(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - help_text='크루를 입력해주세요.', - ) - name = models.TextField( - help_text='활동 이름을 입력해주세요. (예: "1회차")', - ) - start_at = models.DateTimeField( - help_text='활동 시작 일자를 입력해주세요.', - ) - end_at = models.DateTimeField( - help_text='활동 종료 일자를 입력해주세요.', - ) - - class field_name: - CREW = 'crew' - NAME = 'name' - START_AT = 'start_at' - END_AT = 'end_at' - - class Meta: - ordering = ['start_at'] - get_latest_by = ['end_at'] - - def __str__(self) -> str: - return f"[{self.pk}: {self.name} ({self.start_at.date()} ~ {self.end_at.date()})]" diff --git a/app/crews/models/crew_activity_problem.py b/app/crews/models/crew_activity_problem.py deleted file mode 100644 index 23faa98..0000000 --- a/app/crews/models/crew_activity_problem.py +++ /dev/null @@ -1,59 +0,0 @@ -from django.core.validators import MinValueValidator -from django.db import models - -from crews.models.crew import Crew -from crews.models.crew_activity import CrewActivity -from problems.models.problem import Problem - - -class CrewActivityProblem(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - blank=False, - null=False, - ) - activity = models.ForeignKey( - CrewActivity, - on_delete=models.CASCADE, - help_text='활동을 입력해주세요.', - ) - problem = models.ForeignKey( - Problem, - on_delete=models.PROTECT, - help_text='문제를 입력해주세요.', - ) - order = models.IntegerField( - help_text='문제 순서를 입력해주세요.', - validators=[ - MinValueValidator(1), - ], - ) - - class field_name: - # related fields - SUBMISSIONS = 'submissions' - # fields - CREW = 'crew' - ACTIVITY = 'activity' - PROBLEM = 'problem' - ORDER = 'order' - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['activity', 'order'], - name='unique_order_per_activity_problem', - ), - ] - ordering = ['order'] - - def save(self, *args, **kwargs) -> None: - assert self.crew == self.activity.crew - return super().save(*args, **kwargs) - - def __repr__(self) -> str: - return f'{self.activity.__repr__()} ← #{self.order} {self.problem.__repr__()}' - - def __str__(self) -> str: - return f'{self.pk} : {self.__repr__()}' diff --git a/app/crews/models/crew_activity_submission.py b/app/crews/models/crew_activity_submission.py deleted file mode 100644 index 42c5fb4..0000000 --- a/app/crews/models/crew_activity_submission.py +++ /dev/null @@ -1,51 +0,0 @@ -from django.db import models - -from crews import enums -from crews.models.crew_activity_problem import CrewActivityProblem -from users.models import User - - -class CrewActivitySubmission(models.Model): - # TODO: 같은 문제에 여러 번 제출 하는 것을 막기 위한 로직 추가 - problem = models.ForeignKey( - CrewActivityProblem, - on_delete=models.PROTECT, - help_text='활동 문제를 입력해주세요.', - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text='유저를 입력해주세요.', - ) - code = models.TextField( - help_text='유저의 코드를 입력해주세요.', - ) - language = models.TextField( - choices=enums.ProgrammingLanguageChoices.choices, - help_text='유저의 코드 언어를 입력해주세요.', - ) - is_correct = models.BooleanField( - help_text='유저의 코드가 정답인지 여부를 입력해주세요.', - ) - is_help_needed = models.BooleanField( - help_text='유저의 코드에 도움이 필요한지 여부를 입력해주세요.', - default=False, - ) - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class field_name: - PROBLEM = 'problem' - USER = 'user' - CODE = 'code' - LANGUAGE = 'language' - IS_CORRECT = 'is_correct' - IS_HELP_NEEDED = 'is_help_needed' - CREATED_AT = 'created_at' - UPDATED_AT = 'updated_at' - - class Meta: - ordering = ['created_at'] - - def __str__(self) -> str: - return f'[{self.pk} : {self.problem} ← {self.user}]' diff --git a/app/crews/models/crew_member.py b/app/crews/models/crew_member.py deleted file mode 100644 index 62b97ae..0000000 --- a/app/crews/models/crew_member.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver - -from users.models import User -from crews.models.crew import Crew - - -class CrewMember(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - help_text='크루를 입력해주세요.', - ) - user = models.ForeignKey( - User, - on_delete=models.CASCADE, - help_text='유저를 입력해주세요.', - ) - is_captain = models.BooleanField( - default=False, - help_text='크루장 여부', - ) - created_at = models.DateTimeField(auto_now_add=True) - - class field_name: - CREW = 'crew' - USER = 'user' - IS_CAPTAIN = 'is_captain' - CREATED_AT = 'created_at' - - class Meta: - constraints = [ - models.UniqueConstraint( - fields=['crew', 'user'], - name='unique_member_per_crew' - ), - ] - ordering = ['created_at'] - - -@receiver(post_save, sender=Crew) -def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): - """크루 생성 시 선장을 자동으로 생성합니다.""" - if created: - CrewMember.objects.create(**{ - CrewMember.field_name.CREW: instance, - CrewMember.field_name.USER: instance.created_by, - CrewMember.field_name.IS_CAPTAIN: True, - }) diff --git a/app/crews/models/crew_submittable_language.py b/app/crews/models/crew_submittable_language.py deleted file mode 100644 index c74dc2f..0000000 --- a/app/crews/models/crew_submittable_language.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.db import models - -from crews import enums -from crews.models.crew import Crew - - -class CrewSubmittableLanguage(models.Model): - crew = models.ForeignKey( - Crew, - on_delete=models.CASCADE, - ) - language = models.TextField( - choices=enums.ProgrammingLanguageChoices.choices, - help_text='언어 키를 입력해주세요. (최대 20자)', - ) - - class field_name: - CREW = 'crew' - LANGUAGE = 'language' - - class Meta: - ordering = ['crew'] - - def __str__(self) -> str: - return f'[{self.pk} : #{self.language}]' diff --git a/app/crews/permissions.py b/app/crews/permissions.py index afb8877..0dd6a49 100644 --- a/app/crews/permissions.py +++ b/app/crews/permissions.py @@ -1,43 +1,17 @@ -from typing import Union - -from rest_framework import exceptions from rest_framework.permissions import BasePermission -from rest_framework.permissions import AllowAny -from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request -from crews import models -from crews import services - - -class IsJoinable(BasePermission): - def has_object_permission(self, request: Request, view, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = services.CrewService(crew) - try: - service.validate_applicant(request.user, raises_exception=True) - except exceptions.ValidationError as exception: - detail = f"크루에 가입할 수 없습니다. ({exception.args[0]})" - raise exceptions.PermissionDenied(detail) - return True +from crews.models import Crew +from crews.models import CrewMember class IsMember(BasePermission): - def has_object_permission(self, request: Request, view, obj: Union[models.Crew, models.CrewActivity]): - if isinstance(obj, models.Crew): - crew = obj - elif isinstance(obj, models.CrewActivity): - crew = obj.crew - else: - raise ValueError('백엔드의 실책으로 보이는 오류') - service = services.CrewService(crew) - if not service.is_member(request.user): - raise exceptions.PermissionDenied('크루 멤버가 아닙니다.') - return True + def has_object_permission(self, request: Request, view, obj: Crew): + assert isinstance(obj, Crew) + return CrewMember.objects.filter(crew=obj, user=request.user).exists() class IsCaptain(BasePermission): - def has_object_permission(self, request: Request, view, application: models.CrewApplication) -> bool: - assert isinstance(application, models.CrewApplication) - service = services.CrewService(application.crew) - return service.is_captain(request.user) + def has_object_permission(self, request: Request, view, obj: Crew) -> bool: + assert isinstance(obj, Crew) + return CrewMember.objects.filter(crew=obj, user=request.user, is_captain=True).exists() diff --git a/app/crews/serializers.py b/app/crews/serializers.py new file mode 100644 index 0000000..769ce3c --- /dev/null +++ b/app/crews/serializers.py @@ -0,0 +1,246 @@ +from django.db.models import QuerySet +from django.db.transaction import atomic +from rest_framework import serializers + +from crews import enums +from crews import models +from crews.activities.models import CrewActivity +from crews.activities.serializers import CrewActivitySerializer +from crews.applications.services import is_valid_applicant +from crews.models import Crew +from crews.models import CrewMember +from crews.models import CrewSubmittableLanguage +from users.models import User +from users.serializers import UserMinimalSerializer + + +PK = 'id' + + +# Crew Tag Serializer + +class CrewTagSerializer(serializers.Serializer): + key = serializers.CharField() + name = serializers.CharField() + type = serializers.CharField() + + +# Crew Member Serializer + +class CrewMemberSerializer(serializers.ModelSerializer): + user_id = serializers.IntegerField(source=CrewMember.field_name.USER) + username = serializers.CharField(source=CrewMember.field_name.USER+'__'+User.field_name.USERNAME) + profile_image = serializers.ImageField(source=CrewMember.field_name.USER+'__'+User.field_name.PROFILE_IMAGE) + + class Meta: + model = CrewMember + fields = [ + 'user_id', + 'username', + 'profile_image', + CrewMember.field_name.IS_CAPTAIN, + ] + read_only_fields = ['__all__'] + + +# Crew Fields + +class IsJoinableField(serializers.SerializerMethodField): + current_user = serializers.CurrentUserDefault() + + def to_representation(self, value): + return value + + def get_attribute(self, instance: Crew): + assert isinstance(instance, Crew) + user = self.__class__.current_user(self) + return is_valid_applicant(instance, user, raise_exception=False) + + +class IsCaptainField(serializers.SerializerMethodField): + current_user = serializers.CurrentUserDefault() + + def to_representation(self, value): + return value + + def get_attribute(self, instance: Crew): + assert isinstance(instance, Crew) + user = self.__class__.current_user(self) + return CrewMember.objects.crew(instance).user(user).captains().exists() + + +class MemberField(serializers.SerializerMethodField): + def __init__(self, include_member_details: bool, **kwargs): + super().__init__(**kwargs) + self.include_member_details = include_member_details + + def to_representation(self, crew: Crew): + members = CrewMember.objects.crew(crew) + data = { + "count": members.count(), + "max_count": crew.max_members, + } + if self.include_member_details: + data['items'] = CrewMemberSerializer(members, many=True).data + return data + + def get_attribute(self, instance: Crew): + assert isinstance(instance, Crew) + return instance + + +class TagsField(CrewTagSerializer): + def __init__(self, **kwargs): + super().__init__(many=True, **kwargs) + + def get_attribute(self, instance: Crew): + assert isinstance(instance, Crew) + return instance.tags() + + +class LatestActivityField(CrewActivitySerializer): + def get_attribute(self, instance: Crew) -> CrewActivity: + assert isinstance(instance, Crew) + try: + return CrewActivity.objects.crew(instance).has_started().latest() + except: + return CrewActivity(**{ + CrewActivity.field_name.CREW: instance, + CrewActivity.field_name.NAME: '활동 종료', + CrewActivity.field_name.START_AT: None, + CrewActivity.field_name.END_AT: None, + }) + + +class ActivitiesField(CrewActivitySerializer): + def __init__(self, **kwargs): + super().__init__(many=True, **kwargs) + + def get_attribute(self, instance: Crew) -> QuerySet[CrewActivity]: + assert isinstance(instance, Crew) + return CrewActivity.objects.crew(instance) + + +# Crew Serializers + +class CrewCreateSerializer(serializers.ModelSerializer): + created_by = UserMinimalSerializer() + custom_tags = serializers.ListField( + default=list, + child=serializers.CharField(), + ) + languages = serializers.MultipleChoiceField( + choices=enums.ProgrammingLanguageChoices.choices, + ) + + class Meta: + model = Crew + fields = [ + PK, + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.MAX_MEMBERS, + 'languages', + Crew.field_name.MIN_BOJ_LEVEL, + Crew.field_name.NOTICE, + Crew.field_name.IS_RECRUITING, + Crew.field_name.IS_ACTIVE, + Crew.field_name.CREATED_AT, + Crew.field_name.CREATED_BY, + Crew.field_name.CUSTOM_TAGS, + ] + extra_kwargs = { + PK: { + 'read_only': True, + }, + 'languages': { + 'write_only': True, + 'default': list, + }, + Crew.field_name.CREATED_AT: { + 'read_only': True, + }, + Crew.field_name.CREATED_BY: { + 'read_only': True, + 'default': serializers.CurrentUserDefault(), + }, + } + + def save(self, **kwargs): + languages = self.validated_data.pop('languages') + with atomic(): + crew = super().save(**kwargs) + CrewSubmittableLanguage.objects.crew(crew).delete() + CrewSubmittableLanguage.objects.bulk_create_from_languages( + crew=crew, + languages=languages, + ) + return crew + + +class RecruitingCrewSerializer(serializers.ModelSerializer): + """크루 목록""" + is_joinable = IsJoinableField() + members = MemberField(include_member_details=False) + tags = TagsField() + latest_activity = LatestActivityField() + + class Meta: + model = models.Crew + fields = [ + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.IS_ACTIVE, + 'is_joinable', + 'members', + 'tags', + 'latest_activity', + ] + read_only_fields = ['__all__'] + + +class MyCrewSerializer(serializers.ModelSerializer): + "나의 참여 크루" + latest_activity = LatestActivityField() + + class Meta: + model = models.Crew + fields = [ + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.IS_ACTIVE, + 'latest_activity', + ] + read_only_fields = ['__all__'] + + +class CrewDashboardSerializer(serializers.ModelSerializer): + """크루 대시보드 + + - 공지사항 + - 크루 태그 + - 나의 동료 + - 크루가 풀이한 문제 + - 풀이한 문제의 난이도 + """ + + tags = TagsField() + members = MemberField(include_member_details=True) + activities = CrewActivitySerializer() + is_captain = IsCaptainField() + + class Meta: + model = models.Crew + fields = [ + PK, + models.Crew.field_name.ICON, + models.Crew.field_name.NAME, + models.Crew.field_name.NOTICE, + 'is_captain', + 'tags', + 'members', + 'activities', + ] + read_only_fields = ['__all__'] diff --git a/app/crews/serializers/__init__.py b/app/crews/serializersaaa/__init__ copy.py similarity index 75% rename from app/crews/serializers/__init__.py rename to app/crews/serializersaaa/__init__ copy.py index 24bf9e7..6e9a310 100644 --- a/app/crews/serializers/__init__.py +++ b/app/crews/serializersaaa/__init__ copy.py @@ -1,10 +1,10 @@ +from django.db.transaction import atomic from rest_framework import serializers from crews import enums from crews import models -from crews import services -from crews.serializers import fields -from users.serializers import UserMinimalSerializer +from crews import servicesa +from crews.serializersaaa import fields PK = 'id' @@ -14,8 +14,19 @@ class NoInputSerializer(serializers.Serializer): pass +# Crew Serializers + class CrewCreateSerializer(serializers.ModelSerializer): - languages = serializers.MultipleChoiceField(choices=enums.ProgrammingLanguageChoices.choices) + created_by = serializers.HiddenField( + default=serializers.CurrentUserDefault(), + ) + custom_tags = serializers.ListField( + default=list, + child=serializers.CharField(), + ) + languages = serializers.MultipleChoiceField( + choices=enums.ProgrammingLanguageChoices.choices, + ) class Meta: model = models.Crew @@ -29,15 +40,16 @@ class Meta: models.Crew.field_name.NOTICE, models.Crew.field_name.IS_RECRUITING, models.Crew.field_name.IS_ACTIVE, + models.Crew.field_name.CREATED_BY, ] - extra_kwargs = { - models.Crew.field_name.CUSTOM_TAGS: { - 'default': list, - } - } def save(self, **kwargs): - return services.CrewService.create(**self.validated_data) + languages = self.validated_data.pop('languages') + with atomic(): + instance = super().save(**kwargs) + service = servicesa.get_crew_service(instance) + service.set_languages(languages) + return instance class RecruitingCrewSerializer(serializers.ModelSerializer): @@ -143,5 +155,17 @@ class Meta: read_only_fields = ['__all__'] -class CrewApplicationCreateSerializer(serializers.Serializer): +class CrewApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = models.CrewApplication + + +class CrewApplicationCreateSerializer(serializers.ModelSerializer): message = serializers.CharField() + + class Meta: + model = models.CrewApplication + fields = [ + models.CrewApplication.field_name.MESSAGE, + ] + read_only_fields = ['__all__'] diff --git a/app/crews/serializersaaa/__init__.py b/app/crews/serializersaaa/__init__.py new file mode 100644 index 0000000..06e775b --- /dev/null +++ b/app/crews/serializersaaa/__init__.py @@ -0,0 +1,33 @@ +from django.db.transaction import atomic +from rest_framework import serializers + +from crews import enums +from crews import models +from crews import servicesa +from crews.serializersaaa import fields + + +PK = 'id' + + +class NoInputSerializer(serializers.Serializer): + pass + + + +# Crew Retrieve Serializers + +class CrewRecruitingSerializer(serializers.ModelSerializer): + ... + + +class CrewJoinedSerializer(serializers.ModelSerializer): + ... + + +class CrewDashboardSerializer(serializers.ModelSerializer): + ... + + +class CrewCreateSerializer(serializers.ModelSerializer): + ... \ No newline at end of file diff --git a/app/crews/serializers/fields.py b/app/crews/serializersaaa/fields.py similarity index 70% rename from app/crews/serializers/fields.py rename to app/crews/serializersaaa/fields.py index c665d67..9b74221 100644 --- a/app/crews/serializers/fields.py +++ b/app/crews/serializersaaa/fields.py @@ -1,9 +1,12 @@ +from typing import List + from rest_framework import serializers from boj.services import get_boj_user_service -from crews import dto +from app.crews.servicesa import dto +from crews import enums from crews import models -from crews import services +from crews import servicesa from crews import utils from users.models import User @@ -20,7 +23,8 @@ def to_representation(self, crew: models.Crew): "date_end_at": None, } try: - service = services.CrewActivityService.last_started(crew) + service = servicesa.get_crew_service(crew) + activity = service.query_activities_published().latest() except models.CrewActivity.DoesNotExist: return { "name": "등록된 활동 없음", @@ -29,17 +33,17 @@ def to_representation(self, crew: models.Crew): } else: return { - "name": f"{service.nth()}회차", - "date_start_at": service.instance.start_at, - "date_end_at": service.instance.end_at, + "name": activity.name, + "date_start_at": activity.start_at, + "date_end_at": activity.end_at, } class IsCrewCaptainField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) + service = servicesa.get_crew_service(crew) user = serializers.CurrentUserDefault()(self) - service = services.CrewService(crew) return service.is_captain(user) @@ -48,7 +52,7 @@ class CrewMembersField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) image_field = serializers.ImageField() return [ { @@ -65,7 +69,7 @@ class CrewMemberCountField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) return { "count": service.query_members().count(), "max_count": crew.max_members, @@ -77,7 +81,7 @@ class CrewTagsField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) return [ { 'key': tag.key, @@ -90,32 +94,30 @@ def to_representation(self, crew: models.Crew): class CrewIsJoinableField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): - user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) - assert isinstance(user, User) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) + user = serializers.CurrentUserDefault()(self) return service.validate_applicant(user) class CrewIsMemberField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): - user = serializers.CurrentUserDefault()(self) assert isinstance(crew, models.Crew) - assert isinstance(user, User) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) + user = serializers.CurrentUserDefault()(self) return service.is_member(user) class CrewActivitiesField(serializers.SerializerMethodField): def to_representation(self, crew: models.Crew): assert isinstance(crew, models.Crew) - service = services.CrewService(crew) + service = servicesa.get_crew_service(crew) return [ { 'activity_id': activity.activity_id, 'name': activity.name, } - for activity in service.activities() + for activity in service.query_activities() ] @@ -133,35 +135,6 @@ def to_representation(self, activity: models.CrewActivity): ] -class ProblemStatisticsDifficultyField(serializers.SerializerMethodField): - def to_representation(self, statistics: dto.ProblemStatistic): - assert isinstance(statistics, dto.ProblemStatistic) - return [ - { - 'difficulty': difficulty, - 'problem_count': count, - 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), - } - for difficulty, count in statistics.difficulty.items() - ] - - -class ProblemStatisticsTagsField(serializers.SerializerMethodField): - def to_representation(self, statistics: dto.ProblemStatistic): - assert isinstance(statistics, dto.ProblemStatistic) - return [ - { - 'label': { - 'ko': tag.name_ko, - 'en': tag.name_en, - }, - 'problem_count': count, - 'ratio': utils.divide_by_zero_handler(count, statistics.sample_count), - } - for tag, count in statistics.tags.items() - ] - - class CrewApplicationApplicantField(serializers.SerializerMethodField): def to_representation(self, instance: models.CrewApplication): assert isinstance(instance, models.CrewApplication) diff --git a/app/crews/services/crew_application_service.py b/app/crews/services/crew_application_service.py deleted file mode 100644 index 210621d..0000000 --- a/app/crews/services/crew_application_service.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db.transaction import atomic -from django.utils import timezone - -import notifications.services -import users.models -from crews import models - - -class CrewApplicantionService: - @staticmethod - def create(crew: models.Crew, user: users.models.User, message: str) -> models.CrewApplication: - instance = models.CrewApplication(**{ - models.CrewApplication.field_name.CREW: crew, - models.CrewApplication.field_name.APPLICANT: user, - models.CrewApplication.field_name.MESSAGE: message, - }) - instance.save() - notifications.services.notify_crew_application_requested(instance) - - def __init__(self, instance: models.CrewApplication): - assert isinstance(instance, models.CrewApplication) - self.instance = instance - - def reject(self, reviewed_by: users.models.User): - self._review(reviewed_by, accept=False) - self.instance.save() - notifications.services.notify_crew_application_rejected(self.instance) - - def accept(self, reviewed_by: users.models.User): - self._review(reviewed_by, accept=True) - with atomic(): - self.instance.save() - models.CrewMember.objects.create(**{ - models.CrewApplication.field_name.CREW: self.instance.crew, - models.CrewApplication.field_name.APPLICANT: self.instance.applicant, - }) - notifications.services.notify_crew_application_accepted(self.instance) - - def _review(self, by: users.models.User, accept: bool): - self.instance.is_pending = False - self.instance.is_accepted = accept - self.instance.reviewed_by = by - self.instance.reviewed_at = timezone.now() diff --git a/app/crews/services/__init__.py b/app/crews/servicesa/__init__.py similarity index 54% rename from app/crews/services/__init__.py rename to app/crews/servicesa/__init__.py index 2d00252..80d161e 100644 --- a/app/crews/services/__init__.py +++ b/app/crews/servicesa/__init__.py @@ -1,18 +1,20 @@ -from crews.services.base import UserCrewService -from crews.services.base import CrewService -from crews.services.base import CrewActivityService -from crews.services.base import CrewApplicantionService +from crews.servicesa.base import UserCrewService +from crews.servicesa.base import CrewService +from crews.servicesa.base import CrewActivityService +from crews.servicesa.base import CrewApplicantionService +from crews.servicesa.concrete import ConcreteUserCrewService +from crews.servicesa.concrete import ConcreteCrewService from crews import models from users.models import User def get_user_crew_service(user: User) -> UserCrewService: - return UserCrewService(user) + return ConcreteUserCrewService(user) def get_crew_service(crew: models.Crew) -> CrewService: - return CrewService(crew) + return ConcreteCrewService(crew) def get_crew_activity_service(crew_activity: models.CrewActivity) -> CrewActivityService: diff --git a/app/crews/services/base.py b/app/crews/servicesa/base.py similarity index 61% rename from app/crews/services/base.py rename to app/crews/servicesa/base.py index bf9ac21..f54c672 100644 --- a/app/crews/services/base.py +++ b/app/crews/servicesa/base.py @@ -3,7 +3,7 @@ from django.db.models import QuerySet from boj.enums import BOJLevel -from crews import dto +from app.crews.servicesa import dto from crews import enums from crews import models from problems.dto import ProblemStatisticDTO @@ -80,48 +80,3 @@ def set_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> No def apply(self, applicant: User, message: str) -> models.CrewApplication: """지원자가 자격요건을 갖추지 못했다면 ValidationError를 발생시킬 수 있다.""" ... - - -class CrewActivityService: - def __init__(self, instance: models.CrewActivity) -> None: - assert isinstance(instance, models.CrewActivity) - self.instance = instance - - def query_previous_activities(self) -> QuerySet[models.CrewActivity]: - ... - - def nth(self) -> int: - """활동의 회차 번호를 반환합니다. - - 이 값은 1부터 시작합니다. - 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 - 더한 값을 반환하므로, 고정된 값이 아닙니다. - - 느린 연산입니다. - 한 번에 여러 회차 번호들을 조회하기 위해 이 함수를 사용하는 것은 권장하지 않습니다. - """ - ... - - def is_in_progress(self) -> bool: - """활동이 진행 중인지 여부를 반환합니다.""" - ... - - def has_started(self) -> bool: - """활동이 열린적이 있는지 여부를 반환합니다..""" - ... - - def has_ended(self) -> bool: - """활동이 종료되었는지 여부를 반환합니다.""" - ... - - -class CrewApplicantionService: - def __init__(self, instance: models.CrewApplication): - assert isinstance(instance, models.CrewApplication) - self.instance = instance - - def reject(self, reviewed_by: User): - ... - - def accept(self, reviewed_by: User): - ... diff --git a/app/crews/servicesa/concrete.py b/app/crews/servicesa/concrete.py new file mode 100644 index 0000000..f6edd48 --- /dev/null +++ b/app/crews/servicesa/concrete.py @@ -0,0 +1,189 @@ +from textwrap import dedent +from typing import List + +from background_task import background +from django.core.mail import send_mail +from django.db.models import QuerySet +from django.db.transaction import atomic +from django.utils import timezone +from rest_framework.exceptions import ValidationError + +from boj.enums import BOJLevel +from app.crews.servicesa import dto +from crews import enums +from crews import models +from crews.servicesa.base import UserCrewService +from crews.servicesa.base import CrewService +from crews.servicesa.base import CrewActivityService +from crews.servicesa.base import CrewApplicantionService +from problems.dto import ProblemStatisticDTO +from problems.models import Problem +from users.models import User + + +class ConcreteUserCrewService(UserCrewService): + def query_crews_joined(self) -> QuerySet[models.Crew]: + # 활동 종료된 크루는 뒤로 가도록 정렬 + return models.Crew.objects.filter( + pk__in=self._crew_ids_list_joined(), + ).order_by( + '-'+models.Crew.field_name.IS_ACTIVE, + ) + + def query_crews_recruiting(self) -> QuerySet[models.Crew]: + return models.Crew.objects.filter(**{ + models.Crew.field_name.IS_RECRUITING: True, + }).exclude( + pk__in=self._crew_ids_list_joined(), + ) + + def _crew_ids_list_joined(self) -> List[int]: + if not self.instance.is_authenticated: + return [] + return models.CrewMember.objects.filter(**{ + models.CrewMember.field_name.USER: self.instance, + }).values_list(models.CrewMember.field_name.CREW) + + +class ConcreteCrewService(CrewService): + def query_members(self) -> QuerySet[models.CrewMember]: + ... + + def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: + ... + + def query_applications(self) -> QuerySet[models.CrewApplication]: + ... + + def query_activities(self) -> QuerySet[models.CrewActivity]: + ... + + def query_activities_published(self) -> QuerySet[models.CrewActivity]: + ... + + def query_problems(self) -> QuerySet[Problem]: + ... + + def query_captain(self) -> QuerySet[models.CrewMember]: + ... + + def statistics(self) -> ProblemStatisticDTO: + ... + + def display_name(self) -> str: + ... + + def tags(self) -> List[dto.CrewTag]: + ... + + def languages(self) -> List[enums.ProgrammingLanguageChoices]: + ... + + def min_boj_level(self) -> BOJLevel: + ... + + def is_captain(self, user: User) -> bool: + ... + + def is_member(self, user: User) -> bool: + ... + + def set_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> None: + ... + + def apply(self, applicant: User, message: str) -> models.CrewApplication: + ... + + # send notification + subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 도착했습니다' + message=dedent(f""" + [{self.instance.crew.icon} {self.instance.crew.name}]에 새로운 가입 신청이 왔어요! + + 지원자: {applicant.applicant.username} + 지원자의 백준 아이디(레벨): {applicant.applicant.boj_username} ({users.models.UserBojLevelChoices(applicant.applicant.boj_level).get_name(lang='ko', arabic=False)}) + + 지원자의 메시지: + ``` + {applicant.message} + ``` + + 수락하시려면 [여기]를 클릭해주세요. + """) + recipient=self.query_captain().get().user.email + schedule_mail(subject, message, recipient) + + def validate_applicant(self, applicant: User, raises_exception=False) -> bool: + ... + + +class ConcreteCrewActivityService(CrewActivityService): + def query_previous_activities(self) -> QuerySet[models.CrewActivity]: + return models.CrewActivity.objects.filter(**{ + models.CrewActivity.field_name.CREW: self.instance.crew, + models.CrewActivity.field_name.START_AT+'__lt': self.instance.start_at, + }) + + def nth(self) -> int: + return self.query_previous_activities().count()+1 + + def is_in_progress(self) -> bool: + return self.has_started() and not self.has_ended() + + def has_started(self) -> bool: + return self.instance.start_at <= timezone.now() + + def has_ended(self) -> bool: + return self.instance.end_at < timezone.now() + + +class ConcreteCrewApplicantionService(CrewApplicantionService): + def reject(self, reviewed_by: User): + self.instance.is_pending = False + self.instance.is_accepted = True + self.instance.reviewed_by = reviewed_by + self.instance.reviewed_at = timezone.now() + self.instance.save() + + # send notification + subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 거절되었습니다' + message=dedent(f""" + [{self.instance.crew.icon} {self.instance.crew.name}]에 아쉽게도 가입하지 못했어요. + """) + recipient=self.instance.applicant.email + schedule_mail(subject, message, recipient) + + def accept(self, reviewed_by: User): + self.instance.is_pending = False + self.instance.is_accepted = False + self.instance.reviewed_by = reviewed_by + self.instance.reviewed_at = timezone.now() + with atomic(): + self.instance.save() + self._save_member() + + # send notification + subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 승인되었습니다' + message=dedent(f""" + [{self.instance.crew.icon} {self.instance.crew.name}]에 가입하신 것을 축하해요! + + [여기]를 눌러 크루 대시보드로 바로가기 + """) + recipient=self.instance.applicant.email + schedule_mail(subject, message, recipient) + + def _save_member(self) -> models.CrewMember: + return models.CrewMember.objects.create(**{ + models.CrewApplication.field_name.CREW: self.instance.crew, + models.CrewApplication.field_name.APPLICANT: self.instance.applicant, + }) + + +@background +def schedule_mail(subject: str, message: str, recipient: str) -> None: + send_mail( + subject=subject, + message=message, + recipient_list=[recipient], + from_email=None, + fail_silently=False, + ) diff --git a/app/crews/services/crew_activity_service.py b/app/crews/servicesa/crew_activity_service.py similarity index 56% rename from app/crews/services/crew_activity_service.py rename to app/crews/servicesa/crew_activity_service.py index ec1c650..b2ccfdd 100644 --- a/app/crews/services/crew_activity_service.py +++ b/app/crews/servicesa/crew_activity_service.py @@ -45,37 +45,3 @@ def last_started(crew: models.Crew) -> CrewActivityService: models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), }).latest() return CrewActivityService(instance) - - def __init__(self, instance: models.CrewActivity) -> None: - assert isinstance(instance, models.CrewActivity) - self.instance = instance - - def query_previous(self) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: self.instance.crew, - models.CrewActivity.field_name.START_AT+'__lt': self.instance.start_at, - }) - - def nth(self) -> int: - """활동의 회차 번호를 반환합니다. - - 이 값은 1부터 시작합니다. - 자신의 활동 시작일자보다 이전에 시작된 활동의 개수를 센 값에 1을 - 더한 값을 반환하므로, 고정된 값이 아닙니다. - - 느린 연산입니다. - 한 번에 여러 회차 번호들을 조회하기 위해 이 함수를 사용하는 것은 권장하지 않습니다. - """ - return self.query_previous().count()+1 - - def is_in_progress(self) -> bool: - """활동이 진행 중인지 여부를 반환합니다.""" - return self.has_started() and not self.has_ended() - - def has_started(self) -> bool: - """활동이 열린적이 있는지 여부를 반환합니다..""" - return self.instance.start_at <= timezone.now() - - def has_ended(self) -> bool: - """활동이 종료되었는지 여부를 반환합니다.""" - return self.instance.end_at < timezone.now() diff --git a/app/crews/services/crew_service.py b/app/crews/servicesa/crew_service.py similarity index 98% rename from app/crews/services/crew_service.py rename to app/crews/servicesa/crew_service.py index 5ad4ff8..bf834c3 100644 --- a/app/crews/services/crew_service.py +++ b/app/crews/servicesa/crew_service.py @@ -6,10 +6,10 @@ from rest_framework import exceptions from boj.services import get_boj_user_service -from crews import dto +from app.crews.servicesa import dto from crews import enums from crews import models -from crews.services.crew_activity_service import CrewActivityService +from crews.servicesa.crew_activity_service import CrewActivityService from problems.models import Problem from problems.services import ProblemService from users.models import User diff --git a/app/crews/servicesa/dto.py b/app/crews/servicesa/dto.py new file mode 100644 index 0000000..160758d --- /dev/null +++ b/app/crews/servicesa/dto.py @@ -0,0 +1,67 @@ +from collections import Counter +from dataclasses import dataclass +from dataclasses import field +from datetime import datetime +from typing import List + +from crews import enums +from problems.analyses.enums import ProblemDifficulty + + +@dataclass +class ProblemTag: + key: str + name_ko: str + name_en: str + + def __hash__(self) -> int: + return self.key + + +@dataclass +class ProblemStatistic: + sample_count: int = field(default=0) + difficulty: Counter[int] = field(default_factory=Counter) + tags: Counter[ProblemTag] = field(default_factory=Counter) + + +@dataclass +class CrewTag: + key: str + name: str + type: enums.CrewTagType + + +@dataclass +class CrewProblem: + problem_number: int + problem_id: int + problem_title: str + problem_difficulty: ProblemDifficulty + is_submitted: bool + last_submitted_date: datetime + + +@dataclass +class CrewActivity: + activity_id: int + name: str + start_at: datetime + end_at: datetime + is_in_progress: bool + has_started: bool + has_ended: bool + + +@dataclass +class SubmissionGraphNode: + problem_number: int + submitted_at: datetime + is_accepted: bool # 정답인지 여부 + + +@dataclass +class SubmissionGraph: + user_username: str + user_profile_image: str + submissions: List[SubmissionGraphNode] diff --git a/app/crews/tests/__init__.py b/app/crews/tests/__init__.py deleted file mode 100644 index 61d9c5e..0000000 --- a/app/crews/tests/__init__.py +++ /dev/null @@ -1,89 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIClient -from rest_framework import status - -from crews.models import Crew -from users.models import User, BojLevelChoices - - -class CrewRecruitingTest(TestCase): - def setUp(self): - self.maxDiff = None - self.client = APIClient() - self.url = '/api/v1/crews/recruiting' - self.user = User.objects.create(**{ - User.field_name.EMAIL: 'email@example.com', - User.field_name.USERNAME: 'username', - User.field_name.PASSWORD: 'password', - User.field_name.BOJ_USERNAME: 'boj_username', - User.field_name.BOJ_LEVEL: BojLevelChoices.S1, - }) - self.crew = Crew.objects.create(**{ - Crew.field_name.NAME: '크루명', - Crew.field_name.ICON: '😀', - Crew.field_name.MAX_MEMBERS: 4, - Crew.field_name.NOTICE: '공지', - Crew.field_name.CUSTOM_TAGS: ["태그1", "태그2",], - Crew.field_name.MIN_BOJ_LEVEL: BojLevelChoices.G5, - Crew.field_name.IS_RECRUITING: True, - Crew.field_name.IS_ACTIVE: True, - Crew.field_name.CREATED_BY: self.user, - }) - - def test_returns_200(self): - res = self.client.get(self.url) - self.assertEqual(res.status_code, status.HTTP_200_OK) - - def test_200_response(self): - res = self.client.get(self.url) - self.assertJSONEqual(res.content, { - 'count': 1, - 'next': None, - 'previous': None, - 'results': [ - { - 'id': self.crew.pk, - 'icon': self.crew.icon, - 'name': self.crew.name, - 'is_active': self.crew.is_active, - 'is_member': False, - 'is_recruiting': self.crew.is_recruiting, - 'is_joinable': False, - 'activities': { - 'count': 0, - 'recent': { - 'nth': None, - 'name': '등록된 활동 없음', - 'start_at': None, - 'end_at': None, - 'is_open': False, - } - }, - 'members': { - 'count': 1, - 'max_count': self.crew.max_members, - 'items': [], - }, - 'tags': { - 'count': 3, - 'items': [ - { - 'key': None, - 'name': '골드 5 이상', - 'type': 'level', - }, - { - 'key': None, - 'name': '태그1', - 'type': 'custom', - }, - { - 'key': None, - 'name': '태그2', - 'type': 'custom', - }, - ] - }, - } - ], - }) diff --git a/app/crews/urls.py b/app/crews/urls.py index d4eaf11..e54d1c7 100644 --- a/app/crews/urls.py +++ b/app/crews/urls.py @@ -1,25 +1,26 @@ from django.urls import include from django.urls import path -from crews import views +import crews.views +# import crews.applications.views urlpatterns = [ - path("crews/my", views.MyCrewListAPIView.as_view()), - path("crews/recruiting", views.RecruitingCrewListAPIView.as_view()), - path("crew", views.CrewCreateAPIView.as_view()), + path("crews/my", crews.views.MyCrewListAPIView.as_view()), + path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), + path("crew", crews.views.CrewCreateAPIView.as_view()), path("crew/", include([ - path("/dashboard", views.CrewDashboardAPIView.as_view()), - path("/statistics", views.CrewStatisticsAPIView.as_view()), - path("/applications", views.CrewApplicationListAPIView.as_view()), - path("/apply", views.CrewApplicantionCreateAPIView.as_view()), - ])), - path("crew/applications/my", views.CrewApplicantionRejectAPIView.as_view()), - path("crew/application/", include([ - path("/accept", views.CrewApplicantionAcceptAPIView.as_view()), - path("/reject", views.CrewApplicantionRejectAPIView.as_view()), - ])), - path("crew/activities/", include([ - path("/dashboard", views.CrewDashboardActivityAPIView.as_view()), + path("/dashboard", crews.views.CrewDashboardAPIView.as_view()), + path("/statistics", crews.views.CrewStatisticsAPIView.as_view()), + # path("/applications", crews.applications.views.CrewApplicationForCrewListAPIView.as_view()), + # path("/apply", crews.applications.views.CrewApplicantionCreateAPIView.as_view()), ])), + # path("crew/applications/my", crews.applications.views.CrewApplicationForUserListAPIView.as_view()), + # path("crew/application/", include([ + # path("/accept", crews.applications.views.CrewApplicantionAcceptAPIView.as_view()), + # path("/reject", crews.applications.views.CrewApplicantionRejectAPIView.as_view()), + # ])), + # path("crew/activities/", include([ + # path("/dashboard", views.CrewDashboardActivityAPIView.as_view()), + # ])), ] diff --git a/app/crews/utils.py b/app/crews/utils.py deleted file mode 100644 index 8dbdf4d..0000000 --- a/app/crews/utils.py +++ /dev/null @@ -1,4 +0,0 @@ -def divide_by_zero_handler(numerator: float, denominator: float, default=0) -> float: - if denominator == 0: - return default - return numerator / denominator diff --git a/app/crews/views.py b/app/crews/views.py index 99d6b0b..8b2d53d 100644 --- a/app/crews/views.py +++ b/app/crews/views.py @@ -1,156 +1,65 @@ -from django.http.request import HttpRequest from rest_framework import generics -from rest_framework import status -from rest_framework.response import Response - -from crews import models -from crews import permissions -from crews import serializers -from crews import services -from problems.dto import ProblemStatisticDTO +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated + +from crews.activities.models import CrewActivityProblem +from crews.models import Crew +from crews.permissions import IsMember +from crews.serializers import RecruitingCrewSerializer +from crews.serializers import MyCrewSerializer +from crews.serializers import CrewCreateSerializer +from crews.serializers import CrewDashboardSerializer from problems.serializers import ProblemStatisticSerializer +from problems.statistics import create_statistics -# Crew List API Views - class RecruitingCrewListAPIView(generics.ListAPIView): """크루 목록""" - permission_classes = [permissions.AllowAny] - serializer_class = serializers.RecruitingCrewSerializer + permission_classes = [AllowAny] + serializer_class = RecruitingCrewSerializer def get_queryset(self): - service = services.get_user_crew_service(self.request.user) - return service.query_crews_recruiting() + return Crew.objects.recruiting().not_as_member(self.request.user) class MyCrewListAPIView(generics.ListAPIView): """나의 참여 크루""" - permission_classes = [permissions.IsAuthenticated & permissions.IsMember] - serializer_class = serializers.MyCrewSerializer + permission_classes = [IsAuthenticated & IsMember] + serializer_class = MyCrewSerializer def get_queryset(self): - service = services.get_user_crew_service(self.request.user) - return service.query_crews_joined() + return Crew.objects.as_member(self.request.user).order_by( + Crew.field_name.IS_ACTIVE, + Crew.field_name.UPDATED_AT, + ).reverse() -# Crew API Views - class CrewCreateAPIView(generics.CreateAPIView): """크루 생성 API""" - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated] - serializer_class = serializers.CrewCreateSerializer - - def create(self, request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - instance = self.perform_create(serializer) - serializer = serializers.MyCrewSerializer(instance=instance) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + queryset = Crew + permission_classes = [IsAuthenticated] + serializer_class = CrewCreateSerializer class CrewDashboardAPIView(generics.RetrieveAPIView): """크루 대시보드 홈 API""" - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated & permissions.IsMember] - serializer_class = serializers.CrewDashboardSerializer + queryset = Crew + permission_classes = [IsAuthenticated & IsMember] + serializer_class = CrewDashboardSerializer lookup_field = 'id' lookup_url_kwarg = 'crew_id' class CrewStatisticsAPIView(generics.RetrieveAPIView): - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated & permissions.IsMember] + """크루 대시보드 문제 통계 API""" + queryset = Crew + permission_classes = [IsAuthenticated & IsMember] serializer_class = ProblemStatisticSerializer lookup_field = 'id' lookup_url_kwarg = 'crew_id' - def get_object(self) -> ProblemStatisticDTO: - instance = super().get_object() - service = services.get_crew_service(instance) - return service.statistics() - - -# Crew Activity API Views - -class CrewDashboardActivityAPIView(generics.RetrieveAPIView): - """크루 대시보드 홈 - 회차 별 API""" - queryset = models.CrewActivity - permission_classes = [permissions.IsAuthenticated & permissions.IsMember] - serializer_class = serializers.CrewActivityDashboardSerializer - lookup_field = 'id' - lookup_url_kwarg = 'activity_id' - - -# Crew Application API Views - -class CrewApplicationListAPIView(generics.RetrieveAPIView): - """[크루/관리/크루 멤버 관리] 크루 가입 신청 현황 API""" - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.CrewApplicationAboutApplicantSerializer - lookup_field = 'id' - lookup_url_kwarg = 'crew_id' - - def retrieve(self, request, *args, **kwargs): - instance = self.get_object() - service = services.get_crew_service(instance) - queryset = service.query_applications() - page = self.paginate_queryset(queryset) - serializer = self.get_serializer(page, many=True) - return self.get_paginated_response(serializer.data) - - -class CrewApplicantionCreateAPIView(generics.CreateAPIView): - """크루 가입 신청 API""" - queryset = models.Crew - permission_classes = [permissions.IsAuthenticated & permissions.IsJoinable] - serializer_class = serializers.CrewApplicationCreateSerializer - lookup_field = 'id' - lookup_url_kwarg = 'crew_id' - - def create(self, request: HttpRequest, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - serializer = serializers.CrewApplicationSerializer(serializer.instance) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - def perform_create(self, serializer: serializers.CrewApplicationCreateSerializer): - message = serializer.validated_data['message'] - service = services.get_crew_service(crew=self.get_object()) - serializer.instance = service.apply(self.request.user, message) - - -class CrewApplicantionAcceptAPIView(generics.GenericAPIView): - """크루 가입 수락 API""" - queryset = models.CrewApplication - permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.NoInputSerializer - lookup_field = 'id' - lookup_url_kwarg = 'application_id' - - def put(self, request: HttpRequest, *args, **kwargs): - instance = self.get_object() - service = services.get_crew_application_service(instance) - service.accept(reviewed_by=request.user) - serializer = serializers.CrewApplicationSerializer(instance) - return Response(serializer.data) - - -class CrewApplicantionRejectAPIView(generics.GenericAPIView): - """크루 가입 거부 API""" - queryset = models.CrewApplication - permission_classes = [permissions.IsAuthenticated & permissions.IsCaptain] - serializer_class = serializers.NoInputSerializer - lookup_field = 'id' - lookup_url_kwarg = 'application_id' - - def put(self, request: HttpRequest, *args, **kwargs): - instance = self.get_object() - service = services.get_crew_application_service(instance) - service.reject(reviewed_by=request.user) - serializer = serializers.CrewApplicationSerializer(instance) - return Response(serializer.data) + def get_object(self): + activity_problems = CrewActivityProblem.objects.crew( + crew=super().get_object(), + ).select_related(CrewActivityProblem.field_name.PROBLEM) + return create_statistics([ap.problem for ap in activity_problems]) From b8161a07eb13a265460bd8ec4c814d224a86b2a4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 02:41:57 +0900 Subject: [PATCH 454/552] =?UTF-8?q?refactor(boj.services):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EB=AA=85=EC=9D=B4=20=EC=97=86=EB=8A=94=20?= =?UTF-8?q?=EB=B0=B1=EC=A4=80=20=EC=82=AC=EC=9A=A9=EC=9E=90=EC=9D=98=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=EB=8A=94=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/services.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/boj/services.py b/app/boj/services.py index cfcf93d..75acfb1 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -25,8 +25,14 @@ def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): update_boj_user_data(instance.username) -@background def update_boj_user_data(username: str): + assert username.strip().isidentifier() + _update_boj_user_data(username) + + +@background +def _update_boj_user_data(username: str): + assert username.strip().isidentifier() instance = BOJUser.objects.get_by_username(username) url = f'https://solved.ac/api/v3/user/show?handle={username}' data = requests.get(url).json() @@ -38,4 +44,4 @@ def update_boj_user_data(username: str): BOJUserSnapshot.objects.create_snapshot_of(instance) except AssertionError: # Solved.ac API 관련 문제일 가능성이 높다. - logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') + logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') \ No newline at end of file From ee142a5d666466fc5b19dc52ee5a2ddd9e1b6a4f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 02:56:16 +0900 Subject: [PATCH 455/552] =?UTF-8?q?refactor(problems):=20=EB=B6=84?= =?UTF-8?q?=EC=84=9D=EA=B8=B0=20=EA=B4=80=EB=A0=A8=20signals=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EB=A5=BC=20analyzers=20=EB=AA=A8=EB=93=88=EB=A1=9C=20?= =?UTF-8?q?=EC=98=AE=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/analyses/apps.py | 4 +--- app/problems/analyses/services.py | 11 ----------- app/problems/analyzers/__init__.py | 9 ++++++++- app/problems/apps.py | 3 +++ 4 files changed, 12 insertions(+), 15 deletions(-) delete mode 100644 app/problems/analyses/services.py diff --git a/app/problems/analyses/apps.py b/app/problems/analyses/apps.py index 41ef4f2..e34fccd 100644 --- a/app/problems/analyses/apps.py +++ b/app/problems/analyses/apps.py @@ -4,6 +4,4 @@ class AnalysesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "problems.analyses" - - def ready(self) -> None: - import problems.analyses.services + verbose_name = "Problems analyses" diff --git a/app/problems/analyses/services.py b/app/problems/analyses/services.py deleted file mode 100644 index aa448d3..0000000 --- a/app/problems/analyses/services.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from problems.models import Problem -from problems.analyzers import schedule_analyze - - -@receiver(post_save, sender=Problem) -def auto_analyze(sender, instance: Problem, created: bool, **kwargs): - if created: - schedule_analyze(instance.pk) diff --git a/app/problems/analyzers/__init__.py b/app/problems/analyzers/__init__.py index 00e01b1..ea71b62 100644 --- a/app/problems/analyzers/__init__.py +++ b/app/problems/analyzers/__init__.py @@ -1,7 +1,8 @@ from logging import getLogger from background_task import background - +from django.db.models.signals import post_save +from django.dispatch import receiver from problems.analyses.dto import ProblemAnalysisDTO from problems.analyses.models import ProblemAnalysis @@ -19,6 +20,12 @@ def get_analyzer() -> ProblemAnalyzer: return GeminiProblemAnalyzer.get_instance() +@receiver(post_save, sender=Problem) +def auto_analyze(sender, instance: Problem, created: bool, **kwargs): + if created: + schedule_analyze(instance.pk) + + @background def schedule_analyze(problem_id: int): logger.info(f'PK={problem_id} 문제의 분석 준비중.') diff --git a/app/problems/apps.py b/app/problems/apps.py index f35f38e..6c718f6 100644 --- a/app/problems/apps.py +++ b/app/problems/apps.py @@ -4,3 +4,6 @@ class ProblemsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "problems" + + def ready(self) -> None: + import problems.analyzers From d149cf6f3ecbf4f5578600efa9a9f158ecee1b3a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 03:48:51 +0900 Subject: [PATCH 456/552] =?UTF-8?q?refactor(problems.analyses):=20ProblemA?= =?UTF-8?q?nalysis=EC=9D=98=20=EC=97=86=EC=96=B4=EC=A7=84=20=EC=86=8D?= =?UTF-8?q?=EC=84=B1=20TAGS=20=EC=9D=98=20=ED=95=84=EB=93=9C=EB=AA=85=20?= =?UTF-8?q?=EC=B0=B8=EC=A1=B0=EC=9A=A9=20=EC=83=81=EC=88=98=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/analyses/models.py | 1 - app/problems/analyses/tests.py | 3 --- 2 files changed, 4 deletions(-) delete mode 100644 app/problems/analyses/tests.py diff --git a/app/problems/analyses/models.py b/app/problems/analyses/models.py index 2a25e62..0c23b71 100644 --- a/app/problems/analyses/models.py +++ b/app/problems/analyses/models.py @@ -139,7 +139,6 @@ class ProblemAnalysis(models.Model): class field_name: PROBLEM = 'problem' DIFFICULTY = 'difficulty' - TAGS = 'tags' TIME_COMPLEXITY = 'time_complexity' HINT = 'hint' CREATED_AT = 'created_at' diff --git a/app/problems/analyses/tests.py b/app/problems/analyses/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/app/problems/analyses/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. From d7ee5f0a1eea75723cb645a148c42829203e1fb0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 03:42:03 +0900 Subject: [PATCH 457/552] =?UTF-8?q?test(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(fixture=20=EC=82=AC=EC=9A=A9)=20&=20(rename=20tests/=5F=5Finit?= =?UTF-8?q?=5F=5F.py=20->=20tests.py)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 29 ++++++++++++++++ app/users/tests/__init__.py | 68 ------------------------------------- 2 files changed, 29 insertions(+), 68 deletions(-) create mode 100644 app/users/tests.py delete mode 100644 app/users/tests/__init__.py diff --git a/app/users/tests.py b/app/users/tests.py new file mode 100644 index 0000000..96abfd5 --- /dev/null +++ b/app/users/tests.py @@ -0,0 +1,29 @@ +from django.test import TestCase +from rest_framework import status + + +class SignInTest(TestCase): + fixtures = ['user.sample.json'] + + def setUp(self) -> None: + self.client.logout() + + def test_로그인성공(self): + res = self.client.post( + "/api/v1/auth/signin", + { + "email": "test@example.com", + "password": "passw0rd@test", + }, + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_비밀번호_불일치(self): + res = self.client.post( + "/api/v1/auth/signin", + { + "email": "test@example.com", + "password": "password@test", + } + ) + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) diff --git a/app/users/tests/__init__.py b/app/users/tests/__init__.py deleted file mode 100644 index fa5099d..0000000 --- a/app/users/tests/__init__.py +++ /dev/null @@ -1,68 +0,0 @@ -from django.utils import timezone -from django.test import TestCase -from rest_framework.test import APIClient -from rest_framework import status - -from users.models import User, BojLevelChoices - - -class SignInTest(TestCase): - def setUp(self): - self.maxDiff = None - self.client = APIClient() - self.url = '/api/v1/auth/signin' - self.now = timezone.now() - self.user = User.objects.create(**{ - User.field_name.EMAIL: 'email@example.com', - User.field_name.USERNAME: 'username', - User.field_name.PASSWORD: 'password', - User.field_name.BOJ_USERNAME: 'boj_username', - User.field_name.BOJ_LEVEL: BojLevelChoices.S1, - User.field_name.BOJ_LEVEL_UPDATED_AT: self.now, - }) - - def test_returns_200(self): - res = self.client.post(self.url, { - 'email': 'email@example.com', - 'password': 'password', - }) - self.assertEqual(res.status_code, status.HTTP_200_OK) - - def test_returns_400(self): - res = self.client.post(self.url, { - 'username': 'username', - 'password': 'password', - }) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_returns_403(self): - res = self.client.post(self.url, { - 'email': 'email@example.com', - 'password': 'password2', - }) - self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) - - def test_200_response(self): - res = self.client.post(self.url, { - 'email': 'email@example.com', - 'password': 'password', - }) - user = User.objects.get(pk=1) - self.assertJSONEqual(res.content, { - 'id': 1, - 'boj': { - 'username': 'boj_username', - 'profile_url': 'https://boj.kr/boj_username', - 'level': 10, - 'division': 2, - 'division_name_en': 'Silver', - 'division_name_ko': '실버', - 'tier': 1, - 'tier_name': 'I', - 'tier_updated_at': user.boj_level_updated_at.isoformat(), - }, - 'profile_image': None, - 'username': 'username', - 'created_at': user.created_at.isoformat(), - 'last_login': user.last_login.isoformat(), - }) From 5c5bc1585ebdaa45f003be46adddeedc27faba28 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 06:56:53 +0900 Subject: [PATCH 458/552] =?UTF-8?q?test(users):=20=ED=9A=8C=EC=9B=90?= =?UTF-8?q?=EA=B0=80=EC=9E=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 96abfd5..affd368 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -1,6 +1,8 @@ from django.test import TestCase from rest_framework import status +from users.models import UserEmailVerification + class SignInTest(TestCase): fixtures = ['user.sample.json'] @@ -27,3 +29,39 @@ def test_비밀번호_불일치(self): } ) self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + + +class SignUpTest(TestCase): + @classmethod + def setUpTestData(cls) -> None: + super().setUpTestData() + cls.sample_object = UserEmailVerification.objects.create(**{ + UserEmailVerification.field_name.EMAIL: "test@example.com", + UserEmailVerification.field_name.VERIFICATION_TOKEN: 'sample_token', + }) + + def test_회원가입_성공(self): + res = self.client.post( + "/api/v1/auth/signup", + { + "email": self.sample_object.email, + "username": "test", + "password": "passw0rd@test", + "boj_username": "test", + "verification_token": self.sample_object.verification_token, + } + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_인증토큰_불일치(self): + res = self.client.post( + "/api/v1/auth/signup", + { + "email": self.sample_object.email, + "username": "test", + "password": "passw0rd@test", + "boj_username": "test", + "verification_token": 'this_token_must_not_match_the_sample...', + } + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) From 80892f6c8f753d70bd8221b33e2d773d7c46eeb7 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 06:57:11 +0900 Subject: [PATCH 459/552] =?UTF-8?q?test(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index affd368..4482f34 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -65,3 +65,19 @@ def test_인증토큰_불일치(self): } ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +class SignOutTest(TestCase): + fixtures = ['user.sample.json'] + + def setUp(self) -> None: + # authenticate() 함수에서는 email을 username으로 바꾸어서 전달하기 때문에 아래와 같이 설정함. + logged_in = self.client.login(**{ + "username": "test@example.com", + "password": "passw0rd@test", + }) + self.assertTrue(logged_in) + + def test_로그아웃_성공(self): + res = self.client.get("/api/v1/auth/signout") + self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) From bb3fcb7aa3e3647f6dc53caf47cafdc4f2903f0e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:04:49 +0900 Subject: [PATCH 460/552] =?UTF-8?q?test(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 4482f34..47d75b3 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -81,3 +81,27 @@ def setUp(self) -> None: def test_로그아웃_성공(self): res = self.client.get("/api/v1/auth/signout") self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) + + +class UsernameCheckTest(TestCase): + fixtures = ['user.sample.json'] + + def test_사용_가능한_사용자명(self): + res = self.client.get("/api/v1/auth/username/check", { + "username": "unique", + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "username": "unique", + "is_usable": True, + }) + + def test_사용_불가능한_사용자명(self): + res = self.client.get("/api/v1/auth/username/check", { + "username": "test", + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "username": "test", + "is_usable": False, + }) From 0fcd474d772152f322d0d88d270c3031fd10e16d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:05:31 +0900 Subject: [PATCH 461/552] =?UTF-8?q?test(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 47d75b3..6db20d9 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -105,3 +105,27 @@ def test_사용_불가능한_사용자명(self): "username": "test", "is_usable": False, }) + + +class EmailCheckTest(TestCase): + fixtures = ['user.sample.json'] + + def test_사용_가능한_이메일(self): + res = self.client.get("/api/v1/auth/email/check", { + "email": "unique@notexample.com", + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": "unique@notexample.com", + "is_usable": True, + }) + + def test_사용_불가능한_이메일(self): + res = self.client.get("/api/v1/auth/email/check", { + "email": "test@example.com", + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": "test@example.com", + "is_usable": False, + }) From 92232029e248eaa205bbb04fbbf41f28cff575c9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:14:50 +0900 Subject: [PATCH 462/552] =?UTF-8?q?fix(crews.models):=20=ED=81=AC=EB=A3=A8?= =?UTF-8?q?=EA=B0=80=20=EC=83=9D=EC=84=B1=EB=90=9C=20=EC=A7=81=ED=9B=84=20?= =?UTF-8?q?=EC=A7=A7=EC=9D=80=20=EC=8B=9C=EA=B0=84=EB=8F=99=EC=95=88=20?= =?UTF-8?q?=ED=81=AC=EB=A3=A8=EC=9E=A5=EC=9D=B4=20=EC=A1=B4=EC=9E=AC?= =?UTF-8?q?=ED=95=98=EC=A7=80=20=EC=95=8A=EC=95=84=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app/crews/models.py b/app/crews/models.py index 2b9d1f8..41e409c 100644 --- a/app/crews/models.py +++ b/app/crews/models.py @@ -6,7 +6,6 @@ from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models -from django.dispatch import receiver from boj.enums import BOJLevel from crews.dto import CrewTagDTO @@ -146,16 +145,16 @@ def tags(self) -> List[CrewTagDTO]: tags.append(tag_dto) return tags - -@receiver(models.signals.post_save, sender=Crew) -def auto_create_captain(sender, instance: Crew, created: bool, **kwargs): - """크루 생성 시 선장을 자동으로 생성합니다.""" - if created: - CrewMember.objects.create(**{ - CrewMember.field_name.CREW: instance, - CrewMember.field_name.USER: instance.created_by, - CrewMember.field_name.IS_CAPTAIN: True, - }) + def save(self, *args, **kwargs) -> None: + retval = super().save(*args, **kwargs) + if not CrewMember.objects.filter(crew=self, is_captain=True).exists(): + # 크루 생성 시 선장을 자동으로 생성합니다. + CrewMember.objects.create(**{ + CrewMember.field_name.CREW: self, + CrewMember.field_name.USER: self.created_by, + CrewMember.field_name.IS_CAPTAIN: True, + }) + return retval class CrewMemberManager(models.Manager): From dacf4d05477e7c797fc412eb461e76cb3f7aa229 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:24:51 +0900 Subject: [PATCH 463/552] =?UTF-8?q?test(problems.analyses):=20fixture=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/analyses/fixtures/problem_tag.sample.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 app/problems/analyses/fixtures/problem_tag.sample.json diff --git a/app/problems/analyses/fixtures/problem_tag.sample.json b/app/problems/analyses/fixtures/problem_tag.sample.json new file mode 100644 index 0000000..e7a0620 --- /dev/null +++ b/app/problems/analyses/fixtures/problem_tag.sample.json @@ -0,0 +1 @@ +[{"model": "analyses.problemtag", "pk": 1, "fields": {"key": "2_sat", "name_ko": "2-sat", "name_en": "2-sat"}}, {"model": "analyses.problemtag", "pk": 2, "fields": {"key": "aho_corasick", "name_ko": "아호-코라식", "name_en": "aho-corasick"}}, {"model": "analyses.problemtag", "pk": 3, "fields": {"key": "polygon_area", "name_ko": "다각형의 넓이", "name_en": "area of a polygon"}}, {"model": "analyses.problemtag", "pk": 4, "fields": {"key": "articulation", "name_ko": "단절점과 단절선", "name_en": "articulation points and bridges"}}, {"model": "analyses.problemtag", "pk": 5, "fields": {"key": "backtracking", "name_ko": "백트래킹", "name_en": "backtracking"}}, {"model": "analyses.problemtag", "pk": 6, "fields": {"key": "combinatorics", "name_ko": "조합론", "name_en": "combinatorics"}}, {"model": "analyses.problemtag", "pk": 7, "fields": {"key": "graphs", "name_ko": "그래프 이론", "name_en": "graph theory"}}, {"model": "analyses.problemtag", "pk": 8, "fields": {"key": "hashing", "name_ko": "해싱", "name_en": "hashing"}}, {"model": "analyses.problemtag", "pk": 9, "fields": {"key": "primality_test", "name_ko": "소수 판정", "name_en": "primality test"}}, {"model": "analyses.problemtag", "pk": 10, "fields": {"key": "bellman_ford", "name_ko": "벨만–포드", "name_en": "bellman–ford"}}, {"model": "analyses.problemtag", "pk": 11, "fields": {"key": "graph_traversal", "name_ko": "그래프 탐색", "name_en": "graph traversal"}}, {"model": "analyses.problemtag", "pk": 12, "fields": {"key": "binary_search", "name_ko": "이분 탐색", "name_en": "binary search"}}, {"model": "analyses.problemtag", "pk": 13, "fields": {"key": "bipartite_matching", "name_ko": "이분 매칭", "name_en": "bipartite matching"}}, {"model": "analyses.problemtag", "pk": 14, "fields": {"key": "bitmask", "name_ko": "비트마스킹", "name_en": "bitmask"}}, {"model": "analyses.problemtag", "pk": 15, "fields": {"key": "general_matching", "name_ko": "일반적인 매칭", "name_en": "general matching"}}, {"model": "analyses.problemtag", "pk": 16, "fields": {"key": "burnside", "name_ko": "번사이드 보조정리", "name_en": "burnside's lemma"}}, {"model": "analyses.problemtag", "pk": 18, "fields": {"key": "centroid_decomposition", "name_ko": "센트로이드 분할", "name_en": "centroid decomposition"}}, {"model": "analyses.problemtag", "pk": 19, "fields": {"key": "crt", "name_ko": "중국인의 나머지 정리", "name_en": "chinese remainder theorem"}}, {"model": "analyses.problemtag", "pk": 20, "fields": {"key": "convex_hull", "name_ko": "볼록 껍질", "name_en": "convex hull"}}, {"model": "analyses.problemtag", "pk": 21, "fields": {"key": "delaunay", "name_ko": "델로네 삼각분할", "name_en": "delaunay triangulation"}}, {"model": "analyses.problemtag", "pk": 22, "fields": {"key": "dijkstra", "name_ko": "데이크스트라", "name_en": "dijkstra's"}}, {"model": "analyses.problemtag", "pk": 23, "fields": {"key": "directed_mst", "name_ko": "유향 최소 신장 트리", "name_en": "directed minimum spanning tree"}}, {"model": "analyses.problemtag", "pk": 24, "fields": {"key": "divide_and_conquer", "name_ko": "분할 정복", "name_en": "divide and conquer"}}, {"model": "analyses.problemtag", "pk": 25, "fields": {"key": "dp", "name_ko": "다이나믹 프로그래밍", "name_en": "dynamic programming"}}, {"model": "analyses.problemtag", "pk": 26, "fields": {"key": "euclidean", "name_ko": "유클리드 호제법", "name_en": "euclidean algorithm"}}, {"model": "analyses.problemtag", "pk": 27, "fields": {"key": "extended_euclidean", "name_ko": "확장 유클리드 호제법", "name_en": "extended euclidean algorithm"}}, {"model": "analyses.problemtag", "pk": 28, "fields": {"key": "fft", "name_ko": "고속 푸리에 변환", "name_en": "fast fourier transform"}}, {"model": "analyses.problemtag", "pk": 29, "fields": {"key": "flt", "name_ko": "페르마의 소정리", "name_en": "fermat's little theorem"}}, {"model": "analyses.problemtag", "pk": 31, "fields": {"key": "floyd_warshall", "name_ko": "플로이드–워셜", "name_en": "floyd–warshall"}}, {"model": "analyses.problemtag", "pk": 32, "fields": {"key": "gaussian_elimination", "name_ko": "가우스 소거법", "name_en": "gaussian elimination"}}, {"model": "analyses.problemtag", "pk": 33, "fields": {"key": "greedy", "name_ko": "그리디 알고리즘", "name_en": "greedy"}}, {"model": "analyses.problemtag", "pk": 34, "fields": {"key": "hall", "name_ko": "홀의 결혼 정리", "name_en": "hall's theorem"}}, {"model": "analyses.problemtag", "pk": 35, "fields": {"key": "hld", "name_ko": "Heavy-light 분할", "name_en": "heavy-light decomposition"}}, {"model": "analyses.problemtag", "pk": 36, "fields": {"key": "hungarian", "name_ko": "헝가리안", "name_en": "hungarian"}}, {"model": "analyses.problemtag", "pk": 38, "fields": {"key": "inclusion_and_exclusion", "name_ko": "포함 배제의 원리", "name_en": "inclusion and exclusion"}}, {"model": "analyses.problemtag", "pk": 39, "fields": {"key": "exponentiation_by_squaring", "name_ko": "분할 정복을 이용한 거듭제곱", "name_en": "exponentiation by squaring"}}, {"model": "analyses.problemtag", "pk": 40, "fields": {"key": "kmp", "name_ko": "KMP", "name_en": "knuth–morris–pratt"}}, {"model": "analyses.problemtag", "pk": 41, "fields": {"key": "lca", "name_ko": "최소 공통 조상", "name_en": "lowest common ancestor"}}, {"model": "analyses.problemtag", "pk": 42, "fields": {"key": "line_intersection", "name_ko": "선분 교차 판정", "name_en": "line segment intersection check"}}, {"model": "analyses.problemtag", "pk": 43, "fields": {"key": "lis", "name_ko": "가장 긴 증가하는 부분 수열: O(n log n)", "name_en": "longest increasing sequence in o(n log n)"}}, {"model": "analyses.problemtag", "pk": 44, "fields": {"key": "manacher", "name_ko": "매내처", "name_en": "manacher's"}}, {"model": "analyses.problemtag", "pk": 45, "fields": {"key": "flow", "name_ko": "최대 유량", "name_en": "maximum flow"}}, {"model": "analyses.problemtag", "pk": 46, "fields": {"key": "mitm", "name_ko": "중간에서 만나기", "name_en": "meet in the middle"}}, {"model": "analyses.problemtag", "pk": 47, "fields": {"key": "miller_rabin", "name_ko": "밀러–라빈 소수 판별법", "name_en": "miller–rabin"}}, {"model": "analyses.problemtag", "pk": 48, "fields": {"key": "mcmf", "name_ko": "최소 비용 최대 유량", "name_en": "minimum cost maximum flow"}}, {"model": "analyses.problemtag", "pk": 49, "fields": {"key": "mst", "name_ko": "최소 스패닝 트리", "name_en": "minimum spanning tree"}}, {"model": "analyses.problemtag", "pk": 50, "fields": {"key": "mo", "name_ko": "mo's", "name_en": "mo's"}}, {"model": "analyses.problemtag", "pk": 51, "fields": {"key": "mobius_inversion", "name_ko": "뫼비우스 반전 공식", "name_en": "möbius inversion"}}, {"model": "analyses.problemtag", "pk": 52, "fields": {"key": "offline_dynamic_connectivity", "name_ko": "오프라인 동적 연결성 판정", "name_en": "offline dynamic connectivity"}}, {"model": "analyses.problemtag", "pk": 53, "fields": {"key": "palindrome_tree", "name_ko": "회문 트리", "name_en": "palindrome tree"}}, {"model": "analyses.problemtag", "pk": 54, "fields": {"key": "pbs", "name_ko": "병렬 이분 탐색", "name_en": "parallel binary search"}}, {"model": "analyses.problemtag", "pk": 55, "fields": {"key": "pst", "name_ko": "퍼시스턴트 세그먼트 트리", "name_en": "persistent segment tree"}}, {"model": "analyses.problemtag", "pk": 56, "fields": {"key": "point_in_convex_polygon", "name_ko": "볼록 다각형 내부의 점 판정", "name_en": "point in convex polygon check"}}, {"model": "analyses.problemtag", "pk": 57, "fields": {"key": "point_in_non_convex_polygon", "name_ko": "오목 다각형 내부의 점 판정", "name_en": "point in non-convex polygon check"}}, {"model": "analyses.problemtag", "pk": 58, "fields": {"key": "pollard_rho", "name_ko": "폴라드 로", "name_en": "pollard rho"}}, {"model": "analyses.problemtag", "pk": 59, "fields": {"key": "priority_queue", "name_ko": "우선순위 큐", "name_en": "priority queue"}}, {"model": "analyses.problemtag", "pk": 60, "fields": {"key": "pythagoras", "name_ko": "피타고라스 정리", "name_en": "pythagoras theorem"}}, {"model": "analyses.problemtag", "pk": 61, "fields": {"key": "rabin_karp", "name_ko": "라빈–카프", "name_en": "rabin–karp"}}, {"model": "analyses.problemtag", "pk": 62, "fields": {"key": "recursion", "name_ko": "재귀", "name_en": "recursion"}}, {"model": "analyses.problemtag", "pk": 63, "fields": {"key": "regex", "name_ko": "정규 표현식", "name_en": "regular expression"}}, {"model": "analyses.problemtag", "pk": 64, "fields": {"key": "rotating_calipers", "name_ko": "회전하는 캘리퍼스", "name_en": "rotating calipers"}}, {"model": "analyses.problemtag", "pk": 65, "fields": {"key": "segtree", "name_ko": "세그먼트 트리", "name_en": "segment tree"}}, {"model": "analyses.problemtag", "pk": 66, "fields": {"key": "lazyprop", "name_ko": "느리게 갱신되는 세그먼트 트리", "name_en": "segment tree with lazy propagation"}}, {"model": "analyses.problemtag", "pk": 67, "fields": {"key": "sieve", "name_ko": "에라토스테네스의 체", "name_en": "sieve of eratosthenes"}}, {"model": "analyses.problemtag", "pk": 68, "fields": {"key": "sliding_window", "name_ko": "슬라이딩 윈도우", "name_en": "sliding window"}}, {"model": "analyses.problemtag", "pk": 69, "fields": {"key": "splay_tree", "name_ko": "스플레이 트리", "name_en": "splay tree"}}, {"model": "analyses.problemtag", "pk": 70, "fields": {"key": "sprague_grundy", "name_ko": "스프라그–그런디 정리", "name_en": "sprague–grundy theorem"}}, {"model": "analyses.problemtag", "pk": 71, "fields": {"key": "stack", "name_ko": "스택", "name_en": "stack"}}, {"model": "analyses.problemtag", "pk": 72, "fields": {"key": "queue", "name_ko": "큐", "name_en": "queue"}}, {"model": "analyses.problemtag", "pk": 73, "fields": {"key": "deque", "name_ko": "덱", "name_en": "deque"}}, {"model": "analyses.problemtag", "pk": 74, "fields": {"key": "tree_set", "name_ko": "트리를 사용한 집합과 맵", "name_en": "set / map by trees"}}, {"model": "analyses.problemtag", "pk": 75, "fields": {"key": "stoer_wagner", "name_ko": "스토어–바그너", "name_en": "stoer–wagner"}}, {"model": "analyses.problemtag", "pk": 76, "fields": {"key": "scc", "name_ko": "강한 연결 요소", "name_en": "strongly connected component"}}, {"model": "analyses.problemtag", "pk": 77, "fields": {"key": "suffix_array", "name_ko": "접미사 배열과 LCP 배열", "name_en": "suffix array and lcp array"}}, {"model": "analyses.problemtag", "pk": 78, "fields": {"key": "topological_sorting", "name_ko": "위상 정렬", "name_en": "topological sorting"}}, {"model": "analyses.problemtag", "pk": 79, "fields": {"key": "trie", "name_ko": "트라이", "name_en": "trie"}}, {"model": "analyses.problemtag", "pk": 80, "fields": {"key": "two_pointer", "name_ko": "두 포인터", "name_en": "two-pointer"}}, {"model": "analyses.problemtag", "pk": 81, "fields": {"key": "disjoint_set", "name_ko": "분리 집합", "name_en": "disjoint set"}}, {"model": "analyses.problemtag", "pk": 82, "fields": {"key": "voronoi", "name_ko": "보로노이 다이어그램", "name_en": "voronoi diagram"}}, {"model": "analyses.problemtag", "pk": 83, "fields": {"key": "z", "name_ko": "z", "name_en": "z"}}, {"model": "analyses.problemtag", "pk": 84, "fields": {"key": "sparse_table", "name_ko": "희소 배열", "name_en": "sparse table"}}, {"model": "analyses.problemtag", "pk": 87, "fields": {"key": "dp_bitfield", "name_ko": "비트필드를 이용한 다이나믹 프로그래밍", "name_en": "dynamic programming using bitfield"}}, {"model": "analyses.problemtag", "pk": 89, "fields": {"key": "cht", "name_ko": "볼록 껍질을 이용한 최적화", "name_en": "convex hull trick"}}, {"model": "analyses.problemtag", "pk": 90, "fields": {"key": "knuth", "name_ko": "크누스 최적화", "name_en": "knuth optimization"}}, {"model": "analyses.problemtag", "pk": 91, "fields": {"key": "divide_and_conquer_optimization", "name_ko": "분할 정복을 사용한 최적화", "name_en": "divide and conquer optimization"}}, {"model": "analyses.problemtag", "pk": 92, "fields": {"key": "dp_tree", "name_ko": "트리에서의 다이나믹 프로그래밍", "name_en": "dynamic programming on trees"}}, {"model": "analyses.problemtag", "pk": 93, "fields": {"key": "eulerian_path", "name_ko": "오일러 경로", "name_en": "eulerian path / circuit"}}, {"model": "analyses.problemtag", "pk": 94, "fields": {"key": "rb_tree", "name_ko": "레드-블랙 트리", "name_en": "red-black tree"}}, {"model": "analyses.problemtag", "pk": 95, "fields": {"key": "number_theory", "name_ko": "정수론", "name_en": "number theory"}}, {"model": "analyses.problemtag", "pk": 96, "fields": {"key": "parsing", "name_ko": "파싱", "name_en": "parsing"}}, {"model": "analyses.problemtag", "pk": 97, "fields": {"key": "sorting", "name_ko": "정렬", "name_en": "sorting"}}, {"model": "analyses.problemtag", "pk": 98, "fields": {"key": "link_cut_tree", "name_ko": "링크/컷 트리", "name_en": "link/cut tree"}}, {"model": "analyses.problemtag", "pk": 100, "fields": {"key": "geometry", "name_ko": "기하학", "name_en": "geometry"}}, {"model": "analyses.problemtag", "pk": 101, "fields": {"key": "ternary_search", "name_ko": "삼분 탐색", "name_en": "ternary search"}}, {"model": "analyses.problemtag", "pk": 102, "fields": {"key": "implementation", "name_ko": "구현", "name_en": "implementation"}}, {"model": "analyses.problemtag", "pk": 103, "fields": {"key": "linear_programming", "name_ko": "선형 계획법", "name_en": "linear programming"}}, {"model": "analyses.problemtag", "pk": 104, "fields": {"key": "matroid", "name_ko": "매트로이드", "name_en": "matroid"}}, {"model": "analyses.problemtag", "pk": 105, "fields": {"key": "top_tree", "name_ko": "탑 트리", "name_en": "top tree"}}, {"model": "analyses.problemtag", "pk": 106, "fields": {"key": "sweeping", "name_ko": "스위핑", "name_en": "sweeping"}}, {"model": "analyses.problemtag", "pk": 107, "fields": {"key": "dp_connection_profile", "name_ko": "커넥션 프로파일을 이용한 다이나믹 프로그래밍", "name_en": "dynamic programming using connection profile"}}, {"model": "analyses.problemtag", "pk": 108, "fields": {"key": "dp_deque", "name_ko": "덱을 이용한 다이나믹 프로그래밍", "name_en": "dynamic programming using a deque"}}, {"model": "analyses.problemtag", "pk": 109, "fields": {"key": "ad_hoc", "name_ko": "애드 혹", "name_en": "ad-hoc"}}, {"model": "analyses.problemtag", "pk": 110, "fields": {"key": "berlekamp_massey", "name_ko": "벌리캠프–매시", "name_en": "berlekamp–massey"}}, {"model": "analyses.problemtag", "pk": 111, "fields": {"key": "calculus", "name_ko": "미적분학", "name_en": "calculus"}}, {"model": "analyses.problemtag", "pk": 112, "fields": {"key": "kitamasa", "name_ko": "키타마사", "name_en": "kitamasa"}}, {"model": "analyses.problemtag", "pk": 113, "fields": {"key": "lucas", "name_ko": "뤼카 정리", "name_en": "lucas theorem"}}, {"model": "analyses.problemtag", "pk": 114, "fields": {"key": "bayes", "name_ko": "베이즈 정리", "name_en": "bayes theorem"}}, {"model": "analyses.problemtag", "pk": 115, "fields": {"key": "randomization", "name_ko": "무작위화", "name_en": "randomization"}}, {"model": "analyses.problemtag", "pk": 116, "fields": {"key": "physics", "name_ko": "물리학", "name_en": "physics"}}, {"model": "analyses.problemtag", "pk": 117, "fields": {"key": "arbitrary_precision", "name_ko": "임의 정밀도 / 큰 수 연산", "name_en": "arbitrary precision / big integers"}}, {"model": "analyses.problemtag", "pk": 119, "fields": {"key": "euler_characteristic", "name_ko": "오일러 지표 (χ=V-E+F)", "name_en": "euler characteristic (χ=v-e+f)"}}, {"model": "analyses.problemtag", "pk": 120, "fields": {"key": "trees", "name_ko": "트리", "name_en": "tree"}}, {"model": "analyses.problemtag", "pk": 121, "fields": {"key": "arithmetic", "name_ko": "사칙연산", "name_en": "arithmetic"}}, {"model": "analyses.problemtag", "pk": 122, "fields": {"key": "numerical_analysis", "name_ko": "수치해석", "name_en": "numerical analysis"}}, {"model": "analyses.problemtag", "pk": 123, "fields": {"key": "offline_queries", "name_ko": "오프라인 쿼리", "name_en": "offline queries"}}, {"model": "analyses.problemtag", "pk": 124, "fields": {"key": "math", "name_ko": "수학", "name_en": "mathematics"}}, {"model": "analyses.problemtag", "pk": 125, "fields": {"key": "bruteforcing", "name_ko": "브루트포스 알고리즘", "name_en": "bruteforcing"}}, {"model": "analyses.problemtag", "pk": 126, "fields": {"key": "bfs", "name_ko": "너비 우선 탐색", "name_en": "breadth-first search"}}, {"model": "analyses.problemtag", "pk": 127, "fields": {"key": "dfs", "name_ko": "깊이 우선 탐색", "name_en": "depth-first search"}}, {"model": "analyses.problemtag", "pk": 128, "fields": {"key": "constructive", "name_ko": "해 구성하기", "name_en": "constructive"}}, {"model": "analyses.problemtag", "pk": 129, "fields": {"key": "bidirectional_search", "name_ko": "양방향 탐색", "name_en": "bidirectional search"}}, {"model": "analyses.problemtag", "pk": 130, "fields": {"key": "sqrt_decomposition", "name_ko": "제곱근 분할법", "name_en": "square root decomposition"}}, {"model": "analyses.problemtag", "pk": 131, "fields": {"key": "geometry_3d", "name_ko": "3차원 기하학", "name_en": "geometry; 3d"}}, {"model": "analyses.problemtag", "pk": 132, "fields": {"key": "geometry_hyper", "name_ko": "4차원 이상의 기하학", "name_en": "geometry; hyperdimensional"}}, {"model": "analyses.problemtag", "pk": 134, "fields": {"key": "alien", "name_ko": "Aliens 트릭", "name_en": "aliens trick"}}, {"model": "analyses.problemtag", "pk": 135, "fields": {"key": "dominator_tree", "name_ko": "도미네이터 트리", "name_en": "dominator tree"}}, {"model": "analyses.problemtag", "pk": 136, "fields": {"key": "hash_set", "name_ko": "해시를 사용한 집합과 맵", "name_en": "set / map by hashing"}}, {"model": "analyses.problemtag", "pk": 137, "fields": {"key": "case_work", "name_ko": "많은 조건 분기", "name_en": "case work"}}, {"model": "analyses.problemtag", "pk": 138, "fields": {"key": "tsp", "name_ko": "외판원 순회 문제", "name_en": "travelling salesman problem"}}, {"model": "analyses.problemtag", "pk": 139, "fields": {"key": "prefix_sum", "name_ko": "누적 합", "name_en": "prefix sum"}}, {"model": "analyses.problemtag", "pk": 140, "fields": {"key": "game_theory", "name_ko": "게임 이론", "name_en": "game theory"}}, {"model": "analyses.problemtag", "pk": 141, "fields": {"key": "simulation", "name_ko": "시뮬레이션", "name_en": "simulation"}}, {"model": "analyses.problemtag", "pk": 142, "fields": {"key": "heuristics", "name_ko": "휴리스틱", "name_en": "heuristics"}}, {"model": "analyses.problemtag", "pk": 143, "fields": {"key": "cactus", "name_ko": "선인장", "name_en": "cactus"}}, {"model": "analyses.problemtag", "pk": 144, "fields": {"key": "linear_algebra", "name_ko": "선형대수학", "name_en": "linear algebra"}}, {"model": "analyses.problemtag", "pk": 145, "fields": {"key": "tree_isomorphism", "name_ko": "트리 동형 사상", "name_en": "tree isomorphism"}}, {"model": "analyses.problemtag", "pk": 146, "fields": {"key": "discrete_log", "name_ko": "이산 로그", "name_en": "discrete logarithm"}}, {"model": "analyses.problemtag", "pk": 147, "fields": {"key": "discrete_sqrt", "name_ko": "이산 제곱근", "name_en": "discrete square root"}}, {"model": "analyses.problemtag", "pk": 148, "fields": {"key": "knapsack", "name_ko": "배낭 문제", "name_en": "knapsack"}}, {"model": "analyses.problemtag", "pk": 149, "fields": {"key": "discrete_kth_root", "name_ko": "이산 k제곱근", "name_en": "discrete k-th root"}}, {"model": "analyses.problemtag", "pk": 150, "fields": {"key": "euler_tour_technique", "name_ko": "오일러 경로 테크닉", "name_en": "euler tour technique"}}, {"model": "analyses.problemtag", "pk": 151, "fields": {"key": "euler_phi", "name_ko": "오일러 피 함수", "name_en": "euler totient function"}}, {"model": "analyses.problemtag", "pk": 152, "fields": {"key": "bitset", "name_ko": "비트 집합", "name_en": "bit set"}}, {"model": "analyses.problemtag", "pk": 153, "fields": {"key": "biconnected_component", "name_ko": "이중 연결 요소", "name_en": "biconnected component"}}, {"model": "analyses.problemtag", "pk": 154, "fields": {"key": "linked_list", "name_ko": "연결 리스트", "name_en": "linked list"}}, {"model": "analyses.problemtag", "pk": 155, "fields": {"key": "merge_sort_tree", "name_ko": "머지 소트 트리", "name_en": "merge sort tree"}}, {"model": "analyses.problemtag", "pk": 157, "fields": {"key": "slope_trick", "name_ko": "함수 개형을 이용한 최적화", "name_en": "slope trick"}}, {"model": "analyses.problemtag", "pk": 158, "fields": {"key": "string", "name_ko": "문자열", "name_en": "string"}}, {"model": "analyses.problemtag", "pk": 159, "fields": {"key": "rope", "name_ko": "로프", "name_en": "rope"}}, {"model": "analyses.problemtag", "pk": 160, "fields": {"key": "majority_vote", "name_ko": "보이어–무어 다수결 투표", "name_en": "boyer–moore majority vote"}}, {"model": "analyses.problemtag", "pk": 161, "fields": {"key": "coordinate_compression", "name_ko": "값 / 좌표 압축", "name_en": "value / coordinate compression"}}, {"model": "analyses.problemtag", "pk": 162, "fields": {"key": "min_enclosing_circle", "name_ko": "최소 외접원", "name_en": "minimum enclosing circle"}}, {"model": "analyses.problemtag", "pk": 163, "fields": {"key": "hirschberg", "name_ko": "히르쉬버그", "name_en": "hirschberg's"}}, {"model": "analyses.problemtag", "pk": 164, "fields": {"key": "modular_multiplicative_inverse", "name_ko": "모듈로 곱셈 역원", "name_en": "modular multiplicative inverse"}}, {"model": "analyses.problemtag", "pk": 165, "fields": {"key": "monotone_queue_optimization", "name_ko": "단조 큐를 이용한 최적화", "name_en": "monotone queue optimization"}}, {"model": "analyses.problemtag", "pk": 166, "fields": {"key": "multi_segtree", "name_ko": "다차원 세그먼트 트리", "name_en": "multidimensional segment tree"}}, {"model": "analyses.problemtag", "pk": 167, "fields": {"key": "mfmc", "name_ko": "최대 유량 최소 컷 정리", "name_en": "max-flow min-cut theorem"}}, {"model": "analyses.problemtag", "pk": 168, "fields": {"key": "planar_graph", "name_ko": "평면 그래프", "name_en": "planar graph"}}, {"model": "analyses.problemtag", "pk": 169, "fields": {"key": "smaller_to_larger", "name_ko": "작은 집합에서 큰 집합으로 합치는 테크닉", "name_en": "smaller to larger technique"}}, {"model": "analyses.problemtag", "pk": 170, "fields": {"key": "parametric_search", "name_ko": "매개 변수 탐색", "name_en": "parametric search"}}, {"model": "analyses.problemtag", "pk": 171, "fields": {"key": "permutation_cycle_decomposition", "name_ko": "순열 사이클 분할", "name_en": "permutation cycle decomposition"}}, {"model": "analyses.problemtag", "pk": 172, "fields": {"key": "precomputation", "name_ko": "런타임 전의 전처리", "name_en": "precomputation"}}, {"model": "analyses.problemtag", "pk": 173, "fields": {"key": "dancing_links", "name_ko": "춤추는 링크", "name_en": "dancing links"}}, {"model": "analyses.problemtag", "pk": 174, "fields": {"key": "knuth_x", "name_ko": "크누스 X", "name_en": "knuth's x"}}, {"model": "analyses.problemtag", "pk": 175, "fields": {"key": "data_structures", "name_ko": "자료 구조", "name_en": "data structures"}}, {"model": "analyses.problemtag", "pk": 176, "fields": {"key": "0_1_bfs", "name_ko": "0-1 너비 우선 탐색", "name_en": "0-1 bfs"}}, {"model": "analyses.problemtag", "pk": 177, "fields": {"key": "probability", "name_ko": "확률론", "name_en": "probability theory"}}, {"model": "analyses.problemtag", "pk": 178, "fields": {"key": "statistics", "name_ko": "통계학", "name_en": "statistics"}}, {"model": "analyses.problemtag", "pk": 179, "fields": {"key": "linearity_of_expectation", "name_ko": "기댓값의 선형성", "name_en": "linearity of expectation"}}, {"model": "analyses.problemtag", "pk": 180, "fields": {"key": "duality", "name_ko": "쌍대성", "name_en": "duality"}}, {"model": "analyses.problemtag", "pk": 181, "fields": {"key": "dual_graph", "name_ko": "쌍대 그래프", "name_en": "dual graph"}}, {"model": "analyses.problemtag", "pk": 182, "fields": {"key": "suffix_tree", "name_ko": "접미사 트리", "name_en": "suffix tree"}}, {"model": "analyses.problemtag", "pk": 183, "fields": {"key": "green", "name_ko": "그린 정리", "name_en": "green's theorem"}}, {"model": "analyses.problemtag", "pk": 184, "fields": {"key": "simulated_annealing", "name_ko": "담금질 기법", "name_en": "simulated annealing"}}, {"model": "analyses.problemtag", "pk": 185, "fields": {"key": "differential_cryptanalysis", "name_ko": "차분 공격", "name_en": "differential cryptanalysis"}}, {"model": "analyses.problemtag", "pk": 186, "fields": {"key": "a_star", "name_ko": "a*", "name_en": "a*"}}, {"model": "analyses.problemtag", "pk": 187, "fields": {"key": "pick", "name_ko": "픽의 정리", "name_en": "pick's theorem"}}, {"model": "analyses.problemtag", "pk": 188, "fields": {"key": "centroid", "name_ko": "centroid", "name_en": "센트로이드"}}, {"model": "analyses.problemtag", "pk": 189, "fields": {"key": "pigeonhole_principle", "name_ko": "비둘기집 원리", "name_en": "pigeonhole principle"}}, {"model": "analyses.problemtag", "pk": 190, "fields": {"key": "half_plane_intersection", "name_ko": "반평면 교집합", "name_en": "half plane intersection"}}, {"model": "analyses.problemtag", "pk": 191, "fields": {"key": "circulation", "name_ko": "서큘레이션", "name_en": "circulation"}}, {"model": "analyses.problemtag", "pk": 192, "fields": {"key": "stable_marriage", "name_ko": "안정 결혼 문제", "name_en": "stable marriage problem"}}, {"model": "analyses.problemtag", "pk": 193, "fields": {"key": "tree_compression", "name_ko": "트리 압축", "name_en": "tree compression"}}, {"model": "analyses.problemtag", "pk": 196, "fields": {"key": "multipoint_evaluation", "name_ko": "다중 대입값 계산", "name_en": "multipoint evaluation"}}, {"model": "analyses.problemtag", "pk": 197, "fields": {"key": "bipartite_graph", "name_ko": "이분 그래프", "name_en": "bipartite graph"}}, {"model": "analyses.problemtag", "pk": 198, "fields": {"key": "generating_function", "name_ko": "생성 함수", "name_en": "generating function"}}, {"model": "analyses.problemtag", "pk": 199, "fields": {"key": "utf8", "name_ko": "utf-8 입력 처리", "name_en": "utf-8 inputs"}}, {"model": "analyses.problemtag", "pk": 200, "fields": {"key": "degree_sequence", "name_ko": "차수열", "name_en": "degree sequence"}}, {"model": "analyses.problemtag", "pk": 201, "fields": {"key": "chordal_graph", "name_ko": "chordal graph", "name_en": "현 그래프"}}, {"model": "analyses.problemtag", "pk": 202, "fields": {"key": "geometric_boolean_operations", "name_ko": "도형에서의 불 연산", "name_en": "boolean operations on geometric objects"}}, {"model": "analyses.problemtag", "pk": 203, "fields": {"key": "birthday", "name_ko": "생일 문제", "name_en": "birthday problem"}}, {"model": "analyses.problemtag", "pk": 204, "fields": {"key": "tree_decomposition", "name_ko": "트리 분할", "name_en": "tree decomposition"}}, {"model": "analyses.problemtag", "pk": 205, "fields": {"key": "hackenbush", "name_ko": "하켄부시 게임", "name_en": "hackenbush"}}, {"model": "analyses.problemtag", "pk": 206, "fields": {"key": "cartesian_tree", "name_ko": "데카르트 트리", "name_en": "cartesian tree"}}, {"model": "analyses.problemtag", "pk": 207, "fields": {"key": "dp_sum_over_subsets", "name_ko": "부분집합의 합 다이나믹 프로그래밍", "name_en": "sum over subsets dynamic programming"}}, {"model": "analyses.problemtag", "pk": 208, "fields": {"key": "gradient_descent", "name_ko": "경사 하강법", "name_en": "gradient descent"}}, {"model": "analyses.problemtag", "pk": 209, "fields": {"key": "polynomial_interpolation", "name_ko": "다항식 보간법", "name_en": "polynomial interpolation"}}, {"model": "analyses.problemtag", "pk": 210, "fields": {"key": "flood_fill", "name_ko": "플러드 필", "name_en": "flood-fill"}}, {"model": "analyses.problemtag", "pk": 211, "fields": {"key": "functional_graph", "name_ko": "함수형 그래프", "name_en": "functional graph"}}, {"model": "analyses.problemtag", "pk": 212, "fields": {"key": "lte", "name_ko": "지수승강 보조정리", "name_en": "lifting the exponent lemma"}}, {"model": "analyses.problemtag", "pk": 213, "fields": {"key": "dag", "name_ko": "방향 비순환 그래프", "name_en": "directed acyclic graph"}}, {"model": "analyses.problemtag", "pk": 214, "fields": {"key": "lgv", "name_ko": "린드스트롬–게셀–비엔노 보조정리", "name_en": "lindström–gessel–viennot lemma"}}, {"model": "analyses.problemtag", "pk": 215, "fields": {"key": "shortest_path", "name_ko": "최단 경로", "name_en": "shortest path"}}, {"model": "analyses.problemtag", "pk": 216, "fields": {"key": "deque_trick", "name_ko": "덱을 이용한 구간 최댓값 트릭", "name_en": "deque range maximum trick"}}, {"model": "analyses.problemtag", "pk": 217, "fields": {"key": "dp_digit", "name_ko": "자릿수를 이용한 다이나믹 프로그래밍", "name_en": "digit dp"}}, {"model": "analyses.problemtag", "pk": 218, "fields": {"key": "floor_sum", "name_ko": "유리 등차수열의 내림 합", "name_en": "sum of floor of rational arithmetic sequence"}}] \ No newline at end of file From a6f991a648694efc4e9d351fca6d1c50338a6abb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:30:22 +0900 Subject: [PATCH 464/552] =?UTF-8?q?test(users):=20fixture=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20(test=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/fixtures/user.sample.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 app/users/fixtures/user.sample.json diff --git a/app/users/fixtures/user.sample.json b/app/users/fixtures/user.sample.json new file mode 100644 index 0000000..b38895a --- /dev/null +++ b/app/users/fixtures/user.sample.json @@ -0,0 +1,22 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", + "last_login": null, + "username": "test", + "email": "test@example.com", + "profile_image": "", + "boj_username": "test", + "is_active": true, + "is_staff": false, + "is_superuser": false, + "first_name": "", + "last_name": "", + "created_at": "2024-08-30T06:23:35", + "groups": [], + "user_permissions": [] + } + } +] \ No newline at end of file From 485e11e1d5c7c6fcabd7228cf5ca447013251a66 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:40:19 +0900 Subject: [PATCH 465/552] =?UTF-8?q?test(users):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=93=A4?= =?UTF-8?q?=EC=97=AC=EC=93=B0=EA=B8=B0=20=EC=8A=A4=ED=83=80=EC=9D=BC=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index 6db20d9..a5e2504 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -87,9 +87,12 @@ class UsernameCheckTest(TestCase): fixtures = ['user.sample.json'] def test_사용_가능한_사용자명(self): - res = self.client.get("/api/v1/auth/username/check", { - "username": "unique", - }) + res = self.client.get( + "/api/v1/auth/username/check", + { + "username": "unique", + } + ) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "username": "unique", @@ -97,9 +100,12 @@ def test_사용_가능한_사용자명(self): }) def test_사용_불가능한_사용자명(self): - res = self.client.get("/api/v1/auth/username/check", { - "username": "test", - }) + res = self.client.get( + "/api/v1/auth/username/check", + { + "username": "test", + } + ) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "username": "test", From 739c8f7210f0c4aebd446d749475f26b72dca410 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:40:43 +0900 Subject: [PATCH 466/552] =?UTF-8?q?test(users):=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=93=A4=EC=97=AC?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index a5e2504..3cac95e 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -117,9 +117,12 @@ class EmailCheckTest(TestCase): fixtures = ['user.sample.json'] def test_사용_가능한_이메일(self): - res = self.client.get("/api/v1/auth/email/check", { - "email": "unique@notexample.com", - }) + res = self.client.get( + "/api/v1/auth/email/check", + { + "email": "unique@notexample.com", + } + ) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": "unique@notexample.com", @@ -127,9 +130,12 @@ def test_사용_가능한_이메일(self): }) def test_사용_불가능한_이메일(self): - res = self.client.get("/api/v1/auth/email/check", { - "email": "test@example.com", - }) + res = self.client.get( + "/api/v1/auth/email/check", + { + "email": "test@example.com", + } + ) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": "test@example.com", From 0c78567a67d8735ce57867c21568c8dbad8ba63b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 07:56:28 +0900 Subject: [PATCH 467/552] =?UTF-8?q?test(crews):=20fixture=20=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/fixtures/sample.json | 143 +++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 app/crews/fixtures/sample.json diff --git a/app/crews/fixtures/sample.json b/app/crews/fixtures/sample.json new file mode 100644 index 0000000..9ee63d4 --- /dev/null +++ b/app/crews/fixtures/sample.json @@ -0,0 +1,143 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", + "last_login": null, + "username": "test", + "email": "test@example.com", + "profile_image": "", + "boj_username": "hepheir", + "is_active": true, + "is_staff": false, + "is_superuser": false, + "first_name": "", + "last_name": "", + "created_at": "2024-08-30T06:23:35", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "users.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", + "last_login": null, + "username": "test2", + "email": "test2@example.com", + "profile_image": "", + "boj_username": "test", + "is_active": true, + "is_staff": false, + "is_superuser": false, + "first_name": "", + "last_name": "", + "created_at": "2024-08-30T06:23:35", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "boj.bojuser", + "pk": 1, + "fields": { + "username": "hepheir", + "level": 17, + "rating": 1801, + "updated_at": "2024-08-30T04:02:23.327" + } + }, + { + "model": "boj.bojuser", + "pk": 2, + "fields": { + "username": "test", + "level": 1, + "rating": 20, + "updated_at": "2024-08-30T04:02:23.327" + } + }, + { + "model": "crews.crew", + "pk": 1, + "fields": { + "name": "코딩메리호", + "icon": "🚢", + "max_members": 8, + "notice": "공지사항 테스트입니다.", + "custom_tags": [ + "삼성코테준비", + "상명인 모여라" + ], + "min_boj_level": 6, + "is_recruiting": true, + "is_active": true, + "created_at": "2024-08-14T17:44:45.983", + "created_by": 1, + "updated_at": "2024-08-14T17:44:45.983" + } + }, + { + "model": "crews.crew", + "pk": 2, + "fields": { + "name": "다른사람이크루장", + "icon": "🚢", + "max_members": 6, + "notice": "공지사항 테스트입니다.", + "custom_tags": [], + "min_boj_level": 2, + "is_recruiting": true, + "is_active": true, + "created_at": "2024-08-14T17:44:45.983", + "created_by": 2, + "updated_at": "2024-08-14T17:44:45.983" + } + }, + { + "model": "crews.crewsubmittablelanguage", + "pk": 1, + "fields": { + "crew": 1, + "language": "java" + } + }, + { + "model": "crews.crewsubmittablelanguage", + "pk": 2, + "fields": { + "crew": 1, + "language": "kotlin" + } + }, + { + "model": "crews.crewsubmittablelanguage", + "pk": 3, + "fields": { + "crew": 2, + "language": "python" + } + }, + { + "model": "crews.crewmember", + "pk": 1, + "fields": { + "crew": 1, + "user": 1, + "is_captain": true, + "created_at": "2024-08-14T17:44:45.983" + } + }, + { + "model": "crews.crewmember", + "pk": 2, + "fields": { + "crew": 2, + "user": 2, + "is_captain": true, + "created_at": "2024-08-14T17:44:45.983" + } + } +] \ No newline at end of file From 0d6c9af0a60b04d1f42ca98e98b97aa625072c72 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 08:06:35 +0900 Subject: [PATCH 468/552] =?UTF-8?q?fix(crews):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EC=84=A4=EC=A0=95=20=EB=AA=A8=EB=8D=B8=20Manager?= =?UTF-8?q?=EC=9D=98=20=EC=BF=BC=EB=A6=AC=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 커스텀 모델 매니져의 메소드들은 자기 자신을 반환하지 않고 쿼리셋을 반환한다. --- app/crews/applications/views.py | 2 +- app/crews/models.py | 34 ++++++++++++++++++++------------- app/crews/views.py | 7 +++++-- 3 files changed, 27 insertions(+), 16 deletions(-) diff --git a/app/crews/applications/views.py b/app/crews/applications/views.py index c0a5fbe..e5e3ff9 100644 --- a/app/crews/applications/views.py +++ b/app/crews/applications/views.py @@ -26,7 +26,7 @@ def get_queryset(self): def get_crew(self) -> Crew: crew_id = self.kwargs[self.lookup_url_kwarg] try: - return Crew.objects.as_captain(self.request.user).get(pk=crew_id) + return Crew.objects.filter(as_captain=self.request.user).get(pk=crew_id) except Crew.DoesNotExist: raise ValidationError("크루가 존재하지 않거나, 권한이 없습니다.") diff --git a/app/crews/models.py b/app/crews/models.py index 41e409c..872b052 100644 --- a/app/crews/models.py +++ b/app/crews/models.py @@ -16,17 +16,26 @@ class CrewManager(models.Manager): - def as_captain(self, user: User) -> _CrewManager: - return self.filter(pk__in=self._ids_as_captain(user)) - - def as_member(self, user: User) -> _CrewManager: - return self.filter(pk__in=self._ids_as_member(user)) - - def not_as_member(self, user: User) -> _CrewManager: - return self.exclude(pk__in=self._ids_as_member(user)) - - def recruiting(self) -> _CrewManager: - return self.filter(**{Crew.field_name.IS_RECRUITING: True}) + def filter(self, + as_captain: User = None, + as_member: User = None, + not_as_member: User = None, + is_recruiting: bool = None, + *args, + **kwargs) -> models.QuerySet[Crew]: + if is_recruiting is not None: + kwargs[Crew.field_name.IS_RECRUITING] = is_recruiting + queryset = super().filter(*args, **kwargs) + if as_captain is not None: + assert isinstance(as_captain, User) + queryset = queryset.filter(pk__in=self._ids_as_captain(as_captain)) + if as_member is not None: + assert isinstance(as_member, User) + queryset = queryset.filter(pk__in=self._ids_as_member(as_member)) + if not_as_member is not None: + assert isinstance(not_as_member, User) + queryset = queryset.exclude(pk__in=self._ids_as_member(not_as_member)) + return queryset def _ids_as_captain(self, user: User) -> List[int]: return CrewMember.objects.user(user).captains().values_list(CrewMember.field_name.CREW, flat=True) @@ -94,7 +103,7 @@ class Crew(models.Model): ) updated_at = models.DateTimeField(auto_now=True) - objects: _CrewManager = CrewManager() + objects: CrewManager = CrewManager() class field_name: NAME = 'name' @@ -269,7 +278,6 @@ def __str__(self) -> str: return f'[{self.pk} : #{self.language}]' -_CrewManager = Union[CrewManager, models.Manager[Crew]] _CrewMemberManager = Union[CrewMemberManager, models.Manager[CrewMember]] _CrewSubmittableLanguageManager = Union[CrewSubmittableLanguageManager, models.Manager[CrewSubmittableLanguage]] diff --git a/app/crews/views.py b/app/crews/views.py index 8b2d53d..81ca29d 100644 --- a/app/crews/views.py +++ b/app/crews/views.py @@ -19,7 +19,10 @@ class RecruitingCrewListAPIView(generics.ListAPIView): serializer_class = RecruitingCrewSerializer def get_queryset(self): - return Crew.objects.recruiting().not_as_member(self.request.user) + return Crew.objects.filter( + not_as_member=self.request.user, + is_recruiting=True, + ) class MyCrewListAPIView(generics.ListAPIView): @@ -28,7 +31,7 @@ class MyCrewListAPIView(generics.ListAPIView): serializer_class = MyCrewSerializer def get_queryset(self): - return Crew.objects.as_member(self.request.user).order_by( + return Crew.objects.filter(as_member=self.request.user).order_by( Crew.field_name.IS_ACTIVE, Crew.field_name.UPDATED_AT, ).reverse() From fb025bf2b17da47b089ca88708ff17a1d72f621d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 08:56:06 +0900 Subject: [PATCH 469/552] =?UTF-8?q?test(crews):=20Recruiting,=20My=20?= =?UTF-8?q?=ED=81=AC=EB=A3=A8=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/tests.py | 102 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 app/crews/tests.py diff --git a/app/crews/tests.py b/app/crews/tests.py new file mode 100644 index 0000000..9491060 --- /dev/null +++ b/app/crews/tests.py @@ -0,0 +1,102 @@ +from django.test import TestCase +from rest_framework import status + +from users.models import User + + +class CrewAPITest(TestCase): + fixtures = ['sample.json'] + maxDiff = None + + def setUp(self) -> None: + self.user = User.objects.get(pk=1) + self.client.force_login(self.user) + + def test_비로그인_사용자로_recruiting_크루_목록_조회(self): + self.client.logout() + res = self.client.get("/api/v1/crews/recruiting") + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertListEqual(res.json(), [ + { + "id": 1, + "icon": "🚢", + "name": "코딩메리호", + "is_joinable": False, + "is_active": True, + "members": {"count": 1, "max_count": 8}, + "tags": [ + {"type": "language", "key": "java", "name": "Java"}, + {"type": "language", "key": "kotlin", "name": "Kotlin"}, + {"type": "level", "key": None, "name": "실버 이상"}, + {"type": "custom", "key": None, "name": "삼성코테준비"}, + {"type": "custom", "key": None, "name": "상명인 모여라"} + ], + "latest_activity": { + "name": "등록된 활동 없음", + "date_start_at": None, + "date_end_at": None + } + }, + { + "id": 2, + "icon": "🚢", + "name": "다른사람이크루장", + "is_joinable": False, + "is_active": True, + "members": {"count": 1, "max_count": 6}, + "tags": [ + {"type": "language", "key": "python", "name": "Python"}, + {"type": "level", "key": None, "name": "브론즈 4 이상"}, + ], + "latest_activity": { + "name": "등록된 활동 없음", + "date_start_at": None, + "date_end_at": None + } + }, + ]) + + def test_로그인_사용자로_recruiting_크루_목록_조회(self): + res = self.client.get("/api/v1/crews/recruiting") + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertListEqual(res.json(), [ + { + "id": 2, + "icon": "🚢", + "name": "다른사람이크루장", + "is_joinable": True, + "is_active": True, + "members": {"count": 1, "max_count": 6}, + "tags": [ + {"type": "language", "key": "python", "name": "Python"}, + {"type": "level", "key": None, "name": "브론즈 4 이상"}, + ], + "latest_activity": { + "name": "등록된 활동 없음", + "date_start_at": None, + "date_end_at": None + } + }, + ]) + + def test_비로그인_사용자로_my_크루_목록_조회_불가능(self): + self.client.logout() + res = self.client.get("/api/v1/crews/my") + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_my_크루_목록_조회(self): + res = self.client.get("/api/v1/crews/my") + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertListEqual(res.json(), [ + { + "id": 1, + "icon": "🚢", + "name": "코딩메리호", + "is_active": True, + "latest_activity": { + "name": "등록된 활동 없음", + "date_start_at": None, + "date_end_at": None + } + }, + ]) From 29f8a35b5d535adf1a2da2ce1a352b00d4025ad6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 08:58:18 +0900 Subject: [PATCH 470/552] =?UTF-8?q?fix(crews.models):=20CrewManager?= =?UTF-8?q?=EC=9D=98=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/applications/services.py | 6 +++--- app/crews/models.py | 18 ++++-------------- app/crews/serializers.py | 4 ++-- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/app/crews/applications/services.py b/app/crews/applications/services.py index 5b2c6a1..46b12e5 100644 --- a/app/crews/applications/services.py +++ b/app/crews/applications/services.py @@ -31,10 +31,10 @@ def is_valid_applicant(crew: Crew, applicant: User, raise_exception=False) -> bo assert crew.is_recruiting, ( "'크루가 현재 크루원을 모집하고 있지 않습니다." ) - assert CrewMember.objects.crew(crew).count() < crew.max_members, ( + assert CrewMember.objects.filter(crew=crew).count() < crew.max_members, ( "크루의 최대 정원을 초과하였습니다." ) - assert not CrewMember.objects.crew(crew).user(applicant).exists(), ( + assert not CrewMember.objects.filter(crew=crew, user=applicant).exists(), ( "이미 가입한 크루입니다." ) assert (crew.min_boj_level is None) or (crew.min_boj_level <= boj_user.level), ( @@ -81,7 +81,7 @@ def notify_on_reviewed(sender, instance: CrewApplication, **kwargs): def notify_application_recieved(instance: CrewApplication): - captain = CrewMember.objects.crew(instance.crew).captains().get() + captain = CrewMember.objects.filter(crew=instance.crew, is_captain=True).get() boj_user = BOJUser.objects.get(username=instance.applicant.boj_username) subject = '[Time Limit Exceeded] 새로운 크루 가입 신청이 도착했습니다' message = dedent(f""" diff --git a/app/crews/models.py b/app/crews/models.py index 872b052..dc2bbd3 100644 --- a/app/crews/models.py +++ b/app/crews/models.py @@ -6,6 +6,7 @@ from django.core.validators import MaxValueValidator from django.core.validators import MinValueValidator from django.db import models +from django.contrib.auth.models import AnonymousUser from boj.enums import BOJLevel from crews.dto import CrewTagDTO @@ -32,16 +33,16 @@ def filter(self, if as_member is not None: assert isinstance(as_member, User) queryset = queryset.filter(pk__in=self._ids_as_member(as_member)) - if not_as_member is not None: + if not_as_member is not None and not isinstance(not_as_member, AnonymousUser): assert isinstance(not_as_member, User) queryset = queryset.exclude(pk__in=self._ids_as_member(not_as_member)) return queryset def _ids_as_captain(self, user: User) -> List[int]: - return CrewMember.objects.user(user).captains().values_list(CrewMember.field_name.CREW, flat=True) + return CrewMember.objects.filter(user=user, is_captain=True).values_list(CrewMember.field_name.CREW, flat=True) def _ids_as_member(self, user: User) -> List[int]: - return CrewMember.objects.user(user).values_list(CrewMember.field_name.CREW, flat=True) + return CrewMember.objects.filter(user=user).values_list(CrewMember.field_name.CREW, flat=True) class Crew(models.Model): @@ -183,17 +184,6 @@ def filter(self, def get_captain(self, crew: Crew) -> CrewMember: return self.filter(crew=crew, is_captain=True).get() - # TODO: 아래의 메소드들이 체인이 가능한지 검사. - - def captains(self) -> _CrewMemberManager: - return self.filter(**{CrewMember.field_name.IS_CAPTAIN: True}) - - def crew(self, crew: Crew) -> _CrewMemberManager: - return self.filter(**{CrewMember.field_name.CREW: crew}) - - def user(self, user: User) -> _CrewMemberManager: - return self.filter(**{CrewMember.field_name.USER: user}) - class CrewMember(models.Model): crew = models.ForeignKey( diff --git a/app/crews/serializers.py b/app/crews/serializers.py index 769ce3c..4971d14 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -66,7 +66,7 @@ def to_representation(self, value): def get_attribute(self, instance: Crew): assert isinstance(instance, Crew) user = self.__class__.current_user(self) - return CrewMember.objects.crew(instance).user(user).captains().exists() + return CrewMember.objects.filter(crew=instance, user=user, is_captain=True).exists() class MemberField(serializers.SerializerMethodField): @@ -75,7 +75,7 @@ def __init__(self, include_member_details: bool, **kwargs): self.include_member_details = include_member_details def to_representation(self, crew: Crew): - members = CrewMember.objects.crew(crew) + members = CrewMember.objects.filter(crew=crew) data = { "count": members.count(), "max_count": crew.max_members, From 96c30b6baa0aee8796f0b5f30c94d57ffda4daaa Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 08:58:54 +0900 Subject: [PATCH 471/552] =?UTF-8?q?fix(crews.serializers):=20CrewTagSerial?= =?UTF-8?q?izer=EC=9D=98=20=EB=8F=99=EC=9E=91=20=EC=98=A4=EB=A5=98=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers.py | 70 +++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/app/crews/serializers.py b/app/crews/serializers.py index 4971d14..90b1433 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -1,12 +1,15 @@ +from typing import List +from typing import Union + from django.db.models import QuerySet from django.db.transaction import atomic from rest_framework import serializers -from crews import enums -from crews import models from crews.activities.models import CrewActivity from crews.activities.serializers import CrewActivitySerializer from crews.applications.services import is_valid_applicant +from crews.dto import CrewTagDTO +from crews.enums import ProgrammingLanguageChoices from crews.models import Crew from crews.models import CrewMember from crews.models import CrewSubmittableLanguage @@ -19,18 +22,31 @@ # Crew Tag Serializer +class CrewTagTypeField(serializers.SerializerMethodField): + def to_representation(self, value): + return value + + def get_attribute(self, instance: CrewTagDTO) -> str: + assert isinstance(instance, CrewTagDTO) + return instance.type.value + + class CrewTagSerializer(serializers.Serializer): key = serializers.CharField() name = serializers.CharField() - type = serializers.CharField() + type = CrewTagTypeField() # Crew Member Serializer class CrewMemberSerializer(serializers.ModelSerializer): user_id = serializers.IntegerField(source=CrewMember.field_name.USER) - username = serializers.CharField(source=CrewMember.field_name.USER+'__'+User.field_name.USERNAME) - profile_image = serializers.ImageField(source=CrewMember.field_name.USER+'__'+User.field_name.PROFILE_IMAGE) + username = serializers.CharField( + source=CrewMember.field_name.USER+'__'+User.field_name.USERNAME, + ) + profile_image = serializers.ImageField( + source=CrewMember.field_name.USER+'__'+User.field_name.PROFILE_IMAGE, + ) class Meta: model = CrewMember @@ -53,8 +69,8 @@ def to_representation(self, value): def get_attribute(self, instance: Crew): assert isinstance(instance, Crew) - user = self.__class__.current_user(self) - return is_valid_applicant(instance, user, raise_exception=False) + user: User = self.__class__.current_user(self) + return user.is_authenticated and is_valid_applicant(instance, user, raise_exception=False) class IsCaptainField(serializers.SerializerMethodField): @@ -89,13 +105,15 @@ def get_attribute(self, instance: Crew): return instance -class TagsField(CrewTagSerializer): - def __init__(self, **kwargs): - super().__init__(many=True, **kwargs) +class TagsField(serializers.ListSerializer): + child = CrewTagSerializer() - def get_attribute(self, instance: Crew): - assert isinstance(instance, Crew) - return instance.tags() + def to_representation(self, instance: Union[Crew, List[CrewTagDTO]]): + if isinstance(instance, Crew): + return super().to_representation(instance.tags()) + if isinstance(instance, list): + return super().to_representation(instance) + raise ValueError class LatestActivityField(CrewActivitySerializer): @@ -130,7 +148,7 @@ class CrewCreateSerializer(serializers.ModelSerializer): child=serializers.CharField(), ) languages = serializers.MultipleChoiceField( - choices=enums.ProgrammingLanguageChoices.choices, + choices=ProgrammingLanguageChoices.choices, ) class Meta: @@ -186,12 +204,12 @@ class RecruitingCrewSerializer(serializers.ModelSerializer): latest_activity = LatestActivityField() class Meta: - model = models.Crew + model = Crew fields = [ PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.IS_ACTIVE, + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.IS_ACTIVE, 'is_joinable', 'members', 'tags', @@ -205,12 +223,12 @@ class MyCrewSerializer(serializers.ModelSerializer): latest_activity = LatestActivityField() class Meta: - model = models.Crew + model = Crew fields = [ PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.IS_ACTIVE, + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.IS_ACTIVE, 'latest_activity', ] read_only_fields = ['__all__'] @@ -232,12 +250,12 @@ class CrewDashboardSerializer(serializers.ModelSerializer): is_captain = IsCaptainField() class Meta: - model = models.Crew + model = Crew fields = [ PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.NOTICE, + Crew.field_name.ICON, + Crew.field_name.NAME, + Crew.field_name.NOTICE, 'is_captain', 'tags', 'members', From ff4f2a7ac6ce9c406ed79643526c7b42a73e784b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:04:53 +0900 Subject: [PATCH 472/552] =?UTF-8?q?fix(crews.serializers):=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=EB=90=9C=20=ED=99=9C=EB=8F=99=EC=9D=B4=20=EC=97=86?= =?UTF-8?q?=EB=8A=94=20=EA=B2=BD=EC=9A=B0=20'=ED=99=9C=EB=8F=99=20?= =?UTF-8?q?=EC=A2=85=EB=A3=8C'=EB=A5=BC=20=EC=B6=9C=EB=A0=A5=ED=95=98?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/crews/serializers.py b/app/crews/serializers.py index 90b1433..f1084c7 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -120,8 +120,16 @@ class LatestActivityField(CrewActivitySerializer): def get_attribute(self, instance: Crew) -> CrewActivity: assert isinstance(instance, Crew) try: + assert instance.is_active return CrewActivity.objects.crew(instance).has_started().latest() - except: + except CrewActivity.DoesNotExist: + return CrewActivity(**{ + CrewActivity.field_name.CREW: instance, + CrewActivity.field_name.NAME: '등록된 활동 없음', + CrewActivity.field_name.START_AT: None, + CrewActivity.field_name.END_AT: None, + }) + except AssertionError: return CrewActivity(**{ CrewActivity.field_name.CREW: instance, CrewActivity.field_name.NAME: '활동 종료', From 24acae0f2a7222cb545c3f594d5b0df4d9d39f8d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:11:33 +0900 Subject: [PATCH 473/552] =?UTF-8?q?fix(crews.models):=20CrewActivity,=20Cr?= =?UTF-8?q?ewApplication=EC=9D=98=20Manager=EB=93=A4=EC=9D=98=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EC=BF=BC=EB=A6=AC=20=EB=A9=94=EC=86=8C?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/activities/models.py | 21 +++++++-------------- app/crews/admin.py | 4 ++-- app/crews/applications/models.py | 17 ++++++++++++----- app/crews/applications/views.py | 4 ++-- app/crews/serializers.py | 4 ++-- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/app/crews/activities/models.py b/app/crews/activities/models.py index 51e80b1..2bfda50 100644 --- a/app/crews/activities/models.py +++ b/app/crews/activities/models.py @@ -15,27 +15,20 @@ class CrewActivityManager(models.Manager): def filter(self, crew: Crew = None, + has_started: bool = None, + in_progress: bool = None, *args, **kwargs) -> models.QuerySet[CrewActivity]: if crew is not None: assert isinstance(crew, Crew) kwargs[CrewActivity.field_name.CREW] = crew + if has_started is not None: + kwargs[CrewActivity.field_name.START_AT + '__lte'] = timezone.now() + if in_progress is not None: + kwargs[CrewActivity.field_name.START_AT + '__lte'] = timezone.now() + kwargs[CrewActivity.field_name.END_AT + '__gt'] = timezone.now() return super().filter(*args, **kwargs) - def crew(self, crew: Crew) -> _CrewActivityManager: - return self.filter(**{CrewActivity.field_name.CREW: crew}) - - def in_progress(self) -> _CrewActivityManager: - return self.filter(**{ - CrewActivity.field_name.START_AT + '__lte': timezone.now(), - CrewActivity.field_name.END_AT + '__gt': timezone.now(), - }) - - def has_started(self) -> _CrewActivityManager: - return self.filter(**{ - CrewActivity.field_name.START_AT + '__lte': timezone.now(), - }) - class CrewActivity(models.Model): crew = models.ForeignKey( diff --git a/app/crews/admin.py b/app/crews/admin.py index 22bfca6..0d511d5 100644 --- a/app/crews/admin.py +++ b/app/crews/admin.py @@ -41,11 +41,11 @@ def get_members(self, obj: Crew): @admin.display(description='Applicants') def get_applicants(self, obj: Crew): - return CrewApplication.objects.crew(obj).count() + return CrewApplication.objects.filter(crew=obj).count() @admin.display(description='Activities') def get_activities(self, obj: Crew): - return CrewActivity.objects.crew(obj).count() + return CrewActivity.objects.filter(crew=obj).count() @admin.register(CrewMember) diff --git a/app/crews/applications/models.py b/app/crews/applications/models.py index 7ee4588..05ecc04 100644 --- a/app/crews/applications/models.py +++ b/app/crews/applications/models.py @@ -9,11 +9,18 @@ class CrewApplicationManager(models.Manager): - def crew(self, crew: Crew) -> _CrewApplicationManager: - return self.filter(**{CrewApplication.field_name.CREW: crew}) - - def applicant(self, user: User) -> _CrewApplicationManager: - return self.filter(**{CrewApplication.field_name.APPLICANT: user}) + def filter(self, + crew: Crew = None, + applicant: User = None, + *args, + **kwargs) -> models.QuerySet[CrewApplication]: + if crew is not None: + assert isinstance(crew, Crew) + kwargs[CrewApplication.field_name.CREW] = crew + if applicant is not None: + assert isinstance(applicant, Crew) + kwargs[CrewApplication.field_name.APPLICANT] = applicant + return super().filter(*args, **kwargs) class CrewApplication(models.Model): diff --git a/app/crews/applications/views.py b/app/crews/applications/views.py index e5e3ff9..79da1c1 100644 --- a/app/crews/applications/views.py +++ b/app/crews/applications/views.py @@ -21,7 +21,7 @@ class CrewApplicationForCrewListAPIView(generics.ListAPIView): lookup_url_kwarg = 'crew_id' def get_queryset(self): - return CrewApplication.objects.crew(self.get_crew()) + return CrewApplication.objects.filter(crew=self.get_crew()) def get_crew(self) -> Crew: crew_id = self.kwargs[self.lookup_url_kwarg] @@ -36,7 +36,7 @@ class CrewApplicationForUserListAPIView(generics.ListAPIView): serializer_class = serializers.CrewApplicationSerializer def get_queryset(self): - return CrewApplication.objects.applicant(self.request.user) + return CrewApplication.objects.filter(applicant=self.request.user) class CrewApplicantionCreateAPIView(generics.CreateAPIView): diff --git a/app/crews/serializers.py b/app/crews/serializers.py index f1084c7..53d288f 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -121,7 +121,7 @@ def get_attribute(self, instance: Crew) -> CrewActivity: assert isinstance(instance, Crew) try: assert instance.is_active - return CrewActivity.objects.crew(instance).has_started().latest() + return CrewActivity.objects.filter(crew=instance, has_started=True).latest() except CrewActivity.DoesNotExist: return CrewActivity(**{ CrewActivity.field_name.CREW: instance, @@ -144,7 +144,7 @@ def __init__(self, **kwargs): def get_attribute(self, instance: Crew) -> QuerySet[CrewActivity]: assert isinstance(instance, Crew) - return CrewActivity.objects.crew(instance) + return CrewActivity.objects.filter(crew=instance) # Crew Serializers From 3290a31a7d57b6d1f21b883140b2eef017e13ca8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:35:43 +0900 Subject: [PATCH 474/552] =?UTF-8?q?fix(crews.activities.serializers):=20?= =?UTF-8?q?=EB=82=A0=EC=A7=9C=20=EA=B4=80=EB=A0=A8=20=ED=95=84=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=EC=99=80=20=EC=B6=9C=EB=A0=A5=EC=9D=B4=20=EC=9D=BC?= =?UTF-8?q?=EC=B9=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/activities/serializers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/crews/activities/serializers.py b/app/crews/activities/serializers.py index d53a28b..f7c2981 100644 --- a/app/crews/activities/serializers.py +++ b/app/crews/activities/serializers.py @@ -7,12 +7,15 @@ class CrewActivitySerializer(serializers.ModelSerializer): + date_start_at = serializers.DateField(source=CrewActivity.field_name.START_AT) + date_end_at = serializers.DateField(source=CrewActivity.field_name.END_AT) + class Meta: model = CrewActivity fields = [ CrewActivity.field_name.NAME, - CrewActivity.field_name.START_AT, - CrewActivity.field_name.END_AT, + 'date_start_at', + 'date_end_at', ] read_only_fields = ['__all__'] From ac5912f7e19f7f35742052729e0df94aac577a74 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:40:35 +0900 Subject: [PATCH 475/552] =?UTF-8?q?test(crews):=20=ED=81=AC=EB=A3=A8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/tests.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/crews/tests.py b/app/crews/tests.py index 9491060..f740022 100644 --- a/app/crews/tests.py +++ b/app/crews/tests.py @@ -100,3 +100,19 @@ def test_my_크루_목록_조회(self): } }, ]) + + def test_크루_생성(self): + res = self.client.post("/api/v1/crew", { + "icon": "🥇", + "name": "임시로 생성해본 크루", + "max_members": 3, + "languages": [ + "java", + ], + "min_boj_level": 0, + "custom_tags": ['tag1'], + "notice": "string", + "is_recruiting": True, + "is_active": True + }) + self.assertEqual(res.status_code, status.HTTP_201_CREATED, res.json()) From 9c590305468a3903de3045b6b2982c420d664fcf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:49:41 +0900 Subject: [PATCH 476/552] =?UTF-8?q?chore(crews.models):=20code=20style=20f?= =?UTF-8?q?ormatter=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/app/crews/models.py b/app/crews/models.py index dc2bbd3..3df1660 100644 --- a/app/crews/models.py +++ b/app/crews/models.py @@ -35,7 +35,9 @@ def filter(self, queryset = queryset.filter(pk__in=self._ids_as_member(as_member)) if not_as_member is not None and not isinstance(not_as_member, AnonymousUser): assert isinstance(not_as_member, User) - queryset = queryset.exclude(pk__in=self._ids_as_member(not_as_member)) + queryset = queryset.exclude( + pk__in=self._ids_as_member(not_as_member), + ) return queryset def _ids_as_captain(self, user: User) -> List[int]: @@ -147,11 +149,19 @@ def tags(self) -> List[CrewTagDTO]: tag_name = f"{min_level.get_division_name(lang='ko')} 이상" else: tag_name = f"{min_level.get_name(lang='ko', arabic=False)} 이상" - tag_dto = CrewTagDTO(key=None, name=tag_name, type=CrewTagType.LEVEL) + tag_dto = CrewTagDTO( + key=None, + name=tag_name, + type=CrewTagType.LEVEL, + ) tags.append(tag_dto) # 커스텀 태그 for tag_name in self.custom_tags: - tag_dto = CrewTagDTO(key=None, name=tag_name, type=CrewTagType.CUSTOM) + tag_dto = CrewTagDTO( + key=None, + name=tag_name, + type=CrewTagType.CUSTOM, + ) tags.append(tag_dto) return tags From 61ce3b1e0bebd273ad4246e9f546f9bf721a16de Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:50:22 +0900 Subject: [PATCH 477/552] =?UTF-8?q?fix(crews.models):=20CrewSubmittableLan?= =?UTF-8?q?guage=EC=9D=98=20Manager=EB=93=A4=EC=9D=98=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EC=BF=BC=EB=A6=AC=20=EB=A9=94=EC=86=8C=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/models.py | 13 +++++++++---- app/crews/serializers.py | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/crews/models.py b/app/crews/models.py index 3df1660..d0945e2 100644 --- a/app/crews/models.py +++ b/app/crews/models.py @@ -133,7 +133,7 @@ def display_name(self) -> str: def tags(self) -> List[CrewTagDTO]: tags = [] # 사용 가능 언어 - for language_value in CrewSubmittableLanguage.objects.crew(self).values_list(CrewSubmittableLanguage.field_name.LANGUAGE, flat=True): + for language_value in CrewSubmittableLanguage.objects.filter(crew=self).values_list(CrewSubmittableLanguage.field_name.LANGUAGE, flat=True): language = ProgrammingLanguageChoices(language_value) tag_dto = CrewTagDTO( key=language.value, @@ -234,8 +234,13 @@ def __str__(self) -> str: class CrewSubmittableLanguageManager(models.Manager): - def crew(self, crew: Crew) -> _CrewSubmittableLanguageManager: - return self.filter(**{CrewSubmittableLanguage.field_name.CREW: crew}) + def filter(self, + crew: Crew = None, + *args, + **kwargs) -> models.QuerySet[CrewSubmittableLanguage]: + if crew is not None: + kwargs[CrewSubmittableLanguage.field_name.CREW] = crew + return super().filter(*args, **kwargs) def bulk_create_from_languages(self, crew: Crew, languages: List[Union[str, ProgrammingLanguageChoices]]) -> List[CrewSubmittableLanguage]: assert isinstance(crew, Crew) @@ -248,7 +253,7 @@ def bulk_create_from_languages(self, crew: Crew, languages: List[Union[str, Prog else: raise ValueError(f'{language}은 선택 가능한 언어가 아닙니다.') entity = CrewSubmittableLanguage(**{ - CrewSubmittableLanguage.field_name.CREW: self.instance, + CrewSubmittableLanguage.field_name.CREW: crew, CrewSubmittableLanguage.field_name.LANGUAGE: language, }) entities.append(entity) diff --git a/app/crews/serializers.py b/app/crews/serializers.py index 53d288f..23774b9 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -196,7 +196,7 @@ def save(self, **kwargs): languages = self.validated_data.pop('languages') with atomic(): crew = super().save(**kwargs) - CrewSubmittableLanguage.objects.crew(crew).delete() + CrewSubmittableLanguage.objects.filter(crew=crew).delete() CrewSubmittableLanguage.objects.bulk_create_from_languages( crew=crew, languages=languages, From b7f20b1adf4f131f503fcc8a831334759a4ee994 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 09:50:51 +0900 Subject: [PATCH 478/552] =?UTF-8?q?fix(crews.serializers):=20=ED=81=AC?= =?UTF-8?q?=EB=A3=A8=20=EC=83=9D=EC=84=B1=EC=8B=9C=20created=5Fby=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EA=B0=80=20=EC=9E=90=EB=8F=99=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/serializers.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/app/crews/serializers.py b/app/crews/serializers.py index 23774b9..18d1b1e 100644 --- a/app/crews/serializers.py +++ b/app/crews/serializers.py @@ -150,13 +150,15 @@ def get_attribute(self, instance: Crew) -> QuerySet[CrewActivity]: # Crew Serializers class CrewCreateSerializer(serializers.ModelSerializer): - created_by = UserMinimalSerializer() + created_by = UserMinimalSerializer(read_only=True) custom_tags = serializers.ListField( default=list, child=serializers.CharField(), ) languages = serializers.MultipleChoiceField( choices=ProgrammingLanguageChoices.choices, + default=list, + write_only=True, ) class Meta: @@ -176,32 +178,24 @@ class Meta: Crew.field_name.CUSTOM_TAGS, ] extra_kwargs = { - PK: { - 'read_only': True, - }, - 'languages': { - 'write_only': True, - 'default': list, - }, - Crew.field_name.CREATED_AT: { - 'read_only': True, - }, - Crew.field_name.CREATED_BY: { - 'read_only': True, - 'default': serializers.CurrentUserDefault(), - }, + PK: {'read_only': True}, + Crew.field_name.CREATED_AT: {'read_only': True}, } def save(self, **kwargs): languages = self.validated_data.pop('languages') with atomic(): - crew = super().save(**kwargs) - CrewSubmittableLanguage.objects.filter(crew=crew).delete() + self.instance = Crew.objects.create(**{ + Crew.field_name.CREATED_BY: serializers.CurrentUserDefault()(self), + **self.validated_data, + **kwargs, + }) + CrewSubmittableLanguage.objects.filter(crew=self.instance).delete() CrewSubmittableLanguage.objects.bulk_create_from_languages( - crew=crew, + crew=self.instance, languages=languages, ) - return crew + return self.instance class RecruitingCrewSerializer(serializers.ModelSerializer): From 698673ea7b6bf0e982ddd72e6e215a393d395683 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 13:36:38 +0900 Subject: [PATCH 479/552] refactor(users.models): rename models/__init__.py -> models.py --- app/users/models.py | 144 ++++++++++++++++++++ app/users/models/__init__.py | 4 - app/users/models/user.py | 77 ----------- app/users/models/user_email_verification.py | 38 ------ app/users/models/user_manager.py | 30 ---- 5 files changed, 144 insertions(+), 149 deletions(-) create mode 100644 app/users/models.py delete mode 100644 app/users/models/__init__.py delete mode 100644 app/users/models/user.py delete mode 100644 app/users/models/user_email_verification.py delete mode 100644 app/users/models/user_manager.py diff --git a/app/users/models.py b/app/users/models.py new file mode 100644 index 0000000..09f605a --- /dev/null +++ b/app/users/models.py @@ -0,0 +1,144 @@ +from __future__ import annotations + +from datetime import timedelta + +from django.contrib.auth.models import AbstractBaseUser +from django.contrib.auth.models import BaseUserManager +from django.contrib.auth.models import PermissionsMixin +from django.db import models +from django.utils import timezone + + +def get_profile_image_path(user: User, filename: str) -> str: + return f'user/profile/{user.pk}/{filename}' + + +def default_expires_at_factory(): + return timezone.now() + timedelta(minutes=5) + + +class UserManager(BaseUserManager): + def create(self, email: str, username: str, password: str, **kwargs) -> User: + if not email: + raise ValueError('The Email field must be set') + if not username: + raise ValueError('The Username field must be set') + if not password: + raise ValueError('The Password field must be set') + email = self.normalize_email(email) + user = User(email=email, username=username, **kwargs) + user.set_password(password) + user.save() + return user + + def get_by_username(self, username: str) -> User: + return self.get(**{User.field_name.BOJ_USERNAME: username}) + + def create_user(self, email: str, username: str, password: str, **extra_fields): + user = self.model( + email=email, + username=username, + **extra_fields + ) + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, username, password=None, **extra_fields): + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + return self.create(email, username, password, **extra_fields) + + +class User(AbstractBaseUser, PermissionsMixin): + username = models.CharField( + verbose_name='username', + max_length=30, + unique=True, + ) + email = models.EmailField( + verbose_name='email address', + max_length=255, + unique=True, + ) + profile_image = models.ImageField( + help_text='프로필 이미지', + upload_to=get_profile_image_path, + null=True, + blank=True, + validators=[ + # TODO: 이미지 크기 제한 + # TODO: 이미지 확장자 제한 + ] + ) + boj_username = models.CharField( + help_text='백준 아이디', + max_length=40, + ) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + first_name = models.TextField(blank=True, null=True, default=None) + last_name = models.TextField(blank=True, null=True, default=None) + created_at = models.DateTimeField(default=timezone.now) + + objects: UserManager = UserManager() + + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['username'] + + class field_name: + PROFILE_IMAGE = 'profile_image' + BOJ_USERNAME = 'boj_username' + USERNAME = 'username' + EMAIL = 'email' + PASSWORD = 'password' + IS_ACTIVE = 'is_active' + IS_STAFF = 'is_staff' + IS_SUPERUSER = 'is_superuser' + FIRST_NAME = 'first_name' + LAST_NAME = 'last_name' + CREATED_AT = 'created_at' + LAST_LOGIN = 'last_login' + + @property + def date_joined(self): + return self.created_at + + def __str__(self) -> str: + return f'[{self.pk} : "{self.username}"]' + (' (관리자)' if self.is_staff else '') + + def has_perm(self, perm, obj=None): + return True + + def has_module_perms(self, app_label): + return True + + +class UserEmailVerification(models.Model): + email = models.EmailField( + help_text='이메일 주소', + primary_key=True, + ) + verification_code = models.TextField( + help_text='인증 코드', + null=False, + blank=False, + ) + verification_token = models.TextField( + help_text='인증 토큰', + null=True, + blank=True, + ) + expires_at = models.DateTimeField(default=default_expires_at_factory) + created_at = models.DateTimeField(auto_now_add=True) + + class field_name: + EMAIL = 'email' + VERIFICATION_CODE = 'verification_code' + VERIFICATION_TOKEN = 'verification_token' + EXPIRES_AT = 'expires_at' + CREATED_AT = 'created_at' + + def is_expired(self) -> bool: + return self.expires_at < timezone.now() diff --git a/app/users/models/__init__.py b/app/users/models/__init__.py deleted file mode 100644 index ea19936..0000000 --- a/app/users/models/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from boj.enums import BOJLevel as UserBojLevelChoices -from users.models.user import User -from users.models.user_email_verification import UserEmailVerification -from users.models.user_manager import UserManager diff --git a/app/users/models/user.py b/app/users/models/user.py deleted file mode 100644 index 774b83f..0000000 --- a/app/users/models/user.py +++ /dev/null @@ -1,77 +0,0 @@ -from __future__ import annotations - -from django.contrib.auth.models import AbstractBaseUser -from django.contrib.auth.models import PermissionsMixin -from django.db import models -from django.utils import timezone - -from users.models.user_manager import UserManager - - -def get_profile_image_path(user: User, filename: str) -> str: - return f'user/profile/{user.pk}/{filename}' - - -class User(AbstractBaseUser, PermissionsMixin): - username = models.CharField( - verbose_name='username', - max_length=30, - unique=True, - ) - email = models.EmailField( - verbose_name='email address', - max_length=255, - unique=True, - ) - profile_image = models.ImageField( - help_text='프로필 이미지', - upload_to=get_profile_image_path, - null=True, - blank=True, - validators=[ - # TODO: 이미지 크기 제한 - # TODO: 이미지 확장자 제한 - ] - ) - boj_username = models.CharField( - help_text='백준 아이디', - max_length=40, - ) - is_active = models.BooleanField(default=True) - is_staff = models.BooleanField(default=False) - is_superuser = models.BooleanField(default=False) - first_name = models.TextField(blank=True, null=True, default=None) - last_name = models.TextField(blank=True, null=True, default=None) - created_at = models.DateTimeField(default=timezone.now) - - objects = UserManager() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['username'] - - class field_name: - PROFILE_IMAGE = 'profile_image' - BOJ_USERNAME = 'boj_username' - USERNAME = 'username' - EMAIL = 'email' - PASSWORD = 'password' - IS_ACTIVE = 'is_active' - IS_STAFF = 'is_staff' - IS_SUPERUSER = 'is_superuser' - FIRST_NAME = 'first_name' - LAST_NAME = 'last_name' - CREATED_AT = 'created_at' - LAST_LOGIN = 'last_login' - - @property - def date_joined(self): - return self.created_at - - def __str__(self) -> str: - return f'[{self.pk} : "{self.username}"]' + (' (관리자)' if self.is_staff else '') - - def has_perm(self, perm, obj=None): - return True - - def has_module_perms(self, app_label): - return True diff --git a/app/users/models/user_email_verification.py b/app/users/models/user_email_verification.py deleted file mode 100644 index ce795b7..0000000 --- a/app/users/models/user_email_verification.py +++ /dev/null @@ -1,38 +0,0 @@ -from __future__ import annotations -from datetime import timedelta - -from django.db import models -from django.utils import timezone - - -def default_expires_at_factory(): - return timezone.now() + timedelta(minutes=5) - - -class UserEmailVerification(models.Model): - email = models.EmailField( - help_text='이메일 주소', - primary_key=True, - ) - verification_code = models.TextField( - help_text='인증 코드', - null=False, - blank=False, - ) - verification_token = models.TextField( - help_text='인증 토큰', - null=True, - blank=True, - ) - expires_at = models.DateTimeField(default=default_expires_at_factory) - created_at = models.DateTimeField(auto_now_add=True) - - class field_name: - EMAIL = 'email' - VERIFICATION_CODE = 'verification_code' - VERIFICATION_TOKEN = 'verification_token' - EXPIRES_AT = 'expires_at' - CREATED_AT = 'created_at' - - def is_expired(self) -> bool: - return self.expires_at < timezone.now() diff --git a/app/users/models/user_manager.py b/app/users/models/user_manager.py deleted file mode 100644 index a490bb6..0000000 --- a/app/users/models/user_manager.py +++ /dev/null @@ -1,30 +0,0 @@ -import typing - -from django.contrib.auth.models import AbstractUser, BaseUserManager - - -class UserManager(BaseUserManager): - model: typing.Callable[..., AbstractUser] - - def create(self, **kwargs): - return self.create_user(**kwargs) - - def create_user(self, email, username, password=None, **extra_fields): - if not email: - raise ValueError('The Email field must be set') - if not username: - raise ValueError('The Username field must be set') - email = self.normalize_email(email) - user = self.model( - email=email, - username=username, - **extra_fields - ) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, username, password=None, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - return self.create_user(email, username, password, **extra_fields) From 2f2a4bde36ffb65db09c127cab34afacc3366d54 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 14:23:15 +0900 Subject: [PATCH 480/552] =?UTF-8?q?refactor(users.models):=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=9D=B8=EC=A6=9D=20JWT=EB=A5=BC=20?= =?UTF-8?q?=EB=A7=8C=EB=93=A4=EA=B3=A0=20=EA=B4=80=EB=A6=AC=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20User=20=EB=AA=A8?= =?UTF-8?q?=EB=8D=B8=EC=97=90=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/app/users/models.py b/app/users/models.py index 09f605a..a6c651a 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -7,16 +7,13 @@ from django.contrib.auth.models import PermissionsMixin from django.db import models from django.utils import timezone +from rest_framework_simplejwt.tokens import RefreshToken def get_profile_image_path(user: User, filename: str) -> str: return f'user/profile/{user.pk}/{filename}' -def default_expires_at_factory(): - return timezone.now() + timedelta(minutes=5) - - class UserManager(BaseUserManager): def create(self, email: str, username: str, password: str, **kwargs) -> User: if not email: @@ -31,23 +28,16 @@ def create(self, email: str, username: str, password: str, **kwargs) -> User: user.save() return user - def get_by_username(self, username: str) -> User: - return self.get(**{User.field_name.BOJ_USERNAME: username}) - - def create_user(self, email: str, username: str, password: str, **extra_fields): - user = self.model( - email=email, - username=username, - **extra_fields - ) - user.set_password(password) - user.save(using=self._db) - return user - - def create_superuser(self, email, username, password=None, **extra_fields): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - return self.create(email, username, password, **extra_fields) + def filter(self, + email: Optional[str] = None, + username: Optional[str] = None, + *args, + **kwargs) -> models.QuerySet[User]: + if email is None: + kwargs[User.field_name.EMAIL] = email + if username is None: + kwargs[User.field_name.USERNAME] = username + return super().filter(*args, **kwargs) class User(AbstractBaseUser, PermissionsMixin): @@ -75,6 +65,8 @@ class User(AbstractBaseUser, PermissionsMixin): help_text='백준 아이디', max_length=40, ) + token = models.CharField(null=True, blank=True, default=None) + refresh_token = models.CharField(null=True, blank=True, default=None) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) @@ -89,10 +81,12 @@ class User(AbstractBaseUser, PermissionsMixin): class field_name: PROFILE_IMAGE = 'profile_image' - BOJ_USERNAME = 'boj_username' USERNAME = 'username' EMAIL = 'email' PASSWORD = 'password' + BOJ_USERNAME = 'boj_username' + TOKEN = 'token' + REFRESH_TOKEN = 'refresh_token' IS_ACTIVE = 'is_active' IS_STAFF = 'is_staff' IS_SUPERUSER = 'is_superuser' @@ -114,6 +108,12 @@ def has_perm(self, perm, obj=None): def has_module_perms(self, app_label): return True + def rotate_token(self): + token: RefreshToken = RefreshToken.for_user(self) + self.token = str(token.access_token) + self.refresh_token = str(token.token) + self.save() + class UserEmailVerification(models.Model): email = models.EmailField( From b74deb519d7ebbf0bf9267af61d648d23ff3ca42 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 14:24:03 +0900 Subject: [PATCH 481/552] =?UTF-8?q?refactor(users.models):=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=20code=EC=99=80=20token?= =?UTF-8?q?=EC=9D=84=20=EB=A7=8C=EB=93=A4=EA=B3=A0=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EC=B1=85=EC=9E=84=EC=9D=84=20UserEmailVer?= =?UTF-8?q?ification=20=EB=AA=A8=EB=8D=B8=EC=97=90=20=EC=9C=84=EC=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/app/users/models.py b/app/users/models.py index a6c651a..f664cc2 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -1,6 +1,9 @@ from __future__ import annotations from datetime import timedelta +from hashlib import sha256 +from random import randint +from typing import Optional from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import BaseUserManager @@ -130,7 +133,7 @@ class UserEmailVerification(models.Model): null=True, blank=True, ) - expires_at = models.DateTimeField(default=default_expires_at_factory) + expires_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True) class field_name: @@ -142,3 +145,15 @@ class field_name: def is_expired(self) -> bool: return self.expires_at < timezone.now() + + def rotate_code(self, code_len: int = 6): + self.verification_code = self._create_code(code_len) + self.expires_at = timezone.now() + timedelta(minutes=5) + + def rotate_token(self, seed: Optional[str] = None): + if seed is None: + seed = self._create_code(code_len=16) + return sha256(seed.encode()).hexdigest() + + def _create_code(self, code_len: int) -> str: + return ''.join(chr(randint(ord('A'), ord('Z'))) for _ in range(code_len)) From bcd920c574f437989ae4295570e10c9becbe59a3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 14:25:18 +0900 Subject: [PATCH 482/552] =?UTF-8?q?refactor(users.views):=20permissions.py?= =?UTF-8?q?=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/permissions.py | 2 -- app/users/views.py | 17 +++++++++-------- 2 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 app/users/permissions.py diff --git a/app/users/permissions.py b/app/users/permissions.py deleted file mode 100644 index b98d65d..0000000 --- a/app/users/permissions.py +++ /dev/null @@ -1,2 +0,0 @@ -from rest_framework.permissions import AllowAny -from rest_framework.permissions import IsAuthenticated diff --git a/app/users/views.py b/app/users/views.py index 2c06660..9b5ac00 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -2,11 +2,12 @@ from rest_framework import generics from rest_framework import status from rest_framework import throttling +from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from users import models -from users import permissions from users import serializers from users import services @@ -14,7 +15,7 @@ class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] - permission_classes = [permissions.AllowAny] + permission_classes = [AllowAny] serializer_class = serializers.SignInSerializer def post(self, request: Request, *args, **kwargs): @@ -37,7 +38,7 @@ def post(self, request: Request, *args, **kwargs): class SignUpAPIView(generics.CreateAPIView): """사용자 등록(회원가입) API""" authentication_classes = [] - permission_classes = [permissions.AllowAny] + permission_classes = [AllowAny] serializer_class = serializers.SignUpSerializer def create(self, request: Request, *args, **kwargs): @@ -57,7 +58,7 @@ def perform_create(self, serializer: serializers.SignUpSerializer): class SignOutAPIView(generics.GenericAPIView): """사용자 로그아웃 API""" - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated] @swagger_auto_schema(responses={ status.HTTP_204_NO_CONTENT: '로그아웃 성공', @@ -70,7 +71,7 @@ def get(self, request, *args, **kwargs): class UsernameCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" authentication_classes = [] - permission_classes = [permissions.AllowAny] + permission_classes = [AllowAny] serializer_class = serializers.UsernameSerializer @swagger_auto_schema( @@ -97,7 +98,7 @@ class EmailCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" authentication_classes = [] - permission_classes = [permissions.AllowAny] + permission_classes = [AllowAny] serializer_class = serializers.EmailSerializer @swagger_auto_schema( @@ -128,7 +129,7 @@ class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" authentication_classes = [] throttle_classes = [] - permission_classes = [permissions.AllowAny] + permission_classes = [AllowAny] def get_serializer_class(self): if self.request.method == 'GET': @@ -174,7 +175,7 @@ def post(self, request: Request, *args, **kwargs): class CurrentUserRetrieveUpdateAPIView(generics.RetrieveUpdateAPIView): """현재 로그인한 사용자 정보를 조회/수정하는 API""" - permission_classes = [permissions.IsAuthenticated] + permission_classes = [IsAuthenticated] serializer_class = serializers.UserUpdateSerializer def get_object(self) -> models.User: From 0fe2fb416f911575867175e0cdcd75dad6fedc87 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 14:38:21 +0900 Subject: [PATCH 483/552] =?UTF-8?q?fix(users.models):=20CharField=EC=9D=98?= =?UTF-8?q?=20max=5Flen=EC=9D=84=20=EC=84=A4=EC=A0=95=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20TextField=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/users/models.py b/app/users/models.py index f664cc2..ec7059d 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -68,8 +68,8 @@ class User(AbstractBaseUser, PermissionsMixin): help_text='백준 아이디', max_length=40, ) - token = models.CharField(null=True, blank=True, default=None) - refresh_token = models.CharField(null=True, blank=True, default=None) + token = models.TextField(null=True, blank=True, default=None) + refresh_token = models.TextField(null=True, blank=True, default=None) is_active = models.BooleanField(default=True) is_staff = models.BooleanField(default=False) is_superuser = models.BooleanField(default=False) From 6e85c24beab783f9f392e30bd03ad5d2b2151e3b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 14:45:11 +0900 Subject: [PATCH 484/552] =?UTF-8?q?test(users):=20=EC=9D=91=EB=8B=B5?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EA=B2=80=EC=A6=9D=20=EC=8B=A4=ED=8C=A8?= =?UTF-8?q?=EC=8B=9C=20=EC=82=AC=EC=9C=A0=EB=A5=BC=20=EC=B6=9C=EB=A0=A5?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index 3cac95e..dc934a0 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -18,7 +18,7 @@ def test_로그인성공(self): "password": "passw0rd@test", }, ) - self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) def test_비밀번호_불일치(self): res = self.client.post( @@ -28,7 +28,7 @@ def test_비밀번호_불일치(self): "password": "password@test", } ) - self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN, res.json()) class SignUpTest(TestCase): @@ -51,7 +51,7 @@ def test_회원가입_성공(self): "verification_token": self.sample_object.verification_token, } ) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) + self.assertEqual(res.status_code, status.HTTP_201_CREATED, res.json()) def test_인증토큰_불일치(self): res = self.client.post( @@ -64,7 +64,7 @@ def test_인증토큰_불일치(self): "verification_token": 'this_token_must_not_match_the_sample...', } ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.json()) class SignOutTest(TestCase): @@ -80,7 +80,7 @@ def setUp(self) -> None: def test_로그아웃_성공(self): res = self.client.get("/api/v1/auth/signout") - self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) + self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT, res.json()) class UsernameCheckTest(TestCase): @@ -93,7 +93,7 @@ def test_사용_가능한_사용자명(self): "username": "unique", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) self.assertDictEqual(res.json(), { "username": "unique", "is_usable": True, @@ -106,7 +106,7 @@ def test_사용_불가능한_사용자명(self): "username": "test", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) self.assertDictEqual(res.json(), { "username": "test", "is_usable": False, @@ -123,7 +123,7 @@ def test_사용_가능한_이메일(self): "email": "unique@notexample.com", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) self.assertDictEqual(res.json(), { "email": "unique@notexample.com", "is_usable": True, @@ -136,7 +136,7 @@ def test_사용_불가능한_이메일(self): "email": "test@example.com", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) self.assertDictEqual(res.json(), { "email": "test@example.com", "is_usable": False, From d36e568b0eecd09dd563f3cef5c1226c4682f4e8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 15:12:47 +0900 Subject: [PATCH 485/552] =?UTF-8?q?refactor(users.serializers):=20IsEmailU?= =?UTF-8?q?sableSerializer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 19 +++++++++++------- app/users/views.py | 42 ++++++++++++++-------------------------- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 1f14051..0c2ab2c 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -8,17 +8,22 @@ PK = 'id' -class BOJField(serializers.SerializerMethodField): - def to_representation(self, value: BOJUser): - return BOJUserSerializer(value).data +# Username or Email Serializers - def get_attribute(self, instance) -> BOJUser: - assert isinstance(instance, User) - return BOJUser.objects.get_by_user(instance) +class EmailSerializer(serializers.Serializer): + email = serializers.EmailField() -class EmailSerializer(serializers.Serializer): +class IsEmailUsableField(serializers.BooleanField): + def get_attribute(self, instance): + assert 'email' in instance, instance + assert isinstance(instance['email'], str) + return not User.objects.filter(email=instance['email']).exists() + + +class IsEmailUsableSerializer(serializers.Serializer): email = serializers.EmailField() + is_usable = IsEmailUsableField(read_only=True) class EmailCodeSerializer(serializers.Serializer): diff --git a/app/users/views.py b/app/users/views.py index 9b5ac00..106b8f0 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -8,10 +8,25 @@ from rest_framework.response import Response from users import models +from users.serializers import IsEmailUsableSerializer from users import serializers from users import services +class EmailCheckAPIView(generics.GenericAPIView): + """이메일이 사용가능한지 검사 API""" + + authentication_classes = [] + permission_classes = [AllowAny] + serializer_class = IsEmailUsableSerializer + + @swagger_auto_schema(query_serializer=IsEmailUsableSerializer) + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + return Response(serializer.data) + + class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] @@ -94,33 +109,6 @@ def get(self, request: Request, *args, **kwargs): ) -class EmailCheckAPIView(generics.GenericAPIView): - """이메일이 사용가능한지 검사 API""" - - authentication_classes = [] - permission_classes = [AllowAny] - serializer_class = serializers.EmailSerializer - - @swagger_auto_schema( - query_serializer=serializers.EmailSerializer, - responses={ - status.HTTP_200_OK: '검사를 수행했을 경우, 사용가능 여부를 Boolean으로 반환함.', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 형식.', - }, - ) - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - email = serializer.validated_data['email'] - return Response( - data={ - "email": email, - "is_usable": services.is_email_usable(email), - }, - status=status.HTTP_200_OK, - ) - - class EmailVerifyThrottle(throttling.AnonRateThrottle): THROTTLE_RATES = '1/min' From d90b64d8c5d08c769df82e0b40b0f1cba0441552 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 15:14:06 +0900 Subject: [PATCH 486/552] =?UTF-8?q?refactor(users.serializers):=20IsUserna?= =?UTF-8?q?meUsableSerializer=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 12 ++++++++++++ app/users/views.py | 40 ++++++++++++++-------------------------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 0c2ab2c..214e7db 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -26,6 +26,18 @@ class IsEmailUsableSerializer(serializers.Serializer): is_usable = IsEmailUsableField(read_only=True) +class IsUsernameUsableField(serializers.BooleanField): + def get_attribute(self, instance): + assert 'username' in instance, instance + assert isinstance(instance['username'], str) + return not User.objects.filter(username=instance['username']).exists() + + +class IsUsernameUsableSerializer(serializers.Serializer): + username = serializers.CharField() + is_usable = IsUsernameUsableField() + + class EmailCodeSerializer(serializers.Serializer): email = serializers.EmailField() code = serializers.CharField() diff --git a/app/users/views.py b/app/users/views.py index 106b8f0..7b25ebc 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -9,6 +9,7 @@ from users import models from users.serializers import IsEmailUsableSerializer +from users.serializers import IsUsernameUsableSerializer from users import serializers from users import services @@ -27,6 +28,19 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) +class UsernameCheckAPIView(generics.GenericAPIView): + """이메일이 사용가능한지 검사 API""" + authentication_classes = [] + permission_classes = [AllowAny] + serializer_class = IsUsernameUsableSerializer + + @swagger_auto_schema(query_serializer=IsUsernameUsableSerializer) + def get(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(data=request.query_params) + serializer.is_valid(raise_exception=True) + return Response(serializer.data) + + class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] @@ -83,32 +97,6 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class UsernameCheckAPIView(generics.GenericAPIView): - """이메일이 사용가능한지 검사 API""" - authentication_classes = [] - permission_classes = [AllowAny] - serializer_class = serializers.UsernameSerializer - - @swagger_auto_schema( - query_serializer=serializers.UsernameSerializer, - responses={ - status.HTTP_200_OK: '사용자명이 사용가능한지 검사에 성공.', - status.HTTP_400_BAD_REQUEST: '잘못된 데이터 형식을 입력했을 경우.', - }, - ) - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - username = serializer.validated_data['username'] - return Response( - data={ - "username": username, - "is_usable": services.is_username_usable(username), - }, - status=status.HTTP_200_OK, - ) - - class EmailVerifyThrottle(throttling.AnonRateThrottle): THROTTLE_RATES = '1/min' From 2f442c8a0e8a41f9ed6b153c4e84048f4657793b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:06:48 +0900 Subject: [PATCH 487/552] =?UTF-8?q?fix(boj.models):=20BOJUserManager?= =?UTF-8?q?=EC=9D=98=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 11 +++++------ app/crews/applications/services.py | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index 2015d8a..5f9e357 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -10,14 +10,13 @@ class BOJUserManager(Manager): - def username(self, username: str) -> _BOJUserManager: - return self.filter(**{BOJUser.field_name.USERNAME: username}) - - def get_by_user(self, user: User) -> BOJUser: - return self.username(user.boj_username).get_or_create()[0] + def filter(self, username: Optional[str] = None, *args, **kwargs) -> models.QuerySet[BOJUser]: + if username is not None: + kwargs[User.field_name.USERNAME] = username + return super().filter(*args, **kwargs) def get_by_username(self, username: str) -> BOJUser: - return self.username(username).get_or_create()[0] + return self.filter(username=username).get_or_create()[0] class BOJUser(models.Model): diff --git a/app/crews/applications/services.py b/app/crews/applications/services.py index 46b12e5..ac81a5d 100644 --- a/app/crews/applications/services.py +++ b/app/crews/applications/services.py @@ -27,7 +27,7 @@ def apply(crew: Crew, applicant: User, message: str) -> CrewApplication: def is_valid_applicant(crew: Crew, applicant: User, raise_exception=False) -> bool: try: - boj_user = BOJUser.objects.get_by_user(applicant) + boj_user = BOJUser.objects.get_by_username(applicant.boj_username) assert crew.is_recruiting, ( "'크루가 현재 크루원을 모집하고 있지 않습니다." ) From 495c6bee063f20aba01458318fec84836315d8cc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:13:34 +0900 Subject: [PATCH 488/552] =?UTF-8?q?refactor(boj.models):=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=ED=83=80=EC=9E=85=20=ED=9E=8C?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index 5f9e357..6e15d1c 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Union +from typing import Optional from django.db import models from django.db.models import Manager @@ -34,7 +34,7 @@ class BOJUser(models.Model): ) updated_at = models.DateTimeField(auto_now_add=True) - objects: _BOJUserManager = BOJUserManager() + objects: BOJUserManager = BOJUserManager() class field_name: USERNAME = 'username' @@ -65,7 +65,7 @@ class BOJUserSnapshot(models.Model): rating = models.IntegerField() created_at = models.DateTimeField() - objects: _BOJUserSnapshotManager = BOJUserSnapshotManager() + objects: BOJUserSnapshotManager = BOJUserSnapshotManager() class field_name: USER = 'user' @@ -83,8 +83,3 @@ class BOJProblem(models.Model): time_limit = models.FloatField() tags = models.JSONField(default=list) level = models.IntegerField(choices=BOJLevel.choices) - - -_BOJUserManager = Union[BOJUserManager, Manager[BOJUser]] -_BOJUserSnapshotManager = Union[BOJUserSnapshotManager, - Manager[BOJUserSnapshot]] From 820d845961554aa1d900773ddc788a2bff349ecf Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:14:14 +0900 Subject: [PATCH 489/552] =?UTF-8?q?fix(users.models):=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?(=EB=B0=98=EB=8C=80=EB=A1=9C)=EB=90=98=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EB=8D=98=20=EC=A1=B0=EA=B1=B4=EB=AC=B8=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/users/models.py b/app/users/models.py index ec7059d..a918b63 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -36,9 +36,9 @@ def filter(self, username: Optional[str] = None, *args, **kwargs) -> models.QuerySet[User]: - if email is None: + if email is not None: kwargs[User.field_name.EMAIL] = email - if username is None: + if username is not None: kwargs[User.field_name.USERNAME] = username return super().filter(*args, **kwargs) From ccc21ee1d77944f5b26322a63955f42f1c8800a0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:18:01 +0900 Subject: [PATCH 490/552] =?UTF-8?q?fix(users.backends):=20=EB=B0=94?= =?UTF-8?q?=EB=80=90=20=EB=AA=A8=EB=8D=B8=20=EB=A7=A4=EB=8B=88=EC=A0=80?= =?UTF-8?q?=EC=97=90=20=EB=A7=A1=EB=8F=84=EB=A1=9D=20=EB=B0=B1=EC=97=94?= =?UTF-8?q?=EB=93=9C=20=EB=AA=A8=EB=8D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/backends.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/users/backends.py b/app/users/backends.py index 8c1c90e..3f8daaa 100644 --- a/app/users/backends.py +++ b/app/users/backends.py @@ -1,22 +1,16 @@ -import logging - from django.contrib.auth.backends import ModelBackend from django.http import HttpRequest from users.models import User -logger = logging.getLogger(__name__) - - class UserAuthBackend(ModelBackend): def authenticate(self, request: HttpRequest, username=None, password=None, **kwargs): - """username 필드지만 email로 인증하도록 오버라이드 되어있음.""" try: - user = User.objects.get(email=username) + user = User.objects.filter(username=username).get() except User.DoesNotExist: return None + if user.check_password(password): + return user else: - if user.check_password(password): - return user - return None + return None From ff423c0cfe6482a1db21341552c759014138f42a Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:21:47 +0900 Subject: [PATCH 491/552] =?UTF-8?q?refactor(users.serializers,=20users.vie?= =?UTF-8?q?ws):=20serializer=EC=9D=98=20=EB=8D=B0=EC=9D=B4=ED=84=B0?= =?UTF-8?q?=EB=A5=BC=20=ED=8C=8C=EC=8B=B1=ED=95=98=EC=97=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=ED=95=98=EB=8A=94=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=EC=9D=80=20SignInSerializer=20=EC=97=90=EA=B2=8C=20=EC=B1=85?= =?UTF-8?q?=EC=9E=84=EC=9D=84=20=EC=9C=84=EC=9E=84=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 60 ++++++++++++++++++++++++++++++++++++++-- app/users/views.py | 20 +++++--------- 2 files changed, 64 insertions(+), 16 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 214e7db..e09db1e 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -1,4 +1,10 @@ +from typing import Optional + +from django.contrib.auth import authenticate +from django.contrib.auth import login +from django.http.request import HttpRequest from rest_framework import serializers +from rest_framework.exceptions import AuthenticationFailed from boj.models import BOJUser from boj.serializers import BOJUserSerializer @@ -48,9 +54,57 @@ class EmailTokenSerializer(serializers.Serializer): token = serializers.CharField() -class SignInSerializer(serializers.Serializer): - email = serializers.EmailField() - password = serializers.CharField(style={'input_type': 'password'}) +# User Serializers + +class BOJField(serializers.SerializerMethodField): + def to_representation(self, value: BOJUser): + return BOJUserSerializer(value).data + + def get_attribute(self, instance: User) -> BOJUser: + assert isinstance(instance, User) + return BOJUser.objects.get_by_username(instance.boj_username) + + +class SignInSerializer(serializers.ModelSerializer): + boj = BOJField() + + class Meta: + model = User + fields = [ + PK, + User.field_name.EMAIL, + User.field_name.PASSWORD, + User.field_name.USERNAME, + User.field_name.PROFILE_IMAGE, + User.field_name.TOKEN, + User.field_name.REFRESH_TOKEN, + 'boj', + ] + extra_kwargs = { + PK: {'read_only': True}, + User.field_name.PASSWORD: { + 'write_only': True, + 'style': {'input_type': 'password'}, + }, + User.field_name.USERNAME: {'read_only': True}, + User.field_name.PROFILE_IMAGE: {'read_only': True}, + User.field_name.TOKEN: {'read_only': True}, + User.field_name.REFRESH_TOKEN: {'read_only': True}, + } + + def create(self, validated_data): + # 여기서는 모델을 생성하진 않고.. 로그인을 한다! + # P.S. self.instance = None 인 경우 이 곳으로 온다는 점을 이용하였다. + request: HttpRequest = self.context['request'] + user: Optional[User] = authenticate(request=request, **validated_data) + if user is None: + raise AuthenticationFailed('Invalid email or password') + user.rotate_token() + login(request=request, user=user) + return user + + def update(self, instance, validated_data): + raise NotImplementedError class SignUpSerializer(serializers.ModelSerializer): diff --git a/app/users/views.py b/app/users/views.py index 7b25ebc..1a9d5ca 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -10,6 +10,7 @@ from users import models from users.serializers import IsEmailUsableSerializer from users.serializers import IsUsernameUsableSerializer +from users.serializers import SignInSerializer from users import serializers from users import services @@ -45,23 +46,16 @@ class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] permission_classes = [AllowAny] - serializer_class = serializers.SignInSerializer + serializer_class = SignInSerializer def post(self, request: Request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) - user = services.sign_in( - request=self.request, - email=serializer.validated_data['email'], - password=serializer.validated_data['password'], - ) - token = services.get_user_jwt(user) - serializer = serializers.UserSerializer(instance=user) - return Response({ - **serializer.data, - 'access_token': str(token.access_token), - 'refresh_token': str(token.token), - }) + self.perform_login(serializer) + return Response(serializer.data) + + def perform_login(self, serializer: SignInSerializer): + serializer.save() class SignUpAPIView(generics.CreateAPIView): From 489ee7591c4e3854c8ecb31bcc45b4a44b32b136 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 19:45:20 +0900 Subject: [PATCH 492/552] =?UTF-8?q?fix(boj.models):=20get=5For=5Fcreate()?= =?UTF-8?q?=EC=9D=98=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=82=AC=EC=9A=A9=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/boj/models.py b/app/boj/models.py index 6e15d1c..aebf209 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -16,7 +16,7 @@ def filter(self, username: Optional[str] = None, *args, **kwargs) -> models.Quer return super().filter(*args, **kwargs) def get_by_username(self, username: str) -> BOJUser: - return self.filter(username=username).get_or_create()[0] + return self.get_or_create(**{BOJUser.field_name.USERNAME: username})[0] class BOJUser(models.Model): From 57efd266547c413102eb573beb1712274c3fb27e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 20:03:53 +0900 Subject: [PATCH 493/552] =?UTF-8?q?test(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=8A=94=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EC=99=84=EC=84=B1=EB=8F=84=EC=97=90=20=EC=98=81?= =?UTF-8?q?=ED=96=A5=EC=9D=84=20=EB=B0=9B=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20force=5Flogin()=20=EC=9D=84=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index dc934a0..5e6645f 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -1,6 +1,7 @@ from django.test import TestCase from rest_framework import status +from users.models import User from users.models import UserEmailVerification @@ -71,12 +72,8 @@ class SignOutTest(TestCase): fixtures = ['user.sample.json'] def setUp(self) -> None: - # authenticate() 함수에서는 email을 username으로 바꾸어서 전달하기 때문에 아래와 같이 설정함. - logged_in = self.client.login(**{ - "username": "test@example.com", - "password": "passw0rd@test", - }) - self.assertTrue(logged_in) + self.user = User.objects.get(pk=1) + self.client.force_login(self.user) def test_로그아웃_성공(self): res = self.client.get("/api/v1/auth/signout") From 533756ffeb6d874a0f6a3f9c4f708ed828979db4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 20:04:41 +0900 Subject: [PATCH 494/552] =?UTF-8?q?test(users):=20=EA=B8=B0=EB=8A=A5?= =?UTF-8?q?=EC=9D=80=20=EC=A0=95=EC=83=81=20=EB=8F=99=EC=9E=91=ED=95=98?= =?UTF-8?q?=EB=82=98=20res.json()=EC=9D=B4=20=EC=98=A4=EB=A5=98=EB=A5=BC?= =?UTF-8?q?=20=EB=B0=9C=EC=83=9D=EC=8B=9C=ED=82=A4=EB=8A=94=20=EA=B2=BD?= =?UTF-8?q?=EC=9A=B0=EA=B0=80=20=EC=9E=88=EC=96=B4=EC=84=9C=20=ED=95=B4?= =?UTF-8?q?=EB=8B=B9=20=EC=BD=94=EB=93=9C=EB=A5=BC=20=EC=A0=9C=EA=B1=B0?= =?UTF-8?q?=ED=95=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index 5e6645f..6edacca 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -19,7 +19,7 @@ def test_로그인성공(self): "password": "passw0rd@test", }, ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) + self.assertEqual(res.status_code, status.HTTP_200_OK) def test_비밀번호_불일치(self): res = self.client.post( @@ -29,7 +29,7 @@ def test_비밀번호_불일치(self): "password": "password@test", } ) - self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN, res.json()) + self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) class SignUpTest(TestCase): @@ -52,7 +52,7 @@ def test_회원가입_성공(self): "verification_token": self.sample_object.verification_token, } ) - self.assertEqual(res.status_code, status.HTTP_201_CREATED, res.json()) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) def test_인증토큰_불일치(self): res = self.client.post( @@ -65,7 +65,7 @@ def test_인증토큰_불일치(self): "verification_token": 'this_token_must_not_match_the_sample...', } ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST, res.json()) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) class SignOutTest(TestCase): @@ -77,7 +77,7 @@ def setUp(self) -> None: def test_로그아웃_성공(self): res = self.client.get("/api/v1/auth/signout") - self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT, res.json()) + self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) class UsernameCheckTest(TestCase): @@ -90,7 +90,7 @@ def test_사용_가능한_사용자명(self): "username": "unique", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) + self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "username": "unique", "is_usable": True, @@ -103,7 +103,7 @@ def test_사용_불가능한_사용자명(self): "username": "test", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) + self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "username": "test", "is_usable": False, @@ -120,7 +120,7 @@ def test_사용_가능한_이메일(self): "email": "unique@notexample.com", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) + self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": "unique@notexample.com", "is_usable": True, @@ -133,7 +133,7 @@ def test_사용_불가능한_이메일(self): "email": "test@example.com", } ) - self.assertEqual(res.status_code, status.HTTP_200_OK, res.json()) + self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": "test@example.com", "is_usable": False, From 734068f8923d370a5b5738165aac7ef627abf8cb Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 20:10:50 +0900 Subject: [PATCH 495/552] =?UTF-8?q?fix(users.serializers):=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20serializer=EC=9D=B4=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=20validation(=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EA=B2=80=EC=82=AC)=EB=A1=9C=20=EC=9D=B8=ED=95=B4=20=EC=8B=A4?= =?UTF-8?q?=ED=8C=A8=ED=95=98=EB=8D=98=20=EA=B2=83=EC=9D=84=20=EC=A0=95?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit validators를 EmailValidator만 사용하도록 수동으로 지정하였다. --- app/users/serializers.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index e09db1e..803bb1d 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -2,6 +2,7 @@ from django.contrib.auth import authenticate from django.contrib.auth import login +from django.core.validators import EmailValidator from django.http.request import HttpRequest from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed @@ -82,6 +83,10 @@ class Meta: ] extra_kwargs = { PK: {'read_only': True}, + User.field_name.EMAIL: { + 'write_only': True, + 'validators': [EmailValidator], + }, User.field_name.PASSWORD: { 'write_only': True, 'style': {'input_type': 'password'}, @@ -92,19 +97,15 @@ class Meta: User.field_name.REFRESH_TOKEN: {'read_only': True}, } - def create(self, validated_data): - # 여기서는 모델을 생성하진 않고.. 로그인을 한다! - # P.S. self.instance = None 인 경우 이 곳으로 온다는 점을 이용하였다. + def save(self, **kwargs): + # 여기서는 사용자의 액세스 토큰 외의 정보를 수정하지는 않는다. request: HttpRequest = self.context['request'] - user: Optional[User] = authenticate(request=request, **validated_data) - if user is None: - raise AuthenticationFailed('Invalid email or password') + user: Optional[User] + if (user := authenticate(request=request, **self.validated_data)) is None: + raise AuthenticationFailed(f'Invalid email or password {self.validated_data}') + login(request, user) user.rotate_token() - login(request=request, user=user) - return user - - def update(self, instance, validated_data): - raise NotImplementedError + self.instance = user class SignUpSerializer(serializers.ModelSerializer): From bcedc7f6b06c21bb049cc4de14abb940b0e278e0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Fri, 30 Aug 2024 20:11:54 +0900 Subject: [PATCH 496/552] =?UTF-8?q?refactor(users.views):=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=B4=EC=A7=84=20services=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20=EC=9D=98=EC=A1=B4=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/users/views.py b/app/users/views.py index 1a9d5ca..7a88839 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,3 +1,4 @@ +from django.contrib.auth import logout from drf_yasg.utils import swagger_auto_schema from rest_framework import generics from rest_framework import status @@ -83,11 +84,8 @@ class SignOutAPIView(generics.GenericAPIView): """사용자 로그아웃 API""" permission_classes = [IsAuthenticated] - @swagger_auto_schema(responses={ - status.HTTP_204_NO_CONTENT: '로그아웃 성공', - }) def get(self, request, *args, **kwargs): - services.sign_out(request) + logout(request) return Response(status=status.HTTP_204_NO_CONTENT) From e9ed0090aabfdf01bba2d3a4983b4f4c6a9604f3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sat, 31 Aug 2024 00:41:53 +0900 Subject: [PATCH 497/552] =?UTF-8?q?refactor(users.views):=20=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EA=B0=80=EC=9E=85=EC=9D=80=20SignUpSerializer?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EB=8B=B4=EB=8B=B9=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 21 ++++++++++++++++++++- app/users/views.py | 17 ++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 803bb1d..2029339 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -6,10 +6,12 @@ from django.http.request import HttpRequest from rest_framework import serializers from rest_framework.exceptions import AuthenticationFailed +from rest_framework.exceptions import ValidationError from boj.models import BOJUser from boj.serializers import BOJUserSerializer from users.models import User +from users.models import UserEmailVerification PK = 'id' @@ -109,11 +111,12 @@ def save(self, **kwargs): class SignUpSerializer(serializers.ModelSerializer): - verification_token = serializers.CharField() + verification_token = serializers.CharField(write_only=True) class Meta: model = User fields = [ + PK, User.field_name.EMAIL, User.field_name.PROFILE_IMAGE, User.field_name.USERNAME, @@ -121,6 +124,22 @@ class Meta: User.field_name.BOJ_USERNAME, 'verification_token', ] + extra_kwargs = { + PK: {'read_only': True}, + User.field_name.EMAIL: {'write_only': True}, + User.field_name.PASSWORD: {'write_only': True, 'style': {'input_type': 'password'}}, + } + + def create(self, validated_data: dict): + email = validated_data.get('email') + verification_token = validated_data.pop('verification_token') + try: + email_verification = UserEmailVerification.objects.get(email=email) + except UserEmailVerification.DoesNotExist: + raise ValidationError('이메일 인증 토큰이 발급되지 않았습니다.') + if email_verification.verification_token != verification_token: + raise ValidationError('이메일 인증 토큰이 올바르지 않습니다.') + return super().create(validated_data) class UsernameSerializer(serializers.Serializer): diff --git a/app/users/views.py b/app/users/views.py index 7a88839..cf69dc7 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -12,6 +12,7 @@ from users.serializers import IsEmailUsableSerializer from users.serializers import IsUsernameUsableSerializer from users.serializers import SignInSerializer +from users.serializers import SignUpSerializer from users import serializers from users import services @@ -63,21 +64,7 @@ class SignUpAPIView(generics.CreateAPIView): """사용자 등록(회원가입) API""" authentication_classes = [] permission_classes = [AllowAny] - serializer_class = serializers.SignUpSerializer - - def create(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - self.perform_create(serializer) - serializer = serializers.UserSerializer(instance=serializer.instance) - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - - def perform_create(self, serializer: serializers.SignUpSerializer): - email = serializer.validated_data['email'] - token = serializer.validated_data.pop('verification_token') - services.verify_token(email, token) - serializer.instance = services.sign_up(**serializer.validated_data) + serializer_class = SignUpSerializer class SignOutAPIView(generics.GenericAPIView): From 4631336998b5d711a47771d66e0513b6fe9962ae Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 00:34:32 +0900 Subject: [PATCH 498/552] =?UTF-8?q?feat(users):=20/auth/usability=20?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9D=B4=EB=A9=94=EC=9D=BC,=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=EB=AA=85=20=EC=82=AC=EC=9A=A9=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=97=AC=EB=B6=80=EB=A5=BC=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 37 +++++++++++++++++++++++++++++++++++++ app/users/urls.py | 1 + app/users/views.py | 16 ++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/app/users/serializers.py b/app/users/serializers.py index 2029339..486369b 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -19,6 +19,43 @@ # Username or Email Serializers +class UsabilityEmailField(serializers.Serializer): + value = serializers.EmailField(read_only=True) + is_usable = serializers.BooleanField(read_only=True) + + def get_attribute(self, instance): + data = { + 'value': None, + 'is_usable': False, + } + if 'email' in instance: + email = instance['email'] + data['email'] = email + data['is_usable'] = not User.objects.filter(email=email).exists() + return data + + +class UsabilityUsernameField(serializers.Serializer): + value = serializers.CharField(read_only=True) + is_usable = serializers.BooleanField(read_only=True) + + def get_attribute(self, instance): + data = { + 'value': None, + 'is_usable': False, + } + if 'username' in instance: + name = instance['username'] + data['username'] = name + data['is_usable'] = not User.objects.filter(username=name).exists() + return data + + +class UsabilitySerializer(serializers.Serializer): + email = UsabilityEmailField(default=None) + username = UsabilityUsernameField(default=None) + + class EmailSerializer(serializers.Serializer): email = serializers.EmailField() diff --git a/app/users/urls.py b/app/users/urls.py index 57938bd..38dc270 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -12,6 +12,7 @@ path("/username/check", users.views.UsernameCheckAPIView.as_view()), path("/email/check", users.views.EmailCheckAPIView.as_view()), path("/email/verify", users.views.EmailVerifyAPIView.as_view()), + path("/usability", users.views.UsabilityAPIView.as_view()), ])), path("user", include([ path("/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), diff --git a/app/users/views.py b/app/users/views.py index cf69dc7..df4740e 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -11,12 +11,14 @@ from users import models from users.serializers import IsEmailUsableSerializer from users.serializers import IsUsernameUsableSerializer +from users.serializers import UsabilitySerializer from users.serializers import SignInSerializer from users.serializers import SignUpSerializer from users import serializers from users import services +# TODO: Deprecate class EmailCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" @@ -31,8 +33,10 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) +# TODO: Deprecate class UsernameCheckAPIView(generics.GenericAPIView): """이메일이 사용가능한지 검사 API""" + authentication_classes = [] permission_classes = [AllowAny] serializer_class = IsUsernameUsableSerializer @@ -44,6 +48,18 @@ def get(self, request: Request, *args, **kwargs): return Response(serializer.data) +class UsabilityAPIView(generics.RetrieveAPIView): + """현재 로그인한 사용자 정보를 조회/수정하는 API""" + + permission_classes = [AllowAny] + serializer_class = UsabilitySerializer + + @swagger_auto_schema(query_serializer=UsabilitySerializer) + def retrieve(self, request: Request, *args, **kwargs): + serializer = self.get_serializer(request.query_params) + return Response(serializer.data) + + class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] From 48509ed1cccff843e2dd4588d08643e55f4f5ee8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 01:46:30 +0900 Subject: [PATCH 499/552] feat(users.models): create UserEmailVerificationManager --- app/users/models.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/users/models.py b/app/users/models.py index a918b63..47e6fd6 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -118,6 +118,11 @@ def rotate_token(self): self.save() +class UserEmailVerificationManager(BaseUserManager): + def get_or_create_by_email(self, email: str) -> UserEmailVerification: + return super().get_or_create(**{UserEmailVerification.field_name.EMAIL: email})[0] + + class UserEmailVerification(models.Model): email = models.EmailField( help_text='이메일 주소', @@ -136,6 +141,8 @@ class UserEmailVerification(models.Model): expires_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True) + objects: UserEmailVerificationManager = UserEmailVerificationManager() + class field_name: EMAIL = 'email' VERIFICATION_CODE = 'verification_code' From 8000e80a2f1f08e955c0564d21f6f10684d7e02b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 01:48:55 +0900 Subject: [PATCH 500/552] =?UTF-8?q?refactor(users.models):=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=BD=94=EB=93=9C=EC=99=80=20=EB=A7=8C=EB=A3=8C=20?= =?UTF-8?q?=EC=8B=9C=EA=B0=84=EC=97=90=20Null=20=ED=97=88=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/users/models.py b/app/users/models.py index 47e6fd6..14f3ae9 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -130,15 +130,18 @@ class UserEmailVerification(models.Model): ) verification_code = models.TextField( help_text='인증 코드', - null=False, - blank=False, + null=True, + blank=True, ) verification_token = models.TextField( help_text='인증 토큰', null=True, blank=True, ) - expires_at = models.DateTimeField(auto_now_add=True) + expires_at = models.DateTimeField( + null=True, + blank=True, + ) created_at = models.DateTimeField(auto_now_add=True) objects: UserEmailVerificationManager = UserEmailVerificationManager() @@ -151,7 +154,7 @@ class field_name: CREATED_AT = 'created_at' def is_expired(self) -> bool: - return self.expires_at < timezone.now() + return (self.expires_at is not None) and self.expires_at < timezone.now() def rotate_code(self, code_len: int = 6): self.verification_code = self._create_code(code_len) From 8a8a0564bf3215602148a72c0a2b8fa8c43fca03 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 01:49:36 +0900 Subject: [PATCH 501/552] =?UTF-8?q?feat(users.models):=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EC=BD=94=EB=93=9C,=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=ED=95=A8=EC=88=98=EC=99=80=20=ED=8C=8C?= =?UTF-8?q?=EA=B8=B0=20=ED=95=A8=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/app/users/models.py b/app/users/models.py index 14f3ae9..9ea7cce 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -10,6 +10,7 @@ from django.contrib.auth.models import PermissionsMixin from django.db import models from django.utils import timezone +from rest_framework.exceptions import ValidationError from rest_framework_simplejwt.tokens import RefreshToken @@ -156,6 +157,26 @@ class field_name: def is_expired(self) -> bool: return (self.expires_at is not None) and self.expires_at < timezone.now() + def is_valid_code(self, code: str, raise_exception=False) -> bool: + try: + assert isinstance(code, str), '올바르지 않은 형식입니다.' + assert code, '인증 코드가 비어있습니다.' + assert not self.is_expired(), '인증 코드가 만료되었습니다.' + assert self.verification_code == code, '인증 코드가 일치하지 않습니다.' + except AssertionError as exception: + if raise_exception: + raise ValidationError(exception) + return False + else: + return True + + def revoke_code(self): + self.verification_code = None + self.expires_at = None + + def revoke_token(self): + self.verification_token = None + def rotate_code(self, code_len: int = 6): self.verification_code = self._create_code(code_len) self.expires_at = timezone.now() + timedelta(minutes=5) From 06d764777896571bcefb5395b742701a2fc14aa3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 01:49:58 +0900 Subject: [PATCH 502/552] =?UTF-8?q?fix(users.models):=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=EC=9D=B4=20=EC=A0=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/models.py b/app/users/models.py index 9ea7cce..5ed7443 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -184,7 +184,7 @@ def rotate_code(self, code_len: int = 6): def rotate_token(self, seed: Optional[str] = None): if seed is None: seed = self._create_code(code_len=16) - return sha256(seed.encode()).hexdigest() + self.verification_token = sha256(seed.encode()).hexdigest() def _create_code(self, code_len: int) -> str: return ''.join(chr(randint(ord('A'), ord('Z'))) for _ in range(code_len)) From 30b97b75bbbb43d34e33b98b637f937c46ae957c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:01:12 +0900 Subject: [PATCH 503/552] =?UTF-8?q?chore(config):=20test=EC=9A=A9=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=ED=94=84=EB=A1=9C=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/test.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 app/config/settings/test.py diff --git a/app/config/settings/test.py b/app/config/settings/test.py new file mode 100644 index 0000000..4666df3 --- /dev/null +++ b/app/config/settings/test.py @@ -0,0 +1,9 @@ +from config.settings.base import * + + +DEBUG = True + +ALLOWED_HOSTS = ['*'] + +EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend" +EMAIL_FILE_PATH = "/logs" From 458826009088463194bb3f7c2c4b292a5c1c7a66 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:08:22 +0900 Subject: [PATCH 504/552] =?UTF-8?q?test(users):=20UsabilityAPITest=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 76 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 6edacca..557b412 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -138,3 +138,79 @@ def test_사용_불가능한_이메일(self): "email": "test@example.com", "is_usable": False, }) + + +class UsabilityAPITest(TestCase): + fixtures = ['user.sample.json'] + + def test_200_사용_가능한_이메일(self): + res = self.client.get("/api/v1/auth/usability", { + "email": "unique@notexample.com", + } + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": { + "value": "unique@notexample.com", + "is_usable": True, + }, + "username": { + "value": None, + "is_usable": False, + }, + }) + + def test_200_사용_불가능한_이메일(self): + res = self.client.get("/api/v1/auth/usability", { + "email": "test@example.com", + } + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": { + "value": "test@example.com", + "is_usable": False, + }, + "username": { + "value": None, + "is_usable": False, + }, + }) + + def test_200_사용_가능한_사용자명(self): + res = self.client.get("/api/v1/auth/usability", { + "username": "unique", + } + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": { + "value": None, + "is_usable": False, + }, + "username": { + "value": "unique", + "is_usable": True, + }, + }) + + def test_200_사용_불가능한_사용자명(self): + res = self.client.get("/api/v1/auth/usability", { + "username": "test", + } + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": { + "value": None, + "is_usable": False, + }, + "username": { + "value": "test", + "is_usable": True, + }, + }) + + def test_400_빈_데이터_전송(self): + res = self.client.get("/api/v1/auth/usability") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) From da2050599ba1b982343d8b8dcf57d4054273f865 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:32:33 +0900 Subject: [PATCH 505/552] =?UTF-8?q?fix(users.serializers):=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC,=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=AA=85=20?= =?UTF-8?q?=EB=AA=A8=EB=91=90=20=EC=95=88=20=EA=B8=B0=EC=9E=85=ED=95=98?= =?UTF-8?q?=EC=98=80=EC=9D=84=20=EB=95=8C=20400=20=EC=9D=B4=20=EB=9C=A8?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 14 ++++++++++++-- app/users/views.py | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 486369b..50611de 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -30,7 +30,7 @@ def get_attribute(self, instance): } if 'email' in instance: email = instance['email'] - data['email'] = email + data['value'] = email data['is_usable'] = not User.objects.filter(email=email).exists() return data @@ -46,15 +46,25 @@ def get_attribute(self, instance): } if 'username' in instance: name = instance['username'] - data['username'] = name + data['value'] = name data['is_usable'] = not User.objects.filter(username=name).exists() return data +class UsabilitySerializerForQueryParameter(serializers.Serializer): + email = serializers.EmailField(default=None) + username = serializers.CharField(default=None) + + class UsabilitySerializer(serializers.Serializer): email = UsabilityEmailField(default=None) username = UsabilityUsernameField(default=None) + def to_representation(self, instance: dict): + if (instance.get('email', None) is None) and (instance.get('username', None) is None): + raise ValidationError('이메일 혹은 사용자명 중 하나는 주어져야 합니다.') + return super().to_representation(instance) + class EmailSerializer(serializers.Serializer): email = serializers.EmailField() diff --git a/app/users/views.py b/app/users/views.py index df4740e..b678500 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -11,6 +11,7 @@ from users import models from users.serializers import IsEmailUsableSerializer from users.serializers import IsUsernameUsableSerializer +from users.serializers import UsabilitySerializerForQueryParameter from users.serializers import UsabilitySerializer from users.serializers import SignInSerializer from users.serializers import SignUpSerializer @@ -54,7 +55,10 @@ class UsabilityAPIView(generics.RetrieveAPIView): permission_classes = [AllowAny] serializer_class = UsabilitySerializer - @swagger_auto_schema(query_serializer=UsabilitySerializer) + @swagger_auto_schema(query_serializer=UsabilitySerializerForQueryParameter) + def get(self, *args, **kwargs): + return super().get(*args, **kwargs) + def retrieve(self, request: Request, *args, **kwargs): serializer = self.get_serializer(request.query_params) return Response(serializer.data) From 9b483bed7d90a2b15a5da8acba92ff0fb4e417e3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:33:12 +0900 Subject: [PATCH 506/552] =?UTF-8?q?chore(users.admin):=20UserEmailVerifica?= =?UTF-8?q?tion=20=EC=97=90=20=EB=8C=80=ED=95=9C=20=EA=B4=80=EB=A6=AC?= =?UTF-8?q?=EC=9E=90=20=EB=AA=A8=EB=8D=B8=20=EB=B7=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/admin.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/app/users/admin.py b/app/users/admin.py index cba8205..ed71a17 100644 --- a/app/users/admin.py +++ b/app/users/admin.py @@ -5,11 +5,6 @@ from users.models import UserEmailVerification -admin.site.register([ - UserEmailVerification, -]) - - @admin.register(User) class UserAdmin(BaseUserAdmin): fieldsets = None @@ -22,3 +17,13 @@ class UserAdmin(BaseUserAdmin): User.field_name.IS_SUPERUSER, User.field_name.CREATED_AT, ] + + +@admin.register(UserEmailVerification) +class UserEmailVerificationModelAdmin(admin.ModelAdmin): + list_display = [ + UserEmailVerification.field_name.EMAIL, + UserEmailVerification.field_name.VERIFICATION_CODE, + UserEmailVerification.field_name.VERIFICATION_TOKEN, + UserEmailVerification.field_name.EXPIRES_AT, + ] From c04925e23d16df7c6d5562dfc61e73884bcf65c2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:51:13 +0900 Subject: [PATCH 507/552] =?UTF-8?q?feat(users):=20/auth/verification=20?= =?UTF-8?q?=EC=9D=84=20=ED=86=B5=ED=95=B4=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 34 ++++++++++++++++++++++++++++++++++ app/users/services.py | 30 ++++++++++++++++++++++++++++++ app/users/urls.py | 1 + app/users/views.py | 19 +++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 app/users/services.py diff --git a/app/users/serializers.py b/app/users/serializers.py index 50611de..853305a 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -104,6 +104,40 @@ class EmailTokenSerializer(serializers.Serializer): token = serializers.CharField() +# Email Validation Serializers + +class EmailVerificationSerializer(serializers.ModelSerializer): + class Meta: + model = UserEmailVerification + fields = [ + UserEmailVerification.field_name.EMAIL, + UserEmailVerification.field_name.VERIFICATION_CODE, + UserEmailVerification.field_name.VERIFICATION_TOKEN, + UserEmailVerification.field_name.EXPIRES_AT, + ] + extra_kwargs = { + UserEmailVerification.field_name.EMAIL: {'validators': [EmailValidator]}, + UserEmailVerification.field_name.VERIFICATION_CODE: {'write_only': True}, + UserEmailVerification.field_name.VERIFICATION_TOKEN: {'read_only': True}, + UserEmailVerification.field_name.EXPIRES_AT: {'read_only': True}, + } + + def update(self, instance: UserEmailVerification, validated_data: dict): + verification_code = validated_data.get( + UserEmailVerification.field_name.VERIFICATION_CODE, None) + if verification_code is None: + # 코드가 없다면 코드를 만들어 주자. + instance.revoke_token() + instance.rotate_code() + else: + # 인증 코드가 있다면 검증해주자. + instance.is_valid_code(verification_code, raise_exception=True) + instance.revoke_code() + instance.rotate_token() + instance.save() + return instance + + # User Serializers class BOJField(serializers.SerializerMethodField): diff --git a/app/users/services.py b/app/users/services.py new file mode 100644 index 0000000..e75f160 --- /dev/null +++ b/app/users/services.py @@ -0,0 +1,30 @@ +from textwrap import dedent + +from background_task import background +from django.core.mail import send_mail +from django.db.models.signals import post_save +from django.dispatch import receiver + +from users.models import UserEmailVerification + + +@receiver(post_save, sender=UserEmailVerification) +def notify_on_code_generated(sender, instance: UserEmailVerification, created: bool, **kwargs): + if not instance.is_expired(): + subject = '[Time Limit Exceeded] 이메일 주소 인증 코드' + message = dedent(f""" + 인증 코드: {instance.verification_code} + """) + recipient = instance.email + _schedule_mail(subject, message, recipient) + + +@background +def _schedule_mail(subject: str, message: str, recipient: str) -> None: + send_mail( + subject=subject, + message=message, + recipient_list=[recipient], + from_email=None, + fail_silently=False, + ) diff --git a/app/users/urls.py b/app/users/urls.py index 38dc270..bc02e10 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -13,6 +13,7 @@ path("/email/check", users.views.EmailCheckAPIView.as_view()), path("/email/verify", users.views.EmailVerifyAPIView.as_view()), path("/usability", users.views.UsabilityAPIView.as_view()), + path("/verification", users.views.EmailVerificationAPIView.as_view()), ])), path("user", include([ path("/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), diff --git a/app/users/views.py b/app/users/views.py index b678500..5c3c0ba 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -9,6 +9,8 @@ from rest_framework.response import Response from users import models +from users.models import UserEmailVerification +from users.serializers import EmailVerificationSerializer from users.serializers import IsEmailUsableSerializer from users.serializers import IsUsernameUsableSerializer from users.serializers import UsabilitySerializerForQueryParameter @@ -64,6 +66,23 @@ def retrieve(self, request: Request, *args, **kwargs): return Response(serializer.data) +class EmailVerificationAPIView(generics.mixins.UpdateModelMixin, + generics.GenericAPIView): + """이메일 인증 코드 전송 API""" + authentication_classes = [] + throttle_classes = [] + permission_classes = [AllowAny] + queryset = UserEmailVerification + serializer_class = EmailVerificationSerializer + + def get_object(self): + email = self.request.data['email'] + return UserEmailVerification.objects.get_or_create_by_email(email=email) + + def post(self, request, *args, **kwargs): + return self.update(request, *args, **kwargs) + + class SignInAPIView(generics.GenericAPIView): """사용자 로그인 API""" authentication_classes = [] From 38530db24c3b71f0aad3656dd8c577cbf65db3d9 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:51:25 +0900 Subject: [PATCH 508/552] =?UTF-8?q?fix(users.models):=20expiration=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EC=BD=94=EB=93=9C=EC=9D=98=20=EC=9E=98?= =?UTF-8?q?=EB=AA=BB=EB=90=9C=20=EB=A1=9C=EC=A7=81=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/models.py b/app/users/models.py index 5ed7443..27f613e 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -155,7 +155,7 @@ class field_name: CREATED_AT = 'created_at' def is_expired(self) -> bool: - return (self.expires_at is not None) and self.expires_at < timezone.now() + return (self.expires_at is None) or self.expires_at < timezone.now() def is_valid_code(self, code: str, raise_exception=False) -> bool: try: From 9731fb3ff68ca0ac50a900404464a3f1358ea17b Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:51:54 +0900 Subject: [PATCH 509/552] =?UTF-8?q?fix(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=B1=EC=97=94=EB=93=9C=EA=B0=80=20=ED=82=A4?= =?UTF-8?q?=EC=9B=8C=EB=93=9C=20=EC=9D=B8=EC=9E=90=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EB=8C=80=EB=A1=9C=20=EC=A0=84=EB=8B=AC=EB=B0=9B=EC=A7=80=20?= =?UTF-8?q?=EB=AA=BB=ED=95=98=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/backends.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/users/backends.py b/app/users/backends.py index 3f8daaa..cd2ea86 100644 --- a/app/users/backends.py +++ b/app/users/backends.py @@ -5,9 +5,11 @@ class UserAuthBackend(ModelBackend): - def authenticate(self, request: HttpRequest, username=None, password=None, **kwargs): + def authenticate(self, request: HttpRequest, email=None, username=None, password=None, **kwargs): + if email is None and username is not None: + return self.authenticate(request, email=username, password=password) try: - user = User.objects.filter(username=username).get() + user = User.objects.filter(email=email).get() except User.DoesNotExist: return None if user.check_password(password): From 7c820daf98b932cf817997a496b26fdcab6ecd70 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:55:22 +0900 Subject: [PATCH 510/552] =?UTF-8?q?refactor(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=A1=9C=EC=A7=81=EC=9D=80=20Serializer=EA=B0=80?= =?UTF-8?q?=20=EC=95=84=EB=8B=8C=20View=EC=97=90=EC=84=9C=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 16 ---------------- app/users/views.py | 13 ++++++++++++- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 853305a..8dbb084 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -1,11 +1,5 @@ -from typing import Optional - -from django.contrib.auth import authenticate -from django.contrib.auth import login from django.core.validators import EmailValidator -from django.http.request import HttpRequest from rest_framework import serializers -from rest_framework.exceptions import AuthenticationFailed from rest_framework.exceptions import ValidationError from boj.models import BOJUser @@ -180,16 +174,6 @@ class Meta: User.field_name.REFRESH_TOKEN: {'read_only': True}, } - def save(self, **kwargs): - # 여기서는 사용자의 액세스 토큰 외의 정보를 수정하지는 않는다. - request: HttpRequest = self.context['request'] - user: Optional[User] - if (user := authenticate(request=request, **self.validated_data)) is None: - raise AuthenticationFailed(f'Invalid email or password {self.validated_data}') - login(request, user) - user.rotate_token() - self.instance = user - class SignUpSerializer(serializers.ModelSerializer): verification_token = serializers.CharField(write_only=True) diff --git a/app/users/views.py b/app/users/views.py index 5c3c0ba..d4d739d 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -1,14 +1,20 @@ +from typing import Optional + +from django.contrib.auth import authenticate +from django.contrib.auth import login from django.contrib.auth import logout from drf_yasg.utils import swagger_auto_schema from rest_framework import generics from rest_framework import status from rest_framework import throttling +from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import AllowAny from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response from users import models +from users.models import User from users.models import UserEmailVerification from users.serializers import EmailVerificationSerializer from users.serializers import IsEmailUsableSerializer @@ -96,7 +102,12 @@ def post(self, request: Request, *args, **kwargs): return Response(serializer.data) def perform_login(self, serializer: SignInSerializer): - serializer.save() + user: Optional[User] + if (user := authenticate(request=self.request, **serializer.validated_data)) is None: + raise AuthenticationFailed(f'Invalid email or password') + login(self.request, user) + user.rotate_token() # TODO: 이 작업을 로그인 백엔드에서 수행하도록 변경 + serializer.instance = user class SignUpAPIView(generics.CreateAPIView): From 4efe04ce83ac7fe2db6dce78b16a56bd57760c01 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 02:58:18 +0900 Subject: [PATCH 511/552] =?UTF-8?q?chore(users.views):=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EB=8F=85=EC=8A=A4=ED=8A=B8=EB=A7=81=20?= =?UTF-8?q?=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/users/views.py b/app/users/views.py index d4d739d..fa3c025 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -58,7 +58,7 @@ def get(self, request: Request, *args, **kwargs): class UsabilityAPIView(generics.RetrieveAPIView): - """현재 로그인한 사용자 정보를 조회/수정하는 API""" + """이메일/사용자명이 사용 가능한지 조회하는 API""" permission_classes = [AllowAny] serializer_class = UsabilitySerializer @@ -74,7 +74,11 @@ def retrieve(self, request: Request, *args, **kwargs): class EmailVerificationAPIView(generics.mixins.UpdateModelMixin, generics.GenericAPIView): - """이메일 인증 코드 전송 API""" + """이메일을 인증하기 위한 API. + + 이메일만 전달하면 새로운 코드를 발급 후 이메일로 전송해준다. + 코드를 함께 전달하면 새로운 인증 토큰을 발급하여 반환한다. + """ authentication_classes = [] throttle_classes = [] permission_classes = [AllowAny] @@ -126,10 +130,12 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) +# TODO: deprecate class EmailVerifyThrottle(throttling.AnonRateThrottle): THROTTLE_RATES = '1/min' +# TODO: deprecate class EmailVerifyAPIView(generics.GenericAPIView): """이메일 인증 코드 전송 API""" authentication_classes = [] From 2edf49d49ca3c031d4775a26aa08c75f83ae258c Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:03:19 +0900 Subject: [PATCH 512/552] refactor(users): remove unused, deprecated resources --- app/users/serializers.py | 38 -------- app/users/services/__init__.py | 26 ------ app/users/services/authentication.py | 39 -------- app/users/services/verification.py | 127 --------------------------- app/users/tests.py | 60 ------------- app/users/urls.py | 3 - app/users/views.py | 95 +------------------- 7 files changed, 3 insertions(+), 385 deletions(-) delete mode 100644 app/users/services/__init__.py delete mode 100644 app/users/services/authentication.py delete mode 100644 app/users/services/verification.py diff --git a/app/users/serializers.py b/app/users/serializers.py index 8dbb084..046416f 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -60,44 +60,6 @@ def to_representation(self, instance: dict): return super().to_representation(instance) -class EmailSerializer(serializers.Serializer): - email = serializers.EmailField() - - -class IsEmailUsableField(serializers.BooleanField): - def get_attribute(self, instance): - assert 'email' in instance, instance - assert isinstance(instance['email'], str) - return not User.objects.filter(email=instance['email']).exists() - - -class IsEmailUsableSerializer(serializers.Serializer): - email = serializers.EmailField() - is_usable = IsEmailUsableField(read_only=True) - - -class IsUsernameUsableField(serializers.BooleanField): - def get_attribute(self, instance): - assert 'username' in instance, instance - assert isinstance(instance['username'], str) - return not User.objects.filter(username=instance['username']).exists() - - -class IsUsernameUsableSerializer(serializers.Serializer): - username = serializers.CharField() - is_usable = IsUsernameUsableField() - - -class EmailCodeSerializer(serializers.Serializer): - email = serializers.EmailField() - code = serializers.CharField() - - -class EmailTokenSerializer(serializers.Serializer): - email = serializers.EmailField() - token = serializers.CharField() - - # Email Validation Serializers class EmailVerificationSerializer(serializers.ModelSerializer): diff --git a/app/users/services/__init__.py b/app/users/services/__init__.py deleted file mode 100644 index c32dca9..0000000 --- a/app/users/services/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -from users.services.authentication import ( - sign_in, - sign_up, - sign_out, - get_user_jwt, -) -from users.services.verification import ( - send_verification_code, - get_verification_token, - verify_token, - is_email_usable, - is_username_usable, -) - - -__all__ = ( - 'sign_in', - 'sign_up', - 'sign_out', - 'get_user_jwt', - 'send_verification_code', - 'get_verification_token', - 'verify_token', - 'is_email_usable', - 'is_username_usable', -) diff --git a/app/users/services/authentication.py b/app/users/services/authentication.py deleted file mode 100644 index 47b1d97..0000000 --- a/app/users/services/authentication.py +++ /dev/null @@ -1,39 +0,0 @@ -"""인증과 관련된 서비스들입니다. - -사용자 로그인, 회원가입, 로그아웃 로직을 담고 있습니다. -""" -from django.contrib.auth import authenticate, login, logout -from rest_framework.exceptions import AuthenticationFailed, ValidationError -from rest_framework.request import Request -from rest_framework_simplejwt.tokens import RefreshToken - -from users.models import User, UserEmailVerification, UserManager - - -def sign_up(email: str, username: str, password: str, **extra_fields) -> User: - """회원가입 - - 이메일 인증 토큰이 필요합니다. - 인증에 실패할 경우 ValidationError를 발생시킵니다.""" - user_manager: UserManager = User.objects - return user_manager.create_user(email, username, password, **extra_fields) - -def sign_in(request: Request, email: str, password: str) -> User: - """로그인 - - 사용자 인증에 실패할 경우 AuthenticationFailed를 발생시킵니다.""" - # 사용자 인증 - user = authenticate(request, username=email, password=password) - # 사용자 인증 실패 시 예외 발생 - if user is None: - raise AuthenticationFailed('Invalid email or password') - # 사용자 인증 성공 시 (세션) 로그인 - login(request, user) - return user - -def sign_out(request: Request): - """로그아웃""" - logout(request) - -def get_user_jwt(user: User) -> RefreshToken: - return RefreshToken.for_user(user) diff --git a/app/users/services/verification.py b/app/users/services/verification.py deleted file mode 100644 index fe0a7da..0000000 --- a/app/users/services/verification.py +++ /dev/null @@ -1,127 +0,0 @@ -"""이 서비스는 사용자 확인 절차를 수행하는 데 필요한 기능을 제공합니다. - -사용자 확인 절차는 다음과 같습니다: -1. 사용자가 이메일 주소를 입력합니다. -2. 서버는 해당 이메일 주소로 인증 코드를 전송합니다. -3. 사용자는 인증 코드를 입력합니다. -4. 서버는 인증 코드를 확인합니다. -5. 서버는 인증 코드를 확인한 사용자에게 인증 토큰을 전송합니다. -6. 사용자는 회원가입 절차에서 인증 토큰을 입력합니다. -7. 서버는 인증 토큰을 확인하여 사용자를 확인합니다. -""" -from hashlib import sha256 -from random import randint - -from django.core.mail import send_mail -from rest_framework.exceptions import ValidationError - -from users.models import User, UserEmailVerification - - -def is_email_usable(email: str) -> bool: - return not User.objects.filter(**{ - User.field_name.EMAIL: email, - }).exists() - - -def is_username_usable(username: str) -> bool: - return not User.objects.filter(**{ - User.field_name.USERNAME: username, - }).exists() - - -def send_verification_code(email: str) -> None: - if _is_verified(email): - raise ValidationError('Email is already verified.') - code = _get_verification_code(email) - _send_verification_code(email, code) - - -def get_verification_token(email: str, verification_code: str) -> str: - _validate_verification_code(email, verification_code) - return _get_verification_token(email) - - -def verify_token(email: str, verification_token: str) -> None: - _validate_verification_token(email, verification_token) - - -def _is_verified(email: str) -> bool: - return User.objects.filter(**{ - User.field_name.EMAIL: email, - }).exists() - - -def _get_verification_code(email: str) -> str: - if _has_verification_code(email): - if not (obj := _get_verification_object(email)).is_expired(): - return obj.verification_code - else: - obj.delete() - verification_code = _generate_verification_code() - _create_verification_object(email, verification_code) - return verification_code - - -def _send_verification_code(email: str, verification_code: str) -> str: - send_mail( - subject='[Time Limit Exceeded] 이메일 주소 인증 코드', - message=f'인증 코드: {verification_code}', - from_email=None, - recipient_list=[email], - fail_silently=False, - ) - - -def _get_verification_token(email: str) -> str: - verification_token = _generate_verification_token() - obj = _get_verification_object(email) - obj.verification_token = verification_token - obj.save() - return verification_token - - -def _validate_verification_code(email: str, verification_code: str) -> None: - if not _has_verification_code(email): - raise ValidationError('Verification code does not exist.') - obj = _get_verification_object(email) - if obj.is_expired(): - raise ValidationError('Verification code is expired.') - if obj.verification_code != verification_code: - raise ValidationError('Verification code is invalid.') - - -def _validate_verification_token(email: str, verification_token: str) -> None: - if not _has_verification_code(email): - raise ValidationError('Verification token does not exist.') - obj = _get_verification_object(email) - if obj.verification_token != verification_token: - raise ValidationError('Verification token is invalid.') - - -def _has_verification_code(email: str) -> bool: - return UserEmailVerification.objects.filter(**{ - UserEmailVerification.field_name.EMAIL: email - }).exists() - - -def _create_verification_object(email: str, verification_code: str) -> UserEmailVerification: - return UserEmailVerification.objects.create(**{ - UserEmailVerification.field_name.EMAIL: email, - UserEmailVerification.field_name.VERIFICATION_CODE: verification_code - }) - - -def _get_verification_object(email: str) -> UserEmailVerification: - return UserEmailVerification.objects.get(**{ - UserEmailVerification.field_name.EMAIL: email - }) - - -def _generate_verification_code(length: int = 6) -> str: - return ''.join(chr(randint(ord('A'), ord('Z'))) for _ in range(length)) - - -def _generate_verification_token() -> str: - seed = _generate_verification_code() # TODO: Use better seed - return sha256(seed.encode()).hexdigest() diff --git a/app/users/tests.py b/app/users/tests.py index 557b412..7dfb6a8 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -80,66 +80,6 @@ def test_로그아웃_성공(self): self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) -class UsernameCheckTest(TestCase): - fixtures = ['user.sample.json'] - - def test_사용_가능한_사용자명(self): - res = self.client.get( - "/api/v1/auth/username/check", - { - "username": "unique", - } - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertDictEqual(res.json(), { - "username": "unique", - "is_usable": True, - }) - - def test_사용_불가능한_사용자명(self): - res = self.client.get( - "/api/v1/auth/username/check", - { - "username": "test", - } - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertDictEqual(res.json(), { - "username": "test", - "is_usable": False, - }) - - -class EmailCheckTest(TestCase): - fixtures = ['user.sample.json'] - - def test_사용_가능한_이메일(self): - res = self.client.get( - "/api/v1/auth/email/check", - { - "email": "unique@notexample.com", - } - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertDictEqual(res.json(), { - "email": "unique@notexample.com", - "is_usable": True, - }) - - def test_사용_불가능한_이메일(self): - res = self.client.get( - "/api/v1/auth/email/check", - { - "email": "test@example.com", - } - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertDictEqual(res.json(), { - "email": "test@example.com", - "is_usable": False, - }) - - class UsabilityAPITest(TestCase): fixtures = ['user.sample.json'] diff --git a/app/users/urls.py b/app/users/urls.py index bc02e10..f0e6730 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -9,9 +9,6 @@ path("/signin", users.views.SignInAPIView.as_view()), path("/signup", users.views.SignUpAPIView.as_view()), path("/signout", users.views.SignOutAPIView.as_view()), - path("/username/check", users.views.UsernameCheckAPIView.as_view()), - path("/email/check", users.views.EmailCheckAPIView.as_view()), - path("/email/verify", users.views.EmailVerifyAPIView.as_view()), path("/usability", users.views.UsabilityAPIView.as_view()), path("/verification", users.views.EmailVerificationAPIView.as_view()), ])), diff --git a/app/users/views.py b/app/users/views.py index fa3c025..0026456 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -6,55 +6,20 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import generics from rest_framework import status -from rest_framework import throttling from rest_framework.exceptions import AuthenticationFailed from rest_framework.permissions import AllowAny from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response -from users import models from users.models import User from users.models import UserEmailVerification from users.serializers import EmailVerificationSerializer -from users.serializers import IsEmailUsableSerializer -from users.serializers import IsUsernameUsableSerializer from users.serializers import UsabilitySerializerForQueryParameter from users.serializers import UsabilitySerializer +from users.serializers import UserUpdateSerializer from users.serializers import SignInSerializer from users.serializers import SignUpSerializer -from users import serializers -from users import services - - -# TODO: Deprecate -class EmailCheckAPIView(generics.GenericAPIView): - """이메일이 사용가능한지 검사 API""" - - authentication_classes = [] - permission_classes = [AllowAny] - serializer_class = IsEmailUsableSerializer - - @swagger_auto_schema(query_serializer=IsEmailUsableSerializer) - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - return Response(serializer.data) - - -# TODO: Deprecate -class UsernameCheckAPIView(generics.GenericAPIView): - """이메일이 사용가능한지 검사 API""" - - authentication_classes = [] - permission_classes = [AllowAny] - serializer_class = IsUsernameUsableSerializer - - @swagger_auto_schema(query_serializer=IsUsernameUsableSerializer) - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - return Response(serializer.data) class UsabilityAPIView(generics.RetrieveAPIView): @@ -130,64 +95,10 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -# TODO: deprecate -class EmailVerifyThrottle(throttling.AnonRateThrottle): - THROTTLE_RATES = '1/min' - - -# TODO: deprecate -class EmailVerifyAPIView(generics.GenericAPIView): - """이메일 인증 코드 전송 API""" - authentication_classes = [] - throttle_classes = [] - permission_classes = [AllowAny] - - def get_serializer_class(self): - if self.request.method == 'GET': - return serializers.EmailSerializer - if self.request.method == 'POST': - return serializers.EmailCodeSerializer - raise ValueError - - @swagger_auto_schema( - operation_description="이메일을 입력하면, 입력된 이메일 주소로 인증 코드를 발송합니다. 해당 코드를 동일한 주소의 POST 요청으로 전달하면 회원가입시 사용할 수 있는 인증 토큰을 반환합니다.", - query_serializer=serializers.EmailSerializer, - responses={ - status.HTTP_201_CREATED: '이메일 인증 코드 생성 성공 및 발신 완료', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 이미 동일한 이메일로 가입된 계정이 존재.', - }, - ) - def get(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.query_params) - serializer.is_valid(raise_exception=True) - email = serializer.validated_data['email'] - services.send_verification_code(email) - return Response(data=serializer.data, status=status.HTTP_201_CREATED) - - @swagger_auto_schema( - responses={ - status.HTTP_200_OK: '이메일 인증 성공. 새로운 이메일 인증 토큰을 발급받는다. 이 때 받은 토큰은 회원 가입시 이메일 소유 증명을 위해 제출해야한다.', - status.HTTP_400_BAD_REQUEST: '잘못된 이메일 혹은 올바르지 않은 인증번호.', - }, - ) - def post(self, request: Request, *args, **kwargs): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - email = serializer.validated_data['email'] - code = serializer.validated_data['code'] - return Response( - data={ - 'email': email, - 'token': services.get_verification_token(email, code), - }, - status=status.HTTP_200_OK, - ) - - class CurrentUserRetrieveUpdateAPIView(generics.RetrieveUpdateAPIView): """현재 로그인한 사용자 정보를 조회/수정하는 API""" permission_classes = [IsAuthenticated] - serializer_class = serializers.UserUpdateSerializer + serializer_class = UserUpdateSerializer - def get_object(self) -> models.User: + def get_object(self) -> User: return self.request.user From 220ced942caef93c0aee11150f4a21c134be1a22 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:05:53 +0900 Subject: [PATCH 513/552] =?UTF-8?q?test(users):=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/tests.py b/app/users/tests.py index 7dfb6a8..b7c7b41 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -147,7 +147,7 @@ def test_200_사용_불가능한_사용자명(self): }, "username": { "value": "test", - "is_usable": True, + "is_usable": False, }, }) From 599fed6a27e3001c82fb255968488c642df3fd44 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:06:46 +0900 Subject: [PATCH 514/552] =?UTF-8?q?test(users):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EB=AA=85=20=EC=95=9E?= =?UTF-8?q?=EC=97=90=20=EC=83=81=ED=83=9C=EC=BD=94=EB=93=9C=EB=A5=BC=20?= =?UTF-8?q?=EB=B6=99=EC=97=AC=EC=A4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index b7c7b41..db0a7f3 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -11,7 +11,7 @@ class SignInTest(TestCase): def setUp(self) -> None: self.client.logout() - def test_로그인성공(self): + def test_200_로그인성공(self): res = self.client.post( "/api/v1/auth/signin", { @@ -21,7 +21,7 @@ def test_로그인성공(self): ) self.assertEqual(res.status_code, status.HTTP_200_OK) - def test_비밀번호_불일치(self): + def test_403_비밀번호_불일치(self): res = self.client.post( "/api/v1/auth/signin", { @@ -41,7 +41,7 @@ def setUpTestData(cls) -> None: UserEmailVerification.field_name.VERIFICATION_TOKEN: 'sample_token', }) - def test_회원가입_성공(self): + def test_201_회원가입_성공(self): res = self.client.post( "/api/v1/auth/signup", { @@ -54,7 +54,7 @@ def test_회원가입_성공(self): ) self.assertEqual(res.status_code, status.HTTP_201_CREATED) - def test_인증토큰_불일치(self): + def test_400_인증토큰_불일치(self): res = self.client.post( "/api/v1/auth/signup", { @@ -75,7 +75,7 @@ def setUp(self) -> None: self.user = User.objects.get(pk=1) self.client.force_login(self.user) - def test_로그아웃_성공(self): + def test_204_로그아웃_성공(self): res = self.client.get("/api/v1/auth/signout") self.assertEqual(res.status_code, status.HTTP_204_NO_CONTENT) From 9d64289630c3cbb37402fcc14eef4d5ccd16018f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:08:42 +0900 Subject: [PATCH 515/552] refactor(users.views): rename `CurrentUserRetrieveUpdateAPIView` -> `UserManageAPIView` --- app/users/urls.py | 2 +- app/users/views.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/users/urls.py b/app/users/urls.py index f0e6730..55f0b0e 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -13,6 +13,6 @@ path("/verification", users.views.EmailVerificationAPIView.as_view()), ])), path("user", include([ - path("/manage", users.views.CurrentUserRetrieveUpdateAPIView.as_view()), + path("/manage", users.views.UserManageAPIView.as_view()), ])), ] diff --git a/app/users/views.py b/app/users/views.py index 0026456..bf8c9cc 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -95,7 +95,7 @@ def get(self, request, *args, **kwargs): return Response(status=status.HTTP_204_NO_CONTENT) -class CurrentUserRetrieveUpdateAPIView(generics.RetrieveUpdateAPIView): +class UserManageAPIView(generics.RetrieveUpdateAPIView): """현재 로그인한 사용자 정보를 조회/수정하는 API""" permission_classes = [IsAuthenticated] serializer_class = UserUpdateSerializer From 4f2d1fd6bf915af010b929489e68645a73674c2e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:29:25 +0900 Subject: [PATCH 516/552] =?UTF-8?q?test(users):=20=EB=93=A4=EC=97=AC?= =?UTF-8?q?=EC=93=B0=EA=B8=B0=20=EC=8A=A4=ED=83=80=EC=9D=BC=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 76 ++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 46 deletions(-) diff --git a/app/users/tests.py b/app/users/tests.py index db0a7f3..95b07b2 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -12,23 +12,17 @@ def setUp(self) -> None: self.client.logout() def test_200_로그인성공(self): - res = self.client.post( - "/api/v1/auth/signin", - { - "email": "test@example.com", - "password": "passw0rd@test", - }, - ) + res = self.client.post("/api/v1/auth/signin", { + "email": "test@example.com", + "password": "passw0rd@test", + }) self.assertEqual(res.status_code, status.HTTP_200_OK) def test_403_비밀번호_불일치(self): - res = self.client.post( - "/api/v1/auth/signin", - { - "email": "test@example.com", - "password": "password@test", - } - ) + res = self.client.post("/api/v1/auth/signin", { + "email": "test@example.com", + "password": "password@test", + }) self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) @@ -42,29 +36,23 @@ def setUpTestData(cls) -> None: }) def test_201_회원가입_성공(self): - res = self.client.post( - "/api/v1/auth/signup", - { - "email": self.sample_object.email, - "username": "test", - "password": "passw0rd@test", - "boj_username": "test", - "verification_token": self.sample_object.verification_token, - } - ) + res = self.client.post("/api/v1/auth/signup", { + "email": self.sample_object.email, + "username": "test", + "password": "passw0rd@test", + "boj_username": "test", + "verification_token": self.sample_object.verification_token, + }) self.assertEqual(res.status_code, status.HTTP_201_CREATED) def test_400_인증토큰_불일치(self): - res = self.client.post( - "/api/v1/auth/signup", - { - "email": self.sample_object.email, - "username": "test", - "password": "passw0rd@test", - "boj_username": "test", - "verification_token": 'this_token_must_not_match_the_sample...', - } - ) + res = self.client.post("/api/v1/auth/signup", { + "email": self.sample_object.email, + "username": "test", + "password": "passw0rd@test", + "boj_username": "test", + "verification_token": 'this_token_must_not_match_the_sample...', + }) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) @@ -85,9 +73,8 @@ class UsabilityAPITest(TestCase): def test_200_사용_가능한_이메일(self): res = self.client.get("/api/v1/auth/usability", { - "email": "unique@notexample.com", - } - ) + "email": "unique@notexample.com", + }) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": { @@ -102,9 +89,8 @@ def test_200_사용_가능한_이메일(self): def test_200_사용_불가능한_이메일(self): res = self.client.get("/api/v1/auth/usability", { - "email": "test@example.com", - } - ) + "email": "test@example.com", + }) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": { @@ -119,9 +105,8 @@ def test_200_사용_불가능한_이메일(self): def test_200_사용_가능한_사용자명(self): res = self.client.get("/api/v1/auth/usability", { - "username": "unique", - } - ) + "username": "unique", + }) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": { @@ -136,9 +121,8 @@ def test_200_사용_가능한_사용자명(self): def test_200_사용_불가능한_사용자명(self): res = self.client.get("/api/v1/auth/usability", { - "username": "test", - } - ) + "username": "test", + }) self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertDictEqual(res.json(), { "email": { From f726df4cca575ee011e0790a17201d08c1f13bd0 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:30:08 +0900 Subject: [PATCH 517/552] =?UTF-8?q?test(users):=20UserManageAPITest=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 91 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 95b07b2..951adc7 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -138,3 +138,94 @@ def test_200_사용_불가능한_사용자명(self): def test_400_빈_데이터_전송(self): res = self.client.get("/api/v1/auth/usability") self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + +class UserManageAPITest(TestCase): + fixtures = ['user.sample.json'] + + def setUp(self) -> None: + self.client.force_login(user=User.objects.get(username='test')) + + def test_401_GET_비로그인_사용자는_접근_불가(self): + self.client.logout() + res = self.client.get("/api/v1/user/manage") + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_401_PUT_비로그인_사용자는_접근_불가(self): + self.client.logout() + res = self.client.put("/api/v1/user/manage") + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_401_PATCT_비로그인_사용자는_접근_불가(self): + self.client.logout() + res = self.client.patch("/api/v1/user/manage") + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + def test_200_GET_정보_가져오기(self): + res = self.client.get("/api/v1/user/manage") + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": "test@example.com", + "profile_image": None, + "username": "test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "level": { + "value": 0, + "name": "사용자 정보를 불러오지 못함", + }, + "rating": 0, + "updated_at": "2024-08-27T04:02:23.327000", + } + }) + + def test_400_PATCH_이메일_수정하기(self): + res = self.client.patch("/api/v1/user/manage", { + "email": "test@example.com", + }, content_type='application/json') + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_200_PATCH_사용자명_수정하기(self): + res = self.client.patch("/api/v1/user/manage", { + "username": "alt_test", + }, content_type='application/json') + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": "test@example.com", + "profile_image": None, + "username": "alt_test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "level": { + "value": 0, + "name": "사용자 정보를 불러오지 못함", + }, + "rating": 0, + "updated_at": "2024-08-27T04:02:23.327000", + } + }) + + def test_200_PATCH_비밀번호_수정하기(self): + res = self.client.patch("/api/v1/user/manage", { + "password": "passw0rd@new_password", + }, content_type='application/json') + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertDictEqual(res.json(), { + "email": "test@example.com", + "profile_image": None, + "username": "test", + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "level": { + "value": 0, + "name": "사용자 정보를 불러오지 못함", + }, + "rating": 0, + "updated_at": "2024-08-27T04:02:23.327000", + } + }) + user = User.objects.filter(username='test').get() + self.assertTrue(user.check_password("passw0rd@new_password")) From f6b75d65defb4ab36ec9e1d1f05e2f333fbd3160 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:30:27 +0900 Subject: [PATCH 518/552] =?UTF-8?q?test(users):=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EC=9A=A9=20fixture=20epdlxj=20cnrk?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/fixtures/user.sample.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/users/fixtures/user.sample.json b/app/users/fixtures/user.sample.json index b38895a..01ca63a 100644 --- a/app/users/fixtures/user.sample.json +++ b/app/users/fixtures/user.sample.json @@ -18,5 +18,15 @@ "groups": [], "user_permissions": [] } + }, + { + "model": "boj.bojuser", + "pk": 1, + "fields": { + "username": "test", + "level": 0, + "rating": 0, + "updated_at": "2024-08-27T04:02:23.327" + } } ] \ No newline at end of file From 61f1912ecf265891a5679ef9a899ac027d61c01e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:35:32 +0900 Subject: [PATCH 519/552] =?UTF-8?q?fix(users.serializers):=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=88=98=EC=A0=95=20=EC=8B=9C=EB=8F=84?= =?UTF-8?q?=EC=8B=9C=20ValidationError=EA=B0=80=20=EB=B0=9C=EC=83=9D?= =?UTF-8?q?=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=A0=95=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/serializers.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/app/users/serializers.py b/app/users/serializers.py index 046416f..9fa57d0 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -201,23 +201,31 @@ class Meta: 'boj', ] extra_kwargs = { - User.field_name.EMAIL: { - 'read_only': True, - }, + User.field_name.EMAIL: {'read_only': True}, User.field_name.PASSWORD: { 'write_only': True, 'style': {'input_type': 'password'}, }, - User.field_name.BOJ_USERNAME: { - 'write_only': True, - } + User.field_name.BOJ_USERNAME: {'write_only': True} } + def is_valid(self, *, raise_exception=False): + try: + assert User.field_name.EMAIL not in self.initial_data, ( + '이메일은 수정할 수 없습니다.' + ) + except AssertionError as exception: + if raise_exception: + raise ValidationError(exception) + else: + return (False, None) + return super().is_valid(raise_exception=raise_exception) + def save(self, **kwargs): - instance: User = super().save(**kwargs) + self.instance: User = super().save(**kwargs) if User.field_name.PASSWORD in self.validated_data: - instance.set_password(self.validated_data[User.field_name.PASSWORD]) - instance.save() + self.instance.set_password(self.validated_data[User.field_name.PASSWORD]) + self.instance.save() class UserMinimalSerializer(serializers.ModelSerializer): From d1a4cbaaace1ece9fd23803fa3fcb6b7db7e20bc Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 03:54:46 +0900 Subject: [PATCH 520/552] =?UTF-8?q?refactor(config):=20SECRET=5FKEY?= =?UTF-8?q?=EB=A5=BC=20=EB=B9=84=EA=B3=B5=EA=B0=9C=EB=A1=9C=20=EC=A0=84?= =?UTF-8?q?=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/__init__.py | 2 -- app/config/settings/test.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py index 03b0a5a..9d6fd07 100644 --- a/app/config/settings/base/__init__.py +++ b/app/config/settings/base/__init__.py @@ -23,8 +23,6 @@ # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" # SECURITY WARNING: don't run with debug turned on in production! DEBUG = False diff --git a/app/config/settings/test.py b/app/config/settings/test.py index 4666df3..bdb9499 100644 --- a/app/config/settings/test.py +++ b/app/config/settings/test.py @@ -1,6 +1,9 @@ from config.settings.base import * +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-1odep3cb9^%i11_pxm)l&i(hjk_k3+kii7o#_qbip-ubb)rlkc" + DEBUG = True ALLOWED_HOSTS = ['*'] From bdadb25900dfb563495c4584e65aa73f6fbe83df Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 04:09:20 +0900 Subject: [PATCH 521/552] refactor(notifications): remove app notifications --- app/notifications/__init__.py | 0 app/notifications/apps.py | 6 --- app/notifications/migrations/__init__.py | 0 app/notifications/services.py | 66 ------------------------ 4 files changed, 72 deletions(-) delete mode 100644 app/notifications/__init__.py delete mode 100644 app/notifications/apps.py delete mode 100644 app/notifications/migrations/__init__.py delete mode 100644 app/notifications/services.py diff --git a/app/notifications/__init__.py b/app/notifications/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/notifications/apps.py b/app/notifications/apps.py deleted file mode 100644 index 3a08476..0000000 --- a/app/notifications/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class NotificationsConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "notifications" diff --git a/app/notifications/migrations/__init__.py b/app/notifications/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/notifications/services.py b/app/notifications/services.py deleted file mode 100644 index 82690fb..0000000 --- a/app/notifications/services.py +++ /dev/null @@ -1,66 +0,0 @@ -import logging -from textwrap import dedent - -from django.core.mail import send_mail - -import crews.models -import users.models - - -SUBJECT_PREFIX = '[Time Limit Exceeded]' - -LOGGER = logging.getLogger('django.mail') - - -def notify_crew_application_requested(applicant: crews.models.CrewApplication): - assert isinstance(applicant, crews.models.CrewApplication) - send_mail( - subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청', - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 새로운 가입 신청이 왔어요! - - 지원자: {applicant.applicant.username} - 지원자의 백준 아이디(레벨): {applicant.applicant.boj_username} ({users.models.UserBojLevelChoices(applicant.applicant.boj_level).get_name(lang='ko', arabic=False)}) - - 지원자의 메시지: - ``` - {applicant.message} - ``` - - 수락하시려면 [여기]를 클릭해주세요. - """), - recipient_list=[applicant.crew.created_by.email], - from_email=None, - fail_silently=False, - ) - LOGGER.info(f'MAIL crew.application.requested {applicant.crew.created_by.email}') - - -def notify_crew_application_accepted(applicant: crews.models.CrewApplication): - assert isinstance(applicant, crews.models.CrewApplication) - send_mail( - subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 승인되었습니다', - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 가입하신 것을 축하해요! - - [여기]를 눌러 크루 대시보드로 바로가기 - """), - recipient_list=[applicant.applicant.email], - from_email=None, - fail_silently=False, - ) - LOGGER.info(f'MAIL crew.application.accepted {applicant.applicant.email}') - - -def notify_crew_application_rejected(applicant: crews.models.CrewApplication): - assert isinstance(applicant, crews.models.CrewApplication) - send_mail( - subject=f'{SUBJECT_PREFIX} 새로운 크루 가입 신청이 거절되었습니다', - message=dedent(f""" - [{applicant.crew.icon} {applicant.crew.name}]에 아쉽게도 가입하지 못했어요! - """), - recipient_list=[applicant.applicant.email], - from_email=None, - fail_silently=False, - ) - LOGGER.info(f'MAIL crew.application.rejected {applicant.applicant.email}') From 8fd78865a929f82b6d69929882fc7b089fc8f1ed Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 05:27:33 +0900 Subject: [PATCH 522/552] refactor: update .gitignore --- .gitignore | 15 ++++++--------- app/config/settings/.gitignore | 2 -- 2 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 app/config/settings/.gitignore diff --git a/.gitignore b/.gitignore index 989d7c5..5cc7083 100644 --- a/.gitignore +++ b/.gitignore @@ -56,11 +56,16 @@ cover/ *.pot # Django stuff: -*.log.* *.log local_settings.py db.sqlite3 db.sqlite3-journal +app/logs/* +app/config/settings/*.py +!app/config/settings/__init__.py +!app/config/settings/test.py +.static/ +.media/ # Flask stuff: instance/ @@ -165,11 +170,3 @@ cython_debug/ # VS Code .vscode/ *.code-workspace - -# Django -**/migrations/* -!**/migrations/__init__.py -.static/ -.media/ -sync_db -runserver diff --git a/app/config/settings/.gitignore b/app/config/settings/.gitignore deleted file mode 100644 index 49379ca..0000000 --- a/app/config/settings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -local.py -production.py From 903e14287a2d7b174a17bc4e360ed2cc003e1c95 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 05:28:09 +0900 Subject: [PATCH 523/552] =?UTF-8?q?ci:=20=EB=B0=B0=ED=8F=AC=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EC=9A=A9=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/wsgi.py | 2 +- requirements.txt | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/config/wsgi.py b/app/config/wsgi.py index a9afbb3..91f6d66 100644 --- a/app/config/wsgi.py +++ b/app/config/wsgi.py @@ -11,6 +11,6 @@ from django.core.wsgi import get_wsgi_application -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") application = get_wsgi_application() diff --git a/requirements.txt b/requirements.txt index 68bb70e..de5c0b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,15 @@ +wheel +uwsgi # might require "apt install python3-dev" django django-background-tasks django-cors-headers djangorestframework djangorestframework-simplejwt -Pillow drf-yasg -# MODEL DEPENDENCY +Pillow +psycopg2 # PostgreSQL. May require "apt install libpq-dev", "apt install postgresql" + +# LLMs google-generativeai sympy antlr4-python3-runtime==4.11 # sympy dependency for latex parsing From 99e50ad1d81aa873903a61fdf64986bb4ac3200e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 07:36:13 +0900 Subject: [PATCH 524/552] chore(db): make initial migrations --- app/boj/migrations/0001_initial.py | 195 + .../activities/migrations/0001_initial.py | 117 + .../activities/migrations/0002_initial.py | 69 + .../applications/migrations/0001_initial.py | 53 + .../applications/migrations/0002_initial.py | 57 + app/crews/migrations/0001_initial.py | 11273 ++++++++++++++++ .../analyses/migrations/0001_initial.py | 157 + .../analyses/migrations/0002_initial.py | 26 + app/problems/migrations/0001_initial.py | 78 + app/users/migrations/0001_initial.py | 120 + 10 files changed, 12145 insertions(+) create mode 100644 app/boj/migrations/0001_initial.py create mode 100644 app/crews/activities/migrations/0001_initial.py create mode 100644 app/crews/activities/migrations/0002_initial.py create mode 100644 app/crews/applications/migrations/0001_initial.py create mode 100644 app/crews/applications/migrations/0002_initial.py create mode 100644 app/crews/migrations/0001_initial.py create mode 100644 app/problems/analyses/migrations/0001_initial.py create mode 100644 app/problems/analyses/migrations/0002_initial.py create mode 100644 app/problems/migrations/0001_initial.py create mode 100644 app/users/migrations/0001_initial.py diff --git a/app/boj/migrations/0001_initial.py b/app/boj/migrations/0001_initial.py new file mode 100644 index 0000000..ee8fbd9 --- /dev/null +++ b/app/boj/migrations/0001_initial.py @@ -0,0 +1,195 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="BOJProblem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.TextField()), + ("description", models.TextField()), + ("input_description", models.TextField()), + ("output_description", models.TextField()), + ("memory_limit", models.FloatField()), + ("time_limit", models.FloatField()), + ("tags", models.JSONField(default=list)), + ( + "level", + models.IntegerField( + choices=[ + (0, "Unrated"), + (1, "브론즈 5"), + (2, "브론즈 4"), + (3, "브론즈 3"), + (4, "브론즈 2"), + (5, "브론즈 1"), + (6, "실버 5"), + (7, "실버 4"), + (8, "실버 3"), + (9, "실버 2"), + (10, "실버 1"), + (11, "골드 5"), + (12, "골드 4"), + (13, "골드 3"), + (14, "골드 2"), + (15, "골드 1"), + (16, "플래티넘 5"), + (17, "플래티넘 4"), + (18, "플래티넘 3"), + (19, "플래티넘 2"), + (20, "플래티넘 1"), + (21, "다이아몬드 5"), + (22, "다이아몬드 4"), + (23, "다이아몬드 3"), + (24, "다이아몬드 2"), + (25, "다이아몬드 1"), + (26, "루비 5"), + (27, "루비 4"), + (28, "루비 3"), + (29, "루비 2"), + (30, "루비 1"), + (31, "마스터"), + ] + ), + ), + ], + ), + migrations.CreateModel( + name="BOJUser", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "username", + models.TextField(help_text="백준 아이디", max_length=40, unique=True), + ), + ( + "level", + models.IntegerField( + choices=[ + (0, "Unrated"), + (1, "브론즈 5"), + (2, "브론즈 4"), + (3, "브론즈 3"), + (4, "브론즈 2"), + (5, "브론즈 1"), + (6, "실버 5"), + (7, "실버 4"), + (8, "실버 3"), + (9, "실버 2"), + (10, "실버 1"), + (11, "골드 5"), + (12, "골드 4"), + (13, "골드 3"), + (14, "골드 2"), + (15, "골드 1"), + (16, "플래티넘 5"), + (17, "플래티넘 4"), + (18, "플래티넘 3"), + (19, "플래티넘 2"), + (20, "플래티넘 1"), + (21, "다이아몬드 5"), + (22, "다이아몬드 4"), + (23, "다이아몬드 3"), + (24, "다이아몬드 2"), + (25, "다이아몬드 1"), + (26, "루비 5"), + (27, "루비 4"), + (28, "루비 3"), + (29, "루비 2"), + (30, "루비 1"), + (31, "마스터"), + ], + default=0, + ), + ), + ("rating", models.IntegerField(default=0)), + ("updated_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="BOJUserSnapshot", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "level", + models.IntegerField( + choices=[ + (0, "Unrated"), + (1, "브론즈 5"), + (2, "브론즈 4"), + (3, "브론즈 3"), + (4, "브론즈 2"), + (5, "브론즈 1"), + (6, "실버 5"), + (7, "실버 4"), + (8, "실버 3"), + (9, "실버 2"), + (10, "실버 1"), + (11, "골드 5"), + (12, "골드 4"), + (13, "골드 3"), + (14, "골드 2"), + (15, "골드 1"), + (16, "플래티넘 5"), + (17, "플래티넘 4"), + (18, "플래티넘 3"), + (19, "플래티넘 2"), + (20, "플래티넘 1"), + (21, "다이아몬드 5"), + (22, "다이아몬드 4"), + (23, "다이아몬드 3"), + (24, "다이아몬드 2"), + (25, "다이아몬드 1"), + (26, "루비 5"), + (27, "루비 4"), + (28, "루비 3"), + (29, "루비 2"), + (30, "루비 1"), + (31, "마스터"), + ] + ), + ), + ("rating", models.IntegerField()), + ("created_at", models.DateTimeField()), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="boj.bojuser" + ), + ), + ], + ), + ] diff --git a/app/crews/activities/migrations/0001_initial.py b/app/crews/activities/migrations/0001_initial.py new file mode 100644 index 0000000..d81655a --- /dev/null +++ b/app/crews/activities/migrations/0001_initial.py @@ -0,0 +1,117 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CrewActivity", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.TextField(help_text='활동 이름을 입력해주세요. (예: "1회차")')), + ("start_at", models.DateTimeField(help_text="활동 시작 일자를 입력해주세요.")), + ("end_at", models.DateTimeField(help_text="활동 종료 일자를 입력해주세요.")), + ], + options={ + "ordering": ["start_at"], + "get_latest_by": ["end_at"], + }, + ), + migrations.CreateModel( + name="CrewActivityProblem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "order", + models.IntegerField( + help_text="문제 순서를 입력해주세요.", + validators=[django.core.validators.MinValueValidator(1)], + ), + ), + ], + options={ + "ordering": ["order"], + }, + ), + migrations.CreateModel( + name="CrewActivitySubmission", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("code", models.TextField(help_text="유저의 코드를 입력해주세요.")), + ( + "language", + models.TextField( + choices=[ + ("nodejs", "Node.js"), + ("kotlin", "Kotlin"), + ("swift", "Swift"), + ("cpp", "C++"), + ("java", "Java"), + ("python", "Python"), + ("c", "C"), + ("javascript", "JavaScript"), + ("csharp", "C#"), + ("ruby", "Ruby"), + ("php", "PHP"), + ], + help_text="유저의 코드 언어를 입력해주세요.", + ), + ), + ( + "is_correct", + models.BooleanField(help_text="유저의 코드가 정답인지 여부를 입력해주세요."), + ), + ( + "is_help_needed", + models.BooleanField( + default=False, help_text="유저의 코드에 도움이 필요한지 여부를 입력해주세요." + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "problem", + models.ForeignKey( + help_text="활동 문제를 입력해주세요.", + on_delete=django.db.models.deletion.PROTECT, + to="activities.crewactivityproblem", + ), + ), + ], + options={ + "ordering": ["created_at"], + }, + ), + ] diff --git a/app/crews/activities/migrations/0002_initial.py b/app/crews/activities/migrations/0002_initial.py new file mode 100644 index 0000000..ef4438d --- /dev/null +++ b/app/crews/activities/migrations/0002_initial.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("crews", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("problems", "0001_initial"), + ("activities", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="crewactivitysubmission", + name="user", + field=models.ForeignKey( + help_text="유저를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="crewactivityproblem", + name="activity", + field=models.ForeignKey( + help_text="활동을 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to="activities.crewactivity", + ), + ), + migrations.AddField( + model_name="crewactivityproblem", + name="crew", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="crews.crew" + ), + ), + migrations.AddField( + model_name="crewactivityproblem", + name="problem", + field=models.ForeignKey( + help_text="문제를 입력해주세요.", + on_delete=django.db.models.deletion.PROTECT, + to="problems.problem", + ), + ), + migrations.AddField( + model_name="crewactivity", + name="crew", + field=models.ForeignKey( + help_text="크루를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to="crews.crew", + ), + ), + migrations.AddConstraint( + model_name="crewactivityproblem", + constraint=models.UniqueConstraint( + fields=("activity", "order"), name="unique_order_per_activity_problem" + ), + ), + ] diff --git a/app/crews/applications/migrations/0001_initial.py b/app/crews/applications/migrations/0001_initial.py new file mode 100644 index 0000000..b24dbfe --- /dev/null +++ b/app/crews/applications/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="CrewApplication", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "message", + models.TextField( + blank=True, help_text="가입 메시지를 입력해주세요.", null=True + ), + ), + ( + "is_pending", + models.BooleanField( + default=True, help_text="아직 수락/거절 되지 않았다면 True." + ), + ), + ( + "is_accepted", + models.BooleanField(default=False, help_text="수락 여부를 입력해주세요."), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "reviewed_at", + models.DateTimeField( + blank=True, default=None, help_text="리뷰한 시간을 입력해주세요.", null=True + ), + ), + ], + options={ + "ordering": ["reviewed_by", "created_at"], + }, + ), + ] diff --git a/app/crews/applications/migrations/0002_initial.py b/app/crews/applications/migrations/0002_initial.py new file mode 100644 index 0000000..833b8a2 --- /dev/null +++ b/app/crews/applications/migrations/0002_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("crews", "0001_initial"), + ("applications", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name="crewapplication", + name="applicant", + field=models.ForeignKey( + help_text="지원자를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddField( + model_name="crewapplication", + name="crew", + field=models.ForeignKey( + help_text="크루를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to="crews.crew", + ), + ), + migrations.AddField( + model_name="crewapplication", + name="reviewed_by", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="reviewed_applicants", + to=settings.AUTH_USER_MODEL, + ), + ), + migrations.AddConstraint( + model_name="crewapplication", + constraint=models.UniqueConstraint( + condition=models.Q(("is_pending", True)), + fields=("crew", "applicant"), + name="unique_pending_application", + ), + ), + ] diff --git a/app/crews/migrations/0001_initial.py b/app/crews/migrations/0001_initial.py new file mode 100644 index 0000000..f906015 --- /dev/null +++ b/app/crews/migrations/0001_initial.py @@ -0,0 +1,11273 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Crew", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.CharField( + help_text="크루 이름을 입력해주세요. (최대 20자)", max_length=20, unique=True + ), + ), + ( + "icon", + models.TextField( + choices=[ + ("🥇", "🥇 (:1st_place_medal:)"), + ("🥈", "🥈 (:2nd_place_medal:)"), + ("🥉", "🥉 (:3rd_place_medal:)"), + ("🆎", "🆎 (:AB_button_(blood_type):)"), + ("🏧", "🏧 (:ATM_sign:)"), + ("🅰️", "🅰️ (:A_button_(blood_type):)"), + ("🅰", "🅰 (:A_button_(blood_type):)"), + ("🇦🇫", "🇦🇫 (:Afghanistan:)"), + ("🇦🇱", "🇦🇱 (:Albania:)"), + ("🇩🇿", "🇩🇿 (:Algeria:)"), + ("🇦🇸", "🇦🇸 (:American_Samoa:)"), + ("🇦🇩", "🇦🇩 (:Andorra:)"), + ("🇦🇴", "🇦🇴 (:Angola:)"), + ("🇦🇮", "🇦🇮 (:Anguilla:)"), + ("🇦🇶", "🇦🇶 (:Antarctica:)"), + ("🇦🇬", "🇦🇬 (:Antigua_&_Barbuda:)"), + ("♒", "♒ (:Aquarius:)"), + ("🇦🇷", "🇦🇷 (:Argentina:)"), + ("♈", "♈ (:Aries:)"), + ("🇦🇲", "🇦🇲 (:Armenia:)"), + ("🇦🇼", "🇦🇼 (:Aruba:)"), + ("🇦🇨", "🇦🇨 (:Ascension_Island:)"), + ("🇦🇺", "🇦🇺 (:Australia:)"), + ("🇦🇹", "🇦🇹 (:Austria:)"), + ("🇦🇿", "🇦🇿 (:Azerbaijan:)"), + ("🔙", "🔙 (:BACK_arrow:)"), + ("🅱️", "🅱️ (:B_button_(blood_type):)"), + ("🅱", "🅱 (:B_button_(blood_type):)"), + ("🇧🇸", "🇧🇸 (:Bahamas:)"), + ("🇧🇭", "🇧🇭 (:Bahrain:)"), + ("🇧🇩", "🇧🇩 (:Bangladesh:)"), + ("🇧🇧", "🇧🇧 (:Barbados:)"), + ("🇧🇾", "🇧🇾 (:Belarus:)"), + ("🇧🇪", "🇧🇪 (:Belgium:)"), + ("🇧🇿", "🇧🇿 (:Belize:)"), + ("🇧🇯", "🇧🇯 (:Benin:)"), + ("🇧🇲", "🇧🇲 (:Bermuda:)"), + ("🇧🇹", "🇧🇹 (:Bhutan:)"), + ("🇧🇴", "🇧🇴 (:Bolivia:)"), + ("🇧🇦", "🇧🇦 (:Bosnia_&_Herzegovina:)"), + ("🇧🇼", "🇧🇼 (:Botswana:)"), + ("🇧🇻", "🇧🇻 (:Bouvet_Island:)"), + ("🇧🇷", "🇧🇷 (:Brazil:)"), + ("🇮🇴", "🇮🇴 (:British_Indian_Ocean_Territory:)"), + ("🇻🇬", "🇻🇬 (:British_Virgin_Islands:)"), + ("🇧🇳", "🇧🇳 (:Brunei:)"), + ("🇧🇬", "🇧🇬 (:Bulgaria:)"), + ("🇧🇫", "🇧🇫 (:Burkina_Faso:)"), + ("🇧🇮", "🇧🇮 (:Burundi:)"), + ("🆑", "🆑 (:CL_button:)"), + ("🆒", "🆒 (:COOL_button:)"), + ("🇰🇭", "🇰🇭 (:Cambodia:)"), + ("🇨🇲", "🇨🇲 (:Cameroon:)"), + ("🇨🇦", "🇨🇦 (:Canada:)"), + ("🇮🇨", "🇮🇨 (:Canary_Islands:)"), + ("♋", "♋ (:Cancer:)"), + ("🇨🇻", "🇨🇻 (:Cape_Verde:)"), + ("♑", "♑ (:Capricorn:)"), + ("🇧🇶", "🇧🇶 (:Caribbean_Netherlands:)"), + ("🇰🇾", "🇰🇾 (:Cayman_Islands:)"), + ("🇨🇫", "🇨🇫 (:Central_African_Republic:)"), + ("🇪🇦", "🇪🇦 (:Ceuta_&_Melilla:)"), + ("🇹🇩", "🇹🇩 (:Chad:)"), + ("🇨🇱", "🇨🇱 (:Chile:)"), + ("🇨🇳", "🇨🇳 (:China:)"), + ("🇨🇽", "🇨🇽 (:Christmas_Island:)"), + ("🎄", "🎄 (:Christmas_tree:)"), + ("🇨🇵", "🇨🇵 (:Clipperton_Island:)"), + ("🇨🇨", "🇨🇨 (:Cocos_(Keeling)_Islands:)"), + ("🇨🇴", "🇨🇴 (:Colombia:)"), + ("🇰🇲", "🇰🇲 (:Comoros:)"), + ("🇨🇬", "🇨🇬 (:Congo-Brazzaville:)"), + ("🇨🇩", "🇨🇩 (:Congo-Kinshasa:)"), + ("🇨🇰", "🇨🇰 (:Cook_Islands:)"), + ("🇨🇷", "🇨🇷 (:Costa_Rica:)"), + ("🇭🇷", "🇭🇷 (:Croatia:)"), + ("🇨🇺", "🇨🇺 (:Cuba:)"), + ("🇨🇼", "🇨🇼 (:Curaçao:)"), + ("🇨🇾", "🇨🇾 (:Cyprus:)"), + ("🇨🇿", "🇨🇿 (:Czechia:)"), + ("🇨🇮", "🇨🇮 (:Côte_d’Ivoire:)"), + ("🇩🇰", "🇩🇰 (:Denmark:)"), + ("🇩🇬", "🇩🇬 (:Diego_Garcia:)"), + ("🇩🇯", "🇩🇯 (:Djibouti:)"), + ("🇩🇲", "🇩🇲 (:Dominica:)"), + ("🇩🇴", "🇩🇴 (:Dominican_Republic:)"), + ("🔚", "🔚 (:END_arrow:)"), + ("🇪🇨", "🇪🇨 (:Ecuador:)"), + ("🇪🇬", "🇪🇬 (:Egypt:)"), + ("🇸🇻", "🇸🇻 (:El_Salvador:)"), + ( + "🏴\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f", + "🏴\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f (:England:)", + ), + ("🇬🇶", "🇬🇶 (:Equatorial_Guinea:)"), + ("🇪🇷", "🇪🇷 (:Eritrea:)"), + ("🇪🇪", "🇪🇪 (:Estonia:)"), + ("🇸🇿", "🇸🇿 (:Eswatini:)"), + ("🇪🇹", "🇪🇹 (:Ethiopia:)"), + ("🇪🇺", "🇪🇺 (:European_Union:)"), + ("🆓", "🆓 (:FREE_button:)"), + ("🇫🇰", "🇫🇰 (:Falkland_Islands:)"), + ("🇫🇴", "🇫🇴 (:Faroe_Islands:)"), + ("🇫🇯", "🇫🇯 (:Fiji:)"), + ("🇫🇮", "🇫🇮 (:Finland:)"), + ("🇫🇷", "🇫🇷 (:France:)"), + ("🇬🇫", "🇬🇫 (:French_Guiana:)"), + ("🇵🇫", "🇵🇫 (:French_Polynesia:)"), + ("🇹🇫", "🇹🇫 (:French_Southern_Territories:)"), + ("🇬🇦", "🇬🇦 (:Gabon:)"), + ("🇬🇲", "🇬🇲 (:Gambia:)"), + ("♊", "♊ (:Gemini:)"), + ("🇬🇪", "🇬🇪 (:Georgia:)"), + ("🇩🇪", "🇩🇪 (:Germany:)"), + ("🇬🇭", "🇬🇭 (:Ghana:)"), + ("🇬🇮", "🇬🇮 (:Gibraltar:)"), + ("🇬🇷", "🇬🇷 (:Greece:)"), + ("🇬🇱", "🇬🇱 (:Greenland:)"), + ("🇬🇩", "🇬🇩 (:Grenada:)"), + ("🇬🇵", "🇬🇵 (:Guadeloupe:)"), + ("🇬🇺", "🇬🇺 (:Guam:)"), + ("🇬🇹", "🇬🇹 (:Guatemala:)"), + ("🇬🇬", "🇬🇬 (:Guernsey:)"), + ("🇬🇳", "🇬🇳 (:Guinea:)"), + ("🇬🇼", "🇬🇼 (:Guinea-Bissau:)"), + ("🇬🇾", "🇬🇾 (:Guyana:)"), + ("🇭🇹", "🇭🇹 (:Haiti:)"), + ("🇭🇲", "🇭🇲 (:Heard_&_McDonald_Islands:)"), + ("🇭🇳", "🇭🇳 (:Honduras:)"), + ("🇭🇰", "🇭🇰 (:Hong_Kong_SAR_China:)"), + ("🇭🇺", "🇭🇺 (:Hungary:)"), + ("🆔", "🆔 (:ID_button:)"), + ("🇮🇸", "🇮🇸 (:Iceland:)"), + ("🇮🇳", "🇮🇳 (:India:)"), + ("🇮🇩", "🇮🇩 (:Indonesia:)"), + ("🇮🇷", "🇮🇷 (:Iran:)"), + ("🇮🇶", "🇮🇶 (:Iraq:)"), + ("🇮🇪", "🇮🇪 (:Ireland:)"), + ("🇮🇲", "🇮🇲 (:Isle_of_Man:)"), + ("🇮🇱", "🇮🇱 (:Israel:)"), + ("🇮🇹", "🇮🇹 (:Italy:)"), + ("🇯🇲", "🇯🇲 (:Jamaica:)"), + ("🇯🇵", "🇯🇵 (:Japan:)"), + ("🉑", "🉑 (:Japanese_acceptable_button:)"), + ("🈸", "🈸 (:Japanese_application_button:)"), + ("🉐", "🉐 (:Japanese_bargain_button:)"), + ("🏯", "🏯 (:Japanese_castle:)"), + ("㊗️", "㊗️ (:Japanese_congratulations_button:)"), + ("㊗", "㊗ (:Japanese_congratulations_button:)"), + ("🈹", "🈹 (:Japanese_discount_button:)"), + ("🎎", "🎎 (:Japanese_dolls:)"), + ("🈚", "🈚 (:Japanese_free_of_charge_button:)"), + ("🈁", "🈁 (:Japanese_here_button:)"), + ("🈷️", "🈷️ (:Japanese_monthly_amount_button:)"), + ("🈷", "🈷 (:Japanese_monthly_amount_button:)"), + ("🈵", "🈵 (:Japanese_no_vacancy_button:)"), + ("🈶", "🈶 (:Japanese_not_free_of_charge_button:)"), + ("🈺", "🈺 (:Japanese_open_for_business_button:)"), + ("🈴", "🈴 (:Japanese_passing_grade_button:)"), + ("🏣", "🏣 (:Japanese_post_office:)"), + ("🈲", "🈲 (:Japanese_prohibited_button:)"), + ("🈯", "🈯 (:Japanese_reserved_button:)"), + ("㊙️", "㊙️ (:Japanese_secret_button:)"), + ("㊙", "㊙ (:Japanese_secret_button:)"), + ("🈂️", "🈂️ (:Japanese_service_charge_button:)"), + ("🈂", "🈂 (:Japanese_service_charge_button:)"), + ("🔰", "🔰 (:Japanese_symbol_for_beginner:)"), + ("🈳", "🈳 (:Japanese_vacancy_button:)"), + ("🇯🇪", "🇯🇪 (:Jersey:)"), + ("🇯🇴", "🇯🇴 (:Jordan:)"), + ("🇰🇿", "🇰🇿 (:Kazakhstan:)"), + ("🇰🇪", "🇰🇪 (:Kenya:)"), + ("🇰🇮", "🇰🇮 (:Kiribati:)"), + ("🇽🇰", "🇽🇰 (:Kosovo:)"), + ("🇰🇼", "🇰🇼 (:Kuwait:)"), + ("🇰🇬", "🇰🇬 (:Kyrgyzstan:)"), + ("🇱🇦", "🇱🇦 (:Laos:)"), + ("🇱🇻", "🇱🇻 (:Latvia:)"), + ("🇱🇧", "🇱🇧 (:Lebanon:)"), + ("♌", "♌ (:Leo:)"), + ("🇱🇸", "🇱🇸 (:Lesotho:)"), + ("🇱🇷", "🇱🇷 (:Liberia:)"), + ("♎", "♎ (:Libra:)"), + ("🇱🇾", "🇱🇾 (:Libya:)"), + ("🇱🇮", "🇱🇮 (:Liechtenstein:)"), + ("🇱🇹", "🇱🇹 (:Lithuania:)"), + ("🇱🇺", "🇱🇺 (:Luxembourg:)"), + ("🇲🇴", "🇲🇴 (:Macao_SAR_China:)"), + ("🇲🇬", "🇲🇬 (:Madagascar:)"), + ("🇲🇼", "🇲🇼 (:Malawi:)"), + ("🇲🇾", "🇲🇾 (:Malaysia:)"), + ("🇲🇻", "🇲🇻 (:Maldives:)"), + ("🇲🇱", "🇲🇱 (:Mali:)"), + ("🇲🇹", "🇲🇹 (:Malta:)"), + ("🇲🇭", "🇲🇭 (:Marshall_Islands:)"), + ("🇲🇶", "🇲🇶 (:Martinique:)"), + ("🇲🇷", "🇲🇷 (:Mauritania:)"), + ("🇲🇺", "🇲🇺 (:Mauritius:)"), + ("🇾🇹", "🇾🇹 (:Mayotte:)"), + ("🇲🇽", "🇲🇽 (:Mexico:)"), + ("🇫🇲", "🇫🇲 (:Micronesia:)"), + ("🇲🇩", "🇲🇩 (:Moldova:)"), + ("🇲🇨", "🇲🇨 (:Monaco:)"), + ("🇲🇳", "🇲🇳 (:Mongolia:)"), + ("🇲🇪", "🇲🇪 (:Montenegro:)"), + ("🇲🇸", "🇲🇸 (:Montserrat:)"), + ("🇲🇦", "🇲🇦 (:Morocco:)"), + ("🇲🇿", "🇲🇿 (:Mozambique:)"), + ("🤶", "🤶 (:Mrs._Claus:)"), + ("🤶🏿", "🤶🏿 (:Mrs._Claus_dark_skin_tone:)"), + ("🤶🏻", "🤶🏻 (:Mrs._Claus_light_skin_tone:)"), + ("🤶🏾", "🤶🏾 (:Mrs._Claus_medium-dark_skin_tone:)"), + ("🤶🏼", "🤶🏼 (:Mrs._Claus_medium-light_skin_tone:)"), + ("🤶🏽", "🤶🏽 (:Mrs._Claus_medium_skin_tone:)"), + ("🇲🇲", "🇲🇲 (:Myanmar_(Burma):)"), + ("🆕", "🆕 (:NEW_button:)"), + ("🆖", "🆖 (:NG_button:)"), + ("🇳🇦", "🇳🇦 (:Namibia:)"), + ("🇳🇷", "🇳🇷 (:Nauru:)"), + ("🇳🇵", "🇳🇵 (:Nepal:)"), + ("🇳🇱", "🇳🇱 (:Netherlands:)"), + ("🇳🇨", "🇳🇨 (:New_Caledonia:)"), + ("🇳🇿", "🇳🇿 (:New_Zealand:)"), + ("🇳🇮", "🇳🇮 (:Nicaragua:)"), + ("🇳🇪", "🇳🇪 (:Niger:)"), + ("🇳🇬", "🇳🇬 (:Nigeria:)"), + ("🇳🇺", "🇳🇺 (:Niue:)"), + ("🇳🇫", "🇳🇫 (:Norfolk_Island:)"), + ("🇰🇵", "🇰🇵 (:North_Korea:)"), + ("🇲🇰", "🇲🇰 (:North_Macedonia:)"), + ("🇲🇵", "🇲🇵 (:Northern_Mariana_Islands:)"), + ("🇳🇴", "🇳🇴 (:Norway:)"), + ("🆗", "🆗 (:OK_button:)"), + ("👌", "👌 (:OK_hand:)"), + ("👌🏿", "👌🏿 (:OK_hand_dark_skin_tone:)"), + ("👌🏻", "👌🏻 (:OK_hand_light_skin_tone:)"), + ("👌🏾", "👌🏾 (:OK_hand_medium-dark_skin_tone:)"), + ("👌🏼", "👌🏼 (:OK_hand_medium-light_skin_tone:)"), + ("👌🏽", "👌🏽 (:OK_hand_medium_skin_tone:)"), + ("🔛", "🔛 (:ON!_arrow:)"), + ("🅾️", "🅾️ (:O_button_(blood_type):)"), + ("🅾", "🅾 (:O_button_(blood_type):)"), + ("🇴🇲", "🇴🇲 (:Oman:)"), + ("⛎", "⛎ (:Ophiuchus:)"), + ("🅿️", "🅿️ (:P_button:)"), + ("🅿", "🅿 (:P_button:)"), + ("🇵🇰", "🇵🇰 (:Pakistan:)"), + ("🇵🇼", "🇵🇼 (:Palau:)"), + ("🇵🇸", "🇵🇸 (:Palestinian_Territories:)"), + ("🇵🇦", "🇵🇦 (:Panama:)"), + ("🇵🇬", "🇵🇬 (:Papua_New_Guinea:)"), + ("🇵🇾", "🇵🇾 (:Paraguay:)"), + ("🇵🇪", "🇵🇪 (:Peru:)"), + ("🇵🇭", "🇵🇭 (:Philippines:)"), + ("♓", "♓ (:Pisces:)"), + ("🇵🇳", "🇵🇳 (:Pitcairn_Islands:)"), + ("🇵🇱", "🇵🇱 (:Poland:)"), + ("🇵🇹", "🇵🇹 (:Portugal:)"), + ("🇵🇷", "🇵🇷 (:Puerto_Rico:)"), + ("🇶🇦", "🇶🇦 (:Qatar:)"), + ("🇷🇴", "🇷🇴 (:Romania:)"), + ("🇷🇺", "🇷🇺 (:Russia:)"), + ("🇷🇼", "🇷🇼 (:Rwanda:)"), + ("🇷🇪", "🇷🇪 (:Réunion:)"), + ("🔜", "🔜 (:SOON_arrow:)"), + ("🆘", "🆘 (:SOS_button:)"), + ("♐", "♐ (:Sagittarius:)"), + ("🇼🇸", "🇼🇸 (:Samoa:)"), + ("🇸🇲", "🇸🇲 (:San_Marino:)"), + ("🎅", "🎅 (:Santa_Claus:)"), + ("🎅🏿", "🎅🏿 (:Santa_Claus_dark_skin_tone:)"), + ("🎅🏻", "🎅🏻 (:Santa_Claus_light_skin_tone:)"), + ("🎅🏾", "🎅🏾 (:Santa_Claus_medium-dark_skin_tone:)"), + ("🎅🏼", "🎅🏼 (:Santa_Claus_medium-light_skin_tone:)"), + ("🎅🏽", "🎅🏽 (:Santa_Claus_medium_skin_tone:)"), + ("🇸🇦", "🇸🇦 (:Saudi_Arabia:)"), + ("♏", "♏ (:Scorpio:)"), + ( + "🏴\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f", + "🏴\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f (:Scotland:)", + ), + ("🇸🇳", "🇸🇳 (:Senegal:)"), + ("🇷🇸", "🇷🇸 (:Serbia:)"), + ("🇸🇨", "🇸🇨 (:Seychelles:)"), + ("🇸🇱", "🇸🇱 (:Sierra_Leone:)"), + ("🇸🇬", "🇸🇬 (:Singapore:)"), + ("🇸🇽", "🇸🇽 (:Sint_Maarten:)"), + ("🇸🇰", "🇸🇰 (:Slovakia:)"), + ("🇸🇮", "🇸🇮 (:Slovenia:)"), + ("🇸🇧", "🇸🇧 (:Solomon_Islands:)"), + ("🇸🇴", "🇸🇴 (:Somalia:)"), + ("🇿🇦", "🇿🇦 (:South_Africa:)"), + ("🇬🇸", "🇬🇸 (:South_Georgia_&_South_Sandwich_Islands:)"), + ("🇰🇷", "🇰🇷 (:South_Korea:)"), + ("🇸🇸", "🇸🇸 (:South_Sudan:)"), + ("🇪🇸", "🇪🇸 (:Spain:)"), + ("🇱🇰", "🇱🇰 (:Sri_Lanka:)"), + ("🇧🇱", "🇧🇱 (:St._Barthélemy:)"), + ("🇸🇭", "🇸🇭 (:St._Helena:)"), + ("🇰🇳", "🇰🇳 (:St._Kitts_&_Nevis:)"), + ("🇱🇨", "🇱🇨 (:St._Lucia:)"), + ("🇲🇫", "🇲🇫 (:St._Martin:)"), + ("🇵🇲", "🇵🇲 (:St._Pierre_&_Miquelon:)"), + ("🇻🇨", "🇻🇨 (:St._Vincent_&_Grenadines:)"), + ("🗽", "🗽 (:Statue_of_Liberty:)"), + ("🇸🇩", "🇸🇩 (:Sudan:)"), + ("🇸🇷", "🇸🇷 (:Suriname:)"), + ("🇸🇯", "🇸🇯 (:Svalbard_&_Jan_Mayen:)"), + ("🇸🇪", "🇸🇪 (:Sweden:)"), + ("🇨🇭", "🇨🇭 (:Switzerland:)"), + ("🇸🇾", "🇸🇾 (:Syria:)"), + ("🇸🇹", "🇸🇹 (:São_Tomé_&_Príncipe:)"), + ("🦖", "🦖 (:T-Rex:)"), + ("🔝", "🔝 (:TOP_arrow:)"), + ("🇹🇼", "🇹🇼 (:Taiwan:)"), + ("🇹🇯", "🇹🇯 (:Tajikistan:)"), + ("🇹🇿", "🇹🇿 (:Tanzania:)"), + ("♉", "♉ (:Taurus:)"), + ("🇹🇭", "🇹🇭 (:Thailand:)"), + ("🇹🇱", "🇹🇱 (:Timor-Leste:)"), + ("🇹🇬", "🇹🇬 (:Togo:)"), + ("🇹🇰", "🇹🇰 (:Tokelau:)"), + ("🗼", "🗼 (:Tokyo_tower:)"), + ("🇹🇴", "🇹🇴 (:Tonga:)"), + ("🇹🇹", "🇹🇹 (:Trinidad_&_Tobago:)"), + ("🇹🇦", "🇹🇦 (:Tristan_da_Cunha:)"), + ("🇹🇳", "🇹🇳 (:Tunisia:)"), + ("🇹🇲", "🇹🇲 (:Turkmenistan:)"), + ("🇹🇨", "🇹🇨 (:Turks_&_Caicos_Islands:)"), + ("🇹🇻", "🇹🇻 (:Tuvalu:)"), + ("🇹🇷", "🇹🇷 (:Türkiye:)"), + ("🇺🇲", "🇺🇲 (:U.S._Outlying_Islands:)"), + ("🇻🇮", "🇻🇮 (:U.S._Virgin_Islands:)"), + ("🆙", "🆙 (:UP!_button:)"), + ("🇺🇬", "🇺🇬 (:Uganda:)"), + ("🇺🇦", "🇺🇦 (:Ukraine:)"), + ("🇦🇪", "🇦🇪 (:United_Arab_Emirates:)"), + ("🇬🇧", "🇬🇧 (:United_Kingdom:)"), + ("🇺🇳", "🇺🇳 (:United_Nations:)"), + ("🇺🇸", "🇺🇸 (:United_States:)"), + ("🇺🇾", "🇺🇾 (:Uruguay:)"), + ("🇺🇿", "🇺🇿 (:Uzbekistan:)"), + ("🆚", "🆚 (:VS_button:)"), + ("🇻🇺", "🇻🇺 (:Vanuatu:)"), + ("🇻🇦", "🇻🇦 (:Vatican_City:)"), + ("🇻🇪", "🇻🇪 (:Venezuela:)"), + ("🇻🇳", "🇻🇳 (:Vietnam:)"), + ("♍", "♍ (:Virgo:)"), + ( + "🏴\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f", + "🏴\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f (:Wales:)", + ), + ("🇼🇫", "🇼🇫 (:Wallis_&_Futuna:)"), + ("🇪🇭", "🇪🇭 (:Western_Sahara:)"), + ("🇾🇪", "🇾🇪 (:Yemen:)"), + ("💤", "💤 (:ZZZ:)"), + ("🇿🇲", "🇿🇲 (:Zambia:)"), + ("🇿🇼", "🇿🇼 (:Zimbabwe:)"), + ("🧮", "🧮 (:abacus:)"), + ("🪗", "🪗 (:accordion:)"), + ("🩹", "🩹 (:adhesive_bandage:)"), + ("🎟️", "🎟️ (:admission_tickets:)"), + ("🎟", "🎟 (:admission_tickets:)"), + ("🚡", "🚡 (:aerial_tramway:)"), + ("✈️", "✈️ (:airplane:)"), + ("✈", "✈ (:airplane:)"), + ("🛬", "🛬 (:airplane_arrival:)"), + ("🛫", "🛫 (:airplane_departure:)"), + ("⏰", "⏰ (:alarm_clock:)"), + ("⚗️", "⚗️ (:alembic:)"), + ("⚗", "⚗ (:alembic:)"), + ("👽", "👽 (:alien:)"), + ("👾", "👾 (:alien_monster:)"), + ("🚑", "🚑 (:ambulance:)"), + ("🏈", "🏈 (:american_football:)"), + ("🏺", "🏺 (:amphora:)"), + ("🫀", "🫀 (:anatomical_heart:)"), + ("⚓", "⚓ (:anchor:)"), + ("💢", "💢 (:anger_symbol:)"), + ("😠", "😠 (:angry_face:)"), + ("👿", "👿 (:angry_face_with_horns:)"), + ("😧", "😧 (:anguished_face:)"), + ("🐜", "🐜 (:ant:)"), + ("📶", "📶 (:antenna_bars:)"), + ("😰", "😰 (:anxious_face_with_sweat:)"), + ("🚛", "🚛 (:articulated_lorry:)"), + ("🧑\u200d🎨", "🧑\u200d🎨 (:artist:)"), + ("🧑🏿\u200d🎨", "🧑🏿\u200d🎨 (:artist_dark_skin_tone:)"), + ("🧑🏻\u200d🎨", "🧑🏻\u200d🎨 (:artist_light_skin_tone:)"), + ("🧑🏾\u200d🎨", "🧑🏾\u200d🎨 (:artist_medium-dark_skin_tone:)"), + ( + "🧑🏼\u200d🎨", + "🧑🏼\u200d🎨 (:artist_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🎨", "🧑🏽\u200d🎨 (:artist_medium_skin_tone:)"), + ("🎨", "🎨 (:artist_palette:)"), + ("😲", "😲 (:astonished_face:)"), + ("🧑\u200d🚀", "🧑\u200d🚀 (:astronaut:)"), + ("🧑🏿\u200d🚀", "🧑🏿\u200d🚀 (:astronaut_dark_skin_tone:)"), + ("🧑🏻\u200d🚀", "🧑🏻\u200d🚀 (:astronaut_light_skin_tone:)"), + ( + "🧑🏾\u200d🚀", + "🧑🏾\u200d🚀 (:astronaut_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🚀", + "🧑🏼\u200d🚀 (:astronaut_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🚀", "🧑🏽\u200d🚀 (:astronaut_medium_skin_tone:)"), + ("⚛️", "⚛️ (:atom_symbol:)"), + ("⚛", "⚛ (:atom_symbol:)"), + ("🛺", "🛺 (:auto_rickshaw:)"), + ("🚗", "🚗 (:automobile:)"), + ("🥑", "🥑 (:avocado:)"), + ("🪓", "🪓 (:axe:)"), + ("👶", "👶 (:baby:)"), + ("👼", "👼 (:baby_angel:)"), + ("👼🏿", "👼🏿 (:baby_angel_dark_skin_tone:)"), + ("👼🏻", "👼🏻 (:baby_angel_light_skin_tone:)"), + ("👼🏾", "👼🏾 (:baby_angel_medium-dark_skin_tone:)"), + ("👼🏼", "👼🏼 (:baby_angel_medium-light_skin_tone:)"), + ("👼🏽", "👼🏽 (:baby_angel_medium_skin_tone:)"), + ("🍼", "🍼 (:baby_bottle:)"), + ("🐤", "🐤 (:baby_chick:)"), + ("👶🏿", "👶🏿 (:baby_dark_skin_tone:)"), + ("👶🏻", "👶🏻 (:baby_light_skin_tone:)"), + ("👶🏾", "👶🏾 (:baby_medium-dark_skin_tone:)"), + ("👶🏼", "👶🏼 (:baby_medium-light_skin_tone:)"), + ("👶🏽", "👶🏽 (:baby_medium_skin_tone:)"), + ("🚼", "🚼 (:baby_symbol:)"), + ("👇", "👇 (:backhand_index_pointing_down:)"), + ( + "👇🏿", + "👇🏿 (:backhand_index_pointing_down_dark_skin_tone:)", + ), + ( + "👇🏻", + "👇🏻 (:backhand_index_pointing_down_light_skin_tone:)", + ), + ( + "👇🏾", + "👇🏾 (:backhand_index_pointing_down_medium-dark_skin_tone:)", + ), + ( + "👇🏼", + "👇🏼 (:backhand_index_pointing_down_medium-light_skin_tone:)", + ), + ( + "👇🏽", + "👇🏽 (:backhand_index_pointing_down_medium_skin_tone:)", + ), + ("👈", "👈 (:backhand_index_pointing_left:)"), + ( + "👈🏿", + "👈🏿 (:backhand_index_pointing_left_dark_skin_tone:)", + ), + ( + "👈🏻", + "👈🏻 (:backhand_index_pointing_left_light_skin_tone:)", + ), + ( + "👈🏾", + "👈🏾 (:backhand_index_pointing_left_medium-dark_skin_tone:)", + ), + ( + "👈🏼", + "👈🏼 (:backhand_index_pointing_left_medium-light_skin_tone:)", + ), + ( + "👈🏽", + "👈🏽 (:backhand_index_pointing_left_medium_skin_tone:)", + ), + ("👉", "👉 (:backhand_index_pointing_right:)"), + ( + "👉🏿", + "👉🏿 (:backhand_index_pointing_right_dark_skin_tone:)", + ), + ( + "👉🏻", + "👉🏻 (:backhand_index_pointing_right_light_skin_tone:)", + ), + ( + "👉🏾", + "👉🏾 (:backhand_index_pointing_right_medium-dark_skin_tone:)", + ), + ( + "👉🏼", + "👉🏼 (:backhand_index_pointing_right_medium-light_skin_tone:)", + ), + ( + "👉🏽", + "👉🏽 (:backhand_index_pointing_right_medium_skin_tone:)", + ), + ("👆", "👆 (:backhand_index_pointing_up:)"), + ("👆🏿", "👆🏿 (:backhand_index_pointing_up_dark_skin_tone:)"), + ("👆🏻", "👆🏻 (:backhand_index_pointing_up_light_skin_tone:)"), + ( + "👆🏾", + "👆🏾 (:backhand_index_pointing_up_medium-dark_skin_tone:)", + ), + ( + "👆🏼", + "👆🏼 (:backhand_index_pointing_up_medium-light_skin_tone:)", + ), + ( + "👆🏽", + "👆🏽 (:backhand_index_pointing_up_medium_skin_tone:)", + ), + ("🎒", "🎒 (:backpack:)"), + ("🥓", "🥓 (:bacon:)"), + ("🦡", "🦡 (:badger:)"), + ("🏸", "🏸 (:badminton:)"), + ("🥯", "🥯 (:bagel:)"), + ("🛄", "🛄 (:baggage_claim:)"), + ("🥖", "🥖 (:baguette_bread:)"), + ("⚖️", "⚖️ (:balance_scale:)"), + ("⚖", "⚖ (:balance_scale:)"), + ("🦲", "🦲 (:bald:)"), + ("🩰", "🩰 (:ballet_shoes:)"), + ("🎈", "🎈 (:balloon:)"), + ("🗳️", "🗳️ (:ballot_box_with_ballot:)"), + ("🗳", "🗳 (:ballot_box_with_ballot:)"), + ("🍌", "🍌 (:banana:)"), + ("🪕", "🪕 (:banjo:)"), + ("🏦", "🏦 (:bank:)"), + ("📊", "📊 (:bar_chart:)"), + ("💈", "💈 (:barber_pole:)"), + ("⚾", "⚾ (:baseball:)"), + ("🧺", "🧺 (:basket:)"), + ("🏀", "🏀 (:basketball:)"), + ("🦇", "🦇 (:bat:)"), + ("🛁", "🛁 (:bathtub:)"), + ("🔋", "🔋 (:battery:)"), + ("🏖️", "🏖️ (:beach_with_umbrella:)"), + ("🏖", "🏖 (:beach_with_umbrella:)"), + ("😁", "😁 (:beaming_face_with_smiling_eyes:)"), + ("\U0001fad8", "\U0001fad8 (:beans:)"), + ("🐻", "🐻 (:bear:)"), + ("💓", "💓 (:beating_heart:)"), + ("🦫", "🦫 (:beaver:)"), + ("🛏️", "🛏️ (:bed:)"), + ("🛏", "🛏 (:bed:)"), + ("🍺", "🍺 (:beer_mug:)"), + ("🪲", "🪲 (:beetle:)"), + ("🔔", "🔔 (:bell:)"), + ("🫑", "🫑 (:bell_pepper:)"), + ("🔕", "🔕 (:bell_with_slash:)"), + ("🛎️", "🛎️ (:bellhop_bell:)"), + ("🛎", "🛎 (:bellhop_bell:)"), + ("🍱", "🍱 (:bento_box:)"), + ("🧃", "🧃 (:beverage_box:)"), + ("🚲", "🚲 (:bicycle:)"), + ("👙", "👙 (:bikini:)"), + ("🧢", "🧢 (:billed_cap:)"), + ("☣️", "☣️ (:biohazard:)"), + ("☣", "☣ (:biohazard:)"), + ("🐦", "🐦 (:bird:)"), + ("🎂", "🎂 (:birthday_cake:)"), + ("🦬", "🦬 (:bison:)"), + ("\U0001fae6", "\U0001fae6 (:biting_lip:)"), + ("🐦\u200d⬛", "🐦\u200d⬛ (:black_bird:)"), + ("🐈\u200d⬛", "🐈\u200d⬛ (:black_cat:)"), + ("⚫", "⚫ (:black_circle:)"), + ("🏴", "🏴 (:black_flag:)"), + ("🖤", "🖤 (:black_heart:)"), + ("⬛", "⬛ (:black_large_square:)"), + ("◾", "◾ (:black_medium-small_square:)"), + ("◼️", "◼️ (:black_medium_square:)"), + ("◼", "◼ (:black_medium_square:)"), + ("✒️", "✒️ (:black_nib:)"), + ("✒", "✒ (:black_nib:)"), + ("▪️", "▪️ (:black_small_square:)"), + ("▪", "▪ (:black_small_square:)"), + ("🔲", "🔲 (:black_square_button:)"), + ("🌼", "🌼 (:blossom:)"), + ("🐡", "🐡 (:blowfish:)"), + ("📘", "📘 (:blue_book:)"), + ("🔵", "🔵 (:blue_circle:)"), + ("💙", "💙 (:blue_heart:)"), + ("🟦", "🟦 (:blue_square:)"), + ("🫐", "🫐 (:blueberries:)"), + ("🐗", "🐗 (:boar:)"), + ("💣", "💣 (:bomb:)"), + ("🦴", "🦴 (:bone:)"), + ("🔖", "🔖 (:bookmark:)"), + ("📑", "📑 (:bookmark_tabs:)"), + ("📚", "📚 (:books:)"), + ("🪃", "🪃 (:boomerang:)"), + ("🍾", "🍾 (:bottle_with_popping_cork:)"), + ("💐", "💐 (:bouquet:)"), + ("🏹", "🏹 (:bow_and_arrow:)"), + ("🥣", "🥣 (:bowl_with_spoon:)"), + ("🎳", "🎳 (:bowling:)"), + ("🥊", "🥊 (:boxing_glove:)"), + ("👦", "👦 (:boy:)"), + ("👦🏿", "👦🏿 (:boy_dark_skin_tone:)"), + ("👦🏻", "👦🏻 (:boy_light_skin_tone:)"), + ("👦🏾", "👦🏾 (:boy_medium-dark_skin_tone:)"), + ("👦🏼", "👦🏼 (:boy_medium-light_skin_tone:)"), + ("👦🏽", "👦🏽 (:boy_medium_skin_tone:)"), + ("🧠", "🧠 (:brain:)"), + ("🍞", "🍞 (:bread:)"), + ("🤱", "🤱 (:breast-feeding:)"), + ("🤱🏿", "🤱🏿 (:breast-feeding_dark_skin_tone:)"), + ("🤱🏻", "🤱🏻 (:breast-feeding_light_skin_tone:)"), + ("🤱🏾", "🤱🏾 (:breast-feeding_medium-dark_skin_tone:)"), + ("🤱🏼", "🤱🏼 (:breast-feeding_medium-light_skin_tone:)"), + ("🤱🏽", "🤱🏽 (:breast-feeding_medium_skin_tone:)"), + ("🧱", "🧱 (:brick:)"), + ("🌉", "🌉 (:bridge_at_night:)"), + ("💼", "💼 (:briefcase:)"), + ("🩲", "🩲 (:briefs:)"), + ("🔆", "🔆 (:bright_button:)"), + ("🥦", "🥦 (:broccoli:)"), + ("⛓️\u200d💥", "⛓️\u200d💥 (:broken_chain:)"), + ("⛓\u200d💥", "⛓\u200d💥 (:broken_chain:)"), + ("💔", "💔 (:broken_heart:)"), + ("🧹", "🧹 (:broom:)"), + ("🟤", "🟤 (:brown_circle:)"), + ("🤎", "🤎 (:brown_heart:)"), + ("🍄\u200d🟫", "🍄\u200d🟫 (:brown_mushroom:)"), + ("🟫", "🟫 (:brown_square:)"), + ("🧋", "🧋 (:bubble_tea:)"), + ("\U0001fae7", "\U0001fae7 (:bubbles:)"), + ("🪣", "🪣 (:bucket:)"), + ("🐛", "🐛 (:bug:)"), + ("🏗️", "🏗️ (:building_construction:)"), + ("🏗", "🏗 (:building_construction:)"), + ("🚅", "🚅 (:bullet_train:)"), + ("🎯", "🎯 (:bullseye:)"), + ("🌯", "🌯 (:burrito:)"), + ("🚌", "🚌 (:bus:)"), + ("🚏", "🚏 (:bus_stop:)"), + ("👤", "👤 (:bust_in_silhouette:)"), + ("👥", "👥 (:busts_in_silhouette:)"), + ("🧈", "🧈 (:butter:)"), + ("🦋", "🦋 (:butterfly:)"), + ("🌵", "🌵 (:cactus:)"), + ("📅", "📅 (:calendar:)"), + ("🤙", "🤙 (:call_me_hand:)"), + ("🤙🏿", "🤙🏿 (:call_me_hand_dark_skin_tone:)"), + ("🤙🏻", "🤙🏻 (:call_me_hand_light_skin_tone:)"), + ("🤙🏾", "🤙🏾 (:call_me_hand_medium-dark_skin_tone:)"), + ("🤙🏼", "🤙🏼 (:call_me_hand_medium-light_skin_tone:)"), + ("🤙🏽", "🤙🏽 (:call_me_hand_medium_skin_tone:)"), + ("🐪", "🐪 (:camel:)"), + ("📷", "📷 (:camera:)"), + ("📸", "📸 (:camera_with_flash:)"), + ("🏕️", "🏕️ (:camping:)"), + ("🏕", "🏕 (:camping:)"), + ("🕯️", "🕯️ (:candle:)"), + ("🕯", "🕯 (:candle:)"), + ("🍬", "🍬 (:candy:)"), + ("🥫", "🥫 (:canned_food:)"), + ("🛶", "🛶 (:canoe:)"), + ("🗃️", "🗃️ (:card_file_box:)"), + ("🗃", "🗃 (:card_file_box:)"), + ("📇", "📇 (:card_index:)"), + ("🗂️", "🗂️ (:card_index_dividers:)"), + ("🗂", "🗂 (:card_index_dividers:)"), + ("🎠", "🎠 (:carousel_horse:)"), + ("🎏", "🎏 (:carp_streamer:)"), + ("🪚", "🪚 (:carpentry_saw:)"), + ("🥕", "🥕 (:carrot:)"), + ("🏰", "🏰 (:castle:)"), + ("🐈", "🐈 (:cat:)"), + ("🐱", "🐱 (:cat_face:)"), + ("😹", "😹 (:cat_with_tears_of_joy:)"), + ("😼", "😼 (:cat_with_wry_smile:)"), + ("⛓️", "⛓️ (:chains:)"), + ("⛓", "⛓ (:chains:)"), + ("🪑", "🪑 (:chair:)"), + ("📉", "📉 (:chart_decreasing:)"), + ("📈", "📈 (:chart_increasing:)"), + ("💹", "💹 (:chart_increasing_with_yen:)"), + ("☑️", "☑️ (:check_box_with_check:)"), + ("☑", "☑ (:check_box_with_check:)"), + ("✔️", "✔️ (:check_mark:)"), + ("✔", "✔ (:check_mark:)"), + ("✅", "✅ (:check_mark_button:)"), + ("🧀", "🧀 (:cheese_wedge:)"), + ("🏁", "🏁 (:chequered_flag:)"), + ("🍒", "🍒 (:cherries:)"), + ("🌸", "🌸 (:cherry_blossom:)"), + ("♟️", "♟️ (:chess_pawn:)"), + ("♟", "♟ (:chess_pawn:)"), + ("🌰", "🌰 (:chestnut:)"), + ("🐔", "🐔 (:chicken:)"), + ("🧒", "🧒 (:child:)"), + ("🧒🏿", "🧒🏿 (:child_dark_skin_tone:)"), + ("🧒🏻", "🧒🏻 (:child_light_skin_tone:)"), + ("🧒🏾", "🧒🏾 (:child_medium-dark_skin_tone:)"), + ("🧒🏼", "🧒🏼 (:child_medium-light_skin_tone:)"), + ("🧒🏽", "🧒🏽 (:child_medium_skin_tone:)"), + ("🚸", "🚸 (:children_crossing:)"), + ("🐿️", "🐿️ (:chipmunk:)"), + ("🐿", "🐿 (:chipmunk:)"), + ("🍫", "🍫 (:chocolate_bar:)"), + ("🥢", "🥢 (:chopsticks:)"), + ("⛪", "⛪ (:church:)"), + ("🚬", "🚬 (:cigarette:)"), + ("🎦", "🎦 (:cinema:)"), + ("Ⓜ️", "Ⓜ️ (:circled_M:)"), + ("Ⓜ", "Ⓜ (:circled_M:)"), + ("🎪", "🎪 (:circus_tent:)"), + ("🏙️", "🏙️ (:cityscape:)"), + ("🏙", "🏙 (:cityscape:)"), + ("🌆", "🌆 (:cityscape_at_dusk:)"), + ("🗜️", "🗜️ (:clamp:)"), + ("🗜", "🗜 (:clamp:)"), + ("🎬", "🎬 (:clapper_board:)"), + ("👏", "👏 (:clapping_hands:)"), + ("👏🏿", "👏🏿 (:clapping_hands_dark_skin_tone:)"), + ("👏🏻", "👏🏻 (:clapping_hands_light_skin_tone:)"), + ("👏🏾", "👏🏾 (:clapping_hands_medium-dark_skin_tone:)"), + ("👏🏼", "👏🏼 (:clapping_hands_medium-light_skin_tone:)"), + ("👏🏽", "👏🏽 (:clapping_hands_medium_skin_tone:)"), + ("🏛️", "🏛️ (:classical_building:)"), + ("🏛", "🏛 (:classical_building:)"), + ("🍻", "🍻 (:clinking_beer_mugs:)"), + ("🥂", "🥂 (:clinking_glasses:)"), + ("📋", "📋 (:clipboard:)"), + ("🔃", "🔃 (:clockwise_vertical_arrows:)"), + ("📕", "📕 (:closed_book:)"), + ("📪", "📪 (:closed_mailbox_with_lowered_flag:)"), + ("📫", "📫 (:closed_mailbox_with_raised_flag:)"), + ("🌂", "🌂 (:closed_umbrella:)"), + ("☁️", "☁️ (:cloud:)"), + ("☁", "☁ (:cloud:)"), + ("🌩️", "🌩️ (:cloud_with_lightning:)"), + ("🌩", "🌩 (:cloud_with_lightning:)"), + ("⛈️", "⛈️ (:cloud_with_lightning_and_rain:)"), + ("⛈", "⛈ (:cloud_with_lightning_and_rain:)"), + ("🌧️", "🌧️ (:cloud_with_rain:)"), + ("🌧", "🌧 (:cloud_with_rain:)"), + ("🌨️", "🌨️ (:cloud_with_snow:)"), + ("🌨", "🌨 (:cloud_with_snow:)"), + ("🤡", "🤡 (:clown_face:)"), + ("♣️", "♣️ (:club_suit:)"), + ("♣", "♣ (:club_suit:)"), + ("👝", "👝 (:clutch_bag:)"), + ("🧥", "🧥 (:coat:)"), + ("🪳", "🪳 (:cockroach:)"), + ("🍸", "🍸 (:cocktail_glass:)"), + ("🥥", "🥥 (:coconut:)"), + ("⚰️", "⚰️ (:coffin:)"), + ("⚰", "⚰ (:coffin:)"), + ("🪙", "🪙 (:coin:)"), + ("🥶", "🥶 (:cold_face:)"), + ("💥", "💥 (:collision:)"), + ("☄️", "☄️ (:comet:)"), + ("☄", "☄ (:comet:)"), + ("🧭", "🧭 (:compass:)"), + ("💽", "💽 (:computer_disk:)"), + ("🖱️", "🖱️ (:computer_mouse:)"), + ("🖱", "🖱 (:computer_mouse:)"), + ("🎊", "🎊 (:confetti_ball:)"), + ("😖", "😖 (:confounded_face:)"), + ("😕", "😕 (:confused_face:)"), + ("🚧", "🚧 (:construction:)"), + ("👷", "👷 (:construction_worker:)"), + ("👷🏿", "👷🏿 (:construction_worker_dark_skin_tone:)"), + ("👷🏻", "👷🏻 (:construction_worker_light_skin_tone:)"), + ("👷🏾", "👷🏾 (:construction_worker_medium-dark_skin_tone:)"), + ("👷🏼", "👷🏼 (:construction_worker_medium-light_skin_tone:)"), + ("👷🏽", "👷🏽 (:construction_worker_medium_skin_tone:)"), + ("🎛️", "🎛️ (:control_knobs:)"), + ("🎛", "🎛 (:control_knobs:)"), + ("🏪", "🏪 (:convenience_store:)"), + ("🧑\u200d🍳", "🧑\u200d🍳 (:cook:)"), + ("🧑🏿\u200d🍳", "🧑🏿\u200d🍳 (:cook_dark_skin_tone:)"), + ("🧑🏻\u200d🍳", "🧑🏻\u200d🍳 (:cook_light_skin_tone:)"), + ("🧑🏾\u200d🍳", "🧑🏾\u200d🍳 (:cook_medium-dark_skin_tone:)"), + ("🧑🏼\u200d🍳", "🧑🏼\u200d🍳 (:cook_medium-light_skin_tone:)"), + ("🧑🏽\u200d🍳", "🧑🏽\u200d🍳 (:cook_medium_skin_tone:)"), + ("🍚", "🍚 (:cooked_rice:)"), + ("🍪", "🍪 (:cookie:)"), + ("🍳", "🍳 (:cooking:)"), + ("©️", "©️ (:copyright:)"), + ("©", "© (:copyright:)"), + ("\U0001fab8", "\U0001fab8 (:coral:)"), + ("🛋️", "🛋️ (:couch_and_lamp:)"), + ("🛋", "🛋 (:couch_and_lamp:)"), + ("🔄", "🔄 (:counterclockwise_arrows_button:)"), + ("💑", "💑 (:couple_with_heart:)"), + ("💑🏿", "💑🏿 (:couple_with_heart_dark_skin_tone:)"), + ("💑🏻", "💑🏻 (:couple_with_heart_light_skin_tone:)"), + ( + "👨\u200d❤️\u200d👨", + "👨\u200d❤️\u200d👨 (:couple_with_heart_man_man:)", + ), + ( + "👨\u200d❤\u200d👨", + "👨\u200d❤\u200d👨 (:couple_with_heart_man_man:)", + ), + ( + "👨🏿\u200d❤️\u200d👨🏿", + "👨🏿\u200d❤️\u200d👨🏿 (:couple_with_heart_man_man_dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d👨🏿", + "👨🏿\u200d❤\u200d👨🏿 (:couple_with_heart_man_man_dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d👨🏻", + "👨🏿\u200d❤️\u200d👨🏻 (:couple_with_heart_man_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d👨🏻", + "👨🏿\u200d❤\u200d👨🏻 (:couple_with_heart_man_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d👨🏾", + "👨🏿\u200d❤️\u200d👨🏾 (:couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d👨🏾", + "👨🏿\u200d❤\u200d👨🏾 (:couple_with_heart_man_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d👨🏼", + "👨🏿\u200d❤️\u200d👨🏼 (:couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d👨🏼", + "👨🏿\u200d❤\u200d👨🏼 (:couple_with_heart_man_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d👨🏽", + "👨🏿\u200d❤️\u200d👨🏽 (:couple_with_heart_man_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d👨🏽", + "👨🏿\u200d❤\u200d👨🏽 (:couple_with_heart_man_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d👨🏻", + "👨🏻\u200d❤️\u200d👨🏻 (:couple_with_heart_man_man_light_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d👨🏻", + "👨🏻\u200d❤\u200d👨🏻 (:couple_with_heart_man_man_light_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d👨🏿", + "👨🏻\u200d❤️\u200d👨🏿 (:couple_with_heart_man_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d👨🏿", + "👨🏻\u200d❤\u200d👨🏿 (:couple_with_heart_man_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d👨🏾", + "👨🏻\u200d❤️\u200d👨🏾 (:couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d👨🏾", + "👨🏻\u200d❤\u200d👨🏾 (:couple_with_heart_man_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d👨🏼", + "👨🏻\u200d❤️\u200d👨🏼 (:couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d👨🏼", + "👨🏻\u200d❤\u200d👨🏼 (:couple_with_heart_man_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d👨🏽", + "👨🏻\u200d❤️\u200d👨🏽 (:couple_with_heart_man_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d👨🏽", + "👨🏻\u200d❤\u200d👨🏽 (:couple_with_heart_man_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d👨🏾", + "👨🏾\u200d❤️\u200d👨🏾 (:couple_with_heart_man_man_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d👨🏾", + "👨🏾\u200d❤\u200d👨🏾 (:couple_with_heart_man_man_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d👨🏿", + "👨🏾\u200d❤️\u200d👨🏿 (:couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d👨🏿", + "👨🏾\u200d❤\u200d👨🏿 (:couple_with_heart_man_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d👨🏻", + "👨🏾\u200d❤️\u200d👨🏻 (:couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d👨🏻", + "👨🏾\u200d❤\u200d👨🏻 (:couple_with_heart_man_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d👨🏼", + "👨🏾\u200d❤️\u200d👨🏼 (:couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d👨🏼", + "👨🏾\u200d❤\u200d👨🏼 (:couple_with_heart_man_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d👨🏽", + "👨🏾\u200d❤️\u200d👨🏽 (:couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d👨🏽", + "👨🏾\u200d❤\u200d👨🏽 (:couple_with_heart_man_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d👨🏼", + "👨🏼\u200d❤️\u200d👨🏼 (:couple_with_heart_man_man_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d👨🏼", + "👨🏼\u200d❤\u200d👨🏼 (:couple_with_heart_man_man_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d👨🏿", + "👨🏼\u200d❤️\u200d👨🏿 (:couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d👨🏿", + "👨🏼\u200d❤\u200d👨🏿 (:couple_with_heart_man_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d👨🏻", + "👨🏼\u200d❤️\u200d👨🏻 (:couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d👨🏻", + "👨🏼\u200d❤\u200d👨🏻 (:couple_with_heart_man_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d👨🏾", + "👨🏼\u200d❤️\u200d👨🏾 (:couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d👨🏾", + "👨🏼\u200d❤\u200d👨🏾 (:couple_with_heart_man_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d👨🏽", + "👨🏼\u200d❤️\u200d👨🏽 (:couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d👨🏽", + "👨🏼\u200d❤\u200d👨🏽 (:couple_with_heart_man_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d👨🏽", + "👨🏽\u200d❤️\u200d👨🏽 (:couple_with_heart_man_man_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d👨🏽", + "👨🏽\u200d❤\u200d👨🏽 (:couple_with_heart_man_man_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d👨🏿", + "👨🏽\u200d❤️\u200d👨🏿 (:couple_with_heart_man_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d👨🏿", + "👨🏽\u200d❤\u200d👨🏿 (:couple_with_heart_man_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d👨🏻", + "👨🏽\u200d❤️\u200d👨🏻 (:couple_with_heart_man_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d👨🏻", + "👨🏽\u200d❤\u200d👨🏻 (:couple_with_heart_man_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d👨🏾", + "👨🏽\u200d❤️\u200d👨🏾 (:couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d👨🏾", + "👨🏽\u200d❤\u200d👨🏾 (:couple_with_heart_man_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d👨🏼", + "👨🏽\u200d❤️\u200d👨🏼 (:couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d👨🏼", + "👨🏽\u200d❤\u200d👨🏼 (:couple_with_heart_man_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ("💑🏾", "💑🏾 (:couple_with_heart_medium-dark_skin_tone:)"), + ("💑🏼", "💑🏼 (:couple_with_heart_medium-light_skin_tone:)"), + ("💑🏽", "💑🏽 (:couple_with_heart_medium_skin_tone:)"), + ( + "🧑🏿\u200d❤️\u200d🧑🏻", + "🧑🏿\u200d❤️\u200d🧑🏻 (:couple_with_heart_person_person_dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d🧑🏻", + "🧑🏿\u200d❤\u200d🧑🏻 (:couple_with_heart_person_person_dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d🧑🏾", + "🧑🏿\u200d❤️\u200d🧑🏾 (:couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d🧑🏾", + "🧑🏿\u200d❤\u200d🧑🏾 (:couple_with_heart_person_person_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d🧑🏼", + "🧑🏿\u200d❤️\u200d🧑🏼 (:couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d🧑🏼", + "🧑🏿\u200d❤\u200d🧑🏼 (:couple_with_heart_person_person_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d🧑🏽", + "🧑🏿\u200d❤️\u200d🧑🏽 (:couple_with_heart_person_person_dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d🧑🏽", + "🧑🏿\u200d❤\u200d🧑🏽 (:couple_with_heart_person_person_dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d🧑🏿", + "🧑🏻\u200d❤️\u200d🧑🏿 (:couple_with_heart_person_person_light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d🧑🏿", + "🧑🏻\u200d❤\u200d🧑🏿 (:couple_with_heart_person_person_light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d🧑🏾", + "🧑🏻\u200d❤️\u200d🧑🏾 (:couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d🧑🏾", + "🧑🏻\u200d❤\u200d🧑🏾 (:couple_with_heart_person_person_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d🧑🏼", + "🧑🏻\u200d❤️\u200d🧑🏼 (:couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d🧑🏼", + "🧑🏻\u200d❤\u200d🧑🏼 (:couple_with_heart_person_person_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d🧑🏽", + "🧑🏻\u200d❤️\u200d🧑🏽 (:couple_with_heart_person_person_light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d🧑🏽", + "🧑🏻\u200d❤\u200d🧑🏽 (:couple_with_heart_person_person_light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d🧑🏿", + "🧑🏾\u200d❤️\u200d🧑🏿 (:couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d🧑🏿", + "🧑🏾\u200d❤\u200d🧑🏿 (:couple_with_heart_person_person_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d🧑🏻", + "🧑🏾\u200d❤️\u200d🧑🏻 (:couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d🧑🏻", + "🧑🏾\u200d❤\u200d🧑🏻 (:couple_with_heart_person_person_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d🧑🏼", + "🧑🏾\u200d❤️\u200d🧑🏼 (:couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d🧑🏼", + "🧑🏾\u200d❤\u200d🧑🏼 (:couple_with_heart_person_person_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d🧑🏽", + "🧑🏾\u200d❤️\u200d🧑🏽 (:couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d🧑🏽", + "🧑🏾\u200d❤\u200d🧑🏽 (:couple_with_heart_person_person_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d🧑🏿", + "🧑🏼\u200d❤️\u200d🧑🏿 (:couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d🧑🏿", + "🧑🏼\u200d❤\u200d🧑🏿 (:couple_with_heart_person_person_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d🧑🏻", + "🧑🏼\u200d❤️\u200d🧑🏻 (:couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d🧑🏻", + "🧑🏼\u200d❤\u200d🧑🏻 (:couple_with_heart_person_person_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d🧑🏾", + "🧑🏼\u200d❤️\u200d🧑🏾 (:couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d🧑🏾", + "🧑🏼\u200d❤\u200d🧑🏾 (:couple_with_heart_person_person_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d🧑🏽", + "🧑🏼\u200d❤️\u200d🧑🏽 (:couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d🧑🏽", + "🧑🏼\u200d❤\u200d🧑🏽 (:couple_with_heart_person_person_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d🧑🏿", + "🧑🏽\u200d❤️\u200d🧑🏿 (:couple_with_heart_person_person_medium_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d🧑🏿", + "🧑🏽\u200d❤\u200d🧑🏿 (:couple_with_heart_person_person_medium_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d🧑🏻", + "🧑🏽\u200d❤️\u200d🧑🏻 (:couple_with_heart_person_person_medium_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d🧑🏻", + "🧑🏽\u200d❤\u200d🧑🏻 (:couple_with_heart_person_person_medium_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d🧑🏾", + "🧑🏽\u200d❤️\u200d🧑🏾 (:couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d🧑🏾", + "🧑🏽\u200d❤\u200d🧑🏾 (:couple_with_heart_person_person_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d🧑🏼", + "🧑🏽\u200d❤️\u200d🧑🏼 (:couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d🧑🏼", + "🧑🏽\u200d❤\u200d🧑🏼 (:couple_with_heart_person_person_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩\u200d❤️\u200d👨", + "👩\u200d❤️\u200d👨 (:couple_with_heart_woman_man:)", + ), + ( + "👩\u200d❤\u200d👨", + "👩\u200d❤\u200d👨 (:couple_with_heart_woman_man:)", + ), + ( + "👩🏿\u200d❤️\u200d👨🏿", + "👩🏿\u200d❤️\u200d👨🏿 (:couple_with_heart_woman_man_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👨🏿", + "👩🏿\u200d❤\u200d👨🏿 (:couple_with_heart_woman_man_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👨🏻", + "👩🏿\u200d❤️\u200d👨🏻 (:couple_with_heart_woman_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👨🏻", + "👩🏿\u200d❤\u200d👨🏻 (:couple_with_heart_woman_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👨🏾", + "👩🏿\u200d❤️\u200d👨🏾 (:couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👨🏾", + "👩🏿\u200d❤\u200d👨🏾 (:couple_with_heart_woman_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👨🏼", + "👩🏿\u200d❤️\u200d👨🏼 (:couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👨🏼", + "👩🏿\u200d❤\u200d👨🏼 (:couple_with_heart_woman_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👨🏽", + "👩🏿\u200d❤️\u200d👨🏽 (:couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👨🏽", + "👩🏿\u200d❤\u200d👨🏽 (:couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👨🏻", + "👩🏻\u200d❤️\u200d👨🏻 (:couple_with_heart_woman_man_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👨🏻", + "👩🏻\u200d❤\u200d👨🏻 (:couple_with_heart_woman_man_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👨🏿", + "👩🏻\u200d❤️\u200d👨🏿 (:couple_with_heart_woman_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👨🏿", + "👩🏻\u200d❤\u200d👨🏿 (:couple_with_heart_woman_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👨🏾", + "👩🏻\u200d❤️\u200d👨🏾 (:couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👨🏾", + "👩🏻\u200d❤\u200d👨🏾 (:couple_with_heart_woman_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👨🏼", + "👩🏻\u200d❤️\u200d👨🏼 (:couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👨🏼", + "👩🏻\u200d❤\u200d👨🏼 (:couple_with_heart_woman_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👨🏽", + "👩🏻\u200d❤️\u200d👨🏽 (:couple_with_heart_woman_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👨🏽", + "👩🏻\u200d❤\u200d👨🏽 (:couple_with_heart_woman_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👨🏾", + "👩🏾\u200d❤️\u200d👨🏾 (:couple_with_heart_woman_man_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👨🏾", + "👩🏾\u200d❤\u200d👨🏾 (:couple_with_heart_woman_man_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👨🏿", + "👩🏾\u200d❤️\u200d👨🏿 (:couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👨🏿", + "👩🏾\u200d❤\u200d👨🏿 (:couple_with_heart_woman_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👨🏻", + "👩🏾\u200d❤️\u200d👨🏻 (:couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👨🏻", + "👩🏾\u200d❤\u200d👨🏻 (:couple_with_heart_woman_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👨🏼", + "👩🏾\u200d❤️\u200d👨🏼 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👨🏼", + "👩🏾\u200d❤\u200d👨🏼 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👨🏽", + "👩🏾\u200d❤️\u200d👨🏽 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👨🏽", + "👩🏾\u200d❤\u200d👨🏽 (:couple_with_heart_woman_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👨🏼", + "👩🏼\u200d❤️\u200d👨🏼 (:couple_with_heart_woman_man_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👨🏼", + "👩🏼\u200d❤\u200d👨🏼 (:couple_with_heart_woman_man_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👨🏿", + "👩🏼\u200d❤️\u200d👨🏿 (:couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👨🏿", + "👩🏼\u200d❤\u200d👨🏿 (:couple_with_heart_woman_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👨🏻", + "👩🏼\u200d❤️\u200d👨🏻 (:couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👨🏻", + "👩🏼\u200d❤\u200d👨🏻 (:couple_with_heart_woman_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👨🏾", + "👩🏼\u200d❤️\u200d👨🏾 (:couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👨🏾", + "👩🏼\u200d❤\u200d👨🏾 (:couple_with_heart_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👨🏽", + "👩🏼\u200d❤️\u200d👨🏽 (:couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👨🏽", + "👩🏼\u200d❤\u200d👨🏽 (:couple_with_heart_woman_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👨🏽", + "👩🏽\u200d❤️\u200d👨🏽 (:couple_with_heart_woman_man_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👨🏽", + "👩🏽\u200d❤\u200d👨🏽 (:couple_with_heart_woman_man_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👨🏿", + "👩🏽\u200d❤️\u200d👨🏿 (:couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👨🏿", + "👩🏽\u200d❤\u200d👨🏿 (:couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👨🏻", + "👩🏽\u200d❤️\u200d👨🏻 (:couple_with_heart_woman_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👨🏻", + "👩🏽\u200d❤\u200d👨🏻 (:couple_with_heart_woman_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👨🏾", + "👩🏽\u200d❤️\u200d👨🏾 (:couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👨🏾", + "👩🏽\u200d❤\u200d👨🏾 (:couple_with_heart_woman_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👨🏼", + "👩🏽\u200d❤️\u200d👨🏼 (:couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👨🏼", + "👩🏽\u200d❤\u200d👨🏼 (:couple_with_heart_woman_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩\u200d❤️\u200d👩", + "👩\u200d❤️\u200d👩 (:couple_with_heart_woman_woman:)", + ), + ( + "👩\u200d❤\u200d👩", + "👩\u200d❤\u200d👩 (:couple_with_heart_woman_woman:)", + ), + ( + "👩🏿\u200d❤️\u200d👩🏿", + "👩🏿\u200d❤️\u200d👩🏿 (:couple_with_heart_woman_woman_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👩🏿", + "👩🏿\u200d❤\u200d👩🏿 (:couple_with_heart_woman_woman_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👩🏻", + "👩🏿\u200d❤️\u200d👩🏻 (:couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👩🏻", + "👩🏿\u200d❤\u200d👩🏻 (:couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👩🏾", + "👩🏿\u200d❤️\u200d👩🏾 (:couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👩🏾", + "👩🏿\u200d❤\u200d👩🏾 (:couple_with_heart_woman_woman_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👩🏼", + "👩🏿\u200d❤️\u200d👩🏼 (:couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👩🏼", + "👩🏿\u200d❤\u200d👩🏼 (:couple_with_heart_woman_woman_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d👩🏽", + "👩🏿\u200d❤️\u200d👩🏽 (:couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d👩🏽", + "👩🏿\u200d❤\u200d👩🏽 (:couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👩🏻", + "👩🏻\u200d❤️\u200d👩🏻 (:couple_with_heart_woman_woman_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👩🏻", + "👩🏻\u200d❤\u200d👩🏻 (:couple_with_heart_woman_woman_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👩🏿", + "👩🏻\u200d❤️\u200d👩🏿 (:couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👩🏿", + "👩🏻\u200d❤\u200d👩🏿 (:couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👩🏾", + "👩🏻\u200d❤️\u200d👩🏾 (:couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👩🏾", + "👩🏻\u200d❤\u200d👩🏾 (:couple_with_heart_woman_woman_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👩🏼", + "👩🏻\u200d❤️\u200d👩🏼 (:couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👩🏼", + "👩🏻\u200d❤\u200d👩🏼 (:couple_with_heart_woman_woman_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d👩🏽", + "👩🏻\u200d❤️\u200d👩🏽 (:couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d👩🏽", + "👩🏻\u200d❤\u200d👩🏽 (:couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👩🏾", + "👩🏾\u200d❤️\u200d👩🏾 (:couple_with_heart_woman_woman_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👩🏾", + "👩🏾\u200d❤\u200d👩🏾 (:couple_with_heart_woman_woman_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👩🏿", + "👩🏾\u200d❤️\u200d👩🏿 (:couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👩🏿", + "👩🏾\u200d❤\u200d👩🏿 (:couple_with_heart_woman_woman_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👩🏻", + "👩🏾\u200d❤️\u200d👩🏻 (:couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👩🏻", + "👩🏾\u200d❤\u200d👩🏻 (:couple_with_heart_woman_woman_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👩🏼", + "👩🏾\u200d❤️\u200d👩🏼 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👩🏼", + "👩🏾\u200d❤\u200d👩🏼 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d👩🏽", + "👩🏾\u200d❤️\u200d👩🏽 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d👩🏽", + "👩🏾\u200d❤\u200d👩🏽 (:couple_with_heart_woman_woman_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👩🏼", + "👩🏼\u200d❤️\u200d👩🏼 (:couple_with_heart_woman_woman_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👩🏼", + "👩🏼\u200d❤\u200d👩🏼 (:couple_with_heart_woman_woman_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👩🏿", + "👩🏼\u200d❤️\u200d👩🏿 (:couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👩🏿", + "👩🏼\u200d❤\u200d👩🏿 (:couple_with_heart_woman_woman_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👩🏻", + "👩🏼\u200d❤️\u200d👩🏻 (:couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👩🏻", + "👩🏼\u200d❤\u200d👩🏻 (:couple_with_heart_woman_woman_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👩🏾", + "👩🏼\u200d❤️\u200d👩🏾 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👩🏾", + "👩🏼\u200d❤\u200d👩🏾 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d👩🏽", + "👩🏼\u200d❤️\u200d👩🏽 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d👩🏽", + "👩🏼\u200d❤\u200d👩🏽 (:couple_with_heart_woman_woman_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👩🏽", + "👩🏽\u200d❤️\u200d👩🏽 (:couple_with_heart_woman_woman_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👩🏽", + "👩🏽\u200d❤\u200d👩🏽 (:couple_with_heart_woman_woman_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👩🏿", + "👩🏽\u200d❤️\u200d👩🏿 (:couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👩🏿", + "👩🏽\u200d❤\u200d👩🏿 (:couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👩🏻", + "👩🏽\u200d❤️\u200d👩🏻 (:couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👩🏻", + "👩🏽\u200d❤\u200d👩🏻 (:couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👩🏾", + "👩🏽\u200d❤️\u200d👩🏾 (:couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👩🏾", + "👩🏽\u200d❤\u200d👩🏾 (:couple_with_heart_woman_woman_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d👩🏼", + "👩🏽\u200d❤️\u200d👩🏼 (:couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d👩🏼", + "👩🏽\u200d❤\u200d👩🏼 (:couple_with_heart_woman_woman_medium_skin_tone_medium-light_skin_tone:)", + ), + ("🐄", "🐄 (:cow:)"), + ("🐮", "🐮 (:cow_face:)"), + ("🤠", "🤠 (:cowboy_hat_face:)"), + ("🦀", "🦀 (:crab:)"), + ("🖍️", "🖍️ (:crayon:)"), + ("🖍", "🖍 (:crayon:)"), + ("💳", "💳 (:credit_card:)"), + ("🌙", "🌙 (:crescent_moon:)"), + ("🦗", "🦗 (:cricket:)"), + ("🏏", "🏏 (:cricket_game:)"), + ("🐊", "🐊 (:crocodile:)"), + ("🥐", "🥐 (:croissant:)"), + ("❌", "❌ (:cross_mark:)"), + ("❎", "❎ (:cross_mark_button:)"), + ("🤞", "🤞 (:crossed_fingers:)"), + ("🤞🏿", "🤞🏿 (:crossed_fingers_dark_skin_tone:)"), + ("🤞🏻", "🤞🏻 (:crossed_fingers_light_skin_tone:)"), + ("🤞🏾", "🤞🏾 (:crossed_fingers_medium-dark_skin_tone:)"), + ("🤞🏼", "🤞🏼 (:crossed_fingers_medium-light_skin_tone:)"), + ("🤞🏽", "🤞🏽 (:crossed_fingers_medium_skin_tone:)"), + ("🎌", "🎌 (:crossed_flags:)"), + ("⚔️", "⚔️ (:crossed_swords:)"), + ("⚔", "⚔ (:crossed_swords:)"), + ("👑", "👑 (:crown:)"), + ("\U0001fa7c", "\U0001fa7c (:crutch:)"), + ("😿", "😿 (:crying_cat:)"), + ("😢", "😢 (:crying_face:)"), + ("🔮", "🔮 (:crystal_ball:)"), + ("🥒", "🥒 (:cucumber:)"), + ("🥤", "🥤 (:cup_with_straw:)"), + ("🧁", "🧁 (:cupcake:)"), + ("🥌", "🥌 (:curling_stone:)"), + ("🦱", "🦱 (:curly_hair:)"), + ("➰", "➰ (:curly_loop:)"), + ("💱", "💱 (:currency_exchange:)"), + ("🍛", "🍛 (:curry_rice:)"), + ("🍮", "🍮 (:custard:)"), + ("🛃", "🛃 (:customs:)"), + ("🥩", "🥩 (:cut_of_meat:)"), + ("🌀", "🌀 (:cyclone:)"), + ("🗡️", "🗡️ (:dagger:)"), + ("🗡", "🗡 (:dagger:)"), + ("🍡", "🍡 (:dango:)"), + ("🏿", "🏿 (:dark_skin_tone:)"), + ("💨", "💨 (:dashing_away:)"), + ("🧏\u200d♂️", "🧏\u200d♂️ (:deaf_man:)"), + ("🧏\u200d♂", "🧏\u200d♂ (:deaf_man:)"), + ("🧏🏿\u200d♂️", "🧏🏿\u200d♂️ (:deaf_man_dark_skin_tone:)"), + ("🧏🏿\u200d♂", "🧏🏿\u200d♂ (:deaf_man_dark_skin_tone:)"), + ("🧏🏻\u200d♂️", "🧏🏻\u200d♂️ (:deaf_man_light_skin_tone:)"), + ("🧏🏻\u200d♂", "🧏🏻\u200d♂ (:deaf_man_light_skin_tone:)"), + ( + "🧏🏾\u200d♂️", + "🧏🏾\u200d♂️ (:deaf_man_medium-dark_skin_tone:)", + ), + ( + "🧏🏾\u200d♂", + "🧏🏾\u200d♂ (:deaf_man_medium-dark_skin_tone:)", + ), + ( + "🧏🏼\u200d♂️", + "🧏🏼\u200d♂️ (:deaf_man_medium-light_skin_tone:)", + ), + ( + "🧏🏼\u200d♂", + "🧏🏼\u200d♂ (:deaf_man_medium-light_skin_tone:)", + ), + ("🧏🏽\u200d♂️", "🧏🏽\u200d♂️ (:deaf_man_medium_skin_tone:)"), + ("🧏🏽\u200d♂", "🧏🏽\u200d♂ (:deaf_man_medium_skin_tone:)"), + ("🧏", "🧏 (:deaf_person:)"), + ("🧏🏿", "🧏🏿 (:deaf_person_dark_skin_tone:)"), + ("🧏🏻", "🧏🏻 (:deaf_person_light_skin_tone:)"), + ("🧏🏾", "🧏🏾 (:deaf_person_medium-dark_skin_tone:)"), + ("🧏🏼", "🧏🏼 (:deaf_person_medium-light_skin_tone:)"), + ("🧏🏽", "🧏🏽 (:deaf_person_medium_skin_tone:)"), + ("🧏\u200d♀️", "🧏\u200d♀️ (:deaf_woman:)"), + ("🧏\u200d♀", "🧏\u200d♀ (:deaf_woman:)"), + ("🧏🏿\u200d♀️", "🧏🏿\u200d♀️ (:deaf_woman_dark_skin_tone:)"), + ("🧏🏿\u200d♀", "🧏🏿\u200d♀ (:deaf_woman_dark_skin_tone:)"), + ("🧏🏻\u200d♀️", "🧏🏻\u200d♀️ (:deaf_woman_light_skin_tone:)"), + ("🧏🏻\u200d♀", "🧏🏻\u200d♀ (:deaf_woman_light_skin_tone:)"), + ( + "🧏🏾\u200d♀️", + "🧏🏾\u200d♀️ (:deaf_woman_medium-dark_skin_tone:)", + ), + ( + "🧏🏾\u200d♀", + "🧏🏾\u200d♀ (:deaf_woman_medium-dark_skin_tone:)", + ), + ( + "🧏🏼\u200d♀️", + "🧏🏼\u200d♀️ (:deaf_woman_medium-light_skin_tone:)", + ), + ( + "🧏🏼\u200d♀", + "🧏🏼\u200d♀ (:deaf_woman_medium-light_skin_tone:)", + ), + ( + "🧏🏽\u200d♀️", + "🧏🏽\u200d♀️ (:deaf_woman_medium_skin_tone:)", + ), + ("🧏🏽\u200d♀", "🧏🏽\u200d♀ (:deaf_woman_medium_skin_tone:)"), + ("🌳", "🌳 (:deciduous_tree:)"), + ("🦌", "🦌 (:deer:)"), + ("🚚", "🚚 (:delivery_truck:)"), + ("🏬", "🏬 (:department_store:)"), + ("🏚️", "🏚️ (:derelict_house:)"), + ("🏚", "🏚 (:derelict_house:)"), + ("🏜️", "🏜️ (:desert:)"), + ("🏜", "🏜 (:desert:)"), + ("🏝️", "🏝️ (:desert_island:)"), + ("🏝", "🏝 (:desert_island:)"), + ("🖥️", "🖥️ (:desktop_computer:)"), + ("🖥", "🖥 (:desktop_computer:)"), + ("🕵️", "🕵️ (:detective:)"), + ("🕵", "🕵 (:detective:)"), + ("🕵🏿", "🕵🏿 (:detective_dark_skin_tone:)"), + ("🕵🏻", "🕵🏻 (:detective_light_skin_tone:)"), + ("🕵🏾", "🕵🏾 (:detective_medium-dark_skin_tone:)"), + ("🕵🏼", "🕵🏼 (:detective_medium-light_skin_tone:)"), + ("🕵🏽", "🕵🏽 (:detective_medium_skin_tone:)"), + ("♦️", "♦️ (:diamond_suit:)"), + ("♦", "♦ (:diamond_suit:)"), + ("💠", "💠 (:diamond_with_a_dot:)"), + ("🔅", "🔅 (:dim_button:)"), + ("😞", "😞 (:disappointed_face:)"), + ("🥸", "🥸 (:disguised_face:)"), + ("➗", "➗ (:divide:)"), + ("🤿", "🤿 (:diving_mask:)"), + ("🪔", "🪔 (:diya_lamp:)"), + ("💫", "💫 (:dizzy:)"), + ("🧬", "🧬 (:dna:)"), + ("🦤", "🦤 (:dodo:)"), + ("🐕", "🐕 (:dog:)"), + ("🐶", "🐶 (:dog_face:)"), + ("💵", "💵 (:dollar_banknote:)"), + ("🐬", "🐬 (:dolphin:)"), + ("\U0001facf", "\U0001facf (:donkey:)"), + ("🚪", "🚪 (:door:)"), + ("\U0001fae5", "\U0001fae5 (:dotted_line_face:)"), + ("🔯", "🔯 (:dotted_six-pointed_star:)"), + ("➿", "➿ (:double_curly_loop:)"), + ("‼️", "‼️ (:double_exclamation_mark:)"), + ("‼", "‼ (:double_exclamation_mark:)"), + ("🍩", "🍩 (:doughnut:)"), + ("🕊️", "🕊️ (:dove:)"), + ("🕊", "🕊 (:dove:)"), + ("↙️", "↙️ (:down-left_arrow:)"), + ("↙", "↙ (:down-left_arrow:)"), + ("↘️", "↘️ (:down-right_arrow:)"), + ("↘", "↘ (:down-right_arrow:)"), + ("⬇️", "⬇️ (:down_arrow:)"), + ("⬇", "⬇ (:down_arrow:)"), + ("😓", "😓 (:downcast_face_with_sweat:)"), + ("🔽", "🔽 (:downwards_button:)"), + ("🐉", "🐉 (:dragon:)"), + ("🐲", "🐲 (:dragon_face:)"), + ("👗", "👗 (:dress:)"), + ("🤤", "🤤 (:drooling_face:)"), + ("🩸", "🩸 (:drop_of_blood:)"), + ("💧", "💧 (:droplet:)"), + ("🥁", "🥁 (:drum:)"), + ("🦆", "🦆 (:duck:)"), + ("🥟", "🥟 (:dumpling:)"), + ("📀", "📀 (:dvd:)"), + ("📧", "📧 (:e-mail:)"), + ("🦅", "🦅 (:eagle:)"), + ("👂", "👂 (:ear:)"), + ("👂🏿", "👂🏿 (:ear_dark_skin_tone:)"), + ("👂🏻", "👂🏻 (:ear_light_skin_tone:)"), + ("👂🏾", "👂🏾 (:ear_medium-dark_skin_tone:)"), + ("👂🏼", "👂🏼 (:ear_medium-light_skin_tone:)"), + ("👂🏽", "👂🏽 (:ear_medium_skin_tone:)"), + ("🌽", "🌽 (:ear_of_corn:)"), + ("🦻", "🦻 (:ear_with_hearing_aid:)"), + ("🦻🏿", "🦻🏿 (:ear_with_hearing_aid_dark_skin_tone:)"), + ("🦻🏻", "🦻🏻 (:ear_with_hearing_aid_light_skin_tone:)"), + ("🦻🏾", "🦻🏾 (:ear_with_hearing_aid_medium-dark_skin_tone:)"), + ( + "🦻🏼", + "🦻🏼 (:ear_with_hearing_aid_medium-light_skin_tone:)", + ), + ("🦻🏽", "🦻🏽 (:ear_with_hearing_aid_medium_skin_tone:)"), + ("🥚", "🥚 (:egg:)"), + ("🍆", "🍆 (:eggplant:)"), + ("✴️", "✴️ (:eight-pointed_star:)"), + ("✴", "✴ (:eight-pointed_star:)"), + ("✳️", "✳️ (:eight-spoked_asterisk:)"), + ("✳", "✳ (:eight-spoked_asterisk:)"), + ("🕣", "🕣 (:eight-thirty:)"), + ("🕗", "🕗 (:eight_o’clock:)"), + ("⏏️", "⏏️ (:eject_button:)"), + ("⏏", "⏏ (:eject_button:)"), + ("🔌", "🔌 (:electric_plug:)"), + ("🐘", "🐘 (:elephant:)"), + ("🛗", "🛗 (:elevator:)"), + ("🕦", "🕦 (:eleven-thirty:)"), + ("🕚", "🕚 (:eleven_o’clock:)"), + ("🧝", "🧝 (:elf:)"), + ("🧝🏿", "🧝🏿 (:elf_dark_skin_tone:)"), + ("🧝🏻", "🧝🏻 (:elf_light_skin_tone:)"), + ("🧝🏾", "🧝🏾 (:elf_medium-dark_skin_tone:)"), + ("🧝🏼", "🧝🏼 (:elf_medium-light_skin_tone:)"), + ("🧝🏽", "🧝🏽 (:elf_medium_skin_tone:)"), + ("\U0001fab9", "\U0001fab9 (:empty_nest:)"), + ("😡", "😡 (:enraged_face:)"), + ("✉️", "✉️ (:envelope:)"), + ("✉", "✉ (:envelope:)"), + ("📩", "📩 (:envelope_with_arrow:)"), + ("💶", "💶 (:euro_banknote:)"), + ("🌲", "🌲 (:evergreen_tree:)"), + ("🐑", "🐑 (:ewe:)"), + ("⁉️", "⁉️ (:exclamation_question_mark:)"), + ("⁉", "⁉ (:exclamation_question_mark:)"), + ("🤯", "🤯 (:exploding_head:)"), + ("😑", "😑 (:expressionless_face:)"), + ("👁️", "👁️ (:eye:)"), + ("👁", "👁 (:eye:)"), + ("👁️\u200d🗨️", "👁️\u200d🗨️ (:eye_in_speech_bubble:)"), + ("👁\u200d🗨️", "👁\u200d🗨️ (:eye_in_speech_bubble:)"), + ("👁️\u200d🗨", "👁️\u200d🗨 (:eye_in_speech_bubble:)"), + ("👁\u200d🗨", "👁\u200d🗨 (:eye_in_speech_bubble:)"), + ("👀", "👀 (:eyes:)"), + ("😘", "😘 (:face_blowing_a_kiss:)"), + ("😮\u200d💨", "😮\u200d💨 (:face_exhaling:)"), + ("\U0001f979", "\U0001f979 (:face_holding_back_tears:)"), + ("😶\u200d🌫️", "😶\u200d🌫️ (:face_in_clouds:)"), + ("😶\u200d🌫", "😶\u200d🌫 (:face_in_clouds:)"), + ("😋", "😋 (:face_savoring_food:)"), + ("😱", "😱 (:face_screaming_in_fear:)"), + ("🤮", "🤮 (:face_vomiting:)"), + ("😵", "😵 (:face_with_crossed-out_eyes:)"), + ("\U0001fae4", "\U0001fae4 (:face_with_diagonal_mouth:)"), + ("🤭", "🤭 (:face_with_hand_over_mouth:)"), + ("🤕", "🤕 (:face_with_head-bandage:)"), + ("😷", "😷 (:face_with_medical_mask:)"), + ("🧐", "🧐 (:face_with_monocle:)"), + ( + "\U0001fae2", + "\U0001fae2 (:face_with_open_eyes_and_hand_over_mouth:)", + ), + ("😮", "😮 (:face_with_open_mouth:)"), + ("\U0001fae3", "\U0001fae3 (:face_with_peeking_eye:)"), + ("🤨", "🤨 (:face_with_raised_eyebrow:)"), + ("🙄", "🙄 (:face_with_rolling_eyes:)"), + ("😵\u200d💫", "😵\u200d💫 (:face_with_spiral_eyes:)"), + ("😤", "😤 (:face_with_steam_from_nose:)"), + ("🤬", "🤬 (:face_with_symbols_on_mouth:)"), + ("😂", "😂 (:face_with_tears_of_joy:)"), + ("🤒", "🤒 (:face_with_thermometer:)"), + ("😛", "😛 (:face_with_tongue:)"), + ("😶", "😶 (:face_without_mouth:)"), + ("🏭", "🏭 (:factory:)"), + ("🧑\u200d🏭", "🧑\u200d🏭 (:factory_worker:)"), + ( + "🧑🏿\u200d🏭", + "🧑🏿\u200d🏭 (:factory_worker_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🏭", + "🧑🏻\u200d🏭 (:factory_worker_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🏭", + "🧑🏾\u200d🏭 (:factory_worker_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🏭", + "🧑🏼\u200d🏭 (:factory_worker_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🏭", + "🧑🏽\u200d🏭 (:factory_worker_medium_skin_tone:)", + ), + ("🧚", "🧚 (:fairy:)"), + ("🧚🏿", "🧚🏿 (:fairy_dark_skin_tone:)"), + ("🧚🏻", "🧚🏻 (:fairy_light_skin_tone:)"), + ("🧚🏾", "🧚🏾 (:fairy_medium-dark_skin_tone:)"), + ("🧚🏼", "🧚🏼 (:fairy_medium-light_skin_tone:)"), + ("🧚🏽", "🧚🏽 (:fairy_medium_skin_tone:)"), + ("🧆", "🧆 (:falafel:)"), + ("🍂", "🍂 (:fallen_leaf:)"), + ("👪", "👪 (:family:)"), + ( + "🧑\u200d🧑\u200d🧒", + "🧑\u200d🧑\u200d🧒 (:family_adult_adult_child:)", + ), + ( + "🧑\u200d🧑\u200d🧒\u200d🧒", + "🧑\u200d🧑\u200d🧒\u200d🧒 (:family_adult_adult_child_child:)", + ), + ("🧑\u200d🧒", "🧑\u200d🧒 (:family_adult_child:)"), + ( + "🧑\u200d🧒\u200d🧒", + "🧑\u200d🧒\u200d🧒 (:family_adult_child_child:)", + ), + ("👨\u200d👦", "👨\u200d👦 (:family_man_boy:)"), + ( + "👨\u200d👦\u200d👦", + "👨\u200d👦\u200d👦 (:family_man_boy_boy:)", + ), + ("👨\u200d👧", "👨\u200d👧 (:family_man_girl:)"), + ( + "👨\u200d👧\u200d👦", + "👨\u200d👧\u200d👦 (:family_man_girl_boy:)", + ), + ( + "👨\u200d👧\u200d👧", + "👨\u200d👧\u200d👧 (:family_man_girl_girl:)", + ), + ( + "👨\u200d👨\u200d👦", + "👨\u200d👨\u200d👦 (:family_man_man_boy:)", + ), + ( + "👨\u200d👨\u200d👦\u200d👦", + "👨\u200d👨\u200d👦\u200d👦 (:family_man_man_boy_boy:)", + ), + ( + "👨\u200d👨\u200d👧", + "👨\u200d👨\u200d👧 (:family_man_man_girl:)", + ), + ( + "👨\u200d👨\u200d👧\u200d👦", + "👨\u200d👨\u200d👧\u200d👦 (:family_man_man_girl_boy:)", + ), + ( + "👨\u200d👨\u200d👧\u200d👧", + "👨\u200d👨\u200d👧\u200d👧 (:family_man_man_girl_girl:)", + ), + ( + "👨\u200d👩\u200d👦", + "👨\u200d👩\u200d👦 (:family_man_woman_boy:)", + ), + ( + "👨\u200d👩\u200d👦\u200d👦", + "👨\u200d👩\u200d👦\u200d👦 (:family_man_woman_boy_boy:)", + ), + ( + "👨\u200d👩\u200d👧", + "👨\u200d👩\u200d👧 (:family_man_woman_girl:)", + ), + ( + "👨\u200d👩\u200d👧\u200d👦", + "👨\u200d👩\u200d👧\u200d👦 (:family_man_woman_girl_boy:)", + ), + ( + "👨\u200d👩\u200d👧\u200d👧", + "👨\u200d👩\u200d👧\u200d👧 (:family_man_woman_girl_girl:)", + ), + ("👩\u200d👦", "👩\u200d👦 (:family_woman_boy:)"), + ( + "👩\u200d👦\u200d👦", + "👩\u200d👦\u200d👦 (:family_woman_boy_boy:)", + ), + ("👩\u200d👧", "👩\u200d👧 (:family_woman_girl:)"), + ( + "👩\u200d👧\u200d👦", + "👩\u200d👧\u200d👦 (:family_woman_girl_boy:)", + ), + ( + "👩\u200d👧\u200d👧", + "👩\u200d👧\u200d👧 (:family_woman_girl_girl:)", + ), + ( + "👩\u200d👩\u200d👦", + "👩\u200d👩\u200d👦 (:family_woman_woman_boy:)", + ), + ( + "👩\u200d👩\u200d👦\u200d👦", + "👩\u200d👩\u200d👦\u200d👦 (:family_woman_woman_boy_boy:)", + ), + ( + "👩\u200d👩\u200d👧", + "👩\u200d👩\u200d👧 (:family_woman_woman_girl:)", + ), + ( + "👩\u200d👩\u200d👧\u200d👦", + "👩\u200d👩\u200d👧\u200d👦 (:family_woman_woman_girl_boy:)", + ), + ( + "👩\u200d👩\u200d👧\u200d👧", + "👩\u200d👩\u200d👧\u200d👧 (:family_woman_woman_girl_girl:)", + ), + ("🧑\u200d🌾", "🧑\u200d🌾 (:farmer:)"), + ("🧑🏿\u200d🌾", "🧑🏿\u200d🌾 (:farmer_dark_skin_tone:)"), + ("🧑🏻\u200d🌾", "🧑🏻\u200d🌾 (:farmer_light_skin_tone:)"), + ("🧑🏾\u200d🌾", "🧑🏾\u200d🌾 (:farmer_medium-dark_skin_tone:)"), + ( + "🧑🏼\u200d🌾", + "🧑🏼\u200d🌾 (:farmer_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🌾", "🧑🏽\u200d🌾 (:farmer_medium_skin_tone:)"), + ("⏩", "⏩ (:fast-forward_button:)"), + ("⏬", "⏬ (:fast_down_button:)"), + ("⏪", "⏪ (:fast_reverse_button:)"), + ("⏫", "⏫ (:fast_up_button:)"), + ("📠", "📠 (:fax_machine:)"), + ("😨", "😨 (:fearful_face:)"), + ("🪶", "🪶 (:feather:)"), + ("♀️", "♀️ (:female_sign:)"), + ("♀", "♀ (:female_sign:)"), + ("🎡", "🎡 (:ferris_wheel:)"), + ("⛴️", "⛴️ (:ferry:)"), + ("⛴", "⛴ (:ferry:)"), + ("🏑", "🏑 (:field_hockey:)"), + ("🗄️", "🗄️ (:file_cabinet:)"), + ("🗄", "🗄 (:file_cabinet:)"), + ("📁", "📁 (:file_folder:)"), + ("🎞️", "🎞️ (:film_frames:)"), + ("🎞", "🎞 (:film_frames:)"), + ("📽️", "📽️ (:film_projector:)"), + ("📽", "📽 (:film_projector:)"), + ("🔥", "🔥 (:fire:)"), + ("🚒", "🚒 (:fire_engine:)"), + ("🧯", "🧯 (:fire_extinguisher:)"), + ("🧨", "🧨 (:firecracker:)"), + ("🧑\u200d🚒", "🧑\u200d🚒 (:firefighter:)"), + ("🧑🏿\u200d🚒", "🧑🏿\u200d🚒 (:firefighter_dark_skin_tone:)"), + ("🧑🏻\u200d🚒", "🧑🏻\u200d🚒 (:firefighter_light_skin_tone:)"), + ( + "🧑🏾\u200d🚒", + "🧑🏾\u200d🚒 (:firefighter_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🚒", + "🧑🏼\u200d🚒 (:firefighter_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🚒", "🧑🏽\u200d🚒 (:firefighter_medium_skin_tone:)"), + ("🎆", "🎆 (:fireworks:)"), + ("🌓", "🌓 (:first_quarter_moon:)"), + ("🌛", "🌛 (:first_quarter_moon_face:)"), + ("🐟", "🐟 (:fish:)"), + ("🍥", "🍥 (:fish_cake_with_swirl:)"), + ("🎣", "🎣 (:fishing_pole:)"), + ("🕠", "🕠 (:five-thirty:)"), + ("🕔", "🕔 (:five_o’clock:)"), + ("⛳", "⛳ (:flag_in_hole:)"), + ("🦩", "🦩 (:flamingo:)"), + ("🔦", "🔦 (:flashlight:)"), + ("🥿", "🥿 (:flat_shoe:)"), + ("🫓", "🫓 (:flatbread:)"), + ("⚜️", "⚜️ (:fleur-de-lis:)"), + ("⚜", "⚜ (:fleur-de-lis:)"), + ("💪", "💪 (:flexed_biceps:)"), + ("💪🏿", "💪🏿 (:flexed_biceps_dark_skin_tone:)"), + ("💪🏻", "💪🏻 (:flexed_biceps_light_skin_tone:)"), + ("💪🏾", "💪🏾 (:flexed_biceps_medium-dark_skin_tone:)"), + ("💪🏼", "💪🏼 (:flexed_biceps_medium-light_skin_tone:)"), + ("💪🏽", "💪🏽 (:flexed_biceps_medium_skin_tone:)"), + ("💾", "💾 (:floppy_disk:)"), + ("🎴", "🎴 (:flower_playing_cards:)"), + ("😳", "😳 (:flushed_face:)"), + ("\U0001fa88", "\U0001fa88 (:flute:)"), + ("🪰", "🪰 (:fly:)"), + ("🥏", "🥏 (:flying_disc:)"), + ("🛸", "🛸 (:flying_saucer:)"), + ("🌫️", "🌫️ (:fog:)"), + ("🌫", "🌫 (:fog:)"), + ("🌁", "🌁 (:foggy:)"), + ("🙏", "🙏 (:folded_hands:)"), + ("🙏🏿", "🙏🏿 (:folded_hands_dark_skin_tone:)"), + ("🙏🏻", "🙏🏻 (:folded_hands_light_skin_tone:)"), + ("🙏🏾", "🙏🏾 (:folded_hands_medium-dark_skin_tone:)"), + ("🙏🏼", "🙏🏼 (:folded_hands_medium-light_skin_tone:)"), + ("🙏🏽", "🙏🏽 (:folded_hands_medium_skin_tone:)"), + ("\U0001faad", "\U0001faad (:folding_hand_fan:)"), + ("🫕", "🫕 (:fondue:)"), + ("🦶", "🦶 (:foot:)"), + ("🦶🏿", "🦶🏿 (:foot_dark_skin_tone:)"), + ("🦶🏻", "🦶🏻 (:foot_light_skin_tone:)"), + ("🦶🏾", "🦶🏾 (:foot_medium-dark_skin_tone:)"), + ("🦶🏼", "🦶🏼 (:foot_medium-light_skin_tone:)"), + ("🦶🏽", "🦶🏽 (:foot_medium_skin_tone:)"), + ("👣", "👣 (:footprints:)"), + ("🍴", "🍴 (:fork_and_knife:)"), + ("🍽️", "🍽️ (:fork_and_knife_with_plate:)"), + ("🍽", "🍽 (:fork_and_knife_with_plate:)"), + ("🥠", "🥠 (:fortune_cookie:)"), + ("⛲", "⛲ (:fountain:)"), + ("🖋️", "🖋️ (:fountain_pen:)"), + ("🖋", "🖋 (:fountain_pen:)"), + ("🕟", "🕟 (:four-thirty:)"), + ("🍀", "🍀 (:four_leaf_clover:)"), + ("🕓", "🕓 (:four_o’clock:)"), + ("🦊", "🦊 (:fox:)"), + ("🖼️", "🖼️ (:framed_picture:)"), + ("🖼", "🖼 (:framed_picture:)"), + ("🍟", "🍟 (:french_fries:)"), + ("🍤", "🍤 (:fried_shrimp:)"), + ("🐸", "🐸 (:frog:)"), + ("🐥", "🐥 (:front-facing_baby_chick:)"), + ("☹️", "☹️ (:frowning_face:)"), + ("☹", "☹ (:frowning_face:)"), + ("😦", "😦 (:frowning_face_with_open_mouth:)"), + ("⛽", "⛽ (:fuel_pump:)"), + ("🌕", "🌕 (:full_moon:)"), + ("🌝", "🌝 (:full_moon_face:)"), + ("⚱️", "⚱️ (:funeral_urn:)"), + ("⚱", "⚱ (:funeral_urn:)"), + ("🎲", "🎲 (:game_die:)"), + ("🧄", "🧄 (:garlic:)"), + ("⚙️", "⚙️ (:gear:)"), + ("⚙", "⚙ (:gear:)"), + ("💎", "💎 (:gem_stone:)"), + ("🧞", "🧞 (:genie:)"), + ("👻", "👻 (:ghost:)"), + ("\U0001fada", "\U0001fada (:ginger_root:)"), + ("🦒", "🦒 (:giraffe:)"), + ("👧", "👧 (:girl:)"), + ("👧🏿", "👧🏿 (:girl_dark_skin_tone:)"), + ("👧🏻", "👧🏻 (:girl_light_skin_tone:)"), + ("👧🏾", "👧🏾 (:girl_medium-dark_skin_tone:)"), + ("👧🏼", "👧🏼 (:girl_medium-light_skin_tone:)"), + ("👧🏽", "👧🏽 (:girl_medium_skin_tone:)"), + ("🥛", "🥛 (:glass_of_milk:)"), + ("👓", "👓 (:glasses:)"), + ("🌎", "🌎 (:globe_showing_Americas:)"), + ("🌏", "🌏 (:globe_showing_Asia-Australia:)"), + ("🌍", "🌍 (:globe_showing_Europe-Africa:)"), + ("🌐", "🌐 (:globe_with_meridians:)"), + ("🧤", "🧤 (:gloves:)"), + ("🌟", "🌟 (:glowing_star:)"), + ("🥅", "🥅 (:goal_net:)"), + ("🐐", "🐐 (:goat:)"), + ("👺", "👺 (:goblin:)"), + ("🥽", "🥽 (:goggles:)"), + ("\U0001fabf", "\U0001fabf (:goose:)"), + ("🦍", "🦍 (:gorilla:)"), + ("🎓", "🎓 (:graduation_cap:)"), + ("🍇", "🍇 (:grapes:)"), + ("🍏", "🍏 (:green_apple:)"), + ("📗", "📗 (:green_book:)"), + ("🟢", "🟢 (:green_circle:)"), + ("💚", "💚 (:green_heart:)"), + ("🥗", "🥗 (:green_salad:)"), + ("🟩", "🟩 (:green_square:)"), + ("\U0001fa76", "\U0001fa76 (:grey_heart:)"), + ("😬", "😬 (:grimacing_face:)"), + ("😺", "😺 (:grinning_cat:)"), + ("😸", "😸 (:grinning_cat_with_smiling_eyes:)"), + ("😀", "😀 (:grinning_face:)"), + ("😃", "😃 (:grinning_face_with_big_eyes:)"), + ("😄", "😄 (:grinning_face_with_smiling_eyes:)"), + ("😅", "😅 (:grinning_face_with_sweat:)"), + ("😆", "😆 (:grinning_squinting_face:)"), + ("💗", "💗 (:growing_heart:)"), + ("💂", "💂 (:guard:)"), + ("💂🏿", "💂🏿 (:guard_dark_skin_tone:)"), + ("💂🏻", "💂🏻 (:guard_light_skin_tone:)"), + ("💂🏾", "💂🏾 (:guard_medium-dark_skin_tone:)"), + ("💂🏼", "💂🏼 (:guard_medium-light_skin_tone:)"), + ("💂🏽", "💂🏽 (:guard_medium_skin_tone:)"), + ("🦮", "🦮 (:guide_dog:)"), + ("🎸", "🎸 (:guitar:)"), + ("\U0001faae", "\U0001faae (:hair_pick:)"), + ("🍔", "🍔 (:hamburger:)"), + ("🔨", "🔨 (:hammer:)"), + ("⚒️", "⚒️ (:hammer_and_pick:)"), + ("⚒", "⚒ (:hammer_and_pick:)"), + ("🛠️", "🛠️ (:hammer_and_wrench:)"), + ("🛠", "🛠 (:hammer_and_wrench:)"), + ("\U0001faac", "\U0001faac (:hamsa:)"), + ("🐹", "🐹 (:hamster:)"), + ("🖐️", "🖐️ (:hand_with_fingers_splayed:)"), + ("🖐", "🖐 (:hand_with_fingers_splayed:)"), + ("🖐🏿", "🖐🏿 (:hand_with_fingers_splayed_dark_skin_tone:)"), + ("🖐🏻", "🖐🏻 (:hand_with_fingers_splayed_light_skin_tone:)"), + ( + "🖐🏾", + "🖐🏾 (:hand_with_fingers_splayed_medium-dark_skin_tone:)", + ), + ( + "🖐🏼", + "🖐🏼 (:hand_with_fingers_splayed_medium-light_skin_tone:)", + ), + ("🖐🏽", "🖐🏽 (:hand_with_fingers_splayed_medium_skin_tone:)"), + ( + "\U0001faf0", + "\U0001faf0 (:hand_with_index_finger_and_thumb_crossed:)", + ), + ( + "\U0001faf0🏿", + "\U0001faf0🏿 (:hand_with_index_finger_and_thumb_crossed_dark_skin_tone:)", + ), + ( + "\U0001faf0🏻", + "\U0001faf0🏻 (:hand_with_index_finger_and_thumb_crossed_light_skin_tone:)", + ), + ( + "\U0001faf0🏾", + "\U0001faf0🏾 (:hand_with_index_finger_and_thumb_crossed_medium-dark_skin_tone:)", + ), + ( + "\U0001faf0🏼", + "\U0001faf0🏼 (:hand_with_index_finger_and_thumb_crossed_medium-light_skin_tone:)", + ), + ( + "\U0001faf0🏽", + "\U0001faf0🏽 (:hand_with_index_finger_and_thumb_crossed_medium_skin_tone:)", + ), + ("👜", "👜 (:handbag:)"), + ("🤝", "🤝 (:handshake:)"), + ("🤝🏿", "🤝🏿 (:handshake_dark_skin_tone:)"), + ( + "\U0001faf1🏿\u200d\U0001faf2🏻", + "\U0001faf1🏿\u200d\U0001faf2🏻 (:handshake_dark_skin_tone_light_skin_tone:)", + ), + ( + "\U0001faf1🏿\u200d\U0001faf2🏾", + "\U0001faf1🏿\u200d\U0001faf2🏾 (:handshake_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "\U0001faf1🏿\u200d\U0001faf2🏼", + "\U0001faf1🏿\u200d\U0001faf2🏼 (:handshake_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "\U0001faf1🏿\u200d\U0001faf2🏽", + "\U0001faf1🏿\u200d\U0001faf2🏽 (:handshake_dark_skin_tone_medium_skin_tone:)", + ), + ("🤝🏻", "🤝🏻 (:handshake_light_skin_tone:)"), + ( + "\U0001faf1🏻\u200d\U0001faf2🏿", + "\U0001faf1🏻\u200d\U0001faf2🏿 (:handshake_light_skin_tone_dark_skin_tone:)", + ), + ( + "\U0001faf1🏻\u200d\U0001faf2🏾", + "\U0001faf1🏻\u200d\U0001faf2🏾 (:handshake_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "\U0001faf1🏻\u200d\U0001faf2🏼", + "\U0001faf1🏻\u200d\U0001faf2🏼 (:handshake_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "\U0001faf1🏻\u200d\U0001faf2🏽", + "\U0001faf1🏻\u200d\U0001faf2🏽 (:handshake_light_skin_tone_medium_skin_tone:)", + ), + ("🤝🏾", "🤝🏾 (:handshake_medium-dark_skin_tone:)"), + ( + "\U0001faf1🏾\u200d\U0001faf2🏿", + "\U0001faf1🏾\u200d\U0001faf2🏿 (:handshake_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "\U0001faf1🏾\u200d\U0001faf2🏻", + "\U0001faf1🏾\u200d\U0001faf2🏻 (:handshake_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "\U0001faf1🏾\u200d\U0001faf2🏼", + "\U0001faf1🏾\u200d\U0001faf2🏼 (:handshake_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "\U0001faf1🏾\u200d\U0001faf2🏽", + "\U0001faf1🏾\u200d\U0001faf2🏽 (:handshake_medium-dark_skin_tone_medium_skin_tone:)", + ), + ("🤝🏼", "🤝🏼 (:handshake_medium-light_skin_tone:)"), + ( + "\U0001faf1🏼\u200d\U0001faf2🏿", + "\U0001faf1🏼\u200d\U0001faf2🏿 (:handshake_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "\U0001faf1🏼\u200d\U0001faf2🏻", + "\U0001faf1🏼\u200d\U0001faf2🏻 (:handshake_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "\U0001faf1🏼\u200d\U0001faf2🏾", + "\U0001faf1🏼\u200d\U0001faf2🏾 (:handshake_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "\U0001faf1🏼\u200d\U0001faf2🏽", + "\U0001faf1🏼\u200d\U0001faf2🏽 (:handshake_medium-light_skin_tone_medium_skin_tone:)", + ), + ("🤝🏽", "🤝🏽 (:handshake_medium_skin_tone:)"), + ( + "\U0001faf1🏽\u200d\U0001faf2🏿", + "\U0001faf1🏽\u200d\U0001faf2🏿 (:handshake_medium_skin_tone_dark_skin_tone:)", + ), + ( + "\U0001faf1🏽\u200d\U0001faf2🏻", + "\U0001faf1🏽\u200d\U0001faf2🏻 (:handshake_medium_skin_tone_light_skin_tone:)", + ), + ( + "\U0001faf1🏽\u200d\U0001faf2🏾", + "\U0001faf1🏽\u200d\U0001faf2🏾 (:handshake_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "\U0001faf1🏽\u200d\U0001faf2🏼", + "\U0001faf1🏽\u200d\U0001faf2🏼 (:handshake_medium_skin_tone_medium-light_skin_tone:)", + ), + ("🐣", "🐣 (:hatching_chick:)"), + ("🙂\u200d↔️", "🙂\u200d↔️ (:head_shaking_horizontally:)"), + ("🙂\u200d↔", "🙂\u200d↔ (:head_shaking_horizontally:)"), + ("🙂\u200d↕️", "🙂\u200d↕️ (:head_shaking_vertically:)"), + ("🙂\u200d↕", "🙂\u200d↕ (:head_shaking_vertically:)"), + ("🎧", "🎧 (:headphone:)"), + ("🪦", "🪦 (:headstone:)"), + ("🧑\u200d⚕️", "🧑\u200d⚕️ (:health_worker:)"), + ("🧑\u200d⚕", "🧑\u200d⚕ (:health_worker:)"), + ( + "🧑🏿\u200d⚕️", + "🧑🏿\u200d⚕️ (:health_worker_dark_skin_tone:)", + ), + ("🧑🏿\u200d⚕", "🧑🏿\u200d⚕ (:health_worker_dark_skin_tone:)"), + ( + "🧑🏻\u200d⚕️", + "🧑🏻\u200d⚕️ (:health_worker_light_skin_tone:)", + ), + ( + "🧑🏻\u200d⚕", + "🧑🏻\u200d⚕ (:health_worker_light_skin_tone:)", + ), + ( + "🧑🏾\u200d⚕️", + "🧑🏾\u200d⚕️ (:health_worker_medium-dark_skin_tone:)", + ), + ( + "🧑🏾\u200d⚕", + "🧑🏾\u200d⚕ (:health_worker_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d⚕️", + "🧑🏼\u200d⚕️ (:health_worker_medium-light_skin_tone:)", + ), + ( + "🧑🏼\u200d⚕", + "🧑🏼\u200d⚕ (:health_worker_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d⚕️", + "🧑🏽\u200d⚕️ (:health_worker_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d⚕", + "🧑🏽\u200d⚕ (:health_worker_medium_skin_tone:)", + ), + ("🙉", "🙉 (:hear-no-evil_monkey:)"), + ("💟", "💟 (:heart_decoration:)"), + ("❣️", "❣️ (:heart_exclamation:)"), + ("❣", "❣ (:heart_exclamation:)"), + ("\U0001faf6", "\U0001faf6 (:heart_hands:)"), + ( + "\U0001faf6🏿", + "\U0001faf6🏿 (:heart_hands_dark_skin_tone:)", + ), + ( + "\U0001faf6🏻", + "\U0001faf6🏻 (:heart_hands_light_skin_tone:)", + ), + ( + "\U0001faf6🏾", + "\U0001faf6🏾 (:heart_hands_medium-dark_skin_tone:)", + ), + ( + "\U0001faf6🏼", + "\U0001faf6🏼 (:heart_hands_medium-light_skin_tone:)", + ), + ( + "\U0001faf6🏽", + "\U0001faf6🏽 (:heart_hands_medium_skin_tone:)", + ), + ("❤️\u200d🔥", "❤️\u200d🔥 (:heart_on_fire:)"), + ("❤\u200d🔥", "❤\u200d🔥 (:heart_on_fire:)"), + ("♥️", "♥️ (:heart_suit:)"), + ("♥", "♥ (:heart_suit:)"), + ("💘", "💘 (:heart_with_arrow:)"), + ("💝", "💝 (:heart_with_ribbon:)"), + ("💲", "💲 (:heavy_dollar_sign:)"), + ("\U0001f7f0", "\U0001f7f0 (:heavy_equals_sign:)"), + ("🦔", "🦔 (:hedgehog:)"), + ("🚁", "🚁 (:helicopter:)"), + ("🌿", "🌿 (:herb:)"), + ("🌺", "🌺 (:hibiscus:)"), + ("👠", "👠 (:high-heeled_shoe:)"), + ("🚄", "🚄 (:high-speed_train:)"), + ("⚡", "⚡ (:high_voltage:)"), + ("🥾", "🥾 (:hiking_boot:)"), + ("🛕", "🛕 (:hindu_temple:)"), + ("🦛", "🦛 (:hippopotamus:)"), + ("🕳️", "🕳️ (:hole:)"), + ("🕳", "🕳 (:hole:)"), + ("⭕", "⭕ (:hollow_red_circle:)"), + ("🍯", "🍯 (:honey_pot:)"), + ("🐝", "🐝 (:honeybee:)"), + ("🪝", "🪝 (:hook:)"), + ("🚥", "🚥 (:horizontal_traffic_light:)"), + ("🐎", "🐎 (:horse:)"), + ("🐴", "🐴 (:horse_face:)"), + ("🏇", "🏇 (:horse_racing:)"), + ("🏇🏿", "🏇🏿 (:horse_racing_dark_skin_tone:)"), + ("🏇🏻", "🏇🏻 (:horse_racing_light_skin_tone:)"), + ("🏇🏾", "🏇🏾 (:horse_racing_medium-dark_skin_tone:)"), + ("🏇🏼", "🏇🏼 (:horse_racing_medium-light_skin_tone:)"), + ("🏇🏽", "🏇🏽 (:horse_racing_medium_skin_tone:)"), + ("🏥", "🏥 (:hospital:)"), + ("☕", "☕ (:hot_beverage:)"), + ("🌭", "🌭 (:hot_dog:)"), + ("🥵", "🥵 (:hot_face:)"), + ("🌶️", "🌶️ (:hot_pepper:)"), + ("🌶", "🌶 (:hot_pepper:)"), + ("♨️", "♨️ (:hot_springs:)"), + ("♨", "♨ (:hot_springs:)"), + ("🏨", "🏨 (:hotel:)"), + ("⌛", "⌛ (:hourglass_done:)"), + ("⏳", "⏳ (:hourglass_not_done:)"), + ("🏠", "🏠 (:house:)"), + ("🏡", "🏡 (:house_with_garden:)"), + ("🏘️", "🏘️ (:houses:)"), + ("🏘", "🏘 (:houses:)"), + ("💯", "💯 (:hundred_points:)"), + ("😯", "😯 (:hushed_face:)"), + ("🛖", "🛖 (:hut:)"), + ("\U0001fabb", "\U0001fabb (:hyacinth:)"), + ("🧊", "🧊 (:ice:)"), + ("🍨", "🍨 (:ice_cream:)"), + ("🏒", "🏒 (:ice_hockey:)"), + ("⛸️", "⛸️ (:ice_skate:)"), + ("⛸", "⛸ (:ice_skate:)"), + ("\U0001faaa", "\U0001faaa (:identification_card:)"), + ("📥", "📥 (:inbox_tray:)"), + ("📨", "📨 (:incoming_envelope:)"), + ( + "\U0001faf5", + "\U0001faf5 (:index_pointing_at_the_viewer:)", + ), + ( + "\U0001faf5🏿", + "\U0001faf5🏿 (:index_pointing_at_the_viewer_dark_skin_tone:)", + ), + ( + "\U0001faf5🏻", + "\U0001faf5🏻 (:index_pointing_at_the_viewer_light_skin_tone:)", + ), + ( + "\U0001faf5🏾", + "\U0001faf5🏾 (:index_pointing_at_the_viewer_medium-dark_skin_tone:)", + ), + ( + "\U0001faf5🏼", + "\U0001faf5🏼 (:index_pointing_at_the_viewer_medium-light_skin_tone:)", + ), + ( + "\U0001faf5🏽", + "\U0001faf5🏽 (:index_pointing_at_the_viewer_medium_skin_tone:)", + ), + ("☝️", "☝️ (:index_pointing_up:)"), + ("☝", "☝ (:index_pointing_up:)"), + ("☝🏿", "☝🏿 (:index_pointing_up_dark_skin_tone:)"), + ("☝🏻", "☝🏻 (:index_pointing_up_light_skin_tone:)"), + ("☝🏾", "☝🏾 (:index_pointing_up_medium-dark_skin_tone:)"), + ("☝🏼", "☝🏼 (:index_pointing_up_medium-light_skin_tone:)"), + ("☝🏽", "☝🏽 (:index_pointing_up_medium_skin_tone:)"), + ("♾️", "♾️ (:infinity:)"), + ("♾", "♾ (:infinity:)"), + ("ℹ️", "ℹ️ (:information:)"), + ("ℹ", "ℹ (:information:)"), + ("🔤", "🔤 (:input_latin_letters:)"), + ("🔡", "🔡 (:input_latin_lowercase:)"), + ("🔠", "🔠 (:input_latin_uppercase:)"), + ("🔢", "🔢 (:input_numbers:)"), + ("🔣", "🔣 (:input_symbols:)"), + ("🎃", "🎃 (:jack-o-lantern:)"), + ("\U0001fad9", "\U0001fad9 (:jar:)"), + ("👖", "👖 (:jeans:)"), + ("\U0001fabc", "\U0001fabc (:jellyfish:)"), + ("🃏", "🃏 (:joker:)"), + ("🕹️", "🕹️ (:joystick:)"), + ("🕹", "🕹 (:joystick:)"), + ("🧑\u200d⚖️", "🧑\u200d⚖️ (:judge:)"), + ("🧑\u200d⚖", "🧑\u200d⚖ (:judge:)"), + ("🧑🏿\u200d⚖️", "🧑🏿\u200d⚖️ (:judge_dark_skin_tone:)"), + ("🧑🏿\u200d⚖", "🧑🏿\u200d⚖ (:judge_dark_skin_tone:)"), + ("🧑🏻\u200d⚖️", "🧑🏻\u200d⚖️ (:judge_light_skin_tone:)"), + ("🧑🏻\u200d⚖", "🧑🏻\u200d⚖ (:judge_light_skin_tone:)"), + ( + "🧑🏾\u200d⚖️", + "🧑🏾\u200d⚖️ (:judge_medium-dark_skin_tone:)", + ), + ("🧑🏾\u200d⚖", "🧑🏾\u200d⚖ (:judge_medium-dark_skin_tone:)"), + ( + "🧑🏼\u200d⚖️", + "🧑🏼\u200d⚖️ (:judge_medium-light_skin_tone:)", + ), + ("🧑🏼\u200d⚖", "🧑🏼\u200d⚖ (:judge_medium-light_skin_tone:)"), + ("🧑🏽\u200d⚖️", "🧑🏽\u200d⚖️ (:judge_medium_skin_tone:)"), + ("🧑🏽\u200d⚖", "🧑🏽\u200d⚖ (:judge_medium_skin_tone:)"), + ("🕋", "🕋 (:kaaba:)"), + ("🦘", "🦘 (:kangaroo:)"), + ("🔑", "🔑 (:key:)"), + ("⌨️", "⌨️ (:keyboard:)"), + ("⌨", "⌨ (:keyboard:)"), + ("#️⃣", "#️⃣ (:keycap_#:)"), + ("#⃣", "#⃣ (:keycap_#:)"), + ("*️⃣", "*️⃣ (:keycap_*:)"), + ("*⃣", "*⃣ (:keycap_*:)"), + ("0️⃣", "0️⃣ (:keycap_0:)"), + ("0⃣", "0⃣ (:keycap_0:)"), + ("1️⃣", "1️⃣ (:keycap_1:)"), + ("1⃣", "1⃣ (:keycap_1:)"), + ("🔟", "🔟 (:keycap_10:)"), + ("2️⃣", "2️⃣ (:keycap_2:)"), + ("2⃣", "2⃣ (:keycap_2:)"), + ("3️⃣", "3️⃣ (:keycap_3:)"), + ("3⃣", "3⃣ (:keycap_3:)"), + ("4️⃣", "4️⃣ (:keycap_4:)"), + ("4⃣", "4⃣ (:keycap_4:)"), + ("5️⃣", "5️⃣ (:keycap_5:)"), + ("5⃣", "5⃣ (:keycap_5:)"), + ("6️⃣", "6️⃣ (:keycap_6:)"), + ("6⃣", "6⃣ (:keycap_6:)"), + ("7️⃣", "7️⃣ (:keycap_7:)"), + ("7⃣", "7⃣ (:keycap_7:)"), + ("8️⃣", "8️⃣ (:keycap_8:)"), + ("8⃣", "8⃣ (:keycap_8:)"), + ("9️⃣", "9️⃣ (:keycap_9:)"), + ("9⃣", "9⃣ (:keycap_9:)"), + ("\U0001faaf", "\U0001faaf (:khanda:)"), + ("🛴", "🛴 (:kick_scooter:)"), + ("👘", "👘 (:kimono:)"), + ("💏", "💏 (:kiss:)"), + ("💏🏿", "💏🏿 (:kiss_dark_skin_tone:)"), + ("💏🏻", "💏🏻 (:kiss_light_skin_tone:)"), + ( + "👨\u200d❤️\u200d💋\u200d👨", + "👨\u200d❤️\u200d💋\u200d👨 (:kiss_man_man:)", + ), + ( + "👨\u200d❤\u200d💋\u200d👨", + "👨\u200d❤\u200d💋\u200d👨 (:kiss_man_man:)", + ), + ( + "👨🏿\u200d❤️\u200d💋\u200d👨🏿", + "👨🏿\u200d❤️\u200d💋\u200d👨🏿 (:kiss_man_man_dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d💋\u200d👨🏿", + "👨🏿\u200d❤\u200d💋\u200d👨🏿 (:kiss_man_man_dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d💋\u200d👨🏻", + "👨🏿\u200d❤️\u200d💋\u200d👨🏻 (:kiss_man_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d💋\u200d👨🏻", + "👨🏿\u200d❤\u200d💋\u200d👨🏻 (:kiss_man_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d💋\u200d👨🏾", + "👨🏿\u200d❤️\u200d💋\u200d👨🏾 (:kiss_man_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d💋\u200d👨🏾", + "👨🏿\u200d❤\u200d💋\u200d👨🏾 (:kiss_man_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d💋\u200d👨🏼", + "👨🏿\u200d❤️\u200d💋\u200d👨🏼 (:kiss_man_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d💋\u200d👨🏼", + "👨🏿\u200d❤\u200d💋\u200d👨🏼 (:kiss_man_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏿\u200d❤️\u200d💋\u200d👨🏽", + "👨🏿\u200d❤️\u200d💋\u200d👨🏽 (:kiss_man_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏿\u200d❤\u200d💋\u200d👨🏽", + "👨🏿\u200d❤\u200d💋\u200d👨🏽 (:kiss_man_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d💋\u200d👨🏻", + "👨🏻\u200d❤️\u200d💋\u200d👨🏻 (:kiss_man_man_light_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d💋\u200d👨🏻", + "👨🏻\u200d❤\u200d💋\u200d👨🏻 (:kiss_man_man_light_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d💋\u200d👨🏿", + "👨🏻\u200d❤️\u200d💋\u200d👨🏿 (:kiss_man_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d💋\u200d👨🏿", + "👨🏻\u200d❤\u200d💋\u200d👨🏿 (:kiss_man_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d💋\u200d👨🏾", + "👨🏻\u200d❤️\u200d💋\u200d👨🏾 (:kiss_man_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d💋\u200d👨🏾", + "👨🏻\u200d❤\u200d💋\u200d👨🏾 (:kiss_man_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d💋\u200d👨🏼", + "👨🏻\u200d❤️\u200d💋\u200d👨🏼 (:kiss_man_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d💋\u200d👨🏼", + "👨🏻\u200d❤\u200d💋\u200d👨🏼 (:kiss_man_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏻\u200d❤️\u200d💋\u200d👨🏽", + "👨🏻\u200d❤️\u200d💋\u200d👨🏽 (:kiss_man_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏻\u200d❤\u200d💋\u200d👨🏽", + "👨🏻\u200d❤\u200d💋\u200d👨🏽 (:kiss_man_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d💋\u200d👨🏾", + "👨🏾\u200d❤️\u200d💋\u200d👨🏾 (:kiss_man_man_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d💋\u200d👨🏾", + "👨🏾\u200d❤\u200d💋\u200d👨🏾 (:kiss_man_man_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d💋\u200d👨🏿", + "👨🏾\u200d❤️\u200d💋\u200d👨🏿 (:kiss_man_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d💋\u200d👨🏿", + "👨🏾\u200d❤\u200d💋\u200d👨🏿 (:kiss_man_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d💋\u200d👨🏻", + "👨🏾\u200d❤️\u200d💋\u200d👨🏻 (:kiss_man_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d💋\u200d👨🏻", + "👨🏾\u200d❤\u200d💋\u200d👨🏻 (:kiss_man_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d💋\u200d👨🏼", + "👨🏾\u200d❤️\u200d💋\u200d👨🏼 (:kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d💋\u200d👨🏼", + "👨🏾\u200d❤\u200d💋\u200d👨🏼 (:kiss_man_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏾\u200d❤️\u200d💋\u200d👨🏽", + "👨🏾\u200d❤️\u200d💋\u200d👨🏽 (:kiss_man_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏾\u200d❤\u200d💋\u200d👨🏽", + "👨🏾\u200d❤\u200d💋\u200d👨🏽 (:kiss_man_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d💋\u200d👨🏼", + "👨🏼\u200d❤️\u200d💋\u200d👨🏼 (:kiss_man_man_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d💋\u200d👨🏼", + "👨🏼\u200d❤\u200d💋\u200d👨🏼 (:kiss_man_man_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d💋\u200d👨🏿", + "👨🏼\u200d❤️\u200d💋\u200d👨🏿 (:kiss_man_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d💋\u200d👨🏿", + "👨🏼\u200d❤\u200d💋\u200d👨🏿 (:kiss_man_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d💋\u200d👨🏻", + "👨🏼\u200d❤️\u200d💋\u200d👨🏻 (:kiss_man_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d💋\u200d👨🏻", + "👨🏼\u200d❤\u200d💋\u200d👨🏻 (:kiss_man_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d💋\u200d👨🏾", + "👨🏼\u200d❤️\u200d💋\u200d👨🏾 (:kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d💋\u200d👨🏾", + "👨🏼\u200d❤\u200d💋\u200d👨🏾 (:kiss_man_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d❤️\u200d💋\u200d👨🏽", + "👨🏼\u200d❤️\u200d💋\u200d👨🏽 (:kiss_man_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏼\u200d❤\u200d💋\u200d👨🏽", + "👨🏼\u200d❤\u200d💋\u200d👨🏽 (:kiss_man_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d💋\u200d👨🏽", + "👨🏽\u200d❤️\u200d💋\u200d👨🏽 (:kiss_man_man_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d💋\u200d👨🏽", + "👨🏽\u200d❤\u200d💋\u200d👨🏽 (:kiss_man_man_medium_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d💋\u200d👨🏿", + "👨🏽\u200d❤️\u200d💋\u200d👨🏿 (:kiss_man_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d💋\u200d👨🏿", + "👨🏽\u200d❤\u200d💋\u200d👨🏿 (:kiss_man_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d💋\u200d👨🏻", + "👨🏽\u200d❤️\u200d💋\u200d👨🏻 (:kiss_man_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d💋\u200d👨🏻", + "👨🏽\u200d❤\u200d💋\u200d👨🏻 (:kiss_man_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d💋\u200d👨🏾", + "👨🏽\u200d❤️\u200d💋\u200d👨🏾 (:kiss_man_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d💋\u200d👨🏾", + "👨🏽\u200d❤\u200d💋\u200d👨🏾 (:kiss_man_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏽\u200d❤️\u200d💋\u200d👨🏼", + "👨🏽\u200d❤️\u200d💋\u200d👨🏼 (:kiss_man_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d❤\u200d💋\u200d👨🏼", + "👨🏽\u200d❤\u200d💋\u200d👨🏼 (:kiss_man_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ("💋", "💋 (:kiss_mark:)"), + ("💏🏾", "💏🏾 (:kiss_medium-dark_skin_tone:)"), + ("💏🏼", "💏🏼 (:kiss_medium-light_skin_tone:)"), + ("💏🏽", "💏🏽 (:kiss_medium_skin_tone:)"), + ( + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏻", + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏻 (:kiss_person_person_dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d💋\u200d🧑🏻", + "🧑🏿\u200d❤\u200d💋\u200d🧑🏻 (:kiss_person_person_dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏾", + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏾 (:kiss_person_person_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d💋\u200d🧑🏾", + "🧑🏿\u200d❤\u200d💋\u200d🧑🏾 (:kiss_person_person_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏼", + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏼 (:kiss_person_person_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d💋\u200d🧑🏼", + "🧑🏿\u200d❤\u200d💋\u200d🧑🏼 (:kiss_person_person_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏽", + "🧑🏿\u200d❤️\u200d💋\u200d🧑🏽 (:kiss_person_person_dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏿\u200d❤\u200d💋\u200d🧑🏽", + "🧑🏿\u200d❤\u200d💋\u200d🧑🏽 (:kiss_person_person_dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏿", + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏿 (:kiss_person_person_light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d💋\u200d🧑🏿", + "🧑🏻\u200d❤\u200d💋\u200d🧑🏿 (:kiss_person_person_light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏾", + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏾 (:kiss_person_person_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d💋\u200d🧑🏾", + "🧑🏻\u200d❤\u200d💋\u200d🧑🏾 (:kiss_person_person_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏼", + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏼 (:kiss_person_person_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d💋\u200d🧑🏼", + "🧑🏻\u200d❤\u200d💋\u200d🧑🏼 (:kiss_person_person_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏽", + "🧑🏻\u200d❤️\u200d💋\u200d🧑🏽 (:kiss_person_person_light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d❤\u200d💋\u200d🧑🏽", + "🧑🏻\u200d❤\u200d💋\u200d🧑🏽 (:kiss_person_person_light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏿", + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏿 (:kiss_person_person_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d💋\u200d🧑🏿", + "🧑🏾\u200d❤\u200d💋\u200d🧑🏿 (:kiss_person_person_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏻", + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏻 (:kiss_person_person_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d💋\u200d🧑🏻", + "🧑🏾\u200d❤\u200d💋\u200d🧑🏻 (:kiss_person_person_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏼", + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏼 (:kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d💋\u200d🧑🏼", + "🧑🏾\u200d❤\u200d💋\u200d🧑🏼 (:kiss_person_person_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏽", + "🧑🏾\u200d❤️\u200d💋\u200d🧑🏽 (:kiss_person_person_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏾\u200d❤\u200d💋\u200d🧑🏽", + "🧑🏾\u200d❤\u200d💋\u200d🧑🏽 (:kiss_person_person_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏿", + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏿 (:kiss_person_person_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d💋\u200d🧑🏿", + "🧑🏼\u200d❤\u200d💋\u200d🧑🏿 (:kiss_person_person_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏻", + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏻 (:kiss_person_person_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d💋\u200d🧑🏻", + "🧑🏼\u200d❤\u200d💋\u200d🧑🏻 (:kiss_person_person_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏾", + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏾 (:kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d💋\u200d🧑🏾", + "🧑🏼\u200d❤\u200d💋\u200d🧑🏾 (:kiss_person_person_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏽", + "🧑🏼\u200d❤️\u200d💋\u200d🧑🏽 (:kiss_person_person_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏼\u200d❤\u200d💋\u200d🧑🏽", + "🧑🏼\u200d❤\u200d💋\u200d🧑🏽 (:kiss_person_person_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏿", + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏿 (:kiss_person_person_medium_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d💋\u200d🧑🏿", + "🧑🏽\u200d❤\u200d💋\u200d🧑🏿 (:kiss_person_person_medium_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏻", + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏻 (:kiss_person_person_medium_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d💋\u200d🧑🏻", + "🧑🏽\u200d❤\u200d💋\u200d🧑🏻 (:kiss_person_person_medium_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏾", + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏾 (:kiss_person_person_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d💋\u200d🧑🏾", + "🧑🏽\u200d❤\u200d💋\u200d🧑🏾 (:kiss_person_person_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏼", + "🧑🏽\u200d❤️\u200d💋\u200d🧑🏼 (:kiss_person_person_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d❤\u200d💋\u200d🧑🏼", + "🧑🏽\u200d❤\u200d💋\u200d🧑🏼 (:kiss_person_person_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩\u200d❤️\u200d💋\u200d👨", + "👩\u200d❤️\u200d💋\u200d👨 (:kiss_woman_man:)", + ), + ( + "👩\u200d❤\u200d💋\u200d👨", + "👩\u200d❤\u200d💋\u200d👨 (:kiss_woman_man:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👨🏿", + "👩🏿\u200d❤️\u200d💋\u200d👨🏿 (:kiss_woman_man_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👨🏿", + "👩🏿\u200d❤\u200d💋\u200d👨🏿 (:kiss_woman_man_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👨🏻", + "👩🏿\u200d❤️\u200d💋\u200d👨🏻 (:kiss_woman_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👨🏻", + "👩🏿\u200d❤\u200d💋\u200d👨🏻 (:kiss_woman_man_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👨🏾", + "👩🏿\u200d❤️\u200d💋\u200d👨🏾 (:kiss_woman_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👨🏾", + "👩🏿\u200d❤\u200d💋\u200d👨🏾 (:kiss_woman_man_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👨🏼", + "👩🏿\u200d❤️\u200d💋\u200d👨🏼 (:kiss_woman_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👨🏼", + "👩🏿\u200d❤\u200d💋\u200d👨🏼 (:kiss_woman_man_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👨🏽", + "👩🏿\u200d❤️\u200d💋\u200d👨🏽 (:kiss_woman_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👨🏽", + "👩🏿\u200d❤\u200d💋\u200d👨🏽 (:kiss_woman_man_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👨🏻", + "👩🏻\u200d❤️\u200d💋\u200d👨🏻 (:kiss_woman_man_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👨🏻", + "👩🏻\u200d❤\u200d💋\u200d👨🏻 (:kiss_woman_man_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👨🏿", + "👩🏻\u200d❤️\u200d💋\u200d👨🏿 (:kiss_woman_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👨🏿", + "👩🏻\u200d❤\u200d💋\u200d👨🏿 (:kiss_woman_man_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👨🏾", + "👩🏻\u200d❤️\u200d💋\u200d👨🏾 (:kiss_woman_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👨🏾", + "👩🏻\u200d❤\u200d💋\u200d👨🏾 (:kiss_woman_man_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👨🏼", + "👩🏻\u200d❤️\u200d💋\u200d👨🏼 (:kiss_woman_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👨🏼", + "👩🏻\u200d❤\u200d💋\u200d👨🏼 (:kiss_woman_man_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👨🏽", + "👩🏻\u200d❤️\u200d💋\u200d👨🏽 (:kiss_woman_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👨🏽", + "👩🏻\u200d❤\u200d💋\u200d👨🏽 (:kiss_woman_man_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👨🏾", + "👩🏾\u200d❤️\u200d💋\u200d👨🏾 (:kiss_woman_man_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👨🏾", + "👩🏾\u200d❤\u200d💋\u200d👨🏾 (:kiss_woman_man_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👨🏿", + "👩🏾\u200d❤️\u200d💋\u200d👨🏿 (:kiss_woman_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👨🏿", + "👩🏾\u200d❤\u200d💋\u200d👨🏿 (:kiss_woman_man_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👨🏻", + "👩🏾\u200d❤️\u200d💋\u200d👨🏻 (:kiss_woman_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👨🏻", + "👩🏾\u200d❤\u200d💋\u200d👨🏻 (:kiss_woman_man_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👨🏼", + "👩🏾\u200d❤️\u200d💋\u200d👨🏼 (:kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👨🏼", + "👩🏾\u200d❤\u200d💋\u200d👨🏼 (:kiss_woman_man_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👨🏽", + "👩🏾\u200d❤️\u200d💋\u200d👨🏽 (:kiss_woman_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👨🏽", + "👩🏾\u200d❤\u200d💋\u200d👨🏽 (:kiss_woman_man_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👨🏼", + "👩🏼\u200d❤️\u200d💋\u200d👨🏼 (:kiss_woman_man_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👨🏼", + "👩🏼\u200d❤\u200d💋\u200d👨🏼 (:kiss_woman_man_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👨🏿", + "👩🏼\u200d❤️\u200d💋\u200d👨🏿 (:kiss_woman_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👨🏿", + "👩🏼\u200d❤\u200d💋\u200d👨🏿 (:kiss_woman_man_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👨🏻", + "👩🏼\u200d❤️\u200d💋\u200d👨🏻 (:kiss_woman_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👨🏻", + "👩🏼\u200d❤\u200d💋\u200d👨🏻 (:kiss_woman_man_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👨🏾", + "👩🏼\u200d❤️\u200d💋\u200d👨🏾 (:kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👨🏾", + "👩🏼\u200d❤\u200d💋\u200d👨🏾 (:kiss_woman_man_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👨🏽", + "👩🏼\u200d❤️\u200d💋\u200d👨🏽 (:kiss_woman_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👨🏽", + "👩🏼\u200d❤\u200d💋\u200d👨🏽 (:kiss_woman_man_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👨🏽", + "👩🏽\u200d❤️\u200d💋\u200d👨🏽 (:kiss_woman_man_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👨🏽", + "👩🏽\u200d❤\u200d💋\u200d👨🏽 (:kiss_woman_man_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👨🏿", + "👩🏽\u200d❤️\u200d💋\u200d👨🏿 (:kiss_woman_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👨🏿", + "👩🏽\u200d❤\u200d💋\u200d👨🏿 (:kiss_woman_man_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👨🏻", + "👩🏽\u200d❤️\u200d💋\u200d👨🏻 (:kiss_woman_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👨🏻", + "👩🏽\u200d❤\u200d💋\u200d👨🏻 (:kiss_woman_man_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👨🏾", + "👩🏽\u200d❤️\u200d💋\u200d👨🏾 (:kiss_woman_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👨🏾", + "👩🏽\u200d❤\u200d💋\u200d👨🏾 (:kiss_woman_man_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👨🏼", + "👩🏽\u200d❤️\u200d💋\u200d👨🏼 (:kiss_woman_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👨🏼", + "👩🏽\u200d❤\u200d💋\u200d👨🏼 (:kiss_woman_man_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩\u200d❤️\u200d💋\u200d👩", + "👩\u200d❤️\u200d💋\u200d👩 (:kiss_woman_woman:)", + ), + ( + "👩\u200d❤\u200d💋\u200d👩", + "👩\u200d❤\u200d💋\u200d👩 (:kiss_woman_woman:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👩🏿", + "👩🏿\u200d❤️\u200d💋\u200d👩🏿 (:kiss_woman_woman_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👩🏿", + "👩🏿\u200d❤\u200d💋\u200d👩🏿 (:kiss_woman_woman_dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👩🏻", + "👩🏿\u200d❤️\u200d💋\u200d👩🏻 (:kiss_woman_woman_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👩🏻", + "👩🏿\u200d❤\u200d💋\u200d👩🏻 (:kiss_woman_woman_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👩🏾", + "👩🏿\u200d❤️\u200d💋\u200d👩🏾 (:kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👩🏾", + "👩🏿\u200d❤\u200d💋\u200d👩🏾 (:kiss_woman_woman_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👩🏼", + "👩🏿\u200d❤️\u200d💋\u200d👩🏼 (:kiss_woman_woman_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👩🏼", + "👩🏿\u200d❤\u200d💋\u200d👩🏼 (:kiss_woman_woman_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d❤️\u200d💋\u200d👩🏽", + "👩🏿\u200d❤️\u200d💋\u200d👩🏽 (:kiss_woman_woman_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏿\u200d❤\u200d💋\u200d👩🏽", + "👩🏿\u200d❤\u200d💋\u200d👩🏽 (:kiss_woman_woman_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👩🏻", + "👩🏻\u200d❤️\u200d💋\u200d👩🏻 (:kiss_woman_woman_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👩🏻", + "👩🏻\u200d❤\u200d💋\u200d👩🏻 (:kiss_woman_woman_light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👩🏿", + "👩🏻\u200d❤️\u200d💋\u200d👩🏿 (:kiss_woman_woman_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👩🏿", + "👩🏻\u200d❤\u200d💋\u200d👩🏿 (:kiss_woman_woman_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👩🏾", + "👩🏻\u200d❤️\u200d💋\u200d👩🏾 (:kiss_woman_woman_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👩🏾", + "👩🏻\u200d❤\u200d💋\u200d👩🏾 (:kiss_woman_woman_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👩🏼", + "👩🏻\u200d❤️\u200d💋\u200d👩🏼 (:kiss_woman_woman_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👩🏼", + "👩🏻\u200d❤\u200d💋\u200d👩🏼 (:kiss_woman_woman_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d❤️\u200d💋\u200d👩🏽", + "👩🏻\u200d❤️\u200d💋\u200d👩🏽 (:kiss_woman_woman_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏻\u200d❤\u200d💋\u200d👩🏽", + "👩🏻\u200d❤\u200d💋\u200d👩🏽 (:kiss_woman_woman_light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👩🏾", + "👩🏾\u200d❤️\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👩🏾", + "👩🏾\u200d❤\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👩🏿", + "👩🏾\u200d❤️\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👩🏿", + "👩🏾\u200d❤\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👩🏻", + "👩🏾\u200d❤️\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👩🏻", + "👩🏾\u200d❤\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👩🏼", + "👩🏾\u200d❤️\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👩🏼", + "👩🏾\u200d❤\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d❤️\u200d💋\u200d👩🏽", + "👩🏾\u200d❤️\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏾\u200d❤\u200d💋\u200d👩🏽", + "👩🏾\u200d❤\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👩🏼", + "👩🏼\u200d❤️\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👩🏼", + "👩🏼\u200d❤\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👩🏿", + "👩🏼\u200d❤️\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👩🏿", + "👩🏼\u200d❤\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👩🏻", + "👩🏼\u200d❤️\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👩🏻", + "👩🏼\u200d❤\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👩🏾", + "👩🏼\u200d❤️\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👩🏾", + "👩🏼\u200d❤\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d❤️\u200d💋\u200d👩🏽", + "👩🏼\u200d❤️\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏼\u200d❤\u200d💋\u200d👩🏽", + "👩🏼\u200d❤\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👩🏽", + "👩🏽\u200d❤️\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👩🏽", + "👩🏽\u200d❤\u200d💋\u200d👩🏽 (:kiss_woman_woman_medium_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👩🏿", + "👩🏽\u200d❤️\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👩🏿", + "👩🏽\u200d❤\u200d💋\u200d👩🏿 (:kiss_woman_woman_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👩🏻", + "👩🏽\u200d❤️\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👩🏻", + "👩🏽\u200d❤\u200d💋\u200d👩🏻 (:kiss_woman_woman_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👩🏾", + "👩🏽\u200d❤️\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👩🏾", + "👩🏽\u200d❤\u200d💋\u200d👩🏾 (:kiss_woman_woman_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d❤️\u200d💋\u200d👩🏼", + "👩🏽\u200d❤️\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d❤\u200d💋\u200d👩🏼", + "👩🏽\u200d❤\u200d💋\u200d👩🏼 (:kiss_woman_woman_medium_skin_tone_medium-light_skin_tone:)", + ), + ("😽", "😽 (:kissing_cat:)"), + ("😗", "😗 (:kissing_face:)"), + ("😚", "😚 (:kissing_face_with_closed_eyes:)"), + ("😙", "😙 (:kissing_face_with_smiling_eyes:)"), + ("🔪", "🔪 (:kitchen_knife:)"), + ("🪁", "🪁 (:kite:)"), + ("🥝", "🥝 (:kiwi_fruit:)"), + ("🪢", "🪢 (:knot:)"), + ("🐨", "🐨 (:koala:)"), + ("🥼", "🥼 (:lab_coat:)"), + ("🏷️", "🏷️ (:label:)"), + ("🏷", "🏷 (:label:)"), + ("🥍", "🥍 (:lacrosse:)"), + ("🪜", "🪜 (:ladder:)"), + ("🐞", "🐞 (:lady_beetle:)"), + ("💻", "💻 (:laptop:)"), + ("🔷", "🔷 (:large_blue_diamond:)"), + ("🔶", "🔶 (:large_orange_diamond:)"), + ("🌗", "🌗 (:last_quarter_moon:)"), + ("🌜", "🌜 (:last_quarter_moon_face:)"), + ("⏮️", "⏮️ (:last_track_button:)"), + ("⏮", "⏮ (:last_track_button:)"), + ("✝️", "✝️ (:latin_cross:)"), + ("✝", "✝ (:latin_cross:)"), + ("🍃", "🍃 (:leaf_fluttering_in_wind:)"), + ("🥬", "🥬 (:leafy_green:)"), + ("📒", "📒 (:ledger:)"), + ("🤛", "🤛 (:left-facing_fist:)"), + ("🤛🏿", "🤛🏿 (:left-facing_fist_dark_skin_tone:)"), + ("🤛🏻", "🤛🏻 (:left-facing_fist_light_skin_tone:)"), + ("🤛🏾", "🤛🏾 (:left-facing_fist_medium-dark_skin_tone:)"), + ("🤛🏼", "🤛🏼 (:left-facing_fist_medium-light_skin_tone:)"), + ("🤛🏽", "🤛🏽 (:left-facing_fist_medium_skin_tone:)"), + ("↔️", "↔️ (:left-right_arrow:)"), + ("↔", "↔ (:left-right_arrow:)"), + ("⬅️", "⬅️ (:left_arrow:)"), + ("⬅", "⬅ (:left_arrow:)"), + ("↪️", "↪️ (:left_arrow_curving_right:)"), + ("↪", "↪ (:left_arrow_curving_right:)"), + ("🛅", "🛅 (:left_luggage:)"), + ("🗨️", "🗨️ (:left_speech_bubble:)"), + ("🗨", "🗨 (:left_speech_bubble:)"), + ("\U0001faf2", "\U0001faf2 (:leftwards_hand:)"), + ( + "\U0001faf2🏿", + "\U0001faf2🏿 (:leftwards_hand_dark_skin_tone:)", + ), + ( + "\U0001faf2🏻", + "\U0001faf2🏻 (:leftwards_hand_light_skin_tone:)", + ), + ( + "\U0001faf2🏾", + "\U0001faf2🏾 (:leftwards_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf2🏼", + "\U0001faf2🏼 (:leftwards_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf2🏽", + "\U0001faf2🏽 (:leftwards_hand_medium_skin_tone:)", + ), + ("\U0001faf7", "\U0001faf7 (:leftwards_pushing_hand:)"), + ( + "\U0001faf7🏿", + "\U0001faf7🏿 (:leftwards_pushing_hand_dark_skin_tone:)", + ), + ( + "\U0001faf7🏻", + "\U0001faf7🏻 (:leftwards_pushing_hand_light_skin_tone:)", + ), + ( + "\U0001faf7🏾", + "\U0001faf7🏾 (:leftwards_pushing_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf7🏼", + "\U0001faf7🏼 (:leftwards_pushing_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf7🏽", + "\U0001faf7🏽 (:leftwards_pushing_hand_medium_skin_tone:)", + ), + ("🦵", "🦵 (:leg:)"), + ("🦵🏿", "🦵🏿 (:leg_dark_skin_tone:)"), + ("🦵🏻", "🦵🏻 (:leg_light_skin_tone:)"), + ("🦵🏾", "🦵🏾 (:leg_medium-dark_skin_tone:)"), + ("🦵🏼", "🦵🏼 (:leg_medium-light_skin_tone:)"), + ("🦵🏽", "🦵🏽 (:leg_medium_skin_tone:)"), + ("🍋", "🍋 (:lemon:)"), + ("🐆", "🐆 (:leopard:)"), + ("🎚️", "🎚️ (:level_slider:)"), + ("🎚", "🎚 (:level_slider:)"), + ("\U0001fa75", "\U0001fa75 (:light_blue_heart:)"), + ("💡", "💡 (:light_bulb:)"), + ("🚈", "🚈 (:light_rail:)"), + ("🏻", "🏻 (:light_skin_tone:)"), + ("🍋\u200d🟩", "🍋\u200d🟩 (:lime:)"), + ("🔗", "🔗 (:link:)"), + ("🖇️", "🖇️ (:linked_paperclips:)"), + ("🖇", "🖇 (:linked_paperclips:)"), + ("🦁", "🦁 (:lion:)"), + ("💄", "💄 (:lipstick:)"), + ("🚮", "🚮 (:litter_in_bin_sign:)"), + ("🦎", "🦎 (:lizard:)"), + ("🦙", "🦙 (:llama:)"), + ("🦞", "🦞 (:lobster:)"), + ("🔒", "🔒 (:locked:)"), + ("🔐", "🔐 (:locked_with_key:)"), + ("🔏", "🔏 (:locked_with_pen:)"), + ("🚂", "🚂 (:locomotive:)"), + ("🍭", "🍭 (:lollipop:)"), + ("🪘", "🪘 (:long_drum:)"), + ("🧴", "🧴 (:lotion_bottle:)"), + ("\U0001fab7", "\U0001fab7 (:lotus:)"), + ("😭", "😭 (:loudly_crying_face:)"), + ("📢", "📢 (:loudspeaker:)"), + ("🤟", "🤟 (:love-you_gesture:)"), + ("🤟🏿", "🤟🏿 (:love-you_gesture_dark_skin_tone:)"), + ("🤟🏻", "🤟🏻 (:love-you_gesture_light_skin_tone:)"), + ("🤟🏾", "🤟🏾 (:love-you_gesture_medium-dark_skin_tone:)"), + ("🤟🏼", "🤟🏼 (:love-you_gesture_medium-light_skin_tone:)"), + ("🤟🏽", "🤟🏽 (:love-you_gesture_medium_skin_tone:)"), + ("🏩", "🏩 (:love_hotel:)"), + ("💌", "💌 (:love_letter:)"), + ("\U0001faab", "\U0001faab (:low_battery:)"), + ("🧳", "🧳 (:luggage:)"), + ("🫁", "🫁 (:lungs:)"), + ("🤥", "🤥 (:lying_face:)"), + ("🧙", "🧙 (:mage:)"), + ("🧙🏿", "🧙🏿 (:mage_dark_skin_tone:)"), + ("🧙🏻", "🧙🏻 (:mage_light_skin_tone:)"), + ("🧙🏾", "🧙🏾 (:mage_medium-dark_skin_tone:)"), + ("🧙🏼", "🧙🏼 (:mage_medium-light_skin_tone:)"), + ("🧙🏽", "🧙🏽 (:mage_medium_skin_tone:)"), + ("🪄", "🪄 (:magic_wand:)"), + ("🧲", "🧲 (:magnet:)"), + ("🔍", "🔍 (:magnifying_glass_tilted_left:)"), + ("🔎", "🔎 (:magnifying_glass_tilted_right:)"), + ("🀄", "🀄 (:mahjong_red_dragon:)"), + ("♂️", "♂️ (:male_sign:)"), + ("♂", "♂ (:male_sign:)"), + ("🦣", "🦣 (:mammoth:)"), + ("👨", "👨 (:man:)"), + ("👨\u200d🎨", "👨\u200d🎨 (:man_artist:)"), + ("👨🏿\u200d🎨", "👨🏿\u200d🎨 (:man_artist_dark_skin_tone:)"), + ("👨🏻\u200d🎨", "👨🏻\u200d🎨 (:man_artist_light_skin_tone:)"), + ( + "👨🏾\u200d🎨", + "👨🏾\u200d🎨 (:man_artist_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🎨", + "👨🏼\u200d🎨 (:man_artist_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🎨", "👨🏽\u200d🎨 (:man_artist_medium_skin_tone:)"), + ("👨\u200d🚀", "👨\u200d🚀 (:man_astronaut:)"), + ("👨🏿\u200d🚀", "👨🏿\u200d🚀 (:man_astronaut_dark_skin_tone:)"), + ( + "👨🏻\u200d🚀", + "👨🏻\u200d🚀 (:man_astronaut_light_skin_tone:)", + ), + ( + "👨🏾\u200d🚀", + "👨🏾\u200d🚀 (:man_astronaut_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🚀", + "👨🏼\u200d🚀 (:man_astronaut_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🚀", + "👨🏽\u200d🚀 (:man_astronaut_medium_skin_tone:)", + ), + ("👨\u200d🦲", "👨\u200d🦲 (:man_bald:)"), + ("🧔\u200d♂️", "🧔\u200d♂️ (:man_beard:)"), + ("🧔\u200d♂", "🧔\u200d♂ (:man_beard:)"), + ("🚴\u200d♂️", "🚴\u200d♂️ (:man_biking:)"), + ("🚴\u200d♂", "🚴\u200d♂ (:man_biking:)"), + ("🚴🏿\u200d♂️", "🚴🏿\u200d♂️ (:man_biking_dark_skin_tone:)"), + ("🚴🏿\u200d♂", "🚴🏿\u200d♂ (:man_biking_dark_skin_tone:)"), + ("🚴🏻\u200d♂️", "🚴🏻\u200d♂️ (:man_biking_light_skin_tone:)"), + ("🚴🏻\u200d♂", "🚴🏻\u200d♂ (:man_biking_light_skin_tone:)"), + ( + "🚴🏾\u200d♂️", + "🚴🏾\u200d♂️ (:man_biking_medium-dark_skin_tone:)", + ), + ( + "🚴🏾\u200d♂", + "🚴🏾\u200d♂ (:man_biking_medium-dark_skin_tone:)", + ), + ( + "🚴🏼\u200d♂️", + "🚴🏼\u200d♂️ (:man_biking_medium-light_skin_tone:)", + ), + ( + "🚴🏼\u200d♂", + "🚴🏼\u200d♂ (:man_biking_medium-light_skin_tone:)", + ), + ( + "🚴🏽\u200d♂️", + "🚴🏽\u200d♂️ (:man_biking_medium_skin_tone:)", + ), + ("🚴🏽\u200d♂", "🚴🏽\u200d♂ (:man_biking_medium_skin_tone:)"), + ("👱\u200d♂️", "👱\u200d♂️ (:man_blond_hair:)"), + ("👱\u200d♂", "👱\u200d♂ (:man_blond_hair:)"), + ("⛹️\u200d♂️", "⛹️\u200d♂️ (:man_bouncing_ball:)"), + ("⛹\u200d♂️", "⛹\u200d♂️ (:man_bouncing_ball:)"), + ("⛹️\u200d♂", "⛹️\u200d♂ (:man_bouncing_ball:)"), + ("⛹\u200d♂", "⛹\u200d♂ (:man_bouncing_ball:)"), + ( + "⛹🏿\u200d♂️", + "⛹🏿\u200d♂️ (:man_bouncing_ball_dark_skin_tone:)", + ), + ( + "⛹🏿\u200d♂", + "⛹🏿\u200d♂ (:man_bouncing_ball_dark_skin_tone:)", + ), + ( + "⛹🏻\u200d♂️", + "⛹🏻\u200d♂️ (:man_bouncing_ball_light_skin_tone:)", + ), + ( + "⛹🏻\u200d♂", + "⛹🏻\u200d♂ (:man_bouncing_ball_light_skin_tone:)", + ), + ( + "⛹🏾\u200d♂️", + "⛹🏾\u200d♂️ (:man_bouncing_ball_medium-dark_skin_tone:)", + ), + ( + "⛹🏾\u200d♂", + "⛹🏾\u200d♂ (:man_bouncing_ball_medium-dark_skin_tone:)", + ), + ( + "⛹🏼\u200d♂️", + "⛹🏼\u200d♂️ (:man_bouncing_ball_medium-light_skin_tone:)", + ), + ( + "⛹🏼\u200d♂", + "⛹🏼\u200d♂ (:man_bouncing_ball_medium-light_skin_tone:)", + ), + ( + "⛹🏽\u200d♂️", + "⛹🏽\u200d♂️ (:man_bouncing_ball_medium_skin_tone:)", + ), + ( + "⛹🏽\u200d♂", + "⛹🏽\u200d♂ (:man_bouncing_ball_medium_skin_tone:)", + ), + ("🙇\u200d♂️", "🙇\u200d♂️ (:man_bowing:)"), + ("🙇\u200d♂", "🙇\u200d♂ (:man_bowing:)"), + ("🙇🏿\u200d♂️", "🙇🏿\u200d♂️ (:man_bowing_dark_skin_tone:)"), + ("🙇🏿\u200d♂", "🙇🏿\u200d♂ (:man_bowing_dark_skin_tone:)"), + ("🙇🏻\u200d♂️", "🙇🏻\u200d♂️ (:man_bowing_light_skin_tone:)"), + ("🙇🏻\u200d♂", "🙇🏻\u200d♂ (:man_bowing_light_skin_tone:)"), + ( + "🙇🏾\u200d♂️", + "🙇🏾\u200d♂️ (:man_bowing_medium-dark_skin_tone:)", + ), + ( + "🙇🏾\u200d♂", + "🙇🏾\u200d♂ (:man_bowing_medium-dark_skin_tone:)", + ), + ( + "🙇🏼\u200d♂️", + "🙇🏼\u200d♂️ (:man_bowing_medium-light_skin_tone:)", + ), + ( + "🙇🏼\u200d♂", + "🙇🏼\u200d♂ (:man_bowing_medium-light_skin_tone:)", + ), + ( + "🙇🏽\u200d♂️", + "🙇🏽\u200d♂️ (:man_bowing_medium_skin_tone:)", + ), + ("🙇🏽\u200d♂", "🙇🏽\u200d♂ (:man_bowing_medium_skin_tone:)"), + ("🤸\u200d♂️", "🤸\u200d♂️ (:man_cartwheeling:)"), + ("🤸\u200d♂", "🤸\u200d♂ (:man_cartwheeling:)"), + ( + "🤸🏿\u200d♂️", + "🤸🏿\u200d♂️ (:man_cartwheeling_dark_skin_tone:)", + ), + ( + "🤸🏿\u200d♂", + "🤸🏿\u200d♂ (:man_cartwheeling_dark_skin_tone:)", + ), + ( + "🤸🏻\u200d♂️", + "🤸🏻\u200d♂️ (:man_cartwheeling_light_skin_tone:)", + ), + ( + "🤸🏻\u200d♂", + "🤸🏻\u200d♂ (:man_cartwheeling_light_skin_tone:)", + ), + ( + "🤸🏾\u200d♂️", + "🤸🏾\u200d♂️ (:man_cartwheeling_medium-dark_skin_tone:)", + ), + ( + "🤸🏾\u200d♂", + "🤸🏾\u200d♂ (:man_cartwheeling_medium-dark_skin_tone:)", + ), + ( + "🤸🏼\u200d♂️", + "🤸🏼\u200d♂️ (:man_cartwheeling_medium-light_skin_tone:)", + ), + ( + "🤸🏼\u200d♂", + "🤸🏼\u200d♂ (:man_cartwheeling_medium-light_skin_tone:)", + ), + ( + "🤸🏽\u200d♂️", + "🤸🏽\u200d♂️ (:man_cartwheeling_medium_skin_tone:)", + ), + ( + "🤸🏽\u200d♂", + "🤸🏽\u200d♂ (:man_cartwheeling_medium_skin_tone:)", + ), + ("🧗\u200d♂️", "🧗\u200d♂️ (:man_climbing:)"), + ("🧗\u200d♂", "🧗\u200d♂ (:man_climbing:)"), + ( + "🧗🏿\u200d♂️", + "🧗🏿\u200d♂️ (:man_climbing_dark_skin_tone:)", + ), + ("🧗🏿\u200d♂", "🧗🏿\u200d♂ (:man_climbing_dark_skin_tone:)"), + ( + "🧗🏻\u200d♂️", + "🧗🏻\u200d♂️ (:man_climbing_light_skin_tone:)", + ), + ("🧗🏻\u200d♂", "🧗🏻\u200d♂ (:man_climbing_light_skin_tone:)"), + ( + "🧗🏾\u200d♂️", + "🧗🏾\u200d♂️ (:man_climbing_medium-dark_skin_tone:)", + ), + ( + "🧗🏾\u200d♂", + "🧗🏾\u200d♂ (:man_climbing_medium-dark_skin_tone:)", + ), + ( + "🧗🏼\u200d♂️", + "🧗🏼\u200d♂️ (:man_climbing_medium-light_skin_tone:)", + ), + ( + "🧗🏼\u200d♂", + "🧗🏼\u200d♂ (:man_climbing_medium-light_skin_tone:)", + ), + ( + "🧗🏽\u200d♂️", + "🧗🏽\u200d♂️ (:man_climbing_medium_skin_tone:)", + ), + ( + "🧗🏽\u200d♂", + "🧗🏽\u200d♂ (:man_climbing_medium_skin_tone:)", + ), + ("👷\u200d♂️", "👷\u200d♂️ (:man_construction_worker:)"), + ("👷\u200d♂", "👷\u200d♂ (:man_construction_worker:)"), + ( + "👷🏿\u200d♂️", + "👷🏿\u200d♂️ (:man_construction_worker_dark_skin_tone:)", + ), + ( + "👷🏿\u200d♂", + "👷🏿\u200d♂ (:man_construction_worker_dark_skin_tone:)", + ), + ( + "👷🏻\u200d♂️", + "👷🏻\u200d♂️ (:man_construction_worker_light_skin_tone:)", + ), + ( + "👷🏻\u200d♂", + "👷🏻\u200d♂ (:man_construction_worker_light_skin_tone:)", + ), + ( + "👷🏾\u200d♂️", + "👷🏾\u200d♂️ (:man_construction_worker_medium-dark_skin_tone:)", + ), + ( + "👷🏾\u200d♂", + "👷🏾\u200d♂ (:man_construction_worker_medium-dark_skin_tone:)", + ), + ( + "👷🏼\u200d♂️", + "👷🏼\u200d♂️ (:man_construction_worker_medium-light_skin_tone:)", + ), + ( + "👷🏼\u200d♂", + "👷🏼\u200d♂ (:man_construction_worker_medium-light_skin_tone:)", + ), + ( + "👷🏽\u200d♂️", + "👷🏽\u200d♂️ (:man_construction_worker_medium_skin_tone:)", + ), + ( + "👷🏽\u200d♂", + "👷🏽\u200d♂ (:man_construction_worker_medium_skin_tone:)", + ), + ("👨\u200d🍳", "👨\u200d🍳 (:man_cook:)"), + ("👨🏿\u200d🍳", "👨🏿\u200d🍳 (:man_cook_dark_skin_tone:)"), + ("👨🏻\u200d🍳", "👨🏻\u200d🍳 (:man_cook_light_skin_tone:)"), + ( + "👨🏾\u200d🍳", + "👨🏾\u200d🍳 (:man_cook_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🍳", + "👨🏼\u200d🍳 (:man_cook_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🍳", "👨🏽\u200d🍳 (:man_cook_medium_skin_tone:)"), + ("👨\u200d🦱", "👨\u200d🦱 (:man_curly_hair:)"), + ("🕺", "🕺 (:man_dancing:)"), + ("🕺🏿", "🕺🏿 (:man_dancing_dark_skin_tone:)"), + ("🕺🏻", "🕺🏻 (:man_dancing_light_skin_tone:)"), + ("🕺🏾", "🕺🏾 (:man_dancing_medium-dark_skin_tone:)"), + ("🕺🏼", "🕺🏼 (:man_dancing_medium-light_skin_tone:)"), + ("🕺🏽", "🕺🏽 (:man_dancing_medium_skin_tone:)"), + ("👨🏿", "👨🏿 (:man_dark_skin_tone:)"), + ("👨🏿\u200d🦲", "👨🏿\u200d🦲 (:man_dark_skin_tone_bald:)"), + ("🧔🏿\u200d♂️", "🧔🏿\u200d♂️ (:man_dark_skin_tone_beard:)"), + ("🧔🏿\u200d♂", "🧔🏿\u200d♂ (:man_dark_skin_tone_beard:)"), + ( + "👱🏿\u200d♂️", + "👱🏿\u200d♂️ (:man_dark_skin_tone_blond_hair:)", + ), + ( + "👱🏿\u200d♂", + "👱🏿\u200d♂ (:man_dark_skin_tone_blond_hair:)", + ), + ( + "👨🏿\u200d🦱", + "👨🏿\u200d🦱 (:man_dark_skin_tone_curly_hair:)", + ), + ("👨🏿\u200d🦰", "👨🏿\u200d🦰 (:man_dark_skin_tone_red_hair:)"), + ( + "👨🏿\u200d🦳", + "👨🏿\u200d🦳 (:man_dark_skin_tone_white_hair:)", + ), + ("🕵️\u200d♂️", "🕵️\u200d♂️ (:man_detective:)"), + ("🕵\u200d♂️", "🕵\u200d♂️ (:man_detective:)"), + ("🕵️\u200d♂", "🕵️\u200d♂ (:man_detective:)"), + ("🕵\u200d♂", "🕵\u200d♂ (:man_detective:)"), + ( + "🕵🏿\u200d♂️", + "🕵🏿\u200d♂️ (:man_detective_dark_skin_tone:)", + ), + ("🕵🏿\u200d♂", "🕵🏿\u200d♂ (:man_detective_dark_skin_tone:)"), + ( + "🕵🏻\u200d♂️", + "🕵🏻\u200d♂️ (:man_detective_light_skin_tone:)", + ), + ( + "🕵🏻\u200d♂", + "🕵🏻\u200d♂ (:man_detective_light_skin_tone:)", + ), + ( + "🕵🏾\u200d♂️", + "🕵🏾\u200d♂️ (:man_detective_medium-dark_skin_tone:)", + ), + ( + "🕵🏾\u200d♂", + "🕵🏾\u200d♂ (:man_detective_medium-dark_skin_tone:)", + ), + ( + "🕵🏼\u200d♂️", + "🕵🏼\u200d♂️ (:man_detective_medium-light_skin_tone:)", + ), + ( + "🕵🏼\u200d♂", + "🕵🏼\u200d♂ (:man_detective_medium-light_skin_tone:)", + ), + ( + "🕵🏽\u200d♂️", + "🕵🏽\u200d♂️ (:man_detective_medium_skin_tone:)", + ), + ( + "🕵🏽\u200d♂", + "🕵🏽\u200d♂ (:man_detective_medium_skin_tone:)", + ), + ("🧝\u200d♂️", "🧝\u200d♂️ (:man_elf:)"), + ("🧝\u200d♂", "🧝\u200d♂ (:man_elf:)"), + ("🧝🏿\u200d♂️", "🧝🏿\u200d♂️ (:man_elf_dark_skin_tone:)"), + ("🧝🏿\u200d♂", "🧝🏿\u200d♂ (:man_elf_dark_skin_tone:)"), + ("🧝🏻\u200d♂️", "🧝🏻\u200d♂️ (:man_elf_light_skin_tone:)"), + ("🧝🏻\u200d♂", "🧝🏻\u200d♂ (:man_elf_light_skin_tone:)"), + ( + "🧝🏾\u200d♂️", + "🧝🏾\u200d♂️ (:man_elf_medium-dark_skin_tone:)", + ), + ( + "🧝🏾\u200d♂", + "🧝🏾\u200d♂ (:man_elf_medium-dark_skin_tone:)", + ), + ( + "🧝🏼\u200d♂️", + "🧝🏼\u200d♂️ (:man_elf_medium-light_skin_tone:)", + ), + ( + "🧝🏼\u200d♂", + "🧝🏼\u200d♂ (:man_elf_medium-light_skin_tone:)", + ), + ("🧝🏽\u200d♂️", "🧝🏽\u200d♂️ (:man_elf_medium_skin_tone:)"), + ("🧝🏽\u200d♂", "🧝🏽\u200d♂ (:man_elf_medium_skin_tone:)"), + ("🤦\u200d♂️", "🤦\u200d♂️ (:man_facepalming:)"), + ("🤦\u200d♂", "🤦\u200d♂ (:man_facepalming:)"), + ( + "🤦🏿\u200d♂️", + "🤦🏿\u200d♂️ (:man_facepalming_dark_skin_tone:)", + ), + ( + "🤦🏿\u200d♂", + "🤦🏿\u200d♂ (:man_facepalming_dark_skin_tone:)", + ), + ( + "🤦🏻\u200d♂️", + "🤦🏻\u200d♂️ (:man_facepalming_light_skin_tone:)", + ), + ( + "🤦🏻\u200d♂", + "🤦🏻\u200d♂ (:man_facepalming_light_skin_tone:)", + ), + ( + "🤦🏾\u200d♂️", + "🤦🏾\u200d♂️ (:man_facepalming_medium-dark_skin_tone:)", + ), + ( + "🤦🏾\u200d♂", + "🤦🏾\u200d♂ (:man_facepalming_medium-dark_skin_tone:)", + ), + ( + "🤦🏼\u200d♂️", + "🤦🏼\u200d♂️ (:man_facepalming_medium-light_skin_tone:)", + ), + ( + "🤦🏼\u200d♂", + "🤦🏼\u200d♂ (:man_facepalming_medium-light_skin_tone:)", + ), + ( + "🤦🏽\u200d♂️", + "🤦🏽\u200d♂️ (:man_facepalming_medium_skin_tone:)", + ), + ( + "🤦🏽\u200d♂", + "🤦🏽\u200d♂ (:man_facepalming_medium_skin_tone:)", + ), + ("👨\u200d🏭", "👨\u200d🏭 (:man_factory_worker:)"), + ( + "👨🏿\u200d🏭", + "👨🏿\u200d🏭 (:man_factory_worker_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🏭", + "👨🏻\u200d🏭 (:man_factory_worker_light_skin_tone:)", + ), + ( + "👨🏾\u200d🏭", + "👨🏾\u200d🏭 (:man_factory_worker_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🏭", + "👨🏼\u200d🏭 (:man_factory_worker_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🏭", + "👨🏽\u200d🏭 (:man_factory_worker_medium_skin_tone:)", + ), + ("🧚\u200d♂️", "🧚\u200d♂️ (:man_fairy:)"), + ("🧚\u200d♂", "🧚\u200d♂ (:man_fairy:)"), + ("🧚🏿\u200d♂️", "🧚🏿\u200d♂️ (:man_fairy_dark_skin_tone:)"), + ("🧚🏿\u200d♂", "🧚🏿\u200d♂ (:man_fairy_dark_skin_tone:)"), + ("🧚🏻\u200d♂️", "🧚🏻\u200d♂️ (:man_fairy_light_skin_tone:)"), + ("🧚🏻\u200d♂", "🧚🏻\u200d♂ (:man_fairy_light_skin_tone:)"), + ( + "🧚🏾\u200d♂️", + "🧚🏾\u200d♂️ (:man_fairy_medium-dark_skin_tone:)", + ), + ( + "🧚🏾\u200d♂", + "🧚🏾\u200d♂ (:man_fairy_medium-dark_skin_tone:)", + ), + ( + "🧚🏼\u200d♂️", + "🧚🏼\u200d♂️ (:man_fairy_medium-light_skin_tone:)", + ), + ( + "🧚🏼\u200d♂", + "🧚🏼\u200d♂ (:man_fairy_medium-light_skin_tone:)", + ), + ("🧚🏽\u200d♂️", "🧚🏽\u200d♂️ (:man_fairy_medium_skin_tone:)"), + ("🧚🏽\u200d♂", "🧚🏽\u200d♂ (:man_fairy_medium_skin_tone:)"), + ("👨\u200d🌾", "👨\u200d🌾 (:man_farmer:)"), + ("👨🏿\u200d🌾", "👨🏿\u200d🌾 (:man_farmer_dark_skin_tone:)"), + ("👨🏻\u200d🌾", "👨🏻\u200d🌾 (:man_farmer_light_skin_tone:)"), + ( + "👨🏾\u200d🌾", + "👨🏾\u200d🌾 (:man_farmer_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🌾", + "👨🏼\u200d🌾 (:man_farmer_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🌾", "👨🏽\u200d🌾 (:man_farmer_medium_skin_tone:)"), + ("👨\u200d🍼", "👨\u200d🍼 (:man_feeding_baby:)"), + ( + "👨🏿\u200d🍼", + "👨🏿\u200d🍼 (:man_feeding_baby_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🍼", + "👨🏻\u200d🍼 (:man_feeding_baby_light_skin_tone:)", + ), + ( + "👨🏾\u200d🍼", + "👨🏾\u200d🍼 (:man_feeding_baby_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🍼", + "👨🏼\u200d🍼 (:man_feeding_baby_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🍼", + "👨🏽\u200d🍼 (:man_feeding_baby_medium_skin_tone:)", + ), + ("👨\u200d🚒", "👨\u200d🚒 (:man_firefighter:)"), + ( + "👨🏿\u200d🚒", + "👨🏿\u200d🚒 (:man_firefighter_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🚒", + "👨🏻\u200d🚒 (:man_firefighter_light_skin_tone:)", + ), + ( + "👨🏾\u200d🚒", + "👨🏾\u200d🚒 (:man_firefighter_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🚒", + "👨🏼\u200d🚒 (:man_firefighter_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🚒", + "👨🏽\u200d🚒 (:man_firefighter_medium_skin_tone:)", + ), + ("🙍\u200d♂️", "🙍\u200d♂️ (:man_frowning:)"), + ("🙍\u200d♂", "🙍\u200d♂ (:man_frowning:)"), + ( + "🙍🏿\u200d♂️", + "🙍🏿\u200d♂️ (:man_frowning_dark_skin_tone:)", + ), + ("🙍🏿\u200d♂", "🙍🏿\u200d♂ (:man_frowning_dark_skin_tone:)"), + ( + "🙍🏻\u200d♂️", + "🙍🏻\u200d♂️ (:man_frowning_light_skin_tone:)", + ), + ("🙍🏻\u200d♂", "🙍🏻\u200d♂ (:man_frowning_light_skin_tone:)"), + ( + "🙍🏾\u200d♂️", + "🙍🏾\u200d♂️ (:man_frowning_medium-dark_skin_tone:)", + ), + ( + "🙍🏾\u200d♂", + "🙍🏾\u200d♂ (:man_frowning_medium-dark_skin_tone:)", + ), + ( + "🙍🏼\u200d♂️", + "🙍🏼\u200d♂️ (:man_frowning_medium-light_skin_tone:)", + ), + ( + "🙍🏼\u200d♂", + "🙍🏼\u200d♂ (:man_frowning_medium-light_skin_tone:)", + ), + ( + "🙍🏽\u200d♂️", + "🙍🏽\u200d♂️ (:man_frowning_medium_skin_tone:)", + ), + ( + "🙍🏽\u200d♂", + "🙍🏽\u200d♂ (:man_frowning_medium_skin_tone:)", + ), + ("🧞\u200d♂️", "🧞\u200d♂️ (:man_genie:)"), + ("🧞\u200d♂", "🧞\u200d♂ (:man_genie:)"), + ("🙅\u200d♂️", "🙅\u200d♂️ (:man_gesturing_NO:)"), + ("🙅\u200d♂", "🙅\u200d♂ (:man_gesturing_NO:)"), + ( + "🙅🏿\u200d♂️", + "🙅🏿\u200d♂️ (:man_gesturing_NO_dark_skin_tone:)", + ), + ( + "🙅🏿\u200d♂", + "🙅🏿\u200d♂ (:man_gesturing_NO_dark_skin_tone:)", + ), + ( + "🙅🏻\u200d♂️", + "🙅🏻\u200d♂️ (:man_gesturing_NO_light_skin_tone:)", + ), + ( + "🙅🏻\u200d♂", + "🙅🏻\u200d♂ (:man_gesturing_NO_light_skin_tone:)", + ), + ( + "🙅🏾\u200d♂️", + "🙅🏾\u200d♂️ (:man_gesturing_NO_medium-dark_skin_tone:)", + ), + ( + "🙅🏾\u200d♂", + "🙅🏾\u200d♂ (:man_gesturing_NO_medium-dark_skin_tone:)", + ), + ( + "🙅🏼\u200d♂️", + "🙅🏼\u200d♂️ (:man_gesturing_NO_medium-light_skin_tone:)", + ), + ( + "🙅🏼\u200d♂", + "🙅🏼\u200d♂ (:man_gesturing_NO_medium-light_skin_tone:)", + ), + ( + "🙅🏽\u200d♂️", + "🙅🏽\u200d♂️ (:man_gesturing_NO_medium_skin_tone:)", + ), + ( + "🙅🏽\u200d♂", + "🙅🏽\u200d♂ (:man_gesturing_NO_medium_skin_tone:)", + ), + ("🙆\u200d♂️", "🙆\u200d♂️ (:man_gesturing_OK:)"), + ("🙆\u200d♂", "🙆\u200d♂ (:man_gesturing_OK:)"), + ( + "🙆🏿\u200d♂️", + "🙆🏿\u200d♂️ (:man_gesturing_OK_dark_skin_tone:)", + ), + ( + "🙆🏿\u200d♂", + "🙆🏿\u200d♂ (:man_gesturing_OK_dark_skin_tone:)", + ), + ( + "🙆🏻\u200d♂️", + "🙆🏻\u200d♂️ (:man_gesturing_OK_light_skin_tone:)", + ), + ( + "🙆🏻\u200d♂", + "🙆🏻\u200d♂ (:man_gesturing_OK_light_skin_tone:)", + ), + ( + "🙆🏾\u200d♂️", + "🙆🏾\u200d♂️ (:man_gesturing_OK_medium-dark_skin_tone:)", + ), + ( + "🙆🏾\u200d♂", + "🙆🏾\u200d♂ (:man_gesturing_OK_medium-dark_skin_tone:)", + ), + ( + "🙆🏼\u200d♂️", + "🙆🏼\u200d♂️ (:man_gesturing_OK_medium-light_skin_tone:)", + ), + ( + "🙆🏼\u200d♂", + "🙆🏼\u200d♂ (:man_gesturing_OK_medium-light_skin_tone:)", + ), + ( + "🙆🏽\u200d♂️", + "🙆🏽\u200d♂️ (:man_gesturing_OK_medium_skin_tone:)", + ), + ( + "🙆🏽\u200d♂", + "🙆🏽\u200d♂ (:man_gesturing_OK_medium_skin_tone:)", + ), + ("💇\u200d♂️", "💇\u200d♂️ (:man_getting_haircut:)"), + ("💇\u200d♂", "💇\u200d♂ (:man_getting_haircut:)"), + ( + "💇🏿\u200d♂️", + "💇🏿\u200d♂️ (:man_getting_haircut_dark_skin_tone:)", + ), + ( + "💇🏿\u200d♂", + "💇🏿\u200d♂ (:man_getting_haircut_dark_skin_tone:)", + ), + ( + "💇🏻\u200d♂️", + "💇🏻\u200d♂️ (:man_getting_haircut_light_skin_tone:)", + ), + ( + "💇🏻\u200d♂", + "💇🏻\u200d♂ (:man_getting_haircut_light_skin_tone:)", + ), + ( + "💇🏾\u200d♂️", + "💇🏾\u200d♂️ (:man_getting_haircut_medium-dark_skin_tone:)", + ), + ( + "💇🏾\u200d♂", + "💇🏾\u200d♂ (:man_getting_haircut_medium-dark_skin_tone:)", + ), + ( + "💇🏼\u200d♂️", + "💇🏼\u200d♂️ (:man_getting_haircut_medium-light_skin_tone:)", + ), + ( + "💇🏼\u200d♂", + "💇🏼\u200d♂ (:man_getting_haircut_medium-light_skin_tone:)", + ), + ( + "💇🏽\u200d♂️", + "💇🏽\u200d♂️ (:man_getting_haircut_medium_skin_tone:)", + ), + ( + "💇🏽\u200d♂", + "💇🏽\u200d♂ (:man_getting_haircut_medium_skin_tone:)", + ), + ("💆\u200d♂️", "💆\u200d♂️ (:man_getting_massage:)"), + ("💆\u200d♂", "💆\u200d♂ (:man_getting_massage:)"), + ( + "💆🏿\u200d♂️", + "💆🏿\u200d♂️ (:man_getting_massage_dark_skin_tone:)", + ), + ( + "💆🏿\u200d♂", + "💆🏿\u200d♂ (:man_getting_massage_dark_skin_tone:)", + ), + ( + "💆🏻\u200d♂️", + "💆🏻\u200d♂️ (:man_getting_massage_light_skin_tone:)", + ), + ( + "💆🏻\u200d♂", + "💆🏻\u200d♂ (:man_getting_massage_light_skin_tone:)", + ), + ( + "💆🏾\u200d♂️", + "💆🏾\u200d♂️ (:man_getting_massage_medium-dark_skin_tone:)", + ), + ( + "💆🏾\u200d♂", + "💆🏾\u200d♂ (:man_getting_massage_medium-dark_skin_tone:)", + ), + ( + "💆🏼\u200d♂️", + "💆🏼\u200d♂️ (:man_getting_massage_medium-light_skin_tone:)", + ), + ( + "💆🏼\u200d♂", + "💆🏼\u200d♂ (:man_getting_massage_medium-light_skin_tone:)", + ), + ( + "💆🏽\u200d♂️", + "💆🏽\u200d♂️ (:man_getting_massage_medium_skin_tone:)", + ), + ( + "💆🏽\u200d♂", + "💆🏽\u200d♂ (:man_getting_massage_medium_skin_tone:)", + ), + ("🏌️\u200d♂️", "🏌️\u200d♂️ (:man_golfing:)"), + ("🏌\u200d♂️", "🏌\u200d♂️ (:man_golfing:)"), + ("🏌️\u200d♂", "🏌️\u200d♂ (:man_golfing:)"), + ("🏌\u200d♂", "🏌\u200d♂ (:man_golfing:)"), + ("🏌🏿\u200d♂️", "🏌🏿\u200d♂️ (:man_golfing_dark_skin_tone:)"), + ("🏌🏿\u200d♂", "🏌🏿\u200d♂ (:man_golfing_dark_skin_tone:)"), + ( + "🏌🏻\u200d♂️", + "🏌🏻\u200d♂️ (:man_golfing_light_skin_tone:)", + ), + ("🏌🏻\u200d♂", "🏌🏻\u200d♂ (:man_golfing_light_skin_tone:)"), + ( + "🏌🏾\u200d♂️", + "🏌🏾\u200d♂️ (:man_golfing_medium-dark_skin_tone:)", + ), + ( + "🏌🏾\u200d♂", + "🏌🏾\u200d♂ (:man_golfing_medium-dark_skin_tone:)", + ), + ( + "🏌🏼\u200d♂️", + "🏌🏼\u200d♂️ (:man_golfing_medium-light_skin_tone:)", + ), + ( + "🏌🏼\u200d♂", + "🏌🏼\u200d♂ (:man_golfing_medium-light_skin_tone:)", + ), + ( + "🏌🏽\u200d♂️", + "🏌🏽\u200d♂️ (:man_golfing_medium_skin_tone:)", + ), + ("🏌🏽\u200d♂", "🏌🏽\u200d♂ (:man_golfing_medium_skin_tone:)"), + ("💂\u200d♂️", "💂\u200d♂️ (:man_guard:)"), + ("💂\u200d♂", "💂\u200d♂ (:man_guard:)"), + ("💂🏿\u200d♂️", "💂🏿\u200d♂️ (:man_guard_dark_skin_tone:)"), + ("💂🏿\u200d♂", "💂🏿\u200d♂ (:man_guard_dark_skin_tone:)"), + ("💂🏻\u200d♂️", "💂🏻\u200d♂️ (:man_guard_light_skin_tone:)"), + ("💂🏻\u200d♂", "💂🏻\u200d♂ (:man_guard_light_skin_tone:)"), + ( + "💂🏾\u200d♂️", + "💂🏾\u200d♂️ (:man_guard_medium-dark_skin_tone:)", + ), + ( + "💂🏾\u200d♂", + "💂🏾\u200d♂ (:man_guard_medium-dark_skin_tone:)", + ), + ( + "💂🏼\u200d♂️", + "💂🏼\u200d♂️ (:man_guard_medium-light_skin_tone:)", + ), + ( + "💂🏼\u200d♂", + "💂🏼\u200d♂ (:man_guard_medium-light_skin_tone:)", + ), + ("💂🏽\u200d♂️", "💂🏽\u200d♂️ (:man_guard_medium_skin_tone:)"), + ("💂🏽\u200d♂", "💂🏽\u200d♂ (:man_guard_medium_skin_tone:)"), + ("👨\u200d⚕️", "👨\u200d⚕️ (:man_health_worker:)"), + ("👨\u200d⚕", "👨\u200d⚕ (:man_health_worker:)"), + ( + "👨🏿\u200d⚕️", + "👨🏿\u200d⚕️ (:man_health_worker_dark_skin_tone:)", + ), + ( + "👨🏿\u200d⚕", + "👨🏿\u200d⚕ (:man_health_worker_dark_skin_tone:)", + ), + ( + "👨🏻\u200d⚕️", + "👨🏻\u200d⚕️ (:man_health_worker_light_skin_tone:)", + ), + ( + "👨🏻\u200d⚕", + "👨🏻\u200d⚕ (:man_health_worker_light_skin_tone:)", + ), + ( + "👨🏾\u200d⚕️", + "👨🏾\u200d⚕️ (:man_health_worker_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d⚕", + "👨🏾\u200d⚕ (:man_health_worker_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d⚕️", + "👨🏼\u200d⚕️ (:man_health_worker_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d⚕", + "👨🏼\u200d⚕ (:man_health_worker_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d⚕️", + "👨🏽\u200d⚕️ (:man_health_worker_medium_skin_tone:)", + ), + ( + "👨🏽\u200d⚕", + "👨🏽\u200d⚕ (:man_health_worker_medium_skin_tone:)", + ), + ("🧘\u200d♂️", "🧘\u200d♂️ (:man_in_lotus_position:)"), + ("🧘\u200d♂", "🧘\u200d♂ (:man_in_lotus_position:)"), + ( + "🧘🏿\u200d♂️", + "🧘🏿\u200d♂️ (:man_in_lotus_position_dark_skin_tone:)", + ), + ( + "🧘🏿\u200d♂", + "🧘🏿\u200d♂ (:man_in_lotus_position_dark_skin_tone:)", + ), + ( + "🧘🏻\u200d♂️", + "🧘🏻\u200d♂️ (:man_in_lotus_position_light_skin_tone:)", + ), + ( + "🧘🏻\u200d♂", + "🧘🏻\u200d♂ (:man_in_lotus_position_light_skin_tone:)", + ), + ( + "🧘🏾\u200d♂️", + "🧘🏾\u200d♂️ (:man_in_lotus_position_medium-dark_skin_tone:)", + ), + ( + "🧘🏾\u200d♂", + "🧘🏾\u200d♂ (:man_in_lotus_position_medium-dark_skin_tone:)", + ), + ( + "🧘🏼\u200d♂️", + "🧘🏼\u200d♂️ (:man_in_lotus_position_medium-light_skin_tone:)", + ), + ( + "🧘🏼\u200d♂", + "🧘🏼\u200d♂ (:man_in_lotus_position_medium-light_skin_tone:)", + ), + ( + "🧘🏽\u200d♂️", + "🧘🏽\u200d♂️ (:man_in_lotus_position_medium_skin_tone:)", + ), + ( + "🧘🏽\u200d♂", + "🧘🏽\u200d♂ (:man_in_lotus_position_medium_skin_tone:)", + ), + ("👨\u200d🦽", "👨\u200d🦽 (:man_in_manual_wheelchair:)"), + ( + "👨🏿\u200d🦽", + "👨🏿\u200d🦽 (:man_in_manual_wheelchair_dark_skin_tone:)", + ), + ( + "👨\u200d🦽\u200d➡️", + "👨\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right:)", + ), + ( + "👨\u200d🦽\u200d➡", + "👨\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right:)", + ), + ( + "👨🏿\u200d🦽\u200d➡️", + "👨🏿\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👨🏿\u200d🦽\u200d➡", + "👨🏿\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🦽\u200d➡️", + "👨🏻\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👨🏻\u200d🦽\u200d➡", + "👨🏻\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦽\u200d➡️", + "👨🏾\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d🦽\u200d➡", + "👨🏾\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦽\u200d➡️", + "👨🏼\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d🦽\u200d➡", + "👨🏼\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦽\u200d➡️", + "👨🏽\u200d🦽\u200d➡️ (:man_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👨🏽\u200d🦽\u200d➡", + "👨🏽\u200d🦽\u200d➡ (:man_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👨🏻\u200d🦽", + "👨🏻\u200d🦽 (:man_in_manual_wheelchair_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦽", + "👨🏾\u200d🦽 (:man_in_manual_wheelchair_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦽", + "👨🏼\u200d🦽 (:man_in_manual_wheelchair_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦽", + "👨🏽\u200d🦽 (:man_in_manual_wheelchair_medium_skin_tone:)", + ), + ("👨\u200d🦼", "👨\u200d🦼 (:man_in_motorized_wheelchair:)"), + ( + "👨🏿\u200d🦼", + "👨🏿\u200d🦼 (:man_in_motorized_wheelchair_dark_skin_tone:)", + ), + ( + "👨\u200d🦼\u200d➡️", + "👨\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right:)", + ), + ( + "👨\u200d🦼\u200d➡", + "👨\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right:)", + ), + ( + "👨🏿\u200d🦼\u200d➡️", + "👨🏿\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👨🏿\u200d🦼\u200d➡", + "👨🏿\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🦼\u200d➡️", + "👨🏻\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👨🏻\u200d🦼\u200d➡", + "👨🏻\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦼\u200d➡️", + "👨🏾\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d🦼\u200d➡", + "👨🏾\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦼\u200d➡️", + "👨🏼\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d🦼\u200d➡", + "👨🏼\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦼\u200d➡️", + "👨🏽\u200d🦼\u200d➡️ (:man_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👨🏽\u200d🦼\u200d➡", + "👨🏽\u200d🦼\u200d➡ (:man_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👨🏻\u200d🦼", + "👨🏻\u200d🦼 (:man_in_motorized_wheelchair_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦼", + "👨🏾\u200d🦼 (:man_in_motorized_wheelchair_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦼", + "👨🏼\u200d🦼 (:man_in_motorized_wheelchair_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦼", + "👨🏽\u200d🦼 (:man_in_motorized_wheelchair_medium_skin_tone:)", + ), + ("🧖\u200d♂️", "🧖\u200d♂️ (:man_in_steamy_room:)"), + ("🧖\u200d♂", "🧖\u200d♂ (:man_in_steamy_room:)"), + ( + "🧖🏿\u200d♂️", + "🧖🏿\u200d♂️ (:man_in_steamy_room_dark_skin_tone:)", + ), + ( + "🧖🏿\u200d♂", + "🧖🏿\u200d♂ (:man_in_steamy_room_dark_skin_tone:)", + ), + ( + "🧖🏻\u200d♂️", + "🧖🏻\u200d♂️ (:man_in_steamy_room_light_skin_tone:)", + ), + ( + "🧖🏻\u200d♂", + "🧖🏻\u200d♂ (:man_in_steamy_room_light_skin_tone:)", + ), + ( + "🧖🏾\u200d♂️", + "🧖🏾\u200d♂️ (:man_in_steamy_room_medium-dark_skin_tone:)", + ), + ( + "🧖🏾\u200d♂", + "🧖🏾\u200d♂ (:man_in_steamy_room_medium-dark_skin_tone:)", + ), + ( + "🧖🏼\u200d♂️", + "🧖🏼\u200d♂️ (:man_in_steamy_room_medium-light_skin_tone:)", + ), + ( + "🧖🏼\u200d♂", + "🧖🏼\u200d♂ (:man_in_steamy_room_medium-light_skin_tone:)", + ), + ( + "🧖🏽\u200d♂️", + "🧖🏽\u200d♂️ (:man_in_steamy_room_medium_skin_tone:)", + ), + ( + "🧖🏽\u200d♂", + "🧖🏽\u200d♂ (:man_in_steamy_room_medium_skin_tone:)", + ), + ("🤵\u200d♂️", "🤵\u200d♂️ (:man_in_tuxedo:)"), + ("🤵\u200d♂", "🤵\u200d♂ (:man_in_tuxedo:)"), + ( + "🤵🏿\u200d♂️", + "🤵🏿\u200d♂️ (:man_in_tuxedo_dark_skin_tone:)", + ), + ("🤵🏿\u200d♂", "🤵🏿\u200d♂ (:man_in_tuxedo_dark_skin_tone:)"), + ( + "🤵🏻\u200d♂️", + "🤵🏻\u200d♂️ (:man_in_tuxedo_light_skin_tone:)", + ), + ( + "🤵🏻\u200d♂", + "🤵🏻\u200d♂ (:man_in_tuxedo_light_skin_tone:)", + ), + ( + "🤵🏾\u200d♂️", + "🤵🏾\u200d♂️ (:man_in_tuxedo_medium-dark_skin_tone:)", + ), + ( + "🤵🏾\u200d♂", + "🤵🏾\u200d♂ (:man_in_tuxedo_medium-dark_skin_tone:)", + ), + ( + "🤵🏼\u200d♂️", + "🤵🏼\u200d♂️ (:man_in_tuxedo_medium-light_skin_tone:)", + ), + ( + "🤵🏼\u200d♂", + "🤵🏼\u200d♂ (:man_in_tuxedo_medium-light_skin_tone:)", + ), + ( + "🤵🏽\u200d♂️", + "🤵🏽\u200d♂️ (:man_in_tuxedo_medium_skin_tone:)", + ), + ( + "🤵🏽\u200d♂", + "🤵🏽\u200d♂ (:man_in_tuxedo_medium_skin_tone:)", + ), + ("👨\u200d⚖️", "👨\u200d⚖️ (:man_judge:)"), + ("👨\u200d⚖", "👨\u200d⚖ (:man_judge:)"), + ("👨🏿\u200d⚖️", "👨🏿\u200d⚖️ (:man_judge_dark_skin_tone:)"), + ("👨🏿\u200d⚖", "👨🏿\u200d⚖ (:man_judge_dark_skin_tone:)"), + ("👨🏻\u200d⚖️", "👨🏻\u200d⚖️ (:man_judge_light_skin_tone:)"), + ("👨🏻\u200d⚖", "👨🏻\u200d⚖ (:man_judge_light_skin_tone:)"), + ( + "👨🏾\u200d⚖️", + "👨🏾\u200d⚖️ (:man_judge_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d⚖", + "👨🏾\u200d⚖ (:man_judge_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d⚖️", + "👨🏼\u200d⚖️ (:man_judge_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d⚖", + "👨🏼\u200d⚖ (:man_judge_medium-light_skin_tone:)", + ), + ("👨🏽\u200d⚖️", "👨🏽\u200d⚖️ (:man_judge_medium_skin_tone:)"), + ("👨🏽\u200d⚖", "👨🏽\u200d⚖ (:man_judge_medium_skin_tone:)"), + ("🤹\u200d♂️", "🤹\u200d♂️ (:man_juggling:)"), + ("🤹\u200d♂", "🤹\u200d♂ (:man_juggling:)"), + ( + "🤹🏿\u200d♂️", + "🤹🏿\u200d♂️ (:man_juggling_dark_skin_tone:)", + ), + ("🤹🏿\u200d♂", "🤹🏿\u200d♂ (:man_juggling_dark_skin_tone:)"), + ( + "🤹🏻\u200d♂️", + "🤹🏻\u200d♂️ (:man_juggling_light_skin_tone:)", + ), + ("🤹🏻\u200d♂", "🤹🏻\u200d♂ (:man_juggling_light_skin_tone:)"), + ( + "🤹🏾\u200d♂️", + "🤹🏾\u200d♂️ (:man_juggling_medium-dark_skin_tone:)", + ), + ( + "🤹🏾\u200d♂", + "🤹🏾\u200d♂ (:man_juggling_medium-dark_skin_tone:)", + ), + ( + "🤹🏼\u200d♂️", + "🤹🏼\u200d♂️ (:man_juggling_medium-light_skin_tone:)", + ), + ( + "🤹🏼\u200d♂", + "🤹🏼\u200d♂ (:man_juggling_medium-light_skin_tone:)", + ), + ( + "🤹🏽\u200d♂️", + "🤹🏽\u200d♂️ (:man_juggling_medium_skin_tone:)", + ), + ( + "🤹🏽\u200d♂", + "🤹🏽\u200d♂ (:man_juggling_medium_skin_tone:)", + ), + ("🧎\u200d♂️", "🧎\u200d♂️ (:man_kneeling:)"), + ("🧎\u200d♂", "🧎\u200d♂ (:man_kneeling:)"), + ( + "🧎🏿\u200d♂️", + "🧎🏿\u200d♂️ (:man_kneeling_dark_skin_tone:)", + ), + ("🧎🏿\u200d♂", "🧎🏿\u200d♂ (:man_kneeling_dark_skin_tone:)"), + ( + "🧎\u200d♂️\u200d➡️", + "🧎\u200d♂️\u200d➡️ (:man_kneeling_facing_right:)", + ), + ( + "🧎\u200d♂\u200d➡️", + "🧎\u200d♂\u200d➡️ (:man_kneeling_facing_right:)", + ), + ( + "🧎\u200d♂️\u200d➡", + "🧎\u200d♂️\u200d➡ (:man_kneeling_facing_right:)", + ), + ( + "🧎\u200d♂\u200d➡", + "🧎\u200d♂\u200d➡ (:man_kneeling_facing_right:)", + ), + ( + "🧎🏿\u200d♂️\u200d➡️", + "🧎🏿\u200d♂️\u200d➡️ (:man_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♂\u200d➡️", + "🧎🏿\u200d♂\u200d➡️ (:man_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♂️\u200d➡", + "🧎🏿\u200d♂️\u200d➡ (:man_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♂\u200d➡", + "🧎🏿\u200d♂\u200d➡ (:man_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏻\u200d♂️\u200d➡️", + "🧎🏻\u200d♂️\u200d➡️ (:man_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♂\u200d➡️", + "🧎🏻\u200d♂\u200d➡️ (:man_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♂️\u200d➡", + "🧎🏻\u200d♂️\u200d➡ (:man_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♂\u200d➡", + "🧎🏻\u200d♂\u200d➡ (:man_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏾\u200d♂️\u200d➡️", + "🧎🏾\u200d♂️\u200d➡️ (:man_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♂\u200d➡️", + "🧎🏾\u200d♂\u200d➡️ (:man_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♂️\u200d➡", + "🧎🏾\u200d♂️\u200d➡ (:man_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♂\u200d➡", + "🧎🏾\u200d♂\u200d➡ (:man_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏼\u200d♂️\u200d➡️", + "🧎🏼\u200d♂️\u200d➡️ (:man_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♂\u200d➡️", + "🧎🏼\u200d♂\u200d➡️ (:man_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♂️\u200d➡", + "🧎🏼\u200d♂️\u200d➡ (:man_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♂\u200d➡", + "🧎🏼\u200d♂\u200d➡ (:man_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏽\u200d♂️\u200d➡️", + "🧎🏽\u200d♂️\u200d➡️ (:man_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♂\u200d➡️", + "🧎🏽\u200d♂\u200d➡️ (:man_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♂️\u200d➡", + "🧎🏽\u200d♂️\u200d➡ (:man_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♂\u200d➡", + "🧎🏽\u200d♂\u200d➡ (:man_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏻\u200d♂️", + "🧎🏻\u200d♂️ (:man_kneeling_light_skin_tone:)", + ), + ("🧎🏻\u200d♂", "🧎🏻\u200d♂ (:man_kneeling_light_skin_tone:)"), + ( + "🧎🏾\u200d♂️", + "🧎🏾\u200d♂️ (:man_kneeling_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♂", + "🧎🏾\u200d♂ (:man_kneeling_medium-dark_skin_tone:)", + ), + ( + "🧎🏼\u200d♂️", + "🧎🏼\u200d♂️ (:man_kneeling_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♂", + "🧎🏼\u200d♂ (:man_kneeling_medium-light_skin_tone:)", + ), + ( + "🧎🏽\u200d♂️", + "🧎🏽\u200d♂️ (:man_kneeling_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♂", + "🧎🏽\u200d♂ (:man_kneeling_medium_skin_tone:)", + ), + ("🏋️\u200d♂️", "🏋️\u200d♂️ (:man_lifting_weights:)"), + ("🏋\u200d♂️", "🏋\u200d♂️ (:man_lifting_weights:)"), + ("🏋️\u200d♂", "🏋️\u200d♂ (:man_lifting_weights:)"), + ("🏋\u200d♂", "🏋\u200d♂ (:man_lifting_weights:)"), + ( + "🏋🏿\u200d♂️", + "🏋🏿\u200d♂️ (:man_lifting_weights_dark_skin_tone:)", + ), + ( + "🏋🏿\u200d♂", + "🏋🏿\u200d♂ (:man_lifting_weights_dark_skin_tone:)", + ), + ( + "🏋🏻\u200d♂️", + "🏋🏻\u200d♂️ (:man_lifting_weights_light_skin_tone:)", + ), + ( + "🏋🏻\u200d♂", + "🏋🏻\u200d♂ (:man_lifting_weights_light_skin_tone:)", + ), + ( + "🏋🏾\u200d♂️", + "🏋🏾\u200d♂️ (:man_lifting_weights_medium-dark_skin_tone:)", + ), + ( + "🏋🏾\u200d♂", + "🏋🏾\u200d♂ (:man_lifting_weights_medium-dark_skin_tone:)", + ), + ( + "🏋🏼\u200d♂️", + "🏋🏼\u200d♂️ (:man_lifting_weights_medium-light_skin_tone:)", + ), + ( + "🏋🏼\u200d♂", + "🏋🏼\u200d♂ (:man_lifting_weights_medium-light_skin_tone:)", + ), + ( + "🏋🏽\u200d♂️", + "🏋🏽\u200d♂️ (:man_lifting_weights_medium_skin_tone:)", + ), + ( + "🏋🏽\u200d♂", + "🏋🏽\u200d♂ (:man_lifting_weights_medium_skin_tone:)", + ), + ("👨🏻", "👨🏻 (:man_light_skin_tone:)"), + ("👨🏻\u200d🦲", "👨🏻\u200d🦲 (:man_light_skin_tone_bald:)"), + ("🧔🏻\u200d♂️", "🧔🏻\u200d♂️ (:man_light_skin_tone_beard:)"), + ("🧔🏻\u200d♂", "🧔🏻\u200d♂ (:man_light_skin_tone_beard:)"), + ( + "👱🏻\u200d♂️", + "👱🏻\u200d♂️ (:man_light_skin_tone_blond_hair:)", + ), + ( + "👱🏻\u200d♂", + "👱🏻\u200d♂ (:man_light_skin_tone_blond_hair:)", + ), + ( + "👨🏻\u200d🦱", + "👨🏻\u200d🦱 (:man_light_skin_tone_curly_hair:)", + ), + ("👨🏻\u200d🦰", "👨🏻\u200d🦰 (:man_light_skin_tone_red_hair:)"), + ( + "👨🏻\u200d🦳", + "👨🏻\u200d🦳 (:man_light_skin_tone_white_hair:)", + ), + ("🧙\u200d♂️", "🧙\u200d♂️ (:man_mage:)"), + ("🧙\u200d♂", "🧙\u200d♂ (:man_mage:)"), + ("🧙🏿\u200d♂️", "🧙🏿\u200d♂️ (:man_mage_dark_skin_tone:)"), + ("🧙🏿\u200d♂", "🧙🏿\u200d♂ (:man_mage_dark_skin_tone:)"), + ("🧙🏻\u200d♂️", "🧙🏻\u200d♂️ (:man_mage_light_skin_tone:)"), + ("🧙🏻\u200d♂", "🧙🏻\u200d♂ (:man_mage_light_skin_tone:)"), + ( + "🧙🏾\u200d♂️", + "🧙🏾\u200d♂️ (:man_mage_medium-dark_skin_tone:)", + ), + ( + "🧙🏾\u200d♂", + "🧙🏾\u200d♂ (:man_mage_medium-dark_skin_tone:)", + ), + ( + "🧙🏼\u200d♂️", + "🧙🏼\u200d♂️ (:man_mage_medium-light_skin_tone:)", + ), + ( + "🧙🏼\u200d♂", + "🧙🏼\u200d♂ (:man_mage_medium-light_skin_tone:)", + ), + ("🧙🏽\u200d♂️", "🧙🏽\u200d♂️ (:man_mage_medium_skin_tone:)"), + ("🧙🏽\u200d♂", "🧙🏽\u200d♂ (:man_mage_medium_skin_tone:)"), + ("👨\u200d🔧", "👨\u200d🔧 (:man_mechanic:)"), + ("👨🏿\u200d🔧", "👨🏿\u200d🔧 (:man_mechanic_dark_skin_tone:)"), + ("👨🏻\u200d🔧", "👨🏻\u200d🔧 (:man_mechanic_light_skin_tone:)"), + ( + "👨🏾\u200d🔧", + "👨🏾\u200d🔧 (:man_mechanic_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🔧", + "👨🏼\u200d🔧 (:man_mechanic_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🔧", + "👨🏽\u200d🔧 (:man_mechanic_medium_skin_tone:)", + ), + ("👨🏾", "👨🏾 (:man_medium-dark_skin_tone:)"), + ( + "👨🏾\u200d🦲", + "👨🏾\u200d🦲 (:man_medium-dark_skin_tone_bald:)", + ), + ( + "🧔🏾\u200d♂️", + "🧔🏾\u200d♂️ (:man_medium-dark_skin_tone_beard:)", + ), + ( + "🧔🏾\u200d♂", + "🧔🏾\u200d♂ (:man_medium-dark_skin_tone_beard:)", + ), + ( + "👱🏾\u200d♂️", + "👱🏾\u200d♂️ (:man_medium-dark_skin_tone_blond_hair:)", + ), + ( + "👱🏾\u200d♂", + "👱🏾\u200d♂ (:man_medium-dark_skin_tone_blond_hair:)", + ), + ( + "👨🏾\u200d🦱", + "👨🏾\u200d🦱 (:man_medium-dark_skin_tone_curly_hair:)", + ), + ( + "👨🏾\u200d🦰", + "👨🏾\u200d🦰 (:man_medium-dark_skin_tone_red_hair:)", + ), + ( + "👨🏾\u200d🦳", + "👨🏾\u200d🦳 (:man_medium-dark_skin_tone_white_hair:)", + ), + ("👨🏼", "👨🏼 (:man_medium-light_skin_tone:)"), + ( + "👨🏼\u200d🦲", + "👨🏼\u200d🦲 (:man_medium-light_skin_tone_bald:)", + ), + ( + "🧔🏼\u200d♂️", + "🧔🏼\u200d♂️ (:man_medium-light_skin_tone_beard:)", + ), + ( + "🧔🏼\u200d♂", + "🧔🏼\u200d♂ (:man_medium-light_skin_tone_beard:)", + ), + ( + "👱🏼\u200d♂️", + "👱🏼\u200d♂️ (:man_medium-light_skin_tone_blond_hair:)", + ), + ( + "👱🏼\u200d♂", + "👱🏼\u200d♂ (:man_medium-light_skin_tone_blond_hair:)", + ), + ( + "👨🏼\u200d🦱", + "👨🏼\u200d🦱 (:man_medium-light_skin_tone_curly_hair:)", + ), + ( + "👨🏼\u200d🦰", + "👨🏼\u200d🦰 (:man_medium-light_skin_tone_red_hair:)", + ), + ( + "👨🏼\u200d🦳", + "👨🏼\u200d🦳 (:man_medium-light_skin_tone_white_hair:)", + ), + ("👨🏽", "👨🏽 (:man_medium_skin_tone:)"), + ("👨🏽\u200d🦲", "👨🏽\u200d🦲 (:man_medium_skin_tone_bald:)"), + ("🧔🏽\u200d♂️", "🧔🏽\u200d♂️ (:man_medium_skin_tone_beard:)"), + ("🧔🏽\u200d♂", "🧔🏽\u200d♂ (:man_medium_skin_tone_beard:)"), + ( + "👱🏽\u200d♂️", + "👱🏽\u200d♂️ (:man_medium_skin_tone_blond_hair:)", + ), + ( + "👱🏽\u200d♂", + "👱🏽\u200d♂ (:man_medium_skin_tone_blond_hair:)", + ), + ( + "👨🏽\u200d🦱", + "👨🏽\u200d🦱 (:man_medium_skin_tone_curly_hair:)", + ), + ( + "👨🏽\u200d🦰", + "👨🏽\u200d🦰 (:man_medium_skin_tone_red_hair:)", + ), + ( + "👨🏽\u200d🦳", + "👨🏽\u200d🦳 (:man_medium_skin_tone_white_hair:)", + ), + ("🚵\u200d♂️", "🚵\u200d♂️ (:man_mountain_biking:)"), + ("🚵\u200d♂", "🚵\u200d♂ (:man_mountain_biking:)"), + ( + "🚵🏿\u200d♂️", + "🚵🏿\u200d♂️ (:man_mountain_biking_dark_skin_tone:)", + ), + ( + "🚵🏿\u200d♂", + "🚵🏿\u200d♂ (:man_mountain_biking_dark_skin_tone:)", + ), + ( + "🚵🏻\u200d♂️", + "🚵🏻\u200d♂️ (:man_mountain_biking_light_skin_tone:)", + ), + ( + "🚵🏻\u200d♂", + "🚵🏻\u200d♂ (:man_mountain_biking_light_skin_tone:)", + ), + ( + "🚵🏾\u200d♂️", + "🚵🏾\u200d♂️ (:man_mountain_biking_medium-dark_skin_tone:)", + ), + ( + "🚵🏾\u200d♂", + "🚵🏾\u200d♂ (:man_mountain_biking_medium-dark_skin_tone:)", + ), + ( + "🚵🏼\u200d♂️", + "🚵🏼\u200d♂️ (:man_mountain_biking_medium-light_skin_tone:)", + ), + ( + "🚵🏼\u200d♂", + "🚵🏼\u200d♂ (:man_mountain_biking_medium-light_skin_tone:)", + ), + ( + "🚵🏽\u200d♂️", + "🚵🏽\u200d♂️ (:man_mountain_biking_medium_skin_tone:)", + ), + ( + "🚵🏽\u200d♂", + "🚵🏽\u200d♂ (:man_mountain_biking_medium_skin_tone:)", + ), + ("👨\u200d💼", "👨\u200d💼 (:man_office_worker:)"), + ( + "👨🏿\u200d💼", + "👨🏿\u200d💼 (:man_office_worker_dark_skin_tone:)", + ), + ( + "👨🏻\u200d💼", + "👨🏻\u200d💼 (:man_office_worker_light_skin_tone:)", + ), + ( + "👨🏾\u200d💼", + "👨🏾\u200d💼 (:man_office_worker_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d💼", + "👨🏼\u200d💼 (:man_office_worker_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d💼", + "👨🏽\u200d💼 (:man_office_worker_medium_skin_tone:)", + ), + ("👨\u200d✈️", "👨\u200d✈️ (:man_pilot:)"), + ("👨\u200d✈", "👨\u200d✈ (:man_pilot:)"), + ("👨🏿\u200d✈️", "👨🏿\u200d✈️ (:man_pilot_dark_skin_tone:)"), + ("👨🏿\u200d✈", "👨🏿\u200d✈ (:man_pilot_dark_skin_tone:)"), + ("👨🏻\u200d✈️", "👨🏻\u200d✈️ (:man_pilot_light_skin_tone:)"), + ("👨🏻\u200d✈", "👨🏻\u200d✈ (:man_pilot_light_skin_tone:)"), + ( + "👨🏾\u200d✈️", + "👨🏾\u200d✈️ (:man_pilot_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d✈", + "👨🏾\u200d✈ (:man_pilot_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d✈️", + "👨🏼\u200d✈️ (:man_pilot_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d✈", + "👨🏼\u200d✈ (:man_pilot_medium-light_skin_tone:)", + ), + ("👨🏽\u200d✈️", "👨🏽\u200d✈️ (:man_pilot_medium_skin_tone:)"), + ("👨🏽\u200d✈", "👨🏽\u200d✈ (:man_pilot_medium_skin_tone:)"), + ("🤾\u200d♂️", "🤾\u200d♂️ (:man_playing_handball:)"), + ("🤾\u200d♂", "🤾\u200d♂ (:man_playing_handball:)"), + ( + "🤾🏿\u200d♂️", + "🤾🏿\u200d♂️ (:man_playing_handball_dark_skin_tone:)", + ), + ( + "🤾🏿\u200d♂", + "🤾🏿\u200d♂ (:man_playing_handball_dark_skin_tone:)", + ), + ( + "🤾🏻\u200d♂️", + "🤾🏻\u200d♂️ (:man_playing_handball_light_skin_tone:)", + ), + ( + "🤾🏻\u200d♂", + "🤾🏻\u200d♂ (:man_playing_handball_light_skin_tone:)", + ), + ( + "🤾🏾\u200d♂️", + "🤾🏾\u200d♂️ (:man_playing_handball_medium-dark_skin_tone:)", + ), + ( + "🤾🏾\u200d♂", + "🤾🏾\u200d♂ (:man_playing_handball_medium-dark_skin_tone:)", + ), + ( + "🤾🏼\u200d♂️", + "🤾🏼\u200d♂️ (:man_playing_handball_medium-light_skin_tone:)", + ), + ( + "🤾🏼\u200d♂", + "🤾🏼\u200d♂ (:man_playing_handball_medium-light_skin_tone:)", + ), + ( + "🤾🏽\u200d♂️", + "🤾🏽\u200d♂️ (:man_playing_handball_medium_skin_tone:)", + ), + ( + "🤾🏽\u200d♂", + "🤾🏽\u200d♂ (:man_playing_handball_medium_skin_tone:)", + ), + ("🤽\u200d♂️", "🤽\u200d♂️ (:man_playing_water_polo:)"), + ("🤽\u200d♂", "🤽\u200d♂ (:man_playing_water_polo:)"), + ( + "🤽🏿\u200d♂️", + "🤽🏿\u200d♂️ (:man_playing_water_polo_dark_skin_tone:)", + ), + ( + "🤽🏿\u200d♂", + "🤽🏿\u200d♂ (:man_playing_water_polo_dark_skin_tone:)", + ), + ( + "🤽🏻\u200d♂️", + "🤽🏻\u200d♂️ (:man_playing_water_polo_light_skin_tone:)", + ), + ( + "🤽🏻\u200d♂", + "🤽🏻\u200d♂ (:man_playing_water_polo_light_skin_tone:)", + ), + ( + "🤽🏾\u200d♂️", + "🤽🏾\u200d♂️ (:man_playing_water_polo_medium-dark_skin_tone:)", + ), + ( + "🤽🏾\u200d♂", + "🤽🏾\u200d♂ (:man_playing_water_polo_medium-dark_skin_tone:)", + ), + ( + "🤽🏼\u200d♂️", + "🤽🏼\u200d♂️ (:man_playing_water_polo_medium-light_skin_tone:)", + ), + ( + "🤽🏼\u200d♂", + "🤽🏼\u200d♂ (:man_playing_water_polo_medium-light_skin_tone:)", + ), + ( + "🤽🏽\u200d♂️", + "🤽🏽\u200d♂️ (:man_playing_water_polo_medium_skin_tone:)", + ), + ( + "🤽🏽\u200d♂", + "🤽🏽\u200d♂ (:man_playing_water_polo_medium_skin_tone:)", + ), + ("👮\u200d♂️", "👮\u200d♂️ (:man_police_officer:)"), + ("👮\u200d♂", "👮\u200d♂ (:man_police_officer:)"), + ( + "👮🏿\u200d♂️", + "👮🏿\u200d♂️ (:man_police_officer_dark_skin_tone:)", + ), + ( + "👮🏿\u200d♂", + "👮🏿\u200d♂ (:man_police_officer_dark_skin_tone:)", + ), + ( + "👮🏻\u200d♂️", + "👮🏻\u200d♂️ (:man_police_officer_light_skin_tone:)", + ), + ( + "👮🏻\u200d♂", + "👮🏻\u200d♂ (:man_police_officer_light_skin_tone:)", + ), + ( + "👮🏾\u200d♂️", + "👮🏾\u200d♂️ (:man_police_officer_medium-dark_skin_tone:)", + ), + ( + "👮🏾\u200d♂", + "👮🏾\u200d♂ (:man_police_officer_medium-dark_skin_tone:)", + ), + ( + "👮🏼\u200d♂️", + "👮🏼\u200d♂️ (:man_police_officer_medium-light_skin_tone:)", + ), + ( + "👮🏼\u200d♂", + "👮🏼\u200d♂ (:man_police_officer_medium-light_skin_tone:)", + ), + ( + "👮🏽\u200d♂️", + "👮🏽\u200d♂️ (:man_police_officer_medium_skin_tone:)", + ), + ( + "👮🏽\u200d♂", + "👮🏽\u200d♂ (:man_police_officer_medium_skin_tone:)", + ), + ("🙎\u200d♂️", "🙎\u200d♂️ (:man_pouting:)"), + ("🙎\u200d♂", "🙎\u200d♂ (:man_pouting:)"), + ("🙎🏿\u200d♂️", "🙎🏿\u200d♂️ (:man_pouting_dark_skin_tone:)"), + ("🙎🏿\u200d♂", "🙎🏿\u200d♂ (:man_pouting_dark_skin_tone:)"), + ( + "🙎🏻\u200d♂️", + "🙎🏻\u200d♂️ (:man_pouting_light_skin_tone:)", + ), + ("🙎🏻\u200d♂", "🙎🏻\u200d♂ (:man_pouting_light_skin_tone:)"), + ( + "🙎🏾\u200d♂️", + "🙎🏾\u200d♂️ (:man_pouting_medium-dark_skin_tone:)", + ), + ( + "🙎🏾\u200d♂", + "🙎🏾\u200d♂ (:man_pouting_medium-dark_skin_tone:)", + ), + ( + "🙎🏼\u200d♂️", + "🙎🏼\u200d♂️ (:man_pouting_medium-light_skin_tone:)", + ), + ( + "🙎🏼\u200d♂", + "🙎🏼\u200d♂ (:man_pouting_medium-light_skin_tone:)", + ), + ( + "🙎🏽\u200d♂️", + "🙎🏽\u200d♂️ (:man_pouting_medium_skin_tone:)", + ), + ("🙎🏽\u200d♂", "🙎🏽\u200d♂ (:man_pouting_medium_skin_tone:)"), + ("🙋\u200d♂️", "🙋\u200d♂️ (:man_raising_hand:)"), + ("🙋\u200d♂", "🙋\u200d♂ (:man_raising_hand:)"), + ( + "🙋🏿\u200d♂️", + "🙋🏿\u200d♂️ (:man_raising_hand_dark_skin_tone:)", + ), + ( + "🙋🏿\u200d♂", + "🙋🏿\u200d♂ (:man_raising_hand_dark_skin_tone:)", + ), + ( + "🙋🏻\u200d♂️", + "🙋🏻\u200d♂️ (:man_raising_hand_light_skin_tone:)", + ), + ( + "🙋🏻\u200d♂", + "🙋🏻\u200d♂ (:man_raising_hand_light_skin_tone:)", + ), + ( + "🙋🏾\u200d♂️", + "🙋🏾\u200d♂️ (:man_raising_hand_medium-dark_skin_tone:)", + ), + ( + "🙋🏾\u200d♂", + "🙋🏾\u200d♂ (:man_raising_hand_medium-dark_skin_tone:)", + ), + ( + "🙋🏼\u200d♂️", + "🙋🏼\u200d♂️ (:man_raising_hand_medium-light_skin_tone:)", + ), + ( + "🙋🏼\u200d♂", + "🙋🏼\u200d♂ (:man_raising_hand_medium-light_skin_tone:)", + ), + ( + "🙋🏽\u200d♂️", + "🙋🏽\u200d♂️ (:man_raising_hand_medium_skin_tone:)", + ), + ( + "🙋🏽\u200d♂", + "🙋🏽\u200d♂ (:man_raising_hand_medium_skin_tone:)", + ), + ("👨\u200d🦰", "👨\u200d🦰 (:man_red_hair:)"), + ("🚣\u200d♂️", "🚣\u200d♂️ (:man_rowing_boat:)"), + ("🚣\u200d♂", "🚣\u200d♂ (:man_rowing_boat:)"), + ( + "🚣🏿\u200d♂️", + "🚣🏿\u200d♂️ (:man_rowing_boat_dark_skin_tone:)", + ), + ( + "🚣🏿\u200d♂", + "🚣🏿\u200d♂ (:man_rowing_boat_dark_skin_tone:)", + ), + ( + "🚣🏻\u200d♂️", + "🚣🏻\u200d♂️ (:man_rowing_boat_light_skin_tone:)", + ), + ( + "🚣🏻\u200d♂", + "🚣🏻\u200d♂ (:man_rowing_boat_light_skin_tone:)", + ), + ( + "🚣🏾\u200d♂️", + "🚣🏾\u200d♂️ (:man_rowing_boat_medium-dark_skin_tone:)", + ), + ( + "🚣🏾\u200d♂", + "🚣🏾\u200d♂ (:man_rowing_boat_medium-dark_skin_tone:)", + ), + ( + "🚣🏼\u200d♂️", + "🚣🏼\u200d♂️ (:man_rowing_boat_medium-light_skin_tone:)", + ), + ( + "🚣🏼\u200d♂", + "🚣🏼\u200d♂ (:man_rowing_boat_medium-light_skin_tone:)", + ), + ( + "🚣🏽\u200d♂️", + "🚣🏽\u200d♂️ (:man_rowing_boat_medium_skin_tone:)", + ), + ( + "🚣🏽\u200d♂", + "🚣🏽\u200d♂ (:man_rowing_boat_medium_skin_tone:)", + ), + ("🏃\u200d♂️", "🏃\u200d♂️ (:man_running:)"), + ("🏃\u200d♂", "🏃\u200d♂ (:man_running:)"), + ("🏃🏿\u200d♂️", "🏃🏿\u200d♂️ (:man_running_dark_skin_tone:)"), + ("🏃🏿\u200d♂", "🏃🏿\u200d♂ (:man_running_dark_skin_tone:)"), + ( + "🏃\u200d♂️\u200d➡️", + "🏃\u200d♂️\u200d➡️ (:man_running_facing_right:)", + ), + ( + "🏃\u200d♂\u200d➡️", + "🏃\u200d♂\u200d➡️ (:man_running_facing_right:)", + ), + ( + "🏃\u200d♂️\u200d➡", + "🏃\u200d♂️\u200d➡ (:man_running_facing_right:)", + ), + ( + "🏃\u200d♂\u200d➡", + "🏃\u200d♂\u200d➡ (:man_running_facing_right:)", + ), + ( + "🏃🏿\u200d♂️\u200d➡️", + "🏃🏿\u200d♂️\u200d➡️ (:man_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♂\u200d➡️", + "🏃🏿\u200d♂\u200d➡️ (:man_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♂️\u200d➡", + "🏃🏿\u200d♂️\u200d➡ (:man_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♂\u200d➡", + "🏃🏿\u200d♂\u200d➡ (:man_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏻\u200d♂️\u200d➡️", + "🏃🏻\u200d♂️\u200d➡️ (:man_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♂\u200d➡️", + "🏃🏻\u200d♂\u200d➡️ (:man_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♂️\u200d➡", + "🏃🏻\u200d♂️\u200d➡ (:man_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♂\u200d➡", + "🏃🏻\u200d♂\u200d➡ (:man_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏾\u200d♂️\u200d➡️", + "🏃🏾\u200d♂️\u200d➡️ (:man_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♂\u200d➡️", + "🏃🏾\u200d♂\u200d➡️ (:man_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♂️\u200d➡", + "🏃🏾\u200d♂️\u200d➡ (:man_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♂\u200d➡", + "🏃🏾\u200d♂\u200d➡ (:man_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏼\u200d♂️\u200d➡️", + "🏃🏼\u200d♂️\u200d➡️ (:man_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♂\u200d➡️", + "🏃🏼\u200d♂\u200d➡️ (:man_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♂️\u200d➡", + "🏃🏼\u200d♂️\u200d➡ (:man_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♂\u200d➡", + "🏃🏼\u200d♂\u200d➡ (:man_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏽\u200d♂️\u200d➡️", + "🏃🏽\u200d♂️\u200d➡️ (:man_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♂\u200d➡️", + "🏃🏽\u200d♂\u200d➡️ (:man_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♂️\u200d➡", + "🏃🏽\u200d♂️\u200d➡ (:man_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♂\u200d➡", + "🏃🏽\u200d♂\u200d➡ (:man_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏻\u200d♂️", + "🏃🏻\u200d♂️ (:man_running_light_skin_tone:)", + ), + ("🏃🏻\u200d♂", "🏃🏻\u200d♂ (:man_running_light_skin_tone:)"), + ( + "🏃🏾\u200d♂️", + "🏃🏾\u200d♂️ (:man_running_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♂", + "🏃🏾\u200d♂ (:man_running_medium-dark_skin_tone:)", + ), + ( + "🏃🏼\u200d♂️", + "🏃🏼\u200d♂️ (:man_running_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♂", + "🏃🏼\u200d♂ (:man_running_medium-light_skin_tone:)", + ), + ( + "🏃🏽\u200d♂️", + "🏃🏽\u200d♂️ (:man_running_medium_skin_tone:)", + ), + ("🏃🏽\u200d♂", "🏃🏽\u200d♂ (:man_running_medium_skin_tone:)"), + ("👨\u200d🔬", "👨\u200d🔬 (:man_scientist:)"), + ("👨🏿\u200d🔬", "👨🏿\u200d🔬 (:man_scientist_dark_skin_tone:)"), + ( + "👨🏻\u200d🔬", + "👨🏻\u200d🔬 (:man_scientist_light_skin_tone:)", + ), + ( + "👨🏾\u200d🔬", + "👨🏾\u200d🔬 (:man_scientist_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🔬", + "👨🏼\u200d🔬 (:man_scientist_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🔬", + "👨🏽\u200d🔬 (:man_scientist_medium_skin_tone:)", + ), + ("🤷\u200d♂️", "🤷\u200d♂️ (:man_shrugging:)"), + ("🤷\u200d♂", "🤷\u200d♂ (:man_shrugging:)"), + ( + "🤷🏿\u200d♂️", + "🤷🏿\u200d♂️ (:man_shrugging_dark_skin_tone:)", + ), + ("🤷🏿\u200d♂", "🤷🏿\u200d♂ (:man_shrugging_dark_skin_tone:)"), + ( + "🤷🏻\u200d♂️", + "🤷🏻\u200d♂️ (:man_shrugging_light_skin_tone:)", + ), + ( + "🤷🏻\u200d♂", + "🤷🏻\u200d♂ (:man_shrugging_light_skin_tone:)", + ), + ( + "🤷🏾\u200d♂️", + "🤷🏾\u200d♂️ (:man_shrugging_medium-dark_skin_tone:)", + ), + ( + "🤷🏾\u200d♂", + "🤷🏾\u200d♂ (:man_shrugging_medium-dark_skin_tone:)", + ), + ( + "🤷🏼\u200d♂️", + "🤷🏼\u200d♂️ (:man_shrugging_medium-light_skin_tone:)", + ), + ( + "🤷🏼\u200d♂", + "🤷🏼\u200d♂ (:man_shrugging_medium-light_skin_tone:)", + ), + ( + "🤷🏽\u200d♂️", + "🤷🏽\u200d♂️ (:man_shrugging_medium_skin_tone:)", + ), + ( + "🤷🏽\u200d♂", + "🤷🏽\u200d♂ (:man_shrugging_medium_skin_tone:)", + ), + ("👨\u200d🎤", "👨\u200d🎤 (:man_singer:)"), + ("👨🏿\u200d🎤", "👨🏿\u200d🎤 (:man_singer_dark_skin_tone:)"), + ("👨🏻\u200d🎤", "👨🏻\u200d🎤 (:man_singer_light_skin_tone:)"), + ( + "👨🏾\u200d🎤", + "👨🏾\u200d🎤 (:man_singer_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🎤", + "👨🏼\u200d🎤 (:man_singer_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🎤", "👨🏽\u200d🎤 (:man_singer_medium_skin_tone:)"), + ("🧍\u200d♂️", "🧍\u200d♂️ (:man_standing:)"), + ("🧍\u200d♂", "🧍\u200d♂ (:man_standing:)"), + ( + "🧍🏿\u200d♂️", + "🧍🏿\u200d♂️ (:man_standing_dark_skin_tone:)", + ), + ("🧍🏿\u200d♂", "🧍🏿\u200d♂ (:man_standing_dark_skin_tone:)"), + ( + "🧍🏻\u200d♂️", + "🧍🏻\u200d♂️ (:man_standing_light_skin_tone:)", + ), + ("🧍🏻\u200d♂", "🧍🏻\u200d♂ (:man_standing_light_skin_tone:)"), + ( + "🧍🏾\u200d♂️", + "🧍🏾\u200d♂️ (:man_standing_medium-dark_skin_tone:)", + ), + ( + "🧍🏾\u200d♂", + "🧍🏾\u200d♂ (:man_standing_medium-dark_skin_tone:)", + ), + ( + "🧍🏼\u200d♂️", + "🧍🏼\u200d♂️ (:man_standing_medium-light_skin_tone:)", + ), + ( + "🧍🏼\u200d♂", + "🧍🏼\u200d♂ (:man_standing_medium-light_skin_tone:)", + ), + ( + "🧍🏽\u200d♂️", + "🧍🏽\u200d♂️ (:man_standing_medium_skin_tone:)", + ), + ( + "🧍🏽\u200d♂", + "🧍🏽\u200d♂ (:man_standing_medium_skin_tone:)", + ), + ("👨\u200d🎓", "👨\u200d🎓 (:man_student:)"), + ("👨🏿\u200d🎓", "👨🏿\u200d🎓 (:man_student_dark_skin_tone:)"), + ("👨🏻\u200d🎓", "👨🏻\u200d🎓 (:man_student_light_skin_tone:)"), + ( + "👨🏾\u200d🎓", + "👨🏾\u200d🎓 (:man_student_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🎓", + "👨🏼\u200d🎓 (:man_student_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🎓", "👨🏽\u200d🎓 (:man_student_medium_skin_tone:)"), + ("🦸\u200d♂️", "🦸\u200d♂️ (:man_superhero:)"), + ("🦸\u200d♂", "🦸\u200d♂ (:man_superhero:)"), + ( + "🦸🏿\u200d♂️", + "🦸🏿\u200d♂️ (:man_superhero_dark_skin_tone:)", + ), + ("🦸🏿\u200d♂", "🦸🏿\u200d♂ (:man_superhero_dark_skin_tone:)"), + ( + "🦸🏻\u200d♂️", + "🦸🏻\u200d♂️ (:man_superhero_light_skin_tone:)", + ), + ( + "🦸🏻\u200d♂", + "🦸🏻\u200d♂ (:man_superhero_light_skin_tone:)", + ), + ( + "🦸🏾\u200d♂️", + "🦸🏾\u200d♂️ (:man_superhero_medium-dark_skin_tone:)", + ), + ( + "🦸🏾\u200d♂", + "🦸🏾\u200d♂ (:man_superhero_medium-dark_skin_tone:)", + ), + ( + "🦸🏼\u200d♂️", + "🦸🏼\u200d♂️ (:man_superhero_medium-light_skin_tone:)", + ), + ( + "🦸🏼\u200d♂", + "🦸🏼\u200d♂ (:man_superhero_medium-light_skin_tone:)", + ), + ( + "🦸🏽\u200d♂️", + "🦸🏽\u200d♂️ (:man_superhero_medium_skin_tone:)", + ), + ( + "🦸🏽\u200d♂", + "🦸🏽\u200d♂ (:man_superhero_medium_skin_tone:)", + ), + ("🦹\u200d♂️", "🦹\u200d♂️ (:man_supervillain:)"), + ("🦹\u200d♂", "🦹\u200d♂ (:man_supervillain:)"), + ( + "🦹🏿\u200d♂️", + "🦹🏿\u200d♂️ (:man_supervillain_dark_skin_tone:)", + ), + ( + "🦹🏿\u200d♂", + "🦹🏿\u200d♂ (:man_supervillain_dark_skin_tone:)", + ), + ( + "🦹🏻\u200d♂️", + "🦹🏻\u200d♂️ (:man_supervillain_light_skin_tone:)", + ), + ( + "🦹🏻\u200d♂", + "🦹🏻\u200d♂ (:man_supervillain_light_skin_tone:)", + ), + ( + "🦹🏾\u200d♂️", + "🦹🏾\u200d♂️ (:man_supervillain_medium-dark_skin_tone:)", + ), + ( + "🦹🏾\u200d♂", + "🦹🏾\u200d♂ (:man_supervillain_medium-dark_skin_tone:)", + ), + ( + "🦹🏼\u200d♂️", + "🦹🏼\u200d♂️ (:man_supervillain_medium-light_skin_tone:)", + ), + ( + "🦹🏼\u200d♂", + "🦹🏼\u200d♂ (:man_supervillain_medium-light_skin_tone:)", + ), + ( + "🦹🏽\u200d♂️", + "🦹🏽\u200d♂️ (:man_supervillain_medium_skin_tone:)", + ), + ( + "🦹🏽\u200d♂", + "🦹🏽\u200d♂ (:man_supervillain_medium_skin_tone:)", + ), + ("🏄\u200d♂️", "🏄\u200d♂️ (:man_surfing:)"), + ("🏄\u200d♂", "🏄\u200d♂ (:man_surfing:)"), + ("🏄🏿\u200d♂️", "🏄🏿\u200d♂️ (:man_surfing_dark_skin_tone:)"), + ("🏄🏿\u200d♂", "🏄🏿\u200d♂ (:man_surfing_dark_skin_tone:)"), + ( + "🏄🏻\u200d♂️", + "🏄🏻\u200d♂️ (:man_surfing_light_skin_tone:)", + ), + ("🏄🏻\u200d♂", "🏄🏻\u200d♂ (:man_surfing_light_skin_tone:)"), + ( + "🏄🏾\u200d♂️", + "🏄🏾\u200d♂️ (:man_surfing_medium-dark_skin_tone:)", + ), + ( + "🏄🏾\u200d♂", + "🏄🏾\u200d♂ (:man_surfing_medium-dark_skin_tone:)", + ), + ( + "🏄🏼\u200d♂️", + "🏄🏼\u200d♂️ (:man_surfing_medium-light_skin_tone:)", + ), + ( + "🏄🏼\u200d♂", + "🏄🏼\u200d♂ (:man_surfing_medium-light_skin_tone:)", + ), + ( + "🏄🏽\u200d♂️", + "🏄🏽\u200d♂️ (:man_surfing_medium_skin_tone:)", + ), + ("🏄🏽\u200d♂", "🏄🏽\u200d♂ (:man_surfing_medium_skin_tone:)"), + ("🏊\u200d♂️", "🏊\u200d♂️ (:man_swimming:)"), + ("🏊\u200d♂", "🏊\u200d♂ (:man_swimming:)"), + ( + "🏊🏿\u200d♂️", + "🏊🏿\u200d♂️ (:man_swimming_dark_skin_tone:)", + ), + ("🏊🏿\u200d♂", "🏊🏿\u200d♂ (:man_swimming_dark_skin_tone:)"), + ( + "🏊🏻\u200d♂️", + "🏊🏻\u200d♂️ (:man_swimming_light_skin_tone:)", + ), + ("🏊🏻\u200d♂", "🏊🏻\u200d♂ (:man_swimming_light_skin_tone:)"), + ( + "🏊🏾\u200d♂️", + "🏊🏾\u200d♂️ (:man_swimming_medium-dark_skin_tone:)", + ), + ( + "🏊🏾\u200d♂", + "🏊🏾\u200d♂ (:man_swimming_medium-dark_skin_tone:)", + ), + ( + "🏊🏼\u200d♂️", + "🏊🏼\u200d♂️ (:man_swimming_medium-light_skin_tone:)", + ), + ( + "🏊🏼\u200d♂", + "🏊🏼\u200d♂ (:man_swimming_medium-light_skin_tone:)", + ), + ( + "🏊🏽\u200d♂️", + "🏊🏽\u200d♂️ (:man_swimming_medium_skin_tone:)", + ), + ( + "🏊🏽\u200d♂", + "🏊🏽\u200d♂ (:man_swimming_medium_skin_tone:)", + ), + ("👨\u200d🏫", "👨\u200d🏫 (:man_teacher:)"), + ("👨🏿\u200d🏫", "👨🏿\u200d🏫 (:man_teacher_dark_skin_tone:)"), + ("👨🏻\u200d🏫", "👨🏻\u200d🏫 (:man_teacher_light_skin_tone:)"), + ( + "👨🏾\u200d🏫", + "👨🏾\u200d🏫 (:man_teacher_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🏫", + "👨🏼\u200d🏫 (:man_teacher_medium-light_skin_tone:)", + ), + ("👨🏽\u200d🏫", "👨🏽\u200d🏫 (:man_teacher_medium_skin_tone:)"), + ("👨\u200d💻", "👨\u200d💻 (:man_technologist:)"), + ( + "👨🏿\u200d💻", + "👨🏿\u200d💻 (:man_technologist_dark_skin_tone:)", + ), + ( + "👨🏻\u200d💻", + "👨🏻\u200d💻 (:man_technologist_light_skin_tone:)", + ), + ( + "👨🏾\u200d💻", + "👨🏾\u200d💻 (:man_technologist_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d💻", + "👨🏼\u200d💻 (:man_technologist_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d💻", + "👨🏽\u200d💻 (:man_technologist_medium_skin_tone:)", + ), + ("💁\u200d♂️", "💁\u200d♂️ (:man_tipping_hand:)"), + ("💁\u200d♂", "💁\u200d♂ (:man_tipping_hand:)"), + ( + "💁🏿\u200d♂️", + "💁🏿\u200d♂️ (:man_tipping_hand_dark_skin_tone:)", + ), + ( + "💁🏿\u200d♂", + "💁🏿\u200d♂ (:man_tipping_hand_dark_skin_tone:)", + ), + ( + "💁🏻\u200d♂️", + "💁🏻\u200d♂️ (:man_tipping_hand_light_skin_tone:)", + ), + ( + "💁🏻\u200d♂", + "💁🏻\u200d♂ (:man_tipping_hand_light_skin_tone:)", + ), + ( + "💁🏾\u200d♂️", + "💁🏾\u200d♂️ (:man_tipping_hand_medium-dark_skin_tone:)", + ), + ( + "💁🏾\u200d♂", + "💁🏾\u200d♂ (:man_tipping_hand_medium-dark_skin_tone:)", + ), + ( + "💁🏼\u200d♂️", + "💁🏼\u200d♂️ (:man_tipping_hand_medium-light_skin_tone:)", + ), + ( + "💁🏼\u200d♂", + "💁🏼\u200d♂ (:man_tipping_hand_medium-light_skin_tone:)", + ), + ( + "💁🏽\u200d♂️", + "💁🏽\u200d♂️ (:man_tipping_hand_medium_skin_tone:)", + ), + ( + "💁🏽\u200d♂", + "💁🏽\u200d♂ (:man_tipping_hand_medium_skin_tone:)", + ), + ("🧛\u200d♂️", "🧛\u200d♂️ (:man_vampire:)"), + ("🧛\u200d♂", "🧛\u200d♂ (:man_vampire:)"), + ("🧛🏿\u200d♂️", "🧛🏿\u200d♂️ (:man_vampire_dark_skin_tone:)"), + ("🧛🏿\u200d♂", "🧛🏿\u200d♂ (:man_vampire_dark_skin_tone:)"), + ( + "🧛🏻\u200d♂️", + "🧛🏻\u200d♂️ (:man_vampire_light_skin_tone:)", + ), + ("🧛🏻\u200d♂", "🧛🏻\u200d♂ (:man_vampire_light_skin_tone:)"), + ( + "🧛🏾\u200d♂️", + "🧛🏾\u200d♂️ (:man_vampire_medium-dark_skin_tone:)", + ), + ( + "🧛🏾\u200d♂", + "🧛🏾\u200d♂ (:man_vampire_medium-dark_skin_tone:)", + ), + ( + "🧛🏼\u200d♂️", + "🧛🏼\u200d♂️ (:man_vampire_medium-light_skin_tone:)", + ), + ( + "🧛🏼\u200d♂", + "🧛🏼\u200d♂ (:man_vampire_medium-light_skin_tone:)", + ), + ( + "🧛🏽\u200d♂️", + "🧛🏽\u200d♂️ (:man_vampire_medium_skin_tone:)", + ), + ("🧛🏽\u200d♂", "🧛🏽\u200d♂ (:man_vampire_medium_skin_tone:)"), + ("🚶\u200d♂️", "🚶\u200d♂️ (:man_walking:)"), + ("🚶\u200d♂", "🚶\u200d♂ (:man_walking:)"), + ("🚶🏿\u200d♂️", "🚶🏿\u200d♂️ (:man_walking_dark_skin_tone:)"), + ("🚶🏿\u200d♂", "🚶🏿\u200d♂ (:man_walking_dark_skin_tone:)"), + ( + "🚶\u200d♂️\u200d➡️", + "🚶\u200d♂️\u200d➡️ (:man_walking_facing_right:)", + ), + ( + "🚶\u200d♂\u200d➡️", + "🚶\u200d♂\u200d➡️ (:man_walking_facing_right:)", + ), + ( + "🚶\u200d♂️\u200d➡", + "🚶\u200d♂️\u200d➡ (:man_walking_facing_right:)", + ), + ( + "🚶\u200d♂\u200d➡", + "🚶\u200d♂\u200d➡ (:man_walking_facing_right:)", + ), + ( + "🚶🏿\u200d♂️\u200d➡️", + "🚶🏿\u200d♂️\u200d➡️ (:man_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♂\u200d➡️", + "🚶🏿\u200d♂\u200d➡️ (:man_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♂️\u200d➡", + "🚶🏿\u200d♂️\u200d➡ (:man_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♂\u200d➡", + "🚶🏿\u200d♂\u200d➡ (:man_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏻\u200d♂️\u200d➡️", + "🚶🏻\u200d♂️\u200d➡️ (:man_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♂\u200d➡️", + "🚶🏻\u200d♂\u200d➡️ (:man_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♂️\u200d➡", + "🚶🏻\u200d♂️\u200d➡ (:man_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♂\u200d➡", + "🚶🏻\u200d♂\u200d➡ (:man_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏾\u200d♂️\u200d➡️", + "🚶🏾\u200d♂️\u200d➡️ (:man_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♂\u200d➡️", + "🚶🏾\u200d♂\u200d➡️ (:man_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♂️\u200d➡", + "🚶🏾\u200d♂️\u200d➡ (:man_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♂\u200d➡", + "🚶🏾\u200d♂\u200d➡ (:man_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏼\u200d♂️\u200d➡️", + "🚶🏼\u200d♂️\u200d➡️ (:man_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♂\u200d➡️", + "🚶🏼\u200d♂\u200d➡️ (:man_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♂️\u200d➡", + "🚶🏼\u200d♂️\u200d➡ (:man_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♂\u200d➡", + "🚶🏼\u200d♂\u200d➡ (:man_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏽\u200d♂️\u200d➡️", + "🚶🏽\u200d♂️\u200d➡️ (:man_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♂\u200d➡️", + "🚶🏽\u200d♂\u200d➡️ (:man_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♂️\u200d➡", + "🚶🏽\u200d♂️\u200d➡ (:man_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♂\u200d➡", + "🚶🏽\u200d♂\u200d➡ (:man_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏻\u200d♂️", + "🚶🏻\u200d♂️ (:man_walking_light_skin_tone:)", + ), + ("🚶🏻\u200d♂", "🚶🏻\u200d♂ (:man_walking_light_skin_tone:)"), + ( + "🚶🏾\u200d♂️", + "🚶🏾\u200d♂️ (:man_walking_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♂", + "🚶🏾\u200d♂ (:man_walking_medium-dark_skin_tone:)", + ), + ( + "🚶🏼\u200d♂️", + "🚶🏼\u200d♂️ (:man_walking_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♂", + "🚶🏼\u200d♂ (:man_walking_medium-light_skin_tone:)", + ), + ( + "🚶🏽\u200d♂️", + "🚶🏽\u200d♂️ (:man_walking_medium_skin_tone:)", + ), + ("🚶🏽\u200d♂", "🚶🏽\u200d♂ (:man_walking_medium_skin_tone:)"), + ("👳\u200d♂️", "👳\u200d♂️ (:man_wearing_turban:)"), + ("👳\u200d♂", "👳\u200d♂ (:man_wearing_turban:)"), + ( + "👳🏿\u200d♂️", + "👳🏿\u200d♂️ (:man_wearing_turban_dark_skin_tone:)", + ), + ( + "👳🏿\u200d♂", + "👳🏿\u200d♂ (:man_wearing_turban_dark_skin_tone:)", + ), + ( + "👳🏻\u200d♂️", + "👳🏻\u200d♂️ (:man_wearing_turban_light_skin_tone:)", + ), + ( + "👳🏻\u200d♂", + "👳🏻\u200d♂ (:man_wearing_turban_light_skin_tone:)", + ), + ( + "👳🏾\u200d♂️", + "👳🏾\u200d♂️ (:man_wearing_turban_medium-dark_skin_tone:)", + ), + ( + "👳🏾\u200d♂", + "👳🏾\u200d♂ (:man_wearing_turban_medium-dark_skin_tone:)", + ), + ( + "👳🏼\u200d♂️", + "👳🏼\u200d♂️ (:man_wearing_turban_medium-light_skin_tone:)", + ), + ( + "👳🏼\u200d♂", + "👳🏼\u200d♂ (:man_wearing_turban_medium-light_skin_tone:)", + ), + ( + "👳🏽\u200d♂️", + "👳🏽\u200d♂️ (:man_wearing_turban_medium_skin_tone:)", + ), + ( + "👳🏽\u200d♂", + "👳🏽\u200d♂ (:man_wearing_turban_medium_skin_tone:)", + ), + ("👨\u200d🦳", "👨\u200d🦳 (:man_white_hair:)"), + ("👰\u200d♂️", "👰\u200d♂️ (:man_with_veil:)"), + ("👰\u200d♂", "👰\u200d♂ (:man_with_veil:)"), + ( + "👰🏿\u200d♂️", + "👰🏿\u200d♂️ (:man_with_veil_dark_skin_tone:)", + ), + ("👰🏿\u200d♂", "👰🏿\u200d♂ (:man_with_veil_dark_skin_tone:)"), + ( + "👰🏻\u200d♂️", + "👰🏻\u200d♂️ (:man_with_veil_light_skin_tone:)", + ), + ( + "👰🏻\u200d♂", + "👰🏻\u200d♂ (:man_with_veil_light_skin_tone:)", + ), + ( + "👰🏾\u200d♂️", + "👰🏾\u200d♂️ (:man_with_veil_medium-dark_skin_tone:)", + ), + ( + "👰🏾\u200d♂", + "👰🏾\u200d♂ (:man_with_veil_medium-dark_skin_tone:)", + ), + ( + "👰🏼\u200d♂️", + "👰🏼\u200d♂️ (:man_with_veil_medium-light_skin_tone:)", + ), + ( + "👰🏼\u200d♂", + "👰🏼\u200d♂ (:man_with_veil_medium-light_skin_tone:)", + ), + ( + "👰🏽\u200d♂️", + "👰🏽\u200d♂️ (:man_with_veil_medium_skin_tone:)", + ), + ( + "👰🏽\u200d♂", + "👰🏽\u200d♂ (:man_with_veil_medium_skin_tone:)", + ), + ("👨\u200d🦯", "👨\u200d🦯 (:man_with_white_cane:)"), + ( + "👨🏿\u200d🦯", + "👨🏿\u200d🦯 (:man_with_white_cane_dark_skin_tone:)", + ), + ( + "👨\u200d🦯\u200d➡️", + "👨\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right:)", + ), + ( + "👨\u200d🦯\u200d➡", + "👨\u200d🦯\u200d➡ (:man_with_white_cane_facing_right:)", + ), + ( + "👨🏿\u200d🦯\u200d➡️", + "👨🏿\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "👨🏿\u200d🦯\u200d➡", + "👨🏿\u200d🦯\u200d➡ (:man_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🦯\u200d➡️", + "👨🏻\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "👨🏻\u200d🦯\u200d➡", + "👨🏻\u200d🦯\u200d➡ (:man_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦯\u200d➡️", + "👨🏾\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏾\u200d🦯\u200d➡", + "👨🏾\u200d🦯\u200d➡ (:man_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦯\u200d➡️", + "👨🏼\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏼\u200d🦯\u200d➡", + "👨🏼\u200d🦯\u200d➡ (:man_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦯\u200d➡️", + "👨🏽\u200d🦯\u200d➡️ (:man_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "👨🏽\u200d🦯\u200d➡", + "👨🏽\u200d🦯\u200d➡ (:man_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "👨🏻\u200d🦯", + "👨🏻\u200d🦯 (:man_with_white_cane_light_skin_tone:)", + ), + ( + "👨🏾\u200d🦯", + "👨🏾\u200d🦯 (:man_with_white_cane_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🦯", + "👨🏼\u200d🦯 (:man_with_white_cane_medium-light_skin_tone:)", + ), + ( + "👨🏽\u200d🦯", + "👨🏽\u200d🦯 (:man_with_white_cane_medium_skin_tone:)", + ), + ("🧟\u200d♂️", "🧟\u200d♂️ (:man_zombie:)"), + ("🧟\u200d♂", "🧟\u200d♂ (:man_zombie:)"), + ("🥭", "🥭 (:mango:)"), + ("🕰️", "🕰️ (:mantelpiece_clock:)"), + ("🕰", "🕰 (:mantelpiece_clock:)"), + ("🦽", "🦽 (:manual_wheelchair:)"), + ("👞", "👞 (:man’s_shoe:)"), + ("🗾", "🗾 (:map_of_Japan:)"), + ("🍁", "🍁 (:maple_leaf:)"), + ("\U0001fa87", "\U0001fa87 (:maracas:)"), + ("🥋", "🥋 (:martial_arts_uniform:)"), + ("🧉", "🧉 (:mate:)"), + ("🍖", "🍖 (:meat_on_bone:)"), + ("🧑\u200d🔧", "🧑\u200d🔧 (:mechanic:)"), + ("🧑🏿\u200d🔧", "🧑🏿\u200d🔧 (:mechanic_dark_skin_tone:)"), + ("🧑🏻\u200d🔧", "🧑🏻\u200d🔧 (:mechanic_light_skin_tone:)"), + ( + "🧑🏾\u200d🔧", + "🧑🏾\u200d🔧 (:mechanic_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🔧", + "🧑🏼\u200d🔧 (:mechanic_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🔧", "🧑🏽\u200d🔧 (:mechanic_medium_skin_tone:)"), + ("🦾", "🦾 (:mechanical_arm:)"), + ("🦿", "🦿 (:mechanical_leg:)"), + ("⚕️", "⚕️ (:medical_symbol:)"), + ("⚕", "⚕ (:medical_symbol:)"), + ("🏾", "🏾 (:medium-dark_skin_tone:)"), + ("🏼", "🏼 (:medium-light_skin_tone:)"), + ("🏽", "🏽 (:medium_skin_tone:)"), + ("📣", "📣 (:megaphone:)"), + ("🍈", "🍈 (:melon:)"), + ("\U0001fae0", "\U0001fae0 (:melting_face:)"), + ("📝", "📝 (:memo:)"), + ("👬", "👬 (:men_holding_hands:)"), + ("👬🏿", "👬🏿 (:men_holding_hands_dark_skin_tone:)"), + ( + "👨🏿\u200d🤝\u200d👨🏻", + "👨🏿\u200d🤝\u200d👨🏻 (:men_holding_hands_dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏿\u200d🤝\u200d👨🏾", + "👨🏿\u200d🤝\u200d👨🏾 (:men_holding_hands_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏿\u200d🤝\u200d👨🏼", + "👨🏿\u200d🤝\u200d👨🏼 (:men_holding_hands_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏿\u200d🤝\u200d👨🏽", + "👨🏿\u200d🤝\u200d👨🏽 (:men_holding_hands_dark_skin_tone_medium_skin_tone:)", + ), + ("👬🏻", "👬🏻 (:men_holding_hands_light_skin_tone:)"), + ( + "👨🏻\u200d🤝\u200d👨🏿", + "👨🏻\u200d🤝\u200d👨🏿 (:men_holding_hands_light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏻\u200d🤝\u200d👨🏾", + "👨🏻\u200d🤝\u200d👨🏾 (:men_holding_hands_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏻\u200d🤝\u200d👨🏼", + "👨🏻\u200d🤝\u200d👨🏼 (:men_holding_hands_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏻\u200d🤝\u200d👨🏽", + "👨🏻\u200d🤝\u200d👨🏽 (:men_holding_hands_light_skin_tone_medium_skin_tone:)", + ), + ("👬🏾", "👬🏾 (:men_holding_hands_medium-dark_skin_tone:)"), + ( + "👨🏾\u200d🤝\u200d👨🏿", + "👨🏾\u200d🤝\u200d👨🏿 (:men_holding_hands_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏾\u200d🤝\u200d👨🏻", + "👨🏾\u200d🤝\u200d👨🏻 (:men_holding_hands_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👨🏾\u200d🤝\u200d👨🏼", + "👨🏾\u200d🤝\u200d👨🏼 (:men_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👨🏾\u200d🤝\u200d👨🏽", + "👨🏾\u200d🤝\u200d👨🏽 (:men_holding_hands_medium-dark_skin_tone_medium_skin_tone:)", + ), + ("👬🏼", "👬🏼 (:men_holding_hands_medium-light_skin_tone:)"), + ( + "👨🏼\u200d🤝\u200d👨🏿", + "👨🏼\u200d🤝\u200d👨🏿 (:men_holding_hands_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏼\u200d🤝\u200d👨🏻", + "👨🏼\u200d🤝\u200d👨🏻 (:men_holding_hands_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👨🏼\u200d🤝\u200d👨🏾", + "👨🏼\u200d🤝\u200d👨🏾 (:men_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏼\u200d🤝\u200d👨🏽", + "👨🏼\u200d🤝\u200d👨🏽 (:men_holding_hands_medium-light_skin_tone_medium_skin_tone:)", + ), + ("👬🏽", "👬🏽 (:men_holding_hands_medium_skin_tone:)"), + ( + "👨🏽\u200d🤝\u200d👨🏿", + "👨🏽\u200d🤝\u200d👨🏿 (:men_holding_hands_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👨🏽\u200d🤝\u200d👨🏻", + "👨🏽\u200d🤝\u200d👨🏻 (:men_holding_hands_medium_skin_tone_light_skin_tone:)", + ), + ( + "👨🏽\u200d🤝\u200d👨🏾", + "👨🏽\u200d🤝\u200d👨🏾 (:men_holding_hands_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👨🏽\u200d🤝\u200d👨🏼", + "👨🏽\u200d🤝\u200d👨🏼 (:men_holding_hands_medium_skin_tone_medium-light_skin_tone:)", + ), + ("👯\u200d♂️", "👯\u200d♂️ (:men_with_bunny_ears:)"), + ("👯\u200d♂", "👯\u200d♂ (:men_with_bunny_ears:)"), + ("🤼\u200d♂️", "🤼\u200d♂️ (:men_wrestling:)"), + ("🤼\u200d♂", "🤼\u200d♂ (:men_wrestling:)"), + ("❤️\u200d🩹", "❤️\u200d🩹 (:mending_heart:)"), + ("❤\u200d🩹", "❤\u200d🩹 (:mending_heart:)"), + ("🕎", "🕎 (:menorah:)"), + ("🚹", "🚹 (:men’s_room:)"), + ("🧜\u200d♀️", "🧜\u200d♀️ (:mermaid:)"), + ("🧜\u200d♀", "🧜\u200d♀ (:mermaid:)"), + ("🧜🏿\u200d♀️", "🧜🏿\u200d♀️ (:mermaid_dark_skin_tone:)"), + ("🧜🏿\u200d♀", "🧜🏿\u200d♀ (:mermaid_dark_skin_tone:)"), + ("🧜🏻\u200d♀️", "🧜🏻\u200d♀️ (:mermaid_light_skin_tone:)"), + ("🧜🏻\u200d♀", "🧜🏻\u200d♀ (:mermaid_light_skin_tone:)"), + ( + "🧜🏾\u200d♀️", + "🧜🏾\u200d♀️ (:mermaid_medium-dark_skin_tone:)", + ), + ( + "🧜🏾\u200d♀", + "🧜🏾\u200d♀ (:mermaid_medium-dark_skin_tone:)", + ), + ( + "🧜🏼\u200d♀️", + "🧜🏼\u200d♀️ (:mermaid_medium-light_skin_tone:)", + ), + ( + "🧜🏼\u200d♀", + "🧜🏼\u200d♀ (:mermaid_medium-light_skin_tone:)", + ), + ("🧜🏽\u200d♀️", "🧜🏽\u200d♀️ (:mermaid_medium_skin_tone:)"), + ("🧜🏽\u200d♀", "🧜🏽\u200d♀ (:mermaid_medium_skin_tone:)"), + ("🧜\u200d♂️", "🧜\u200d♂️ (:merman:)"), + ("🧜\u200d♂", "🧜\u200d♂ (:merman:)"), + ("🧜🏿\u200d♂️", "🧜🏿\u200d♂️ (:merman_dark_skin_tone:)"), + ("🧜🏿\u200d♂", "🧜🏿\u200d♂ (:merman_dark_skin_tone:)"), + ("🧜🏻\u200d♂️", "🧜🏻\u200d♂️ (:merman_light_skin_tone:)"), + ("🧜🏻\u200d♂", "🧜🏻\u200d♂ (:merman_light_skin_tone:)"), + ( + "🧜🏾\u200d♂️", + "🧜🏾\u200d♂️ (:merman_medium-dark_skin_tone:)", + ), + ("🧜🏾\u200d♂", "🧜🏾\u200d♂ (:merman_medium-dark_skin_tone:)"), + ( + "🧜🏼\u200d♂️", + "🧜🏼\u200d♂️ (:merman_medium-light_skin_tone:)", + ), + ( + "🧜🏼\u200d♂", + "🧜🏼\u200d♂ (:merman_medium-light_skin_tone:)", + ), + ("🧜🏽\u200d♂️", "🧜🏽\u200d♂️ (:merman_medium_skin_tone:)"), + ("🧜🏽\u200d♂", "🧜🏽\u200d♂ (:merman_medium_skin_tone:)"), + ("🧜", "🧜 (:merperson:)"), + ("🧜🏿", "🧜🏿 (:merperson_dark_skin_tone:)"), + ("🧜🏻", "🧜🏻 (:merperson_light_skin_tone:)"), + ("🧜🏾", "🧜🏾 (:merperson_medium-dark_skin_tone:)"), + ("🧜🏼", "🧜🏼 (:merperson_medium-light_skin_tone:)"), + ("🧜🏽", "🧜🏽 (:merperson_medium_skin_tone:)"), + ("🚇", "🚇 (:metro:)"), + ("🦠", "🦠 (:microbe:)"), + ("🎤", "🎤 (:microphone:)"), + ("🔬", "🔬 (:microscope:)"), + ("🖕", "🖕 (:middle_finger:)"), + ("🖕🏿", "🖕🏿 (:middle_finger_dark_skin_tone:)"), + ("🖕🏻", "🖕🏻 (:middle_finger_light_skin_tone:)"), + ("🖕🏾", "🖕🏾 (:middle_finger_medium-dark_skin_tone:)"), + ("🖕🏼", "🖕🏼 (:middle_finger_medium-light_skin_tone:)"), + ("🖕🏽", "🖕🏽 (:middle_finger_medium_skin_tone:)"), + ("🪖", "🪖 (:military_helmet:)"), + ("🎖️", "🎖️ (:military_medal:)"), + ("🎖", "🎖 (:military_medal:)"), + ("🌌", "🌌 (:milky_way:)"), + ("🚐", "🚐 (:minibus:)"), + ("➖", "➖ (:minus:)"), + ("🪞", "🪞 (:mirror:)"), + ("\U0001faa9", "\U0001faa9 (:mirror_ball:)"), + ("🗿", "🗿 (:moai:)"), + ("📱", "📱 (:mobile_phone:)"), + ("📴", "📴 (:mobile_phone_off:)"), + ("📲", "📲 (:mobile_phone_with_arrow:)"), + ("🤑", "🤑 (:money-mouth_face:)"), + ("💰", "💰 (:money_bag:)"), + ("💸", "💸 (:money_with_wings:)"), + ("🐒", "🐒 (:monkey:)"), + ("🐵", "🐵 (:monkey_face:)"), + ("🚝", "🚝 (:monorail:)"), + ("🥮", "🥮 (:moon_cake:)"), + ("🎑", "🎑 (:moon_viewing_ceremony:)"), + ("\U0001face", "\U0001face (:moose:)"), + ("🕌", "🕌 (:mosque:)"), + ("🦟", "🦟 (:mosquito:)"), + ("🛥️", "🛥️ (:motor_boat:)"), + ("🛥", "🛥 (:motor_boat:)"), + ("🛵", "🛵 (:motor_scooter:)"), + ("🏍️", "🏍️ (:motorcycle:)"), + ("🏍", "🏍 (:motorcycle:)"), + ("🦼", "🦼 (:motorized_wheelchair:)"), + ("🛣️", "🛣️ (:motorway:)"), + ("🛣", "🛣 (:motorway:)"), + ("🗻", "🗻 (:mount_fuji:)"), + ("⛰️", "⛰️ (:mountain:)"), + ("⛰", "⛰ (:mountain:)"), + ("🚠", "🚠 (:mountain_cableway:)"), + ("🚞", "🚞 (:mountain_railway:)"), + ("🐁", "🐁 (:mouse:)"), + ("🐭", "🐭 (:mouse_face:)"), + ("🪤", "🪤 (:mouse_trap:)"), + ("👄", "👄 (:mouth:)"), + ("🎥", "🎥 (:movie_camera:)"), + ("✖️", "✖️ (:multiply:)"), + ("✖", "✖ (:multiply:)"), + ("🍄", "🍄 (:mushroom:)"), + ("🎹", "🎹 (:musical_keyboard:)"), + ("🎵", "🎵 (:musical_note:)"), + ("🎶", "🎶 (:musical_notes:)"), + ("🎼", "🎼 (:musical_score:)"), + ("🔇", "🔇 (:muted_speaker:)"), + ("🧑\u200d🎄", "🧑\u200d🎄 (:mx_claus:)"), + ("🧑🏿\u200d🎄", "🧑🏿\u200d🎄 (:mx_claus_dark_skin_tone:)"), + ("🧑🏻\u200d🎄", "🧑🏻\u200d🎄 (:mx_claus_light_skin_tone:)"), + ( + "🧑🏾\u200d🎄", + "🧑🏾\u200d🎄 (:mx_claus_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🎄", + "🧑🏼\u200d🎄 (:mx_claus_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🎄", "🧑🏽\u200d🎄 (:mx_claus_medium_skin_tone:)"), + ("💅", "💅 (:nail_polish:)"), + ("💅🏿", "💅🏿 (:nail_polish_dark_skin_tone:)"), + ("💅🏻", "💅🏻 (:nail_polish_light_skin_tone:)"), + ("💅🏾", "💅🏾 (:nail_polish_medium-dark_skin_tone:)"), + ("💅🏼", "💅🏼 (:nail_polish_medium-light_skin_tone:)"), + ("💅🏽", "💅🏽 (:nail_polish_medium_skin_tone:)"), + ("📛", "📛 (:name_badge:)"), + ("🏞️", "🏞️ (:national_park:)"), + ("🏞", "🏞 (:national_park:)"), + ("🤢", "🤢 (:nauseated_face:)"), + ("🧿", "🧿 (:nazar_amulet:)"), + ("👔", "👔 (:necktie:)"), + ("🤓", "🤓 (:nerd_face:)"), + ("\U0001faba", "\U0001faba (:nest_with_eggs:)"), + ("🪆", "🪆 (:nesting_dolls:)"), + ("😐", "😐 (:neutral_face:)"), + ("🌑", "🌑 (:new_moon:)"), + ("🌚", "🌚 (:new_moon_face:)"), + ("📰", "📰 (:newspaper:)"), + ("⏭️", "⏭️ (:next_track_button:)"), + ("⏭", "⏭ (:next_track_button:)"), + ("🌃", "🌃 (:night_with_stars:)"), + ("🕤", "🕤 (:nine-thirty:)"), + ("🕘", "🕘 (:nine_o’clock:)"), + ("🥷", "🥷 (:ninja:)"), + ("🥷🏿", "🥷🏿 (:ninja_dark_skin_tone:)"), + ("🥷🏻", "🥷🏻 (:ninja_light_skin_tone:)"), + ("🥷🏾", "🥷🏾 (:ninja_medium-dark_skin_tone:)"), + ("🥷🏼", "🥷🏼 (:ninja_medium-light_skin_tone:)"), + ("🥷🏽", "🥷🏽 (:ninja_medium_skin_tone:)"), + ("🚳", "🚳 (:no_bicycles:)"), + ("⛔", "⛔ (:no_entry:)"), + ("🚯", "🚯 (:no_littering:)"), + ("📵", "📵 (:no_mobile_phones:)"), + ("🔞", "🔞 (:no_one_under_eighteen:)"), + ("🚷", "🚷 (:no_pedestrians:)"), + ("🚭", "🚭 (:no_smoking:)"), + ("🚱", "🚱 (:non-potable_water:)"), + ("👃", "👃 (:nose:)"), + ("👃🏿", "👃🏿 (:nose_dark_skin_tone:)"), + ("👃🏻", "👃🏻 (:nose_light_skin_tone:)"), + ("👃🏾", "👃🏾 (:nose_medium-dark_skin_tone:)"), + ("👃🏼", "👃🏼 (:nose_medium-light_skin_tone:)"), + ("👃🏽", "👃🏽 (:nose_medium_skin_tone:)"), + ("📓", "📓 (:notebook:)"), + ("📔", "📔 (:notebook_with_decorative_cover:)"), + ("🔩", "🔩 (:nut_and_bolt:)"), + ("🐙", "🐙 (:octopus:)"), + ("🍢", "🍢 (:oden:)"), + ("🏢", "🏢 (:office_building:)"), + ("🧑\u200d💼", "🧑\u200d💼 (:office_worker:)"), + ("🧑🏿\u200d💼", "🧑🏿\u200d💼 (:office_worker_dark_skin_tone:)"), + ( + "🧑🏻\u200d💼", + "🧑🏻\u200d💼 (:office_worker_light_skin_tone:)", + ), + ( + "🧑🏾\u200d💼", + "🧑🏾\u200d💼 (:office_worker_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d💼", + "🧑🏼\u200d💼 (:office_worker_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d💼", + "🧑🏽\u200d💼 (:office_worker_medium_skin_tone:)", + ), + ("👹", "👹 (:ogre:)"), + ("🛢️", "🛢️ (:oil_drum:)"), + ("🛢", "🛢 (:oil_drum:)"), + ("🗝️", "🗝️ (:old_key:)"), + ("🗝", "🗝 (:old_key:)"), + ("👴", "👴 (:old_man:)"), + ("👴🏿", "👴🏿 (:old_man_dark_skin_tone:)"), + ("👴🏻", "👴🏻 (:old_man_light_skin_tone:)"), + ("👴🏾", "👴🏾 (:old_man_medium-dark_skin_tone:)"), + ("👴🏼", "👴🏼 (:old_man_medium-light_skin_tone:)"), + ("👴🏽", "👴🏽 (:old_man_medium_skin_tone:)"), + ("👵", "👵 (:old_woman:)"), + ("👵🏿", "👵🏿 (:old_woman_dark_skin_tone:)"), + ("👵🏻", "👵🏻 (:old_woman_light_skin_tone:)"), + ("👵🏾", "👵🏾 (:old_woman_medium-dark_skin_tone:)"), + ("👵🏼", "👵🏼 (:old_woman_medium-light_skin_tone:)"), + ("👵🏽", "👵🏽 (:old_woman_medium_skin_tone:)"), + ("🧓", "🧓 (:older_person:)"), + ("🧓🏿", "🧓🏿 (:older_person_dark_skin_tone:)"), + ("🧓🏻", "🧓🏻 (:older_person_light_skin_tone:)"), + ("🧓🏾", "🧓🏾 (:older_person_medium-dark_skin_tone:)"), + ("🧓🏼", "🧓🏼 (:older_person_medium-light_skin_tone:)"), + ("🧓🏽", "🧓🏽 (:older_person_medium_skin_tone:)"), + ("🫒", "🫒 (:olive:)"), + ("🕉️", "🕉️ (:om:)"), + ("🕉", "🕉 (:om:)"), + ("🚘", "🚘 (:oncoming_automobile:)"), + ("🚍", "🚍 (:oncoming_bus:)"), + ("👊", "👊 (:oncoming_fist:)"), + ("👊🏿", "👊🏿 (:oncoming_fist_dark_skin_tone:)"), + ("👊🏻", "👊🏻 (:oncoming_fist_light_skin_tone:)"), + ("👊🏾", "👊🏾 (:oncoming_fist_medium-dark_skin_tone:)"), + ("👊🏼", "👊🏼 (:oncoming_fist_medium-light_skin_tone:)"), + ("👊🏽", "👊🏽 (:oncoming_fist_medium_skin_tone:)"), + ("🚔", "🚔 (:oncoming_police_car:)"), + ("🚖", "🚖 (:oncoming_taxi:)"), + ("🩱", "🩱 (:one-piece_swimsuit:)"), + ("🕜", "🕜 (:one-thirty:)"), + ("🕐", "🕐 (:one_o’clock:)"), + ("🧅", "🧅 (:onion:)"), + ("📖", "📖 (:open_book:)"), + ("📂", "📂 (:open_file_folder:)"), + ("👐", "👐 (:open_hands:)"), + ("👐🏿", "👐🏿 (:open_hands_dark_skin_tone:)"), + ("👐🏻", "👐🏻 (:open_hands_light_skin_tone:)"), + ("👐🏾", "👐🏾 (:open_hands_medium-dark_skin_tone:)"), + ("👐🏼", "👐🏼 (:open_hands_medium-light_skin_tone:)"), + ("👐🏽", "👐🏽 (:open_hands_medium_skin_tone:)"), + ("📭", "📭 (:open_mailbox_with_lowered_flag:)"), + ("📬", "📬 (:open_mailbox_with_raised_flag:)"), + ("💿", "💿 (:optical_disk:)"), + ("📙", "📙 (:orange_book:)"), + ("🟠", "🟠 (:orange_circle:)"), + ("🧡", "🧡 (:orange_heart:)"), + ("🟧", "🟧 (:orange_square:)"), + ("🦧", "🦧 (:orangutan:)"), + ("☦️", "☦️ (:orthodox_cross:)"), + ("☦", "☦ (:orthodox_cross:)"), + ("🦦", "🦦 (:otter:)"), + ("📤", "📤 (:outbox_tray:)"), + ("🦉", "🦉 (:owl:)"), + ("🐂", "🐂 (:ox:)"), + ("🦪", "🦪 (:oyster:)"), + ("📦", "📦 (:package:)"), + ("📄", "📄 (:page_facing_up:)"), + ("📃", "📃 (:page_with_curl:)"), + ("📟", "📟 (:pager:)"), + ("🖌️", "🖌️ (:paintbrush:)"), + ("🖌", "🖌 (:paintbrush:)"), + ("\U0001faf3", "\U0001faf3 (:palm_down_hand:)"), + ( + "\U0001faf3🏿", + "\U0001faf3🏿 (:palm_down_hand_dark_skin_tone:)", + ), + ( + "\U0001faf3🏻", + "\U0001faf3🏻 (:palm_down_hand_light_skin_tone:)", + ), + ( + "\U0001faf3🏾", + "\U0001faf3🏾 (:palm_down_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf3🏼", + "\U0001faf3🏼 (:palm_down_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf3🏽", + "\U0001faf3🏽 (:palm_down_hand_medium_skin_tone:)", + ), + ("🌴", "🌴 (:palm_tree:)"), + ("\U0001faf4", "\U0001faf4 (:palm_up_hand:)"), + ( + "\U0001faf4🏿", + "\U0001faf4🏿 (:palm_up_hand_dark_skin_tone:)", + ), + ( + "\U0001faf4🏻", + "\U0001faf4🏻 (:palm_up_hand_light_skin_tone:)", + ), + ( + "\U0001faf4🏾", + "\U0001faf4🏾 (:palm_up_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf4🏼", + "\U0001faf4🏼 (:palm_up_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf4🏽", + "\U0001faf4🏽 (:palm_up_hand_medium_skin_tone:)", + ), + ("🤲", "🤲 (:palms_up_together:)"), + ("🤲🏿", "🤲🏿 (:palms_up_together_dark_skin_tone:)"), + ("🤲🏻", "🤲🏻 (:palms_up_together_light_skin_tone:)"), + ("🤲🏾", "🤲🏾 (:palms_up_together_medium-dark_skin_tone:)"), + ("🤲🏼", "🤲🏼 (:palms_up_together_medium-light_skin_tone:)"), + ("🤲🏽", "🤲🏽 (:palms_up_together_medium_skin_tone:)"), + ("🥞", "🥞 (:pancakes:)"), + ("🐼", "🐼 (:panda:)"), + ("📎", "📎 (:paperclip:)"), + ("🪂", "🪂 (:parachute:)"), + ("🦜", "🦜 (:parrot:)"), + ("〽️", "〽️ (:part_alternation_mark:)"), + ("〽", "〽 (:part_alternation_mark:)"), + ("🎉", "🎉 (:party_popper:)"), + ("🥳", "🥳 (:partying_face:)"), + ("🛳️", "🛳️ (:passenger_ship:)"), + ("🛳", "🛳 (:passenger_ship:)"), + ("🛂", "🛂 (:passport_control:)"), + ("⏸️", "⏸️ (:pause_button:)"), + ("⏸", "⏸ (:pause_button:)"), + ("🐾", "🐾 (:paw_prints:)"), + ("\U0001fadb", "\U0001fadb (:pea_pod:)"), + ("☮️", "☮️ (:peace_symbol:)"), + ("☮", "☮ (:peace_symbol:)"), + ("🍑", "🍑 (:peach:)"), + ("🦚", "🦚 (:peacock:)"), + ("🥜", "🥜 (:peanuts:)"), + ("🍐", "🍐 (:pear:)"), + ("🖊️", "🖊️ (:pen:)"), + ("🖊", "🖊 (:pen:)"), + ("✏️", "✏️ (:pencil:)"), + ("✏", "✏ (:pencil:)"), + ("🐧", "🐧 (:penguin:)"), + ("😔", "😔 (:pensive_face:)"), + ( + "🧑\u200d🤝\u200d🧑", + "🧑\u200d🤝\u200d🧑 (:people_holding_hands:)", + ), + ( + "🧑🏿\u200d🤝\u200d🧑🏿", + "🧑🏿\u200d🤝\u200d🧑🏿 (:people_holding_hands_dark_skin_tone:)", + ), + ( + "🧑🏿\u200d🤝\u200d🧑🏻", + "🧑🏿\u200d🤝\u200d🧑🏻 (:people_holding_hands_dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏿\u200d🤝\u200d🧑🏾", + "🧑🏿\u200d🤝\u200d🧑🏾 (:people_holding_hands_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏿\u200d🤝\u200d🧑🏼", + "🧑🏿\u200d🤝\u200d🧑🏼 (:people_holding_hands_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏿\u200d🤝\u200d🧑🏽", + "🧑🏿\u200d🤝\u200d🧑🏽 (:people_holding_hands_dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d🤝\u200d🧑🏻", + "🧑🏻\u200d🤝\u200d🧑🏻 (:people_holding_hands_light_skin_tone:)", + ), + ( + "🧑🏻\u200d🤝\u200d🧑🏿", + "🧑🏻\u200d🤝\u200d🧑🏿 (:people_holding_hands_light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🤝\u200d🧑🏾", + "🧑🏻\u200d🤝\u200d🧑🏾 (:people_holding_hands_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🤝\u200d🧑🏼", + "🧑🏻\u200d🤝\u200d🧑🏼 (:people_holding_hands_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏻\u200d🤝\u200d🧑🏽", + "🧑🏻\u200d🤝\u200d🧑🏽 (:people_holding_hands_light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏾\u200d🤝\u200d🧑🏾", + "🧑🏾\u200d🤝\u200d🧑🏾 (:people_holding_hands_medium-dark_skin_tone:)", + ), + ( + "🧑🏾\u200d🤝\u200d🧑🏿", + "🧑🏾\u200d🤝\u200d🧑🏿 (:people_holding_hands_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏾\u200d🤝\u200d🧑🏻", + "🧑🏾\u200d🤝\u200d🧑🏻 (:people_holding_hands_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🤝\u200d🧑🏼", + "🧑🏾\u200d🤝\u200d🧑🏼 (:people_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "🧑🏾\u200d🤝\u200d🧑🏽", + "🧑🏾\u200d🤝\u200d🧑🏽 (:people_holding_hands_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏼\u200d🤝\u200d🧑🏼", + "🧑🏼\u200d🤝\u200d🧑🏼 (:people_holding_hands_medium-light_skin_tone:)", + ), + ( + "🧑🏼\u200d🤝\u200d🧑🏿", + "🧑🏼\u200d🤝\u200d🧑🏿 (:people_holding_hands_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🤝\u200d🧑🏻", + "🧑🏼\u200d🤝\u200d🧑🏻 (:people_holding_hands_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏼\u200d🤝\u200d🧑🏾", + "🧑🏼\u200d🤝\u200d🧑🏾 (:people_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🤝\u200d🧑🏽", + "🧑🏼\u200d🤝\u200d🧑🏽 (:people_holding_hands_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d🤝\u200d🧑🏽", + "🧑🏽\u200d🤝\u200d🧑🏽 (:people_holding_hands_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d🤝\u200d🧑🏿", + "🧑🏽\u200d🤝\u200d🧑🏿 (:people_holding_hands_medium_skin_tone_dark_skin_tone:)", + ), + ( + "🧑🏽\u200d🤝\u200d🧑🏻", + "🧑🏽\u200d🤝\u200d🧑🏻 (:people_holding_hands_medium_skin_tone_light_skin_tone:)", + ), + ( + "🧑🏽\u200d🤝\u200d🧑🏾", + "🧑🏽\u200d🤝\u200d🧑🏾 (:people_holding_hands_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "🧑🏽\u200d🤝\u200d🧑🏼", + "🧑🏽\u200d🤝\u200d🧑🏼 (:people_holding_hands_medium_skin_tone_medium-light_skin_tone:)", + ), + ("🫂", "🫂 (:people_hugging:)"), + ("👯", "👯 (:people_with_bunny_ears:)"), + ("🤼", "🤼 (:people_wrestling:)"), + ("🎭", "🎭 (:performing_arts:)"), + ("😣", "😣 (:persevering_face:)"), + ("🧑", "🧑 (:person:)"), + ("🧑\u200d🦲", "🧑\u200d🦲 (:person_bald:)"), + ("🧔", "🧔 (:person_beard:)"), + ("🚴", "🚴 (:person_biking:)"), + ("🚴🏿", "🚴🏿 (:person_biking_dark_skin_tone:)"), + ("🚴🏻", "🚴🏻 (:person_biking_light_skin_tone:)"), + ("🚴🏾", "🚴🏾 (:person_biking_medium-dark_skin_tone:)"), + ("🚴🏼", "🚴🏼 (:person_biking_medium-light_skin_tone:)"), + ("🚴🏽", "🚴🏽 (:person_biking_medium_skin_tone:)"), + ("👱", "👱 (:person_blond_hair:)"), + ("⛹️", "⛹️ (:person_bouncing_ball:)"), + ("⛹", "⛹ (:person_bouncing_ball:)"), + ("⛹🏿", "⛹🏿 (:person_bouncing_ball_dark_skin_tone:)"), + ("⛹🏻", "⛹🏻 (:person_bouncing_ball_light_skin_tone:)"), + ("⛹🏾", "⛹🏾 (:person_bouncing_ball_medium-dark_skin_tone:)"), + ( + "⛹🏼", + "⛹🏼 (:person_bouncing_ball_medium-light_skin_tone:)", + ), + ("⛹🏽", "⛹🏽 (:person_bouncing_ball_medium_skin_tone:)"), + ("🙇", "🙇 (:person_bowing:)"), + ("🙇🏿", "🙇🏿 (:person_bowing_dark_skin_tone:)"), + ("🙇🏻", "🙇🏻 (:person_bowing_light_skin_tone:)"), + ("🙇🏾", "🙇🏾 (:person_bowing_medium-dark_skin_tone:)"), + ("🙇🏼", "🙇🏼 (:person_bowing_medium-light_skin_tone:)"), + ("🙇🏽", "🙇🏽 (:person_bowing_medium_skin_tone:)"), + ("🤸", "🤸 (:person_cartwheeling:)"), + ("🤸🏿", "🤸🏿 (:person_cartwheeling_dark_skin_tone:)"), + ("🤸🏻", "🤸🏻 (:person_cartwheeling_light_skin_tone:)"), + ("🤸🏾", "🤸🏾 (:person_cartwheeling_medium-dark_skin_tone:)"), + ("🤸🏼", "🤸🏼 (:person_cartwheeling_medium-light_skin_tone:)"), + ("🤸🏽", "🤸🏽 (:person_cartwheeling_medium_skin_tone:)"), + ("🧗", "🧗 (:person_climbing:)"), + ("🧗🏿", "🧗🏿 (:person_climbing_dark_skin_tone:)"), + ("🧗🏻", "🧗🏻 (:person_climbing_light_skin_tone:)"), + ("🧗🏾", "🧗🏾 (:person_climbing_medium-dark_skin_tone:)"), + ("🧗🏼", "🧗🏼 (:person_climbing_medium-light_skin_tone:)"), + ("🧗🏽", "🧗🏽 (:person_climbing_medium_skin_tone:)"), + ("🧑\u200d🦱", "🧑\u200d🦱 (:person_curly_hair:)"), + ("🧑🏿", "🧑🏿 (:person_dark_skin_tone:)"), + ("🧑🏿\u200d🦲", "🧑🏿\u200d🦲 (:person_dark_skin_tone_bald:)"), + ("🧔🏿", "🧔🏿 (:person_dark_skin_tone_beard:)"), + ("👱🏿", "👱🏿 (:person_dark_skin_tone_blond_hair:)"), + ( + "🧑🏿\u200d🦱", + "🧑🏿\u200d🦱 (:person_dark_skin_tone_curly_hair:)", + ), + ( + "🧑🏿\u200d🦰", + "🧑🏿\u200d🦰 (:person_dark_skin_tone_red_hair:)", + ), + ( + "🧑🏿\u200d🦳", + "🧑🏿\u200d🦳 (:person_dark_skin_tone_white_hair:)", + ), + ("🤦", "🤦 (:person_facepalming:)"), + ("🤦🏿", "🤦🏿 (:person_facepalming_dark_skin_tone:)"), + ("🤦🏻", "🤦🏻 (:person_facepalming_light_skin_tone:)"), + ("🤦🏾", "🤦🏾 (:person_facepalming_medium-dark_skin_tone:)"), + ("🤦🏼", "🤦🏼 (:person_facepalming_medium-light_skin_tone:)"), + ("🤦🏽", "🤦🏽 (:person_facepalming_medium_skin_tone:)"), + ("🧑\u200d🍼", "🧑\u200d🍼 (:person_feeding_baby:)"), + ( + "🧑🏿\u200d🍼", + "🧑🏿\u200d🍼 (:person_feeding_baby_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🍼", + "🧑🏻\u200d🍼 (:person_feeding_baby_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🍼", + "🧑🏾\u200d🍼 (:person_feeding_baby_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🍼", + "🧑🏼\u200d🍼 (:person_feeding_baby_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🍼", + "🧑🏽\u200d🍼 (:person_feeding_baby_medium_skin_tone:)", + ), + ("🤺", "🤺 (:person_fencing:)"), + ("🙍", "🙍 (:person_frowning:)"), + ("🙍🏿", "🙍🏿 (:person_frowning_dark_skin_tone:)"), + ("🙍🏻", "🙍🏻 (:person_frowning_light_skin_tone:)"), + ("🙍🏾", "🙍🏾 (:person_frowning_medium-dark_skin_tone:)"), + ("🙍🏼", "🙍🏼 (:person_frowning_medium-light_skin_tone:)"), + ("🙍🏽", "🙍🏽 (:person_frowning_medium_skin_tone:)"), + ("🙅", "🙅 (:person_gesturing_NO:)"), + ("🙅🏿", "🙅🏿 (:person_gesturing_NO_dark_skin_tone:)"), + ("🙅🏻", "🙅🏻 (:person_gesturing_NO_light_skin_tone:)"), + ("🙅🏾", "🙅🏾 (:person_gesturing_NO_medium-dark_skin_tone:)"), + ("🙅🏼", "🙅🏼 (:person_gesturing_NO_medium-light_skin_tone:)"), + ("🙅🏽", "🙅🏽 (:person_gesturing_NO_medium_skin_tone:)"), + ("🙆", "🙆 (:person_gesturing_OK:)"), + ("🙆🏿", "🙆🏿 (:person_gesturing_OK_dark_skin_tone:)"), + ("🙆🏻", "🙆🏻 (:person_gesturing_OK_light_skin_tone:)"), + ("🙆🏾", "🙆🏾 (:person_gesturing_OK_medium-dark_skin_tone:)"), + ("🙆🏼", "🙆🏼 (:person_gesturing_OK_medium-light_skin_tone:)"), + ("🙆🏽", "🙆🏽 (:person_gesturing_OK_medium_skin_tone:)"), + ("💇", "💇 (:person_getting_haircut:)"), + ("💇🏿", "💇🏿 (:person_getting_haircut_dark_skin_tone:)"), + ("💇🏻", "💇🏻 (:person_getting_haircut_light_skin_tone:)"), + ( + "💇🏾", + "💇🏾 (:person_getting_haircut_medium-dark_skin_tone:)", + ), + ( + "💇🏼", + "💇🏼 (:person_getting_haircut_medium-light_skin_tone:)", + ), + ("💇🏽", "💇🏽 (:person_getting_haircut_medium_skin_tone:)"), + ("💆", "💆 (:person_getting_massage:)"), + ("💆🏿", "💆🏿 (:person_getting_massage_dark_skin_tone:)"), + ("💆🏻", "💆🏻 (:person_getting_massage_light_skin_tone:)"), + ( + "💆🏾", + "💆🏾 (:person_getting_massage_medium-dark_skin_tone:)", + ), + ( + "💆🏼", + "💆🏼 (:person_getting_massage_medium-light_skin_tone:)", + ), + ("💆🏽", "💆🏽 (:person_getting_massage_medium_skin_tone:)"), + ("🏌️", "🏌️ (:person_golfing:)"), + ("🏌", "🏌 (:person_golfing:)"), + ("🏌🏿", "🏌🏿 (:person_golfing_dark_skin_tone:)"), + ("🏌🏻", "🏌🏻 (:person_golfing_light_skin_tone:)"), + ("🏌🏾", "🏌🏾 (:person_golfing_medium-dark_skin_tone:)"), + ("🏌🏼", "🏌🏼 (:person_golfing_medium-light_skin_tone:)"), + ("🏌🏽", "🏌🏽 (:person_golfing_medium_skin_tone:)"), + ("🛌", "🛌 (:person_in_bed:)"), + ("🛌🏿", "🛌🏿 (:person_in_bed_dark_skin_tone:)"), + ("🛌🏻", "🛌🏻 (:person_in_bed_light_skin_tone:)"), + ("🛌🏾", "🛌🏾 (:person_in_bed_medium-dark_skin_tone:)"), + ("🛌🏼", "🛌🏼 (:person_in_bed_medium-light_skin_tone:)"), + ("🛌🏽", "🛌🏽 (:person_in_bed_medium_skin_tone:)"), + ("🧘", "🧘 (:person_in_lotus_position:)"), + ("🧘🏿", "🧘🏿 (:person_in_lotus_position_dark_skin_tone:)"), + ("🧘🏻", "🧘🏻 (:person_in_lotus_position_light_skin_tone:)"), + ( + "🧘🏾", + "🧘🏾 (:person_in_lotus_position_medium-dark_skin_tone:)", + ), + ( + "🧘🏼", + "🧘🏼 (:person_in_lotus_position_medium-light_skin_tone:)", + ), + ("🧘🏽", "🧘🏽 (:person_in_lotus_position_medium_skin_tone:)"), + ("🧑\u200d🦽", "🧑\u200d🦽 (:person_in_manual_wheelchair:)"), + ( + "🧑🏿\u200d🦽", + "🧑🏿\u200d🦽 (:person_in_manual_wheelchair_dark_skin_tone:)", + ), + ( + "🧑\u200d🦽\u200d➡️", + "🧑\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right:)", + ), + ( + "🧑\u200d🦽\u200d➡", + "🧑\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right:)", + ), + ( + "🧑🏿\u200d🦽\u200d➡️", + "🧑🏿\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏿\u200d🦽\u200d➡", + "🧑🏿\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🦽\u200d➡️", + "🧑🏻\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "🧑🏻\u200d🦽\u200d➡", + "🧑🏻\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦽\u200d➡️", + "🧑🏾\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏾\u200d🦽\u200d➡", + "🧑🏾\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦽\u200d➡️", + "🧑🏼\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏼\u200d🦽\u200d➡", + "🧑🏼\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦽\u200d➡️", + "🧑🏽\u200d🦽\u200d➡️ (:person_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d🦽\u200d➡", + "🧑🏽\u200d🦽\u200d➡ (:person_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d🦽", + "🧑🏻\u200d🦽 (:person_in_manual_wheelchair_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦽", + "🧑🏾\u200d🦽 (:person_in_manual_wheelchair_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦽", + "🧑🏼\u200d🦽 (:person_in_manual_wheelchair_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦽", + "🧑🏽\u200d🦽 (:person_in_manual_wheelchair_medium_skin_tone:)", + ), + ("🧑\u200d🦼", "🧑\u200d🦼 (:person_in_motorized_wheelchair:)"), + ( + "🧑🏿\u200d🦼", + "🧑🏿\u200d🦼 (:person_in_motorized_wheelchair_dark_skin_tone:)", + ), + ( + "🧑\u200d🦼\u200d➡️", + "🧑\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right:)", + ), + ( + "🧑\u200d🦼\u200d➡", + "🧑\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right:)", + ), + ( + "🧑🏿\u200d🦼\u200d➡️", + "🧑🏿\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏿\u200d🦼\u200d➡", + "🧑🏿\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🦼\u200d➡️", + "🧑🏻\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "🧑🏻\u200d🦼\u200d➡", + "🧑🏻\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦼\u200d➡️", + "🧑🏾\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏾\u200d🦼\u200d➡", + "🧑🏾\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦼\u200d➡️", + "🧑🏼\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏼\u200d🦼\u200d➡", + "🧑🏼\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦼\u200d➡️", + "🧑🏽\u200d🦼\u200d➡️ (:person_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d🦼\u200d➡", + "🧑🏽\u200d🦼\u200d➡ (:person_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d🦼", + "🧑🏻\u200d🦼 (:person_in_motorized_wheelchair_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦼", + "🧑🏾\u200d🦼 (:person_in_motorized_wheelchair_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦼", + "🧑🏼\u200d🦼 (:person_in_motorized_wheelchair_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦼", + "🧑🏽\u200d🦼 (:person_in_motorized_wheelchair_medium_skin_tone:)", + ), + ("🧖", "🧖 (:person_in_steamy_room:)"), + ("🧖🏿", "🧖🏿 (:person_in_steamy_room_dark_skin_tone:)"), + ("🧖🏻", "🧖🏻 (:person_in_steamy_room_light_skin_tone:)"), + ( + "🧖🏾", + "🧖🏾 (:person_in_steamy_room_medium-dark_skin_tone:)", + ), + ( + "🧖🏼", + "🧖🏼 (:person_in_steamy_room_medium-light_skin_tone:)", + ), + ("🧖🏽", "🧖🏽 (:person_in_steamy_room_medium_skin_tone:)"), + ("🕴️", "🕴️ (:person_in_suit_levitating:)"), + ("🕴", "🕴 (:person_in_suit_levitating:)"), + ("🕴🏿", "🕴🏿 (:person_in_suit_levitating_dark_skin_tone:)"), + ("🕴🏻", "🕴🏻 (:person_in_suit_levitating_light_skin_tone:)"), + ( + "🕴🏾", + "🕴🏾 (:person_in_suit_levitating_medium-dark_skin_tone:)", + ), + ( + "🕴🏼", + "🕴🏼 (:person_in_suit_levitating_medium-light_skin_tone:)", + ), + ("🕴🏽", "🕴🏽 (:person_in_suit_levitating_medium_skin_tone:)"), + ("🤵", "🤵 (:person_in_tuxedo:)"), + ("🤵🏿", "🤵🏿 (:person_in_tuxedo_dark_skin_tone:)"), + ("🤵🏻", "🤵🏻 (:person_in_tuxedo_light_skin_tone:)"), + ("🤵🏾", "🤵🏾 (:person_in_tuxedo_medium-dark_skin_tone:)"), + ("🤵🏼", "🤵🏼 (:person_in_tuxedo_medium-light_skin_tone:)"), + ("🤵🏽", "🤵🏽 (:person_in_tuxedo_medium_skin_tone:)"), + ("🤹", "🤹 (:person_juggling:)"), + ("🤹🏿", "🤹🏿 (:person_juggling_dark_skin_tone:)"), + ("🤹🏻", "🤹🏻 (:person_juggling_light_skin_tone:)"), + ("🤹🏾", "🤹🏾 (:person_juggling_medium-dark_skin_tone:)"), + ("🤹🏼", "🤹🏼 (:person_juggling_medium-light_skin_tone:)"), + ("🤹🏽", "🤹🏽 (:person_juggling_medium_skin_tone:)"), + ("🧎", "🧎 (:person_kneeling:)"), + ("🧎🏿", "🧎🏿 (:person_kneeling_dark_skin_tone:)"), + ("🧎\u200d➡️", "🧎\u200d➡️ (:person_kneeling_facing_right:)"), + ("🧎\u200d➡", "🧎\u200d➡ (:person_kneeling_facing_right:)"), + ( + "🧎🏿\u200d➡️", + "🧎🏿\u200d➡️ (:person_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d➡", + "🧎🏿\u200d➡ (:person_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏻\u200d➡️", + "🧎🏻\u200d➡️ (:person_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d➡", + "🧎🏻\u200d➡ (:person_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏾\u200d➡️", + "🧎🏾\u200d➡️ (:person_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d➡", + "🧎🏾\u200d➡ (:person_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏼\u200d➡️", + "🧎🏼\u200d➡️ (:person_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d➡", + "🧎🏼\u200d➡ (:person_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏽\u200d➡️", + "🧎🏽\u200d➡️ (:person_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d➡", + "🧎🏽\u200d➡ (:person_kneeling_facing_right_medium_skin_tone:)", + ), + ("🧎🏻", "🧎🏻 (:person_kneeling_light_skin_tone:)"), + ("🧎🏾", "🧎🏾 (:person_kneeling_medium-dark_skin_tone:)"), + ("🧎🏼", "🧎🏼 (:person_kneeling_medium-light_skin_tone:)"), + ("🧎🏽", "🧎🏽 (:person_kneeling_medium_skin_tone:)"), + ("🏋️", "🏋️ (:person_lifting_weights:)"), + ("🏋", "🏋 (:person_lifting_weights:)"), + ("🏋🏿", "🏋🏿 (:person_lifting_weights_dark_skin_tone:)"), + ("🏋🏻", "🏋🏻 (:person_lifting_weights_light_skin_tone:)"), + ( + "🏋🏾", + "🏋🏾 (:person_lifting_weights_medium-dark_skin_tone:)", + ), + ( + "🏋🏼", + "🏋🏼 (:person_lifting_weights_medium-light_skin_tone:)", + ), + ("🏋🏽", "🏋🏽 (:person_lifting_weights_medium_skin_tone:)"), + ("🧑🏻", "🧑🏻 (:person_light_skin_tone:)"), + ("🧑🏻\u200d🦲", "🧑🏻\u200d🦲 (:person_light_skin_tone_bald:)"), + ("🧔🏻", "🧔🏻 (:person_light_skin_tone_beard:)"), + ("👱🏻", "👱🏻 (:person_light_skin_tone_blond_hair:)"), + ( + "🧑🏻\u200d🦱", + "🧑🏻\u200d🦱 (:person_light_skin_tone_curly_hair:)", + ), + ( + "🧑🏻\u200d🦰", + "🧑🏻\u200d🦰 (:person_light_skin_tone_red_hair:)", + ), + ( + "🧑🏻\u200d🦳", + "🧑🏻\u200d🦳 (:person_light_skin_tone_white_hair:)", + ), + ("🧑🏾", "🧑🏾 (:person_medium-dark_skin_tone:)"), + ( + "🧑🏾\u200d🦲", + "🧑🏾\u200d🦲 (:person_medium-dark_skin_tone_bald:)", + ), + ("🧔🏾", "🧔🏾 (:person_medium-dark_skin_tone_beard:)"), + ("👱🏾", "👱🏾 (:person_medium-dark_skin_tone_blond_hair:)"), + ( + "🧑🏾\u200d🦱", + "🧑🏾\u200d🦱 (:person_medium-dark_skin_tone_curly_hair:)", + ), + ( + "🧑🏾\u200d🦰", + "🧑🏾\u200d🦰 (:person_medium-dark_skin_tone_red_hair:)", + ), + ( + "🧑🏾\u200d🦳", + "🧑🏾\u200d🦳 (:person_medium-dark_skin_tone_white_hair:)", + ), + ("🧑🏼", "🧑🏼 (:person_medium-light_skin_tone:)"), + ( + "🧑🏼\u200d🦲", + "🧑🏼\u200d🦲 (:person_medium-light_skin_tone_bald:)", + ), + ("🧔🏼", "🧔🏼 (:person_medium-light_skin_tone_beard:)"), + ("👱🏼", "👱🏼 (:person_medium-light_skin_tone_blond_hair:)"), + ( + "🧑🏼\u200d🦱", + "🧑🏼\u200d🦱 (:person_medium-light_skin_tone_curly_hair:)", + ), + ( + "🧑🏼\u200d🦰", + "🧑🏼\u200d🦰 (:person_medium-light_skin_tone_red_hair:)", + ), + ( + "🧑🏼\u200d🦳", + "🧑🏼\u200d🦳 (:person_medium-light_skin_tone_white_hair:)", + ), + ("🧑🏽", "🧑🏽 (:person_medium_skin_tone:)"), + ("🧑🏽\u200d🦲", "🧑🏽\u200d🦲 (:person_medium_skin_tone_bald:)"), + ("🧔🏽", "🧔🏽 (:person_medium_skin_tone_beard:)"), + ("👱🏽", "👱🏽 (:person_medium_skin_tone_blond_hair:)"), + ( + "🧑🏽\u200d🦱", + "🧑🏽\u200d🦱 (:person_medium_skin_tone_curly_hair:)", + ), + ( + "🧑🏽\u200d🦰", + "🧑🏽\u200d🦰 (:person_medium_skin_tone_red_hair:)", + ), + ( + "🧑🏽\u200d🦳", + "🧑🏽\u200d🦳 (:person_medium_skin_tone_white_hair:)", + ), + ("🚵", "🚵 (:person_mountain_biking:)"), + ("🚵🏿", "🚵🏿 (:person_mountain_biking_dark_skin_tone:)"), + ("🚵🏻", "🚵🏻 (:person_mountain_biking_light_skin_tone:)"), + ( + "🚵🏾", + "🚵🏾 (:person_mountain_biking_medium-dark_skin_tone:)", + ), + ( + "🚵🏼", + "🚵🏼 (:person_mountain_biking_medium-light_skin_tone:)", + ), + ("🚵🏽", "🚵🏽 (:person_mountain_biking_medium_skin_tone:)"), + ("🤾", "🤾 (:person_playing_handball:)"), + ("🤾🏿", "🤾🏿 (:person_playing_handball_dark_skin_tone:)"), + ("🤾🏻", "🤾🏻 (:person_playing_handball_light_skin_tone:)"), + ( + "🤾🏾", + "🤾🏾 (:person_playing_handball_medium-dark_skin_tone:)", + ), + ( + "🤾🏼", + "🤾🏼 (:person_playing_handball_medium-light_skin_tone:)", + ), + ("🤾🏽", "🤾🏽 (:person_playing_handball_medium_skin_tone:)"), + ("🤽", "🤽 (:person_playing_water_polo:)"), + ("🤽🏿", "🤽🏿 (:person_playing_water_polo_dark_skin_tone:)"), + ("🤽🏻", "🤽🏻 (:person_playing_water_polo_light_skin_tone:)"), + ( + "🤽🏾", + "🤽🏾 (:person_playing_water_polo_medium-dark_skin_tone:)", + ), + ( + "🤽🏼", + "🤽🏼 (:person_playing_water_polo_medium-light_skin_tone:)", + ), + ("🤽🏽", "🤽🏽 (:person_playing_water_polo_medium_skin_tone:)"), + ("🙎", "🙎 (:person_pouting:)"), + ("🙎🏿", "🙎🏿 (:person_pouting_dark_skin_tone:)"), + ("🙎🏻", "🙎🏻 (:person_pouting_light_skin_tone:)"), + ("🙎🏾", "🙎🏾 (:person_pouting_medium-dark_skin_tone:)"), + ("🙎🏼", "🙎🏼 (:person_pouting_medium-light_skin_tone:)"), + ("🙎🏽", "🙎🏽 (:person_pouting_medium_skin_tone:)"), + ("🙋", "🙋 (:person_raising_hand:)"), + ("🙋🏿", "🙋🏿 (:person_raising_hand_dark_skin_tone:)"), + ("🙋🏻", "🙋🏻 (:person_raising_hand_light_skin_tone:)"), + ("🙋🏾", "🙋🏾 (:person_raising_hand_medium-dark_skin_tone:)"), + ("🙋🏼", "🙋🏼 (:person_raising_hand_medium-light_skin_tone:)"), + ("🙋🏽", "🙋🏽 (:person_raising_hand_medium_skin_tone:)"), + ("🧑\u200d🦰", "🧑\u200d🦰 (:person_red_hair:)"), + ("🚣", "🚣 (:person_rowing_boat:)"), + ("🚣🏿", "🚣🏿 (:person_rowing_boat_dark_skin_tone:)"), + ("🚣🏻", "🚣🏻 (:person_rowing_boat_light_skin_tone:)"), + ("🚣🏾", "🚣🏾 (:person_rowing_boat_medium-dark_skin_tone:)"), + ("🚣🏼", "🚣🏼 (:person_rowing_boat_medium-light_skin_tone:)"), + ("🚣🏽", "🚣🏽 (:person_rowing_boat_medium_skin_tone:)"), + ("🏃", "🏃 (:person_running:)"), + ("🏃🏿", "🏃🏿 (:person_running_dark_skin_tone:)"), + ("🏃\u200d➡️", "🏃\u200d➡️ (:person_running_facing_right:)"), + ("🏃\u200d➡", "🏃\u200d➡ (:person_running_facing_right:)"), + ( + "🏃🏿\u200d➡️", + "🏃🏿\u200d➡️ (:person_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d➡", + "🏃🏿\u200d➡ (:person_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏻\u200d➡️", + "🏃🏻\u200d➡️ (:person_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d➡", + "🏃🏻\u200d➡ (:person_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏾\u200d➡️", + "🏃🏾\u200d➡️ (:person_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d➡", + "🏃🏾\u200d➡ (:person_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏼\u200d➡️", + "🏃🏼\u200d➡️ (:person_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d➡", + "🏃🏼\u200d➡ (:person_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏽\u200d➡️", + "🏃🏽\u200d➡️ (:person_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d➡", + "🏃🏽\u200d➡ (:person_running_facing_right_medium_skin_tone:)", + ), + ("🏃🏻", "🏃🏻 (:person_running_light_skin_tone:)"), + ("🏃🏾", "🏃🏾 (:person_running_medium-dark_skin_tone:)"), + ("🏃🏼", "🏃🏼 (:person_running_medium-light_skin_tone:)"), + ("🏃🏽", "🏃🏽 (:person_running_medium_skin_tone:)"), + ("🤷", "🤷 (:person_shrugging:)"), + ("🤷🏿", "🤷🏿 (:person_shrugging_dark_skin_tone:)"), + ("🤷🏻", "🤷🏻 (:person_shrugging_light_skin_tone:)"), + ("🤷🏾", "🤷🏾 (:person_shrugging_medium-dark_skin_tone:)"), + ("🤷🏼", "🤷🏼 (:person_shrugging_medium-light_skin_tone:)"), + ("🤷🏽", "🤷🏽 (:person_shrugging_medium_skin_tone:)"), + ("🧍", "🧍 (:person_standing:)"), + ("🧍🏿", "🧍🏿 (:person_standing_dark_skin_tone:)"), + ("🧍🏻", "🧍🏻 (:person_standing_light_skin_tone:)"), + ("🧍🏾", "🧍🏾 (:person_standing_medium-dark_skin_tone:)"), + ("🧍🏼", "🧍🏼 (:person_standing_medium-light_skin_tone:)"), + ("🧍🏽", "🧍🏽 (:person_standing_medium_skin_tone:)"), + ("🏄", "🏄 (:person_surfing:)"), + ("🏄🏿", "🏄🏿 (:person_surfing_dark_skin_tone:)"), + ("🏄🏻", "🏄🏻 (:person_surfing_light_skin_tone:)"), + ("🏄🏾", "🏄🏾 (:person_surfing_medium-dark_skin_tone:)"), + ("🏄🏼", "🏄🏼 (:person_surfing_medium-light_skin_tone:)"), + ("🏄🏽", "🏄🏽 (:person_surfing_medium_skin_tone:)"), + ("🏊", "🏊 (:person_swimming:)"), + ("🏊🏿", "🏊🏿 (:person_swimming_dark_skin_tone:)"), + ("🏊🏻", "🏊🏻 (:person_swimming_light_skin_tone:)"), + ("🏊🏾", "🏊🏾 (:person_swimming_medium-dark_skin_tone:)"), + ("🏊🏼", "🏊🏼 (:person_swimming_medium-light_skin_tone:)"), + ("🏊🏽", "🏊🏽 (:person_swimming_medium_skin_tone:)"), + ("🛀", "🛀 (:person_taking_bath:)"), + ("🛀🏿", "🛀🏿 (:person_taking_bath_dark_skin_tone:)"), + ("🛀🏻", "🛀🏻 (:person_taking_bath_light_skin_tone:)"), + ("🛀🏾", "🛀🏾 (:person_taking_bath_medium-dark_skin_tone:)"), + ("🛀🏼", "🛀🏼 (:person_taking_bath_medium-light_skin_tone:)"), + ("🛀🏽", "🛀🏽 (:person_taking_bath_medium_skin_tone:)"), + ("💁", "💁 (:person_tipping_hand:)"), + ("💁🏿", "💁🏿 (:person_tipping_hand_dark_skin_tone:)"), + ("💁🏻", "💁🏻 (:person_tipping_hand_light_skin_tone:)"), + ("💁🏾", "💁🏾 (:person_tipping_hand_medium-dark_skin_tone:)"), + ("💁🏼", "💁🏼 (:person_tipping_hand_medium-light_skin_tone:)"), + ("💁🏽", "💁🏽 (:person_tipping_hand_medium_skin_tone:)"), + ("🚶", "🚶 (:person_walking:)"), + ("🚶🏿", "🚶🏿 (:person_walking_dark_skin_tone:)"), + ("🚶\u200d➡️", "🚶\u200d➡️ (:person_walking_facing_right:)"), + ("🚶\u200d➡", "🚶\u200d➡ (:person_walking_facing_right:)"), + ( + "🚶🏿\u200d➡️", + "🚶🏿\u200d➡️ (:person_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d➡", + "🚶🏿\u200d➡ (:person_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏻\u200d➡️", + "🚶🏻\u200d➡️ (:person_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d➡", + "🚶🏻\u200d➡ (:person_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏾\u200d➡️", + "🚶🏾\u200d➡️ (:person_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d➡", + "🚶🏾\u200d➡ (:person_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏼\u200d➡️", + "🚶🏼\u200d➡️ (:person_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d➡", + "🚶🏼\u200d➡ (:person_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏽\u200d➡️", + "🚶🏽\u200d➡️ (:person_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d➡", + "🚶🏽\u200d➡ (:person_walking_facing_right_medium_skin_tone:)", + ), + ("🚶🏻", "🚶🏻 (:person_walking_light_skin_tone:)"), + ("🚶🏾", "🚶🏾 (:person_walking_medium-dark_skin_tone:)"), + ("🚶🏼", "🚶🏼 (:person_walking_medium-light_skin_tone:)"), + ("🚶🏽", "🚶🏽 (:person_walking_medium_skin_tone:)"), + ("👳", "👳 (:person_wearing_turban:)"), + ("👳🏿", "👳🏿 (:person_wearing_turban_dark_skin_tone:)"), + ("👳🏻", "👳🏻 (:person_wearing_turban_light_skin_tone:)"), + ( + "👳🏾", + "👳🏾 (:person_wearing_turban_medium-dark_skin_tone:)", + ), + ( + "👳🏼", + "👳🏼 (:person_wearing_turban_medium-light_skin_tone:)", + ), + ("👳🏽", "👳🏽 (:person_wearing_turban_medium_skin_tone:)"), + ("🧑\u200d🦳", "🧑\u200d🦳 (:person_white_hair:)"), + ("\U0001fac5", "\U0001fac5 (:person_with_crown:)"), + ( + "\U0001fac5🏿", + "\U0001fac5🏿 (:person_with_crown_dark_skin_tone:)", + ), + ( + "\U0001fac5🏻", + "\U0001fac5🏻 (:person_with_crown_light_skin_tone:)", + ), + ( + "\U0001fac5🏾", + "\U0001fac5🏾 (:person_with_crown_medium-dark_skin_tone:)", + ), + ( + "\U0001fac5🏼", + "\U0001fac5🏼 (:person_with_crown_medium-light_skin_tone:)", + ), + ( + "\U0001fac5🏽", + "\U0001fac5🏽 (:person_with_crown_medium_skin_tone:)", + ), + ("👲", "👲 (:person_with_skullcap:)"), + ("👲🏿", "👲🏿 (:person_with_skullcap_dark_skin_tone:)"), + ("👲🏻", "👲🏻 (:person_with_skullcap_light_skin_tone:)"), + ("👲🏾", "👲🏾 (:person_with_skullcap_medium-dark_skin_tone:)"), + ( + "👲🏼", + "👲🏼 (:person_with_skullcap_medium-light_skin_tone:)", + ), + ("👲🏽", "👲🏽 (:person_with_skullcap_medium_skin_tone:)"), + ("👰", "👰 (:person_with_veil:)"), + ("👰🏿", "👰🏿 (:person_with_veil_dark_skin_tone:)"), + ("👰🏻", "👰🏻 (:person_with_veil_light_skin_tone:)"), + ("👰🏾", "👰🏾 (:person_with_veil_medium-dark_skin_tone:)"), + ("👰🏼", "👰🏼 (:person_with_veil_medium-light_skin_tone:)"), + ("👰🏽", "👰🏽 (:person_with_veil_medium_skin_tone:)"), + ("🧑\u200d🦯", "🧑\u200d🦯 (:person_with_white_cane:)"), + ( + "🧑🏿\u200d🦯", + "🧑🏿\u200d🦯 (:person_with_white_cane_dark_skin_tone:)", + ), + ( + "🧑\u200d🦯\u200d➡️", + "🧑\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right:)", + ), + ( + "🧑\u200d🦯\u200d➡", + "🧑\u200d🦯\u200d➡ (:person_with_white_cane_facing_right:)", + ), + ( + "🧑🏿\u200d🦯\u200d➡️", + "🧑🏿\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏿\u200d🦯\u200d➡", + "🧑🏿\u200d🦯\u200d➡ (:person_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "🧑🏻\u200d🦯\u200d➡️", + "🧑🏻\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "🧑🏻\u200d🦯\u200d➡", + "🧑🏻\u200d🦯\u200d➡ (:person_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦯\u200d➡️", + "🧑🏾\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏾\u200d🦯\u200d➡", + "🧑🏾\u200d🦯\u200d➡ (:person_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦯\u200d➡️", + "🧑🏼\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏼\u200d🦯\u200d➡", + "🧑🏼\u200d🦯\u200d➡ (:person_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦯\u200d➡️", + "🧑🏽\u200d🦯\u200d➡️ (:person_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏽\u200d🦯\u200d➡", + "🧑🏽\u200d🦯\u200d➡ (:person_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "🧑🏻\u200d🦯", + "🧑🏻\u200d🦯 (:person_with_white_cane_light_skin_tone:)", + ), + ( + "🧑🏾\u200d🦯", + "🧑🏾\u200d🦯 (:person_with_white_cane_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🦯", + "🧑🏼\u200d🦯 (:person_with_white_cane_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d🦯", + "🧑🏽\u200d🦯 (:person_with_white_cane_medium_skin_tone:)", + ), + ("🧫", "🧫 (:petri_dish:)"), + ("🐦\u200d🔥", "🐦\u200d🔥 (:phoenix:)"), + ("⛏️", "⛏️ (:pick:)"), + ("⛏", "⛏ (:pick:)"), + ("🛻", "🛻 (:pickup_truck:)"), + ("🥧", "🥧 (:pie:)"), + ("🐖", "🐖 (:pig:)"), + ("🐷", "🐷 (:pig_face:)"), + ("🐽", "🐽 (:pig_nose:)"), + ("💩", "💩 (:pile_of_poo:)"), + ("💊", "💊 (:pill:)"), + ("🧑\u200d✈️", "🧑\u200d✈️ (:pilot:)"), + ("🧑\u200d✈", "🧑\u200d✈ (:pilot:)"), + ("🧑🏿\u200d✈️", "🧑🏿\u200d✈️ (:pilot_dark_skin_tone:)"), + ("🧑🏿\u200d✈", "🧑🏿\u200d✈ (:pilot_dark_skin_tone:)"), + ("🧑🏻\u200d✈️", "🧑🏻\u200d✈️ (:pilot_light_skin_tone:)"), + ("🧑🏻\u200d✈", "🧑🏻\u200d✈ (:pilot_light_skin_tone:)"), + ( + "🧑🏾\u200d✈️", + "🧑🏾\u200d✈️ (:pilot_medium-dark_skin_tone:)", + ), + ("🧑🏾\u200d✈", "🧑🏾\u200d✈ (:pilot_medium-dark_skin_tone:)"), + ( + "🧑🏼\u200d✈️", + "🧑🏼\u200d✈️ (:pilot_medium-light_skin_tone:)", + ), + ("🧑🏼\u200d✈", "🧑🏼\u200d✈ (:pilot_medium-light_skin_tone:)"), + ("🧑🏽\u200d✈️", "🧑🏽\u200d✈️ (:pilot_medium_skin_tone:)"), + ("🧑🏽\u200d✈", "🧑🏽\u200d✈ (:pilot_medium_skin_tone:)"), + ("🤌", "🤌 (:pinched_fingers:)"), + ("🤌🏿", "🤌🏿 (:pinched_fingers_dark_skin_tone:)"), + ("🤌🏻", "🤌🏻 (:pinched_fingers_light_skin_tone:)"), + ("🤌🏾", "🤌🏾 (:pinched_fingers_medium-dark_skin_tone:)"), + ("🤌🏼", "🤌🏼 (:pinched_fingers_medium-light_skin_tone:)"), + ("🤌🏽", "🤌🏽 (:pinched_fingers_medium_skin_tone:)"), + ("🤏", "🤏 (:pinching_hand:)"), + ("🤏🏿", "🤏🏿 (:pinching_hand_dark_skin_tone:)"), + ("🤏🏻", "🤏🏻 (:pinching_hand_light_skin_tone:)"), + ("🤏🏾", "🤏🏾 (:pinching_hand_medium-dark_skin_tone:)"), + ("🤏🏼", "🤏🏼 (:pinching_hand_medium-light_skin_tone:)"), + ("🤏🏽", "🤏🏽 (:pinching_hand_medium_skin_tone:)"), + ("🎍", "🎍 (:pine_decoration:)"), + ("🍍", "🍍 (:pineapple:)"), + ("🏓", "🏓 (:ping_pong:)"), + ("\U0001fa77", "\U0001fa77 (:pink_heart:)"), + ("🏴\u200d☠️", "🏴\u200d☠️ (:pirate_flag:)"), + ("🏴\u200d☠", "🏴\u200d☠ (:pirate_flag:)"), + ("🍕", "🍕 (:pizza:)"), + ("🪅", "🪅 (:piñata:)"), + ("🪧", "🪧 (:placard:)"), + ("🛐", "🛐 (:place_of_worship:)"), + ("▶️", "▶️ (:play_button:)"), + ("▶", "▶ (:play_button:)"), + ("⏯️", "⏯️ (:play_or_pause_button:)"), + ("⏯", "⏯ (:play_or_pause_button:)"), + ("\U0001f6dd", "\U0001f6dd (:playground_slide:)"), + ("🥺", "🥺 (:pleading_face:)"), + ("🪠", "🪠 (:plunger:)"), + ("➕", "➕ (:plus:)"), + ("🐻\u200d❄️", "🐻\u200d❄️ (:polar_bear:)"), + ("🐻\u200d❄", "🐻\u200d❄ (:polar_bear:)"), + ("🚓", "🚓 (:police_car:)"), + ("🚨", "🚨 (:police_car_light:)"), + ("👮", "👮 (:police_officer:)"), + ("👮🏿", "👮🏿 (:police_officer_dark_skin_tone:)"), + ("👮🏻", "👮🏻 (:police_officer_light_skin_tone:)"), + ("👮🏾", "👮🏾 (:police_officer_medium-dark_skin_tone:)"), + ("👮🏼", "👮🏼 (:police_officer_medium-light_skin_tone:)"), + ("👮🏽", "👮🏽 (:police_officer_medium_skin_tone:)"), + ("🐩", "🐩 (:poodle:)"), + ("🎱", "🎱 (:pool_8_ball:)"), + ("🍿", "🍿 (:popcorn:)"), + ("🏤", "🏤 (:post_office:)"), + ("📯", "📯 (:postal_horn:)"), + ("📮", "📮 (:postbox:)"), + ("🍲", "🍲 (:pot_of_food:)"), + ("🚰", "🚰 (:potable_water:)"), + ("🥔", "🥔 (:potato:)"), + ("🪴", "🪴 (:potted_plant:)"), + ("🍗", "🍗 (:poultry_leg:)"), + ("💷", "💷 (:pound_banknote:)"), + ("\U0001fad7", "\U0001fad7 (:pouring_liquid:)"), + ("😾", "😾 (:pouting_cat:)"), + ("📿", "📿 (:prayer_beads:)"), + ("\U0001fac3", "\U0001fac3 (:pregnant_man:)"), + ( + "\U0001fac3🏿", + "\U0001fac3🏿 (:pregnant_man_dark_skin_tone:)", + ), + ( + "\U0001fac3🏻", + "\U0001fac3🏻 (:pregnant_man_light_skin_tone:)", + ), + ( + "\U0001fac3🏾", + "\U0001fac3🏾 (:pregnant_man_medium-dark_skin_tone:)", + ), + ( + "\U0001fac3🏼", + "\U0001fac3🏼 (:pregnant_man_medium-light_skin_tone:)", + ), + ( + "\U0001fac3🏽", + "\U0001fac3🏽 (:pregnant_man_medium_skin_tone:)", + ), + ("\U0001fac4", "\U0001fac4 (:pregnant_person:)"), + ( + "\U0001fac4🏿", + "\U0001fac4🏿 (:pregnant_person_dark_skin_tone:)", + ), + ( + "\U0001fac4🏻", + "\U0001fac4🏻 (:pregnant_person_light_skin_tone:)", + ), + ( + "\U0001fac4🏾", + "\U0001fac4🏾 (:pregnant_person_medium-dark_skin_tone:)", + ), + ( + "\U0001fac4🏼", + "\U0001fac4🏼 (:pregnant_person_medium-light_skin_tone:)", + ), + ( + "\U0001fac4🏽", + "\U0001fac4🏽 (:pregnant_person_medium_skin_tone:)", + ), + ("🤰", "🤰 (:pregnant_woman:)"), + ("🤰🏿", "🤰🏿 (:pregnant_woman_dark_skin_tone:)"), + ("🤰🏻", "🤰🏻 (:pregnant_woman_light_skin_tone:)"), + ("🤰🏾", "🤰🏾 (:pregnant_woman_medium-dark_skin_tone:)"), + ("🤰🏼", "🤰🏼 (:pregnant_woman_medium-light_skin_tone:)"), + ("🤰🏽", "🤰🏽 (:pregnant_woman_medium_skin_tone:)"), + ("🥨", "🥨 (:pretzel:)"), + ("🤴", "🤴 (:prince:)"), + ("🤴🏿", "🤴🏿 (:prince_dark_skin_tone:)"), + ("🤴🏻", "🤴🏻 (:prince_light_skin_tone:)"), + ("🤴🏾", "🤴🏾 (:prince_medium-dark_skin_tone:)"), + ("🤴🏼", "🤴🏼 (:prince_medium-light_skin_tone:)"), + ("🤴🏽", "🤴🏽 (:prince_medium_skin_tone:)"), + ("👸", "👸 (:princess:)"), + ("👸🏿", "👸🏿 (:princess_dark_skin_tone:)"), + ("👸🏻", "👸🏻 (:princess_light_skin_tone:)"), + ("👸🏾", "👸🏾 (:princess_medium-dark_skin_tone:)"), + ("👸🏼", "👸🏼 (:princess_medium-light_skin_tone:)"), + ("👸🏽", "👸🏽 (:princess_medium_skin_tone:)"), + ("🖨️", "🖨️ (:printer:)"), + ("🖨", "🖨 (:printer:)"), + ("🚫", "🚫 (:prohibited:)"), + ("🟣", "🟣 (:purple_circle:)"), + ("💜", "💜 (:purple_heart:)"), + ("🟪", "🟪 (:purple_square:)"), + ("👛", "👛 (:purse:)"), + ("📌", "📌 (:pushpin:)"), + ("🧩", "🧩 (:puzzle_piece:)"), + ("🐇", "🐇 (:rabbit:)"), + ("🐰", "🐰 (:rabbit_face:)"), + ("🦝", "🦝 (:raccoon:)"), + ("🏎️", "🏎️ (:racing_car:)"), + ("🏎", "🏎 (:racing_car:)"), + ("📻", "📻 (:radio:)"), + ("🔘", "🔘 (:radio_button:)"), + ("☢️", "☢️ (:radioactive:)"), + ("☢", "☢ (:radioactive:)"), + ("🚃", "🚃 (:railway_car:)"), + ("🛤️", "🛤️ (:railway_track:)"), + ("🛤", "🛤 (:railway_track:)"), + ("🌈", "🌈 (:rainbow:)"), + ("🏳️\u200d🌈", "🏳️\u200d🌈 (:rainbow_flag:)"), + ("🏳\u200d🌈", "🏳\u200d🌈 (:rainbow_flag:)"), + ("🤚", "🤚 (:raised_back_of_hand:)"), + ("🤚🏿", "🤚🏿 (:raised_back_of_hand_dark_skin_tone:)"), + ("🤚🏻", "🤚🏻 (:raised_back_of_hand_light_skin_tone:)"), + ("🤚🏾", "🤚🏾 (:raised_back_of_hand_medium-dark_skin_tone:)"), + ("🤚🏼", "🤚🏼 (:raised_back_of_hand_medium-light_skin_tone:)"), + ("🤚🏽", "🤚🏽 (:raised_back_of_hand_medium_skin_tone:)"), + ("✊", "✊ (:raised_fist:)"), + ("✊🏿", "✊🏿 (:raised_fist_dark_skin_tone:)"), + ("✊🏻", "✊🏻 (:raised_fist_light_skin_tone:)"), + ("✊🏾", "✊🏾 (:raised_fist_medium-dark_skin_tone:)"), + ("✊🏼", "✊🏼 (:raised_fist_medium-light_skin_tone:)"), + ("✊🏽", "✊🏽 (:raised_fist_medium_skin_tone:)"), + ("✋", "✋ (:raised_hand:)"), + ("✋🏿", "✋🏿 (:raised_hand_dark_skin_tone:)"), + ("✋🏻", "✋🏻 (:raised_hand_light_skin_tone:)"), + ("✋🏾", "✋🏾 (:raised_hand_medium-dark_skin_tone:)"), + ("✋🏼", "✋🏼 (:raised_hand_medium-light_skin_tone:)"), + ("✋🏽", "✋🏽 (:raised_hand_medium_skin_tone:)"), + ("🙌", "🙌 (:raising_hands:)"), + ("🙌🏿", "🙌🏿 (:raising_hands_dark_skin_tone:)"), + ("🙌🏻", "🙌🏻 (:raising_hands_light_skin_tone:)"), + ("🙌🏾", "🙌🏾 (:raising_hands_medium-dark_skin_tone:)"), + ("🙌🏼", "🙌🏼 (:raising_hands_medium-light_skin_tone:)"), + ("🙌🏽", "🙌🏽 (:raising_hands_medium_skin_tone:)"), + ("🐏", "🐏 (:ram:)"), + ("🐀", "🐀 (:rat:)"), + ("🪒", "🪒 (:razor:)"), + ("🧾", "🧾 (:receipt:)"), + ("⏺️", "⏺️ (:record_button:)"), + ("⏺", "⏺ (:record_button:)"), + ("♻️", "♻️ (:recycling_symbol:)"), + ("♻", "♻ (:recycling_symbol:)"), + ("🍎", "🍎 (:red_apple:)"), + ("🔴", "🔴 (:red_circle:)"), + ("🧧", "🧧 (:red_envelope:)"), + ("❗", "❗ (:red_exclamation_mark:)"), + ("🦰", "🦰 (:red_hair:)"), + ("❤️", "❤️ (:red_heart:)"), + ("❤", "❤ (:red_heart:)"), + ("🏮", "🏮 (:red_paper_lantern:)"), + ("❓", "❓ (:red_question_mark:)"), + ("🟥", "🟥 (:red_square:)"), + ("🔻", "🔻 (:red_triangle_pointed_down:)"), + ("🔺", "🔺 (:red_triangle_pointed_up:)"), + ("®️", "®️ (:registered:)"), + ("®", "® (:registered:)"), + ("😌", "😌 (:relieved_face:)"), + ("🎗️", "🎗️ (:reminder_ribbon:)"), + ("🎗", "🎗 (:reminder_ribbon:)"), + ("🔁", "🔁 (:repeat_button:)"), + ("🔂", "🔂 (:repeat_single_button:)"), + ("⛑️", "⛑️ (:rescue_worker’s_helmet:)"), + ("⛑", "⛑ (:rescue_worker’s_helmet:)"), + ("🚻", "🚻 (:restroom:)"), + ("◀️", "◀️ (:reverse_button:)"), + ("◀", "◀ (:reverse_button:)"), + ("💞", "💞 (:revolving_hearts:)"), + ("🦏", "🦏 (:rhinoceros:)"), + ("🎀", "🎀 (:ribbon:)"), + ("🍙", "🍙 (:rice_ball:)"), + ("🍘", "🍘 (:rice_cracker:)"), + ("🤜", "🤜 (:right-facing_fist:)"), + ("🤜🏿", "🤜🏿 (:right-facing_fist_dark_skin_tone:)"), + ("🤜🏻", "🤜🏻 (:right-facing_fist_light_skin_tone:)"), + ("🤜🏾", "🤜🏾 (:right-facing_fist_medium-dark_skin_tone:)"), + ("🤜🏼", "🤜🏼 (:right-facing_fist_medium-light_skin_tone:)"), + ("🤜🏽", "🤜🏽 (:right-facing_fist_medium_skin_tone:)"), + ("🗯️", "🗯️ (:right_anger_bubble:)"), + ("🗯", "🗯 (:right_anger_bubble:)"), + ("➡️", "➡️ (:right_arrow:)"), + ("➡", "➡ (:right_arrow:)"), + ("⤵️", "⤵️ (:right_arrow_curving_down:)"), + ("⤵", "⤵ (:right_arrow_curving_down:)"), + ("↩️", "↩️ (:right_arrow_curving_left:)"), + ("↩", "↩ (:right_arrow_curving_left:)"), + ("⤴️", "⤴️ (:right_arrow_curving_up:)"), + ("⤴", "⤴ (:right_arrow_curving_up:)"), + ("\U0001faf1", "\U0001faf1 (:rightwards_hand:)"), + ( + "\U0001faf1🏿", + "\U0001faf1🏿 (:rightwards_hand_dark_skin_tone:)", + ), + ( + "\U0001faf1🏻", + "\U0001faf1🏻 (:rightwards_hand_light_skin_tone:)", + ), + ( + "\U0001faf1🏾", + "\U0001faf1🏾 (:rightwards_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf1🏼", + "\U0001faf1🏼 (:rightwards_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf1🏽", + "\U0001faf1🏽 (:rightwards_hand_medium_skin_tone:)", + ), + ("\U0001faf8", "\U0001faf8 (:rightwards_pushing_hand:)"), + ( + "\U0001faf8🏿", + "\U0001faf8🏿 (:rightwards_pushing_hand_dark_skin_tone:)", + ), + ( + "\U0001faf8🏻", + "\U0001faf8🏻 (:rightwards_pushing_hand_light_skin_tone:)", + ), + ( + "\U0001faf8🏾", + "\U0001faf8🏾 (:rightwards_pushing_hand_medium-dark_skin_tone:)", + ), + ( + "\U0001faf8🏼", + "\U0001faf8🏼 (:rightwards_pushing_hand_medium-light_skin_tone:)", + ), + ( + "\U0001faf8🏽", + "\U0001faf8🏽 (:rightwards_pushing_hand_medium_skin_tone:)", + ), + ("💍", "💍 (:ring:)"), + ("\U0001f6df", "\U0001f6df (:ring_buoy:)"), + ("🪐", "🪐 (:ringed_planet:)"), + ("🍠", "🍠 (:roasted_sweet_potato:)"), + ("🤖", "🤖 (:robot:)"), + ("🪨", "🪨 (:rock:)"), + ("🚀", "🚀 (:rocket:)"), + ("🧻", "🧻 (:roll_of_paper:)"), + ("🗞️", "🗞️ (:rolled-up_newspaper:)"), + ("🗞", "🗞 (:rolled-up_newspaper:)"), + ("🎢", "🎢 (:roller_coaster:)"), + ("🛼", "🛼 (:roller_skate:)"), + ("🤣", "🤣 (:rolling_on_the_floor_laughing:)"), + ("🐓", "🐓 (:rooster:)"), + ("🌹", "🌹 (:rose:)"), + ("🏵️", "🏵️ (:rosette:)"), + ("🏵", "🏵 (:rosette:)"), + ("📍", "📍 (:round_pushpin:)"), + ("🏉", "🏉 (:rugby_football:)"), + ("🎽", "🎽 (:running_shirt:)"), + ("👟", "👟 (:running_shoe:)"), + ("😥", "😥 (:sad_but_relieved_face:)"), + ("🧷", "🧷 (:safety_pin:)"), + ("🦺", "🦺 (:safety_vest:)"), + ("⛵", "⛵ (:sailboat:)"), + ("🍶", "🍶 (:sake:)"), + ("🧂", "🧂 (:salt:)"), + ("\U0001fae1", "\U0001fae1 (:saluting_face:)"), + ("🥪", "🥪 (:sandwich:)"), + ("🥻", "🥻 (:sari:)"), + ("🛰️", "🛰️ (:satellite:)"), + ("🛰", "🛰 (:satellite:)"), + ("📡", "📡 (:satellite_antenna:)"), + ("🦕", "🦕 (:sauropod:)"), + ("🎷", "🎷 (:saxophone:)"), + ("🧣", "🧣 (:scarf:)"), + ("🏫", "🏫 (:school:)"), + ("🧑\u200d🔬", "🧑\u200d🔬 (:scientist:)"), + ("🧑🏿\u200d🔬", "🧑🏿\u200d🔬 (:scientist_dark_skin_tone:)"), + ("🧑🏻\u200d🔬", "🧑🏻\u200d🔬 (:scientist_light_skin_tone:)"), + ( + "🧑🏾\u200d🔬", + "🧑🏾\u200d🔬 (:scientist_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🔬", + "🧑🏼\u200d🔬 (:scientist_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🔬", "🧑🏽\u200d🔬 (:scientist_medium_skin_tone:)"), + ("✂️", "✂️ (:scissors:)"), + ("✂", "✂ (:scissors:)"), + ("🦂", "🦂 (:scorpion:)"), + ("🪛", "🪛 (:screwdriver:)"), + ("📜", "📜 (:scroll:)"), + ("🦭", "🦭 (:seal:)"), + ("💺", "💺 (:seat:)"), + ("🙈", "🙈 (:see-no-evil_monkey:)"), + ("🌱", "🌱 (:seedling:)"), + ("🤳", "🤳 (:selfie:)"), + ("🤳🏿", "🤳🏿 (:selfie_dark_skin_tone:)"), + ("🤳🏻", "🤳🏻 (:selfie_light_skin_tone:)"), + ("🤳🏾", "🤳🏾 (:selfie_medium-dark_skin_tone:)"), + ("🤳🏼", "🤳🏼 (:selfie_medium-light_skin_tone:)"), + ("🤳🏽", "🤳🏽 (:selfie_medium_skin_tone:)"), + ("🐕\u200d🦺", "🐕\u200d🦺 (:service_dog:)"), + ("🕢", "🕢 (:seven-thirty:)"), + ("🕖", "🕖 (:seven_o’clock:)"), + ("🪡", "🪡 (:sewing_needle:)"), + ("\U0001fae8", "\U0001fae8 (:shaking_face:)"), + ("🥘", "🥘 (:shallow_pan_of_food:)"), + ("☘️", "☘️ (:shamrock:)"), + ("☘", "☘ (:shamrock:)"), + ("🦈", "🦈 (:shark:)"), + ("🍧", "🍧 (:shaved_ice:)"), + ("🌾", "🌾 (:sheaf_of_rice:)"), + ("🛡️", "🛡️ (:shield:)"), + ("🛡", "🛡 (:shield:)"), + ("⛩️", "⛩️ (:shinto_shrine:)"), + ("⛩", "⛩ (:shinto_shrine:)"), + ("🚢", "🚢 (:ship:)"), + ("🌠", "🌠 (:shooting_star:)"), + ("🛍️", "🛍️ (:shopping_bags:)"), + ("🛍", "🛍 (:shopping_bags:)"), + ("🛒", "🛒 (:shopping_cart:)"), + ("🍰", "🍰 (:shortcake:)"), + ("🩳", "🩳 (:shorts:)"), + ("🚿", "🚿 (:shower:)"), + ("🦐", "🦐 (:shrimp:)"), + ("🔀", "🔀 (:shuffle_tracks_button:)"), + ("🤫", "🤫 (:shushing_face:)"), + ("🤘", "🤘 (:sign_of_the_horns:)"), + ("🤘🏿", "🤘🏿 (:sign_of_the_horns_dark_skin_tone:)"), + ("🤘🏻", "🤘🏻 (:sign_of_the_horns_light_skin_tone:)"), + ("🤘🏾", "🤘🏾 (:sign_of_the_horns_medium-dark_skin_tone:)"), + ("🤘🏼", "🤘🏼 (:sign_of_the_horns_medium-light_skin_tone:)"), + ("🤘🏽", "🤘🏽 (:sign_of_the_horns_medium_skin_tone:)"), + ("🧑\u200d🎤", "🧑\u200d🎤 (:singer:)"), + ("🧑🏿\u200d🎤", "🧑🏿\u200d🎤 (:singer_dark_skin_tone:)"), + ("🧑🏻\u200d🎤", "🧑🏻\u200d🎤 (:singer_light_skin_tone:)"), + ("🧑🏾\u200d🎤", "🧑🏾\u200d🎤 (:singer_medium-dark_skin_tone:)"), + ( + "🧑🏼\u200d🎤", + "🧑🏼\u200d🎤 (:singer_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🎤", "🧑🏽\u200d🎤 (:singer_medium_skin_tone:)"), + ("🕡", "🕡 (:six-thirty:)"), + ("🕕", "🕕 (:six_o’clock:)"), + ("🛹", "🛹 (:skateboard:)"), + ("⛷️", "⛷️ (:skier:)"), + ("⛷", "⛷ (:skier:)"), + ("🎿", "🎿 (:skis:)"), + ("💀", "💀 (:skull:)"), + ("☠️", "☠️ (:skull_and_crossbones:)"), + ("☠", "☠ (:skull_and_crossbones:)"), + ("🦨", "🦨 (:skunk:)"), + ("🛷", "🛷 (:sled:)"), + ("😴", "😴 (:sleeping_face:)"), + ("😪", "😪 (:sleepy_face:)"), + ("🙁", "🙁 (:slightly_frowning_face:)"), + ("🙂", "🙂 (:slightly_smiling_face:)"), + ("🎰", "🎰 (:slot_machine:)"), + ("🦥", "🦥 (:sloth:)"), + ("🛩️", "🛩️ (:small_airplane:)"), + ("🛩", "🛩 (:small_airplane:)"), + ("🔹", "🔹 (:small_blue_diamond:)"), + ("🔸", "🔸 (:small_orange_diamond:)"), + ("😻", "😻 (:smiling_cat_with_heart-eyes:)"), + ("☺️", "☺️ (:smiling_face:)"), + ("☺", "☺ (:smiling_face:)"), + ("😇", "😇 (:smiling_face_with_halo:)"), + ("😍", "😍 (:smiling_face_with_heart-eyes:)"), + ("🥰", "🥰 (:smiling_face_with_hearts:)"), + ("😈", "😈 (:smiling_face_with_horns:)"), + ("🤗", "🤗 (:smiling_face_with_open_hands:)"), + ("😊", "😊 (:smiling_face_with_smiling_eyes:)"), + ("😎", "😎 (:smiling_face_with_sunglasses:)"), + ("🥲", "🥲 (:smiling_face_with_tear:)"), + ("😏", "😏 (:smirking_face:)"), + ("🐌", "🐌 (:snail:)"), + ("🐍", "🐍 (:snake:)"), + ("🤧", "🤧 (:sneezing_face:)"), + ("🏔️", "🏔️ (:snow-capped_mountain:)"), + ("🏔", "🏔 (:snow-capped_mountain:)"), + ("🏂", "🏂 (:snowboarder:)"), + ("🏂🏿", "🏂🏿 (:snowboarder_dark_skin_tone:)"), + ("🏂🏻", "🏂🏻 (:snowboarder_light_skin_tone:)"), + ("🏂🏾", "🏂🏾 (:snowboarder_medium-dark_skin_tone:)"), + ("🏂🏼", "🏂🏼 (:snowboarder_medium-light_skin_tone:)"), + ("🏂🏽", "🏂🏽 (:snowboarder_medium_skin_tone:)"), + ("❄️", "❄️ (:snowflake:)"), + ("❄", "❄ (:snowflake:)"), + ("☃️", "☃️ (:snowman:)"), + ("☃", "☃ (:snowman:)"), + ("⛄", "⛄ (:snowman_without_snow:)"), + ("🧼", "🧼 (:soap:)"), + ("⚽", "⚽ (:soccer_ball:)"), + ("🧦", "🧦 (:socks:)"), + ("🍦", "🍦 (:soft_ice_cream:)"), + ("🥎", "🥎 (:softball:)"), + ("♠️", "♠️ (:spade_suit:)"), + ("♠", "♠ (:spade_suit:)"), + ("🍝", "🍝 (:spaghetti:)"), + ("❇️", "❇️ (:sparkle:)"), + ("❇", "❇ (:sparkle:)"), + ("🎇", "🎇 (:sparkler:)"), + ("✨", "✨ (:sparkles:)"), + ("💖", "💖 (:sparkling_heart:)"), + ("🙊", "🙊 (:speak-no-evil_monkey:)"), + ("🔊", "🔊 (:speaker_high_volume:)"), + ("🔈", "🔈 (:speaker_low_volume:)"), + ("🔉", "🔉 (:speaker_medium_volume:)"), + ("🗣️", "🗣️ (:speaking_head:)"), + ("🗣", "🗣 (:speaking_head:)"), + ("💬", "💬 (:speech_balloon:)"), + ("🚤", "🚤 (:speedboat:)"), + ("🕷️", "🕷️ (:spider:)"), + ("🕷", "🕷 (:spider:)"), + ("🕸️", "🕸️ (:spider_web:)"), + ("🕸", "🕸 (:spider_web:)"), + ("🗓️", "🗓️ (:spiral_calendar:)"), + ("🗓", "🗓 (:spiral_calendar:)"), + ("🗒️", "🗒️ (:spiral_notepad:)"), + ("🗒", "🗒 (:spiral_notepad:)"), + ("🐚", "🐚 (:spiral_shell:)"), + ("🧽", "🧽 (:sponge:)"), + ("🥄", "🥄 (:spoon:)"), + ("🚙", "🚙 (:sport_utility_vehicle:)"), + ("🏅", "🏅 (:sports_medal:)"), + ("🐳", "🐳 (:spouting_whale:)"), + ("🦑", "🦑 (:squid:)"), + ("😝", "😝 (:squinting_face_with_tongue:)"), + ("🏟️", "🏟️ (:stadium:)"), + ("🏟", "🏟 (:stadium:)"), + ("⭐", "⭐ (:star:)"), + ("🤩", "🤩 (:star-struck:)"), + ("☪️", "☪️ (:star_and_crescent:)"), + ("☪", "☪ (:star_and_crescent:)"), + ("✡️", "✡️ (:star_of_David:)"), + ("✡", "✡ (:star_of_David:)"), + ("🚉", "🚉 (:station:)"), + ("🍜", "🍜 (:steaming_bowl:)"), + ("🩺", "🩺 (:stethoscope:)"), + ("⏹️", "⏹️ (:stop_button:)"), + ("⏹", "⏹ (:stop_button:)"), + ("🛑", "🛑 (:stop_sign:)"), + ("⏱️", "⏱️ (:stopwatch:)"), + ("⏱", "⏱ (:stopwatch:)"), + ("📏", "📏 (:straight_ruler:)"), + ("🍓", "🍓 (:strawberry:)"), + ("🧑\u200d🎓", "🧑\u200d🎓 (:student:)"), + ("🧑🏿\u200d🎓", "🧑🏿\u200d🎓 (:student_dark_skin_tone:)"), + ("🧑🏻\u200d🎓", "🧑🏻\u200d🎓 (:student_light_skin_tone:)"), + ( + "🧑🏾\u200d🎓", + "🧑🏾\u200d🎓 (:student_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🎓", + "🧑🏼\u200d🎓 (:student_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🎓", "🧑🏽\u200d🎓 (:student_medium_skin_tone:)"), + ("🎙️", "🎙️ (:studio_microphone:)"), + ("🎙", "🎙 (:studio_microphone:)"), + ("🥙", "🥙 (:stuffed_flatbread:)"), + ("☀️", "☀️ (:sun:)"), + ("☀", "☀ (:sun:)"), + ("⛅", "⛅ (:sun_behind_cloud:)"), + ("🌥️", "🌥️ (:sun_behind_large_cloud:)"), + ("🌥", "🌥 (:sun_behind_large_cloud:)"), + ("🌦️", "🌦️ (:sun_behind_rain_cloud:)"), + ("🌦", "🌦 (:sun_behind_rain_cloud:)"), + ("🌤️", "🌤️ (:sun_behind_small_cloud:)"), + ("🌤", "🌤 (:sun_behind_small_cloud:)"), + ("🌞", "🌞 (:sun_with_face:)"), + ("🌻", "🌻 (:sunflower:)"), + ("🕶️", "🕶️ (:sunglasses:)"), + ("🕶", "🕶 (:sunglasses:)"), + ("🌅", "🌅 (:sunrise:)"), + ("🌄", "🌄 (:sunrise_over_mountains:)"), + ("🌇", "🌇 (:sunset:)"), + ("🦸", "🦸 (:superhero:)"), + ("🦸🏿", "🦸🏿 (:superhero_dark_skin_tone:)"), + ("🦸🏻", "🦸🏻 (:superhero_light_skin_tone:)"), + ("🦸🏾", "🦸🏾 (:superhero_medium-dark_skin_tone:)"), + ("🦸🏼", "🦸🏼 (:superhero_medium-light_skin_tone:)"), + ("🦸🏽", "🦸🏽 (:superhero_medium_skin_tone:)"), + ("🦹", "🦹 (:supervillain:)"), + ("🦹🏿", "🦹🏿 (:supervillain_dark_skin_tone:)"), + ("🦹🏻", "🦹🏻 (:supervillain_light_skin_tone:)"), + ("🦹🏾", "🦹🏾 (:supervillain_medium-dark_skin_tone:)"), + ("🦹🏼", "🦹🏼 (:supervillain_medium-light_skin_tone:)"), + ("🦹🏽", "🦹🏽 (:supervillain_medium_skin_tone:)"), + ("🍣", "🍣 (:sushi:)"), + ("🚟", "🚟 (:suspension_railway:)"), + ("🦢", "🦢 (:swan:)"), + ("💦", "💦 (:sweat_droplets:)"), + ("🕍", "🕍 (:synagogue:)"), + ("💉", "💉 (:syringe:)"), + ("👕", "👕 (:t-shirt:)"), + ("🌮", "🌮 (:taco:)"), + ("🥡", "🥡 (:takeout_box:)"), + ("🫔", "🫔 (:tamale:)"), + ("🎋", "🎋 (:tanabata_tree:)"), + ("🍊", "🍊 (:tangerine:)"), + ("🚕", "🚕 (:taxi:)"), + ("🧑\u200d🏫", "🧑\u200d🏫 (:teacher:)"), + ("🧑🏿\u200d🏫", "🧑🏿\u200d🏫 (:teacher_dark_skin_tone:)"), + ("🧑🏻\u200d🏫", "🧑🏻\u200d🏫 (:teacher_light_skin_tone:)"), + ( + "🧑🏾\u200d🏫", + "🧑🏾\u200d🏫 (:teacher_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d🏫", + "🧑🏼\u200d🏫 (:teacher_medium-light_skin_tone:)", + ), + ("🧑🏽\u200d🏫", "🧑🏽\u200d🏫 (:teacher_medium_skin_tone:)"), + ("🍵", "🍵 (:teacup_without_handle:)"), + ("🫖", "🫖 (:teapot:)"), + ("📆", "📆 (:tear-off_calendar:)"), + ("🧑\u200d💻", "🧑\u200d💻 (:technologist:)"), + ("🧑🏿\u200d💻", "🧑🏿\u200d💻 (:technologist_dark_skin_tone:)"), + ("🧑🏻\u200d💻", "🧑🏻\u200d💻 (:technologist_light_skin_tone:)"), + ( + "🧑🏾\u200d💻", + "🧑🏾\u200d💻 (:technologist_medium-dark_skin_tone:)", + ), + ( + "🧑🏼\u200d💻", + "🧑🏼\u200d💻 (:technologist_medium-light_skin_tone:)", + ), + ( + "🧑🏽\u200d💻", + "🧑🏽\u200d💻 (:technologist_medium_skin_tone:)", + ), + ("🧸", "🧸 (:teddy_bear:)"), + ("☎️", "☎️ (:telephone:)"), + ("☎", "☎ (:telephone:)"), + ("📞", "📞 (:telephone_receiver:)"), + ("🔭", "🔭 (:telescope:)"), + ("📺", "📺 (:television:)"), + ("🕥", "🕥 (:ten-thirty:)"), + ("🕙", "🕙 (:ten_o’clock:)"), + ("🎾", "🎾 (:tennis:)"), + ("⛺", "⛺ (:tent:)"), + ("🧪", "🧪 (:test_tube:)"), + ("🌡️", "🌡️ (:thermometer:)"), + ("🌡", "🌡 (:thermometer:)"), + ("🤔", "🤔 (:thinking_face:)"), + ("🩴", "🩴 (:thong_sandal:)"), + ("💭", "💭 (:thought_balloon:)"), + ("🧵", "🧵 (:thread:)"), + ("🕞", "🕞 (:three-thirty:)"), + ("🕒", "🕒 (:three_o’clock:)"), + ("👎", "👎 (:thumbs_down:)"), + ("👎🏿", "👎🏿 (:thumbs_down_dark_skin_tone:)"), + ("👎🏻", "👎🏻 (:thumbs_down_light_skin_tone:)"), + ("👎🏾", "👎🏾 (:thumbs_down_medium-dark_skin_tone:)"), + ("👎🏼", "👎🏼 (:thumbs_down_medium-light_skin_tone:)"), + ("👎🏽", "👎🏽 (:thumbs_down_medium_skin_tone:)"), + ("👍", "👍 (:thumbs_up:)"), + ("👍🏿", "👍🏿 (:thumbs_up_dark_skin_tone:)"), + ("👍🏻", "👍🏻 (:thumbs_up_light_skin_tone:)"), + ("👍🏾", "👍🏾 (:thumbs_up_medium-dark_skin_tone:)"), + ("👍🏼", "👍🏼 (:thumbs_up_medium-light_skin_tone:)"), + ("👍🏽", "👍🏽 (:thumbs_up_medium_skin_tone:)"), + ("🎫", "🎫 (:ticket:)"), + ("🐅", "🐅 (:tiger:)"), + ("🐯", "🐯 (:tiger_face:)"), + ("⏲️", "⏲️ (:timer_clock:)"), + ("⏲", "⏲ (:timer_clock:)"), + ("😫", "😫 (:tired_face:)"), + ("🚽", "🚽 (:toilet:)"), + ("🍅", "🍅 (:tomato:)"), + ("👅", "👅 (:tongue:)"), + ("🧰", "🧰 (:toolbox:)"), + ("🦷", "🦷 (:tooth:)"), + ("🪥", "🪥 (:toothbrush:)"), + ("🎩", "🎩 (:top_hat:)"), + ("🌪️", "🌪️ (:tornado:)"), + ("🌪", "🌪 (:tornado:)"), + ("🖲️", "🖲️ (:trackball:)"), + ("🖲", "🖲 (:trackball:)"), + ("🚜", "🚜 (:tractor:)"), + ("™️", "™️ (:trade_mark:)"), + ("™", "™ (:trade_mark:)"), + ("🚆", "🚆 (:train:)"), + ("🚊", "🚊 (:tram:)"), + ("🚋", "🚋 (:tram_car:)"), + ("🏳️\u200d⚧️", "🏳️\u200d⚧️ (:transgender_flag:)"), + ("🏳\u200d⚧️", "🏳\u200d⚧️ (:transgender_flag:)"), + ("🏳️\u200d⚧", "🏳️\u200d⚧ (:transgender_flag:)"), + ("🏳\u200d⚧", "🏳\u200d⚧ (:transgender_flag:)"), + ("⚧️", "⚧️ (:transgender_symbol:)"), + ("⚧", "⚧ (:transgender_symbol:)"), + ("🚩", "🚩 (:triangular_flag:)"), + ("📐", "📐 (:triangular_ruler:)"), + ("🔱", "🔱 (:trident_emblem:)"), + ("\U0001f9cc", "\U0001f9cc (:troll:)"), + ("🚎", "🚎 (:trolleybus:)"), + ("🏆", "🏆 (:trophy:)"), + ("🍹", "🍹 (:tropical_drink:)"), + ("🐠", "🐠 (:tropical_fish:)"), + ("🎺", "🎺 (:trumpet:)"), + ("🌷", "🌷 (:tulip:)"), + ("🥃", "🥃 (:tumbler_glass:)"), + ("🦃", "🦃 (:turkey:)"), + ("🐢", "🐢 (:turtle:)"), + ("🕧", "🕧 (:twelve-thirty:)"), + ("🕛", "🕛 (:twelve_o’clock:)"), + ("🐫", "🐫 (:two-hump_camel:)"), + ("🕝", "🕝 (:two-thirty:)"), + ("💕", "💕 (:two_hearts:)"), + ("🕑", "🕑 (:two_o’clock:)"), + ("☂️", "☂️ (:umbrella:)"), + ("☂", "☂ (:umbrella:)"), + ("⛱️", "⛱️ (:umbrella_on_ground:)"), + ("⛱", "⛱ (:umbrella_on_ground:)"), + ("☔", "☔ (:umbrella_with_rain_drops:)"), + ("😒", "😒 (:unamused_face:)"), + ("🦄", "🦄 (:unicorn:)"), + ("🔓", "🔓 (:unlocked:)"), + ("↕️", "↕️ (:up-down_arrow:)"), + ("↕", "↕ (:up-down_arrow:)"), + ("↖️", "↖️ (:up-left_arrow:)"), + ("↖", "↖ (:up-left_arrow:)"), + ("↗️", "↗️ (:up-right_arrow:)"), + ("↗", "↗ (:up-right_arrow:)"), + ("⬆️", "⬆️ (:up_arrow:)"), + ("⬆", "⬆ (:up_arrow:)"), + ("🙃", "🙃 (:upside-down_face:)"), + ("🔼", "🔼 (:upwards_button:)"), + ("🧛", "🧛 (:vampire:)"), + ("🧛🏿", "🧛🏿 (:vampire_dark_skin_tone:)"), + ("🧛🏻", "🧛🏻 (:vampire_light_skin_tone:)"), + ("🧛🏾", "🧛🏾 (:vampire_medium-dark_skin_tone:)"), + ("🧛🏼", "🧛🏼 (:vampire_medium-light_skin_tone:)"), + ("🧛🏽", "🧛🏽 (:vampire_medium_skin_tone:)"), + ("🚦", "🚦 (:vertical_traffic_light:)"), + ("📳", "📳 (:vibration_mode:)"), + ("✌️", "✌️ (:victory_hand:)"), + ("✌", "✌ (:victory_hand:)"), + ("✌🏿", "✌🏿 (:victory_hand_dark_skin_tone:)"), + ("✌🏻", "✌🏻 (:victory_hand_light_skin_tone:)"), + ("✌🏾", "✌🏾 (:victory_hand_medium-dark_skin_tone:)"), + ("✌🏼", "✌🏼 (:victory_hand_medium-light_skin_tone:)"), + ("✌🏽", "✌🏽 (:victory_hand_medium_skin_tone:)"), + ("📹", "📹 (:video_camera:)"), + ("🎮", "🎮 (:video_game:)"), + ("📼", "📼 (:videocassette:)"), + ("🎻", "🎻 (:violin:)"), + ("🌋", "🌋 (:volcano:)"), + ("🏐", "🏐 (:volleyball:)"), + ("🖖", "🖖 (:vulcan_salute:)"), + ("🖖🏿", "🖖🏿 (:vulcan_salute_dark_skin_tone:)"), + ("🖖🏻", "🖖🏻 (:vulcan_salute_light_skin_tone:)"), + ("🖖🏾", "🖖🏾 (:vulcan_salute_medium-dark_skin_tone:)"), + ("🖖🏼", "🖖🏼 (:vulcan_salute_medium-light_skin_tone:)"), + ("🖖🏽", "🖖🏽 (:vulcan_salute_medium_skin_tone:)"), + ("🧇", "🧇 (:waffle:)"), + ("🌘", "🌘 (:waning_crescent_moon:)"), + ("🌖", "🌖 (:waning_gibbous_moon:)"), + ("⚠️", "⚠️ (:warning:)"), + ("⚠", "⚠ (:warning:)"), + ("🗑️", "🗑️ (:wastebasket:)"), + ("🗑", "🗑 (:wastebasket:)"), + ("⌚", "⌚ (:watch:)"), + ("🐃", "🐃 (:water_buffalo:)"), + ("🚾", "🚾 (:water_closet:)"), + ("🔫", "🔫 (:water_pistol:)"), + ("🌊", "🌊 (:water_wave:)"), + ("🍉", "🍉 (:watermelon:)"), + ("👋", "👋 (:waving_hand:)"), + ("👋🏿", "👋🏿 (:waving_hand_dark_skin_tone:)"), + ("👋🏻", "👋🏻 (:waving_hand_light_skin_tone:)"), + ("👋🏾", "👋🏾 (:waving_hand_medium-dark_skin_tone:)"), + ("👋🏼", "👋🏼 (:waving_hand_medium-light_skin_tone:)"), + ("👋🏽", "👋🏽 (:waving_hand_medium_skin_tone:)"), + ("〰️", "〰️ (:wavy_dash:)"), + ("〰", "〰 (:wavy_dash:)"), + ("🌒", "🌒 (:waxing_crescent_moon:)"), + ("🌔", "🌔 (:waxing_gibbous_moon:)"), + ("🙀", "🙀 (:weary_cat:)"), + ("😩", "😩 (:weary_face:)"), + ("💒", "💒 (:wedding:)"), + ("🐋", "🐋 (:whale:)"), + ("\U0001f6de", "\U0001f6de (:wheel:)"), + ("☸️", "☸️ (:wheel_of_dharma:)"), + ("☸", "☸ (:wheel_of_dharma:)"), + ("♿", "♿ (:wheelchair_symbol:)"), + ("🦯", "🦯 (:white_cane:)"), + ("⚪", "⚪ (:white_circle:)"), + ("❕", "❕ (:white_exclamation_mark:)"), + ("🏳️", "🏳️ (:white_flag:)"), + ("🏳", "🏳 (:white_flag:)"), + ("💮", "💮 (:white_flower:)"), + ("🦳", "🦳 (:white_hair:)"), + ("🤍", "🤍 (:white_heart:)"), + ("⬜", "⬜ (:white_large_square:)"), + ("◽", "◽ (:white_medium-small_square:)"), + ("◻️", "◻️ (:white_medium_square:)"), + ("◻", "◻ (:white_medium_square:)"), + ("❔", "❔ (:white_question_mark:)"), + ("▫️", "▫️ (:white_small_square:)"), + ("▫", "▫ (:white_small_square:)"), + ("🔳", "🔳 (:white_square_button:)"), + ("🥀", "🥀 (:wilted_flower:)"), + ("🎐", "🎐 (:wind_chime:)"), + ("🌬️", "🌬️ (:wind_face:)"), + ("🌬", "🌬 (:wind_face:)"), + ("🪟", "🪟 (:window:)"), + ("🍷", "🍷 (:wine_glass:)"), + ("\U0001fabd", "\U0001fabd (:wing:)"), + ("😉", "😉 (:winking_face:)"), + ("😜", "😜 (:winking_face_with_tongue:)"), + ("\U0001f6dc", "\U0001f6dc (:wireless:)"), + ("🐺", "🐺 (:wolf:)"), + ("👩", "👩 (:woman:)"), + ("👫", "👫 (:woman_and_man_holding_hands:)"), + ("👫🏿", "👫🏿 (:woman_and_man_holding_hands_dark_skin_tone:)"), + ( + "👩🏿\u200d🤝\u200d👨🏻", + "👩🏿\u200d🤝\u200d👨🏻 (:woman_and_man_holding_hands_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👨🏾", + "👩🏿\u200d🤝\u200d👨🏾 (:woman_and_man_holding_hands_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👨🏼", + "👩🏿\u200d🤝\u200d👨🏼 (:woman_and_man_holding_hands_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👨🏽", + "👩🏿\u200d🤝\u200d👨🏽 (:woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone:)", + ), + ( + "👫🏻", + "👫🏻 (:woman_and_man_holding_hands_light_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👨🏿", + "👩🏻\u200d🤝\u200d👨🏿 (:woman_and_man_holding_hands_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👨🏾", + "👩🏻\u200d🤝\u200d👨🏾 (:woman_and_man_holding_hands_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👨🏼", + "👩🏻\u200d🤝\u200d👨🏼 (:woman_and_man_holding_hands_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👨🏽", + "👩🏻\u200d🤝\u200d👨🏽 (:woman_and_man_holding_hands_light_skin_tone_medium_skin_tone:)", + ), + ( + "👫🏾", + "👫🏾 (:woman_and_man_holding_hands_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👨🏿", + "👩🏾\u200d🤝\u200d👨🏿 (:woman_and_man_holding_hands_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👨🏻", + "👩🏾\u200d🤝\u200d👨🏻 (:woman_and_man_holding_hands_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👨🏼", + "👩🏾\u200d🤝\u200d👨🏼 (:woman_and_man_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👨🏽", + "👩🏾\u200d🤝\u200d👨🏽 (:woman_and_man_holding_hands_medium-dark_skin_tone_medium_skin_tone:)", + ), + ( + "👫🏼", + "👫🏼 (:woman_and_man_holding_hands_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👨🏿", + "👩🏼\u200d🤝\u200d👨🏿 (:woman_and_man_holding_hands_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👨🏻", + "👩🏼\u200d🤝\u200d👨🏻 (:woman_and_man_holding_hands_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👨🏾", + "👩🏼\u200d🤝\u200d👨🏾 (:woman_and_man_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👨🏽", + "👩🏼\u200d🤝\u200d👨🏽 (:woman_and_man_holding_hands_medium-light_skin_tone_medium_skin_tone:)", + ), + ( + "👫🏽", + "👫🏽 (:woman_and_man_holding_hands_medium_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👨🏿", + "👩🏽\u200d🤝\u200d👨🏿 (:woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👨🏻", + "👩🏽\u200d🤝\u200d👨🏻 (:woman_and_man_holding_hands_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👨🏾", + "👩🏽\u200d🤝\u200d👨🏾 (:woman_and_man_holding_hands_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👨🏼", + "👩🏽\u200d🤝\u200d👨🏼 (:woman_and_man_holding_hands_medium_skin_tone_medium-light_skin_tone:)", + ), + ("👩\u200d🎨", "👩\u200d🎨 (:woman_artist:)"), + ("👩🏿\u200d🎨", "👩🏿\u200d🎨 (:woman_artist_dark_skin_tone:)"), + ("👩🏻\u200d🎨", "👩🏻\u200d🎨 (:woman_artist_light_skin_tone:)"), + ( + "👩🏾\u200d🎨", + "👩🏾\u200d🎨 (:woman_artist_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🎨", + "👩🏼\u200d🎨 (:woman_artist_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🎨", + "👩🏽\u200d🎨 (:woman_artist_medium_skin_tone:)", + ), + ("👩\u200d🚀", "👩\u200d🚀 (:woman_astronaut:)"), + ( + "👩🏿\u200d🚀", + "👩🏿\u200d🚀 (:woman_astronaut_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🚀", + "👩🏻\u200d🚀 (:woman_astronaut_light_skin_tone:)", + ), + ( + "👩🏾\u200d🚀", + "👩🏾\u200d🚀 (:woman_astronaut_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🚀", + "👩🏼\u200d🚀 (:woman_astronaut_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🚀", + "👩🏽\u200d🚀 (:woman_astronaut_medium_skin_tone:)", + ), + ("👩\u200d🦲", "👩\u200d🦲 (:woman_bald:)"), + ("🧔\u200d♀️", "🧔\u200d♀️ (:woman_beard:)"), + ("🧔\u200d♀", "🧔\u200d♀ (:woman_beard:)"), + ("🚴\u200d♀️", "🚴\u200d♀️ (:woman_biking:)"), + ("🚴\u200d♀", "🚴\u200d♀ (:woman_biking:)"), + ( + "🚴🏿\u200d♀️", + "🚴🏿\u200d♀️ (:woman_biking_dark_skin_tone:)", + ), + ("🚴🏿\u200d♀", "🚴🏿\u200d♀ (:woman_biking_dark_skin_tone:)"), + ( + "🚴🏻\u200d♀️", + "🚴🏻\u200d♀️ (:woman_biking_light_skin_tone:)", + ), + ("🚴🏻\u200d♀", "🚴🏻\u200d♀ (:woman_biking_light_skin_tone:)"), + ( + "🚴🏾\u200d♀️", + "🚴🏾\u200d♀️ (:woman_biking_medium-dark_skin_tone:)", + ), + ( + "🚴🏾\u200d♀", + "🚴🏾\u200d♀ (:woman_biking_medium-dark_skin_tone:)", + ), + ( + "🚴🏼\u200d♀️", + "🚴🏼\u200d♀️ (:woman_biking_medium-light_skin_tone:)", + ), + ( + "🚴🏼\u200d♀", + "🚴🏼\u200d♀ (:woman_biking_medium-light_skin_tone:)", + ), + ( + "🚴🏽\u200d♀️", + "🚴🏽\u200d♀️ (:woman_biking_medium_skin_tone:)", + ), + ( + "🚴🏽\u200d♀", + "🚴🏽\u200d♀ (:woman_biking_medium_skin_tone:)", + ), + ("👱\u200d♀️", "👱\u200d♀️ (:woman_blond_hair:)"), + ("👱\u200d♀", "👱\u200d♀ (:woman_blond_hair:)"), + ("⛹️\u200d♀️", "⛹️\u200d♀️ (:woman_bouncing_ball:)"), + ("⛹\u200d♀️", "⛹\u200d♀️ (:woman_bouncing_ball:)"), + ("⛹️\u200d♀", "⛹️\u200d♀ (:woman_bouncing_ball:)"), + ("⛹\u200d♀", "⛹\u200d♀ (:woman_bouncing_ball:)"), + ( + "⛹🏿\u200d♀️", + "⛹🏿\u200d♀️ (:woman_bouncing_ball_dark_skin_tone:)", + ), + ( + "⛹🏿\u200d♀", + "⛹🏿\u200d♀ (:woman_bouncing_ball_dark_skin_tone:)", + ), + ( + "⛹🏻\u200d♀️", + "⛹🏻\u200d♀️ (:woman_bouncing_ball_light_skin_tone:)", + ), + ( + "⛹🏻\u200d♀", + "⛹🏻\u200d♀ (:woman_bouncing_ball_light_skin_tone:)", + ), + ( + "⛹🏾\u200d♀️", + "⛹🏾\u200d♀️ (:woman_bouncing_ball_medium-dark_skin_tone:)", + ), + ( + "⛹🏾\u200d♀", + "⛹🏾\u200d♀ (:woman_bouncing_ball_medium-dark_skin_tone:)", + ), + ( + "⛹🏼\u200d♀️", + "⛹🏼\u200d♀️ (:woman_bouncing_ball_medium-light_skin_tone:)", + ), + ( + "⛹🏼\u200d♀", + "⛹🏼\u200d♀ (:woman_bouncing_ball_medium-light_skin_tone:)", + ), + ( + "⛹🏽\u200d♀️", + "⛹🏽\u200d♀️ (:woman_bouncing_ball_medium_skin_tone:)", + ), + ( + "⛹🏽\u200d♀", + "⛹🏽\u200d♀ (:woman_bouncing_ball_medium_skin_tone:)", + ), + ("🙇\u200d♀️", "🙇\u200d♀️ (:woman_bowing:)"), + ("🙇\u200d♀", "🙇\u200d♀ (:woman_bowing:)"), + ( + "🙇🏿\u200d♀️", + "🙇🏿\u200d♀️ (:woman_bowing_dark_skin_tone:)", + ), + ("🙇🏿\u200d♀", "🙇🏿\u200d♀ (:woman_bowing_dark_skin_tone:)"), + ( + "🙇🏻\u200d♀️", + "🙇🏻\u200d♀️ (:woman_bowing_light_skin_tone:)", + ), + ("🙇🏻\u200d♀", "🙇🏻\u200d♀ (:woman_bowing_light_skin_tone:)"), + ( + "🙇🏾\u200d♀️", + "🙇🏾\u200d♀️ (:woman_bowing_medium-dark_skin_tone:)", + ), + ( + "🙇🏾\u200d♀", + "🙇🏾\u200d♀ (:woman_bowing_medium-dark_skin_tone:)", + ), + ( + "🙇🏼\u200d♀️", + "🙇🏼\u200d♀️ (:woman_bowing_medium-light_skin_tone:)", + ), + ( + "🙇🏼\u200d♀", + "🙇🏼\u200d♀ (:woman_bowing_medium-light_skin_tone:)", + ), + ( + "🙇🏽\u200d♀️", + "🙇🏽\u200d♀️ (:woman_bowing_medium_skin_tone:)", + ), + ( + "🙇🏽\u200d♀", + "🙇🏽\u200d♀ (:woman_bowing_medium_skin_tone:)", + ), + ("🤸\u200d♀️", "🤸\u200d♀️ (:woman_cartwheeling:)"), + ("🤸\u200d♀", "🤸\u200d♀ (:woman_cartwheeling:)"), + ( + "🤸🏿\u200d♀️", + "🤸🏿\u200d♀️ (:woman_cartwheeling_dark_skin_tone:)", + ), + ( + "🤸🏿\u200d♀", + "🤸🏿\u200d♀ (:woman_cartwheeling_dark_skin_tone:)", + ), + ( + "🤸🏻\u200d♀️", + "🤸🏻\u200d♀️ (:woman_cartwheeling_light_skin_tone:)", + ), + ( + "🤸🏻\u200d♀", + "🤸🏻\u200d♀ (:woman_cartwheeling_light_skin_tone:)", + ), + ( + "🤸🏾\u200d♀️", + "🤸🏾\u200d♀️ (:woman_cartwheeling_medium-dark_skin_tone:)", + ), + ( + "🤸🏾\u200d♀", + "🤸🏾\u200d♀ (:woman_cartwheeling_medium-dark_skin_tone:)", + ), + ( + "🤸🏼\u200d♀️", + "🤸🏼\u200d♀️ (:woman_cartwheeling_medium-light_skin_tone:)", + ), + ( + "🤸🏼\u200d♀", + "🤸🏼\u200d♀ (:woman_cartwheeling_medium-light_skin_tone:)", + ), + ( + "🤸🏽\u200d♀️", + "🤸🏽\u200d♀️ (:woman_cartwheeling_medium_skin_tone:)", + ), + ( + "🤸🏽\u200d♀", + "🤸🏽\u200d♀ (:woman_cartwheeling_medium_skin_tone:)", + ), + ("🧗\u200d♀️", "🧗\u200d♀️ (:woman_climbing:)"), + ("🧗\u200d♀", "🧗\u200d♀ (:woman_climbing:)"), + ( + "🧗🏿\u200d♀️", + "🧗🏿\u200d♀️ (:woman_climbing_dark_skin_tone:)", + ), + ( + "🧗🏿\u200d♀", + "🧗🏿\u200d♀ (:woman_climbing_dark_skin_tone:)", + ), + ( + "🧗🏻\u200d♀️", + "🧗🏻\u200d♀️ (:woman_climbing_light_skin_tone:)", + ), + ( + "🧗🏻\u200d♀", + "🧗🏻\u200d♀ (:woman_climbing_light_skin_tone:)", + ), + ( + "🧗🏾\u200d♀️", + "🧗🏾\u200d♀️ (:woman_climbing_medium-dark_skin_tone:)", + ), + ( + "🧗🏾\u200d♀", + "🧗🏾\u200d♀ (:woman_climbing_medium-dark_skin_tone:)", + ), + ( + "🧗🏼\u200d♀️", + "🧗🏼\u200d♀️ (:woman_climbing_medium-light_skin_tone:)", + ), + ( + "🧗🏼\u200d♀", + "🧗🏼\u200d♀ (:woman_climbing_medium-light_skin_tone:)", + ), + ( + "🧗🏽\u200d♀️", + "🧗🏽\u200d♀️ (:woman_climbing_medium_skin_tone:)", + ), + ( + "🧗🏽\u200d♀", + "🧗🏽\u200d♀ (:woman_climbing_medium_skin_tone:)", + ), + ("👷\u200d♀️", "👷\u200d♀️ (:woman_construction_worker:)"), + ("👷\u200d♀", "👷\u200d♀ (:woman_construction_worker:)"), + ( + "👷🏿\u200d♀️", + "👷🏿\u200d♀️ (:woman_construction_worker_dark_skin_tone:)", + ), + ( + "👷🏿\u200d♀", + "👷🏿\u200d♀ (:woman_construction_worker_dark_skin_tone:)", + ), + ( + "👷🏻\u200d♀️", + "👷🏻\u200d♀️ (:woman_construction_worker_light_skin_tone:)", + ), + ( + "👷🏻\u200d♀", + "👷🏻\u200d♀ (:woman_construction_worker_light_skin_tone:)", + ), + ( + "👷🏾\u200d♀️", + "👷🏾\u200d♀️ (:woman_construction_worker_medium-dark_skin_tone:)", + ), + ( + "👷🏾\u200d♀", + "👷🏾\u200d♀ (:woman_construction_worker_medium-dark_skin_tone:)", + ), + ( + "👷🏼\u200d♀️", + "👷🏼\u200d♀️ (:woman_construction_worker_medium-light_skin_tone:)", + ), + ( + "👷🏼\u200d♀", + "👷🏼\u200d♀ (:woman_construction_worker_medium-light_skin_tone:)", + ), + ( + "👷🏽\u200d♀️", + "👷🏽\u200d♀️ (:woman_construction_worker_medium_skin_tone:)", + ), + ( + "👷🏽\u200d♀", + "👷🏽\u200d♀ (:woman_construction_worker_medium_skin_tone:)", + ), + ("👩\u200d🍳", "👩\u200d🍳 (:woman_cook:)"), + ("👩🏿\u200d🍳", "👩🏿\u200d🍳 (:woman_cook_dark_skin_tone:)"), + ("👩🏻\u200d🍳", "👩🏻\u200d🍳 (:woman_cook_light_skin_tone:)"), + ( + "👩🏾\u200d🍳", + "👩🏾\u200d🍳 (:woman_cook_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🍳", + "👩🏼\u200d🍳 (:woman_cook_medium-light_skin_tone:)", + ), + ("👩🏽\u200d🍳", "👩🏽\u200d🍳 (:woman_cook_medium_skin_tone:)"), + ("👩\u200d🦱", "👩\u200d🦱 (:woman_curly_hair:)"), + ("💃", "💃 (:woman_dancing:)"), + ("💃🏿", "💃🏿 (:woman_dancing_dark_skin_tone:)"), + ("💃🏻", "💃🏻 (:woman_dancing_light_skin_tone:)"), + ("💃🏾", "💃🏾 (:woman_dancing_medium-dark_skin_tone:)"), + ("💃🏼", "💃🏼 (:woman_dancing_medium-light_skin_tone:)"), + ("💃🏽", "💃🏽 (:woman_dancing_medium_skin_tone:)"), + ("👩🏿", "👩🏿 (:woman_dark_skin_tone:)"), + ("👩🏿\u200d🦲", "👩🏿\u200d🦲 (:woman_dark_skin_tone_bald:)"), + ("🧔🏿\u200d♀️", "🧔🏿\u200d♀️ (:woman_dark_skin_tone_beard:)"), + ("🧔🏿\u200d♀", "🧔🏿\u200d♀ (:woman_dark_skin_tone_beard:)"), + ( + "👱🏿\u200d♀️", + "👱🏿\u200d♀️ (:woman_dark_skin_tone_blond_hair:)", + ), + ( + "👱🏿\u200d♀", + "👱🏿\u200d♀ (:woman_dark_skin_tone_blond_hair:)", + ), + ( + "👩🏿\u200d🦱", + "👩🏿\u200d🦱 (:woman_dark_skin_tone_curly_hair:)", + ), + ( + "👩🏿\u200d🦰", + "👩🏿\u200d🦰 (:woman_dark_skin_tone_red_hair:)", + ), + ( + "👩🏿\u200d🦳", + "👩🏿\u200d🦳 (:woman_dark_skin_tone_white_hair:)", + ), + ("🕵️\u200d♀️", "🕵️\u200d♀️ (:woman_detective:)"), + ("🕵\u200d♀️", "🕵\u200d♀️ (:woman_detective:)"), + ("🕵️\u200d♀", "🕵️\u200d♀ (:woman_detective:)"), + ("🕵\u200d♀", "🕵\u200d♀ (:woman_detective:)"), + ( + "🕵🏿\u200d♀️", + "🕵🏿\u200d♀️ (:woman_detective_dark_skin_tone:)", + ), + ( + "🕵🏿\u200d♀", + "🕵🏿\u200d♀ (:woman_detective_dark_skin_tone:)", + ), + ( + "🕵🏻\u200d♀️", + "🕵🏻\u200d♀️ (:woman_detective_light_skin_tone:)", + ), + ( + "🕵🏻\u200d♀", + "🕵🏻\u200d♀ (:woman_detective_light_skin_tone:)", + ), + ( + "🕵🏾\u200d♀️", + "🕵🏾\u200d♀️ (:woman_detective_medium-dark_skin_tone:)", + ), + ( + "🕵🏾\u200d♀", + "🕵🏾\u200d♀ (:woman_detective_medium-dark_skin_tone:)", + ), + ( + "🕵🏼\u200d♀️", + "🕵🏼\u200d♀️ (:woman_detective_medium-light_skin_tone:)", + ), + ( + "🕵🏼\u200d♀", + "🕵🏼\u200d♀ (:woman_detective_medium-light_skin_tone:)", + ), + ( + "🕵🏽\u200d♀️", + "🕵🏽\u200d♀️ (:woman_detective_medium_skin_tone:)", + ), + ( + "🕵🏽\u200d♀", + "🕵🏽\u200d♀ (:woman_detective_medium_skin_tone:)", + ), + ("🧝\u200d♀️", "🧝\u200d♀️ (:woman_elf:)"), + ("🧝\u200d♀", "🧝\u200d♀ (:woman_elf:)"), + ("🧝🏿\u200d♀️", "🧝🏿\u200d♀️ (:woman_elf_dark_skin_tone:)"), + ("🧝🏿\u200d♀", "🧝🏿\u200d♀ (:woman_elf_dark_skin_tone:)"), + ("🧝🏻\u200d♀️", "🧝🏻\u200d♀️ (:woman_elf_light_skin_tone:)"), + ("🧝🏻\u200d♀", "🧝🏻\u200d♀ (:woman_elf_light_skin_tone:)"), + ( + "🧝🏾\u200d♀️", + "🧝🏾\u200d♀️ (:woman_elf_medium-dark_skin_tone:)", + ), + ( + "🧝🏾\u200d♀", + "🧝🏾\u200d♀ (:woman_elf_medium-dark_skin_tone:)", + ), + ( + "🧝🏼\u200d♀️", + "🧝🏼\u200d♀️ (:woman_elf_medium-light_skin_tone:)", + ), + ( + "🧝🏼\u200d♀", + "🧝🏼\u200d♀ (:woman_elf_medium-light_skin_tone:)", + ), + ("🧝🏽\u200d♀️", "🧝🏽\u200d♀️ (:woman_elf_medium_skin_tone:)"), + ("🧝🏽\u200d♀", "🧝🏽\u200d♀ (:woman_elf_medium_skin_tone:)"), + ("🤦\u200d♀️", "🤦\u200d♀️ (:woman_facepalming:)"), + ("🤦\u200d♀", "🤦\u200d♀ (:woman_facepalming:)"), + ( + "🤦🏿\u200d♀️", + "🤦🏿\u200d♀️ (:woman_facepalming_dark_skin_tone:)", + ), + ( + "🤦🏿\u200d♀", + "🤦🏿\u200d♀ (:woman_facepalming_dark_skin_tone:)", + ), + ( + "🤦🏻\u200d♀️", + "🤦🏻\u200d♀️ (:woman_facepalming_light_skin_tone:)", + ), + ( + "🤦🏻\u200d♀", + "🤦🏻\u200d♀ (:woman_facepalming_light_skin_tone:)", + ), + ( + "🤦🏾\u200d♀️", + "🤦🏾\u200d♀️ (:woman_facepalming_medium-dark_skin_tone:)", + ), + ( + "🤦🏾\u200d♀", + "🤦🏾\u200d♀ (:woman_facepalming_medium-dark_skin_tone:)", + ), + ( + "🤦🏼\u200d♀️", + "🤦🏼\u200d♀️ (:woman_facepalming_medium-light_skin_tone:)", + ), + ( + "🤦🏼\u200d♀", + "🤦🏼\u200d♀ (:woman_facepalming_medium-light_skin_tone:)", + ), + ( + "🤦🏽\u200d♀️", + "🤦🏽\u200d♀️ (:woman_facepalming_medium_skin_tone:)", + ), + ( + "🤦🏽\u200d♀", + "🤦🏽\u200d♀ (:woman_facepalming_medium_skin_tone:)", + ), + ("👩\u200d🏭", "👩\u200d🏭 (:woman_factory_worker:)"), + ( + "👩🏿\u200d🏭", + "👩🏿\u200d🏭 (:woman_factory_worker_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🏭", + "👩🏻\u200d🏭 (:woman_factory_worker_light_skin_tone:)", + ), + ( + "👩🏾\u200d🏭", + "👩🏾\u200d🏭 (:woman_factory_worker_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🏭", + "👩🏼\u200d🏭 (:woman_factory_worker_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🏭", + "👩🏽\u200d🏭 (:woman_factory_worker_medium_skin_tone:)", + ), + ("🧚\u200d♀️", "🧚\u200d♀️ (:woman_fairy:)"), + ("🧚\u200d♀", "🧚\u200d♀ (:woman_fairy:)"), + ("🧚🏿\u200d♀️", "🧚🏿\u200d♀️ (:woman_fairy_dark_skin_tone:)"), + ("🧚🏿\u200d♀", "🧚🏿\u200d♀ (:woman_fairy_dark_skin_tone:)"), + ( + "🧚🏻\u200d♀️", + "🧚🏻\u200d♀️ (:woman_fairy_light_skin_tone:)", + ), + ("🧚🏻\u200d♀", "🧚🏻\u200d♀ (:woman_fairy_light_skin_tone:)"), + ( + "🧚🏾\u200d♀️", + "🧚🏾\u200d♀️ (:woman_fairy_medium-dark_skin_tone:)", + ), + ( + "🧚🏾\u200d♀", + "🧚🏾\u200d♀ (:woman_fairy_medium-dark_skin_tone:)", + ), + ( + "🧚🏼\u200d♀️", + "🧚🏼\u200d♀️ (:woman_fairy_medium-light_skin_tone:)", + ), + ( + "🧚🏼\u200d♀", + "🧚🏼\u200d♀ (:woman_fairy_medium-light_skin_tone:)", + ), + ( + "🧚🏽\u200d♀️", + "🧚🏽\u200d♀️ (:woman_fairy_medium_skin_tone:)", + ), + ("🧚🏽\u200d♀", "🧚🏽\u200d♀ (:woman_fairy_medium_skin_tone:)"), + ("👩\u200d🌾", "👩\u200d🌾 (:woman_farmer:)"), + ("👩🏿\u200d🌾", "👩🏿\u200d🌾 (:woman_farmer_dark_skin_tone:)"), + ("👩🏻\u200d🌾", "👩🏻\u200d🌾 (:woman_farmer_light_skin_tone:)"), + ( + "👩🏾\u200d🌾", + "👩🏾\u200d🌾 (:woman_farmer_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🌾", + "👩🏼\u200d🌾 (:woman_farmer_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🌾", + "👩🏽\u200d🌾 (:woman_farmer_medium_skin_tone:)", + ), + ("👩\u200d🍼", "👩\u200d🍼 (:woman_feeding_baby:)"), + ( + "👩🏿\u200d🍼", + "👩🏿\u200d🍼 (:woman_feeding_baby_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🍼", + "👩🏻\u200d🍼 (:woman_feeding_baby_light_skin_tone:)", + ), + ( + "👩🏾\u200d🍼", + "👩🏾\u200d🍼 (:woman_feeding_baby_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🍼", + "👩🏼\u200d🍼 (:woman_feeding_baby_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🍼", + "👩🏽\u200d🍼 (:woman_feeding_baby_medium_skin_tone:)", + ), + ("👩\u200d🚒", "👩\u200d🚒 (:woman_firefighter:)"), + ( + "👩🏿\u200d🚒", + "👩🏿\u200d🚒 (:woman_firefighter_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🚒", + "👩🏻\u200d🚒 (:woman_firefighter_light_skin_tone:)", + ), + ( + "👩🏾\u200d🚒", + "👩🏾\u200d🚒 (:woman_firefighter_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🚒", + "👩🏼\u200d🚒 (:woman_firefighter_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🚒", + "👩🏽\u200d🚒 (:woman_firefighter_medium_skin_tone:)", + ), + ("🙍\u200d♀️", "🙍\u200d♀️ (:woman_frowning:)"), + ("🙍\u200d♀", "🙍\u200d♀ (:woman_frowning:)"), + ( + "🙍🏿\u200d♀️", + "🙍🏿\u200d♀️ (:woman_frowning_dark_skin_tone:)", + ), + ( + "🙍🏿\u200d♀", + "🙍🏿\u200d♀ (:woman_frowning_dark_skin_tone:)", + ), + ( + "🙍🏻\u200d♀️", + "🙍🏻\u200d♀️ (:woman_frowning_light_skin_tone:)", + ), + ( + "🙍🏻\u200d♀", + "🙍🏻\u200d♀ (:woman_frowning_light_skin_tone:)", + ), + ( + "🙍🏾\u200d♀️", + "🙍🏾\u200d♀️ (:woman_frowning_medium-dark_skin_tone:)", + ), + ( + "🙍🏾\u200d♀", + "🙍🏾\u200d♀ (:woman_frowning_medium-dark_skin_tone:)", + ), + ( + "🙍🏼\u200d♀️", + "🙍🏼\u200d♀️ (:woman_frowning_medium-light_skin_tone:)", + ), + ( + "🙍🏼\u200d♀", + "🙍🏼\u200d♀ (:woman_frowning_medium-light_skin_tone:)", + ), + ( + "🙍🏽\u200d♀️", + "🙍🏽\u200d♀️ (:woman_frowning_medium_skin_tone:)", + ), + ( + "🙍🏽\u200d♀", + "🙍🏽\u200d♀ (:woman_frowning_medium_skin_tone:)", + ), + ("🧞\u200d♀️", "🧞\u200d♀️ (:woman_genie:)"), + ("🧞\u200d♀", "🧞\u200d♀ (:woman_genie:)"), + ("🙅\u200d♀️", "🙅\u200d♀️ (:woman_gesturing_NO:)"), + ("🙅\u200d♀", "🙅\u200d♀ (:woman_gesturing_NO:)"), + ( + "🙅🏿\u200d♀️", + "🙅🏿\u200d♀️ (:woman_gesturing_NO_dark_skin_tone:)", + ), + ( + "🙅🏿\u200d♀", + "🙅🏿\u200d♀ (:woman_gesturing_NO_dark_skin_tone:)", + ), + ( + "🙅🏻\u200d♀️", + "🙅🏻\u200d♀️ (:woman_gesturing_NO_light_skin_tone:)", + ), + ( + "🙅🏻\u200d♀", + "🙅🏻\u200d♀ (:woman_gesturing_NO_light_skin_tone:)", + ), + ( + "🙅🏾\u200d♀️", + "🙅🏾\u200d♀️ (:woman_gesturing_NO_medium-dark_skin_tone:)", + ), + ( + "🙅🏾\u200d♀", + "🙅🏾\u200d♀ (:woman_gesturing_NO_medium-dark_skin_tone:)", + ), + ( + "🙅🏼\u200d♀️", + "🙅🏼\u200d♀️ (:woman_gesturing_NO_medium-light_skin_tone:)", + ), + ( + "🙅🏼\u200d♀", + "🙅🏼\u200d♀ (:woman_gesturing_NO_medium-light_skin_tone:)", + ), + ( + "🙅🏽\u200d♀️", + "🙅🏽\u200d♀️ (:woman_gesturing_NO_medium_skin_tone:)", + ), + ( + "🙅🏽\u200d♀", + "🙅🏽\u200d♀ (:woman_gesturing_NO_medium_skin_tone:)", + ), + ("🙆\u200d♀️", "🙆\u200d♀️ (:woman_gesturing_OK:)"), + ("🙆\u200d♀", "🙆\u200d♀ (:woman_gesturing_OK:)"), + ( + "🙆🏿\u200d♀️", + "🙆🏿\u200d♀️ (:woman_gesturing_OK_dark_skin_tone:)", + ), + ( + "🙆🏿\u200d♀", + "🙆🏿\u200d♀ (:woman_gesturing_OK_dark_skin_tone:)", + ), + ( + "🙆🏻\u200d♀️", + "🙆🏻\u200d♀️ (:woman_gesturing_OK_light_skin_tone:)", + ), + ( + "🙆🏻\u200d♀", + "🙆🏻\u200d♀ (:woman_gesturing_OK_light_skin_tone:)", + ), + ( + "🙆🏾\u200d♀️", + "🙆🏾\u200d♀️ (:woman_gesturing_OK_medium-dark_skin_tone:)", + ), + ( + "🙆🏾\u200d♀", + "🙆🏾\u200d♀ (:woman_gesturing_OK_medium-dark_skin_tone:)", + ), + ( + "🙆🏼\u200d♀️", + "🙆🏼\u200d♀️ (:woman_gesturing_OK_medium-light_skin_tone:)", + ), + ( + "🙆🏼\u200d♀", + "🙆🏼\u200d♀ (:woman_gesturing_OK_medium-light_skin_tone:)", + ), + ( + "🙆🏽\u200d♀️", + "🙆🏽\u200d♀️ (:woman_gesturing_OK_medium_skin_tone:)", + ), + ( + "🙆🏽\u200d♀", + "🙆🏽\u200d♀ (:woman_gesturing_OK_medium_skin_tone:)", + ), + ("💇\u200d♀️", "💇\u200d♀️ (:woman_getting_haircut:)"), + ("💇\u200d♀", "💇\u200d♀ (:woman_getting_haircut:)"), + ( + "💇🏿\u200d♀️", + "💇🏿\u200d♀️ (:woman_getting_haircut_dark_skin_tone:)", + ), + ( + "💇🏿\u200d♀", + "💇🏿\u200d♀ (:woman_getting_haircut_dark_skin_tone:)", + ), + ( + "💇🏻\u200d♀️", + "💇🏻\u200d♀️ (:woman_getting_haircut_light_skin_tone:)", + ), + ( + "💇🏻\u200d♀", + "💇🏻\u200d♀ (:woman_getting_haircut_light_skin_tone:)", + ), + ( + "💇🏾\u200d♀️", + "💇🏾\u200d♀️ (:woman_getting_haircut_medium-dark_skin_tone:)", + ), + ( + "💇🏾\u200d♀", + "💇🏾\u200d♀ (:woman_getting_haircut_medium-dark_skin_tone:)", + ), + ( + "💇🏼\u200d♀️", + "💇🏼\u200d♀️ (:woman_getting_haircut_medium-light_skin_tone:)", + ), + ( + "💇🏼\u200d♀", + "💇🏼\u200d♀ (:woman_getting_haircut_medium-light_skin_tone:)", + ), + ( + "💇🏽\u200d♀️", + "💇🏽\u200d♀️ (:woman_getting_haircut_medium_skin_tone:)", + ), + ( + "💇🏽\u200d♀", + "💇🏽\u200d♀ (:woman_getting_haircut_medium_skin_tone:)", + ), + ("💆\u200d♀️", "💆\u200d♀️ (:woman_getting_massage:)"), + ("💆\u200d♀", "💆\u200d♀ (:woman_getting_massage:)"), + ( + "💆🏿\u200d♀️", + "💆🏿\u200d♀️ (:woman_getting_massage_dark_skin_tone:)", + ), + ( + "💆🏿\u200d♀", + "💆🏿\u200d♀ (:woman_getting_massage_dark_skin_tone:)", + ), + ( + "💆🏻\u200d♀️", + "💆🏻\u200d♀️ (:woman_getting_massage_light_skin_tone:)", + ), + ( + "💆🏻\u200d♀", + "💆🏻\u200d♀ (:woman_getting_massage_light_skin_tone:)", + ), + ( + "💆🏾\u200d♀️", + "💆🏾\u200d♀️ (:woman_getting_massage_medium-dark_skin_tone:)", + ), + ( + "💆🏾\u200d♀", + "💆🏾\u200d♀ (:woman_getting_massage_medium-dark_skin_tone:)", + ), + ( + "💆🏼\u200d♀️", + "💆🏼\u200d♀️ (:woman_getting_massage_medium-light_skin_tone:)", + ), + ( + "💆🏼\u200d♀", + "💆🏼\u200d♀ (:woman_getting_massage_medium-light_skin_tone:)", + ), + ( + "💆🏽\u200d♀️", + "💆🏽\u200d♀️ (:woman_getting_massage_medium_skin_tone:)", + ), + ( + "💆🏽\u200d♀", + "💆🏽\u200d♀ (:woman_getting_massage_medium_skin_tone:)", + ), + ("🏌️\u200d♀️", "🏌️\u200d♀️ (:woman_golfing:)"), + ("🏌\u200d♀️", "🏌\u200d♀️ (:woman_golfing:)"), + ("🏌️\u200d♀", "🏌️\u200d♀ (:woman_golfing:)"), + ("🏌\u200d♀", "🏌\u200d♀ (:woman_golfing:)"), + ( + "🏌🏿\u200d♀️", + "🏌🏿\u200d♀️ (:woman_golfing_dark_skin_tone:)", + ), + ("🏌🏿\u200d♀", "🏌🏿\u200d♀ (:woman_golfing_dark_skin_tone:)"), + ( + "🏌🏻\u200d♀️", + "🏌🏻\u200d♀️ (:woman_golfing_light_skin_tone:)", + ), + ( + "🏌🏻\u200d♀", + "🏌🏻\u200d♀ (:woman_golfing_light_skin_tone:)", + ), + ( + "🏌🏾\u200d♀️", + "🏌🏾\u200d♀️ (:woman_golfing_medium-dark_skin_tone:)", + ), + ( + "🏌🏾\u200d♀", + "🏌🏾\u200d♀ (:woman_golfing_medium-dark_skin_tone:)", + ), + ( + "🏌🏼\u200d♀️", + "🏌🏼\u200d♀️ (:woman_golfing_medium-light_skin_tone:)", + ), + ( + "🏌🏼\u200d♀", + "🏌🏼\u200d♀ (:woman_golfing_medium-light_skin_tone:)", + ), + ( + "🏌🏽\u200d♀️", + "🏌🏽\u200d♀️ (:woman_golfing_medium_skin_tone:)", + ), + ( + "🏌🏽\u200d♀", + "🏌🏽\u200d♀ (:woman_golfing_medium_skin_tone:)", + ), + ("💂\u200d♀️", "💂\u200d♀️ (:woman_guard:)"), + ("💂\u200d♀", "💂\u200d♀ (:woman_guard:)"), + ("💂🏿\u200d♀️", "💂🏿\u200d♀️ (:woman_guard_dark_skin_tone:)"), + ("💂🏿\u200d♀", "💂🏿\u200d♀ (:woman_guard_dark_skin_tone:)"), + ( + "💂🏻\u200d♀️", + "💂🏻\u200d♀️ (:woman_guard_light_skin_tone:)", + ), + ("💂🏻\u200d♀", "💂🏻\u200d♀ (:woman_guard_light_skin_tone:)"), + ( + "💂🏾\u200d♀️", + "💂🏾\u200d♀️ (:woman_guard_medium-dark_skin_tone:)", + ), + ( + "💂🏾\u200d♀", + "💂🏾\u200d♀ (:woman_guard_medium-dark_skin_tone:)", + ), + ( + "💂🏼\u200d♀️", + "💂🏼\u200d♀️ (:woman_guard_medium-light_skin_tone:)", + ), + ( + "💂🏼\u200d♀", + "💂🏼\u200d♀ (:woman_guard_medium-light_skin_tone:)", + ), + ( + "💂🏽\u200d♀️", + "💂🏽\u200d♀️ (:woman_guard_medium_skin_tone:)", + ), + ("💂🏽\u200d♀", "💂🏽\u200d♀ (:woman_guard_medium_skin_tone:)"), + ("👩\u200d⚕️", "👩\u200d⚕️ (:woman_health_worker:)"), + ("👩\u200d⚕", "👩\u200d⚕ (:woman_health_worker:)"), + ( + "👩🏿\u200d⚕️", + "👩🏿\u200d⚕️ (:woman_health_worker_dark_skin_tone:)", + ), + ( + "👩🏿\u200d⚕", + "👩🏿\u200d⚕ (:woman_health_worker_dark_skin_tone:)", + ), + ( + "👩🏻\u200d⚕️", + "👩🏻\u200d⚕️ (:woman_health_worker_light_skin_tone:)", + ), + ( + "👩🏻\u200d⚕", + "👩🏻\u200d⚕ (:woman_health_worker_light_skin_tone:)", + ), + ( + "👩🏾\u200d⚕️", + "👩🏾\u200d⚕️ (:woman_health_worker_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d⚕", + "👩🏾\u200d⚕ (:woman_health_worker_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d⚕️", + "👩🏼\u200d⚕️ (:woman_health_worker_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d⚕", + "👩🏼\u200d⚕ (:woman_health_worker_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d⚕️", + "👩🏽\u200d⚕️ (:woman_health_worker_medium_skin_tone:)", + ), + ( + "👩🏽\u200d⚕", + "👩🏽\u200d⚕ (:woman_health_worker_medium_skin_tone:)", + ), + ("🧘\u200d♀️", "🧘\u200d♀️ (:woman_in_lotus_position:)"), + ("🧘\u200d♀", "🧘\u200d♀ (:woman_in_lotus_position:)"), + ( + "🧘🏿\u200d♀️", + "🧘🏿\u200d♀️ (:woman_in_lotus_position_dark_skin_tone:)", + ), + ( + "🧘🏿\u200d♀", + "🧘🏿\u200d♀ (:woman_in_lotus_position_dark_skin_tone:)", + ), + ( + "🧘🏻\u200d♀️", + "🧘🏻\u200d♀️ (:woman_in_lotus_position_light_skin_tone:)", + ), + ( + "🧘🏻\u200d♀", + "🧘🏻\u200d♀ (:woman_in_lotus_position_light_skin_tone:)", + ), + ( + "🧘🏾\u200d♀️", + "🧘🏾\u200d♀️ (:woman_in_lotus_position_medium-dark_skin_tone:)", + ), + ( + "🧘🏾\u200d♀", + "🧘🏾\u200d♀ (:woman_in_lotus_position_medium-dark_skin_tone:)", + ), + ( + "🧘🏼\u200d♀️", + "🧘🏼\u200d♀️ (:woman_in_lotus_position_medium-light_skin_tone:)", + ), + ( + "🧘🏼\u200d♀", + "🧘🏼\u200d♀ (:woman_in_lotus_position_medium-light_skin_tone:)", + ), + ( + "🧘🏽\u200d♀️", + "🧘🏽\u200d♀️ (:woman_in_lotus_position_medium_skin_tone:)", + ), + ( + "🧘🏽\u200d♀", + "🧘🏽\u200d♀ (:woman_in_lotus_position_medium_skin_tone:)", + ), + ("👩\u200d🦽", "👩\u200d🦽 (:woman_in_manual_wheelchair:)"), + ( + "👩🏿\u200d🦽", + "👩🏿\u200d🦽 (:woman_in_manual_wheelchair_dark_skin_tone:)", + ), + ( + "👩\u200d🦽\u200d➡️", + "👩\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right:)", + ), + ( + "👩\u200d🦽\u200d➡", + "👩\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right:)", + ), + ( + "👩🏿\u200d🦽\u200d➡️", + "👩🏿\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👩🏿\u200d🦽\u200d➡", + "👩🏿\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🦽\u200d➡️", + "👩🏻\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👩🏻\u200d🦽\u200d➡", + "👩🏻\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦽\u200d➡️", + "👩🏾\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d🦽\u200d➡", + "👩🏾\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦽\u200d➡️", + "👩🏼\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d🦽\u200d➡", + "👩🏼\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦽\u200d➡️", + "👩🏽\u200d🦽\u200d➡️ (:woman_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👩🏽\u200d🦽\u200d➡", + "👩🏽\u200d🦽\u200d➡ (:woman_in_manual_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👩🏻\u200d🦽", + "👩🏻\u200d🦽 (:woman_in_manual_wheelchair_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦽", + "👩🏾\u200d🦽 (:woman_in_manual_wheelchair_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦽", + "👩🏼\u200d🦽 (:woman_in_manual_wheelchair_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦽", + "👩🏽\u200d🦽 (:woman_in_manual_wheelchair_medium_skin_tone:)", + ), + ("👩\u200d🦼", "👩\u200d🦼 (:woman_in_motorized_wheelchair:)"), + ( + "👩🏿\u200d🦼", + "👩🏿\u200d🦼 (:woman_in_motorized_wheelchair_dark_skin_tone:)", + ), + ( + "👩\u200d🦼\u200d➡️", + "👩\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right:)", + ), + ( + "👩\u200d🦼\u200d➡", + "👩\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right:)", + ), + ( + "👩🏿\u200d🦼\u200d➡️", + "👩🏿\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👩🏿\u200d🦼\u200d➡", + "👩🏿\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🦼\u200d➡️", + "👩🏻\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👩🏻\u200d🦼\u200d➡", + "👩🏻\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦼\u200d➡️", + "👩🏾\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d🦼\u200d➡", + "👩🏾\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦼\u200d➡️", + "👩🏼\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d🦼\u200d➡", + "👩🏼\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦼\u200d➡️", + "👩🏽\u200d🦼\u200d➡️ (:woman_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👩🏽\u200d🦼\u200d➡", + "👩🏽\u200d🦼\u200d➡ (:woman_in_motorized_wheelchair_facing_right_medium_skin_tone:)", + ), + ( + "👩🏻\u200d🦼", + "👩🏻\u200d🦼 (:woman_in_motorized_wheelchair_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦼", + "👩🏾\u200d🦼 (:woman_in_motorized_wheelchair_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦼", + "👩🏼\u200d🦼 (:woman_in_motorized_wheelchair_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦼", + "👩🏽\u200d🦼 (:woman_in_motorized_wheelchair_medium_skin_tone:)", + ), + ("🧖\u200d♀️", "🧖\u200d♀️ (:woman_in_steamy_room:)"), + ("🧖\u200d♀", "🧖\u200d♀ (:woman_in_steamy_room:)"), + ( + "🧖🏿\u200d♀️", + "🧖🏿\u200d♀️ (:woman_in_steamy_room_dark_skin_tone:)", + ), + ( + "🧖🏿\u200d♀", + "🧖🏿\u200d♀ (:woman_in_steamy_room_dark_skin_tone:)", + ), + ( + "🧖🏻\u200d♀️", + "🧖🏻\u200d♀️ (:woman_in_steamy_room_light_skin_tone:)", + ), + ( + "🧖🏻\u200d♀", + "🧖🏻\u200d♀ (:woman_in_steamy_room_light_skin_tone:)", + ), + ( + "🧖🏾\u200d♀️", + "🧖🏾\u200d♀️ (:woman_in_steamy_room_medium-dark_skin_tone:)", + ), + ( + "🧖🏾\u200d♀", + "🧖🏾\u200d♀ (:woman_in_steamy_room_medium-dark_skin_tone:)", + ), + ( + "🧖🏼\u200d♀️", + "🧖🏼\u200d♀️ (:woman_in_steamy_room_medium-light_skin_tone:)", + ), + ( + "🧖🏼\u200d♀", + "🧖🏼\u200d♀ (:woman_in_steamy_room_medium-light_skin_tone:)", + ), + ( + "🧖🏽\u200d♀️", + "🧖🏽\u200d♀️ (:woman_in_steamy_room_medium_skin_tone:)", + ), + ( + "🧖🏽\u200d♀", + "🧖🏽\u200d♀ (:woman_in_steamy_room_medium_skin_tone:)", + ), + ("🤵\u200d♀️", "🤵\u200d♀️ (:woman_in_tuxedo:)"), + ("🤵\u200d♀", "🤵\u200d♀ (:woman_in_tuxedo:)"), + ( + "🤵🏿\u200d♀️", + "🤵🏿\u200d♀️ (:woman_in_tuxedo_dark_skin_tone:)", + ), + ( + "🤵🏿\u200d♀", + "🤵🏿\u200d♀ (:woman_in_tuxedo_dark_skin_tone:)", + ), + ( + "🤵🏻\u200d♀️", + "🤵🏻\u200d♀️ (:woman_in_tuxedo_light_skin_tone:)", + ), + ( + "🤵🏻\u200d♀", + "🤵🏻\u200d♀ (:woman_in_tuxedo_light_skin_tone:)", + ), + ( + "🤵🏾\u200d♀️", + "🤵🏾\u200d♀️ (:woman_in_tuxedo_medium-dark_skin_tone:)", + ), + ( + "🤵🏾\u200d♀", + "🤵🏾\u200d♀ (:woman_in_tuxedo_medium-dark_skin_tone:)", + ), + ( + "🤵🏼\u200d♀️", + "🤵🏼\u200d♀️ (:woman_in_tuxedo_medium-light_skin_tone:)", + ), + ( + "🤵🏼\u200d♀", + "🤵🏼\u200d♀ (:woman_in_tuxedo_medium-light_skin_tone:)", + ), + ( + "🤵🏽\u200d♀️", + "🤵🏽\u200d♀️ (:woman_in_tuxedo_medium_skin_tone:)", + ), + ( + "🤵🏽\u200d♀", + "🤵🏽\u200d♀ (:woman_in_tuxedo_medium_skin_tone:)", + ), + ("👩\u200d⚖️", "👩\u200d⚖️ (:woman_judge:)"), + ("👩\u200d⚖", "👩\u200d⚖ (:woman_judge:)"), + ("👩🏿\u200d⚖️", "👩🏿\u200d⚖️ (:woman_judge_dark_skin_tone:)"), + ("👩🏿\u200d⚖", "👩🏿\u200d⚖ (:woman_judge_dark_skin_tone:)"), + ( + "👩🏻\u200d⚖️", + "👩🏻\u200d⚖️ (:woman_judge_light_skin_tone:)", + ), + ("👩🏻\u200d⚖", "👩🏻\u200d⚖ (:woman_judge_light_skin_tone:)"), + ( + "👩🏾\u200d⚖️", + "👩🏾\u200d⚖️ (:woman_judge_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d⚖", + "👩🏾\u200d⚖ (:woman_judge_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d⚖️", + "👩🏼\u200d⚖️ (:woman_judge_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d⚖", + "👩🏼\u200d⚖ (:woman_judge_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d⚖️", + "👩🏽\u200d⚖️ (:woman_judge_medium_skin_tone:)", + ), + ("👩🏽\u200d⚖", "👩🏽\u200d⚖ (:woman_judge_medium_skin_tone:)"), + ("🤹\u200d♀️", "🤹\u200d♀️ (:woman_juggling:)"), + ("🤹\u200d♀", "🤹\u200d♀ (:woman_juggling:)"), + ( + "🤹🏿\u200d♀️", + "🤹🏿\u200d♀️ (:woman_juggling_dark_skin_tone:)", + ), + ( + "🤹🏿\u200d♀", + "🤹🏿\u200d♀ (:woman_juggling_dark_skin_tone:)", + ), + ( + "🤹🏻\u200d♀️", + "🤹🏻\u200d♀️ (:woman_juggling_light_skin_tone:)", + ), + ( + "🤹🏻\u200d♀", + "🤹🏻\u200d♀ (:woman_juggling_light_skin_tone:)", + ), + ( + "🤹🏾\u200d♀️", + "🤹🏾\u200d♀️ (:woman_juggling_medium-dark_skin_tone:)", + ), + ( + "🤹🏾\u200d♀", + "🤹🏾\u200d♀ (:woman_juggling_medium-dark_skin_tone:)", + ), + ( + "🤹🏼\u200d♀️", + "🤹🏼\u200d♀️ (:woman_juggling_medium-light_skin_tone:)", + ), + ( + "🤹🏼\u200d♀", + "🤹🏼\u200d♀ (:woman_juggling_medium-light_skin_tone:)", + ), + ( + "🤹🏽\u200d♀️", + "🤹🏽\u200d♀️ (:woman_juggling_medium_skin_tone:)", + ), + ( + "🤹🏽\u200d♀", + "🤹🏽\u200d♀ (:woman_juggling_medium_skin_tone:)", + ), + ("🧎\u200d♀️", "🧎\u200d♀️ (:woman_kneeling:)"), + ("🧎\u200d♀", "🧎\u200d♀ (:woman_kneeling:)"), + ( + "🧎🏿\u200d♀️", + "🧎🏿\u200d♀️ (:woman_kneeling_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♀", + "🧎🏿\u200d♀ (:woman_kneeling_dark_skin_tone:)", + ), + ( + "🧎\u200d♀️\u200d➡️", + "🧎\u200d♀️\u200d➡️ (:woman_kneeling_facing_right:)", + ), + ( + "🧎\u200d♀\u200d➡️", + "🧎\u200d♀\u200d➡️ (:woman_kneeling_facing_right:)", + ), + ( + "🧎\u200d♀️\u200d➡", + "🧎\u200d♀️\u200d➡ (:woman_kneeling_facing_right:)", + ), + ( + "🧎\u200d♀\u200d➡", + "🧎\u200d♀\u200d➡ (:woman_kneeling_facing_right:)", + ), + ( + "🧎🏿\u200d♀️\u200d➡️", + "🧎🏿\u200d♀️\u200d➡️ (:woman_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♀\u200d➡️", + "🧎🏿\u200d♀\u200d➡️ (:woman_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♀️\u200d➡", + "🧎🏿\u200d♀️\u200d➡ (:woman_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏿\u200d♀\u200d➡", + "🧎🏿\u200d♀\u200d➡ (:woman_kneeling_facing_right_dark_skin_tone:)", + ), + ( + "🧎🏻\u200d♀️\u200d➡️", + "🧎🏻\u200d♀️\u200d➡️ (:woman_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♀\u200d➡️", + "🧎🏻\u200d♀\u200d➡️ (:woman_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♀️\u200d➡", + "🧎🏻\u200d♀️\u200d➡ (:woman_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♀\u200d➡", + "🧎🏻\u200d♀\u200d➡ (:woman_kneeling_facing_right_light_skin_tone:)", + ), + ( + "🧎🏾\u200d♀️\u200d➡️", + "🧎🏾\u200d♀️\u200d➡️ (:woman_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♀\u200d➡️", + "🧎🏾\u200d♀\u200d➡️ (:woman_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♀️\u200d➡", + "🧎🏾\u200d♀️\u200d➡ (:woman_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♀\u200d➡", + "🧎🏾\u200d♀\u200d➡ (:woman_kneeling_facing_right_medium-dark_skin_tone:)", + ), + ( + "🧎🏼\u200d♀️\u200d➡️", + "🧎🏼\u200d♀️\u200d➡️ (:woman_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♀\u200d➡️", + "🧎🏼\u200d♀\u200d➡️ (:woman_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♀️\u200d➡", + "🧎🏼\u200d♀️\u200d➡ (:woman_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♀\u200d➡", + "🧎🏼\u200d♀\u200d➡ (:woman_kneeling_facing_right_medium-light_skin_tone:)", + ), + ( + "🧎🏽\u200d♀️\u200d➡️", + "🧎🏽\u200d♀️\u200d➡️ (:woman_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♀\u200d➡️", + "🧎🏽\u200d♀\u200d➡️ (:woman_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♀️\u200d➡", + "🧎🏽\u200d♀️\u200d➡ (:woman_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♀\u200d➡", + "🧎🏽\u200d♀\u200d➡ (:woman_kneeling_facing_right_medium_skin_tone:)", + ), + ( + "🧎🏻\u200d♀️", + "🧎🏻\u200d♀️ (:woman_kneeling_light_skin_tone:)", + ), + ( + "🧎🏻\u200d♀", + "🧎🏻\u200d♀ (:woman_kneeling_light_skin_tone:)", + ), + ( + "🧎🏾\u200d♀️", + "🧎🏾\u200d♀️ (:woman_kneeling_medium-dark_skin_tone:)", + ), + ( + "🧎🏾\u200d♀", + "🧎🏾\u200d♀ (:woman_kneeling_medium-dark_skin_tone:)", + ), + ( + "🧎🏼\u200d♀️", + "🧎🏼\u200d♀️ (:woman_kneeling_medium-light_skin_tone:)", + ), + ( + "🧎🏼\u200d♀", + "🧎🏼\u200d♀ (:woman_kneeling_medium-light_skin_tone:)", + ), + ( + "🧎🏽\u200d♀️", + "🧎🏽\u200d♀️ (:woman_kneeling_medium_skin_tone:)", + ), + ( + "🧎🏽\u200d♀", + "🧎🏽\u200d♀ (:woman_kneeling_medium_skin_tone:)", + ), + ("🏋️\u200d♀️", "🏋️\u200d♀️ (:woman_lifting_weights:)"), + ("🏋\u200d♀️", "🏋\u200d♀️ (:woman_lifting_weights:)"), + ("🏋️\u200d♀", "🏋️\u200d♀ (:woman_lifting_weights:)"), + ("🏋\u200d♀", "🏋\u200d♀ (:woman_lifting_weights:)"), + ( + "🏋🏿\u200d♀️", + "🏋🏿\u200d♀️ (:woman_lifting_weights_dark_skin_tone:)", + ), + ( + "🏋🏿\u200d♀", + "🏋🏿\u200d♀ (:woman_lifting_weights_dark_skin_tone:)", + ), + ( + "🏋🏻\u200d♀️", + "🏋🏻\u200d♀️ (:woman_lifting_weights_light_skin_tone:)", + ), + ( + "🏋🏻\u200d♀", + "🏋🏻\u200d♀ (:woman_lifting_weights_light_skin_tone:)", + ), + ( + "🏋🏾\u200d♀️", + "🏋🏾\u200d♀️ (:woman_lifting_weights_medium-dark_skin_tone:)", + ), + ( + "🏋🏾\u200d♀", + "🏋🏾\u200d♀ (:woman_lifting_weights_medium-dark_skin_tone:)", + ), + ( + "🏋🏼\u200d♀️", + "🏋🏼\u200d♀️ (:woman_lifting_weights_medium-light_skin_tone:)", + ), + ( + "🏋🏼\u200d♀", + "🏋🏼\u200d♀ (:woman_lifting_weights_medium-light_skin_tone:)", + ), + ( + "🏋🏽\u200d♀️", + "🏋🏽\u200d♀️ (:woman_lifting_weights_medium_skin_tone:)", + ), + ( + "🏋🏽\u200d♀", + "🏋🏽\u200d♀ (:woman_lifting_weights_medium_skin_tone:)", + ), + ("👩🏻", "👩🏻 (:woman_light_skin_tone:)"), + ("👩🏻\u200d🦲", "👩🏻\u200d🦲 (:woman_light_skin_tone_bald:)"), + ( + "🧔🏻\u200d♀️", + "🧔🏻\u200d♀️ (:woman_light_skin_tone_beard:)", + ), + ("🧔🏻\u200d♀", "🧔🏻\u200d♀ (:woman_light_skin_tone_beard:)"), + ( + "👱🏻\u200d♀️", + "👱🏻\u200d♀️ (:woman_light_skin_tone_blond_hair:)", + ), + ( + "👱🏻\u200d♀", + "👱🏻\u200d♀ (:woman_light_skin_tone_blond_hair:)", + ), + ( + "👩🏻\u200d🦱", + "👩🏻\u200d🦱 (:woman_light_skin_tone_curly_hair:)", + ), + ( + "👩🏻\u200d🦰", + "👩🏻\u200d🦰 (:woman_light_skin_tone_red_hair:)", + ), + ( + "👩🏻\u200d🦳", + "👩🏻\u200d🦳 (:woman_light_skin_tone_white_hair:)", + ), + ("🧙\u200d♀️", "🧙\u200d♀️ (:woman_mage:)"), + ("🧙\u200d♀", "🧙\u200d♀ (:woman_mage:)"), + ("🧙🏿\u200d♀️", "🧙🏿\u200d♀️ (:woman_mage_dark_skin_tone:)"), + ("🧙🏿\u200d♀", "🧙🏿\u200d♀ (:woman_mage_dark_skin_tone:)"), + ("🧙🏻\u200d♀️", "🧙🏻\u200d♀️ (:woman_mage_light_skin_tone:)"), + ("🧙🏻\u200d♀", "🧙🏻\u200d♀ (:woman_mage_light_skin_tone:)"), + ( + "🧙🏾\u200d♀️", + "🧙🏾\u200d♀️ (:woman_mage_medium-dark_skin_tone:)", + ), + ( + "🧙🏾\u200d♀", + "🧙🏾\u200d♀ (:woman_mage_medium-dark_skin_tone:)", + ), + ( + "🧙🏼\u200d♀️", + "🧙🏼\u200d♀️ (:woman_mage_medium-light_skin_tone:)", + ), + ( + "🧙🏼\u200d♀", + "🧙🏼\u200d♀ (:woman_mage_medium-light_skin_tone:)", + ), + ( + "🧙🏽\u200d♀️", + "🧙🏽\u200d♀️ (:woman_mage_medium_skin_tone:)", + ), + ("🧙🏽\u200d♀", "🧙🏽\u200d♀ (:woman_mage_medium_skin_tone:)"), + ("👩\u200d🔧", "👩\u200d🔧 (:woman_mechanic:)"), + ( + "👩🏿\u200d🔧", + "👩🏿\u200d🔧 (:woman_mechanic_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🔧", + "👩🏻\u200d🔧 (:woman_mechanic_light_skin_tone:)", + ), + ( + "👩🏾\u200d🔧", + "👩🏾\u200d🔧 (:woman_mechanic_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🔧", + "👩🏼\u200d🔧 (:woman_mechanic_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🔧", + "👩🏽\u200d🔧 (:woman_mechanic_medium_skin_tone:)", + ), + ("👩🏾", "👩🏾 (:woman_medium-dark_skin_tone:)"), + ( + "👩🏾\u200d🦲", + "👩🏾\u200d🦲 (:woman_medium-dark_skin_tone_bald:)", + ), + ( + "🧔🏾\u200d♀️", + "🧔🏾\u200d♀️ (:woman_medium-dark_skin_tone_beard:)", + ), + ( + "🧔🏾\u200d♀", + "🧔🏾\u200d♀ (:woman_medium-dark_skin_tone_beard:)", + ), + ( + "👱🏾\u200d♀️", + "👱🏾\u200d♀️ (:woman_medium-dark_skin_tone_blond_hair:)", + ), + ( + "👱🏾\u200d♀", + "👱🏾\u200d♀ (:woman_medium-dark_skin_tone_blond_hair:)", + ), + ( + "👩🏾\u200d🦱", + "👩🏾\u200d🦱 (:woman_medium-dark_skin_tone_curly_hair:)", + ), + ( + "👩🏾\u200d🦰", + "👩🏾\u200d🦰 (:woman_medium-dark_skin_tone_red_hair:)", + ), + ( + "👩🏾\u200d🦳", + "👩🏾\u200d🦳 (:woman_medium-dark_skin_tone_white_hair:)", + ), + ("👩🏼", "👩🏼 (:woman_medium-light_skin_tone:)"), + ( + "👩🏼\u200d🦲", + "👩🏼\u200d🦲 (:woman_medium-light_skin_tone_bald:)", + ), + ( + "🧔🏼\u200d♀️", + "🧔🏼\u200d♀️ (:woman_medium-light_skin_tone_beard:)", + ), + ( + "🧔🏼\u200d♀", + "🧔🏼\u200d♀ (:woman_medium-light_skin_tone_beard:)", + ), + ( + "👱🏼\u200d♀️", + "👱🏼\u200d♀️ (:woman_medium-light_skin_tone_blond_hair:)", + ), + ( + "👱🏼\u200d♀", + "👱🏼\u200d♀ (:woman_medium-light_skin_tone_blond_hair:)", + ), + ( + "👩🏼\u200d🦱", + "👩🏼\u200d🦱 (:woman_medium-light_skin_tone_curly_hair:)", + ), + ( + "👩🏼\u200d🦰", + "👩🏼\u200d🦰 (:woman_medium-light_skin_tone_red_hair:)", + ), + ( + "👩🏼\u200d🦳", + "👩🏼\u200d🦳 (:woman_medium-light_skin_tone_white_hair:)", + ), + ("👩🏽", "👩🏽 (:woman_medium_skin_tone:)"), + ("👩🏽\u200d🦲", "👩🏽\u200d🦲 (:woman_medium_skin_tone_bald:)"), + ( + "🧔🏽\u200d♀️", + "🧔🏽\u200d♀️ (:woman_medium_skin_tone_beard:)", + ), + ("🧔🏽\u200d♀", "🧔🏽\u200d♀ (:woman_medium_skin_tone_beard:)"), + ( + "👱🏽\u200d♀️", + "👱🏽\u200d♀️ (:woman_medium_skin_tone_blond_hair:)", + ), + ( + "👱🏽\u200d♀", + "👱🏽\u200d♀ (:woman_medium_skin_tone_blond_hair:)", + ), + ( + "👩🏽\u200d🦱", + "👩🏽\u200d🦱 (:woman_medium_skin_tone_curly_hair:)", + ), + ( + "👩🏽\u200d🦰", + "👩🏽\u200d🦰 (:woman_medium_skin_tone_red_hair:)", + ), + ( + "👩🏽\u200d🦳", + "👩🏽\u200d🦳 (:woman_medium_skin_tone_white_hair:)", + ), + ("🚵\u200d♀️", "🚵\u200d♀️ (:woman_mountain_biking:)"), + ("🚵\u200d♀", "🚵\u200d♀ (:woman_mountain_biking:)"), + ( + "🚵🏿\u200d♀️", + "🚵🏿\u200d♀️ (:woman_mountain_biking_dark_skin_tone:)", + ), + ( + "🚵🏿\u200d♀", + "🚵🏿\u200d♀ (:woman_mountain_biking_dark_skin_tone:)", + ), + ( + "🚵🏻\u200d♀️", + "🚵🏻\u200d♀️ (:woman_mountain_biking_light_skin_tone:)", + ), + ( + "🚵🏻\u200d♀", + "🚵🏻\u200d♀ (:woman_mountain_biking_light_skin_tone:)", + ), + ( + "🚵🏾\u200d♀️", + "🚵🏾\u200d♀️ (:woman_mountain_biking_medium-dark_skin_tone:)", + ), + ( + "🚵🏾\u200d♀", + "🚵🏾\u200d♀ (:woman_mountain_biking_medium-dark_skin_tone:)", + ), + ( + "🚵🏼\u200d♀️", + "🚵🏼\u200d♀️ (:woman_mountain_biking_medium-light_skin_tone:)", + ), + ( + "🚵🏼\u200d♀", + "🚵🏼\u200d♀ (:woman_mountain_biking_medium-light_skin_tone:)", + ), + ( + "🚵🏽\u200d♀️", + "🚵🏽\u200d♀️ (:woman_mountain_biking_medium_skin_tone:)", + ), + ( + "🚵🏽\u200d♀", + "🚵🏽\u200d♀ (:woman_mountain_biking_medium_skin_tone:)", + ), + ("👩\u200d💼", "👩\u200d💼 (:woman_office_worker:)"), + ( + "👩🏿\u200d💼", + "👩🏿\u200d💼 (:woman_office_worker_dark_skin_tone:)", + ), + ( + "👩🏻\u200d💼", + "👩🏻\u200d💼 (:woman_office_worker_light_skin_tone:)", + ), + ( + "👩🏾\u200d💼", + "👩🏾\u200d💼 (:woman_office_worker_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d💼", + "👩🏼\u200d💼 (:woman_office_worker_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d💼", + "👩🏽\u200d💼 (:woman_office_worker_medium_skin_tone:)", + ), + ("👩\u200d✈️", "👩\u200d✈️ (:woman_pilot:)"), + ("👩\u200d✈", "👩\u200d✈ (:woman_pilot:)"), + ("👩🏿\u200d✈️", "👩🏿\u200d✈️ (:woman_pilot_dark_skin_tone:)"), + ("👩🏿\u200d✈", "👩🏿\u200d✈ (:woman_pilot_dark_skin_tone:)"), + ( + "👩🏻\u200d✈️", + "👩🏻\u200d✈️ (:woman_pilot_light_skin_tone:)", + ), + ("👩🏻\u200d✈", "👩🏻\u200d✈ (:woman_pilot_light_skin_tone:)"), + ( + "👩🏾\u200d✈️", + "👩🏾\u200d✈️ (:woman_pilot_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d✈", + "👩🏾\u200d✈ (:woman_pilot_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d✈️", + "👩🏼\u200d✈️ (:woman_pilot_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d✈", + "👩🏼\u200d✈ (:woman_pilot_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d✈️", + "👩🏽\u200d✈️ (:woman_pilot_medium_skin_tone:)", + ), + ("👩🏽\u200d✈", "👩🏽\u200d✈ (:woman_pilot_medium_skin_tone:)"), + ("🤾\u200d♀️", "🤾\u200d♀️ (:woman_playing_handball:)"), + ("🤾\u200d♀", "🤾\u200d♀ (:woman_playing_handball:)"), + ( + "🤾🏿\u200d♀️", + "🤾🏿\u200d♀️ (:woman_playing_handball_dark_skin_tone:)", + ), + ( + "🤾🏿\u200d♀", + "🤾🏿\u200d♀ (:woman_playing_handball_dark_skin_tone:)", + ), + ( + "🤾🏻\u200d♀️", + "🤾🏻\u200d♀️ (:woman_playing_handball_light_skin_tone:)", + ), + ( + "🤾🏻\u200d♀", + "🤾🏻\u200d♀ (:woman_playing_handball_light_skin_tone:)", + ), + ( + "🤾🏾\u200d♀️", + "🤾🏾\u200d♀️ (:woman_playing_handball_medium-dark_skin_tone:)", + ), + ( + "🤾🏾\u200d♀", + "🤾🏾\u200d♀ (:woman_playing_handball_medium-dark_skin_tone:)", + ), + ( + "🤾🏼\u200d♀️", + "🤾🏼\u200d♀️ (:woman_playing_handball_medium-light_skin_tone:)", + ), + ( + "🤾🏼\u200d♀", + "🤾🏼\u200d♀ (:woman_playing_handball_medium-light_skin_tone:)", + ), + ( + "🤾🏽\u200d♀️", + "🤾🏽\u200d♀️ (:woman_playing_handball_medium_skin_tone:)", + ), + ( + "🤾🏽\u200d♀", + "🤾🏽\u200d♀ (:woman_playing_handball_medium_skin_tone:)", + ), + ("🤽\u200d♀️", "🤽\u200d♀️ (:woman_playing_water_polo:)"), + ("🤽\u200d♀", "🤽\u200d♀ (:woman_playing_water_polo:)"), + ( + "🤽🏿\u200d♀️", + "🤽🏿\u200d♀️ (:woman_playing_water_polo_dark_skin_tone:)", + ), + ( + "🤽🏿\u200d♀", + "🤽🏿\u200d♀ (:woman_playing_water_polo_dark_skin_tone:)", + ), + ( + "🤽🏻\u200d♀️", + "🤽🏻\u200d♀️ (:woman_playing_water_polo_light_skin_tone:)", + ), + ( + "🤽🏻\u200d♀", + "🤽🏻\u200d♀ (:woman_playing_water_polo_light_skin_tone:)", + ), + ( + "🤽🏾\u200d♀️", + "🤽🏾\u200d♀️ (:woman_playing_water_polo_medium-dark_skin_tone:)", + ), + ( + "🤽🏾\u200d♀", + "🤽🏾\u200d♀ (:woman_playing_water_polo_medium-dark_skin_tone:)", + ), + ( + "🤽🏼\u200d♀️", + "🤽🏼\u200d♀️ (:woman_playing_water_polo_medium-light_skin_tone:)", + ), + ( + "🤽🏼\u200d♀", + "🤽🏼\u200d♀ (:woman_playing_water_polo_medium-light_skin_tone:)", + ), + ( + "🤽🏽\u200d♀️", + "🤽🏽\u200d♀️ (:woman_playing_water_polo_medium_skin_tone:)", + ), + ( + "🤽🏽\u200d♀", + "🤽🏽\u200d♀ (:woman_playing_water_polo_medium_skin_tone:)", + ), + ("👮\u200d♀️", "👮\u200d♀️ (:woman_police_officer:)"), + ("👮\u200d♀", "👮\u200d♀ (:woman_police_officer:)"), + ( + "👮🏿\u200d♀️", + "👮🏿\u200d♀️ (:woman_police_officer_dark_skin_tone:)", + ), + ( + "👮🏿\u200d♀", + "👮🏿\u200d♀ (:woman_police_officer_dark_skin_tone:)", + ), + ( + "👮🏻\u200d♀️", + "👮🏻\u200d♀️ (:woman_police_officer_light_skin_tone:)", + ), + ( + "👮🏻\u200d♀", + "👮🏻\u200d♀ (:woman_police_officer_light_skin_tone:)", + ), + ( + "👮🏾\u200d♀️", + "👮🏾\u200d♀️ (:woman_police_officer_medium-dark_skin_tone:)", + ), + ( + "👮🏾\u200d♀", + "👮🏾\u200d♀ (:woman_police_officer_medium-dark_skin_tone:)", + ), + ( + "👮🏼\u200d♀️", + "👮🏼\u200d♀️ (:woman_police_officer_medium-light_skin_tone:)", + ), + ( + "👮🏼\u200d♀", + "👮🏼\u200d♀ (:woman_police_officer_medium-light_skin_tone:)", + ), + ( + "👮🏽\u200d♀️", + "👮🏽\u200d♀️ (:woman_police_officer_medium_skin_tone:)", + ), + ( + "👮🏽\u200d♀", + "👮🏽\u200d♀ (:woman_police_officer_medium_skin_tone:)", + ), + ("🙎\u200d♀️", "🙎\u200d♀️ (:woman_pouting:)"), + ("🙎\u200d♀", "🙎\u200d♀ (:woman_pouting:)"), + ( + "🙎🏿\u200d♀️", + "🙎🏿\u200d♀️ (:woman_pouting_dark_skin_tone:)", + ), + ("🙎🏿\u200d♀", "🙎🏿\u200d♀ (:woman_pouting_dark_skin_tone:)"), + ( + "🙎🏻\u200d♀️", + "🙎🏻\u200d♀️ (:woman_pouting_light_skin_tone:)", + ), + ( + "🙎🏻\u200d♀", + "🙎🏻\u200d♀ (:woman_pouting_light_skin_tone:)", + ), + ( + "🙎🏾\u200d♀️", + "🙎🏾\u200d♀️ (:woman_pouting_medium-dark_skin_tone:)", + ), + ( + "🙎🏾\u200d♀", + "🙎🏾\u200d♀ (:woman_pouting_medium-dark_skin_tone:)", + ), + ( + "🙎🏼\u200d♀️", + "🙎🏼\u200d♀️ (:woman_pouting_medium-light_skin_tone:)", + ), + ( + "🙎🏼\u200d♀", + "🙎🏼\u200d♀ (:woman_pouting_medium-light_skin_tone:)", + ), + ( + "🙎🏽\u200d♀️", + "🙎🏽\u200d♀️ (:woman_pouting_medium_skin_tone:)", + ), + ( + "🙎🏽\u200d♀", + "🙎🏽\u200d♀ (:woman_pouting_medium_skin_tone:)", + ), + ("🙋\u200d♀️", "🙋\u200d♀️ (:woman_raising_hand:)"), + ("🙋\u200d♀", "🙋\u200d♀ (:woman_raising_hand:)"), + ( + "🙋🏿\u200d♀️", + "🙋🏿\u200d♀️ (:woman_raising_hand_dark_skin_tone:)", + ), + ( + "🙋🏿\u200d♀", + "🙋🏿\u200d♀ (:woman_raising_hand_dark_skin_tone:)", + ), + ( + "🙋🏻\u200d♀️", + "🙋🏻\u200d♀️ (:woman_raising_hand_light_skin_tone:)", + ), + ( + "🙋🏻\u200d♀", + "🙋🏻\u200d♀ (:woman_raising_hand_light_skin_tone:)", + ), + ( + "🙋🏾\u200d♀️", + "🙋🏾\u200d♀️ (:woman_raising_hand_medium-dark_skin_tone:)", + ), + ( + "🙋🏾\u200d♀", + "🙋🏾\u200d♀ (:woman_raising_hand_medium-dark_skin_tone:)", + ), + ( + "🙋🏼\u200d♀️", + "🙋🏼\u200d♀️ (:woman_raising_hand_medium-light_skin_tone:)", + ), + ( + "🙋🏼\u200d♀", + "🙋🏼\u200d♀ (:woman_raising_hand_medium-light_skin_tone:)", + ), + ( + "🙋🏽\u200d♀️", + "🙋🏽\u200d♀️ (:woman_raising_hand_medium_skin_tone:)", + ), + ( + "🙋🏽\u200d♀", + "🙋🏽\u200d♀ (:woman_raising_hand_medium_skin_tone:)", + ), + ("👩\u200d🦰", "👩\u200d🦰 (:woman_red_hair:)"), + ("🚣\u200d♀️", "🚣\u200d♀️ (:woman_rowing_boat:)"), + ("🚣\u200d♀", "🚣\u200d♀ (:woman_rowing_boat:)"), + ( + "🚣🏿\u200d♀️", + "🚣🏿\u200d♀️ (:woman_rowing_boat_dark_skin_tone:)", + ), + ( + "🚣🏿\u200d♀", + "🚣🏿\u200d♀ (:woman_rowing_boat_dark_skin_tone:)", + ), + ( + "🚣🏻\u200d♀️", + "🚣🏻\u200d♀️ (:woman_rowing_boat_light_skin_tone:)", + ), + ( + "🚣🏻\u200d♀", + "🚣🏻\u200d♀ (:woman_rowing_boat_light_skin_tone:)", + ), + ( + "🚣🏾\u200d♀️", + "🚣🏾\u200d♀️ (:woman_rowing_boat_medium-dark_skin_tone:)", + ), + ( + "🚣🏾\u200d♀", + "🚣🏾\u200d♀ (:woman_rowing_boat_medium-dark_skin_tone:)", + ), + ( + "🚣🏼\u200d♀️", + "🚣🏼\u200d♀️ (:woman_rowing_boat_medium-light_skin_tone:)", + ), + ( + "🚣🏼\u200d♀", + "🚣🏼\u200d♀ (:woman_rowing_boat_medium-light_skin_tone:)", + ), + ( + "🚣🏽\u200d♀️", + "🚣🏽\u200d♀️ (:woman_rowing_boat_medium_skin_tone:)", + ), + ( + "🚣🏽\u200d♀", + "🚣🏽\u200d♀ (:woman_rowing_boat_medium_skin_tone:)", + ), + ("🏃\u200d♀️", "🏃\u200d♀️ (:woman_running:)"), + ("🏃\u200d♀", "🏃\u200d♀ (:woman_running:)"), + ( + "🏃🏿\u200d♀️", + "🏃🏿\u200d♀️ (:woman_running_dark_skin_tone:)", + ), + ("🏃🏿\u200d♀", "🏃🏿\u200d♀ (:woman_running_dark_skin_tone:)"), + ( + "🏃\u200d♀️\u200d➡️", + "🏃\u200d♀️\u200d➡️ (:woman_running_facing_right:)", + ), + ( + "🏃\u200d♀\u200d➡️", + "🏃\u200d♀\u200d➡️ (:woman_running_facing_right:)", + ), + ( + "🏃\u200d♀️\u200d➡", + "🏃\u200d♀️\u200d➡ (:woman_running_facing_right:)", + ), + ( + "🏃\u200d♀\u200d➡", + "🏃\u200d♀\u200d➡ (:woman_running_facing_right:)", + ), + ( + "🏃🏿\u200d♀️\u200d➡️", + "🏃🏿\u200d♀️\u200d➡️ (:woman_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♀\u200d➡️", + "🏃🏿\u200d♀\u200d➡️ (:woman_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♀️\u200d➡", + "🏃🏿\u200d♀️\u200d➡ (:woman_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏿\u200d♀\u200d➡", + "🏃🏿\u200d♀\u200d➡ (:woman_running_facing_right_dark_skin_tone:)", + ), + ( + "🏃🏻\u200d♀️\u200d➡️", + "🏃🏻\u200d♀️\u200d➡️ (:woman_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♀\u200d➡️", + "🏃🏻\u200d♀\u200d➡️ (:woman_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♀️\u200d➡", + "🏃🏻\u200d♀️\u200d➡ (:woman_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♀\u200d➡", + "🏃🏻\u200d♀\u200d➡ (:woman_running_facing_right_light_skin_tone:)", + ), + ( + "🏃🏾\u200d♀️\u200d➡️", + "🏃🏾\u200d♀️\u200d➡️ (:woman_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♀\u200d➡️", + "🏃🏾\u200d♀\u200d➡️ (:woman_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♀️\u200d➡", + "🏃🏾\u200d♀️\u200d➡ (:woman_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♀\u200d➡", + "🏃🏾\u200d♀\u200d➡ (:woman_running_facing_right_medium-dark_skin_tone:)", + ), + ( + "🏃🏼\u200d♀️\u200d➡️", + "🏃🏼\u200d♀️\u200d➡️ (:woman_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♀\u200d➡️", + "🏃🏼\u200d♀\u200d➡️ (:woman_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♀️\u200d➡", + "🏃🏼\u200d♀️\u200d➡ (:woman_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♀\u200d➡", + "🏃🏼\u200d♀\u200d➡ (:woman_running_facing_right_medium-light_skin_tone:)", + ), + ( + "🏃🏽\u200d♀️\u200d➡️", + "🏃🏽\u200d♀️\u200d➡️ (:woman_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♀\u200d➡️", + "🏃🏽\u200d♀\u200d➡️ (:woman_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♀️\u200d➡", + "🏃🏽\u200d♀️\u200d➡ (:woman_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♀\u200d➡", + "🏃🏽\u200d♀\u200d➡ (:woman_running_facing_right_medium_skin_tone:)", + ), + ( + "🏃🏻\u200d♀️", + "🏃🏻\u200d♀️ (:woman_running_light_skin_tone:)", + ), + ( + "🏃🏻\u200d♀", + "🏃🏻\u200d♀ (:woman_running_light_skin_tone:)", + ), + ( + "🏃🏾\u200d♀️", + "🏃🏾\u200d♀️ (:woman_running_medium-dark_skin_tone:)", + ), + ( + "🏃🏾\u200d♀", + "🏃🏾\u200d♀ (:woman_running_medium-dark_skin_tone:)", + ), + ( + "🏃🏼\u200d♀️", + "🏃🏼\u200d♀️ (:woman_running_medium-light_skin_tone:)", + ), + ( + "🏃🏼\u200d♀", + "🏃🏼\u200d♀ (:woman_running_medium-light_skin_tone:)", + ), + ( + "🏃🏽\u200d♀️", + "🏃🏽\u200d♀️ (:woman_running_medium_skin_tone:)", + ), + ( + "🏃🏽\u200d♀", + "🏃🏽\u200d♀ (:woman_running_medium_skin_tone:)", + ), + ("👩\u200d🔬", "👩\u200d🔬 (:woman_scientist:)"), + ( + "👩🏿\u200d🔬", + "👩🏿\u200d🔬 (:woman_scientist_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🔬", + "👩🏻\u200d🔬 (:woman_scientist_light_skin_tone:)", + ), + ( + "👩🏾\u200d🔬", + "👩🏾\u200d🔬 (:woman_scientist_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🔬", + "👩🏼\u200d🔬 (:woman_scientist_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🔬", + "👩🏽\u200d🔬 (:woman_scientist_medium_skin_tone:)", + ), + ("🤷\u200d♀️", "🤷\u200d♀️ (:woman_shrugging:)"), + ("🤷\u200d♀", "🤷\u200d♀ (:woman_shrugging:)"), + ( + "🤷🏿\u200d♀️", + "🤷🏿\u200d♀️ (:woman_shrugging_dark_skin_tone:)", + ), + ( + "🤷🏿\u200d♀", + "🤷🏿\u200d♀ (:woman_shrugging_dark_skin_tone:)", + ), + ( + "🤷🏻\u200d♀️", + "🤷🏻\u200d♀️ (:woman_shrugging_light_skin_tone:)", + ), + ( + "🤷🏻\u200d♀", + "🤷🏻\u200d♀ (:woman_shrugging_light_skin_tone:)", + ), + ( + "🤷🏾\u200d♀️", + "🤷🏾\u200d♀️ (:woman_shrugging_medium-dark_skin_tone:)", + ), + ( + "🤷🏾\u200d♀", + "🤷🏾\u200d♀ (:woman_shrugging_medium-dark_skin_tone:)", + ), + ( + "🤷🏼\u200d♀️", + "🤷🏼\u200d♀️ (:woman_shrugging_medium-light_skin_tone:)", + ), + ( + "🤷🏼\u200d♀", + "🤷🏼\u200d♀ (:woman_shrugging_medium-light_skin_tone:)", + ), + ( + "🤷🏽\u200d♀️", + "🤷🏽\u200d♀️ (:woman_shrugging_medium_skin_tone:)", + ), + ( + "🤷🏽\u200d♀", + "🤷🏽\u200d♀ (:woman_shrugging_medium_skin_tone:)", + ), + ("👩\u200d🎤", "👩\u200d🎤 (:woman_singer:)"), + ("👩🏿\u200d🎤", "👩🏿\u200d🎤 (:woman_singer_dark_skin_tone:)"), + ("👩🏻\u200d🎤", "👩🏻\u200d🎤 (:woman_singer_light_skin_tone:)"), + ( + "👩🏾\u200d🎤", + "👩🏾\u200d🎤 (:woman_singer_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🎤", + "👩🏼\u200d🎤 (:woman_singer_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🎤", + "👩🏽\u200d🎤 (:woman_singer_medium_skin_tone:)", + ), + ("🧍\u200d♀️", "🧍\u200d♀️ (:woman_standing:)"), + ("🧍\u200d♀", "🧍\u200d♀ (:woman_standing:)"), + ( + "🧍🏿\u200d♀️", + "🧍🏿\u200d♀️ (:woman_standing_dark_skin_tone:)", + ), + ( + "🧍🏿\u200d♀", + "🧍🏿\u200d♀ (:woman_standing_dark_skin_tone:)", + ), + ( + "🧍🏻\u200d♀️", + "🧍🏻\u200d♀️ (:woman_standing_light_skin_tone:)", + ), + ( + "🧍🏻\u200d♀", + "🧍🏻\u200d♀ (:woman_standing_light_skin_tone:)", + ), + ( + "🧍🏾\u200d♀️", + "🧍🏾\u200d♀️ (:woman_standing_medium-dark_skin_tone:)", + ), + ( + "🧍🏾\u200d♀", + "🧍🏾\u200d♀ (:woman_standing_medium-dark_skin_tone:)", + ), + ( + "🧍🏼\u200d♀️", + "🧍🏼\u200d♀️ (:woman_standing_medium-light_skin_tone:)", + ), + ( + "🧍🏼\u200d♀", + "🧍🏼\u200d♀ (:woman_standing_medium-light_skin_tone:)", + ), + ( + "🧍🏽\u200d♀️", + "🧍🏽\u200d♀️ (:woman_standing_medium_skin_tone:)", + ), + ( + "🧍🏽\u200d♀", + "🧍🏽\u200d♀ (:woman_standing_medium_skin_tone:)", + ), + ("👩\u200d🎓", "👩\u200d🎓 (:woman_student:)"), + ("👩🏿\u200d🎓", "👩🏿\u200d🎓 (:woman_student_dark_skin_tone:)"), + ( + "👩🏻\u200d🎓", + "👩🏻\u200d🎓 (:woman_student_light_skin_tone:)", + ), + ( + "👩🏾\u200d🎓", + "👩🏾\u200d🎓 (:woman_student_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🎓", + "👩🏼\u200d🎓 (:woman_student_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🎓", + "👩🏽\u200d🎓 (:woman_student_medium_skin_tone:)", + ), + ("🦸\u200d♀️", "🦸\u200d♀️ (:woman_superhero:)"), + ("🦸\u200d♀", "🦸\u200d♀ (:woman_superhero:)"), + ( + "🦸🏿\u200d♀️", + "🦸🏿\u200d♀️ (:woman_superhero_dark_skin_tone:)", + ), + ( + "🦸🏿\u200d♀", + "🦸🏿\u200d♀ (:woman_superhero_dark_skin_tone:)", + ), + ( + "🦸🏻\u200d♀️", + "🦸🏻\u200d♀️ (:woman_superhero_light_skin_tone:)", + ), + ( + "🦸🏻\u200d♀", + "🦸🏻\u200d♀ (:woman_superhero_light_skin_tone:)", + ), + ( + "🦸🏾\u200d♀️", + "🦸🏾\u200d♀️ (:woman_superhero_medium-dark_skin_tone:)", + ), + ( + "🦸🏾\u200d♀", + "🦸🏾\u200d♀ (:woman_superhero_medium-dark_skin_tone:)", + ), + ( + "🦸🏼\u200d♀️", + "🦸🏼\u200d♀️ (:woman_superhero_medium-light_skin_tone:)", + ), + ( + "🦸🏼\u200d♀", + "🦸🏼\u200d♀ (:woman_superhero_medium-light_skin_tone:)", + ), + ( + "🦸🏽\u200d♀️", + "🦸🏽\u200d♀️ (:woman_superhero_medium_skin_tone:)", + ), + ( + "🦸🏽\u200d♀", + "🦸🏽\u200d♀ (:woman_superhero_medium_skin_tone:)", + ), + ("🦹\u200d♀️", "🦹\u200d♀️ (:woman_supervillain:)"), + ("🦹\u200d♀", "🦹\u200d♀ (:woman_supervillain:)"), + ( + "🦹🏿\u200d♀️", + "🦹🏿\u200d♀️ (:woman_supervillain_dark_skin_tone:)", + ), + ( + "🦹🏿\u200d♀", + "🦹🏿\u200d♀ (:woman_supervillain_dark_skin_tone:)", + ), + ( + "🦹🏻\u200d♀️", + "🦹🏻\u200d♀️ (:woman_supervillain_light_skin_tone:)", + ), + ( + "🦹🏻\u200d♀", + "🦹🏻\u200d♀ (:woman_supervillain_light_skin_tone:)", + ), + ( + "🦹🏾\u200d♀️", + "🦹🏾\u200d♀️ (:woman_supervillain_medium-dark_skin_tone:)", + ), + ( + "🦹🏾\u200d♀", + "🦹🏾\u200d♀ (:woman_supervillain_medium-dark_skin_tone:)", + ), + ( + "🦹🏼\u200d♀️", + "🦹🏼\u200d♀️ (:woman_supervillain_medium-light_skin_tone:)", + ), + ( + "🦹🏼\u200d♀", + "🦹🏼\u200d♀ (:woman_supervillain_medium-light_skin_tone:)", + ), + ( + "🦹🏽\u200d♀️", + "🦹🏽\u200d♀️ (:woman_supervillain_medium_skin_tone:)", + ), + ( + "🦹🏽\u200d♀", + "🦹🏽\u200d♀ (:woman_supervillain_medium_skin_tone:)", + ), + ("🏄\u200d♀️", "🏄\u200d♀️ (:woman_surfing:)"), + ("🏄\u200d♀", "🏄\u200d♀ (:woman_surfing:)"), + ( + "🏄🏿\u200d♀️", + "🏄🏿\u200d♀️ (:woman_surfing_dark_skin_tone:)", + ), + ("🏄🏿\u200d♀", "🏄🏿\u200d♀ (:woman_surfing_dark_skin_tone:)"), + ( + "🏄🏻\u200d♀️", + "🏄🏻\u200d♀️ (:woman_surfing_light_skin_tone:)", + ), + ( + "🏄🏻\u200d♀", + "🏄🏻\u200d♀ (:woman_surfing_light_skin_tone:)", + ), + ( + "🏄🏾\u200d♀️", + "🏄🏾\u200d♀️ (:woman_surfing_medium-dark_skin_tone:)", + ), + ( + "🏄🏾\u200d♀", + "🏄🏾\u200d♀ (:woman_surfing_medium-dark_skin_tone:)", + ), + ( + "🏄🏼\u200d♀️", + "🏄🏼\u200d♀️ (:woman_surfing_medium-light_skin_tone:)", + ), + ( + "🏄🏼\u200d♀", + "🏄🏼\u200d♀ (:woman_surfing_medium-light_skin_tone:)", + ), + ( + "🏄🏽\u200d♀️", + "🏄🏽\u200d♀️ (:woman_surfing_medium_skin_tone:)", + ), + ( + "🏄🏽\u200d♀", + "🏄🏽\u200d♀ (:woman_surfing_medium_skin_tone:)", + ), + ("🏊\u200d♀️", "🏊\u200d♀️ (:woman_swimming:)"), + ("🏊\u200d♀", "🏊\u200d♀ (:woman_swimming:)"), + ( + "🏊🏿\u200d♀️", + "🏊🏿\u200d♀️ (:woman_swimming_dark_skin_tone:)", + ), + ( + "🏊🏿\u200d♀", + "🏊🏿\u200d♀ (:woman_swimming_dark_skin_tone:)", + ), + ( + "🏊🏻\u200d♀️", + "🏊🏻\u200d♀️ (:woman_swimming_light_skin_tone:)", + ), + ( + "🏊🏻\u200d♀", + "🏊🏻\u200d♀ (:woman_swimming_light_skin_tone:)", + ), + ( + "🏊🏾\u200d♀️", + "🏊🏾\u200d♀️ (:woman_swimming_medium-dark_skin_tone:)", + ), + ( + "🏊🏾\u200d♀", + "🏊🏾\u200d♀ (:woman_swimming_medium-dark_skin_tone:)", + ), + ( + "🏊🏼\u200d♀️", + "🏊🏼\u200d♀️ (:woman_swimming_medium-light_skin_tone:)", + ), + ( + "🏊🏼\u200d♀", + "🏊🏼\u200d♀ (:woman_swimming_medium-light_skin_tone:)", + ), + ( + "🏊🏽\u200d♀️", + "🏊🏽\u200d♀️ (:woman_swimming_medium_skin_tone:)", + ), + ( + "🏊🏽\u200d♀", + "🏊🏽\u200d♀ (:woman_swimming_medium_skin_tone:)", + ), + ("👩\u200d🏫", "👩\u200d🏫 (:woman_teacher:)"), + ("👩🏿\u200d🏫", "👩🏿\u200d🏫 (:woman_teacher_dark_skin_tone:)"), + ( + "👩🏻\u200d🏫", + "👩🏻\u200d🏫 (:woman_teacher_light_skin_tone:)", + ), + ( + "👩🏾\u200d🏫", + "👩🏾\u200d🏫 (:woman_teacher_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🏫", + "👩🏼\u200d🏫 (:woman_teacher_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🏫", + "👩🏽\u200d🏫 (:woman_teacher_medium_skin_tone:)", + ), + ("👩\u200d💻", "👩\u200d💻 (:woman_technologist:)"), + ( + "👩🏿\u200d💻", + "👩🏿\u200d💻 (:woman_technologist_dark_skin_tone:)", + ), + ( + "👩🏻\u200d💻", + "👩🏻\u200d💻 (:woman_technologist_light_skin_tone:)", + ), + ( + "👩🏾\u200d💻", + "👩🏾\u200d💻 (:woman_technologist_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d💻", + "👩🏼\u200d💻 (:woman_technologist_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d💻", + "👩🏽\u200d💻 (:woman_technologist_medium_skin_tone:)", + ), + ("💁\u200d♀️", "💁\u200d♀️ (:woman_tipping_hand:)"), + ("💁\u200d♀", "💁\u200d♀ (:woman_tipping_hand:)"), + ( + "💁🏿\u200d♀️", + "💁🏿\u200d♀️ (:woman_tipping_hand_dark_skin_tone:)", + ), + ( + "💁🏿\u200d♀", + "💁🏿\u200d♀ (:woman_tipping_hand_dark_skin_tone:)", + ), + ( + "💁🏻\u200d♀️", + "💁🏻\u200d♀️ (:woman_tipping_hand_light_skin_tone:)", + ), + ( + "💁🏻\u200d♀", + "💁🏻\u200d♀ (:woman_tipping_hand_light_skin_tone:)", + ), + ( + "💁🏾\u200d♀️", + "💁🏾\u200d♀️ (:woman_tipping_hand_medium-dark_skin_tone:)", + ), + ( + "💁🏾\u200d♀", + "💁🏾\u200d♀ (:woman_tipping_hand_medium-dark_skin_tone:)", + ), + ( + "💁🏼\u200d♀️", + "💁🏼\u200d♀️ (:woman_tipping_hand_medium-light_skin_tone:)", + ), + ( + "💁🏼\u200d♀", + "💁🏼\u200d♀ (:woman_tipping_hand_medium-light_skin_tone:)", + ), + ( + "💁🏽\u200d♀️", + "💁🏽\u200d♀️ (:woman_tipping_hand_medium_skin_tone:)", + ), + ( + "💁🏽\u200d♀", + "💁🏽\u200d♀ (:woman_tipping_hand_medium_skin_tone:)", + ), + ("🧛\u200d♀️", "🧛\u200d♀️ (:woman_vampire:)"), + ("🧛\u200d♀", "🧛\u200d♀ (:woman_vampire:)"), + ( + "🧛🏿\u200d♀️", + "🧛🏿\u200d♀️ (:woman_vampire_dark_skin_tone:)", + ), + ("🧛🏿\u200d♀", "🧛🏿\u200d♀ (:woman_vampire_dark_skin_tone:)"), + ( + "🧛🏻\u200d♀️", + "🧛🏻\u200d♀️ (:woman_vampire_light_skin_tone:)", + ), + ( + "🧛🏻\u200d♀", + "🧛🏻\u200d♀ (:woman_vampire_light_skin_tone:)", + ), + ( + "🧛🏾\u200d♀️", + "🧛🏾\u200d♀️ (:woman_vampire_medium-dark_skin_tone:)", + ), + ( + "🧛🏾\u200d♀", + "🧛🏾\u200d♀ (:woman_vampire_medium-dark_skin_tone:)", + ), + ( + "🧛🏼\u200d♀️", + "🧛🏼\u200d♀️ (:woman_vampire_medium-light_skin_tone:)", + ), + ( + "🧛🏼\u200d♀", + "🧛🏼\u200d♀ (:woman_vampire_medium-light_skin_tone:)", + ), + ( + "🧛🏽\u200d♀️", + "🧛🏽\u200d♀️ (:woman_vampire_medium_skin_tone:)", + ), + ( + "🧛🏽\u200d♀", + "🧛🏽\u200d♀ (:woman_vampire_medium_skin_tone:)", + ), + ("🚶\u200d♀️", "🚶\u200d♀️ (:woman_walking:)"), + ("🚶\u200d♀", "🚶\u200d♀ (:woman_walking:)"), + ( + "🚶🏿\u200d♀️", + "🚶🏿\u200d♀️ (:woman_walking_dark_skin_tone:)", + ), + ("🚶🏿\u200d♀", "🚶🏿\u200d♀ (:woman_walking_dark_skin_tone:)"), + ( + "🚶\u200d♀️\u200d➡️", + "🚶\u200d♀️\u200d➡️ (:woman_walking_facing_right:)", + ), + ( + "🚶\u200d♀\u200d➡️", + "🚶\u200d♀\u200d➡️ (:woman_walking_facing_right:)", + ), + ( + "🚶\u200d♀️\u200d➡", + "🚶\u200d♀️\u200d➡ (:woman_walking_facing_right:)", + ), + ( + "🚶\u200d♀\u200d➡", + "🚶\u200d♀\u200d➡ (:woman_walking_facing_right:)", + ), + ( + "🚶🏿\u200d♀️\u200d➡️", + "🚶🏿\u200d♀️\u200d➡️ (:woman_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♀\u200d➡️", + "🚶🏿\u200d♀\u200d➡️ (:woman_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♀️\u200d➡", + "🚶🏿\u200d♀️\u200d➡ (:woman_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏿\u200d♀\u200d➡", + "🚶🏿\u200d♀\u200d➡ (:woman_walking_facing_right_dark_skin_tone:)", + ), + ( + "🚶🏻\u200d♀️\u200d➡️", + "🚶🏻\u200d♀️\u200d➡️ (:woman_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♀\u200d➡️", + "🚶🏻\u200d♀\u200d➡️ (:woman_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♀️\u200d➡", + "🚶🏻\u200d♀️\u200d➡ (:woman_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♀\u200d➡", + "🚶🏻\u200d♀\u200d➡ (:woman_walking_facing_right_light_skin_tone:)", + ), + ( + "🚶🏾\u200d♀️\u200d➡️", + "🚶🏾\u200d♀️\u200d➡️ (:woman_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♀\u200d➡️", + "🚶🏾\u200d♀\u200d➡️ (:woman_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♀️\u200d➡", + "🚶🏾\u200d♀️\u200d➡ (:woman_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♀\u200d➡", + "🚶🏾\u200d♀\u200d➡ (:woman_walking_facing_right_medium-dark_skin_tone:)", + ), + ( + "🚶🏼\u200d♀️\u200d➡️", + "🚶🏼\u200d♀️\u200d➡️ (:woman_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♀\u200d➡️", + "🚶🏼\u200d♀\u200d➡️ (:woman_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♀️\u200d➡", + "🚶🏼\u200d♀️\u200d➡ (:woman_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♀\u200d➡", + "🚶🏼\u200d♀\u200d➡ (:woman_walking_facing_right_medium-light_skin_tone:)", + ), + ( + "🚶🏽\u200d♀️\u200d➡️", + "🚶🏽\u200d♀️\u200d➡️ (:woman_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♀\u200d➡️", + "🚶🏽\u200d♀\u200d➡️ (:woman_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♀️\u200d➡", + "🚶🏽\u200d♀️\u200d➡ (:woman_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♀\u200d➡", + "🚶🏽\u200d♀\u200d➡ (:woman_walking_facing_right_medium_skin_tone:)", + ), + ( + "🚶🏻\u200d♀️", + "🚶🏻\u200d♀️ (:woman_walking_light_skin_tone:)", + ), + ( + "🚶🏻\u200d♀", + "🚶🏻\u200d♀ (:woman_walking_light_skin_tone:)", + ), + ( + "🚶🏾\u200d♀️", + "🚶🏾\u200d♀️ (:woman_walking_medium-dark_skin_tone:)", + ), + ( + "🚶🏾\u200d♀", + "🚶🏾\u200d♀ (:woman_walking_medium-dark_skin_tone:)", + ), + ( + "🚶🏼\u200d♀️", + "🚶🏼\u200d♀️ (:woman_walking_medium-light_skin_tone:)", + ), + ( + "🚶🏼\u200d♀", + "🚶🏼\u200d♀ (:woman_walking_medium-light_skin_tone:)", + ), + ( + "🚶🏽\u200d♀️", + "🚶🏽\u200d♀️ (:woman_walking_medium_skin_tone:)", + ), + ( + "🚶🏽\u200d♀", + "🚶🏽\u200d♀ (:woman_walking_medium_skin_tone:)", + ), + ("👳\u200d♀️", "👳\u200d♀️ (:woman_wearing_turban:)"), + ("👳\u200d♀", "👳\u200d♀ (:woman_wearing_turban:)"), + ( + "👳🏿\u200d♀️", + "👳🏿\u200d♀️ (:woman_wearing_turban_dark_skin_tone:)", + ), + ( + "👳🏿\u200d♀", + "👳🏿\u200d♀ (:woman_wearing_turban_dark_skin_tone:)", + ), + ( + "👳🏻\u200d♀️", + "👳🏻\u200d♀️ (:woman_wearing_turban_light_skin_tone:)", + ), + ( + "👳🏻\u200d♀", + "👳🏻\u200d♀ (:woman_wearing_turban_light_skin_tone:)", + ), + ( + "👳🏾\u200d♀️", + "👳🏾\u200d♀️ (:woman_wearing_turban_medium-dark_skin_tone:)", + ), + ( + "👳🏾\u200d♀", + "👳🏾\u200d♀ (:woman_wearing_turban_medium-dark_skin_tone:)", + ), + ( + "👳🏼\u200d♀️", + "👳🏼\u200d♀️ (:woman_wearing_turban_medium-light_skin_tone:)", + ), + ( + "👳🏼\u200d♀", + "👳🏼\u200d♀ (:woman_wearing_turban_medium-light_skin_tone:)", + ), + ( + "👳🏽\u200d♀️", + "👳🏽\u200d♀️ (:woman_wearing_turban_medium_skin_tone:)", + ), + ( + "👳🏽\u200d♀", + "👳🏽\u200d♀ (:woman_wearing_turban_medium_skin_tone:)", + ), + ("👩\u200d🦳", "👩\u200d🦳 (:woman_white_hair:)"), + ("🧕", "🧕 (:woman_with_headscarf:)"), + ("🧕🏿", "🧕🏿 (:woman_with_headscarf_dark_skin_tone:)"), + ("🧕🏻", "🧕🏻 (:woman_with_headscarf_light_skin_tone:)"), + ("🧕🏾", "🧕🏾 (:woman_with_headscarf_medium-dark_skin_tone:)"), + ( + "🧕🏼", + "🧕🏼 (:woman_with_headscarf_medium-light_skin_tone:)", + ), + ("🧕🏽", "🧕🏽 (:woman_with_headscarf_medium_skin_tone:)"), + ("👰\u200d♀️", "👰\u200d♀️ (:woman_with_veil:)"), + ("👰\u200d♀", "👰\u200d♀ (:woman_with_veil:)"), + ( + "👰🏿\u200d♀️", + "👰🏿\u200d♀️ (:woman_with_veil_dark_skin_tone:)", + ), + ( + "👰🏿\u200d♀", + "👰🏿\u200d♀ (:woman_with_veil_dark_skin_tone:)", + ), + ( + "👰🏻\u200d♀️", + "👰🏻\u200d♀️ (:woman_with_veil_light_skin_tone:)", + ), + ( + "👰🏻\u200d♀", + "👰🏻\u200d♀ (:woman_with_veil_light_skin_tone:)", + ), + ( + "👰🏾\u200d♀️", + "👰🏾\u200d♀️ (:woman_with_veil_medium-dark_skin_tone:)", + ), + ( + "👰🏾\u200d♀", + "👰🏾\u200d♀ (:woman_with_veil_medium-dark_skin_tone:)", + ), + ( + "👰🏼\u200d♀️", + "👰🏼\u200d♀️ (:woman_with_veil_medium-light_skin_tone:)", + ), + ( + "👰🏼\u200d♀", + "👰🏼\u200d♀ (:woman_with_veil_medium-light_skin_tone:)", + ), + ( + "👰🏽\u200d♀️", + "👰🏽\u200d♀️ (:woman_with_veil_medium_skin_tone:)", + ), + ( + "👰🏽\u200d♀", + "👰🏽\u200d♀ (:woman_with_veil_medium_skin_tone:)", + ), + ("👩\u200d🦯", "👩\u200d🦯 (:woman_with_white_cane:)"), + ( + "👩🏿\u200d🦯", + "👩🏿\u200d🦯 (:woman_with_white_cane_dark_skin_tone:)", + ), + ( + "👩\u200d🦯\u200d➡️", + "👩\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right:)", + ), + ( + "👩\u200d🦯\u200d➡", + "👩\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right:)", + ), + ( + "👩🏿\u200d🦯\u200d➡️", + "👩🏿\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "👩🏿\u200d🦯\u200d➡", + "👩🏿\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🦯\u200d➡️", + "👩🏻\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "👩🏻\u200d🦯\u200d➡", + "👩🏻\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦯\u200d➡️", + "👩🏾\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏾\u200d🦯\u200d➡", + "👩🏾\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦯\u200d➡️", + "👩🏼\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏼\u200d🦯\u200d➡", + "👩🏼\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦯\u200d➡️", + "👩🏽\u200d🦯\u200d➡️ (:woman_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "👩🏽\u200d🦯\u200d➡", + "👩🏽\u200d🦯\u200d➡ (:woman_with_white_cane_facing_right_medium_skin_tone:)", + ), + ( + "👩🏻\u200d🦯", + "👩🏻\u200d🦯 (:woman_with_white_cane_light_skin_tone:)", + ), + ( + "👩🏾\u200d🦯", + "👩🏾\u200d🦯 (:woman_with_white_cane_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🦯", + "👩🏼\u200d🦯 (:woman_with_white_cane_medium-light_skin_tone:)", + ), + ( + "👩🏽\u200d🦯", + "👩🏽\u200d🦯 (:woman_with_white_cane_medium_skin_tone:)", + ), + ("🧟\u200d♀️", "🧟\u200d♀️ (:woman_zombie:)"), + ("🧟\u200d♀", "🧟\u200d♀ (:woman_zombie:)"), + ("👢", "👢 (:woman’s_boot:)"), + ("👚", "👚 (:woman’s_clothes:)"), + ("👒", "👒 (:woman’s_hat:)"), + ("👡", "👡 (:woman’s_sandal:)"), + ("👭", "👭 (:women_holding_hands:)"), + ("👭🏿", "👭🏿 (:women_holding_hands_dark_skin_tone:)"), + ( + "👩🏿\u200d🤝\u200d👩🏻", + "👩🏿\u200d🤝\u200d👩🏻 (:women_holding_hands_dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👩🏾", + "👩🏿\u200d🤝\u200d👩🏾 (:women_holding_hands_dark_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👩🏼", + "👩🏿\u200d🤝\u200d👩🏼 (:women_holding_hands_dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏿\u200d🤝\u200d👩🏽", + "👩🏿\u200d🤝\u200d👩🏽 (:women_holding_hands_dark_skin_tone_medium_skin_tone:)", + ), + ("👭🏻", "👭🏻 (:women_holding_hands_light_skin_tone:)"), + ( + "👩🏻\u200d🤝\u200d👩🏿", + "👩🏻\u200d🤝\u200d👩🏿 (:women_holding_hands_light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👩🏾", + "👩🏻\u200d🤝\u200d👩🏾 (:women_holding_hands_light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👩🏼", + "👩🏻\u200d🤝\u200d👩🏼 (:women_holding_hands_light_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏻\u200d🤝\u200d👩🏽", + "👩🏻\u200d🤝\u200d👩🏽 (:women_holding_hands_light_skin_tone_medium_skin_tone:)", + ), + ("👭🏾", "👭🏾 (:women_holding_hands_medium-dark_skin_tone:)"), + ( + "👩🏾\u200d🤝\u200d👩🏿", + "👩🏾\u200d🤝\u200d👩🏿 (:women_holding_hands_medium-dark_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👩🏻", + "👩🏾\u200d🤝\u200d👩🏻 (:women_holding_hands_medium-dark_skin_tone_light_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👩🏼", + "👩🏾\u200d🤝\u200d👩🏼 (:women_holding_hands_medium-dark_skin_tone_medium-light_skin_tone:)", + ), + ( + "👩🏾\u200d🤝\u200d👩🏽", + "👩🏾\u200d🤝\u200d👩🏽 (:women_holding_hands_medium-dark_skin_tone_medium_skin_tone:)", + ), + ("👭🏼", "👭🏼 (:women_holding_hands_medium-light_skin_tone:)"), + ( + "👩🏼\u200d🤝\u200d👩🏿", + "👩🏼\u200d🤝\u200d👩🏿 (:women_holding_hands_medium-light_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👩🏻", + "👩🏼\u200d🤝\u200d👩🏻 (:women_holding_hands_medium-light_skin_tone_light_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👩🏾", + "👩🏼\u200d🤝\u200d👩🏾 (:women_holding_hands_medium-light_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏼\u200d🤝\u200d👩🏽", + "👩🏼\u200d🤝\u200d👩🏽 (:women_holding_hands_medium-light_skin_tone_medium_skin_tone:)", + ), + ("👭🏽", "👭🏽 (:women_holding_hands_medium_skin_tone:)"), + ( + "👩🏽\u200d🤝\u200d👩🏿", + "👩🏽\u200d🤝\u200d👩🏿 (:women_holding_hands_medium_skin_tone_dark_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👩🏻", + "👩🏽\u200d🤝\u200d👩🏻 (:women_holding_hands_medium_skin_tone_light_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👩🏾", + "👩🏽\u200d🤝\u200d👩🏾 (:women_holding_hands_medium_skin_tone_medium-dark_skin_tone:)", + ), + ( + "👩🏽\u200d🤝\u200d👩🏼", + "👩🏽\u200d🤝\u200d👩🏼 (:women_holding_hands_medium_skin_tone_medium-light_skin_tone:)", + ), + ("👯\u200d♀️", "👯\u200d♀️ (:women_with_bunny_ears:)"), + ("👯\u200d♀", "👯\u200d♀ (:women_with_bunny_ears:)"), + ("🤼\u200d♀️", "🤼\u200d♀️ (:women_wrestling:)"), + ("🤼\u200d♀", "🤼\u200d♀ (:women_wrestling:)"), + ("🚺", "🚺 (:women’s_room:)"), + ("🪵", "🪵 (:wood:)"), + ("🥴", "🥴 (:woozy_face:)"), + ("🗺️", "🗺️ (:world_map:)"), + ("🗺", "🗺 (:world_map:)"), + ("🪱", "🪱 (:worm:)"), + ("😟", "😟 (:worried_face:)"), + ("🎁", "🎁 (:wrapped_gift:)"), + ("🔧", "🔧 (:wrench:)"), + ("✍️", "✍️ (:writing_hand:)"), + ("✍", "✍ (:writing_hand:)"), + ("✍🏿", "✍🏿 (:writing_hand_dark_skin_tone:)"), + ("✍🏻", "✍🏻 (:writing_hand_light_skin_tone:)"), + ("✍🏾", "✍🏾 (:writing_hand_medium-dark_skin_tone:)"), + ("✍🏼", "✍🏼 (:writing_hand_medium-light_skin_tone:)"), + ("✍🏽", "✍🏽 (:writing_hand_medium_skin_tone:)"), + ("\U0001fa7b", "\U0001fa7b (:x-ray:)"), + ("🧶", "🧶 (:yarn:)"), + ("🥱", "🥱 (:yawning_face:)"), + ("🟡", "🟡 (:yellow_circle:)"), + ("💛", "💛 (:yellow_heart:)"), + ("🟨", "🟨 (:yellow_square:)"), + ("💴", "💴 (:yen_banknote:)"), + ("☯️", "☯️ (:yin_yang:)"), + ("☯", "☯ (:yin_yang:)"), + ("🪀", "🪀 (:yo-yo:)"), + ("🤪", "🤪 (:zany_face:)"), + ("🦓", "🦓 (:zebra:)"), + ("🤐", "🤐 (:zipper-mouth_face:)"), + ("🧟", "🧟 (:zombie:)"), + ("🇦🇽", "🇦🇽 (:Åland_Islands:)"), + ], + default="🚢", + help_text="크루 아이콘을 입력해주세요. (이모지)", + ), + ), + ( + "max_members", + models.IntegerField( + default=8, + help_text="크루 최대 인원을 입력해주세요.", + validators=[ + django.core.validators.MinValueValidator(1), + django.core.validators.MaxValueValidator(8), + ], + ), + ), + ( + "notice", + models.TextField( + blank=True, + help_text="크루 공지를 입력해주세요.", + max_length=500, + null=True, + ), + ), + ( + "custom_tags", + models.JSONField(blank=True, default=list, help_text="태그를 입력해주세요."), + ), + ( + "min_boj_level", + models.IntegerField( + choices=[ + (0, "Unrated"), + (1, "브론즈 5"), + (2, "브론즈 4"), + (3, "브론즈 3"), + (4, "브론즈 2"), + (5, "브론즈 1"), + (6, "실버 5"), + (7, "실버 4"), + (8, "실버 3"), + (9, "실버 2"), + (10, "실버 1"), + (11, "골드 5"), + (12, "골드 4"), + (13, "골드 3"), + (14, "골드 2"), + (15, "골드 1"), + (16, "플래티넘 5"), + (17, "플래티넘 4"), + (18, "플래티넘 3"), + (19, "플래티넘 2"), + (20, "플래티넘 1"), + (21, "다이아몬드 5"), + (22, "다이아몬드 4"), + (23, "다이아몬드 3"), + (24, "다이아몬드 2"), + (25, "다이아몬드 1"), + (26, "루비 5"), + (27, "루비 4"), + (28, "루비 3"), + (29, "루비 2"), + (30, "루비 1"), + (31, "마스터"), + ], + default=0, + help_text="최소 백준 레벨을 입력해주세요. 0: Unranked, 1: Bronze V, 2: Bronze IV, ..., 6: Silver V, ..., 30: Ruby I", + ), + ), + ( + "is_recruiting", + models.BooleanField(default=True, help_text="모집 중 여부를 입력해주세요."), + ), + ( + "is_active", + models.BooleanField(default=True, help_text="활동 중인지 여부를 입력해주세요."), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "created_by", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-updated_at"], + }, + ), + migrations.CreateModel( + name="CrewSubmittableLanguage", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "language", + models.TextField( + choices=[ + ("nodejs", "Node.js"), + ("kotlin", "Kotlin"), + ("swift", "Swift"), + ("cpp", "C++"), + ("java", "Java"), + ("python", "Python"), + ("c", "C"), + ("javascript", "JavaScript"), + ("csharp", "C#"), + ("ruby", "Ruby"), + ("php", "PHP"), + ], + help_text="언어 키를 입력해주세요. (최대 20자)", + ), + ), + ( + "crew", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, to="crews.crew" + ), + ), + ], + options={ + "ordering": ["crew"], + }, + ), + migrations.CreateModel( + name="CrewMember", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("is_captain", models.BooleanField(default=False, help_text="크루장 여부")), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "crew", + models.ForeignKey( + help_text="크루를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to="crews.crew", + ), + ), + ( + "user", + models.ForeignKey( + help_text="유저를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["created_at"], + }, + ), + migrations.AddConstraint( + model_name="crewmember", + constraint=models.UniqueConstraint( + fields=("crew", "user"), name="unique_member_per_crew" + ), + ), + ] diff --git a/app/problems/analyses/migrations/0001_initial.py b/app/problems/analyses/migrations/0001_initial.py new file mode 100644 index 0000000..7ccbfed --- /dev/null +++ b/app/problems/analyses/migrations/0001_initial.py @@ -0,0 +1,157 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="ProblemAnalysis", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "difficulty", + models.IntegerField( + choices=[(0, "분석 중"), (1, "쉬움"), (2, "보통"), (3, "어려움")], + help_text="문제 난이도를 입력해주세요.", + ), + ), + ( + "time_complexity", + models.CharField( + help_text=( + "문제 시간 복잡도를 입력해주세요. ", + "예) O(1), O(n), O(n^2), O(V \\log E) 등", + ), + max_length=100, + ), + ), + ( + "hint", + models.JSONField( + default=list, help_text="문제 힌트를 입력해주세요. Step-by-step 으로 입력해주세요." + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + options={ + "verbose_name_plural": "Problem analyses", + "ordering": ["-created_at"], + "get_latest_by": ["created_at"], + }, + ), + migrations.CreateModel( + name="ProblemTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "key", + models.CharField( + help_text="알고리즘 태그 키를 입력해주세요. (최대 20자)", + max_length=50, + unique=True, + ), + ), + ( + "name_ko", + models.CharField( + help_text="알고리즘 태그 이름(국문)을 입력해주세요. (최대 50자)", + max_length=50, + unique=True, + ), + ), + ( + "name_en", + models.CharField( + help_text="알고리즘 태그 이름(영문)을 입력해주세요. (최대 50자)", + max_length=50, + unique=True, + ), + ), + ], + options={ + "ordering": ["key"], + }, + ), + migrations.CreateModel( + name="ProblemTagRelation", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "child", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="child", + to="analyses.problemtag", + ), + ), + ( + "parent", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="parent", + to="analyses.problemtag", + ), + ), + ], + ), + migrations.CreateModel( + name="ProblemAnalysisTag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "analysis", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="analyses.problemanalysis", + ), + ), + ( + "tag", + models.ForeignKey( + help_text="문제의 DSA 태그를 입력해주세요.", + on_delete=django.db.models.deletion.PROTECT, + to="analyses.problemtag", + ), + ), + ], + ), + ] diff --git a/app/problems/analyses/migrations/0002_initial.py b/app/problems/analyses/migrations/0002_initial.py new file mode 100644 index 0000000..cd51625 --- /dev/null +++ b/app/problems/analyses/migrations/0002_initial.py @@ -0,0 +1,26 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("analyses", "0001_initial"), + ("problems", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="problemanalysis", + name="problem", + field=models.ForeignKey( + help_text="문제를 입력해주세요.", + on_delete=django.db.models.deletion.CASCADE, + to="problems.problem", + ), + ), + ] diff --git a/app/problems/migrations/0001_initial.py b/app/problems/migrations/0001_initial.py new file mode 100644 index 0000000..458d9fd --- /dev/null +++ b/app/problems/migrations/0001_initial.py @@ -0,0 +1,78 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Problem", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("title", models.CharField(help_text="문제 이름을 입력해주세요.", max_length=100)), + ("link", models.URLField(blank=True, help_text="문제 링크를 입력해주세요. (선택)")), + ("description", models.TextField(help_text="문제 설명을 입력해주세요.")), + ( + "input_description", + models.TextField(blank=True, help_text="문제 입력 설명을 입력해주세요."), + ), + ( + "output_description", + models.TextField(blank=True, help_text="문제 출력 설명을 입력해주세요."), + ), + ( + "memory_limit", + models.FloatField(help_text="문제 메모리 제한을 입력해주세요. (MB 단위)"), + ), + ( + "memory_limit_unit", + models.TextField( + choices=[("MB", "메가 바이트"), ("s", "초")], default="MB" + ), + ), + ( + "time_limit", + models.FloatField( + default=1.0, help_text="문제 시간 제한을 입력해주세요. (초 단위)" + ), + ), + ( + "time_limit_unit", + models.TextField( + choices=[("MB", "메가 바이트"), ("s", "초")], default="s" + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("updated_at", models.DateTimeField(auto_now=True)), + ( + "created_by", + models.ForeignKey( + help_text="이 문제를 추가한 사용자를 입력해주세요.", + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["-created_at"], + }, + ), + ] diff --git a/app/users/migrations/0001_initial.py b/app/users/migrations/0001_initial.py new file mode 100644 index 0000000..07fd031 --- /dev/null +++ b/app/users/migrations/0001_initial.py @@ -0,0 +1,120 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models +import django.utils.timezone +import users.models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("auth", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="UserEmailVerification", + fields=[ + ( + "email", + models.EmailField( + help_text="이메일 주소", + max_length=254, + primary_key=True, + serialize=False, + ), + ), + ( + "verification_code", + models.TextField(blank=True, help_text="인증 코드", null=True), + ), + ( + "verification_token", + models.TextField(blank=True, help_text="인증 토큰", null=True), + ), + ("expires_at", models.DateTimeField(blank=True, null=True)), + ("created_at", models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name="User", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("password", models.CharField(max_length=128, verbose_name="password")), + ( + "last_login", + models.DateTimeField( + blank=True, null=True, verbose_name="last login" + ), + ), + ( + "username", + models.CharField( + max_length=30, unique=True, verbose_name="username" + ), + ), + ( + "email", + models.EmailField( + max_length=255, unique=True, verbose_name="email address" + ), + ), + ( + "profile_image", + models.ImageField( + blank=True, + help_text="프로필 이미지", + null=True, + upload_to=users.models.get_profile_image_path, + ), + ), + ("boj_username", models.CharField(help_text="백준 아이디", max_length=40)), + ("token", models.TextField(blank=True, default=None, null=True)), + ( + "refresh_token", + models.TextField(blank=True, default=None, null=True), + ), + ("is_active", models.BooleanField(default=True)), + ("is_staff", models.BooleanField(default=False)), + ("is_superuser", models.BooleanField(default=False)), + ("first_name", models.TextField(blank=True, default=None, null=True)), + ("last_name", models.TextField(blank=True, default=None, null=True)), + ("created_at", models.DateTimeField(default=django.utils.timezone.now)), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "abstract": False, + }, + ), + ] From 3072778d1d5780fc1fd7bf21565b1eedaf5982e2 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 07:44:37 +0900 Subject: [PATCH 525/552] =?UTF-8?q?chore(config.settings):=20=EA=B8=B0?= =?UTF-8?q?=EB=B3=B8=20=EB=A1=9C=EA=B9=85=EC=9D=B4=20=EB=94=94=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EB=AA=A8=EB=93=9C=EA=B0=80=20=EC=95=84=EB=8B=90=20?= =?UTF-8?q?=EB=95=8C=EC=97=90=EB=8F=84=20=EC=9D=B4=EB=A3=A8=EC=96=B4?= =?UTF-8?q?=EC=A7=80=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/logging.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/config/settings/base/logging.py b/app/config/settings/base/logging.py index 51dfe85..5319145 100644 --- a/app/config/settings/base/logging.py +++ b/app/config/settings/base/logging.py @@ -24,14 +24,13 @@ "handlers": { "console": { "level": "INFO", - "filters": ["require_debug_true"], - 'class': 'config.utils.FileAndStreamHandler', + 'class': 'logging.handlers.TimedRotatingFileHandler', 'filename': 'logs/console.log', - "formatter": "standard", + 'when': 'D', + "formatter": "django.server", }, "django.mail": { "level": "ERROR", - "filters": ["require_debug_true"], 'class': 'config.utils.FileAndStreamHandler', 'filename': 'logs/django.mail.log', }, From 5347dc8a20a509ea212cae3c495ec04093dd31c3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 09:06:50 +0900 Subject: [PATCH 526/552] =?UTF-8?q?ci(config.settings):=20uWSGI=20?= =?UTF-8?q?=EB=A1=9C=20=EB=B0=B0=ED=8F=AC=ED=95=A0=20=EB=95=8C=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=ED=95=98=EB=8A=94=20=EC=84=A4=EC=A0=95=20=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/uwsgi.ini | 20 ++++++++++++++++++++ app/config/uwsgi.service | 21 +++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 app/config/uwsgi.ini create mode 100644 app/config/uwsgi.service diff --git a/app/config/uwsgi.ini b/app/config/uwsgi.ini new file mode 100644 index 0000000..f627134 --- /dev/null +++ b/app/config/uwsgi.ini @@ -0,0 +1,20 @@ +[uwsgi] +chdir = /root/be-django/app/ +module = config.wsgi:application +home = /root/be-django/venv/ + +uid = root +gid = root + +socket = /tmp/tle.socket +chmod-socket = 666 +chown-socket = root:root + +http = :80 + +enable-threads = true +master = true +vacuum = true +pidfile = /tmp/be-django.pid +logto = /root/be-django/app/logs/uwsgi-@(exec://date +%%Y-%%m-%%d).log +log-reopen = true diff --git a/app/config/uwsgi.service b/app/config/uwsgi.service new file mode 100644 index 0000000..7d3a838 --- /dev/null +++ b/app/config/uwsgi.service @@ -0,0 +1,21 @@ +; should be copied to /etc/systemd/system/uwsgi.service +; then... +; systemctl daemon-reload +; systemctl enable uwsgi +; systemctl restart uwsgi + +[Unit] +Description=uWSGI service +After=syslog.target + +[Service] +ExecStart=/root/be-django/venv/bin/uwsgi -i /root/be-django/app/config/uwsgi.ini + +Restart=always +KillSignal=SIGQUIT +Type=notify +StandardError=syslog +NotifyAccess=all + +[Install] +WantedBy=multi-user.target \ No newline at end of file From 99c2d4092a4cbc959c60bffa095857d8bc1ba099 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 09:11:51 +0900 Subject: [PATCH 527/552] =?UTF-8?q?chore(users):=20UsabilityAPIView=20docs?= =?UTF-8?q?tring=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/users/views.py b/app/users/views.py index bf8c9cc..6c8679e 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -23,7 +23,11 @@ class UsabilityAPIView(generics.RetrieveAPIView): - """이메일/사용자명이 사용 가능한지 조회하는 API""" + """이메일/사용자명이 사용 가능한지 조회하는 API. + + 이메일 혹은 사용자명 중 하나만 입력해도 동작하지만, + 둘 다 입력하지 않을 경우 400 BAD_REQUEST를 반환한다. + """ permission_classes = [AllowAny] serializer_class = UsabilitySerializer From 3d3fd3fcd623835475c8bf7e20dd9c30795e480f Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 09:13:29 +0900 Subject: [PATCH 528/552] =?UTF-8?q?chore(users):=20users=20=EC=95=B1=20vie?= =?UTF-8?q?ws=20=EB=93=A4=EC=9D=98=20=EC=84=A4=EB=AA=85=EC=9D=B4=20swagger?= =?UTF-8?q?=20=ED=99=88=EC=97=90=EC=84=9C=EB=8F=84=20=EB=B3=B4=EC=9D=B4?= =?UTF-8?q?=EA=B2=8C=20docstring=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/views.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/app/users/views.py b/app/users/views.py index 6c8679e..5b05a20 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -63,7 +63,10 @@ def post(self, request, *args, **kwargs): class SignInAPIView(generics.GenericAPIView): - """사용자 로그인 API""" + """사용자 로그인 API. + + . + """ authentication_classes = [] permission_classes = [AllowAny] serializer_class = SignInSerializer @@ -84,14 +87,20 @@ def perform_login(self, serializer: SignInSerializer): class SignUpAPIView(generics.CreateAPIView): - """사용자 등록(회원가입) API""" + """사용자 등록(회원가입) API. + + . + """ authentication_classes = [] permission_classes = [AllowAny] serializer_class = SignUpSerializer class SignOutAPIView(generics.GenericAPIView): - """사용자 로그아웃 API""" + """사용자 로그아웃 API. + + . + """ permission_classes = [IsAuthenticated] def get(self, request, *args, **kwargs): @@ -100,7 +109,10 @@ def get(self, request, *args, **kwargs): class UserManageAPIView(generics.RetrieveUpdateAPIView): - """현재 로그인한 사용자 정보를 조회/수정하는 API""" + """현재 로그인한 사용자 정보를 조회/수정하는 API. + + . + """ permission_classes = [IsAuthenticated] serializer_class = UserUpdateSerializer From 8fd9d94f74792d0a9588ae71cfd058c9e985740d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 09:27:35 +0900 Subject: [PATCH 529/552] =?UTF-8?q?fix(users):=20=EC=8B=9C=EA=B7=B8?= =?UTF-8?q?=EB=84=90=20=EB=A6=AC=EC=8B=9C=EB=B2=84=EA=B0=80=20=EB=A1=9C?= =?UTF-8?q?=EB=93=9C=EB=90=98=EC=A7=80=20=EC=95=8A=EC=95=84=20=EC=9D=B4?= =?UTF-8?q?=EB=A9=94=EC=9D=BC=20=EC=9D=B8=EC=A6=9D=EC=BD=94=EB=93=9C?= =?UTF-8?q?=EA=B0=80=20=EB=B0=9C=EC=86=A1=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/apps.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/users/apps.py b/app/users/apps.py index 88f7b17..c1e0f7e 100644 --- a/app/users/apps.py +++ b/app/users/apps.py @@ -4,3 +4,6 @@ class UsersConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "users" + + def ready(self) -> None: + import users.services From 005d79289051cd9af01f793fb2ffc8be87323138 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 09:47:57 +0900 Subject: [PATCH 530/552] =?UTF-8?q?fix(config):=20service=20=EC=97=90?= =?UTF-8?q?=EC=84=9C=EB=8F=84=20=EB=A1=9C=EA=B9=85=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EA=B0=80=20=EC=98=AC=EB=B0=94=EB=A5=B4=EA=B2=8C=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=EB=90=A0=20=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/__init__.py | 194 +-------------------------- app/config/settings/base/core.py | 192 ++++++++++++++++++++++++++ app/config/settings/base/logging.py | 15 ++- 3 files changed, 202 insertions(+), 199 deletions(-) create mode 100644 app/config/settings/base/core.py diff --git a/app/config/settings/base/__init__.py b/app/config/settings/base/__init__.py index 9d6fd07..e4edbfa 100644 --- a/app/config/settings/base/__init__.py +++ b/app/config/settings/base/__init__.py @@ -1,194 +1,2 @@ -""" -Django settings for project. - -Generated by 'django-admin startproject' using Django 4.2. - -For more information on this file, see -https://docs.djangoproject.com/en/4.2/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.2/ref/settings/ -""" - -from datetime import timedelta -from pathlib import Path - +from config.settings.base.core import * from config.settings.base.logging import * - - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ - - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - -ALLOWED_HOSTS = [] - - -# CORS - -CORS_ORIGIN_ALLOW_ALL = True -CORS_ALLOW_CREDENTIALS = True - - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - - "corsheaders", - "drf_yasg", - 'background_task', - "rest_framework", - 'rest_framework_simplejwt', - - "boj", - "crews", - "crews.activities", - "crews.applications", - "users", - "problems", - "problems.analyses", -] - -MIDDLEWARE = [ - "corsheaders.middleware.CorsMiddleware", - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "config.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "config.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/4.2/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - -AUTH_USER_MODEL = 'users.User' - -AUTHENTICATION_BACKENDS = [ - 'users.backends.UserAuthBackend', -] - -SIMPLE_JWT = { - 'ACCESS_TOKEN_LIFETIME': timedelta(days=60), - 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=100), - 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=100), - 'SLIDING_TOKEN_REFRESH_MAX_LIFETIME': timedelta(days=200), -} - -REST_FRAMEWORK = { - 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'rest_framework_simplejwt.authentication.JWTAuthentication', - 'rest_framework.authentication.SessionAuthentication', - 'rest_framework.authentication.BasicAuthentication', - ), -} - - -# Internationalization -# https://docs.djangoproject.com/en/4.2/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = 'Asia/Seoul' - -USE_I18N = True - -USE_TZ = False - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.2/howto/static-files/ - -STATIC_URL = "static/" - -STATIC_ROOT = BASE_DIR / '.static' - - -# Meida files (Images) -# https://docs.djangoproject.com/en/4.2/topics/files/ - -MEDIA_URL = "media/" - -MEDIA_ROOT = BASE_DIR / '.media' - - -# Default primary key field type -# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" - - -DEFAULT_EXCEPTION_REPORTER = "config.utils.NACLExceptionReporter" - -APPEND_SLASH = False - -# Swagger Settings (DRf-YASG) - -SWAGGER_SETTINGS = { - "LOGIN_URL": "/api/v1/auth/signin", - "LOGOUT_URL": "/api/v1/auth/signout", -} - - -#Django Background Tasks - -BACKGROUND_TASK_ASYNC_THREADS = 1 diff --git a/app/config/settings/base/core.py b/app/config/settings/base/core.py new file mode 100644 index 0000000..f65e583 --- /dev/null +++ b/app/config/settings/base/core.py @@ -0,0 +1,192 @@ +""" +Django settings for project. + +Generated by 'django-admin startproject' using Django 4.2. + +For more information on this file, see +https://docs.djangoproject.com/en/4.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.2/ref/settings/ +""" + +from datetime import timedelta +from pathlib import Path + + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ + + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = False + +ALLOWED_HOSTS = [] + + +# CORS + +CORS_ORIGIN_ALLOW_ALL = True +CORS_ALLOW_CREDENTIALS = True + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + + "corsheaders", + "drf_yasg", + 'background_task', + "rest_framework", + 'rest_framework_simplejwt', + + "boj", + "crews", + "crews.activities", + "crews.applications", + "users", + "problems", + "problems.analyses", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "config.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "config.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + +AUTH_USER_MODEL = 'users.User' + +AUTHENTICATION_BACKENDS = [ + 'users.backends.UserAuthBackend', +] + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(days=60), + 'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_LIFETIME_GRACE_PERIOD': timedelta(days=100), + 'SLIDING_TOKEN_REFRESH_MAX_LIFETIME': timedelta(days=200), +} + +REST_FRAMEWORK = { + 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'rest_framework_simplejwt.authentication.JWTAuthentication', + 'rest_framework.authentication.SessionAuthentication', + 'rest_framework.authentication.BasicAuthentication', + ), +} + + +# Internationalization +# https://docs.djangoproject.com/en/4.2/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = 'Asia/Seoul' + +USE_I18N = True + +USE_TZ = False + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.2/howto/static-files/ + +STATIC_URL = "static/" + +STATIC_ROOT = BASE_DIR / '.static' + + +# Meida files (Images) +# https://docs.djangoproject.com/en/4.2/topics/files/ + +MEDIA_URL = "media/" + +MEDIA_ROOT = BASE_DIR / '.media' + + +# Default primary key field type +# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +DEFAULT_EXCEPTION_REPORTER = "config.utils.NACLExceptionReporter" + +APPEND_SLASH = False + +# Swagger Settings (DRf-YASG) + +SWAGGER_SETTINGS = { + "LOGIN_URL": "/api/v1/auth/signin", + "LOGOUT_URL": "/api/v1/auth/signout", +} + + +#Django Background Tasks + +BACKGROUND_TASK_ASYNC_THREADS = 1 diff --git a/app/config/settings/base/logging.py b/app/config/settings/base/logging.py index 5319145..e4397cc 100644 --- a/app/config/settings/base/logging.py +++ b/app/config/settings/base/logging.py @@ -1,3 +1,6 @@ +from config.settings.base.core import BASE_DIR + + LOGGING = { "version": 1, "disable_existing_loggers": False, @@ -25,26 +28,26 @@ "console": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/console.log', + 'filename': BASE_DIR / 'logs/console.log', 'when': 'D', "formatter": "django.server", }, "django.mail": { "level": "ERROR", 'class': 'config.utils.FileAndStreamHandler', - 'filename': 'logs/django.mail.log', + 'filename': BASE_DIR / 'logs/django.mail.log', }, "django.server": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/django.server.log', + 'filename': BASE_DIR / 'logs/django.server.log', 'when': 'D', "formatter": "django.server", }, "problems": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/problems.log', + 'filename': BASE_DIR / 'logs/problems.log', 'when': 'D', "formatter": "standard", }, @@ -52,12 +55,12 @@ "level": "ERROR", "filters": ["require_debug_false"], 'class': 'logging.FileHandler', - 'filename': 'logs/mail_admins.log', + 'filename': BASE_DIR / 'logs/mail_admins.log', }, "django.security.DisallowedHost": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': 'logs/django.security.DisallowedHost.log', + 'filename': BASE_DIR / 'logs/django.security.DisallowedHost.log', 'when': 'D', "formatter": "standard", }, From a9f38eb858148b42274c4fe3738fbf7c25abbb19 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 10:39:02 +0900 Subject: [PATCH 531/552] =?UTF-8?q?fix(boj.services):=20=EC=97=86=EB=8A=94?= =?UTF-8?q?=20=EC=82=AC=EC=9A=A9=EC=9E=90=EB=AA=85=EC=97=90=20=EB=8C=80?= =?UTF-8?q?=ED=95=9C=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/services.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/app/boj/services.py b/app/boj/services.py index 75acfb1..39cc29e 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -1,9 +1,11 @@ +from json import JSONDecodeError from logging import getLogger from background_task import background from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone +from rest_framework import status import requests from boj.models import BOJUser @@ -35,13 +37,20 @@ def _update_boj_user_data(username: str): assert username.strip().isidentifier() instance = BOJUser.objects.get_by_username(username) url = f'https://solved.ac/api/v3/user/show?handle={username}' - data = requests.get(url).json() - try: - instance.level = data['tier'] - instance.rating = data['rating'] - instance.updated_at = timezone.now() - instance.save() - BOJUserSnapshot.objects.create_snapshot_of(instance) - except AssertionError: - # Solved.ac API 관련 문제일 가능성이 높다. - logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') \ No newline at end of file + res = requests.get(url) + if res.status_code == status.HTTP_404_NOT_FOUND: + logger.info(f'사용자 명이 "{username}"인 사용자가 존재하지 않습니다.') + else: + try: + data = res.json() + instance.level = data['tier'] + instance.rating = data['rating'] + instance.updated_at = timezone.now() + instance.save() + BOJUserSnapshot.objects.create_snapshot_of(instance) + except AssertionError: + # Solved.ac API 관련 문제일 가능성이 높다. + logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') + except JSONDecodeError: + logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') + logger.error(f'받은 데이터: "{res.content}"') From d38b62750be2cd6dbbba0d6f0f3f0d92f16f2de6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 22:43:02 +0900 Subject: [PATCH 532/552] =?UTF-8?q?refactor(background=5Ftask):=20PYPI?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=B9=98=ED=96=88=EB=8D=98=20=EB=AA=A8?= =?UTF-8?q?=EB=93=88=20background=5Ftask=EB=A5=BC=20=EC=8B=A4=EC=A0=9C=20?= =?UTF-8?q?=EC=95=B1=EC=9C=BC=EB=A1=9C=20=EA=B0=80=EC=A0=B8=EC=98=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/background_task/README.md | 3 + app/background_task/__init__.py | 8 + app/background_task/admin.py | 32 ++ app/background_task/apps.py | 10 + app/background_task/exceptions.py | 15 + app/background_task/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/process_tasks.py | 123 +++++ .../migrations/0001_initial.py | 167 +++++++ app/background_task/migrations/__init__.py | 0 app/background_task/models.py | 447 ++++++++++++++++++ app/background_task/settings.py | 61 +++ app/background_task/signals.py | 34 ++ app/background_task/tasks.py | 314 ++++++++++++ app/background_task/utils.py | 31 ++ requirements.txt | 3 +- 16 files changed, 1247 insertions(+), 1 deletion(-) create mode 100644 app/background_task/README.md create mode 100644 app/background_task/__init__.py create mode 100644 app/background_task/admin.py create mode 100644 app/background_task/apps.py create mode 100644 app/background_task/exceptions.py create mode 100644 app/background_task/management/__init__.py create mode 100644 app/background_task/management/commands/__init__.py create mode 100644 app/background_task/management/commands/process_tasks.py create mode 100644 app/background_task/migrations/0001_initial.py create mode 100644 app/background_task/migrations/__init__.py create mode 100644 app/background_task/models.py create mode 100644 app/background_task/settings.py create mode 100644 app/background_task/signals.py create mode 100644 app/background_task/tasks.py create mode 100644 app/background_task/utils.py diff --git a/app/background_task/README.md b/app/background_task/README.md new file mode 100644 index 0000000..16db90c --- /dev/null +++ b/app/background_task/README.md @@ -0,0 +1,3 @@ +# django-background-tasks + +Source: https://github.com/django-background-tasks/django-background-tasks diff --git a/app/background_task/__init__.py b/app/background_task/__init__.py new file mode 100644 index 0000000..febc2c0 --- /dev/null +++ b/app/background_task/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +__version__ = '1.2.8' + +default_app_config = 'background_task.apps.BackgroundTasksAppConfig' + +def background(*arg, **kw): + from background_task.tasks import tasks + return tasks.background(*arg, **kw) diff --git a/app/background_task/admin.py b/app/background_task/admin.py new file mode 100644 index 0000000..a7d6635 --- /dev/null +++ b/app/background_task/admin.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +from django.contrib import admin +from background_task.models import Task +from background_task.models import CompletedTask + + +def inc_priority(modeladmin, request, queryset): + for obj in queryset: + obj.priority += 1 + obj.save() +inc_priority.short_description = "priority += 1" + +def dec_priority(modeladmin, request, queryset): + for obj in queryset: + obj.priority -= 1 + obj.save() +dec_priority.short_description = "priority -= 1" + +class TaskAdmin(admin.ModelAdmin): + display_filter = ['task_name'] + search_fields = ['task_name', 'task_params', ] + list_display = ['task_name', 'task_params', 'run_at', 'priority', 'attempts', 'has_error', 'locked_by', 'locked_by_pid_running', ] + actions = [inc_priority, dec_priority] + +class CompletedTaskAdmin(admin.ModelAdmin): + display_filter = ['task_name'] + search_fields = ['task_name', 'task_params', ] + list_display = ['task_name', 'task_params', 'run_at', 'priority', 'attempts', 'has_error', 'locked_by', 'locked_by_pid_running', ] + + +admin.site.register(Task, TaskAdmin) +admin.site.register(CompletedTask, CompletedTaskAdmin) diff --git a/app/background_task/apps.py b/app/background_task/apps.py new file mode 100644 index 0000000..d5f7a5d --- /dev/null +++ b/app/background_task/apps.py @@ -0,0 +1,10 @@ +from django.apps import AppConfig + + +class BackgroundTasksAppConfig(AppConfig): + name = 'background_task' + from background_task import __version__ as version_info + verbose_name = 'Background Tasks ({})'.format(version_info) + + def ready(self): + import background_task.signals # noqa diff --git a/app/background_task/exceptions.py b/app/background_task/exceptions.py new file mode 100644 index 0000000..cf3b308 --- /dev/null +++ b/app/background_task/exceptions.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + + +class BackgroundTaskError(Exception): + + def __init__(self, message, errors=None): + super(BackgroundTaskError, self).__init__(message) + self.errors = errors + + +class InvalidTaskError(BackgroundTaskError): + """ + The task will not be rescheduled if it fails with this error + """ + pass diff --git a/app/background_task/management/__init__.py b/app/background_task/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/background_task/management/commands/__init__.py b/app/background_task/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/background_task/management/commands/process_tasks.py b/app/background_task/management/commands/process_tasks.py new file mode 100644 index 0000000..af9528f --- /dev/null +++ b/app/background_task/management/commands/process_tasks.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +import logging +import random +import sys +import time + +from django import VERSION +from django.core.management.base import BaseCommand +from django.utils import autoreload + +from background_task.tasks import tasks, autodiscover +from background_task.utils import SignalManager +from django.db import close_old_connections as close_connection + + +logger = logging.getLogger(__name__) + + +def _configure_log_std(): + class StdOutWrapper(object): + def write(self, s): + logger.info(s) + + class StdErrWrapper(object): + def write(self, s): + logger.error(s) + sys.stdout = StdOutWrapper() + sys.stderr = StdErrWrapper() + + +class Command(BaseCommand): + help = 'Run tasks that are scheduled to run on the queue' + + # Command options are specified in an abstract way to enable Django < 1.8 compatibility + OPTIONS = ( + (('--duration', ), { + 'action': 'store', + 'dest': 'duration', + 'type': int, + 'default': 0, + 'help': 'Run task for this many seconds (0 or less to run forever) - default is 0', + }), + (('--sleep', ), { + 'action': 'store', + 'dest': 'sleep', + 'type': float, + 'default': 5.0, + 'help': 'Sleep for this many seconds before checking for new tasks (if none were found) - default is 5', + }), + (('--queue', ), { + 'action': 'store', + 'dest': 'queue', + 'help': 'Only process tasks on this named queue', + }), + (('--log-std', ), { + 'action': 'store_true', + 'dest': 'log_std', + 'help': 'Redirect stdout and stderr to the logging system', + }), + (('--dev', ), { + 'action': 'store_true', + 'dest': 'dev', + 'help': 'Auto-reload your code on changes. Use this only for development', + }), + ) + + if VERSION < (1, 8): + from optparse import make_option + option_list = BaseCommand.option_list + tuple([make_option(*args, **kwargs) for args, kwargs in OPTIONS]) + + # Used in Django >= 1.8 + def add_arguments(self, parser): + for (args, kwargs) in self.OPTIONS: + parser.add_argument(*args, **kwargs) + + def __init__(self, *args, **kwargs): + super(Command, self).__init__(*args, **kwargs) + self.sig_manager = None + self._tasks = tasks + + def run(self, *args, **options): + duration = options.get('duration', 0) + sleep = options.get('sleep', 5.0) + queue = options.get('queue', None) + log_std = options.get('log_std', False) + is_dev = options.get('dev', False) + sig_manager = self.sig_manager + + if is_dev: + # raise last Exception is exist + autoreload.raise_last_exception() + + if log_std: + _configure_log_std() + + autodiscover() + + start_time = time.time() + + while (duration <= 0) or (time.time() - start_time) <= duration: + if sig_manager.kill_now: + # shutting down gracefully + break + + if not self._tasks.run_next_task(queue): + # there were no tasks in the queue, let's recover. + close_connection() + logger.debug('waiting for tasks') + time.sleep(sleep) + else: + # there were some tasks to process, let's check if there is more work to do after a little break. + time.sleep(random.uniform(sig_manager.time_to_wait[0], sig_manager.time_to_wait[1])) + + def handle(self, *args, **options): + is_dev = options.get('dev', False) + self.sig_manager = SignalManager() + if is_dev: + reload_func = autoreload.run_with_reloader + if VERSION < (2, 2): + reload_func = autoreload.main + reload_func(self.run, *args, **options) + else: + self.run(*args, **options) diff --git a/app/background_task/migrations/0001_initial.py b/app/background_task/migrations/0001_initial.py new file mode 100644 index 0000000..2e82c56 --- /dev/null +++ b/app/background_task/migrations/0001_initial.py @@ -0,0 +1,167 @@ +# Generated by Django 4.2.13 on 2024-09-01 07:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("contenttypes", "0002_remove_content_type_name"), + ] + + operations = [ + migrations.CreateModel( + name="Task", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("task_name", models.CharField(db_index=True, max_length=190)), + ("task_params", models.TextField()), + ("task_hash", models.CharField(db_index=True, max_length=40)), + ( + "verbose_name", + models.CharField(blank=True, max_length=255, null=True), + ), + ("priority", models.IntegerField(db_index=True, default=0)), + ("run_at", models.DateTimeField(db_index=True)), + ( + "repeat", + models.BigIntegerField( + choices=[ + (3600, "hourly"), + (86400, "daily"), + (604800, "weekly"), + (1209600, "every 2 weeks"), + (2419200, "every 4 weeks"), + (0, "never"), + ], + default=0, + ), + ), + ("repeat_until", models.DateTimeField(blank=True, null=True)), + ( + "queue", + models.CharField( + blank=True, db_index=True, max_length=190, null=True + ), + ), + ("attempts", models.IntegerField(db_index=True, default=0)), + ( + "failed_at", + models.DateTimeField(blank=True, db_index=True, null=True), + ), + ("last_error", models.TextField(blank=True)), + ( + "locked_by", + models.CharField( + blank=True, db_index=True, max_length=64, null=True + ), + ), + ( + "locked_at", + models.DateTimeField(blank=True, db_index=True, null=True), + ), + ( + "creator_object_id", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "creator_content_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="background_task", + to="contenttypes.contenttype", + ), + ), + ], + options={ + "db_table": "background_task", + }, + ), + migrations.CreateModel( + name="CompletedTask", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("task_name", models.CharField(db_index=True, max_length=190)), + ("task_params", models.TextField()), + ("task_hash", models.CharField(db_index=True, max_length=40)), + ( + "verbose_name", + models.CharField(blank=True, max_length=255, null=True), + ), + ("priority", models.IntegerField(db_index=True, default=0)), + ("run_at", models.DateTimeField(db_index=True)), + ( + "repeat", + models.BigIntegerField( + choices=[ + (3600, "hourly"), + (86400, "daily"), + (604800, "weekly"), + (1209600, "every 2 weeks"), + (2419200, "every 4 weeks"), + (0, "never"), + ], + default=0, + ), + ), + ("repeat_until", models.DateTimeField(blank=True, null=True)), + ( + "queue", + models.CharField( + blank=True, db_index=True, max_length=190, null=True + ), + ), + ("attempts", models.IntegerField(db_index=True, default=0)), + ( + "failed_at", + models.DateTimeField(blank=True, db_index=True, null=True), + ), + ("last_error", models.TextField(blank=True)), + ( + "locked_by", + models.CharField( + blank=True, db_index=True, max_length=64, null=True + ), + ), + ( + "locked_at", + models.DateTimeField(blank=True, db_index=True, null=True), + ), + ( + "creator_object_id", + models.PositiveIntegerField(blank=True, null=True), + ), + ( + "creator_content_type", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="completed_background_task", + to="contenttypes.contenttype", + ), + ), + ], + ), + ] diff --git a/app/background_task/migrations/__init__.py b/app/background_task/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/background_task/models.py b/app/background_task/models.py new file mode 100644 index 0000000..70121fe --- /dev/null +++ b/app/background_task/models.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- +from datetime import timedelta +from hashlib import sha1 +import json +import logging +import os +import traceback + +from io import StringIO +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType +from django.db import models +from django.db.models import Q +from django.utils import timezone +from six import python_2_unicode_compatible + +from background_task.exceptions import InvalidTaskError +from background_task.settings import app_settings +from background_task.signals import task_failed +from background_task.signals import task_rescheduled + + +logger = logging.getLogger(__name__) + + +class TaskQuerySet(models.QuerySet): + + def created_by(self, creator): + """ + :return: A Task queryset filtered by creator + """ + content_type = ContentType.objects.get_for_model(creator) + return self.filter( + creator_content_type=content_type, + creator_object_id=creator.id, + ) + + +class TaskManager(models.Manager): + + def get_queryset(self): + return TaskQuerySet(self.model, using=self._db) + + def created_by(self, creator): + return self.get_queryset().created_by(creator) + + def find_available(self, queue=None): + now = timezone.now() + qs = self.unlocked(now) + if queue: + qs = qs.filter(queue=queue) + ready = qs.filter(run_at__lte=now, failed_at=None) + _priority_ordering = '{}priority'.format( + app_settings.BACKGROUND_TASK_PRIORITY_ORDERING) + ready = ready.order_by(_priority_ordering, 'run_at') + + if app_settings.BACKGROUND_TASK_RUN_ASYNC: + currently_failed = self.failed().count() + currently_locked = self.locked(now).count() + count = app_settings.BACKGROUND_TASK_ASYNC_THREADS - \ + (currently_locked - currently_failed) + if count > 0: + ready = ready[:count] + else: + ready = self.none() + return ready + + def unlocked(self, now): + max_run_time = app_settings.BACKGROUND_TASK_MAX_RUN_TIME + qs = self.get_queryset() + expires_at = now - timedelta(seconds=max_run_time) + unlocked = Q(locked_by=None) | Q(locked_at__lt=expires_at) + return qs.filter(unlocked) + + def locked(self, now): + max_run_time = app_settings.BACKGROUND_TASK_MAX_RUN_TIME + qs = self.get_queryset() + expires_at = now - timedelta(seconds=max_run_time) + locked = Q(locked_by__isnull=False) | Q(locked_at__gt=expires_at) + return qs.filter(locked) + + def failed(self): + """ + `currently_locked - currently_failed` in `find_available` assues that + tasks marked as failed are also in processing by the running PID. + """ + qs = self.get_queryset() + return qs.filter(failed_at__isnull=False) + + def new_task(self, task_name, args=None, kwargs=None, + run_at=None, priority=0, queue=None, verbose_name=None, + creator=None, repeat=None, repeat_until=None, + remove_existing_tasks=False): + """ + If `remove_existing_tasks` is True, all unlocked tasks with the identical task hash will be removed. + The attributes `repeat` and `repeat_until` are not supported at the moment. + """ + args = args or () + kwargs = kwargs or {} + if run_at is None: + run_at = timezone.now() + task_params = json.dumps((args, kwargs), sort_keys=True) + s = "%s%s" % (task_name, task_params) + task_hash = sha1(s.encode('utf-8')).hexdigest() + if remove_existing_tasks: + Task.objects.filter(task_hash=task_hash, + locked_at__isnull=True).delete() + return Task(task_name=task_name, + task_params=task_params, + task_hash=task_hash, + priority=priority, + run_at=run_at, + queue=queue, + verbose_name=verbose_name, + creator=creator, + repeat=repeat or Task.NEVER, + repeat_until=repeat_until, + ) + + def get_task(self, task_name, args=None, kwargs=None): + args = args or () + kwargs = kwargs or {} + task_params = json.dumps((args, kwargs), sort_keys=True) + s = "%s%s" % (task_name, task_params) + task_hash = sha1(s.encode('utf-8')).hexdigest() + qs = self.get_queryset() + return qs.filter(task_hash=task_hash) + + def drop_task(self, task_name, args=None, kwargs=None): + return self.get_task(task_name, args, kwargs).delete() + + +@python_2_unicode_compatible +class Task(models.Model): + # the "name" of the task/function to be run + task_name = models.CharField(max_length=190, db_index=True) + # the json encoded parameters to pass to the task + task_params = models.TextField() + # a sha1 hash of the name and params, to lookup already scheduled tasks + task_hash = models.CharField(max_length=40, db_index=True) + + verbose_name = models.CharField(max_length=255, null=True, blank=True) + + # what priority the task has + priority = models.IntegerField(default=0, db_index=True) + # when the task should be run + run_at = models.DateTimeField(db_index=True) + + # Repeat choices are encoded as number of seconds + # The repeat implementation is based on this encoding + HOURLY = 3600 + DAILY = 24 * HOURLY + WEEKLY = 7 * DAILY + EVERY_2_WEEKS = 2 * WEEKLY + EVERY_4_WEEKS = 4 * WEEKLY + NEVER = 0 + REPEAT_CHOICES = ( + (HOURLY, 'hourly'), + (DAILY, 'daily'), + (WEEKLY, 'weekly'), + (EVERY_2_WEEKS, 'every 2 weeks'), + (EVERY_4_WEEKS, 'every 4 weeks'), + (NEVER, 'never'), + ) + repeat = models.BigIntegerField(choices=REPEAT_CHOICES, default=NEVER) + repeat_until = models.DateTimeField(null=True, blank=True) + + # the "name" of the queue this is to be run on + queue = models.CharField(max_length=190, db_index=True, + null=True, blank=True) + + # how many times the task has been tried + attempts = models.IntegerField(default=0, db_index=True) + # when the task last failed + failed_at = models.DateTimeField(db_index=True, null=True, blank=True) + # details of the error that occurred + last_error = models.TextField(blank=True) + + # details of who's trying to run the task at the moment + locked_by = models.CharField(max_length=64, db_index=True, + null=True, blank=True) + locked_at = models.DateTimeField(db_index=True, null=True, blank=True) + + creator_content_type = models.ForeignKey( + ContentType, null=True, blank=True, + related_name='background_task', on_delete=models.CASCADE + ) + creator_object_id = models.PositiveIntegerField(null=True, blank=True) + creator = GenericForeignKey('creator_content_type', 'creator_object_id') + + objects = TaskManager() + + def locked_by_pid_running(self): + """ + Check if the locked_by process is still running. + """ + if self.locked_by: + try: + # won't kill the process. kill is a bad named system call + os.kill(int(self.locked_by), 0) + return True + except: + return False + else: + return None + locked_by_pid_running.boolean = True + + def has_error(self): + """ + Check if the last_error field is empty. + """ + return bool(self.last_error) + has_error.boolean = True + + def params(self): + args, kwargs = json.loads(self.task_params) + # need to coerce kwargs keys to str + kwargs = dict((str(k), v) for k, v in kwargs.items()) + return args, kwargs + + def lock(self, locked_by): + now = timezone.now() + unlocked = Task.objects.unlocked(now).filter(pk=self.pk) + updated = unlocked.update(locked_by=locked_by, locked_at=now) + if updated: + return Task.objects.get(pk=self.pk) + return None + + def _extract_error(self, type, err, tb): + file = StringIO() + traceback.print_exception(type, err, tb, None, file) + return file.getvalue() + + def increment_attempts(self): + self.attempts += 1 + self.save() + + def has_reached_max_attempts(self): + max_attempts = app_settings.BACKGROUND_TASK_MAX_ATTEMPTS + return self.attempts >= max_attempts + + def is_repeating_task(self): + return self.repeat > self.NEVER + + def reschedule(self, type, err, traceback): + ''' + Set a new time to run the task in future, or create a CompletedTask and delete the Task + if it has reached the maximum of allowed attempts + ''' + self.last_error = self._extract_error(type, err, traceback) + self.increment_attempts() + if self.has_reached_max_attempts() or isinstance(err, InvalidTaskError): + self.failed_at = timezone.now() + logger.warning('Marking task %s as failed', self) + completed = self.create_completed_task() + task_failed.send(sender=self.__class__, + task_id=self.id, completed_task=completed) + self.delete() + else: + backoff = timedelta(seconds=(self.attempts ** 4) + 5) + self.run_at = timezone.now() + backoff + logger.warning('Rescheduling task %s for %s later at %s', self, + backoff, self.run_at) + task_rescheduled.send(sender=self.__class__, task=self) + self.locked_by = None + self.locked_at = None + self.save() + + def create_completed_task(self): + ''' + Returns a new CompletedTask instance with the same values + ''' + completed_task = CompletedTask( + task_name=self.task_name, + task_params=self.task_params, + task_hash=self.task_hash, + priority=self.priority, + run_at=timezone.now(), + queue=self.queue, + attempts=self.attempts, + failed_at=self.failed_at, + last_error=self.last_error, + locked_by=self.locked_by, + locked_at=self.locked_at, + verbose_name=self.verbose_name, + creator=self.creator, + repeat=self.repeat, + repeat_until=self.repeat_until, + ) + completed_task.save() + return completed_task + + def create_repetition(self): + """ + :return: A new Task with an offset of self.repeat, or None if the self.repeat_until is reached + """ + if not self.is_repeating_task(): + return None + + if self.repeat_until and self.repeat_until <= timezone.now(): + # Repeat chain completed + return None + + args, kwargs = self.params() + new_run_at = self.run_at + timedelta(seconds=self.repeat) + while new_run_at < timezone.now(): + new_run_at += timedelta(seconds=self.repeat) + + new_task = TaskManager().new_task( + task_name=self.task_name, + args=args, + kwargs=kwargs, + run_at=new_run_at, + priority=self.priority, + queue=self.queue, + verbose_name=self.verbose_name, + creator=self.creator, + repeat=self.repeat, + repeat_until=self.repeat_until, + ) + new_task.save() + return new_task + + def save(self, *arg, **kw): + # force NULL rather than empty string + self.locked_by = self.locked_by or None + return super(Task, self).save(*arg, **kw) + + def __str__(self): + return u'{}'.format(self.verbose_name or self.task_name) + + class Meta: + db_table = 'background_task' + + +class CompletedTaskQuerySet(models.QuerySet): + + def created_by(self, creator): + """ + :return: A CompletedTask queryset filtered by creator + """ + content_type = ContentType.objects.get_for_model(creator) + return self.filter( + creator_content_type=content_type, + creator_object_id=creator.id, + ) + + def failed(self, within=None): + """ + :param within: A timedelta object + :return: A queryset of CompletedTasks that failed within the given timeframe (e.g. less than 1h ago) + """ + qs = self.filter( + failed_at__isnull=False, + ) + if within: + time_limit = timezone.now() - within + qs = qs.filter(failed_at__gt=time_limit) + return qs + + def succeeded(self, within=None): + """ + :param within: A timedelta object + :return: A queryset of CompletedTasks that completed successfully within the given timeframe + (e.g. less than 1h ago) + """ + qs = self.filter( + failed_at__isnull=True, + ) + if within: + time_limit = timezone.now() - within + qs = qs.filter(run_at__gt=time_limit) + return qs + + +@python_2_unicode_compatible +class CompletedTask(models.Model): + # the "name" of the task/function to be run + task_name = models.CharField(max_length=190, db_index=True) + # the json encoded parameters to pass to the task + task_params = models.TextField() + # a sha1 hash of the name and params, to lookup already scheduled tasks + task_hash = models.CharField(max_length=40, db_index=True) + + verbose_name = models.CharField(max_length=255, null=True, blank=True) + + # what priority the task has + priority = models.IntegerField(default=0, db_index=True) + # when the task should be run + run_at = models.DateTimeField(db_index=True) + + repeat = models.BigIntegerField( + choices=Task.REPEAT_CHOICES, default=Task.NEVER) + repeat_until = models.DateTimeField(null=True, blank=True) + + # the "name" of the queue this is to be run on + queue = models.CharField(max_length=190, db_index=True, + null=True, blank=True) + + # how many times the task has been tried + attempts = models.IntegerField(default=0, db_index=True) + # when the task last failed + failed_at = models.DateTimeField(db_index=True, null=True, blank=True) + # details of the error that occurred + last_error = models.TextField(blank=True) + + # details of who's trying to run the task at the moment + locked_by = models.CharField(max_length=64, db_index=True, + null=True, blank=True) + locked_at = models.DateTimeField(db_index=True, null=True, blank=True) + + creator_content_type = models.ForeignKey( + ContentType, null=True, blank=True, + related_name='completed_background_task', on_delete=models.CASCADE + ) + creator_object_id = models.PositiveIntegerField(null=True, blank=True) + creator = GenericForeignKey('creator_content_type', 'creator_object_id') + + objects = CompletedTaskQuerySet.as_manager() + + def locked_by_pid_running(self): + """ + Check if the locked_by process is still running. + """ + if self.locked_by: + try: + # won't kill the process. kill is a bad named system call + os.kill(int(self.locked_by), 0) + return True + except: + return False + else: + return None + locked_by_pid_running.boolean = True + + def has_error(self): + """ + Check if the last_error field is empty. + """ + return bool(self.last_error) + has_error.boolean = True + + def __str__(self): + return u'{} - {}'.format( + self.verbose_name or self.task_name, + self.run_at, + ) diff --git a/app/background_task/settings.py b/app/background_task/settings.py new file mode 100644 index 0000000..6a60812 --- /dev/null +++ b/app/background_task/settings.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +import multiprocessing + +from django.conf import settings + +try: + cpu_count = multiprocessing.cpu_count() +except Exception: + cpu_count = 1 + + +class AppSettings(object): + """ + """ + @property + def MAX_ATTEMPTS(self): + """Control how many times a task will be attempted.""" + return getattr(settings, 'MAX_ATTEMPTS', 25) + + @property + def BACKGROUND_TASK_MAX_ATTEMPTS(self): + """Control how many times a task will be attempted.""" + return self.MAX_ATTEMPTS + + @property + def MAX_RUN_TIME(self): + """Maximum possible task run time, after which tasks will be unlocked and tried again.""" + return getattr(settings, 'MAX_RUN_TIME', 3600) + + @property + def BACKGROUND_TASK_MAX_RUN_TIME(self): + """Maximum possible task run time, after which tasks will be unlocked and tried again.""" + return self.MAX_RUN_TIME + + @property + def BACKGROUND_TASK_RUN_ASYNC(self): + """Control if tasks will run asynchronous in a ThreadPool.""" + return getattr(settings, 'BACKGROUND_TASK_RUN_ASYNC', False) + + @property + def BACKGROUND_TASK_ASYNC_THREADS(self): + """Specify number of concurrent threads.""" + return getattr(settings, 'BACKGROUND_TASK_ASYNC_THREADS', cpu_count) + + @property + def BACKGROUND_TASK_PRIORITY_ORDERING(self): + """ + Control the ordering of tasks in the queue. + Choose either `DESC` or `ASC`. + + https://en.m.wikipedia.org/wiki/Nice_(Unix) + A niceness of −20 is the highest priority and 19 is the lowest priority. The default niceness for processes is inherited from its parent process and is usually 0. + """ + order = getattr(settings, 'BACKGROUND_TASK_PRIORITY_ORDERING', 'DESC') + if order == 'ASC': + prefix = '' + else: + prefix = '-' + return prefix + +app_settings = AppSettings() diff --git a/app/background_task/signals.py b/app/background_task/signals.py new file mode 100644 index 0000000..ea05ac0 --- /dev/null +++ b/app/background_task/signals.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +import django.dispatch +from django.db import connections +from background_task.settings import app_settings + +task_created = django.dispatch.Signal(['task']) +task_error = django.dispatch.Signal(['task']) +task_rescheduled = django.dispatch.Signal(['task']) +task_failed = django.dispatch.Signal(['task_id', 'completed_task']) +task_successful = django.dispatch.Signal(['task_id', 'completed_task']) +task_started = django.dispatch.Signal() +task_finished = django.dispatch.Signal() + + +# Register an event to reset saved queries when a Task is started. +def reset_queries(**kwargs): + if app_settings.BACKGROUND_TASK_RUN_ASYNC: + for conn in connections.all(): + conn.queries_log.clear() + + +task_started.connect(reset_queries) + + +# Register an event to reset transaction state and close connections past +# their lifetime. +def close_old_connections(**kwargs): + if app_settings.BACKGROUND_TASK_RUN_ASYNC: + for conn in connections.all(): + conn.close_if_unusable_or_obsolete() + + +task_started.connect(close_old_connections) +task_finished.connect(close_old_connections) diff --git a/app/background_task/tasks.py b/app/background_task/tasks.py new file mode 100644 index 0000000..c2beaf6 --- /dev/null +++ b/app/background_task/tasks.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from datetime import datetime, timedelta +from importlib import import_module +from multiprocessing.pool import ThreadPool +import logging +import os +import sys + +from django.db.utils import OperationalError +from django.utils import timezone +from six import python_2_unicode_compatible + +from background_task.exceptions import BackgroundTaskError +from background_task.models import Task +from background_task.settings import app_settings +from background_task import signals + +logger = logging.getLogger(__name__) + + +def bg_runner(proxy_task, task=None, *args, **kwargs): + """ + Executes the function attached to task. Used to enable threads. + If a Task instance is provided, args and kwargs are ignored and retrieved from the Task itself. + """ + signals.task_started.send(Task) + try: + func = getattr(proxy_task, 'task_function', None) + if isinstance(task, Task): + args, kwargs = task.params() + else: + task_name = getattr(proxy_task, 'name', None) + task_queue = getattr(proxy_task, 'queue', None) + task_qs = Task.objects.get_task(task_name=task_name, args=args, kwargs=kwargs) + if task_queue: + task_qs = task_qs.filter(queue=task_queue) + if task_qs: + task = task_qs[0] + if func is None: + raise BackgroundTaskError("Function is None, can't execute!") + func(*args, **kwargs) + + if task: + # task done, so can delete it + task.increment_attempts() + completed = task.create_completed_task() + signals.task_successful.send(sender=task.__class__, task_id=task.id, completed_task=completed) + task.create_repetition() + task.delete() + logger.info('Ran task and deleting %s', task) + + except Exception as ex: + t, e, traceback = sys.exc_info() + if task: + logger.error('Rescheduling %s', task, exc_info=(t, e, traceback)) + signals.task_error.send(sender=ex.__class__, task=task) + task.reschedule(t, e, traceback) + del traceback + signals.task_finished.send(Task) + + +class PoolRunner: + def __init__(self, bg_runner, num_processes): + self._bg_runner = bg_runner + self._num_processes = num_processes + + _pool_instance = None + + @property + def _pool(self): + if not self._pool_instance: + self._pool_instance = ThreadPool(processes=self._num_processes) + return self._pool_instance + + def run(self, proxy_task, task=None, *args, **kwargs): + self._pool.apply_async(func=self._bg_runner, args=(proxy_task, task) + tuple(args), kwds=kwargs) + + __call__ = run + + +class Tasks(object): + def __init__(self): + self._tasks = {} + self._runner = DBTaskRunner() + self._task_proxy_class = TaskProxy + self._bg_runner = bg_runner + self._pool_runner = PoolRunner(bg_runner, app_settings.BACKGROUND_TASK_ASYNC_THREADS) + + def background(self, name=None, schedule=None, queue=None, + remove_existing_tasks=False): + ''' + decorator to turn a regular function into + something that gets run asynchronously in + the background, at a later time + ''' + + # see if used as simple decorator + # where first arg is the function to be decorated + fn = None + if name and callable(name): + fn = name + name = None + + def _decorator(fn): + _name = name + if not _name: + _name = '%s.%s' % (fn.__module__, fn.__name__) + proxy = self._task_proxy_class(_name, fn, schedule, queue, + remove_existing_tasks, self._runner) + self._tasks[_name] = proxy + return proxy + + if fn: + return _decorator(fn) + + return _decorator + + def run_task(self, task_name, args=None, kwargs=None): + # task_name can be either the name of a task or a Task instance. + if isinstance(task_name, Task): + task = task_name + task_name = task.task_name + # When we have a Task instance we do not need args and kwargs, but + # they are kept for backward compatibility + args = [] + kwargs = {} + else: + task = None + proxy_task = self._tasks[task_name] + if app_settings.BACKGROUND_TASK_RUN_ASYNC: + self._pool_runner(proxy_task, task, *args, **kwargs) + else: + self._bg_runner(proxy_task, task, *args, **kwargs) + + def run_next_task(self, queue=None): + return self._runner.run_next_task(self, queue) + + +class TaskSchedule(object): + SCHEDULE = 0 + RESCHEDULE_EXISTING = 1 + CHECK_EXISTING = 2 + + def __init__(self, run_at=None, priority=None, action=None): + self._run_at = run_at + self._priority = priority + self._action = action + + @classmethod + def create(self, schedule): + if isinstance(schedule, TaskSchedule): + return schedule + priority = None + run_at = None + action = None + + if schedule: + if isinstance(schedule, (int, timedelta, datetime)): + run_at = schedule + else: + run_at = schedule.get('run_at', None) + priority = schedule.get('priority', None) + action = schedule.get('action', None) + + return TaskSchedule(run_at=run_at, priority=priority, action=action) + + def merge(self, schedule): + params = {} + for name in ['run_at', 'priority', 'action']: + attr_name = '_%s' % name + value = getattr(self, attr_name, None) + if value is None: + params[name] = getattr(schedule, attr_name, None) + else: + params[name] = value + return TaskSchedule(**params) + + @property + def run_at(self): + run_at = self._run_at or timezone.now() + if isinstance(run_at, int): + run_at = timezone.now() + timedelta(seconds=run_at) + if isinstance(run_at, timedelta): + run_at = timezone.now() + run_at + return run_at + + @property + def priority(self): + return self._priority or 0 + + @property + def action(self): + return self._action or TaskSchedule.SCHEDULE + + def __repr__(self): + return 'TaskSchedule(run_at=%s, priority=%s)' % (self._run_at, + self._priority) + + def __eq__(self, other): + return self._run_at == other._run_at \ + and self._priority == other._priority \ + and self._action == other._action + + +class DBTaskRunner(object): + ''' + Encapsulate the model related logic in here, in case + we want to support different queues in the future + ''' + + def __init__(self): + self.worker_name = str(os.getpid()) + + def schedule(self, task_name, args, kwargs, run_at=None, + priority=0, action=TaskSchedule.SCHEDULE, queue=None, + verbose_name=None, creator=None, + repeat=None, repeat_until=None, remove_existing_tasks=False): + '''Simply create a task object in the database''' + task = Task.objects.new_task(task_name, args, kwargs, run_at, priority, + queue, verbose_name, creator, repeat, + repeat_until, remove_existing_tasks) + if action != TaskSchedule.SCHEDULE: + task_hash = task.task_hash + now = timezone.now() + unlocked = Task.objects.unlocked(now) + existing = unlocked.filter(task_hash=task_hash) + if queue: + existing = existing.filter(queue=queue) + if action == TaskSchedule.RESCHEDULE_EXISTING: + updated = existing.update(run_at=run_at, priority=priority) + if updated: + return + elif action == TaskSchedule.CHECK_EXISTING: + if existing.count(): + return + + task.save() + signals.task_created.send(sender=self.__class__, task=task) + return task + + def get_task_to_run(self, tasks, queue=None): + try: + available_tasks = [task for task in Task.objects.find_available(queue) + if task.task_name in tasks._tasks][:5] + for task in available_tasks: + # try to lock task + locked_task = task.lock(self.worker_name) + if locked_task: + return locked_task + return None + except OperationalError: + logger.warning('Failed to retrieve tasks. Database unreachable.') + + def run_task(self, tasks, task): + logger.info('Running %s', task) + tasks.run_task(task) + + def run_next_task(self, tasks, queue=None): + task = self.get_task_to_run(tasks, queue) + if task: + self.run_task(tasks, task) + return True + else: + return False + + +@python_2_unicode_compatible +class TaskProxy(object): + def __init__(self, name, task_function, schedule, queue, remove_existing_tasks, runner): + self.name = name + self.now = self.task_function = task_function + self.runner = runner + self.schedule = TaskSchedule.create(schedule) + self.queue = queue + self.remove_existing_tasks = remove_existing_tasks + + def __call__(self, *args, **kwargs): + schedule = kwargs.pop('schedule', None) + schedule = TaskSchedule.create(schedule).merge(self.schedule) + run_at = schedule.run_at + priority = kwargs.pop('priority', schedule.priority) + action = schedule.action + queue = kwargs.pop('queue', self.queue) + verbose_name = kwargs.pop('verbose_name', None) + creator = kwargs.pop('creator', None) + repeat = kwargs.pop('repeat', None) + repeat_until = kwargs.pop('repeat_until', None) + remove_existing_tasks = kwargs.pop('remove_existing_tasks', self.remove_existing_tasks) + + return self.runner.schedule(self.name, args, kwargs, run_at, priority, + action, queue, verbose_name, creator, + repeat, repeat_until, + remove_existing_tasks) + + def __str__(self): + return 'TaskProxy(%s)' % self.name + + +tasks = Tasks() + + +def autodiscover(): + """ + Autodiscover tasks.py files in much the same way as admin app + """ + from django.conf import settings + + for app in settings.INSTALLED_APPS: + try: + import_module("%s.tasks" % app) + except ImportError: + continue diff --git a/app/background_task/utils.py b/app/background_task/utils.py new file mode 100644 index 0000000..d7fb8ef --- /dev/null +++ b/app/background_task/utils.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import signal +import platform + +TTW_SLOW = [0.5, 1.5] +TTW_FAST = [0.0, 0.1] + + +class SignalManager(object): + """Manages POSIX signals.""" + + kill_now = False + time_to_wait = TTW_FAST + + def __init__(self): + # Temporary workaround for signals not available on Windows + if platform.system() == 'Windows': + signal.signal(signal.SIGTERM, self.exit_gracefully) + else: + signal.signal(signal.SIGTSTP, self.exit_gracefully) + signal.signal(signal.SIGUSR1, self.speed_up) + signal.signal(signal.SIGUSR2, self.slow_down) + + def exit_gracefully(self, signum, frame): + self.kill_now = True + + def speed_up(self, signum, frame): + self.time_to_wait = TTW_FAST + + def slow_down(self, signum, frame): + self.time_to_wait = TTW_SLOW diff --git a/requirements.txt b/requirements.txt index de5c0b8..1160fb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,14 @@ wheel uwsgi # might require "apt install python3-dev" django -django-background-tasks +# django-background-tasks # 폴더 채로 들고왔으므로 설치 불필요 django-cors-headers djangorestframework djangorestframework-simplejwt drf-yasg Pillow psycopg2 # PostgreSQL. May require "apt install libpq-dev", "apt install postgresql" +six # background tasks dependency # LLMs google-generativeai From f38d6d8bbbee3442478e843592562b0d3db631e3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 22:54:33 +0900 Subject: [PATCH 533/552] =?UTF-8?q?feat(background=5Ftasks):=20=EC=84=9C?= =?UTF-8?q?=EB=B2=84=EB=A5=BC=20=EC=BC=9C=EA=B8=B0=EB=A7=8C=20=ED=95=B4?= =?UTF-8?q?=EB=8F=84=20=EC=9E=90=EB=8F=99=EC=9C=BC=EB=A1=9C=20=EC=93=B0?= =?UTF-8?q?=EB=A0=88=EB=93=9C=EB=A5=BC=20=EC=83=9D=EC=84=B1=ED=95=98?= =?UTF-8?q?=EC=97=AC=20BG=20Task=EB=A5=BC=20=EC=88=98=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/background_task/apps.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/background_task/apps.py b/app/background_task/apps.py index d5f7a5d..aa0e80b 100644 --- a/app/background_task/apps.py +++ b/app/background_task/apps.py @@ -1,6 +1,12 @@ +from logging import getLogger +from threading import Thread + from django.apps import AppConfig +logger = getLogger(__name__) + + class BackgroundTasksAppConfig(AppConfig): name = 'background_task' from background_task import __version__ as version_info @@ -8,3 +14,14 @@ class BackgroundTasksAppConfig(AppConfig): def ready(self): import background_task.signals # noqa + + logger.info('creating thread for background tasks') + + from background_task.management.commands.process_tasks import Command as ProcessTasksCommand + + runner = ProcessTasksCommand() + thread = Thread(target=runner.run) + thread.setDaemon(True) + thread.start() + + logger.info('background tasks thread started') From 5fa764b571715d185cd3cfbc0434b1214337fbd8 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 23:05:58 +0900 Subject: [PATCH 534/552] =?UTF-8?q?chore:=20app=EB=B3=84=20=EB=A1=9C?= =?UTF-8?q?=EA=B9=85=20=ED=8F=B4=EB=8D=94=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 - app/boj/services.py | 2 +- app/config/settings/base/logging.py | 50 ++++++++++++++++------- app/logs/.gitkeep | 0 app/logs/app/background_task/.gitignore | 2 + app/logs/app/boj/.gitignore | 2 + app/logs/app/problems/.gitignore | 2 + app/logs/django/.gitignore | 2 + app/problems/analyzers/__init__.py | 2 +- app/problems/analyzers/gemini/analyzer.py | 2 +- app/problems/analyzers/gemini/parsers.py | 2 +- 11 files changed, 48 insertions(+), 19 deletions(-) delete mode 100644 app/logs/.gitkeep create mode 100644 app/logs/app/background_task/.gitignore create mode 100644 app/logs/app/boj/.gitignore create mode 100644 app/logs/app/problems/.gitignore create mode 100644 app/logs/django/.gitignore diff --git a/.gitignore b/.gitignore index 5cc7083..2b8a995 100644 --- a/.gitignore +++ b/.gitignore @@ -60,7 +60,6 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal -app/logs/* app/config/settings/*.py !app/config/settings/__init__.py !app/config/settings/test.py diff --git a/app/boj/services.py b/app/boj/services.py index 39cc29e..7b01a16 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -13,7 +13,7 @@ from users.models import User -logger = getLogger('django.server') +logger = getLogger(__name__) @receiver(post_save, sender=User) diff --git a/app/config/settings/base/logging.py b/app/config/settings/base/logging.py index e4397cc..186e6f5 100644 --- a/app/config/settings/base/logging.py +++ b/app/config/settings/base/logging.py @@ -15,7 +15,7 @@ "formatters": { "standard": { "()": "config.utils.ColorlessServerFormatter", - "format": "[{server_time}] {message}", + "format": "[{server_time}] [{name}] [{levelname}] {message}", "style": "{", }, "django.server": { @@ -28,39 +28,53 @@ "console": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/console.log', + 'filename': BASE_DIR / 'logs/django/console.log', 'when': 'D', - "formatter": "django.server", + "formatter": "standard", + }, + "mail_admins": { + "level": "ERROR", + "filters": ["require_debug_false"], + 'class': 'logging.FileHandler', + 'filename': BASE_DIR / 'logs/django/mail_admins.log', }, "django.mail": { "level": "ERROR", 'class': 'config.utils.FileAndStreamHandler', - 'filename': BASE_DIR / 'logs/django.mail.log', + 'filename': BASE_DIR / 'logs/django/mail.log', }, "django.server": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/django.server.log', + 'filename': BASE_DIR / 'logs/django/server.log', 'when': 'D', "formatter": "django.server", }, - "problems": { + "django.security.DisallowedHost": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/problems.log', + 'filename': BASE_DIR / 'logs/django/security.DisallowedHost.log', 'when': 'D', "formatter": "standard", }, - "mail_admins": { - "level": "ERROR", - "filters": ["require_debug_false"], - 'class': 'logging.FileHandler', - 'filename': BASE_DIR / 'logs/mail_admins.log', + "background_task": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': BASE_DIR / 'logs/app/background_task/.log', + 'when': 'D', + "formatter": "standard", }, - "django.security.DisallowedHost": { + "boj": { + "level": "INFO", + 'class': 'logging.handlers.TimedRotatingFileHandler', + 'filename': BASE_DIR / 'logs/app/boj/.log', + 'when': 'D', + "formatter": "standard", + }, + "problems": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/django.security.DisallowedHost.log', + 'filename': BASE_DIR / 'logs/app/problems/.log', 'when': 'D', "formatter": "standard", }, @@ -80,6 +94,14 @@ "level": "DEBUG", 'propagate': False, }, + "background_task": { + "handlers": ["background_task"], + "level": "INFO", + }, + "boj": { + "handlers": ["boj"], + "level": "INFO", + }, "problems": { "handlers": ["problems"], "level": "INFO", diff --git a/app/logs/.gitkeep b/app/logs/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/app/logs/app/background_task/.gitignore b/app/logs/app/background_task/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/logs/app/background_task/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/logs/app/boj/.gitignore b/app/logs/app/boj/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/logs/app/boj/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/logs/app/problems/.gitignore b/app/logs/app/problems/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/logs/app/problems/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/logs/django/.gitignore b/app/logs/django/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/logs/django/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/problems/analyzers/__init__.py b/app/problems/analyzers/__init__.py index ea71b62..2437955 100644 --- a/app/problems/analyzers/__init__.py +++ b/app/problems/analyzers/__init__.py @@ -13,7 +13,7 @@ from problems.models import Problem -logger = getLogger('problems.analyzers') +logger = getLogger(__name__) def get_analyzer() -> ProblemAnalyzer: diff --git a/app/problems/analyzers/gemini/analyzer.py b/app/problems/analyzers/gemini/analyzer.py index 97e33cb..7acd6bd 100644 --- a/app/problems/analyzers/gemini/analyzer.py +++ b/app/problems/analyzers/gemini/analyzer.py @@ -10,7 +10,7 @@ from problems.analyzers.gemini import parsers -logger = getLogger('problems.analyzers.gemini.analyzer') +logger = getLogger(__name__) class GeminiProblemAnalyzer(ProblemAnalyzer): diff --git a/app/problems/analyzers/gemini/parsers.py b/app/problems/analyzers/gemini/parsers.py index 5159383..e364cad 100644 --- a/app/problems/analyzers/gemini/parsers.py +++ b/app/problems/analyzers/gemini/parsers.py @@ -9,7 +9,7 @@ from problems.analyses.models import ProblemTag -logger = getLogger('problems.analyzers.gemini.parsers') +logger = getLogger(__name__) @cache From fcd98018f09b1ef9c702799b0ff2eb92a6055e3e Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 23:14:34 +0900 Subject: [PATCH 535/552] =?UTF-8?q?refactor(background=5Ftask):=20import?= =?UTF-8?q?=EB=AC=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/services.py | 4 ++-- app/problems/analyzers/__init__.py | 4 ++-- app/users/services.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/boj/services.py b/app/boj/services.py index 7b01a16..c4f2a9f 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -1,13 +1,13 @@ from json import JSONDecodeError from logging import getLogger -from background_task import background from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone from rest_framework import status import requests +from background_task.tasks import tasks from boj.models import BOJUser from boj.models import BOJUserSnapshot from users.models import User @@ -32,7 +32,7 @@ def update_boj_user_data(username: str): _update_boj_user_data(username) -@background +@tasks.background def _update_boj_user_data(username: str): assert username.strip().isidentifier() instance = BOJUser.objects.get_by_username(username) diff --git a/app/problems/analyzers/__init__.py b/app/problems/analyzers/__init__.py index 2437955..36a59c8 100644 --- a/app/problems/analyzers/__init__.py +++ b/app/problems/analyzers/__init__.py @@ -1,9 +1,9 @@ from logging import getLogger -from background_task import background from django.db.models.signals import post_save from django.dispatch import receiver +from background_task.tasks import tasks from problems.analyses.dto import ProblemAnalysisDTO from problems.analyses.models import ProblemAnalysis from problems.analyses.models import ProblemTag @@ -26,7 +26,7 @@ def auto_analyze(sender, instance: Problem, created: bool, **kwargs): schedule_analyze(instance.pk) -@background +@tasks.background def schedule_analyze(problem_id: int): logger.info(f'PK={problem_id} 문제의 분석 준비중.') problem = Problem.objects.get(pk=problem_id) diff --git a/app/users/services.py b/app/users/services.py index e75f160..6800481 100644 --- a/app/users/services.py +++ b/app/users/services.py @@ -1,10 +1,10 @@ from textwrap import dedent -from background_task import background from django.core.mail import send_mail from django.db.models.signals import post_save from django.dispatch import receiver +from background_task.tasks import tasks from users.models import UserEmailVerification @@ -19,7 +19,7 @@ def notify_on_code_generated(sender, instance: UserEmailVerification, created: b _schedule_mail(subject, message, recipient) -@background +@tasks.background def _schedule_mail(subject: str, message: str, recipient: str) -> None: send_mail( subject=subject, From 9d06d169e90c7b219fb34c9be5b14a99a0e4dad6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 23:46:49 +0900 Subject: [PATCH 536/552] =?UTF-8?q?fix(users.models):=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=8B=9C=20None=20=EB=8C=80=EC=8B=A0=20"None?= =?UTF-8?q?"=EC=9D=B4=20=EB=93=A4=EC=96=B4=EA=B0=80=EB=8A=94=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/users/models.py b/app/users/models.py index 27f613e..1c90924 100644 --- a/app/users/models.py +++ b/app/users/models.py @@ -115,7 +115,7 @@ def has_module_perms(self, app_label): def rotate_token(self): token: RefreshToken = RefreshToken.for_user(self) self.token = str(token.access_token) - self.refresh_token = str(token.token) + self.refresh_token = token.token self.save() From cadcf46b69aadcb51048d71ec440114e87dfe525 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 23:51:57 +0900 Subject: [PATCH 537/552] =?UTF-8?q?test(users):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=B5=EC=8B=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EA=B2=80=EC=A6=9D=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/app/users/tests.py b/app/users/tests.py index 951adc7..d96ee10 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -17,6 +17,25 @@ def test_200_로그인성공(self): "password": "passw0rd@test", }) self.assertEqual(res.status_code, status.HTTP_200_OK) + data = res.json() + token = data.pop('token') + self.assertGreater(len(token), 1) + self.assertDictEqual(data, { + "id": 1, + "username": "test", + "profile_image": None, + "refresh_token": None, + "boj": { + "username": "test", + "profile_url": "https://boj.kr/test", + "level": { + "value": 0, + "name": "사용자 정보를 불러오지 못함", + }, + "rating": 0, + "updated_at": "2024-08-27T04:02:23.327000", + }, + }) def test_403_비밀번호_불일치(self): res = self.client.post("/api/v1/auth/signin", { From 2702037d58edbc57a2facaa232be1643cd538a34 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Sun, 1 Sep 2024 23:59:57 +0900 Subject: [PATCH 538/552] =?UTF-8?q?feat(users):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/users/tests.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/app/users/tests.py b/app/users/tests.py index d96ee10..906c45c 100644 --- a/app/users/tests.py +++ b/app/users/tests.py @@ -11,7 +11,7 @@ class SignInTest(TestCase): def setUp(self) -> None: self.client.logout() - def test_200_로그인성공(self): + def test_200_세션_로그인성공(self): res = self.client.post("/api/v1/auth/signin", { "email": "test@example.com", "password": "passw0rd@test", @@ -44,6 +44,24 @@ def test_403_비밀번호_불일치(self): }) self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN) + def test_200_Bearer_토큰_로그인(self): + # 인증 토큰 발급받기 + token = self.client.post("/api/v1/auth/signin", { + "email": "test@example.com", + "password": "passw0rd@test", + }).json()['token'] + + # 임의로 로그인이 필요한 기능 사용 (로그아웃 됨을 확인) + self.client.logout() + res = self.client.get("/api/v1/user/manage") + self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) + + # 임의로 로그인이 필요한 기능 사용 (토큰 로그인 시도) + res = self.client.get("/api/v1/user/manage", headers={ + 'Authorization': f'Bearer {token}', + }) + self.assertEqual(res.status_code, status.HTTP_200_OK) + class SignUpTest(TestCase): @classmethod From a376e4e7b7a654d2ee6939f4629541f600900147 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 02:53:24 +0900 Subject: [PATCH 539/552] =?UTF-8?q?chore(config.settings):=20=EB=AF=BC?= =?UTF-8?q?=EA=B0=90=ED=95=9C=20=EC=A0=95=EB=B3=B4=EB=8A=94=20secret?= =?UTF-8?q?=EC=97=90=20=EC=88=A8=EA=B8=B4=20=ED=9B=84=20symlink=EB=A1=9C?= =?UTF-8?q?=20=EC=97=B0=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 --- app/config/settings/base/core.py | 3 --- app/config/settings/dev.py | 1 + app/config/settings/production.py | 1 + app/config/settings/secret/.gitignore | 2 ++ app/manage.py | 10 +++++++++- 6 files changed, 13 insertions(+), 7 deletions(-) create mode 120000 app/config/settings/dev.py create mode 120000 app/config/settings/production.py create mode 100644 app/config/settings/secret/.gitignore diff --git a/.gitignore b/.gitignore index 2b8a995..3fea83f 100644 --- a/.gitignore +++ b/.gitignore @@ -60,9 +60,6 @@ cover/ local_settings.py db.sqlite3 db.sqlite3-journal -app/config/settings/*.py -!app/config/settings/__init__.py -!app/config/settings/test.py .static/ .media/ diff --git a/app/config/settings/base/core.py b/app/config/settings/base/core.py index f65e583..dca3c45 100644 --- a/app/config/settings/base/core.py +++ b/app/config/settings/base/core.py @@ -22,9 +22,6 @@ # See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/ -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = False - ALLOWED_HOSTS = [] diff --git a/app/config/settings/dev.py b/app/config/settings/dev.py new file mode 120000 index 0000000..f9af3c9 --- /dev/null +++ b/app/config/settings/dev.py @@ -0,0 +1 @@ +./secret/dev.py \ No newline at end of file diff --git a/app/config/settings/production.py b/app/config/settings/production.py new file mode 120000 index 0000000..773e206 --- /dev/null +++ b/app/config/settings/production.py @@ -0,0 +1 @@ +./secret/production.py \ No newline at end of file diff --git a/app/config/settings/secret/.gitignore b/app/config/settings/secret/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/app/config/settings/secret/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/app/manage.py b/app/manage.py index 6326a7c..c7da7ac 100755 --- a/app/manage.py +++ b/app/manage.py @@ -4,9 +4,17 @@ import sys +TESTING = sys.argv[1:2] == ['test'] + + def main(): """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") + + if TESTING: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.test") + else: + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.dev") + try: from django.core.management import execute_from_command_line except ImportError as exc: From bb13772b37b51471e258b1e30871f653afa163a4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 03:20:10 +0900 Subject: [PATCH 540/552] =?UTF-8?q?fix(background=5Ftask):=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=93=B0=EB=A0=88=EB=93=9C=EA=B0=80=20=EC=95=84?= =?UTF-8?q?=EB=8B=88=EC=96=B4=EC=84=9C=20=EB=B0=B1=EA=B7=B8=EB=9D=BC?= =?UTF-8?q?=EC=9A=B4=EB=93=9C=20=ED=94=84=EB=A1=9C=EC=84=B8=EC=8A=A4?= =?UTF-8?q?=EA=B0=80=20=EC=88=98=ED=96=89=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/background_task/apps.py | 18 +++++++++------ .../management/commands/process_tasks.py | 23 ++++++++----------- 2 files changed, 20 insertions(+), 21 deletions(-) diff --git a/app/background_task/apps.py b/app/background_task/apps.py index aa0e80b..9c9fbdf 100644 --- a/app/background_task/apps.py +++ b/app/background_task/apps.py @@ -14,14 +14,18 @@ class BackgroundTasksAppConfig(AppConfig): def ready(self): import background_task.signals # noqa - - logger.info('creating thread for background tasks') - from background_task.management.commands.process_tasks import Command as ProcessTasksCommand - runner = ProcessTasksCommand() - thread = Thread(target=runner.run) + def task_runner(*args, **kwargs): + logger.info('background tasks thread started') + try: + command = ProcessTasksCommand() + command.handle() + except Exception as exception: + logger.error(exception) + finally: + logger.info('shutting down background task thread.') + + thread = Thread(target=task_runner) thread.setDaemon(True) thread.start() - - logger.info('background tasks thread started') diff --git a/app/background_task/management/commands/process_tasks.py b/app/background_task/management/commands/process_tasks.py index af9528f..6fd8552 100644 --- a/app/background_task/management/commands/process_tasks.py +++ b/app/background_task/management/commands/process_tasks.py @@ -57,11 +57,6 @@ class Command(BaseCommand): 'dest': 'log_std', 'help': 'Redirect stdout and stderr to the logging system', }), - (('--dev', ), { - 'action': 'store_true', - 'dest': 'dev', - 'help': 'Auto-reload your code on changes. Use this only for development', - }), ) if VERSION < (1, 8): @@ -112,12 +107,12 @@ def run(self, *args, **options): time.sleep(random.uniform(sig_manager.time_to_wait[0], sig_manager.time_to_wait[1])) def handle(self, *args, **options): - is_dev = options.get('dev', False) - self.sig_manager = SignalManager() - if is_dev: - reload_func = autoreload.run_with_reloader - if VERSION < (2, 2): - reload_func = autoreload.main - reload_func(self.run, *args, **options) - else: - self.run(*args, **options) + # 메인 스레드에서 수행할 것이 아니므로 시그널을 처리하지 못한다. + # 임의로 Placeholder Class만 만들어서 사용한다. + self.sig_manager = FakeSignalManager() + self.run(*args, **options) + + +class FakeSignalManager(SignalManager): + def __init__(self): + self.slow_down(None, None) From 014d6c27e1b3084ecad10ba6a79abd646dec1132 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 03:25:42 +0900 Subject: [PATCH 541/552] refactor(boj.service): rename `update_boj_user` -> `schedule_update_boj_user` --- app/boj/admin.py | 10 +++++----- app/boj/services.py | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/boj/admin.py b/app/boj/admin.py index 58b54d1..ca6beda 100644 --- a/app/boj/admin.py +++ b/app/boj/admin.py @@ -4,7 +4,7 @@ from boj.models import BOJUser from boj.models import BOJUserSnapshot -from boj.services import update_boj_user_data +from boj.services import schedule_update_boj_user_data @admin.register(BOJUser) @@ -16,13 +16,13 @@ class BOJUserModelAdmin(admin.ModelAdmin): BOJUser.field_name.UPDATED_AT, ] actions = [ - 'update', + 'schedule_update', ] - @admin.action(description="Update selected BOJ user data. (via solved.ac API)") - def update(self, request: HttpRequest, queryset: QuerySet[BOJUser]): + @admin.action(description="Schedule update selected BOJ user data. (via solved.ac API)") + def schedule_update(self, request: HttpRequest, queryset: QuerySet[BOJUser]): for obj in queryset: - update_boj_user_data(obj.username) + schedule_update_boj_user_data(obj.username) @admin.register(BOJUserSnapshot) diff --git a/app/boj/services.py b/app/boj/services.py index c4f2a9f..6b121ad 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -18,16 +18,16 @@ @receiver(post_save, sender=User) def auto_create_boj_user(sender, instance: User, created: bool, **kwargs): - update_boj_user_data(instance.username) + schedule_update_boj_user_data(instance.username) @receiver(post_save, sender=BOJUser) def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): if created: - update_boj_user_data(instance.username) + schedule_update_boj_user_data(instance.username) -def update_boj_user_data(username: str): +def schedule_update_boj_user_data(username: str): assert username.strip().isidentifier() _update_boj_user_data(username) From 3ff958df172bbbd030c1a0e2936e8e50e4a1b4e6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 03:34:09 +0900 Subject: [PATCH 542/552] =?UTF-8?q?feat(boj.services):=20=EB=B0=B1?= =?UTF-8?q?=EC=A4=80=20=EC=9C=A0=EC=A0=80=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=94=EB=A1=9C=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8=20?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EA=B2=83=EA=B3=BC=20=EC=97=85=EB=8D=B0?= =?UTF-8?q?=EC=9D=B4=ED=8A=B8=EB=A5=BC=20=EC=98=88=EC=95=BD=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EA=B2=83=EC=9D=84=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/admin.py | 7 +++++++ app/boj/services.py | 27 +++++++++++++++++---------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/app/boj/admin.py b/app/boj/admin.py index ca6beda..136b1d9 100644 --- a/app/boj/admin.py +++ b/app/boj/admin.py @@ -5,6 +5,7 @@ from boj.models import BOJUser from boj.models import BOJUserSnapshot from boj.services import schedule_update_boj_user_data +from boj.services import update_boj_user @admin.register(BOJUser) @@ -16,6 +17,7 @@ class BOJUserModelAdmin(admin.ModelAdmin): BOJUser.field_name.UPDATED_AT, ] actions = [ + 'update', 'schedule_update', ] @@ -24,6 +26,11 @@ def schedule_update(self, request: HttpRequest, queryset: QuerySet[BOJUser]): for obj in queryset: schedule_update_boj_user_data(obj.username) + @admin.action(description="Update selected BOJ user data right now. (via solved.ac API)") + def update(self, request: HttpRequest, queryset: QuerySet[BOJUser]): + for obj in queryset: + update_boj_user(obj) + @admin.register(BOJUserSnapshot) class BOJUserSnapshotModelAdmin(admin.ModelAdmin): diff --git a/app/boj/services.py b/app/boj/services.py index 6b121ad..2b3ec3d 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -27,15 +27,23 @@ def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): schedule_update_boj_user_data(instance.username) +@tasks.background def schedule_update_boj_user_data(username: str): assert username.strip().isidentifier() - _update_boj_user_data(username) + instance = BOJUser.objects.get_by_username(username) + update_boj_user(instance) -@tasks.background -def _update_boj_user_data(username: str): - assert username.strip().isidentifier() - instance = BOJUser.objects.get_by_username(username) +def update_boj_user(instance: BOJUser): + raw_boj_user_data = fetch_boj_user_data(instance.username) + instance.level = raw_boj_user_data['tier'] + instance.rating = raw_boj_user_data['rating'] + instance.updated_at = timezone.now() + instance.save() + BOJUserSnapshot.objects.create_snapshot_of(instance) + + +def fetch_boj_user_data(username: str) -> dict: url = f'https://solved.ac/api/v3/user/show?handle={username}' res = requests.get(url) if res.status_code == status.HTTP_404_NOT_FOUND: @@ -43,14 +51,13 @@ def _update_boj_user_data(username: str): else: try: data = res.json() - instance.level = data['tier'] - instance.rating = data['rating'] - instance.updated_at = timezone.now() - instance.save() - BOJUserSnapshot.objects.create_snapshot_of(instance) + assert 'tier' in data + assert 'rating' in data except AssertionError: # Solved.ac API 관련 문제일 가능성이 높다. logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') except JSONDecodeError: logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') logger.error(f'받은 데이터: "{res.content}"') + else: + return data From 6630ec1ca04838be03723afe9704d30f3edf0778 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 03:34:42 +0900 Subject: [PATCH 543/552] =?UTF-8?q?feat(boj.services):=2090=EC=B4=88=20?= =?UTF-8?q?=EC=9D=B4=EB=82=B4=EC=97=90=20=EB=8F=99=EC=9D=BC=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=80=20=EB=AC=B4=EC=8B=9C=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=93=B0=EB=A1=9C=ED=8B=80=EB=A7=81=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/services.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/boj/services.py b/app/boj/services.py index 2b3ec3d..f4293f3 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -1,5 +1,6 @@ from json import JSONDecodeError from logging import getLogger +from datetime import timedelta from django.db.models.signals import post_save from django.dispatch import receiver @@ -31,6 +32,11 @@ def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): def schedule_update_boj_user_data(username: str): assert username.strip().isidentifier() instance = BOJUser.objects.get_by_username(username) + + # 마지막 갱신으로 부터 90초 이내에 시도한 갱신 요청은 무시 함. + if (timezone.now() - instance.updated_at) < timedelta(seconds=90): + return + update_boj_user(instance) From d1fdf88d27b7b36e6ed85f0e007202fdffdcb410 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 03:44:45 +0900 Subject: [PATCH 544/552] =?UTF-8?q?fix(boj.services):=20=EA=B0=93=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EB=90=9C=20=EB=B0=B1=EC=A4=80=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EC=A0=95=EB=B3=B4=20=EA=B0=B1=EC=8B=A0=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EB=8F=84=20=EB=AC=B4=EC=8B=9C=EB=90=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/boj/models.py | 6 +++++- app/boj/services.py | 41 +++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/app/boj/models.py b/app/boj/models.py index aebf209..bd6f3f4 100644 --- a/app/boj/models.py +++ b/app/boj/models.py @@ -1,6 +1,7 @@ from __future__ import annotations from typing import Optional +from typing import Tuple from django.db import models from django.db.models import Manager @@ -15,8 +16,11 @@ def filter(self, username: Optional[str] = None, *args, **kwargs) -> models.Quer kwargs[User.field_name.USERNAME] = username return super().filter(*args, **kwargs) + def get_or_create_by_username(self, username: str) -> Tuple[BOJUser, bool]: + return self.get_or_create(**{BOJUser.field_name.USERNAME: username}) + def get_by_username(self, username: str) -> BOJUser: - return self.get_or_create(**{BOJUser.field_name.USERNAME: username})[0] + return self.get_or_create_by_username(username)[0] class BOJUser(models.Model): diff --git a/app/boj/services.py b/app/boj/services.py index f4293f3..5d40616 100644 --- a/app/boj/services.py +++ b/app/boj/services.py @@ -9,6 +9,7 @@ import requests from background_task.tasks import tasks +from boj.enums import BOJLevel from boj.models import BOJUser from boj.models import BOJUserSnapshot from users.models import User @@ -19,22 +20,17 @@ @receiver(post_save, sender=User) def auto_create_boj_user(sender, instance: User, created: bool, **kwargs): - schedule_update_boj_user_data(instance.username) - - -@receiver(post_save, sender=BOJUser) -def auto_update_boj_user(sender, instance: BOJUser, created: bool, **kwargs): - if created: - schedule_update_boj_user_data(instance.username) + schedule_update_boj_user_data(instance.boj_username) @tasks.background def schedule_update_boj_user_data(username: str): assert username.strip().isidentifier() - instance = BOJUser.objects.get_by_username(username) + instance, created = BOJUser.objects.get_or_create_by_username(username) # 마지막 갱신으로 부터 90초 이내에 시도한 갱신 요청은 무시 함. - if (timezone.now() - instance.updated_at) < timedelta(seconds=90): + if not created and (timezone.now() - instance.updated_at) < timedelta(seconds=90): + logger.info(f'백준 사용자 "{username}"의 정보 갱신 요청이 무시됨. (사유: 너무 잦은 갱신 요청)') return update_boj_user(instance) @@ -54,16 +50,17 @@ def fetch_boj_user_data(username: str) -> dict: res = requests.get(url) if res.status_code == status.HTTP_404_NOT_FOUND: logger.info(f'사용자 명이 "{username}"인 사용자가 존재하지 않습니다.') - else: - try: - data = res.json() - assert 'tier' in data - assert 'rating' in data - except AssertionError: - # Solved.ac API 관련 문제일 가능성이 높다. - logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') - except JSONDecodeError: - logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') - logger.error(f'받은 데이터: "{res.content}"') - else: - return data + try: + data = res.json() + assert 'tier' in data + assert 'rating' in data + except (AssertionError, JSONDecodeError): + # Solved.ac API 관련 문제일 가능성이 높다. + logger.warning(f'"{url}"로 부터 데이터를 파싱해오는 것에 실패했습니다.') + logger.error(f'받은 데이터: "{res.content}"') + data = { + 'tier': BOJLevel.U, + 'rating': 0, + } + finally: + return data From 39f56138c283388558c22a8260f9572ee48367b3 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 05:06:50 +0900 Subject: [PATCH 545/552] =?UTF-8?q?test(crews):=20view=20=EB=B3=84=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/crews/tests.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/app/crews/tests.py b/app/crews/tests.py index f740022..258bd03 100644 --- a/app/crews/tests.py +++ b/app/crews/tests.py @@ -4,7 +4,7 @@ from users.models import User -class CrewAPITest(TestCase): +class RecruitingCrewListAPIViewTest(TestCase): fixtures = ['sample.json'] maxDiff = None @@ -12,7 +12,7 @@ def setUp(self) -> None: self.user = User.objects.get(pk=1) self.client.force_login(self.user) - def test_비로그인_사용자로_recruiting_크루_목록_조회(self): + def test_200_비로그인_사용자로_recruiting_크루_목록_조회(self): self.client.logout() res = self.client.get("/api/v1/crews/recruiting") self.assertEqual(res.status_code, status.HTTP_200_OK) @@ -56,7 +56,7 @@ def test_비로그인_사용자로_recruiting_크루_목록_조회(self): }, ]) - def test_로그인_사용자로_recruiting_크루_목록_조회(self): + def test_200_로그인_사용자로_recruiting_크루_목록_조회(self): res = self.client.get("/api/v1/crews/recruiting") self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertListEqual(res.json(), [ @@ -79,12 +79,21 @@ def test_로그인_사용자로_recruiting_크루_목록_조회(self): }, ]) - def test_비로그인_사용자로_my_크루_목록_조회_불가능(self): + +class MyCrewListAPIViewTest(TestCase): + fixtures = ['sample.json'] + maxDiff = None + + def setUp(self) -> None: + self.user = User.objects.get(pk=1) + self.client.force_login(self.user) + + def test_401_비로그인_사용자로_my_크루_목록_조회_불가능(self): self.client.logout() res = self.client.get("/api/v1/crews/my") self.assertEqual(res.status_code, status.HTTP_401_UNAUTHORIZED) - def test_my_크루_목록_조회(self): + def test_200_my_크루_목록_조회(self): res = self.client.get("/api/v1/crews/my") self.assertEqual(res.status_code, status.HTTP_200_OK) self.assertListEqual(res.json(), [ @@ -101,7 +110,16 @@ def test_my_크루_목록_조회(self): }, ]) - def test_크루_생성(self): + +class CrewCreateAPIViewTest(TestCase): + fixtures = ['sample.json'] + maxDiff = None + + def setUp(self) -> None: + self.user = User.objects.get(pk=1) + self.client.force_login(self.user) + + def test_201_크루_생성(self): res = self.client.post("/api/v1/crew", { "icon": "🥇", "name": "임시로 생성해본 크루", From 25d7b1bb97d6bf8c4381db645a977a0fb42f3cab Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 05:20:24 +0900 Subject: [PATCH 546/552] =?UTF-8?q?chore:=20swagger=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EC=B6=9C=EB=A0=A5=EC=9D=84=20=EB=B0=A9=EC=A7=80=ED=95=98?= =?UTF-8?q?=EA=B8=B0=EC=9C=84=ED=95=9C=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/problems/serializers.py | 3 +-- app/users/views.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/problems/serializers.py b/app/problems/serializers.py index 7295c48..7bd9b80 100644 --- a/app/problems/serializers.py +++ b/app/problems/serializers.py @@ -169,8 +169,7 @@ class ProblemStatisticSerializer(serializers.Serializer): difficulty = ProblemStatisticsDifficultyField() tags = ProblemStatisticsTagsField() - def __init__(self, instance: ProblemStatisticDTO, **kwargs): - assert isinstance(instance, ProblemStatisticDTO) + def __init__(self, instance: ProblemStatisticDTO = None, **kwargs): super().__init__(instance, **kwargs) diff --git a/app/users/views.py b/app/users/views.py index 5b05a20..4e0a8af 100644 --- a/app/users/views.py +++ b/app/users/views.py @@ -11,6 +11,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.request import Request from rest_framework.response import Response +from rest_framework.serializers import Serializer from users.models import User from users.models import UserEmailVerification @@ -102,6 +103,7 @@ class SignOutAPIView(generics.GenericAPIView): . """ permission_classes = [IsAuthenticated] + serializer_class = Serializer def get(self, request, *args, **kwargs): logout(request) From daf860bb4422d7b583e31d6a4bf184a05fb79f8d Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 11:32:51 +0900 Subject: [PATCH 547/552] refactor(crews.submissions): mv app submissions -> crews.submissions --- app/config/settings/base/core.py | 1 + app/crews/serializersaaa/__init__ copy.py | 171 ------------- app/crews/serializersaaa/__init__.py | 33 --- app/crews/serializersaaa/fields.py | 153 ----------- app/crews/servicesa/__init__.py | 25 -- app/crews/servicesa/base.py | 82 ------ app/crews/servicesa/concrete.py | 189 -------------- app/crews/servicesa/crew_activity_service.py | 47 ---- app/crews/servicesa/crew_service.py | 242 ------------------ app/crews/servicesa/dto.py | 67 ----- app/{ => crews}/submissions/__init__.py | 0 app/{ => crews}/submissions/admin.py | 2 +- app/{ => crews}/submissions/apps.py | 3 +- .../submissions/migrations/__init__.py | 0 app/crews/submissions/models/__init__.py | 8 + .../submissions/models/submission.py | 0 .../submissions/models/submission_comment.py | 2 +- .../submissions/serializers/__init__.py | 2 +- app/{ => crews}/submissions/views.py | 2 +- app/submissions/models/__init__.py | 8 - 20 files changed, 15 insertions(+), 1022 deletions(-) delete mode 100644 app/crews/serializersaaa/__init__ copy.py delete mode 100644 app/crews/serializersaaa/__init__.py delete mode 100644 app/crews/serializersaaa/fields.py delete mode 100644 app/crews/servicesa/__init__.py delete mode 100644 app/crews/servicesa/base.py delete mode 100644 app/crews/servicesa/concrete.py delete mode 100644 app/crews/servicesa/crew_activity_service.py delete mode 100644 app/crews/servicesa/crew_service.py delete mode 100644 app/crews/servicesa/dto.py rename app/{ => crews}/submissions/__init__.py (100%) rename app/{ => crews}/submissions/admin.py (71%) rename app/{ => crews}/submissions/apps.py (65%) rename app/{ => crews}/submissions/migrations/__init__.py (100%) create mode 100644 app/crews/submissions/models/__init__.py rename app/{ => crews}/submissions/models/submission.py (100%) rename app/{ => crews}/submissions/models/submission_comment.py (96%) rename app/{ => crews}/submissions/serializers/__init__.py (90%) rename app/{ => crews}/submissions/views.py (98%) delete mode 100644 app/submissions/models/__init__.py diff --git a/app/config/settings/base/core.py b/app/config/settings/base/core.py index dca3c45..8bf5d76 100644 --- a/app/config/settings/base/core.py +++ b/app/config/settings/base/core.py @@ -51,6 +51,7 @@ "crews", "crews.activities", "crews.applications", + "crews.submissions", "users", "problems", "problems.analyses", diff --git a/app/crews/serializersaaa/__init__ copy.py b/app/crews/serializersaaa/__init__ copy.py deleted file mode 100644 index 6e9a310..0000000 --- a/app/crews/serializersaaa/__init__ copy.py +++ /dev/null @@ -1,171 +0,0 @@ -from django.db.transaction import atomic -from rest_framework import serializers - -from crews import enums -from crews import models -from crews import servicesa -from crews.serializersaaa import fields - - -PK = 'id' - - -class NoInputSerializer(serializers.Serializer): - pass - - -# Crew Serializers - -class CrewCreateSerializer(serializers.ModelSerializer): - created_by = serializers.HiddenField( - default=serializers.CurrentUserDefault(), - ) - custom_tags = serializers.ListField( - default=list, - child=serializers.CharField(), - ) - languages = serializers.MultipleChoiceField( - choices=enums.ProgrammingLanguageChoices.choices, - ) - - class Meta: - model = models.Crew - fields = [ - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.MAX_MEMBERS, - 'languages', - models.Crew.field_name.MIN_BOJ_LEVEL, - models.Crew.field_name.CUSTOM_TAGS, - models.Crew.field_name.NOTICE, - models.Crew.field_name.IS_RECRUITING, - models.Crew.field_name.IS_ACTIVE, - models.Crew.field_name.CREATED_BY, - ] - - def save(self, **kwargs): - languages = self.validated_data.pop('languages') - with atomic(): - instance = super().save(**kwargs) - service = servicesa.get_crew_service(instance) - service.set_languages(languages) - return instance - - -class RecruitingCrewSerializer(serializers.ModelSerializer): - """크루 목록""" - - is_joinable = fields.CrewIsJoinableField() - members = fields.CrewMemberCountField() - tags = fields.CrewTagsField() - latest_activity = fields.LatestActivityField() - - class Meta: - model = models.Crew - fields = [ - PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.IS_ACTIVE, - 'is_joinable', - 'members', - 'tags', - 'latest_activity', - ] - read_only_fields = ['__all__'] - - -class MyCrewSerializer(serializers.ModelSerializer): - "나의 참여 크루" - - latest_activity = fields.LatestActivityField() - - class Meta: - model = models.Crew - fields = [ - PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.IS_ACTIVE, - 'latest_activity', - ] - read_only_fields = ['__all__'] - - -class CrewDashboardSerializer(serializers.ModelSerializer): - """크루 대시보드 - - - 공지사항 - - 크루 태그 - - 나의 동료 - - 크루가 풀이한 문제 - - 풀이한 문제의 난이도 - """ - - tags = fields.CrewTagsField() - members = fields.CrewMembersField() - activities = fields.CrewActivitiesField() - is_captain = fields.IsCrewCaptainField() - - class Meta: - model = models.Crew - fields = [ - PK, - models.Crew.field_name.ICON, - models.Crew.field_name.NAME, - models.Crew.field_name.NOTICE, - 'is_captain', - 'tags', - 'members', - 'activities', - ] - read_only_fields = ['__all__'] - - -class CrewStatisticsSerializer(serializers.Serializer): - difficulty = fields.ProblemStatisticsDifficultyField() - tags = fields.ProblemStatisticsTagsField() - - -class CrewActivityDashboardSerializer(serializers.ModelSerializer): - problems = fields.CrewAcitivityProblemsField() - - class Meta: - model = models.CrewActivity - fields = [ - PK, - 'problems', - ] - read_only_fields = ['__all__'] - - -class CrewApplicationAboutApplicantSerializer(serializers.ModelSerializer): - applicant = fields.CrewApplicationApplicantField() - - class Meta: - model = models.CrewApplication - fields = [ - PK, - models.CrewApplication.field_name.MESSAGE, - models.CrewApplication.field_name.IS_PENDING, - models.CrewApplication.field_name.IS_ACCEPTED, - models.CrewApplication.field_name.CREATED_AT, - 'applicant', - ] - read_only_fields = ['__all__'] - - -class CrewApplicationSerializer(serializers.ModelSerializer): - class Meta: - model = models.CrewApplication - - -class CrewApplicationCreateSerializer(serializers.ModelSerializer): - message = serializers.CharField() - - class Meta: - model = models.CrewApplication - fields = [ - models.CrewApplication.field_name.MESSAGE, - ] - read_only_fields = ['__all__'] diff --git a/app/crews/serializersaaa/__init__.py b/app/crews/serializersaaa/__init__.py deleted file mode 100644 index 06e775b..0000000 --- a/app/crews/serializersaaa/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -from django.db.transaction import atomic -from rest_framework import serializers - -from crews import enums -from crews import models -from crews import servicesa -from crews.serializersaaa import fields - - -PK = 'id' - - -class NoInputSerializer(serializers.Serializer): - pass - - - -# Crew Retrieve Serializers - -class CrewRecruitingSerializer(serializers.ModelSerializer): - ... - - -class CrewJoinedSerializer(serializers.ModelSerializer): - ... - - -class CrewDashboardSerializer(serializers.ModelSerializer): - ... - - -class CrewCreateSerializer(serializers.ModelSerializer): - ... \ No newline at end of file diff --git a/app/crews/serializersaaa/fields.py b/app/crews/serializersaaa/fields.py deleted file mode 100644 index 9b74221..0000000 --- a/app/crews/serializersaaa/fields.py +++ /dev/null @@ -1,153 +0,0 @@ -from typing import List - -from rest_framework import serializers - -from boj.services import get_boj_user_service -from app.crews.servicesa import dto -from crews import enums -from crews import models -from crews import servicesa -from crews import utils -from users.models import User - - -class LatestActivityField(serializers.SerializerMethodField): - """마지막 활동회차""" - - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - if not crew.is_active: - return { - "name": "활동 종료", - "date_start_at": None, - "date_end_at": None, - } - try: - service = servicesa.get_crew_service(crew) - activity = service.query_activities_published().latest() - except models.CrewActivity.DoesNotExist: - return { - "name": "등록된 활동 없음", - "date_start_at": None, - "date_end_at": None, - } - else: - return { - "name": activity.name, - "date_start_at": activity.start_at, - "date_end_at": activity.end_at, - } - - -class IsCrewCaptainField(serializers.SerializerMethodField): - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - user = serializers.CurrentUserDefault()(self) - return service.is_captain(user) - - -class CrewMembersField(serializers.SerializerMethodField): - """나의 동료""" - - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - image_field = serializers.ImageField() - return [ - { - "username": member.user.username, - "profile_image": image_field.to_representation(member.user.profile_image), - "is_captain": member.is_captain, - } - for member in service.query_members() - ] - - -class CrewMemberCountField(serializers.SerializerMethodField): - """크루 인원""" - - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - return { - "count": service.query_members().count(), - "max_count": crew.max_members, - } - - -class CrewTagsField(serializers.SerializerMethodField): - """크루 태그""" - - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - return [ - { - 'key': tag.key, - 'name': tag.name, - 'type': tag.type.value, - } - for tag in service.tags() - ] - - -class CrewIsJoinableField(serializers.SerializerMethodField): - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - user = serializers.CurrentUserDefault()(self) - return service.validate_applicant(user) - - -class CrewIsMemberField(serializers.SerializerMethodField): - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - user = serializers.CurrentUserDefault()(self) - return service.is_member(user) - - -class CrewActivitiesField(serializers.SerializerMethodField): - def to_representation(self, crew: models.Crew): - assert isinstance(crew, models.Crew) - service = servicesa.get_crew_service(crew) - return [ - { - 'activity_id': activity.activity_id, - 'name': activity.name, - } - for activity in service.query_activities() - ] - - -class CrewAcitivityProblemsField(serializers.SerializerMethodField): - def to_representation(self, activity: models.CrewActivity): - assert isinstance(activity, models.CrewActivity) - queryset = models.CrewActivityProblem.objects.filter(**{ - models.CrewActivityProblem.field_name.ACTIVITY: activity, - }) - return [ - { - 'is_solved': problem, - } - for problem in queryset - ] - - -class CrewApplicationApplicantField(serializers.SerializerMethodField): - def to_representation(self, instance: models.CrewApplication): - assert isinstance(instance, models.CrewApplication) - service = get_boj_user_service(instance.applicant.boj_username) - level = service.level() - return { - "user_id": instance.applicant.pk, - "username": instance.applicant.username, - "profile_image": instance.applicant.profile_image.url, - "boj": { - "level": { - "value": level.value, - "name": level.get_name(lang="ko", arabic=False), - } - } - } diff --git a/app/crews/servicesa/__init__.py b/app/crews/servicesa/__init__.py deleted file mode 100644 index 80d161e..0000000 --- a/app/crews/servicesa/__init__.py +++ /dev/null @@ -1,25 +0,0 @@ -from crews.servicesa.base import UserCrewService -from crews.servicesa.base import CrewService -from crews.servicesa.base import CrewActivityService -from crews.servicesa.base import CrewApplicantionService -from crews.servicesa.concrete import ConcreteUserCrewService -from crews.servicesa.concrete import ConcreteCrewService - -from crews import models -from users.models import User - - -def get_user_crew_service(user: User) -> UserCrewService: - return ConcreteUserCrewService(user) - - -def get_crew_service(crew: models.Crew) -> CrewService: - return ConcreteCrewService(crew) - - -def get_crew_activity_service(crew_activity: models.CrewActivity) -> CrewActivityService: - return CrewActivityService(crew_activity) - - -def get_crew_application_service(crew_application: models.CrewApplication) -> CrewApplicantionService: - return CrewApplicantionService(crew_application) diff --git a/app/crews/servicesa/base.py b/app/crews/servicesa/base.py deleted file mode 100644 index f54c672..0000000 --- a/app/crews/servicesa/base.py +++ /dev/null @@ -1,82 +0,0 @@ -from typing import List - -from django.db.models import QuerySet - -from boj.enums import BOJLevel -from app.crews.servicesa import dto -from crews import enums -from crews import models -from problems.dto import ProblemStatisticDTO -from problems.models import Problem -from users.models import User - - -class UserCrewService: - def __init__(self, instance: User) -> None: - assert isinstance(instance, User) - self.instance = instance - - def query_crews_joined(self) -> QuerySet[models.Crew]: - """자신이 멤버로 있는 크루를 조회하는 쿼리를 반환""" - ... - - def query_crews_recruiting(self) -> QuerySet[models.Crew]: - """자신이 멤버로 있지 않으면서 크루원을 모집중인 크루를 조회하는 쿼리를 반환""" - ... - - -class CrewService: - def __init__(self, instance: models.Crew) -> None: - assert isinstance(instance, models.Crew) - self.instance = instance - - def query_members(self) -> QuerySet[models.CrewMember]: - ... - - def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: - ... - - def query_applications(self) -> QuerySet[models.CrewApplication]: - ... - - def query_activities(self) -> QuerySet[models.CrewActivity]: - ... - - def query_activities_published(self) -> QuerySet[models.CrewActivity]: - """크루원들에게 공개된 활동들""" - ... - - def query_problems(self) -> QuerySet[Problem]: - ... - - def query_captain(self) -> QuerySet[models.CrewMember]: - ... - - def statistics(self) -> ProblemStatisticDTO: - """대쉬보드에 사용되는 크루가 풀이해온 문제 통계""" - ... - - def display_name(self) -> str: - ... - - def tags(self) -> List[dto.CrewTag]: - ... - - def languages(self) -> List[enums.ProgrammingLanguageChoices]: - ... - - def min_boj_level(self) -> BOJLevel: - ... - - def is_captain(self, user: User) -> bool: - ... - - def is_member(self, user: User) -> bool: - ... - - def set_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> None: - ... - - def apply(self, applicant: User, message: str) -> models.CrewApplication: - """지원자가 자격요건을 갖추지 못했다면 ValidationError를 발생시킬 수 있다.""" - ... diff --git a/app/crews/servicesa/concrete.py b/app/crews/servicesa/concrete.py deleted file mode 100644 index f6edd48..0000000 --- a/app/crews/servicesa/concrete.py +++ /dev/null @@ -1,189 +0,0 @@ -from textwrap import dedent -from typing import List - -from background_task import background -from django.core.mail import send_mail -from django.db.models import QuerySet -from django.db.transaction import atomic -from django.utils import timezone -from rest_framework.exceptions import ValidationError - -from boj.enums import BOJLevel -from app.crews.servicesa import dto -from crews import enums -from crews import models -from crews.servicesa.base import UserCrewService -from crews.servicesa.base import CrewService -from crews.servicesa.base import CrewActivityService -from crews.servicesa.base import CrewApplicantionService -from problems.dto import ProblemStatisticDTO -from problems.models import Problem -from users.models import User - - -class ConcreteUserCrewService(UserCrewService): - def query_crews_joined(self) -> QuerySet[models.Crew]: - # 활동 종료된 크루는 뒤로 가도록 정렬 - return models.Crew.objects.filter( - pk__in=self._crew_ids_list_joined(), - ).order_by( - '-'+models.Crew.field_name.IS_ACTIVE, - ) - - def query_crews_recruiting(self) -> QuerySet[models.Crew]: - return models.Crew.objects.filter(**{ - models.Crew.field_name.IS_RECRUITING: True, - }).exclude( - pk__in=self._crew_ids_list_joined(), - ) - - def _crew_ids_list_joined(self) -> List[int]: - if not self.instance.is_authenticated: - return [] - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: self.instance, - }).values_list(models.CrewMember.field_name.CREW) - - -class ConcreteCrewService(CrewService): - def query_members(self) -> QuerySet[models.CrewMember]: - ... - - def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: - ... - - def query_applications(self) -> QuerySet[models.CrewApplication]: - ... - - def query_activities(self) -> QuerySet[models.CrewActivity]: - ... - - def query_activities_published(self) -> QuerySet[models.CrewActivity]: - ... - - def query_problems(self) -> QuerySet[Problem]: - ... - - def query_captain(self) -> QuerySet[models.CrewMember]: - ... - - def statistics(self) -> ProblemStatisticDTO: - ... - - def display_name(self) -> str: - ... - - def tags(self) -> List[dto.CrewTag]: - ... - - def languages(self) -> List[enums.ProgrammingLanguageChoices]: - ... - - def min_boj_level(self) -> BOJLevel: - ... - - def is_captain(self, user: User) -> bool: - ... - - def is_member(self, user: User) -> bool: - ... - - def set_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> None: - ... - - def apply(self, applicant: User, message: str) -> models.CrewApplication: - ... - - # send notification - subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 도착했습니다' - message=dedent(f""" - [{self.instance.crew.icon} {self.instance.crew.name}]에 새로운 가입 신청이 왔어요! - - 지원자: {applicant.applicant.username} - 지원자의 백준 아이디(레벨): {applicant.applicant.boj_username} ({users.models.UserBojLevelChoices(applicant.applicant.boj_level).get_name(lang='ko', arabic=False)}) - - 지원자의 메시지: - ``` - {applicant.message} - ``` - - 수락하시려면 [여기]를 클릭해주세요. - """) - recipient=self.query_captain().get().user.email - schedule_mail(subject, message, recipient) - - def validate_applicant(self, applicant: User, raises_exception=False) -> bool: - ... - - -class ConcreteCrewActivityService(CrewActivityService): - def query_previous_activities(self) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: self.instance.crew, - models.CrewActivity.field_name.START_AT+'__lt': self.instance.start_at, - }) - - def nth(self) -> int: - return self.query_previous_activities().count()+1 - - def is_in_progress(self) -> bool: - return self.has_started() and not self.has_ended() - - def has_started(self) -> bool: - return self.instance.start_at <= timezone.now() - - def has_ended(self) -> bool: - return self.instance.end_at < timezone.now() - - -class ConcreteCrewApplicantionService(CrewApplicantionService): - def reject(self, reviewed_by: User): - self.instance.is_pending = False - self.instance.is_accepted = True - self.instance.reviewed_by = reviewed_by - self.instance.reviewed_at = timezone.now() - self.instance.save() - - # send notification - subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 거절되었습니다' - message=dedent(f""" - [{self.instance.crew.icon} {self.instance.crew.name}]에 아쉽게도 가입하지 못했어요. - """) - recipient=self.instance.applicant.email - schedule_mail(subject, message, recipient) - - def accept(self, reviewed_by: User): - self.instance.is_pending = False - self.instance.is_accepted = False - self.instance.reviewed_by = reviewed_by - self.instance.reviewed_at = timezone.now() - with atomic(): - self.instance.save() - self._save_member() - - # send notification - subject='[Time Limit Exceeded] 새로운 크루 가입 신청이 승인되었습니다' - message=dedent(f""" - [{self.instance.crew.icon} {self.instance.crew.name}]에 가입하신 것을 축하해요! - - [여기]를 눌러 크루 대시보드로 바로가기 - """) - recipient=self.instance.applicant.email - schedule_mail(subject, message, recipient) - - def _save_member(self) -> models.CrewMember: - return models.CrewMember.objects.create(**{ - models.CrewApplication.field_name.CREW: self.instance.crew, - models.CrewApplication.field_name.APPLICANT: self.instance.applicant, - }) - - -@background -def schedule_mail(subject: str, message: str, recipient: str) -> None: - send_mail( - subject=subject, - message=message, - recipient_list=[recipient], - from_email=None, - fail_silently=False, - ) diff --git a/app/crews/servicesa/crew_activity_service.py b/app/crews/servicesa/crew_activity_service.py deleted file mode 100644 index b2ccfdd..0000000 --- a/app/crews/servicesa/crew_activity_service.py +++ /dev/null @@ -1,47 +0,0 @@ -from __future__ import annotations - -from django.db.models import QuerySet -from django.utils import timezone - -from crews import models - - -class CrewActivityService: - @staticmethod - def query_all(crew: models.Crew, order_by=[models.CrewActivity.field_name.START_AT]) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: crew, - }).order_by(*order_by) - - @staticmethod - def query_in_progress(crew: models.Crew) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: crew, - models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), - models.CrewActivity.field_name.END_AT + '__gt': timezone.now(), - }) - - @staticmethod - def query_has_started(crew: models.Crew) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: crew, - models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), - }) - - @staticmethod - def query_has_ended(crew: models.Crew) -> QuerySet[models.CrewActivity]: - return models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: crew, - models.CrewActivity.field_name.END_AT + '__lt': timezone.now(), - }) - - @staticmethod - def last_started(crew: models.Crew) -> CrewActivityService: - """ - 주의: CrewActivity.DoesNotExist를 발생시킬 수도 있습니다. - """ - instance = models.CrewActivity.objects.filter(**{ - models.CrewActivity.field_name.CREW: crew, - models.CrewActivity.field_name.START_AT + '__lte': timezone.now(), - }).latest() - return CrewActivityService(instance) diff --git a/app/crews/servicesa/crew_service.py b/app/crews/servicesa/crew_service.py deleted file mode 100644 index bf834c3..0000000 --- a/app/crews/servicesa/crew_service.py +++ /dev/null @@ -1,242 +0,0 @@ -from typing import List -from typing import Iterable - -from django.db.models import QuerySet -from django.db.transaction import atomic -from rest_framework import exceptions - -from boj.services import get_boj_user_service -from app.crews.servicesa import dto -from crews import enums -from crews import models -from crews.servicesa.crew_activity_service import CrewActivityService -from problems.models import Problem -from problems.services import ProblemService -from users.models import User -from users.models import UserBojLevelChoices - - -class CrewService: - @staticmethod - def create(languages: List[str] = [], **fields) -> models.Crew: - with atomic(): - crew = models.Crew.objects.create(**fields) - service = CrewService(crew) - service.save_languages(languages) - - @staticmethod - def query_as_member(user: User) -> QuerySet[models.Crew]: - """자신이 멤버로 있는 크루를 조회하는 쿼리를 반환""" - # TODO: Query 최적화 필요 - crew_ids = CrewService._crew_ids_as_member(user) - return models.Crew.objects.filter(pk__in=crew_ids) - - @staticmethod - def query_recruiting(user: User) -> QuerySet[models.Crew]: - """자신이 멤버로 있지 않으면서 크루원을 모집중인 크루를 조회하는 쿼리를 반환""" - crew_ids = CrewService._crew_ids_as_member(user) - return models.Crew.objects.filter(**{ - models.Crew.field_name.IS_RECRUITING: True, - }).exclude(pk__in=crew_ids) - - @staticmethod - def _crew_ids_as_member(user: User) -> List[int]: - if not user.is_authenticated: - return [] - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.USER: user, - }).values_list(models.CrewMember.field_name.CREW) - - def __init__(self, instance: models.Crew) -> None: - assert isinstance(instance, models.Crew) - self.instance = instance - - def query_members(self) -> QuerySet[models.CrewMember]: - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: self.instance, - }) - - def query_languages(self) -> QuerySet[models.CrewSubmittableLanguage]: - return models.CrewSubmittableLanguage.objects.filter(**{ - models.CrewSubmittableLanguage.field_name.CREW: self.instance, - }) - - def query_applications(self) -> QuerySet[models.CrewApplication]: - return models.CrewApplication.objects.filter(**{ - models.CrewApplication.field_name.CREW: self.instance, - }) - - def query_activities(self) -> QuerySet[models.CrewActivity]: - return CrewActivityService.query_all(self.instance) - - def query_problems(self) -> QuerySet[models.CrewActivityProblem]: - return models.CrewActivityProblem.objects.filter(**{ - models.CrewActivityProblem.field_name.CREW: self.instance, - }) - - def statistics(self) -> dto.ProblemStatistic: - stat = dto.ProblemStatistic() - for problem in self.problems(): - service = ProblemService(problem) - for tag in service.tags(): - problem_tag = dto.ProblemTag( - key=tag.key, - name_ko=tag.name_ko, - name_en=tag.name_en, - ) - stat.tags[problem_tag] += 1 - stat.difficulty[service.difficulty()] += 1 - stat.sample_count += 1 - return stat - - def activities(self) -> List[dto.CrewActivity]: - activities = [] - queryset = CrewActivityService.query_all(self.instance) - for nth, entity in enumerate(queryset, start=1): - service = CrewActivityService(entity) - # TODO: 회차 이름을 모델 생성과 함께 고정 - activities.append(dto.CrewActivity( - activity_id=service.instance.pk, - name=f'{nth}회차', - start_at=service.instance.start_at, - end_at=service.instance.end_at, - is_in_progress=service.is_in_progress(), - has_started=service.has_started(), - has_ended=service.has_ended(), - )) - return activities - - def captain(self) -> models.CrewMember: - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: self.instance, - models.CrewMember.field_name.IS_CAPTAIN: True, - }).get() - - def problems(self) -> List[Problem]: - return models.CrewActivityProblem.objects.filter(**{ - models.CrewActivityProblem.field_name.CREW: self.instance, - }).values_list(models.CrewActivityProblem.field_name.PROBLEM, flat=True) - - def display_name(self) -> str: - return f'{self.instance.icon} {self.instance.name}' - - def languages(self) -> List[enums.ProgrammingLanguageChoices]: - languages = [] - for submittable_language in self.query_languages(): - language = enums.ProgrammingLanguageChoices(submittable_language.language) - languages.append(language) - return languages - - def save_languages(self, languages: List[enums.ProgrammingLanguageChoices]) -> List[models.CrewSubmittableLanguage]: - assert isinstance(languages, list) - self.delete_languages() - entities = [] - for language in languages: - self._validate_language(language) - entity = models.CrewSubmittableLanguage(**{ - models.CrewSubmittableLanguage.field_name.CREW: self.instance, - models.CrewSubmittableLanguage.field_name.LANGUAGE: language, - }) - entities.append(entity) - return models.CrewSubmittableLanguage.objects.bulk_create(entities) - - def _validate_language(self, language: str): - assert isinstance(language, str) or isinstance( - language, enums.ProgrammingLanguageChoices) - if language not in enums.ProgrammingLanguageChoices: - raise exceptions.ValidationError(f'{language}은 선택 가능한 언어가 아닙니다.') - - def delete_languages(self): - self.query_languages().delete() - - def min_boj_level(self) -> UserBojLevelChoices: - if self.instance.min_boj_level is None: - return UserBojLevelChoices.U - return UserBojLevelChoices(self.instance.min_boj_level) - - def is_captain(self, user: User) -> bool: - assert isinstance(user, User) - return self.captain().user == user - - def is_member(self, user: User) -> bool: - assert isinstance(user, User) - return models.CrewMember.objects.filter(**{ - models.CrewMember.field_name.CREW: self.instance, - models.CrewMember.field_name.USER: user, - }).exists() - - def validate_applicant(self, applicant: User, raises_exception=False) -> bool: - assert isinstance(applicant, User) - try: - self._validate_applicant_boj_level(applicant) - except exceptions.ValidationError as exception: - if not raises_exception: - return False - raise exception - return True - - def _validate_applicant(self, applicant: User): - if not self.instance.is_recruiting: - raise exceptions.ValidationError('크루가 현재 크루원을 모집하고 있지 않습니다.') - if self.query_members().count() >= self.instance.max_members: - raise exceptions.ValidationError('크루의 최대 정원을 초과하였습니다.') - if self.is_member(applicant): - raise exceptions.ValidationError('이미 가입한 크루입니다.') - self._validate_applicant_boj_level(applicant) - - def validate_applicant_boj_level(self, applicant: User, raises_exception=False) -> bool: - assert isinstance(applicant, User) - try: - self._validate_applicant_boj_level(applicant) - except exceptions.ValidationError as exception: - if not raises_exception: - return False - raise exception - return True - - def _validate_applicant_boj_level(self, applicant: User): - if self.instance.min_boj_level is None: - return - service = get_boj_user_service(applicant.boj_username) - if service.instance.level < self.instance.min_boj_level: - raise exceptions.ValidationError('최소 백준 레벨 요구조건을 달성하지 못하였습니다.') - - def tags(self) -> List[dto.CrewTag]: - # 태그의 나열 순서는 리스트에 선언한 순서를 따름. - return [ - *self._get_language_tags(), - *self._get_min_level_tags(), - *self._get_custom_tags(), - ] - - def _get_language_tags(self) -> Iterable[dto.CrewTag]: - for language in self.languages(): - yield dto.CrewTag( - key=language.value, - name=language.label, - type=enums.CrewTagType.LANGUAGE, - ) - - def _get_min_level_tags(self) -> Iterable[dto.CrewTag]: - yield dto.CrewTag( - key=None, - name=self._get_min_level_tag_name(), - type=enums.CrewTagType.LEVEL, - ) - - def _get_min_level_tag_name(self) -> str: - min_level = self.min_boj_level() - if min_level == UserBojLevelChoices.U: - return '티어 무관' - elif min_level.get_tier() == 5: - return f"{min_level.get_division_name(lang='ko')} 이상" - else: - return f"{min_level.get_name(lang='ko', arabic=False)} 이상" - - def _get_custom_tags(self) -> Iterable[dto.CrewTag]: - for tag in self.instance.custom_tags: - yield dto.CrewTag( - key=None, - name=tag, - type=enums.CrewTagType.CUSTOM, - ) diff --git a/app/crews/servicesa/dto.py b/app/crews/servicesa/dto.py deleted file mode 100644 index 160758d..0000000 --- a/app/crews/servicesa/dto.py +++ /dev/null @@ -1,67 +0,0 @@ -from collections import Counter -from dataclasses import dataclass -from dataclasses import field -from datetime import datetime -from typing import List - -from crews import enums -from problems.analyses.enums import ProblemDifficulty - - -@dataclass -class ProblemTag: - key: str - name_ko: str - name_en: str - - def __hash__(self) -> int: - return self.key - - -@dataclass -class ProblemStatistic: - sample_count: int = field(default=0) - difficulty: Counter[int] = field(default_factory=Counter) - tags: Counter[ProblemTag] = field(default_factory=Counter) - - -@dataclass -class CrewTag: - key: str - name: str - type: enums.CrewTagType - - -@dataclass -class CrewProblem: - problem_number: int - problem_id: int - problem_title: str - problem_difficulty: ProblemDifficulty - is_submitted: bool - last_submitted_date: datetime - - -@dataclass -class CrewActivity: - activity_id: int - name: str - start_at: datetime - end_at: datetime - is_in_progress: bool - has_started: bool - has_ended: bool - - -@dataclass -class SubmissionGraphNode: - problem_number: int - submitted_at: datetime - is_accepted: bool # 정답인지 여부 - - -@dataclass -class SubmissionGraph: - user_username: str - user_profile_image: str - submissions: List[SubmissionGraphNode] diff --git a/app/submissions/__init__.py b/app/crews/submissions/__init__.py similarity index 100% rename from app/submissions/__init__.py rename to app/crews/submissions/__init__.py diff --git a/app/submissions/admin.py b/app/crews/submissions/admin.py similarity index 71% rename from app/submissions/admin.py rename to app/crews/submissions/admin.py index 5a5005b..d944fcf 100644 --- a/app/submissions/admin.py +++ b/app/crews/submissions/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from submissions.models import * +from crews.submissions.models import * admin.site.register([ diff --git a/app/submissions/apps.py b/app/crews/submissions/apps.py similarity index 65% rename from app/submissions/apps.py rename to app/crews/submissions/apps.py index 1ffc7b6..348c1ba 100644 --- a/app/submissions/apps.py +++ b/app/crews/submissions/apps.py @@ -3,4 +3,5 @@ class SubmissionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "submissions" + name = "crews.submissions" + verbose_name = "Crew submissions" diff --git a/app/submissions/migrations/__init__.py b/app/crews/submissions/migrations/__init__.py similarity index 100% rename from app/submissions/migrations/__init__.py rename to app/crews/submissions/migrations/__init__.py diff --git a/app/crews/submissions/models/__init__.py b/app/crews/submissions/models/__init__.py new file mode 100644 index 0000000..300481f --- /dev/null +++ b/app/crews/submissions/models/__init__.py @@ -0,0 +1,8 @@ +from crews.submissions.models.submission import Submission +from crews.submissions.models.submission_comment import SubmissionComment + + +__all__ = ( + 'Submission', + 'SubmissionComment', +) diff --git a/app/submissions/models/submission.py b/app/crews/submissions/models/submission.py similarity index 100% rename from app/submissions/models/submission.py rename to app/crews/submissions/models/submission.py diff --git a/app/submissions/models/submission_comment.py b/app/crews/submissions/models/submission_comment.py similarity index 96% rename from app/submissions/models/submission_comment.py rename to app/crews/submissions/models/submission_comment.py index 7b094b4..9f252b8 100644 --- a/app/submissions/models/submission_comment.py +++ b/app/crews/submissions/models/submission_comment.py @@ -1,7 +1,7 @@ from django.core.validators import MinValueValidator from django.db import models -from submissions.models.submission import Submission +from crews.submissions.models.submission import Submission from users.models import User diff --git a/app/submissions/serializers/__init__.py b/app/crews/submissions/serializers/__init__.py similarity index 90% rename from app/submissions/serializers/__init__.py rename to app/crews/submissions/serializers/__init__.py index f5d91f6..51c617b 100644 --- a/app/submissions/serializers/__init__.py +++ b/app/crews/submissions/serializers/__init__.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from submissions.models import Submission +from crews.submissions.models import Submission class SubmissionSerializer(serializers.ModelField): diff --git a/app/submissions/views.py b/app/crews/submissions/views.py similarity index 98% rename from app/submissions/views.py rename to app/crews/submissions/views.py index 8cdaff5..6cbf39c 100644 --- a/app/submissions/views.py +++ b/app/crews/submissions/views.py @@ -6,7 +6,7 @@ from rest_framework.serializers import Serializer from rest_framework.response import Response -from submissions import serializers +from crews.submissions import serializers class CreateCodeReview(generics.RetrieveAPIView): diff --git a/app/submissions/models/__init__.py b/app/submissions/models/__init__.py deleted file mode 100644 index f8dcd36..0000000 --- a/app/submissions/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from submissions.models.submission import Submission -from submissions.models.submission_comment import SubmissionComment - - -__all__ = ( - 'Submission', - 'SubmissionComment', -) From fd6f7f813a4561b21e6c418e70b96472526292e4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 12:07:46 +0900 Subject: [PATCH 548/552] =?UTF-8?q?refactor!:=20users=EB=A5=BC=20=EC=A0=9C?= =?UTF-8?q?=EC=99=B8=ED=95=9C=20=EB=AA=A8=EB=93=A0=20=EC=95=B1=EC=9D=84=20?= =?UTF-8?q?apps/*=EB=A1=9C=20=EC=98=AE=EA=B9=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../management => apps}/__init__.py | 0 app/{ => apps}/background_task/README.md | 0 app/{ => apps}/background_task/__init__.py | 2 +- app/{ => apps}/background_task/admin.py | 4 ++-- app/{ => apps}/background_task/apps.py | 8 ++++---- app/{ => apps}/background_task/exceptions.py | 0 .../background_task/management}/__init__.py | 0 .../management/commands}/__init__.py | 0 .../management/commands/process_tasks.py | 4 ++-- .../background_task/migrations/0001_initial.py | 0 .../background_task/migrations}/__init__.py | 0 app/{ => apps}/background_task/models.py | 8 ++++---- app/{ => apps}/background_task/settings.py | 0 app/{ => apps}/background_task/signals.py | 2 +- app/{ => apps}/background_task/tasks.py | 8 ++++---- app/{ => apps}/background_task/utils.py | 0 app/{boj/migrations => apps/boj}/__init__.py | 0 app/{ => apps}/boj/admin.py | 8 ++++---- app/{ => apps}/boj/apps.py | 4 ++-- app/{ => apps}/boj/enums.py | 0 app/{ => apps}/boj/migrations/0001_initial.py | 0 .../boj/migrations}/__init__.py | 0 app/{ => apps}/boj/models.py | 2 +- app/{ => apps}/boj/serializers.py | 4 ++-- app/{ => apps}/boj/services.py | 8 ++++---- app/{ => apps}/crews/__init__.py | 0 .../crews/activities}/__init__.py | 0 app/{ => apps}/crews/activities/admin.py | 8 ++++---- app/{ => apps}/crews/activities/apps.py | 2 +- app/{ => apps}/crews/activities/dto.py | 3 +-- .../activities/migrations/0001_initial.py | 0 .../activities/migrations/0002_initial.py | 0 .../crews/activities/migrations}/__init__.py | 0 app/{ => apps}/crews/activities/models.py | 6 +++--- app/{ => apps}/crews/activities/serializers.py | 2 +- app/{ => apps}/crews/activities/views.py | 8 +++----- app/{ => apps}/crews/admin.py | 10 +++++----- .../crews/applications}/__init__.py | 0 app/{ => apps}/crews/applications/admin.py | 6 +++--- app/{ => apps}/crews/applications/apps.py | 2 +- .../applications/migrations/0001_initial.py | 0 .../applications/migrations/0002_initial.py | 0 .../crews/applications}/migrations/__init__.py | 0 app/{ => apps}/crews/applications/models.py | 2 +- .../crews/applications/permissions.py | 4 ++-- .../crews/applications/serializers.py | 2 +- app/{ => apps}/crews/applications/services.py | 14 +++++++------- app/{ => apps}/crews/applications/signals.py | 0 app/{ => apps}/crews/applications/views.py | 12 ++++++------ app/{ => apps}/crews/apps.py | 2 +- app/{ => apps}/crews/dto.py | 2 +- app/{ => apps}/crews/enums.py | 0 app/{ => apps}/crews/fixtures/sample.json | 0 .../crews/migrations/0001_initial.py | 0 .../crews/migrations}/__init__.py | 0 app/{ => apps}/crews/models.py | 10 +++++----- app/{ => apps}/crews/permissions.py | 4 ++-- app/{ => apps}/crews/serializers.py | 16 ++++++++-------- .../crews/submissions}/__init__.py | 0 app/{ => apps}/crews/submissions/admin.py | 2 +- app/{ => apps}/crews/submissions/apps.py | 2 +- .../crews/submissions/migrations}/__init__.py | 0 app/apps/crews/submissions/models/__init__.py | 8 ++++++++ .../crews/submissions/models/submission.py | 4 ++-- .../submissions/models/submission_comment.py | 2 +- .../crews/submissions/serializers/__init__.py | 2 +- app/{ => apps}/crews/submissions/views.py | 2 +- app/{ => apps}/crews/tests.py | 0 app/{ => apps}/crews/urls.py | 17 ++++++++++------- app/{ => apps}/crews/views.py | 18 +++++++++--------- .../analyses => apps/problems}/__init__.py | 0 app/{ => apps}/problems/admin.py | 4 ++-- .../problems/analyses}/__init__.py | 0 app/{ => apps}/problems/analyses/admin.py | 10 +++++----- app/{ => apps}/problems/analyses/apps.py | 2 +- app/{ => apps}/problems/analyses/dto.py | 2 +- app/{ => apps}/problems/analyses/enums.py | 0 .../analyses/fixtures/problem_tag.sample.json | 0 .../analyses/migrations/0001_initial.py | 0 .../analyses/migrations/0002_initial.py | 0 .../problems/analyses/migrations}/__init__.py | 0 app/{ => apps}/problems/analyses/models.py | 8 ++++---- .../problems/analyses/serializers.py | 8 ++++---- app/{ => apps}/problems/analyzers/__init__.py | 16 ++++++++-------- app/{ => apps}/problems/analyzers/base.py | 4 ++-- app/apps/problems/analyzers/gemini/__init__.py | 1 + .../problems/analyzers/gemini/analyzer.py | 10 +++++----- .../problems/analyzers/gemini/parsers.py | 2 +- .../problems/analyzers/gemini/prompts.py | 4 ++-- .../problems/analyzers/gpt}/__init__.py | 0 .../problems/analyzers/gpt/analyzer.py | 4 ++-- app/{ => apps}/problems/apps.py | 4 ++-- app/{ => apps}/problems/dto.py | 2 +- app/{ => apps}/problems/enums.py | 0 .../problems/migrations/0001_initial.py | 0 app/apps/problems/migrations/__init__.py | 0 app/{ => apps}/problems/models.py | 4 ++-- app/{ => apps}/problems/serializers.py | 18 +++++++++--------- app/{ => apps}/problems/statistics.py | 10 +++++----- app/{ => apps}/problems/urls.py | 2 +- app/{ => apps}/problems/views.py | 4 ++-- app/config/settings/base/core.py | 16 ++++++++-------- app/config/urls.py | 8 ++++---- app/crews/submissions/models/__init__.py | 8 -------- app/problems/analyzers/gemini/__init__.py | 1 - app/users/serializers.py | 4 ++-- app/users/services.py | 2 +- 107 files changed, 196 insertions(+), 196 deletions(-) rename app/{background_task/management => apps}/__init__.py (100%) rename app/{ => apps}/background_task/README.md (100%) rename app/{ => apps}/background_task/__init__.py (79%) rename app/{ => apps}/background_task/admin.py (91%) rename app/{ => apps}/background_task/apps.py (73%) rename app/{ => apps}/background_task/exceptions.py (100%) rename app/{background_task/management/commands => apps/background_task/management}/__init__.py (100%) rename app/{background_task/migrations => apps/background_task/management/commands}/__init__.py (100%) rename app/{ => apps}/background_task/management/commands/process_tasks.py (97%) rename app/{ => apps}/background_task/migrations/0001_initial.py (100%) rename app/{boj => apps/background_task/migrations}/__init__.py (100%) rename app/{ => apps}/background_task/models.py (98%) rename app/{ => apps}/background_task/settings.py (100%) rename app/{ => apps}/background_task/signals.py (95%) rename app/{ => apps}/background_task/tasks.py (98%) rename app/{ => apps}/background_task/utils.py (100%) rename app/{boj/migrations => apps/boj}/__init__.py (100%) rename app/{ => apps}/boj/admin.py (86%) rename app/{ => apps}/boj/apps.py (73%) rename app/{ => apps}/boj/enums.py (100%) rename app/{ => apps}/boj/migrations/0001_initial.py (100%) rename app/{crews/activities => apps/boj/migrations}/__init__.py (100%) rename app/{ => apps}/boj/models.py (98%) rename app/{ => apps}/boj/serializers.py (93%) rename app/{ => apps}/boj/services.py (92%) rename app/{ => apps}/crews/__init__.py (100%) rename app/{crews/activities/migrations => apps/crews/activities}/__init__.py (100%) rename app/{ => apps}/crews/activities/admin.py (85%) rename app/{ => apps}/crews/activities/apps.py (82%) rename app/{ => apps}/crews/activities/dto.py (90%) rename app/{ => apps}/crews/activities/migrations/0001_initial.py (100%) rename app/{ => apps}/crews/activities/migrations/0002_initial.py (100%) rename app/{crews/applications => apps/crews/activities/migrations}/__init__.py (100%) rename app/{ => apps}/crews/activities/models.py (97%) rename app/{ => apps}/crews/activities/serializers.py (94%) rename app/{ => apps}/crews/activities/views.py (50%) rename app/{ => apps}/crews/admin.py (90%) rename app/{crews/applications/migrations => apps/crews/applications}/__init__.py (100%) rename app/{ => apps}/crews/applications/admin.py (85%) rename app/{ => apps}/crews/applications/apps.py (82%) rename app/{ => apps}/crews/applications/migrations/0001_initial.py (100%) rename app/{ => apps}/crews/applications/migrations/0002_initial.py (100%) rename app/{crews => apps/crews/applications}/migrations/__init__.py (100%) rename app/{ => apps}/crews/applications/models.py (98%) rename app/{ => apps}/crews/applications/permissions.py (70%) rename app/{ => apps}/crews/applications/serializers.py (96%) rename app/{ => apps}/crews/applications/services.py (93%) rename app/{ => apps}/crews/applications/signals.py (100%) rename app/{ => apps}/crews/applications/views.py (92%) rename app/{ => apps}/crews/apps.py (83%) rename app/{ => apps}/crews/dto.py (73%) rename app/{ => apps}/crews/enums.py (100%) rename app/{ => apps}/crews/fixtures/sample.json (100%) rename app/{ => apps}/crews/migrations/0001_initial.py (100%) rename app/{crews/submissions => apps/crews/migrations}/__init__.py (100%) rename app/{ => apps}/crews/models.py (97%) rename app/{ => apps}/crews/permissions.py (88%) rename app/{ => apps}/crews/serializers.py (94%) rename app/{crews/submissions/migrations => apps/crews/submissions}/__init__.py (100%) rename app/{ => apps}/crews/submissions/admin.py (69%) rename app/{ => apps}/crews/submissions/apps.py (82%) rename app/{problems => apps/crews/submissions/migrations}/__init__.py (100%) create mode 100644 app/apps/crews/submissions/models/__init__.py rename app/{ => apps}/crews/submissions/models/submission.py (92%) rename app/{ => apps}/crews/submissions/models/submission_comment.py (96%) rename app/{ => apps}/crews/submissions/serializers/__init__.py (89%) rename app/{ => apps}/crews/submissions/views.py (98%) rename app/{ => apps}/crews/tests.py (100%) rename app/{ => apps}/crews/urls.py (60%) rename app/{ => apps}/crews/views.py (79%) rename app/{problems/analyses => apps/problems}/__init__.py (100%) rename app/{ => apps}/problems/admin.py (89%) rename app/{problems/analyses/migrations => apps/problems/analyses}/__init__.py (100%) rename app/{ => apps}/problems/analyses/admin.py (90%) rename app/{ => apps}/problems/analyses/apps.py (82%) rename app/{ => apps}/problems/analyses/dto.py (89%) rename app/{ => apps}/problems/analyses/enums.py (100%) rename app/{ => apps}/problems/analyses/fixtures/problem_tag.sample.json (100%) rename app/{ => apps}/problems/analyses/migrations/0001_initial.py (100%) rename app/{ => apps}/problems/analyses/migrations/0002_initial.py (100%) rename app/{problems/analyzers/gpt => apps/problems/analyses/migrations}/__init__.py (100%) rename app/{ => apps}/problems/analyses/models.py (96%) rename app/{ => apps}/problems/analyses/serializers.py (91%) rename app/{ => apps}/problems/analyzers/__init__.py (81%) rename app/{ => apps}/problems/analyzers/base.py (76%) create mode 100644 app/apps/problems/analyzers/gemini/__init__.py rename app/{ => apps}/problems/analyzers/gemini/analyzer.py (89%) rename app/{ => apps}/problems/analyzers/gemini/parsers.py (97%) rename app/{ => apps}/problems/analyzers/gemini/prompts.py (97%) rename app/{problems/migrations => apps/problems/analyzers/gpt}/__init__.py (100%) rename app/{ => apps}/problems/analyzers/gpt/analyzer.py (80%) rename app/{ => apps}/problems/apps.py (70%) rename app/{ => apps}/problems/dto.py (91%) rename app/{ => apps}/problems/enums.py (100%) rename app/{ => apps}/problems/migrations/0001_initial.py (100%) create mode 100644 app/apps/problems/migrations/__init__.py rename app/{ => apps}/problems/models.py (96%) rename app/{ => apps}/problems/serializers.py (92%) rename app/{ => apps}/problems/statistics.py (71%) rename app/{ => apps}/problems/urls.py (89%) rename app/{ => apps}/problems/views.py (95%) delete mode 100644 app/crews/submissions/models/__init__.py delete mode 100644 app/problems/analyzers/gemini/__init__.py diff --git a/app/background_task/management/__init__.py b/app/apps/__init__.py similarity index 100% rename from app/background_task/management/__init__.py rename to app/apps/__init__.py diff --git a/app/background_task/README.md b/app/apps/background_task/README.md similarity index 100% rename from app/background_task/README.md rename to app/apps/background_task/README.md diff --git a/app/background_task/__init__.py b/app/apps/background_task/__init__.py similarity index 79% rename from app/background_task/__init__.py rename to app/apps/background_task/__init__.py index febc2c0..5d3fdce 100644 --- a/app/background_task/__init__.py +++ b/app/apps/background_task/__init__.py @@ -4,5 +4,5 @@ default_app_config = 'background_task.apps.BackgroundTasksAppConfig' def background(*arg, **kw): - from background_task.tasks import tasks + from apps.background_task.tasks import tasks return tasks.background(*arg, **kw) diff --git a/app/background_task/admin.py b/app/apps/background_task/admin.py similarity index 91% rename from app/background_task/admin.py rename to app/apps/background_task/admin.py index a7d6635..0df76a7 100644 --- a/app/background_task/admin.py +++ b/app/apps/background_task/admin.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- from django.contrib import admin -from background_task.models import Task -from background_task.models import CompletedTask +from apps.background_task.models import Task +from apps.background_task.models import CompletedTask def inc_priority(modeladmin, request, queryset): diff --git a/app/background_task/apps.py b/app/apps/background_task/apps.py similarity index 73% rename from app/background_task/apps.py rename to app/apps/background_task/apps.py index 9c9fbdf..e8104a8 100644 --- a/app/background_task/apps.py +++ b/app/apps/background_task/apps.py @@ -8,13 +8,13 @@ class BackgroundTasksAppConfig(AppConfig): - name = 'background_task' - from background_task import __version__ as version_info + name = 'apps.background_task' + from apps.background_task import __version__ as version_info verbose_name = 'Background Tasks ({})'.format(version_info) def ready(self): - import background_task.signals # noqa - from background_task.management.commands.process_tasks import Command as ProcessTasksCommand + import apps.background_task.signals # noqa + from apps.background_task.management.commands.process_tasks import Command as ProcessTasksCommand def task_runner(*args, **kwargs): logger.info('background tasks thread started') diff --git a/app/background_task/exceptions.py b/app/apps/background_task/exceptions.py similarity index 100% rename from app/background_task/exceptions.py rename to app/apps/background_task/exceptions.py diff --git a/app/background_task/management/commands/__init__.py b/app/apps/background_task/management/__init__.py similarity index 100% rename from app/background_task/management/commands/__init__.py rename to app/apps/background_task/management/__init__.py diff --git a/app/background_task/migrations/__init__.py b/app/apps/background_task/management/commands/__init__.py similarity index 100% rename from app/background_task/migrations/__init__.py rename to app/apps/background_task/management/commands/__init__.py diff --git a/app/background_task/management/commands/process_tasks.py b/app/apps/background_task/management/commands/process_tasks.py similarity index 97% rename from app/background_task/management/commands/process_tasks.py rename to app/apps/background_task/management/commands/process_tasks.py index 6fd8552..38f3d8b 100644 --- a/app/background_task/management/commands/process_tasks.py +++ b/app/apps/background_task/management/commands/process_tasks.py @@ -8,8 +8,8 @@ from django.core.management.base import BaseCommand from django.utils import autoreload -from background_task.tasks import tasks, autodiscover -from background_task.utils import SignalManager +from apps.background_task.tasks import tasks, autodiscover +from apps.background_task.utils import SignalManager from django.db import close_old_connections as close_connection diff --git a/app/background_task/migrations/0001_initial.py b/app/apps/background_task/migrations/0001_initial.py similarity index 100% rename from app/background_task/migrations/0001_initial.py rename to app/apps/background_task/migrations/0001_initial.py diff --git a/app/boj/__init__.py b/app/apps/background_task/migrations/__init__.py similarity index 100% rename from app/boj/__init__.py rename to app/apps/background_task/migrations/__init__.py diff --git a/app/background_task/models.py b/app/apps/background_task/models.py similarity index 98% rename from app/background_task/models.py rename to app/apps/background_task/models.py index 70121fe..3c366c8 100644 --- a/app/background_task/models.py +++ b/app/apps/background_task/models.py @@ -14,10 +14,10 @@ from django.utils import timezone from six import python_2_unicode_compatible -from background_task.exceptions import InvalidTaskError -from background_task.settings import app_settings -from background_task.signals import task_failed -from background_task.signals import task_rescheduled +from apps.background_task.exceptions import InvalidTaskError +from apps.background_task.settings import app_settings +from apps.background_task.signals import task_failed +from apps.background_task.signals import task_rescheduled logger = logging.getLogger(__name__) diff --git a/app/background_task/settings.py b/app/apps/background_task/settings.py similarity index 100% rename from app/background_task/settings.py rename to app/apps/background_task/settings.py diff --git a/app/background_task/signals.py b/app/apps/background_task/signals.py similarity index 95% rename from app/background_task/signals.py rename to app/apps/background_task/signals.py index ea05ac0..be3b20d 100644 --- a/app/background_task/signals.py +++ b/app/apps/background_task/signals.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- import django.dispatch from django.db import connections -from background_task.settings import app_settings +from apps.background_task.settings import app_settings task_created = django.dispatch.Signal(['task']) task_error = django.dispatch.Signal(['task']) diff --git a/app/background_task/tasks.py b/app/apps/background_task/tasks.py similarity index 98% rename from app/background_task/tasks.py rename to app/apps/background_task/tasks.py index c2beaf6..cf84945 100644 --- a/app/background_task/tasks.py +++ b/app/apps/background_task/tasks.py @@ -12,10 +12,10 @@ from django.utils import timezone from six import python_2_unicode_compatible -from background_task.exceptions import BackgroundTaskError -from background_task.models import Task -from background_task.settings import app_settings -from background_task import signals +from apps.background_task.exceptions import BackgroundTaskError +from apps.background_task.models import Task +from apps.background_task.settings import app_settings +from apps.background_task import signals logger = logging.getLogger(__name__) diff --git a/app/background_task/utils.py b/app/apps/background_task/utils.py similarity index 100% rename from app/background_task/utils.py rename to app/apps/background_task/utils.py diff --git a/app/boj/migrations/__init__.py b/app/apps/boj/__init__.py similarity index 100% rename from app/boj/migrations/__init__.py rename to app/apps/boj/__init__.py diff --git a/app/boj/admin.py b/app/apps/boj/admin.py similarity index 86% rename from app/boj/admin.py rename to app/apps/boj/admin.py index 136b1d9..f0bb1dd 100644 --- a/app/boj/admin.py +++ b/app/apps/boj/admin.py @@ -2,10 +2,10 @@ from django.db.models import QuerySet from django.http.request import HttpRequest -from boj.models import BOJUser -from boj.models import BOJUserSnapshot -from boj.services import schedule_update_boj_user_data -from boj.services import update_boj_user +from apps.boj.models import BOJUser +from apps.boj.models import BOJUserSnapshot +from apps.boj.services import schedule_update_boj_user_data +from apps.boj.services import update_boj_user @admin.register(BOJUser) diff --git a/app/boj/apps.py b/app/apps/boj/apps.py similarity index 73% rename from app/boj/apps.py rename to app/apps/boj/apps.py index 209829a..1749095 100644 --- a/app/boj/apps.py +++ b/app/apps/boj/apps.py @@ -3,7 +3,7 @@ class BojConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "boj" + name = "apps.boj" def ready(self) -> None: - import boj.services + import apps.boj.services diff --git a/app/boj/enums.py b/app/apps/boj/enums.py similarity index 100% rename from app/boj/enums.py rename to app/apps/boj/enums.py diff --git a/app/boj/migrations/0001_initial.py b/app/apps/boj/migrations/0001_initial.py similarity index 100% rename from app/boj/migrations/0001_initial.py rename to app/apps/boj/migrations/0001_initial.py diff --git a/app/crews/activities/__init__.py b/app/apps/boj/migrations/__init__.py similarity index 100% rename from app/crews/activities/__init__.py rename to app/apps/boj/migrations/__init__.py diff --git a/app/boj/models.py b/app/apps/boj/models.py similarity index 98% rename from app/boj/models.py rename to app/apps/boj/models.py index bd6f3f4..672c9cf 100644 --- a/app/boj/models.py +++ b/app/apps/boj/models.py @@ -6,7 +6,7 @@ from django.db import models from django.db.models import Manager -from boj.enums import BOJLevel +from apps.boj.enums import BOJLevel from users.models import User diff --git a/app/boj/serializers.py b/app/apps/boj/serializers.py similarity index 93% rename from app/boj/serializers.py rename to app/apps/boj/serializers.py index 6b937ee..38dd994 100644 --- a/app/boj/serializers.py +++ b/app/apps/boj/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers -from boj.enums import BOJLevel -from boj.models import BOJUser +from apps.boj.enums import BOJLevel +from apps.boj.models import BOJUser class BOJLevelField(serializers.SerializerMethodField): diff --git a/app/boj/services.py b/app/apps/boj/services.py similarity index 92% rename from app/boj/services.py rename to app/apps/boj/services.py index 5d40616..395d17a 100644 --- a/app/boj/services.py +++ b/app/apps/boj/services.py @@ -8,10 +8,10 @@ from rest_framework import status import requests -from background_task.tasks import tasks -from boj.enums import BOJLevel -from boj.models import BOJUser -from boj.models import BOJUserSnapshot +from apps.background_task.tasks import tasks +from apps.boj.enums import BOJLevel +from apps.boj.models import BOJUser +from apps.boj.models import BOJUserSnapshot from users.models import User diff --git a/app/crews/__init__.py b/app/apps/crews/__init__.py similarity index 100% rename from app/crews/__init__.py rename to app/apps/crews/__init__.py diff --git a/app/crews/activities/migrations/__init__.py b/app/apps/crews/activities/__init__.py similarity index 100% rename from app/crews/activities/migrations/__init__.py rename to app/apps/crews/activities/__init__.py diff --git a/app/crews/activities/admin.py b/app/apps/crews/activities/admin.py similarity index 85% rename from app/crews/activities/admin.py rename to app/apps/crews/activities/admin.py index 2de99d9..1763021 100644 --- a/app/crews/activities/admin.py +++ b/app/apps/crews/activities/admin.py @@ -1,9 +1,9 @@ from django.contrib import admin -from crews.models import Crew -from crews.activities.models import CrewActivity -from crews.activities.models import CrewActivityProblem -from crews.activities.models import CrewActivitySubmission +from apps.crews.models import Crew +from apps.crews.activities.models import CrewActivity +from apps.crews.activities.models import CrewActivityProblem +from apps.crews.activities.models import CrewActivitySubmission admin.site.register([ diff --git a/app/crews/activities/apps.py b/app/apps/crews/activities/apps.py similarity index 82% rename from app/crews/activities/apps.py rename to app/apps/crews/activities/apps.py index 9362c54..409afcc 100644 --- a/app/crews/activities/apps.py +++ b/app/apps/crews/activities/apps.py @@ -3,5 +3,5 @@ class CrewActivitiesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "crews.activities" + name = "apps.crews.activities" verbose_name = "Crew activities" diff --git a/app/crews/activities/dto.py b/app/apps/crews/activities/dto.py similarity index 90% rename from app/crews/activities/dto.py rename to app/apps/crews/activities/dto.py index 335f9aa..cfec7b5 100644 --- a/app/crews/activities/dto.py +++ b/app/apps/crews/activities/dto.py @@ -2,8 +2,7 @@ from datetime import datetime from typing import List -from crews import enums -from problems.analyses.enums import ProblemDifficulty +from apps.problems.analyses.enums import ProblemDifficulty @dataclass diff --git a/app/crews/activities/migrations/0001_initial.py b/app/apps/crews/activities/migrations/0001_initial.py similarity index 100% rename from app/crews/activities/migrations/0001_initial.py rename to app/apps/crews/activities/migrations/0001_initial.py diff --git a/app/crews/activities/migrations/0002_initial.py b/app/apps/crews/activities/migrations/0002_initial.py similarity index 100% rename from app/crews/activities/migrations/0002_initial.py rename to app/apps/crews/activities/migrations/0002_initial.py diff --git a/app/crews/applications/__init__.py b/app/apps/crews/activities/migrations/__init__.py similarity index 100% rename from app/crews/applications/__init__.py rename to app/apps/crews/activities/migrations/__init__.py diff --git a/app/crews/activities/models.py b/app/apps/crews/activities/models.py similarity index 97% rename from app/crews/activities/models.py rename to app/apps/crews/activities/models.py index 2bfda50..3427f3a 100644 --- a/app/crews/activities/models.py +++ b/app/apps/crews/activities/models.py @@ -6,9 +6,9 @@ from django.db import models from django.utils import timezone -from crews.enums import ProgrammingLanguageChoices -from crews.models import Crew -from problems.models import Problem +from apps.crews.enums import ProgrammingLanguageChoices +from apps.crews.models import Crew +from apps.problems.models import Problem from users.models import User diff --git a/app/crews/activities/serializers.py b/app/apps/crews/activities/serializers.py similarity index 94% rename from app/crews/activities/serializers.py rename to app/apps/crews/activities/serializers.py index f7c2981..c6e9685 100644 --- a/app/crews/activities/serializers.py +++ b/app/apps/crews/activities/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from crews.activities.models import CrewActivity +from apps.crews.activities.models import CrewActivity PK = 'id' diff --git a/app/crews/activities/views.py b/app/apps/crews/activities/views.py similarity index 50% rename from app/crews/activities/views.py rename to app/apps/crews/activities/views.py index 6e2be4c..7cf68be 100644 --- a/app/crews/activities/views.py +++ b/app/apps/crews/activities/views.py @@ -1,14 +1,12 @@ from rest_framework import generics -from crews.activities import models -from crews import permissions -from crews import serializersaaa +from apps.crews.activities import models class CrewDashboardActivityAPIView(generics.RetrieveAPIView): """크루 대시보드 홈 - 회차 별 API""" queryset = models.CrewActivity - permission_classes = [permissions.IsAuthenticated & permissions.IsMember] - serializer_class = serializersaaa.CrewActivityDashboardSerializer + permission_classes = [] # TODO + serializer_class = ... # TODO lookup_field = 'id' lookup_url_kwarg = 'activity_id' diff --git a/app/crews/admin.py b/app/apps/crews/admin.py similarity index 90% rename from app/crews/admin.py rename to app/apps/crews/admin.py index 0d511d5..6dab4f7 100644 --- a/app/crews/admin.py +++ b/app/apps/crews/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin -from crews.activities.models import CrewActivity -from crews.applications.models import CrewApplication -from crews.models import Crew -from crews.models import CrewMember -from crews.models import CrewSubmittableLanguage +from apps.crews.activities.models import CrewActivity +from apps.crews.applications.models import CrewApplication +from apps.crews.models import Crew +from apps.crews.models import CrewMember +from apps.crews.models import CrewSubmittableLanguage from users.models import User diff --git a/app/crews/applications/migrations/__init__.py b/app/apps/crews/applications/__init__.py similarity index 100% rename from app/crews/applications/migrations/__init__.py rename to app/apps/crews/applications/__init__.py diff --git a/app/crews/applications/admin.py b/app/apps/crews/applications/admin.py similarity index 85% rename from app/crews/applications/admin.py rename to app/apps/crews/applications/admin.py index c2f18c1..a59f5bb 100644 --- a/app/crews/applications/admin.py +++ b/app/apps/crews/applications/admin.py @@ -2,9 +2,9 @@ from django.db.models import QuerySet from django.http.request import HttpRequest -from crews.applications.models import CrewApplication -from crews.applications.services import accept -from crews.applications.services import reject +from apps.crews.applications.models import CrewApplication +from apps.crews.applications.services import accept +from apps.crews.applications.services import reject @admin.register(CrewApplication) diff --git a/app/crews/applications/apps.py b/app/apps/crews/applications/apps.py similarity index 82% rename from app/crews/applications/apps.py rename to app/apps/crews/applications/apps.py index 2f45887..1c208db 100644 --- a/app/crews/applications/apps.py +++ b/app/apps/crews/applications/apps.py @@ -3,5 +3,5 @@ class CrewApplicationsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "crews.applications" + name = "apps.crews.applications" verbose_name = "Crew applications" diff --git a/app/crews/applications/migrations/0001_initial.py b/app/apps/crews/applications/migrations/0001_initial.py similarity index 100% rename from app/crews/applications/migrations/0001_initial.py rename to app/apps/crews/applications/migrations/0001_initial.py diff --git a/app/crews/applications/migrations/0002_initial.py b/app/apps/crews/applications/migrations/0002_initial.py similarity index 100% rename from app/crews/applications/migrations/0002_initial.py rename to app/apps/crews/applications/migrations/0002_initial.py diff --git a/app/crews/migrations/__init__.py b/app/apps/crews/applications/migrations/__init__.py similarity index 100% rename from app/crews/migrations/__init__.py rename to app/apps/crews/applications/migrations/__init__.py diff --git a/app/crews/applications/models.py b/app/apps/crews/applications/models.py similarity index 98% rename from app/crews/applications/models.py rename to app/apps/crews/applications/models.py index 05ecc04..e7c7caa 100644 --- a/app/crews/applications/models.py +++ b/app/apps/crews/applications/models.py @@ -4,7 +4,7 @@ from django.db import models -from crews.models import Crew +from apps.crews.models import Crew from users.models import User diff --git a/app/crews/applications/permissions.py b/app/apps/crews/applications/permissions.py similarity index 70% rename from app/crews/applications/permissions.py rename to app/apps/crews/applications/permissions.py index e852a79..ec0a3a1 100644 --- a/app/crews/applications/permissions.py +++ b/app/apps/crews/applications/permissions.py @@ -1,7 +1,7 @@ from rest_framework.request import Request -from crews.applications.models import CrewApplication -from crews.permissions import IsCaptain as _IsCaptain +from apps.crews.applications.models import CrewApplication +from apps.crews.permissions import IsCaptain as _IsCaptain class IsCaptain(_IsCaptain): diff --git a/app/crews/applications/serializers.py b/app/apps/crews/applications/serializers.py similarity index 96% rename from app/crews/applications/serializers.py rename to app/apps/crews/applications/serializers.py index f0b8418..deb7d77 100644 --- a/app/crews/applications/serializers.py +++ b/app/apps/crews/applications/serializers.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from crews.applications.models import CrewApplication +from apps.crews.applications.models import CrewApplication PK = 'id' diff --git a/app/crews/applications/services.py b/app/apps/crews/applications/services.py similarity index 93% rename from app/crews/applications/services.py rename to app/apps/crews/applications/services.py index ac81a5d..1fa3bb7 100644 --- a/app/crews/applications/services.py +++ b/app/apps/crews/applications/services.py @@ -1,18 +1,18 @@ from textwrap import dedent -from background_task import background from django.core.mail import send_mail from django.db.models.signals import post_save from django.dispatch import receiver from django.utils import timezone from rest_framework.exceptions import ValidationError -from boj.enums import BOJLevel -from boj.models import BOJUser -from crews.applications.models import CrewApplication -from crews.applications.signals import reviewed -from crews.models import Crew -from crews.models import CrewMember +from apps.background_task import background +from apps.boj.enums import BOJLevel +from apps.boj.models import BOJUser +from apps.crews.applications.models import CrewApplication +from apps.crews.applications.signals import reviewed +from apps.crews.models import Crew +from apps.crews.models import CrewMember from users.models import User diff --git a/app/crews/applications/signals.py b/app/apps/crews/applications/signals.py similarity index 100% rename from app/crews/applications/signals.py rename to app/apps/crews/applications/signals.py diff --git a/app/crews/applications/views.py b/app/apps/crews/applications/views.py similarity index 92% rename from app/crews/applications/views.py rename to app/apps/crews/applications/views.py index 79da1c1..069267d 100644 --- a/app/crews/applications/views.py +++ b/app/apps/crews/applications/views.py @@ -6,12 +6,12 @@ from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated -from crews.applications.models import CrewApplication -from crews.applications.permissions import IsCaptain -from crews.applications.services import review -from crews.applications import serializers -from crews.models import Crew -from crews import servicesa +from apps.crews.applications.models import CrewApplication +from apps.crews.applications.permissions import IsCaptain +from apps.crews.applications.services import review +from apps.crews.applications import serializers +from apps.crews.models import Crew +from apps.crews import servicesa class CrewApplicationForCrewListAPIView(generics.ListAPIView): diff --git a/app/crews/apps.py b/app/apps/crews/apps.py similarity index 83% rename from app/crews/apps.py rename to app/apps/crews/apps.py index 1e88651..3508efe 100644 --- a/app/crews/apps.py +++ b/app/apps/crews/apps.py @@ -3,4 +3,4 @@ class CrewsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "crews" + name = "apps.crews" diff --git a/app/crews/dto.py b/app/apps/crews/dto.py similarity index 73% rename from app/crews/dto.py rename to app/apps/crews/dto.py index 9651e24..3e38ab0 100644 --- a/app/crews/dto.py +++ b/app/apps/crews/dto.py @@ -1,6 +1,6 @@ from dataclasses import dataclass -from crews.enums import CrewTagType +from apps.crews.enums import CrewTagType @dataclass diff --git a/app/crews/enums.py b/app/apps/crews/enums.py similarity index 100% rename from app/crews/enums.py rename to app/apps/crews/enums.py diff --git a/app/crews/fixtures/sample.json b/app/apps/crews/fixtures/sample.json similarity index 100% rename from app/crews/fixtures/sample.json rename to app/apps/crews/fixtures/sample.json diff --git a/app/crews/migrations/0001_initial.py b/app/apps/crews/migrations/0001_initial.py similarity index 100% rename from app/crews/migrations/0001_initial.py rename to app/apps/crews/migrations/0001_initial.py diff --git a/app/crews/submissions/__init__.py b/app/apps/crews/migrations/__init__.py similarity index 100% rename from app/crews/submissions/__init__.py rename to app/apps/crews/migrations/__init__.py diff --git a/app/crews/models.py b/app/apps/crews/models.py similarity index 97% rename from app/crews/models.py rename to app/apps/crews/models.py index d0945e2..7e9b872 100644 --- a/app/crews/models.py +++ b/app/apps/crews/models.py @@ -8,11 +8,11 @@ from django.db import models from django.contrib.auth.models import AnonymousUser -from boj.enums import BOJLevel -from crews.dto import CrewTagDTO -from crews.enums import CrewTagType -from crews.enums import EmojiChoices -from crews.enums import ProgrammingLanguageChoices +from apps.boj.enums import BOJLevel +from apps.crews.dto import CrewTagDTO +from apps.crews.enums import CrewTagType +from apps.crews.enums import EmojiChoices +from apps.crews.enums import ProgrammingLanguageChoices from users.models import User diff --git a/app/crews/permissions.py b/app/apps/crews/permissions.py similarity index 88% rename from app/crews/permissions.py rename to app/apps/crews/permissions.py index 0dd6a49..9740a19 100644 --- a/app/crews/permissions.py +++ b/app/apps/crews/permissions.py @@ -1,8 +1,8 @@ from rest_framework.permissions import BasePermission from rest_framework.request import Request -from crews.models import Crew -from crews.models import CrewMember +from apps.crews.models import Crew +from apps.crews.models import CrewMember class IsMember(BasePermission): diff --git a/app/crews/serializers.py b/app/apps/crews/serializers.py similarity index 94% rename from app/crews/serializers.py rename to app/apps/crews/serializers.py index 18d1b1e..9f3cc35 100644 --- a/app/crews/serializers.py +++ b/app/apps/crews/serializers.py @@ -5,14 +5,14 @@ from django.db.transaction import atomic from rest_framework import serializers -from crews.activities.models import CrewActivity -from crews.activities.serializers import CrewActivitySerializer -from crews.applications.services import is_valid_applicant -from crews.dto import CrewTagDTO -from crews.enums import ProgrammingLanguageChoices -from crews.models import Crew -from crews.models import CrewMember -from crews.models import CrewSubmittableLanguage +from apps.crews.activities.models import CrewActivity +from apps.crews.activities.serializers import CrewActivitySerializer +from apps.crews.applications.services import is_valid_applicant +from apps.crews.dto import CrewTagDTO +from apps.crews.enums import ProgrammingLanguageChoices +from apps.crews.models import Crew +from apps.crews.models import CrewMember +from apps.crews.models import CrewSubmittableLanguage from users.models import User from users.serializers import UserMinimalSerializer diff --git a/app/crews/submissions/migrations/__init__.py b/app/apps/crews/submissions/__init__.py similarity index 100% rename from app/crews/submissions/migrations/__init__.py rename to app/apps/crews/submissions/__init__.py diff --git a/app/crews/submissions/admin.py b/app/apps/crews/submissions/admin.py similarity index 69% rename from app/crews/submissions/admin.py rename to app/apps/crews/submissions/admin.py index d944fcf..f9377cb 100644 --- a/app/crews/submissions/admin.py +++ b/app/apps/crews/submissions/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from crews.submissions.models import * +from apps.crews.submissions.models import * admin.site.register([ diff --git a/app/crews/submissions/apps.py b/app/apps/crews/submissions/apps.py similarity index 82% rename from app/crews/submissions/apps.py rename to app/apps/crews/submissions/apps.py index 348c1ba..b605179 100644 --- a/app/crews/submissions/apps.py +++ b/app/apps/crews/submissions/apps.py @@ -3,5 +3,5 @@ class SubmissionsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "crews.submissions" + name = "apps.crews.submissions" verbose_name = "Crew submissions" diff --git a/app/problems/__init__.py b/app/apps/crews/submissions/migrations/__init__.py similarity index 100% rename from app/problems/__init__.py rename to app/apps/crews/submissions/migrations/__init__.py diff --git a/app/apps/crews/submissions/models/__init__.py b/app/apps/crews/submissions/models/__init__.py new file mode 100644 index 0000000..2939f92 --- /dev/null +++ b/app/apps/crews/submissions/models/__init__.py @@ -0,0 +1,8 @@ +from apps.crews.submissions.models.submission import Submission +from apps.crews.submissions.models.submission_comment import SubmissionComment + + +__all__ = ( + 'Submission', + 'SubmissionComment', +) diff --git a/app/crews/submissions/models/submission.py b/app/apps/crews/submissions/models/submission.py similarity index 92% rename from app/crews/submissions/models/submission.py rename to app/apps/crews/submissions/models/submission.py index 681d237..39f0f3c 100644 --- a/app/crews/submissions/models/submission.py +++ b/app/apps/crews/submissions/models/submission.py @@ -1,7 +1,7 @@ from django.db import models -from crews.enums import ProgrammingLanguageChoices -from crews.models import CrewActivityProblem +from apps.crews.enums import ProgrammingLanguageChoices +from apps.crews.activities.models import CrewActivityProblem from users.models import User diff --git a/app/crews/submissions/models/submission_comment.py b/app/apps/crews/submissions/models/submission_comment.py similarity index 96% rename from app/crews/submissions/models/submission_comment.py rename to app/apps/crews/submissions/models/submission_comment.py index 9f252b8..1a80a69 100644 --- a/app/crews/submissions/models/submission_comment.py +++ b/app/apps/crews/submissions/models/submission_comment.py @@ -1,7 +1,7 @@ from django.core.validators import MinValueValidator from django.db import models -from crews.submissions.models.submission import Submission +from apps.crews.submissions.models.submission import Submission from users.models import User diff --git a/app/crews/submissions/serializers/__init__.py b/app/apps/crews/submissions/serializers/__init__.py similarity index 89% rename from app/crews/submissions/serializers/__init__.py rename to app/apps/crews/submissions/serializers/__init__.py index 51c617b..aa4dd39 100644 --- a/app/crews/submissions/serializers/__init__.py +++ b/app/apps/crews/submissions/serializers/__init__.py @@ -1,6 +1,6 @@ from rest_framework import serializers -from crews.submissions.models import Submission +from apps.crews.submissions.models import Submission class SubmissionSerializer(serializers.ModelField): diff --git a/app/crews/submissions/views.py b/app/apps/crews/submissions/views.py similarity index 98% rename from app/crews/submissions/views.py rename to app/apps/crews/submissions/views.py index 6cbf39c..2d8ae17 100644 --- a/app/crews/submissions/views.py +++ b/app/apps/crews/submissions/views.py @@ -6,7 +6,7 @@ from rest_framework.serializers import Serializer from rest_framework.response import Response -from crews.submissions import serializers +from apps.crews.submissions import serializers class CreateCodeReview(generics.RetrieveAPIView): diff --git a/app/crews/tests.py b/app/apps/crews/tests.py similarity index 100% rename from app/crews/tests.py rename to app/apps/crews/tests.py diff --git a/app/crews/urls.py b/app/apps/crews/urls.py similarity index 60% rename from app/crews/urls.py rename to app/apps/crews/urls.py index e54d1c7..5f383b8 100644 --- a/app/crews/urls.py +++ b/app/apps/crews/urls.py @@ -1,17 +1,20 @@ from django.urls import include from django.urls import path -import crews.views -# import crews.applications.views +from apps.crews.views import MyCrewListAPIView +from apps.crews.views import RecruitingCrewListAPIView +from apps.crews.views import CrewCreateAPIView +from apps.crews.views import CrewDashboardAPIView +from apps.crews.views import CrewStatisticsAPIView urlpatterns = [ - path("crews/my", crews.views.MyCrewListAPIView.as_view()), - path("crews/recruiting", crews.views.RecruitingCrewListAPIView.as_view()), - path("crew", crews.views.CrewCreateAPIView.as_view()), + path("crews/my", MyCrewListAPIView.as_view()), + path("crews/recruiting", RecruitingCrewListAPIView.as_view()), + path("crew", CrewCreateAPIView.as_view()), path("crew/", include([ - path("/dashboard", crews.views.CrewDashboardAPIView.as_view()), - path("/statistics", crews.views.CrewStatisticsAPIView.as_view()), + path("/dashboard", CrewDashboardAPIView.as_view()), + path("/statistics", CrewStatisticsAPIView.as_view()), # path("/applications", crews.applications.views.CrewApplicationForCrewListAPIView.as_view()), # path("/apply", crews.applications.views.CrewApplicantionCreateAPIView.as_view()), ])), diff --git a/app/crews/views.py b/app/apps/crews/views.py similarity index 79% rename from app/crews/views.py rename to app/apps/crews/views.py index 81ca29d..3aa97e0 100644 --- a/app/crews/views.py +++ b/app/apps/crews/views.py @@ -2,15 +2,15 @@ from rest_framework.permissions import AllowAny from rest_framework.permissions import IsAuthenticated -from crews.activities.models import CrewActivityProblem -from crews.models import Crew -from crews.permissions import IsMember -from crews.serializers import RecruitingCrewSerializer -from crews.serializers import MyCrewSerializer -from crews.serializers import CrewCreateSerializer -from crews.serializers import CrewDashboardSerializer -from problems.serializers import ProblemStatisticSerializer -from problems.statistics import create_statistics +from apps.crews.activities.models import CrewActivityProblem +from apps.crews.models import Crew +from apps.crews.permissions import IsMember +from apps.crews.serializers import RecruitingCrewSerializer +from apps.crews.serializers import MyCrewSerializer +from apps.crews.serializers import CrewCreateSerializer +from apps.crews.serializers import CrewDashboardSerializer +from apps.problems.serializers import ProblemStatisticSerializer +from apps.problems.statistics import create_statistics class RecruitingCrewListAPIView(generics.ListAPIView): diff --git a/app/problems/analyses/__init__.py b/app/apps/problems/__init__.py similarity index 100% rename from app/problems/analyses/__init__.py rename to app/apps/problems/__init__.py diff --git a/app/problems/admin.py b/app/apps/problems/admin.py similarity index 89% rename from app/problems/admin.py rename to app/apps/problems/admin.py index 9090707..454cc4e 100644 --- a/app/problems/admin.py +++ b/app/apps/problems/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin from django.db.models import QuerySet -from problems.models import Problem -from problems.analyzers import schedule_analyze +from apps.problems.models import Problem +from apps.problems.analyzers import schedule_analyze from users.models import User diff --git a/app/problems/analyses/migrations/__init__.py b/app/apps/problems/analyses/__init__.py similarity index 100% rename from app/problems/analyses/migrations/__init__.py rename to app/apps/problems/analyses/__init__.py diff --git a/app/problems/analyses/admin.py b/app/apps/problems/analyses/admin.py similarity index 90% rename from app/problems/analyses/admin.py rename to app/apps/problems/analyses/admin.py index b8adb53..bf8c062 100644 --- a/app/problems/analyses/admin.py +++ b/app/apps/problems/analyses/admin.py @@ -1,10 +1,10 @@ from django.contrib import admin -from problems.models import Problem -from problems.analyses.models import ProblemAnalysis -from problems.analyses.models import ProblemAnalysisTag -from problems.analyses.models import ProblemTag -from problems.analyses.models import ProblemTagRelation +from apps.problems.models import Problem +from apps.problems.analyses.models import ProblemAnalysis +from apps.problems.analyses.models import ProblemAnalysisTag +from apps.problems.analyses.models import ProblemTag +from apps.problems.analyses.models import ProblemTagRelation @admin.register(ProblemAnalysis) diff --git a/app/problems/analyses/apps.py b/app/apps/problems/analyses/apps.py similarity index 82% rename from app/problems/analyses/apps.py rename to app/apps/problems/analyses/apps.py index e34fccd..1ddcda2 100644 --- a/app/problems/analyses/apps.py +++ b/app/apps/problems/analyses/apps.py @@ -3,5 +3,5 @@ class AnalysesConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "problems.analyses" + name = "apps.problems.analyses" verbose_name = "Problems analyses" diff --git a/app/problems/analyses/dto.py b/app/apps/problems/analyses/dto.py similarity index 89% rename from app/problems/analyses/dto.py rename to app/apps/problems/analyses/dto.py index 1d3b3b1..498a471 100644 --- a/app/problems/analyses/dto.py +++ b/app/apps/problems/analyses/dto.py @@ -4,7 +4,7 @@ from dataclasses import field from typing import Tuple -from problems.analyses.enums import ProblemDifficulty +from apps.problems.analyses.enums import ProblemDifficulty @dataclass diff --git a/app/problems/analyses/enums.py b/app/apps/problems/analyses/enums.py similarity index 100% rename from app/problems/analyses/enums.py rename to app/apps/problems/analyses/enums.py diff --git a/app/problems/analyses/fixtures/problem_tag.sample.json b/app/apps/problems/analyses/fixtures/problem_tag.sample.json similarity index 100% rename from app/problems/analyses/fixtures/problem_tag.sample.json rename to app/apps/problems/analyses/fixtures/problem_tag.sample.json diff --git a/app/problems/analyses/migrations/0001_initial.py b/app/apps/problems/analyses/migrations/0001_initial.py similarity index 100% rename from app/problems/analyses/migrations/0001_initial.py rename to app/apps/problems/analyses/migrations/0001_initial.py diff --git a/app/problems/analyses/migrations/0002_initial.py b/app/apps/problems/analyses/migrations/0002_initial.py similarity index 100% rename from app/problems/analyses/migrations/0002_initial.py rename to app/apps/problems/analyses/migrations/0002_initial.py diff --git a/app/problems/analyzers/gpt/__init__.py b/app/apps/problems/analyses/migrations/__init__.py similarity index 100% rename from app/problems/analyzers/gpt/__init__.py rename to app/apps/problems/analyses/migrations/__init__.py diff --git a/app/problems/analyses/models.py b/app/apps/problems/analyses/models.py similarity index 96% rename from app/problems/analyses/models.py rename to app/apps/problems/analyses/models.py index 0c23b71..15f483f 100644 --- a/app/problems/analyses/models.py +++ b/app/apps/problems/analyses/models.py @@ -5,10 +5,10 @@ from django.db import models from django.db.transaction import atomic -from problems.analyses.dto import ProblemAnalysisDTO -from problems.analyses.dto import ProblemTagDTO -from problems.analyses.enums import ProblemDifficulty -from problems.models import Problem +from apps.problems.analyses.dto import ProblemAnalysisDTO +from apps.problems.analyses.dto import ProblemTagDTO +from apps.problems.analyses.enums import ProblemDifficulty +from apps.problems.models import Problem class ProblemTagManager(models.Manager): diff --git a/app/problems/analyses/serializers.py b/app/apps/problems/analyses/serializers.py similarity index 91% rename from app/problems/analyses/serializers.py rename to app/apps/problems/analyses/serializers.py index b5af272..993a6a4 100644 --- a/app/problems/analyses/serializers.py +++ b/app/apps/problems/analyses/serializers.py @@ -3,10 +3,10 @@ from django.db.models import QuerySet from rest_framework import serializers -from problems.analyses.enums import ProblemDifficulty -from problems.analyses.models import ProblemAnalysis -from problems.analyses.models import ProblemAnalysisTag -from problems.analyses.models import ProblemTag +from apps.problems.analyses.enums import ProblemDifficulty +from apps.problems.analyses.models import ProblemAnalysis +from apps.problems.analyses.models import ProblemAnalysisTag +from apps.problems.analyses.models import ProblemTag PK = 'id' diff --git a/app/problems/analyzers/__init__.py b/app/apps/problems/analyzers/__init__.py similarity index 81% rename from app/problems/analyzers/__init__.py rename to app/apps/problems/analyzers/__init__.py index 36a59c8..d0379de 100644 --- a/app/problems/analyzers/__init__.py +++ b/app/apps/problems/analyzers/__init__.py @@ -3,14 +3,14 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from background_task.tasks import tasks -from problems.analyses.dto import ProblemAnalysisDTO -from problems.analyses.models import ProblemAnalysis -from problems.analyses.models import ProblemTag -from problems.analyzers.base import ProblemAnalyzer -from problems.analyzers.gemini import GeminiProblemAnalyzer -from problems.dto import ProblemDTO -from problems.models import Problem +from apps.background_task.tasks import tasks +from apps.problems.analyses.dto import ProblemAnalysisDTO +from apps.problems.analyses.models import ProblemAnalysis +from apps.problems.analyses.models import ProblemTag +from apps.problems.analyzers.base import ProblemAnalyzer +from apps.problems.analyzers.gemini import GeminiProblemAnalyzer +from apps.problems.dto import ProblemDTO +from apps.problems.models import Problem logger = getLogger(__name__) diff --git a/app/problems/analyzers/base.py b/app/apps/problems/analyzers/base.py similarity index 76% rename from app/problems/analyzers/base.py rename to app/apps/problems/analyzers/base.py index c9e74dd..533683b 100644 --- a/app/problems/analyzers/base.py +++ b/app/apps/problems/analyzers/base.py @@ -1,5 +1,5 @@ -from problems.dto import ProblemDTO -from problems.analyses.dto import ProblemAnalysisDTO +from apps.problems.dto import ProblemDTO +from apps.problems.analyses.dto import ProblemAnalysisDTO class ProblemAnalyzer: diff --git a/app/apps/problems/analyzers/gemini/__init__.py b/app/apps/problems/analyzers/gemini/__init__.py new file mode 100644 index 0000000..f07dcc8 --- /dev/null +++ b/app/apps/problems/analyzers/gemini/__init__.py @@ -0,0 +1 @@ +from apps.problems.analyzers.gemini.analyzer import GeminiProblemAnalyzer diff --git a/app/problems/analyzers/gemini/analyzer.py b/app/apps/problems/analyzers/gemini/analyzer.py similarity index 89% rename from app/problems/analyzers/gemini/analyzer.py rename to app/apps/problems/analyzers/gemini/analyzer.py index 7acd6bd..19e068f 100644 --- a/app/problems/analyzers/gemini/analyzer.py +++ b/app/apps/problems/analyzers/gemini/analyzer.py @@ -3,11 +3,11 @@ from django.conf import settings from google import generativeai as genai -from problems.analyzers.base import ProblemAnalyzer -from problems.analyzers.base import ProblemDTO -from problems.analyzers.base import ProblemAnalysisDTO -from problems.analyzers.gemini import prompts -from problems.analyzers.gemini import parsers +from apps.problems.analyzers.base import ProblemAnalyzer +from apps.problems.analyzers.base import ProblemDTO +from apps.problems.analyzers.base import ProblemAnalysisDTO +from apps.problems.analyzers.gemini import prompts +from apps.problems.analyzers.gemini import parsers logger = getLogger(__name__) diff --git a/app/problems/analyzers/gemini/parsers.py b/app/apps/problems/analyzers/gemini/parsers.py similarity index 97% rename from app/problems/analyzers/gemini/parsers.py rename to app/apps/problems/analyzers/gemini/parsers.py index e364cad..6cd2702 100644 --- a/app/problems/analyzers/gemini/parsers.py +++ b/app/apps/problems/analyzers/gemini/parsers.py @@ -6,7 +6,7 @@ from sympy import latex from sympy.parsing.latex import parse_latex -from problems.analyses.models import ProblemTag +from apps.problems.analyses.models import ProblemTag logger = getLogger(__name__) diff --git a/app/problems/analyzers/gemini/prompts.py b/app/apps/problems/analyzers/gemini/prompts.py similarity index 97% rename from app/problems/analyzers/gemini/prompts.py rename to app/apps/problems/analyzers/gemini/prompts.py index f36eb0f..aeb2b23 100644 --- a/app/problems/analyzers/gemini/prompts.py +++ b/app/apps/problems/analyzers/gemini/prompts.py @@ -1,7 +1,7 @@ from textwrap import dedent -from problems.analyzers.base import ProblemDTO -from problems.analyzers.base import ProblemAnalysisDTO +from apps.problems.analyzers.base import ProblemDTO +from apps.problems.analyzers.base import ProblemAnalysisDTO def get_difficulty_prompt(problem_dto: ProblemDTO, analysis_dto: ProblemAnalysisDTO) -> str: diff --git a/app/problems/migrations/__init__.py b/app/apps/problems/analyzers/gpt/__init__.py similarity index 100% rename from app/problems/migrations/__init__.py rename to app/apps/problems/analyzers/gpt/__init__.py diff --git a/app/problems/analyzers/gpt/analyzer.py b/app/apps/problems/analyzers/gpt/analyzer.py similarity index 80% rename from app/problems/analyzers/gpt/analyzer.py rename to app/apps/problems/analyzers/gpt/analyzer.py index 2f481ba..94fbda3 100644 --- a/app/problems/analyzers/gpt/analyzer.py +++ b/app/apps/problems/analyzers/gpt/analyzer.py @@ -1,5 +1,5 @@ -from problems import dto -from problems.analyzers.base import ProblemAnalyzer +from apps.problems import dto +from apps.problems.analyzers.base import ProblemAnalyzer class GPTProblemAnalyzer(ProblemAnalyzer): diff --git a/app/problems/apps.py b/app/apps/problems/apps.py similarity index 70% rename from app/problems/apps.py rename to app/apps/problems/apps.py index 6c718f6..513626f 100644 --- a/app/problems/apps.py +++ b/app/apps/problems/apps.py @@ -3,7 +3,7 @@ class ProblemsConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" - name = "problems" + name = "apps.problems" def ready(self) -> None: - import problems.analyzers + import apps.problems.analyzers diff --git a/app/problems/dto.py b/app/apps/problems/dto.py similarity index 91% rename from app/problems/dto.py rename to app/apps/problems/dto.py index a445f06..4ad1314 100644 --- a/app/problems/dto.py +++ b/app/apps/problems/dto.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from dataclasses import field -from problems.analyses.dto import ProblemTagDTO +from apps.problems.analyses.dto import ProblemTagDTO @dataclass diff --git a/app/problems/enums.py b/app/apps/problems/enums.py similarity index 100% rename from app/problems/enums.py rename to app/apps/problems/enums.py diff --git a/app/problems/migrations/0001_initial.py b/app/apps/problems/migrations/0001_initial.py similarity index 100% rename from app/problems/migrations/0001_initial.py rename to app/apps/problems/migrations/0001_initial.py diff --git a/app/apps/problems/migrations/__init__.py b/app/apps/problems/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/problems/models.py b/app/apps/problems/models.py similarity index 96% rename from app/problems/models.py rename to app/apps/problems/models.py index b4dbc88..d9607e6 100644 --- a/app/problems/models.py +++ b/app/apps/problems/models.py @@ -1,7 +1,7 @@ from django.db import models -from problems.dto import ProblemDTO -from problems.enums import Unit +from apps.problems.dto import ProblemDTO +from apps.problems.enums import Unit from users.models import User diff --git a/app/problems/serializers.py b/app/apps/problems/serializers.py similarity index 92% rename from app/problems/serializers.py rename to app/apps/problems/serializers.py index 7bd9b80..b202911 100644 --- a/app/problems/serializers.py +++ b/app/apps/problems/serializers.py @@ -2,15 +2,15 @@ from rest_framework import serializers -from problems import dto -from problems import models -from problems.analyses.enums import ProblemDifficulty -from problems.analyses.models import ProblemAnalysis -from problems.analyses.serializers import ProblemAnalysisSerializer -from problems.analyses.serializers import ProblemAnalysisDifficultyField -from problems.dto import ProblemStatisticDTO -from problems.enums import Unit -from problems.models import Problem +from apps.problems import dto +from apps.problems import models +from apps.problems.analyses.enums import ProblemDifficulty +from apps.problems.analyses.models import ProblemAnalysis +from apps.problems.analyses.serializers import ProblemAnalysisSerializer +from apps.problems.analyses.serializers import ProblemAnalysisDifficultyField +from apps.problems.dto import ProblemStatisticDTO +from apps.problems.enums import Unit +from apps.problems.models import Problem from users.serializers import UserMinimalSerializer diff --git a/app/problems/statistics.py b/app/apps/problems/statistics.py similarity index 71% rename from app/problems/statistics.py rename to app/apps/problems/statistics.py index 856bdfb..ed09dbb 100644 --- a/app/problems/statistics.py +++ b/app/apps/problems/statistics.py @@ -1,10 +1,10 @@ from typing import Iterable -from problems.models import Problem -from problems.analyses.enums import ProblemDifficulty -from problems.analyses.models import ProblemAnalysis -from problems.analyses.models import ProblemAnalysisTag -from problems.dto import ProblemStatisticDTO +from apps.problems.models import Problem +from apps.problems.analyses.enums import ProblemDifficulty +from apps.problems.analyses.models import ProblemAnalysis +from apps.problems.analyses.models import ProblemAnalysisTag +from apps.problems.dto import ProblemStatisticDTO def create_statistics(problems: Iterable[Problem]) -> ProblemStatisticDTO: diff --git a/app/problems/urls.py b/app/apps/problems/urls.py similarity index 89% rename from app/problems/urls.py rename to app/apps/problems/urls.py index d5f3458..82459f2 100644 --- a/app/problems/urls.py +++ b/app/apps/problems/urls.py @@ -1,6 +1,6 @@ from django.urls import path -from problems import views +from apps.problems import views urlpatterns = [ diff --git a/app/problems/views.py b/app/apps/problems/views.py similarity index 95% rename from app/problems/views.py rename to app/apps/problems/views.py index 5283cdc..85a1f96 100644 --- a/app/problems/views.py +++ b/app/apps/problems/views.py @@ -3,8 +3,8 @@ from rest_framework import status from rest_framework.response import Response -from problems import models -from problems import serializers +from apps.problems import models +from apps.problems import serializers class ProblemCreateAPIView(generics.CreateAPIView): diff --git a/app/config/settings/base/core.py b/app/config/settings/base/core.py index 8bf5d76..04c8f83 100644 --- a/app/config/settings/base/core.py +++ b/app/config/settings/base/core.py @@ -43,18 +43,18 @@ "corsheaders", "drf_yasg", - 'background_task', "rest_framework", 'rest_framework_simplejwt', - "boj", - "crews", - "crews.activities", - "crews.applications", - "crews.submissions", "users", - "problems", - "problems.analyses", + 'apps.background_task', + "apps.boj", + "apps.crews", + "apps.crews.activities", + "apps.crews.applications", + # "apps.crews.submissions", + "apps.problems", + "apps.problems.analyses", ] MIDDLEWARE = [ diff --git a/app/config/urls.py b/app/config/urls.py index 3c62116..550e138 100644 --- a/app/config/urls.py +++ b/app/config/urls.py @@ -6,8 +6,8 @@ from drf_yasg.views import get_schema_view from rest_framework import permissions -import crews.urls -import problems.urls +import apps.crews.urls +import apps.problems.urls import users.urls @@ -26,9 +26,9 @@ urlpatterns = [ path("admin/", admin.site.urls), path("api/v1/", include([ - *crews.urls.urlpatterns, + *apps.crews.urls.urlpatterns, + *apps.problems.urls.urlpatterns, *users.urls.urlpatterns, - *problems.urls.urlpatterns, ])), path(r'swagger/', schema_view.with_ui('swagger', cache_timeout=0), name='schema-swagger-ui'), path(r'swagger(?P\.json|\.yaml)', schema_view.without_ui(cache_timeout=0), name='schema-json'), diff --git a/app/crews/submissions/models/__init__.py b/app/crews/submissions/models/__init__.py deleted file mode 100644 index 300481f..0000000 --- a/app/crews/submissions/models/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from crews.submissions.models.submission import Submission -from crews.submissions.models.submission_comment import SubmissionComment - - -__all__ = ( - 'Submission', - 'SubmissionComment', -) diff --git a/app/problems/analyzers/gemini/__init__.py b/app/problems/analyzers/gemini/__init__.py deleted file mode 100644 index 8187e73..0000000 --- a/app/problems/analyzers/gemini/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from problems.analyzers.gemini.analyzer import GeminiProblemAnalyzer diff --git a/app/users/serializers.py b/app/users/serializers.py index 9fa57d0..cb799fe 100644 --- a/app/users/serializers.py +++ b/app/users/serializers.py @@ -2,8 +2,8 @@ from rest_framework import serializers from rest_framework.exceptions import ValidationError -from boj.models import BOJUser -from boj.serializers import BOJUserSerializer +from apps.boj.models import BOJUser +from apps.boj.serializers import BOJUserSerializer from users.models import User from users.models import UserEmailVerification diff --git a/app/users/services.py b/app/users/services.py index 6800481..c551e81 100644 --- a/app/users/services.py +++ b/app/users/services.py @@ -4,7 +4,7 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from background_task.tasks import tasks +from apps.background_task.tasks import tasks from users.models import UserEmailVerification From e926b43ab5cee1e7d1601dba4190e03d87763cb4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 12:15:51 +0900 Subject: [PATCH 549/552] =?UTF-8?q?refactor:=20apps=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=EC=9E=84=ED=8F=AC=ED=8A=B8=ED=95=B4?= =?UTF-8?q?=EC=98=A4=EB=8A=94=20=EB=AA=A8=EB=93=A0=20=EA=B2=83=EC=9D=80=20?= =?UTF-8?q?config.urls=20=EB=A5=BC=20=EC=A0=9C=EC=99=B8=ED=95=98=EA=B3=A0?= =?UTF-8?q?=20from=20apps....=20import=20..=20=EB=AC=B8=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/apps/background_task/apps.py | 2 +- app/apps/boj/apps.py | 2 +- app/apps/problems/apps.py | 2 +- app/users/urls.py | 19 ++++++++++++------- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/apps/background_task/apps.py b/app/apps/background_task/apps.py index e8104a8..f8688f1 100644 --- a/app/apps/background_task/apps.py +++ b/app/apps/background_task/apps.py @@ -13,7 +13,7 @@ class BackgroundTasksAppConfig(AppConfig): verbose_name = 'Background Tasks ({})'.format(version_info) def ready(self): - import apps.background_task.signals # noqa + from apps.background_task import signals # noqa from apps.background_task.management.commands.process_tasks import Command as ProcessTasksCommand def task_runner(*args, **kwargs): diff --git a/app/apps/boj/apps.py b/app/apps/boj/apps.py index 1749095..2202384 100644 --- a/app/apps/boj/apps.py +++ b/app/apps/boj/apps.py @@ -6,4 +6,4 @@ class BojConfig(AppConfig): name = "apps.boj" def ready(self) -> None: - import apps.boj.services + from apps.boj import services diff --git a/app/apps/problems/apps.py b/app/apps/problems/apps.py index 513626f..9836538 100644 --- a/app/apps/problems/apps.py +++ b/app/apps/problems/apps.py @@ -6,4 +6,4 @@ class ProblemsConfig(AppConfig): name = "apps.problems" def ready(self) -> None: - import apps.problems.analyzers + from apps.problems import analyzers diff --git a/app/users/urls.py b/app/users/urls.py index 55f0b0e..4a3ff5d 100644 --- a/app/users/urls.py +++ b/app/users/urls.py @@ -1,18 +1,23 @@ from django.urls import include from django.urls import path -import users.views +from users.views import SignInAPIView +from users.views import SignUpAPIView +from users.views import SignOutAPIView +from users.views import UsabilityAPIView +from users.views import EmailVerificationAPIView +from users.views import UserManageAPIView urlpatterns = [ path("auth", include([ - path("/signin", users.views.SignInAPIView.as_view()), - path("/signup", users.views.SignUpAPIView.as_view()), - path("/signout", users.views.SignOutAPIView.as_view()), - path("/usability", users.views.UsabilityAPIView.as_view()), - path("/verification", users.views.EmailVerificationAPIView.as_view()), + path("/signin", SignInAPIView.as_view()), + path("/signup", SignUpAPIView.as_view()), + path("/signout", SignOutAPIView.as_view()), + path("/usability", UsabilityAPIView.as_view()), + path("/verification", EmailVerificationAPIView.as_view()), ])), path("user", include([ - path("/manage", users.views.UserManageAPIView.as_view()), + path("/manage", UserManageAPIView.as_view()), ])), ] From 529ba0be6b754ae70579fb9fe6e73834e9851cd4 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 15:52:53 +0900 Subject: [PATCH 550/552] =?UTF-8?q?test(boj,crews,problems,users):=20fixtu?= =?UTF-8?q?re=20sample=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/apps/boj/fixtures/sample.json | 22 ++++++++++ app/apps/crews/fixtures/sample.json | 60 -------------------------- app/apps/problems/fixtures/sample.json | 38 ++++++++++++++++ app/users/fixtures/sample.json | 42 ++++++++++++++++++ 4 files changed, 102 insertions(+), 60 deletions(-) create mode 100644 app/apps/boj/fixtures/sample.json create mode 100644 app/apps/problems/fixtures/sample.json create mode 100644 app/users/fixtures/sample.json diff --git a/app/apps/boj/fixtures/sample.json b/app/apps/boj/fixtures/sample.json new file mode 100644 index 0000000..08d227c --- /dev/null +++ b/app/apps/boj/fixtures/sample.json @@ -0,0 +1,22 @@ +[ + { + "model": "boj.bojuser", + "pk": 1, + "fields": { + "username": "hepheir", + "level": 17, + "rating": 1801, + "updated_at": "2024-08-30T04:02:23.327" + } + }, + { + "model": "boj.bojuser", + "pk": 2, + "fields": { + "username": "test", + "level": 1, + "rating": 20, + "updated_at": "2024-08-30T04:02:23.327" + } + } +] \ No newline at end of file diff --git a/app/apps/crews/fixtures/sample.json b/app/apps/crews/fixtures/sample.json index 9ee63d4..522be7e 100644 --- a/app/apps/crews/fixtures/sample.json +++ b/app/apps/crews/fixtures/sample.json @@ -1,64 +1,4 @@ [ - { - "model": "users.user", - "pk": 1, - "fields": { - "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", - "last_login": null, - "username": "test", - "email": "test@example.com", - "profile_image": "", - "boj_username": "hepheir", - "is_active": true, - "is_staff": false, - "is_superuser": false, - "first_name": "", - "last_name": "", - "created_at": "2024-08-30T06:23:35", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "users.user", - "pk": 2, - "fields": { - "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", - "last_login": null, - "username": "test2", - "email": "test2@example.com", - "profile_image": "", - "boj_username": "test", - "is_active": true, - "is_staff": false, - "is_superuser": false, - "first_name": "", - "last_name": "", - "created_at": "2024-08-30T06:23:35", - "groups": [], - "user_permissions": [] - } - }, - { - "model": "boj.bojuser", - "pk": 1, - "fields": { - "username": "hepheir", - "level": 17, - "rating": 1801, - "updated_at": "2024-08-30T04:02:23.327" - } - }, - { - "model": "boj.bojuser", - "pk": 2, - "fields": { - "username": "test", - "level": 1, - "rating": 20, - "updated_at": "2024-08-30T04:02:23.327" - } - }, { "model": "crews.crew", "pk": 1, diff --git a/app/apps/problems/fixtures/sample.json b/app/apps/problems/fixtures/sample.json new file mode 100644 index 0000000..6aaa1ea --- /dev/null +++ b/app/apps/problems/fixtures/sample.json @@ -0,0 +1,38 @@ +[ + { + "model": "problems.problem", + "pk": 1, + "fields": { + "title": "A+B", + "link": "https://www.acmicpc.net/problem/1000", + "description": "두 정수 A와 B를 입력받은 다음, A+B를 출력하는 프로그램을 작성하시오.", + "input_description": "첫째 줄에 A와 B가 주어진다. (0 < A, B < 10)", + "output_description": "첫째 줄에 A+B를 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": "MB", + "time_limit": 1.0, + "time_limit_unit": "s", + "created_at": "2024-08-13T02:00:06.358", + "created_by": 1, + "updated_at": "2024-08-14T18:41:16.575" + } + }, + { + "model": "problems.problem", + "pk": 2, + "fields": { + "title": "칵테일", + "link": "https://www.acmicpc.net/problem/1033", + "description": "august14는 세상에서 가장 맛있는 칵테일이다. 이 칵테일을 만드는 정확한 방법은 아직 세상에 공개되지 않았지만, 들어가는 재료 N개는 공개되어 있다. \r\n\r\n경근이는 인터넷 검색을 통해서 재료 쌍 N-1개의 비율을 알아냈고, 이 비율을 이용해서 칵테일에 들어가는 전체 재료의 비율을 알아낼 수 있다.\r\n\r\n총 재료 쌍 N-1개의 비율이 입력으로 주어진다. 이때, 칵테일을 만드는데 필요한 각 재료의 양을 구하는 프로그램을 작성하시오. 이때, 필요한 재료의 질량을 모두 더한 값이 최소가 되어야 한다. 칵테일을 만드는 재료의 양은 정수이고, 총 질량은 0보다 커야한다.\r\n\r\n비율은 \"a b p q\"와 같은 형식이고, a번 재료의 질량을 b번 재료의 질량으로 나눈 값이 p/q라는 뜻이다.", + "input_description": "첫째 줄에 august14를 만드는데 필요한 재료의 개수 N이 주어지며, N은 10보다 작거나 같은 자연수이다.\r\n\r\n둘째 줄부터 N-1개의 줄에는 재료 쌍의 비율이 한 줄에 하나씩 주어지는데, 문제 설명에 나온 형식인 \"a b p q\"로 주어진다. 재료는 0번부터 N-1까지이며, a와 b는 모두 N-1보다 작거나 같은 음이 아닌 정수이다. p와 q는 9보다 작거나 같은 자연수이다.", + "output_description": "첫째 줄에 칵테일을 만드는데 필요한 각 재료의 질량을 0번 재료부터 순서대로 공백으로 구분해 출력한다.", + "memory_limit": 128.0, + "memory_limit_unit": "MB", + "time_limit": 2.0, + "time_limit_unit": "s", + "created_at": "2024-08-13T02:02:27.489", + "created_by": 2, + "updated_at": "2024-08-13T02:08:56.109" + } + } +] \ No newline at end of file diff --git a/app/users/fixtures/sample.json b/app/users/fixtures/sample.json new file mode 100644 index 0000000..6198c84 --- /dev/null +++ b/app/users/fixtures/sample.json @@ -0,0 +1,42 @@ +[ + { + "model": "users.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", + "last_login": null, + "username": "test", + "email": "test@example.com", + "profile_image": "", + "boj_username": "hepheir", + "is_active": true, + "is_staff": false, + "is_superuser": false, + "first_name": "", + "last_name": "", + "created_at": "2024-08-30T06:23:35", + "groups": [], + "user_permissions": [] + } + }, + { + "model": "users.user", + "pk": 2, + "fields": { + "password": "pbkdf2_sha256$600000$RdN5V44kFC4GzDWhboJNeS$FZ0xxnw4+aoOpTlGPzWhODe9g3fMuBuYFOKZ1WoosjA=", + "last_login": null, + "username": "test2", + "email": "test2@example.com", + "profile_image": "", + "boj_username": "test", + "is_active": true, + "is_staff": false, + "is_superuser": false, + "first_name": "", + "last_name": "", + "created_at": "2024-08-30T06:23:35", + "groups": [], + "user_permissions": [] + } + } +] \ No newline at end of file From 766d9234f219ca0bbd3b314c9db3143c965ea8e6 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 16:22:47 +0900 Subject: [PATCH 551/552] =?UTF-8?q?fix(crews.serializers):=20=EB=82=A0?= =?UTF-8?q?=EC=A7=9C=20=ED=95=84=EB=93=9C=EC=9D=98=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=9C=20=EB=A7=A4=ED=95=91=EC=9C=BC=EB=A1=9C=20=EC=98=A4?= =?UTF-8?q?=EB=A5=98=EA=B0=80=20=EB=B0=9C=EC=83=9D=ED=95=98=EB=8D=98=20?= =?UTF-8?q?=EA=B2=83=20=EC=88=98=EC=A0=95=20DateField=20->=20DateTimeField?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/apps/crews/activities/serializers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/apps/crews/activities/serializers.py b/app/apps/crews/activities/serializers.py index c6e9685..7789a24 100644 --- a/app/apps/crews/activities/serializers.py +++ b/app/apps/crews/activities/serializers.py @@ -7,8 +7,8 @@ class CrewActivitySerializer(serializers.ModelSerializer): - date_start_at = serializers.DateField(source=CrewActivity.field_name.START_AT) - date_end_at = serializers.DateField(source=CrewActivity.field_name.END_AT) + date_start_at = serializers.DateTimeField(source=CrewActivity.field_name.START_AT) + date_end_at = serializers.DateTimeField(source=CrewActivity.field_name.END_AT) class Meta: model = CrewActivity From 6ee5264abdb104303ebbc7d88a344ddea4e32d75 Mon Sep 17 00:00:00 2001 From: Hepheir Date: Mon, 2 Sep 2024 16:02:44 +0900 Subject: [PATCH 552/552] =?UTF-8?q?chore:=20=EB=B3=80=EA=B2=BD=ED=95=9C=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8=20=EA=B5=AC=EC=A1=B0=EC=97=90=20=EB=A7=9E?= =?UTF-8?q?=EC=B6=B0=20logging=20=EC=84=A4=EC=A0=95=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/config/settings/base/logging.py | 30 +++---------------- app/logs/app/boj/.gitignore | 2 -- app/logs/app/problems/.gitignore | 2 -- .../{app/background_task => apps}/.gitignore | 0 4 files changed, 4 insertions(+), 30 deletions(-) delete mode 100644 app/logs/app/boj/.gitignore delete mode 100644 app/logs/app/problems/.gitignore rename app/logs/{app/background_task => apps}/.gitignore (100%) diff --git a/app/config/settings/base/logging.py b/app/config/settings/base/logging.py index 186e6f5..d81dd1f 100644 --- a/app/config/settings/base/logging.py +++ b/app/config/settings/base/logging.py @@ -57,24 +57,10 @@ 'when': 'D', "formatter": "standard", }, - "background_task": { + "apps": { "level": "INFO", 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/app/background_task/.log', - 'when': 'D', - "formatter": "standard", - }, - "boj": { - "level": "INFO", - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/app/boj/.log', - 'when': 'D', - "formatter": "standard", - }, - "problems": { - "level": "INFO", - 'class': 'logging.handlers.TimedRotatingFileHandler', - 'filename': BASE_DIR / 'logs/app/problems/.log', + 'filename': BASE_DIR / 'logs/apps/.log', 'when': 'D', "formatter": "standard", }, @@ -94,16 +80,8 @@ "level": "DEBUG", 'propagate': False, }, - "background_task": { - "handlers": ["background_task"], - "level": "INFO", - }, - "boj": { - "handlers": ["boj"], - "level": "INFO", - }, - "problems": { - "handlers": ["problems"], + "apps": { + "handlers": ["apps"], "level": "INFO", }, }, diff --git a/app/logs/app/boj/.gitignore b/app/logs/app/boj/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/app/logs/app/boj/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/app/logs/app/problems/.gitignore b/app/logs/app/problems/.gitignore deleted file mode 100644 index d6b7ef3..0000000 --- a/app/logs/app/problems/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore diff --git a/app/logs/app/background_task/.gitignore b/app/logs/apps/.gitignore similarity index 100% rename from app/logs/app/background_task/.gitignore rename to app/logs/apps/.gitignore