?

Log in

No account? Create an account

Зри в корень: java.lang.Object

В Java в вершине иерархии классов лежит класс java.lang.Object. Лежит и лежит, долгое время я им совсем не интересовался.

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

Однако, такой класс есть и я приведу тут исходники java/lang/Object.java, опустив javadoc, и попытаюсь пролить свет на некоторые моменты связанные с реализацией jvm:

Продолжение на хабре.

Метки:

Java байткод "Hello world"

На хабре уже есть статья про java байткод. Я решил ее немного дополнить и в меру сил развить тему. Мне кажется довольно логичным разобрать простейшее приложение на Java. А что может быть проще «Hello world»?
Для своего эксперимента я создал директорию src, куда в папку hello положил файл App.java:

package hello;

public class App {

    public static void main(String[] args) {
        System.out.println("Hello world!");
    }

} 


Метки:

Сериализация в строку

Сейчас в программe есть запутанная работа с датами. Вводить их можно только в том виде, в котором их понимает pg 2015-08-10, 4 цифры год тире 2 цифры месяца 2 цифры день. А для вывода я использую специальное форматирование в шаблоне items.jade:

item[field.sys_name].getDate() + '.' + (item[field.sys_name].getMonth() + 1) + '.' + item[field.sys_name].getFullYear()


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

function StringTransform () {
}

StringTransform.prototype.format = function (field, value) {
    if (field.type_id == 4) {
        return value.getDate() + '.' + (value.getMonth() + 1) + '.' + value.getFullYear();
    }
    return value;
}

module.exports.stringTransform = new StringTransform();

Это пока только форматтер. Добавим обработку элементов перед передачей в шаблон:

app.get('/lists/:id/items', function (req, res) {
    storage.readList(req.params.id, function (list, fields, items) {
        for (var i = 0; i < fields.length; i++) {
            var field = fields[i];
            for (var j = 0; j < items.length; j++) {
                var item = items[j];
                item[field.sys_name] = stringTransform.format(field, item[field.sys_name]);
            }
        }
        res.render('items', {
            list: list,
            fields: fields,
            items: items,
            editable: auth.isListEditableByUser(list, req.user)
        });
    });
});

От этого шаблон упроститься, надо будет просто писать:



td= item[field.sys_name]


А уже превращение полей элемента в строку будет заниматься форматтер. И хотя за представление должна отвечать вьюха, форматирование может понадобиться в csv файлах, json и xml. Теперь добавим парсер дат:

StringTransform.prototype.parse = function (field, value) {
    if (field.type_id == 4) {
        var parts = value.split('.'),
            day = parseInt(parts[0]),
            month = parseInt(parts[1]) - 1,
            year = parseInt(parts[2]),
            d = new Date(year, month, day);
       return d;
    }
    return value;
}


И будем его использовать при добавлении элементов:



for (var i = 0; i < fields.length; i++) {
    var field = fields[i];
    item[field.sys_name] = stringTransform.parse(field, req.body[field.sys_name]);
}

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

Все изменения в этом коммите:
https://github.com/corvette888/listapps/commit/a4fcc3a3e00ac863a0871df6d214afbfb5861b73

Список депутатов 2

Начало.
Добавляю дату в таблицу type:
insert into type (name) values ('дата');
И добавляю информацию в конструктор таблиц:

if (field.type_id == 3) {
    columns += ',\n' + field.sys_name + ' bigint references ' + field.type_param + '(id) not null';
} else if (field.type_id == 4) {
    columns += ',\n' + field.sys_name + ' timestamp';
} else {
    columns += ',\n' + field.sys_name + ' ' + (field.type_id == 1 ? 'decimal' : 'text');
}


Еще надо заменить форму описание формы ввода поля:

field = {
    name: 'Поля списка',
    fields: [
        { name: 'Название', sys_name: 'name' },
        { name: 'Идентификатор', sys_name: 'sys_name' },
        { name: 'Тип', sys_name: 'type_id',
            control: {
                select: [
                    {id: 1, name: 'число'},
                    {id: 2, name: 'строка'},
                    {id: 3, name: 'список'},
                    {id: 4, name: 'дата'}]}}
    ]
},

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

