• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gcivil-nyu-org / team3-wed-spring25 / #632597538

04 May 2025 06:18PM UTC coverage: 95.672%. First build
#632597538

Pull #450

travis-ci

Pull Request #450: Develop to Main

435 of 476 new or added lines in 5 files covered. (91.39%)

5770 of 6031 relevant lines covered (95.67%)

0.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.08
/listings/utils.py
1
import math
1✔
2
import datetime as dt
1✔
3
from datetime import datetime, time, timedelta
1✔
4
from django.db.models import Q
1✔
5

6

7
def is_booking_slot_covered(booking_slot, intervals):
1✔
8
    """
9
    Check if the given booking slot is completely covered by at least one interval.
10

11
    :param booking_slot: A booking slot instance with attributes start_date, start_time, end_date, end_time.
12
    :param intervals: A list of tuples (start_dt, end_dt) where each is a datetime object.
13
    :return: True if the booking slot is fully within one of the intervals, otherwise False.
14
    """
15
    booking_start = datetime.combine(booking_slot.start_date, booking_slot.start_time)
×
16
    booking_end = datetime.combine(booking_slot.end_date, booking_slot.end_time)
×
17

18
    for iv_start, iv_end in intervals:
×
19
        if iv_start <= booking_start and iv_end >= booking_end:
×
20
            return True
×
21
    return False
×
22

23

24
def is_booking_covered_by_intervals(booking, intervals):
1✔
25
    """
26
    Check if every slot in the booking is completely covered by one of the intervals.
27

28
    :param booking: A booking instance with related slots accessible via booking.slots.all()
29
    :param intervals: A list of tuples (start_dt, end_dt) representing new availability intervals.
30
    :return: True if all booking slots are fully covered, otherwise False.
31
    """
32
    for slot in booking.slots.all():
×
33
        if not is_booking_slot_covered(slot, intervals):
×
34
            return False
×
35
    return True
×
36

37

38
def simplify_location(location_string):
1✔
39
    """
40
    Simplifies a location string.
41
    Example: "Tandon School of Engineering, Johnson Street, Downtown Brooklyn, Brooklyn..."
42
    becomes "Tandon School of Engineering, Brooklyn"
43

44
    Args:
45
        location_string (str): Full location string that may include coordinates
46

47
    Returns:
48
        str: Simplified location name
49
    """
50
    # Extract location name part before coordinates
51
    location_full = location_string.split("[")[0].strip()
1✔
52
    if not location_full:
1✔
53
        return ""
1✔
54

55
    parts = [part.strip() for part in location_full.split(",")]
1✔
56
    if len(parts) < 2:
1✔
57
        return location_full
1✔
58

59
    building = parts[0]
1✔
60
    city = next(
1✔
61
        (
62
            part
63
            for part in parts
64
            if part.strip()
65
            in ["Brooklyn", "Manhattan", "Queens", "Bronx", "Staten Island"]
66
        ),
67
        "New York",
68
    )
69

70
    # Handle educational institutions differently
71
    if any(
1✔
72
        term in building.lower()
73
        for term in ["school", "university", "college", "institute"]
74
    ):
75
        return f"{building}, {city}"
1✔
76

77
    street = parts[1]
1✔
78
    return f"{building}, {street}, {city}"
1✔
79

80

81
def calculate_distance(lat1, lng1, lat2, lng2):
1✔
82
    """
83
    Calculate the Haversine distance between two points on the earth.
84

85
    Args:
86
        lat1, lng1: Latitude and longitude of first point
87
        lat2, lng2: Latitude and longitude of second point
88

89
    Returns:
90
        Distance in kilometers, rounded to 1 decimal place
91
    """
92
    R = 6371  # Earth's radius in kilometers
1✔
93

94
    dlat = math.radians(lat2 - lat1)
1✔
95
    dlng = math.radians(lng2 - lng1)
1✔
96

97
    a = math.sin(dlat / 2) * math.sin(dlat / 2) + math.cos(
1✔
98
        math.radians(lat1)
99
    ) * math.cos(math.radians(lat2)) * math.sin(dlng / 2) * math.sin(dlng / 2)
100
    c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
1✔
101

102
    return round(R * c, 1)
1✔
103

104

