Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving GUI out of the controllers #1241

Merged
merged 36 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
0b4a4bd
Moved database table actions to new ABAction format
mrvisscher Feb 20, 2024
b504577
Added kwargs to ABActions
mrvisscher Feb 21, 2024
94ebe52
Moved database actions to new ABAction format
mrvisscher Feb 21, 2024
1962ab8
Fixed Python 3.8 typing issue
mrvisscher Feb 21, 2024
90710ce
Fixed Python 3.8 typing issue
mrvisscher Feb 21, 2024
234ab91
Fixed Python 3.8 typing issue
mrvisscher Feb 21, 2024
41cdb75
Moved CS actions to new ABAction format
mrvisscher Feb 21, 2024
09575fd
Moved Parameter actions to new ABAction format
mrvisscher Feb 22, 2024
db03524
Refactor actions into folders
mrvisscher Feb 22, 2024
546dad9
Moved project actions to new ABAction format
mrvisscher Feb 22, 2024
df88f25
Finished activity_duplicate_to_loc ABAction
mrvisscher Feb 26, 2024
c47ba3f
Moved plugins and utilities to new ABAction format
mrvisscher Feb 26, 2024
1b28814
Moved exchange actions to new ABAction format
mrvisscher Feb 26, 2024
85cfd29
Added docstrings to all ABActions
mrvisscher Feb 27, 2024
6344812
Moved method actions to new ABAction format
mrvisscher Feb 28, 2024
b6af22f
Temporarily moved old tests
mrvisscher Feb 29, 2024
2139395
Added base testing project and changed conftest
mrvisscher Feb 29, 2024
89bb132
Added tests for activity actions
mrvisscher Feb 29, 2024
c989ade
Fixed conftest for automated tests
mrvisscher Feb 29, 2024
55b26ed
Added tests for cs actions
mrvisscher Feb 29, 2024
d48b59a
Added tests for database actions
mrvisscher Feb 29, 2024
7be791c
Added tests for exchange actions
mrvisscher Feb 29, 2024
61c5929
Skip copy sdf test on Linux
mrvisscher Feb 29, 2024
db6befa
Added tests for method actions
mrvisscher Feb 29, 2024
2955fef
Added tests for parameter actions
mrvisscher Mar 1, 2024
5fbf9ea
Added tests for project actions
mrvisscher Mar 1, 2024
85d9528
Added tests for various actions
mrvisscher Mar 1, 2024
4f9e91e
Reimplemented wizard tests
mrvisscher Mar 1, 2024
80d57da
Longer time for biosphere install during tests
mrvisscher Mar 1, 2024
17e7c54
Fixed faster testing
mrvisscher Mar 1, 2024
3429686
Moved and removed legacy tests
mrvisscher Mar 4, 2024
7e53124
General testing improvements
mrvisscher Mar 4, 2024
80dbfe9
Switch button to get_button
mrvisscher Mar 20, 2024
05cd3b6
Updated imports
mrvisscher Mar 20, 2024
d0db19f
Fixed base.py comments
mrvisscher Mar 20, 2024
6f46bb9
Added docstrings to ABAction base.py
mrvisscher Mar 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions activity_browser/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from .activity.activity_relink import ActivityRelink
from .activity.activity_new import ActivityNew
from .activity.activity_duplicate import ActivityDuplicate
from .activity.activity_open import ActivityOpen
from .activity.activity_graph import ActivityGraph
from .activity.activity_duplicate_to_loc import ActivityDuplicateToLoc
from .activity.activity_delete import ActivityDelete
from .activity.activity_duplicate_to_db import ActivityDuplicateToDB

from .calculation_setup.cs_new import CSNew
from .calculation_setup.cs_delete import CSDelete
from .calculation_setup.cs_duplicate import CSDuplicate
from .calculation_setup.cs_rename import CSRename

