Merge pull request #4962 from christianbeeznest/ofaj-20927-2

Social: Add social post attachments - refs BT#20927
pull/4965/head
christianbeeznest 1 year ago committed by GitHub
commit 139acd882f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 66
      assets/vue/components/social/SocialWallPost.vue
  2. 26
      assets/vue/components/social/SocialWallPostForm.vue
  3. 58
      src/CoreBundle/Controller/Api/CreateSocialPostAttachmentAction.php
  4. 38
      src/CoreBundle/Controller/Api/SocialPostAttachmentsController.php
  5. 30
      src/CoreBundle/Entity/SocialPost.php
  6. 192
      src/CoreBundle/Entity/SocialPostAttachment.php
  7. 6
      src/CoreBundle/Framework/Container.php
  8. 27
      src/CoreBundle/Migrations/Schema/V200/Version20231026221100.php
  9. 116
      src/CoreBundle/Migrations/Schema/V200/Version20231026231100.php
  10. 19
      src/CoreBundle/Repository/Node/SocialPostAttachmentRepository.php
  11. 2
      src/CoreBundle/Tool/User.php

@ -39,22 +39,17 @@
</div>
<div class="flex flex-col gap-2">
<img
v-if="containsImage"
:alt="attachment.comment"
:src="attachment.contentUrl"
>
<video
v-if="containsVideo"
width="320"
height="240"
controls
>
<source
:src="attachment.contentUrl"
<div v-for="(attachment, index) in computedAttachments" :key="index">
<img
v-if="attachment.filename.includes('.jpg') || attachment.filename.includes('.png')"
:src="attachment.path"
:alt="attachment.filename"
>
{{ attachment.comment }}
</video>
<video v-if="isVideoAttachment(attachment)" controls>
<source :src="attachment.path" :type="attachment.mimeType">
</video>
</div>
<div v-html="post.content"/>
@ -85,7 +80,7 @@
<script setup>
import WallCommentForm from "./SocialWallCommentForm.vue";
import {computed, onMounted, reactive} from "vue";
import {ref, computed, onMounted, reactive} from "vue";
import WallComment from "./SocialWallComment.vue";
import WallActions from "./Actions";
import axios from "axios";
@ -105,20 +100,36 @@ const props = defineProps({
const emit = defineEmits(["post-deleted"]);
const store = useStore();
import { useSecurityStore } from "../../store/securityStore"
const { relativeDatetime } = useFormatDate()
const attachment = null;//props.post.attachments.length ? props.post.attachments[0] : null;
let comments = reactive([]);
const attachments = ref([]);
const securityStore = useSecurityStore()
const containsImage = false; //attachment && attachment.resourceNode.resourceFile.mimeType.includes('image/');
const containsVideo = false; //attachment && attachment.resourceNode.resourceFile.mimeType.includes('video/');
const currentUser = computed(() => securityStore.user)
const isOwner = computed(() => currentUser['@id'] === props.post.sender['@id'])
const currentUser = store.getters['security/getUser'];
onMounted(async () => {
loadComments();
const isOwner = computed(() => currentUser['@id'] === props.post.sender['@id'])
await loadAttachments();
});
onMounted(loadComments);
const computedAttachments = computed(() => {
return attachments.value;
});
async function loadAttachments() {
try {
const postIri = props.post["@id"]
const response = await axios.get(`${postIri}/attachments`)
attachments.value = response.data
} catch (error) {
console.error("There was an error loading the attachments!", error)
}
}
function loadComments() {
axios
@ -148,4 +159,13 @@ function onCommentPosted(newComment) {
function onPostDeleted(post) {
emit('post-deleted', post);
}
const isVideoAttachment = (attachment) => {
if (attachment.filename) {
const fileExtension = attachment.filename.split('.').pop().toLowerCase();
return ['mp4', 'webm', 'ogg'].includes(fileExtension);
}
return false;
};
</script>

@ -20,11 +20,11 @@
<div class="flex mb-2">
<BaseFileUpload
v-model="attachment"
:label="$t('File upload')"
id="post-file"
:label="t('File upload')"
accept="image"
size="small"
@file-selected="v$.attachment.$touch()"
@file-selected="selectedFile = $event"
/>
<BaseButton
@ -52,6 +52,7 @@ import BaseButton from "../basecomponents/BaseButton.vue";
import BaseFileUpload from "../basecomponents/BaseFileUpload.vue";
import BaseCheckbox from "../basecomponents/BaseCheckbox.vue";
import BaseInputTextWithVuelidate from "../basecomponents/BaseInputTextWithVuelidate.vue";
import axios from "axios";
const emit = defineEmits(['post-created']);
const store = useStore();
@ -62,6 +63,7 @@ const user = inject('social-user');
const currentUser = store.getters['security/getUser'];
const userIsAdmin = store.getters['security/isAdmin'];
const selectedFile = ref(null)
const postState = reactive({
content: '',
attachment: null,
@ -118,13 +120,21 @@ async function sendPost() {
userReceiver: currentUser['@id'] === user.value['@id'] ? null : user.value['@id'],
});
if (postState.attachment) {
if (selectedFile.value) {
const formData = new FormData();
formData.append('file', postState.attachment);
const post = store.state.socialpost.created;
await store.dispatch('messageattachment/createWithFormData', {
postId: post.id,
file: formData
let idUrl = post["@id"];
let parts = idUrl.split('/');
let socialPostId = parts[parts.length - 1];
formData.append('file', selectedFile.value);
formData.append('messageId', socialPostId);
const endpoint = '/api/social_post_attachments';
const fileUploadResponse = await axios.post(endpoint, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}

@ -0,0 +1,58 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\SocialPost;
use Chamilo\CoreBundle\Entity\SocialPostAttachment;
use Chamilo\CoreBundle\Entity\User;
use Chamilo\CoreBundle\Repository\Node\SocialPostAttachmentRepository;
use Doctrine\ORM\EntityManager;
use Exception;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;
class CreateSocialPostAttachmentAction extends BaseResourceFileAction
{
/**
* @throws Exception
*/
public function __invoke(Request $request, SocialPostAttachmentRepository $repo, EntityManager $em, Security $security): SocialPostAttachment
{
/** @var UploadedFile $uploadedFile */
$uploadedFile = $request->files->get('file');
$socialPostId = $request->request->get('messageId');
if (!$uploadedFile instanceof UploadedFile) {
throw new \Exception('No file uploaded');
}
$socialPost = $em->getRepository(SocialPost::class)->find($socialPostId);
if (!$socialPost) {
throw new \Exception('No social post found');
}
/** @var User $currentUser */
$currentUser = $security->getUser();
$attachment = new SocialPostAttachment();
$attachment->setSocialPost($socialPost);
$attachment->setPath(uniqid('social_post', true));
$attachment->setFilename($uploadedFile->getClientOriginalName());
$attachment->setSize($uploadedFile->getSize());
$attachment->setInsertUserId($currentUser->getId());
$attachment->setInsertDateTime(new \DateTime('now', new \DateTimeZone('UTC')));
$attachment->setParent($currentUser);
$attachment->addUserLink($currentUser);
$em->persist($attachment);
$em->flush();
$repo->addFile($attachment, $uploadedFile);
return $attachment;
}
}

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Chamilo\CoreBundle\Controller\Api;
use Chamilo\CoreBundle\Entity\SocialPost;
use Chamilo\CoreBundle\Entity\SocialPostAttachment;
use Chamilo\CoreBundle\Repository\Node\SocialPostAttachmentRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Security;
class SocialPostAttachmentsController extends BaseResourceFileAction
{
public function __invoke(SocialPost $socialPost, EntityManager $em, Security $security, SocialPostAttachmentRepository $attachmentRepo): JsonResponse
{
$attachments = $em->getRepository(SocialPostAttachment::class)->findBy(['socialPost' => $socialPost->getId()]);
if (!$attachments) {
return new JsonResponse(['error' => 'No attachments found for this post'], JsonResponse::HTTP_NOT_FOUND);
}
$attachmentsInfo = [];
foreach ($attachments as $attachment) {
$attachmentsInfo[] = [
'id' => $attachment->getId(),
'filename' => $attachment->getFilename(),
'path' => $attachmentRepo->getResourceFileUrl($attachment),
'size' => $attachment->getSize(),
];
}
return new JsonResponse($attachmentsInfo, JsonResponse::HTTP_OK);
}
}

@ -17,6 +17,7 @@ use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use Chamilo\CoreBundle\Controller\Api\DislikeSocialPostController;
use Chamilo\CoreBundle\Controller\Api\LikeSocialPostController;
use Chamilo\CoreBundle\Controller\Api\SocialPostAttachmentsController;
use Chamilo\CoreBundle\Filter\SocialWallFilter;
use Chamilo\CoreBundle\Repository\SocialPostRepository;
use DateTime;
@ -54,6 +55,11 @@ use Symfony\Component\Validator\Constraints as Assert;
denormalizationContext: ['groups' => []],
security: "is_granted('ROLE_USER')"
),
new Get(
uriTemplate: '/social_posts/{id}/attachments',
controller: SocialPostAttachmentsController::class,
normalizationContext: ['groups' => ['attachment:read']],
),
new GetCollection(security: "is_granted('ROLE_USER')"),
],
normalizationContext: ['groups' => ['social_post:read']],
@ -139,6 +145,14 @@ class SocialPost
#[ApiFilter(filterClass: SocialWallFilter::class)]
protected User $wallOwner;
#[ORM\OneToMany(
targetEntity: SocialPostAttachment::class,
mappedBy: "socialPost",
cascade: ["persist", "remove"],
orphanRemoval: true
)]
private Collection $attachments;
public function __construct()
{
$this->userReceiver = null;
@ -151,6 +165,7 @@ class SocialPost
$this->type = self::TYPE_WALL_POST;
$this->countFeedbackLikes = 0;
$this->countFeedbackDislikes = 0;
$this->attachments = new ArrayCollection();
}
public function getId(): int
@ -338,4 +353,19 @@ class SocialPost
return $this;
}
public function getAttachments(): Collection
{
return $this->attachments;
}
public function addAttachment(SocialPostAttachment $attachment): self
{
if (!$this->attachments->contains($attachment)) {
$this->attachments[] = $attachment;
$attachment->setSocialPost($this);
}
return $this;
}
}

@ -0,0 +1,192 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Entity;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use Chamilo\CoreBundle\Controller\Api\CreateSocialPostAttachmentAction;
use Chamilo\CoreBundle\Repository\Node\SocialPostAttachmentRepository;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Stringable;
#[ApiResource(
types: ['http://schema.org/MediaObject'],
operations: [
new Get(),
new GetCollection(),
new Post(
controller: CreateSocialPostAttachmentAction::class,
openapiContext: [
'requestBody' => [
'content' => [
'multipart/form-data' => [
'schema' => [
'type' => 'object',
'properties' => [
'file' => [
'type' => 'string',
'format' => 'binary',
],
'messageId' => [
'type' => 'integer',
],
],
],
],
],
],
],
security: 'is_granted(\'ROLE_USER\')',
validationContext: [
'groups' => [
'Default',
'message_attachment:create',
],
],
deserialize: false
),
],
normalizationContext: [
'groups' => ['message:read'],
],
)]
#[ORM\Table(name: 'social_post_attachments')]
#[ORM\Entity(repositoryClass: SocialPostAttachmentRepository::class)]
class SocialPostAttachment extends AbstractResource implements ResourceInterface, Stringable
{
#[ORM\Column(name: 'id', type: 'integer')]
#[ORM\Id]
#[ORM\GeneratedValue]
protected ?int $id = null;
#[ORM\ManyToOne(targetEntity: \Chamilo\CoreBundle\Entity\SocialPost::class)]
#[ORM\JoinColumn(name: 'social_post_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
protected SocialPost $socialPost;
#[ORM\Column(name: 'path', type: 'string', length: 255, nullable: false)]
protected string $path;
#[ORM\Column(name: 'filename', type: 'text', nullable: false)]
protected string $filename;
#[ORM\Column(name: 'size', type: 'integer')]
protected int $size;
#[ORM\Column(name: 'sys_insert_user_id', type: 'integer')]
protected int $insertUserId;
#[ORM\Column(name: 'sys_insert_datetime', type: 'datetime')]
protected DateTime $insertDateTime;
#[ORM\Column(name: 'sys_lastedit_user_id', type: 'integer', nullable: true, unique: false)]
protected ?int $lastEditUserId = null;
#[ORM\Column(name: 'sys_lastedit_datetime', type: 'datetime', nullable: true, unique: false)]
protected ?DateTime $lastEditDateTime = null;
public function __toString(): string
{
return $this->getFilename();
}
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return string
*/
public function getPath()
{
return $this->path;
}
public function setPath(string $path): self
{
$this->path = $path;
return $this;
}
/**
* @return string
*/
public function getFilename()
{
return $this->filename;
}
public function setFilename(string $filename): self
{
$this->filename = $filename;
return $this;
}
/**
* @return int
*/
public function getSize()
{
return $this->size;
}
public function setSize(int $size): self
{
$this->size = $size;
return $this;
}
public function getSocialPost(): SocialPost
{
return $this->socialPost;
}
public function setSocialPost(SocialPost $socialPost): self
{
$this->socialPost = $socialPost;
return $this;
}
public function getResourceName(): string
{
return $this->getFilename();
}
public function setResourceName(string $name)
{
return $this->setFilename($name);
}
public function getResourceIdentifier(): int
{
return $this->getId();
}
public function setInsertUserId(int $insertUserId): self
{
$this->insertUserId = $insertUserId;
return $this;
}
public function setInsertDateTime(DateTime $insertDateTime): self
{
$this->insertDateTime = $insertDateTime;
return $this;
}
}

@ -22,6 +22,7 @@ use Chamilo\CoreBundle\Repository\Node\CourseRepository;
use Chamilo\CoreBundle\Repository\Node\IllustrationRepository;
use Chamilo\CoreBundle\Repository\Node\MessageAttachmentRepository;
use Chamilo\CoreBundle\Repository\Node\PersonalFileRepository;
use Chamilo\CoreBundle\Repository\Node\SocialPostAttachmentRepository;
use Chamilo\CoreBundle\Repository\Node\TicketMessageAttachmentRepository;
use Chamilo\CoreBundle\Repository\Node\UsergroupRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
@ -300,6 +301,11 @@ class Container
return self::$container->get(TicketMessageAttachmentRepository::class);
}
public static function getSocialPostAttachmentRepository(): SocialPostAttachmentRepository
{
return self::$container->get(SocialPostAttachmentRepository::class);
}
public static function getCourseRepository(): CourseRepository
{
return self::$container->get(CourseRepository::class);

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Doctrine\DBAL\Schema\Schema;
class Version20231026221100 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Add table social_post_attachments';
}
public function up(Schema $schema): void
{
if (false === $schema->hasTable('social_post_attachments')) {
$this->addSql("CREATE TABLE social_post_attachments (id INT AUTO_INCREMENT NOT NULL, social_post_id BIGINT DEFAULT NULL, resource_node_id BIGINT DEFAULT NULL, path VARCHAR(255) NOT NULL, filename LONGTEXT NOT NULL, size INT NOT NULL, sys_insert_user_id INT NOT NULL, sys_insert_datetime DATETIME NOT NULL COMMENT '(DC2Type:datetime)', sys_lastedit_user_id INT DEFAULT NULL, sys_lastedit_datetime DATETIME DEFAULT NULL COMMENT '(DC2Type:datetime)', INDEX IDX_DF2A8F34C4F2D6B1 (social_post_id), UNIQUE INDEX UNIQ_DF2A8F341BAD783F (resource_node_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB ROW_FORMAT = DYNAMIC;");
$this->addSql("ALTER TABLE social_post_attachments ADD CONSTRAINT FK_DF2A8F34C4F2D6B1 FOREIGN KEY (social_post_id) REFERENCES social_post (id) ON DELETE CASCADE;");
$this->addSql("ALTER TABLE social_post_attachments ADD CONSTRAINT FK_DF2A8F341BAD783F FOREIGN KEY (resource_node_id) REFERENCES resource_node (id) ON DELETE CASCADE;");
}
}
}

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/* For licensing terms, see /license.txt */
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
use Chamilo\CoreBundle\Entity\SocialPost;
use Chamilo\CoreBundle\Entity\SocialPostAttachment;
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
use Chamilo\CoreBundle\Repository\Node\SocialPostAttachmentRepository;
use Chamilo\CoreBundle\Repository\Node\UserRepository;
use DirectoryIterator;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Schema\Schema;
use Symfony\Component\HttpFoundation\File\UploadedFile;
final class Version20231026231100 extends AbstractMigrationChamilo
{
public function getDescription(): string
{
return 'Migrate message_attachments to social_post_attachments';
}
public function up(Schema $schema): void
{
$container = $this->getContainer();
$em = $this->getEntityManager();
/** @var Connection $connection */
$connection = $em->getConnection();
$kernel = $container->get('kernel');
$rootPath = $kernel->getProjectDir();
$repo = $container->get(SocialPostAttachmentRepository::class);
$userRepo = $container->get(UserRepository::class);
$admin = $this->getAdmin();
$sub = $em->createQueryBuilder();
$sub->select('sp.id')
->from('Chamilo\CoreBundle\Entity\SocialPost', 'sp');
$qb = $em->createQueryBuilder();
$qb->select('ma')
->from('Chamilo\CoreBundle\Entity\MessageAttachment', 'ma')
->where($qb->expr()->in('ma.message', $sub->getDQL()));
$query = $qb->getQuery();
$messageAttachments = $query->getResult();
foreach ($messageAttachments as $attachment) {
$message = $attachment->getMessage();
if ($message) {
$messageId = $message->getId();
$filename = $attachment->getFilename();
$rootDir = $rootPath.'/app/upload/users';
$targetFile = $attachment->getPath();
$foundFilePath = $this->findFileRecursively($rootDir, $targetFile);
if ($foundFilePath) {
echo "Archivo encontrado en: " . $foundFilePath;
$mimeType = mime_content_type($foundFilePath);
$uploadFile = new UploadedFile($foundFilePath, $filename, $mimeType, null, true);
$socialPost = $em->getRepository(SocialPost::class)->find($messageId);
$attachment = new SocialPostAttachment();
$attachment->setSocialPost($socialPost);
$attachment->setPath(uniqid('social_post', true));
$attachment->setFilename($uploadFile->getClientOriginalName());
$attachment->setSize($uploadFile->getSize());
$attachment->setInsertUserId($admin->getId());
$attachment->setInsertDateTime(new \DateTime('now', new \DateTimeZone('UTC')));
$attachment->setParent($admin);
$attachment->addUserLink($admin);
$attachment->setCreator($admin);
$em->persist($attachment);
$em->flush();
$repo->addFile($attachment, $uploadFile);
}
}
}
}
private function findFileRecursively(string $directory, string $targetFile): ?string
{
if (!is_dir($directory)) {
return null;
}
foreach (new DirectoryIterator($directory) as $fileInfo) {
if ($fileInfo->isDot()) {
continue;
}
$filePath = $fileInfo->getPathname();
if ($fileInfo->isDir()) {
$result = $this->findFileRecursively($filePath, $targetFile);
if ($result !== null) {
return $result;
}
} else {
if (str_contains($fileInfo->getFilename(), $targetFile)) {
return $filePath;
}
}
}
return null;
}
}

@ -0,0 +1,19 @@
<?php
/* For licensing terms, see /license.txt */
declare(strict_types=1);
namespace Chamilo\CoreBundle\Repository\Node;
use Chamilo\CoreBundle\Entity\SocialPostAttachment;
use Chamilo\CoreBundle\Repository\ResourceRepository;
use Doctrine\Persistence\ManagerRegistry;
class SocialPostAttachmentRepository extends ResourceRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, SocialPostAttachment::class);
}
}

@ -8,6 +8,7 @@ namespace Chamilo\CoreBundle\Tool;
use Chamilo\CoreBundle\Entity\MessageAttachment;
use Chamilo\CoreBundle\Entity\PersonalFile;
use Chamilo\CoreBundle\Entity\SocialPostAttachment;
use Chamilo\CoreBundle\Entity\TicketMessageAttachment;
class User extends AbstractTool implements ToolInterface
@ -43,6 +44,7 @@ class User extends AbstractTool implements ToolInterface
'files' => PersonalFile::class,
'message_attachments' => MessageAttachment::class,
'ticket_message_attachments' => TicketMessageAttachment::class,
'social_post_attachments' => SocialPostAttachment::class,
];
}
}

Loading…
Cancel
Save