Parcourir la source

Update whole application

Jérôme BUISINE il y a 4 ans
Parent
commit
1ffcef1620

+ 3 - 0
Dockerfile

@@ -17,5 +17,8 @@ RUN python --version
 RUN pip install -r requirements.txt
 RUN pip install -r requirements.txt
 RUN python manage.py makemigrations
 RUN python manage.py makemigrations
 RUN python manage.py migrate
 RUN python manage.py migrate
+RUN echo $WEBEXPE_PREFIX_URL
+RUN WEBEXPE_PREFIX_URL=$WEBEXPE_PREFIX_URL
+RUN WEB_API_PREFIX_URL=$WEB_API_PREFIX_URL
 
 
 CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
 CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

+ 28 - 0
README.md

@@ -64,6 +64,34 @@ python manage.py createsuperuser
 
 
 You can now access `/admin/results` route with your credentials in order to download experience results.
 You can now access `/admin/results` route with your credentials in order to download experience results.
 
 
+<hr />
+
+Configure your own URL prefix using `WEBEXPE_PREFIX_URL`:
+
+```
+WEBEXPE_PREFIX_URL=experience python manage.py runserver
+```
+
+or using docker:
+
+```
+WEBEXPE_PREFIX_URL=experience make deploy
+```
+
+<hr />
+
+Using custom API base URL using `WEB_API_PREFIX_URL`:
+
+```
+WEBEXPE_PREFIX_URL=experience WEB_API_PREFIX_URL=expe/api python manage.py runserver
+```
+
+or using docker:
+
+```
+WEBEXPE_PREFIX_URL=experience WEB_API_PREFIX_URL=expe/api make deploy
+```
+
 ## How to contribute ?
 ## How to contribute ?
 
 
 This project uses [git-flow](https://danielkummer.github.io/git-flow-cheatsheet/) to improve cooperation during the development.
 This project uses [git-flow](https://danielkummer.github.io/git-flow-cheatsheet/) to improve cooperation during the development.

+ 6 - 1
ThesisWebExpeDjango/settings.py

@@ -125,4 +125,9 @@ STATICFILES_DIRS = (
 )
 )
 
 
 MEDIA_ROOT = "media/"
 MEDIA_ROOT = "media/"
-MEDIA_URL = "media/"
+MEDIA_URL = "media/"
+
+# env variables
+WEBEXPE_PREFIX_URL_KEY       = 'WEBEXPE_PREFIX_URL'
+WEBEXPE_PREFIX_URL           = os.environ.get(WEBEXPE_PREFIX_URL_KEY) \
+                               if os.environ.get(WEBEXPE_PREFIX_URL_KEY) is not None else ''

+ 6 - 2
ThesisWebExpeDjango/urls.py

@@ -15,8 +15,12 @@ Including another URLconf
 """
 """
 from django.contrib import admin
 from django.contrib import admin
 from django.urls import include, path
 from django.urls import include, path
+from django.views.generic import RedirectView
+
+from .settings import WEBEXPE_PREFIX_URL
 
 
 urlpatterns = [
 urlpatterns = [
-    path('', include('expe.urls', namespace='expe')),
-    path('admin/', admin.site.urls),
+    path('', RedirectView.as_view(url=WEBEXPE_PREFIX_URL)),
+    path(WEBEXPE_PREFIX_URL + '/', include('expe.urls', namespace='expe')),
+    path(WEBEXPE_PREFIX_URL + '/admin/', admin.site.urls),
 ]
 ]

+ 0 - 0
__init__.py


+ 4 - 1
docker-compose.yml

@@ -9,4 +9,7 @@ services:
         volumes:
         volumes:
             - "./media:/usr/src/app/media" # get access to media files
             - "./media:/usr/src/app/media" # get access to media files
         ports:
         ports:
-           - "8000:8000"
+           - "8000:8000"
+        environment:
+           WEBEXPE_PREFIX_URL: "${WEBEXPE_PREFIX_URL:-}"
+           WEB_API_PREFIX_URL: "${WEB_API_PREFIX_URL:-api}"

+ 12 - 4
expe/config.py

@@ -1,8 +1,16 @@
+# main imports
+import os
+
+# env variables
+WEB_API_PREFIX_URL_KEY       = 'WEB_API_PREFIX_URL'
+WEB_API_PREFIX_URL           = os.environ.get(WEB_API_PREFIX_URL_KEY) \
+                               if os.environ.get(WEB_API_PREFIX_URL_KEY) is not None else 'api'
+
 # api variables
 # api variables
 DIRAN_DOMAIN_NAME            = "https://diran.univ-littoral.fr/"
 DIRAN_DOMAIN_NAME            = "https://diran.univ-littoral.fr/"
-GET_SCENE_QUALITIES_API_URL  = DIRAN_DOMAIN_NAME + "api/listSceneQualities?sceneName={0}"
-GET_SCENE_IMAGE_API_URL      = DIRAN_DOMAIN_NAME + "api/getImage?sceneName={0}&imageQuality={1}"
-GET_SCENES_API_URL           = DIRAN_DOMAIN_NAME + "api/listScenes"
+GET_SCENE_QUALITIES_API_URL  = DIRAN_DOMAIN_NAME + WEB_API_PREFIX_URL + "/listSceneQualities?sceneName={0}"
+GET_SCENE_IMAGE_API_URL      = DIRAN_DOMAIN_NAME + WEB_API_PREFIX_URL + "/getImage?sceneName={0}&imageQuality={1}"
+GET_SCENES_API_URL           = DIRAN_DOMAIN_NAME + WEB_API_PREFIX_URL + "/listScenes"
 
 
 # folder variables
 # folder variables
 model_expe_folder            = "expes_models/{0}/{1}"
 model_expe_folder            = "expes_models/{0}/{1}"
@@ -22,7 +30,7 @@ expes_configuration         = {
             'end_text': "Experience is finished. Thanks for your participation",
             'end_text': "Experience is finished. Thanks for your participation",
         },
         },
         'params':{
         'params':{
-            'iterations': 5
+            'iterations': 20
         }
         }
     }
     }
 }
 }

