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 @@ { submission.owner } { submission.organization.name } { get_score(column, submission) } - Show detailed results + Show detailed results @@ -121,8 +121,6 @@ CODALAB.api.get_leaderboard_for_render(self.phase_id) .done(responseData => { self.selected_leaderboard = responseData - - self.columns = [] // Make fake task and columns for Metadata so it can be filtered like columns if(self.selected_leaderboard.fact_sheet_keys){ diff --git a/src/static/riot/competitions/detail/submission_upload.tag b/src/static/riot/competitions/detail/submission_upload.tag index 4172bd78a..f1405cfb9 100644 --- a/src/static/riot/competitions/detail/submission_upload.tag +++ b/src/static/riot/competitions/detail/submission_upload.tag @@ -4,6 +4,8 @@

Submission upload

+
This phase has ended and no longer accepts submissions!
+
This phase hasn't started yet!

Metadata or Fact Sheet

@@ -349,17 +351,33 @@ } self.check_can_upload = function () { - CODALAB.api.can_make_submissions(self.selected_phase.id) - .done(function (data) { - if (data.can) { - self.prepare_upload(self.upload)() - } else { - toastr.error(data.reason) + + // Check if selected phase accepts submissions (within the deadline of the phase) + if(self.selected_phase.phase_started && !self.selected_phase.phase_ended){ + + CODALAB.api.can_make_submissions(self.selected_phase.id) + .done(function (data) { + if (data.can) { + self.prepare_upload(self.upload)() + } else { + toastr.error(data.reason) + } + }) + .fail(function (data) { + toastr.error('Could not verify your ability to make a submission') + }) + }else{ + // Error when phase is not accepting submissions + if(!self.selected_phase.phase_started){ + toastr.error('This phase has not started yet. Please check the phase start date!') + + }else { + if(self.selected_phase.phase_ended){ + toastr.error('This phase has ended and no longer accepts submissions!') } - }) - .fail(function (data) { - toastr.error('Could not verify your ability to make a submission') - }) + } + self.clear_form() + } } self.get_fact_sheet_answers = function () { diff --git a/src/static/riot/competitions/editor/_leaderboard.tag b/src/static/riot/competitions/editor/_leaderboard.tag index 6dad1fc7f..dce33e22b 100644 --- a/src/static/riot/competitions/editor/_leaderboard.tag +++ b/src/static/riot/competitions/editor/_leaderboard.tag @@ -179,6 +179,12 @@ + + Column Precision * + + + + Hidden @@ -392,6 +398,7 @@ id: data.id, title: data.title, key: data.key, + precision : (data.precision === undefined) ? 2 : data.precision , submission_rule: self.selected_submission_rule, hidden: self.refs.hidden_leaderboard.checked, primary_index: _.get($('input[name=primary_index]:checked').data(), 'index', 0), @@ -400,6 +407,7 @@ index: i, title: _.get(data, `title_${i}`), key: _.get(data, `column_key_${i}`), + precision: _.get(data, `column_precision_${i}`), sorting: _.get(data, `sorting_${i}`), hidden: self.refs[`hidden_${i}`].checked, } diff --git a/src/templates/registration/login.html b/src/templates/registration/login.html index f4b802cb2..55d8f10cf 100644 --- a/src/templates/registration/login.html +++ b/src/templates/registration/login.html @@ -24,7 +24,7 @@