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

gcivil-nyu-org / fall24-monday-team2 / 488

16 Dec 2024 11:56PM UTC coverage: 82.626% (+3.4%) from 79.234%
488

Pull #167

travis-pro

web-flow
Merge 815598c66 into f38dc7d7f
Pull Request #167: User trainer access

264 of 321 new or added lines in 3 files covered. (82.24%)

52 existing lines in 3 files now uncovered.

4028 of 4875 relevant lines covered (82.63%)

0.83 hits per line

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

71.36
/FitOn/dynamodb.py
1
from boto3.dynamodb.conditions import Attr
1✔
2
import boto3
1✔
3
from botocore.exceptions import NoCredentialsError, PartialCredentialsError, ClientError
1✔
4
from django.contrib.auth.hashers import make_password, check_password
1✔
5
from datetime import datetime, timezone
1✔
6

7
# from asgiref.sync import sync_to_async
8

9
from boto3.dynamodb.conditions import Key
1✔
10
from asgiref.sync import sync_to_async
1✔
11

12

13
# from django.core.files.storage import default_storage
14
from django.utils import timezone
1✔
15
from pytz import timezone
1✔
16
from django.conf import settings
1✔
17
import uuid
1✔
18

19
# Connect to DynamoDB
20
dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
21
s3_client = boto3.client("s3", region_name="us-west-2")
1✔
22

23
users_table = dynamodb.Table("Users")
1✔
24
threads_table = dynamodb.Table("ForumThreads")
1✔
25
posts_table = dynamodb.Table("ForumPosts")
1✔
26
fitness_table = dynamodb.Table("UserFitnessData")
1✔
27

28
password_reset_table = dynamodb.Table("PasswordResetRequests")
1✔
29

30
applications_table = dynamodb.Table("FitnessTrainerApplications")
1✔
31
fitness_trainers_table = dynamodb.Table("FitnessTrainers")
1✔
32
custom_plans_table = dynamodb.Table("CustomPlans")
1✔
33

34
chat_table = dynamodb.Table("chat_table")
1✔
35

36
tz = timezone("EST")
1✔
37

38
GENDER_OPTIONS = {"M": "Male", "F": "Female", "O": "Other", "PNTS": "Prefer not to say"}
1✔
39
AGE_GROUPS = [
1✔
40
    (0, 12, "Child"),
41
    (13, 19, "Teenager"),
42
    (20, 35, "Young Adult"),
43
    (36, 55, "Middle-aged"),
44
    (56, 74, "Senior"),
45
    (75, float("inf"), "Elderly"),
46
]
47

48

49
class MockUser:
1✔
50
    def __init__(self, user_data):
1✔
51
        if isinstance(user_data, dict):
1✔
52
            self.user_id = user_data.get("user_id", None)
1✔
53
            self.email = user_data.get("email", "")
1✔
54
            self.username = user_data.get("username", "")
1✔
55
            self.password = user_data.get("password", "")
1✔
56
            self.date_of_birth = user_data.get("date_of_birth", "")
1✔
57
            self.is_active = user_data.get("is_active", True)
1✔
58
            self.last_login = user_data.get("last_login", None)
1✔
59
            self.pk = self.user_id
1✔
60
        # else:
61
        #     self.user_id = None
62
        #     self.email = ""
63
        #     self.username = ""
64
        #     self.password = ""
65
        #     self.date_of_birth = ""
66
        #     self.is_active = True
67
        #     self.last_login = None
68
        #     self.pk = None
69

70
    def get_email_field_name(self):
1✔
71
        return "email"
1✔
72

73
    # def get_username(self):
74
    #     return self.username
75

76
    # def is_authenticated(self):
77
    #     return True
78

79

80
def get_user_by_username(username):
1✔
81
    # try:
82
    response = users_table.scan(
1✔
83
        FilterExpression="#n = :username",
84
        ExpressionAttributeNames={"#n": "username"},
85
        ExpressionAttributeValues={":username": username},
86
    )
87
    users = response.get("Items", [])
1✔
88
    if users:
1✔
89
        return users[0]
1✔
90
    return None
1✔
91
    # except Exception as e:
92
    #     print(f"Error querying DynamoDB for username '{username}': {e}")
93
    #     return None
94

95

96
def get_users_by_username_query(query):
1✔
97
    # try:
98
    # Scan the Users table to get all users
99
    response = users_table.scan()
1✔
100
    users = response.get("Items", [])
1✔
101

102
    # Filter users based on case-insensitive match
103
    filtered_users = [
1✔
104
        user for user in users if query.lower() in user["username"].lower()
105
    ]
106

UNCOV
107
    return filtered_users
×
108
    # except Exception as e:
109
    #     print(f"Error querying DynamoDB for usernames: {e}")
110
    #     return []
111

112

113
def create_user(
1✔
114
    user_id,
115
    username,
116
    email,
117
    name,
118
    date_of_birth,
119
    gender,
120
    height,
121
    weight,
122
    password,
123
    is_warned=False,
124
):
125
    # try:
126
    users_table.put_item(
1✔
127
        Item={
128
            "user_id": user_id,  # Partition key
129
            "username": username,
130
            "email": email,
131
            "name": name,
132
            "date_of_birth": str(date_of_birth),
133
            "gender": gender,
134
            "height": height,
135
            "weight": weight,
136
            "password": password,  # Hashed password
137
            "is_admin": False,
138
            "is_fitness_trainer": False,
139
            "is_muted": False,
140
            "is_banned": False,
141
            "punishment_date": "",
142
            "is_warned": is_warned,
143
        }
144
    )
145

146
    return True
1✔
147

148

149
def delete_user_by_username(username):
1✔
150
    response = users_table.scan(
1✔
151
        FilterExpression="#n = :username",
152
        ExpressionAttributeNames={"#n": "username"},
153
        ExpressionAttributeValues={":username": username},
154
    )
155

156
    users = response.get("Items", [])
1✔
157
    if not users:
1✔
158
        return False  # No user to delete
1✔
159

160
    # Assuming the 'user_id' is the partition key
161
    user_id = users[0]["user_id"]  # Get the user's 'user_id'
1✔
162

163
    # Delete the user by user_id (or username if it's the primary key)
164
    users_table.delete_item(Key={"user_id": user_id})  # Replace with your partition key
1✔
165

166
    # print(f"User '{username}' successfully deleted.")
167
    return True
1✔
168

169
    # except Exception as e:
170
    #     print(f"Error deleting user with username '{username}': {e}")
171
    #     return False
172

173

174
def get_user_by_email(email):
1✔
175
    # try:
176
    response = users_table.scan(FilterExpression=Attr("email").eq(email))
1✔
177
    users = response.get("Items", [])
1✔
178
    if users:
1✔
179
        return MockUser(users[0])
1✔
180
    return None
1✔
181
    # except Exception as e:
182
    #     print(f"Error querying DynamoDB for email '{email}': {e}")
183
    #     return None
184

185

186
def get_user_by_uid(uid):
1✔
187
    # try:
188
    # Fetch from DynamoDB table
189
    response = users_table.get_item(Key={"user_id": uid})
1✔
190
    user_data = response.get("Item", None)
1✔
191

192
    if user_data:
1✔
193
        return user_data
1✔
194
    return None
1✔
195
    # except Exception as e:
196
    #     return e
197

198

199
def get_mock_user_by_uid(uid):
1✔
200
    # try:
