Parcourir la source

Update of front content

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

+ 147 - 0
DOCUMENTATION.md

@@ -0,0 +1,147 @@
+# Create your own expe
+
+## Description
+
+This website can let you create and manage your own experiences 
+
+## Explanations
+
+The `expe` module is the django app created for managing experiences.
+
+- `expe/config.py`: contains the main variables used by website, save experiences content and experiences configuration.
+- `expe/views.py`: is django app file used for enable routes of website.
+- `expe/expes/run.py`: contains **run** functions in order to launch step of experience.
+- `expe/expes/classes`: is folder which contains all the necessary Python classes for experiences.
+
+## Create your own experience
+
+### 1. Experience configuration
+
+Let's start with the `expe/config.py` python file. As explained earlier, this file contains experiences configuration. The variable `expes_configuration` is the dictionnary which declares all information of experiences.
+
+An example for the `quest_one_image` key experience:
+
+```python
+'quest_one_image':{
+    'text':{
+        'question': "Do you see one image or a composition of more than one?",
+        'indication': "press left if you see one image, right if not",
+        'end_text': "Experience is finished. Thanks for your participation",
+    },
+    'params':{
+        'iterations': 10
+    },
+    'session_params': [
+        'expe_percentage',
+        'expe_orientation',
+        'expe_position',
+        'expe_stim',
+        'expe_previous_iteration'
+    ],
+
+    # template file used in django `expe` route
+    'template': 'expe/expe.html',
+
+    # javascript file used
+    'js':[
+        'loadImg.js',
+        'keyEvents.js'
+    ],
+    'output_header': "stimulus;name_stimulus;cropping_percentage;...\n"
+}
+```
+
+The `params` section is where you put all your necessary information for your experience.
+
+### 2. The experience `expe` route
+
+The `expe/` route define by the `expe` function in `expe/views.py` is used to launch experience. This route uses few parameters pass using GET method:
+- `expe`: the experience name to use
+- `scene`: the scene name to use for this experience
+- `iteration`: step of this experience
+- `answer`: the answer of the user
+
+Using this parameter, the route know which experience to launch with specific scene and manage experience steps.
+
+**Note:** `answer` and `iteration` parameters are used into `js/keyEvents.js` file. This means the `answer` and `iteration` values are sent depending of user interactions. You can implement your own interaction by creating your own `js` file and add it into your experience configuration declaration (see `expe/config.py`).
+
+### 3. The `run` experience function
+
+Into the `expe` function in `expe/views.py`, the `run` method your experience is dynamically call. Hence you need to implement into the `expe/expes/run.py` a function which follow this naming convention:
+
+- `run_{{you_experience_name}}`
+
+As you have communication exchanges between the django server and the client side, it's necessary to store the experience process at each step.
+
+Hence, this function need to follow this prototype:
+
+```python
+def run_experience_name(request, model_filepath, output_file):
+```
+
+Information about parameters:
+- `request`: contains all information into GET, POST and session storages
+- `model_filepath`: filename where you need to store information about experience model into a binary file (can be just data information or object instanciated from file of `expe/expes/classes`)
+- `output_file`: buffer where you can add information about your experience (following your `output_header` declared into your experience configuration)
+
+
+Example of accessing request variables:
+```python
+scene_name_session = request.session.get('scene')
+scene_name_get     = request.GET.get('scene')
+scene_name_post    = request.POST.get('scene')
+```
+
+Example of loading or saving Python object (need of pickle):
+```python
+# check if necessary to construct `quest` object or if backup exists
+if not os.path.exists(model_filepath):
+    qp = QuestPlus(stim_space, [thresholds, slopes], function=psychometric_fun)
+else:
+    print('Load `qp` model')
+    filehandler = open(model_filepath, 'rb') 
+    qp = pickle.load(filehandler)
+``` 
+
+```python
+# save `quest` model
+file_pi = open(model_filepath, 'wb') 
+pickle.dump(qp, file_pi)
+```
+
+Example of writing and append information into `output_file`:
+
+```python
+line = str(previous_stim) 
+line += ";" + scene_name 
+line += ";" + str(previous_percentage)
+line += ";" + str(previous_orientation) 
+line += ";" + str(previous_position) 
+line += ";" + str(answer) 
+line += ";" + str(answer_time) 
+line += ";" + str(entropy) 
+line += '\n'
+
+output_file.write(line)
+output_file.flush()
+```
+
+### 5. Display experience data into custom template
+
+Finally your `run` function need to return python dictionnary of data your need to use into your `expe/` django template. 
+
+If you want to create your own template, specify your template path into configuration:
+
+```python
+'experience_name':{
+    ...
+    # template file used in django `expe` route
+    'template': 'expe/my_expe_template.html',
+    ...
+}
+```
+
+Example of way to use your experience data into template:
+```python
+{{expe_data|get_value_from_dict:'image_path'}}
+```

