diff --git a/.gitignore b/.gitignore index 1ab3f47..458be9a 100644 --- a/.gitignore +++ b/.gitignore @@ -176,5 +176,38 @@ cython_debug/ # Ignore the results/ directory if it contains generated files or outputs that should not be committed to version control. results/ +# Exceptions for new apps - all apps should be tracked +!apps/classes/ +!apps/classes/** +!apps/clubs/ +!apps/clubs/** +!apps/controls/ +!apps/controls/** +!apps/courses/ +!apps/courses/** +!apps/events/ +!apps/events/** +!apps/federations/ +!apps/federations/** +!apps/rawuploads/ +!apps/rawuploads/** +!apps/resulttypes/ +!apps/resulttypes/** +!apps/runners/ +!apps/runners/** +!apps/splits/ +!apps/splits/** +!apps/stages/ +!apps/stages/** +!apps/teams/ +!apps/teams/** +!apps/tokens/ +!apps/tokens/** +!apps/uploadlogs/ +!apps/uploadlogs/** +!apps/usersevents/ +!apps/usersevents/** +!apps/shared_models.py + # Ignore the requirements.txt file if it contains sensitive information such as private package indexes or credentials. requirements.txt diff --git a/Pipfile b/Pipfile index 2bce9e8..69edb01 100644 --- a/Pipfile +++ b/Pipfile @@ -13,6 +13,7 @@ mysqlclient = "2.2" # Soporte para MySQL python-decouple = "3.8" # Gestión de variables de entorno django-cors-headers = "==4.9.0" # Manejo de CORS (actualizado) drf-spectacular = "*" +bcrypt = "*" setuptools = "==82.0.1" wheel = "==0.46.3" diff --git a/Pipfile.lock b/Pipfile.lock index 81b4bf6..04d7c2f 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "fb8f819fcaedb78bea12b69af1c0375c62d64d7ae55579a6cb416bb1643bab98" + "sha256": "7b55c29950eea22626f0b95b7976b866cd936431ec3f8d7ac7a35cd54ea00796" }, "pipfile-spec": 6, "requires": { @@ -32,6 +32,76 @@ "markers": "python_version >= '3.9'", "version": "==26.1.0" }, + "bcrypt": { + "hashes": [ + "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", + "sha256:0c418ca99fd47e9c59a301744d63328f17798b5947b0f791e9af3c1c499c2d0a", + "sha256:0c8e093ea2532601a6f686edbc2c6b2ec24131ff5c52f7610dd64fa4553b5464", + "sha256:0cae4cb350934dfd74c020525eeae0a5f79257e8a201c0c176f4b84fdbf2a4b4", + "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", + "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", + "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", + "sha256:2b732e7d388fa22d48920baa267ba5d97cca38070b69c0e2d37087b381c681fd", + "sha256:35a77ec55b541e5e583eb3436ffbbf53b0ffa1fa16ca6782279daf95d146dcd9", + "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", + "sha256:3abeb543874b2c0524ff40c57a4e14e5d3a66ff33fb423529c88f180fd756538", + "sha256:3ca8a166b1140436e058298a34d88032ab62f15aae1c598580333dc21d27ef10", + "sha256:3cf67a804fc66fc217e6914a5635000259fbbbb12e78a99488e4d5ba445a71eb", + "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", + "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", + "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", + "sha256:560ddb6ec730386e7b3b26b8b4c88197aaed924430e7b74666a586ac997249ef", + "sha256:5b1589f4839a0899c146e8892efe320c0fa096568abd9b95593efac50a87cb75", + "sha256:5feebf85a9cefda32966d8171f5db7e3ba964b77fdfe31919622256f80f9cf42", + "sha256:611f0a17aa4a25a69362dcc299fda5c8a3d4f160e2abb3831041feb77393a14a", + "sha256:61afc381250c3182d9078551e3ac3a41da14154fbff647ddf52a769f588c4172", + "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", + "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", + "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", + "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", + "sha256:744d3c6b164caa658adcb72cb8cc9ad9b4b75c7db507ab4bc2480474a51989da", + "sha256:79cfa161eda8d2ddf29acad370356b47f02387153b11d46042e93a0a95127493", + "sha256:7aeef54b60ceddb6f30ee3db090351ecf0d40ec6e2abf41430997407a46d2254", + "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", + "sha256:7f277a4b3390ab4bebe597800a90da0edae882c6196d3038a73adf446c4f969f", + "sha256:7f4c94dec1b5ab5d522750cb059bb9409ea8872d4494fd152b53cca99f1ddd8c", + "sha256:801cad5ccb6b87d1b430f183269b94c24f248dddbbc5c1f78b6ed231743e001c", + "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", + "sha256:89042e61b5e808b67daf24a434d89bab164d4de1746b37a8d173b6b14f3db9ff", + "sha256:92864f54fb48b4c718fc92a32825d0e42265a627f956bc0361fe869f1adc3e7d", + "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", + "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", + "sha256:a28bc05039bdf3289d757f49d616ab3efe8cf40d8e8001ccdd621cd4f98f4fc9", + "sha256:a5393eae5722bcef046a990b84dff02b954904c36a194f6cfc817d7dca6c6f0b", + "sha256:a71f70ee269671460b37a449f5ff26982a6f2ba493b3eabdd687b4bf35f875ac", + "sha256:b17366316c654e1ad0306a6858e189fc835eca39f7eb2cafd6aaca8ce0c40a2e", + "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", + "sha256:c2388ca94ffee269b6038d48747f4ce8df0ffbea43f31abfa18ac72f0218effb", + "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", + "sha256:cde08734f12c6a4e28dc6755cd11d3bdfea608d93d958fffbe95a7026ebe4980", + "sha256:d79e5c65dcc9af213594d6f7f1fa2c98ad3fc10431e7aa53c176b441943efbdd", + "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", + "sha256:db99dca3b1fdc3db87d7c57eac0c82281242d1eabf19dcb8a6b10eb29a2e72d1", + "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", + "sha256:dd19cf5184a90c873009244586396a6a884d591a5323f0e8a5922560718d4993", + "sha256:ddb4e1500f6efdd402218ffe34d040a1196c072e07929b9820f363a1fd1f4191", + "sha256:e3cf5b2560c7b5a142286f69bde914494b6d8f901aaa71e453078388a50881c4", + "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", + "sha256:edfcdcedd0d0f05850c52ba3127b1fce70b9f89e0fe5ff16517df7e81fa3cbb8", + "sha256:f0ce778135f60799d89c9693b9b398819d15f1921ba15fe719acb3178215a7db", + "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", + "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", + "sha256:f632fd56fc4e61564f78b46a2269153122db34988e78b6be8b32d28507b7eaeb", + "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", + "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", + "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", + "sha256:f8429e1c410b4073944f03bd778a9e066e7fad723564a52ff91841d278dfc822", + "sha256:fc746432b951e92b58317af8e0ca746efe93e66555f1b40888865ef5bf56446b" + ], + "index": "pypi", + "markers": "python_version >= '3.8'", + "version": "==5.0.0" + }, "django": { "hashes": [ "sha256:5788fce61da23788a8ce6f02583765ab060d396720924789f97fa42119d37f7a", @@ -1245,20 +1315,20 @@ }, "pipdeptree": { "hashes": [ - "sha256:4f5b77c07871103f6cf3c402093263609cbfd4e2ce3402bc8b9a7e984b2c6265", - "sha256:9a43a0b4a16a5cbc4e6ab02966dfd5411b949ff8c7cb1de7ade960bef2db3537" + "sha256:4bbcffe7d1edead00f5ca64c265b0bf902c919f872637e055924c3bea8c615d1", + "sha256:9e7ce2533f5c18349c3988f002b6afc1dd503cb7c40826ac83e643da0263590a" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==2.34.0" + "version": "==2.35.1" }, "platformdirs": { "hashes": [ - "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", - "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868" + "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", + "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917" ], "markers": "python_version >= '3.10'", - "version": "==4.9.4" + "version": "==4.9.6" }, "pluggy": { "hashes": [ @@ -1648,11 +1718,11 @@ }, "rich": { "hashes": [ - "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", - "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b" + "sha256:07e7adb4690f68864777b1450859253bed81a99a31ac321ac1817b2313558952", + "sha256:817e02727f2b25b40ef56f5aa2217f400c8489f79ca8f46ea2b70dd5e14558a9" ], "markers": "python_full_version >= '3.8.0'", - "version": "==14.3.3" + "version": "==14.3.4" }, "ruamel-yaml": { "hashes": [ diff --git a/apps/admin_base.py b/apps/admin_base.py new file mode 100644 index 0000000..163c24f --- /dev/null +++ b/apps/admin_base.py @@ -0,0 +1,61 @@ +from django.contrib import admin +from django.utils import timezone + + +class TimestampedModelAdmin(admin.ModelAdmin): + timestamp_fields = ("created", "modified", "deleted") + + def get_readonly_fields(self, request, obj=None): + readonly_fields = list(super().get_readonly_fields(request, obj)) + for field_name in self.timestamp_fields: + if ( + field_name in {field.name for field in self.model._meta.fields} + and field_name not in readonly_fields + ): + readonly_fields.append(field_name) + return readonly_fields + + def save_model(self, request, obj, form, change): + now = timezone.now() + field_names = {field.name for field in self.model._meta.fields} + + if ( + "created" in field_names + and not change + and getattr(obj, "created", None) is None + ): + obj.created = now + if "modified" in field_names: + obj.modified = now + if "deleted" in field_names and getattr(obj, "deleted", None) is None: + obj.deleted = None + + super().save_model(request, obj, form, change) + + def delete_model(self, request, obj): + field_names = {field.name for field in self.model._meta.fields} + if "deleted" in field_names: + now = timezone.now() + obj.deleted = now + if "modified" in field_names: + obj.modified = now + obj.save( + update_fields=[ + field for field in ("deleted", "modified") if field in field_names + ] + ) + return + + super().delete_model(request, obj) + + def delete_queryset(self, request, queryset): + field_names = {field.name for field in self.model._meta.fields} + if "deleted" in field_names: + now = timezone.now() + update_fields = {"deleted": now} + if "modified" in field_names: + update_fields["modified"] = now + queryset.update(**update_fields) + return + + super().delete_queryset(request, queryset) diff --git a/apps/classes/__init__.py b/apps/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/classes/admin.py b/apps/classes/admin.py new file mode 100644 index 0000000..1eb9065 --- /dev/null +++ b/apps/classes/admin.py @@ -0,0 +1,42 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Classes, ClassesControls + + +@admin.register(Classes) +class ClassesAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "short_name", + "long_name", + "oe_key", + "course", + "created", + ] + search_fields = ["short_name", "long_name", "oe_key"] + list_filter = ["event", "stage"] + list_select_related = ["event", "stage", "course"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(ClassesControls) +class ClassesControlsAdmin(TimestampedModelAdmin): + list_display = [ + "event", + "stage", + "control_class", + "control", + "order_number", + "created", + ] + search_fields = ["order_number"] + list_filter = ["event", "stage", "order_number"] + list_select_related = ["event", "stage", "control_class", "control"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/classes/apps.py b/apps/classes/apps.py new file mode 100644 index 0000000..c74203f --- /dev/null +++ b/apps/classes/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ClassesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.classes" diff --git a/apps/classes/migrations/__init__.py b/apps/classes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/classes/models.py b/apps/classes/models.py new file mode 100644 index 0000000..e11a932 --- /dev/null +++ b/apps/classes/models.py @@ -0,0 +1,81 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Classes(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="classes_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="classes_rows", + ) + oe_key = models.CharField(max_length=50, null=True, blank=True) + short_name = models.CharField(max_length=100, null=True, blank=True) + long_name = models.CharField(max_length=255, null=True, blank=True) + course = models.ForeignKey( + "courses.Courses", + on_delete=models.DO_NOTHING, + db_column="course_id", + null=True, + blank=True, + related_name="classes_rows", + ) + upload_hash = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "classes" + + def __str__(self): + return self.short_name or self.long_name or f"Class {self.id}" + + +class ClassesControls(BaseUnmanagedModel): + id = None + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="classes_controls_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="classes_controls_rows", + ) + control_class = models.ForeignKey( + "Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + primary_key=True, + related_name="classes_controls_rows", + ) + control = models.ForeignKey( + "controls.Controls", + on_delete=models.DO_NOTHING, + db_column="control_id", + null=True, + blank=True, + related_name="classes_controls_rows", + ) + order_number = models.IntegerField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "classes_controls" + + def __str__(self): + return f"ClassControl {self.control_class_id}:{self.control_id}:{self.order_number}" diff --git a/apps/classes/tests.py b/apps/classes/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/classes/urls.py b/apps/classes/urls.py new file mode 100644 index 0000000..35d334f --- /dev/null +++ b/apps/classes/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "classes" + +urlpatterns = [] diff --git a/apps/classes/views.py b/apps/classes/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/clubs/__init__.py b/apps/clubs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/clubs/admin.py b/apps/clubs/admin.py new file mode 100644 index 0000000..c81e0d2 --- /dev/null +++ b/apps/clubs/admin.py @@ -0,0 +1,23 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Clubs + + +@admin.register(Clubs) +class ClubsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "short_name", + "long_name", + "oe_key", + "event", + "stage", + "created", + ] + search_fields = ["short_name", "long_name", "oe_key"] + list_filter = ["event", "stage"] + list_select_related = ["event", "stage"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/clubs/apps.py b/apps/clubs/apps.py new file mode 100644 index 0000000..b692b9b --- /dev/null +++ b/apps/clubs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ClubsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.clubs" diff --git a/apps/clubs/migrations/__init__.py b/apps/clubs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/clubs/models.py b/apps/clubs/models.py new file mode 100644 index 0000000..21199ae --- /dev/null +++ b/apps/clubs/models.py @@ -0,0 +1,30 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Clubs(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="clubs_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="clubs_rows", + ) + oe_key = models.CharField(max_length=50, null=True, blank=True) + short_name = models.CharField(max_length=100, null=True, blank=True) + long_name = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "clubs" + + def __str__(self): + return self.short_name or self.long_name or f"Club {self.id}" diff --git a/apps/clubs/tests.py b/apps/clubs/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/clubs/urls.py b/apps/clubs/urls.py new file mode 100644 index 0000000..07c28f1 --- /dev/null +++ b/apps/clubs/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "clubs" + +urlpatterns = [] diff --git a/apps/clubs/views.py b/apps/clubs/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/controls/__init__.py b/apps/controls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/controls/admin.py b/apps/controls/admin.py new file mode 100644 index 0000000..2cb472f --- /dev/null +++ b/apps/controls/admin.py @@ -0,0 +1,24 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Controls, ControlTypes + + +@admin.register(ControlTypes) +class ControlTypesAdmin(TimestampedModelAdmin): + list_display = ["id", "description", "created"] + search_fields = ["description"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(Controls) +class ControlsAdmin(TimestampedModelAdmin): + list_display = ["id", "event", "stage", "station", "control_type", "created"] + search_fields = ["station"] + list_filter = ["event", "stage", "control_type"] + list_select_related = ["event", "stage", "control_type"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/controls/apps.py b/apps/controls/apps.py new file mode 100644 index 0000000..a54c25b --- /dev/null +++ b/apps/controls/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ControlsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.controls" diff --git a/apps/controls/migrations/__init__.py b/apps/controls/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/controls/models.py b/apps/controls/models.py new file mode 100644 index 0000000..e428dc1 --- /dev/null +++ b/apps/controls/models.py @@ -0,0 +1,46 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class ControlTypes(BaseUnmanagedModel): + description = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "control_types" + + def __str__(self): + return self.description or f"ControlType {self.id}" + + +class Controls(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="controls_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="controls_rows", + ) + station = models.IntegerField(null=True, blank=True) + control_type = models.ForeignKey( + "ControlTypes", + on_delete=models.DO_NOTHING, + db_column="control_type_id", + null=True, + blank=True, + related_name="controls_rows", + ) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "controls" + + def __str__(self): + return f"Control {self.station}" if self.station else f"Control {self.id}" diff --git a/apps/controls/tests.py b/apps/controls/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/controls/urls.py b/apps/controls/urls.py new file mode 100644 index 0000000..812226f --- /dev/null +++ b/apps/controls/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "controls" + +urlpatterns = [] diff --git a/apps/controls/views.py b/apps/controls/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/courses/__init__.py b/apps/courses/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/courses/admin.py b/apps/courses/admin.py new file mode 100644 index 0000000..641e6e6 --- /dev/null +++ b/apps/courses/admin.py @@ -0,0 +1,24 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Courses + + +@admin.register(Courses) +class CoursesAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "short_name", + "long_name", + "distance", + "climb", + "created", + ] + search_fields = ["short_name", "long_name", "oe_key"] + list_filter = ["event", "stage"] + list_select_related = ["event", "stage"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/courses/apps.py b/apps/courses/apps.py new file mode 100644 index 0000000..e8b8583 --- /dev/null +++ b/apps/courses/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoursesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.courses" diff --git a/apps/courses/migrations/__init__.py b/apps/courses/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/courses/models.py b/apps/courses/models.py new file mode 100644 index 0000000..93fb48a --- /dev/null +++ b/apps/courses/models.py @@ -0,0 +1,40 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Courses(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="courses_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="courses_rows", + ) + short_name = models.CharField(max_length=100, null=True, blank=True) + long_name = models.CharField(max_length=255, null=True, blank=True) + oe_key = models.CharField(max_length=50, null=True, blank=True) + distance = models.FloatField(null=True, blank=True) + climb = models.FloatField(null=True, blank=True) + controls = models.TextField(null=True, blank=True) + coord_system = models.CharField(max_length=50, null=True, blank=True) + datum = models.CharField(max_length=50, null=True, blank=True) + utm_zone = models.CharField(max_length=20, null=True, blank=True) + hemisphere = models.CharField(max_length=10, null=True, blank=True) + latitude = models.FloatField(null=True, blank=True) + longitude = models.FloatField(null=True, blank=True) + zoom = models.IntegerField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "courses" + + def __str__(self): + return self.short_name or self.long_name or f"Course {self.id}" diff --git a/apps/courses/tests.py b/apps/courses/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/courses/urls.py b/apps/courses/urls.py new file mode 100644 index 0000000..afe4237 --- /dev/null +++ b/apps/courses/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "courses" + +urlpatterns = [] diff --git a/apps/courses/views.py b/apps/courses/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/events/__init__.py b/apps/events/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/events/admin.py b/apps/events/admin.py new file mode 100644 index 0000000..069084d --- /dev/null +++ b/apps/events/admin.py @@ -0,0 +1,25 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Events + + +@admin.register(Events) +class EventsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "description", + "scope", + "location", + "country_code", + "is_hidden", + "initial_date", + "final_date", + "created", + ] + search_fields = ["description", "location", "scope"] + list_filter = ["is_hidden", "country_code", "scope"] + list_select_related = ["federation", "organizer"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/events/apps.py b/apps/events/apps.py new file mode 100644 index 0000000..cec9b91 --- /dev/null +++ b/apps/events/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class EventsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.events" diff --git a/apps/events/migrations/__init__.py b/apps/events/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/events/models.py b/apps/events/models.py new file mode 100644 index 0000000..62b81c9 --- /dev/null +++ b/apps/events/models.py @@ -0,0 +1,36 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Events(BaseUnmanagedModel): + description = models.TextField(null=True, blank=True) + scope = models.CharField(max_length=100, null=True, blank=True) + location = models.CharField(max_length=255, null=True, blank=True) + country_code = models.CharField(max_length=10, null=True, blank=True) + website = models.CharField(max_length=255, null=True, blank=True) + picture = models.CharField(max_length=255, null=True, blank=True) + initial_date = models.DateField(null=True, blank=True) + final_date = models.DateField(null=True, blank=True) + is_hidden = models.BooleanField(default=False) + federation = models.ForeignKey( + "federations.Federations", + on_delete=models.DO_NOTHING, + db_column="federation_id", + null=True, + blank=True, + related_name="events_rows", + ) + organizer = models.ForeignKey( + "organizers.Organizer", + on_delete=models.DO_NOTHING, + db_column="organizer_id", + null=True, + blank=True, + related_name="events_rows", + ) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "events" + + def __str__(self): + return self.description or f"Event {self.id}" diff --git a/apps/events/tests.py b/apps/events/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/events/urls.py b/apps/events/urls.py new file mode 100644 index 0000000..4c2c093 --- /dev/null +++ b/apps/events/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "events" + +urlpatterns = [] diff --git a/apps/events/views.py b/apps/events/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/federations/__init__.py b/apps/federations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/federations/admin.py b/apps/federations/admin.py new file mode 100644 index 0000000..d46df09 --- /dev/null +++ b/apps/federations/admin.py @@ -0,0 +1,13 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Federations + + +@admin.register(Federations) +class FederationsAdmin(TimestampedModelAdmin): + list_display = ["id", "description", "created"] + search_fields = ["description"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/federations/apps.py b/apps/federations/apps.py new file mode 100644 index 0000000..7520005 --- /dev/null +++ b/apps/federations/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class FederationsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.federations" diff --git a/apps/federations/migrations/__init__.py b/apps/federations/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/federations/models.py b/apps/federations/models.py new file mode 100644 index 0000000..a7f8d2c --- /dev/null +++ b/apps/federations/models.py @@ -0,0 +1,12 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Federations(BaseUnmanagedModel): + description = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "federations" + + def __str__(self): + return self.description or f"Federations {self.id}" diff --git a/apps/federations/tests.py b/apps/federations/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/federations/urls.py b/apps/federations/urls.py new file mode 100644 index 0000000..c461c50 --- /dev/null +++ b/apps/federations/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "federations" + +urlpatterns = [] diff --git a/apps/federations/views.py b/apps/federations/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rankings/__init__.py b/apps/rankings/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rankings/admin.py b/apps/rankings/admin.py new file mode 100644 index 0000000..93f9dbf --- /dev/null +++ b/apps/rankings/admin.py @@ -0,0 +1,25 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Ranking + + +@admin.register(Ranking) +class RankingAdmin(TimestampedModelAdmin): + list_display = ( + "id", + "event", + "stage", + "max_points", + "round_precision", + "nc_true", + "nc_false", + "created", + ) + search_fields = ( + "id", + "event__id", + "stage__id", + "status_scores", + "excluded_class_names", + ) diff --git a/apps/rankings/apps.py b/apps/rankings/apps.py new file mode 100644 index 0000000..37a36cb --- /dev/null +++ b/apps/rankings/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RankingsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.rankings" diff --git a/apps/rankings/migrations/__init__.py b/apps/rankings/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rankings/models.py b/apps/rankings/models.py new file mode 100644 index 0000000..ce17f45 --- /dev/null +++ b/apps/rankings/models.py @@ -0,0 +1,41 @@ +from django.db import models + + +class Ranking(models.Model): + id = models.CharField(max_length=36, primary_key=True) + scoring_algorithm = models.CharField(max_length=150) + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="rankings_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="rankings_rows", + ) + max_points = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + round_precision = models.IntegerField(null=True, blank=True) + nc_true = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + nc_false = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + status_scores = models.TextField(null=True, blank=True) + excluded_class_names = models.TextField(null=True, blank=True) + created = models.DateTimeField(null=True, blank=True) + modified = models.DateTimeField(null=True, blank=True) + deleted = models.DateTimeField(null=True, blank=True) + + class Meta: + managed = False + db_table = "rankings" diff --git a/apps/rankings/tests.py b/apps/rankings/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/apps/rankings/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/apps/rankings/urls.py b/apps/rankings/urls.py new file mode 100644 index 0000000..e39cb2c --- /dev/null +++ b/apps/rankings/urls.py @@ -0,0 +1,3 @@ +from django.urls import path + +urlpatterns = [] diff --git a/apps/rankings/views.py b/apps/rankings/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/apps/rankings/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/apps/rawuploads/__init__.py b/apps/rawuploads/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rawuploads/admin.py b/apps/rawuploads/admin.py new file mode 100644 index 0000000..b717242 --- /dev/null +++ b/apps/rawuploads/admin.py @@ -0,0 +1,15 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import RawUploads + + +@admin.register(RawUploads) +class RawUploadsAdmin(TimestampedModelAdmin): + list_display = ["id", "event", "stage", "upload_log", "created"] + search_fields = ["file_data"] + list_filter = ["event", "stage"] + list_select_related = ["event", "stage", "upload_log"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/rawuploads/apps.py b/apps/rawuploads/apps.py new file mode 100644 index 0000000..e1b39e0 --- /dev/null +++ b/apps/rawuploads/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RawuploadsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.rawuploads" diff --git a/apps/rawuploads/migrations/__init__.py b/apps/rawuploads/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rawuploads/models.py b/apps/rawuploads/models.py new file mode 100644 index 0000000..745beec --- /dev/null +++ b/apps/rawuploads/models.py @@ -0,0 +1,36 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class RawUploads(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="raw_uploads_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="raw_uploads_rows", + ) + upload_log = models.ForeignKey( + "uploadlogs.UploadLogs", + on_delete=models.DO_NOTHING, + db_column="upload_log_id", + null=True, + blank=True, + related_name="raw_uploads_rows", + ) + file_data = models.TextField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "raw_uploads" + + def __str__(self): + return f"RawUpload {self.id}" diff --git a/apps/rawuploads/tests.py b/apps/rawuploads/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/rawuploads/urls.py b/apps/rawuploads/urls.py new file mode 100644 index 0000000..600956e --- /dev/null +++ b/apps/rawuploads/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "rawuploads" + +urlpatterns = [] diff --git a/apps/rawuploads/views.py b/apps/rawuploads/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/resulttypes/__init__.py b/apps/resulttypes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/resulttypes/admin.py b/apps/resulttypes/admin.py new file mode 100644 index 0000000..c15cbbe --- /dev/null +++ b/apps/resulttypes/admin.py @@ -0,0 +1,13 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import ResultTypes + + +@admin.register(ResultTypes) +class ResultTypesAdmin(TimestampedModelAdmin): + list_display = ["id", "description", "created"] + search_fields = ["description"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/resulttypes/apps.py b/apps/resulttypes/apps.py new file mode 100644 index 0000000..270d16b --- /dev/null +++ b/apps/resulttypes/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ResulttypesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.resulttypes" diff --git a/apps/resulttypes/migrations/__init__.py b/apps/resulttypes/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/resulttypes/models.py b/apps/resulttypes/models.py new file mode 100644 index 0000000..25a0be6 --- /dev/null +++ b/apps/resulttypes/models.py @@ -0,0 +1,12 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class ResultTypes(BaseUnmanagedModel): + description = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "result_types" + + def __str__(self): + return self.description or f"ResultTypes {self.id}" diff --git a/apps/resulttypes/tests.py b/apps/resulttypes/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/resulttypes/urls.py b/apps/resulttypes/urls.py new file mode 100644 index 0000000..f163a73 --- /dev/null +++ b/apps/resulttypes/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "resulttypes" + +urlpatterns = [] diff --git a/apps/resulttypes/views.py b/apps/resulttypes/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/runners/__init__.py b/apps/runners/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/runners/admin.py b/apps/runners/admin.py new file mode 100644 index 0000000..9bad37a --- /dev/null +++ b/apps/runners/admin.py @@ -0,0 +1,53 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import RunnerResults, Runners + + +@admin.register(Runners) +class RunnersAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "first_name", + "last_name", + "sicard", + "bib_number", + "sex", + "is_nc", + "created", + ] + search_fields = ["first_name", "last_name", "sicard"] + list_filter = ["event", "stage", "is_nc", "sex"] + list_select_related = ["event", "stage", "entry_class", "club", "team"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(RunnerResults) +class RunnerResultsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "runner", + "entry_class", + "position", + "status_code", + "time_seconds", + "points_final", + "created", + ] + search_fields = ["runner__first_name", "runner__last_name"] + list_filter = [ + "event", + "stage", + "status_code", + "result_type", + ] + list_select_related = ["event", "stage", "runner", "entry_class", "result_type"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/runners/apps.py b/apps/runners/apps.py new file mode 100644 index 0000000..298c2b1 --- /dev/null +++ b/apps/runners/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class RunnersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.runners" diff --git a/apps/runners/migrations/__init__.py b/apps/runners/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/runners/models.py b/apps/runners/models.py new file mode 100644 index 0000000..718a019 --- /dev/null +++ b/apps/runners/models.py @@ -0,0 +1,137 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Runners(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="runners_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="runners_rows", + ) + first_name = models.CharField(max_length=150, null=True, blank=True) + last_name = models.CharField(max_length=150, null=True, blank=True) + entry_class = models.ForeignKey( + "classes.Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + null=True, + blank=True, + related_name="runners_rows", + ) + club = models.ForeignKey( + "clubs.Clubs", + on_delete=models.DO_NOTHING, + db_column="club_id", + null=True, + blank=True, + related_name="runners_rows", + ) + team = models.ForeignKey( + "teams.Teams", + on_delete=models.DO_NOTHING, + db_column="team_id", + null=True, + blank=True, + related_name="runners_rows", + ) + db_id = models.CharField(max_length=100, null=True, blank=True) + sicard = models.CharField(max_length=50, null=True, blank=True) + bib_number = models.IntegerField(null=True, blank=True) + sex = models.CharField(max_length=10, null=True, blank=True) + leg_number = models.IntegerField(null=True, blank=True) + is_nc = models.BooleanField(default=False) + eligibility = models.CharField(max_length=50, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "runners" + + def __str__(self): + full_name = f"{self.first_name} {self.last_name}".strip() + return full_name or f"Runner {self.id}" + + +class RunnerResults(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="runner_results_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="runner_results_rows", + ) + runner = models.ForeignKey( + "Runners", + on_delete=models.DO_NOTHING, + db_column="runner_id", + null=True, + blank=True, + related_name="runner_results_rows", + ) + entry_class = models.ForeignKey( + "classes.Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + null=True, + blank=True, + related_name="runner_results_rows", + ) + result_type = models.ForeignKey( + "resulttypes.ResultTypes", + on_delete=models.DO_NOTHING, + db_column="result_type_id", + null=True, + blank=True, + related_name="runner_results_rows", + ) + upload_hash = models.CharField(max_length=255, null=True, blank=True) + upload_type = models.CharField(max_length=100, null=True, blank=True) + position = models.IntegerField(null=True, blank=True) + status_code = models.IntegerField(null=True, blank=True) + start_time = models.DateTimeField(null=True, blank=True) + finish_time = models.DateTimeField(null=True, blank=True) + time_seconds = models.IntegerField(null=True, blank=True) + time_behind = models.IntegerField(null=True, blank=True) + time_adjusted = models.IntegerField(null=True, blank=True) + time_penalty = models.IntegerField(null=True, blank=True) + time_bonus = models.IntegerField(null=True, blank=True) + time_neutralization = models.IntegerField(null=True, blank=True) + points_final = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_adjusted = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_penalty = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_bonus = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + stage_order = models.IntegerField(null=True, blank=True) + leg_number = models.IntegerField(null=True, blank=True) + note = models.TextField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "runner_results" + + def __str__(self): + return f"Result {self.position}" if self.position else f"Result {self.id}" diff --git a/apps/runners/tests.py b/apps/runners/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/runners/urls.py b/apps/runners/urls.py new file mode 100644 index 0000000..d8d8dc2 --- /dev/null +++ b/apps/runners/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "runners" + +urlpatterns = [] diff --git a/apps/runners/views.py b/apps/runners/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/shared_models.py b/apps/shared_models.py new file mode 100644 index 0000000..d7a7e71 --- /dev/null +++ b/apps/shared_models.py @@ -0,0 +1,92 @@ +"""Shared base models for all unmanaged apps.""" + +from django.apps import apps as django_apps +from django.db import models + +VERBOSE_NAME_OVERRIDES = { + "Classes": "Class", + "ClassesControls": "Class Control", + "Clubs": "Club", + "ControlTypes": "Control Type", + "Controls": "Control", + "Courses": "Course", + "Events": "Event", + "Federations": "Federation", + "RawUploads": "Raw Upload", + "ResultTypes": "Result Type", + "Runners": "Runner", + "RunnerResults": "Runner Result", + "Splits": "Split", + "StageOrders": "Stage Order", + "StageTypes": "Stage Type", + "Stages": "Stage", + "Teams": "Team", + "TeamResults": "Team Result", + "Tokens": "Token", + "UploadLogs": "Upload Log", + "UsersEvents": "User Event", +} + +VERBOSE_NAME_PLURAL_OVERRIDES = { + "Classes": "Classes", +} + + +def normalize_unmanaged_model_names() -> None: + for model in django_apps.get_models(): + if not issubclass(model, BaseUnmanagedModel) or model._meta.abstract: + continue + + verbose_name = VERBOSE_NAME_OVERRIDES.get(model.__name__, model.__name__) + model._meta.verbose_name = verbose_name + model._meta.verbose_name_plural = VERBOSE_NAME_PLURAL_OVERRIDES.get( + model.__name__, + f"{verbose_name}s", + ) + + +class BaseUnmanagedModel(models.Model): + id = models.CharField(max_length=36, primary_key=True) + created = models.DateTimeField(null=True, blank=True) + modified = models.DateTimeField(null=True, blank=True) + deleted = models.DateTimeField(null=True, blank=True) + + class Meta: + abstract = True + managed = False + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + if getattr(cls._meta, "abstract", False): + return + + verbose_name = VERBOSE_NAME_OVERRIDES.get(cls.__name__, cls.__name__) + cls._meta.verbose_name = verbose_name + cls._meta.verbose_name_plural = VERBOSE_NAME_PLURAL_OVERRIDES.get( + cls.__name__, + f"{verbose_name}s", + ) + + +class StageScopedModel(BaseUnmanagedModel): + """Base model for entities scoped to events and stages.""" + + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="%(app_label)s_%(class)s_event", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="%(app_label)s_%(class)s_stage", + ) + + class Meta(BaseUnmanagedModel.Meta): + abstract = True diff --git a/apps/splits/__init__.py b/apps/splits/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/splits/admin.py b/apps/splits/admin.py new file mode 100644 index 0000000..06f6807 --- /dev/null +++ b/apps/splits/admin.py @@ -0,0 +1,33 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Splits + + +@admin.register(Splits) +class SplitsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "runner", + "station", + "order_number", + "is_intermediate", + "reading_time", + "created", + ] + search_fields = ["sicard", "station"] + list_filter = ["event", "stage", "is_intermediate"] + list_select_related = [ + "event", + "stage", + "runner", + "runner_result", + "team_result", + "entry_class", + "control", + ] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/splits/apps.py b/apps/splits/apps.py new file mode 100644 index 0000000..0a07359 --- /dev/null +++ b/apps/splits/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class SplitsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.splits" diff --git a/apps/splits/migrations/__init__.py b/apps/splits/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/splits/models.py b/apps/splits/models.py new file mode 100644 index 0000000..3e69d2b --- /dev/null +++ b/apps/splits/models.py @@ -0,0 +1,77 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Splits(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="splits_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="splits_rows", + ) + runner = models.ForeignKey( + "runners.Runners", + on_delete=models.DO_NOTHING, + db_column="runner_id", + null=True, + blank=True, + related_name="splits_rows", + ) + runner_result = models.ForeignKey( + "runners.RunnerResults", + on_delete=models.DO_NOTHING, + db_column="runner_result_id", + null=True, + blank=True, + related_name="splits_rows", + ) + team_result = models.ForeignKey( + "teams.TeamResults", + on_delete=models.DO_NOTHING, + db_column="team_result_id", + null=True, + blank=True, + related_name="splits_rows", + ) + entry_class = models.ForeignKey( + "classes.Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + null=True, + blank=True, + related_name="splits_rows", + ) + control = models.ForeignKey( + "controls.Controls", + on_delete=models.DO_NOTHING, + db_column="control_id", + null=True, + blank=True, + related_name="splits_rows", + ) + sicard = models.CharField(max_length=50, null=True, blank=True) + station = models.IntegerField(null=True, blank=True) + points = models.DecimalField(max_digits=10, decimal_places=3, null=True, blank=True) + order_number = models.IntegerField(null=True, blank=True) + is_intermediate = models.BooleanField(default=False) + reading_time = models.DateTimeField(null=True, blank=True) + battery_perc = models.DecimalField( + max_digits=6, decimal_places=2, null=True, blank=True + ) + battery_time = models.DateTimeField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "splits" + + def __str__(self): + return f"Split {self.station}" if self.station else f"Split {self.id}" diff --git a/apps/splits/tests.py b/apps/splits/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/splits/urls.py b/apps/splits/urls.py new file mode 100644 index 0000000..0923a9a --- /dev/null +++ b/apps/splits/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "splits" + +urlpatterns = [] diff --git a/apps/splits/views.py b/apps/splits/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/stages/__init__.py b/apps/stages/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/stages/admin.py b/apps/stages/admin.py new file mode 100644 index 0000000..6204b5e --- /dev/null +++ b/apps/stages/admin.py @@ -0,0 +1,50 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import StageOrders, Stages, StageTypes + + +@admin.register(StageTypes) +class StageTypesAdmin(TimestampedModelAdmin): + list_display = ["id", "description", "created"] + search_fields = ["description"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(Stages) +class StagesAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage_type", + "description", + "order_number", + "created", + ] + search_fields = ["description"] + list_filter = ["event", "stage_type", "order_number"] + list_select_related = ["event", "stage_type"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(StageOrders) +class StageOrdersAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "original_stage", + "stage_order", + "description", + "created", + ] + search_fields = ["description"] + list_filter = ["event", "stage_order"] + list_select_related = ["event", "stage", "original_stage"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/stages/apps.py b/apps/stages/apps.py new file mode 100644 index 0000000..dc6c6b7 --- /dev/null +++ b/apps/stages/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class StagesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.stages" diff --git a/apps/stages/migrations/__init__.py b/apps/stages/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/stages/models.py b/apps/stages/models.py new file mode 100644 index 0000000..a21938e --- /dev/null +++ b/apps/stages/models.py @@ -0,0 +1,78 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class StageTypes(BaseUnmanagedModel): + description = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "stage_types" + + def __str__(self): + return self.description or f"StageType {self.id}" + + +class Stages(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="stages_rows", + ) + stage_type = models.ForeignKey( + "StageTypes", + on_delete=models.DO_NOTHING, + db_column="stage_type_id", + null=True, + blank=True, + related_name="stages_rows", + ) + description = models.CharField(max_length=255, null=True, blank=True) + base_date = models.DateField(null=True, blank=True) + base_time = models.TimeField(null=True, blank=True) + order_number = models.IntegerField(null=True, blank=True) + server_offset = models.IntegerField(null=True, blank=True) + utc_value = models.IntegerField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "stages" + + def __str__(self): + return self.description or f"Stage {self.id}" + + +class StageOrders(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="stage_orders_rows", + ) + stage = models.ForeignKey( + "Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="stage_orders_rows", + ) + original_stage = models.ForeignKey( + "Stages", + on_delete=models.DO_NOTHING, + db_column="original_stage_id", + null=True, + blank=True, + related_name="original_stage_orders_rows", + ) + stage_order = models.IntegerField(null=True, blank=True) + description = models.CharField(max_length=255, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "stage_orders" + + def __str__(self): + return self.description or f"StageOrder {self.id}" diff --git a/apps/stages/tests.py b/apps/stages/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/stages/urls.py b/apps/stages/urls.py new file mode 100644 index 0000000..11928f1 --- /dev/null +++ b/apps/stages/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "stages" + +urlpatterns = [] diff --git a/apps/stages/views.py b/apps/stages/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/teams/__init__.py b/apps/teams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/teams/admin.py b/apps/teams/admin.py new file mode 100644 index 0000000..3f43d12 --- /dev/null +++ b/apps/teams/admin.py @@ -0,0 +1,52 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import TeamResults, Teams + + +@admin.register(Teams) +class TeamsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "team_name", + "entry_class", + "club", + "bib_number", + "is_nc", + "created", + ] + search_fields = ["team_name"] + list_filter = ["event", "stage", "is_nc"] + list_select_related = ["event", "stage", "entry_class", "club"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] + + +@admin.register(TeamResults) +class TeamResultsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "team", + "entry_class", + "position", + "status_code", + "time_seconds", + "points_final", + "created", + ] + search_fields = ["team__team_name"] + list_filter = [ + "event", + "stage", + "status_code", + "result_type", + ] + list_select_related = ["event", "stage", "team", "entry_class", "result_type"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/teams/apps.py b/apps/teams/apps.py new file mode 100644 index 0000000..77d9755 --- /dev/null +++ b/apps/teams/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TeamsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.teams" diff --git a/apps/teams/migrations/__init__.py b/apps/teams/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/teams/models.py b/apps/teams/models.py new file mode 100644 index 0000000..262eb42 --- /dev/null +++ b/apps/teams/models.py @@ -0,0 +1,124 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Teams(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="teams_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="teams_rows", + ) + team_name = models.CharField(max_length=255, null=True, blank=True) + entry_class = models.ForeignKey( + "classes.Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + null=True, + blank=True, + related_name="teams_rows", + ) + club = models.ForeignKey( + "clubs.Clubs", + on_delete=models.DO_NOTHING, + db_column="club_id", + null=True, + blank=True, + related_name="teams_rows", + ) + bib_number = models.IntegerField(null=True, blank=True) + is_nc = models.BooleanField(default=False) + eligibility = models.CharField(max_length=50, null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "teams" + + def __str__(self): + return self.team_name or f"Team {self.id}" + + +class TeamResults(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="team_results_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="team_results_rows", + ) + team = models.ForeignKey( + "Teams", + on_delete=models.DO_NOTHING, + db_column="team_id", + null=True, + blank=True, + related_name="team_results_rows", + ) + entry_class = models.ForeignKey( + "classes.Classes", + on_delete=models.DO_NOTHING, + db_column="class_id", + null=True, + blank=True, + related_name="team_results_rows", + ) + result_type = models.ForeignKey( + "resulttypes.ResultTypes", + on_delete=models.DO_NOTHING, + db_column="result_type_id", + null=True, + blank=True, + related_name="team_results_rows", + ) + upload_hash = models.CharField(max_length=255, null=True, blank=True) + upload_type = models.CharField(max_length=100, null=True, blank=True) + position = models.IntegerField(null=True, blank=True) + status_code = models.IntegerField(null=True, blank=True) + start_time = models.DateTimeField(null=True, blank=True) + finish_time = models.DateTimeField(null=True, blank=True) + time_seconds = models.IntegerField(null=True, blank=True) + time_behind = models.IntegerField(null=True, blank=True) + time_adjusted = models.IntegerField(null=True, blank=True) + time_penalty = models.IntegerField(null=True, blank=True) + time_bonus = models.IntegerField(null=True, blank=True) + time_neutralization = models.IntegerField(null=True, blank=True) + points_final = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_adjusted = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_penalty = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + points_bonus = models.DecimalField( + max_digits=12, decimal_places=3, null=True, blank=True + ) + stage_order = models.IntegerField(null=True, blank=True) + note = models.TextField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "team_results" + + def __str__(self): + return ( + f"TeamResult {self.position}" if self.position else f"TeamResult {self.id}" + ) diff --git a/apps/teams/tests.py b/apps/teams/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/teams/urls.py b/apps/teams/urls.py new file mode 100644 index 0000000..3af0dc2 --- /dev/null +++ b/apps/teams/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "teams" + +urlpatterns = [] diff --git a/apps/teams/views.py b/apps/teams/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tokens/__init__.py b/apps/tokens/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tokens/admin.py b/apps/tokens/admin.py new file mode 100644 index 0000000..9033b82 --- /dev/null +++ b/apps/tokens/admin.py @@ -0,0 +1,14 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import Tokens + + +@admin.register(Tokens) +class TokensAdmin(TimestampedModelAdmin): + list_display = ["id", "token", "foreign_model", "expires", "created"] + search_fields = ["token", "foreign_model"] + list_filter = ["foreign_model"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/tokens/apps.py b/apps/tokens/apps.py new file mode 100644 index 0000000..7ff2342 --- /dev/null +++ b/apps/tokens/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class TokensConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.tokens" diff --git a/apps/tokens/migrations/__init__.py b/apps/tokens/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tokens/models.py b/apps/tokens/models.py new file mode 100644 index 0000000..c7a2eea --- /dev/null +++ b/apps/tokens/models.py @@ -0,0 +1,15 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class Tokens(BaseUnmanagedModel): + token = models.CharField(max_length=255, null=True, blank=True) + foreign_key = models.CharField(max_length=36, null=True, blank=True) + foreign_model = models.CharField(max_length=120, null=True, blank=True) + expires = models.DateTimeField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "tokens" + + def __str__(self): + return f"Token {self.token[:10]}" if self.token else f"Token {self.id}" diff --git a/apps/tokens/tests.py b/apps/tokens/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/tokens/urls.py b/apps/tokens/urls.py new file mode 100644 index 0000000..df9f004 --- /dev/null +++ b/apps/tokens/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "tokens" + +urlpatterns = [] diff --git a/apps/tokens/views.py b/apps/tokens/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/uploadlogs/__init__.py b/apps/uploadlogs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/uploadlogs/admin.py b/apps/uploadlogs/admin.py new file mode 100644 index 0000000..00e71f9 --- /dev/null +++ b/apps/uploadlogs/admin.py @@ -0,0 +1,23 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import UploadLogs + + +@admin.register(UploadLogs) +class UploadLogsAdmin(TimestampedModelAdmin): + list_display = [ + "id", + "event", + "stage", + "upload_type", + "upload_status", + "state", + "created", + ] + search_fields = ["upload_type", "info"] + list_filter = ["event", "stage", "upload_type", "upload_status"] + list_select_related = ["event", "stage"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/uploadlogs/apps.py b/apps/uploadlogs/apps.py new file mode 100644 index 0000000..2b6d32c --- /dev/null +++ b/apps/uploadlogs/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UploadlogsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.uploadlogs" diff --git a/apps/uploadlogs/migrations/__init__.py b/apps/uploadlogs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/uploadlogs/models.py b/apps/uploadlogs/models.py new file mode 100644 index 0000000..bde5a4f --- /dev/null +++ b/apps/uploadlogs/models.py @@ -0,0 +1,35 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class UploadLogs(BaseUnmanagedModel): + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + null=True, + blank=True, + related_name="upload_logs_rows", + ) + stage = models.ForeignKey( + "stages.Stages", + on_delete=models.DO_NOTHING, + db_column="stage_id", + null=True, + blank=True, + related_name="upload_logs_rows", + ) + upload_type = models.CharField(max_length=100, null=True, blank=True) + upload_status = models.IntegerField(null=True, blank=True) + state = models.IntegerField(null=True, blank=True) + info = models.TextField(null=True, blank=True) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "upload_logs" + + def __str__(self): + return ( + f"UploadLog {self.upload_type}" + if self.upload_type + else f"UploadLog {self.id}" + ) diff --git a/apps/uploadlogs/tests.py b/apps/uploadlogs/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/uploadlogs/urls.py b/apps/uploadlogs/urls.py new file mode 100644 index 0000000..23d7940 --- /dev/null +++ b/apps/uploadlogs/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "uploadlogs" + +urlpatterns = [] diff --git a/apps/uploadlogs/views.py b/apps/uploadlogs/views.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/users/admin.py b/apps/users/admin.py index 8c38f3f..1b32c1f 100644 --- a/apps/users/admin.py +++ b/apps/users/admin.py @@ -1,3 +1,47 @@ +from apps.admin_base import TimestampedModelAdmin from django.contrib import admin -# Register your models here. +from .models import AccessToken, User + + +@admin.register(User) +class UserAdmin(TimestampedModelAdmin): + list_display = ( + "email", + "username", + "first_name", + "last_name", + "is_admin", + "is_super", + "is_staff", + ) + search_fields = ( + "email", + "first_name", + "last_name", + ) + list_filter = ( + "is_admin", + "is_super", + ) + readonly_fields = ( + "username", + "is_staff", + "is_superuser", + ) + + +@admin.register(AccessToken) +class AccessTokenAdmin(TimestampedModelAdmin): + list_display = ( + "access_token", + "client_id", + "user", + "expires", + ) + search_fields = ( + "access_token", + "client_id", + "user__email", + ) + list_select_related = ("user",) diff --git a/apps/users/apps.py b/apps/users/apps.py index 37ba421..05680ea 100644 --- a/apps/users/apps.py +++ b/apps/users/apps.py @@ -4,3 +4,8 @@ class UsersConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.users" + + def ready(self): + from apps.shared_models import normalize_unmanaged_model_names + + normalize_unmanaged_model_names() diff --git a/apps/users/models.py b/apps/users/models.py index 4e2b082..6c64c83 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -1,8 +1,41 @@ +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.hashers import check_password as django_check_password +from django.contrib.auth.hashers import make_password from django.db import models +from django.utils.crypto import salted_hmac from django.utils.timezone import now +try: + import bcrypt +except ImportError: # pragma: no cover - dependency is installed in Docker + bcrypt = None + + +class UserManager(BaseUserManager): + def get_by_natural_key(self, email): + return self.get(email=email) + + def create_user(self, email, password=None, **extra_fields): + if not email: + raise ValueError("The email field is required") + email = self.normalize_email(email) + user = self.model(email=email, **extra_fields) + if password: + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password=None, **extra_fields): + extra_fields.setdefault("is_admin", True) + extra_fields.setdefault("is_super", True) + return self.create_user(email, password, **extra_fields) + class User(models.Model): + USERNAME_FIELD = "email" + EMAIL_FIELD = "email" + REQUIRED_FIELDS = [] + id = models.CharField(max_length=36, primary_key=True) email = models.EmailField(max_length=160, unique=True) password = models.CharField(max_length=128, null=True, blank=True) @@ -14,15 +47,75 @@ class User(models.Model): modified = models.DateTimeField(auto_now=True, null=True, blank=True) deleted = models.DateTimeField(null=True, blank=True) + objects = UserManager() + def delete(self, using=None, keep_parents=False): self.deleted = now() self.save() - objects = models.Manager() - class Meta: db_table = "users" + def __str__(self): + return self.email + + def get_username(self): + return self.email + + def set_password(self, raw_password): + self.password = make_password(raw_password) + + def check_password(self, raw_password): + if not self.password or raw_password is None: + return False + + if self.password.startswith("$2y$") or self.password.startswith("$2b$"): + if bcrypt is None: + return False + encoded_password = self.password.replace("$2y$", "$2b$", 1).encode("utf-8") + return bcrypt.checkpw(raw_password.encode("utf-8"), encoded_password) + + return django_check_password(raw_password, self.password) + + def get_session_auth_hash(self): + return salted_hmac( + f"{self.__class__.__module__}.{self.__class__.__name__}", + self.password or "", + ).hexdigest() + + @property + def is_authenticated(self): + return True + + @property + def is_anonymous(self): + return False + + @property + def is_active(self): + return self.deleted is None + + @property + def username(self): + return self.email + + @property + def is_staff(self): + return self.is_admin + + @property + def is_superuser(self): + return self.is_super + + def has_perm(self, perm, obj=None): + return bool(self.is_admin or self.is_super) + + def has_perms(self, perm_list, obj=None): + return all(self.has_perm(perm, obj=obj) for perm in perm_list) + + def has_module_perms(self, app_label): + return bool(self.is_admin or self.is_super) + class AccessToken(models.Model): access_token = models.CharField(max_length=40, primary_key=True) diff --git a/apps/usersevents/__init__.py b/apps/usersevents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/usersevents/admin.py b/apps/usersevents/admin.py new file mode 100644 index 0000000..453745e --- /dev/null +++ b/apps/usersevents/admin.py @@ -0,0 +1,15 @@ +from apps.admin_base import TimestampedModelAdmin +from django.contrib import admin + +from .models import UsersEvents + + +@admin.register(UsersEvents) +class UsersEventsAdmin(TimestampedModelAdmin): + list_display = ["user", "event", "created"] + search_fields = ["user__email", "event__description"] + list_filter = ["event"] + list_select_related = ["user", "event"] + list_per_page = 50 + show_full_result_count = False + ordering = ["-created"] diff --git a/apps/usersevents/apps.py b/apps/usersevents/apps.py new file mode 100644 index 0000000..58eca00 --- /dev/null +++ b/apps/usersevents/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UserseventsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "apps.usersevents" diff --git a/apps/usersevents/migrations/__init__.py b/apps/usersevents/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/usersevents/models.py b/apps/usersevents/models.py new file mode 100644 index 0000000..a7be0bc --- /dev/null +++ b/apps/usersevents/models.py @@ -0,0 +1,25 @@ +from apps.shared_models import BaseUnmanagedModel +from django.db import models + + +class UsersEvents(BaseUnmanagedModel): + id = None + user = models.ForeignKey( + "users.User", + on_delete=models.DO_NOTHING, + db_column="user_id", + primary_key=True, + related_name="users_events_rows", + ) + event = models.ForeignKey( + "events.Events", + on_delete=models.DO_NOTHING, + db_column="event_id", + related_name="users_events_rows", + ) + + class Meta(BaseUnmanagedModel.Meta): + db_table = "users_events" + + def __str__(self): + return f"UserEvent {self.user_id} -> {self.event_id}" diff --git a/apps/usersevents/tests.py b/apps/usersevents/tests.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/usersevents/urls.py b/apps/usersevents/urls.py new file mode 100644 index 0000000..7a4270a --- /dev/null +++ b/apps/usersevents/urls.py @@ -0,0 +1,5 @@ +from django.urls import path + +app_name = "usersevents" + +urlpatterns = [] diff --git a/apps/usersevents/views.py b/apps/usersevents/views.py new file mode 100644 index 0000000..e69de29 diff --git a/docker-compose-python.yaml b/docker-compose-python.yaml index eee3ea7..74a2de7 100644 --- a/docker-compose-python.yaml +++ b/docker-compose-python.yaml @@ -11,12 +11,12 @@ services: volumes: - .:/app/ env_file: - - .env.example + - .env extra_hosts: - "host.docker.internal:host-gateway" networks: - - oreplay_backend_oreplay_network + - oreplay-backend_oreplay_network networks: - oreplay_backend_oreplay_network: + oreplay-backend_oreplay_network: external: true \ No newline at end of file diff --git a/oreplay/environments/base.py b/oreplay/environments/base.py index 361e657..609b1aa 100644 --- a/oreplay/environments/base.py +++ b/oreplay/environments/base.py @@ -31,6 +31,8 @@ API_VERSION = config("API_VERSION", default="v1") +AUTH_USER_MODEL = "users.User" + # Base de datos (esto se puede hacer en local.py también) DATABASES = { "default": { @@ -53,8 +55,31 @@ "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", - "apps.organizers", + # Core apps "apps.users", + "apps.organizers", + # Event and structure apps + "apps.federations", + "apps.events", + "apps.stages", + "apps.courses", + "apps.controls", + "apps.classes", + # Team and club apps + "apps.clubs", + "apps.teams", + # Participant apps + "apps.runners", + # Result apps + "apps.resulttypes", + "apps.splits", + # Administrative apps + "apps.tokens", + "apps.uploadlogs", + "apps.rawuploads", + "apps.rankings", + "apps.usersevents", + # Third-party "drf_spectacular", ]