201
    # Fetch from DynamoDB table
202
    response = users_table.get_item(Key={"user_id": uid})
1✔
203
    user_data = response.get("Item", None)
1✔
204

205
    if user_data:
1✔
206
        return MockUser(user_data)
1✔
207
    return None
×
208
    # except Exception:
209
    # return None
210

211

212
def update_user_password(user_id, new_password):
1✔
213
    # try:
214
    hashed_password = make_password(new_password)
1✔
215
    response = users_table.update_item(
1✔
216
        Key={"user_id": user_id},
217
        UpdateExpression="SET password = :val",
218
        ExpressionAttributeValues={":val": hashed_password},
219
        ReturnValues="UPDATED_NEW",
220
    )
221
    return response
1✔
222
    # except Exception as e:
223
    #     print(f"Error updating user password: {e}")
224
    # return None
225

226

227
def get_last_reset_request_time(user_id):
1✔
228
    # try:
229
    response = password_reset_table.get_item(Key={"user_id": user_id})
1✔
230
    if "Item" in response:
1✔
231
        return response["Item"].get("last_request_time", None)
1✔
232
    return None
1✔
233
    # except Exception as e:
234
    #     print(f"Error fetching reset request for user_id '{user_id}': {e}")
235
    #     return None
236

237

238
def update_reset_request_time(user_id):
1✔
239
    # try:
240
    #     if not user_id:
241
    #         print("User ID is None. Cannot update reset request time.")
242
    #         return None
243

244
    # Insert a new entry or update the existing reset request time
245
    password_reset_table.put_item(
1✔
246
        Item={"user_id": user_id, "last_request_time": datetime.now(tz).isoformat()}
247
    )
248
    #     print(f"Reset request time updated for user_id '{user_id}'.")
249
    # except Exception as e:
250
    #     print(f"Error updating reset request time for user_id '{user_id}': {e}")
251

252

253
def get_user(user_id):
1✔
254
    response = users_table.get_item(Key={"user_id": user_id})
1✔
255
    return response.get("Item") or {}
1✔
256

257

258
def verify_user_credentials(username, password):
1✔
259
    user = get_user_by_username(username)
1✔
260
    if user and check_password(password, user["password"]):
1✔
261
        return user
1✔
262
    return None
1✔
263

264

265
def upload_profile_picture(user_id, profile_picture):
1✔
266
    # try:
267
    # Create a custom filename based on user_id
268
    picture_name = f"{user_id}_profile.jpg"
×
269

270
    # Upload to S3
271
    s3_client.upload_fileobj(
×
272
        profile_picture,
273
        settings.AWS_STORAGE_BUCKET_NAME,
274
        f"static/images/{picture_name}",
275
        ExtraArgs={"ContentType": profile_picture.content_type},
276
    )
277

278
    # Construct the new image URL
279
    image_url = f"https://{settings.AWS_STORAGE_BUCKET_NAME}.s3.amazonaws.com/static/images/{picture_name}"
×
280
    return image_url
×
281

282
    # except ClientError as e:
283
    #     print(e.response["Error"]["Message"])
284
    #     return None
285

286

287
def update_user(user_id, update_data):
1✔
288
    # try:
289
    # Create a mapping for reserved keywords
290
    expression_attribute_names = {}
1✔
291
    expression_attribute_values = {}
1✔
292

293
    # Build the update expression components
294
    update_expression_parts = []
1✔
295

296
    for key, value in update_data.items():
1✔
297
        placeholder_name = f"#{key}"
1✔
298
        placeholder_value = f":{key}"
1✔
299
        expression_attribute_names[placeholder_name] = key  # For reserved keywords
1✔
300
        expression_attribute_values[placeholder_value] = value["Value"]
1✔
301
        update_expression_parts.append(f"{placeholder_name} = {placeholder_value}")
1✔
302

303
    # Join the update expression parts
304
    update_expression = ", ".join(update_expression_parts)
1✔
305

306
    response = users_table.update_item(
1✔
307
        Key={"user_id": user_id},
308
        UpdateExpression=f"SET {update_expression}",
309
        ExpressionAttributeNames=expression_attribute_names,
310
        ExpressionAttributeValues=expression_attribute_values,
311
        ReturnValues="UPDATED_NEW",
312
    )
313
    return response
1✔
314

315
    # except ClientError as e:
316
    #     print(e.response["Error"]["Message"])
317
    #     return None
318

319

320
def add_fitness_trainer_application(
1✔
321
    user_id,
322
    past_experience_trainer,
323
    past_experience_dietician,
324
    resume,
325
    certifications,
326
    reference_name,
327
    reference_contact,
328
):
329
    try:
1✔
330
        # Define S3 paths
331
        resume_key = f"media/resumes/{user_id}_{resume.name}"
1✔
332
        certifications_key = None
×
333

334
        # Upload resume to S3
335
        s3_client.upload_fileobj(
×
336
            resume,
337
            settings.AWS_STORAGE_BUCKET_NAME,
338
            resume_key,
339
            ExtraArgs={"ContentType": resume.content_type},
340
        )
341

342
        # Check if certifications are provided and upload them
343
        if certifications:
×
344
            certifications_key = f"media/certifications/{user_id}_{certifications.name}"
×
345
            s3_client.upload_fileobj(
×
346
                certifications,
347
                settings.AWS_STORAGE_BUCKET_NAME,
348
                certifications_key,
349
                ExtraArgs={"ContentType": certifications.content_type},
350
            )
351

352
        # Insert data into the DynamoDB table
353
        response = applications_table.put_item(
×
354
            Item={
355
                "user_id": user_id,
356
                "past_experience_trainer": past_experience_trainer,
357
                "past_experience_dietician": past_experience_dietician,
358
                "resume": resume_key,  # Save S3 path to the uploaded resume
359
                "certifications": certifications_key,  # Save S3 path to the uploaded certification
360
                "reference_name": reference_name,
361
                "reference_contact": reference_contact,
362
            }
363
        )
364

365
        # Check response status
366
        if response["ResponseMetadata"]["HTTPStatusCode"] == 200:
×
367
            print("Fitness trainer application submitted successfully.")
×
368
            return True
×
369
        else:
370
            print(f"Failed to submit application: {response}")
×
371
            return False
×
372

373
    except (NoCredentialsError, PartialCredentialsError) as cred_err:
1✔
374
        print(f"Credentials error: {cred_err}")
×
375
        return False
×
376

377
    except ClientError as client_err:
1✔
378
        print(f"Client error: {client_err.response['Error']['Message']}")
×
379
        return False
×
380

381
    except Exception as e:
1✔
382
        print(f"Unexpected error submitting application: {e}")
1✔
383
        return False
1✔
384

385

386
def get_fitness_trainer_applications():
1✔
387
    try:
1✔
388
        # Scan DynamoDB table for all applications
389
        response = applications_table.scan()
1✔
390
        applications = response.get("Items", [])
1✔
391

392
        # Process the list of applications to generate S3 URLs
393
        for application in applications:
1✔
394
            application["resume_url"] = s3_client.generate_presigned_url(
×
395
                "get_object",
396
                Params={
397
                    "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
398
                    "Key": application["resume"],
399
                },
400
                ExpiresIn=3600,  # URL valid for 1 hour
401
            )
402

403
            # Check if certifications exist, and generate presigned URL
404
            if application.get("certifications"):
