Merge pull request #5180 from christianbeeznest/social-groups

Social: Improve group management and add image upload feature
pull/5189/head
christianbeeznest 2 years ago committed by GitHub
commit 9b0e6fc6df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      assets/css/scss/_social.scss
  2. 129
      assets/vue/components/social/GroupInfoCard.vue
  3. 3
      assets/vue/components/social/SocialGroupMenu.vue
  4. 40
      assets/vue/views/usergroup/List.vue
  5. 79
      assets/vue/views/usergroup/Show.vue
  6. 37
      assets/vue/views/userreluser/Invitations.vue
  7. 48
      src/CoreBundle/Controller/SocialController.php
  8. 18
      src/CoreBundle/Entity/Usergroup.php
  9. 44
      src/CoreBundle/Repository/Node/UsergroupRepository.php
  10. 73
      src/CoreBundle/Security/Authorization/Voter/UsergroupVoter.php

@ -246,6 +246,12 @@
} }
.social-groups { .social-groups {
.group-image {
width: 50px;
height: 50px;
border-radius: 50%;
object-fit: cover;
}
.search-header { .search-header {
display: flex; display: flex;

@ -12,11 +12,60 @@
:label="t('Edit this group')" :label="t('Edit this group')"
type="primary" type="primary"
class="mt-4" class="mt-4"
@click="editGroup" @click="showEditGroupDialog = true"
icon="edit" icon="edit"
/> />
</div> </div>
</BaseCard> </BaseCard>
<Dialog header="Edit Group" v-model:visible="showEditGroupDialog" :modal="true" :closable="true">
<form @submit.prevent="submitGroupEdit">
<div class="p-fluid">
<BaseInputTextWithVuelidate
v-model="editGroupForm.name"
label="Name*"
:vuelidate-property="v$.editGroupForm.name"
/>
<BaseInputTextWithVuelidate
v-model="editGroupForm.description"
label="Description"
:vuelidate-property="v$.editGroupForm.description"
as="textarea"
rows="3"
/>
<BaseInputTextWithVuelidate
v-model="editGroupForm.url"
label="URL"
:vuelidate-property="v$.editGroupForm.url"
/>
<BaseFileUpload
:label="t('Add a picture')"
accept="image"
size="small"
@file-selected="selectedFile = $event"
/>
<div class="p-field mt-2">
<label for="groupPermissions">Group Permissions</label>
<Dropdown id="groupPermissions" v-model="editGroupForm.permissions" :options="permissionsOptions" optionLabel="label" placeholder="Select Permission" />
</div>
<div class="p-field-checkbox mt-2">
<BaseCheckbox
id="leaveGroup"
v-model="editGroupForm.allowLeave"
:label="$t('Allow members to leave group')"
name="leaveGroup"
/>
</div>
</div>
<Button label="Save" icon="pi pi-check" class="p-button-rounded p-button-text" @click="submitGroupEdit" />
<Button label="Close" class="p-button-text" @click="closeEditDialog" />
</form>
</Dialog>
</template> </template>
<script setup> <script setup>
@ -25,15 +74,87 @@ import { useStore } from 'vuex'
import BaseCard from "../basecomponents/BaseCard.vue" import BaseCard from "../basecomponents/BaseCard.vue"
import BaseButton from "../basecomponents/BaseButton.vue" import BaseButton from "../basecomponents/BaseButton.vue"
import { useI18n } from "vue-i18n" import { useI18n } from "vue-i18n"
import { useRoute } from "vue-router" import { useRoute, useRouter } from "vue-router"
import BaseInputTextWithVuelidate from "../basecomponents/BaseInputTextWithVuelidate.vue"
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue"
import BaseFileUpload from "../basecomponents/BaseFileUpload.vue"
import useVuelidate from "@vuelidate/core"
import { required } from '@vuelidate/validators'
import axios from "axios"
import { ENTRYPOINT } from "../../config/entrypoint"
const { t } = useI18n() const { t } = useI18n()
const store = useStore() const store = useStore()
const route = useRoute() const route = useRoute()
const router = useRouter()
const groupInfo = inject('group-info') const groupInfo = inject('group-info')
const isGroup = inject('is-group') const isGroup = inject('is-group')
const editGroup = () => { const showEditGroupDialog = ref(false)
window.location = "/account/edit" const selectedFile = ref(null)
const permissionsOptions = [
{ label: 'Open', value: 1 },
{ label: 'Closed', value: 2 },
]
const editGroupForm = ref({
name: groupInfo.value.title,
description: groupInfo.value.description,
url: groupInfo.value.url,
permissions: permissionsOptions.find(option => option.value === groupInfo.value.visibility),
allowLeave: Boolean(groupInfo.value.allowMembersToLeaveGroup),
})
const v$ = useVuelidate({
editGroupForm: {
name: { required },
description: {},
url: {},
permissions: { required },
}
}, { editGroupForm })
const submitGroupEdit = () => {
v$.value.$touch()
if (!v$.value.$invalid) {
const updatedGroupData = {
title: editGroupForm.value.name,
description: editGroupForm.value.description,
url: editGroupForm.value.url,
visibility: String(editGroupForm.value.permissions.value),
allowMembersToLeaveGroup: editGroupForm.value.allowLeave ? 1 : 0,
}
axios.put(`${ENTRYPOINT}usergroups/${groupInfo.value.id}`, updatedGroupData, {
headers: {
'Content-Type': 'application/json',
},
})
.then((response) => {
if (selectedFile.value && response.data && response.data.id) {
const formData = new FormData()
formData.append('picture', selectedFile.value)
return axios.post(`/social-network/upload-group-picture/${response.data.id}`, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
}
})
.then(() => {
showEditGroupDialog.value = false
router.push('/dummy').then(() => {
router.go(-1)
})
})
.catch((error) => {
console.error('Error updating group:', error)
})
}
}
const closeEditDialog = () => {
showEditGroupDialog.value = false
} }
</script> </script>

@ -25,13 +25,12 @@
{{ t("Invite friends") }} {{ t("Invite friends") }}
</router-link> </router-link>
</li> </li>
<li class="menu-item"> <li class="menu-item" v-if="groupInfo.isAllowedToLeave">
<button @click="leaveGroup"> <button @click="leaveGroup">
<i class="mdi mdi-exit-to-app" aria-hidden="true"></i> <i class="mdi mdi-exit-to-app" aria-hidden="true"></i>
{{ t("Leave group") }} {{ t("Leave group") }}
</button> </button>
</li> </li>
</ul> </ul>
<ul v-else> <ul v-else>
<li class="menu-item"> <li class="menu-item">

@ -9,11 +9,12 @@
<TabPanel header="Newest" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Newest' }"> <TabPanel header="Newest" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Newest' }">
<div class="group-list"> <div class="group-list">
<div class="group-item" v-for="group in newestGroups" :key="group['@id']"> <div class="group-item" v-for="group in newestGroups" :key="group['@id']">
<i class="mdi mdi-account-group-outline group-icon"></i> <img v-if="group.pictureUrl" :src="group.pictureUrl" class="group-image" alt="Group Image" />
<i v-else class="mdi mdi-account-group-outline group-icon"></i>
<div class="group-details"> <div class="group-details">
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> <a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a>
<div class="group-info"> <div class="group-info">
<span class="group-member-count">{{ group.memberCount }} Member</span> <span class="group-member-count">{{ group.memberCount }} {{ t('Member') }}</span>
<span class="group-description">{{ group.description }}</span> <span class="group-description">{{ group.description }}</span>
</div> </div>
</div> </div>
@ -23,11 +24,12 @@
<TabPanel header="Popular" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Popular' }"> <TabPanel header="Popular" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'Popular' }">
<div class="group-list"> <div class="group-list">
<div class="group-item" v-for="group in popularGroups" :key="group['@id']"> <div class="group-item" v-for="group in popularGroups" :key="group['@id']">
<i class="mdi mdi-account-group-outline group-icon"></i> <img v-if="group.pictureUrl" :src="group.pictureUrl" class="group-image" alt="Group Image" />
<i v-else class="mdi mdi-account-group-outline group-icon"></i>
<div class="group-details"> <div class="group-details">
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> <a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a>
<div class="group-info"> <div class="group-info">
<span class="group-member-count">{{ group.memberCount }} Member</span> <span class="group-member-count">{{ group.memberCount }} {{ t('Member') }}</span>
<span class="group-description">{{ group.description }}</span> <span class="group-description">{{ group.description }}</span>
</div> </div>
</div> </div>
@ -36,11 +38,12 @@
<TabPanel header="My groups" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'My groups' }"> <TabPanel header="My groups" headerClass="tab-header" :class="{ 'active-tab': activeTab === 'My groups' }">
<div class="group-list"> <div class="group-list">
<div class="group-item" v-for="group in myGroups" :key="group['@id']"> <div class="group-item" v-for="group in myGroups" :key="group['@id']">
<i class="mdi mdi-account-group-outline group-icon"></i> <img v-if="group.pictureUrl" :src="group.pictureUrl" class="group-image" alt="Group Image" />
<i v-else class="mdi mdi-account-group-outline group-icon"></i>
<div class="group-details"> <div class="group-details">
<a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a> <a :href="`/resources/usergroups/show/${extractGroupId(group)}`" class="group-title">{{ group.title }}</a>
<div class="group-info"> <div class="group-info">
<span class="group-member-count">{{ group.memberCount }} Member</span> <span class="group-member-count">{{ group.memberCount }} {{ t('Member') }}</span>
<span class="group-description">{{ group.description }}</span> <span class="group-description">{{ group.description }}</span>
</div> </div>
</div> </div>
@ -51,7 +54,7 @@
</div> </div>
</div> </div>
<Dialog header="Add" :visible="showCreateGroupDialog" :modal="true" :closable="true" @hide="showCreateGroupDialog = false"> <Dialog header="Add" v-model:visible="showCreateGroupDialog" modal="true" closable="true">
<form @submit.prevent="createGroup"> <form @submit.prevent="createGroup">
<div class="p-fluid"> <div class="p-fluid">
<BaseInputTextWithVuelidate <BaseInputTextWithVuelidate
@ -94,6 +97,7 @@
</div> </div>
<Button label="Add" icon="pi pi-check" class="p-button-rounded p-button-text" @click="createGroup" /> <Button label="Add" icon="pi pi-check" class="p-button-rounded p-button-text" @click="createGroup" />
<Button label="Close" class="p-button-text" @click="showCreateGroupDialog = false" />
</form> </form>
</Dialog> </Dialog>
</template> </template>
@ -154,7 +158,8 @@ const createGroup = async () => {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
}) })
/*if (selectedFile.value && response.data && response.data.id) {
if (selectedFile.value && response.data && response.data.id) {
const formData = new FormData() const formData = new FormData()
formData.append('picture', selectedFile.value) formData.append('picture', selectedFile.value)
await axios.post(`/social-network/upload-group-picture/${response.data.id}`, formData, { await axios.post(`/social-network/upload-group-picture/${response.data.id}`, formData, {
@ -162,9 +167,10 @@ const createGroup = async () => {
'Content-Type': 'multipart/form-data', 'Content-Type': 'multipart/form-data',
}, },
}) })
}*/ }
showCreateGroupDialog.value = false showCreateGroupDialog.value = false
resetForm()
await updateGroupsList() await updateGroupsList()
} catch (error) { } catch (error) {
console.error('Failed to create group or upload picture:', error.response.data) console.error('Failed to create group or upload picture:', error.response.data)
@ -202,4 +208,20 @@ const redirectToGroupDetails = (groupId) => {
onMounted(async () => { onMounted(async () => {
await updateGroupsList() await updateGroupsList()
}) })
const closeDialog = () => {
showCreateGroupDialog.value = false
}
const resetForm = () => {
groupForm.value = {
name: '',
description: '',
url: '',
picture: null,
permissions: '',
allowLeave: false,
}
selectedFile.value = null
v$.value.$reset()
}
</script> </script>

@ -1,38 +1,76 @@
<template> <template>
<div v-if="!isLoading && groupInfo.isMember" class="social-group-show">
<div v-if="!isLoading && !groupInfo.isMember" class="social-group-show">
<div class="social-group-details-info">
<p v-if="groupInfo.visibility === 1" class="text-center">
{{ t('This is an open group') }}
</p>
<p v-else>
{{ t('This is a closed group') }}
</p>
</div>
</div>
<div class="social-group-show group-info text-center">
<div class="group-header"> <div class="group-header">
<h1 class="group-title">{{ groupInfo?.title || '...' }}</h1> <h1 class="group-title">{{ groupInfo?.title || '...' }}</h1>
<p class="group-description">{{ groupInfo?.description }}</p> <p class="group-description">{{ groupInfo?.description }}</p>
</div> </div>
</div>
<div v-if="!isLoading && (groupInfo.isMember || groupInfo.visibility === 1)" class="social-group-show">
<div v-if="!groupInfo.isMember" class="text-center">
<div v-if="![4, 3].includes(groupInfo.role)">
<BaseButton
:label="t('Join to group')"
type="primary"
class="mt-4"
@click="joinGroup"
icon="join-group"
/>
</div>
<div v-else-if="groupInfo.role === 3">
<BaseButton
:label="t('You have been invited to join this group')"
type="primary"
class="mt-4"
@click="joinGroup"
icon="email-unread"
/>
</div>
</div>
<div v-if="groupInfo.isMember" class="text-center">
<ul class="tabs"> <ul class="tabs">
<li :class="{ active: activeTab === 'discussions' }" @click="activeTab = 'discussions'">{{ t('Discussions') }}</li> <li :class="{ active: activeTab === 'discussions' }" @click="activeTab = 'discussions'">{{ t('Discussions') }}</li>
<li :class="{ active: activeTab === 'members' }" @click="activeTab = 'members'">{{ t('Members') }}</li> <li :class="{ active: activeTab === 'members' }" @click="activeTab = 'members'">{{ t('Members') }}</li>
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
<GroupDiscussions v-if="activeTab === 'discussions'" :group-id="groupInfo.id" /> <GroupDiscussions v-if="activeTab === 'discussions'" :group-id="groupInfo.id" />
<GroupMembers v-if="activeTab === 'members'" :group-id="groupInfo.id" /> <GroupMembers v-if="activeTab === 'members'" :group-id="groupInfo.id" />
</div> </div>
</div> </div>
<div v-if="!isLoading && !groupInfo.isMember" class="text-center">
<div class="group-header">
<h1 class="group-title">{{ groupInfo?.title || '...' }}</h1>
<p class="group-description">{{ groupInfo?.description }}</p>
</div> </div>
<p v-if="groupInfo.visibility === 2">{{ t('This is a closed group.') }}</p>
<p v-if="groupInfo.role === 3">{{ t('You already sent an invitation') }}</p> <div v-if="!isLoading && !(groupInfo.isMember || groupInfo.visibility === 1)" class="text-center">
<p v-else>{{ t('Join this group to see the content.') }}</p> <div v-if="![4, 3].includes(groupInfo.role)">
<BaseButton <BaseButton
v-if="groupInfo.visibility === 1 && groupInfo.role !== 3"
:label="t('Join to group')" :label="t('Join to group')"
type="primary" type="primary"
class="mt-4" class="mt-4"
@click="joinGroup" @click="joinGroup"
icon="mdi-account-multiple-plus" icon="join-group"
/> />
</div> </div>
<div v-else-if="groupInfo.role === 3">
<BaseButton
:label="t('You have been invited to join this group')"
type="primary"
class="mt-4"
@click="joinGroup"
icon="email-unread"
/>
</div>
</div>
</template> </template>
<script setup> <script setup>
@ -48,27 +86,24 @@ import BaseButton from "../../components/basecomponents/BaseButton.vue"
const { t } = useI18n() const { t } = useI18n()
const route = useRoute() const route = useRoute()
const activeTab = ref('discussions') const activeTab = ref('discussions')
const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo(); const { user, groupInfo, isGroup, loadGroup, isLoading } = useSocialInfo()
const joinGroup = async () => { const joinGroup = async () => {
try { try {
const response = await axios.post('/social-network/group-action', { const response = await axios.post('/social-network/group-action', {
userId: user.value.id, userId: user.value.id,
groupId: groupInfo.value.id, groupId: groupInfo.value.id,
action: 'join' action: 'join'
}); })
if (response.data.success) { if (response.data.success) {
await loadGroup(groupInfo.value.id); await loadGroup(groupInfo.value.id)
} }
} catch (error) { } catch (error) {
console.error('Error joining the group:', error); console.error('Error joining the group:', error)
} }
}; }
onMounted(async () => { onMounted(async () => {
if (route.params.group_id) { if (route.params.group_id) {
await loadGroup(route.params.group_id); await loadGroup(route.params.group_id)
} }
}); })
</script> </script>

@ -105,10 +105,39 @@ const denyInvitation = async (invitationId) => {
console.error('Error denying invitation:', error) console.error('Error denying invitation:', error)
} }
} }
const acceptGroupInvitation = (groupId) => { const acceptGroupInvitation = async (groupId) => {
console.log(`Accepted group invitation with ID: ${groupId}`) try {
const response = await axios.post('/social-network/group-action', {
userId: user.value.id,
groupId: groupId,
action: 'accept',
});
if (response.data.success) {
console.log('Group invitation accepted successfully');
await fetchInvitations(user.value.id)
} else {
console.error('Failed to accept group invitation');
}
} catch (error) {
console.error('Error accepting group invitation:', error);
}
} }
const denyGroupInvitation = (groupId) => {
console.log(`Denied group invitation with ID: ${groupId}`) const denyGroupInvitation = async (groupId) => {
try {
const response = await axios.post('/social-network/group-action', {
userId: user.value.id,
groupId: groupId,
action: 'deny',
});
if (response.data.success) {
console.log('Group invitation denied successfully');
await fetchInvitations(user.value.id)
} else {
console.error('Failed to deny group invitation');
}
} catch (error) {
console.error('Error denying group invitation:', error);
}
} }
</script> </script>