105
def extract_coordinates(location_string):
1✔
106
    """
107
    Extract latitude and longitude from a location string.
108

109
    Args:
110
        location_string: String containing coordinates in format "name [lat,lng]"
111

112
    Returns:
113
        tuple: (latitude, longitude) as floats
114

115
    Raises:
116
        ValueError: If coordinates cannot be extracted
117
    """
118
    try:
1✔
119
        coords = location_string.split("[")[1].strip("]").split(",")
1✔
120
        return float(coords[0]), float(coords[1])
1✔
121
    except (IndexError, ValueError) as e:
1✔
122
        raise ValueError(f"Could not extract coordinates from location string: {e}")
1✔
123

124

125
def has_active_filters(request):
1✔
126
    """
127
    Check if any filters are actively applied (have non-empty values)
128

129
    Args:
130
        request: The HTTP request object
131

132
    Returns:
133
        bool: True if any filter is actively applied, False otherwise
134
    """
135
    # Check non-recurring filters first
136
    non_recurring_filters = [
1✔
137
        "max_price",
138
        "has_ev_charger",
139
        "charger_level",
140
        "connector_type",
141
    ]
142

143
    for param in non_recurring_filters:
1✔
144
        value = request.GET.get(param, "")
1✔
145
        if value and value != "None" and value != "":
1✔
146
            return True
1✔
147

148
    # Check if single-time filter is active
149
    if request.GET.get("filter_type") == "single" and any(
1✔
150
        request.GET.get(param)
151
        for param in ["start_date", "end_date", "start_time", "end_time"]
152
    ):
153
        return True
1✔
154

155
    # Check if all recurring filters are active together
156
    recurring_filters = [
1✔
157
        "recurring_start_date",
158
        "recurring_start_time",
159
        "recurring_end_time",
160
        "recurring_pattern",
161
        "recurring_end_date",
162
        "recurring_weeks",
163
    ]
164

165
    # Only consider recurring filters active if all necessary ones have values
166
    recurring_values = [request.GET.get(param, "") for param in recurring_filters]
1✔
167
    if all(value and value != "None" and value != "" for value in recurring_values):
1✔
168
        return True
×
169

170
    return False
1✔
171

172

173
def parse_date_safely(date_value):
1✔
174
    """Helper function to safely parse date values"""
175
    if not date_value:
1✔
176
        return None
1✔
177
    if isinstance(date_value, dt.date):
1✔
178
        return date_value
1✔
179
    try:
1✔
180
        return datetime.strptime(date_value, "%Y-%m-%d").date()
1✔
NEW
181
    except (ValueError, TypeError):
×
NEW
182
        return None
×
183

184

185
def parse_time_safely(time_value):
1✔
186
    """Helper function to safely parse time values"""
187
    if not time_value:
1✔
188
        return None
1✔
189
    if isinstance(time_value, dt.time):
1✔
190
        return time_value
1✔
191
    try:
1✔
192
        return datetime.strptime(time_value, "%H:%M").time()
1✔
NEW
193
    except (ValueError, TypeError):
×
NEW
194
        return None
×
195

196

197
def filter_listings(all_listings, request):
1✔
198
    """
199
    Filter listings based on request parameters.
200

201
    Args:
202
        all_listings: Initial queryset of listings
203
        request: The HTTP request containing filter parameters
204
        current_datetime: Current datetime for reference
205

206
    Returns:
207
        tuple: (filtered_listings, error_messages, warning_messages)
208
    """
209
    error_messages = []
1✔
210
    warning_messages = []
1✔
211

212
    # Apply price filter
213
    max_price = request.GET.get("max_price")
1✔
214
    if max_price:
1✔
215
        try:
1✔
216
            max_price_val = float(max_price)
1✔
217
            if max_price_val <= 0:
1✔
218
                error_messages.append("Maximum price must be positive.")
1✔
219
            else:
220
                all_listings = all_listings.filter(rent_per_hour__lte=max_price_val)
1✔
221
        except ValueError:
×
222
            pass
×
223

224
    filter_type = request.GET.get("filter_type", "single")
1✔
225

226
    # Single date/time filter
227
    if filter_type == "single":
1✔
228
        # Get both the direct values and the values from hidden fields
229
        start_date = request.GET.get("start_date") or request.GET.get("real_start_date")
1✔
230
        end_date = request.GET.get("end_date") or request.GET.get("real_end_date")
