Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

487

488

489

490

491

492

493

494

495

496

497

498

499

500

501

502

503

504

505

506

507

508

509

510

511

512

513

514

515

516

517

518

519

520

521

522

523

524

525

526

527

528

529

530

531

532

533

534

535

536

537

538

539

540

541

542

543

544

545

546

547

548

549

550

551

552

553

554

555

556

557

558

559

560

561

562

563

564

565

566

567

568

569

570

571

572

573

574

575

576

577

578

579

580

581

582

583

584

585

586

587

588

589

590

591

592

593

594

595

596

597

598

599

600

601

602

603

604

605

606

607

608

609

610

611

612

613

614

615

616

617

618

619

620

621

622

623

624

625

626

627

628

629

630

631

632

633

634

635

636

637

638

639

640

641

642

643

644

645

646

647

648

649

650

651

652

653

654

655

656

657

658

659

660

661

662

663

664

665

666

import datetime 

import logging 

import csv 

from csv import reader, writer 

 

from collections import OrderedDict 

from itertools import zip_longest 

import io 

 

from .models import EmailAddress, Erfa, Member, BalanceTransactionLog, DeliveryNumber, EmailToMember 

from .countryfield import get_country_dict 

 

 

def uglify_date(date): 

return "{:%d.%m.%y}".format(date) 

 

 

class MemberCSVHelper: 

 

fieldnames = () 

 

def __init__(self, member_set=None): 

self.member_set = member_set 

self.writer = None 

self.reader = None 

self._fh = None 

self.warnings = [] 

self.logs = [] 

 

def do_export(self): 

assert self.member_set is not None, "We need members if we want to export them" 

self.setup_writer() 

for member in self.member_set: 

self.writer.writerow(self.get_row(member)) 

result = self._fh.getvalue() 

self._fh.close() 

return result 

 

def do_import(self, file): 

self._fh = file 

self.setup_reader() 

for row in self.reader: 

self.import_row(row) 

 

def _get_writer(self,): 

return writer(self._fh) 

 

def setup_writer(self,): 

if not self._fh: 

self._fh = io.StringIO() 

self.writer = self._get_writer() 

if len(self.fieldnames) > 0: 

self.writer.writerow(self.fieldnames) 

 

def _get_reader(self,): 

return reader(self._fh) 

 

def setup_reader(self,): 

if not self._fh: 

assert False, 'No input specified yet. Try using do_import(file_io_object).' 

self.reader = self._get_reader() 

 

def get_row(self, member): 

raise NotImplementedError 

 

def import_row(self, row): 

member = self.get_member_from(row) 

if self.update_member(member, row): 

member.save() 

self.logs.append('created/updated: {}'.format(member)) 

 

def get_member_from(self, row): 

raise NotImplementedError 

 

def update_member(self, member, row): 

raise NotImplementedError 

 

 

class ErfaCSVExporter: 

 

def __init__(self, erfas=None): 

self.file = None 

self._erfas = erfas 

self._writer = None 

 

def export(self): 

self._setup_writer() 

for erfa in self._erfas: 

self._writer.writerow(self._get_row(erfa)) 

 

def _get_writer(self): 

return writer(self.file, delimiter='\t') 

 

def _setup_writer(self): 

if not self.file: 

self.file = io.StringIO() 

self._writer = self._get_writer() 

 

@staticmethod 

def _get_row(erfa): 

return erfa.short_name, erfa.long_name 

 

 

# TODO: Remove. CCC abolished the Erfa Doppelmitgliedschaften as of 12.2017 

class ErfaAbgleich(MemberCSVHelper): 

fieldnames = ("Änderung", "Eintrittsdatum CCC", "CNR", "Status", "Vorname", 

"Nachname", "Anschrift1", "Anschrift2", "Anschrift3", "E-Mail") 

 

def __init__(self, filename, erfa, member_set=None): 

if member_set is None: 

member_set = erfa.member_set 

self.erfa = erfa 

super().__init__(filename, member_set) 

 

def get_row(self, member): 

