class WikiPagesController < ApplicationController rescue_from Wiki::Commit::Conflict, with: :render_wiki_conflict def index title = params[:title].to_s.strip descriptions = TagDescription.where(language_code: 'ja') .includes(tag: :tag_names) pages = unmigrated_wiki_pages.includes(tag_name: :tag) tag_groups = TagGroup.all if title.present? pattern = "%#{ ApplicationRecord.sanitize_sql_like(title) }%" descriptions = descriptions.joins(tag: :tag_names) .where(tag_names: { language_code: 'ja', primary_flg: true }) .where('tag_names.name LIKE ?', pattern).limit(20) tag_groups = tag_groups.where( 'CONCAT(?, name) LIKE ?', TagGroupRepr::PREFIX, pattern).limit(20) pages = pages.where('tag_names.name LIKE ?', pattern).limit(20) end json = descriptions.map { |description| WikiPageRepr.tag_description(description) } json.concat(tag_groups.map { WikiPageRepr.tag_group(_1) }) json.concat(WikiPageRepr.base(pages)) json = json.first(20) if title.present? render json: end def show tag_description = TagDescription.find_by(id: params[:id]) return render_wiki_page_by_tag_description tag_description if tag_description tag_group = TagGroup.find_by(id: params[:id]) return render_tag_group tag_group if tag_group page = WikiPage.joins(:tag_name) .includes(tag_name: :tag) .find_by(id: params[:id]) render_wiki_page_or_404 page end def show_by_title title = params[:title].to_s.strip if title.start_with?(TagGroupRepr::PREFIX) name = title.delete_prefix(TagGroupRepr::PREFIX) tag_group = TagGroup.find_by(name:) return render_tag_group tag_group if tag_group return head :not_found end tag_description = tag_description_by_title(title) return render_wiki_page_by_tag_description tag_description if tag_description page = WikiPage.joins(:tag_name) .includes(tag_name: :tag) .find_by(tag_name: { name: title }) render_wiki_page_or_404 page end def exists return head :no_content if TagDescription.exists?(id: params[:id]) return head :no_content if TagGroup.exists?(id: params[:id]) head WikiPage.exists?(id: params[:id]) ? :no_content : :not_found end def exists_by_title title = params[:title].to_s.strip if title.start_with?(TagGroupRepr::PREFIX) exists = TagGroup.exists?(name: title.delete_prefix(TagGroupRepr::PREFIX)) return head exists ? :no_content : :not_found end return head :no_content if tag_description_by_title(title) exists = WikiPage.joins(:tag_name).exists?(tag_names: { name: title }) head exists ? :no_content : :not_found end def diff id = params[:id] return render_bad_request('id は必須です.') if id.blank? from = params[:from].presence to = params[:to].presence description = TagDescription.find_by(id:) if description versions = description_versions(description) from_rev = from && versions.find(from) to_rev = to ? versions.find(to) : versions.find_by!(version_no: description.version_no) resource = WikiPageRepr.tag_description(description) else tag_group = TagGroup.find_by(id:) if tag_group from_rev = from && group_version(tag_group, from) to_rev = to ? group_version(tag_group, to) : current_group_version(tag_group) return head :not_found if (from && !from_rev) || !to_rev from_body = from_rev && render_group_version(from_rev) to_body = render_group_version(to_rev) resource = { 'id' => tag_group.id, 'title' => "#{ TagGroupRepr::PREFIX }#{ tag_group.name }", 'deprecated_at' => nil } else page = WikiPage.joins(:tag_name).includes(tag_name: :tag).find(id) from_rev = from && page.wiki_revisions.find(from) to_rev = to ? page.wiki_revisions.find(to) : page.current_revision if ((from_rev && !from_rev.content?) || !to_rev&.content?) return render_unprocessable_entity('差分を表示できない版です.') end from_body = from_rev&.body to_body = to_rev.body resource = WikiPageRepr.base(page) end end from_body ||= from_rev&.body to_body ||= to_rev.body diff_json = wiki_diff(from_body, to_body) render json: { wiki_page_id: resource.fetch('id'), title: resource.fetch('title'), deprecated_at: resource.fetch('deprecated_at'), older_revision_id: from_rev&.id, newer_revision_id: to_rev.id, diff: diff_json } rescue TagGroupMarkdownRenderer::UnsupportedStructure => error render_unprocessable_entity(error.message) end def create return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? title = params[:title].to_s.strip body = params[:body].to_s message = params[:message].presence return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank? return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? page = if title.include?(':') ApplicationRecord.transaction do tag_name = TagName.find_or_create_by!(language_code: 'ja', name: title) do |name| name.script_code = 'Jpan' name.primary_flg = true name.auto_generated = false end Wiki::Commit.create_legacy_content!( tag_name:, body:, created_by_user: current_user, message:) end else tag_name = TagName.find_by(language_code: 'ja', name: title) return render_unprocessable_entity field: :title unless tag_name&.tag TagDescriptionOperation.create!( tag: tag_name.tag, body:, created_by_user: current_user, message:) end render json: WikiPageRepr.base(page), status: :created rescue ActiveRecord::RecordInvalid => e render_validation_error e.record rescue ActiveRecord::RecordNotUnique render_record_not_unique end def update return head :unauthorized unless current_user return head :forbidden unless current_user.gte_member? title = params[:title]&.strip body = params[:body].to_s return render_unprocessable_entity('タイトルは必須です.', field: :title) if title.blank? return render_unprocessable_entity('本文は必須です.', field: :body) if body.blank? page = WikiPage.find(params[:id]) base_revision_id = params[:base_revision_id].presence ApplicationRecord.transaction do page.lock! old_title = page.title tag = page.tag_name.tag if tag && title != old_title TagVersioning.ensure_snapshot!(tag, created_by_user: current_user) end page.tag_name.update!(name: title) if title != old_title message = params[:message].presence Wiki::Commit.content!(page:, body:, created_user: current_user, message:, base_revision_id:) if tag && title != old_title tag.reload TagVersionRecorder.record!(tag:, event_type: :update, created_by_user: current_user) end end head :ok end def changes id = params[:id].presence description = id && TagDescription.find_by(id:) if description return render json: description_changes(TagDescription.where(id: description.id)) end tag_group = id && TagGroup.find_by(id:) if tag_group return render json: tag_group_changes(TagGroupVersion.where(tag_group_id: tag_group.id)) end pages = id ? WikiPage.where(id:) : unmigrated_wiki_pages revisions = WikiRevision.joins(wiki_page: :tag_name) .where(wiki_page_id: pages.select(:id)) .includes(:created_user, wiki_page: { tag_name: :tag }) .order(id: :desc).limit(200) json = revisions.map { |rev| { revision_id: rev.id, pred: rev.base_revision_id, succ: nil, wiki_page: { id: rev.wiki_page_id, title: rev.wiki_page.title, deprecated_at: rev.wiki_page.deprecated_at }, user: rev.created_user && { id: rev.created_user.id, name: rev.created_user.name }, kind: rev.kind, message: rev.message, timestamp: rev.created_at } } unless id json.concat(tag_group_changes(TagGroupVersion.all)) json.concat(description_changes(TagDescription.where(language_code: 'ja'))) json = json.each_with_index.sort_by do |row, index| [-row.fetch(:timestamp).to_f, index] end.map(&:first) end render json: json.first(200) end private def render_tag_group group version = if params[:version].present? group_version(group, params[:version]) else current_group_version(group) end return head :not_found unless version representation = if params[:version].present? TagGroupRepr.historical(version) else TagGroupRepr.one(group) end render_group_representation(representation, version) end def render_group_version version representation = TagGroupRepr.historical(version) TagGroupMarkdownRenderer.render(representation) end def render_group_representation representation, version body = TagGroupMarkdownRenderer.render(representation) versions = TagGroupVersion.where(tag_group_id: version.tag_group_id) pred = versions.find_by(version_no: version.version_no - 1)&.id succ = versions.find_by(version_no: version.version_no + 1)&.id updated_at = version.created_at render json: { 'id' => version.tag_group_id, 'title' => "#{ TagGroupRepr::PREFIX }#{ representation.fetch('name') }", 'deprecated_at' => nil, 'body' => body, 'revision_id' => version.id, 'pred' => pred, 'succ' => succ, 'updated_at' => updated_at } rescue TagGroupMarkdownRenderer::UnsupportedStructure => error render_unprocessable_entity(error.message) end def current_group_version group TagGroupVersion.find_by(tag_group_id: group.id, version_no: group.version_no) end def group_version group, version_id TagGroupVersion.find_by(tag_group_id: group.id, id: version_id) end def wiki_diff from_body, to_body diffs = Diff::LCS.sdiff(from_body.to_s.lines, to_body.to_s.lines) diffs.map { |change| case change.action when ?= { type: 'context', content: change.old_element } when ?! [{ type: 'removed', content: change.old_element }, { type: 'added', content: change.new_element }] when ?+ { type: 'added', content: change.new_element } when ?- { type: 'removed', content: change.old_element } end }.flatten.compact end def tag_group_changes versions rows = versions.includes(:created_by_user).order(id: :desc).limit(200).to_a groups = TagGroup.where(id: rows.map(&:tag_group_id).uniq).index_by(&:id) predecessors = TagGroupVersion.where(tag_group_id: groups.keys, version_no: rows.map { _1.version_no - 1 }) .pluck(:tag_group_id, :version_no, :id) .to_h { |group_id, number, version_id| [[group_id, number], version_id] } rows.map do |version| group = groups.fetch(version.tag_group_id) user = version.created_by_user { revision_id: version.id, pred: predecessors[[group.id, version.version_no - 1]], succ: nil, wiki_page: { id: group.id, title: "#{ TagGroupRepr::PREFIX }#{ version.name }", deprecated_at: nil }, user: user && { id: user.id, name: user.name }, kind: 'content', message: nil, timestamp: version.created_at } end end def render_wiki_page_or_404 page return head :not_found unless page rev = find_revision(page) return head :not_found unless rev if rev.redirect? return ( redirect_to wiki_page_by_title_path(title: rev.redirect_page.title), status: :moved_permanently) end body = rev.body revision_id = rev.id pred = page.pred_revision_id(revision_id) succ = page.succ_revision_id(revision_id) updated_at = rev.created_at render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:) end def render_wiki_page_by_tag_description description versions = description_versions(description) historical = params[:version].present? rev = historical ? versions.find_by(id: params[:version]) : versions.find_by(version_no: description.version_no) return head :not_found unless rev pred = versions.find_by(version_no: rev.version_no - 1)&.id succ = versions.find_by(version_no: rev.version_no + 1)&.id body = historical ? rev.body : description.body updated_at = historical ? rev.created_at : description.updated_at render json: WikiPageRepr.tag_description(description).merge( body:, revision_id: rev.id, pred:, succ:, updated_at:) end def description_versions description TagDescriptionVersion.where(tag_id: description.tag_id, language_code: description.language_code) end def tag_description_by_title title TagDescription.joins(tag: :tag_names) .find_by(language_code: 'ja', tag_names: { language_code: 'ja', name: title }) end def unmigrated_wiki_pages descriptions = TagDescription.where(language_code: 'ja') WikiPage .joins(:tag_name) .where.not(id: descriptions.select(:id)) .where.not('tag_names.name LIKE ?', "#{ TagGroupRepr::PREFIX }%") .where('tag_names.tag_id IS NULL OR tag_names.tag_id NOT IN (?)', descriptions.select(:tag_id)) end def description_changes descriptions versions = TagDescriptionVersion.where(language_code: 'ja', tag_id: descriptions.select(:tag_id)) .order(id: :desc).limit(200).to_a return [] if versions.empty? by_tag = descriptions.where(tag_id: versions.map(&:tag_id)) .includes(tag: :tag_names).index_by(&:tag_id) users = User.where(id: versions.map(&:created_by_user_id)).index_by(&:id) predecessors = TagDescriptionVersion.where( language_code: 'ja', tag_id: by_tag.keys, version_no: versions.map { |version| version.version_no - 1 }) .pluck(:tag_id, :version_no, :id) .to_h { |tag_id, version_no, id| [[tag_id, version_no], id] } versions.map do |version| description = by_tag.fetch(version.tag_id) user = users[version.created_by_user_id] { revision_id: version.id, pred: predecessors[[version.tag_id, version.version_no - 1]], succ: nil, wiki_page: { id: description.id, title: description.tag.name('ja'), deprecated_at: description.tag.deprecated_at }, user: user && { id: user.id, name: user.name }, kind: 'content', message: nil, timestamp: version.created_at } end end def find_revision page if params[:version].present? page.wiki_revisions.find_by(id: params[:version]) else page.current_revision end end def render_wiki_conflict err render json: { error: 'conflict', message: err.message }, status: :conflict end end