Главная » 2013 » Август » 1 » Программирование от Чертенка.ру Выпуск 12. Работа
01:11
 

Программирование от Чертенка.ру Выпуск 12. Работа

Сегодня будут рассмотрены остальные основные компоненты ODAC.

А для начала - интересная новость от IBM:

IBM выпустила бесплатную версию DB2
IBM начала предлагать бесплатную версию СУБД DB2 - DB2 Universal Database Express-C, которую можно использовать не более чем на двух двухпроцессорных серверах, имеющих до 4 Гбайт памяти. Продукт доступен в вариантах для Linux и Windows. Отличием от аналогичных предложений основных конкурентов IBM является отсутствие ограничений на количество одновременно подключенных к СУБД пользователей и на размер баз. Однако в DB2 Express-C отсутствуют некоторые из функций платной DB2 Express, включая модуль DB2 Warehouse Manager, механизм тиражирования источников данных формата Informix и адаптеры обмена данными DB2 Connect. Лицензия разрешает использовать Express-C для нужд предприятия и использовать в составе коммерческих программных продуктов. Продукт можно загрузить по этому адресу. До IBM бесплатные варианты своих СУБД также выпустили Oracle и Microsoft.

Если после выходных Вам лень работать, то ждем Вас на форуме, где можно малость отвлечься от работы и поболтать на отвлеченные темы:

Культура программирования или как НАДО (или как не надо) писать программы.

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

Как часто вы меняете работу ?

Голосование и обсуждение.
К сожалению, наблюдается тенденция, что для повышения зп и своего статуса приходится менять работу.
А для многих это стало способом быстрого увеличения зп и они буквально скачут с места на место.
Как часто меняете работу Вы ? С чем это связано и как по Вашему должно быть?

Обсудить

Собственно хочу узнать какую пользу природа хотела получить и что она получила в итоге, создав человечество на этом земном шаре? Высказывайте свои предположения!
Обсудить

В начале сегодняшнего выпуска хотелось бы остановится на объектах-полях. Объекты-поля не являются "собственностью" ODAC, а уже заложены в механизм доступа к БД TDataSet.

При выполнении запроса датасет автоматически создает в памяти поля-объекты и именно к ним происходит обращение при помоши FieldByName/Fields. Однако такие поля-объекты можно создавать самим, что дает определенные преимущества, например, можно настраивать формат отображения, заголовок поля, а также обращаться к значению поля как к обычному компоненту. Также можно создавать лукап-поля и калк-поля. Рассмотрим все перечисленные возможности.

Создать объекты-поля очень легко - для этого нужно вызвать редактор Fields Editor (одноименный пункт в контекстном меню датасета или редактор свойства Fields). В появившемся пустом списке
из контекстного меню нужно выбрать один из трех вариантов:

  • Add fields... - добавить одно или несколько плей (из предложенных в списке)
  • New field... - создать новое поле (тут можно создать калк- или лукап-поле)
  • Add all fields... - добавить все поля.
Внимание! обратите внимание, что если Ваш запрос выбирает скажем 10 полей, а Вы создали 4 объекта-поля, то и обращаться в коде будет возможно только к этим четерем полям. При попытке обратится к другим полям будет выдана ошибка, мол поле не найдено.

Чтобы не лить много воды (хоть я и Водолей :) ) рассмотрим создание полей на конкретном примере. Создаем новый проект, настраиваем подключение к Ораклу (TOraSession, см 7 выпуск), в TOraQuery пишем следующий запрос select owner, object_name, object_type, status from all_objects. Теперь открываем редактор полей и вызываем команду Add all fields. У вас должно получится так:

Свойства подробно расписаны в справочной системе и я останавливаться на них не буду, замечу, что назначение свойств понятно из названия. Обратите просто внимание на такие свойства, как DisplayFormat, DisplayLabel, ReadOnly, Required. Остановлюсь только на событии OnGetText. Это событие позволяет произвольно изменять визуальное содержимое поля. Например, если в поле Вы храните пол человека в виде M/F, то в этом событии можно сделать виртуальную подстановку на Муж/Жен:

