diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy-test.yml similarity index 74% rename from .github/workflows/deploy.yml rename to .github/workflows/deploy-test.yml index 4446fa95..c8f5ac81 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy-test.yml @@ -1,16 +1,16 @@ -name: deploy +name: Deploy Test Environment -on: workflow_dispatch +on: push jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: wshihadeh/docker-deployment-action@v2 with: remote_docker_host: webstrom@server.strom.sk ssh_private_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PRIVATE_KEY }} ssh_public_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PUBLIC_KEY }} - stack_file_name: compose.yaml + stack_file_name: deployment/compose-test.yaml args: up --build --force-recreate --detach diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 8192b463..13698a6b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Upgrade pip run: pip3 install --upgrade pip @@ -37,7 +37,7 @@ jobs: # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Upgrade pip run: pip3 install --upgrade pip diff --git a/.github/workflows/migrate-test.yml b/.github/workflows/migrate-test.yml new file mode 100644 index 00000000..bdf85e4c --- /dev/null +++ b/.github/workflows/migrate-test.yml @@ -0,0 +1,16 @@ +name: Migrate Test Environment + +on: push + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: wshihadeh/docker-deployment-action@v2 + with: + remote_docker_host: webstrom@server.strom.sk + ssh_private_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PRIVATE_KEY }} + ssh_public_key: ${{ secrets.WEBSTROM_DEPLOY_SSH_PUBLIC_KEY }} + stack_file_name: deployment/compose-test.yaml + args: run webstrom-backend python manage.py migrate --noinput diff --git a/.gitignore b/.gitignore index a6d9e34b..b2a5c20a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,6 @@ db.sqlite3-journal /media /protected_media -**/migrations/** -!**/migrations/__init__.py - fixtures/sources !fixtures/sources/*.py !fixtures/sources/requirements.txt diff --git a/.vscode/settings.json b/.vscode/settings.json index 3784257a..05430d69 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,8 +6,11 @@ "editor.defaultFormatter": "ms-python.autopep8", "editor.formatOnSave": true, }, + "files.associations": { + "compose-*.yaml": "dockercompose" + }, "git.branchProtection": [ "master" ], "git.branchProtectionPrompt": "alwaysCommitToNewBranch" -} \ No newline at end of file +} diff --git a/Pipfile b/Pipfile index ad8d4a45..1bd6ba65 100644 --- a/Pipfile +++ b/Pipfile @@ -6,13 +6,14 @@ name = "pypi" [packages] daphne = "~=4.0.0" dj-rest-auth = "~=5.0.1" -django = "~=3.2.23" +django = "~=4.2.11" django-allauth = "~=0.58.2" django-filter = "~=23.5" django-sendfile2 = "~=0.7.1" djangorestframework = "~=3.14.0" drf-writable-nested = "~=0.7.0" pillow = "~=10.3.0" +psycopg = "~=3.1.18" python-magic = "~=0.4.27" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index 580c70b5..7a58bd09 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "7df0459bbb2186ea13bb8c25ce90a415fc188092d1f00f88ca5d6350af79aa21" + "sha256": "9ad21e237de67487379a6de0311773509ba85f5d3532b9d175b886953783853b" }, "pipfile-spec": 6, "requires": {}, @@ -277,11 +277,11 @@ }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4", + "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3" ], "index": "pypi", - "version": "==3.2.25" + "version": "==4.2.11" }, "django-allauth": { "hashes": [ @@ -418,6 +418,14 @@ "index": "pypi", "version": "==10.3.0" }, + "psycopg": { + "hashes": [ + "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", + "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e" + ], + "index": "pypi", + "version": "==3.1.18" + }, "pyasn1": { "hashes": [ "sha256:3a35ab2c4b5ef98e17dfdec8ab074046fbda76e281c5a706ccd82328cfc8f64c", @@ -752,11 +760,11 @@ }, "django": { "hashes": [ - "sha256:7ca38a78654aee72378594d63e51636c04b8e28574f5505dff630895b5472777", - "sha256:a52ea7fcf280b16f7b739cec38fa6d3f8953a5456986944c3ca97e79882b4e38" + "sha256:6e6ff3db2d8dd0c986b4eec8554c8e4f919b5c1ff62a5b4390c17aff2ed6e5c4", + "sha256:ddc24a0a8280a0430baa37aff11f28574720af05888c62b7cfe71d219f4599d3" ], "index": "pypi", - "version": "==3.2.25" + "version": "==4.2.11" }, "django-rest-swagger": { "hashes": [ diff --git a/cms/migrations/0001_initial.py b/cms/migrations/0001_initial.py new file mode 100644 index 00000000..4f62e71f --- /dev/null +++ b/cms/migrations/0001_initial.py @@ -0,0 +1,105 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +import base.models +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('sites', '0002_alter_domain_unique'), + ] + + operations = [ + migrations.CreateModel( + name='InfoBanner', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible_after', models.DateTimeField(verbose_name='Zobrazuj od')), + ('visible_until', models.DateTimeField(verbose_name='Zobrazuj do')), + ('message', models.CharField(help_text='Správa sa zobrazí v baneri. Správa musí byť stručná - jedna krátka veta.', max_length=200, verbose_name='správa')), + ], + options={ + 'verbose_name': 'Informácia v pohyblivom baneri', + 'verbose_name_plural': 'Informácie v pohyblivom baneri', + }, + ), + migrations.CreateModel( + name='Logo', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=150, verbose_name='názov loga')), + ('disabled', models.BooleanField()), + ('image', base.models.RestrictedFileField(upload_to='logo_images/', verbose_name='Logo')), + ], + options={ + 'verbose_name': 'logo', + 'verbose_name_plural': 'logá', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='MessageTemplate', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Pomenovanie generickej správy. Slúži pre orientáciu', max_length=50, verbose_name='Názov')), + ('message', models.CharField(help_text='Generické správy pre banner a posty', max_length=200, verbose_name='Generická správa')), + ('is_active', models.BooleanField(default=True, verbose_name='Aktívna')), + ], + options={ + 'verbose_name': 'Generické správy pre banner a posty', + 'verbose_name_plural': 'Generické správy pre banner a posty', + }, + ), + migrations.CreateModel( + name='Post', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('visible_after', models.DateTimeField(verbose_name='Zobrazuj od')), + ('visible_until', models.DateTimeField(verbose_name='Zobrazuj do')), + ('caption', models.CharField(max_length=50, verbose_name='nadpis')), + ('short_text', models.CharField(help_text='Krátky 1-2 vetový popis.', max_length=200, verbose_name='krátky text')), + ('details', models.TextField(blank=True, help_text='Dlhší text, ktorý sa zobrazí po rozkliknutí.', verbose_name='podrobnosti k príspevku')), + ('added_at', models.DateTimeField(auto_now_add=True, verbose_name='pridané')), + ('sites', models.ManyToManyField(to='sites.site')), + ], + options={ + 'verbose_name': 'príspevok', + 'verbose_name_plural': 'príspevky', + 'ordering': ['-added_at'], + }, + ), + migrations.CreateModel( + name='PostLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('caption', models.CharField(help_text='Nápis, ktorý po kliknutí presmeruje na link. Maximálne 2 slová.', max_length=25, verbose_name='názov')), + ('url', models.CharField(help_text='URL stránky kam má preklik viesť', max_length=100, verbose_name='URL')), + ('post', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='links', to='cms.post', verbose_name='Relevantný príspevok')), + ], + options={ + 'verbose_name': 'link k príspevku', + 'verbose_name_plural': 'linky k príspevkom', + }, + ), + migrations.CreateModel( + name='MenuItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('caption', models.CharField(help_text='Nápis, ktorý sa zobrazí v menu. Maximálne 2 slová.', max_length=25, verbose_name='názov')), + ('url', models.CharField(help_text='URL stránky kam má preklik viesť', max_length=100, verbose_name='URL')), + ('priority', models.SmallIntegerField(help_text='Priorita, čím väčšie, tým vyššie v menu.', verbose_name='priorita')), + ('in_footer', models.BooleanField(default=False, verbose_name='Je v pätičke')), + ('in_menu', models.BooleanField(default=True, verbose_name='Je v menu')), + ('sites', models.ManyToManyField(to='sites.site')), + ], + options={ + 'verbose_name': 'položka v menu', + 'verbose_name_plural': 'položky v menu', + 'ordering': ['-priority'], + }, + ), + ] diff --git a/cms/migrations/0002_initial.py b/cms/migrations/0002_initial.py new file mode 100644 index 00000000..0c854b68 --- /dev/null +++ b/cms/migrations/0002_initial.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('flatpages', '0001_initial'), + ('cms', '0001_initial'), + ('competition', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='infobanner', + name='event', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='competition.event'), + ), + migrations.AddField( + model_name='infobanner', + name='message_template', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='cms.messagetemplate'), + ), + migrations.AddField( + model_name='infobanner', + name='page', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='flatpages.flatpage'), + ), + migrations.AddField( + model_name='infobanner', + name='series', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='competition.series'), + ), + ] diff --git a/competition/migrations/0001_initial.py b/competition/migrations/0001_initial.py new file mode 100644 index 00000000..f9263d2d --- /dev/null +++ b/competition/migrations/0001_initial.py @@ -0,0 +1,235 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +import base.models +import base.validators +import django.core.files.storage +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import re + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('posted_at', models.DateTimeField(auto_now_add=True, verbose_name='dátum pridania')), + ('state', models.IntegerField(choices=[(1, 'čaká'), (2, 'zverejnený'), (3, 'skrytý')], default=1, verbose_name='komentár publikovaný')), + ('hidden_response', models.TextField(blank=True, null=True, verbose_name='Skrytá odpoveď na komentár')), + ], + options={ + 'verbose_name': 'komentár', + 'verbose_name_plural': 'komentáre', + 'ordering': ['posted_at'], + }, + ), + migrations.CreateModel( + name='Competition', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='názov')), + ('slug', models.SlugField()), + ('start_year', models.PositiveSmallIntegerField(blank=True, verbose_name='rok prvého ročníka súťaže')), + ('description', models.TextField(blank=True, verbose_name='Popis súťaže')), + ('rules', models.TextField(blank=True, null=True, verbose_name='Pravidlá súťaže')), + ('who_can_participate', models.CharField(blank=True, max_length=50, verbose_name='Pre koho je súťaž určená')), + ('min_years_until_graduation', models.PositiveSmallIntegerField(help_text='Horná hranica na účasť v súťaži. Zadáva sa v počte rokov do maturity. Ak najstraší, kto môže riešiť súťaž je deviatak, zadá sa 4.', null=True, verbose_name='Minimálny počet rokov do maturity')), + ], + options={ + 'verbose_name': 'súťaž', + 'verbose_name_plural': 'súťaže', + }, + ), + migrations.CreateModel( + name='CompetitionType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=200, verbose_name='typ súťaže')), + ], + options={ + 'verbose_name': 'Typ súťaže', + 'verbose_name_plural': 'Typy súťaží', + }, + ), + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.PositiveSmallIntegerField(blank=True, verbose_name='ročník')), + ('school_year', models.CharField(blank=True, max_length=10, validators=[base.validators.school_year_validator], verbose_name='školský rok')), + ('season_code', models.PositiveSmallIntegerField(choices=[(0, 'Zimný'), (1, 'Letný'), (2, '')], default=2)), + ('start', models.DateTimeField(verbose_name='dátum začiatku súťaže')), + ('end', models.DateTimeField(verbose_name='dátum konca súťaže')), + ('additional_name', models.CharField(blank=True, max_length=50, null=True, verbose_name='Prívlastok súťaže')), + ], + options={ + 'verbose_name': 'ročník súťaže', + 'verbose_name_plural': 'ročníky súťaží', + 'ordering': ['-school_year'], + }, + ), + migrations.CreateModel( + name='EventRegistration', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + options={ + 'verbose_name': 'registrácia užívateľa na akciu', + 'verbose_name_plural': 'registrácie užívateľov na akcie', + }, + ), + migrations.CreateModel( + name='Grade', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='názov ročníku')), + ('tag', models.CharField(max_length=2, unique=True, verbose_name='skratka')), + ('years_until_graduation', models.SmallIntegerField(verbose_name='počet rokov do maturity')), + ('is_active', models.BooleanField(verbose_name='aktuálne používaný ročník')), + ], + options={ + 'verbose_name': 'ročník účastníka', + 'verbose_name_plural': 'ročníky účastníka', + 'ordering': ['years_until_graduation'], + }, + ), + migrations.CreateModel( + name='LateTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50, verbose_name='označenie štítku pre riešiteľa')), + ('slug', models.CharField(max_length=50, validators=[django.core.validators.RegexValidator(re.compile('^[-a-zA-Z0-9_]+\\Z'), 'Enter a valid “slug” consisting of letters, numbers, underscores or hyphens.', 'invalid')], verbose_name='označenie priečinku pri stiahnutí')), + ('upper_bound', models.DurationField(verbose_name='maximálna dĺžka omeškania')), + ('comment', models.TextField(verbose_name='komentár pre opravovateľa')), + ('can_resubmit', models.BooleanField(verbose_name='Možnosť prepísať odovzdané riešenie')), + ], + options={ + 'verbose_name': 'omeškanie', + 'verbose_name_plural': 'omeškanie', + }, + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField(verbose_name='znenie úlohy')), + ('order', models.PositiveSmallIntegerField(verbose_name='poradie v sérii')), + ('image', models.ImageField(blank=True, null=True, upload_to='problem_images/', verbose_name='Obrázok k úlohe')), + ('solution_pdf', base.models.RestrictedFileField(blank=True, null=True, upload_to='model_solutions/', verbose_name='Vzorové riešenie')), + ], + options={ + 'verbose_name': 'úloha', + 'verbose_name_plural': 'úlohy', + 'ordering': ['series', 'order'], + }, + ), + migrations.CreateModel( + name='PublicationType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='názov typu')), + ], + options={ + 'verbose_name': 'Typ publikácie', + 'verbose_name_plural': 'Typy publikácií', + }, + ), + migrations.CreateModel( + name='RegistrationLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('url', models.URLField(verbose_name='url registrácie')), + ('start', models.DateTimeField(verbose_name='Začiatok registrácie')), + ('end', models.DateTimeField(verbose_name='Koniec registrácie')), + ('additional_info', models.TextField(blank=True, null=True, verbose_name='Doplňujúce informácie')), + ], + options={ + 'verbose_name': 'link na registráciu', + 'verbose_name_plural': 'linky na registráciu', + }, + ), + migrations.CreateModel( + name='Series', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order', models.PositiveSmallIntegerField(verbose_name='poradie série')), + ('deadline', models.DateTimeField(verbose_name='termín série')), + ('sum_method', models.CharField(blank=True, choices=[('series_simple_sum', 'Jednoduchý súčet bodov'), ('series_Malynar_sum', 'Bonifikácia Malynár'), ('series_Matik_sum', 'Bonifikácia Matik'), ('series_STROM_sum', 'Bonifikácia STROM'), ('series_Malynar_sum_until_2021', 'Bonifikácia Malynár (Do 2020/2021)'), ('series_Matik_sum_until_2021', 'Bonifikácia Matik (Do 2020/2021)'), ('series_STROM_sum_until_2021', 'Bonifikácia STROM (Do 2020/2021)'), ('series_STROM_4problems_sum', 'Bonifikácia STROM (4. úlohy)')], max_length=50, verbose_name='Súčtová metóda')), + ('frozen_results', models.TextField(blank=True, default=None, null=True)), + ], + options={ + 'verbose_name': 'séria', + 'verbose_name_plural': 'série', + 'ordering': ['semester', '-order'], + }, + ), + migrations.CreateModel( + name='Semester', + fields=[ + ('event_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='competition.event')), + ('frozen_results', models.TextField(blank=True, default=None, null=True)), + ], + options={ + 'verbose_name': 'semester', + 'verbose_name_plural': 'semestre', + 'ordering': ['-year', '-season_code'], + }, + bases=('competition.event',), + ), + migrations.CreateModel( + name='Solution', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('solution', base.models.RestrictedFileField(blank=True, storage=django.core.files.storage.FileSystemStorage(location='/home/mihal/Documents/STROM/webstrom/webstrom-backend/protected_media'), upload_to='solutions/user_solutions', verbose_name='účastnícke riešenie')), + ('corrected_solution', base.models.RestrictedFileField(blank=True, storage=django.core.files.storage.FileSystemStorage(location='/home/mihal/Documents/STROM/webstrom/webstrom-backend/protected_media'), upload_to='solutions/corrected/', verbose_name='opravené riešenie')), + ('score', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='body')), + ('vote', models.IntegerField(choices=[(-1, 'negatívny'), (0, 'žiaden'), (1, 'pozitívny')], default=0)), + ('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='dátum pridania')), + ('is_online', models.BooleanField(default=False, verbose_name='internetové riešenie')), + ('late_tag', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.latetag', verbose_name='Stavy omeškania')), + ('problem', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.problem')), + ('semester_registration', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.eventregistration')), + ], + options={ + 'verbose_name': 'riešenie', + 'verbose_name_plural': 'riešenia', + }, + ), + migrations.CreateModel( + name='Publication', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(blank=True, max_length=30)), + ('file', base.models.RestrictedFileField(upload_to='publications/%Y', verbose_name='súbor')), + ('order', models.PositiveSmallIntegerField(blank=True, null=True, verbose_name='poradie')), + ('event', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.event')), + ('publication_type', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.publicationtype')), + ], + options={ + 'verbose_name': 'Publikácia', + 'verbose_name_plural': 'Publikácie', + 'ordering': ['order'], + }, + ), + migrations.CreateModel( + name='ProblemCorrection', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('correct_solution_text', models.TextField(verbose_name='vzorák')), + ('best_solution', models.ManyToManyField(to='competition.solution', verbose_name='najkrajšie riešenia')), + ], + options={ + 'verbose_name': 'opravenie úlohy', + 'verbose_name_plural': 'opravene ulohy', + }, + ), + ] diff --git a/competition/migrations/0002_initial.py b/competition/migrations/0002_initial.py new file mode 100644 index 00000000..8355aed2 --- /dev/null +++ b/competition/migrations/0002_initial.py @@ -0,0 +1,105 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ('personal', '0002_initial'), + ('competition', '0001_initial'), + ('sites', '0002_alter_domain_unique'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='problemcorrection', + name='corrected_by', + field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, verbose_name='opravovatelia'), + ), + migrations.AddField( + model_name='problemcorrection', + name='problem', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='correction', to='competition.problem'), + ), + migrations.AddField( + model_name='problem', + name='series', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='problems', to='competition.series', verbose_name='úloha zaradená do série'), + ), + migrations.AddField( + model_name='eventregistration', + name='event', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.event', verbose_name='semester'), + ), + migrations.AddField( + model_name='eventregistration', + name='grade', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.grade', verbose_name='ročník'), + ), + migrations.AddField( + model_name='eventregistration', + name='profile', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='personal.profile', verbose_name='profil'), + ), + migrations.AddField( + model_name='eventregistration', + name='school', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='personal.school', verbose_name='škola'), + ), + migrations.AddField( + model_name='event', + name='competition', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.competition'), + ), + migrations.AddField( + model_name='event', + name='registration_link', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.registrationlink'), + ), + migrations.AddField( + model_name='competition', + name='competition_type', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='competition.competitiontype', verbose_name='typ súťaže'), + ), + migrations.AddField( + model_name='competition', + name='permission_group', + field=models.ManyToManyField(blank=True, related_name='competition_permissions', to='auth.group', verbose_name='Skupiny práv'), + ), + migrations.AddField( + model_name='competition', + name='sites', + field=models.ManyToManyField(to='sites.site'), + ), + migrations.AddField( + model_name='comment', + name='posted_by', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='autor komentára'), + ), + migrations.AddField( + model_name='comment', + name='problem', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.problem', verbose_name='komentár k úlohe'), + ), + migrations.AddField( + model_name='series', + name='semester', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='competition.semester', verbose_name='semester'), + ), + migrations.AddField( + model_name='semester', + name='late_tags', + field=models.ManyToManyField(blank=True, to='competition.latetag', verbose_name='Stavy omeškania'), + ), + migrations.AddConstraint( + model_name='eventregistration', + constraint=models.UniqueConstraint(fields=('profile', 'event'), name='single_registration_in_event'), + ), + ] diff --git a/competition/migrations/0003_alter_grade_name.py b/competition/migrations/0003_alter_grade_name.py new file mode 100644 index 00000000..74ae0f94 --- /dev/null +++ b/competition/migrations/0003_alter_grade_name.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-04-14 10:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competition', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='grade', + name='name', + field=models.CharField(max_length=256, verbose_name='názov ročníku'), + ), + ] diff --git a/competition/models.py b/competition/models.py index 72c24f5e..b103f2e1 100644 --- a/competition/models.py +++ b/competition/models.py @@ -542,7 +542,7 @@ class Meta: verbose_name_plural = 'ročníky účastníka' ordering = ['years_until_graduation', ] - name = models.CharField(verbose_name='názov ročníku', max_length=32) + name = models.CharField(verbose_name='názov ročníku', max_length=256) tag = models.CharField(verbose_name='skratka', max_length=2, unique=True) years_until_graduation = models.SmallIntegerField( verbose_name='počet rokov do maturity') diff --git a/compose.yaml b/compose.yaml deleted file mode 100644 index 6aed2794..00000000 --- a/compose.yaml +++ /dev/null @@ -1,9 +0,0 @@ -version: "3.3" - -services: - website: - build: . - image: webstrom-backend - ports: - - "8000:8000" - restart: always diff --git a/deployment/compose-test.yaml b/deployment/compose-test.yaml new file mode 100644 index 00000000..be492213 --- /dev/null +++ b/deployment/compose-test.yaml @@ -0,0 +1,32 @@ +version: "3" + +services: + webstrom-backend: + build: + dockerfile: deployment/webstrom-backend.dockerfile + context: .. + + image: webstrom-test-backend + + environment: + - DJANGO_SETTINGS_MODULE=webstrom.settings_test + + volumes: + - /var/run/postgresql:/var/run/postgresql:rw + + ports: + - "127.0.0.1:8920:8000" + + restart: always + + static-files: + build: + dockerfile: deployment/static-files.dockerfile + context: .. + + image: webstrom-test-static + + ports: + - "127.0.0.1:8921:80" + + restart: always diff --git a/deployment/static-files.dockerfile b/deployment/static-files.dockerfile new file mode 100644 index 00000000..89c9875d --- /dev/null +++ b/deployment/static-files.dockerfile @@ -0,0 +1,17 @@ +FROM python:3.11 AS static-files-builder + +WORKDIR /app + +COPY Pipfile /app +COPY Pipfile.lock /app + +RUN pip install pipenv +RUN pipenv sync --dev --system + +COPY . /app + +RUN python manage.py collectstatic --no-input + +FROM nginx:1.25 + +COPY --from=static-files-builder /app/static /usr/share/nginx/html diff --git a/Dockerfile b/deployment/webstrom-backend.dockerfile similarity index 52% rename from Dockerfile rename to deployment/webstrom-backend.dockerfile index bbe44070..191986a0 100644 --- a/Dockerfile +++ b/deployment/webstrom-backend.dockerfile @@ -10,8 +10,4 @@ RUN pipenv sync --dev --system COPY . /app -RUN python manage.py restoredb - -EXPOSE 8000 - -ENTRYPOINT [ "daphne", "-b", "0.0.0.0", "-p", "8000", "webstrom.asgi:application" ] +CMD [ "daphne", "-b", "0.0.0.0", "-p", "8000", "webstrom.asgi:application" ] diff --git a/downloads/urls.py b/downloads/urls.py index d871f9c7..247c8f67 100644 --- a/downloads/urls.py +++ b/downloads/urls.py @@ -1,4 +1,4 @@ -from django.conf.urls import url +from django.urls import re_path from django.utils.translation import ugettext_lazy as _ from competition.models import Solution @@ -7,12 +7,11 @@ urlpatterns = [ # Include non-translated versions only since Admin ignores lang prefix - url(r'solutions/(?P.*)$', download_protected_file, - {'path_prefix': 'solutions/', 'model_class': Solution}, - name='download_solution'), - url(r'corrected_solutions/(?P.*)$', download_protected_file, - {'path_prefix': 'corrected_solutions/', - 'model_class': Solution}, - name='download_corrected_solution'), - + re_path(r'solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'solutions/', 'model_class': Solution}, + name='download_solution'), + re_path(r'corrected_solutions/(?P.*)$', download_protected_file, + {'path_prefix': 'corrected_solutions/', + 'model_class': Solution}, + name='download_corrected_solution'), ] diff --git a/personal/migrations/0001_initial.py b/personal/migrations/0001_initial.py new file mode 100644 index 00000000..9d549156 --- /dev/null +++ b/personal/migrations/0001_initial.py @@ -0,0 +1,72 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +import base.managers +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='County', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=30, verbose_name='názov')), + ], + options={ + 'verbose_name': 'kraj', + 'verbose_name_plural': 'kraje', + }, + ), + migrations.CreateModel( + name='District', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=30, verbose_name='názov')), + ('abbreviation', models.CharField(max_length=2, verbose_name='skratka')), + ], + options={ + 'verbose_name': 'okres', + 'verbose_name_plural': 'okresy', + }, + ), + migrations.CreateModel( + name='School', + fields=[ + ('code', models.AutoField(primary_key=True, serialize=False, verbose_name='kód')), + ('name', models.CharField(max_length=100, verbose_name='názov')), + ('abbreviation', models.CharField(max_length=10, verbose_name='skratka')), + ('street', models.CharField(max_length=100, verbose_name='ulica')), + ('city', models.CharField(max_length=100, verbose_name='obec')), + ('zip_code', models.CharField(max_length=6, verbose_name='PSČ')), + ('email', models.CharField(blank=True, max_length=50, verbose_name='email')), + ('district', models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.district', verbose_name='okres')), + ], + options={ + 'verbose_name': 'škola', + 'verbose_name_plural': 'školy', + }, + ), + migrations.CreateModel( + name='Profile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('first_name', models.CharField(max_length=150, verbose_name='krstné meno')), + ('last_name', models.CharField(max_length=150, verbose_name='priezvisko')), + ('year_of_graduation', models.PositiveSmallIntegerField(verbose_name='rok maturity')), + ('phone', models.CharField(blank=True, help_text='Telefonné číslo v medzinárodnom formáte (napr. +421 123 456 789).', max_length=32, validators=[django.core.validators.RegexValidator(message='Zadaj telefónne číslo vo formáte +421 123 456 789 alebo 0912 345 678.', regex='^(\\+\\d{1,3}\\d{9})$')], verbose_name='telefónne číslo')), + ('parent_phone', models.CharField(blank=True, help_text='Telefonné číslo v medzinárodnom formáte (napr. +421 123 456 789).', max_length=32, validators=[django.core.validators.RegexValidator(message='Zadaj telefónne číslo vo formáte +421 123 456 789 alebo 0912 345 678.', regex='^(\\+\\d{1,3}\\d{9})$')], verbose_name='telefónne číslo na rodiča')), + ('school', models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.school', verbose_name='škola')), + ], + options={ + 'verbose_name': 'profil', + 'verbose_name_plural': 'profily', + }, + ), + ] diff --git a/personal/migrations/0002_initial.py b/personal/migrations/0002_initial.py new file mode 100644 index 00000000..c5b595ec --- /dev/null +++ b/personal/migrations/0002_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +import base.managers +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('personal', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='user', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL), + ), + migrations.AddField( + model_name='district', + name='county', + field=models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.county', verbose_name='kraj'), + ), + ] diff --git a/personal/migrations/0003_alter_district_county_alter_profile_school_and_more.py b/personal/migrations/0003_alter_district_county_alter_profile_school_and_more.py new file mode 100644 index 00000000..3425ec86 --- /dev/null +++ b/personal/migrations/0003_alter_district_county_alter_profile_school_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.11 on 2024-04-14 10:51 + +import base.managers +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('personal', '0002_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='district', + name='county', + field=models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.county', verbose_name='kraj'), + ), + migrations.AlterField( + model_name='profile', + name='school', + field=models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.school', verbose_name='škola'), + ), + migrations.AlterField( + model_name='school', + name='district', + field=models.ForeignKey(on_delete=models.SET(base.managers.UnspecifiedValueManager.get_unspecified_value), to='personal.district', verbose_name='okres'), + ), + ] diff --git a/problem_database/migrations/0001_initial.py b/problem_database/migrations/0001_initial.py new file mode 100644 index 00000000..186e8e6e --- /dev/null +++ b/problem_database/migrations/0001_initial.py @@ -0,0 +1,153 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Activity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(verbose_name='dátum')), + ('description', models.TextField(verbose_name='popis')), + ('soft_deleted', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'aktivita', + 'verbose_name_plural': 'aktivity', + }, + ), + migrations.CreateModel( + name='ActivityType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov')), + ], + options={ + 'verbose_name': 'typ aktivity', + 'verbose_name_plural': 'typy aktivít', + }, + ), + migrations.CreateModel( + name='Difficulty', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=128, verbose_name='názov')), + ('activity_type', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activitytype', verbose_name='typ aktivity')), + ], + options={ + 'verbose_name': 'náročnosť', + 'verbose_name_plural': 'náročnosti', + }, + ), + migrations.CreateModel( + name='Problem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('problem', models.TextField(verbose_name='príklad')), + ('result', models.CharField(max_length=128, verbose_name='výsledok')), + ('solution', models.TextField(verbose_name='riešenie')), + ('soft_deleted', models.BooleanField(default=False)), + ], + options={ + 'verbose_name': 'príklad', + 'verbose_name_plural': 'príklady', + }, + ), + migrations.CreateModel( + name='Seminar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=32, verbose_name='názov seminára')), + ], + options={ + 'verbose_name': 'seminár', + 'verbose_name_plural': 'semináre', + }, + ), + migrations.CreateModel( + name='Tag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov tagu')), + ], + options={ + 'verbose_name': 'tag', + 'verbose_name_plural': 'tagy', + }, + ), + migrations.CreateModel( + name='ProblemType', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=64, verbose_name='názov')), + ('description', models.TextField(verbose_name='popis')), + ('seminar', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.seminar', verbose_name='seminár')), + ], + options={ + 'verbose_name': 'typ príkladu', + 'verbose_name_plural': 'typy príkladov', + }, + ), + migrations.CreateModel( + name='ProblemTag', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ('tag', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.tag', verbose_name='aktivita')), + ], + options={ + 'verbose_name': 'priradenie príkladu k tagu', + 'verbose_name_plural': 'priradenia príkladov k tagom', + }, + ), + migrations.CreateModel( + name='ProblemActivity', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('activity', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activity', verbose_name='aktivita')), + ('difficulty', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.difficulty', verbose_name='náročnosť')), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ], + options={ + 'verbose_name': 'priradenie problému k aktivite/obtiežnosti', + 'verbose_name_plural': 'priradenie problémov k aktivitám/obtiažnostiam', + }, + ), + migrations.AddField( + model_name='problem', + name='problem_type', + field=models.ManyToManyField(to='problem_database.problemtype'), + ), + migrations.CreateModel( + name='Media', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.ImageField(upload_to='', verbose_name='priložené súbory')), + ('soft_deleted', models.BooleanField(default=False)), + ('problem', models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.problem', verbose_name='príklad')), + ], + options={ + 'verbose_name': 'súbor', + 'verbose_name_plural': 'súbory', + }, + ), + migrations.AddField( + model_name='activitytype', + name='seminar', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.seminar', verbose_name='seminár'), + ), + migrations.AddField( + model_name='activity', + name='activity_type', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='problem_database.activitytype', verbose_name='typ aktivity'), + ), + ] diff --git a/requirements.txt b/requirements.txt index c4036f94..fdd18e21 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,7 +15,7 @@ daphne==4.0.0 defusedxml==0.7.1 dill==0.3.8 dj-rest-auth==5.0.2 -Django==3.2.25 +Django==4.2.11 django-allauth==0.58.2 django-filter==23.5 django-rest-swagger==2.2.0 diff --git a/user/migrations/0001_initial.py b/user/migrations/0001_initial.py new file mode 100644 index 00000000..f5ccea1d --- /dev/null +++ b/user/migrations/0001_initial.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.11 on 2024-04-14 09:24 + +import django.contrib.auth.models +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('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')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True, verbose_name='email')), + ('verified_email', models.BooleanField(default=False, verbose_name='overený email')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/webstrom/settings_test.py b/webstrom/settings_test.py new file mode 100644 index 00000000..65d76d71 --- /dev/null +++ b/webstrom/settings_test.py @@ -0,0 +1,23 @@ +# pylint: disable=wildcard-import,unused-wildcard-import + +from .settings import * + +DEBUG = True + +ALLOWED_HOSTS = [ + "test.strom.sk", +] + +USE_X_FORWARDED_HOST = True + +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': 'webstrom-test', + 'USER': 'webstrom', + } +} + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"