├── .coveralls.yml ├── Resource ├── config │ └── services.yaml ├── template │ ├── default │ │ ├── complete.twig │ │ ├── review.twig │ │ ├── confirm.twig │ │ └── index.twig │ └── admin │ │ ├── config.twig │ │ ├── edit.twig │ │ └── index.twig └── locale │ └── messages.ja.yaml ├── composer.json ├── Tests ├── bootstrap.php └── Web │ ├── ProductReviewConfigControllerTest.php │ ├── ReviewControllerTest.php │ └── ReviewAdminControllerTest.php ├── Entity ├── ProductReviewStatus.php ├── ProductReviewConfig.php └── ProductReview.php ├── ProductReviewNav.php ├── Repository ├── ProductReviewStatusRepository.php ├── ProductReviewConfigRepository.php └── ProductReviewRepository.php ├── .github └── workflows │ ├── main.yml │ └── ci.yml ├── README.md ├── phpunit.xml.dist ├── Controller ├── Admin │ ├── ConfigController.php │ └── ProductReviewController.php └── ProductReviewController.php ├── Form └── Type │ ├── Admin │ ├── ProductReviewConfigType.php │ ├── ProductReviewType.php │ └── ProductReviewSearchType.php │ └── ProductReviewType.php ├── ProductReviewEvent.php ├── PluginManager.php └── LICENSE /.coveralls.yml: -------------------------------------------------------------------------------- 1 | # for php-coveralls 2 | #src_dir: src 3 | coverage_clover: coverage.clover 4 | json_path: coveralls-upload.json 5 | -------------------------------------------------------------------------------- /Resource/config/services.yaml: -------------------------------------------------------------------------------- 1 | parameters: 2 | product_review_display_count_min: 1 3 | product_review_display_count_max: 30 -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ec-cube/productreview42", 3 | "version": "4.3.0", 4 | "description": "商品レビュー管理プラグイン", 5 | "type": "eccube-plugin", 6 | "require": { 7 | "ec-cube/plugin-installer": "^2.0" 8 | }, 9 | "extra": { 10 | "code": "ProductReview42" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | load($envFile); 18 | } 19 | -------------------------------------------------------------------------------- /Entity/ProductReviewStatus.php: -------------------------------------------------------------------------------- 1 | [ 24 | 'children' => [ 25 | 'product_review' => [ 26 | 'name' => 'product_review.admin.product_review.title', 27 | 'url' => 'product_review_admin_product_review', 28 | ], 29 | ], 30 | ], 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Repository/ProductReviewStatusRepository.php: -------------------------------------------------------------------------------- 1 | find($id); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Resource/template/default/complete.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This file is part of EC-CUBE 3 | 4 | Copyright(c) LOCKON CO.,LTD. All Rights Reserved. 5 | 6 | http://www.lockon.co.jp/ 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | #} 11 | {% extends 'default_frame.twig' %} 12 | 13 | {% set body_class = 'product_review_complete' %} 14 | 15 | {% block main %} 16 |
17 |
18 |

{{ 'product_review.front.review.title'|trans}}

19 |
20 |
21 |
22 |
23 |
24 |
25 |

{{ 'product_review.front.review.complete.thanks'|trans}}

26 |
27 |

{{ 'product_review.front.review.complete.thanks_detail'|trans|nl2br }}

