def version(id: str): if not request.cookies.get("userid"): return redirect( url_for( 'index', msg="You are not signed in, please sign in to see this page.")) versionData = file_query( {"versionid": id})[0] # Query the file associated with the passed file version if not versionData: # If the file version cannot be found, or the user doesn't have access return redirect( url_for('dash', msg="Sorry, you do not have access to this file")) userData = getUserData( request.cookies.get("userid") ) # Gets the users Data to check whether they have leader/admin/creator access isLeader = (versionData['versions'][id]['author'] == userData)\ or leaderCheck(versionData["groups"], userData["userid"]) \ or userData["admin"] return render_template("version.html", id=id, version=versionData, isLeader=isLeader)
def dash(): if not request.cookies.get( 'userid' ): # Repeated for every route, checks if the user is signed / has a userid in the coookies return redirect( url_for('index', msg="Please sign in to see your dashboard") ) # Redirects them if it doesn't exist userData = getUserData(request.cookies.get( "userid")) # Gets dictionary of the user's usertable data groupData = getUserGroups( request.cookies.get("userid") ) # Get all the user's groups in order to show them in the dropdown files = file_query( {"archived": False}) # Retrieve all user's files in order to show in file list unread = getUnreadComments( files, request.cookies.get("userid") ) # Gets any unread comments, to display in the notifications pane return render_template("dashboard.html", user=userData, groups=groupData, files=files, unreadComments=unread)
def bulkCommentPage(): if not request.cookies.get("userid"): return redirect( url_for( 'index', msg="You are not signed in, please sign in to see this page.")) files = file_query( {}) # Empty query fetches all the files accessible to a user return render_template("bulkcomment.html", files=files)
def archive(): if not request.cookies.get("userid"): return redirect( url_for( 'index', msg="You are not signed in, please sign in to see this page.")) userData = getUserData(request.cookies.get( "userid")) # Gets user data to check whether it is admin if not userData["admin"]: return redirect( url_for("dash", msg="You don't have permission to see this page")) files = file_query({"archived": True }) # Fetches all files with any archived file versions return render_template("archive.html", files=files)
def getGroupData(groupID): from src.api.files import file_query """ Util function to get serialised group data, along with files accessible to the group - groupid: returns the serialised JSON data from a group, alongside all the files they can access - returns: a response for the client to interpret based on the success of the operation """ groupData = GroupTable.query.filter_by(groupid=groupID).first() return { "groupid": groupID, "members": getGroupUsers(groupID), "files": file_query.file_query( {"groupid": groupID}), # Has to do absolute path of the module here "groupname": groupData.groupname, # due to circular import error "groupleader": getUserData(groupData.groupleaderid), }
def download(id: str, filename: str): versionData = file_query( {"versionid": id} )[0] # Retrieve the file data with the version id using the file_query method backblaze = B2Interface( application_key_id=APPLICATION_KEY_ID, application_key=APPLICATION_KEY, bucket_name=BUCKET_NAME ) # Define an instance of the B2Interface in order to communicate # with the Backblaze bucket fileInfo = backblaze.downloadFileByVersionId( str(versionData["versions"][id] ["versionid"])) # Downloads the file from the bucket return send_file( # Uses Flask's send_file function in order io.BytesIO(bytes( fileInfo["file_body"])), # serve the file in a readable format for attachment_filename=versionData["filename"] # the browser )
def file(id: str): if not request.cookies.get("userid"): return redirect( url_for( 'index', msg="You are not signed in, please sign in to see this page.")) fileData = file_query( {"fileid": id} ) # Gets list of files associated with file id provided in url, although should only be one if not fileData: # Checks if any file is returned from the file query return redirect( url_for('dash', msg="Sorry, you do not have access to this file")) else: fileData = fileData[ 0] # Should only be one, so fetches the first index userData = getUserData(request.cookies.get("userid")) userGroups = getUserGroups( userData["userid"] ) # Gets groups in order for admins to give other group's file access isLeader = leaderCheck(fileData["groups"], userData["userid"]) or userData["admin"] readUnreadComments( id, userData["userid"] ) # Marks any comment in the file that isn't read as read fileComments = getComments( id ) # Fetches the files comments, with all of them read by current user return render_template("file.html", file=fileData, isLeader=isLeader, comments=fileComments, userGroups=userGroups, archive=False)
def getComments(fileid: str, archived: bool = False): fileData = file_query({"fileid": fileid, "archived": archived})[0] groupMembers = [] try: comments = CommentTable.query.filter( CommentTable.fileid == fileid).order_by( CommentTable.date.desc()).all() return list( map( ( lambda x: { "comment": x.comment, "date": x.date, "commentid": str(x.commentid), "file": fileid, "user": getUserData(str(x.userid) ), # User data of the commenter "read": False not in [ commentRead(str(x.commentid), group["groupid"]) for group in fileData["groups"] ] }), comments) ) # Checks if all users from all groups have read the comment except Exception as e: print(print_exc( )) # Prints any exceptions if they occur and returns an empty list return []
def archivedFile(id: str): if not request.cookies.get("userid"): return redirect( url_for( 'index', msg="You are not signed in, please sign in to see this page.")) file = file_query({"fileid": id, "archived": True}) if not file: return redirect( url_for('dash', msg="Sorry, you do not have access to this file")) userGroups = getUserGroups(request.cookies.get("userid")) fileComments = getComments( id, True ) # Fetches the files comments, with all of them read by current user return render_template("file.html", file=file[0], isLeader=True, userGroups=userGroups, comments=fileComments, archive=True)
def generateReportHTML(groupid: str, email: str) -> str: """ Generate the report file in memory and returns the markdown string in html using markdown package - groupid: if not none then create a group report - email: if not none then create a user report - returns: if both params are not none then report on user inside group, otherwise as above """ # Create the StringIO object to store the report file in memory report = io.StringIO() title = "# Report on " + (("group \"" + getGroupData(groupid)['groupname'] + "\"") if groupid is not None and email is None else ("user \"" + getUserDataFromEmail(email)['username'] + "\"") + (" in group \"" + getGroupData(groupid)['groupname'] + "\"" if email is not None and groupid is not None else "")) report.write(title + '\n\n---\n\n') user_list_prefix = "" users_in_group = [] if groupid is not None and email is None: users_in_group = getGroupUsers(groupid) users_heading = "## Users in group\n\n" user_list_prefix = "1. " tabulations = "\t" report.write(users_heading) if email is not None: users_in_group.append(getUserDataFromEmail(email)) user_list_prefix = "## " tabulations = "" # Write user's info to report for user in users_in_group: user_info = user_list_prefix + "Info on user \"" + user['username'] + "\"\n\n" + tabulations + "- User ID: " + str(user['userid']) + "\n" + tabulations + "- Email: " + user['email'] + "\n" + tabulations + "- Last login: "******"\n" + tabulations + "- Admin: " + ("Yes" if user['admin'] else "No") + "\n\n" report.write(user_info) # Write group details if groupid or groupid and email is not None if groupid is not None or (groupid is not None and email is not None): group = getGroupData(groupid) group_info = "## Info on group \"" + group['groupname'] + "\"\n\n- Group ID: " + groupid + "\n- Groupleader: " + getUserData(group['groupleader']['userid'])['username'] + " (ID: " + str(getUserData(group['groupleader']['userid'])['userid']) + ")\n\n" report.write(group_info) # Display all the files belonging to the group if groupid is not none if groupid is not None and email is None: group = getGroupData(groupid) report.write("## Files belonging to \"" + group['groupname'] + "\"\n\n") files_in_group = file_query({'groupid': groupid}) if len(files_in_group) > 0: for file in files_in_group: file_info = "1. Filename: \"" + file['filename'] + "\"\n\t- File ID: " + file['fileid'] + "\n\t- Extension: " + file['extension'] + "\n\t- Comments:\n" report.write(file_info) # Then display a list of all comments relating to the file comments = getComments(file['fileid']) if len(comments) > 0: for comment in comments: comment_info = "\t\t1. \"" + comment['comment'] + "\"\n\t\t\t- By: " + comment['user']['username'] + "\n\t\t\t- Written on: " + str(comment['date']) + "\n\t\t\t- Read: " + ("Yes" if comment['read'] else "No") + "\n" report.write(comment_info) else: report.write("\t\t- No comments for this file\n") report.write("\t- Versions:\n") # Display version info versions = file['versions'] version_info = "" if len(versions) > 0: for version in versions.keys(): version = versions[version] version_info += "\t\t1. \"" + version['title'] + "\" (Hash: " + version['versionhash'] + ")\n\t\t\t- Uploaded on: " + version['uploaded'] + "\n\t\t\t- Author: " + str(version['author']['username']) + "\n" report.write(version_info) else: report.write("\t\t- No versions found\n") report.write("\n") else: report.write("No files for this group\n\n") # Display all file versions that the user with email has created. If groupid is not none then it will only display file versions from that group if email is not None: user = getUserDataFromEmail(email) group = getGroupData(groupid) if groupid is not None else None report.write("## File versions belonging to \"" + user['username'] + "\"" + ((" in group \"" + group['groupname'] + "\"\n\n") if group is not None else "\n\n")) groups_to_find_versions = [] if groupid is None: # Find all the groups that the user is in groups_to_find_versions = [getGroupData(group['groupid']) for group in getUserGroups(user['userid'])] else: # Add only the group that is specified by groupid groups_to_find_versions = [group] versions_user_has_authored = {} # Find all versions inside the group['files'] that the user in question has authored for group in groups_to_find_versions: for file in group['files']: # Create a more succint file dictionary used later small_file = {} small_file['filename'] = file['filename'] small_file['versions'] = [] found_something = False for version in file['versions'].keys(): version = file['versions'][version] if version['author']['email'] == email: found_something = True print('found version', version) small_file['versions'].append(version) # Only add small file if it contains versions related to the user if found_something: versions_user_has_authored[file['fileid']] = small_file # Display version data found if len(versions_user_has_authored) > 0: for fileid in versions_user_has_authored.keys(): file = versions_user_has_authored[fileid] version_info = "1. For \"" + file['filename'] + "\" (File ID: " + fileid + ") they created:\n" for version in file['versions']: version_info += "\t2. \"" + version['title'] + "\"\n\t\t- Hash: " + version['versionhash'] + "\n\t\t- Version ID: " + version['versionid'] + "\n\t\t- Uploaded: " + str(version['uploaded']) + "\n\t\t- Author: " + version['author']['username'] + "\n" report.write(version_info) report.write("\n") else: report.write("User has authored no versions" + (" inside this group\n\n" if groupid is not None else "\n\n")) return markdown.markdown(report.getvalue()).replace("\n", "")
def editMetadata(): """ Endpoint to edit metadata - versionid: the UUID of the version where the metadata is associated with - title: the title of the metadata to amend - value: the value to change the metadata to - userid: the UUID of the user who triggered the metadata edit - returns: a response for the client to interpret based on the success of the operation """ isBrowser = "versionid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect( url_for("index", msg="You must be signed in to complete this action")) else: return json.dumps({ "code": 403, "msg": "You must be signed in to complete this action" }), 403 file = file_query({"versionid": data["versionid"]}) if not file: if isBrowser: return redirect( url_for( "index", msg= "You either do not have access to this file, or it does not exist" )) else: return json.dumps({ "code": 401, "msg": "You either do not have access to this file or it does not exist" }), 401 try: metadata = MetadataTable.query.filter( MetadataTable.versionid == data["versionid"], MetadataTable.title == data["title"]).first() metadata.value = data[ "value"] # Sets the queried metadata row value to the value sent by the user db.session.commit() if isBrowser: return redirect( url_for("version", id=data["versionid"], msg="Metadata '" + data["title"] + "' changed!")) else: return json.dumps({ "code": 200, "msg": "The metadata for this file has been successfully altered" }) except Exception as e: print(print_exc()) if isBrowser: return redirect( url_for( "version", id=data["versionid"], msg="Sorry, something went wrong when adding your metadata" )) else: return json.dumps({ "code": 500, "msg": "Something went wrong when adding metadata", "err": print_exc() }), 500
def deleteVersion(): """ Endpoint which can delete a file version by an admin, once moved to the archive - versionid: the UUID of the version in which to delete - userid: the UUID of the admin user who triggered the version delete - returns: an accurate response/redirection depending on the level of success in deleting the version """ isBrowser = "versionid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect(url_for('index', msg="You must be signed in to do this!")) else: return json.dumps({ "code": 401, "msg": "You must be signed in to do this", }), 401 fileData = file_query({"versionid": data["versionid"], "archived": True})[0] versionCount = len(fileData["versionorder"]) # Version order is a list of versionids therefore can be # used as an accurate count for the number of versions if versionCount <= 1: return deleteFile(fileData["fileid"]) # Deletes the entire file in the case of there only being # one version left try: userData = getUserData(request.cookies.get("userid")) if not userData["admin"]: # User has to be an admin to access this endpoint if isBrowser: return redirect(url_for("dash", msg="You don't have permission to do this!")) else: return json.dumps({ "code": 403, "msg": "You don't have permission to delete this file version", }) fileVersion = FileVersionTable.query.get(data["versionid"]) # Gets file version row from the table db.session.delete(fileVersion) # Deletes it and commits changes db.session.commit() b2 = B2Interface( os.environ.get("APPLICATION_KEY_ID"), os.environ.get("APPLICATION_KEY"), os.environ.get("BUCKET_NAME") ) b2.removeVersion(data["versionid"]) # Removes the version's B2 file if isBrowser: # Returns with a response upon successful deletion return redirect(url_for("archivedFile", id=fileData["fileid"], msg="File version successfully deleted!")) else: return json.dumps({ "code": 200, "msg": "The file version has been successfully deleted" }) except Exception as e: # Provides error response for failed deletion print(print_exc()) if isBrowser: return redirect(url_for("file", id=fileData["fileid"], msg="Something went wrong when deleting the file version")) else: return json.dumps({ "code": 500, "msg": "Something went wrong when deleting the file version" }), 500
def deleteFile(fileid=None): """ Endpoint for the complete deletion of a file once an admin deletes it from the archive. Accessible three ways, it is callable (for when duplicate versions might be found), accessible from the web app, and also from the CLI - userid: the UUID of the user who created the file, used to check for admin privileges - fileid: the UUID of the file in which to delete - returns: an accurate response/redirection depending on the level of success of file removal/db record removal """ if fileid is not None: isBrowser = True data = {"fileid": fileid} else: isBrowser = "fileid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect(url_for('index', msg="You must be signed in to do this!")) else: return json.dumps({ "code": 401, "msg": "You must be signed in to do this", }), 401 fileData = file_query({"fileid": data["fileid"], "archived": True})[0] userData = getUserData(request.cookies.get("userid")) if not userData["admin"]: if isBrowser: return redirect(url_for("dash", msg="You don't have permission to do this!")) else: return json.dumps({ "code": 403, "msg": "You don't have permission to delete this file", }) try: file = FileTable.query.filter(FileTable.fileid == fileData["fileid"]).first() db.session.delete(file) db.session.commit() # Remove the FileTable record from the database, and due to the cascading deletion # rules, any associated metadata, versions, comments, group linkings should be removed also b2 = B2Interface( os.environ.get("APPLICATION_KEY_ID"), # Create an instance of the B2Interface in order to communicate os.environ.get("APPLICATION_KEY"), # with the Backblaze bucket os.environ.get("BUCKET_NAME") ) for version in fileData["versions"].values(): # Run the B2Interface remove method in order to delete each b2.removeVersion(version["versionid"]) # file versions' actual file from the B2 bucket if isBrowser: # If all goes successfully, the user should be redirected or... return redirect(url_for("archive", msg=fileData["filename"] + " successfully deleted!")) else: return json.dumps({ # receive a JSON response indicating success "code": 200, "msg": fileData["filename"] + " has been successfully deleted" }) except Exception as e: print(print_exc()) # Print the stack trace if an error occurs and redirect the user to the dashboard if isBrowser: return redirect(url_for("dash", msg="Something went wrong when deleting the file")) else: return json.dumps({ "code": 500, "msg": "Something went wrong when deleting the file" }), 500
def removeGroup(): """ Endpoint for removing access from a file for a group - groupid: the UUID of the group in which the file will no longer be able to access - userid: the UUID of the user who triggered the group permissions change - fileid: UUID of the file in which to revoke the group's access to - returns: an accurate response/redirection depending on the level of success in changing the file permissions """ isBrowser = "fileid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect(url_for('index', msg="You must be signed in to do this")) else: return json.dumps({ "code": 401, "msg": "You must be signed in to do this", }), 401 file = file_query({"fileid": data["fileid"]})[0] userData = getUserData(request.cookies.get("userid")) if not file or not (userData["admin"] or leaderCheck(file["groups"], str(userData["userid"]))): if isBrowser: return redirect(url_for("dash", msg="You don't have permission to do this!")) else: return json.dumps({ "code": 403, "msg": "You don't have permission to add groups", }) try: groupExists = True in [str(group["groupid"]) == data["groupid"] for group in file["groups"]] # Runs checks to see if this group is in the file's groups, and raises an exception if not if not groupExists: raise Exception() filegroup = FileGroupTable.query.filter(and_(FileGroupTable.groupid == data["groupid"], FileGroupTable.fileid == data["fileid"])).first() # Retrieves the corresponding record from the group-file linking table so it can be deleted db.session.delete(filegroup) db.session.commit() if isBrowser: return redirect(url_for('file', id=data["fileid"], msg="Group removed successfully!")) else: return json.dumps({ "code": 200, "msg": "Group was removed successfully!", }) except Exception as e: print(print_exc()) if isBrowser: return redirect(url_for("file", id=data["fileid"], msg="Sorry, something went wrong when removing this group")) else: return json.dumps({ "code": 500, "msg": "Sorry, something went wrong when removing this group" }), 500
def archiveFile(): """ Endpoint which given a fileid will archive all the non-archived versions of that specific file, by running the archiveVersion util function iteratively, setting the archived field in the respective FileVersionTable records to true - fileid: UUID of the file in which to archive any unarchived versions from - userid: UUID of the user triggering the file archiving, must be an admin - returns: a response which may be either successful or with an error message, depending on outcome of operation """ isBrowser = "fileid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect( url_for("index", msg="You must be signed in to do this")) else: return json.dumps({ "code": 403, "msg": "You must be signed in to do this" }), 403 if not getUserData(request.cookies.get("userid"))["admin"]: if isBrowser: return redirect( url_for("dash", msg="You don't have permission to do this")) else: return json.dumps({ "code": 403, "msg": "You don't have the permissions to do this" }), 403 try: files = file_query({"fileid": data["fileid"]})[0] for version in files["versionorder"]: if not setVersionArchive(version, True): raise Exception() if isBrowser: return redirect( url_for("archive", msg="All versions successfully archived")) else: return json.dumps({ "code": 200, "msg": "All versions successfully archived" }) except Exception as e: print(print_exc()) if isBrowser: return redirect( url_for("archive", msg="Something went wrong when restoring your files")) else: return json.dumps({ "code": 500, "msg": "Something went wrong when restoring your files" })
def newVersion(): """ Endpoint for the creation of a new VERSION within a FILE. - userid: the UUID of the user who created the file, and will be associated with the initial version - title: what the uploaded version should show as in the file page's list of versions. - fileid: the UUID of the file record to associate the version with. - file: the file to upload for the new version, which will be passed into the Backblaze interface - returns: an accurate response/redirection depending on the level of success of file upload/database alteration """ isBrowser = "fileid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get( "userid" ): # If the user isn't authenticated, redirect them to sign in or produce if isBrowser: # appropriate JSON response return redirect( url_for("index", code=401, msg="You must be signed in to do this")) else: return json.dumps({ "code": 401, "msg": "You must be signed in to do this", }), 401 upload = request.files[ "file"] # Retrieve the uploaded file from the request object try: fileData = file_query({ "fileid": data["fileid"] })[0] # Fetch the data associated with the specified fileid if newFileVersion(fileData, upload, data["title"], request.cookies.get("userid")): fileData = file_query({ "fileid": data["fileid"] })[0] # If the new version was successfully created, # refetch the file data with the new version in if isBrowser: return redirect( url_for("version", id=fileData["versions"][fileData["versionorder"] [0]]["versionid"], msg="New version created successfully!")) else: return json.dumps({ "code": 200, "msg": "New version, " + fileData["versions"][fileData["versionorder"][0]]["title"] + ", created successfully!" }) else: # Won't upload successfully if there already exists a version with an identical hash, user informed of this if isBrowser: return redirect( url_for( "file", id=fileData["fileid"], msg= "Sorry, your new version already matches one in this file" )) else: return json.dumps({ "code": 500, "msg": "Sorry, your new version matches one already in the file" }), 500 except Exception as e: print( print_exc() ) # If an exception occurs, then print it to the log and inform the user of an issue if isBrowser: return redirect( url_for( "file", id=fileData["fileid"], msg="Sorry, something went wrong creating your new version" )) else: return json.dumps({ "code": 500, "msg": "Sorry, something went wrong when creating your new version" }), 500
def newComment(): isBrowser = "comment" in request.form # Endpoint can be reached from a form on web app and the CLI so check must be done to alter response data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get( "userid" ): # Checks if the user is signed in, or at least has the cookie if isBrowser: return redirect( url_for("index", msg="You must be signed in to complete this action")) else: # Redirects if on the web app and sends an error msg for the CLI, with a 401 response return json.dumps({ "code": 401, "msg": "You must be signed in to complete this action" }), 401 commentFile = file_query( {"fileid": data["fileid"]} ) # Attempts to find a file the user has access to with the provided file id if not commentFile: # If it cannot be found, they are informed they do not have correct permissions if isBrowser: return redirect( url_for( "index", msg="You do not have permission to complete this action")) else: return json.dumps({ "code": 403, "msg": "You do not have permission to complete this action" }), 403 try: addComment( data, request.cookies.get("userid") ) # Runs the abstracted addComment util function, included for code reusability if isBrowser: return redirect( # If successful, the user is redirected back to the file page where the comment url_for("file", id=data["fileid"], msg="Your comment has been added!") # can be seen ) else: # Or a JSON response is sent back to the CLI return json.dumps({ "code": 200, "msg": "Your comment has been added!", }) except Exception as e: # If an error occurs in this function, ideally we'd have more specialised exception handlers print(print_exc()) # Prints the error stack trace to the Heroku log if isBrowser: # Redirects the user to the dashboard with a user-friendly error message return redirect( url_for( "dash", msg="Sorry, something went wrong when adding your comment") ) else: # Or sends a JSON response with this message return json.dumps({ "code": 500, "msg": "Something went wrong when adding comment", "err": print_exc() }), 500
def addGroup(): """ Endpoint for provided file access to a specified group. - groupid: the UUID of the group in which the file will become associated with - userid: the UUID of the user who triggered the group permissions change - fileid: UUID of the file in which to provide group access to - returns: an accurate response/redirection depending on the level of success in changing the file permissions """ isBrowser = "fileid" in request.form data = request.form if isBrowser else json.loads(request.data) if not request.cookies.get("userid"): if isBrowser: return redirect(url_for('errors.error', code=401)) else: return json.dumps({ "code": 401, "msg": "You must be signed in to do this", }), 401 file = file_query({"fileid": data["fileid"]})[0] userData = getUserData(request.cookies.get("userid")) # Checks the user has the permission to add groups to a file (must be admin/group leader) if not file or not (userData["admin"] or leaderCheck( file["groups"], str(userData["userid"]))): if isBrowser: return redirect( url_for("file", id=file["fileid"], msg="You don't have permission to do this!")) else: return json.dumps({ "code": 403, "msg": "You don't have permission to add groups", }) try: filegroup = FileGroupTable({ "fileid": data["fileid"], "groupid": data["groupid"] }) db.session.add( filegroup ) # Adds a record to the filegrouptable to link the group and the file db.session.commit() if isBrowser: # Redirects the user back to the file page, or sends a JSON success response return redirect( url_for('file', id=data["fileid"], msg="Group added successfully!")) else: return json.dumps({ "code": 200, "msg": "Group was added successfully!", }) except Exception as e: print(print_exc()) if isBrowser: return redirect( url_for( "file", id=data["fileid"], msg="Sorry, something went wrong when adding this group")) else: return json.dumps({ "code": 500, "msg": "Sorry, something went wrong when adding this group" }), 500