diff --git a/src/apps/api/serializers/leaderboards.py b/src/apps/api/serializers/leaderboards.py index 444425861..75834f7da 100644 --- a/src/apps/api/serializers/leaderboards.py +++ b/src/apps/api/serializers/leaderboards.py @@ -24,6 +24,7 @@ class Meta: 'sorting', 'index', 'hidden', + 'precision', ) def validate(self, attrs): diff --git a/src/apps/api/views/competitions.py b/src/apps/api/views/competitions.py index fde3aedf9..342e237ce 100644 --- a/src/apps/api/views/competitions.py +++ b/src/apps/api/views/competitions.py @@ -482,8 +482,21 @@ def get_leaderboard(self, request, pk): 'organization': submission['organization'], }) for score in submission['scores']: + + # default precision is set to 2 + precision = 2 + + # loop over columns to find a column with the same index + # replace default precision with column precision + for col in columns: + if col["index"] == score["index"]: + precision = col["precision"] + break + tempScore = score tempScore['task_id'] = submission['task'] + # round the score to 'precision' decimal points + tempScore['score'] = str(round(float(tempScore["score"]), precision)) response['submissions'][submissions_keys[submission_key]]['scores'].append(tempScore) for task in query['tasks']: diff --git a/src/apps/competitions/migrations/0031_auto_20230504_1016.py b/src/apps/competitions/migrations/0031_auto_20230504_1016.py new file mode 100644 index 000000000..7b645906c --- /dev/null +++ b/src/apps/competitions/migrations/0031_auto_20230504_1016.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2023-05-04 10:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('competitions', '0030_submission_started_when'), + ] + + operations = [ + migrations.AlterField( + model_name='competition', + name='docker_image', + field=models.CharField(default='codalab/codalab-legacy:py37', max_length=128), + ), + ] diff --git a/src/apps/competitions/tests/test_v15_unpacker.py b/src/apps/competitions/tests/test_v15_unpacker.py index 24ac3137f..cc5aabc96 100644 --- a/src/apps/competitions/tests/test_v15_unpacker.py +++ b/src/apps/competitions/tests/test_v15_unpacker.py @@ -34,4 +34,4 @@ def test_page_unpacking(self): def test_leaderboard_unpacking(self): self.unpacker._unpack_leaderboards() - assert self.unpacker.competition['leaderboards'] == test_data.LEADERBOARDS + assert self.unpacker.competition['leaderboards'] == test_data.V1_LEADERBOARDS diff --git a/src/apps/competitions/tests/test_v2_unpacker.py b/src/apps/competitions/tests/test_v2_unpacker.py index 8aa64a7b3..28d269212 100644 --- a/src/apps/competitions/tests/test_v2_unpacker.py +++ b/src/apps/competitions/tests/test_v2_unpacker.py @@ -37,7 +37,7 @@ def test_page_unpacking(self): def test_leaderboard_unpacking(self): self.unpacker._unpack_leaderboards() - assert self.unpacker.competition['leaderboards'] == test_data.LEADERBOARDS + assert self.unpacker.competition['leaderboards'] == test_data.V2_LEADERBOARDS def test_competition_type_if_not_set(self): assert self.unpacker.competition['competition_type'] == 'competition' diff --git a/src/apps/competitions/tests/unpacker_test_data.py b/src/apps/competitions/tests/unpacker_test_data.py index 5172e261b..5b6f4cf97 100644 --- a/src/apps/competitions/tests/unpacker_test_data.py +++ b/src/apps/competitions/tests/unpacker_test_data.py @@ -143,7 +143,28 @@ # Truth Data # ------------------------------------------------- -LEADERBOARDS = [{ +V1_LEADERBOARDS = [{ + "title": "Results", + "key": "Results", + "columns": [ + { + "title": "prediction_score", + "key": "prediction_score", + "index": 0, + "sorting": "desc", + "precision": 4, + }, + { + "title": "Duration", + "key": "Duration", + "index": 1, + "sorting": "desc", + "precision": 2, + } + ] +}] + +V2_LEADERBOARDS = [{ "title": "Results", "key": "Results", "columns": [ diff --git a/src/apps/competitions/unpackers/v1.py b/src/apps/competitions/unpackers/v1.py index 897f6be02..8afe291be 100644 --- a/src/apps/competitions/unpackers/v1.py +++ b/src/apps/competitions/unpackers/v1.py @@ -158,7 +158,9 @@ def _unpack_leaderboards(self): 'title': column['title'], 'key': column['title'], 'index': index, - 'sorting': column.get('sort') or 'desc' + 'sorting': column.get('sort') or 'desc', + # get precision as numeric_format, if not found, use default value = 2 + 'precision': column.get('numeric_format', 2) } for leaderboard_data in self.competition['leaderboards']: diff --git a/src/apps/competitions/views.py b/src/apps/competitions/views.py index 7a4045f7f..8d3e46720 100644 --- a/src/apps/competitions/views.py +++ b/src/apps/competitions/views.py @@ -2,7 +2,7 @@ from django.http import Http404 from django.views.generic import TemplateView, DetailView -from .models import Competition +from .models import Competition, CompetitionParticipant class CompetitionManagement(LoginRequiredMixin, TemplateView): @@ -29,8 +29,12 @@ def get_object(self, *args, **kwargs): competition = super().get_object(*args, **kwargs) is_creator = self.request.user.is_superuser or self.request.user == competition.created_by is_collaborator = self.request.user in competition.collaborators.all() + + # get participants from CompetitionParticipant where user=user and competition=competition + is_participant = CompetitionParticipant.objects.filter(user=self.request.user, competition=competition).count() > 0 + valid_secret_key = self.request.GET.get('secret_key') == str(competition.secret_key) - if is_creator or is_collaborator or competition.published or valid_secret_key: + if is_creator or is_collaborator or competition.published or valid_secret_key or is_participant: return competition raise Http404() diff --git a/src/apps/leaderboards/migrations/0008_column_precision.py b/src/apps/leaderboards/migrations/0008_column_precision.py new file mode 100644 index 000000000..e09e0fbb8 --- /dev/null +++ b/src/apps/leaderboards/migrations/0008_column_precision.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.17 on 2023-05-04 10:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('leaderboards', '0007_auto_20201110_1731'), + ] + + operations = [ + migrations.AddField( + model_name='column', + name='precision', + field=models.IntegerField(default=2), + ), + ] diff --git a/src/apps/leaderboards/models.py b/src/apps/leaderboards/models.py index 00dcf0248..4c9eda5a2 100644 --- a/src/apps/leaderboards/models.py +++ b/src/apps/leaderboards/models.py @@ -62,6 +62,7 @@ class Column(models.Model): index = models.PositiveIntegerField() leaderboard = models.ForeignKey(Leaderboard, on_delete=models.CASCADE, related_name="columns") hidden = models.BooleanField(default=False) + precision = models.IntegerField(default=2) class Meta: unique_together = ('leaderboard', 'key') diff --git a/src/apps/profiles/backends.py b/src/apps/profiles/backends.py new file mode 100644 index 000000000..dd5cdd502 --- /dev/null +++ b/src/apps/profiles/backends.py @@ -0,0 +1,24 @@ +from django.contrib.auth.backends import ModelBackend +from django.core.exceptions import ObjectDoesNotExist +from django.contrib.auth import get_user_model + +User = get_user_model() + + +class EmailAuthenticationBackend(ModelBackend): + def authenticate(self, request, username=None, password=None, **kwargs): + try: + user = User.objects.get(email=username) + if user.check_password(password): + return user + else: + return None + except ObjectDoesNotExist: + return None + + def get_user(self, user_id): + try: + user = User.objects.get(id=user_id) + return user + except ObjectDoesNotExist: + return None diff --git a/src/apps/profiles/forms.py b/src/apps/profiles/forms.py index ba75c6a55..7b4ad71e8 100644 --- a/src/apps/profiles/forms.py +++ b/src/apps/profiles/forms.py @@ -27,3 +27,9 @@ class Meta: model = User fields = ("username", "email", "password1", "password2") + + +class LoginForm(forms.Form): + + username = forms.CharField(max_length=150) + password = forms.CharField(max_length=150, widget=forms.PasswordInput) diff --git a/src/apps/profiles/urls_accounts.py b/src/apps/profiles/urls_accounts.py index 86321d24e..03acf8254 100644 --- a/src/apps/profiles/urls_accounts.py +++ b/src/apps/profiles/urls_accounts.py @@ -7,9 +7,10 @@ urlpatterns = [ url(r'^signup', views.sign_up, name="signup"), + path('login/', views.log_in, name='login'), # url(r'^user_profile', views.user_profile, name="user_profile"), # path('login/', auth_views.LoginView.as_view(extra_context=extra_context), name='login'), - path('login/', views.LoginView.as_view(), name='login'), + # path('login/', views.LoginView.as_view(), name='login'), # path('logout/', auth_views.LogoutView.as_view(), name='logout'), path('logout/', views.LogoutView.as_view(), name='logout'), path('password_reset/', views.CustomPasswordResetView.as_view(), name='password_reset'), diff --git a/src/apps/profiles/views.py b/src/apps/profiles/views.py index fe02bdee9..8abbf94b2 100644 --- a/src/apps/profiles/views.py +++ b/src/apps/profiles/views.py @@ -3,7 +3,7 @@ from django.conf import settings from django.contrib import messages -from django.contrib.auth import authenticate +from django.contrib.auth import authenticate, login from django.contrib.sites.shortcuts import get_current_site from django.core.mail import EmailMessage, EmailMultiAlternatives from django.http import Http404 @@ -18,7 +18,7 @@ from api.serializers.profiles import UserSerializer, OrganizationDetailSerializer, OrganizationEditSerializer, \ UserNotificationSerializer -from .forms import SignUpForm +from .forms import SignUpForm, LoginForm from .models import User, Organization, Membership from .tokens import account_activation_token @@ -128,6 +128,33 @@ def sign_up(request): return render(request, 'registration/signup.html', context) +def log_in(request): + + context = {} + context['chahub_signup_url'] = "{}/profiles/signup?next={}/social/login/chahub".format( + settings.SOCIAL_AUTH_CHAHUB_BASE_URL, + settings.SITE_DOMAIN + ) + if request.method == 'POST': + form = LoginForm(request.POST) + + if form.is_valid(): + username = form.cleaned_data.get('username') + password = form.cleaned_data.get('password') + user = authenticate(username=username, password=password) + if user: + login(request, user) + return redirect('pages:home') + else: + messages.error(request, "Wrong Credentials!") + else: + context['form'] = form + + if not context.get('form'): + context['form'] = LoginForm() + return render(request, 'registration/login.html', context) + + # Password Reset views/forms below # auth_forms class CustomPasswordResetForm(auth_forms.PasswordResetForm): diff --git a/src/settings/base.py b/src/settings/base.py index 79e1d2381..64ab54589 100644 --- a/src/settings/base.py +++ b/src/settings/base.py @@ -117,6 +117,7 @@ 'utils.oauth_backends.ChahubOAuth2', 'django.contrib.auth.backends.ModelBackend', 'django_su.backends.SuBackend', + 'profiles.backends.EmailAuthenticationBackend', ) SOCIAL_AUTH_PIPELINE = ( diff --git a/src/static/riot/competitions/detail/_tabs.tag b/src/static/riot/competitions/detail/_tabs.tag index 15a8246b3..719308664 100644 --- a/src/static/riot/competitions/detail/_tabs.tag +++ b/src/static/riot/competitions/detail/_tabs.tag @@ -280,6 +280,40 @@ }) }) + // loop over competition phases to mark if phase has started or ended + self.competition.phases.forEach(function (phase, index) { + + phase_ended = false + phase_started = false + + // check if phase has started + if((Date.parse(phase["start"]) - Date.parse(new Date())) > 0){ + // start date is in the future, phase started = NO + phase_started = false + }else{ + // start date is not in the future, phase started = YES + phase_started = true + } + + if(phase_started){ + // check if end data exists for this phase + if(phase["end"]){ + if((Date.parse(phase["end"]) - Date.parse(new Date())) < 0){ + // Phase cannote accept submissions if end date is in the past + phase_ended = true + }else{ + // Phase can accept submissions if end date is in the future + phase_ended = false + } + }else{ + // Phase can accept submissions if end date is not given + phase_ended = false + } + } + self.competition.phases[index]["phase_ended"] = phase_ended + self.competition.phases[index]["phase_started"] = phase_started + }) + self.competition.is_admin = CODALAB.state.user.has_competition_admin_privileges(competition) self.selected_phase_index = _.get(_.find(self.competition.phases, {'status': 'Current'}), 'id') if (self.selected_phase_index == null) { diff --git a/src/static/riot/competitions/detail/leaderboards.tag b/src/static/riot/competitions/detail/leaderboards.tag index f23d59a01..6e476108f 100644 --- a/src/static/riot/competitions/detail/leaderboards.tag +++ b/src/static/riot/competitions/detail/leaderboards.tag @@ -57,7 +57,7 @@