├── .gitignore
├── LICENSE
├── Procfile
├── ProcfileHoncho
├── README.md
├── code_interview
├── __init__.py
├── asgi.py
├── celery.py
├── routing.py
├── settings.py
├── urls.py
└── wsgi.py
├── diagram.png
├── manage.py
├── requirements.txt
└── rooms
├── __init__.py
├── admin.py
├── apps.py
├── consumers.py
├── migrations
├── 0001_initial.py
├── 0002_room_created.py
└── __init__.py
├── models.py
├── serializers.py
├── signals.py
├── static
└── codemirror-init.js
├── tasks.py
├── tests.py
├── views
├── home.py
└── room.py
└── wdigets.py
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | *.sqlite3
3 | *.py[co]
4 | *.mo
5 | *~
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: daphne code_interview.asgi:application --port $PORT --bind 0.0.0.0
2 | channelsworker: python manage.py runworker -v2
3 | celeryhoncho: honcho start -f ProcfileHoncho
--------------------------------------------------------------------------------
/ProcfileHoncho:
--------------------------------------------------------------------------------
1 | celeryworker: celery worker -A code_interview -l info -Q callbacks,celery_beat -c 1
2 | celerybeat: celery -A code_interview beat -l info
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # codeinterview-backend
2 | CodeInterview is a fun self-learning project. You can check it out [here](https://codeinterview.netlify.app/).
3 |
4 |
5 |
6 |
7 |
8 | ## Related repos:
9 |
10 | [codeinterview-frontend](https://github.com/areebbeigh/codeinterview-frontend)
11 |
12 | [codeinterview-sandbox](https://github.com/areebbeigh/codeinterview-sandbox)
13 |
14 |
15 | ## Features:
16 |
17 | - Realtime p2p collaborative code editing.
18 | - P2P video confrencing.
19 | - Code execution in any of the supported languages.
20 |
21 | ## Architecture diagram
22 |
23 | 
24 |
25 | ## Contributing
26 |
27 | Create an issue regarding your suggestion/idea/bug and we can pick it up from there :).
28 |
--------------------------------------------------------------------------------
/code_interview/__init__.py:
--------------------------------------------------------------------------------
1 | from .celery import app as celery_app
2 |
3 | __all__ = ['celery_app']
--------------------------------------------------------------------------------
/code_interview/asgi.py:
--------------------------------------------------------------------------------
1 | """
2 | ASGI entrypoint. Configures Django and then runs the application
3 | defined in the ASGI_APPLICATION setting.
4 | """
5 |
6 | import os
7 | import django
8 | from channels.routing import get_default_application
9 |
10 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "code_interview.settings")
11 | os.environ['ASGI_THREADS'] = '4'
12 | django.setup()
13 | application = get_default_application()
14 |
--------------------------------------------------------------------------------
/code_interview/celery.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery import Celery
4 |
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'code_interview.settings')
6 |
7 | app = Celery('code_interview')
8 | app.config_from_object('django.conf:settings', namespace='CELERY')
9 | # app.autodiscover_tasks()
--------------------------------------------------------------------------------
/code_interview/routing.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from channels.routing import ProtocolTypeRouter, URLRouter
4 |
5 | from rooms.consumers import RoomConsumer
6 |
7 | application = ProtocolTypeRouter({
8 | 'websocket': URLRouter([
9 | path(r'ws/rooms/', RoomConsumer),
10 | ])
11 | })
--------------------------------------------------------------------------------
/code_interview/settings.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery.schedules import crontab
4 |
5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
6 |
7 | SECRET_KEY = 'j%4s9n79np!^nrq3&h4=6a8r2c^ex9s)gg3s(zsx((o@qll2yj'
8 | DEBUG = True
9 | ALLOWED_HOSTS = []
10 |
11 | # Celery config
12 |
13 | CELERY_BROKER_URL = 'pyamqp://'
14 | CELERY_RESULT_BACKEND = 'redis://'
15 |
16 | CELERY_TASK_ROUTES = {
17 | # WARNING: room.tasks still need explicit queue name when chaining.
18 | # see rooms.signals.dispatch_run_task. Help?
19 | 'rooms.tasks.*': {'queue': 'callbacks'},
20 | 'tasks.sandbox.run_user_code': {'queue': 'sandbox'}
21 | }
22 | # Comment out to disable auto-deletion
23 | CELERY_BEAT_SCHEDULE = {
24 | 'delete_rooms': {
25 | 'task': 'rooms.tasks.delete_rooms',
26 | 'schedule': crontab(minute='*/1'),
27 | 'options': {'queue' : 'celery_beat'},
28 | },
29 | }
30 |
31 | # Application definition
32 |
33 | INSTALLED_APPS = [
34 | 'django.contrib.admin',
35 | 'django.contrib.auth',
36 | 'django.contrib.contenttypes',
37 | 'django.contrib.sessions',
38 | 'django.contrib.messages',
39 | 'django.contrib.staticfiles',
40 | 'corsheaders',
41 | 'rest_framework',
42 | 'django_filters',
43 | 'channels',
44 | 'rooms',
45 | ]
46 |
47 | MIDDLEWARE = [
48 | 'django.middleware.security.SecurityMiddleware',
49 | 'django.contrib.sessions.middleware.SessionMiddleware',
50 | 'corsheaders.middleware.CorsMiddleware',
51 | 'django.middleware.common.CommonMiddleware',
52 | 'django.middleware.csrf.CsrfViewMiddleware',
53 | 'django.contrib.auth.middleware.AuthenticationMiddleware',
54 | 'django.contrib.messages.middleware.MessageMiddleware',
55 | 'django.middleware.clickjacking.XFrameOptionsMiddleware',
56 | ]
57 |
58 | ROOT_URLCONF = 'code_interview.urls'
59 |
60 | TEMPLATES = [
61 | {
62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates',
63 | 'DIRS': [
64 | os.path.join(BASE_DIR, 'templates'),
65 | ],
66 | 'APP_DIRS': True,
67 | 'OPTIONS': {
68 | 'context_processors': [
69 | 'django.template.context_processors.debug',
70 | 'django.template.context_processors.request',
71 | 'django.contrib.auth.context_processors.auth',
72 | 'django.contrib.messages.context_processors.messages',
73 | ],
74 | },
75 | },
76 | ]
77 |
78 | WSGI_APPLICATION = 'code_interview.wsgi.application'
79 | ASGI_APPLICATION = "code_interview.routing.application"
80 |
81 | ##### Channels-specific settings
82 |
83 | redis_url = 'redis://'
84 |
85 | # Channel layer definitions
86 | # http://channels.readthedocs.io/en/latest/topics/channel_layers.html
87 | CHANNEL_LAYERS = {
88 | # "default": {
89 | # # This example app uses the Redis channel layer implementation channels_redis
90 | # "BACKEND": "channels_redis.core.RedisChannelLayer",
91 | # "CONFIG": {
92 | # "hosts": [redis_url,],
93 | # },
94 | # },
95 | "default": {
96 | "BACKEND": "channels.layers.InMemoryChannelLayer"
97 | }
98 | }
99 |
100 | CORS_ORIGIN_WHITELIST = [
101 | 'http://localhost:8080' # front-end
102 | ]
103 |
104 | # REST framework
105 | default_renderers = []
106 | if DEBUG:
107 | default_renderers = ['rest_framework.renderers.BrowsableAPIRenderer']
108 |
109 | REST_FRAMEWORK = {
110 | 'DEFAULT_RENDERER_CLASSES': [
111 | 'rest_framework.renderers.JSONRenderer',
112 | ] + default_renderers,
113 | }
114 |
115 | # Database
116 | # https://docs.djangoproject.com/en/2.2/ref/settings/#databases
117 |
118 | DATABASES = {
119 | 'default': {
120 | 'ENGINE': 'django.db.backends.sqlite3',
121 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
122 | }
123 | }
124 |
125 |
126 | # Password validation
127 | # https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
128 |
129 | AUTH_PASSWORD_VALIDATORS = [
130 | {
131 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
132 | },
133 | {
134 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
135 | },
136 | {
137 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
138 | },
139 | {
140 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
141 | },
142 | ]
143 |
144 |
145 | # Internationalization
146 | # https://docs.djangoproject.com/en/2.2/topics/i18n/
147 |
148 | LANGUAGE_CODE = 'en-us'
149 |
150 | TIME_ZONE = 'UTC'
151 |
152 | USE_I18N = True
153 |
154 | USE_L10N = True
155 |
156 | USE_TZ = True
157 |
158 |
159 | # Static files (CSS, JavaScript, Images)
160 | # https://docs.djangoproject.com/en/2.2/howto/static-files/
161 |
162 | STATIC_URL = '/static/'
163 |
--------------------------------------------------------------------------------
/code_interview/urls.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 | from django.urls import path, include
3 | from rest_framework import routers
4 |
5 | from rooms.views.home import HomePageView
6 | from rooms.views.room import RoomViewSet, LanguageViewSet
7 |
8 | router = routers.DefaultRouter()
9 | router.register(r'rooms', RoomViewSet)
10 | router.register(r'languages', LanguageViewSet)
11 |
12 | urlpatterns = [
13 | path('', HomePageView.as_view(), name='home'),
14 | path('api/', include(router.urls)),
15 | path('admin/', admin.site.urls),
16 | ]
17 |
--------------------------------------------------------------------------------
/code_interview/wsgi.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from django.core.wsgi import get_wsgi_application
4 |
5 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'code_interview.settings')
6 |
7 | application = get_wsgi_application()
8 |
--------------------------------------------------------------------------------
/diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/areebbeigh/codeinterview-backend/4d88baa4bf9525e330427910741476ee5f59e5b5/diagram.png
--------------------------------------------------------------------------------
/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | """Django's command-line utility for administrative tasks."""
3 | import os
4 | import sys
5 |
6 |
7 | def main():
8 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'code_interview.settings')
9 | try:
10 | from django.core.management import execute_from_command_line
11 | except ImportError as exc:
12 | raise ImportError(
13 | "Couldn't import Django. Are you sure it's installed and "
14 | "available on your PYTHONPATH environment variable? Did you "
15 | "forget to activate a virtual environment?"
16 | ) from exc
17 | execute_from_command_line(sys.argv)
18 |
19 |
20 | if __name__ == '__main__':
21 | main()
22 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | aioredis==1.3.1
2 | amqp==2.5.2
3 | asgi-redis==1.4.3
4 | asgiref==3.2.7
5 | astroid==2.3.3
6 | async-timeout==3.0.1
7 | attrs==19.3.0
8 | autobahn==20.4.2
9 | Automat==20.2.0
10 | autopep8==1.5.1
11 | billiard==3.6.3.0
12 | celery==4.4.2
13 | cffi==1.14.0
14 | channels==2.4.0
15 | channels-redis==2.4.2
16 | colorama==0.4.3
17 | constantly==15.1.0
18 | cryptography==2.9
19 | daphne==2.5.0
20 | dj-database-url==0.5.0
21 | Django==2.2
22 | django-cors-headers==3.2.1
23 | django-filter==2.2.0
24 | django-webpack-loader==0.7.0
25 | djangorestframework==3.11.0
26 | gunicorn==20.0.4
27 | hiredis==1.0.1
28 | hyperlink==19.0.0
29 | idna==2.9
30 | importlib-metadata==1.6.0
31 | incremental==17.5.0
32 | isort==4.3.21
33 | jsonfield==3.1.0
34 | kombu==4.6.8
35 | lazy-object-proxy==1.4.3
36 | mccabe==0.6.1
37 | msgpack==0.6.2
38 | msgpack-python==0.5.6
39 | psycopg2==2.8.5
40 | pyasn1==0.4.8
41 | pyasn1-modules==0.2.8
42 | pycodestyle==2.5.0
43 | pycparser==2.20
44 | PyHamcrest==2.0.2
45 | pylint==2.4.4
46 | pyOpenSSL==19.1.0
47 | pytz==2019.3
48 | redis==3.5.2
49 | service-identity==18.1.0
50 | six==1.14.0
51 | sqlparse==0.3.1
52 | Twisted==20.3.0
53 | txaio==20.4.1
54 | typed-ast==1.4.1
55 | vine==1.3.0
56 | whitenoise==5.1.0
57 | wrapt==1.11.2
58 | yapf==0.29.0
59 | zipp==3.1.0
60 | zope.interface==5.1.0
61 |
--------------------------------------------------------------------------------
/rooms/__init__.py:
--------------------------------------------------------------------------------
1 | default_app_config = 'rooms.apps.RoomsConfig'
--------------------------------------------------------------------------------
/rooms/admin.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 | from django.contrib import admin
3 |
4 | from rooms.wdigets import CodeEditor
5 | from rooms.models import Room, LogEntry, RunRequest, Language
6 |
7 |
8 | class LogEntryAdmin(admin.ModelAdmin):
9 | list_display = (
10 | '__str__',
11 | 'room_uuid',
12 | 'author_name',
13 | 'type',
14 | 'created',
15 | )
16 | readonly_fields = (
17 | 'room_uuid',
18 | 'author_name',
19 | 'content',
20 | 'created',
21 | )
22 | exclude = ('room', )
23 | ordering = ('-created', )
24 |
25 | def room_uuid(self, obj):
26 | if obj.room is None:
27 | return None
28 | return obj.room.room_id
29 |
30 |
31 | class LanguageAdminForm(forms.ModelForm):
32 | model = Language
33 |
34 | class Meta:
35 | fields = '__all__'
36 | widgets = {
37 | 'template': CodeEditor(),
38 | }
39 |
40 |
41 | class LanguageAdmin(admin.ModelAdmin):
42 | form = LanguageAdminForm
43 | list_display = (
44 | 'name',
45 | 'code',
46 | )
47 |
48 |
49 | class RoomAdmin(admin.ModelAdmin):
50 | list_display = (
51 | 'room_id',
52 | 'participants',
53 | 'created',
54 | )
55 | readonly_fields = (
56 | 'room_id',
57 | 'participants',
58 | 'created',
59 | )
60 | ordering = ('-participants', )
61 |
62 |
63 | class RunRequestAdmin(admin.ModelAdmin):
64 | list_display = (
65 | 'room_uuid',
66 | 'status',
67 | 'exec_time',
68 | 'created',
69 | )
70 | readonly_fields = (
71 | 'room',
72 | 'celery_task_id',
73 | 'status',
74 | 'language',
75 | 'code',
76 | 'stdin',
77 | 'output',
78 | 'error',
79 | 'exec_time',
80 | 'created',
81 | )
82 | order = ('-created', )
83 |
84 | def room_uuid(self, obj):
85 | if obj.room is None:
86 | return None
87 | return obj.room.room_id
88 |
89 |
90 | admin.site.register(Room, RoomAdmin)
91 | admin.site.register(LogEntry, LogEntryAdmin)
92 | admin.site.register(RunRequest, RunRequestAdmin)
93 | admin.site.register(Language, LanguageAdmin)
--------------------------------------------------------------------------------
/rooms/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class RoomsConfig(AppConfig):
5 | name = 'rooms'
6 |
7 | def ready(self):
8 | import rooms.signals
--------------------------------------------------------------------------------
/rooms/consumers.py:
--------------------------------------------------------------------------------
1 | import json
2 | from urllib.parse import parse_qsl
3 | from django.db import transaction
4 | from django.core.serializers.json import DjangoJSONEncoder
5 |
6 | from asgiref.sync import async_to_sync
7 | from colorama import Fore, Back, Style
8 | from channels.generic.websocket import JsonWebsocketConsumer
9 |
10 | from rooms.models import Room, LogEntry, RunRequest, Language
11 |
12 | LogEntryType = LogEntry.EntryType
13 |
14 |
15 | class RoomConsumer(JsonWebsocketConsumer):
16 | # WebSocket event handlers
17 |
18 | def connect(self):
19 | self.room_id = self.scope['url_route']['kwargs']['room_id']
20 | self.room_exists = Room.objects.filter(room_id=self.room_id).exists()
21 | self.group_name = f'room_{self.room_id}'
22 |
23 | if not self.room_exists:
24 | self.close()
25 | return
26 |
27 | self.query = dict(parse_qsl(
28 | self.scope['query_string'].decode('utf-8')))
29 | self.username = self.query['username'].strip()
30 |
31 | async_to_sync(self.channel_layer.group_add)(self.group_name,
32 | self.channel_name)
33 | self.accept()
34 | # send user pre-join room logs
35 | self.get_logs(None)
36 | self.create_log_entry(f'{self.username} joined the room',
37 | LogEntryType.USER_JOIN)
38 | with transaction.atomic():
39 | room = self.room_object_locked()
40 | room.participants += 1
41 | room.save()
42 |
43 | def receive_json(self, content):
44 | command = content.get('command', None)
45 | handler = self.get_handler(command)
46 | if handler:
47 | try:
48 | return handler(content)
49 | except Exception as e:
50 | self.send_json({
51 | 'type': 'event',
52 | 'event': 'error',
53 | 'data': f'[{e.__class__.__name__}] {e}'
54 | })
55 | return
56 | self.send_json({
57 | 'type': 'event',
58 | 'event': 'error',
59 | 'data': 'Command not found.'
60 | })
61 |
62 | def disconnect(self, code):
63 | async_to_sync(self.channel_layer.group_discard)(self.group_name,
64 | self.channel_name)
65 | # This won't be called if the server just crashed or restarted.
66 | # TODO: Should this be in __del__() instead?
67 | if self.room_exists:
68 | self.create_log_entry(f'{self.username} left the room',
69 | LogEntryType.USER_LEAVE)
70 | with transaction.atomic():
71 | room = self.room_object_locked()
72 | room.participants -= 1
73 | room.save()
74 |
75 | # ORM helper methods
76 |
77 | def create_log_entry(self, content, type, announce=True):
78 | entry = LogEntry(room=self.room_object,
79 | type=type,
80 | author_name=self.username,
81 | content=content)
82 | entry.save()
83 | if announce:
84 | async_to_sync(self.channel_layer.group_send)(self.group_name, {
85 | 'type': 'event.log.entry',
86 | 'entry': entry.to_dict(),
87 | })
88 | return entry
89 |
90 | def room_object_locked(self):
91 | return Room.objects.select_for_update().get(room_id=self.room_id)
92 |
93 | @property
94 | def room_object(self):
95 | return Room.objects.get(room_id=self.room_id)
96 |
97 | def create_run_request(self, language, code, stdin, create_entry=True):
98 | try:
99 | language = Language.objects.get(code=language)
100 | except Language.DoesNotExist:
101 | raise Exception('Unsupported language')
102 | request = RunRequest(room=self.room_object,
103 | language=language,
104 | code=code,
105 | stdin=stdin)
106 | request.save()
107 |
108 | if create_entry:
109 | self.create_log_entry(
110 | f'{self.username} made a run request', LogEntryType.USER_RUN)
111 |
112 | # Command handlers
113 |
114 | def get_handler(self, command):
115 | handlers = {
116 | 'get-logs': self.get_logs,
117 | 'send-message': self.send_message,
118 | 'run': self.run,
119 | 'join-call': self.join_call,
120 | 'leave-call': self.leave_call,
121 | }
122 | return handlers.get(command)
123 |
124 | def get_logs(self, data):
125 | entries = [
126 | entry.to_dict() for entry in self.room_object.get_logs()
127 | ]
128 | response = {
129 | 'type': 'event',
130 | 'event': 'logs',
131 | 'data': entries,
132 | }
133 | self.send_json(response)
134 |
135 | def send_message(self, data):
136 | message = data['message'].strip()
137 | self.create_log_entry(message, type=LogEntryType.USER_MSG)
138 |
139 | def run(self, data):
140 | self.create_run_request(data['language'], data['code'], data['stdin'])
141 |
142 | def join_call(self, data):
143 | message = f'{self.username} joined the video call'
144 | self.create_log_entry(message, type=LogEntryType.USER_JOIN_CALL)
145 |
146 | def leave_call(self, data):
147 | message = f'{self.username} left the video call'
148 | self.create_log_entry(message, type=LogEntryType.USER_LEAVE_CALL)
149 |
150 | # Event handlers
151 |
152 | def event_log_entry(self, event):
153 | entry = event['entry']
154 | if isinstance(entry, LogEntry):
155 | entry = entry.to_dict()
156 | self.send_json({
157 | 'type': 'event',
158 | 'event': 'new-entry',
159 | 'data': entry,
160 | })
161 |
162 | # Helper methods
163 |
164 | @classmethod
165 | def encode_json(cls, obj):
166 | return json.dumps(obj, cls=DjangoJSONEncoder)
167 |
--------------------------------------------------------------------------------
/rooms/migrations/0001_initial.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2 on 2020-05-27 20:46
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 | import uuid
6 |
7 |
8 | class Migration(migrations.Migration):
9 |
10 | initial = True
11 |
12 | dependencies = [
13 | ]
14 |
15 | operations = [
16 | migrations.CreateModel(
17 | name='Language',
18 | fields=[
19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
20 | ('name', models.CharField(max_length=50, verbose_name='Display Name')),
21 | ('code', models.CharField(db_index=True, max_length=50, verbose_name='Language code')),
22 | ('template', models.TextField(max_length=500)),
23 | ],
24 | ),
25 | migrations.CreateModel(
26 | name='Room',
27 | fields=[
28 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
29 | ('participants', models.IntegerField(auto_created=True, default=0)),
30 | ('room_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
31 | ],
32 | ),
33 | migrations.CreateModel(
34 | name='RunRequest',
35 | fields=[
36 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
37 | ('celery_task_id', models.CharField(blank=True, max_length=100)),
38 | ('status', models.CharField(choices=[('QD', 'Queued'), ('CP', 'Completed'), ('ER', 'Error')], default='QD', max_length=3)),
39 | ('code', models.TextField()),
40 | ('stdin', models.TextField(blank=True)),
41 | ('output', models.TextField(blank=True)),
42 | ('error', models.TextField(blank=True)),
43 | ('exec_time', models.FloatField(null=True, verbose_name='Execution time')),
44 | ('created', models.DateTimeField(auto_now=True)),
45 | ('language', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, to='rooms.Language')),
46 | ('room', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='rooms.Room')),
47 | ],
48 | ),
49 | migrations.CreateModel(
50 | name='LogEntry',
51 | fields=[
52 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
53 | ('type', models.CharField(choices=[('UJ', 'User Join'), ('UL', 'User Leave'), ('UR', 'User Run'), ('UM', 'User Message'), ('UJC', 'User Join Call'), ('ULC', 'User Leave Call'), ('LM', 'Log Message'), ('CO', 'Code Output')], max_length=3)),
54 | ('author_name', models.CharField(blank=True, max_length=20)),
55 | ('content', models.TextField(editable=False, max_length=500)),
56 | ('created', models.DateTimeField(auto_now=True)),
57 | ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='rooms.Room')),
58 | ],
59 | ),
60 | ]
61 |
--------------------------------------------------------------------------------
/rooms/migrations/0002_room_created.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 2.2 on 2020-06-17 05:20
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ('rooms', '0001_initial'),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name='room',
15 | name='created',
16 | field=models.DateTimeField(auto_now=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/rooms/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/areebbeigh/codeinterview-backend/4d88baa4bf9525e330427910741476ee5f59e5b5/rooms/migrations/__init__.py
--------------------------------------------------------------------------------
/rooms/models.py:
--------------------------------------------------------------------------------
1 | import json
2 | import uuid
3 | import calendar
4 |
5 | from django.db import models
6 | from django.core.serializers.json import DjangoJSONEncoder
7 | from jsonfield import JSONField
8 |
9 |
10 | class LogEntry(models.Model):
11 | class EntryType:
12 | USER_JOIN = 'UJ'
13 | USER_LEAVE = 'UL'
14 | USER_RUN = 'UR'
15 | USER_MSG = 'UM'
16 | USER_JOIN_CALL = 'UJC'
17 | USER_LEAVE_CALL = 'ULC'
18 | LOG_MSG = 'LM'
19 | CODE_OUTPUT = 'CO'
20 |
21 | types = [
22 | (EntryType.USER_JOIN, 'User Join'),
23 | (EntryType.USER_LEAVE, 'User Leave'),
24 | (EntryType.USER_RUN, 'User Run'),
25 | (EntryType.USER_MSG, 'User Message'),
26 | (EntryType.USER_JOIN_CALL, 'User Join Call'),
27 | (EntryType.USER_LEAVE_CALL, 'User Leave Call'),
28 | (EntryType.LOG_MSG, 'Log Message'),
29 | (EntryType.CODE_OUTPUT, 'Code Output'),
30 | ]
31 |
32 | room = models.ForeignKey('Room', on_delete=models.CASCADE)
33 | type = models.CharField(max_length=3, choices=types)
34 | author_name = models.CharField(max_length=20, blank=True)
35 | content = models.TextField(editable=False, max_length=500)
36 | created = models.DateTimeField(auto_now=True)
37 |
38 | def __str__(self):
39 | return f'Entry ({self.id})'
40 |
41 | @property
42 | def timestamp(self):
43 | """UTC timestamp of self.created datetime.datetime field"""
44 | return calendar.timegm(self.created.utctimetuple())
45 |
46 | def to_dict(self):
47 | rv = {
48 | 'timestamp': self.timestamp,
49 | 'type': self.type,
50 | 'author': self.author_name,
51 | }
52 | if self.type == self.EntryType.CODE_OUTPUT:
53 | rv['content'] = json.loads(self.content)
54 | else:
55 | rv['content'] = self.content
56 | return rv
57 |
58 |
59 | class Room(models.Model):
60 | room_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
61 | # Since we're not concerned about user authentication, we'll just store member IDs
62 | # in a JSONField for every room
63 | # metadata = JSONField()
64 | participants = models.IntegerField(default=0, auto_created=True)
65 | created = models.DateTimeField(auto_now=True)
66 |
67 | def __str__(self):
68 | return f'Room ({self.room_id})'
69 |
70 | def get_logs(self):
71 | return LogEntry.objects.filter(room__room_id=self.room_id)
72 |
73 |
74 | class RunRequest(models.Model):
75 | class RequestStatus:
76 | QUEUED = 'QD'
77 | ERROR = 'ER'
78 | COMPLETED = 'CP'
79 |
80 | status_types = [
81 | (RequestStatus.QUEUED, 'Queued'),
82 | (RequestStatus.COMPLETED, 'Completed'),
83 | (RequestStatus.ERROR, 'Error'),
84 | ]
85 | room = models.ForeignKey(Room, null=True, on_delete=models.SET_NULL)
86 | celery_task_id = models.CharField(max_length=100, blank=True)
87 | status = models.CharField(max_length=3,
88 | choices=status_types,
89 | default=RequestStatus.QUEUED)
90 | language = models.ForeignKey('Language', on_delete=models.DO_NOTHING)
91 | code = models.TextField()
92 | stdin = models.TextField(blank=True)
93 | output = models.TextField(blank=True)
94 | error = models.TextField(blank=True)
95 | exec_time = models.FloatField(null=True, verbose_name='Execution time')
96 | created = models.DateTimeField(auto_now=True)
97 |
98 | def __str__(self):
99 | return f'RunRequest ({self.id})'
100 |
101 |
102 | class Language(models.Model):
103 | name = models.CharField(verbose_name='Display Name', max_length=50)
104 | code = models.CharField(verbose_name='Language code',
105 | max_length=50,
106 | db_index=True)
107 | template = models.TextField(max_length=500)
108 |
109 | def __str__(self):
110 | return f'{self.name} ({self.code})'
--------------------------------------------------------------------------------
/rooms/serializers.py:
--------------------------------------------------------------------------------
1 | from rest_framework import serializers
2 |
3 | from rooms.models import Room, Language
4 |
5 |
6 | class RoomSerializer(serializers.HyperlinkedModelSerializer):
7 | class Meta:
8 | model = Room
9 | fields = ['room_id', 'participants']
10 |
11 | def create(self, validated_data):
12 | if validated_data.get('participants'):
13 | del validated_data['participants']
14 | return super(RoomSerializer, self).create(validated_data)
15 |
16 |
17 | class LanguageSerializer(serializers.HyperlinkedModelSerializer):
18 | class Meta:
19 | model = Language
20 | fields = ['name', 'code', 'template']
--------------------------------------------------------------------------------
/rooms/signals.py:
--------------------------------------------------------------------------------
1 | from django.db.models.signals import post_save, pre_save
2 | from django.dispatch import receiver
3 |
4 | from celery import current_app
5 |
6 | from rooms.models import RunRequest
7 | from rooms.tasks import save_run_output
8 |
9 |
10 | @receiver(post_save, sender=RunRequest)
11 | def dispatch_run_task(sender, instance, created, **kwargs):
12 | if created:
13 | res = current_app.send_task(
14 | 'tasks.sandbox.run_user_code',
15 | (instance.language.code, instance.code, instance.stdin),
16 | chain=[
17 | # Without the .set() this request goes to the default queue
18 | # instead of the queue defined in settings. WHY?!
19 | save_run_output.s(instance.id).set(queue='callbacks')
20 | ])
21 | instance.celery_task_id = res.task_id
22 | instance.save()
23 |
--------------------------------------------------------------------------------
/rooms/static/codemirror-init.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | window.onload = function() {
3 | function load(el) {
4 | CodeMirror.fromTextArea(el, {
5 | lineNumbers: true,
6 | autoCloseBrackets: true,
7 | matchBrackets: true,
8 | // theme: 'ayu-dark',
9 | });
10 | }
11 | load(document.querySelector('textarea.code-editor'));
12 | };
13 | })();
14 |
--------------------------------------------------------------------------------
/rooms/tasks.py:
--------------------------------------------------------------------------------
1 | import json
2 | from datetime import datetime, timedelta
3 |
4 | from asgiref.sync import async_to_sync
5 | from channels.layers import get_channel_layer
6 |
7 | from celery import shared_task
8 | from rooms.models import RunRequest, LogEntry, Room
9 |
10 | RequestStatus = RunRequest.RequestStatus
11 | LogEntryType = LogEntry.EntryType
12 |
13 |
14 | @shared_task
15 | def save_run_output(result, request_id):
16 | request = RunRequest.objects.get(id=request_id)
17 |
18 | if result['error']:
19 | request.error = result['error_msg']
20 | request.status = RequestStatus.ERROR
21 | else:
22 | request.status = RequestStatus.COMPLETED
23 |
24 | request.output = result['output']
25 | try:
26 | request.exec_time = float(result['exec_time'])
27 | except ValueError:
28 | pass
29 | except TypeError:
30 | pass
31 | request.save()
32 |
33 | entry = LogEntry(room=request.room,
34 | type=LogEntryType.CODE_OUTPUT,
35 | author_name='',
36 | content=json.dumps({
37 | 'request_id': request.id,
38 | 'error': request.error,
39 | 'status': request.status,
40 | 'output': request.output,
41 | 'exec_time': request.exec_time,
42 | }))
43 | entry.save()
44 |
45 | # announce new log entry to associated room
46 | channel_layer = get_channel_layer()
47 | async_to_sync(channel_layer.group_send)(
48 | f'room_{request.room.room_id}',
49 | {
50 | 'type': 'event.log.entry',
51 | 'entry': entry.to_dict(),
52 | },
53 | )
54 |
55 |
56 | @shared_task
57 | def delete_rooms():
58 | """Deletes all the rooms that are empty and more than 5 minutes old"""
59 | rooms = Room.objects.filter(participants=0)
60 | delta = timedelta(minutes=5)
61 | for r in rooms:
62 | if datetime.now(r.created.tzinfo) - r.created >= delta:
63 | r.delete()
64 |
--------------------------------------------------------------------------------
/rooms/tests.py:
--------------------------------------------------------------------------------
1 | from django.test import TestCase
2 |
3 | # Create your tests here.
4 |
--------------------------------------------------------------------------------
/rooms/views/home.py:
--------------------------------------------------------------------------------
1 | from django.views.generic.base import TemplateView
2 |
3 |
4 | class HomePageView(TemplateView):
5 | template_name = 'home.html'
--------------------------------------------------------------------------------
/rooms/views/room.py:
--------------------------------------------------------------------------------
1 | from urllib.parse import urljoin
2 |
3 | from django.views.generic.base import View
4 | from rest_framework import viewsets
5 | from django_filters.rest_framework import DjangoFilterBackend
6 | from django.conf import settings
7 |
8 | from rooms.models import Room, Language
9 | from rooms.serializers import RoomSerializer, LanguageSerializer
10 |
11 |
12 | class RoomViewSet(viewsets.ModelViewSet):
13 | queryset = Room.objects.all()
14 | serializer_class = RoomSerializer
15 | lookup_field = 'room_id'
16 |
17 | class LanguageViewSet(viewsets.ModelViewSet):
18 | queryset = Language.objects.all()
19 | serializer_class = LanguageSerializer
20 | lookup_field = 'code'
21 | lookup_value_regex = r'[^/]+'
--------------------------------------------------------------------------------
/rooms/wdigets.py:
--------------------------------------------------------------------------------
1 | from django import forms
2 |
3 |
4 | class CodeEditor(forms.Textarea):
5 | def __init__(self, *args, **kwargs):
6 | super(CodeEditor, self).__init__(*args, **kwargs)
7 | self.attrs['class'] = 'code-editor'
8 |
9 | class Media:
10 | css = {
11 | 'all': (
12 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.54.0/codemirror.min.css',
13 | # 'https://codemirror.net/theme/ayu-dark.css',
14 | )
15 | }
16 | js = (
17 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.54.0/codemirror.min.js',
18 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.54.0/addon/edit/closebrackets.min.js',
19 | 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.54.0/addon/edit/matchbrackets.min.js',
20 | '/static/codemirror-init.js',
21 | )
22 |
--------------------------------------------------------------------------------