1✔
231

232
        # If we still don't have dates, check if multi-day toggle is enabled
233
        multiple_days = request.GET.get("multiple_days") == "on"
1✔
234

235
        if not start_date:
1✔
236
            if multiple_days:
1✔
237
                # Multi-day mode: use multi fields
NEW
238
                start_date = request.GET.get("start_date_multi")
×
239
            else:
240
                # Single-day mode: use single field
241
                start_date = request.GET.get("start_date_single")
1✔
242

243
        if not end_date:
1✔
244
            if multiple_days:
1✔
245
                # Multi-day mode: use multi end date
NEW
246
                end_date = request.GET.get("end_date_multi")
×
247
            else:
248
                # Single-day mode: end date equals start date
249
                end_date = request.GET.get("start_date_single")
1✔
250

251
        # Get time values (these names are consistent)
252
        start_time = request.GET.get("start_time")
1✔
253
        end_time = request.GET.get("end_time")
1✔
254
        # print("Start date:", start_date)
255
        # print("End date:", end_date)
256
        # print("Start time:", start_time)
257
        # print("End time:", end_time)
258

259
        # Validate date combinations
260
        if start_date and end_date:
1✔
261
            try:
1✔
262
                start_date_obj = parse_date_safely(start_date)
1✔
263
                end_date_obj = parse_date_safely(end_date)
1✔
264
                if start_date_obj > end_date_obj:
1✔
265
                    error_messages.append("Start date cannot be after end date.")
1✔
266
                    return [], error_messages, warning_messages
1✔
NEW
267
            except ValueError:
×
NEW
268
                error_messages.append("Invalid date format.")
×
269

270
        # Validate time combinations (only for same-day bookings or time-only searches)
271
        if start_time and end_time:
1✔
272
            try:
1✔
273
                start_time_obj = parse_time_safely(start_time)
1✔
274
                end_time_obj = parse_time_safely(end_time)
1✔
275

276
                # Only enforce start_time < end_time when:
277
                # 1. We have the same date (start_date equals end_date)
278
                # 2. Or we only have one date
279
                # 3. Or we have no dates at all (time-only search)
280
                same_day_or_time_only = (
1✔
281
                    (start_date and end_date and start_date == end_date)
282
                    or (start_date and not end_date)
283
                    or (end_date and not start_date)
284
                    or (not start_date and not end_date)
285
                )
286

287
                if same_day_or_time_only and start_time_obj >= end_time_obj:
1✔
NEW
288
                    error_messages.append("Start time must be before end time.")
×
NEW
289
                    return [], error_messages, warning_messages
×
290
            except ValueError:
×
NEW
291
                error_messages.append("Invalid time format.")
×
292

293
        # print("Start date:", start_date)
294
        # print("End date:", end_date)
295

296
        # Check for invalid date/time combinations
297
        invalid_combo = False
1✔
298
        error_message = None
1✔
299

300
        # Case 1: Start date and end time without end date
301
        if start_date and end_time and not end_date:
1✔
302
            invalid_combo = True
1✔
303
            error_message = (
1✔
304
                "When providing an end time, you must also select an end date"
305
            )
306

307
        # Case 2: End date and start time without start date
308
        elif end_date and start_time and not start_date:
1✔
309
            invalid_combo = True
1✔
310
            error_message = (
1✔
311
                "When providing a start time, you must also select a start date"
312
            )
313

314
        if invalid_combo:
1✔
315
            error_messages.append(error_message)
1✔
316
            return [], error_messages, warning_messages
1✔
317

318
        # Handle single-field cases first
319
        if any([start_date, end_date, start_time, end_time]):
1✔
320
            filtered = []
1✔
321
            for listing in all_listings:
1✔
322
                include = True
1✔
323

324
                # Individual filter logic
325
                if start_date and not (end_date or start_time or end_time):
1✔
326
                    # Only start date filter
327
                    try:
1✔
328
                        start_date_obj = parse_date_safely(start_date)
1✔
329
                        if not listing.has_availability_after_date(start_date_obj):
1✔
330
                            include = False
1✔
NEW
331
                    except ValueError:
×
NEW
332
                        pass
×
333

334
                elif end_date and not (start_date or start_time or end_time):
1✔
335
                    # Only end date filter
