Beispiel #1
0
    def parse_amount(cls, amount: str, locale=LC_NUMERIC):
        """
        Parses an amount string

        Args:
            amount (): The amount string to parse
            locale: The user's locale for parsing

        Returns:
            (float, float): amount and range amount

            Both values can be none (although a amount of none and a range amount with a value isn't legal) with a
            valid amount_string - for example "some" wouldn't have any amount

        Raises:
            ValueError: The amount string is invalid
        """

        # An amount can have the following valid formats
        # 1.) None/Empty string ("Some")
        # 2.) A single amount (the usual case): "5 kg")
        # 3.) A range amount: "5 - 7"

        # 1.) - empty string
        if nullify(amount) is None:
            return None, None

        # 2.) and 3.)
        values = amount.split("-")
        if (len(values)) == 1:
            # Only one value (amount)
            return float(parse_decimal(amount, locale)), None

        if len(values) > 2:
            # More than one -
            raise ValueError()

        amount_value = None
        range_value = None

        if nullify(values[0]) is not None:
            amount_value = float(parse_decimal(values[0], locale))
        if nullify(values[1]) is not None:
            range_value = float(parse_decimal(values[1], locale))

        # Something on the line of "- 5"
        if amount_value is None and range_value is not None:
            raise ValueError()

        # Just a convenience - sort both values
        if amount_value is not None and range_value is not None:
            if amount_value > range_value:
                amount_value, range_value = range_value, amount_value

        return amount_value, range_value
    def setData(self,
                index: QtCore.QModelIndex,
                value: typing.Any,
                role: int = ...) -> bool:
        if self.immutable:
            return False

        if index.isValid():
            if role in (QtCore.Qt.EditRole, QtCore.Qt.CheckStateRole,
                        QtCore.Qt.UserRole):

                # For the optional column. Otherwise ingredient and amount have told the controller that something
                # is going to change
                self.dataToBeChanged.emit()

                column = index.column()

                # The item to manipulate
                ingredient_list_item = self._recipe.ingredientlist[int(
                    index.siblingAtColumn(
                        self.IngredientColumns.INGREDIENTLISTROW).data(
                            role=QtCore.Qt.DisplayRole))]
                if column == self.IngredientColumns.OPTIONAL and role == QtCore.Qt.CheckStateRole:
                    ingredient_list_item.optional = (
                        value == QtCore.Qt.Checked)

                if column == self.IngredientColumns.INGREDIENT and role == QtCore.Qt.EditRole:
                    new_name = nullify(value)

                    # There's no point in having a empty name - well, in that case we could
                    # display/use the ingredient's generic name, but this might be quite confusing
                    # for the user
                    if new_name is None:
                        return False

                    old_name = ingredient_list_item.name

                    # This means that a change in the group's name will be visible to all
                    # recipes having this pseudo ingredient. Not exactly sure if it's the
                    # right thing to to do, on the other hand it's more consistent
                    if old_name is None and ingredient_list_item.ingredient.is_group:
                        ingredient_list_item.ingredient.name = new_name
                    else:
                        ingredient_list_item.name = new_name

                if column == self.IngredientColumns.AMOUNT and role == QtCore.Qt.UserRole:
                    (amount, range_amount), unit_string = value
                    ingredient_list_item.amount = amount
                    ingredient_list_item.range_amount = range_amount
                    ingredient_list_item.unit = data.IngredientUnit.unit_dict[
                        unit_string]

                    self.setData(
                        index,
                        QtCore.QVariant(ingredient_list_item.amount_string()),
                        QtCore.Qt.DisplayRole)

            return super().setData(index, value, role)
        return False
Beispiel #3
0
    def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
        unitname = nullify(value)
        # This should never happen, because there *is* a empty value as a valid unit
        if unitname is None:
            return False

        self.unitsToBeChanged.emit()

        # It's pure guesswork which type of unit the user wanted
        data.IngredientUnit.get_or_add_ingredient_unit_name(self.session, name=unitname,
                                                            type_=data.IngredientUnit.UnitType.UNSPECIFIC)
        data.IngredientUnit.update_unit_dict(self.session)
        return super().setData(index, value, role)