+ 1 - 1
README.md

@@ -1,4 +1,4 @@
-# DjangoRecipes
+# Django Web expe
 
 ## Description
 

+ 22 - 2
expe/config.py

@@ -23,6 +23,8 @@ expe_name_list              = ["quest_one_image"]
 
 # configure experiences labels
 expes_configuration         = {
+
+    # First experience configuration
     'quest_one_image':{
         'text':{
             'question': "Do you see one image or a composition of more than one?",
@@ -30,7 +32,25 @@ expes_configuration         = {
             'end_text': "Experience is finished. Thanks for your participation",
         },
         'params':{
-            'iterations': 4
-        }
+            'iterations': 10
+        },
+        'session_params': [
+            'expe_percentage',
+            'expe_orientation',
+            'expe_position',
+            'expe_stim',
+            'expe_previous_iteration'
+        ],
+
+        # template file used in django `expe` route
+        'template': 'expe/expe.html',
+
+        # javascript file used
+        'js':[
+            'loadImg.js',
+            'keyEvents.js'
+        ],
+        'output_header': 
+            "stimulus;name_stimulus;cropping_percentage;orientation;image_ref_position;answer;time_reaction;entropy\n"
     }
 }

+ 0 - 0
expe/expes/classes/__init__.py


expe/expes/quest_plus.py → expe/expes/classes/quest_plus.py


+ 31 - 4
expe/expes/run.py

@@ -4,6 +4,9 @@ import time
 import numpy as np
 import pickle
 
+# django imports
+from django.conf import settings
+
 # module imports
 from ..utils import api
 
@@ -11,13 +14,13 @@ from ..utils.processing import crop_images
 from .. import config as cfg
 
 # expe imports
-from .quest_plus import QuestPlus
-from .quest_plus import psychometric_fun
+from .classes.quest_plus import QuestPlus
+from .classes.quest_plus import psychometric_fun
 
 
 def run_quest_one_image(request, model_filepath, output_file):
 
-    # get parameters
+    # 1. get session parameters
     qualities = request.session.get('qualities')
     scene_name = request.session.get('scene')
     expe_name = request.session.get('expe')
@@ -31,6 +34,7 @@ def run_quest_one_image(request, model_filepath, output_file):
     else:
         request.session['expe_started'] = False
 
+    # 2. Get expe information if started
     # first time only init `quest`
     # if experience is started we can save data
     if request.session.get('expe_started'):
@@ -47,6 +51,7 @@ def run_quest_one_image(request, model_filepath, output_file):
             previous_position = request.session.get('expe_position')
             previous_stim = request.session.get('expe_stim')
 
+    # 3. Load or create Quest instance
     # default params
     thresholds = np.arange(50, 10000, 50)
     stim_space=np.asarray(qualities)
@@ -60,6 +65,7 @@ def run_quest_one_image(request, model_filepath, output_file):
         filehandler = open(model_filepath, 'rb') 
         qp = pickle.load(filehandler)
     
+    # 4. If expe started update and save experience information and model
     # if experience is already began
     if request.session.get('expe_started'):
 
@@ -86,6 +92,7 @@ def run_quest_one_image(request, model_filepath, output_file):
     file_pi = open(model_filepath, 'wb') 
     pickle.dump(qp, file_pi)
 
+    # 5. Contruct new image and save it
     # construct image 
     if iteration < cfg.expes_configuration[expe_name]['params']['iterations']:
         # process `quest`
@@ -102,6 +109,21 @@ def run_quest_one_image(request, model_filepath, output_file):
         request.session['expe_finished'] = True
         return None
 
