#include #include #include "global.h" #include "board.h" #include "xmlrpcinterface.h" #include "fetchpostsaction.h" #include "topicmodel.h" TopicModel::TopicModel(QObject *parent) : QAbstractListModel(parent), _boardUrl(), _board(0), _topicId(-1) { QHash roles = roleNames(); roles[TitleRole] = QByteArray("title"); roles[ContentRole] = QByteArray("content"); roles[IconRole] = QByteArray("icon"); roles[PostIdRole] = QByteArray("postId"); roles[UserIdRole] = QByteArray("userId"); roles[UserNameRole] = QByteArray("userName"); roles[DateTimeRole] = QByteArray("dateTime"); roles[HumanDateRole] = QByteArray("humanDate"); roles[HumanTimeRole] = QByteArray("humanTime"); setRoleNames(roles); } QString TopicModel::boardUrl() const { return _boardUrl; } void TopicModel::setBoardUrl(const QString &url) { if (_boardUrl != url) { disconnect(this, SLOT(handleTopicPostsChanged(int,int,int))); clearModel(); _board = 0; _boardUrl = url; if (!_boardUrl.isEmpty()) { _board = board_manager->getBoard(_boardUrl); connect(_board, SIGNAL(topicPostsChanged(int,int,int)), SLOT(handleTopicPostsChanged(int,int,int))); if (_topicId >= 0) { update(); reload(); } } emit boardUrlChanged(); } } int TopicModel::topicId() const { return _topicId; } void TopicModel::setTopicId(const int id) { if (_topicId != id) { clearModel(); _topicId = id; if (_topicId >= 0 && _board) { update(); reload(); } emit topicIdChanged(); } } int TopicModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : _data.size(); } QVariant TopicModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (!_board) return QVariant(); const int row = index.row(); if (row >= _data.size()) { qWarning() << "Could not seek to" << row; return QVariant(); } switch (role) { // Mind the lack of break statements case TitleRole: return _data[row].title; case ContentRole: return _data[row].content; case PostIdRole: return _data[row].post_id; case UserIdRole: return _data[row].user_id; case UserNameRole: return _data[row].user_name; case DateTimeRole: return _data[row].time; case HumanDateRole: return _board->renderHumanDate(_data[row].time); case HumanTimeRole: return _board->renderHumanTime(_data[row].time); } return QVariant(); } bool TopicModel::canFetchMore(const QModelIndex &parent) const { if (parent.isValid() || !_board) return false; // Invalid state return !_eof; } void TopicModel::fetchMore(const QModelIndex &parent) { if (parent.isValid()) return; if (!_board) return; if (_eof) return; const int start = _data.size(); QList posts = loadPosts(start, start + TOPIC_PAGE_SIZE - 1); const int new_end = start + _data.size() - 1; if (posts.empty()) { // We could not load anything more from DB! _eof = true; } else { beginInsertRows(QModelIndex(), start, new_end); _data.append(posts); _eof = posts.size() < TOPIC_PAGE_SIZE; // If short read, we reached EOF. endInsertRows(); } if (_board->service()->isAccessible()) { if (!_data.empty()) { QDateTime last = oldestPostUpdate(posts); // If the posts we got from DB are too old, refresh online. if (last.secsTo(QDateTime::currentDateTime()) > TOPIC_POSTS_TLL) { qDebug() << "Fetching posts because of old"; _board->enqueueAction(new FetchPostsAction(_topicId, start, new_end, _board)); } } // Try to fetch more posts if board is online and we reached the end of DB if (_eof) { qDebug() << "Fetching posts because of EOF"; _board->enqueueAction(new FetchPostsAction(_topicId, _data.size(), _data.size() + FORUM_PAGE_SIZE - 1, _board)); } } } QDateTime TopicModel::parseDbDateTime(const QVariant &v) { QString s = v.toString(); return QDateTime::fromString(s, Qt::ISODate); } QDateTime TopicModel::oldestPostUpdate(const QList &posts) { if (posts.empty()) return QDateTime::currentDateTime(); QDateTime min = posts.first().last_update_time; foreach (const Post& post, posts) { if (min < post.last_update_time) min = post.last_update_time; } return min; } QDateTime TopicModel::lastTopPostUpdate() { if (!_board) return QDateTime(); QSqlDatabase db = _board->database(); QSqlQuery query(db); query.prepare("SELECT last_update_time FROM posts " "WHERE topic_id = :topic_id " "ORDER BY post_time ASC " "LIMIT 1"); query.bindValue(":topic_id", _topicId); if (query.exec()) { if (query.next()) { return parseDbDateTime(query.value(0)); } } else { qWarning() << "Could not load top posts:" << query.lastError().text(); } return QDateTime(); } QList TopicModel::loadPosts(int start, int end) { Q_ASSERT(_board); const int rows = end - start + 1; QList posts; QSqlQuery query(_board->database()); query.prepare("SELECT post_id, post_title, post_content, post_author_id, post_author_name, post_time, last_update_time FROM posts " "WHERE topic_id = :topic_id " "ORDER by post_time ASC " "LIMIT :start, :limit"); query.bindValue(":topic_id", _topicId); query.bindValue(":start", start); query.bindValue(":limit", rows); if (query.exec()) { posts.reserve(rows); while (query.next()) { Post post; post.post_id = query.value(0).toInt(); post.title = query.value(1).toString(); post.content = query.value(2).toString(); post.user_id = query.value(3).toInt(); post.user_name = query.value(4).toString(); post.time = parseDbDateTime(query.value(5)); post.last_update_time = parseDbDateTime(query.value(6)); posts.append(post); } } else { qWarning() << "Could not load posts:" << query.lastError().text(); } return posts; } void TopicModel::clearModel() { beginResetModel(); _eof = false; _data.clear(); endResetModel(); } void TopicModel::handleTopicPostsChanged(int topicId, int start, int end) { if (topicId == _topicId) { // Yep, our posts list changed. qDebug() << "My posts changed" << start << end; if (end > _data.size()) { // If for any reason we have more posts now, it means we might // no longer be EOF... _eof = false; } if (start > _data.size() + 1) { // We are still not interested into these posts. qDebug() << "Posts too far"; return; } QList posts = loadPosts(start, end); if (posts.size() < end - start + 1) { _eof = true; // Short read end = start + posts.size() - 1; } if (end >= _data.size()) { qDebug() << "Call insert rows" << _data.size() << end; beginInsertRows(QModelIndex(), _data.size(), end); _data.reserve(end + 1); for (int i = start; i < _data.size(); i++) { _data[i] = posts[i - start]; } for (int i = _data.size(); i <= end; i++) { Q_ASSERT(i >= start); _data.append(posts[i - start]); } endInsertRows(); emit dataChanged(createIndex(start, 0), createIndex(_data.size() - 1, 0)); } else { qDebug() << "Just refresh the data"; for (int i = start; i < end; i++) { _data[i] = posts[i - start]; } emit dataChanged(createIndex(start, 0), createIndex(end, 0)); } } } void TopicModel::update() { if (!_board || _topicId < 0) return; // Start by requesting an update of the first 20 topics if (!_board->service()->isAccessible()) return; QDateTime last = lastTopPostUpdate(); if (!last.isValid() || last.secsTo(QDateTime::currentDateTime()) > TOPIC_TOP_TLL) { // Outdated or empty, refresh. _board->enqueueAction(new FetchPostsAction(_topicId, 0, TOPIC_PAGE_SIZE - 1, _board)); } else { qDebug() << "Topics not outdated"; } } void TopicModel::reload() { Q_ASSERT(_data.empty()); Q_ASSERT(!_eof); fetchMore(); }