Repository: oppia/oppia

Issue:
[BUG]: Console Errors on opening question editor(question with Image Region Interaction) from Skill Editor page
### Describe the bug

There is a console error when we try to open question editor from skill editor page
This error seems similar to #17157 

### Steps To Reproduce

1.Go to Topics and skills dashboard
2.Click on Skills
3. Select any skill
4. Navigate to questions tab
5. Select a question(Question with image region interaction)
6. After you enter question editor page see the console for errors.

### Expected Behavior

There shoulfd be no console errors.

### Screenshots/Videos

![image](https://user-images.githubusercontent.com/91470808/218263003-349478e6-27ad-44ac-ad09-766273f4406e.png)


### What device are you using?

Desktop

### Operating System

Windows

### What browsers are you seeing the problem on?

Firefox

### Browser version

Version 109.0.5414.120

### Additional context

_No response_


Discussion:
@Lawful2002 @Priyansh61 PTAL this is blocking the release and is a no. 1 priority. Can you please find a person who will be able to fix this or fix it yourself?
@kshitij01042002 I think the issue is already resolved. I dont find any console errors.

https://user-images.githubusercontent.com/99071926/220544728-e90394d2-d7ef-4aa4-84b9-ea846f976fdf.mp4


@Kej-r03 Did you replicate the exact conditions as on the test server?
@vojtechjelinek actually I cannot access this page on test server. Probably some authorization is not there
@Kej-r03 What is your test server username?

@vojtechjelinek it is **rishi**
But I cannot open the test server in any of my browser or private window [see here](https://github.com/oppia/oppia/discussions/17446#discussion-4885302)
@Kej-r03 Did you check it with the --prod_env flag ?
@kshitij01042002 I don't think this happens for all questions, and in particular it looks like the skill mentioned in your first screenshot has been deleted on the test server.

Could you please check if you can still reproduce the issue? If you can, could you please confirm whether this happens for all skills, or just some?
@seanlip The issue is not reproducible on the test server anymore.
I guess we can safely close this issue if no one is encountering it at the moment.
@kshitij01042002 This is your call since you filed it. If you decide to close it, please feel free to do so and explain why. Thanks.
@seanlip Thanks, you left it to me to make the call, on investigating more I can confirm that the error still persist and occurs when the question have a image region interaction. But the endpoint URL is different in the previous error and the error that ocurs now.

https://user-images.githubusercontent.com/91470808/221366144-f7724212-9f8c-4d1c-a78b-c9f4e10ea35e.mp4


![image](https://user-images.githubusercontent.com/91470808/221366290-8bedc65c-14af-49a4-817f-8d97b528477a.png)


That's great, thanks for investigating @kshitij01042002. It would have been good to include this in the original issue since the original description wasn't clear about which skills/questions the issue applied to.

@Kej-r03 Do you now have enough info to reproduce locally? If not please liaise with @kshitij01042002 and try to make sure that all the necessary context is captured here. Thanks!
@seanlip I have added appropriate context to Issue title as well as description.

@Kej-r03 If you still need any help reproducing the issue feel free to ping me.
@kshitij01042002 I can reproduce the issue, but I am unable to trace the exact cause of the issue. I see there's a similar use of interactions in exploration editor page (`state-interaction-editor.component.ts` onwards), but this error does not occur there. 

I have compared the saving and fetching calls for both these pages, and I believe this must be due to some difference **where** the image file is stored in the 2 cases, beacuse the `imgurl` (`image-click-input-interaction.component.html`) is of exact similar format for both cases, but in case of exploration, the img src can get the value, and not in case of question. **(infact I also tried hardcoding exploration's imgurl in src, and the image showed up in question, but hardcoding question's imgurl in src, the image does not show up in exploration too, which means question's image is wrongly stored somewhere)**

Are you aware of any such storage difference?
@Kej-r03 the images in the question editor are only stored in the local storage so when you edit the question back they don't appear due to broken link as it doesnt get saved in the server.

I have worked with the same type of issue in explorstion editor, since it is a regression issue should I realise the PR for this as it is time sensitive. 
Ok @Priyansh61 , I understand the error
If you like, you can brief me the steps, I will make the changes to the earliest
Wait -- just to double check, is this for an existing question or a new question? I can imagine that images for new questions might be saved in localStorage, but that wouldn't be the case for existing questions. 
@seanlip this issue is happening for existing ones only
Then I don't think they would be saved in local storage. Try to figure out where they get stored when you create a question.
The new instance of QuestionModel is stored by calling commit() of VersionedModel class
Yup, but where are the images for the question stored and what code does
that? See if you can find how they get sent to the backend.

On Thu, Mar 9, 2023, 8:10 PM Kej-r03 ***@***.***> wrote:

> The new instance of QuestionModel is stored by calling commit() of
> VersionedModel class
>
> —
> Reply to this email directly, view it on GitHub
> <https://github.com/oppia/oppia/issues/17326#issuecomment-1461922227>, or
> unsubscribe
> <https://github.com/notifications/unsubscribe-auth/ACQV5SUP7H7ILI3LH2DEUOLW3HCDDANCNFSM6AAAAAAUYXG32I>
> .
> You are receiving this because you were mentioned.Message ID:
> ***@***.***>
>

@seanlip the request consists of a **question_dict** (which contains all the details of the question, including the interaction customizaion args with image path), and **image filename** from localStorageService(having same name as in question_dict) and **imageblob**.

The question_dict is used to create new Question instance and commit as I have told before.
The image and blob are saved via redis hset().
OK, great. Let's focus on the image/blob. You mention they're saved via redis hset() (which actually seems slightly odd to me). What line of code in the codebase is saving them, and which URL are they being saved to?
> OK, great. Let's focus on the image/blob. You mention they're saved via redis hset() (which actually seems slightly odd to me). What line of code in the codebase is saving them, and which URL are they being saved to?

From the endpoint [here](https://github.com/oppia/oppia/blob/7f820eb83dc59dd0bfb38e392e5f67e5ddd9475d/core/controllers/question_editor.py#L150)(here "image" variable is the blob), eventually it is getting saved over [here](https://github.com/oppia/oppia/blob/7f820eb83dc59dd0bfb38e392e5f67e5ddd9475d/core/platform/storage/cloud_storage_emulator.py#L216)
OK, great. Which filepath is it being saved too, then? Is it the same filepath that is being retrieved from later?
filepath is simply  the string "image/(filename)"
You haven't answered my second question.
I just tested this endpoint once again and found that the [filenames](https://github.com/oppia/oppia/blob/7f820eb83dc59dd0bfb38e392e5f67e5ddd9475d/core/controllers/question_editor.py#L131) array is empty, so all of this code is not executed at all ! :/

So the image and blob part is not getting saved anywhere...Just the Question model instance.
OK, good find. Try to dig more around that area, and see if you can fix it.
So, I just need a small guidance, how should the image ideally get stored in the server, to be retrievable? In the form of the image blob, or some other way
I would suggest looking at how exploration images get saved. I suspect the filepaths list for those is nonempty, but you'll want to double-check that. When you've found the difference between the "working version" of uploading images and the "non-working version", that should give you a clue about what needs to be changed.
@seanlip thanks, I found the code...
the question image is getting saved in local, whereas, the exploration is getting posted to server, hence the difference
Will try to address it in a commit soon
@Kej-r03, sounds good, glad you were able to figure it out! Just a note -- in your PR description, try to explain in more detail what's going on with the current code and why it didn't work. This helps orient reviewers and other folks who are reading the PR.

Thanks!
@seanlip In the final part of my work, I just found a fresh issue. Basically, the location url where image is posted includes "**/question/(skill_id)**", and interestingly, the location from where retrieval takes place is from "**/skill/(question_id)**" location!
Ideally, they both post and retrieve location should be from "**/question/(question_id)**".

But now the problem is that image posting call takes place before question creation call at backend, so the "question_id" of the url is not available for the image posting.
I compared this with how it is dealt with in exploration editor, and found that there first the new exploration is registered, to get the exploration id, so id is available for the image posting, and then exploration is saved.

So how should I go about this?
There's another flow for submitting questions, which is through the contributor dashboard. How is it handled there -- what URL do the images for those questions get saved to, when the suggestion is accepted?
@seanlip I tried searching, but I am unable to find the questions feature, since I am relatively unfamiliar with using contributor dashboard.
But what I can understand, that to associate every image with its question only, we will definitely be needing the question_id before the image posting takes place, which implies creating the question in backend on clicking "Add Question",  beforehand, then updating its fields upon clicking "Save"
> Ok @Priyansh61 , I understand the error If you like, you can brief me the steps, I will make the changes to the earliest

@Kej-r03  Here  are my findings, 

- when using image interaction untill the final `Save` is not pressed we just keep the image in the local storage as we can see in the `image-editor.component.ts` and when the button is clicked we make a post request to the server, but we only save the question details through the following code.

``` typescript
this.editableQuestionBackendApiService.createQuestionAsync(
        this.newQuestionSkillIds, this.newQuestionSkillDifficulties,
        (this.question.toBackendDict(true)), imagesData
      )
```

Before this we flush the image in the local storage `this.imageLocalStorageService.flushStoredImagesData();`


- So now the problem in front of us is we need to save the image to the server and that can only be done once you create the question details. Therefore once the question is made through above post request you can make another request to the server as shown below

```typescript
 postImageToServer(
    filename: string,
    resampledFile: Blob,
): void {
  let form = new FormData();
  form.append('image', resampledFile);
  form.append('payload', filename);
  const imageUploadUrlTemplate = (
    '/createhandler/imageupload/<entity_type>/<entity_id>');
  this.http.post<ImageUploadBackendResponse>(
    this.urlInterpolationService.interpolateUrl(
      imageUploadUrlTemplate, {
        entity_type: this.entityType,
        entity_id: this.entityId
      }
    ),
    form
  ).toPromise().then((data) => {
    // Pre-load image before marking the image as saved.
    const img = new Image();
    img.onload = () => {
      this.setSavedImageFilename(data.filename, true);
      let dimensions = (
        this.imagePreloaderService.getDimensionsOfImage(data.filename));
      this.imageContainerStyle = {
        height: dimensions.height + 'px',
        width: dimensions.width + 'px'
      };
    };
    // Check point 2 in the note before imports and after fileoverview.
    img.src = this.getTrustedResourceUrlForImageFileName(
      data.filename) as string;
  },
  (response) => {
    this.alertsService.addWarning(
      response.error || 'Error communicating with server.');
  });
}
```
You can use the filename and blob from the local storage before flushing it.

I hope this helps and try to do it ASAP, since this is a regression issue and do let me know if you need any help with above approach.


Resolving patch:
diff --git a/core/controllers/question_editor.py b/core/controllers/question_editor.py
--- a/core/controllers/question_editor.py
+++ b/core/controllers/question_editor.py
@@ -17,7 +17,7 @@
 """
 
 from __future__ import annotations
-
+import json
 import logging
 
 from core import feconf
@@ -26,7 +26,6 @@
 from core.controllers import acl_decorators
 from core.controllers import base
 from core.domain import fs_services
-from core.domain import html_cleaner
 from core.domain import image_validation_services
 from core.domain import question_domain
 from core.domain import question_services
@@ -122,34 +121,38 @@ def post(self) -> None:
             question.id,
             skill_ids,
             skill_difficulties)
-        html_list = question.get_all_html_content_strings()
-        filenames = (
-            html_cleaner.get_image_filenames_from_html_strings(html_list))
         image_validation_error_message_suffix = (
             'Please go to the question editor for question with id %s and edit '
             'the image.' % question.id)
-        for filename in filenames:
-            image = self.request.get(filename)
-            if not image:
-                logging.exception(
-                    'Image not provided for file with name %s when the question'
-                    ' with id %s was created.' % (filename, question.id))
-                raise self.InvalidInputException(
-                    'No image data provided for file with name %s. %s'
-                    % (filename, image_validation_error_message_suffix))
-            try:
-                file_format = (
-                    image_validation_services.validate_image_and_filename(
-                        image, filename))
-            except utils.ValidationError as e:
-                raise self.InvalidInputException(
-                    '%s %s' % (e, image_validation_error_message_suffix)
-                )
-            image_is_compressible = (
-                file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS)
-            fs_services.save_original_and_compressed_versions_of_image(
-                filename, feconf.ENTITY_TYPE_QUESTION, question.id, image,
-                'image', image_is_compressible)
+
+        filenames = self.payload.get('filenames')
+
+        if filenames:
+            filenames_list = json.loads(filenames)
+            for filename in filenames_list:
+                index = filenames_list.index(filename)
+                image = self.request.get(f'image{index}')
+                if not image:
+                    logging.exception(
+                        'Image not provided for file with'
+                        ' name %s when the question'
+                        ' with id %s was created.' % (filename, question.id))
+                    raise self.InvalidInputException(
+                        'No image data provided for file with name %s. %s'
+                        % (filename, image_validation_error_message_suffix))
+                try:
+                    file_format = (
+                        image_validation_services.validate_image_and_filename(
+                            image, filename))
+                except utils.ValidationError as e:
+                    raise self.InvalidInputException(
+                        '%s %s' % (e, image_validation_error_message_suffix)
+                    )
+                image_is_compressible = (
+                    file_format in feconf.COMPRESSIBLE_IMAGE_FORMATS)
+                fs_services.save_original_and_compressed_versions_of_image(
+                    filename, feconf.ENTITY_TYPE_QUESTION, question.id, image,
+                    'image', image_is_compressible)
 
         self.values.update({
             'question_id': question.id


Test patch:
diff --git a/core/controllers/question_editor_test.py b/core/controllers/question_editor_test.py
--- a/core/controllers/question_editor_test.py
+++ b/core/controllers/question_editor_test.py
@@ -15,7 +15,7 @@
 """Tests for the Question Editor controller."""
 
 from __future__ import annotations
-
+import json
 import os
 
 from core import feconf
@@ -380,7 +380,8 @@ def test_post_with_valid_images(self) -> None:
         post_data = {
             'question_dict': question_dict,
             'skill_ids': [self.skill_id],
-            'skill_difficulties': [0.6]
+            'skill_difficulties': [0.6],
+            'filenames': json.dumps(['img.png'])
         }
 
         with utils.open_file(
@@ -391,7 +392,7 @@ def test_post_with_valid_images(self) -> None:
         self.post_json(
             feconf.NEW_QUESTION_URL, post_data,
             csrf_token=csrf_token,
-            upload_files=[(filename, filename, raw_image)]
+            upload_files=[('image0', filename, raw_image)]
         )
         all_models = question_models.QuestionModel.get_all()
         questions = [
@@ -421,7 +422,8 @@ def test_post_with_invalid_images(self) -> None:
         post_data = {
             'question_dict': question_dict,
             'skill_ids': [self.skill_id],
-            'skill_difficulties': [0.6]
+            'skill_difficulties': [0.6],
+            'filenames': json.dumps(['img.svg'])
         }
 
         response_dict = self.post_json(
@@ -438,7 +440,7 @@ def test_post_with_invalid_images(self) -> None:
             feconf.NEW_QUESTION_URL, post_data,
             csrf_token=csrf_token,
             upload_files=[
-                ('img.svg', 'img.svg', large_image)
+                ('image0', 'img.svg', large_image)
             ], expected_status_int=400)
         self.assertIn(
             'Image exceeds file size limit of 100 KB.',