Beispiel #4
0
    def setData(self,
                index: QtCore.QModelIndex,
                value: typing.Any,
                role: int = ...) -> bool:
        column = index.column()
        row = index.row()

        if row >= 0 and column == self.ImageTableColumns.DESCRIPTION:
            imagelist_row = int(
                index.siblingAtColumn(self.ImageTableColumns.IMAGE).data(
                    QtCore.Qt.UserRole))
            self._recipe.imagelist[imagelist_row].description = nullify(value)

        return super().setData(index, value, role)
Beispiel #5
0
    def setData(self,
                index: QtCore.QModelIndex,
                value: typing.Any,
                role: int = ...) -> bool:
        # TODO: Maybe test for the role?
        value = nullify(value)
        if value is None:
            return False

        column = index.internalId()
        row = index.row()
        root_row = self._parent_row[self.Columns.ROOT]

        item = None
        if column == self.Columns.INGREDIENTLIST_ENTRIES:
            item = self._item_lists[self.Columns.INGREDIENTLIST_ENTRIES][row]
        else:
            item = self._item_lists[column][row][0]

        if item.name == value:
            # User has double clicked without changing the value
            return False

        # Test if the value already exists - but only in case of items.
        if column == self.Columns.ITEMS:
            the_table = self._first_column[root_row][0]
            duplicate = self._session.query(the_table).filter(
                the_table.name == value).first()
            if duplicate:
                if duplicate == item:
                    # The same item - user has double clicked and then again. Nothing to do here.
                    return False
                else:
                    # Duplicate item. There three possible ways to deal with this:
                    # 1.) Silently discard the change
                    # 2.) Open a Error dialog telling the user about the problem
                    # 3.) Like in drag&drop, merge both items.

                    # Currently: #2

                    self.illegalValue.emit(
                        misc.IllegalValueEntered.ISDUPLICATE, value)
                    return False

        self.changed.emit()
        item.name = value
        self.dataChanged.emit(index, index)
        return True
    def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int = ...) -> bool:
        index_row = index.row()
        index_column = index.column()

        if index_row == index_column:
            # Shouldn't happen.
            return False

        unit_horizontal = self._unit_list[index_column]
        unit_vertical = self._unit_list[index_row]
        factor = unit_horizontal.factor
        if factor is None:
            # Bug from initializing the database with wrong defaults
            factor = 1.0

        if unit_vertical in data.IngredientUnit.base_units.values():
            # Should also not happen - flags should have been set
            return False

        value = nullify(value)
        _translate = translate

        if value is None:
            self.illegalValue.emit(misc.IllegalValueEntered.ISEMPTY, None)
            return False
        try:
            value = float(parse_decimal(value))
        except NumberFormatError:
            # The user has entered something strange.
            self.illegalValue.emit(misc.IllegalValueEntered.ISNONUMBER, value)
            return False

        if value == 0:
            self.illegalValue.emit(misc.IllegalValueEntered.ISZERO, "0")
            return False

        if value < 0.0:
            self.illegalValue.emit(misc.IllegalValueEntered.ISNONUMBER, str(value))
            return False

        self.changed.emit()
        unit_vertical.factor = value * factor
        if unit_horizontal.factor is None:
            # Compensate for init bug
            unit_horizontal.factor = 1.0
        self.reload_model()
        return True
Beispiel #7
0
def ask_database() -> (str, bool):
    _translate = translate
    initialize = True

    home_directory = QtCore.QStandardPaths.writableLocation(
        QtCore.QStandardPaths.HomeLocation)
    suggested_filename = QtCore.QDir(home_directory).filePath("qisit.db")
    filename, filter_ = QtWidgets.QFileDialog.getSaveFileName(
        None,
        caption=_translate("StartUp", "Open or create new database"),
        directory=suggested_filename,
        options=QtWidgets.QFileDialog.DontConfirmOverwrite)
    database = nullify(filename)
    if database is not None:
        initialize = not QtCore.QFileInfo.exists(filename)
        database = f"sqlite:///{database}"
    return database, initialize
