Repository: fitbenchmarking/fitbenchmarking

Issue:
Make the css links relative
At the moment the css files are linked to using an absolute file path.  We should change this to a relative path, which will allow easy deployment of the result on the web, as well as viewing the results generated in a Docker container on a native filesystem.

Probably the cleanest way to do this would be to copy the css files into a `css` subdirectory of the `results` directory, and change paths to be to that relative to root, e.g., `../../css/main_style.css`, not `/home/fitbenchmarking/fitbenchmarking/templates/main_style.css`.  This would allow the `results` directory to be moved around without breaking the output.



Discussion:

Patch:
diff --git a/fitbenchmarking/cli/main.py b/fitbenchmarking/cli/main.py
index b8fece31..603f7912 100755
--- a/fitbenchmarking/cli/main.py
+++ b/fitbenchmarking/cli/main.py
@@ -22,6 +22,7 @@ from fitbenchmarking.core.fitting_benchmarking import benchmark
 from fitbenchmarking.core.results_output import save_results
 from fitbenchmarking.utils.exceptions import OptionsError
 from fitbenchmarking.utils.log import get_logger, setup_logger
+from fitbenchmarking.utils.misc import get_css
 from fitbenchmarking.utils.options import Options
 
 LOGGER = get_logger()
@@ -138,8 +139,7 @@ def run(problem_sets, options_file='', debug=False):
     root = os.path.dirname(inspect.getfile(fitbenchmarking))
     template_dir = os.path.join(root, 'templates')
     env = Environment(loader=FileSystemLoader(template_dir))
-    style_css = os.path.join(template_dir, 'main_style.css')
-    custom_style = os.path.join(template_dir, 'custom_style.css')
+    css = get_css(options,options.results_dir)
     template = env.get_template("index_page.html")
     group_links = [os.path.join(d, "{}_index.html".format(g))
                    for g, d in zip(groups, result_dir)]
@@ -147,8 +147,8 @@ def run(problem_sets, options_file='', debug=False):
 
     with open(output_file, 'w') as fh:
         fh.write(template.render(
-            css_style_sheet=style_css,
-            custom_style=custom_style,
+            css_style_sheet=css['main'],
+            custom_style=css['custom'],
             groups=groups,
             group_link=group_links,
             zip=zip))
diff --git a/fitbenchmarking/core/results_output.py b/fitbenchmarking/core/results_output.py
index 81a14c39..ecdcc824 100644
--- a/fitbenchmarking/core/results_output.py
+++ b/fitbenchmarking/core/results_output.py
@@ -6,11 +6,13 @@ from __future__ import (absolute_import, division, print_function)
 import inspect
 import os
 from jinja2 import Environment, FileSystemLoader
+from shutil import copy2
 
 import fitbenchmarking
 from fitbenchmarking.results_processing import performance_profiler, plots, \
     support_page, tables
 from fitbenchmarking.utils import create_dirs
