mirror of
https://github.com/wallabag/wallabag.git
synced 2024-12-25 17:10:29 +00:00
Merge pull request #1653 from wallabag/v2-annotator-comments
V2 annotator comments
This commit is contained in:
commit
162954763e
27 changed files with 891 additions and 13 deletions
|
@ -35,6 +35,7 @@ class AppKernel extends Kernel
|
|||
new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
|
||||
new Craue\ConfigBundle\CraueConfigBundle(),
|
||||
new Lexik\Bundle\MaintenanceBundle\LexikMaintenanceBundle(),
|
||||
new Wallabag\AnnotationBundle\WallabagAnnotationBundle(),
|
||||
];
|
||||
|
||||
if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
wallabag_annotation:
|
||||
type : rest
|
||||
resource: "@WallabagAnnotationBundle/Resources/config/routing_annotations.yml"
|
||||
|
||||
wallabag_import:
|
||||
resource: "@WallabagImportBundle/Controller/"
|
||||
type: annotation
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
Rest_Wallabag:
|
||||
type : rest
|
||||
resource: "@WallabagApiBundle/Resources/config/routing_rest.yml"
|
||||
|
||||
|
|
|
@ -58,4 +58,5 @@ security:
|
|||
- { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
|
||||
- { path: ^/settings, roles: ROLE_SUPER_ADMIN }
|
||||
- { path: ^/annotations, roles: ROLE_USER }
|
||||
- { path: ^/, roles: ROLE_USER }
|
||||
|
|
|
@ -29,6 +29,7 @@ The main documentation for this application is organized into a couple sections:
|
|||
user/configuration
|
||||
user/first_article
|
||||
user/errors_during_fetching
|
||||
user/annotations
|
||||
user/import
|
||||
user/download_articles
|
||||
user/filters
|
||||
|
|
24
docs/en/user/annotations.rst
Normal file
24
docs/en/user/annotations.rst
Normal file
|
@ -0,0 +1,24 @@
|
|||
Annotations
|
||||
===========
|
||||
|
||||
In each article you read, you can write annotations. It's easier to understand with some pictures.
|
||||
|
||||
Select the part of the article that you want to annotate and click on the pencil:
|
||||
|
||||
.. image:: ../../img/user/annotations_1.png
|
||||
:alt: Select your text
|
||||
:align: center
|
||||
|
||||
Then, write your annotation:
|
||||
|
||||
.. image:: ../../img/user/annotations_2.png
|
||||
:alt: Write your annotation
|
||||
:align: center
|
||||
|
||||
The text is now highlighted and you can read your annotation if you move the mouse cursor over it.
|
||||
|
||||
.. image:: ../../img/user/annotations_3.png
|
||||
:alt: Read your annotation
|
||||
:align: center
|
||||
|
||||
You can create as many annotations as you wish.
|
|
@ -30,6 +30,7 @@ La documentation principale de cette application est découpée en plusieurs sec
|
|||
user/configuration
|
||||
user/first_article
|
||||
user/errors_during_fetching
|
||||
user/annotations
|
||||
user/import
|
||||
user/download_articles
|
||||
user/filters
|
||||
|
|
25
docs/fr/user/annotations.rst
Normal file
25
docs/fr/user/annotations.rst
Normal file
|
@ -0,0 +1,25 @@
|
|||
Annotations
|
||||
===========
|
||||
|
||||
Sur chaque article que vous lisez, vous pouvez écrire des annotations. Puisqu'une image vaut mieux qu'un long discours,
|
||||
voici ce que ça donne.
|
||||
|
||||
Sélectionnez la zone du texte que vous souhaitez annoter et cliquez sur le crayon :
|
||||
|
||||
.. image:: ../../img/user/annotations_1.png
|
||||
:alt: Sélectionnez votre texte
|
||||
:align: center
|
||||
|
||||
Ensuite, écrivez votre annotation :
|
||||
|
||||
.. image:: ../../img/user/annotations_2.png
|
||||
:alt: Écrivez votre annotation
|
||||
:align: center
|
||||
|
||||
Le texte est maintenant surligné et vous pouvez lire le annotation en le survolant avec votre souris.
|
||||
|
||||
.. image:: ../../img/user/annotations_3.png
|
||||
:alt: Lisez votre annotation
|
||||
:align: center
|
||||
|
||||
Vous pouvez créer autant de annotations que vous le souhaitez.
|
BIN
docs/img/user/annotations_1.png
Normal file
BIN
docs/img/user/annotations_1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
docs/img/user/annotations_2.png
Normal file
BIN
docs/img/user/annotations_2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
BIN
docs/img/user/annotations_3.png
Normal file
BIN
docs/img/user/annotations_3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\Controller;
|
||||
|
||||
use FOS\RestBundle\Controller\FOSRestController;
|
||||
use Nelmio\ApiDocBundle\Annotation\ApiDoc;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
|
||||
class WallabagAnnotationController extends FOSRestController
|
||||
{
|
||||
/**
|
||||
* Retrieve annotations for an entry.
|
||||
*
|
||||
* @ApiDoc(
|
||||
* requirements={
|
||||
* {"name"="entry", "dataType"="integer", "requirement"="\w+", "description"="The entry ID"}
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getAnnotationsAction(Entry $entry)
|
||||
{
|
||||
$annotationRows = $this
|
||||
->getDoctrine()
|
||||
->getRepository('WallabagAnnotationBundle:Annotation')
|
||||
->findAnnotationsByPageId($entry->getId(), $this->getUser()->getId());
|
||||
$total = count($annotationRows);
|
||||
$annotations = array('total' => $total, 'rows' => $annotationRows);
|
||||
|
||||
$json = $this->get('serializer')->serialize($annotations, 'json');
|
||||
|
||||
return $this->renderJsonResponse($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new annotation.
|
||||
*
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @ApiDoc(
|
||||
* requirements={
|
||||
* {"name"="ranges", "dataType"="array", "requirement"="\w+", "description"="The range array for the annotation"},
|
||||
* {"name"="quote", "dataType"="string", "required"=false, "description"="Optional, quote for the annotation"},
|
||||
* {"name"="text", "dataType"="string", "required"=true, "description"=""},
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function postAnnotationAction(Request $request, Entry $entry)
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
|
||||
$annotation = new Annotation($this->getUser());
|
||||
|
||||
$annotation->setText($data['text']);
|
||||
if (array_key_exists('quote', $data)) {
|
||||
$annotation->setQuote($data['quote']);
|
||||
}
|
||||
if (array_key_exists('ranges', $data)) {
|
||||
$annotation->setRanges($data['ranges']);
|
||||
}
|
||||
|
||||
$annotation->setEntry($entry);
|
||||
|
||||
$em->persist($annotation);
|
||||
$em->flush();
|
||||
|
||||
$json = $this->get('serializer')->serialize($annotation, 'json');
|
||||
|
||||
return $this->renderJsonResponse($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an annotation.
|
||||
*
|
||||
* @ApiDoc(
|
||||
* requirements={
|
||||
* {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function putAnnotationAction(Annotation $annotation, Request $request)
|
||||
{
|
||||
$data = json_decode($request->getContent(), true);
|
||||
|
||||
if (!is_null($data['text'])) {
|
||||
$annotation->setText($data['text']);
|
||||
}
|
||||
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->flush();
|
||||
|
||||
$json = $this->get('serializer')->serialize($annotation, 'json');
|
||||
|
||||
return $this->renderJsonResponse($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes an annotation.
|
||||
*
|
||||
* @ApiDoc(
|
||||
* requirements={
|
||||
* {"name"="annotation", "dataType"="string", "requirement"="\w+", "description"="The annotation ID"}
|
||||
* }
|
||||
* )
|
||||
*
|
||||
* @ParamConverter("annotation", class="WallabagAnnotationBundle:Annotation")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAnnotationAction(Annotation $annotation)
|
||||
{
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->remove($annotation);
|
||||
$em->flush();
|
||||
|
||||
$json = $this->get('serializer')->serialize($annotation, 'json');
|
||||
|
||||
return $this->renderJsonResponse($json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a JSON Response.
|
||||
* We don't use the Symfony JsonRespone, because it takes an array as parameter instead of a JSON string.
|
||||
*
|
||||
* @param string $json
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
private function renderJsonResponse($json, $code = 200)
|
||||
{
|
||||
return new Response($json, $code, array('application/json'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
|
||||
class LoadAnnotationData extends AbstractFixture implements OrderedFixtureInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$annotation1 = new Annotation($this->getReference('admin-user'));
|
||||
$annotation1->setEntry($this->getReference('entry1'));
|
||||
$annotation1->setText('This is my annotation /o/');
|
||||
$annotation1->setQuote('content');
|
||||
|
||||
$manager->persist($annotation1);
|
||||
|
||||
$this->addReference('annotation1', $annotation1);
|
||||
|
||||
$annotation2 = new Annotation($this->getReference('admin-user'));
|
||||
$annotation2->setEntry($this->getReference('entry2'));
|
||||
$annotation2->setText('This is my 2nd annotation /o/');
|
||||
$annotation2->setQuote('content');
|
||||
|
||||
$manager->persist($annotation2);
|
||||
|
||||
$this->addReference('annotation2', $annotation2);
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getOrder()
|
||||
{
|
||||
return 35;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
|
||||
use Symfony\Component\Config\Definition\ConfigurationInterface;
|
||||
|
||||
class Configuration implements ConfigurationInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfigTreeBuilder()
|
||||
{
|
||||
$treeBuilder = new TreeBuilder();
|
||||
$rootNode = $treeBuilder->root('wallabag_annotation');
|
||||
|
||||
return $treeBuilder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
|
||||
|
||||
class WallabagAnnotationExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container)
|
||||
{
|
||||
$configuration = new Configuration();
|
||||
$config = $this->processConfiguration($configuration, $configs);
|
||||
}
|
||||
}
|
270
src/Wallabag/AnnotationBundle/Entity/Annotation.php
Normal file
270
src/Wallabag/AnnotationBundle/Entity/Annotation.php
Normal file
|
@ -0,0 +1,270 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use JMS\Serializer\Annotation\ExclusionPolicy;
|
||||
use JMS\Serializer\Annotation\Exclude;
|
||||
use JMS\Serializer\Annotation\VirtualProperty;
|
||||
use JMS\Serializer\Annotation\SerializedName;
|
||||
use Wallabag\UserBundle\Entity\User;
|
||||
use Wallabag\CoreBundle\Entity\Entry;
|
||||
|
||||
/**
|
||||
* Annotation.
|
||||
*
|
||||
* @ORM\Table(name="annotation")
|
||||
* @ORM\Entity(repositoryClass="Wallabag\AnnotationBundle\Repository\AnnotationRepository")
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
* @ExclusionPolicy("none")
|
||||
*/
|
||||
class Annotation
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="text", type="text")
|
||||
*/
|
||||
private $text;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="created_at", type="datetime")
|
||||
*/
|
||||
private $createdAt;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(name="updated_at", type="datetime")
|
||||
*/
|
||||
private $updatedAt;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="quote", type="string")
|
||||
*/
|
||||
private $quote;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*
|
||||
* @ORM\Column(name="ranges", type="array")
|
||||
*/
|
||||
private $ranges;
|
||||
|
||||
/**
|
||||
* @Exclude
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Wallabag\UserBundle\Entity\User")
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @Exclude
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\Entry", inversedBy="annotations")
|
||||
* @ORM\JoinColumn(name="entry_id", referencedColumnName="id")
|
||||
*/
|
||||
private $entry;
|
||||
|
||||
/*
|
||||
* @param User $user
|
||||
*/
|
||||
public function __construct(\Wallabag\UserBundle\Entity\User $user)
|
||||
{
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set text.
|
||||
*
|
||||
* @param string $text
|
||||
*
|
||||
* @return Annotation
|
||||
*/
|
||||
public function setText($text)
|
||||
{
|
||||
$this->text = $text;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get text.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getText()
|
||||
{
|
||||
return $this->text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\PrePersist
|
||||
* @ORM\PreUpdate
|
||||
*/
|
||||
public function timestamps()
|
||||
{
|
||||
if (is_null($this->createdAt)) {
|
||||
$this->createdAt = new \DateTime();
|
||||
}
|
||||
$this->updatedAt = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get created.
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getCreatedAt()
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get updated.
|
||||
*
|
||||
* @return \DateTime
|
||||
*/
|
||||
public function getUpdatedAt()
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quote.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getQuote()
|
||||
{
|
||||
return $this->quote;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set quote.
|
||||
*
|
||||
* @param string $quote
|
||||
*
|
||||
* @return Annotation
|
||||
*/
|
||||
public function setQuote($quote)
|
||||
{
|
||||
$this->quote = $quote;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ranges.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRanges()
|
||||
{
|
||||
return $this->ranges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set ranges.
|
||||
*
|
||||
* @param array $ranges
|
||||
*
|
||||
* @return Annotation
|
||||
*/
|
||||
public function setRanges($ranges)
|
||||
{
|
||||
$this->ranges = $ranges;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user.
|
||||
*
|
||||
* @param string $user
|
||||
*
|
||||
* @return Annotation
|
||||
*/
|
||||
public function setUser($user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @VirtualProperty
|
||||
* @SerializedName("user")
|
||||
*/
|
||||
public function getUserName()
|
||||
{
|
||||
return $this->user->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set entry.
|
||||
*
|
||||
* @param Entry $entry
|
||||
*
|
||||
* @return Annotation
|
||||
*/
|
||||
public function setEntry($entry)
|
||||
{
|
||||
$this->entry = $entry;
|
||||
$entry->setAnnotation($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry.
|
||||
*
|
||||
* @return Entry
|
||||
*/
|
||||
public function getEntry()
|
||||
{
|
||||
return $this->entry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @VirtualProperty
|
||||
* @SerializedName("annotator_schema_version")
|
||||
*/
|
||||
public function getVersion()
|
||||
{
|
||||
return 'v1.0';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\Repository;
|
||||
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
/**
|
||||
* AnnotationRepository.
|
||||
*/
|
||||
class AnnotationRepository extends EntityRepository
|
||||
{
|
||||
/**
|
||||
* Return a query builder to used by other getBuilderFor* method.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
private function getBuilderByUser($userId)
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->leftJoin('a.user', 'u')
|
||||
->andWhere('u.id = :userId')->setParameter('userId', $userId)
|
||||
->orderBy('a.id', 'desc')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all annotations for a user.
|
||||
*
|
||||
* @param int $userId
|
||||
*
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getBuilderForAllByUser($userId)
|
||||
{
|
||||
return $this
|
||||
->getBuilderByUser($userId)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get annotation for this id.
|
||||
*
|
||||
* @param int $annotationId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findAnnotationById($annotationId)
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->andWhere('a.id = :annotationId')->setParameter('annotationId', $annotationId)
|
||||
->getQuery()->getSingleResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find annotations for entry id.
|
||||
*
|
||||
* @param int $entryId
|
||||
* @param int $userId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findAnnotationsByPageId($entryId, $userId)
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->where('a.entry = :entryId')->setParameter('entryId', $entryId)
|
||||
->andwhere('a.user = :userId')->setParameter('userId', $userId)
|
||||
->getQuery()->getResult()
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find last annotation for a given entry id. Used only for tests.
|
||||
*
|
||||
* @param int $entryId
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function findLastAnnotationByPageId($entryId, $userId)
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->where('a.entry = :entryId')->setParameter('entryId', $entryId)
|
||||
->andwhere('a.user = :userId')->setParameter('userId', $userId)
|
||||
->orderBy('a.id', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
annotations:
|
||||
type: rest
|
||||
resource: "WallabagAnnotationBundle:WallabagAnnotation"
|
||||
name_prefix: annotations_
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\Tests\Controller;
|
||||
|
||||
use Wallabag\AnnotationBundle\Tests\WallabagAnnotationTestCase;
|
||||
|
||||
class AnnotationControllerTest extends WallabagAnnotationTestCase
|
||||
{
|
||||
public function testGetAnnotations()
|
||||
{
|
||||
$annotation = $this->client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagAnnotationBundle:Annotation')
|
||||
->findOneBy(array('user' => 1));
|
||||
|
||||
if (!$annotation) {
|
||||
$this->markTestSkipped('No content found in db.');
|
||||
}
|
||||
$this->logInAs('admin');
|
||||
$crawler = $this->client->request('GET', 'annotations/'.$annotation->getEntry()->getId().'.json');
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
$this->assertEquals(1, $content['total']);
|
||||
$this->assertEquals($annotation->getText(), $content['rows'][0]['text']);
|
||||
}
|
||||
|
||||
public function testSetAnnotation()
|
||||
{
|
||||
$this->logInAs('admin');
|
||||
|
||||
$entry = $this->client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagCoreBundle:Entry')
|
||||
->findOneBy(array('user' => 1));
|
||||
|
||||
$headers = array('CONTENT_TYPE' => 'application/json');
|
||||
$content = json_encode(array(
|
||||
'text' => 'my annotation',
|
||||
'quote' => 'my quote',
|
||||
'range' => '[{"start":"","startOffset":24,"end":"","endOffset":31}]',
|
||||
));
|
||||
$crawler = $this->client->request('POST', 'annotations/'.$entry->getId().'.json', array(), array(), $headers, $content);
|
||||
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$annotation = $this->client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagAnnotationBundle:Annotation')
|
||||
->findLastAnnotationByPageId($entry->getId(), 1);
|
||||
|
||||
$this->assertEquals('my annotation', $annotation->getText());
|
||||
}
|
||||
|
||||
public function testEditAnnotation()
|
||||
{
|
||||
$annotation = $this->client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagAnnotationBundle:Annotation')
|
||||
->findOneBy(array('user' => 1));
|
||||
|
||||
$this->logInAs('admin');
|
||||
|
||||
$headers = array('CONTENT_TYPE' => 'application/json');
|
||||
$content = json_encode(array(
|
||||
'text' => 'a modified annotation',
|
||||
));
|
||||
$crawler = $this->client->request('PUT', 'annotations/'.$annotation->getId().'.json', array(), array(), $headers, $content);
|
||||
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
|
||||
|
||||
$content = json_decode($this->client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertEquals('a modified annotation', $content['text']);
|
||||
|
||||
$annotationUpdated = $this->client->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository('WallabagAnnotationBundle:Annotation')
|
||||
->findAnnotationById($annotation->getId());
|
||||
$this->assertEquals('a modified annotation', $annotationUpdated->getText());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle\Tests;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\BrowserKit\Cookie;
|
||||
|
||||
abstract class WallabagAnnotationTestCase extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
protected $client = null;
|
||||
|
||||
/**
|
||||
* @var \FOS\UserBundle\Model\UserInterface
|
||||
*/
|
||||
protected $user;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
$this->client = $this->createAuthorizedClient();
|
||||
}
|
||||
|
||||
public function logInAs($username)
|
||||
{
|
||||
$crawler = $this->client->request('GET', '/login');
|
||||
$form = $crawler->filter('button[type=submit]')->form();
|
||||
$data = array(
|
||||
'_username' => $username,
|
||||
'_password' => 'mypassword',
|
||||
);
|
||||
|
||||
$this->client->submit($form, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Client
|
||||
*/
|
||||
protected function createAuthorizedClient()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$container = $client->getContainer();
|
||||
|
||||
/** @var $userManager \FOS\UserBundle\Doctrine\UserManager */
|
||||
$userManager = $container->get('fos_user.user_manager');
|
||||
/** @var $loginManager \FOS\UserBundle\Security\LoginManager */
|
||||
$loginManager = $container->get('fos_user.security.login_manager');
|
||||
$firewallName = $container->getParameter('fos_user.firewall_name');
|
||||
|
||||
$this->user = $userManager->findUserBy(array('username' => 'admin'));
|
||||
$loginManager->loginUser($firewallName, $this->user);
|
||||
|
||||
// save the login token into the session and put it in a cookie
|
||||
$container->get('session')->set('_security_'.$firewallName, serialize($container->get('security.token_storage')->getToken()));
|
||||
$container->get('session')->save();
|
||||
|
||||
$session = $container->get('session');
|
||||
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
|
||||
|
||||
return $client;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace Wallabag\AnnotationBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class WallabagAnnotationBundle extends Bundle
|
||||
{
|
||||
}
|
|
@ -9,6 +9,7 @@ use JMS\Serializer\Annotation\Groups;
|
|||
use JMS\Serializer\Annotation\XmlRoot;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Wallabag\UserBundle\Entity\User;
|
||||
use Wallabag\AnnotationBundle\Entity\Annotation;
|
||||
|
||||
/**
|
||||
* Entry.
|
||||
|
@ -98,13 +99,12 @@ class Entry
|
|||
private $updatedAt;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @ORM\OneToMany(targetEntity="Wallabag\AnnotationBundle\Entity\Annotation", mappedBy="entry", cascade={"persist", "remove"})
|
||||
* @ORM\JoinTable
|
||||
*
|
||||
* @ORM\Column(name="comments", type="text", nullable=true)
|
||||
*
|
||||
* @Groups({"export_all"})
|
||||
* @Groups({"entries_for_user", "export_all"})
|
||||
*/
|
||||
private $comments;
|
||||
private $annotations;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
|
@ -366,19 +366,19 @@ class Entry
|
|||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return ArrayCollection<Annotation>
|
||||
*/
|
||||
public function getComments()
|
||||
public function getAnnotations()
|
||||
{
|
||||
return $this->comments;
|
||||
return $this->annotations;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $comments
|
||||
* @param Annotation $annotation
|
||||
*/
|
||||
public function setComments($comments)
|
||||
public function setAnnotation(Annotation $annotation)
|
||||
{
|
||||
$this->comments = $comments;
|
||||
$this->annotations[] = $annotation;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
23
src/Wallabag/CoreBundle/Resources/public/themes/_global/js/annotator.min.js
vendored
Normal file
23
src/Wallabag/CoreBundle/Resources/public/themes/_global/js/annotator.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -98,6 +98,7 @@ Toggle favorite: 'Marquer comme favori'
|
|||
Delete: 'Supprimer'
|
||||
"{0} There is no entry.|{1} There is one entry.|]1,Inf[ There are %count% entries.": "{0} Il n'y a pas d'articles.|{1} Il y a un article.|]1,Inf[ Il y a %count% articles."
|
||||
http://website: "http://siteweb"
|
||||
"{0} No annotations|{1} One annotation|]1,Inf[ %nbAnnotations% annotations": "{0} Aucune annotation|{1} Une annotation|]1,Inf[ %nbAnnotations% annotations"
|
||||
|
||||
# Edit entry
|
||||
Edit an entry: "Éditer un article"
|
||||
|
|
|
@ -38,11 +38,13 @@
|
|||
|
||||
<link rel="shortcut icon" type="image/x-icon" href="{{ asset('bundles/wallabagcore/themes/_global/img/appicon/favicon.ico') }}">
|
||||
|
||||
{% block css %}{% endblock %}
|
||||
{% block css %}
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="{{ asset('bundles/wallabagcore/themes/_global/js/jquery-2.0.3.min.js') }}"></script>
|
||||
<script src="{{ asset('bundles/wallabagcore/themes/_global/js/jquery.cookie.js') }}"></script>
|
||||
<script src="{{ asset('bundles/wallabagcore/themes/_global/js/bookmarklet.js') }}"></script>
|
||||
<script src="{{ asset('bundles/wallabagcore/themes/_global/js/annotator.min.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
<li><a href="mailto:hello@wallabag.org?subject=Wrong%20display%20in%20wallabag&body={{ entry.url|url_encode }}" title="{% trans %}Does this article appear wrong?{% endtrans %}" class="tool bad-display icon icon-delete"><span>{% trans %}Does this article appear wrong?{% endtrans %}</span></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{% set nbAnnotations = entry.annotations | length %}
|
||||
<span class="tool link mdi-communication-comment"> {% transchoice nbAnnotations %}{0} No annotations|{1} One annotation|]1,Inf[ %nbAnnotations% annotations{% endtranschoice %}</span>
|
||||
<aside class="tags">
|
||||
{% for tag in entry.tags %}
|
||||
<span class="mdi-action-label-outline">{{ tag.label }}</span> <a href="{{ path('remove_tag', { 'entry': entry.id, 'tag': tag.id }) }}"><i>✘</i></a>
|
||||
|
@ -107,5 +108,25 @@
|
|||
retrievePercent({{ entry.id }});
|
||||
});
|
||||
});
|
||||
|
||||
var app = new annotator.App();
|
||||
app.include(annotator.ui.main, {
|
||||
element: document.querySelector('article')
|
||||
});
|
||||
app.include(annotator.storage.http, {
|
||||
prefix: '',
|
||||
urls: {
|
||||
create: '{{ path('annotations_post_annotation', { 'entry': entry.id }) }}',
|
||||
update: '{{ path('annotations_put_annotation', { 'annotation': 'idAnnotation' }) }}',
|
||||
destroy: '{{ path('annotations_delete_annotation', { 'annotation': 'idAnnotation' }) }}',
|
||||
search: '{{ path('annotations_get_annotations', { 'entry': entry.id }) }}'
|
||||
}
|
||||
});
|
||||
app
|
||||
.start()
|
||||
.then(function () {
|
||||
app.annotations.load({entry: {{ entry.id }}});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
|
|
@ -177,6 +177,7 @@ main {
|
|||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
@ -186,6 +187,8 @@ main {
|
|||
</header>
|
||||
<aside>
|
||||
<a href="{{ entry.url|e }}" target="_blank" title="{% trans %}original{% endtrans %} : {{ entry.title|e }}" class="tool link mdi-content-link"> <span>{{ entry.domainName|removeWww }}</span></a>
|
||||
{% set nbAnnotations = entry.annotations | length %}
|
||||
<span class="tool link mdi-communication-comment"> {% transchoice nbAnnotations %}{0} No annotations|{1} One annotation|]1,Inf[ %nbAnnotations% annotations{% endtranschoice %}</span>
|
||||
<div id="list">
|
||||
{% for tag in entry.tags %}
|
||||
<div class="chip">
|
||||
|
@ -207,6 +210,29 @@ main {
|
|||
{{ entry.content | raw }}
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
var app = new annotator.App();
|
||||
app.include(annotator.ui.main, {
|
||||
element: document.querySelector('article')
|
||||
});
|
||||
app.include(annotator.storage.http, {
|
||||
prefix: '',
|
||||
urls: {
|
||||
create: '{{ path('annotations_post_annotation', { 'entry': entry.id }) }}',
|
||||
update: '{{ path('annotations_put_annotation', { 'annotation': 'idAnnotation' }) }}',
|
||||
destroy: '{{ path('annotations_delete_annotation', { 'annotation': 'idAnnotation' }) }}',
|
||||
search: '{{ path('annotations_get_annotations', { 'entry': entry.id }) }}'
|
||||
}
|
||||
});
|
||||
app
|
||||
.start()
|
||||
.then(function () {
|
||||
app.annotations.load({entry: {{ entry.id }}});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
|
|
Loading…
Reference in a new issue