#include #include #include "global.h" #include "board.h" #include "xmlrpcinterface.h" #include "fetchtopicsaction.h" #include "forummodel.h" ForumModel::ForumModel(QObject *parent) : QAbstractListModel(parent), _board(0), _forumId(-1), _eof(false) { QHash roles = roleNames(); roles[TitleRole] = QByteArray("title"); roles[IconRole] = QByteArray("icon"); roles[TopicIdRole] = QByteArray("topicId"); roles[NumRepliesRole] = QByteArray("numReplies"); roles[UnreadRole] = QByteArray("unread"); setRoleNames(roles); } Board * ForumModel::board() const { return _board; } void ForumModel::setBoard(Board *board) { if (_board != board) { disconnect(this, SLOT(handleForumTopicsChanged(int,int,int))); disconnect(this, SLOT(handleForumTopicChanged(int,int))); clearModel(); _board = board; if (_board) { connect(_board, SIGNAL(forumTopicsChanged(int,int,int)), SLOT(handleForumTopicsChanged(int,int,int))); connect(_board, SIGNAL(forumTopicChanged(int,int)), SLOT(handleForumTopicChanged(int,int))); if (_forumId >= 0) { update(); reload(); } } emit boardChanged(); } } int ForumModel::forumId() const { return _forumId; } void ForumModel::setForumId(const int id) { if (_forumId != id) { clearModel(); _forumId = id; if (_forumId >= 0 && _board) { update(); reload(); } emit forumIdChanged(); } } int ForumModel::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : _data.size(); } QVariant ForumModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); const int row = index.row(); if (row >= _data.size()) { qWarning() << "Could not seek to" << row; return QVariant(); } switch (role) { case TitleRole: return _data[row].title; case TopicIdRole: return _data[row].topic_id; case NumRepliesRole: return _data[row].num_replies; case UnreadRole: return _data[row].unread; } return QVariant(); } bool ForumModel::canFetchMore(const QModelIndex &parent) const { if (parent.isValid() || !_board || _forumId < 0) return false; // Invalid state return !_eof; } void ForumModel::fetchMore(const QModelIndex &parent) { if (parent.isValid()) return; if (!_board || _forumId < 0) return; if (_eof) return; const int start = _data.size(); QList topics = loadTopics(start, start + FORUM_PAGE_SIZE - 1); const int new_end = start + topics.size() - 1; if (topics.empty()) { // We could not load anything more from DB! _eof = true; } else { beginInsertRows(QModelIndex(), start, new_end); _data.append(topics); _eof = topics.size() < FORUM_PAGE_SIZE; // If short read, we reached EOF. endInsertRows(); } if (_board->service()->isAccessible()) { if (!topics.empty()) { QDateTime last = oldestPostUpdate(topics); // If the topics we got from DB are too old, refresh online. if (last.secsTo(QDateTime::currentDateTimeUtc()) > FORUM_TOPICS_TLL) { qDebug() << "Fetching topics because of old"; Q_ASSERT(new_end >= 0); _board->enqueueAction(new FetchTopicsAction(_forumId, start, new_end, _board)); } } // Try to fetch more topics if board is online and we reached the end of DB if (_eof) { qDebug() << "Fetching topics because of EOF"; _board->enqueueAction(new FetchTopicsAction(_forumId, _data.size(), _data.size() + FORUM_PAGE_SIZE - 1, _board)); } } } void ForumModel::refresh() { // Forcefully refresh all topics on this forum _board->enqueueAction(new FetchTopicsAction(_forumId, 0, FetchTopicsAction::FetchAllTopics, _board)); } QDateTime ForumModel::parseDateTime(const QVariant &v) { QString s = v.toString(); return QDateTime::fromString(s, Qt::ISODate); } QDateTime ForumModel::oldestPostUpdate(const QList &topics) { if (topics.empty()) return QDateTime::currentDateTimeUtc(); QDateTime min = topics.first().last_update_time; foreach (const Topic& topic, topics) { if (min < topic.last_update_time) min = topic.last_update_time; } return min; } QDateTime ForumModel::lastTopPostUpdate() { if (!_board) return QDateTime(); QSqlDatabase db = _board->database(); QSqlQuery query(db); query.prepare("SELECT last_update_time FROM topics " "WHERE forum_id = :forum_id " "ORDER BY position ASC " "LIMIT 1"); query.bindValue(":forum_id", _forumId); if (query.exec()) { if (query.next()) { return parseDateTime(query.value(0)); } } else { qWarning() << "Could not fetch posts:" << query.lastError().text(); } return QDateTime(); } QList ForumModel::loadTopics(int start, int end) { Q_ASSERT(_board); QList topics; QSqlQuery query(_board->database()); query.prepare("SELECT topic_id, topic_title, reply_number, new_post, last_reply_time, last_update_time FROM topics " "WHERE forum_id = :forum_id AND position BETWEEN :start AND :end " "ORDER by position ASC "); query.bindValue(":forum_id", _forumId); query.bindValue(":start", start); query.bindValue(":end", end); if (query.exec()) { topics.reserve(end - start + 1); while (query.next()) { Topic topic; topic.topic_id = query.value(0).toInt(); topic.title = query.value(1).toString(); topic.num_replies = query.value(2).toInt(); topic.unread = query.value(3).toBool(); topic.last_reply_time = parseDateTime(query.value(4)); topic.last_update_time = parseDateTime(query.value(5)); topics.append(topic); } } else { qWarning() << "Could not load topics:" << query.lastError().text(); } return topics; } void ForumModel::clearModel() { beginResetModel(); _eof = false; _data.clear(); endResetModel(); } void ForumModel::handleForumTopicsChanged(int forumId, int start, int end) { if (forumId == _forumId) { // Yep, our topics list changed. qDebug() << "My topics changed" << start << end; if (end > _data.size()) { // If for any reason we have more topics now, it means we might // no longer be EOF... _eof = false; } if (start > _data.size() + 1) { // We are still not interested into these topics. qDebug() << "Topics too far"; return; } QList topics = loadTopics(start, end); if (topics.size() < end - start + 1) { _eof = true; // Short read end = start + topics.size() - 1; if (_data.size() > end + 1) { beginRemoveRows(QModelIndex(), end + 1, _data.size()); while (_data.size() > end + 1) { _data.removeLast(); } endRemoveRows(); } } 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] = topics[i - start]; } for (int i = _data.size(); i <= end; i++) { Q_ASSERT(i >= start); _data.append(topics[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] = topics[i - start]; } emit dataChanged(createIndex(start, 0), createIndex(end, 0)); } } } void ForumModel::handleForumTopicChanged(int forumId, int topicId) { if (forumId == _forumId) {qDebug() << "Me topic cha"; for (int i = 0; i < _data.size(); i++) { Topic& topic = _data[i]; if (topic.topic_id == topicId) {qDebug() << "Me topic cha cha"; // Need to refresh this topic QList topics = loadTopics(i, i); if (topics.size() == 1) { _data[i] = topics[0]; emit dataChanged(createIndex(i, 0), createIndex(i, 0)); } else { qWarning() << "Topic changed yet not in DB"; } } } } } void ForumModel::update() { if (!_board || _forumId < 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::currentDateTimeUtc()) > FORUM_TOP_TLL) { // Outdated or empty, refresh. qDebug() << "Fetching topics because the top are old"; _board->enqueueAction(new FetchTopicsAction(_forumId, 0, FORUM_PAGE_SIZE - 1, _board)); } else { qDebug() << "Topics not outdated"; } } void ForumModel::reload() { Q_ASSERT(_data.empty()); Q_ASSERT(!_eof); // Fetch an initial bunch of topics fetchMore(); }