+from fitbenchmarking.utils.misc import get_css
 
 
 def save_results(options, results, group_name,
@@ -36,7 +38,15 @@ def save_results(options, results, group_name,
     :return: Path to directory of group results
     :rtype: str
     """
-    _, group_dir, supp_dir, fig_dir = create_directories(options, group_name)
+    group_dir, supp_dir, fig_dir, local_css_dir = \
+                                        create_directories(options, group_name)
+
+    # copy the template css files into a subfolder of results
+    root = os.path.dirname(inspect.getfile(fitbenchmarking))
+    template_dir = os.path.join(root, 'templates')
+    local_css_dir = os.path.join(options.results_dir,'css')
+    for css_file in ["main_style","custom_style","math_style","table_style"]:
+        copy2(os.path.join(template_dir,css_file+".css"),local_css_dir)
     best_results = preproccess_data(results)
     pp_locations = performance_profiler.profile(results, fig_dir)
 
@@ -82,7 +92,8 @@ def create_directories(options, group_name):
     group_dir = create_dirs.group_results(results_dir, group_name)
     support_dir = create_dirs.support_pages(group_dir)
     figures_dir = create_dirs.figures(support_dir)
-    return results_dir, group_dir, support_dir, figures_dir
+    local_css_dir = create_dirs.css(options.results_dir)
+    return group_dir, support_dir, figures_dir, local_css_dir
 
 
 def preproccess_data(results_per_test):
@@ -139,7 +150,7 @@ def create_plots(options, results, best_results, figures_dir):
         # If none of the fits succeeded, params could be None
         # Otherwise, add the best fit to the plot
         if best.params is not None:
-            plot_path = plot.plot_best(best.minimizer, best.params)
+            plot_path = plot.plot_best(best.sanitised_min_name, best.params)
             best.figure_link = plot_path
         else:
             best.figure_error = 'Minimizer failed to produce any parameters'
@@ -151,7 +162,8 @@ def create_plots(options, results, best_results, figures_dir):
             # Don't plot best again
             if not result.is_best_fit:
                 if result.params is not None:
-                    plot_path = plot.plot_fit(result.minimizer, result.params)
+                    plot_path = plot.plot_fit(result.sanitised_min_name,
+                                              result.params)
                     result.figure_link = plot_path
                 else:
                     result.figure_error = 'Minimizer failed to produce any ' \
@@ -180,11 +192,8 @@ def create_problem_level_index(options, table_names, group_name,
     root = os.path.dirname(inspect.getfile(fitbenchmarking))
     template_dir = os.path.join(root, 'templates')
     env = Environment(loader=FileSystemLoader(template_dir))
-    style_css = os.path.join(template_dir, 'main_style.css')
-    custom_style = os.path.join(template_dir, 'custom_style.css')
-    maths_style = os.path.join(template_dir, 'math_style.css')
+    css = get_css(options,group_dir)
     template = env.get_template("problem_index_page.html")
-
     output_file = os.path.join(group_dir, '{}_index.html'.format(group_name))
     links = [v + "html" for v in table_names.values()]
     names = table_names.keys()
@@ -192,9 +201,9 @@ def create_problem_level_index(options, table_names, group_name,
     index = table_descriptions[options.comparison_mode]
     with open(output_file, 'w', encoding="utf-8") as fh:
         fh.write(template.render(
-            css_style_sheet=style_css,
-            custom_style=custom_style,
-            maths_style=maths_style,
+            css_style_sheet=css['main'],
+            custom_style=css['custom'],
+            maths_style=css['math'],
             group_name=group_name,
             index=index,
             table_type=names,
diff --git a/fitbenchmarking/results_processing/acc_table.py b/fitbenchmarking/results_processing/acc_table.py
index 12e4be1c..dd2e9efd 100644
--- a/fitbenchmarking/results_processing/acc_table.py
+++ b/fitbenchmarking/results_processing/acc_table.py
@@ -2,6 +2,7 @@
 Accuracy table
 """
 import numpy as np
+import os
 from fitbenchmarking.results_processing.base_table import Table
 
 
@@ -44,7 +45,7 @@ class AccTable(Table):
                                        group_dir, pp_locations, table_name)
 
         self.has_pp = True
-        self.pp_filenames = [self.pp_locations[0]]
+        self.pp_filenames = [os.path.relpath(self.pp_locations[0],group_dir)]
 
     def get_values(self, results_dict):
         """
diff --git a/fitbenchmarking/results_processing/compare_table.py b/fitbenchmarking/results_processing/compare_table.py
index 3a1cc483..6fee7f93 100644
--- a/fitbenchmarking/results_processing/compare_table.py
+++ b/fitbenchmarking/results_processing/compare_table.py
@@ -2,6 +2,7 @@
 compare table
 """
 import numpy as np
+import os
 from fitbenchmarking.results_processing.base_table import Table
 
 
@@ -41,7 +42,8 @@ class CompareTable(Table):
                                            group_dir, pp_locations, table_name)
 
         self.has_pp = True
-        self.pp_filenames = pp_locations
+        self.pp_filenames = \
+                        [os.path.relpath(pp,group_dir) for pp in pp_locations]
 
     def get_values(self, results_dict):
         """
diff --git a/fitbenchmarking/results_processing/local_min_table.py b/fitbenchmarking/results_processing/local_min_table.py
index 818a6e11..65114212 100644
--- a/fitbenchmarking/results_processing/local_min_table.py
+++ b/fitbenchmarking/results_processing/local_min_table.py
@@ -2,6 +2,7 @@
 compare table
 """
 import numpy as np