+ 34 - 26
expe/expes/run.py

@@ -28,16 +28,24 @@ def run_quest_one_image(request, model_filepath, output_file):
     # used to stop when necessary
     # used to stop when necessary
     if 'iteration' in request.GET:
     if 'iteration' in request.GET:
         iteration = int(request.GET.get('iteration'))
         iteration = int(request.GET.get('iteration'))
+    else:
+        request.session['expe_started'] = False
 
 
     # first time only init `quest`
     # first time only init `quest`
     # if experience is started we can save data
     # if experience is started we can save data
     if request.session.get('expe_started'):
     if request.session.get('expe_started'):
-        answer = int(request.GET.get('answer'))
-        answer_time = time.time() - request.session['answer_time']
-        print("Answer time is ", answer_time)
-        previous_percentage = request.session.get('expe_percentage')
-        previous_orientation = request.session.get('expe_orientation')
-        previous_position = request.session.get('expe_position')
+
+         # does not change expe parameters
+        if request.session['expe_previous_iteration'] == iteration:
+            return None
+        else:
+            answer = int(request.GET.get('answer'))
+            answer_time = time.time() - request.session['answer_time']
+            print("Answer time is ", answer_time)
+            previous_percentage = request.session.get('expe_percentage')
+            previous_orientation = request.session.get('expe_orientation')
+            previous_position = request.session.get('expe_position')
+            previous_stim = request.session.get('expe_stim')
 
 
     # default params
     # default params
     thresholds = np.arange(50, 10000, 50)
     thresholds = np.arange(50, 10000, 50)
@@ -52,23 +60,7 @@ def run_quest_one_image(request, model_filepath, output_file):
         filehandler = open(model_filepath, 'rb') 
         filehandler = open(model_filepath, 'rb') 
         qp = pickle.load(filehandler)
         qp = pickle.load(filehandler)
     
     
-    # construct image and update `quest` only if necessary
-    if iteration < cfg.expes_configuration[expe_name]['params']['iterations']:
-        # process `quest`
-        next_stim = qp.next_contrast()
-        print("Next quality ", next_stim)
-
-        # construct new image
-        noisy_image = api.get_image(scene_name, next_stim)
-
-        # reconstruct reference image from list stored into session
-        ref_image = api.get_image(scene_name, 'max')
-        img_merge, percentage, orientation, position = crop_images(noisy_image, ref_image)
-    else:
-        request.session['expe_finished'] = True
-        return None
-    
-    # if experience is already begin
+    # if experience is already began
     if request.session.get('expe_started'):
     if request.session.get('expe_started'):
 
 
         # TODO : check `i` variable 
         # TODO : check `i` variable 
