FAQ для тех, кто собирается в поход первый раз

- Posted in Uncategorized by

В походах, как и в любом деле, через некоторое время оказывается, что то, что знаешь и считаешь само собой разумеющимся, для других - иногда новость или откровение. Или "о, я об этом даже не подумал!". А объяснить, что к чему и почему, зачастую уже тоже не можешь, поскольку давно забыл проблемы и вопросы начального этапа и просто не способен их вспомнить. То есть можешь (ответы есть в голове), но некому задать правильные вопросы. И что желательно - вовремя. И вспоминаешь и отвечаешь на них уже в процессе. А потом снова забываешь.

И вот поэтому, когда в руки попадаются начинающие туристы со всем ворохом первых вопросов, надо спешить и записывать ответы. Потому что это оказывается полезным для всех.

Все советы тщательно выстраданы и бережно оттестированы на собственной персоне.

Travel Hiking Howto

Table of Contents


Еда

Крупы, сахар, чай, кофе перепаковываются в пластиковые бутылки с широким горлом (в которое проходит чайная ложка). Удобны бутылки из-под соков "Биола" на 0.5 л и меньше (меньше - это в основном для чая и кофе).

  • такие бутылки можно не бояться забыть оставить ночью в темноте возле костра или в тамбуре палатки: звери и насекомые не заберутся и под дождём не промокнет. Однако если ночью выпадет снег, вы будете ругаться матом на тех, кто оставил их возле костра. Поэтому лучше не оставляйте. У каждой вещи должно быть своё место и она должна находиться там, а не где попало. Чтобы вы могли наощупь и в темноте без проблем в своих вещах ориентироваться, а при сборе достаточно было взять рюкзак, а не бегать по всему лагерю;

  • альтернативный способ - канцелярская "прищепка". Главное открыть пачку аккуратно, чтобы можно было тщательно закрыть отверстие, несколько раз согнув упаковку и надёжно закрепив прищепкой. Начатую пачку можно нести в рюкзаке внутри котелка, чашки или внутри коробки с жёсткими стенками (пищевые контейнеры, походные закрытые котелки типа армейских). Круп и каш это не касается: способ в основном для небольших запасов чая и кофе;

  • сахар кубиками в походе не очень удобен: он всё равно крошится, объём занимает приличный, перепаковывать его сложно. Хотя удобен тем, что его легко равномерно отмерить и разделить между участниками.

Крупы и каши ОЧЕНЬ РЕКОМЕНДУЮ перед походом расфасовывать в индивидуальные порции. Покупаете на базаре упаковку из сотни небольших (10 x 15 см) кулечков с зип-локом, вооружаетесь весами поточнее (надо будет отмерять порции по 80-100 грамм), столовой ложкой, терпением и списком рациона... Дальше всё понятно. Потраченные усилия (небольшие на самом деле) окупаются удобствами в самых разных ситуациях: и при внезапной переорганизации группы, и в одиночных походах, спасают от неразберихи или необходимости иметь толкового завхоза (кто что несет, как равномерно разгружать участников, как быстро собрать комплект еды для приготовления), и от необходимости бороться с неудобными большими упаковками (они превращаются в удобные атомарные: взял, использовал, забыл). Ну и при аварийных ситуациях вы не останетесь вдруг без всех круп или вообще без еды. Или с очень-очень промокшей и негодной едой.

Уклоняться от употребления сахара и особенно соли в походах, мотивируя это "не ем" и "худею" я категорически не советую. Единственная веская причина уклоняться - если вас от этих продуктов порвет в клочья. Или превратитесь в зеленое бревно и вас надо будет нести. Соль интенсивно теряется организмом при физических нагрузках. Если потери не восполнять, довольно быстро возникнут проблемы с усвоением воды. И сопутствующие неприятности в виде плохого самочувствия, утомляемости, плохой реакции, голова не соображает и т.п. И ещё будете пить много и часто, но напиться и жажду утолить не сможете. Хотите с этим побаловаться - дома перед зеркалом пожалуйста. Где привычные нагрузки - это дойти от дома/офиса до маршрутки или магазина.

Сахар - это углеводы. Их ваш организма сжигает первыми при нагрузках, если не вдаваться в детали. Поэтому в походе можно и нужно бросаться на сладкое, если тянет. Слушайте свой организм, в общем. Он не дурак. А если дурак - слушайте более опытных туристов. Или спортсменов. Но не тех спортсменов, которые спортсмены, а тех, которые физкультурники. И выглядят здоровыми и бодрыми, а не круглыми и большими. Большие могут лишнего насоветовать.

Еда в первый и последний день похода. Обычно поезда в Крым идут ночью и приходят утром. Поэтому

  • в походную раскладку обычно не включается завтрак первого дня, а также ужин (а может и обед) последнего дня похода - в это время группа обычно находится в цивилизации и костры разводить для приготовления пищи просто негде. Поэтому рассчитывать надо на питание в цивилизации или сухими перекусами (орехи, изюм, печенье и т.п.);
  • завтракаем обычно либо своими запасами в поезде или в маршрутке/троллейбусе, либо на вокзале в городе: в кафе (макдональдс), киосках и т.п. можно купить поесть-попить по мелочам;
  • где-нибудь недалеко от цивилизации под конец похода может очень захотеться посидеть в кафе, поэтому желательно иметь запас денег и на такие случаи. Примерно по 50-100 грн за раз.

Посуда

Вилка в походе не нужна. Нужны КЛМН: кружка, ложка, миска, нож.

  • посуду желательно паковать недалеко от верха рюкзака, т.к. кружка и нож могут понадобиться в дороге и на перекусах;

  • то же самое касается туалетной бумаги. Она должна соседствовать с посудой;

  • некоторым кажется, что ножей можно иметь один-два на всю группу. Но нож применяется не только на стоянке и не только для еды. Он может понадобиться для того, чтобы отрезать лейкопластырь в пути, когда вся группа растянута и искать, у кого нож - нереально. И больно. И долго. В общем, нож желательно иметь каждому. Как и спички в непромокаемой упаковке и одноразовую зажигалку.

Зайдите на базар вместо спортивных и туристических магазинов: посуда из нержавейки весит как пух и очень удобна. И дешёвая. Тяжёлые термокружки -- штука довольно бесполезная. Поэтому на любителя. Лучше взять с собой тонкую и невесомую кружку из нержавейки. Её при желании можно поставить на огонь, чтобы вскипятить немного воды (чего не сделаешь термокружкой).

Термобельё и термоноски -- штука очень полезная, в отличие от термокружек.

Одежда

по просьбам трудящихся информирую: кто не уверен, что из одежды брать:

  1. всегда помните, что вам это нести на себе и не один день

  2. может быть холодно и погода вообще штука непредсказуемая

  3. минимум - штаны, шорты, футболка, свитер, носки, шапка+головной убор от солнца, перчатки, УДОБНАЯ и РАЗНОШЕННАЯ обувь, сменная обувь на время стоянок для отдыха ног. при желании и лёгком весе удваиваем или утраиваем всё это:)

Помните, что свитер удваивать не надо :)

По курткам, если сомневаетесь, что брать, совет такой: она должна быть полегче-потоньше (чаще всего вы её будете нести в рюкзаке), но защищать от ветра. Налетает ветер на плато -- надеваете на футболку куртку-ветровку и идёте в ней. Холодно - надеваете свитер и куртку. Дождь - надевается сверху дождевик (плащ из клеёнки - самый бюджетный и самый лёгкий и компактный вариант для Крыма).

Сменная обувь -- это не шлёпанцы. Стоянки -- это не асфальт и паркет, а опавшая листва, ветки и камни, а бегать на стоянке придется за хворостом, за водой и в туалет, и не всегда это рядом и по прямой, а по склону в тапочках ходить неудобно. В принципе можно, особенно если знаете, куда идёте. Всегда можно надеть и ходовые ботинки. Просто шансов просохнуть у них в этом случае будет меньше.

Шорты очень желательны -- на майские почти всегда очень тепло. Если возьмёте максимально простые и незатейливые спортивные шорты (без карманов, а-ля спортивные трусы), их можно легко комбинировать со штанами (утром понюхал небо, надел шорты под штаны, потеплело -- снял штаны, идёшь в шортах).

Самый оптимальный способ понять, что надевать - на стоянке перед выходом на маршрут вам должно быть немного прохладно, а не тепло. То есть, к примеру, выходите утром, а в шортах и футболке кажется прохладно - это замечательно. Наденьте сверху спорт. штаны и ветровку (куртку-штормовку), пока собираете лагерь и завтракаете, а перед выходом снимите и положите их в голове рюкзака. Начнёте идти - будет тепло. А на остановках набрасывайте куртку-штормовку из рюкзака, если спина мокрая и ветер.

todo: 50/10, 45/15, остановки - обязательно снимается рюкзак (проветривание спины и отдых плечам и спине)

Штаны должны быть лёгкие и защищающие от ветра. Это главное. Ещё лучше быстросохнущие. В идеале -- легкие спортивные. Я предпочитаю в походах современную синтетику: бабушкины сказки о пользе х/б материалов устарели лет так на 50 как минимум. Современная спортивная синтетика не раздражает, быстро сохнет и мало весит, ну и не мнётся в качестве бонуса. Поэтому в походе гораздо удобней.

Тёплые штаны во время ходьбы на маршруте в мае не понадобятся - вам будет жарко и некомфортно. В джинсах можно пойти в такой поход, но нежелательно. Когда они намокают, становятся очень тяжёлыми и очень долго сохнут. И в таком состоянии натрут вам всё, что можно. Попадёте под дождь или оступитесь, переходя брод - намучаетесь потом в них. И днём под солнцем и с рюкзаком будете идти -- поясница у вас джинсы промочит гарантированно.

Вечером и ночью в мае может быть прохладно, поэтому думайте сами с оглядкой на свои ощущения, что и как будете надевать на стоянках вечером и утром.

Температура на высоте ниже, чем на уровне моря! Не забывайте, что у моря - ровно, песок и жарко. На высоте - помимо естественного уменьшения температуры (примерно 0.5-0.7 градуса каждые 100 метров по вертикали) есть ещё ВЕТЕР на плато и перевалах, а также тенистые и сырые леса, где заметно прохладней, чем на берегу моря. То есть, что мы имеем? В Крыму высота гуляний по горам - примерно 1 км над уровнем моря. Если в мае на побережье 20-25 градусов днём и около 10 ночью, то на высоте Ай-Петри в это же время будет уже градусов 15 днём и в лучшем случае около 5-6 ночью.

Про ветер на плато и перевалах я не зря написал большими буквами. Там ситуация такая: идёте вы по склону горы в футболке, солнце печёт, жара, тишина, благодать. достигаете вершины того, куда шли - и оказываетесь натурально в таком ветре, что дышать мешает и чуть с ног не сбивает. И надо весьма проворно одеваться в флиску/свитер/штормовку, иначе слишком холодно, а то и искать шапку, если баффа не хватает. Бафф в таких условиях гораздо удобней на шее и лице (чтобы закрывал рот и нос, а может и уши с затылком).

Ночью ночуют нередко возле воды: ручья, озера. А там прохладнее. Даже если мощный родник недалеко - почва вокруг довольно влажная и прохлада чувствуется не только вблизи источника. Ну и если это лес, тем более высокий, куда днём лучи солнца не особенно достают, чтобы прогреть почву - ночевать в таких условиях вдруг оказывается не очень комфортно и холодно. Поэтому помните что наверху - примерно на 5-10 градусов холоднее. И если днём вы это не заметите, то ночью - вполне.

Аптечка

Минимум личных медикаментов:

  • 1-2 упаковки таблеток активированного угля;

  • несколько таблеток, которые вам от головной боли помогают (цитрамон / анальгин / пиркофен);

  • солнцезащитный крем (лучше один на 3-5 человек).

Желательно ещё:

  • наборчик бактерицидных лейкопластырей (в небольших плоских упаковках или коробочках продают, они там разной формы для разных участков тела);

  • эластичный бинт (нужен не всегда, но лучше по одному у каждого иметь).

На случай мозолей в дополнение к набору мелких пластырей можно взять один пластырь в виде ленты и маленькую индивидуальную упаковку ваты (они размером с пачку влажных салфеток). (Вот здесь вам и пригодится персональный ножик из комплекта КЛМН).

Может это и будет в общей аптечке, но лучше индивидуально каждому иметь небольшой комплект. А ещё очень полезно иметь хотя бы одни маленькие ножницы на группу.

<

p>

Гигиена

Берите много туалетной бумаги (у каждого должен быть полный рулон с собой): ей хорошо мыть посуду сразу после еды, может помочь при растопке костра, служит носовым платком и для вытирания рук. Ну и по назначению придётся применять иногда.

Не берите полотенце. Толку от него в походе - ноль. Промокнуть лицо после умывания можно и футболкой. Если кто-то рискнёт купаться -- под конец похода у вас будут грязные футболки, которыми вполне можно вытереться.

Для гигиены лучше взять упаковки влажных салфеток из расчёта как минимум пара салфеток в день: вечером лицо и ноги протереть (вам с этими ногами спать в палатке), ну и 1-2 салфетки в течение дня могут уйти на жирные руки и т.п.

<

p>

Туалет

В туалет желательно ходить парами :) Один страхует, чтобы ищущие хворост для костра не пошли в строну сидящего в засаде товарища. Если сидите в засаде и замечаете движущегося в вашем направлении человека - не стесняйтесь проорать волшебное слово "Занято!". Иначе стесняться придётся потом, когда вас заметят, это гораздо менее приятно. Те, кто не первый раз в походах, обычно соображают, что к чему, орут "Понял" и разворачиваются.

Для большой группы и на днёвках очень желательно по приходу сразу организовать ОДНО отхожее место, вырыв узкую достаточно глубокую ямку и закопав её перед уходом со стоянки. Это гораздо экологичней. К тому же кучки и бумажки не украшают лес и стоянки, если вы не в курсе. А судя по состоянию Крыма, многие не в курсе.

<

p>

Спальник

Насчёт спальников и холодных ночей. Не планируйте спать в них в одежде. В спальнике быстро согреваешься, а ночью обычно потеешь. Если ваша одежда -- обычная х/б ткань, вы проснётесь в 3-4 утра мокрыми и замёрзшими. Не верите - приготовьте часы и попробуйте.

Спать желательно раздетым (бельё, футболка), тогда спальник отводит излишнюю влагу наружу, оставляя тело сухим и в тепле. В холодные ночи -- класть куртку и свитер СВЕРХУ на спальник. Это гарантированно греет, в отличие от попыток начинающих надеть на себя всю одежду вечером и залезть в спальник.

Если вечером холодно -- можно залезть в спальник в одежде, чтобы согреться, но через время обязательно снимите её перед сном.

Отмазка: если видите опытных туристов, спящих в спальнике в одежде, не спешите с выводами (насчёт них или меня). Они скорей всего в термобелье, а спальник у них весит с пол-кило (ваш раз в 4-5 больше). То есть это совсем другие материалы и другие условия. Они сознательно идут на экономию веса и комбинирование функций вещей в походах. Термобельё - это вобще отдельная и обширная тема. В общем, не спешите с выводами. Никакой ошибки тут нет.

<

p>

Упаковка вещей

todo: что снизу, что сверху, куда тяжёлое, куда кухонную утварь, тапочки, сахар, чай, перекус, кружку

todo: карта, компас, часы, телефон, ключи, деньги, билеты

Кому нужны полиэтиленовые пакеты с зип-локом (многоразового использования, герметичные)

В объявленном на сходке магазине на Философской есть пакеты 30х40см. Продаются только упаковкой 100 шт - 45,50 грн. Думаю, одной упаковки на всех неэкипированных должно хватить. Могу купить, а при дележной встрече раздать. Думаю, пакетов по 10 на лицо будет достаточно.

Какие-то большие они: туда и ноутбук поместится. Мне интересны несколько штук (2-6 наверное) -- для карт, документов пригодятся.

А более мелких нет? Кстати, такие кульки с зипом продаются на Озёрке. Мы такие мелкие кулёчки покупали для индивидуальной расфасовки еды

Если поход возле воды, то во избежание сюрпризов лучше найти на Озёрке большие "мешки для сахара". Они полиэтиленовые, но габаритами с рюкзак примерно. Берётся 2 таких мешка, заполняются наполовину (чтобы оставшийся хвост надёжно завязать крепкой верёвкой, согнув его предварительно). В один - то, что понадобится только вечером на стоянке, в верхний -- то что может потребоваться днём при смене погоды. А в остальном всё пакуется как обычно, только аккуратно, чтобы мешок не порвать. Изнутри рюкзак получается защищён полиэтиленом. Вещи не намокнут ни под дождями, ни даже если вдруг свалится в реку ненадолго.

Но более мелкие мешки и кульки тоже нужны: и для разграничения вещей (для этого и обычные АТБ-шные и т.п. кульки подойдут), и для отделения грязной и мокрой одежды. Но для этого можно взять упаковку крепких мусорных пакетов: за 2-4 грн обычно получаешь 30 пакетов, если правильно помню.

В пакет зип-лок большой свитер не засунешь, но другая одежда (футболки, майки, штаны, рубашки и остальная мелочь) запросто помещается. Они удобны ещё тем что выдавив воздух из них и застегнув вещь занимает меньший объём.

Особой водонепроницаемости не требуется, лучше о весе не забывать. Самый универсальный способ - большие мусорные пакеты. Весят и стоят мало, в одной упаковке 30 мешков. Или 1-2 больших полиэтиленовых пакета размером с рюкзак. Главное - защитить от намокания спальник.

<

p>

Палатка

Палатки -- советую исходить из расчёта 50 см ширины на человека. Без проблем можно спать на спине, не касаясь соседей.

Палатку в дороге можно разделить между 2-3 людьми: один несёт тент (1-1.5 кг), другой внутреннюю палатку (~1.5 кг), третий колышки и дуги (400-600 грамм).

Палатку желательно нести внутри рюкзака. Снаружи она может быть повреждена колючками и ветками, а нести её в руках - неудобно. Хотя если вы ходите без трекинговых палок, можете носить и в руках.

В случае 10-15 человек удобней несколько вместительных палаток (если есть возможность) вместо кучи 2-местных: во-первых, на стоянках часто не так уж много ровного места под палатки, и поставить 3-4 больших палатки гораздо легче, чем 5-7 маленьких, а во-вторых, выбранные даты и места гарантируют наличие огромного количества людей на стоянках. Это интересно (если вы не мизантроп), но неудобно, если вам негде поставить палатки.

<

p>

Рюкзак

Если есть возможность взять большой рюкзак (например 100-литровый вместо 60-литрового) -- берите не раздумывая. Чем больше рюкзак, тем меньше проблем с его упаковкой. В идеале снаружи не должно ничего крепиться: ни карематы, ни палатки, ни тем более спальники. Если порвать каремат о кустарник или камни -- ещё не трагедия, то проколотая палатка или намокший или порванный спальник вас не порадуют. Соседей ваших тоже, поскольку им также придётся терпеть неудобства и как-то спасать вас от холода.

К тому же дождевые накидка на рюкзак часто не рассчитаны на что-то, висящее снаружи. Вообще-то от такой накидки можно вообще отказаться, если ВСЕ вещи внутри рюкзака лежат в больших крепких герметичных полиэтиленовых мешках. Мелочь в боковых кармашках и верхнем кармане желательно тоже.

<

p>

Клещи

Клещи. За 6-7 лет походов вроде бы видел на себе всего одного клеща. И то не уверен до сих пор, что это он был -- может неглубокая заноза (мы тогда вечно в заросли терновника попадали, были везде исцарапаны - может мелкая щепка неглубоко в кожу встряла). При том, что я хожу почти всё время в шортах и от травы на обочинах не шарахаюсь (вчера читал, что там их рассадники), впрочем как и от кустов. Хотя говорят, это индивидуально: одних клещи охотно едят, других нет.

Внимательного осмотра себя и задних частей тел соседей вполне достаточно. Вот пинцет в аптечку не забудьте. С ним гораздо проще ковыряться в случае обнаружения засады. Только вот живого клеща не вздумайте пинцетом вытаскивать. Живого очень аккуратно и постепенно, в несколько подходов, вытаскивают тонкой ниткой или волосом, сделав петлю.

Напоследок цитата: "Случаи клещевого вирусного энцефалита среди людей регистрируются почти ежегодно в Крыму и Волынской области. В 2009 году их было зарегистрировано 9." Когда оцените количество туристов, гуляющих на майские по Крыму... В общем, шансов подцепить гадость от клещей у вас невелика.

<

p>

Трекинговые палки

Трекинговые палки позволяют СУЩЕСТВЕННО разгрузить ваши ноги.

На их освоение и понимание придётся потратить несколько дней. Впрочем, оно того стоит. Легче и быстрее всего научиться работать и понимать палки при ходьбе в гору. Легче всего почувствовать от них пользу и разгрузку - на спусках. А получать от палок удовольствие и помощь при ходьбе по равнине получается примерно через неделю плотного с ними знакомства. Когда уже понял, зачем руки в походе. И насколько сильно они могут разгрузить ноги вместо того, чтоб болтаться при переходах без дела.

Но на равнине не стоит увлекаться палками: надо почаще их убирать и ходить как все, а не как паук. Не отвыкайте от привычного чувства равновесия под грузом, иначе грозит зависимость от палок и неприятные ощущения при их отсутствии.

Quickcheckout (1.5.1.3) адаптирован для темы Shoppica

- Posted in Uncategorized by