app.post('/lists/:id/fields/add/type', function (req, res) {
    var type_id = req.body.type_id;
    if (type_id == 1 || type_id == 2 || type_id == 4 || req.body.type_param) {
...


И немного улучшить представление:

for field in fields
    if field.control && field.control.select
        for option in field.control.select
            if option.id == item[field.sys_name]
                td= option.name
    else
        if field.type_id == 4
            td= item[field.sys_name].getDate() + '.' + (item[field.sys_name].getMonth() + 1) + '.' + item[field.sys_name].getFullYear()
        else
            td= item[field.sys_name]

Все теперь можно создавать поля типа дата. Как мы видим, чтобы добавить новый тип надо провести много рутинных операций. Это утомительно, а типов полей в моей системе будет очень много, поэтому в будущем надо придумать хороший способ добавлять новые типы данных.
Теперь надо сделать список элементов публично доступным. Для этого добавим в список настройку:
alter table list add column public boolean not null default false;
Поменяем описание для списков:

list = {
    name: 'Мои списки',
    fields: [
        { name: 'Название', sys_name: 'name' },
        { name: 'Идентификатор', sys_name: 'sys_name' },
        { name: 'Публичный', sys_name: 'public'}
    ]
},

Подправим сохранение списков:

app.post('/lists/:id/edit', function (req, res) {
    storage.update('list', { id: req.params.id,
                             name: req.body.name,
                             'public': req.body['public']}, function () {
        res.redirect('/lists');
    });
});


И поменяем правила авторизации:

app.all(/^\/lists\/(\d+).*/, function(req, res, next) {
    storage.readById('list', req.params[0], function (err, result) {
        var list = result.rows[0];
        if (req.url == '/lists/' + req.params[0] + '/items' && auth.isListViewableByUser(list, req.user)) {
            next();
        } else if (auth.isListEditableByUser(list, req.user)) {
            next();
        } else {
            res.redirect('/permission-denied');
        }
    });
});

var auth = {
    isListEditableByUser: function (list, user) {
        return list.app_user_id == user.id;
    },
    isListViewableByUser: function (list, user) {
        return list['public'] || auth.isEditableByUser(list, user)
    }
}


Теперь осталось скрыть элементы редактирования от пользователей не имеющих на это прав:

app.get('/lists/:id/items', function (req, res) {
    storage.readList(req.params.id, function (list, fields, items) {
        res.render('items', {
            list: list,
            fields: fields,
            items: items,
            editable: auth.isListEditableByUser(list, req.user)
        });
    });
});


и еще в шаблоне:

td
    if editable
        a(href="items/#{item.id}/delete") Удалить
td
    if editable
        a(href="items/#{item.id}/edit") Редактировать


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

app.get('/public/:list_id', function (req, res) {
    storage.readList(req.params.list_id, function (list, fields, items) {
        if (list && list['public']) {
            res.render('items', {
                list: list,
                fields: fields,
                items: items,
                editable: false
            });
        } else {
            res.redirect('/permission-denied');
        }
    });
});


Тут есть один недочет, владелец списка на этой странице не сможет его редактировать.
Но пока я не знаю как его устранить, надо чуть больше разобраться в Passport.
Выкатываем новую версию на сервер и создаем список http://listapps.ru/public/2
Как я уже писал, я хочу опубликовать список депутатов государственной думы. Для начала создадим такой список. Я буду хранить следующую информацию о каждом депутате:

  • ФИО

  • Пол

  • Дата рождения

ФИО будет обычным текстовым полем, Пол будет с типом список, Дата Рождения будет датой. Добавим новый список пол, в нем будет только одно поле "название". Теперь создадим новый тип данных "список":
insert into type (name) values ('список');
Добавим в описание field новый элемент:

field = {
    name: 'Поля списка',
    fields: [
        { name: 'Название', sys_name: 'name' },
        { name: 'Идентификатор', sys_name: 'sys_name' },
        { name: 'Тип', sys_name: 'type_id',
            control: {
                select: [
                    {id: 1, name: 'число'},
                    {id: 2, name: 'строка'},
                    {id: 3, name: 'список'}]}}
    ]
}

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

  • Задание названия и идентификатора

  • Выбор типа поля

  • Указание параметров типа

Слегка изменив шаблон формы, чтобы можно было управлять тем, куда отправятся данные:

form(method="post" action="#{next || ''}")

И изменим ручку для отображения формы добавления поля:

app.get('/lists/:id/fields/add', function (req, res) {
    res.render('form', {
        list: field,
        item: {
            name: '',
            sys_name: '',
            type_id: 1},
        next: '/lists/:id/fields/add/type'});
});

Теперь нужно добавить обработчик для указания параметров типа:

app.post('/lists/:id/fields/add/type', function (req, res) {
    var type_id = req.body.type_id;
    if (type_id == 1 || type_id == 2 || req.body.type_param) {
        storage.create('field', {
            name: req.body.name,
            sys_name: req.body.sys_name,
            type_id: type_id,
            list_id: req.params.id,
            type_param: req.body.type_param || null
        }, function (err, result) {
            res.redirect('/lists/' + req.params.id);
        });
    } else if (type_id == 3) {
        var f = function () {};
        f.prototype = field;
        var fieldWithTypeParam = new f();

        fieldWithTypeParam.fields = [];
        for (var i = 0; i < field.fields.length; i++) {
            fieldWithTypeParam.fields.push(field.fields[i]);
        }
        fieldWithTypeParam.fields.push({name: "Параметр типа", sys_name: "type_param"});

        res.render('form', {
            list: fieldWithTypeParam,
            item: {
                name: req.body.name,
                sys_name: req.body.sys_name,
                type_id: type_id,
                type_param: 'gender'
            }
        });
    }
});

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

Storage.prototype.createTable = function (name, fields, callback) {
    var columns = 'id bigserial primary key';
    for(var i = 0; i < fields.length; i++) {
        var field = fields[i];
        if (field.type_id == 3) {
            columns += ',\n' + field.sys_name + ' bigint references ' + field.type_param + '(id) not null';
        } else {
            columns += ',\n' + field.sys_name + ' ' + (field.type_id == 1 ? 'decimal' : 'text');
        }
    }
    query('create table ' + name + '(' + columns + ')', [], callback);
};


Теперь надо поменять загрузку описания формы, чтобы начитывались списочные значения:

Storage.prototype.readFieldParams = function (list, fields, callback) {
    var _this = this,
        i = 0;
    while (i < fields.length) {
        var field = fields[i];
        i++;
        if (field.type_id == 3 && !field.control) {
            _this.read(field.type_param, function (err, result) {
                field.control = {
                    select: result.rows
                }
                _this.readFieldParams(list, fields, callback);
            });
            return;
        }
    }
    callback(list, fields);
};

Storage.prototype.readDescription = function (list_id, callback) {
    var _this = this;
    _this.readById('list', list_id, function (err, result) {
        var list = result.rows[0];
        _this.readByFieldValue('field', 'list_id', list_id, function (err, result) {
            _this.readFieldParams(list, result.rows, callback);
        });
    })
};


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





for item in items
    tr
        for field in fields
            if field.control && field.control.select
                for option in field.control.select
                    if option.id == item[field.sys_name]
                        td= option.name
            else
                td= item[field.sys_name]
        td
            a(href="items/#{item.id}/delete") Удалить
        td
            a(href="items/#{item.id}/edit") Редактировать

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

Продолжение.
Я подготовил вторую версию списков, которая была описана в http://yasha-makarov.livejournal.com/27111.html Теперь пользователи могут регистрироваться и добавлять свои списки. В списках можно указывать набор полей и два типа этих полей строковый и числовой. Уже первую версию списков я начал использовать под список своих задач. Теперь у любого желающего появилась такая возможность на сайте http://listapps.ru Пока я не вижу никакого смысла другим делать это, разве что из любопытства. Скажем прямо те возможности, которые предоставляет docs.google.com, гораздо шире и качественнeе реализованы. Однако, немножко проявив фантазию, я хотел бы перечислить приложения для которых можно было бы использовать списки.
Список задач.
Как я уже сказал, я использую свое приложение для ведения списка задач. Каждая задача в списке представлена одним элементом с одним поле "содержание". Для моего сценария использования ничего другого и не надо.
Блог.
В будущем я хотел бы использовать списки для ведения блога. Сейчас я это делаю в livejournal, но хочется перевести на свой движок.
Как говорится "eat your own food" Для этого надо добавить возможность другим пользователям, а также анонимным пользователям просматривать блог. Люди будут читать блог и он должен быть прилично оформлен. Они будут искать в нем информацию. Поэтому должен быть полнотекстовый поиск. Они наверняка захотят получать уведомления о новых сообщениях в блог и подписываться.
Читатели будут с удовольствием комментировать мои сообщения в блоге и поэтому нужно дать им такую возможность. Я точно буду хотеть смотреть статистику использования моего блога и поэтому в нем должны быть инструменты для отображения статистики. В целом ход мыслей понятен, и добавить можно еще довольно много, например сделать блог коллективным и добавить рейтинг и оценки публикаций, а также шаринг в социальных сетях. Ну вы поняли.
Публикация открытых данных.
Есть такое движение открытые данные. Там люди требуют от государства дать доступ к данным, собранным на средства налогоплательщиков. По закону эти данные должны быть общедоступные и нет никакого резона скрывать их. Есть такой сайт с открытым данным http://hubofdata.ru/, на котором можно эти данные посмотреть и скачать. Как мне кажется выбранные форматы хранения данных не очень хороши, то есть нет средств прямо на сайте оперировать данными. Я хочу закачивать открытые данные в свои списки и давать возможность для начала их сортировать.В дальнейшем есть прекрасные библиотеки для построения графиков, которые я хотел бы использовать для инфографики. Также многие данные можно было красиво показать на карте, определенный опыт в этом направлении у меня имеется. В целом это направление мне кажется очень перспективным для развития идеи списков.
Прайс лист.
Мои списки можно использовать для публикации прайс листов компаний. Для этого не хватает тех же возможностей, что и для блога. Надо уметь давать доступ людям и настраивать внешний вид. Со списоком товаров с комментариями и списком заказов, можно теоретически получить работоспособный магазин. Я не считаю это направление перспективным потому, что тема денежная и очень многие в ней сделали очень много.

Надеюсь, приведенные примеры проливают свет на возможности далнейшего развития моего проекта. По-видимому я буду превращать эту платформу в блоговый движок с возможностью публикации открытых данных. Сначала я опубликую данные о составе государственной думы из википедии, а потом попробую написать об этих данных сообщение в самодельный блог. Следите за новостями, и пожалуйств, подумайте чего вам не хватает в списках, чтообы они были вам полезны, я постараюсь реализовать ваши хотелки, если это будет в моих силах.
Код открыт https://github.com/corvette888/listapps и вы сможете забрать его себе.

Метки:

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

passport.use(new LocalStrategy(
    function (username, password, done) {
        storage.readByFieldValue('app_user', 'login', username, function (err, result) {
            if (err || result.rows.length != 1) {
                return done(null, false, {message: 'Неверный логин.'});
            }
            var user = result.rows[0];
            console.log(user);
            if (password != user.password) {
                return done(null, false, {message: 'Неверный пароль.'});
            }
            return done(null, user);
        });
    }));

Тут я снова воспользовался удобным инструментом storage описанным ранее.Теперь надо добавить форму с регистрацией. И для этого я воспользуюсь универсальной формой:

registration = {
        name: 'Регистрация',
        fields: [
            { name: 'Логин', sys_name: 'login' },
            { name: 'Пароль', sys_name: 'password', control : { input: 'password'} },
            { name: 'Подтверждение', sys_name: 'confirmation', control: { input: 'password'} }
        ]
    };

app.get('/registration', function (req, res) {
    res.render('form', {
        list: registration,
        item: {
            login: '', password: '', confirmation: ''
        }});
});

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


doctype html
html(lang="en")
    head
        title Логин
    body
        h1 Логин
        form(method="post")
            div
                label Логин:
                input(type="text" name="username")
            div
                label Пароль:
                input(type="password" name="password")
            div
                input(type="Submit" value="Войти")
        a(href="/registration") Зарегистрироваться

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

app.get('/lists', function (req, res) {
    storage.readByFieldValue('list', 'app_user_id', req.user.id, function (err, result) {
        res.render('list', {list: result.rows});
    });
});

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

var auth = {
    isListEditableByUser: function (list, user) {
        return list.app_user_id == user.id;
    }
}

module.exports = auth;


И использую его в app.js:

app.all('/permission-denied', function (req, res) {
    res.render('permission-denied', {});
});

app.all(/^\/lists\/(\d+).*/, function(req, res, next) {
    storage.readById('list', req.params[0], function (err, result) {
        var list = result.rows[0];
        if (auth.isListEditableByUser(list, req.user)) {
            next();
        } else {
            res.redirect('/permission-denied');
        }
    });
});


И простой шаблон для страницы с ошибкой доступа permission-denied.jade:


doctype html
html(lang="en")
    head
        title Нет доступа
    body
        h1 Нет доступа


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

Новые изменения можно посмотреть здесь:
https://github.com/corvette888/listapps/commit/855c59d40100fc5f6fa2bfeb5a1b45e56d981eac
В предыдущем посте я определил Storage, который умеет создавать таблицы для списков. Добавлю кнопку для создания списков на страницу редактирования списка  views/item.jade:


form(method="post" action="#{item.id}/create")
    input(type="submit" value="Создать список")

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

app.post('/lists/:id/create', function (req, res) {
    var list_id = req.params.id;
    storage.readById('list', list_id, function (err, result) {
        var list_name = result.rows[0].sys_name;
        storage.readByFieldValue('field', 'list_id', list_id, function (err, result) {
            var fields = result.rows;
            storage.createTable(list_name, fields, function () {
                res.redirect('/lists/' + list_id + '/items');
            });
        });
    });
});

Тоже самое я сделаю с delete и edit:

/lists/delete/:id -> /lists/:id/delete
/lists/edit/:id -> /lists/:id/edit

Так гораздо изящнее, как я раньше этого не замечал. Теперь надо добавить средства для просмотра и редактирования произвольных списков. Шаблон для формы уже есть см. http://yasha-makarov.livejournal.com/28064.html Шаблон для отображения списка выглядит так:


doctype html
html(lang="en")
    head
        title #{list.name}
    body
        h1 Элементы списка #{list.name}
        table
            tr
                for field in fields
                    th= field.name
                th
                th
            for item in items
                tr
                    for field in fields
                        td= item[field.sys_name]
                    td
                        a(href="items/#{item.id}/delete") Удалить
                    td
                        a(href="items/#{item.id}/edit") Редактировать
        a(href="items/add") Добавить

Обработчики для действий получились вот такими:

app.get('/lists/:id/items', function (req, res) {
    storage.readList(req.params.id, function (list, fields, items) {
        res.render('items', {
            list: list,
            fields: fields,
            items: items
        });
    });
});

app.get('/lists/:id/items/add', function (req, res) {
    storage.readDescription(req.params.id, function (list, fields) {
        list.fields = fields;
        var item = {};
        for (var i = 0; i < fields.length; i++) {
            item[fields[i].sys_name] = '';
        }
        res.render('form', {
            list: list,
            item: item
        });
    });
});

app.post('/lists/:id/items/add', function (req, res) {
    var list_id = req.params.id;
    storage.readDescription(list_id, function (list, fields) {
        var item = {};
        for (var i = 0; i < fields.length; i++) {
            item[fields[i].sys_name] = req.body[fields[i].sys_name];
        }
        storage.create(list.sys_name, item, function () {
            res.redirect('/lists/' + list_id + '/items');
        });
    });
});

app.get('/lists/:id/items/:item_id/edit', function (req, res) {
    storage.readDescription(req.params.id, function (list, fields) {
        storage.readById(list.sys_name, req.params.item_id, function (err, result) {
            list.fields = fields;
            var item = result.rows[0];
            res.render('form', {
                list: list,
                item: item
            });
        });
    });
});

app.post('/lists/:id/items/:item_id/edit', function (req, res) {
    var list_id = req.params.id;
    storage.readDescription(list_id, function (list, fields) {
        var item = {
            id: req.params.item_id
        };
        for (var i = 0; i < fields.length; i++) {
            item[fields[i].sys_name] = req.body[fields[i].sys_name];
        }
        storage.update(list.sys_name, item, function () {
            res.redirect('/lists/' + list_id + '/items');
        });
    });
});


app.get('/lists/:id/items/:item_id/delete', function (req, res) {
    storage.readDescription(req.params.id, function (list, fields) {
        storage.del(list.sys_name, req.params.item_id, function () {
            res.redirect('/lists/' + req.params.id + '/items');
        });
    });
});


Для того, чтобы они работали мне пришлось в  app_modules/db.js добавить несколько методов в Storage:

Storage.prototype.readDescription = function (list_id, callback) {
    var _this = this;
    _this.readById('list', list_id, function (err, result) {
        var list = result.rows[0];
        _this.readByFieldValue('field', 'list_id', list_id, function (err, result) {
            callback(list, result.rows);
        });
    })
};

Storage.prototype.readList = function (list_id, callback) {
    var _this = this;
    _this.readDescription(list_id, function (list, fields) {
        _this.read(list.sys_name, function (err, result) {
            callback(list, fields, result.rows);
        });
    });
};


Методы добавленные в Storage, не совсем к нему относятся, они добавляют в него знания о существовании специальных таблиц list и field, которые особенным образом управляются. По-хорошему эти методы надо вынести в отдельный модуль. В целом реализация второй версии списков закончена, можно создавать произвольные списки и добавлять в них элементы. Дальше надо переходить к тестированию кода и приведению его в порядок. В частности надо добавить логирование и улучшить обработку ошибок. Также надо добавить регистрацию пользователей и проверку прав доступа к спискам. Чтобы все манипуляции были более осознанны, я хочу в ближайшее время прописать сценарии использования списков. Когда все это будет закончено, надо будет привести представление в минимально пристойный вид. В этом я надеюсь мне поможет бутстрап.

Изменения описанные в этом посте можно посмотреть в этом коммите:
https://github.com/corvette888/listapps/commit/8e19bdd15990d2d4c957b40983f132e0b0ded442
Сейчас есть два очеь похожих шаблона list_form.jade:

doctype html
html(lang="en")
    head
        title Список элементов
    body
        h1 Добавить элемент
        form(method="post")
            .field
                label(for="name") Название:
                input(type="text" name="name" value="#{item.name}")
            .field
                label(for="sys_name") Идентификатор:
                input(type="text" name="sys_name" value="#{item.sys_name}")
            input(type="submit" value="Отправить")

и field_form.jade:

doctype html
html(lang="en")
    head
        title Список элементов
    body
        h1 Добавить элемент
        form(method="post")
            .field
                label(for="name") Название:
                input(type="text" name="name" value="#{item.name}")
            .field
                label(for="sys_name") Идентификатор:
                input(type="text" name="sys_name" value="#{item.sys_name}")
            .field
                label(for="type_id") Тип:
                select(name="type_id")
                    option(value="1") число
                    option(value="2") строка
            input(type="submit" value="Отправить")

Попробую сделать универсальный шаблон form.jade, который на вход будет принимать описание списка, а на выходе выдавать форму ввода для списка:

doctype html
html(lang="en")
    head
        title Добавить элемент в #{list.name}
    body
        h1 Добавить элемент в #{list.name}
        form(method="post")
            for field in list.fields
                .field
                    label(for="#{field.sys_name}") #{field.name}:
                    input(type="text" name="#{field.sys_name}" value="#{item[field.sys_name]}")
            input(type="submit" value="Отправить")

И попробуем вызвать ее для списка list:

var list = {
    name: 'Мои списки',
    fields: [
        { name: 'Название', sys_name: 'name' },
        { name: 'Идентификатор', sys_name: 'sys_name' }
    ]
};

app.get('/lists/add', function (req, res) {
    res.render('form', {
        list: list,
        item: {
            name: '',
            sys_name: ''}});
});

app.get('/lists/edit/:id', function (req, res) {
    storage.readById('list', req.params.id, function (err, result) {
        res.render('form', {
            list: list,
            item: result.rows[0]
        });
    });
});

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

field = {
        name: 'Поля списка',
        fields: [
            { name: 'Название', sys_name: 'name' },
            { name: 'Идентификатор', sys_name: 'sys_name' },
            { name: 'Тип', sys_name: 'type_id',
                control: {
                    select: [
                        {id: 1, name: 'число'},
                        {id: 2, name: 'строка'}]}}
        ]
    }

И добавлю поддержку нового элемента управления в шаблон:


if field.control && field.control.select
    select(name="#{field.sys_name}")
        for option in field.control.select
            option(value="#{option.id}") #{option.name}
else
    input(type="text" name="#{field.sys_name}" value="#{item[field.sys_name]}")

Теперь можно заменить рендеринг для полей в app.js. Осталось пофиксить ошибку с неправильной инициализацией селекта. В будущем я планирую добавить поддержку всех возможнох типов полей ввода. Прежде чем переходить к рефакторингу шаблонов для отображения списком list и field я хочу доделать показ произвольно определенных списков.

Все изменения связанные с введением универсальной формой ввода можно посмотреть здесь: https://github.com/corvette888/listapps/commit/193b78b7e373ebf1cd33dab5f236c605b3112dfa
В модуле app_modules/db.js уже есть средства для работы с базой данных. Они предназначены для редактирования таблиц list и field. По этим таблицам мне нужно создавать новые таблицы для хранения списков, определенных пользователем. Для этого надо написать код, который по полям в списке дает средства редактирования и доступа. Приступим. Для начала надо создать таблицу для списка, передав название и типы полей:

function Storage() {};

Storage.prototype.createTable = function (name, fields, callback) {
    var columns = '';
    for(var i = 0; i < fields.length; i++) {
        if (columns != '') {
            columns += ',\n'
        };
        var field = fields[i];
        columns += field.name + ' ' + (field.type_id == 1 ? 'decimal' : 'text');
    }
    query('create table ' + name + '(' + columns + ')', [], callback);
};


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

var Storage = require('../app_modules/db').Storage;

var storage = new Storage();

var fields = [
    {name: 'a', type_id: 1},
    {name: 'b', type_id: 2}
];

storage.createTable('test', fields, function () {});

Идем дальше. После того, как таблица создана хочется добавлять в нее информацию:

Storage.prototype.create = function (list_name, item, callback) {
    var fields = '',
        params = '',
        i = 0,
        values = [];
    for (var field in item) {
        if (fields != '') {
            fields += ', ';
            params += ', ';
        }
        fields += field;
        i++;
        params += '$' + i;
        values.push(item[field]);
    }
    query('insert into ' + list_name + ' (' + fields + ') values (' + params + ')', values, callback);
};

Эти манипуляции с sql недостаточно безопасны и fields не экранируется. Сейчас я не буду этим заниматься, но в дальнейшем при создании списков надо очень строго проверять названия полей, чтобы нельзя было сделать sql-injection. Следующим шагом будет чтение элементов из списка:

Storage.prototype.read = function (list_name, callback) {
    query('select * from ' + list_name, [], callback);
};

Storaga.prototype.readById = function (list_name, id, callback) {
    query('select * from ' + list_name + ' where id = $1', [id], callback);
};

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

Storage.prototype.readByFieldValue = function (list_name, field, value, callback) {
    query('select * from ' + list_name + ' where ' + field + ' = $1', [value], callback); 

};

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

Storage.prototype.update = function (list_name, item, callback) {
    var sets = '',
        i = 0
        values = [];
    for (var field in item) {
        if (field == 'id') {
            continue;
        };
        if (sets != '') {
            sets += ', ';
        }
        i++;
        sets +=  field + ' = $' + i;
        values.push(item[field]);
    }
    values.push(item.id);
    query('update ' + list_name + ' set ' + sets + ' where id = $' + (i + 1), values, callback);
}

И удаление:


Storage.prototype.del = function (list_name, id, callback) {
    query('delete from ' + list_name + ' where id = $1', [id], callback);
}

Полученный код очень похож на тот, что я использовал в List и Field. По-видимому пришло время встроить его на их место. Просто надо все вызовы вида:
List.read(...)
Заменить на:
Storage.read('list' ...)
Внести унификацию в create и update методы.
Отлично мы унифицировали средства работы с базой данных.

Внесенные изменения содержит этот коммит https://github.com/corvette888/listapps/commit/193b78b7e373ebf1cd33dab5f236c605b3112dfa

Метки:

Profile

yasha_makarov
yasha_makarov

Latest Month

Август 2015
Вс Пн Вт Ср Чт Пт Сб
      1
2345678
9101112131415
16171819202122
23242526272829
3031     

Syndicate

RSS Atom
Разработано LiveJournal.com
Designed by Tiffany Chow