Parcourir la source

Merge branch 'release/v0.0.2'

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

+ 4 - 2
.gitignore

@@ -104,5 +104,7 @@ venv.bak/
 .mypy_cache/
 
 # data folder
-data
-expe
+links/data
+links/expe
+.vscode
+media

+ 26 - 0
Dockerfile

@@ -0,0 +1,26 @@
+FROM python
+
+COPY . /usr/src/app
+WORKDIR /usr/src/app
+
+# Server port
+EXPOSE 8000
+
+RUN apt-get update
+
+# update project source code if necessary (use by default https protocol)
+RUN git remote set-url origin https://github.com/prise-3d/SIN3D-launcher.git
+RUN git pull origin master
+
+# Install dependencies and prepare project
+RUN python --version
+RUN pip install -r requirements.txt
+RUN python manage.py makemigrations
+RUN python manage.py migrate
+
+# only comment in case it will be necessary
+# 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"]

+ 48 - 0
Makefile

@@ -0,0 +1,48 @@
+build:
+	@echo "----------------------------------------------------------------"
+	@echo "Update of sin3dlauncher image"
+	@echo "----------------------------------------------------------------"
+	docker build --no-cache . --tag sin3dlauncher
+	@echo "----------------------------------------------------------------"
+	@echo "Image is now build you can run instance using 'make run'"
+	@echo "----------------------------------------------------------------"
+
+run: 
+	@echo "----------------------------------------------------------------"
+	@echo "Process to run new instance"
+	@echo "----------------------------------------------------------------"
+	docker-compose up
+	@echo "----------------------------------------------------------------"
+	@echo "Your docker instance is now launched with name 'sin3dlauncherinst'"
+	@echo "Your website is now accessible at http://localhost:8000"
+	@echo "----------------------------------------------------------------"
+
+stop:
+	@echo "----------------------------------------------------------------"
+	@echo "Process to stop current instance"
+	@echo "----------------------------------------------------------------"
+	docker stop sin3dlauncherinst
+	@echo "----------------------------------------------------------------"
+	@echo "App is now stopped"
+	@echo "----------------------------------------------------------------"
+
+remove:
+	@echo "----------------------------------------------------------------"
+	@echo "Process to stop current instance"
+	@echo "----------------------------------------------------------------"
+	docker stop sin3dlauncherinst
+	docker rm sin3dlauncherinst
+	@echo "----------------------------------------------------------------"
+	@echo "App is now stopped and removed"
+	@echo "----------------------------------------------------------------"
+
+clean: 
+	@echo "----------------------------------------------------------------"
+	@echo "Process to remove image"
+	@echo "----------------------------------------------------------------"
+	docker rmi sin3dlauncher
+	@echo "----------------------------------------------------------------"
+	@echo "sin3dlauncher image is now deleted"
+	@echo "----------------------------------------------------------------"
+
+deploy: build run

+ 48 - 4
README.md

@@ -9,21 +9,65 @@ Project for generating users links in order to launch SIN3D application quickly
 
 Generate user links (file saved into `data` folder):
 ```sh
-python generate/generate_experiment_link.py --experiment AreSameImagesRandom --experimentId expe1 --scenes Appart1opt02,Bureau1,Cendrier --output expe1.csv
+python links/generate/generate_experiment_link.py --experiment AreSameImagesRandom --experimentId expe1 --scenes Appart1opt02,Bureau1,Cendrier --output expe1.csv
 ```
 
 Generate experiment links for each user (file saved into `expe` folder):
 ```sh
-python generate/generate_experiment.py --data data/expe1.csv --scenes 2 --users 150 --output expe1_user_links.csv
+python links/generate/generate_experiment.py --data data/expe1.csv --scenes 2 --users 150 --userId 1 --output expe1_user_links.csv
 ```
 
+- `userId`: tell if an userId is used or not (use of index of line from generated output file)
+
 The final file is generated and contains data just as follow:
 
 ```
-link1;link2;...;linkN
+0;sceneName1:::link1;sceneName2:::link2;...;sceneNameN:::linkN
 ...
-link2;link3;...;linkN
+N;sceneName2:::link2;sceneName3:::link3;...;sceneNameN:::linkN
+```
+
+## Launch WebApp
+
+### 1. Manually
+
+```
+pip install -r requirements.txt
+```
+
+```
+python manage.py migrate
+```
+
 ```
+python manage.py runserver
+```
+
+### 2. Using docker (recommended)
+
+You can use make commands:
+
+```
+make build
+```
+
+```
+make run
+```
+
+Or simply:
+
+```
+make deploy
+```
+
+Will run `build` and `run` commands at once.
+
+You also have `stop`, `remove`, `clean` commands:
+- `stop`: stop current container instance if exists
+- `remove`: stop and remove container instance if exists
+- `clean`: remove docker image if exists
+
 
 ## Licence
 