×
405
                application["certifications_url"] = s3_client.generate_presigned_url(
×
406
                    "get_object",
407
                    Params={
408
                        "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
409
                        "Key": application["certifications"],
410
                    },
411
                    ExpiresIn=3600,
412
                )
413
            else:
414
                application["certifications_url"] = None
×
415

416
            user = get_user(application["user_id"])
×
417
            application["username"] = user["username"] if user else "Unknown"
×
418

419
        return applications
1✔
420

421
    except ClientError as client_err:
×
422
        print(f"Client error: {client_err.response['Error']['Message']}")
×
423
        return []
×
424
    except Exception as e:
×
425
        print(f"Unexpected error retrieving applications: {e}")
×
426
        return []
×
427

428

429
def get_fitness_trainers():
1✔
430
    try:
×
431
        # Scan DynamoDB table for all fitness trainers
432
        response = fitness_trainers_table.scan()
×
433
        trainers = response.get("Items", [])
×
434

435
        # Process the list of fitness trainers to generate S3 URLs
436
        for trainer in trainers:
×
437
            trainer["resume_url"] = s3_client.generate_presigned_url(
×
438
                "get_object",
439
                Params={
440
                    "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
441
                    "Key": trainer["resume"],
442
                },
443
                ExpiresIn=3600,  # URL valid for 1 hour
444
            )
445

446
            # Check if certifications exist, and generate presigned URL
447
            if trainer.get("certifications"):
×
448
                trainer["certifications_url"] = s3_client.generate_presigned_url(
×
449
                    "get_object",
450
                    Params={
451
                        "Bucket": settings.AWS_STORAGE_BUCKET_NAME,
452
                        "Key": trainer["certifications"],
453
                    },
454
                    ExpiresIn=3600,
455
                )
456
            else:
457
                trainer["certifications_url"] = None
×
458

459
            user = get_user(trainer["user_id"])
×
460
            trainer["username"] = user["username"] if user else "Unknown"
×
461
            trainer["name"] = user["name"] if user else "Unknown"
×
462
            trainer["gender"] = GENDER_OPTIONS[user["gender"]] if user else "Unknown"
×
463

464
        return trainers
×
465

466
    except ClientError as client_err:
×
467
        print(f"Client error: {client_err.response['Error']['Message']}")
×
468
        return []
×
469
    except Exception as e:
×
470
        print(f"Unexpected error retrieving fitness trainers: {e}")
×
471
        return []
×
472

473

474
def make_fitness_trainer(user_id):
1✔
475
    try:
1✔
476
        users_table.update_item(
1✔
477
            Key={"user_id": user_id},
478
            UpdateExpression="SET is_fitness_trainer = :ft",
479
            ExpressionAttributeValues={":ft": True},
480
        )
481

482
        response = applications_table.get_item(Key={"user_id": user_id})
1✔
483
        application_item = response["Item"]
1✔
484

485
        fitness_trainers_table.put_item(Item=application_item)
×
486
        applications_table.delete_item(Key={"user_id": user_id})
×
487
    except Exception as e:
1✔
488
        print(f"Unexpected error making updates: {e}")
1✔
489
        return []
1✔
490

491

492
def remove_fitness_trainer(user_id):
1✔
493
    try:
1✔
494
        users_table.update_item(
1✔
495
            Key={"user_id": user_id},
496
            UpdateExpression="SET is_fitness_trainer = :ft, is_rejected = :r",
497
            ExpressionAttributeValues={":ft": False, ":r": True},
498
        )
499
        fitness_trainers_table.delete_item(Key={"user_id": user_id})
1✔
500
        applications_table.delete_item(Key={"user_id": user_id})
1✔
501
    except Exception as e:
×
502
        print(f"Unexpected error making updates: {e}")
×
503
        return []
×
504

505

506
def calculate_age_group(date_of_birth):
1✔
507
    try:
1✔
508
        dob = datetime.strptime(
1✔
509
            date_of_birth, "%Y-%m-%d"
510
        )  # Assuming the date format is YYYY-MM-DD
511
        today = datetime.today()
1✔
512
        age = today.year - dob.year - ((today.month, today.day) < (dob.month, dob.day))
1✔
513

514
        # Find the age group based on the age
515
        for min_age, max_age, group_name in AGE_GROUPS:
1✔
516
            if min_age <= age <= max_age:
1✔
517
                return group_name
1✔
518
    except (ValueError, TypeError):
1✔
519
        return "Unknown"  # Return "Unknown" if date_of_birth is invalid or missing
1✔
520

521

522
def get_standard_users():
1✔
523
    try:
1✔
524
        # Scan the DynamoDB table to fetch all standard users
525
        response = users_table.scan(
1✔
526
            FilterExpression="is_fitness_trainer = :is_fitness_trainer AND is_admin = :is_admin",
527
            ExpressionAttributeValues={
528
                ":is_fitness_trainer": False,
529
                ":is_admin": False,
530
            },
531
        )
532

533
        # Extract users
534
        standard_users = response.get("Items", [])
1✔
535

536
        # Update the gender field for each user
537
        for user in standard_users:
1✔
538
            gender_code = user.get(
1✔
539
                "gender", "PNTS"
540
            )  # Default to "PNTS" if no gender is set
541
            user["gender"] = GENDER_OPTIONS.get(
1✔
542
                gender_code, "Unknown"
543
            )  # Map gender code to description
544

545
            # Update age based on date_of_birth
546
            date_of_birth = user.get(
1✔
547
                "date_of_birth"
548
            )  # Assuming this field exists in the DynamoDB table
549
            user["age"] = (
1✔
550
                calculate_age_group(date_of_birth) if date_of_birth else "Unknown"
551
            )
552

553
        return standard_users
1✔
554

555
    except ClientError as e:
×
556
        print(f"Error fetching standard users: {e.response['Error']['Message']}")
×
557
        return []
×
558

559

560
def send_data_request_to_user(fitness_trainer_id, standard_user_id):
1✔
561
    try:
1✔
562
        standard_user = get_user(standard_user_id)
1✔
563
        fitness_trainer = get_user(fitness_trainer_id)
1✔
564

565
        if not standard_user or not fitness_trainer:
1✔
566
            print("User(s) not found")
1✔
567
            return False
1✔
568

569
        if "waiting_list_of_trainers" not in standard_user:
1✔
570
            standard_user["waiting_list_of_trainers"] = []
1✔
571

572
        if fitness_trainer_id not in standard_user["waiting_list_of_trainers"]:
1✔
573
            standard_user["waiting_list_of_trainers"].append(fitness_trainer_id)
1✔
574

575
        users_table.update_item(
1✔
576
            Key={"user_id": standard_user_id},
577
            UpdateExpression="SET waiting_list_of_trainers = :waiting_list_of_trainers",
578
            ExpressionAttributeValues={
579
                ":waiting_list_of_trainers": standard_user["waiting_list_of_trainers"]
580
            },
581
        )
582

583
        if "waiting_list_of_users" not in fitness_trainer:
1✔
584
            fitness_trainer["waiting_list_of_users"] = []
1✔
585

586
        if standard_user_id not in fitness_trainer["waiting_list_of_users"]:
1✔
587
            fitness_trainer["waiting_list_of_users"].append(standard_user_id)
1✔
588

589
        users_table.update_item(
1✔
590
            Key={"user_id": fitness_trainer_id},
591
            UpdateExpression="SET waiting_list_of_users = :waiting_list_of_users",
592
            ExpressionAttributeValues={
593
                ":waiting_list_of_users": fitness_trainer["waiting_list_of_users"]
594
            },
595
        )
