Module soitool.modules.module_code_phrase

SOI module for coded phrases.

Expand source code
"""SOI module for coded phrases."""

import secrets
from PySide2.QtWidgets import (
    QTableWidgetItem,
    QSizePolicy,
    QWidget,
    QVBoxLayout,
)
from PySide2.QtCore import Qt
from PySide2.QtGui import QIcon
from soitool.modules.module_base import (
    ModuleBase,
    DEFAULT_FONT,
    prepare_table_for_pdf_export,
    prepare_line_edit_for_pdf_export,
    is_event_add_row,
    is_event_remove_row,
)
from soitool.modules.fit_to_contents_widgets import (
    TableWithSizeOfContent,
    LineEditWithSizeOfContent,
)

START_ROWS = 1

CODE_COLUMN = 0
PHRASE_COLUMN = 1


class NoMoreAvailableWords(Exception):
    """There are no more available words."""


class NoMoreAvailableCategories(Exception):
    """There are no more available categories."""


class Meta(type(ModuleBase), type(QWidget)):
    """Used as a metaclass to enable multiple inheritance."""


class CodePhraseModule(ModuleBase, QWidget, metaclass=Meta):
    """Module for coded phrases.

    Codes are words fetched from the database in a certain category. All
    instances of this module is guaranteed to use different categories, and
    thereby different words for their codes.

    ## Note about `self.adjustSize`

    When used outside a layout the widget may not be asked to resize itself.
    For this reason we need to call `self.adjustSize` manually when we want the
    size to update.

    Parameters
    ----------
    database : soitool.database.Database
        Database to fetch categoried words from.
    data : list
        Data to initialize module from. See `self.initialize_from_data` for
        format.
    category : str
        Category to fetch words from to use as codes. If not passed a unique
        category will be chosen at random.

    Raises
    ------
    NoMoreAvailableCategories
        Indicates that there are no more categories to choose from.
    """

    used_categories = []
    """List of categories that are used by other instances of this class."""

    def __init__(self, database, data=None, category=None):
        self.type = CodePhraseModule.__name__
        QWidget.__init__(self)
        ModuleBase.__init__(self)

        self.line_edit_header = LineEditWithSizeOfContent("KODEFRASER")
        self.line_edit_header.setFont(self.headline_font)
        self.line_edit_header.setAlignment(Qt.AlignCenter)
        self.table = TableWithSizeOfContent(0, 2)
        self.table.setFont(DEFAULT_FONT)
        self.table.setStyleSheet(
            "QTableView { gridline-color: black; }"
            "QHeaderView::section { border: 1px solid black }"
        )

        # Qt should let this widget be exactly the size of it's sizeHint
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        # See class docstring
        self.line_edit_header.textChanged.connect(self.adjustSize)
        self.table.cellChanged.connect(self.adjustSize)

        self.database = database

        self.table.setHorizontalHeaderItem(0, QTableWidgetItem("Kode"))
        self.table.setHorizontalHeaderItem(1, QTableWidgetItem("Frase"))
        self.table.horizontalHeader().setStyleSheet("font-weight: bold")

        # Forcing height of the header because it changes from screen to screen
        self.table.horizontalHeader().setFixedHeight(30)

        # To ensure table is initially larger than title
        self.table.horizontalHeader().setMinimumSectionSize(125)
        self.table.verticalHeader().hide()

        # Initialize either default values or from data. Either way from this
        # point on the widget should never have less than one row
        if data is None:
            self.initialize_default(category)
        else:
            self.initialize_from_data(data)

        self.layout = QVBoxLayout()
        self.layout.setSpacing(0)
        self.layout.setMargin(0)
        self.layout.addWidget(self.line_edit_header)
        self.layout.addWidget(self.table)
        self.setLayout(self.layout)

    def initialize_from_data(self, data):
        """Initialize from data.

        Parameters
        ----------
        data : list
            Refer to `get_data` for detailed description.
        """
        self.line_edit_header.setText(data[0])
        self.category = data[1]
        self.used_categories.append(self.category)
        self.available_words = data[2]
        table = data[3]
        for i, row in enumerate(table):
            self.table.insertRow(i)
            self.set_code_table_item(i, row[CODE_COLUMN])
            self.set_phrase_table_item(i, row[PHRASE_COLUMN])

    def initialize_default(self, category=None):
        """Initialize to default values.

        Prepares `self.category` and `self.available_words` from
        `self.database`.

        Parameters
        ----------
        category : str
            Category to use. If 'None' will choose one from the database.

        Raises
        ------
        NoMoreAvailableCategories
            Indicates that there are no more categories to choose from.
        """
        if category is not None:
            self.category = category
        else:
            self.category = self.get_category()
        self.available_words = self.database.get_category_words(self.category)
        for _ in range(START_ROWS):
            self.add_row()

    def add_row(self):
        """Add row below selected row.

        New row will include non-editable code.
        """
        code = None
        try:
            code = self.get_code()
        except NoMoreAvailableWords:
            # NOTE: Failing silently here, but could notify user if we wanted
            # to
            pass
        else:
            self.table.insertRow(self.table.currentRow() + 1)
            self.set_code_table_item(self.table.currentRow() + 1, code)

    def set_code_table_item(self, row, code):
        """Set code at row.

        Parameters
        ----------
        row : int
            Row to set code.
        code : str
            Code to set.
        """
        item = QTableWidgetItem(code)
        item.setFlags(item.flags() ^ Qt.ItemIsEditable)
        self.table.setItem(row, CODE_COLUMN, item)

    def set_phrase_table_item(self, row, phrase):
        """Set phrase at row.

        Parameters
        ----------
        row : int
            Row to set code.
        phrase : str
            Phrase to set.
        """
        item = QTableWidgetItem(phrase)
        self.table.setItem(row, PHRASE_COLUMN, item)

    def remove_row(self):
        """Remove selected row.

        Also adds code to list of available codes for re-use.
        """
        if self.table.rowCount() > 1:
            code = self.table.item(self.table.currentRow(), CODE_COLUMN).text()
            self.table.removeRow(self.table.currentRow())
            self.available_words.append(code)
            self.adjustSize()

    def keyPressEvent(self, event):
        """Launch actions when specific combinations of keys are pressed.

        * CTRL +: New row under current.
        * CTRL -: Remove current row.

        Parameters
        ----------
        event : QKeyEvent
            Event sent by Qt for us to handle.
        """
        if is_event_add_row(event):
            self.add_row()
        elif is_event_remove_row(event):
            self.remove_row()

    def get_category(self):
        """Get available category.

        Queries database for all categories and picks one at random that has
        not been used by an instance of this class before. This function
        utilizes the class variable `self.used_categories`.

        Returns
        -------
        str
            Available category.

        Raises
        ------
        NoMoreAvailableCategories
            Indicates that there are no more categories to choose from.
        """
        all_categories = self.database.get_categories()
        available_categories = [
            category
            for category in all_categories
            if category not in self.used_categories
        ]
        if not available_categories:
            raise NoMoreAvailableCategories()
        category = secrets.choice(available_categories)
        self.used_categories.append(category)
        return category

    def get_code(self):
        """Get available code.

        Returns
        -------
        str
            Available code.

        Raises
        ------
        NoMoreAvailableWords
            Indicate that there are no more words to use as codes.
        """
        if not self.available_words:
            raise NoMoreAvailableWords()
        code = secrets.choice(self.available_words)
        self.available_words.remove(code)
        return code

    def get_size(self):
        """Get size of widget.

        Returns
        -------
        Tuple
            (width, height)
        """
        size = self.sizeHint()
        return (size.width(), size.height())

    def get_data(self):
        """Return list containing module data.

        Returns
        -------
        list
            First element in the list is the header. Second element is the
            category. Third element is available words in the category. Fourth
            element is a list of rows, each containing a list of columns.
        """
        content = []
        content.append(self.line_edit_header.text())
        content.append(self.category)
        content.append(self.available_words)
        table = []
        for i in range(self.table.rowCount()):
            row = []
            for j in range(self.table.columnCount()):
                item = self.table.item(i, j)
                if item is not None:
                    row.append(item.text())
                else:
                    row.append("")
            table.append(row)
        content.append(table)

        return content

    def prepare_for_pdf_export(self):
        """Prepare for PDF-export."""
        prepare_line_edit_for_pdf_export(self.line_edit_header)
        prepare_table_for_pdf_export(self.table)

    @staticmethod
    def get_user_friendly_name():
        """Get user-friendly name of module."""
        return "Kodefraser"

    @staticmethod
    def get_icon():
        """Get icon of module."""
        return QIcon("soitool/media/codephrasemodule.png")

