Skip to content
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

Merged
merged 2 commits into from
Jul 12, 2017

Conversation

RishabhJain2018
Copy link
Member

@RishabhJain2018 RishabhJain2018 commented Jun 12, 2017

fixes #1038 @deshraj @taranjeet Please review the PR.

@RishabhJain2018 RishabhJain2018 force-pushed the zip-challenge-views branch 2 times, most recently from 18cea0a to 73481ad Compare June 12, 2017 12:39
@taranjeet
Copy link
Member

@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 :)

@RishabhJain2018
Copy link
Member Author

@taranjeet yeah sure. I will do that. 👍

@RishabhJain2018 RishabhJain2018 changed the title Add Challenge creation using zip file. [REVIEW] Add Challenge creation using zip file. Jun 20, 2017
@RishabhJain2018 RishabhJain2018 changed the title [REVIEW] Add Challenge creation using zip file. [REVIEW PENDING] Add Challenge creation using zip file. Jun 20, 2017
@@ -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/$',
Copy link
Member

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.

Copy link
Member

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?

Copy link
Member Author

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. 👍

DatasetSplitSerializer,
LeaderboardSerializer,
ZipConfigurationChallengeSerializer,
ZipFileCreateChallengePhaseSplitSerializer,)
Copy link
Member

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?

return Response(response_data, status=status.HTTP_400_BAD_REQUEST)

try:
with transaction.atomic():
Copy link
Member

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?

Copy link
Member Author

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 ?

# All files download and extract location.
BASE_LOCATION = tempfile.mkdtemp()

CHALLENGE_ZIP_DOWNLOAD_LOCATION = os.path.join('{}/zip_challenge.zip'.format(BASE_LOCATION))
Copy link
Member

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.

Copy link
Member Author

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.

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:
Copy link
Member

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 :)

Copy link
Member

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.

# Search for yaml file
for name in zip_ref.namelist():
if name.endswith('.yaml') or name.endswith('.yml'):
yaml_file = name
Copy link
Member

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.

@RishabhJain2018
Copy link
Member Author

@deshraj @taranjeet Please review the PR. Once this is approved, I will add the test cases regarding the same.

@@ -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/$',
Copy link
Member

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.

response = requests.get(uploaded_zip_file_path, stream=True)

# All files download and extract location.
BASE_LOCATION = tempfile.mkdtemp()
Copy link
Member

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.

# 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))
Copy link
Member

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))


except:
response_data = {
'error': 'Some error occured during file upload. Please upload it again.'
Copy link
Member

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.

zip_ref = zipfile.ZipFile(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'r')
zip_ref.extractall('{}/{}'.format(BASE_LOCATION, unique_folder_name))
zip_ref.close()
except:
Copy link
Member

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?

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)
Copy link
Member

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.

Copy link
Member Author

@RishabhJain2018 RishabhJain2018 Jun 26, 2017

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?

# 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]
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

if zip_config:
zip_config.challenge = challenge
zip_config.save()
response_data = {'success': 'The Challenge is successfully created'}
Copy link
Member

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

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'}
Copy link
Member

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.

response_data = {'error': 'Error in challenge creation. Please try again'}
return Response(response_data, status=status.HTTP_400_BAD_REQUEST)

try:
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok sure.

@RishabhJain2018
Copy link
Member Author

@taranjeet @deshraj I have fixed most of the review changes, but some require a discussion so, I will fix those after tonight's discussion.

@@ -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/$',
Copy link
Member

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?

with open(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'w') as zip_file:
zip_file.write(response.content)

except:
Copy link
Member

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.

zip_ref = zipfile.ZipFile(CHALLENGE_ZIP_DOWNLOAD_LOCATION, 'r')
zip_ref.extractall(join(BASE_LOCATION, unique_folder_name))
zip_ref.close()
except BadZipfile:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good. :)

zip_ref.close()
except BadZipfile:
response_data = {
'error': 'The zip file contents cannot be extracted. Please check the format.'
Copy link
Member

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!


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.'
Copy link
Member

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?

else:
response_data = {
'error': 'There is no key for evaluation script in yaml file.'\
'Please add a key and then try uploading it again.'
Copy link
Member

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.

else:
response_data = {
'error': 'No evaluation script is present in the zip file.'\
'Please try uploading again the zip file after adding evaluation script.'
Copy link
Member

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.

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'])
Copy link
Member

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.

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'])
Copy link
Member

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.

save=True)

# Create Leaderboard
yaml_file_data_of_leaderboard = yaml_file_data['leaderboard']
Copy link
Member

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.

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',
Copy link
Member

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.

Copy link
Member Author

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.

zip_file.write(response.content)
except IOError:
response_data = {
'error': 'Unable to process the uploaded zip file. Please upload it again! '
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space after !


except requests.exceptions.RequestException:
response_data = {
'error': 'A server error occured while processing zip file. Please try uploading it again.'
Copy link
Member

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!'


if not yaml_file_count:
response_data = {
'error': 'There is no yaml file in zip folder.'
Copy link
Member

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!'


if yaml_file_count > 1:
response_data = {
'error': 'There are more than one yaml file in zip folder.'
Copy link
Member

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'):
Copy link
Member

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.

Copy link
Member Author

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)
Copy link
Member

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.

@RishabhJain2018
Copy link
Member Author

RishabhJain2018 commented Jul 4, 2017

@taranjeet @deshraj Please review the PR. Also guide me to write the tests for some cases. I am stuck here.


def test__str__(self):
self.assertEqual(self.participant_team.team_name,
self.participant_team.__str__())

def test_get_all_participants_email(self):
Copy link
Member

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?

@deshraj deshraj merged commit 86bfa28 into Cloud-CV:master Jul 12, 2017
@RishabhJain2018 RishabhJain2018 deleted the zip-challenge-views branch July 13, 2017 19:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add API endpoint to create challenge by uploading zip file.
3 participants