change = "" 

membership_start = member.membership_start 

chaos_number = member.chaos_number 

first_name = member.first_name 

last_name = member.last_name 

address_1 = member.address_1 

address_2 = member.address_2 

address_3 = member.address_3 

country = member.address_country 

email = member.get_primary_mail() 

if not member.is_active: 

status = "Ruhend" 

elif member.membership_type == Member.MEMBERSHIP_TYPE_HONORARY: 

status = "Ehrenmitglied" 

elif member.membership_reduced: 

status = "Ermäßigt" 

else: 

status = "Vollmitglied" 

return (change, membership_start, chaos_number, status, first_name, last_name, address_1, address_2, address_3, 

country, email) 

 

def get_member_from(self, row): 

if not row[2]: 

return Member() 

return Member.objects.get(pk=int(row[2])) 

 

def update_member(self, member, row): 

change, membership_start, chaos_number, status, first_name, last_name, address_1, address_2, address_3, country, email = row 

if not change: 

# nothing changed, we do not need to save the model 

return False 

member.first_name = first_name 

member.last_name = last_name 

member.address_1 = address_1 

member.address_2 = address_2 

member.address_3 = address_3 

member.address_country = country 

EmailAddress(member=member, email_address=email, is_primary=True).save() 

if status == "Vollmitglied": 

member.membership_reduced = False 

member.fee_override = None 

elif status == "Ehrenmitglied": 

member.membership_type = Member.MEMBERSHIP_TYPE_HONORARY 

member.membership_reduced = False 

member.fee_override = None 

elif status == "Ermäßigt": 

member.fee_override = None 

member.membership_reduced = True 

elif status == "Ruhend": 

member.is_active = False 

if member.account_balance < 0: 

# member will be saved after this 

member.set_balance_to(0) 

member.fee_paid_until = datetime.date.today() 

elif status == "Austritt Erfa": 

member.alienate(self.erfa) 

elif status == "Austritt CCC": 

# ToDo: set membership_end date to the imported date of the resignation 

pass 

else: 

warn = "Member status unknown, got '{status}': {first} {last} ({pk})".format( 

status=status, first=first_name, last=last_name, pk=chaos_number) 

logging.warning(warn) 

self.warnings.append(warn) 

 

member.last_update = datetime.date.today() 

 

return True 

 

 

class Vereinstisch(MemberCSVHelper): 

 

# export 

def _get_writer(self,): 

return writer(self._fh, delimiter='\t') 

 

def _get_reader(self,): 

return reader(self._fh, delimiter='\t') 

 

def setup_writer(self,): 

if not self._fh: 

self._fh = io.StringIO() 

self.writer = self._get_writer() 

 

def do_export(self): 

assert self.member_set is not None, "We need members if we want to export them" 

self.setup_writer() 

for member in self.member_set: 

self.writer.writerow(self.get_row(member)) 

 

def get_row(self, member): 

date_format = '%d.%m.%Y' 

emails_queryset = member.emailaddress_set.order_by('-is_primary') 

return ( 

member.chaos_number, 

member.first_name, 

member.last_name, 

member.address_1, 

member.address_2, 

member.address_3, 

member.address_country, 

member.erfa.short_name, 

member.address_unknown, 

member.erfa.has_doppelmitgliedschaft, 

member.membership_reduced, 

member.is_active, 

member.notification_consent, 

member.membership_type, 

member.membership_start.strftime(date_format), 

member.fee_last_paid.strftime(date_format), 

member.fee_paid_until.strftime(date_format), 

member.last_update.strftime(date_format), 

member.account_balance, 

','.join(list(emails_queryset.values_list('email_address', flat=True))), 

','.join(list(emails_queryset.values_list('gpg_key_id', flat=True))), 

member.comment 

) 

 

def get_member_from(self, row): 

# if there is no chaos number, we create a new Member 

if row[1] == '-1': 

return Member() 

 

try: 

chaos_number = int(row[1]) 

except ValueError: 

warn = 'Ignoring illegal chaos_number: {}. Setting up new member.'.format(row[1]) 