596

597
        return True
1✔
598

599
    except ClientError as e:
1✔
600
        print(f"Error sending data request: {e}")
1✔
601
        return False
1✔
602

603

604
def cancel_data_request_to_user(fitness_trainer_id, standard_user_id):
1✔
605
    try:
1✔
606
        standard_user = get_user(standard_user_id)
1✔
607
        fitness_trainer = get_user(fitness_trainer_id)
1✔
608

609
        if not standard_user or not fitness_trainer:
1✔
610
            print("User(s) not found")
1✔
611
            return False
1✔
612

613
        if "waiting_list_of_trainers" in standard_user:
1✔
614
            waiting_list_of_trainers = standard_user["waiting_list_of_trainers"]
1✔
615
            if fitness_trainer_id in waiting_list_of_trainers:
1✔
616
                waiting_list_of_trainers.remove(fitness_trainer_id)
1✔
617
                users_table.update_item(
1✔
618
                    Key={"user_id": standard_user_id},
619
                    UpdateExpression="SET waiting_list_of_trainers = :new_list",
620
                    ExpressionAttributeValues={":new_list": waiting_list_of_trainers},
621
                )
622

623
        if "waiting_list_of_users" in fitness_trainer:
1✔
624
            waiting_list_of_users = fitness_trainer["waiting_list_of_users"]
1✔
625
            if standard_user_id in waiting_list_of_users:
1✔
626
                waiting_list_of_users.remove(standard_user_id)
1✔
627
                users_table.update_item(
1✔
628
                    Key={"user_id": fitness_trainer_id},
629
                    UpdateExpression="SET waiting_list_of_users = :new_list",
630
                    ExpressionAttributeValues={":new_list": waiting_list_of_users},
631
                )
632

633
        return True
1✔
634

635
    except Exception as e:
1✔
636
        print(f"Error in cancelling data request: {e}")
1✔
637
        return False
1✔
638

639

640
def add_to_list(user_id, field, value):
1✔
641
    users_table.update_item(
1✔
642
        Key={"user_id": user_id},
643
        UpdateExpression=f"SET {field} = list_append(if_not_exists({field}, :empty_list), :val)",
644
        ExpressionAttributeValues={":val": [value], ":empty_list": []},
645
    )
646

647

648
def remove_from_list(user_id, field, value):
1✔
649
    try:
1✔
650
        # Fetch the current user data to get the list
651
        user = get_user(user_id)
1✔
652
        if not user or field not in user:
1✔
653
            raise ValueError(f"Field {field} does not exist in user {user_id}")
1✔
654

655
        # Find the index of the value in the list
NEW
656
        try:
×
NEW
657
            index = user[field].index(value)
×
NEW
658
        except ValueError:
×
NEW
659
            raise ValueError(
×
660
                f"Value {value} not found in field {field} for user {user_id}"
661
            )
662

663
        # Remove the value using its index
NEW
664
        users_table.update_item(
×
665
            Key={"user_id": user_id},
666
            UpdateExpression=f"REMOVE {field}[{index}]",
667
            ConditionExpression=f"contains({field}, :val)",
668
            ExpressionAttributeValues={":val": value},
669
        )
NEW
670
        print(f"Successfully removed {value} from {field} for user {user_id}")
×
671

672
    except Exception as e:
1✔
673
        print(
1✔
674
            f"An error occurred while removing {value} from {field} for user {user_id}: {e}"
675
        )
676

677

678
def store_custom_plan(user_id, trainer_id, exercise_ids):
1✔
NEW
679
    try:
×
NEW
680
        custom_plans_table.put_item(
×
681
            Item={
682
                "user_id": user_id,
683
                "trainer_id": trainer_id,
684
                "exercise_ids": exercise_ids,
685
                "created_at": str(datetime.now()),
686
            }
687
        )
NEW
688
        return {"success": True, "message": "Custom plan created successfully."}
×
NEW
689
    except Exception as e:
×
NEW
690
        return {"success": False, "message": str(e)}
×
691

692

693
# -------------------------------
694
# Forums Functions
695
# -------------------------------
696

697

698
def create_thread(title, user_id, content, section="General"):
1✔
699
    thread_id = str(uuid.uuid4())
1✔
700
    created_at = datetime.now(tz).isoformat()
1✔
701

702
    thread = {
1✔
703
        "ThreadID": thread_id,
704
        "Title": title,
705
        "UserID": user_id,
706
        "Content": content,
707
        "Section": section,
708
        "CreatedAt": created_at,
709
        "Likes": 0,
710
        "LikedBy": [],
711
    }
712

713
    threads_table.put_item(Item=thread)
1✔
714
    return thread
1✔
715

716

717
def fetch_all_threads():
1✔
718
    threads = threads_table.scan().get("Items", [])
1✔
719

720
    for thread in threads:
1✔
721
        # Convert the thread's 'CreatedAt' string to a datetime object
722
        thread_created_at_str = thread.get("CreatedAt")
1✔
723
        if thread_created_at_str:
1✔
724
            thread["CreatedAt"] = datetime.fromisoformat(thread_created_at_str)
1✔
725

726
        # Fetch all posts for this thread
727
        replies = fetch_posts_for_thread(thread["ThreadID"])
1✔
728

729
        # Add reply count
730
        thread["ReplyCount"] = len(replies)
1✔
731

732
        # Determine the latest post (if there are any replies)
733
        if replies:
1✔
734
            latest_post = max(replies, key=lambda x: x["CreatedAt"])
1✔
735

736
            # Convert 'CreatedAt' string to a Python datetime object for the latest post
737
            last_post_time_str = latest_post["CreatedAt"]
1✔
738
            last_post_time = datetime.fromisoformat(last_post_time_str)
1✔
739

740
            thread["LastPostUser"] = latest_post["UserID"]
1✔
741
            thread["LastPostTime"] = last_post_time
1✔
742
        else:
743
            thread["LastPostUser"] = "No replies yet"
1✔
744
            thread["LastPostTime"] = None
1✔
745

746
    return threads
1✔
747

748

749
def fetch_thread(thread_id):
1✔
750
    response = threads_table.get_item(Key={"ThreadID": thread_id})
1✔
751
    return response.get("Item", None)
1✔
752

753

754
def create_post(thread_id, user_id, content):
1✔
755
    post_id = str(uuid.uuid4())
1✔
756
    created_at = datetime.now(tz).isoformat()
1✔
757

758
    post = {
1✔
759
        "PostID": post_id,
760
        "ThreadID": thread_id,
761
        "UserID": user_id,
762
        "Content": content,
763
        "CreatedAt": created_at,
764
    }
765

766
    posts_table.put_item(Item=post)
1✔
767
    return post
1✔
768

769

770
def fetch_posts_for_thread(thread_id):
1✔
771
    response = posts_table.scan(
1✔
772
        FilterExpression="#tid = :thread_id",
773
        ExpressionAttributeNames={
774
            "#tid": "ThreadID"
775
        },  # Handle 'ThreadID' as an attribute name
776
        ExpressionAttributeValues={
777
            ":thread_id": thread_id
778
        },  # Corrected to pass the thread ID
779
    )
780
    return response.get("Items", [])
1✔
781

782

