1 <?php
2
3 4 5 6 7 8
9 class Catalog extends Page implements ImportInterface {
10
11 static $icon = array('cms/images/treeicons/folder', 'folder');
12 static $allowed_children = array('Product', 'Catalog', 'SpecialCatalog');
13 static $default_child = 'Product';
14
15 static $db = array(
16 'Description' => 'Text',
17 'OwnFilter' => 'Boolean',
18 'FilterFields' => 'Varchar(255)',
19 'StandartView' => 'Varchar',
20 'ImportID' => 'Varchar'
21 );
22
23 static $defaults = array(
24 'AutoChild' => 0,
25 'FilterType' => false,
26 );
27
28 static $has_one = array(
29 'Photo' => 'Image'
30 );
31
32 static $indexes = array(
33 'ImportID' => true,
34 );
35
36 static $subpage_children = 'Product';
37
38 39 40
41 private static $view_options = array('table');
42
43 44 45 46 47 48
49 static function set_view_options(array $data) {
50 self::$view_options = $data;
51 }
52
53 54 55 56 57
58 static function get_view_options() {
59 return self::$view_options;
60 }
61
62 63 64 65 66 67 68
69 static function view_options_dropdown_map($addDefault = false) {
70 $map = array();
71 if ($addDefault) {
72 $map[''] = _t('Catalog.ViewOption_default', 'Default');
73 }
74 foreach (self::get_view_options() as $key) {
75 $map[$key] = _t('Catalog.ViewOption_'.$key, ucfirst($key));
76 }
77 return $map;
78 }
79
80 81 82
83 private static $user_can_change_view = false;
84
85 86 87 88 89
90 static function allow_user_change_view($val = true) {
91 self::$user_can_change_view = $val;
92 }
93
94 95 96
97 private static $show_products_from_subcategories = false;
98
99 100 101 102
103 static function get_show_products_from_subcategories() {
104 return self::$show_products_from_subcategories;
105 }
106
107 108 109 110 111
112 static function set_show_products_from_subcategories($val = true) {
113 self::$show_products_from_subcategories = $val;
114 }
115
116 117 118
119 static $hide_allow_purchase_products = false;
120
121 122 123 124 125
126 static function hide_allow_purchase_products($val) {
127 self::$hide_allow_purchase_products = $val;
128 }
129
130
131 132 133
134 static $possibleFields = array('Title', 'Description', 'Content', 'URLSegment', 'MenuTitle', 'MetaTitle', 'MetaDescription', 'MetaKeywords', 'Sort');
135
136 137 138 139 140
141 static function addPossibleFields($fields) {
142 if ($fields)
143 foreach($fields as $field)
144 self::$possibleFields[] = $field;
145 }
146
147 148 149 150
151 static function import_find($importID) {
152 return DataObject::get_one('Catalog', "ImportID = '" . Convert::raw2sql($importID) . "'");
153 }
154
155 156 157 158 159 160 161
162 function importUpdate($importLog, $data) {
163 if (!$this->importValidate($importLog, $data)) {
164 return false;
165 }
166
167 $rs = $this->extend('onBeforeImport', $importLog, $data);
168 if ($rs && max($rs) == false) return false;
169
170 if (isset($data['id'])) {
171 $this->ImportID = $data['id'];
172 }
173
174 foreach(self::$possibleFields as $field) {
175 if (isset($data[$field]))
176 $this->{$field} = $data[$field];
177 }
178
179 if (isset($data['Photo'])) {
180 $this->PhotoID = ($data['Photo']) ? $data['Photo']->ID : 0;
181 }
182
183 if (isset($data['ParentID']) && !$this->ParentID) {
184 $this->ParentID = $data['ParentID'];
185 }
186 if (isset($data['Publish'])) {
187 if ($data['Publish']) {
188 $this->doPublish();
189 } else {
190 $this->doUnpublish();
191 }
192 } else {
193
194 if ($this->isPublished()) {
195 $this->doPublish();
196 } else {
197 $this->write();
198 }
199 }
200 if (isset($data['Unpublish']) && $data['Unpublish']) {
201 $this->doUnpublish();
202 }
203 $this->extend('onAfterImport', $importLog, $data);
204 return true;
205 }
206
207 208 209 210 211
212 function importValidate($importLog, $data) {
213 if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) {
214 $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
215 return false;
216 }
217 if (isset($data['Title']) && trim($data['Title']) == '') {
218 $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
219 return false;
220 }
221 if ((!$this->Title) && (!isset($data['Title']) || trim($data['Title']) == '')) {
222 $importLog->addLog("Раздел каталога с id='{$data['id']}' не имеет названия!", 'error');
223 return false;
224 }
225 if (isset($data['Sort']) && $data['Sort'] != (int)$data['Sort']) {
226 $importLog->addLog("Параметр Sort раздела каталога {$data['Title']} не является целым числом!", 'warning');
227 $data['Sort'] = 0;
228 }
229 $rs = $this->extend('importValidate', $importLog, $data);
230 if ($rs && max($rs) == false) return false;
231 return true;
232 }
233
234 235 236
237 function importClearAll($importLog) {
238 $oldMode = Versioned::get_reading_mode();
239
240 Versioned::reading_stage('Stage');
241 $catalogs = DataObject::get('Catalog', "ClassName <> 'SpecialCatalog'");
242 if ($catalogs)
243 foreach($catalogs as $catalog) {
244 $catalog->doUnpublish();
245 $catalog->delete();
246 }
247 unset($catalogs);
248
249 Versioned::reading_stage('Live');
250 $catalogs = DataObject::get('Catalog', "ClassName <> 'SpecialCatalog'");
251 if ($catalogs)
252 foreach($catalogs as $catalog) {
253 $catalog->doUnpublish();
254 $catalog->delete();
255 }
256
257 Versioned::set_reading_mode($oldMode);
258 }
259
260
261 262 263 264 265 266
267 static function productFieldTable($field) {
268 $classes = array_reverse(ClassInfo::ancestry(singleton(static::$subpage_children)));
269 foreach($classes as $class) {
270 $fields = Object::uninherited_static($class, 'db');
271 if (isset($fields[$field])) {
272 return $class;
273 }
274 $fields = Object::uninherited_static($class, 'has_one');
275 if ((substr($field, -2) == 'ID') && ($hasOneField = substr($field, 0, -2))) {
276 if (isset($fields[$hasOneField])) {
277 return $class;
278 }
279 }
280 }
281 return false;
282 }
283
284 function getCMSFields() {
285 SiteTree::disableCMSFieldsExtensions();
286 $fields = parent::getCMSFields();
287 SiteTree::enableCMSFieldsExtensions();
288
289 $fields->addFieldToTab('Root.Content.Main', new ImageField('Photo', $this->fieldLabel('Photo')), 'Content');
290 $fields->addFieldToTab('Root.Content.Main', new TextareaField('Description', $this->fieldLabel('Description')), 'Content');
291
292
293
294 $product_class = static::$subpage_children;
295
296 if (count(self::get_view_options()) > 1 || $product_class::filter_fields_list()) {
297 $fields->addFieldToTab('Root.Content', new Tab('ViewSetup', _t('Catalog.tab_ViewSetup', 'View Setup')), 'Metadata');
298 if (count(self::get_view_options()) > 1)
299 $fields->addFieldToTab('Root.Content.ViewSetup', new DropdownField("StandartView", $this->fieldLabel('StandartView'), self::view_options_dropdown_map(true)));
300
301 if ($product_class::filter_fields_list()) {
302 $fields->addFieldToTab("Root.Content.ViewSetup", new CheckboxField('OwnFilter', $this->fieldLabel('OwnFilter')));
303 $fields->addFieldToTab("Root.Content.ViewSetup", new CheckboxSetField('FilterFields', $this->fieldLabel('FilterFields'), $product_class::filter_fields_list()));
304 }
305 }
306
307 Requirements::css('catalog/css/ProductAdmin.css');
308
309
310
311 $fields->addFieldToTab('Root.Content', new Tab('tabSubPages', _t('Catalog.tab_Products', 'Products')), 'Metadata');
312
313
314
315 $sp = new SubpageListField('Subpages', $this, $product_class);
316 $sp->Dragable = true;
317 $url = '<a href=\"admin/show/$ID\">$value</a>';
318 $sp->setFieldFormatting(array_combine(array_keys(singleton($product_class)->summaryFields()), array_fill(0, count(singleton($product_class)->summaryFields()), $url)));
319 $fields->addFieldToTab('Root.Content.tabSubPages', $sp);
320
321
322 323 324 325 326 327 328 329 330 331
332
333 $this->extend('updateCMSFields', $fields);
334
335 return $fields;
336 }
337
338 339 340 341 342
343 public function AllowChangeView() {
344 return self::$user_can_change_view && (count(self::$view_options) > 1);
345 }
346
347 348 349 350 351
352 public function Subcats() {
353 return DataObject::get("Catalog", "ParentID = {$this->ID} AND ShowInMenus=1", "Sort");
354 }
355
356 357 358 359 360
361 public function CountItems() {
362 $count = 0;
363 if ($items = DataObject::get("Product", "ParentID = {$this->ID}")) {
364 $count += $items->Count();
365 }
366 if ($virtualitems = DataObject::get("VirtualProduct", "ParentID = {$this->ID}")) {
367 $count += $virtualitems->Count();
368 }
369 return $count;
370 }
371
372 373 374 375 376
377 public function defaultSort() {
378 return ($this->SiteConfig->CatalogDefaultSort)? $this->SiteConfig->CatalogDefaultSort : 'title';
379 }
380
381 382 383 384 385
386 public function defaultView() {
387 if ($this->StandartView)
388 return $this->StandartView;
389
390 return ($this->SiteConfig->CatalogDefaultView) ? $this->SiteConfig->CatalogDefaultView : 'table';
391 }
392
393 394 395 396 397
398 public function catalogFilterFields() {
399 if ($this->OwnFilter) {
400 return explode(',', $this->FilterFields);
401 }
402 if ($this->ParentID && ($parent = $this->Parent()) && $parent->ID && is_a($parent, 'Catalog')) {
403 return $parent->catalogFilterFields();
404 }
405 return explode(',', $this->SiteConfig->CatalogDefaultFilter);
406 }
407
408 409 410 411 412 413 414 415 416
417 public function filteredProducts($order=null, $filters=null, $limit=null) {
418 $orderby = false;
419 if (!is_null($order)) {
420 $orderby = Product::sort_options_orderby($order);
421 }
422 if (!$orderby) {
423 $orderby = Product::sort_options_orderby($this->defaultSort());
424 }
425
426 $query = $this->getProductsListQuery($orderby, $limit);
427 if ($filters) {
428 $context = $this->productSearchContext();
429 $query = $context->getQuery($filters, $orderby, $limit, $query);
430 }
431 $this->extend('updateFilteredProductsQuery', $query, $filters);
432
433 $results = DataObject::buildDataObjectSet($query->execute());
434 if($results) $results->parseQueryLimit($query);
435 return $results;
436 }
437
438 function getProductsListQuery($orderby, $limit) {
439 $where = $this->getProductsListWhere();
440 $query = singleton(self::$subpage_children)->extendedSQL(implode(' AND ', $where), $orderby, $limit);
441 return $query;
442 }
443
444 445 446 447 448
449 function getProductsListWhere() {
450 $where = array();
451 $productClasses = array();
452 if($subclasses = ClassInfo::subclassesFor(self::$subpage_children)) {
453 foreach ($subclasses as $subclass)
454 $productClasses[] = $subclass;
455 } else {
456 $productClasses[] = self::$subpage_children;
457 }
458 $where[] = "(ClassName IN ('".implode("','", $productClasses)."'))";
459
460 $categoryIDs = array($this->ID);
461
462 if (Catalog::get_show_products_from_subcategories()) {
463 $categoryIDs = $this->getDescendantIDList();
464 $categoryIDs[] = $this->ID;
465 }
466 $where[] = "(ParentID IN (".implode(',', $categoryIDs) ."))";
467
468
469 if (self::$hide_allow_purchase_products) {
470 $where[] = "(AllowPurchase = 1)";
471 }
472 return $where;
473 }
474
475
476 function TotalProductsCount() {
477 $where = $this->getProductsListWhere();
478 $stage = (Versioned::current_stage() == 'Live' ? '_Live' : '');
479 $query = "SELECT SiteTree{$stage}.ID FROM SiteTree{$stage} JOIN Product{$stage} ON SiteTree{$stage}.ID = Product{$stage}.ID WHERE " . implode(' AND ', $where);
480 return DB::Query($query)->numRecords();
481 }
482
483 484 485 486 487
488 public function productSearchContext() {
489 $product = singleton(static::$subpage_children);
490 return new SearchContext(
491 static::$subpage_children,
492 $product->scaffoldSearchFields(array('restrictFields'=> $this->catalogFilterFields())),
493 $product->defaultSearchFilters()
494 );
495 }
496 }
497
498 class Catalog_Controller extends Page_Controller {
499
500 public $CurrentSort;
501 public $CurrentView;
502
503 function init() {
504 parent::init();
505
506 $this->CurrentSort = $this->defaultSort();
507 if ($sort = Session::get('Catalog.Sort')) {
508 $this->CurrentSort = $sort;
509 }
510
511 $this->CurrentView = $this->defaultView();
512 if ($this->AllowChangeView() && $view = Session::get('Catalog.View')) {
513 $this->CurrentView = $view;
514 }
515 }
516
517 function isEmptyContent() {
518 return parent::isEmptyContent() && $this->CountItems() == 0;
519 }
520
521 522 523 524 525
526 public function setView($value = '') {
527 if (!$value || !$this->AllowChangeView())
528 return;
529 if ($value == 'default') {
530 Session::set('Catalog.View', '');
531 $this->CurrentView = $this->defaultView();
532 }
533 elseif (in_array($value, Catalog::get_view_options())) {
534 Session::set('Catalog.View', $value);
535 $this->CurrentView = $value;
536 }
537 }
538
539 540 541 542 543
544 public function setSort($value = '') {
545 if (!$value) return;
546
547 if ($value == 'default') {
548 Session::set('Catalog.Sort', '');
549 $this->CurrentSort = $this->defaultSort();
550 }
551 elseif (array_key_exists($value, Product::get_sort_options())) {
552 Session::set('Catalog.Sort', $value);
553 $this->CurrentSort = $value;
554 }
555 }
556
557 558 559 560 561 562 563
564 function cleanParams($values = false) {
565 if (!$values)
566 $values = $this->getRequest()->requestVars();
567
568 unset($values['url']);
569 unset($values['start']);
570 unset($values['sort']);
571 unset($values['view']);
572 unset($values['action_filter']);
573 unset($values['action_filterclear']);
574 return $values;
575 }
576
577 578 579 580 581 582 583
584 function linkWithParams($params = array()) {
585 if (!$params)
586 $params = $this->cleanParams();
587 if (!$params)
588 return $this->Link();
589
590 $action = ($this->IsFilterActive()) ? 'filter' : '';
591 return $this->Link($action) . '?' . http_build_query($params);
592 }
593
594 595 596
597 function Sorts($default = false) {
598 $values = $this->cleanParams();
599
600 $items = new DataObjectSet();
601 foreach (Product::sort_options_dropdown_map($default) as $id => $title) {
602 if (!$id) $id = 'default';
603 $values['sort'] = $id;
604 $items->push(new ArrayData(array(
605 'ID' => $id,
606 'Title' => $title,
607 'Link' => $this->linkWithParams($values),
608 'isCurrent' => ($id == $this->CurrentSort),
609 'LinkOrCurrent' => ($id == $this->CurrentSort) ? 'current' : 'link',
610 )));
611 }
612 return $items;
613 }
614
615 616 617
618 function Views($default = false) {
619 if (!$this->AllowChangeView()) return false;
620
621 $values = $this->cleanParams();
622
623 $items = new DataObjectSet();
624 foreach (Catalog::view_options_dropdown_map($default) as $id => $title) {
625 if (!$id) $id = 'default';
626 $values['view'] = $id;
627 $items->push(new ArrayData(array(
628 'ID' => $id,
629 'Title' => $title,
630 'Link' => $this->linkWithParams($values),
631 'isCurrent' => ($id == $this->CurrentView),
632 'LinkOrCurrent' => ($id == $this->CurrentView) ? 'current' : 'link',
633 )));
634 }
635 return $items;
636 }
637
638 639 640 641 642
643 function ShowAll() {
644 return isset($_GET['all']);
645 }
646
647 648 649 650 651
652 function ShowAllLink() {
653 $values = $this->cleanParams();
654 if ($this->ShowAll())
655 unset($values['all']);
656 else
657 $values['all'] = 1;
658
659 return $this->linkWithParams($values);
660 }
661
662 663 664 665 666
667 function ShowAllTitle() {
668 return ($this->ShowAll()) ? _t('Catalog.ShowAll_on', 'show all') : _t('Catalog.ShowAll_off', 'show by page');
669 }
670
671 function getSeoCanonicalLink() {
672 if (!$this->data()->SeoIsAlternative) return false;
673
674 return $this->Link();
675 }
676
677 678 679 680 681
682 function Filters() {
683 if ($this->TotalProductsCount() == 0) {
684 return false;
685 }
686
687 $context = $this->productSearchContext();
688 $fields = $context->getSearchFields();
689 $this->extend("updateFilterFields", $fields);
690
691 $actions = new FieldSet(
692 new FormAction('filter', _t('Catalog.FILTER_SELECT','Выбрать')),
693 new FormAction('filterclear', _t('Catalog.FILTER_CLEAR','Очистить'))
694 );
695
696 $validator = new RequiredFields();
697 $validator->setJavascriptValidationHandler('none');
698 $form = new Form($this, 'Filters', $fields, $actions, $validator);
699 $form->disableSecurityToken();
700 $form->setFormMethod('GET');
701 $form->addExtraClass('filters');
702 $form->setTemplate('CatalogFilterForm');
703
704 $this->extend('updateFilterForm', $form);
705
706 if ($form->Fields()->Count() == 0)
707 return false;
708
709 if ($this->getRequest()) {
710 $form->loadDataFrom($this->getRequest()->requestVars());
711 }
712
713 return $form;
714 }
715
716 function index() {
717 $request = $this->getRequest();
718 $start = intval($request->getVar('start'));
719
720
721 if ($view = $request->requestVar('view')) {
722 $this->setView($view);
723 $this->data()->SeoIsAlternative = true;
724 }
725
726
727 $sort = $request->requestVar('sort');
728 if ($sort) {
729 $this->data()->SeoIsAlternative = true;
730 if ($sort != $this->CurrentSort) {
731 $this->setSort($sort);
732 $start = 0;
733 }
734 }
735
736
737 $limit = '';
738 if (!$this->ShowAll()) {
739 $count = ($this->SiteConfig->ProductPerPage) ? $this->SiteConfig->ProductPerPage : 20;
740 $limit = "$start,$count";
741 }
742 else {
743 $this->data()->SeoIsAlternative = true;
744 }
745
746
747 if ($this->IsFilterActive()) {
748 $this->data()->SeoIsAlternative = true;
749 }
750
751 $this->Products = $this->filteredProducts($this->CurrentSort, $this->cleanParams(), $limit);
752 if ($this->hasMethod('setSEOVars')) {
753 $this->setSEOVars($this->Products);
754 }
755
756 if (Director::is_ajax()) {
757 $this->getResponse()->addHeader('X-Set-Url', $this->linkWithParams());
758 }
759
760 $action = (Director::is_ajax()) ? 'ajax' : 'index';
761 return parent::defaultAction($action);
762 }
763
764 765 766 767 768 769 770 771
772 function filterclear($data) {
773
774 return $this->redirect($this->Link());
775 }
776
777 778 779
780 function filter($data, $form = null) {
781 if (Director::is_ajax()) {
782 $this->getResponse()->addHeader('X-Set-Url', $this->linkWithParams());
783 }
784
785 if ($form)
786 return $this->redirect($this->linkWithParams());
787
788 return $this->index();
789 }
790
791 function IsFilterActive() {
792 $data = $this->cleanParams();
793 if (!count($data)) return false;
794
795 $filters = $this->Filters();
796 if (!$filters) {
797 return false;
798 }
799 $filterFields = $filters->Fields();
800 foreach ($data as $k => $v) {
801 if ($filterFields->fieldByName($k)) {
802 if ($v !== '') return true;
803 }
804 }
805 return false;
806 }
807 }
808
[Raise a SilverStripe Framework issue/bug](https://github.com/silverstripe/silverstripe-framework/issues/new)
- [Raise a SilverStripe CMS issue/bug](https://github.com/silverstripe/silverstripe-cms/issues/new)
- Please use the
Silverstripe Forums to ask development related questions.
-