+    # save image using user information
+    # create output folder for tmp files if necessary
+    tmp_folder = os.path.join(settings.MEDIA_ROOT, cfg.output_tmp_folder)
+
+    if not os.path.exists(tmp_folder):
+        os.makedirs(tmp_folder)
+
+    # generate tmp merged image (pass as BytesIO was complicated..)
+    filepath_img = os.path.join(tmp_folder, request.session.get('id') + '_' + scene_name + '' + expe_name + '.png')
+    
+    # replace img_merge if necessary (new iteration of expe)
+    if img_merge is not None:
+        img_merge.save(filepath_img)
+
+    # 6. Prepare session data for current iteration and data for view
     # set current step data
     request.session['expe_percentage'] = percentage
     request.session['expe_orientation'] = orientation
@@ -113,4 +135,9 @@ def run_quest_one_image(request, model_filepath, output_file):
     # expe is now started
     request.session['expe_started'] = True
 
-    return img_merge
+    # here you can save whatever you need for you experience
+    data_expe = {
+        'image_path': filepath_img
+    }
+
+    return data_expe

+ 18 - 2
expe/templates/base.html

@@ -9,6 +9,8 @@
     </title>
 
     {% load staticfiles %}
+    {% load apptags %}
+    
     {% 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/expe.css" %}">
@@ -36,9 +38,23 @@
             baseUrl += BASE + '/'
         }
 
-        const expeUrl  = baseUrl + 'expe'     
+        const expeUrl  = baseUrl + 'expe'  
+
+        // EXPE variables parts
+        // get access to django variables
+        var BEGIN_EXPE = "{{request.session.expe_started}}"
+        var END_EXPE   = "{{request.session.expe_finished}}"
     </script>
+
+    <!-- Custom Javascript file for experience template is placed here -->
     {% block javascripts %}
-    
+        
     {% endblock %}
+
+    <!-- Dynamically load JS file of experience -->
+    {% for file in js %}
+        {% with 'js/'|add:file as js_static %}
+            <script src="{% static js_static %}"></script>
+        {% endwith %}
+    {% endfor %}
 </body>

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

@@ -1,6 +1,7 @@
 {% extends 'base.html' %}
 
 {% load staticfiles %}
+{% load apptags %}
 
 {% block title %}
     Expe {{ expe_name }}
@@ -17,16 +18,10 @@
 
     <!-- TODO : Load    img from bitmap with javascript `loadImg.js` -->
     {% if not request.session.expe_finished %}
-        <img id="expeImg" src="{{img_merged_path}}" data-img="{{request.session.img_merged}}"/>
+        <img id="expeImg" src="{{expe_data|get_value_from_dict:'image_path'}}" data-img="{{request.session.img_merged}}"/>
     {% endif %}
 
     {% block javascripts %}
-        <script type="text/javascript"> 
-            // get access to django variables
-            var BEGIN_EXPE = "{{request.session.expe_started}}"
-            var END_EXPE   = "{{request.session.expe_finished}}"
-        </script>
-        <script src="{% static "js/loadImg.js" %}"></script>
-        <script src="{% static "js/keyEvents.js" %}"></script>
+ 
     {% endblock %}
 {% endblock %}

+ 0 - 0
expe/templatetags/__init__.py


+ 13 - 0
expe/templatetags/apptags.py

@@ -0,0 +1,13 @@
+# django imports
+from django import template
+
+register = template.Library()
+
+@register.filter('get_value_from_dict')
+def get_value_from_dict(dict_data, key):
+    """
+    usage example {{ your_dict|get_value_from_dict:your_key }}
+    """
+    print(dict_data)
+    if key:
+        return dict_data.get(key)

+ 4 - 3
expe/utils/functions.py

@@ -1,6 +1,9 @@
 # main imports
 import random
 
+# module imports
+from .. import config as cfg
+
 def uniqueID():
     '''
     Return unique identifier for current user and 
@@ -13,6 +16,4 @@ def write_header_expe(f, expe_name):
     Write specific header into file
     '''
 
-    if expe_name == 'quest_one_image':
-        f.write('stimulus' + ";" + "name_stimulus" + ";" + 'cropping_percentage' + ";" + 'orientation' + ';' 
-            + 'image_ref_position' + ';' + 'answer' + ';' + 'time_reaction' + ';' + 'entropy' + '\n')
+    f.write(cfg.expes_configuration[expe_name]['output_header'])

