-
Notifications
You must be signed in to change notification settings - Fork 784
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[REVIEW PENDING] Add Challenge creation using zip file. #1037
Conversation
18cea0a
to
73481ad
Compare
@RishabhJain2018 : just a minor suggestion here, Can you please mark WIP or COMPLETE in your PR title so that the one reviewing can easily get an overview. Also it will be great if you keep managing the state (WIP, COMPLETE, ... )after some one has reviewed. You can even use REVIEW PENDING as a state also. Thanks :) |
@taranjeet yeah sure. I will do that. 👍 |
apps/challenges/urls.py
Outdated
@@ -25,4 +25,7 @@ | |||
name='get_challenges_based_on_teams'), | |||
url(r'(?P<challenge_pk>[0-9]+)/challenge_phase_split$', views.challenge_phase_split_list, | |||
name='challenge_phase_split_list'), | |||
url(r'challenge/challenge_host_team/(?P<challenge_host_team_pk>[0-9]+)/zip/$', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am actually not clear of whether we should take challenge host team as a paramter in the api or not. Writing it down here, so that I dont forget. Will update you over this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also Can we change the api-endpoint zip
to something more apt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@taranjeet I think we we will need challenge host team
as a parameter because it is related as a Foreign Key
to Challenge
model. What do you say?
Moreover, I will change the endpoint zip
to more verbose name. 👍
apps/challenges/views.py
Outdated
DatasetSplitSerializer, | ||
LeaderboardSerializer, | ||
ZipConfigurationChallengeSerializer, | ||
ZipFileCreateChallengePhaseSplitSerializer,) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ZipFileCreateChallengePhaseSplitSerializer
is too long. Can we please shortened it and still keep it as meaningful?
apps/challenges/views.py
Outdated
return Response(response_data, status=status.HTTP_400_BAD_REQUEST) | ||
|
||
try: | ||
with transaction.atomic(): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use transaction.atomic
whenever we want to execute a set of more than db queries as a whole. Explaining this further, it means that we want either the whole set of query should be executed, or None if any of them fails.
Here we have such a use case, but i am really confused of using transaction.atomic to handle code related to http requests(like get request for zip file).
Can you please explain me your reason of using transaction.atomic
in such a way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have used transaction.atomic
because if anything fails then it will go directly into except block ans we don't have to handle cases manually. Moreover, we can have the code in minimal lines. Am I correct, please guide ?
apps/challenges/views.py
Outdated
# All files download and extract location. | ||
BASE_LOCATION = tempfile.mkdtemp() | ||
|
||
CHALLENGE_ZIP_DOWNLOAD_LOCATION = os.path.join('{}/zip_challenge.zip'.format(BASE_LOCATION)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of hardcoding the name of the zip file, can we please make it a random file name. This will help us in dealing with situations where in we get more than one request at the same timestamp. I will prefer the random name to be of 8-10 characters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ohh..I didn't thought about this case. I will update that accordingly.
apps/challenges/views.py
Outdated
CHALLENGE_ZIP_DOWNLOAD_LOCATION = os.path.join('{}/zip_challenge.zip'.format(BASE_LOCATION)) | ||
|
||
if response and response.status_code == 200: | ||
with open(str(CHALLENGE_ZIP_DOWNLOAD_LOCATION), 'w') as f: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CHALLENGE_ZIP_DOWNLOAD_LOCATION is already a string. I think there is no need of using it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also can we make variable f
as zip_file
or something related.
apps/challenges/views.py
Outdated
# Search for yaml file | ||
for name in zip_ref.namelist(): | ||
if name.endswith('.yaml') or name.endswith('.yml'): | ||
yaml_file = name |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
variable yaml_file
is defined only if there is a file ending with extension yaml
or yml
. What if there is no such file. In that case the code should not proceed further.
c60995b
to
9c17086
Compare
@deshraj @taranjeet Please review the PR. Once this is approved, I will add the test cases regarding the same. |
0589018
to
8dd4959
Compare
apps/challenges/urls.py
Outdated
@@ -25,4 +25,7 @@ | |||
name='get_challenges_based_on_teams'), | |||
url(r'(?P<challenge_pk>[0-9]+)/challenge_phase_split$', views.challenge_phase_split_list, | |||
name='challenge_phase_split_list'), | |||
url(r'challenge/challenge_host_team/(?P<challenge_host_team_pk>[0-9]+)/create-challenge-using-zip/$', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The url pattern overall is correct. But we are using http verbs to communicate things like create, delete, edit etc. So the part create-challenge-using-zip
is a bit redundant. Can we keep it something like to zip-upload
. I am open to alternatives from your side.
apps/challenges/views.py
Outdated
response = requests.get(uploaded_zip_file_path, stream=True) | ||
|
||
# All files download and extract location. | ||
BASE_LOCATION = tempfile.mkdtemp() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can move this variable outside try-except. Generally we only use part of code which is prone to through error like a network call. This is kind of abusing try-except block. Also it is not necessary to use uuid here. Just create a random string of 10 characters and use it.
apps/challenges/views.py
Outdated
# All files download and extract location. | ||
BASE_LOCATION = tempfile.mkdtemp() | ||
unique_folder_name = uuid.uuid4() | ||
CHALLENGE_ZIP_DOWNLOAD_LOCATION = os.path.join('{}/{}.zip'.format(BASE_LOCATION, unique_folder_name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since os.path.join
is used in many a place, you can simply import join by
from os.path import join
One more thing, since you are already using join, so need of using /
. You can simply wrie
CHALLENGE_ZIP_DOWNLOAD_LOCATION = join(BASE_LOCATION, '{}.zip'.format(unique_folder_name))
apps/challenges/views.py
Outdated
|
||
except: | ||
response_data = { | ||
'error': 'Some error occured during file upload. Please upload it again.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make the message to An error occured while uploading file again. Please try uploading it again.
apps/challenges/views.py
Outdated
zip_ref = zipfile.ZipFile(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'r') | ||
zip_ref.extractall('{}/{}'.format(BASE_LOCATION, unique_folder_name)) | ||
zip_ref.close() | ||
except: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we be more specific here and catch only BadZipfile
exception?
apps/challenges/views.py
Outdated
if os.path.isfile(test_annotation_file_path): | ||
challenge_test_annotation = open(test_annotation_file_path, 'rb') | ||
challenge_test_annotation_file = File(challenge_test_annotation) | ||
challenge_phase = ChallengePhase.objects.get(pk=challenge_phase.pk) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please catch this in try-catch block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this block of code is under transaction.atomic,
it will automatically go into except block on line 571. So, should we still catch it here?
apps/challenges/views.py
Outdated
# Create Challenge Phase Splits | ||
yaml_file_data_of_challenge_phase_splits = yaml_file_data['challenge_phase_splits'] | ||
for data in yaml_file_data_of_challenge_phase_splits: | ||
challenge_phase = challenge_phase_ids[data['challenge_phase_id']-1] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the logic of using -1
here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The logic for -1
here is that since I am storing all the saved Leaderboards
, Challenge Phases
, Dataset splits
in a list and the list indexing starts from 0 and the user will input the fields starting from 1,2 & so on. So, I reduced it by 1 to match the list index with user's input. Please tell if I am not clear.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can create a dict here.
apps/challenges/views.py
Outdated
if zip_config: | ||
zip_config.challenge = challenge | ||
zip_config.save() | ||
response_data = {'success': 'The Challenge is successfully created'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please pass id in the message and remove the
apps/challenges/views.py
Outdated
response_data = {'success': 'The Challenge is successfully created'} | ||
return Response(response_data, status=status.HTTP_201_CREATED) | ||
except: | ||
response_data = {'error': 'Error in challenge creation. Please try again'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we update the message to Error in creating challenge. Please try again
.
apps/challenges/views.py
Outdated
response_data = {'error': 'Error in challenge creation. Please try again'} | ||
return Response(response_data, status=status.HTTP_400_BAD_REQUEST) | ||
|
||
try: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use shutil.rmtree
for deleting folder. Also logger are for your help. We can make them more meaningful by making them more specific like adding ids or other data. So here in case folder is successfully deleted, no need of logging the message but please log proper specific message in case it fails to remove.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok sure.
8dd4959
to
4962dca
Compare
@taranjeet @deshraj I have fixed most of the review changes, but some require a discussion so, I will fix those after tonight's discussion. |
apps/challenges/urls.py
Outdated
@@ -25,4 +25,7 @@ | |||
name='get_challenges_based_on_teams'), | |||
url(r'(?P<challenge_pk>[0-9]+)/challenge_phase_split$', views.challenge_phase_split_list, | |||
name='challenge_phase_split_list'), | |||
url(r'challenge/challenge_host_team/(?P<challenge_host_team_pk>[0-9]+)/zip-upload/$', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I remember, we settled with using Underscores in the URLs and not hyphens, right?
apps/challenges/views.py
Outdated
with open(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'w') as zip_file: | ||
zip_file.write(response.content) | ||
|
||
except: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets not make the exception to be generic. Please handle the proper exceptions.
apps/challenges/views.py
Outdated
zip_ref = zipfile.ZipFile(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'r') | ||
zip_ref.extractall(join(BASE_LOCATION, unique_folder_name)) | ||
zip_ref.close() | ||
except BadZipfile: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good. :)
apps/challenges/views.py
Outdated
zip_ref.close() | ||
except BadZipfile: | ||
response_data = { | ||
'error': 'The zip file contents cannot be extracted. Please check the format.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change the error message to The zip file contents cannot be extracted. Please check the format!
apps/challenges/views.py
Outdated
|
||
if (yaml_file_count > 1) or (yaml_file_count == 0): | ||
response_data = { | ||
'error': 'There are no or more than one yaml file in zip folder.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have two different error statements for these two conditions?
apps/challenges/views.py
Outdated
else: | ||
response_data = { | ||
'error': 'There is no key for evaluation script in yaml file.'\ | ||
'Please add a key and then try uploading it again.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace .
with !
at the end.
apps/challenges/views.py
Outdated
else: | ||
response_data = { | ||
'error': 'No evaluation script is present in the zip file.'\ | ||
'Please try uploading again the zip file after adding evaluation script.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace .
with !
at the end.
apps/challenges/views.py
Outdated
response_data = { | ||
'error': 'There is no key for test annotation file for'\ | ||
'challenge phase {} in yaml file.'\ | ||
'Please add a key and then try uploading it again.'.format(data['name']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace .
with !
at the end.
apps/challenges/views.py
Outdated
response_data = { | ||
'error': 'No test annotation file is present in the zip file'\ | ||
'for challenge phase {} Please try uploading'\ | ||
'again the zip file after adding test annotation file.'.format(data['name']) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace .
with !
at the end.
apps/challenges/views.py
Outdated
save=True) | ||
|
||
# Create Leaderboard | ||
yaml_file_data_of_leaderboard = yaml_file_data['leaderboard'] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also even if there are more serializers, I think in that case we can create a serializer which simply takes list as argument and then saves it. See ChallengeParticipantTeamListSerializer in the code base.
@taranjeet you are right here.
apps/challenges/serializers.py
Outdated
class Meta: | ||
model = Challenge | ||
fields = ('id', 'title', 'short_description', 'description', 'terms_and_conditions', | ||
'submission_guidelines', 'start_date', 'end_date', 'creator', | ||
'published', 'enable_forum', 'anonymous_leaderboard', 'is_active',) | ||
'submission_guidelines', 'start_date', 'end_date', 'creator', 'evaluation_details', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@RishabhJain2018 can you please tell me what is the significance of evaluation_details
here? I might be missing something here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, evaluation_criteria
and evaluation_details
are same fields. In models we have defined it by evaluation_details
. So, I am using evaluation_details
to represent evaluation_criteria
here.
apps/challenges/views.py
Outdated
zip_file.write(response.content) | ||
except IOError: | ||
response_data = { | ||
'error': 'Unable to process the uploaded zip file. Please upload it again! ' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No space after !
apps/challenges/views.py
Outdated
|
||
except requests.exceptions.RequestException: | ||
response_data = { | ||
'error': 'A server error occured while processing zip file. Please try uploading it again.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with 'error': 'A server error occurred while processing zip file. Please try uploading it again!'
apps/challenges/views.py
Outdated
|
||
if not yaml_file_count: | ||
response_data = { | ||
'error': 'There is no yaml file in zip folder.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with 'error': 'There is no YAML file in zip configuration you provided!'
apps/challenges/views.py
Outdated
|
||
if yaml_file_count > 1: | ||
response_data = { | ||
'error': 'There are more than one yaml file in zip folder.' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with 'error': 'There are more than one YAML files in zip folder!'
|
||
# Check for challenge image in yaml file. | ||
image = yaml_file_data['image'] | ||
if image.endswith('.jpg') or image.endswith('.jpeg') or image.endswith('.png'): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please make sure that we mention in our documentation that the logo can be of jpg/jpeg/png
type only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@deshraj yeah sure. I will take a note of it.
|
||
response = self.client.post(self.url, {}) | ||
self.assertEqual(response.data.values()[0], expected['error']) | ||
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am sure that there are a lot of test cases that we are missing out here.
3e6265b
to
fe09682
Compare
@taranjeet @deshraj Please review the PR. Also guide me to write the |
c14f397
to
43f8433
Compare
|
||
def test__str__(self): | ||
self.assertEqual(self.participant_team.team_name, | ||
self.participant_team.__str__()) | ||
|
||
def test_get_all_participants_email(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we removing this test?
842b608
to
b958314
Compare
fixes #1038 @deshraj @taranjeet Please review the PR.