336
                    try:
1✔
337
                        end_date_obj = parse_date_safely(end_date)
1✔
338
                        if not listing.has_availability_before_date(end_date_obj):
1✔
339
                            include = False
1✔
NEW
340
                    except ValueError:
×
NEW
341
                        pass
×
342

343
                elif start_time and not (start_date or end_date or end_time):
1✔
344
                    # Only start time filter
345
                    try:
1✔
346
                        start_time_obj = parse_time_safely(start_time)
1✔
347
                        if not listing.has_availability_after_time(start_time_obj):
1✔
348
                            include = False
1✔
NEW
349
                    except ValueError:
×
NEW
350
                        pass
×
351

352
                elif end_time and not (start_date or end_date or start_time):
1✔
353
                    # Only end time filter
354
                    try:
1✔
355
                        end_time_obj = parse_time_safely(end_time)
1✔
356
                        if not listing.has_availability_before_time(end_time_obj):
1✔
357
                            include = False
1✔
NEW
358
                    except ValueError:
×
NEW
359
                        pass
×
360

361
                # All combinations for full date range search
362
                elif all([start_date, end_date, start_time, end_time]):
1✔
363
                    # Full range search
364
                    try:
1✔
365
                        user_start_dt = datetime.combine(
1✔
366
                            parse_date_safely(start_date), parse_time_safely(start_time)
367
                        )
368
                        user_end_dt = datetime.combine(
1✔
369
                            parse_date_safely(end_date), parse_time_safely(end_time)
370
                        )
371
                        if not listing.is_available_for_range(
1✔
372
                            user_start_dt, user_end_dt
373
                        ):
374
                            include = False
1✔
NEW
375
                    except ValueError:
×
NEW
376
                        pass
×
377

378
                # Various combinations of date and time
379
                else:
380
                    try:
1✔
381
                        # Combine the available parameters
382
                        s_date = parse_date_safely(start_date)
1✔
383
                        e_date = parse_date_safely(end_date)
1✔
384
                        s_time = parse_time_safely(start_time)
1✔
385
                        e_time = parse_time_safely(end_time)
1✔
386

387
                        # Create datetime range or partial ranges
388
                        if s_date and s_time and e_date:
1✔
389
                            # Handle two cases: same day or different days
390
                            s_date_obj = parse_date_safely(s_date)
1✔
391
                            e_date_obj = parse_date_safely(e_date)
1✔
392
                            s_time_obj = parse_time_safely(s_time)
1✔
393

394
                            if s_date == e_date:
1✔
395
                                # Same day - Check for listings with slots that:
396
                                # 1. Start before or at the requested time (start_time <= s_time_obj)
397
                                # 2. End after the requested time (end_time > s_time_obj)
398
                                # 3. Are on the requested date
399
                                if not listing.slots.filter(
1✔
400
                                    start_date=s_date_obj,
401
                                    start_time__lte=s_time_obj,
402
                                    end_time__gt=s_time_obj,
403
                                ).exists():
404
                                    include = False
1✔
405
                            else:
406
                                # Different days - Need availability from start date/time to end date
407
                                s_dt = datetime.combine(s_date_obj, s_time_obj)
1✔
408

409
                                # Check if any availability spans from start date/time
410
                                # to at least the beginning of end date
411
                                if (
1✔
412
                                    not listing.slots.filter(
413
                                        # Slot starts before or at the requested start date/time
414
                                        Q(start_date__lt=s_date_obj)
415
                                        | Q(
416
                                            start_date=s_date_obj,
417
                                            start_time__lte=s_time_obj,
418
                                        )
419
                                    )
420
                                    .filter(
421
                                        # And slot ends on or after the end date
422
                                        end_date__gte=e_date_obj
423
                                    )
424
                                    .exists()
425
                                ):
426
                                    include = False
1✔
427

428
                        elif s_date and e_date and e_time:
1✔
429
                            s_date_obj = parse_date_safely(s_date)
1✔
430
                            e_date_obj = parse_date_safely(e_date)
1✔
431
                            e_time_obj = parse_time_safely(e_time)
1✔
432

433
                            if s_date == e_date:
1✔
434
                                # Same day case:
435
                                # Filter spots with a start time < the end time and end time > the end time
