├── .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 | [](https://www.python.org/downloads/release/python-370/)
3 | [](https://www.djangoproject.com/)
4 | [](https://travis-ci.com/lutece-awesome/lutece-backend)
5 | [](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 |
--------------------------------------------------------------------------------