+ 17 - 0
docker-compose.yml

@@ -0,0 +1,17 @@
+version: '3'
+
+services:
+
+    webexpe:
+        container_name: sin3dlauncherinst
+        image: sin3dlauncher
+        restart: always
+        volumes:
+            - "./media:/usr/src/app/media" # get access to media files
+        ports:
+           - "8000:8000"
+        
+        # only comment in case it will be necessary
+        # environment:
+        #    WEBEXPE_PREFIX_URL: "${WEBEXPE_PREFIX_URL:-}"
+        #    WEB_API_PREFIX_URL: "${WEB_API_PREFIX_URL:-api}"

+ 0 - 0
launcher/__init__.py


+ 128 - 0
launcher/settings.py

@@ -0,0 +1,128 @@
+"""
+Django settings for launcher project.
+
+Generated by 'django-admin startproject' using Django 2.2.4.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/2.2/ref/settings/
+"""
+
+import os
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = 'a(wm02y7+q^voeqj9i8w&9*ryvtn0gg3bo$-k=()oz!8+5_okg'
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+ALLOWED_HOSTS = []
+
+
+# Application definition
+
+INSTALLED_APPS = [
+    'django.contrib.admin',
+    'django.contrib.auth',
+    'django.contrib.contenttypes',
+    'django.contrib.sessions',
+    'django.contrib.messages',
+    'django.contrib.staticfiles',
+    'links'
+]
+
+MIDDLEWARE = [
+    'django.middleware.security.SecurityMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
+    'django.middleware.common.CommonMiddleware',
+    'django.middleware.csrf.CsrfViewMiddleware',
+    'django.contrib.auth.middleware.AuthenticationMiddleware',
+    'django.contrib.messages.middleware.MessageMiddleware',
+    'django.middleware.clickjacking.XFrameOptionsMiddleware',
+]
+
+ROOT_URLCONF = 'launcher.urls'
+
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'DIRS': [],
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'context_processors': [
+                'django.template.context_processors.debug',
+                'django.template.context_processors.request',
+                'django.contrib.auth.context_processors.auth',
+                'django.contrib.messages.context_processors.messages',
+            ],
+        },
+    },
+]
+
+WSGI_APPLICATION = 'launcher.wsgi.application'
+
+
+# Database
+# https://docs.djangoproject.com/en/2.2/ref/settings/#databases
+
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.sqlite3',
+        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
+    }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/2.2/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+    {
+        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
+    },
+    {
+        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
+    },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/2.2/topics/i18n/
+
+LANGUAGE_CODE = 'en-us'
+
+TIME_ZONE = 'UTC'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/2.2/howto/static-files/
+
+STATIC_URL = '/static/'
+
+STATICFILES_DIRS = (
+    os.path.join(BASE_DIR, 'static'),
+)
+
+MEDIA_ROOT = "media/"
+MEDIA_URL = "media/"

+ 22 - 0
launcher/urls.py

@@ -0,0 +1,22 @@
+"""launcher URL Configuration
+
+The `urlpatterns` list routes URLs to views. For more information please see:
+    https://docs.djangoproject.com/en/2.2/topics/http/urls/
+Examples:
+Function views
+    1. Add an import:  from my_app import views
+    2. Add a URL to urlpatterns:  path('', views.home, name='home')
+Class-based views
+    1. Add an import:  from other_app.views import Home
+    2. Add a URL to urlpatterns:  path('', Home.as_view(), name='home')
+Including another URLconf
+    1. Import the include() function: from django.urls import include, path
+    2. Add a URL to urlpatterns:  path('blog/', include('blog.urls'))
+"""
+from django.contrib import admin
+from django.urls import path, include
+
+urlpatterns = [
+    path('admin/', admin.site.urls),
+    path('', include('links.urls', namespace='links')),
+]

+ 16 - 0
launcher/wsgi.py