436
                                if not listing.slots.filter(
1✔
437
                                    start_date=s_date_obj,
438
                                    start_time__lt=e_time_obj,  # Start time before specified end time
439
                                    end_time__gte=e_time_obj,  # End time after specified end time
440
                                ).exists():
441
                                    include = False
1✔
442
                            else:
443
                                # Different dates case:
444
                                # Filter spots with start date <= start date and end date/time >= end date/time
445
                                if (
1✔
446
                                    not listing.slots.filter(
447
                                        # Start date is on or before specified start date
448
                                        start_date__lte=s_date_obj
449
                                    )
450
                                    .filter(
451
                                        # End date/time is on or after specified end date/time
452
                                        Q(end_date__gt=e_date_obj)
453
                                        | Q(
454
                                            end_date=e_date_obj,
455
                                            end_time__gte=e_time_obj,
456
                                        )
457
                                    )
458
                                    .exists()
459
                                ):
460
                                    include = False
1✔
461

462
                        elif s_date and s_time:
1✔
463
                            # Start date with specific time to latest end date available
464
                            s_dt = datetime.combine(s_date, s_time)
1✔
465

466
                            # Filter spots that have a start date/time that is less than or equal to that date/time
467
                            # and any end date/time after that
468
                            if (
1✔
469
                                not listing.slots.filter(
470
                                    Q(start_date__lt=s_date)
471
                                    | Q(start_date=s_date, start_time__lte=s_time)
472
                                )
473
                                .filter(
474
                                    Q(end_date__gt=s_date)
475
                                    | Q(end_date=s_date, end_time__gt=s_time)
476
                                )
477
                                .exists()
478
                            ):
479
                                include = False
1✔
480

481
                        elif s_date and e_date:
1✔
482
                            # Date range filter
483
                            # Filter spots with slots that have availability
484
                            # starting ≤ start date and ending ≥ end date
485
                            s_date_obj = parse_date_safely(s_date)
1✔
486
                            e_date_obj = parse_date_safely(e_date)
1✔
487

488
                            # Check if any slot exists that:
489
                            # 1. Starts on or before the start date
490
                            # 2. Ends on or after the end date
491
                            if (
1✔
492
                                not listing.slots.filter(start_date__lte=s_date_obj)
493
                                .filter(end_date__gte=e_date_obj)
494
                                .exists()
495
                            ):
496
                                include = False
1✔
497

498
                        elif s_time and e_time:
1✔
499
                            # Time range on any day
500
                            s_time_obj = parse_time_safely(s_time)
1✔
501
                            e_time_obj = parse_time_safely(e_time)
1✔
502

503
                            # Filter for spots with a start time ≤ start_time and end time ≥ end_time on any day
504
                            if not listing.slots.filter(
1✔
505
                                start_time__lte=s_time_obj, end_time__gte=e_time_obj
506
                            ).exists():
NEW
507
                                include = False
×
508

509
                        elif e_date and e_time:
1✔
510
                            # End date with specific end time filter
511
                            # Filter spots with end date/time ≥ specified end date/time
512
                            # and any start date/time before that
513
                            e_date_obj = parse_date_safely(e_date)
1✔
514
                            e_time_obj = parse_time_safely(e_time)
1✔
515

516
                            # Check if any slot exists that:
517
                            # 1. Ends on or after the specified end date/time
518
                            # 2. Starts before the specified end date/time
519
                            if (
1✔
520
                                not listing.slots.filter(
521
                                    # Slot ends on or after the specified end date/time
522
                                    Q(end_date__gt=e_date_obj)
523
                                    | Q(end_date=e_date_obj, end_time__gte=e_time_obj)
524
                                )
525
                                .filter(
526
                                    # Slot starts before the specified end date/time
527
                                    Q(start_date__lt=e_date_obj)
528
                                    | Q(
529
                                        start_date=e_date_obj, start_time__lt=e_time_obj
530
                                    )
531
                                )
532
                                .exists()
533
                            ):
534
                                include = False
1✔
NEW
535
                    except ValueError:
×
NEW
536
                        pass
×
537

538
                if include:
1✔
539
                    filtered.append(listing)
1✔
540

541
            all_listings = filtered
1✔
542

543
    # Multiple date/time ranges filter
544
    elif filter_type == "multiple":
1✔
545
        try:
1✔
546
            interval_count = int(request.GET.get("interval_count", "0"))