logging.warning(warn) 

self.warnings.append(warn) 

return Member() 

 

try: 

return Member.objects.get(pk=chaos_number) 

except Member.DoesNotExist: 

warn = 'chaos_number not found: {}. Setting up new member.'.format(row[1]) 

logging.warning(warn) 

self.warnings.append(warn) 

return Member() 

 

def _str2date(self, string): 

try: 

return datetime.datetime.strptime(string, '%d.%m.%Y').date() 

except ValueError: 

warn = 'Illegal date: {}.'.format(string) 

logging.warning(warn) 

self.warnings.append(warn) 

return None 

 

def update_member(self, member, row): 

# (new_member, chaos_number, first_name, last_name, address_1, address_2, address_3, address_country, erfa, email, gpg_key_id, membership_type, membership_reduced, changed, amount_paid, membership_end, Kommentar) 

 

if row[1] == '-1': 

# new member 

member.membership_type = row[11] 

else: 

# Prepare for re-entering email addresses 

EmailAddress.objects.filter(person=member).delete() 

 

member.first_name = row[2] 

member.last_name = row[3] 

member.address_1 = row[4] 

member.address_2 = row[5] 

member.address_3 = row[6] 

country = row[7] 

if country in get_country_dict(): 

member.address_country = row[7] 

else: 

warn = 'Illegal country code: {}. Defaulted to DE for Germany.'.format(row[7]) 

logging.warning(warn) 

self.warnings.append(warn) 

try: 

member.erfa = Erfa.objects.get(short_name=row[8]) 

except Erfa.DoesNotExist: 

warn = 'Erfa {} does not exist. Member {} set to Alien'.format(row[8], member.get_name()) 

logging.warning(warn) 

self.warnings.append(warn) 

 

# Membership type cannot be changed and is thus ignored in case of changes to a member 

 

member.membership_reduced = row[12].lower() == 'true' 

member.notification_consent = row[13].lower() == 'true' 

 

if row[16]: 

membership_end = self._str2date(row[16]) 

if membership_end: 

member.membership_end = membership_end 

else: 

return False 

 

member.comment = row[17].strip() 

 

# We expect that users have cleaned up their member record before import 

member.address_unknown = 0 

 

member.save() 

 

try: 

# Save email addresses after the member was saved 

if row[9].strip(): 

email_pairs = zip_longest(row[9].split(','), row[10].split(',')) 

for email_pair in email_pairs: 

EmailAddress(person=member, email_address=email_pair[0], gpg_key_id=email_pair[1] or '').save() 

 

# Process payments after member is fully set-up 

amount_paid = int(row[15]) 

if amount_paid > 0: 

member.increase_balance_by(amount_paid, reason=BalanceTransactionLog.MANUAL_BOOKING, 

booking_day=member.last_update, comment='Vereinstisch: ' + row[17].strip()) 

except: 

return False 

 

return True 

 

# This is not using the MemberCSVHelper base class, because this class seems a bit complicated… 

# For the purpose of not accidentally introducing bugs I am not going to refactor this before 

# congress, but if I would the implementation would look something like this, so all simple 

# modifications could be made through the fieldnames dict (instead of actually having to change any code). 

# 

# The actual implementation provided here basically implements CashdeskExport(CSVImportExportBase)::export_to_file. 

# The main difference here is that we skip importing (we do not require a cashdesk import) and that the implementation 

# is not generic (which it does not have to be for now because I am not going to refactor the Vereinstisch export 

# to use the same base class this short before 34C3). 

# 

# ``` 

# # It should be possible to do 

# # with on_warning(my_handler): 

# # 

# def warn(): 

# ... 

# 

# # Base class used to facilitate customizing various member export & import formats 

# class CSVImportExportBase: 

# # List of columns in the exported CSV and their mappings… 

# # This should be some dict like this: 

# # 

# # ``` 

# # class MyExport: 

# # fieldnames = { 

# # 'Eintrittsdatum CCC': 'membership_start', 