@@ -77,7 +69,7 @@ def run_quest_one_image(request, model_filepath, output_file):
         qp.update(qualities[iteration], answer) 
         qp.update(qualities[iteration], answer) 
         entropy = qp.get_entropy()
         entropy = qp.get_entropy()
 
 
-        line = str(next_stim) 
+        line = str(previous_stim) 
         line += ";" + scene_name 
         line += ";" + scene_name 
         line += ";" + str(previous_percentage)
         line += ";" + str(previous_percentage)
         line += ";" + str(previous_orientation) 
         line += ";" + str(previous_orientation) 
@@ -87,8 +79,6 @@ def run_quest_one_image(request, model_filepath, output_file):
         line += ";" + str(entropy) 
         line += ";" + str(entropy) 
         line += '\n'
         line += '\n'
 
 
-        print(line)
-        # TODO : add answer time from javascript
         output_file.write(line)
         output_file.write(line)
         output_file.flush()
         output_file.flush()
 
 
@@ -96,11 +86,29 @@ def run_quest_one_image(request, model_filepath, output_file):
     file_pi = open(model_filepath, 'wb') 
     file_pi = open(model_filepath, 'wb') 
     pickle.dump(qp, file_pi)
     pickle.dump(qp, file_pi)
 
 
+    # construct image 
+    if iteration < cfg.expes_configuration[expe_name]['params']['iterations']:
+        # process `quest`
+        next_stim = qp.next_contrast()
+        print("Next quality ", next_stim)
+
+        # construct new image
+        noisy_image = api.get_image(scene_name, next_stim)
+
+        # reconstruct reference image from list stored into session
+        ref_image = api.get_image(scene_name, 'max')
+        img_merge, percentage, orientation, position = crop_images(noisy_image, ref_image)
+    else:
+        request.session['expe_finished'] = True
+        return None
+
     # set current step data
     # set current step data
     request.session['expe_percentage'] = percentage
     request.session['expe_percentage'] = percentage
     request.session['expe_orientation'] = orientation
     request.session['expe_orientation'] = orientation
     request.session['expe_position'] = position
     request.session['expe_position'] = position
     request.session['answer_time'] = time.time()
     request.session['answer_time'] = time.time()
+    request.session['expe_previous_iteration'] = iteration
+    request.session['expe_stim'] = str(next_stim)
     
     
     # expe is now started
     # expe is now started
     request.session['expe_started'] = True
     request.session['expe_started'] = True

+ 8 - 0
expe/templates/base.html

@@ -23,6 +23,14 @@
     {% endblock %}
     {% endblock %}
     </div>
     </div>
 
 
+    <!-- Global scripts used -->
+    <script type="text/javascript"> 
+        const BASE_URL = "{{BASE_URL}}"
+        const host     = window.location.host
+        const baseUrl  = location.protocol + '//' + host + '/' + BASE_URL
+        const expeUrl  = baseUrl + '/expe'     
+        console.log(expeUrl)
+    </script>
     {% block javascripts %}
     {% block javascripts %}
     
     
     {% endblock %}
     {% endblock %}

+ 1 - 8
expe/templates/expe/expe.html

@@ -17,18 +17,11 @@
 
 
     <!-- TODO : Load    img from bitmap with javascript `loadImg.js` -->
     <!-- TODO : Load    img from bitmap with javascript `loadImg.js` -->
     {% if not request.session.expe_finished %}
     {% if not request.session.expe_finished %}
-        <img id="expeImg" src="/{{img_merged_path}}" data-img="{{request.session.img_merged}}"/>
+        <img id="expeImg" src="/{{BASE_URL}}/{{img_merged_path}}" data-img="{{request.session.img_merged}}"/>
     {% endif %}
     {% endif %}
 
 
     {% block javascripts %}
     {% block javascripts %}
         <script type="text/javascript"> 
         <script type="text/javascript"> 