1✔
547
        except ValueError:
×
548
            interval_count = 0
×
549

550
        intervals = []
1✔
551
        for i in range(1, interval_count + 1):
1✔
552
            s_date = request.GET.get(f"start_date_{i}")
1✔
553
            e_date = request.GET.get(f"end_date_{i}")
1✔
554
            s_time = request.GET.get(f"start_time_{i}")
1✔
555
            e_time = request.GET.get(f"end_time_{i}")
1✔
556
            if s_date and e_date and s_time and e_time:
1✔
557
                try:
1✔
558
                    s_dt = datetime.combine(
1✔
559
                        parse_date_safely(s_date), parse_time_safely(s_time)
560
                    )
561
                    e_dt = datetime.combine(
1✔
562
                        parse_date_safely(e_date), parse_time_safely(e_time)
563
                    )
564
                    intervals.append((s_dt, e_dt))
1✔
565
                except ValueError:
×
566
                    continue
×
567

568
        if intervals:
1✔
569
            filtered = []
1✔
570
            for listing in all_listings:
1✔
571
                available_for_all = True
1✔
572
                for s_dt, e_dt in intervals:
1✔
573
                    if not listing.is_available_for_range(s_dt, e_dt):
1✔
574
                        available_for_all = False
1✔
575
                        break
1✔
576
                if available_for_all:
1✔
577
                    filtered.append(listing)
1✔
578
            all_listings = filtered
1✔
579

580
    # Recurring pattern filter
581
    elif filter_type == "recurring":
1✔
582
        r_start_date = request.GET.get("recurring_start_date")
1✔
583
        r_start_time = request.GET.get("recurring_start_time")
1✔
584
        r_end_time = request.GET.get("recurring_end_time")
1✔
585
        pattern = request.GET.get("recurring_pattern", "daily")
1✔
586
        overnight = request.GET.get("recurring_overnight") == "on"
1✔
587
        continue_with_filter = True
1✔
588

589
        has_any_recurring = bool(r_start_date) or bool(r_start_time) or bool(r_end_time)
1✔
590
        has_all_recurring = (
1✔
591
            bool(r_start_date) and bool(r_start_time) and bool(r_end_time)
592
        )
593

594
        if has_any_recurring and not has_all_recurring:
1✔
NEW
595
            error_messages.append(
×
596
                "Start date, start time, and end time are all required for recurring bookings"
597
            )
NEW
598
            continue_with_filter = False
×
NEW
599
            all_listings = all_listings.none()
×
600

601
        if has_all_recurring:
1✔
602
            try:
1✔
603
                intervals = []
1✔
604
                start_date_obj = parse_date_safely(r_start_date)
1✔
605
                s_time = parse_time_safely(r_start_time)
1✔
606
                e_time = parse_time_safely(r_end_time)
1✔
607

608
                if s_time >= e_time and not overnight:
1✔
609
                    error_messages.append(
1✔
610
                        "Start time must be before end time unless overnight booking is selected"
611
                    )
612
                    continue_with_filter = False
1✔
613

614
                if pattern == "daily":
1✔
615
                    r_end_date = request.GET.get("recurring_end_date")
1✔
616
                    if not r_end_date:
1✔
617
                        error_messages.append(
1✔
618
                            "End date is required for daily recurring pattern"
619
                        )
620
                        continue_with_filter = False
1✔
621
                    else:
622
                        end_date_obj = parse_date_safely(r_end_date)
1✔
623
                        if end_date_obj < start_date_obj:
1✔
624
                            error_messages.append(
1✔
625
                                "End date must be on or after start date"
626
                            )
627
                            continue_with_filter = False
1✔
628
                        else:
629
                            days_count = (end_date_obj - start_date_obj).days + 1
1✔
630
                            if days_count > 90:
1✔
631
                                warning_messages.append(
×
632
                                    "Daily recurring pattern spans over 90 days, results may be limited"
633
                                )
634
                            if continue_with_filter:
1✔
635
                                for day_offset in range(days_count):
1✔
636
                                    current_date = start_date_obj + timedelta(
1✔
637
                                        days=day_offset
638
                                    )
639
                                    s_dt = datetime.combine(current_date, s_time)