Classes

class CodePhraseModule (database, data=None, category=None)

Module for coded phrases.

Codes are words fetched from the database in a certain category. All instances of this module is guaranteed to use different categories, and thereby different words for their codes.

Note about self.adjustSize

When used outside a layout the widget may not be asked to resize itself. For this reason we need to call self.adjustSize manually when we want the size to update.

Parameters

database : Database
Database to fetch categoried words from.
data : list
Data to initialize module from. See self.initialize_from_data for format.
category : str
Category to fetch words from to use as codes. If not passed a unique category will be chosen at random.

Raises

NoMoreAvailableCategories
Indicates that there are no more categories to choose from.

Class-variable 'type' should be set by derived class.

Expand source code
class CodePhraseModule(ModuleBase, QWidget, metaclass=Meta):
    """Module for coded phrases.

    Codes are words fetched from the database in a certain category. All
    instances of this module is guaranteed to use different categories, and
    thereby different words for their codes.

    ## Note about `self.adjustSize`

    When used outside a layout the widget may not be asked to resize itself.
    For this reason we need to call `self.adjustSize` manually when we want the
    size to update.

    Parameters
    ----------
    database : soitool.database.Database
        Database to fetch categoried words from.
    data : list
        Data to initialize module from. See `self.initialize_from_data` for
        format.
    category : str
        Category to fetch words from to use as codes. If not passed a unique
        category will be chosen at random.

    Raises
    ------
    NoMoreAvailableCategories
        Indicates that there are no more categories to choose from.
    """

    used_categories = []
    """List of categories that are used by other instances of this class."""

    def __init__(self, database, data=None, category=None):
        self.type = CodePhraseModule.__name__
        QWidget.__init__(self)
        ModuleBase.__init__(self)

        self.line_edit_header = LineEditWithSizeOfContent("KODEFRASER")
        self.line_edit_header.setFont(self.headline_font)
        self.line_edit_header.setAlignment(Qt.AlignCenter)
        self.table = TableWithSizeOfContent(0, 2)
        self.table.setFont(DEFAULT_FONT)
        self.table.setStyleSheet(
            "QTableView { gridline-color: black; }"
            "QHeaderView::section { border: 1px solid black }"
        )

        # Qt should let this widget be exactly the size of it's sizeHint
        self.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        # See class docstring
        self.line_edit_header.textChanged.connect(self.adjustSize)
        self.table.cellChanged.connect(self.adjustSize)

        self.database = database

        self.table.setHorizontalHeaderItem(0, QTableWidgetItem("Kode"))
        self.table.setHorizontalHeaderItem(1, QTableWidgetItem("Frase"))
        self.table.horizontalHeader().setStyleSheet("font-weight: bold")

        # Forcing height of the header because it changes from screen to screen
        self.table.horizontalHeader().setFixedHeight(30)

        # To ensure table is initially larger than title
        self.table.horizontalHeader().setMinimumSectionSize(125)
        self.table.verticalHeader().hide()

        # Initialize either default values or from data. Either way from this
        # point on the widget should never have less than one row
        if data is None:
            self.initialize_default(category)
        else:
            self.initialize_from_data(data)

        self.layout = QVBoxLayout()
        self.layout.setSpacing(0)
        self.layout.setMargin(0)
        self.layout.addWidget(self.line_edit_header)
        self.layout.addWidget(self.table)
        self.setLayout(self.layout)

    def initialize_from_data(self, data):
        """Initialize from data.

        Parameters
        ----------
        data : list
            Refer to `get_data` for detailed description.
        """
        self.line_edit_header.setText(data[0])
        self.category = data[1]
        self.used_categories.append(self.category)
        self.available_words = data[2]
        table = data[3]
        for i, row in enumerate(table):
            self.table.insertRow(i)
            self.set_code_table_item(i, row[CODE_COLUMN])
            self.set_phrase_table_item(i, row[PHRASE_COLUMN])

    def initialize_default(self, category=None):
        """Initialize to default values.

        Prepares `self.category` and `self.available_words` from
        `self.database`.

        Parameters
        ----------
        category : str
            Category to use. If 'None' will choose one from the database.

        Raises
        ------
        NoMoreAvailableCategories
            Indicates that there are no more categories to choose from.
        """
        if category is not None:
            self.category = category
        else:
            self.category = self.get_category()
        self.available_words = self.database.get_category_words(self.category)
        for _ in range(START_ROWS):
            self.add_row()

    def add_row(self):
        """Add row below selected row.

        New row will include non-editable code.
        """
        code = None
        try:
            code = self.get_code()
        except NoMoreAvailableWords:
            # NOTE: Failing silently here, but could notify user if we wanted
            # to
            pass
        else:
            self.table.insertRow(self.table.currentRow() + 1)
            self.set_code_table_item(self.table.currentRow() + 1, code)

    def set_code_table_item(self, row, code):
        """Set code at row.

        Parameters
        ----------
        row : int
            Row to set code.
        code : str
            Code to set.
        """
        item = QTableWidgetItem(code)
        item.setFlags(item.flags() ^ Qt.ItemIsEditable)
        self.table.setItem(row, CODE_COLUMN, item)

    def set_phrase_table_item(self, row, phrase):
        """Set phrase at row.

        Parameters
        ----------
        row : int
            Row to set code.
        phrase : str
            Phrase to set.
        """
        item = QTableWidgetItem(phrase)
        self.table.setItem(row, PHRASE_COLUMN, item)

    def remove_row(self):
        """Remove selected row.

        Also adds code to list of available codes for re-use.
        """
        if self.table.rowCount() > 1:
            code = self.table.item(self.table.currentRow(), CODE_COLUMN).text()
            self.table.removeRow(self.table.currentRow())
            self.available_words.append(code)
            self.adjustSize()

    def keyPressEvent(self, event):
        """Launch actions when specific combinations of keys are pressed.

        * CTRL +: New row under current.
        * CTRL -: Remove current row.

        Parameters
        ----------
        event : QKeyEvent
            Event sent by Qt for us to handle.
        """
        if is_event_add_row(event):
            self.add_row()
        elif is_event_remove_row(event):
            self.remove_row()

    def get_category(self):
        """Get available category.

        Queries database for all categories and picks one at random that has
        not been used by an instance of this class before. This function
        utilizes the class variable `self.used_categories`.

        Returns
        -------
        str
            Available category.

        Raises
        ------
        NoMoreAvailableCategories
            Indicates that there are no more categories to choose from.
        """
        all_categories = self.database.get_categories()
        available_categories = [
            category
            for category in all_categories
            if category not in self.used_categories
        ]
        if not available_categories:
            raise NoMoreAvailableCategories()
        category = secrets.choice(available_categories)
        self.used_categories.append(category)
        return category

    def get_code(self):
        """Get available code.

        Returns
        -------
        str
            Available code.

        Raises
        ------
        NoMoreAvailableWords
            Indicate that there are no more words to use as codes.
        """
        if not self.available_words:
            raise NoMoreAvailableWords()
        code = secrets.choice(self.available_words)
        self.available_words.remove(code)
        return code

    def get_size(self):
        """Get size of widget.

        Returns
        -------
        Tuple
            (width, height)
        """
        size = self.sizeHint()
        return (size.width(), size.height())

    def get_data(self):
        """Return list containing module data.

        Returns
        -------
        list
            First element in the list is the header. Second element is the
            category. Third element is available words in the category. Fourth
            element is a list of rows, each containing a list of columns.
        """
        content = []
        content.append(self.line_edit_header.text())
        content.append(self.category)
        content.append(self.available_words)
        table = []
        for i in range(self.table.rowCount()):
            row = []
            for j in range(self.table.columnCount()):
                item = self.table.item(i, j)
                if item is not None:
                    row.append(item.text())
                else:
                    row.append("")
            table.append(row)
        content.append(table)

        return content

    def prepare_for_pdf_export(self):
        """Prepare for PDF-export."""
        prepare_line_edit_for_pdf_export(self.line_edit_header)
        prepare_table_for_pdf_export(self.table)

    @staticmethod
    def get_user_friendly_name():
        """Get user-friendly name of module."""
        return "Kodefraser"

    @staticmethod
    def get_icon():
        """Get icon of module."""
        return QIcon("soitool/media/codephrasemodule.png")

