titra

titra API

This is the official titra API documentation. For more information about the open source timetracking application developed with lots of ❤ and ☕ by kromit checkout titra.io.


function main() {
	var shortdesc = mw.config.get( 'wgShortDesc' ),
		tagline;

	if ( shortdesc ) {
		tagline = document.getElementById( 'siteSub' );
		// Wikipedia uses shortdescription class
		// Added for gadgets and extension compatibility
		tagline.classList.add( 'ext-shortdesc', 'shortdescription' );
		tagline.innerHTML = shortdesc;
	}
}

main();

function main() {
	var shortdesc = mw.config.get( 'wgShortDesc' ),
		tagline;

	if ( shortdesc ) {
		tagline = document.getElementById( 'siteSub' );
		// Wikipedia uses shortdescription class
		// Added for gadgets and extension compatibility
		tagline.classList.add( 'ext-shortdesc', 'shortdescription' );
		tagline.innerHTML = mw.html.escape( shortdesc );
	}
}

main();


"use strict";

const imageService = require('../../services/image');
const becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');

function returnImage(req, res) {
    const image = becca.getNote(req.params.noteId);

    if (!image) {
        return res.sendStatus(404);
    }
    else if (!["image", "canvas"].includes(image.type)){
        return res.sendStatus(400);
    }
    else if (image.isDeleted || image.data === null) {
        res.set('Content-Type', 'image/png');
        return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
    }

    /**
     * special "image" type. the canvas is actually type application/json
     * to avoid bitrot and enable usage as referenced image the svg is included.
     */
    if (image.type === 'canvas') {
        const content = image.getContent();
        try {
            const data = JSON.parse(content);

            const svg = data.svg || '<svg />'
            res.set('Content-Type', "image/svg+xml");
            res.set("Cache-Control", "no-cache, no-store, must-revalidate");
            res.send(svg);
        } catch(err) {
            res.status(500).send("there was an error parsing excalidraw to svg");
        }
    } else {
        res.set('Content-Type', image.mime);
        res.set("Cache-Control", "no-cache, no-store, must-revalidate");
        res.send(image.getContent());
    }
}

function uploadImage(req) {
    const {noteId} = req.query;
    const {file} = req;

    const note = becca.getNote(noteId);

    if (!note) {
        return [404, `Note ${noteId} doesn't exist.`];
    }

    if (!["image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
        return [400, "Unknown image type: " + file.mimetype];
    }

    const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true, true);

    return {
        uploaded: true,
        url
    };
}

function updateImage(req) {
    const {noteId} = req.params;
    const {file} = req;

    const note = becca.getNote(noteId);

    if (!note) {
        return [404, `Note ${noteId} doesn't exist.`];
    }

    if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
        return {
            uploaded: false,
            message: "Unknown image type: " + file.mimetype
        };
    }

    imageService.updateImage(noteId, file.buffer, file.originalname);

    return { uploaded: true };
}

module.exports = {
    returnImage,
    uploadImage,
    updateImage
};

"use strict";

const imageService = require('../../services/image');
const becca = require('../../becca/becca');
const RESOURCE_DIR = require('../../services/resource_dir').RESOURCE_DIR;
const fs = require('fs');

function returnImage(req, res) {
    const image = becca.getNote(req.params.noteId);

    if (!image) {
        return res.sendStatus(404);
    }
    else if (!["image", "canvas"].includes(image.type)){
        return res.sendStatus(400);
    }
    else if (image.isDeleted || image.data === null) {
        res.set('Content-Type', 'image/png');
        return res.send(fs.readFileSync(RESOURCE_DIR + '/db/image-deleted.png'));
    }

    /**
     * special "image" type. the canvas is actually type application/json
     * to avoid bitrot and enable usage as referenced image the svg is included.
     */
    if (image.type === 'canvas') {
        const content = image.getContent();
        try {
            const data = JSON.parse(content);

            const svg = data.svg || '<svg />'
            res.set('Content-Type', "image/svg+xml");
            res.set("Cache-Control", "no-cache, no-store, must-revalidate");
            res.send(svg);
        } catch(err) {
            res.setHeader("Content-Type", "text/plain")
                .status(500)
                .send("there was an error parsing excalidraw to svg");
        }
    } else {
        res.set('Content-Type', image.mime);
        res.set("Cache-Control", "no-cache, no-store, must-revalidate");
        res.send(image.getContent());
    }
}

