#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), _numAnnouncements(0), _numSticky(0), _eof(false) { QHash roles = roleNames(); roles[TitleRole] = QByteArray("title"); roles[IconRole] = QByteArray("icon"); roles[TopicIdRole] = QByteArray("topicId"); roles[TopicTypeRole] = QByteArray("topicType"); roles[LastReplyTimeRole] = QByteArray("lastReplyTime"); roles[NumRepliesRole] = QByteArray("numReplies"); roles[RelativeDateRole] = QByteArray("relativeDate"); 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,Board::TopicType,int,int))); disconnect(this, SLOT(handleForumTopicChanged(int,int))); clearModel(); _board = board; if (_board) { connect(_board, SIGNAL(forumTopicsChanged(int,Board::TopicType,int,int)), SLOT(handleForumTopicsChanged(int,Board::TopicType,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: if (_data[row].last_update_time.secsTo(QDateTime::currentDateTimeUtc()) > FORUM_TOPICS_TLL) { fetchTopic(row); } return _data[row].title; case TopicIdRole: return _data[row].topic_id; case TopicTypeRole: return _data[row].type; case LastReplyTimeRole: return _data[row].last_reply_time; case NumRepliesRole: return _data[row].num_replies; case RelativeDateRole: switch (_data[row].type) { case Board::Announcement: return tr("Announcement"); case Board::Sticky: return tr("Sticky"); default: return _board->formatDateTime(_data[row].last_reply_time, Board::RelativeDate); } 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 normal_offset = _numAnnouncements + _numSticky; const int normal_start = _data.size() - normal_offset; QList topics = loadTopics(Board::Normal, normal_start, normal_start + FORUM_PAGE_SIZE - 1); const int normal_new_end = normal_start + topics.size() - 1; if (topics.empty()) { // We could not load anything more from DB! _eof = true; } else { qDebug() << "Insert rows" << normal_offset + normal_start << normal_offset + normal_new_end; beginInsertRows(QModelIndex(), normal_offset + normal_start, normal_offset + normal_new_end); _data.append(topics); _eof = topics.size() < FORUM_PAGE_SIZE; // If short read, we reached EOF. endInsertRows(); } if (_board->service()->isAccessible() && _eof) { // Try to fetch more topics if board is online and we reached the end of DB qDebug() << "Fetching topics because of EOF"; const int normal_cur_end = _data.size() - 1 - normal_offset; _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Normal, normal_cur_end + 1, normal_cur_end + FORUM_PAGE_SIZE, _board)); } } void ForumModel::refresh() { // Forcefully refresh all topics on this forum _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Announcement, 0, FetchTopicsAction::FetchAllTopics, _board)); _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Sticky, 0, FetchTopicsAction::FetchAllTopics, _board)); _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Normal, 0, FetchTopicsAction::FetchAllTopics, _board)); } QDateTime ForumModel::parseDateTime(const QVariant &v) { QString s = v.toString(); QDateTime dt = QDateTime::fromString(s, Qt::ISODate); dt.setTimeSpec(Qt::UTC); return dt; } 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 MIN(last_update_time) FROM topics " "WHERE forum_id = :forum_id AND position = 0"); 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(Board::TopicType type, int start, int end) { Q_ASSERT(_board); QList topics; QSqlQuery query(_board->database()); query.prepare("SELECT topic_id, topic_type, topic_title, reply_number, new_post, last_reply_time, last_update_time FROM topics " "WHERE forum_id = :forum_id AND topic_type = :topic_type AND position BETWEEN :start AND :end " "ORDER by position ASC "); query.bindValue(":forum_id", _forumId); query.bindValue(":topic_type", type); 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.type = type; topic.title = query.value(2).toString(); topic.num_replies = query.value(3).toInt(); topic.unread = query.value(4).toBool(); topic.last_reply_time = parseDateTime(query.value(5)); topic.last_update_time = parseDateTime(query.value(6)); topics.append(topic); } } else { qWarning() << "Could not load topics:" << query.lastError().text(); } return topics; } void ForumModel::fetchTopic(int position) const { if (_board->service()->isAccessible()) { // There are less posts on the DB than we wanted qDebug() << "Fetching topic" << position << "because of unfetched/old"; // Figure out the topic type Board::TopicType type; if (position < _numAnnouncements) { type = Board::Announcement; } else if (position < _numAnnouncements + _numSticky) { type = Board::Sticky; position -= _numAnnouncements; } else { type = Board::Normal; position -= _numAnnouncements + _numSticky; } // Always fetch one page at least. int start = (position / FORUM_PAGE_SIZE) * FORUM_PAGE_SIZE; int end = (start + FORUM_PAGE_SIZE) - 1; qDebug() << "From" << start << "to" << end; _board->enqueueAction(new FetchTopicsAction(_forumId, type, start, end, _board)); } } void ForumModel::clearModel() { beginResetModel(); _eof = false; _data.clear(); endResetModel(); } void ForumModel::handleForumTopicsChanged(int forumId, Board::TopicType type, int start, int end) { if (forumId != _forumId) { return; // Not our topics } // TODO This probably can be simplified quite a lot. if (type == Board::Normal) { // Yep, our normal topics list changed. const int normal_offset = _numAnnouncements + _numSticky; int current_normal_end = _data.size() - 1 - normal_offset; if (end >= current_normal_end) { // If for any reason we have more topics now, it means we might // no longer be EOF... _eof = false; } if (start > current_normal_end + 1) { // On the other hand, if the topics are too far away, // do not load them until we fetchMore(). qDebug() << "Topics too far"; return; } const int requested = end - start + 1; QList topics = loadTopics(Board::Normal, start, end); end = start + topics.size() - 1; // Less topics than the number of topics we requested? if (topics.size() < requested) { _eof = true; // Short read if (current_normal_end > end) { const int current_full_end = _data.size() - 1; const int new_full_end = end + normal_offset; Q_ASSERT(new_full_end < current_full_end); qDebug() << "Remove rows" << new_full_end << current_full_end; beginRemoveRows(QModelIndex(), new_full_end, current_full_end); while (_data.size() > new_full_end) { _data.removeLast(); } current_normal_end = _data.size() - 1 - normal_offset; Q_ASSERT(current_normal_end == end); endRemoveRows(); } } Q_ASSERT(end == start + topics.size() - 1); if (end > current_normal_end) { // More topics than we currently have in the model const int current_full_end = _data.size() - 1; const int new_full_end = end + normal_offset; qDebug() << "Insert rows" << current_full_end + 1 << new_full_end; beginInsertRows(QModelIndex(), current_full_end + 1, new_full_end); _data.reserve(new_full_end + 1); const int full_start = normal_offset + start; Q_ASSERT(current_full_end + 1 >= full_start); for (int i = current_full_end + 1; i <= new_full_end; i++) { _data.append(topics[i - full_start]); } endInsertRows(); Q_ASSERT(new_full_end == _data.size() - 1); end = current_normal_end; } Q_ASSERT(end <= start + topics.size() - 1); const int full_start = normal_offset + start; const int full_end = normal_offset + end; for (int i = full_start; i < full_end; i++) { _data[i] = topics[i - full_start]; } qDebug() << "Changed rows" << full_start << full_end; emit dataChanged(createIndex(full_start, 0), createIndex(full_end, 0)); } else if (type == Board::Sticky || type == Board::Announcement) { // We just reload these fully QList topics = loadTopics(type, 0, MAX_FORUM_PAGE_SIZE - 1); const int offset = type == Board::Sticky ? _numAnnouncements : 0; int * const cur_number_p = type == Board::Sticky ? &_numSticky : &_numAnnouncements; const int cur_number = *cur_number_p; if (topics.size() < cur_number) { const int cur_end = offset + cur_number - 1; const int new_end = offset + topics.size() - 1; qDebug() << "Remove rows" << new_end + 1 << cur_end; beginRemoveRows(QModelIndex(), new_end + 1, cur_end); for (int i = new_end + 1; i <= cur_end; i++) { _data.removeAt(i); (*cur_number_p)--; } endRemoveRows(); } else if (topics.size() > cur_number) { const int cur_end = offset + cur_number - 1; const int new_end = offset + topics.size() - 1; const int new_start = cur_end + 1; Q_ASSERT(new_end - offset == topics.size() - 1); qDebug() << "Insert rows" << new_start << new_end; beginInsertRows(QModelIndex(), new_start, new_end); for (int i = new_start; i <= new_end; i++) { _data.insert(i, topics.takeAt(new_start - offset)); (*cur_number_p)++; } endInsertRows(); Q_ASSERT(topics.size() == cur_number); } const int start = offset; const int end = offset + topics.size() - 1; if (end >= start) { for (int i = start; i <= end; i++) { _data.insert(i, topics.at(i - offset)); } qDebug() << "Changed rows" << start << end; emit dataChanged(createIndex(start, 0), createIndex(end, 0)); } } } void ForumModel::handleForumTopicChanged(int forumId, int topicId) { if (forumId == _forumId) { Board::TopicType type = Board::Announcement; int pos = 0; for (int i = 0; i < _data.size(); i++) { Topic& topic = _data[i]; if (topic.type != type) { type = topic.type; pos = 0; } if (topic.topic_id == topicId) { // Need to refresh this topic QList topics = loadTopics(type, pos, pos); if (topics.size() == 1) { _data[i] = topics[0]; emit dataChanged(createIndex(i, 0), createIndex(i, 0)); } else { qWarning() << "Topic changed yet not in DB"; } } pos++; } } } 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, Board::Announcement, 0, FORUM_PAGE_SIZE - 1, _board)); _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Sticky, 0, FORUM_PAGE_SIZE - 1, _board)); _board->enqueueAction(new FetchTopicsAction(_forumId, Board::Normal, 0, FORUM_PAGE_SIZE - 1, _board)); } else { qDebug() << "Topics not outdated"; } } void ForumModel::reload() { Q_ASSERT(_data.empty()); Q_ASSERT(!_eof); // Load announcements / sticky topics fully QList topics; topics = loadTopics(Board::Announcement, 0, MAX_FORUM_PAGE_SIZE - 1); if (!topics.empty()) { qDebug() << "Insert rows" << 0 << topics.size() - 1; beginInsertRows(QModelIndex(), 0, topics.size() - 1); _data.append(topics); _numAnnouncements = topics.size(); endInsertRows(); } else { _numAnnouncements = 0; } topics = loadTopics(Board::Sticky, 0, MAX_FORUM_PAGE_SIZE - 1); if (!topics.empty()) { qDebug() << "Insert rows" << _numAnnouncements << _numAnnouncements + topics.size() - 1; beginInsertRows(QModelIndex(), _numAnnouncements, _numAnnouncements + topics.size() - 1); _data.append(topics); _numSticky = topics.size(); endInsertRows(); } else { _numSticky = 0; } // Fetch an initial bunch of normal topics fetchMore(); }