File indexing completed on 2024-04-28 16:31:51

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