Ancestors

  • ModuleBase
  • abc.ABC
  • PySide2.QtWidgets.QWidget
  • PySide2.QtCore.QObject
  • PySide2.QtGui.QPaintDevice
  • Shiboken.Object

Class variables

var staticMetaObject
var used_categories

List of categories that are used by other instances of this class.

Static methods

def get_icon()

Get icon of module.

Expand source code
@staticmethod
def get_icon():
    """Get icon of module."""
    return QIcon("soitool/media/codephrasemodule.png")
def get_user_friendly_name()

Get user-friendly name of module.

Expand source code
@staticmethod
def get_user_friendly_name():
    """Get user-friendly name of module."""
    return "Kodefraser"

Methods

def add_row(self)

Add row below selected row.

New row will include non-editable code.

Expand source code
def add_row(self):
    """Add row below selected row.

    New row will include non-editable code.
    """
    code = None
    try:
        code = self.get_code()
    except NoMoreAvailableWords:
        # NOTE: Failing silently here, but could notify user if we wanted
        # to
        pass
    else:
        self.table.insertRow(self.table.currentRow() + 1)
        self.set_code_table_item(self.table.currentRow() + 1, code)
def get_category(self)

Get available category.

Queries database for all categories and picks one at random that has not been used by an instance of this class before. This function utilizes the class variable self.used_categories.

