Example #1
0
 def test_get_collection_id(self):
     """Testing get_collection_id."""
     collections_manager = CollectionManager()
     collection_name = 'Westeros Map'
     repository_url = 'https://github.com/john.doe/my_map'
     collection_id = collections_manager.get_collection_id(
         collection_name, repository_url)
     expected_id = '01ece258a505a060830bcecce29f16333f706538'
     self.assertEqual(collection_id, expected_id)
    def __init__(self):
        """Constructor.

        ..note:
        - Directories is a list of repositories. It is stored in the
        settings. The data structure of directories:
        self._directories = {
            'QGIS Official Repository': {
                'url': '[email protected]:anitagraser/QGIS-style-repo-dummy.git',
                'auth_cfg': '0193jkad'
             }
        }

        - Repositories is a dictionary of repositories with all their
        collections. The data structure of repositories:
        self._repositories = {
            repo_name: [{
                'register_name': collection,
                'author': author,
                'author_email': email,
                'repository_url': self.url,
                'status': COLLECTION_NOT_INSTALLED_STATUS,
                'name': parser.get(collection, 'name'),
                'tags': parser.get(collection, 'tags'),
                'description': parser.get(collection, 'description'),
                'qgis_min_version': '2.0',
                'qgis_max_version': '2.99'
                'preview': ['preview/image1.png', 'preview/image2.png']
            },
            .... //other collections from this repository
            ],
            ... //other repository
        }
        """
        QObject.__init__(self)
        # Online directories from the DIRECTORY_URL
        self._online_directories = {}
        # Registered directories
        self._directories = {}
        # Registered repositories
        self._repositories = {}
        # Collection manager instance to deal with collections
        self._collections_manager = CollectionManager()
        # Load repositories from cache
        self.load_repositories()
        # Fetch online directories
        self.fetch_online_directories()
        # Load directory of repositories from settings
        self.load_directories()
    def __init__(self):
        """Constructor.

        ..note:
        - Directories is a list of repository that are registered in user's
        QGIS. Data structure of directories:
        self._directories = {
            'QGIS Official Repository': {
                'url': '[email protected]:anitagraser/QGIS-style-repo-dummy.git',
                'auth_cfg': '0193jkad'
             }
        }

        - Repositories is a dictionary of repository with all the collections
        contained in that repository. Data structure of repositories:
        self._repositories = {
            repo_name: [{
                'register_name': collection,
                'author': author,
                'author_email': email,
                'repository_url': self.url,
                'status': COLLECTION_NOT_INSTALLED_STATUS,
                'name': parser.get(collection, 'name'),
                'tags': parser.get(collection, 'tags'),
                'description': parser.get(collection, 'description'),
                'qgis_min_version': '2.0',
                'qgis_max_version': '2.99'
                'preview': ['preview/image1.png', 'preview/image2.png']
            },
            .... //other collections from this repository
            ],
            ... //other repository
        }
        """
        QObject.__init__(self)
        # Online directories from the DIRECTORY_URL
        self._online_directories = {}
        # Registered directories
        self._directories = {}
        # Registered repositories
        self._repositories = {}
        # Collection manager instance to deal with collections
        self._collections_manager = CollectionManager()
        # Fetch online directories
        self.fetch_online_directories()
        # Load directory of repositories from settings
        self.load_directories()
        # Load repositories from cache
        self.load_repositories()
    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set up the "main menu" - QListWidgetItem
        # All collections
        icon_all = QIcon()
        icon_all.addFile(str(resources_path('img', 'plugin.svg')), QSize(),
                         QIcon.Normal, QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All collections'))
        # Installed collections
        icon_installed = QIcon()
        icon_installed.addFile(
            str(resources_path('img', 'plugin-installed.svg')), QSize(),
            QIcon.Normal, QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed collections'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings / repositories
        icon_settings = QIcon()
        icon_settings.addFile(str(resources_path('img', 'settings.svg')),
                              QSize(), QIcon.Normal, QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the items to the list widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for long running processes
        self.progress_dialog = None

        # Init the repository manager dialog
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate the repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()
class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set up the "main menu" - QListWidgetItem
        # All collections
        icon_all = QIcon()
        icon_all.addFile(str(resources_path('img', 'plugin.svg')), QSize(),
                         QIcon.Normal, QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All collections'))
        # Installed collections
        icon_installed = QIcon()
        icon_installed.addFile(
            str(resources_path('img', 'plugin-installed.svg')), QSize(),
            QIcon.Normal, QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed collections'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings / repositories
        icon_settings = QIcon()
        icon_settings.addFile(str(resources_path('img', 'settings.svg')),
                              QSize(), QIcon.Normal, QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the items to the list widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for long running processes
        self.progress_dialog = None

        # Init the repository manager dialog
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate the repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on the active tab.

        :param index: The index of the active widget (in the list widget).
        :type index: int
        """
        # Clear message bar
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Last menu entry - Settings
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Not settings, must be Collections (all or installed)
            if index == 1:
                # Installed collections
                self.collection_proxy.accepted_status = \
                    COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr('Installed Collections')
                description = self.tr(
                    'On the left you see the list of all the '
                    'installed collections.')
            else:
                # All collections (0)
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr('All Collections')
                description = self.tr(
                    'On the left you see a list of all the collections '
                    'that are available from the registered repositories.<br> '
                    'Installed collections are emphasized (in <b>bold</b>).')

            context = {
                'resources_path': str(resources_path()),
                'title': title,
                'description': description
            }
            self.web_view_details.setHtml(
                render_template('tab_description.html', context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return

        for repoName, repo in self.repository_manager.directories.items():
            if dlg.line_edit_url.text().strip() == repo['url']:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add another repository with the same URL!'),
                    Qgis.Warning, 5)
                return
            if dlg.line_edit_name.text().strip() == repoName:
                self.message_bar.pushMessage(
                    self.tr('Repositories must have unique names!'),
                    Qgis.Warning, 5)
                return

        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += '(2)'

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Add repository
        try:
            status, adderror = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository was successfully added'), Qgis.Success,
                    5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to add repository: %s') % adderror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr('%s') % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return

        # Check if it is among the officially approved QGIS repositories
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        if settings.value(repo_name + '/url') in \
                self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr('You can not edit the official repositories!'),
                Qgis.Warning, 5)
            return

        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]['url'])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]['auth_cfg'])

        if not dlg.exec_():
            return

        # Check if the changed URL is already present and that
        # the new repository name is unique
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]['url']
        new_name = dlg.line_edit_name.text().strip()
        for repoName, repo in self.repository_manager.directories.items():
            if new_url == repo['url'] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr('Unable to add another repository with the same '
                            'URL!'), Qgis.Warning, 5)
                return
            if new_name == repoName and (repo_name != new_name):
                self.message_bar.pushMessage(
                    self.tr('Repositories must have unique names!'),
                    Qgis.Warning, 5)
                return

        # Redundant
        if (new_name in self.repository_manager.directories) and (new_name !=
                                                                  repo_name):
            new_name += '(2)'

        new_auth_cfg = dlg.line_edit_auth_id.text()

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Edit repository
        try:
            status, editerror = self.repository_manager.edit_directory(
                repo_name, new_name, old_url, new_url, new_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository is successfully updated'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to edit repository: %s') % editerror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr('%s') % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate the edit and delete buttons
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return
        # Check if it is among the offical repositories
        repo_url = self.repository_manager.directories[repo_name]['url']
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr('You can not remove official repositories!'),
                Qgis.Warning, 5)
            return

        warning = self.tr('Are you sure you want to remove the following '
                          'repository?') + '\n' + repo_name
        if QMessageBox.warning(self, self.tr('QGIS Resource Sharing'), warning,
                               QMessageBox.Yes,
                               QMessageBox.No) == QMessageBox.No:
            return

        # Remove repository
        installed_collections = \
            self.collection_manager.get_installed_collections(repo_url)
        if installed_collections:
            message = ('You have installed collections from this '
                       'repository. Please uninstall them first!')
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate the edit and delete buttons
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_repositories(self):
        """Slot for when user clicks reload repositories button."""
        # Show progress dialog
        self.show_progress_dialog('Reloading all repositories')

        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory['url']
            auth_cfg = directory['auth_cfg']
            try:
                status, reloaderror = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr('Repository %s is successfully reloaded') %
                        repo_name, Qgis.Info, 5)
                else:
                    self.message_bar.pushMessage(
                        self.tr('Unable to reload %s: %s') %
                        (repo_name, reloaderror), Qgis.Warning, 5)
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr('%s') % e, Qgis.Warning, 5)

        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot for when the user clicks the install/reinstall button."""
        # Save the current index to enable selection after installation
        self.current_index = self.list_view_collections.currentIndex()
        self.show_progress_dialog('Starting installation...')
        self.progress_dialog.canceled.connect(self.install_canceled)

        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(
            self.collection_manager, self._selected_collection_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        installStatus = self.installer_worker.install_status
        if not installStatus:
            message = self.installer_worker.error_message
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()

        if installStatus:
            self.reload_collections_model()
            # Report what has been installed
            message = '<b>%s</b> was successfully installed, containing:\n<ul>' % (
                config.COLLECTIONS[self._selected_collection_id]['name'])
            number = 0
            if 'style' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['style']
                message = message + '\n<li> ' + str(
                    number) + ' Layer style (QML) file'
                if number > 1:
                    message = message + 's'
            if 'symbol' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['symbol']
                message = message + '\n<li> ' + str(
                    number) + ' XML symbol file'
                if number > 1:
                    message = message + 's'
            if 'svg' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['svg']
                message = message + '\n<li> ' + str(number) + ' SVG file'
                if number > 1:
                    message = message + 's'
            if 'models' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['models']
                message = message + '\n<li> ' + str(number) + ' model'
                if number > 1:
                    message = message + 's'
            if 'expressions' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['expressions']
                message = message + '\n<li> ' + str(
                    number) + ' expression file'
                if number > 1:
                    message = message + 's'
            if 'processing' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['processing']
                message = message + '\n<li> ' + str(
                    number) + ' processing script'
                if number > 1:
                    message = message + 's'
            if 'rscripts' in config.COLLECTIONS[
                    self._selected_collection_id].keys():
                number = config.COLLECTIONS[
                    self._selected_collection_id]['rscripts']
                message = message + '\n<li> ' + str(number) + ' R script'
                if number > 1:
                    message = message + 's'
            message = message + '\n</ul>'
        QMessageBox.information(self, 'Resource Sharing', message)
        self.populate_repositories_widget()
        # Set the selection
        oldRow = self.current_index.row()
        newIndex = self.collections_model.createIndex(oldRow, 0)
        selection_model = self.list_view_collections.selectionModel()
        selection_model.setCurrentIndex(newIndex,
                                        selection_model.ClearAndSelect)
        selection_model.select(newIndex, selection_model.ClearAndSelect)
        # Update the buttons
        self.button_install.setEnabled(True)
        self.button_install.setText('Reinstall')
        self.button_open.setEnabled(True)
        self.button_uninstall.setEnabled(True)

        self.show_collection_metadata(self._selected_collection_id)

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog('Cancelling installation...')
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when user clicks the uninstall button."""
        # get the QModelIndex for the item to be uninstalled
        uninstall_index = self.list_view_collections.currentIndex()
        coll_id = self._selected_collection_id
        try:
            self.collection_manager.uninstall(coll_id)
        except Exception as e:
            LOGGER.error('Could not uninstall collection ' +
                         config.COLLECTIONS[coll_id]['name'] + ':\n' + str(e))
        else:
            QMessageBox.information(
                self, 'Resource Sharing',
                'The collection was successfully uninstalled!')
            self.reload_collections_model()
            # Fix the GUI
            currentMenuRow = self.menu_list_widget.currentRow()
            self.set_current_tab(currentMenuRow)
            self.populate_repositories_widget()

            rowCount = self.collection_proxy.rowCount()
            if rowCount > 0:
                # Set the current (and selected) row in the listview
                newRow = uninstall_index.row()
                # Check if this was the last element
                rowCount = self.collection_proxy.rowCount()
                if newRow == rowCount:
                    newRow = newRow - 1
                # Select the new current element
                newIndex = self.collections_model.createIndex(newRow, 0)
                selection_model = self.list_view_collections.selectionModel()
                selection_model.setCurrentIndex(newIndex,
                                                selection_model.ClearAndSelect)
                # Get the id of the current collection
                proxyModel = self.list_view_collections.model()
                proxyIndex = proxyModel.index(newRow, 0)
                current_coll_id = proxyIndex.data(COLLECTION_ID_ROLE)
                self._selected_collection_id = current_coll_id
                # Update buttons
                status = config.COLLECTIONS[current_coll_id]['status']
                if status == COLLECTION_INSTALLED_STATUS:
                    self.button_install.setEnabled(True)
                    self.button_install.setText('Reinstall')
                    self.button_open.setEnabled(True)
                    self.button_uninstall.setEnabled(True)
                else:
                    self.button_install.setEnabled(True)
                    self.button_install.setText('Install')
                    self.button_open.setEnabled(False)
                    self.button_uninstall.setEnabled(False)
                # Update the web_view_details frame
                self.show_collection_metadata(current_coll_id)
            else:
                self.button_install.setEnabled(False)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

    def open_collection(self):
        """Slot for when user clicks 'Open' button."""
        collection_path = local_collection_path(self._selected_collection_id)
        directory_url = QUrl.fromLocalFile(str(collection_path))
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()
        installed_collections = \
            self.collection_manager.get_installed_collections()
        # Export the updated ones from the repository manager
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]['url']
            item = QTreeWidgetItem(self.tree_repositories, REPOSITORY_ITEM)
            item.setText(0, repo_name)
            item.setText(1, url)
            for coll_id in config.COLLECTIONS:
                if ('repository_name' in config.COLLECTIONS[coll_id].keys()
                        and config.COLLECTIONS[coll_id]['repository_name']
                        == repo_name):
                    coll_name = config.COLLECTIONS[coll_id]['name']
                    coll_tags = config.COLLECTIONS[coll_id]['tags']
                    collectionItem = QTreeWidgetItem(item, COLLECTION_ITEM)
                    collitemtext = coll_name
                    if installed_collections and coll_id in installed_collections.keys(
                    ):
                        collitemtext = coll_name + ' (installed)'
                        collectionFont = QFont()
                        collectionFont.setWeight(60)
                        collectionItem.setFont(0, collectionFont)
                    collectionItem.setText(0, collitemtext)
                    collectionItem.setText(1, coll_tags)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        installed_collections = \
            self.collection_manager.get_installed_collections()
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]['name']
            collection_author = config.COLLECTIONS[id]['author']
            collection_tags = config.COLLECTIONS[id]['tags']
            collection_description = config.COLLECTIONS[id]['description']
            collection_status = config.COLLECTIONS[id]['status']
            repository_name = ''
            if 'repository_name' in config.COLLECTIONS[id].keys():
                repository_name = config.COLLECTIONS[id]['repository_name']
            item = QStandardItem(collection_name + ' (' + repository_name +
                                 ')')
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            # Make installed collections stand out
            if installed_collections and id in installed_collections.keys():
                collectionFont = QFont()
                collectionFont.setWeight(60)
                item.setFont(collectionFont)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for the itemSelectionChanged signal of tree_repositories."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item and selected_item.type() == REPOSITORY_ITEM:
            if selected_item:
                repo_name = selected_item.text(0)
            if not repo_name:
                return
            if not repo_name in self.repository_manager.directories.keys():
                return
            repo_url = self.repository_manager.directories[repo_name]['url']
            # Disable the edit and delete buttons for "official" repositories
            if repo_url in self.repository_manager._online_directories.values(
            ):
                self.button_edit.setEnabled(False)
                self.button_delete.setEnabled(False)
            else:
                # Activate the edit and delete buttons
                self.button_edit.setEnabled(True)
                self.button_delete.setEnabled(True)
        elif selected_item and selected_item.type() == COLLECTION_ITEM:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)
        else:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def on_list_view_collections_clicked(self, index):
        """Slot for when the list_view_collections is clicked."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._selected_collection_id = collection_id

            # Enable / disable buttons
            status = config.COLLECTIONS[self._selected_collection_id]['status']
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText('Reinstall')
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the ID."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl('http://qgis-contribution.github.io/' +
                       'QGIS-ResourceSharing/')
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr('Resource Sharing')
            self.progress_dialog.setWindowTitle(title)
            # Just use an infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)

        self.progress_dialog.show()
class RepositoryManager(QObject):
    """Class to handle repositories."""

    DIRECTORY_URL = (
        "https://raw.githubusercontent.com/qgis/" "QGIS-Resources/master/directory.csv"
    )

    def __init__(self):
        """Constructor.

        ..note:
        - Directories is a list of repositories. It is stored in the
        settings. The data structure of directories:
        self._directories = {
            'QGIS Official Repository': {
                'url': '[email protected]:anitagraser/QGIS-style-repo-dummy.git',
                'auth_cfg': '0193jkad'
             }
        }

        - Repositories is a dictionary of repositories with all their
        collections. The data structure of repositories:
        self._repositories = {
            repo_name: [{
                'register_name': collection,
                'author': author,
                'author_email': email,
                'repository_url': self.url,
                'status': COLLECTION_NOT_INSTALLED_STATUS,
                'name': parser.get(collection, 'name'),
                'tags': parser.get(collection, 'tags'),
                'description': parser.get(collection, 'description'),
                'qgis_min_version': '2.0',
                'qgis_max_version': '2.99'
                'preview': ['preview/image1.png', 'preview/image2.png']
            },
            .... //other collections from this repository
            ],
            ... //other repository
        }
        """
        QObject.__init__(self)
        # Online directories from the DIRECTORY_URL
        self._online_directories = {}
        # Registered directories
        self._directories = {}
        # Registered repositories
        self._repositories = {}
        # Collection manager instance to deal with collections
        self._collections_manager = CollectionManager()
        # Load repositories from cache
        self.load_repositories()
        # Fetch online directories
        self.fetch_online_directories()
        # Load directory of repositories from settings
        self.load_directories()

    @property
    def directories(self):
        """Directories contains all the repositories (name and URL)
        registered in setting.

        :returns: Dictionary of repositories registered
        :rtype: dict
        """
        return self._directories

    def fetch_online_directories(self):
        """Fetch online directory of repositories."""
        downloader = NetworkManager(self.DIRECTORY_URL)
        status, _ = downloader.fetch()
        if status:
            directory_file = QTemporaryFile()
            if directory_file.open():
                directory_file.write(downloader.content)
                directory_file.close()
            with open(directory_file.fileName()) as csv_file:
                reader = csv.DictReader(csv_file, fieldnames=("name", "url"))
                for row in reader:
                    repName = row["name"]
                    repUrl = row["url"]
                    # Check name and URL for None before stripping and adding
                    if repName is not None and repUrl is not None:
                        self._online_directories[row["name"]] = repUrl.strip()
                    else:
                        if repName is None:
                            # No name
                            LOGGER.warning("Missing name for repository" " - not added")
                        else:
                            # No URL
                            LOGGER.warning(
                                "Missing URL for repository"
                                + str(row["name"])
                                + " - not added"
                            )
            # Save to settings
            settings = QgsSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue("online_directories", self._online_directories)
            settings.endGroup()
        else:
            # Use settings
            settings = QgsSettings()
            settings.beginGroup(repo_settings_group())
            self._online_directories = settings.value("online_directories", {})
            settings.endGroup()

    def load_directories(self):
        """Update the repository directory."""
        self._directories = {}
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        # Loop through the repositories from the official directory
        for online_dir_name in self._online_directories:
            # Check if the repository is already present
            repo_present = False
            for repo_name in settings.childGroups():
                url = settings.value(repo_name + "/url", "", type=str)
                if url == self._online_directories[online_dir_name]:
                    repo_present = True
                    break
                if online_dir_name == repo_name:
                    repo_present = True
                    nameWarn = (
                        "The repository " + repo_name + " is masking "
                        "an official repository with the same name"
                    )
                    LOGGER.warning(nameWarn)
                    break
            if not repo_present:
                self.add_directory(
                    online_dir_name, self._online_directories[online_dir_name]
                )
        for repo_name in settings.childGroups():
            self._directories[repo_name] = {}
            url = settings.value(repo_name + "/url", "", type=str)
            self._directories[repo_name]["url"] = url
            auth_cfg = settings.value(repo_name + "/auth_cfg", "", type=str).strip()
            self._directories[repo_name]["auth_cfg"] = auth_cfg
        settings.endGroup()

    def add_directory(self, repo_name, url, auth_cfg=None):
        """Add a directory to settings and add the collections from that repo.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        repo_handler = BaseRepositoryHandler.get_handler(url)
        if repo_handler is None:
            LOGGER.warning("There is no handler available for URL '" + str(url) + "'!")
        if auth_cfg:
            repo_handler.auth_cfg = auth_cfg
        # Fetch metadata
        status, fetcherror = repo_handler.fetch_metadata()
        if status:
            # Parse metadata
            try:
                collections = repo_handler.parse_metadata()
            except MetadataError as me:
                metadata_warning = (
                    "Error parsing metadata for " + str(repo_name) + ":\n" + str(me)
                )
                LOGGER.warning(metadata_warning)
                return False, metadata_warning
            # Add the repository and its collections
            self._repositories[repo_name] = collections
            self.rebuild_collections()
            # Add to QgsSettings
            settings = QgsSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue(repo_name + "/url", url)
            if auth_cfg:
                settings.setValue(repo_name + "/auth_cfg", auth_cfg)
            settings.endGroup()
            # Serialize repositories every time we successfully added a repo
            self.serialize_repositories()
        return status, fetcherror

    def edit_directory(
        self, old_repo_name, new_repo_name, old_url, new_url, new_auth_cfg
    ):
        """Edit the directory of repositories and update the
        collections.
        Also used to reload repositories (old == new for url and repo_name)

        :param old_repo_name: The old name of the repository
        :type old_repo_name: str
        :param new_repo_name: The new name of the repository
        :type new_repo_name: str
        :param old_url: The old URL of the repository
        :type old_url: str
        :param new_url: The new URL of the repository
        :type new_url: str
        :param new_auth_cfg: The auth config id.
        :type new_auth_cfg: str
        :return: (status, error)
        :rtype: (boolean, string)
        """
        old_collections = self._repositories.get(old_repo_name, [])
        if (old_repo_name != new_repo_name) and (old_url == new_url):
            # Renaming a repository (same URL)
            for old_collection in old_collections:
                coll_id = self._collections_manager.get_collection_id(
                    old_collection["register_name"], old_collection["repository_url"]
                )
                old_path = local_collection_path(coll_id)
                # Update the repository name for this collection
                config.COLLECTIONS[coll_id]["repository_name"] = new_repo_name
                new_path = local_collection_path(coll_id)
                # If the repository is renamed (same URL), the directories
                # of its collections should be renamed accordingly (so that
                # they remain accessible)
                if old_path.exists():
                    old_path.rename(new_path)
            new_collections = old_collections
            status = True
            fetcherror = ""
        else:
            # old_repo_name == new_repo_name and old_url == new_url
            # or new_url != old_url
            # Fetch the metadata (metadata.ini) from the new url
            repo_handler = BaseRepositoryHandler.get_handler(new_url)
            if repo_handler is None:
                repo_warning = "No handler for URL '" + str(new_url) + "'!"
                LOGGER.warning(repo_warning)
                return (False, repo_warning)
            if new_auth_cfg:
                repo_handler.auth_cfg = new_auth_cfg
            status, fetcherror = repo_handler.fetch_metadata()
            if status:
                # Parse metadata
                try:
                    new_collections = repo_handler.parse_metadata()
                except MetadataError as me:
                    metadata_warning = (
                        "Error parsing metadata for "
                        + str(new_repo_name)
                        + ":\n"
                        + str(me)
                    )
                    LOGGER.warning(metadata_warning)
                    return (False, metadata_warning)
                    # raise MetadataError(metadata_warning)
                # Get all the installed collections from the old repository
                installed_old_collections = []
                for old_collection in old_collections:
                    if old_collection["status"] == COLLECTION_INSTALLED_STATUS:
                        installed_old_collections.append(old_collection)
                # Handling installed collections
                # An old collection that is present in the new location
                # (URL) is identified by its register name.
                # Cases for installed collections:
                # 1. Old collection exists in the new, same URL: use the new
                # one, else: update the status to INSTALLED
                # 2. Old collection exists in the new, different URL: keep them
                # both (add the old one). Because they should be treated as
                # different collections
                # 3. Old collection doesn't exist in the new, same URL: keep
                # the old collection
                # 4. Old collection doesn't exist in the new, different URL:
                # same as 3
                for installed_collection in installed_old_collections:
                    reg_name = installed_collection["register_name"]
                    is_present = False
                    for n_coll in new_collections:
                        # Look for collections that are already present
                        if n_coll["register_name"] == reg_name:
                            # Already present
                            is_present = True
                            if old_url == new_url:
                                # Set the status to installed
                                n_coll["status"] = COLLECTION_INSTALLED_STATUS
                                # Keep the collection statistics
                                for key in installed_collection.keys():
                                    if key in [
                                        "models",
                                        "processing",
                                        "rscripts",
                                        "style",
                                        "svg",
                                        "symbol",
                                        "expressions",
                                    ]:
                                        n_coll[key] = installed_collection[key]
                            else:
                                # Different repository URLs, so append
                                new_collections.append(installed_collection)
                            break
                    if not is_present:
                        new_collections.append(installed_collection)
        # Remove the old repository and add the new one
        self._repositories.pop(old_repo_name, None)
        self._repositories[new_repo_name] = new_collections
        self.rebuild_collections()
        # Update QgsSettings
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        settings.remove(old_repo_name)
        settings.setValue(new_repo_name + "/url", new_url)
        settings.setValue(new_repo_name + "/auth_cfg", new_auth_cfg)
        settings.endGroup()
        # Serialize repositories every time we successfully edited repo
        self.serialize_repositories()
        return status, fetcherror

    def remove_directory(self, repo_name):
        """Remove a directory and all its collections.

        :param repo_name: The old name of the repository
        :type repo_name: str
        """
        self._repositories.pop(repo_name, None)
        self.rebuild_collections()
        # Remove repository from QgsSettings
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        settings.remove(repo_name)
        settings.endGroup()
        # Serialize repositories
        self.serialize_repositories()

    def reload_directory(self, repo_name, url, auth_cfg):
        """Re-fetch the directory and update the collections registry.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        # We are basically editing a directory (same repo name and url)
        status, editerror = self.edit_directory(
            repo_name, repo_name, url, url, auth_cfg
        )
        return status, editerror

    def rebuild_collections(self):
        """Rebuild the collections for all the repositories."""
        config.COLLECTIONS = {}
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection["register_name"], collection["repository_url"]
                )
                collection["repository_name"] = repo
                config.COLLECTIONS[collection_id] = collection
                # Get the collection path (updating if neccessary)
                collection_path = local_collection_path(collection_id)
                # Check the file system to see if the collection exists.
                # If not, also uninstall its resources
                current_status = config.COLLECTIONS[collection_id]["status"]
                if current_status == COLLECTION_INSTALLED_STATUS:
                    if not collection_path.exists():
                        # Uninstall the collection
                        self._collections_manager.uninstall(collection_id)

    def resync_repository(self):
        """Resync from collections as opposed to rebuild_collections."""
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            synced_repo_collections = []
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection["register_name"], collection["repository_url"]
                )
                synced_repo_collections.append(config.COLLECTIONS[collection_id])
            self._repositories[repo] = synced_repo_collections

    def serialize_repositories(self):
        """Save repositories to cache."""
        if not repositories_cache_path().parent.exists():
            repositories_cache_path().parent.mkdir(parents=True)
        self.resync_repository()
        with open(str(repositories_cache_path()), "wb") as f:
            pickle.dump(self._repositories, f)

    def load_repositories(self):
        """Load repositories from cache and rebuild collections."""
        repo_collections = {}
        if repositories_cache_path().exists():
            with open(str(repositories_cache_path()), "rb") as f:
                repo_collections = pickle.load(f)
        self._repositories = repo_collections
        self.rebuild_collections()
Example #7
0
class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface
        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)
        # Set up the "main menu" - QListWidgetItem
        # All collections
        icon_all = QIcon()
        icon_all.addFile(str(resources_path("img", "plugin.svg")), QSize(),
                         QIcon.Normal, QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr("All collections"))
        # Installed collections
        icon_installed = QIcon()
        icon_installed.addFile(
            str(resources_path("img", "plugin-installed.svg")),
            QSize(),
            QIcon.Normal,
            QIcon.Off,
        )
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr("Installed collections"))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings / repositories
        icon_settings = QIcon()
        icon_settings.addFile(str(resources_path("img", "settings.svg")),
                              QSize(), QIcon.Normal, QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr("Settings"))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Add the items to the list widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)
        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)
        # Progress dialog for long running processes
        self.progress_dialog = None
        # Init the repository manager dialog
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._sel_coll_id = None
        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.button_reload_dir.clicked.connect(self.reload_off_res_directory)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)
        # Populate the repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on the active tab.

        :param index: The index of the active widget (in the list widget).
        :type index: int
        """
        # Clear message bar
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Last menu entry - Settings
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Not settings, must be Collections (all or installed)
            if index == 1:
                # Installed collections
                self.collection_proxy.accepted_status = COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr("Installed Collections")
                description = self.tr(
                    "On the left you see the list of all the "
                    "installed collections.")
            else:
                # All collections (0)
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr("All Collections")
                description = self.tr(
                    "On the left you see a list of all the collections "
                    "that are available from the registered repositories.<br> "
                    "Installed collections are emphasized (in <b>bold</b>).")

            context = {
                "resources_path": str(resources_path()),
                "title": title,
                "description": description,
            }
            self.web_view_details.setHtml(
                render_template("tab_description.html", context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return
        for repoName, repo in self.repository_manager.directories.items():
            if dlg.line_edit_url.text().strip() == repo["url"]:
                self.message_bar.pushMessage(
                    self.tr(
                        "Unable to add another repository with the same URL!"),
                    Qgis.Warning,
                    5,
                )
                return
            if dlg.line_edit_name.text().strip() == repoName:
                self.message_bar.pushMessage(
                    self.tr("Repositories must have unique names!"),
                    Qgis.Warning, 5)
                return
        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += "(2)"
        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")
        # Add repository
        try:
            status, adderror = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr("Repository was successfully added"), Qgis.Success,
                    5)
            else:
                self.message_bar.pushMessage(
                    self.tr("Unable to add repository: %s") % adderror,
                    Qgis.Warning, 5)
        except Exception as e:
            self.message_bar.pushMessage(self.tr("%s") % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()
        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)
        if not repo_name:
            return
        # Check if it is among the officially approved QGIS repositories
        settings = QgsSettings()
        settings.beginGroup(repo_settings_group())
        if (settings.value(repo_name + "/url")
                in self.repository_manager._online_directories.values()):
            self.message_bar.pushMessage(
                self.tr("You can not edit the official repositories!"),
                Qgis.Warning, 5)
            return
        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]["url"])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]["auth_cfg"])
        if not dlg.exec_():
            return
        # Check if the changed URL is already present and that
        # the new repository name is unique
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]["url"]
        new_name = dlg.line_edit_name.text().strip()
        for repoName, repo in self.repository_manager.directories.items():
            if new_url == repo["url"] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr("Unable to add another repository with the same "
                            "URL!"),
                    Qgis.Warning,
                    5,
                )
                return
            if new_name == repoName and (repo_name != new_name):
                self.message_bar.pushMessage(
                    self.tr("Repositories must have unique names!"),
                    Qgis.Warning, 5)
                return
        # Redundant
        if (new_name in self.repository_manager.directories) and (new_name !=
                                                                  repo_name):
            new_name += "(2)"
        new_auth_cfg = dlg.line_edit_auth_id.text()
        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")
        # Edit repository
        try:
            status, editerror = self.repository_manager.edit_directory(
                repo_name, new_name, old_url, new_url, new_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr("Repository is successfully updated"),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr("Unable to edit repository: %s") % editerror,
                    Qgis.Warning,
                    5,
                )
        except Exception as e:
            self.message_bar.pushMessage(self.tr("%s") % e, Qgis.Warning, 5)
        finally:
            self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()
        # Deactivate the edit and delete buttons
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)
        if not repo_name:
            return
        # Check if it is among the offical repositories
        repo_url = self.repository_manager.directories[repo_name]["url"]
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr("You can not remove official repositories!"),
                Qgis.Warning, 5)
            return
        warning = (self.tr("Are you sure you want to remove the following "
                           "repository?") + "\n" + repo_name)
        if (QMessageBox.warning(
                self,
                self.tr("QGIS Resource Sharing"),
                warning,
                QMessageBox.Yes,
                QMessageBox.No,
        ) == QMessageBox.No):
            return

        # Remove repository
        installed_collections = self.collection_manager.get_installed_collections(
            repo_url)
        if installed_collections:
            message = ("You have installed collections from this "
                       "repository. Please uninstall them first!")
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate the edit and delete buttons
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_off_res_directory(self):
        """Slot called when the user clicks the 'Reload directory'
        button."""
        # Show progress dialog
        self.show_progress_dialog("Reloading the official QGIS resource"
                                  " directory")
        self.repository_manager._online_directories = {}
        # Registered directories
        self.repository_manager._directories = {}
        self.repository_manager.fetch_online_directories()
        # Load directory of repositories from settings
        self.repository_manager.load_directories()
        self.message_bar.pushMessage("On-line directory reloaded", Qgis.Info,
                                     5)
        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def reload_repositories(self):
        """Slot called when the user clicks the 'Reload repositories'
        button."""
        # Show progress dialog
        self.show_progress_dialog("Reloading all repositories")
        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory["url"]
            auth_cfg = directory["auth_cfg"]
            try:
                status, reloaderror = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr("Repository %s is successfully reloaded") %
                        repo_name,
                        Qgis.Info,
                        5,
                    )
                else:
                    self.message_bar.pushMessage(
                        self.tr("Unable to reload %s: %s") %
                        (repo_name, reloaderror),
                        Qgis.Warning,
                        5,
                    )
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr("%s") % e, Qgis.Warning, 5)
        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot called when the user clicks the Install/Reinstall button."""
        # Save the current index to enable selection after installation
        self.current_index = self.list_view_collections.currentIndex()
        self.show_progress_dialog("Starting installation...")
        self.progress_dialog.canceled.connect(self.install_canceled)
        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(self.collection_manager,
                                                    self._sel_coll_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        installStatus = self.installer_worker.install_status
        if not installStatus:
            message = self.installer_worker.error_message
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()
        if installStatus:
            self.reload_collections_model()
            # Report what has been installed
            message = "<b>%s</b> was successfully installed, " "containing:\n<ul>" % (
                config.COLLECTIONS[self._sel_coll_id]["name"])
            number = 0
            for type_, description in SUPPORTED_RESOURCES_MAP.items():
                if type_ in config.COLLECTIONS[self._sel_coll_id].keys():
                    number = config.COLLECTIONS[self._sel_coll_id][type_]
                    message += (f"\n<li>{number} {description}"
                                f'{"s" if number > 1 else ""}'
                                f"</li>")
            message += "\n</ul>"
        QMessageBox.information(self, "Resource Sharing", message)
        self.populate_repositories_widget()
        # Set the selection
        oldRow = self.current_index.row()
        newIndex = self.collections_model.createIndex(oldRow, 0)
        selection_model = self.list_view_collections.selectionModel()
        selection_model.setCurrentIndex(newIndex,
                                        selection_model.ClearAndSelect)
        selection_model.select(newIndex, selection_model.ClearAndSelect)
        # Update the buttons
        self.button_install.setEnabled(True)
        self.button_install.setText("Reinstall")
        self.button_open.setEnabled(True)
        self.button_uninstall.setEnabled(True)
        self.show_collection_metadata(self._sel_coll_id)

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog("Cancelling installation...")
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when the user clicks the 'Uninstall' button."""
        # get the QModelIndex for the item to be uninstalled
        uninstall_index = self.list_view_collections.currentIndex()
        coll_id = self._sel_coll_id
        try:
            self.collection_manager.uninstall(coll_id)
        except Exception as e:
            LOGGER.error("Could not uninstall collection " +
                         config.COLLECTIONS[coll_id]["name"] + ":\n" + str(e))
        else:
            QMessageBox.information(
                self, "Resource Sharing",
                "The collection was successfully uninstalled!")
            self.reload_collections_model()
            # Fix the GUI
            currentMenuRow = self.menu_list_widget.currentRow()
            self.set_current_tab(currentMenuRow)
            self.populate_repositories_widget()
            rowCount = self.collection_proxy.rowCount()
            if rowCount > 0:
                # Set the current (and selected) row in the listview
                newRow = uninstall_index.row()
                # Check if this was the last element
                rowCount = self.collection_proxy.rowCount()
                if newRow == rowCount:
                    newRow = newRow - 1
                # Select the new current element
                newIndex = self.collections_model.createIndex(newRow, 0)
                selection_model = self.list_view_collections.selectionModel()
                selection_model.setCurrentIndex(newIndex,
                                                selection_model.ClearAndSelect)
                # Get the id of the current collection
                proxyModel = self.list_view_collections.model()
                proxyIndex = proxyModel.index(newRow, 0)
                current_coll_id = proxyIndex.data(COLLECTION_ID_ROLE)
                self._sel_coll_id = current_coll_id
                # Update buttons
                status = config.COLLECTIONS[current_coll_id]["status"]
                if status == COLLECTION_INSTALLED_STATUS:
                    self.button_install.setEnabled(True)
                    self.button_install.setText("Reinstall")
                    self.button_open.setEnabled(True)
                    self.button_uninstall.setEnabled(True)
                else:
                    self.button_install.setEnabled(True)
                    self.button_install.setText("Install")
                    self.button_open.setEnabled(False)
                    self.button_uninstall.setEnabled(False)
                # Update the web_view_details frame
                self.show_collection_metadata(current_coll_id)
            else:
                self.button_install.setEnabled(False)
                self.button_install.setText("Install")
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

    def open_collection(self):
        """Slot called when the user clicks the 'Open' button."""
        collection_path = local_collection_path(self._sel_coll_id)
        directory_url = QUrl.fromLocalFile(str(collection_path))
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()
        installed_collections = self.collection_manager.get_installed_collections(
        )
        # Export the updated ones from the repository manager
        repo_Font = QFont()
        repo_with_installed_Font = QFont()
        repo_with_installed_Font.setWeight(60)
        collection_brush = QBrush(Qt.darkGray)
        installed_collection_brush = QBrush(QColor(60, 25, 10))
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]["url"]
            item = QTreeWidgetItem(self.tree_repositories, REPOSITORY_ITEM)
            # Is the repository in the QGIS resource directory?
            if url in self.repository_manager._online_directories.values():
                repo_with_installed_Font.setUnderline(True)
                repo_Font.setUnderline(True)
            else:
                repo_with_installed_Font.setUnderline(False)
                repo_Font.setUnderline(False)
            item.setText(0, repo_name)
            item.setText(1, url)
            item.setFont(0, repo_Font)
            for coll_id in config.COLLECTIONS:
                if ("repository_name" in config.COLLECTIONS[coll_id].keys()
                        and config.COLLECTIONS[coll_id]["repository_name"]
                        == repo_name):
                    coll_name = config.COLLECTIONS[coll_id]["name"]
                    coll_tags = config.COLLECTIONS[coll_id]["tags"]
                    collectionItem = QTreeWidgetItem(item, COLLECTION_ITEM)
                    brush = collection_brush
                    collectionFont = QFont()
                    collectionFont.setStyle(QFont.StyleItalic)
                    collitemtext = coll_name
                    if (installed_collections
                            and coll_id in installed_collections.keys()):
                        collitemtext = coll_name + " (installed)"
                        brush = installed_collection_brush
                        item.setFont(0, repo_with_installed_Font)
                        item.setForeground(0, brush)
                        item.setForeground(1, brush)
                    collectionItem.setFont(0, collectionFont)
                    collectionItem.setForeground(0, brush)
                    collectionItem.setText(0, collitemtext)
                    collectionItem.setFont(1, collectionFont)
                    collectionItem.setForeground(1, brush)
                    collectionItem.setText(1, coll_tags)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        installed_collections = self.collection_manager.get_installed_collections(
        )
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]["name"]
            collection_author = config.COLLECTIONS[id]["author"]
            collection_tags = config.COLLECTIONS[id]["tags"]
            collection_description = config.COLLECTIONS[id]["description"]
            collection_status = config.COLLECTIONS[id]["status"]
            repository_name = ""
            if "repository_name" in config.COLLECTIONS[id].keys():
                repository_name = config.COLLECTIONS[id]["repository_name"]
            item = QStandardItem(collection_name + " (" + repository_name +
                                 ")")
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            # Make installed collections stand out
            if installed_collections and id in installed_collections.keys():
                collectionFont = QFont()
                collectionFont.setWeight(60)
                item.setFont(collectionFont)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for the itemSelectionChanged signal of tree_repositories."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item and selected_item.type() == REPOSITORY_ITEM:
            if selected_item:
                repo_name = selected_item.text(0)
            if not repo_name:
                return
            if repo_name not in self.repository_manager.directories.keys():
                return
            repo_url = self.repository_manager.directories[repo_name]["url"]
            # Disable the edit and delete buttons for "official" repositories
            if repo_url in self.repository_manager._online_directories.values(
            ):
                self.button_edit.setEnabled(False)
                self.button_delete.setEnabled(False)
            else:
                # Activate the edit and delete buttons
                self.button_edit.setEnabled(True)
                self.button_delete.setEnabled(True)
        elif selected_item and selected_item.type() == COLLECTION_ITEM:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)
        else:
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def on_list_view_collections_clicked(self, index):
        """Slot called when the user clicks an item in
        list_view_collections."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._sel_coll_id = collection_id
            # Enable / disable buttons
            status = config.COLLECTIONS[self._sel_coll_id]["status"]
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText("Reinstall")
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText("Install")
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)
            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(text, Qt.CaseInsensitive, QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the ID."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot called when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl("http://qgis-contribution.github.io/" +
                       "QGIS-ResourceSharing/")
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr("Resource Sharing")
            self.progress_dialog.setWindowTitle(title)
            # Just use an infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)
        self.progress_dialog.show()