Beispiel #8
0
    def search_recipe_textChanged(self, text: str):
        """
        The user entered some input in the search recipe LineEdit


        Args:
            text ():

        Returns:

        """
        search_text = nullify(text)
        if search_text != self._current_search_text:
            # A real change (not spaces...). If the search field is empty, search_text will be None
            self.table_model.offset = 0
            self._current_search_text = search_text
            self.table_model.search_title = search_text
            self._reload_model()
    def actionAdd_Ingredient_triggered(self, checked: bool = False):
        """
        Asks the user for a new ingredient

        Args:
            checked (): Ignored

        Returns:

        """

        _translate = translate
        new_ingredient_name, ok = Qt.QInputDialog.getText(
            self, _translate("DataEditor", "Add New Ingredient"),
            _translate("DataEditor", "New Ingredient"))
        if ok:
            new_ingredient_name = nullify(new_ingredient_name)
            if new_ingredient_name:
                self.set_modified()
                new_ingredient_name = data.Ingredient.get_or_add_ingredient(
                    self._session, new_ingredient_name)
                self._item_model.new_ingredient_item()
    def okButton_clicked(self):
        """
        OK button has been clicked - save the values

        Returns:

        """

        if self._selected_index is None:
            # Huh?
            return

        model = self._item_model
        stackedwidget_index = self.stackedWidget.currentIndex()

        the_item = self._get_item_for_stackedwidget(self._selected_index)
        new_name = nullify(self.nameLineEdit.text())
        self.set_modified()
        if new_name is None:
            self.nameLineEdit.setText(the_item.name)
        else:
            # This will take care of saving the value
            model.setData(self._selected_index, new_name, QtCore.Qt.EditRole)

            # Trimmed name / rejected
            self.nameLineEdit.setText(the_item.name)

        # Items with descriptions - Author, Cuisine, Yield Units
        if stackedwidget_index == self.StackedItems.ITEM_WITH_DESCRIPTION:
            new_description = nullify(self.descriptionTextEdit.toPlainText())
            the_item.description = new_description

        elif stackedwidget_index == self.StackedItems.INGREDIENT_UNIT:
            new_type = self.typeComboBox.currentIndex()
            if new_type >= 0:
                # new_type == -1 should never happen - it would mean no type has been selected which should be
                # impossible. However, better play it safe :-)
                the_item.type_ = new_type
                new_factor = nullify(self.factorLineEdit.text())

                # Depending on the type of  the factor (whatever the user entered) should either be None or have a
                # value. Unit Type GROUP isn't visible for the user so don't bother to check
                if new_type != data.IngredientUnit.UnitType.UNSPECIFIC:
                    if new_factor is None:
                        self.illegal_value(misc.IllegalValueEntered.ISEMPTY,
                                           None)
                        new_factor = the_item.factor
                    else:
                        try:
                            new_factor = parse_decimal(new_factor)
                            if math.isclose(new_factor, 0.0):
                                self.illegal_value(
                                    misc.IllegalValueEntered.ISZERO, "0")
                                new_factor = the_item.factor
                            if new_factor < 0.0:
                                self.illegal_value(
                                    misc.IllegalValueEntered.ISNONUMBER,
                                    new_factor)
                                new_factor = the_item.factor
                        except NumberFormatError:
                            # The user has entered something strange.
                            self.illegal_value(
                                misc.IllegalValueEntered.ISNONUMBER,
                                new_factor)
                            new_factor = the_item.factor
                else:
                    # Unspecific -> no Factor
                    new_factor = None

                if new_factor is not None:
                    self.factorLineEdit.setText(str(new_factor))
                else:
                    self.factorLineEdit.clear()
                the_item.factor = new_factor

        elif stackedwidget_index == self.StackedItems.INGREDIENTS:
            the_item.icon = self._ingredient_icon

        self.okButton.setEnabled(False)
        self.cancelButton.setEnabled(False)