Исправлен внешний вид и проблема с Шоппикой: теперь не надо модифицировать common/header.tpl, перенося в него строчку с включением скрипта Fancybox из аналогичного файла темы Default (см. bugtracker, #2).

Единственная проблема, которая там осталась - при использовании галочки согласия покупателя с условиями я пока не могу вывести текст в окно PrettyPhoto, как сделано в стандартной форме оформления заказа. Если кто силён в jQuery - буду благодарен за подсказку. Этот яваскрипт мне уже весь мозг выел. PrettyPhoto ругается на неправильную картинку, хотя отличий от стандартной формы оформления заказа я при беглом осмотре не увидел. Придётся искать-читать про премудрости преттифото. А лучше отключите эту галочку и не мучайте своих покупателей. Всё равно в 2-недельный срок товар вернуть можно по закону, несмотря ни на какие галочки.

P.S. Архив ещё не рассылал. Чуть позже сделаю.

UPD 22 Dec. Со вчерашнего дня по семейным делам пришлось срочно бегать по аптекам и больницам, поэтому обновление задерживается.

UPD 26 Dec. Разослал наконец-то.

Как облегчить процесс публикации изменений на сервер

- Posted in Uncategorized by

[Git, FTP] Для FTP и shared hosting (без SSH доступа и полноценной консоли)

Рекомендую готовый скрипт git-extract:

По команде, в которой указывается диапазон коммитов, создаёт папку .deployment с готовым деревом и изменившимися файлами. И список удаленных файлов, которые придётся удалить вручную (если пользуетесь FTP клиентом) или через SSH на сервере (если есть такая возможность).

У скрипта есть маленькая особенность: запускать его надо в корне репозитория (там, где расположена скрытая папка `.git`). Не в подпапках из любого места репо: там он просто отработает вхолостую.

[Git, SSH] При полноценном хостинге (с SSH доступом)

Судя по всему, у меня сделано один в один как в http://habrahabr.ru/blogs/Git/127213/, поэтому не вижу смысла описывать то же самое. У меня немного отличается структура папок проектов, но это несущественно. Суть проста - на сервере лежит как Git-репозиторий, так и обычная рабочая копия (на которую смотрит веб-сервер). Точнее, на одну из папок репозитория - public_html. Потому что в репозитории хранится ещё документация, служебные скрипты, тестовые и чистые SQL дампы.

И при новых коммитах от разработчиков (git push) репозиторий по хуку делает автоматически две операции - обновление локальной серверной копии (git pull origin dev) и копирование набора файлов из config_sets (здесь у меня хранятся файлы, специфические для разных конфигураций: для одного разработчика, для другого, для dev2-windows, dev2-linux, для production1, production-dev и так далее, если надо ещё больше). Понятна идея? Требуемый набор конфигов просто перезаписывается поверх того, что есть в репозитории (а туда могут попасть и локальные конфиги девелоперов, если они не исключены через .gitignore), и получается чистая и настроенная конфигурация. Быстро, без чек-листов, ручных проверок-исправлений и условий-ветвлений с множеством девелоперских конфигов (зачем они на сервере?).

Естественно, на сервере настроено использование SSH-ключей, чтобы избавиться от необходимоси ввода пароля после git pull.

Есть, конечно, мелкие особенности - за неделю мы уже наступили на пару граблей и может это стоило бы описать.

См. также

[PHP, FTP] Web based FTP Sync Tool written in PHP. Есть возможность запрещать синхронизацию для отдельных файлов/папок.

[Windows, GUI] Сделать архив, содержащий только измененные файлы (сохранив при этом всю структуру папок) может также WinMerge: http://opencartforum.ru/topic/28606-решено-winmerge-как-сделать-изменения-в-движке-и-сохра/#entry223289

Не хватает сбора-подсчета спамерских IP в MaxSite CMS

- Posted in Webdev by

При модерируемых комменатриях от анонимов иногда возникают наплывы спам-ботов, которые мешают.

Для начала я выделил розовым фоном запрещенные комментарии. Это уже заметно облегчило разборки.

Но вот просто удалять спамерские комментарии мне не хочется. Шаблон у меня такой: если это разовое - то удалю и забуду, пусть тешатся, а если где-то бот поселился - хотелось бы подождать и убедиться, что это регулярное явление и забанить IP. Из-за одного спам-комментария банить весь IP как-то чересчур. Но сейчас приходится заниматься этим вручную.

Пригодился бы подсчёт количества удаленных спам-комменатриев с определенного IP. Чтобы я мог удалять их сразу (десяток в день удалить несложно и руками), но информация об их авторстве не пропадала впустую.

И через отметку галочек удалять или разрешать комментарии тоже очень не нравится: я обычно разрешаю 1-2 комментария. А для этого надо их отметить, промотать список до конца (спамеров гораздо больше успевает накопиться) и там не ошибиться с кнопкой (потому что пока проматываешь, иногда забывается - это я сейчас разрешить полезные хотел и их отметил? Или запретить/удалить бесполезное?).

Мне гораздо больше подошл бы вариант с парой ссылок возле комментария: разрешить, запретить, удалить.

Opencart 1.5.1: возможность регулировать точность и количество десятичных знаков при выводе веса и размеров

- Posted in Uncategorized by

Добавлена возможность настройки точности чисел при выводе веса и размеров (количество знаков после зяпятой). Регулируется парой цифр в языковых файлах - там же, где и десятичные разделители и разделители тысяч (точка, запятая, пробел, апостроф и т.п. - кому что нравится).

commit b8607fb31b2d9c414506c513b8fb4b8593cbc214
Author: Ruslan Brest <rb@labtodo.com>
Date:   Sun Dec 4 14:12:47 2011 +0200
    [+] Возможность настройки точности чисел при выводе веса и размеров (количество знаков после зяпятой)
diff --git a/public_html/admin/language/english/english.php b/public_html/admin/language/english/english.php
index 5a0223c..71bb46d 100644
--- a/public_html/admin/language/english/english.php
+++ b/public_html/admin/language/english/english.php
@@ -7,6 +7,8 @@ $_['date_format_long']        = 'l dS F Y';
 $_['time_format']             = 'h:i:s A';
 $_['decimal_point']           = '.';
 $_['thousand_point']          = ',';
+$_['length_decimals']         = 2;
+$_['weight_decimals']         = 2;
 
 // Text
 $_['text_yes']                = 'Yes';
diff --git a/public_html/admin/language/russian/russian.php b/public_html/admin/language/russian/russian.php
index d68a762..2cdbc95 100644
--- a/public_html/admin/language/russian/russian.php
+++ b/public_html/admin/language/russian/russian.php
@@ -7,6 +7,8 @@ $_['date_format_long']        = 'l, d F Y';
 $_['time_format']             = 'H:i:s';
 $_['decimal_point']           = '.';
 $_['thousand_point']          = ',';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_yes']                = 'Да';
diff --git a/public_html/catalog/language/english/english.php b/public_html/catalog/language/english/english.php
index cdf17cf..524e1d9 100644
--- a/public_html/catalog/language/english/english.php
+++ b/public_html/catalog/language/english/english.php
@@ -7,6 +7,8 @@ $_['date_format_long']      = 'l dS F Y';
 $_['time_format']           = 'h:i:s A';
 $_['decimal_point']         = '.';
 $_['thousand_point']        = ',';
+$_['length_decimals']       = 2;
+$_['weight_decimals']       = 2;
 
 // Text
 $_['text_home']             = 'Home';
diff --git a/public_html/catalog/language/russian/russian.php b/public_html/catalog/language/russian/russian.php
index 48829a5..f69f200 100644
--- a/public_html/catalog/language/russian/russian.php
+++ b/public_html/catalog/language/russian/russian.php
@@ -7,6 +7,8 @@ $_['date_format_long']      = 'l d F Y';
 $_['time_format']           = 'H:i:s';
 $_['decimal_point']         = '.';
 $_['thousand_point']        = '';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_home']             = 'Главная';
diff --git a/public_html/system/library/length.php b/public_html/system/library/length.php
index 03ca441..3d02025 100644
--- a/public_html/system/library/length.php
+++ b/public_html/system/library/length.php
@@ -5,6 +5,7 @@ final class Length {
    public function __construct($registry) {
        $this->db = $registry->get('db');
        $this->config = $registry->get('config');
+       $this->language = $registry->get('language');
 
        $length_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "length_class mc LEFT JOIN " . DB_PREFIX . "length_class_description mcd ON (mc.length_class_id = mcd.length_class_id) WHERE mcd.language_id = '" . (int)$this->config->get('config_language_id') . "'");
     
@@ -41,9 +42,9 @@ final class Length {
    public function format($value, $length_class_id, $decimal_point = '.', $thousand_point = ',') {
        if( 0 == $value ) return "";
        if (isset($this->lengths[$length_class_id])) {
-           return number_format($value, 2, $decimal_point, $thousand_point) . $this->lengths[$length_class_id]['unit'];
+           return number_format($value, $this->language->get('length_decimals'), $decimal_point, $thousand_point) . $this->lengths[$length_class_id]['unit'];
        } else {
-           return number_format($value, 2, $decimal_point, $thousand_point);
+           return number_format($value, $this->language->get('length_decimals'), $decimal_point, $thousand_point);
        }
    }
 }
diff --git a/public_html/system/library/weight.php b/public_html/system/library/weight.php
index d55fdf9..cda9d4d 100644
--- a/public_html/system/library/weight.php
+++ b/public_html/system/library/weight.php
@@ -5,6 +5,7 @@ final class Weight {
    public function __construct($registry) {
        $this->db = $registry->get('db');
        $this->config = $registry->get('config');
+       $this->language = $registry->get('language');
        
        $weight_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "weight_class wc LEFT JOIN " . DB_PREFIX . "weight_class_description wcd ON (wc.weight_class_id = wcd.weight_class_id) WHERE wcd.language_id = '" . (int)$this->config->get('config_language_id') . "'");
        
@@ -36,9 +37,9 @@ final class Weight {
    public function format($value, $weight_class_id, $decimal_point = '.', $thousand_point = ',') {
        if( 0 == $value ) return "";
        if (isset($this->weights[$weight_class_id])) {
-           return number_format($value, 2, $decimal_point, $thousand_point) . $this->weights[$weight_class_id]['unit'];
+           return number_format($value, $this->language->get('weight_decimals'), $decimal_point, $thousand_point) . $this->weights[$weight_class_id]['unit'];
        } else {
-           return number_format($value, 2, $decimal_point, $thousand_point);
+           return number_format($value, $this->language->get('weight_decimals'), $decimal_point, $thousand_point);
        }
    }
 }

Opencart 1.5.1.x: страница товара - на вкладку характеристик добавлен вес и размеры

- Posted in Opencart by
Добавлен показ веса и размеров на вкладке характеристик товара (Opencart 1.5)

По умолчанию они почему-то не выводятся в характеристиках товара и увидеть их можно только на странице сравнения.

Вывести подготовленные данные (вес и размеры товара) можно в другом месте шаблона (например вверху, рядом с моделью и бонусными баллами) -- думаю, не составит труда переставить вывод в более удобное место самостоятельно.

Изменения для обеих версий одинаковые. Просто на вкладке для 1.5.1.3 более полная версия: включает в себя изменение системных библиотек для более аккуратного вывода нулевых значений веса и размеров. В версии 1.5.1.2 я это изменение вносил давно и забыл про него написать.

commit a26a849f1ac31fd2ab24f491860748b64f588ef8
Author: Ruslan Brest 
Date:   Thu Nov 17 18:20:47 2011 +0200
    [+] catalog: информация о товаре - на вкладку характеристик добавлен вес и размеры
diff --git a/public_html/catalog/controller/product/product.php b/public_html/catalog/controller/product/product.php
index 25ba580..8062941 100644
--- a/public_html/catalog/controller/product/product.php
+++ b/public_html/catalog/controller/product/product.php
@@ -3,6 +3,7 @@ class ControllerProductProduct extends Controller {
    private $error = array();
 
    public function index() {
+       $this->language->load('product/compare');
        $this->language->load('product/product');
 
        $this->data['breadcrumbs'] = array();
@@ -154,6 +155,9 @@ class ControllerProductProduct extends Controller {
            $this->data['text_share'] = $this->language->get('text_share');
            $this->data['text_wait'] = $this->language->get('text_wait');
            $this->data['text_tags'] = $this->language->get('text_tags');
+           $this->data['text_weight_and_dim'] = $this->language->get('text_weight_and_dim');
+           $this->data['text_weight'] = $this->language->get('text_weight');
+           $this->data['text_dimension'] = $this->language->get('text_dimension');
 
            $this->data['entry_name'] = $this->language->get('entry_name');
            $this->data['entry_review'] = $this->language->get('entry_review');
@@ -178,10 +182,14 @@ class ControllerProductProduct extends Controller {
            $this->data['product_id'] = $this->request->get['product_id'];
            $this->data['manufacturer'] = $product_info['manufacturer'];
            $this->data['manufacturers'] = $this->url->link('product/manufacturer/product', 'manufacturer_id=' . $product_info['manufacturer_id']);
-           $this->data['model'] = $product_info['model'];
+           $this->data['model']  = $product_info['model'];
            $this->data['reward'] = $product_info['reward'];
            $this->data['points'] = $product_info['points'];
-
+           $this->data['weight'] = $this->weight->format($product_info['weight'], $product_info['weight_class_id']);
+           $this->data['length'] = $this->length->format($product_info['length'], $product_info['length_class_id']);
+           $this->data['width']  = $this->length->format($product_info['width'], $product_info['length_class_id']);
+           $this->data['height'] = $this->length->format($product_info['height'], $product_info['length_class_id']);
+           
            if ($product_info['quantity'] <= 0) {
                $this->data['stock'] = $product_info['stock_status'];
            } elseif ($this->config->get('config_stock_display')) {
@@ -309,7 +317,7 @@ class ControllerProductProduct extends Controller {
            $this->data['rating'] = (int)$product_info['rating'];
            $this->data['description'] = html_entity_decode($product_info['description'], ENT_QUOTES, 'UTF-8');
            $this->data['attribute_groups'] = $this->model_catalog_product->getProductAttributes($this->request->get['product_id']);
-
+           
            $this->data['products'] = array();
 
            $results = $this->model_catalog_product->getProductRelated($this->request->get['product_id']);
diff --git a/public_html/catalog/language/english/product/product.php b/public_html/catalog/language/english/product/product.php
index 16f42c5..1771dc0 100644
--- a/public_html/catalog/language/english/product/product.php
+++ b/public_html/catalog/language/english/product/product.php
@@ -25,6 +25,7 @@ $_['text_upload']       = 'Your file was successfully uploaded!';
 $_['text_wait']         = 'Please Wait!';
 $_['text_tags']         = 'Tags:';
 $_['text_error']        = 'Product not found!';
+$_['text_weight_and_dim'] = 'Weight and dimension';
 
 // Entry
 $_['entry_name']        = 'Your Name:';
diff --git a/public_html/catalog/language/russian/product/product.php b/public_html/catalog/language/russian/product/product.php
index 7542b96..fae7e99 100644
--- a/public_html/catalog/language/russian/product/product.php
+++ b/public_html/catalog/language/russian/product/product.php
@@ -25,6 +25,8 @@ $_['text_upload']       = 'Ваш файл был успешно загруже
 $_['text_wait']         = 'Пожалуйста, подождите!';
 $_['text_tags']         = 'Метки:';
 $_['text_error']        = 'Товар не найден!';
+$_['text_weight']       = 'Вес';
+$_['text_weight_and_dim'] = 'Вес и размеры';
 // Entry
 $_['entry_name']        = 'Ваше имя:';
 $_['entry_review']      = 'Ваш отзыв:';
diff --git a/public_html/catalog/view/theme/default/template/product/product.tpl b/public_html/catalog/view/theme/default/template/product/product.tpl
index aaeab64..8e8f8e8 100644
--- a/public_html/catalog/view/theme/default/template/product/product.tpl
+++ b/public_html/catalog/view/theme/default/template/product/product.tpl
@@ -221,6 +221,24 @@
   
   
+ + + + + + + + + + + + + + + + + +
commit 2dec5cba9d01d1fbef34e34bfaa5558bb7676c3e
Author: Ruslan Brest 
Date:   Wed Feb 15 10:17:52 2012 +0200
    [+] catalog: информация о товаре - на вкладку характеристик добавлен вес и размеры
diff --git a/upload/admin/language/english/english.php b/upload/admin/language/english/english.php
index 891da64..7f05d1c 100644
--- a/upload/admin/language/english/english.php
+++ b/upload/admin/language/english/english.php
@@ -7,6 +7,8 @@ $_['date_format_long']        = 'l dS F Y';
 $_['time_format']             = 'h:i:s A';
 $_['decimal_point']           = '.';
 $_['thousand_point']          = ',';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_yes']                = 'Yes';
diff --git a/upload/admin/language/russian/russian.php b/upload/admin/language/russian/russian.php
index 2860121..f3fcd1a 100644
--- a/upload/admin/language/russian/russian.php
+++ b/upload/admin/language/russian/russian.php
@@ -7,6 +7,8 @@ $_['date_format_long']        = 'l, d F Y';
 $_['time_format']             = 'H:i:s';
 $_['decimal_point']           = '.';
 $_['thousand_point']          = ',';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_yes']                = 'Да';
diff --git a/upload/catalog/controller/product/product.php b/upload/catalog/controller/product/product.php
index eadd2f3..c51d220 100644
--- a/upload/catalog/controller/product/product.php
+++ b/upload/catalog/controller/product/product.php
@@ -3,6 +3,7 @@ class ControllerProductProduct extends Controller {
    private $error = array(); 
    
    public function index() { 
+       $this->language->load('product/compare');
        $this->language->load('product/product');
    
        $this->data['breadcrumbs'] = array();
@@ -154,6 +155,9 @@ class ControllerProductProduct extends Controller {
            $this->data['text_share'] = $this->language->get('text_share');
            $this->data['text_wait'] = $this->language->get('text_wait');
            $this->data['text_tags'] = $this->language->get('text_tags');
+           $this->data['text_weight_and_dim'] = $this->language->get('text_weight_and_dim');
+           $this->data['text_weight'] = $this->language->get('text_weight');
+           $this->data['text_dimension'] = $this->language->get('text_dimension');
            
            $this->data['entry_name'] = $this->language->get('entry_name');
            $this->data['entry_review'] = $this->language->get('entry_review');
@@ -181,6 +185,11 @@ class ControllerProductProduct extends Controller {
            $this->data['model'] = $product_info['model'];
            $this->data['reward'] = $product_info['reward'];
            $this->data['points'] = $product_info['points'];
+
+           $this->data['weight'] = $this->weight->format($product_info['weight'], $product_info['weight_class_id']);
+           $this->data['length'] = $this->length->format($product_info['length'], $product_info['length_class_id']);
+           $this->data['width']  = $this->length->format($product_info['width'], $product_info['length_class_id']);
+           $this->data['height'] = $this->length->format($product_info['height'], $product_info['length_class_id']);
            
            if ($product_info['quantity'] <= 0) {
                $this->data['stock'] = $product_info['stock_status'];
diff --git a/upload/catalog/language/english/english.php b/upload/catalog/language/english/english.php
index cdf17cf..74b1bd6 100644
--- a/upload/catalog/language/english/english.php
+++ b/upload/catalog/language/english/english.php
@@ -7,6 +7,8 @@ $_['date_format_long']      = 'l dS F Y';
 $_['time_format']           = 'h:i:s A';
 $_['decimal_point']         = '.';
 $_['thousand_point']        = ',';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_home']             = 'Home';
diff --git a/upload/catalog/language/english/product/product.php b/upload/catalog/language/english/product/product.php
index 16f42c5..762f0b5 100644
--- a/upload/catalog/language/english/product/product.php
+++ b/upload/catalog/language/english/product/product.php
@@ -25,6 +25,8 @@ $_['text_upload']       = 'Your file was successfully uploaded!';
 $_['text_wait']         = 'Please Wait!';
 $_['text_tags']         = 'Tags:';
 $_['text_error']        = 'Product not found!';
+$_['text_weight']       = 'Weight';
+$_['text_weight_and_dim'] = 'Weight and dimension';
 
 // Entry
 $_['entry_name']        = 'Your Name:';
diff --git a/upload/catalog/language/russian/product/product.php b/upload/catalog/language/russian/product/product.php
index 7542b96..e44ea79 100644
--- a/upload/catalog/language/russian/product/product.php
+++ b/upload/catalog/language/russian/product/product.php
@@ -25,6 +25,9 @@ $_['text_upload']       = 'Ваш файл был успешно загруже
 $_['text_wait']         = 'Пожалуйста, подождите!';
 $_['text_tags']         = 'Метки:';
 $_['text_error']        = 'Товар не найден!';
+$_['text_weight']       = 'Вес';
+$_['text_weight_and_dim'] = 'Вес и размеры';
+
 // Entry
 $_['entry_name']        = 'Ваше имя:';
 $_['entry_review']      = 'Ваш отзыв:';
diff --git a/upload/catalog/language/russian/russian.php b/upload/catalog/language/russian/russian.php
index 48829a5..f69f200 100644
--- a/upload/catalog/language/russian/russian.php
+++ b/upload/catalog/language/russian/russian.php
@@ -7,6 +7,8 @@ $_['date_format_long']      = 'l d F Y';
 $_['time_format']           = 'H:i:s';
 $_['decimal_point']         = '.';
 $_['thousand_point']        = '';
+$_['length_decimals']       = 0;
+$_['weight_decimals']       = 0;
 
 // Text
 $_['text_home']             = 'Главная';
diff --git a/upload/catalog/view/theme/default/template/product/product.tpl b/upload/catalog/view/theme/default/template/product/product.tpl
index 81d6334..b7a3d15 100644
--- a/upload/catalog/view/theme/default/template/product/product.tpl
+++ b/upload/catalog/view/theme/default/template/product/product.tpl
@@ -27,6 +27,8 @@
          
         
          
+       
+
          
          
@@ -227,7 +229,7 @@
- + @@ -238,14 +240,26 @@
+
- - x x - + + + + + + + + + + + + + + + -
diff --git a/upload/system/library/length.php b/upload/system/library/length.php index 70f1b0f..f4d120a 100644 --- a/upload/system/library/length.php +++ b/upload/system/library/length.php @@ -5,6 +5,7 @@ final class Length { public function __construct($registry) { $this->db = $registry->get('db'); $this->config = $registry->get('config'); + $this->language = $registry->get('language'); $length_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "length_class mc LEFT JOIN " . DB_PREFIX . "length_class_description mcd ON (mc.length_class_id = mcd.length_class_id) WHERE mcd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); @@ -39,10 +40,11 @@ final class Length { } public function format($value, $length_class_id, $decimal_point = '.', $thousand_point = ',') { + if( 0 == $value ) return ""; if (isset($this->lengths[$length_class_id])) { - return number_format($value, 2, $decimal_point, $thousand_point) . $this->lengths[$length_class_id]['unit']; + return number_format($value, (int)$this->language->get('length_decimals'), $decimal_point, $thousand_point) . $this->lengths[$length_class_id]['unit']; } else { - return number_format($value, 2, $decimal_point, $thousand_point); + return number_format($value, (int)$this->language->get('length_decimals'), $decimal_point, $thousand_point); } } } diff --git a/upload/system/library/weight.php b/upload/system/library/weight.php index 584df3c..6b8c59f 100644 --- a/upload/system/library/weight.php +++ b/upload/system/library/weight.php @@ -5,6 +5,7 @@ final class Weight { public function __construct($registry) { $this->db = $registry->get('db'); $this->config = $registry->get('config'); + $this->language = $registry->get('language'); $weight_class_query = $this->db->query("SELECT * FROM " . DB_PREFIX . "weight_class wc LEFT JOIN " . DB_PREFIX . "weight_class_description wcd ON (wc.weight_class_id = wcd.weight_class_id) WHERE wcd.language_id = '" . (int)$this->config->get('config_language_id') . "'"); @@ -34,10 +35,11 @@ final class Weight { } public function format($value, $weight_class_id, $decimal_point = '.', $thousand_point = ',') { + if( 0 == $value ) return ""; if (isset($this->weights[$weight_class_id])) { - return number_format($value, 2, $decimal_point, $thousand_point) . $this->weights[$weight_class_id]['unit']; + return number_format($value, (int)$this->language->get('weight_decimals'), $decimal_point, $thousand_point) . $this->weights[$weight_class_id]['unit']; } else { - return number_format($value, 2, $decimal_point, $thousand_point); + return number_format($value, (int)$this->language->get('weight_decimals'), $decimal_point, $thousand_point); } }

Opencart 1.5.1 (catalog): показ SKU в списке товаров (product/category)

- Posted in Uncategorized by

Для 1.5.1.3. Отличается ли для 1.5.1.2 - не смотрел. Не должно, но номера строк придется искать самостоятельно.

Внизу есть ссылка на скачивание модидифицированных файлов на свежеустановленном Опенкарте.

commit 1e06f21174b1fb07414cbe0cf2eeee0627b75e2f
Author: Ruslan Brest <rb@labtodo.com>
Date:   Mon Oct 17 13:24:33 2011 +0300
    [+] catalog: Добавлен показ SKU в списке товаров (product/category)
diff --git a/upload/catalog/controller/product/category.php b/upload/catalog/controller/product/category.php
index 500e6bb..1be14db 100644
--- a/upload/catalog/controller/product/category.php
+++ b/upload/catalog/controller/product/category.php
@@ -188,6 +188,7 @@ class ControllerProductCategory extends Controller {
                    'product_id'  => $result['product_id'],
                    'thumb'       => $image,
                    'name'        => $result['name'],
+                   'sku'         => (empty($result['sku'])) ? '' : $this->language->get('text_sku') .' '. $result['sku'],
                    'description' => mb_substr(strip_tags(html_entity_decode($result['description'], ENT_QUOTES, 'UTF-8')), 0, 100, 'UTF-8') . '&hellip;',
                    'price'       => $price,
                    'special'     => $special,
diff --git a/upload/catalog/language/english/product/category.php b/upload/catalog/language/english/product/category.php
index def6f88..f3ddd08 100644
--- a/upload/catalog/language/english/product/category.php
+++ b/upload/catalog/language/english/product/category.php
@@ -7,6 +7,7 @@ $_['text_empty']        = 'There are no products to list in this category.';
 $_['text_quantity']     = 'Qty:';
 $_['text_manufacturer'] = 'Brand:';
 $_['text_model']        = 'Product Code:'; 
+$_['text_sku']          = 'SKU:';
 $_['text_points']       = 'Reward Points:'; 
 $_['text_price']        = 'Price:'; 
 $_['text_tax']          = 'Ex Tax:'; 
diff --git a/upload/catalog/language/russian/product/category.php b/upload/catalog/language/russian/product/category.php
index 762c91e..cfea2b9 100644
--- a/upload/catalog/language/russian/product/category.php
+++ b/upload/catalog/language/russian/product/category.php
@@ -7,6 +7,7 @@ $_['text_empty']        = 'В этой категории нет товаров.
 $_['text_quantity']     = 'Кол-во:';
 $_['text_manufacturer'] = 'Производитель:';
 $_['text_model']        = 'Модель:';
+$_['text_sku']          = 'SKU (Артикул):';
 $_['text_points']       = 'Бонусные баллы:';
 $_['text_price']        = 'Цена:';
 $_['text_tax']          = 'Без НДС:'; 
diff --git a/upload/catalog/view/theme/default/template/product/category.tpl b/upload/catalog/view/theme/default/template/product/category.tpl
index 37ff9c0..909edc4 100644
--- a/upload/catalog/view/theme/default/template/product/category.tpl
+++ b/upload/catalog/view/theme/default/template/product/category.tpl
@@ -72,7 +72,7 @@
       <?php if ($product['thumb']) { ?>
       <div class="image"><a href="<?php echo $product['href']; ?>"><img src="<?php echo $product['thumb']; ?>" title="<?php echo $product['name']; ?>" alt="<?php echo $product['name']; ?>" /></a></div>
       <?php } ?>
-      <div class="name"><a href="<?php echo $product['href']; ?>"><?php echo $product['name']; ?></a></div>
+      <div class="name"><a href="<?php echo $product['href']; ?>"><?php echo $product['name']; ?></a><?php echo $product['sku']; ?></div>
       <div class="description"><?php echo $product['description']; ?></div>
       <?php if ($product['price']) { ?>
       <div class="price">

Модифицированные файлы:

(Opencart 1.5.1.3) catalog: Добавлен показ SKU в списке товаров (product/category) 20

Применять с осторожностью: вы рискуете затереть свои изменения, внесенные ранее.

Завёл репозиторий для MaxSite CMS на GitHub-е

- Posted in Webdev by

https://github.com/rb2/MaxSite-CMS

Основная причина - неудобство предлагаемого способа обновления между релизами. Мне не хватает diff-ов и легкого способа их получения для чистых версий, чтобы иметь возможность накатить эти изменения вручную на модифицированный движок. Обновляться заменой файлов мне чрезвычайно неудобно.

Если кто-то хочет присоединиться к переводу на английский или правке украинского - теперь есть куда:

  • Исключительно языковые файлы живут и переводятся в соответствующих ветках репозитория https://github.com/g3d/MaxSite-TC (сейчас там украинский в ветке master и английская заготовка в ветке english).
  • Участвовать в корректировке переводов может любой человек, даже понятия не имеющий и не желающий разбираться с Git-ом: достаточно оставлять комментарии к строкам файлов при их просмотре. Тем, кто дружит с Git-ом, объяснять наверное ничего не надо.
  • Файлы скачивать можно там же: переключиться на соответствующую ветку ("Current branch" справа) и скачать ZIP.

В https://github.com/rb2/MaxSite-CMS живёт полностью CMS. Языки туда по мере готовности будут периодически вливаться.

Если автор MaxSite CMS переберётся на GitHub - это значительно упростит дело: можно будет предлагать/публиковать дополнения гораздо проще. Пока что я предполагаю держать этот репозиторий (по крайней мере основную ветку) только для "чистых" релизов версий CMS. И пускать разработчиков только с условием коммитить любые дополнения и модификации в соседние ветки, но не в master. Master branch сейчас - исключительно для оригиналов и получения списка отличий между ними.

Убираем ограничение 999.99 на вес и размеры в Opencart

- Posted in Uncategorized by
ALTER TABLE `opencart_product`
CHANGE `length` `length` DECIMAL(7,2) NOT NULL DEFAULT '0.00',
CHANGE `width` `width` DECIMAL(7,2) NOT NULL DEFAULT '0.00',
CHANGE `height` `height` DECIMAL(7,2) NOT NULL DEFAULT '0.00',
CHANGE `weight` `weight` DECIMAL(7,2) NOT NULL DEFAULT '0.00'

Префикс таблиц "opencart_" меняете на свой, если он используется.

Установка (1.5.1.x)

  1. Запишите это в текстовыый файл, например 999.sql.txt.
  2. Измените префикс таблиц на свой.
  3. Сделайте на всякий случай резервную копию базы данных (Система / Резервные копии / все таблицы должны быть отмечены / кнопка Бэкап). Вообще делайте их почаще, это полезно.
  4. Там же в поле "Востановить базу данных" выберите файл 999.sql.txt и нажмите кнопку "Восстановить".
  5. Теперь можете идти в редактирование товаров и проверять вес и размеры, например 15000 грамм. Если вам вдруг не хватит и этого количества цифр и позарез надо вводить вес в граммах, а не килограмах или тоннах - измените в этом коде цифры 7 на что-то большее (например 10) и повторите процедуру.

Что-то как ни взгляну на часы - то 11:11, то 03:14, то 09:11

- Posted in Uncategorized by

Иногда (гораздо реже) натыкаюсь на 12:12 или 13:13. И ещё 59. Причем не могу сказать, что на часы я часто смотрю - ну 2, ну 3 раза в день могу взгляд бросить. И весь этот год так. Раньше случалось, но воспринималось как: "Ух ты! Смотрите скорей, пока не исчезло!". А сейчас это настолько часто, что уже: "Ох, опять?".

Вот недавно: просыпаюсь - 9:00. Ну вот почему 00, а? Как это так точно? :) Я без будильника встаю. Думаю, ну вот, опять же будет 9:11, как пить дать. Умываюсь, зубы, кухня, то-сё, цветок полил, забыл уже про часы - возвращаюсь ноут включить, перекладываю автоматически телефон с постели на стол - опа, в руках 09:11 светится! Ну как по заказу. Вот почему я не через 5 минут вернулся, или через 20? Чай там заварить или ещё что подобное.

Что это, а?

И почему я не натыкаюсь на 18:28 например? Кусок e ничем не хуже 03:14 :) Вселенная, а другие константы мне показывать будут?

Извините за задержку

- Posted in Uncategorized by

Всем, кто сегодня не получил от меня файлы - у меня временные проблемы с доступом в интернет на основном компьютере. И решение что-то затянулось.

Сейчас постараюсь всем отправить. Простите за задержку.

Здесь ни аськи, ни скайпа нет, так что может недоступен буду сегодня. К email доступ есть.

UPD @ 21:41. Всё, я ожил в нормальном режиме. Отправил и ответил всем (если ничего не перепутал).

(Не?)Эффективность стандартной формы заказа Opencart 1.5.1

- Posted in Opencart by

Магазин молодой, так что цифры соответствующие. В люди вышел в сентябре (может ближе к концу - в 20 числах; точно не помню). Посещаемость небольшая, но мы над этим работаем и она понемногу стабильно растёт. Заказов было немного, и во многом это заслуга монстрообразной формы оформления заказа. А не только малой посещаемости.

Недавно я поставил укороченную форму оформления заказа (на выходных) и поэтому решил сохранить предыдущую историю как иллюстрацию и чтобы иметь возможность визуально убедиться во влиянии этого шага оформления покупки. На картинках - данные до её установки.

На нашей статистике видно, что из 72 человек, более-менее созревших что-то купить, сумели выдержать все испытания формы заказа всего 7. Очень наглядно. Из них наверное около половины - мы сами: в этот период точно были тестовые оформления заказа для проверки.

Funnel conversion rate: 7.37%. То есть почти 93% не выдерживают до конца и уходят без оформления покупки.

Некоторые уходят бесповоротно, часть возвращается к просмотру корзины (это скорей всего мы по большей части - владельцы и разработчики). Часть уходит на просмотр условий доставки. То есть их не хватает на этой старнице - и короткая форма заказа это отчасти решает.

Так что теперь остаётся подождать несколько месяцев (хотя бы 3) и узнать, насколько изменится уровень конверсии.

В понедельник (а короткая форма была установлена на выходных) мы получили сразу 4 заказа в один день. Посмотрев на статистику предыдущих 3 месяцев, вы наверное догадаетесь, что нас это приятно удивило. Это могло оказаться и совпадением, но тем не менее форма однозначно сыграла роль на своём этапе. Причём если до этого у нас были заказы на совсем небольшие суммы (около 200 грн / ~$25 USD), то в понедельник помимо роста их количества выросла существенно и сумма заказов (раз в 5).

Предварительные выводы за неделю работы короткой формы заказа

Тенденция видна уже даже за такой короткий промежуток (ещё не прошло и недели):

Что можно увидеть? Во-первых, заметно, что люди стали покупать быстрее: очень мало просматривают корзину и сомневаются - 90% перешли сразу к заказу/оплате и быстро его завершили. Обычно в заказе 1-2 позиции. То есть люди принимают решение и покупают из каталога.

Заметно и то, что эффективность "воронки продаж" выросла в 4 раза: было 7%, стало 29%. Время ещё уточнит и сгладит цифры. Пока такие :) Скоро у нас будет выкладка подготовленных зимних товаров, так что есть надежда на рост продаж сезонных категорий.

UPD 2011-11-26. На самом деле на картинке есть неточность: на шаге "shopping cart view" показано 23 посетителя на входе, 21 вышли, 2 прошли дальше. На самом деле 11 "вышло" на Quickcheckout - не успел отредактировать пути. Попросту забыл про них, только сейчас вспомнил об этом. И двое вернулись на тот же просмотр корзины. То есть на следующий шаг воронки прошло не 2 (8.70%), а 13 посетителей. И не на тот шаг, что на картинке, а на "параллельный" quickcheckout (который я забыл и его здесь нет). А на картинке - значит, один заказ в этот период я зацепил ещё с оформлением по старой форме. Плюс-минус день наверное промазал. В итоге более правильная картина: 23 => 13 => 9. И эффективность соответственно выше.

См. также:

Opencart 1.5.1: вывод информации о наличии под картинкой в каталоге товаров

- Posted in Uncategorized by

В ответ на

http://rb.labtodo.com/page/izmenjaem-dlinu-opisanij-tovarov-v-kataloge-opencart-1505#comment-281 Подскажите пожалуйста как сделать для opencart 1.5.1 чтоб в категории в описании товаров под картинкой отображалось наличие товара (есть в наличии или доступен под заказ...)

Для 1.5.1.2 и 1.5.1.3 код ниже. На 1.5.1.3 не проверял - если есть отличия и не работает, сообщите.

Небольшие изменения вносились раньше, поэтому могут быть небольшие отличия между тем, что вы видите у себя, и здесь (например, thumb_w, thumb_h, cut_descr) -- не обращайте на них внимания. Вставляемый код неизменный как для 1.5.1.2, так и для 1.5.1.3, номера строк совпадают - главное найти место для вставки, а их легко идентифицировать по остальными имеющимся признакам.

diff --git a/public_html/catalog/controller/product/category.php b/public_html/catalog/controller/product/category.php
index d3ba191..3613e7c 100644
--- a/public_html/catalog/controller/product/category.php
+++ b/public_html/catalog/controller/product/category.php
@@ -1,6 +1,7 @@
 <?php
 class ControllerProductCategory extends Controller {
    public function index() {
+       $this->language->load('product/product');
        $this->language->load('product/category');
 
        $this->load->model('catalog/category');
@@ -182,6 +183,15 @@ class ControllerProductCategory extends Controller {
                    $rating = false;
                }
 
+               $stock = $this->language->get('text_stock') . ' ';
+               if ($result['quantity'] <= 0) {
+                   $stock .= $result['stock_status'];
+               } elseif ($this->config->get('config_stock_display')) {
+                   $stock .= $result['quantity'];
+               } else {
+                   $stock .= $this->language->get('text_instock');
+               }
+
                $cut_descr_symbols = 400;
                $descr_plaintext = strip_tags(html_entity_decode($result['description'], ENT_QUOTES, 'UTF-8'));
                if( mb_strlen($descr_plaintext, 'UTF-8') > $cut_descr_symbols )
@@ -195,6 +205,7 @@ class ControllerProductCategory extends Controller {
                    'thumb'       => $image,
                    'thumb_w'     => $this->config->get('config_image_product_width'),
                    'thumb_h'     => $this->config->get('config_image_product_height'),
+                   'stock'       => $stock,
                    'name'        => $result['name'],
                    'description' => $descr_plaintext,
                    'price'       => $price,
diff --git a/public_html/catalog/view/theme/default/template/product/category.tpl b/public_html/catalog/view/theme/default/template/product/category.tpl
index 6e9054a..19680ad 100644
--- a/public_html/catalog/view/theme/default/template/product/category.tpl
+++ b/public_html/catalog/view/theme/default/template/product/category.tpl
@@ -70,7 +70,9 @@
     <?php foreach ($products as $product) { ?>
     <div>
       <?php if ($product['thumb']) { ?>
-      <div class="image"><?php if( $product['special'] ) { ?><div class="akcii-img"></div><?php } ?><a href="<?php echo $product['href']; ?>"><img src="<?php echo $product['thumb']; ?>" width="<?php echo $product['thumb_w']; ?>" height="<?php echo $product['thumb_h']; ?>" title="<?php echo $product['name']; ?>" alt="<?php echo $product['name']; ?>" /></a></div>
+      <div class="image"><?php if( $product['special'] ) { ?><div class="akcii-img"></div><?php } ?><a href="<?php echo $product['href']; ?>"><img src="<?php echo $product['thumb']; ?>" width="<?php echo $product['thumb_w']; ?>" height="<?php echo $product['thumb_h']; ?>" title="<?php echo $product['name']; ?>" alt="<?php echo $product['name']; ?>" /></a>
+      <?php echo $product['stock'];?>
+      </div>
       <?php } ?>
       <div class="name"><a href="<?php echo $product['href']; ?>"><?php echo $product['name']; ?></a></div>
       <div class="description"><?php echo $product['description']; ?></div>

Продолжение обсуждение этой и охожих тем -- на форуме: http://rb.labtodo.com/forum/topic/4/vyvod-informatsii-o-nalichii-v-kataloge-tovarov/

Что такое diff

- Posted in Uncategorized by

При публикации изменений используется формат DIFF. С его помощью можно перенести небольшие изменения к себе вручную (см. ниже). Или автоматически: командами git apply rb-patch.diff или patch -p2 < rb-patch.diff (находясь в корневой папке магазина - там, где расположены каталоги admin и catalog).

Предварительно, конечно же, скопировав и сохранив в файл опубликованный патч и отрезав первые несколько строк на русском с пояснениями про diff.

Перенос изменений вручную

См. http://rb.labtodo.com/page/opencart-admin-attributes-usability-improvement#comment-138

Это список изменений. В начале строк - индикаторы: что убрать (минусы, красный цвет) в старом файле и что добавить (плюсики, зелёный цвет).

В каких именно файлах - указано утроенным значком (---, +++)

@@ -106,10 +106,12 @@ -- строки, т.е. примерно на 106 строке вы должны найти похожий кусок кода, перед и после блока изменений обычно выводится пара строк текста. "Примерно 106" - потому что я множество правок вношу в свой магазин и содержимое файлов может не совпадать с оригинальным OpenCart 1.5.1.1 или с тем правками, которые внесены у вас до меня.

Правки индицируются значками "-" и "+" в самом начале строки: те, что с минусом, были убраны (они должны совпадать с тем, что вы у себя видите перед внесением изменений), а те, что с плюсом -- добавлены в это место. Поэтому можно руками внести и проконтролировать все изменения.

В данном случае надо закомментировать небольшую часть в файле admin/controller/catalog/attribute.php и модифицировать одну строку в admin/model/catalog/attribute.php (заменить её на несколько новых, которые можно скопировать и убрать плюсики в начале строк).

См. также

  • Улучшение сортировки на витрине Опенкарт - ещё одна статья с примером описания "обычным языком" и diff-файлом. До сих пор считаете длинное описание более понятным, чем diff? Не перестаю удивляться таким заявлениям.

Opencart 1.5.1.x: Упрощённое оформление заказа на одной странице (Quickcheckout one-page simple checkout)

- Posted in Opencart by

Сделал вот такую укороченную форму заказа (скриншоты ниже). Упрощённое гостевое оформление заказа на одной странице.

Преимущества (они же и недостатки):

  • Предельно компактно и быстро, не утомляет покупателя. По статистике на каждом лишнем шаге при оформлении заказа сходят с дистанции 20% покупателей;
  • Всё на виду и получилось весьма удобно и наглядно;
  • Предполагается оплата исключительно методом "Cash on delivery" (оплата наличными по факту доставки). Всё остальное решается по телефону и комментариями;
  • В админке нет никаких настроек (это касаемо предыдущего пункта; используется только cash-on-delivery способ, и он указан в коде и должен быть разрешен).
  • Нет никакого учёта всего, что связано с адресом и гео-информацией (включая налоги, так как они зависят от адреса)

Модуль совместим только с версиями 1.5.1.*. Под более новые адаптировать не планирую, т.к. альтернатив уже существует большое количество и можно взять готовое решение. Создавать ещё одно решение будет также стоить денег, причины постарался описать в отдельной статье (см. также в конце, в списке связанных статей). Есть версия под Opencart 1.4.9.x (ocStore 0.2.2 / 0.2.0). Ниже описаны недостатки и возможности.

Купить

Оплата вручную: Webmoney кошельки: Z385421512525, R304747241741 U415109965360 E180553785592
Оплата карточкой или Paypal:
Version
Email for sending .ZIP-file:
  1. Удобнее всего купить модуль быстрого оформления заказа на Opencart.com. Там архив сразу доступен для скачивания и не надо ждать, пока я его отправлю. Также проще будет забирать обновления в любой удобный момент - вы будете получать уведомления от сервиса по email;
  2. Вручную за 10WMZ, или по курсу в WMR или WMU. Или 10$ через Paypal (там же принимаются банковские карты). В комментариях к платежу укажите свой email, на который надо выслать ZIP архив с модулем.

Вручную я высылаю архив сразу после того, как узнаю о платеже. Обычно это занимает не более суток. Чаще гораздо меньше, но случаи разные бывают и иногда я могу быть недоступен. Webmoney не присылает мне никаких уведомлений на email (единственный из сервисов, кстати), поэтому о платеже там лучше написать дополнительно на email или через форму обратной связи в блоге. Иначе узнать о платеже там я могу лишь по чистой случайности.

Поэтому рекомендую довериться автоматике на Opencart.com. Там немного дороже, но покупка удобнее для последующих обновлений. И может оказаться быстрее, чем с моим участием.

Если покупаете на Опенкарт.ком, посмотрите на пару способов доставки с текстом и ссылками вместо цены — они удобны для курьерских служб.

Возможности и ограничения

<

p>Версия Quickcheckout для Opencart 1.5.1 поставляется в 2 версиях:

  • более старая (QC) не имеет выбора способов оплаты (покупателю надо выбрать только доставку), но может быть гораздо удобней тем магазинам, которые торгуют локально по городу и доставляют товары по стране, используя единственный способ оплаты - из рук в руки;
    • Этот вариант не заменяет никакие файлы Opencart;
  • QCPM (с поддержкой способов оплаты, payment methods) существует для версии Опенкарт 1.5.1.3.
    • этот вариант заменяет часть стандартных файлов - более подробная информация в README в соответствующей папке. Присылайте неизвестные мне модули или содержимое этой папки (tpl файлы) - я их модифицирую для QC/PM и включу tpl в архив

Admin: Дополнения / Доставка:

Будут использоваться только те модули, в которых установлена географическая зона ВСЕ РЕГИОНЫ. Это связано с тем, что из модуля полностью убрано всё, что связано с географической информацией.

Внимание: модуль "Доставка в зависимости от веса" не будет работать -- у него отсутствуют настройки для всех регионов. Если вы используете вес товаров - включите вместо него способ "Доставка по городу Citylink". Если вес не нужен - попробуйте "Фиксированную стоимость доставки" или другой подходящий способ.

Если пользователь зарегистрирован и предварительно залогинился, его данные подставятся в форму (имя, email, телефон) и заказ будет связан с этим покупателем. Планируется добавить возможность быстрой регистрации, но сроки неизвестны.

Поддерживаются Shoppica и Spicylicious темы оформления - для них в архиве есть отдельные папки. По вопросам поддержки других специфических тем оформления, на которых форма заказа выглядит плохо - обращайтесь.

Демо: http://rb.labtodo.com/shop/test-demo

В версии Quickcheckout для Opencart 1.4.9.x (ocStore 0.2.2 / 0.2.0) есть и оплата, и доставка, но не проверялась работа с зарегистрированными пользователями - было сделано только гостевое оформление. Эта версия заменяет стандартные файлы опенкарт, поэтому если используется другая тема оформления -будьте аккуратны при установке и сделайте предварительно резервные копии ваших файлов.

Демо: http://ocs020.labtodo.com/

Вебмани мне не присылает уведомления на email, поэтому лучше сообщить дополнительно (через форму обратной связи например), если архив нужен срочно. Если я на связи, то сразу же и отправлю. Остальные сервисы уведомляют. Или можно подождать - я в течение дня обычно высылаю, просматриваю почту регулярно. Автоматически на полном автопилоте можно купить на сайте opencart.com, но там чуть дороже. Зато можно в любой момент скачать архив (и апдейты), не дожидаясь ответа от меня.

Если нет электронных кошельков и желания платить карточкой - обычно можно без проблем пополнить WMR или WMU кошелёк наличными через платёжные терминалы.

2011-11-27. Исправлена ошибка в версии для Opencart 1.5.1.3. Всем, кто покупал на opencart.com, должно было прийти уведомление об обновлении. Кто покупал вручную, через Webmoney/Paypal - напишите, вышлю обновление для 1.5.1.3.

2011-11-30. Появилась тестовая версия для ocStore 0.2.0 / 0.2.2 (Opencart 1.4.9.?). Возможно, будет ещё дорабатываться. Но шансов мало - я вообще не планировал версиею для 1.4. Пока не проверена покупка зарегистрированным пользователем. И форма работает в 2 шага. Если будет время - попробую улучшить. Пока так, как на демо: http://ocs020.labtodo.com/

Позже собираюсь сделать такую же одностраничную форму со всеми опциями. Наверное все эти способы тоже кому-то, да нужны (нужны, да? Или нет?).

Ошибки или проблемы с установкой?

Если возникают проблемы с установкой или какие-то ошибки, которые не можете устранить после прочтения инструкции по установке - мне нужен как минимум адрес вашего сайта, где я могу посмотреть на работу формы и увидеть ошибки. Укажите адрес сайта и email, с которого покупали модуль, на странице Контакт.

<

p>Мелкие модификации также входят в список того, что я помогаю сделать при первичной настройке:

  • сделать другие поля обязательными или необязательными;
  • убрать или переименовать какие-то поля (например, поменять ФИО на что-то другое, заменить продолжение адреса на ближайшую станцию метро, удобный склад перевозчика или индекс).

Значительно ускорить процесс может наличие в письме параметров доступа к FTP и в админ-часть Опенкарт. Это позволит гораздо быстрее обнаружить и устранить ошибки в конфигурации (модулей оплаты и доставки, например) или неправильной установке, убедиться в том, что ничего не напутано с версиями, увидеть лог ошибок сразу же по мере их воспроизведения и после изменений. Объяснять в переписке, что именно попробовать, исправить или проверить - в большинстве случаев гораздо дольше и сложнее. Поэтому по возможности сразу их указывайте, если понимаете, что сами этим заниматься не будете.

Opencart 1.5.x (catalog): SEO - автоматическое заполнение meta keywords

- Posted in Opencart by

Если поле "meta keywords" не заполнено в админке, Опенкарт его не выводит. Для улучшения ситуации заполним его автоматически, продублировав слова из названия товара.

В результате у нас будет что-то вроде:


Аппарат для приготовления блинов Bomann CM 2221 CB

Не идеал, но вроде бы лучше, чем ничего. Или ручное прописывание у всех товаров, хи-хи. В то же время вручную описать эти поля никто не мешает, и в случае их заполненности автоматика не вмешивается. Делать автозаполнение для "meta description" не решился, потому что одинаковое название и описание не приветствуется. Но кому хочется - могут дописать if( empty($this->data['description']) ) $this->data['description'] = $this->data['title'];

Для версий Opencart 1.5.x, включая 1.5.1.3; ocStore 1.0.1.

diff --git a/public_html/catalog/controller/common/header.php b/public_html/catalog/controller/common/header.php
index 79bf735..1904774 100644
--- a/public_html/catalog/controller/common/header.php
+++ b/public_html/catalog/controller/common/header.php
@@ -18,6 +18,15 @@ class ControllerCommonHeader extends Controller {
                $this->data['direction'] = $this->language->get('direction');
                $this->data['google_analytics'] = html_entity_decode($this->config->get('config_google_analytics'), ENT_QUOTES, 'UTF-8');
 
+               // SEO improvements:
+               // If keywords and description not set - use title here too
+               if( empty($this->data['keywords']) )
+               {
+                       $words = explode(' ', $this->data['title']);
+                       if(count($words) > 1) $this->data['keywords'] = implode(',', $words) . ',';
+                       $this->data['keywords'] .= $this->data['title'];
+               }
+
                $this->language->load('common/header');
 
                if (isset($this->request->server['HTTPS']) && (($this->request->server['HTTPS'] == 'on') || ($this->request->server['HTTPS']

Загрузка jQuery по требованию

- Posted in Webdev by

Оригинал: Load jQuery on demand | Web-developer notepad

Для загрузки jQuery по требованию вы можете пользоваться скриптом loadJquery.js.

Быстрый старт

Загрузите скрипт по этой ссылке, отредактируйте его нижнюю часть после слов PUT YOUR CODE HERE, вот эту:

(function () {
...
    /* PUT YOUR CODE HERE  */
    loadJquery({
        url: 'http://code.jquery.com/jquery-latest.min.js',
        timeout: 5000, //ms
        success: function ($) {
            $('h1').css('border', '2px solid red');
        },
        error: function () {
            alert("Can't load jQuery.");
        }
    });
}());

Преимущества скрипта:

  • кроссбраузерность
  • основан на коде jquery 1.7.2
  • отлов исключений при помощи try/catch
  • возможность указания таймаута загрузки
  • загрузка не будет производиться, если jQuery уже загружен.
  • простота использования для конечного пользователя: просто пишите свой код в success/error-колбеки
  • отсутствие конфликтов с другими js-библиотеками типа prototype.js
  • скрипт проходит jslint-валидацию

Теперь больше информации для тех, кто хочет узнать подробности.

Скрипт состоит из двух частей: loadScript и loadJquery.

loadScript

Функция практически полностью сделана на основе кода jQuery 1.7.2, актуальной в данный момент. Загрузка скрипта производится посредством добавления тега script в head. Производится контроль исключений при помощи try/catch, теоретически не должно быть никаких ошибок яваскрипта. Предусмотрена возможность указания тайм-аута, по достижению которого будет вызван error-callback.

Пример использования.

loadScript({
    url: 'http://example.com/script.js', //script url
    timeout: 5000, //optional timeout in ms
    success: function () {
        alert('Script loaded callback.');
    },
    error: function () {
        alert('Script was not loaded.');
    }
});

loadJquery

Особенности функции loadJquery следующие:

  • загрузка jQuery не будет производиться, если jQuery уже загружен
  • используется .noConflict(), т.е. можно не беспокоиться о возникновении конфликтов с другой библиотекой типа prototype.js

Полный листинг скрипта

/*!
 *
 * loadJquery.js
 * Load jQuery on demand from javascript
 * Author: contact@slicezilla.com
 * November 2011
 *
 * The script uses part of jQuery JavaScript Library v1.7
 * http://jquery.com/
 * Copyright 2011, John Resig
 * Dual licensed under the MIT or GPL Version 2 licenses.
 * http://jquery.org/license
 *
*/
/*global document, setTimeout, clearTimeout, alert, jQuery*/
(function () {
    "use strict";
    var loadScript = function (options) {
        var script, transportTimeout,
            head = document.head || document.getElementsByTagName('head')[0] || document.documentElement,
            transport = {
                send: function (url, successCallback) {
                    script = document.createElement('script');
                    script.async = 'async';
                    script.src = url;
                    // Attach handlers for all browsers
                    script.onload = script.onreadystatechange = function (event, isAbort) {
                        if (isAbort || !script.readyState || /loaded|complete/.test(script.readyState)) {
                            // Handle memory leak in IE
                            script.onload = script.onreadystatechange = null;
                            // Remove the script
                            if (head && script.parentNode) {
                                head.removeChild(script);
                            }
                            // Dereference the script
                            script = undefined;
                            //callback if not abort
                            if (!isAbort && typeof successCallback === 'function') {
                                clearTimeout(transportTimeout);
                                successCallback();
                            }
                        }
                    };
                    // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
                    // This arises when a base node is used (#2709 and #4378).
                    head.insertBefore(script, head.firstChild);
                    //head.appendChild(script);
                },
                abort: function (errorCallback) {
                    clearTimeout(transportTimeout);
                    if (script) {
                        script.onload(0, 1);
                    }
                    if (typeof errorCallback === 'function') {
                        errorCallback();
                    }
                }
            };
        //init timeout
        if (options.timeout > 0) {
            transportTimeout = setTimeout(function () {
                transport.abort(options.error);
            }, options.timeout);
        }
        //get script
        try {
            transport.send(options.url, options.success);
        } catch (e) {
            transport.abort(options.error);
        }
    };
    //function which will load jQuery using loadScript
    function loadJquery(options) {
        if (typeof jQuery !== 'undefined') {
            //jQuery is loaded already, just perform business logic
            if (typeof options.success === 'function') {
                //pass jQuery as parameter, in order to have $ === jQuery in callback
                //just in case that $.noConflict was used already, and $ !== jQuery
                jQuery(options.success);
            }
        } else {
            //jQuery was not loaded on a page
            loadScript({
                url: options.url,
                timeout: options.timeout,
                success: function () {
                    jQuery.noConflict();
                    if (typeof options.success === 'function') {
                        jQuery(options.success);
                    }
                },
                error: options.error
            });
        }
    }
    /* -------- PUT YOUR CODE HERE -------- */
    loadJquery({
        url: 'http://code.jquery.com/jquery-latest.min.js',
        timeout: 5000, //ms
        success: function ($) {
            $('h1').css('border', '2px solid red');
        },
        error: function () {
            alert("Can't load jQuery.");
        }
    });
}());

Обсудить

Оказывается, Ubuntu можно обновить до следующего релиза через CD

- Posted in Uncategorized by

Не знал, для меня это оказалось приятным сюрпризом. Апгрейд до следующего релиза через сеть происходит очень долго - несколько утомительных часов, насколько помню. С возможными вопросами и проблемами посреди процесса. И это слабо зависит от скорости сети. Я обновлялся ближе к вечеру и пришлось на ночь ноутбук оставить докачивать и устанавливать, запретив впадать в спячку.

Через CD тоже процесс небыстрый, но гораздо меньше времени занимает. Так что советую попробовать ISO образ скачать, записать на болванку, и с неё уже обновляться.

Задумались, как пускать в админку сторонних людей (редакторов)

- Posted in Uncategorized by

Появилась потребность и желание воспользоваться для наполнения каталога услугами людей. Которые не всегда могут быть хорошо технически подкованы - то есть вероятность ошибок, будем считать, велика. И рисковать уже имеющимся контентом не хочется, даже если человек -- полный зайка и ничего такого не хотел. Допустим, не так понял или считал, что будет лучше по-другому.

В Opencart возможность ограничения доступа базовые: можно разрешить/запретить просмотр и редактирование разных сущностей группе пользователей. Но если человеку доступно редактирование товаров, то он может удалить или внести изменения в том числе и старые. И это ограничить нельзя.

Пока додумались до варианта продублировать админки и базы, чтобы редакторам были доступны только их товары. И периодически эти товары будут переноситься в основную базу, а дополнительная -- очищаться.

Проблемы видны, но пока кажутся вполне нормально решаемыми:

  • в новой базе надо продублировать информацию о категориях, производителях, атрибутах и опциях из старой (с сохранением тех же ID);
  • мы и в основной базе убрали возможность вносить новые атрибуты и опции - они под жёстким контролем и впустую не заводятся. На новой базе аналогично. Но простым редакторам помимо прав на изменение товаров и файл-менеджер даны ещё права на изменение категорий (чтобы могли добавлять новые и оформлять их описанием и картинкой);
  • перенос категорий - ну что ж, придётся сравнивать дампы этой таблицы до и после. И новые вставлять в основную базу с теми же идентификаторами категорий. А изменённые - исправлять вручную;
  • дальше перенос товаров (таблиц product и product_description). Пока кажется, что всё должно пройти без приключений (контроля и конвертирования идентификаторов).

Надеюсь, что я не очень просчитался в прогнозах :)

Интересно было бы также узнать, есть ли что-то у Даниэля в планах по этому поводу. Потому что если придуманный перенос кроме потенциальной опасности ошибиться будет ещё и неудобен, придётся что-то делать с дополнительными ограничениями и оставаться в пределах одной базы.

P.S. Платные модули:

Opencart 1.5.x - 1.5.1.2 (admin): редактирование заказов

- Posted in Opencart by
Не рекомендуется применять! В 1.5.1.1 / 1.5.1.2 вроде бы работает, но отсутствия проблем никто не обещал.

Добавляем команду редактирования заказа в админке. Изменения для 1.5.1.2 и 1.5.1.3 идентичны. Но в 1.5.1.3 форма редактирования переделывается и на данный момент не работает полностью. То есть удалить товар может и получится, а вот добавить - вряд ли. Поэтому пользователям OpenCart 1.5.1.3 лучше подождать обновлений или следующей версии.

Впрочем, всем остальным -- тоже.

На свой страх и риск. Авторы OpenCart не закончили модификацию редактирования заказов и поэтому функция спрятана в версии 1.5

diff --git a/public_html/admin/controller/common/home.php b/public_html/admin/controller/common/home.php
index 8d96fac..daa59b9 100644
--- a/public_html/admin/controller/common/home.php
+++ b/public_html/admin/controller/common/home.php
@@ -174,10 +174,14 @@ class ControllerCommonHome extends Controller {
             
            $action[] = array(
                'text' => $this->language->get('text_view'),
                'href' => $this->url->link('sale/order/info', 'token=' . $this->session->data['token'] . '&order_id=' . $result['order_id'], 'SSL')
            );
+           $action[] = array(
+               'text' => $this->language->get('text_edit'),
+               'href' => $this->url->link('sale/order/update', 'token=' . $this->session->data['token'] . '&order_id=' . $result['order_id'], 'SSL')
+           );
                    
            $this->data['orders'][] = array(
                'order_id'   => $result['order_id'],
                'customer'   => $result['customer'],
                'status'     => $result['status'],
diff --git a/public_html/admin/view/stylesheet/stylesheet.css b/public_html/admin/view/stylesheet/stylesheet.css
index cde1569..556b225 100644
--- a/public_html/admin/view/stylesheet/stylesheet.css
+++ b/public_html/admin/view/stylesheet/stylesheet.css
@@ -31,10 +31,13 @@ a, a:visited {
    cursor: pointer;
 }
 a img {
    border: 0;
 }
+a.action {
+   padding-left: 5px;
+}
 form {
    margin: 0;
    padding: 0;
 }
 label {
diff --git a/public_html/admin/view/template/common/home.tpl b/public_html/admin/view/template/common/home.tpl
index dd0d996..83a57f7 100644
--- a/public_html/admin/view/template/common/home.tpl
+++ b/public_html/admin/view/template/common/home.tpl
@@ -103,11 +103,11 @@
                 
                 
                 
                 
                 
-                  [  ]
+                  
                   
               
               
               
               

Не пользуйтесь этим для версии 1.5.1.3! Авторы OpenCart не закончили её модификацию и поэтому она спрятана в версии 1.5

diff --git a/upload/admin/controller/common/home.php b/upload/admin/controller/common/home.php
index b3eb55a..e41a285 100644
--- a/upload/admin/controller/common/home.php
+++ b/upload/admin/controller/common/home.php
@@ -174,10 +174,14 @@ class ControllerCommonHome extends Controller {
             
            $action[] = array(
                'text' => $this->language->get('text_view'),
                'href' => $this->url->link('sale/order/info', 'token=' . $this->session->data['token'] . '&order_id=' . $result['order_id'], 'SSL')
            );
+           $action[] = array(
+               'text' => $this->language->get('text_edit'),
+               'href' => $this->url->link('sale/order/update', 'token=' . $this->session->data['token'] . '&order_id=' . $result['order_id'], 'SSL')
+           );
                    
            $this->data['orders'][] = array(
                'order_id'   => $result['order_id'],
                'customer'   => $result['customer'],
                'status'     => $result['status'],
diff --git a/upload/admin/view/stylesheet/stylesheet.css b/upload/admin/view/stylesheet/stylesheet.css
index d80516b..bb9e451 100644
--- a/upload/admin/view/stylesheet/stylesheet.css
+++ b/upload/admin/view/stylesheet/stylesheet.css
@@ -29,10 +29,13 @@ a, a:visited {
    cursor: pointer;
 }
 a img {
    border: 0;
 }
+a.action {
+   padding-left: 5px;
+}
 form {
    margin: 0;
    padding: 0;
 }
 label {
diff --git a/upload/admin/view/template/common/home.tpl b/upload/admin/view/template/common/home.tpl
index dd0d996..83a57f7 100644
--- a/upload/admin/view/template/common/home.tpl
+++ b/upload/admin/view/template/common/home.tpl
@@ -103,11 +103,11 @@
                 
                 
                 
                 
                 
-                  [  ]
+                  
                   
               
               
               
               

Opencart 1.5.1.3 и ниже (admin): колонка SKU (поиск, фильтр, автодополнение, сортировка)

- Posted in Uncategorized by
Opencart 1.5.1.3 (admin): products SKU column Opencart 1.5.1.3 (admin): products SKU column

Для OpenCart 1.5.1.3 в админ-части в список товаров добавлена колонка SKU (по-русски -- артикул или складской номер). В отличие от логики авторов поиск возможен по любому вхождению искомых символов - они могут встречаться в любом месте строчки SKU, а не только с её начала. Не знаю, почему у них везде так, мне гораздо удобнее без этого ограничения.

На свежую установку Opencart 1.5.1.3 можно залить поверх изменённые файлы (которые вы найдёте в папке admin внутри архива): oc1513-admin-sku-column.zip 25. Но если вы уже вносили свои изменения в эти файлы, надо вручную внести изменения (см. diff), чтобы не затереть предыдущие исправления.

commit 144af0b3466be74fb55f913686f96290b2c231a5
Author: Ruslan Brest <rb@labtodo.com>
Date:   Mon Oct 10 13:49:45 2011 +0300
    [+] admin: products SKU column (search, filter, autocomplete, sort)
diff --git a/upload/admin/controller/catalog/product.php b/upload/admin/controller/catalog/product.php
index 655b09d..72382b6 100644
--- a/upload/admin/controller/catalog/product.php
+++ b/upload/admin/controller/catalog/product.php
@@ -234,10 +234,16 @@ class ControllerCatalogProduct extends Controller {
        if (isset($this->request->get['filter_model'])) {
            $filter_model = $this->request->get['filter_model'];
        } else {
            $filter_model = null;
        }
+
+       if (isset($this->request->get['filter_sku'])) {
+           $filter_sku = $this->request->get['filter_sku'];
+       } else {
+           $filter_sku = null;
+       }
        
        if (isset($this->request->get['filter_price'])) {
            $filter_price = $this->request->get['filter_price'];
        } else {
            $filter_price = null;
@@ -280,10 +286,13 @@ class ControllerCatalogProduct extends Controller {
        }
        
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
        
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
        
@@ -328,10 +337,11 @@ class ControllerCatalogProduct extends Controller {
        $this->data['products'] = array();
 
        $data = array(
            'filter_name'     => $filter_name, 
            'filter_model'    => $filter_model,
+           'filter_sku'      => $filter_sku,
            'filter_price'    => $filter_price,
            'filter_quantity' => $filter_quantity,
            'filter_status'   => $filter_status,
            'sort'            => $sort,
            'order'           => $order,
@@ -373,10 +383,11 @@ class ControllerCatalogProduct extends Controller {
    
            $this->data['products'][] = array(
                'product_id' => $result['product_id'],
                'name'       => $result['name'],
                'model'      => $result['model'],
+               'sku'        => $result['sku'],
                'price'      => $result['price'],
                'special'    => $special,
                'image'      => $image,
                'quantity'   => $result['quantity'],
                'status'     => ($result['status'] ? $this->language->get('text_enabled') : $this->language->get('text_disabled')),
@@ -391,12 +402,13 @@ class ControllerCatalogProduct extends Controller {
        $this->data['text_disabled'] = $this->language->get('text_disabled');      
        $this->data['text_no_results'] = $this->language->get('text_no_results');      
        $this->data['text_image_manager'] = $this->language->get('text_image_manager');        
            
        $this->data['column_image'] = $this->language->get('column_image');        
-       $this->data['column_name'] = $this->language->get('column_name');      
+       $this->data['column_name']  = $this->language->get('column_name');     
        $this->data['column_model'] = $this->language->get('column_model');        
+       $this->data['column_sku']   = $this->language->get('entry_sku');       
        $this->data['column_price'] = $this->language->get('column_price');        
        $this->data['column_quantity'] = $this->language->get('column_quantity');      
        $this->data['column_status'] = $this->language->get('column_status');      
        $this->data['column_action'] = $this->language->get('column_action');      
                
@@ -428,10 +440,13 @@ class ControllerCatalogProduct extends Controller {
        }
        
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
        
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
        
@@ -453,10 +468,11 @@ class ControllerCatalogProduct extends Controller {
            $url .= '&page=' . $this->request->get['page'];
        }
                    
        $this->data['sort_name'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=pd.name' . $url, 'SSL');
        $this->data['sort_model'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.model' . $url, 'SSL');
+       $this->data['sort_sku']   = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.sku' . $url, 'SSL');
        $this->data['sort_price'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.price' . $url, 'SSL');
        $this->data['sort_quantity'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.quantity' . $url, 'SSL');
        $this->data['sort_status'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.status' . $url, 'SSL');
        $this->data['sort_order'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.sort_order' . $url, 'SSL');
        
@@ -467,10 +483,13 @@ class ControllerCatalogProduct extends Controller {
        }
        
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
        
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
        
@@ -499,10 +518,11 @@ class ControllerCatalogProduct extends Controller {
            
        $this->data['pagination'] = $pagination->render();
    
        $this->data['filter_name'] = $filter_name;
        $this->data['filter_model'] = $filter_model;
+       $this->data['filter_sku']   = $filter_sku;
        $this->data['filter_price'] = $filter_price;
        $this->data['filter_quantity'] = $filter_quantity;
        $this->data['filter_status'] = $filter_status;
        
        $this->data['sort'] = $sort;
@@ -649,10 +669,13 @@ class ControllerCatalogProduct extends Controller {
        }
        
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
        
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
        
@@ -1204,11 +1227,11 @@ class ControllerCatalogProduct extends Controller {
    }
        
    public function autocomplete() {
        $json = array();
        
-       if (isset($this->request->get['filter_name']) || isset($this->request->get['filter_model']) || isset($this->request->get['filter_category_id'])) {
+       if (isset($this->request->get['filter_name']) || isset($this->request->get['filter_model']) || isset($this->request->get['filter_sku']) || isset($this->request->get['filter_category_id'])) {
            $this->load->model('catalog/product');
            
            if (isset($this->request->get['filter_name'])) {
                $filter_name = $this->request->get['filter_name'];
            } else {
@@ -1218,11 +1241,17 @@ class ControllerCatalogProduct extends Controller {
            if (isset($this->request->get['filter_model'])) {
                $filter_model = $this->request->get['filter_model'];
            } else {
                $filter_model = '';
            }
-                       
+
+           if (isset($this->request->get['filter_sku'])) {
+               $filter_sku = $this->request->get['filter_sku'];
+           } else {
+               $filter_sku = '';
+           }
+
            if (isset($this->request->get['filter_category_id'])) {
                $filter_category_id = $this->request->get['filter_category_id'];
            } else {
                $filter_category_id = '';
            }
@@ -1240,10 +1269,11 @@ class ControllerCatalogProduct extends Controller {
            }           
                        
            $data = array(
                'filter_name'         => $filter_name,
                'filter_model'        => $filter_model,
+               'filter_sku'          => $filter_sku,
                'filter_category_id'  => $filter_category_id,
                'filter_sub_category' => $filter_sub_category,
                'start'               => 0,
                'limit'               => $limit
            );
@@ -1291,10 +1321,11 @@ class ControllerCatalogProduct extends Controller {
                
                $json[] = array(
                    'product_id' => $result['product_id'],
                    'name'       => html_entity_decode($result['name'], ENT_QUOTES, 'UTF-8'),    
                    'model'      => $result['model'],
+                   'sku'        => $result['sku'],
                    'option'     => $option_data,
                    'price'      => $result['price']
                );  
            }
        }
diff --git a/upload/admin/model/catalog/product.php b/upload/admin/model/catalog/product.php
index b75d072..fab30d9 100644
--- a/upload/admin/model/catalog/product.php
+++ b/upload/admin/model/catalog/product.php
@@ -350,10 +350,13 @@ class ModelCatalogProduct extends Model {
            }
 
            if (!empty($data['filter_model'])) {
                $sql .= " AND LCASE(p.model) LIKE '" . $this->db->escape(utf8_strtolower($data['filter_model'])) . "%'";
            }
+           if (!empty($data['filter_sku'])) {
+               $sql .= " AND LCASE(p.sku) LIKE '%" . $this->db->escape(utf8_strtolower($data['filter_sku'])) . "%'";
+           }
            
            if (!empty($data['filter_price'])) {
                $sql .= " AND p.price LIKE '" . $this->db->escape($data['filter_price']) . "%'";
            }
            
@@ -388,10 +391,11 @@ class ModelCatalogProduct extends Model {
            $sql .= " GROUP BY p.product_id";
                        
            $sort_data = array(
                'pd.name',
                'p.model',
+               'p.sku',
                'p.price',
                'p.quantity',
                'p.status',
                'p.sort_order'
            );  
@@ -659,10 +663,14 @@ class ModelCatalogProduct extends Model {
 
        if (!empty($data['filter_model'])) {
            $sql .= " AND LCASE(p.model) LIKE '" . $this->db->escape(utf8_strtolower($data['filter_model'])) . "%'";
        }
        
+       if (!empty($data['filter_sku'])) {
+           $sql .= " AND LCASE(p.sku) LIKE '%" . $this->db->escape(utf8_strtolower($data['filter_sku'])) . "%'";
+       }
+       
        if (!empty($data['filter_price'])) {
            $sql .= " AND p.price LIKE '" . $this->db->escape($data['filter_price']) . "%'";
        }
        
        if (isset($data['filter_quantity']) && !is_null($data['filter_quantity'])) {
diff --git a/upload/admin/view/template/catalog/product_list.tpl b/upload/admin/view/template/catalog/product_list.tpl
index 25b90b3..c183b7d 100644
--- a/upload/admin/view/template/catalog/product_list.tpl
+++ b/upload/admin/view/template/catalog/product_list.tpl
@@ -31,10 +31,15 @@
               <td class="left"><?php if ($sort == 'p.model') { ?>
                 <a href="<?php echo $sort_model; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_model; ?></a>
                 <?php } else { ?>
                 <a href="<?php echo $sort_model; ?>"><?php echo $column_model; ?></a>
                 <?php } ?></td>
+              <td class="left"><?php if ($sort == 'p.sku') { ?>
+                <a href="<?php echo $sort_sku; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_sku; ?></a>
+                <?php } else { ?>
+                <a href="<?php echo $sort_sku; ?>"><?php echo $column_sku; ?></a>
+                <?php } ?></td>
               <td class="left"><?php if ($sort == 'p.price') { ?>
                 <a href="<?php echo $sort_price; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_price; ?></a>
                 <?php } else { ?>
                 <a href="<?php echo $sort_price; ?>"><?php echo $column_price; ?></a>
                 <?php } ?></td>
@@ -55,10 +60,11 @@
             <tr class="filter">
               <td></td>
               <td></td>
               <td><input type="text" name="filter_name" value="<?php echo $filter_name; ?>" /></td>
               <td><input type="text" name="filter_model" value="<?php echo $filter_model; ?>" /></td>
+              <td><input type="text" name="filter_sku" value="<?php echo $filter_sku; ?>" /></td>
               <td align="left"><input type="text" name="filter_price" value="<?php echo $filter_price; ?>" size="8"/></td>
               <td align="right"><input type="text" name="filter_quantity" value="<?php echo $filter_quantity; ?>" style="text-align: right;" /></td>
               <td><select name="filter_status">
                   <option value="*"></option>
                   <?php if ($filter_status) { ?>
@@ -83,10 +89,11 @@
                 <input type="checkbox" name="selected[]" value="<?php echo $product['product_id']; ?>" />
                 <?php } ?></td>
               <td class="center"><img src="<?php echo $product['image']; ?>" alt="<?php echo $product['name']; ?>" style="padding: 1px; border: 1px solid #DDDDDD;" /></td>
               <td class="left"><?php echo $product['name']; ?></td>
               <td class="left"><?php echo $product['model']; ?></td>
+              <td class="left"><?php echo $product['sku']; ?></td>
               <td class="left"><?php if ($product['special']) { ?>
                 <span style="text-decoration: line-through;"><?php echo $product['price']; ?></span>
                 <span style="color: #b00;"><?php echo $product['special']; ?></span>
                 <?php } else { ?>
                 <?php echo $product['price']; ?>
@@ -130,10 +137,16 @@ function filter() {
    
    if (filter_model) {
        url += '&filter_model=' + encodeURIComponent(filter_model);
    }
    
+   var filter_sku = $('input[name=\'filter_sku\']').attr('value');
+   
+   if (filter_sku) {
+       url += '&filter_sku=' + encodeURIComponent(filter_sku);
+   }
+   
    var filter_price = $('input[name=\'filter_price\']').attr('value');
    
    if (filter_price) {
        url += '&filter_price=' + encodeURIComponent(filter_price);
    }
@@ -204,7 +217,30 @@ $('input[name=\'filter_model\']').autocomplete({
        $('input[name=\'filter_model\']').val(ui.item.label);
                        
        return false;
    }
 });
+
+$('input[name=\'filter_sku\']').autocomplete({
+   delay: 0,
+   source: function(request, response) {
+       $.ajax({
+           url: 'index.php?route=catalog/product/autocomplete&token=<?php echo $token; ?>&filter_sku=' +  encodeURIComponent(request.term),
+           dataType: 'json',
+           success: function(json) {       
+               response($.map(json, function(item) {
+                   return {
+                       label: item.sku,
+                       value: item.product_id
+                   }
+               }));
+           }
+       });
+   }, 
+   select: function(event, ui) {
+       $('input[name=\'filter_sku\']').val(ui.item.label);
+                       
+       return false;
+   }
+});
 //--></script> 
 <?php echo $footer; ?>
\ No newline at end of file

И ещё найдите в .tpl файле строку

<td class="center" colspan="8"><?php echo $text_no_results; ?></td>

и увеличьте цифру на единичку (вместо 8 напишите 9). Забыл это, в diff-е этого нет. В архиве обновил.

Изменения для версий 1.5.1.2-1.5.0.5 (и наверное ниже в ветке 1.5.x)

В этих файлах у меня были и другие правки, поэтому готовые файлы не выкладываю.

commit e55686bfea6bf91b65c235d68c24ac7840139a89
Author: Ruslan Brest <rb@labtodo.com>
Date:   Mon Oct 10 21:47:01 2011 +0300
    [+] v1.5.1.2: admin: SKU column in product list
diff --git a/public_html/admin/controller/catalog/product.php b/public_html/admin/controller/catalog/product.php
index e407995..283e41b 100644
--- a/public_html/admin/controller/catalog/product.php
+++ b/public_html/admin/controller/catalog/product.php
@@ -253,10 +253,16 @@ class ControllerCatalogProduct extends Controller {
            $filter_model = $this->request->get['filter_model'];
        } else {
            $filter_model = null;
        }
 
+       if (isset($this->request->get['filter_sku'])) {
+           $filter_sku = $this->request->get['filter_sku'];
+       } else {
+           $filter_sku = null;
+       }
+
        if (isset($this->request->get['filter_price'])) {
            $filter_price = $this->request->get['filter_price'];
        } else {
            $filter_price = null;
        }
@@ -307,10 +313,14 @@ class ControllerCatalogProduct extends Controller {
 
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
 
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
+
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
 
        //filter category start//
@@ -360,10 +370,11 @@ class ControllerCatalogProduct extends Controller {
        $this->data['products'] = array();
 
        $data = array(
            'filter_name'     => $filter_name,
            'filter_model'    => $filter_model,
+           'filter_sku'      => $filter_sku,
            'filter_price'    => $filter_price,
            //filter category start//
            'filter_category' => $filter_category,
            // End
            'filter_quantity' => $filter_quantity,
@@ -417,10 +428,11 @@ class ControllerCatalogProduct extends Controller {
 
            $this->data['products'][] = array(
                'product_id' => $result['product_id'],
                'name'       => $result['name'],
                'model'      => $result['model'],
+               'sku'        => $result['sku'],
                'price'      => $this->currency->format($result['price']),
                //filter category start//
                'category'   => $category,
                        //filter category end//
                'special'    => $special['price'],
@@ -445,10 +457,11 @@ class ControllerCatalogProduct extends Controller {
        $this->data['column_name'] = $this->language->get('column_name');
        // Add
        $this->data['column_category'] = $this->language->get('column_category');
        // End add
        $this->data['column_model'] = $this->language->get('column_model');
+       $this->data['column_sku']   = $this->language->get('entry_sku');
        $this->data['column_price'] = $this->language->get('column_price');
        $this->data['column_quantity'] = $this->language->get('column_quantity');
        $this->data['column_status'] = $this->language->get('column_status');
        $this->data['column_action'] = $this->language->get('column_action');
 
@@ -481,10 +494,14 @@ class ControllerCatalogProduct extends Controller {
 
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
 
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
+
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
 
        if (isset($this->request->get['filter_quantity'])) {
@@ -505,10 +522,11 @@ class ControllerCatalogProduct extends Controller {
            $url .= '&page=' . $this->request->get['page'];
        }
 
        $this->data['sort_name'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=pd.name' . $url, 'SSL');
        $this->data['sort_model'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.model' . $url, 'SSL');
+       $this->data['sort_sku']   = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.sku' . $url, 'SSL');
        //filter category start//
         $this->data['sort_category'] = HTTPS_SERVER . 'index.php?route=catalog/product&token=' . $this->session->data['token'] . '&sort=p2c.category' . $url;
         //filter category end//
        $this->data['sort_price'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.price' . $url, 'SSL');
        $this->data['sort_quantity'] = $this->url->link('catalog/product', 'token=' . $this->session->data['token'] . '&sort=p.quantity' . $url, 'SSL');
@@ -523,10 +541,14 @@ class ControllerCatalogProduct extends Controller {
 
        if (isset($this->request->get['filter_model'])) {
            $url .= '&filter_model=' . $this->request->get['filter_model'];
        }
 
+       if (isset($this->request->get['filter_sku'])) {
+           $url .= '&filter_sku=' . $this->request->get['filter_sku'];
+       }
+
        if (isset($this->request->get['filter_price'])) {
            $url .= '&filter_price=' . $this->request->get['filter_price'];
        }
 
        // Add
@@ -560,10 +582,11 @@ class ControllerCatalogProduct extends Controller {
 
        $this->data['pagination'] = $pagination->render();
 
        $this->data['filter_name'] = $filter_name;
        $this->data['filter_model'] = $filter_model;
+       $this->data['filter_sku']   = $filter_sku;
        $this->data['filter_price'] = $filter_price;
        // Add
         $this->data['filter_category'] = $filter_category;
         // End add
        $this->data['filter_quantity'] = $filter_quantity;
diff --git a/public_html/admin/model/catalog/product.php b/public_html/admin/model/catalog/product.php
index d0c3772..2fcb162 100644
--- a/public_html/admin/model/catalog/product.php
+++ b/public_html/admin/model/catalog/product.php
@@ -361,10 +361,14 @@ class ModelCatalogProduct extends Model {
 
            if (isset($data['filter_model']) && !is_null($data['filter_model'])) {
                $sql .= " AND LCASE(p.model) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_model'], 'UTF-8')) . "%'";
            }
 
+           if (isset($data['filter_sku']) && !is_null($data['filter_sku'])) {
+               $sql .= " AND LCASE(p.sku) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_sku'], 'UTF-8')) . "%'";
+           }
+
            if (isset($data['filter_price']) && !is_null($data['filter_price'])) {
                $sql .= " AND p.price LIKE '" . $this->db->escape($data['filter_price']) . "%'";
            }
 
            // Add Category Filter
@@ -385,10 +389,11 @@ class ModelCatalogProduct extends Model {
            }
 
            $sort_data = array(
                'pd.name',
                'p.model',
+               'p.sku',
                'p.price',
                // add
                 'p2c.category_id',
                // end
                'p.quantity',
@@ -671,10 +676,14 @@ class ModelCatalogProduct extends Model {
 
        if (isset($data['filter_model']) && !is_null($data['filter_model'])) {
            $sql .= " AND LCASE(p.model) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_model'], 'UTF-8')) . "%'";
        }
 
+       if (isset($data['filter_sku']) && !is_null($data['filter_sku'])) {
+           $sql .= " AND LCASE(p.sku) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_sku'], 'UTF-8')) . "%'";
+       }
+
        if (isset($data['filter_price']) && !is_null($data['filter_price'])) {
            $sql .= " AND p.price LIKE '" . $this->db->escape($data['filter_price']) . "%'";
        }
 
        // Add
diff --git a/public_html/admin/view/template/catalog/product_list.tpl b/public_html/admin/view/template/catalog/product_list.tpl
index a57163f..19a25fa 100644
--- a/public_html/admin/view/template/catalog/product_list.tpl
+++ b/public_html/admin/view/template/catalog/product_list.tpl
@@ -31,10 +31,15 @@
               <td class="left"><?php if ($sort == 'p.model') { ?>
                 <a href="<?php echo $sort_model; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_model; ?></a>
                 <?php } else { ?>
                 <a href="<?php echo $sort_model; ?>"><?php echo $column_model; ?></a>
                 <?php } ?></td>
+              <td class="left"><?php if ($sort == 'p.sku') { ?>
+                <a href="<?php echo $sort_sku; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_sku; ?></a>
+                <?php } else { ?>
+                <a href="<?php echo $sort_sku; ?>"><?php echo $column_sku; ?></a>
+                <?php } ?></td>
               <td class="left"><?php if ($sort == 'p.price') { ?>
                 <a href="<?php echo $sort_price; ?>" class="<?php echo strtolower($order); ?>"><?php echo $column_price; ?></a>
                 <?php } else { ?>
                 <a href="<?php echo $sort_price; ?>"><?php echo $column_price; ?></a>
                 <?php } ?></td>
@@ -60,10 +65,11 @@
             <tr class="filter">
               <td></td>
               <td></td>
               <td><input type="text" name="filter_name" value="<?php echo $filter_name; ?>" /></td>
               <td><input type="text" name="filter_model" value="<?php echo $filter_model; ?>" size="10" /></td>
+              <td><input type="text" name="filter_sku"   value="<?php echo $filter_sku; ?>" size="10" /></td>
               <td align="left"><input type="text" name="filter_price" value="<?php echo $filter_price; ?>" size="8"/></td>
               <td ><select name="filter_category" style="width: 100%;" >
               <option value="*"></option>
               <?php foreach ($categories as $category) { ?>
                 <?php if ($category['category_id']==$filter_category) { ?>
@@ -98,10 +104,11 @@
                 <input type="checkbox" name="selected[]" value="<?php echo $product['product_id']; ?>" />
                 <?php } ?></td>
               <td class="center"><img src="<?php echo $product['image']; ?>" width="<?php echo $product['image_width']; ?>" height="<?php echo $product['image_height']; ?>" alt="<?php echo $product['name']; ?>" style="padding: 1px; border: 1px solid #DDDDDD;" /></td>
               <td class="left"><?php echo $product['name']; ?></td>
               <td class="left"><?php echo $product['model']; ?></td>
+              <td class="left"><?php echo $product['sku']; ?></td>
               <td class="right">
                <?php if ($product['special']) { ?>
                 <span style="text-decoration:line-through"><?php echo $product['price']; ?></span>
<span style="color:#b00;"><?php echo $product['special']; ?></span>
                 <?php } else { ?>
                <?php echo $product['price']; ?>
@@ -126,11 +133,11 @@
                 <?php } ?></td>
             </tr>
             <?php } ?>
             <?php } else { ?>
             <tr>
-              <td class="center" colspan="8"><?php echo $text_no_results; ?></td>
+              <td class="center" colspan="10"><?php echo $text_no_results; ?></td>
             </tr>
             <?php } ?>
           </tbody>
         </table>
       </form>
@@ -152,10 +159,16 @@ function filter() {
 
    if (filter_model) {
        url += '&filter_model=' + encodeURIComponent(filter_model);
    }
 
+   var filter_sku = $('input[name=\'filter_sku\']').attr('value');
+
+   if (filter_sku) {
+       url += '&filter_sku=' + encodeURIComponent(filter_sku);
+   }
+
    var filter_price = $('input[name=\'filter_price\']').attr('value');
 
    if (filter_price) {
        url += '&filter_price=' + encodeURIComponent(filter_price);
    }

Цветовые схемы для Geany

- Posted in Uncategorized by

Одновременно с появлением вчера шрифтов Ubuntu Mono и Ubuntu Condensed вспомнил, что хотел ещё цвета в Geany настроить.

В итоге обзавёлся десятком (или даже двумя) новых светлых и тёмных тем

отсюда: https://github.com/codebrainz/geany-themes/tree/deprecated (у меня Ubuntu 11.04 и Geany 0.20):

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 11.04
Release:    11.04
Codename:   natty
$ geany --version
geany 0.20 (built on Mar 11 2011 with GTK 2.24.1, GLib 2.28.2, GIO)

Днём предпочитаю что-то вроде Gedit или Tango-light. Только цвет комментариев изменил на серый: легко читающийся на белом, но не бросающийся в глаза.

Самое приятное - в этом наборе много тёмных цветовых тем, удобных для работы вечером и ночью. Пока не выбрал фаворитов, т.к. сейчас ночные посиделки с кодом редки, но планирую сильно сдвинуть рабочие часы на утро, так что вскоре снова пригодятся и будут опробованы.

UPD: из ночных победила цветовая тема Bespin.

Яндексу потребовалось 12 дней, чтобы проиндексировать наш интернет-магазин

- Posted in Webdev by

Я удивлён таким чудовищным сроком. Sitemap.xml был добавлен 11-го числа на http://webmaster.yandex.ua/, и только сегодня (23 сентября) наконец-то сайт попал в поисковую выдачу, а надпись про "сайт успешно добавлен в очередь, ждите" сменилась информацией о том, что столько-то страниц проиндексировано.

Обычно люди говорят о 2-3 днях на индексацию.

Я уж думал, что мы попали в немилость -- предположим, посчитали злостными копипастерами. В той области, кстати, которой мы занялись, выглядеть оригинально по части текстов весьма затруднительно. По фотографиям тоже, поскольку основная масса фото берётся с сайтов производителей и, разумеется, в сотнях магазинов одинаковые. Но мы над этим работаем.

Для сравнения: Google понадобилось меньше дня. Я даже не собирался проверять раньше, чем через сутки, но приятель накатал всем членам команды вопль радости по email :) Мол, ура, всем радоваться - мы попали в поисковую выдачу. Он случайно обнаружил, я даже не успел сказать ему, что занялся поисковиками.

Google Webmaster Tools

Яндекс.Вебмастер

Хотите грохнуть репозиторий на Google Code, но не знаете как?

- Posted in Uncategorized by

Смените тип репозитория с SVN на Git. Это не сконвертирует его, как можно было бы предположить, а молча удалит. Сюрприз!

Git User&apos;s Survey 2011

- Posted in Uncategorized by

UPD: This survey is currently closed. More information can be found on http://git.wiki.kernel.org/index.php/GitSurvey2011


Все на выборы!

В смысле заполните анкетку: https://www.survs.com/survey/VCAGZA8CT5

Результаты наверное будут многим пользователям Git интересны.

Я по ходу дела узнал много новых букв названий интересного Git-софта.

Вывод Git branch в подсказку командной строки

- Posted in Uncategorized by

При активном использовании Git-а и переключений между разными ветками часто хочется видеть, в какой ветке находишься. Удобно добавить вывод названия branch-а в подсказку командной строки bash:

rb@rb-msi:~/projects/opencart-a4u (master)$ 

Для этого в ~/.bashrc находим строку, похожую на PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ ':

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
fi

и изменяем (я закомментировал старую строку и добавил новую):

if [ "$color_prompt" = yes ]; then
    PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
else
    # PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\$ '
    PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w$(git branch &>/dev/null; if [ $? -eq 0 ]; then echo " ($(git branch | grep '^*' |sed s/\*\ //))"; fi)\$ '
fi

Теперь при попадании в каталог внутри Git репозитория bash prompt будет показывать в скобках имя текущего branch-а, что выглядит примерно так:

rb@rb-msi:~/projects/opencart-a4u (master)$ 

UPD 2012-02-07: See also http://en.newinstance.it/2010/05/23/git-autocompletion-and-enhanced-bash-prompt/

Opencart 1.5.x (admin): улучшения при вводе атрибутов товаров

- Posted in Uncategorized by

Кому нравится способ ввода атрибутов товаров в OpenCart 1.5.1.2? Думаю, что никому: если их хотя бы десяток, начинаешь забывать, путаться или упорно пытаться вспомнить, как этот атрибут назвали ранее. Кабель? Шнур питания? Длина шнура? Ох... Почему по умолчанию везде в админке сделан поиск по "сначалам" названий (и в связях товаров, и в атрибутах, и в рекомендуемых, и... да везде, в общем) -- мне совершенно непонятно. Это неудобно. Мешает, утомляет, раздражает и тормозит.

Некоторым вначале даже может показаться, что в Опенкарт не работает добавление атрибутов: атрибуты ещё не заведены, в подсказке ничего не выпадает. Люди пытаются ввести своё название атрибута и ожидают, что оно сохранится. А оно не сохраняется. И логика поведения неочевидна. На самом деле надо предварительно зарегистрировать атрибуты (см. меню Каталог - Атрибуты), а затем в этих полях выбирать их из выпадающего списка по мере набора первых букв названий атрибутов.

В идеале я бы предпочёл видеть и редактировать атрибуты товаров в виде таблицы. Или хотя бы выпадающие списки. Почти как сейчас, но чтобы не приходилось мучительно вспоминать первую букву или держать рядом страницу со списком. Ну а пока таблицы нет, можно существенно облегчить себе ввод атрибутов, внеся совсем небольшое исправление.

Которое реализует:

  • поиск любого вхождения введённых символов, а не только с начала названий атрибутов;
  • при вводе "." в пустое поле показывается полный список атрибутов, остаётся только выбрать необходимый.

У нас в магазине сейчас заведено около 20 атрибутов -- облегчение даже на таком количестве колоссальное.

Модификации для 1.5.1.2 и ниже (1.5.x)

diff --git a/public_html/admin/controller/catalog/attribute.php b/public_html/admin/controller/catalog/attribute.php
index 637ab6c..8097fc1 100644
--- a/public_html/admin/controller/catalog/attribute.php
+++ b/public_html/admin/controller/catalog/attribute.php
@@ -432,6 +432,7 @@ class ControllerCatalogAttribute extends Controller {
            }
        }
 
+       /*
        $sort_order = array();
 
        foreach ($json as $key => $value) {
@@ -439,6 +440,7 @@ class ControllerCatalogAttribute extends Controller {
        }
 
        array_multisort($sort_order, SORT_ASC, $json);
+       */
 
        $this->load->library('json');
 
diff --git a/public_html/admin/model/catalog/attribute.php b/public_html/admin/model/catalog/attribute.php
index 1e438ef..019b155 100644
--- a/public_html/admin/model/catalog/attribute.php
+++ b/public_html/admin/model/catalog/attribute.php
@@ -35,7 +35,10 @@ class ModelCatalogAttribute extends Model {
        $sql = "SELECT *, (SELECT agd.name FROM " . DB_PREFIX . "attribute_group_description agd WHERE agd.attribute_group_id = a.attribute_group_id AND agd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS attribute_group FROM " . DB_PREFIX . "attribute a LEFT JOIN " . DB_PREFIX . "attribute_description ad ON (a.attribute_id = ad.attribute_id) WHERE ad.language_id = '" . (int)$this->config->get('config_language_id') . "'";
 
        if (isset($data['filter_name']) && !is_null($data['filter_name'])) {
-           $sql .= " AND LCASE(ad.name) LIKE '" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%'";
+           if( $data['filter_name'] == '.' )
+               $sql .= " AND LCASE(ad.name) LIKE '%'";
+           else
+               $sql .= " AND LCASE(ad.name) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%'";
        }
 
        if (isset($data['filter_attribute_group_id']) && !is_null($data['filter_attribute_group_id'])) {

Модификации для 1.5.1.3

diff --git a/upload/admin/model/catalog/attribute.php b/upload/admin/model/catalog/attribute.php
index f38a92e..1d6c8e1 100644
--- a/upload/admin/model/catalog/attribute.php
+++ b/upload/admin/model/catalog/attribute.php
@@ -35,7 +35,10 @@ class ModelCatalogAttribute extends Model {
        $sql = "SELECT *, (SELECT agd.name FROM " . DB_PREFIX . "attribute_group_description agd WHERE agd.attribute_group_id = a.attribute_group_id AND agd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS attribute_group FROM " . DB_PREFIX . "attribute a LEFT JOIN " . DB_PREFIX . "attribute_description ad ON (a.attribute_id = ad.attribute_id) WHERE ad.language_id = '" . (int)$this->config->get('config_language_id') . "'";
 
        if (!empty($data['filter_name'])) {
-           $sql .= " AND LCASE(ad.name) LIKE '" . $this->db->escape(utf8_strtolower($data['filter_name'])) . "%'";
+           if( $data['filter_name'] == '.' )
+               $sql .= " AND LCASE(ad.name) LIKE '%'";
+           else
+               $sql .= " AND LCASE(ad.name) LIKE '%" . $this->db->escape(utf8_strtolower($data['filter_name'])) . "%'";
        }
 
        if (!empty($data['filter_attribute_group_id'])) {

Модификации для 1.5.6

Вообще-то никакой разницы с 1513 нет - просто слегка отличается исходный код (убрали зачем-то LCASE). Поэтому на всякий случай привожу diff для этой версии:

diff --git a/public_html/admin/model/catalog/attribute.php b/public_html/admin/model/catalog/attribute.php
index e527efe..77d8a8b 100644
--- a/public_html/admin/model/catalog/attribute.php
+++ b/public_html/admin/model/catalog/attribute.php
@@ -35,7 +35,10 @@ class ModelCatalogAttribute extends Model {
        $sql = "SELECT *, (SELECT agd.name FROM " . DB_PREFIX . "attribute_group_description agd WHERE agd.attribute_group_id = a.attribute_group_id AND agd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS attribute_group FROM " . DB_PREFIX . "attribute a LEFT JOIN " . DB_PREFIX . "attribute_description ad ON (a.attribute_id = ad.attribute_id) WHERE ad.language_id = '" . (int)$this->config->get('config_language_id') . "'";
 
        if (!empty($data['filter_name'])) {
-           $sql .= " AND ad.name LIKE '" . $this->db->escape($data['filter_name']) . "%'";
+           if( $data['filter_name'] == '.' )
+               $sql .= " AND ad.name LIKE '%'";
+           else
+               $sql .= " AND LCASE(ad.name) LIKE '%" . $this->db->escape($data['filter_name']) . "%'";
        }
 
        if (!empty($data['filter_attribute_group_id'])) {

См. также коментарий #28 - для тех, кто "не понимает, что такое дифф", там в виде "было / стало".

Видеокамера на орбите снаружи космической станции

- Posted in Uncategorized by

Johnson Space Center - NASA-ISS-push Streaming ISS Video:

mms://a1709.l1856953708.c18569.g.lm.akamaistream.net/D/1709/18569/v0001/reflector:53708

Смотреть можно в любом видео-плеере, обычно сейчас все умеют кроме "Open File" что-то вроде "Open Location", "Open URL", "Open Network Location" и т.п.

Поток примерно 20-50 килобайт в секунду, 640x360. Иногда можно послушать переговоры астронавтов с Землёй :)

UPD: включил в 17:50 - вовсю идут переговоры по-русски вперемешку с английским.

Камер несколько, смотрят в разные стороны - время от времени происходит переключение.

Орбиту и текущее положение станции можно увидеть здесь: http://www.heavens-above.com/orbit.aspx?satid=25544.

P.S. Доложили, что Totem 2.28.2 (устанавливаемый в Ubuntu по умолчанию видео-плеер) не показывает поток. С Gnome-mplayer и VLC проблем не замечено. Под Windows не пробовал, но если что-то имеющееся под рукой не воспроизводит -- могу порекомендовать GOM Player (с этим плеером у меня приключений за долгие годы не было). Ну и VLC существует под Windows.

Opencart 1.5.1.x (catalog): как вывести третий уровень категорий в главном меню

- Posted in Uncategorized by

UPD 2015-01: Статья написана в 2011 году для актуальных тогда версий OpenCart. Для более новых описанные изменения могут не подойти. Скорей всего описанные изменения актуальны для всех oc151x (делалось на них), может быть до oc153x (не уверен). Есть информация о том, что для v1564 не подходит. Я не пользовался всей этой линейкой версий и соответственно не делал для них изменения. Если не получается разобраться, можно нанять меня, за 10-15 WMZ сделаю для вашей версии.

Если используется тема, отличная от default, изменения надо производить в её файлах, разумеется (css, tpl). И только в случае отсутствия их в используемой теме их следует искать в default. И в этом случае может понадобиться индивидуальный подход, поскольку CSS может значительно отличаться. Так что будьте особенно аккуратны, делайте резервные копии изменяемых файлов.

Имейте в виду, что оригинальное решение, принятое в Опенкарт, очень неоптимальное по производительности и при большом количестве категорий и товаров этот блок надо переделать и сделать совершенно по-другому. Здесь показан способ, использующий стандартный механизм Опенкарт.

 

diff --git a/public_html/catalog/controller/common/header.php b/public_html/catalog/controller/common/header.php
index 21bfe6f..a603a5f 100644
--- a/public_html/catalog/controller/common/header.php
+++ b/public_html/catalog/controller/common/header.php
@@ -200,10 +200,11 @@ class ControllerCommonHeader extends Controller {
                                    
                    $children_data[] = array(
                        'name'  => $child['name'] . ' (' . $product_total . ')',
-                       'href'  => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $child['category_id'])  
-                   );                  
+                       'href'  => $this->url->link('product/category', 'path=' . $category['category_id'] . '_' . $child['category_id']),
+                       'children' => $this->getChildrenData($child['category_id'], $category['category_id']) // rb, 2011-09-03: menu 3rd level
+                   );
                }
-               
+
                // Level 1
                $this->data['categories'][] = array(
                    'name'     => $category['name'],
@@ -237,6 +238,28 @@ class ControllerCommonHeader extends Controller {
        }
        
        $this->render();
-   }   
+   }
+
+   // http://rb.labtodo.com/, 2011-09-03: menu 3rd level
+   private function getChildrenData( $ctg_id, $path_prefix )
+   {
+       $children_data = array();
+       $children = $this->model_catalog_category->getCategories($ctg_id);
+
+       foreach ($children as $child) {
+           $data = array(
+               'filter_category_id'  => $child['category_id'],
+               'filter_sub_category' => true
+           );
+
+           $product_total = $this->model_catalog_product->getTotalProducts($data);
+
+           $children_data[] = array(
+               'name'  => $child['name'] . ' (' . $product_total . ')',
+               'href'  => $this->url->link('product/category', 'path=' . $path_prefix . '_' . $child['category_id'])
+           );
+       }
+       return $children_data;
+   }
 }
 ?>
\ No newline at end of file
diff --git a/public_html/catalog/view/theme/default/stylesheet/stylesheet.css b/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
index ecee15b..3f89efa 100644
--- a/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
+++ b/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
@@ -343,7 +343,9 @@ label {
 #menu > ul > li > a.active {
    background: #000;
 }
-#menu > ul > li > a {
+#menu > ul > li > a
+/*#menu > ul > li > div > ul > li > a*/
+{
    font-size: 13px;
    color: #FFF;
    line-height: 14px;
@@ -353,7 +355,8 @@ label {
    z-index: 6;
    position: relative;
 }
-#menu > ul > li > div {
+#menu > ul > li > div
+{
    display: none;
    background: #FFFFFF;
    position: absolute;
@@ -366,16 +369,39 @@ label {
    border-radius: 0px 0px 5px 5px;
    background: url('../image/menu.png');
 }
-#menu > ul > li:hover > div {
+#menu > ul > li > div > ul > li > div
+{
+   display: none;
+   background: #FFFFFF;
+   position: absolute;
+   z-index: 5;
+   padding: 5px;
+   margin-left: 50px;
+   border: 1px solid #000000;
+   -webkit-border-radius: 0px 0px 5px 5px;
+   -moz-border-radius: 0px 0px 5px 5px;
+   -khtml-border-radius: 0px 0px 5px 5px;
+   border-radius: 0px 0px 5px 5px;
+   background: url('../image/menu.png');
+}
+#menu > ul > li:hover > div,
+#menu > ul > li:hover > div > ul > li:hover > div
+{
    display: table;
 }
-#menu > ul > li > div > ul {
+#menu > ul > li > div > ul,
+#menu > ul > li > div > ul > li > div > ul
+{
    display: table-cell;
 }
-#menu > ul > li ul + ul {
+#menu > ul > li ul + ul,
+#menu > ul > li ul > li ul + ul
+{
    padding-left: 20px;
 }
-#menu > ul > li ul > li > a {
+#menu > ul > li ul > li > a,
+#menu > ul > li ul > li ul > li > a
+{
    text-decoration: none;
    padding: 4px;
    color: #FFFFFF;
@@ -386,9 +412,12 @@ label {
 #menu > ul > li ul > li > a:hover {
    background: #000000;
 }
-#menu > ul > li > div > ul > li > a {
+#menu > ul > li > div > ul > li > a,
+#menu > ul > li > div > ul > li > div > ul > li > a
+{
    color: #FFFFFF;
 }
+
 .breadcrumb {
    color: #CCCCCC;
    margin-bottom: 10px;
diff --git a/public_html/catalog/view/theme/default/template/common/header.tpl b/public_html/catalog/view/theme/default/template/common/header.tpl
index eef4ae7..d93120a 100644
--- a/public_html/catalog/view/theme/default/template/common/header.tpl
+++ b/public_html/catalog/view/theme/default/template/common/header.tpl
@@ -123,7 +123,17 @@ DD_belatedPNG.fix('#logo img');
           <?php $j = $i + ceil(count($category['children']) / $category['column']); ?>
           <?php for (; $i < $j; $i++) { ?>
           <?php if (isset($category['children'][$i])) { ?>
-          <li><a href="<?php echo $category['children'][$i]['href']; ?>"><?php echo $category['children'][$i]['name']; ?></a></li>
+          <li><a href="<?php echo $category['children'][$i]['href']; ?>"><?php echo $category['children'][$i]['name']; ?></a>
+            <?php if( $category['children'][$i]['children'] ) { ?>
+            <div>
+              <ul>
+              <?php foreach( $category['children'][$i]['children'] as $menu3item ) { ?>
+              <li><a href="<?php echo $menu3item['href']; ?>"><?php echo $menu3item['name']; ?></a></li>
+              <?php } ?>
+              </ul>
+            </div>
+            <?php } ?>
+          </li>
           <?php } ?>
           <?php } ?>
         </ul>

Первичные настройки Git

- Posted in Uncategorized by

Представляемся надолго, чтобы коммиты не были ничейными:

git config --global user.name "Your Name Comes Here"
git config --global user.email you@yourdomain.example.com

Облегчаем визуальное восприятие изменений:

git config --global color.diff auto
git config --global color.status auto
git config --global color.branch auto
git config --global color.ui auto

Не забываем про заплетающиеся пальцы (вы способны быстро набрать "status" без опечаток?! Раз 50 в день?!?! Ого!):

git config --global alias.st status
git config --global alias.ci commit
git config --global alias.co checkout
git config --global alias.logd 'log --oneline --graph --decorate'
git config --global alias.logdm 'log --oneline --graph --decorate --no-merges'
git config --global alias.logn 'log --pretty=format:"%cd %C(auto)%h (%an) %s%+b%d" --date=short'
git config --global alias.logst 'log --stat=140,100'
git config --global alias.logf 'log --stat=140,100'
git config --global alias.bav 'branch -av'
git config --global alias.rv 'remote -v'
git config --global alias.diffw 'diff --word-diff'

Проделав это один раз, можно добавить в свой бекап файл ~/.gitconfig, в котором настройки и хранятся. Или скопируйте его в Дропбокс, а в домашнем каталоге оставьте симлинк на этот файл.

И не забывайте про существование возможности авторизоваться на гит-сервере с репозиториями с помощью своего публичного SSH ключа: пара генерируется один раз, публичный ключ раздаёте тем, кто должен вас узнавать и пускать без стука, и забываете про ввод паролей.

Если нужен gitk, то перед вторым запуском (первый под Линуксом нужен, чтобы слегка удивиться, проморгаться и взбодриться) исправьте в ~/.gitk интерфейсные шрифты:

set mainfont {{andale mono} 9}
set textfont {{andale mono} 9}
set uifont {clean 10 bold}
set tabstop 4

Хотя это в принципе и через GUI сделать можно.

Opencart 1.5.x (admin): показ в списке категорий их SEO Keyword

- Posted in Uncategorized by

Буду краток:

Рецепт приготовления:

diff --git a/public_html/admin/controller/catalog/category.php b/public_html/admin/controller/catalog/category.php
index d449b87..4a33c14 100644
--- a/public_html/admin/controller/catalog/category.php
+++ b/public_html/admin/controller/catalog/category.php
@@ -108,6 +108,7 @@ class ControllerCatalogCategory extends Controller {
        $this->data['text_no_results'] = $this->language->get('text_no_results');
 
        $this->data['column_name'] = $this->language->get('column_name');
+       $this->data['column_seo_keyword'] = $this->language->get('column_seo_keyword');
        $this->data['column_sort_order'] = $this->language->get('column_sort_order');
        $this->data['column_action'] = $this->language->get('column_action');
 
@@ -406,6 +408,7 @@ class ControllerCatalogCategory extends Controller {
            $output[$result['category_id']] = array(
                'category_id' => $result['category_id'],
                'name'        => $name,
+               'keyword'     => $result['keyword'],
                'sort_order'  => $result['sort_order'],
                'selected'    => $selected,
                'action'      => $action,
diff --git a/public_html/admin/language/english/catalog/category.php b/public_html/admin/language/english/catalog/category.php
index 88a5549..669bb3a 100644
--- a/public_html/admin/language/english/catalog/category.php
+++ b/public_html/admin/language/english/catalog/category.php
@@ -9,6 +9,7 @@ $_['text_image_manager']     = 'Image Manager';
 
 // Column
 $_['column_name']            = 'Category Name';
+$_['column_seo_keyword']     = 'SEO Keyword';
 $_['column_sort_order']      = 'Sort Order';
 $_['column_action']          = 'Action';
 
diff --git a/public_html/admin/language/russian/catalog/category.php b/public_html/admin/language/russian/catalog/category.php
index 6acd92a..faa975b 100644
--- a/public_html/admin/language/russian/catalog/category.php
+++ b/public_html/admin/language/russian/catalog/category.php
@@ -9,6 +9,7 @@ $_['text_image_manager']     = 'Менеджер изображений';
 
 // Column
 $_['column_name']            = 'Название категории';
+$_['column_seo_keyword']     = 'SEO URL';
 $_['column_sort_order']      = 'Порядок сортировки';
 $_['column_action']          = 'Действие';
 
diff --git a/public_html/admin/model/catalog/category.php b/public_html/admin/model/catalog/category.php
index 27a35f4..ffbee94 100644
--- a/public_html/admin/model/catalog/category.php
+++ b/public_html/admin/model/catalog/category.php
@@ -193,7 +193,7 @@ class ModelCatalogCategory extends Model {
    }
 
    public function getCategoriesByParentId($parent_id = 0) {
-       $query = $this->db->query("SELECT *, (SELECT COUNT(parent_id) FROM " . DB_PREFIX . "category WHERE parent_id = c.category_id) AS children FROM " . DB_PREFIX . "category c LEFT JOIN " . DB_PREFIX . "category_description cd ON (c.category_id = cd.category_id) LEFT JOIN " . DB_PREFIX . "category_to_store c2s ON (c.category_id = c2s.category_id) WHERE c.parent_id = '" . (int)$parent_id . "' AND cd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND c2s.store_id = '" . (int)$this->config->get('config_store_id') . "' ORDER BY c.sort_order, cd.name");
+       $query = $this->db->query("SELECT *, (SELECT COUNT(parent_id) FROM " . DB_PREFIX . "category WHERE parent_id = c.category_id) AS children FROM " . DB_PREFIX . "category c LEFT JOIN " . DB_PREFIX . "category_description cd ON (c.category_id = cd.category_id) LEFT JOIN " . DB_PREFIX . "category_to_store c2s ON (c.category_id = c2s.category_id) LEFT JOIN `".DB_PREFIX."url_alias` ua ON ( ua.query = CONCAT('category_id=', c.category_id) ) WHERE c.parent_id = '" . (int)$parent_id . "' AND cd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND c2s.store_id = '" . (int)$this->config->get('config_store_id') . "' ORDER BY c.sort_order, cd.name");
 
        return $query->rows;
    }
diff --git a/public_html/admin/view/template/catalog/category_list.tpl b/public_html/admin/view/template/catalog/category_list.tpl
index 76cdd01..b5d9af9 100644
--- a/public_html/admin/view/template/catalog/category_list.tpl
+++ b/public_html/admin/view/template/catalog/category_list.tpl
@@ -23,6 +23,7 @@
             <tr>
               <td width="1" style="text-align: center;"><input type="checkbox" onclick="$('input[name*=\'selected\']').attr('checked', this.checked);" /></td>
               <td class="left"><?php echo $column_name; ?></td>
+              <td class="left"><?php echo $column_seo_keyword; ?></td>
               <td class="right"><?php echo $column_sort_order; ?></td>
               <td class="right"><?php echo $column_action; ?></td>
             </tr>
@@ -41,6 +42,7 @@
               <?php } else { ?>
                 <td class="left"><?php echo $category['indent']; ?><?php echo $category['name']; ?></td>
               <?php } ?>
+              <td class="left"><?php echo $category['keyword']; ?></td>
               <td class="right"><?php echo $category['sort_order']; ?></td>
               <td class="right"><?php foreach ($category['action'] as $action) { ?>
                 [ <a href="<?php echo $action['href']; ?>"><?php echo $action['text']; ?></a> ]
@@ -49,7 +51,7 @@
             <?php } ?>
             <?php } else { ?>
             <tr>
-              <td class="center" colspan="4"><?php echo $text_no_results; ?></td>
+              <td class="center" colspan="5"><?php echo $text_no_results; ?></td>
             </tr>
             <?php } ?>
           </tbody>

Opencart 1.5.x (catalog): как сделать, чтобы модуль категорий показывал все подкатегории

- Posted in Uncategorized by

Чтобы модуль категорий показывал все подкатегории развёрнуто, а не только выбранную, надо изменить CSS стиль шаблона. Если используется шаблон 'default', то:

diff --git a/public_html/catalog/view/theme/default/stylesheet/stylesheet.css b/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
index 3f89efa..690b824 100644
--- a/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
+++ b/public_html/catalog/view/theme/default/stylesheet/stylesheet.css
@@ -730,7 +730,8 @@ a.button:hover span {
    color: #333;
 }
 .box-category > ul > li ul {
-   display: none;
+   /* display: none; */
+   font-size: 0.8em;
 }
 .box-category > ul > li a.active {
    font-weight: bold;

Пример того, как модуль категорий будет выглядеть, можно видеть на демонстрационном видео:

Opencart 1.5.x (catalog): как в каталоге добавить вывод атрибута к описанию товара

- Posted in Uncategorized by

В комментариях к статье об изменении длины описаний в каталоге Opencart 1.5 спросили, можно ли сделать так,

чтобы вместо описания товара в каталоге выводился определённый атрибут товара (краткое описание товара). Интересует для OpenCart 1.5.1

Это несложно, изменения в двух файлах приведены ниже.

Как можно заметить, в файле catalog/controller/product/category.php я добавил текст атрибута к описанию. Не совсем то, что просили, но если надо выводить только атрибут -- думаю, несложно догадаться, как это сделать (заменить строчку на $descr_plaintext = $result['attribute_text'];), а несколько предыдущих строк там могут быть убраны. В общем, при наличии минимальных познаний или аккуратно методом тыка это будет несложно сделать, поэтому не описываю.

Изменения базируются на моём коде, а не чистом OpenCart v1.5: в этом месте была правка, но она задокументирована в упомянутой статье и минимальна. Поэтому, думаю, проблем с нахождением места и внесением изменений не возникнет.

Требуется заменить переменную $attr_id_to_descr = 0; на идентификатор своего атрибута, который должен выводиться. Его можно найти в админ-части магазина, внимательно изучив ссылку редактирования нужного нам атрибута: в адресе после токена (token=...) присутствует переменная ...&attribute_id=9 (например). Вот этот идентификатор и надо указать вместо нуля.

diff --git a/public_html/catalog/controller/product/category.php b/public_html/catalog/controller/product/category.php
index 54ad9bc..f94cc43 100644
--- a/public_html/catalog/controller/product/category.php
+++ b/public_html/catalog/controller/product/category.php
@@ -185,6 +185,8 @@ class ControllerProductCategory extends Controller {
                                {
                                        $descr_plaintext = mb_substr($descr_plaintext, 0, $cut_descr_symbols, 'UTF-8') . '&nbsp;&hellip;';
                                }
+                               if( !empty($result['attribute_text']) )
+                                       $descr_plaintext .= '<hr />' . $result['attribute_text'];
                                $this->data['products'][] = array(
                                        'product_id'  => $result['product_id'],
                                        'thumb'       => $image,
diff --git a/public_html/catalog/model/catalog/product.php b/public_html/catalog/model/catalog/product.php
index 595bfc6..2b19a72 100644
--- a/public_html/catalog/model/catalog/product.php
+++ b/public_html/catalog/model/catalog/product.php
@@ -11,13 +11,40 @@ class ModelCatalogProduct extends Model {
            $customer_group_id = $this->config->get('config_customer_group_id');
        }   
                
-       $query = $this->db->query("SELECT DISTINCT *, pd.name AS name, p.image, m.name AS manufacturer, (SELECT price FROM " . DB_PREFIX . "product_discount pd2 WHERE pd2.product_id = p.product_id AND pd2.customer_group_id = '" . (int)$customer_group_id . "' AND pd2.quantity = '1' AND ((pd2.date_start = '0000-00-00' OR pd2.date_start < NOW()) AND (pd2.date_end = '0000-00-00' OR pd2.date_end > NOW())) ORDER BY pd2.priority ASC, pd2.price ASC LIMIT 1) AS discount, (SELECT price FROM " . DB_PREFIX . "product_special ps WHERE ps.product_id = p.product_id AND ps.customer_group_id = '" . (int)$customer_group_id . "' AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW()) AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())) ORDER BY ps.priority ASC, ps.price ASC LIMIT 1) AS special, (SELECT points FROM " . DB_PREFIX . "product_reward pr WHERE pr.product_id = p.product_id AND customer_group_id = '" . (int)$customer_group_id . "') AS reward, (SELECT ss.name FROM " . DB_PREFIX . "stock_status ss WHERE ss.stock_status_id = p.stock_status_id AND ss.language_id = '" . (int)$this->config->get('config_language_id') . "') AS stock_status, (SELECT wcd.unit FROM " . DB_PREFIX . "weight_class_description wcd WHERE p.weight_class_id = wcd.weight_class_id AND wcd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS weight_class, (SELECT lcd.unit FROM " . DB_PREFIX . "length_class_description lcd WHERE p.length_class_id = lcd.length_class_id AND lcd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS length_class, (SELECT AVG(rating) AS total FROM " . DB_PREFIX . "review r1 WHERE r1.product_id = p.product_id AND r1.status = '1' GROUP BY r1.product_id) AS rating, (SELECT COUNT(*) AS total FROM " . DB_PREFIX . "review r2 WHERE r2.product_id = p.product_id AND r2.status = '1' GROUP BY r2.product_id) AS reviews FROM " . DB_PREFIX . "product p LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id) LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id) LEFT JOIN " . DB_PREFIX . "manufacturer m ON (p.manufacturer_id = m.manufacturer_id) WHERE p.product_id = '" . (int)$product_id . "' AND pd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND p.status = '1' AND p.date_available <= NOW() AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'");
-       
+       $attr_id_to_descr = 0;
+       if( 0 != $attr_id_to_descr )
+       {
+           $sql_attr1 = " p.product_id AS product_id, pa.text as attribute_text, ";
+           $sql_attr2 = " LEFT JOIN " . DB_PREFIX . "product_attribute pa ON (p.product_id = pa.product_id AND pa.attribute_id=" . $attr_id_to_descr .')';
+       }
+       else
+       {
+           $sql_attr1 = "";
+           $sql_attr2 = "";
+       }
+       $query = $this->db->query("SELECT DISTINCT *, pd.name AS name, p.image, m.name AS manufacturer,". $sql_attr1
+           ." (SELECT price FROM " . DB_PREFIX . "product_discount pd2 WHERE pd2.product_id = p.product_id AND pd2.customer_group_id = '" . (int)$customer_group_id . "' AND pd2.quantity = '1' AND ((pd2.date_start = '0000-00-00' OR pd2.date_start < NOW()) AND (pd2.date_end = '0000-00-00' OR pd2.date_end > NOW())) ORDER BY pd2.priority ASC, pd2.price ASC LIMIT 1) AS discount,"
+           ." (SELECT price FROM " . DB_PREFIX . "product_special ps WHERE ps.product_id = p.product_id AND ps.customer_group_id = '" . (int)$customer_group_id . "' AND ((ps.date_start = '0000-00-00' OR ps.date_start < NOW()) AND (ps.date_end = '0000-00-00' OR ps.date_end > NOW())) ORDER BY ps.priority ASC, ps.price ASC LIMIT 1) AS special,"
+           ." (SELECT points FROM " . DB_PREFIX . "product_reward pr WHERE pr.product_id = p.product_id AND customer_group_id = '" . (int)$customer_group_id . "') AS reward,"
+           ." (SELECT ss.name FROM " . DB_PREFIX . "stock_status ss WHERE ss.stock_status_id = p.stock_status_id AND ss.language_id = '" . (int)$this->config->get('config_language_id') . "') AS stock_status,"
+           ." (SELECT wcd.unit FROM " . DB_PREFIX . "weight_class_description wcd WHERE p.weight_class_id = wcd.weight_class_id AND wcd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS weight_class,"
+           ." (SELECT lcd.unit FROM " . DB_PREFIX . "length_class_description lcd WHERE p.length_class_id = lcd.length_class_id AND lcd.language_id = '" . (int)$this->config->get('config_language_id') . "') AS length_class,"
+           ." (SELECT AVG(rating) AS total FROM " . DB_PREFIX . "review r1 WHERE r1.product_id = p.product_id AND r1.status = '1' GROUP BY r1.product_id) AS rating,"
+           ." (SELECT COUNT(*) AS total FROM " . DB_PREFIX . "review r2 WHERE r2.product_id = p.product_id AND r2.status = '1' GROUP BY r2.product_id) AS reviews"
+           ." FROM " . DB_PREFIX . "product p"
+           ." LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id)"
+           ." LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id)"
+           ." LEFT JOIN " . DB_PREFIX . "manufacturer m ON (p.manufacturer_id = m.manufacturer_id)"
+           . $sql_attr2
+           ." WHERE p.product_id = '" . (int)$product_id . "' AND pd.language_id = '" . (int)$this->config->get('config_language_id')
+           ."' AND p.status = '1' AND p.date_available <= NOW() AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'");
+
        if ($query->num_rows) {
            return array(
                'product_id'       => $query->row['product_id'],
                'name'             => $query->row['name'],
                'description'      => $query->row['description'],
+               'attribute_text'   => ( (0 == $attr_id_to_descr) ? '' : $query->row['attribute_text'] ),
                'meta_description' => $query->row['meta_description'],
                'meta_keyword'     => $query->row['meta_keyword'],
                'model'            => $query->row['model'],

Opencart 1.5.x: (catalog) Как добавить сортировку по производителю в каталоге товаров

- Posted in Uncategorized by

Примечание: вообще-то пока сделал сортировку не по имени, а по ID производителей: нам этого сейчас хватает. Чтобы сделать именно по названию, нужно небольшое дополнение. Возможно, добавлю его позже.

Изменяемые файлы:

catalog/controller/product/category.php

catalog/language/russian/product/category.php

catalog/model/catalog/product.php

Изменения (diff):

diff --git a/public_html/catalog/controller/product/category.php b/public_html/catalog/controller/product/category.php
index 9ef6331..54ad9bc 100644
--- a/public_html/catalog/controller/product/category.php
+++ b/public_html/catalog/controller/product/category.php
@@ -260,6 +260,19 @@ class ControllerProductCategory extends Controller {
                'value' => 'p.model-DESC',
                'href'  => $this->url->link('product/category', 'path=' . $this->request->get['path'] . '&sort=p.model&order=DESC' . $url)
            );
+
+           $this->data['sorts'][] = array(
+               'text'  => $this->language->get('text_manufacturer_asc'),
+               'value' => 'p.manufacturer_id-ASC',
+               'href'  => $this->url->link('product/category', 'path=' . $this->request->get['path'] . '&sort=p.manufacturer_id&order=ASC' . $url)
+           );
+
+           $this->data['sorts'][] = array(
+               'text'  => $this->language->get('text_manufacturer_desc'),
+               'value' => 'p.manufacturer_id-DESC',
+               'href'  => $this->url->link('product/category', 'path=' . $this->request->get['path'] . '&sort=p.manufacturer_id&order=DESC' . $url)
+           );
+
            
            $url = '';
    
diff --git a/public_html/catalog/language/russian/product/category.php b/public_html/catalog/language/russian/product/category.php
index 4e28d8c..762c91e 100644
--- a/public_html/catalog/language/russian/product/category.php
+++ b/public_html/catalog/language/russian/product/category.php
@@ -1,5 +1,5 @@
-<?php
-// Text
+<?php
+// Text
 $_['text_refine']       = 'Уточнение поиска';
 $_['text_product']      = 'Товары';
 $_['text_error']        = 'Категория не найдена!';
@@ -25,5 +25,7 @@ $_['text_rating_asc']   = 'Рейтинг (начиная с низкого)';
 $_['text_rating_desc']  = 'Рейтинг (начиная с высокого)';
 $_['text_model_asc']    = 'Модель (А- Я)';
 $_['text_model_desc']   = 'Модель (Я - А)';
+$_['text_manufacturer_asc']    = 'Производитель (А- Я)';
+$_['text_manufacturer_desc']   = 'Производитель (Я - А)';
 $_['text_limit']        = 'Показать:';
 ?>
\ No newline at end of file
diff --git a/public_html/catalog/model/catalog/product.php b/public_html/catalog/model/catalog/product.php
index c963f3e..8d90fc6 100644
--- a/public_html/catalog/model/catalog/product.php
+++ b/public_html/catalog/model/catalog/product.php
@@ -68,7 +68,7 @@ class ModelCatalogProduct extends Model {
        $product_data = $this->cache->get('product.' . $cache . '.' . $customer_group_id);
        
        if (!$product_data) {
-           $sql = "SELECT p.product_id, (SELECT AVG(rating) AS total FROM " . DB_PREFIX . "review r1 WHERE r1.product_id = p.product_id AND r1.status = '1' GROUP BY r1.product_id) AS rating FROM " . DB_PREFIX . "product p LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id) LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id) WHERE pd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND p.status = '1' AND p.date_available <= NOW() AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'"; 
+           $sql = "SELECT p.product_id, p.manufacturer_id, (SELECT AVG(rating) AS total FROM " . DB_PREFIX . "review r1 WHERE r1.product_id = p.product_id AND r1.status = '1' GROUP BY r1.product_id) AS rating FROM " . DB_PREFIX . "product p LEFT JOIN " . DB_PREFIX . "product_description pd ON (p.product_id = pd.product_id) LEFT JOIN " . DB_PREFIX . "product_to_store p2s ON (p.product_id = p2s.product_id) WHERE pd.language_id = '" . (int)$this->config->get('config_language_id') . "' AND p.status = '1' AND p.date_available <= NOW() AND p2s.store_id = '" . (int)$this->config->get('config_store_id') . "'"; 
            
            if (isset($data['filter_name']) && $data['filter_name']) {
                if (isset($data['filter_description']) && $data['filter_description']) {
@@ -113,7 +113,8 @@ class ModelCatalogProduct extends Model {
                'p.price',
                'rating',
                'p.sort_order',
-               'p.date_added'
+               'p.date_added',
+               'p.manufacturer_id'
            );  
            
            if (isset($data['sort']) && in_array($data['sort'], $sort_data)) {

Eclipse PDT и Ubuntu

- Posted in Uncategorized by

Eclipse - монстрообразинко. Шевелится неторопливо, интерфейс разлапистый и на любителя. Сперва поставил полный (sudo apt-get install eclipse), а из него - последний доступный PDT (3.x.x) через Help - Install new software. Получил постоянные ошибки при попытках открыть PHP файлы, доустановил какой-то WST (на него были намёки в сообщениях об ошибках). Никаких изменений в лучшую сторону.

Вчера не выдержал, удалил всё и попробовал ограничиться малым: оставил только минимум эклипса (sudo apt-get remove eclipse && sudo apt-get autoremove && sudo apt-get install eclipse-platform), а PDT взял с Galileo репозитория (там только 2.x.x какая-то версия доступна, слово Galileo присутствовало на заставке Эклипса при запуске, хотя второй репозиторий не сам же собой появился). Наконец-то после всех его рестартов увидел Эклипс в работоспособном состоянии. Хотя на некоторых PHP файлах его всё равно плющит (например, он не может открыть index.php из приложения Yii фреймворка).

Поживёт пока. Знакомство с Yii Framework пока только добавляет плюсов к впечатлениям о CodeIgniter. С Eclipse пользоваться и изучать Yii и всю его перенавороченную иерархию странностей будет полегче, надеюсь.

chive - MySQL database management tool

- Posted in Webdev by

Chive, alternative to PHPMyAdmin : World Gone Web

chive - MySQL database management tool: http://www.chive-project.com/

Chive is a next generation MySQL database management tool. The web-based user interface supports most of the common operations needed by software developers and database admins (databases, tables, indices, keys, triggers, views, routines, privileges management, import/export).

Chive aims to be an alternative to phpMyAdmin.

Бекап, контроль изменений и откат на стабильные конфигурации &quot;/etc&quot; в Linux&apos;е с помощью Git

- Posted in Uncategorized by

Как говорят, если вас посетила гениальная мысль -- в линукс-сообществе это уже давно реализовано или создаётся.

Решил добавить /etc в Git-репозиторий, чтобы получить все плюшки контроля версий. И, разумеется, оказалось, что я не слишком оригинален :)

Более подробно для интересующихся и сомневающихся это уже подробно описано ранее Антоном Чернышовым в статье Управление каталогом /etc с использованием tar и git.

<

p>См. также:

Распробовал Git

- Posted in Uncategorized by

Распробовал Git: он действительно оказался во многом гораздо удобнее, чем SVN. В первый день мне не очень понравилась большая, чем в Subversion, атомарность действий и необходимость каждый раз добавлять изменённые файлы в коммит. Я понимал, что это сулит и чем может быть полезно, но сперва показалось неудобным: ты машина — ты и следи/добавляй, раз всё известно, зачем мне лишняя рутина? Позже у коммита обнаружился ключ -a вдобавок к "git add .", так что по этому поводу ворчать стало некуда. Ну а потом всё-таки понравился этот мелочный контроль (можно некоторые файлы держать у себя изменёнными для экспериментов и не пускать их в репозиторий).

Что нравится и заметно в первую очередь в Git после SVN, так это:

  1. деление на оффлайн и онлайн работу: здесь можно комфортно коммитить сколько угодно локально, затем слить всё пачкой из дома или из офиса. Мне как любителю работать с ноутбуком и нелюбителю зря переплачивать за мобильный трафик это очень удобно (хотя основная причина скорее быстрая разрядка батарей телефона при постоянном или частом использовании GPRS/EDGE/3G). См. также пункт 5 этого списка;
  2. служебных папок нет по всему дереву каталогов (как в SVN), есть только одна в корне проекта (репозитория). При работе с SVN приходилось перед массовой заливкой на сервер не забывать очищать дерево каталогов от этих .svn -- и места занимают прилично, да и не дело их на сервере держать. А если и держать, то надо не забывать блокировать через .htaccess. См. также пункт 5;
    • в SVN локальная рабочая копия очень жёстко привязана к абсолютному пути на диске, поэтому любая попытка скопировать или перенести папки внутри копии обычными средствами, а не командами SVN, гарантируют массу приключений и неприятных открытий;
  3. branch и merge устроены гораздо проще и логичней -- пользоваться ими хочется сразу же. В SVN они вроде бы тоже есть, но столько лишнего надо в голове держать, что писать километровые пути просто сил никаких нет. В итоге при работе в командах до десятка человек мы работали только с транком и пользовались буквально 3 командами: checkout первый раз и update/commit в процессе. Хватало. Но git branch -- это очень даже. Концепция и сценарии работы очень понравились, они гораздо удобней и при самостоятельной работе, и для 2 человек. Не говоря уже про более обширные команды;
  4. простота и удобство создания репозиториев: буквально зашёл в папку с проектом, git init && git add . && git commit -m 'initial commit' -- и вот у нас уже новый репозиторий. Не связанный пока никак с онлайн, но зато уже сразу можно работать с кодом, удобно следить за изменениями и пользоваться ветками. Процесс заведения репозиториев в SVN у меня был гораздо дольше, ну и онлайн в случае SVN обязателен, что для меня далеко не всегда плюс: иногда это здорово мешает. Так что я очень доволен этой разницей.
  5. служебные файлы SVN занимают столько же места, сколько и отслеживаемые, удваивая таким образом объём папки проекта (было 100 мегабайт - стало 200). Что с ветками - не знаю, не пользовался ими под SVN. Репозиторий Git'а раза в 2 с хвостиком МЕНЬШЕ отслеживаемого проекта. Из очень приятного: этот репозиторий (папку .git в корне проекта) очень легко сбекапить или перенести на другой компьютер, где его можно восстановить даже в глухой деревне без онлайна (git checkout master -- и из пустоты рядом с .git мгновенно возникает весь проект). См. также пункты 1 и 2.

Есть и существенный недостаток. При использовании Git каждый человек держит у себя всю копию репозитория. У нас при работе в Proxistep была актуальна проблема больших графических файлов. С ними в команде работал обычно один, от силы два человека: художник-дизайнер и верстальщик. И верстальщику в итоге нужна, условно говоря, всего одна картинка после всех согласований с клиентом. А дизайнеру совершенно не нужен код. А остальным разработчикам (3-4 человека) -- дизайн. Наблюдать и выкачивать все эти изменения и версии дизайна было неудобно и тяжело. Это тормозило и было большинству команды совершенно неинтересно, отнимало время, а в некоторых проектах - весьма заметное место на диске. К тому же обычным состоянием разработчика являлось участие в нескольких текущих проектах и часть недавних проектов тоже лежала в состоянии быстрой готовности.

Сейчас, конечно, полегче - скорости доступа к интернет заметно выросли и объёмы HDD растут год от года. Но иногда это может стать проблемой.

SVN в принципе способен частично решить эту проблему: в нём, в отличие от Git, можно делать checkout-ы и commit-ы отдельных папок репозитория, поэтому на первый взгляд достаточно всего лишь разумно разделить по каталогам работу. Проблема лишь в том, что некоторым ролям нужны разные наборы каталогов, и если кто-то оказался "на стыке", сделать одним коммитом изменения в 2 разных каталогах уже не получится: придётся видеть в общем репозитории 2 коммита вместо одного. Проблема на самом деле мелкая и скорее эстетическая, но на всякий случай стоит иметь в виду.

Для повседневной работы мне хватает с головой командной строки git'а и Meld для сравнения, но на всякий случай скажу про знакомые мне GUI инструменты для работы с Git:

  • RabbitVCS поддерживает и SVN, и Git;
  • Giggle гораздо приятнее глазу, чем gitk.

Полезные материалы для начинающих знакомство с Git:

  • Git - SVN Crash Course — для тех, кто мигрирует с SVN на Git; таблицы соответствия для быстрого старта и укладывания в голове аналогий
  • http://progit.org/book/ru/ — хороший перевод об устройстве и логике работы Git; кратко, последовательно и понятно, без "воды" и лирических отступлений
  • http://habrahabr.ru/blogs/development/68341/ — одна из самых понятных и кратких статей о правильной организации коллективной работы над проектами с использованием Git
  • http://help.github.com/git-cheat-sheets/
  • Git-SVN Comparison

И не забудьте про git-svn! Этот пакет даёт нам в распоряжение такую замечательную команду, как git svn clone <SVN_repo_URL> /path/to/git/repo, которая позволит создать Git-репозиторий из SVN-репо, сохранив всю историю коммитов. Безусловно полезная при миграции вещь.

Знакомлюсь с Git и Yii Framework

- Posted in Uncategorized by

Пригласили в проект, где используется Git VCS и Yii Framework. Поэтому на некоторое время объявляю себя потерянным для мира, но открытым для полезной информации по этим темам. Советуйте, если есть что-то полезное на ваш взгляд.

Yii обещает прям какую-то гигантскую производительность. Пока ничего такого не заметил: имеющаяся минимальная заготовка проекта на Yii на глаз шевелится медленней моих CodeIgniter проектов, местами более нагруженных неоптимальными запросами (о которых я знаю и в планах стоит поправить логику в нескольких местах, но и без этого работает замечательно. Даже у хостера, который пока отказывается включть mod_deflate у апача).

См. также: Первые впечатления о Git в сравнении с SVN.

PayPal?! Но у меня нет PayPal! Как заплатить?!

- Posted in Uncategorized by

Через Paypal можно заплатить человеку, даже если у вас самих нет счёта в PayPal. Достаточно всего лишь пластиковой карты, которой можно расплачиваться в интернет. А это есть наверное у каждого в наше время.

К сожалению, платёжный интерфейс пайпэла не очень-то очевиден и удобен, поэтому немного опишу процесс. Кнопку Donate можно использовать для перечисления любой удобной вам суммы. Если нажать на неё, удерживая при этом на клавиатуре Shift, то окно Paypal откроется в новом окне: возможно, так вам будет удобнее изучить, что к чему.

  1. Первую же страницу, которая появляется после нажатия кнопки Donate, надо прокрутить вверх: в расположенной там форме надо просто ввести сумму (Donation Amount), которую хотите перевести человеку (что за человек - более-менее понятно по его ID в левом верхнем углу: в моём случае это адрес почты на GMail) и нажать кнопку Update Totals.
  2. Теперь в левой части страницы более чётко видны эмблемы популярных платёжных карт. Нам именно туда и надо: рядом с ними находится надпись на английском, перевод которой звучит так: "У Вас нет счёта в PayPal? Используйте кредитную/дебетовую карту или банковский перевод (там, где он доступен)". Дело в том, что для разных стран доступны разные варианты осуществления платежа, но пластиковая карта как наиболее распространённый способ обычно доступен повсеместно. Так вот, самая главная ссылка расположена здесь и является наверное самой незаметной на странице: нажимаем на Continue.
  3. Попадаем на более знакомо выглядящую страницу. Осталось выбрать вашу страну...
  4. И вот наконец-то можно выбрать более удобный язык интерфейса (в правом верхнем углу)
  5. Ну а дальше всё более-менее очевидно и понятно

Думаю, дальше пояснения не требуются. Если вы просто экспериментировали и всё стало понятно, можно закрыть страшную страницу. Или перечислить мне мешок денег :)

Opencart 1.5.x: индикация текущей категории в главном меню

- Posted in Uncategorized by

Добавляем визуальную индикацию текущей выбранной категории в главном меню магазина. Учитывается только первая указанная в пути категория, возможные альтернативы не обрабатываются.

Модифицируемые файлы:

catalog/controller/common/header.php
catalog/view/theme/default/stylesheet/stylesheet.css
catalog/view/theme/default/template/common/header.tpl

В файле catalog/controller/common/header.php ищем

// Level 1
                $this->data['categories'][] = array(
                    'name'     => $category['name'],
                    'children' => $children_data,
                    'column'   => $category['column'] ? $category['column'] : 1,
                    'href'     => $this->url->link('product/category', 'path=' . $category['category_id'])
                );
            }
        }

изменяем:

// Level 1
                $this->data['categories'][] = array(
                    'name'     => $category['name'],
                    'children' => $children_data,
                    'column'   => $category['column'] ? $category['column'] : 1,
                    'href'     => $this->url->link('product/category', 'path=' . $category['category_id']),
                    'category_id' => $category['category_id']    // http://rb.labtodo.com/opencart-15x-show-current-category-in-menu
                );
            }
        }
        // http://rb.labtodo.com/opencart-15x-show-current-category-in-menu :: BEGIN
        if (isset($this->request->get['path'])) {
            $parts = explode('_', (string)$this->request->get['path']);
        } else {
            $parts = array();
        }
        if (isset($parts[0])) {
            $this->data['category_id'] = $parts[0];
        } else {
            $this->data['category_id'] = 0;
        }
        // http://rb.labtodo.com/opencart-15x-show-current-category-in-menu :: END

В файле catalog/view/theme/default/stylesheet/stylesheet.css ищем

#menu > ul > li:hover {
    background: #000;
}

добавляем:

#menu > ul > li:hover {
    background: #000;
}
#menu > ul > li > a.active {
    background: #000;
}

В файле catalog/view/theme/default/template/common/header.tpl ищем

<div id="menu">
  <ul>
    <?php foreach ($categories as $category) { ?>
    <li><a href="<?php echo $category['href']; ?>"><?php echo $category['name']; ?></a>

изменяем:

<div id="menu">
  <ul>
    <?php foreach ($categories as $category) { ?>
    <li><?php if ($category['category_id'] == $category_id) { ?>
    <a href="<?php echo $category['href']; ?>" class="active"><?php echo $category['name']; ?></a>
    <?php } else { ?>
    <a href="<?php echo $category['href']; ?>"><?php echo $category['name']; ?></a>
    <?php } ?>

Изменяем длину описаний товаров в каталоге OpenCart 1.5.x

- Posted in Uncategorized by
Это решение также убирает "черные ромбики" и иероглифы в конце русских описаний товаров в каталоге. В других местах делается аналогично.

Вероятно, вас тоже удивляет, почему описания товаров, которые выводятся при просмотре каталога магазина, так бездарно и коротко обрезаются. Ведь даже в стандартном дизайне OpenCart v1.5 там могло бы поместиться ещё 2-3 таких же строки с весьма полезной информацией как для покупателя, так и для поисковых систем. Впрочем, и для внешнего вида стандартного шаблона польза немалая.

В основном, конечно, выигрывает посетитель сайта: уменьшается необходимость открывать каждый товар, а у владельцев магазина появляется возможность вынести в начало описания самую важную информацию и дать покупателю удобный способ просмотра основных характеристик похожих товаров уже на этапе ознакомления с ассортиментом, при навигации.

Вот как выглядит страница каталога с описаниями товаров до и после предлагаемой модификации:

<

p>Чтобы увеличить длину описаний товаров на страницах каталога, необходимо:

  1. Открыть файл catalog/controller/product/category.php
  2. найти поиском строку "substr" (она там одна, OpenCart v1.5.0.5 — 1.5.1.1). Вы увидите небольшой блок из десятка строчек, который начинается с $this->data['products'][] = array(. Перед этой строчкой вставляем 6 строк, приведённых ниже, сразу после этого списка;
  3. Найденную строку 'description' => ... заменяем на 'description' => $descr_plaintext,

В результате должно получиться:

$cut_descr_symbols = 400;
$descr_plaintext = strip_tags(html_entity_decode($result['description'],
    ENT_QUOTES, 'UTF-8'));
if( mb_strlen($descr_plaintext, 'UTF-8') > $cut_descr_symbols )
{
    $descr_plaintext = mb_substr($descr_plaintext, 0,
        $cut_descr_symbols, 'UTF-8') . '&nbsp;&hellip;';
}
$this->data['products'][] = array(
    'product_id'  => $result['product_id'],
    'thumb'       => $image,
    'name'        => $result['name'],
    'description' => $descr_plaintext,
    'price'       => $price,
    'special'     => $special,
    'tax'         => $tax,
    'rating'      => $result['rating'],
    'reviews'     => sprintf($this->language->get('text_reviews'),
        (int)$result['reviews']),
    'href'        => $this->url->link('product/product', 'path='
        . $this->request->get['path']
        . '&product_id=' . $result['product_id'])
);

Длина описания регулируется переменной $cut_descr_symbols. В зависимости от дизайна вам может потребоваться изменить эту величину.

Что исправлено:

  • До модификации строки укорачивались до 100 символов. Описания длиной 300-500 символов выглядят гораздо лучше;
  • символы ".." добавлялись даже к полностью поместившимся описаниям: сейчас символ заменён на HTML-ное троеточие и добавляется только к обрезанным описаниям;
  • ну и mbstring, разумеется. Про которую англоязычные авторы часто забывают.

Аналогичным образом стоит сразу же изменить файлы:

  • catalog/controller/product/search.php
  • catalog/controller/product/special.php
  • catalog/controller/product/compare.php
  • catalog/controller/product/manufacturer.php

То есть найти похожие места (например, по строке "substr") и внести аналогичные изменения.

То же самое для VQmod / Opencart 1.5.1.3.1

<

dl>

stalker780:

Мож кому пригодится vqmod для 1.5.1.3.1 http://shop.clicker.com.ua/download/vqmod_product_long_description_1.0_oc_1.5.1.3.1.zip (локальная копия файла)

Как сделать, чтобы слова не обрезались посередине

А как сделать чтобы слова не обрезались, а последнее слово дописывалось полностью?

Можно строку внутри { ... } заменить на две:

$descr_plaintext = mb_substr($descr_plaintext, 0, $cut_descr_symbols, 'UTF-8');
$descr_plaintext = mb_substr($descr_plaintext, 0, mb_strripos($descr_plaintext, ' ', 0, 'UTF-8'), 'UTF-8') . '&nbsp;&hellip;';

Не отображаются кириллические (русские) буквы в "Производители"

В версиях 1.5.х до 1.5.1.2 надо в файле catalog/controller/product/manufacturer.php найти строку

$key = '0 - 9';

и её окрестности (должно быть примерно в районе 35-40 строк). И изменить там 2 строки, чтобы результат был такой:

foreach ($results as $result) {
    if (is_int(mb_substr($result['name'], 0, 1, 'UTF-8'))) {
        $key = '0 - 9';
    } else {
        $key = mb_substr(mb_strtoupper($result['name'], 'UTF-8'), 0, 1, 'UTF-8');
    }

Типичные проблемы, которые решаются этими правками

[...] когда я делаю длинные описания товара (превью описания в разделе) то в конце иногда появляются непонятные символы-иероглифы. [...]

Как можно исправить проблему с ромбиками/иероглифами/вопросиками проще, не меняя многие файлы?

Попробуйте в файл .htaccess в корне сайта вставить строки:

# php_value mbstring.internal_encoding UTF-8
# php_value default_charset UTF-8
# php_value mbstring.http_output UTF-8
# php_value mbstring.encoding_translation On
# php_value mbstring.detect_order UTF-8
# php_value mbstring.func_overload 6

Они закомментированы (символ "#" в начале строки). Раскомментируйте их все (в идеале). Если возникнет ошибка - закомментируйте и пробуйте включать по одной строке. Ту, на которой возникает ошибка, оставьте закомментированной. Возможно, это поможет решить проблему проще и быстрее.

Просто не у всех хостеров это работает. Либо спросите в техподдержке, как этот код в .htaccess правильно указать и возможно ли это. У некоторых вместо php_value применяется php_admin_value, у кого-то запрещено совсем. А у кого-то это делается через php.ini или может через панель управления.

Bash and Tar: скрипт инкрементального бекапа с помощь after-date

- Posted in Webdev by

Подробная статья о том, как весь этот процесс обустраивается: http://ilab.me/howto/bash-tar-vps-backup/

Я обычно делал гораздо проще (бекап по списку, который берётся из файла), без инкрементальных заморочек: http://baadoo.livejournal.com/25398.html.

Но только потому, что архивы были невелики и усложнять себе жизнь не было мотивов.

Джон Бартон прекращает работу над Firebug

- Posted in Uncategorized by

Ну вот, приплыли. А я держу Firefox считай только из-за него.

http://web-standards.ru/news/384/ Джон Бартон (John J Barton), один из главных разработчиков Firebug, прекращает работу над популярным отладчиком и уходит из IBM в Google для работы вместе с командой Chrome «над новым поколением инструментов для разработки». В своём прощальном письме Джон говорит, что не видит смысла конкурировать с отладчиками, встроенными в браузеры.

Мне Firefox нужен из-за HTML Validator, Firebug и немного YSlow for Firebug. А основным броузером уже лет 10 является Opera. Медленным Firefox'ом после Оперы пользоваться очень трудно, а для исправления интерфейса (чтобы сделать работу удобней и на ноутбуке, и на десктопе) надо ещё найти и поставить неизвестное количество плагинов, которые его явно не ускоряют.

P.S. Тем не менее, http://www.getfirebug.com/. Пока жив.

Русский перевод Opencart v1.5.0.5, v1.5.1, 1.5.2, 1.5.3, 1.5.4/1.5.5, 1.5.6. Информация о других переводах (русский, украинский языки)

- Posted in Uncategorized by

Перевёл пользовательскую часть OpenCart v1.5.0.5. Админку не трогал: на её перевод сейчас нет времени. Возможные проблемы и неточности:

  • Wishlist перевёл как закладки, ничего более подходящего и короткого не придумывается, а по смыслу подходит прекрасно;
  • в районе налогов скорей всего ошибка: в оригинале было "Ex Tax". Я не знаю, что это такое, хотя может оказаться, что "excluding tax", т.е. без НДС или что-то в этом роде. В общем, в этих местах я писал "Без НДС" "НДС", и скорей всего это неправильно. Мы в своём магазине ещё будем это смотреть и проверять с партнёрами, а я пока предупреждаю, что здесь может быть засада.
  • Мне не понравилось стандартное оформление сообщений пары способов доставки (2 практически одинаковые строчки) при оформлении заказа, поэтому принял несколько спорное решение: включил цену доставки прямо в сообщение. То есть цена указана прямо в языковых файлах и изменить её из админки не удастся. Получилось замечательно, но имейте этот момент в виду. В языковых файлах всего в паре мест должна встречаться строчка "грн". Поэтому достаточно найти и убрать упоминания "грн" и конкретной стоимости в 4 файлах:
    • catalog/language/russian/shipping/free.php
    • catalog/language/russian/shipping/flat.php
    • admin/language/russian/shipping/free.php
    • admin/language/russian/shipping/flat.php

Базовым вариантом послужил русский перевод пользователя alsrmurad от 22 июня 2011 года. Но многие строки там были предельно кошмарны и с ошибками: то ли машинный перевод, то ли переводчик не русский, а просто знает язык недостаточно хорошо. Или банально спешил выложить. Во всяком случае я по-любому благодарен автору за частично сделанный перевод: это сэкономило немало времени и избавило от приличной части рутинных операций.

UPD 2011-07-21: небольшие обновления и исправления ошибок. Узнал также о существовании ещё одного перевода Opencart, есть SVN. См. также ветку форума opencartforum.ru.

UPD 2011-07-25: Dmitry добавил свой перевод административной части Opencart 1.5.0-1.5.1. Правда, скачать без регистрации невозможно.

UPD 2011-07-29: Очередное обновление замеченных ошибок, неточностей и корявостей. На этом я прощаюсь с версией 1.5.0.5 и мигрирую на 1.5.1.1 - они очень похожи, но мелкие отличия в языковых файлах есть. Собираюсь держать свою версию перевода 1.5.1 здесь же рядом с 1.5.0.5, и может быть на этот раз будет время заняться административной частью. Я присоединился к команде переводчиков ocStore (русской сборки OpenCart) на assembla.com (см. выше про SVN), поэтому частично мои правки попадают туда (коммичу не все свои варианты, и местами там не все варианты нравятся), частично многое я возьму оттуда. Но так как там обновляется только текущая версия перевода, а ветки по конкретным версиям не хранятся, я решил продолжать держать у себя те версии, с которыми работаю, в виде отдельных архивов.

UPD 2011-09-16: Исправлен ошибочный перевод в админке, также ранее вносились мелкие изменения.

UPD: Украинским переводом занимаются парни с opencart-ua.org

Скачать

Как можно помочь проекту:

Скачать также можно на официальном сайте opencart.com в расширениях.

Сообщайте о любых неточностях или ошибках! Предложения более удачных вариантов перевода также приветствуются.

Пользователи Github могут помочь улучшению перевода:

  • сообщайте об ошибках и предлагайте варианты в Issues - этот вариант не требует никаких специальных знаний;
  • для небольших исправлений можете найти необходимый файл и отредактировать его или оставить комментарий к конкретным строкам;
  • пользователи, знакомые с работой Git, могут пользоваться пулл-реквестами для предложения своих исправлений и дополнений.

Установка

  1. Скопируйте содержимое архива (папки admin и catalog) в каталог интернет-магазина.
  2. Зайдите в админку и добавьте ещё один язык, русский: System / Localisation / Languages / Insert. Все поля появившейся формы должны быть пустые. Заполните, как показано ниже:
    Language Name: Русский
    Code: ru
    Locale: ru,ru_RU,ru_RU.UTF-8
    Image: ru.png
    Directory: russian
    Filename: russian
    Status: Enabled
    Sort Order: 1

    и нажмите кнопку Save

  3. Проверьте работоспособность добавленного языка: перейдите в магазин и переключитесь там на русский язык. Есил всё отображается без сообщений без ошибок и по-русски, можете переходить к следующему пункту. Если возникают какие-либо ошибки - не переключайте язык админки на русский ни в коем случае, пока не разберётесь с причиной и не исправите ошибки.
  4. перейдите на вкладку System / Settings / [Edit] / Local и выберите в выпадающих списках Language и Administration Language параметр "Русский" (чтобы созданный язык использовался в интерфейсе магазина и в админ-части)

Если у вас проблема с установкой, и не можете справиться

Если не можете найти ошибку и есть возможность - дайте мне доступ к FTP и админке Опенкарта, я проверю. Приватное сообщение мне можно отправить со страницы Контакт.

Если разбираетесь, что к чему у хостера -- лучше сделать для меня временную учетную запись для доступа к FTP/базе (и в Opencart тоже можно заодно отдельный логин в группе администраторов), чтобы не раздавать свой основной логин/пароль. А после того, как всё закончится - вы просто удаляете этот временный доступ.

Желательно, по возможности, чтобы в имени пользователя и пароле к FTP не использовались символы `@` и `:` - они сильно замедляют мне работу, приходится пользоваться неудобными инструментами.

Facebook Like Button плагин для MaxSite CMS

- Posted in Uncategorized by

Написал плагин для MaxSite CMS, который добавляет кнопку Facebook Like внизу страниц.

Настройки в админке ещё не все внёс. Пока хочу найти причину, почему кнопка Like работает не на всех страницах?!?! (Update: уже работает, видимо это были временные проблемы Facebook.) Может дело в самом блоге, которому всего пара дней от роду? Но раз некоторые страницы добавляются, то дело не в коде кнопки и не в плагине, поэтому выкладываю AS IS.

Странно, что его нет в стандартном комплекте, ведь на http://maxsite.org/ эти кнопки присутствуют. Ну и гуглом, разумеется, ничего не нашёл.

Скачать Facebook Like Button плагин для MaxSite CMS 8

Улучшение поиска в OpenCart 1.5: поиск по описанию, подкатегориям, по модели и SKU

- Posted in Uncategorized by

По умолчанию поиск в OpenCart производится только по названиям продуктов. Что довольно неудобно для покупателя и не оправдано, если продуктов на сайте не так уж и много (думаю, на нескольких сотнях наименований никакой разницы не будет заметно).

Поиск по описаниям и подкатегориям

На странице расширенного поиска есть возможность включить поиск по описаниям товаров и во всех категориях, но по умолчанию эти опции отключены и чаще всего пользователь видит отсутствие результатов.

Для того, чтобы исправить поведение поиска по умолчанию в версиях OpenCart от v1.5.0.5 до 1.5.2 (а может и в более поздних, но на данный момент я знаю только про эти), надо отредактировать файл catalog/view/javascript/common.js:

строки #9 и #22 изменить с

url += '&filter_name=' + encodeURIComponent(filter_name);

на

url += '&filter_name=' + encodeURIComponent(filter_name) +
'&filter_sub_category=true&filter_description=true';

Поиск по модели и SKU (артикулу) - вариант для версий 1.5.0.5 - 1.5.1.2 и может более ранних 1.5.x

Чтобы добавить поиск по модели и SKU (артикулу), надо модифицировать ещё один файл:

commit 56a765ccb474075b7073f11474100e2251e0d762
Author: Ruslan Brest <rb@labtodo.com>
Date:   Mon Sep 5 11:55:51 2011 +0300
    [+] catalog: search: добавлен поиск по модели и SKU
diff --git a/public_html/catalog/model/catalog/product.php b/public_html/catalog/model/catalog/product.php
index 520bc34..e9a3d73 100644
--- a/public_html/catalog/model/catalog/product.php
+++ b/public_html/catalog/model/catalog/product.php
@@ -106,10 +106,12 @@ class ModelCatalogProduct extends Model {
            . (int)$this->config->get('config_store_id') . "'";
 
            if (isset($data['filter_name']) && $data['filter_name']) {
+               $sFilterName = $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8'));
+               $sql_search_model_sku = 'LCASE(p.model) LIKE "%' . $sFilterName . '%" OR LCASE(p.sku) LIKE "%' . $sFilterName . '%"';
                if (isset($data['filter_description']) && $data['filter_description']) {
-                   $sql .= " AND (LCASE(pd.name) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%' OR p.product_id IN (SELECT pt.product_id FROM " . DB_PREFIX . "product_tag pt WHERE pt.language_id = '" . (int)$this->config->get('config_language_id') . "' AND LCASE(pt.tag) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%') OR LCASE(pd.description) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%')";
+                   $sql .= " AND (".$sql_search_model_sku." OR LCASE(pd.name) LIKE '%" . $sFilterName . "%' OR p.product_id IN (SELECT pt.product_id FROM " . DB_PREFIX . "product_tag pt WHERE pt.language_id = '" . (int)$this->config->get('config_language_id') . "' AND LCASE(pt.tag) LIKE '%" . $sFilterName . "%') OR LCASE(pd.description) LIKE '%" . $sFilterName . "%')";
                } else {
-                   $sql .= " AND (LCASE(pd.name) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%' OR p.product_id IN (SELECT pt.product_id FROM " . DB_PREFIX . "product_tag pt WHERE pt.language_id = '" . (int)$this->config->get('config_language_id') . "' AND LCASE(pt.tag) LIKE '%" . $this->db->escape(mb_strtolower($data['filter_name'], 'UTF-8')) . "%'))";
+                   $sql .= " AND (".$sql_search_model_sku." OR LCASE(pd.name) LIKE '%" . $sFilterName . "%' OR p.product_id IN (SELECT pt.product_id FROM " . DB_PREFIX . "product_tag pt WHERE pt.language_id = '" . (int)$this->config->get('config_language_id') . "' AND LCASE(pt.tag) LIKE '%" . $sFilterName . "%'))";
                }
            }

Поиск по модели и SKU (артикулу) - вариант для версии 1.5.1.3 (и наверное выше)

commit 77611b6dcad3fb21484c889bb7726688cb68de34
Author: Ruslan Brest <rb@labtodo.com>
Date:   Mon Oct 10 11:58:24 2011 +0300
    [+] catalog: search by model and SKU
diff --git a/upload/catalog/model/catalog/product.php b/upload/catalog/model/catalog/product.php
index 25a7861..a9ed128 100644
--- a/upload/catalog/model/catalog/product.php
+++ b/upload/catalog/model/catalog/product.php
@@ -54,13 +54,19 @@ class ModelCatalogProduct extends Model {
                    $implode = array();
                    
                    $words = explode(' ', $data['filter_name']);
                    foreach ($words as $word) {
-                       if (!empty($data['filter_description'])) {
-                           $implode[] = "LCASE(pd.name) LIKE '%" . $this->db->escape(utf8_strtolower($word)) . "%' OR LCASE(pd.description) LIKE '%" . $this->db->escape(utf8_strtolower($word)) . "%'";
-                       } else {
-                           $implode[] = "LCASE(pd.name) LIKE '%" . $this->db->escape(utf8_strtolower($word)) . "%'";
-                       }               
+                       if( !empty($word) )
+                       {
+                           $lword = $this->db->escape(utf8_strtolower(trim($word)));
+                           // Add search by model and SKU
+                           $implode[] = 'LCASE(p.model) LIKE "%' . $lword . '%" OR LCASE(p.sku) LIKE "%' . $lword . '%"';
+                           if (!empty($data['filter_description'])) {
+                               $implode[] = "LCASE(pd.name) LIKE '%" . $lword . "%' OR LCASE(pd.description) LIKE '%" . $lword . "%'";
+                           } else {
+                               $implode[] = "LCASE(pd.name) LIKE '%" . $lword . "%'";
+                           }
+                       }
                    }
                    
                    if ($implode) {

А как же админка? Там тоже SKU не хватает!

Можете также добавить возможность поиска по SKU в админ-зоне магазина.

Opera Dragonfly не работает без интернета? Исправляем

- Posted in Uncategorized by

Opera Dragonfly (инструмент для веб-разработчиков) в какой-то момент стала веб-приложением и без наличия интернета нельзя воспользоваться удобным инструментарием. Что весьма неудобно, если вы любитель отлипнуть от розетки и кодить где-то, сидя с ноутбуком.

Ситуацию, однако, можно исправить. Для этого надо установить Dragonfly локально, скачав архив с http://dragonfly.opera.com/app/zips/ и указать местоположение файла client-en.xml в настройках Developer Tools.

По умолчанию там адрес https://dragonfly.opera.com/app/

См. также:

Opera: добавляем автоскролл и быстрое скрытие/показ адресной строки

- Posted in Uncategorized by

Что это даёт?

"Как бы автоскролл" лично мне часто гораздо удобнее построчной прокрутки стрелками курсора, не говоря уже о постраничном (пробелом или PgUp/PgDn) при чтении больших текстов. Я жму Alt-Down и текст аккуратно едет, подставляя глазам новые порции без резких скачков. То же самое можно делать и мышью (средней кнопкой, так называемый Panning; под линуксами где-то была настройка, подо что отдать среднюю кнопку - под этот pan&scroll или привычную линуксоидам вставку выделенного текста).

Но есть такие штуки, как ноутбуки, пуза-колени, кресла, шезлонги. Далеко не всегда мышь подключена или ей удобно пользоваться, а вот клавиатура есть и обычно этот способ мне гораздо удобней. А без него начинаются страдания (на всяких странных популярных броузерах, если надо ими пользоваться вместо Оперы).

Ctrl-F8 (скрытие/показ адресной строки) -- способ освободить приличный кусок экрана на ноутбуках и нетбуках, где вертикальное пространство на вес золота. Или быстро восстановить панель адреса во всяких попап-окошках с картинкой, где от тебя скрывают адрес страницы или изображения, а его вопреки желанию автора хочется посмотреть или скопировать.

Покажу инструкцию на пальцах, чтобы меньше писать. Нам сюда:

Ctrl-F12, Advanced , Shortcuts, Keyboard setup

Затем редактируем текущий клавиатурный профиль, открываем в нём секцию "Browser Window"

и добавлем пару новых строчек:

Down AltScroll, 3
F8 ctrlSet alignment, "document toolbar", 6 | Set alignment, "document toolbar", 0

Предварительно, конечно же, лучше проверить, не используются ли уже эти сочетания -- поле быстрого поиска там вверху виднеется, вот им и пользуемся, попробовав ввести туда, например, "down" или "f8".

Opera: используем свои шрифты на неудобных сайтах

- Posted in Uncategorized by

Вы не поверите, но есть ещё сайты, которые верят в мифы, придуманные полиграфистами, о том, что шрифты с засечками (serif) удобны и легко читаются даже с экранов. Мои глаза верят в обратное: для экранов -- исключительно sans-шрифты. Тот же Ubuntu Font Family, на который я сейчас плотно подсел (а заодно и подсадил некоторых друзей).

<

p>Вот пример:

У меня от оригинала, да на маленьком экране, ощущение одно: серые расплывающиеся мелкие буквы, которые тяжело читать и сквозь них надо пробираться. Ни о каком порхании и лёгком восприятии речь не идёт. В основных статьях там чуть получше (Times New Roman побольше размером и выглядит более чёрным, но читать мне его всё равно очень неприятно: глаза постоянно цепляются и спотыкаются о буквы, а не о смысл).

<

p>Итого: выводы каждый может сделать для себя сам, вкусы, DPI и мониторы на столах и в карманах у всех разные, но решение для броузера Opera простое:

  • создаём CSS файл, внутри которого может быть всего одна строчка: body, .content, p, a { font-family: Ubuntu, Sans !important; font-size: 100% !important }
  • на сайте, где за буквами не видно информации -- F12, Edit Site Preferences, Display, My style sheet. И выбираем там наш файл
  • всё

Если не помогает, значит там со стилями дизайнеры уж очень расстарались и надо смотреть внимательней. Но в большинстве случаев должен помочь и этот простой CSS. Разумеется, любители Times New Roman могут таким же образом сменить неудобные им санс-серифы на шрифты с засечками.

Page 1 of 2