@@ -0,0 +1,16 @@
+"""
+WSGI config for launcher project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'launcher.settings')
+
+application = get_wsgi_application()

+ 0 - 0
links/__init__.py


+ 3 - 0
links/admin.py

@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.

+ 5 - 0
links/apps.py

@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class LinksConfig(AppConfig):
+    name = 'links'

+ 2 - 2
config.py

@@ -10,5 +10,5 @@ experiment_list = [
 
 default_host = 'https://diran.univ-littoral.fr'
 
-links_data_folder = 'data'
-expe_data_folder  = 'expe'
+links_data_folder = 'links/media/data'
+expe_data_folder  = 'links/media/expe'

+ 0 - 0
links/generate/__init__.py


+ 52 - 8
generate/generate_experiment.py

@@ -10,7 +10,28 @@ import random
 # modules imports
 sys.path.insert(0, '') # trick to enable import of main folder module
 
-import config  as cfg
+# config imports
+import links.config  as cfg
+
+
+def decoded_data(data):
+    decoded_data = str(base64.b64decode(data), "utf-8")
+
+    return decoded_data
+
+
+def encode_data(data):
+    json_data = json.dumps(data)
+    link_data = base64.b64encode(str(json_data).encode('utf-8'))
+    
+    return link_data
+
+
+def extract_data(line):
+
+    data = line.replace('\n', '').split(';')
+
+    return (data[0], data[-1])
 
 
 def main():
@@ -20,6 +41,7 @@ def main():
     parser.add_argument('--data', type=str, help='data links to use', required=True)
     parser.add_argument('--scenes', type=int, help="number of scenes", required=True)
     parser.add_argument('--users', type=int, help="number of users of experiment", required=True)
+    parser.add_argument('--userId', type=int, help="tell if user identifier is used or not", required=False, default=0)
     parser.add_argument('--output', type=str, help="output filename of user links", required=True)
 
     args = parser.parse_args()
@@ -27,12 +49,15 @@ def main():
     p_data          = args.data
     p_scenes        = args.scenes
     p_users         = args.users
+    p_userId        = bool(args.userId)
     p_output        = args.output
 
+    print(p_userId)
+
     # generate link for each scene
     with open(p_data, 'r') as f:
         lines = f.readlines()
-        lines = [l.replace('\n', '') for l in lines]
+        data_lines = [extract_data(l) for l in lines]
         
         nb_elements = len(lines)
 
@@ -48,14 +73,33 @@ def main():
 
         output_f = open(filename_path, 'w')
 
-        for _ in range(p_users):
-
-            user_links = random.choices(lines, k=p_scenes)
+        for i in range(p_users):
+            
+            scene_links = random.sample(data_lines, k=p_scenes)
 
             # generate output line
-            output_line = ""
-            for link in user_links:
-                output_line += link + ';'
+            output_line = str(i) + ';' 
+            for scene_name, link in scene_links:
+
+                if p_userId:
+                    data = link.split('?q=')
+
+                    hostname = data[0]
+                    link_data = data[1]
+
+                    # decode and add user id link if asked
+                    decoded_data_link = decoded_data(link_data)
+                    json_data = json.loads(decoded_data_link)
+                    json_data['userId'] = str(i)
+                    encoded_data_link = encode_data(json_data)
+                    new_link = hostname + '?q=' + str(encoded_data_link, "utf-8")
+
+                    # add new link
+                    output_line += scene_name + ':::' + new_link + ';'
+                else:
+                    output_line += scene_name + ':::' + link + ';'
+
+
             output_line += '\n'
 
             output_f.write(output_line)

+ 4 - 3
generate/generate_experiment_link.py

@@ -9,7 +9,8 @@ import requests
 # modules imports
 sys.path.insert(0, '') # trick to enable import of main folder module
 
-import config  as cfg
+# config imports
+import links.config  as cfg
 
 
 def encode_data(data):
@@ -66,8 +67,8 @@ def main():
 
     with open(filename_path, 'w') as f:
 
-        for link in links:
-            f.write(link + '\n')
+        for id, link in enumerate(links):
+            f.write(p_scenes[id] + ';' + p_experiment + ';' + p_experiment_id + ';' + link + '\n')
 
     print("Links are saved into.. %s" % filename_path)
 

+ 0 - 0
links/migrations/__init__.py


+ 3 - 0
links/models.py

@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.

+ 35 - 0
links/templates/base.html

@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<html lang="fr">
+<head>
+    <meta charset="UTF-8"/>
+    <title>
+        {% block title %}
+
+        {% endblock %}
+    </title>
+
+    {% load staticfiles %}
+    
+    {% block stylesheets %}
+        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
+        <link rel="stylesheet" type="text/css" href="{% static "css/links.css" %}">
+        <script src="https://kit.fontawesome.com/ee9d97bd14.js" crossorigin="anonymous"></script>
+    {% endblock %}
+</head>
+<body>
+
+    <div class="container">
+        {% block content %}
+            
+        {% endblock %}
+    </div>
+
+    <!-- Global scripts used -->
+    <script type="text/javascript"> 
+    </script>
+
+    <!-- Custom Javascript file for experiments template is placed here -->
+    {% block javascripts %}
+        
+    {% endblock %}
+</body>

+ 47 - 0
links/templates/links/files.html

@@ -0,0 +1,47 @@
+{% extends 'base.html' %}
+
+{% load staticfiles %}
+
+{% block title %}
+    List of files
+{% endblock %}
+
+{% block content %}
+    
+    <h3>Links files of experiments</h3>
+    <br />
+
+    <div class="files-expe">
+
+        <br/>
+        {% if folder %}
+
+            <!-- List of items which has identifier when user pass experiment -->
+            <h3>List of files</h3>
+            <ul class="list-group">
+
+                {% for file in folder %} 
+                                    
+                    <li class="list-group-item">
+                        
+                        <div class="row">
+                            <div class="col-md-11">{{file}} </div>
+                            <div class="col-md-1 files-list" data-redirect-path="{{file}}"><i class="fas fa-arrow-circle-right"></i></div>
+                        </div>
+                    </li>
+                {% endfor %} 
+            </ul>
+
+
+        {% else %}
+            <div class="alert alert-warning" role="alert">
+                <i>Experiment folder is empty</i>
+            </div>
+        {% endif %}
+    </div>
+{% endblock %}
+
+{% block javascripts %}
+    <script src="{% static "js/files.js" %}"></script>
+{% endblock %}
+

+ 41 - 0
links/templates/links/links.html

@@ -0,0 +1,41 @@
+{% extends 'base.html' %}
+
+{% load staticfiles %}
+
+{% block title %}
+    Links list
+{% endblock %}
+
+{% block content %}
+    
+    <h3>Select your user identifier</h3>
+    <br />
+
+    <div class="row">
+        <div class="col-md-4 offset-md-4">
+            <form method="GET" action="/expe">
+                <div class="form-group">
+                    <input type="number" min="0" class="form-control" name="userId" placeholder="Enter your user identifier"/>
+                </div>
+            </form>
+        </div>
+    </div>
+
+    <br />
+    <hr />
+    <br />
+
+    <ul class="list-group" id="links-list">
+    
+    </ul>
+
+{% endblock %}
+
+{% block javascripts %}
+    <script type="text/javascript"> 
+        // Utils informations
+        var links = "{{links}}"
+    </script>
+    <script src="{% static "js/links.js" %}"></script>
+{% endblock %}
+

+ 3 - 0
links/tests.py

@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.

+ 16 - 0
links/urls.py

@@ -0,0 +1,16 @@
+from django.contrib import admin
+from django.urls import path
+from django.conf import settings
+from django.conf.urls.static import static
+
+from . import views
+
+app_name = 'expe'
+
+urlpatterns = [
+    path('', views.list_files, name='list_files'),
+    path('links', views.user_links, name='user_links'),
+]
+
+if settings.DEBUG is True:
+    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

+ 63 - 0
links/views.py

@@ -0,0 +1,63 @@
+# django imports
+from django.shortcuts import render
+from django.http import HttpResponse
+from django.conf import settings
+from django.contrib.auth.decorators import login_required
+from django.http import Http404
+
+# main imports
+import os
+import json
+
+from . import config  as cfg
+
+
+def list_files(request):
+
+    # get param 
+    # TODO : implement view which list all expe links file
+
+    experiment_path = cfg.expe_data_folder
+
+    files = sorted(os.listdir(experiment_path))
+
+    data = {
+        'folder': files
+    }
+
+    return render(request, 'links/files.html', data)
+
+
+def user_links(request):
+
+    filename = request.GET.get('filename')
+
+    if filename is None:
+        # send 404 error
+        raise Http404("Page does not exist")
+    
+    filepath = os.path.join(cfg.expe_data_folder, filename)
+
+    if not os.path.exists(filepath):
+        # send 404 error
+        raise Http404("File asked does not exist")
+
+    # read data and send it
+    with open(filepath, 'r') as f:
+        lines = [l.replace('\n', '') for l in f.readlines()]
+
+    links = {}
+    for line in lines:
+        data = line.split(';')
+        links[data[0]] = data[1:]
+            
+    data = {
+        'links': json.dumps(links)
+    }
+    
+    return render(request, 'links/links.html', data)
+
+
+
+
+

+ 21 - 0
manage.py

@@ -0,0 +1,21 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'launcher.settings')
+    try:
+        from django.core.management import execute_from_command_line
+    except ImportError as exc:
+        raise ImportError(
+            "Couldn't import Django. Are you sure it's installed and "
+            "available on your PYTHONPATH environment variable? Did you "
+            "forget to activate a virtual environment?"
+        ) from exc
+    execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+    main()

+ 3 - 0
requirements.txt

@@ -0,0 +1,3 @@
+Django
+numpy
+requests

+ 29 - 0
static/css/links.css

@@ -0,0 +1,29 @@
+body {
+    background-color: lightgrey;
+}
+
+.files-expe{
+    margin-left: 20%;
+    margin-right: 20%;
+    text-align: left;
+}
+
+.already-used{
+    background-color: lightslategrey;
+}
+
+.already-used a{
+    color:black;
+}
+
+.already-used a:hover{
+    text-decoration: none;
+}
+
+.files-expe a:hover{
+    text-decoration: none;
+}
+
+.files-expe a{
+    color:black;
+}

+ 18 - 0
static/js/files.js

@@ -0,0 +1,18 @@
+const toggleVisible = ele => ele.style.display = ele.style.display === 'none' ? 'block' : 'none'
+const toggleClass = (ele, class1, class2) => ele.className = ele.className === class1 ? class2 : class1
+const baseUrl    = location.protocol + '//' + window.location.host + '/'
+window.addEventListener('DOMContentLoaded', () => {
+    // Display list of files from day folder
+    // need to parse as `Array`
+    Array.from(document.getElementsByClassName('files-list')).forEach(item => {
+        item.addEventListener('click', event => {
+            event.preventDefault()
+            currentElem = event.currentTarget
+
+            // get list element
+            let filePath = currentElem.getAttribute('data-redirect-path')
+
+            window.location = baseUrl + 'links?filename=' + filePath
+        })
+    })
+})

+ 98 - 0
static/js/links.js

@@ -0,0 +1,98 @@
+const links_data = JSON.parse(links.replace(/&quot;/g, '"'))
+
+
+function loadDataList(elem, list){
+    userId = elem.value
+
+    if (userId){
+        let currentLinks = links_data[userId]
+        
+        // remove event listener of each element by default
+        if (list.children.length > 0){
+            for (var element of list.children){
+                element.removeEventListener('click', elemClick)
+            }
+        }
+
+        list.innerHTML = ""
+    
+        currentLinks.forEach((element, index) => {
+    
+            if (element.length > 0){
+    
+                data = element.split(':::')
+
+                // add of div elements
+                rowDiv = document.createElement('div')
+                rowDiv.setAttribute('class', 'row')
+
+                rowDivLeft = document.createElement('div')
+                rowDivLeft.setAttribute('class', 'col-md-11')
+
+                rowDivRight = document.createElement('div')
+                rowDivRight.setAttribute('class', 'col-md-1')
+
+                // create link
+                currentLink = document.createElement('a')
+                currentLink.setAttribute('href', data[1])
+                currentLink.innerHTML = 'Link ' + (index + 1) + ': ' + data[0]
+                
+                // add of elements
+                rowDivLeft.appendChild(currentLink)
+                rowDiv.appendChild(rowDivLeft)
+                rowDiv.appendChild(rowDivRight)
+
+                currentLi = document.createElement('li')
+                currentLi.setAttribute('class', 'list-group-item')
+                currentLi.appendChild(rowDiv)
+
+                list.appendChild(currentLi)
+            }
+        });
+    }
+}
+
+function elemClick(event){
+    event.preventDefault()
+    
+    currentElem = event.currentTarget
+    
+    // Add <i class="fas fa-check"></i>
+    divLeft = currentElem.getElementsByClassName('col-md-1')[0]
+
+    if (divLeft.children.length <= 0){
+        iconElem = document.createElement('li')
+        iconElem.setAttribute('class', 'fas fa-check')
+        divLeft.appendChild(iconElem)
+
+        // update `li` class element
+        currentElem.setAttribute('class', 'list-group-item already-used')
+    }
+
+    // retrieve and open link in new tab
+    link = currentElem.getElementsByTagName('a')[0]
+    url = link.getAttribute('href')
+    var win = window.open(url, '_blank');
+    win.focus();
+}
+
+window.addEventListener('DOMContentLoaded', () => {
+    // Display list of files from day folder
+    // need to parse as `Array`
+
+    const inputElement = document.getElementsByName('userId')[0]
+    const linksList = document.getElementById('links-list')
+
+    loadDataList(inputElement, linksList)
+    
+    inputElement.addEventListener('change', event => {
+        event.preventDefault()
+        currentElem = event.currentTarget
+
+        loadDataList(currentElem, linksList)
+
+        for (var element of linksList.children){
+            element.addEventListener('click', elemClick)
+        }
+    })
+})