783
def post_comment(thread_id, user_id, content):
1✔
784
    post_id = str(uuid.uuid4())
×
785
    created_at = datetime.now(tz).isoformat()
×
786

787
    reply = {
×
788
        "ThreadID": thread_id,
789
        "PostID": post_id,
790
        "UserID": user_id,
791
        "Content": content,
792
        "CreatedAt": created_at,
793
    }
794

795
    # Insert the reply into the DynamoDB table
796
    posts_table.put_item(Item=reply)
×
797

798

799
def get_replies(thread_id):
1✔
800
    response = posts_table.scan(
×
801
        FilterExpression="#tid = :thread_id",
802
        ExpressionAttributeNames={
803
            "#tid": "ThreadID"
804
        },  # Handle 'ThreadID' as an attribute name
805
        ExpressionAttributeValues={":thread_id": thread_id},  # Pass the thread ID value
806
    )
807
    return response.get("Items", [])
×
808

809

810
def get_thread_details(thread_id):
1✔
811
    response = posts_table.scan(
1✔
812
        FilterExpression="#tid = :thread_id",
813
        ExpressionAttributeNames={"#tid": "ThreadID"},
814
        ExpressionAttributeValues={":thread_id": thread_id},
815
    )
816

817
    items = response.get("Items", [])
1✔
818
    if items:
1✔
819
        return items[0]  # Assuming the thread details are in the first item
1✔
820
    return None
1✔
821

822

823
def delete_post(post_id, thread_id):
1✔
824
    """
825
    Deletes a post from the DynamoDB posts table based on the post ID and thread ID.
826
    """
827
    posts_table.delete_item(
1✔
828
        Key={
829
            "ThreadID": thread_id,  # Adjust this according to your table schema
830
            "PostID": post_id,
831
        }
832
    )
833
    return True
1✔
834

835

836
def fetch_filtered_threads(
1✔
837
    section=None,
838
    username="",
839
    thread_type="all",
840
    start_date="",
841
    end_date="",
842
    search_text="",
843
):
844
    # Start building the filter expression
845
    filter_expression = Attr(
1✔
846
        "ThreadID"
847
    ).exists()  # A base filter that always evaluates to true (returns all)
848

849
    # Apply username filter if provided
850
    if username:
1✔
851
        filter_expression &= Attr("UserID").eq(username)
1✔
852

853
    # Apply date range filter if provided
854
    if start_date:
1✔
855
        start_date_dt = datetime.strptime(start_date, "%Y-%m-%d").isoformat()
1✔
856
        filter_expression &= Attr("CreatedAt").gte(start_date_dt)
1✔
857

858
    if end_date:
1✔
859
        end_date_dt = datetime.strptime(end_date, "%Y-%m-%d").isoformat()
1✔
860
        filter_expression &= Attr("CreatedAt").lte(end_date_dt)
1✔
861

862
    if section:
1✔
863
        filter_expression &= Attr("Section").eq(section)
1✔
864

865
    # Apply search text filter if provided (checks both thread titles and content)
866
    if search_text:
1✔
867
        filter_expression &= Attr("Title").contains(search_text) | Attr(
1✔
868
            "Content"
869
        ).contains(search_text)
870

871
    # Apply type filter (thread/reply) if provided
872
    if thread_type == "thread":
1✔
873
        filter_expression &= Attr("ReplyCount").eq(
×
874
            0
875
        )  # Assuming threads with 0 replies are initial posts
876
    elif thread_type == "reply":
1✔
877
        filter_expression &= Attr("ReplyCount").gt(0)  # Show threads with replies
×
878

879
    # Scan the DynamoDB table with the filter expression
880
    response = threads_table.scan(FilterExpression=filter_expression)
1✔
881

882
    threads = response.get("Items", [])
1✔
883

884
    # Process each thread (e.g., add reply count and last post info)
885
    for thread in threads:
1✔
886
        thread_created_at_str = thread.get("CreatedAt")
1✔
887
        if thread_created_at_str:
1✔
888
            thread["CreatedAt"] = datetime.fromisoformat(thread_created_at_str)
1✔
889

890
        replies = fetch_posts_for_thread(thread["ThreadID"])
1✔
891
        thread["ReplyCount"] = len(replies)
1✔
892

893
        if replies:
1✔
894
            latest_post = max(replies, key=lambda x: x["CreatedAt"])
1✔
895
            last_post_time_str = latest_post["CreatedAt"]
1✔
896
            thread["LastPostUser"] = latest_post["UserID"]
1✔
897
            thread["LastPostTime"] = datetime.fromisoformat(last_post_time_str)
1✔
898
        else:
899
            thread["LastPostUser"] = "No replies yet"
1✔
900
            thread["LastPostTime"] = None
1✔
901

902
    return threads
1✔
903

904

905
def fetch_all_users():
1✔
906
    # This will scan the threads table to get all unique users
907
    response = threads_table.scan(
1✔
908
        ProjectionExpression="UserID"  # Only fetch the UserID attribute
909
    )
910

911
    threads = response.get("Items", [])
1✔
912

913
    # Set to store unique user IDs
914
    unique_users = set()
1✔
915

916
    for thread in threads:
1✔
917
        user_id = thread.get("UserID")
1✔
918
        if user_id:
1✔
919
            unique_users.add(user_id)
1✔
920

921
    # Return the list of unique user IDs
922
    return [{"username": user} for user in unique_users]
1✔
923

924

925
############################
926
# Fetchng Fitness Data     #
927
############################
928

929

930
def get_fitness_data(metric, email, start_time, end_time):
1✔
931
    # try:
932
    # print("Inside Fitness Data Function\n")
933
    # print("Start Time: \n", start_time)
934
    # print("End Time: \n", end_time)
935
    response = fitness_table.scan(
1✔
936
        FilterExpression="metric = :m AND #t BETWEEN :start AND :end AND email = :email",
937
        ExpressionAttributeNames={"#t": "time"},
938
        ExpressionAttributeValues={
939
            ":m": metric,
940
            ":email": email,
941
            ":start": start_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
942
            ":end": end_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
943
        },
944
    )
945
    # print(
946
    #     f"Metric : {metric}\nResponse: {response}\n",
947
    # )
948
    return response
1✔
949
    # except Exception as e:
950
    #     print(f"Error querying DynamoDB for fitness data. {e}")
951

952

953
def delete_threads_by_user(user_id):
1✔
954
    """
955
    Deletes all threads in the specified DynamoDB table for a given user ID.
956

957
    Parameters:
958
    - user_id (str): The UserID for which threads should be deleted.
959
    """
960
    while True:
1✔
961
        # Scan the table for items where UserID matches the specified user_id
962
        response = threads_table.scan(
1✔
963
            FilterExpression="UserID = :user",
964
            ExpressionAttributeValues={":user": user_id},
965
            ProjectionExpression="ThreadID",
966
        )
967

968
        # Extract ThreadIDs from the scan result
969
        thread_ids = [item["ThreadID"] for item in response.get("Items", [])]
1✔
970

971
        # If there are no more items to delete, exit the loop
972
        if not thread_ids:
1✔
973
            break
1✔
974

975
        # Loop through each ThreadID and delete the item
976
        for thread_id in thread_ids:
1✔
977
            threads_table.delete_item(Key={"ThreadID": thread_id})
1✔
978

979

980
def delete_thread_by_id(thread_id):
1✔
981
    # Initialize the response to scan for posts associated with the thread
