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 }