Блог Загирова Рустама

Около-интернетные заметки

Yii: CGridView и запрос с Group by и Having By

| Комментарии

Задача такая: вывести данные через компонент CGridView в YII, сгруппированные по определённому полю, а также применение условий по агрегированным данным.

Возьмём простую таблицу:

1
date | clicks

Нам нужно получить сгруппированные данные по каждому часу.

По сути нужно применить следующий запрос

1
2
3
SELECT *
FROM table
GROUP BY LEFT(date, 13);

Возникают следующие трудности в реализации через Yii:

  • Yii не может посчитать общее количество, а соответственно будет неправильно создавать пагинатор. Yii просто обнуляет поля group by и having при подсчёте общего количества. Да ещё count(*) тут не сработает, нужно просто. посчитать количество строк, которое вернул запрос с группировкой
  • Также нам не подходит использование where, чтобы искать по группированным данным. Тут нужно использовать having by.
  • Сортировка. Yii автоматически подставляет поле без агрегирующей функции. А если использовать отношения, то ещё и подставит алиас главной таблицы (t.clicks).

Соответственно, мы руками высчитываем общее количество и устанавливаем найденное значение в атрибут totalItemCount компонента CActiveDataProvider

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public function search()
{
  $criteria=new CDbCriteria;

  $criteria->compare('date',$this->date,true);

  if ($this->clicks)
  {
    // Скопипастил из исходников yii
    if(preg_match('/^(?:\s*(<>|<=|>=|<|>|=))(.*)$/', $this->clicks, $matches))
    {
      $clicks=$matches[2];
      $op=$matches[1];
    }
    else
    {
      $clicks = $this->clicks;
      $op='=';
    }
    // Проставляем having by
    $criteria->having = 'SUM(clicks) ' . $op . ' ' . Yii::app()->db->quoteValue($clicks);
  }
  // Выбираем дату без минут и секунд И сумму кликов за определённый час
  $criteria->select = 'LEFT(date, 13) AS date, SUM(clicks) AS clicks';
  // Применяем группировку по часам
  $criteria->group = 'LEFT(date, 13)';

  // Клонируем объект критерии, чтобы посчитать общее количество записей
  $countCriteria = clone $criteria;
  $countCriteria->select = '1';
  $sum = count(static::model()->findAll($countCriteria));


  $pages=new CPagination($sum);
  $pages->pageSize=50;
  $pages->applyLimit($criteria);

  return new CActiveDataProvider(get_class($this), array(
    'totalItemCount' => $sum,
    'criteria'=>$criteria,
    'pagination'=>$pages,
    'sort' => array(
      'attributes' => array(
        // Тут устанавливаем свою сортировку по нужному полю
        'clicks' => array(
          'asc' => 'SUM(clicks) ASC',
          'desc' => 'SUM(clicks) DESC',
        )
      ),
      'defaultOrder' => 'date DESC',
    ),
   ));
}

Настройка Exim на почту для доменов от Яндекса (pdd.yandex.ru)

| Комментарии

Настраиваем отправку почты с нашего сервера через почту для доменов от Яндекса.

Для примера, используем домен zagirov.name.

Запускаем конфигурирование exim’а:

1
dpkg-reconfigure exim4-config

Отвечаем на вопросы:

  • mail sent by SMARTHOST; received via SMTP or fetchmail
  • Type System Mail Name: пусто
  • Type IP Adresses to listen on for incoming SMTP connections: 127.0.0.1 ; ::1
  • Other destinations for which mail: пусто
  • Machines to relay mail for: пусто
  • Type Machine handling outgoing mail for this host (smarthost): smtp.yandex.ru:587
  • Hide local mail name in outgoing mail: Нет
  • Keep number of DNS-queries minimal (Dial-on-Demand): Нет
  • Delivery method for local mail: mbox format in /var/mail/ /exim4/conf.d/rewrite/00_exim4-config_header
  • split configuration into small files: Да

Теперь прописываем пароль для ящика в файле /etc/exim4/passwd.client:

1
smtp.yandex.ru:no-reply@zagirov.name:СВОЙ_ПАРОЛЬ

Почтовый сервер яндекс ругается, что нужно заполненное поле FROM. Прописываем его в файле /etc/exim4/conf.d/rewrite/00_exim4-config_header:

1
begin rewrite *@* no-reply@zagirov.name Ffr

Сохраняем письма, отправленные из Php, в файлы

| Комментарии

При разработке нужно как-то складировать письма, отправляемые из php через функцию mail. Был написан такой скрипт:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env php
<?php
define('SENDMAIL_DIR', '/tmp/mail/');
if ( ! file_exists(SENDMAIL_DIR))
{
  mkdir(SENDMAIL_DIR, 0777, true);
}