28 | 31 |
32 |
33 |
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ./Tests 24 | 25 | 26 | 27 | 32 | 33 | 34 | 35 | 36 | ./ 37 | 38 | ./Tests 39 | ./Resource 40 | ./PluginManager.php 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /Controller/Admin/ConfigController.php: -------------------------------------------------------------------------------- 1 | get(); 39 | $form = $this->createForm(ProductReviewConfigType::class, $Config); 40 | $form->handleRequest($request); 41 | 42 | if ($form->isSubmitted() && $form->isValid()) { 43 | $Config = $form->getData(); 44 | $this->entityManager->persist($Config); 45 | $this->entityManager->flush($Config); 46 | 47 | log_info('Product review config', ['status' => 'Success']); 48 | $this->addSuccess('product_review.admin.save.complete', 'admin'); 49 | 50 | return $this->redirectToRoute('product_review42_admin_config'); 51 | } 52 | 53 | return [ 54 | 'form' => $form->createView(), 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Form/Type/Admin/ProductReviewConfigType.php: -------------------------------------------------------------------------------- 1 | eccubeConfig = $eccubeConfig; 42 | } 43 | 44 | /** 45 | * Build form. 46 | * 47 | * @param FormBuilderInterface $builder 48 | * @param array $options 49 | */ 50 | public function buildForm(FormBuilderInterface $builder, array $options) 51 | { 52 | $min = $this->eccubeConfig['product_review_display_count_min']; 53 | $max = $this->eccubeConfig['product_review_display_count_max']; 54 | 55 | $builder 56 | ->add('review_max', IntegerType::class, [ 57 | 'constraints' => [ 58 | new Assert\NotBlank(), 59 | new Assert\Range(['min' => $min, 'max' => $max]), 60 | ], 61 | ]); 62 | } 63 | 64 | /** 65 | * Config. 66 | * 67 | * @param OptionsResolver $resolver 68 | */ 69 | public function configureOptions(OptionsResolver $resolver) 70 | { 71 | $resolver->setDefaults([ 72 | 'data_class' => ProductReviewConfig::class, 73 | ]); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /ProductReviewEvent.php: -------------------------------------------------------------------------------- 1 | productReviewConfigRepository = $productReviewConfigRepository; 54 | $this->productStatusRepository = $productStatusRepository; 55 | $this->productReviewRepository = $productReviewRepository; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public static function getSubscribedEvents() 62 | { 63 | return [ 64 | 'Product/detail.twig' => 'detail', 65 | ]; 66 | } 67 | 68 | /** 69 | * @param TemplateEvent $event 70 | */ 71 | public function detail(TemplateEvent $event) 72 | { 73 | $event->addSnippet('ProductReview42/Resource/template/default/review.twig'); 74 | 75 | $Config = $this->productReviewConfigRepository->get(); 76 | 77 | /** @var Product $Product */ 78 | $Product = $event->getParameter('Product'); 79 | 80 | $ProductReviews = $this->productReviewRepository->findBy(['Status' => ProductReviewStatus::SHOW, 'Product' => $Product], ['id' => 'DESC'], $Config->getReviewMax()); 81 | 82 | $rate = $this->productReviewRepository->getAvgAll($Product); 83 | $avg = round($rate['recommend_avg']); 84 | $count = intval($rate['review_count']); 85 | 86 | $parameters = $event->getParameters(); 87 | $parameters['ProductReviews'] = $ProductReviews; 88 | $parameters['ProductReviewAvg'] = $avg; 89 | $parameters['ProductReviewCount'] = $count; 90 | $event->setParameters($parameters); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Form/Type/Admin/ProductReviewType.php: -------------------------------------------------------------------------------- 1 | eccubeConfig = $eccubeConfig; 46 | } 47 | 48 | /** 49 | * Build form. 50 | * 51 | * @param FormBuilderInterface $builder 52 | * @param array $options 53 | */ 54 | public function buildForm(FormBuilderInterface $builder, array $options) 55 | { 56 | $config = $this->eccubeConfig; 57 | $builder 58 | ->add('Status', EntityType::class, [ 59 | 'class' => ProductReviewStatus::class, 60 | 'constraints' => [ 61 | new Assert\NotBlank(), 62 | ], 63 | ]) 64 | ->add('reviewer_name', TextType::class, [ 65 | 'constraints' => [ 66 | new Assert\NotBlank(), 67 | new Assert\Length(['max' => $config['eccube_stext_len']]), 68 | ], 69 | 'attr' => [ 70 | 'maxlength' => $config['eccube_stext_len'], 71 | ], 72 | ]) 73 | ->add('reviewer_url', TextType::class, [ 74 | 'required' => false, 75 | 'constraints' => [ 76 | new Assert\Url(), 77 | new Assert\Length(['max' => $config['eccube_url_len']]), 78 | ], 79 | 'attr' => [ 80 | 'maxlength' => $config['eccube_url_len'], 81 | ], 82 | ]) 83 | ->add('sex', SexType::class, [ 84 | 'required' => false, 85 | ]) 86 | ->add('recommend_level', ChoiceType::class, [ 87 | 'choices' => array_flip([ 88 | '5' => '★★★★★', 89 | '4' => '★★★★', 90 | '3' => '★★★', 91 | '2' => '★★', 92 | '1' => '★', 93 | ]), 94 | 'expanded' => false, 95 | 'multiple' => false, 96 | 'constraints' => [ 97 | new Assert\NotBlank(), 98 | ], 99 | ]) 100 | ->add('title', TextType::class, [ 101 | 'constraints' => [ 102 | new Assert\NotBlank(), 103 | new Assert\Length(['max' => 50]), 104 | ], 105 | 'attr' => [ 106 | 'maxlength' => $config['eccube_stext_len'], 107 | ], 108 | ]) 109 | ->add('comment', TextareaType::class, [ 110 | 'constraints' => [ 111 | new Assert\NotBlank(), 112 | new Assert\Length(['max' => $config['eccube_ltext_len']]), 113 | ], 114 | 'attr' => [ 115 | 'maxlength' => $config['eccube_ltext_len'], 116 | ], 117 | ]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Form/Type/ProductReviewType.php: -------------------------------------------------------------------------------- 1 | eccubeConfig = $eccubeConfig; 44 | } 45 | 46 | /** 47 | * build form. 48 | * 49 | * @param FormBuilderInterface $builder 50 | * @param array $options 51 | */ 52 | public function buildForm(FormBuilderInterface $builder, array $options) 53 | { 54 | $config = $this->eccubeConfig; 55 | $builder 56 | ->add('reviewer_name', TextType::class, [ 57 | 'label' => 'product_review.form.product_review.reviewer_name', 58 | 'constraints' => [ 59 | new Assert\NotBlank(), 60 | new Assert\Length(['max' => $config['eccube_stext_len']]), 61 | ], 62 | 'attr' => [ 63 | 'maxlength' => $config['eccube_stext_len'], 64 | ], 65 | ]) 66 | ->add('reviewer_url', TextType::class, [ 67 | 'label' => 'product_review.form.product_review.reviewer_url', 68 | 'required' => false, 69 | 'constraints' => [ 70 | new Assert\Url(), 71 | new Assert\Length(['max' => $config['eccube_mltext_len']]), 72 | ], 73 | 'attr' => [ 74 | 'maxlength' => $config['eccube_mltext_len'], 75 | ], 76 | ]) 77 | ->add('sex', SexType::class, [ 78 | 'required' => false, 79 | ]) 80 | ->add('recommend_level', ChoiceType::class, [ 81 | 'label' => 'product_review.form.product_review.recommend_level', 82 | 'choices' => array_flip([ 83 | '5' => '★★★★★', 84 | '4' => '★★★★', 85 | '3' => '★★★', 86 | '2' => '★★', 87 | '1' => '★', 88 | ]), 89 | 'expanded' => true, 90 | 'multiple' => false, 91 | 'placeholder' => false, 92 | 'constraints' => [ 93 | new Assert\NotBlank(), 94 | ], 95 | ]) 96 | ->add('title', TextType::class, [ 97 | 'label' => 'product_review.form.product_review.title', 98 | 'constraints' => [ 99 | new Assert\NotBlank(), 100 | new Assert\Length(['max' => 50]), 101 | ], 102 | 'attr' => [ 103 | 'maxlength' => $config['eccube_stext_len'], 104 | ], 105 | ]) 106 | ->add('comment', TextareaType::class, [ 107 | 'label' => 'product_review.form.product_review.comment', 108 | 'constraints' => [ 109 | new Assert\NotBlank(), 110 | new Assert\Length(['max' => $config['eccube_ltext_len']]), 111 | ], 112 | 'attr' => [ 113 | 'maxlength' => $config['eccube_ltext_len'], 114 | ], 115 | ]); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Entity/ProductReviewConfig.php: -------------------------------------------------------------------------------- 1 | id = $id; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Get id. 84 | * 85 | * @return int 86 | */ 87 | public function getId() 88 | { 89 | return $this->id; 90 | } 91 | 92 | /** 93 | * Get ReviewMax. 94 | * 95 | * @return int 96 | */ 97 | public function getReviewMax() 98 | { 99 | return $this->review_max; 100 | } 101 | 102 | /** 103 | * Set max. 104 | * 105 | * @param int $max 106 | * 107 | * @return ProductReview 108 | */ 109 | public function setReviewMax($max) 110 | { 111 | $this->review_max = $max; 112 | 113 | return $this; 114 | } 115 | 116 | /** 117 | * Get CsvType 118 | * 119 | * @return \Eccube\Entity\Master\CsvType 120 | */ 121 | public function getCsvType() 122 | { 123 | return $this->CsvType; 124 | } 125 | 126 | /** 127 | * Set CsvType 128 | * 129 | * @param CsvType $CsvType 130 | * 131 | * @return $this 132 | */ 133 | public function setCsvType(CsvType $CsvType = null) 134 | { 135 | $this->CsvType = $CsvType; 136 | 137 | return $this; 138 | } 139 | 140 | /** 141 | * Set create_date. 142 | * 143 | * @param \DateTime $createDate 144 | * 145 | * @return $this 146 | */ 147 | public function setCreateDate($createDate) 148 | { 149 | $this->create_date = $createDate; 150 | 151 | return $this; 152 | } 153 | 154 | /** 155 | * Get create_date. 156 | * 157 | * @return \DateTime 158 | */ 159 | public function getCreateDate() 160 | { 161 | return $this->create_date; 162 | } 163 | 164 | /** 165 | * Set update_date. 166 | * 167 | * @param \DateTime $updateDate 168 | * 169 | * @return $this 170 | */ 171 | public function setUpdateDate($updateDate) 172 | { 173 | $this->update_date = $updateDate; 174 | 175 | return $this; 176 | } 177 | 178 | /** 179 | * Get update_date. 180 | * 181 | * @return \DateTime 182 | */ 183 | public function getUpdateDate() 184 | { 185 | return $this->update_date; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /Resource/template/admin/config.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /* 3 | * This file is part of the ProductReview plugin 4 | * 5 | * Copyright (C) 2016 LOCKON CO.,LTD. All Rights Reserved. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | #} 11 | {# 12 | [商品管理]-[商品レビュー]-[編集]画面 13 | #} 14 | 15 | {% extends '@admin/default_frame.twig' %} 16 | 17 | {% set menus = ['store', 'plugin', 'plugin_list'] %} 18 | 19 | {% block title %}{{ 'product_review.admin.config.title'|trans }}{% endblock %} 20 | {% block sub_title %}{{ 'product_review.admin.config.sub_title'|trans }}{% endblock %} 21 | 22 | {% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %} 23 | 24 | {% block main %} 25 | 26 |
27 | {{ form_widget(form._token) }} 28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
37 | {{ 'product_review.admin.config.config_title'|trans }} 38 |
39 |
40 |
41 | 45 |
46 |
47 |
48 | 49 |
50 |
51 |
52 |
53 | 57 |
58 |
59 | {{ form_widget(form.review_max) }} 60 | {{ form_errors(form.review_max) }} 61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 | 70 |
71 |
72 |
73 | 79 |
80 |
81 |
82 | 83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | 91 | {% endblock %} 92 | -------------------------------------------------------------------------------- /Resource/locale/messages.ja.yaml: -------------------------------------------------------------------------------- 1 | # flash messages 2 | product_review.admin.save.complete: 登録しました。 3 | product_review.admin.save.error: 登録に失敗しました。 4 | product_review.admin.not_found: 商品レビューは見つかりませんでした。 5 | product_review.admin.delete.complete: 商品レビューを削除しました。 6 | product_review.admin.product.not_found: 商品が見つかりません。 7 | 8 | # 共通 9 | product_review.common.required: 必須 10 | 11 | # 設定画面 12 | product_review.admin.config.title: レビュー設定 13 | product_review.admin.config.sub_title: プラグイン一覧 14 | product_review.admin.config.config_title: 設定 15 | product_review.admin.config.review_max: 'レビューの表示件数(%min%〜%max%)' 16 | product_review.admin.config.conversion.save: 登録 17 | product_review.admin.config.conversion.back: プラグイン一覧 18 | 19 | # レビュー一覧画面 20 | product_review.admin.product_review.title: レビュー管理 21 | product_review.admin.product_review.sub_title: 商品管理 22 | product_review.admin.product_review.search_detail: 詳細検索 23 | product_review.admin.product_review.search_button: 検索 24 | product_review.admin.product_review.search_clear: 検索条件をクリア 25 | product_review.admin.product_review.search_result_count: '検索結果:%count%件が該当しました。' 26 | product_review.admin.product_review.search_multi: 投稿者名・投稿者URL 27 | product_review.admin.product_review.search_product_name: 商品名 28 | product_review.admin.product_review.search_product_code: 商品コード 29 | product_review.admin.product_review.search_posted_date: 投稿日 30 | product_review.admin.product_review.search_posted_date_start: 投稿日(開始) 31 | product_review.admin.product_review.search_posted_date_end: 投稿日(終了) 32 | product_review.admin.product_review.search_sex: 性別 33 | product_review.admin.product_review.search_recommend_level: おすすめレベル 34 | product_review.admin.product_review.search_review_status: 公開・非公開 35 | product_review.admin.product_review.search_no_result: 検索条件に合致するデータが見つかりませんでした 36 | product_review.admin.product_review.search_invalid_condition: 検索条件に誤りがあります 37 | product_review.admin.product_review.search_change_condition_and_retry: 検索条件を変えて、再度検索をお試しください。 38 | product_review.admin.product_review.search_try_detail_condition: '[詳細検索]も試してみましょう。' 39 | product_review.admin.product_review.diaply_count: '%count%件' 40 | product_review.admin.product_review.csv_download: CSVダウンロード 41 | product_review.admin.product_review.csv_download_setting: CSV出力項目設定 42 | product_review.admin.product_review.th_posted_date: 投稿日 43 | product_review.admin.product_review.th_contributor: 投稿者名 44 | product_review.admin.product_review.th_product_name: 商品名 45 | product_review.admin.product_review.th_title: タイトル 46 | product_review.admin.product_review.th_level: おすすめレベル 47 | product_review.admin.product_review.th_status: 公開・非公開 48 | product_review.admin.product_review.delete: 削除 49 | product_review.admin.product_review.delete_cancel: キャンセル 50 | product_review.admin.product_review.delete_confirm_title: レビューを削除します 51 | product_review.admin.product_review.delete_confirm_message: 削除してよろしいですか? 52 | 53 | # レビュー編集画面 54 | product_review.admin.product_review_edit.title: レビュー登録・編集 55 | product_review.admin.product_review_edit.sub_title: 商品管理 56 | product_review.admin.product_review_edit.review_detail: レビュー詳細 57 | product_review.admin.product_review_edit.product_name: 商品名 58 | product_review.admin.product_review_edit.posted_date: 投稿日時 59 | product_review.admin.product_review_edit.conversion.save: 登録 60 | product_review.admin.product_review_edit.conversion.back: レビュー管理 61 | 62 | # レビュー投稿画面 63 | product_review.front.review.title: レビューを投稿 64 | product_review.front.review.description: 商品について、お客様のご感想をお待ちしております。 65 | product_review.front.review.product_name: 商品名 66 | product_review.front.review.back: 戻る 67 | product_review.front.review.confirm: 確認ページへ 68 | 69 | # レビュー確認画面 70 | product_review.front.review.confirm.description: | 71 | 下記の内容で送信してもよろしいでしょうか? 72 | よろしければ、一番下の「投稿する」ボタンをクリックしてください。 73 | product_review.front.review.comfirm.post: 投稿する 74 | 75 | # レビュー完了画面 76 | product_review.front.review.complete.thanks: ご投稿ありがとうございます。 77 | product_review.front.review.complete.thanks_detail: | 78 | 内容を確認後、反映させていただきます。 79 | 今しばらくお待ちくださいませ。 80 | product_review.front.review.complete.back: 商品ページへ戻る 81 | 82 | # レビューフォーム 83 | product_review.form.product_review.reviewer_name: 投稿者名 84 | product_review.form.product_review.reviewer_url: URL 85 | product_review.form.product_review.title: タイトル 86 | product_review.form.product_review.comment: コメント 87 | product_review.form.product_review.recommend_level: おすすめレベル 88 | 89 | # 商品詳細画面への差し込みブロック 90 | product_review.front.product_detail.title: この商品のレビュー 91 | product_review.front.product_detail.no_review: レビューはありません。 92 | product_review.front.product_detail.post_review: レビューを投稿 93 | product_review.front.product_detail.name: '%name% さん' 94 | -------------------------------------------------------------------------------- /Tests/Web/ProductReviewConfigControllerTest.php: -------------------------------------------------------------------------------- 1 | faker = $this->getFaker(); 38 | } 39 | 40 | /** 41 | * Config routing. 42 | */ 43 | public function testRouting() 44 | { 45 | /** 46 | * @var Client 47 | */ 48 | $client = $this->client; 49 | /** 50 | * @var Crawler 51 | */ 52 | $crawler = $this->client->request('GET', $this->generateUrl('product_review42_admin_config')); 53 | 54 | $this->assertTrue($client->getResponse()->isSuccessful()); 55 | 56 | $min = $this->eccubeConfig['product_review_display_count_min']; 57 | $max = $this->eccubeConfig['product_review_display_count_max']; 58 | $this->assertStringContainsString('レビューの表示件数('.$min.'〜'.$max.')', $crawler->html()); 59 | } 60 | 61 | /** 62 | * Config submit. 63 | */ 64 | public function testMin() 65 | { 66 | $min = $this->eccubeConfig['product_review_display_count_min']; 67 | /** 68 | * @var Client 69 | */ 70 | $client = $this->client; 71 | /** 72 | * @var Crawler 73 | */ 74 | $crawler = $this->client->request('GET', $this->generateUrl('product_review42_admin_config')); 75 | 76 | $this->assertTrue($client->getResponse()->isSuccessful()); 77 | 78 | $form = $crawler->selectButton('登録')->form(); 79 | 80 | $form['product_review_config[review_max]'] = $this->faker->numberBetween(-10, $min - 1); 81 | $crawler = $client->submit($form); 82 | 83 | $this->assertStringContainsString($min.'以上', $crawler->html()); 84 | } 85 | 86 | /** 87 | * Config submit. 88 | */ 89 | public function testMax() 90 | { 91 | $max = $this->eccubeConfig['product_review_display_count_max']; 92 | /** 93 | * @var Client 94 | */ 95 | $client = $this->client; 96 | /** 97 | * @var Crawler 98 | */ 99 | $crawler = $this->client->request('GET', $this->generateUrl('product_review42_admin_config')); 100 | 101 | $this->assertTrue($client->getResponse()->isSuccessful()); 102 | 103 | $form = $crawler->selectButton('登録')->form(); 104 | 105 | $form['product_review_config[review_max]'] = $this->faker->numberBetween($max + 1, 100); 106 | $crawler = $client->submit($form); 107 | 108 | $this->assertStringContainsString($max.'以下でなければなりません。', $crawler->html()); 109 | } 110 | 111 | /** 112 | * Config submit. 113 | */ 114 | public function testSuccess() 115 | { 116 | $min = $this->eccubeConfig['product_review_display_count_min']; 117 | $max = $this->eccubeConfig['product_review_display_count_max']; 118 | /** 119 | * @var Client 120 | */ 121 | $client = $this->client; 122 | /** 123 | * @var Crawler 124 | */ 125 | $crawler = $this->client->request('GET', $this->generateUrl('product_review42_admin_config')); 126 | 127 | $this->assertTrue($client->getResponse()->isSuccessful()); 128 | 129 | $form = $crawler->selectButton('登録')->form(); 130 | 131 | $form['product_review_config[review_max]'] = $this->faker->numberBetween($min, $max); 132 | $crawler = $client->submit($form); 133 | 134 | $this->assertTrue($client->getResponse()->isRedirection($this->generateUrl('product_review42_admin_config'))); 135 | 136 | $crawler = $client->followRedirect(); 137 | $this->assertStringContainsString('登録しました。', $crawler->html()); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Form/Type/Admin/ProductReviewSearchType.php: -------------------------------------------------------------------------------- 1 | eccubeConfig = $eccubeConfig; 46 | } 47 | 48 | /** 49 | * {@inheritdoc} 50 | * build form method. 51 | * 52 | * @param FormBuilderInterface $builder 53 | * @param array $options 54 | */ 55 | public function buildForm(FormBuilderInterface $builder, array $options) 56 | { 57 | $config = $this->eccubeConfig; 58 | $builder 59 | ->add('multi', TextType::class, [ 60 | 'label' => 'product_review.admin.product_review.search_multi', 61 | 'required' => false, 62 | 'constraints' => [ 63 | new Assert\Length(['max' => $config['eccube_stext_len']]), 64 | ], 65 | ]) 66 | ->add('product_name', TextType::class, [ 67 | 'label' => 'product_review.admin.product_review.search_product_name', 68 | 'required' => false, 69 | 'constraints' => [ 70 | new Assert\Length(['max' => $config['eccube_stext_len']]), 71 | ], 72 | ]) 73 | ->add('product_code', TextType::class, [ 74 | 'label' => 'product_review.admin.product_review.search_product_code', 75 | 'required' => false, 76 | 'constraints' => [ 77 | new Assert\Length(['max' => $config['eccube_stext_len']]), 78 | ], 79 | ]) 80 | ->add('sex', SexType::class, [ 81 | 'label' => 'product_review.admin.product_review.search_sex', 82 | 'required' => false, 83 | 'expanded' => true, 84 | 'multiple' => true, 85 | ]) 86 | ->add('recommend_level', ChoiceType::class, [ 87 | 'label' => 'product_review.admin.product_review.search_recommend_level', 88 | 'choices' => array_flip([ 89 | '5' => '★★★★★', 90 | '4' => '★★★★', 91 | '3' => '★★★', 92 | '2' => '★★', 93 | '1' => '★', 94 | ]), 95 | 'placeholder' => 'product_review.admin.product_review.search_recommend_level', 96 | 'expanded' => false, 97 | 'multiple' => false, 98 | 'required' => false, 99 | ]) 100 | ->add('review_start', DateType::class, [ 101 | 'label' => 'product_review.admin.product_review.search_posted_date_start', 102 | 'required' => false, 103 | 'input' => 'datetime', 104 | 'widget' => 'single_text', 105 | 'attr' => [ 106 | 'class' => 'datetimepicker-input', 107 | 'data-bs-target' => '#'.$this->getBlockPrefix().'_review_start', 108 | 'data-bs-toggle' => 'datetimepicker', 109 | ], 110 | ]) 111 | ->add('review_end', DateType::class, [ 112 | 'label' => 'product_review.admin.product_review.search_posted_date_end', 113 | 'required' => false, 114 | 'input' => 'datetime', 115 | 'widget' => 'single_text', 116 | 'attr' => [ 117 | 'class' => 'datetimepicker-input', 118 | 'data-bs-target' => '#'.$this->getBlockPrefix().'_review_end', 119 | 'data-bs-toggle' => 'datetimepicker', 120 | ], 121 | ]) 122 | ->add('status', EntityType::class, [ 123 | 'class' => ProductReviewStatus::class, 124 | 'label' => 'product_review.admin.product_review.search_review_status', 125 | 'required' => false, 126 | 'expanded' => true, 127 | 'multiple' => true, 128 | ]); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /Repository/ProductReviewRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('r') 52 | ->select('r, p, pc') 53 | ->innerJoin('r.Product', 'p') 54 | ->innerJoin('p.ProductClasses', 'pc'); 55 | 56 | // 投稿者名・投稿者URL 57 | if (isset($searchData['multi']) && StringUtil::isNotBlank($searchData['multi'])) { 58 | $qb 59 | ->andWhere('r.reviewer_name LIKE :reviewer_name OR r.reviewer_url LIKE :reviewer_url') 60 | ->setParameter('reviewer_name', '%'.str_replace('%', '\\%', $searchData['multi']).'%') 61 | ->setParameter('reviewer_url', '%'.str_replace('%', '\\%', $searchData['multi']).'%'); 62 | } 63 | 64 | // 商品名 65 | if (isset($searchData['product_name']) && StringUtil::isNotBlank($searchData['product_name'])) { 66 | $qb 67 | ->andWhere('p.name LIKE :product_name') 68 | ->setParameter('product_name', '%'.str_replace('%', '\\%', $searchData['product_name']).'%'); 69 | } 70 | 71 | // 商品コード 72 | if (isset($searchData['product_code']) && StringUtil::isNotBlank($searchData['product_code'])) { 73 | $qb 74 | ->andWhere('pc.code LIKE :code') 75 | ->setParameter('code', '%'.str_replace('%', '\\%', $searchData['product_code']).'%'); 76 | } 77 | 78 | // 性別 79 | if (!empty($searchData['sex']) && count($searchData['sex']) > 0) { 80 | $qb 81 | ->andWhere($qb->expr()->in('r.Sex', ':Sex')) 82 | ->setParameter(':Sex', $searchData['sex']); 83 | } 84 | 85 | // おすすめレベル 86 | if (isset($searchData['recommend_level']) && StringUtil::isNotBlank($searchData['recommend_level'])) { 87 | $qb 88 | ->andWhere($qb->expr()->in('r.recommend_level', ':recommend_level')) 89 | ->setParameter('recommend_level', $searchData['recommend_level']); 90 | } 91 | 92 | // 投稿日(開始) 93 | if (isset($searchData['review_start']) && !is_null($searchData['review_start'])) { 94 | $date = $searchData['review_start']; 95 | $qb 96 | ->andWhere('r.create_date >= :review_start') 97 | ->setParameter('review_start', $date); 98 | } 99 | 100 | // 投稿日(終了) 101 | if (isset($searchData['review_end']) && !is_null($searchData['review_end'])) { 102 | $date = clone $searchData['review_end']; 103 | $date 104 | ->modify('+1 days'); 105 | $qb 106 | ->andWhere('r.create_date < :review_end') 107 | ->setParameter('review_end', $date); 108 | } 109 | 110 | // 公開・非公開 111 | if (!empty($searchData['status']) && count($searchData['status']) > 0) { 112 | $qb 113 | ->andWhere($qb->expr()->in('r.Status', ':Status')) 114 | ->setParameter('Status', $searchData['status']); 115 | } 116 | 117 | // Order By 118 | $qb->addOrderBy('r.id', 'DESC'); 119 | 120 | return $qb; 121 | } 122 | 123 | /** 124 | * Get Avg and count. 125 | * 126 | * @param Product $Product 127 | * 128 | * @return mixed 129 | */ 130 | public function getAvgAll(Product $Product) 131 | { 132 | $defaults = [ 133 | 'recommend_avg' => 0, 134 | 'review_count' => 0, 135 | ]; 136 | try { 137 | $qb = $this->createQueryBuilder('r') 138 | ->select('avg(r.recommend_level) as recommend_avg, count(r.id) as review_count') 139 | ->leftJoin('r.Product', 'p') 140 | ->where('r.Product = :Product') 141 | ->setParameter('Product', $Product) 142 | ->andWhere('r.Status = :Status') 143 | ->setParameter('Status', ProductReviewStatus::SHOW) 144 | ->groupBy('r.Product'); 145 | 146 | return $qb->getQuery()->getSingleResult(); 147 | } catch (\Exception $exception) { 148 | return $defaults; 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /Controller/ProductReviewController.php: -------------------------------------------------------------------------------- 1 | productReviewStatusRepository = $productStatusRepository; 57 | $this->productReviewRepository = $productReviewRepository; 58 | } 59 | 60 | /** 61 | * @Route("/product_review/{id}/review", name="product_review_index", requirements={"id" = "\d+"}) 62 | * @Route("/product_review/{id}/review", name="product_review_confirm", requirements={"id" = "\d+"}) 63 | * 64 | * @param Request $request 65 | * @param Product $Product 66 | * 67 | * @return RedirectResponse|Response 68 | */ 69 | public function index(Request $request, Product $Product) 70 | { 71 | if (!$this->session->has('_security_admin') && $Product->getStatus()->getId() !== ProductStatus::DISPLAY_SHOW) { 72 | log_info('Product review', ['status' => 'Not permission']); 73 | 74 | throw new NotFoundHttpException(); 75 | } 76 | 77 | $ProductReview = new ProductReview(); 78 | $form = $this->createForm(ProductReviewType::class, $ProductReview); 79 | 80 | $form->handleRequest($request); 81 | if ($form->isSubmitted() && $form->isValid()) { 82 | /** @var $ProductReview ProductReview */ 83 | $ProductReview = $form->getData(); 84 | 85 | switch ($request->get('mode')) { 86 | case 'confirm': 87 | log_info('Product review config confirm'); 88 | 89 | return $this->render('ProductReview42/Resource/template/default/confirm.twig', [ 90 | 'form' => $form->createView(), 91 | 'Product' => $Product, 92 | 'ProductReview' => $ProductReview, 93 | ]); 94 | break; 95 | 96 | case 'complete': 97 | log_info('Product review complete'); 98 | if ($this->isGranted('ROLE_USER')) { 99 | $Customer = $this->getUser(); 100 | $ProductReview->setCustomer($Customer); 101 | } 102 | $ProductReview->setProduct($Product); 103 | $ProductReview->setStatus($this->productReviewStatusRepository->find(ProductReviewStatus::HIDE)); 104 | $this->entityManager->persist($ProductReview); 105 | $this->entityManager->flush($ProductReview); 106 | 107 | log_info('Product review complete', ['id' => $Product->getId()]); 108 | 109 | return $this->redirectToRoute('product_review_complete', ['id' => $Product->getId()]); 110 | break; 111 | 112 | case 'back': 113 | // 確認画面から投稿画面へ戻る 114 | break; 115 | 116 | default: 117 | // do nothing 118 | break; 119 | } 120 | } 121 | 122 | return $this->render('ProductReview42/Resource/template/default/index.twig', [ 123 | 'Product' => $Product, 124 | 'ProductReview' => $ProductReview, 125 | 'form' => $form->createView(), 126 | ]); 127 | } 128 | 129 | /** 130 | * Complete. 131 | * 132 | * @Route("/product_review/{id}/complete", name="product_review_complete", requirements={"id" = "\d+"}) 133 | * @Template("ProductReview42/Resource/template/default/complete.twig") 134 | * 135 | * @param $id 136 | * 137 | * @return array 138 | */ 139 | public function complete($id) 140 | { 141 | return ['id' => $id]; 142 | } 143 | 144 | /** 145 | * ページ管理表示用のダミールーティング. 146 | * 147 | * @Route("/product_review/display", name="product_review_display") 148 | */ 149 | public function display() 150 | { 151 | return new Response(); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Resource/template/default/review.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /* 3 | * This file is part of the ProductReview plugin 4 | * 5 | * Copyright (C) 2016 LOCKON CO.,LTD. All Rights Reserved. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | #} 11 | 12 | {% import _self as stars %} 13 | 14 | {# 星テキスト生成用マクロ #} 15 | {% macro stars(positive, negative) %} 16 | {% set positive_stars = ["", "★", "★★", "★★★", "★★★★", "★★★★★"] %} 17 | {% set negative_stars = ["", "☆", "☆☆", "☆☆☆", "☆☆☆☆", "☆☆☆☆☆"] %} 18 | {{ positive_stars[positive] }}{{ negative_stars[negative] }} 19 | {% endmacro %} 20 | 21 | 66 | 67 | 86 | 87 | 88 |
89 |
90 | {% set positive_avg_star = ProductReviewAvg %} 91 | {% set negative_avg_star = 5 - positive_avg_star %} 92 | 93 |
94 |

{{ 'product_review.front.product_detail.title'|trans }} 95 | 96 | {{ stars.stars(positive_avg_star, negative_avg_star) }} 97 | 98 | ({{ ProductReviewCount }}) 99 | 100 | 101 | 102 |

103 |
104 |
105 | {% if ProductReviews %} 106 |
    107 | {% for ProductReview in ProductReviews %} 108 |
  • 109 |

    110 | 111 | {{ ProductReview.create_date|date_day }} 112 | 113 | 114 | 115 | {% if ProductReview.reviewer_url %} 116 | {{ 'product_review.front.product_detail.name'|trans({ '%name%': ProductReview.reviewer_name }) }} 118 | {% else %} 119 | {{ 'product_review.front.product_detail.name'|trans({ '%name%': ProductReview.reviewer_name }) }} 120 | {% endif %} 121 | 122 | 123 | 124 | {% set positive_star = ProductReview.recommend_level %} 125 | {% set negative_star = 5 - positive_star %} 126 | 127 | {{ stars.stars(positive_star, negative_star) }} 128 | 129 |

    130 | 131 | 132 | {{ ProductReview.title }} 133 | 134 | 135 |

    {{ ProductReview.comment|nl2br }}

    136 |
  • 137 | {% endfor %} 138 |
139 | {% else %} 140 |

{{ 'product_review.front.product_detail.no_review'|trans }}

141 | {% endif %} 142 |
143 |
144 | {{ 'product_review.front.product_detail.post_review'|trans }} 146 |
147 |
148 |
149 | 150 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI for ProductReview42 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags: 7 | - '*' 8 | paths: 9 | - '**' 10 | - '!*.md' 11 | pull_request: 12 | branches: 13 | - '*' 14 | paths: 15 | - '**' 16 | - '!*.md' 17 | jobs: 18 | run-on-linux: 19 | name: Run on Linux 20 | runs-on: ubuntu-22.04 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | eccube_version: [ '4.2', '4.3' ] 25 | php: [ '7.4', '8.0', '8.1', '8.2', '8.3' ] 26 | db: [ 'mysql', 'mysql8', 'pgsql' ] 27 | plugin_code: [ 'ProductReview42' ] 28 | include: 29 | - db: mysql 30 | database_url: mysql://root:password@127.0.0.1:3306/eccube_db 31 | database_server_version: 5.7 32 | database_charset: utf8mb4 33 | - db: mysql8 34 | database_url: mysql://root:password@127.0.0.1:3308/eccube_db 35 | database_server_version: 8 36 | database_charset: utf8mb4 37 | - db: pgsql 38 | database_url: postgres://postgres:password@127.0.0.1:5432/eccube_db 39 | database_server_version: 14 40 | database_charset: utf8 41 | exclude: 42 | - eccube_version: 4.2 43 | php: 8.2 44 | - eccube_version: 4.2 45 | php: 8.3 46 | - eccube_version: 4.3 47 | php: 7.4 48 | - eccube_version: 4.3 49 | php: 8.0 50 | 51 | services: 52 | mysql: 53 | image: mysql:5.7 54 | env: 55 | MYSQL_ROOT_PASSWORD: password 56 | MYSQL_DATABASE: ${{ matrix.dbname }} 57 | ports: 58 | - 3306:3306 59 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 60 | mysql8: 61 | image: mysql:8 62 | env: 63 | MYSQL_ROOT_PASSWORD: password 64 | MYSQL_DATABASE: ${{ matrix.dbname }} 65 | ports: 66 | - 3308:3306 67 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 68 | postgres: 69 | image: postgres:14 70 | env: 71 | POSTGRES_USER: postgres 72 | POSTGRES_PASSWORD: password 73 | POSTGRES_DB: ${{ matrix.dbname }} 74 | ports: 75 | - 5432:5432 76 | # needed because the postgres container does not provide a healthcheck 77 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 78 | mailcatcher: 79 | image: schickling/mailcatcher 80 | ports: 81 | - 1080:1080 82 | - 1025:1025 83 | steps: 84 | - name: Checkout 85 | uses: actions/checkout@v2 86 | 87 | - name: Setup PHP 88 | uses: nanasess/setup-php@master 89 | with: 90 | php-version: ${{ matrix.php }} 91 | 92 | - name: Archive Plugin 93 | env: 94 | PLUGIN_CODE: ${{ matrix.plugin_code }} 95 | run: | 96 | tar cvzf ${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz ./* 97 | - name: Checkout EC-CUBE 98 | uses: actions/checkout@v2 99 | with: 100 | repository: 'EC-CUBE/ec-cube' 101 | ref: ${{ matrix.eccube_version }} 102 | path: 'ec-cube' 103 | 104 | - name: Get Composer Cache Directory 105 | id: composer-cache 106 | run: | 107 | echo "::set-output name=dir::$(composer config cache-files-dir)" 108 | - uses: actions/cache@v1 109 | with: 110 | path: ${{ steps.composer-cache.outputs.dir }} 111 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 112 | restore-keys: | 113 | ${{ runner.os }}-composer- 114 | - name: Install to composer 115 | working-directory: 'ec-cube' 116 | run: composer install --no-interaction -o --apcu-autoloader 117 | 118 | - name: Setup EC-CUBE 119 | env: 120 | APP_ENV: 'test' 121 | APP_DEBUG: 0 122 | DATABASE_URL: ${{ matrix.database_url }} 123 | DATABASE_SERVER_VERSION: ${{ matrix.database_server_version }} 124 | DATABASE_CHARSET: ${{ matrix.database_charset }} 125 | working-directory: 'ec-cube' 126 | run: | 127 | bin/console doctrine:database:create 128 | bin/console doctrine:schema:create 129 | bin/console eccube:fixtures:load 130 | - name: Setup Plugin 131 | env: 132 | APP_ENV: 'test' 133 | APP_DEBUG: 0 134 | DATABASE_URL: ${{ matrix.database_url }} 135 | DATABASE_SERVER_VERSION: ${{ matrix.database_server_version }} 136 | DATABASE_CHARSET: ${{ matrix.database_charset }} 137 | PLUGIN_CODE: ${{ matrix.plugin_code }} 138 | working-directory: 'ec-cube' 139 | run: | 140 | bin/console eccube:plugin:install --code=${PLUGIN_CODE} --path=${GITHUB_WORKSPACE}/${PLUGIN_CODE}.tar.gz 141 | bin/console cache:clear --no-warmup 142 | bin/console eccube:plugin:enable --code=${PLUGIN_CODE} 143 | - name: Run PHPUnit 144 | env: 145 | APP_ENV: 'test' 146 | APP_DEBUG: 0 147 | DATABASE_URL: ${{ matrix.database_url }} 148 | DATABASE_SERVER_VERSION: ${{ matrix.database_server_version }} 149 | DATABASE_CHARSET: ${{ matrix.database_charset }} 150 | PLUGIN_CODE: ${{ matrix.plugin_code }} 151 | working-directory: 'ec-cube' 152 | run: | 153 | bin/console cache:clear --no-warmup 154 | vendor/bin/phpunit -c app/Plugin/${PLUGIN_CODE}/phpunit.xml.dist app/Plugin/${PLUGIN_CODE}/Tests 155 | 156 | - name: Disable Plugin 157 | working-directory: 'ec-cube' 158 | env: 159 | APP_ENV: 'test' 160 | APP_DEBUG: 0 161 | DATABASE_URL: ${{ matrix.database_url }} 162 | DATABASE_SERVER_VERSION: ${{ matrix.database_server_version }} 163 | DATABASE_CHARSET: ${{ matrix.database_charset }} 164 | PLUGIN_CODE: ${{ matrix.plugin_code }} 165 | run: bin/console eccube:plugin:disable --code=${PLUGIN_CODE} 166 | 167 | - name: Uninstall Plugin 168 | env: 169 | APP_ENV: 'test' 170 | APP_DEBUG: 0 171 | DATABASE_URL: ${{ matrix.database_url }} 172 | DATABASE_SERVER_VERSION: ${{ matrix.database_server_version }} 173 | DATABASE_CHARSET: ${{ matrix.database_charset }} 174 | PLUGIN_CODE: ${{ matrix.plugin_code }} 175 | working-directory: 'ec-cube' 176 | run: bin/console eccube:plugin:uninstall --code=${PLUGIN_CODE} 177 | -------------------------------------------------------------------------------- /Resource/template/default/confirm.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This file is part of EC-CUBE 3 | 4 | Copyright(c) LOCKON CO.,LTD. All Rights Reserved. 5 | 6 | http://www.lockon.co.jp/ 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | #} 11 | {% extends 'default_frame.twig' %} 12 | 13 | {% set body_class = 'product_review_confirm' %} 14 | 15 | {% form_theme form 'Form/form_div_layout.twig' %} 16 | 17 | {% block main %} 18 |
19 |
20 |

{{ 'product_review.front.review.title'|trans }}

21 |
22 |
23 |
24 |
25 | {{ form_widget(form._token) }} 26 |

{{ 'product_review.front.review.confirm.description'|trans|nl2br }}

27 |
28 | {# 商品名 #} 29 |
30 |
31 | 32 |
33 |
34 |
35 | {{ Product.name }} 36 |
37 |
38 |
39 | {# 投稿者名 #} 40 |
41 |
42 | {{ form_label(form.reviewer_name, '', {'label_attr': {'class': 'ec-label'}}) }} 43 |
44 |
45 |
46 | {{ form.reviewer_name.vars.data }} 47 | {{ form_widget(form.reviewer_name, { type: 'hidden' }) }} 48 |
49 |
50 |
51 | {# 投稿者URL #} 52 |
53 |
54 | {{ form_label(form.reviewer_url, '', {'label_attr': {'class': 'ec-label'}}) }} 55 |
56 |
57 |
58 | {{ form.reviewer_url.vars.data }} 59 | {{ form_widget(form.reviewer_url, { type: 'hidden' }) }} 60 |
61 |
62 |
63 | {# 性別 #} 64 |
65 |
66 | {{ form_label(form.sex, 'common.gender', {'label_attr': {'class': 'ec-label'}}) }} 67 |
68 |
69 |
70 | {{ form.sex.vars.data }} 71 | {{ form_widget(form.sex, { type: 'hidden' }) }} 72 |
73 |
74 |
75 | {# おすすめレベル #} 76 |
77 |
78 | {{ form_label(form.recommend_level, '', {'label_attr': {'class': 'ec-label'}}) }} 79 |
80 |
81 |
82 | {% for child in form.recommend_level.children|filter(child => child.vars.checked) %} 83 | {{ child.vars.label }} 84 | {% endfor %} 85 | {{ form_widget(form.recommend_level, { type: 'hidden' }) }} 86 |
87 |
88 |
89 | {# タイトル #} 90 |
91 |
92 | {{ form_label(form.title, '', {'label_attr': {'class': 'ec-label'}}) }} 93 |
94 |
95 |
96 | {{ form.title.vars.data }} 97 | {{ form_widget(form.title, { type: 'hidden' }) }} 98 |
99 |
100 |
101 | {# コメント #} 102 |
103 |
104 | {{ form_label(form.comment, '', {'label_attr': {'class': 'ec-label'}}) }} 105 |
106 |
107 |
108 | {{ form.comment.vars.data }} 109 | {{ form_widget(form.comment, { type: 'hidden' }) }} 110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 | 119 | 121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | {% endblock %} 129 | 130 | -------------------------------------------------------------------------------- /Resource/template/default/index.twig: -------------------------------------------------------------------------------- 1 | {# 2 | This file is part of EC-CUBE 3 | 4 | Copyright(c) LOCKON CO.,LTD. All Rights Reserved. 5 | 6 | http://www.lockon.co.jp/ 7 | 8 | For the full copyright and license information, please view the LICENSE 9 | file that was distributed with this source code. 10 | #} 11 | {% extends 'default_frame.twig' %} 12 | 13 | {% set body_class = 'product_review' %} 14 | 15 | {% form_theme form 'Form/form_div_layout.twig' %} 16 | 17 | {% block stylesheet %} 18 | 31 | {% endblock %} 32 | 33 | {% block main %} 34 |
35 |
36 |

{{ 'product_review.front.review.title'|trans }}

37 |
38 |
39 |
40 |
41 | {{ form_widget(form._token) }} 42 |

{{ 'product_review.front.review.description'|trans }}

43 |
44 | {# 商品名 #} 45 |
46 |
47 | 48 |
49 |
50 |
51 | {{ Product.name }} 52 |
53 |
54 |
55 | {# 投稿者名 #} 56 |
57 |
58 | {{ form_label(form.reviewer_name, '', {'label_attr': {'class': 'ec-label'}}) }} 59 |
60 |
61 |
62 | {{ form_widget(form.reviewer_name) }} 63 | {{ form_errors(form.reviewer_name) }} 64 |
65 |
66 |
67 | {# 投稿者URL #} 68 |
69 |
70 | {{ form_label(form.reviewer_url, '', {'label_attr': {'class': 'ec-label'}}) }} 71 |
72 |
73 |
74 | {{ form_widget(form.reviewer_url) }} 75 | {{ form_errors(form.reviewer_url) }} 76 |
77 |
78 |
79 | {# 性別 #} 80 |
81 |
82 | {{ form_label(form.sex, 'common.gender', {'label_attr': {'class': 'ec-label'}}) }} 83 |
84 |
85 |
86 | {{ form_widget(form.sex) }} 87 | {{ form_errors(form.sex) }} 88 |
89 |
90 |
91 | {# おすすめレベル #} 92 |
93 |
94 | {{ form_label(form.recommend_level, '', {'label_attr': {'class': 'ec-label'}}) }} 95 |
96 |
97 |
98 | {{ form_widget(form.recommend_level) }} 99 | {{ form_errors(form.recommend_level) }} 100 |
101 |
102 |
103 | {# タイトル #} 104 |
105 |
106 | {{ form_label(form.title, '', {'label_attr': {'class': 'ec-label'}}) }} 107 |
108 |
109 |
110 | {{ form_widget(form.title) }} 111 | {{ form_errors(form.title) }} 112 |
113 |
114 |
115 | {# コメント #} 116 |
117 |
118 | {{ form_label(form.comment, '', {'label_attr': {'class': 'ec-label'}}) }} 119 |
120 |
121 |
122 | {{ form_widget(form.comment) }} 123 | {{ form_errors(form.comment) }} 124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 | 133 | {{ 'product_review.front.review.back'|trans }} 135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | {% endblock %} 143 | 144 | -------------------------------------------------------------------------------- /Tests/Web/ReviewControllerTest.php: -------------------------------------------------------------------------------- 1 | faker = $this->getFaker(); 61 | $this->deleteAllRows(['plg_product_review']); 62 | 63 | $this->productRepo = $this->entityManager->getRepository(Product::class); 64 | $this->sexMasterRepo = $this->entityManager->getRepository(Sex::class); 65 | $this->productReviewRepo = $this->entityManager->getRepository(ProductReview::class); 66 | } 67 | 68 | /** 69 | * Add product review. 70 | */ 71 | public function testProductReviewAddConfirmComplete() 72 | { 73 | $productId = 1; 74 | $crawler = $this->client->request( 75 | 'POST', 76 | $this->generateUrl('product_review_index', ['id' => $productId]), 77 | [ 78 | 'product_review' => [ 79 | 'comment' => $this->faker->text(2999), 80 | 'title' => $this->faker->word, 81 | 'sex' => 1, 82 | 'recommend_level' => $this->faker->numberBetween(1, 5), 83 | 'reviewer_url' => $this->faker->url, 84 | 'reviewer_name' => $this->faker->word, 85 | '_token' => 'dummy', 86 | ], 87 | 'mode' => 'confirm', 88 | ] 89 | ); 90 | $this->assertStringContainsString('投稿する', $crawler->html()); 91 | 92 | // Complete 93 | $form = $crawler->selectButton('投稿する')->form(); 94 | $this->client->submit($form); 95 | 96 | $this->assertTrue($this->client->getResponse()->isRedirect($this->generateUrl('product_review_complete', ['id' => $productId]))); 97 | 98 | // Verify back to product detail link. 99 | /** 100 | * @var Crawler 101 | */ 102 | $crawler = $this->client->followRedirect(); 103 | $link = $crawler->selectLink('商品ページへ戻る')->link(); 104 | 105 | $this->actual = $link->getUri(); 106 | 107 | $this->expected = $this->generateUrl('product_detail', ['id' => $productId], UrlGeneratorInterface::ABSOLUTE_URL); 108 | $this->verify(); 109 | } 110 | 111 | /** 112 | * Back test. 113 | */ 114 | public function testProductReviewAddConfirmBack() 115 | { 116 | $productId = 1; 117 | $inputForm = [ 118 | 'comment' => $this->faker->text(2999), 119 | 'title' => $this->faker->word, 120 | 'sex' => 1, 121 | 'recommend_level' => $this->faker->numberBetween(1, 5), 122 | 'reviewer_url' => $this->faker->url, 123 | 'reviewer_name' => $this->faker->word, 124 | '_token' => 'dummy', 125 | ]; 126 | $crawler = $this->client->request( 127 | 'POST', 128 | $this->generateUrl('product_review_index', ['id' => $productId]), 129 | ['product_review' => $inputForm, 130 | 'mode' => 'confirm', 131 | ] 132 | ); 133 | $this->assertStringContainsString('投稿する', $crawler->html()); 134 | 135 | // Back click 136 | $form = $crawler->selectButton('戻る')->form(); 137 | $crawlerConfirm = $this->client->submit($form); 138 | $html = $crawlerConfirm->html(); 139 | $this->assertStringContainsString('確認ページへ', $html); 140 | 141 | // Verify data 142 | $this->assertStringContainsString($inputForm['comment'], $html); 143 | } 144 | 145 | // /** 146 | // * review list. 147 | // */ 148 | /*public function testProductReview() 149 | { 150 | $productId = 1; 151 | $ProductReview = $this->createProductReviewData($productId); 152 | $crawler = $this->client->request( 153 | 'GET', 154 | $this->generateUrl('product_detail', ['id' => $productId]) 155 | ); 156 | 157 | $codeStatus = $this->client->getResponse()->getStatusCode(); 158 | 159 | // review area 160 | $this->assertStringContainsString('id="product_review_area"', $crawler->html()); 161 | 162 | // review content 163 | $reviewArea = $crawler->filter('#product_review_area'); 164 | $this->assertStringContainsString($ProductReview->getComment(), $reviewArea->html()); 165 | 166 | // review total 167 | $totalNum = $reviewArea->filter('.heading02')->html(); 168 | $this->assertStringContainsString('1', $totalNum); 169 | }*/ 170 | 171 | /** 172 | * review list. 173 | */ 174 | public function testProductReviewMaxNumber() 175 | { 176 | $max = 31; 177 | $Product = $this->createProduct(); 178 | $productId = $Product->getId(); 179 | $this->createProductReviewByNumber($max, $productId); 180 | $crawler = $this->client->request( 181 | 'GET', 182 | $this->generateUrl('product_detail', ['id' => $productId]) 183 | ); 184 | 185 | // review area 186 | $this->assertStringContainsString('id="product_review_area"', $crawler->html()); 187 | 188 | // review content 189 | $reviewArea = $crawler->filter('#product_review_area'); 190 | 191 | // review total 192 | $totalHtml = $reviewArea->filter('.ec-rectHeading')->html(); 193 | $this->assertStringContainsString((string) $max, $totalHtml); 194 | } 195 | 196 | /** 197 | * @param $number 198 | * @param int $productId 199 | */ 200 | private function createProductReviewByNumber($number, $productId = 1) 201 | { 202 | $Product = $this->productRepo->find($productId); 203 | if (!$Product) { 204 | $Product = $this->createProduct(); 205 | } 206 | 207 | for ($i = 0; $i < $number; ++$i) { 208 | $this->createProductReviewData($Product); 209 | } 210 | } 211 | 212 | /** 213 | * Create data. 214 | * 215 | * @param int|Product $product 216 | * 217 | * @return ProductReview 218 | */ 219 | private function createProductReviewData($product = 1) 220 | { 221 | if ($product instanceof Product) { 222 | $Product = $product; 223 | } else { 224 | $Product = $this->productRepo->find($product); 225 | } 226 | 227 | $Display = $this->entityManager->find(ProductReviewStatus::class, ProductReviewStatus::SHOW); 228 | $Sex = $this->sexMasterRepo->find(1); 229 | $Customer = $this->createCustomer(); 230 | 231 | $Review = new ProductReview(); 232 | $Review->setComment($this->faker->word); 233 | $Review->setTitle($this->faker->word); 234 | $Review->setProduct($Product); 235 | $Review->setRecommendLevel($this->faker->numberBetween(1, 5)); 236 | $Review->setReviewerName($this->faker->word); 237 | $Review->setReviewerUrl($this->faker->url); 238 | $Review->setStatus($Display); 239 | $Review->setSex($Sex); 240 | $Review->setCustomer($Customer); 241 | 242 | $this->entityManager->persist($Review); 243 | $this->entityManager->flush($Review); 244 | 245 | return $Review; 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Resource/template/admin/edit.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /* 3 | * This file is part of the ProductReview plugin 4 | * 5 | * Copyright (C) 2016 LOCKON CO.,LTD. All Rights Reserved. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | #} 11 | {# 12 | [商品管理]-[商品レビュー]-[編集]画面 13 | #} 14 | 15 | {% extends '@admin/default_frame.twig' %} 16 | 17 | {% set menus = ['product', 'product_review'] %} 18 | 19 | {% block title %}{{ 'product_review.admin.product_review_edit.title'|trans }}{% endblock %} 20 | {% block sub_title %}{{ 'product_review.admin.product_review_edit.sub_title'|trans }}{% endblock %} 21 | 22 | {% form_theme form '@admin/Form/bootstrap_4_horizontal_layout.html.twig' %} 23 | 24 | {% block main %} 25 | 26 |
28 | {{ form_widget(form._token) }} 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
38 | 39 | {{ 'product_review.admin.product_review_edit.review_detail'|trans() }} 40 | 41 |
42 |
43 |
44 | 47 |
48 |
49 |
50 | 51 |
52 |
53 |
54 |
55 | {{ 'product_review.admin.product_review_edit.product_name'|trans }} 56 |
57 |
58 |
{{ Product.name }}
59 |
60 |
61 | 62 |
63 |
64 | {{ 'product_review.admin.product_review_edit.posted_date'|trans }} 65 |
66 |
67 | {{ ProductReview.create_date|date_min }} 68 |
69 |
70 | 71 |
72 |
73 | {{ '投稿者'|trans }} 74 | {{ 'admin.common.required'|trans }} 75 |
76 |
77 | {{ form_widget(form.reviewer_name) }} 78 | {{ form_errors(form.reviewer_name) }} 79 |
80 |
81 |
82 |
83 | {{ 'URL'|trans }} 84 |
85 |
86 | {{ form_widget(form.reviewer_url) }} 87 | {{ form_errors(form.reviewer_url) }} 88 |
89 |
90 |
91 |
92 | {{ '性別'|trans }} 93 |
94 |
95 | {{ form_widget(form.sex, {'label_attr': {'class': 'radio-inline'}}) }} 96 | {{ form_errors(form.sex) }} 97 |
98 |
99 | 100 |
101 |
102 | {{ 'おすすめレベル'|trans }} 103 | {{ 'admin.common.required'|trans }} 104 |
105 |
106 | {{ form_widget(form.recommend_level) }} 107 | {{ form_errors(form.recommend_level) }} 108 |
109 |
110 | 111 |
112 |
113 | {{ 'タイトル'|trans }} 114 | {{ 'admin.common.required'|trans }} 115 |
116 |
117 | {{ form_widget(form.title) }} 118 | {{ form_errors(form.title) }} 119 |
120 |
121 | 122 |
123 |
124 | {{ 'コメント'|trans }} 125 | {{ 'admin.common.required'|trans }} 126 |
127 |
128 | {{ form_widget(form.comment, {'attr': {'rows': '6'}}) }} 129 | {{ form_errors(form.comment) }} 130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 | 139 |
140 |
141 |
142 | 150 |
151 |
152 |
153 | {{ form_widget(form.Status) }} 154 | {{ form_errors(form.Status) }} 155 |
156 |
157 | 159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 167 | {% endblock %} 168 | -------------------------------------------------------------------------------- /Entity/ProductReview.php: -------------------------------------------------------------------------------- 1 | id; 136 | } 137 | 138 | /** 139 | * Get reviewer_name. 140 | * 141 | * @return string 142 | */ 143 | public function getReviewerName() 144 | { 145 | return $this->reviewer_name; 146 | } 147 | 148 | /** 149 | * Set reviewer_name. 150 | * 151 | * @param string $reviewer_name 152 | * 153 | * @return ProductReview 154 | */ 155 | public function setReviewerName($reviewer_name) 156 | { 157 | $this->reviewer_name = $reviewer_name; 158 | 159 | return $this; 160 | } 161 | 162 | /** 163 | * Get reviewer_url. 164 | * 165 | * @return string 166 | */ 167 | public function getReviewerUrl() 168 | { 169 | return $this->reviewer_url; 170 | } 171 | 172 | /** 173 | * Set reviewer_url. 174 | * 175 | * @param string $reviewer_url 176 | * 177 | * @return ProductReview 178 | */ 179 | public function setReviewerUrl($reviewer_url) 180 | { 181 | $this->reviewer_url = $reviewer_url; 182 | 183 | return $this; 184 | } 185 | 186 | /** 187 | * Get recommend_level. 188 | * 189 | * @return int 190 | */ 191 | public function getRecommendLevel() 192 | { 193 | return $this->recommend_level; 194 | } 195 | 196 | /** 197 | * Set recommend_level. 198 | * 199 | * @param int $recommend_level 200 | * 201 | * @return ProductReview 202 | */ 203 | public function setRecommendLevel($recommend_level) 204 | { 205 | $this->recommend_level = $recommend_level; 206 | 207 | return $this; 208 | } 209 | 210 | /** 211 | * Set Sex. 212 | * 213 | * @param Sex $Sex 214 | * 215 | * @return ProductReview 216 | */ 217 | public function setSex(Sex $Sex = null) 218 | { 219 | $this->Sex = $Sex; 220 | 221 | return $this; 222 | } 223 | 224 | /** 225 | * Get Sex. 226 | * 227 | * @return Sex 228 | */ 229 | public function getSex() 230 | { 231 | return $this->Sex; 232 | } 233 | 234 | /** 235 | * Get title. 236 | * 237 | * @return string 238 | */ 239 | public function getTitle() 240 | { 241 | return $this->title; 242 | } 243 | 244 | /** 245 | * Set title. 246 | * 247 | * @param string $title 248 | * 249 | * @return ProductReview 250 | */ 251 | public function setTitle($title) 252 | { 253 | $this->title = $title; 254 | 255 | return $this; 256 | } 257 | 258 | /** 259 | * Get comment. 260 | * 261 | * @return string 262 | */ 263 | public function getComment() 264 | { 265 | return $this->comment; 266 | } 267 | 268 | /** 269 | * Set comment. 270 | * 271 | * @param string $comment 272 | * 273 | * @return ProductReview 274 | */ 275 | public function setComment($comment) 276 | { 277 | $this->comment = $comment; 278 | 279 | return $this; 280 | } 281 | 282 | /** 283 | * Set Product. 284 | * 285 | * @param Product $Product 286 | * 287 | * @return $this 288 | */ 289 | public function setProduct(Product $Product) 290 | { 291 | $this->Product = $Product; 292 | 293 | return $this; 294 | } 295 | 296 | /** 297 | * Get Product. 298 | * 299 | * @return Product 300 | */ 301 | public function getProduct() 302 | { 303 | return $this->Product; 304 | } 305 | 306 | /** 307 | * Set Customer. 308 | * 309 | * @param Customer $Customer 310 | * 311 | * @return $this 312 | */ 313 | public function setCustomer(Customer $Customer) 314 | { 315 | $this->Customer = $Customer; 316 | 317 | return $this; 318 | } 319 | 320 | /** 321 | * Get Customer. 322 | * 323 | * @return Customer 324 | */ 325 | public function getCustomer() 326 | { 327 | return $this->Customer; 328 | } 329 | 330 | /** 331 | * @return \Plugin\ProductReview42\Entity\ProductReviewStatus 332 | */ 333 | public function getStatus() 334 | { 335 | return $this->Status; 336 | } 337 | 338 | /** 339 | * @param \Plugin\ProductReview42\Entity\ProductReviewStatus $status 340 | */ 341 | public function setStatus(\Plugin\ProductReview42\Entity\ProductReviewStatus $Status) 342 | { 343 | $this->Status = $Status; 344 | } 345 | 346 | /** 347 | * Set create_date. 348 | * 349 | * @param \DateTime $createDate 350 | * 351 | * @return $this 352 | */ 353 | public function setCreateDate($createDate) 354 | { 355 | $this->create_date = $createDate; 356 | 357 | return $this; 358 | } 359 | 360 | /** 361 | * Get create_date. 362 | * 363 | * @return \DateTime 364 | */ 365 | public function getCreateDate() 366 | { 367 | return $this->create_date; 368 | } 369 | 370 | /** 371 | * Set update_date. 372 | * 373 | * @param \DateTime $updateDate 374 | * 375 | * @return $this 376 | */ 377 | public function setUpdateDate($updateDate) 378 | { 379 | $this->update_date = $updateDate; 380 | 381 | return $this; 382 | } 383 | 384 | /** 385 | * Get update_date. 386 | * 387 | * @return \DateTime 388 | */ 389 | public function getUpdateDate() 390 | { 391 | return $this->update_date; 392 | } 393 | } 394 | -------------------------------------------------------------------------------- /Controller/Admin/ProductReviewController.php: -------------------------------------------------------------------------------- 1 | pageMaxRepository = $pageMaxRepository; 71 | $this->productReviewRepository = $productReviewRepository; 72 | $this->productReviewConfigRepository = $productReviewConfigRepository; 73 | $this->csvExportService = $csvExportService; 74 | } 75 | 76 | /** 77 | * Search function. 78 | * 79 | * @Route("/%eccube_admin_route%/product_review/", name="product_review_admin_product_review") 80 | * @Route("/%eccube_admin_route%/product_review/page/{page_no}", requirements={"page_no" = "\d+"}, name="product_review_admin_product_review_page") 81 | * @Template("@ProductReview42/admin/index.twig") 82 | * 83 | * @param Request $request 84 | * @param null $page_no 85 | * 86 | * @return array 87 | */ 88 | public function index(Request $request, PaginatorInterface $paginator, $page_no = null) 89 | { 90 | $CsvType = $this->productReviewConfigRepository 91 | ->get() 92 | ->getCsvType(); 93 | $builder = $this->formFactory->createBuilder(ProductReviewSearchType::class); 94 | $searchForm = $builder->getForm(); 95 | 96 | $pageMaxis = $this->pageMaxRepository->findAll(); 97 | $pageCount = $this->session->get( 98 | 'product_review.admin.product_review.search.page_count', 99 | $this->eccubeConfig['eccube_default_page_count'] 100 | ); 101 | $pageCountParam = $request->get('page_count'); 102 | if ($pageCountParam && is_numeric($pageCountParam)) { 103 | foreach ($pageMaxis as $pageMax) { 104 | if ($pageCountParam == $pageMax->getName()) { 105 | $pageCount = $pageMax->getName(); 106 | $this->session->set('product_review.admin.product_review.search.page_count', $pageCount); 107 | break; 108 | } 109 | } 110 | } 111 | 112 | if ('POST' === $request->getMethod()) { 113 | $searchForm->handleRequest($request); 114 | if ($searchForm->isValid()) { 115 | $searchData = $searchForm->getData(); 116 | $page_no = 1; 117 | 118 | $this->session->set('product_review.admin.product_review.search', FormUtil::getViewData($searchForm)); 119 | $this->session->set('product_review.admin.product_review.search.page_no', $page_no); 120 | } else { 121 | return [ 122 | 'searchForm' => $searchForm->createView(), 123 | 'pagination' => [], 124 | 'pageMaxis' => $pageMaxis, 125 | 'page_no' => $page_no, 126 | 'page_count' => $pageCount, 127 | 'CsvType' => $CsvType, 128 | 'has_errors' => true, 129 | ]; 130 | } 131 | } else { 132 | if (null !== $page_no || $request->get('resume')) { 133 | if ($page_no) { 134 | $this->session->set('product_review.admin.product_review.search.page_no', (int) $page_no); 135 | } else { 136 | $page_no = $this->session->get('product_review.admin.product_review.search.page_no', 1); 137 | } 138 | $viewData = $this->session->get('product_review.admin.product_review.search', []); 139 | } else { 140 | $page_no = 1; 141 | $viewData = FormUtil::getViewData($searchForm); 142 | $this->session->set('product_review.admin.product_review.search', $viewData); 143 | $this->session->set('product_review.admin.product_review.search.page_no', $page_no); 144 | } 145 | $searchData = FormUtil::submitAndGetData($searchForm, $viewData); 146 | } 147 | 148 | $qb = $this->productReviewRepository->getQueryBuilderBySearchData($searchData); 149 | 150 | $pagination = $paginator->paginate( 151 | $qb, 152 | $page_no, 153 | $pageCount 154 | ); 155 | 156 | return [ 157 | 'searchForm' => $searchForm->createView(), 158 | 'pagination' => $pagination, 159 | 'pageMaxis' => $pageMaxis, 160 | 'page_no' => $page_no, 161 | 'page_count' => $pageCount, 162 | 'CsvType' => $CsvType, 163 | 'has_errors' => false, 164 | ]; 165 | } 166 | 167 | /** 168 | * 編集. 169 | * 170 | * @Route("%eccube_admin_route%/product_review/{id}/edit", name="product_review_admin_product_review_edit") 171 | * @Template("@ProductReview42/admin/edit.twig") 172 | * 173 | * @param Request $request 174 | * @param $id 175 | * 176 | * @return array|RedirectResponse 177 | */ 178 | public function edit(Request $request, ProductReview $ProductReview) 179 | { 180 | $Product = $ProductReview->getProduct(); 181 | if (!$Product) { 182 | $this->addError('product_review.admin.product.not_found', 'admin'); 183 | 184 | return $this->redirectToRoute('product_review_admin_product_review', ['resume' => 1]); 185 | } 186 | 187 | $form = $this->createForm(ProductReviewType::class, $ProductReview); 188 | $form->handleRequest($request); 189 | 190 | if ($form->isSubmitted() && $form->isValid()) { 191 | $ProductReview = $form->getData(); 192 | $this->entityManager->persist($ProductReview); 193 | $this->entityManager->flush($ProductReview); 194 | 195 | log_info('Product review edit'); 196 | 197 | $this->addSuccess('product_review.admin.save.complete', 'admin'); 198 | 199 | return $this->redirectToRoute( 200 | 'product_review_admin_product_review_edit', 201 | ['id' => $ProductReview->getId()] 202 | ); 203 | } 204 | 205 | return [ 206 | 'form' => $form->createView(), 207 | 'Product' => $Product, 208 | 'ProductReview' => $ProductReview, 209 | ]; 210 | } 211 | 212 | /** 213 | * Product review delete function. 214 | * 215 | * @Route("%eccube_admin_route%/product_review/{id}/delete", name="product_review_admin_product_review_delete", methods={"DELETE"}) 216 | * 217 | * @param Request $request 218 | * @param int $id 219 | * 220 | * @return RedirectResponse 221 | */ 222 | public function delete(ProductReview $ProductReview) 223 | { 224 | $this->isTokenValid(); 225 | 226 | $this->entityManager->remove($ProductReview); 227 | $this->entityManager->flush($ProductReview); 228 | $this->addSuccess('product_review.admin.delete.complete', 'admin'); 229 | 230 | log_info('Product review delete', ['id' => $ProductReview->getId()]); 231 | 232 | return $this->redirect($this->generateUrl('product_review_admin_product_review_page', ['resume' => 1])); 233 | } 234 | 235 | /** 236 | * 商品レビューCSVの出力. 237 | * 238 | * @Route("%eccube_admin_route%/product_review/download", name="product_review_admin_product_review_download") 239 | * 240 | * @param Request $request 241 | * 242 | * @return StreamedResponse 243 | */ 244 | public function download(Request $request) 245 | { 246 | // タイムアウトを無効にする. 247 | set_time_limit(0); 248 | 249 | // sql loggerを無効にする. 250 | $em = $this->entityManager; 251 | $em->getConfiguration()->setSQLLogger(null); 252 | $response = new StreamedResponse(); 253 | $response->setCallback(function () use ($request) { 254 | /** @var ProductReviewConfig $Config */ 255 | $Config = $this->productReviewConfigRepository->get(); 256 | $csvType = $Config->getCsvType(); 257 | 258 | /* @var $csvService CsvExportService */ 259 | $csvService = $this->csvExportService; 260 | 261 | /* @var $repo ProductReviewRepository */ 262 | $repo = $this->productReviewRepository; 263 | 264 | // CSV種別を元に初期化. 265 | $csvService->initCsvType($csvType); 266 | 267 | // ヘッダ行の出力. 268 | $csvService->exportHeader(); 269 | 270 | $session = $request->getSession(); 271 | $searchForm = $this->createForm(ProductReviewSearchType::class); 272 | 273 | $viewData = $session->get('eccube.admin.product.search', []); 274 | $searchData = FormUtil::submitAndGetData($searchForm, $viewData); 275 | 276 | $qb = $repo->getQueryBuilderBySearchData($searchData); 277 | 278 | // データ行の出力. 279 | $csvService->setExportQueryBuilder($qb); 280 | $csvService->exportData(function ($entity, CsvExportService $csvService) { 281 | $arrCsv = $csvService->getCsvs(); 282 | 283 | $row = []; 284 | // CSV出力項目と合致するデータを取得. 285 | foreach ($arrCsv as $csv) { 286 | // 受注データを検索. 287 | $data = $csvService->getData($csv, $entity); 288 | $row[] = $data; 289 | } 290 | // 出力. 291 | $csvService->fputcsv($row); 292 | }); 293 | }); 294 | 295 | $now = new \DateTime(); 296 | $filename = 'product_review_'.$now->format('YmdHis').'.csv'; 297 | $response->headers->set('Content-Type', 'application/octet-stream'); 298 | $response->headers->set('Content-Disposition', 'attachment; filename='.$filename); 299 | 300 | log_info('商品レビューCSV出力ファイル名', [$filename]); 301 | 302 | return $response; 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /PluginManager.php: -------------------------------------------------------------------------------- 1 | 'レビューを表示', 35 | 'url' => 'product_review_display', 36 | 'filename' => 'ProductReview42/Resource/template/default/review', 37 | ], 38 | [ 39 | 'name' => 'レビューを投稿', 40 | 'url' => 'product_review_index', 41 | 'filename' => 'ProductReview42/Resource/template/default/index', 42 | ], 43 | [ 44 | 'name' => 'レビューを投稿(確認)', 45 | 'url' => 'product_review_confirm', 46 | 'filename' => 'ProductReview42/Resource/template/default/confirm', 47 | ], 48 | [ 49 | 'name' => 'レビューを投稿(完了)', 50 | 'url' => 'product_review_complete', 51 | 'filename' => 'ProductReview42/Resource/template/default/complete', 52 | ], 53 | ]; 54 | 55 | public function enable(array $meta, ContainerInterface $container) 56 | { 57 | $em = $container->get('doctrine')->getManager(); 58 | 59 | // プラグイン設定を追加 60 | $Config = $this->createConfig($em); 61 | 62 | // レビューステータス(公開・非公開)を追加 63 | $this->createStatus($em); 64 | 65 | // CSV出力項目設定を追加 66 | $CsvType = $Config->getCsvType(); 67 | if (null === $CsvType) { 68 | $CsvType = $this->createCsvType($em); 69 | $this->createCsvData($em, $CsvType); 70 | 71 | $Config->setCsvType($CsvType); 72 | $em->flush($Config); 73 | } 74 | 75 | // ページを追加 76 | foreach ($this->pages as $pageInfo) { 77 | $Page = $em->getRepository(Page::class)->findOneBy(['url' => $pageInfo['url']]); 78 | if (null === $Page) { 79 | $this->createPage($em, $pageInfo['name'], $pageInfo['url'], $pageInfo['filename']); 80 | } 81 | } 82 | 83 | $this->copyTwigFiles($container); 84 | } 85 | 86 | public function disable(array $meta, ContainerInterface $container) 87 | { 88 | $em = $container->get('doctrine.orm.entity_manager'); 89 | 90 | // ページを削除 91 | foreach ($this->pages as $pageInfo) { 92 | $this->removePage($em, $pageInfo['url']); 93 | } 94 | } 95 | 96 | public function uninstall(array $meta, ContainerInterface $container) 97 | { 98 | $em = $container->get('doctrine')->getManager(); 99 | 100 | // ページを削除 101 | foreach ($this->pages as $pageInfo) { 102 | $this->removePage($em, $pageInfo['url']); 103 | } 104 | 105 | $this->removeTwigFiles($container); 106 | 107 | $Config = $em->find(ProductReviewConfig::class, 1); 108 | if ($Config) { 109 | $CsvType = $Config->getCsvType(); 110 | 111 | // CSV出力項目設定を削除 112 | $this->removeCsvData($em, $CsvType); 113 | 114 | $Config->setCsvType(null); 115 | $em->flush($Config); 116 | 117 | $em->remove($CsvType); 118 | $em->flush($CsvType); 119 | } 120 | } 121 | 122 | protected function createConfig(EntityManagerInterface $em) 123 | { 124 | $Config = $em->find(ProductReviewConfig::class, 1); 125 | if ($Config) { 126 | return $Config; 127 | } 128 | $Config = new ProductReviewConfig(); 129 | $Config->setReviewMax(5); 130 | 131 | $em->persist($Config); 132 | $em->flush($Config); 133 | 134 | return $Config; 135 | } 136 | 137 | protected function createStatus(EntityManagerInterface $em) 138 | { 139 | $Status = $em->find(ProductReviewStatus::class, 1); 140 | if ($Status) { 141 | return; 142 | } 143 | 144 | $Status = new ProductReviewStatus(); 145 | $Status->setId(1); 146 | $Status->setName('公開'); 147 | $Status->setSortNo(1); 148 | 149 | $em->persist($Status); 150 | $em->flush($Status); 151 | 152 | $Status = new ProductReviewStatus(); 153 | $Status->setId(2); 154 | $Status->setName('非公開'); 155 | $Status->setSortNo(2); 156 | 157 | $em->persist($Status); 158 | $em->flush($Status); 159 | } 160 | 161 | protected function createCsvType(EntityManagerInterface $em) 162 | { 163 | $result = $em->createQueryBuilder('ct') 164 | ->select('COALESCE(MAX(ct.id), 0) AS id, COALESCE(MAX(ct.sort_no), 0) AS sort_no') 165 | ->from(CsvType::class, 'ct') 166 | ->getQuery() 167 | ->getSingleResult(); 168 | 169 | $result['id']++; 170 | $result['sort_no']++; 171 | 172 | $CsvType = new CsvType(); 173 | $CsvType 174 | ->setId($result['id']) 175 | ->setName('商品レビューCSV') 176 | ->setSortNo($result['sort_no']); 177 | $em->persist($CsvType); 178 | $em->flush($CsvType); 179 | 180 | return $CsvType; 181 | } 182 | 183 | protected function createPage(EntityManagerInterface $em, $name, $url, $filename) 184 | { 185 | $Page = new Page(); 186 | $Page->setEditType(Page::EDIT_TYPE_DEFAULT); 187 | $Page->setName($name); 188 | $Page->setUrl($url); 189 | $Page->setFileName($filename); 190 | 191 | // DB登録 192 | $em->persist($Page); 193 | $em->flush($Page); 194 | $Layout = $em->find(Layout::class, Layout::DEFAULT_LAYOUT_UNDERLAYER_PAGE); 195 | $PageLayout = new PageLayout(); 196 | $PageLayout->setPage($Page) 197 | ->setPageId($Page->getId()) 198 | ->setLayout($Layout) 199 | ->setLayoutId($Layout->getId()) 200 | ->setSortNo(0); 201 | $em->persist($PageLayout); 202 | $em->flush($PageLayout); 203 | } 204 | 205 | protected function copyTwigFiles(ContainerInterface $container) 206 | { 207 | $templatePath = $container->get(EccubeConfig::class)->get('eccube_theme_front_dir') 208 | .'/ProductReview42/Resource/template/default'; 209 | $fs = new Filesystem(); 210 | if ($fs->exists($templatePath)) { 211 | return; 212 | } 213 | $fs->mkdir($templatePath); 214 | $fs->mirror(__DIR__.'/Resource/template/default', $templatePath); 215 | } 216 | 217 | protected function createCsvData(EntityManagerInterface $em, CsvType $CsvType) 218 | { 219 | $rank = 1; 220 | $Csv = new Csv(); 221 | $Csv->setCsvType($CsvType) 222 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 223 | ->setFieldName('Product') 224 | ->setReferenceFieldName('name') 225 | ->setDispName('商品名') 226 | ->setSortNo($rank); 227 | $em->persist($Csv); 228 | $em->flush(); 229 | 230 | $Csv = new Csv(); 231 | ++$rank; 232 | $Csv->setCsvType($CsvType) 233 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 234 | ->setFieldName('Status') 235 | ->setReferenceFieldName('name') 236 | ->setDispName('公開・非公開') 237 | ->setSortNo($rank); 238 | $em->persist($Csv); 239 | $em->flush(); 240 | 241 | $Csv = new Csv(); 242 | ++$rank; 243 | $Csv->setCsvType($CsvType) 244 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 245 | ->setFieldName('create_date') 246 | ->setReferenceFieldName('create_date') 247 | ->setDispName('投稿日') 248 | ->setSortNo($rank); 249 | $em->persist($Csv); 250 | $em->flush(); 251 | 252 | $Csv = new Csv(); 253 | ++$rank; 254 | $Csv->setCsvType($CsvType) 255 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 256 | ->setFieldName('reviewer_name') 257 | ->setReferenceFieldName('reviewer_name') 258 | ->setDispName('投稿者名') 259 | ->setSortNo($rank); 260 | $em->persist($Csv); 261 | $em->flush(); 262 | 263 | $Csv = new Csv(); 264 | ++$rank; 265 | $Csv->setCsvType($CsvType) 266 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 267 | ->setFieldName('reviewer_url') 268 | ->setReferenceFieldName('reviewer_url') 269 | ->setDispName('投稿者URL') 270 | ->setSortNo($rank); 271 | $em->persist($Csv); 272 | $em->flush(); 273 | 274 | $Csv = new Csv(); 275 | ++$rank; 276 | $Csv->setCsvType($CsvType) 277 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 278 | ->setFieldName('Sex') 279 | ->setReferenceFieldName('name') 280 | ->setDispName('性別') 281 | ->setSortNo($rank); 282 | $em->persist($Csv); 283 | $em->flush(); 284 | 285 | $Csv = new Csv(); 286 | ++$rank; 287 | $Csv->setCsvType($CsvType) 288 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 289 | ->setFieldName('recommend_level') 290 | ->setReferenceFieldName('recommend_level') 291 | ->setDispName('おすすめレベル') 292 | ->setSortNo($rank); 293 | $em->persist($Csv); 294 | $em->flush(); 295 | 296 | $Csv = new Csv(); 297 | ++$rank; 298 | $Csv->setCsvType($CsvType) 299 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 300 | ->setFieldName('title') 301 | ->setReferenceFieldName('title') 302 | ->setDispName('タイトル') 303 | ->setSortNo($rank); 304 | $em->persist($Csv); 305 | $em->flush(); 306 | 307 | $Csv = new Csv(); 308 | ++$rank; 309 | $Csv->setCsvType($CsvType) 310 | ->setEntityName('Plugin\ProductReview42\Entity\ProductReview') 311 | ->setFieldName('comment') 312 | ->setReferenceFieldName('comment') 313 | ->setDispName('コメント') 314 | ->setSortNo($rank); 315 | $em->persist($Csv); 316 | $em->flush(); 317 | 318 | return $CsvType; 319 | } 320 | 321 | protected function removePage(EntityManagerInterface $em, $url) 322 | { 323 | $Page = $em->getRepository(Page::class)->findOneBy(['url' => $url]); 324 | 325 | if (!$Page) { 326 | return; 327 | } 328 | foreach ($Page->getPageLayouts() as $PageLayout) { 329 | $em->remove($PageLayout); 330 | $em->flush($PageLayout); 331 | } 332 | 333 | $em->remove($Page); 334 | $em->flush($Page); 335 | } 336 | 337 | protected function removeTwigFiles(ContainerInterface $container) 338 | { 339 | $templatePath = $container->get(EccubeConfig::class)->get('eccube_theme_front_dir') 340 | .'/ProductReview42'; 341 | $fs = new Filesystem(); 342 | $fs->remove($templatePath); 343 | } 344 | 345 | protected function removeCsvData(EntityManagerInterface $em, CsvType $CsvType) 346 | { 347 | $CsvData = $em->getRepository(Csv::class)->findBy(['CsvType' => $CsvType]); 348 | foreach ($CsvData as $Csv) { 349 | $em->remove($Csv); 350 | $em->flush($Csv); 351 | } 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /Tests/Web/ReviewAdminControllerTest.php: -------------------------------------------------------------------------------- 1 | faker = $this->getFaker(); 63 | $this->deleteAllRows(['plg_product_review']); 64 | 65 | $this->productRepo = $this->entityManager->getRepository(Product::class); 66 | $this->sexMasterRepo = $this->entityManager->getRepository(Sex::class); 67 | $this->productReviewRepo = $this->entityManager->getRepository(ProductReview::class); 68 | } 69 | 70 | /** 71 | * Search list. 72 | */ 73 | public function testReviewList() 74 | { 75 | $number = 5; 76 | $this->createProductReviewByNumber($number); 77 | $crawler = $this->client->request('GET', $this->generateUrl('product_review_admin_product_review')); 78 | $this->assertStringContainsString('レビュー管理', $crawler->html()); 79 | $form = $crawler->selectButton('検索')->form(); 80 | $crawlerSearch = $this->client->submit($form); 81 | $this->assertStringContainsString('検索結果', $crawlerSearch->html()); 82 | /* @var $crawlerSearch Crawler */ 83 | $actual = $crawlerSearch->filter('form#search_form span#search-result')->html(); 84 | 85 | $this->actual = preg_replace('/\D/', '', $actual); 86 | $this->expected = $number; 87 | 88 | $this->assertStringContainsString((string) $this->expected, $this->actual); 89 | } 90 | 91 | /** 92 | * test delete. 93 | */ 94 | public function testReviewDeleteIdNotFound() 95 | { 96 | $this->client->request( 97 | 'DELETE', 98 | $this->generateUrl('product_review_admin_product_review_delete', ['id' => 99999]) 99 | ); 100 | 101 | $this->assertEquals(404, $this->client->getResponse()->getStatusCode()); 102 | } 103 | 104 | /** 105 | * test delete. 106 | */ 107 | public function testReviewDelete() 108 | { 109 | $Review = $this->createProductReviewData(); 110 | $productReviewId = $Review->getId(); 111 | $this->client->request( 112 | 'DELETE', 113 | $this->generateUrl('product_review_admin_product_review_delete', ['id' => $productReviewId]) 114 | ); 115 | $this->assertTrue($this->client->getResponse()->isRedirection()); 116 | 117 | $this->assertNull($this->productReviewRepo->find($productReviewId)); 118 | } 119 | 120 | /** 121 | * Test edit. 122 | */ 123 | public function testReviewEditWithIdInvalid() 124 | { 125 | /* 126 | * @var $crawler Crawler 127 | */ 128 | $crawler = $this->client->request( 129 | 'GET', 130 | $this->generateUrl('product_review_admin_product_review_edit', ['id' => 99999]) 131 | ); 132 | 133 | $this->assertEquals($this->client->getResponse()->getStatusCode(), Response::HTTP_NOT_FOUND); 134 | } 135 | 136 | /** 137 | * Test edit. 138 | */ 139 | public function testReviewEditWithProductReviewDeleted() 140 | { 141 | $Review = $this->createProductReviewData(); 142 | $reviewId = $Review->getId(); 143 | 144 | $this->productReviewRepo->delete($Review); 145 | $this->entityManager->flush(); 146 | $this->entityManager->detach($Review); 147 | 148 | $this->client->request( 149 | 'GET', 150 | $this->generateUrl('product_review_admin_product_review_edit', ['id' => $reviewId]) 151 | ); 152 | $this->assertEquals($this->client->getResponse()->getStatusCode(), Response::HTTP_NOT_FOUND); 153 | } 154 | 155 | /** 156 | * Test edit. 157 | */ 158 | public function testReviewEditSuccess() 159 | { 160 | $Review = $this->createProductReviewData(); 161 | $reviewId = $Review->getId(); 162 | $fakeTitle = $this->faker->word; 163 | 164 | $crawler = $this->client->request( 165 | 'GET', 166 | $this->generateUrl('product_review_admin_product_review_edit', ['id' => $reviewId]) 167 | ); 168 | $form = $crawler->selectButton('登録')->form(); 169 | $form['product_review[recommend_level]'] = 1; 170 | $form['product_review[title]'] = $fakeTitle; 171 | $crawler = $this->client->submit($form); 172 | 173 | $crawler = $this->client->followRedirect(); 174 | // check message. 175 | $this->expected = '登録しました。'; 176 | $this->actual = $crawler->filter('.alert')->html(); 177 | $this->assertStringContainsString($this->expected, $this->actual); 178 | 179 | // Check entity 180 | $this->expected = $fakeTitle; 181 | $this->actual = $this->productReviewRepo->find($reviewId)->getTitle(); 182 | $this->verify(); 183 | 184 | // Stay in edit page 185 | $this->assertStringContainsString('レビュー管理', $crawler->html()); 186 | } 187 | 188 | /** 189 | * Search test. 190 | */ 191 | public function testReviewSearch() 192 | { 193 | $review = $this->createProductReviewData(); 194 | $form = $this->initForm($review); 195 | 196 | $crawler = $this->client->request( 197 | 'POST', 198 | $this->generateUrl('product_review_admin_product_review'), 199 | ['product_review_search' => $form] 200 | ); 201 | 202 | $this->assertStringContainsString('検索', $crawler->html()); 203 | 204 | $numberResult = $crawler->filter('#search_form #search-result')->html(); 205 | 206 | $numberResult = preg_replace('/\D/', '', $numberResult); 207 | $this->assertStringContainsString('1', $numberResult); 208 | 209 | $table = $crawler->filter('.table tbody'); 210 | $this->assertStringContainsString($review->getReviewerName(), $table->html()); 211 | } 212 | 213 | /** 214 | * Search test. 215 | */ 216 | public function testReviewSearchWithPaging() 217 | { 218 | $number = 51; 219 | $this->createProductReviewByNumber($number); 220 | 221 | $crawler = $this->client->request('GET', $this->generateUrl('product_review_admin_product_review')); 222 | $this->assertStringContainsString('検索', $crawler->html()); 223 | $form = $crawler->selectButton('検索')->form(); 224 | $crawlerSearch = $this->client->submit($form); 225 | 226 | $numberResult = $crawlerSearch->filter('form#search_form #search-result'); 227 | $numberResult = preg_replace('/\D/', '', $numberResult->html()); 228 | $this->assertStringContainsString((string) $number, $numberResult); 229 | 230 | /* @var $crawler Crawler */ 231 | $crawler = $this->client->request('GET', $this->generateUrl('product_review_admin_product_review_page', ['page_no' => 2])); 232 | 233 | // page 2 234 | $paging = $crawler->filter('ul.pagination .page-item')->last(); 235 | 236 | // Current active on page 2. 237 | $this->assertStringContainsString('active', $paging->ancestors()->html()); 238 | $this->expected = 2; 239 | $this->actual = intval($paging->text()); 240 | $this->verify(); 241 | } 242 | 243 | /** 244 | * Download csv test. 245 | */ 246 | public function testDownloadCsv() 247 | { 248 | $Product = $this->createProduct(); 249 | $review = $this->createProductReviewData($Product->getId()); 250 | $form = $this->initForm($review); 251 | $crawler = $this->client->request( 252 | 'POST', 253 | $this->generateUrl('product_review_admin_product_review'), 254 | ['product_review_search' => $form] 255 | ); 256 | 257 | $this->assertStringContainsString('検索', $crawler->html()); 258 | $numberResult = $crawler->filter('form#search_form span#search-result')->html(); 259 | 260 | $numberResult = preg_replace('/\D/', '', $numberResult); 261 | $this->assertStringContainsString('1', $numberResult); 262 | 263 | $table = $crawler->filter('.table tbody'); 264 | 265 | $this->assertStringContainsString($review->getReviewerName(), $table->html()); 266 | 267 | if (version_compare(Constant::VERSION, '4.3', '<')) { 268 | $this->expectOutputRegex("/{$review->getTitle()}/"); 269 | } 270 | 271 | $this->client->request( 272 | 'POST', 273 | $this->generateUrl('product_review_admin_product_review_download') 274 | ); 275 | 276 | $this->assertTrue($this->client->getResponse()->isSuccessful()); 277 | 278 | if (version_compare(Constant::VERSION, '4.3', '>=')) { 279 | $content = $this->client->getInternalResponse()->getContent(); 280 | $content = mb_convert_encoding($content, 'UTF-8', 'SJIS-win'); 281 | $this->assertMatchesRegularExpression("/{$review->getTitle()}/", $content); 282 | } 283 | } 284 | 285 | /** 286 | * Search form. 287 | * 288 | * @param ProductReview $review 289 | * 290 | * @return array 291 | */ 292 | private function initForm(ProductReview $review) 293 | { 294 | return [ 295 | '_token' => 'dummy', 296 | 'multi' => $review->getReviewerName(), 297 | 'product_name' => $review->getProduct()->getName(), 298 | 'product_code' => $review->getProduct()->getCodeMax(), 299 | 'sex' => [$review->getSex()->getId()], 300 | 'recommend_level' => $review->getRecommendLevel(), 301 | 'review_start' => $review->getCreateDate()->modify('-2 days')->format('Y-m-d'), 302 | 'review_end' => $review->getCreateDate()->modify('+2 days')->format('Y-m-d'), 303 | ]; 304 | } 305 | 306 | /** 307 | * @param $number 308 | * @param int $productId 309 | */ 310 | private function createProductReviewByNumber($number, $productId = 1) 311 | { 312 | $Product = $this->productRepo->find($productId); 313 | if (!$Product) { 314 | $Product = $this->createProduct(); 315 | } 316 | 317 | for ($i = 0; $i < $number; ++$i) { 318 | $this->createProductReviewData($Product); 319 | } 320 | } 321 | 322 | /** 323 | * Create data. 324 | * 325 | * @param int $product 326 | * 327 | * @return ProductReview 328 | */ 329 | private function createProductReviewData($product = 1) 330 | { 331 | if ($product instanceof Product) { 332 | $Product = $product; 333 | } else { 334 | $Product = $this->productRepo->find($product); 335 | } 336 | 337 | $Display = $this->entityManager->find(ProductReviewStatus::class, ProductReviewStatus::SHOW); 338 | $Sex = $this->sexMasterRepo->find(1); 339 | $Customer = $this->createCustomer(); 340 | 341 | $Review = new ProductReview(); 342 | $Review->setComment($this->faker->word); 343 | $Review->setTitle($this->faker->word); 344 | $Review->setProduct($Product); 345 | $Review->setRecommendLevel($this->faker->numberBetween(1, 5)); 346 | $Review->setReviewerName($this->faker->word); 347 | $Review->setReviewerUrl($this->faker->url); 348 | $Review->setStatus($Display); 349 | $Review->setSex($Sex); 350 | $Review->setCustomer($Customer); 351 | 352 | $this->entityManager->persist($Review); 353 | $this->entityManager->flush(); 354 | 355 | return $Review; 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /Resource/template/admin/index.twig: -------------------------------------------------------------------------------- 1 | {# 2 | /* 3 | * This file is part of the ProductReview plugin 4 | * 5 | * Copyright (C) 2016 LOCKON CO.,LTD. All Rights Reserved. 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | #} 11 | {# 12 | [商品管理]-[商品レビュー]-[一覧・検索]画面 13 | #} 14 | 15 | {% extends '@admin/default_frame.twig' %} 16 | 17 | {% set menus = ['product', 'product_review'] %} 18 | {% block title %}{{ 'product_review.admin.product_review.title'|trans }}{% endblock %} 19 | {% block sub_title %}{{ 'product_review.admin.product_review.sub_title'|trans }}{% endblock %} 20 | 21 | {% form_theme searchForm '@admin/Form/bootstrap_4_layout.html.twig' %} 22 | 23 | {% block stylesheet %} 24 | 25 | 36 | {% endblock stylesheet %} 37 | 38 | {% block javascript %} 39 | 70 | {% endblock javascript %} 71 | 72 | {% block main %} 73 | 74 |
75 |
76 | {{ form_widget(searchForm._token) }} 77 |
78 |
79 |
80 |
81 | 82 | {{ form_widget(searchForm.multi) }} 83 | {{ form_errors(searchForm.multi) }} 84 |
85 | 90 |
91 |
92 |
93 |
94 |
95 |
96 | 97 |
98 | {{ form_widget(searchForm.product_name) }} 99 | {{ form_errors(searchForm.product_name) }} 100 |
101 |
102 |
103 | 104 |
105 | {{ form_widget(searchForm.product_code) }} 106 | {{ form_errors(searchForm.product_code) }} 107 |
108 |
109 |
110 | 111 |
112 | {{ form_widget(searchForm.recommend_level) }} 113 | {{ form_errors(searchForm.recommend_level) }} 114 |
115 |
116 |
117 | 118 |
119 | {{ form_widget(searchForm.review_start) }}{{ form_errors(searchForm.review_start) }} ~ {{ form_widget(searchForm.review_end) }}{{ form_errors(searchForm.review_end) }} 120 |
121 |
122 |
123 | 124 |
125 | {{ form_widget(searchForm.sex) }} 126 | {{ form_errors(searchForm.sex) }} 127 |
128 |
129 |
130 | 131 |
132 | {{ form_widget(searchForm.status) }} 133 | {{ form_errors(searchForm.status) }} 134 |
135 |
136 |
137 |
138 | 140 |
141 |
142 |
143 | 145 | {% if pagination %} 146 | {{ 'product_review.admin.product_review.search_result_count'|trans({"%count%":pagination.totalItemCount}) }} 148 | {% endif %} 149 |
150 |
151 | {{ include('@admin/search_items.twig', { 'form': searchForm }, ignore_missing = true) }} 152 |
153 |
154 |
155 | 156 |
157 |
158 |
159 | {% if pagination and pagination.totalItemCount %} 160 | 161 |
162 |
163 |   164 |
165 |
166 |
167 |
168 | 174 |
175 |
176 |
177 |
178 | 183 | 187 |
188 |
189 |
190 |
191 |
192 |
193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | {% for Review in pagination %} 208 | 209 | 210 | 211 | 212 | 213 | 216 | 217 | 218 | 268 | 269 | {% endfor %} 270 | 271 |
ID{{ 'product_review.admin.product_review.th_posted_date'|trans }}{{ 'product_review.admin.product_review.th_contributor'|trans }}{{ 'product_review.admin.product_review.th_product_name'|trans }}{{ 'product_review.admin.product_review.th_title'|trans }}{{ 'product_review.admin.product_review.th_level'|trans }}{{ 'product_review.admin.product_review.th_status'|trans }} 
{{ Review.id }}{{ Review.create_date|date_min }}{{ Review.reviewer_name }}{{ Review.Product.name }} 214 | {{ Review.title }} 215 | {% for i in 1..Review.recommend_level %}★{% endfor %}{{ Review.status }} 219 |
220 | 225 | 226 | 233 |
234 | 267 |
272 |
273 | 274 |
275 | {% if pagination|length > 0 %} 276 | {% include "@admin/pager.twig" with { 'pages' : pagination.paginationData, 'routes' : 'product_review_admin_product_review_page' } %} 277 | {% endif %} 278 |
279 | {% elseif has_errors %} 280 |
281 |
282 |
{{ 'product_review.admin.product_review.search_invalid_condition'|trans }}
283 |
{{ 'product_review.admin.product_review.search_change_condition_and_retry'|trans }}
284 |
285 |
286 | {% else %} 287 |
288 |
289 |
{{ 'product_review.admin.product_review.search_no_result'|trans }}
290 |
{{ 'product_review.admin.product_review.search_change_condition_and_retry'|trans }}
291 |
{{ 'product_review.admin.product_review.search_try_detail_condition'|trans }}
292 |
293 |
294 | {% endif %} 295 |
296 |
297 |
298 |
299 | {% endblock %} 300 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | --------------------------------------------------------------------------------