982
    response = posts_table.scan(FilterExpression=Attr("ThreadID").eq(thread_id))
1✔
983

984
    # Iterate through each item and delete it
985
    for item in response["Items"]:
1✔
986
        # Include both the partition key ('PostID') and the sort key ('ThreadID')
987
        posts_table.delete_item(
×
988
            Key={
989
                "PostID": item["PostID"],  # Your partition key
990
                "ThreadID": item["ThreadID"],  # Your sort key
991
            }
992
        )
993

994
    # Delete the thread from the threads table
995
    threads_table.delete_item(
1✔
996
        Key={
997
            "ThreadID": thread_id
998
        }  # Ensure this matches your table's primary key schema
999
    )
1000

1001
    return True
1✔
1002

1003

1004
def get_thread(title, user_id, content, created_at):
1✔
1005
    # try:
1006
    response = threads_table.scan(
1✔
1007
        FilterExpression="#title = :title AND #user = :user_id AND #content = :content AND #created = :created_at",
1008
        ExpressionAttributeNames={
1009
            "#title": "Title",
1010
            "#user": "UserID",
1011
            "#content": "Content",
1012
            "#created": "CreatedAt",
1013
        },
1014
        ExpressionAttributeValues={
1015
            ":title": title,
1016
            ":user_id": user_id,
1017
            ":content": content,
1018
            ":created_at": created_at,
1019
        },
1020
    )
1021

1022
    threads = response.get("Items", [])
1✔
1023
    if threads:
1✔
1024
        return threads[0]
1✔
1025
    else:
1026
        return None
×
1027

1028
    # except Exception as e:
1029
    #     print(f"Error retrieving thread: {e}")
1030
    #     return None
1031

1032

1033
# Zejun's Code
1034

1035

1036
def create_reply(thread_id, user_id, content):
1✔
1037
    reply_id = str(uuid.uuid4())
×
1038
    created_at = datetime.now(tz).isoformat()
×
1039

1040
    reply = {
×
1041
        "ReplyID": reply_id,
1042
        "UserID": user_id,
1043
        "Content": content,
1044
        "CreatedAt": created_at,
1045
    }
1046

1047
    # Append the reply to the post's Replies attribute
1048
    try:
×
1049
        # Update the post by appending the new reply to the Replies list
1050
        # UPDATE THIS LATER
1051
        post_id = 1
×
1052
        posts_table.update_item(
×
1053
            Key={"PostID": post_id},
1054
            UpdateExpression="SET Replies = list_append(if_not_exists(Replies, :empty_list), :reply)",
1055
            ExpressionAttributeValues={
1056
                ":reply": [reply],  # Append the reply as a list item
1057
                ":empty_list": [],  # Default to an empty list if Replies doesn't exist
1058
            },
1059
        )
1060
        return {"status": "success", "reply_id": reply_id}
×
1061
    except Exception as e:
×
1062
        print(f"Error adding reply: {e}")
×
1063
        return {"status": "error", "message": str(e)}
×
1064

1065

1066
def like_comment(post_id, user_id):
1✔
1067
    # Fetch the comment by post_id
1068
    response = posts_table.get_item(Key={"PostID": post_id})
×
1069
    post = response.get("Item")
×
1070

1071
    if not post:
×
1072
        raise ValueError("Comment not found")
×
1073

1074
    liked_by = post.get("LikedBy", [])
×
1075
    likes = post.get("Likes", 0)
×
1076

1077
    # Check if the user has already liked the post
1078
    if user_id in liked_by:
×
1079
        # Unlike the post
1080
        likes = max(0, likes - 1)
×
1081
        liked_by.remove(user_id)
×
1082
        liked = False
×
1083
    else:
1084
        # Like the post
1085
        likes += 1
×
1086
        liked_by.append(user_id)
×
1087
        liked = True
×
1088

1089
    # Update the item in DynamoDB
1090
    posts_table.update_item(
×
1091
        Key={"PostID": post_id},
1092
        UpdateExpression="SET Likes = :likes, LikedBy = :liked_by",
1093
        ExpressionAttributeValues={":likes": likes, ":liked_by": liked_by},
1094
    )
1095

1096
    return likes, liked
×
1097

1098

1099
def report_comment(post_id, user_id):
1✔
1100
    # Update the comment as reported by adding user_id to ReportedBy
1101
    response = posts_table.get_item(Key={"PostID": post_id})
×
1102
    post = response.get("Item")
×
1103

1104
    if not post:
×
1105
        raise ValueError("Comment not found")
×
1106

1107
    reported_by = post.get("ReportedBy", [])
×
1108
    if user_id not in reported_by:
×
1109
        reported_by.append(user_id)
×
1110

1111
    # Update the item in DynamoDB
1112
    posts_table.update_item(
×
1113
        Key={"PostID": post_id},
1114
        UpdateExpression="SET ReportedBy = :reported_by",
1115
        ExpressionAttributeValues={":reported_by": reported_by},
1116
    )
1117

1118

1119
def delete_reply(post_id, thread_id, reply_id):
1✔
1120
    # try:
1121
    # Fetch the post to get the current list of replies
1122
    response = posts_table.get_item(Key={"PostID": post_id, "ThreadID": thread_id})
×
1123
    post = response.get("Item")
×
1124

1125
    if not post or "Replies" not in post:
×
1126
        return {"status": "error", "message": "Post or replies not found"}
×
1127

1128
    # Filter out the reply with the specific reply_id
1129
    updated_replies = [
×
1130
        reply for reply in post["Replies"] if reply["ReplyID"] != reply_id
1131
    ]
1132

1133
    # Update the post in DynamoDB with the new list of replies
1134
    posts_table.update_item(
×
1135
        Key={"PostID": post_id, "ThreadID": thread_id},
1136
        UpdateExpression="SET Replies = :updated_replies",
1137
        ExpressionAttributeValues={":updated_replies": updated_replies},
1138
    )
1139

1140
    return {"status": "success"}
×
1141
    # except Exception as e:
1142
    #     print(f"Error deleting reply: {e}")
1143
    #     return {"status": "error", "message": str(e)}
1144

1145

1146
def fetch_reported_threads_and_comments():
1✔
1147
    reported_threads = []
×
1148
    reported_comments = []
×
1149

1150
    # Fetch reported threads
1151
    try:
×
1152
        response = threads_table.scan(FilterExpression=Attr("ReportedBy").exists())
×
1153
        reported_threads = response.get("Items", [])
×
1154
        print(f"Fetched {len(reported_threads)} reported threads.")
×
1155
    except ClientError as e:
×
1156
        print(f"Error fetching reported threads: {e.response['Error']['Message']}")
×
1157

1158
    # Fetch reported comments
1159
    try:
×
1160
        response = posts_table.scan(FilterExpression=Attr("ReportedBy").exists())
×
1161
        reported_comments = response.get("Items", [])
×
1162
        print(f"Fetched {len(reported_comments)} reported comments.")
×
1163
    except ClientError as e:
×
1164
        print(f"Error fetching reported comments: {e.response['Error']['Message']}")
×
1165

1166
    return {
×
1167
        "reported_threads": reported_threads,
1168
        "reported_comments": reported_comments,
1169
    }
1170

1171

1172
def mark_thread_as_reported(thread_id):
1✔
1173
    try:
×
1174
        # Fetch the thread to check if it already has "ReportedBy" attribute