function generateFileName($i = 1)
{
  $fileName = SENDMAIL_DIR . date('Y-m-d_H-i-s_') . $i . '.eml';
  return file_exists($fileName) ? generateFileName(++$i) : $fileName;
}

$mail = fopen('php://stdin', 'r') or die();
$file = fopen(generateFileName(), 'w');

while ( ! feof($mail))
{
  fwrite($file, fgets($mail));
}

fclose($mail);
fclose($file);

Даём ему право на исполнение для пользователя веб-сервера (обычно www-data).

Теперь создаём файл конфига для php в debian /etc/php5/conf.d/sendmail-local.conf со следующим содержимым:

1
2
[mail function]
sendmail_path = "/path/to/script/sendmail"

Nginx конфиги

| Комментарии

В интернете довольно много можно выложено конфигов nginx под различные веб-приложения. Но в большинстве из них используются if совместно с rewrite, что достойно всяческого порицания по словам автора nginx Игоря Сысоева. Выложу используемые мной конфиги для различных систем (redmine, chive)

Redmine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
upstream thin {
  server unix:/tmp/redmine.0.sock;
}

server {
  listen 80;
  server_name redmine.zagirov.name;
  root /usr/share/redmine/public/;
  access_log /var/log/nginx/redmine.access.log;
  error_log /var/log/nginx/redmine.error.log;

  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    include proxy.conf;
    proxy_pass http://thin;
  }

  error_page 500 502 503 504 /500.html;
  error_page 404 403 /404.html;
}

Chive – отличный аналог phpmyadmin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
location /chive/ {
  try_files $uri $uri/ /chive/index.php?$request_uri&$args;
  location ~ ^/chive/(protected|yii)/ {
    deny all;
  }

  location ~ /chive/(assets|css|images|js|themes)/ {
    expires 1y;
  }

  location = /chive/index.php {
    fastcgi_pass admin_zagirov_backend;
    include fastcgi_params;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    fastcgi_param   QUERY_STRING        $query_string&__chive_rewrite_on=1;
  }
}

Конфиг для мною обожаемого Yii:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
upstream yii-clean.zagirov.name {
  server unix:/var/run/php5-fpm/yii-clean.zagirov.name.sock;
}
server {
  listen   80;
  server_name yii-clean.zagirov.name;
  server_tokens off;
  server_name_in_redirect  off;

  access_log  /var/www/yii-clean.zagirov.name/logs/nginx.access.log;
  error_log /var/www/yii-clean.zagirov.name/logs/nginx.error.log warn;

  charset utf-8;
  client_max_body_size 10m;
  fastcgi_intercept_errors on;
  root /var/www/yii-clean.zagirov.name/www;
  index index.php index.html index.htm;

  fastcgi_read_timeout 6000;

  location ~ /(.ht|index-test.php){
    deny all;
  }

  location ~ /(.svn|.git|.svn|themes/[^/]+/views)/ {
    deny all;
  }


  location ~ /(assets|css|themes/new/css)/ {
    expires 7d;
  }

    location / {
    try_files $uri $uri/ /index.php?$request_uri&$args;
  }

  location /index.php {
    fastcgi_pass yii-clean.zagirov.name;
    include fastcgi_params;
    fastcgi_index index.php;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }
}

server {
  listen   80;
  server_name test.yii-clean.zagirov.name;
  server_tokens off;
  server_name_in_redirect  off;

  access_log  /var/www/yii-clean.zagirov.name/logs/nginx.test.access.log;
  error_log /var/www/yii-clean.zagirov.name/logs/nginx.test.error.log warn;

  gzip off;
  charset utf-8;
  client_max_body_size 10m;
  fastcgi_intercept_errors on;
  root /var/www/yii-clean.zagirov.name/www;
  index index-test.php index.html index.htm;

  fastcgi_read_timeout 6000;

  location ~ /(.ht|index.php)
  {
    deny all;
  }

  location ~ /(.svn|.git|.svn|themes/[^/]+/views)/ {
    deny all;
  }


  location ~ /(assets|css|themes/new/css)/ {
    expires 7d;
  }

  location / {
    try_files $uri $uri/ /index-test.php?$request_uri&$args;
  }

  location /index-test.php {
    fastcgi_pass yii-clean.zagirov.name;
    include fastcgi_params;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  }
}

Перенос Svn-репозитария в Git

| Комментарии

Если у вас есть репозиторий svn, но вы познали все прелести git и не хотите терять историю правок, то эта статья для вас ;)

Первым этапом будет получение всех авторов, которые коммитили в svn. Для гита нужен дополнительно email коммитера.

