1 /***************************************************************************
2     Copyright (C) 2001-2009 Robby Stephenson <robby@periapsis.org>
3  ***************************************************************************/
4 
5 /***************************************************************************
6  *                                                                         *
7  *   This program is free software; you can redistribute it and/or         *
8  *   modify it under the terms of the GNU General Public License as        *
9  *   published by the Free Software Foundation; either version 2 of        *
10  *   the License or (at your option) version 3 or any later version        *
11  *   accepted by the membership of KDE e.V. (or its successor approved     *
12  *   by the membership of KDE e.V.), which shall act as a proxy            *
13  *   defined in Section 14 of version 3 of the license.                    *
14  *                                                                         *
15  *   This program is distributed in the hope that it will be useful,       *
16  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
17  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
18  *   GNU General Public License for more details.                          *
19  *                                                                         *
20  *   You should have received a copy of the GNU General Public License     *
21  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
22  *                                                                         *
23  ***************************************************************************/
24 
25 #include "collection.h"
26 #include "field.h"
27 #include "entry.h"
28 #include "entrygroup.h"
29 #include "derivedvalue.h"
30 #include "fieldformat.h"
31 #include "utils/string_utils.h"
32 #include "utils/stringset.h"
33 #include "entrycomparison.h"
34 #include "tellico_debug.h"
35 
36 #include <KLocalizedString>
37 
38 using namespace Tellico;
39 using Tellico::Data::Collection;
40 
41 const QString Collection::s_peopleGroupName = QStringLiteral("_people");
42 
Collection(const QString & title_)43 Collection::Collection(const QString& title_)
44     : QObject(), QSharedData(), m_nextEntryId(1), m_title(title_), m_trackGroups(false) {
45   m_id = getID();
46 }
47 
Collection(bool addDefaultFields_,const QString & title_)48 Collection::Collection(bool addDefaultFields_, const QString& title_)
49     : QObject(), QSharedData(), m_nextEntryId(1), m_title(title_), m_trackGroups(false) {
50   if(m_title.isEmpty()) {
51     m_title = i18n("My Collection");
52   }
53   m_id = getID();
54   if(addDefaultFields_) {
55     addField(Field::createDefaultField(Field::IDField));
56     addField(Field::createDefaultField(Field::TitleField));
57     addField(Field::createDefaultField(Field::CreatedDateField));
58     addField(Field::createDefaultField(Field::ModifiedDateField));
59   }
60 }
61 
~Collection()62 Collection::~Collection() {
63   // maybe we should just call clear() ?
64   foreach(EntryGroupDict* dict, m_entryGroupDicts) {
65     qDeleteAll(*dict);
66   }
67   qDeleteAll(m_entryGroupDicts);
68   m_entryGroupDicts.clear();
69 }
70 
addFields(Tellico::Data::FieldList list_)71 bool Collection::addFields(Tellico::Data::FieldList list_) {
72   bool success = true;
73   foreach(FieldPtr field, list_) {
74     success &= addField(field);
75   }
76   return success;
77 }
78 
addField(Tellico::Data::FieldPtr field_)79 bool Collection::addField(Tellico::Data::FieldPtr field_) {
80   Q_ASSERT(field_);
81   if(!field_) {
82     return false;
83   }
84 
85   // this essentially checks for duplicates
86   if(hasField(field_->name())) {
87     myDebug() << "replacing" << field_->name() << "in collection" << m_title;
88     removeField(fieldByName(field_->name()), true);
89   }
90 
91   m_fields.append(field_);
92   m_fieldByName.insert(field_->name(), field_.data());
93   m_fieldByTitle.insert(field_->title(), field_.data());
94 
95   if(field_->formatType() == FieldFormat::FormatName) {
96     m_peopleFields.append(field_); // list of people attributes
97     if(m_peopleFields.count() > 1) {
98       // the second time that a person field is added, add a "pseudo-group" for people
99       if(!m_entryGroupDicts.contains(s_peopleGroupName)) {
100         EntryGroupDict* d = new EntryGroupDict();
101         m_entryGroupDicts.insert(s_peopleGroupName, d);
102         m_entryGroups.prepend(s_peopleGroupName);
103       }
104     }
105   }
106 
107   if(field_->type() == Field::Image) {
108     m_imageFields.append(field_);
109   }
110 
111   if(!field_->category().isEmpty() && !m_fieldCategories.contains(field_->category())) {
112     m_fieldCategories << field_->category();
113   }
114 
115   if(field_->hasFlag(Field::AllowGrouped)) {
116     // m_entryGroupsDicts autoDeletes each QDict when the Collection d'tor is called
117     EntryGroupDict* dict = new EntryGroupDict();
118     m_entryGroupDicts.insert(field_->name(), dict);
119     // cache the possible groups of entries
120     m_entryGroups << field_->name();
121   }
122 
123   if(m_defaultGroupField.isEmpty() && field_->hasFlag(Field::AllowGrouped)) {
124     m_defaultGroupField = field_->name();
125   }
126 
127   if(field_->hasFlag(Field::Derived)) {
128     DerivedValue dv(field_);
129     if(dv.isRecursive(this)) {
130       field_->setProperty(QStringLiteral("template"), QString());
131     }
132   }
133 
134   // refresh all dependent fields, in case one references this new one
135   foreach(FieldPtr existingField, m_fields) {
136     if(existingField->hasFlag(Field::Derived)) {
137       emit signalRefreshField(existingField);
138     }
139   }
140 
141   return true;
142 }
143 
mergeField(Tellico::Data::FieldPtr newField_)144 bool Collection::mergeField(Tellico::Data::FieldPtr newField_) {
145   if(!newField_) {
146     return false;
147   }
148 
149   FieldPtr currField = fieldByName(newField_->name());
150   if(!currField) {
151     // does not exist in current collection, add it
152     Data::FieldPtr f(new Field(*newField_));
153     bool success = addField(f);
154     emit mergeAddedField(CollPtr(this), f);
155     return success;
156   }
157 
158   if(newField_->type() == Field::Table2) {
159     newField_->setType(Data::Field::Table);
160     newField_->setProperty(QStringLiteral("columns"), QStringLiteral("2"));
161   }
162 
163   // the original field type is kept
164   if(currField->type() != newField_->type()) {
165     myDebug() << "skipping, field type mismatch for " << currField->title();
166     return false;
167   }
168 
169   // if field is a Choice, then make sure all values are there
170   if(currField->type() == Field::Choice && currField->allowed() != newField_->allowed()) {
171     QStringList allowed = currField->allowed();
172     const QStringList& newAllowed = newField_->allowed();
173     for(QStringList::ConstIterator it = newAllowed.begin(); it != newAllowed.end(); ++it) {
174       if(!allowed.contains(*it)) {
175         allowed.append(*it);
176       }
177     }
178     currField->setAllowed(allowed);
179   }
180 
181   // don't change original format flags
182   // don't change original category
183   // add new description if current is empty
184   if(currField->description().isEmpty()) {
185     currField->setDescription(newField_->description());
186   }
187 
188   // if new field has additional extended properties, add those
189   for(StringMap::const_iterator it = newField_->propertyList().begin(); it != newField_->propertyList().end(); ++it) {
190     const QString propName = it.key();
191     const QString currValue = currField->property(propName);
192     if(currValue.isEmpty()) {
193       currField->setProperty(propName, it.value());
194     } else if (it.value() != currValue) {
195       if(currField->type() == Field::URL && propName == QLatin1String("relative")) {
196         myWarning() << "relative URL property does not match for " << currField->name();
197       } else if((currField->type() == Field::Table && propName == QLatin1String("columns"))
198              || (currField->type() == Field::Rating && propName == QLatin1String("maximum"))) {
199         bool ok;
200         uint currNum = Tellico::toUInt(currValue, &ok);
201         uint newNum = Tellico::toUInt(it.value(), &ok);
202         if(newNum > currNum) { // bigger values
203           currField->setProperty(propName, QString::number(newNum));
204         }
205       } else if(currField->type() == Field::Rating && propName == QLatin1String("minimum")) {
206         bool ok;
207         uint currNum = Tellico::toUInt(currValue, &ok);
208         uint newNum = Tellico::toUInt(it.value(), &ok);
209         if(newNum < currNum) { // smaller values
210           currField->setProperty(propName, QString::number(newNum));
211         }
212       }
213     }
214     if(propName == QLatin1String("template") && currField->hasFlag(Field::Derived)) {
215       DerivedValue dv(currField);
216       if(dv.isRecursive(this)) {
217         currField->setProperty(QStringLiteral("template"), QString());
218       }
219     }
220   }
221 
222   // combine flags
223   currField->setFlags(currField->flags() | newField_->flags());
224   return true;
225 }
226 
227 // be really careful with these field pointers, try not to call too many other functions
228 // which may depend on the field list
modifyField(Tellico::Data::FieldPtr newField_)229 bool Collection::modifyField(Tellico::Data::FieldPtr newField_) {
230   if(!newField_) {
231     return false;
232   }
233 //  myDebug() << ";
234 
235 // the field name never changes
236   const QString fieldName = newField_->name();
237   FieldPtr oldField = fieldByName(fieldName);
238   if(!oldField) {
239     myDebug() << "no field named " << fieldName;
240     return false;
241   }
242 
243   // update name dict
244   m_fieldByName.insert(fieldName, newField_.data());
245 
246   // update titles
247   const QString oldTitle = oldField->title();
248   const QString newTitle = newField_->title();
249   if(oldTitle == newTitle) {
250     m_fieldByTitle.insert(newTitle, newField_.data());
251   } else {
252     m_fieldByTitle.remove(oldTitle);
253     m_fieldByTitle.insert(newTitle, newField_.data());
254   }
255 
256   // now replace the field pointer in the list
257   int pos = m_fields.indexOf(oldField);
258   if(pos > -1) {
259     m_fields.replace(pos, newField_);
260   } else {
261     myDebug() << "no index found!";
262     return false;
263   }
264 
265   // update category list.
266   if(oldField->category() != newField_->category()) {
267     m_fieldCategories.clear();
268     foreach(FieldPtr it, m_fields) {
269       // add category if it's not in the list yet
270       if(!it->category().isEmpty() && !m_fieldCategories.contains(it->category())) {
271         m_fieldCategories += it->category();
272       }
273     }
274   }
275 
276   if(newField_->hasFlag(Field::Derived)) {
277     DerivedValue dv(newField_);
278     if(dv.isRecursive(this)) {
279       newField_->setProperty(QStringLiteral("template"), QString());
280     }
281   }
282 
283   // keep track of if the entry groups will need to be reset
284   bool resetGroups = false;
285 
286   // if format is different, go ahead and invalidate all formatted entry values
287   if(oldField->formatType() != newField_->formatType()) {
288     // invalidate cached format strings of all entry attributes of this name
289     foreach(EntryPtr entry, m_entries) {
290       entry->invalidateFormattedFieldValue(fieldName);
291     }
292     resetGroups = true;
293   }
294 
295   // check to see if the people "pseudo-group" needs to be updated
296   // only if only one of the two is a name
297   bool wasPeople = oldField->formatType() == FieldFormat::FormatName;
298   bool isPeople = newField_->formatType() == FieldFormat::FormatName;
299   if(wasPeople) {
300     m_peopleFields.removeAll(oldField);
301     if(!isPeople) {
302       resetGroups = true;
303     }
304   }
305   if(isPeople) {
306     // if there's more than one people field and no people dict exists yet, add it
307     if(m_peopleFields.count() > 1 && !m_entryGroupDicts.contains(s_peopleGroupName)) {
308       EntryGroupDict* d = new EntryGroupDict();
309       m_entryGroupDicts.insert(s_peopleGroupName, d);
310       // put it at the top of the list
311       m_entryGroups.prepend(s_peopleGroupName);
312     }
313     m_peopleFields.append(newField_);
314     if(!wasPeople) {
315       resetGroups = true;
316     }
317   }
318 
319   bool wasGrouped = oldField->hasFlag(Field::AllowGrouped);
320   bool isGrouped = newField_->hasFlag(Field::AllowGrouped);
321   if(wasGrouped) {
322     if(!isGrouped) {
323       // in order to keep list in the same order, don't remove unless new field is not groupable
324       m_entryGroups.removeAll(fieldName);
325       delete m_entryGroupDicts.take(fieldName); // no auto-delete here
326       myDebug() << "no longer grouped: " << fieldName;
327       resetGroups = true;
328     } else {
329       // don't do this, it wipes out the old groups!
330 //      m_entryGroupDicts.replace(fieldName, new EntryGroupDict());
331     }
332   } else if(isGrouped) {
333     EntryGroupDict* d = new EntryGroupDict();
334     m_entryGroupDicts.insert(fieldName, d);
335     if(!wasGrouped) {
336       // cache the possible groups of entries
337       m_entryGroups << fieldName;
338     }
339     resetGroups = true;
340   }
341 
342   if(oldField->type() == Field::Image) {
343     m_imageFields.removeAll(oldField);
344   }
345   if(newField_->type() == Field::Image) {
346     m_imageFields.append(newField_);
347   }
348 
349   if(resetGroups) {
350 //    myLog() << "invalidating groups";
351     invalidateGroups();
352   }
353 
354   // now to update all entries if the field is a derived value and the template changed
355   if(newField_->hasFlag(Field::Derived) &&
356      oldField->property(QStringLiteral("template")) != newField_->property(QStringLiteral("template"))) {
357     emit signalRefreshField(newField_);
358   }
359 
360   return true;
361 }
362 
removeField(const QString & name_,bool force_)363 bool Collection::removeField(const QString& name_, bool force_) {
364   return removeField(fieldByName(name_), force_);
365 }
366 
367 // force allows me to force the deleting of the title field if I need to
removeField(Tellico::Data::FieldPtr field_,bool force_)368 bool Collection::removeField(Tellico::Data::FieldPtr field_, bool force_/*=false*/) {
369   if(!field_ || !m_fields.contains(field_)) {
370     if(field_) {
371       myDebug() << "can't delete field:" << field_->name();
372     }
373     return false;
374   }
375 //  myDebug() << "name = " << field_->name();
376 
377   // can't delete the title field
378   if((field_->hasFlag(Field::NoDelete)) && !force_) {
379     return false;
380   }
381 
382   foreach(EntryPtr entry, m_entries) {
383     // setting the fields to an empty string removes the value from the entry's list
384     entry->setField(field_, QString());
385   }
386 
387   bool success = true;
388   if(field_->formatType() == FieldFormat::FormatName) {
389     m_peopleFields.removeAll(field_);
390   }
391 
392   if(field_->type() == Field::Image) {
393     m_imageFields.removeAll(field_);
394   }
395   m_fieldByName.remove(field_->name());
396   m_fieldByTitle.remove(field_->title());
397 
398   if(fieldsByCategory(field_->category()).count() == 1) {
399     m_fieldCategories.removeAll(field_->category());
400   }
401 
402   if(field_->hasFlag(Field::AllowGrouped)) {
403     EntryGroupDict* dict = m_entryGroupDicts.take(field_->name());
404     qDeleteAll(*dict);
405     m_entryGroups.removeAll(field_->name());
406     if(field_->name() == m_defaultGroupField && !m_entryGroups.isEmpty()) {
407       setDefaultGroupField(m_entryGroups.first());
408     }
409   }
410 
411   m_fields.removeAll(field_);
412 
413   // refresh all dependent fields, rather lazy, but there's
414   // likely to be weird effects when checking dependent fields
415   // while removing one, so refresh all of them
416   foreach(FieldPtr field, m_fields) {
417     if(field->hasFlag(Field::Derived)) {
418       emit signalRefreshField(field);
419     }
420   }
421 
422   return success;
423 }
424 
reorderFields(const Tellico::Data::FieldList & list_)425 void Collection::reorderFields(const Tellico::Data::FieldList& list_) {
426 // assume the lists have the same pointers!
427   m_fields = list_;
428 
429   // also reset category list, since the order may have changed
430   m_fieldCategories.clear();
431   foreach(FieldPtr field, m_fields) {
432     if(!field->category().isEmpty() && !m_fieldCategories.contains(field->category())) {
433       m_fieldCategories << field->category();
434     }
435   }
436 }
437 
addEntries(const Tellico::Data::EntryList & entries_)438 void Collection::addEntries(const Tellico::Data::EntryList& entries_) {
439   if(entries_.isEmpty()) {
440     return;
441   }
442 
443   foreach(EntryPtr entry, entries_) {
444     if(!entry) {
445       Q_ASSERT(entry);
446       continue;
447     }
448     bool foster = false;
449     if(this != entry->collection().data()) {
450       entry->setCollection(CollPtr(this));
451       foster = true;
452     }
453 
454     m_entries.append(entry);
455 //    myDebug() << "added entry (" << entry->title() << ")" <<  entry->id();
456 
457     if(entry->id() >= m_nextEntryId) {
458       m_nextEntryId = entry->id() + 1;
459     } else if(entry->id() == -1) {
460       entry->setId(m_nextEntryId);
461       ++m_nextEntryId;
462     } else if(m_entryById.contains(entry->id())) {
463       if(!foster) {
464         myDebug() << "the collection already has an entry with id = " << entry->id();
465       }
466       entry->setId(m_nextEntryId);
467       ++m_nextEntryId;
468     }
469     m_entryById.insert(entry->id(), entry.data());
470 
471     if(hasField(QStringLiteral("cdate")) && entry->field(QStringLiteral("cdate")).isEmpty()) {
472       // use mdate if it exists
473       QString cdate = entry->field(QStringLiteral("mdate"));
474       if(cdate.isEmpty()) {
475         cdate = QDate::currentDate().toString(Qt::ISODate);
476       }
477       entry->setField(QStringLiteral("cdate"), cdate, false);
478     }
479     if(hasField(QStringLiteral("mdate")) && entry->field(QStringLiteral("mdate")).isEmpty()) {
480       entry->setField(QStringLiteral("mdate"), QDate::currentDate().toString(Qt::ISODate), false);
481     }
482   }
483   if(m_trackGroups) {
484     populateCurrentDicts(entries_, fieldNames());
485   }
486 }
487 
removeEntriesFromDicts(const Tellico::Data::EntryList & entries_,const QStringList & fields_)488 void Collection::removeEntriesFromDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) {
489   QSet<EntryGroup*> modifiedGroups;
490   foreach(EntryPtr entry, entries_) {
491     // need a copy of the vector since it gets changed
492     QList<EntryGroup*> groups = entry->groups();
493     foreach(EntryGroup* group, groups) {
494       // only clear groups for the modified fields, skip the others
495       // also clear for all derived values, just in case
496       if(!fields_.contains(group->fieldName()) && hasField(group->fieldName()) && !fieldByName(group->fieldName())->hasFlag(Field::Derived))  {
497         continue;
498       }
499       if(entry->removeFromGroup(group)) {
500         modifiedGroups.insert(group);
501       }
502       if(group->isEmpty() && !m_groupsToDelete.contains(group)) {
503         m_groupsToDelete.push_back(group);
504       }
505     }
506   }
507   if(!modifiedGroups.isEmpty()) {
508     emit signalGroupsModified(CollPtr(this), modifiedGroups.values());
509   }
510 }
511 
512 // this function gets called whenever an entry is modified. Its purpose is to keep the
513 // groupDicts current. It first removes the entry from every group to which it belongs,
514 // then it repopulates the dicts with the entry's fields
updateDicts(const Tellico::Data::EntryList & entries_,const QStringList & fields_)515 void Collection::updateDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) {
516   if(entries_.isEmpty() || !m_trackGroups) {
517     return;
518   }
519   QStringList modifiedFields = fields_;
520   if(modifiedFields.isEmpty()) {
521 //    myDebug() << "updating all fields";
522     modifiedFields = fieldNames();
523   }
524   removeEntriesFromDicts(entries_, modifiedFields);
525   populateCurrentDicts(entries_, modifiedFields);
526   cleanGroups();
527 }
528 
removeEntries(const Tellico::Data::EntryList & vec_)529 bool Collection::removeEntries(const Tellico::Data::EntryList& vec_) {
530   if(vec_.isEmpty()) {
531     return false;
532   }
533 
534   removeEntriesFromDicts(vec_, fieldNames());
535   bool success = true;
536   foreach(EntryPtr entry, vec_) {
537     m_entryById.remove(entry->id());
538     m_entries.removeAll(entry);
539   }
540   cleanGroups();
541   return success;
542 }
543 
fieldsByCategory(const QString & cat_)544 Tellico::Data::FieldList Collection::fieldsByCategory(const QString& cat_) {
545 #ifndef NDEBUG
546   if(!m_fieldCategories.contains(cat_)) {
547     myDebug() << cat_ << "' is not in category list";
548   }
549 #endif
550   if(cat_.isEmpty()) {
551     myDebug() << "empty category!";
552     return FieldList();
553   }
554 
555   FieldList list;
556   foreach(FieldPtr field, m_fields) {
557     if(field->category() == cat_) {
558       list.append(field);
559     }
560   }
561   return list;
562 }
563 
fieldNameByTitle(const QString & title_) const564 QString Collection::fieldNameByTitle(const QString& title_) const {
565   if(title_.isEmpty()) {
566     return QString();
567   }
568   FieldPtr f = fieldByTitle(title_);
569   if(!f) { // might happen in MainWindow::saveCollectionOptions
570     return QString();
571   }
572   return f->name();
573 }
574 
fieldNames() const575 QStringList Collection::fieldNames() const {
576   return m_fieldByName.keys();
577 }
578 
fieldTitles() const579 QStringList Collection::fieldTitles() const {
580   return m_fieldByTitle.keys();
581 }
582 
fieldTitleByName(const QString & name_) const583 QString Collection::fieldTitleByName(const QString& name_) const {
584   if(name_.isEmpty()) {
585     return QString();
586   }
587   FieldPtr f = fieldByName(name_);
588   if(!f) {
589     myWarning() << "no field named " << name_;
590     return QString();
591   }
592   return f->title();
593 }
594 
valuesByFieldName(const QString & name_) const595 QStringList Collection::valuesByFieldName(const QString& name_) const {
596   if(name_.isEmpty()) {
597     return QStringList();
598   }
599 
600   StringSet values;
601   foreach(EntryPtr entry, m_entries) {
602     values.add(FieldFormat::splitValue(entry->field(name_)));
603   } // end entry loop
604 
605   return values.values();
606 }
607 
fieldByName(const QString & name_) const608 Tellico::Data::FieldPtr Collection::fieldByName(const QString& name_) const {
609   return FieldPtr(m_fieldByName.value(name_));
610 }
611 
fieldByTitle(const QString & title_) const612 Tellico::Data::FieldPtr Collection::fieldByTitle(const QString& title_) const {
613   return FieldPtr(m_fieldByTitle.value(title_));
614 }
615 
hasField(const QString & name_) const616 bool Collection::hasField(const QString& name_) const {
617   return m_fieldByName.contains(name_);
618 }
619 
isAllowed(const QString & field_,const QString & value_) const620 bool Collection::isAllowed(const QString& field_, const QString& value_) const {
621   // empty string is always allowed
622   if(value_.isEmpty()) {
623     return true;
624   }
625 
626   // find the field with a name of 'key_'
627   FieldPtr field = fieldByName(field_);
628 
629   // if the type is not multiple choice or if value_ is allowed, return true
630   if(field && (field->type() != Field::Choice || field->allowed().contains(value_))) {
631     return true;
632   }
633 
634   return false;
635 }
636 
entryGroupDictByName(const QString & name_)637 Tellico::Data::EntryGroupDict* Collection::entryGroupDictByName(const QString& name_) {
638 //  myDebug() << name_;
639   m_lastGroupField = name_; // keep track, even if it's invalid
640   if(name_.isEmpty() || !m_entryGroupDicts.contains(name_) || m_entries.isEmpty()) {
641     return nullptr;
642   }
643   EntryGroupDict* dict = m_entryGroupDicts.value(name_);
644   if(dict && dict->isEmpty()) {
645     const bool b = signalsBlocked();
646     // block signals so all the group created/modified signals don't fire
647     blockSignals(true);
648     populateDict(dict, name_, m_entries);
649     blockSignals(b);
650   }
651   return dict;
652 }
653 
populateDict(Tellico::Data::EntryGroupDict * dict_,const QString & fieldName_,const Tellico::Data::EntryList & entries_)654 void Collection::populateDict(Tellico::Data::EntryGroupDict* dict_, const QString& fieldName_, const Tellico::Data::EntryList& entries_) {
655 //  myDebug() << fieldName_;
656   Q_ASSERT(dict_);
657   const bool isBool = hasField(fieldName_) && fieldByName(fieldName_)->type() == Field::Bool;
658 
659   QSet<EntryGroup*> modifiedGroups;
660   foreach(EntryPtr entry, entries_) {
661     const QStringList groups = entryGroupNamesByField(entry, fieldName_);
662     foreach(QString groupTitle, groups) { // krazy:exclude=foreach
663       // find the group for this group name
664       // bool fields use the field title
665       if(isBool && !groupTitle.isEmpty()) {
666         groupTitle = fieldTitleByName(fieldName_);
667       }
668       EntryGroup* group = dict_->value(groupTitle);
669       // if the group doesn't exist, create it
670       if(!group) {
671         group = new EntryGroup(groupTitle, fieldName_);
672         dict_->insert(groupTitle, group);
673       } else if(group->isEmpty()) {
674         // if it's empty, then it was previously added to the vector of groups to delete
675         // remove it from that vector now that we're adding to it
676         m_groupsToDelete.removeOne(group);
677       }
678       if(entry->addToGroup(group)) {
679         modifiedGroups.insert(group);
680       }
681     } // end group loop
682   } // end entry loop
683   if(!modifiedGroups.isEmpty()) {
684     emit signalGroupsModified(CollPtr(this), modifiedGroups.values());
685   }
686 }
687 
populateCurrentDicts(const Tellico::Data::EntryList & entries_,const QStringList & fields_)688 void Collection::populateCurrentDicts(const Tellico::Data::EntryList& entries_, const QStringList& fields_) {
689   if(m_entryGroupDicts.isEmpty()) {
690     return;
691   }
692 
693   // special case when adding an entry to a new empty collection
694   // there are no existing non-empty groups
695   bool allEmpty = true;
696 
697   // iterate over all the possible groupDicts
698   // for each dict, get the value of that field for the entry
699   // if multiple values are allowed, split the value and then insert the
700   // entry pointer into the dict for each value
701   QHash<QString, EntryGroupDict*>::const_iterator dictIt = m_entryGroupDicts.constBegin();
702   for( ; dictIt != m_entryGroupDicts.constEnd(); ++dictIt) {
703     // skip dicts for fields not in the modified list
704     if(!fields_.contains(dictIt.key())) {
705       continue;
706     }
707     // only populate if it's not empty, since they are
708     // populated on demand
709     if(!dictIt.value()->isEmpty()) {
710       populateDict(dictIt.value(), dictIt.key(), entries_);
711       allEmpty = false;
712     }
713   }
714 
715   if(allEmpty) {
716 //    myDebug() << "all collection dicts are empty";
717     // still need to populate the current group dict
718     EntryGroupDict* dict = m_entryGroupDicts.value(m_lastGroupField);
719     if(dict) {
720       populateDict(dict, m_lastGroupField, entries_);
721     }
722   }
723 }
724 
725 // return a string list for all the groups that the entry belongs to
726 // for a given field. Normally, this would just be splitting the entry's value
727 // for the field, but if the field name is the people pseudo-group, then it gets
728 // a bit more complicated
entryGroupNamesByField(Tellico::Data::EntryPtr entry_,const QString & fieldName_)729 QStringList Collection::entryGroupNamesByField(Tellico::Data::EntryPtr entry_, const QString& fieldName_) {
730   if(fieldName_ != s_peopleGroupName) {
731     return entry_->groupNamesByFieldName(fieldName_);
732   }
733 
734   // the empty group is only returned if the entry has an empty list for every people field
735   bool allEmpty = true;
736   StringSet values;
737   foreach(FieldPtr field, m_peopleFields) {
738     const QStringList groups = entry_->groupNamesByFieldName(field->name());
739     if(allEmpty && (groups.count() != 1 || !groups.at(0).isEmpty())) {
740       allEmpty = false;
741     }
742     values.add(groups);
743   }
744   if(!allEmpty) {
745     // we don't want the empty string
746     values.remove(QString());
747   }
748   return values.values();
749 }
750 
invalidateGroups()751 void Collection::invalidateGroups() {
752   foreach(EntryGroupDict* dict, m_entryGroupDicts) {
753     qDeleteAll(*dict);
754     dict->clear();
755     // don't delete the dict, just clear it
756   }
757 
758   // populateDicts() will make signals that the group view is connected to, block those
759   blockSignals(true);
760   foreach(EntryPtr entry, m_entries) {
761     entry->invalidateFormattedFieldValue();
762     entry->clearGroups();
763   }
764   blockSignals(false);
765 }
766 
entryById(Data::ID id_)767 Tellico::Data::EntryPtr Collection::entryById(Data::ID id_) {
768   return EntryPtr(m_entryById.value(id_));
769 }
770 
addBorrower(Tellico::Data::BorrowerPtr borrower_)771 void Collection::addBorrower(Tellico::Data::BorrowerPtr borrower_) {
772   if(!borrower_) {
773     return;
774   }
775   // check against existing borrower uid
776   BorrowerPtr existingBorrower;
777   foreach(BorrowerPtr bor, m_borrowers) {
778     if(bor->uid() == borrower_->uid()) {
779       existingBorrower = bor;
780       break;
781     }
782   }
783   if(!existingBorrower) {
784     m_borrowers.append(borrower_);
785   } else if(existingBorrower != borrower_) {
786     // need to merge loans
787     QHash<QString, LoanPtr> existingLoans;
788     foreach(LoanPtr loan, existingBorrower->loans()) {
789       existingLoans.insert(loan->uid(), loan);
790     }
791     foreach(LoanPtr loan, borrower_->loans()) {
792       if(!existingLoans.contains(loan->uid())) {
793         existingBorrower->addLoan(loan);
794       }
795     }
796   }
797 }
798 
addFilter(Tellico::FilterPtr filter_)799 void Collection::addFilter(Tellico::FilterPtr filter_) {
800   if(!filter_) {
801     return;
802   }
803 
804   m_filters.append(filter_);
805 }
806 
removeFilter(Tellico::FilterPtr filter_)807 bool Collection::removeFilter(Tellico::FilterPtr filter_) {
808   if(!filter_) {
809     return false;
810   }
811 
812   return m_filters.removeAll(filter_) > 0;
813 }
814 
clear()815 void Collection::clear() {
816   // since the collection holds a pointer to each entry and each entry
817   // hold a pointer to the collection, and they're both sharedptrs,
818   // neither will ever get deleted, unless the collection removes
819   // all held pointers, specifically to entries
820   m_fields.clear();
821   m_peopleFields.clear();
822   m_imageFields.clear();
823   m_fieldCategories.clear();
824   m_fieldByName.clear();
825   m_fieldByTitle.clear();
826   m_defaultGroupField.clear();
827 
828   m_entries.clear();
829   m_entryById.clear();
830   foreach(EntryGroupDict* dict, m_entryGroupDicts) {
831     qDeleteAll(*dict);
832   }
833   qDeleteAll(m_entryGroupDicts);
834   m_entryGroupDicts.clear();
835   m_entryGroups.clear();
836   m_groupsToDelete.clear();
837   m_filters.clear();
838   m_borrowers.clear();
839 }
840 
cleanGroups()841 void Collection::cleanGroups() {
842   foreach(EntryGroup* group, m_groupsToDelete) {
843     EntryGroupDict* dict = entryGroupDictByName(group->fieldName());
844     if(!dict) {
845       continue;
846     }
847     EntryGroup* groupToDelete = dict->take(group->groupName());
848     delete groupToDelete;
849   }
850   m_groupsToDelete.clear();
851 }
852 
prepareText(const QString & text_) const853 QString Collection::prepareText(const QString& text_) const {
854   return text_;
855 }
856 
sameEntry(Tellico::Data::EntryPtr entry1_,Tellico::Data::EntryPtr entry2_) const857 int Collection::sameEntry(Tellico::Data::EntryPtr entry1_, Tellico::Data::EntryPtr entry2_) const {
858   if(!entry1_ || !entry2_) {
859     return 0;
860   }
861   // used to just return 0, but we really want a default generic implementation
862   // that specific collections can override.
863 
864   int res = 0;
865   // start with twice the title score
866   // and since the minimum is > 10, then need more than just a perfect title match
867   res += EntryComparison::MATCH_WEIGHT_MED*EntryComparison::score(entry1_, entry2_, QStringLiteral("title"), this);
868   // then add score for each field
869   foreach(FieldPtr field, entry1_->collection()->fields()) {
870     res += EntryComparison::MATCH_WEIGHT_LOW*EntryComparison::score(entry1_, entry2_, field->name(), this);
871     if(res >= EntryComparison::ENTRY_PERFECT_MATCH) return res;
872   }
873   return res;
874 }
875 
getID()876 Tellico::Data::ID Collection::getID() {
877   static ID id = 0;
878   return ++id;
879 }
880 
primaryImageField() const881 Data::FieldPtr Collection::primaryImageField() const {
882   return m_imageFields.isEmpty() ? Data::FieldPtr() : fieldByName(m_imageFields.front()->name());
883 }
884