1✔
640
                                    end_date_for_slot = current_date + timedelta(
1✔
641
                                        days=1 if overnight else 0
642
                                    )
643
                                    e_dt = datetime.combine(end_date_for_slot, e_time)
1✔
644
                                    intervals.append((s_dt, e_dt))
1✔
645

646
                elif pattern == "weekly":
1✔
647
                    try:
1✔
648
                        weeks_str = request.GET.get("recurring_weeks")
1✔
649
                        if not weeks_str:
1✔
650
                            error_messages.append(
1✔
651
                                "Number of weeks is required for weekly recurring pattern"
652
                            )
653
                            continue_with_filter = False
1✔
654
                        else:
655
                            weeks = int(weeks_str)
1✔
656
                            if weeks <= 0:
1✔
657
                                error_messages.append(
1✔
658
                                    "Number of weeks must be positive"
659
                                )
660
                                continue_with_filter = False
1✔
661
                            elif weeks > 52:
1✔
662
                                warning_messages.append(
×
663
                                    "Weekly recurring pattern spans over 52 weeks, results may be limited"
664
                                )
665
                            if continue_with_filter:
1✔
666
                                for week_offset in range(weeks):
1✔
667
                                    current_date = start_date_obj + timedelta(
1✔
668
                                        weeks=week_offset
669
                                    )
670
                                    s_dt = datetime.combine(current_date, s_time)
1✔
671
                                    end_date_for_slot = current_date + timedelta(
1✔
672
                                        days=1 if overnight else 0
673
                                    )
674
                                    e_dt = datetime.combine(end_date_for_slot, e_time)
1✔
675
                                    intervals.append((s_dt, e_dt))
1✔
676
                    except ValueError:
×
677
                        error_messages.append("Invalid number of weeks")
×
678
                        continue_with_filter = False
×
679

680
                if continue_with_filter and intervals:
1✔
681
                    filtered = []
1✔
682
                    for listing in all_listings:
1✔
683
                        available_for_all = True
1✔
684
                        for s_dt, e_dt in intervals:
1✔
685
                            if overnight and s_time >= e_time:
1✔
686
                                evening_available = listing.is_available_for_range(
×
687
                                    s_dt, datetime.combine(s_dt.date(), time(23, 59))
688
                                )
689
                                morning_available = listing.is_available_for_range(
×
690
                                    datetime.combine(e_dt.date(), time(0, 0)), e_dt
691
                                )
692
                                if not (evening_available and morning_available):
×
693
                                    available_for_all = False
×
694
                                    break
×
695
                            elif not listing.is_available_for_range(s_dt, e_dt):
1✔
696
                                available_for_all = False
1✔
697
                                break
1✔
698
                        if available_for_all:
1✔
699
                            filtered.append(listing)
1✔
700
                    all_listings = filtered
1✔
701
            except ValueError:
×
702
                error_messages.append("Invalid date or time format")
×
703

704
            if not continue_with_filter:
1✔
705
                from django.db.models import QuerySet
1✔
706

707
                if isinstance(all_listings, QuerySet):
1✔
708
                    all_listings = all_listings.none()
1✔
709
                else:
710
                    all_listings = []
×
711

712
    # Apply EV charger filters
713
    if request.GET.get("has_ev_charger") == "on":
1✔
714
        if hasattr(all_listings, "filter"):
1✔
715
            # It's still a QuerySet
716
            all_listings = all_listings.filter(has_ev_charger=True)
1✔
717
        else:
718
            # It's been converted to a list already
719
            all_listings = [
1✔
720
                listing for listing in all_listings if listing.has_ev_charger
721
            ]
722

723
        # Apply additional EV filters only if has_ev_charger is selected
724
        charger_level = request.GET.get("charger_level")
1✔
725
        if charger_level:
1✔
726
            all_listings = all_listings.filter(charger_level=charger_level)
1✔
727

728
        connector_type = request.GET.get("connector_type")
1✔
729
        if connector_type:
1✔
730
            all_listings = all_listings.filter(connector_type=connector_type)
1✔
731

732
    # Add filter for parking spot size
733
    if "parking_spot_size" in request.GET and request.GET["parking_spot_size"]:
1✔
734
        all_listings = all_listings.filter(
1✔
735
            parking_spot_size=request.GET["parking_spot_size"]
736
        )
737

738
    # Apply location-based filtering