from .database.database_import import DatabaseImport
from .database.database_export import DatabaseExport
from .database.database_new import DatabaseNew
from .database.database_delete import DatabaseDelete
from .database.database_duplicate import DatabaseDuplicate
from .database.database_relink import DatabaseRelink

from .exchange.exchange_new import ExchangeNew
from .exchange.exchange_delete import ExchangeDelete
from .exchange.exchange_modify import ExchangeModify
from .exchange.exchange_formula_remove import ExchangeFormulaRemove
from .exchange.exchange_uncertainty_modify import ExchangeUncertaintyModify
from .exchange.exchange_uncertainty_remove import ExchangeUncertaintyRemove
from .exchange.exchange_copy_sdf import ExchangeCopySDF

from .method.method_duplicate import MethodDuplicate
from .method.method_delete import MethodDelete

from .method.cf_uncertainty_modify import CFUncertaintyModify
from .method.cf_amount_modify import CFAmountModify
from .method.cf_remove import CFRemove
from .method.cf_new import CFNew
from .method.cf_uncertainty_remove import CFUncertaintyRemove

from .parameter.parameter_new import ParameterNew
from .parameter.parameter_new_automatic import ParameterNewAutomatic
from .parameter.parameter_rename import ParameterRename

from .project.project_new import ProjectNew
from .project.project_duplicate import ProjectDuplicate
from .project.project_delete import ProjectDelete

from .default_install import DefaultInstall
from .biosphere_update import BiosphereUpdate
from .plugin_wizard_open import PluginWizardOpen
from .settings_wizard_open import SettingsWizardOpen
45 changes: 45 additions & 0 deletions activity_browser/actions/activity/activity_delete.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from typing import Union, Callable, List

from PySide2 import QtWidgets, QtCore

from activity_browser import application, activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDelete(ABAction):
"""
ABAction to delete one or multiple activities if supplied by activity keys. Will check if an activity has any
downstream processes and ask the user whether they want to continue if so. Exchanges from any downstream processes
will be removed
"""
icon = qicons.delete
title = 'Delete ***'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
# retrieve activity objects from the controller using the provided keys
activities = activity_controller.get_activities(self.activity_keys)

# check for downstream processes
if any(len(act.upstream()) > 0 for act in activities):
# warning text
text = ("One or more activities have downstream processes. Deleting these activities will remove the "
"exchange from the downstream processes, this can't be undone.\n\nAre you sure you want to "
"continue?")

# alert the user
choice = QtWidgets.QMessageBox.warning(application.main_window,
"Activity/Activities has/have downstream processes",
text,
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
QtWidgets.QMessageBox.No)

# return if the user cancels
if choice == QtWidgets.QMessageBox.No: return

# use the activity controller to delete multiple activities
activity_controller.delete_activities(self.activity_keys)
24 changes: 24 additions & 0 deletions activity_browser/actions/activity/activity_duplicate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Union, Callable, List

from PySide2 import QtCore

from activity_browser import activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDuplicate(ABAction):
"""
Duplicate one or multiple activities using their keys. Proxy action to call the controller.
"""
icon = qicons.copy
title = 'Duplicate ***'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
activity_controller.duplicate_activities(self.activity_keys)


56 changes: 56 additions & 0 deletions activity_browser/actions/activity/activity_duplicate_to_db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from typing import Union, Callable, List

from PySide2 import QtWidgets, QtCore

from activity_browser import application, project_settings, activity_controller
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction


class ActivityDuplicateToDB(ABAction):
"""
ABAction to duplicate an activity to another database. Asks the user to what database they want to copy the activity
to, returns if there are no valid databases or when the user cancels. Otherwise uses the activity controller to
duplicate the activities to the chosen database.
"""
icon = qicons.duplicate_to_other_database
title = 'Duplicate to other database'
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
# get bw activity objects from keys
activities = activity_controller.get_activities(self.activity_keys)

