Browse Source

Merge branch 'olympia' into olympia-proposals-mappings

Leszek Wiesner 3 years ago
parent
commit
e6a08f77ba

+ 0 - 2
Cargo.lock

@@ -1,7 +1,5 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
-
 [[package]]
 name = "Inflector"
 version = "0.11.4"

+ 2 - 3
pioneer/packages/joy-election/src/VoteForm.tsx

@@ -1,5 +1,5 @@
 import BN from 'bn.js';
-import uuid from 'uuid/v4';
+import { randomAsHex } from '@polkadot/util-crypto';
 
 import React from 'react';
 import { Message, Table } from 'semantic-ui-react';
@@ -25,9 +25,8 @@ import { saveVote, NewVote } from './myVotesStore';
 import { TxFailedCallback } from '@polkadot/react-components/Status/types';
 import { RouteProps } from 'react-router-dom';
 
-// TODO use a crypto-prooven generator instead of UUID 4.
 function randomSalt () {
-  return uuid().replace(/-/g, '');
+  return randomAsHex();
 }
 
 // AppsProps is needed to get a location from the route.

+ 9 - 2
query-node/mappings/workingGroups.ts

@@ -71,6 +71,7 @@ import {
   BudgetSetEvent,
   BudgetSpendingEvent,
   LeaderSetEvent,
+  WorkerStatusLeaving,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 
@@ -803,9 +804,15 @@ export async function workingGroups_WorkerExited({ store, event }: EventContext
   })
 
   await store.save<WorkerExitedEvent>(workerExitedEvent)
-  ;(worker.status as WorkerStatusLeft).workerExitedEventId = workerExitedEvent.id
+
+  const newStatus = new WorkerStatusLeft()
+  newStatus.workerStartedLeavingEventId = (worker.status as WorkerStatusLeaving).workerStartedLeavingEventId
+  newStatus.workerExitedEventId = workerExitedEvent.id
+
+  worker.status = newStatus
   worker.stake = new BN(0)
   worker.rewardPerBlock = new BN(0)
+  worker.missingRewardAmount = undefined
   worker.updatedAt = eventTime
 
   await store.save<Worker>(worker)
