diff --git a/app/controllers/edit_histories_controller.rb b/app/controllers/edit_histories_controller.rb new file mode 100644 index 000000000..d96114178 --- /dev/null +++ b/app/controllers/edit_histories_controller.rb @@ -0,0 +1,14 @@ +class EditHistoriesController < ApplicationController + respond_to :html + before_action :moderator_only + + def index + @edit_history = EditHistory.includes(:user).paginate(params[:page], limit: params[:limit]) + respond_with(@edit_history) + end + + def show + @edits = EditHistoryDecorator.decorate_collection(EditHistory.includes(:user).where('versionable_id = ? AND versionable_type = ?', params[:id], params[:type]).order(:id)) + respond_with(@edits) + end +end \ No newline at end of file diff --git a/app/decorators/edit_history_decorator.rb b/app/decorators/edit_history_decorator.rb new file mode 100644 index 000000000..c125ee04a --- /dev/null +++ b/app/decorators/edit_history_decorator.rb @@ -0,0 +1,58 @@ +class EditHistoryDecorator < ApplicationDecorator + def self.collection_decorator_class + PaginatedDecorator + end + + delegate_all + + def diff(other) + pattern = Regexp.new('(?:<.+?>)|(?:\w+)|(?:[ \t]+)|(?:\r?\n)|(?:.+?)') + + thisarr = other.body.scan(pattern) + otharr = object.body.scan(pattern) + + cbo = Diff::LCS::ContextDiffCallbacks.new + diffs = thisarr.diff(otharr, cbo) + + escape_html = ->(str) {str.gsub(/&/, '&').gsub(//, '>')} + + output = thisarr + output.each {|q| q.replace(escape_html[q])} + + diffs.reverse_each do |hunk| + newchange = hunk.max {|a, b| a.old_position <=> b.old_position} + newstart = newchange.old_position + oldstart = hunk.min {|a, b| a.old_position <=> b.old_position}.old_position + + if newchange.action == '+' + output.insert(newstart, '') + end + + hunk.reverse_each do |chg| + case chg.action + when '-' + oldstart = chg.old_position + output[chg.old_position] = '
' if chg.old_element.match(/^\r?\n$/) + when '+' + if chg.new_element.match(/^\r?\n$/) + output.insert(chg.old_position, '
') + else + output.insert(chg.old_position, "#{escape_html[chg.new_element]}") + end + end + end + + if newchange.action == '+' + output.insert(newstart, '') + end + + if hunk[0].action == '-' + output.insert((newstart == oldstart || newchange.action != '+') ? newstart + 1 : newstart, '') + output.insert(oldstart, '') + end + end + + output.join.gsub(/\r?\n/, '
').html_safe + end + +end diff --git a/app/javascript/src/styles/specific/edit_history.scss b/app/javascript/src/styles/specific/edit_history.scss new file mode 100644 index 000000000..d967f910a --- /dev/null +++ b/app/javascript/src/styles/specific/edit_history.scss @@ -0,0 +1,20 @@ +@import "../base/000_vars.scss"; + +#c-edit-history { + .edit-item { + display: flex; + &>div { + margin-right: 1em; + display: inline-block; + } + } + + ins { + background-color: $success_color; + font-weight: bold; + } + del { + background-color: $error_color; + font-weight: bold; + } +} \ No newline at end of file diff --git a/app/models/application_record.rb b/app/models/application_record.rb index a95c850c4..4b8e2c71f 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -241,6 +241,60 @@ class ApplicationRecord < ActiveRecord::Base end end + concerning :SimpleVersioningMethods do + class_methods do + def simple_versioning(options = {}) + cattr_accessor :versioning_body_column, :versioning_ip_column, :versioning_user_column, :versioning_subject_column + self.versioning_body_column = options[:body_column] || "body" + self.versioning_subject_column = options[:subject_column] + self.versioning_ip_column = options[:ip_column] || "creator_ip_addr" + self.versioning_user_column = options[:user_column] || "creator_id" + + class_eval do + has_many :versions, class_name: 'EditHistory', as: :versionable + after_update :save_version, if: :should_version_change + + define_method :should_version_change do + if self.versioning_subject_column + return true if send "saved_change_to_#{self.versioning_subject_column}?" + end + send "saved_change_to_#{self.versioning_body_column}?" + end + + define_method :save_version do + EditHistory.transaction do + our_next_version = next_version + if our_next_version == 0 + our_next_version += 1 + new = EditHistory.new + new.versionable = self + new.version = 1 + new.ip_addr = self.send self.versioning_ip_column + new.body = self.send "#{self.versioning_body_column}_before_last_save" + new.user_id = self.send self.versioning_user_column + new.subject = self.send "#{self.versioning_subject_column}_before_last_save" if self.versioning_subject_column + new.created_at = self.created_at + new.save + end + + version = EditHistory.new + version.version = our_next_version + 1 + version.versionable = self + version.ip_addr = CurrentUser.ip_addr + version.body = self.send self.versioning_body_column + version.user_id = CurrentUser.id + version.save + end + end + + define_method :next_version do + versions.count + end + end + end + end + end + concerning :UserMethods do class_methods do def belongs_to_creator(options = {}) diff --git a/app/models/blip.rb b/app/models/blip.rb index a1f450726..cf9314ca7 100644 --- a/app/models/blip.rb +++ b/app/models/blip.rb @@ -1,4 +1,5 @@ class Blip < ApplicationRecord + simple_versioning belongs_to_creator validates_presence_of :body belongs_to :parent, class_name: "Blip", foreign_key: "response_to", optional: true diff --git a/app/models/comment.rb b/app/models/comment.rb index ecbc81412..48ae5d4b2 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,6 +1,7 @@ class Comment < ApplicationRecord include Mentionable + simple_versioning validate :validate_post_exists, :on => :create validate :validate_creator_is_not_limited, :on => :create validates_presence_of :body, :message => "has no content" diff --git a/app/models/edit_history.rb b/app/models/edit_history.rb new file mode 100644 index 000000000..c5d66f5b6 --- /dev/null +++ b/app/models/edit_history.rb @@ -0,0 +1,14 @@ +class EditHistory < ApplicationRecord + self.table_name = 'edit_histories' + belongs_to :versionable, polymorphic: true + belongs_to :user + + attr_accessor :difference + + + TYPE_MAP = { + comment: 'Comment', + forum: 'ForumPost', + blip: 'Blip' + } +end \ No newline at end of file diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb index a7a301491..9bb380187 100644 --- a/app/models/forum_post.rb +++ b/app/models/forum_post.rb @@ -1,6 +1,7 @@ class ForumPost < ApplicationRecord include Mentionable + simple_versioning attr_readonly :topic_id belongs_to_creator belongs_to_updater @@ -160,7 +161,7 @@ class ForumPost < ApplicationRecord end def category_allows_replies - if topic && !topic.can_rely?(creator) + if topic && !topic.can_reply?(creator) errors[:topic] << "does not allow replies" return false end diff --git a/app/views/blips/partials/show/_blip.html.erb b/app/views/blips/partials/show/_blip.html.erb index d5cf82d88..f7871d917 100644 --- a/app/views/blips/partials/show/_blip.html.erb +++ b/app/views/blips/partials/show/_blip.html.erb @@ -53,6 +53,7 @@ <% if CurrentUser.is_moderator? %>
  • |
  • +
  • <%= link_to "Show Edits", edit_history_path(id: blip.id, type: 'Blip') %>
  • IP <%= link_to_ip blip.creator_ip_addr %> diff --git a/app/views/comments/partials/show/_comment.html.erb b/app/views/comments/partials/show/_comment.html.erb index 08c507a88..7cdab3de2 100644 --- a/app/views/comments/partials/show/_comment.html.erb +++ b/app/views/comments/partials/show/_comment.html.erb @@ -40,6 +40,7 @@
  • <%= link_to "Report", new_ticket_path(disp_id: comment.id, type: 'comment') %>
  • <% if CurrentUser.is_moderator? %>
  • |
  • +
  • <%= link_to "Show Edits", edit_history_path(id: comment.id, type: 'Comment') %>
  • IP <%= link_to_ip comment.creator_ip_addr %> diff --git a/app/views/edit_histories/index.html.erb b/app/views/edit_histories/index.html.erb new file mode 100644 index 000000000..9618ceb1f --- /dev/null +++ b/app/views/edit_histories/index.html.erb @@ -0,0 +1,37 @@ +
    +
    +

    Recent Edits

    + + + + + + + + + + + + + + + + <% @edit_history.each do |edit| %> + + + + + + + + + + <% end %> + +
    TypeDateIP AddressEditorBodySubject
    <%= link_to "Show", action: "show", id: edit.versionable_id, type: edit.versionable_type %><%= edit.versionable_type %><%= edit.updated_at.strftime("%b %d, %Y %I:%M %p") %><%= link_to_ip edit.ip_addr %><%= link_to_user edit.user %><%= edit.body[0..30] %><%= edit.subject&[0..30] %>
    + +
    + <%= numbered_paginator(@edit_history) %> +
    +
    +
    diff --git a/app/views/edit_histories/show.html.erb b/app/views/edit_histories/show.html.erb new file mode 100644 index 000000000..81d1d9556 --- /dev/null +++ b/app/views/edit_histories/show.html.erb @@ -0,0 +1,26 @@ +
    +
    +

    Edits for <%= h params[:type] %> #<%= h params[:id] %>

    + +
    + <% @edits.each_with_index do |edit, idx| %> +
    +
    +
    <%= link_to_user edit.user %>
    + "><%= edit.created_at.strftime("%b %d, %Y %I:%M %p") %> +
    <%= link_to_ip edit.ip_addr %>
    +
    +
    +
    + <% if edit.version > 1 %> + <%= edit.diff(@edits[idx-1]) %> + <% else %> + <%= edit.body %> + <% end %> +
    +
    +
    + <% end %> +
    +
    +
    diff --git a/app/views/forum_posts/_forum_post.html.erb b/app/views/forum_posts/_forum_post.html.erb index cc98c5253..2a7e2fa55 100644 --- a/app/views/forum_posts/_forum_post.html.erb +++ b/app/views/forum_posts/_forum_post.html.erb @@ -40,6 +40,11 @@ <% if CurrentUser.is_member? %>
  • <%= link_to "Report", new_ticket_path(disp_id: forum_post.id, type: 'forum') %>
  • <% end %> + <% if CurrentUser.is_moderator? %> +
  • |
  • +
  • <%= link_to "Show Edits", edit_history_path(id: forum_post.id, type: 'ForumPost') %>
  • +
  • |
  • + <% end %> <% if params[:controller] == "forum_posts" %>
  • <%= link_to "Parent", forum_topic_path(forum_post.topic, :page => forum_post.forum_topic_page, :anchor => "forum_post_#{forum_post.id}") %>
  • <% else %> diff --git a/config/routes.rb b/config/routes.rb index f31ac2440..1f2463385 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -10,6 +10,7 @@ Rails.application.routes.draw do resource :alias_and_implication_import, :only => [:new, :create] resource :dashboard, :only => [:show] end + resources :edit_histories namespace :moderator do resource :bulk_revert, :only => [:new, :create] resource :dashboard, :only => [:show] diff --git a/db/migrate/20190427163107_create_edit_histories.rb b/db/migrate/20190427163107_create_edit_histories.rb new file mode 100644 index 000000000..89d5f38cc --- /dev/null +++ b/db/migrate/20190427163107_create_edit_histories.rb @@ -0,0 +1,19 @@ +class CreateEditHistories < ActiveRecord::Migration[5.2] + def self.up + create_table :edit_histories do |t| + t.timestamps + t.text :body, null: false + t.text :subject, null: true + t.string :versionable_type, limit: 100, null: false + t.integer :versionable_id, null: false + t.integer :version, null: false + t.column :ip_addr, "inet", null: false + t.integer :user_id, index: true, null: false + t.index [:versionable_id, :versionable_type] + end + end + + def self.down + drop_table :edit_histories + end +end diff --git a/db/migrate/20190427181805_add_ip_to_forums.rb b/db/migrate/20190427181805_add_ip_to_forums.rb new file mode 100644 index 000000000..9c69f08a1 --- /dev/null +++ b/db/migrate/20190427181805_add_ip_to_forums.rb @@ -0,0 +1,8 @@ +class AddIpToForums < ActiveRecord::Migration[5.2] + def change + ForumPost.without_timeout do + add_column :forum_posts, :creator_ip_addr, :inet + add_column :forum_topics, :creator_ip_addr, :inet + end + end +end diff --git a/db/structure.sql b/db/structure.sql index dabcf1a54..57b32d612 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -779,6 +779,43 @@ CREATE SEQUENCE public.dmails_id_seq ALTER SEQUENCE public.dmails_id_seq OWNED BY public.dmails.id; +-- +-- Name: edit_histories; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.edit_histories ( + id bigint NOT NULL, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL, + body text NOT NULL, + subject text, + versionable_type character varying(100) NOT NULL, + versionable_id integer NOT NULL, + version integer NOT NULL, + ip_addr inet NOT NULL, + user_id integer NOT NULL +); + + +-- +-- Name: edit_histories_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.edit_histories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: edit_histories_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.edit_histories_id_seq OWNED BY public.edit_histories.id; + + -- -- Name: email_blacklists; Type: TABLE; Schema: public; Owner: - -- @@ -958,7 +995,8 @@ CREATE TABLE public.forum_posts ( text_index tsvector NOT NULL, is_deleted boolean DEFAULT false NOT NULL, created_at timestamp without time zone, - updated_at timestamp without time zone + updated_at timestamp without time zone, + creator_ip_addr inet ); @@ -1066,7 +1104,8 @@ CREATE TABLE public.forum_topics ( created_at timestamp without time zone, updated_at timestamp without time zone, category_id integer DEFAULT 0 NOT NULL, - min_level integer DEFAULT 0 NOT NULL + min_level integer DEFAULT 0 NOT NULL, + creator_ip_addr inet ); @@ -2649,6 +2688,13 @@ ALTER TABLE ONLY public.dmail_filters ALTER COLUMN id SET DEFAULT nextval('publi ALTER TABLE ONLY public.dmails ALTER COLUMN id SET DEFAULT nextval('public.dmails_id_seq'::regclass); +-- +-- Name: edit_histories id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.edit_histories ALTER COLUMN id SET DEFAULT nextval('public.edit_histories_id_seq'::regclass); + + -- -- Name: email_blacklists id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3116,6 +3162,14 @@ ALTER TABLE ONLY public.dmails ADD CONSTRAINT dmails_pkey PRIMARY KEY (id); +-- +-- Name: edit_histories edit_histories_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.edit_histories + ADD CONSTRAINT edit_histories_pkey PRIMARY KEY (id); + + -- -- Name: email_blacklists email_blacklists_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3778,6 +3832,20 @@ CREATE INDEX index_dmails_on_message_index ON public.dmails USING gin (message_i CREATE INDEX index_dmails_on_owner_id ON public.dmails USING btree (owner_id); +-- +-- Name: index_edit_histories_on_user_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_edit_histories_on_user_id ON public.edit_histories USING btree (user_id); + + +-- +-- Name: index_edit_histories_on_versionable_id_and_versionable_type; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_edit_histories_on_versionable_id_and_versionable_type ON public.edit_histories USING btree (versionable_id, versionable_type); + + -- -- Name: index_favorite_groups_on_creator_id; Type: INDEX; Schema: public; Owner: - -- @@ -4810,6 +4878,8 @@ INSERT INTO "schema_migrations" (version) VALUES ('20190409195837'), ('20190410022203'), ('20190413055451'), -('20190418093745'); +('20190418093745'), +('20190427163107'), +('20190427181805');