# # 'Anschrift1': 'address_1', 

# # 'Status': None 

# # } 

# # ``` 

# # 

# # Basically on the left side is the field in the CSV, on the right 

# # is the field in the actual member model. 

# # 

# # You can overwrite row2member and member2row to define more complex exports & imports 

# # like for Status which has no member field for it's row 

# fieldnames = None 

# 

# # Takes a set of members and writes them all 

# # in this CSV format to the given file 

# @classmethod 

# def export_to_file(cls, fh, member_set): 

# writer = csv.DictWriter(fh, fieldnames=cls.fieldnames.keys()) 

# writer.writeheader() 

# cls.export_with_writer(writer) 

# 

# # Exports the given member set using the given writer. 

# # The writer is some object that supports the `writerow` function, 

# # which takes a dict with all the fields and serializes them *somehow*. 

# # 

# # The writer usually is a csv.DictWriter; be careful to call writeheader() 

# # the writer yourself, because export_with_writer will not call it. 

# # 

# # See export_to_file for an example of how to use this to generate CSV. 

# # You could however also export to json like so: 

# # 

# # ``` 

# # class JsonWriter: 

# # def __init__(fh): 

# # self._fh = fh 

# # self._num = 0 

# # 

# # def writerow(self, data): 

# # if self._num != 0: 

# # self._fh.write(',') 

# # json.dump(data, self._fh) 

# # self._num += 1 

# # 

# # member_set = ... 

# # fh = ... 

# # 

# # MyCSVExporter.export_with_writer(JsonWriter(fh), member_set) 

# # ``` 

# @classmethod 

# def export_with_writer(cls, writer, member_set): 

# for member in member_set: 

# writer.writerow(cls.member2row(member)) 

# 

# # Takes a member and converts it to a row 

# # for using in export_with_writer. 

# # (the row will be passed to writerow and should be a dict with 

# # all the keys from fieldnames) 

# @classmethod 

# def member2row(cls, member): 

# return {k: getattr(member, v) for k, v in cls.fieldnames.items() if v != None} 

# 

# # Takes a file handle that contains CSV data where each row represents 

# # a CCC member. 

# # The CSV file must have a header... 

# # 

# # This does not actually save the imported members, instead an iterator 

# # is returned withe (row, member, save) tuples where the caller should save 

# # each member themselves (save is the reccomendation of the importer whether 

# # the imported document should be saved); this is useful to implement transactional semantics 

# # (store all members in a list; use `with on_warnin(...)`) to throw in case 

# # of any warning; the result would be that no document can be saved if any 

# # document import produced an exception or warning 

# @classmethod 

# def import_from_file(cls, fh): 

# # TODO: Possibly detect whether the input CSV has a header or not 

# return cls.import_with_reader(csv.DictReader(fh)) 

# 

# # Like import_from_file, but allows the caller to customize the 

# # serialization format. 

# # See import_from_file to see how this is normally used. 

# # This could – again – also be used to implement a JSON based importer 

# # instead of a csv based one; The given reader must be an iterable containing 

# # dict like objects containing the keys from fieldnames... 

# # 

# # ``` 

# # fh = ... # Some file handle 

# # data = json.load(fh) # In this case, the JSON must contain an array of objects 

# # members = list(MyCSVExporter.import_with_reader(data)) 

# # for (row, member, save) in members: 

# # if save: 

# # member.save() 

# # ``` 

# @classmethod 

# def import_with_reader(cls, reader): 

# for row in reader: 

# yield cls.row2member(row) 

# 

# # This basically takes a row and returns a three-tuple containing 

# # (row, member, save) where save tells the caller whether this particular 

# # member should be saved 

# @classmethod 

# def row2member(cls, row) 

# member = cls.retrieve_member(row) 

# do_save = cls.update_member(row) 

# return (row, member, do_save) 

# 

# # Determines the chaos number for a specific row; this is used to 

# # actually retrieve existing members from the database; if -1 is returned, 

# # a new Member is created. 

# # The default implementation looks in cls.fieldnames for the field that 