@@ -919,7 +926,7 @@ export async function workingGroups_WorkerStartedLeaving({ store, event }: Event
 
   await store.save<WorkerStartedLeavingEvent>(workerStartedLeavingEvent)
 
-  const status = new WorkerStatusLeft()
+  const status = new WorkerStatusLeaving()
   status.workerStartedLeavingEventId = workerStartedLeavingEvent.id
   worker.status = status
   worker.updatedAt = eventTime

+ 8 - 3
query-node/schemas/workingGroups.graphql

@@ -3,12 +3,17 @@ type WorkerStatusActive @variant {
   _phantom: Int
 }
 
+type WorkerStatusLeaving @variant {
+  "Related event emitted on leaving initialization"
+  workerStartedLeavingEvent: WorkerStartedLeavingEvent!
+}
+
 type WorkerStatusLeft @variant {
   "Related event emitted on leaving initialization"
   workerStartedLeavingEvent: WorkerStartedLeavingEvent!
 
-  "Related event emitted (and set) when the unstaking period is finished"
-  workerExitedEvent: WorkerExitedEvent
+  "Related event emitted once the worker has exited the role (after the unstaking period)"
+  workerExitedEvent: WorkerExitedEvent!
 }
 
 type WorkerStatusTerminated @variant {
@@ -16,7 +21,7 @@ type WorkerStatusTerminated @variant {
   terminatedWorkerEvent: TerminatedWorkerEvent!
 }
 
-union WorkerStatus = WorkerStatusActive | WorkerStatusLeft | WorkerStatusTerminated
+union WorkerStatus = WorkerStatusActive | WorkerStatusLeaving | WorkerStatusLeft | WorkerStatusTerminated
 
 # Working Groups
 type Worker @entity {

+ 214 - 2
runtime-modules/forum/src/benchmarking.rs

@@ -591,7 +591,7 @@ benchmarks! {
 
         assert_eq!(Module::<T>::category_by_id(category_id), new_category);
         assert_last_event::<T>(
-            RawEvent::CategoryUpdated(
+            RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 PrivilegedActor::Lead
@@ -636,7 +636,7 @@ benchmarks! {
 
         assert_eq!(Module::<T>::category_by_id(category_id), new_category);
         assert_last_event::<T>(
-            RawEvent::CategoryUpdated(
+            RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 PrivilegedActor::Moderator(moderator_id)
@@ -644,6 +644,188 @@ benchmarks! {
         );
     }
 
+    update_category_title_lead{
+        let lead_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(lead_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_title = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+
+    }: update_category_title(RawOrigin::Signed(caller_id), PrivilegedActor::Lead, category_id, new_title.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_title_hash = T::calculate_hash(new_title.as_slice());
+
+        let new_category = Category {
+            title_hash: new_title_hash,
+            description_hash: T::calculate_hash(text.as_slice()),
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 0,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                PrivilegedActor::Lead
+            ).into()
+        );
+    }
+
+    update_category_title_moderator{
+        let moderator_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(moderator_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_title = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+        let moderator_id = ModeratorId::<T>::from(moderator_id.try_into().unwrap());
+
+        // Set up category membership of moderator.
+        Module::<T>::update_category_membership_of_moderator(
+            RawOrigin::Signed(caller_id.clone()).into(), moderator_id, category_id, true
+        ).unwrap();
+
+    }: update_category_title(RawOrigin::Signed(caller_id), PrivilegedActor::Moderator(moderator_id), category_id, new_title.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_title_hash = T::calculate_hash(new_title.as_slice());
+
+        let new_category = Category {
+            title_hash: new_title_hash,
+            description_hash: T::calculate_hash(text.as_slice()),
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 1,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                PrivilegedActor::Moderator(moderator_id)
+            ).into()
+        );
+    }
+
+    update_category_description_lead{
+        let lead_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(lead_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_description = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+
+    }: update_category_description(RawOrigin::Signed(caller_id), PrivilegedActor::Lead, category_id, new_description.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_description_hash = T::calculate_hash(new_description.as_slice());
+
+        let new_category = Category {
+            title_hash: T::calculate_hash(text.as_slice()),
+            description_hash: new_description_hash,
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 0,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                PrivilegedActor::Lead
+            ).into()
+        );
+    }
+
+    update_category_description_moderator{
+        let moderator_id = 0;
+
+        let caller_id =
+            insert_a_leader::<T>(moderator_id);
+
+        let i in 1 .. (T::MaxCategoryDepth::get() + 1) as u32;
+
+
+        let j in 0 .. MAX_BYTES - 1;
+
+        let new_description = vec![0u8].repeat(j as usize);
+
+        // Generate categories tree
+        let (category_id, parent_category_id) = generate_categories_tree::<T>(caller_id.clone(), i, None);
+
+        let moderator_id = ModeratorId::<T>::from(moderator_id.try_into().unwrap());
+
+        // Set up category membership of moderator.
+        Module::<T>::update_category_membership_of_moderator(
+            RawOrigin::Signed(caller_id.clone()).into(), moderator_id, category_id, true
+        ).unwrap();
+
+    }: update_category_description(RawOrigin::Signed(caller_id), PrivilegedActor::Moderator(moderator_id), category_id, new_description.clone())
+    verify {
+        let text = vec![0u8].repeat(MAX_BYTES as usize);
+        let new_description_hash = T::calculate_hash(new_description.as_slice());
+
+        let new_category = Category {
+            title_hash: T::calculate_hash(text.as_slice()),
+            description_hash: new_description_hash,
+            archived: false,
+            num_direct_subcategories: 0,
+            num_direct_threads: 0,
+            num_direct_moderators: 1,
+            parent_category_id,
+            sticky_thread_ids: vec![],
+        };
+
+        assert_eq!(Module::<T>::category_by_id(category_id), new_category);
+        assert_last_event::<T>(
+            RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                PrivilegedActor::Moderator(moderator_id)
+            ).into()
+        );
+    }
+
     delete_category_lead {
 
         let lead_id = 0;
@@ -1833,6 +2015,36 @@ mod tests {
         });
     }
 
+    #[test]
+    fn test_update_category_title_lead() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_title_lead::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_title_moderator() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_title_moderator::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_description_lead() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_description_lead::<Runtime>());
+        });
+    }
+
+    #[test]
+    fn test_update_category_description_moderator() {
+        with_test_externalities(|| {
+            assert_ok!(test_benchmark_update_category_description_moderator::<
+                Runtime,
+            >());
+        });
+    }
+
     #[test]
     fn test_delete_posts() {
         with_test_externalities(|| {

+ 123 - 8
runtime-modules/forum/src/lib.rs

@@ -61,6 +61,10 @@ pub trait WeightInfo {
     fn update_category_membership_of_moderator_old() -> Weight;
     fn update_category_archival_status_lead(i: u32) -> Weight;
     fn update_category_archival_status_moderator(i: u32) -> Weight;
+    fn update_category_title_lead(i: u32, j: u32) -> Weight;
+    fn update_category_title_moderator(i: u32, j: u32) -> Weight;
+    fn update_category_description_lead(i: u32, j: u32) -> Weight;
+    fn update_category_description_moderator(i: u32, j: u32) -> Weight;
     fn delete_category_lead(i: u32) -> Weight;
     fn delete_category_moderator(i: u32) -> Weight;
     fn create_thread(j: u32, k: u32, i: u32) -> Weight;
@@ -209,7 +213,7 @@ pub struct Poll<Timestamp, Hash> {
 
 /// Represents a thread post
 #[cfg_attr(feature = "std", derive(Serialize, Deserialize))]
-#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, Debug)]
+#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
 pub struct Post<ForumUserId, ThreadId, Hash, Balance, BlockNumber> {
     /// Id of thread to which this post corresponds.
     pub thread_id: ThreadId,
@@ -469,6 +473,7 @@ decl_event!(
         ModeratorId = ModeratorId<T>,
         <T as Trait>::ThreadId,
         <T as Trait>::PostId,
+        <T as frame_system::Trait>::Hash,
         ForumUserId = ForumUserId<T>,
         <T as Trait>::PostReactionId,
         PrivilegedActor = PrivilegedActor<T>,
@@ -477,9 +482,17 @@ decl_event!(
         /// A category was introduced
         CategoryCreated(CategoryId, Option<CategoryId>, Vec<u8>, Vec<u8>),
 
-        /// A category with given id was updated.
+        /// An arhical status of category with given id was updated.
         /// The second argument reflects the new archival status of the category.
-        CategoryUpdated(CategoryId, bool, PrivilegedActor),
+        CategoryArchivalStatusUpdated(CategoryId, bool, PrivilegedActor),
+
+        /// A title of category with given id was updated.
+        /// The second argument reflects the new title hash of the category.
+        CategoryTitleUpdated(CategoryId, Hash, PrivilegedActor),
+
+        /// A discription of category with given id was updated.
+        /// The second argument reflects the new description hash of the category.
+        CategoryDescriptionUpdated(CategoryId, Hash, PrivilegedActor),
 
         /// A category was deleted
         CategoryDeleted(CategoryId, PrivilegedActor),
@@ -578,6 +591,7 @@ decl_module! {
             Ok(())
         }
 
+
         /// Add a new category.
         ///
         /// <weight>
@@ -687,7 +701,105 @@ decl_module! {
 
             // Generate event
             Self::deposit_event(
-                RawEvent::CategoryUpdated(category_id, new_archival_status, actor)
+                RawEvent::CategoryArchivalStatusUpdated(category_id, new_archival_status, actor)
+            );
+
+            Ok(())
+        }
+
+        /// Update category title
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (W + V)` where:
+        /// - `W` is the category depth
+        /// - `V` is the length of the category title.
+        /// - DB:
+        ///    - O(W)
+        /// # </weight>
+        #[weight = WeightInfoForum::<T>::update_category_title_lead(
+            T::MaxCategoryDepth::get() as u32,
+            title.len().saturated_into(),
+        ).max(WeightInfoForum::<T>::update_category_title_moderator(
+            T::MaxCategoryDepth::get() as u32,
+            title.len().saturated_into(),
+        ))]
+        fn update_category_title(origin, actor: PrivilegedActor<T>, category_id: T::CategoryId, title: Vec<u8>) -> DispatchResult {
+            // Ensure data migration is done
+            Self::ensure_data_migration_done()?;
+
+            let account_id = ensure_signed(origin)?;
+
+            // Ensure actor can update category
+            let category = Self::ensure_can_moderate_category(&account_id, &actor, &category_id)?;
+
+            let title_hash = T::calculate_hash(title.as_slice());
+
+            // No change, invalid transaction
+            if title_hash == category.title_hash {
+                return Err(Error::<T>::CategoryNotBeingUpdated.into())
+            }
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Mutate category, and set possible new change parameters
+            <CategoryById<T>>::mutate(category_id, |c| c.title_hash = title_hash);
+
+            // Generate event
+            Self::deposit_event(
+                RawEvent::CategoryTitleUpdated(category_id, title_hash, actor)
+            );
+
+            Ok(())
+        }
+
+        /// Update category description
+        ///
+        /// <weight>
+        ///
+        /// ## Weight
+        /// `O (W)` where:
+        /// - `W` is the category depth
+        /// - `V` is the length of the category description.
+        /// - DB:
+        ///    - O(W)
+        /// # </weight>
+        #[weight = WeightInfoForum::<T>::update_category_description_lead(
+            T::MaxCategoryDepth::get() as u32,
+            description.len().saturated_into(),
+        ).max(WeightInfoForum::<T>::update_category_description_moderator(
+            T::MaxCategoryDepth::get() as u32,
+            description.len().saturated_into(),
+        ))]
+        fn update_category_description(origin, actor: PrivilegedActor<T>, category_id: T::CategoryId, description: Vec<u8>) -> DispatchResult {
+            // Ensure data migration is done
+            Self::ensure_data_migration_done()?;
+
+            let account_id = ensure_signed(origin)?;
+
+            // Ensure actor can update category
+            let category = Self::ensure_can_moderate_category(&account_id, &actor, &category_id)?;
+
+            let description_hash = T::calculate_hash(description.as_slice());
+
+            // No change, invalid transaction
+            if description_hash == category.description_hash {
+                return Err(Error::<T>::CategoryNotBeingUpdated.into())
+            }
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            // Mutate category, and set possible new change parameters
+            <CategoryById<T>>::mutate(category_id, |c| c.description_hash = description_hash);
+
+            // Generate event
+            Self::deposit_event(
+                RawEvent::CategoryDescriptionUpdated(category_id, description_hash, actor)
             );
 
             Ok(())
@@ -1333,13 +1445,16 @@ decl_module! {
             rationale: Vec<u8>,
         ) -> DispatchResult {
 
+            // Check only unique post instances.
+            let unique_posts: BTreeSet<_> = posts.into_iter().collect();
+
             // Ensure data migration is done
             Self::ensure_data_migration_done()?;
 
             let account_id = ensure_signed(origin)?;
 
-            let mut deleting_posts = Vec::new();
-            for (category_id, thread_id, post_id, hide) in &posts {
+            let mut deleting_posts = BTreeSet::new();
+            for (category_id, thread_id, post_id, hide) in &unique_posts {
                 // Ensure actor is allowed to moderate post and post is editable
                 let post = Self::ensure_can_delete_post(
                     &account_id,
@@ -1350,7 +1465,7 @@ decl_module! {
                     *hide,
                 )?;
 
-                deleting_posts.push((category_id, thread_id, post_id, post));
+                deleting_posts.insert((category_id, thread_id, post_id, post));
             }
 
             //
@@ -1366,7 +1481,7 @@ decl_module! {
 
             // Generate event
             Self::deposit_event(
-                RawEvent::PostDeleted(rationale, forum_user_id, posts)
+                RawEvent::PostDeleted(rationale, forum_user_id, unique_posts.into_iter().collect())
             );
 
             Ok(())

+ 81 - 3
runtime-modules/forum/src/mock.rs

@@ -435,6 +435,18 @@ impl WeightInfo for () {
     fn update_category_archival_status_moderator(_: u32) -> Weight {
         0
     }
+    fn update_category_title_lead(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_title_moderator(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_description_lead(_: u32, _: u32) -> Weight {
+        0
+    }
+    fn update_category_description_moderator(_: u32, _: u32) -> Weight {
+        0
+    }
     fn delete_category_lead(_: u32) -> Weight {
         0
     }
@@ -540,10 +552,18 @@ pub fn good_category_title() -> Vec<u8> {
     b"Great new category".to_vec()
 }
 
+pub fn good_category_title_new() -> Vec<u8> {
+    b"Great new category title".to_vec()
+}
+
 pub fn good_category_description() -> Vec<u8> {
     b"This is a great new category for the forum".to_vec()
 }
 
+pub fn good_category_description_new() -> Vec<u8> {
+    b"This is a great new category description for the forum".to_vec()
+}
+
 pub fn good_thread_title() -> Vec<u8> {
     b"Great new thread".to_vec()
 }
@@ -1072,7 +1092,7 @@ pub fn update_category_archival_status_mock(
     if result.is_ok() {
         assert_eq!(
             System::events().last().unwrap().event,
-            TestEvent::forum_mod(RawEvent::CategoryUpdated(
+            TestEvent::forum_mod(RawEvent::CategoryArchivalStatusUpdated(
                 category_id,
                 new_archival_status,
                 actor
@@ -1081,12 +1101,70 @@ pub fn update_category_archival_status_mock(
     }
 }
 
+pub fn update_category_title_mock(
+    origin: OriginType,
+    actor: PrivilegedActor<Runtime>,
+    category_id: <Runtime as Trait>::CategoryId,
+    new_title: Vec<u8>,
+    result: DispatchResult,
+) {
+    let new_title_hash = Runtime::calculate_hash(new_title.as_slice());
+    assert_eq!(
+        TestForumModule::update_category_title(
+            mock_origin(origin),
+            actor.clone(),
+            category_id,
+            new_title
+        ),
+        result
+    );
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            TestEvent::forum_mod(RawEvent::CategoryTitleUpdated(
+                category_id,
+                new_title_hash,
+                actor
+            ))
+        );
+    }
+}
+
+pub fn update_category_description_mock(
+    origin: OriginType,
+    actor: PrivilegedActor<Runtime>,
+    category_id: <Runtime as Trait>::CategoryId,
+    new_description: Vec<u8>,
+    result: DispatchResult,
+) {
+    let new_description_hash = Runtime::calculate_hash(new_description.as_slice());
+    assert_eq!(
+        TestForumModule::update_category_description(
+            mock_origin(origin),
+            actor.clone(),
+            category_id,
+            new_description
+        ),
+        result
+    );
+    if result.is_ok() {
+        assert_eq!(
+            System::events().last().unwrap().event,
+            TestEvent::forum_mod(RawEvent::CategoryDescriptionUpdated(
+                category_id,
+                new_description_hash,
+                actor
+            ))
+        );
+    }
+}
+
 pub fn delete_category_mock(
     origin: OriginType,
     moderator_id: PrivilegedActor<Runtime>,
     category_id: <Runtime as Trait>::CategoryId,
     result: DispatchResult,
-) -> () {
+) {
     assert_eq!(
         TestForumModule::delete_category(mock_origin(origin), moderator_id.clone(), category_id),
         result,
@@ -1097,7 +1175,7 @@ pub fn delete_category_mock(
             System::events().last().unwrap().event,
             TestEvent::forum_mod(RawEvent::CategoryDeleted(category_id, moderator_id))
         );
-    };
+    }
 }
 
 pub fn moderate_thread_mock(

+ 254 - 0
runtime-modules/forum/src/tests.rs

@@ -415,6 +415,260 @@ fn update_category_archival_status_lock_works() {
     });
 }
 
+#[test]
+// test if category updator is forum lead
+fn update_category_description_origin() {
+    let origins = [FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![Ok(()), Err(Error::<Runtime>::OriginNotForumLead.into())];
+
+    for index in 0..origins.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+        with_test_externalities(|| {
+            let category_id = create_category_mock(
+                origin,
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_description_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                good_category_description_new(),
+                results[index],
+            );
+        });
+    }
+}
+
+#[test]
+// test case for new setting actually not update category description
+fn update_category_description_no_change() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin,
+            PrivilegedActor::Lead,
+            category_id,
+            good_category_description(),
+            Err(Error::<Runtime>::CategoryNotBeingUpdated.into()),
+        );
+    });
+}
+
+#[test]
+// test case for editing nonexistent category
+fn update_category_description_does_not_exist() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            1,
+            good_category_description_new(),
+            Ok(()),
+        );
+        update_category_description_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            2,
+            good_category_description_new(),
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+        );
+    });
+}
+
+#[test]
+// test if moderator can update category description
+fn update_category_description_moderator() {
+    let moderators = [FORUM_MODERATOR_ORIGIN_ID];
+    let origins = [FORUM_MODERATOR_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+
+        // unprivileged moderator will fail to update category
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_description_new(),
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
+        );
+
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
+
+        // moderator associated with category will succeed
+        update_category_description_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_description_new(),
+            Ok(()),
+        );
+    });
+}
+
+#[test]
+// test if category updator is forum lead
+fn update_category_title_origin() {
+    let origins = [FORUM_LEAD_ORIGIN, NOT_FORUM_LEAD_ORIGIN];
+    let results = vec![Ok(()), Err(Error::<Runtime>::OriginNotForumLead.into())];
+
+    for index in 0..origins.len() {
+        let forum_lead = FORUM_LEAD_ORIGIN_ID;
+        let origin = OriginType::Signed(forum_lead);
+        with_test_externalities(|| {
+            let category_id = create_category_mock(
+                origin,
+                None,
+                good_category_title(),
+                good_category_description(),
+                Ok(()),
+            );
+            update_category_title_mock(
+                origins[index].clone(),
+                PrivilegedActor::Lead,
+                category_id,
+                good_category_title_new(),
+                results[index],
+            );
+        });
+    }
+}
+
+#[test]
+// test case for new setting actually not update category title
+fn update_category_title_no_change() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin,
+            PrivilegedActor::Lead,
+            category_id,
+            good_category_title(),
+            Err(Error::<Runtime>::CategoryNotBeingUpdated.into()),
+        );
+    });
+}
+
+#[test]
+// test case for editing nonexistent category
+fn update_category_title_does_not_exist() {
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            1,
+            good_category_title_new(),
+            Ok(()),
+        );
+        update_category_title_mock(
+            origin.clone(),
+            PrivilegedActor::Lead,
+            2,
+            good_category_title_new(),
+            Err(Error::<Runtime>::CategoryDoesNotExist.into()),
+        );
+    });
+}
+
+#[test]
+// test if moderator can update category title
+fn update_category_title_moderator() {
+    let moderators = [FORUM_MODERATOR_ORIGIN_ID];
+    let origins = [FORUM_MODERATOR_ORIGIN];
+
+    let forum_lead = FORUM_LEAD_ORIGIN_ID;
+    let origin = OriginType::Signed(forum_lead);
+    with_test_externalities(|| {
+        let category_id = create_category_mock(
+            origin.clone(),
+            None,
+            good_category_title(),
+            good_category_description(),
+            Ok(()),
+        );
+
+        // unprivileged moderator will fail to update category
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_title_new(),
+            Err(Error::<Runtime>::ModeratorCantUpdateCategory.into()),
+        );
+
+        // give permision to moderate category itself
+        update_category_membership_of_moderator_mock(
+            origin.clone(),
+            moderators[0],
+            category_id,
+            true,
+            Ok(()),
+        );
+
+        // moderator associated with category will succeed
+        update_category_title_mock(
+            origins[0].clone(),
+            PrivilegedActor::Moderator(moderators[0]),
+            category_id,
+            good_category_title_new(),
+            Ok(()),
+        );
+    });
+}
+
 #[test]
 // test category can be deleted
 fn delete_category() {

+ 36 - 0
runtime/src/weights/forum.rs

@@ -183,4 +183,40 @@ impl forum::WeightInfo for WeightInfo {
             .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
             .saturating_add(DbWeight::get().writes(1 as Weight))
     }
+    fn update_category_title_lead(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_title_moderator(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_description_lead(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
+    fn update_category_description_moderator(i: u32, j: u32) -> Weight {
+        (20_591_000 as Weight)
+            .saturating_add((72_609_000 as Weight).saturating_mul(i as Weight))
+            .saturating_add((233_007_000 as Weight).saturating_mul(j as Weight))
+            .saturating_add(DbWeight::get().reads(3 as Weight))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(i as Weight)))
+            .saturating_add(DbWeight::get().reads((1 as Weight).saturating_mul(j as Weight)))
+            .saturating_add(DbWeight::get().writes(1 as Weight))
+    }
 }

+ 1 - 1
tests/integration-tests/src/fixtures/workingGroups/LeaveRoleFixture.ts

@@ -56,7 +56,7 @@ export class LeaveRoleFixture extends BaseWorkingGroupFixture {
       const worker = qWorkers.find((w) => w.runtimeId === workerId.toNumber())
       Utils.assert(worker, 'Query node: Worker not found!')
       Utils.assert(
-        worker.status.__typename === 'WorkerStatusLeft',
+        worker.status.__typename === 'WorkerStatusLeaving',
         `Invalid worker status: ${worker.status.__typename}`
       )
       Utils.assert(worker.status.workerStartedLeavingEvent, 'Query node: Missing workerStartedLeavingEvent relation')

+ 6 - 0
tests/integration-tests/src/graphql/generated/queries.ts

@@ -1005,6 +1005,7 @@ export type WorkerFieldsFragment = {
   membership: { id: string }
   status:
     | { __typename: 'WorkerStatusActive' }
+    | { __typename: 'WorkerStatusLeaving'; workerStartedLeavingEvent?: Types.Maybe<{ id: string }> }
     | {
         __typename: 'WorkerStatusLeft'
         workerStartedLeavingEvent?: Types.Maybe<{ id: string }>
@@ -2457,6 +2458,11 @@ export const WorkerFields = gql`
     stakeAccount
     status {
       __typename
+      ... on WorkerStatusLeaving {
+        workerStartedLeavingEvent {
+          id
+        }
+      }
       ... on WorkerStatusLeft {
         workerStartedLeavingEvent {
           id

+ 7 - 2
tests/integration-tests/src/graphql/generated/schema.ts

@@ -12317,7 +12317,7 @@ export type WorkerStartedLeavingEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
-export type WorkerStatus = WorkerStatusActive | WorkerStatusLeft | WorkerStatusTerminated
+export type WorkerStatus = WorkerStatusActive | WorkerStatusLeaving | WorkerStatusLeft | WorkerStatusTerminated
 
 export type WorkerStatusActive = {
   phantom?: Maybe<Scalars['Int']>
@@ -12370,10 +12370,15 @@ export type WorkerStatusActiveWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export type WorkerStatusLeaving = {
+  /** Related event emitted on leaving initialization */
+  workerStartedLeavingEvent?: Maybe<WorkerStartedLeavingEvent>
+}
+
 export type WorkerStatusLeft = {
   /** Related event emitted on leaving initialization */
   workerStartedLeavingEvent?: Maybe<WorkerStartedLeavingEvent>
-  /** Related event emitted (and set) when the unstaking period is finished */
+  /** Related event emitted once the worker has exited the role (after the unstaking period) */
   workerExitedEvent?: Maybe<WorkerExitedEvent>
 }
 

+ 5 - 0
tests/integration-tests/src/graphql/queries/workingGroups.graphql

@@ -71,6 +71,11 @@ fragment WorkerFields on Worker {
   stakeAccount
   status {
     __typename
+    ... on WorkerStatusLeaving {
+      workerStartedLeavingEvent {
+        id
+      }
+    }
     ... on WorkerStatusLeft {
       workerStartedLeavingEvent {
         id

+ 45 - 17
yarn.lock

@@ -6905,6 +6905,14 @@ anymatch@^3.0.3, anymatch@~3.1.1:
     normalize-path "^3.0.0"
     picomatch "^2.0.4"
 
+anymatch@~3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
+  integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
+  dependencies:
+    normalize-path "^3.0.0"
+    picomatch "^2.0.4"
+
 apisauce@^1.0.1:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/apisauce/-/apisauce-1.1.2.tgz#1778156802ea8cb07e27ad4ec87cd0c9990a51d1"
@@ -7548,9 +7556,9 @@ aws-credstash@^3.0.0:
     debug "^4.3.1"
 
 aws-sdk@^2.567.0:
-  version "2.925.0"
-  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.925.0.tgz#a352f3adc8274120fb31fa4fc04527d44b6107cf"
-  integrity sha512-dHXngzuSTZvIFWizWbU+ceZTAKmgodzEIi/AWbB+z0THKfxD3Iz/CJ7kZ7a9QyZv7X082L68vv3siMhXBMoTLA==
+  version "2.936.0"
+  resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.936.0.tgz#b69f5db7c0c745f260b014415a8fbfb38c5e615d"
+  integrity sha512-X0kuyycck0fEPN5V0Vw1PmPIQ4BO0qupsL1El5jnXzXxNkf1cOmn5PMSxPXPsdcqua4w4h3sf143/yME0V9w8g==
   dependencies:
     buffer "4.9.2"
     events "1.1.1"
@@ -9124,7 +9132,7 @@ check-type@^0.4.11:
   dependencies:
     underscore "1.6.0"
 
-chokidar@3.5.1, chokidar@^3.4.3, chokidar@^3.5.1:
+chokidar@3.5.1, chokidar@^3.4.3:
   version "3.5.1"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
   integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
@@ -9173,6 +9181,21 @@ chokidar@^3.4.0, chokidar@^3.4.1:
   optionalDependencies:
     fsevents "~2.1.2"
 
+chokidar@^3.5.1:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+  integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
+  dependencies:
+    anymatch "~3.1.2"
+    braces "~3.0.2"
+    glob-parent "~5.1.2"
+    is-binary-path "~2.1.0"
+    is-glob "~4.0.1"
+    normalize-path "~3.0.0"
+    readdirp "~3.6.0"
+  optionalDependencies:
+    fsevents "~2.3.2"
+
 chownr@^1.0.1, chownr@^1.1.1, chownr@^1.1.2:
   version "1.1.4"
   resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b"
@@ -10778,14 +10801,6 @@ dateformat@^3.0.0, dateformat@^3.0.2:
   resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
   integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
 
-dateformat@~1.0.4-1.2.3:
-  version "1.0.12"
-  resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9"
-  integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=
-  dependencies:
-    get-stdin "^4.0.1"
-    meow "^3.3.0"
-
 datejs@^1.0.0-rc3:
   version "1.0.0-rc3"
   resolved "https://registry.yarnpkg.com/datejs/-/datejs-1.0.0-rc3.tgz#bffa1efedefeb41fdd8a242af55afa01fb58de57"
@@ -13757,7 +13772,7 @@ fsevents@~2.1.2:
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e"
   integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==
 
-fsevents@~2.3.1:
+fsevents@~2.3.1, fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -14093,6 +14108,13 @@ glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0:
   dependencies:
     is-glob "^4.0.1"
 
+glob-parent@~5.1.2:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
+  integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+  dependencies:
+    is-glob "^4.0.1"
+
 glob-stream@^6.1.0:
   version "6.1.0"
   resolved "https://registry.yarnpkg.com/glob-stream/-/glob-stream-6.1.0.tgz#7045c99413b3eb94888d83ab46d0b404cc7bdde4"
@@ -24069,6 +24091,13 @@ readdirp@~3.5.0:
   dependencies:
     picomatch "^2.2.1"
 
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+  dependencies:
+    picomatch "^2.2.1"
+
 realpath-native@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c"
@@ -27347,12 +27376,11 @@ ts-log@^2.1.4, ts-log@^2.2.3:
   integrity sha512-XvB+OdKSJ708Dmf9ore4Uf/q62AYDTzFcAdxc8KNML1mmAWywRFVt/dn1KYJH8Agt5UJNujfM3znU5PxgAzA2w==
 
 ts-node-dev@^1.0.0-pre.60:
-  version "1.1.6"
-  resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.6.tgz#ee2113718cb5a92c1c8f4229123ad6afbeba01f8"
-  integrity sha512-RTUi7mHMNQospArGz07KiraQcdgUVNXKsgO2HAi7FoiyPMdTDqdniB6K1dqyaIxT7c9v/VpSbfBZPS6uVpaFLQ==
+  version "1.1.7"
+  resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.1.7.tgz#f157c25235e86a9e8ce3470b1ec04b89bacd90ff"
+  integrity sha512-/YvByJdIw/p88RXmaRB3Kkk+PiUP7g/EAbBvQjDIG+kkm0CMvhdHSB21yEiws22Uls4uFAfCiuEZM4929yjWjg==
   dependencies:
     chokidar "^3.5.1"
-    dateformat "~1.0.4-1.2.3"
     dynamic-dedupe "^0.3.0"
     minimist "^1.2.5"
     mkdirp "^1.0.4"