-            // Utils informations
-            var host     = window.location.host
-            var pathname = window.location.pathname
-
-            var baseUrl      = location.protocol + "//" + host
-            var baseExpeUrl  = location.protocol + "//" + host + pathname
-
             // get access to django variables
             // get access to django variables
             var BEGIN_EXPE = "{{request.session.expe_started}}"
             var BEGIN_EXPE = "{{request.session.expe_started}}"
             var END_EXPE   = "{{request.session.expe_finished}}"
             var END_EXPE   = "{{request.session.expe_finished}}"

+ 1 - 1
expe/templates/expe/expe_list.html

@@ -11,7 +11,7 @@
 
 
     <div class="row">
     <div class="row">
         <div class="col-md-4 offset-md-4">
         <div class="col-md-4 offset-md-4">
-            <form action="/indications" id="expeChoice">
+            <form action="/{{BASE_URL}}/indications" id="expeChoice">
 
 
                 <div class="form-group">
                 <div class="form-group">
                     <label for="scene">Select scene:</label>
                     <label for="scene">Select scene:</label>

+ 1 - 1
expe/templates/expe/expe_results.html

@@ -75,7 +75,7 @@
     </div>
     </div>
 
 
     {% block javascripts %}
     {% block javascripts %}
-         <script type="text/javascript"> 
+        <script type="text/javascript"> 
             // Utils informations
             // Utils informations
             var expe_name = "{{expe}}"
             var expe_name = "{{expe}}"
         </script>
         </script>

+ 50 - 15
expe/views.py

@@ -36,6 +36,17 @@ from .utils.processing import crop_images
 from . import config as cfg
 from . import config as cfg
 
 
 
 
+def get_base_data():
+    '''
+    Used to store default data to send for each view
+    '''
+    data = {}
+
+    data['BASE_URL'] = settings.WEBEXPE_PREFIX_URL
+
+    return data
+
+
 def expe_list(request):
 def expe_list(request):
 
 
     # get all scenes from dataset
     # get all scenes from dataset
@@ -47,19 +58,26 @@ def expe_list(request):
     # by default user restart expe
     # by default user restart expe
     request.session['expe_started'] = False
     request.session['expe_started'] = False
 
 
-    return render(request, 'expe/expe_list.html', {'scenes': scenes, 'expes': expes})
+    # get base data
+    data = get_base_data()
+    # expe data
+    data['scenes'] = scenes
+    data['expes']  = expes
+
+    return render(request, 'expe/expe_list.html', data)
+
 
 
 def indications(request):
 def indications(request):
 
 
     # get param 
     # get param 
     expe_name = request.GET.get('expe')
     expe_name = request.GET.get('expe')
 
 
+    # get base data
+    data = get_base_data()
     # expe parameters
     # expe parameters
-    data = {
-        'expe_name': expe_name,
-        'question': cfg.expes_configuration[expe_name]['text']['question'],
-        'indication': cfg.expes_configuration[expe_name]['text']['indication']
-    }
+    data['expe_name']  = expe_name
+    data['question']   = cfg.expes_configuration[expe_name]['text']['question']
+    data['indication'] = cfg.expes_configuration[expe_name]['text']['indication']
 
 
     return render(request, 'expe/expe_indications.html', data)
     return render(request, 'expe/expe_indications.html', data)
 
 
@@ -124,20 +142,29 @@ def expe(request):
         # generate tmp merged image (pass as BytesIO was complicated..)
         # generate tmp merged image (pass as BytesIO was complicated..)
         # TODO : add crontab task to erase generated img
         # TODO : add crontab task to erase generated img
         filepath_img = os.path.join(tmp_folder, request.session.get('id') + '_' + scene_name + '' + expe_name + '.png')
         filepath_img = os.path.join(tmp_folder, request.session.get('id') + '_' + scene_name + '' + expe_name + '.png')
-        img_merge.save(filepath_img)
+        
+        # replace img_merge if necessary (new iteration of expe)
+        if img_merge is not None:
+            img_merge.save(filepath_img)
     else:
     else:
         # reinit session as default value
         # reinit session as default value
         del request.session['expe']
         del request.session['expe']
         del request.session['scene']
         del request.session['scene']
         del request.session['qualities']
         del request.session['qualities']
         del request.session['timestamp']
         del request.session['timestamp']
