diff --git a/client/src/components/comments/Comment.js b/client/src/components/comments/Comment.js index ff2a58e0..bcf1d823 100644 --- a/client/src/components/comments/Comment.js +++ b/client/src/components/comments/Comment.js @@ -171,7 +171,7 @@ const Comment = ({ comment, isDraft, callback }) => { const generateDropdownOptions = () => { if (userRole) { let deleteOption = - userRole.delete.postComment && + userRole.delete.comment && (comment.postedBy._id == user._id || comment.postedBy._id == user.anonymousId) ? { @@ -180,7 +180,7 @@ const Comment = ({ comment, isDraft, callback }) => { } : null; let editOption = - userRole.edit.postComment && + userRole.edit.comment && (comment.postedBy._id == user._id || comment.postedBy._id == user.anonymousId) ? { onClick: handleEdit, label: "Edit Comment" } @@ -277,21 +277,25 @@ const Comment = ({ comment, isDraft, callback }) => { ) : ( <> - - - { - toggleReply(true); - }} - > - Reply - + {userRole.participation.reactions && ( + + )} + + {userRole.publish.reply && ( + { + toggleReply(true); + }} + > + Reply + + )} )} diff --git a/client/src/components/comments/CommentReply.js b/client/src/components/comments/CommentReply.js index cb68acd0..5e07cf87 100644 --- a/client/src/components/comments/CommentReply.js +++ b/client/src/components/comments/CommentReply.js @@ -232,12 +232,14 @@ const CommentReply = ({ ) : ( <> - + {userRole.participation.reactions && ( + + )} )} diff --git a/client/src/components/comments/CommentView.js b/client/src/components/comments/CommentView.js index 4c27b188..ca679455 100644 --- a/client/src/components/comments/CommentView.js +++ b/client/src/components/comments/CommentView.js @@ -58,7 +58,7 @@ const CommentView = ({ classroomName }) => { // const setUserRole = useContext(UserRoleDispatchContext); const userRole = useContext(UserRoleContext); - // console.log("Comment View Role Object: ", userRole); + console.log("Comment View Role Object: ", userRole); const handleVote = (voteAnswer) => { var pa = pollAns; @@ -233,13 +233,13 @@ const CommentView = ({ classroomName }) => { > - {postExists && ( + {postExists && userRole.publish.comment && ( )} - {postid === "newQorA" && } + {postid === "newQorA" && } {/* {postid === "newPoll" && } */} {postid === "newPoll" && } {postExists && diff --git a/client/src/components/configPage/ConfigPanel.js b/client/src/components/configPage/ConfigPanel.js index 8becbe86..a4c451b6 100644 --- a/client/src/components/configPage/ConfigPanel.js +++ b/client/src/components/configPage/ConfigPanel.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useContext } from "react"; import PropTypes from "prop-types"; import styled from "styled-components"; import Button from "../common/Button"; @@ -7,6 +7,7 @@ import RolePanel from "./roleConfigComponents/RolePanel"; import UserPanel from "./userConfigComponents/UserPanel"; import LazyFetch from "../common/requests/LazyFetch"; import LoadingDots from "../common/animation/LoadingDots"; +import { UserRoleContext } from "../context/UserRoleProvider"; const colorTest = [ "#dd0000", @@ -48,14 +49,20 @@ const createRoleObject = (itemId) => { // roleColor: colorTest[Math.floor(Math.random() * colorTest.length)], permissions: { publish: { - postComment: false, - reply: false, + question: false, + announcement: false, poll: false, + general: false, + comment: false, + reply: false, }, delete: { - postComment: false, - reply: false, + question: false, + announcement: false, poll: false, + general: false, + comment: false, + reply: false, }, participation: { reactions: false, @@ -63,9 +70,12 @@ const createRoleObject = (itemId) => { pin: false, }, edit: { - postComment: false, - reply: false, + question: false, + announcement: false, poll: false, + general: false, + comment: false, + reply: false, }, privacy: { private: false, @@ -74,7 +84,6 @@ const createRoleObject = (itemId) => { admin: { banUsers: false, removeUsers: false, - announce: false, configure: false, highlightName: false, }, @@ -106,7 +115,13 @@ const GenerateRoleList = (roles, setRoles, userList, setUserList) => { /** * Generates a list of User Components for State Management */ -const GenerateUserList = (users, roles) => { +const GenerateUserList = ( + users, + roles, + displayDropdown, + displayBan, + displayRemove +) => { var userRole = { name: "null", roleColor: "#e7e7e7" }; return users.map((user, index) => { @@ -123,12 +138,15 @@ const GenerateUserList = (users, roles) => { userRole={userRole} allRoles={roles} unbanList={false} + displayDropdown={displayDropdown} + displayBan={displayBan} + displayRemove={displayRemove} /> ); }); }; -const GenerateBannedUserList = (blacklist) => { +const GenerateBannedUserList = (blacklist, displayBan) => { if (!blacklist) { return <>; } @@ -141,6 +159,7 @@ const GenerateBannedUserList = (blacklist) => { userName={bannedUser.userName} userImg={bannedUser.userImg} unbanList={true} + displayBan={displayBan} /> ); }); @@ -155,6 +174,8 @@ const ConfigPanel = ({ setRoleIdCounter, ...props }) => { + console.log("Course Roles: ", courseRoles); + const userRole = useContext(UserRoleContext); const [loadingIcons, setLoadingIcons] = useState(true); const [bannedUserList, setBannedUserList] = useState(null); @@ -171,7 +192,9 @@ const ConfigPanel = ({ setDisplayBanned(true); changeNumBanned(data.success.length); } - setBannedUserList(GenerateBannedUserList(data.success)); + setBannedUserList( + GenerateBannedUserList(data.success, userRole.admin.banUsers) + ); }, onFailure: () => { console.log( @@ -186,9 +209,11 @@ const ConfigPanel = ({ let realUserList = GenerateUserList( courseUsers, courseRoles, - numBannedUsers, - changeNumBanned + userRole.admin.configure, + userRole.admin.banUsers, + userRole.admin.removeUsers ); + console.log("realUserList:", realUserList); const [userList, setUserList] = useState(realUserList); // State for roles ------------------------------------------------------ @@ -208,83 +233,90 @@ const ConfigPanel = ({ return ( - - {loadingIcons ? ( -
- -
- ) : ( - realRoleList - )} - -
- - + + )} + {userRole.admin.configure && ( + + + + )} - // alert("Feature is work in progress."); - LazyFetch({ - type: "put", - endpoint: "/courses/" + courseId + "/roles", - data: { - roles: testList, - }, - onSuccess: (data) => { - console.log("Success PUT Roles: ", data); - alert("Changes saved successfully."); - }, - onFailure: (err) => { - console.log("Failed PUT Roles. ", err?.response); - alert("Error: Changes not saved. Please try again."); - }, - }); - }} - > - Confirm - - diff --git a/client/src/components/configPage/ConfigView.js b/client/src/components/configPage/ConfigView.js index 425b0960..e387fa48 100644 --- a/client/src/components/configPage/ConfigView.js +++ b/client/src/components/configPage/ConfigView.js @@ -150,7 +150,13 @@ const ConfigView = ({ props }) => { // ------------------------------------------------------------ var userIsAdmin = false; - if (userRole) userIsAdmin = userRole.admin.configure; + var userCanBan = false; + var userCanRemove = false; + if (userRole) { + userIsAdmin = userRole.admin.configure; + userCanBan = userRole.admin.banUsers; + userCanRemove = userRole.admin.removeUsers; + } /** * Redirects the user to the landing page @@ -172,7 +178,7 @@ const ConfigView = ({ props }) => { {/* */} - {userIsAdmin ? ( + {userIsAdmin || userCanBan || userCanRemove ? (

