generated from Code-Institute-Org/gitpod-full-template
-
Notifications
You must be signed in to change notification settings - Fork 3
/
classes.py
641 lines (498 loc) · 18.8 KB
/
classes.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
"""
Classes - Message Sub-module
=============
Contains the Wyspa class, for storing, retreiving,
editing, and preparing data to be read and written
to the Wyspa Database.
Classes: Wyspa
"""
# https://stackoverflow.com/questions/24434510/how-to-deal-with-pylints-too-many-instance-attributes-message
# pylint: disable=too-many-arguments, too-many-instance-attributes
# Considered attributes for Class and believe it is reasonable in this scenario
import re
from random import uniform
from datetime import datetime
from dateutil import tz
from bson.objectid import ObjectId
from flask import session
from geopy.geocoders import Nominatim
from wyspa.factory.initialisation import mongo
# Create a class for WYSPAs
class Wyspa():
"""
A class to represent the Wyspa Message.
Contains all required methods to store,
retrieve, edit, and prepare data appropriately,
along with reading and writing Wyspas to the Database.
Attributes
-----------
_id : str
A unique identifier for the Wyspa.
author : str
The username of the author who created the Wyspa.
message : str
The main body of the message.
mood : int
A number representing the mood of the message (0,1,2) =>
(Sad, Neutral, Happy)
location : dict
Dict format: {lat: int, long: int}
A dictionary containing latitude and longitude.
comments : list
List format: [{author: comment}, {author: comment},...]
A list of dictionaries, containing comments and their
respective author.
listens : list
List format: [username, username,...]
A unique list of users usernames who have
listened to a Wyspa.
listen_count : int
An integer representation of the length of the
listen list.
expiry : datetime
A datetime object. When writing to the DB this is
in the users time zone, when reading from the DB this is
in the Server's time zone (UTC).
Methods
-------
wyspa_id():
Returns the protected _id property of the Wyspa.
get_info()
Formats and returns the current Wyspa's attributes as a dict.
write_wyspa()
Writes a Wyspa to the Database.
edit_wyspa(message, mood, location, expiry)
Edits an existing Wyspa, and updates the Database.
remove_comment(index)
Removes a comment from the Wyspa, and updates the Database.
add_comment(new_comment, comment_author)
Adds a comment to the Wyspa, and updates the Database.
add_listen(listener)
Adds a Listen to the Wyspa, and updates the Database.
get_by_id(_id)
Retrieves a Wyspa from the database using an ID.
get_by_user(username)
Retrieves all of a given user's Wyspas from the Database.
get_random_wyspa()
Obtains a random Wyspa from the Database.
get_all_wyspas()
Retreives all Wyspas from the Database.
delete_wyspa(_id)
Deletes a Wyspa from the Database.
location_to_latlong(user_location)
Converts the name of a Location to scrambled geocoordinates.
string_to_datetime(expiry_date, expiry_time)
Converts datetime string to time zone aware datetime object.
datetime_to_string(formatted_expiry)
Converts datetime object to user time zone formatted string.
wyspa_to_map(wyspas)
Isolates and prepares the required data for Map routing.
def whitespace_check(message):
Checks given string for non-whitespace characters.
"""
def __init__(self, author, message, mood, location, expiry=None,
comments=None, listens=None, listen_count=None, _id=None):
"""Constructs all the necessary attributes for the Wyspa object.
Attributes
-----------
_id : str
A unique identifier for the Wyspa.
author : str
The username of the author who created the Wyspa.
message : str
The main body of the message.
mood : int
A number representing the mood of the message (0,1,2) =>
(Sad, Neutral, Happy)
location : dict
Dict format: {lat: int, long: int}
A dictionary containing latitude and longitude.
comments : list
List format: [{author: comment}, {author: comment},...]
A list of dictionaries, containing comments and their
respective author.
listens : list
List format: [username, username,...]
A unique list of users usernames who have
listened to a Wyspa.
listen_count :int
An integer representation of the length of the
listen list.
expiry : datetime
A datetime object. When writing to the DB this is
in the users timezone, when reading from the DB this is
in the Server's timezone (UTC).
"""
self._id = _id
self.author = author
self.message = message
self.mood = mood
self.location = location
self.comments = comments if comments else []
self.listens = listens if listens else []
self.listen_count = listen_count if listen_count else 0
self.expiry = expiry if expiry else None
@property
def wyspa_id(self):
"""Returns the protected _id property of the Wyspa.
Parameters
----------
None
Returns
-------
_id : str
"""
# https://stackoverflow.com/questions/24833362/pylint-warning-w0212-with-properties-accessing-a-protected-member-how-to-avoi
return self._id
def get_info(self):
"""Formats and returns the current Wyspa's attributes as a dict.
The format of the dictionary allows the return of this method
to be written directly to the Database.
Parameters
----------
None
Returns
-------
info : dict
"""
info = {'author': self.author, 'message': self.message,
'mood': self.mood, 'location': self.location,
'expiry': self.expiry, 'comments': self.comments,
'listens': self.listens, 'listen_count': self.listen_count}
return info
def write_wyspa(self):
"""Writes a Wyspa to the Database.
Writes the output of the get_info
method directly to the database.
Parameters
----------
None
Returns
-------
None
"""
mongo.db.messages.insert_one(self.get_info())
def edit_wyspa(self, message, mood, location, expiry):
"""Edits an existing Wyspa, and updates the Database.
Updates the attributes of a Wyspa object, and updates
the respective entry in the Database.
Parameters
----------
message : str
The main body of the message.
mood : int
A number representing the mood of the message (0,1,2) =>
(Sad, Neutral, Happy)
location : dict
Dict format: {lat: int, long: int}
A dictionary containing latitude and longitude.
expiry : datetime
A datetime object. When writing to the DB this is
in the users timezone, when reading from the DB this is
in the Server's timezone (UTC).
Returns
-------
None
"""
self.message = message
self.mood = mood
self.location = location
self.expiry = expiry
mongo.db.messages.update_one({"_id": ObjectId(self._id)},
{"$set": self.get_info()})
def remove_comment(self, index):
"""Removes a comment from the Wyspa, and updates the Database.
Uses the index of the comment required for deletion
to remove the comment from the object's comment list, and
updates the Database accordingly.
Parameters
----------
index : int
The index of the comment to be removed
Returns
-------
None
"""
self.comments.pop(index)
mongo.db.messages.update_one({"_id": ObjectId(self._id)},
{"$set": self.get_info()})
def add_comment(self, new_comment, comment_author):
"""Adds a comment to the Wyspa, and updates the Database.
Appends a new comment (in the form of a dictionary) to the
comment attribute, and updates the Database.
Parameters
----------
new_comment : str
The body of the comment
comment_author: str
The author of the comment
Returns
-------
None
"""
self.comments.append({comment_author: new_comment})
mongo.db.messages.update_one({"_id": ObjectId(self._id)},
{"$set": self.get_info()})
def add_listen(self, listener):
"""Adds a Listen to the Wyspa, and updates the Database.
Appends the username of the listener to the
Listen attribute, increments the listen count,
and updates the database.
Parameters
----------
listener : str
The username of the listener.
Returns
-------
None
"""
self.listens.append(listener)
self.listen_count += 1
mongo.db.messages.update_one({"_id": ObjectId(self._id)},
{"$set": self.get_info()})
@classmethod
def get_by_id(cls, _id):
"""Retrieves a Wyspa from the database using an ID.
Checks the ID passed in is a valid ObjectID,
queries the Database for the ID, then returns
the database entry as a constructed Wyspa object.
Parameters
----------
listener : str
The username of the listener.
Returns
-------
data: Wyspa Object or bool
Constructed Wyspa object returned if successful.
False bool returned if unsuccessful.
"""
# Checks ID passed in is valid ObjectID
if ObjectId.is_valid(_id):
data = mongo.db.messages.find_one({"_id": ObjectId(_id)})
if data is not None:
return cls(**data)
# Return False if message not in DB or if ID is not valid ObjectID
data = False
return data
@classmethod
def get_by_user(cls, username):
"""Retrieves all of a given user's Wyspas from the Database.
Queries the Database for a given user's Wyspas,
converts the Expiry datetime object to logged in user's
time zone, then returns a list of constructed Wyspa objects.
Parameters
----------
username : str
The username to query the "author" field within the database.
Returns
-------
return_data : list
A list of constructed Wyspa objects.
"""
# Query the database for all user's Wyspas
data = list(mongo.db.messages.find({"author": username}))
# Initialise return_data list
return_data = []
# Checks to see if any documents have been retrieved
if data is not None:
# Obtain user's time zone from session
user_timezone = tz.gettz(session["timezone"])
# For each Wyspa obtained
for wyspa in data:
# Set the time zone of each Wyspa's expiry.
wyspa['expiry'] = wyspa['expiry'].astimezone(user_timezone)
# Create a Wyspa with the data, and append to return_data list
return_data.append(cls(**wyspa))
return return_data
@classmethod
def get_random_wyspa(cls):
"""Obtains a random Wyspa from the Database.
Obtains a single random sample through MongoDB
aggregation, and returns the document as a constructed
Wyspa object.
Parameters
----------
None
Returns
-------
data : object or list
object : Constructed Wyspa Object (if successful).
list : empty list (if unsuccessful).
"""
# Obtain a random sample from the messages database
data = list(mongo.db.messages.aggregate(
[{"$sample": {"size": 1}}]))
if data != []:
# Pass the database object into the Wyspa class
data = cls(**data[0])
return data
@classmethod
def get_all_wyspas(cls):
"""Retreives all Wyspas from the Database.
Queries the database for all Wyspas, sorted by
listen_count in descending order, and returns
a list of constructed Wyspa objects.
Parameters
----------
None
Returns
-------
return_data : list
A list of all available constructed Wyspa objects.
"""
data = list(mongo.db.messages.aggregate(
[{"$sort": {"listen_count": -1}}]))
# Initialise return_data list
return_data = []
# Checks to see if any documents have been retrieved
if data != []:
# Returns a list of constructed Wyspas from documents
for wyspa in data:
return_data.append(cls(**wyspa))
return return_data
@staticmethod
def delete_wyspa(_id):
"""Deletes a Wyspa from the Database.
Removes a Wyspa from the Database given its
unique ID, once the ID has been verified as
a valid ObjectID.
Parameters
----------
_id : str
The ID of Wyspa to be deleted.
Returns
-------
None
"""
if ObjectId.is_valid(_id):
mongo.db.messages.remove_one({"_id": ObjectId(_id)})
@staticmethod
def location_to_latlong(user_location):
"""Converts a literal location to scrambled geocoordinates.
Uses GeoPy to convert a string location to
latitude and longitude, then scrambles them
by a random floating-point value of between
0.1 and -0.1.
Parameters
----------
user_location : str
A literal string location to be converted to lat/long
Returns
-------
latlong : dict or bool
A dictionary containing scrambled "lat" and "lng" values,
False bool if unsuccessful conversion.
"""
# Instantiate Geopy Geolocator
geolocator = Nominatim(user_agent="WYSPA")
# Convert Location to Lat/Long Co-ordinates
location = geolocator.geocode(user_location)
if location is None:
latlong = False
else:
lat = location.latitude + (round(uniform(0.1, -0.1), 10))
long = location.longitude + (round(uniform(0.1, -0.1), 10))
# Scramble Lat/Long by +-0.1 (float), and save as dict
latlong = {"lat": lat, "lng": long}
return latlong
@staticmethod
def string_to_datetime(expiry_date, expiry_time):
"""Converts datetime string to time zone aware datetime object.
Converts date and time strings to datetime object,
applies users timezone to the datetime, verifies
the datetime is in the future, and returns the
datetime object.
Parameters
----------
expiry_date : str
Date in required format: "%d-%m-%Y".
expiry_time : str
Time in required format: "%H:%M".
Returns
-------
formatted_expiry: datetime or bool
A timezone aware datetime object (if successful).
False bool (if unsuccessful).
"""
# Convert date and time strings to Datetime object.
date_string = expiry_date + " " + expiry_time
date_format = "%d-%m-%Y %H:%M"
formatted_expiry = datetime.strptime(date_string, date_format)
# Set users time zone
user_timezone = tz.gettz(session["timezone"])
formatted_expiry = formatted_expiry.replace(tzinfo=user_timezone)
# Set up time zone aware date time object for comparison
server_timezone = tz.tzlocal()
server_time = datetime.now().replace(tzinfo=server_timezone)
# Ensure expiry date is in the future
if formatted_expiry < server_time:
formatted_expiry = False
return formatted_expiry
@staticmethod
def datetime_to_string(formatted_expiry):
"""Converts datetime object to user time zone formatted string.
Applies the user's timezone to the datetime object
obtained from the Database, and converts it to a list
containing the string formatted Date and Time.
Parameters
----------
formatted_expiry : datetime
Expiry datetime object stored in database.
Returns
-------
list
formatted_date : str
dormatted_time : str
"""
# Set expiry date timezone
user_timezone = tz.gettz(session["timezone"])
formatted_expiry = formatted_expiry.astimezone(user_timezone)
# Extract date and time from datetime object
date_format = "%d-%m-%Y %H:%M"
string_date = formatted_expiry.strftime(date_format)
# Seperate date and time
formatted_date = string_date[:10]
formatted_time = string_date[11:]
return [formatted_date, formatted_time]
@staticmethod
def wyspa_to_map(wyspas):
"""Isolates and prepares the required data for Map routing.
Takes a list of Wyspa objects, isolates and formats the required
data for the map routing, and returns the prepared data.
Parameters
----------
wyspas : list
A list of constructed Wyspa objects.
Returns
-------
prepared_data : list
A list of dictionaries containing condensed Wyspa parameters.
"""
# Initialise prepared data list
prepared_data = []
# Checks the list contains Wyspas
if wyspas is not None:
prepared_data = []
# Isolates the data the map routing requires
for wyspa in wyspas:
prepared_data.append(
{"_id": str(wyspa.wyspa_id), "location": wyspa.location,
"mood": wyspa.mood, "listens": (wyspa.listen_count)})
return prepared_data
@staticmethod
def whitespace_check(message):
"""Checks given string for non-whitespace characters.
Parameters
----------
message : str
String to check for non-whitespace characters
Returns
-------
whitspace_check : re.match Object
"""
# Set regex pattern for not empty and not whitespace
# https://stackoverflow.com/questions/7967075/regex-for-not-empty-and-not-whitespace
whitespace_pattern = re.compile(r"(.|\s)*\S(.|\s)*")
whitespace_check = re.match(whitespace_pattern, message)
# Check the message and return the re.match object
return whitespace_check