Строим блог на основе Node.js MVC фреймворка Areto

Контроллер статьи

Класс ArticleController отвечает за вывод, поиск, сортировку и фильтрацию статей.

controllers/ArticleController.js

'use strict';
const Base = require('areto/base/Controller');
module.exports = class ArticleController extends Base {
  // place methods here
};
module.exports.init(module);
const ActiveDataProvider = require('areto/data/ActiveDataProvider');
const Article = require('../models/Article');

Метод проверяет корректность идентификатора статьи.

controllers/ArticleController.js

...
static isValidId (id) {
  return id && /^[a-f0-9]{24}$/.test(id);
}
...

Метод getModel находит модель статьи по идентификатору, указанному в GET-запросе this.getQueryParam('id'). В необязательном аргументе relations передается массив отношений, которые должны быть загружены вместе с моделью. Если статья не найдена, то пользователю возвращается 404 ошибка (this.throwNotFound()).

controllers/ArticleController.js

...
getModel (cb, ...relations) {
  let id = this.getQueryParam('id');
  if (this.constructor.isValidId(id)) {
    Article.findById(id).with(relations).one((err, model)=> {
      err ? this.throwError(err)
          : model ? cb(model) : this.throwNotFound();
    });
  } else {
    this.throwNotFound();
  }  
}
...

Метод actionIndex выводит основной список статей. Для сортировки и разбиения списка на страницы используется класс areto/data/ActiveDataProvider. В параметре query задается запрос, который находит список опубликованных статей.

Для вывода статей в различном порядке, используйте параметр sort. В свойстве attrs укажите атрибуты статьи, по которым доступна сортировка списка. Для сортировки по умолчанию используйте defaultOrder.

Для удобства представления и снижения нагрузки в ActiveDataProvider по умолчанию включено постраничное разбиение (10 элементов на страницу). Используйте параметр pageSize, чтобы задать другой размер страницы. Если требуется вывести весь список целиком, то установите pagination: null.

controllers/ArticleController.js

...
actionIndex () {
  let provider = new ActiveDataProvider({
    controller: this,
    query: Article.findPublished(),
    sort: {
      attrs: {
        date: true,
        title: true
      },
      defaultOrder: {date: -1}
    },
    pagination: {
      pageSize: 15
    }    
  });
  provider.prepare(err => {
    err ? this.throwError(err) : this.render('index', {provider});
  });
}
...

Метод actionSearch выводит список статей отфильтрованных по поисковому тексту. Реализация аналогична методу actionIndex за исключением параметра query. Пользовательский текст из GET-параметра this.getQueryParam('text') передается аргументом в статический метод Article.findBySearch.

controllers/ArticleController.js

...
actionSearch () {
  let provider = new ActiveDataProvider({
    controller: this,
    query: Article.findBySearch(this.getQueryParam('text')),
    sort: {
      attrs: {
        date: true,
        title: true
      },
      defaultOrder: {date: -1}
    }
  });
  provider.prepare(err => {
    err ? this.throwError(err) : this.render('index', {provider});
  });
}
...

Метод actionView работает с отдельной статьей. Если запрос приходит в формате POST (isPost), то обрабатывается форма создания нового комментария.

Для создания нового комментария в объект класса Comment загрузите данные формы comment.load(this.getBodyParams()). Установите статью, к которой относится комментарий, и IP пользователя.

Если комментарий успешно сохранен, то пользователю выводится соответствующее сообщение (setFlash). Сообщение записывается в сессию и однократно отобразится при следующей загрузке страницы. Затем происходит перезагрузка страницы this.redirect, чтобы сбросить текущий POST запрос. В случае провала сохранения, ошибки будут отражены на форме комментария.

controllers/ArticleController.js

...
actionView () {
  this.getModel(model => {
    let Comment = require('../models/Comment');
    let comment = new Comment;
    if (this.isPost()) {
      comment.load(this.getBodyParams());
      comment.set('articleId', model.getId());
      comment.set('ip', this.req.ip);
      comment.save(err => {
        if (err) {
          return this.throwError(err);
        }
        if (comment.hasError()) {
          this.view(model, comment);
        } else {
          this.setFlash('comment-done', 'You message has been sent successfully!');
          this.redirect(['view', model]);
        }
      });
    } else {
      this.view(model, comment);
    }
  }, 'mainPhoto', 'photos', 'tags');
}
...

Метод view отображает отдельную статью, указанную в аргументе model. Комментарии, относящихся к статье, выводятся через ActiveDataProvider, который формирует постраничное разбиение списка.

controllers/ArticleController.js

...
view (model, comment) {
  let comments = new ActiveDataProvider({
    controller: this,
    query: model.relComments()
  });
  comments.prepare(err => {
    err ? this.throwError(err) : this.render('view', {model, comments, comment});
  });
}
...

Метод actionTagged выводит список статей отфильтрованных по метке.

Сначала нужно найти модель метки по ее названию, которое передается в параметре tag GET-запроса. Создайте новую модель Tag, установите атрибут name. Проверьте полученные данные. В случае отсутствия ошибок, создайте запрос поиска метки по имени. Если метка найдена, то инициализируйте провайдер ActiveDataProvider, где в качестве источника данных query укажите отношение tag.relArticles(), возвращающее все статьи с данной меткой.

controllers/ArticleController.js

...
actionTagged () {
  let tagName = this.getQueryParam('tag');  
  let Tag = require('../models/Tag');
  let tag = new Tag;
  tag.set('name', tagName);
  tag.validate(err => {
    if (err) {
      return this.throwError(err);
    }
    if (tag.hasError()) {
      return this.render('tagged', {tagName});
    }
    Tag.find({name: tagName}).one((err, tag)=> {
      if (err) {
        return this.throwError(err);
      }
      if (!tag) {
        return this.render('tagged', {tagName});
      }
      let provider = new ActiveDataProvider({
        controller: this,
        query: tag.relArticles(),
        sort: {
          attrs: {
            date: true,
            title: true
          },
          defaultOrder: {date: -1}
        }
      });
      provider.prepare(err => {
        err ? this.throwError(err) : this.render('tagged', {provider, tagName});
      });
    });
  });
}
...