# get valid databases (not the original database, or locked databases)
origin_db = next(iter(activities)).get("database")
target_dbs = [db for db in project_settings.get_editable_databases() if db != origin_db]

# return if there are no valid databases to duplicate to
if not target_dbs:
QtWidgets.QMessageBox.warning(
application.main_window,
"No target database",
"No valid target databases available. Create a new database or set one to writable (not read-only)."
)
return

# construct a dialog where the user can choose a database to duplicate to
target_db, ok = QtWidgets.QInputDialog.getItem(
application.main_window,
"Copy activity to database",
"Target database:",
target_dbs,
0,
False
)

# return if the user didn't choose, or canceled
if not target_db or not ok: return

# otherwise move all supplied activities to the db using the controller
for activity in activities:
activity_controller.duplicate_activity_to_db(target_db, activity)

170 changes: 170 additions & 0 deletions activity_browser/actions/activity/activity_duplicate_to_loc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
from typing import Union, Callable, Optional

import pandas as pd
import brightway2 as bw
from PySide2 import QtCore

from activity_browser import signals, application, activity_controller, exchange_controller
from activity_browser.bwutils import AB_metadata
from activity_browser.ui.icons import qicons
from activity_browser.actions.base import ABAction
from ...ui.widgets import LocationLinkingDialog


class ActivityDuplicateToLoc(ABAction):
"""
ABAction to duplicate an activity and possibly their exchanges to a new location.
"""
icon = qicons.copy
title = 'Duplicate activity to new location'
activity_key: tuple
db_name: str

def __init__(self, activity_key: Union[tuple, Callable], parent: QtCore.QObject):
super().__init__(parent, activity_key=activity_key)

def onTrigger(self, toggled):
act = activity_controller.get_activities(self.activity_key)[0]
self.db_name = act.key[0]

# get list of dependent databases for activity and load to MetaDataStore
databases = []
for exchange in act.technosphere():
databases.append(exchange.input[0])
if self.db_name not in databases: # add own database if it wasn't added already
databases.append(self.db_name)

# load all dependent databases to MetaDataStore
dbs = {db: AB_metadata.get_database_metadata(db) for db in databases}
# get list of all unique locations in the dependent databases (sorted alphabetically)
locations = []
for db in dbs.values():
locations += db['location'].to_list() # add all locations to one list
locations = list(set(locations)) # reduce the list to only unique items
locations.sort()

# get the location to relink
db = dbs[self.db_name]
old_location = db.loc[db['key'] == act.key]['location'].iloc[0]

# trigger dialog with autocomplete-writeable-dropdown-list
options = (old_location, locations)
dialog = LocationLinkingDialog.relink_location(act['name'], options, application.main_window)

if dialog.exec_() != LocationLinkingDialog.Accepted: return

# read the data from the dialog
for old, new in dialog.relink.items():
alternatives = []
new_location = new
if dialog.use_rer.isChecked(): # RER
alternatives.append(dialog.use_rer.text())
if dialog.use_ews.isChecked(): # Europe without Switzerland
alternatives.append(dialog.use_ews.text())
if dialog.use_row.isChecked(): # RoW
alternatives.append(dialog.use_row.text())
# the order we add alternatives is important, they are checked in this order!
if len(alternatives) > 0:
use_alternatives = True
else:
use_alternatives = False

successful_links = {} # dict of dicts, key of new exch : {new values} <-- see 'values' below
# in the future, 'alternatives' could be improved by making use of some location hierarchy. From that we could
# get things like if the new location is NL but there is no NL, but RER exists, we use that. However, for that
# we need some hierarchical structure to the location data, which may be available from ecoinvent, but we need
# to look for that.

# get exchanges that we want to relink
for exch in act.technosphere():
candidate = self.find_candidate(dbs, exch, old_location, new_location, use_alternatives, alternatives)
if candidate is None:
continue # no suitable candidate was found, try the next exchange