Returns

str
Available category.

Raises

NoMoreAvailableCategories
Indicates that there are no more categories to choose from.
Expand source code
def get_category(self):
    """Get available category.

    Queries database for all categories and picks one at random that has
    not been used by an instance of this class before. This function
    utilizes the class variable `self.used_categories`.

    Returns
    -------
    str
        Available category.

    Raises
    ------
    NoMoreAvailableCategories
        Indicates that there are no more categories to choose from.
    """
    all_categories = self.database.get_categories()
    available_categories = [
        category
        for category in all_categories
        if category not in self.used_categories
    ]
    if not available_categories:
        raise NoMoreAvailableCategories()
    category = secrets.choice(available_categories)
    self.used_categories.append(category)
    return category
def get_code(self)

Get available code.

Returns

str
Available code.

Raises

NoMoreAvailableWords
Indicate that there are no more words to use as codes.
Expand source code
def get_code(self):
    """Get available code.

    Returns
    -------
    str
        Available code.

    Raises
    ------
    NoMoreAvailableWords
        Indicate that there are no more words to use as codes.
    """
    if not self.available_words:
        raise NoMoreAvailableWords()
    code = secrets.choice(self.available_words)
    self.available_words.remove(code)
    return code
def get_data(self)

Return list containing module data.

Returns