+import os
 from fitbenchmarking.results_processing.base_table import Table
 
 GRAD_TOL = 1e-1
@@ -57,7 +58,8 @@ class LocalMinTable(Table):
                                             table_name)
 
         self.has_pp = True
-        self.pp_filenames = pp_locations
+        self.pp_filenames = \
+                        [os.path.relpath(pp,group_dir) for pp in pp_locations]
 
     def get_values(self, results_dict):
         """
diff --git a/fitbenchmarking/results_processing/plots.py b/fitbenchmarking/results_processing/plots.py
index 0262cef7..830228cd 100644
--- a/fitbenchmarking/results_processing/plots.py
+++ b/fitbenchmarking/results_processing/plots.py
@@ -176,7 +176,7 @@ class Plot(object):
                        plot_options=plot_options_dict,
                        y=y)
         self.format_plot()
-        file = "{}_fit_for_{}.png".format(minimizer,
+        file = "{}_fit_for_{}.png".format(self.result.sanitised_min_name,
                                           self.result.sanitised_name)
         file_name = os.path.join(self.figures_dir, file)
         self.fig.savefig(file_name)
@@ -215,7 +215,7 @@ class Plot(object):
                        x=self.x,
                        y=self.problem.eval_f(params, self.x))
         self.format_plot()
-        file = "{}_fit_for_{}.png".format(minimizer,
+        file = "{}_fit_for_{}.png".format(self.result.sanitised_min_name,
                                           self.result.sanitised_name)
         file_name = os.path.join(self.figures_dir, file)
         self.fig.savefig(file_name)
diff --git a/fitbenchmarking/results_processing/runtime_table.py b/fitbenchmarking/results_processing/runtime_table.py
index 6ba4b609..b0076c17 100644
--- a/fitbenchmarking/results_processing/runtime_table.py
+++ b/fitbenchmarking/results_processing/runtime_table.py
@@ -2,6 +2,7 @@
 Runtime table
 """
 import numpy as np
+import os
 from fitbenchmarking.results_processing.base_table import Table
 
 
@@ -42,7 +43,7 @@ class RuntimeTable(Table):
                                            group_dir, pp_locations, table_name)
 
         self.has_pp = True
