review_utils.py 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. import re
  2. import time
  3. import json
  4. from functools import reduce
  5. from requests import get, put, post
  6. from gauth.auth_utils import get_gmb_id, get_auth_header
  7. from .models import Review, Reply
  8. from gauth.models import Location
  9. from django.utils import timezone
  10. _, account_id = get_gmb_id()
  11. STAR_REVIEW_NUM = {'STAR_RATING_UNSPECIFIED': 0, 'ONE': 1, 'TWO': 2, 'THREE': 3, 'FOUR': 4, 'FIVE': 5}
  12. BASE_URL = f'https://mybusiness.googleapis.com/v4/'
  13. def remove_emoji(string):
  14. emoji_pattern = re.compile("["
  15. u"\U0001F600-\U0001F64F" # emoticons
  16. u"\U0001F300-\U0001F5FF" # symbols & pictographs
  17. u"\U0001F680-\U0001F6FF" # transport & map symbols
  18. u"\U0001F1E0-\U0001F1FF" # flags (iOS)
  19. u"\U00002702-\U000027B0"
  20. u"\U000024C2-\U0001F251"
  21. "]+", flags=re.UNICODE)
  22. return emoji_pattern.sub(r'', string)
  23. def filter_4bytechar(text):
  24. return reduce(lambda x,y:x+y ,filter(lambda x: len(x.encode('utf8'))<4, text))
  25. def clean_comment(text):
  26. rules = [
  27. {r'[^\x00-\x7F]+': ''},
  28. {r'^\(Google-\s*\)(.|\n|]\s)*\(\)': ''},
  29. {r'^\n*': ''}
  30. ]
  31. for rule in rules:
  32. for (k, v) in rule.items():
  33. regex = re.compile(k)
  34. text = regex.sub(v, text)
  35. text = text.rstrip()
  36. return text
  37. def get_review_list_url(location_id, next_page_token=''):
  38. # An helper function that make a url that need to consume GMB review api
  39. return f'{BASE_URL}accounts/{account_id}/locations/{location_id}/reviews?pageToken='+next_page_token
  40. def get_reply_url(location_id, review_id):
  41. return f'{BASE_URL}accounts/{account_id}/locations/{location_id}/reviews/{review_id}/reply'
  42. def reply_review(review, replied_text):
  43. '''
  44. reply a review with a put request.
  45. :param review: review object -> a review which you want to reply.
  46. :param replied_text: string -> The actual reply that you want to post.
  47. :return:
  48. '''
  49. url = get_reply_url(review.location_id, review.review_id)
  50. headers = get_auth_header()
  51. payload = json.dumps({'comment': replied_text})
  52. response = put(url, headers=headers, data=payload)
  53. return response
  54. def insert_review_into_database(reviews, loc_id):
  55. '''
  56. Insert reviews to database.
  57. :param reviews: all reviews for location.
  58. :param loc_id: location id unrecorded_reviews belongs to.
  59. :return: It insert all reviews if it is not exits in database and return nothing.
  60. '''
  61. for rev in reviews:
  62. review_id = rev.get('reviewId')
  63. try:
  64. review = Review.objects.get(pk=review_id)
  65. except Review.DoesNotExist:
  66. review = Review(review_id=review_id)
  67. comment = rev.get('comment')
  68. if comment:
  69. comment = clean_comment(comment)
  70. comment = remove_emoji(comment)
  71. comment = filter_4bytechar(comment)
  72. review.comment = comment
  73. review.create_time = rev.get('createTime')
  74. review.update_time = rev.get('updateTime')
  75. review.star_rating = STAR_REVIEW_NUM[rev.get('starRating')]
  76. reviewer = rev.get('reviewer')
  77. review.reviewer_name = reviewer.get('displayName')
  78. review.reviewer_photo = reviewer.get('profilePhotoUrl')
  79. review.location_id = loc_id
  80. review_reply = rev.get('reviewReply')
  81. # Check if it is already replied.
  82. if review_reply:
  83. replied_text = review_reply.get('comment')
  84. create_time = review_reply.get('updateTime')
  85. reply = Reply.objects.filter(
  86. replied_text=replied_text,
  87. create_time=create_time
  88. ).first()
  89. if not reply:
  90. reply = Reply.objects.create(
  91. replied_text=replied_text,
  92. create_time=create_time
  93. )
  94. review.reply = reply
  95. else:
  96. review.reply = None
  97. review.save()
  98. def sync_all_review(loc_id):
  99. '''
  100. Sync a location if any bad thing occur i.e. any network break.
  101. :param: loc_id -> Location id of a particular location
  102. :return: None -> It just update all reviews of this location and return nothing.
  103. '''
  104. next_page_token = ''
  105. headers = get_auth_header()
  106. while True:
  107. url = get_review_list_url(loc_id, next_page_token)
  108. time.sleep(5)
  109. res = get(url, headers=headers)
  110. if res.status_code == 401:
  111. headers = get_auth_header()
  112. continue
  113. data = res.json()
  114. reviews = data.get('reviews')
  115. if reviews:
  116. insert_review_into_database(reviews, loc_id)
  117. next_page_token = data.get('nextPageToken')
  118. if next_page_token is None:
  119. break
  120. def fetch_last_20_reviews(loc_id, page_size=20):
  121. headers = get_auth_header()
  122. url = get_review_list_url(loc_id)+'&pageSize='+str(page_size)
  123. res = get(url, headers=headers)
  124. data = res.json()
  125. reviews = data.get('reviews')
  126. if len(reviews) > 0:
  127. insert_review_into_database(reviews, loc_id)
  128. def store_batch_of_reviews(reviews):
  129. for rev in reviews:
  130. location_id = rev.get('name').split('/')[-1]
  131. rev = rev.get('review')
  132. review_id = rev.get('reviewId')
  133. try:
  134. review = Review.objects.get(pk=review_id)
  135. except Review.DoesNotExist:
  136. review = Review(review_id=review_id)
  137. comment = rev.get('comment')
  138. if comment:
  139. comment = clean_comment(comment)
  140. comment = remove_emoji(comment)
  141. comment = filter_4bytechar(comment)
  142. review.comment = clean_comment(comment)
  143. review.create_time = rev.get('createTime')
  144. review.update_time = rev.get('updateTime')
  145. review.star_rating = STAR_REVIEW_NUM[rev.get('starRating')]
  146. reviewer = rev.get('reviewer')
  147. review.reviewer_name = reviewer.get('displayName')
  148. review.reviewer_photo = reviewer.get('profilePhotoUrl')
  149. review.location_id = location_id
  150. review_reply = rev.get('reviewReply')
  151. # Check if it is already replied.
  152. if review_reply:
  153. replied_text = review_reply.get('comment')
  154. create_time = review_reply.get('updateTime')
  155. reply = Reply.objects.filter(
  156. replied_text=replied_text,
  157. create_time=create_time
  158. ).first()
  159. if not reply:
  160. reply = Reply.objects.create(
  161. replied_text=replied_text,
  162. create_time=create_time
  163. )
  164. review.reply = reply
  165. else:
  166. review.reply = None
  167. review.save()
  168. def fetch_batch_of_reviews():
  169. headers = get_auth_header()
  170. url = f'{BASE_URL}accounts/{account_id}/locations:batchGetReviews'
  171. # location names should be in this format:
  172. # "accounts/103266181421855655295/locations/8918455867446117794",
  173. locations = Location.objects.all()
  174. location_names = [f'accounts/{account_id}/locations/{loc.location_id}' for loc in locations]
  175. '''
  176. post data format:
  177. {
  178. "locationNames": [
  179. string
  180. ],
  181. "pageSize": integer, -> Total number of reviews
  182. "pageToken": string, -> If has any to go next page.
  183. "orderBy": string, -> By-default updateTime desc
  184. "ignoreRatingOnlyReviews": boolean -> Whether to ignore rating-only reviews
  185. }
  186. '''
  187. payload = json.dumps({
  188. "locationNames": location_names
  189. })
  190. response = post(url, headers=headers, data=payload)
  191. if response.status_code == 200:
  192. data = response.json()
  193. location_reviews = data.get('locationReviews')
  194. store_batch_of_reviews(location_reviews)
  195. else:
  196. return None
  197. def populate_reviews():
  198. start = timezone.now()
  199. locations = Location.objects.all().values('location_id')
  200. for loc in locations:
  201. loc_id = loc.get('location_id')
  202. # sync_all_review(loc_id)
  203. fetch_last_20_reviews(loc_id, page_size=200)
  204. end = timezone.now()
  205. elapsed = end - start
  206. print(f'Elapsed time: {elapsed.seconds//60} minutes and {elapsed.seconds % 60} secs.')
  207. def get_bad_reviews(location_id, **kwargs):
  208. '''
  209. a utility function that return all reviews has less or equal three.
  210. :param location_id: str -> id of the location where reviews are belongs to
  211. :param kwargs: i.e (days=__, hours=__, minutes=__)
  212. :return: QuerySet -> all low rating reviews in last * days/hours/minutes
  213. Example --------------
  214. => get_bad_reviews(location_id='123456', days=5, hours=2, minute=1)
  215. => get_bad_reviews(location_id='123456', days=5)
  216. => get_bad_reviews(location_id='123456', hours=5)
  217. '''
  218. now = timezone.now()
  219. date = now - timezone.timedelta(**kwargs)
  220. reviews = Review.objects.filter(location_id=location_id, update_time__gte=date, star_rating__lte=3)
  221. return reviews