procedure TForm1.OraQuery1STATUSGetText(Sender: TField; var Text: String; DisplayText: Boolean); begin if Sender.AsString = 'F' then Text := 'Жен' else if Sender.AsString = 'M' then Text := 'Муж' else Text := 'Гемофродит'; // :) end;

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

Теперь для доступа к полям можно написать OraQuery1STATUS.Value, хотя OraQuery1.FieldByName('status').asString никто не отменял :)

Очень часто Вы будите создавать калк- и лукап-поля. Для примера создадим калк-поле. Для этого выберем команду New field. В появившемся окне вводим имя поля, тип, и выбираем вид поля - Calculated:

Так как поле вычисляемое на стороне клиента, то Вы сами должны позаботится о его значении. Для этого у TDataSet есть событие onCalcFields:

procedure TForm1.OraQuery1CalcFields(DataSet: TDataSet); begin OraQuery1cnt.Value := OraQuery1.RecordCount; end;

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

Рассмотрим создание лукап-поля. Лукап-поля используются для подстановки значения указанного поля из другой таблицы вместо поля текущего запроса (организация связи основной запрос - справочники). Давайте в нашем примере добавим новое поле, которое будет показывать код владельца объекта базы. (Хотя это лучше сделать одним запросом). Для этого добавим еще один компонент TOraQuery и припишем там запрос select * from all_users. У OraQuery1 создаем новое поле:

Результат виден моментально.

Теперь вернемся к рассмотрению компонентов ODAC.

OraQuery может также выполнять хранимые процедуры/функции, а также анонимные блоки PL/SQL. Вот рабочий пример вызова хранимой функции:

function IsUniqueRNN(dm: TDMCommon; clid: Double; RNN: Double; cltype: string): Boolean; begin if cltype = '1' then dm.OraSession.ExecSQL( 'declare' + #13#10 + ' v_RESULT boolean;' + #13#10 + 'begin' + #13#10 + ' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$, :CLIENTTYPE$);' + #13#10 + ' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 + 'end;', [clid, rnn, '1']) else dm.OraSession.ExecSQL( 'declare' + #13#10 + ' v_RESULT boolean;' + #13#10 + 'begin' + #13#10 + ' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$);' + #13#10 + ' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 + 'end;', [clid, rnn]); Result := dm.OraSession.ParamByName('Result').AsBoolean; if cltype = '1' then begin dm.OraSession.ExecSQL( 'declare' + #13#10 + ' v_RESULT boolean;' + #13#10 + 'begin' + #13#10 + ' v_RESULT := pf.PKG_CLIENTS.ISUNIQUERNN(:CLID$, :RNN$, :CLIENTTYPE$);' + #13#10 + ' :RESULT := sys.DIUTIL.BOOL_TO_INT(v_RESULT);' + #13#10 + 'end;', [clid, rnn, '2']); Result := dm.OraSession.ParamByName('Result').AsBoolean and Result; end; end; Хотя здесь и используется компонент OraSession вместо OraQuery, но в этом нет ничего страшного, можно смело использовать OraQuery.

Компонент TSmartQuery

В прошлом выпуске мы научились создавать "живые" запросы (это такие запросы, которые поддерживают модификацию данных). Для этого нужно было заполнить соответствующими командами свойства SQLInsert/SQLUpdate/SQLDelete. Это все хорошо, если бы не было столь утомительным :) (даже с учетом того, что редактор помогает сгенерировать эти команды). SmartQuery расширяет возможности OraQuery и самостоятельно во время генерирует нужные команды модификации. Достаточно только заполнить свойства SQL, KeySequence, KeyFields - все остальное сделает компонент. В конце выпуска будет приведен рабочий пример.