list
First element in the list is the header. Second element is the category. Third element is available words in the category. Fourth element is a list of rows, each containing a list of columns.
Expand source code
def get_data(self):
    """Return list containing module data.

    Returns
    -------
    list
        First element in the list is the header. Second element is the
        category. Third element is available words in the category. Fourth
        element is a list of rows, each containing a list of columns.
    """
    content = []
    content.append(self.line_edit_header.text())
    content.append(self.category)
    content.append(self.available_words)
    table = []
    for i in range(self.table.rowCount()):
        row = []
        for j in range(self.table.columnCount()):
            item = self.table.item(i, j)
            if item is not None:
                row.append(item.text())
            else:
                row.append("")
        table.append(row)
    content.append(table)

    return content
def get_size(self)

Get size of widget.

Returns

Tuple
(width, height)
Expand source code
def get_size(self):
    """Get size of widget.

    Returns
    -------
    Tuple
        (width, height)
    """
    size = self.sizeHint()
    return (size.width(), size.height())
def initialize_default(self, category=None)

Initialize to default values.

Prepares self.category and self.available_words from self.database.

Parameters

category : str
Category to use. If 'None' will choose one from the database.

Raises

NoMoreAvailableCategories
Indicates that there are no more categories to choose from.
Expand source code
def initialize_default(self, category=None):
    """Initialize to default values.

    Prepares `self.category` and `self.available_words` from
    `self.database`.

    Parameters
    ----------
    category : str
        Category to use. If 'None' will choose one from the database.

    Raises
    ------
    NoMoreAvailableCategories
        Indicates that there are no more categories to choose from.
    """
    if category is not None:
        self.category = category
    else:
        self.category = self.get_category()
    self.available_words = self.database.get_category_words(self.category)
    for _ in range(START_ROWS):
        self.add_row()