1
2
3
4
5
#!/usr/bin/env bash
authors=$(svn log -q | grep -e '^r' | awk 'BEGIN { FS = "|" } ; { print $2 }' | sort | uniq)
for author in ${authors}; do
  echo "${author} = ${author} <email>";
done

Сохраняем вывод в какой-нибудь файл (/home/user/svn-authors.txt) и проставляем у всех авторов email. Иногда первый коммит совершается из консоли и он получается от неизвестного пользователя, поэтому стоит добавить строку об этом неизвестном пользователе, иначе экспорт в git не сработает.

1
(no author) = Name Surname < email >

И вот собственно скрипт. Он вытягивает репозиторий из svn (используя файл соотношения авторов с email), проставляет тэги и пушит изменения на удалённый git-репозитарий. В данном случае вытягивается исходники yii и пушаться на github.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#!/usr/bin/env bash

AUTHOR_FILE="/home/user/svn-authors.txt"
SVN_PATH="http://yii.googlecode.com/svn/"
cd /path/to/git/repo

git svn clone -s --authors-file=$AUTHOR_FILE $SVN_PATH .
git merge remotes/trunk > /dev/null 2>&1

git for-each-ref refs/remotes/tags | cut -d / -f 4- |
while read ref
do
     git tag -a "$ref" "refs/remotes/tags/$ref" > /dev/null 2>&1
done
git push --all > /dev/null 2>&1
git push --tags > /dev/null 2>&1

Этот скрипт можно поставить в крон. Скрипт не будет вытягивать весь svn-репозиторий заново (если уже был раз вытянут), а только изменения.

Можно даже использовать git совместно с svn.

Используемые ссылки: http://leonid.shevtsov.me/ru/perenos-svn-repozitariya-v-git

Непрерывная интеграция: Jenkins + Yii + Git

| Комментарии

Услышав шумиху про некую систему интеграционного тестирования hudson, который переименовали в jenkins, я захотел узнать что это и как это можно использовать. Для чего собственно он нужен? В кратце: он вытягивает последнюю версию из git/svn-репозитория и выполняет определённые действия (тестирует, выкладывает на другой сервер, делает отчёты). Всё это происходит автоматически: можно задать время, когда будет выполнятся задания. Можно почитать про пример использования jenkins.

Задача такая: Выполнение unit тестов для yii с отображением покрытия кода

Все действия проводятся на сервере debian squeeze от root.

Установка jenkins

1
2
3
4
echo "deb http://pkg.jenkins-ci.org/debian binary/" > /etc/apt/sources.list.d/jenkins.list
wget -q -O - http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add -
aptitude update
aptitude install jenkins

По адресу http://serverName:8080/ должен открыться jenkins.

Установка плагинов

1
2
cd /tmp
wget http://localhost:8080/jnlpJars/jenkins-cli.jar

Если от jenkins нужно, чтобы выполнял unit-тесты и делал code coverage, то достаточно установить несколько плагинов, но на самом деле их очень много.

1
2
3
java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin xunit && \
java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin clover && \
java -jar jenkins-cli.jar -s http://localhost:8080 install-plugin git

Перезапускаем jenkins:

1
java -jar jenkins-cli.jar -s http://localhost:8080 safe-restart

Настройка проекта

Создаём проект. Открываем Новая задача → Имя: Yii. Выбираем “Создать задачу со свободной конфигурацией” → Сохранить.

Меняем файл protected/tests/phpunit.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<phpunit bootstrap="bootstrap.php"
   colors="false"
   convertErrorsToExceptions="true"
   convertNoticesToExceptions="true"
   convertWarningsToExceptions="true"
   stopOnFailure="false">

  <selenium>
    <browser name="Internet Explorer" browser="*iexplore" />
    <browser name="Firefox" browser="*firefox" />
  </selenium>

  <logging>
    <log type="coverage-html" target="../../build/coverage" title="prj"
     charset="UTF-8" yui="true" highlight="true"
     lowUpperBound="35" highLowerBound="70"/>
    <log type="coverage-clover" target="../../build/logs/clover.xml"/>
    <log type="junit" target="../../build/logs/junit.xml" logIncompleteSkipped="false"/>
  </logging>
</phpunit>

../../build – должен указывать на папку build в корневой папке репозитория, создавать её не нужно.

Теперь создаём конфиги, скачивая из https://github.com/Stamm/jenkins-yii/:

1
2
3
4
rm /var/lib/jenkins/jobs/yii/config.xml
sudo -u jenkins wget -O /var/lib/jenkins/jobs/yii/config.xml --no-check-certificate https://github.com/Stamm/jenkins-yii/raw/master/config.xml
sudo -u jenkins mkdir /var/lib/jenkins/jobs/yii/workspace
sudo -u jenkins wget -O /var/lib/jenkins/jobs/yii/workspace/build.xml --no-check-certificate https://github.com/Stamm/jenkins-yii/raw/master/build.xml

