Commit 470f667a authored by Emma's avatar Emma 😻

use DTOs for forums

parent 74b3744a
......@@ -9,6 +9,7 @@ use Raddit\AppBundle\Form\ForumAppearanceType;
use Raddit\AppBundle\Form\ForumBanType;
use Raddit\AppBundle\Form\ForumType;
use Raddit\AppBundle\Form\Model\ForumBanData;
use Raddit\AppBundle\Form\Model\ForumData;
use Raddit\AppBundle\Form\ModeratorType;
use Raddit\AppBundle\Form\PasswordConfirmType;
use Raddit\AppBundle\Repository\ForumBanRepository;
......@@ -59,14 +60,13 @@ final class ForumController extends Controller {
* @return Response
*/
public function createForumAction(Request $request, EntityManager $em) {
$forum = new Forum();
$data = new ForumData();
$form = $this->createForm(ForumType::class, $forum);
$form = $this->createForm(ForumType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$forum->addUserAsModerator($this->getUser());
$forum->subscribe($this->getUser());
$forum = $data->toForum($this->getUser());
$em->persist($forum);
$em->flush();
......@@ -96,10 +96,14 @@ final class ForumController extends Controller {
* @return Response
*/
public function editForumAction(Request $request, Forum $forum, EntityManager $em) {
$form = $this->createForm(ForumType::class, $forum);
$data = ForumData::createFromForum($forum);
$form = $this->createForm(ForumType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data->updateForum($forum);
$em->flush();
$this->addFlash('success', 'flash.forum_updated');
......@@ -286,10 +290,14 @@ final class ForumController extends Controller {
* @return Response
*/
public function appearanceAction(Forum $forum, Request $request, EntityManager $em) {
$form = $this->createForm(ForumAppearanceType::class, $forum);
$data = ForumData::createFromForum($forum);
$form = $this->createForm(ForumAppearanceType::class, $data);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$data->updateForum($forum);
$em->flush();
return $this->redirectToRoute('raddit_app_forum_appearance', [
......
......@@ -14,12 +14,15 @@ class LoadExampleForums extends AbstractFixture implements DependentFixtureInter
*/
public function load(ObjectManager $manager) {
foreach ($this->provideForums() as $data) {
$forum = new Forum();
$forum = new Forum(
$data['name'],
$data['title'],
$data['description'],
$data['sidebar'],
null,
$data['created']
);
$forum->setName($data['name']);
$forum->setTitle($data['title']);
$forum->setSidebar($data['sidebar']);
$forum->setCreated($data['created']);
$forum->setFeatured($data['featured']);
foreach ($data['moderators'] as $username) {
......@@ -28,6 +31,12 @@ class LoadExampleForums extends AbstractFixture implements DependentFixtureInter
$forum->addUserAsModerator($user);
}
foreach ($data['subscribers'] as $username) {
/** @var User $user */
$user = $this->getReference('user-'.$username);
$forum->subscribe($user);
}
$this->addReference('forum-'.$data['name'], $forum);
$manager->persist($forum);
......@@ -41,7 +50,9 @@ class LoadExampleForums extends AbstractFixture implements DependentFixtureInter
'name' => 'cats',
'title' => 'Cat Memes',
'sidebar' => 'le memes',
'description' => 'memes',
'moderators' => ['emma', 'zach'],
'subscribers' => ['emma', 'zach', 'third'],
'created' => new \DateTime('2017-04-20 13:12'),
'featured' => true,
];
......@@ -50,7 +61,9 @@ class LoadExampleForums extends AbstractFixture implements DependentFixtureInter
'name' => 'news',
'title' => 'News',
'sidebar' => "Discussion of current events\n\n### Rules\n\n* rulez go here",
'description' => 'Discussion of current events',
'moderators' => ['zach'],
'subscribers' => ['zach'],
'created' => new \DateTime('2017-01-01 00:00'),
'featured' => false,
];
......
......@@ -9,8 +9,6 @@ use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping as ORM;
use Pagerfanta\Adapter\DoctrineSelectableAdapter;
use Pagerfanta\Pagerfanta;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
/**
* aka Subraddit.
......@@ -21,8 +19,6 @@ use Symfony\Component\Validator\Constraints as Assert;
* }, uniqueConstraints={
* @ORM\UniqueConstraint(name="uniq_fe5e5ab8d69c0128", columns={"canonical_name"})
* })
*
* @UniqueEntity("canonicalName", errorPath="name")
*/
class Forum {
/**
......@@ -30,17 +26,13 @@ class Forum {
* @ORM\GeneratedValue(strategy="AUTO")
* @ORM\Id()
*
* @var int
* @var int|null
*/
private $id;
/**
* @ORM\Column(type="text", unique=true)
*
* @Assert\NotBlank()
* @Assert\Length(min=3, max=25)
* @Assert\Regex("/^\w+$/", message="The name must contain only contain letters, numbers, and underscores.")
*
* @var string
*/
private $name;
......@@ -55,9 +47,6 @@ class Forum {
/**
* @ORM\Column(type="text")
*
* @Assert\Length(max=100)
* @Assert\NotBlank()
*
* @var string
*/
private $title;
......@@ -65,9 +54,6 @@ class Forum {
/**
* @ORM\Column(type="text", nullable=true)
*
* @Assert\Length(max=300)
* @Assert\NotBlank()
*
* @var string|null
*/
private $description;
......@@ -75,10 +61,7 @@ class Forum {
/**
* @ORM\Column(type="text", nullable=true)
*
* @Assert\Length(max=1500)
* @Assert\NotBlank()
*
* @var string|null
* @var string
*/
private $sidebar;
......@@ -139,54 +122,55 @@ class Forum {
*/
private $theme;
public function __construct() {
$this->created = new \DateTime('@'.time());
public function __construct(
string $name,
string $title,
string $description,
string $sidebar,
User $user = null,
\DateTime $created = null
) {
$this->setName($name);
$this->title = $title;
$this->description = $description;
$this->sidebar = $sidebar;
$this->created = $created ?: new \DateTime('@'.time());
$this->bans = new ArrayCollection();
$this->moderators = new ArrayCollection();
$this->submissions = new ArrayCollection();
$this->subscriptions = new ArrayCollection();
if ($user) {
$this->addUserAsModerator($user);
$this->subscribe($user);
}
}
/**
* @return int
* @return int|null
*/
public function getId() {
return $this->id;
}
/**
* @return string
*/
public function getName() {
public function getName(): string {
return $this->name;
}
/**
* @param string $name
*/
public function setName($name) {
public function setName(string $name) {
$this->name = $name;
$this->canonicalName = mb_strtolower($name, 'UTF-8');
$this->canonicalName = self::canonicalizeName($name);
}
/**
* @return string
*/
public function getCanonicalName() {
return $this->canonicalName;
}
/**
* @return string
*/
public function getTitle() {
public function getTitle(): string {
return $this->title;
}
/**
* @param string $title
*/
public function setTitle($title) {
public function setTitle(string $title) {
$this->title = $title;
}
......@@ -204,17 +188,11 @@ class Forum {
$this->description = $description;
}
/**
* @return string|null
*/
public function getSidebar() {
public function getSidebar(): string {
return $this->sidebar;
}
/**
* @param string|null $sidebar
*/
public function setSidebar($sidebar) {
public function setSidebar(string $sidebar) {
$this->sidebar = $sidebar;
}
......@@ -241,7 +219,7 @@ class Forum {
return $moderators;
}
public function userIsModerator($user, $adminsAreMods = true): bool {
public function userIsModerator($user, bool $adminsAreMods = true): bool {
if (!$user instanceof User) {
return false;
}
......@@ -285,20 +263,10 @@ class Forum {
return $this->submissions;
}
/**
* @return \DateTime
*/
public function getCreated() {
public function getCreated(): \DateTime {
return $this->created;
}
/**
* @param \DateTime $created
*/
public function setCreated($created) {
$this->created = $created;
}
/**
* @return ForumSubscription[]|Collection|Selectable
*/
......@@ -306,7 +274,7 @@ class Forum {
return $this->subscriptions;
}
public function isSubscribed(User $user) {
public function isSubscribed(User $user): bool {
$criteria = Criteria::create()
->where(Criteria::expr()->eq('user', $user));
......@@ -328,7 +296,7 @@ class Forum {
$this->subscriptions->removeElement($subscription);
}
public function userIsBanned(User $user) {
public function userIsBanned(User $user): bool {
if ($user->isAdmin()) {
// should we check for mod permissions too?
return false;
......@@ -382,16 +350,10 @@ class Forum {
}
}
/**
* @return bool
*/
public function isFeatured(): bool {
return $this->featured;
}
/**
* @param bool $featured
*/
public function setFeatured(bool $featured) {
$this->featured = $featured;
}
......@@ -423,4 +385,8 @@ class Forum {
public function setTheme($theme) {
$this->theme = $theme;
}
public static function canonicalizeName(string $name): string {
return mb_strtolower($name, 'UTF-8');
}
}
......@@ -5,6 +5,7 @@ namespace Raddit\AppBundle\Form;
use Doctrine\ORM\EntityRepository;
use Raddit\AppBundle\Entity\Forum;
use Raddit\AppBundle\Entity\Theme;
use Raddit\AppBundle\Form\Model\ForumData;
use Raddit\AppBundle\Form\Type\UuidAwareEntityType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
......@@ -40,7 +41,7 @@ class ForumAppearanceType extends AbstractType {
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Forum::class,
'data_class' => ForumData::class,
'validation_groups' => ['appearance'],
]);
}
......
......@@ -4,9 +4,8 @@ namespace Raddit\AppBundle\Form;
use Doctrine\ORM\EntityRepository;
use Eo\HoneypotBundle\Form\Type\HoneypotType;
use Raddit\AppBundle\Entity\Forum;
use Raddit\AppBundle\Entity\ForumCategory;
use Raddit\AppBundle\Entity\Theme;
use Raddit\AppBundle\Form\Model\ForumData;
use Raddit\AppBundle\Form\Type\MarkdownType;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
......@@ -15,6 +14,7 @@ use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
......@@ -36,7 +36,9 @@ final class ForumType extends AbstractType {
$builder->add('email', HoneypotType::class);
}
$editing = $builder->getData() && $builder->getData()->getId() !== null;
/* @var ForumData $data */
$data = $builder->getData();
$editing = $data && $data->getEntityId();
$builder
->add('name', TextType::class)
......@@ -73,9 +75,14 @@ final class ForumType extends AbstractType {
*/
public function configureOptions(OptionsResolver $resolver) {
$resolver->setDefaults([
'data_class' => Forum::class,
'data_class' => ForumData::class,
'label_format' => 'forum_form.%name%',
'honeypot' => true,
'validation_groups' => function (FormInterface $form) {
$editing = $form->getData() && $form->getData()->getEntityId();
return $editing ? ['edit'] : ['create'];
}
]);
$resolver->setAllowedTypes('honeypot', ['bool']);
......
<?php
namespace Raddit\AppBundle\Form\Model;
use Raddit\AppBundle\Entity\Forum;
use Raddit\AppBundle\Entity\User;
use Raddit\AppBundle\Validator\Constraints\UniqueForum;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @UniqueForum(groups={"create", "edit"})
*/
class ForumData {
private $entityId;
/**
* @Assert\NotBlank(groups={"create", "edit"})
* @Assert\Length(min=3, max=25, groups={"create", "edit"})
* @Assert\Regex("/^\w+$/",
* message="The name must contain only contain letters, numbers, and underscores.",
* groups={"create", "edit"}
* )
*/
private $name;
/**
* @Assert\Length(max=100, groups={"create", "edit"})
* @Assert\NotBlank(groups={"create", "edit"})
*/
private $title;
/**
* @Assert\Length(max=1500, groups={"create", "edit"})
* @Assert\NotBlank(groups={"create", "edit"})
*/
private $sidebar;
/**
* @Assert\Length(max=300, groups={"create", "edit"})
* @Assert\NotBlank(groups={"create", "edit"})
*/
private $description;
private $featured = false;
private $theme;
private $category;
public static function createFromForum(Forum $forum): self {
$self = new self();
$self->entityId = $forum->getId();
$self->name = $forum->getName();
$self->title = $forum->getTitle();
$self->sidebar = $forum->getSidebar();
$self->description = $forum->getDescription();
$self->featured = $forum->isFeatured();
$self->theme = $forum->getTheme();
$self->category = $forum->getCategory();
return $self;
}
public function toForum(User $user): Forum {
$forum = new Forum(
$this->name,
$this->title,
$this->description,
$this->sidebar,
$user
);
$forum->setFeatured($this->featured);
$forum->setTheme($this->theme);
$forum->setCategory($this->category);
return $forum;
}
public function updateForum(Forum $forum) {
$forum->setName($this->name);
$forum->setTitle($this->title);
$forum->setSidebar($this->sidebar);
$forum->setDescription($this->description);
$forum->setFeatured($this->featured);
$forum->setTheme($this->theme);
$forum->setCategory($this->category);
}
public function getEntityId() {
return $this->entityId;
}
public function getName() {
return $this->name;
}
public function setName($name) {
$this->name = $name;
}
public function getTitle() {
return $this->title;
}
public function setTitle($title) {
$this->title = $title;
}
public function getSidebar() {
return $this->sidebar;
}
public function setSidebar($sidebar) {
$this->sidebar = $sidebar;
}
public function getDescription() {
return $this->description;
}
public function setDescription($description) {
$this->description = $description;
}
public function isFeatured(): bool {
return $this->featured;
}
public function setFeatured(bool $featured) {
$this->featured = $featured;
}
public function getTheme() {
return $this->theme;
}
public function setTheme($theme) {
$this->theme = $theme;
}
public function getCategory() {
return $this->category;
}
public function setCategory($category) {
$this->category = $category;
}
}
......@@ -116,7 +116,7 @@ final class ForumRepository extends EntityRepository {
->where('f.name = ?1')
->orWhere('f.canonicalName = ?2')
->setParameter(1, $name)
->setParameter(2, mb_strtolower($name, 'UTF-8'))
->setParameter(2, Forum::canonicalizeName($name))
->getQuery()
->getOneOrNullResult();
}
......
......@@ -14,3 +14,4 @@
'@import syntax is not allowed on line {{ line }}': '@import syntax is not allowed on line {{ line }}'
'You cannot post more. Wait a while before trying again.': 'You cannot post more. Wait a while before trying again.'
'That name is already taken.': 'That name is already taken.'
'A forum by that name already exists.': 'A forum by that name already exists.'
<?php
namespace Raddit\AppBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
* @Target("CLASS")
*/
class UniqueForum extends Constraint {
const NOT_UNIQUE_ERROR = '8b7e0994-0e6e-4ffb-a350-6b3294ac7985';
protected static $errorNames = [
self::NOT_UNIQUE_ERROR => 'NOT_UNIQUE_ERROR',
];
public $message = 'A forum by that name already exists.';
/**
* {@inheritdoc}
*/
public function getTargets() {
return [self::CLASS_CONSTRAINT];
}
}
<?php
namespace Raddit\AppBundle\Validator\Constraints;
use Doctrine\Common\Collections\Criteria;
use Raddit\AppBundle\Entity\Forum;
use Raddit\AppBundle\Form\Model\ForumData;
use Raddit\AppBundle\Repository\ForumRepository;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
/**
* @see UniqueForum
*/
class UniqueForumValidator extends ConstraintValidator {
/**
* @var ForumRepository
*/
private $repository;
public function __construct(ForumRepository $repository) {
$this->repository = $repository;
}
/**
* {@inheritdoc}
*/
public function validate($value, Constraint $constraint) {
if (!$value instanceof ForumData) {
throw new UnexpectedTypeException($value, ForumData::class);
}
if (!$constraint instanceof UniqueForum) {
throw new UnexpectedTypeException($constraint, UniqueForum::class);
}
$criteria = Criteria::create()
->where(Criteria::expr()->eq('canonicalName', Forum::canonicalizeName($value->getName())))
->andWhere(Criteria::expr()->neq('id', $value->getEntityId()));
if (count($this->repository->matching($criteria)) > 0) {
$this->context->buildViolation($constraint->message)
->setCode(UniqueForum::NOT_UNIQUE_ERROR)
->atPath('name')
->addViolation();
}
}
}
......@@ -38,6 +38,6 @@ class ForumControllerTest extends WebTestCase {
$form = $crawler->filter('.subscribe-button--subscribe')->form();
$crawler = $client->submit($form);
$this->assertCount(1, $crawler->filter('.subscribe-button--unsubscribe'));
$this->assertCount(2, $crawler->filter('.subscribe-button--unsubscribe'));
}
}
......@@ -9,111 +9,107 @@ use Raddit\AppBundle\Entity\User;
use Symfony\Component\Security\Core\User\UserInterface;
class ForumTest extends TestCase {
/**
* @var Forum
*/
private $forum;
protected function setUp() {
$this->forum = new Forum('name', 'title', 'description', 'sidebar');
}
/**
* @dataProvider nonPrivilegedProvider
*/
public function testRandomsAreNotModerators($nonPrivilegedUser) {
$forum = new Forum();
$this->assertFalse($forum->userIsModerator($nonPrivilegedUser));
$this->assertFalse($this->forum->userIsModerator($nonPrivilegedUser));
}
public function testModeratorsAreModerators() {
$forum = new Forum();
$user = new User();
$forum->addUserAsModerator($user);
$this->forum->addUserAsModerator($user);
$admin = new User();
$admin->setAdmin(true);
$forum->addUserAsModerator($admin);
$this->forum->addUserAsModerator($admin);
$this->assertTrue($forum->userIsModerator($user));
$this->assertTrue($forum->userIsModerator($admin));
$this->assertTrue($this->forum->userIsModerator($user));
$this->assertTrue($this->forum->userIsModerator($admin));
}
public function testAdminsAreNotModeratorsWithFlag() {
$user = new User();
$user->setAdmin(true);
$forum = new Forum();
$this->assertFalse($forum->userIsModerator($user, false));
$this->assertFalse($this->forum->userIsModerator($user, false));
}
/**
* @dataProvider nonPrivilegedProvider
*/
public function testRandomsCanNotDeleteForum($nonPrivilegedUser) {
$forum = new Forum();
$this->assertFalse($forum->userCanDelete($nonPrivilegedUser));
$this->assertFalse($this->forum->userCanDelete($nonPrivilegedUser));
}
public function testAdminCanDeleteEmptyForum() {
$user = new User();