├── .gitignore ├── .travis.yml ├── LICENSE ├── Lutece ├── __init__.py ├── asgi.py ├── celery.py ├── config.py.template ├── routing.py ├── schema.py ├── settings.py ├── urls.py └── wsgi.py ├── README.md ├── article ├── __init__.py ├── admin.py ├── apps.py ├── base │ ├── __init__.py │ ├── constant.py │ ├── form.py │ └── models.py ├── constant.py ├── form.py ├── models.py ├── mutation.py ├── query.py ├── tests.py └── type.py ├── contest ├── __init__.py ├── admin.py ├── apps.py ├── constant.py ├── decorators.py ├── form.py ├── models.py ├── mutation.py ├── query.py ├── tests.py └── type.py ├── data ├── __init__.py ├── constant.py ├── decorators.py ├── service.py ├── test.py ├── urls.py ├── util.py └── views.py ├── image ├── __init__.py ├── admin.py ├── apps.py ├── constant.py ├── form.py ├── models.py ├── schema.py └── tests.py ├── judge ├── __init__.py ├── admin.py ├── apps.py ├── case │ ├── __init__.py │ └── models.py ├── checker.py ├── constant.py ├── language.py ├── models.py ├── result.py ├── tasks.py └── tests.py ├── manage.py ├── problem ├── README.md ├── __init__.py ├── apps.py ├── base │ ├── __init__.py │ ├── constant.py │ ├── form.py │ └── models.py ├── constant.py ├── form.py ├── limitation │ ├── __init__.py │ ├── constant.py │ ├── form.py │ ├── models.py │ └── type.py ├── models.py ├── mutation.py ├── query.py ├── sample │ ├── __init__.py │ ├── constant.py │ ├── form.py │ └── models.py ├── tests.py └── type.py ├── record ├── __init__.py ├── admin.py ├── apps.py ├── models.py ├── schema.py ├── tests.py └── views.py ├── release_port.sh ├── reply ├── __init__.py ├── apps.py ├── constant.py ├── form.py ├── models.py ├── mutation.py ├── query.py ├── schema.py ├── tests.py └── type.py ├── requirements └── requirements.txt ├── run_worker.sh ├── sample ├── __init__.py ├── apps.py ├── constant.py ├── models.py ├── schema.py └── tests.py ├── submission ├── __init__.py ├── apps.py ├── attachinfo │ ├── __init__.py │ └── models.py ├── basesubmission │ ├── __init__.py │ ├── constant.py │ └── models.py ├── constant.py ├── consumers.py ├── form.py ├── models.py ├── mutation.py ├── query.py ├── routing.py ├── tests.py ├── type.py └── util.py ├── tests ├── __init__.py └── utils.py ├── user ├── __init__.py ├── admin.py ├── apps.py ├── attachinfo │ ├── __init__.py │ ├── constant.py │ ├── form.py │ ├── models.py │ └── type.py ├── constant.py ├── form.py ├── jwt │ ├── __init__.py │ ├── decode.py │ └── payload.py ├── models.py ├── mutation.py ├── query.py ├── statistics │ ├── __init__.py │ └── type.py ├── tests.py ├── type.py └── util.py └── utils ├── __init__.py ├── apps.py ├── decorators.py ├── function.py ├── interface.py ├── language.py ├── schema.py └── tests.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | develop-eggs/ 13 | downloads/ 14 | eggs/ 15 | .eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | wheels/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | *.cover 45 | .hypothesis/ 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | local_settings.py 54 | 55 | # Flask stuff: 56 | instance/ 57 | .webassets-cache 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # Jupyter Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # SageMath parsed files 78 | *.sage.py 79 | 80 | # dotenv 81 | .env 82 | 83 | # virtualenv 84 | .venv 85 | venv/ 86 | ENV/ 87 | 88 | # Spyder project settings 89 | .spyderproject 90 | .spyproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | 95 | # mkdocs documentation 96 | /site 97 | 98 | # mypy 99 | .mypy_cache/ 100 | 101 | # vscode work file 102 | .vscode/ 103 | 104 | # pycharm work file 105 | .idea/ 106 | 107 | # vs work file 108 | .vs/ 109 | 110 | # setting.py 111 | Lutece/xiper_local.py 112 | 113 | # runserver_xiper 114 | runserver_xiper.sh 115 | update_xiper.sh 116 | genroot_xiper.sh 117 | test.sh 118 | 119 | # SQLite3 Default DB 120 | Lutece.db 121 | db.sqlite3 122 | Lutece/local_hezhu.py 123 | 124 | # migrations 125 | */migrations/ 126 | 127 | # Lutece_Data 128 | */Lutece_Data/ 129 | 130 | # production.py 131 | production.py 132 | 133 | # media 134 | media/ 135 | 136 | # configure 137 | Lutece/config.py -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | sudo: enabled 4 | services: 5 | - mysql 6 | python: 7 | - "3.7.0" 8 | env: 9 | global: 10 | - lutece_runtime_mode=travis 11 | before_install: 12 | - mysql -u root -e "CREATE DATABASE IF NOT EXISTS runtime_test_db CHARACTER SET utf8 COLLATE utf8_general_ci;" 13 | - mysql -u root -e "CREATE USER 'test_user'@'localhost' IDENTIFIED BY 'lUtEcEtRaViSdB';" 14 | - mysql -u root -e "GRANT ALL PRIVILEGES ON *.* TO 'test_user'@'localhost';" 15 | - mysql -u root -e "FLUSH PRIVILEGES;" 16 | - cp Lutece/config.py.template Lutece/config.py 17 | install: 18 | - pip install -r requirements/requirements.txt 19 | - pip install coveralls 20 | 21 | script: 22 | # Database migrations / migrate test 23 | - python manage.py makemigrations user problem judge submission data article record reply contest 24 | - python manage.py migrate 25 | # Coverage unit test 26 | - coverage run --source=./ manage.py test --noinput 27 | 28 | after_success: 29 | - coveralls -------------------------------------------------------------------------------- /Lutece/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | # This will make sure the app is always imported when 4 | # Django starts so that shared_task will use this app. 5 | from .celery import app as celery_app 6 | 7 | __all__ = ['celery_app'] 8 | -------------------------------------------------------------------------------- /Lutece/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI entrypoint. Configures Django and then runs the application 3 | defined in the ASGI_APPLICATION setting. 4 | """ 5 | 6 | import django 7 | import os 8 | from channels.routing import get_default_application 9 | 10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lutece.settings") 11 | django.setup() 12 | application = get_default_application() 13 | -------------------------------------------------------------------------------- /Lutece/celery.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import os 4 | from celery import Celery 5 | from django.conf import settings 6 | 7 | # set the default Django settings module for the 'celery' program. 8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'Lutece.settings') 9 | 10 | JUDGE = settings.JUDGE 11 | 12 | BROKER_URL = 'pyamqp://{user}:{pwd}@{ip}:{port}/{vhost}'.format( 13 | user=JUDGE.get('rabbitmq_user'), 14 | pwd=JUDGE.get('rabbitmq_pwd'), 15 | ip=JUDGE.get('rabbitmq_ip'), 16 | port=JUDGE.get('rabbitmq_port'), 17 | vhost=JUDGE.get('rabbitmq_vhost')) 18 | 19 | app = Celery( 20 | name='Lutece', 21 | broker=BROKER_URL 22 | ) 23 | 24 | # Using a string here means the worker doesn't have to serialize 25 | # the configuration object to child processes. 26 | # - namespace='CELERY' means all celery-related configuration keys 27 | # should have a `CELERY_` prefix. 28 | app.config_from_object('django.conf:settings', namespace='CELERY') 29 | 30 | # Load task modules from all registered Django app configs. 31 | app.autodiscover_tasks() 32 | 33 | 34 | @app.task(bind=True) 35 | def debug_task(self): 36 | print('Request: {0!r}'.format(self.request)) 37 | -------------------------------------------------------------------------------- /Lutece/config.py.template: -------------------------------------------------------------------------------- 1 | import os 2 | from enum import Enum 3 | 4 | from utils.function import recursive_merge_dicts 5 | 6 | DEFAULT_SECURITY_KEY = 'MakeSecretKeySecretInProdEnv' 7 | TRAVIS_TEST_DB_NAME = 'runtime_test_db' 8 | TRAVIS_TEST_DB_USER = 'test_user' 9 | TRAVIS_TEST_DB_PASSWORD = 'lUtEcEtRaViSdB' 10 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 | 12 | class RunTimeEnv(Enum): 13 | DEV = 1 14 | PROD = 2 15 | TRAVIS_CI = 3 16 | 17 | @staticmethod 18 | def value_of(mode: str): 19 | assert isinstance(mode, str) 20 | mode = mode.lower() 21 | if mode == 'dev' or mode == 'develop': 22 | return RunTimeEnv.DEV 23 | elif mode == 'prod' or mode == 'production': 24 | return RunTimeEnv.PROD 25 | elif mode == 'travis' or mode == 'travisci' or mode == 'tarvis_ci' or mode == 'travis-ci': 26 | return RunTimeEnv.TRAVIS_CI 27 | else: 28 | raise TypeError('Unknown mode ', mode) 29 | 30 | 31 | class RunTimeConfiguration: 32 | 33 | def get_runtime_variables(self, check: bool) -> dict: 34 | if check: 35 | self._check_db() 36 | self._check_security_key() 37 | self._check_data_server() 38 | self._check_judge() 39 | return dict() 40 | 41 | def _check_db(self): 42 | conf = self.get_runtime_variables(False) 43 | if 'DATABASES' not in conf: 44 | raise RuntimeError('There is no dataset configuration') 45 | db = conf.get('DATABASES').get('default') 46 | engine = db.get('ENGINE') 47 | if engine == 'django.db.backends.sqlite3': 48 | pass 49 | elif engine == 'django.db.backends.mysql': 50 | name = db.get('NAME') 51 | user = db.get('USER') 52 | pwd = db.get('PASSWORD') 53 | if not name: 54 | raise RuntimeError('DB name should not empty.') 55 | if not user: 56 | raise RuntimeError('DB username should not empty.') 57 | elif not pwd: 58 | raise RuntimeError('DB password should not empty.') 59 | else: 60 | raise TypeError("Unknown dataset type.") 61 | 62 | def _check_security_key(self): 63 | if isinstance(self, ProdConfiguration): 64 | conf = self.get_runtime_variables(False) 65 | security_key = conf.get('SECRET_KEY') 66 | if security_key == DEFAULT_SECURITY_KEY: 67 | raise RuntimeError( 68 | 'In prod env, security key should not set as default, ' 69 | 'please ref https://docs.djangoproject.com/en/2.1/ref/settings/#std:setting-SECRET_KEY ' 70 | 'to gain detail info.') 71 | 72 | def _check_data_server(self): 73 | if isinstance(self, ProdConfiguration): 74 | conf = self.get_runtime_variables(False) 75 | if not conf.get('DATA_SERVER').get('auth_key'): 76 | raise RuntimeError('In prod env, the data server password should not empty.') 77 | 78 | def _check_judge(self): 79 | if isinstance(self, ProdConfiguration): 80 | conf = self.get_runtime_variables(False) 81 | if not conf.get('JUDGE').get('rabbitmq_pwd'): 82 | raise RuntimeError('In prod env, the RabbitMQ password should not empty.') 83 | 84 | 85 | # The Default Configuration, for most cases, there is no need to change this. 86 | class DefaultConfiguration(RunTimeConfiguration): 87 | _default_config = { 88 | # Open debug mode 89 | 'DEBUG': True, 90 | # Close CORS checking 91 | 'CORS_ORIGIN_ALLOW_ALL': False, 92 | # Allowed host 93 | 'ALLOWED_HOSTS': ['*'], 94 | # SecretKey, keep this secret in prod env 95 | 'SECRET_KEY': DEFAULT_SECURITY_KEY, 96 | # Use sqlite3 as default DB 97 | 'DATABASES': { 98 | 'default': { 99 | 'ENGINE': 'django.db.backends.sqlite3', 100 | 'NAME': 'Lutece.db', 101 | } 102 | }, 103 | # The static files dirs 104 | 'STATICFILES_DIRS': [os.path.join(BASE_DIR, 'frontend/dist/static')], 105 | # Channels layer(using in web-socket message publish and subscribe), use redis as default 106 | 'CHANNEL_LAYERS': { 107 | "default": { 108 | "BACKEND": "channels_redis.core.RedisChannelLayer", 109 | "CONFIG": { 110 | "hosts": [("localhost", 6379)], 111 | }, 112 | }, 113 | }, 114 | # Data server configuration 115 | 'DATA_SERVER': { 116 | 'auth_key': '' 117 | }, 118 | # The judge configuration 119 | 'JUDGE': { 120 | # The ip address that RabbitMQ server 121 | 'rabbitmq_ip': '127.0.0.1', 122 | # The port of RabbitMQ 123 | 'rabbitmq_port': '5672', 124 | # The RabbitMQ user 125 | 'rabbitmq_user': 'task_user', 126 | # The RabbitMQ user pwd 127 | 'rabbitmq_pwd': '', 128 | # The virtual host of RabbitMQ 129 | 'rabbitmq_vhost': 'judger_host', 130 | # The celery task queue name, for most cases, this should not be changed. 131 | 'task_queue': 'task', 132 | # The celery result queue name, for most cases, this should not be changed. 133 | 'result_queue': 'result' 134 | } 135 | } 136 | 137 | def get_runtime_variables(self, check) -> dict: 138 | return recursive_merge_dicts(super().get_runtime_variables(check), self._default_config) 139 | 140 | 141 | class DevConfiguration(DefaultConfiguration): 142 | _dev_config = { 143 | 'CORS_ORIGIN_ALLOW_ALL': True 144 | } 145 | 146 | def get_runtime_variables(self, check) -> dict: 147 | return recursive_merge_dicts(super().get_runtime_variables(check), self._dev_config) 148 | 149 | 150 | class ProdConfiguration(DefaultConfiguration): 151 | _prod_config = { 152 | # Close debug in prod env 153 | 'DEBUG': False, 154 | # Open CORS checking 155 | 'CORS_ORIGIN_ALLOW_ALL': False, 156 | # Only accept the request delegated by nginx 157 | 'ALLOWED_HOSTS': ['127.0.0.1:80'], 158 | # SecretKey, keep this secret in prod env 159 | 'SECRET_KEY': DEFAULT_SECURITY_KEY, 160 | 'DATABASES': { 161 | 'default': { 162 | 'ENGINE': 'django.db.backends.mysql', 163 | 'NAME': '', 164 | 'USER': '', 165 | 'PASSWORD': '', 166 | 'HOST': 'localhost', # default as localhost 167 | 'PORT': '3306', # default as 3306 168 | } 169 | }, 170 | 'STATICFILES_DIRS': ['lutece-frontend-dirs/dist'], 171 | # Data server auth key. 172 | 'DATA_SERVER': { 173 | 'auth_key': '' 174 | }, 175 | # The judge configuration 176 | 'JUDGE': { 177 | # The RabbitMQ user pwd 178 | 'rabbitmq_pwd': '', 179 | } 180 | } 181 | 182 | def get_runtime_variables(self, check) -> dict: 183 | return recursive_merge_dicts(super().get_runtime_variables(check), self._prod_config) 184 | 185 | 186 | class TravisConfiguration(DefaultConfiguration): 187 | _travis_config = { 188 | 'DEBUG': False, 189 | 'DATABASES': { 190 | 'default': { 191 | 'ENGINE': 'django.db.backends.mysql', 192 | 'NAME': TRAVIS_TEST_DB_NAME, 193 | 'USER': TRAVIS_TEST_DB_USER, 194 | 'PASSWORD': TRAVIS_TEST_DB_PASSWORD, 195 | 'HOST': 'localhost', # default as localhost 196 | 'PORT': '3306', # default as 3306 197 | 'TEST': { 198 | 'NAME': 'unit_test_db', 199 | 'CHARSET': "utf8", 200 | 'COLLATION': 'utf8_general_ci' 201 | } 202 | } 203 | } 204 | } 205 | 206 | def get_runtime_variables(self, check) -> dict: 207 | return recursive_merge_dicts(super().get_runtime_variables(check), self._travis_config) 208 | 209 | 210 | def get_runtime_configuration(mode: RunTimeEnv) -> RunTimeConfiguration: 211 | assert isinstance(mode, RunTimeEnv) 212 | if mode is RunTimeEnv.DEV: 213 | return DevConfiguration() 214 | elif mode is RunTimeEnv.PROD: 215 | return ProdConfiguration() 216 | elif mode is RunTimeEnv.TRAVIS_CI: 217 | return TravisConfiguration() 218 | else: 219 | raise TypeError('Unknown type of ', mode) 220 | -------------------------------------------------------------------------------- /Lutece/routing.py: -------------------------------------------------------------------------------- 1 | from channels.routing import ProtocolTypeRouter, URLRouter 2 | 3 | from submission.routing import websocket_urlpatterns 4 | 5 | application = ProtocolTypeRouter({ 6 | 'websocket': ( 7 | URLRouter( 8 | websocket_urlpatterns 9 | ) 10 | ), 11 | }) 12 | -------------------------------------------------------------------------------- /Lutece/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphql_jwt import Verify 3 | 4 | from article.mutation import Mutation as ArticleMutationSchema 5 | from article.query import Query as ArticleQuerySchema 6 | from contest.mutation import Mutation as ContestMutationSchema 7 | from contest.query import Query as ContestQuerySchema 8 | from problem.mutation import Mutation as ProblemMutationSchema 9 | from problem.query import Query as ProblemQuerySchema 10 | from reply.mutation import Mutation as ReplyMutationSchema 11 | from reply.query import Query as ReplyQuerySchema 12 | from submission.mutation import Mutation as SubmissionMutationSchema 13 | from submission.query import Query as SubmissionQuerySchema 14 | from user.mutation import Mutation as UserMutationSchema 15 | from user.query import Query as UserQuerySchema 16 | 17 | 18 | class Query(UserQuerySchema, ProblemQuerySchema, SubmissionQuerySchema, ArticleQuerySchema, ReplyQuerySchema, 19 | graphene.ObjectType, 20 | ContestQuerySchema): 21 | pass 22 | 23 | 24 | class Mutations(UserMutationSchema, ProblemMutationSchema, SubmissionMutationSchema, ArticleMutationSchema, 25 | ReplyMutationSchema, 26 | ContestMutationSchema, 27 | graphene.ObjectType): 28 | verify_token = Verify.Field() 29 | 30 | 31 | schema = graphene.Schema(query=Query, mutation=Mutations) 32 | -------------------------------------------------------------------------------- /Lutece/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for Lutece project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.0.3. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.0/ref/settings/ 11 | """ 12 | import environ 13 | import os 14 | from datetime import timedelta 15 | 16 | from Lutece.config import RunTimeEnv, get_runtime_configuration 17 | 18 | env = environ.Env() 19 | 20 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 21 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 22 | 23 | # Application definition 24 | 25 | INSTALLED_APPS = [ 26 | 'django.contrib.admin', 27 | 'django.contrib.auth', 28 | 'django.contrib.contenttypes', 29 | 'django.contrib.sessions', 30 | 'django.contrib.messages', 31 | 'django.contrib.staticfiles', 32 | 'django.contrib.humanize', 33 | 'django_gravatar', 34 | 'gunicorn', 35 | 'graphene_django', 36 | 'corsheaders', 37 | 'channels', 38 | ] 39 | 40 | LUTECE_APPS = [ 41 | 'user', 42 | 'problem', 43 | 'judge', 44 | 'submission', 45 | 'data', 46 | 'article', 47 | 'record', 48 | 'reply', 49 | 'contest', 50 | ] 51 | 52 | INSTALLED_APPS += LUTECE_APPS 53 | 54 | GRAPHENE = { 55 | 'SCHEMA': 'Lutece.schema.schema', 56 | 'MIDDLEWARE': [ 57 | 'graphql_jwt.middleware.JSONWebTokenMiddleware', 58 | ], 59 | } 60 | 61 | MIDDLEWARE = [ 62 | 'django.middleware.security.SecurityMiddleware', 63 | 'django.contrib.sessions.middleware.SessionMiddleware', 64 | 'corsheaders.middleware.CorsMiddleware', 65 | 'django.middleware.common.CommonMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | 'graphql_jwt.middleware.JSONWebTokenMiddleware', 68 | 'django.contrib.messages.middleware.MessageMiddleware', 69 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 70 | ] 71 | 72 | AUTHENTICATION_BACKENDS = [ 73 | 'graphql_jwt.backends.JSONWebTokenBackend', 74 | 'django.contrib.auth.backends.ModelBackend', 75 | ] 76 | 77 | # Django channels 78 | ASGI_APPLICATION = "Lutece.routing.application" 79 | 80 | ROOT_URLCONF = 'Lutece.urls' 81 | 82 | GRAPHQL_JWT = { 83 | 'JWT_VERIFY_EXPIRATION': True, 84 | 'JWT_EXPIRATION_DELTA': timedelta(hours=12), 85 | 'JWT_REFRESH_EXPIRATION_DELTA': timedelta(days=7), 86 | 'JWT_PAYLOAD_HANDLER': 'user.jwt.payload.payload_handler', 87 | 'JWT_DECODE_HANDLER': 'user.jwt.decode.decode_handler', 88 | } 89 | 90 | WSGI_APPLICATION = 'Lutece.wsgi.application' 91 | AUTH_USER_MODEL = 'user.User' 92 | 93 | PASSWORD_HASHERS = [ 94 | 'django.contrib.auth.hashers.PBKDF2PasswordHasher', 95 | 'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher', 96 | 'django.contrib.auth.hashers.BCryptSHA256PasswordHasher', 97 | 'django.contrib.auth.hashers.BCryptPasswordHasher', 98 | ] 99 | 100 | # Password validation 101 | # https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators 102 | 103 | AUTH_PASSWORD_VALIDATORS = [ 104 | { 105 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 106 | }, 107 | { 108 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 109 | }, 110 | { 111 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 112 | }, 113 | { 114 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 115 | }, 116 | ] 117 | 118 | # Internationalization 119 | # https://docs.djangoproject.com/en/2.0/topics/i18n/ 120 | 121 | LANGUAGE_CODE = 'en-us' 122 | 123 | TIME_ZONE = 'Asia/Shanghai' 124 | 125 | USE_I18N = True 126 | 127 | USE_L10N = True 128 | 129 | USE_TZ = False 130 | 131 | # LOGIN Session: 12 hour 132 | SESSION_COOKIE_AGE = 12 * 60 * 60 133 | 134 | # Templates config 135 | TEMPLATES = [ 136 | { 137 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 138 | 'DIRS': [''], 139 | 'APP_DIRS': True, 140 | 'OPTIONS': { 141 | 'context_processors': [ 142 | 'django.template.context_processors.debug', 143 | 'django.template.context_processors.request', 144 | 'django.contrib.auth.context_processors.auth', 145 | 'django.contrib.messages.context_processors.messages', 146 | ], 147 | }, 148 | }, 149 | ] 150 | 151 | # Static files (CSS, JavaScript, Images) 152 | # https://docs.djangoproject.com/en/2.0/howto/static-files/ 153 | 154 | STATIC_URL = '/static/' 155 | 156 | STATICFILES_FINDERS = ( 157 | "django.contrib.staticfiles.finders.FileSystemFinder", 158 | "django.contrib.staticfiles.finders.AppDirectoriesFinder" 159 | ) 160 | 161 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 162 | 163 | MEDIA_URL = '/media/' 164 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 165 | 166 | # Lutece setting 167 | mode = RunTimeEnv.value_of(env.get_value('lutece_runtime_mode', str, 'dev')) 168 | print(f'- start server with {mode}') 169 | config = get_runtime_configuration(mode).get_runtime_variables(check=True) 170 | 171 | # Inject config variables to settings 172 | 173 | # The frontend dist dir 174 | STATICFILES_DIRS = config.get('STATICFILES_DIRS') 175 | 176 | # The DB settings 177 | DATABASES = config.get('DATABASES') 178 | 179 | # SECURITY WARNING: don't run with debug turned on in production! 180 | DEBUG = config.get('DEBUG') 181 | 182 | # SECURITY WARNING: keep the secret key used in production secret! 183 | SECRET_KEY = config.get('SECRET_KEY') 184 | 185 | # The allowed hosts 186 | ALLOWED_HOSTS = config.get('ALLOWED_HOSTS') 187 | 188 | # CORS settings 189 | CORS_ORIGIN_ALLOW_ALL = config.get('CORS_ORIGIN_ALLOW_ALL') 190 | 191 | # The channel layers 192 | CHANNEL_LAYERS = config.get('CHANNEL_LAYERS') 193 | 194 | # The data server configuration 195 | DATA_SERVER = config.get('DATA_SERVER') 196 | 197 | # The judge configuration 198 | JUDGE = config.get('JUDGE') 199 | 200 | # Max 200 mb file 201 | FILE_UPLOAD_MAX_MEMORY_SIZE = 200 * 1024 * 1024 202 | DATA_UPLOAD_MAX_MEMORY_SIZE = FILE_UPLOAD_MAX_MEMORY_SIZE 203 | -------------------------------------------------------------------------------- /Lutece/urls.py: -------------------------------------------------------------------------------- 1 | """Lutece URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/2.0/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.conf import settings 17 | from django.conf.urls.static import static 18 | from django.contrib import admin 19 | from django.urls import include, path, re_path 20 | from django.views.generic import TemplateView 21 | from graphene_file_upload.django import FileUploadGraphQLView 22 | 23 | urlpatterns = static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + [ 24 | path('admin/', admin.site.urls), 25 | path('data/', include('data.urls')), 26 | ] 27 | 28 | # import graphql 29 | urlpatterns += [path('graphql', FileUploadGraphQLView.as_view(graphiql=settings.DEBUG))] 30 | 31 | urlpatterns += [re_path(r'^.*$', TemplateView.as_view(template_name='static/index.html'))] 32 | -------------------------------------------------------------------------------- /Lutece/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for Lutece project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | from django.core.wsgi import get_wsgi_application 12 | 13 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lutece.settings") 14 | 15 | application = get_wsgi_application() 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lutece-Online-Judge 2 | [![Python](https://img.shields.io/badge/python-3.7.0-red.svg?style=flat-square)](https://www.python.org/downloads/release/python-370/) 3 | [![Django](https://img.shields.io/badge/django-2.1.7-ff69b4.svg?style=flat-square)](https://www.djangoproject.com/) 4 | [![Build Status](https://travis-ci.com/lutece-awesome/lutece-backend.svg?branch=master)](https://travis-ci.com/lutece-awesome/lutece-backend) 5 | [![Coverage Status](https://coveralls.io/repos/github/lutece-awesome/lutece-backend/badge.svg?branch=master)](https://coveralls.io/github/lutece-awesome/lutece-backend?branch=master) 6 | 7 | Simplicity online judge 8 | 9 | ## Installation 10 | 11 | + Install requirements 12 |
13 |     pip3 install -r requirements/requirements.txt
14 | 
15 | 16 | + Create configurion file 17 |
18 |     cp Lutece/config.py.template Lutece/config.py
19 | 
20 | 21 | + Install rabbitmq-server 22 | ### Debian: 23 |
24 |     sudo apt-get update
25 |     sudo apt-get install rabbitmq-server
26 |     sudo systemctl enable rabbitmq-server
27 |     sudo systemctl start rabbitmq-server
28 | 
29 | 30 | ### Arch: 31 |
32 |     sudo pacman -S rabbitmq
33 |     sudo systemctl enable rabbitmq
34 |     sudo systemctl start rabbitmq
35 | 
36 | 37 | + Set task user 38 |
39 |     # You need to set Judger AUTH_KEY
40 |     $ sudo rabbitmqctl add_user task_user AUTH_KEY
41 |     $ sudo rabbitmqctl set_user_tags task_user normal
42 |     $ sudo rabbitmqctl add_vhost judger_host
43 |     $ sudo rabbitmqctl set_permissions -p judger_host task_user ".*" ".*" ".*"
44 | 
45 | 46 | 47 | + Install redis for websocket backend 48 | ### Debian: 49 |
50 |     $ sudo apt-get update
51 |     $ sudo apt-get install redis-server
52 |     $ sudo systemctl enable redis-server
53 |     $ sudo systemctl start redis-server
54 | 
55 | 56 | ### Arch: 57 |
58 |     $ sudo pacman -S redis
59 |     $ sudo systemctl enable redis
60 |     $ sudo systemctl start redis
61 | 
62 | 63 | ### Set running mode 64 | open the .bash_profile file and append the following: 65 | 66 | ``` 67 | # Lutece settings, can be 'dev' or 'prod' or 'travis', default is dev 68 | export lutece_runtime_mode=dev 69 | ``` 70 | 71 | Then source the terminal to make the change work: 72 |
73 |     $ source ~/.bash_profile
74 | 
75 | 76 | ### Create data folder 77 |
78 |     mkdir ~/lutece_data
79 | 
80 | -------------------------------------------------------------------------------- /article/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/article/__init__.py -------------------------------------------------------------------------------- /article/admin.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/article/admin.py -------------------------------------------------------------------------------- /article/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = 'blog' 6 | -------------------------------------------------------------------------------- /article/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/article/base/__init__.py -------------------------------------------------------------------------------- /article/base/constant.py: -------------------------------------------------------------------------------- 1 | MAX_TITLE_LENGTH = 128 2 | MAX_CONTENT_LENGTH = 65535 3 | -------------------------------------------------------------------------------- /article/base/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from article.base.constant import MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH 4 | 5 | 6 | class AbstractArticleForm(forms.Form): 7 | title = forms.CharField(required=True, max_length=MAX_TITLE_LENGTH) 8 | content = forms.CharField(required=False, max_length=MAX_CONTENT_LENGTH) 9 | disable = forms.BooleanField(required=False) 10 | -------------------------------------------------------------------------------- /article/base/models.py: -------------------------------------------------------------------------------- 1 | import django.utils.timezone as timezone 2 | from django.db import models 3 | 4 | from article.base.constant import MAX_TITLE_LENGTH 5 | from user.models import User 6 | 7 | 8 | class AbstractArticle(models.Model): 9 | class Meta: 10 | abstract = True 11 | 12 | title = models.CharField(max_length=MAX_TITLE_LENGTH, blank=True) 13 | author = models.ForeignKey(User, null=True, on_delete=models.SET_NULL) 14 | create_time = models.DateTimeField(default=timezone.now) 15 | last_update_time = models.DateTimeField(default=timezone.now) 16 | disable = models.BooleanField(default=False) 17 | content = models.TextField(blank=True) 18 | 19 | def __str__(self): 20 | return f'Article<{self.title}>' 21 | 22 | def save(self, *args, **kwargs): 23 | self.last_update_time = timezone.now() 24 | super().save(*args, **kwargs) 25 | -------------------------------------------------------------------------------- /article/constant.py: -------------------------------------------------------------------------------- 1 | MAX_PREVIEW_LENGTH = 4096 2 | MAX_SLUG_LENGTH = 256 3 | PER_PAGE_COUNT = 15 4 | COMMENT_PER_PAGE_COUNT = 10 -------------------------------------------------------------------------------- /article/form.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from django import forms 3 | 4 | from article.base.form import AbstractArticleForm 5 | from article.constant import MAX_PREVIEW_LENGTH 6 | from article.models import HomeArticle, UserArticle, Article, ArticleComment 7 | from reply.constant import MAX_CONTENT_LENGTH 8 | 9 | 10 | class UpdateHomeArticleForm(AbstractArticleForm): 11 | preview = forms.CharField(required=False, max_length=MAX_PREVIEW_LENGTH) 12 | slug = forms.CharField(required=True) 13 | 14 | def clean(self) -> dict: 15 | cleaned_data = super().clean() 16 | slug = cleaned_data.get('slug') 17 | if not slug or not get_object_or_None(HomeArticle, slug=slug): 18 | self.add_error("slug", "No such home article") 19 | return cleaned_data 20 | 21 | 22 | class CreateHomeArticleForm(AbstractArticleForm): 23 | preview = forms.CharField(required=False, max_length=MAX_PREVIEW_LENGTH) 24 | 25 | 26 | class CreateUserArticleForm(AbstractArticleForm): 27 | pass 28 | 29 | 30 | class UpdateUserArticleForm(AbstractArticleForm): 31 | pk = forms.IntegerField(required=True) 32 | 33 | def clean(self) -> dict: 34 | cleaned_data = super().clean() 35 | pk = cleaned_data.get('pk') 36 | if pk and not get_object_or_None(UserArticle, pk=pk): 37 | self.add_error("pk", "No such user article") 38 | return cleaned_data 39 | 40 | 41 | class UpdateArticleRecordForm(forms.Form): 42 | pk = forms.IntegerField(required=True) 43 | 44 | def clean(self) -> dict: 45 | cleaned_data = super().clean() 46 | pk = cleaned_data.get('pk') 47 | if pk and not get_object_or_None(Article, pk=pk): 48 | self.add_error("pk", "No such article") 49 | return cleaned_data 50 | 51 | 52 | class ToggleArticleStarForm(forms.Form): 53 | pk = forms.IntegerField(required=True) 54 | 55 | def clean(self) -> dict: 56 | cleaned_data = super().clean() 57 | pk = cleaned_data.get('pk') 58 | if pk and not get_object_or_None(Article, pk=pk): 59 | self.add_error("pk", "No such article") 60 | return cleaned_data 61 | 62 | 63 | class CreateArticleCommentForm(forms.Form): 64 | pk = forms.IntegerField(required=True) 65 | content = forms.CharField(max_length=MAX_CONTENT_LENGTH) 66 | reply = forms.IntegerField(required=False) 67 | 68 | def clean(self) -> dict: 69 | cleaned_data = super().clean() 70 | pk = cleaned_data.get('pk') 71 | if pk and not get_object_or_None(Article, pk=pk): 72 | self.add_error("pk", "No such article") 73 | reply = cleaned_data.get('reply') 74 | if reply and not get_object_or_None(ArticleComment, pk=reply): 75 | self.add_error("reply", "No such reply node") 76 | return cleaned_data 77 | -------------------------------------------------------------------------------- /article/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from uuslug import uuslug 3 | 4 | from article.base.models import AbstractArticle 5 | from article.constant import MAX_SLUG_LENGTH 6 | from record.models import SimpleRecord, DetailedRecord 7 | from reply.models import BaseReply 8 | 9 | 10 | class ArticleRecord(SimpleRecord): 11 | pass 12 | 13 | 14 | # The base class of all sub-class of article 15 | class Article(AbstractArticle): 16 | record = models.OneToOneField(ArticleRecord, on_delete=models.SET_NULL, null=True) 17 | 18 | def save(self, *args, **kwargs): 19 | super().save(*args, **kwargs) 20 | 21 | 22 | class ArticleVote(DetailedRecord): 23 | attitude = models.BooleanField(default=False) 24 | article = models.ForeignKey(Article, on_delete=models.SET_NULL, null=True) 25 | 26 | 27 | # The home page article model 28 | class HomeArticle(Article): 29 | slug = models.CharField(max_length=MAX_SLUG_LENGTH) 30 | preview = models.TextField(blank=True) 31 | rank = models.IntegerField(default=0) 32 | 33 | def save(self, *args, **kwargs): 34 | self.slug = uuslug(self.title, instance=self) 35 | super().save(*args, **kwargs) 36 | 37 | 38 | # The user article model 39 | class UserArticle(Article): 40 | pass 41 | 42 | 43 | class ArticleComment(BaseReply): 44 | article = models.ForeignKey(Article, on_delete=models.SET_NULL, null=True) 45 | -------------------------------------------------------------------------------- /article/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphql import ResolveInfo, GraphQLError 3 | from graphql_jwt.decorators import permission_required, login_required 4 | 5 | from article.form import UpdateHomeArticleForm, CreateHomeArticleForm, CreateUserArticleForm, UpdateUserArticleForm, \ 6 | UpdateArticleRecordForm, ToggleArticleStarForm, CreateArticleCommentForm 7 | from article.models import HomeArticle, UserArticle, ArticleRecord, Article, ArticleVote, ArticleComment 8 | from utils.function import assign 9 | 10 | 11 | class UpdateHomeArticle(graphene.Mutation): 12 | class Arguments: 13 | title = graphene.String(required=True) 14 | slug = graphene.String(required=True) 15 | preview = graphene.String(required=True) 16 | content = graphene.String(required=True) 17 | disable = graphene.Boolean(required=True) 18 | 19 | slug = graphene.String() 20 | 21 | @permission_required('article.change_homearticle') 22 | def mutate(self: None, info: ResolveInfo, **kwargs): 23 | update_home_article_form = UpdateHomeArticleForm(kwargs) 24 | if update_home_article_form.is_valid(): 25 | values = update_home_article_form.cleaned_data 26 | article = HomeArticle.objects.get(slug=values.get('slug')) 27 | assign(article, **values) 28 | article.save() 29 | return UpdateHomeArticle(slug=article.slug) 30 | else: 31 | raise GraphQLError(update_home_article_form.errors.as_json()) 32 | 33 | 34 | class CreateHomeArticle(graphene.Mutation): 35 | class Arguments: 36 | title = graphene.String(required=True) 37 | preview = graphene.String(required=True) 38 | content = graphene.String(required=True) 39 | 40 | slug = graphene.String() 41 | 42 | @permission_required('article.add_homearticle') 43 | def mutate(self: None, info: ResolveInfo, **kwargs): 44 | create_home_article_form = CreateHomeArticleForm(kwargs) 45 | if create_home_article_form.is_valid(): 46 | values = create_home_article_form.cleaned_data 47 | article = HomeArticle.objects.create( 48 | **values, 49 | author=info.context.user, 50 | record=ArticleRecord.objects.create() 51 | ) 52 | return CreateHomeArticle(slug=article.slug) 53 | else: 54 | raise RuntimeError(create_home_article_form.errors.as_json()) 55 | 56 | 57 | class UpdateUserArticle(graphene.Mutation): 58 | class Arguments: 59 | pk = graphene.ID(required=True) 60 | title = graphene.String(required=True) 61 | content = graphene.String(required=True) 62 | 63 | state = graphene.Boolean() 64 | 65 | def mutate(self: None, info: ResolveInfo, **kwargs): 66 | update_user_article_form = UpdateUserArticleForm(kwargs) 67 | if update_user_article_form.is_valid(): 68 | values = update_user_article_form.cleaned_data 69 | article = UserArticle.objects.get(pk=values.get('pk')) 70 | if article.author != info.context.user and not info.context.user.has_perm('article.change_userarticle'): 71 | raise PermissionError('Permission Denied') 72 | article.title = values.get('title') 73 | article.content = values.get('content') 74 | article.save() 75 | return UpdateUserArticle(state=True) 76 | else: 77 | raise RuntimeError(update_user_article_form.errors.as_json()) 78 | 79 | 80 | class CreateUserArticle(graphene.Mutation): 81 | class Arguments: 82 | title = graphene.String(required=True) 83 | content = graphene.String(required=True) 84 | 85 | pk = graphene.ID() 86 | 87 | @login_required 88 | def mutate(self: None, info: ResolveInfo, **kwargs): 89 | create_user_article_form = CreateUserArticleForm(kwargs) 90 | if create_user_article_form.is_valid(): 91 | values = create_user_article_form.cleaned_data 92 | article = UserArticle.objects.create( 93 | **values, 94 | author=info.context.user, 95 | record=ArticleRecord.objects.create() 96 | ) 97 | return CreateUserArticle(pk=article.pk) 98 | else: 99 | raise RuntimeError(create_user_article_form.errors.as_json()) 100 | 101 | 102 | class UpdateArticleRecord(graphene.Mutation): 103 | class Arguments: 104 | pk = graphene.ID(required=True) 105 | 106 | state = graphene.Boolean() 107 | 108 | def mutate(self: None, info: ResolveInfo, **kwargs): 109 | update_article_record = UpdateArticleRecordForm(kwargs) 110 | if update_article_record.is_valid(): 111 | values = update_article_record.cleaned_data 112 | article = Article.objects.get(pk=values.get('pk')) 113 | article.record.increase() 114 | article.record.save() 115 | return UpdateArticleRecord(state=True) 116 | else: 117 | raise RuntimeError(update_article_record.errors.as_json()) 118 | 119 | 120 | class ToggleArticleVote(graphene.Mutation): 121 | class Arguments: 122 | pk = graphene.ID(required=True) 123 | 124 | state = graphene.Boolean() 125 | 126 | @login_required 127 | def mutate(self: None, info: ResolveInfo, **kwargs): 128 | toggle_article_star = ToggleArticleStarForm(kwargs) 129 | if toggle_article_star.is_valid(): 130 | values = toggle_article_star.cleaned_data 131 | article = Article.objects.get(pk=values.get('pk')) 132 | vote, state = ArticleVote.objects.get_or_create(article=article, record_user=info.context.user) 133 | vote.attitude = False if vote.attitude else True 134 | vote.save() 135 | return ToggleArticleVote(state=True) 136 | else: 137 | raise GraphQLError(toggle_article_star.errors.as_json()) 138 | 139 | 140 | class CreateArticleComment(graphene.Mutation): 141 | class Arguments: 142 | pk = graphene.ID(required=True) 143 | content = graphene.String(required=True) 144 | reply = graphene.ID(required=False) 145 | 146 | pk = graphene.ID() 147 | 148 | @login_required 149 | def mutate(self: None, info: ResolveInfo, **kwargs): 150 | create_article_comment = CreateArticleCommentForm(kwargs) 151 | if create_article_comment.is_valid(): 152 | values = create_article_comment.cleaned_data 153 | article = Article.objects.get(pk=values.get('pk')) 154 | reply = values.get('reply') 155 | if reply: 156 | reply = ArticleComment.objects.get(pk=reply) 157 | comment = ArticleComment.objects.create( 158 | article=article, 159 | content=values.get('content'), 160 | reply=reply, 161 | author=info.context.user 162 | ) 163 | return CreateArticleComment(pk=comment.pk) 164 | else: 165 | raise GraphQLError(create_article_comment.errors.as_json()) 166 | 167 | 168 | class Mutation(graphene.AbstractType): 169 | update_home_article = UpdateHomeArticle.Field() 170 | create_home_article = CreateHomeArticle.Field() 171 | update_user_article = UpdateUserArticle.Field() 172 | create_user_article = CreateUserArticle.Field() 173 | update_article_record = UpdateArticleRecord.Field() 174 | toggle_article_vote = ToggleArticleVote.Field() 175 | create_article_comment = CreateArticleComment.Field() 176 | -------------------------------------------------------------------------------- /article/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from django.core.paginator import Paginator 4 | from graphql import ResolveInfo, GraphQLError 5 | 6 | from article.constant import PER_PAGE_COUNT, COMMENT_PER_PAGE_COUNT 7 | from article.models import HomeArticle, UserArticle, ArticleComment, Article 8 | from article.type import HomeArticleType, UserArticleType, HomeArticleListType, ArticleCommentListType 9 | 10 | 11 | class Query(object): 12 | user_article = graphene.Field(UserArticleType, pk=graphene.ID()) 13 | home_article = graphene.Field(HomeArticleType, slug=graphene.ID()) 14 | home_article_list = graphene.Field(HomeArticleListType, page=graphene.Int(), filter=graphene.String()) 15 | article_comment_list = graphene.Field(ArticleCommentListType, pk=graphene.ID(), page=graphene.Int()) 16 | 17 | def resolve_user_article(self: None, info: ResolveInfo, pk: int) -> UserArticle or None: 18 | ret = get_object_or_None(UserArticle, pk=pk) 19 | # if ret is None and been disabled and the request user do not have read permission, ignore 20 | # this request and return none 21 | if ret and ret.disable and not info.context.user.has_perm('article.view_userarticle'): 22 | return None 23 | return ret 24 | 25 | def resolve_home_article(self: None, info: ResolveInfo, slug: str) -> HomeArticle or None: 26 | ret = get_object_or_None(HomeArticle, slug=slug) 27 | # if ret is not None and been disabled and the request user do not have read permission, ignore 28 | # this request and return none 29 | if ret and ret.disable and not info.context.user.has_perm('article.view_homearticle'): 30 | return None 31 | return ret 32 | 33 | def resolve_home_article_list(self: None, info: ResolveInfo, page: int, filter: str) -> HomeArticleListType: 34 | home_article_list = HomeArticle.objects.all() 35 | privilege = info.context.user.has_perm('article.view_homearticle') 36 | if not privilege: 37 | home_article_list = home_article_list.filter(disable=False) 38 | if filter: 39 | home_article_list = home_article_list.filter(title__icontains=filter) 40 | home_article_list = home_article_list.order_by('-create_time') 41 | paginator = Paginator(home_article_list, PER_PAGE_COUNT) 42 | return HomeArticleListType(max_page=paginator.num_pages, home_article_list=paginator.get_page(page)) 43 | 44 | def resolve_article_comment_list(self: None, info: ResolveInfo, pk: int, page: int) -> ArticleCommentListType: 45 | article = get_object_or_None(Article, pk=pk) 46 | if not article: 47 | raise GraphQLError('No such article') 48 | article_comment_list = ArticleComment.objects.filter(article=article) 49 | privilege = info.context.user.has_perm('article.view_articlecomment') 50 | if not privilege: 51 | article_comment_list = article_comment_list.filter(disable=False) 52 | article_comment_list = article_comment_list.order_by('-vote') 53 | paginator = Paginator(article_comment_list, COMMENT_PER_PAGE_COUNT) 54 | return ArticleCommentListType(max_page=paginator.num_pages, article_comment_list=paginator.get_page(page)) 55 | -------------------------------------------------------------------------------- /article/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from graphene_django import DjangoObjectType 4 | from graphql import ResolveInfo 5 | 6 | from article.models import ArticleRecord, ArticleVote 7 | from user.type import UserType 8 | from utils.interface import PaginatorList 9 | from reply.type import AbstractBaseReplyType 10 | 11 | 12 | class ArticleRecordType(DjangoObjectType): 13 | class Meta: 14 | model = ArticleRecord 15 | only_fields = ('count',) 16 | 17 | 18 | class ArticleType(graphene.ObjectType): 19 | pk = graphene.ID() 20 | title = graphene.String() 21 | author = graphene.Field(UserType) 22 | content = graphene.String() 23 | create_time = graphene.DateTime() 24 | last_update_time = graphene.DateTime() 25 | record = graphene.Field(ArticleRecordType) 26 | vote = graphene.Int() 27 | self_attitude = graphene.Boolean() 28 | disable = graphene.Boolean() 29 | 30 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID(): 31 | return self.pk 32 | 33 | def resolve_title(self, info: ResolveInfo) -> graphene.String(): 34 | return self.title 35 | 36 | def resolve_author(self, info: ResolveInfo) -> graphene.Field(UserType): 37 | return self.author 38 | 39 | def resolve_content(self, info: ResolveInfo) -> graphene.String(): 40 | return self.content 41 | 42 | def resolve_create_time(self, info: ResolveInfo) -> graphene.DateTime(): 43 | return self.create_time 44 | 45 | def resolve_last_update_time(self, info: ResolveInfo) -> graphene.DateTime(): 46 | return self.last_update_time 47 | 48 | def resolve_record(self, info: ResolveInfo) -> ArticleRecordType: 49 | return self.record 50 | 51 | def resolve_vote(self, info: ResolveInfo) -> graphene.Int(): 52 | return ArticleVote.objects.filter(article=self, attitude=True).count() 53 | 54 | def resolve_self_attitude(self, info: ResolveInfo) -> graphene.Boolean(): 55 | usr = info.context.user 56 | if not usr.is_authenticated: 57 | return False 58 | vote = get_object_or_None(ArticleVote, article=self, record_user=usr) 59 | return vote.attitude if vote else False 60 | 61 | def resolve_disable(self, info: ResolveInfo) -> graphene.Boolean(): 62 | return self.disable 63 | 64 | 65 | class HomeArticleType(ArticleType): 66 | slug = graphene.String() 67 | preview = graphene.String() 68 | rank = graphene.Int() 69 | 70 | def resolve_slug(self, info: ResolveInfo) -> graphene.String(): 71 | return self.slug 72 | 73 | def resolve_preview(self, info: ResolveInfo) -> graphene.String(): 74 | return self.preview 75 | 76 | def resolve_rank(self, info: ResolveInfo) -> graphene.Int(): 77 | return self.rank 78 | 79 | 80 | class UserArticleType(ArticleType): 81 | pass 82 | 83 | 84 | class HomeArticleListType(graphene.ObjectType, interfaces=[PaginatorList]): 85 | home_article_list = graphene.List(HomeArticleType, ) 86 | 87 | 88 | class ArticleCommentType(AbstractBaseReplyType): 89 | pass 90 | 91 | 92 | class ArticleCommentListType(graphene.ObjectType, interfaces=[PaginatorList]): 93 | article_comment_list = graphene.List(ArticleCommentType, ) 94 | -------------------------------------------------------------------------------- /contest/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/contest/__init__.py -------------------------------------------------------------------------------- /contest/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import Contest 4 | 5 | # Register your models here. 6 | 7 | 8 | admin.site.register(Contest) 9 | -------------------------------------------------------------------------------- /contest/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ContestConfig(AppConfig): 5 | name = 'contest' 6 | -------------------------------------------------------------------------------- /contest/constant.py: -------------------------------------------------------------------------------- 1 | MAX_CONTEST_TITLE_LENGTH = 128 2 | MIN_CONTEST_TEAM_MEMBER = 1 3 | MAX_CONTEST_TEAM_MEMBER = 10 4 | MAX_CONTEST_TEAM_NAME_LENGTH = 48 5 | MAX_CONTEST_PASSWORD_LENGTH = 32 6 | PER_PAGE_COUNT = 15 7 | CLARIFICATION_PER_PAGE_COUNT = 10 8 | MAX_USER_LIST_LENGTH = 8192 9 | MAX_CONTEST_TEAM_INFO_LENGTH = 2048 10 | -------------------------------------------------------------------------------- /contest/decorators.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from graphql import GraphQLError 3 | 4 | from contest.models import Contest, ContestTeamMember 5 | 6 | 7 | def check_contest_permission(func): 8 | def wrapper(*args, **kwargs): 9 | info = args[func.__code__.co_varnames.index('info')] 10 | pk = info.variable_values.get('pk') 11 | contest = get_object_or_None(Contest, pk=pk) 12 | usr = info.context.user 13 | if not contest: 14 | raise GraphQLError('No such contest') 15 | else: 16 | privilege = usr.has_perm('contest.view_contest') 17 | member = None 18 | if usr.is_authenticated: 19 | member = get_object_or_None(ContestTeamMember, user=usr, contest_team__contest=contest, confirmed=True) 20 | if privilege or contest.is_public() or ( 21 | usr.is_authenticated and member and member.contest_team.approved): 22 | return func(*args, **kwargs) 23 | else: 24 | return None 25 | 26 | return wrapper 27 | -------------------------------------------------------------------------------- /contest/form.py: -------------------------------------------------------------------------------- 1 | import json 2 | from annoying.functions import get_object_or_None 3 | from django import forms 4 | from django.utils import timezone 5 | 6 | from contest.constant import MAX_CONTEST_TITLE_LENGTH, MAX_CONTEST_TEAM_MEMBER, MIN_CONTEST_TEAM_MEMBER, \ 7 | MAX_USER_LIST_LENGTH, MAX_CONTEST_TEAM_NAME_LENGTH, MAX_CONTEST_TEAM_INFO_LENGTH 8 | from contest.models import Contest, ContestClarification, ContestTeam 9 | from problem.models import Problem 10 | from reply.constant import MAX_CONTENT_LENGTH 11 | from submission.form import SubmitSubmissionForm 12 | from user.models import User 13 | 14 | 15 | class ContestSettingForm(forms.Form): 16 | title = forms.CharField(required=True, min_length=1, max_length=MAX_CONTEST_TITLE_LENGTH) 17 | note = forms.CharField(required=False) 18 | disable = forms.BooleanField(required=False) 19 | start_time = forms.DateTimeField(required=False) 20 | end_time = forms.DateTimeField(required=False) 21 | max_team_member_number = forms.IntegerField(required=False, min_value=MIN_CONTEST_TEAM_MEMBER, 22 | max_value=MAX_CONTEST_TEAM_MEMBER) 23 | is_public = forms.BooleanField(required=False) 24 | 25 | def clean(self): 26 | cleaned_data = super().clean() 27 | start_time = timezone.localtime(cleaned_data.get('start_time')).replace(tzinfo=None) 28 | end_time = timezone.localtime(cleaned_data.get('end_time')).replace(tzinfo=None) 29 | if start_time >= end_time: 30 | self.add_error('start_time', 'Start time could not before the end time') 31 | return cleaned_data 32 | 33 | 34 | class ContestProblemForm(forms.Form): 35 | problems = forms.CharField(required=True) 36 | 37 | def clean(self): 38 | cleaned_data = super().clean() 39 | problems = cleaned_data.get('problems') 40 | problem_pk_arr = json.loads(problems) 41 | problem_arr = list() 42 | for each_pk in problem_pk_arr: 43 | problem_arr.append(Problem.objects.get(pk=each_pk)) 44 | if len(problem_arr) != len(problem_pk_arr): 45 | self.add_error('problems', 'No duplicated problem allowded') 46 | elif len(problem_arr) > 26: 47 | self.add_error('problem', 'At most 26 problems') 48 | return cleaned_data 49 | 50 | 51 | class ContestForm(ContestProblemForm, ContestSettingForm): 52 | 53 | def clean(self): 54 | return super().clean() 55 | 56 | 57 | class UpdateContestForm(ContestForm): 58 | pk = forms.IntegerField(required=True) 59 | 60 | 61 | class CreateContestClarificationForm(forms.Form): 62 | pk = forms.IntegerField(required=True) 63 | content = forms.CharField(max_length=MAX_CONTENT_LENGTH) 64 | reply = forms.IntegerField(required=False) 65 | 66 | def clean(self) -> dict: 67 | cleaned_data = super().clean() 68 | pk = cleaned_data.get('pk') 69 | if pk and not get_object_or_None(Contest, pk=pk): 70 | self.add_error("pk", "No such contest") 71 | reply = cleaned_data.get('reply') 72 | if reply and not get_object_or_None(ContestClarification, pk=reply): 73 | self.add_error("reply", "No such reply node") 74 | return cleaned_data 75 | 76 | 77 | # Check time on main logic 78 | class ContestSubmissionForm(SubmitSubmissionForm): 79 | pk = forms.IntegerField(required=True) 80 | 81 | def clean(self) -> dict: 82 | cleaned_data = super().clean() 83 | pk = cleaned_data.get('pk') 84 | contest = get_object_or_None(Contest, pk=pk) 85 | if pk and not contest: 86 | self.add_error("pk", "No such contest") 87 | return cleaned_data 88 | 89 | 90 | class CreateContestTeamForm(forms.Form): 91 | pk = forms.IntegerField(required=True) 92 | members = forms.CharField(max_length=MAX_USER_LIST_LENGTH) 93 | name = forms.CharField(max_length=MAX_CONTEST_TEAM_NAME_LENGTH) 94 | additional_info = forms.CharField(required=False, max_length=MAX_CONTEST_TEAM_INFO_LENGTH) 95 | 96 | def clean(self) -> dict: 97 | cleaned_data = super().clean() 98 | pk = cleaned_data.get('pk') 99 | name = cleaned_data.get('name') 100 | contest = get_object_or_None(Contest, pk=pk) 101 | if pk and not contest: 102 | self.add_error("pk", "No such contest") 103 | members = json.loads(cleaned_data.get('members')) 104 | if len(members) > contest.settings.max_team_member_number: 105 | self.add_error('members', 'Team Size exceeded') 106 | if len(set(members)) != len(members): 107 | self.add_error('members', 'Duplicate users') 108 | else: 109 | for each in members: 110 | usr = get_object_or_None(User, username=each) 111 | if not usr: 112 | self.add_error('members', 'no such user') 113 | if get_object_or_None(ContestTeam, contest=contest, name=name): 114 | self.add_error('name', 'duplicate team name') 115 | return cleaned_data 116 | 117 | 118 | class ExitContestTeamForm(forms.Form): 119 | pk = forms.IntegerField(required=True) 120 | 121 | def clean(self) -> dict: 122 | cleaned_data = super().clean() 123 | pk = cleaned_data.get('pk') 124 | contest_team = get_object_or_None(ContestTeam, pk=pk) 125 | if not contest_team: 126 | self.add_error("team_pk", "No such team") 127 | return cleaned_data 128 | 129 | 130 | class ToggleContestTeamForm(forms.Form): 131 | pk = forms.IntegerField(required=True) 132 | 133 | def clean(self) -> dict: 134 | cleaned_data = super().clean() 135 | pk = cleaned_data.get('pk') 136 | contest_team = get_object_or_None(ContestTeam, pk=pk) 137 | if not contest_team: 138 | self.add_error("pk", "No such team") 139 | return cleaned_data 140 | 141 | 142 | class JoinContestTeamForm(forms.Form): 143 | pk = forms.IntegerField(required=True) 144 | 145 | def clean(self) -> dict: 146 | cleaned_data = super().clean() 147 | pk = cleaned_data.get('pk') 148 | team = get_object_or_None(ContestTeam, pk=pk) 149 | if not team: 150 | self.add_error('pk', 'no such team') 151 | return cleaned_data 152 | 153 | 154 | class UpdateContestTeamForm(forms.Form): 155 | pk = forms.IntegerField(required=True) 156 | members = forms.CharField(max_length=MAX_USER_LIST_LENGTH) 157 | name = forms.CharField(max_length=MAX_CONTEST_TEAM_NAME_LENGTH) 158 | additional_info = forms.CharField(required=False, max_length=MAX_CONTEST_TEAM_INFO_LENGTH) 159 | 160 | def clean(self) -> dict: 161 | cleaned_data = super().clean() 162 | pk = cleaned_data.get('pk') 163 | name = cleaned_data.get('name') 164 | team = ContestTeam.objects.get(pk=pk) 165 | members = json.loads(cleaned_data.get('members')) 166 | if len(members) > team.contest.settings.max_team_member_number: 167 | self.add_error('members', 'Team Size exceeded') 168 | if len(set(members)) != len(members): 169 | self.add_error('members', 'Duplicate users') 170 | else: 171 | for each in members: 172 | usr = get_object_or_None(User, username=each) 173 | if not usr: 174 | self.add_error('members', 'no such user') 175 | check_team = get_object_or_None(ContestTeam, contest=team.contest, name=name) 176 | if check_team and check_team != team: 177 | self.add_error('name', 'duplicate team name') 178 | return cleaned_data 179 | -------------------------------------------------------------------------------- /contest/models.py: -------------------------------------------------------------------------------- 1 | import django.utils.timezone as timezone 2 | from django.db import models 3 | 4 | from contest.constant import MAX_CONTEST_TITLE_LENGTH, MAX_CONTEST_PASSWORD_LENGTH, MAX_CONTEST_TEAM_NAME_LENGTH, \ 5 | MAX_CONTEST_TEAM_INFO_LENGTH 6 | from problem.models import Problem 7 | from reply.models import BaseReply 8 | from submission.models import Submission 9 | from user.models import User 10 | 11 | 12 | class ContestSettings(models.Model): 13 | note = models.TextField(blank=True) 14 | disable = models.BooleanField(default=False) 15 | start_time = models.DateTimeField(null=False, default=timezone.now) 16 | end_time = models.DateTimeField(null=False, default=timezone.now) 17 | max_team_member_number = models.IntegerField(default=1) 18 | password = models.CharField(max_length=MAX_CONTEST_PASSWORD_LENGTH) 19 | is_public = models.BooleanField(default=True) 20 | can_join_after_contest_begin = models.BooleanField(default=False) 21 | join_need_approve = models.BooleanField(default=False) 22 | 23 | 24 | class Contest(models.Model): 25 | title = models.CharField(max_length=MAX_CONTEST_TITLE_LENGTH, blank=True, unique=True) 26 | settings = models.OneToOneField(ContestSettings, on_delete=models.CASCADE) 27 | 28 | def is_public(self): 29 | return self.settings.is_public 30 | 31 | 32 | class ContestProblem(models.Model): 33 | contest = models.ForeignKey(Contest, on_delete=models.SET_NULL, null=True) 34 | problem = models.ForeignKey(Problem, on_delete=models.SET_NULL, null=True) 35 | 36 | 37 | class ContestTeam(models.Model): 38 | contest = models.ForeignKey(Contest, on_delete=models.SET_NULL, null=True) 39 | name = models.CharField(max_length=MAX_CONTEST_TEAM_NAME_LENGTH) 40 | owner = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 41 | approved = models.BooleanField(default=False) 42 | additional_info = models.CharField(max_length=MAX_CONTEST_TEAM_INFO_LENGTH, default='') 43 | 44 | def member_list(self): 45 | return self.memeber.all() 46 | 47 | 48 | class ContestTeamMember(models.Model): 49 | contest_team = models.ForeignKey(ContestTeam, on_delete=models.SET_NULL, null=True, related_name='memeber') 50 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 51 | confirmed = models.BooleanField(default=False) 52 | 53 | 54 | # SubmissionType = 1 55 | class ContestSubmission(Submission): 56 | contest = models.ForeignKey(Contest, on_delete=models.SET_NULL, null=True) 57 | team = models.ForeignKey(ContestTeam, on_delete=models.SET_NULL, null=True) 58 | 59 | 60 | class ContestClarification(BaseReply): 61 | contest = models.ForeignKey(Contest, on_delete=models.SET_NULL, null=True) 62 | -------------------------------------------------------------------------------- /contest/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import json 3 | from annoying.functions import get_object_or_None 4 | from django.conf import settings 5 | from django.utils import timezone 6 | from django.utils.datetime_safe import datetime 7 | from graphql import ResolveInfo, GraphQLError 8 | from graphql_jwt.decorators import permission_required, login_required 9 | 10 | from contest.decorators import check_contest_permission 11 | from contest.form import ContestForm, CreateContestClarificationForm, ContestSubmissionForm, CreateContestTeamForm, \ 12 | ExitContestTeamForm, ToggleContestTeamForm, JoinContestTeamForm, UpdateContestTeamForm, UpdateContestForm 13 | from contest.models import Contest, ContestSettings, ContestProblem, ContestClarification, ContestTeamMember, \ 14 | ContestSubmission, ContestTeam 15 | from data.service import DataService 16 | from judge.models import JudgeResult as JudgeResultModel 17 | from judge.result import JudgeResult 18 | from judge.tasks import apply_submission 19 | from problem.models import Problem 20 | from submission.models import SubmissionAttachInfo 21 | from user.models import User 22 | from utils.function import assign 23 | 24 | 25 | class CreateContest(graphene.Mutation): 26 | class Arguments: 27 | title = graphene.String(required=True) 28 | note = graphene.String(required=True) 29 | disable = graphene.Boolean(required=True) 30 | start_time = graphene.DateTime(required=True) 31 | end_time = graphene.DateTime(required=True) 32 | max_team_member_number = graphene.Int(required=True) 33 | is_public = graphene.Boolean(required=True) 34 | problems = graphene.String(required=True) 35 | 36 | pk = graphene.ID() 37 | 38 | @permission_required('contest.add_contest') 39 | def mutate(self, info: ResolveInfo, **kwargs): 40 | form = ContestForm(kwargs) 41 | if form.is_valid(): 42 | values = form.cleaned_data 43 | values['start_time'] = timezone.localtime(values['start_time']).replace(tzinfo=None) 44 | values['end_time'] = timezone.localtime(values['end_time']).replace(tzinfo=None) 45 | problems = json.loads(values.get('problems')) 46 | contest = Contest() 47 | settings = ContestSettings() 48 | assign(settings, **values) 49 | settings.save() 50 | contest.title = values.get('title') 51 | contest.settings = settings 52 | contest.save() 53 | for each in problems: 54 | ContestProblem( 55 | contest=contest, 56 | problem=Problem.objects.get(pk=each) 57 | ).save() 58 | return CreateContest(pk=contest.pk) 59 | else: 60 | raise RuntimeError(form.errors.as_json()) 61 | 62 | 63 | class UpdateContest(graphene.Mutation): 64 | class Arguments: 65 | pk = graphene.ID(required=True) 66 | title = graphene.String(required=True) 67 | note = graphene.String(required=True) 68 | disable = graphene.Boolean(required=True) 69 | start_time = graphene.DateTime(required=True) 70 | end_time = graphene.DateTime(required=True) 71 | max_team_member_number = graphene.Int(required=True) 72 | is_public = graphene.Boolean(required=True) 73 | problems = graphene.String(required=True) 74 | 75 | pk = graphene.ID() 76 | 77 | @permission_required('contest.change_contest') 78 | def mutate(self, info: ResolveInfo, **kwargs): 79 | form = UpdateContestForm(kwargs) 80 | if form.is_valid(): 81 | values = form.cleaned_data 82 | values['start_time'] = timezone.localtime(values['start_time']).replace(tzinfo=None) 83 | values['end_time'] = timezone.localtime(values['end_time']).replace(tzinfo=None) 84 | problems = json.loads(values.get('problems')) 85 | contest = Contest.objects.get(pk=values.get('pk')) 86 | contest.title = values.get('title') 87 | contest.settings.note = values.get('note') 88 | contest.settings.disable = values.get('disable') 89 | contest.settings.start_time = values.get('start_time') 90 | contest.settings.end_time = values.get('end_time') 91 | contest.settings.max_team_member_number = values.get('max_team_member_number') 92 | contest.settings.is_public = values.get('is_public') 93 | contest.settings.save() 94 | contest.save() 95 | ContestProblem.objects.filter(contest=contest).delete() 96 | for each in problems: 97 | ContestProblem( 98 | contest=contest, 99 | problem=Problem.objects.get(pk=each) 100 | ).save() 101 | return UpdateContest(pk=contest.pk) 102 | else: 103 | raise RuntimeError(form.errors.as_json()) 104 | 105 | 106 | class CreateContestClarification(graphene.Mutation): 107 | class Arguments: 108 | pk = graphene.ID(required=True) 109 | content = graphene.String(required=True) 110 | reply = graphene.ID(required=False) 111 | 112 | pk = graphene.ID() 113 | 114 | @check_contest_permission 115 | def mutate(self: None, info: ResolveInfo, **kwargs): 116 | form = CreateContestClarificationForm(kwargs) 117 | if form.is_valid(): 118 | values = form.cleaned_data 119 | contest = Contest.objects.get(pk=values.get('pk')) 120 | reply = values.get('reply') 121 | privilege = info.context.user.has_perm('contest.view_contest') 122 | if datetime.now() < contest.settings.start_time and not privilege: 123 | raise GraphQLError('Time denied') 124 | if reply: 125 | reply = ContestClarification.objects.get(pk=reply) 126 | comment = ContestClarification.objects.create( 127 | contest=contest, 128 | content=values.get('content'), 129 | reply=reply, 130 | author=info.context.user 131 | ) 132 | return CreateContestClarification(pk=comment.pk) 133 | else: 134 | raise GraphQLError(form.errors.as_json()) 135 | 136 | 137 | class ContestSubmitSubmission(graphene.Mutation): 138 | class Arguments: 139 | problem_slug = graphene.String(required=True) 140 | code = graphene.String(required=True) 141 | language = graphene.String(required=True) 142 | pk = graphene.ID(required=True) 143 | 144 | pk = graphene.ID() 145 | 146 | @check_contest_permission 147 | def mutate(self, info: ResolveInfo, *args, **kwargs): 148 | if not info.context.user.is_authenticated: 149 | raise GraphQLError('Permission Denied') 150 | form = ContestSubmissionForm(kwargs) 151 | if form.is_valid(): 152 | values = form.cleaned_data 153 | contest = Contest.objects.get(pk=values.get('pk')) 154 | privilege = info.context.user.has_perm('contest.view_contest') 155 | current_time = datetime.now() 156 | if (current_time < contest.settings.start_time or current_time > contest.settings.end_time) \ 157 | and not privilege: 158 | raise GraphQLError('Time denied') 159 | problem = get_object_or_None(Problem, slug=values['problem_slug']) 160 | attach_info = SubmissionAttachInfo(cases_count=DataService.get_cases_count(problem.pk)) 161 | result = JudgeResultModel(_result=JudgeResult.PD.full) 162 | team_member = get_object_or_None(ContestTeamMember, user=info.context.user, contest_team__contest=contest, 163 | confirmed=True) 164 | if (not team_member or not team_member.confirmed or not team_member.contest_team.approved) \ 165 | and not info.context.user.has_perm('contest.view_contest'): 166 | raise GraphQLError('Permission Denied') 167 | sub = ContestSubmission( 168 | code=values.get('code'), 169 | _language=values.get('language'), 170 | user=info.context.user, 171 | problem=problem, 172 | contest=contest, 173 | team=team_member.contest_team if team_member else None, 174 | submission_type=1 175 | ) 176 | attach_info.save() 177 | result.save() 178 | sub.attach_info = attach_info 179 | sub.result = result 180 | sub.save() 181 | apply_submission.apply_async(args=(sub.get_judge_field(),), queue=settings.JUDGE.get('task_queue')) 182 | problem.ins_submit_times() 183 | return ContestSubmitSubmission(pk=sub.pk) 184 | else: 185 | raise RuntimeError(form.errors.as_json()) 186 | 187 | 188 | class CreateContestTeam(graphene.Mutation): 189 | class Arguments: 190 | pk = graphene.ID(required=True) 191 | members = graphene.String(required=True) 192 | name = graphene.String(required=True) 193 | additional_info = graphene.String(required=False) 194 | 195 | state = graphene.Boolean() 196 | 197 | @login_required 198 | def mutate(self, info: ResolveInfo, *args, **kwargs): 199 | form = CreateContestTeamForm(kwargs) 200 | if form.is_valid(): 201 | values = form.cleaned_data 202 | contest = Contest.objects.get(pk=values.get('pk')) 203 | current_time = datetime.now() 204 | usr = info.context.user 205 | if current_time > contest.settings.end_time: 206 | raise GraphQLError('Time denied') 207 | if get_object_or_None(ContestTeam, contest=contest, owner=usr): 208 | raise GraphQLError('Only one team can be created in one contest') 209 | if get_object_or_None(ContestTeamMember, contest_team__contest=contest, user=usr, confirmed=True): 210 | raise GraphQLError('To create a team, must exit the previous one') 211 | members = json.loads(values.get('members')) 212 | usr = info.context.user 213 | if usr.username not in members: 214 | raise GraphQLError('No owner') 215 | team = ContestTeam.objects.create( 216 | contest=contest, 217 | name=values.get('name'), 218 | owner=info.context.user, 219 | additional_info=values.get('additional_info') 220 | ) 221 | for each in members: 222 | ContestTeamMember.objects.create( 223 | contest_team=team, 224 | user=User.objects.get(username=each), 225 | confirmed=True if each == usr.username else False 226 | ) 227 | return CreateContestTeam(state=True) 228 | else: 229 | raise RuntimeError(form.errors.as_json()) 230 | 231 | 232 | # If team member call this function, would exit team, if owner or user with delete permission, delete the entire team. 233 | class ExitContestTeam(graphene.Mutation): 234 | class Arguments: 235 | pk = graphene.ID() 236 | 237 | state = graphene.Boolean() 238 | 239 | @login_required 240 | def mutate(self, info: ResolveInfo, *args, **kwargs): 241 | form = ExitContestTeamForm(kwargs) 242 | if form.is_valid(): 243 | values = form.cleaned_data 244 | team = ContestTeam.objects.get(pk=values.get('pk')) 245 | contest = team.contest 246 | current_time = datetime.now() 247 | if current_time > contest.settings.end_time: 248 | raise GraphQLError('Time denied') 249 | usr = info.context.user 250 | # If owner exit, delete the entire team 251 | if team.owner == usr: 252 | team.memeber.all().delete() 253 | team.delete() 254 | else: 255 | member = team.memeber.get(user=usr) 256 | member.confirmed = False 257 | member.save() 258 | team.approved = False 259 | team.save() 260 | return ExitContestTeam(state=True) 261 | else: 262 | raise RuntimeError(form.errors.as_json()) 263 | 264 | 265 | class ToggleContestTeam(graphene.Mutation): 266 | class Arguments: 267 | pk = graphene.ID() 268 | 269 | state = graphene.Boolean() 270 | 271 | @permission_required('contest.change_contestteam') 272 | def mutate(self, info: ResolveInfo, *args, **kwargs): 273 | form = ToggleContestTeamForm(kwargs) 274 | if form.is_valid(): 275 | values = form.cleaned_data 276 | team = ContestTeam.objects.get(pk=values.get('pk')) 277 | team.approved = False if team.approved else True 278 | team.save() 279 | return ToggleContestTeam(state=team.approved) 280 | else: 281 | raise RuntimeError(form.errors.as_json()) 282 | 283 | 284 | class JoinContestTeam(graphene.Mutation): 285 | class Arguments: 286 | pk = graphene.ID() 287 | 288 | state = graphene.Boolean() 289 | 290 | @login_required 291 | def mutate(self, info: ResolveInfo, *args, **kwargs): 292 | form = JoinContestTeamForm(kwargs) 293 | if form.is_valid(): 294 | values = form.cleaned_data 295 | usr = info.context.user 296 | team = ContestTeam.objects.get(pk=values.get('pk')) 297 | if get_object_or_None(ContestTeamMember, contest_team__contest=team.contest, user=usr, confirmed=True): 298 | raise GraphQLError('To join other team, must exit the previous one.') 299 | member = team.memeber.get(user=usr) 300 | member.confirmed = True 301 | member.save() 302 | return JoinContestTeam(state=True) 303 | else: 304 | raise RuntimeError(form.errors.as_json()) 305 | 306 | 307 | class UpdateContestTeam(graphene.Mutation): 308 | class Arguments: 309 | pk = graphene.ID(required=True) 310 | members = graphene.String(required=True) 311 | name = graphene.String(required=True) 312 | additional_info = graphene.String(required=False) 313 | 314 | state = graphene.Boolean() 315 | 316 | @login_required 317 | def mutate(self, info: ResolveInfo, *args, **kwargs): 318 | form = UpdateContestTeamForm(kwargs) 319 | if form.is_valid(): 320 | values = form.cleaned_data 321 | team = ContestTeam.objects.get(pk=values.get('pk')) 322 | current_time = datetime.now() 323 | if current_time > team.contest.settings.end_time: 324 | raise GraphQLError('Time denied') 325 | members = json.loads(values.get('members')) 326 | usr = info.context.user 327 | contain_owner = usr.username in members 328 | if (not contain_owner or team.owner != usr) and not usr.has_perm('contest.change_contestteam'): 329 | raise GraphQLError('No owner or permission denied') 330 | name = values.get('name') 331 | additional_info = values.get('additional_info') 332 | if name != team.name or additional_info != team.additional_info or set( 333 | map(lambda each: each.user.username, team.memeber.all())) != set(members): 334 | team.approved = False 335 | team.name = name 336 | team.additional_info = additional_info 337 | team.save() 338 | for each in team.memeber.all(): 339 | if each.user.username not in members: 340 | each.delete() 341 | for each in members: 342 | ContestTeamMember.objects.get_or_create( 343 | contest_team=team, 344 | user=User.objects.get(username=each) 345 | ) 346 | return UpdateContestTeam(state=True) 347 | else: 348 | raise RuntimeError(form.errors.as_json()) 349 | 350 | 351 | class Mutation(graphene.AbstractType): 352 | create_contest = CreateContest.Field() 353 | update_contest = UpdateContest.Field() 354 | create_contest_clarification = CreateContestClarification.Field() 355 | contest_submit_submission = ContestSubmitSubmission.Field() 356 | create_contest_team = CreateContestTeam.Field() 357 | exit_contest_team = ExitContestTeam.Field() 358 | toggle_contest_team = ToggleContestTeam.Field() 359 | join_contest_team = JoinContestTeam.Field() 360 | update_contest_team = UpdateContestTeam.Field() 361 | -------------------------------------------------------------------------------- /contest/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | import json 3 | from annoying.functions import get_object_or_None 4 | from django.core.paginator import Paginator 5 | from django.db.models import Q 6 | from django.utils.datetime_safe import datetime 7 | from graphql import ResolveInfo, GraphQLError 8 | 9 | from contest.constant import PER_PAGE_COUNT, CLARIFICATION_PER_PAGE_COUNT 10 | from contest.decorators import check_contest_permission 11 | from contest.models import Contest, ContestSubmission, ContestTeamMember, \ 12 | ContestClarification, ContestTeam 13 | from contest.type import ContestListType, ContestType, ContestClarificationListType, ContestTeamType 14 | from submission.type import SubmissionListType 15 | 16 | 17 | class Query(object): 18 | contest = graphene.Field(ContestType, pk=graphene.ID()) 19 | contest_list = graphene.Field(ContestListType, page=graphene.Int(), filter=graphene.String()) 20 | contest_submission_list = graphene.Field(SubmissionListType, pk=graphene.ID(), page=graphene.Int(), 21 | problem=graphene.String(), user=graphene.String(), 22 | judge_status=graphene.String(), language=graphene.String()) 23 | contest_ranking_list = graphene.Field(graphene.String, pk=graphene.ID()) 24 | contest_clarification_list = graphene.Field(ContestClarificationListType, pk=graphene.ID(), page=graphene.Int()) 25 | contest_team = graphene.Field(ContestTeamType, pk=graphene.ID()) 26 | contest_team_list = graphene.List(ContestTeamType, pk=graphene.ID()) 27 | related_contest_team_list = graphene.List(ContestTeamType, pk=graphene.ID()) 28 | 29 | def resolve_contest(self: None, info: ResolveInfo, pk: int): 30 | contest_list = Contest.objects.all() 31 | privilege = info.context.user.has_perm('contest.view_contest') 32 | if not privilege: 33 | contest_list = contest_list.filter(settings__disable=False) 34 | return contest_list.get(pk=pk) 35 | 36 | def resolve_contest_list(self: None, info: ResolveInfo, page: int, filter: str): 37 | contest_list = Contest.objects.all() 38 | privilege = info.context.user.has_perm('contest.view_contest') 39 | if not privilege: 40 | contest_list = contest_list.filter(settings__disable=False) 41 | if filter: 42 | contest_list = contest_list.filter(Q(pk__contains=filter) | Q(title__icontains=filter)) 43 | contest_list = contest_list.order_by('-pk') 44 | paginator = Paginator(contest_list, PER_PAGE_COUNT) 45 | return ContestListType(max_page=paginator.num_pages, contest_list=paginator.get_page(page)) 46 | 47 | @check_contest_permission 48 | def resolve_contest_submission_list(self: None, info: ResolveInfo, pk: graphene.ID(), page: graphene.Int(), 49 | **kwargs): 50 | judge_status = kwargs.get('judge_status') 51 | language = kwargs.get('language') 52 | problem = kwargs.get('problem') 53 | user = kwargs.get('user') 54 | contest = Contest.objects.get(pk=pk) 55 | privilege = info.context.user.has_perm('contest.view_contest') 56 | if datetime.now() < contest.settings.start_time and not privilege: 57 | return SubmissionListType(max_page=1, submission_list=[]) 58 | status_list = ContestSubmission.objects.filter(contest=contest) 59 | if not privilege: 60 | team_member = get_object_or_None(ContestTeamMember, contest_team__contest=contest, user=info.context.user, 61 | confirmed=True) 62 | if not team_member or not team_member.contest_team.approved: 63 | return SubmissionListType(max_page=1, submission_list=[]) 64 | status_list = status_list.filter(team=team_member.contest_team) 65 | status_list = status_list.order_by('-pk') 66 | if user: 67 | status_list = status_list.filter(user__username=user) 68 | if problem: 69 | status_list = status_list.filter(problem__slug=problem) 70 | if judge_status: 71 | status_list = status_list.filter(result___result=judge_status) 72 | if language: 73 | status_list = status_list.filter(_language=language) 74 | paginator = Paginator(status_list, PER_PAGE_COUNT) 75 | return SubmissionListType(max_page=paginator.num_pages, submission_list=paginator.get_page(page)) 76 | 77 | @check_contest_permission 78 | def resolve_contest_ranking_list(self: None, info: ResolveInfo, pk: graphene.ID()): 79 | contest = Contest.objects.get(pk=pk) 80 | privilege = info.context.user.has_perm('contest.view_contest') 81 | if datetime.now() < contest.settings.start_time and not privilege: 82 | return '' 83 | return json.dumps([{ 84 | 'status': each.judge_result, 85 | 'createTime': str(each.create_time), 86 | 'team': each.team_name, 87 | 'problemId': each.problem_id, 88 | 'teamApproved': each.team_approved 89 | } for each in (ContestSubmission.objects.raw( 90 | ''' 91 | SELECT 92 | submission_ptr_id, 93 | contest_contestteam.name as team_name, 94 | contest_contestteam.approved as team_approved, 95 | submission_submission.create_time as create_time, 96 | submission_submission.problem_id as problem_id, 97 | submission_submission.result_id as result_id, 98 | judge_judgeresult._result as judge_result 99 | FROM contest_contestsubmission 100 | LEFT JOIN contest_contestteam ON contest_contestsubmission.team_id = contest_contestteam.id 101 | LEFT JOIN submission_submission ON contest_contestsubmission.submission_ptr_id = submission_submission.id 102 | LEFT JOIN judge_judgeresult ON result_id = judge_judgeresult.id 103 | WHERE contest_contestsubmission.contest_id = (%s) and contest_contestsubmission.team_id IS NOT NULL 104 | ''', 105 | (pk,) 106 | ))]) 107 | 108 | @check_contest_permission 109 | def resolve_contest_clarification_list(self: None, info: ResolveInfo, pk: graphene.ID(), page: graphene.Int()): 110 | contest = get_object_or_None(Contest, pk=pk) 111 | privilege = info.context.user.has_perm('contest.view_contest') 112 | if datetime.now() < contest.settings.start_time and not privilege: 113 | return ContestClarificationListType(max_page=1, contest_clarification_list=[]) 114 | if not contest: 115 | raise GraphQLError('No such contest') 116 | clarification_list = ContestClarification.objects.filter(contest=contest) 117 | privilege = info.context.user.has_perm('contest.view_contestclarification') 118 | if not privilege: 119 | clarification_list = clarification_list.filter(disable=False) 120 | clarification_list = clarification_list.order_by('-vote') 121 | paginator = Paginator(clarification_list, CLARIFICATION_PER_PAGE_COUNT) 122 | return ContestClarificationListType(max_page=paginator.num_pages, 123 | contest_clarification_list=paginator.get_page(page)) 124 | 125 | def resolve_contest_team_list(self: None, info: ResolveInfo, pk: graphene.ID()): 126 | contest = Contest.objects.get(pk=pk) 127 | return ContestTeam.objects.filter(contest=contest) 128 | 129 | def resolve_related_contest_team_list(self: None, info: ResolveInfo, pk: graphene.ID()): 130 | contest = Contest.objects.get(pk=pk) 131 | return map(lambda each: each.contest_team, 132 | ContestTeamMember.objects.filter(contest_team__contest=contest, user=info.context.user)) 133 | 134 | def resolve_contest_team(self: None, info: ResolveInfo, pk: graphene.ID()): 135 | return ContestTeam.objects.get(pk=pk) 136 | -------------------------------------------------------------------------------- /contest/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /contest/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from django.db.models import Q 4 | from django.utils.datetime_safe import datetime 5 | from graphql import ResolveInfo 6 | 7 | from contest.decorators import check_contest_permission 8 | from contest.models import ContestTeamMember, ContestSubmission, Contest, ContestTeam, ContestProblem 9 | from judge.result import JudgeResult 10 | from problem.type import ProblemType 11 | from reply.type import AbstractBaseReplyType 12 | from user.type import UserType 13 | from utils.interface import PaginatorList 14 | 15 | 16 | class ContestProblemType(ProblemType): 17 | tried = graphene.Boolean() 18 | solved = graphene.Boolean() 19 | submit = graphene.Int() 20 | accept = graphene.Int() 21 | 22 | def resolve_tried(self, info: ResolveInfo) -> graphene.Boolean(): 23 | usr = info.context.user 24 | if not usr.is_authenticated: 25 | return False 26 | contest = Contest.objects.get(pk=info.variable_values.get('pk')) 27 | team = get_object_or_None(ContestTeam, contest=contest, memeber__user=usr, memeber__confirmed=True) 28 | return ContestSubmission.objects.filter(contest=contest, team=team, problem=self).exists() 29 | 30 | def resolve_solved(self, info: ResolveInfo) -> graphene.Boolean(): 31 | usr = info.context.user 32 | if not usr.is_authenticated: 33 | return False 34 | contest = Contest.objects.get(pk=info.variable_values.get('pk')) 35 | team = get_object_or_None(ContestTeam, contest=contest, memeber__user=usr, memeber__confirmed=True) 36 | return ContestSubmission.objects.filter(contest=contest, team=team, problem=self, 37 | result___result=JudgeResult.AC.full).exists() 38 | 39 | def resolve_submit(self, info: ResolveInfo) -> graphene.Int(): 40 | contest = Contest.objects.get(pk=info.variable_values.get('pk')) 41 | return ContestSubmission.objects.filter(Q(contest=contest) & Q(problem=self) & ~Q(team=None)).count() 42 | 43 | def resolve_accept(self, info: ResolveInfo) -> graphene.Int(): 44 | contest = Contest.objects.get(pk=info.variable_values.get('pk')) 45 | return ContestSubmission.objects.filter( 46 | Q(contest=contest) & Q(problem=self) & ~Q(team=None) & Q(result___result=JudgeResult.AC.full)).values( 47 | 'team').distinct().count() 48 | 49 | 50 | class ContestSettingsType(graphene.ObjectType): 51 | note = graphene.String() 52 | disable = graphene.Boolean() 53 | start_time = graphene.DateTime() 54 | end_time = graphene.DateTime() 55 | max_team_member_number = graphene.Int() 56 | is_public = graphene.Boolean() 57 | 58 | def resolve_note(self, info: ResolveInfo) -> graphene.String(): 59 | return self.note 60 | 61 | def resolve_disable(self, info: ResolveInfo) -> graphene.Boolean(): 62 | return self.disable 63 | 64 | def resolve_start_time(self, info: ResolveInfo) -> graphene.DateTime(): 65 | return self.start_time 66 | 67 | def resolve_end_time(self, info: ResolveInfo) -> graphene.DateTime(): 68 | return self.end_time 69 | 70 | def resolve_max_team_member_number(self, info: ResolveInfo) -> graphene.Int(): 71 | return self.max_team_member_number 72 | 73 | def resolve_is_public(self, info: ResolveInfo) -> graphene.Boolean(): 74 | return self.is_public 75 | 76 | 77 | class ContestType(graphene.ObjectType): 78 | pk = graphene.ID() 79 | title = graphene.String() 80 | settings = graphene.Field(ContestSettingsType) 81 | registered = graphene.Boolean() 82 | register_member_number = graphene.Int() 83 | is_public = graphene.Boolean() 84 | problems = graphene.List(ContestProblemType) 85 | 86 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID(): 87 | return self.pk 88 | 89 | def resolve_title(self, info: ResolveInfo) -> graphene.String(): 90 | return self.title 91 | 92 | def resolve_settings(self, info: ResolveInfo) -> ContestSettingsType: 93 | return self.settings 94 | 95 | def resolve_registered(self, info: ResolveInfo) -> graphene.Boolean(): 96 | usr = info.context.user 97 | if not usr.is_authenticated: 98 | return False 99 | member = get_object_or_None(ContestTeamMember, user=usr, contest_team__contest=self, confirmed=True) 100 | return usr.has_perm('contest.view_contest') or (member and member.contest_team.approved) 101 | 102 | def resolve_register_member_number(self, info: ResolveInfo) -> graphene.Int(): 103 | return ContestTeamMember.objects.filter(contest_team__contest=self, contest_team__approved=True, 104 | confirmed=True).count() 105 | 106 | def resolve_is_public(self, info: ResolveInfo) -> graphene.Boolean(): 107 | return self.is_public() 108 | 109 | @check_contest_permission 110 | def resolve_problems(self, info: ResolveInfo): 111 | privilege = info.context.user.has_perm('contest.view_contestproblem') 112 | if datetime.now() < self.settings.start_time and not privilege: 113 | return [] 114 | return map(lambda each: each.problem, ContestProblem.objects.filter(contest=self)) 115 | 116 | 117 | class ContestListType(graphene.ObjectType, interfaces=[PaginatorList]): 118 | contest_list = graphene.List(ContestType, ) 119 | 120 | 121 | class ContestClarificationType(AbstractBaseReplyType): 122 | pass 123 | 124 | 125 | class ContestClarificationListType(graphene.ObjectType, interfaces=[PaginatorList]): 126 | contest_clarification_list = graphene.List(ContestClarificationType, ) 127 | 128 | 129 | class ContestTeamMemberType(graphene.ObjectType): 130 | user = graphene.Field(UserType) 131 | confirmed = graphene.Boolean() 132 | 133 | def resolve_user(self, info: ResolveInfo): 134 | return self.user 135 | 136 | def resolve_confirmed(self, info: ResolveInfo): 137 | return self.confirmed 138 | 139 | 140 | class ContestTeamType(graphene.ObjectType): 141 | pk = graphene.ID() 142 | name = graphene.String() 143 | member_list = graphene.List(ContestTeamMemberType) 144 | approved = graphene.Boolean() 145 | owner = graphene.Field(UserType) 146 | info = graphene.String() 147 | 148 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID: 149 | return self.pk 150 | 151 | def resolve_name(self, info: ResolveInfo) -> graphene.String: 152 | return self.name 153 | 154 | def resolve_member_list(self, info: ResolveInfo) -> graphene.List: 155 | return self.memeber.all() 156 | 157 | def resolve_approved(self, info: ResolveInfo) -> graphene.Boolean: 158 | return self.approved 159 | 160 | def resolve_owner(self, info: ResolveInfo): 161 | return self.owner 162 | 163 | def resolve_info(self, info: ResolveInfo): 164 | usr = info.context.user 165 | if not usr.has_perm('contest.view_contestteam') and not get_object_or_None(ContestTeamMember, contest_team=self, 166 | user=usr, confirmed=True): 167 | return '' 168 | return self.additional_info 169 | 170 | 171 | class ContestRankingMetaType(graphene.ObjectType): 172 | start_time = graphene.DateTime() 173 | 174 | def resolve_start_time(self, info: ResolveInfo): 175 | return self.settings.start_time 176 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/data/__init__.py -------------------------------------------------------------------------------- /data/constant.py: -------------------------------------------------------------------------------- 1 | # The path of lutece data 2 | DATA_PATH = '~/lutece_data' 3 | 4 | # The input file extension 5 | INPUT_FILE_EXTENSION = '.in' 6 | 7 | # The output file extension 8 | OUTPUT_FILE_EXTENSION = '.out' 9 | 10 | # The md5 file extension 11 | MD5_FILE_EXTENSION = '.md5' 12 | 13 | # The data zip name 14 | DATA_ZIP_NAME = 'Data.zip' 15 | 16 | # MD5 file name 17 | MD5_FILE_NAME = 'data.md5' 18 | 19 | # Allowed data extension 20 | ALLOWED_EXTENSION = ('.zip',) 21 | 22 | # The http meta_field 23 | META_FIELD = { 24 | 'test-data': [INPUT_FILE_EXTENSION, OUTPUT_FILE_EXTENSION], 25 | 'md5-check': [INPUT_FILE_EXTENSION, OUTPUT_FILE_EXTENSION], 26 | 'md5-file': [MD5_FILE_EXTENSION] 27 | } 28 | -------------------------------------------------------------------------------- /data/decorators.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse 3 | 4 | AUTH_KEY = settings.DATA_SERVER.get('auth_key') 5 | 6 | 7 | def judger_auth(function): 8 | def wrapper(*argv, **kw): 9 | try: 10 | if argv[0].POST.get('authkey').encode('ascii') != AUTH_KEY: 11 | return HttpResponse(None) 12 | except Exception as e: 13 | return HttpResponse(None) 14 | return function(*argv, **kw) 15 | 16 | return wrapper 17 | -------------------------------------------------------------------------------- /data/service.py: -------------------------------------------------------------------------------- 1 | import zipfile 2 | from os import path, remove, listdir, mkdir 3 | 4 | from data.constant import DATA_PATH, INPUT_FILE_EXTENSION 5 | 6 | 7 | class DataService: 8 | 9 | @staticmethod 10 | def get_cases_count(problem_pk): 11 | try: 12 | dr = path.join(path.expanduser(DATA_PATH), str(problem_pk)) 13 | return len(list(filter(lambda x: path.splitext(x)[1] == INPUT_FILE_EXTENSION, listdir(dr)))) 14 | except Exception: 15 | raise RuntimeError('Can not load test data folder.') 16 | 17 | @staticmethod 18 | def create_data_dir(problem_pk): 19 | try: 20 | dr = path.join(path.expanduser(DATA_PATH), str(problem_pk)) 21 | mkdir(dr) 22 | except Exception: 23 | raise RuntimeError('Can not create data folder.') 24 | 25 | @staticmethod 26 | def clear_folder_and_extract_data(problem_pk, file): 27 | dr = path.join(path.expanduser(DATA_PATH), str(problem_pk)) 28 | if not path.exists(dr): 29 | mkdir(dr) 30 | for each in listdir(dr): 31 | remove(path.join(dr, each)) 32 | with zipfile.ZipFile(file) as zip_file: 33 | zip_file.extractall(path=dr) 34 | 35 | @staticmethod 36 | def check_datazip(file): 37 | zip_file = zipfile.ZipFile(file) 38 | file_list = zip_file.infolist() 39 | in_li = [] 40 | out_li = [] 41 | for each in file_list: 42 | name = each.filename.title() 43 | if name.find('.') < 0: 44 | raise RuntimeError('Unknown file name ' + name) 45 | spl = name.split('.') 46 | extension = spl[-1].lower() 47 | if extension != 'in' and extension != 'out': 48 | raise RuntimeError('Unknown file name ' + name) 49 | if extension == 'in': 50 | in_li.append(spl[:-1]) 51 | elif extension == 'out': 52 | out_li.append(spl[:-1]) 53 | if len(in_li) != len(out_li): 54 | raise RuntimeError('Input file number not equal output file number') 55 | for each in in_li: 56 | if each not in out_li: 57 | raise RuntimeError('Input / Output file not matching') 58 | -------------------------------------------------------------------------------- /data/test.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | 4 | class TestSerivce(TestCase): 5 | 6 | def simple_test(self): 7 | pass 8 | -------------------------------------------------------------------------------- /data/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import path 2 | 3 | from data.views import fetch_data 4 | 5 | urlpatterns = [ 6 | path('fetch/', fetch_data), 7 | ] 8 | -------------------------------------------------------------------------------- /data/util.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | from os import path, listdir 3 | 4 | from data.constant import META_FIELD, DATA_PATH, MD5_FILE_NAME 5 | 6 | 7 | def get_data(problem, data_type): 8 | ''' 9 | get data of data_type 10 | ''' 11 | try: 12 | dr = path.join(path.expanduser(DATA_PATH), str(problem)) 13 | li = list(filter(lambda x: path.splitext(x)[1] in META_FIELD.get(data_type), listdir(dr))) 14 | li.sort() 15 | _send = {} 16 | for _ in li: 17 | with open(path.join(dr, _), "rb") as file: 18 | _send[_] = file.read() 19 | return _send 20 | except: 21 | return None 22 | 23 | 24 | def cal_md5_or_create(problem, force=False): 25 | ''' 26 | Calcuate the md5-field of problem folder 27 | if force is True, always create/update md5 file 28 | ''' 29 | try: 30 | dr = path.join(path.expanduser(DATA_PATH), str(problem)) 31 | li = listdir(dr) 32 | if MD5_FILE_NAME in li and force is False: 33 | return True, None 34 | li = list(filter(lambda x: path.splitext(x)[1] in META_FIELD['md5-check'], li)) 35 | args = [] 36 | for _ in li: 37 | with open(path.join(dr, _), "rb") as file: 38 | md5 = hashlib.md5() 39 | md5.update(file.read()) 40 | content = md5.hexdigest() 41 | args.append((_, content)) 42 | args.sort() 43 | with open(path.join(dr, MD5_FILE_NAME), "w") as file: 44 | file.write(str(args)) 45 | except Exception as e: 46 | return False, str(e) 47 | return True, None 48 | 49 | 50 | def process(request): 51 | ''' 52 | process the target request 53 | ''' 54 | problem = request.POST.get('problem') 55 | data_type = request.POST.get('type') 56 | if data_type == 'md5-file': 57 | cal_md5_or_create(problem) 58 | return get_data(problem, data_type) 59 | -------------------------------------------------------------------------------- /data/views.py: -------------------------------------------------------------------------------- 1 | from django.http import HttpResponse 2 | from pickle import dumps as pickle_dumps 3 | 4 | from data.decorators import judger_auth 5 | from data.util import process 6 | 7 | 8 | @judger_auth 9 | def fetch_data(request): 10 | return HttpResponse(pickle_dumps(process(request), 2), content_type='application/json') 11 | -------------------------------------------------------------------------------- /image/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/image/__init__.py -------------------------------------------------------------------------------- /image/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /image/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ImageConfig(AppConfig): 5 | name = 'image' 6 | -------------------------------------------------------------------------------- /image/constant.py: -------------------------------------------------------------------------------- 1 | # The max size limitation for image uploading. 2 | MAX_IMAGE_SIZE = 2 * 1024 * 1024 3 | 4 | # Image Path 5 | UPLOAD_IMAGE_PATH = 'image/%Y/%m/%d' 6 | -------------------------------------------------------------------------------- /image/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from image.constant import MAX_IMAGE_SIZE 4 | 5 | 6 | class UploadImageForm(forms.Form): 7 | image = forms.ImageField(required=True) 8 | 9 | def clean(self): 10 | cleaned_data = super().clean() 11 | image = cleaned_data.get('image') 12 | if image and image.size > MAX_IMAGE_SIZE: 13 | self.add_error('image', 'Image size can not exceed 2mb') 14 | -------------------------------------------------------------------------------- /image/models.py: -------------------------------------------------------------------------------- 1 | import django.utils.timezone as timezone 2 | from django.db import models 3 | 4 | from image.constant import UPLOAD_IMAGE_PATH 5 | from user.models import User 6 | 7 | 8 | # Create your models here. 9 | 10 | class Image(models.Model): 11 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, db_index=True) 12 | upload_time = models.DateTimeField(default=timezone.now) 13 | image = models.ImageField(upload_to=UPLOAD_IMAGE_PATH) 14 | -------------------------------------------------------------------------------- /image/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_file_upload.scalars import Upload 3 | from graphql_jwt.decorators import login_required 4 | 5 | from image.form import UploadImageForm 6 | 7 | 8 | class UploadImage(graphene.Mutation): 9 | class Arguments: 10 | file = Upload(required=True) 11 | 12 | path = graphene.String() 13 | 14 | @login_required 15 | def mutate(self, info, *args, **kwargs): 16 | request = info.context 17 | image = request.FILES['0'] 18 | request.FILES.pop('0') 19 | request.FILES['image'] = image 20 | image_form = UploadImageForm(request.POST, request.FILES) 21 | if image_form.is_valid(): 22 | values = image_form.cleaned_data 23 | s = UploadFile( 24 | image=values['image'], 25 | user=request.user 26 | ) 27 | s.save() 28 | return UploadImage(path=s.image.url) 29 | else: 30 | raise RuntimeError(image_form.errors.as_json()) 31 | 32 | 33 | class Mutation(graphene.AbstractType): 34 | uploadImage = UploadImage.Field() 35 | -------------------------------------------------------------------------------- /image/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /judge/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/judge/__init__.py -------------------------------------------------------------------------------- /judge/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /judge/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JudgeConfig(AppConfig): 5 | name = 'judge' 6 | -------------------------------------------------------------------------------- /judge/case/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/judge/case/__init__.py -------------------------------------------------------------------------------- /judge/case/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from judge.constant import MAX_RESULT_LENGTH 4 | from judge.result import JudgeResult as JudgeResultService 5 | 6 | 7 | class AbstractCase(models.Model): 8 | class Meta: 9 | abstract = True 10 | 11 | time_cost = models.IntegerField(default=0) 12 | memory_cost = models.IntegerField(default=0) 13 | case = models.IntegerField(default=0) 14 | _result = models.CharField(choices=((each.full, each.detail) for each in JudgeResultService.all()), 15 | max_length=MAX_RESULT_LENGTH, db_index=True) 16 | 17 | @property 18 | def result(self, *args, **kwargs): 19 | return JudgeResultService.value_of(self._result) 20 | -------------------------------------------------------------------------------- /judge/checker.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | from utils.decorators import classproperty 4 | 5 | 6 | class _meta: 7 | __slots__ = ( 8 | 'full', 9 | 'info', 10 | '_field' 11 | ) 12 | 13 | def __init__(self, **kw): 14 | for _ in kw: 15 | self.__setattr__(_, kw[_]) 16 | self._field = [x for x in kw] 17 | 18 | def __str__(self): 19 | return f'' 20 | 21 | def __repr__(self): 22 | return f'' 23 | 24 | @property 25 | def attribute(self): 26 | return {x: getattr(self, x) for x in self._field} 27 | 28 | 29 | @unique 30 | class Checker(Enum): 31 | _WCMP = _meta( 32 | full='wcmp', 33 | info='Compare sequences of tokens', 34 | ) 35 | 36 | _RCMP6 = _meta( 37 | full='rcmp6', 38 | info='Compare two sequences of doubles, max absolute or relative error = 1E-6', 39 | ) 40 | 41 | _SPJ = _meta( 42 | full='spj', 43 | info='Customized judge program', 44 | ) 45 | 46 | @classproperty 47 | def WCMP(cls): 48 | return cls._WCMP.value 49 | 50 | @classproperty 51 | def RCMP6(cls): 52 | return cls._RCMP6.value 53 | 54 | @classproperty 55 | def SPJ(cls): 56 | return cls._SPJ.value 57 | 58 | @classmethod 59 | def value_of(cls, value): 60 | for each in cls: 61 | if each.value.full == value: 62 | return each.value 63 | return None 64 | 65 | @classmethod 66 | def all(cls): 67 | return [each.value for each in cls] 68 | -------------------------------------------------------------------------------- /judge/constant.py: -------------------------------------------------------------------------------- 1 | # The max length of result length 2 | MAX_RESULT_LENGTH = 32 3 | 4 | # The max length of compile info 5 | MAX_COMPILE_LENGTH = 512 6 | 7 | # The max length of error info 8 | MAX_ERROR_LENGTH = 512 9 | -------------------------------------------------------------------------------- /judge/language.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | from typing import List 3 | 4 | from utils.decorators import classproperty 5 | 6 | 7 | class _meta: 8 | __slots__ = ( 9 | 'full', 10 | 'version', 11 | 'codemirror', 12 | 'info', 13 | '_field' 14 | ) 15 | 16 | def __init__(self, **kw): 17 | for _ in kw: 18 | self.__setattr__(_, kw[_]) 19 | self._field = [x for x in kw] 20 | 21 | def __str__(self): 22 | return f'' 23 | 24 | def __repr__(self): 25 | return f'' 26 | 27 | @property 28 | def attribute(self): 29 | return {x: getattr(self, x) for x in self._field} 30 | 31 | 32 | @unique 33 | class Language(Enum): 34 | _GNUCPP = _meta( 35 | full='GNU G++', 36 | version='7.3.0', 37 | info='GNU G++17', 38 | codemirror='text/x-c++src', 39 | ) 40 | 41 | _GNUGCC = _meta( 42 | full='GNU GCC', 43 | version='7.3.0', 44 | info='GNU GCC 7.3', 45 | codemirror='text/x-csrc', 46 | ) 47 | 48 | _CLANG = _meta( 49 | full='Clang', 50 | version='6.0.0', 51 | info='Clang 6.0.0', 52 | codemirror='text/x-c++src', 53 | ) 54 | 55 | _PYTHON = _meta( 56 | full='Python', 57 | version='3.6.5', 58 | info='Python 3.6.5', 59 | codemirror='text/x-python' 60 | ) 61 | 62 | _JAVA = _meta( 63 | full='Java', 64 | version='10', 65 | info='Java 10', 66 | codemirror='text/x-java' 67 | ) 68 | 69 | _GO = _meta( 70 | full='Go', 71 | version='1.10.2', 72 | info='Go 1.10.2', 73 | codemirror='text/x-go' 74 | ) 75 | 76 | _RUBY = _meta( 77 | full='Ruby', 78 | version='2.5.1', 79 | info='Ruby 2.5.1', 80 | codemirror='text/x-ruby' 81 | ) 82 | 83 | _RUST = _meta( 84 | full='Rust', 85 | version='1.26.1', 86 | info='Rust 1.26.1', 87 | codemirror='text/x-rustsrc' 88 | ) 89 | 90 | @classproperty 91 | def GNUCPP(cls) -> _meta: 92 | return cls._GNUCPP.value 93 | 94 | @classproperty 95 | def GNUGCC(cls) -> _meta: 96 | return cls._GNUGCC.value 97 | 98 | @classproperty 99 | def CLANG(cls) -> _meta: 100 | return cls._CLANG.value 101 | 102 | @classproperty 103 | def PYTHON(cls) -> _meta: 104 | return cls._PYTHON.value 105 | 106 | @classproperty 107 | def JAVA(cls) -> _meta: 108 | return cls._JAVA.value 109 | 110 | @classproperty 111 | def GO(cls) -> _meta: 112 | return cls._GO.value 113 | 114 | @classproperty 115 | def RUBY(cls) -> _meta: 116 | return cls._RUBY.value 117 | 118 | @classproperty 119 | def RUST(cls) -> _meta: 120 | return cls._RUST.value 121 | 122 | @classmethod 123 | def value_of(cls, value: str) -> _meta: 124 | for each in cls: 125 | if each.value.full == value: 126 | return each.value 127 | return None 128 | 129 | @classmethod 130 | def all(cls) -> List[_meta]: 131 | return [each.value for each in cls] 132 | -------------------------------------------------------------------------------- /judge/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from judge.constant import MAX_RESULT_LENGTH, MAX_COMPILE_LENGTH, MAX_ERROR_LENGTH 4 | from judge.result import JudgeResult as JudgeResultService 5 | 6 | 7 | class JudgeResult(models.Model): 8 | _result = models.CharField(choices=((each.full, each.detail) for each in JudgeResultService.all()), 9 | max_length=MAX_RESULT_LENGTH, db_index=True) 10 | compile_info = models.CharField(max_length=MAX_COMPILE_LENGTH, blank=True) 11 | error_info = models.CharField(max_length=MAX_ERROR_LENGTH, blank=True) 12 | done = models.BooleanField(default=False) 13 | 14 | @property 15 | def result(self, *args, **kwargs): 16 | return JudgeResultService.value_of(self._result) 17 | -------------------------------------------------------------------------------- /judge/result.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | from typing import List 3 | 4 | from utils.decorators import classproperty 5 | 6 | 7 | class _meta: 8 | __slots__ = ( 9 | 'full', 10 | 'alias', 11 | 'color', 12 | 'detail', 13 | '_field' 14 | ) 15 | 16 | def __init__(self, **kw): 17 | for _ in kw: 18 | setattr(self, _, kw[_]) 19 | self._field = [x for x in kw] 20 | 21 | def __str__(self): 22 | return f'' 23 | 24 | def __repr__(self): 25 | return f'' 26 | 27 | @property 28 | def attribute(self): 29 | return {x: getattr(self, x) for x in self._field} 30 | 31 | 32 | @unique 33 | class JudgeResult(Enum): 34 | _PD = _meta( 35 | full='Pending', 36 | alias='PD', 37 | color='info', 38 | detail='Judger is too busy to judge your solution. Just be kindly patient to waiting a moment.', 39 | ) 40 | 41 | _PR = _meta( 42 | full='Preparing', 43 | alias='PR', 44 | color='info', 45 | detail='Judger has fetched your solution, now is preparing test data.', 46 | ) 47 | 48 | _AC = _meta( 49 | full='Accepted', 50 | alias='AC', 51 | color='success', 52 | detail='Your solution has produced exactly right output.', 53 | ) 54 | 55 | _RN = _meta( 56 | full='Running', 57 | alias='RN', 58 | color='info', 59 | detail='The program of your solution is running on the judger.', 60 | ) 61 | 62 | _CE = _meta( 63 | full='Compile Error', 64 | alias='CE', 65 | color='warning', 66 | detail='Your solution cannot be compiled into any program that executed by the system.', 67 | ) 68 | 69 | _WA = _meta( 70 | full='Wrong Answer', 71 | alias='WA', 72 | color='error', 73 | detail='Your solution has not produced the desired output for the input given by system.', 74 | ) 75 | 76 | _RE = _meta( 77 | full='Runtime Error', 78 | alias='RE', 79 | color='error', 80 | detail='Your solution has caused an unhandled exception during execution.', 81 | ) 82 | 83 | _TLE = _meta( 84 | full='Time Limit Exceeded', 85 | alias='TLE', 86 | color='error', 87 | detail='Your solution has run for longer time than permitted time limit.', 88 | ) 89 | 90 | _OLE = _meta( 91 | full='Output Limit Exceeded', 92 | alias='OLE', 93 | color='error', 94 | detail='Your solution has produced overmuch output.', 95 | ) 96 | 97 | _MLE = _meta( 98 | full='Memory Limit Exceeded', 99 | alias='MLE', 100 | color='error', 101 | detail='Your solution has consumed more memory than permitted memory limit.', 102 | ) 103 | 104 | _JE = _meta( 105 | full='Judger Error', 106 | alias='JE', 107 | color='warning', 108 | detail='Some unexpected errors occur in judger.', 109 | ) 110 | 111 | @classproperty 112 | def PD(cls) -> _meta: 113 | return cls._PD.value 114 | 115 | @classproperty 116 | def PR(cls) -> _meta: 117 | return cls._PR.value 118 | 119 | @classproperty 120 | def AC(cls) -> _meta: 121 | return cls._AC.value 122 | 123 | @classproperty 124 | def RN(cls) -> _meta: 125 | return cls._RN.value 126 | 127 | @classproperty 128 | def CE(cls) -> _meta: 129 | return cls._CE.value 130 | 131 | @classproperty 132 | def WA(cls) -> _meta: 133 | return cls._WA.value 134 | 135 | @classproperty 136 | def RE(cls) -> _meta: 137 | return cls._RE.value 138 | 139 | @classproperty 140 | def TLE(cls) -> _meta: 141 | return cls._TLE.value 142 | 143 | @classproperty 144 | def OLE(cls) -> _meta: 145 | return cls._OLE.value 146 | 147 | @classproperty 148 | def MLE(cls) -> _meta: 149 | return cls._MLE.value 150 | 151 | @classproperty 152 | def JE(cls) -> _meta: 153 | return cls._JE.value 154 | 155 | @classmethod 156 | def value_of(cls, value) -> _meta: 157 | for each in cls: 158 | if each.value.full == value: 159 | return each.value 160 | return None 161 | 162 | @classmethod 163 | def all(cls) -> List[_meta]: 164 | return [each.value for each in cls] 165 | 166 | @classmethod 167 | def is_failed(cls, which: _meta) -> bool: 168 | _fail = (cls.WA, cls.RE, cls.RN, cls.TLE, cls.OLE, cls.MLE) 169 | if which in _fail: 170 | return True 171 | return False 172 | -------------------------------------------------------------------------------- /judge/tasks.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | from celery import shared_task 4 | 5 | from submission.util import Modify_submission_status 6 | 7 | 8 | @shared_task(name='Judger.task') 9 | def apply_submission(submission): 10 | pass 11 | 12 | 13 | @shared_task(name='Judger.result') 14 | def Submission_result(report): 15 | Modify_submission_status(**report) 16 | -------------------------------------------------------------------------------- /judge/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | import os 5 | 6 | if __name__ == "__main__": 7 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Lutece.settings") 8 | try: 9 | from django.core.management import execute_from_command_line 10 | except ImportError as exc: 11 | raise ImportError( 12 | "Couldn't import Django. Are you sure it's installed and " 13 | "available on your PYTHONPATH environment variable? Did you " 14 | "forget to activate a virtual environment?" 15 | ) from exc 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /problem/README.md: -------------------------------------------------------------------------------- 1 | ## Problem App 架构设计 2 | -------------------------------------------------------------------------------- /problem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/problem/__init__.py -------------------------------------------------------------------------------- /problem/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProblemConfig(AppConfig): 5 | name = 'problem' 6 | -------------------------------------------------------------------------------- /problem/base/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/problem/base/__init__.py -------------------------------------------------------------------------------- /problem/base/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of problem title 2 | MAX_TITLE_LENGTH = 256 3 | 4 | # The max length limitation of problem content 5 | MAX_CONTENT_LENGTH = 32768 6 | 7 | # The max length limitation of problem resources 8 | MAX_RESOURCES_LENGTH = 256 9 | 10 | # Te max length limitation of problem constraints 11 | MAX_CONSTRAINTS_LENGTH = 16384 12 | 13 | # The max length limitation of problem note 14 | MAX_NOTE_LENGTH = 16384 15 | 16 | # The max length limitation of problem standard input 17 | MAX_STANDARD_INPUT_LENGTH = 16384 18 | 19 | # The max length limitation of problem standard output 20 | MAX_STANDARD_OUTPUT_LENGTH = 16384 21 | 22 | # The max length of slug 23 | MAX_SLUG_LENGTH = MAX_TITLE_LENGTH * 2 24 | -------------------------------------------------------------------------------- /problem/base/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from problem.base.constant import MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH, MAX_RESOURCES_LENGTH, \ 4 | MAX_CONSTRAINTS_LENGTH, MAX_NOTE_LENGTH, MAX_STANDARD_INPUT_LENGTH, MAX_STANDARD_OUTPUT_LENGTH 5 | 6 | 7 | class AbstractProblemForm(forms.Form): 8 | title = forms.CharField(required=True, max_length=MAX_TITLE_LENGTH) 9 | content = forms.CharField(required=False, max_length=MAX_CONTENT_LENGTH) 10 | resources = forms.CharField(required=False, max_length=MAX_RESOURCES_LENGTH) 11 | constraints = forms.CharField(required=False, max_length=MAX_CONSTRAINTS_LENGTH) 12 | note = forms.CharField(required=False, max_length=MAX_NOTE_LENGTH) 13 | standard_input = forms.CharField(required=False, max_length=MAX_STANDARD_INPUT_LENGTH) 14 | standard_output = forms.CharField(required=False, max_length=MAX_STANDARD_OUTPUT_LENGTH) 15 | disable = forms.BooleanField(required=False) 16 | -------------------------------------------------------------------------------- /problem/base/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from uuslug import uuslug 3 | 4 | from problem.base.constant import MAX_TITLE_LENGTH, MAX_CONTENT_LENGTH, MAX_RESOURCES_LENGTH, \ 5 | MAX_CONSTRAINTS_LENGTH, MAX_NOTE_LENGTH, MAX_STANDARD_INPUT_LENGTH, MAX_STANDARD_OUTPUT_LENGTH, MAX_SLUG_LENGTH 6 | 7 | 8 | class AbstractProblem(models.Model): 9 | class Meta: 10 | abstract = True 11 | 12 | title = models.CharField(max_length=MAX_TITLE_LENGTH, db_index=True) 13 | content = models.TextField(max_length=MAX_CONTENT_LENGTH, blank=True) 14 | resources = models.CharField(max_length=MAX_RESOURCES_LENGTH, blank=True) 15 | constraints = models.TextField(max_length=MAX_CONSTRAINTS_LENGTH, blank=True) 16 | standard_input = models.TextField(max_length=MAX_STANDARD_INPUT_LENGTH, blank=True) 17 | standard_output = models.TextField(max_length=MAX_STANDARD_OUTPUT_LENGTH, blank=True) 18 | note = models.TextField(max_length=MAX_NOTE_LENGTH, blank=True) 19 | slug = models.CharField(max_length=MAX_SLUG_LENGTH) 20 | disable = models.BooleanField(default=False) 21 | 22 | def __str__(self): 23 | return f'' 24 | 25 | def __unicode__(self): 26 | return f'' 27 | 28 | def save(self, *args, **kwargs): 29 | self.slug = uuslug(self.title, instance=self) 30 | super().save(*args, **kwargs) 31 | -------------------------------------------------------------------------------- /problem/constant.py: -------------------------------------------------------------------------------- 1 | # The limitation of items number in single page 2 | PER_PAGE_COUNT = 15 3 | 4 | # The max length of checker 5 | MAX_CHECKER_LENGTH = 32 6 | -------------------------------------------------------------------------------- /problem/form.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from django import forms 3 | 4 | from problem.base.form import AbstractProblemForm 5 | from problem.limitation.form import LimitationForm 6 | from problem.models import Problem 7 | from problem.sample.form import SampleForm 8 | 9 | 10 | class UpdateProblemForm(AbstractProblemForm, LimitationForm, SampleForm): 11 | slug = forms.CharField(required=True) 12 | 13 | def clean(self, *args, **kwargs): 14 | cleaned_data = super().clean() 15 | slug = cleaned_data.get('slug') 16 | if not get_object_or_None(Problem, slug=slug): 17 | self.add_error('slug', 'Unknown problem for such slug.') 18 | return cleaned_data 19 | 20 | 21 | class CreateProblemForm(AbstractProblemForm, LimitationForm, SampleForm): 22 | pass 23 | -------------------------------------------------------------------------------- /problem/limitation/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/problem/limitation/__init__.py -------------------------------------------------------------------------------- /problem/limitation/constant.py: -------------------------------------------------------------------------------- 1 | # The deault time limitation for problem 2 | DEFAULT_TIME_LIMIT = 2000 3 | 4 | # The default memory limitation for problem 5 | DEFAULT_MEMORY_LIMIT = 128 6 | 7 | # The default output limitation for problem 8 | DEFAULT_OUTPUT_LIMIT = 64 9 | 10 | # The deault cpu number limitation for problem 11 | DEFAULT_CPU_NUMBER = 1 12 | 13 | # The maximum time limitation 14 | MAX_TIME_LIMITATION = 60000 15 | 16 | # The minimum time limitation 17 | MIN_TIME_LIMITATION = 100 18 | 19 | # The maximum memory limitation 20 | MAX_MEMORY_LIMITATION = 1024 21 | 22 | # The minimum memory limitation 23 | MIN_MEMORY_LIMITATION = 16 24 | 25 | # The maximum output limitation 26 | MAX_OUTPUT_LIMITATION = 512 27 | 28 | # The minimum output limitation 29 | MIN_OUTPUT_LIMITATION = 16 30 | 31 | # The maximum cpu number limitation 32 | MAX_CPU_LIMITATION = 8 33 | 34 | # The minimum cpu number limitation 35 | MIN_CPU_LIMITATION = 1 36 | -------------------------------------------------------------------------------- /problem/limitation/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from problem.limitation.constant import MAX_TIME_LIMITATION, MIN_TIME_LIMITATION, MAX_MEMORY_LIMITATION, \ 4 | MIN_MEMORY_LIMITATION, MAX_OUTPUT_LIMITATION, MIN_OUTPUT_LIMITATION, MAX_CPU_LIMITATION, MIN_CPU_LIMITATION 5 | 6 | 7 | class LimitationForm(forms.Form): 8 | time_limit = forms.IntegerField(required=True, max_value=MAX_TIME_LIMITATION, min_value=MIN_TIME_LIMITATION) 9 | memory_limit = forms.IntegerField(required=True, max_value=MAX_MEMORY_LIMITATION, min_value=MIN_MEMORY_LIMITATION) 10 | output_limit = forms.IntegerField(required=False, max_value=MAX_OUTPUT_LIMITATION, min_value=MIN_OUTPUT_LIMITATION) 11 | cpu_limit = forms.IntegerField(required=False, max_value=MAX_CPU_LIMITATION, min_value=MIN_CPU_LIMITATION) 12 | -------------------------------------------------------------------------------- /problem/limitation/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from problem.limitation.constant import DEFAULT_TIME_LIMIT, DEFAULT_MEMORY_LIMIT, DEFAULT_OUTPUT_LIMIT, \ 4 | DEFAULT_CPU_NUMBER 5 | 6 | 7 | class Limitation(models.Model): 8 | time_limit = models.PositiveIntegerField(default=DEFAULT_TIME_LIMIT) 9 | memory_limit = models.PositiveIntegerField(default=DEFAULT_MEMORY_LIMIT) 10 | output_limit = models.PositiveIntegerField(default=DEFAULT_OUTPUT_LIMIT) 11 | cpu_limit = models.PositiveIntegerField(default=DEFAULT_CPU_NUMBER) 12 | -------------------------------------------------------------------------------- /problem/limitation/type.py: -------------------------------------------------------------------------------- 1 | from graphene_django.types import DjangoObjectType 2 | 3 | from problem.limitation.models import Limitation 4 | 5 | 6 | class AbstractLimiationType(DjangoObjectType): 7 | class Meta: 8 | model = Limitation 9 | only_fields = ("time_limit", "memory_limit", "output_limit", "cpu_limit") 10 | -------------------------------------------------------------------------------- /problem/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from judge.checker import Checker 4 | from problem.base.models import AbstractProblem 5 | from problem.constant import MAX_CHECKER_LENGTH 6 | from problem.limitation.models import Limitation 7 | from problem.sample.models import AbstractSample 8 | 9 | 10 | class Problem(AbstractProblem): 11 | submit = models.IntegerField(default=0) 12 | accept = models.IntegerField(default=0) 13 | limitation = models.OneToOneField(Limitation, on_delete=models.CASCADE) 14 | _checker = models.CharField(choices=((each.full, each.info) for each in Checker.all()), 15 | max_length=MAX_CHECKER_LENGTH, db_index=True, default=Checker.WCMP.full) 16 | 17 | def ins_submit_times(self): 18 | self.submit += 1 19 | self.save() 20 | 21 | def ins_accept_times(self): 22 | self.accept += 1 23 | self.save() 24 | 25 | @property 26 | def checker(self): 27 | return Checker.value_of(self._checker) 28 | 29 | 30 | class ProblemSample(AbstractSample): 31 | problem = models.ForeignKey(Problem, on_delete=models.SET_NULL, null=True) 32 | -------------------------------------------------------------------------------- /problem/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_file_upload.scalars import Upload 3 | from graphql import ResolveInfo 4 | from graphql_jwt.decorators import permission_required 5 | from json import loads 6 | 7 | from data.service import DataService 8 | from problem.form import UpdateProblemForm, CreateProblemForm 9 | from problem.limitation.models import Limitation 10 | from problem.models import Problem, ProblemSample 11 | from utils.function import assign 12 | 13 | 14 | class UpdateProblem(graphene.Mutation): 15 | class Arguments: 16 | title = graphene.String(required=True) 17 | standard_input = graphene.String(required=True) 18 | standard_output = graphene.String(required=True) 19 | content = graphene.String(required=True) 20 | resources = graphene.String(required=True) 21 | constraints = graphene.String(required=True) 22 | note = graphene.String(required=True) 23 | 24 | time_limit = graphene.Int(required=True) 25 | memory_limit = graphene.Int(required=True) 26 | output_limit = graphene.Int(required=True) 27 | cpu_limit = graphene.Int(required=True) 28 | 29 | samples = graphene.String(required=True) 30 | 31 | disable = graphene.Boolean(required=True) 32 | 33 | slug = graphene.String(required=True) 34 | 35 | slug = graphene.String() 36 | 37 | @permission_required('problem.change') 38 | def mutate(self, info: ResolveInfo, **kwargs): 39 | form = UpdateProblemForm(kwargs) 40 | if form.is_valid(): 41 | values = form.cleaned_data 42 | samples = loads(values.get('samples')) 43 | prob = Problem.objects.get(slug=values.get('slug')) 44 | assign(prob, **values) 45 | assign(prob.limitation, **values) 46 | prob.limitation.save() 47 | prob.save() 48 | ProblemSample.objects.filter(problem=prob).delete() 49 | for each in samples: 50 | ProblemSample( 51 | input_content=each.get('inputContent'), 52 | output_content=each.get('outputContent'), 53 | problem=prob 54 | ).save() 55 | # To avoid the slug change, re-fetch the problem object 56 | return UpdateProblem(slug=prob.slug) 57 | else: 58 | raise RuntimeError(form.errors.as_json()) 59 | 60 | 61 | class CreateProblem(graphene.Mutation): 62 | class Arguments: 63 | title = graphene.String(required=True) 64 | content = graphene.String(required=False) 65 | resources = graphene.String(required=False) 66 | constraints = graphene.String(required=False) 67 | note = graphene.String(required=False) 68 | standard_input = graphene.String(required=False) 69 | standard_output = graphene.String(required=False) 70 | 71 | time_limit = graphene.Int(required=True) 72 | memory_limit = graphene.Int(required=True) 73 | output_limit = graphene.Int(required=True) 74 | cpu_limit = graphene.Int(required=True) 75 | 76 | disable = graphene.Boolean(required=True) 77 | 78 | samples = graphene.String(required=True) 79 | 80 | slug = graphene.String() 81 | 82 | @permission_required('problem.add') 83 | def mutate(self, info: ResolveInfo, **kwargs): 84 | form = CreateProblemForm(kwargs) 85 | if form.is_valid(): 86 | values = form.cleaned_data 87 | samples = loads(values.get('samples')) 88 | prob = Problem() 89 | limitation = Limitation() 90 | assign(prob, **values) 91 | assign(limitation, **values) 92 | limitation.save() 93 | prob.limitation = limitation 94 | prob.save() 95 | for each in samples: 96 | ProblemSample( 97 | input_content=each.get('inputContent'), 98 | output_content=each.get('outputContent'), 99 | problem=prob 100 | ).save() 101 | return CreateProblem(slug=prob.slug) 102 | else: 103 | raise RuntimeError(form.errors.as_json()) 104 | 105 | 106 | class UpdateProblemData(graphene.Mutation): 107 | class Arguments: 108 | pk = graphene.ID(required=True) 109 | file = Upload(required=True) 110 | 111 | state = graphene.Boolean() 112 | 113 | @permission_required('problem.change') 114 | def mutate(self, info: ResolveInfo, **kwargs): 115 | try: 116 | file = kwargs.get('file') 117 | pk = kwargs.get('pk') 118 | DataService.check_datazip(file) 119 | DataService.clear_folder_and_extract_data(pk, file) 120 | except Exception as e: 121 | raise RuntimeError(str(e)) 122 | 123 | 124 | class Mutation(graphene.AbstractType): 125 | update_problem = UpdateProblem.Field() 126 | create_problem = CreateProblem.Field() 127 | update_problem_data = UpdateProblemData.Field() 128 | -------------------------------------------------------------------------------- /problem/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.core.paginator import Paginator 3 | from django.db.models import Q 4 | from graphql import ResolveInfo 5 | 6 | from problem.constant import PER_PAGE_COUNT 7 | from problem.models import Problem 8 | from problem.type import ProblemType, ProblemListType 9 | 10 | 11 | class Query(object): 12 | problem = graphene.Field(ProblemType, slug=graphene.String()) 13 | problem_list = graphene.Field(ProblemListType, page=graphene.Int(), filter=graphene.String()) 14 | problem_search = graphene.Field(ProblemListType, filter=graphene.String()) 15 | 16 | def resolve_problem(self: None, info: ResolveInfo, slug): 17 | problem_list = Problem.objects.all() 18 | privilege = info.context.user.has_perm('problem.view') 19 | if not privilege: 20 | problem_list = problem_list.filter(disable=False) 21 | return problem_list.get(slug=slug) 22 | 23 | def resolve_problem_list(self: None, info: ResolveInfo, page: int, filter: str): 24 | problem_list = Problem.objects.all() 25 | privilege = info.context.user.has_perm('problem.view') 26 | if not privilege: 27 | problem_list = problem_list.filter(disable=False) 28 | if filter: 29 | problem_list = problem_list.filter(Q(pk__contains=filter) | Q(title__icontains=filter)) 30 | paginator = Paginator(problem_list, PER_PAGE_COUNT) 31 | return ProblemListType(max_page=paginator.num_pages, problem_list=paginator.get_page(page)) 32 | 33 | ''' 34 | Search the matching problem of the specific filter. 35 | Nothing would return if there is no filter(to avoid the empty filter situation). 36 | ''' 37 | def resolve_problem_search(self: None, info: ResolveInfo, filter: str): 38 | problem_list = Problem.objects.all() 39 | if not info.context.user.has_perm('problem.view_all'): 40 | problem_list = problem_list.filter(disable=False) 41 | if filter: 42 | problem_list = problem_list.filter(Q(pk__contains=filter) | Q(title__icontains=filter)) 43 | else: 44 | problem_list = [] 45 | return ProblemListType(max_page=1, problem_list=problem_list[:5]) 46 | -------------------------------------------------------------------------------- /problem/sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/problem/sample/__init__.py -------------------------------------------------------------------------------- /problem/sample/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of sample input 2 | MAX_SAMPLE_INPUT_LENGTH = 1024 3 | 4 | # The max length limitation of sample output 5 | MAX_SAMPLE_OUTPUT_LENGTH = 1024 6 | -------------------------------------------------------------------------------- /problem/sample/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from json import loads 3 | 4 | from problem.sample.constant import MAX_SAMPLE_INPUT_LENGTH, MAX_SAMPLE_OUTPUT_LENGTH 5 | 6 | 7 | class SampleForm(forms.Form): 8 | samples = forms.CharField(required=True) 9 | 10 | def clean(self, *args, **kwargs) -> dict: 11 | cleaned_data = super().clean() 12 | samples = cleaned_data.get('samples') 13 | sample_arr = loads(samples) 14 | for each in sample_arr: 15 | input_content = each.get('inputContent') 16 | output_content = each.get('outputContent') 17 | if len(input_content) > MAX_SAMPLE_INPUT_LENGTH or len(output_content) > MAX_SAMPLE_OUTPUT_LENGTH: 18 | self.add_error('samples', 'The length of sample is too long.') 19 | break 20 | return cleaned_data 21 | -------------------------------------------------------------------------------- /problem/sample/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from problem.sample.constant import MAX_SAMPLE_INPUT_LENGTH, MAX_SAMPLE_OUTPUT_LENGTH 4 | 5 | 6 | class AbstractSample(models.Model): 7 | class Meta: 8 | abstract = True 9 | 10 | input_content = models.CharField(max_length=MAX_SAMPLE_INPUT_LENGTH, blank=True) 11 | output_content = models.CharField(max_length=MAX_SAMPLE_OUTPUT_LENGTH, blank=True) 12 | -------------------------------------------------------------------------------- /problem/tests.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /problem/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | from graphql import ResolveInfo 4 | 5 | from data.service import DataService 6 | from problem.limitation.type import AbstractLimiationType 7 | from problem.models import ProblemSample 8 | from utils.interface import PaginatorList 9 | 10 | 11 | class ProblemSampleType(DjangoObjectType): 12 | class Meta: 13 | model = ProblemSample 14 | only_fields = ("input_content", 'output_content') 15 | 16 | 17 | class ProblemSampleListType(graphene.ObjectType): 18 | sample_list = graphene.List(ProblemSampleType, ) 19 | 20 | 21 | class ProblemType(graphene.ObjectType): 22 | title = graphene.String() 23 | content = graphene.String() 24 | resources = graphene.String() 25 | note = graphene.String() 26 | slug = graphene.String() 27 | constraints = graphene.String() 28 | standard_input = graphene.String() 29 | standard_output = graphene.String() 30 | submit = graphene.Int() 31 | accept = graphene.Int() 32 | disable = graphene.Boolean() 33 | pk = graphene.ID() 34 | limitation = graphene.Field(AbstractLimiationType) 35 | samples = graphene.Field(ProblemSampleListType) 36 | data_count = graphene.Int() 37 | 38 | def resolve_title(self, info: ResolveInfo) -> graphene.String(): 39 | return self.title 40 | 41 | def resolve_content(self, info: ResolveInfo) -> graphene.String(): 42 | return self.content 43 | 44 | def resolve_resources(self, info: ResolveInfo) -> graphene.String(): 45 | return self.resources 46 | 47 | def resolve_note(self, info: ResolveInfo) -> graphene.String(): 48 | return self.note 49 | 50 | def resolve_slug(self, info: ResolveInfo) -> graphene.String(): 51 | return self.slug 52 | 53 | def resolve_constraints(self, info: ResolveInfo) -> graphene.String(): 54 | return self.constraints 55 | 56 | def resolve_standard_input(self, info: ResolveInfo) -> graphene.String(): 57 | return self.standard_input 58 | 59 | def resolve_standard_output(self, info: ResolveInfo) -> graphene.String(): 60 | return self.standard_output 61 | 62 | def resolve_standard_submit(self, info: ResolveInfo) -> graphene.Int(): 63 | return self.submit 64 | 65 | def resolve_standard_accept(self, info: ResolveInfo) -> graphene.Int(): 66 | return self.accept 67 | 68 | def resolve_disable(self, info: ResolveInfo) -> graphene.Boolean(): 69 | return self.disable 70 | 71 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID(): 72 | return self.pk 73 | 74 | def resolve_limitation(self, info: ResolveInfo) -> graphene.Field(AbstractLimiationType): 75 | return self.limitation 76 | 77 | def resolve_samples(self, info: ResolveInfo) -> graphene.Field(ProblemSampleListType): 78 | result = ProblemSample.objects.filter(problem=self) 79 | return ProblemSampleListType(sample_list=result) 80 | 81 | def resolve_data_count(self, info: ResolveInfo) -> graphene.Int: 82 | return DataService.get_cases_count(self.pk) 83 | 84 | 85 | class ProblemListType(graphene.ObjectType, interfaces=[PaginatorList]): 86 | problem_list = graphene.List(ProblemType, ) 87 | -------------------------------------------------------------------------------- /record/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/record/__init__.py -------------------------------------------------------------------------------- /record/admin.py: -------------------------------------------------------------------------------- 1 | # Register your models here. 2 | -------------------------------------------------------------------------------- /record/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class RecordConfig(AppConfig): 5 | name = 'record' 6 | -------------------------------------------------------------------------------- /record/models.py: -------------------------------------------------------------------------------- 1 | import django.utils.timezone as timezone 2 | from django.db import models 3 | 4 | from user.models import User 5 | 6 | 7 | # Create your models here. 8 | 9 | 10 | class AbstractRecord(models.Model): 11 | class Meta: 12 | abstract = True 13 | 14 | def save(self, *args, **kwargs): 15 | super().save(*args, **kwargs) 16 | 17 | 18 | class DetailedRecord(AbstractRecord): 19 | record_user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 20 | record_time = models.DateTimeField(default=timezone.now, db_index=True) 21 | 22 | def save(self, *args, **kwargs): 23 | super().save(*args, **kwargs) 24 | 25 | 26 | class SimpleRecord(AbstractRecord): 27 | count = models.IntegerField(default=0) 28 | 29 | def __ins(self, add): 30 | self.count += add 31 | 32 | def save(self, *args, **kwargs): 33 | super().save(*args, **kwargs) 34 | 35 | def increase(self): 36 | self.__ins(1) 37 | 38 | 39 | class Attitude: 40 | agree = 'Agree' 41 | neutral = 'Neutral' 42 | disagree = 'Disagree' 43 | choice = { 44 | (agree, 'Agree'), 45 | (neutral, 'Only god knows'), 46 | (disagree, 'Disagree'), 47 | } 48 | 49 | # class BaseReplyVote(DetailedRecord): 50 | # reply = models.ForeignKey(AbstractReply, on_delete=models.SET_NULL, null=True) 51 | # attitude = models.CharField(choices=Attitude.choice, default=Attitude.neutral, max_length=MAX_ATTITUDE_LENGTH, 52 | # db_index=True) 53 | -------------------------------------------------------------------------------- /record/schema.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /record/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /record/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /release_port.sh: -------------------------------------------------------------------------------- 1 | kill -9 $(lsof -ti tcp:6106) 2 | kill -9 $(lsof -ti tcp:6100) -------------------------------------------------------------------------------- /reply/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/reply/__init__.py -------------------------------------------------------------------------------- /reply/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ReplyConfig(AppConfig): 5 | name = 'reply' 6 | -------------------------------------------------------------------------------- /reply/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of reply content 2 | MAX_CONTENT_LENGTH = 8192 3 | 4 | # The max length limitation of reply vote attitude 5 | MAX_ATTITUDE_LENGTH = 16 6 | 7 | # The per page count for reply comment 8 | REPLY_COMMENT_PER_PAGE_COUNT = 10 9 | -------------------------------------------------------------------------------- /reply/form.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from django import forms 3 | 4 | from reply.constant import MAX_CONTENT_LENGTH 5 | from reply.models import BaseReply 6 | 7 | 8 | class BaseReplyForm(forms.Form): 9 | content = forms.CharField(required=True, max_length=1024) 10 | parent = forms.IntegerField(required=False) 11 | 12 | def clean(self): 13 | cleaned_data = super().clean() 14 | parent = cleaned_data.get('parent') 15 | # If this reply have parent, check it 16 | if parent: 17 | par = get_object_or_None(BaseReply, pk=parent) 18 | if par is None: 19 | self.add_error('parent', 'Unknown reply parent node') 20 | 21 | 22 | class UpdateBaseReplyForm(forms.Form): 23 | pk = forms.IntegerField(required=True) 24 | content = forms.CharField(max_length=MAX_CONTENT_LENGTH) 25 | 26 | def clean(self) -> dict: 27 | cleaned_data = super().clean() 28 | pk = cleaned_data.get('pk') 29 | if pk and not get_object_or_None(BaseReply, pk=pk): 30 | self.add_error("pk", "No such reply") 31 | return cleaned_data 32 | 33 | 34 | class CreateCommentReplyForm(forms.Form): 35 | parent = forms.IntegerField(required=True) 36 | content = forms.CharField(max_length=MAX_CONTENT_LENGTH) 37 | 38 | def clean(self) -> dict: 39 | cleaned_data = super().clean() 40 | parent = cleaned_data.get('parent') 41 | reply = get_object_or_None(BaseReply, pk=parent) 42 | if parent and (not reply or reply.disable): 43 | self.add_error("parent", "No such reply") 44 | if reply.ancestor: 45 | self.add_error("parent", "Nested reply is not supported") 46 | 47 | 48 | class ToggleReplyVoteForm(forms.Form): 49 | pk = forms.IntegerField(required=True) 50 | 51 | def clean(self) -> dict: 52 | cleaned_data = super().clean() 53 | pk = cleaned_data.get('pk') 54 | if pk and not get_object_or_None(BaseReply, pk=pk): 55 | self.add_error("pk", "No such reply") 56 | return cleaned_data 57 | -------------------------------------------------------------------------------- /reply/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | from record.models import DetailedRecord 5 | from reply.constant import MAX_CONTENT_LENGTH 6 | from user.models import User 7 | 8 | 9 | class BaseReply(models.Model): 10 | content = models.CharField(max_length=MAX_CONTENT_LENGTH, blank=True) 11 | author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 12 | reply = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, db_index=True, related_name='reply_node') 13 | ancestor = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, db_index=True, 14 | related_name='ancestor_node') 15 | create_time = models.DateField(default=timezone.now) 16 | disable = models.BooleanField(default=False, db_index=True) 17 | create_time = models.DateTimeField(default=timezone.now) 18 | last_update_time = models.DateTimeField(default=timezone.now) 19 | vote = models.IntegerField(default=0) 20 | 21 | def save(self, *args, **kwargs): 22 | self.last_update_time = timezone.now() 23 | super().save(*args, **kwargs) 24 | 25 | 26 | class ReplyVote(DetailedRecord): 27 | attitude = models.BooleanField(default=False) 28 | reply = models.ForeignKey(BaseReply, on_delete=models.SET_NULL, null=True) 29 | -------------------------------------------------------------------------------- /reply/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphql import ResolveInfo, GraphQLError 3 | from graphql_jwt.decorators import login_required 4 | 5 | from reply.form import UpdateBaseReplyForm, ToggleReplyVoteForm, CreateCommentReplyForm 6 | from reply.models import BaseReply, ReplyVote 7 | 8 | 9 | class UpdateBaseReply(graphene.Mutation): 10 | class Arguments: 11 | pk = graphene.ID(required=True) 12 | content = graphene.String(required=True) 13 | 14 | state = graphene.Boolean() 15 | 16 | def mutate(self: None, info: ResolveInfo, **kwargs): 17 | update_article_comment = UpdateBaseReplyForm(kwargs) 18 | if update_article_comment.is_valid(): 19 | values = update_article_comment.cleaned_data 20 | usr = info.context.user 21 | comment = BaseReply.objects.get(pk=values.get('pk')) 22 | if not usr.has_perm('reply.change_basereply') and usr != comment.author: 23 | raise PermissionError('Permission Denied.') 24 | comment.content = values.get('content') 25 | comment.save() 26 | return UpdateBaseReply(state=True) 27 | else: 28 | raise GraphQLError(update_article_comment.errors.as_json()) 29 | 30 | 31 | class CreateCommentReply(graphene.Mutation): 32 | class Arguments: 33 | parent = graphene.ID(required=True) 34 | content = graphene.String(required=True) 35 | 36 | state = graphene.Boolean() 37 | 38 | @login_required 39 | def mutate(self: None, info: ResolveInfo, **kwargs): 40 | create_comment_reply = CreateCommentReplyForm(kwargs) 41 | if create_comment_reply.is_valid(): 42 | values = create_comment_reply.cleaned_data 43 | usr = info.context.user 44 | parent = BaseReply.objects.get(pk=values.get('parent')) 45 | BaseReply.objects.create( 46 | content=values.get('content'), 47 | reply=parent, 48 | ancestor=parent.ancestor if parent.ancestor else parent, 49 | author=usr 50 | ) 51 | return CreateCommentReply(state=True) 52 | else: 53 | raise GraphQLError(create_comment_reply.errors.as_json()) 54 | 55 | 56 | class ToggleReplyVote(graphene.Mutation): 57 | class Arguments: 58 | pk = graphene.ID(required=True) 59 | 60 | state = graphene.Boolean() 61 | 62 | @login_required 63 | def mutate(self: None, info: ResolveInfo, **kwargs): 64 | toggle_reply_vote = ToggleReplyVoteForm(kwargs) 65 | if toggle_reply_vote.is_valid(): 66 | values = toggle_reply_vote.cleaned_data 67 | reply = BaseReply.objects.get(pk=values.get('pk')) 68 | vote, state = ReplyVote.objects.get_or_create(reply=reply, record_user=info.context.user) 69 | vote.attitude = False if vote.attitude else True 70 | vote.save() 71 | reply.vote = ReplyVote.objects.filter(reply=reply, attitude=True).count() 72 | reply.save() 73 | return ToggleReplyVote(state=True) 74 | else: 75 | raise GraphQLError(toggle_reply_vote.errors.as_json()) 76 | 77 | 78 | class Mutation(graphene.AbstractType): 79 | update_base_reply = UpdateBaseReply.Field() 80 | create_comment_reply = CreateCommentReply.Field() 81 | toggle_reply_vote = ToggleReplyVote.Field() 82 | -------------------------------------------------------------------------------- /reply/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.core.paginator import Paginator 3 | from graphql import ResolveInfo 4 | 5 | from reply.constant import REPLY_COMMENT_PER_PAGE_COUNT 6 | from reply.models import BaseReply 7 | from reply.type import AbstractBaseReplyType 8 | 9 | 10 | class Query(object): 11 | comment_reply_list = graphene.List(AbstractBaseReplyType, pk=graphene.ID(), page=graphene.Int()) 12 | 13 | def resolve_comment_reply_list(self: None, info: ResolveInfo, pk: int, page: int): 14 | return Paginator( 15 | BaseReply.objects.filter(ancestor=BaseReply.objects.get(pk=pk), disable=False).order_by('-vote'), 16 | REPLY_COMMENT_PER_PAGE_COUNT).get_page(page) 17 | -------------------------------------------------------------------------------- /reply/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from graphene_django.types import DjangoObjectType 4 | from graphql_jwt.decorators import login_required 5 | from user.schema import UserType 6 | 7 | from reply.models import AbstractReply, AbstractReplyVote, Attitude 8 | 9 | 10 | class AbstractReplyType(DjangoObjectType): 11 | pk = graphene.ID() 12 | content = graphene.String() 13 | user = graphene.Field(UserType) 14 | vote = graphene.Int() 15 | self_attitude = graphene.String() 16 | discussion = graphene.List(AbstractReplyType) 17 | disable = graphene.Boolean() 18 | 19 | def resolve_pk(self, info, *args, **kwargs): 20 | return self.pk 21 | 22 | def resolve_content(self, info, *args, **kwargs): 23 | privilege = info.context.user.has_perm('AbstractReply.view') 24 | if self.disable and not privilege: 25 | return '' 26 | return self.content 27 | 28 | def resolve_user(self, info, *args, **kwargs): 29 | return self.user 30 | 31 | def resolve_vote(self, info, *args, **kwargs): 32 | return AbstractReplyVote.objects.filter(reply=self, 33 | attitude=Attitude.agree).count() - AbstractReply.objects.filter( 34 | reply=self, attitude=Attitude.disagree).count() 35 | 36 | def resolve_self_attitude(self, info, *args, **kwargs): 37 | user = info.context.user 38 | if not user.is_authenticated: 39 | return Attitude.neutral 40 | vote = get_object_or_None(AbstractReplyVote, reply=self, record_user=user) 41 | if vote is None: 42 | return Attitude.neutral 43 | return vote.attitude 44 | 45 | def resolve_discussion(self, info, *args, **kwargs): 46 | return list(AbstractReply.objects.filter(ancestor=self.pk)) 47 | 48 | def resolve_disable(self, info, *args, **kwargs): 49 | return self.disable 50 | 51 | 52 | class UpdateAbstractReplyVote(graphene.Mutation): 53 | class Arguments: 54 | attitude = graphene.Boolean(required=True) 55 | reply_pk = graphene.ID(required=True) 56 | 57 | result = graphene.String() 58 | 59 | @login_required 60 | def mutate(self, info, reply_pk, attitude): 61 | reply = AbstractReply.objects.get(pk=reply_pk) 62 | node, created = AbstractReplyVote.objects.get_or_create( 63 | user=info.context.user, 64 | discussion=i 65 | ) 66 | attitude = Attitude.agree if attitude else Attitude.disagree 67 | node.vote = attitude if created or attitude != node.vote else Attitude.neutral 68 | node.save() 69 | return UpdateReplyVote(result=attitude) 70 | 71 | 72 | class CreateAbstractReply(graphene.Mutation): 73 | class Arguments: 74 | parent = graphene.ID() 75 | content = graphene.String() 76 | 77 | state = graphene.Boolean() 78 | 79 | @login_required 80 | def mutate(self, info, *args, **kwargs): 81 | from reply.form import AbstractReplyForm 82 | reply_form = AbstractReplyForm(**kwargs) 83 | if reply_form.is_valid(): 84 | values = reply_form.cleaned_data 85 | parent = AbstractReply.objects.get(pk=values['parent']) 86 | AbstractReply( 87 | user=info.context.user, 88 | content=values['content'], 89 | parent=parent, 90 | ancestor=parent.ancestor if parent.ancestor else parent, 91 | ).save() 92 | return CreateAbstractReply(state=True) 93 | else: 94 | raise RuntimeError(reply_form.errors.as_json()) 95 | -------------------------------------------------------------------------------- /reply/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /reply/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from graphql import ResolveInfo 4 | 5 | from reply.models import BaseReply, ReplyVote 6 | from user.type import UserType 7 | 8 | 9 | class AbstractBaseReplyType(graphene.ObjectType): 10 | pk = graphene.ID() 11 | content = graphene.String() 12 | author = graphene.Field(UserType) 13 | create_time = graphene.DateTime() 14 | last_update_time = graphene.DateTime() 15 | vote = graphene.Int() 16 | self_attitude = graphene.Boolean() 17 | total_reply_number = graphene.Int() 18 | 19 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID(): 20 | return self.pk 21 | 22 | def resolve_content(self, info: ResolveInfo) -> graphene.String(): 23 | return self.content 24 | 25 | def resolve_author(self, info: ResolveInfo) -> UserType: 26 | return self.author 27 | 28 | def resolve_create_time(self, info: ResolveInfo) -> graphene.DateTime(): 29 | return self.create_time 30 | 31 | def resolve_last_update_time(self, info: ResolveInfo) -> graphene.DateTime(): 32 | return self.last_update_time 33 | 34 | def resolve_vote(self, info: ResolveInfo) -> graphene.Int(): 35 | return self.vote 36 | 37 | def resolve_self_attitude(self, info: ResolveInfo) -> graphene.Boolean(): 38 | usr = info.context.user 39 | if not usr.is_authenticated: 40 | return False 41 | vote = get_object_or_None(ReplyVote, reply=self, record_user=usr) 42 | return vote.attitude if vote else False 43 | 44 | def resolve_total_reply_number(self, info: ResolveInfo) -> graphene.Int(): 45 | return BaseReply.objects.filter(ancestor=self).count() 46 | -------------------------------------------------------------------------------- /requirements/requirements.txt: -------------------------------------------------------------------------------- 1 | aioredis==1.2.0 2 | amqp==2.5.0 3 | aniso8601==6.0.0 4 | asgiref==3.1.2 5 | asn1crypto==0.24.0 6 | async-timeout==3.0.1 7 | attrs==19.1.0 8 | autobahn==19.6.2 9 | Automat==0.7.0 10 | backcall==0.1.0 11 | billiard==3.6.0.0 12 | celery==4.3.0 13 | cffi==1.12.3 14 | channels==2.2.0 15 | channels-redis==2.4.0 16 | constantly==15.1.0 17 | coverage==4.5.3 18 | cryptography==2.7 19 | daphne==2.3.0 20 | decorator==4.4.0 21 | Django==2.2.10 22 | django-annoying==0.10.4 23 | django-cors-headers==3.0.2 24 | django-environ==0.4.5 25 | django-extensions==2.1.9 26 | django-graphql-jwt==0.2.1 27 | django-gravatar2==1.4.2 28 | django-js-asset==1.2.2 29 | django-uuslug==1.1.8 30 | graphene==2.1.6 31 | graphene-django==2.3.2 32 | graphene-file-upload==1.2.2 33 | graphql-core==2.2 34 | graphql-relay==0.4.5 35 | gunicorn==19.9.0 36 | hiredis==1.0.0 37 | hyperlink==19.0.0 38 | idna==2.8 39 | incremental==17.5.0 40 | ipython==7.5.0 41 | ipython-genutils==0.2.0 42 | jedi==0.14.0 43 | kombu==4.6.3 44 | msgpack==0.6.1 45 | mysqlclient==1.4.2.post1 46 | packaging==19.0 47 | parso==0.5.0 48 | pexpect==4.7.0 49 | pickleshare==0.7.5 50 | Pillow==6.2.0 51 | pip-review==1.0 52 | promise==2.2.1 53 | prompt-toolkit==2.0.9 54 | ptyprocess==0.6.0 55 | pycparser==2.19 56 | Pygments==2.4.2 57 | PyHamcrest==1.9.0 58 | pyhumps==1.2.2 59 | PyJWT==1.7.1 60 | pyparsing==2.4.0 61 | python-slugify==3.0.2 62 | pytz==2019.1 63 | Rx==1.6.1 64 | singledispatch==3.4.0.3 65 | six==1.12.0 66 | sqlparse==0.3.0 67 | text-unidecode==1.2 68 | traitlets==4.3.2 69 | Twisted==19.7.0 70 | txaio==18.8.1 71 | typing==3.7.4 72 | Unidecode==1.1.1 73 | vine==1.3.0 74 | wcwidth==0.1.7 75 | zope.interface==4.6.0 76 | -------------------------------------------------------------------------------- /run_worker.sh: -------------------------------------------------------------------------------- 1 | celery -A Lutece worker -l info -Q result -c 1 2 | -------------------------------------------------------------------------------- /sample/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/sample/__init__.py -------------------------------------------------------------------------------- /sample/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SampleConfig(AppConfig): 5 | name = 'sample' 6 | -------------------------------------------------------------------------------- /sample/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of sample input 2 | MAX_INPUT_LENGTH = 512 3 | 4 | # The max length limitation of sample output 5 | MAX_OUTPUT_LENGTH = 512 6 | -------------------------------------------------------------------------------- /sample/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from sample.constant import MAX_INPUT_LENGTH, MAX_OUTPUT_LENGTH 4 | 5 | 6 | class AbstractSample(models.Model): 7 | input_content = models.CharField(max_length=MAX_INPUT_LENGTH, blank=True) 8 | output_content = models.CharField(max_length=MAX_OUTPUT_LENGTH, blank=True) 9 | 10 | def __str__(self): 11 | return str(self.pk) 12 | -------------------------------------------------------------------------------- /sample/schema.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class AbstractSampleType(DjangoObjectType): 5 | pk = graphene.ID() 6 | input_content = graphene.String() 7 | output_content = graphene.String() 8 | 9 | def resolve_pk(self, info, *args, **kwargs): 10 | return self.pk 11 | 12 | def resolve_input_content(self, info, *args, **kwargs): 13 | return self.input_content 14 | 15 | def resolve_output_content(self, info, *args, **kwargs): 16 | return self.output_content 17 | -------------------------------------------------------------------------------- /sample/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /submission/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/submission/__init__.py -------------------------------------------------------------------------------- /submission/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class SubmissionConfig(AppConfig): 5 | name = 'submission' 6 | 7 | def ready(self): 8 | pass 9 | -------------------------------------------------------------------------------- /submission/attachinfo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/submission/attachinfo/__init__.py -------------------------------------------------------------------------------- /submission/attachinfo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class AbstractAttachInfo(models.Model): 5 | class Meta: 6 | abstract = True 7 | 8 | visibility = models.BooleanField(default=False) 9 | -------------------------------------------------------------------------------- /submission/basesubmission/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/submission/basesubmission/__init__.py -------------------------------------------------------------------------------- /submission/basesubmission/constant.py: -------------------------------------------------------------------------------- 1 | # The max length of language 2 | MAX_LANGUAGE_LENGTH = 32 3 | -------------------------------------------------------------------------------- /submission/basesubmission/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.utils import timezone 3 | 4 | from judge.language import Language 5 | from judge.models import JudgeResult 6 | from problem.models import Problem 7 | from submission.basesubmission.constant import MAX_LANGUAGE_LENGTH 8 | from user.models import User 9 | 10 | 11 | class AbstractSubmission(models.Model): 12 | class Meta: 13 | abstract = True 14 | 15 | result = models.OneToOneField(JudgeResult, on_delete=models.CASCADE) 16 | problem = models.ForeignKey(Problem, on_delete=models.SET_NULL, null=True) 17 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 18 | create_time = models.DateTimeField(default=timezone.now) 19 | _language = models.CharField(choices=((each.full, each.full) for each in Language.all()), 20 | max_length=MAX_LANGUAGE_LENGTH, db_index=True) 21 | 22 | @property 23 | def language(self, *args, **kwargs): 24 | return Language.value_of(self._language) 25 | -------------------------------------------------------------------------------- /submission/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of code 2 | MAX_CODE_LENGTH = 65535 3 | 4 | # The limitation of items number in single page 5 | PER_PAGE_COUNT = 15 6 | -------------------------------------------------------------------------------- /submission/consumers.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from channels.db import database_sync_to_async 3 | from channels.generic.websocket import AsyncWebsocketConsumer 4 | from django.contrib.auth.models import AnonymousUser 5 | from graphql_jwt.shortcuts import get_user_by_token 6 | from humps import camelize 7 | from json import dumps 8 | from typing import List 9 | 10 | from contest.models import ContestSubmission, ContestTeamMember 11 | from submission.models import Submission, SubmissionCase 12 | from user.models import User 13 | from utils.function import close_old_connections, pop_property 14 | 15 | 16 | class CaseData: 17 | __slots__ = { 18 | 'result', # type: str 19 | 'time_cost', # type: int 20 | 'memory_cost', # type: int 21 | 'case', # type: int 22 | } 23 | 24 | def __init__(self, *args, **kwargs): 25 | for key, value in kwargs.items(): 26 | if key in self.__slots__: 27 | setattr(self, key, value) 28 | 29 | def serialization(self): 30 | return {camelize(each): getattr(self, each) for each in self.__slots__} 31 | 32 | 33 | class UpdatingData: 34 | __slots__ = { 35 | 'result', # type: str 36 | 'code', # type: str 37 | 'case_number', # type: int 38 | 'submit_time', # type: str 39 | 'language', # type: str 40 | 'compile_info', # type: str 41 | 'error_info', # type: str 42 | 'problem_title', # type: str 43 | 'problem_slug', # type: str 44 | 'submit_user', # type: str 45 | 'case_list', # type: List[UpdatingData] 46 | } 47 | 48 | def filter(self, filter_data: List[str]): 49 | for each in filter_data: 50 | if hasattr(self, each): 51 | setattr(self, each, None) 52 | 53 | def __init__(self, *args, **kwargs): 54 | for key, value in kwargs.items(): 55 | if key in self.__slots__: 56 | setattr(self, key, value) 57 | 58 | def serialization(self): 59 | case_list = 'case_list' 60 | ret = dict() 61 | for each in self.__slots__: 62 | if hasattr(self, each): 63 | val = getattr(self, each) 64 | if val: 65 | ret[each] = val 66 | if hasattr(self, case_list): 67 | ret[case_list] = [each.serialization() for each in getattr(self, case_list)] 68 | return ret 69 | 70 | 71 | class SubmissionDetailConsumer(AsyncWebsocketConsumer): 72 | __slots__ = { 73 | 'submission', # type: Submission 74 | 'group_name', # type: str 75 | 'user', # type: User 76 | } 77 | 78 | async def connect(self): 79 | close_old_connections() 80 | self.submission = Submission.objects.get(pk=self.scope['url_route']['kwargs']['pk']) 81 | self.group_name = f'SubmissionDetail-{self.submission.pk}' 82 | try: 83 | self.user = await database_sync_to_async(get_user_by_token)(token=self.scope['query_string']) 84 | except Exception: 85 | self.user = AnonymousUser() 86 | self.contest_permission = False 87 | if self.submission.submission_type == 1: 88 | sub = ContestSubmission.objects.get(pk=self.submission.pk) 89 | contest = sub.contest 90 | sub_team = sub.team 91 | usr_team_member = get_object_or_None(ContestTeamMember, contest_team__contest=contest, user=self.user) 92 | if usr_team_member and usr_team_member.contest_team == sub_team: 93 | self.contest_permission = True 94 | if not self.user.has_perm('problem.view') and ( 95 | self.submission.problem.disable or self.submission.user.is_staff) and not self.contest_permission: 96 | raise RuntimeError('Permission Denied') 97 | await self.channel_layer.group_add( 98 | self.group_name, 99 | self.channel_name 100 | ) 101 | await self.accept() 102 | await self.init() 103 | if self.submission.result.done: 104 | await self.close() 105 | 106 | async def init(self): 107 | cases = await database_sync_to_async(SubmissionCase.objects.filter)(submission=self.submission) 108 | await self.update_result( 109 | event={ 110 | 'data': UpdatingData( 111 | result=self.submission.result.result.full, 112 | code=self.submission.code, 113 | case_number=self.submission.attach_info.cases_count, 114 | submit_time=self.submission.create_time.strftime("%Y-%m-%d %H:%M:%S"), 115 | language=self.submission.language.full, 116 | compile_info=self.submission.result.compile_info, 117 | error_info=self.submission.result.error_info, 118 | problem_title=self.submission.problem.title, 119 | problem_slug=self.submission.problem.slug, 120 | submit_user=self.submission.user.username, 121 | case_list=[CaseData( 122 | result=each.result.full, 123 | time_cost=each.time_cost, 124 | memory_cost=each.memory_cost, 125 | case=each.case 126 | ) for each in cases] 127 | ).serialization() 128 | } 129 | ) 130 | 131 | async def disconnect(self, close_code): 132 | await self.channel_layer.group_discard( 133 | self.group_name, 134 | self.channel_name 135 | ) 136 | 137 | async def update_result(self, event: dict): 138 | data = event.get('data') 139 | perm = self.submission.user == self.user 140 | privilege = self.user.has_perm('submission.view') 141 | if not (perm or privilege or self.contest_permission): 142 | pop_property(data, ['compile_info', 'code']) 143 | if not privilege: 144 | pop_property(data, ['error_info']) 145 | ret = {camelize(each): data.get(each) for each in data} 146 | await self.send(text_data=dumps(ret)) 147 | -------------------------------------------------------------------------------- /submission/form.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from django import forms 3 | 4 | from judge.language import Language 5 | from problem.models import Problem 6 | from submission.constant import MAX_CODE_LENGTH 7 | 8 | 9 | class SubmitSubmissionForm(forms.Form): 10 | problem_slug = forms.CharField(required=True) 11 | code = forms.CharField(required=True, max_length=MAX_CODE_LENGTH, min_length=1) 12 | language = forms.CharField(required=True) 13 | 14 | def clean(self): 15 | cleaned_data = super().clean() 16 | problemslug = cleaned_data.get('problem_slug') 17 | language = Language.value_of(cleaned_data.get('language')) 18 | prob = get_object_or_None(Problem, slug=problemslug) 19 | if problemslug and not prob: 20 | self.add_error('problemslug', 'Problem not exists.') 21 | if not language: 22 | self.add_error('language', 'Unknown language') 23 | return cleaned_data 24 | -------------------------------------------------------------------------------- /submission/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from judge.case.models import AbstractCase 4 | from submission.attachinfo.models import AbstractAttachInfo 5 | from submission.basesubmission.models import AbstractSubmission 6 | from submission.constant import MAX_CODE_LENGTH 7 | 8 | 9 | class SubmissionAttachInfo(AbstractAttachInfo): 10 | cases_count = models.IntegerField(default=0) 11 | time_cost = models.IntegerField(default=0) 12 | memory_cost = models.IntegerField(default=0) 13 | 14 | 15 | class Submission(AbstractSubmission): 16 | code = models.TextField(max_length=MAX_CODE_LENGTH, blank=True) 17 | attach_info = models.OneToOneField(SubmissionAttachInfo, on_delete=models.CASCADE) 18 | # Used to divide the base class and subclass 19 | submission_type = models.IntegerField(default=0) 20 | 21 | def __str__(self): 22 | return f'' 23 | 24 | def get_judge_field(self): 25 | return { 26 | 'submission_id': self.pk, 27 | 'language': self.language.full, 28 | 'code': self.code, 29 | 'problem': self.problem.pk, 30 | 'time_limit': self.problem.limitation.time_limit, 31 | 'memory_limit': self.problem.limitation.time_limit, 32 | 'checker': self.problem.checker.full 33 | } 34 | 35 | 36 | class SubmissionCase(AbstractCase): 37 | submission = models.ForeignKey(Submission, on_delete=models.SET_NULL, null=True) 38 | -------------------------------------------------------------------------------- /submission/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from annoying.functions import get_object_or_None 3 | from django.conf import settings 4 | from graphql_jwt.decorators import login_required 5 | 6 | from data.service import DataService 7 | from judge.models import JudgeResult as JudgeResultModel 8 | from judge.result import JudgeResult 9 | from judge.tasks import apply_submission 10 | from problem.models import Problem 11 | from submission.form import SubmitSubmissionForm 12 | from submission.models import Submission, SubmissionAttachInfo 13 | 14 | 15 | class SubmitSubmission(graphene.Mutation): 16 | class Arguments: 17 | problem_slug = graphene.String(required=True) 18 | code = graphene.String(required=True) 19 | language = graphene.String(required=True) 20 | 21 | pk = graphene.ID() 22 | 23 | @login_required 24 | def mutate(self, info, *args, **kwargs): 25 | form = SubmitSubmissionForm(kwargs) 26 | if form.is_valid(): 27 | values = form.cleaned_data 28 | problem = get_object_or_None(Problem, slug=values['problem_slug']) 29 | attach_info = SubmissionAttachInfo(cases_count=DataService.get_cases_count(problem.pk)) 30 | result = JudgeResultModel(_result=JudgeResult.PD.full) 31 | sub = Submission( 32 | code=values.get('code'), 33 | _language=values.get('language'), 34 | user=info.context.user, 35 | problem=problem 36 | ) 37 | attach_info.save() 38 | result.save() 39 | sub.attach_info = attach_info 40 | sub.result = result 41 | sub.save() 42 | apply_submission.apply_async(args=(sub.get_judge_field(),), queue=settings.JUDGE.get('task_queue')) 43 | problem.ins_submit_times() 44 | return SubmitSubmission(pk=sub.pk) 45 | else: 46 | raise RuntimeError(form.errors.as_json()) 47 | 48 | 49 | class Mutation(graphene.AbstractType): 50 | submit_submission = SubmitSubmission.Field() 51 | -------------------------------------------------------------------------------- /submission/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.core.paginator import Paginator 3 | from graphql import ResolveInfo 4 | 5 | from submission.constant import PER_PAGE_COUNT 6 | from submission.models import Submission 7 | from submission.type import SubmissionType, SubmissionListType 8 | 9 | 10 | class Query(object): 11 | submission = graphene.Field(SubmissionType, pk=graphene.ID()) 12 | submissionList = graphene.Field(SubmissionListType, page=graphene.Int(), pk=graphene.ID(), user=graphene.String(), 13 | problem=graphene.String(), judge_status=graphene.String(), 14 | language=graphene.String()) 15 | 16 | def resolve_submission(self: None, info: ResolveInfo, pk: int): 17 | return Submission.objects.get(pk=pk) 18 | 19 | def resolve_submissionList(self: None, info: ResolveInfo, page: int, **kwargs): 20 | pk = kwargs.get('pk') 21 | user = kwargs.get('user') 22 | problem = kwargs.get('problem') 23 | judge_status = kwargs.get('judge_status') 24 | language = kwargs.get('language') 25 | status_list = Submission.objects.all().order_by('-pk') 26 | # Only consider base class 27 | status_list = status_list.filter(submission_type=0) 28 | if not info.context.user.has_perm('problem.view'): 29 | status_list = status_list.filter(problem__disable=False) 30 | if not info.context.user.has_perm('user.view') or not info.context.user.has_perm('submission.view'): 31 | status_list = status_list.filter(user__is_staff=False) 32 | if pk: 33 | status_list = status_list.filter(pk=pk) 34 | if user: 35 | status_list = status_list.filter(user__username=user) 36 | if problem: 37 | status_list = status_list.filter(problem__slug=problem) 38 | if judge_status: 39 | status_list = status_list.filter(result___result=judge_status) 40 | if language: 41 | status_list = status_list.filter(_language=language) 42 | paginator = Paginator(status_list, PER_PAGE_COUNT) 43 | return SubmissionListType(max_page=paginator.num_pages, submission_list=paginator.get_page(page)) 44 | -------------------------------------------------------------------------------- /submission/routing.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from submission.consumers import SubmissionDetailConsumer 4 | 5 | websocket_urlpatterns = [ 6 | url(r'ws/status/(?P\d{1,})/$', SubmissionDetailConsumer) 7 | ] 8 | -------------------------------------------------------------------------------- /submission/tests.py: -------------------------------------------------------------------------------- 1 | # Create your tests here. 2 | -------------------------------------------------------------------------------- /submission/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | 4 | from judge.result import JudgeResult 5 | from problem.type import ProblemType 6 | from submission.models import SubmissionAttachInfo, SubmissionCase 7 | from user.type import UserType 8 | from utils.interface import PaginatorList 9 | 10 | 11 | class SubmissionAttachInfoType(DjangoObjectType): 12 | class Meta: 13 | model = SubmissionAttachInfo 14 | only_fields = ('visibility', 'cases_count', 'time_cost', 'memory_cost') 15 | 16 | 17 | class JudgeResultType(graphene.ObjectType): 18 | status = graphene.String() 19 | color = graphene.String() 20 | done = graphene.Boolean() 21 | compile_info = graphene.String() 22 | error_info = graphene.String() 23 | 24 | def resolve_status(self, info, *args, **kwargs): 25 | return self.result.full 26 | 27 | def resolve_color(self, info, *args, **kwargs): 28 | return self.result.color 29 | 30 | def resolve_done(self, info, *args, **kwargs): 31 | return self.done 32 | 33 | def resolve_compile_info(self, info, *args, **kwargs): 34 | usr = info.context.user 35 | if self.user == usr or usr.has_perm('Submission.view'): 36 | return self.compile_info 37 | return '' 38 | 39 | def resolve_error_info(self, info, *args, **kwargs): 40 | if info.context.user.has_perm('Submission.view'): 41 | return self.error_info 42 | return '' 43 | 44 | 45 | class SubmissionCaseType(DjangoObjectType): 46 | class Meta: 47 | model = SubmissionCase 48 | only_fields = ('time_cost', 'memory_cost') 49 | 50 | 51 | class SubmissionCaseTypeList(graphene.ObjectType): 52 | cases_list = graphene.List(SubmissionCaseType) 53 | 54 | 55 | class SubmissionType(graphene.ObjectType): 56 | pk = graphene.ID() 57 | code = graphene.String() 58 | create_time = graphene.DateTime() 59 | user = graphene.Field(UserType) 60 | problem = graphene.Field(ProblemType) 61 | result = graphene.Field(JudgeResultType) 62 | attach_info = graphene.Field(SubmissionAttachInfoType) 63 | cases = graphene.Field(SubmissionCaseTypeList) 64 | language = graphene.String() 65 | failed_case = graphene.Int() 66 | 67 | def resolve_pk(self, info, *args, **kwargs): 68 | return self.pk 69 | 70 | def resolve_code(self, info, *args, **kwargs): 71 | usr = info.context.user 72 | if self.user == usr or usr.has_perm('Submission.view'): 73 | return self.code 74 | return '' 75 | 76 | def resolve_create_time(self, info, *args, **kwargs): 77 | return self.create_time 78 | 79 | def resolve_user(self, info, *args, **kwargs): 80 | return self.user 81 | 82 | def resolve_problem(self, info, *args, **kwargs): 83 | return self.problem 84 | 85 | def resolve_result(self, info, *args, **kwargs): 86 | return self.result 87 | 88 | def resolve_language(self, info, *args, **kwargs): 89 | return self.language.full 90 | 91 | def resolve_failed_case(self, info, *args, **kwargs): 92 | if JudgeResult.is_failed(self.result): 93 | return SubmissionCase.objects.filter(submission=self).count() 94 | return None 95 | 96 | def resolve_cases(self, info, *args, **kwargs): 97 | return list(SubmissionCase.objects.filter(submission=self)) 98 | 99 | 100 | class SubmissionListType(graphene.ObjectType): 101 | class Meta: 102 | interfaces = (PaginatorList,) 103 | 104 | submission_list = graphene.List(SubmissionType) 105 | -------------------------------------------------------------------------------- /submission/util.py: -------------------------------------------------------------------------------- 1 | from django.apps import apps 2 | 3 | from judge.result import JudgeResult 4 | from problem.models import Problem 5 | from submission.consumers import UpdatingData, CaseData 6 | from submission.models import Submission, SubmissionCase 7 | 8 | 9 | def Modify_submission_status(**report): 10 | ''' 11 | Update the status of target submission. 12 | ''' 13 | result = report['result'] 14 | submission = report['submission'] 15 | name = f'SubmissionDetail-{submission}' 16 | send_data = UpdatingData() 17 | sub = Submission.objects.get(pk=submission) 18 | compile_info = report.get('compileerror_msg') 19 | error_info = report.get('judgererror_msg') 20 | if result == JudgeResult.RN.full or result == JudgeResult.PR.full: 21 | sub.result._result = result 22 | sub.result.save() 23 | send_data.result = result 24 | elif error_info: 25 | sub.result.done = True 26 | sub.result.error_info = error_info 27 | sub.result._result = result 28 | sub.result.save() 29 | send_data.result = result 30 | send_data.error_info = error_info 31 | elif compile_info: 32 | sub.result.done = True 33 | sub.result.compile_info = compile_info 34 | sub.result._result = result 35 | sub.result.save() 36 | send_data.result = result 37 | send_data.compile_info = compile_info 38 | else: 39 | complete = report['complete'] 40 | sub = Submission.objects.get(pk=submission) 41 | s = SubmissionCase( 42 | submission=sub, 43 | _result=report.get('result'), 44 | time_cost=report.get('time_cost'), 45 | memory_cost=report.get('memory_cost'), 46 | case=report.get('case'), 47 | ) 48 | s.save() 49 | send_data.case_list = [CaseData( 50 | result=s.result.full, 51 | time_cost=s.time_cost, 52 | memory_cost=s.memory_cost, 53 | case=s.case 54 | )] 55 | sub.attach_info.time_cost = max(sub.attach_info.time_cost, int(s.time_cost)) 56 | sub.attach_info.memory_cost = max(sub.attach_info.memory_cost, int(s.memory_cost)) 57 | sub.attach_info.save() 58 | if complete: 59 | sub.result._result = result 60 | sub.result.done = True 61 | sub.result.save() 62 | if JudgeResult.value_of(result) is JudgeResult.AC: 63 | Problem.objects.get(pk=sub.problem.pk).ins_accept_times() 64 | from user.util import update_user_solve 65 | update_user_solve(sub.user, sub.problem, True if JudgeResult.value_of(result) is JudgeResult.AC else False) 66 | sub.user.refresh_solve() 67 | send_data.result = result 68 | if apps.is_installed("channels"): 69 | from channels.layers import get_channel_layer 70 | from asgiref.sync import async_to_sync 71 | channel_layer = get_channel_layer() 72 | async_to_sync(channel_layer.group_send)( 73 | name, 74 | { 75 | "type": "update_result", 76 | 'data': send_data.serialization() 77 | } 78 | ) 79 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/tests/__init__.py -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from django.test import RequestFactory 2 | from graphene.test import Client 3 | 4 | from Lutece.schema import schema 5 | from user.attachinfo.models import AttachInfo 6 | from user.models import User 7 | 8 | 9 | def create_mock_user(username: str, password: str) -> User: 10 | attach_info = AttachInfo.objects.create() 11 | return User.objects.create( 12 | username=username, 13 | password=password, 14 | attach_info=attach_info 15 | ) 16 | 17 | 18 | def get_test_graphql_client() -> Client: 19 | return Client(schema) 20 | 21 | 22 | def get_query_context() -> RequestFactory: 23 | return RequestFactory() 24 | -------------------------------------------------------------------------------- /user/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/user/__init__.py -------------------------------------------------------------------------------- /user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import User 4 | 5 | admin.site.register(User) 6 | -------------------------------------------------------------------------------- /user/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UserConfig(AppConfig): 5 | name = 'user' 6 | -------------------------------------------------------------------------------- /user/attachinfo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/user/attachinfo/__init__.py -------------------------------------------------------------------------------- /user/attachinfo/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of school 2 | MAX_SCHOOL_LENGTH = 64 3 | 4 | # The max length limitation of company 5 | MAX_COMPANY_LENGTH = 64 6 | 7 | # The max length limitation of location 8 | MAX_LOCATION_LENGTH = 64 9 | 10 | # The max length limitation of about 11 | MAX_ABOUT_LENGTH = 256 12 | 13 | # The default string of about 14 | DEFAULT_ABOUT = '这个人很懒,什么都没有写.' 15 | 16 | # The max length limitation of user gravatar 17 | MAX_GRAVATAR_LENGTH = 128 18 | 19 | # The max length limitation of codeforces username 20 | MAX_CODEFORCESNAME_LENGTH = 32 21 | 22 | # The max length limitation of atcoder username 23 | MAX_ATCODERNAME_LENGTH = 32 24 | 25 | # The max length limitation of studentId 26 | MAX_STUDENTID_LENGTH = 13 27 | -------------------------------------------------------------------------------- /user/attachinfo/form.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | from user.attachinfo.constant import MAX_ABOUT_LENGTH, MAX_COMPANY_LENGTH, MAX_LOCATION_LENGTH, MAX_SCHOOL_LENGTH, \ 4 | MAX_CODEFORCESNAME_LENGTH, MAX_ATCODERNAME_LENGTH, MAX_STUDENTID_LENGTH 5 | 6 | 7 | class AttachInfoForm(forms.Form): 8 | about = forms.CharField(required=False, max_length=MAX_ABOUT_LENGTH) 9 | school = forms.CharField(required=False, max_length=MAX_SCHOOL_LENGTH) 10 | company = forms.CharField(required=False, max_length=MAX_COMPANY_LENGTH) 11 | location = forms.CharField(required=False, max_length=MAX_LOCATION_LENGTH) 12 | # gravatar = forms.CharField( required = False , max_length = MAX_GRAVATAR_LENGTH ) 13 | codeforces = forms.CharField(required=False, max_length=MAX_CODEFORCESNAME_LENGTH) 14 | atcoder = forms.CharField(required=False, max_length=MAX_ATCODERNAME_LENGTH) 15 | studentid = forms.CharField(required=False, max_length=MAX_STUDENTID_LENGTH) 16 | gender = forms.BooleanField(required=False) -------------------------------------------------------------------------------- /user/attachinfo/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from user.attachinfo.constant import MAX_SCHOOL_LENGTH, MAX_COMPANY_LENGTH, MAX_LOCATION_LENGTH, MAX_ABOUT_LENGTH, \ 4 | DEFAULT_ABOUT, MAX_GRAVATAR_LENGTH, MAX_ATCODERNAME_LENGTH, MAX_CODEFORCESNAME_LENGTH, \ 5 | MAX_STUDENTID_LENGTH 6 | 7 | 8 | class AttachInfo(models.Model): 9 | school = models.CharField(max_length=MAX_SCHOOL_LENGTH, blank=True) 10 | company = models.CharField(max_length=MAX_COMPANY_LENGTH, blank=True) 11 | location = models.CharField(max_length=MAX_LOCATION_LENGTH, blank=True) 12 | about = models.CharField(max_length=MAX_ABOUT_LENGTH, blank=True, default=DEFAULT_ABOUT) 13 | gravatar = models.CharField(max_length=MAX_GRAVATAR_LENGTH, blank=True) 14 | codeforces = models.CharField(max_length=MAX_CODEFORCESNAME_LENGTH, blank=True) 15 | atcoder = models.CharField(max_length=MAX_ATCODERNAME_LENGTH, blank=True) 16 | studentid = models.CharField(max_length=MAX_STUDENTID_LENGTH, blank=True) 17 | gender = models.BooleanField(default=True) 18 | -------------------------------------------------------------------------------- /user/attachinfo/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django_gravatar.helpers import get_gravatar_url 3 | from graphene_django.types import DjangoObjectType 4 | from graphql import ResolveInfo 5 | 6 | from user.attachinfo.models import AttachInfo 7 | 8 | 9 | class UserAttachInfoType(DjangoObjectType): 10 | class Meta: 11 | model = AttachInfo 12 | only_fields = ('school', 'company', 'location', 'about', 'codeforces', 'atcoder', 'studentid', 'gender') 13 | 14 | gravatar = graphene.String() 15 | 16 | def resolve_gravatar(self, info: ResolveInfo) -> str: 17 | return get_gravatar_url(self.user.email, size=250) 18 | -------------------------------------------------------------------------------- /user/constant.py: -------------------------------------------------------------------------------- 1 | # The max length limitation of username 2 | MAX_USERNAME_LENGTH = 32 3 | 4 | # The max length limitation of password 5 | MAX_PASSWORD_LENGTH = 32 6 | 7 | # The minimum length limitation of username 8 | MIN_USERNAME_LENGTH = 4 9 | 10 | # The minimum length limitation of password 11 | MIN_PASSWORD_LENGTH = 6 12 | 13 | # The limitation of items number in single page 14 | PER_PAGE_COUNT = 15 15 | -------------------------------------------------------------------------------- /user/form.py: -------------------------------------------------------------------------------- 1 | from annoying.functions import get_object_or_None 2 | from django import forms 3 | from re import compile 4 | 5 | from user.attachinfo.form import AttachInfoForm 6 | from user.constant import MAX_USERNAME_LENGTH, MIN_USERNAME_LENGTH, MAX_PASSWORD_LENGTH, MIN_PASSWORD_LENGTH 7 | from user.models import User 8 | 9 | 10 | class UserLoginForm(forms.Form): 11 | username = forms.CharField(required=True) 12 | password = forms.CharField(required=True) 13 | 14 | def clean(self) -> dict: 15 | cleaned_data = super().clean() 16 | username = cleaned_data.get('username') 17 | password = cleaned_data.get('password') 18 | usr = get_object_or_None(User, username=username) 19 | if username and usr is None: 20 | self.add_error('username', 'Username not exists.') 21 | if password and usr and not usr.check_password(password): 22 | self.add_error('password', 'Password is wrong.') 23 | return cleaned_data 24 | 25 | 26 | class UserSignupForm(AttachInfoForm): 27 | username = forms.CharField(required=True, max_length=MAX_USERNAME_LENGTH, min_length=MIN_USERNAME_LENGTH) 28 | password = forms.CharField(required=True, max_length=MAX_PASSWORD_LENGTH, min_length=MIN_PASSWORD_LENGTH) 29 | email = forms.EmailField(required=True) 30 | 31 | def clean(self) -> dict: 32 | cleaned_data = super().clean() 33 | username = cleaned_data.get('username') 34 | password = cleaned_data.get('password') 35 | email = cleaned_data.get('email') 36 | if username and get_object_or_None(User, username=username) is not None: 37 | self.add_error('username', 'Username already exists.') 38 | if password and compile('[a-zA-Z]').search(password) is None: 39 | self.add_error('password', 'Password should contain at least one lowercase or uppercase letter.') 40 | if email and get_object_or_None(User, email=email) is not None: 41 | self.add_error('email', 'Email already exists.') 42 | return cleaned_data 43 | 44 | 45 | class UserAttachInfoUpdateForm(AttachInfoForm): 46 | 47 | def clean(self) -> dict: 48 | return super().clean() 49 | -------------------------------------------------------------------------------- /user/jwt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/user/jwt/__init__.py -------------------------------------------------------------------------------- /user/jwt/decode.py: -------------------------------------------------------------------------------- 1 | import jwt 2 | from graphql_jwt.settings import jwt_settings 3 | from graphql_jwt.utils import get_user_by_payload 4 | 5 | 6 | def decode_handler(token, context=None): 7 | payload = jwt.decode( 8 | token, 9 | jwt_settings.JWT_SECRET_KEY, 10 | jwt_settings.JWT_VERIFY, 11 | options={ 12 | 'verify_exp': jwt_settings.JWT_VERIFY_EXPIRATION, 13 | }, 14 | leeway=jwt_settings.JWT_LEEWAY, 15 | audience=jwt_settings.JWT_AUDIENCE, 16 | issuer=jwt_settings.JWT_ISSUER, 17 | algorithms=[jwt_settings.JWT_ALGORITHM]) 18 | user = get_user_by_payload(payload) 19 | if user is not None: 20 | if 'password' not in payload or payload['password'] != user.password[-8:]: 21 | raise Exception('Password has changed') 22 | return payload 23 | -------------------------------------------------------------------------------- /user/jwt/payload.py: -------------------------------------------------------------------------------- 1 | from graphql_jwt.utils import jwt_payload 2 | 3 | 4 | def payload_handler(user, context=None): 5 | payload = jwt_payload(user) 6 | payload['password'] = user.password[-8:] 7 | return payload 8 | -------------------------------------------------------------------------------- /user/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | from django.db import models 3 | 4 | from problem.models import Problem 5 | from user.attachinfo.models import AttachInfo 6 | 7 | 8 | class User(AbstractUser): 9 | attach_info = models.OneToOneField(AttachInfo, on_delete=models.CASCADE) 10 | solved = models.IntegerField(default=0) 11 | tried = models.IntegerField(default=0) 12 | 13 | def __str__(self) -> str: 14 | return f'' 15 | 16 | def refresh_solve(self): 17 | self.tried = Solve.objects.filter(user=self).count() 18 | self.solved = Solve.objects.filter(user=self, status=True).count() 19 | self.save() 20 | 21 | 22 | class Solve(models.Model): 23 | user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) 24 | problem = models.ForeignKey(Problem, on_delete=models.SET_NULL, null=True) 25 | status = models.BooleanField(default=False) 26 | -------------------------------------------------------------------------------- /user/mutation.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.contrib.auth.models import update_last_login 3 | from graphene.types.generic import GenericScalar 4 | from graphql import ResolveInfo 5 | from graphql_jwt.decorators import login_required 6 | from graphql_jwt.mixins import RefreshMixin, JSONWebTokenMixin 7 | from graphql_jwt.shortcuts import get_token, get_payload, get_user_by_payload 8 | from graphql_jwt import Refresh 9 | 10 | from user.attachinfo.models import AttachInfo 11 | from user.form import UserLoginForm, UserSignupForm, UserAttachInfoUpdateForm 12 | from user.models import User 13 | from user.type import UserType 14 | from utils.function import assign 15 | 16 | 17 | class UserLogin(graphene.Mutation): 18 | class Arguments: 19 | username = graphene.String(required=True) 20 | password = graphene.String(required=True) 21 | 22 | token = graphene.String() 23 | payload = GenericScalar() 24 | permission = GenericScalar() 25 | user = graphene.Field(UserType) 26 | 27 | def mutate(self, info: ResolveInfo, **kwargs): 28 | login_form = UserLoginForm(kwargs) 29 | if login_form.is_valid(): 30 | values = login_form.cleaned_data 31 | usr = User.objects.get(username=values.get('username')) 32 | token = get_token(usr) 33 | payload = get_payload(token, info.context) 34 | update_last_login(None, usr) 35 | return UserLogin(payload=payload, token=token, permission=list(usr.get_all_permissions()), user=usr) 36 | else: 37 | raise RuntimeError(login_form.errors.as_json()) 38 | 39 | class UserTokenRefresh(Refresh): 40 | permission = GenericScalar() 41 | user = graphene.Field(UserType) 42 | 43 | @classmethod 44 | def mutate(cls, *arg, **kwargs): 45 | result = cls.refresh(*arg, **kwargs) 46 | user = get_user_by_payload(result.payload) 47 | result.user = user 48 | result.permission = list(user.get_all_permissions()) 49 | return result 50 | 51 | 52 | class UserRegister(graphene.Mutation): 53 | class Arguments: 54 | username = graphene.String(required=True) 55 | password = graphene.String(required=True) 56 | email = graphene.String(required=True) 57 | school = graphene.String() 58 | company = graphene.String() 59 | location = graphene.String() 60 | about = graphene.String() 61 | codeforces = graphene.String() 62 | atcoder = graphene.String() 63 | studentid = graphene.String() 64 | gender = graphene.Boolean() 65 | 66 | 67 | token = graphene.String() 68 | payload = GenericScalar() 69 | permission = GenericScalar() 70 | user = graphene.Field(UserType) 71 | 72 | def mutate(self, info: ResolveInfo, **kwargs): 73 | signup_form = UserSignupForm(kwargs) 74 | if signup_form.is_valid(): 75 | values = signup_form.cleaned_data 76 | usr = User() 77 | attach_info = AttachInfo() 78 | assign(usr, **values) 79 | assign(attach_info, **values) 80 | usr.set_password(usr.password) 81 | attach_info.save() 82 | usr.attach_info = attach_info 83 | usr.save() 84 | token = get_token(usr) 85 | payload = get_payload(token, info.context) 86 | return UserRegister(payload=payload, token=token, permission=list(usr.get_all_permissions()), user=usr) 87 | else: 88 | raise RuntimeError(signup_form.errors.as_json()) 89 | 90 | 91 | class UserAttachInfoUpdate(graphene.Mutation): 92 | class Arguments: 93 | about = graphene.String(required=True) 94 | school = graphene.String(required=True) 95 | company = graphene.String(required=True) 96 | location = graphene.String(required=True) 97 | # gravatar = graphene.String( required = True ) 98 | codeforces = graphene.String(required=True) 99 | atcoder = graphene.String(required=True) 100 | studentid = graphene.String(required=True) 101 | gender = graphene.Boolean(required=True) 102 | 103 | state = graphene.Boolean() 104 | 105 | @login_required 106 | def mutate(self, info: ResolveInfo, **kwargs): 107 | update_form = UserAttachInfoUpdateForm(kwargs) 108 | if update_form.is_valid(): 109 | values = update_form.cleaned_data 110 | usr = info.context.user 111 | assign(usr.attach_info, **values) 112 | usr.attach_info.save() 113 | return UserAttachInfoUpdate(state=True) 114 | else: 115 | raise RuntimeError(update_form.errors.as_json()) 116 | 117 | 118 | class Mutation(graphene.AbstractType): 119 | user_register = UserRegister.Field() 120 | user_login = UserLogin.Field() 121 | user_token_refresh = UserTokenRefresh.Field() 122 | user_attach_info_update = UserAttachInfoUpdate.Field() 123 | -------------------------------------------------------------------------------- /user/query.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from django.core.paginator import Paginator 3 | from graphql import ResolveInfo 4 | 5 | from user.constant import PER_PAGE_COUNT 6 | from user.models import User 7 | from user.type import UserType, UserListType 8 | 9 | 10 | class Query(object): 11 | user = graphene.Field(UserType, username=graphene.String()) 12 | user_list = graphene.Field(UserListType, filter=graphene.String(), page=graphene.Int()) 13 | user_search = graphene.Field(UserListType, filter=graphene.String()) 14 | 15 | def resolve_user(self: None, info: ResolveInfo, username) -> User: 16 | return User.objects.get(username=username) 17 | 18 | def resolve_user_list(self: None, info: ResolveInfo, page: int, filter: str) -> UserListType: 19 | request_usr = info.context.user 20 | user_list = User.objects.all().order_by('-solved') 21 | if not request_usr.has_perm('user.view'): 22 | user_list = user_list.filter(is_active=True, is_staff=False) 23 | if filter: 24 | user_list = user_list.filter(username__icontains=filter) 25 | paginator = Paginator(user_list, PER_PAGE_COUNT) 26 | return UserListType(max_page=paginator.num_pages, user_list=paginator.get_page(page)) 27 | 28 | ''' 29 | Search the matching user of the specific filter. 30 | Nothing would return if there is no filter(to avoid the empty filter situation). 31 | ''' 32 | def resolve_user_search(self: None, info: ResolveInfo, filter: str) -> UserListType: 33 | user_list = User.objects.all() 34 | if not info.context.user.has_perm('user.view'): 35 | user_list = user_list.filter(is_staff=False) 36 | if filter: 37 | user_list = user_list.filter(username__icontains=filter) 38 | else: 39 | user_list = [] 40 | return UserListType(max_page=1, user_list=user_list[:5]) 41 | -------------------------------------------------------------------------------- /user/statistics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/user/statistics/__init__.py -------------------------------------------------------------------------------- /user/statistics/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from graphene_django.types import DjangoObjectType 3 | from graphql import ResolveInfo 4 | from typing import List 5 | 6 | from judge.result import JudgeResult 7 | from submission.models import Submission 8 | from user.models import Solve, User 9 | 10 | 11 | class UserSolveType(DjangoObjectType): 12 | class Meta: 13 | model = Solve 14 | only_fields = 'status' 15 | 16 | pk = graphene.ID() 17 | slug = graphene.String() 18 | 19 | def resolve_pk(self: Solve, info: ResolveInfo) -> int: 20 | return self.problem.pk 21 | 22 | def resolve_slug(self: Solve, info: ResolveInfo) -> int: 23 | return self.problem.slug 24 | 25 | 26 | class UserSubmissionStatisticsType(graphene.ObjectType): 27 | ac = graphene.Int() 28 | tle = graphene.Int() 29 | ce = graphene.Int() 30 | wa = graphene.Int() 31 | re = graphene.Int() 32 | ole = graphene.Int() 33 | mle = graphene.Int() 34 | ratio = graphene.Float() 35 | solve = graphene.List(UserSolveType) 36 | 37 | __slots__ = ( 38 | 'user' # type: User 39 | ) 40 | 41 | def __init__(self, user, *args, **kwargs): 42 | super().__init__(*args, **kwargs) 43 | self.user = user 44 | 45 | def resolve_ac(self, info: ResolveInfo) -> int: 46 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.AC.full).count() 47 | 48 | def resolve_tle(self, info: ResolveInfo) -> int: 49 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.TLE.full).count() 50 | 51 | def resolve_ce(self, info: ResolveInfo) -> int: 52 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.CE.full).count() 53 | 54 | def resolve_wa(self, info: ResolveInfo) -> int: 55 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.WA.full).count() 56 | 57 | def resolve_re(self, info: ResolveInfo) -> int: 58 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.RE.full).count() 59 | 60 | def resolve_ole(self, info: ResolveInfo) -> int: 61 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.OLE.full).count() 62 | 63 | def resolve_mle(self, info: ResolveInfo) -> int: 64 | return Submission.objects.filter(user=self.user, result___result=JudgeResult.MLE.full).count() 65 | 66 | def resolve_ratio(self, info: ResolveInfo) -> float: 67 | ac = self.resolve_ac(info) 68 | _all = Submission.objects.filter(user=self.user).count() 69 | return ac / _all if _all else 0 70 | 71 | def resolve_solve(self, info: ResolveInfo) -> List: 72 | return list(Solve.objects.filter(user=self.user).order_by('problem__pk')) 73 | -------------------------------------------------------------------------------- /user/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | TEST_USER_USERNAME = '123456a' 4 | TEST_USER_PASSWORD = '66666aaaa' 5 | TEST_USER_EMAIL = '1@q.com' 6 | 7 | 8 | def generate_test_user_form(): 9 | return { 10 | 'username': TEST_USER_USERNAME, 11 | 'password': TEST_USER_PASSWORD, 12 | 'email': TEST_USER_EMAIL 13 | } 14 | 15 | 16 | # TODO(KeShen): Add unit tests for user model 17 | class UserTestCase(TestCase): 18 | pass 19 | -------------------------------------------------------------------------------- /user/type.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | from datetime import datetime, date 3 | from django.db.models import Q 4 | from graphql import ResolveInfo 5 | 6 | from user.attachinfo.type import UserAttachInfoType 7 | from user.models import User 8 | from user.statistics.type import UserSubmissionStatisticsType 9 | from utils.interface import PaginatorList 10 | 11 | 12 | class UserRankType(graphene.ObjectType): 13 | position = graphene.Int() 14 | count = graphene.Int() 15 | solve = graphene.JSONString() 16 | 17 | __slots__ = { 18 | 'user', # type: User 19 | } 20 | 21 | def __init__(self, user: User, *args, **kwargs): 22 | self.user = user 23 | super().__init__(*args, **kwargs) 24 | 25 | def resolve_position(self, info: ResolveInfo) -> int: 26 | return User.objects.filter(is_staff=False).filter( 27 | Q(solved__gt=self.user.solved) | Q(solved__exact=self.user.solved, pk__lt=self.user.pk)).count() + 1 28 | 29 | def resolve_count(self, info: ResolveInfo) -> int: 30 | return User.objects.filter(is_staff=False).count() 31 | 32 | 33 | class UserType(graphene.ObjectType): 34 | pk = graphene.ID() 35 | username = graphene.String() 36 | joined_date = graphene.Date() 37 | last_login_date = graphene.DateTime() 38 | attach_info = graphene.Field(UserAttachInfoType) 39 | solved = graphene.Int() 40 | tried = graphene.Int() 41 | rank = graphene.Field(UserRankType) 42 | statistics = graphene.Field(UserSubmissionStatisticsType) 43 | 44 | def resolve_pk(self, info: ResolveInfo) -> graphene.ID: 45 | return self.pk 46 | 47 | def resolve_username(self, info: ResolveInfo) -> graphene.String: 48 | return self.username 49 | 50 | def resolve_joined_date(self: User, info: ResolveInfo) -> date: 51 | return self.date_joined.date() 52 | 53 | def resolve_last_login_date(self: User, info: ResolveInfo) -> datetime: 54 | return self.last_login or self.date_joined 55 | 56 | def resolve_attach_info(self, info: ResolveInfo) -> graphene.Field(UserAttachInfoType): 57 | return self.attach_info 58 | 59 | def resolve_solved(self, info: ResolveInfo) -> graphene.Int: 60 | return self.solved 61 | 62 | def resolve_tried(self, info: ResolveInfo) -> graphene.Int: 63 | return self.tried 64 | 65 | def resolve_rank(self: User, info: ResolveInfo) -> UserRankType: 66 | return UserRankType(user=self) 67 | 68 | def resolve_statistics(self, info: ResolveInfo) -> graphene.Field(UserSubmissionStatisticsType): 69 | return UserSubmissionStatisticsType(user=self) 70 | 71 | 72 | class UserListType(graphene.ObjectType, interfaces=[PaginatorList]): 73 | user_list = graphene.List(UserType) 74 | -------------------------------------------------------------------------------- /user/util.py: -------------------------------------------------------------------------------- 1 | from problem.models import Problem 2 | from user.models import User, Solve 3 | 4 | 5 | def update_user_solve(usr: User, prob: Problem, status: bool): 6 | solve, created = Solve.objects.get_or_create(user=usr, problem=prob) 7 | if not solve.status and status is True: 8 | solve.status = True 9 | solve.save() 10 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/utils/__init__.py -------------------------------------------------------------------------------- /utils/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class UtilConfig(AppConfig): 5 | name = 'util' 6 | -------------------------------------------------------------------------------- /utils/decorators.py: -------------------------------------------------------------------------------- 1 | class classproperty(property): 2 | def __get__(self, cls, owner): 3 | return classmethod(self.fget).__get__(None, owner)() 4 | -------------------------------------------------------------------------------- /utils/function.py: -------------------------------------------------------------------------------- 1 | from django.db import connections 2 | from typing import Any, List 3 | 4 | 5 | def assign(obj: Any, **kwargs: dict): 6 | for key, value in kwargs.items(): 7 | if hasattr(obj, key): 8 | setattr(obj, key, value) 9 | 10 | 11 | def pop_property(obj: dict, field: List[str]): 12 | for each in field: 13 | if each in obj: 14 | obj.pop(each) 15 | 16 | 17 | def close_old_connections(): 18 | for conn in connections.all(): 19 | conn.close_if_unusable_or_obsolete() 20 | 21 | 22 | def recursive_merge_dicts(d1, d2) -> dict: 23 | if isinstance(d1, dict) and isinstance(d2, dict): 24 | return { 25 | **d1, 26 | **d2, 27 | **{k: recursive_merge_dicts(d1[k], d2[k]) for k in {*d1} & {*d2}} 28 | } 29 | else: 30 | return d2 31 | -------------------------------------------------------------------------------- /utils/interface.py: -------------------------------------------------------------------------------- 1 | import graphene 2 | 3 | 4 | class PaginatorList(graphene.Interface): 5 | max_page = graphene.Int(required=True) 6 | -------------------------------------------------------------------------------- /utils/language.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | 4 | class _meta: 5 | __slots__ = ( 6 | 'full', 7 | 'version', 8 | 'prism', 9 | 'codemirror', 10 | 'info', 11 | '_field' 12 | ) 13 | 14 | def __init__(self, **kw): 15 | for _ in kw: 16 | self.__setattr__(_, kw[_]) 17 | self._field = [x for x in kw] 18 | 19 | def __str__(self): 20 | return self.full 21 | 22 | def __repr__(self): 23 | return str(self.full) 24 | 25 | @property 26 | def attribute(self): 27 | return {x: getattr(self, x) for x in self._field} 28 | 29 | 30 | @unique 31 | class Language(Enum): 32 | GNUCPP = _meta( 33 | full='GNU G++', 34 | version='7.3.0', 35 | prism='language-cpp', 36 | info='GNU G++17', 37 | codemirror='text/x-c++src', 38 | ) 39 | GNUGCC = _meta( 40 | full='GNU GCC', 41 | version='7.3.0', 42 | prism='language-c', 43 | info='GNU GCC 7.3', 44 | codemirror='text/x-csrc', 45 | ) 46 | CLANG = _meta( 47 | full='Clang', 48 | version='6.0.0', 49 | prism='language-cpp', 50 | info='Clang 6.0.0', 51 | codemirror='text/x-c++src', 52 | ) 53 | PYTHON = _meta( 54 | full='Python', 55 | version='3.6.5', 56 | prism='language-python', 57 | info='Python 3.6.5', 58 | codemirror='text/x-python' 59 | ) 60 | JAVA = _meta( 61 | full='Java', 62 | version='10', 63 | prism='language-java', 64 | info='Java 10', 65 | codemirror='text/x-java' 66 | ) 67 | GO = _meta( 68 | full='Go', 69 | version='1.10.2', 70 | prism='language-go', 71 | info='Go 1.10.2', 72 | codemirror='text/x-go' 73 | ) 74 | RUBY = _meta( 75 | full='Ruby', 76 | version='2.5.1', 77 | prism='language-ruby', 78 | info='Ruby 2.5.1', 79 | codemirror='text/x-ruby' 80 | ) 81 | RUST = _meta( 82 | full='Rust', 83 | version='1.26.1', 84 | prism='language-rust', 85 | info='Rust 1.26.1', 86 | codemirror='text/x-rustsrc' 87 | ) 88 | 89 | @classmethod 90 | def get_language(cls, result): 91 | for each in cls: 92 | if each.value.full == result: 93 | return each 94 | return None 95 | -------------------------------------------------------------------------------- /utils/schema.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lutece-awesome/lutece-backend/038d1b316cad6c3d33849ce4a236e9c6248a75c7/utils/schema.py -------------------------------------------------------------------------------- /utils/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | from utils.function import recursive_merge_dicts 4 | 5 | 6 | class MergeDictTest(TestCase): 7 | 8 | def test_merge(self): 9 | a = { 10 | 'a': 1, 11 | 'b': { 12 | 'e': 5, 13 | 'g': 7 14 | }, 15 | 'c': 3, 16 | 'd': { 17 | '1': 'a', 18 | '2': 'b', 19 | '3': 'f' 20 | } 21 | } 22 | b = { 23 | 'a': 2, 24 | 'b': { 25 | 'e': 2, 26 | 'f': 6, 27 | }, 28 | 'd': { 29 | '1': 'a', 30 | '2': { 31 | 'a', 32 | 'b' 33 | } 34 | } 35 | } 36 | assert recursive_merge_dicts(a, b) == { 37 | 'a': 2, 38 | 'b': { 39 | 'e': 2, 40 | 'g': 7, 41 | 'f': 6 42 | }, 43 | 'c': 3, 44 | 'd': { 45 | '1': 'a', 46 | '2': { 47 | 'b', 48 | 'a' 49 | }, 50 | '3': 'f' 51 | } 52 | } 53 | --------------------------------------------------------------------------------