Чтобы наши конфиги подхватились идём в настройки jenkins и кликаем по ссылке “Пересчитать настройки из файла”. В настройках проекта yii указываем git репозиторий до нашего проекта. Можно даже указать локальный путь, но должны быть права для пользователя jenkins. Добавляем файл теста в protected/unit/

Теперь запускаем сборку. Вот как примерно это выглядит.

jenkis board

Уведомления по email

В конфиге jenkins указываем параметры для подключения к SMTP. Если настроен sendmail или exim, то нужно указать только от кого отсылать письмо. И теперь в настройках проекта yii указываем кому слать письмо об упавших тестах.

Авто-сборка

Можно настроить, чтобы сборки проводились автоматически. Для этого в настройках проекта yii ставим галку у Собирать периодически и в появившемся тестовом поле указываем время запуска в формате cron. Или сделать hook в git:

1
2
3
4
#!/bin/sh

echo "Running jenkins"
wget http://serverName:8080/job/yii/build > /dev/null 2>&1

P.S. Я показал только малую часть того, что может делать jenkins. Он может создавать документацию по коду, проверять на наличие дублирования в коде, проверять стиль кодирования и многое, многое другое. Примеры более сложных конфигов config.xml и build.xml

Используемые ссылки:

Правильная отдача заголовков при технических работах на сайте

| Комментарии

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

1
2
3
4
5
6
7
8
9
10
server {
    listen 80 default;
    server_name _;
    root /var/www/default/www;
    charset utf-8;
    error_page 404 403 =503 /503.html;
    location = /503.html {
        add_header Retry-After "Sun, 27 Feb 2011 23:59:59";
    }
}

Строка error_page 404 403 =503 /503.html означает, что перенаправляем все запросы с ошибками 404 и 403 на файл 503.html, попутно меняя код ответа на 503. И при запросе файла 503.html отдаём заголовок Retry-After, чтобы поисковики знали, когда можно опять запросить страницу. Вместо даты можно указать количество секунд перед следующей попыткой. В файле 503.html можно написать что-нибудь осмысленное для пользователей и даже встроить флэш-игру, чтобы не было скучно.

Ubuntu 11.04 Alpha 2. Asus EeePC 1000H. Не работает Wifi RaLink 2790 (RT2860)

| Комментарии

Нашёл как исправить баг с глючащим wifi в моём нетбуке Asus EeePC 1000H. Карточка RaLink 2790 (RT2860). Нетбук подключался к wifi, получал ip-адрес по DHCP, но ничего не работало. Потом разрывалось соединение и заново подключалось с тем же печальным эффектом. Решение подсмотрел тут. Комманда lspci -vv выдала

1
2
3
4
5
6
7
8
9
10
01:00.0 Network controller: RaLink RT2860
Subsystem: RaLink Device 2790
Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx-
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx-
Latency: 0, Cache Line Size: 32 bytes
Interrupt: pin A routed to IRQ 19
Region 0: Memory at fbef0000 (32-bit, non-prefetchable) [size=64K]
Capabilities: <access denied>
Kernel driver in use: rt2800pci
Kernel modules: rt2860sta, rt2800pci

Нужно отключить модуль rt2800pci и использовать модуль rt2860. Добавляем в конец файла /etc/modprobe.d/blacklist.conf:

1
blacklist rt2800pci

Yii: поведения (Behaviors)

| Комментарии

Поведения в yii позволяют применять некоторые методы к уже существующему объекту из другого класса. Для чего могут понадобиться поведения? Рассмотрим “жизненный” пример. Нужно получить какие-то данные по залогиненному пользователю. Можно, конечно, использовать что-то вроде:

1
User::model()->findByPk(Yii::app()->user->id);

А можно использовать поведения и добавить метод в Yii::app()–>user protected/components/WebUser.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class WebUser extends CBehavior
{
  public function getData()
  {
    if ($this->getOwner()->id)
    {
      return User::model()->findByPk($this->getOwner()->id);
    }
    else
    {
      return FALSE;
    }
  }
}

Добавляем в конфиг protected/config/main.php

1
2
3
4
5
6
7
8
9
10
11
12
...
'user'=>array(
  'loginUrl' => '/login',
  // enable cookie-based authentication
  'allowAutoLogin'=>true,
  'behaviors' => array(
    'WebUser' => array(
      'class' => 'WebUser',
    )
  )
),
...

Теперь можно использовать так:

1
Yii::app()->user->data->posts;