-        self.pp_filenames = [self.pp_locations[1]]
+        self.pp_filenames = [os.path.relpath(self.pp_locations[1],group_dir)]
 
     def get_values(self, results_dict):
         """
diff --git a/fitbenchmarking/results_processing/support_page.py b/fitbenchmarking/results_processing/support_page.py
index 2719e9fc..eed6b1bb 100644
--- a/fitbenchmarking/results_processing/support_page.py
+++ b/fitbenchmarking/results_processing/support_page.py
@@ -10,6 +10,7 @@ import os
 from jinja2 import Environment, FileSystemLoader
 
 import fitbenchmarking
+from fitbenchmarking.utils.misc import get_css
 
 
 def create(results_per_test, group_name, support_pages_dir,
@@ -58,7 +59,8 @@ def create_prob_group(prob_results, group_name, support_pages_dir,
         prob_name = result.sanitised_name
 
         file_name = '{}_{}_{}.html'.format(
-            group_name, prob_name, result.minimizer).lower()
+            group_name, prob_name, result.sanitised_min_name)
+        file_name = file_name.lower()
         file_path = os.path.join(support_pages_dir, file_name)
 
         # Bool for print message/insert image
@@ -79,16 +81,14 @@ def create_prob_group(prob_results, group_name, support_pages_dir,
         root = os.path.dirname(inspect.getfile(fitbenchmarking))
         template_dir = os.path.join(root, "templates")
         env = Environment(loader=FileSystemLoader(template_dir))
-        style_css = os.path.join(template_dir, 'main_style.css')
-        table_css = os.path.join(template_dir, 'table_style.css')
-        custom_style = os.path.join(template_dir, 'custom_style.css')
+        css = get_css(options,support_pages_dir)
         template = env.get_template("support_page_template.html")
 
         with open(file_path, 'w') as fh:
             fh.write(template.render(
-                css_style_sheet=style_css,
-                table_style=table_css,
-                custom_style=custom_style,
+                css_style_sheet=css['main'],
+                table_style=css['table'],
+                custom_style=css['custom'],
                 title=result.name,
                 equation=result.problem.equation,
                 initial_guess=result.ini_function_params,
@@ -100,7 +100,7 @@ def create_prob_group(prob_results, group_name, support_pages_dir,
                 fitted_plot_available=fit_success,
                 fitted_plot=fig_fit))
 
-        result.support_page_link = file_path
+        result.support_page_link = os.path.relpath(file_path)
 
 
 def get_figure_paths(result):
diff --git a/fitbenchmarking/results_processing/tables.py b/fitbenchmarking/results_processing/tables.py
index 59e6b7c9..81c858e4 100644
--- a/fitbenchmarking/results_processing/tables.py
+++ b/fitbenchmarking/results_processing/tables.py
@@ -12,6 +12,7 @@ from jinja2 import Environment, FileSystemLoader
 import fitbenchmarking
 from fitbenchmarking.results_processing.base_table import Table
 from fitbenchmarking.utils.exceptions import UnknownTableError
+from fitbenchmarking.utils.misc import get_css
 
 ERROR_OPTIONS = {0: "Successfully converged",
                  1: "Software reported maximum number of iterations exceeded",
@@ -91,10 +92,7 @@ def create_results_tables(options, results, best_results, group_name,
 
             root = os.path.dirname(getfile(fitbenchmarking))
             template_dir = os.path.join(root, 'templates')
-            style_css = os.path.join(template_dir, 'main_style.css')
-            table_css = os.path.join(template_dir, 'table_style.css')
-            custom_style = os.path.join(template_dir, 'custom_style.css')
-            maths_style = os.path.join(template_dir, 'math_style.css')
+            css = get_css(options,group_dir)
             env = Environment(loader=FileSystemLoader(template_dir))
             template = env.get_template("table_template.html")
             html_output_file = file_path + 'html'
@@ -107,10 +105,10 @@ def create_results_tables(options, results, best_results, group_name,
 
             with open(html_output_file, "w", encoding="utf-8") as f:
                 f.write(
-                    template.render(css_style_sheet=style_css,
-                                    custom_style=custom_style,
-                                    table_style=table_css,
-                                    maths_style=maths_style,
+                    template.render(css_style_sheet=css['main'],
+                                    custom_style=css['custom'],
+                                    table_style=css['table'],
+                                    maths_style=css['math'],
                                     table_description=description[suffix],
                                     table_format=table_format,
                                     result_name=table_title,
diff --git a/fitbenchmarking/utils/create_dirs.py b/fitbenchmarking/utils/create_dirs.py
index 5fa216bd..bccb0cee 100644
--- a/fitbenchmarking/utils/create_dirs.py
+++ b/fitbenchmarking/utils/create_dirs.py
@@ -85,6 +85,22 @@ def figures(support_pages_dir):
 
     return figures_dir
 
+def css(results_dir):
+    """
+    Creates a local css directory inside the results directory.
+
+    :param support_pages_dir: path to the results directory
+    :type support_pages_dir: str
+
+    :return: path to the local css directory
+    :rtype: str
+    """
+    local_css_dir = os.path.join(results_dir, "css")
+    if not os.path.exists(local_css_dir):
+        os.makedirs(local_css_dir)
+
+    return local_css_dir
+
 
 def del_contents_of_dir(directory):
     """
diff --git a/fitbenchmarking/utils/fitbm_result.py b/fitbenchmarking/utils/fitbm_result.py
index e98dfacc..57f74d6a 100644
--- a/fitbenchmarking/utils/fitbm_result.py
+++ b/fitbenchmarking/utils/fitbm_result.py
@@ -147,13 +147,27 @@ class FittingResult:
     @property
     def sanitised_name(self):
         """
-        Sanitise the problem name ito one which can be used as a filename.
+        Sanitise the problem name into one which can be used as a filename.
 
         :return: sanitised name
         :rtype: str
         """
         return self.name.replace(',', '').replace(' ', '_')