# at this point, we have found 1 suitable candidate, whether that is new_location or alternative location
values = {
'amount': exch.get('amount', False),
'comment': exch.get('comment', False),
'formula': exch.get('formula', False),
'uncertainty': exch.get('uncertainty', False)
}
successful_links[candidate['key'].iloc[0]] = values

# now, create a new activity by copying the old one
new_code = activity_controller.generate_copy_code(act.key)
new_act = act.copy(new_code)
# update production exchanges
for exc in new_act.production():
if exc.input.key == act.key:
exc.input = new_act
exc.save()
# update 'products'
for product in new_act.get('products', []):
if product.get('input') == act.key:
product.input = new_act.key
new_act.save()
# save the new location to the activity
activity_controller.modify_activity(new_act.key, 'location', new_location)

# get exchanges that we want to delete
del_exch = [] # delete these exchanges
for exch in new_act.technosphere():
candidate = self.find_candidate(dbs, exch, old_location, new_location, use_alternatives, alternatives)
if candidate is None:
continue # no suitable candidate was found, try the next exchange
del_exch.append(exch)
# delete exchanges with old locations
exchange_controller.delete_exchanges(del_exch)

# add the new exchanges with all values carried over from last exchange
exchange_controller.add_exchanges(list(successful_links.keys()), new_act.key, successful_links)

# update the MetaDataStore and open new activity
AB_metadata.update_metadata(new_act.key)
signals.safe_open_activity_tab.emit(new_act.key)

# send signals to relevant locations
bw.databases.set_modified(self.db_name)
signals.database_changed.emit(self.db_name)
signals.databases_changed.emit()

def find_candidate(self, dbs, exch, old_location, new_location, use_alternatives, alternatives) -> Optional[object]:
"""Find a candidate to replace the exchange with."""
current_db = exch.input[0]
if current_db == self.db_name:
db = dbs[current_db]
else: # if the exchange is not from the current database, also check the current
# (user may have added their own alternative dependents already)
db = pd.concat([dbs[current_db], dbs[self.db_name]])

if db.loc[db['key'] == exch.input]['location'].iloc[0] != old_location:
return # this exchange has a location we're not trying to re-link

# get relevant data to match on
row = db.loc[db['key'] == exch.input]
name = row['name'].iloc[0]
prod = row['reference product'].iloc[0]
unit = row['unit'].iloc[0]

# get candidates to match (must have same name, product and unit)
candidates = db.loc[(db['name'] == name)
& (db['reference product'] == prod)
& (db['unit'] == unit)]
if len(candidates) <= 1:
return # this activity does not exist in this database with another location (1 is self)

# check candidates for new_location
candidate = candidates.loc[candidates['location'] == new_location]
if len(candidate) == 0 and not use_alternatives:
return # there is no candidate
elif len(candidate) > 1:
return # there is more than one candidate, we can't know what to use
elif len(candidate) == 0:
# there are no candidates, but we can try alternatives
for alt in alternatives:
candidate = candidates.loc[candidates['location'] == alt]
if len(candidate) == 1:
break # found an alternative in with this alternative location, stop looking
if len(candidate) != 1:
return # there are either no or multiple matches with alternative locations
return candidate
23 changes: 23 additions & 0 deletions activity_browser/actions/activity/activity_graph.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Union, Callable, List

from PySide2 import QtCore

from activity_browser import signals
from activity_browser.actions.base import ABAction
from activity_browser.ui.icons import qicons


class ActivityGraph(ABAction):
"""
ABAction to open one or multiple activities in the graph explorer
"""
icon = qicons.graph_explorer
title = "'Open *** in Graph Explorer'"
activity_keys: List[tuple]

def __init__(self, activity_keys: Union[List[tuple], Callable], parent: QtCore.QObject):
super().__init__(parent, activity_keys=activity_keys)

def onTrigger(self, toggled):
for key in self.activity_keys:
signals.open_activity_graph_tab.emit(key)
Loading
Loading