def initialize_from_data(self, data)

Initialize from data.

Parameters

data : list
Refer to get_data for detailed description.
Expand source code
def initialize_from_data(self, data):
    """Initialize from data.

    Parameters
    ----------
    data : list
        Refer to `get_data` for detailed description.
    """
    self.line_edit_header.setText(data[0])
    self.category = data[1]
    self.used_categories.append(self.category)
    self.available_words = data[2]
    table = data[3]
    for i, row in enumerate(table):
        self.table.insertRow(i)
        self.set_code_table_item(i, row[CODE_COLUMN])
        self.set_phrase_table_item(i, row[PHRASE_COLUMN])
def keyPressEvent(self, event)

Launch actions when specific combinations of keys are pressed.

  • CTRL +: New row under current.
  • CTRL -: Remove current row.

Parameters

event : QKeyEvent
Event sent by Qt for us to handle.
Expand source code
def keyPressEvent(self, event):
    """Launch actions when specific combinations of keys are pressed.

    * CTRL +: New row under current.
    * CTRL -: Remove current row.

    Parameters
    ----------
    event : QKeyEvent
        Event sent by Qt for us to handle.
    """
    if is_event_add_row(event):
        self.add_row()
    elif is_event_remove_row(event):
        self.remove_row()
def prepare_for_pdf_export(self)

Prepare for PDF-export.

Expand source code
def prepare_for_pdf_export(self):
    """Prepare for PDF-export."""
    prepare_line_edit_for_pdf_export(self.line_edit_header)
    prepare_table_for_pdf_export(self.table)
def remove_row(self)

Remove selected row.

Also adds code to list of available codes for re-use.

Expand source code
def remove_row(self):
    """Remove selected row.

    Also adds code to list of available codes for re-use.
    """
    if self.table.rowCount() > 1:
        code = self.table.item(self.table.currentRow(), CODE_COLUMN).text()
        self.table.removeRow(self.table.currentRow())
        self.available_words.append(code)
        self.adjustSize()
def set_code_table_item(self, row, code)

Set code at row.

Parameters

row : int
Row to set code.
code : str
Code to set.
Expand source code
def set_code_table_item(self, row, code):
    """Set code at row.

    Parameters
    ----------
    row : int
        Row to set code.
    code : str
        Code to set.
    """
    item = QTableWidgetItem(code)
    item.setFlags(item.flags() ^ Qt.ItemIsEditable)
    self.table.setItem(row, CODE_COLUMN, item)
def set_phrase_table_item(self, row, phrase)

Set phrase at row.

Parameters

row : int
Row to set code.
phrase : str
Phrase to set.
Expand source code
def set_phrase_table_item(self, row, phrase):
    """Set phrase at row.

    Parameters
    ----------
    row : int
        Row to set code.
    phrase : str
        Phrase to set.
    """
    item = QTableWidgetItem(phrase)
    self.table.setItem(row, PHRASE_COLUMN, item)
class Meta (name, bases, namespace, **kwargs)

Used as a metaclass to enable multiple inheritance.

Expand source code
class Meta(type(ModuleBase), type(QWidget)):
    """Used as a metaclass to enable multiple inheritance."""

Ancestors

  • abc.ABCMeta
  • Shiboken.ObjectType
  • builtins.type
class NoMoreAvailableCategories (...)

There are no more available categories.

Expand source code
class NoMoreAvailableCategories(Exception):
    """There are no more available categories."""

Ancestors

  • builtins.Exception
  • builtins.BaseException
class NoMoreAvailableWords (...)

There are no more available words.

Expand source code
class NoMoreAvailableWords(Exception):
    """There are no more available words."""

Ancestors

  • builtins.Exception
  • builtins.BaseException