+    
+    @property
+    def sanitised_min_name(self):
+        """
+        Sanitise the minimizer name into one which can be used as a filename.
+
+        :return: sanitised name
+        :rtype: str
+        """
+        return self.minimizer.replace(':', '').replace(' ', '_')
 
     @sanitised_name.setter
     def sanitised_name(self, value):
         raise RuntimeError('sanitised_name can not be edited')
+    
+    @sanitised_min_name.setter
+    def sanitised_min_name(self, value):
+        raise RuntimeError('sanitised_min_name can not be edited')
diff --git a/fitbenchmarking/utils/misc.py b/fitbenchmarking/utils/misc.py
index 94e03f3e..eadbd429 100644
--- a/fitbenchmarking/utils/misc.py
+++ b/fitbenchmarking/utils/misc.py
@@ -39,3 +39,24 @@ def get_problem_files(data_dir):
         LOGGER.debug(problem)
 
     return problems
+
+def get_css(options,working_directory):
+    """
+    Returns the path of the local css folder
+    
+    :param working_directory: location of current directory
+    :type working_directory: string
+    
+    :return: A dictionary containing relative links to the local css directory
+    :rtype: dict of strings
+    """
+    local_css_dir = os.path.join(options.results_dir,"css")
+    css_path = os.path.relpath(local_css_dir,working_directory)
+    css_dict = {
+        'main'   : os.path.join(css_path,'main_style.css'),
+        'table'  : os.path.join(css_path,'table_style.css'),
+        'math'   : os.path.join(css_path,'math_style.css'),
+        'custom' : os.path.join(css_path,'custom_style.css')
+    }
+    
+    return css_dict


Test patch:
diff --git a/fitbenchmarking/core/tests/test_results_output.py b/fitbenchmarking/core/tests/test_results_output.py
index 303da671..5e228cd8 100644
--- a/fitbenchmarking/core/tests/test_results_output.py
+++ b/fitbenchmarking/core/tests/test_results_output.py
@@ -179,19 +179,20 @@ class CreateDirectoriesTests(unittest.TestCase):
         expected_support_dir = os.path.join(expected_group_dir,
                                             'support_pages')
         expected_figures_dir = os.path.join(expected_support_dir, 'figures')
+        expected_css_dir = os.path.join(expected_results_dir,'css')
 
-        results_dir, group_dir, support_dir, figures_dir = \
+        group_dir, support_dir, figures_dir, css_dir = \
             create_directories(self.options, group_name)
 
-        assert results_dir == expected_results_dir
         assert group_dir == expected_group_dir
         assert support_dir == expected_support_dir
         assert figures_dir == expected_figures_dir
-
-        assert os.path.isdir(results_dir)
+        assert css_dir == expected_css_dir
+        
         assert os.path.isdir(group_dir)
         assert os.path.isdir(support_dir)
         assert os.path.isdir(figures_dir)
+        assert os.path.isdir(css_dir)
 
 
 class PreproccessDataTests(unittest.TestCase):
diff --git a/fitbenchmarking/results_processing/tests/test_plots.py b/fitbenchmarking/results_processing/tests/test_plots.py
index 4849ce1a..e25e019b 100644
--- a/fitbenchmarking/results_processing/tests/test_plots.py
+++ b/fitbenchmarking/results_processing/tests/test_plots.py
@@ -98,9 +98,9 @@ class PlotTests(unittest.TestCase):
         """
         Test that best plot creates a file.
         """
-        file_name = self.plot.plot_best('best', [0.1, 3])
+        file_name = self.plot.plot_best('fit', [0.1, 3])
 
-        self.assertEqual(file_name, 'best_fit_for_full_name.png')
+        self.assertEqual(file_name, 'fit_fit_for_full_name.png')
         path = os.path.join(self.dir.name, file_name)
         self.assertTrue(os.path.exists(path))
 
diff --git a/fitbenchmarking/results_processing/tests/test_support_page.py b/fitbenchmarking/results_processing/tests/test_support_page.py
index 753550d3..251ced5b 100644
--- a/fitbenchmarking/results_processing/tests/test_support_page.py
+++ b/fitbenchmarking/results_processing/tests/test_support_page.py
@@ -103,10 +103,11 @@ class CreateProbGroupTests(unittest.TestCase):
                                        support_pages_dir=self.dir.name,
                                        options=self.options)
         file_names = [r.support_page_link for r in self.results]