{

)} - {userIsAdmin ? ( + {userIsAdmin || userCanBan || userCanRemove ? ( - - - - + {displayDraftPost && ( + + + + )} + {displayDraftPoll && ( + + + + )} {/* The Config page conditionally renders based on whether or not the user has ADMIN priviledges for this course */} - {userIsAdmin && ( + {(userIsAdmin || userCanBan || userCanRemove) && ( { var content; if ( post.content.type === "question" || - post.content.type === "announcement" + post.content.type === "announcement" || + post.content.type === "general" ) { content = ( @@ -120,6 +121,9 @@ const PostView = ({ userRole, highlightedSection }) => { case "Polls": endpoint += "?filterby=poll"; break; + case "General Posts": + endpoint += "?filterby=general"; + break; default: // Don't add a filter to endpoint } diff --git a/client/src/components/posts/Sidebar.js b/client/src/components/posts/Sidebar.js index 7f56ecb7..6845e876 100644 --- a/client/src/components/posts/Sidebar.js +++ b/client/src/components/posts/Sidebar.js @@ -10,6 +10,7 @@ import HeartImg from "../../imgs/heart.svg"; import AnnouncementsImg from "../../imgs/announcements.svg"; import QuestionsImg from "../../imgs/questions.svg"; import PollsImg from "../../imgs/polls.svg"; +import GeneralImg from "../../imgs/general.svg"; /* Sidebar view shows tabs of different post feeds and shows which one is selected */ const Sidebar = ({ userRole, setHighlightedSection, highlightedSection }) => { @@ -64,6 +65,12 @@ const Sidebar = ({ userRole, setHighlightedSection, highlightedSection }) => { setHighlightedSection={setHighlightedSection} highlightedSection={highlightedSection} /> + { return "#4a86fa"; case "Announcement": return "#FA6A4A"; + case "General": + return "#EDEDED"; default: - return "#4a86fa"; + return "#E7E7E7"; } }; -const Draft = () => { +const Draft = ({ userRole }) => { const { courseId } = useParams(); const history = useHistory(); // State and handler for drafting posts @@ -31,8 +33,19 @@ const Draft = () => { isPrivate: false, }); + var defaultType; + if (userRole.publish.general) { + defaultType = "General"; + } else if (userRole.publish.question) { + defaultType = "Question"; + } else if (userRole.publish.announcement) { + defaultType = "Announcement"; + } else { + // This should never happen + defaultType = "Unknown"; + } const [content, setContent] = useState({ - type: "Question", + type: defaultType, raw: EditorState.createEmpty(), plainText: EditorState.createEmpty(), }); @@ -78,39 +91,71 @@ const Draft = () => { }); }; - var titlePlaceholder = content.type ? content.type + " title" : "Post title"; + // Styling Variables + var titlePlaceholder = + content.type != "Unknown" ? content.type + " title" : "Post title"; + var displayQuestion; + var displayAnnouncement; + var displayGeneral; + if (userRole) { + displayQuestion = userRole.publish.question; + displayAnnouncement = userRole.publish.announcement; + displayGeneral = userRole.publish.general; + } + var accent = accentColor(content.type); + return ( - + - - - + )} + {displayQuestion && ( + + )} + {displayAnnouncement && ( + + + Announcement + + + )} (props.selected ? "#fff" : "#000")}; + color: ${(props) => + props.selected ? (props.isGeneral ? "#162B55" : "#ededed") : "#162B55"}; background-color: ${(props) => - props.selected ? props.accentColor : "#e7e7e7"}; + props.selected + ? props.isGeneral + ? "#e7e7e7" + : props.accentColor + : "#e7e7e7"}; border-radius: 2px; `; diff --git a/client/src/components/posts/refactorComponents/PollWrapper.js b/client/src/components/posts/refactorComponents/PollWrapper.js index 520e9a7e..b82bc03d 100644 --- a/client/src/components/posts/refactorComponents/PollWrapper.js +++ b/client/src/components/posts/refactorComponents/PollWrapper.js @@ -1,10 +1,12 @@ -import React, { useState } from "react"; +import React, { useContext, useState } from "react"; import Poll from "react-polls"; import { useParams } from "react-router"; import LazyFetch from "../../common/requests/LazyFetch"; +import { UserRoleContext } from "../../context/UserRoleProvider"; const PollWrapper = ({ post }) => { const { courseId } = useParams(); + const userRole = useContext(UserRoleContext); // console.log("pollWrapper post: ", post); const establishPollAns = (post) => { @@ -25,18 +27,21 @@ const PollWrapper = ({ post }) => { return answer; }); - LazyFetch({ - type: "put", - endpoint: "/courses/" + courseId + "/polls", - data: { selectedOption: voteAnswer, postId: post._id }, - onSuccess: (data) => { - // console.log("Data: ", data); - setPollAns(newPollAnswers); - }, - onFailure: (err) => { - console.log("Err: ", err); - }, - }); + // Only send the request to the backend if the user has permission to do so + if (userRole.participation.voteInPoll) { + LazyFetch({ + type: "put", + endpoint: "/courses/" + courseId + "/polls", + data: { selectedOption: voteAnswer, postId: post._id }, + onSuccess: (data) => { + // console.log("Data: ", data); + setPollAns(newPollAnswers); + }, + onFailure: (err) => { + console.log("Err: ", err); + }, + }); + } }; // console.log("pollAns: ", pollAns); @@ -46,7 +51,11 @@ const PollWrapper = ({ post }) => { question={post.title} answers={pollAns} onVote={handleVote} - vote={post.content.vote} + vote={ + userRole && userRole.participation.voteInPoll + ? post.content.vote + : pollAns[0].option + } noStorage onClick={(event) => { event.stopPropagation(); diff --git a/client/src/components/posts/refactorComponents/PostWrapper.js b/client/src/components/posts/refactorComponents/PostWrapper.js index 4485bbea..0017a1df 100644 --- a/client/src/components/posts/refactorComponents/PostWrapper.js +++ b/client/src/components/posts/refactorComponents/PostWrapper.js @@ -22,6 +22,8 @@ const accentColor = (type) => { return "#FA6A4A"; case "Poll": return "#4CAF50"; + case "General": + return "#ededed"; default: return "#4a86fa"; } @@ -96,21 +98,76 @@ const PostWrapper = ({ setIsEditing(true); }; + const generateEditDeleteOption = (optionType) => { + var resultingOption; + var role = optionType == "delete" ? userRole.delete : userRole.edit; + if ( + postType == "Question" && + role.question && + (postObject.postedBy._id == user._id || + postObject.postedBy._id == user.anonymousId) + ) { + resultingOption = { + onClick: () => { + optionType == "delete" + ? handleDelete(postObject._id, postObject.courseId) + : handleEdit(); + }, + label: optionType == "delete" ? "Delete question" : "Edit question", + }; + } else if ( + postType == "Announcement" && + role.announcement && + (postObject.postedBy._id == user._id || + postObject.postedBy._id == user.anonymousId) + ) { + resultingOption = { + onClick: () => { + optionType == "delete" + ? handleDelete(postObject._id, postObject.courseId) + : handleEdit(); + }, + label: + optionType == "delete" ? "Delete announcement" : "Edit announcement", + }; + } else if ( + postType == "Poll" && + role.poll && + (postObject.postedBy._id == user._id || + postObject.postedBy._id == user.anonymousId) + ) { + resultingOption = { + onClick: () => { + optionType == "delete" + ? handleDelete(postObject._id, postObject.courseId) + : handleEdit(); + }, + label: optionType == "delete" ? "Delete poll" : "Edit poll", + }; + } else if ( + postType == "General" && + role.general && + (postObject.postedBy._id == user._id || + postObject.postedBy._id == user.anonymousId) + ) { + resultingOption = { + onClick: () => { + optionType == "delete" + ? handleDelete(postObject._id, postObject.courseId) + : handleEdit(); + }, + label: optionType == "delete" ? "Delete post" : "Edit post", + }; + } else { + resultingOption = null; + } + return resultingOption; + }; + const generateDropdownOptions = () => { if (userRole) { - let deleteOption = - userRole.delete.postComment && postObject.postedBy._id == user._id - ? { - onClick: () => { - handleDelete(postObject._id, postObject.courseId); - }, - label: "Delete post", - } - : null; - let editOption = - userRole.edit.postComment && postObject.postedBy._id == user._id - ? { onClick: handleEdit, label: "Edit post" } - : null; + let deleteOption = generateEditDeleteOption("delete"); + let editOption = generateEditDeleteOption("edit"); let pinOption = userRole.participation.pin ? { onClick: () => { @@ -159,9 +216,13 @@ const PostWrapper = ({ isRead={postObject.read} accentColor={accentColor(postType)} /> - - {postType ? postType : "Question"} - + {postType != "General" ? ( + + {postType ? postType : "Question"} + + ) : ( + <> + )} {postObject.title ? postObject.title : "Error getting post title"} @@ -194,7 +255,9 @@ const PostWrapper = ({ }} postType={postType} > - {postType == "Question" || postType == "Announcement" + {postType == "Question" || + postType == "Announcement" || + postType == "General" ? React.cloneElement(content, { edit: { isEditing, setIsEditing } }) : content} @@ -212,12 +275,15 @@ const PostWrapper = ({ Posted by {postObject.postedBy.first} {postObject.postedBy.last} - + {userRole && userRole.participation.reactions && ( + + )} + + + + + + + diff --git a/server/README-server.md b/server/README-server.md index 7e67c4bf..14c340df 100644 --- a/server/README-server.md +++ b/server/README-server.md @@ -244,7 +244,7 @@ This component handles setting up all of our MongoDB models so that each collect ] ``` -### Roles Model (NEW 04.18.21) +### Roles Model (OLD) ```json { @@ -286,6 +286,56 @@ This component handles setting up all of our MongoDB models so that each collect } ``` +### Roles Model (NEW 07.15.21) + +```json +{ + "_id": "role id goes here", + "name": "the name of the role goes here", + "permissions": { + "publish": { + "question": true, + "announcement": true, + "poll": true, + "general": true, + "comment": true, + "reply": true + }, + "delete": { + "question": true, + "announcement": true, + "poll": true, + "general": true, + "comment": true, + "reply": true + }, + "participation": { + "reactions": true, + "voteInPoll": true, + "pin": true + }, + "edit": { + "question": true, + "announcement": true, + "poll": true, + "general": true, + "comment": true, + "reply": true + }, + "privacy": { + "private": true, + "anonymous": true + }, + "admin": { + "banUsers": true, + "removeUsers": true, + "configure": true, + "highlightName": true + } + } +} +``` + ### Deprecated Models #### Old User Model diff --git a/server/inquire/auth.py b/server/inquire/auth.py index f2a4e4b7..01b68399 100644 --- a/server/inquire/auth.py +++ b/server/inquire/auth.py @@ -63,7 +63,6 @@ def get_current_user(): if role: user.permissions = role.permissions g.current_user = user - return g.current_user @@ -90,11 +89,12 @@ def _deep_access(d, keys): return None return d + def _permission_comparison(user_permissions: dict, required_permissions: list): """ Checks if the values in the user_permissions dict are true for the keys specified in the required_permissions list. For nested values specify keys in the following format "level1key-level2key-level3key". - + Required_permissions can contain a subset of permissions to check. Example: _permission_comparison({"a":True, b:False, c:{d:True, e:True}}, ["a", "c-e"]) == True @@ -133,9 +133,11 @@ def wrapper(*args, **kwargs): if not current_user.permissions: missing_permissions = required_permissions else: - missing_permissions = _permission_comparison(current_user.permissions, required_permissions) + missing_permissions = _permission_comparison( + current_user.permissions, required_permissions) if missing_permissions: - abort(401, errors=[f'Resource access restricted: missing course permission(s) {", ".join(missing_permissions)}']) + abort(401, errors=[ + f'Resource access restricted: missing course permission(s) {", ".join(missing_permissions)}']) return func(*args, **kwargs) return wrapper return actual_decorator @@ -227,6 +229,7 @@ def retrieve_user(_id): else: return None + def retrieve_role(_id): '''Retrieves the role object from the database''' query = Role.objects.raw({'_id': _id}) @@ -240,7 +243,6 @@ def retrieve_role(_id): return None - def create_user(data, mode="google"): if mode == "google": user = User(_id=data['sub'], first=data['given_name'], last=data['family_name'], diff --git a/server/inquire/mongo.py b/server/inquire/mongo.py index 18b24949..835b927e 100644 --- a/server/inquire/mongo.py +++ b/server/inquire/mongo.py @@ -31,6 +31,8 @@ def post_content_validator(content): return True elif post_type == "announcement": return True + elif post_type == "general": + return True else: raise ValidationError("Post type not recognized") diff --git a/server/inquire/resources/ban_remove.py b/server/inquire/resources/ban_remove.py index d8ef48b7..a67de20c 100644 --- a/server/inquire/resources/ban_remove.py +++ b/server/inquire/resources/ban_remove.py @@ -9,13 +9,14 @@ class BanRemove(Resource): - @permission_layer(required_permissions=["admin-configure"]) def put(self, courseId): + if not current_user.permissions['admin']['banUsers'] and not current_user.permissions['admin']['removeUsers']: + return {"errors": ["You do not have permission to ban or remove users from the course."]}, 401 + parser = reqparse.RequestParser() parser.add_argument('type') parser.add_argument('userId') args = parser.parse_args() - print("userId:", args['userId']) # Validate course exists and store it in course variable try: @@ -42,10 +43,14 @@ def put(self, courseId): filler = "" if args['type'] == "remove": + if not current_user.permissions['admin']['removeUsers']: + return {"errors": ["You do not have permission to remove users in this course"]}, 401 # Removing students self.remove_user(user, course) filler = "removed" elif args['type'] == "ban": + if not current_user.permissions['admin']['banUsers']: + return {"errors": ["You do not have permission to ban users in this course"]}, 401 # Banning students if args['userId'] not in course.blacklist: # Add the banned user to the blacklist diff --git a/server/inquire/resources/comments.py b/server/inquire/resources/comments.py index b73a0655..0a9b1576 100644 --- a/server/inquire/resources/comments.py +++ b/server/inquire/resources/comments.py @@ -53,7 +53,7 @@ def get(self, courseId=None, postId=None): return [self.serialize(comment) for comment in Comment.objects.raw({'postId': postId})] - @permission_layer(required_permissions=["publish-postComment"]) + @permission_layer(required_permissions=["publish-comment"]) def post(self, courseId=None, postId=None): """ Creates a new comment @@ -99,6 +99,7 @@ def post(self, courseId=None, postId=None): # Adding user info to dict anonymous = args['isAnonymous'] if anonymous: + highlighted = False postedBy = {"first": "Anonymous", "last": "", "_id": current_user.anonymousId, "anonymous": anonymous} else: @@ -121,7 +122,7 @@ def post(self, courseId=None, postId=None): current_app.socketio.emit('Comment/create', result, room=postId) return result, 200 - @permission_layer(required_permissions=["edit-postComment"]) + @permission_layer(required_permissions=["edit-comment"]) def put(self, courseId=None, postId=None): """ Updates a comment @@ -181,7 +182,7 @@ def put(self, courseId=None, postId=None): result = self.serialize(comment) return result, 200 - @permission_layer(required_permissions=["delete-postComment"]) + @permission_layer(required_permissions=["delete-comment"]) def delete(self, courseId=None, postId=None): """ Deletes a comment diff --git a/server/inquire/resources/posts.py b/server/inquire/resources/posts.py index 1cfdde99..b492bb2a 100644 --- a/server/inquire/resources/posts.py +++ b/server/inquire/resources/posts.py @@ -19,7 +19,7 @@ class Posts(Resource): - @permission_layer(required_permissions=["publish-postComment"], require_joined_course=True) + @permission_layer(require_joined_course=True) def post(self, courseId=None): """ Creates a new post @@ -51,6 +51,11 @@ def post(self, courseId=None): schema: $ref: '#/definitions/403Response' """ + # Make sure the current user at least has one permission to post + # print("User Perms: ", current_user.permissions) + if not current_user.permissions['publish']['question'] and not current_user.permissions['publish']['announcement'] and not current_user.permissions['publish']['poll'] and not current_user.permissions['publish']['general']: + return {"errors": ["You do not have permission to make any post in this course."]}, 401 + # Parse arguments parser = reqparse.RequestParser() parser.add_argument('title') @@ -59,6 +64,11 @@ def post(self, courseId=None): parser.add_argument('isAnonymous', type=bool) args = parser.parse_args() + # Check that the user has permission to make the type of post they wish to make + permission_errs = self.check_permissions("publish", args) + if (bool(permission_errs)): + return {"errors": permission_errs}, 401 + # How polls look being sent from frontend # { # "type": "blah", @@ -76,7 +86,9 @@ def post(self, courseId=None): # Adding user info to dict anonymous = args['isAnonymous'] + if anonymous: + highlighted = False postedBy = {"first": "Anonymous", "last": "", "_id": current_user.anonymousId, "anonymous": anonymous} else: @@ -217,6 +229,16 @@ def get(self, courseId=None): query = Post.objects.raw(queryParams).order_by( [("isPinned", -1), ("createdDate", sort_date)]) + # Filter by 'general' + elif filterby == 'general': + # Check if the user can see private posts + if not user_perms["privacy"]["private"]: + queryParams['$or'] = [{'isPrivate': False}, {'postedBy._id': { + '$in': [current_user._id, current_user.anonymousId]}}] + queryParams["content.type"] = "general" + query = Post.objects.raw(queryParams).order_by( + [("isPinned", -1), ("createdDate", sort_date)]) + # If the current user can see private posts and there's no search elif user_perms["privacy"]["private"] and (req is None): query = Post.objects.raw( @@ -250,7 +272,6 @@ def get(self, courseId=None): return result, 200 - @permission_layer(required_permissions=["delete-postComment"]) def delete(self, courseId=None): """ Deletes a post @@ -297,6 +318,10 @@ def delete(self, courseId=None): description: Whether or not the post was deleted example: false """ + # Make sure the current user at least has one permission to delete + if not current_user.permissions['delete']['question'] and not current_user.permissions['delete']['announcement'] and not current_user.permissions['delete']['poll'] and not current_user.permissions['delete']['general']: + return {"errors": ["You do not have permission to delete any type of post in this course."]}, 401 + # Parse arguments parser = reqparse.RequestParser() parser.add_argument('_id') @@ -310,6 +335,11 @@ def delete(self, courseId=None): except Post.MultipleObjectsReturned: return {'deleted': False, 'errors': f"Duplicate post detected, multiple posts in database with id {args['_id']}"}, 400 + # Make sure the user has permission to delete the type of post they want to delete + permission_errs = self.check_permissions("delete", post) + if (bool(permission_errs)): + return {"errors": permission_errs}, 401 + # Permission check # FIX ME: Switch to using something other than "admin-configure" as the admin permission for deleting posts if current_user._id == post.postedBy['_id'] or current_user.anonymousId == post.postedBy['_id'] or current_user.permissions["admin"]["configure"]: @@ -326,7 +356,6 @@ def delete(self, courseId=None): else: return {'deleted': False}, 403 - @permission_layer(required_permissions=["edit-postComment"]) def put(self, courseId=None): """ Edits a post @@ -358,21 +387,27 @@ def put(self, courseId=None): schema: $ref: '#/definitions/403Response' """ + # Make sure the current user at least has one permission to edit + if not current_user.permissions['edit']['question'] and not current_user.permissions['edit']['announcement'] and not current_user.permissions['edit']['poll'] and not current_user.permissions['edit']['general']: + return {"errors": ["You do not have permission to edit any type of post in this course."]}, 401 + # Parse the request parser = reqparse.RequestParser() parser.add_argument('title') parser.add_argument('content', type=dict) parser.add_argument('_id') args = parser.parse_args() - print(args.content) + + # Make sure the user has permission to edit the type of post they wish to edit + permission_errs = self.check_permissions("edit", args) + if (bool(permission_errs)): + return {"errors": permission_errs}, 401 # Validate the args errors = self.validate_post(args) if(bool(errors)): return {"errors": errors}, 400 - # print("AFTER VALIDATION") - # Get the post you want to update try: post = Post.objects.get({'_id': args['_id']}) @@ -381,22 +416,17 @@ def put(self, courseId=None): except Post.MultipleObjectsReturned: return {'updated': False, 'errors': f"Duplicate post detected, multiple posts in database with id {args['_id']}"}, 400 - # print("AFTER DUPLICATE POST DETECTION TEST") - new_content_type = args["content"]["type"] if new_content_type != post.content["type"]: return {'updated': False, 'errors': f"Cannot change post type"}, 400 # if post.content["type"] == "poll": # return {'updated': False, 'errors': f"Cannot modify polls"}, 400 - # print("AFTER CHANGE / MODIFY TEST") - id_match = current_user._id == post.postedBy[ '_id'] or current_user.anonymousId == post.postedBy['_id'] if not id_match: return {'updated': False, 'errors': f"Cannot modify other users posts"}, 400 - # print("AFTER Other users test") if post.content["type"] != "poll": if post.title != args['title']: post.title = args['title'] @@ -409,6 +439,23 @@ def put(self, courseId=None): result = self.serialize(post) return result, 200 + def check_permissions(self, request_type, post_args): + # Check that the user has permission to do what they wish to do + errors = [] + if (not current_user.permissions[request_type]['question']) and (post_args.content.get("type") == "question"): + errors.append( + f"You do not have permission to {request_type} questions in this course") + elif not current_user.permissions[request_type]['announcement'] and (post_args.content.get("type") == "announcement"): + errors.append( + f"You do not have permission to {request_type} announcements in this course") + elif not current_user.permissions[request_type]['poll'] and (post_args.content.get("type") == "poll"): + errors.append( + f"You do not have permission to {request_type} polls in this course") + elif not current_user.permissions[request_type]['general'] and (post_args.content.get("type") == "general"): + errors.append( + f"You do not have permission to {request_type} general posts in this course") + return errors + def validate_post(self, args): errors = [] # Make sure title is provided @@ -423,11 +470,11 @@ def validate_post(self, args): errors.append("Please provide a type for the post") return errors # Validate the type. Types include question, announcement, and poll. - if not (args.content["type"] == "question" or args.content["type"] == "announcement" or args.content["type"] == "poll"): + if not (args.content["type"] == "question" or args.content["type"] == "announcement" or args.content["type"] == "poll" or args.content["type"] == "general"): errors.append( - "Invalid type provided. Valid types are: question, announcement, or poll.") + "Invalid type provided. Valid types are: question, announcement, poll, or general.") # Make sure text field is provided for questions and announcements - if (args.content["type"] == "question" or args.content["type"] == "announcement"): + if (args.content["type"] == "question" or args.content["type"] == "announcement" or args.content['type'] == "general"): raw = args.content.get("raw") plaintext = args.content.get("plainText") if not (raw and plaintext and type(raw) == dict and type(plaintext) == str): diff --git a/server/inquire/resources/replies.py b/server/inquire/resources/replies.py index a426aa60..330ae033 100644 --- a/server/inquire/resources/replies.py +++ b/server/inquire/resources/replies.py @@ -65,6 +65,7 @@ def post(self, courseId=None, postId=None, comment_id=None): # Adding user info to dict anonymous = args['isAnonymous'] if anonymous: + highlighted = False postedBy = {"first": "Anonymous", "last": "", "_id": current_user.anonymousId, "anonymous": anonymous} else: diff --git a/server/inquire/resources/roles.py b/server/inquire/resources/roles.py index 73771240..96659928 100644 --- a/server/inquire/resources/roles.py +++ b/server/inquire/resources/roles.py @@ -9,8 +9,10 @@ class Roles(Resource): - @permission_layer(required_permissions=["admin-configure"]) def get(self, courseId): + if not current_user.permissions['admin']['configure'] and not current_user.permissions['admin']['banUsers'] and not current_user.permissions['admin']['removeUsers']: + return {"errors": ["You do not have permission to view the course configuration page."]}, 401 + try: course = Course.objects.get({"_id": courseId}) except Course.DoesNotExist: @@ -114,14 +116,13 @@ def delete(self, courseId): try: role = Role.objects.get({"_id": roleId}) role.delete() - course.roles.pop(roleId,None) + course.roles.pop(roleId, None) course.save() return {"deleted": True} except Role.DoesNotExist: return {"deleted": False, "error": f"Role with id {str(roleId)} does not exit"} except Exception: return {"deleted": False, "error": "Unspecified error occured"} - def _serialize(self, role): j = role.to_son() diff --git a/server/inquire/roles/admin.py b/server/inquire/roles/admin.py index 87f88df7..7738e0c7 100644 --- a/server/inquire/roles/admin.py +++ b/server/inquire/roles/admin.py @@ -1,13 +1,19 @@ admin = { "publish": { - "postComment": True, - "reply": True, - "poll": True + "question": True, + "announcement": True, + "poll": True, + "general": True, + "comment": True, + "reply": True }, "delete": { - "postComment": True, - "reply": True, - "poll": True + "question": True, + "announcement": True, + "poll": True, + "general": True, + "comment": True, + "reply": True }, "participation": { "reactions": True, @@ -15,18 +21,22 @@ "pin": True }, "edit": { - "postComment": True, - "reply": True, - "poll": True + "question": True, + "announcement": False, + "poll": True, + "general": True, + "comment": True, + "reply": True }, "privacy": { "private": True, + # Change to anonymous posts/anonymous comments/replies "anonymous": True }, "admin": { "banUsers": True, "removeUsers": True, - "announce": True, + # "deleteOther": False, "configure": True, "highlightName": True } diff --git a/server/inquire/roles/student.py b/server/inquire/roles/student.py index a962ad57..2a2b8ea9 100644 --- a/server/inquire/roles/student.py +++ b/server/inquire/roles/student.py @@ -1,13 +1,19 @@ student = { "publish": { - "postComment": True, - "reply": True, - "poll": False + "question": True, + "announcement": True, + "poll": False, + "general": True, + "comment": True, + "reply": True }, "delete": { - "postComment": True, - "reply": True, - "poll": False + "question": True, + "announcement": True, + "poll": False, + "general": True, + "comment": True, + "reply": True }, "participation": { "reactions": True, @@ -15,9 +21,12 @@ "pin": False }, "edit": { - "postComment": True, - "reply": True, - "poll": True + "question": True, + "announcement": True, + "poll": False, + "general": True, + "comment": True, + "reply": True }, "privacy": { "private": False, @@ -26,7 +35,7 @@ "admin": { "banUsers": False, "removeUsers": False, - "announce": False, + # "deleteOther": False, "configure": False, "highlightName": False }