SmartQuery поддерживает очень интересную возможность SmartRefresh (см. одноименное свойство). Суть этой техники состоит в следующем. Как вы наверное заметили при многопользовательской работе, если один клиент внес какие-либо изменения в таблицу, то все другие клиенты для отображения изменений должны переоткрыть нужные запросы. Это справедливо для всех СУБД и всех библиотек доступа к СУБД. Но ODAC и тут на высоте. Благодаря встроенному пакету Оракла dbms_pipe ODAC может давать сигналы другим клиентам, что были внесены изменения в данные и их нужно перечитать. Чтобы посмотреть SmartRefresh в действии, создайте новый проект, "натравите" TSmartQuery на какую-либо табличку (примерный запрос select t.*, t.rowid from mytable), включите SmartRefresh и подключите SmartQuery к гриду. Запустите программу несколько раз (или на разных компах) и внесите изменения на одном клиенте. Посмотрите, как ведут себя другие клиенты.

Компонент TOraSQL

Этот компонент аналогичен TOraQuery за небольшим исключением. Он предназначен для выполнения любой команды/процедуры/анонимного блока PL/SQL, кроме команды select. В результате этого этот компонент меньше весит и меньше расходует память.

Компонент TOraTable

Предназначен для работы с одной таблицей без написания какого-то бы ни было sql-кода, просто указываете имя таблицы. Является наследником TSmartQuery.

Компонент TOraStoredProc

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

Компонент TOraScript

Этот компонент предназначен для выполнения последовательности команд. Вы можете сказать, что последовательность команд можно оформить в виде PL/SQL кода и выполнить через другие компоненты, но в PL/SQL коде нельзя указывать команды DDL. Вот тут и приходит на помощь TOraScript. Каждая команда должна быть отделена от других символом ; или /, причем / должен начинаться с новой строки и любой блок PL/SQL должен заканчиваться символом /.

Компонент TOraPackage

Предназначен для инкапсуляции работы с пакетами. Хочу заметить, что с пакетами можно работать с помощью анонимного блока PL/SQL, а следовательно с помощью компонент TOraQuery, TOraSmartQuery, TOraSQL.

Компонент TOraLoader

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

  • триггеры не поддерживаются
  • проверочные ограничения не поддерживаются
  • ограничения справочной целостности не поддерживаются
  • кластерные таблицы не поддерживаются
  • пользовательские типы данных не поддерживаются

Для использования загрузчика Оракла нужно выполнить следующие шаги:

  1. указать имя таблицы в свойстве TableName
  2. настроить поля, в которые будут загружаться данные (свойство Columns)
  3. написать обработчик события OnGetColumnData или OnPutData
  4. вызвать метод Load для начала загрузки данных
Ниже приведены примеры обработчиков OnGetColumnData и OnPutData procedure TfmMain.GetColumnData(Sender: TObject; Column: TDAColumn; Row: Integer; var Value: Variant; var EOF: Boolean); begin if Row Где строка EOF := True; сигнализирует о том, что достигнут конец данных. procedure TfmMain.PutData(Sender: TDALoader); var Count: Integer; i: Integer; begin Count := StrToInt(edRows.Text); for i := 1 to Count do begin Sender.PutColumnData(0, i, 1); Sender.PutColumnData(1, i, Random(100)); Sender.PutColumnData(2, i, Random*100); Sender.PutColumnData(3, i, 'abc01234567890123456789'); Sender.PutColumnData(4, i, Date); end; end;

Компонент TOraErrorHandler

Этот компонент позволяет транслировать сообщения Oracle в понятные для пользователя сообщения. Трансляция текста ошибки проходит или в событии OnError или с помощью специальной таблицы, которую можно создать и редактировать в design-time с помощью самого компонента (дважды кликните на компоненте). Например, таблица может содержать такие данные:

Обработчик OnError может быть примерно таким:

