From 42cee7ece168c740fde961b05fa47eef66c4b262 Mon Sep 17 00:00:00 2001 From: open-mss-build <77272604+open-mss-build@users.noreply.github.com> Date: Fri, 28 May 2021 12:48:58 +0200 Subject: [PATCH 01/37] updated documentation (#1008) * updated documentation * improved install/update procedure Co-authored-by: Reimar Bauer --- AUTHORS | 4 +- docs/installation.rst | 35 +++++++------ docs/mscolab.rst | 50 ++++++++++++------- .../config/mss/mss_settings.json.sample | 25 ++++++++-- docs/usage.rst | 7 +-- 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/AUTHORS b/AUTHORS index 566c48461..8d0b883a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,8 @@ Contributors ============ +in alphabetic order by first name + - Andreas Hilboll - Anveshan Lal @@ -10,10 +12,10 @@ Contributors - Jens-Uwe Grooß - Jörn Ungermann - Marc Rautenhaus +- May Bär - Reimar Bauer - Sakshi Chopkar - Shivashis Padhi - Tanish Grover - Thomas Breuer - Vaibhav Mehra -- May Bär diff --git a/docs/installation.rst b/docs/installation.rst index 6b98af578..4bae826d1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -70,7 +70,9 @@ variables. :: update ++++++ For updating an existing MSS installation to the current version, it is best to install -it into a new environment. +it into a new environment. If your current version is not far behind the new version +you could try the mamba update mss as described. + .. Important:: mamba is under development. All dependencies of MSS and MSS itselfs are under development. @@ -80,8 +82,12 @@ search for MSS what you can get :: (mssenv) $ mamba search mss - mss 3.0.3 py39hf3d152e_0 conda-forge - mss 3.0.3 py39hf3d152e_1 conda-forge + mss 3.0.4 py38h578d9bd_0 conda-forge + mss 3.0.4 py39hf3d152e_0 conda-forge + mss 4.0.0 py36h5fab9bb_0 conda-forge + mss 4.0.0 py37h89c1867_0 conda-forge + mss 4.0.0 py38h578d9bd_0 conda-forge + mss 4.0.0 py39hf3d152e_0 conda-forge compare what you have installed :: @@ -89,25 +95,22 @@ compare what you have installed :: mss 3.0.2 py39hf3d152e_0 conda-forge -If an existing environment shall be updated, it is important to update all packages in this environment. :: +We found that sometimes mss can be updated in an existing environment :: + + (mssenv) $ mamba update mss + +We have also reports that sometimes an update suceeds by giving by the install option and the new version number, +in this example 4.0.0 and python as second option :: - $ conda activate mssenv - (mssenv) $ mamba update --all + (mssenv) $ mamba install mss=4.0.0 python -In this example there was a further build done after the first release of 3.0.3. -Compare in the list of proposed updates what you would get :: +All attemmpts show what you get if you continue. **Continue only if you get what you want.** - matplotlib 3.4.2 py39hf3d152e_0 conda-forge - matplotlib-base 3.4.2 py39h2fa2bec_0 conda-forge - mss 3.0.3 py39hf3d152e_0 conda-forge - multidict 5.1.0 py39h3811e60_1 conda-forge +The alternative is to use a new environment and install mss. -If you see a mismatch like this, not getting the recent buildnumber. In this example value in -third column ends with "_1" you have to force the update by the conda command:: - (mssenv) $ conda install mss==3.0.3=py39hf3d152e_1 -For further details :ref:`mss-configuration` +For further details of configurating mss :ref:`mss-configuration` diff --git a/docs/mscolab.rst b/docs/mscolab.rst index a67bd2a48..945329b71 100644 --- a/docs/mscolab.rst +++ b/docs/mscolab.rst @@ -121,39 +121,48 @@ You can turn the `Work Locally` toggle off at any points and work on the common Notes for server administrators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're configuring mscolab server, there isn't a GUI to add or manage a group of users. There is however a -proposal to bring this on around the next release of mss. For now, there is a command line tool available with the -installation of mss, :code:`mscolab_add_permissions`. It's usage is as follows +If you're configuring mscolab server, there is a manage users GUI to add or manage users to a project. +There is a command line tool available with the installation of mss, :code:`mscolab`. It can import users to the database +and can handle joins to projects. -- Make a text file with the following format +Make a text file with the following format to import many users to the mscolab database .. code-block:: text - path1 - u1-c - u2-c - u3-a + suggested_username name + suggested_username2 name2 - path2 - u1-a + .. code-block:: text - path3 - u2-v + $ mscolab db --users_by_file /path/to/file -- `path1` represents the path of project in mscolab db. -- u1, u2, u3 are usernames. -- `c` stands for collaborator, `a` for admin, `v` for viewer. -- Different paths are separated by 2 '\n's. -- The tool can be invocated anywhere by a command, where :code:`/path/to/file` represents the path to file created above. +After executed you get informations to exchange with users. .. code-block:: text - $ mscolab_add_permissions /path/to/file + Are you sure you want to add users to the database? (y/[n]): + y + Userdata: email suggested_username 30736d0350c9b886 + + "MSCOLAB_mailid": "email", + "MSCOLAB_password": "30736d0350c9b886", + + + Userdata: email2 suggested_username2 342434de34904303 + + "MSCOLAB_mailid": "email2", + "MSCOLAB_password": "342434de34904303", -instructions to use mscolab wsgi +Further options can be listed by `mscolab db -h` + + +Instructions to use mscolab wsgi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ make a file called :code:`server.py` +and install :: + + mamba install eventlet==0.30.2 gunicorn **server.py**:: @@ -163,3 +172,6 @@ Then run the following commands. :: $ mamba install gunicorn eventlet $ gunicorn -b 0.0.0.0:8087 server:app + + +For further options read ``_ \ No newline at end of file diff --git a/docs/samples/config/mss/mss_settings.json.sample b/docs/samples/config/mss/mss_settings.json.sample index 4944e9c88..a34f573e6 100644 --- a/docs/samples/config/mss/mss_settings.json.sample +++ b/docs/samples/config/mss/mss_settings.json.sample @@ -1,6 +1,21 @@ { "data_dir": "~/mssdata", + "filepicker_default": "default", + + "import_plugins": { + "CSV": ["csv", "mslib.plugins.io.csv", "load_from_csv"], + "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"], + "Text": ["txt", "mslib.plugins.io.text", "load_from_txt"] + }, + + "export_plugins": { + "CSV": ["csv", "mslib.plugins.io.csv", "save_to_csv"], + "Text": ["txt", "mslib.plugins.io.text", "save_to_txt"], + "KML": ["kml", "mslib.plugins.io.kml", "save_to_kml"], + "GPX": ["gpx", "mslib.plugins.io.gpx", "save_to_gpx"] + }, + "layout": { "topview": [963, 702], @@ -53,10 +68,15 @@ "new_flighttrack_template": ["Kiruna", "Ny-Alesund"], "new_flighttrack_flightlevel": 250, + "num_interpolation_points": 201, + "num_labels": 10, + "WMS_request_timeout": 30, "default_WMS": ["http://www.your-server.de/forecasts"], "default_VSEC_WMS": ["http://www.your-server.de/forecasts"], + "default_LSEC_WMS": ["http://www.your-server.de/forecasts"], + "default_MSCOLAB": ["http://www.your-mscolab-server.de/"], "WMS_login": { "http://www.your-server.de/forecasts" : ["youruser", "yourpassword"] @@ -64,8 +84,7 @@ "MSC_login": { "http://www.your-mscolab-server.de" : ["youruser", "yourpassword"] }, - "num_interpolation_points": 201, - "num_labels": 10, - "filepicker_default": "default" + "MSCOLAB_mailid": "", + "MSCOLAB_password": "" } diff --git a/docs/usage.rst b/docs/usage.rst index 777badfb3..0b61ec13d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -92,14 +92,15 @@ importing changed files back in addition to the main FTML format. These filters from the File menu of the Main Window. MSS currently offers several import/export filters in the mslib.plugins.io module, which may serve -as an example for the definition of own plugins. They are listed below. The CSV plugin is enabled -by default. Enabling the experimental FliteStar text import plugin would require those lines in +as an example for the definition of own plugins. Take care that added plugins use different file extensions. +They are listed below. The CSV plugin is enabled by default. +Enabling the experimental FliteStar text import plugin would require those lines in the UI settings file: .. code:: text "import_plugins": { - "FliteStar": ["txt", "mslib.plugins.io.flitestar", "load_from_flitestar"] + "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"] }, The dictionary entry defines the name of the filter in the File menu. The list specifies in this From d058237ad27be9093eef17691bc67a9b49c8d4ea Mon Sep 17 00:00:00 2001 From: Jatin Jain <72596619+Jatin2020-24@users.noreply.github.com> Date: Sat, 29 May 2021 15:31:26 +0530 Subject: [PATCH 02/37] Fixed: #999 (#1010) * .format replaced with f" string * updated copyright year --- conftest.py | 2 +- docs/samples/config/mscolab/mscolab_settings.py.sample | 1 + docs/samples/config/wms/mss_chem_plots.py | 8 ++++---- docs/samples/config/wms/mss_wms_auth.py.sample | 2 +- docs/samples/config/wms/mss_wms_settings.py.chem_plots | 2 +- mslib/_tests/constants.py | 2 +- mslib/mscolab/_tests/test_chat_manager.py | 1 + mslib/mscolab/_tests/test_file_manager.py | 1 + mslib/mscolab/_tests/test_server.py | 1 + mslib/msui/hexagon_dockwidget.py | 2 +- mslib/msui/mscolab_admin_window.py | 1 + mslib/msui/mscolab_version_history.py | 1 + mslib/msui/mss_pyui.py | 2 +- mslib/msui/multilayers.py | 6 +++--- mslib/mswms/demodata.py | 4 ++-- mslib/mswms/mpl_hsec.py | 2 +- 16 files changed, 22 insertions(+), 16 deletions(-) diff --git a/conftest.py b/conftest.py index 0944e8439..35b345dc9 100644 --- a/conftest.py +++ b/conftest.py @@ -78,7 +78,7 @@ def pytest_generate_tests(metafunc): This file is part of mss. :copyright: Copyright 2019 Shivashis Padhi - :copyright: Copyright 2019-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2019-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/mscolab/mscolab_settings.py.sample b/docs/samples/config/mscolab/mscolab_settings.py.sample index 824e2bdd2..3b80fa59b 100644 --- a/docs/samples/config/mscolab/mscolab_settings.py.sample +++ b/docs/samples/config/mscolab/mscolab_settings.py.sample @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2019 Shivashis Padhi + :copyright: Copyright 2019-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/wms/mss_chem_plots.py b/docs/samples/config/wms/mss_chem_plots.py index 80e2133b2..9f7ad0b59 100644 --- a/docs/samples/config/wms/mss_chem_plots.py +++ b/docs/samples/config/wms/mss_chem_plots.py @@ -139,14 +139,14 @@ def make_msschem_hs_class( _contourname = "_pcontours" class fnord(HS_MSSChemStyle): - name = "HS_{}_{}{}".format(entity, vert, _contourname) + name = f"HS_{entity}_{vert}{_contourname}" dataname = entity units = units unit_scale = scale _title_tpl = nam + " (" + vert + ")" long_name = entity if units: - _title_tpl += " ({})".format(units) + _title_tpl += f"({units})" required_datafields = [(vert, entity, None)] + add_data contours = add_contours @@ -313,14 +313,14 @@ def make_msschem_vs_class( add_contours = [] class fnord(VS_MSSChemStyle): - name = "VS_{}_{}".format(entity, vert) + name = f"VS_{entity}_{vert} " dataname = entity units = units unit_scale = scale title = nam + " (" + vert + ")" long_name = entity if units: - title += " ({})".format(units) + title += f"({units})" required_datafields = [(vert, entity, None)] + add_data contours = add_contours if add_contours else [] diff --git a/docs/samples/config/wms/mss_wms_auth.py.sample b/docs/samples/config/wms/mss_wms_auth.py.sample index 042b5a3c2..a3e2dcf09 100644 --- a/docs/samples/config/wms/mss_wms_auth.py.sample +++ b/docs/samples/config/wms/mss_wms_auth.py.sample @@ -10,7 +10,7 @@ :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus - :copyright: Copyright 2016-2017 by the mss team, see AUTHORS. + :copyright: Copyright 2016-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/wms/mss_wms_settings.py.chem_plots b/docs/samples/config/wms/mss_wms_settings.py.chem_plots index 3d4e83871..8a8471f22 100644 --- a/docs/samples/config/wms/mss_wms_settings.py.chem_plots +++ b/docs/samples/config/wms/mss_wms_settings.py.chem_plots @@ -11,7 +11,7 @@ :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2019 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/_tests/constants.py b/mslib/_tests/constants.py index a54aa4ba2..2bac03775 100644 --- a/mslib/_tests/constants.py +++ b/mslib/_tests/constants.py @@ -40,7 +40,7 @@ CACHED_CONFIG_FILE = None SERVER_CONFIG_FILE = "mss_wms_settings.py" MSCOLAB_CONFIG_FILE = "mscolab_settings.py" -ROOT_FS = TempFS(identifier="mss{}".format(SHA)) +ROOT_FS = TempFS(identifier=f"mss{SHA}") OSFS_URL = ROOT_FS.geturl("", purpose="fs") ROOT_DIR = ROOT_FS.getsyspath("") diff --git a/mslib/mscolab/_tests/test_chat_manager.py b/mslib/mscolab/_tests/test_chat_manager.py index aa8cef5e7..8cae4485a 100644 --- a/mslib/mscolab/_tests/test_chat_manager.py +++ b/mslib/mscolab/_tests/test_chat_manager.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mscolab/_tests/test_file_manager.py b/mslib/mscolab/_tests/test_file_manager.py index bb863a0e0..25feaa735 100644 --- a/mslib/mscolab/_tests/test_file_manager.py +++ b/mslib/mscolab/_tests/test_file_manager.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mscolab/_tests/test_server.py b/mslib/mscolab/_tests/test_server.py index 19b7904d4..06284b691 100644 --- a/mslib/mscolab/_tests/test_server.py +++ b/mslib/mscolab/_tests/test_server.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/hexagon_dockwidget.py b/mslib/msui/hexagon_dockwidget.py index 89a04f225..e61895a12 100644 --- a/mslib/msui/hexagon_dockwidget.py +++ b/mslib/msui/hexagon_dockwidget.py @@ -140,7 +140,7 @@ def _remove_hexagon(self): row_max = row + (7 - idx) if row_min < 0 or row_max > len(waypoints_model.all_waypoint_data()): raise HexagonException("Cannot remove hexagon, hexagon is not complete " - "(min, max = {:d}, {:d})".format(row_min, row_max)) + f"min, max = {row_min:d}, {row_max:d}") else: found_one = False for i in range(0, row_max - row_min): diff --git a/mslib/msui/mscolab_admin_window.py b/mslib/msui/mscolab_admin_window.py index 4031cf1de..f8c70fc18 100644 --- a/mslib/msui/mscolab_admin_window.py +++ b/mslib/msui/mscolab_admin_window.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: 2020 Tanish Grover + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/mscolab_version_history.py b/mslib/msui/mscolab_version_history.py index a00607570..af1ef98e0 100644 --- a/mslib/msui/mscolab_version_history.py +++ b/mslib/msui/mscolab_version_history.py @@ -10,6 +10,7 @@ This file is part of mss. :copyright: 2020 Tanish Grover + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/mss_pyui.py b/mslib/msui/mss_pyui.py index 586c34588..b47edf1ce 100644 --- a/mslib/msui/mss_pyui.py +++ b/mslib/msui/mss_pyui.py @@ -197,7 +197,7 @@ def __init__(self, parent=None): """ super(MSS_AboutDialog, self).__init__(parent) self.setupUi(self) - self.lblVersion.setText("Version: {}".format(__version__)) + self.lblVersion.setText(f"Version: {__version__}") self.milestone_url = f'https://github.com/Open-MSS/MSS/issues?q=is%3Aclosed+milestone%3A{__version__[:-1]}' self.lblChanges.setText(f'New Features and Changes') blub = QtGui.QPixmap(python_powered()) diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index 2e0a48695..3709bf01a 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -562,7 +562,7 @@ def _parse_levels(self): if "elevation" in self.extents: units = self.dimensions["elevation"]["units"] values = self.extents["elevation"]["values"] - self.levels = ["{} ({})".format(e.strip(), units) for e in values] + self.levels = [f"{e.strip()} ({units})" for e in values] self.level = self.levels[0] def _parse_itimes(self): @@ -583,7 +583,7 @@ def _parse_itimes(self): logging.error(msg) QtWidgets.QMessageBox.critical( self.parent.dock_widget, self.parent.dock_widget.tr("Web Map Service"), - self.parent.dock_widget.tr("ERROR: {}".format(msg))) + self.parent.dock_widget.tr(f"ERROR: {msg}")) else: self.itime = self.itimes[-1] @@ -605,7 +605,7 @@ def _parse_vtimes(self): logging.error(msg) QtWidgets.QMessageBox.critical( self.parent.dock_widget, self.parent.dock_widget.tr("Web Map Service"), - self.parent.dock_widget.tr("ERROR: {}".format(msg))) + self.parent.dock_widget.tr(f"ERROR: {msg}")) else: if self.itime: self.vtime = next((vtime for vtime in self.vtimes if vtime >= self.itime), self.vtimes[0]) diff --git a/mslib/mswms/demodata.py b/mslib/mswms/demodata.py index a43f0bae8..19c2dd025 100644 --- a/mslib/mswms/demodata.py +++ b/mslib/mswms/demodata.py @@ -864,7 +864,7 @@ def create_server_config(self, detailed_information=False): :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); @@ -904,7 +904,7 @@ def create_server_config(self, detailed_information=False): :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mswms/mpl_hsec.py b/mslib/mswms/mpl_hsec.py index b34de9461..3e84727ef 100644 --- a/mslib/mswms/mpl_hsec.py +++ b/mslib/mswms/mpl_hsec.py @@ -104,7 +104,7 @@ def supported_crs(self): "EPSG:4326", # WGS 84 / cylindric "MSS:stere"]) for code in self.supported_epsg_codes(): - crs_list.add("EPSG:{:d}".format(code)) + crs_list.add(f"EPSG:{code:d}") return sorted(crs_list) def _draw_auto_graticule(self, bm): From a7d79a1cef48b82eb14f0a6a326f35441348d499 Mon Sep 17 00:00:00 2001 From: Aryan Gupta <42470695+withoutwaxaryan@users.noreply.github.com> Date: Sat, 5 Jun 2021 14:06:29 +0530 Subject: [PATCH 03/37] fixes #1014 (#1015) --- docs/help.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/help.rst b/docs/help.rst index 239ca9ed4..123095351 100644 --- a/docs/help.rst +++ b/docs/help.rst @@ -9,12 +9,12 @@ MSCOLAB .. raw:: html - + KML Docking Widget ------------------ .. raw:: html - + From 5ff94e91bebb9400444b710d053c3cd36982504b Mon Sep 17 00:00:00 2001 From: May Date: Tue, 8 Jun 2021 15:55:40 +0200 Subject: [PATCH 04/37] Remove Qt imports (#1019) --- mslib/msui/_tests/test_mscolab_project.py | 4 ++-- mslib/msui/mscolab_project.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mslib/msui/_tests/test_mscolab_project.py b/mslib/msui/_tests/test_mscolab_project.py index 79196633a..306b3848a 100644 --- a/mslib/msui/_tests/test_mscolab_project.py +++ b/mslib/msui/_tests/test_mscolab_project.py @@ -31,7 +31,7 @@ from mslib.msui.mscolab import MSSMscolabWindow from mslib.mscolab.conf import mscolab_settings from mslib.mscolab.models import Message -from PyQt5 import QtCore, QtTest, QtWidgets, Qt +from PyQt5 import QtCore, QtTest, QtWidgets from mslib._tests.utils import mscolab_start_server @@ -103,7 +103,7 @@ def test_copy_message(self): self._send_message("**test message**") self._send_message("**test message**") self._activate_context_menu_action(Actions.COPY) - assert Qt.QApplication.clipboard().text() == "**test message**" + assert QtWidgets.QApplication.clipboard().text() == "**test message**" def test_reply_message(self): self._send_message("**test message**") diff --git a/mslib/msui/mscolab_project.py b/mslib/msui/mscolab_project.py index 746df309d..8f81d66c2 100644 --- a/mslib/msui/mscolab_project.py +++ b/mslib/msui/mscolab_project.py @@ -33,7 +33,7 @@ from werkzeug.urls import url_join from mslib.mscolab.models import MessageType -from PyQt5 import Qt, QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from mslib.msui.mss_qt import get_open_filename, get_save_filename from mslib.msui.qt5 import ui_mscolab_project_window as ui from mslib.utils import config_loader, show_popup @@ -293,7 +293,7 @@ def start_message_edit(self, message_text, message_id): self.active_edit_id = message_id self.messageText.setText(message_text) self.messageText.setFocus() - self.messageText.moveCursor(Qt.QTextCursor.End) + self.messageText.moveCursor(QtGui.QTextCursor.End) self.editMessageBtn.setVisible(True) self.cancelBtn.setVisible(True) self.sendMessageBtn.setVisible(False) @@ -621,7 +621,7 @@ def open_context_menu(self, pos): self.context_menu.exec_(self.messageBox.mapToGlobal(pos)) def handle_copy_action(self): - Qt.QApplication.clipboard().setText(self.message_text) + QtWidgets.QApplication.clipboard().setText(self.message_text) def handle_download_action(self): file_name = fs.path.basename(self.attachment_path) @@ -668,7 +668,7 @@ def set_selected(self, selected): def on_link_click(self, url): if url.scheme() == "": url.setScheme("http") - Qt.QDesktopServices.openUrl(url) + QtGui.QDesktopServices.openUrl(url) # Deregister all the syntax that we don't want to allow From 6cdc65b1c7967f5a0b2d51033b1d7b01b1a88437 Mon Sep 17 00:00:00 2001 From: Aryan Gupta <42470695+withoutwaxaryan@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:16:33 +0530 Subject: [PATCH 05/37] Fixes #1014 changing http to https (#1017) Co-authored-by: ReimarBauer --- docs/help.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/help.rst b/docs/help.rst index 123095351..5965a1e6f 100644 --- a/docs/help.rst +++ b/docs/help.rst @@ -9,12 +9,12 @@ MSCOLAB .. raw:: html - + KML Docking Widget ------------------ .. raw:: html - + From 92cbd445f282904117d830521cd47909b556aa19 Mon Sep 17 00:00:00 2001 From: Aravind Murali Date: Wed, 9 Jun 2021 17:50:59 +0530 Subject: [PATCH 06/37] Linear view mscolab bug (#1021) * fixed tableview not opening bug; added test * added raising of mscolab window after closing view --- mslib/msui/_tests/test_mscolab.py | 4 ++++ mslib/msui/mscolab.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mslib/msui/_tests/test_mscolab.py b/mslib/msui/_tests/test_mscolab.py index 0777269b0..39a15257a 100644 --- a/mslib/msui/_tests/test_mscolab.py +++ b/mslib/msui/_tests/test_mscolab.py @@ -105,6 +105,7 @@ def test_view_open(self): QtTest.QTest.mouseClick(self.window.topview, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(self.window.sideview, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(self.window.tableview, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.window.linearview, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.active_windows) == 0 # test after activating project @@ -118,6 +119,9 @@ def test_view_open(self): QtTest.QTest.mouseClick(self.window.sideview, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.active_windows) == 3 + QtTest.QTest.mouseClick(self.window.linearview, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + assert len(self.window.active_windows) == 4 @mock.patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName", return_value=(fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, 'test_export.ftml'), None)) diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index a816e0a01..d50799e09 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -966,7 +966,7 @@ def create_view_window(self, _type): view_window = linearview.MSSLinearViewWindow(model=self.waypoints_model, parent=self.listProjects, _id=self.id_count) - view_window.view_type = "tableview" + view_window.view_type = "linearview" if self.access_level == "viewer": self.disable_navbar_action_buttons(_type, view_window) @@ -1271,6 +1271,7 @@ def handle_view_close(self, value): for index, window in enumerate(self.active_windows): if window._id == value: del self.active_windows[index] + self.raise_() def setIdentifier(self, identifier): self.identifier = identifier From 5bcb1ea8eada9f288aef4ac6009f0aa6327a7e6b Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Thu, 10 Jun 2021 16:16:43 +0200 Subject: [PATCH 07/37] preparation of v4.0.1 (#1023) * preparation of v4.0.1 * updated install instruction Co-authored-by: J. Ungermann --- CHANGES.rst | 8 ++++++++ docs/development.rst | 2 +- docs/installation.rst | 23 ++++++++--------------- mslib/version.py | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index afc3db2f4..e6964b48d 100755 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changelog ========= +Version 4.0.1 +~~~~~~~~~~~~~ + +Bug Fix release + +All changes: +https://github.com/Open-MSS/MSS/milestone/58?closed=1 + Version 4.0.0 ------------- diff --git a/docs/development.rst b/docs/development.rst index b53392a8b..2b33532f1 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -53,7 +53,7 @@ Create an environment and install the whole mss package dependencies then remove $ conda create -n mssdev mamba $ conda activate mssdev - $ mamba install mss --only-deps + $ mamba install mss=4.0.1 --only-deps You can also use conda to install mss, but mamba is a way faster. Compare versions used in the meta.yaml between stable and develop branch and apply needed changes. diff --git a/docs/installation.rst b/docs/installation.rst index 4bae826d1..4b6bb26ac 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -56,7 +56,7 @@ leave out the 'source' here and below). :: $ conda create -n mssenv mamba $ conda activate mssenv - (mssenv) $ mamba install mss + (mssenv) $ mamba install mss=4.0.1 python You need to reactivate after the installation once the environment to setup all needed enironment @@ -81,13 +81,9 @@ you could try the mamba update mss as described. search for MSS what you can get :: (mssenv) $ mamba search mss - - mss 3.0.4 py38h578d9bd_0 conda-forge - mss 3.0.4 py39hf3d152e_0 conda-forge - mss 4.0.0 py36h5fab9bb_0 conda-forge - mss 4.0.0 py37h89c1867_0 conda-forge - mss 4.0.0 py38h578d9bd_0 conda-forge - mss 4.0.0 py39hf3d152e_0 conda-forge + ... + mss 4.0.1 py38h578d9bd_0 conda-forge + mss 4.0.1 py39hf3d152e_0 conda-forge compare what you have installed :: @@ -95,14 +91,11 @@ compare what you have installed :: mss 3.0.2 py39hf3d152e_0 conda-forge -We found that sometimes mss can be updated in an existing environment :: - - (mssenv) $ mamba update mss -We have also reports that sometimes an update suceeds by giving by the install option and the new version number, -in this example 4.0.0 and python as second option :: +We have reports that often an update suceeds by using the install option and the new version number, +in this example 4.0.1 and python as second option :: - (mssenv) $ mamba install mss=4.0.0 python + (mssenv) $ mamba install mss=4.0.1 python All attemmpts show what you get if you continue. **Continue only if you get what you want.** @@ -130,7 +123,7 @@ We suggest to create a mss user. * login again or export PATH="/home/mss/miniconda3/bin:$PATH" * conda create -n mssenv mamba * conda activate mssenv -* mamba install mss +* mamba install mss=4.0.1 python For a simple test you could start the builtin standalone *mswms* and *mscolab* server:: diff --git a/mslib/version.py b/mslib/version.py index ee3ac7860..a570979c4 100644 --- a/mslib/version.py +++ b/mslib/version.py @@ -24,4 +24,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__ = u'4.0.0.' +__version__ = u'4.0.1.' From 333c37325a3509edc1cd14e0fd19cda977cbc56b Mon Sep 17 00:00:00 2001 From: May Date: Fri, 11 Jun 2021 15:57:04 +0200 Subject: [PATCH 08/37] Allow specification of file for linear defaultstyle (#1028) * Allow specification of file for default * Simplify if condition --- mslib/mswms/_tests/test_mss_plot_driver.py | 5 +++++ mslib/mswms/mpl_lsec_styles.py | 6 ++++-- mslib/mswms/mss_plot_driver.py | 13 ++++++++----- mslib/mswms/wms.py | 8 +++++--- mslib/utils.py | 1 + 5 files changed, 23 insertions(+), 10 deletions(-) diff --git a/mslib/mswms/_tests/test_mss_plot_driver.py b/mslib/mswms/_tests/test_mss_plot_driver.py index fcedde50a..1ebc23c66 100644 --- a/mslib/mswms/_tests/test_mss_plot_driver.py +++ b/mslib/mswms/_tests/test_mss_plot_driver.py @@ -174,6 +174,11 @@ def test_LS_DefaultStyle(self): img = self.plot(mpl_lsec_styles.LS_DefaultStyle(driver=self.lsec, variable=variable)) assert img is not None + def test_LS_DefaultStyle_PL(self): + img = self.plot(mpl_lsec_styles.LS_DefaultStyle(driver=self.lsec, variable="air_potential_temperature", + filetype="pl")) + assert img is not None + def test_LS_RelativeHumdityStyle_01(self): img = self.plot(mpl_lsec_styles.LS_RelativeHumdityStyle_01(driver=self.lsec)) assert img is not None diff --git a/mslib/mswms/mpl_lsec_styles.py b/mslib/mswms/mpl_lsec_styles.py index 94bb50cf2..6f2571c23 100644 --- a/mslib/mswms/mpl_lsec_styles.py +++ b/mslib/mswms/mpl_lsec_styles.py @@ -36,10 +36,12 @@ class LS_DefaultStyle(AbstractLinearSectionStyle): """ Style for single variables that require no further calculation """ - def __init__(self, driver, variable="air_temperature"): + def __init__(self, driver, variable="air_temperature", filetype="ml"): super(AbstractLinearSectionStyle, self).__init__(driver=driver) self.variable = variable - self.required_datafields = [("ml", "air_pressure", "Pa"), ("ml", self.variable, None)] + self.required_datafields = [(filetype, self.variable, None)] + if filetype != "pl": + self.required_datafields.insert(0, (filetype, "air_pressure", "Pa")) abbreviation = "".join([text[0] for text in self.variable.split("_")]) self.name = f"LS_{str.upper(abbreviation)}" self.title = f"{self.variable} Linear Plot" diff --git a/mslib/mswms/mss_plot_driver.py b/mslib/mswms/mss_plot_driver.py index 7b602cc25..956627ceb 100644 --- a/mslib/mswms/mss_plot_driver.py +++ b/mslib/mswms/mss_plot_driver.py @@ -773,10 +773,11 @@ def _load_interpolate_timestep(self): lon_data = lon_data[lon_indices] factors = [] - # Make sure air_pressure is the first to be evaluated + # Make sure air_pressure is the first to be evaluated if needed variables = list(self.data_vars) - if variables[0] != "air_pressure": - variables.insert(0, variables.pop(variables.index("air_pressure"))) + if "air_pressure" in self.data_vars: + if variables[0] != "air_pressure": + variables.insert(0, variables.pop(variables.index("air_pressure"))) for name in variables: var = self.data_vars[name] @@ -796,9 +797,10 @@ def _load_interpolate_timestep(self): cross_section = utils.interpolate_vertsec(var_data, self.lat_data, lon_data, self.lats, self.lons) # Create vertical interpolation factors and indices for subsequent variables # TODO: Improve performance for this interpolation in general - if name == "air_pressure": + if len(factors) == 0: for index_lonlat, alt in enumerate(self.alts): - pressures = cross_section[:, index_lonlat] + pressures = cross_section[:, index_lonlat] if name == "air_pressure" \ + else self.vert_data[::-self.vert_order] * (100 if self.vert_units.lower() == "hpa" else 1) closest = 0 direction = 1 for index_altitude, pressure in enumerate(pressures): @@ -818,6 +820,7 @@ def _load_interpolate_timestep(self): [[closest, 1 - (abs(pressures[closest] - alt) / distance)], [next_closest, 1 - (abs(pressures[next_closest] - alt) / distance)]]) + # Interpolate with the previously calculated pressure indices and factors for index in range(len(self.alts)): cur_factor = factors[index] value = cross_section[cur_factor[0][0], index] * cur_factor[0][1] + \ diff --git a/mslib/mswms/wms.py b/mslib/mswms/wms.py index 04a63a283..abbc1bace 100644 --- a/mslib/mswms/wms.py +++ b/mslib/mswms/wms.py @@ -205,7 +205,9 @@ def __init__(self): mss_wms_settings.register_linear_layers = [] for layer in mss_wms_settings.register_linear_layers: if len(layer) == 3: - self.register_lsec_layer(layer[2], layer[1], layer[0]) + self.register_lsec_layer(layer[2], layer[1], layer_class=layer[0]) + elif len(layer) == 4: + self.register_lsec_layer(layer[3], layer[1], layer[2], layer[0]) else: self.register_lsec_layer(layer[1], layer_class=layer[0]) @@ -262,7 +264,7 @@ def register_vsec_layer(self, datasets, layer_class): raise ValueError("dataset '%s' not available", dataset) self.vsec_layer_registry[dataset][layer.name] = layer - def register_lsec_layer(self, datasets, variable=None, layer_class=None): + def register_lsec_layer(self, datasets, variable=None, filetype="ml", layer_class=None): """ Register linear section layer in internal dict of layers. @@ -274,7 +276,7 @@ def register_lsec_layer(self, datasets, variable=None, layer_class=None): for dataset in datasets: try: if variable: - layer = layer_class(self.lsec_drivers[dataset], variable) + layer = layer_class(self.lsec_drivers[dataset], variable, filetype) else: layer = layer_class(self.lsec_drivers[dataset]) except KeyError as ex: diff --git a/mslib/utils.py b/mslib/utils.py index 5cf682b2e..f80476ddb 100644 --- a/mslib/utils.py +++ b/mslib/utils.py @@ -83,6 +83,7 @@ UR.define("fraction = [] = frac") UR.define("sigma = 1 fraction") +UR.define("level = sigma") UR.define("percent = 1e-2 fraction") UR.define("permille = 1e-3 fraction") UR.define("ppm = 1e-6 fraction") From 849dfe0b7b521fea256b32293e6b4f734ea75ad3 Mon Sep 17 00:00:00 2001 From: May Date: Fri, 11 Jun 2021 20:12:22 +0200 Subject: [PATCH 09/37] Increase coverage (#1020) * Add wms_control tests * Add more mscolab tests * Add mpl_qtwidget coverage * Add mss_pyui coverage * Add transparency, noframe and xml checks * Add more wms tests * Test import with plugins * Add linearview tests * Add more multilayering tests * Revert to old testing.yml * Leave no dangling windows * Remove todo, move ConnectionError to bottom Co-authored-by: ReimarBauer --- mslib/_tests/utils.py | 14 +++ mslib/msui/_tests/test_linearview.py | 38 +++++- mslib/msui/_tests/test_mscolab.py | 140 ++++++++++++++++----- mslib/msui/_tests/test_mss_pyui.py | 77 ++++++++++++ mslib/msui/_tests/test_sideview.py | 13 +- mslib/msui/_tests/test_topview.py | 5 +- mslib/msui/_tests/test_wms_control.py | 103 ++++++++++++++- mslib/msui/mscolab.py | 21 ++-- mslib/msui/multilayers.py | 2 +- mslib/msui/socket_control.py | 3 +- mslib/mswms/_tests/test_mss_plot_driver.py | 131 +++++++++++++++++-- mslib/mswms/_tests/test_wms.py | 42 +++++++ 12 files changed, 532 insertions(+), 57 deletions(-) diff --git a/mslib/_tests/utils.py b/mslib/_tests/utils.py index 2a98b5f67..53d3d4ee8 100644 --- a/mslib/_tests/utils.py +++ b/mslib/_tests/utils.py @@ -228,3 +228,17 @@ def done(*args): pass finally: return finished + + +class ExceptionMock: + """ + Replace function calls with raised exceptions + e.g. + with mock.patch("requests.get", new=ExceptionMock(requests.exceptions.ConnectionError).raise_exc): + self._login() + """ + def __init__(self, exc): + self.exc = exc + + def raise_exc(self, *args, **kwargs): + raise self.exc diff --git a/mslib/msui/_tests/test_linearview.py b/mslib/msui/_tests/test_linearview.py index 347ab7364..0361df6be 100644 --- a/mslib/msui/_tests/test_linearview.py +++ b/mslib/msui/_tests/test_linearview.py @@ -41,6 +41,31 @@ PORTS = list(range(8106, 8107)) +class Test_MSS_LV_Options_Dialog(object): + def setup(self): + self.application = QtWidgets.QApplication(sys.argv) + self.window = tv.MSS_LV_Options_Dialog() + self.window.show() + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWaitForWindowExposed(self.window) + QtWidgets.QApplication.processEvents() + + def teardown(self): + self.window.hide() + QtWidgets.QApplication.processEvents() + self.application.quit() + QtWidgets.QApplication.processEvents() + + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_show(self, mockcrit): + assert mockcrit.critical.call_count == 0 + + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_get(self, mockcrit): + self.window.get_settings() + assert mockcrit.critical.call_count == 0 + + class Test_MSSLinearViewWindow(object): def setup(self): self.application = QtWidgets.QApplication(sys.argv) @@ -73,9 +98,20 @@ def test_mouse_over(self, mockbox): # Test mouse over QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(782, 266), -1) QtWidgets.QApplication.processEvents() - QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(20, 20), -1) + QtTest.QTest.mouseMove(self.window.mpl.canvas, QtCore.QPoint(100, 100), -1) QtWidgets.QApplication.processEvents() + @mock.patch("PyQt5.QtWidgets.QMessageBox") + @mock.patch("mslib.msui.linearview.MSS_LV_Options_Dialog") + def test_options(self, mockdlg, mockbox): + QtTest.QTest.mouseClick(self.window.lvoptionbtn, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + assert mockbox.critical.call_count == 0 + assert mockdlg.call_count == 1 + assert mockdlg.return_value.setModal.call_count == 1 + assert mockdlg.return_value.exec_.call_count == 1 + assert mockdlg.return_value.destroy.call_count == 1 + @pytest.mark.skipif(os.name == "nt", reason="multiprocessing needs currently start_method fork") diff --git a/mslib/msui/_tests/test_mscolab.py b/mslib/msui/_tests/test_mscolab.py index 0777269b0..020503292 100644 --- a/mslib/msui/_tests/test_mscolab.py +++ b/mslib/msui/_tests/test_mscolab.py @@ -27,6 +27,9 @@ import sys import os import fs +import fs.errors +import fs.opener.errors +import requests.exceptions import mock import pytest @@ -35,7 +38,7 @@ from mslib.msui.flighttrack import WaypointsTableModel from mslib.msui.mscolab import MSSMscolabWindow from PyQt5 import QtCore, QtTest, QtWidgets -from mslib._tests.utils import mscolab_start_server +from mslib._tests.utils import mscolab_start_server, ExceptionMock PORTS = list(range(9481, 9530)) @@ -61,6 +64,8 @@ def teardown(self): self.window.version_window.close() if self.window.conn: self.window.conn.disconnect() + self.window.force_close_view_windows() + self.window.close_external_windows() self.application.quit() QtWidgets.QApplication.processEvents() self.process.terminate() @@ -68,8 +73,8 @@ def teardown(self): def test_url_combo(self): assert self.window.url.count() >= 1 - def test_login(self): - pytest.skip("Failing randomly for unknown reasons #870") + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_login(self, mockbox): self._connect_to_mscolab() self._login() # screen shows logout button @@ -85,8 +90,13 @@ def test_login(self): # assert self.window.label.text() == "" assert self.window.conn is None + for exc in [requests.exceptions.ConnectionError, requests.exceptions.InvalidSchema, + requests.exceptions.InvalidURL, requests.exceptions.SSLError, Exception("")]: + with mock.patch("requests.get", new=ExceptionMock(exc).raise_exc): + self.window.connect_handler() + assert mockbox.critical.call_count == 5 + def test_disconnect(self): - pytest.skip("Failing randomly for unknown reasons #870") self._connect_to_mscolab() QtTest.QTest.mouseClick(self.window.toggleConnectionBtn, QtCore.Qt.LeftButton) assert self.window.mscolab_server_url is None @@ -98,13 +108,15 @@ def test_activate_project(self): self._activate_project_at_index(0) assert self.window.active_pid is not None - def test_view_open(self): + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_view_open(self, mockbox): self._connect_to_mscolab() self._login() # test without activating project QtTest.QTest.mouseClick(self.window.topview, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(self.window.sideview, QtCore.Qt.LeftButton) QtTest.QTest.mouseClick(self.window.tableview, QtCore.Qt.LeftButton) + QtTest.QTest.mouseClick(self.window.linearview, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.active_windows) == 0 # test after activating project @@ -118,6 +130,23 @@ def test_view_open(self): QtTest.QTest.mouseClick(self.window.sideview, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() assert len(self.window.active_windows) == 3 + QtTest.QTest.mouseClick(self.window.linearview, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + assert len(self.window.active_windows) == 4 + QtTest.QTest.mouseClick(self.window.topview, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + assert len(self.window.active_windows) == 4 + + project = self.window.active_pid + uid = self.window.user["id"] + topview = self.window.active_windows[1] + tableview = self.window.active_windows[0] + self.window.handle_update_permission(project, uid, "viewer") + assert not tableview.btAddWayPointToFlightTrack.isEnabled() + assert any(action.text() == "Ins WP" and not action.isEnabled() for action in topview.mpl.navbar.actions()) + self.window.handle_update_permission(project, uid, "creator") + assert tableview.btAddWayPointToFlightTrack.isEnabled() + assert any(action.text() == "Ins WP" and action.isEnabled() for action in topview.mpl.navbar.actions()) @mock.patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName", return_value=(fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, 'test_export.ftml'), None)) @@ -133,31 +162,38 @@ def test_handle_export(self, mockbox): for i in range(wp_count): assert exported_waypoints.waypoint_data(i).lat == self.window.waypoints_model.waypoint_data(i).lat - @mock.patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName", - return_value=(fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, 'test_import.ftml'), None)) - @mock.patch("PyQt5.QtWidgets.QFileDialog.getOpenFileName", - return_value=(fs.path.join(mscolab_settings.MSCOLAB_DATA_DIR, 'test_import.ftml'), None)) + @pytest.mark.parametrize("ext", [".ftml", ".txt"]) @mock.patch("PyQt5.QtWidgets.QMessageBox") - def test_import_file(self, mockExport, mockImport, mockMessage): - self._connect_to_mscolab() - self._login() - self._activate_project_at_index(0) - exported_wp = WaypointsTableModel(waypoints=self.window.waypoints_model.waypoints) - QtTest.QTest.mouseClick(self.window.exportBtn, QtCore.Qt.LeftButton) - QtWidgets.QApplication.processEvents() - self.window.waypoints_model.invert_direction() - QtWidgets.QApplication.processEvents() - QtTest.QTest.qWait(100) - assert exported_wp.waypoint_data(0).lat != self.window.waypoints_model.waypoint_data(0).lat - QtTest.QTest.mouseClick(self.window.importBtn, QtCore.Qt.LeftButton) - QtWidgets.QApplication.processEvents() - QtTest.QTest.qWait(100) - assert len(self.window.waypoints_model.waypoints) == 2 - imported_wp = self.window.waypoints_model - wp_count = len(imported_wp.waypoints) - assert wp_count == 2 - for i in range(wp_count): - assert exported_wp.waypoint_data(i).lat == imported_wp.waypoint_data(i).lat + def test_import_file(self, mockbox, ext): + with mock.patch("mslib.msui.mscolab.config_loader", + return_value={"Text": ["txt", "mslib.plugins.io.text", "save_to_txt"]}): + self.window.export_plugins = self.window.add_plugins() + with mock.patch("mslib.msui.mscolab.config_loader", + return_value={"Text": ["txt", "mslib.plugins.io.text", "load_from_txt"]}): + self.window.import_plugins = self.window.add_plugins() + with mock.patch("PyQt5.QtWidgets.QFileDialog.getSaveFileName", return_value=(fs.path.join( + mscolab_settings.MSCOLAB_DATA_DIR, f'test_import{ext}'), None)): + with mock.patch("PyQt5.QtWidgets.QFileDialog.getOpenFileName", return_value=(fs.path.join( + mscolab_settings.MSCOLAB_DATA_DIR, f'test_import{ext}'), None)): + self._connect_to_mscolab() + self._login() + self._activate_project_at_index(0) + exported_wp = WaypointsTableModel(waypoints=self.window.waypoints_model.waypoints) + QtTest.QTest.mouseClick(self.window.exportBtn, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + self.window.waypoints_model.invert_direction() + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait(100) + assert exported_wp.waypoint_data(0).lat != self.window.waypoints_model.waypoint_data(0).lat + QtTest.QTest.mouseClick(self.window.importBtn, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait(100) + assert len(self.window.waypoints_model.waypoints) == 2 + imported_wp = self.window.waypoints_model + wp_count = len(imported_wp.waypoints) + assert wp_count == 2 + for i in range(wp_count): + assert exported_wp.waypoint_data(i).lat == imported_wp.waypoint_data(i).lat def test_work_locally_toggle(self): self._connect_to_mscolab() @@ -211,16 +247,43 @@ def test_set_exported_file(self, mockopen, mockmessage): QtWidgets.QApplication.processEvents() assert self.window.listProjects.model().rowCount() == 1 - def test_add_project(self): - # ToDo test needs to be independent from test_user_delete + @mock.patch("PyQt5.QtWidgets.QErrorMessage") + def test_add_project(self, mockbox): self._connect_to_mscolab() self._create_user("something", "something@something.org", "something") self._login("something@something.org", "something") assert self.window.label.text() == 'Welcome, something' assert self.window.loginWidget.isVisible() is False self._create_project("Alpha", "Description Alpha") + assert mockbox.return_value.showMessage.call_count == 2 + with mock.patch("PyQt5.QtWidgets.QLineEdit.text", return_value=None): + self._create_project("Alpha2", "Description Alpha") + with mock.patch("PyQt5.QtWidgets.QTextEdit.toPlainText", return_value=None): + self._create_project("Alpha3", "Description Alpha") + self._create_project("/", "Description Alpha") + assert mockbox.return_value.showMessage.call_count == 5 assert self.window.listProjects.model().rowCount() == 1 + @mock.patch("mslib.msui.mscolab.MSCOLAB_AuthenticationDialog.exec_", return_value=QtWidgets.QDialog.Accepted) + @mock.patch("PyQt5.QtWidgets.QErrorMessage") + def test_failed_authorize(self, mockbox, mockauth): + class response: + def __init__(self, code, text): + self.status_code = code + self.text = text + + self._connect_to_mscolab() + with mock.patch("requests.Session.post", new=ExceptionMock(requests.exceptions.ConnectionError).raise_exc): + self._login() + with mock.patch("requests.Session.post", return_value=response(201, "False")): + self._login() + with mock.patch("requests.Session.post", return_value=response(401, "False")): + self._login() + + # No return after self.error_dialog.showMessage('Oh no, server authentication were incorrect.') + # causes 4 instead of 3 messages, I am not sure if this is on purpose. + assert mockbox.return_value.showMessage.call_count == 4 + def test_add_user(self): self._connect_to_mscolab() self._create_user("something", "something@something.org", "something") @@ -301,6 +364,21 @@ def test_delete_project_from_list(self): self.window.delete_project_from_list(p_id) assert self.window.active_pid is None + @mock.patch("PyQt5.QtWidgets.QMessageBox") + @mock.patch("sys.exit") + def test_create_dir_exceptions(self, mockexit, mockbox): + with mock.patch("fs.open_fs", new=ExceptionMock(fs.errors.CreateFailed).raise_exc): + self.window.data_dir = "://" + self.window.create_dir() + assert mockbox.critical.call_count == 1 + assert mockexit.call_count == 1 + + with mock.patch("fs.open_fs", new=ExceptionMock(fs.opener.errors.UnsupportedProtocol).raise_exc): + self.window.data_dir = "://" + self.window.create_dir() + assert mockbox.critical.call_count == 2 + assert mockexit.call_count == 2 + def _connect_to_mscolab(self): self.window.url.setEditText(self.url) QtTest.QTest.mouseClick(self.window.toggleConnectionBtn, QtCore.Qt.LeftButton) diff --git a/mslib/msui/_tests/test_mss_pyui.py b/mslib/msui/_tests/test_mss_pyui.py index 9fe8356df..b136e5092 100644 --- a/mslib/msui/_tests/test_mss_pyui.py +++ b/mslib/msui/_tests/test_mss_pyui.py @@ -34,6 +34,7 @@ from mslib import __version__ from mslib._tests.constants import ROOT_DIR import mslib.msui.mss_pyui as mss_pyui +from mslib._tests.utils import ExceptionMock from mslib.plugins.io.text import load_from_txt, save_to_txt from mslib.plugins.io.flitestar import load_from_flitestar @@ -138,12 +139,34 @@ def test_open_tableview(self, mockbox): assert mockbox.critical.call_count == 0 assert self.window.listViews.count() == 1 + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_open_linearview(self, mockbox): + assert self.window.listViews.count() == 0 + self.window.actionLinearView.trigger() + self.window.listViews.itemActivated.emit(self.window.listViews.item(0)) + QtWidgets.QApplication.processEvents() + assert self.window.listViews.count() == 1 + assert mockbox.critical.call_count == 0 + @mock.patch("PyQt5.QtWidgets.QMessageBox") def test_open_about(self, mockbox): self.window.actionAboutMSUI.trigger() QtWidgets.QApplication.processEvents() assert mockbox.critical.call_count == 0 + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_open_config(self, mockbox): + self.window.actionConfigurationEditor.trigger() + QtWidgets.QApplication.processEvents() + self.window.config_editor.close() + assert mockbox.critical.call_count == 0 + + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_open_shotcut(self, mockbox): + self.window.actionShortcuts.trigger() + QtWidgets.QApplication.processEvents() + assert mockbox.critical.call_count == 0 + @mock.patch("mslib.msui.mss_pyui.get_save_filename", return_value=save_ftml) def test_plugin_ftml_saveas(self, mocksave): assert self.window.listFlightTracks.count() == 1 @@ -207,3 +230,57 @@ def test_plugin_flitestar(self, mockopen): QtWidgets.QApplication.processEvents() assert self.window.listFlightTracks.count() == 2 assert mockopen.call_count == 1 + + @mock.patch("PyQt5.QtWidgets.QMessageBox") + @mock.patch("mslib.msui.mss_pyui.config_loader", + return_value={"Text": ["txt", "mslib.plugins.io.text", "save_to_txt"]}) + def test_add_plugins(self, mockopen, mockbox): + assert len(self.window.menuImport_Flight_Track.actions()) == 1 + assert len(self.window.menuExport_Active_Flight_Track.actions()) == 1 + assert len(self.window._imported_plugins) == 0 + assert len(self.window._exported_plugins) == 0 + self.window.add_plugins() + assert len(self.window._imported_plugins) == 1 + assert len(self.window._exported_plugins) == 1 + assert len(self.window.menuImport_Flight_Track.actions()) == 2 + assert len(self.window.menuExport_Active_Flight_Track.actions()) == 2 + assert mockbox.critical.call_count == 0 + + with mock.patch("importlib.import_module", new=ExceptionMock(Exception()).raise_exc): + self.window.add_plugins() + assert mockbox.critical.call_count == 2 + with mock.patch("mslib.msui.mss_pyui.MSSMainWindow.add_import_filter", + new=ExceptionMock(Exception()).raise_exc): + self.window.add_plugins() + assert mockbox.critical.call_count == 4 + + self.window.remove_plugins() + assert len(self.window.menuImport_Flight_Track.actions()) == 1 + assert len(self.window.menuExport_Active_Flight_Track.actions()) == 1 + + @mock.patch("PyQt5.QtWidgets.QMessageBox.critical") + @mock.patch("PyQt5.QtWidgets.QMessageBox.warning", return_value=QtWidgets.QMessageBox.Yes) + @mock.patch("PyQt5.QtWidgets.QMessageBox.information", return_value=QtWidgets.QMessageBox.Yes) + @mock.patch("PyQt5.QtWidgets.QMessageBox.question", return_value=QtWidgets.QMessageBox.Yes) + @mock.patch("mslib.msui.mss_pyui.get_save_filename", return_value=save_ftml) + @mock.patch("mslib.msui.mss_pyui.get_open_filename", return_value=save_ftml) + def test_flight_track_io(self, mockload, mocksave, mockq, mocki, mockw, mockbox): + self.window.actionCloseSelectedFlightTrack.trigger() + assert mocki.call_count == 1 + self.window.actionNewFlightTrack.trigger() + self.window.listFlightTracks.setCurrentRow(0) + assert self.window.listFlightTracks.count() == 2 + tmp_ft = self.window.active_flight_track + self.window.active_flight_track = self.window.listFlightTracks.currentItem().flighttrack_model + self.window.actionCloseSelectedFlightTrack.trigger() + assert mocki.call_count == 2 + self.window.last_save_directory = self.sample_path + self.window.actionSaveActiveFlightTrack.trigger() + self.window.actionSaveActiveFlightTrack.trigger() + self.window.active_flight_track = tmp_ft + self.window.actionCloseSelectedFlightTrack.trigger() + assert self.window.listFlightTracks.count() == 1 + self.window.actionOpenFlightTrack.trigger() + assert self.window.listFlightTracks.count() == 2 + assert os.path.exists(self.save_ftml) + os.remove(self.save_ftml) diff --git a/mslib/msui/_tests/test_sideview.py b/mslib/msui/_tests/test_sideview.py index 5bd214d70..67e61672c 100644 --- a/mslib/msui/_tests/test_sideview.py +++ b/mslib/msui/_tests/test_sideview.py @@ -38,7 +38,7 @@ import mslib.msui.sideview as tv from mslib._tests.utils import wait_until_signal -PORTS = list(range(8095, 8105)) +PORTS = list(range(8095, 8106)) class Test_MSS_SV_OptionsDialog(object): @@ -182,6 +182,9 @@ def test_server_getmap(self, mockbox): QtTest.QTest.mouseClick(self.wms_control.btGetMap, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() wait_until_signal(self.wms_control.image_displayed) + assert self.window.getView().image is not None + self.window.getView().clear_figure() + assert self.window.getView().image is None assert mockbox.critical.call_count == 0 @mock.patch("PyQt5.QtWidgets.QMessageBox") @@ -215,3 +218,11 @@ def test_insert_point(self, mockbox): QtWidgets.QApplication.processEvents() assert len(self.window.waypoints_model.waypoints) == 5 assert mockbox.critical.call_count == 0 + + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_y_axes(self, mockbox): + self.window.getView().get_settings()["secondary_axis"] = "pressure altitude" + self.window.getView().set_settings(self.window.getView().get_settings()) + self.window.getView().get_settings()["secondary_axis"] = "flight level" + self.window.getView().set_settings(self.window.getView().get_settings()) + assert mockbox.critical.call_count == 0 diff --git a/mslib/msui/_tests/test_topview.py b/mslib/msui/_tests/test_topview.py index ad9111312..195f4f81d 100644 --- a/mslib/msui/_tests/test_topview.py +++ b/mslib/msui/_tests/test_topview.py @@ -313,6 +313,9 @@ def test_server_getmap(self, mockbox): QtTest.QTest.mouseClick(self.wms_control.btGetMap, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() wait_until_signal(self.wms_control.image_displayed) - QtWidgets.QApplication.processEvents() + assert self.window.getView().map.image is not None + self.window.getView().set_map_appearance({}) + self.window.getView().clear_figure() + assert self.window.getView().map.image is None self.window.mpl.canvas.redraw_map() assert mockbox.critical.call_count == 0 diff --git a/mslib/msui/_tests/test_wms_control.py b/mslib/msui/_tests/test_wms_control.py index dd6bc7a39..0c4af039a 100644 --- a/mslib/msui/_tests/test_wms_control.py +++ b/mslib/msui/_tests/test_wms_control.py @@ -31,15 +31,17 @@ import shutil import tempfile import pytest +import hashlib import multiprocessing from mslib.mswms.mswms import application from PyQt5 import QtWidgets, QtCore, QtTest from mslib.msui import flighttrack as ft import mslib.msui.wms_control as wc +from mslib.msui.mss_pyui import MSSMainWindow from mslib._tests.utils import wait_until_signal -PORTS = list(range(8107, 8121)) +PORTS = list(range(8107, 8125)) class HSecViewMockup(mock.Mock): @@ -153,6 +155,25 @@ def test_connection_error(self, mockbox): self.query_server(f"http://.....127.0.0.1:{self.port}") assert mockbox.critical.call_count == 1 + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_forward_backward_clicks(self, mockbox): + self.query_server(f"http://127.0.0.1:{self.port}") + self.window.init_time_back_click() + self.window.init_time_fwd_click() + self.window.valid_time_fwd_click() + self.window.valid_time_back_click() + self.window.level_fwd_click() + self.window.level_back_click() + self.window.cb_init_time_back_click() + self.window.cb_valid_time_back_click() + self.window.cb_init_time_fwd_click() + self.window.cb_valid_time_fwd_click() + try: + self.window.secs_from_timestep("Wrong") + except ValueError: + pass + assert mockbox.critical.call_count == 0 + def test_server_abort_getmap(self): """ assert that an aborted getmap call does not change the displayed image @@ -287,10 +308,6 @@ def test_multilayer_handling(self, mockbox): assert self.window.multilayers.listLayers.itemWidget(server.child(2), 2) is None assert self.window.multilayers.listLayers.itemWidget(server.child(0), 2).currentText() == "1" - # Check layer filter is working - self.window.multilayers.leMultiFilter.setText("No matches") - assert server.isHidden() - # Check drawing not causing errors QtTest.QTest.mouseClick(self.window.btGetMap, QtCore.Qt.LeftButton) QtWidgets.QApplication.processEvents() @@ -301,6 +318,51 @@ def test_multilayer_handling(self, mockbox): assert self.view.draw_legend.call_count == 1 assert self.view.draw_metadata.call_count == 1 + @mock.patch("PyQt5.QtWidgets.QMessageBox") + def test_filter_handling(self, mockbox): + self.query_server(f"http://127.0.0.1:{self.port}") + server = self.window.multilayers.listLayers.findItems(f"http://127.0.0.1:{self.port}/", + QtCore.Qt.MatchFixedString)[0] + self.window.cbAutoUpdate.setCheckState(False) + assert server is not None + assert "header" in self.window.multilayers.layers[f"http://127.0.0.1:{self.port}/"] + assert "wms" in self.window.multilayers.layers[f"http://127.0.0.1:{self.port}/"] + + starts_at = 40 * self.window.multilayers.scale + icon_start_fav = starts_at + 3 + if self.window.multilayers.cbMultilayering.isChecked(): + checkbox_width = round(self.window.multilayers.height * 0.75) + icon_start_fav += checkbox_width + 6 + + starts_at = 20 * self.window.multilayers.scale + icon_start_del = starts_at + 3 + + # Check layer filter is working + server.child(0).is_favourite = False + self.window.multilayers.leMultiFilter.setText("No matches") + assert server.isHidden() + self.window.multilayers.remove_filter_triggered() + assert not server.isHidden() + self.window.multilayers.filter_favourite_toggled() + assert server.isHidden() + self.window.multilayers.filter_favourite_toggled() + QtTest.QTest.qWait(100) + QtTest.QTest.mouseMove(self.window.multilayers.listLayers, QtCore.QPoint(icon_start_fav + 3, 0), -1) + QtWidgets.QApplication.processEvents() + self.window.multilayers.check_icon_clicked(server.child(0)) + self.window.multilayers.filter_favourite_toggled() + assert not server.isHidden() + server.child(0).favourite_triggered() + self.window.multilayers.remove_filter_triggered() + + # Check deleting server is working + QtTest.QTest.mouseMove(self.window.multilayers.listLayers, QtCore.QPoint(icon_start_del + 3, 0), -1) + QtWidgets.QApplication.processEvents() + self.window.multilayers.check_icon_clicked(server) + assert len(self.window.multilayers.listLayers.findItems(f"http://127.0.0.1:{self.port}/", + QtCore.Qt.MatchFixedString)) == 0 + assert mockbox.critical.call_count == 0 + @mock.patch("PyQt5.QtWidgets.QMessageBox") def test_singlelayer_handling(self, mockbox): """ @@ -370,6 +432,37 @@ def test_multilayer_syncing(self, mockbox): assert layer_a.get_itime() == layer_a.get_itimes()[-1] assert mockbox.critical.call_count == 0 + @mock.patch("PyQt5.QtWidgets.QMessageBox") + @mock.patch("mslib.msui.wms_control.WMSMapFetcher.moveToThread") + def test_server_no_thread(self, mockbox, mockthread): + self.query_server(f"http://127.0.0.1:{self.port}") + server = self.window.multilayers.listLayers.findItems(f"http://127.0.0.1:{self.port}/", + QtCore.Qt.MatchFixedString)[0] + self.window.cbAutoUpdate.setCheckState(False) + server.setExpanded(True) + self.window.multilayers.cbMultilayering.setChecked(True) + server.child(0).setCheckState(0, 2) + server.child(1).setCheckState(0, 2) + + QtTest.QTest.mouseClick(self.window.btGetMap, QtCore.Qt.LeftButton) + QtWidgets.QApplication.processEvents() + wait_until_signal(self.window.image_displayed) + + urlstr = f"http://127.0.0.1:{self.port}/mss/logo.png" + md5_filname = os.path.join(self.window.wms_cache, hashlib.md5(urlstr.encode('utf-8')).hexdigest() + ".png") + self.window.fetcher.fetch_legend(urlstr, use_cache=False, md5_filename=md5_filname) + self.window.fetcher.fetch_legend(urlstr, use_cache=True, md5_filename=md5_filname) + + assert mockbox.critical.call_count == 0 + assert self.view.draw_image.call_count == 1 + assert self.view.draw_legend.call_count == 1 + assert self.view.draw_metadata.call_count == 1 + + def test_preload(self): + assert f"http://127.0.0.1:{self.port}/" not in wc.WMS_SERVICE_CACHE + MSSMainWindow.preload_wms([f"http://127.0.0.1:{self.port}/"]) + assert f"http://127.0.0.1:{self.port}/" in wc.WMS_SERVICE_CACHE + @pytest.mark.skipif(os.name == "nt", reason="multiprocessing needs currently start_method fork") diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index a816e0a01..085845d37 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -260,18 +260,19 @@ def connect_handler(self): self.loginButton.setEnabled(True) else: show_popup(self, "Error", "Some unexpected error occurred. Please try again.") - except requests.exceptions.ConnectionError: - logging.debug("MSColab server isn't active") - show_popup(self, "Error", "MSColab server isn't active") + except requests.exceptions.SSLError: + logging.debug("Certificate Verification Failed") + show_popup(self, "Error", "Certificate Verification Failed") except requests.exceptions.InvalidSchema: logging.debug("invalid schema of url") show_popup(self, "Error", "Invalid Url Scheme!") except requests.exceptions.InvalidURL: logging.debug("invalid url") show_popup(self, "Error", "Invalid URL") - except requests.exceptions.SSLError: - logging.debug("Certificate Verification Failed") - show_popup(self, "Error", "Certificate Verification Failed") + # ConnectionError is a superclass of other errors, best to put it in the bottom and first catch the others + except requests.exceptions.ConnectionError: + logging.debug("MSColab server isn't active") + show_popup(self, "Error", "MSColab server isn't active") except Exception as e: logging.debug("Error %s", str(e)) show_popup(self, "Error", "Some unexpected error occurred. Please try again.") @@ -966,7 +967,7 @@ def create_view_window(self, _type): view_window = linearview.MSSLinearViewWindow(model=self.waypoints_model, parent=self.listProjects, _id=self.id_count) - view_window.view_type = "tableview" + view_window.view_type = "linearview" if self.access_level == "viewer": self.disable_navbar_action_buttons(_type, view_window) @@ -986,7 +987,7 @@ def disable_navbar_action_buttons(self, _type, view_window): function disables some control, used if access_level is not appropriate """ - if _type == "topview" or _type == "sideview": + if _type == "topview" or _type == "sideview" or _type == "linearview": actions = view_window.mpl.navbar.actions() for action in actions: action_text = action.text() @@ -1009,7 +1010,7 @@ def enable_navbar_action_buttons(self, _type, view_window): function enables some control, used if access_level is appropriate """ - if _type == "topview" or _type == "sideview": + if _type == "topview" or _type == "sideview" or _type == "linearview": actions = view_window.mpl.navbar.actions() for action in actions: action_text = action.text() @@ -1101,6 +1102,7 @@ def save_wp_mscolab(self, comment=None): self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() show_popup(self, "Success", "New Waypoints Saved To Server!", icon=1) + self.merge_dialog.close() self.merge_dialog = None else: show_popup(self, "Error", "Your Connection is expired. New Login required!") @@ -1145,6 +1147,7 @@ def fetch_wp_mscolab(self): self.waypoints_model.dataChanged.connect(self.handle_waypoints_changed) self.reload_view_windows() show_popup(self, "Success", "New Waypoints Fetched To Local File!", icon=1) + self.merge_dialog.close() self.merge_dialog = None else: show_popup(self, "Error", "Your Connection is expired. New Login required!") diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index 2e0a48695..8727ff670 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -209,7 +209,7 @@ def filter_multilayers(self, filter_string=None): wms_hits += 1 else: widget.setHidden(True) - if wms_hits == 0 and len(filter_string) > 0: + if wms_hits == 0 and (len(filter_string) > 0 or self.filter_favourite): header.setHidden(True) else: header.setHidden(False) diff --git a/mslib/msui/socket_control.py b/mslib/msui/socket_control.py index b7e03e060..e7cb9f094 100644 --- a/mslib/msui/socket_control.py +++ b/mslib/msui/socket_control.py @@ -50,7 +50,8 @@ def __init__(self, token, user, mscolab_server_url=mss_default.mscolab_server_ur self.token = token self.user = user self.mscolab_server_url = mscolab_server_url - logging.getLogger("engineio.client").addFilter(filter=lambda record: token not in record.getMessage()) + if token is not None: + logging.getLogger("engineio.client").addFilter(filter=lambda record: token not in record.getMessage()) self.sio = socketio.Client(reconnection_attempts=5) self.sio.connect(self.mscolab_server_url) diff --git a/mslib/mswms/_tests/test_mss_plot_driver.py b/mslib/mswms/_tests/test_mss_plot_driver.py index 1ebc23c66..79c19a7b4 100644 --- a/mslib/mswms/_tests/test_mss_plot_driver.py +++ b/mslib/mswms/_tests/test_mss_plot_driver.py @@ -29,6 +29,9 @@ from datetime import datetime import pytest +from PIL import Image +from xml.etree import ElementTree +import io from mslib.mswms.mss_plot_driver import VerticalSectionDriver, HorizontalSectionDriver, LinearSectionDriver import mss_wms_settings import mslib.mswms.mpl_vsec_styles as mpl_vsec_styles @@ -36,6 +39,18 @@ import mslib.mswms.mpl_lsec_styles as mpl_lsec_styles +def is_image_transparent(img): + with Image.open(io.BytesIO(img)) as image: + if image.mode == "P": + transparent = image.info.get("transparency", -1) + for _, index in image.getcolors(): + if index == transparent: + return True + elif image.mode == "RGBA": + return image.getextrema()[3][0] < 255 + return False + + class Test_VSec(object): def setup(self): p1 = [45.00, 8.] @@ -51,7 +66,8 @@ def setup(self): self.valid_time = datetime(2012, 10, 17, 12) self.vsec = VerticalSectionDriver(data) - def plot(self, plot_object, style="default"): + def plot(self, plot_object, style="default", noframe=False, transparent=False, draw_verticals=False, + return_format="image/png"): self.vsec.set_plot_parameters(plot_object=plot_object, bbox=self.bbox, vsec_path=self.path, @@ -60,7 +76,10 @@ def plot(self, plot_object, style="default"): init_time=self.init_time, valid_time=self.valid_time, style=style, - noframe=False, + noframe=noframe, + draw_verticals=draw_verticals, + transparent=transparent, + return_format=return_format, show=False) return self.vsec.plot() @@ -77,32 +96,63 @@ def test_repeated_locations(self): img = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec)) assert img is not None + def test_VS_verticals(self): + img = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec), draw_verticals=True) + assert img is not None + assert not is_image_transparent(img) + + def test_VS_transparent(self): + img = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec), transparent=True) + assert img is not None + assert is_image_transparent(img) + + def test_xml(self): + img = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec), return_format="text/xml") + assert img is not None + ElementTree.fromstring(img) + def test_VS_TemperatureStyle_01(self): img = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_TemperatureStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_GenericStyle(self): img = self.plot(mpl_vsec_styles.VS_GenericStyle_PL_mole_fraction_of_ozone_in_air(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_GenericStyle_PL_mole_fraction_of_ozone_in_air(driver=self.vsec), + noframe=True) + assert noframe != img img = self.plot(mpl_vsec_styles.VS_GenericStyle_TL_mole_fraction_of_ozone_in_air(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_GenericStyle_TL_mole_fraction_of_ozone_in_air(driver=self.vsec), + noframe=True) + assert noframe != img def test_VS_CloudsStyle_01(self): img = self.plot(mpl_vsec_styles.VS_CloudsStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_CloudsStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_CloudsWindStyle_01(self): img = self.plot(mpl_vsec_styles.VS_CloudsWindStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_CloudsWindStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_RelativeHumdityStyle_01(self): img = self.plot(mpl_vsec_styles.VS_RelativeHumdityStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_RelativeHumdityStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_SpecificHumdityStyle_01(self): img = self.plot(mpl_vsec_styles.VS_SpecificHumdityStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_SpecificHumdityStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_VerticalVelocityStyle_01(self): img = self.plot(mpl_vsec_styles.VS_VerticalVelocityStyle_01(driver=self.vsec)) @@ -111,23 +161,33 @@ def test_VS_VerticalVelocityStyle_01(self): def test_VS_HorizontalVelocityStyle_01(self): img = self.plot(mpl_vsec_styles.VS_HorizontalVelocityStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_HorizontalVelocityStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_PotentialVorticityStyle_01(self): img = self.plot(mpl_vsec_styles.VS_PotentialVorticityStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_PotentialVorticityStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_ProbabilityOfWCBStyle_01(self): img = self.plot(mpl_vsec_styles.VS_ProbabilityOfWCBStyle_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_ProbabilityOfWCBStyle_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_LagrantoTrajStyle_PL_01(self): pytest.skip("data not available") img = self.plot(mpl_vsec_styles.VS_LagrantoTrajStyle_PL_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_LagrantoTrajStyle_PL_01(driver=self.vsec), noframe=True) + assert noframe != img def test_VS_EMACEyja_Style_01(self): img = self.plot(mpl_vsec_styles.VS_EMACEyja_Style_01(driver=self.vsec)) assert img is not None + noframe = self.plot(mpl_vsec_styles.VS_EMACEyja_Style_01(driver=self.vsec), noframe=True) + assert noframe != img class Test_LSec(object): @@ -203,12 +263,13 @@ def setup(self): self.valid_time = datetime(2012, 10, 17, 12) self.hsec = HorizontalSectionDriver(data) - def plot(self, plot_object, style="default", level=None, crs="EPSG:4326", bbox=None): + def plot(self, plot_object, style="default", level=None, crs="EPSG:4326", bbox=None, noframe=False, + transparent=False): if bbox is None: bbox = self.bbox self.hsec.set_plot_parameters(plot_object=plot_object, bbox=bbox, level=level, crs=crs, - init_time=self.init_time, valid_time=self.valid_time, style=style, noframe=False, - show=False) + init_time=self.init_time, valid_time=self.valid_time, style=style, + noframe=noframe, show=False, transparent=transparent) return self.hsec.plot() @pytest.mark.parametrize("crs", [ @@ -246,32 +307,52 @@ def test_repeated_locations(self): def test_HS_CloudsStyle_01(self): for style in ["TOT", "HIGH", "MED", "LOW"]: img = self.plot(mpl_hsec_styles.HS_CloudsStyle_01(driver=self.hsec), style=style) + assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_CloudsStyle_01(driver=self.hsec), style=style, noframe=True) + assert noframe != img + assert not is_image_transparent(img) + + def test_HS_transparent(self): + img = self.plot(mpl_hsec_styles.HS_MSLPStyle_01(driver=self.hsec), transparent=True) assert img is not None + assert is_image_transparent(img) def test_HS_MSLPStyle_01(self): img = self.plot(mpl_hsec_styles.HS_MSLPStyle_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_MSLPStyle_01(driver=self.hsec), noframe=True) + assert noframe != img def test_HS_SEAStyle_01(self): img = self.plot(mpl_hsec_styles.HS_SEAStyle_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_SEAStyle_01(driver=self.hsec), noframe=True) + assert noframe != img @pytest.mark.parametrize("style", ["PCOL", "CONT"]) def test_HS_SeaIceStyle_01(self, style): img = self.plot(mpl_hsec_styles.HS_SeaIceStyle_01(driver=self.hsec), style=style) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_SeaIceStyle_01(driver=self.hsec), style=style, noframe=True) + assert noframe != img def test_HS_TemperatureStyle_ML_01(self): img = self.plot(mpl_hsec_styles.HS_TemperatureStyle_ML_01(driver=self.hsec), level=10) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_TemperatureStyle_ML_01(driver=self.hsec), level=10, noframe=True) + assert noframe != img def test_HS_TemperatureStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_TemperatureStyle_PL_01(driver=self.hsec), level=800) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_TemperatureStyle_PL_01(driver=self.hsec), level=800, noframe=True) + assert noframe != img def test_HS_GeopotentialWindStyle_PL(self): img = self.plot(mpl_hsec_styles.HS_GeopotentialWindStyle_PL(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_GeopotentialWindStyle_PL(driver=self.hsec), level=300, noframe=True) + assert noframe != img @pytest.mark.parametrize("style", ["default", "nonlinear", "auto", "log", "autolog"]) def test_HS_GenericStyle_styles(self, style): @@ -279,64 +360,100 @@ def test_HS_GenericStyle_styles(self, style): mpl_hsec_styles.HS_GenericStyle_PL_mole_fraction_of_ozone_in_air(driver=self.hsec), level=300, style=style) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_mole_fraction_of_ozone_in_air(driver=self.hsec), + level=300, style=style, noframe=True) + assert noframe != img def test_HS_GenericStyle_other(self): img = self.plot(mpl_hsec_styles.HS_GenericStyle_TL_mole_fraction_of_ozone_in_air(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_TL_mole_fraction_of_ozone_in_air(driver=self.hsec), + level=300, noframe=True) + assert noframe != img img = self.plot( mpl_hsec_styles.HS_GenericStyle_PL_ertel_potential_vorticity(driver=self.hsec), style="ertel_potential_vorticity", level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_ertel_potential_vorticity(driver=self.hsec), + level=300, style="ertel_potential_vorticity", noframe=True) + assert noframe != img img = self.plot( mpl_hsec_styles.HS_GenericStyle_PL_equivalent_latitude(driver=self.hsec), style="equivalent_latitude", level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_equivalent_latitude(driver=self.hsec), + level=300, style="equivalent_latitude", noframe=True) + assert noframe != img def test_HS_RelativeHumidityStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_RelativeHumidityStyle_PL_01(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_RelativeHumidityStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe != img def test_HS_EQPTStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_EQPTStyle_PL_01(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_EQPTStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe != img def test_HS_WStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_WStyle_PL_01(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_WStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe != img def test_HS_DivStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_DivStyle_PL_01(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_DivStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe != img def test_HS_EMAC_TracerStyle_ML_01(self): img = self.plot(mpl_hsec_styles.HS_EMAC_TracerStyle_ML_01(driver=self.hsec), level=10) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_EMAC_TracerStyle_ML_01(driver=self.hsec), level=10, noframe=True) + assert noframe != img def test_HS_EMAC_TracerStyle_SFC_01(self): img = self.plot(mpl_hsec_styles.HS_EMAC_TracerStyle_SFC_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_EMAC_TracerStyle_SFC_01(driver=self.hsec), noframe=True) + assert noframe != img def test_HS_PVTropoStyle_PV_01(self): # test fractional levels and non-existing levels - img = self.plot(mpl_hsec_styles.HS_PVTropoStyle_PV_01(driver=self.hsec), level=2.5) - assert img is not None + for style in ["PRES", "PT", "GEOP"]: + img = self.plot(mpl_hsec_styles.HS_PVTropoStyle_PV_01(driver=self.hsec), level=2.5, style=style) + assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_PVTropoStyle_PV_01(driver=self.hsec), level=2.5, style=style, + noframe=True) + assert noframe != img with pytest.raises(ValueError): self.plot(mpl_hsec_styles.HS_PVTropoStyle_PV_01(driver=self.hsec), level=2.75) def test_HS_VIProbWCB_Style_01(self): img = self.plot(mpl_hsec_styles.HS_VIProbWCB_Style_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_VIProbWCB_Style_01(driver=self.hsec), noframe=True) + assert noframe != img def test_HS_LagrantoTrajStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_LagrantoTrajStyle_PL_01(driver=self.hsec), level=300) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_LagrantoTrajStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe != img def test_HS_BLH_MSLP_Style_01(self): img = self.plot(mpl_hsec_styles.HS_BLH_MSLP_Style_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_BLH_MSLP_Style_01(driver=self.hsec), noframe=True) + assert noframe != img def test_HS_Meteosat_BT108_01(self): img = self.plot(mpl_hsec_styles.HS_Meteosat_BT108_01(driver=self.hsec)) assert img is not None + noframe = self.plot(mpl_hsec_styles.HS_Meteosat_BT108_01(driver=self.hsec), noframe=True) + assert noframe != img diff --git a/mslib/mswms/_tests/test_wms.py b/mslib/mswms/_tests/test_wms.py index bdff54194..b733c5f2b 100644 --- a/mslib/mswms/_tests/test_wms.py +++ b/mslib/mswms/_tests/test_wms.py @@ -26,7 +26,9 @@ limitations under the License. """ +import mock import mslib.mswms.mswms as mswms +from importlib import reload from mslib._tests.utils import callback_ok_image, callback_ok_xml, callback_ok_html, callback_404_plain @@ -333,3 +335,43 @@ def test_application_unkown_request(self): callback_404_plain(result.status, result.headers) assert isinstance(result.data, bytes), result assert result.data.count(b"") > 0, result + + def test_multiple_images(self): + environ = { + 'wsgi.url_scheme': 'http', + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8081', + 'QUERY_STRING': + 'layers=ecmwf_EUR_LL015.PLDiv01,ecmwf_EUR_LL015.PLTemp01&styles=&elevation=200&' + 'srs=EPSG%3A4326&format=image%2Fpng&' + 'request=GetMap&bgcolor=0xFFFFFF&height=376&dim_init_time=2012-10-17T12%3A00%3A00Z&width=479&' + 'version=1.1.1&bbox=-50.0%2C20.0%2C20.0%2C75.0&time=2012-10-17T12%3A00%3A00Z&' + 'exceptions=application%2Fvnd.ogc.se_xml&transparent=FALSE'} + + self.client = mswms.application.test_client() + result = self.client.get('/?{}'.format(environ["QUERY_STRING"])) + callback_ok_image(result.status, result.headers) + assert isinstance(result.data, bytes), result + + def test_multiple_xml(self): + environ = { + 'wsgi.url_scheme': 'http', + 'REQUEST_METHOD': 'GET', 'PATH_INFO': '/', 'SERVER_PROTOCOL': 'HTTP/1.1', 'HTTP_HOST': 'localhost:8081', + 'QUERY_STRING': + 'layers=ecmwf_EUR_LL015.LS_HV01,ecmwf_EUR_LL015.LS_HV01&styles=&srs=LINE%3A1&format=text%2Fxml&' + 'request=GetMap&dim_init_time=2012-10-17T12%3A00%3A00Z&' + 'version=1.1.1&bbox=201&time=2012-10-17T12%3A00%3A00Z&' + 'exceptions=application%2Fvnd.ogc.se_xml&path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000'} + + self.client = mswms.application.test_client() + result = self.client.get('/?{}'.format(environ["QUERY_STRING"])) + callback_ok_xml(result.status, result.headers) + + def test_import_error(self): + import mslib.mswms.wms + with mock.patch.dict("sys.modules", {"mss_wms_settings": None, "mss_wms_auth": None}): + reload(mslib.mswms.wms) + assert mslib.mswms.wms.mss_wms_settings.__file__ is None + assert mslib.mswms.wms.mss_wms_auth.__file__ is None + reload(mslib.mswms.wms) + assert mslib.mswms.wms.mss_wms_settings.__file__ is not None + assert mslib.mswms.wms.mss_wms_auth.__file__ is not None From b99ffe088faa2629a4a4fafabf09c8d86067f1f7 Mon Sep 17 00:00:00 2001 From: Jatin Jain <72596619+Jatin2020-24@users.noreply.github.com> Date: Mon, 14 Jun 2021 00:26:28 +0530 Subject: [PATCH 10/37] Migrate existing thermodynamic functions to metpy functions (#1009) * removed functions from thermolib.py functions to be replaced by metpy were removed * fixed: flake8 * fixed: flake8 * migrated to metpy functions Co-Authored-By: J. Ungermann <28449201+joernu76@users.noreply.github.com> * Fixed: flake8 * Fixed: flake8 * Fixed tests * Fixed: flake8 * Fixed: failing tests * Ficed: tests * Units discarded upon returning results Co-Authored-By: J. Ungermann <28449201+joernu76@users.noreply.github.com> * `omega_to_w` migrated to metpy Co-authored-by: ReimarBauer Co-authored-by: J. Ungermann <28449201+joernu76@users.noreply.github.com> --- localbuild/meta.yaml | 1 + mslib/thermolib.py | 598 +++---------------------------------------- 2 files changed, 37 insertions(+), 562 deletions(-) diff --git a/localbuild/meta.yaml b/localbuild/meta.yaml index 00ed5bb56..698519e1d 100644 --- a/localbuild/meta.yaml +++ b/localbuild/meta.yaml @@ -80,6 +80,7 @@ requirements: - sqlalchemy <1.4.0 - sqlite <3.35.1 - gpxpy >=1.4.2 + - metpy test: imports: diff --git a/mslib/thermolib.py b/mslib/thermolib.py index dd4636913..d9a094212 100644 --- a/mslib/thermolib.py +++ b/mslib/thermolib.py @@ -26,12 +26,11 @@ limitations under the License. """ -# The function sat_vapour_pressure() has been ported from the IDL function -# 'VaporPressure' by Holger Voemel, available at http://cires.colorado.edu/~voemel/vp.html. - import numpy import scipy.integrate import logging +import metpy.calc as mpcalc +from metpy.units import units class VapourPressureError(Exception): @@ -43,353 +42,21 @@ def __init__(self, error_string): logging.debug("%s", error_string) -def sat_vapour_pressure(t, liquid='HylandWexler', ice='GoffGratch', - force_phase='None'): - """ - Compute the saturation vapour pressure over liquid water and over ice - with a variety of formulations. - - This function is a direct port from the IDL function 'VaporPressure' by - Holger Voemel, available at http://cires.colorado.edu/~voemel/vp.html. - - By default, for temperatures > 0 degC, the saturation pressure over - liquid water is computed; from temperatures <= 0 degC a formulation - over ice is used. - - The current default fomulas are Hyland and Wexler for liquid and - Goff Gratch for ice. (hv20040521) +def sat_vapour_pressure(t): + """Compute saturation vapour presure in Pa from temperature. Arguments: - t -- Temperature in [K]. Can be a scaler or an n-dimensional NumPy array. - liquid -- Optional; specify the formulation for computing the saturation - pressure over liquid water. Can be one of: - - HylandWexler, GoffGratch, Wexler, MagnusTeten, Buck_original, - Buck_manual, WMO_Goff, WMO2000, Sonntag, Bolton, [Fukuta (N/A)], - IAPWS, MurphyKoop. - - ice -- Optional; specify the formulation for computing the saturation - pressure over ice. Can be one of: + t -- temperature in [K] - MartiMauersberger, HylandWexler, GoffGratch, MagnusTeten, - Buck_original, Buck_manual, WMO_Goff, Sonntag, MurphyKoop. + Returns: Saturation Vapour Pressure in [Pa], in the same dimensions as the input. + """ + v_pr = mpcalc.saturation_vapor_pressure(t * units.kelvin) - Please have a look at the source code for further information - about the formulations. + # Convert return value units from mbar to Pa. + return v_pr.to('Pa').magnitude - force_phase -- Optional; force liquid or ice phase to avoid automatic - switching of formulations at 0 degC. Can be 'liquid' - or 'ice'. - Returns: - Saturation vapor pressure [Pa], in the same dimensions as the input. - """ - - # Make sure the input is a NumPy array. - if numpy.isscalar(t): - t = numpy.array([t]) - input_scalar = True - else: - t = numpy.array(t) - input_scalar = False - - # Get indexes of input temperatures above and below freezing, to select - # the appropriate method for each temperature. - if force_phase == "ice": - idx_ice = () # numpy.where(t is not None) - idx_liq = None - elif force_phase == "liquid": - idx_liq = () # numpy.where(t is not None) - idx_ice = None - elif force_phase == "None": - idx_ice = numpy.where(t <= 273.15) - idx_liq = numpy.where(t > 273.15) - else: - raise VapourPressureError("Cannot recognize the force_phase " - f"keyword: '{force_phase}' (valid are ice, liquid, None)") - - # Initialise output field. - e_sat = numpy.zeros(numpy.shape(t)) - - # ============================================================================= - # Calculate saturation pressure over liquid water ---------------------------- - if not force_phase == 'ice': - - if liquid == 'MartiMauersberger': - raise VapourPressureError("Marti and Mauersberger don't " - "have a vapour pressure curve over liquid.") - - elif liquid == 'HylandWexler': - # Source: Hyland, R. W. and A. Wexler, Formulations for the - # Thermodynamic Properties of the saturated Phases of H2O - # from 173.15K to 473.15K, ASHRAE Trans, 89(2A), 500-519, 1983. - e_sat[idx_liq] = (numpy.exp((-0.58002206E4 / t[idx_liq]) + - 0.13914993E1 - - 0.48640239E-1 * t[idx_liq] + - 0.41764768E-4 * t[idx_liq] ** 2. - - 0.14452093E-7 * t[idx_liq] ** 3. + - 0.65459673E1 * numpy.log(t[idx_liq])) / 100.) - - elif liquid == 'Wexler': - # Wexler, A., Vapor pressure formulation for ice, Journal of - # Research of the National Bureau of Standards-A. 81A, 5-20, 1977. - e_sat[idx_liq] = (numpy.exp(-2.9912729E3 * t[idx_liq] ** (-2.) - - 6.0170128E3 * t[idx_liq] ** (-1.) + - 1.887643854E1 * t[idx_liq] ** 0. - - 2.8354721E-2 * t[idx_liq] ** 1. + - 1.7838301E-5 * t[idx_liq] ** 2. - - 8.4150417E-10 * t[idx_liq] ** 3. - - 4.4412543E-13 * t[idx_liq] ** 4. + - 2.858487 * numpy.log(t[idx_liq])) / 100.) - - elif liquid == 'GoffGratch': - # Goff Gratch formulation. - # Source: Smithsonian Meteorological Tables, 5th edition, - # p. 350, 1984 - # From original source: Goff and Gratch (1946), p. 107. - ts = 373.16 # steam point temperature in K - ews = 1013.246 # saturation pressure at steam point - # temperature, normal atmosphere - e_sat[idx_liq] = 10. ** (-7.90298 * ((ts / t[idx_liq]) - 1.) + - 5.02808 * numpy.log10((ts / t[idx_liq])) - - 1.3816E-7 * (10. ** (11.344 * (1. - (t[idx_liq] / ts))) - 1.) + - 8.1328E-3 * (10. ** (-3.49149 * ((ts / t[idx_liq]) - 1)) - 1.) + - numpy.log10(ews)) - - elif liquid == 'MagnusTeten': - # Source: Murray, F. W., On the computation of saturation - # vapor pressure, J. Appl. Meteorol., 6, 203-204, 1967. - tc = t - 273.15 - e_sat[idx_liq] = 10. ** (7.5 * (tc[idx_liq]) / (tc[idx_liq] + 237.5) + 0.7858) - - elif liquid == 'Buck_original': - # Bucks vapor pressure formulation based on Tetens formula - # Source: Buck, A. L., New equations for computing vapor - # pressure and enhancement factor, J. Appl. Meteorol., 20, - # 1527-1532, 1981. - tc = t - 273.15 - e_sat[idx_liq] = 6.1121 * numpy.exp(17.502 * tc[idx_liq] / (240.97 + tc[idx_liq])) - - elif liquid == 'Buck_manual': - # Bucks vapor pressure formulation based on Tetens formula - # Source: Buck Research, Model CR-1A Hygrometer Operating - # Manual, Sep 2001 - tc = t - 273.15 - e_sat[idx_liq] = 6.1121 * numpy.exp((18.678 - (tc[idx_liq] / 234.5)) * - (tc[idx_liq]) / (257.14 + tc[idx_liq])) - - elif liquid == 'WMO_Goff': - # Intended WMO formulation, originally published by Goff (1957) - # incorrectly referenced by WMO technical regulations, WMO-NO 49, - # Vol I, General Meteorological Standards and Recommended - # Practices, App. A, Corrigendum Aug 2000. - # and incorrectly referenced by WMO technical regulations, - # WMO-NO 49, Vol I, General Meteorological Standards and - # Recommended Practices, App. A, 1988. - ts = 273.16 # steam point temperature in K - e_sat[idx_liq] = 10. ** (10.79574 * (1. - (ts / t[idx_liq])) - - 5.02800 * numpy.log10((t[idx_liq] / ts)) + - 1.50475E-4 * (1. - 10. ** (-8.2969 * ((t[idx_liq] / ts) - 1.))) + - 0.42873E-3 * (10. ** (+4.76955 * (1. - (ts / t[idx_liq]))) - 1.) + - 0.78614) - - elif liquid == 'WMO2000': - # WMO formulation, which is very similar to Goff Gratch - # Source: WMO technical regulations, WMO-NO 49, Vol I, - # General Meteorological Standards and Recommended Practices, - # App. A, Corrigendum Aug 2000. - ts = 273.16 # steam point temperature in K - e_sat[idx_liq] = 10. ** (10.79574 * (1. - (ts / t[idx_liq])) - - 5.02800 * numpy.log10((t[idx_liq] / ts)) + - 1.50475E-4 * (1. - 10. ** (-8.2969 * ((t[idx_liq] / ts) - 1.))) + - 0.42873E-3 * (10. ** (-4.76955 * (1. - (ts / t[idx_liq]))) - 1.) + - 0.78614) - - elif liquid == 'Sonntag': - # Source: Sonntag, D., Advancements in the field of hygrometry, - # Meteorol. Z., N. F., 3, 51-66, 1994. - e_sat[idx_liq] = numpy.exp(-6096.9385 * t[idx_liq] ** (-1.) + - 16.635794 - - 2.711193E-2 * t[idx_liq] ** 1. + - 1.673952E-5 * t[idx_liq] ** 2. + - 2.433502 * numpy.log(t[idx_liq])) - - elif liquid == 'Bolton': - # Source: Bolton, D., The computation of equivalent potential - # temperature, Monthly Weather Report, 108, 1046-1053, 1980. - # equation (10) - tc = t - 273.15 - e_sat[idx_liq] = 6.112 * numpy.exp(17.67 * tc[idx_liq] / (tc[idx_liq] + 243.5)) - - # THIS CURVE LOOKS WRONG! - # elif liquid == 'Fukuta': - # # Source: Fukuta, N. and C. M. Gramada, Vapor pressure - # # measurement of supercooled water, J. Atmos. Sci., 60, - # # 1871-1875, 2003. - # # This paper does not give a vapor pressure formulation, - # # but rather a correction over the Smithsonian Tables. - # # Thus calculate the table value first, then use the - # # correction to get to the measured value. - # ts = 373.16 # steam point temperature in K - # ews = 1013.246 # saturation pressure at steam point - # # temperature, normal atmosphere - - # e_sat[idx_liq] = 10.**(-7.90298*(ts/t[idx_liq]-1.) - # + 5.02808 * numpy.log10(ts/t[idx_liq]) - # - 1.3816E-7 * (10.**(11.344*(1.-t[idx_liq]/ts))-1.) - # + 8.1328E-3*(10.**(-3.49149*(ts/t[idx_liq]-1)) -1.) - # + numpy.log10(ews)) - - # tc = t - 273.15 - # x = tc[idx_liq] + 19 - # e_sat[idx_liq] = e_sat[idx_liq] * (0.9992 + 7.113E-4*x - # - 1.847E-4*x**2. - # + 1.189E-5*x**3. - # + 1.130E-7*x**4. - # - 1.743E-8*x**5.) - - # e_sat[numpy.where(tc < -39.)] = None - - elif liquid == 'IAPWS': - # Source: Wagner W. and A. Pruss (2002), The IAPWS - # formulation 1995 for the thermodynamic properties - # of ordinary water substance for general and scientific - # use, J. Phys. Chem. Ref. Data, 31(2), 387-535. - # This is the 'official' formulation from the International - # Association for the Properties of Water and Steam - # The valid range of this formulation is 273.16 <= T <= - # 647.096 K and is based on the ITS90 temperature scale. - Tc = 647.096 # K : Temperature at the critical point - Pc = 22.064 * 10 ** 4 # hPa : Vapor pressure at the critical point - nu = (1. - (t[idx_liq] / Tc)) - a1 = -7.85951783 - a2 = 1.84408259 - a3 = -11.7866497 - a4 = 22.6807411 - a5 = -15.9618719 - a6 = 1.80122502 - e_sat[idx_liq] = Pc * numpy.exp(Tc / t[idx_liq] * - (a1 * nu + a2 * nu ** 1.5 + a3 * nu ** 3. + - a4 * nu ** 3.5 + a5 * nu ** 4. + a6 * nu ** 7.5)) - - elif liquid == 'MurphyKoop': - # Source : Murphy and Koop, Review of the vapour pressure - # of ice and supercooled water for atmospheric applications, - # Q. J. R. Meteorol. Soc (2005), 131, pp. 1539-1565. - e_sat[idx_liq] = (numpy.exp(54.842763 - (6763.22 / t[idx_liq]) - - 4.210 * numpy.log(t[idx_liq]) + - 0.000367 * t[idx_liq] + - numpy.tanh(0.0415 * (t[idx_liq] - 218.8)) * - (53.878 - (1331.22 / t[idx_liq]) - - 9.44523 * numpy.log(t[idx_liq]) + - 0.014025 * t[idx_liq])) / 100.) - - else: - raise VapourPressureError("Unkown method for computing " - f"the vapour pressure curve over liquid: {liquid}") - - # ============================================================================= - # Calculate saturation pressure over ice ------------------------------------- - if not force_phase == 'liquid': - - if ice == 'WMO2000': - ice = 'WMO_Goff' - - if ice == 'IAWPS': - raise VapourPressureError("IAPWS does not provide a vapour " - "pressure formulation over ice") - - elif ice == 'MartiMauersberger': - # Source: Marti, J. and K Mauersberger, A survey and new - # measurements of ice vapor pressure at temperatures between - # 170 and 250 K, GRL 20, 363-366, 1993. - e_sat[idx_ice] = (10. ** ((-2663.5 / t[idx_ice]) + 12.537) / 100.) - - elif ice == 'HylandWexler': - # Source Hyland, R. W. and A. Wexler, Formulations for the - # Thermodynamic Properties of the saturated Phases of H2O - # from 173.15K to 473.15K, ASHRAE Trans, 89(2A), 500-519, 1983. - e_sat[idx_ice] = (numpy.exp((-0.56745359E4 / t[idx_ice]) + - 0.63925247E1 - - 0.96778430E-2 * t[idx_ice] + - 0.62215701E-6 * t[idx_ice] ** 2. + - 0.20747825E-8 * t[idx_ice] ** 3. - - 0.94840240E-12 * t[idx_ice] ** 4. + - 0.41635019E1 * numpy.log(t[idx_ice])) / 100.) - - elif ice == 'GoffGratch': - # Source: Smithsonian Meteorological Tables, 5th edition, - # p. 350, 1984 - - ei0 = 6.1071 # mbar - T0 = 273.16 # freezing point in K - - e_sat[idx_ice] = 10. ** (-9.09718 * ((T0 / t[idx_ice]) - 1.) - - 3.56654 * numpy.log10((T0 / t[idx_ice])) + - 0.876793 * (1. - (t[idx_ice] / T0)) + - numpy.log10(ei0)) - - elif ice == 'MagnusTeten': - # Source: Murray, F. W., On the computation of saturation - # vapour pressure, J. Appl. Meteorol., 6, 203-204, 1967. - tc = t - 273.15 - e_sat[idx_ice] = 10. ** (9.5 * tc[idx_ice] / (265.5 + tc[idx_ice]) + 0.7858) - - elif ice == 'Buck_original': - # Bucks vapor pressure formulation based on Tetens formula - # Source: Buck, A. L., New equations for computing vapor - # pressure and enhancement factor, J. Appl. Meteorol., 20, - # 1527-1532, 1981. - tc = t - 273.15 - e_sat[idx_ice] = 6.1115 * numpy.exp(22.452 * tc[idx_ice] / (272.55 + tc[idx_ice])) - - elif ice == 'Buck_manual': - # Bucks vapor pressure formulation based on Tetens formula - # Source: Buck Research, Model CR-1A Hygrometer Operating - # Manual, Sep 2001 - tc = t - 273.15 - e_sat[idx_ice] = 6.1115 * numpy.exp((23.036 - (tc[idx_ice] / 333.7)) * - tc[idx_ice] / (279.82 + tc[idx_ice])) - - elif ice == 'WMO_Goff': - # WMO formulation, which is very similar to Goff Gratch - # Source: WMO technical regulations, WMO-NO 49, Vol I, - # General Meteorological Standards and Recommended Practices, - # Aug 2000, App. A. - - T0 = 273.16 # steam point temperature in K - - e_sat[idx_ice] = 10. ** (-9.09685 * ((T0 / t[idx_ice]) - 1.) - - 3.56654 * numpy.log10((T0 / t[idx_ice])) + - 0.87682 * (1. - (t[idx_ice] / T0)) + 0.78614) - - elif ice == 'Sonntag': - # Source: Sonntag, D., Advancements in the field of hygrometry, - # Meteorol. Z., N. F., 3, 51-66, 1994. - e_sat[idx_ice] = numpy.exp(-6024.5282 * t[idx_ice] ** (-1.) + - 24.721994 + - 1.0613868E-2 * t[idx_ice] ** 1. - - 1.3198825E-5 * t[idx_ice] ** 2. - - 0.49382577 * numpy.log(t[idx_ice])) - - elif ice == 'MurphyKoop': - # Source: Murphy and Koop, Review of the vapour pressure of ice - # and supercooled water for atmospheric applications, Q. J. R. - # Meteorol. Soc (2005), 131, pp. 1539-1565. - e_sat[idx_ice] = (numpy.exp(9.550426 - (5723.265 / t[idx_ice]) + - 3.53068 * numpy.log(t[idx_ice]) - - 0.00728332 * t[idx_ice]) / 100.) - - else: - raise VapourPressureError("Unkown method for computing " - f"the vapour pressure curve over ice: {ice}") - - # Convert return value units from hPa to Pa. - return e_sat * 100. if not input_scalar else e_sat[0] * 100. - - -def rel_hum(p, t, q, liquid='HylandWexler', ice='GoffGratch', - force_phase='None'): +def rel_hum(p, t, q): """Compute relative humidity in [%] from pressure, temperature, and specific humidity. @@ -398,39 +65,17 @@ def rel_hum(p, t, q, liquid='HylandWexler', ice='GoffGratch', t -- temperature in [K] q -- specific humidity in [kg/kg] - p, t and q can be scalars of NumPy arrays. They just have to either all - scalars, or all arrays. - - liquid, ice, force_phase -- optional keywords to control the calculation - of the saturation vapour pressure; see - help of function 'sat_vapour_pressure()' for - details. - Returns: Relative humidity in [%]. Same dimension as input fields. """ - if not (numpy.isscalar(p) or numpy.isscalar(t) or numpy.isscalar(q)): - if not isinstance(p, numpy.ndarray): - p = numpy.array(p) - if not isinstance(t, numpy.ndarray): - t = numpy.array(t) - if not isinstance(q, numpy.ndarray): - q = numpy.array(q) - - # Compute mixing ratio w from specific humidiy q. - w = q / (1. - q) - - # Compute saturation vapour pressure from temperature t. - e_sat = sat_vapour_pressure(t, liquid=liquid, ice=ice, - force_phase=force_phase) + p = units.Quantity(p, "Pa") + t = units.Quantity(t, "K") + rel_humidity = mpcalc.relative_humidity_from_specific_humidity(p, t, q) - # Compute saturation mixing ratio from e_sat and pressure p. - w_sat = 0.622 * e_sat / (p - e_sat) + # Return specific humidity in [%]. + return rel_humidity * 100 - # Return the relative humidity, computed from w and w_sat. - return 100. * w / w_sat - -def virt_temp(t, q, method='exact'): +def virt_temp(t, q): """ Compute virtual temperature in [K] from temperature and specific humidity. @@ -442,33 +87,12 @@ def virt_temp(t, q, method='exact'): t and q can be scalars of NumPy arrays. They just have to either all scalars, or all arrays. - method -- optional keyword to specify the equation used. Default is - 'exact', which uses - Tv = T * (q + 0.622(1-q)) / 0.622, - 'approx' uses - Tv = T * (1 + 0.61w), - with w = q/(1-q) being the water vapour mixing ratio. - - Reference: Wallace&Hobbs 2nd ed., eq. 3.16, 3.59, and 3.60 - (substitute w=q/(1-q) in 3.16 and 3.59 to obtain the exact - formula). - Returns: Virtual temperature in [K]. Same dimension as input fields. """ - if not (numpy.isscalar(t) or numpy.isscalar(q)): - if not isinstance(t, numpy.ndarray): - t = numpy.array(t) - if not isinstance(q, numpy.ndarray): - q = numpy.array(q) - - if method == 'exact': - return t * (q + 0.622 * (1. - q)) / 0.622 - elif method == 'approx': - # Compute mixing ratio w from specific humidiy q. - w = q / (1. - q) - return t * (1. + 0.61 * w) - else: - raise TypeError('virtual temperature method not understood') + t = units.Quantity(t, "K") + mix_rat = mpcalc.mixing_ratio_from_specific_humidity(q) + v_temp = mpcalc.virtual_temperature(t, mix_rat) + return v_temp def geop_difference(p, t, method='trapz', axis=-1): @@ -521,129 +145,6 @@ def geop_difference(p, t, method='trapz', axis=-1): raise TypeError('integration method for geopotential not understood') -def geop_thickness(p, t, q=None, cumulative=False, axis=-1): - """ - Compute the geopotential thickness in [m] between the pressure levels - given by the first and last element in p (= pressure). - - Implements the hypsometric equation (1.18) from Holton, 3rd edition (or - alternatively (3.24) in Wallace and Hobbs, 2nd ed.). - - Arguments: - p -- pressure in [Pa] - t -- temperature in [K] - q -- [optional] specific humidity in [kg/kg]. If q is given, T will - be converted to virtual temperature to account for the effects - of moisture in the air. - - All inputs need to be NumPy arrays with at least 2 elements. - - cumulative -- optional keyword to specify whether the single geopotential - thickness between p[0] and p[-1] is returned (False, default), - or whether an array containing the thicknesses between - p[0] and all other elements in p is returned (True). The - latter option is useful for computing the geopotential height - of all model levels. - - axis -- see geop_difference(). - - Uses geop_difference() for the integral in the above equations. - - Returns: Geopotential thickness between p[0] and p[-1] in [m]. - If 'cumtrapz' is specified, an array of dimension dim(p)-1 - will be returned, in which value n represents the geopotential - thickness between p[0] and p[n+1]. - """ - - # Check whether humidity effects should be considered. If q is specified, - # simply evaluate the hypsometric equation with virtual temperature instead - # of absolute temperature (see Wallace and Hobbs, 2nd ed., section 3.2.1). - if q is None: - tv = t - else: - tv = virt_temp(t, q) - - # Evaluate equation 3.24 in Wallace and Hobbs: - # delta Z = -Rd/g0 * int( Tv, d ln(p), p1, p2 ), - # where Z denotes the geopotential height, Z = phi/g0. - return -1. / 9.80665 * geop_difference(p, tv, method='cumtrapz' if cumulative else 'trapz', axis=axis) - - -def spec_hum_from_pTd(p, td, liquid='HylandWexler'): - """ - Computes specific humidity in [kg/kg] from pressure and dew point - temperature. - - Arguments: - p -- pressure in [Pa] - td -- dew point temperature in [K] - - p and td can be scalars or NumPy arrays. They just have to either both - scalars, or both arrays. - - liquid -- optional keyword to specify the method used for computing the - saturation water wapour. See sat_vapour_pressure() for - further details. - - Returns: specific humidity in [kg/kg]. Same dimensions as the inputs. - - Method: - Specific humidity q = w / (1+w), with w = mixing ratio. (Wallace & Hobbs, - 2nd ed., (3.57)). W&H write: 'The dew point [Td] is the temperature at - which the saturation mixing ratio ws with respect to liquid water becomes - equal to the actual mixing ratio w.'. Hence we need ws(Td). - From W&H 3.62, we get ws = 0.622 * es / (p-es). Plugging this into the - above equation for q and simplifying, we get - q = 0.622 * es / (p + es * [0.622-1.]) - """ - # Compute saturation vapour pressure from dew point temperature td. - e_sat = sat_vapour_pressure(td, liquid=liquid) - - return 0.622 * e_sat / (p + e_sat * (0.622 - 1.)) - - -def dewpoint_approx(p, q, method='Bolton'): - """ - Computes dew point in [K] from pressure and specific humidity. - - Arguments: - p -- pressure in [Pa] - q -- specific humidity in [kg/kg] - - p and q can be scalars or NumPy arrays. They just have to either both - scalars, or both arrays. - - method -- optional keyword to specify the method used to approximate - the dew point temperature. Valid values are: - - 'Bolton' (default): Use the inversion of Bolton (1980), eq. - 10, to compute dewpoint. According to Bolton, this is accurate - to 0.03 K in the range 238..308 K. See also Emanuel (1994, - 'Atmospheric Convection', eq. 4.6.2). - - Returns: dew point temperature in [K]. - """ - if not (numpy.isscalar(p) or numpy.isscalar(q)): - if not isinstance(p, numpy.ndarray): - p = numpy.array(p) - if not isinstance(q, numpy.ndarray): - q = numpy.array(q) - - # Compute mixing ratio w from specific humidiy q. - w = q / (1. - q) - - # Compute vapour pressure from pressure and mixing ratio - # (Wallace and Hobbs 2nd ed. eq. 3.59). - e_q = w / (w + 0.622) * p - - if method == 'Bolton': - td = (243.5 / ((17.67 / numpy.log(e_q / 100. / 6.112)) - 1)) + 273.15 - else: - raise ValueError(f"invalid dew point method '{method}'") - - return td - - def pot_temp(p, t): """ Computes potential temperature in [K] from pressure and temperature. @@ -656,16 +157,14 @@ def pot_temp(p, t): scalars, or both arrays. Returns: potential temperature in [K]. Same dimensions as the inputs. - - Method: - theta = T * (p0/p)^(R/cp) - with p0 = 100000. Pa, R = 287.058 JK-1kg-1, cp = 1004 JK-1kg-1. """ - return t * (100000. / p) ** (287.058 / 1004.) + p = units.Quantity(p, "Pa") + t = units.Quantity(t, "K") + potential_temp = mpcalc.potential_temperature(p, t) + return potential_temp -def eqpt_approx(p, t, q, liquid='HylandWexler', ice='GoffGratch', - force_phase='None'): +def eqpt_approx(p, t, q): """ Computes equivalent potential temperature in [K] from pressure, temperature and specific humidity. @@ -679,31 +178,12 @@ def eqpt_approx(p, t, q, liquid='HylandWexler', ice='GoffGratch', Returns: equivalent potential temperature in [K]. Same dimensions as the inputs. - - Method: - theta_e = theta * exp((Lv*w_sat)/(cp*T)) - with theta = potential temperature (see pot_temp()), Lv = 2.25e6 Jkg-1, - cp = 1004 JK-1kg-1. - - Reference: Wallace & Hobbs, 2nd ed., eq. 3.71 """ - # Compute potential temperature from p and t. - theta = pot_temp(p, t) - - # Compute saturation vapour pressure from temperature t. - e_sat = sat_vapour_pressure(t, liquid=liquid, ice=ice, - force_phase=force_phase) - - # Compute saturation mixing ratio from e_sat and pressure p. - w_sat = 0.622 * e_sat / (p - e_sat) - - # Latent heat of evaporation. - Lv = 2.25 * 1.e6 - cp = 1004. - - # Equation 3.71 from Wallace & Hobbs, 2nd ed. - theta_e = theta * numpy.exp((Lv * w_sat) / (cp * t)) - return theta_e + p = units.Quantity(p, "Pa") + t = units.Quantity(t, "K") + dew_temp = mpcalc.dewpoint_from_specific_humidity(p, t, q) + eqpt_temp = mpcalc.equivalent_potential_temperature(p, t, dew_temp) + return eqpt_temp.to('degC').magnitude def omega_to_w(omega, p, t): @@ -718,18 +198,12 @@ def omega_to_w(omega, p, t): All inputs can be scalars or NumPy arrays. Returns the vertical velocity in geometric coordinates, [m/s]. - - For all grid points, the pressure vertical velocity in Pa/s is converted - to m/s via - w[m/s] =(approx) omega[Pa/s] / (-g*rho) - rho = p / R*T - with R = 287.058 JK-1kg-1, g = 9.80665 m2s-2. - (see p.13 of 'Introduction to circulating atmospheres' by Ian N. James). - - NOTE: Please check the resulting values, especially in the upper atmosphere! """ - rho = p / (287.058 * t) - return (omega / (-9.80665 * rho)) + omega = units.Quantity(omega, "Pa/s") + p = units.Quantity(p, "Pa") + t = units.Quantity(t, "K") + om_w = mpcalc.vertical_velocity(omega, p, t) + return om_w def flightlevel2pressure(flightlevel): From 4bf4882b61c917ededfc9183a56886ddb7be06ad Mon Sep 17 00:00:00 2001 From: May Date: Wed, 16 Jun 2021 17:02:33 +0200 Subject: [PATCH 11/37] Fix pyproj 3.1.0 error (#1033) --- mslib/msui/mpl_map.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mslib/msui/mpl_map.py b/mslib/msui/mpl_map.py index cc51b38e9..ae24365eb 100644 --- a/mslib/msui/mpl_map.py +++ b/mslib/msui/mpl_map.py @@ -561,7 +561,9 @@ def gcpoints_path(self, lons, lats, del_s=100., map_coords=True): # projection, gc.npts() returns lons that connect lon1 and lat2, not lon1 and # lon2 ... I cannot figure out why, maybe this is an issue in certain versions # of pyproj?? (mr, 16Oct2012) - lonlats = gc.npts(lons[i], lats[i], lons[i + 1], lats[i + 1], npoints) + lonlats = [] + if npoints > 0: + lonlats = gc.npts(lons[i], lats[i], lons[i + 1], lats[i + 1], npoints) # The cylindrical projection of matplotlib is not periodic, that means that # -170 longitude and 190 longitude are not identical. The gc projection however # assumes identity and maps all longitudes to -180 to 180. This is no issue for From 11656dd784a98599e11c3fd41b6bbccdc1b02853 Mon Sep 17 00:00:00 2001 From: open-mss-build <77272604+open-mss-build@users.noreply.github.com> Date: Fri, 28 May 2021 12:48:58 +0200 Subject: [PATCH 12/37] updated documentation (#1008) * updated documentation * improved install/update procedure Co-authored-by: Reimar Bauer --- AUTHORS | 4 +- docs/installation.rst | 35 +++++++------ docs/mscolab.rst | 50 ++++++++++++------- .../config/mss/mss_settings.json.sample | 25 ++++++++-- docs/usage.rst | 7 +-- 5 files changed, 79 insertions(+), 42 deletions(-) diff --git a/AUTHORS b/AUTHORS index 566c48461..8d0b883a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,6 +1,8 @@ Contributors ============ +in alphabetic order by first name + - Andreas Hilboll - Anveshan Lal @@ -10,10 +12,10 @@ Contributors - Jens-Uwe Grooß - Jörn Ungermann - Marc Rautenhaus +- May Bär - Reimar Bauer - Sakshi Chopkar - Shivashis Padhi - Tanish Grover - Thomas Breuer - Vaibhav Mehra -- May Bär diff --git a/docs/installation.rst b/docs/installation.rst index 6b98af578..4bae826d1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -70,7 +70,9 @@ variables. :: update ++++++ For updating an existing MSS installation to the current version, it is best to install -it into a new environment. +it into a new environment. If your current version is not far behind the new version +you could try the mamba update mss as described. + .. Important:: mamba is under development. All dependencies of MSS and MSS itselfs are under development. @@ -80,8 +82,12 @@ search for MSS what you can get :: (mssenv) $ mamba search mss - mss 3.0.3 py39hf3d152e_0 conda-forge - mss 3.0.3 py39hf3d152e_1 conda-forge + mss 3.0.4 py38h578d9bd_0 conda-forge + mss 3.0.4 py39hf3d152e_0 conda-forge + mss 4.0.0 py36h5fab9bb_0 conda-forge + mss 4.0.0 py37h89c1867_0 conda-forge + mss 4.0.0 py38h578d9bd_0 conda-forge + mss 4.0.0 py39hf3d152e_0 conda-forge compare what you have installed :: @@ -89,25 +95,22 @@ compare what you have installed :: mss 3.0.2 py39hf3d152e_0 conda-forge -If an existing environment shall be updated, it is important to update all packages in this environment. :: +We found that sometimes mss can be updated in an existing environment :: + + (mssenv) $ mamba update mss + +We have also reports that sometimes an update suceeds by giving by the install option and the new version number, +in this example 4.0.0 and python as second option :: - $ conda activate mssenv - (mssenv) $ mamba update --all + (mssenv) $ mamba install mss=4.0.0 python -In this example there was a further build done after the first release of 3.0.3. -Compare in the list of proposed updates what you would get :: +All attemmpts show what you get if you continue. **Continue only if you get what you want.** - matplotlib 3.4.2 py39hf3d152e_0 conda-forge - matplotlib-base 3.4.2 py39h2fa2bec_0 conda-forge - mss 3.0.3 py39hf3d152e_0 conda-forge - multidict 5.1.0 py39h3811e60_1 conda-forge +The alternative is to use a new environment and install mss. -If you see a mismatch like this, not getting the recent buildnumber. In this example value in -third column ends with "_1" you have to force the update by the conda command:: - (mssenv) $ conda install mss==3.0.3=py39hf3d152e_1 -For further details :ref:`mss-configuration` +For further details of configurating mss :ref:`mss-configuration` diff --git a/docs/mscolab.rst b/docs/mscolab.rst index a67bd2a48..945329b71 100644 --- a/docs/mscolab.rst +++ b/docs/mscolab.rst @@ -121,39 +121,48 @@ You can turn the `Work Locally` toggle off at any points and work on the common Notes for server administrators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you're configuring mscolab server, there isn't a GUI to add or manage a group of users. There is however a -proposal to bring this on around the next release of mss. For now, there is a command line tool available with the -installation of mss, :code:`mscolab_add_permissions`. It's usage is as follows +If you're configuring mscolab server, there is a manage users GUI to add or manage users to a project. +There is a command line tool available with the installation of mss, :code:`mscolab`. It can import users to the database +and can handle joins to projects. -- Make a text file with the following format +Make a text file with the following format to import many users to the mscolab database .. code-block:: text - path1 - u1-c - u2-c - u3-a + suggested_username name + suggested_username2 name2 - path2 - u1-a + .. code-block:: text - path3 - u2-v + $ mscolab db --users_by_file /path/to/file -- `path1` represents the path of project in mscolab db. -- u1, u2, u3 are usernames. -- `c` stands for collaborator, `a` for admin, `v` for viewer. -- Different paths are separated by 2 '\n's. -- The tool can be invocated anywhere by a command, where :code:`/path/to/file` represents the path to file created above. +After executed you get informations to exchange with users. .. code-block:: text - $ mscolab_add_permissions /path/to/file + Are you sure you want to add users to the database? (y/[n]): + y + Userdata: email suggested_username 30736d0350c9b886 + + "MSCOLAB_mailid": "email", + "MSCOLAB_password": "30736d0350c9b886", + + + Userdata: email2 suggested_username2 342434de34904303 + + "MSCOLAB_mailid": "email2", + "MSCOLAB_password": "342434de34904303", -instructions to use mscolab wsgi +Further options can be listed by `mscolab db -h` + + +Instructions to use mscolab wsgi ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ make a file called :code:`server.py` +and install :: + + mamba install eventlet==0.30.2 gunicorn **server.py**:: @@ -163,3 +172,6 @@ Then run the following commands. :: $ mamba install gunicorn eventlet $ gunicorn -b 0.0.0.0:8087 server:app + + +For further options read ``_ \ No newline at end of file diff --git a/docs/samples/config/mss/mss_settings.json.sample b/docs/samples/config/mss/mss_settings.json.sample index 4944e9c88..a34f573e6 100644 --- a/docs/samples/config/mss/mss_settings.json.sample +++ b/docs/samples/config/mss/mss_settings.json.sample @@ -1,6 +1,21 @@ { "data_dir": "~/mssdata", + "filepicker_default": "default", + + "import_plugins": { + "CSV": ["csv", "mslib.plugins.io.csv", "load_from_csv"], + "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"], + "Text": ["txt", "mslib.plugins.io.text", "load_from_txt"] + }, + + "export_plugins": { + "CSV": ["csv", "mslib.plugins.io.csv", "save_to_csv"], + "Text": ["txt", "mslib.plugins.io.text", "save_to_txt"], + "KML": ["kml", "mslib.plugins.io.kml", "save_to_kml"], + "GPX": ["gpx", "mslib.plugins.io.gpx", "save_to_gpx"] + }, + "layout": { "topview": [963, 702], @@ -53,10 +68,15 @@ "new_flighttrack_template": ["Kiruna", "Ny-Alesund"], "new_flighttrack_flightlevel": 250, + "num_interpolation_points": 201, + "num_labels": 10, + "WMS_request_timeout": 30, "default_WMS": ["http://www.your-server.de/forecasts"], "default_VSEC_WMS": ["http://www.your-server.de/forecasts"], + "default_LSEC_WMS": ["http://www.your-server.de/forecasts"], + "default_MSCOLAB": ["http://www.your-mscolab-server.de/"], "WMS_login": { "http://www.your-server.de/forecasts" : ["youruser", "yourpassword"] @@ -64,8 +84,7 @@ "MSC_login": { "http://www.your-mscolab-server.de" : ["youruser", "yourpassword"] }, - "num_interpolation_points": 201, - "num_labels": 10, - "filepicker_default": "default" + "MSCOLAB_mailid": "", + "MSCOLAB_password": "" } diff --git a/docs/usage.rst b/docs/usage.rst index 777badfb3..0b61ec13d 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -92,14 +92,15 @@ importing changed files back in addition to the main FTML format. These filters from the File menu of the Main Window. MSS currently offers several import/export filters in the mslib.plugins.io module, which may serve -as an example for the definition of own plugins. They are listed below. The CSV plugin is enabled -by default. Enabling the experimental FliteStar text import plugin would require those lines in +as an example for the definition of own plugins. Take care that added plugins use different file extensions. +They are listed below. The CSV plugin is enabled by default. +Enabling the experimental FliteStar text import plugin would require those lines in the UI settings file: .. code:: text "import_plugins": { - "FliteStar": ["txt", "mslib.plugins.io.flitestar", "load_from_flitestar"] + "FliteStar": ["fls", "mslib.plugins.io.flitestar", "load_from_flitestar"] }, The dictionary entry defines the name of the filter in the File menu. The list specifies in this From 19252609c5fd179fd110d26820f1e302d21416f2 Mon Sep 17 00:00:00 2001 From: Jatin Jain <72596619+Jatin2020-24@users.noreply.github.com> Date: Sat, 29 May 2021 15:31:26 +0530 Subject: [PATCH 13/37] Fixed: #999 (#1010) * .format replaced with f" string * updated copyright year --- conftest.py | 2 +- docs/samples/config/mscolab/mscolab_settings.py.sample | 1 + docs/samples/config/wms/mss_chem_plots.py | 8 ++++---- docs/samples/config/wms/mss_wms_auth.py.sample | 2 +- docs/samples/config/wms/mss_wms_settings.py.chem_plots | 2 +- mslib/_tests/constants.py | 2 +- mslib/mscolab/_tests/test_chat_manager.py | 1 + mslib/mscolab/_tests/test_file_manager.py | 1 + mslib/mscolab/_tests/test_server.py | 1 + mslib/msui/hexagon_dockwidget.py | 2 +- mslib/msui/mscolab_admin_window.py | 1 + mslib/msui/mscolab_version_history.py | 1 + mslib/msui/mss_pyui.py | 2 +- mslib/msui/multilayers.py | 6 +++--- mslib/mswms/demodata.py | 4 ++-- mslib/mswms/mpl_hsec.py | 2 +- 16 files changed, 22 insertions(+), 16 deletions(-) diff --git a/conftest.py b/conftest.py index 0944e8439..35b345dc9 100644 --- a/conftest.py +++ b/conftest.py @@ -78,7 +78,7 @@ def pytest_generate_tests(metafunc): This file is part of mss. :copyright: Copyright 2019 Shivashis Padhi - :copyright: Copyright 2019-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2019-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/mscolab/mscolab_settings.py.sample b/docs/samples/config/mscolab/mscolab_settings.py.sample index 824e2bdd2..3b80fa59b 100644 --- a/docs/samples/config/mscolab/mscolab_settings.py.sample +++ b/docs/samples/config/mscolab/mscolab_settings.py.sample @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2019 Shivashis Padhi + :copyright: Copyright 2019-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/wms/mss_chem_plots.py b/docs/samples/config/wms/mss_chem_plots.py index 021fdad98..e55508254 100644 --- a/docs/samples/config/wms/mss_chem_plots.py +++ b/docs/samples/config/wms/mss_chem_plots.py @@ -139,14 +139,14 @@ def make_msschem_hs_class( _contourname = "_pcontours" class fnord(HS_MSSChemStyle): - name = "HS_{}_{}{}".format(entity, vert, _contourname) + name = f"HS_{entity}_{vert}{_contourname}" dataname = entity units = units unit_scale = scale _title_tpl = nam + " (" + vert + ")" long_name = entity if units: - _title_tpl += " ({})".format(units) + _title_tpl += f"({units})" required_datafields = [(vert, entity, None)] + add_data contours = add_contours @@ -308,14 +308,14 @@ def make_msschem_vs_class( add_contours = [] class fnord(VS_MSSChemStyle): - name = "VS_{}_{}".format(entity, vert) + name = f"VS_{entity}_{vert} " dataname = entity units = units unit_scale = scale title = nam + " (" + vert + ")" long_name = entity if units: - title += " ({})".format(units) + title += f"({units})" required_datafields = [(vert, entity, None)] + add_data contours = add_contours if add_contours else [] diff --git a/docs/samples/config/wms/mss_wms_auth.py.sample b/docs/samples/config/wms/mss_wms_auth.py.sample index 042b5a3c2..a3e2dcf09 100644 --- a/docs/samples/config/wms/mss_wms_auth.py.sample +++ b/docs/samples/config/wms/mss_wms_auth.py.sample @@ -10,7 +10,7 @@ :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus - :copyright: Copyright 2016-2017 by the mss team, see AUTHORS. + :copyright: Copyright 2016-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/docs/samples/config/wms/mss_wms_settings.py.chem_plots b/docs/samples/config/wms/mss_wms_settings.py.chem_plots index 3d4e83871..8a8471f22 100644 --- a/docs/samples/config/wms/mss_wms_settings.py.chem_plots +++ b/docs/samples/config/wms/mss_wms_settings.py.chem_plots @@ -11,7 +11,7 @@ :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2019 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/_tests/constants.py b/mslib/_tests/constants.py index a54aa4ba2..2bac03775 100644 --- a/mslib/_tests/constants.py +++ b/mslib/_tests/constants.py @@ -40,7 +40,7 @@ CACHED_CONFIG_FILE = None SERVER_CONFIG_FILE = "mss_wms_settings.py" MSCOLAB_CONFIG_FILE = "mscolab_settings.py" -ROOT_FS = TempFS(identifier="mss{}".format(SHA)) +ROOT_FS = TempFS(identifier=f"mss{SHA}") OSFS_URL = ROOT_FS.geturl("", purpose="fs") ROOT_DIR = ROOT_FS.getsyspath("") diff --git a/mslib/mscolab/_tests/test_chat_manager.py b/mslib/mscolab/_tests/test_chat_manager.py index aa8cef5e7..8cae4485a 100644 --- a/mslib/mscolab/_tests/test_chat_manager.py +++ b/mslib/mscolab/_tests/test_chat_manager.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mscolab/_tests/test_file_manager.py b/mslib/mscolab/_tests/test_file_manager.py index bb863a0e0..25feaa735 100644 --- a/mslib/mscolab/_tests/test_file_manager.py +++ b/mslib/mscolab/_tests/test_file_manager.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mscolab/_tests/test_server.py b/mslib/mscolab/_tests/test_server.py index 19b7904d4..06284b691 100644 --- a/mslib/mscolab/_tests/test_server.py +++ b/mslib/mscolab/_tests/test_server.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: Copyright 2020 Reimar Bauer + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/hexagon_dockwidget.py b/mslib/msui/hexagon_dockwidget.py index 89a04f225..e61895a12 100644 --- a/mslib/msui/hexagon_dockwidget.py +++ b/mslib/msui/hexagon_dockwidget.py @@ -140,7 +140,7 @@ def _remove_hexagon(self): row_max = row + (7 - idx) if row_min < 0 or row_max > len(waypoints_model.all_waypoint_data()): raise HexagonException("Cannot remove hexagon, hexagon is not complete " - "(min, max = {:d}, {:d})".format(row_min, row_max)) + f"min, max = {row_min:d}, {row_max:d}") else: found_one = False for i in range(0, row_max - row_min): diff --git a/mslib/msui/mscolab_admin_window.py b/mslib/msui/mscolab_admin_window.py index 4031cf1de..f8c70fc18 100644 --- a/mslib/msui/mscolab_admin_window.py +++ b/mslib/msui/mscolab_admin_window.py @@ -9,6 +9,7 @@ This file is part of mss. :copyright: 2020 Tanish Grover + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/mscolab_version_history.py b/mslib/msui/mscolab_version_history.py index a00607570..af1ef98e0 100644 --- a/mslib/msui/mscolab_version_history.py +++ b/mslib/msui/mscolab_version_history.py @@ -10,6 +10,7 @@ This file is part of mss. :copyright: 2020 Tanish Grover + :copyright: Copyright 2020-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/msui/mss_pyui.py b/mslib/msui/mss_pyui.py index 44a40649c..2112eb795 100644 --- a/mslib/msui/mss_pyui.py +++ b/mslib/msui/mss_pyui.py @@ -198,7 +198,7 @@ def __init__(self, parent=None): """ super(MSS_AboutDialog, self).__init__(parent) self.setupUi(self) - self.lblVersion.setText("Version: {}".format(__version__)) + self.lblVersion.setText(f"Version: {__version__}") self.milestone_url = f'https://github.com/Open-MSS/MSS/issues?q=is%3Aclosed+milestone%3A{__version__[:-1]}' self.lblChanges.setText(f'New Features and Changes') blub = QtGui.QPixmap(python_powered()) diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index 8727ff670..839707455 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -562,7 +562,7 @@ def _parse_levels(self): if "elevation" in self.extents: units = self.dimensions["elevation"]["units"] values = self.extents["elevation"]["values"] - self.levels = ["{} ({})".format(e.strip(), units) for e in values] + self.levels = [f"{e.strip()} ({units})" for e in values] self.level = self.levels[0] def _parse_itimes(self): @@ -583,7 +583,7 @@ def _parse_itimes(self): logging.error(msg) QtWidgets.QMessageBox.critical( self.parent.dock_widget, self.parent.dock_widget.tr("Web Map Service"), - self.parent.dock_widget.tr("ERROR: {}".format(msg))) + self.parent.dock_widget.tr(f"ERROR: {msg}")) else: self.itime = self.itimes[-1] @@ -605,7 +605,7 @@ def _parse_vtimes(self): logging.error(msg) QtWidgets.QMessageBox.critical( self.parent.dock_widget, self.parent.dock_widget.tr("Web Map Service"), - self.parent.dock_widget.tr("ERROR: {}".format(msg))) + self.parent.dock_widget.tr(f"ERROR: {msg}")) else: if self.itime: self.vtime = next((vtime for vtime in self.vtimes if vtime >= self.itime), self.vtimes[0]) diff --git a/mslib/mswms/demodata.py b/mslib/mswms/demodata.py index a43f0bae8..19c2dd025 100644 --- a/mslib/mswms/demodata.py +++ b/mslib/mswms/demodata.py @@ -864,7 +864,7 @@ def create_server_config(self, detailed_information=False): :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); @@ -904,7 +904,7 @@ def create_server_config(self, detailed_information=False): :copyright: 2008-2014 Deutsches Zentrum fuer Luft- und Raumfahrt e.V. :copyright: 2011-2014 Marc Rautenhaus :copyright: Copyright 2017 Jens-Uwe Grooss, Joern Ungermann, Reimar Bauer - :copyright: Copyright 2017-2020 by the mss team, see AUTHORS. + :copyright: Copyright 2017-2021 by the mss team, see AUTHORS. :license: APACHE-2.0, see LICENSE for details. Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/mslib/mswms/mpl_hsec.py b/mslib/mswms/mpl_hsec.py index fa18795df..95f2da362 100644 --- a/mslib/mswms/mpl_hsec.py +++ b/mslib/mswms/mpl_hsec.py @@ -104,7 +104,7 @@ def supported_crs(self): "EPSG:4326", # WGS 84 / cylindric "MSS:stere"]) for code in self.supported_epsg_codes(): - crs_list.add("EPSG:{:d}".format(code)) + crs_list.add(f"EPSG:{code:d}") return sorted(crs_list) def _draw_auto_graticule(self, bm): From 32ec7c1bea05af000932806827e4f1b978d1fd55 Mon Sep 17 00:00:00 2001 From: Aryan Gupta <42470695+withoutwaxaryan@users.noreply.github.com> Date: Sat, 5 Jun 2021 14:06:29 +0530 Subject: [PATCH 14/37] fixes #1014 (#1015) --- docs/help.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/help.rst b/docs/help.rst index 239ca9ed4..123095351 100644 --- a/docs/help.rst +++ b/docs/help.rst @@ -9,12 +9,12 @@ MSCOLAB .. raw:: html - + KML Docking Widget ------------------ .. raw:: html - + From e375d7b2115d2e3f72a28d584752bf5dfc3543ee Mon Sep 17 00:00:00 2001 From: May Date: Tue, 8 Jun 2021 15:55:40 +0200 Subject: [PATCH 15/37] Remove Qt imports (#1019) --- mslib/msui/_tests/test_mscolab_project.py | 4 ++-- mslib/msui/mscolab_project.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mslib/msui/_tests/test_mscolab_project.py b/mslib/msui/_tests/test_mscolab_project.py index 79196633a..306b3848a 100644 --- a/mslib/msui/_tests/test_mscolab_project.py +++ b/mslib/msui/_tests/test_mscolab_project.py @@ -31,7 +31,7 @@ from mslib.msui.mscolab import MSSMscolabWindow from mslib.mscolab.conf import mscolab_settings from mslib.mscolab.models import Message -from PyQt5 import QtCore, QtTest, QtWidgets, Qt +from PyQt5 import QtCore, QtTest, QtWidgets from mslib._tests.utils import mscolab_start_server @@ -103,7 +103,7 @@ def test_copy_message(self): self._send_message("**test message**") self._send_message("**test message**") self._activate_context_menu_action(Actions.COPY) - assert Qt.QApplication.clipboard().text() == "**test message**" + assert QtWidgets.QApplication.clipboard().text() == "**test message**" def test_reply_message(self): self._send_message("**test message**") diff --git a/mslib/msui/mscolab_project.py b/mslib/msui/mscolab_project.py index 746df309d..8f81d66c2 100644 --- a/mslib/msui/mscolab_project.py +++ b/mslib/msui/mscolab_project.py @@ -33,7 +33,7 @@ from werkzeug.urls import url_join from mslib.mscolab.models import MessageType -from PyQt5 import Qt, QtCore, QtGui, QtWidgets +from PyQt5 import QtCore, QtGui, QtWidgets from mslib.msui.mss_qt import get_open_filename, get_save_filename from mslib.msui.qt5 import ui_mscolab_project_window as ui from mslib.utils import config_loader, show_popup @@ -293,7 +293,7 @@ def start_message_edit(self, message_text, message_id): self.active_edit_id = message_id self.messageText.setText(message_text) self.messageText.setFocus() - self.messageText.moveCursor(Qt.QTextCursor.End) + self.messageText.moveCursor(QtGui.QTextCursor.End) self.editMessageBtn.setVisible(True) self.cancelBtn.setVisible(True) self.sendMessageBtn.setVisible(False) @@ -621,7 +621,7 @@ def open_context_menu(self, pos): self.context_menu.exec_(self.messageBox.mapToGlobal(pos)) def handle_copy_action(self): - Qt.QApplication.clipboard().setText(self.message_text) + QtWidgets.QApplication.clipboard().setText(self.message_text) def handle_download_action(self): file_name = fs.path.basename(self.attachment_path) @@ -668,7 +668,7 @@ def set_selected(self, selected): def on_link_click(self, url): if url.scheme() == "": url.setScheme("http") - Qt.QDesktopServices.openUrl(url) + QtGui.QDesktopServices.openUrl(url) # Deregister all the syntax that we don't want to allow From b3535f7a3996ce55c8b1414984ed71370e46f8c2 Mon Sep 17 00:00:00 2001 From: Aryan Gupta <42470695+withoutwaxaryan@users.noreply.github.com> Date: Wed, 9 Jun 2021 11:16:33 +0530 Subject: [PATCH 16/37] Fixes #1014 changing http to https (#1017) Co-authored-by: ReimarBauer --- docs/help.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/help.rst b/docs/help.rst index 123095351..5965a1e6f 100644 --- a/docs/help.rst +++ b/docs/help.rst @@ -9,12 +9,12 @@ MSCOLAB .. raw:: html - + KML Docking Widget ------------------ .. raw:: html - + From 15fcef98d72048303daf6be13e8e4507b463fe32 Mon Sep 17 00:00:00 2001 From: Aravind Murali Date: Wed, 9 Jun 2021 17:50:59 +0530 Subject: [PATCH 17/37] Linear view mscolab bug (#1021) * fixed tableview not opening bug; added test * added raising of mscolab window after closing view --- mslib/msui/mscolab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index 085845d37..0932f7388 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -1274,6 +1274,7 @@ def handle_view_close(self, value): for index, window in enumerate(self.active_windows): if window._id == value: del self.active_windows[index] + self.raise_() def setIdentifier(self, identifier): self.identifier = identifier From c1c13c398d8afcac4721dd5f558e09066d3fa35e Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Thu, 10 Jun 2021 16:16:43 +0200 Subject: [PATCH 18/37] preparation of v4.0.1 (#1023) * preparation of v4.0.1 * updated install instruction Co-authored-by: J. Ungermann --- CHANGES.rst | 8 ++++++++ docs/development.rst | 2 +- docs/installation.rst | 23 ++++++++--------------- mslib/version.py | 2 +- 4 files changed, 18 insertions(+), 17 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index afc3db2f4..e6964b48d 100755 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changelog ========= +Version 4.0.1 +~~~~~~~~~~~~~ + +Bug Fix release + +All changes: +https://github.com/Open-MSS/MSS/milestone/58?closed=1 + Version 4.0.0 ------------- diff --git a/docs/development.rst b/docs/development.rst index b53392a8b..2b33532f1 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -53,7 +53,7 @@ Create an environment and install the whole mss package dependencies then remove $ conda create -n mssdev mamba $ conda activate mssdev - $ mamba install mss --only-deps + $ mamba install mss=4.0.1 --only-deps You can also use conda to install mss, but mamba is a way faster. Compare versions used in the meta.yaml between stable and develop branch and apply needed changes. diff --git a/docs/installation.rst b/docs/installation.rst index 4bae826d1..4b6bb26ac 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -56,7 +56,7 @@ leave out the 'source' here and below). :: $ conda create -n mssenv mamba $ conda activate mssenv - (mssenv) $ mamba install mss + (mssenv) $ mamba install mss=4.0.1 python You need to reactivate after the installation once the environment to setup all needed enironment @@ -81,13 +81,9 @@ you could try the mamba update mss as described. search for MSS what you can get :: (mssenv) $ mamba search mss - - mss 3.0.4 py38h578d9bd_0 conda-forge - mss 3.0.4 py39hf3d152e_0 conda-forge - mss 4.0.0 py36h5fab9bb_0 conda-forge - mss 4.0.0 py37h89c1867_0 conda-forge - mss 4.0.0 py38h578d9bd_0 conda-forge - mss 4.0.0 py39hf3d152e_0 conda-forge + ... + mss 4.0.1 py38h578d9bd_0 conda-forge + mss 4.0.1 py39hf3d152e_0 conda-forge compare what you have installed :: @@ -95,14 +91,11 @@ compare what you have installed :: mss 3.0.2 py39hf3d152e_0 conda-forge -We found that sometimes mss can be updated in an existing environment :: - - (mssenv) $ mamba update mss -We have also reports that sometimes an update suceeds by giving by the install option and the new version number, -in this example 4.0.0 and python as second option :: +We have reports that often an update suceeds by using the install option and the new version number, +in this example 4.0.1 and python as second option :: - (mssenv) $ mamba install mss=4.0.0 python + (mssenv) $ mamba install mss=4.0.1 python All attemmpts show what you get if you continue. **Continue only if you get what you want.** @@ -130,7 +123,7 @@ We suggest to create a mss user. * login again or export PATH="/home/mss/miniconda3/bin:$PATH" * conda create -n mssenv mamba * conda activate mssenv -* mamba install mss +* mamba install mss=4.0.1 python For a simple test you could start the builtin standalone *mswms* and *mscolab* server:: diff --git a/mslib/version.py b/mslib/version.py index ee3ac7860..a570979c4 100644 --- a/mslib/version.py +++ b/mslib/version.py @@ -24,4 +24,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__ = u'4.0.0.' +__version__ = u'4.0.1.' From 302a18651162b16898562bd2d26e42fb126e7c74 Mon Sep 17 00:00:00 2001 From: May Date: Wed, 16 Jun 2021 17:02:33 +0200 Subject: [PATCH 19/37] Fix pyproj 3.1.0 error (#1033) --- mslib/msui/mpl_map.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mslib/msui/mpl_map.py b/mslib/msui/mpl_map.py index 547186869..7102cbd07 100644 --- a/mslib/msui/mpl_map.py +++ b/mslib/msui/mpl_map.py @@ -558,7 +558,9 @@ def gcpoints_path(self, lons, lats, del_s=100., map_coords=True): # projection, gc.npts() returns lons that connect lon1 and lat2, not lon1 and # lon2 ... I cannot figure out why, maybe this is an issue in certain versions # of pyproj?? (mr, 16Oct2012) - lonlats = gc.npts(lons[i], lats[i], lons[i + 1], lats[i + 1], npoints) + lonlats = [] + if npoints > 0: + lonlats = gc.npts(lons[i], lats[i], lons[i + 1], lats[i + 1], npoints) # The cylindrical projection of matplotlib is not periodic, that means that # -170 longitude and 190 longitude are not identical. The gc projection however # assumes identity and maps all longitudes to -180 to 180. This is no issue for From c1e538535c5673189b9dc92eb9b4c220cf7f73d9 Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Fri, 18 Jun 2021 08:55:30 +0200 Subject: [PATCH 20/37] removed fixation for sqlalchemy and sqlite (#1038) --- localbuild/meta.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/localbuild/meta.yaml b/localbuild/meta.yaml index 5fa0abedf..c6f83f895 100644 --- a/localbuild/meta.yaml +++ b/localbuild/meta.yaml @@ -77,8 +77,6 @@ requirements: - xstatic-bootstrap - pyperclip - geos <3.9.0 - - sqlalchemy <1.4.0 - - sqlite <3.35.1 - gpxpy >=1.4.2 test: From cc43a66a0b4b7c5c812bffea83adafd5011c3048 Mon Sep 17 00:00:00 2001 From: "J. Ungermann" Date: Fri, 18 Jun 2021 19:46:54 +0200 Subject: [PATCH 21/37] Handle all combinations from project name and crs and proper update. (#1041) Fix flake8 error. Fix #1037 --- mslib/msui/mpl_map.py | 20 +++++++++++--------- mslib/mswms/_tests/test_wms.py | 3 ++- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/mslib/msui/mpl_map.py b/mslib/msui/mpl_map.py index ae24365eb..afdc59513 100644 --- a/mslib/msui/mpl_map.py +++ b/mslib/msui/mpl_map.py @@ -133,16 +133,18 @@ def __init__(self, identifier=None, CRS=None, BBOX_UNITS=None, PROJECT_NAME=None self.image = None - # Print CRS identifier and project name into figure. - if self.crs is not None and self.project_name is not None: - self.crs_text = self.ax.figure.text(0, 0, f"{self.project_name}\n{self.crs}") + # Print project name and CRS identifier into figure. + crs_text = "" + if self.project_name is not None: + crs_text += self.project_name + if self.crs is not None: + if len(crs_text) > 0: + crs_text += "\n" + crs_text += self.crs + if hasattr(self, "crs_text"): # update existing textbox + self.crs_text.set_text(crs_text) else: - # Print only CRS identifier into the figure. - if self.crs is not None: - if hasattr(self, "crs_text"): - self.crs_text.set_text(self.crs) - else: - self.crs_text = self.ax.figure.text(0, 0, self.crs) + self.crs_text = self.ax.figure.text(0, 0, crs_text) if self.appearance["draw_graticule"]: try: diff --git a/mslib/mswms/_tests/test_wms.py b/mslib/mswms/_tests/test_wms.py index fcca8a2c3..bdff54194 100644 --- a/mslib/mswms/_tests/test_wms.py +++ b/mslib/mswms/_tests/test_wms.py @@ -271,7 +271,8 @@ def test_produce_lsec_service_exception(self): ("layers=ecmwf_EUR_LL015.LS_HV01", "layers=ecmwf_AUR_LL015.LS_HV01"), ("layers=ecmwf_EUR_LL015.LS_HV01", "layers=ecmwf_EUR_LL015.LS_HV99"), ("format=text%2Fxml", "format=oext%2Fxml"), - ("path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", "path=aaaa%2C-8.93%2C25000%2C48.08%2C11.28%2C25000"), + ("path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", + "path=aaaa%2C-8.93%2C25000%2C48.08%2C11.28%2C25000"), ("&path=52.78%2C-8.93%2C25000%2C48.08%2C11.28%2C25000", ""), ("bbox=201", "bbox=aaa")]: environ["QUERY_STRING"] = query_string.replace(orig, fake) From f3195cbb94c5961d8cbc603f29b5fe36702fd2f7 Mon Sep 17 00:00:00 2001 From: "J. Ungermann" Date: Fri, 18 Jun 2021 20:16:31 +0200 Subject: [PATCH 22/37] Fix behaviour for projects with dash "-" in name. (#1043) Fix #1036 Co-authored-by: ReimarBauer --- mslib/msui/_tests/test_mscolab.py | 6 ++++++ mslib/msui/mscolab.py | 15 +++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mslib/msui/_tests/test_mscolab.py b/mslib/msui/_tests/test_mscolab.py index 39a15257a..5a8471e82 100644 --- a/mslib/msui/_tests/test_mscolab.py +++ b/mslib/msui/_tests/test_mscolab.py @@ -224,6 +224,12 @@ def test_add_project(self): assert self.window.loginWidget.isVisible() is False self._create_project("Alpha", "Description Alpha") assert self.window.listProjects.model().rowCount() == 1 + self._create_project("reproduce-test", "Description Test") + assert self.window.listProjects.model().rowCount() == 2 + self._activate_project_at_index(0) + assert self.window.active_project_name == "Alpha" + self._activate_project_at_index(1) + assert self.window.active_project_name == "reproduce-test" def test_add_user(self): self._connect_to_mscolab() diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index d50799e09..1212c9ced 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -816,6 +816,7 @@ def add_projects_to_ui(self, projects): widgetItem = QtWidgets.QListWidgetItem(project_desc, parent=self.listProjects) widgetItem.p_id = project["p_id"] widgetItem.access_level = project["access_level"] + widgetItem.project_path = project["path"] if widgetItem.p_id == self.active_pid: selectedProject = widgetItem self.listProjects.addItem(widgetItem) @@ -845,7 +846,7 @@ def set_active_pid(self, item): # set active_pid here self.active_pid = item.p_id self.access_level = item.access_level - self.active_project_name = item.text().split("-")[0].strip() + self.active_project_name = item.project_path self.waypoints_model = None # set active flightpath here self.load_wps_from_server() @@ -1165,12 +1166,9 @@ def handle_update_permission(self, p_id, u_id, access_level): for i in range(self.listProjects.count()): item = self.listProjects.item(i) if item.p_id == p_id: - desc = item.text().split(' - ') - project_name = desc[0] - desc[-1] = access_level - desc = ' - '.join(desc) - item.setText(desc) + project_name = item.project_path item.access_level = access_level + item.setText('{project_name} - {item.access_level}') break if project_name is not None: show_popup(self, "Permission Updated", @@ -1212,10 +1210,11 @@ def delete_project_from_list(self, p_id): item = self.listProjects.item(i) if item.p_id == p_id: remove_item = item + break if remove_item is not None: - logging.debug("remove_item: %s" % remove_item) + logging.debug("remove_item: %s", remove_item) self.listProjects.takeItem(self.listProjects.row(remove_item)) - return remove_item.text().split(' - ')[0] + return remove_item.project_path @QtCore.Slot(int, int) def handle_revoke_permission(self, p_id, u_id): From c47666cf47b2511561504e6a2b98312bb12d07b0 Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Mon, 21 Jun 2021 19:06:02 +0200 Subject: [PATCH 23/37] preparation of v4.0.2 (#1044) * preparation of v4.0.2 * typo fixed --- CHANGES.rst | 8 ++++++++ docs/development.rst | 2 +- docs/installation.rst | 12 ++++++------ mslib/version.py | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index e6964b48d..5d1c7783f 100755 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changelog ========= +Version 4.0.2 +~~~~~~~~~~~~~ + +Bug fix release + +All changes: +https://github.com/Open-MSS/MSS/milestone/60?closed=1 + Version 4.0.1 ~~~~~~~~~~~~~ diff --git a/docs/development.rst b/docs/development.rst index 2b33532f1..7084c3261 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -53,7 +53,7 @@ Create an environment and install the whole mss package dependencies then remove $ conda create -n mssdev mamba $ conda activate mssdev - $ mamba install mss=4.0.1 --only-deps + $ mamba install mss=4.0.2 --only-deps You can also use conda to install mss, but mamba is a way faster. Compare versions used in the meta.yaml between stable and develop branch and apply needed changes. diff --git a/docs/installation.rst b/docs/installation.rst index 4b6bb26ac..705e93e6d 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -56,7 +56,7 @@ leave out the 'source' here and below). :: $ conda create -n mssenv mamba $ conda activate mssenv - (mssenv) $ mamba install mss=4.0.1 python + (mssenv) $ mamba install mss=4.0.2 python You need to reactivate after the installation once the environment to setup all needed enironment @@ -82,8 +82,8 @@ search for MSS what you can get :: (mssenv) $ mamba search mss ... - mss 4.0.1 py38h578d9bd_0 conda-forge - mss 4.0.1 py39hf3d152e_0 conda-forge + mss 4.0.2 py38....._0 conda-forge + mss 4.0.2 py39....._0 conda-forge compare what you have installed :: @@ -93,9 +93,9 @@ compare what you have installed :: We have reports that often an update suceeds by using the install option and the new version number, -in this example 4.0.1 and python as second option :: +in this example 4.0.2 and python as second option :: - (mssenv) $ mamba install mss=4.0.1 python + (mssenv) $ mamba install mss=4.0.2 python All attemmpts show what you get if you continue. **Continue only if you get what you want.** @@ -123,7 +123,7 @@ We suggest to create a mss user. * login again or export PATH="/home/mss/miniconda3/bin:$PATH" * conda create -n mssenv mamba * conda activate mssenv -* mamba install mss=4.0.1 python +* mamba install mss=4.0.2 python For a simple test you could start the builtin standalone *mswms* and *mscolab* server:: diff --git a/mslib/version.py b/mslib/version.py index a570979c4..2667892a6 100644 --- a/mslib/version.py +++ b/mslib/version.py @@ -24,4 +24,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__ = u'4.0.1.' +__version__ = u'4.0.2.' From 9b0ed4608f0f0a6f9151c9d2c48197cb71847fff Mon Sep 17 00:00:00 2001 From: Aravind Murali Date: Fri, 25 Jun 2021 17:02:49 +0530 Subject: [PATCH 24/37] fix for #1048 (#1049) --- mslib/msui/mscolab.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mslib/msui/mscolab.py b/mslib/msui/mscolab.py index 1212c9ced..8d26d94e6 100644 --- a/mslib/msui/mscolab.py +++ b/mslib/msui/mscolab.py @@ -1168,7 +1168,7 @@ def handle_update_permission(self, p_id, u_id, access_level): if item.p_id == p_id: project_name = item.project_path item.access_level = access_level - item.setText('{project_name} - {item.access_level}') + item.setText(f'{project_name} - {item.access_level}') break if project_name is not None: show_popup(self, "Permission Updated", From 5d5e5e04afc2b3e7f9712042b90df7da3225fa6d Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Fri, 25 Jun 2021 17:43:29 +0200 Subject: [PATCH 25/37] preparation of v4.0.3 (#1050) --- CHANGES.rst | 8 ++++++++ docs/development.rst | 2 +- docs/installation.rst | 12 ++++++------ mslib/version.py | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5d1c7783f..f03f08d09 100755 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,14 @@ Changelog ========= +Version 4.0.3 +~~~~~~~~~~~~~ + +Bug fix release + +All changes: +https://github.com/Open-MSS/MSS/milestone/62?closed=1 + Version 4.0.2 ~~~~~~~~~~~~~ diff --git a/docs/development.rst b/docs/development.rst index 7084c3261..b7b4b6323 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -53,7 +53,7 @@ Create an environment and install the whole mss package dependencies then remove $ conda create -n mssdev mamba $ conda activate mssdev - $ mamba install mss=4.0.2 --only-deps + $ mamba install mss=4.0.3 --only-deps You can also use conda to install mss, but mamba is a way faster. Compare versions used in the meta.yaml between stable and develop branch and apply needed changes. diff --git a/docs/installation.rst b/docs/installation.rst index 705e93e6d..9bd63bb30 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -56,7 +56,7 @@ leave out the 'source' here and below). :: $ conda create -n mssenv mamba $ conda activate mssenv - (mssenv) $ mamba install mss=4.0.2 python + (mssenv) $ mamba install mss=4.0.3 python You need to reactivate after the installation once the environment to setup all needed enironment @@ -82,8 +82,8 @@ search for MSS what you can get :: (mssenv) $ mamba search mss ... - mss 4.0.2 py38....._0 conda-forge - mss 4.0.2 py39....._0 conda-forge + mss 4.0.3 py38....._0 conda-forge + mss 4.0.3 py39....._0 conda-forge compare what you have installed :: @@ -93,9 +93,9 @@ compare what you have installed :: We have reports that often an update suceeds by using the install option and the new version number, -in this example 4.0.2 and python as second option :: +in this example 4.0.3 and python as second option :: - (mssenv) $ mamba install mss=4.0.2 python + (mssenv) $ mamba install mss=4.0.3 python All attemmpts show what you get if you continue. **Continue only if you get what you want.** @@ -123,7 +123,7 @@ We suggest to create a mss user. * login again or export PATH="/home/mss/miniconda3/bin:$PATH" * conda create -n mssenv mamba * conda activate mssenv -* mamba install mss=4.0.2 python +* mamba install mss=4.0.3 python For a simple test you could start the builtin standalone *mswms* and *mscolab* server:: diff --git a/mslib/version.py b/mslib/version.py index 2667892a6..ca72907fe 100644 --- a/mslib/version.py +++ b/mslib/version.py @@ -24,4 +24,4 @@ See the License for the specific language governing permissions and limitations under the License. """ -__version__ = u'4.0.2.' +__version__ = u'4.0.3.' From a80eb87d40c8e8d1ef668aa119b6e5d88e022a4b Mon Sep 17 00:00:00 2001 From: "J. Ungermann" Date: Mon, 28 Jun 2021 15:00:51 +0200 Subject: [PATCH 26/37] Minor bug fixes. (#1054) * Return proper unit for eq. potential temperature (degC->K) * Do not assume units of pressure levels for several old plotting styles. * Fix values of geopotential altitude in demodata --- mslib/mswms/_tests/test_demodata.py | 4 +- mslib/mswms/_tests/test_mss_plot_driver.py | 11 ++ mslib/mswms/demodata.py | 128 ++++++++++----------- mslib/mswms/mpl_hsec_styles.py | 9 +- mslib/thermolib.py | 2 +- 5 files changed, 84 insertions(+), 70 deletions(-) diff --git a/mslib/mswms/_tests/test_demodata.py b/mslib/mswms/_tests/test_demodata.py index ec10e886f..5d95bca69 100644 --- a/mslib/mswms/_tests/test_demodata.py +++ b/mslib/mswms/_tests/test_demodata.py @@ -50,8 +50,8 @@ def test_get_profile(self): assert np.allclose(std, [3.03, 6.31, 6.33]) mean, std = demodata.get_profile("air_potential_temperature", [300, 350, 400], "geopotential_height") - assert np.allclose(mean, [2953.99369248, 12007.44365002, 15285.95068235]) - assert np.allclose(std, [117.675343762, 248.127730176, 164.947761167]) + assert np.allclose(mean, [2970., 12026.92307692, 15307.89473684]) + assert np.allclose(std, [118, 247.69230769, 164.34210526]) mean, std = demodata.get_profile("ertel_potential_vorticity", [2, 4, 8], "geopotential_height") assert np.allclose(mean, [9565.89928058, 11148.1865285, 14139.43661972]) diff --git a/mslib/mswms/_tests/test_mss_plot_driver.py b/mslib/mswms/_tests/test_mss_plot_driver.py index 79c19a7b4..8d52a6751 100644 --- a/mslib/mswms/_tests/test_mss_plot_driver.py +++ b/mslib/mswms/_tests/test_mss_plot_driver.py @@ -309,6 +309,7 @@ def test_HS_CloudsStyle_01(self): img = self.plot(mpl_hsec_styles.HS_CloudsStyle_01(driver=self.hsec), style=style) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_CloudsStyle_01(driver=self.hsec), style=style, noframe=True) + assert noframe is not None assert noframe != img assert not is_image_transparent(img) @@ -321,12 +322,14 @@ def test_HS_MSLPStyle_01(self): img = self.plot(mpl_hsec_styles.HS_MSLPStyle_01(driver=self.hsec)) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_MSLPStyle_01(driver=self.hsec), noframe=True) + assert noframe is not None assert noframe != img def test_HS_SEAStyle_01(self): img = self.plot(mpl_hsec_styles.HS_SEAStyle_01(driver=self.hsec)) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_SEAStyle_01(driver=self.hsec), noframe=True) + assert noframe is not None assert noframe != img @pytest.mark.parametrize("style", ["PCOL", "CONT"]) @@ -334,24 +337,28 @@ def test_HS_SeaIceStyle_01(self, style): img = self.plot(mpl_hsec_styles.HS_SeaIceStyle_01(driver=self.hsec), style=style) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_SeaIceStyle_01(driver=self.hsec), style=style, noframe=True) + assert noframe is not None assert noframe != img def test_HS_TemperatureStyle_ML_01(self): img = self.plot(mpl_hsec_styles.HS_TemperatureStyle_ML_01(driver=self.hsec), level=10) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_TemperatureStyle_ML_01(driver=self.hsec), level=10, noframe=True) + assert noframe is not None assert noframe != img def test_HS_TemperatureStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_TemperatureStyle_PL_01(driver=self.hsec), level=800) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_TemperatureStyle_PL_01(driver=self.hsec), level=800, noframe=True) + assert noframe is not None assert noframe != img def test_HS_GeopotentialWindStyle_PL(self): img = self.plot(mpl_hsec_styles.HS_GeopotentialWindStyle_PL(driver=self.hsec), level=300) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_GeopotentialWindStyle_PL(driver=self.hsec), level=300, noframe=True) + assert noframe is not None assert noframe != img @pytest.mark.parametrize("style", ["default", "nonlinear", "auto", "log", "autolog"]) @@ -362,6 +369,7 @@ def test_HS_GenericStyle_styles(self, style): assert img is not None noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_mole_fraction_of_ozone_in_air(driver=self.hsec), level=300, style=style, noframe=True) + assert noframe is not None assert noframe != img def test_HS_GenericStyle_other(self): @@ -377,6 +385,7 @@ def test_HS_GenericStyle_other(self): assert img is not None noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_ertel_potential_vorticity(driver=self.hsec), level=300, style="ertel_potential_vorticity", noframe=True) + assert noframe is not None assert noframe != img img = self.plot( @@ -385,12 +394,14 @@ def test_HS_GenericStyle_other(self): assert img is not None noframe = self.plot(mpl_hsec_styles.HS_GenericStyle_PL_equivalent_latitude(driver=self.hsec), level=300, style="equivalent_latitude", noframe=True) + assert noframe is not None assert noframe != img def test_HS_RelativeHumidityStyle_PL_01(self): img = self.plot(mpl_hsec_styles.HS_RelativeHumidityStyle_PL_01(driver=self.hsec), level=300) assert img is not None noframe = self.plot(mpl_hsec_styles.HS_RelativeHumidityStyle_PL_01(driver=self.hsec), level=300, noframe=True) + assert noframe is not None assert noframe != img def test_HS_EQPTStyle_PL_01(self): diff --git a/mslib/mswms/demodata.py b/mslib/mswms/demodata.py index 19c2dd025..fceab92b7 100644 --- a/mslib/mswms/demodata.py +++ b/mslib/mswms/demodata.py @@ -137,12 +137,12 @@ 1.95e-01 4.09e-00 geopotential_height m - 5.95e+01 9.22e+01 - 5.27e+01 9.22e+01 - 4.73e+01 9.22e+01 - 4.20e+01 9.22e+01 - 3.55e+01 9.22e+01 - 3.08e+01 9.22e+01 + 5.95e+04 9.22e+01 + 5.27e+04 9.22e+01 + 4.73e+04 9.22e+01 + 4.20e+04 9.22e+01 + 3.55e+04 9.22e+01 + 3.08e+04 9.22e+01 2.57e+04 9.22e+01 2.33e+04 6.80e+01 2.01e+04 6.96e+01 @@ -195,24 +195,24 @@ 1.45e+03 1.83e+01 1.05e+03 2.14e+01 8.29e+02 1.12e+01 - 657.443538145 3.79 - 580.079898831 2.07 - 498.951941454 3.10 - 453.218730010 4.82 - 409.307918523 6.31 - 371.412271319 5.91 - 345.272674711 5.03 - 329.890732188 3.86 - 323.019997594 5.00 - 317.020226317 6.20 - 310.848481822 6.33 - 305.484573481 6.30 - 300.073050812 6.62 - 295.235341640 7.36 - 292.260568339 7.67 - 289.587551318 7.91 - 288.351954158 7.97 - 288.192732138 7.93 + 6.57e+02 3.79 + 5.80e+02 2.07 + 4.98e+02 3.10 + 4.53e+02 4.82 + 4.09e+02 6.31 + 3.71e+02 5.91 + 3.45e+02 5.03 + 3.29e+02 3.86 + 3.23e+02 5.00 + 3.17e+02 6.20 + 3.10e+02 6.33 + 3.05e+02 6.30 + 3.00e+02 6.62 + 2.95e+02 7.36 + 2.92e+02 7.67 + 2.89e+02 7.91 + 2.88e+02 7.97 + 2.87e+02 7.93 eastward_wind m.s^-1 4.68e+01 1.36e+01 @@ -281,16 +281,16 @@ 4.99e-06 3.43e-06 1.78e-05 1.58e-05 5.30e-05 4.71e-05 - 1.27e-04 1.14e-04 - 3.88e-04 3.59e-04 - 8.00e-04 7.17e-04 - 1.38e-03 1.16e-03 - 2.25e-03 1.63e-03 - 3.45e-03 2.02e-03 - 4.24e-03 2.18e-03 - 5.16e-03 2.37e-03 - 5.64e-03 2.44e-03 - 6.07e-03 2.55e-03 + 1.72e-04 1.74e-04 + 4.88e-04 4.59e-04 + 8.00e-04 1.17e-03 + 1.38e-03 2.16e-03 + 2.25e-03 2.63e-03 + 3.45e-03 3.02e-03 + 4.24e-03 3.18e-03 + 5.16e-03 3.37e-03 + 5.64e-03 3.44e-03 + 6.07e-03 3.55e-03 lagrangian_tendency_of_air_pressure Pa.s^-1 -4.38e-03 6.64e-02 @@ -319,30 +319,30 @@ 1.85e-02 3.18e-01 divergence_of_wind s^-1 - 2.66e-09 1.74e-05 - 1.00e-06 2.00e-05 - 4.04e-06 2.49e-05 - 2.20e-06 2.36e-05 - 3.60e-07 2.05e-05 - 3.64e-07 2.07e-05 - 2.19e-07 1.75e-05 - 2.29e-07 1.73e-05 - 2.93e-07 2.00e-05 - 2.17e-07 2.65e-05 - 2.76e-07 2.91e-05 - 4.83e-07 3.13e-05 - 2.32e-07 3.50e-05 - 1.98e-07 3.45e-05 - 2.86e-07 3.43e-05 - 8.41e-08 3.28e-05 - -8.39e-08 3.42e-05 - -2.00e-07 3.79e-05 - -2.87e-07 4.33e-05 - -2.19e-07 5.11e-05 - -8.42e-08 5.39e-05 - 1.39e-08 5.63e-05 - 4.93e-08 5.71e-05 - 8.47e-08 5.94e-05 + 2.66e-09 1.74e-04 + 1.00e-06 2.00e-04 + 4.04e-06 2.49e-04 + 2.20e-06 2.36e-04 + 3.60e-07 2.05e-04 + 3.64e-07 2.07e-04 + 2.19e-07 1.75e-04 + 2.29e-07 1.73e-04 + 2.93e-07 2.00e-04 + 2.17e-07 2.65e-04 + 2.76e-07 2.91e-04 + 4.83e-07 3.13e-04 + 2.32e-07 3.50e-04 + 1.98e-07 3.45e-04 + 2.86e-07 3.43e-04 + 8.41e-08 3.28e-04 + -8.39e-08 3.42e-04 + -2.00e-07 3.79e-04 + -2.87e-07 4.33e-04 + -2.19e-07 5.11e-04 + -8.42e-08 5.39e-04 + 1.39e-08 5.63e-04 + 4.93e-08 5.71e-04 + 8.47e-08 5.94e-04 mole_fraction_of_ozone_in_air kg.kg^-1 1.45e-06 7.50e-08 @@ -397,12 +397,12 @@ 6.09e-08 4.65e-09 equivalent_latitude degree - 4.30e+01 1.05e+01 - 4.48e+01 8.70e+00 - 4.58e+01 5.58e+00 - 4.21e+01 8.11e+00 - 4.37e+01 7.42e+00 - 4.57e+01 5.66e+00 + 43.0 1.05e+01 + 44.8 8.70e+00 + 45.8 5.58e+00 + 42.1 8.11e+00 + 43.7 7.42e+00 + 45.7 5.66e+00 80.09 1.21 50.70 5.69 30.46 4.69 diff --git a/mslib/mswms/mpl_hsec_styles.py b/mslib/mswms/mpl_hsec_styles.py index 2ad1ca356..b1c6ba919 100644 --- a/mslib/mswms/mpl_hsec_styles.py +++ b/mslib/mswms/mpl_hsec_styles.py @@ -753,8 +753,9 @@ def _prepare_datafields(self): """ Computes relative humidity from p, t, q. """ + pressure = convert_to(self.level, self.get_elevation_units(), "Pa") self.data["relative_humidity"] = thermolib.rel_hum( - self.level * 100., self.data["air_temperature"], self.data["specific_humidity"]) + pressure, self.data["air_temperature"], self.data["specific_humidity"]) def _plot_style(self): """ @@ -835,8 +836,9 @@ def _prepare_datafields(self): """ Computes relative humidity from p, t, q. """ + pressure = convert_to(self.level, self.get_elevation_units(), "Pa") self.data["equivalent_potential_temperature"] = thermolib.eqpt_approx( - self.level * 100., self.data["air_temperature"], self.data["specific_humidity"]) + pressure, self.data["air_temperature"], self.data["specific_humidity"]) self.data["equivalent_potential_temperature"] = convert_to( self.data["equivalent_potential_temperature"], "K", "degC") @@ -922,9 +924,10 @@ def _prepare_datafields(self): """ Computes relative humidity from p, t, q. """ + pressure = convert_to(self.level, self.get_elevation_units(), "Pa") self.data["upward_wind"] = thermolib.omega_to_w( self.data["lagrangian_tendency_of_air_pressure"], - self.level * 100., self.data["air_temperature"]) + pressure, self.data["air_temperature"]) self.data["upward_wind"] = convert_to(self.data["upward_wind"], "m/s", "cm/s") def _plot_style(self): diff --git a/mslib/thermolib.py b/mslib/thermolib.py index d9a094212..4eb0ba142 100644 --- a/mslib/thermolib.py +++ b/mslib/thermolib.py @@ -183,7 +183,7 @@ def eqpt_approx(p, t, q): t = units.Quantity(t, "K") dew_temp = mpcalc.dewpoint_from_specific_humidity(p, t, q) eqpt_temp = mpcalc.equivalent_potential_temperature(p, t, dew_temp) - return eqpt_temp.to('degC').magnitude + return eqpt_temp.to('K').magnitude def omega_to_w(omega, p, t): From 3b4fdf18b9e916e3c4a8ace5dc94ce9302ea5053 Mon Sep 17 00:00:00 2001 From: Archishman Sengupta <71402528+ArchishmanSengupta@users.noreply.github.com> Date: Mon, 28 Jun 2021 19:46:55 +0530 Subject: [PATCH 27/37] issue #719 solved (#729) * issue #719 solved * updated development.rst * updated development.rst Co-authored-by: ReimarBauer --- docs/development.rst | 117 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/docs/development.rst b/docs/development.rst index 2b33532f1..b3e834d9e 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -1,6 +1,97 @@ .. _development: +1. System requirements + + | Any system with basic configuration. + | Operating System : Any (Windows / Linux / Mac). + +2. Software requirement + | Python + | `Additional Requirements `_ + + +3. Skill set + | Knowledge of git & github + | python + +============================ +Setting Up local Environement +============================ + +============================ +Forking the Repo +============================ +1. Firstly you have to make your own copy of project. For that you have to fork the repository. You can find the fork button on the top-right side of the browser window. + +2. Kindly wait till it gets forked. + +3. After that copy will look like */MSS* forked from *Open-MSS/MSS*. + +============================ +Cloning the Repo +============================ + +1. Now you have your own copy of project. Here you have to start your work. + +2. Go to desired location on your computer where you want to set-up the project. + +3. Right click there and click on git bash. A terminal window will pop up + +4. Click The big green button which says "Code". Copy the URL. `Like this `_ + +5. Now Type the command ``git clone .git`` and hit enter. + +6. Wait for few seconds till the project gets copied + + or simply head over here for `cloning a repository `_ + +============================ +Setting up remote : +============================ + +1. Now you have to set up remote repositories + +2. Type ``git remote -v`` in terminal to list remote connections to your repo. + +3. It will show something like this: + + ``origin https://github.com//MSS.git`` (fetch) + + ``origin https://github.com//MSS.git`` (push) + +4. Now type the command git remote add upstream ``https://github.com/Open-MSS/MSS.git`` this will set upstream as main directory + +5. Again type in command git remote -v to check if remote has been set up correctly + +6. It should show something like this : + + ``origin https://github.com//MSS.git`` (fetch) + + ``origin https://github.com//MSS.git`` (push) + + upstream ``https://github.com/Open-MSS/MSS.git`` (fetch) + + upstream ``https://github.com/Open-MSS/MSS.git`` (push) + + +============================ +How to Report Bugs: +============================ +Please open a new issue in the appropriate GitHub repository `here `_ with steps to reproduce the problem you're experiencing. + +Be sure to include as much information including screenshots, text output, and both your expected and actual results. + +============================ +How to Request Enhancements: +============================ +First, please refer to the applicable `GitHub repository `_ and search `the repository's GitHub issues `_ to make sure your idea has not been (or is not still) considered. + +Then, please `create a new issue `_ in the GitHub repository describing your enhancement. + +Be sure to include as much detail as possible including step-by-step descriptions, specific examples, screenshots or mockups, and reasoning for why the enhancement might be worthwhile. + +============================ Development ============================ @@ -40,6 +131,7 @@ MSS repository needs a different folder, e.g. workspace/mss. Avoid to mix data a MSS is based on the software of the conda-forge channel located, so we have to add this channel to the default:: + $ conda config --add channels conda-forge Your content of the .condarc config file should have conda-forge on top:: @@ -256,3 +348,28 @@ Publish on Conda Forge * send a pull request * maintainer will merge if there is no error + +============================ +Pushing your changes: +============================ + +1. Now you have made the changes, tested them and built them. So now it's time to push them. +2. Goto your terminal and type git status and hit enter, this will show your changes from the files +3. Then type in git add and hit enter, this will add all the files to staging area +4. Commit the changes by ``git commit -m ""`` and hit enter. +5. Now push your branch to your fork by ``git push origin `` and hit enter. + + +============================ +Creating a pull request: +============================ +By this time you can see a message on your github fork as your fork is ahead of Open-MSS:develop by of commits and also you can see a button called Compare and pull request. + +Click on Compare and pull request button. + +You will see a template. + +Fill out the template completely by describing your change, cause of change, issue getting fixed etc. + +After filling the template completely click on Pull request + From 844f5add81bb0988beccd509ecff54524a8af3df Mon Sep 17 00:00:00 2001 From: May Date: Mon, 28 Jun 2021 16:55:06 +0200 Subject: [PATCH 28/37] Select newest init-time and carry over multilayering (#1047) Co-authored-by: J. Ungermann --- mslib/msui/multilayers.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/mslib/msui/multilayers.py b/mslib/msui/multilayers.py index 839707455..ccbf69480 100644 --- a/mslib/msui/multilayers.py +++ b/mslib/msui/multilayers.py @@ -181,14 +181,24 @@ def reload_sync(self): self.synced_reference.vtimes = vtimes self.synced_reference.allowed_crs = crs + if self.current_layer: + if not self.synced_reference.level: + self.synced_reference.level = self.current_layer.level + if not self.synced_reference.itime: + self.synced_reference.itime = self.current_layer.itime + if not self.synced_reference.vtime: + self.synced_reference.vtime = self.current_layer.vtime + if self.synced_reference.level not in self.synced_reference.levels: - self.synced_reference.level = self.synced_reference.levels[0] if self.synced_reference.levels else None + self.synced_reference.level = levels[0] if levels else None if self.synced_reference.itime not in self.synced_reference.itimes: - self.synced_reference.itime = self.synced_reference.itimes[0] if self.synced_reference.itimes else None + self.synced_reference.itime = itimes[-1] if itimes else None - if self.synced_reference.vtime not in self.synced_reference.vtimes: - self.synced_reference.vtime = self.synced_reference.vtimes[0] if self.synced_reference.vtimes else None + if self.synced_reference.vtime not in self.synced_reference.vtimes or \ + self.synced_reference.vtime < self.synced_reference.itime: + self.synced_reference.vtime = next((vtime for vtime in vtimes if + vtime >= self.synced_reference.itime), None) if vtimes else None def filter_multilayers(self, filter_string=None): """ From b707b2f5593e76c5c2531b7464a7246a4d6fa81a Mon Sep 17 00:00:00 2001 From: "J. Ungermann" Date: Wed, 30 Jun 2021 12:18:16 +0200 Subject: [PATCH 29/37] Fix some issues with elevation caching and model level dimensions. (#1061) Fix #1066 --- mslib/mswms/dataaccess.py | 3 ++- mslib/netCDF4tools.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mslib/mswms/dataaccess.py b/mslib/mswms/dataaccess.py index 20c92d823..4c3f59438 100644 --- a/mslib/mswms/dataaccess.py +++ b/mslib/mswms/dataaccess.py @@ -227,7 +227,6 @@ def _determine_filename(self, variable, vartype, init_time, valid_time, reload=T def _parse_file(self, filename): elevations = {"levels": [], "units": None} with netCDF4.Dataset(os.path.join(self._root_path, filename)) as dataset: - time_name, time_var = netCDF4tools.identify_CF_time(dataset) init_time = netCDF4tools.num2date(0, time_var.units) if not self.uses_inittime_dimension(): @@ -448,5 +447,7 @@ def setup(self): except IOError as ex: logging.error("Skipping file '%s' (%s: %s)", filename, type(ex), ex) continue + if content["vert_type"] not in self._elevations: + self._elevations[content["vert_type"]] = content["elevations"] self._file_cache[filename] = (mtime, content) self._add_to_filetree(filename, content) diff --git a/mslib/netCDF4tools.py b/mslib/netCDF4tools.py index 248bb07a1..11d7b3425 100644 --- a/mslib/netCDF4tools.py +++ b/mslib/netCDF4tools.py @@ -111,7 +111,7 @@ def identify_vertical_axis(dataset): name, var = identify_variable(dataset, standard_name) orientation = hybrid_orientation(var) if var is not None: - units = getattr(var, "units", "unknown units") + units = getattr(var, "units", "dimensionless") result.append((name, var, orientation, units, layertype)) if len(result) == 0: return None, None, None, None, "sfc" From 5efa673185116ec22a0243881e48a83c6271fc5d Mon Sep 17 00:00:00 2001 From: "J. Ungermann" Date: Wed, 30 Jun 2021 14:07:35 +0200 Subject: [PATCH 30/37] Added missing filename key. (#1062) See #1066 --- mslib/mswms/dataaccess.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mslib/mswms/dataaccess.py b/mslib/mswms/dataaccess.py index 4c3f59438..36cbf2ff2 100644 --- a/mslib/mswms/dataaccess.py +++ b/mslib/mswms/dataaccess.py @@ -225,7 +225,7 @@ def _determine_filename(self, variable, vartype, init_time, valid_time, reload=T raise ValueError(f"variable type {vartype} not available for variable {variable}") def _parse_file(self, filename): - elevations = {"levels": [], "units": None} + elevations = {"filename": filename, "levels": [], "units": None} with netCDF4.Dataset(os.path.join(self._root_path, filename)) as dataset: time_name, time_var = netCDF4tools.identify_CF_time(dataset) init_time = netCDF4tools.num2date(0, time_var.units) @@ -248,7 +248,10 @@ def _parse_file(self, filename): raise IOError("Problem with longitude coordinate variable") if vert_type != "sfc": - elevations = {"levels": vert_var[:], "units": getattr(vert_var, "units", "1")} + elevations = { + "filename": filename, + "levels": vert_var[:], + "units": getattr(vert_var, "units", "dimensionless")} if vert_type in self._elevations: if len(vert_var[:]) != len(self._elevations[vert_type]["levels"]): raise IOError(f"Number of vertical levels does not fit to levels of " @@ -419,7 +422,7 @@ def setup(self): del self._file_cache[filename] self._filetree = {} - self._elevations = {"sfc": {"filename": None, "levels": []}} + self._elevations = {"sfc": {"filename": None, "levels": [], "units": None}} # Build the tree structure. for filename in self._available_files: From e5171d7dccc07825eea0545f495d1d7410576f08 Mon Sep 17 00:00:00 2001 From: ReimarBauer Date: Thu, 1 Jul 2021 11:23:51 +0200 Subject: [PATCH 31/37] adjusted theme navigation for container-fluid (#1065) --- mslib/static/templates/theme.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mslib/static/templates/theme.html b/mslib/static/templates/theme.html index 10123037b..5d0b2ec4b 100644 --- a/mslib/static/templates/theme.html +++ b/mslib/static/templates/theme.html @@ -16,7 +16,7 @@