@ -28,6 +28,7 @@ use Exception;
use ExtraFieldValue; use ExtraFieldValue;
use MessageManager; use MessageManager;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\RequestStack;
@ -457,7 +458,8 @@ class SocialController extends AbstractController
int $userId, int $userId,
MessageRepository $messageRepository, MessageRepository $messageRepository,
UsergroupRepository $usergroupRepository, UsergroupRepository $usergroupRepository,
UserRepository $userRepository UserRepository $userRepository,
TranslatorInterface $translator
): JsonResponse { ): JsonResponse {
$user = $this->getUser(); $user = $this->getUser();
if ($userId !== $user->getId()) { if ($userId !== $user->getId()) {
@ -500,15 +502,19 @@ class SocialController extends AbstractController
$pendingGroupInvitations = []; $pendingGroupInvitations = [];
$pendingGroups = $usergroupRepository->getGroupsByUser($userId, Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION); $pendingGroups = $usergroupRepository->getGroupsByUser($userId, Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION);
/* @var Usergroup $group */
foreach ($pendingGroups as $group) { foreach ($pendingGroups as $group) {
$isGroupVisible = (int) $group->getVisibility() === 1;
$infoVisibility = !$isGroupVisible ? ' - '.$translator->trans('This group is closed.') : '';
$pendingGroupInvitations[] = [ $pendingGroupInvitations[] = [
'id' => $group->getId(), 'id' => $group->getId(),
'itemId' => $group->getId(), 'itemId' => $group->getId(),
'itemName' => $group->getTitle(), 'itemName' => $group->getTitle().$infoVisibility,
'itemPicture' => $usergroupRepository->getUsergroupPicture($group->getId()), 'itemPicture' => $usergroupRepository->getUsergroupPicture($group->getId()),
'content' => $group->getDescription(), 'content' => $group->getDescription(),
'date' => $group->getCreatedAt()->format('Y-m-d H:i:s'), 'date' => $group->getCreatedAt()->format('Y-m-d H:i:s'),
'canAccept' => true, 'canAccept' => $isGroupVisible,
'canDeny' => true, 'canDeny' => true,
]; ];
} }
@ -612,12 +618,15 @@ class SocialController extends AbstractController
'id' => $group->getId(), 'id' => $group->getId(),
'title' => $group->getTitle(), 'title' => $group->getTitle(),
'description' => $group->getDescription(), 'description' => $group->getDescription(),
'url' => $group->getUrl(),
'image' => $usergroupRepository->getUsergroupPicture($group->getId()), 'image' => $usergroupRepository->getUsergroupPicture($group->getId()),
'visibility' => (int) $group->getVisibility(),
'allowMembersToLeaveGroup' => $group->getAllowMembersToLeaveGroup(),
'isMember' => $isMember, 'isMember' => $isMember,
'isModerator' => $isModerator, 'isModerator' => $isModerator,
'role' => $role, 'role' => $role,
'isUserOnline' => $isUserOnline, 'isUserOnline' => $isUserOnline,
'visibility' => (int) $group->getVisibility(), 'isAllowedToLeave' => $group->getAllowMembersToLeaveGroup() === 1,
]; ];
return $this->json($groupDetails); return $this->json($groupDetails);
@ -638,11 +647,26 @@ class SocialController extends AbstractController
try { try {
switch ($action) { switch ($action) {
case 'accept':
$userRole = $usergroupRepository->getUserGroupRole($groupId, $userId);
if (in_array(
$userRole,
[
Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION_SENT_BY_USER,
Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION,
]
)) {
$usergroupRepository->updateUserRole($userId, $groupId, Usergroup::GROUP_USER_PERMISSION_READER);
}
break;
case 'join': case 'join':
$usergroupRepository->addUserToGroup($userId, $groupId); $usergroupRepository->addUserToGroup($userId, $groupId);
break; break;
case 'deny':
case 'leave': case 'leave':
$usergroupRepository->removeUserFromGroup($userId, $groupId); $usergroupRepository->removeUserFromGroup($userId, $groupId);
@ -753,6 +777,22 @@ class SocialController extends AbstractController
return $this->json($onlineStatuses); return $this->json($onlineStatuses);
} }
#[Route('/upload-group-picture/{groupId}', name: 'chamilo_core_social_upload_group_picture')]
public function uploadGroupPicture(
Request $request,
int $groupId,
UsergroupRepository $usergroupRepository,
IllustrationRepository $illustrationRepository
): JsonResponse {
$file = $request->files->get('picture');
if ($file instanceof UploadedFile) {
$userGroup = $usergroupRepository->find($groupId);
$illustrationRepository->addIllustration($userGroup, $this->getUser(), $file);
}
return new JsonResponse(['success' => 'Group and image saved successfully'], Response::HTTP_OK);
}
/** /**
* Checks the relationship between the current user and another user. * Checks the relationship between the current user and another user.
* *

@ -41,25 +41,29 @@ use Symfony\Component\Validator\Constraints as Assert;
uriTemplate: '/usergroup/list/my', uriTemplate: '/usergroup/list/my',
normalizationContext: ['groups' => ['usergroup:read']], normalizationContext: ['groups' => ['usergroup:read']],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
name: 'get_my_usergroups' name: 'get_my_usergroups',
provider: UsergroupDataProvider::class
), ),
new GetCollection( new GetCollection(
uriTemplate: '/usergroup/list/newest', uriTemplate: '/usergroup/list/newest',
normalizationContext: ['groups' => ['usergroup:read']], normalizationContext: ['groups' => ['usergroup:read']],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
name: 'get_newest_usergroups' name: 'get_newest_usergroups',
provider: UsergroupDataProvider::class
), ),
new GetCollection( new GetCollection(
uriTemplate: '/usergroup/list/popular', uriTemplate: '/usergroup/list/popular',
normalizationContext: ['groups' => ['usergroup:read']], normalizationContext: ['groups' => ['usergroup:read']],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
name: 'get_popular_usergroups' name: 'get_popular_usergroups',
provider: UsergroupDataProvider::class
), ),
new GetCollection( new GetCollection(
uriTemplate: '/usergroups/search', uriTemplate: '/usergroups/search',
normalizationContext: ['groups' => ['usergroup:read']], normalizationContext: ['groups' => ['usergroup:read']],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
name: 'search_usergroups' name: 'search_usergroups',
provider: UsergroupDataProvider::class
), ),
new GetCollection( new GetCollection(
uriTemplate: '/usergroups/{id}/members', uriTemplate: '/usergroups/{id}/members',
@ -80,7 +84,6 @@ use Symfony\Component\Validator\Constraints as Assert;
'groups' => ['usergroup:write'], 'groups' => ['usergroup:write'],
], ],
security: "is_granted('ROLE_USER')", security: "is_granted('ROLE_USER')",
provider: UsergroupDataProvider::class
)] )]
#[ORM\Table(name: 'usergroup')] #[ORM\Table(name: 'usergroup')]
#[ORM\Entity(repositoryClass: UsergroupRepository::class)] #[ORM\Entity(repositoryClass: UsergroupRepository::class)]
@ -101,6 +104,7 @@ class Usergroup extends AbstractResource implements ResourceInterface, ResourceI
public const GROUP_PERMISSION_OPEN = 1; public const GROUP_PERMISSION_OPEN = 1;
public const GROUP_PERMISSION_CLOSED = 2; public const GROUP_PERMISSION_CLOSED = 2;
#[Groups(['usergroup:read'])]
#[ORM\Column(name: 'id', type: 'integer', nullable: false)] #[ORM\Column(name: 'id', type: 'integer', nullable: false)]
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
@ -165,7 +169,7 @@ class Usergroup extends AbstractResource implements ResourceInterface, ResourceI
#[Groups(['usergroup:read'])] #[Groups(['usergroup:read'])]
private ?int $memberCount = null; private ?int $memberCount = null;
#[Groups(['usergroup:read'])] #[Groups(['usergroup:read', 'usergroup:write'])]
private ?string $pictureUrl = ''; private ?string $pictureUrl = '';
public function __construct() public function __construct()
@ -371,7 +375,7 @@ class Usergroup extends AbstractResource implements ResourceInterface, ResourceI
public function getPictureUrl(): ?string public function getPictureUrl(): ?string
{ {
return $this->picture; return $this->pictureUrl;
} }
public function setPictureUrl(?string $pictureUrl): self public function setPictureUrl(?string $pictureUrl): self

@ -73,7 +73,7 @@ class UsergroupRepository extends ResourceRepository
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
} }
public function getNewestGroups(int $limit = 6, string $query = ''): array public function getNewestGroups(int $limit = 30, string $query = ''): array
{ {
$qb = $this->createQueryBuilder('g') $qb = $this->createQueryBuilder('g')
->select('g, COUNT(gu) AS HIDDEN memberCount') ->select('g, COUNT(gu) AS HIDDEN memberCount')
@ -102,7 +102,7 @@ class UsergroupRepository extends ResourceRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
public function getPopularGroups(int $limit = 6): array public function getPopularGroups(int $limit = 30): array
{ {
$qb = $this->createQueryBuilder('g') $qb = $this->createQueryBuilder('g')
->select('g, COUNT(gu) as HIDDEN memberCount') ->select('g, COUNT(gu) as HIDDEN memberCount')
@ -187,6 +187,10 @@ class UsergroupRepository extends ResourceRepository
throw new Exception('Group or User not found'); throw new Exception('Group or User not found');
} }
if (Usergroup::GROUP_PERMISSION_CLOSED === (int) $group->getVisibility()) {
$relationType = Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION;
}
$existingRelation = $this->_em->getRepository(UsergroupRelUser::class)->findOneBy([ $existingRelation = $this->_em->getRepository(UsergroupRelUser::class)->findOneBy([
'usergroup' => $group, 'usergroup' => $group,
'user' => $user, 'user' => $user,
@ -198,16 +202,41 @@ class UsergroupRepository extends ResourceRepository
$existingRelation->setUser($user); $existingRelation->setUser($user);
} }
if (Usergroup::GROUP_PERMISSION_CLOSED === $group->getVisibility()) {
$relationType = Usergroup::GROUP_USER_PERMISSION_PENDING_INVITATION;
}
$existingRelation->setRelationType($relationType); $existingRelation->setRelationType($relationType);
$this->_em->persist($existingRelation); $this->_em->persist($existingRelation);
$this->_em->flush(); $this->_em->flush();
} }
public function updateUserRole($userId, $groupId, $relationType = Usergroup::GROUP_USER_PERMISSION_READER)
{
$qb = $this->createQueryBuilder('g');
$qb->delete(UsergroupRelUser::class, 'gu')
->where('gu.usergroup = :groupId')
->andWhere('gu.user = :userId')
->setParameter('groupId', $groupId)
->setParameter('userId', $userId);
$query = $qb->getQuery();
$query->execute();
$group = $this->find($groupId);
$user = $this->_em->getRepository(User::class)->find($userId);
if (!$group || !$user) {
throw new Exception('Group or User not found');
}
$usergroupRelUser = new UsergroupRelUser();
$usergroupRelUser->setUsergroup($group);
$usergroupRelUser->setUser($user);
$usergroupRelUser->setRelationType($relationType);
$this->_em->persist($usergroupRelUser);
$this->_em->flush();
}
public function removeUserFromGroup(int $userId, int $groupId): bool public function removeUserFromGroup(int $userId, int $groupId): bool
{ {
/** @var Usergroup $group */ /** @var Usergroup $group */
@ -345,8 +374,9 @@ class UsergroupRepository extends ResourceRepository
->where('g.id = :groupId AND gu.user = :userId') ->where('g.id = :groupId AND gu.user = :userId')
->setParameter('groupId', $groupId) ->setParameter('groupId', $groupId)
->setParameter('userId', $userId) ->setParameter('userId', $userId)
->orderBy('gu.id', 'DESC')
->select('gu.relationType') ->select('gu.relationType')
; ->setMaxResults(1);
$result = $qb->getQuery()->getOneOrNullResult(); $result = $qb->getQuery()->getOneOrNullResult();

@ -0,0 +1,73 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
use Chamilo\CoreBundle\Entity\Usergroup;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Security\Core\User\UserInterface;
class UsergroupVoter extends Voter
{
public const CREATE = 'CREATE';
public const VIEW = 'VIEW';
public const EDIT = 'EDIT';
public const DELETE = 'DELETE';
public function __construct(
private Security $security,
private UsergroupRepository $usergroupRepository
) {}
protected function supports(string $attribute, $subject): bool
{
$options = [
self::CREATE,
self::VIEW,
self::EDIT,
self::DELETE,
];
// if the attribute isn't one we support, return false
if (!\in_array($attribute, $options, true)) {
return false;
}
// only vote on Post objects inside this voter
return $subject instanceof Usergroup;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
{
$currentUser = $token->getUser();
if (!$currentUser instanceof UserInterface) {
return false;
}
if ($this->security->isGranted('ROLE_ADMIN')) {
return true;
}
/** @var Usergroup $usergroup */
$usergroup = $subject;
switch ($attribute) {
case self::EDIT:
return $this->canEdit($usergroup, $currentUser);
}
return false;
}
private function canEdit(Usergroup $usergroup, $currentUser): bool
{
return $this->usergroupRepository->isGroupModerator($usergroup->getId(), $currentUser->getId());
}
}
Loading…
Cancel
Save