procedure TdmCommon.OraErrorHandler1Error(Sender: TObject; E: Exception; ErrorCode: Integer; const ConstraintName: String; var Msg: String); var s: string; begin s := ''; case ErrorCode of 1017: s := 'Неправильно введено имя пользователя/пароль'; 12560: s := 'Неправильно введено имя пользователя/пароль или отсутствует связь с сервером'; 12154: s := 'Введено неверное имя сервера'; end; Msg := s; if Msg = '' then begin s := E.Message; if (Pos('!* 0) and (Pos('>*!', s) > 0) then Msg := Format('Поле ''%s'' должно содержать значение' , [Copy(s, Pos('!**!', s) - Pos('!*end; end;

Тут все должно быть понятно, кроме разве конструкции

if (Pos('!* 0) and (Pos('>*!', s) > 0) then Msg := Format('Поле ''%s'' должно содержать значение' , [Copy(s, Pos('!**!', s) - Pos('!*end;

Если у поля установлен флаг Required, то поле обязательно должно иметь какое-либо значение. Если поле не будет содержать никакого значения (скажем пользователь не ввел его) то датасет перед отправкой данных на сервер выполнит проверку на наличие значения, и если поле не содержит значение, то будет выведено стандартное сообщение на английском языке (Field ... must have a value), что обычному пользователю не совсем понятно. Хочу обратить Ваше внимание, что эту ошибку генерирует не Оракл. А так как TOraErrorHandler обрабатывает все ошибки, а не только ошибки Оракла, то введя для отображаемого имени поля (TField.DisplayLabel) специальные маркеры, можно написать единый централизованный обработчик для таких ошибок, причем для каждого поля можно сделать свой текст ошибок.

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

Для начала была создана следующуя стуктура таблиц:

-- Create table create table PERIODS_TAB ( ID NUMBER not null, YEAR NUMBER(4), MONTH NUMBER(2) ); -- Create/Recreate primary, unique and foreign key constraints alter table PERIODS_TAB add constraint PK_PERIODS primary key (ID); -- Create/Recreate check constraints alter table PERIODS_TAB add constraint CHK_PERIODS_MONTH check (month between 1 and 12); -- Create/Recreate indexes create unique index UN_PERIODS on PERIODS_TAB (YEAR, MONTH); -- Grant/Revoke object privileges grant select, insert, delete on PERIODS_TAB to PF_USER; -- Create sequence create sequence PERIODS_SEQ minvalue 1 maxvalue 999999999999999999999999999 start with 1 increment by 1 nocache; create or replace trigger trg_periods_bi before insert on periods_tab for each row declare -- local variables here begin if :new.id is null then getid('periods', :new.id); end if; end trg_periods_bi; -- Create table create table PLAN_CITY_TAB ( ID NUMBER not null, IDPERIOD NUMBER not null, IDCITY NUMBER not null, PLAN_DOG NUMBER default 0, PLAN_REM NUMBER default 0, MIN_PLAN_DOG NUMBER default 5, MIN_PLAN_REM NUMBER default 4 ); -- Create/Recreate primary, unique and foreign key constraints alter table PLAN_CITY_TAB add constraint PK_PLAN_CITY primary key (ID); alter table PLAN_CITY_TAB add constraint FK_PLAN_CITY_CITY foreign key (IDCITY) references DIVISIONS_TAB (ID); alter table PLAN_CITY_TAB add constraint FK_PLAN_CITY_PERIOD foreign key (IDPERIOD) references PERIODS_TAB (ID) on delete cascade; -- Create/Recreate indexes create index IDX_PLAN_CITY_CITY on PLAN_CITY_TAB (IDCITY) ; create index IDX_PLAN_CITY_PETIOD on PLAN_CITY_TAB (IDPERIOD); -- Grant/Revoke object privileges grant select, update on PLAN_CITY_TAB to PF_USER; -- Create sequence create sequence PLAN_CITY_SEQ minvalue 1 maxvalue 999999999999999999999999999 start with 1 increment by 1 nocache; create or replace trigger trg_plan_city_bi before insert on plan_city_tab for each row declare -- local variables here begin if :new.id is null then getid('plan_city', :new.id); end if; end trg_plan_city_bi; create or replace trigger trg_periods_ai after insert on periods_tab for each row declare -- local variables here begin insert into plan_city_tab(idperiod, idcity) select :new.id, id from divisions_tab where isagp='0'; end trg_periods_ai;

Несколько замечаний по структуре:
1. На таблицу periods_tab не выданы гранты на изменение, так как нет абсолютно никакого смысла изменять период.
2. На таблицу PLAN_CITY_TAB не были выданы гранты на вставку и удаление, так как вставка и удаление происходит автоматически - добавление городов в триггере trg_periods_ai, а удаление - ограничением справочной целостности FK_PLAN_CITY_PERIOD.
3. В триггерах trg_plan_city_bi и trg_periods_bi сначала проверяется, установлено ли значение первичного ключа и если нет, то генерируется.
4. Значение месяца должно быть в интервале 1..12 (проверка CHK_PERIODS_MONTH)
Таким образом, вся бизнес-логика была реализована на стороне сервера. Клиент будет реализовывать только интерфейс пользователя (ввод и редактирование первичных данных).

В качестве интерфейса были взяты два DBGridEh (библиотека EhLib). В первом гриде будут отображаться плановые периоды, а во втором - планы по городам за выбранный период (классическая связь master/detail)
Настройки первого грида:

object gPeriods: TDBGridEh Left = 2 Top = 15 Width = 245 Height = 384 Align = alClient DataSource = dsPeriods Columns = FieldName = 'YEAR' Footers = Title.Caption = 'Год' end item EditButtons = FieldName = 'MONTH' Footers = KeyList.Strings = ( '1' '2' '3' '4' '5' '6' '7' '8' '9' '10' '11' '12') PickList.Strings = ( 'Январь' 'Февраль' 'Март' 'Апрель' 'Май' 'Июнь' 'Июль' 'Август' 'Сентябрь' 'Октябрь' 'Ноябрь' 'Декабрь') Title.Caption = 'Месяц' Width = 113 end> end end

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

Настройки второго грида:

object gCity: TDBGridEh Left = 2 Top = 15 Width = 427 Height = 384 Align = alClient AllowedOperations = [alopUpdateEh] DataSource = dsPlanCity ShowHint = True UseMultiTitle = True Columns = FieldName = 'CITY' Footers = ToolTips = True Width = 133 end item EditButtons = FieldName = 'PLAN_DOG' Footers = end item EditButtons = FieldName = 'PLAN_REM' Footers = end item EditButtons = FieldName = 'MIN_PLAN_DOG' Footers = end item EditButtons = FieldName = 'MIN_PLAN_REM' Footers = end> end

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

Настала очередь датасетов. С первым гридом ассоциирован датасет:

object qPeriods: TSmartQuery KeyFields = 'ID' KeySequence = 'PF.PERIODS_SEQ' SequenceMode = smInsert Session = dmCommon.OraSession SQL.Strings = ( 'select t.*, t.rowid from pf.periods_tab t') FetchAll = True ReadOnly = True Options.ReturnParams = True Left = 32 Top = 113 end

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

Второй датасет (ассоциирован со втором гридом):

object qPlanCity: TSmartQuery KeySequence = 'PF.PLAN_CITY_SEQ' SequenceMode = smInsert Session = dmCommon.OraSession SQL.Strings = ( 'select t.*, t.rowid from pf.plan_city_tab t') MasterFields = 'id' DetailFields = 'idperiod' MasterSource = dsPeriods FetchAll = True Left = 549 Top = 141 ParamData = object qPlanCityID: TFloatField FieldName = 'ID' Required = True end object qPlanCityIDPERIOD: TFloatField FieldName = 'IDPERIOD' Required = True end object qPlanCityIDCITY: TFloatField FieldName = 'IDCITY' Required = True end object qPlanCityPLAN_DOG: TFloatField DisplayLabel = 'План|Договора' FieldName = 'PLAN_DOG' end object qPlanCityPLAN_REM: TFloatField DisplayLabel = 'План|Заявления' FieldName = 'PLAN_REM' end object qPlanCityMIN_PLAN_DOG: TFloatField DisplayLabel = 'Минимальный план|Договора' FieldName = 'MIN_PLAN_DOG' end object qPlanCityMIN_PLAN_REM: TFloatField DisplayLabel = 'Минимальный план|Заявления' FieldName = 'MIN_PLAN_REM' end object qPlanCityROWID: TStringField FieldName = 'ROWID' ReadOnly = True Size = 18 end object qPlanCityCITY: TStringField DisplayLabel = 'Город' FieldKind = fkLookup FieldName = 'CITY' LookupDataSet = qAreas LookupKeyFields = 'ID' LookupResultField = 'NAME' KeyFields = 'IDCITY' Size = 128 Lookup = True end end

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

Осталась самая малость - при открытии формы открыть датасеты:
qPeriods.Open;
qPlanCity.Open;
и сделать три кнопочки:
* добавить период
* удалить период
* сохранить изменения
вот их обработчики:

procedure TFrmPensionHandBook.actAddPeriodExecute(Sender: TObject); begin qPeriods.ReadOnly := False; //Разрешаем редактировать датасет qPeriods.Append; // добавляем пустую запись, конкретные значения // юзер будет вводить непосредственно в гриде end; procedure TFrmPensionHandBook.actDelPeriodExecute(Sender: TObject); begin if Application.MessageBox('Вы дейсвительно хотите удалить период и все плановые показатели?', 'Подтверждение', MB_YESNO) ID_YES then Exit; qPeriods.ReadOnly := False; //Разрешаем редактировать датасет qPeriods.Delete; // удаляем период, все города за этот период //удалятся автоматически сервером qPeriods.ReadOnly := True; //Запрещаем редактировать датасет end; procedure TFrmPensionHandBook.actPostPeriodExecute(Sender: TObject); begin qPeriods.Post; // Отсылаем изменения на сервер qPeriods.ReadOnly := True; //Запрещаем редактировать датасет qPlanCity.Refresh; // Обновляем планы по городам end;

Ну и еще несколько обработчиков для управления кнопками

// Кнопка "сохранить изменения" доступна когда датасет не в режиме ReadOnly procedure TFrmPensionHandBook.actPostPeriodUpdate(Sender: TObject); begin TAction(Sender).Enabled := not qPeriods.ReadOnly; end; // Кнопка "добавить период" доступна когда датасет в режиме ReadOnly procedure TFrmPensionHandBook.actAddPeriodUpdate(Sender: TObject); begin TAction(Sender).Enabled := qPeriods.ReadOnly; end; // Кнопка "удалить период" доступна когда датасет в режиме ReadOnly и // есть хотя бы одна запись procedure TFrmPensionHandBook.actDelPeriodUpdate(Sender: TObject); begin TAction(Sender).Enabled := qPeriods.ReadOnly and not qPeriods.IsEmpty; end;

Форма в работе:

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

Вы не забыли, что мы ждем Вас на форуме Чертенок.ру?

Просмотров: 1928 | Добавил: binestall | Рейтинг: 0.0/0
Всего комментариев: 0
Мини-чат
Статистика

Онлайн всего: 2
Гостей: 2
Пользователей: 0
Форма входа
Поиск
Календарь
«  Август 2013  »
ПнВтСрЧтПтСбВс
   1234
567891011
12131415161718
19202122232425
262728293031
Архив записей
Друзья сайта
  • Официальный блог
  • Сообщество uCoz
  • FAQ по системе
  • Инструкции для uCoz