1175
        response = threads_table.get_item(Key={"ThreadID": thread_id})
×
1176
        thread = response.get("Item", {})
×
1177

1178
        reported_by = thread.get("ReportedBy", [])
×
1179

1180
        # Mark the thread as reported (or add to the list if already exists)
1181
        reported_by.append(
×
1182
            "admin"
1183
        )  # Replace "admin" with the reporting user ID if needed
1184

1185
        # Update the thread with the reported status
1186
        threads_table.update_item(
×
1187
            Key={"ThreadID": thread_id},
1188
            UpdateExpression="SET ReportedBy = :reported_by",
1189
            ExpressionAttributeValues={":reported_by": reported_by},
1190
        )
1191
        print(f"Thread {thread_id} reported.")
×
1192
    except Exception as e:
×
1193
        print(f"Error reporting thread {thread_id}: {e}")
×
1194

1195

1196
def mark_comment_as_reported(thread_id, post_id, reporting_user):
1✔
1197
    try:
×
1198
        # print(f"Fetching comment {post_id} in thread {thread_id}")
1199
        response = posts_table.get_item(Key={"ThreadID": thread_id, "PostID": post_id})
×
1200
        comment = response.get("Item", {})
×
1201
        # print(f"Comment fetched: {comment}")
1202

1203
        if not comment:
×
1204
            print(f"Comment {post_id} not found in thread {thread_id}")
×
1205
            return
×
1206

1207
        # Initialize ReportedBy if it doesn't exist
1208
        reported_by = comment.get("ReportedBy", [])
×
1209
        # print(f"Current ReportedBy list: {reported_by}")
1210

1211
        # Avoid duplicate reporting
1212
        if reporting_user not in reported_by:
×
1213
            reported_by.append(reporting_user)
×
1214

1215
        # Update the comment with the ReportedBy field
1216
        posts_table.update_item(
×
1217
            Key={"ThreadID": thread_id, "PostID": post_id},
1218
            UpdateExpression="SET ReportedBy = :reported_by",
1219
            ExpressionAttributeValues={":reported_by": reported_by},
1220
        )
1221
        # print(f"Successfully reported comment {post_id} in thread {thread_id}")
1222
    except Exception as e:
×
1223
        print(f"Error reporting comment: {e}")
×
1224

1225

1226
def mark_user_as_warned_thread(thread_id, user_id):
1✔
1227
    # try:
1228
    # print(f"Fetching user with ID: {user_id}")
1229
    response = users_table.get_item(Key={"user_id": user_id})
1✔
1230
    user = response.get("Item", {})
1✔
1231

1232
    if not user:
1✔
1233
        print(f"User with ID {user_id} not found in users_table.")
×
1234
        raise ValueError(f"User with ID {user_id} not found.")
×
1235

1236
    print(f"User fetched successfully: {user}")
1✔
1237

1238
    warning_reason = f"Warned for behavior in thread {thread_id}"
1✔
1239

1240
    users_table.update_item(
1✔
1241
        Key={"user_id": user_id},
1242
        UpdateExpression="SET is_warned = :warned, warning_reason = :reason",
1243
        ExpressionAttributeValues={
1244
            ":warned": True,
1245
            ":reason": warning_reason,
1246
        },
1247
    )
1248
    print(f"User {user_id} has been warned for comment {thread_id}.")
1✔
1249
    # except Exception as e:
1250
    #     print(f"Error warning user {user_id} for comment {thread_id}: {e}")
1251
    #     raise
1252

1253

1254
def mark_user_as_warned_comment(post_id, user_id):
1✔
1255
    # try:
1256
    # print(f"Fetching user with ID: {user_id}")
UNCOV
1257
    response = users_table.get_item(Key={"user_id": user_id})
×
UNCOV
1258
    user = response.get("Item", {})
×
1259

UNCOV
1260
    if not user:
×
1261
        print(f"User with ID {user_id} not found in users_table.")
×
1262
        raise ValueError(f"User with ID {user_id} not found.")
×
1263

UNCOV
1264
    print(f"User fetched successfully: {user}")
×
1265

UNCOV
1266
    warning_reason = f"Warned for behavior in comment {post_id}"
×
1267

UNCOV
1268
    users_table.update_item(
×
1269
        Key={"user_id": user_id},
1270
        UpdateExpression="SET is_warned = :warned, warning_reason = :reason",
1271
        ExpressionAttributeValues={
1272
            ":warned": True,
1273
            ":reason": warning_reason,
1274
        },
1275
    )
UNCOV
1276
    print(f"User {user_id} has been warned for comment {post_id}.")
×
1277
    # except Exception as e:
1278
    #     print(f"Error warning user {user_id} for comment {post_id}: {e}")
1279
    #     raise
1280

1281

1282
def set_user_warned_to_false(user_id):
1✔
1283
    """
1284
    Sets the is_warned attribute to False for a user in the Users table.
1285

1286
    :param user_id: The ID of the user to update.
1287
    """
1288
    # Initialize DynamoDB resource
1289
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1290
    users_table = dynamodb.Table("Users")
1✔
1291

1292
    # try:
1293
    # Update the is_warned attribute to False
1294
    response = users_table.update_item(
1✔
1295
        Key={
1296
            "user_id": user_id
1297
        },  # Replace this key with your partition key field name if different
1298
        UpdateExpression="SET is_warned = :warned",
1299
        ExpressionAttributeValues={":warned": False},
1300
        ReturnValues="UPDATED_NEW",
1301
    )
1302
    print(f"User {user_id} successfully updated: {response}")
1✔
1303
    return {"status": "success", "message": f"User {user_id} warning dismissed."}
1✔
1304
    # except ClientError as e:
1305
    #     print(f"Error updating user {user_id}: {e.response['Error']['Message']}")
1306
    #     return {"status": "error", "message": e.response["Error"]["Message"]}
1307

1308

1309
def get_section_stats(section_name):
1✔
1310
    # Fetch threads for the section
1311
    threads_response = threads_table.scan(
1✔
1312
        FilterExpression=Attr("Section").eq(section_name)
1313
    )
1314
    threads = threads_response.get("Items", [])
1✔
1315

1316
    # Count threads
1317
    thread_count = len(threads)
1✔
1318

1319
    # Count posts (assuming each thread has a "PostCount" attribute)
1320
    post_count = sum(thread.get("PostCount", 0) for thread in threads)
1✔
1321

1322
    # Find the latest thread
1323
    latest_thread = max(threads, key=lambda x: x.get("CreatedAt"), default=None)
1✔
1324

1325
    if latest_thread:
1✔
1326
        latest_thread_title = latest_thread.get("Title", "No threads")
1✔
1327
        latest_thread_author = latest_thread.get("UserID", "Unknown")
1✔
1328
        latest_thread_id = latest_thread.get("ThreadID", None)
1✔
1329
        created_at_raw = latest_thread.get("CreatedAt")
1✔
1330
        latest_thread_created_at = (
1✔
1331
            datetime.fromisoformat(created_at_raw) if created_at_raw else None
1332
        )
1333
    else:
1334
        latest_thread_title = "No threads"
1✔
1335
        latest_thread_author = "N/A"
1✔
1336
        latest_thread_id = None
1✔
1337
        latest_thread_created_at = "N/A"
1✔
1338