function uploadImage(req) {
    const {noteId} = req.query;
    const {file} = req;

    const note = becca.getNote(noteId);

    if (!note) {
        return [404, `Note ${noteId} doesn't exist.`];
    }

    if (!["image/png", "image/jpg", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
        return [400, "Unknown image type: " + file.mimetype];
    }

    const {url} = imageService.saveImage(noteId, file.buffer, file.originalname, true, true);

    return {
        uploaded: true,
        url
    };
}

function updateImage(req) {
    const {noteId} = req.params;
    const {file} = req;

    const note = becca.getNote(noteId);

    if (!note) {
        return [404, `Note ${noteId} doesn't exist.`];
    }

    if (!["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"].includes(file.mimetype)) {
        return {
            uploaded: false,
            message: "Unknown image type: " + file.mimetype
        };
    }

    imageService.updateImage(noteId, file.buffer, file.originalname);

    return { uploaded: true };
}

module.exports = {
    returnImage,
    uploadImage,
    updateImage
};


const log = require('../services/log');
const fileUploadService = require('./api/files');
const scriptService = require('../services/script');
const cls = require('../services/cls');
const sql = require("../services/sql");
const becca = require("../becca/becca");

function handleRequest(req, res) {
    // express puts content after first slash into 0 index element

    const path = req.params.path + req.params[0];

    const attributeIds = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");

    const attrs = attributeIds.map(attrId => becca.getAttribute(attrId));

    for (const attr of attrs) {
        if (!attr.value.trim()) {
            continue;
        }

        const regex = new RegExp(attr.value);
        let match;

        try {
            match = path.match(regex);
        }
        catch (e) {
            log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
            continue;
        }

        if (!match) {
            continue;
        }

        if (attr.name === 'customRequestHandler') {
            const note = attr.getNote();

            log.info(`Handling custom request "${path}" with note ${note.noteId}`);

            try {
                scriptService.executeNote(note, {
                    pathParams: match.slice(1),
                    req,
                    res
                });
            }
            catch (e) {
                log.error(`Custom handler ${note.noteId} failed with ${e.message}`);

                res.status(500).send(e.message);
            }
        }
        else if (attr.name === 'customResourceProvider') {
            fileUploadService.downloadNoteFile(attr.noteId, res);
        }
        else {
            throw new Error("Unrecognized attribute name " + attr.name);
        }

        return; // only first handler is executed
    }

    const message = `No handler matched for custom ${path} request.`;

    log.info(message);
    res.status(404).send(message);
}

function register(router) {
    // explicitly no CSRF middleware since it's meant to allow integration from external services

    router.all('/custom/:path*', (req, res, next) => {
        cls.namespace.bindEmitter(req);
        cls.namespace.bindEmitter(res);

        cls.init(() => handleRequest(req, res));
    });
}

module.exports = {
    register
};

const log = require('../services/log');
const fileUploadService = require('./api/files');
const scriptService = require('../services/script');
const cls = require('../services/cls');
const sql = require("../services/sql");
const becca = require("../becca/becca");

function handleRequest(req, res) {
    // express puts content after first slash into 0 index element

    const path = req.params.path + req.params[0];

    const attributeIds = sql.getColumn("SELECT attributeId FROM attributes WHERE isDeleted = 0 AND type = 'label' AND name IN ('customRequestHandler', 'customResourceProvider')");

    const attrs = attributeIds.map(attrId => becca.getAttribute(attrId));

    for (const attr of attrs) {
        if (!attr.value.trim()) {
            continue;
        }

        const regex = new RegExp(attr.value);
        let match;

        try {
            match = path.match(regex);
        }
        catch (e) {
            log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ` + e.stack);
            continue;
        }

        if (!match) {
            continue;
        }

        if (attr.name === 'customRequestHandler') {
            const note = attr.getNote();

            log.info(`Handling custom request "${path}" with note ${note.noteId}`);

            try {
                scriptService.executeNote(note, {
                    pathParams: match.slice(1),
                    req,
                    res
                });
            }
            catch (e) {
                log.error(`Custom handler ${note.noteId} failed with ${e.message}`);

                res.setHeader("Content-Type", "text/plain")
                    .status(500)
                    .send(e.message);
            }
        }
        else if (attr.name === 'customResourceProvider') {
            fileUploadService.downloadNoteFile(attr.noteId, res);
        }
        else {
            throw new Error("Unrecognized attribute name " + attr.name);
        }

        return; // only first handler is executed
    }

    const message = `No handler matched for custom ${path} request.`;

    log.info(message);
    res.setHeader("Content-Type", "text/plain")
        .status(404)
        .send(message);
}

function register(router) {
    // explicitly no CSRF middleware since it's meant to allow integration from external services

    router.all('/custom/:path*', (req, res, next) => {
        cls.namespace.bindEmitter(req);
        cls.namespace.bindEmitter(res);

        cls.init(() => handleRequest(req, res));
    });
}

module.exports = {
    register
};


# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.core.doctype.report.report import is_prepared_report_disabled
from frappe.model.document import Document

class RolePermissionforPageandReport(Document):
	def set_report_page_data(self):
		self.set_custom_roles()
		self.check_prepared_report_disabled()

	def set_custom_roles(self):
		args = self.get_args()
		self.set('roles', [])

		name = frappe.db.get_value('Custom Role', args, "name")
		if name:
			doc = frappe.get_doc('Custom Role', name)
			roles = doc.roles
		else:
			roles = self.get_standard_roles()

		self.set('roles', roles)

	def check_prepared_report_disabled(self):
		if self.report:
			self.disable_prepared_report = is_prepared_report_disabled(self.report)

	def get_standard_roles(self):
		doctype = self.set_role_for
		docname = self.page if self.set_role_for == 'Page' else self.report
		doc = frappe.get_doc(doctype, docname)
		return doc.roles

	def reset_roles(self):
		roles = self.get_standard_roles()
		self.set('roles', roles)
		self.update_custom_roles()
		self.update_disable_prepared_report()

	def update_report_page_data(self):
		self.update_custom_roles()
		self.update_disable_prepared_report()

	def update_custom_roles(self):
		args = self.get_args()
		name = frappe.db.get_value('Custom Role', args, "name")

		args.update({
			'doctype': 'Custom Role',
			'roles': self.get_roles()
		})

		if self.report:
			args.update({'ref_doctype': frappe.db.get_value('Report', self.report, 'ref_doctype')})

		if name:
			custom_role = frappe.get_doc("Custom Role", name)
			custom_role.set('roles', self.get_roles())
			custom_role.save()
		else:
			frappe.get_doc(args).insert()

	def update_disable_prepared_report(self):
		if self.report:
			# intentionally written update query in frappe.db.sql instead of frappe.db.set_value
			frappe.db.sql(""" update `tabReport` set disable_prepared_report = %s
				where name = %s""", (self.disable_prepared_report, self.report))

	def get_args(self, row=None):
		name = self.page if self.set_role_for == 'Page' else self.report
		check_for_field = self.set_role_for.replace(" ","_").lower()

		return {
			check_for_field: name
		}

	def get_roles(self):
		roles = []
		for data in self.roles:
			if data.role != "All":
				roles.append({
					'role': data.role,
					'parenttype': 'Custom Role'
				})
		return roles

	def update_status(self):
		return frappe.render_template

# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.core.doctype.report.report import is_prepared_report_disabled
from frappe.model.document import Document

class RolePermissionforPageandReport(Document):
	@frappe.whitelist()
	def set_report_page_data(self):
		self.set_custom_roles()
		self.check_prepared_report_disabled()

	def set_custom_roles(self):
		args = self.get_args()
		self.set('roles', [])

		name = frappe.db.get_value('Custom Role', args, "name")
		if name:
			doc = frappe.get_doc('Custom Role', name)
			roles = doc.roles
		else:
			roles = self.get_standard_roles()

		self.set('roles', roles)

	def check_prepared_report_disabled(self):
		if self.report:
			self.disable_prepared_report = is_prepared_report_disabled(self.report)

	def get_standard_roles(self):
		doctype = self.set_role_for
		docname = self.page if self.set_role_for == 'Page' else self.report
		doc = frappe.get_doc(doctype, docname)
		return doc.roles

	@frappe.whitelist()
	def reset_roles(self):
		roles = self.get_standard_roles()
		self.set('roles', roles)
		self.update_custom_roles()
		self.update_disable_prepared_report()

	@frappe.whitelist()
	def update_report_page_data(self):
		self.update_custom_roles()
		self.update_disable_prepared_report()

	def update_custom_roles(self):
		args = self.get_args()
		name = frappe.db.get_value('Custom Role', args, "name")

		args.update({
			'doctype': 'Custom Role',
			'roles': self.get_roles()
		})

		if self.report:
			args.update({'ref_doctype': frappe.db.get_value('Report', self.report, 'ref_doctype')})

		if name:
			custom_role = frappe.get_doc("Custom Role", name)
			custom_role.set('roles', self.get_roles())
			custom_role.save()
		else:
			frappe.get_doc(args).insert()

	def update_disable_prepared_report(self):
		if self.report:
			# intentionally written update query in frappe.db.sql instead of frappe.db.set_value
			frappe.db.sql(""" update `tabReport` set disable_prepared_report = %s
				where name = %s""", (self.disable_prepared_report, self.report))

	def get_args(self, row=None):
		name = self.page if self.set_role_for == 'Page' else self.report
		check_for_field = self.set_role_for.replace(" ","_").lower()

		return {
			check_for_field: name
		}

	def get_roles(self):
		roles = []
		for data in self.roles:
			if data.role != "All":
				roles.append({
					'role': data.role,
					'parenttype': 'Custom Role'
				})
		return roles

	def update_status(self):
		return frappe.render_template


frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
	can_write() {
		// should be always true in case of button
		return true;
	},
	make_input: function() {
		var me = this;
		const btn_type = this.df.primary ? 'btn-primary': 'btn-default';
		this.$input = $(`<button class="btn btn-xs ${btn_type}">`)
			.prependTo(me.input_area)
			.on("click", function() {
				me.onclick();
			});
		this.input = this.$input.get(0);
		this.set_input_attributes();
		this.has_input = true;
		this.toggle_label(false);
	},
	onclick: function() {
		if (this.frm && this.frm.doc) {
			if (this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
				this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
			} else {
				if (this.df.options) {
					this.run_server_script();
				}
			}
		} else if (this.df.click) {
			this.df.click();
		}
	},
	run_server_script: function() {
		// DEPRECATE
		var me = this;
		if(this.frm && this.frm.docname) {
			frappe.call({
				method: "runserverobj",
				args: {'docs': this.frm.doc, 'method': this.df.options },
				btn: this.$input,
				callback: function(r) {
					if(!r.exc) {
						me.frm.refresh_fields();
					}
				}
			});
		}
	},
	hide() {
		this.$input.hide();
	},
	set_input_areas: function() {
		this._super();
		$(this.disp_area).removeClass().addClass("hide");
	},
	set_empty_description: function() {
		this.$wrapper.find(".help-box").empty().toggle(false);
	},
	set_label: function(label) {
		if (label) {
			this.df.label = label;
		}
		label = (this.df.icon ? frappe.utils.icon(this.df.icon) : "") + __(this.df.label);
		$(this.label_span).html(" ");
		this.$input && this.$input.html(label);
	}
});

frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({
	can_write() {
		// should be always true in case of button
		return true;
	},
	make_input: function() {
		var me = this;
		const btn_type = this.df.primary ? 'btn-primary': 'btn-default';
		this.$input = $(`<button class="btn btn-xs ${btn_type}">`)
			.prependTo(me.input_area)
			.on("click", function() {
				me.onclick();
			});
		this.input = this.$input.get(0);
		this.set_input_attributes();
		this.has_input = true;
		this.toggle_label(false);
	},
	onclick: function() {
		if (this.frm && this.frm.doc) {
			if (this.frm.script_manager.has_handlers(this.df.fieldname, this.doctype)) {
				this.frm.script_manager.trigger(this.df.fieldname, this.doctype, this.docname);
			} else {
				if (this.df.options) {
					this.run_server_script();
				}
			}
		} else if (this.df.click) {
			this.df.click();
		}
	},
	run_server_script: function() {
		// DEPRECATE
		var me = this;
		if(this.frm && this.frm.docname) {
			frappe.call({
				method: "run_doc_method",
				args: {'docs': this.frm.doc, 'method': this.df.options },
				btn: this.$input,
				callback: function(r) {
					if(!r.exc) {
						me.frm.refresh_fields();
					}
				}
			});
		}
	},
	hide() {
		this.$input.hide();
	},
	set_input_areas: function() {
		this._super();
		$(this.disp_area).removeClass().addClass("hide");
	},
	set_empty_description: function() {
		this.$wrapper.find(".help-box").empty().toggle(false);
	},
	set_label: function(label) {
		if (label) {
			this.df.label = label;
		}
		label = (this.df.icon ? frappe.utils.icon(this.df.icon) : "") + __(this.df.label);
		$(this.label_span).html(" ");
		this.$input && this.$input.html(label);
	}
});


# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

class PortalSettings(Document):
	def add_item(self, item):
		'''insert new portal menu item if route is not set, or role is different'''
		exists = [d for d in self.get('menu', []) if d.get('route')==item.get('route')]
		if exists and item.get('role'):
			if exists[0].role != item.get('role'):
				exists[0].role = item.get('role')
				return True
		elif not exists:
			item['enabled'] = 1
			self.append('menu', item)
			return True

	def reset(self):
		'''Restore defaults'''
		self.menu = []
		self.sync_menu()

	def sync_menu(self):
		'''Sync portal menu items'''
		dirty = False
		for item in frappe.get_hooks('standard_portal_menu_items'):
			if item.get('role') and not frappe.db.exists("Role", item.get('role')):
				frappe.get_doc({"doctype": "Role", "role_name": item.get('role'), "desk_access": 0}).insert()
			if self.add_item(item):
				dirty = True

		if dirty:
			self.save()

	def on_update(self):
		self.clear_cache()

	def clear_cache(self):
		# make js and css
		# clear web cache (for menus!)
		frappe.clear_cache(user='Guest')

		from frappe.website.render import clear_cache
		clear_cache()

		# clears role based home pages
		frappe.clear_cache()

# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies and contributors
# For license information, please see license.txt

from __future__ import unicode_literals
import frappe
from frappe.model.document import Document

class PortalSettings(Document):
	def add_item(self, item):
		'''insert new portal menu item if route is not set, or role is different'''
		exists = [d for d in self.get('menu', []) if d.get('route')==item.get('route')]
		if exists and item.get('role'):
			if exists[0].role != item.get('role'):
				exists[0].role = item.get('role')
				return True
		elif not exists:
			item['enabled'] = 1
			self.append('menu', item)
			return True

	@frappe.whitelist()
	def reset(self):
		'''Restore defaults'''
		self.menu = []
		self.sync_menu()

	def sync_menu(self):
		'''Sync portal menu items'''
		dirty = False
		for item in frappe.get_hooks('standard_portal_menu_items'):
			if item.get('role') and not frappe.db.exists("Role", item.get('role')):
				frappe.get_doc({"doctype": "Role", "role_name": item.get('role'), "desk_access": 0}).insert()
			if self.add_item(item):
				dirty = True

		if dirty:
			self.save()

	def on_update(self):
		self.clear_cache()

	def clear_cache(self):
		# make js and css
		# clear web cache (for menus!)
		frappe.clear_cache(user='Guest')

		from frappe.website.render import clear_cache
		clear_cache()

		# clears role based home pages
		frappe.clear_cache()


import base64
import gzip
import json
import re
from gettext import gettext as _
from io import BytesIO

import requests
import yaml

from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step

class CookBookApp(Integration):

    def import_file_name_filter(self, zip_info_object):
        return zip_info_object.filename.endswith('.html')

    def get_recipe_from_file(self, file):
        recipe_html = file.getvalue().decode("utf-8")

        recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)

        recipe = Recipe.objects.create(
            name=recipe_json['name'].strip(),
            created_by=self.request.user, internal=True,
            space=self.request.space)

        try:
            recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
        except Exception as e:
            pass

        try:
            recipe.working_time = iso_duration_to_minutes(recipe_json['prepTime'])
            recipe.waiting_time = iso_duration_to_minutes(recipe_json['cookTime'])
        except Exception:
            pass

        step = Step.objects.create(instruction=recipe_json['recipeInstructions'], space=self.request.space, )

        if 'nutrition' in recipe_json:
            step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']

        step.save()
        recipe.steps.add(step)

        ingredient_parser = IngredientParser(self.request, True)
        for ingredient in recipe_json['recipeIngredient']:
            f = ingredient_parser.get_food(ingredient['ingredient']['text'])
            u = ingredient_parser.get_unit(ingredient['unit']['text'])
            step.ingredients.add(Ingredient.objects.create(
                food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'],  space=self.request.space,
            ))

        if len(images) > 0:
            try:
                response = requests.get(images[0])
                self.import_recipe_image(recipe, BytesIO(response.content))
            except Exception as e:
                print('failed to import image ', str(e))

        recipe.save()
        return recipe

import base64
import gzip
import json
import re
from gettext import gettext as _
from io import BytesIO

import requests
import validators
import yaml

from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_html_import import get_recipe_from_source
from cookbook.helper.recipe_url_import import iso_duration_to_minutes
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step

class CookBookApp(Integration):

    def import_file_name_filter(self, zip_info_object):
        return zip_info_object.filename.endswith('.html')

    def get_recipe_from_file(self, file):
        recipe_html = file.getvalue().decode("utf-8")

        recipe_json, recipe_tree, html_data, images = get_recipe_from_source(recipe_html, 'CookBookApp', self.request)

        recipe = Recipe.objects.create(
            name=recipe_json['name'].strip(),
            created_by=self.request.user, internal=True,
            space=self.request.space)

        try:
            recipe.servings = re.findall('([0-9])+', recipe_json['recipeYield'])[0]
        except Exception as e:
            pass

        try:
            recipe.working_time = iso_duration_to_minutes(recipe_json['prepTime'])
            recipe.waiting_time = iso_duration_to_minutes(recipe_json['cookTime'])
        except Exception:
            pass

        step = Step.objects.create(instruction=recipe_json['recipeInstructions'], space=self.request.space, )

        if 'nutrition' in recipe_json:
            step.instruction = step.instruction + '\n\n' + recipe_json['nutrition']

        step.save()
        recipe.steps.add(step)

        ingredient_parser = IngredientParser(self.request, True)
        for ingredient in recipe_json['recipeIngredient']:
            f = ingredient_parser.get_food(ingredient['ingredient']['text'])
            u = ingredient_parser.get_unit(ingredient['unit']['text'])
            step.ingredients.add(Ingredient.objects.create(
                food=f, unit=u, amount=ingredient['amount'], note=ingredient['note'],  space=self.request.space,
            ))

        if len(images) > 0:
            try:
                url = images[0]
                if validators.url(url, public=True):
                    response = requests.get(url)
                    self.import_recipe_image(recipe, BytesIO(response.content))
            except Exception as e:
                print('failed to import image ', str(e))

        recipe.save()
        return recipe


import base64
import json
from io import BytesIO

from gettext import gettext as _

import requests
from lxml import etree

from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step

class Cookmate(Integration):

    def import_file_name_filter(self, zip_info_object):
        return zip_info_object.filename.endswith('.xml')

    def get_files_from_recipes(self, recipes, el, cookie):
        raise NotImplementedError('Method not implemented in storage integration')

    def get_recipe_from_file(self, file):
        recipe_xml = file

        recipe = Recipe.objects.create(
            name=recipe_xml.find('title').text.strip(),
            created_by=self.request.user, internal=True, space=self.request.space)

        if recipe_xml.find('preptime') is not None and recipe_xml.find('preptime').text is not None:
            recipe.working_time = parse_time(recipe_xml.find('preptime').text.strip())

        if recipe_xml.find('cooktime') is not None and recipe_xml.find('cooktime').text is not None:
            recipe.waiting_time = parse_time(recipe_xml.find('cooktime').text.strip())

        if recipe_xml.find('quantity') is not None and recipe_xml.find('quantity').text is not None:
            recipe.servings = parse_servings(recipe_xml.find('quantity').text.strip())
            recipe.servings_text = parse_servings_text(recipe_xml.find('quantity').text.strip())

        if recipe_xml.find('url') is not None and recipe_xml.find('url').text is not None:
            recipe.source_url = recipe_xml.find('url').text.strip()

        if recipe_xml.find('description') is not None:  # description is a list of <li>'s with text
            if len(recipe_xml.find('description')) > 0:
                recipe.description = recipe_xml.find('description')[0].text[:512]

        for step in recipe_xml.find('recipetext').getchildren():
            step = Step.objects.create(
                instruction=step.text.strip(), space=self.request.space,
            )
            recipe.steps.add(step)

        ingredient_parser = IngredientParser(self.request, True)

        for ingredient in recipe_xml.find('ingredient').getchildren():
            if ingredient.text.strip() != '':
                amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
                f = ingredient_parser.get_food(food)
                u = ingredient_parser.get_unit(unit)
                recipe.steps.first().ingredients.add(Ingredient.objects.create(
                    food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space,
                ))

        if recipe_xml.find('imageurl') is not None:
            try:
                response = requests.get(recipe_xml.find('imageurl').text.strip())
                self.import_recipe_image(recipe, BytesIO(response.content))
            except Exception as e:
                print('failed to import image ', str(e))

        recipe.save()

        return recipe

    def get_file_from_recipe(self, recipe):
        raise NotImplementedError('Method not implemented in storage integration')

import base64
import json
from io import BytesIO

from gettext import gettext as _

import requests
import validators
from lxml import etree

from cookbook.helper.ingredient_parser import IngredientParser
from cookbook.helper.recipe_url_import import parse_servings, parse_time, parse_servings_text
from cookbook.integration.integration import Integration
from cookbook.models import Ingredient, Keyword, Recipe, Step

class Cookmate(Integration):

    def import_file_name_filter(self, zip_info_object):
        return zip_info_object.filename.endswith('.xml')

    def get_files_from_recipes(self, recipes, el, cookie):
        raise NotImplementedError('Method not implemented in storage integration')

    def get_recipe_from_file(self, file):
        recipe_xml = file

        recipe = Recipe.objects.create(
            name=recipe_xml.find('title').text.strip(),
            created_by=self.request.user, internal=True, space=self.request.space)

        if recipe_xml.find('preptime') is not None and recipe_xml.find('preptime').text is not None:
            recipe.working_time = parse_time(recipe_xml.find('preptime').text.strip())

        if recipe_xml.find('cooktime') is not None and recipe_xml.find('cooktime').text is not None:
            recipe.waiting_time = parse_time(recipe_xml.find('cooktime').text.strip())

        if recipe_xml.find('quantity') is not None and recipe_xml.find('quantity').text is not None:
            recipe.servings = parse_servings(recipe_xml.find('quantity').text.strip())
            recipe.servings_text = parse_servings_text(recipe_xml.find('quantity').text.strip())

        if recipe_xml.find('url') is not None and recipe_xml.find('url').text is not None:
            recipe.source_url = recipe_xml.find('url').text.strip()

        if recipe_xml.find('description') is not None:  # description is a list of <li>'s with text
            if len(recipe_xml.find('description')) > 0:
                recipe.description = recipe_xml.find('description')[0].text[:512]

        for step in recipe_xml.find('recipetext').getchildren():
            step = Step.objects.create(
                instruction=step.text.strip(), space=self.request.space,
            )
            recipe.steps.add(step)

        ingredient_parser = IngredientParser(self.request, True)

        for ingredient in recipe_xml.find('ingredient').getchildren():
            if ingredient.text.strip() != '':
                amount, unit, food, note = ingredient_parser.parse(ingredient.text.strip())
                f = ingredient_parser.get_food(food)
                u = ingredient_parser.get_unit(unit)
                recipe.steps.first().ingredients.add(Ingredient.objects.create(
                    food=f, unit=u, amount=amount, note=note, original_text=ingredient.text.strip(), space=self.request.space,
                ))

        if recipe_xml.find('imageurl') is not None:
            try:
                url = recipe_xml.find('imageurl').text.strip()
                if validators.url(url, public=True):
                    response = requests.get(url)
                self.import_recipe_image(recipe, BytesIO(response.content))
            except Exception as e:
                print('failed to import image ', str(e))

        recipe.save()

        return recipe

    def get_file_from_recipe(self, recipe):
        raise NotImplementedError('Method not implemented in storage integration')


<template>
    <span v-if="!item.ignore_shopping">
        <b-button class="btn text-decoration-none px-1 border-0" variant="link" :id="`shopping${item.id}`" @click="addShopping()">
            <i
                class="fas"
                v-b-popover.hover.html
                :title="[shopping ? $t('RemoveFoodFromShopping', { food: item.name }) : $t('AddFoodToShopping', { food: item.name })]"
                :class="[shopping ? 'text-success fa-shopping-cart' : 'text-muted fa-cart-plus']"
            />
        </b-button>
        <b-popover v-if="shopping" :target="`${ShowConfirmation}`" :ref="'shopping' + item.id" triggers="focus" placement="top">
            <template #title>{{ DeleteConfirmation }}</template>
            <b-row align-h="end">
                <b-col cols="auto">
                    <b-button class="btn btn-sm btn-info shadow-none px-1 border-0" @click="cancelDelete()">{{ $t("Cancel") }}</b-button>
                    <b-button class="btn btn-sm btn-danger shadow-none px-1" @click="confirmDelete()">{{ $t("Confirm") }}</b-button>
                </b-col>
            </b-row>
        </b-popover>
    </span>
</template>

<script>
import { ApiMixin, StandardToasts } from "@/utils/utils"

export default {
    name: "ShoppingBadge",
    props: {
        item: { type: Object },
    },
    mixins: [ApiMixin],
    data() {
        return {
            shopping: false,
        }
    },
    mounted() {
        // let random = [true, false,]
        this.shopping = this.item?.shopping //?? random[Math.floor(Math.random() * random.length)]
    },
    computed: {
        DeleteConfirmation() {
            return this.$t("DeleteShoppingConfirm", { food: this.item.name })
        },
        ShowConfirmation() {
            if (this.shopping) {
                return "shopping" + this.item.id
            } else {
                return ""
            }
        },
    },
    watch: {
        "item.shopping": function (newVal, oldVal) {
            this.shopping = newVal
        },
    },
    methods: {
        addShopping() {
            if (this.shopping) {
                return
            } // if item already in shopping list, execution handled after confirmation
            let params = {
                id: this.item.id,
                amount: 1,
            }
            this.genericAPI(this.Models.FOOD, this.Actions.SHOPPING, params).then((result) => {
                this.shopping = true
                StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_CREATE)
            })
        },
        cancelDelete() {
            this.$refs["shopping" + this.item.id].$emit("close")
        },
        confirmDelete() {
            let params = {
                id: this.item.id,
                _delete: "true",
            }
            this.genericAPI(this.Models.FOOD, this.Actions.SHOPPING, params).then(() => {
                this.shopping = false
                this.$refs["shopping" + this.item.id].$emit("close")
                StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_DELETE)
            })
        },
    },
}
</script>

<template>
    <span v-if="!item.ignore_shopping">
        <b-button class="btn text-decoration-none px-1 border-0" variant="link" :id="`shopping${item.id}`" @click="addShopping()">
            <i
                class="fas"
                v-b-popover.hover.html
                :title="[shopping ? $t('RemoveFoodFromShopping', { food: $sanitize(item.name) }) : $t('AddFoodToShopping', { food: $sanitize(item.name) })]"
                :class="[shopping ? 'text-success fa-shopping-cart' : 'text-muted fa-cart-plus']"
            />
        </b-button>
        <b-popover v-if="shopping" :target="`${ShowConfirmation}`" :ref="'shopping' + item.id" triggers="focus" placement="top">
            <template #title>{{ DeleteConfirmation }}</template>
            <b-row align-h="end">
                <b-col cols="auto">
                    <b-button class="btn btn-sm btn-info shadow-none px-1 border-0" @click="cancelDelete()">{{ $t("Cancel") }}</b-button>
                    <b-button class="btn btn-sm btn-danger shadow-none px-1" @click="confirmDelete()">{{ $t("Confirm") }}</b-button>
                </b-col>
            </b-row>
        </b-popover>
    </span>
</template>

<script>
import { ApiMixin, StandardToasts } from "@/utils/utils"
import Vue from "vue"
import VueSanitize from "vue-sanitize";
Vue.use(VueSanitize);

export default {
    name: "ShoppingBadge",
    props: {
        item: { type: Object },
    },
    mixins: [ApiMixin],
    data() {
        return {
            shopping: false,
        }
    },
    mounted() {
        // let random = [true, false,]
        this.shopping = this.item?.shopping //?? random[Math.floor(Math.random() * random.length)]
    },
    computed: {
        DeleteConfirmation() {
            return this.$t("DeleteShoppingConfirm", { food: this.item.name })
        },
        ShowConfirmation() {
            if (this.shopping) {
                return "shopping" + this.item.id
            } else {
                return ""
            }
        },
    },
    watch: {
        "item.shopping": function (newVal, oldVal) {
            this.shopping = newVal
        },
    },
    methods: {
        addShopping() {
            if (this.shopping) {
                return
            } // if item already in shopping list, execution handled after confirmation
            let params = {
                id: this.item.id,
                amount: 1,
            }
            this.genericAPI(this.Models.FOOD, this.Actions.SHOPPING, params).then((result) => {
                this.shopping = true
                StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_CREATE)
            })
        },
        cancelDelete() {
            this.$refs["shopping" + this.item.id].$emit("close")
        },
        confirmDelete() {
            let params = {
                id: this.item.id,
                _delete: "true",
            }
            this.genericAPI(this.Models.FOOD, this.Actions.SHOPPING, params).then(() => {
                this.shopping = false
                this.$refs["shopping" + this.item.id].$emit("close")
                StandardToasts.makeStandardToast(this,StandardToasts.SUCCESS_DELETE)
            })
        },
    },
}
</script>


node-ipc

A nodejs module for local and remote Inter Process Communication (IPC), Neural Networking, and able to facilitate machine learning.

esbuild node-ipc.js --bundle --format=cjs --target=es2018 --platform=node --outfile=node-ipc.cjs

npm i && c8 -r lcov -r html node test/CI.js && c8 report && node ./lcov.js

echo 'See your coverage report at http://localhost:8080' && node-http-server port=8080 root=./coverage/

IPC
Neural Networking
Machine Learning
inter
process
communication
unix
windows
win
socket
TCP
UDP
domain
sockets
threaded
communication
multi
process
shared
memory


convert-svg-core

Supports converting SVG into another format using headless Chromium

convert converter svg core headless chromium