739
    processed_listings = []
1✔
740

741
    location = request.GET.get("location")
1✔
742
    search_lat = request.GET.get("lat")
1✔
743
    search_lng = request.GET.get("lng")
1✔
744
    radius = request.GET.get("radius")
1✔
745

746
    if location and not (search_lat and search_lng):
1✔
NEW
747
        error_messages.append(
×
748
            "Location could not be found. Please select a valid location."
749
        )
750

751
    if radius and not (search_lat and search_lng):
1✔
NEW
752
        error_messages.append("Distance filtering requires a location to be selected.")
×
NEW
753
        radius = None  # Ignore radius if no location
×
754

755
    if search_lat and search_lng:
1✔
756
        try:
1✔
757
            search_lat = float(search_lat)
1✔
758
            search_lng = float(search_lng)
1✔
759

760
            for listing in all_listings:
1✔
761
                try:
1✔
762
                    distance = calculate_distance(
1✔
763
                        search_lat, search_lng, listing.latitude, listing.longitude
764
                    )
765
                    listing.distance = distance
1✔
766
                    if radius:
1✔
767
                        radius = float(radius)
1✔
768
                        if distance <= radius:
1✔
769
                            processed_listings.append(listing)
1✔
770
                    else:
771
                        processed_listings.append(listing)
1✔
772
                except ValueError:
1✔
773
                    listing.distance = None
1✔
774
                    processed_listings.append(listing)
1✔
775
        except ValueError:
×
776
            error_messages.append("Invalid coordinates provided")
×
777
            processed_listings = list(all_listings)
×
778
    else:
779
        for listing in all_listings:
1✔
780
            listing.distance = None
1✔
781
            processed_listings.append(listing)
1✔
782

783
    # Sort by distance if location search was applied
784
    if search_lat and search_lng:
1✔
785
        processed_listings.sort(
1✔
786
            key=lambda x: x.distance if x.distance is not None else float("inf")
787
        )
788

789
    return processed_listings, error_messages, warning_messages
1✔
790

791

792
def generate_recurring_listing_slots(
1✔
793
    start_date, start_time, end_time, pattern, is_overnight=False, **kwargs
794
):
795
    """
796
    Generate listing slots based on recurring pattern.
797

798
    Args:
799
        start_date: The starting date
800
        start_time: The start time for each slot
801
        end_time: The end time for each slot
802
        pattern: Either "daily" or "weekly"
803
        is_overnight: Whether slots extend overnight
804
        **kwargs: Additional pattern-specific parameters
805
            - For daily: end_date required
806
            - For weekly: weeks required
807

808
    Returns:
809
        list: List of dicts with start_date, start_time, end_date, end_time
810
    """
811
    listing_slots = []
1✔
812

813
    if pattern == "daily":
1✔
814
        end_date = kwargs.get("end_date")
1✔
815
        if not end_date:
1✔
816
            raise ValueError("End date is required for daily pattern")
1✔
817

818
        days_count = (end_date - start_date).days + 1
1✔
819
        for day_offset in range(days_count):
1✔
820
            current_date = start_date + timedelta(days=day_offset)
1✔
821
            end_date_for_slot = current_date + timedelta(days=1 if is_overnight else 0)
1✔
822

823
            listing_slots.append(
1✔
824
                {
825
                    "start_date": current_date,
826
                    "start_time": start_time,
827
                    "end_date": end_date_for_slot,
828
                    "end_time": end_time,
829
                }
830
            )
831

832
    elif pattern == "weekly":
1✔
833
        weeks = kwargs.get("weeks")
1✔
834
        if not weeks:
1✔
835
            raise ValueError("Number of weeks is required for weekly pattern")
1✔
836

837
        for week_offset in range(weeks):
1✔
838
            current_date = start_date + timedelta(weeks=week_offset)
1✔
839
            end_date_for_slot = current_date + timedelta(days=1 if is_overnight else 0)
1✔
840

841
            listing_slots.append(
1✔
842
                {
843
                    "start_date": current_date,
844
                    "start_time": start_time,
845
                    "end_date": end_date_for_slot,
846
                    "end_time": end_time,
847
                }
848
            )
849

850
    else:
851
        raise ValueError(f"Unknown pattern: {pattern}")
1✔
852

853
    return listing_slots
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc