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
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)
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)
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
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
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)
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)
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)
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