# # refers to the member with the name 'chaos_number' in the model 

# @classmethod 

# def get_row_chaosno(cls, row): 

# chaosno_field = None 

# for k, v in cls.fieldnames: 

# if v == 'chaos_number': 

# chaosno_field = k 

# break 

# 

# chaosno_str = row.get(chaosno_field) 

# 

# if chaosno_field == None: 

# throw NotImplementedError("Cannot determine chaosno from fieldnames.") 

# 

# return chaosno_str 

# 

# @classmethod 

# def retrieve_member(cls, row, _warn): 

# chaosno_str = cls.get_row_chaosno() 

# 

# try: 

# chaosno = int(chaosno_str) 

# catch Exception as e: 

# warn('Could not decode chaos number {} because. Setting up new member'.format(chaosno_str, e)) 

# return Member() 

# 

# if chaosno == -1: 

# return Member() 

# 

# try: 

# Member.objects.get(pk=chaosno) 

# catch Exception as e: 

# warn('Could not retrieve member with chaos number {} because {}. Setting up new member'.format(chaosno_str, e)) 

# return Member() 

# 

# @classmethod 

# def update_member(cls, row, member): 

# for k, v in cls.fieldnames 

# setattr(member, v, row.get(k)) 

# return true 

# ``` 

def cashpoint_export_impl(fh, member_set): 

fieldnames = OrderedDict([ 

('chaos_number', 'chaos_number'), 

('first_name', 'first_name'), 

('last_name', 'last_name'), 

('state', None)]) 

 

writer = csv.DictWriter(fh, fieldnames=fieldnames.keys(), dialect='excel-tab') 

writer.writeheader() 

 

payday = datetime.date.today().replace(month=12, day=1) 

 

for member in member_set.order_by('chaos_number'): 

row = {k: getattr(member, v) for k, v in fieldnames.items() if v is not None} 

if not member.is_active: 

row['state'] = 'ruhend' 

else: 

row['state'] = 'Verzug' if member.is_payment_late(payday) else 'bezahlt' 

writer.writerow(row) 

 

 

# Mapping of country names as used by the Post to ISO 2-letter abbreviations as used in AA 

post_countries = { 

'': 'DE', 

'Österreich': 'AT', 

} 

 

 

def premiumaddress_reader(csv_string): 

""" 

Interprets the reasons for returned letters from the Premiumaddress csv files and executes sensible actions in 

response. 

:param csv_string: 

:return: 

""" 

 

# Based on the information from: https://www.direktmarketingcenter.de/fileadmin/Download-Center/Handbuch_Premiumadress_-_Stand_Oktober_2017.pdf 

 

# AdrMerk 

address_unknown = ['10', '11', '13', '14', '18', '19', '21', '22', '25', '30', '31', '50', '53', '55'] 

exit_through_death = ['12', ] 

new_address = ['20', '51'] 

 

# SdgS 

arrived = ['20', '30'] 

not_arrived = ['10', '40'] 

 

failed_lines = [] 

processed_lines = [] 

 

file_handle = io.StringIO(csv_string) 

csv_reader = csv.DictReader(file_handle, delimiter=';') 

 

for field in ['SdgS', 'AdrMerk', 'E_Na1', 'E_Na2', 'Kd_Info', 'NSA_Na1', 'NSA_Na2', 'NSA_Na3', 'NSA_Na4', 

'NSA_Str', 'NSA_HNr', 'NSA_PLZ', 'NSA_Ort', 'NSA_Land', 'NSA_Postf', 'NSA_PLZPostf', 

'NSA_OrtPostfach', 'NSA_LandPostfach', 'UebgID']: 

 

562 ↛ 563line 562 didn't jump to line 563, because the condition on line 562 was never true if field not in csv_reader.fieldnames: 

return [], ['Missing field \'{}\' in uploaded file.'.format(field)] 

 

for line in csv_reader: 

 

delivery_number = line['Kd_Info'].upper() 

 