1339
    return {
1✔
1340
        "thread_count": thread_count,
1341
        "post_count": post_count,
1342
        "latest_thread": {
1343
            "title": latest_thread_title,
1344
            "author": latest_thread_author,
1345
            "thread_id": latest_thread_id,
1346
            "created_at": latest_thread_created_at,
1347
        },
1348
    }
1349

1350

1351
@sync_to_async
1✔
1352
def save_chat_message(sender, message, room_name, sender_name, test_mode=False):
1✔
1353
    if len(message) > 500:
1✔
1354
        raise Exception("Message exceeds character limit")
1✔
1355

1356
    if test_mode:
1✔
1357
        room_name = f"test_{room_name}"
1✔
1358

1359
    timestamp = int(datetime.now(tz).timestamp())
1✔
1360

1361
    chat_table.put_item(
1✔
1362
        Item={
1363
            "room_name": room_name,
1364
            "sender": sender,
1365
            "sender_name": sender_name,
1366
            "message": message,
1367
            "timestamp": timestamp,
1368
            "is_read": False,
1369
        }
1370
    )
1371

1372

1373
def get_users_without_specific_username(exclude_username):
1✔
1374
    # try:
1375
    response = users_table.scan(
1✔
1376
        FilterExpression=Attr("username").ne(exclude_username),
1377
        ProjectionExpression="user_id, username",  # Fetch only required fields
1378
    )
1379
    users = response.get("Items", [])
1✔
1380
    print(f"Users fetched for search: {users}")
1✔
1381
    return users
1✔
1382
    # except Exception as e:
1383
    #     print(
1384
    #         f"Error querying DynamoDB for users excluding username '{exclude_username}': {e}"
1385
    #     )
1386
    #     return []
1387

1388

1389
def get_chat_history_from_db(room_id):
1✔
1390
    response = chat_table.query(
1✔
1391
        KeyConditionExpression=Key("room_name").eq(room_id),
1392
        ScanIndexForward=True,
1393
    )
1394
    return response
1✔
1395

1396

1397
# def get_unread_messages_count(receiver_id):
1398
#     """
1399
#     Fetch unread messages for a specific user using the GSI.
1400
#     """
1401
#     try:
1402
#         response = chat_table.query(
1403
#             IndexName="receiver-is_read-index",  # GSI name
1404
#             KeyConditionExpression=Key("receiver").eq(receiver_id)
1405
#             & Key("is_read").eq(0),
1406
#         )
1407
#         # Count unread messages grouped by sender
1408
#         unread_counts = {}
1409
#         for item in response.get("Items", []):
1410
#             sender = item["sender"]
1411
#             unread_counts[sender] = unread_counts.get(sender, 0) + 1
1412

1413
#         return unread_counts
1414
#     except Exception as e:
1415
#         print(f"Error querying unread messages: {e}")
1416
#         return {}
1417

1418

1419
def get_users_with_chat_history(user_id):
1✔
1420
    # Scan the table to find chat history involving the given user
1421
    response = chat_table.scan(
1✔
1422
        FilterExpression=Attr("user_id").eq(user_id) | Attr("other_user_id").eq(user_id)
1423
    )
1424

1425
    chat_history = response.get("Items", [])
1✔
1426

1427
    # Debug: Check the raw chat history
1428
    print(f"Chat history raw response: {chat_history}")
1✔
1429

1430
    # Extract unique user IDs and their chat information
1431
    users_with_activity = {}
1✔
1432
    for chat in chat_history:
1✔
1433
        # Identify the other participant in the chat
1434
        other_user_id = (
1✔
1435
            chat["other_user_id"] if chat["user_id"] == user_id else chat["user_id"]
1436
        )
1437
        room_name = chat.get("room_name", "")
1✔
1438
        last_activity = chat.get(
1✔
1439
            "timestamp", 0
1440
        )  # Assuming `timestamp` indicates last activity
1441

1442
        # Store the latest activity for each user
1443
        if other_user_id not in users_with_activity:
1✔
1444
            users_with_activity[other_user_id] = {
1✔
1445
                "user_id": other_user_id,
1446
                "room_name": room_name,
1447
                "last_activity": last_activity,
1448
            }
1449
        else:
1450
            # Update the last activity if this message is more recent
1451
            users_with_activity[other_user_id]["last_activity"] = max(
×
1452
                users_with_activity[other_user_id]["last_activity"], last_activity
1453
            )
1454

1455
    # Convert dictionary to a list and sort by last activity
1456
    sorted_users = sorted(
1✔
1457
        users_with_activity.values(), key=lambda x: x["last_activity"], reverse=True
1458
    )
1459

1460
    # Fetch usernames for the users
1461
    for user in sorted_users:
1✔
1462
        user_details = get_user_by_uid(user["user_id"])
1✔
1463
        user["username"] = (
1✔
1464
            user_details.get("username", "Unknown") if user_details else "Unknown"
1465
        )
1466

1467
    return sorted_users
1✔
1468

1469

1470
def get_step_user_goals(user_id):
1✔
1471
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1472
    user_goals_table = dynamodb.Table("UserGoals")
1✔
1473
    response = user_goals_table.query(
1✔
1474
        KeyConditionExpression=Key("user_id").eq(user_id),
1475
        FilterExpression=Attr("Type").eq("steps"),
1476
    )
1477
    existing_goals = response.get("Items", [])
1✔
1478
    value = existing_goals[0]["Value"] if existing_goals else None
1✔
1479
    return value
1✔
1480

1481

1482
def get_weight_user_goals(user_id):
1✔
1483
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1484
    user_goals_table = dynamodb.Table("UserGoals")
1✔
1485
    response = user_goals_table.query(
1✔
1486
        KeyConditionExpression=Key("user_id").eq(user_id),
1487
        FilterExpression=Attr("Type").eq("weight"),
1488
    )
1489
    existing_goals = response.get("Items", [])
1✔
1490
    value = existing_goals[0]["Value"] if existing_goals else None
1✔
1491
    return value
1✔
1492

1493

1494
def get_sleep_user_goals(user_id):
1✔
1495
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1496
    user_goals_table = dynamodb.Table("UserGoals")
1✔
1497
    response = user_goals_table.query(
1✔
1498
        KeyConditionExpression=Key("user_id").eq(user_id),
1499
        FilterExpression=Attr("Type").eq("sleep"),
1500
    )
1501
    existing_goals = response.get("Items", [])
1✔
1502
    value = existing_goals[0]["Value"] if existing_goals else None
1✔
1503
    return value
1✔
1504

1505

1506
def get_custom_user_goals(user_id):
1✔
1507
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1508
    user_goals_table = dynamodb.Table("UserGoals")
1✔
1509
    response = user_goals_table.query(
1✔
1510
        KeyConditionExpression=Key("user_id").eq(user_id),
1511
        FilterExpression=Attr("Type").eq("custom"),
1512
    )
1513
    existing_goals = response.get("Items", [])
1✔
1514
    return existing_goals if existing_goals else None
1✔
1515

1516

1517
def get_activity_user_goals(user_id):
1✔
1518
    dynamodb = boto3.resource("dynamodb", region_name="us-west-2")
1✔
1519
    user_goals_table = dynamodb.Table("UserGoals")
1✔
1520
    response = user_goals_table.query(
1✔
1521
        KeyConditionExpression=Key("user_id").eq(user_id),
1522
        FilterExpression=Attr("Type").eq("activity"),
1523
    )
1524
    existing_goals = response.get("Items", [])
1✔
1525
    return existing_goals if existing_goals else None
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