-
+        del request.session['answer_time']
+        del request.session['expe_percentage']
+        del request.session['expe_orientation']
+        del request.session['expe_position']
+        del request.session['expe_stim']
+        del request.session['expe_previous_iteration']
+
+    # get base data
+    data = get_base_data()
     # expe parameters
     # expe parameters
-    data = {
-        'expe_name': expe_name,
-        'img_merged_path': filepath_img,
-        'end_text': cfg.expes_configuration[expe_name]['text']['end_text']
-    }
+    data['expe_name']       = expe_name
+    data['img_merged_path'] = filepath_img
+    data['end_text']        = cfg.expes_configuration[expe_name]['text']['end_text']
 
 
     return render(request, 'expe/expe.html', data)
     return render(request, 'expe/expe.html', data)
 
 
@@ -155,6 +182,7 @@ def list_results(request, expe=None):
 
 
     else:
     else:
         if expe in cfg.expe_name_list:
         if expe in cfg.expe_name_list:
+
             folder_path = os.path.join(settings.MEDIA_ROOT, cfg.output_expe_folder, expe)
             folder_path = os.path.join(settings.MEDIA_ROOT, cfg.output_expe_folder, expe)
 
 
             # init folder dictionnary
             # init folder dictionnary
@@ -162,7 +190,7 @@ def list_results(request, expe=None):
 
 
             if os.path.exists(folder_path):
             if os.path.exists(folder_path):
             
             
-                days = os.listdir(folder_path)
+                days = sorted(os.listdir(folder_path), reverse=True)
 
 
                 for day in days:
                 for day in days:
                     day_path = os.path.join(folder_path, day)
                     day_path = os.path.join(folder_path, day)
@@ -171,7 +199,14 @@ def list_results(request, expe=None):
         else:
         else:
             raise Http404("Expe does not exists")
             raise Http404("Expe does not exists")
 
 
-    return render(request, 'expe/expe_results.html', {'expe': expe, 'folders': folders, 'infos': cfg.expes_configuration[expe]['text']})
+    # get base data
+    data = get_base_data()
+    # expe parameters
+    data['expe']    = expe
+    data['folders'] = folders
+    data['infos']   = cfg.expes_configuration[expe]['text']
+
+    return render(request, 'expe/expe_results.html', data)
 
 
 
 
 @login_required(login_url="login/")
 @login_required(login_url="login/")

+ 4 - 5
static/js/indications.js

@@ -1,10 +1,6 @@
 // implement `key` events
 // implement `key` events
 document.onkeydown = checkKey;
 document.onkeydown = checkKey;
 
 
-var host     = window.location.host
-var expe_url = '/expe'     
-var baseUrl  = location.protocol + "//" + host
-
 // Utils informations
 // Utils informations
 var KEYCODE_Q           = '81'
 var KEYCODE_Q           = '81'
 var KEYCODE_ENTER       = '13'
 var KEYCODE_ENTER       = '13'
@@ -14,6 +10,8 @@ urlParams = new URLSearchParams(window.location.search);
 var scene = urlParams.get('scene')
 var scene = urlParams.get('scene')
 var expe  = urlParams.get('expe')
 var expe  = urlParams.get('expe')
 
 
+console.log(expeUrl)
+
 function checkKey(e) {
 function checkKey(e) {
 
 
    e = e || window.event;
    e = e || window.event;
@@ -27,6 +25,7 @@ function checkKey(e) {
 
 
         // right arrow
         // right arrow
         var params = "?scene=" + scene + "&expe=" + expe + "&iteration=0"
         var params = "?scene=" + scene + "&expe=" + expe + "&iteration=0"
-        window.location = baseUrl + expe_url + params
+        console.log(expeUrl + params)
+        window.location = expeUrl + params
    }
    }
 }
 }

+ 3 - 3
static/js/keyEvents.js

@@ -27,11 +27,10 @@ function checkKey(e) {
    }
    }
    else if (e.keyCode == '13') {
    else if (e.keyCode == '13') {
 
 
-      console.log("Here")
       // check if experience is begin
       // check if experience is begin
       if (!BEGIN_EXPE){
       if (!BEGIN_EXPE){
 
 
-         console.log("And Here")
+         console.log(window.location.href + "&begin=true")
          // right arrow
          // right arrow
          window.location = window.location.href + "&begin=true"
          window.location = window.location.href + "&begin=true"
       } 
       } 
@@ -66,7 +65,8 @@ function checkKey(e) {
          
          
          // construct url with params for experience
          // construct url with params for experience
          var params = "?scene=" + scene + "&expe=" + expe + "&iteration=" + iteration + "&answer=" + answer
          var params = "?scene=" + scene + "&expe=" + expe + "&iteration=" + iteration + "&answer=" + answer
-         window.location = baseExpeUrl + params
+         console.log(expeUrl + params)
+         window.location = expeUrl + params
       }
       }
    }
    }
 }
 }

