diff --git a/.gitignore b/.gitignore index c053bb1b..fa37b356 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tags wiki/docs/current wiki/public/css +wiki/public/dist diff --git a/README.md b/README.md index 16d338ca..e53e00b3 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ -Simple Wiki App built on the [Frappe Framework](https://frappeframework.com) +Simple Wiki App built on the [Frappe Framework](https://frappeframework.com). Powers [docs.erpnext.com](http://docs.erpnext.com/) + ## Installation @@ -16,25 +17,31 @@ $ bench --site sitename install-app wiki ## Features 1. Create Wiki Pages -2. Author content in markdown -3. Track page edits with revisions +2. Author content in Markdown or Rich Text +3. Set-up Controlled Wiki Updates +4. Unlimited Sidebar Hierarchy +5. Add attachments +6. Table of Contents +7. Caching +8. Custom Script Support via `Wiki Settings` ## Screenshots ### 1. Rendered Page - + -### 2. New Page - +### 2. Edit Page + -### 3. Edit Page - +### 3. Review Edited Page + ### 4. Revisions - + ### 5. Compare changes - + + #### License diff --git a/wiki/hooks.py b/wiki/hooks.py index 40049ec4..cd83aebf 100644 --- a/wiki/hooks.py +++ b/wiki/hooks.py @@ -11,6 +11,14 @@ app_email = "developers@frappe.io" app_license = "MIT" +page_renderer = "wiki.wiki.doctype.wiki_page.wiki_renderer.WikiPageRenderer" + +website_route_rules = [ + {"from_route": "//edit", "to_route": "/edit"}, + {"from_route": "//new", "to_route": "/new"}, + {"from_route": "//revisions", "to_route": "/revisions"}, +] + # Includes in # ------------------ diff --git a/wiki/install.py b/wiki/install.py index d0dc180c..4f77b97b 100644 --- a/wiki/install.py +++ b/wiki/install.py @@ -9,31 +9,21 @@ def after_install(): # create the wiki homepage page = frappe.new_doc("Wiki Page") page.title = "Home" + page.route = "home" page.content = "Welcome to the homepage of your wiki!" page.published = True page.insert() # create the wiki sidebar - sidebar = frappe.new_doc("Website Sidebar") - sidebar.title = "Wiki Sidebar" + sidebar = frappe.new_doc("Wiki Sidebar") + sidebar.title = "Wiki" + sidebar.route = "wiki" sidebar.append( - "sidebar_items", {"title": "Home", "route": "/wiki/home", "group": "Pages"} - ) - sidebar.append( - "sidebar_items", - { - "title": "Edit Sidebar", - "route": "/desk#Form/Website Sidebar/Wiki Sidebar", - "group": "Manage Wiki", - }, - ) - sidebar.append( - "sidebar_items", - {"title": "Settings", "route": "/desk#Form/Wiki Settings", "group": "Manage Wiki"}, + "sidebar_items", {"item": page.name} ) sidebar.insert() # set the sidebar in settings settings = frappe.get_single("Wiki Settings") - settings.sidebar = "Wiki Sidebar" + settings.sidebar = sidebar.name settings.save() diff --git a/wiki/patches.txt b/wiki/patches.txt index e23470c5..8ef29f6b 100644 --- a/wiki/patches.txt +++ b/wiki/patches.txt @@ -1 +1,2 @@ wiki.wiki.doctype.wiki_page.patches.set_allow_guest +wiki.wiki.doctype.wiki_page.patches.delete_is_new diff --git a/wiki/public/build.json b/wiki/public/build.json index aad6a21c..ab471142 100644 --- a/wiki/public/build.json +++ b/wiki/public/build.json @@ -1,3 +1,4 @@ { - "wiki/css/wiki.css": ["public/scss/wiki.scss"] -} + "wiki/css/wiki.css": ["public/scss/wiki.scss"], + "wiki/js/wiki.min.js": ["www/editu.js"] +} \ No newline at end of file diff --git a/wiki/public/js/edit_asset.js b/wiki/public/js/edit_asset.js new file mode 100644 index 00000000..a55ad7bf --- /dev/null +++ b/wiki/public/js/edit_asset.js @@ -0,0 +1,468 @@ +window.EditAsset = class EditAsset { + constructor() { + this.make_code_field_group(); + this.add_attachment_popover(); + this.set_code_editor_height(); + this.render_preview(); + this.add_attachment_handler(); + this.set_listeners(); + this.create_comment_box(); + this.make_title_editable(); + } + + make_code_field_group() { + this.code_field_group = new frappe.ui.FieldGroup({ + fields: [ + { + fieldname: "type", + fieldtype: "Select", + default: "Markdown", + options: "Markdown\nRich-Text(Experimental)", + }, + { + fieldtype: "Column Break", + }, + { + fieldname: "attachment_controls", + fieldtype: "HTML", + options: this.get_attachment_controls_html(), + }, + { + fieldtype: "Section Break", + }, + { + fieldname: "code_html", + fieldtype: "Text Editor", + default: $(".wiki-content-html").html(), + depends_on: 'eval:doc.type=="Rich-Text(Experimental)"', + }, + { + fieldname: "code_md", + fieldtype: "Code", + options: "Markdown", + default: $(".wiki-content-md").text(), + depends_on: 'eval:doc.type=="Markdown"', + }, + ], + body: $(".wiki-write").get(0), + }); + this.code_field_group.make(); + $(".wiki-write .form-section:last").removeClass("empty-section"); + } + + get_attachment_controls_html() { + return ` + + + + 0 attachments + + + + + Upload Attachment + + + +`; + } + + add_attachment_popover() { + let picker_wrapper = $("skjdfjs"); + + $(".show-attachments").popover({ + trigger: "click", + placement: "bottom", + + content: () => { + return this.build_attachment_table(); + }, + html: true, + }); + } + + build_attachment_table() { + var wrapper = $(''); + wrapper.empty(); + + var table = $(this.get_attachment_table_header_html()).appendTo(wrapper); + if (!this.attachments || !this.attachments.length) + return "No attachments uploaded"; + + this.attachments.forEach((f) => { + const row = $("").appendTo(table.find("tbody")); + $(`${f.file_name}`).appendTo(row); + // $(`${f.file_url}`).appendTo(row); + $(` + + Copy Link + + `).appendTo(row); + $(` + + + + + + `).appendTo(row); + }); + return wrapper; + } + + get_attachment_table_header_html() { + return ` + + `; + } + + set_code_editor_height() { + setTimeout(() => { + // expand_code_editor + const code_md = this.code_field_group.get_field("code_md"); + code_md.expanded = !this.expanded; + code_md.refresh_height(); + code_md.toggle_label(); + }, 120); + } + + raise_patch() { + var side = {}; + + let name = $(".doc-sidebar .web-sidebar").get(0).dataset.name; + side[name] = []; + let items = $($(".doc-sidebar .web-sidebar").get(0)) + .children(".sidebar-items") + .children("ul") + .not(".hidden") + .children("li"); + items.each((item) => { + if (!items[item].dataset.name) return; + side[name].push({ + name: items[item].dataset.name, + type: items[item].dataset.type, + new: items[item].dataset.new, + title: items[item].dataset.title, + group_name: items[item].dataset.groupName, + }); + }); + + $('.doc-sidebar [data-type="Wiki Sidebar"]').each(function () { + let name = $(this).get(0).dataset.groupName; + side[name] = []; + let items = $(this).children("ul").children("li"); + items.each((item) => { + if (!items[item].dataset.name) return; + side[name].push({ + name: items[item].dataset.name, + type: items[item].dataset.type, + new: items[item].dataset.new, + title: items[item].dataset.title, + group_name: items[item].dataset.groupName, + }); + }); + }); + + var me = this; + var dfs = []; + const title_of_page = $('[name="title_of_page"]').val(); + dfs.push( + { + fieldname: "edit_message", + fieldtype: "Text", + label: "Message", + default: $('[name="new"]').val() + ? `Add new page: ${title_of_page}` + : `Edited ${title_of_page}`, + mandatory: 1, + }, + { + fieldname: "sidebar_edited", + fieldtype: "Check", + label: "I Updated the sidebar", + default: $('[name="new"]').val() ? 1 : 0, + } + ); + + let dialog = new frappe.ui.Dialog({ + fields: dfs, + title: __("Please describe your changes"), + primary_action: function () { + frappe.call({ + method: "wiki.wiki.doctype.wiki_page.wiki_page.update", + args: { + name: $('[name="wiki_page"]').val(), + wiki_page_patch: $('[name="wiki_page_patch"]').val(), + message: this.get_value("edit_message"), + sidebar_edited: this.get_value("sidebar_edited"), + content: me.content, + type: me.code_field_group.get_value("type"), + attachments: me.attachments, + new: $('[name="new"]').val(), + title: $('[name="title_of_page"]').val(), + new_sidebar: $(".doc-sidebar").get(0).innerHTML, + new_sidebar_items: side, + }, + callback: () => { + frappe.msgprint({ + message: + "A Change Request has been created. You can track your requests on the contributions page", + indicator: "green", + title: "Change Request Created", + }); + window.location.href = "/contributions"; + }, + freeze: true, + }); + dialog.hide(); + $("#freeze").addClass("show"); + }, + }); + dialog.show(); + } + + render_preview() { + $('a[data-toggle="tab"]').on("click", (e) => { + let activeTab = $(e.target); + + if ( + activeTab.prop("id") === "preview-tab" || + activeTab.prop("id") === "diff-tab" + ) { + let $preview = $(".wiki-preview"); + let $diff = $(".wiki-diff"); + const type = this.code_field_group.get_value("type"); + let content = ""; + if (type == "Markdown") { + content = this.code_field_group.get_value("code_md"); + } else { + content = this.code_field_group.get_value("code_html"); + var turndownService = new TurndownService(); + turndownService = turndownService.keep(["div class", "iframe"]); + content = turndownService.turndown(content); + } + if (!content) { + this.set_empty_message($preview, $diff); + return; + } + this.set_loading_message($preview, $diff); + + frappe.call({ + method: "wiki.wiki.doctype.wiki_page.wiki_page.preview", + args: { + content: content, + type: type, + path: this.route, + name: $('[name="wiki_page"]').val(), + attachments: this.attachments, + new: $('[name="new"]').val(), + }, + callback: (r) => { + if (r.message) { + $preview.html(r.message.html); + if (!$('[name="new"]').val()) { + $diff.html(r.message.diff); + } + } + }, + }); + } + }); + } + + set_empty_message($preview, $diff) { + $preview.html("Please add some code"); + $diff.html("Please add some code"); + } + + set_loading_message($preview, $diff) { + $preview.html("Loading preview..."); + $diff.html("Loading diff..."); + } + + add_attachment_handler() { + var me = this; + $(".add-attachment-wiki").click(function () { + me.new_attachment(); + }); + $(".submit-wiki-page").click(function () { + me.get_markdown(); + }); + } + + new_attachment() { + if (this.dialog) { + // remove upload dialog + this.dialog.$wrapper.remove(); + } + + new frappe.ui.FileUploader({ + folder: "Home/Attachments", + on_success: (file_doc) => { + if (!this.attachments) this.attachments = []; + if (!this.save_paths) this.save_paths = {}; + this.attachments.push(file_doc); + $(".wiki-attachment").empty().append(this.build_attachment_table()); + $(".attachment-controls").find(".number").text(this.attachments.length); + }, + }); + } + + get_markdown() { + var me = this; + + if (me.code_field_group.get_value("type") == "Markdown") { + this.content = me.code_field_group.get_value("code_md"); + this.raise_patch(); + } else { + this.content = this.code_field_group.get_value("code_html"); + + frappe.call({ + method: + "wiki.wiki.doctype.wiki_page.wiki_page.extract_images_from_html", + args: { + content: this.content, + }, + callback: (r) => { + if (r.message) { + me.content = r.message; + var turndownService = new TurndownService(); + turndownService = turndownService.keep(["div class", "iframe"]); + me.content = turndownService.turndown(me.content); + me.raise_patch(); + } + }, + }); + } + } + + set_listeners() { + var me = this; + + $(`body`).on("click", `.copy-link`, function () { + frappe.utils.copy_to_clipboard($(this).attr("data-link")); + }); + + $(`body`).on("click", `.delete-button`, function () { + frappe.confirm( + `Are you sure you want to delete the file "${$(this).attr( + "data-name" + )}"`, + () => { + me.attachments.forEach((f, index, object) => { + if (f.file_name == $(this).attr("data-name")) { + object.splice(index, 1); + } + }); + $(".wiki-attachment").empty().append(me.build_attachment_table()); + $(".attachment-controls").find(".number").text(me.attachments.length); + } + ); + }); + } + + create_comment_box() { + this.comment_box = frappe.ui.form.make_control({ + parent: $(".comment-box"), + df: { + fieldname: "new_comment", + fieldtype: "Comment", + }, + enable_mentions: false, + render_input: true, + only_input: true, + on_submit: (comment) => { + this.add_comment_to_patch(comment); + }, + }); + } + + add_comment_to_patch(comment) { + if (strip_html(comment).trim() != "") { + this.comment_box.disable(); + + frappe.call({ + method: + "wiki.wiki.doctype.wiki_page_patch.wiki_page_patch.add_comment_to_patch", + args: { + reference_name: $('[name="wiki_page_patch"]').val(), + content: comment, + comment_email: frappe.session.user, + comment_by: frappe.session.user_fullname, + }, + callback: (r) => { + comment = r.message; + + this.display_new_comment(comment, this.comment_box); + }, + always: () => { + this.comment_box.enable(); + }, + }); + } + } + + display_new_comment(comment, comment_box) { + if (comment) { + comment_box.set_value(""); + + const new_comment = this.get_comment_html( + comment.owner, + comment.creation, + comment.timepassed, + comment.content + ); + + $(".timeline-items").prepend(new_comment); + } + } + + get_comment_html(owner, creation, timepassed, content) { + return $(` + + + + + + + + + + + + ${owner} + + ${timepassed} + + + + + + ${content} + + + + + `); + } + + make_title_editable() { + const title_span = $(".edit-title>span"); + const title_handle = $(".edit-title>i"); + const title_input = $(".edit-title>input"); + title_handle.click(() => { + title_span.addClass("hide"); + title_handle.addClass("hide"); + title_input.removeClass("hide"); + title_input.val(title_span.text()); + title_input.focus(); + }); + title_input.focusout(() => { + title_span.removeClass("hide"); + title_handle.removeClass("hide"); + title_input.addClass("hide"); + title_span.text(title_input.val()); + }); + } +}; diff --git a/wiki/public/js/edit_wiki.js b/wiki/public/js/edit_wiki.js new file mode 100644 index 00000000..a469a439 --- /dev/null +++ b/wiki/public/js/edit_wiki.js @@ -0,0 +1,230 @@ +window.EditWiki = class EditWiki extends Wiki { + constructor() { + super(); + frappe.provide("frappe.ui.keys"); + $("document").ready(() => { + frappe + .call("wiki.wiki.doctype.wiki_page.wiki_page.get_sidebar_for_page", { + wiki_page: $('[name="wiki_page"]').val(), + }) + .then((result) => { + $(".doc-sidebar").empty().append(result.message); + this.activate_sidebars(); + this.set_active_sidebar(); + this.set_empty_ul(); + this.set_sortable(); + this.set_add_item(); + if ($('[name="new"]').first().val()) { + this.add_new_link(); + } + }); + }); + } + + activate_sidebars() { + $(".sidebar-item").each(function (index) { + const active_class = "active"; + let page_href = window.location.pathname; + if (page_href.indexOf("#") !== -1) { + page_href = page_href.slice(0, page_href.indexOf("#")); + } + if (page_href.includes($(this).data("route"))) { + $(this).addClass(active_class); + $(this).find("a").addClass(active_class); + } + }); + // scroll the active sidebar item into view + let active_sidebar_item = $(".sidebar-item.active"); + if (active_sidebar_item.length > 0) { + active_sidebar_item.get(0).scrollIntoView(true, { + behavior: "smooth", + block: "nearest", + }); + } + } + + set_empty_ul() { + $(".collapsible").each(function () { + if ($(this).parent().find("ul").length == 0) { + $(this) + .parent() + .append( + $(` + Add Item`).appendTo( + $(".web-sidebar") + ); + var me = this; + $(".add-sidebar-item").click(function () { + var dfs = me.get_add_new_item_dialog_fields(); + + var dialog = new frappe.ui.Dialog({ + fields: dfs, + primary_action: function (fields) { + if (fields.type == "Add Wiki Page") { + me.add_wiki_page(fields); + } else { + me.add_wiki_sidebar(fields); + } + dialog.hide(); + }, + }); + dialog.show(); + }); + } + + get_add_new_item_dialog_fields() { + return [ + { + fieldname: "type", + label: "Add Type", + fieldtype: "Autocomplete", + options: ["Add Wiki Page", "New Wiki Sidebar"], + }, + { + fieldname: "wiki_page", + label: "Wiki Page", + fieldtype: "Link", + options: "Wiki Page", + depends_on: "eval: doc.type=='Add Wiki Page'", + mandatory_depends_on: "eval: doc.type=='Add Wiki Page'", + }, + { + fieldname: "route", + label: "Name", + fieldtype: "Data", + depends_on: "eval: doc.type=='New Wiki Sidebar'", + mandatory_depends_on: "eval: doc.type=='New Wiki Sidebar'", + }, + { + fieldname: "title", + label: "Title", + fieldtype: "Data", + depends_on: "eval: doc.type=='New Wiki Sidebar'", + mandatory_depends_on: "eval: doc.type=='New Wiki Sidebar'", + }, + ]; + } + + add_wiki_page(fields) { + var me = this; + frappe.call({ + method: "frappe.client.get_value", + args: { + doctype: "Wiki Page", + fieldname: "title", + filters: fields.wiki_page, + }, + callback: function (r) { + let $new_page = me.get_new_page_html(r, fields); + + $new_page.appendTo( + $(".doc-sidebar .sidebar-items") + .children(".list-unstyled") + .not(".hidden") + .first() + ); + }, + }); + } + + get_new_page_html(r, fields) { + return $(` + + + + + ${r.message.title} + + + + + `); + } + + add_wiki_sidebar(fields) { + let $new_page = this.get_wiki_sidebar_html(fields); + + $new_page.appendTo( + $(".doc-sidebar .sidebar-items") + .children(".list-unstyled") + .not(".hidden") + .first() + ); + + $(".web-sidebar ul").each(function () { + new Sortable(this, { + group: { + name: "qux", + put: ["qux"], + pull: ["qux"], + }, + }); + }); + } + + get_wiki_sidebar_html(fields) { + return $(` + + + + + + + + + + + + + + + ${fields.title} + + + + `); + } + + add_new_link() { + let $new_page = $(` + + + New Wiki Page + + + `); + + $new_page.appendTo( + $(".doc-sidebar .sidebar-items") + .children(".list-unstyled") + .not(".hidden") + .first() + ); + } +}; diff --git a/wiki/public/js/render_wiki.js b/wiki/public/js/render_wiki.js new file mode 100644 index 00000000..a7237e8a --- /dev/null +++ b/wiki/public/js/render_wiki.js @@ -0,0 +1,89 @@ +window.RenderWiki = class RenderWiki extends Wiki { + constructor(opts) { + super(); + $("document").ready(() => { + if ( + window.location.pathname != "/revisions" && + window.location.pathname != "/compare" + ) { + this.activate_sidebars(); + this.set_active_sidebar(); + this.set_nav_buttons(); + this.set_toc_highlighter(); + } + }); + } + + set_toc_highlighter() { + $(document).ready(function () { + $(window).scroll(function () { + $(".page-toc a").removeClass("active"); + currentAnchor().addClass("active"); + }); + }); + + function tocItem(anchor) { + return $('[href="' + anchor + '"]'); + } + + function heading(anchor) { + return $("[id=" + anchor.substr(1) + "]"); + } + + var _anchors = null; + function anchors() { + if (!_anchors) { + _anchors = $(".page-toc a").map(function () { + return $(this).attr("href"); + }); + } + return _anchors; + } + + function currentAnchor() { + var winY = window.pageYOffset; + var currAnchor = null; + anchors().each(function () { + var y = heading(this).position().top; + if (y < winY + window.innerHeight * 0.23) { + currAnchor = this; + return; + } + }); + return tocItem(currAnchor); + } + } + + set_nav_buttons() { + var current_index = -1; + + $(".sidebar-column") + .find("a") + .each(function (index) { + if ($(this).attr("class")) { + let dish = $(this).attr("class").split(/\s+/)[0]; + if (dish === "active") { + current_index = index; + } + } + }); + + if (current_index != 0) { + $(".btn.left")[0].href = + $(".sidebar-column").find("a")[current_index - 1].href; + $(".btn.left")[0].innerHTML = + "←" + $(".sidebar-column").find("a")[current_index - 1].innerHTML; + } else { + $(".btn.left").hide(); + } + + if (current_index < $(".sidebar-column").find("a").length - 1) { + $(".btn.right")[0].href = + $(".sidebar-column").find("a")[current_index + 1].href; + $(".btn.right")[0].innerHTML = + $(".sidebar-column").find("a")[current_index + 1].innerHTML + "→"; + } else { + $(".btn.right").hide(); + } + } +}; diff --git a/wiki/public/js/wiki.bundle.js b/wiki/public/js/wiki.bundle.js new file mode 100644 index 00000000..bf39e09c --- /dev/null +++ b/wiki/public/js/wiki.bundle.js @@ -0,0 +1,4 @@ +import './edit_asset' +import './wiki' +import './edit_wiki' +import './render_wiki' diff --git a/wiki/public/js/wiki.js b/wiki/public/js/wiki.js new file mode 100644 index 00000000..298d17d4 --- /dev/null +++ b/wiki/public/js/wiki.js @@ -0,0 +1,51 @@ +window.Wiki = class Wiki { + activate_sidebars() { + $(".sidebar-item").each(function (index) { + const active_class = "active"; + let page_href = window.location.pathname; + if (page_href.indexOf("#") !== -1) { + page_href = page_href.slice(0, page_href.indexOf("#")); + } + if ($(this).data("route") == page_href) { + $(this).addClass(active_class); + $(this).find("a").addClass(active_class); + } + }); + // scroll the active sidebar item into view + let active_sidebar_item = $(".sidebar-item.active"); + if (active_sidebar_item.length > 0) { + active_sidebar_item.get(0).scrollIntoView(true, { + behavior: "smooth", + block: "nearest", + }); + } + } + + toggle_sidebar(event) { + $(event.currentTarget).parent().children("ul").toggleClass("hidden"); + $(event.currentTarget).find(".drop-icon").toggleClass("hidden"); + $(event.currentTarget).find(".drop-left").toggleClass("hidden"); + event.stopPropagation(); + } + + set_active_sidebar() { + $(".doc-sidebar,.web-sidebar").on( + "click", + ".collapsible", + this.toggle_sidebar + ); + $(".sidebar-group").children("ul").addClass("hidden"); + $(".sidebar-item.active") + .parents(" .web-sidebar .sidebar-group>ul") + .removeClass("hidden"); + const sidebar_groups = $(".sidebar-item.active").parents( + ".web-sidebar .sidebar-group" + ); + sidebar_groups.each(function () { + $(this).children(".collapsible").find(".drop-left").addClass("hidden"); + }); + sidebar_groups.each(function () { + $(this).children(".collapsible").find(".drop-icon").removeClass("hidden"); + }); + } +}; diff --git a/wiki/public/scss/edit_wiki.bundle.scss b/wiki/public/scss/edit_wiki.bundle.scss new file mode 100644 index 00000000..2cfecf1c --- /dev/null +++ b/wiki/public/scss/edit_wiki.bundle.scss @@ -0,0 +1 @@ +@import "./edit_wiki.scss"; \ No newline at end of file diff --git a/wiki/public/scss/edit_wiki.scss b/wiki/public/scss/edit_wiki.scss new file mode 100644 index 00000000..2b0e6af4 --- /dev/null +++ b/wiki/public/scss/edit_wiki.scss @@ -0,0 +1,105 @@ +@import "frappe/public/scss/website/variables"; + +.edit-page-head { + margin-top: 0.5rem; + display: flex; + justify-content: space-between; + .edit-title { + width: 100%; + margin-right: 20px; + } +} + +.nav-tabs { + padding-bottom: 0; + + .nav-item { + margin: 0 2rem 0 0; + + .nav-link { + padding: 0.5rem 0; + margin-right: 0; + font-weight: 500; + } + } +} + +.edit-form { + margin-top: 1.25rem !important; +} + +.form-column { + padding: 0; + + .frappe-control .ql-editor { + background-color: white; + } +} + +.form-control { + background-color: white; + border: solid rgb(244, 245, 246) 1px; +} + +.form-control:focus { + background-color: white; + border: solid rgb(244, 245, 246) 1px; +} + +.ace-editor-target { + background-color: white; +} + +.ace-tm .ace_gutter { + background-color: white; + border-right: solid rgb(244, 245, 246) 1px; +} + +.wiki-preview { + border: solid #f4f5f6 1px; + padding: 0.5rem; +} + +// edit page + +.attachment-controls { + cursor: pointer; + max-height: 28px; + display: flex; + justify-content: flex-end; + + .show-attachments { + padding: 4px 8px; + color: $primary; + } +} + +div[data-fieldname="type"] { + .form-group { + margin-bottom: 0; + } + .clearfix { + display: none; + } +} + +.popover { + max-width: 400px; + td { + border-top: 0; + } +} + +.edit-title { + display: flex; + align-items: center; + + span { + margin-bottom: 0; + } + + i { + cursor: pointer; + padding: 0.5rem; + } +} diff --git a/wiki/public/scss/wiki.bundle.scss b/wiki/public/scss/wiki.bundle.scss new file mode 100644 index 00000000..6f2954e1 --- /dev/null +++ b/wiki/public/scss/wiki.bundle.scss @@ -0,0 +1 @@ +@import "./wiki.scss"; \ No newline at end of file diff --git a/wiki/public/scss/wiki.scss b/wiki/public/scss/wiki.scss index f25caeb1..ba49c235 100644 --- a/wiki/public/scss/wiki.scss +++ b/wiki/public/scss/wiki.scss @@ -1,4 +1,14 @@ +@import "frappe/public/scss/desk/css_variables"; @import "frappe/public/scss/website/variables"; +@import "frappe/public/scss/website/index"; +@import "frappe/public/scss/common/awesomeplete"; +@import "frappe/public/scss/common/buttons"; +@import "frappe/public/scss/common/mixins"; +@import "frappe/public/scss/common/controls"; +@import "frappe/public/scss/desk/timeline"; +@import "frappe/public/scss/desk/form"; +@import "frappe/public/css/fonts/inter/inter"; +@import "frappe/public/css/octicons/octicons.css"; textarea.wiki-content { font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, @@ -19,36 +29,448 @@ textarea.wiki-content { margin-top: 2rem; } -.wiki-diff .diff { - font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, - monospace; +.wiki-diff { + max-width: 700px; + + .diff { + font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, + monospace; + font-size: $font-size-sm; + border: 1px solid $border-color; + border-radius: 0.5rem; + overflow: auto; + + div { + padding: 0.25rem 0.5rem; + background-color: $light; + color: $body-color; + } + + .control { + color: $gray-500; + background-color: $input-bg; + } + + .delete { + background-color: #fff5f5; + color: $gray-900; + } + + .insert { + background-color: #f0fff4; + color: $gray-900; + } + } +} +.wiki-revision-meta { font-size: $font-size-sm; - border: 1px solid $border-color; - border-radius: 0.5rem; - overflow: hidden; + display: flex; + justify-content: space-between; +} + +// sidebar + +.sidebar-group { + border-bottom: 1px solid #eef0f2; + margin: 0; + font-family: Inter; + font-style: normal; + font-weight: 500; + font-size: $font-size-base; + line-height: 1.72; + /* identical to box height, or 28px */ + + letter-spacing: -0.011em; + + padding-left: 0.25rem; + + &:last-child { + border-bottom: none; + } +} +.sidebar-group { + ul.list-unstyled { + padding-left: 0.65rem; + ul { + padding-left: 0.65rem; + } + } + + .collapsible { + padding-top: 0.5rem; + padding-bottom: 0.5rem; + display: flex; + align-items: center; + } + + .active { + border-radius: 4px; + color: $primary; + background-color: #ebf5ff; + } div { - padding: 0.25rem 0.5rem; - background-color: $gray-50; - color: $gray-700; + cursor: pointer; + flex-basis: 100%; + flex-wrap: wrap; + .h6 { + font-size: $font-size-sm; + margin-bottom: 0; + line-height: 1.5; + } + } + + .sidebar-item { + font-family: Inter; + font-style: normal; + font-weight: normal; + font-size: 1px; + line-height: 1.5; + /* identical to box height, or 24px */ + color: $text-muted; + letter-spacing: -0.011em; + padding: 0.375rem 0 0.375rem 0.8rem; + + div { + display: flex; + } + + a { + margin: 0; + padding-left: 0; + padding-top: unset; + padding-bottom: unset; + font-weight: normal; + width: 100%; + } } - .control { - color: $gray-500; - background-color: $gray-100; + .drop-icon, + .drop-left { + display: inline-flex; } +} + +.sidebar-item div { + display: flex; +} +.sidebar-column { + border-right: 1px solid #eef0f2; + flex: 1.5; +} + +.doc-sidebar { + .web-sidebar { + margin-top: 10px; + margin-bottom: 8px; + padding-top: 1rem; + padding-bottom: 0; + margin-right: -0.5rem; - .delete { - background-color: #fff5f5; - color: $gray-900; + .sidebar-items > .list-unstyled > .sidebar-item { + margin-left: -0.5rem; + } + .sidebar-items > .list-unstyled > .sidebar-group { + margin-left: -0.7rem; + } } +} - .insert { - background-color: #f0fff4; - color: $gray-900; +@media (min-width: 992px) { + .doc-sidebar { + margin-top: 3.9rem; } } -.wiki-revision-meta { - font-size: $font-size-sm; +// navbar +.navbar-nav { + height: 100%; +} + +.nav-item { + margin-left: 24px; + + #search-container { + width: 312px; + height: 100%; + padding-right: 0px; + padding-left: 0px; + .dropdown { + height: 100%; + + input { + height: 100%; + background: #f4f5f6; + border-radius: 4px; + } + } + } + + .nav-link { + font-family: Inter; + font-style: normal; + font-weight: normal; + font-size: $font-size-base; + line-height: 150%; + /* identical to box height, or 24px */ + + letter-spacing: -0.011em; + + color: $body-color; + padding-left: 0rem !important; + padding-right: 0rem !important; + } + + select { + height: 100%; + } +} +.navbar { + width: 100%; + position: fixed; + z-index: 1; + + .container { + height: 36px; + } +} + +.navbar-light { + border-bottom: 0; +} + +.navbar.navbar-light.navbar-expand-lg { + border-bottom: 1px solid $gray-200; +} + +.navbar.navbar-light.navbar-expand-lg { + border-bottom: 1px solid $gray-200; +} + +.doc-layout { + padding-top: 0rem; +} + +.navbar-brand { + img { + max-height: 18px; + } +} + +// main-content +.main-column { + flex: 4; + + .page-content-wrapper { + padding-top: 1rem; + } +} + +.wiki-flex { + flex-wrap: nowrap; +} + +.doc-content .from-markdown > :first-child { + margin-top: 0; +} + +.doc-content { + font-family: Inter; + + .from-markdown { + h2 { + margin-top: 2.5rem; + } + } +} + +// toc +.page-toc { + flex: 1; + + div { + padding-top: 2rem; + width: 100%; + + ul { + padding-bottom: 0.75rem; + } + + .h5 { + font-family: Inter; + font-style: normal; + font-weight: 500; + font-size: $font-size-xs; + line-height: 1.35; + color: $headings-color; + } + } + + li { + padding-left: 4px; + margin: 2px 0; + } + + .active { + border-radius: 4px; + color: $primary; + background-color: #ebf4ff; + border-left: 3px solid $primary; + border-radius: 0.1rem 0.375rem 0.375rem 0.1rem; + } + + a { + padding: 0.25rem; + } +} + +// footer +.footer-link::after { + margin-left: 1rem; + content: "•"; +} + +.footer-link::after:last-child { + content: ""; +} + +.breadcrumb-item + .breadcrumb-item::before { + content: ""; + display: none; +} + +.breadcrumb-item + .breadcrumb-item { + padding-left: 0; +} + +@media (min-width: 1920px) { + .doc-container { + max-width: 1440px; + } +} + +.doc-sidebar { + padding-right: 0.5rem; +} + +.sidebar-group > ul { + margin-bottom: 0.5rem; +} + +ul.user-contributions { + list-style-type: none; + display: flex; + padding-left: 0; + flex-wrap: wrap; +} + +ul.user-contributions li { + margin: 0 4px; +} + +/* +==== +breadcrumbs +==== +*/ + +.doc-content .breadcrumb-container { + margin-top: 0rem; /* spacing adjusment for breadcrumb */ +} + +.page_content { + padding-top: 3rem; +} + +ol.breadcrumb { + font-size: $font-size-xs; + + li [itemprop="item"] { + align-self: center; + } +} + +/* +==== +navbar +==== +*/ + +.navbar .navbar-light .navbar-expand-lg { + width: 100%; + position: fixed; + top: 0; /*ensure navbar stays affixes to the top*/ + left: 0; + right: 0; +} + +@media (max-width: 767px) { + .navbar { + position: inherit; + } + + .navbar-expand-lg .doc-container { + padding-left: 0; + padding-right: 0; + } + .web-sidebar { + padding-top: 0; + } + + .web-sidebar > a { + display: none; + } + + .page-content-wrapper { + padding-top: 0 !important; + margin-top: 0 !important; + } + + .wiki-footer { + .btn.left, + .btn.right { + width: 100%; + margin-bottom: 10px; + } + } + + .nav-item { + margin-left: 10px; + } + .search-nav-item { + height: 2.2rem; + } + // #dropdownMenuSearch { + // margin-right: 20px; + // } +} + +.my-contributions, +.new-wiki-page, +.add-sidebar-item { + padding: 0.25rem 0.5rem; + cursor: pointer; +} + +.web-footer { + border-top: 1px solid #eef0f2; +} + +.search-nav-item { + position: relative; + + svg { + top: 10px; + right: 12px; + position: absolute; + } +} + +// contributions page + +.contributions-header { + margin: 3rem 0 1rem 0; + font-size: $font-size-xl; + font-weight: 700; + line-height: 16px; } diff --git a/wiki/wiki/doctype/migrate_to_wiki/__init__.py b/wiki/wiki/doctype/migrate_to_wiki/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.js b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.js new file mode 100644 index 00000000..cd3ca3ca --- /dev/null +++ b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Migrate To Wiki', { + // refresh: function(frm) { + + // } +}); diff --git a/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.json b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.json new file mode 100644 index 00000000..ab368c39 --- /dev/null +++ b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.json @@ -0,0 +1,84 @@ +{ + "actions": [], + "creation": "2021-05-07 03:52:41.720362", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "app_name", + "assets_directory", + "docs_directory", + "create_new_assets", + "column_break_5", + "documentation_route", + "docs_base_url", + "assets_prepend" + ], + "fields": [ + { + "fieldname": "app_name", + "fieldtype": "Data", + "label": "App Name" + }, + { + "fieldname": "docs_directory", + "fieldtype": "Data", + "label": "Docs Directory" + }, + { + "fieldname": "assets_directory", + "fieldtype": "Data", + "label": "Assets Directory" + }, + { + "fieldname": "documentation_route", + "fieldtype": "Data", + "label": "New Documentation Route" + }, + { + "description": "with docs base url", + "fieldname": "assets_prepend", + "fieldtype": "Data", + "label": "Assets Prepend" + }, + { + "fieldname": "docs_base_url", + "fieldtype": "Data", + "label": "Docs Base Utl" + }, + { + "default": "0", + "description": "Create New Images if they exist on Disk", + "fieldname": "create_new_assets", + "fieldtype": "Check", + "label": "Create New Assets " + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-06-01 14:22:25.142960", + "modified_by": "Administrator", + "module": "Wiki", + "name": "Migrate To Wiki", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.py b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.py new file mode 100644 index 00000000..c9a27114 --- /dev/null +++ b/wiki/wiki/doctype/migrate_to_wiki/migrate_to_wiki.py @@ -0,0 +1,283 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +import os +import shutil +from frappe.core.doctype.file.file import get_content_hash, get_file_name + +# www/docs/user/manual/en/accounts/bank-reconciliation.md +class MigrateToWiki(Document): + # app_name = erpnext_documentation + # docs_directory = www/docs/user/manual/en + # assets_directory = www/docs/assets/img + # assets_prepend = {{docs_base_url}}/assets/img + # documentation_route = / + + def validate(self): + self.app_name = self.clean_paths(self.app_name) + self.docs_directory = self.clean_paths(self.docs_directory) + self.assets_directory = self.clean_paths(self.assets_directory) + self.assets_prepend = self.clean_paths(self.assets_prepend) + self.documentation_route = self.clean_paths(self.documentation_route) + + def clean_paths(self, path): + if not path: path = '' + return path.strip(' ').strip('/').replace('//', '/') + + def on_update(self): + if frappe.flags.in_install: + return + self.create_first_path() + self.set_docs_tree_generator() + # self.copy_assets() + self.set_assets_tree_generator() + self.create_files() + self.migrate_wiki() + + def create_first_path(self): + self.docs_change_dict = {} + wiki_sidebar = frappe.new_doc("Wiki Sidebar") + wiki_sidebar_dict = { + "route": self.documentation_route, + "title": 'Home', + 'is_group': True + } + wiki_sidebar.update(wiki_sidebar_dict) + try: + wiki_sidebar.save() + except frappe.DuplicateEntryError : + return + + def set_docs_tree_generator(self): + self.docs_tree_generator = os.walk(f'{frappe.get_app_path(self.app_name)}{os.sep}{self.docs_directory}') + + def set_assets_tree_generator(self): + self.assets_tree_generator = os.walk(frappe.get_app_path(self.app_name) + os.sep + self.assets_directory) + + + def migrate_wiki(self): + for root, dirs, files in self.docs_tree_generator: + self.migrate_dir(root, dirs, files) + for file in files: + self.migrate_file(root, file, files) + + def migrate_dir(self, root, dirs, files): + for directory in dirs: + if directory == '__pycache__': continue + wiki_sidebar = frappe.new_doc("Wiki Sidebar") + parent_wiki_sidebar = f'{self.documentation_route}{os.sep}{root[root.find(self.docs_directory) + len(self.docs_directory) + 1: ]}'.replace('//', '/').strip('/') + wiki_sidebar_dict = { + "route": f'{parent_wiki_sidebar}{os.sep}{directory}'.replace('//', '/'), + "title": directory.capitalize(), + "parent_wiki_sidebar": parent_wiki_sidebar, + } + + wiki_sidebar.update(wiki_sidebar_dict) + wiki_sidebar.save() + + + wiki_sidebar_item = frappe.new_doc('Wiki Sidebar Item') + wiki_sidebar_item_dict = { + "type": "Wiki Sidebar", + "item": wiki_sidebar.name, + "parent": parent_wiki_sidebar, + 'parenttype': 'Wiki Sidebar', + 'parentfield': 'sidebar_items' + } + wiki_sidebar_item.update(wiki_sidebar_item_dict) + wiki_sidebar_item.save() + + + def migrate_file(self, root, file, files): + + if file=="index.md" and "contents.md" in files: + return + heading_index = -1 + if not file.endswith('.md'): + return + with open(f'{root}{os.sep}{file}') as f: + lines = f.readlines() + for index, line in enumerate(lines): + if line.startswith('#'): + heading_index = index + break + + parent= f'{self.documentation_route}{os.sep}{root[root.find(self.docs_directory) + len(self.docs_directory) + 1: ]}'.replace('//', '/').strip('/') + route = f'{parent}{os.sep}{file[:-3]}' + + title = lines[heading_index].strip('#').strip(' ') if heading_index != -1 else route.split(os.sep)[-1] + content = ''.join(lines[heading_index + 1 : ]) if heading_index != -1 else ''.join(lines) + if 'shifted to landing page' in content: + return + + + if content: + for prev, new in self.docs_change_dict.items(): + content = content.replace(prev, new) + content = content.replace(self.docs_directory.strip('w'), f'/{self.documentation_route}') + + if file.endswith('index.md') or file.endswith('contents.md'): + + try: + with open(f'{root}{os.sep}index.txt') as f: + lines = f.readlines() + content = content.replace('{index}',"" + "".join(lines) + "") + except: + content = content.replace('{index}',"" + "".join(files) + "") + + route = f'{parent}'.strip('/') + + + else: + content = f"{parent}" + + + wiki_page = frappe.new_doc("Wiki Page") + wiki_page_dict = { + "title": title, + 'allow_guest': 1, + 'published': 1, + 'content': content, + 'route': route, + } + wiki_page.update(wiki_page_dict) + try: + wiki_page.save() + except: + print(wiki_page.name) + print(wiki_page.title) + + + wiki_sidebar_item = frappe.new_doc('Wiki Sidebar Item') + wiki_sidebar_item_dict = { + "item": wiki_page.name, + "title": wiki_page.title, + "parent": parent, + 'parenttype': 'Wiki Sidebar', + 'route': route, + 'parentfield': 'sidebar_items' + } + wiki_sidebar_item.update(wiki_sidebar_item_dict) + wiki_sidebar_item.save() + + frappe.db.commit() + + def copy_assets(self): + shutil.copytree( + frappe.get_app_path(self.app_name) + os.sep + self.assets_directory, + f"{os.getcwd()}{os.sep}{frappe.local.site}{os.sep}public{os.sep}files{os.sep}{self.app_name}{os.sep}{self.documentation_route}" + ) + + + def create_files(self): + self.docs_change_dict = {} + file_doc = frappe.new_doc('File') + + file_doc.update({ + "doctype": "File", + "folder": f'Home', + "file_name": f'{self.app_name}', + "is_private": 0, + "is_folder": 1, + }) + try: + file_doc.save(ignore_permissions=True) + except Exception as ex: + print(ex) + + folder = f'Home{os.sep}{self.app_name}' + for directory in self.documentation_route.split(os.sep): + file_doc = frappe.new_doc('File') + + file_doc.update({ + "doctype": "File", + "folder": folder, + "file_name": directory, + "is_private": 0, + "is_folder": 1, + }) + try: + file_doc.save(ignore_permissions=True) + except Exception as ex: + print(ex) + + folder = f'{folder}{os.sep}{directory}' + + for root, dirs, files in self.assets_tree_generator: + + for directory in dirs: + fold = f'{folder}{os.sep}{root[root.find(self.assets_directory) + len(self.assets_directory) + 1:]}' + fold=fold.replace(f'{os.sep}{os.sep}',os.sep) + if fold.endswith(f'{os.sep}'): + fold = fold[:-1] + file_doc = frappe.new_doc('File') + + file_doc.update({ + "doctype": "File", + "folder": fold, + "file_name": directory, + "is_private": 0, + "is_folder": 1, + }) + + try: + file_doc.save(ignore_permissions=True) + except Exception as ex: + print(ex) + + + + for file in files: + if file == "__init__.py": + continue + if os.path.exists(f'{os.getcwd()}{os.sep}{frappe.local.site}{os.sep}public{os.sep}files{os.sep}{file}'): + if self.create_new_assets: + try: + with open(f'{root}{os.sep}{file}', "rb") as f: + content_hash = get_content_hash(f.read()) + except IOError: + frappe.msgprint(frappe._("File {0} does not exist").format(f'{root}{os.sep}{file}')) + raise + + + new_file_name = get_file_name(file, content_hash[-6:]) + shutil.copy( + f'{root}{os.sep}{file}', + f'{os.getcwd()}{os.sep}{frappe.local.site}{os.sep}public{os.sep}files{os.sep}{new_file_name}' + ) + file_url = f'{os.sep}files{os.sep}{new_file_name}' + else: + file_url = f'{os.sep}files{os.sep}{file}' + else: + shutil.copy( + f'{root}{os.sep}{file}', + f'{os.getcwd()}{os.sep}{frappe.local.site}{os.sep}public{os.sep}files{os.sep}' + ) + file_url = f'{os.sep}files{os.sep}{file}' + + fol = f'{folder}{os.sep}{root[root.find(self.assets_directory) + len(self.assets_directory):] }' + fol=fol.replace(f'{os.sep}{os.sep}',os.sep) + if fol.endswith(os.sep): + fol = fol[:-1] + file_doc = frappe.new_doc('File') + file_doc.update({ + "doctype": "File", + "folder": fol, + "file_name": file, + "is_private": 0, + "file_url": file_url + }) + try: + file_doc.save(ignore_permissions=True) + except Exception as ex: + print(ex) + + orig_file_url = f'{self.assets_prepend}{os.sep}{root[root.find(self.assets_directory) + len(self.assets_directory) + 1:] }{os.sep}{file}' + + + self.docs_change_dict[orig_file_url] = file_url + self.docs_change_dict[orig_file_url.replace('{{docs_base_url}}', self.docs_base_url)] = file_url diff --git a/wiki/wiki/doctype/migrate_to_wiki/test_migrate_to_wiki.py b/wiki/wiki/doctype/migrate_to_wiki/test_migrate_to_wiki.py new file mode 100644 index 00000000..81f116a9 --- /dev/null +++ b/wiki/wiki/doctype/migrate_to_wiki/test_migrate_to_wiki.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestMigrateToWiki(unittest.TestCase): + pass diff --git a/wiki/wiki/doctype/wiki_page/patches/delete_is_new.py b/wiki/wiki/doctype/wiki_page/patches/delete_is_new.py new file mode 100644 index 00000000..b2361bc0 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/patches/delete_is_new.py @@ -0,0 +1,13 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + try: + frappe.db.sql('alter table `tabWiki Page Patch` drop column is_new;') + frappe.db.commit() + except: + pass diff --git a/wiki/wiki/doctype/wiki_page/templates/breadcrumbs.html b/wiki/wiki/doctype/wiki_page/templates/breadcrumbs.html new file mode 100644 index 00000000..c584abda --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/breadcrumbs.html @@ -0,0 +1,27 @@ +{%- if not no_breadcrumbs and parents -%} + + + + {%- set parents = parents[-3:] %} + {% set count = (parents | length) + 1 %} + {% for parent in parents %} + + + {{ parent.title or parent.label or parent.name or "" }} + + + + + + + {% endfor %} + + + {{ title }} + + + + + + +{%- endif -%} diff --git a/wiki/wiki/doctype/wiki_page/templates/comment.html b/wiki/wiki/doctype/wiki_page/templates/comment.html new file mode 100644 index 00000000..3785f775 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/comment.html @@ -0,0 +1,50 @@ + + + + + + + + + Activity + + + + + {% for comment in comments %} + + + + + + + + + + + + + + {{comment.owner }} + + {{ frappe.utils.pretty_date(comment.creation) }} + + + + + + {{comment.content}} + + + + + + + + {% endfor %} + + + + \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/templates/compare.html b/wiki/wiki/doctype/wiki_page/templates/compare.html deleted file mode 100644 index a815461e..00000000 --- a/wiki/wiki/doctype/wiki_page/templates/compare.html +++ /dev/null @@ -1,12 +0,0 @@ - - {{ title }} - {{ revision.message }} - - {{ revision.owner }} edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} - - - - - {{ diff or 'No changes to show' }} - - diff --git a/wiki/wiki/doctype/wiki_page/templates/edit.html b/wiki/wiki/doctype/wiki_page/templates/edit.html index 610b8dd8..8fed114b 100644 --- a/wiki/wiki/doctype/wiki_page/templates/edit.html +++ b/wiki/wiki/doctype/wiki_page/templates/edit.html @@ -1,60 +1,134 @@ - - {{ title }} - + - - - - - Title - + + {{ doc.title }} + + - - Content - - - Write - - - Preview - - - - - {{ doc.content }} - - - + + Submit Changes + + + + + + + + + + + + + + + Write + + + + Preview + + + {%- if not frappe.form_dict.new -%} + + Compare + + {%- endif -%} + + {% if wiki_page_patch %} + + Discussion + + {% endif %} + + + + + + + {{ content_md }} + {{ content_html }} + + + + + + + + {%- if not frappe.form_dict.new -%} + + + + {%- endif -%} + + {% if wiki_page_patch %} + + + {% include "wiki/doctype/wiki_page/templates/comment.html" %} + + {% endif %} + + - - - Edit Message - - - Save Page - - -{%- block script -%} - -{%- endblock -%} + + + + + + + {% block base_scripts %} + + + + + + {{ include_script('libs.bundle.js') }} + + {{ include_script('frappe-web.bundle.js') }} + {{ include_script('controls.bundle.js') }} + {{ include_script('dialog.bundle.js') }} + {{ include_script('bootstrap-4-web.bundle.js') }} + + + {{ include_script('desk.bundle.js') }} + {{ include_script('wiki.bundle.js') }} + + + + + + + + + + {%- if frappe.form_dict.new -%} + + + + {%- if script -%} + + {%- endif -%} + + {%- endif -%} + + {% endblock %} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/templates/navbar_items.html b/wiki/wiki/doctype/wiki_page/templates/navbar_items.html new file mode 100644 index 00000000..d90b07aa --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/navbar_items.html @@ -0,0 +1,86 @@ +{% macro render_item(item, submenu=False, parent=False) %} +{% if item.child_items %} + + {% if parent %} + + {%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} + + + {{ _(item.label) }} + + + {% for child in item.child_items %} + {{ render_item(child, True) }} + {% endfor %} + + + {% else %} + {%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} + + + {{ _(item.label) }} + + + {% for child in item.child_items %} + {{ render_item(child, True) }} + {% endfor %} + + + {% endif %} + +{% else %} + + {% if parent %} + + + {{ _(item.label) }} + + + {% else %} + + {{ _(item.label) }} + + {% endif %} + +{% endif %} +{% endmacro %} + +{% if top_bar_items -%} + + {%- for item in top_bar_items -%} + {% if not item.parent_label and not item.right -%} + {{ render_item(item, parent=True) }} + {%- endif -%} + {%- endfor %} + +{%- endif %} + + {% include "wiki/doctype/wiki_page/templates/navbar_search.html" %} + {%- for item in top_bar_items -%} + {% if not item.parent_label and item.right -%} + {{ render_item(item, parent=True) }} + {%- endif -%} + {%- endfor %} + {% if not only_static %} + {% block navbar_right_extension %}{% endblock %} + {% endif %} + + + + + + + + + {% include "templates/includes/navbar/navbar_login.html" %} + + +{%- if call_to_action -%} + + {{ call_to_action }} + +{%- endif -%} diff --git a/wiki/wiki/doctype/wiki_page/templates/navbar_search.html b/wiki/wiki/doctype/wiki_page/templates/navbar_search.html new file mode 100644 index 00000000..a215c421 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/navbar_search.html @@ -0,0 +1,25 @@ +{% if navbar_search %} + + + + + + + + + + + + + + + + + + + + + + + +{% endif %} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/templates/revisions.html b/wiki/wiki/doctype/wiki_page/templates/revisions.html deleted file mode 100644 index 507b78e1..00000000 --- a/wiki/wiki/doctype/wiki_page/templates/revisions.html +++ /dev/null @@ -1,15 +0,0 @@ - - {{ title }} - - - {%- for revision in revisions -%} - - - {{ revision.message }} - - {{ revision.owner }} edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} - - - - {%- endfor -%} - diff --git a/wiki/wiki/doctype/wiki_page/templates/show.html b/wiki/wiki/doctype/wiki_page/templates/show.html index cf537196..a6209e02 100644 --- a/wiki/wiki/doctype/wiki_page/templates/show.html +++ b/wiki/wiki/doctype/wiki_page/templates/show.html @@ -1,23 +1,40 @@ {{ title }} - - {%- if can_edit -%} - Edit - New Page - {%- endif -%} - + {{ content }} + + +{{ include_script('wiki.bundle.js') }} + + + +{%- if script -%} + +{%- endif -%} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/templates/web_sidebar.html b/wiki/wiki/doctype/wiki_page/templates/web_sidebar.html new file mode 100644 index 00000000..a9bc4db9 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/web_sidebar.html @@ -0,0 +1,93 @@ +{% macro render_sidebar_item(item) %} + + {%- if item.group_title -%} + + + + + + + + + + + + {{ item.group_title }} + + + {{ render_sidebar_items(item.group_items) }} + + {%- else -%} + + + {% if item.type != 'input' %} + + {{ item.title or item.label or item.name }} + + {% else %} + + + + + + + {% endif %} + + {%- endif -%} + +{% endmacro %} + +{% macro render_sidebar_items(items) %} +{%- if items | len > 0 -%} + + {% for item in items -%} + {{ render_sidebar_item(item) }} + {%- endfor %} + +{%- endif -%} +{% endmacro %} + +{% macro my_account() %} + + + {{ _("My Account") }} + + + + + New Page + + + + My Contributions + + +{% endmacro %} + + + {% if sidebar_title %} + + {{ sidebar_title }} + + {% endif %} + + {{ render_sidebar_items(sidebar_items) }} + {{ my_account() }} + + + + + + \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/templates/wiki_doc.html b/wiki/wiki/doctype/wiki_page/templates/wiki_doc.html new file mode 100644 index 00000000..92ecfd3b --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/wiki_doc.html @@ -0,0 +1,116 @@ +{% extends "templates/base.html" %} +{%- from "templates/includes/navbar/navbar_items.html" import render_item -%} + +{%- block head_include %} + +{% endblock -%} + +{%- block navbar -%} + + + + + + {%- if brand_html -%} + {{ brand_html }} + {%- elif banner_image -%} + + {%- else -%} + {{ (frappe.get_hooks("brand_html") or [_("Home")])[0] }} + {%- endif -%} + + + + + + + + + + + + + + + {%- set items = docs_navbar_items or [] -%} + {%- for item in items -%} + {{ render_item(item, parent=True) }} + {%- endfor -%} + + {% include "templates/includes/web_sidebar.html" %} + + + + + +{%- endblock -%} + +{% block content %} + + +{% macro container_attributes() -%} +id="page-{{ name or route | e }}" data-path="{{ pathname | e }}" +{%- if page_or_generator=="Generator" %}source-type="Generator" data-doctype="{{ doctype }}"{%- endif %} +{%- if source_content_type %}source-content-type="{{ source_content_type }}"{%- endif %} +{%- endmacro %} + + + + {%- if not no_sidebar -%} + + + + + {%- endif -%} + + + + {% block page_container %} + + + {%- if add_breadcrumbs -%} + {% include "wiki/doctype/wiki_page/templates/breadcrumbs.html" %} + {%- endif -%} + {%- block page_content -%}{%- endblock -%} + + + {% endblock %} + + + {%- if page_toc_html -%} + + {% block page_toc %} + {% if page_toc_html %} + + On this page + {{ page_toc_html }} + + {% endif %} + {% endblock %} + + {%- endif -%} + + + +{% endblock %} + +{%- block script -%} + +{%- endblock -%} diff --git a/wiki/wiki/doctype/wiki_page/templates/wiki_navbar.html b/wiki/wiki/doctype/wiki_page/templates/wiki_navbar.html new file mode 100644 index 00000000..00019eb0 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/templates/wiki_navbar.html @@ -0,0 +1,30 @@ + + + + + {%- if brand_html -%} + {{ brand_html }} + {%- elif banner_image -%} + + {%- else -%} + {{ (frappe.get_hooks("brand_html") or [_("Home")])[0] }} + {%- endif -%} + + + + + + + + {% include "wiki/doctype/wiki_page/templates/navbar_items.html" %} + + + + + + diff --git a/wiki/wiki/doctype/wiki_page/templates/wiki_page.html b/wiki/wiki/doctype/wiki_page/templates/wiki_page.html index 19534568..523e2d4f 100644 --- a/wiki/wiki/doctype/wiki_page/templates/wiki_page.html +++ b/wiki/wiki/doctype/wiki_page/templates/wiki_page.html @@ -1,20 +1,28 @@ -{% extends "templates/doc.html" %} +{% extends "wiki/doctype/wiki_page/templates/wiki_doc.html" %} +{%- block page_sidebar -%} + +{%- endblock -%} {%- block head_include %} {{ super() }} - +{{ include_style('wiki.bundle.css') }} +{%- if frappe.form_dict.edit or frappe.form_dict.new -%} +{{ include_style('edit_wiki.bundle.css') }} +{%- endif -%} + {% endblock -%} {% block page_content %} -{%- if frappe.form_dict.edit -%} +{%- if frappe.form_dict.edit or frappe.form_dict.new -%} {% include "wiki/doctype/wiki_page/templates/edit.html" %} -{%- elif frappe.form_dict.new -%} -{% include "wiki/doctype/wiki_page/templates/new.html" %} -{%- elif frappe.form_dict.revisions -%} -{% include "wiki/doctype/wiki_page/templates/revisions.html" %} -{%- elif frappe.form_dict.compare -%} -{% include "wiki/doctype/wiki_page/templates/compare.html" %} {%- else -%} {% include "wiki/doctype/wiki_page/templates/show.html" %} {%- endif -%} {% endblock %} + + +{%- block navbar -%} +{% include "wiki/doctype/wiki_page/templates/wiki_navbar.html" %} + +{%- endblock -%} + diff --git a/wiki/wiki/doctype/wiki_page/test_wiki_page.py b/wiki/wiki/doctype/wiki_page/test_wiki_page.py index 64353de4..a0e11d02 100644 --- a/wiki/wiki/doctype/wiki_page/test_wiki_page.py +++ b/wiki/wiki/doctype/wiki_page/test_wiki_page.py @@ -3,8 +3,59 @@ # See license.txt from __future__ import unicode_literals -# import frappe +import frappe import unittest +from wiki.wiki.doctype.wiki_page.wiki_page import update class TestWikiPage(unittest.TestCase): - pass + def test_wiki_page_lifecycle(self): + if frappe.db.exists('Wiki Page', 'wiki/page'): + frappe.delete_doc('Wiki Page', 'wiki/page') + for name in frappe.db.get_all('Wiki Page Revision', {'wiki_page':'wiki/page'}, pluck='name'): + frappe.delete_doc('Wiki Page Revision', name) + wiki_page = frappe.new_doc('Wiki Page') + wiki_page.route = 'wiki/page' + wiki_page.content = 'Hello World' + wiki_page.title = 'Hello World Title' + wiki_page.save() + self.assertEqual(frappe.db.get_value('Wiki Page', 'wiki/page', 'route'), 'wiki/page') + + + update( + name=wiki_page.name, + content='New Content', + title='New Title', + type='Markdown', + message="test" + ) + + patches = frappe.get_all('Wiki Page Patch', + {'wiki_page': wiki_page.name}, + [ 'message', 'new_title', 'new_code', 'name']) + + self.assertEqual(patches[0].message, "test") + self.assertEqual(patches[0].new_title, "New Title") + self.assertEqual(patches[0].new_code, "New Content") + + patch = frappe.get_doc('Wiki Page Patch', patches[0].name) + patch.status = 'Approved' + patch.approved_by = 'Administrator' + patch.save() + patch.submit() + + wiki_page = frappe.get_doc('Wiki Page', wiki_page.name) + + self.assertEqual(wiki_page.title, "New Title") + self.assertEqual(wiki_page.content, "New Content") + + self.assertEqual( + len(frappe.db.get_all( + "Wiki Page Revision", + filters={ + "wiki_page": wiki_page.name + }, + )), + 2 + ) + + wiki_page.delete() \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/wiki_page.json b/wiki/wiki/doctype/wiki_page/wiki_page.json index 5b8befa7..2bee4ad8 100644 --- a/wiki/wiki/doctype/wiki_page/wiki_page.json +++ b/wiki/wiki/doctype/wiki_page/wiki_page.json @@ -17,10 +17,12 @@ ], "fields": [ { + "allow_in_quick_entry": 1, + "default": "No Content", "description": "It is recommended that you start your first heading with h2, as the title will be the h1.", "fieldname": "content", "fieldtype": "Code", - "in_list_view": 1, + "in_preview": 1, "label": "Content", "options": "Markdown", "reqd": 1 @@ -34,6 +36,9 @@ { "fieldname": "route", "fieldtype": "Data", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, "label": "Route", "reqd": 1, "unique": 1 @@ -72,9 +77,14 @@ "group": "Linked Documents", "link_doctype": "Wiki Page Revision", "link_fieldname": "wiki_page" + }, + { + "group": "Linked Documents", + "link_doctype": "Wiki Page Patch", + "link_fieldname": "wiki_page" } ], - "modified": "2021-02-04 15:08:08.965522", + "modified": "2021-07-27 18:44:43.276703", "modified_by": "Administrator", "module": "Wiki", "name": "Wiki Page", @@ -96,5 +106,6 @@ "sort_field": "modified", "sort_order": "DESC", "title_field": "title", - "track_changes": 1 + "track_changes": 1, + "website_search_field": "content" } \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/wiki_page.py b/wiki/wiki/doctype/wiki_page/wiki_page.py index f8b39897..d9dec16f 100644 --- a/wiki/wiki/doctype/wiki_page/wiki_page.py +++ b/wiki/wiki/doctype/wiki_page/wiki_page.py @@ -5,41 +5,76 @@ from __future__ import unicode_literals import frappe +import json +import re from frappe import _ from frappe.website.utils import cleanup_page_name from frappe.website.website_generator import WebsiteGenerator - +from frappe.desk.form.load import get_comments +from frappe.core.doctype.file.file import get_random_filename +from six import PY2, StringIO, string_types, text_type class WikiPage(WebsiteGenerator): + + def autoname(self): + self.name = self.route + def after_insert(self): revision = frappe.new_doc("Wiki Page Revision") revision.wiki_page = self.name revision.content = self.content revision.message = "Create Wiki Page" revision.insert() + frappe.cache().hdel('website_page', self.name) def on_trash(self): for name in frappe.get_all( "Wiki Page Revision", {"wiki_page": self.name}, pluck="name" ): frappe.delete_doc("Wiki Page Revision", name) + for name in frappe.get_all( + "Wiki Page Patch", { + "wiki_page": self.name, + 'new': 0 + }, pluck="name" + ): + patch = frappe.get_doc('Wiki Page Patch', name ) + try: + patch.cancel() + except frappe.exceptions.DocstatusTransitionError: + pass + patch.delete() + for name in frappe.get_all( + "Wiki Page Patch", { + "wiki_page": self.name, + 'new': 1 + }, pluck="name" + ): + frappe.db.set_value('Wiki Page Patch',name ,'wiki_page', '') + + for name in frappe.get_all( + "Wiki Sidebar Item", + {"type": 'Wiki Page', "item": self.name}, + pluck="name" + ): + frappe.delete_doc("Wiki Sidebar Item", name) def set_route(self): if not self.route: self.route = "wiki/" + cleanup_page_name(self.title) - def update_page(self, title, content, edit_message): + def update_page(self, title, content, edit_message, raised_by=None): """ Update Wiki Page and create a Wiki Page Revision """ self.title = title - if content != self.content: self.content = content revision = frappe.new_doc("Wiki Page Revision") revision.wiki_page = self.name revision.content = content revision.message = edit_message + revision.raised_by = raised_by revision.insert() self.save() @@ -56,68 +91,35 @@ def verify_permission(self, permtype): _("Not Permitted to {0} Wiki Page").format(action), frappe.PermissionError ) - def get_context(self, context): - self.verify_permission("read") - - wiki_settings = frappe.get_single("Wiki Settings") - context.banner_image = wiki_settings.logo - context.home_route = "wiki" - context.docs_search_scope = "wiki" - context.can_edit = frappe.session.user != "Guest" - context.no_cache = 1 + def redirect_to_login(self, action): + frappe.throw( + _("Not Permitted to {0} Wiki Page").format(action), frappe.PermissionError + ) + def set_breadcrumbs(self, context): + context.add_breadcrumbs = True if frappe.form_dict: context.parents = [{"route": "/" + self.route, "label": self.title}] - context.add_breadcrumbs = True - - if frappe.form_dict.new: - self.verify_permission("create") - context.title = "New Wiki Page" - return - - if frappe.form_dict.edit: - self.verify_permission("write") - context.title = "Editing " + self.title - return - - if frappe.form_dict.revisions: - context.title = "Revisions: " + self.title - revisions = frappe.db.get_all( - "Wiki Page Revision", - filters={"wiki_page": self.name}, - fields=["message", "creation", "owner", "name"], - ) - context.revisions = revisions - return - - if frappe.form_dict.compare: - from ghdiff import diff - - revision = frappe.form_dict.compare - context.title = "Revision: " + revision - context.parents = [ - {"route": "/" + self.route, "label": self.title}, - {"route": "/" + self.route + "?revisions=true", "label": "Revisions"}, - ] - - revision = frappe.get_doc("Wiki Page Revision", revision) - - context.revision = revision - previous_revision_content = frappe.db.get_value( - "Wiki Page Revision", - filters={"creation": ("<", revision.creation), "wiki_page": self.name}, - fieldname=["content"], - order_by="creation asc", - ) - - if not previous_revision_content: - return - - context.diff = diff(previous_revision_content, revision.content, css=False) - return + else: + parents = [] + splits = self.route.split('/') + if splits: + for index, route in enumerate(splits[:-1], start=1): + full_route = '/'.join(splits[:index]) + wiki_page = frappe.get_all('Wiki Page', filters=[['route','=',full_route]],fields=['title']) + if wiki_page: + parents.append({"route": "/" + full_route, "label": wiki_page[0].title}) + + context.parents = parents + def get_context(self, context): + self.verify_permission("read") + self.set_breadcrumbs(context) + wiki_settings = frappe.get_single("Wiki Settings") + context.banner_image = wiki_settings.logo + context.script = wiki_settings.javascript + context.docs_search_scope = self.get_docs_search_scope() context.metatags = {"title": self.title} - context.sidebar_items = self.get_sidebar_items() context.last_revision = self.get_last_revision() context.number_of_revisions = frappe.db.count( "Wiki Page Revision", {"wiki_page": self.name} @@ -125,15 +127,42 @@ def get_context(self, context): html = frappe.utils.md_to_html(self.content) context.content = html context.page_toc_html = html.toc_html + context.show_sidebar = True + context.hide_login = True + + + def get_docs_search_scope(self): + sidebar = frappe.get_all( + doctype="Wiki Sidebar Item", + fields=["name", "parent"], + filters=[["item", "=", self.route]], + ) + topmost= '' + if sidebar: + topmost = frappe.get_doc("Wiki Sidebar", sidebar[0].parent).find_topmost(sidebar[0].parent) + return topmost + + def get_sidebar_items(self, context): + sidebar = frappe.get_all( + doctype="Wiki Sidebar Item", + fields=["name", "parent"], + filters=[["item", "=", self.route]], + ) + sidebar_html = '' + topmost = '/' + if sidebar: + sidebar_html, topmost = frappe.get_doc("Wiki Sidebar", sidebar[0].parent).get_items() + else: + sidebar = frappe.db.get_single_value("Wiki Settings", "sidebar") + if sidebar: + + sidebar_html, topmost = frappe.get_doc("Wiki Sidebar", sidebar).get_items() - def get_sidebar_items(self): - sidebar = frappe.db.get_single_value("Wiki Settings", "sidebar") - sidebar_items = frappe.get_doc("Website Sidebar", sidebar).get_items() - if frappe.session.user == "Guest": - sidebar_items = [ - item for item in sidebar_items if item.get("group_title") != "Manage Wiki" - ] - return sidebar_items + else: + sidebar_html = '' + + + return sidebar_html, topmost def get_last_revision(self): last_revision = frappe.db.get_value( @@ -143,26 +172,141 @@ def get_last_revision(self): @frappe.whitelist() -def preview(content): - return frappe.utils.md_to_html(content) +def preview(content, name, new, type, diff_css=False): + html = frappe.utils.md_to_html(content) + if new: + return {"html": html} + from ghdiff import diff + + old_content = frappe.db.get_value("Wiki Page", name, "content") + diff = diff(old_content, content, css=diff_css) + return {"html": html, "diff": diff, "orignal_preview": frappe.utils.md_to_html(old_content)} + +@frappe.whitelist() +def extract_images_from_html(content): + frappe.flags.has_dataurl = False + def _save_file(match): + data = match.group(1) + data = data.split("data:")[1] + headers, content = data.split(",") -@frappe.whitelist(methods=["POST"]) -def update(wiki_page, title, content, edit_message): - wiki_page = frappe.get_doc("Wiki Page", wiki_page) - wiki_page.update_page(title, content, edit_message) + if "filename=" in headers: + filename = headers.split("filename=")[-1] - frappe.response.location = "/" + wiki_page.route - frappe.response.type = "redirect" + # decode filename + if not isinstance(filename, text_type): + filename = text_type(filename, 'utf-8') + else: + mtype = headers.split(";")[0] + filename = get_random_filename(content_type=mtype) -@frappe.whitelist(methods=["POST"]) -def new(title, content): - wiki_page = frappe.new_doc("Wiki Page") - wiki_page.title = title - wiki_page.content = content - wiki_page.published = True - wiki_page.insert() + _file = frappe.get_doc({ + "doctype": "File", + "file_name": filename, + "content": content, + "decode": True + }) + _file.save(ignore_permissions=True) + file_url = _file.file_url + if not frappe.flags.has_dataurl: + frappe.flags.has_dataurl = True - frappe.response.location = "/" + wiki_page.route - frappe.response.type = "redirect" + return ']*src\s*=\s*["\'](?=data:)(.*?)["\']', _save_file, content) + return content + + +@frappe.whitelist() +def update(name, content, title, type, attachments="{}", message="", wiki_page_patch=None, new=False, new_sidebar = '', new_sidebar_items = '', sidebar_edited=False): + from ghdiff import diff + context = {'route': name} + context = frappe._dict(context) + if type == "Rich-Text": + content = extract_images_from_html(content) + + if new: + new = True + + if wiki_page_patch: + patch = frappe.get_doc("Wiki Page Patch", wiki_page_patch) + patch.new_title = title + patch.new_code = content + patch.status = "Under Review" + patch.message = message + patch.new= new + patch.new_sidebar = new_sidebar + patch.new_sidebar_items = new_sidebar_items + patch.sidebar_edited = sidebar_edited + patch.save() + return + + patch = frappe.new_doc("Wiki Page Patch") + + patch_dict = { + "wiki_page": name, + "status": "Under Review", + "raised_by": frappe.session.user, + "new_code": content, + "message": message, + "new": new, + "new_title": title, + "sidebar_edited" : sidebar_edited, + 'new_sidebar_items' : new_sidebar_items, + } + + patch.update(patch_dict) + + patch.save() + + update_file_links(attachments, patch.name) + + if 'System Manager' in frappe.get_roles(frappe.session.user): + patch.approved_by = frappe.session.user + patch.status = 'Approved' + patch.submit() + + frappe.db.commit() + + return True + + +def update_file_links(attachments, name): + for attachment in json.loads(attachments): + file = frappe.get_doc("File", attachment.get("name")) + file.attached_to_doctype = "Wiki Page Patch" + file.attached_to_name = name + file.save() + + +def get_source_generator(resolved_route, jenv): + path = resolved_route.controller.split(".") + path[-1] = "templates" + path.append(path[-2] + ".html") + path = "/".join(path) + return jenv.loader.get_source(jenv, path)[0] + + +def get_source(resolved_route, jenv): + if resolved_route.page_or_generator == "Generator": + return get_source_generator(resolved_route, jenv) + + elif resolved_route.page_or_generator == "Page": + return jenv.loader.get_source(jenv, resolved_route.template)[0] + + +def get_path_without_slash(path): + return path[1:] if path.startswith("/") else path + + +@frappe.whitelist(allow_guest=True) +def get_sidebar_for_page(wiki_page): + sidebar = [] + context = frappe._dict({}) + matching_pages = frappe.get_all('Wiki Page', {'route': wiki_page}) + if matching_pages: + sidebar, _ = frappe.get_doc('Wiki Page', matching_pages[0].get('name')).get_sidebar_items(context) + return sidebar \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page/wiki_renderer.py b/wiki/wiki/doctype/wiki_page/wiki_renderer.py new file mode 100644 index 00000000..8e6250b9 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page/wiki_renderer.py @@ -0,0 +1,40 @@ +import re + +import frappe +from frappe.website.utils import build_response +from frappe.website.page_renderers.document_page import DocumentPage +from frappe.website.router import get_doctypes_with_web_view + +from wiki.wiki.doctype.wiki_page.wiki_page import get_sidebar_for_page + +reg = re.compile('') +class WikiPageRenderer(DocumentPage): + + def search_in_doctypes_with_web_view(self): + for doctype in get_doctypes_with_web_view(): + if doctype != 'Wiki Page': + continue + filters = dict(route=self.path) + meta = frappe.get_meta(doctype) + condition_field = self.get_condition_field(meta) + + if condition_field: + filters[condition_field] = 1 + + try: + self.docname = frappe.db.get_value(doctype, filters, 'name') + if self.docname: + self.doctype = doctype + return True + except Exception as e: + if not frappe.db.is_missing_column(e): + raise e + + def render(self): + html = self.get_html() + html = self.add_csrf_token(html) + html = self.add_sidebar(html) + return build_response(self.path, html, self.http_status_code or 200, self.headers) + + def add_sidebar(self, html): + return reg.sub(get_sidebar_for_page(self.path), html) \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page_patch/__init__.py b/wiki/wiki/doctype/wiki_page_patch/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/wiki/doctype/wiki_page_patch/test_wiki_page_patch.py b/wiki/wiki/doctype/wiki_page_patch/test_wiki_page_patch.py new file mode 100644 index 00000000..886037ca --- /dev/null +++ b/wiki/wiki/doctype/wiki_page_patch/test_wiki_page_patch.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestWikiPagePatch(unittest.TestCase): + pass diff --git a/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.js b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.js new file mode 100644 index 00000000..ddcaf66d --- /dev/null +++ b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.js @@ -0,0 +1,75 @@ +// Copyright (c) 2021, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Wiki Page Patch", { + refresh: function (frm) { + + + frappe.call({ + method: "wiki.wiki.doctype.wiki_page.wiki_page.preview", + args: { + content: frm.doc.new_code, + name: frm.doc.wiki_page, + new: frm.doc.new ? 1 : "", + type: 'markdown', + diff_css:1 + }, + callback: (r) => { + if (r.message) { + $("#orignal_preview").append(r.message.orignal_preview); + $("#new_preview").append(r.message.html); + + if (!frm.doc.new) { + $(".wiki-diff").append(r.message.diff); + } + } + }, + }); + + + const lis = $("#new_sidebar"); + const sidebar_items = JSON.parse(cur_frm.doc.new_sidebar_items); + lis.empty(); + for (let sidebar in sidebar_items) { + for (let item in sidebar_items[sidebar]) { + let class_name = ("." + sidebar).replaceAll("/", "\\/"); + let target = lis.find(class_name); + if (!target.length) { + target = $("#new_sidebar"); + } + if (sidebar_items[sidebar][item].type == "Wiki Sidebar") { + $(target).append( + "" + + sidebar_items[sidebar][item].title + + "" + + "" + ); + } else { + $(target).append( + "" + + sidebar_items[sidebar][item].title + + "" + ); + } + } + } + + frappe + .call("wiki.wiki.doctype.wiki_page.wiki_page.get_sidebar_for_page", { + wiki_page: frm.doc.wiki_page, + }) + .then((result) => { + $("#old_sidebar").empty().append(result.message); + $("#old_sidebar .h6").removeClass("h6"); + $(".form-section .list-unstyled").removeClass("hidden"); + $(".form-section .list-unstyled").removeClass("list-unstyled"); + $(".form-section .web-sidebar").find("svg").remove(); + }); + + + }, +}); diff --git a/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.json b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.json new file mode 100644 index 00000000..766d633b --- /dev/null +++ b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.json @@ -0,0 +1,243 @@ +{ + "actions": [], + "creation": "2021-05-13 10:08:45.648142", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "wiki_page", + "message", + "new_title", + "new", + "sidebar_edited", + "column_break_3", + "raised_by", + "status", + "approved_by", + "compare_changes_section", + "diff", + "compare", + "code", + "orignal_code", + "new_code", + "previews_section", + "orignal_preview", + "new_preview_section_section", + "new_preview", + "orignal_preview_store", + "new_preview_store", + "amended_from", + "sidebar_section", + "new_sidebar_store", + "old_sidebar_store", + "new_sidebar", + "new_sidebar_items" + ], + "fields": [ + { + "fieldname": "wiki_page", + "fieldtype": "Link", + "label": "Wiki Page", + "options": "Wiki Page" + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "User" + }, + { + "depends_on": "eval:doc.status=='Approved';", + "fieldname": "approved_by", + "fieldtype": "Link", + "label": "Approved By", + "mandatory_depends_on": "eval:doc.status=='Approved';", + "options": "User" + }, + { + "default": "Under Review", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Under Review\nChanges Requested\nRejected\nApproved" + }, + { + "depends_on": "eval: !doc.new; ", + "fieldname": "compare_changes_section", + "fieldtype": "Section Break", + "label": "Compare Changes" + }, + { + "fieldname": "diff", + "fieldtype": "Text", + "hidden": 1, + "label": "Diff" + }, + { + "fieldname": "compare", + "fieldtype": "HTML", + "label": "Compare", + "options": "\n\t\n\t\n" + }, + { + "fieldname": "code", + "fieldtype": "Section Break", + "label": "Code" + }, + { + "depends_on": "eval: !doc.new; ", + "fieldname": "orignal_code", + "fieldtype": "Code", + "label": "Orignal Code" + }, + { + "fieldname": "new_code", + "fieldtype": "Code", + "label": "New Code" + }, + { + "depends_on": "eval: !doc.new; ", + "fieldname": "previews_section", + "fieldtype": "Section Break", + "label": "Orignal Preview Section" + }, + { + "fieldname": "orignal_preview", + "fieldtype": "HTML", + "label": "Orignal Preview", + "options": "" + }, + { + "fieldname": "new_preview_section_section", + "fieldtype": "Section Break", + "label": "New Preview Section" + }, + { + "fieldname": "new_preview", + "fieldtype": "HTML", + "label": "New Preview", + "options": "" + }, + { + "fieldname": "orignal_preview_store", + "fieldtype": "Text", + "hidden": 1, + "label": "Orignal Preview Store" + }, + { + "fieldname": "new_preview_store", + "fieldtype": "Text", + "hidden": 1, + "label": "New Preview Store" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Wiki Page Patch", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "message", + "fieldtype": "Data", + "label": "Message" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "new", + "fieldtype": "Check", + "in_list_view": 1, + "label": "New" + }, + { + "fieldname": "new_title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "New Title" + }, + { + "depends_on": "eval: doc.sidebar_edited;", + "fieldname": "sidebar_section", + "fieldtype": "Section Break", + "label": "SIdebar" + }, + { + "fieldname": "new_sidebar_store", + "fieldtype": "Text", + "hidden": 1, + "label": "New Sidebar Store" + }, + { + "fieldname": "new_sidebar", + "fieldtype": "HTML", + "label": "New Sidebar", + "options": "\n \n New Sidebar\n . \n\n \n \n\n \n Old Sidebar\n . \n\n \n \n" + }, + { + "fieldname": "old_sidebar_store", + "fieldtype": "Text", + "hidden": 1, + "label": "Old Sidebar Store" + }, + { + "fieldname": "new_sidebar_items", + "fieldtype": "Code", + "hidden": 1, + "label": "New Sidebar Items", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "sidebar_edited", + "fieldtype": "Check", + "label": "Sidebar Edited" + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-07-28 21:25:05.163797", + "modified_by": "Administrator", + "module": "Wiki", + "name": "Wiki Page Patch", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "message", + "track_changes": 1, + "track_seen": 1, + "track_views": 1 +} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.py b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.py new file mode 100644 index 00000000..72e633c3 --- /dev/null +++ b/wiki/wiki/doctype/wiki_page_patch/wiki_page_patch.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe.model.document import Document +from ghdiff import diff +from frappe import _ +from frappe.desk.form.utils import add_comment + + +class WikiPagePatch(Document): + def validate(self): + self.new_preview_store = frappe.utils.md_to_html(self.new_code) + if not self.new: + self.orignal_code = frappe.db.get_value("Wiki Page", self.wiki_page, "content") + self.diff = diff(self.orignal_code, self.new_code) + self.orignal_preview_store = frappe.utils.md_to_html(self.orignal_code) + def after_insert(self): + add_comment_to_patch(self.name, self.message) + frappe.db.commit() + + + + def on_submit(self): + if self.status == 'Rejected': + return + + if self.status != "Approved": + frappe.throw(_("Please approve/ reject the request before submitting")) + + wiki_page = frappe.get_doc("Wiki Page", self.wiki_page) + + if self.new: + self.create_new_wiki_page(wiki_page) + else: + self.update_old_page(wiki_page) + if self.sidebar_edited == '1': + self.update_sidebars() + for key in frappe.cache().hgetall('wiki_sidebar').keys(): + frappe.cache().hdel('wiki_sidebar', key) + + def create_new_wiki_page(self, wiki_page): + self.new_wiki_page = frappe.new_doc("Wiki Page") + + wiki_page_dict = { + "title": self.new_title, + "content": self.new_code, + "route": "/".join(wiki_page.route.split("/")[:-1] + [frappe.scrub(self.new_title)]), + "published": 1, + } + + self.new_wiki_page.update(wiki_page_dict) + self.new_wiki_page.save() + + def update_old_page(self, wiki_page): + wiki_page.update_page(self.new_title, self.new_code, self.message, self.raised_by) + updated_page = frappe.get_all('Wiki Sidebar Item', {'item': self.wiki_page, 'type': 'Wiki Page'}, pluck = 'name') + for page in updated_page: + frappe.db.set_value('Wiki Sidebar Item', page, 'title', self.new_title) + return + + def update_sidebars(self): + if not self.new_sidebar_items: + self.new_sidebar_items = '{}' + sidebars = json.loads(self.new_sidebar_items) + self.create_new_child(sidebars) + sidebar_items = sidebars.items() + if sidebar_items: + for sidebar, items in sidebar_items: + for idx, item in enumerate(items): + if sidebar == 'docs/v13/user/manual/en': + print(idx, item) + frappe.db.set_value('Wiki Sidebar Item', item['name'], 'parent', sidebar) + frappe.db.set_value('Wiki Sidebar Item', item['name'], 'idx', idx) + + def create_new_child(self, sidebars): + for sidebar, items in sidebars.items(): + for item in items: + if item['name'] == 'new-wiki-page': + # new wiki page was created(/new) + wiki_sidebar_item = frappe.new_doc('Wiki Sidebar Item') + wiki_sidebar_item_dict = { + "type": item['type'], + "item": self.new_wiki_page.name, + "parent": sidebar, + 'parenttype': 'Wiki Sidebar', + 'parentfield': 'sidebar_items' + } + wiki_sidebar_item.update(wiki_sidebar_item_dict) + wiki_sidebar_item.save() + item['name'] = self.new_wiki_page.name + + elif item.get('new'): + # new item was added via the add item button + sidebar_name = item.get('name') + if item['type'] == 'Wiki Sidebar': + # Create New Sidebar + wiki_sidebar = frappe.new_doc("Wiki Sidebar") + wiki_sidebar_dict = { + "route": item.get('group_name'), + "title": item.get('title'), + } + wiki_sidebar.update(wiki_sidebar_dict) + wiki_sidebar.save() + sidebar_name = wiki_sidebar.name + + # add new sidebar or page to wiki sidebar + wiki_sidebar_item = frappe.new_doc('Wiki Sidebar Item') + wiki_sidebar_item_dict = { + "type": item['type'], + "item":sidebar_name, + "parent": sidebar, + 'parenttype': 'Wiki Sidebar', + 'parentfield': 'sidebar_items' + } + wiki_sidebar_item.update(wiki_sidebar_item_dict) + wiki_sidebar_item.save() + item['name'] = wiki_sidebar_item.name + +@frappe.whitelist() +def add_comment_to_patch(reference_name, content): + email = frappe.session.user + name = frappe.db.get_value( + "User", frappe.session.user, ["first_name"], as_dict=True + ).get("first_name") + comment = add_comment("Wiki Page Patch", reference_name, content, email, name) + comment.timepassed = frappe.utils.pretty_date(comment.creation) + return comment diff --git a/wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.json b/wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.json index ca767a07..3059b46e 100644 --- a/wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.json +++ b/wiki/wiki/doctype/wiki_page_revision/wiki_page_revision.json @@ -7,7 +7,8 @@ "field_order": [ "message", "content", - "wiki_page" + "wiki_page", + "raised_by" ], "fields": [ { @@ -18,7 +19,6 @@ { "fieldname": "content", "fieldtype": "Code", - "in_list_view": 1, "label": "Content", "options": "Markdown", "reqd": 1 @@ -26,14 +26,21 @@ { "fieldname": "wiki_page", "fieldtype": "Link", + "in_list_view": 1, "label": "Wiki Page", "options": "Wiki Page", "reqd": 1 - } + }, + { + "fieldname": "raised_by", + "fieldtype": "Link", + "label": "Raised By", + "options": "User" + } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-09-26 18:57:03.558737", + "modified": "2021-05-17 11:56:08.260349", "modified_by": "Administrator", "module": "Wiki", "name": "Wiki Page Revision", diff --git a/wiki/wiki/doctype/wiki_settings/wiki_settings.json b/wiki/wiki/doctype/wiki_settings/wiki_settings.json index cb4fe4db..b8261378 100644 --- a/wiki/wiki/doctype/wiki_settings/wiki_settings.json +++ b/wiki/wiki/doctype/wiki_settings/wiki_settings.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "logo", - "sidebar" + "sidebar", + "javascript" ], "fields": [ { @@ -17,14 +18,20 @@ { "fieldname": "sidebar", "fieldtype": "Link", - "label": "Website Sidebar", - "options": "Website Sidebar" + "label": "Wiki Sidebar", + "options": "Wiki Sidebar" + }, + { + "fieldname": "javascript", + "fieldtype": "Code", + "label": "Javascript", + "options": "Javascript" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-12-15 17:03:18.387177", + "modified": "2021-06-22 15:30:18.457671", "modified_by": "Administrator", "module": "Wiki", "name": "Wiki Settings", diff --git a/wiki/wiki/doctype/wiki_sidebar/__init__.py b/wiki/wiki/doctype/wiki_sidebar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/wiki/doctype/wiki_sidebar/test_wiki_sidebar.py b/wiki/wiki/doctype/wiki_sidebar/test_wiki_sidebar.py new file mode 100644 index 00000000..d7585fba --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar/test_wiki_sidebar.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestWikiSidebar(unittest.TestCase): + pass diff --git a/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.js b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.js new file mode 100644 index 00000000..c190ce14 --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.js @@ -0,0 +1,17 @@ +// Copyright (c) 2021, Frappe and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Wiki Sidebar", { + refresh: function (frm) { + frm.set_query('type', 'sidebar_items' , function () { + return { + filters: { + name: ["in", ["Wiki Page", "Wiki Sidebar"]], + } + + + + }; + }); + }, +}); diff --git a/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.json b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.json new file mode 100644 index 00000000..7bf8b3cf --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.json @@ -0,0 +1,63 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "field:route", + "creation": "2021-05-07 01:38:50.629713", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "route", + "title", + "sidebar_items" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "sidebar_items", + "fieldtype": "Table", + "label": "Sidebar Items", + "options": "Wiki Sidebar Item" + }, + { + "allow_in_quick_entry": 1, + "fieldname": "route", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Sidebar Name", + "reqd": 1, + "unique": 1 + }, + { + "allow_in_quick_entry": 1, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title" + } + ], + "links": [], + "modified": "2021-07-14 09:46:23.477838", + "modified_by": "Administrator", + "module": "Wiki", + "name": "Wiki Sidebar", + "nsm_parent_field": "parent_wiki_sidebar", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.py b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.py new file mode 100644 index 00000000..dbb8986b --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar/wiki_sidebar.py @@ -0,0 +1,78 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + + +class WikiSidebar(Document): + + def get_children(self): + out = self.get_sidebar_items() + for sidebar_item in self.sidebar_items: + if sidebar_item.type == 'Wiki Sidebar': + sidebar = frappe.get_doc('Wiki Sidebar', sidebar_item.item) + children = sidebar.get_children() + out.append({ + "group_title": sidebar_item.title, + "group_items": children, + "name": sidebar_item.name, + "group_name": sidebar.name, + "type": "Wiki Sidebar" + }) + return out + + def get_items(self): + + topmost = self.find_topmost(self.name) + + sidebar_html = frappe.cache().hget('wiki_sidebar', topmost) + if not sidebar_html or frappe.conf.disable_website_cache or frappe.conf.developer_mode: + sidebar_items = frappe.get_doc('Wiki Sidebar', topmost).get_children() + context = frappe._dict({}) + context.sidebar_items = sidebar_items + context.docs_search_scope = topmost + sidebar_html = frappe.render_template('wiki/wiki/doctype/wiki_page/templates/web_sidebar.html', context) + frappe.cache().hset('wiki_sidebar', topmost, sidebar_html) + + return sidebar_html, topmost + + + + def get_sidebar_items(self): + items_without_group = [] + items = frappe.get_all( + "Wiki Sidebar Item", + filters={"parent": self.name, "type": 'Wiki Page'}, + fields=["title", "item", 'name', 'type'], + order_by="idx asc", + ) + + for item in items: + item.item = "/" + item.item + items_without_group.append(item) + + # return [{"group_title": "Topics", "group_items": items_without_group}] if items else [] + return items_without_group if items else [] + + + def validate(self): + self.clear_cache() + + def on_trash(self): + self.clear_cache() + + def on_update(self): + self.clear_cache() + + def find_topmost(self,me): + parent = frappe.db.get_value('Wiki Sidebar Item', { 'item' : me, 'type':'Wiki Sidebar' } , 'parent') + if not parent: + return me + return self.find_topmost(parent) + + def clear_cache(self): + topmost = self.find_topmost(self.name) + frappe.cache().hdel('wiki_sidebar', topmost) \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_sidebar_item/__init__.py b/wiki/wiki/doctype/wiki_sidebar_item/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.json b/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.json new file mode 100644 index 00000000..f16434c6 --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.json @@ -0,0 +1,51 @@ +{ + "actions": [], + "creation": "2021-05-07 01:55:10.733283", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "type", + "item", + "title" + ], + "fields": [ + { + "fetch_from": "item.title", + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title" + }, + { + "default": "Wiki Page", + "fieldname": "type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Type", + "options": "DocType", + "search_index": 1 + }, + { + "fieldname": "item", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Item", + "options": "type", + "reqd": 1, + "search_index": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2021-06-11 12:26:56.953302", + "modified_by": "Administrator", + "module": "Wiki", + "name": "Wiki Sidebar Item", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.py b/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.py new file mode 100644 index 00000000..11fe1a76 --- /dev/null +++ b/wiki/wiki/doctype/wiki_sidebar_item/wiki_sidebar_item.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WikiSidebarItem(Document): + pass diff --git a/wiki/wiki/workspace/wiki/wiki.json b/wiki/wiki/workspace/wiki/wiki.json index 31a804b1..b825e64e 100644 --- a/wiki/wiki/workspace/wiki/wiki.json +++ b/wiki/wiki/workspace/wiki/wiki.json @@ -27,12 +27,30 @@ { "hidden": 0, "is_query_report": 0, + "label": "Wiki Sidebar", + "link_to": "Wiki Sidebar", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, "label": "Wiki Settings", "link_to": "Wiki Settings", "link_type": "DocType", "onboard": 1, "type": "Link" }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Wiki Page Patch", + "link_to": "Wiki Page Patch", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, { "hidden": 0, "is_query_report": 0, diff --git a/wiki/www/__init__.py b/wiki/www/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/www/__pycache__/__init__.py b/wiki/www/__pycache__/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/wiki/www/compare.html b/wiki/www/compare.html new file mode 100644 index 00000000..f9f72bf2 --- /dev/null +++ b/wiki/www/compare.html @@ -0,0 +1,29 @@ +{% extends "wiki/doctype/wiki_page/templates/wiki_doc.html" %} + +{%- block head_include %} +{{ super() }} +{{ include_style('wiki.bundle.css') }} +{% endblock -%} + +{% block page_content %} + + {{ title }} + {{ revision.message }} + + {{ revision.owner }} edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} + + + + + {{ diff or 'No changes to show' }} + + + +{% endblock %} + + +{%- block navbar -%} +{% include "wiki/doctype/wiki_page/templates/wiki_navbar.html" %} + +{%- endblock -%} + diff --git a/wiki/www/compare.py b/wiki/www/compare.py new file mode 100644 index 00000000..ad017a37 --- /dev/null +++ b/wiki/www/compare.py @@ -0,0 +1,45 @@ + +import frappe + +def get_context(context): + context.no_cache = 1 + + wiki_settings = frappe.get_single("Wiki Settings") + context.banner_image = wiki_settings.logo + context.script = wiki_settings.javascript + context.docs_search_scope = "" + can_edit = frappe.session.user != "Guest" + context.can_edit = can_edit + context.show_my_account = False + + + context.doc = frappe.get_doc('Wiki Page', frappe.form_dict.wiki_page) + context.doc.set_breadcrumbs(context) + + + from ghdiff import diff + + revision = frappe.form_dict.compare + context.title = "Revision: " + revision + context.parents = [ + {"route": "/" + context.doc.route, "label": context.doc.title}, + {"route": "/" + context.doc.route + "?revisions=true", "label": "Revisions"}, + ] + + revision = frappe.get_doc("Wiki Page Revision", revision) + + context.revision = revision + previous_revision_content = frappe.db.get_value( + "Wiki Page Revision", + filters={"creation": ("<", revision.creation), "wiki_page": context.doc.name}, + fieldname=["content"], + order_by="creation asc", + ) + + if not previous_revision_content: + return + + context.diff = diff(previous_revision_content, revision.content, css=False) + + + return context \ No newline at end of file diff --git a/wiki/www/contributions.css b/wiki/www/contributions.css new file mode 100644 index 00000000..fea27dd7 --- /dev/null +++ b/wiki/www/contributions.css @@ -0,0 +1,51 @@ +.list-jobs { + font-size: 14px; +} + +.table { + margin-bottom: 0px; + margin-top: 0px; +} +thead td { + border-top: 0 !important;; +} + +.worker-name { + display: flex; + align-items: center; +} + +.job-name { + font-size: 13px; + font-family: "Courier New", Courier, monospace; + /* background-color: var(--control-bg); */ + /* padding: var(--padding-xs) var(--padding-sm); */ + /* border-radius: var(--border-radius-md); */ +} + +.background-job-row:hover { + background-color: #F9FAFA; +} + +.no-background-jobs { + min-height: 320px; + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; +} + +.no-background-jobs > img { + margin-bottom: var(15px); + max-height: 100px; +} + +.footer { + align-items: flex-end; + margin-top: 15px; + font-size: 14px; +} + +body { + background-color: #F0F0F0; +} \ No newline at end of file diff --git a/wiki/www/contributions.html b/wiki/www/contributions.html new file mode 100644 index 00000000..660467ed --- /dev/null +++ b/wiki/www/contributions.html @@ -0,0 +1,67 @@ +{% extends "wiki/doctype/wiki_page/templates/wiki_page.html" %} + +{%- block head_include %} +{{ super() }} +{{ include_style('wiki.bundle.css') }} + + + +{% endblock -%} + + +{% block page_content %} + Your Contributions + + + + + {% if contributions %} + + + + {{ _("Status") }} + {{ _("Message") }} + {{ _("Last update on") }} + {{ _("Link") }} + + + + {% for j in contributions %} + + {{ j.status }} + {{ j.message }} + {{ j.creation }} + Open Contribution + + + {% endfor %} + + + {% else %} + + + {{ _("No Contributions Made") }} + + {% endif %} + + + + +{% endblock %} + + +{% block base_scripts %} + + + + + + +{% endblock %} + + + + + +{% block page_sidebar %} +{% endblock %} \ No newline at end of file diff --git a/wiki/www/contributions.py b/wiki/www/contributions.py new file mode 100644 index 00000000..bbb6f42a --- /dev/null +++ b/wiki/www/contributions.py @@ -0,0 +1,27 @@ + +import frappe + +def get_context(context): + context.no_cache = 1 + context.no_sidebar = 1 + color_map = { + 'Changes Requested': 'blue', + 'Under Review': 'orange', + 'Rejected': 'red', + 'Approved': 'green', + } + + context.contributions = [] + contributions = frappe.get_list("Wiki Page Patch", + ["message", "status", "name", "wiki_page", 'creation', 'new'], + order_by='modified desc') + for contribution in contributions: + route = frappe.db.get_value("Wiki Page", contribution.wiki_page, "route") + if contribution.new: + contribution.edit_link = f'/{route}/new?wiki_page_patch={contribution.name}' + else: + contribution.edit_link = f'/{route}/edit?wiki_page_patch={contribution.name}' + contribution.color = color_map[contribution.status] + contribution.creation = frappe.utils.pretty_date(contribution.creation) + context.contributions.extend([contribution]) + return context \ No newline at end of file diff --git a/wiki/www/edit.html b/wiki/www/edit.html new file mode 100644 index 00000000..fc6d8eeb --- /dev/null +++ b/wiki/www/edit.html @@ -0,0 +1 @@ +{% extends "wiki/doctype/wiki_page/templates/wiki_page.html" %} \ No newline at end of file diff --git a/wiki/www/edit.py b/wiki/www/edit.py new file mode 100644 index 00000000..661d1f82 --- /dev/null +++ b/wiki/www/edit.py @@ -0,0 +1,53 @@ +import re +import frappe +from frappe.desk.form.load import get_comments + +def get_context(context): + context.no_cache = 1 + frappe.form_dict.edit = True + context.doc = frappe.get_doc('Wiki Page', frappe.form_dict.wiki_page) + + + context.doc.verify_permission("read") + + try: + boot = frappe.sessions.get() + except Exception as e: + boot = frappe._dict(status='failed', error = str(e)) + print(frappe.get_traceback()) + + boot_json = frappe.as_json(boot) + + # remove script tags from boot + boot_json = re.sub(r"\", "", boot_json) + + context.boot = boot_json + wiki_settings = frappe.get_single("Wiki Settings") + context.banner_image = wiki_settings.logo + context.script = wiki_settings.javascript + context.docs_search_scope = "" + can_edit = frappe.session.user != "Guest" + context.can_edit = can_edit + context.show_my_account = False + context.doc.set_breadcrumbs(context) + + if not can_edit: + context.doc.redirect_to_login("edit") + context.title = "Editing " + context.doc.title + if frappe.form_dict.wiki_page_patch: + context.wiki_page_patch = frappe.form_dict.wiki_page_patch + context.doc.content = frappe.db.get_value( + "Wiki Page Patch", context.wiki_page_patch, "new_code" + ) + context.comments = get_comments( + "Wiki Page Patch", frappe.form_dict.wiki_page_patch, "Comment" + ) + + context.content_md = context.doc.content + context.content_html = frappe.utils.md_to_html(context.doc.content) + context.sidebar_items, context.docs_search_scope = context.doc.get_sidebar_items(context) + return context + diff --git a/wiki/www/new.html b/wiki/www/new.html new file mode 100644 index 00000000..fc6d8eeb --- /dev/null +++ b/wiki/www/new.html @@ -0,0 +1 @@ +{% extends "wiki/doctype/wiki_page/templates/wiki_page.html" %} \ No newline at end of file diff --git a/wiki/www/new.py b/wiki/www/new.py new file mode 100644 index 00000000..3c7d4b49 --- /dev/null +++ b/wiki/www/new.py @@ -0,0 +1,60 @@ +import re +import frappe +from frappe.desk.form.load import get_comments + +def get_context(context): + context.no_cache = 1 + frappe.form_dict.edit = True + frappe.form_dict.new = 'true' + context.doc = frappe.get_doc('Wiki Page', frappe.form_dict.wiki_page) + # context = context.doc.get_context(context) + + + context.doc.verify_permission("read") + + try: + boot = frappe.sessions.get() + except Exception as e: + boot = frappe._dict(status='failed', error = str(e)) + print(frappe.get_traceback()) + + boot_json = frappe.as_json(boot) + + # remove script tags from boot + boot_json = re.sub(r"\", "", boot_json) + + context.boot = boot_json + wiki_settings = frappe.get_single("Wiki Settings") + context.banner_image = wiki_settings.logo + context.script = wiki_settings.javascript + context.docs_search_scope = "" + can_edit = frappe.session.user != "Guest" + context.can_edit = can_edit + context.show_my_account = False + context.doc.set_breadcrumbs(context) + + if not can_edit: + context.doc.redirect_to_login("create") + context.sidebar_items, context.docs_search_scope = context.doc.get_sidebar_items(context) + context.title = "New Wiki Page" + context.doc.title='New Wiki Page' + context.content_md = "New Wiki Page" + context.content_html = "New Wiki Page" + if frappe.form_dict.wiki_page_patch: + context.wiki_page_patch = frappe.form_dict.wiki_page_patch + context.doc.content = frappe.db.get_value( + "Wiki Page Patch", context.wiki_page_patch, "new_code" + ) + context.comments = get_comments( + "Wiki Page Patch", frappe.form_dict.wiki_page_patch, "Comment" + ) + context.content_md = context.doc.content + context.content_html = frappe.utils.md_to_html(context.doc.content) + return context + + + + diff --git a/wiki/www/revisions.html b/wiki/www/revisions.html new file mode 100644 index 00000000..b8e371c9 --- /dev/null +++ b/wiki/www/revisions.html @@ -0,0 +1,37 @@ +{% extends "wiki/doctype/wiki_page/templates/wiki_doc.html" %} + + +{%- block head_include %} +{{ super() }} +{{ include_style('wiki.bundle.css') }} +{% endblock -%} + +{% block page_content %} + + {{ title }} + + + {%- for revision in revisions -%} + + + {{ revision.message }} + + {%- if revision.raised_by -%} + {{ revision.raised_by }} + {%- else -%} + {{revision.owner }} + {%- endif -%} +  edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} + + + + {%- endfor -%} + +{% endblock %} + + +{%- block navbar -%} +{% include "wiki/doctype/wiki_page/templates/wiki_navbar.html" %} + +{%- endblock -%} + diff --git a/wiki/www/revisions.py b/wiki/www/revisions.py new file mode 100644 index 00000000..1da4e665 --- /dev/null +++ b/wiki/www/revisions.py @@ -0,0 +1,26 @@ + +import frappe + +def get_context(context): + + frappe.form_dict.revisions = True + wiki_settings = frappe.get_single("Wiki Settings") + context.banner_image = wiki_settings.logo + context.script = wiki_settings.javascript + context.docs_search_scope = "" + can_edit = frappe.session.user != "Guest" + context.can_edit = can_edit + context.show_my_account = False + + revisions = frappe.db.get_all( + "Wiki Page Revision", + filters={"wiki_page": frappe.form_dict.wiki_page}, + fields=["message", "creation", "owner", "name", "raised_by"], + ) + context.revisions = revisions + context.no_cache = 1 + context.doc = frappe.get_doc('Wiki Page', frappe.form_dict.wiki_page) + context.title = "Revisions: " + context.doc.title + + context.doc.set_breadcrumbs(context) + return context \ No newline at end of file
- {{ revision.owner }} edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} -
+ {{ revision.owner }} edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} +
{{ _("No Contributions Made") }}
+ {%- if revision.raised_by -%} + {{ revision.raised_by }} + {%- else -%} + {{revision.owner }} + {%- endif -%} +  edited on {{ frappe.utils.format_datetime(revision.creation, 'medium') }} +
Activity
+