diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2cc78054b51483849ed15482c51ea543774d9fda..ecf8b913e76007aa02b22e5bfc2a954df1b389f0 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -103,15 +103,11 @@ class ApplicationController < ActionController::Base
   end
 
   def top_level_post_types
-    Rails.cache.fetch 'top_level_post_types' do
-      PostType.where(is_top_level: true).select(:id).map(&:id)
-    end
+    helpers.post_type_ids(is_top_level: true)
   end
 
   def second_level_post_types
-    Rails.cache.fetch 'second_level_post_types' do
-      PostType.where(is_top_level: false, has_parent: true).select(:id).map(&:id)
-    end
+    helpers.post_type_ids(is_top_level: false, has_parent: true)
   end
 
   def check_edits_limit!(post)
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 0486ab3947d63eb905a5cd57d7a08b5df6df638b..e2e20379578a9327631cf1baebdf2779d132c77f 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -129,7 +129,8 @@ class PostsController < ApplicationController
       return redirect_to post_path(@post)
     end
 
-    if current_user.privilege?('edit_posts') || current_user.is_moderator || current_user == @post.user
+    if current_user.privilege?('edit_posts') || current_user.is_moderator || current_user == @post.user || \
+       (@post_type.is_freely_editable && current_user.privilege?('unrestricted'))
       if @post.update(edit_post_params.merge(body: body_rendered,
                                              last_edited_at: DateTime.now, last_edited_by: current_user,
                                              last_activity: DateTime.now, last_activity_by: current_user))
diff --git a/app/helpers/post_types_helper.rb b/app/helpers/post_types_helper.rb
index 222e2844b89b52de333214d8d58453e5bc8b2791..073e8ade026c43a00424e7d0b92fee45d1a38288 100644
--- a/app/helpers/post_types_helper.rb
+++ b/app/helpers/post_types_helper.rb
@@ -8,4 +8,15 @@ module PostTypesHelper
       tag.i(class: icon_class) + ' ' + tag.span(type) # rubocop:disable Style/StringConcatenation
     end
   end
+
+  def post_type_criteria
+    PostType.new.attributes.keys.select { |k| k.start_with?('has_') || k.start_with?('is_') }.map(&:to_sym)
+  end
+
+  def post_type_ids(**opts)
+    key = post_type_criteria.map { |a| opts[a] ? '1' : '0' }.join
+    Rails.cache.fetch "post_type_ids/#{key}" do
+      PostType.where(**opts).select(:id).map(&:id)
+    end
+  end
 end
diff --git a/app/models/community_user.rb b/app/models/community_user.rb
index 897672e95f4752708767345416f3ec2c5c07f551..a2c685ed4c2f14209564b2e383a435737012a703 100644
--- a/app/models/community_user.rb
+++ b/app/models/community_user.rb
@@ -25,8 +25,9 @@ class CommunityUser < ApplicationRecord
   # These are quite expensive, so we'll cache them for a while
   def post_score
     Rails.cache.fetch("privileges/#{id}/post_score", expires_in: 3.hours) do
-      good_posts = Post.where(user: user).where('score > 0.5').count
-      bad_posts = Post.where(user: user).where('score < 0.5').count
+      exclude_types = ApplicationController.helpers.post_type_ids(is_freely_editable: true)
+      good_posts = Post.where(user: user).where('score > 0.5').where.not(post_type_id: exclude_types).count
+      bad_posts = Post.where(user: user).where('score < 0.5').where.not(post_type_id: exclude_types).count
 
       (good_posts + 2.0) / (good_posts + bad_posts + 4.0)
     end
diff --git a/db/migrate/20201216225353_add_is_freely_editable_to_post_types.rb b/db/migrate/20201216225353_add_is_freely_editable_to_post_types.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1d5180118d8932a9e58ebc80e5af8f0d2396495b
--- /dev/null
+++ b/db/migrate/20201216225353_add_is_freely_editable_to_post_types.rb
@@ -0,0 +1,5 @@
+class AddIsFreelyEditableToPostTypes < ActiveRecord::Migration[5.2]
+  def change
+    add_column :post_types, :is_freely_editable, :boolean, null: false, default: false
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 8c4c796443e9b058652f5aeab6014b1a8c892de0..a701c093bc3df28db58167c4039169d82ed8a47f 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2020_12_12_235514) do
+ActiveRecord::Schema.define(version: 2020_12_16_225353) do
 
   create_table "abilities", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci", force: :cascade do |t|
     t.bigint "community_id"
@@ -309,6 +309,7 @@ ActiveRecord::Schema.define(version: 2020_12_12_235514) do
     t.boolean "is_public_editable", default: false, null: false
     t.boolean "is_closeable", default: false, null: false
     t.boolean "is_top_level", default: false, null: false
+    t.boolean "is_freely_editable", default: false, null: false
     t.index ["name"], name: "index_post_types_on_name"
   end
 
