├── accounts ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0004_alter_user_account_type.py │ ├── 0003_alter_user_problems_solved_and_more.py │ ├── 0002_initial.py │ └── 0001_initial.py ├── tests.py ├── admin.py ├── apps.py ├── models.py ├── forms.py ├── serializers.py └── views.py ├── jobboard ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0003_alter_job_location.py │ ├── 0001_initial.py │ └── 0002_remove_job_job_id_remove_job_short_info_and_more.py ├── tests.py ├── admin.py ├── apps.py ├── models.py ├── serializers.py ├── forms.py └── views.py ├── problemset ├── __init__.py ├── migrations │ ├── __init__.py │ ├── 0002_submission_code.py │ ├── 0005_alter_problem_code.py │ ├── 0004_problem_code.py │ ├── 0006_alter_submission_code.py │ ├── 0003_alter_submission_verdict.py │ ├── 0008_alter_submission_verdict.py │ ├── 0007_problem_editorial_problem_statement.py │ └── 0001_initial.py ├── tests.py ├── apps.py ├── admin.py ├── serializers.py ├── models.py ├── forms.py ├── utils.py ├── tasks.py └── views.py ├── .pgpass.template ├── .gitattributes ├── app ├── __init__.py ├── celery.py ├── jinja2.py ├── asgi.py ├── wsgi.py ├── urls.py └── settings.py ├── static ├── img │ ├── favicon.ico │ ├── dynamic_programming.png │ ├── problemset.svg │ ├── graph_theory.svg │ ├── main_page_card1.svg │ ├── data_structures.svg │ ├── main_page_card3.svg │ ├── strings.svg │ └── math.svg ├── js │ ├── problem_parser.js │ ├── add_problem.js │ └── submission.js └── css │ ├── jobs.css │ ├── home.css │ ├── base.css │ ├── code_display.css │ ├── problems.css │ └── accounts.css ├── readme_screenshots └── screenshot_1.png ├── templates ├── errors │ ├── 404.html │ ├── 403.html │ ├── 500.html │ └── 401.html ├── problemset │ ├── problem_navbar.html │ ├── problem_submissions.html │ ├── submissions_list.html │ ├── editorial.html │ ├── problem.html │ └── submission.html ├── index.html ├── accounts │ ├── login.html │ ├── edit_profile.html │ └── profile.html ├── jobboard │ ├── job.html │ ├── add_job.html │ └── jobs.html └── faq.html ├── requirements.txt ├── .env.template ├── ruff.toml ├── manage.py ├── .github └── FUNDING.yml ├── .gitignore └── README.md /accounts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jobboard/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problemset/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /jobboard/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /problemset/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.pgpass.template: -------------------------------------------------------------------------------- 1 | :::: -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | *.html linguist-detectable=false -------------------------------------------------------------------------------- /accounts/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | from .celery import app as celery_app 2 | 3 | __all__ = ('celery_app',) 4 | -------------------------------------------------------------------------------- /jobboard/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /problemset/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spike1236/WnSOJ/HEAD/static/img/favicon.ico -------------------------------------------------------------------------------- /accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import User 3 | 4 | admin.site.register(User) 5 | -------------------------------------------------------------------------------- /jobboard/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Job 3 | 4 | admin.site.register(Job) 5 | -------------------------------------------------------------------------------- /readme_screenshots/screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spike1236/WnSOJ/HEAD/readme_screenshots/screenshot_1.png -------------------------------------------------------------------------------- /static/img/dynamic_programming.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spike1236/WnSOJ/HEAD/static/img/dynamic_programming.png -------------------------------------------------------------------------------- /accounts/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class AccountsConfig(AppConfig): 5 | default_auto_field = 'django.db.models.BigAutoField' 6 | name = 'accounts' 7 | -------------------------------------------------------------------------------- /jobboard/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class JobboardConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "jobboard" 7 | -------------------------------------------------------------------------------- /problemset/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class ProblemsetConfig(AppConfig): 5 | default_auto_field = "django.db.models.BigAutoField" 6 | name = "problemset" 7 | -------------------------------------------------------------------------------- /problemset/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from .models import Problem, Category, Submission 3 | 4 | admin.site.register(Problem) 5 | admin.site.register(Category) 6 | admin.site.register(Submission) 7 | -------------------------------------------------------------------------------- /templates/errors/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

404 Page not found

4 |
The requested URL was not found on the server.
5 | {% endblock %} -------------------------------------------------------------------------------- /templates/errors/403.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

403 Forbidden

4 |
You don't have the permission to access the requested resource.
5 | {% endblock %} -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Django>=5.1.5 2 | Jinja2>=3.1.5 3 | pillow>=11.1.0 4 | psycopg2-binary>=2.9.10 5 | celery>=5.4.0 6 | redis>=5.2.1 7 | python-dotenv==1.0.1 8 | djangorestframework>=3.15.2 9 | djangorestframework-simplejwt>=5.5.0 10 | drf_spectacular>=0.28.0 -------------------------------------------------------------------------------- /app/celery.py: -------------------------------------------------------------------------------- 1 | import os 2 | from celery import Celery 3 | 4 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 5 | 6 | app = Celery('wnsoj') 7 | 8 | app.config_from_object('django.conf:settings', namespace='CELERY') 9 | app.autodiscover_tasks() 10 | -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | SECRET_KEY= 2 | DEBUG= 3 | CELERY_BROKER_URL= 4 | CELERY_RESULT_BACKEND= 5 | ISOLATE_PATH= 6 | ALLOWED_HOSTS= 7 | NO_ISOLATE= -------------------------------------------------------------------------------- /templates/errors/500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

500 Internal Server Error

4 |
The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application.
5 | {% endblock %} -------------------------------------------------------------------------------- /app/jinja2.py: -------------------------------------------------------------------------------- 1 | from django.templatetags.static import static 2 | from django.urls import reverse 3 | from jinja2 import Environment 4 | 5 | 6 | def environment(**options): 7 | env = Environment(**options) 8 | env.globals.update( 9 | { 10 | "static": static, 11 | "url": reverse, 12 | } 13 | ) 14 | return env 15 | -------------------------------------------------------------------------------- /templates/errors/401.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |

401 Unauthorized

