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