@@ -545,6 +546,7 @@ ActiveRecord::Schema.define(version: 2020_12_12_235514) do
     t.integer "failed_attempts", default: 0, null: false
     t.string "unlock_token"
     t.datetime "locked_at"
+    t.integer "trust_level"
     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true
     t.index ["email"], name: "index_users_on_email", unique: true
     t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb
index f59de979b46d35a966f937339cd2a65274036a39..16e50a1da49e15a5d473ffd083cd49795b9c1593 100644
--- a/test/controllers/posts_controller_test.rb
+++ b/test/controllers/posts_controller_test.rb
@@ -393,6 +393,20 @@ class PostsControllerTest < ActionController::TestCase
     assert_equal before_history, after_history, 'PostHistory event incorrectly created on update'
   end
 
+  test 'anyone with unrestricted can update free-edit post' do
+    sign_in users(:standard_user)
+    before_history = PostHistory.where(post: posts(:free_edit)).count
+    patch :update, params: { id: posts(:free_edit).id,
+                             post: { title: sample.edit.title, body_markdown: sample.edit.body_markdown,
+                                     tags_cache: sample.edit.tags_cache } }
+    after_history = PostHistory.where(post: posts(:free_edit)).count
+    assert_response 302
+    assert_redirected_to post_path(posts(:free_edit))
+    assert_not_nil assigns(:post)
+    assert_equal sample.edit.body_markdown, assigns(:post).body_markdown
+    assert_equal before_history + 1, after_history, 'No PostHistory event created on free-edit update'
+  end
+
   # Close
 
   test 'can close question' do
diff --git a/test/fixtures/categories.yml b/test/fixtures/categories.yml
index 5669e36b4f47099b43bdd50227c02333a0817546..f922ef750d97c18f39c1d2df6347f3e3c0ac5530 100644
--- a/test/fixtures/categories.yml
+++ b/test/fixtures/categories.yml
@@ -11,6 +11,7 @@ main:
     - question
     - answer
     - article
+    - free_edit
   tag_set: main
   license: cc_by_sa
 
diff --git a/test/fixtures/post_types.yml b/test/fixtures/post_types.yml
index dea8f1923fecae1422e2a244f1099156f06cd0ef..7cb6bd2b407e1a20eed4d0423c955a723401b8bd 100644
--- a/test/fixtures/post_types.yml
+++ b/test/fixtures/post_types.yml
@@ -10,6 +10,7 @@ question:
   is_public_editable: true
   is_closeable: true
   is_top_level: true
+  is_freely_editable: false
 
 answer:
   name: Answer
@@ -23,6 +24,7 @@ answer:
   is_public_editable: true
   is_closeable: false
   is_top_level: false
+  is_freely_editable: false
 
 article:
   name: Article
@@ -36,6 +38,7 @@ article:
   is_public_editable: true
   is_closeable: false
   is_top_level: true
+  is_freely_editable: false
 
 policy_doc:
   name: PolicyDoc
@@ -49,6 +52,7 @@ policy_doc:
   is_public_editable: false
   is_closeable: false
   is_top_level: false
+  is_freely_editable: false
 
 help_doc:
   name: HelpDoc
@@ -62,6 +66,7 @@ help_doc:
   is_public_editable: false
   is_closeable: false
   is_top_level: false
+  is_freely_editable: false
 
 blog_post:
   name: BlogPost
@@ -74,4 +79,19 @@ blog_post:
   has_license: true
   is_public_editable: false
   is_closeable: false
-  is_top_level: true
\ No newline at end of file
+  is_top_level: true
+  is_freely_editable: false
+
+free_edit:
+  name: FreeEdit
+  description: ~
+  has_answers: true
+  has_votes: true
+  has_tags: true
+  has_parent: false
+  has_category: true
+  has_license: true
+  is_public_editable: true
+  is_closeable: true
+  is_top_level: true
+  is_freely_editable: true
\ No newline at end of file
diff --git a/test/fixtures/posts.yml b/test/fixtures/posts.yml
index 54e50a21a3d00b8259dea8ec0b6219eff0a82e25..98444323b6aad4f7a5abfe252035e46ea1985116 100644
--- a/test/fixtures/posts.yml
+++ b/test/fixtures/posts.yml
@@ -183,6 +183,27 @@ locked_mod:
   upvote_count: 0
   downvote_count: 0
 
+free_edit:
+  post_type: free_edit
+  title: FE ABCDEF GHIJKL MNOPQR STUVWX YZ
+  body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+  body_markdown: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
+  tags_cache:
+    - discussion
+    - support
+    - bug
+  tags:
+    - discussion
+    - support
+    - bug
+  score: 0.5
+  user: moderator
+  community: sample
+  category: main
+  license: cc_by_sa
+  upvote_count: 0
+  downvote_count: 0
+
 answer_one:
   post_type: answer
   body: A1 ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