4 |
The server could not verify that you are authorized to access the URL requested.
5 |
You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.
6 | {% endblock %} -------------------------------------------------------------------------------- /app/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for app project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /app/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for app 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/5.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /jobboard/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from accounts.models import User 3 | 4 | 5 | class Job(models.Model): 6 | title = models.CharField(max_length=200) 7 | location = models.CharField(max_length=200, default="Remote") 8 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="jobs") 9 | salary_range = models.JSONField(default=dict) 10 | info = models.TextField(default="Placeholder text") 11 | created_at = models.DateTimeField(auto_now_add=True) 12 | -------------------------------------------------------------------------------- /problemset/migrations/0002_submission_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-12 12:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0001_initial'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='submission', 15 | name='code', 16 | field=models.CharField(default='', max_length=65536), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /problemset/migrations/0005_alter_problem_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-16 16:47 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0004_problem_code'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='problem', 15 | name='code', 16 | field=models.TextField(default='', max_length=65536), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /problemset/migrations/0004_problem_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-16 16:40 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0003_alter_submission_verdict'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AddField( 14 | model_name='problem', 15 | name='code', 16 | field=models.CharField(default='', max_length=65536), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /problemset/migrations/0006_alter_submission_code.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-16 16:49 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0005_alter_problem_code'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='submission', 15 | name='code', 16 | field=models.TextField(default='', max_length=65536), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /accounts/migrations/0004_alter_user_account_type.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-19 06:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0003_alter_user_problems_solved_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='user', 15 | name='account_type', 16 | field=models.IntegerField(default=1), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobboard/migrations/0003_alter_job_location.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-19 06:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('jobboard', '0002_remove_job_job_id_remove_job_short_info_and_more'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='job', 15 | name='location', 16 | field=models.CharField(default='Remote', max_length=200), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /jobboard/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import Job 3 | from accounts.serializers import UserSerializer 4 | 5 | 6 | class JobSerializer(serializers.ModelSerializer): 7 | user = UserSerializer(read_only=True) 8 | 9 | class Meta: 10 | model = Job 11 | fields = [ 12 | "id", 13 | "title", 14 | "location", 15 | "user", 16 | "salary_range", 17 | "info", 18 | "created_at", 19 | ] 20 | read_only_fields = ["user", "created_at"] 21 | -------------------------------------------------------------------------------- /jobboard/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Job 3 | 4 | 5 | class AddJobForm(forms.ModelForm): 6 | salary_range = forms.JSONField( 7 | widget=forms.Textarea(attrs={"class": "form-control"}), required=False 8 | ) 9 | 10 | class Meta: 11 | model = Job 12 | fields = ["title", "location", "salary_range", "info"] 13 | widgets = { 14 | "title": forms.TextInput(attrs={"class": "form-control"}), 15 | "location": forms.TextInput(attrs={"class": "form-control"}), 16 | "info": forms.Textarea(attrs={"class": "form-control"}), 17 | } 18 | -------------------------------------------------------------------------------- /problemset/migrations/0003_alter_submission_verdict.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-12 17:15 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0002_submission_code'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='submission', 15 | name='verdict', 16 | field=models.CharField(choices=[('IQ', 'In queue'), ('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('CE', 'Compilation Error'), ('RE', 'Runtime Error')], default='In queue', max_length=20), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /ruff.toml: -------------------------------------------------------------------------------- 1 | exclude = [ 2 | ".bzr", 3 | ".direnv", 4 | ".eggs", 5 | ".git", 6 | ".git-rewrite", 7 | ".hg", 8 | ".ipynb_checkpoints", 9 | ".mypy_cache", 10 | ".nox", 11 | ".pants.d", 12 | ".pyenv", 13 | ".pytest_cache", 14 | ".pytype", 15 | ".ruff_cache", 16 | ".svn", 17 | ".tox", 18 | ".venv", 19 | ".vscode", 20 | "__pypackages__", 21 | "_build", 22 | "buck-out", 23 | "build", 24 | "dist", 25 | "node_modules", 26 | "site-packages", 27 | "venv", 28 | "migrations", 29 | "app/settings.py", 30 | "app/urls.py" 31 | ] 32 | 33 | line-length = 88 34 | indent-width = 4 35 | 36 | [lint] 37 | select = ["E", "W"] 38 | fixable = ["ALL"] 39 | -------------------------------------------------------------------------------- /problemset/migrations/0008_alter_submission_verdict.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-20 16:59 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('problemset', '0007_problem_editorial_problem_statement'), 10 | ] 11 | 12 | operations = [ 13 | migrations.AlterField( 14 | model_name='submission', 15 | name='verdict', 16 | field=models.CharField(choices=[('IQ', 'In queue'), ('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('CE', 'Compilation Error'), ('RE', 'Runtime Error')], default='IQ', max_length=20), 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /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 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'app.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /problemset/migrations/0007_problem_editorial_problem_statement.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-19 06:22 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('problemset', '0006_alter_submission_code'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='problem', 16 | name='editorial', 17 | field=models.TextField(default=django.utils.timezone.now), 18 | preserve_default=False, 19 | ), 20 | migrations.AddField( 21 | model_name='problem', 22 | name='statement', 23 | field=models.TextField(default=django.utils.timezone.now), 24 | preserve_default=False, 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /accounts/migrations/0003_alter_user_problems_solved_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-12 12:22 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | dependencies = [ 9 | ('accounts', '0002_initial'), 10 | ('problemset', '0002_submission_code'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='user', 16 | name='problems_solved', 17 | field=models.ManyToManyField(blank=True, related_name='users_solved', to='problemset.problem'), 18 | ), 19 | migrations.AlterField( 20 | model_name='user', 21 | name='problems_unsolved', 22 | field=models.ManyToManyField(blank=True, related_name='users_unsolved', to='problemset.problem'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | # open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | # liberapay: # Replace with a single Liberapay username 10 | # issuehunt: # Replace with a single IssueHunt username 11 | # lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | # polar: # Replace with a single Polar username 13 | # buy_me_a_coffee: akram758 # Replace with a single Buy Me a Coffee username 14 | # thanks_dev: # Replace with a single thanks.dev username 15 | custom: ["https://www.paypal.me/akram769"] 16 | -------------------------------------------------------------------------------- /jobboard/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-18 15:12 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Job', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('job_id', models.IntegerField()), 22 | ('title', models.CharField(max_length=200)), 23 | ('short_info', models.TextField()), 24 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='jobs', to=settings.AUTH_USER_MODEL)), 25 | ], 26 | ), 27 | ] 28 | -------------------------------------------------------------------------------- /static/js/problem_parser.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', function() { 2 | marked.setOptions({ 3 | renderer: new marked.Renderer(), 4 | gfm: true, 5 | breaks: true, 6 | sanitize: false, 7 | smartLists: true, 8 | smartypants: false 9 | }); 10 | const problemDescription = document.getElementById('problem-description'); 11 | if (problemDescription) { 12 | const originalContent = problemDescription.innerHTML; 13 | const parsedContent = marked.parse(originalContent); 14 | problemDescription.innerHTML = parsedContent; 15 | renderMathInElement(problemDescription, { 16 | delimiters: [ 17 | {left: '$$', right: '$$', display: true}, 18 | {left: '$', right: '$', display: false}, 19 | // {left: "\\(", right: "\)", display: false}, 20 | // {left: '\[', right: '\]', display: true} 21 | ], 22 | throwOnError: false 23 | }); 24 | } 25 | }); -------------------------------------------------------------------------------- /templates/problemset/problem_navbar.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /accounts/migrations/0002_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-18 15:12 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ('accounts', '0001_initial'), 12 | ('auth', '0012_alter_user_first_name_max_length'), 13 | ('problemset', '0001_initial'), 14 | ] 15 | 16 | operations = [ 17 | migrations.AddField( 18 | model_name='user', 19 | name='problems_solved', 20 | field=models.ManyToManyField(related_name='users_solved', to='problemset.problem'), 21 | ), 22 | migrations.AddField( 23 | model_name='user', 24 | name='problems_unsolved', 25 | field=models.ManyToManyField(related_name='users_unsolved', to='problemset.problem'), 26 | ), 27 | migrations.AddField( 28 | model_name='user', 29 | name='user_permissions', 30 | field=models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions'), 31 | ), 32 | ] 33 | -------------------------------------------------------------------------------- /accounts/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import AbstractUser 3 | import random 4 | from django.conf import settings 5 | import os 6 | 7 | 8 | class User(AbstractUser): 9 | phone_number = models.CharField(max_length=20, blank=True, null=True) 10 | account_type = models.IntegerField(default=1) 11 | icon_id = models.IntegerField(null=True) 12 | problems_solved = models.ManyToManyField( 13 | "problemset.Problem", related_name="users_solved", blank=True 14 | ) 15 | problems_unsolved = models.ManyToManyField( 16 | "problemset.Problem", related_name="users_unsolved", blank=True 17 | ) 18 | 19 | @property 20 | def icon64_url(self): 21 | if self.icon_id < 0: 22 | return os.path.join(settings.MEDIA_URL, "users_icons/icon64/default.png") 23 | return os.path.join( 24 | settings.MEDIA_URL, f"users_icons/icon64/{self.icon_id}.png" 25 | ) 26 | 27 | @property 28 | def icon170_url(self): 29 | if self.icon_id < 0: 30 | return os.path.join(settings.MEDIA_URL, "users_icons/icon170/default.png") 31 | return os.path.join( 32 | settings.MEDIA_URL, f"users_icons/icon170/{self.icon_id}.png" 33 | ) 34 | 35 | def save(self, *args, **kwargs): 36 | super().save(*args, **kwargs) 37 | -------------------------------------------------------------------------------- /jobboard/migrations/0002_remove_job_job_id_remove_job_short_info_and_more.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-03-19 02:19 2 | 3 | import django.utils.timezone 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('jobboard', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='job', 16 | name='job_id', 17 | ), 18 | migrations.RemoveField( 19 | model_name='job', 20 | name='short_info', 21 | ), 22 | migrations.AddField( 23 | model_name='job', 24 | name='created_at', 25 | field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), 26 | preserve_default=False, 27 | ), 28 | migrations.AddField( 29 | model_name='job', 30 | name='info', 31 | field=models.TextField(default='Placeholder text'), 32 | ), 33 | migrations.AddField( 34 | model_name='job', 35 | name='location', 36 | field=models.CharField(default=django.utils.timezone.now, max_length=200), 37 | preserve_default=False, 38 | ), 39 | migrations.AddField( 40 | model_name='job', 41 | name='salary_range', 42 | field=models.JSONField(default=dict), 43 | ), 44 | ] 45 | -------------------------------------------------------------------------------- /problemset/serializers.py: -------------------------------------------------------------------------------- 1 | # problemset/serializers.py 2 | 3 | from rest_framework import serializers 4 | from .models import Category, Problem, Submission 5 | from accounts.serializers import UserSerializer 6 | 7 | 8 | class CategorySerializer(serializers.ModelSerializer): 9 | class Meta: 10 | model = Category 11 | fields = ["id", "short_name", "long_name", "img_url"] 12 | 13 | 14 | class ProblemSerializer(serializers.ModelSerializer): 15 | categories = CategorySerializer(many=True, read_only=True) 16 | users_solved = UserSerializer(many=True, read_only=True) 17 | users_unsolved = UserSerializer(many=True, read_only=True) 18 | 19 | class Meta: 20 | model = Problem 21 | fields = [ 22 | "id", 23 | "title", 24 | "time_limit", 25 | "memory_limit", 26 | "statement", 27 | "editorial", 28 | "categories", 29 | "code", 30 | "users_solved", 31 | "users_unsolved", 32 | ] 33 | 34 | 35 | class SubmissionSerializer(serializers.ModelSerializer): 36 | user = UserSerializer(read_only=True) 37 | problem = ProblemSerializer(read_only=True) 38 | 39 | class Meta: 40 | model = Submission 41 | fields = [ 42 | "id", 43 | "user", 44 | "problem", 45 | "verdict", 46 | "time", 47 | "memory", 48 | "language", 49 | "code", 50 | "send_time", 51 | ] 52 | read_only_fields = ["verdict", "time", "memory", "send_time"] 53 | -------------------------------------------------------------------------------- /problemset/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from accounts.models import User 3 | 4 | 5 | class Category(models.Model): 6 | short_name = models.CharField(max_length=50) 7 | long_name = models.CharField(max_length=100) 8 | img_url = models.CharField(max_length=200) 9 | 10 | def __str__(self): 11 | return self.long_name 12 | 13 | 14 | class Problem(models.Model): 15 | title = models.CharField(max_length=200) 16 | time_limit = models.FloatField() 17 | memory_limit = models.IntegerField() 18 | statement = models.TextField() 19 | editorial = models.TextField() 20 | categories = models.ManyToManyField(Category, related_name="problems") 21 | code = models.TextField(max_length=65536, default="") 22 | 23 | 24 | class Submission(models.Model): 25 | VERDICT_CHOICES = [ 26 | ("IQ", "In queue"), 27 | ("AC", "Accepted"), 28 | ("WA", "Wrong Answer"), 29 | ("TLE", "Time Limit Exceeded"), 30 | ("MLE", "Memory Limit Exceeded"), 31 | ("CE", "Compilation Error"), 32 | ("RE", "Runtime Error"), 33 | ] 34 | user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="submissions") 35 | problem = models.ForeignKey( 36 | Problem, on_delete=models.CASCADE, related_name="submissions" 37 | ) 38 | verdict = models.CharField(max_length=20, choices=VERDICT_CHOICES, default="IQ") 39 | time = models.IntegerField(default=0) 40 | memory = models.IntegerField(default=0) 41 | language = models.CharField(max_length=20) 42 | code = models.TextField(max_length=65536, default="") 43 | send_time = models.DateTimeField(auto_now_add=True) 44 | -------------------------------------------------------------------------------- /accounts/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from django.contrib.auth.forms import UserCreationForm, AuthenticationForm 3 | from django.contrib.auth.forms import PasswordChangeForm 4 | from .models import User 5 | 6 | 7 | class RegisterForm(UserCreationForm): 8 | email = forms.EmailField(required=True) 9 | first_name = forms.CharField(max_length=30, required=False) 10 | last_name = forms.CharField(max_length=30, required=False) 11 | phone_number = forms.CharField(max_length=20, required=False) 12 | icon = forms.ImageField(required=False) 13 | is_business = forms.BooleanField(required=False) 14 | 15 | class Meta: 16 | model = User 17 | fields = [ 18 | "username", 19 | "email", 20 | "password1", 21 | "password2", 22 | "first_name", 23 | "last_name", 24 | "phone_number", 25 | "is_business", 26 | ] 27 | 28 | def __init__(self, *args, **kwargs): 29 | super().__init__(*args, **kwargs) 30 | for field in self.fields.values(): 31 | field.widget.attrs.update({"class": "form-control"}) 32 | 33 | 34 | class LoginForm(AuthenticationForm): 35 | remember = forms.BooleanField(required=False) 36 | 37 | def __init__(self, *args, **kwargs): 38 | super().__init__(*args, **kwargs) 39 | for field in self.fields.values(): 40 | field.widget.attrs.update({"class": "form-control"}) 41 | 42 | 43 | class ChangeIconForm(forms.Form): 44 | icon = forms.ImageField() 45 | 46 | def __init__(self, *args, **kwargs): 47 | super().__init__(*args, **kwargs) 48 | for field in self.fields.values(): 49 | field.widget.attrs.update({"class": "form-control"}) 50 | -------------------------------------------------------------------------------- /static/img/problemset.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/js/add_problem.js: -------------------------------------------------------------------------------- 1 | let solutionCodeArea = document.getElementById("code_area"); 2 | let solutionThemeSelect = document.getElementById("solution_theme_select"); 3 | let solutionEditor; 4 | 5 | // Initialize CodeMirror editor 6 | if (solutionCodeArea) { 7 | solutionEditor = CodeMirror.fromTextArea(solutionCodeArea, { 8 | lineNumbers: true, 9 | styleActiveLine: true, 10 | matchBrackets: true, 11 | tabSize: 4, 12 | indentUnit: 4, 13 | theme: "default", 14 | mode: "text/x-c++src", 15 | lineWrapping: true, 16 | viewportMargin: Infinity, 17 | autoRefresh: true 18 | }); 19 | 20 | solutionEditor.setSize("100%", "auto"); 21 | 22 | // Force refresh when document is fully loaded 23 | setTimeout(() => solutionEditor.refresh(), 100); 24 | 25 | // Ensure the textarea gets updated with editor content 26 | solutionEditor.on("change", function() { 27 | solutionCodeArea.value = solutionEditor.getValue(); 28 | }); 29 | } 30 | 31 | // Handle form submission to sync CodeMirror content with textarea 32 | document.addEventListener('DOMContentLoaded', function() { 33 | const problemForm = document.querySelector('.problem-form'); 34 | if (problemForm) { 35 | problemForm.addEventListener('submit', function(e) { 36 | if (solutionEditor) { 37 | // Sync editor content to the original textarea before submission 38 | solutionCodeArea.value = solutionEditor.getValue(); 39 | } 40 | }); 41 | } 42 | }); 43 | 44 | // Theme selection 45 | function selectSolutionTheme() { 46 | if (solutionEditor && solutionThemeSelect) { 47 | let theme = solutionThemeSelect.value; 48 | solutionEditor.setOption("theme", theme); 49 | solutionEditor.refresh(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /problemset/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Problem, Category 3 | 4 | 5 | class AddProblemForm(forms.Form): 6 | title = forms.CharField(max_length=255, required=True) 7 | categories = forms.ModelMultipleChoiceField( 8 | queryset=Category.objects.exclude(short_name="problemset"), 9 | widget=forms.CheckboxSelectMultiple, 10 | required=True, 11 | help_text="Select problem categories", 12 | ) 13 | statement = forms.CharField( 14 | widget=forms.Textarea(attrs={"rows": 10}), 15 | required=True, 16 | help_text="Markdown format", 17 | ) 18 | editorial = forms.CharField( 19 | widget=forms.Textarea(attrs={"rows": 10}), 20 | required=True, 21 | help_text="Markdown format", 22 | ) 23 | time_limit = forms.FloatField(required=True) 24 | memory_limit = forms.IntegerField(required=True) 25 | test_data = forms.FileField(required=True, help_text="ZIP file") 26 | solution = forms.CharField( 27 | widget=forms.Textarea(attrs={"rows": 10, "id": "code_area"}), 28 | required=True, 29 | help_text="Solution code for this problem", 30 | ) 31 | 32 | class Meta: 33 | model = Problem 34 | fields = ["title", "time_limit", "memory_limit"] 35 | 36 | def __init__(self, *args, **kwargs): 37 | super().__init__(*args, **kwargs) 38 | for field_name, field in self.fields.items(): 39 | if field_name != "categories": 40 | field.widget.attrs.update({"class": "form-control"}) 41 | 42 | 43 | class SubmitForm(forms.Form): 44 | LANGUAGE_CHOICES = [ 45 | ("cpp", "GNU C++17"), 46 | ("py", "Python 3"), 47 | ] 48 | language = forms.ChoiceField(choices=LANGUAGE_CHOICES) 49 | code = forms.CharField(widget=forms.Textarea, max_length=65536) 50 | 51 | def __init__(self, *args, **kwargs): 52 | super().__init__(*args, **kwargs) 53 | for field in self.fields.values(): 54 | field.widget.attrs.update({"class": "form-control"}) 55 | -------------------------------------------------------------------------------- /static/img/graph_theory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # WnSOJ stuff 2 | .ruff_cache 3 | data/ 4 | media/ 5 | static/admin/ 6 | 7 | ### Django ### 8 | *.log 9 | *.pot 10 | *.pyc 11 | __pycache__/ 12 | local_settings.py 13 | 14 | ### PostgreSQL ### 15 | .pgpass 16 | 17 | ### Django.Python Stack ### 18 | # Byte-compiled / optimized / DLL files 19 | *.py[cod] 20 | *$py.class 21 | 22 | # C extensions 23 | *.so 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | parts/ 34 | sdist/ 35 | var/ 36 | wheels/ 37 | share/python-wheels/ 38 | *.egg-info/ 39 | .installed.cfg 40 | *.egg 41 | MANIFEST 42 | 43 | 44 | # Installer logs 45 | pip-log.txt 46 | pip-delete-this-directory.txt 47 | 48 | # Unit test / coverage reports 49 | htmlcov/ 50 | .tox/ 51 | .nox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *.cover 58 | *.py,cover 59 | .hypothesis/ 60 | .pytest_cache/ 61 | cover/ 62 | 63 | # Translations 64 | *.mo 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | .pybuilder/ 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | __pypackages__/ 85 | 86 | celerybeat-schedule 87 | celerybeat.pid 88 | 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | env.bak/ 98 | venv.bak/ 99 | 100 | # Spyder project settings 101 | .spyderproject 102 | .spyproject 103 | 104 | # Rope project settings 105 | .ropeproject 106 | 107 | # mkdocs documentation 108 | /site 109 | 110 | # mypy 111 | .mypy_cache/ 112 | .dmypy.json 113 | dmypy.json 114 | 115 | # Pyre type checker 116 | .pyre/ 117 | 118 | # pytype static type analyzer 119 | .pytype/ 120 | 121 | # Cython debug symbols 122 | cython_debug/ 123 | 124 | ### macOS ### 125 | # General 126 | .DS_Store 127 | .AppleDouble 128 | .LSOverride 129 | 130 | # Icon must end with two \r 131 | Icon 132 | 133 | # Files that might appear in the root of a volume 134 | .DocumentRevisions-V100 135 | .fseventsd 136 | .Spotlight-V100 137 | .TemporaryItems 138 | .Trashes 139 | .VolumeIcon.icns 140 | .com.apple.timemachine.donotpresent 141 | 142 | # Directories potentially created on remote AFP share 143 | .AppleDB 144 | .AppleDesktop 145 | Network Trash Folder 146 | Temporary Items 147 | .apdisk 148 | -------------------------------------------------------------------------------- /problemset/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-18 15:12 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | initial = True 11 | 12 | dependencies = [ 13 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Category', 19 | fields=[ 20 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('short_name', models.CharField(max_length=50)), 22 | ('long_name', models.CharField(max_length=100)), 23 | ('img_url', models.CharField(max_length=200)), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Problem', 28 | fields=[ 29 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('title', models.CharField(max_length=200)), 31 | ('time_limit', models.FloatField()), 32 | ('memory_limit', models.IntegerField()), 33 | ('categories', models.ManyToManyField(related_name='problems', to='problemset.category')), 34 | ], 35 | ), 36 | migrations.CreateModel( 37 | name='Submission', 38 | fields=[ 39 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 40 | ('verdict', models.CharField(choices=[('In queue', 'In queue'), ('AC', 'Accepted'), ('WA', 'Wrong Answer'), ('TLE', 'Time Limit Exceeded'), ('MLE', 'Memory Limit Exceeded'), ('CE', 'Compilation Error'), ('RE', 'Runtime Error')], default='In queue', max_length=20)), 41 | ('time', models.IntegerField(default=0)), 42 | ('memory', models.IntegerField(default=0)), 43 | ('language', models.CharField(max_length=20)), 44 | ('send_time', models.DateTimeField(auto_now_add=True)), 45 | ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='problemset.problem')), 46 | ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to=settings.AUTH_USER_MODEL)), 47 | ], 48 | ), 49 | ] 50 | -------------------------------------------------------------------------------- /templates/problemset/problem_submissions.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 | {% include "problemset/problem_navbar.html" %} 5 | 6 |
7 |
8 |

Submissions

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | {% for item in submissions %} 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% if item.verdict == 'AC' %} 33 | 34 | {% elif item.verdict == 'CE' %} 35 | 36 | {% elif item.verdict == 'IQ' %} 37 | 38 | {% else %} 39 | 40 | {% endif %} 41 | 42 | 43 | 44 | {% endfor %} 45 | 46 |
IDSending timeUserProblemLanguageVerdictTimeMemory
{{ item.id }}{{ item.send_time.strftime("%b/%d/%Y %H:%M") }}{{ item.user.username }}{{ item.problem.title }}{{ item.language }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.time }} ms{{ item.memory }} KB
47 |
48 |
49 |
50 | {% endblock %} -------------------------------------------------------------------------------- /templates/problemset/submissions_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |

Submissions

7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% for item in submissions %} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {% if item.verdict == 'AC' %} 31 | 32 | {% elif item.verdict == 'CE' %} 33 | 34 | {% elif item.verdict == 'IQ' %} 35 | 36 | {% else %} 37 | 38 | {% endif %} 39 | 40 | 41 | 42 | {% endfor %} 43 | 44 |
IDSending timeUserProblemLanguageVerdictTimeMemory
{{ item.id }}{{ item.send_time.strftime("%b/%d/%Y %H:%M") }}{{ item.user.username }}{{ item.problem.title }}{{ item.language }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.time }} ms{{ item.memory }} KB
45 |
46 |
47 |
48 | {% endblock %} -------------------------------------------------------------------------------- /static/css/jobs.css: -------------------------------------------------------------------------------- 1 | /* Job card styling */ 2 | .job-card { 3 | margin-bottom: 1.5rem; 4 | transition: transform 0.2s ease, box-shadow 0.2s ease; 5 | border: 1px solid rgba(0, 0, 0, 0.125); 6 | border-left: 4px solid #4e73df; 7 | } 8 | 9 | .job-card:hover { 10 | transform: translateY(-3px); 11 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); 12 | } 13 | 14 | .job-card .card-header { 15 | background-color: #f8f9fc; 16 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 17 | font-weight: 600; 18 | color: #3a3b45; 19 | } 20 | 21 | .job-card .card-footer { 22 | background-color: #f8f9fc; 23 | border-top: 1px solid rgba(0, 0, 0, 0.125); 24 | font-size: 0.875rem; 25 | color: #6e707e; 26 | } 27 | 28 | /* Job details page */ 29 | .job-details { 30 | background-color: #fff; 31 | border-radius: 0.35rem; 32 | box-shadow: 0 0.15rem 1.75rem rgba(0, 0, 0, 0.15); 33 | padding: 1.5rem; 34 | } 35 | 36 | .job-meta { 37 | display: flex; 38 | justify-content: space-between; 39 | margin-bottom: 1.5rem; 40 | padding-bottom: 1rem; 41 | border-bottom: 1px solid #e3e6f0; 42 | } 43 | 44 | .job-meta-item { 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | 49 | .job-meta-label { 50 | font-size: 0.875rem; 51 | color: #858796; 52 | font-weight: 600; 53 | } 54 | 55 | .job-meta-value { 56 | font-size: 1rem; 57 | font-weight: 500; 58 | } 59 | 60 | .job-description { 61 | padding: 1rem 0; 62 | } 63 | 64 | /* Job form styling */ 65 | .job-form { 66 | max-width: 800px; 67 | margin: 0 auto; 68 | padding: 1.5rem; 69 | background-color: #fff; 70 | border-radius: 0.35rem; 71 | box-shadow: 0 0.15rem 1.75rem rgba(0, 0, 0, 0.15); 72 | } 73 | 74 | .job-form h3 { 75 | margin-bottom: 1.5rem; 76 | color: #3a3b45; 77 | font-weight: 600; 78 | padding-bottom: 0.75rem; 79 | border-bottom: 1px solid #e3e6f0; 80 | } 81 | 82 | .job-form label { 83 | font-weight: 500; 84 | color: #5a5c69; 85 | margin-bottom: 0.5rem; 86 | } 87 | 88 | .job-form .form-control { 89 | margin-bottom: 1rem; 90 | } 91 | 92 | .job-form textarea { 93 | min-height: 200px; 94 | } 95 | 96 | /* Salary range slider */ 97 | .salary-range-container { 98 | margin-bottom: 1rem; 99 | } 100 | 101 | .salary-range-values { 102 | display: flex; 103 | justify-content: space-between; 104 | margin-top: 0.5rem; 105 | color: #5a5c69; 106 | font-size: 0.875rem; 107 | } 108 | 109 | /* Job action buttons */ 110 | .job-actions { 111 | margin-top: 1rem; 112 | display: flex; 113 | gap: 0.5rem; 114 | } 115 | 116 | /* Job listings header */ 117 | .jobs-header { 118 | display: flex; 119 | justify-content: space-between; 120 | align-items: center; 121 | margin-bottom: 1.5rem; 122 | padding-bottom: 0.75rem; 123 | border-bottom: 1px solid #e3e6f0; 124 | } 125 | -------------------------------------------------------------------------------- /accounts/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 5.1.5 on 2025-01-18 15:12 2 | 3 | import django.contrib.auth.models 4 | import django.contrib.auth.validators 5 | import django.utils.timezone 6 | from django.db import migrations, models 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('auth', '0012_alter_user_first_name_max_length'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='User', 20 | fields=[ 21 | ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('password', models.CharField(max_length=128, verbose_name='password')), 23 | ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), 24 | ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), 25 | ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), 26 | ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), 27 | ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), 28 | ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), 29 | ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), 30 | ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), 31 | ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), 32 | ('phone_number', models.CharField(blank=True, max_length=20, null=True)), 33 | ('account_type', models.IntegerField(default=0)), 34 | ('icon_id', models.IntegerField(null=True)), 35 | ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), 36 | ], 37 | options={ 38 | 'verbose_name': 'user', 39 | 'verbose_name_plural': 'users', 40 | 'abstract': False, 41 | }, 42 | managers=[ 43 | ('objects', django.contrib.auth.models.UserManager()), 44 | ], 45 | ), 46 | ] 47 | -------------------------------------------------------------------------------- /static/img/main_page_card1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/css/home.css: -------------------------------------------------------------------------------- 1 | /* Home page styling */ 2 | 3 | .home-hero-title { 4 | font-size: 2.5rem; 5 | font-weight: 700; 6 | color: #4e73df; 7 | margin-bottom: 1.2rem; 8 | } 9 | 10 | .home-hero-subtitle { 11 | font-size: 1.2rem; 12 | color: #6c757d; 13 | max-width: 800px; 14 | margin: 0 auto 1.5rem; 15 | } 16 | 17 | .home-divider { 18 | max-width: 100px; 19 | margin: 0 auto; 20 | height: 4px; 21 | background-color: #4e73df; 22 | opacity: 0.7; 23 | border-radius: 2px; 24 | } 25 | 26 | .home-features { 27 | justify-content: center; 28 | margin-top: 2rem; 29 | } 30 | 31 | .home-feature-card { 32 | transition: all 0.3s ease; 33 | border: none; 34 | border-radius: 0.75rem; 35 | overflow: hidden; 36 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.07); 37 | height: 100%; 38 | } 39 | 40 | .home-feature-card:hover { 41 | transform: translateY(-5px); 42 | box-shadow: 0 10px 20px rgba(0,0,0,0.1); 43 | } 44 | 45 | .home-card-img-container { 46 | height: auto; 47 | min-height: 200px; 48 | overflow: hidden; 49 | display: flex; 50 | align-items: center; 51 | justify-content: center; 52 | background-color: #f8f9fa; 53 | padding: 10px; 54 | } 55 | 56 | .home-card-img-container img { 57 | width: 100%; 58 | height: auto; 59 | object-fit: contain; 60 | transition: transform 0.3s ease; 61 | max-height: 200px; 62 | } 63 | 64 | .home-feature-card:hover .home-card-img-container img { 65 | transform: scale(1.05); 66 | } 67 | 68 | .home-card-body { 69 | padding: 1.5rem 1.25rem; 70 | text-align: center; 71 | } 72 | 73 | .home-card-title { 74 | color: #3a5bbf; 75 | margin-bottom: 1rem; 76 | font-weight: 600; 77 | } 78 | 79 | .home-card-text { 80 | color: #6c757d; 81 | margin-bottom: 1.5rem; 82 | } 83 | 84 | .home-btn { 85 | padding: 0.5rem 1.5rem; 86 | font-weight: 500; 87 | border-radius: 0.25rem; 88 | } 89 | 90 | /* Collapse buttons styling */ 91 | .collapse-btn { 92 | transition: all 0.3s ease; 93 | border-radius: 0.5rem; 94 | border: none; 95 | font-weight: 500; 96 | padding: 0.5rem 1.25rem; 97 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 98 | } 99 | 100 | .collapse-btn:hover { 101 | transform: translateY(-2px); 102 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); 103 | } 104 | 105 | .collapse-btn:focus { 106 | box-shadow: 0 0 0 0.25rem rgba(78, 115, 223, 0.25); 107 | } 108 | 109 | .code-collapse { 110 | margin-left: 10px; 111 | transition: all 0.3s ease; 112 | } 113 | 114 | .code-collapse .card { 115 | border-radius: 0.5rem; 116 | border: none; 117 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08); 118 | background-color: #f8f9fc; 119 | } 120 | 121 | .code-collapse .card-body { 122 | padding: 1rem; 123 | } 124 | 125 | .code-collapse p { 126 | font-family: monospace; 127 | color: #5a5c69; 128 | } 129 | 130 | /* Fix for Bootstrap 5 ml-3 compatibility */ 131 | .ml-3 { 132 | margin-left: 1rem !important; 133 | } 134 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 | 5 |
6 |

Welcome to WnSOJ!

7 |

Your platform for programming challenges, problem-solving, and career opportunities

8 |
9 |
10 | 11 | 12 |
13 |
14 |
15 |
16 | Practice coding 17 |
18 |
19 |

Solve Problems

20 |

Challenge yourself with programming and mathematical problems across various difficulty levels and categories.

21 | Start Coding 22 |
23 |
24 |
25 | 26 |
27 |
28 |
29 | Find jobs 30 |
31 |
32 |

Get a Job

33 |

Discover exciting career opportunities. Connect with employers and take your next professional step forward.

34 | Browse Jobs 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 | Improve skills 43 |
44 |
45 |

Be the Best

46 |

Sharpen your skills, climb the leaderboards, and establish yourself as a top programmer in the community.

47 | Start Competing 48 |
49 |
50 |
51 |
52 |
53 | {% endblock %} -------------------------------------------------------------------------------- /templates/accounts/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |
6 |
7 |
8 |

Log in to WnSOJ

9 |
10 |
11 |
{{ csrf_input }} 12 | 13 |
14 | 15 |
16 | 17 | {{ form.username }} 18 |
19 | {% for error in form.username.errors %} 20 | 23 | {% endfor %} 24 |
25 | 26 | 27 |
28 | 29 |
30 | 31 | {{ form.password }} 32 |
33 | {% for error in form.password.errors %} 34 | 37 | {% endfor %} 38 |
39 | 40 | 41 |
42 |
43 | {{ form.remember }} 44 | 47 |
48 |
49 | 50 | 51 |
52 | 55 |
56 | 57 | 58 | 61 |
62 |
63 |
64 |
65 |
66 | {% endblock %} 67 | -------------------------------------------------------------------------------- /app/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | URL configuration for app project. 3 | 4 | The `urlpatterns` list routes URLs to views. For more information please see: 5 | https://docs.djangoproject.com/en/5.1/topics/http/urls/ 6 | Examples: 7 | Function views 8 | 1. Add an import: from my_app import views 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 10 | Class-based views 11 | 1. Add an import: from other_app.views import Home 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 13 | Including another URLconf 14 | 1. Import the include() function: from django.urls import include, path 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 16 | """ 17 | from django.contrib import admin 18 | from django.urls import path, include 19 | from accounts import views as accounts_views 20 | from problemset import views as problem_views 21 | from jobboard import views as job_views 22 | from . import settings 23 | from django.conf.urls.static import static 24 | from rest_framework.routers import DefaultRouter 25 | from rest_framework_simplejwt.views import ( 26 | TokenObtainPairView, 27 | TokenRefreshView, 28 | ) 29 | from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView, SpectacularRedocView 30 | 31 | router = DefaultRouter() 32 | router.register(r'categories', problem_views.CategoryAPIViewSet) 33 | router.register(r'problems', problem_views.ProblemAPIViewSet) 34 | router.register(r'submissions', problem_views.SubmissionAPIViewSet) 35 | router.register(r'jobs', job_views.JobAPIViewSet) 36 | 37 | urlpatterns = [ 38 | path('admin/', admin.site.urls), 39 | path('', problem_views.home_page, name='home'), 40 | path('home/', problem_views.home_page, name='home'), 41 | path('register/', accounts_views.register, name='register'), 42 | path('login/', accounts_views.user_login, name='login'), 43 | path('logout/', accounts_views.user_logout, name='logout'), 44 | path('edit_profile/', accounts_views.edit_profile, name='edit_profile'), 45 | path('profile//', accounts_views.profile, name='profile'), 46 | path('problems/', problem_views.categories, name='problems'), 47 | path('add_problem/', problem_views.add_problem, name='add_problem'), 48 | path('problem//', problem_views.problem_statement, name='problem_statement'), 49 | path('problem//editorial/', problem_views.problem_editorial, name='problem_editorial'), 50 | path('problems//', problem_views.problems, name='problems'), 51 | path('problem//submissions/', problem_views.problem_submissions_list, name='problem_submissions_list'), 52 | path('submissions/', problem_views.submissions, name='submissions'), 53 | path('submission//', problem_views.submission, name='submission'), 54 | path('faq/', problem_views.faq, name='faq'), 55 | path('jobs/', job_views.jobs, name='jobs'), 56 | path('add_job/', job_views.add_job, name='add_job'), 57 | path('job//', job_views.job, name='job'), 58 | path('job//edit/', job_views.edit_job, name='edit_job'), 59 | path('job//delete/', job_views.delete_job, name='delete_job'), 60 | path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # Get JWT 61 | path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # Refresh JWT 62 | path('api/register/', accounts_views.RegisterAPIView.as_view(), name='register_api'), 63 | path('api/profile/', accounts_views.UserDetailAPIView.as_view(), name='profile_api'), 64 | path('api/', include(router.urls)), 65 | path('api/schema/', SpectacularAPIView.as_view(), name='schema'), 66 | path('api/schema/swagger-ui/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), 67 | path('api/schema/redoc/', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), 68 | ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) 69 | -------------------------------------------------------------------------------- /accounts/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers 2 | from .models import User 3 | import os 4 | from app import settings 5 | from PIL import Image 6 | import random 7 | 8 | 9 | class UserSerializer(serializers.ModelSerializer): 10 | class Meta: 11 | model = User 12 | fields = [ 13 | "id", 14 | "username", 15 | "email", 16 | "first_name", 17 | "last_name", 18 | "phone_number", 19 | "account_type", 20 | "icon_id", 21 | ] 22 | 23 | 24 | class UserDetailSerializer(serializers.ModelSerializer): 25 | is_business = serializers.BooleanField(required=False) 26 | 27 | class Meta: 28 | model = User 29 | fields = [ 30 | "id", 31 | "username", 32 | "email", 33 | "first_name", 34 | "last_name", 35 | "phone_number", 36 | "is_business", 37 | ] 38 | 39 | def to_representation(self, instance): 40 | ret = super().to_representation(instance) 41 | ret["is_business"] = instance.account_type == 2 42 | return ret 43 | 44 | def update(self, instance, validated_data): 45 | if "is_business" in validated_data: 46 | is_business = validated_data.pop("is_business") 47 | instance.account_type = 2 if is_business else 1 48 | return super().update(instance, validated_data) 49 | 50 | 51 | class RegisterSerializer(serializers.ModelSerializer): 52 | password = serializers.CharField( 53 | write_only=True, required=True, style={"input_type": "password"} 54 | ) 55 | password2 = serializers.CharField( 56 | write_only=True, required=True, style={"input_type": "password"} 57 | ) 58 | icon = serializers.ImageField(required=False) 59 | is_business = serializers.BooleanField(required=False, default=False) 60 | 61 | class Meta: 62 | model = User 63 | fields = [ 64 | "id", 65 | "username", 66 | "email", 67 | "first_name", 68 | "last_name", 69 | "phone_number", 70 | "password", 71 | "password2", 72 | "icon", 73 | "is_business", 74 | ] 75 | 76 | def validate(self, attrs): 77 | if attrs["password"] != attrs["password2"]: 78 | raise serializers.ValidationError({"password": "Passwords must match."}) 79 | return attrs 80 | 81 | def create(self, validated_data): 82 | validated_data.pop("password2") 83 | icon = validated_data.pop("icon", None) 84 | is_business = validated_data.pop("is_business", False) 85 | user = User(**validated_data) 86 | user.account_type = 2 if is_business else 1 87 | user.set_password(validated_data["password"]) 88 | user.icon_id = random.randint(10000000, 99999999) 89 | 90 | if icon: 91 | user.icon_id = abs(user.icon_id) 92 | icon64_dir = os.path.join( 93 | settings.BASE_DIR, "media", "users_icons", "icon64" 94 | ) 95 | icon170_dir = os.path.join( 96 | settings.BASE_DIR, "media", "users_icons", "icon170" 97 | ) 98 | os.makedirs(icon64_dir, exist_ok=True) 99 | os.makedirs(icon170_dir, exist_ok=True) 100 | 101 | icon64_path = os.path.join(icon64_dir, f"{user.icon_id}.png") 102 | icon170_path = os.path.join(icon170_dir, f"{user.icon_id}.png") 103 | 104 | img = Image.open(icon) 105 | img = img.resize((64, 64)) 106 | img.save(icon64_path) 107 | icon.seek(0) 108 | img170 = Image.open(icon) 109 | img170 = img170.resize((170, 170)) 110 | img170.save(icon170_path) 111 | else: 112 | user.icon_id = -user.icon_id 113 | 114 | user.save() 115 | return user 116 | -------------------------------------------------------------------------------- /static/img/data_structures.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/jobboard/job.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 |
6 |
7 | {% if user.is_authenticated and (user.account_type != 1 and job in user.jobs.all()) %} 8 | 16 |
17 | {% endif %} 18 | 19 |

{{ job.title }}

20 | 21 |
22 |
23 | Location 24 | {{ job.location }} 25 |
26 | 27 |
28 | Salary Range 29 | 30 | {% if job.salary_range %} 31 | {% if 'min' not in job.salary_range.keys() and 'max' not in job.salary_range.keys() %} 32 | Salary not specified 33 | {% elif 'min' in job.salary_range.keys() and 'max' not in job.salary_range.keys() %} 34 | {% if job.salary_range.min|string() == "0" %} 35 | Salary not specified 36 | {% else %} 37 | At least {{ format_number(job.salary_range.min) }}{{ job.salary_range.currency }}/yr 38 | {% endif %} 39 | {% elif 'min' not in job.salary_range.keys() and 'max' in job.salary_range.keys() %} 40 | Up to {{ format_number(job.salary_range.max) }}{{ job.salary_range.currency }}/yr 41 | {% else %} 42 | {{ format_number(job.salary_range.min) }}{{ job.salary_range.currency }} - {{ format_number(job.salary_range.max) }}{{ job.salary_range.currency }}/yr 43 | {% endif %} 44 | {% else %} 45 | Salary not specified 46 | {% endif %} 47 | 48 |
49 | 50 |
51 | Posted By 52 | 53 | {{ job.user.username }} 54 | 55 |
56 | 57 |
58 | Posted On 59 | {{ job.created_at.strftime('%B %d, %Y at %I:%M %p') }} 60 |
61 |
62 | 63 |
64 |

Job Description

65 |
{{ job.info }}
66 |
67 | 72 |
73 |
74 | 75 | 85 | {% endblock %} -------------------------------------------------------------------------------- /templates/jobboard/add_job.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 |
5 |

{% if is_edit %}Edit{% else %}Add{% endif %} Job

6 |
{{ csrf_input }} 7 |
8 | {{ form.title.label }} 9 | {{ form.title }} 10 | {% for error in form.title.errors %} 11 | 14 | {% endfor %} 15 |
16 | 17 |
18 | {{ form.location.label }} 19 | {{ form.location }} 20 | {% for error in form.location.errors %} 21 | 24 | {% endfor %} 25 |
26 | 27 |
28 | 29 |
30 |
31 | 32 | 34 |
35 |
36 | 37 | 39 |
40 |
41 | 42 | 50 |
51 |
52 | All salary fields are optional. 53 | {% if salary_error %} 54 | 57 | {% endif %} 58 |
59 | 60 |
61 | {{ form.info.label }} 62 | {{ form.info }} 63 | Markdown formatting is supported. 64 | {% for error in form.info.errors %} 65 | 68 | {% endfor %} 69 |
70 | 71 | 74 | {% if is_edit %} 75 | 76 | Cancel 77 | 78 | {% endif %} 79 |
80 |
81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /static/css/base.css: -------------------------------------------------------------------------------- 1 | /* Base styling for elements shared across all pages */ 2 | 3 | .navbar.fixed-top { 4 | z-index: 1030; 5 | } 6 | 7 | body { 8 | padding-top: 55px; 9 | } 10 | 11 | .content-with-fixed-nav { 12 | padding-top: 1rem; 13 | } 14 | 15 | /* Alert styles */ 16 | .alert { 17 | border-left: 4px solid #dc3545; 18 | border-radius: 0.25rem; 19 | padding: 0.75rem 1.25rem; 20 | text-align: center; 21 | } 22 | 23 | .form-alert { 24 | left: 0; 25 | margin: auto; 26 | right: 0; 27 | text-align: center; 28 | top: 3em; 29 | width: 100%; 30 | z-index: 1; 31 | } 32 | 33 | /* Adjust alert container to appear above fixed navbar */ 34 | .alert-container { 35 | position: fixed; 36 | top: 70px; /* Position below the navbar */ 37 | left: 50%; 38 | transform: translateX(-50%); 39 | width: 100%; 40 | max-width: 600px; 41 | z-index: 1040; /* Higher than navbar */ 42 | pointer-events: none; 43 | } 44 | 45 | .alert-floating { 46 | margin-bottom: 10px; 47 | border: none; 48 | border-radius: 6px; 49 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); 50 | padding: 12px 20px; 51 | opacity: 0.95; 52 | pointer-events: auto; 53 | transition: opacity 0.5s ease, transform 0.5s ease; 54 | } 55 | 56 | .alert-floating.fade-out { 57 | opacity: 0; 58 | transform: translateY(-20px); 59 | } 60 | 61 | .alert-floating.alert-success { 62 | background-color: #d4edda; 63 | color: #155724; 64 | border-left: 4px solid #28a745; 65 | } 66 | 67 | .alert-floating.alert-danger, 68 | .alert-floating.alert-error { 69 | background-color: #f8d7da; 70 | color: #721c24; 71 | border-left: 4px solid #dc3545; 72 | } 73 | 74 | .alert-floating.alert-warning { 75 | background-color: #fff3cd; 76 | color: #856404; 77 | border-left: 4px solid #ffc107; 78 | } 79 | 80 | .alert-floating.alert-info { 81 | background-color: #d1ecf1; 82 | color: #0c5460; 83 | border-left: 4px solid #17a2b8; 84 | } 85 | 86 | /* Form elements */ 87 | .form-control:focus, 88 | .form-select:focus { 89 | border-color: #86b7fe; 90 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.2); 91 | outline: none; 92 | } 93 | 94 | /* Button styles */ 95 | .btn-primary { 96 | background-image: linear-gradient(135deg, #5a7ce2, #3a5bbf); 97 | border: none; 98 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 99 | transition: all 0.2s ease; 100 | border-radius: 0.3rem; 101 | font-weight: 500; 102 | } 103 | 104 | .btn-primary:hover { 105 | transform: translateY(-2px); 106 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15); 107 | background-image: linear-gradient(135deg, #4e73df, #3a5bbf); 108 | } 109 | 110 | .btn-outline-primary { 111 | color: #4e73df; 112 | border: 1px solid #4e73df; 113 | background-color: transparent; 114 | box-shadow: none; 115 | font-weight: 500; 116 | transition: all 0.2s ease; 117 | } 118 | 119 | .btn-outline-primary:hover { 120 | background-color: #f0f4ff; 121 | color: #224abe; 122 | border-color: #224abe; 123 | transform: translateY(-1px); 124 | box-shadow: 0 2px 4px rgba(78, 115, 223, 0.15); 125 | } 126 | 127 | /* Container styles */ 128 | .container { 129 | max-width: 1200px; 130 | margin: 1rem auto; 131 | } 132 | 133 | /* Navbar styles */ 134 | .navbar { 135 | padding: 0.75rem 1rem; 136 | } 137 | 138 | .navbar-brand { 139 | font-size: 1.5rem; 140 | letter-spacing: 0.05rem; 141 | } 142 | 143 | .navbar .nav-link { 144 | padding: 0.5rem 1rem; 145 | font-weight: 500; 146 | transition: all 0.2s; 147 | } 148 | 149 | .navbar .nav-link:hover { 150 | background-color: rgba(255,255,255,0.1); 151 | border-radius: 0.25rem; 152 | } 153 | 154 | .navbar .nav-link.active { 155 | background-color: rgba(255,255,255,0.2); 156 | border-radius: 0.25rem; 157 | } 158 | 159 | /* Footer styles */ 160 | .footer { 161 | color: #6c757d; 162 | border-top: 1px solid #e9ecef; 163 | } 164 | 165 | .footer a { 166 | color: #4e73df; 167 | } 168 | 169 | .footer a:hover { 170 | color: #224abe; 171 | } 172 | 173 | .footer h5 { 174 | font-weight: 600; 175 | margin-bottom: 1rem; 176 | } 177 | 178 | .footer ul li { 179 | margin-bottom: 0.5rem; 180 | } 181 | -------------------------------------------------------------------------------- /static/img/main_page_card3.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /static/img/strings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 11 | 13 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 30 | 35 | 38 | 39 | 43 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /templates/jobboard/jobs.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 27 |
28 |
29 |

Job Listings

30 | {% if user.is_authenticated and user.account_type != 1 %} 31 | 32 | Post a New Job 33 | 34 | {% endif %} 35 |
36 | 37 | {% if jobs %} 38 | {% for job in jobs %} 39 |
40 |
{{ job.title }}
41 |
42 |
43 | {{ job.location }} 44 | 45 | {% if job.salary_range %} 46 | {% if 'min' not in job.salary_range.keys() and 'max' not in job.salary_range.keys() %} 47 | Salary not specified 48 | {% elif 'min' in job.salary_range.keys() and 'max' not in job.salary_range.keys() %} 49 | {% if job.salary_range.min|string() == "0" %} 50 | Salary not specified 51 | {% else %} 52 | At least {{ format_number(job.salary_range.min) }}{{ job.salary_range.currency }}/yr 53 | {% endif %} 54 | {% elif 'min' not in job.salary_range.keys() and 'max' in job.salary_range.keys() %} 55 | Up to {{ format_number(job.salary_range.max) }}{{ job.salary_range.currency }}/yr 56 | {% else %} 57 | {{ format_number(job.salary_range.min) }}{{ job.salary_range.currency }} - {{ format_number(job.salary_range.max) }}{{ job.salary_range.currency }}/yr 58 | {% endif %} 59 | {% else %} 60 | Salary not specified 61 | {% endif %} 62 | 63 |
64 |
65 |
{{ job.info }}
66 |
67 | 68 | View Details 69 | 70 |
71 | 75 |
76 | {% endfor %} 77 | {% else %} 78 |
79 | No jobs available at the moment. Check back later! 80 |
81 | {% endif %} 82 | 94 |
95 | {% endblock %} -------------------------------------------------------------------------------- /problemset/utils.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | from app.settings import ISOLATE_PATH 4 | 5 | 6 | def parse_meta_file(meta_file_path): 7 | meta_data = {} 8 | try: 9 | with open(meta_file_path, "r") as file: 10 | for line in file: 11 | if ":" in line: 12 | key, value = line.strip().split(":", 1) 13 | try: 14 | value = float(value) if "." in value else int(value) 15 | except ValueError: 16 | pass 17 | meta_data[key] = value 18 | except FileNotFoundError: 19 | pass 20 | return meta_data 21 | 22 | 23 | def run_isolate(box_id, cmd, time_limit, mem_limit, input_data=None, is_compile=False): 24 | isolate_cmd = [ 25 | "isolate", 26 | f"--box-id={box_id}", 27 | "--cg", 28 | f"--time={time_limit}", 29 | f"--wall-time={time_limit * 2}", 30 | f"--cg-mem={mem_limit}", 31 | f"--meta={ISOLATE_PATH}/{box_id}/box/meta.txt", 32 | ] 33 | 34 | if is_compile: 35 | isolate_cmd.extend( 36 | [ 37 | "--processes=4", 38 | "--env=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", 39 | ] 40 | ) 41 | 42 | isolate_cmd.append("--run") 43 | isolate_cmd.append("--") 44 | isolate_cmd.extend(cmd) 45 | 46 | try: 47 | result = subprocess.run( 48 | isolate_cmd, input=input_data, capture_output=True, timeout=time_limit * 2 49 | ) 50 | res = parse_meta_file(f"{ISOLATE_PATH}/{box_id}/box/meta.txt") 51 | res["stdout"] = result.stdout.decode() 52 | res["run_success"] = result.returncode == 0 53 | except subprocess.TimeoutExpired: 54 | res = {"status": "TO", "run_success": False, "stdout": ""} 55 | return res 56 | 57 | 58 | def run_tests(box_id, config, problem_id, time_limit, mem_limit, submission): 59 | path_to_tests = os.path.join("data", "problems", str(problem_id), "tests") 60 | stat = {"verdict": "AC", "time": 0, "memory": 0} 61 | test_case = 0 62 | input_dir = os.path.join(path_to_tests, "input") 63 | output_dir = os.path.join(path_to_tests, "output") 64 | 65 | for filename in sorted(os.listdir(input_dir)): 66 | test_case += 1 67 | input_path = os.path.join(input_dir, filename) 68 | output_path = os.path.join(output_dir, filename) 69 | 70 | with open(input_path, "rb") as input_file: 71 | input_data = input_file.read() 72 | 73 | res = run_isolate(box_id, config["run"], time_limit, mem_limit, input_data) 74 | 75 | stat["time"] = max(stat["time"], int(res.get("time", 0) * 1000)) 76 | stat["memory"] = max(stat["memory"], int(res.get("max-rss", 0))) 77 | if not res.get("run_success", False): 78 | if res.get("max-rss", 0) >= mem_limit: 79 | stat["verdict"] = f"MLE {test_case}" 80 | stat["memory"] = mem_limit 81 | break 82 | elif res.get("status") == "TO": 83 | stat["verdict"] = f"TLE {test_case}" 84 | stat["time"] = time_limit * 1000 85 | break 86 | elif res.get("status") in ["RE", "SG"]: 87 | stat["verdict"] = f"RE {test_case}" 88 | break 89 | 90 | try: 91 | with open(output_path, "r") as output_file: 92 | expected_output = output_file.read().strip() 93 | actual_output = res["stdout"].strip() 94 | if actual_output != expected_output: 95 | stat["verdict"] = f"WA {test_case}" 96 | break 97 | except FileNotFoundError: 98 | stat["verdict"] = f"WA {test_case}" 99 | break 100 | 101 | submission.verdict = stat["verdict"] 102 | submission.time = stat["time"] 103 | submission.memory = stat["memory"] 104 | 105 | if submission.verdict == "AC": 106 | if submission.problem in submission.user.problems_unsolved.all(): 107 | submission.user.problems_unsolved.remove(submission.problem) 108 | if submission.problem not in submission.user.problems_solved.all(): 109 | submission.user.problems_solved.add(submission.problem) 110 | else: 111 | if ( 112 | submission.problem not in submission.user.problems_solved.all() 113 | and submission.problem not in submission.user.problems_unsolved.all() 114 | ): 115 | submission.user.problems_unsolved.add(submission.problem) 116 | submission.save() 117 | -------------------------------------------------------------------------------- /static/js/submission.js: -------------------------------------------------------------------------------- 1 | let codeElement = document.getElementById("code_area"); 2 | let themeSelect = document.getElementById("theme_select"); 3 | let langElement = document.getElementById("code_language"); 4 | let lang = langElement ? langElement.value : 'cpp'; 5 | 6 | let editor = CodeMirror.fromTextArea(codeElement, { 7 | lineNumbers: true, 8 | styleActiveLine: true, 9 | matchBrackets: true, 10 | // readOnly: true, 11 | tabSize: 4, 12 | indentUnit: 4, 13 | theme: "default", 14 | mode: (lang === 'GNU C++17' || lang == 'cpp') ? "text/x-c++src" : "text/x-python", 15 | lineWrapping: true, 16 | viewportMargin: Infinity, 17 | autoRefresh: true 18 | }); 19 | 20 | editor.setSize("100%", "auto"); 21 | 22 | // Force refresh when document is fully loaded 23 | document.addEventListener('DOMContentLoaded', function() { 24 | setTimeout(function() { 25 | editor.refresh(); 26 | }, 100); 27 | }); 28 | 29 | function selectTheme() { 30 | let theme = themeSelect.value; 31 | editor.setOption("theme", theme); 32 | editor.refresh(); 33 | } 34 | 35 | function selectLanguage() { 36 | let lang = langElement.value; 37 | editor.setOption("mode", (lang == "GNU C++17" || lang == "cpp") ? "text/x-c++src" : "text/x-python"); 38 | editor.refresh(); 39 | } 40 | 41 | function toggleSolution() { 42 | const solutionContent = document.getElementById('solution_content'); 43 | const toggleButton = document.getElementById('toggle_solution'); 44 | 45 | if (solutionContent.classList.contains('show')) { 46 | 47 | solutionContent.classList.remove('show'); 48 | toggleButton.innerHTML = ' Show Solution'; 49 | toggleButton.setAttribute('aria-expanded', 'false'); 50 | } else { 51 | 52 | solutionContent.classList.add('show'); 53 | toggleButton.innerHTML = ' Hide Solution'; 54 | toggleButton.setAttribute('aria-expanded', 'true'); 55 | 56 | setTimeout(() => { 57 | editor.refresh(); 58 | }, 350); 59 | } 60 | } 61 | 62 | function setAutoRefresh() { 63 | editor.setOption("autoRefresh", true); 64 | setTimeout(function() { 65 | editor.refresh(); 66 | }, 10); 67 | } 68 | 69 | let copyInProgress = false; 70 | let copyTimeout = null; 71 | 72 | function copyCode(editor) { 73 | 74 | if (copyInProgress) return; 75 | 76 | const copyButton = document.getElementById('copy_button'); 77 | const originalText = copyButton.innerHTML; 78 | const originalClass = 'btn-outline-dark'; 79 | const successClass = 'btn-success'; 80 | const errorClass = 'btn-danger'; 81 | 82 | if (editor) { 83 | 84 | copyInProgress = true; 85 | 86 | if (copyTimeout) { 87 | clearTimeout(copyTimeout); 88 | } 89 | 90 | const code = editor.getValue(); 91 | navigator.clipboard.writeText(code) 92 | .then(() => { 93 | 94 | copyButton.innerHTML = ' Copied!'; 95 | copyButton.classList.remove(originalClass, errorClass); 96 | copyButton.classList.add(successClass); 97 | 98 | copyTimeout = setTimeout(() => { 99 | copyButton.innerHTML = originalText; 100 | copyButton.classList.remove(successClass); 101 | copyButton.classList.add(originalClass); 102 | copyInProgress = false; 103 | copyTimeout = null; 104 | }, 2000); 105 | }) 106 | .catch((error) => { 107 | 108 | console.error('Failed to copy: ', error); 109 | copyButton.innerHTML = ' Failed!'; 110 | copyButton.classList.remove(originalClass, successClass); 111 | copyButton.classList.add(errorClass); 112 | 113 | copyTimeout = setTimeout(() => { 114 | copyButton.innerHTML = originalText; 115 | copyButton.classList.remove(errorClass); 116 | copyButton.classList.add(originalClass); 117 | copyInProgress = false; 118 | copyTimeout = null; 119 | }, 2000); 120 | }); 121 | } 122 | } 123 | 124 | document.addEventListener('DOMContentLoaded', function() { 125 | setAutoRefresh(); 126 | 127 | const langBadge = document.getElementById('code_language_badge'); 128 | if (langBadge && langElement) { 129 | langBadge.textContent = langElement.innerHTML; 130 | } 131 | }); -------------------------------------------------------------------------------- /static/css/code_display.css: -------------------------------------------------------------------------------- 1 | .content-container { 2 | padding: 1.5rem 0; 3 | } 4 | 5 | .section-header { 6 | margin-bottom: 1.5rem; 7 | } 8 | 9 | .section-title { 10 | font-size: 1.8rem; 11 | font-weight: 600; 12 | color: #3a5bbf; 13 | margin-bottom: 0.8rem; 14 | } 15 | 16 | .content-section { 17 | margin-bottom: 2rem; 18 | line-height: 1.6; 19 | } 20 | 21 | .subsection { 22 | margin-bottom: 1.5rem; 23 | } 24 | 25 | .subsection-title { 26 | font-size: 1.2rem; 27 | font-weight: 600; 28 | margin-bottom: 0.8rem; 29 | color: #495057; 30 | } 31 | 32 | .code-container { 33 | position: relative; 34 | border-radius: 0.5rem; 35 | overflow: hidden; 36 | margin: 0.5rem 0; 37 | background-color: #f8f9fa; 38 | border: 1px solid #e9ecef; 39 | } 40 | 41 | .code-header { 42 | display: flex; 43 | justify-content: space-between; 44 | align-items: center; 45 | padding: 0.5rem; 46 | border-bottom: 1px solid #e9ecef; 47 | } 48 | 49 | .code-language { 50 | font-family: monospace; 51 | font-weight: 600; 52 | color: #495057; 53 | } 54 | 55 | .code-controls { 56 | display: flex; 57 | align-items: center; 58 | gap: 0.75rem; 59 | } 60 | 61 | .toggle-code { 62 | transition: all 0.3s ease; 63 | } 64 | 65 | .toggle-code:focus { 66 | box-shadow: none; 67 | } 68 | 69 | .collapse { 70 | transition: height 0.35s ease; 71 | } 72 | 73 | .CodeMirror { 74 | height: auto !important; 75 | min-height: 300px; 76 | font-size: 14px; 77 | line-height: 1.6; 78 | border-radius: 0 0 0.5rem 0.5rem; 79 | border: none; 80 | } 81 | 82 | .CodeMirror-focused { 83 | box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15); 84 | } 85 | 86 | .math-container { 87 | margin: 1.5rem 0; 88 | overflow-x: auto; 89 | } 90 | 91 | .content-image { 92 | max-width: 100%; 93 | margin: 1.5rem 0; 94 | border-radius: 0.5rem; 95 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 96 | } 97 | 98 | .tag-list { 99 | display: flex; 100 | flex-wrap: wrap; 101 | gap: 0.5rem; 102 | margin: 1rem 0; 103 | } 104 | 105 | .tag { 106 | display: inline-block; 107 | padding: 0.25rem 0.75rem; 108 | background-color: #e9ecef; 109 | color: #495057; 110 | font-size: 0.85rem; 111 | border-radius: 2rem; 112 | font-weight: 500; 113 | } 114 | 115 | .tag:hover { 116 | background-color: #dee2e6; 117 | text-decoration: none; 118 | } 119 | 120 | .hint-container { 121 | background-color: #f8f9fa; 122 | border-left: 4px solid #4e73df; 123 | padding: 1rem; 124 | margin: 1.5rem 0; 125 | border-radius: 0.25rem; 126 | } 127 | 128 | .hint-title { 129 | font-weight: 600; 130 | color: #3a5bbf; 131 | margin-bottom: 0.5rem; 132 | } 133 | 134 | .copy-button { 135 | transition: all 0.2s ease; 136 | } 137 | 138 | #copy_button { 139 | transition: all 0.2s ease; 140 | position: relative; 141 | overflow: hidden; 142 | } 143 | 144 | #copy_button:active { 145 | transform: translateY(1px); 146 | } 147 | 148 | #copy_button.transition-active { 149 | pointer-events: none; 150 | } 151 | 152 | #copy_button.btn-success, 153 | #copy_button.btn-danger { 154 | transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out, border-color 0.2s ease-in-out; 155 | } 156 | 157 | .submission-code-wrapper { 158 | width: 100%; 159 | overflow: hidden; 160 | } 161 | 162 | .submission-code-wrapper .CodeMirror { 163 | width: 100% !important; 164 | height: auto !important; 165 | min-height: 400px; 166 | border: none; 167 | font-size: 14px; 168 | line-height: 1.6; 169 | border-radius: 0; 170 | } 171 | 172 | .CodeMirror-sizer { 173 | min-height: 400px !important; 174 | } 175 | 176 | .CodeMirror-gutters { 177 | height: 100% !important; 178 | min-height: 400px !important; 179 | } 180 | 181 | .CodeMirror-scroll { 182 | min-height: 400px !important; 183 | } 184 | 185 | /* Card styles */ 186 | .card { 187 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.07); 188 | border-radius: 0.5rem; 189 | border: 0; 190 | overflow: hidden; 191 | margin-bottom: 1rem; 192 | } 193 | 194 | .card-header { 195 | padding: 1rem 1.25rem; 196 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 197 | background-color: #f8f9fa; 198 | } 199 | 200 | @media (max-width: 768px) { 201 | .code-controls { 202 | flex-direction: column; 203 | align-items: flex-start; 204 | gap: 0.5rem; 205 | } 206 | 207 | .card-header .d-flex { 208 | flex-direction: column; 209 | align-items: flex-start; 210 | } 211 | 212 | .code-header { 213 | flex-direction: column; 214 | gap: 0.5rem; 215 | align-items: flex-start; 216 | } 217 | 218 | .copy-button { 219 | align-self: flex-end; 220 | } 221 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WnSOJ - Work and Solve online judge! 2 | WnSOJ is a platform where you can solve programming and math tasks, learn new algorithms and concepts and find job. Platform offers `effective testing system`, `categorized problemset`, `editorials and solutions`, `submissions`, `job search`, `users` and `statistics`. 3 | 4 | ### Check it out at [wnsoj.xyz](https://wnsoj.xyz)! 5 | 6 | ![Main Page](https://github.com/spike1236/WnSOJ/blob/main/readme_screenshots/screenshot_1.png) 7 | 8 | ## Getting Started 9 | 1. Download the project: 10 | ```shell 11 | git clone https://github.com/spike1236/WnSOJ.git 12 | cd WnSOJ 13 | ``` 14 | 2. Download required Python modules: 15 | ```shell 16 | pip install -r requirements.txt 17 | ``` 18 | 3. Install g++ compiler, [isolate](https://github.com/ioi/isolate); Work and Solve Online Judge uses cgroups v2-based isolate, check [this](https://askubuntu.com/questions/1469526/how-can-i-turn-on-cgroup-v2-cpu-controller-on-modern-ubuntu) for installation. 19 | 20 | 4. Fill out `./.pgpass`, `./.env` and `~/.pg_service.conf` according to provided templates. 21 | 5. Launch server and testing system (celery worker): 22 | ```shell 23 | python3 manage.py runserver 24 | celery -A app worker -B -l info 25 | ``` 26 | > You can also wrap launches as systemd services (which is recommended). 27 | 28 | 6. Open [Main page](http://127.0.0.1:8000) 29 | 7. Enjoy the project! :sunglasses: 30 | 31 | ## About Project 32 | ### Problems and submissions 33 | Platform provides an extensive set of olympiad programming tasks. To submit solution you need to be signed in system. You can register or sign in into existing account and submit solutions to problems. The testing system runs in parallel with server using Celery worker. You can use Redis, RabbitMQ or Amazon SQS as a broker for Celery worker (more info [here](https://docs.celeryq.dev/en/stable/getting-started/backends-and-brokers); personally, I use [Redis](https://redis.io/)). 34 | System will automatically test your solution in isolated sandboxes provided by [isolate](https://github.com/ioi/isolate) and report verdict, max used time and max used memory.\ 35 | Also, each problem has editorial and solution in C++ language.\ 36 | Platform administrators can add new problems. 37 | ### Jobs 38 | In the platform you can also find or publish/edit/delete job. 39 | There are 2 types of accounts: 40 | 1. Common account - these users can find job and communicate with employers by email or phone.\ 41 | Open job that you liked, read the description and if job suits you, communicate with employer by email or phone. 42 | 2. Business account - these users or companies can publish, edit or delete jobs, also find and communicate by email or phone with other users.\ 43 | Publish job, edit it if it is need, and just wait until some qualified specialist will communicate with you by email or phone number. 44 | ### Profile 45 | In the profile, you can see user's username, email, phone number and statistics about problems: submissions statistics and last 10 attempts.\ 46 | Also you can change your icon or password in 'Edit profile' page. 47 | ### API 48 | The platform provides a comprehensive REST API with the following features: 49 | - JWT Authentication 50 | - User registration and profile management 51 | - Access to problems, categories, and submissions 52 | - Job board integration 53 | 54 | API documentation is available through: 55 | - Swagger UI: `/api/schema/swagger-ui/` 56 | - ReDoc: `/api/schema/redoc/` 57 | 58 | Main API endpoints: 59 | - Authentication: `/api/token/` and `/api/token/refresh/` 60 | - User Registration: `/api/register/` 61 | - User Profile: `/api/profile/` 62 | - Problems: `/api/problems/` 63 | - Categories: `/api/categories/` 64 | - Submissions: `/api/submissions/` 65 | - Jobs: `/api/jobs/` 66 | 67 | ## Technologies 68 | Following technologies and libraries were used to create this project: 69 | * [Django](https://www.djangoproject.com) 70 | * [Django REST Framework](https://www.django-rest-framework.org) 71 | * [drf-spectacular](https://drf-spectacular.readthedocs.io/) (OpenAPI Schema) 72 | * [PostgreSQL](https://www.postgresql.org) 73 | * [isolate](https://github.com/ioi/isolate) 74 | * [Celery](https://docs.celeryq.dev/en/stable) 75 | * [Redis](https://redis.io) 76 | * [Pillow](https://pillow.readthedocs.io/en/stable) 77 | * [ZipFile](https://docs.python.org/3/library/zipfile.html) 78 | * [io](https://docs.python.org/3/library/io.html) 79 | ## Components (CSS and JS) 80 | Following components were used to create this project: 81 | * [Bootstrap](https://getbootstrap.com) 82 | * [CodeMirror](https://codemirror.net) 83 | * [marked](https://marked.js.org) 84 | * [katex](https://katex.org) 85 | * [FontAwesome](https://fontawesome.com) 86 | * [Swagger UI](https://swagger.io/tools/swagger-ui/) 87 | * [ReDoc](https://redocly.github.io/redoc/) 88 | 89 | ## Author 90 | * **Rakhmetulla Akram** - [spike1236](https://github.com/spike1236) 91 | ## License 92 | This work is licensed under a [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License](https://creativecommons.org/licenses/by-nc-sa/4.0).\ 93 | See [LICENSE](https://github.com/spike1236/WnSOJ/blob/main/LICENSE.md) file for details. 94 | -------------------------------------------------------------------------------- /problemset/tasks.py: -------------------------------------------------------------------------------- 1 | from celery import shared_task 2 | from .models import Submission 3 | import os 4 | import shutil 5 | import random 6 | import subprocess 7 | import logging 8 | from logging.handlers import RotatingFileHandler 9 | from .utils import run_isolate, run_tests 10 | from app import settings 11 | 12 | 13 | def configure_logger(): 14 | logger = logging.getLogger(__name__) 15 | 16 | if not logger.handlers: 17 | log_file = settings.BASE_DIR / "logs" / "problemset.log" 18 | os.makedirs(os.path.dirname(log_file), exist_ok=True) 19 | 20 | handler = RotatingFileHandler( 21 | filename=log_file, 22 | maxBytes=50 * 1024 * 1024, # 50MB 23 | backupCount=10, 24 | ) 25 | 26 | formatter = logging.Formatter( 27 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 28 | ) 29 | handler.setFormatter(formatter) 30 | 31 | logger.addHandler(handler) 32 | 33 | return logger 34 | 35 | 36 | logger = configure_logger() 37 | 38 | LANGUAGE_CONFIGS = { 39 | "cpp": { 40 | "compile": ["/usr/bin/g++", "-std=c++17", "source.cpp", "-o", "program"], 41 | "run": ["./program"], 42 | "source_file": "source.cpp", 43 | }, 44 | "py": {"run": ["/usr/bin/python3", "source.py"], "source_file": "source.py"}, 45 | } 46 | 47 | 48 | @shared_task 49 | def test_submission_task(submission_id): 50 | try: 51 | submission = Submission.objects.get(id=submission_id) 52 | except Submission.DoesNotExist: 53 | logger.error(f"Submission {submission_id} does not exist.") 54 | return 55 | 56 | if submission.verdict != "IQ": 57 | logger.info( 58 | "Submission {submission_id} already processed with verdict" 59 | + f"{submission.verdict}." 60 | ) 61 | return 62 | 63 | language = submission.language 64 | config = LANGUAGE_CONFIGS.get(language) 65 | 66 | box_id = random.randint(1, 999) 67 | 68 | try: 69 | subprocess.run( 70 | ["isolate", "--cg", "--box-id", str(box_id), "--init"], check=True 71 | ) 72 | logger.info( 73 | "Initialized isolate box with box_id={box_id} for submission" 74 | + f"={submission_id}" 75 | ) 76 | except subprocess.CalledProcessError: 77 | submission.verdict = "RE 1" 78 | submission.save() 79 | logger.error(f"Failed to initialize isolate box for submission {submission_id}") 80 | return 81 | 82 | isolate_box_path = f"{settings.ISOLATE_PATH}/{box_id}/box" 83 | try: 84 | with open(os.path.join(isolate_box_path, config["source_file"]), "w") as f: 85 | f.write(submission.code) 86 | logger.info( 87 | f"Copied source file to isolate box: {submission_id} -> {isolate_box_path}" 88 | ) 89 | except (FileNotFoundError, shutil.Error) as e: 90 | submission.verdict = "RE 1" 91 | submission.save() 92 | logger.error(f"Failed to copy source file for submission {submission_id}: {e}") 93 | subprocess.run(["isolate", "--cg", "--box-id", str(box_id), "--cleanup"]) 94 | return 95 | 96 | try: 97 | if "compile" in config: 98 | compile_cmd = config["compile"] 99 | compile_result = run_isolate( 100 | box_id, compile_cmd, time_limit=5, mem_limit=256 * 1024, is_compile=True 101 | ) 102 | if not compile_result.get("run_success", False): 103 | submission.verdict = "CE" 104 | submission.time = 0 105 | submission.memory = 0 106 | submission.save() 107 | return 108 | 109 | run_tests( 110 | box_id, 111 | config, 112 | submission.problem.id, 113 | time_limit=submission.problem.time_limit, 114 | mem_limit=submission.problem.memory_limit * 1024, 115 | submission=submission, 116 | ) 117 | except Exception as e: 118 | submission.verdict = "RE 1" 119 | submission.save() 120 | logger.error(f"Error while testing submission {submission_id}: {e}") 121 | finally: 122 | subprocess.run(["isolate", "--cg", "--box-id", str(box_id), "--cleanup"]) 123 | logger.info( 124 | f"Cleaned up isolate box with box_id={box_id} for submission" 125 | + f"{submission_id}" 126 | ) 127 | 128 | 129 | @shared_task 130 | def process_submission_queue(): 131 | submissions = Submission.objects.filter(verdict="IQ")[:30] 132 | if settings.NO_ISOLATE: 133 | for submission in submissions: 134 | submission.verdict = "RE 1" 135 | submission.time = random.randint( 136 | 0, int(submission.problem.time_limit * 1000) 137 | ) 138 | submission.memory = random.randint( 139 | 0, int(submission.problem.memory_limit * 1024) 140 | ) 141 | submission.save() 142 | logger.warning("NO_ISOLATE is set to True. Skipping submission processing.") 143 | return 144 | for submission in submissions: 145 | test_submission_task.delay(submission.id) 146 | -------------------------------------------------------------------------------- /static/img/math.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 11 | 15 | 19 | 34 | 40 | 45 | 51 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /templates/problemset/editorial.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | {% include "problemset/problem_navbar.html" %} 21 | 22 |
23 |
24 |

Editorial

25 |
26 |
27 |
{{ problem.editorial }}
28 |
29 |
30 | 31 |
32 |
33 |
34 |

Author's Solution

35 |
36 | 39 |
40 | 41 | 46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | GNU C++17 54 |
55 | 58 |
59 |
60 | 61 | 64 |
65 |
66 |
67 |
68 | 69 | 70 | {% endblock %} -------------------------------------------------------------------------------- /templates/problemset/problem.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | {% include "problemset/problem_navbar.html" %} 20 | 21 |
22 |
23 |

{{ problem.title }}

24 |
25 |
26 |
27 |
28 | Time limit: {{ problem.time_limit }} sec 29 |
30 |
31 | Memory limit: {{ problem.memory_limit }} MB 32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |

Problem Statement

40 |
41 |
{{ problem.statement }}
42 |
43 | 44 |
45 |
46 |

Code Editor

47 |
48 |
49 |
{{ csrf_input }} 50 |
51 |
52 |
53 | Language: 54 | 58 |
59 |
60 |
61 |
62 | Theme: 63 | 68 |
69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | 82 | 83 | {% endblock %} 84 | -------------------------------------------------------------------------------- /templates/problemset/submission.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

Submission #{{ item.id }}

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | {% if item.verdict == 'AC' %} 40 | 41 | {% elif item.verdict == 'CE' %} 42 | 43 | {% elif item.verdict == 'IQ' %} 44 | 45 | {% else %} 46 | 47 | {% endif %} 48 | 49 | 50 | 51 | 52 |
IDTimeUserProblemLanguageVerdictTimeMemory
{{ item.id }}{{ item.send_time.strftime("%b/%d/%Y %H:%M") }}{{ item.user.username }}{{ item.problem.title }}{{ item.language }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.time }} ms{{ item.memory }} KB
53 | 54 |
55 |
56 |

Source Code

57 |
58 |
59 | 60 | 65 |
66 | 69 |
70 |
71 |
72 |
73 | 74 | 77 | 78 |
79 |
80 |
81 |
82 | 83 | {% endblock %} -------------------------------------------------------------------------------- /templates/faq.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |
5 |

This site contains an archive of Olympiad programming tasks with a built-in testing system and work search platform.

6 |
    7 |
  • To open problems list go to "Problems" section.
  • 8 |
  • To open work search platform platform go to "Jobs" section.
  • 9 |
10 | 11 |
12 |
13 |
14 |

"Problems" section


15 |

16 | In "Problems" section you can solve various programming and math tasks.
17 | To submit solution to problem go to "Problems" section, then choose problem, paste your code in code editor and click on submit button.
18 | After submitting system runs your solution on tests and then reports verdict.
19 | Each problem has editorial and solution in C++ language. 20 | Also, platform administrators can add new problems. 21 |

22 |

Testing system uses the following compilers:

23 |
24 | 25 |
26 |
27 |
28 |

g++ source.cpp -std=c++17 -o source

29 |
30 |
31 |
32 |
33 | 34 |
35 |
36 |

python3 source.py

37 |
38 |
39 |
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 |
Verdict
Description
AC
Accepted. Program works correctly and passed all tests. Most likely, tests are week :)
IQ
In queue. Solution is in queue for testing.
CE
Compilation error. Syntax or another error in the code.
WA
Wrong answer. The output of the program is not correct.
RE
Runtime error. Program terminated with a non-zero return code.
TLE
Time limit exceeded. Program runs longer than the allocated time.
MLE
Memory limit exceeded. Program uses more than the allocated memory.
79 |
80 |
81 |
82 |

"Jobs" section


83 |

In "Jobs" section you can find work.

84 | There are 2 types of accounts: 85 |
    86 |
  1. Common account - these users can find job and communicate with employers by email or phone.
    87 | Open job that you liked, read the description and if job suits you, communicate with employer by email or phone. 88 |
  2. 89 |
  3. 90 | Business account - these users or companies can publish, edit or delete jobs, also find and communicate by email or phone with other users.
    91 | Publish job, edit it if it is need, and just wait until some qualified specialist will communicate with you by email or phone number. 92 |
  4. 93 |
94 |
95 |
96 |
97 |

API


98 |

Our platform provides a comprehensive REST API. You can request:

99 |
    100 |
  • authentication (get JWT tokens)
  • 101 |
  • user registration and profile information
  • 102 |
  • problems list and problem details
  • 103 |
  • categories list and category details
  • 104 |
  • submissions list and submission details
  • 105 |
  • jobs list and job details
  • 106 |
107 |

API documentation is available at:

108 | 112 |

Examples of API endpoints:

113 |
    114 |
  • JWT Authentication: /api/token/ and /api/token/refresh/
  • 115 |
  • Registration: /api/register/
  • 116 |
  • User Profile: /api/profile/
  • 117 |
  • Categories: /api/categories/
  • 118 |
  • Problems: /api/problems/
  • 119 |
  • Submissions: /api/submissions/
  • 120 |
  • Jobs: /api/jobs/
  • 121 |
122 |

For detailed API usage, please refer to the Swagger UI or ReDoc documentation.

123 |
124 |
125 | {% endblock %} 126 | -------------------------------------------------------------------------------- /app/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for app project. 3 | 4 | Generated by 'django-admin startproject' using Django 5.1.5. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/5.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/5.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | import os 15 | from dotenv import load_dotenv 16 | from celery.schedules import crontab 17 | from datetime import timedelta 18 | 19 | load_dotenv() 20 | 21 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 22 | BASE_DIR = Path(__file__).resolve().parent.parent 23 | 24 | SECRET_KEY = os.getenv('SECRET_KEY') 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = os.getenv('DEBUG', 'True') == 'True' 28 | 29 | ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', 'localhost').split(',') 30 | 31 | REST_FRAMEWORK = { 32 | 'DEFAULT_AUTHENTICATION_CLASSES': ( 33 | 'rest_framework_simplejwt.authentication.JWTAuthentication', 34 | ), 35 | 'DEFAULT_PERMISSION_CLASSES': ( 36 | 'rest_framework.permissions.IsAuthenticatedOrReadOnly', 37 | ), 38 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 39 | 'PAGE_SIZE': 10, 40 | 'DEFAULT_THROTTLE_CLASSES': [ 41 | 'rest_framework.throttling.UserRateThrottle', 42 | 'rest_framework.throttling.AnonRateThrottle', 43 | ], 44 | 'DEFAULT_THROTTLE_RATES': { 45 | 'user': '1000/day', 46 | 'anon': '100/day', 47 | }, 48 | 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema' 49 | } 50 | 51 | SPECTACULAR_SETTINGS = { 52 | 'TITLE': 'WnSOJ API', 53 | 'DESCRIPTION': 'WnSOJ API documentation', 54 | 'VERSION': '1.0.0', 55 | 'SERVE_INCLUDE_SCHEMA': False, 56 | # OTHER SETTINGS 57 | } 58 | 59 | SIMPLE_JWT = { 60 | 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1), 61 | 'REFRESH_TOKEN_LIFETIME': timedelta(days=1), 62 | 'ROTATE_REFRESH_TOKENS': True, 63 | 'BLACKLIST_AFTER_ROTATION': True, 64 | 'AUTH_HEADER_TYPES': ('Bearer',), 65 | 'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',), 66 | } 67 | 68 | CELERY_BROKER_URL = os.getenv('CELERY_BROKER_URL', 'redis://localhost:6379/0') 69 | CELERY_RESULT_BACKEND = os.getenv('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0') 70 | CELERY_BEAT_SCHEDULE = { 71 | 'process-submission-queue-every-minute': { 72 | 'task': 'problemset.tasks.process_submission_queue', 73 | 'schedule': crontab(minute='*/1') 74 | }, 75 | } 76 | CELERY_TIMEZONE = "UTC" 77 | CELERY_TASK_TRACK_STARTED = True 78 | CELERY_TASK_TIME_LIMIT = 10 * 60 79 | 80 | NO_ISOLATE = os.getenv('NO_ISOLATE', 'False') == 'True' 81 | ISOLATE_PATH = os.getenv('ISOLATE_PATH', '/var/lib/isolate') 82 | 83 | # Application definition 84 | 85 | INSTALLED_APPS = [ 86 | 'django.contrib.admin', 87 | 'django.contrib.auth', 88 | 'django.contrib.contenttypes', 89 | 'django.contrib.sessions', 90 | 'django.contrib.messages', 91 | 'django.contrib.staticfiles', 92 | 'accounts', 93 | 'problemset', 94 | 'jobboard', 95 | 'rest_framework', 96 | 'rest_framework_simplejwt', 97 | 'drf_spectacular' 98 | ] 99 | 100 | MIDDLEWARE = [ 101 | 'django.middleware.security.SecurityMiddleware', 102 | 'django.contrib.sessions.middleware.SessionMiddleware', 103 | 'django.middleware.common.CommonMiddleware', 104 | 'django.middleware.csrf.CsrfViewMiddleware', 105 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 106 | 'django.contrib.messages.middleware.MessageMiddleware', 107 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 108 | ] 109 | 110 | ROOT_URLCONF = 'app.urls' 111 | 112 | TEMPLATES = [ 113 | { 114 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 115 | 'DIRS': [ 116 | ], 117 | 'APP_DIRS': True, 118 | 'OPTIONS': { 119 | 'context_processors': [ 120 | 'django.template.context_processors.debug', 121 | 'django.template.context_processors.request', 122 | 'django.contrib.auth.context_processors.auth', 123 | 'django.contrib.messages.context_processors.messages', 124 | ], 125 | }, 126 | }, 127 | { 128 | "BACKEND": "django.template.backends.jinja2.Jinja2", 129 | "DIRS": [ 130 | BASE_DIR / "templates", 131 | ], 132 | "OPTIONS": { 133 | 'context_processors': [ 134 | 'django.template.context_processors.debug', 135 | 'django.template.context_processors.request', 136 | 'django.contrib.auth.context_processors.auth', 137 | 'django.contrib.messages.context_processors.messages', 138 | ], 139 | "environment": "app.jinja2.environment" 140 | } 141 | }, 142 | ] 143 | 144 | WSGI_APPLICATION = 'app.wsgi.application' 145 | 146 | 147 | # Database 148 | # https://docs.djangoproject.com/en/5.1/ref/settings/#databases 149 | 150 | DATABASES = { 151 | "default": { 152 | "ENGINE": "django.db.backends.postgresql", 153 | "OPTIONS": { 154 | "service": "wnsoj_service", # ~/.pg_service.conf 155 | "passfile": ".pgpass", 156 | }, 157 | } 158 | } 159 | 160 | AUTH_USER_MODEL = 'accounts.User' 161 | 162 | # Password validation 163 | # https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators 164 | 165 | AUTH_PASSWORD_VALIDATORS = [ 166 | { 167 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 168 | }, 169 | { 170 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 171 | }, 172 | { 173 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 174 | }, 175 | { 176 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 177 | }, 178 | ] 179 | 180 | 181 | # Internationalization 182 | # https://docs.djangoproject.com/en/5.1/topics/i18n/ 183 | 184 | LANGUAGE_CODE = 'en-us' 185 | 186 | TIME_ZONE = 'UTC' 187 | 188 | USE_I18N = True 189 | 190 | USE_TZ = True 191 | 192 | 193 | # Static files (CSS, JavaScript, Images) 194 | # https://docs.djangoproject.com/en/5.1/howto/static-files/ 195 | 196 | STATIC_URL = 'static/' 197 | STATICFILES_DIRS = ('static',) 198 | STATIC_ROOT = BASE_DIR / "staticfiles" 199 | 200 | MEDIA_URL = 'media/' 201 | MEDIA_ROOT = BASE_DIR / 'media' 202 | 203 | # Default primary key field type 204 | # https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field 205 | 206 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' 207 | -------------------------------------------------------------------------------- /templates/accounts/edit_profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |
5 | 6 |
7 |
8 |
9 |
Profile Picture
10 |
11 |
12 |
13 | {{ user.username }}'s profile picture 14 |
15 |
{{ user.username }}
16 |
17 | 18 | View Profile 19 | 20 |
21 | 22 |
{{ csrf_input }} 23 |
24 | 25 |
26 | 27 | {{ change_icon_form.icon }} 28 |
29 | {% for error in change_icon_form.icon.errors %} 30 | 33 | {% endfor %} 34 |
35 |
36 | 39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 |
47 |
48 |
49 |
Change Password
50 |
51 |
52 |
{{ csrf_input }} 53 | 54 |
55 | 56 |
57 | 58 | {{ password_change_form.old_password }} 59 |
60 | {% for error in password_change_form.old_password.errors %} 61 | 64 | {% endfor %} 65 |
66 | 67 | 68 |
69 | 70 |
71 | 72 | {{ password_change_form.new_password1 }} 73 |
74 | {% for error in password_change_form.new_password1.errors %} 75 | 78 | {% endfor %} 79 |
80 | 81 | 82 |
83 | 84 |
85 | 86 | {{ password_change_form.new_password2 }} 87 |
88 | {% for error in password_change_form.new_password2.errors %} 89 | 92 | {% endfor %} 93 |
94 | 95 | 96 |
97 | 100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | {% endblock %} -------------------------------------------------------------------------------- /accounts/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.contrib.auth import login, logout, authenticate 3 | from .forms import RegisterForm, LoginForm, ChangeIconForm, PasswordChangeForm 4 | from django.contrib import messages 5 | from django.contrib.auth.decorators import login_required 6 | import os 7 | from django.conf import settings 8 | from PIL import Image 9 | from .models import User 10 | from problemset.models import Submission 11 | import random 12 | from rest_framework import generics, permissions 13 | from .serializers import RegisterSerializer, UserSerializer, UserDetailSerializer 14 | from rest_framework_simplejwt.authentication import JWTAuthentication 15 | 16 | 17 | def register(request): 18 | if request.user.is_authenticated: 19 | return redirect("home") 20 | if request.method == "POST": 21 | form = RegisterForm(request.POST, request.FILES) 22 | if form.is_valid(): 23 | user = form.save(commit=False) 24 | user.account_type = 2 if form.cleaned_data.get("is_business") else 1 25 | user.icon_id = random.randint(10000000, 99999999) 26 | if form.cleaned_data.get("icon"): 27 | icon = form.cleaned_data.get("icon") 28 | icon64_dir = os.path.join( 29 | settings.BASE_DIR, "media", "users_icons", "icon64" 30 | ) 31 | icon170_dir = os.path.join( 32 | settings.BASE_DIR, "media", "users_icons", "icon170" 33 | ) 34 | os.makedirs(icon64_dir, exist_ok=True) 35 | os.makedirs(icon170_dir, exist_ok=True) 36 | 37 | icon64_path = os.path.join(icon64_dir, f"{user.icon_id}.png") 38 | icon170_path = os.path.join(icon170_dir, f"{user.icon_id}.png") 39 | 40 | img = Image.open(icon) 41 | img = img.resize((64, 64)) 42 | img.save(icon64_path) 43 | icon.seek(0) 44 | img170 = Image.open(icon) 45 | img170 = img170.resize((170, 170)) 46 | img170.save(icon170_path) 47 | else: 48 | user.icon_id = -user.icon_id 49 | 50 | user.save() 51 | login(request, user) 52 | return redirect("home") 53 | else: 54 | messages.error(request, "Please correct the error below.") 55 | else: 56 | form = RegisterForm() 57 | return render( 58 | request, 59 | "accounts/register.html", 60 | {"form": form, "navbar_item_id": -1, "title": "Registration | WnSOJ"}, 61 | ) 62 | 63 | 64 | def user_login(request): 65 | if request.user.is_authenticated: 66 | return redirect("home") 67 | if request.method == "POST": 68 | form = LoginForm(request, data=request.POST) 69 | if form.is_valid(): 70 | user = authenticate( 71 | username=form.cleaned_data.get("username"), 72 | password=form.cleaned_data.get("password"), 73 | ) 74 | if user is not None: 75 | login(request, user) 76 | return redirect("home") 77 | messages.error(request, "Invalid username or password.") 78 | else: 79 | form = LoginForm() 80 | return render( 81 | request, 82 | "accounts/login.html", 83 | { 84 | "form": form, 85 | "navbar_item_id": -1, 86 | "title": "Authorization | WnSOJ", 87 | }, 88 | ) 89 | 90 | 91 | @login_required 92 | def user_logout(request): 93 | logout(request) 94 | return redirect("home") 95 | 96 | 97 | @login_required 98 | def edit_profile(request): 99 | user = request.user 100 | if request.method == "POST": 101 | if "password_change_submit" in request.POST: 102 | password_form = PasswordChangeForm(user, request.POST) 103 | if password_form.is_valid(): 104 | user = password_form.save() 105 | login(request, user) 106 | messages.success(request, "Password updated successfully.") 107 | return redirect("edit_profile") 108 | else: 109 | messages.error(request, "Please correct the errors below.") 110 | elif "change_icon_submit" in request.POST: 111 | icon = request.FILES.get("icon") 112 | if icon: 113 | user.icon_id = abs(user.icon_id) 114 | icon64_dir = os.path.join( 115 | settings.BASE_DIR, "media", "users_icons", "icon64" 116 | ) 117 | icon170_dir = os.path.join( 118 | settings.BASE_DIR, "media", "users_icons", "icon170" 119 | ) 120 | os.makedirs(icon64_dir, exist_ok=True) 121 | os.makedirs(icon170_dir, exist_ok=True) 122 | 123 | icon64_path = os.path.join(icon64_dir, f"{user.icon_id}.png") 124 | icon170_path = os.path.join(icon170_dir, f"{user.icon_id}.png") 125 | 126 | img = Image.open(icon) 127 | img = img.resize((64, 64)) 128 | img.save(icon64_path) 129 | 130 | icon.seek(0) 131 | img170 = Image.open(icon) 132 | img170 = img170.resize((170, 170)) 133 | img170.save(icon170_path) 134 | 135 | messages.success(request, "Icon updated successfully.") 136 | return redirect("edit_profile") 137 | 138 | return render( 139 | request, 140 | "accounts/edit_profile.html", 141 | { 142 | "user": user, 143 | "navbar_item_id": -1, 144 | "title": "Edit Profile | WnSOJ", 145 | "change_icon_form": ChangeIconForm(), 146 | "password_change_form": PasswordChangeForm(user), 147 | }, 148 | ) 149 | 150 | 151 | def profile(request, username): 152 | user = get_object_or_404(User, username=username) 153 | 154 | params = { 155 | "navbar_item_id": -1, 156 | "title": f"{user.username}'s profile | WnSOJ", 157 | "profile_user": user, 158 | } 159 | 160 | submissions = Submission.objects.filter(user_id=user.id).order_by("-id") 161 | all_submissions = list(submissions) 162 | params["submissions"] = all_submissions[:10] 163 | 164 | params["cnt"] = {"AC": 0, "CE": 0, "WA": 0, "TLE": 0, "MLE": 0, "RE": 0} 165 | 166 | for submission in all_submissions: 167 | if submission.verdict == "IQ": 168 | continue 169 | params["cnt"][submission.verdict.split()[0]] += 1 170 | 171 | return render(request, "accounts/profile.html", params) 172 | 173 | 174 | class RegisterAPIView(generics.CreateAPIView): 175 | """ 176 | API view to register users. 177 | """ 178 | 179 | queryset = User.objects.all() 180 | permission_classes = (permissions.AllowAny,) 181 | serializer_class = RegisterSerializer 182 | 183 | 184 | class UserDetailAPIView(generics.RetrieveUpdateAPIView): 185 | """ 186 | API view to retrieve and update user details. 187 | Only the authenticated user can access their own details. 188 | """ 189 | 190 | serializer_class = UserDetailSerializer 191 | permission_classes = [permissions.IsAuthenticated] 192 | authentication_classes = [JWTAuthentication] 193 | 194 | def get_object(self): 195 | return self.request.user 196 | -------------------------------------------------------------------------------- /templates/accounts/profile.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block content %} 3 |
4 |
5 |
6 |
7 |
8 |
9 | {{ profile_user.username }}'s profile picture 10 |
11 |

{{ profile_user.username }}

12 |
13 | {% if profile_user.account_type != 1 %} 14 | {{ user.username }}'s jobs 15 | {% endif %} 16 |
17 |
18 | {% if user.id == profile_user.id %} 19 | 20 | Edit Profile 21 | 22 | {% endif %} 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Full Name
33 |
34 |
{{ profile_user.first_name + " " + profile_user.last_name }}
35 |
36 |
37 |
38 |
39 |
Email
40 |
41 |
{{ profile_user.email }}
42 |
43 |
44 |
45 |
46 |
Phone
47 |
48 |
{{ profile_user.phone_number }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
Submission Stats
57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |
VerdictCount
Accepted (AC){{ cnt['AC'] }}
Compilation Error (CE){{ cnt['CE'] }}
Runtime Error (RE){{ cnt['RE'] }}
Wrong Answer (WA){{ cnt['WA'] }}
Time Limit (TLE){{ cnt['TLE'] }}
Memory Limit (MLE){{ cnt['MLE'] }}
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |

Recent Submissions

100 |
101 |
102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | {% for item in submissions %} 118 | 119 | 120 | 121 | 122 | 123 | 124 | {% if item.verdict == 'AC' %} 125 | 126 | {% elif item.verdict == 'CE' %} 127 | 128 | {% elif item.verdict == 'IQ' %} 129 | 130 | {% else %} 131 | 132 | {% endif %} 133 | 134 | 135 | 136 | {% endfor %} 137 | 138 |
IDSending timeUserProblemLanguageVerdictTimeMemory
{{ item.id }}{{ item.send_time.strftime("%b/%d/%Y %H:%M") }}{{ item.user.username }}{{ item.problem.title }}{{ item.language }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.verdict }}{{ item.time }} ms{{ item.memory }} KB
139 |
140 |
141 |
142 |
143 |
144 |
145 | {% endblock %} -------------------------------------------------------------------------------- /jobboard/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from .models import Job 3 | from .forms import AddJobForm 4 | from rest_framework import viewsets, permissions 5 | from .serializers import JobSerializer 6 | from rest_framework_simplejwt.authentication import JWTAuthentication 7 | 8 | 9 | def format_number(value): 10 | """Format large numbers with k (thousands) or M (millions) suffix.""" 11 | try: 12 | value = float(value) 13 | if value >= 1000000000: 14 | return f"{value / 1000000000:.1f}B".replace(".0B", "B") 15 | if value >= 1000000: 16 | return f"{value / 1000000:.1f}M".replace(".0M", "M") 17 | elif value >= 1000: 18 | return f"{value / 1000:.1f}k".replace(".0k", "k") 19 | else: 20 | return str(int(value)) 21 | except (ValueError, TypeError): 22 | return str(value) 23 | 24 | 25 | def jobs(request): 26 | jobs = Job.objects.all() 27 | return render( 28 | request, 29 | "jobboard/jobs.html", 30 | { 31 | "title": "Jobs | WnSOJ", 32 | "navbar_item_id": 3, 33 | "jobs": jobs, 34 | "format_number": format_number, 35 | }, 36 | ) 37 | 38 | 39 | def add_job(request): 40 | if request.method == "POST": 41 | form = AddJobForm(request.POST) 42 | if form.is_valid() and request.user.account_type != 1: 43 | min_salary = request.POST.get("min_salary", "") 44 | max_salary = request.POST.get("max_salary", "") 45 | currency = request.POST.get("currency", "$") 46 | 47 | salary_error = None 48 | if min_salary and max_salary and float(min_salary) > float(max_salary): 49 | salary_error = "Minimum salary cannot be greater than maximum salary." 50 | 51 | if not salary_error: 52 | job = form.save(commit=False) 53 | job.user = request.user 54 | 55 | salary_range = {} 56 | if min_salary: 57 | salary_range["min"] = min_salary 58 | else: 59 | salary_range["min"] = 0 60 | 61 | if max_salary: 62 | salary_range["max"] = max_salary 63 | 64 | salary_range["currency"] = currency 65 | 66 | job.salary_range = salary_range 67 | job.save() 68 | return redirect("jobs") 69 | else: 70 | return render( 71 | request, 72 | "jobboard/add_job.html", 73 | { 74 | "title": "Add Job | WnSOJ", 75 | "navbar_item_id": 3, 76 | "form": form, 77 | "min_salary": min_salary, 78 | "max_salary": max_salary, 79 | "currency": currency, 80 | "salary_error": salary_error, 81 | }, 82 | ) 83 | else: 84 | form = AddJobForm() 85 | return render( 86 | request, 87 | "jobboard/add_job.html", 88 | {"title": "Add Job | WnSOJ", "navbar_item_id": 3, "form": form}, 89 | ) 90 | 91 | 92 | def job(request, job_id): 93 | job = Job.objects.get(id=job_id) 94 | return render( 95 | request, 96 | "jobboard/job.html", 97 | { 98 | "title": job.title + " | WnSOJ", 99 | "navbar_item_id": 3, 100 | "job": job, 101 | "format_number": format_number, 102 | }, 103 | ) 104 | 105 | 106 | def edit_job(request, job_id): 107 | job = get_object_or_404(Job, id=job_id) 108 | 109 | if not request.user.is_authenticated or ( 110 | not request.user.is_staff or request.user != job.user 111 | ): 112 | return redirect("job", job_id=job_id) 113 | 114 | min_salary = job.salary_range.get("min", "") if job.salary_range else "" 115 | max_salary = job.salary_range.get("max", "") if job.salary_range else "" 116 | currency = job.salary_range.get("currency", "$") if job.salary_range else "$" 117 | 118 | if request.method == "POST": 119 | form = AddJobForm(request.POST, instance=job) 120 | if form.is_valid(): 121 | min_salary = request.POST.get("min_salary", "") 122 | max_salary = request.POST.get("max_salary", "") 123 | currency = request.POST.get("currency", "$") 124 | 125 | salary_error = None 126 | if min_salary and max_salary and float(min_salary) > float(max_salary): 127 | salary_error = "Minimum salary cannot be greater than maximum salary." 128 | 129 | if not salary_error: 130 | job_instance = form.save(commit=False) 131 | 132 | salary_range = {} 133 | if min_salary: 134 | salary_range["min"] = min_salary 135 | else: 136 | salary_range["min"] = 0 137 | 138 | if max_salary: 139 | salary_range["max"] = max_salary 140 | 141 | salary_range["currency"] = currency 142 | 143 | job_instance.salary_range = salary_range 144 | job_instance.save() 145 | return redirect("job", job_id=job_id) 146 | else: 147 | return render( 148 | request, 149 | "jobboard/add_job.html", 150 | { 151 | "title": f"Edit {job.title} | WnSOJ", 152 | "navbar_item_id": 3, 153 | "form": form, 154 | "job": job, 155 | "min_salary": min_salary, 156 | "max_salary": max_salary, 157 | "currency": currency, 158 | "is_edit": True, 159 | "salary_error": salary_error, 160 | }, 161 | ) 162 | else: 163 | form = AddJobForm(instance=job) 164 | 165 | return render( 166 | request, 167 | "jobboard/add_job.html", 168 | { 169 | "title": f"Edit {job.title} | WnSOJ", 170 | "navbar_item_id": 3, 171 | "form": form, 172 | "job": job, 173 | "min_salary": min_salary, 174 | "max_salary": max_salary, 175 | "currency": currency, 176 | "is_edit": True, 177 | }, 178 | ) 179 | 180 | 181 | def delete_job(request, job_id): 182 | job = get_object_or_404(Job, id=job_id) 183 | if not request.user.is_authenticated or ( 184 | not request.user.is_staff or request.user != job.user 185 | ): 186 | return redirect("job", job_id=job_id) 187 | job.delete() 188 | return redirect("jobs") 189 | 190 | 191 | class JobAPIViewSet(viewsets.ModelViewSet): 192 | """ 193 | ViewSet for viewing and editing jobs. 194 | - List, Retrieve: available to all users. 195 | - Create, Update, Destroy: restricted to the job's owner or admin users. 196 | """ 197 | 198 | queryset = Job.objects.all().order_by("-created_at") 199 | serializer_class = JobSerializer 200 | authentication_classes = [JWTAuthentication] 201 | 202 | def get_permissions(self): 203 | if self.action in ["create"]: 204 | self.permission_classes = [permissions.IsAuthenticated] 205 | elif self.action in ["update", "partial_update", "destroy"]: 206 | self.permission_classes = [permissions.IsAuthenticated] 207 | else: 208 | self.permission_classes = [permissions.AllowAny] 209 | return super().get_permissions() 210 | 211 | def perform_create(self, serializer): 212 | serializer.save(user=self.request.user) 213 | 214 | def perform_update(self, serializer): 215 | job = self.get_object() 216 | if self.request.user != job.user and not self.request.user.is_staff: 217 | raise permissions.PermissionDenied( 218 | "You do not have permission to edit" + "this job." 219 | ) 220 | serializer.save() 221 | 222 | def perform_destroy(self, instance): 223 | if self.request.user != instance.user and not self.request.user.is_staff: 224 | raise permissions.PermissionDenied( 225 | "You do not have permission to delete" + "this job." 226 | ) 227 | instance.delete() 228 | -------------------------------------------------------------------------------- /static/css/problems.css: -------------------------------------------------------------------------------- 1 | /* Problem-related styling */ 2 | 3 | .problem-container { 4 | padding: 0.75rem 0; 5 | } 6 | 7 | .problem-header { 8 | margin-bottom: 2rem; 9 | } 10 | 11 | .problem-title { 12 | font-size: 1.8rem; 13 | font-weight: 600; 14 | color: #3a5bbf; 15 | margin-bottom: 0.8rem; 16 | } 17 | 18 | .problem-meta { 19 | display: flex; 20 | flex-wrap: wrap; 21 | gap: 1rem; 22 | margin-bottom: 1.5rem; 23 | } 24 | 25 | .problem-meta-item { 26 | display: flex; 27 | align-items: center; 28 | margin-right: 1.2rem; 29 | } 30 | 31 | .problem-difficulty { 32 | font-weight: 500; 33 | } 34 | 35 | .problem-difficulty-easy { 36 | color: #28a745; 37 | } 38 | 39 | .problem-difficulty-medium { 40 | color: #fd7e14; 41 | } 42 | 43 | .problem-difficulty-hard { 44 | color: #dc3545; 45 | } 46 | 47 | .problem-description { 48 | margin-bottom: 2rem; 49 | line-height: 1.6; 50 | } 51 | 52 | .problem-section { 53 | margin-bottom: 1.5rem; 54 | } 55 | 56 | .problem-section-title { 57 | font-size: 1.2rem; 58 | font-weight: 600; 59 | margin-bottom: 0.8rem; 60 | color: #495057; 61 | } 62 | 63 | .problem-section-header { 64 | font-size: 1.8rem; 65 | font-weight: 600; 66 | margin-bottom: 1.2rem; 67 | color: #3a5bbf; 68 | padding-bottom: 0.5rem; 69 | border-bottom: 2px solid #e9ecef; 70 | } 71 | 72 | .problem-subsection-header { 73 | font-size: 1.3rem; 74 | font-weight: 600; 75 | margin: 1.5rem 0 0.8rem; 76 | color: #495057; 77 | } 78 | 79 | .problem-example { 80 | background-color: #f8f9fa; 81 | border-radius: 0.5rem; 82 | padding: 1rem; 83 | margin-bottom: 1rem; 84 | } 85 | 86 | .problem-card { 87 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.07); 88 | transition: all 0.3s ease-in-out; 89 | border-radius: 0.5rem; 90 | border: 0; 91 | overflow: hidden; 92 | } 93 | 94 | .problem-card-header { 95 | padding: 1rem 1.25rem; 96 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 97 | } 98 | 99 | .problem-card-body { 100 | padding: 1.5rem; 101 | } 102 | 103 | .problem-form-group { 104 | margin-bottom: 1.5rem; 105 | } 106 | 107 | .problem-form-label { 108 | margin-bottom: 0.5rem; 109 | color: #495057; 110 | font-weight: 600; 111 | display: inline-block; 112 | } 113 | 114 | .problem-input-group { 115 | position: relative; 116 | display: flex; 117 | flex-wrap: wrap; 118 | align-items: stretch; 119 | width: 100%; 120 | } 121 | 122 | .problem-input-group .input-group-text { 123 | background-color: #f8f9fa; 124 | border-right: none; 125 | } 126 | 127 | .problem-input-group .form-control { 128 | border-left: 0; 129 | } 130 | 131 | .problem-input-group:focus-within { 132 | box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.2); 133 | } 134 | 135 | .problem-input-group:focus-within .input-group-text, 136 | .problem-input-group:focus-within .form-control { 137 | border-color: #86b7fe; 138 | } 139 | 140 | .problem-alert { 141 | border-left: 4px solid #dc3545; 142 | border-radius: 0.25rem; 143 | padding: 0.6rem 1rem; 144 | text-align: center; 145 | font-size: 0.9rem; 146 | } 147 | 148 | .problem-help-text { 149 | font-size: 0.85rem; 150 | margin-top: 0.25rem; 151 | color: #6c757d; 152 | } 153 | 154 | .problem-btn { 155 | padding: 0.5rem 2rem; 156 | font-weight: 500; 157 | } 158 | 159 | .problem-submit { 160 | margin-top: 2rem; 161 | } 162 | 163 | .problem-table { 164 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.05); 165 | border-radius: 0.5rem; 166 | overflow: hidden; 167 | margin-bottom: 2rem; 168 | } 169 | 170 | .problem-table th { 171 | background-color: #f8f9fa; 172 | font-weight: 600; 173 | padding: 0.75rem; 174 | } 175 | 176 | .problem-table tr { 177 | transition: background-color 0.2s; 178 | } 179 | 180 | .problem-table tr:hover { 181 | background-color: #f8f9fa; 182 | } 183 | 184 | .problem-status { 185 | font-weight: 500; 186 | text-align: center; 187 | } 188 | 189 | .problem-submissions-table { 190 | margin-top: 1rem; 191 | box-shadow: 0 2px 10px rgba(0,0,0,0.05); 192 | border-radius: 0.5rem; 193 | overflow: hidden; 194 | } 195 | 196 | .problem-submissions-table th { 197 | background-color: #f8f9fa; 198 | font-weight: 600; 199 | } 200 | 201 | .problem-category-header { 202 | display: flex; 203 | justify-content: space-between; 204 | align-items: center; 205 | margin-bottom: 1.5rem; 206 | } 207 | 208 | .problem-category-section { 209 | margin-bottom: 2.5rem; 210 | } 211 | 212 | .problem-category-card { 213 | transition: all 0.3s ease; 214 | border: none; 215 | border-radius: 0.75rem; 216 | overflow: hidden; 217 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.07); 218 | } 219 | 220 | .problem-category-card:hover { 221 | transform: translateY(-5px); 222 | box-shadow: 0 10px 20px rgba(0,0,0,0.1); 223 | } 224 | 225 | .problem-card-img-container { 226 | height: 160px; 227 | overflow: hidden; 228 | display: flex; 229 | align-items: center; 230 | justify-content: center; 231 | background-color: #f8f9fa; 232 | } 233 | 234 | .problem-card-img-container img { 235 | max-height: 100%; 236 | width: auto; 237 | max-width: 100%; 238 | object-fit: contain; 239 | transition: transform 0.3s ease; 240 | } 241 | 242 | .problem-category-card:hover .problem-card-img-container img { 243 | transform: scale(1.05); 244 | } 245 | 246 | .problem-card-footer { 247 | background-color: transparent; 248 | border-top: 1px solid #f0f0f0; 249 | padding: 0.75rem 1.25rem; 250 | margin: 0; 251 | font-weight: 500; 252 | } 253 | 254 | .problem-navbar { 255 | margin-bottom: 1rem; 256 | border-bottom: 1px solid #dee2e6; 257 | } 258 | 259 | .problem-nav-link { 260 | color: #6c757d; 261 | font-weight: 500; 262 | border-radius: 0.25rem 0.25rem 0 0; 263 | padding: 0.5rem 1rem; 264 | } 265 | 266 | .problem-nav-link.active { 267 | color: #3a5bbf; 268 | border-bottom: 2px solid #3a5bbf; 269 | background-color: transparent; 270 | } 271 | 272 | .problem-nav-link:hover:not(.active) { 273 | color: #3a5bbf; 274 | background-color: #f8f9fa; 275 | } 276 | 277 | .problem-submission-header { 278 | margin-bottom: 2rem; 279 | padding-bottom: 1rem; 280 | border-bottom: 1px solid #e9ecef; 281 | } 282 | 283 | .problem-verdict-container { 284 | display: flex; 285 | align-items: center; 286 | margin: 1rem 0; 287 | } 288 | 289 | .problem-verdict-label { 290 | font-weight: 600; 291 | margin-right: 1rem; 292 | } 293 | 294 | .problem-verdict { 295 | padding: 0.5rem 1.5rem; 296 | font-weight: bold; 297 | border-radius: 0.25rem; 298 | text-align: center; 299 | min-width: 100px; 300 | } 301 | 302 | .problem-verdict-ac { 303 | background-color: #d4edda; 304 | color: #155724; 305 | } 306 | 307 | .problem-verdict-ce { 308 | background-color: #fff3cd; 309 | color: #856404; 310 | } 311 | 312 | .problem-verdict-queue { 313 | background-color: #e2e3e5; 314 | color: #383d41; 315 | } 316 | 317 | .problem-verdict-error { 318 | background-color: #f8d7da; 319 | color: #721c24; 320 | } 321 | 322 | .problem-stats { 323 | display: flex; 324 | gap: 2rem; 325 | margin-top: 1rem; 326 | } 327 | 328 | .problem-stat-item { 329 | display: flex; 330 | align-items: center; 331 | } 332 | 333 | .problem-stat-label { 334 | font-weight: 600; 335 | margin-right: 0.5rem; 336 | } 337 | 338 | .problem-stat-value { 339 | font-family: monospace; 340 | font-size: 1.1rem; 341 | } 342 | 343 | .problem-code-container { 344 | background-color: #f8f9fa; 345 | border-radius: 0.5rem; 346 | padding: 1rem; 347 | margin: 1rem 0; 348 | overflow: auto; 349 | } 350 | 351 | .problem-code-block { 352 | background-color: transparent; 353 | border: none; 354 | padding: 0; 355 | margin: 0; 356 | font-family: monospace; 357 | font-size: 0.9rem; 358 | line-height: 1.5; 359 | white-space: pre-wrap; 360 | } 361 | 362 | .problem-error-block { 363 | background-color: #f8d7da; 364 | border-radius: 0.25rem; 365 | padding: 1rem; 366 | color: #721c24; 367 | font-family: monospace; 368 | font-size: 0.9rem; 369 | white-space: pre-wrap; 370 | } 371 | 372 | .problem-tests-table { 373 | margin-top: 1rem; 374 | } 375 | 376 | .problem-categories-divider { 377 | margin: 2rem 0; 378 | height: 2px; 379 | background-color: #f0f0f0; 380 | } 381 | -------------------------------------------------------------------------------- /problemset/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, redirect, get_object_or_404 2 | from django.templatetags.static import static 3 | from django.contrib.auth.decorators import login_required 4 | from django.http import HttpResponseForbidden 5 | from .models import Category, Problem, Submission 6 | from .forms import AddProblemForm, SubmitForm 7 | import os 8 | from zipfile import ZipFile 9 | from io import BytesIO 10 | from rest_framework import viewsets, permissions 11 | from .serializers import CategorySerializer, ProblemSerializer, SubmissionSerializer 12 | 13 | 14 | def home_page(request): 15 | return render( 16 | request, 17 | "index.html", 18 | { 19 | "title": "Home | WnSOJ", 20 | "navbar_item_id": 1, 21 | "card1": static("img/main_page_card1.svg"), 22 | "card2": static("img/main_page_card2.svg"), 23 | "card3": static("img/main_page_card3.svg"), 24 | }, 25 | ) 26 | 27 | 28 | def categories(request): 29 | return render( 30 | request, 31 | "problemset/problems_list.html", 32 | { 33 | "title": "Problems | WnSOJ", 34 | "navbar_item_id": 2, 35 | "categories": list(Category.objects.all()), 36 | "show_categories": True, 37 | }, 38 | ) 39 | 40 | 41 | def problems(request, category): 42 | cat = get_object_or_404(Category, short_name=category) 43 | return render( 44 | request, 45 | "problemset/problems_list.html", 46 | { 47 | "title": f"{cat.long_name} | WnSOJ", 48 | "navbar_item_id": 2, 49 | "problems": cat.problems.all(), 50 | }, 51 | ) 52 | 53 | 54 | @login_required 55 | def add_problem(request): 56 | if not request.user.is_staff: 57 | return HttpResponseForbidden() 58 | 59 | form = AddProblemForm() 60 | if request.method == "POST": 61 | form = AddProblemForm(request.POST, request.FILES) 62 | if form.is_valid(): 63 | problem = Problem( 64 | time_limit=form.cleaned_data["time_limit"], 65 | memory_limit=form.cleaned_data["memory_limit"], 66 | title=form.cleaned_data["title"], 67 | statement=form.cleaned_data["statement"], 68 | editorial=form.cleaned_data["editorial"], 69 | code=form.cleaned_data["solution"], 70 | ) 71 | problem.save() 72 | 73 | os.makedirs(f"data/problems/{problem.id}", exist_ok=True) 74 | with ZipFile(BytesIO(request.FILES["test_data"].read()), "r") as file: 75 | file.extractall(f"data/problems/{problem.id}") 76 | 77 | selected_categories = form.cleaned_data["categories"] 78 | for category in selected_categories: 79 | problem.categories.add(category) 80 | 81 | problem.categories.add(Category.objects.get(short_name="problemset")) 82 | 83 | return redirect("problems") 84 | 85 | context = {"title": "Add Problem | WnSOJ", "navbar_item_id": 2, "form": form} 86 | 87 | return render(request, "problemset/add_problem.html", context) 88 | 89 | 90 | def problem_statement(request, problem_id): 91 | problem = get_object_or_404(Problem, id=problem_id) 92 | form = SubmitForm() 93 | if request.method == "POST": 94 | form = SubmitForm(request.POST, request.FILES) 95 | if form.is_valid(): 96 | if request.user.is_authenticated: 97 | submission = Submission( 98 | problem=problem, 99 | user=request.user, 100 | language=form.cleaned_data["language"], 101 | code=form.cleaned_data["code"], 102 | verdict="IQ", 103 | ) 104 | submission.save() 105 | username = request.user.username 106 | return redirect( 107 | f"/problem/{problem_id}/submissions?username={username}" 108 | ) 109 | else: 110 | return redirect("login") 111 | return render( 112 | request, 113 | "problemset/problem.html", 114 | { 115 | "title": f"{problem.title} | WnSOJ", 116 | "current_bar_id": 1, 117 | "navbar_item_id": 2, 118 | "problem": problem, 119 | "form": form, 120 | }, 121 | ) 122 | 123 | 124 | def problem_editorial(request, problem_id): 125 | problem = get_object_or_404(Problem, id=problem_id) 126 | return render( 127 | request, 128 | "problemset/editorial.html", 129 | { 130 | "title": f"{problem.title} | WnSOJ", 131 | "navbar_item_id": 2, 132 | "current_bar_id": 2, 133 | "problem": problem, 134 | }, 135 | ) 136 | 137 | 138 | def problem_submissions_list(request, problem_id): 139 | problem = get_object_or_404(Problem, id=problem_id) 140 | submissions = Submission.objects.filter(problem=problem) 141 | 142 | if "user" in request.GET and request.GET["user"]: 143 | submissions = submissions.filter(user__username=request.GET["user"]) 144 | 145 | if "verdict" in request.GET and request.GET["verdict"]: 146 | submissions = submissions.filter(verdict=request.GET["verdict"]) 147 | 148 | submissions = submissions.order_by("-id")[:10] 149 | 150 | return render( 151 | request, 152 | "problemset/problem_submissions.html", 153 | { 154 | "title": "Submissions | WnSOJ", 155 | "navbar_item_id": 2, 156 | "submissions": list(submissions), 157 | "problem": problem, 158 | "current_bar_id": 3, 159 | }, 160 | ) 161 | 162 | 163 | def submissions(request): 164 | submissions = Submission.objects.all() 165 | 166 | if "username" in request.GET and request.GET["username"]: 167 | submissions = submissions.filter(user__username=request.GET["username"]) 168 | 169 | if "verdict" in request.GET and request.GET["verdict"]: 170 | submissions = submissions.filter(verdict=request.GET["verdict"]) 171 | 172 | submissions = submissions.order_by("-id")[:10] 173 | 174 | return render( 175 | request, 176 | "problemset/submissions_list.html", 177 | { 178 | "title": "Submissions | WnSOJ", 179 | "navbar_item_id": 2, 180 | "submissions": list(submissions), 181 | }, 182 | ) 183 | 184 | 185 | def submission(request, submission_id): 186 | submission = get_object_or_404(Submission, id=submission_id) 187 | return render( 188 | request, 189 | "problemset/submission.html", 190 | {"title": "Submission | WnSOJ", "navbar_item_id": 2, "item": submission}, 191 | ) 192 | 193 | 194 | def faq(request): 195 | return render(request, "faq.html", {"title": "FAQ | WnSOJ", "navbar_item_id": 4}) 196 | 197 | 198 | class CategoryAPIViewSet(viewsets.ReadOnlyModelViewSet): 199 | queryset = Category.objects.all() 200 | serializer_class = CategorySerializer 201 | permission_classes = [permissions.AllowAny] 202 | 203 | 204 | class ProblemAPIViewSet(viewsets.ModelViewSet): 205 | """ 206 | ViewSet for viewing and editing problems. 207 | - List, Retrieve: available to all users. 208 | - Create, Update, Destroy: restricted to admin users. 209 | """ 210 | 211 | queryset = Problem.objects.all() 212 | serializer_class = ProblemSerializer 213 | 214 | def get_permissions(self): 215 | if self.action in ["create", "update", "partial_update", "destroy"]: 216 | self.permission_classes = [permissions.IsAdminUser] 217 | else: 218 | self.permission_classes = [permissions.AllowAny] 219 | return super().get_permissions() 220 | 221 | 222 | class SubmissionAPIViewSet(viewsets.ModelViewSet): 223 | """ 224 | ViewSet for viewing and creating submissions. 225 | - List: available to authenticated users. 226 | - Create: available to authenticated users. 227 | - Retrieve: available to the submitting user or admin. 228 | - Update, Destroy: restricted to admin users. 229 | """ 230 | 231 | queryset = Submission.objects.all() 232 | serializer_class = SubmissionSerializer 233 | 234 | def get_queryset(self): 235 | user = self.request.user 236 | if user.is_authenticated: 237 | return Submission.objects.filter(user=user) 238 | return Submission.objects.none() 239 | 240 | def perform_create(self, serializer): 241 | serializer.save(user=self.request.user, verdict="IQ") 242 | 243 | def get_permissions(self): 244 | if self.action in ["update", "partial_update", "destroy"]: 245 | self.permission_classes = [permissions.IsAdminUser] 246 | else: 247 | self.permission_classes = [permissions.IsAuthenticated] 248 | return super().get_permissions() 249 | -------------------------------------------------------------------------------- /static/css/accounts.css: -------------------------------------------------------------------------------- 1 | /* Account-related styling */ 2 | 3 | .accounts-container { 4 | max-width: 600px; 5 | margin: 0 auto; 6 | } 7 | 8 | .accounts-card { 9 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.07); 10 | transition: all 0.3s ease-in-out; 11 | border-radius: 0.5rem; 12 | position: relative; 13 | background-color: #fff; 14 | border: 0 solid rgba(0, 0, 0, 0.125); 15 | overflow: hidden; 16 | } 17 | 18 | .accounts-card-header { 19 | padding: 1rem 1.25rem; 20 | border-bottom: 1px solid rgba(0, 0, 0, 0.125); 21 | } 22 | 23 | .accounts-card-body { 24 | flex: 1 1 auto; 25 | min-height: 1px; 26 | padding: 1.25rem; 27 | } 28 | 29 | .accounts-form-group { 30 | margin-bottom: 1rem; 31 | } 32 | 33 | .accounts-form-label { 34 | margin-bottom: 0.5rem; 35 | color: #495057; 36 | font-weight: 600; 37 | display: inline-block; 38 | } 39 | 40 | .accounts-input-group { 41 | position: relative; 42 | display: flex; 43 | flex-wrap: wrap; 44 | align-items: stretch; 45 | width: 100%; 46 | border-radius: 0.25rem; 47 | } 48 | 49 | .accounts-input-group .input-group-text { 50 | background-color: transparent; 51 | border-right: none; 52 | color: #6c757d; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | width: 40px; 57 | border-top-left-radius: 0.25rem; 58 | border-bottom-left-radius: 0.25rem; 59 | border-color: #ced4da; 60 | } 61 | 62 | .accounts-input-group .form-control { 63 | border-left: 0; 64 | border-top-left-radius: 0; 65 | border-bottom-left-radius: 0; 66 | flex: 1 1 auto; 67 | border-color: #ced4da; 68 | background-color: #fff; 69 | } 70 | 71 | .accounts-input-group .form-control:focus { 72 | border-color: #86b7fe; 73 | box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15); 74 | border-left: 0; 75 | z-index: 5; 76 | } 77 | 78 | .accounts-input-group .form-control:focus + .input-group-text, 79 | .accounts-input-group .input-group-text + .form-control:focus { 80 | border-color: #86b7fe; 81 | z-index: 5; 82 | } 83 | 84 | .accounts-input-group:focus-within { 85 | box-shadow: none; 86 | } 87 | 88 | .accounts-input-group input[type="file"] { 89 | padding: 0.25rem 0.5rem; 90 | line-height: 1.5; 91 | height: auto; 92 | } 93 | 94 | .accounts-input-group input[type="file"]::file-selector-button { 95 | margin: -0.25rem 0.5rem -0.25rem -0.5rem; 96 | padding: 0.25rem 0.5rem; 97 | height: calc(1.5em + 0.5rem + 2px); 98 | background-color: #f0f0f0; 99 | } 100 | 101 | .accounts-alert { 102 | border-left: 4px solid #dc3545; 103 | border-radius: 0.25rem; 104 | padding: 0.6rem 1rem; 105 | text-align: center; 106 | font-size: 0.9rem; 107 | } 108 | 109 | .form-check { 110 | display: flex; 111 | align-items: center; 112 | padding-left: 0; 113 | margin-bottom: 1rem; 114 | position: relative; 115 | } 116 | 117 | .form-check input[type="checkbox"] { 118 | position: relative; 119 | margin-right: 8px; 120 | width: 16px !important; 121 | height: 16px !important; 122 | cursor: pointer; 123 | flex-shrink: 0; 124 | appearance: auto !important; 125 | -webkit-appearance: checkbox !important; 126 | opacity: 1; 127 | z-index: 1; 128 | } 129 | 130 | .form-check-label { 131 | margin-bottom: 0; 132 | cursor: pointer; 133 | user-select: none; 134 | } 135 | 136 | .accounts-checkbox, 137 | .custom-checkbox { 138 | display: flex; 139 | align-items: center; 140 | } 141 | 142 | .accounts-checkbox-label { 143 | display: flex; 144 | align-items: center; 145 | margin-bottom: 0; 146 | font-weight: normal; 147 | cursor: pointer; 148 | } 149 | 150 | .accounts-checkbox input[type="checkbox"] { 151 | margin-left: 8px; 152 | width: 16px; 153 | height: 16px; 154 | cursor: pointer; 155 | } 156 | 157 | .accounts-checkbox input[type="checkbox"], 158 | .custom-checkbox input[type="checkbox"] { 159 | position: relative; 160 | margin-right: 8px; 161 | width: 16px !important; 162 | height: 16px !important; 163 | margin-left: 0 !important; 164 | cursor: pointer; 165 | flex-shrink: 0; 166 | appearance: auto !important; 167 | -webkit-appearance: checkbox !important; 168 | opacity: 1; 169 | z-index: 1; 170 | } 171 | 172 | .accounts-submit { 173 | margin-top: 1.5rem; 174 | } 175 | 176 | .accounts-submit button { 177 | width: auto; 178 | } 179 | 180 | .accounts-btn { 181 | padding: 0.5rem 2rem; 182 | } 183 | 184 | .accounts-link { 185 | color: #4e73df; 186 | text-decoration: none; 187 | transition: all 0.2s ease; 188 | } 189 | 190 | .accounts-link:hover { 191 | color: #224abe; 192 | text-decoration: underline; 193 | } 194 | 195 | .accounts-footer { 196 | margin-top: 1.5rem; 197 | padding-top: 1rem; 198 | border-top: 1px solid #f0f0f0; 199 | text-align: center; 200 | } 201 | 202 | /* Profile Page Styles */ 203 | .profile-container { 204 | padding: 1.5rem 0; 205 | } 206 | 207 | .profile-img-container { 208 | text-align: center; 209 | margin-bottom: 1rem; 210 | } 211 | 212 | .profile-img-container img { 213 | border-radius: 50%; 214 | box-shadow: 0 3px 10px rgba(0,0,0,0.1); 215 | transition: all 0.3s ease; 216 | width: 150px; 217 | height: 150px; 218 | object-fit: cover; 219 | } 220 | 221 | .profile-img { 222 | border: 4px solid #fff; 223 | box-shadow: 0 2px 10px rgba(0,0,0,0.1); 224 | transition: all 0.3s ease; 225 | } 226 | 227 | .profile-img:hover { 228 | transform: scale(1.03); 229 | } 230 | 231 | .status-row { 232 | display: flex; 233 | justify-content: space-between; 234 | padding: 0.5rem 1rem; 235 | margin-bottom: 0.25rem; 236 | border-radius: 0.25rem; 237 | font-weight: 500; 238 | } 239 | 240 | .accounts-input-group, 241 | .input-group { 242 | position: relative; 243 | display: flex; 244 | flex-wrap: wrap; 245 | align-items: stretch; 246 | width: 100%; 247 | border-radius: 0.25rem; 248 | } 249 | 250 | .accounts-card .input-group-text, 251 | .accounts-input-group .input-group-text { 252 | background-color: transparent; 253 | border-right: none; 254 | color: #6c757d; 255 | display: flex; 256 | align-items: center; 257 | justify-content: center; 258 | width: 40px; 259 | border-top-left-radius: 0.25rem; 260 | border-bottom-left-radius: 0.25rem; 261 | border-color: #ced4da; 262 | } 263 | 264 | .accounts-card .input-group .form-control, 265 | .accounts-input-group .form-control { 266 | border-left: 0; 267 | border-top-left-radius: 0; 268 | border-bottom-left-radius: 0; 269 | flex: 1 1 auto; 270 | border-color: #ced4da; 271 | background-color: #fff; 272 | } 273 | 274 | .accounts-card .input-group .form-control:focus, 275 | .accounts-input-group .form-control:focus { 276 | border-color: #86b7fe; 277 | box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15); 278 | border-left: 0; 279 | z-index: 5; 280 | } 281 | 282 | .accounts-card .input-group:focus-within .input-group-text, 283 | .accounts-input-group:focus-within .input-group-text { 284 | border-color: #86b7fe; 285 | background-color: #f8f9fa; 286 | color: #4e73df; 287 | } 288 | 289 | .accounts-card .input-group:focus-within, 290 | .accounts-input-group:focus-within { 291 | box-shadow: none; 292 | } 293 | 294 | .accounts-card .input-group, 295 | .accounts-input-group { 296 | position: relative; 297 | display: flex; 298 | flex-wrap: wrap; 299 | align-items: stretch; 300 | width: 100%; 301 | border-radius: 0.25rem; 302 | } 303 | 304 | .accounts-card .input-group .input-group-text, 305 | .accounts-input-group .input-group-text { 306 | background-color: transparent !important; 307 | border-right: none; 308 | color: #6c757d; 309 | display: flex; 310 | align-items: center; 311 | justify-content: center; 312 | width: 40px; 313 | border-top-left-radius: 0.25rem; 314 | border-bottom-left-radius: 0.25rem; 315 | border-color: #ced4da; 316 | } 317 | 318 | .accounts-card .input-group .form-control, 319 | .accounts-input-group .form-control { 320 | border-left: 0; 321 | border-top-left-radius: 0; 322 | border-bottom-left-radius: 0; 323 | flex: 1 1 auto; 324 | border-color: #ced4da; 325 | background-color: #fff; 326 | } 327 | 328 | .accounts-card .input-group .form-control:focus, 329 | .accounts-input-group .form-control:focus { 330 | border-color: #86b7fe; 331 | box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15); 332 | border-left: 0; 333 | z-index: 5; 334 | } 335 | 336 | .accounts-card .input-group:focus-within .input-group-text, 337 | .accounts-input-group:focus-within .input-group-text { 338 | border-color: #86b7fe; 339 | background-color: transparent !important; 340 | color: #4e73df; 341 | } 342 | 343 | .accounts-card .input-group:focus-within, 344 | .accounts-input-group:focus-within { 345 | box-shadow: none !important; 346 | } 347 | 348 | 349 | .accounts-card .input-group input[type="file"]::file-selector-button, 350 | .accounts-input-group input[type="file"]::file-selector-button { 351 | background-color: #f0f0f0; 352 | } 353 | --------------------------------------------------------------------------------