+ 1 - 0
static/js/loadImg.js

@@ -21,6 +21,7 @@ window.onload = function () {
         } 
         } 
 
 
         setTimeout(function(){ 
         setTimeout(function(){ 
+            // TODO : refresh to home app
             window.location = baseUrl
             window.location = baseUrl
         }, 5000);
         }, 5000);
     }
     }

+ 12 - 7
static/js/results.js

@@ -1,11 +1,12 @@
-const toggle = ele => ele.style.display = ele.style.display === 'none' ? 'block' : 'none'
-const toggleClass = (ele, class1, class2) => elem.className = elem.className === class1 ? class2 : class1
+// create
+const toggleVisible = ele => ele.style.display = ele.style.display === 'none' ? 'block' : 'none'
+const toggleClass = (elem, class1, class2) => elem.className = elem.className === class1 ? class2 : class1
 
 
 // Download endpoint response as a file using a POST request
 // Download endpoint response as a file using a POST request
 const downloadContent = path => {
 const downloadContent = path => {
     const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value
     const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value
 
 
-    const res = await fetch('/admin/download', {
+    fetch('/' + BASE_URL + '/admin/download', {
         method: 'POST',
         method: 'POST',
         body: `path=${path}`,
         body: `path=${path}`,
         headers: {
         headers: {
@@ -13,13 +14,15 @@ const downloadContent = path => {
             'X-CSRFToken': csrfToken
             'X-CSRFToken': csrfToken
         }
         }
     }).then(async res => {
     }).then(async res => {
+        console.log(res)
         if (res.status === 200) {
         if (res.status === 200) {
             // Try to find out the filename from the content disposition `filename` value
             // Try to find out the filename from the content disposition `filename` value
-            const disposition = res.headers['content-disposition']
+            const disposition = res.headers.get('Content-Disposition')
             // expe is find from django
             // expe is find from django
             const filename = `${expe_name}_${disposition.split('=')[1]}`
             const filename = `${expe_name}_${disposition.split('=')[1]}`
 
 
             const blob = await res.blob()
             const blob = await res.blob()
+            // use of `FileSaver`
             saveAs(blob, filename)
             saveAs(blob, filename)
         }
         }
     })
     })
@@ -28,7 +31,8 @@ const downloadContent = path => {
 
 
 window.addEventListener('DOMContentLoaded', () => {
 window.addEventListener('DOMContentLoaded', () => {
     // Display list of files from day folder
     // Display list of files from day folder
-    document.getElementsByClassName('date-folder-list').forEach(item => {
+    // need to parse as `Array`
+    Array.from(document.getElementsByClassName('date-folder-list')).forEach(item => {
         item.addEventListener('click', event => {
         item.addEventListener('click', event => {
             event.preventDefault()
             event.preventDefault()
             currentElem = event.currentTarget
             currentElem = event.currentTarget
@@ -37,7 +41,7 @@ window.addEventListener('DOMContentLoaded', () => {
             list = currentElem.parentElement.nextElementSibling
             list = currentElem.parentElement.nextElementSibling
             
             
             // display or hide list elements
             // display or hide list elements
-            toggle(list)
+            toggleVisible(list)
 
 
             // toggle arrow class for display effect
             // toggle arrow class for display effect
             iconElem = currentElem.children[0]
             iconElem = currentElem.children[0]
@@ -45,7 +49,8 @@ window.addEventListener('DOMContentLoaded', () => {
         })
         })
     })
     })
 
 
-    document.getElementsByClassName('download-list').forEach(downloadElem => {
+    // need to parse as `Array`
+    Array.from(document.getElementsByClassName('download-list')).forEach(downloadElem => {
         downloadElem.addEventListener('click', event => {
         downloadElem.addEventListener('click', event => {
             event.preventDefault()
             event.preventDefault()