class RepositoryManager(QObject):
    """Class to handle repositories."""

    DIRECTORY_URL = ('https://raw.githubusercontent.com/qgis/'
                     'QGIS-Resources/master/directory.csv')

    def __init__(self):
        """Constructor.

        ..note:
        - Directories is a list of repository that are registered in user's
        QGIS. Data structure of directories:
        self._directories = {
            'QGIS Official Repository': {
                'url': '[email protected]:anitagraser/QGIS-style-repo-dummy.git',
                'auth_cfg': '0193jkad'
             }
        }

        - Repositories is a dictionary of repository with all the collections
        contained in that repository. Data structure of repositories:
        self._repositories = {
            repo_name: [{
                'register_name': collection,
                'author': author,
                'author_email': email,
                'repository_url': self.url,
                'status': COLLECTION_NOT_INSTALLED_STATUS,
                'name': parser.get(collection, 'name'),
                'tags': parser.get(collection, 'tags'),
                'description': parser.get(collection, 'description'),
                'qgis_min_version': '2.0',
                'qgis_max_version': '2.99'
                'preview': ['preview/image1.png', 'preview/image2.png']
            },
            .... //other collections from this repository
            ],
            ... //other repository
        }
        """
        QObject.__init__(self)
        # Online directories from the DIRECTORY_URL
        self._online_directories = {}
        # Registered directories
        self._directories = {}
        # Registered repositories
        self._repositories = {}
        # Collection manager instance to deal with collections
        self._collections_manager = CollectionManager()
        # Fetch online directories
        self.fetch_online_directories()
        # Load directory of repositories from settings
        self.load_directories()
        # Load repositories from cache
        self.load_repositories()

    @property
    def directories(self):
        """Directories contains all the repositories (name and URL)
        registered in setting.

        :returns: Dictionary of repositories registered
        :rtype: dict
        """
        return self._directories

    def fetch_online_directories(self):
        """Fetch online directory of repositories."""
        downloader = NetworkManager(self.DIRECTORY_URL)
        status, _ = downloader.fetch()
        if status:
            directory_file = QTemporaryFile()
            if directory_file.open():
                directory_file.write(downloader.content)
                directory_file.close()

            with open(directory_file.fileName()) as csv_file:
                reader = csv.DictReader(csv_file, fieldnames=('name', 'url'))
                for row in reader:
                    self._online_directories[row['name']] = row['url'].strip()
            # Save it to cache
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue('online_directories', self._online_directories)
            settings.endGroup()
        else:
            # Just use cache from previous use
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            self._online_directories = settings.value('online_directories', {})
            settings.endGroup()

    def load_directories(self):
        """Load directories of repository registered in settings."""
        self._directories = {}
        settings = QSettings()
        settings.beginGroup(repo_settings_group())

        # Write online directory first to QSettings if needed
        for online_dir_name in self._online_directories:
            repo_present = False
            for repo_name in settings.childGroups():
                url = settings.value(repo_name + '/url', '', type=unicode)
                if url == self._online_directories[online_dir_name]:
                    repo_present = True
                    break
            if not repo_present:
                self.add_directory(
                    online_dir_name, self._online_directories[online_dir_name])

        for repo_name in settings.childGroups():
            self._directories[repo_name] = {}
            url = settings.value(
                repo_name + '/url', '', type=unicode)
            self._directories[repo_name]['url'] = url
            auth_cfg = settings.value(
                repo_name + '/auth_cfg', '', type=unicode).strip()
            self._directories[repo_name]['auth_cfg'] = auth_cfg
        settings.endGroup()

    def add_directory(self, repo_name, url, auth_cfg=None):
        """Add a directory to settings and add the collections from that repo.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        repo_handler = BaseRepositoryHandler.get_handler(url)
        if repo_handler is None:
            raise Exception('There is no handler available for the given URL!')

        if auth_cfg:
            repo_handler.auth_cfg = auth_cfg

        # Fetch metadata
        status, description = repo_handler.fetch_metadata()
        if status:
            # Parse metadata
            try:
                collections = repo_handler.parse_metadata()
            except MetadataError:
                raise
            # Add the repo and the collections
            self._repositories[repo_name] = collections
            self.rebuild_collections()
            # Add to QSettings
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue(repo_name + '/url', url)
            if auth_cfg:
                settings.setValue(repo_name + '/auth_cfg', auth_cfg)
            settings.endGroup()
            # Serialize repositories every time we successfully added a repo
            self.serialize_repositories()

        return status, description

    def edit_directory(
            self,
            old_repo_name,
            new_repo_name,
            old_url,
            new_url,
            new_auth_cfg):
        """Edit a directory and update the collections.

        :param old_repo_name: The old name of the repository
        :type old_repo_name: str

        :param new_repo_name: The new name of the repository
        :type new_repo_name: str

        :param old_url: The old URL of the repository
        :type old_url: str

        :param new_url: The new URL of the repository
        :type new_url: str

        :param new_auth_cfg: The auth config id.
        :type new_auth_cfg: str
        """
        # Fetch the metadata from the new url
        repo_handler = BaseRepositoryHandler.get_handler(new_url)
        if repo_handler is None:
            raise Exception('There is no handler available for the given URL!')

        if new_auth_cfg:
            repo_handler.auth_cfg = new_auth_cfg

        status, description = repo_handler.fetch_metadata()

        if status:
            # Parse metadata
            try:
                new_collections = repo_handler.parse_metadata()
            except MetadataError:
                raise

            old_collections = self._repositories.get(old_repo_name, [])
            # Get all the installed collections from the old repository
            installed_old_collections = []
            for old_collection in old_collections:
                if old_collection['status'] == COLLECTION_INSTALLED_STATUS:
                    installed_old_collections.append(old_collection)

            # Beware of the installed collections
            # Old collection exists in the new URL are identified by its
            # register name. Cases for installed collections:
            # 1. Old collection exists in the new URL, same URL: use the new
            # one, update the status to INSTALLED
            # 2. Old collection exists in the new URL, different URL: keep them
            # both (add the old one). Because they should be treated as
            # different collection
            # 3. Old collection doesn't exist in the new URL, same URL: keep
            # the old collection
            # 4. Old collection doesn't exist in the new URL, different URL:
            # same with 3
            for installed_collection in installed_old_collections:
                reg_name = installed_collection['register_name']
                is_present = False

                for collection in new_collections:
                    if collection['register_name'] == reg_name:
                        is_present = True
                        if old_url == new_url:
                            collection['status'] = COLLECTION_INSTALLED_STATUS
                        else:
                            new_collections.append(installed_collection)
                        break

                # Get to this point could be because it's present or the old
                # installed collection doesn't exist in the new URL
                if not is_present:
                    new_collections.append(installed_collection)

            # Remove old repository and add new one
            self._repositories.pop(old_repo_name, None)
            self._repositories[new_repo_name] = new_collections
            self.rebuild_collections()

            # Update QSettings
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.remove(old_repo_name)
            settings.setValue(new_repo_name + '/url', new_url)
            settings.setValue(new_repo_name + '/auth_cfg', new_auth_cfg)
            settings.endGroup()
            # Serialize repositories every time we successfully edited repo
            self.serialize_repositories()
        return status, description

    def remove_directory(self, repo_name):
        """Remove a directory and all the collections of that repository.

        :param repo_name: The old name of the repository
        :type repo_name: str
        """
        self._repositories.pop(repo_name, None)
        self.rebuild_collections()
        # Remove repo from QSettings
        settings = QSettings()
        settings.beginGroup(repo_settings_group())
        settings.remove(repo_name)
        settings.endGroup()
        # Serialize repositories every time successfully removed a repo
        self.serialize_repositories()

    def reload_directory(self, repo_name, url, auth_cfg):
        """Re-fetch the directory and update the collections registry.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        # We're basically editing a directory with the same repo name and url
        status, description = self.edit_directory(
            repo_name,
            repo_name,
            url,
            url,
            auth_cfg
        )
        return status, description

    def rebuild_collections(self):
        """Rebuild collections from repositories."""
        config.COLLECTIONS = {}
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection['register_name'],
                    collection['repository_url']
                )
                config.COLLECTIONS[collection_id] = collection

                # Check in the file system if the collection exists for all
                # installed collections. If not, also uninstall resources
                current_status = config.COLLECTIONS[collection_id]['status']
                if current_status == COLLECTION_INSTALLED_STATUS:
                    collection_path = local_collection_path(collection_id)
                    if not os.path.exists(collection_path):
                        # Uninstall the collection
                        self._collections_manager.uninstall(collection_id)

    def resync_repository(self):
        """Resync from collections as opposed to rebuild_collections."""
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            synced_repo_collections = []
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection['register_name'],
                    collection['repository_url']
                )
                synced_repo_collections.append(
                    config.COLLECTIONS[collection_id]
                )
            self._repositories[repo] = synced_repo_collections

    def serialize_repositories(self):
        """Save repositories to cache."""
        if not os.path.exists(os.path.dirname(repositories_cache_path())):
            os.makedirs(os.path.dirname(repositories_cache_path()))

        self.resync_repository()
        with open(repositories_cache_path(), 'wb') as f:
            pickle.dump(self._repositories, f)

    def load_repositories(self):
        """Load repositories from cache and rebuild collections."""
        repo_collections = {}
        if os.path.exists(repositories_cache_path()):
            with open(repositories_cache_path(), 'rb') as f:
                repo_collections = pickle.load(f)
        self._repositories = repo_collections
        self.rebuild_collections()
    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set QListWidgetItem
        # All
        icon_all = QIcon()
        icon_all.addFile(
            resources_path('img', 'plugin.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All'))
        # Installed
        icon_installed = QIcon()
        icon_installed.addFile(
            resources_path('img', 'plugin-installed.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings
        icon_settings = QIcon()
        icon_settings.addFile(
            resources_path('img', 'settings.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the list widget item to the widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for any long running process
        self.progress_dialog = None

        # Init repository manager
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()
class ResourceSharingDialog(QDialog, FORM_CLASS):
    TAB_ALL = 0
    TAB_INSTALLED = 1
    TAB_SETTINGS = 2

    def __init__(self, parent=None, iface=None):
        """Constructor.

        :param parent: Optional widget to use as parent
        :type parent: QWidget

        :param iface: An instance of QGisInterface
        :type iface: QGisInterface
        """
        super(ResourceSharingDialog, self).__init__(parent)
        self.setupUi(self)
        self.iface = iface

        # Reconfigure UI
        self.setModal(True)
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)
        self.button_install.setEnabled(False)
        self.button_open.setEnabled(False)
        self.button_uninstall.setEnabled(False)

        # Set QListWidgetItem
        # All
        icon_all = QIcon()
        icon_all.addFile(
            resources_path('img', 'plugin.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_all = QListWidgetItem()
        item_all.setIcon(icon_all)
        item_all.setText(self.tr('All'))
        # Installed
        icon_installed = QIcon()
        icon_installed.addFile(
            resources_path('img', 'plugin-installed.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_installed = QListWidgetItem()
        item_installed.setIcon(icon_installed)
        item_installed.setText(self.tr('Installed'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)
        # Settings
        icon_settings = QIcon()
        icon_settings.addFile(
            resources_path('img', 'settings.svg'),
            QSize(),
            QIcon.Normal,
            QIcon.Off)
        item_settings = QListWidgetItem()
        item_settings.setIcon(icon_settings)
        item_settings.setText(self.tr('Settings'))
        item_all.setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled)

        # Add the list widget item to the widget
        self.menu_list_widget.addItem(item_all)
        self.menu_list_widget.addItem(item_installed)
        self.menu_list_widget.addItem(item_settings)

        # Init the message bar
        self.message_bar = QgsMessageBar(self)
        self.message_bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.vlayoutRightColumn.insertWidget(0, self.message_bar)

        # Progress dialog for any long running process
        self.progress_dialog = None

        # Init repository manager
        self.repository_manager = RepositoryManager()
        self.collection_manager = CollectionManager()
        # Collections list view
        self.collections_model = QStandardItemModel(0, 1)
        self.collections_model.sort(0, Qt.AscendingOrder)
        self.collection_proxy = CustomSortFilterProxyModel(self)
        self.collection_proxy.setSourceModel(self.collections_model)
        self.list_view_collections.setModel(self.collection_proxy)
        # Active selected collection
        self._selected_collection_id = None

        # Slots
        self.button_add.clicked.connect(self.add_repository)
        self.button_edit.clicked.connect(self.edit_repository)
        self.button_delete.clicked.connect(self.delete_repository)
        self.button_reload.clicked.connect(self.reload_repositories)
        self.menu_list_widget.currentRowChanged.connect(self.set_current_tab)
        self.list_view_collections.selectionModel().currentChanged.connect(
            self.on_list_view_collections_clicked)
        self.line_edit_filter.textChanged.connect(self.filter_collections)
        self.button_install.clicked.connect(self.install_collection)
        self.button_open.clicked.connect(self.open_collection)
        self.button_uninstall.clicked.connect(self.uninstall_collection)
        self.button_box.button(QDialogButtonBox.Help).clicked.connect(
            self.open_help)

        # Populate repositories widget and collections list view
        self.populate_repositories_widget()
        self.reload_collections_model()

    def set_current_tab(self, index):
        """Set stacked widget based on active tab.

        :param index: The index of the active list widget item.
        :type index: int
        """
        # Clear message bar first
        self.message_bar.clearWidgets()
        if index == (self.menu_list_widget.count() - 1):
            # Switch to settings tab
            self.stacked_menu_widget.setCurrentIndex(1)
        else:
            # Switch to plugins tab
            if index == 1:
                # Installed
                self.collection_proxy.accepted_status = \
                    COLLECTION_INSTALLED_STATUS
                # Set the web view
                title = self.tr('Installed Collections')
                description = self.tr(
                    'On the left you see the list of all collections '
                    'installed on your QGIS')
            else:
                # All
                self.collection_proxy.accepted_status = COLLECTION_ALL_STATUS
                # Set the web view
                title = self.tr('All Collections')
                description = self.tr(
                    'On the left you see the list of all collections '
                    'available from the repositories registered in the '
                    'settings.')

            context = {
                'resources_path': resources_path(),
                'title': title,
                'description': description
            }
            self.web_view_details.setHtml(
                render_template('tab_description.html', context))
            self.stacked_menu_widget.setCurrentIndex(0)

    def add_repository(self):
        """Open add repository dialog."""
        dlg = ManageRepositoryDialog(self)
        if not dlg.exec_():
            return

        for repo in self.repository_manager.directories.values():
            if dlg.line_edit_url.text().strip() == repo['url']:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add another repository with the same URL!'),
                    Qgis.Critical, 5)
                return

        repo_name = dlg.line_edit_name.text()
        repo_url = dlg.line_edit_url.text().strip()
        repo_auth_cfg = dlg.line_edit_auth_id.text().strip()
        if repo_name in self.repository_manager.directories:
            repo_name += '(2)'

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Add repository
        try:
            status, description = self.repository_manager.add_directory(
                repo_name, repo_url, repo_auth_cfg)
            if status:
                self.message_bar.pushMessage(
                    self.tr(
                        'Repository is successfully added'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr(
                        'Unable to add repository: %s') % description,
                    Qgis.Critical, 5)
        except Exception as e:
            self.message_bar.pushMessage(
                self.tr('%s') % e,
                Qgis.Critical, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def edit_repository(self):
        """Open edit repository dialog."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return

        # Check if it's the approved online dir repository
        settings = QSettings()
        settings.beginGroup(repo_settings_group())
        if settings.value(repo_name + '/url') in \
                self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr(
                    'You can not edit the official repositories!'),
                Qgis.Warning, 5)
            return

        dlg = ManageRepositoryDialog(self)
        dlg.line_edit_name.setText(repo_name)
        dlg.line_edit_url.setText(
            self.repository_manager.directories[repo_name]['url'])
        dlg.line_edit_auth_id.setText(
            self.repository_manager.directories[repo_name]['auth_cfg'])

        if not dlg.exec_():
            return

        # Check if the changed URL is already there in the repo
        new_url = dlg.line_edit_url.text().strip()
        old_url = self.repository_manager.directories[repo_name]['url']
        for repo in self.repository_manager.directories.values():
            if new_url == repo['url'] and (old_url != new_url):
                self.message_bar.pushMessage(
                    self.tr('Unable to add another repository with the same '
                            'URL!'),
                    Qgis.Critical, 5)
                return

        new_name = dlg.line_edit_name.text()
        if (new_name in self.repository_manager.directories) and (
                    new_name != repo_name):
            new_name += '(2)'

        new_auth_cfg = dlg.line_edit_auth_id.text()

        # Show progress dialog
        self.show_progress_dialog("Fetching repository's metadata")

        # Edit repository
        try:
            status, description = self.repository_manager.edit_directory(
                repo_name,
                new_name,
                old_url,
                new_url,
                new_auth_cfg
            )
            if status:
                self.message_bar.pushMessage(
                    self.tr('Repository is successfully updated'),
                    Qgis.Success, 5)
            else:
                self.message_bar.pushMessage(
                    self.tr('Unable to add repository: %s') % description,
                    Qgis.Critical, 5)
        except Exception as e:
            self.message_bar.pushMessage(
                self.tr('%s') % e, Qgis.Critical, 5)
        finally:
            self.progress_dialog.hide()

        # Reload data and widget
        self.reload_data_and_widget()

        # Deactivate edit and delete button
        self.button_edit.setEnabled(False)
        self.button_delete.setEnabled(False)

    def delete_repository(self):
        """Delete a repository in the tree widget."""
        selected_item = self.tree_repositories.currentItem()
        if selected_item:
            repo_name = selected_item.text(0)

        if not repo_name:
            return
        # Check if it's the approved online dir repository
        repo_url = self.repository_manager.directories[repo_name]['url']
        if repo_url in self.repository_manager._online_directories.values():
            self.message_bar.pushMessage(
                self.tr(
                    'You can not remove the official repositories!'),
                Qgis.Warning, 5)
            return

        warning = self.tr('Are you sure you want to remove the following '
                          'repository?') + '\n' + repo_name
        if QMessageBox.warning(
                self,
                self.tr('QGIS Resource Sharing'),
                warning,
                QMessageBox.Yes,
                QMessageBox.No) == QMessageBox.No:
            return

        # Remove repository
        installed_collections = \
            self.collection_manager.get_installed_collections(repo_url)
        if installed_collections:
            message = ('You have some installed collections from this '
                       'repository. Please uninstall them first!')
            self.message_bar.pushMessage(message, Qgis.Warning, 5)
        else:
            self.repository_manager.remove_directory(repo_name)
            # Reload data and widget
            self.reload_data_and_widget()
            # Deactivate edit and delete button
            self.button_edit.setEnabled(False)
            self.button_delete.setEnabled(False)

    def reload_repositories(self):
        """Slot for when user clicks reload repositories button."""
        # Show progress dialog
        self.show_progress_dialog('Reloading all repositories')

        for repo_name in self.repository_manager.directories:
            directory = self.repository_manager.directories[repo_name]
            url = directory['url']
            auth_cfg = directory['auth_cfg']
            try:
                status, description = self.repository_manager.reload_directory(
                    repo_name, url, auth_cfg)
                if status:
                    self.message_bar.pushMessage(
                        self.tr(
                            'Repository %s is successfully reloaded') %
                        repo_name, Qgis.Info, 5)
                else:
                    self.message_bar.pushMessage(
                        self.tr(
                            'Unable to reload %s: %s') % (
                            repo_name, description),
                        Qgis.Critical, 5)
            except Exception as e:
                self.message_bar.pushMessage(
                    self.tr('%s') % e,
                    Qgis.Critical, 5)

        self.progress_dialog.hide()
        # Reload data and widget
        self.reload_data_and_widget()

    def install_collection(self):
        """Slot for when user clicks download button."""
        self.show_progress_dialog('Starting installation process...')
        self.progress_dialog.canceled.connect(self.install_canceled)

        self.installer_thread = QThread()
        self.installer_worker = CollectionInstaller(
            self.collection_manager, self._selected_collection_id)
        self.installer_worker.moveToThread(self.installer_thread)
        self.installer_worker.finished.connect(self.install_finished)
        self.installer_worker.aborted.connect(self.install_aborted)
        self.installer_worker.progress.connect(self.install_progress)
        self.installer_thread.started.connect(self.installer_worker.run)
        self.installer_thread.start()

    def install_finished(self):
        # Process the result
        self.progress_dialog.hide()
        if self.installer_worker.install_status:
            self.reload_collections_model()
            message = '%s is installed successfully' % (
                config.COLLECTIONS[self._selected_collection_id]['name'])
        else:
            message = self.installer_worker.error_message
        QMessageBox.information(self, 'Resource Sharing', message)
        # Clean up the worker and thread
        self.installer_worker.deleteLater()
        self.installer_thread.quit()
        self.installer_thread.wait()
        self.installer_thread.deleteLater()

    def install_canceled(self):
        self.progress_dialog.hide()
        self.show_progress_dialog('Cancelling installation...')
        self.installer_worker.abort()

    def install_aborted(self):
        if self.installer_thread.isRunning():
            self.installer_thread.quit()
        self.installer_thread.finished.connect(self.progress_dialog.hide)

    def install_progress(self, text):
        self.progress_dialog.setLabelText(text)

    def uninstall_collection(self):
        """Slot called when user clicks uninstall button."""
        try:
            self.collection_manager.uninstall(self._selected_collection_id)
        except Exception as e:
            raise
        self.reload_collections_model()
        QMessageBox.information(
            self,
            'Resource Sharing',
            'The collection is uninstalled succesfully!')

    def open_collection(self):
        """Slot for when user clicks 'Open' button."""
        collection_path = local_collection_path(self._selected_collection_id)
        directory_url = QUrl.fromLocalFile(collection_path)
        QDesktopServices.openUrl(directory_url)

    def reload_data_and_widget(self):
        """Reload repositories and collections and update widgets related."""
        self.reload_repositories_widget()
        self.reload_collections_model()

    def reload_repositories_widget(self):
        """Refresh tree repositories using new repositories data."""
        self.repository_manager.load_directories()
        self.populate_repositories_widget()

    def populate_repositories_widget(self):
        """Populate the current dictionary repositories to the tree widget."""
        # Clear the current tree widget
        self.tree_repositories.clear()

        # Export the updated ones from the repository manager
        for repo_name in self.repository_manager.directories:
            url = self.repository_manager.directories[repo_name]['url']
            item = QTreeWidgetItem(self.tree_repositories)
            item.setText(0, repo_name)
            item.setText(1, url)
        self.tree_repositories.resizeColumnToContents(0)
        self.tree_repositories.resizeColumnToContents(1)
        self.tree_repositories.sortItems(1, Qt.AscendingOrder)

    def reload_collections_model(self):
        """Reload the collections model with the current collections."""
        self.collections_model.clear()
        for id in config.COLLECTIONS:
            collection_name = config.COLLECTIONS[id]['name']
            collection_author = config.COLLECTIONS[id]['author']
            collection_tags = config.COLLECTIONS[id]['tags']
            collection_description = config.COLLECTIONS[id]['description']
            collection_status = config.COLLECTIONS[id]['status']
            item = QStandardItem(collection_name)
            item.setEditable(False)
            item.setData(id, COLLECTION_ID_ROLE)
            item.setData(collection_name, COLLECTION_NAME_ROLE)
            item.setData(collection_description, COLLECTION_DESCRIPTION_ROLE)
            item.setData(collection_author, COLLECTION_AUTHOR_ROLE)
            item.setData(collection_tags, COLLECTION_TAGS_ROLE)
            item.setData(collection_status, COLLECTION_STATUS_ROLE)
            self.collections_model.appendRow(item)
        self.collections_model.sort(0, Qt.AscendingOrder)

    def on_tree_repositories_itemSelectionChanged(self):
        """Slot for when the itemSelectionChanged signal emitted."""
        # Activate edit and delete button
        self.button_edit.setEnabled(True)
        self.button_delete.setEnabled(True)

    def on_list_view_collections_clicked(self, index):
        """Slot for when the list_view_collections is clicked."""
        real_index = self.collection_proxy.mapToSource(index)
        if real_index.row() != -1:
            collection_item = self.collections_model.itemFromIndex(real_index)
            collection_id = collection_item.data(COLLECTION_ID_ROLE)
            self._selected_collection_id = collection_id

            # Enable/disable button
            status = config.COLLECTIONS[self._selected_collection_id]['status']
            is_installed = status == COLLECTION_INSTALLED_STATUS
            if is_installed:
                self.button_install.setEnabled(True)
                self.button_install.setText('Reinstall')
                self.button_open.setEnabled(True)
                self.button_uninstall.setEnabled(True)
            else:
                self.button_install.setEnabled(True)
                self.button_install.setText('Install')
                self.button_open.setEnabled(False)
                self.button_uninstall.setEnabled(False)

            # Show  metadata
            self.show_collection_metadata(collection_id)

    @pyqtSlot(str)
    def filter_collections(self, text):
        search = QRegExp(
            text,
            Qt.CaseInsensitive,
            QRegExp.RegExp)
        self.collection_proxy.setFilterRegExp(search)

    def show_collection_metadata(self, id):
        """Show the collection metadata given the id."""
        html = self.collection_manager.get_html(id)
        self.web_view_details.setHtml(html)

    def reject(self):
        """Slot when the dialog is closed."""
        # Serialize collections to settings
        self.repository_manager.serialize_repositories()
        self.done(0)

    def open_help(self):
        """Open help."""
        doc_url = QUrl('http://www.akbargumbira.com/qgis_resources_sharing')
        QDesktopServices.openUrl(doc_url)

    def show_progress_dialog(self, text):
        """Show infinite progress dialog with given text.

        :param text: Text as the label of the progress dialog
        :type text: str
        """
        if self.progress_dialog is None:
            self.progress_dialog = QProgressDialog(self)
            self.progress_dialog.setWindowModality(Qt.WindowModal)
            self.progress_dialog.setAutoClose(False)
            title = self.tr('Resource Sharing')
            self.progress_dialog.setWindowTitle(title)
            # Just use infinite progress bar here
            self.progress_dialog.setMaximum(0)
            self.progress_dialog.setMinimum(0)
            self.progress_dialog.setValue(0)
            self.progress_dialog.setLabelText(text)

        self.progress_dialog.show()
Example #11
0
class RepositoryManager(QObject):
    """Class to handle repositories."""

    DIRECTORY_URL = ('https://raw.githubusercontent.com/qgis/'
                     'QGIS-Resources/master/directory.csv')

    def __init__(self):
        """Constructor.

        ..note:
        - Directories is a list of repository that are registered in user's
        QGIS. Data structure of directories:
        self._directories = {
            'QGIS Official Repository': {
                'url': '[email protected]:anitagraser/QGIS-style-repo-dummy.git',
                'auth_cfg': '0193jkad'
             }
        }

        - Repositories is a dictionary of repository with all the collections
        contained in that repository. Data structure of repositories:
        self._repositories = {
            repo_name: [{
                'register_name': collection,
                'author': author,
                'author_email': email,
                'repository_url': self.url,
                'status': COLLECTION_NOT_INSTALLED_STATUS,
                'name': parser.get(collection, 'name'),
                'tags': parser.get(collection, 'tags'),
                'description': parser.get(collection, 'description'),
                'qgis_min_version': '2.0',
                'qgis_max_version': '2.99'
                'preview': ['preview/image1.png', 'preview/image2.png']
            },
            .... //other collections from this repository
            ],
            ... //other repository
        }
        """
        QObject.__init__(self)
        # Online directories from the DIRECTORY_URL
        self._online_directories = {}
        # Registered directories
        self._directories = {}
        # Registered repositories
        self._repositories = {}
        # Collection manager instance to deal with collections
        self._collections_manager = CollectionManager()
        # Fetch online directories
        self.fetch_online_directories()
        # Load directory of repositories from settings
        self.load_directories()
        # Load repositories from cache
        self.load_repositories()

    @property
    def directories(self):
        """Directories contains all the repositories (name and URL)
        registered in setting.

        :returns: Dictionary of repositories registered
        :rtype: dict
        """
        return self._directories

    def fetch_online_directories(self):
        """Fetch online directory of repositories."""
        downloader = NetworkManager(self.DIRECTORY_URL)
        status, _ = downloader.fetch()
        if status:
            directory_file = QTemporaryFile()
            if directory_file.open():
                directory_file.write(downloader.content)
                directory_file.close()

            with open(directory_file.fileName()) as csv_file:
                reader = csv.DictReader(csv_file, fieldnames=('name', 'url'))
                for row in reader:
                    self._online_directories[row['name']] = row['url'].strip()
            # Save it to cache
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue('online_directories', self._online_directories)
            settings.endGroup()
        else:
            # Just use cache from previous use
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            self._online_directories = settings.value('online_directories', {})
            settings.endGroup()

    def load_directories(self):
        """Load directories of repository registered in settings."""
        self._directories = {}
        settings = QSettings()
        settings.beginGroup(repo_settings_group())

        # Write online directory first to QSettings if needed
        for online_dir_name in self._online_directories:
            repo_present = False
            for repo_name in settings.childGroups():
                url = settings.value(repo_name + '/url', '', type=unicode)
                if url == self._online_directories[online_dir_name]:
                    repo_present = True
                    break
            if not repo_present:
                self.add_directory(online_dir_name,
                                   self._online_directories[online_dir_name])

        for repo_name in settings.childGroups():
            self._directories[repo_name] = {}
            url = settings.value(repo_name + '/url', '', type=unicode)
            self._directories[repo_name]['url'] = url
            auth_cfg = settings.value(repo_name + '/auth_cfg',
                                      '',
                                      type=unicode).strip()
            self._directories[repo_name]['auth_cfg'] = auth_cfg
        settings.endGroup()

    def add_directory(self, repo_name, url, auth_cfg=None):
        """Add a directory to settings and add the collections from that repo.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        repo_handler = BaseRepositoryHandler.get_handler(url)
        if repo_handler is None:
            raise Exception('There is no handler available for the given URL!')

        if auth_cfg:
            repo_handler.auth_cfg = auth_cfg

        # Fetch metadata
        status, description = repo_handler.fetch_metadata()
        if status:
            # Parse metadata
            try:
                collections = repo_handler.parse_metadata()
            except MetadataError:
                raise
            # Add the repo and the collections
            self._repositories[repo_name] = collections
            self.rebuild_collections()
            # Add to QSettings
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.setValue(repo_name + '/url', url)
            if auth_cfg:
                settings.setValue(repo_name + '/auth_cfg', auth_cfg)
            settings.endGroup()
            # Serialize repositories every time we successfully added a repo
            self.serialize_repositories()

        return status, description

    def edit_directory(self, old_repo_name, new_repo_name, old_url, new_url,
                       new_auth_cfg):
        """Edit a directory and update the collections.

        :param old_repo_name: The old name of the repository
        :type old_repo_name: str

        :param new_repo_name: The new name of the repository
        :type new_repo_name: str

        :param old_url: The old URL of the repository
        :type old_url: str

        :param new_url: The new URL of the repository
        :type new_url: str

        :param new_auth_cfg: The auth config id.
        :type new_auth_cfg: str
        """
        # Fetch the metadata from the new url
        repo_handler = BaseRepositoryHandler.get_handler(new_url)
        if repo_handler is None:
            raise Exception('There is no handler available for the given URL!')

        if new_auth_cfg:
            repo_handler.auth_cfg = new_auth_cfg

        status, description = repo_handler.fetch_metadata()

        if status:
            # Parse metadata
            try:
                new_collections = repo_handler.parse_metadata()
            except MetadataError:
                raise

            old_collections = self._repositories.get(old_repo_name, [])
            # Get all the installed collections from the old repository
            installed_old_collections = []
            for old_collection in old_collections:
                if old_collection['status'] == COLLECTION_INSTALLED_STATUS:
                    installed_old_collections.append(old_collection)

            # Beware of the installed collections
            # Old collection exists in the new URL are identified by its
            # register name. Cases for installed collections:
            # 1. Old collection exists in the new URL, same URL: use the new
            # one, update the status to INSTALLED
            # 2. Old collection exists in the new URL, different URL: keep them
            # both (add the old one). Because they should be treated as
            # different collection
            # 3. Old collection doesn't exist in the new URL, same URL: keep
            # the old collection
            # 4. Old collection doesn't exist in the new URL, different URL:
            # same with 3
            for installed_collection in installed_old_collections:
                reg_name = installed_collection['register_name']
                is_present = False

                for collection in new_collections:
                    if collection['register_name'] == reg_name:
                        is_present = True
                        if old_url == new_url:
                            collection['status'] = COLLECTION_INSTALLED_STATUS
                        else:
                            new_collections.append(installed_collection)
                        break

                # Get to this point could be because it's present or the old
                # installed collection doesn't exist in the new URL
                if not is_present:
                    new_collections.append(installed_collection)

            # Remove old repository and add new one
            self._repositories.pop(old_repo_name, None)
            self._repositories[new_repo_name] = new_collections
            self.rebuild_collections()

            # Update QSettings
            settings = QSettings()
            settings.beginGroup(repo_settings_group())
            settings.remove(old_repo_name)
            settings.setValue(new_repo_name + '/url', new_url)
            settings.setValue(new_repo_name + '/auth_cfg', new_auth_cfg)
            settings.endGroup()
            # Serialize repositories every time we successfully edited repo
            self.serialize_repositories()
        return status, description

    def remove_directory(self, repo_name):
        """Remove a directory and all the collections of that repository.

        :param repo_name: The old name of the repository
        :type repo_name: str
        """
        self._repositories.pop(repo_name, None)
        self.rebuild_collections()
        # Remove repo from QSettings
        settings = QSettings()
        settings.beginGroup(repo_settings_group())
        settings.remove(repo_name)
        settings.endGroup()
        # Serialize repositories every time successfully removed a repo
        self.serialize_repositories()

    def reload_directory(self, repo_name, url, auth_cfg):
        """Re-fetch the directory and update the collections registry.

        :param repo_name: The name of the repository
        :type repo_name: str

        :param url: The URL of the repository
        :type url: str
        """
        # We're basically editing a directory with the same repo name and url
        status, description = self.edit_directory(repo_name, repo_name, url,
                                                  url, auth_cfg)
        return status, description

    def rebuild_collections(self):
        """Rebuild collections from repositories."""
        config.COLLECTIONS = {}
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection['register_name'], collection['repository_url'])
                config.COLLECTIONS[collection_id] = collection

                # Check in the file system if the collection exists for all
                # installed collections. If not, also uninstall resources
                current_status = config.COLLECTIONS[collection_id]['status']
                if current_status == COLLECTION_INSTALLED_STATUS:
                    collection_path = local_collection_path(collection_id)
                    if not os.path.exists(collection_path):
                        # Uninstall the collection
                        self._collections_manager.uninstall(collection_id)

    def resync_repository(self):
        """Resync from collections as opposed to rebuild_collections."""
        for repo in self._repositories.keys():
            repo_collections = self._repositories[repo]
            synced_repo_collections = []
            for collection in repo_collections:
                collection_id = self._collections_manager.get_collection_id(
                    collection['register_name'], collection['repository_url'])
                synced_repo_collections.append(
                    config.COLLECTIONS[collection_id])
            self._repositories[repo] = synced_repo_collections

    def serialize_repositories(self):
        """Save repositories to cache."""
        if not os.path.exists(os.path.dirname(repositories_cache_path())):
            os.makedirs(os.path.dirname(repositories_cache_path()))

        self.resync_repository()
        with open(repositories_cache_path(), 'wb') as f:
            pickle.dump(self._repositories, f)

    def load_repositories(self):
        """Load repositories from cache and rebuild collections."""
        repo_collections = {}
        if os.path.exists(repositories_cache_path()):
            with open(repositories_cache_path(), 'r') as f:
                repo_collections = pickle.load(f)
        self._repositories = repo_collections
        self.rebuild_collections()