Beispiel #11
0
    def actionGourmnet_DB_triggered(self, checked=False):
        """
        Import Gourmet's DB

        Args:
            checked ():

        Returns:

        """

        _translate = self._translate

        home_directory = QtCore.QStandardPaths.writableLocation(
            QtCore.QStandardPaths.HomeLocation)

        # TODO: Depending on the OS?
        directory = f"{home_directory}/.gourmet"

        if not QtCore.QDir(directory).exists():
            directory = home_directory

        database_filter = _translate("RecipeWindow",
                                     "Gourmet's database file (recipes.db)")
        options = Qt.QFileDialog.ReadOnly
        filename, _ = Qt.QFileDialog.getOpenFileName(
            self,
            caption=_translate("RecipeWindow", "Select Gourmet's DB"),
            directory=directory,
            filter=database_filter,
            options=options)
        filename = nullify(filename)
        if filename is not None:
            gourmet_engine = create_engine(f"sqlite:///{filename}", echo=False)

            gourmetdb.GourmetSession.configure(bind=gourmet_engine)
            gourmet_session = gourmetdb.GourmetSession()

            progress_dialog = QtWidgets.QProgressDialog(self)
            progress_dialog.setWindowTitle(
                _translate("RecipeListWindow", "Importing Gourmet DB"))
            progress_dialog.setCancelButtonText(
                _translate("RecipeListWindow", "Abort"))
            progress_dialog.setModal(True)
            importer = QTImportGourmet(progress_dialog, gourmet_session,
                                       self._session)
            try:
                errors = importer.import_gourmet(check_duplicates=True)
                if errors:
                    for error in errors:
                        print(f"{error}: {errors[error]}")
                self.update_filters()
                self._reload_model()
            except exc.OperationalError as error:
                importer.abort()
                progress_dialog.close()
                Qt.QMessageBox.critical(
                    self,
                    _translate("RecipeWindow", "Error importing Gourmet DB"),
                    _translate("RecipeWindow", "Error importing Gourmet DB!"))
                # TODO: Log
                print(error)
Beispiel #12
0
    def __import_ingredients(self, gourmet_ingredient: gdata.Ingredients,
                             qisit_recipe: data.Recipe):
        """
        Import an ingredient into a recipe

        Args:
            gourmet_ingredient (): Gourmet's ingredient
            qisit_recipe (): The recipe to import to

        Returns:

        """
        _translate = self._translate

        # Test if the ingredient is part of a group
        gourmet_inggroup = nullify(gourmet_ingredient.inggroup)
        group_item = None

        if gourmet_inggroup:
            # The ingredient is part of the group stored in gourmet_inggroup
            group = data.Ingredient.get_or_add_ingredient(self._qisit,
                                                          gourmet_inggroup,
                                                          is_group=True)

            # Find out if there's already a group with the name in the ingredient list. Note: assumes that there's
            # only one group (or none) with the name stored in gourmet_inggroup. This is a safe assumption, two
            # (or more) groups of ingredients with the same name make no sense, although Qisit's db design would
            # theoretically allow this (there's no way to prevent it with a simple CHECK constraint)
            group_item = self._qisit.query(data.IngredientListEntry).filter(
                data.IngredientListEntry.recipe == qisit_recipe,
                data.IngredientListEntry.ingredient == group).first()
            if not group_item:
                # New group for the recipe
                group_position = data.IngredientListEntry.get_position_for_new_group(
                    self._qisit, qisit_recipe)
                group_item = data.IngredientListEntry(
                    recipe=qisit_recipe,
                    ingredient=group,
                    unit=data.IngredientUnit.unit_group,
                    position=group_position)
                self._qisit.add(group_item)
                self._qisit.merge(group_item)

        # Convert the ingredient data
        qisit_amount = gourmet_ingredient.amount
        qisit_range_amount = gourmet_ingredient.rangeamount

        # For an empty (or None/NULL) unit there's a special unit/unit_name: the base unit, singular ""
        if not gourmet_ingredient.unit:
            gourmet_unit_name = ""
        else:
            gourmet_unit_name = gourmet_ingredient.unit.strip()

        qisit_ingredient_unit = None

        if gourmet_unit_name in data.IngredientUnit.unit_dict:
            qisit_ingredient_unit = data.IngredientUnit.unit_dict[
                gourmet_unit_name]
        else:
            # A (yet) unknown unit. Well, time to take a guess - volume? mass? quantity? Probably unspecific
            qisit_ingredient_unit = data.IngredientUnit(
                type_=data.IngredientUnit.UnitType.UNSPECIFIC,
                name=gourmet_unit_name,
                factor=None,
                cldr=False,
                description=_translate("ImportGourmet",
                                       f"{gourmet_unit_name} (imported)"))
            self._qisit.add(qisit_ingredient_unit)
            self._qisit.merge(qisit_ingredient_unit)
            data.IngredientUnit.unit_dict[
                gourmet_unit_name] = qisit_ingredient_unit
            self._imported_ingredient_units += 1

        qisit_name = nullify(gourmet_ingredient.item)

        gourmet_ingkey = nullify(gourmet_ingredient.ingkey)

        if gourmet_ingkey is None:
            # Such an ingredient shouldn't be in the database. Unfortunately Gourmet doesn't check input
            # very thoroughly...
            if qisit_name:
                # This shouldn't be possible, but better safe than sorry
                gourmet_ingkey = qisit_name
            else:
                return

        group_position = None
        if group_item is not None:
            group_position = group_item.position
        qisit_ingredient = data.Ingredient.get_or_add_ingredient(
            self._qisit, gourmet_ingkey)
        qisit_position = data.IngredientListEntry.get_position_for_ingredient(
            self._qisit, recipe=qisit_recipe, parent=group_position)
        # Time to put everything together
        qisit_ingredient_list_entry = data.IngredientListEntry(
            recipe=qisit_recipe,
            unit=qisit_ingredient_unit,
            ingredient=qisit_ingredient,
            amount=qisit_amount,
            range_amount=qisit_range_amount,
            name=qisit_name,
            optional=gourmet_ingredient.optional,
            position=qisit_position)
        self._qisit.add(qisit_ingredient_list_entry)
        self._qisit.merge(qisit_ingredient_list_entry)