-        expected = [os.path.join(self.dir.name, f)
+        expected = [os.path.join(os.path.relpath(self.dir.name), f)
                     for f in ['test_group_prob_a_min_a.html',
                               'test_group_prob_a_min_b.html',
                               'test_group_prob_a_min_c.html']]
+        
         self.assertListEqual(file_names, expected)
 
 
diff --git a/fitbenchmarking/utils/tests/test_create_dirs.py b/fitbenchmarking/utils/tests/test_create_dirs.py
index bb815dde..89791bb4 100644
--- a/fitbenchmarking/utils/tests/test_create_dirs.py
+++ b/fitbenchmarking/utils/tests/test_create_dirs.py
@@ -6,7 +6,7 @@ import shutil
 import unittest
 
 from fitbenchmarking.utils.create_dirs import (figures, group_results, results,
-                                               support_pages)
+                                               support_pages, css)
 
 
 class CreateDirsTests(unittest.TestCase):
@@ -72,6 +72,19 @@ class CreateDirsTests(unittest.TestCase):
 
         shutil.rmtree(results_dir)
 
+    def test_css_create_correct_dir(self):
+
+        results_dir = results(self.results_dir)
+        group_results_dir = group_results(results_dir, "test_group")
+        css_dir = css(group_results_dir)
+        css_dir_expected = os.path.join(group_results_dir,
+                                    'css')
+        
+        self.assertEqual(css_dir_expected, css_dir)
+        self.assertTrue(os.path.exists(css_dir_expected))
+
+        shutil.rmtree(css_dir)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/fitbenchmarking/utils/tests/test_fitbm_result.py b/fitbenchmarking/utils/tests/test_fitbm_result.py
index c5a18ddb..23030b50 100644
--- a/fitbenchmarking/utils/tests/test_fitbm_result.py
+++ b/fitbenchmarking/utils/tests/test_fitbm_result.py
@@ -113,6 +113,13 @@ class FitbmResultTests(unittest.TestCase):
         self.result.name = 'test, name with commas'
         self.assertEqual(self.result.sanitised_name, 'test_name_with_commas')
 
+    def test_sanitised_min_name(self):
+        """
+        Test that sanitised minimizer names are correct.
+        """
+        self.result.minimizer = 'test: name with colon'
+        self.assertEqual(self.result.sanitised_min_name, 'test_name_with_colon')
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/fitbenchmarking/utils/tests/test_misc.py b/fitbenchmarking/utils/tests/test_misc.py
index cdd751a4..1757fc72 100644
--- a/fitbenchmarking/utils/tests/test_misc.py
+++ b/fitbenchmarking/utils/tests/test_misc.py
@@ -7,7 +7,8 @@ import unittest
 
 from fitbenchmarking import mock_problems
 from fitbenchmarking.utils.misc import get_problem_files
-
+from fitbenchmarking.utils.misc import get_css
+from fitbenchmarking.utils.options import Options
 
 class CreateDirsTests(unittest.TestCase):
 
@@ -55,6 +56,23 @@ class CreateDirsTests(unittest.TestCase):
         self.assertIsInstance(problems, list)
         self.assertEqual(self.expected, sorted(problems))
 
+    def test_get_css(self):
+        
+        options = Options()
+        print(options.results_dir)
+        test_dir = os.path.join(options.results_dir,"foo")
+        
+        expected_css_dir = os.path.join("..","css")
+        expected_main_css = os.path.join(expected_css_dir,"main_style.css")
+        expected_table_css = os.path.join(expected_css_dir,"table_style.css")
+        expected_math_css = os.path.join(expected_css_dir,"math_style.css")
+        expected_custom_css = os.path.join(expected_css_dir,"custom_style.css")
+        css = get_css(options,test_dir)
+
+        self.assertEqual(css['main'], expected_main_css)
+        self.assertEqual(css['table'], expected_table_css)
+        self.assertEqual(css['math'], expected_math_css)
+        self.assertEqual(css['custom'], expected_custom_css)
 
 if __name__ == "__main__":
     unittest.main()