+ 25 - 30
expe/views.py

@@ -19,10 +19,10 @@ from io import BytesIO
 
 
 # expe imports
-from .expes.quest_plus import QuestPlus
-from .expes.quest_plus import psychometric_fun
+from .expes.classes.quest_plus import QuestPlus
+from .expes.classes.quest_plus import psychometric_fun
 
-from .expes.run import run_quest_one_image
+from .expes import run as run_expe
 
 # image processing imports
 import io
@@ -36,7 +36,7 @@ from .utils.processing import crop_images
 from . import config as cfg
 
 
-def get_base_data():
+def get_base_data(expe_name=None):
     '''
     Used to store default data to send for each view
     '''
@@ -44,6 +44,10 @@ def get_base_data():
 
     data['BASE'] = settings.WEBEXPE_PREFIX_URL
 
+    # if expe name is used
+    if expe_name is not None:
+        data['js'] = cfg.expes_configuration[expe_name]['js']
+
     return data
 
 
@@ -89,9 +93,6 @@ def expe(request):
     expe_name = request.GET.get('expe')
     scene_name = request.GET.get('scene')
     
-    # default filepath name
-    filepath_img = ''
-
     # unique user ID during session (user can launch multiple exeperiences)
     if 'id' not in request.session:
         request.session['id'] = functions.uniqueID()
@@ -120,6 +121,7 @@ def expe(request):
     else:
         output_file = open(results_filepath, 'a')
 
+    # TODO : add crontab task to erase generated img and model data
     # create `quest` object if not exists    
     models_folder = os.path.join(settings.MEDIA_ROOT, cfg.model_expe_folder.format(expe_name, current_day))
 
@@ -129,44 +131,37 @@ def expe(request):
     model_filename = result_filename.replace('.csv', '.obj')
     model_filepath = os.path.join(models_folder, model_filename)
 
-    # run `quest` expe
-    img_merge = run_quest_one_image(request, model_filepath, output_file)
+    # run expe method using `expe_name`
+    function_name = 'run_' + expe_name
 
-    if not request.session.get('expe_finished'):
-        # create output folder for tmp files if necessary
-        tmp_folder = os.path.join(settings.MEDIA_ROOT, cfg.output_tmp_folder)
+    try:
+        run_expe_method = getattr(run_expe, function_name)
+    except AttributeError:
+        raise NotImplementedError("Run expe method `{}` not implement `{}`".format(run_expe.__name__, function_name))
 
-        if not os.path.exists(tmp_folder):
-            os.makedirs(tmp_folder)
+    expe_data = run_expe_method(request, model_filepath, output_file)
 
-        # generate tmp merged image (pass as BytesIO was complicated..)
-        # TODO : add crontab task to erase generated img
-        filepath_img = os.path.join(tmp_folder, request.session.get('id') + '_' + scene_name + '' + expe_name + '.png')
-        
-        # replace img_merge if necessary (new iteration of expe)
-        if img_merge is not None:
-            img_merge.save(filepath_img)
-    else:
+    if request.session.get('expe_finished'):
         # reinit session as default value
+        # here generic expe params
         del request.session['expe']
         del request.session['scene']
         del request.session['qualities']
         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']
+
+        # specific current expe session params (see `config.py`)
+        for key in cfg.expes_configuration[expe_name]['session_params']:
+            del request.session[key]
 
     # get base data
-    data = get_base_data()
+    data = get_base_data(expe_name)
     # expe parameters
     data['expe_name']       = expe_name
-    data['img_merged_path'] = filepath_img
+    data['expe_data']       = expe_data
     data['end_text']        = cfg.expes_configuration[expe_name]['text']['end_text']
 
-    return render(request, 'expe/expe.html', data)
+    return render(request, cfg.expes_configuration[expe_name]['template'], data)
 
 
 @login_required(login_url="login/")

+ 0 - 1
static/js/keyEvents.js

@@ -65,7 +65,6 @@ function checkKey(e) {
          
          // construct url with params for experience
          var params = "?scene=" + scene + "&expe=" + expe + "&iteration=" + iteration + "&answer=" + answer
-         console.log(expeUrl + params)
          window.location = expeUrl + params
       }
    }