File indexing completed on 2024-04-28 05:08:14
0001 /*************************************************************************** 0002 Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "collection.h" 0026 #include "field.h" 0027 #include "entry.h" 0028 #include "entrygroup.h" 0029 #include "derivedvalue.h" 0030 #include "fieldformat.h" 0031 #include "utils/string_utils.h" 0032 #include "utils/stringset.h" 0033 #include "entrycomparison.h" 0034 #include "tellico_debug.h" 0035 0036 #include <KLocalizedString> 0037 0038 #include <QDate> 0039 0040 using namespace Tellico; 0041 using Tellico::Data::Collection; 0042 0043 const QString Collection::s_peopleGroupName = QStringLiteral("_people"); 0044 0045 Collection::Collection(const QString& title_) 0046 : QObject(), QSharedData(), m_nextEntryId(1), m_title(title_), m_trackGroups(false) { 0047 m_id = getID(); 0048 } 0049 0050 Collection::Collection(bool addDefaultFields_, const QString& title_) 0051 : QObject(), QSharedData(), m_nextEntryId(1), m_title(title_), m_trackGroups(false) { 0052 if(m_title.isEmpty()) { 0053 m_title = i18n("My Collection"); 0054 } 0055 m_id = getID(); 0056 if(addDefaultFields_) { 0057 addField(Field::createDefaultField(Field::IDField)); 0058 addField(Field::createDefaultField(Field::TitleField)); 0059 addField(Field::createDefaultField(Field::CreatedDateField)); 0060 addField(Field::createDefaultField(Field::ModifiedDateField)); 0061 } 0062 } 0063 0064 Collection::~Collection() { 0065 // maybe we should just call clear() ? 0066 foreach(EntryGroupDict* dict, m_entryGroupDicts) { 0067 qDeleteAll(*dict); 0068 } 0069 qDeleteAll(m_entryGroupDicts); 0070 m_entryGroupDicts.clear(); 0071 } 0072 0073 bool Collection::addFields(Tellico::Data::FieldList list_) { 0074 bool success = true; 0075 foreach(FieldPtr field, list_) { 0076 success &= addField(field); 0077 } 0078 return success; 0079 } 0080 0081 bool Collection::addField(Tellico::Data::FieldPtr field_) { 0082 Q_ASSERT(field_); 0083 if(!field_) { 0084 return false; 0085 } 0086 0087 // this essentially checks for duplicates 0088 if(hasField(field_->name())) { 0089 myDebug() << "replacing" << field_->name() << "in collection" << m_title; 0090 removeField(fieldByName(field_->name()), true); 0091 } 0092 0093 m_fields.append(field_); 0094 m_fieldByName.insert(field_->name(), field_.data()); 0095 m_fieldByTitle.insert(field_->title(), field_.data()); 0096 0097 // always default to using field with title name as title 0098 if(field_->name() == QLatin1String("title")) m_titleField = field_->name(); 0099 0100 if(field_->formatType() == FieldFormat::FormatName) { 0101 m_peopleFields.append(field_); // list of people attributes 0102 if(m_peopleFields.count() > 1) { 0103 // the second time that a person field is added, add a "pseudo-group" for people 0104 if(!m_entryGroupDicts.contains(s_peopleGroupName)) { 0105 EntryGroupDict* d = new EntryGroupDict(); 0106 m_entryGroupDicts.insert(s_peopleGroupName, d); 0107 m_entryGroups.prepend(s_peopleGroupName); 0108 } 0109 } 0110 } else if(m_titleField.isEmpty() && field_->formatType() == FieldFormat::FormatTitle) { 0111 m_titleField = field_->name(); 0112 } 0113 0114 if(field_->type() == Field::Image) { 0115 m_imageFields.append(field_); 0116 } 0117 0118 if(!field_->category().isEmpty() && !m_fieldCategories.contains(field_->category())) { 0119 m_fieldCategories << field_->category(); 0120 } 0121 0122 if(field_->hasFlag(Field::AllowGrouped)) { 0123 // m_entryGroupsDicts autoDeletes each QDict when the Collection d'tor is called 0124 EntryGroupDict* dict = new EntryGroupDict(); 0125 m_entryGroupDicts.insert(field_->name(), dict); 0126 // cache the possible groups of entries 0127 m_entryGroups << field_->name(); 0128 } 0129 0130 if(m_defaultGroupField.isEmpty() && field_->hasFlag(Field::AllowGrouped)) { 0131 m_defaultGroupField = field_->name(); 0132 } 0133 0134 if(field_->hasFlag(Field::Derived)) { 0135 DerivedValue dv(field_); 0136 if(dv.isRecursive(this)) { 0137 field_->setProperty(QStringLiteral("template"), QString()); 0138 } 0139 } 0140 0141 // refresh all dependent fields, in case one references this new one 0142 foreach(FieldPtr existingField, m_fields) { 0143 if(existingField->hasFlag(Field::Derived)) { 0144 emit signalRefreshField(existingField); 0145 } 0146 } 0147 0148 return true; 0149 } 0150 0151 bool Collection::mergeField(Tellico::Data::FieldPtr newField_) { 0152 bool structuralChange = false; 0153 if(!newField_) { 0154 return structuralChange; 0155 } 0156 0157 FieldPtr currField = fieldByName(newField_->name()); 0158 if(!currField) { 0159 // does not exist in current collection, add it 0160 Data::FieldPtr f(new Field(*newField_)); 0161 bool success = addField(f); 0162 if(success) { 0163 emit mergeAddedField(CollPtr(this), f); 0164 } else { 0165 myDebug() << "Failed to add field:" << f->name(); 0166 } 0167 // adding a new field is a structural change to the collection 0168 return success; 0169 } 0170 0171 if(newField_->type() == Field::Table2) { 0172 newField_->setType(Data::Field::Table); 0173 newField_->setProperty(QStringLiteral("columns"), QStringLiteral("2")); 0174 } 0175 0176 // the original field type is kept 0177 if(currField->type() != newField_->type()) { 0178 myDebug() << "skipping, field type mismatch for " << currField->title(); 0179 return structuralChange; 0180 } 0181 0182 // if field is a Choice, then make sure all values are there 0183 if(currField->type() == Field::Choice && currField->allowed() != newField_->allowed()) { 0184 QStringList allowed = currField->allowed(); 0185 const QStringList& newAllowed = newField_->allowed(); 0186 for(QStringList::ConstIterator it = newAllowed.begin(); it != newAllowed.end(); ++it) { 0187 if(!allowed.contains(*it)) { 0188 allowed.append(*it); 0189 structuralChange = true; 0190 } 0191 } 0192 currField->setAllowed(allowed); 0193 } 0194 0195 // don't change original format flags 0196 // don't change original category 0197 // add new description if current is empty 0198 if(currField->description().isEmpty()) { 0199 currField->setDescription(newField_->description()); 0200 } 0201 0202 // if new field has additional extended properties, add those 0203 for(StringMap::const_iterator it = newField_->propertyList().begin(); it != newField_->propertyList().end(); ++it) { 0204 const QString propName = it.key(); 0205 const QString currValue = currField->property(propName); 0206 if(currValue.isEmpty()) { 0207 currField->setProperty(propName, it.value()); 0208 structuralChange = true; 0209 } else if (it.value() != currValue) { 0210 if(currField->type() == Field::URL && propName == QLatin1String("relative")) { 0211 myWarning() << "relative URL property does not match for " << currField->name(); 0212 } else if((currField->type() == Field::Table && propName == QLatin1String("columns")) 0213 || (currField->type() == Field::Rating && propName == QLatin1String("maximum"))) { 0214 bool ok; 0215 uint currNum = Tellico::toUInt(currValue, &ok); 0216 uint newNum = Tellico::toUInt(it.value(), &ok); 0217 if(newNum > currNum) { // bigger values 0218 currField->setProperty(propName, QString::number(newNum)); 0219 structuralChange = true; 0220 } 0221 } else if(currField->type() == Field::Rating && propName == QLatin1String("minimum")) { 0222 bool ok; 0223 uint currNum = Tellico::toUInt(currValue, &ok); 0224 uint newNum = Tellico::toUInt(it.value(), &ok); 0225 if(newNum < currNum) { // smaller values 0226 currField->setProperty(propName, QString::number(newNum)); 0227 structuralChange = true; 0228 } 0229 } 0230 } 0231 if(propName == QLatin1String("template") && currField->hasFlag(Field::Derived)) { 0232 DerivedValue dv(currField); 0233 if(dv.isRecursive(this)) { 0234 currField->setProperty(QStringLiteral("template"), QString()); 0235 } 0236 } 0237 } 0238 0239 // combine flags 0240 currField->setFlags(currField->flags() | newField_->flags()); 0241 return structuralChange; 0242 } 0243 0244 // be really careful with these field pointers, try not to call too many other functions 0245 // which may depend on the field list 0246 bool Collection::modifyField(Tellico::Data::FieldPtr newField_) { 0247 if(!newField_) { 0248 return false; 0249 } 0250 // myDebug() << "; 0251 0252 // the field name never changes 0253 const QString fieldName = newField_->name(); 0254 FieldPtr oldField = fieldByName(fieldName); 0255 if(!oldField) { 0256 myDebug() << "no field named " << fieldName; 0257 return false; 0258 } 0259 0260 // update name dict 0261 m_fieldByName.insert(fieldName, newField_.data()); 0262 0263 // update titles 0264 const QString oldTitle = oldField->title(); 0265 const QString newTitle = newField_->title(); 0266 if(oldTitle == newTitle) { 0267 m_fieldByTitle.insert(newTitle, newField_.data()); 0268 } else { 0269 m_fieldByTitle.remove(oldTitle); 0270 m_fieldByTitle.insert(newTitle, newField_.data()); 0271 } 0272 0273 // now replace the field pointer in the list 0274 int pos = m_fields.indexOf(oldField); 0275 if(pos > -1) { 0276 m_fields.replace(pos, newField_); 0277 } else { 0278 myDebug() << "no index found!"; 0279 return false; 0280 } 0281 0282 // update category list. 0283 if(oldField->category() != newField_->category()) { 0284 m_fieldCategories.clear(); 0285 foreach(FieldPtr it, m_fields) { 0286 // add category if it's not in the list yet 0287 if(!it->category().isEmpty() && !m_fieldCategories.contains(it->category())) { 0288 m_fieldCategories += it->category(); 0289 } 0290 } 0291 } 0292 0293 if(newField_->hasFlag(Field::Derived)) { 0294 DerivedValue dv(newField_); 0295 if(dv.isRecursive(this)) { 0296 newField_->setProperty(QStringLiteral("template"), QString()); 0297 } 0298 } 0299 0300 // keep track of if the entry groups will need to be reset 0301 bool resetGroups = false; 0302 0303 // if format is different, go ahead and invalidate all formatted entry values 0304 if(oldField->formatType() != newField_->formatType()) { 0305 // invalidate cached format strings of all entry attributes of this name 0306 foreach(EntryPtr entry, m_entries) { 0307 entry->invalidateFormattedFieldValue(fieldName); 0308 } 0309 resetGroups = true; 0310 } 0311 0312 // check to see if the people "pseudo-group" needs to be updated 0313 // only if only one of the two is a name 0314 bool wasPeople = oldField->formatType() == FieldFormat::FormatName; 0315 bool isPeople = newField_->formatType() == FieldFormat::FormatName; 0316 if(wasPeople) { 0317 m_peopleFields.removeAll(oldField); 0318 if(!isPeople) { 0319 resetGroups = true; 0320 } 0321 } 0322 if(isPeople) { 0323 // if there's more than one people field and no people dict exists yet, add it 0324 if(m_peopleFields.count() > 1 && !m_entryGroupDicts.contains(s_peopleGroupName)) { 0325 EntryGroupDict* d = new EntryGroupDict(); 0326 m_entryGroupDicts.insert(s_peopleGroupName, d); 0327 // put it at the top of the list 0328 m_entryGroups.prepend(s_peopleGroupName); 0329 } 0330 m_peopleFields.append(newField_); 0331 if(!wasPeople) { 0332 resetGroups = true; 0333 } 0334 } 0335 0336 bool wasGrouped = oldField->hasFlag(Field::AllowGrouped); 0337 bool isGrouped = newField_->hasFlag(Field::AllowGrouped); 0338 if(wasGrouped) { 0339 if(!isGrouped) { 0340 // in order to keep list in the same order, don't remove unless new field is not groupable 0341 m_entryGroups.removeAll(fieldName); 0342 delete m_entryGroupDicts.take(fieldName); // no auto-delete here 0343 myDebug() << "no longer grouped: " << fieldName; 0344 resetGroups = true; 0345 } else { 0346 // don't do this, it wipes out the old groups! 0347 // m_entryGroupDicts.replace(fieldName, new EntryGroupDict()); 0348 } 0349 } else if(isGrouped) { 0350 EntryGroupDict* d = new EntryGroupDict(); 0351 m_entryGroupDicts.insert(fieldName, d); 0352 // cache the possible groups of entries 0353 m_entryGroups << fieldName; 0354 resetGroups = true; 0355 } 0356 0357 if(oldField->type() == Field::Image) { 0358 m_imageFields.removeAll(oldField); 0359 } 0360 if(newField_->type() == Field::Image) { 0361 m_imageFields.append(newField_); 0362 } 0363 0364 if(resetGroups) { 0365 // myLog() << "invalidating groups"; 0366 invalidateGroups(); 0367 } 0368 0369 // now to update all entries if the field is a derived value and the template changed 0370 if(newField_->hasFlag(Field::Derived) && 0371 oldField->property(QStringLiteral("template")) != newField_->property(QStringLiteral("template"))) { 0372 emit signalRefreshField(newField_); 0373 } 0374 0375 return true; 0376 } 0377 0378 bool Collection::removeField(const QString& name_, bool force_) { 0379 return removeField(fieldByName(name_), force_); 0380 } 0381 0382 // force allows me to force the deleting of the title field if I need to 0383 bool Collection::removeField(Tellico::Data::FieldPtr field_, bool force_/*=false*/) { 0384 if(!field_ || !m_fields.contains(field_)) { 0385 if(field_) { 0386 myDebug() << "can't delete field:" << field_->name(); 0387 } 0388 return false; 0389 } 0390 // myDebug() << "name = " << field_->name(); 0391 0392 // can't delete the title field 0393 if((field_->hasFlag(Field::NoDelete)) && !force_) { 0394 return false; 0395 } 0396 0397 foreach(EntryPtr entry, m_entries) { 0398 // setting the fields to an empty string removes the value from the entry's list 0399 entry->setField(field_, QString()); 0400 } 0401 0402 bool success = true; 0403 if(field_->formatType() == FieldFormat::FormatName) { 0404 m_peopleFields.removeAll(field_); 0405 } 0406 0407 if(field_->type() == Field::Image) { 0408 m_imageFields.removeAll(field_); 0409 } 0410 m_fieldByName.remove(field_->name()); 0411 m_fieldByTitle.remove(field_->title()); 0412 0413 if(fieldsByCategory(field_->category()).count() == 1) { 0414 m_fieldCategories.removeAll(field_->category()); 0415 } 0416 0417 if(field_->hasFlag(Field::AllowGrouped)) { 0418 EntryGroupDict* dict = m_entryGroupDicts.take(field_->name()); 0419 qDeleteAll(*dict); 0420 m_entryGroups.removeAll(field_->name()); 0421 if(field_->name() == m_defaultGroupField && !m_entryGroups.isEmpty()) { 0422 setDefaultGroupField(m_entryGroups.first()); 0423 } 0424 } 0425 0426 m_fields.removeAll(field_); 0427 0428 // refresh all dependent fields, rather lazy, but there's 0429 // likely to be weird effects when checking dependent fields 0430 // while removing one, so refresh all of them 0431 foreach(FieldPtr field, m_fields) { 0432 if(field->hasFlag(Field::Derived)) { 0433 emit signalRefreshField(field); 0434 } 0435 } 0436 0437 return success; 0438 } 0439 0440 void Collection::reorderFields(const Tellico::Data::FieldList& list_) { 0441 // assume the lists have the same pointers! 0442 m_fields = list_; 0443 0444 // also reset category list, since the order may have changed 0445 m_fieldCategories.clear(); 0446 foreach(FieldPtr field, m_fields) { 0447 if(!field->category().isEmpty() && !m_fieldCategories.contains(field->category())) { 0448 m_fieldCategories << field->category(); 0449 } 0450 } 0451 } 0452 0453 void Collection::addEntries(const Tellico::Data::EntryList& entries_) { 0454 if(entries_.isEmpty()) { 0455 return; 0456 } 0457 0458 foreach(EntryPtr entry, entries_) { 0459 if(!entry) { 0460 Q_ASSERT(entry); 0461 continue; 0462 } 0463 bool foster = false; 0464 if(this != entry->collection().data()) { 0465 entry->setCollection(CollPtr(this)); 0466 foster = true; 0467 } 0468 0469 m_entries.append(entry); 0470 // myDebug() << "added entry (" << entry->title() << ")" << entry->id(); 0471 0472 if(entry->id() >= m_nextEntryId) { 0473 m_nextEntryId = entry->id() + 1; 0474 } else if(entry->id() == -1) { 0475 entry->setId(m_nextEntryId); 0476 ++m_nextEntryId; 0477 } else if(m_entryById.contains(entry->id())) { 0478 if(!foster) { 0479 myDebug() << "the collection already has an entry with id = " << entry->id(); 0480 } 0481 entry->setId(m_nextEntryId); 0482 ++m_nextEntryId; 0483 } 0484 m_entryById.insert(entry->id(), entry.data()); 0485 0486 if(hasField(QStringLiteral("cdate")) && entry->field(QStringLiteral("cdate")).isEmpty()) { 0487 // use mdate if it exists 0488 QString cdate = entry->field(QStringLiteral("mdate")); 0489 if(cdate.isEmpty()) { 0490 cdate = QDate::currentDate().toString(Qt::ISODate); 0491 } 0492 entry->setField(QStringLiteral("cdate"), cdate, false); 0493 } 0494 if(hasField(QStringLiteral("mdate")) && entry->field(QStringLiteral("mdate")).isEmpty()) { 0495 entry->setField(QStringLiteral("mdate"), QDate::currentDate().toString(Qt::ISODate), false); 0496 } 0497 } 0498 if(m_trackGroups) { 0499 populateCurrentDicts(entries_, fieldNames()); 0500 } 0501 } 0502 0503 void Collection::removeEntriesFromDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) { 0504 QSet<EntryGroup*> modifiedGroups; 0505 foreach(EntryPtr entry, entries_) { 0506 // need a copy of the vector since it gets changed 0507 QList<EntryGroup*> groups = entry->groups(); 0508 foreach(EntryGroup* group, groups) { 0509 // only clear groups for the modified fields, skip the others 0510 // also clear for all derived values, just in case 0511 if(!fields_.contains(group->fieldName()) && hasField(group->fieldName()) && !fieldByName(group->fieldName())->hasFlag(Field::Derived)) { 0512 continue; 0513 } 0514 if(entry->removeFromGroup(group)) { 0515 modifiedGroups.insert(group); 0516 } 0517 if(group->isEmpty() && !m_groupsToDelete.contains(group)) { 0518 m_groupsToDelete.push_back(group); 0519 } 0520 } 0521 } 0522 if(!modifiedGroups.isEmpty()) { 0523 emit signalGroupsModified(CollPtr(this), modifiedGroups.values()); 0524 } 0525 } 0526 0527 // this function gets called whenever an entry is modified. Its purpose is to keep the 0528 // groupDicts current. It first removes the entry from every group to which it belongs, 0529 // then it repopulates the dicts with the entry's fields 0530 void Collection::updateDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) { 0531 if(entries_.isEmpty() || !m_trackGroups) { 0532 return; 0533 } 0534 QStringList modifiedFields = fields_; 0535 if(modifiedFields.isEmpty()) { 0536 // myDebug() << "updating all fields"; 0537 modifiedFields = fieldNames(); 0538 } 0539 removeEntriesFromDicts(entries_, modifiedFields); 0540 populateCurrentDicts(entries_, modifiedFields); 0541 cleanGroups(); 0542 } 0543 0544 bool Collection::removeEntries(const Tellico::Data::EntryList& vec_) { 0545 if(vec_.isEmpty()) { 0546 return false; 0547 } 0548 0549 removeEntriesFromDicts(vec_, fieldNames()); 0550 bool success = true; 0551 foreach(EntryPtr entry, vec_) { 0552 m_entryById.remove(entry->id()); 0553 m_entries.removeAll(entry); 0554 } 0555 cleanGroups(); 0556 return success; 0557 } 0558 0559 Tellico::Data::FieldList Collection::fieldsByCategory(const QString& cat_) { 0560 if(!m_fieldCategories.contains(cat_)) { 0561 myDebug() << cat_ << "' is not in category list"; 0562 } 0563 0564 if(cat_.isEmpty()) { 0565 myDebug() << "empty category!"; 0566 return FieldList(); 0567 } 0568 0569 FieldList list; 0570 foreach(FieldPtr field, m_fields) { 0571 if(field->category() == cat_) { 0572 list.append(field); 0573 } 0574 } 0575 return list; 0576 } 0577 0578 QString Collection::fieldNameByTitle(const QString& title_) const { 0579 if(title_.isEmpty()) { 0580 return QString(); 0581 } 0582 FieldPtr f = fieldByTitle(title_); 0583 if(!f) { // might happen in MainWindow::saveCollectionOptions 0584 return QString(); 0585 } 0586 return f->name(); 0587 } 0588 0589 QStringList Collection::fieldNames() const { 0590 return m_fieldByName.keys(); 0591 } 0592 0593 QStringList Collection::fieldTitles() const { 0594 return m_fieldByTitle.keys(); 0595 } 0596 0597 QString Collection::fieldTitleByName(const QString& name_) const { 0598 if(name_.isEmpty()) { 0599 return QString(); 0600 } 0601 FieldPtr f = fieldByName(name_); 0602 if(!f) { 0603 myWarning() << "no field named " << name_; 0604 return QString(); 0605 } 0606 return f->title(); 0607 } 0608 0609 QStringList Collection::valuesByFieldName(const QString& name_) const { 0610 if(name_.isEmpty()) { 0611 return QStringList(); 0612 } 0613 0614 StringSet values; 0615 foreach(EntryPtr entry, m_entries) { 0616 values.add(FieldFormat::splitValue(entry->field(name_))); 0617 } // end entry loop 0618 0619 return values.values(); 0620 } 0621 0622 Tellico::Data::FieldPtr Collection::fieldByName(const QString& name_) const { 0623 return FieldPtr(m_fieldByName.value(name_)); 0624 } 0625 0626 Tellico::Data::FieldPtr Collection::fieldByTitle(const QString& title_) const { 0627 return FieldPtr(m_fieldByTitle.value(title_)); 0628 } 0629 0630 bool Collection::hasField(const QString& name_) const { 0631 return m_fieldByName.contains(name_); 0632 } 0633 0634 bool Collection::isAllowed(const QString& field_, const QString& value_) const { 0635 // empty string is always allowed 0636 if(value_.isEmpty()) { 0637 return true; 0638 } 0639 0640 // find the field with a name of 'key_' 0641 FieldPtr field = fieldByName(field_); 0642 0643 // if the type is not multiple choice or if value_ is allowed, return true 0644 if(field && (field->type() != Field::Choice || field->allowed().contains(value_))) { 0645 return true; 0646 } 0647 0648 return false; 0649 } 0650 0651 Tellico::Data::EntryGroupDict* Collection::entryGroupDictByName(const QString& name_) { 0652 // myDebug() << name_; 0653 m_lastGroupField = name_; // keep track, even if it's invalid 0654 if(name_.isEmpty() || !m_entryGroupDicts.contains(name_) || m_entries.isEmpty()) { 0655 return nullptr; 0656 } 0657 EntryGroupDict* dict = m_entryGroupDicts.value(name_); 0658 if(dict && dict->isEmpty()) { 0659 const bool b = signalsBlocked(); 0660 // block signals so all the group created/modified signals don't fire 0661 blockSignals(true); 0662 populateDict(dict, name_, m_entries); 0663 blockSignals(b); 0664 } 0665 return dict; 0666 } 0667 0668 void Collection::populateDict(Tellico::Data::EntryGroupDict* dict_, const QString& fieldName_, const Tellico::Data::EntryList& entries_) { 0669 // myDebug() << fieldName_; 0670 Q_ASSERT(dict_); 0671 const bool isBool = hasField(fieldName_) && fieldByName(fieldName_)->type() == Field::Bool; 0672 0673 QSet<EntryGroup*> modifiedGroups; 0674 foreach(EntryPtr entry, entries_) { 0675 const QStringList groups = entryGroupNamesByField(entry, fieldName_); 0676 foreach(QString groupTitle, groups) { // krazy:exclude=foreach 0677 // find the group for this group name 0678 // bool fields use the field title 0679 if(isBool && !groupTitle.isEmpty()) { 0680 groupTitle = fieldTitleByName(fieldName_); 0681 } 0682 EntryGroup* group = dict_->value(groupTitle); 0683 // if the group doesn't exist, create it 0684 if(!group) { 0685 group = new EntryGroup(groupTitle, fieldName_); 0686 dict_->insert(groupTitle, group); 0687 } else if(group->isEmpty()) { 0688 // if it's empty, then it was previously added to the vector of groups to delete 0689 // remove it from that vector now that we're adding to it 0690 m_groupsToDelete.removeOne(group); 0691 } 0692 if(entry->addToGroup(group)) { 0693 modifiedGroups.insert(group); 0694 } 0695 } // end group loop 0696 } // end entry loop 0697 if(!modifiedGroups.isEmpty()) { 0698 emit signalGroupsModified(CollPtr(this), modifiedGroups.values()); 0699 } 0700 } 0701 0702 void Collection::populateCurrentDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) { 0703 if(m_entryGroupDicts.isEmpty()) { 0704 return; 0705 } 0706 0707 // special case when adding an entry to a new empty collection 0708 // there are no existing non-empty groups 0709 bool allEmpty = true; 0710 0711 // iterate over all the possible groupDicts 0712 // for each dict, get the value of that field for the entry 0713 // if multiple values are allowed, split the value and then insert the 0714 // entry pointer into the dict for each value 0715 QHash<QString, EntryGroupDict*>::const_iterator dictIt = m_entryGroupDicts.constBegin(); 0716 for( ; dictIt != m_entryGroupDicts.constEnd(); ++dictIt) { 0717 // skip dicts for fields not in the modified list 0718 if(!fields_.contains(dictIt.key())) { 0719 continue; 0720 } 0721 // only populate if it's not empty, since they are 0722 // populated on demand 0723 if(!dictIt.value()->isEmpty()) { 0724 populateDict(dictIt.value(), dictIt.key(), entries_); 0725 allEmpty = false; 0726 } 0727 } 0728 0729 if(allEmpty) { 0730 // myDebug() << "all collection dicts are empty"; 0731 // still need to populate the current group dict 0732 EntryGroupDict* dict = m_entryGroupDicts.value(m_lastGroupField); 0733 if(dict) { 0734 populateDict(dict, m_lastGroupField, entries_); 0735 } 0736 } 0737 } 0738 0739 // return a string list for all the groups that the entry belongs to 0740 // for a given field. Normally, this would just be splitting the entry's value 0741 // for the field, but if the field name is the people pseudo-group, then it gets 0742 // a bit more complicated 0743 QStringList Collection::entryGroupNamesByField(Tellico::Data::EntryPtr entry_, const QString& fieldName_) { 0744 if(fieldName_ != s_peopleGroupName) { 0745 return entry_->groupNamesByFieldName(fieldName_); 0746 } 0747 0748 // the empty group is only returned if the entry has an empty list for every people field 0749 bool allEmpty = true; 0750 StringSet values; 0751 foreach(FieldPtr field, m_peopleFields) { 0752 const QStringList groups = entry_->groupNamesByFieldName(field->name()); 0753 if(allEmpty && (groups.count() != 1 || !groups.at(0).isEmpty())) { 0754 allEmpty = false; 0755 } 0756 values.add(groups); 0757 } 0758 if(!allEmpty) { 0759 // we don't want the empty string 0760 values.remove(QString()); 0761 } 0762 return values.values(); 0763 } 0764 0765 void Collection::invalidateGroups() { 0766 foreach(EntryGroupDict* dict, m_entryGroupDicts) { 0767 qDeleteAll(*dict); 0768 dict->clear(); 0769 // don't delete the dict, just clear it 0770 } 0771 0772 // populateDicts() will make signals that the group view is connected to, block those 0773 blockSignals(true); 0774 foreach(EntryPtr entry, m_entries) { 0775 entry->invalidateFormattedFieldValue(); 0776 entry->clearGroups(); 0777 } 0778 blockSignals(false); 0779 } 0780 0781 Tellico::Data::EntryPtr Collection::entryById(Data::ID id_) { 0782 return EntryPtr(m_entryById.value(id_)); 0783 } 0784 0785 void Collection::addBorrower(Tellico::Data::BorrowerPtr borrower_) { 0786 if(!borrower_) { 0787 return; 0788 } 0789 // check against existing borrower uid 0790 BorrowerPtr existingBorrower; 0791 foreach(BorrowerPtr bor, m_borrowers) { 0792 if(bor->uid() == borrower_->uid()) { 0793 existingBorrower = bor; 0794 break; 0795 } 0796 } 0797 if(!existingBorrower) { 0798 m_borrowers.append(borrower_); 0799 } else if(existingBorrower != borrower_) { 0800 // need to merge loans 0801 QHash<QString, LoanPtr> existingLoans; 0802 foreach(LoanPtr loan, existingBorrower->loans()) { 0803 existingLoans.insert(loan->uid(), loan); 0804 } 0805 foreach(LoanPtr loan, borrower_->loans()) { 0806 if(!existingLoans.contains(loan->uid())) { 0807 existingBorrower->addLoan(loan); 0808 } 0809 } 0810 } 0811 } 0812 0813 void Collection::addFilter(Tellico::FilterPtr filter_) { 0814 if(!filter_) { 0815 return; 0816 } 0817 0818 m_filters.append(filter_); 0819 } 0820 0821 bool Collection::removeFilter(Tellico::FilterPtr filter_) { 0822 if(!filter_) { 0823 return false; 0824 } 0825 0826 return m_filters.removeAll(filter_) > 0; 0827 } 0828 0829 void Collection::clear() { 0830 // since the collection holds a pointer to each entry and each entry 0831 // hold a pointer to the collection, and they're both sharedptrs, 0832 // neither will ever get deleted, unless the collection removes 0833 // all held pointers, specifically to entries 0834 m_fields.clear(); 0835 m_peopleFields.clear(); 0836 m_imageFields.clear(); 0837 m_fieldCategories.clear(); 0838 m_fieldByName.clear(); 0839 m_fieldByTitle.clear(); 0840 m_defaultGroupField.clear(); 0841 0842 m_entries.clear(); 0843 m_entryById.clear(); 0844 foreach(EntryGroupDict* dict, m_entryGroupDicts) { 0845 qDeleteAll(*dict); 0846 } 0847 qDeleteAll(m_entryGroupDicts); 0848 m_entryGroupDicts.clear(); 0849 m_entryGroups.clear(); 0850 m_groupsToDelete.clear(); 0851 m_filters.clear(); 0852 m_borrowers.clear(); 0853 } 0854 0855 void Collection::cleanGroups() { 0856 foreach(EntryGroup* group, m_groupsToDelete) { 0857 EntryGroupDict* dict = entryGroupDictByName(group->fieldName()); 0858 if(!dict) { 0859 continue; 0860 } 0861 EntryGroup* groupToDelete = dict->take(group->groupName()); 0862 delete groupToDelete; 0863 } 0864 m_groupsToDelete.clear(); 0865 } 0866 0867 QString Collection::prepareText(const QString& text_) const { 0868 return text_; 0869 } 0870 0871 int Collection::sameEntry(Tellico::Data::EntryPtr entry1_, Tellico::Data::EntryPtr entry2_) const { 0872 if(!entry1_ || !entry2_) { 0873 return 0; 0874 } 0875 // used to just return 0, but we really want a default generic implementation 0876 // that specific collections can override. 0877 0878 int res = 0; 0879 // start with twice the title score 0880 // and since the minimum is > 10, then need more than just a perfect title match 0881 res += EntryComparison::MATCH_WEIGHT_MED*EntryComparison::score(entry1_, entry2_, QStringLiteral("title"), this); 0882 // then add score for each field 0883 foreach(FieldPtr field, entry1_->collection()->fields()) { 0884 // skip title field and personal category 0885 if(field->name() == QLatin1String("title") || 0886 field->category() == i18n("Personal")) { 0887 continue; 0888 } 0889 // url link is extra 0890 if(field->type() == Field::URL) { 0891 res += EntryComparison::MATCH_WEIGHT_MED*EntryComparison::score(entry1_, entry2_, field->name(), this); 0892 } else { 0893 res += EntryComparison::MATCH_WEIGHT_LOW*EntryComparison::score(entry1_, entry2_, field->name(), this); 0894 } 0895 if(res >= EntryComparison::ENTRY_PERFECT_MATCH) return res; 0896 } 0897 return res; 0898 } 0899 0900 Tellico::Data::ID Collection::getID() { 0901 static ID id = 0; 0902 return ++id; 0903 } 0904 0905 Data::FieldPtr Collection::primaryImageField() const { 0906 return m_imageFields.isEmpty() ? Data::FieldPtr() : fieldByName(m_imageFields.front()->name()); 0907 } 0908 0909 QString Collection::titleField() const { 0910 return m_titleField.isEmpty() 0911 ? (m_fields.isEmpty() ? QString() : m_fields.at(0)->name()) 0912 : m_titleField; 0913 }