# The alphabet used by the delivery numbers is: ABCDEFGHJKLMNPQRSTUVWXYZ3789 

# All other letters and numbers are mapped to another character of this alphabet 

delivery_number_substitution = { 

'I': 'J', 

'O': 'D', 

'0': 'D', 

'1': 'J', 

'2': 'Z', 

'4': 'A', 

'5': 'S', 

'6': 'G', 

} 

 

cleaned_delivery_number = (delivery_number_substitution.get(char, char) for char in delivery_number) 

 

delivery_number = ''.join(cleaned_delivery_number) 

 

try: 

del_num_obj = DeliveryNumber.objects.get(number__exact=delivery_number) 

except DeliveryNumber.DoesNotExist: 

failed_lines.append( 

'Delivery \'{}\' has no valid delivery number: \'{}\'.'.format(line['UebgID'], delivery_number)) 

continue 

 

593 ↛ 594line 593 didn't jump to line 594, because the condition on line 593 was never true if del_num_obj.returned: 

failed_lines.append( 

'Delivery number \'{}\' has previously been marked as returned.'.format(delivery_number)) 

continue 

 

recipient = del_num_obj.recipient 

sdgs = line['SdgS'] 

adrmerk = line['AdrMerk'] 

 

if (adrmerk in address_unknown and sdgs in not_arrived) or (sdgs == '20' and adrmerk in ['21', '22']): 

# Adresse ist nicht mehr aktuell 

recipient.address_unknown += 1 

recipient.save() 

 

processed_lines.append( 

'{}: Member {} increased address unknown counter.'.format(delivery_number, recipient.chaos_number)) 

 

del_num_obj.returned = True 

del_num_obj.save() 

 

elif adrmerk in new_address and sdgs in ['10', '20', '40']: 

# Neue Adresse 

615 ↛ 620line 615 didn't jump to line 620, because the condition on line 615 was never false if line['NSA_Postf']: 

street = line['NSA_Postf'] 

town = (line['NSA_PLZPostf'].zfill(5) + ' ' + line['NSA_OrtPostfach']).strip() 

country = line['NSA_LandPostfach'] 

else: 

street = (line['NSA_Str'] + ' ' + line['NSA_HNr']).strip() 

town = (line['NSA_PLZ'].zfill(5) + ' ' + line['NSA_Ort']).strip() 

country = line['NSA_Land'] 

 

if country not in post_countries.keys(): 

failed_lines.append( 

'{}: Country \'{}\' not in mapping table for country IDs.'.format(delivery_number, country)) 

return processed_lines, failed_lines 

 

recipient.first_name = line['NSA_Na1'] 

recipient.last_name = line['NSA_Na2'] 

 

632 ↛ 637line 632 didn't jump to line 637, because the condition on line 632 was never false if line['NSA_Na3']: 

recipient.address_1 = (line['NSA_Na3'] + ' ' + line['NSA_Na4']).strip() 

recipient.address_2 = street 

recipient.address_3 = town 

else: 

recipient.address_1 = street 

recipient.address_2 = town 

recipient.address_3 = '' 

 

recipient.address_country = post_countries[country] 

 

recipient.save() 

 

processed_lines.append('{}: Member {} updated address.'.format(delivery_number, recipient.chaos_number)) 

 

del_num_obj.returned = True 

del_num_obj.save() 

 

elif adrmerk in exit_through_death and sdgs in not_arrived: 

# Austritt durch Tod 

processed_lines.append('{}: Member {} exited by death.'.format(delivery_number, recipient.chaos_number)) 

 

del_num_obj.returned = True 

del_num_obj.save() 

 

657 ↛ 658line 657 didn't jump to line 658, because the condition on line 657 was never true if Member.objects.filter(chaos_number=recipient.chaos_number).exists(): 

member = Member.objects.get(chaos_number=recipient.chaos_number) 

member.emailtomember_set.all().delete() 

 

recipient.delete() 

 

else: 

failed_lines.append('{}: Could not determine reason for not delivering'.format(delivery_number)) 

 

return processed_lines, failed_lines