Beispiel #13
0
    def __import_recipe(self, gourmet_recipe: gdata.Recipe) -> data.Recipe:
        """
        Convert / Import a Gourmet recipe into Qisit (basic data like title, categories, author..)

        Args:
            gourmet_recipe (): The recipe

        Returns:
            New Qisit's recipe
        """
        # 1.) Fill out all the recipes data

        # Gourmet uses 0 in rating to mark the recipes as unrated. Qisit uses None for this purpose, allowing
        # 0 to be a valid rating
        rating = gourmet_recipe.rating
        if rating == 0:
            rating = None

        # Empty links are stored as "" in Gourmet
        url = nullify(gourmet_recipe.link)

        last_modified = datetime.fromtimestamp(
            gourmet_recipe.last_modified).date()

        qisit_recipe = data.Recipe(title=gourmet_recipe.title,
                                   description=gourmet_recipe.description,
                                   instructions=nullify(
                                       gourmet_recipe.instructions),
                                   notes=nullify(gourmet_recipe.modifications),
                                   rating=rating,
                                   preparation_time=gourmet_recipe.preptime,
                                   cooking_time=gourmet_recipe.cooktime,
                                   yields=gourmet_recipe.yields,
                                   url=url,
                                   last_modified=last_modified)
        self._qisit.add(qisit_recipe)

        # 2.) Yield unit
        gourmet_yield_unit = nullify(gourmet_recipe.yield_unit)
        if gourmet_yield_unit is not None:
            qisit_recipe.yield_unit_name = data.YieldUnitName.get_or_add_yield_unit_name(
                session_=self._qisit, name=gourmet_yield_unit)

        # 3.) Author
        gourmet_source = nullify(gourmet_recipe.source)
        if gourmet_source:
            qisit_recipe.author = data.Author.get_or_add_author(
                self._qisit, gourmet_source)

        # 4.) Cuisine
        gourmet_cuisine = nullify(gourmet_recipe.cuisine)
        if gourmet_cuisine:
            qisit_recipe.cuisine = data.Cuisine.get_or_add_cusine(
                self._qisit, gourmet_cuisine)

        # 5.) Categories
        # Gourmet really *does* support multiple categories, although this is rather well hidden in the UI
        category_list = []
        gourmet_category_list = gourmet_recipe.categories

        if gourmet_category_list:
            for gourmet_category in gourmet_category_list:
                category_list.append(nullify(gourmet_category.category))

        for gourmet_item in category_list:
            if gourmet_item:
                qisit_category = data.Category.get_or_add_category(
                    self._qisit, gourmet_item)
                qisit_recipe.categories.append(qisit_category)

        self._qisit.merge(qisit_recipe)
        return qisit_recipe