Advertisement
Guest User

Untitled

a guest
Jul 26th, 2016
79
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 61.92 KB | None | 0 0
  1. class Title(models.Model):
  2. # In the DB, title_slug is a VARCHAR(50)
  3. title_slug = models.SlugField((u'Slug'), unique=True, help_text="This is *very* important. This value, along with the Primary Author Slug, is used to define the URL for this review. It must be unique, and garbage characters in here make us (and our customers) look bad. However, if you change this, you need to also setup a Redirect to map the old URL to the new one.")
  4.  
  5. # What issue this Title/Review was published in
  6. issue = models.ForeignKey('issues.Issue', blank=True, null=True, related_name='titles')
  7. issue.custom_filter_spec = True
  8. print_supplement = models.ManyToManyField('issues.IssuePrintEditionSupplement', blank=True)
  9. print_supplement_section = models.ForeignKey('issues.IssuePrintEditionSupplementSection', blank=True, null=True)
  10.  
  11. # Ex., Draft, Assigned, Published, etc.
  12. state = models.CharField(max_length=1, choices=constants.TITLE_STATE_CHOICES, default='D')
  13.  
  14. # Online only
  15. web_only = models.BooleanField(default=False, help_text="The review can be online, but do not publish it in a magazine, even if someone selects an Issue for it.")
  16.  
  17. # A Kirkus Star?
  18. star = models.BooleanField(default=False, help_text="Second only to The Prize as the greatest award in literature.")
  19.  
  20. # Is it featured? Not sure how this is used, but likely it pulls it up to the Home Page
  21. featured_review = models.BooleanField("Recommended review", default=False, help_text="Not a Star, but you'd still recommend this book to a friend.")
  22.  
  23. # Include this book in the list considered for The Prize
  24. prize_worthy = models.BooleanField(default=False, help_text="Mail this book to the judges!")
  25.  
  26. # These are used in the CMS as list_display fields, but *are not* used to drive logic.
  27. # The values are rolled-up here so that we don't need extra DB queries when building
  28. # the listings.
  29. denorm_reviewer = models.CharField("qReviewer", editable=False, max_length=32, blank=True, null=True)
  30. denorm_publisher = models.CharField("qPublisher", editable=False, max_length=64, blank=True, null=True)
  31. denorm_issue_date = models.DateField("qIssue Date", editable=False, blank=True, null=True)
  32.  
  33. # If the review was particularly negative, we might want to know so that we don't
  34. # push this person with Marketing outreach (like ad sales and Pro)
  35. is_negative_review = models.BooleanField("Ouch! Particulary bad review", default=False, help_text="Let's just say that the author might be upset with the review and we probably shouldn't send marketing outreach campaigns their way.")
  36.  
  37. # Guessing this means whether it was a Kirkus Discovery submission
  38. discovery = models.BooleanField("Indie", default=False, help_text="This is an Indie review--ain't no shame in that, baby!")
  39.  
  40. # How the manuscript was submitted for review.
  41. manuscript_submission_format = models.ForeignKey(ManuscriptSubmissionFormat, blank=True, null=True)
  42.  
  43. # Reviews have four text fields to play with, though we really only use 'review'.
  44. abstract = models.TextField(blank=True)
  45. review = models.TextField(blank=True)
  46. aftermatter = models.TextField(blank=True)
  47. notes = models.TextField(blank=True)
  48. synopsis = models.TextField(blank=True)
  49.  
  50. # Field for extra keywords/phrases that should be indexed for search.
  51. search_extra = models.TextField("Additional text for Search", blank=True, help_text="The text here will be indexed for searching, but will not be visible to the customer.")
  52.  
  53. ################ Book-related ####################
  54. series = models.ForeignKey(Series, blank=True, null=True)
  55. title = models.CharField(max_length=255)
  56. subtitle = models.CharField(max_length=255, blank=True, help_text="Use the following formatting in Title or Subtitle fields:<br><b><b>bold</b></b><br><i><i>italic</i></i><br><u><u>underline</u></u>")
  57. volume = models.CharField(max_length=10, blank=True)
  58. author = models.ManyToManyField(Author, through='TitleAuthor', blank=True)
  59. publisher = models.ForeignKey(Publisher, blank=True, null=True)
  60. num_pages = models.IntegerField("Number of Pages", blank=True, null=True, validators=[MinValueValidator(0)])
  61.  
  62. # These are demormalized fields set by signals off of MediaType and Author
  63. primary_isbn = models.CharField(max_length=20, blank=True, help_text="This is the MOST IMPORTANT ISBN field. It is published with the reivew, and used to lookup the cover from Amazon.")
  64. primary_author_slug = models.CharField(max_length=50, blank=True, help_text="This is used in the review\'s URL. It's *very* important. If you change this, you need to also setup a Redirect to map the old URL to the new one.")
  65. primary_author_first_name = models.CharField("Author First", max_length=100, blank=True)
  66. primary_author_last_name = models.CharField("Author Last", max_length=100, blank=True)
  67. primary_author_list = models.TextField("Authors", blank=True)
  68.  
  69. # There's no Admin for Stage, but expect possible Strings like "Adult" or "Children's"
  70. # The annoying part of this is that various 'stage', 'category', and 'classification1'
  71. # queries are used to drive the front-end, top-level navigation results.
  72. stage = models.ForeignKey(Stage)
  73.  
  74. category = ChainedForeignKey(Category, chained_field="stage", chained_model_field="stage", include_jquery=False, null=True, blank=True)
  75.  
  76. classification1 = ChainedForeignKey(Classification1, chained_field="category", chained_model_field="category", include_jquery=False, null=True, blank=True)
  77. classification2 = ChainedForeignKey(Classification2, chained_field="classification1", chained_model_field="classification1", include_jquery=False, null=True, blank=True)
  78. classification3 = ChainedForeignKey(Classification3, chained_field="classification2", chained_model_field="classification2", include_jquery=False, null=True, blank=True)
  79.  
  80. navigation_categories = models.ManyToManyField(NavigationCategory, blank=True, limit_choices_to={'parent__isnull': True}, help_text="These are very important. They define the sections of the site that you want this book to show up in. For Indie books, select the Indie category, but you MUST also select another relevant category.")
  81. navigation_formats = models.ManyToManyField(NavigationFormat, blank=True)
  82.  
  83. # Age ranges
  84. lower_age = models.SmallIntegerField(blank=True, null=True, default=0)
  85. upper_age = models.SmallIntegerField(blank=True, null=True, default=0)
  86.  
  87. # For ticket #553
  88. audience = models.SmallIntegerField(choices=constants.TITLE_AUDIENCE, blank=True, default=3)
  89.  
  90. # When the book was/will-be published
  91. pub_date = models.DateField("Book Pub Date", blank=True, null=True)
  92. ################ End Book-related ####################
  93.  
  94. received_date = models.DateField("Galley Rec'd", blank=True, null=True)
  95.  
  96. # BKD: This is a bad related_name. Change to titles?
  97. reviewer = models.ForeignKey(User, blank=True, null=True, related_name='reviewer')
  98. assigned_time = models.DateTimeField(blank=True, null=True)
  99. assigned_by = models.ForeignKey(User, blank=True, null=True)
  100. date_assignment_acknowledged = models.DateTimeField("Assignment Acknowledged", blank=True, null=True)
  101.  
  102. review_editor = models.ForeignKey(User, blank=True, null=True, related_name='editor', limit_choices_to={'groups__id': 1})
  103. # review_date is temporary
  104. review_date = models.DateTimeField(editable=False, blank=True, null=True)
  105. # review_deadline1 = models.DateField("First Deadline", blank=True, null=True, help_text='This field is deprecated, and hidden in the Admin')
  106. review_deadline2 = models.DateField("Reviewer Deadline", blank=True, null=True)
  107.  
  108. review_type = models.ForeignKey(ReviewRate)
  109.  
  110. # EBS: Does the new Invoice package deprecate these fields???!
  111. invoice_state = models.CharField(max_length=1, choices=constants.INVOICE_STATE_CHOICES, null=True, blank=True, editable=False)
  112. invoice_date = models.DateField("Invoice Date", blank=True, null=True)
  113. base_rate = models.DecimalField('Base Rate', max_digits=8, decimal_places=2, null=True, blank=True)
  114. addl_rate = models.DecimalField('Additional Pay', max_digits=8, decimal_places=2, null=True, blank=True, default='0.00', help_text="Wow--this book is book is long, yo.")
  115. total_rate = models.DecimalField('Total', max_digits=8, decimal_places=2, null=True, blank=True)
  116.  
  117. # This was an actual data field in the Nielsen data:
  118. # old_title_id = models.IntegerField(blank=True, null=True, editable=False)
  119.  
  120. is_archive = models.BooleanField(default=False)
  121. is_express_review = models.BooleanField("Express Review", default=False, help_text="A lot like the Pony Express, but with Indie books.")
  122.  
  123. has_sent_clipping = models.BooleanField(default=False, blank=True, help_text="If this is checked, the system thinks it has already sent a clipping for this review, and it will not send another one.")
  124. has_sent_prize_book_request = models.BooleanField(default=False, blank=True, help_text="If this is checked, the system thinks it has already sent a book request for this review, and it will not send another one.")
  125.  
  126. is_public = models.BooleanField("Marketing Promotion", default=False, help_text="Does things only the marketing department can understand.")
  127.  
  128. roundup = models.BooleanField("Roundup", default=False, help_text="Giddy-up cowboy--we've got some reviews to roundup.")
  129.  
  130. # available_rights = models.ManyToManyField('TitleRight', blank=True)
  131.  
  132. ################ Affiliate URLs/links ####################
  133. # These are denorm caches, since we can look this up.
  134. amazon_affiliate_link = models.CharField(max_length=300, blank=True, null=True, help_text="We try auto-detecting this URL, but it doesn\'t always work for Kindle books. You might need to paste in a new URL for Kindle-only books.")
  135. amazon_affiliate_link_updated_date = models.DateTimeField(editable=False, null=True)
  136. amazon_isbn = models.CharField("IndieBound Buy Button ISBN", max_length=24, blank=True, null=True, help_text="This is what Amazon responded with as the ISBN they use.")
  137. amazon_ean = models.CharField("BN Buy/Button EAN", max_length=32, blank=True, null=True, help_text="This comes from Amazon, but we use it for the BN Buy button. Replace with a BN ID if you want to link to a Nook book.")
  138.  
  139. # Potentially Deprecated, pending further development with BN API integration.
  140. # These fields are now hidden within the Admin since we rely on BN lookups now.
  141. # bn_affiliate_link = models.TextField(blank=True, null=True)
  142. # bn_affiliate_link_updated_date = models.DateTimeField(editable=False, null=True)
  143. # kobo_affiliate_link = models.TextField(blank=True, null=True)
  144. # kobo_affiliate_link_updated_date = models.DateTimeField(editable=False, null=True)
  145. ################ END Affiliate URLs/links ####################
  146.  
  147. published_date = models.DateTimeField("Publish Online", null=True, blank=True, help_text="If the `State` is `Publish`, this date sets the time after which the review will be on the website. The review will not be online without this set!")
  148. created_date = models.DateField(editable=False)
  149. updated_date = models.DateTimeField(editable=False)
  150.  
  151. ################ Custom Managers ####################
  152. objects = TitleManager()
  153. available_titles = AvailableTitleManager()
  154.  
  155. ################ Instance Variables ####################
  156. _is_app_only = None
  157. _book_cover_object = None
  158. _best_seller_object = None
  159. _vintage_review_object = None
  160. _review_first_sentence = ''
  161. _review_middle_block = ''
  162. _review_last_sentence = ''
  163. _review_age_range_str = ''
  164. _review_parsed = ''
  165. _oliver_data = {}
  166. _bic_subjects = []
  167. _parenthetical_genres = []
  168.  
  169. @classmethod
  170. def gen_title_slug(cls, title_string):
  171. # Check for duplicate slug. If one exists, we add some random characters
  172. # and cross our fingers.
  173. #
  174. # start with your standard-issue `slugify`
  175. slug = slugify(title_string)
  176.  
  177. had_collision = False
  178. first_collision_slug = None
  179.  
  180. def _slug_is_available(s):
  181. if Title.objects.filter(title_slug=slug).exists():
  182. return False
  183. else:
  184. return True
  185.  
  186. is_available = _slug_is_available(slug)
  187.  
  188. count = 0
  189. mode = 'letters'
  190. nums = '0123456789'
  191.  
  192. while not is_available:
  193. count += 1
  194. had_collision = True
  195.  
  196. if first_collision_slug is None:
  197. first_collision_slug = slug
  198.  
  199. if mode == 'letters':
  200. # Append a random char
  201. slug = '{}{}'.format(slug, random.choice(string.letters))
  202.  
  203. elif mode == 'numbers':
  204. # Append a random number
  205. slug = '{}{}'.format(slug, random.choice(nums))
  206.  
  207. if count == 4 or len(slug) > 50: # we won't be able to store it in the DB
  208. # try something else
  209. if mode == 'letters':
  210. # Switch it
  211. mode = 'numbers'
  212. slug = ''.join(random.choice(nums) for i in xrange(8))
  213.  
  214. else:
  215. break
  216.  
  217. if count > 8:
  218. # This is too much
  219. break
  220.  
  221. is_available = _slug_is_available(slug)
  222.  
  223. if is_available:
  224. return slug, had_collision, first_collision_slug
  225.  
  226. return None, had_collision, first_collision_slug
  227.  
  228.  
  229. @classmethod
  230. def get_stage_and_audience_for_string(cls, s):
  231. if s == 'children':
  232. the_stage = Stage.objects.get(id=2)
  233. the_audience = 0
  234.  
  235. elif s == 'adult':
  236. the_stage = Stage.objects.get(id=1)
  237. the_audience = 3
  238.  
  239. elif s == 'teen':
  240. the_stage = Stage.objects.get(id=2)
  241. the_audience = 4
  242.  
  243. else: # Default to adult if we get something we don't understand
  244. the_stage = Stage.objects.get(id=1)
  245. the_audience = 3
  246.  
  247. return the_stage, the_audience
  248.  
  249.  
  250. def denormalize_isbn(self):
  251. """
  252. Set the primary_isbn from the first MediaType with an ISBN.
  253. """
  254. mt_qs = self.mediatype_set.exclude(isbn='').order_by('type')[0:1]
  255. if mt_qs.count() == 1:
  256. self.primary_isbn = mt_qs.get().isbn
  257.  
  258. # Added 11/6/12
  259. # Even though we're calling save() on `self`, this method can
  260. # be triggered during a delete(), which isn't what we want,
  261. # so ignore the exception if `self` isn't actually in the DB.
  262. try:
  263. self.save()
  264. except:
  265. pass
  266.  
  267. @property
  268. def should_have_an_invoice(self):
  269. if self.reviewer and self.state in ('E', 'C', 'R', 'L', 'P'):
  270. return True
  271. else:
  272. return False
  273.  
  274. @property
  275. def files(self):
  276. return TitleFileRelatedManager(self)
  277.  
  278. @property
  279. def indie_review(self):
  280. if self.indie_reviews.all().count() == 1:
  281. return self.indie_reviews.all()[0]
  282.  
  283. else:
  284. return None
  285.  
  286. def disclosure_note(self):
  287. single_note = "Note: {last} is a freelance contributor to Kirkus."
  288. multi_note = "Note: {names} are freelance contributors to Kirkus."
  289.  
  290. aid_set = []
  291. names = []
  292.  
  293. for ta in self.titleauthor_set.all():
  294. if ta.author and ta.author.is_kirkus_contributor:
  295. if ta.author.last_name and ta.author.id not in aid_set:
  296. names.append(ta.author.last_name)
  297. aid_set.append(ta.author.id)
  298.  
  299. if names:
  300. if len(names) > 2:
  301. return multi_note.format(names='{base} and {last}'.format(base=', '.join(names[:-1]), last=names[-1]))
  302.  
  303. elif len(names) == 2:
  304. return multi_note.format(names=' and '.join(names))
  305.  
  306. elif len(names) == 1:
  307. return single_note.format(last=names[0])
  308. else:
  309. return None
  310.  
  311. def save(self):
  312. # Grab a previous version from the DB (if it exists) for comparison
  313. prev_version = None
  314.  
  315. if self.id:
  316. try:
  317. prev_version = Title.objects.get(id=self.id)
  318. except Title.DoesNotExist:
  319. pass
  320.  
  321. # Added 2015-01-09
  322. # Truncate the slug if we're a new record and the slug is too long
  323. if ((prev_version is None) and (len(self.title_slug) > 50)):
  324. self.title_slug = self.title_slug[0:50]
  325.  
  326. # We now track the date the review was submitted:
  327. if not self.review_date and self.review:
  328. self.review_date = datetime.now()
  329.  
  330. # If someone sets this Title to publish but didn't set the
  331. # date for when to go live, assume they meant right now.
  332. if not self.published_date and self.state == 'P':
  333. self.published_date = datetime.now()
  334.  
  335. # Lame crap because the original model didn't use auto date-stamps
  336. if not self.id:
  337. self.created_date = date.today()
  338.  
  339. self.updated_date = datetime.now()
  340.  
  341. # Added 2013-02-28
  342. if self.stage:
  343. # Added 2012-11-13
  344. # If our 'stage' is Children's, but our 'audience' is Adult..
  345. if self.stage.id == 2 and self.audience == 3:
  346. # ...set the audience to Children.
  347. self.audience = 0
  348.  
  349. # Likewise, if our stage is Adult, but the audience is Children...
  350. elif self.stage.id == 1 and self.audience == 0:
  351. # ...set the audience to Adult.
  352. self.audience = 3
  353.  
  354. # Added 9/5/12
  355. if self.addl_rate:
  356. self.addl_rate = extract.price_like(str(self.addl_rate))
  357.  
  358. # Added 8/13/12
  359. # If the reivew has been assigned...
  360. if self.reviewer and self.state == 'A':
  361. # Set a due-date if missing
  362. if self.review_deadline2 is None:
  363. self.review_deadline2 = self.suggest_review_deadline()
  364.  
  365. # Fill-in missing assignment-times
  366. if self.assigned_time is None:
  367. self.assigned_time = datetime.now()
  368.  
  369. #
  370. if settings.USE_ZMQ_WORKERS:
  371. # We'll be sending background-work requests...
  372. work_pusher = pusher.Pusher(ip=settings.ZMQ_IP_ADDRESS, port=settings.ZMQ_PORT_WORKER)
  373.  
  374. task_data = {"id": self.id, "model": "Title", "package": "kirkus.titles.models"}
  375.  
  376. if self.is_published():
  377. task_data["action"] = "call"
  378. task_data["method"] = "put_in_search_index"
  379. else:
  380. task_data["action"] = "call"
  381. task_data["method"] = "remove_from_search_index"
  382.  
  383. work_pusher.send_json(task_data)
  384.  
  385. else:
  386. # Make it (new) searchable
  387. if self.is_published():
  388. try:
  389. self.put_in_search_index()
  390. except Exception: # likely a NoServerAvailable error
  391. # Don't take the system down for this!
  392. pass
  393. else:
  394. # Remove it from the index
  395. try:
  396. self.remove_from_search_index()
  397. except Exception: # likely a NoServerAvailable error
  398. # Don't take the system down for this!
  399. pass
  400.  
  401. # Added 2014-6-23
  402. # Denorm a few properties for list_display usage
  403. if self.reviewer:
  404. self.denorm_reviewer = self.reviewer.username
  405.  
  406. if self.publisher:
  407. self.denorm_publisher = self.publisher.publisher_name
  408.  
  409. if self.issue:
  410. self.denorm_issue_date = self.issue.issue_date
  411.  
  412. # Clean it everytime
  413. if self.synopsis:
  414. self.synopsis = fmt.full_html_strip(self.synopsis)
  415.  
  416. #
  417. if prev_version is not None:
  418. old_path = prev_version.get_absolute_url()
  419. the_site = Site.objects.get_current()
  420.  
  421. # If this review was just un-published...
  422. if self.state in ('U', 'Z', 'T') and prev_version.state == 'P':
  423. # ..we need to setup a Redirect
  424. try:
  425. r = Redirect.objects.get(site=the_site, old_path=old_path)
  426.  
  427. except Redirect.DoesNotExist:
  428. r = Redirect(site=the_site, old_path=old_path, new_path='/')
  429.  
  430. else:
  431. r.new_path = '/'
  432.  
  433. r.save()
  434.  
  435. # Or if this review was just resurected..
  436. elif self.state == 'P' and prev_version.state in ('U', 'Z', 'T'):
  437. # ..we need to delete any matching Redirects
  438. Redirect.objects.filter(site=the_site, old_path=old_path).delete()
  439.  
  440. # Save it!
  441. super(Title, self).save()
  442.  
  443. def __unicode__(self):
  444. return u'{s}'.format(s=self.title)
  445.  
  446. def citation_date(self):
  447. if self.published_date and self.published_date.year > 2011:
  448. return self.published_date
  449.  
  450. else:
  451. if self.issue and self.issue.issue_date:
  452. return self.issue.issue_date
  453.  
  454. else:
  455. return None
  456.  
  457. def get_synopsis(self):
  458. """
  459. Returns the synopsis.
  460. If there is no synopsis, ask oliver for one.
  461. """
  462. if not self.synopsis:
  463. oliver_data = self.oliver_data()
  464. synopsis = oliver_data.get('google_description', None)
  465.  
  466. if synopsis:
  467. self.synopsis = fmt.full_html_strip(synopsis)
  468.  
  469. try:
  470. self.save()
  471. except:
  472. pass
  473.  
  474. return self.synopsis
  475.  
  476. def get_updated_field(self):
  477. return 'updated_date'
  478.  
  479. def review_stripped(self):
  480. stripped = removetags(self.review, 'p')
  481. return stripped
  482.  
  483. @cached_property
  484. def pro_page_url(self):
  485. qs = self.titleauthor_set.all().select_related('author')
  486.  
  487. for ta in qs:
  488. if ta.author and ta.author.is_pro_published and ta.author.is_pro:
  489. return ta.author.get_absolute_url()
  490.  
  491. return None
  492.  
  493. @cached_property
  494. def get_pro_author(self):
  495. """
  496. Gets the first Pro author. EBS: This logic fails for books with multiple Pro authors.
  497. """
  498. qs = self.titleauthor_set.all().select_related('author')
  499.  
  500. for ta in qs:
  501. if ta.author and ta.author.is_pro_published and ta.author.is_pro:
  502. return ta.author
  503.  
  504. return None
  505.  
  506. def get_selected_pro_genres(self):
  507. return [atga.pro_genre for atga in self.authortitlegenreassignment_set.all()]
  508.  
  509. def authors(self):
  510. return self.primary_author_list.replace('\n', '<br>')
  511. authors.allow_tags = True
  512. authors.admin_order_field = 'primary_author_last_name'
  513.  
  514. def primary_author_obj(self):
  515. qs = self.titleauthor_set.all()
  516.  
  517. try:
  518. return qs[0].author
  519. except:
  520. return None
  521.  
  522. def primary_author(self):
  523. rtn = ""
  524. sep = ""
  525.  
  526. if self.primary_author_last_name:
  527. rtn += self.primary_author_last_name
  528. sep = ", "
  529.  
  530. if self.primary_author_first_name:
  531. rtn += sep + self.primary_author_first_name
  532.  
  533. return rtn
  534. primary_author.admin_order_field = 'primary_author_last_name'
  535.  
  536. def primary_author_full_name(self):
  537. qs = self.titleauthor_set.all()
  538.  
  539. try:
  540. return u'{name}'.format(name=qs[0].author.full_name())
  541. except:
  542. return u'{first} {last}'.format(first=self.primary_author_first_name, last=self.primary_author_last_name)
  543.  
  544. def secondary_author(self):
  545. qs = self.titleauthor_set.all()
  546.  
  547. try:
  548. return qs[1].author
  549. except:
  550. return None
  551.  
  552. @property
  553. def secondary_author_first_name(self):
  554. sa = self.secondary_author()
  555.  
  556. if sa:
  557. return u'{first}'.format(first=sa.first_name)
  558.  
  559. @property
  560. def secondary_author_last_name(self):
  561. sa = self.secondary_author()
  562.  
  563. if sa:
  564. return u'{last}'.format(last=sa.last_name)
  565.  
  566. def awards(self):
  567. results = []
  568.  
  569. if self.star:
  570. results.append(BookAward(award=1, title=self))
  571.  
  572. # Fake a Best-of award
  573. sup = self.get_best_of_supplement()
  574. if sup:
  575. results.append(BookAward(award=2, title=self, award_year=sup.date_published_online.year))
  576.  
  577. results.extend(self.bookaward_set.exclude(award__in=(1, 2)))
  578.  
  579. return results
  580.  
  581. def stagecat(self):
  582. if self.discovery == True:
  583. return "Indie"
  584.  
  585. elif self.stage_id == 2:
  586. return "Children"
  587.  
  588. elif self.stage_id == 1 and self.classification1_id == 8:
  589. return "Mystery"
  590.  
  591. elif self.stage_id == 1 and self.classification1_id == 10:
  592. return "Science Fiction"
  593.  
  594. elif self.stage_id == 1 and self.category_id == 1:
  595. return "Fiction"
  596.  
  597. elif self.stage_id == 1 and self.category_id == 2:
  598. return "Nonfiction"
  599.  
  600. else:
  601. return "Adult"
  602.  
  603. def stagecat_slug(self):
  604. if self.discovery == True:
  605. return "indie"
  606.  
  607. elif self.stage_id == 2:
  608. return "childrens-books"
  609.  
  610. elif self.stage_id == 1 and self.classification1_id == 8:
  611. return "mystery"
  612.  
  613. elif self.stage_id == 1 and self.classification1_id == 10:
  614. return "science-fiction"
  615.  
  616. elif self.stage_id == 1 and self.category_id == 1:
  617. return "fiction"
  618.  
  619. elif self.stage_id == 1 and self.category_id == 2:
  620. return "non-fiction"
  621.  
  622. elif self.category_id > 0:
  623. tmp = u'%s' % (self.category)
  624. return slugify(tmp).lower()
  625.  
  626. else:
  627. return "adult"
  628.  
  629. def print_section(self):
  630. if self.stage_id == 2:
  631. return "Children"
  632.  
  633. elif self.stage_id == 1 and self.category_id == 1:
  634. return "Fiction"
  635.  
  636. elif self.stage_id == 1 and self.category_id == 2:
  637. return "Nonfiction"
  638.  
  639. else:
  640. return "Adult"
  641.  
  642. def web_genre(self):
  643. qs = self.nav_sections()
  644.  
  645. if qs.count() > 0:
  646. return qs[0].name
  647. else:
  648. return None
  649.  
  650. def web_section(self, allow_indie=True):
  651. if self.discovery and allow_indie:
  652. return "Indie"
  653.  
  654. elif self.stage_id == 2:
  655. return "Children"
  656.  
  657. elif self.stage_id == 1 and self.classification1_id == 8:
  658. return "Mystery"
  659.  
  660. elif self.stage_id == 1 and self.classification1_id == 10:
  661. return "Sci-Fi"
  662.  
  663. elif self.stage_id == 1 and self.category_id == 1:
  664. return "Fiction"
  665.  
  666. elif self.stage_id == 1 and self.category_id == 2:
  667. return "Nonfiction"
  668.  
  669. else:
  670. return "Adult"
  671.  
  672. def print_subsection(self):
  673. if self.stage_id == 1 and self.classification1_id == 8:
  674. return "Mystery"
  675.  
  676. elif self.stage_id == 1 and self.classification1_id == 10:
  677. return "Science Fiction"
  678.  
  679. elif self.category_id > 0:
  680. tmp = u'%s' % (self.category)
  681. return tmp
  682.  
  683. else:
  684. return ""
  685.  
  686. def nav_sections(self):
  687. return self.navigation_categories.filter(active=True, display_in_books_nav=True).exclude(navcat_slug="indie")
  688.  
  689. def main_nav_sections(self):
  690. return self.nav_sections().filter(parent__isnull=True)
  691.  
  692. def best_date(self):
  693. if self.pub_date:
  694. return str(self.pub_date)
  695.  
  696. else:
  697. return ""
  698.  
  699. def pub_date_state(self):
  700. # this is used te determine how much of teh review to show
  701. # 0 = more than 14 days until pub_date
  702. # 1 = less than 14 days until pub_date
  703. # 2 = pub_date has passed
  704. pds = 2
  705.  
  706. if self.pub_date:
  707. pub_yr = self.pub_date.year
  708. pub_mo = self.pub_date.month
  709. pub_dy = self.pub_date.day
  710.  
  711. days_to_pub = days_diff(pub_yr, pub_mo, pub_dy)
  712. if days_to_pub > 14:
  713. pds = 0
  714. elif days_to_pub > 0:
  715. pds = 1
  716. else:
  717. pds = 2
  718.  
  719. return pds
  720.  
  721. def public_review(self):
  722. """DEPRECATED"""
  723. pr = False
  724. pds = self.pub_date_state()
  725.  
  726. if self.is_public or pds > 0:
  727. pr = True
  728.  
  729. return pr
  730.  
  731. @cached_property
  732. def best_seller_object(self):
  733. if self._best_seller_object is None:
  734. try:
  735. self._best_seller_object = BestSeller.objects.get(title=self)
  736. except BestSeller.DoesNotExist:
  737. pass
  738. except MultipleObjectsReturned:
  739. self._best_seller_object = BestSeller.objects.filter(title=self)[0]
  740.  
  741. return self._best_seller_object
  742.  
  743. @property
  744. def vintage_review_object(self):
  745. now = datetime.now()
  746.  
  747. if self._vintage_review_object is None:
  748. try:
  749. self._vintage_review_object = VintageReview.objects.get(title=self, published_date__lte=now)
  750. except VintageReview.DoesNotExist:
  751. pass
  752.  
  753. return self._vintage_review_object
  754.  
  755. def is_published(self):
  756. now = datetime.now()
  757.  
  758. if (self.published_date and self.published_date <= now) and (self.state == 'P'):
  759. return True
  760. else:
  761. return False
  762.  
  763. def is_app_only(self):
  764. """
  765. Return boolean indicated whether this Title only has MediaType(s) of type App.
  766. """
  767. # We cache the result since we're likely to be called multiple times from the front-end code
  768. if self._is_app_only is not None:
  769. return self._is_app_only
  770.  
  771. found_an_app = False
  772. found_a_non_app_type = False
  773.  
  774. if self.category and self.category.category_name == 'iPad':
  775. found_an_app = True
  776.  
  777. else:
  778. for mt in self.mediatype_set.all():
  779. if mt.type == 7:
  780. found_an_app = True
  781.  
  782. else:
  783. found_a_non_app_type = True
  784.  
  785. self._is_app_only = found_an_app and not found_a_non_app_type
  786.  
  787. return self._is_app_only
  788.  
  789. def isbns(self):
  790. """
  791. Return a list of all ISBNs we know about for this Title.
  792. """
  793. results = []
  794.  
  795.  
  796. if self.primary_isbn:
  797. results.append(fmt.super_flat(self.primary_isbn))
  798.  
  799. for mt in self.mediatype_set.all():
  800. if mt.isbn:
  801. the_isbn = fmt.super_flat(mt.isbn)
  802. if the_isbn not in results:
  803. results.append(the_isbn)
  804.  
  805. return results
  806.  
  807. def isbn10(self):
  808. """Return the first 10-char ISBN we have."""
  809. for i in self.isbns():
  810. if len(i) == 10:
  811. return i
  812.  
  813. return ''
  814.  
  815. def isbn13(self):
  816. """Return the first 13-char ISBN we have."""
  817. for i in self.isbns():
  818. if len(i) == 13:
  819. return i
  820.  
  821. return ''
  822.  
  823. def get_absolute_url(self):
  824. author_slug = self.primary_author_slug if self.primary_author_slug else 'unknown'
  825. url = u"/book-reviews/{author_slug}/{title_slug}/".format(author_slug=urlquote(author_slug), title_slug=urlquote(self.title_slug))
  826.  
  827. return iri_to_uri(url)
  828.  
  829. def similar_titles(self, published_only=True):
  830. """
  831. Return a list of Titles that are recommended as similar, and that have had
  832. this book listed as being similar.
  833.  
  834. This call makes a few DB queries and iterates over the results. Best to cache it.
  835. """
  836. # Books listed as similars to this title
  837. similar_set = self.similartitle_set.all()
  838.  
  839. # Books that list this book as similar
  840. similar_to_set = self.similar.all()
  841.  
  842. results = []
  843. ids = [] # Used to track uniqueness
  844.  
  845. for st in similar_set:
  846. if st.similar and st.similar.id not in ids:
  847. if published_only and not st.similar.is_published():
  848. continue
  849.  
  850. results.append(st.similar)
  851. ids.append(st.similar.id)
  852.  
  853. for st in similar_to_set:
  854. if st.title.id not in ids:
  855. if published_only and not st.title.is_published():
  856. continue
  857.  
  858. results.append(st.title)
  859. ids.append(st.title.id)
  860.  
  861. # Shuffle the order...
  862. random.shuffle(results)
  863.  
  864. return results
  865.  
  866. @cached_property
  867. def app_cover_art(self):
  868. """
  869. Return all MediaTypeAssets (for all my MediaTypes) that contain cover-shots
  870. """
  871. r = []
  872. for mt in self.mediatype_set.all():
  873. for mta in mt.mediatypeasset_set.all():
  874. if (mta.asset_type == 1) and mta.is_cover:
  875. r.append(mta)
  876.  
  877. return r
  878.  
  879. @cached_property
  880. def screen_shots(self):
  881. """
  882. Return all MediaTypeAssets (for all my MediaTypes) that contain screen-shots
  883.  
  884. # EBS: This is a good candidate for caching, since the review template calls it a couple times.
  885. """
  886. r = []
  887. for mt in self.mediatype_set.all():
  888. for mta in mt.mediatypeasset_set.all():
  889. if (mta.asset_type == 1) and mta.is_screenshot:
  890. r.append(mta)
  891.  
  892. return r
  893.  
  894. @cached_property
  895. def videos(self):
  896. """
  897. Return all MediaTypeAssets (for all my MediaTypes) that contain video embed code.
  898.  
  899. # EBS: This is a good candidate for caching, since the review template calls it a couple times.
  900. """
  901. r = []
  902. for mt in self.mediatype_set.all():
  903. for mta in mt.mediatypeasset_set.all():
  904. if (mta.asset_type == 2) and (mta.embed_code is not None):
  905. r.append(mta)
  906.  
  907. return r
  908.  
  909. def age_range_str(self):
  910. if not self._review_age_range_str:
  911. # Then we need to build one
  912.  
  913. # First, let's look at our stored values
  914. lower = self.lower_age
  915. upper = self.upper_age
  916.  
  917. if ((lower is None) and (upper is None)) or ((lower == 0) and (upper == 0)):
  918. # Then we need new values
  919. rp = ReviewParser()
  920. rp.parse(self.review)
  921.  
  922. self._review_first_sentence = rp.first_sentence
  923. self._review_middle_block = rp.middle_block
  924. self._review_last_sentence = rp.last_sentence
  925.  
  926. lower, upper = rp.extract_age_range()
  927.  
  928. # Save these for next time
  929. self.lower_age = lower
  930. self.upper_age = upper
  931.  
  932. try:
  933. self.save()
  934. except:
  935. pass
  936.  
  937. if (lower is not None and upper is not None) and (upper > lower):
  938. self._review_age_range_str = '{l} - {u}'.format(l=lower, u=upper)
  939.  
  940. elif lower is not None and upper is None:
  941. self._review_age_range_str = '{l} & up'.format(l=lower)
  942.  
  943. else:
  944. self._review_age_range_str = None
  945.  
  946. return self._review_age_range_str
  947.  
  948. def review_first_sentence(self):
  949. if not self._review_first_sentence:
  950. rp = ReviewParser()
  951. rp.parse(self.review)
  952.  
  953. self._review_first_sentence = rp.first_sentence
  954. self._review_middle_block = rp.middle_block
  955. self._review_last_sentence = rp.last_sentence
  956. self._review_parsed = rp.content
  957.  
  958. # lower, upper = rp.extract_age_range()
  959. # if lower is not None and upper is not None:
  960. # self._review_age_range_str = '{l} - {u}'.format(l=lower, u=upper)
  961. # else:
  962. # self._review_age_range_str = None
  963.  
  964. return self._review_first_sentence
  965.  
  966. def review_middle_block(self):
  967. if not self._review_middle_block:
  968. rp = ReviewParser()
  969. rp.parse(self.review)
  970.  
  971. self._review_first_sentence = rp.first_sentence
  972. self._review_middle_block = rp.middle_block
  973. self._review_last_sentence = rp.last_sentence
  974. self._review_parsed = rp.content
  975.  
  976. # lower, upper = rp.extract_age_range()
  977. # if lower is not None and upper is not None:
  978. # self._review_age_range_str = '{l} - {u}'.format(l=lower, u=upper)
  979. # else:
  980. # self._review_age_range_str = None
  981.  
  982. return self._review_middle_block
  983.  
  984. def review_last_sentence(self):
  985. if not self._review_last_sentence:
  986. rp = ReviewParser()
  987. rp.parse(self.review)
  988.  
  989. self._review_first_sentence = rp.first_sentence
  990. self._review_middle_block = rp.middle_block
  991. self._review_last_sentence = rp.last_sentence
  992. self._review_parsed = rp.content
  993.  
  994. # lower, upper = rp.extract_age_range()
  995. # if lower is not None and upper is not None:
  996. # self._review_age_range_str = '{l} - {u}'.format(l=lower, u=upper)
  997. # else:
  998. # self._review_age_range_str = None
  999.  
  1000. return self._review_last_sentence
  1001.  
  1002. def review_parsed(self):
  1003. if not self._review_parsed:
  1004. rp = ReviewParser()
  1005. rp.parse(self.review)
  1006.  
  1007. self._review_first_sentence = rp.first_sentence
  1008. self._review_middle_block = rp.middle_block
  1009. self._review_last_sentence = rp.last_sentence
  1010. self._review_parsed = rp.content
  1011.  
  1012. # lower, upper = rp.extract_age_range()
  1013. # if lower is not None and upper is not None:
  1014. # self._review_age_range_str = '{l} - {u}'.format(l=lower, u=upper)
  1015. # else:
  1016. # self._review_age_range_str = None
  1017.  
  1018. return self._review_parsed
  1019.  
  1020. def review_last_sentence_is_usable(self):
  1021. """
  1022. Replaces template logic that looked for a last_sentence that was between 3 and 300 chars.
  1023. Also allows customizing the logic to add exceptions for some long reviews.
  1024. """
  1025. s = self.review_last_sentence()
  1026.  
  1027. if not s:
  1028. return False
  1029.  
  1030. # The easy test:
  1031. if 3 < len(s) < 300:
  1032. return True
  1033.  
  1034. else:
  1035. # Allow a wider range for newer reviews
  1036. if self.published_date and self.published_date.year >= 2011:
  1037. if 3 < len(s) < 500:
  1038. return True
  1039.  
  1040. return False
  1041.  
  1042. def has_primary_isbn(self):
  1043. return bool(self.primary_isbn)
  1044. has_primary_isbn.boolean = True
  1045. has_primary_isbn.short_description = "ISBN"
  1046.  
  1047. def cover_image_preview_url(self):
  1048. return self.cover_image_url('180', mode='s3_direct_url')
  1049.  
  1050. def cover_image_preview_urls(self):
  1051. urls = []
  1052.  
  1053. for width in settings.COVER_WIDTHS:
  1054. urls.append({'width': width, 'url': self.cover_image_url(width, mode='s3_direct_url')})
  1055.  
  1056. return urls
  1057.  
  1058. def has_cover_by_size(self, width, check_disk=True):
  1059. """
  1060. Used by: util/printout/gen_issue_xtags.py
  1061. """
  1062. result = False
  1063.  
  1064. # If this is an app, get the MediaType cover.
  1065. mta_covers = self.app_cover_art
  1066.  
  1067. if mta_covers or self.is_app_only():
  1068. try:
  1069. return '{base}{path}'.format(base=settings.STATIC_ROOT, path=mta_covers[0].image)
  1070.  
  1071. except IndexError:
  1072. return '{base}/{w}/default.png'.format(base=settings.COVERS_ROOT, w=width)
  1073.  
  1074. else:
  1075. bc = self.book_cover_object
  1076.  
  1077. result = bc.has_cover_by_size(width=width, check_disk=check_disk)
  1078.  
  1079. return result
  1080.  
  1081. def cover_image_url(self, width, mode='s3_origin'):
  1082. if self.is_app_only():
  1083. mta_covers = self.app_cover_art
  1084.  
  1085. try:
  1086. path = "{base}{img}".format(base=settings.STATIC_URL, img=mta_covers[0].image)
  1087.  
  1088. except IndexError:
  1089. path = '{base}{size}/default.png'.format(base=settings.COVERS_URL, size=width)
  1090.  
  1091. else:
  1092. try:
  1093. path = self.book_cover_object.cover_image_url(width=width, mode=mode)
  1094. except AttributeError:
  1095. path = '{base}{size}/default.png'.format(base=settings.COVERS_URL, size=width)
  1096.  
  1097. return path
  1098.  
  1099. @property
  1100. def discovery_transaction(self):
  1101. """
  1102. Return a related transaction, if it exists.
  1103. """
  1104. from discoveries.models import IndieReview
  1105.  
  1106. transaction_qs = IndieReview.objects.filter(title__id=self.id)
  1107.  
  1108. if transaction_qs.count() > 0:
  1109. return transaction_qs[0]
  1110.  
  1111. return None
  1112.  
  1113. def get_prefered_affiliate_dict(self):
  1114. d = dict()
  1115.  
  1116. d['amazon_link'] = self.amazon_affiliate_link
  1117.  
  1118. if self.primary_isbn or d['amazon_link']:
  1119. d['store'] = ''
  1120. d['store_display'] = ''
  1121. d['type'] = ''
  1122. d['platform'] = ''
  1123.  
  1124. # If we have a cached/denormed copy of the affiliate link, use that
  1125. #d['amazon_link'] = self.amazon_affiliate_link
  1126. d['isbn'] = self.amazon_isbn
  1127. d['ean'] = self.amazon_ean
  1128.  
  1129. if d['amazon_link']:
  1130. d['store'] = 'amazon'
  1131. d['store_display'] = 'Amazon'
  1132.  
  1133. # If we didn't get an amazon_link, ask Oliver for it
  1134. # if d['amazon_link'] and d['ean']:
  1135. # pass
  1136.  
  1137. else:
  1138. clean_isbn = self.primary_isbn.upper().replace('-', '').replace(' ', '')
  1139.  
  1140. # If there's no ISBN, don't try to fetch Oliver data
  1141. if len(clean_isbn) > 5:
  1142. save_title = False # Flag used to store whether we found new denorm data to cache
  1143.  
  1144. o = Oliver()
  1145. meta_data = o.detail_by_isbn(isbn=clean_isbn)
  1146.  
  1147. #link = o.property_by_isbn(isbn=clean_isbn, property='amazon_detail_url', provider='amazon')
  1148. try:
  1149. d['amazon_link'] = meta_data['amazon_detail_url']
  1150. except KeyError:
  1151. pass
  1152. else:
  1153. if d['amazon_link']:
  1154. d['store'] = 'amazon'
  1155. d['store_display'] = 'Amazon'
  1156.  
  1157. # Save a copy
  1158. self.amazon_affiliate_link = d['amazon_link']
  1159. self.amazon_affiliate_link_updated_date = datetime.now()
  1160. save_title = True
  1161.  
  1162. if not d['ean']:
  1163. try:
  1164. d['ean'] = meta_data['amazon_ean']
  1165. except KeyError:
  1166. d['ean'] = None
  1167. else:
  1168. self.amazon_ean = d['ean']
  1169. save_title = True
  1170.  
  1171. if not d['isbn']:
  1172. try:
  1173. d['isbn'] = meta_data['amazon_isbn']
  1174. except KeyError:
  1175. d['isbn'] = None
  1176. else:
  1177. self.amazon_isbn = d['isbn']
  1178. save_title = True
  1179.  
  1180. if save_title:
  1181. try:
  1182. self.save()
  1183. except ValueError:
  1184. # Dang. We've gotten a ValueError in the past with bunk affiliate data.
  1185. pass
  1186.  
  1187. if d['ean'] and not(d['isbn']) and not d['store']:
  1188. d['store'] = 'bn'
  1189. d['store_display'] = 'Barnes & Noble'
  1190.  
  1191. return d
  1192.  
  1193. else:
  1194.  
  1195. matches = {}
  1196.  
  1197. for mt in self.mediatype_set.all():
  1198. d = mt.affiliate_buy_data()
  1199.  
  1200. # Apple trumps all
  1201. if d['store'] == 'apple':
  1202. return d
  1203.  
  1204. if d['store'] is not None:
  1205. matches[d['type'].lower()] = d
  1206.  
  1207. # If no Apple Store, try the paperback, then the hardback
  1208. try:
  1209. return matches['paperback']
  1210. except KeyError:
  1211. try:
  1212. return matches['hardcover']
  1213. except KeyError:
  1214. return None
  1215.  
  1216. def book_price(self, type='Hardcover'):
  1217. last_price = ''
  1218.  
  1219. for mt in self.mediatype_set.all():
  1220. if str(mt.get_type_display()) == str(type) and mt.price:
  1221. return str(mt.price)
  1222. else:
  1223. if mt.price:
  1224. last_price = str(mt.price)
  1225.  
  1226. return last_price
  1227.  
  1228. @property
  1229. def invoiced_item(self):
  1230. """
  1231. Return the first invoiced item associated with this title.
  1232. """
  1233. invoiced_items = self.invoiced_items
  1234.  
  1235. if invoiced_items.count() > 0:
  1236. return invoiced_items[0]
  1237.  
  1238. return None
  1239.  
  1240. @property
  1241. def invoiced_items(self):
  1242. """
  1243. Return a QuerySet of InvoicedItem objects associated with this title.
  1244. """
  1245. title_content_type = ContentType.objects.get_for_model(Title)
  1246.  
  1247. return InvoicedItem.objects.filter(content_object_id=self.id, content_type=title_content_type)
  1248.  
  1249. def has_invoice(self):
  1250. if self.invoiced_item is not None:
  1251. return True
  1252. else:
  1253. return False
  1254.  
  1255. has_invoice.boolean = True
  1256.  
  1257. def get_review_payable_price(self, mode='total', user_to_invoice=None):
  1258. amount = 0.0
  1259.  
  1260. if user_to_invoice is None:
  1261. user_to_invoice = self.reviewer
  1262.  
  1263. try:
  1264. # Look for a UserReviewRate for the assigned reviewer
  1265. from profiles.models import UserReviewRate
  1266.  
  1267. amount = float(UserReviewRate.objects.filter(user=user_to_invoice, review_type=self.review_type)[0].user_rate)
  1268.  
  1269. except:
  1270. amount = float(self.review_type.default_rate)
  1271.  
  1272. if mode == 'total':
  1273. try:
  1274. amount += float(self.addl_rate)
  1275. except:
  1276. pass
  1277.  
  1278. return amount
  1279.  
  1280.  
  1281. def is_confirmed(self):
  1282. if self.date_assignment_acknowledged is not None:
  1283. return True
  1284. else:
  1285. return False
  1286.  
  1287. is_confirmed.boolean = True
  1288. is_confirmed.short_description = 'Confirmed'
  1289.  
  1290. @property
  1291. def user_to_invoice(self):
  1292. return self.reviewer
  1293.  
  1294. def oliver_data(self, verbose=False):
  1295. """
  1296. Returns dictionary of data from Oliver for the book.
  1297. """
  1298. if not self._oliver_data:
  1299. oliver = Oliver()
  1300. oliver_data = oliver.detail_by_isbn(self.primary_isbn.strip())
  1301.  
  1302. if len(oliver_data) == 0:
  1303. if verbose:
  1304. print 'NO OLIVER DATA!', oliver_data
  1305.  
  1306. self._oliver_data = oliver_data
  1307.  
  1308. return self._oliver_data
  1309.  
  1310. def bic_subjects(self, verbose=False):
  1311. """
  1312. Returns a list of BIC subjects for a book.
  1313. """
  1314. if not self._bic_subjects:
  1315. bic_subjects = []
  1316.  
  1317. if self.primary_isbn:
  1318. # Get book data from Oliver.
  1319. oliver_data = self.oliver_data(verbose=verbose)
  1320.  
  1321. bic_subjects_string = oliver_data.get('bowker_bicsubject', '')
  1322. bic_subjects_string = force_latin1(unescapeHTML(bic_subjects_string))
  1323.  
  1324. # Clean subjects and split into a list.
  1325. bic_subjects_string = bic_subjects_string.replace('&', '&')
  1326. bic_subject_names = bic_subjects_string.split(';')
  1327.  
  1328. for bic_subject_name in bic_subject_names:
  1329. bic_subject_name = bic_subject_name.strip()
  1330.  
  1331. if len(bic_subject_name) > 0:
  1332. bic_subject = get_or_create_bic_subject(bic_subject_name)
  1333. bic_subjects.append(bic_subject)
  1334.  
  1335. self._bic_subjects = bic_subjects
  1336.  
  1337. return self._bic_subjects
  1338.  
  1339. def parenthetical_genres(self):
  1340. """
  1341. Returns a list of genres parsed from the last sentence of the review.
  1342. """
  1343. if not self._parenthetical_genres:
  1344. genres = []
  1345. parser = ReviewParser()
  1346. parser.parse(self.review)
  1347.  
  1348. extracted_genre = parser.extract_genre()
  1349. if extracted_genre:
  1350. for genre in extracted_genre.split('/'):
  1351. if len(genre) > 0:
  1352. genre = get_or_create_parenthetical_genre(genre)
  1353. genres.append(genre)
  1354.  
  1355. self._parenthetical_genres = genres
  1356.  
  1357. return self._parenthetical_genres
  1358.  
  1359. def update_navigation_categories(self, overwrite=False, modify=False, verbose=False):
  1360. """
  1361. Updates the Title's navigation categories based on bowker_bicsubject and classification/navigation category relations.
  1362. """
  1363. if overwrite:
  1364. self.navigation_categories = []
  1365. self.navigation_formats = []
  1366.  
  1367. navigation_categories_count = self.navigation_categories.count()
  1368.  
  1369. if navigation_categories_count == 0 or (modify and navigation_categories_count > 0):
  1370. # Add all navigation categories/formats related to this BIC subject, with the same Stage/Category.
  1371. for bic_subject in self.bic_subjects(verbose=verbose):
  1372. #for navigation_category in bic_subject.navigation_categories.filter(stage_category=self.category):
  1373. for navigation_category in bic_subject.navigation_categories.filter(stage_category__stage=self.stage):
  1374. self.navigation_categories.add(navigation_category)
  1375.  
  1376. for navigation_format in bic_subject.navigation_formats.filter(stage_category__stage=self.stage):
  1377. self.navigation_formats.add(navigation_format)
  1378.  
  1379. if self.category:
  1380. for navigation_category in self.category.navigation_categories.all():
  1381. self.navigation_categories.add(navigation_category)
  1382.  
  1383. for navigation_format in self.category.navigation_formats.all():
  1384. self.navigation_formats.add(navigation_format)
  1385.  
  1386. # Add navigation categories by classification.
  1387. for classification_name in ('classification1', 'classification2', 'classification3'):
  1388. try:
  1389. title_classification = getattr(self, classification_name, None)
  1390. if title_classification:
  1391. for navigation_category in title_classification.navigation_categories.filter(stage_category=self.category):
  1392. self.navigation_categories.add(navigation_category)
  1393. for navigation_format in title_classification.navigation_formats.all():
  1394. self.navigation_formats.add(navigation_format)
  1395. except ObjectDoesNotExist:
  1396. # The Classification object does not exist.
  1397. # For some reason, there are lots of classification1_id fields set to 0, where there is no Classification1 with an id of 0.
  1398. pass
  1399.  
  1400. self.save()
  1401.  
  1402. def prize_entry_obj(self):
  1403. obj = None
  1404.  
  1405. try:
  1406. obj = self.kirkusprizeentry_set.all()[0]
  1407. except IndexError:
  1408. pass
  1409.  
  1410. return obj
  1411.  
  1412. def is_prize_finalist(self):
  1413. return self.kirkusprizeentry_set.filter(is_finalist=True).exists()
  1414.  
  1415. def is_prize_winner(self):
  1416. return self.kirkusprizeentry_set.filter(is_winner=True).exists()
  1417.  
  1418. def prize_year_eligible(self):
  1419. """
  1420. If this book is/was eligible to be considered for the Kirkus Prize, return the year's prize it
  1421. was eligible for. Otherwise, return 0.
  1422. """
  1423. # Has to be a star
  1424. # Has to be published
  1425. # And we didn't start until 2013 books
  1426. if self.star and self.state == 'P' and self.pub_date and self.published_date and self.published_date.year > 2012:
  1427. # No apps
  1428. if self.is_app_only():
  1429. return 0
  1430.  
  1431. # Adult
  1432. if self.stage == 1:
  1433. start_date, end_date = TitleManager.kirkus_prize_date_range(year=self.pub_date.year)
  1434.  
  1435. if start_date <= self.pub_date <= end_date:
  1436. return self.pub_date.year
  1437.  
  1438. # YA
  1439. elif self.stage == 2:
  1440. start_date, end_date = TitleManager.kirkus_prize_date_range(year=self.pub_date.year, section='young-readers')
  1441.  
  1442. if start_date <= self.pub_date <= end_date:
  1443. return self.pub_date.year
  1444.  
  1445. # Indie
  1446. elif self.stage == 4:
  1447. start_date, end_date = TitleManager.kirkus_prize_date_range(year=self.published_date.year, section='indie')
  1448.  
  1449. if start_date <= self.published_date <= end_date:
  1450. return self.published_date.year
  1451.  
  1452. else:
  1453. start_date, end_date = TitleManager.kirkus_prize_date_range(year=self.pub_date.year)
  1454.  
  1455. if start_date <= self.pub_date <= end_date:
  1456. return self.pub_date.year
  1457.  
  1458. return 0
  1459.  
  1460. @classmethod
  1461. def index_available_items(cls, limit=0, delay=0, verbose=False, very_verbose=False):
  1462. count = 0
  1463.  
  1464. qs = Title.available_titles.all()
  1465.  
  1466. if limit > 0:
  1467. qs = qs[0:limit]
  1468.  
  1469. for t in qs:
  1470. if verbose:
  1471. try:
  1472. print("[{c}] <{id}> {title}".format(c=count, id=t.id, title=t.title))
  1473. except UnicodeEncodeError:
  1474. print("ERR: <{id}>".format(id=t.id))
  1475.  
  1476. t.put_in_search_index(verbose=very_verbose)
  1477.  
  1478. count += 1
  1479.  
  1480. if delay:
  1481. time.sleep(delay)
  1482.  
  1483. def put_in_search_index(self, verbose=False):
  1484. d = dict()
  1485.  
  1486. if verbose:
  1487. print(u'Indexing Title<{id}>: {title}'.format(id=self.id, title=self.title))
  1488.  
  1489. #
  1490. # Load cluster data from Oliver...
  1491. if self.primary_isbn:
  1492. o = Oliver()
  1493. meta_data = o.detail_by_isbn(isbn=self.primary_isbn)
  1494. else:
  1495. o = None
  1496.  
  1497. #
  1498. d['audience'] = self.get_audience_display().lower()
  1499.  
  1500. d['authors'] = [ta.author.full_name().replace('*', '').replace('"', '').replace("'", '') for ta in self.titleauthor_set.all()]
  1501.  
  1502. try:
  1503. d['category'] = slugify(self.category.category_name)
  1504. except AttributeError:
  1505. d['category'] = ''
  1506.  
  1507. if o is not None:
  1508. try:
  1509. # De-slugify for searching
  1510. d['clusters'] = [c.replace('-', ' ') for c in meta_data['oliver_subject_terms']]
  1511. except KeyError:
  1512. d['clusters'] = []
  1513. else:
  1514. d['clusters'] = []
  1515.  
  1516. d['id'] = str(self.id)
  1517.  
  1518. d['is_indie'] = self.discovery
  1519.  
  1520. d['is_recommended'] = self.featured_review
  1521.  
  1522. d['is_star'] = self.star
  1523.  
  1524. d['is_app'] = self.is_app_only()
  1525.  
  1526. d['prize_eligible_year'] = self.prize_year_eligible()
  1527.  
  1528. d['isbn'] = self.isbns()
  1529.  
  1530. try:
  1531. d['last_modified'] = self.updated_date.strftime('%Y-%m-%d %H:%M:%S')
  1532. except AttributeError:
  1533. pass
  1534.  
  1535. try:
  1536. d['issue_date'] = self.issue.issue_date.strftime('%Y-%m-%d')
  1537. except AttributeError:
  1538. pass
  1539.  
  1540. try:
  1541. d['navigation_categories'] = [slugify(nc.name) for nc in self.navigation_categories.all()]
  1542. except Exception:
  1543. d['navigation_categories'] = ''
  1544.  
  1545. d['page_count'] = self.num_pages
  1546.  
  1547. try:
  1548. d['pub_date'] = self.pub_date.strftime('%Y-%m-%d')
  1549. except AttributeError:
  1550. pass
  1551.  
  1552. try:
  1553. d['publisher'] = self.publisher.get_publisher_inheritance_list(verbose=verbose)
  1554. except AttributeError:
  1555. d['publisher'] = ''
  1556.  
  1557. d['reader_age_min'] = self.lower_age
  1558. d['reader_age_max'] = self.upper_age
  1559.  
  1560. try:
  1561. d['review'] = fmt.strip_tags(smart_str(self.review))
  1562. except UnicodeEncodeError:
  1563. pass
  1564.  
  1565. try:
  1566. d['search_extra'] = fmt.strip_tags(smart_str(self.search_extra))
  1567.  
  1568. if d['search_extra'] == 'None' or d['search_extra'] == None:
  1569. d['search_extra'] = ''
  1570.  
  1571. except Exception:
  1572. d['search_extra'] = ''
  1573.  
  1574. d['series'] = self.series.series_name if self.series else ''
  1575.  
  1576. d['series_position'] = self.volume
  1577.  
  1578. d['slug'] = self.title_slug
  1579.  
  1580. try:
  1581. d['stage'] = slugify(self.stage.stage_name)
  1582. except AttributeError:
  1583. d['stage'] = ''
  1584.  
  1585. d['subtitle'] = self.subtitle
  1586.  
  1587. d['title'] = self.title
  1588.  
  1589. d['type'] = 'title'
  1590.  
  1591. #
  1592. es_search_util.put_d_in_index(index='reviews', mapping='review', d=d, id=d['id'], verbose=verbose)
  1593.  
  1594. def remove_from_search_index(self, verbose=False):
  1595. es_search_util.remove_from_index(index='reviews', mapping='review', id=self.id, verbose=verbose)
  1596.  
  1597. def suggest_review_deadline(self):
  1598. policy = get_business_dates_policy()
  1599. today = date.today()
  1600.  
  1601. deadline = None
  1602.  
  1603. if self.discovery:
  1604. if self.manuscript_submission_format and self.manuscript_submission_format.name == 'digital':
  1605. deadline = today + timedelta(weeks=2)
  1606.  
  1607. else:
  1608. deadline = today + timedelta(weeks=2, days=2)
  1609.  
  1610. deadline = policy.closest_biz_day(deadline, forward=True)
  1611.  
  1612. return deadline
  1613.  
  1614. def get_book_cover_key(self):
  1615. key = None
  1616.  
  1617. if self.primary_isbn:
  1618. key = 'isbn:{isbn}'.format(isbn=self.primary_isbn)
  1619. else:
  1620. key = 'kr:{id}'.format(id=self.id)
  1621.  
  1622. return key
  1623.  
  1624. @property
  1625. def book_cover_object(self):
  1626. if self._book_cover_object is not None:
  1627. return self._book_cover_object
  1628.  
  1629. else:
  1630. if self.is_app_only():
  1631. self._book_cover_object = AppCover.get_or_create(key=self.get_book_cover_key())
  1632. # get_or_create returns a BookCover, so we need to tell the
  1633. # interpretor that we want an AppCover instead:
  1634. self._book_cover_object.__class__ = AppCover
  1635.  
  1636. else:
  1637. self._book_cover_object = BookCover.get_or_create(key=self.get_book_cover_key())
  1638.  
  1639. if self._book_cover_object is not None:
  1640. self._book_cover_object.parent = self
  1641.  
  1642. return self._book_cover_object
  1643.  
  1644. return None
  1645.  
  1646. def best_of_2013_schedule_slug(self):
  1647. return self.print_supplement_schedule_slug()
  1648.  
  1649. def print_supplement_schedule_slug(self):
  1650. if self.print_supplement_section:
  1651. if self.print_supplement_section.slug == 'apps':
  1652. return 'book-apps'
  1653.  
  1654. elif self.print_supplement_section.slug in ('children', 'childrens'):
  1655. return 'children'
  1656.  
  1657. elif self.print_supplement_section.slug == 'fiction':
  1658. return 'fiction'
  1659.  
  1660. elif self.print_supplement_section.slug == 'indie':
  1661. return 'indie'
  1662.  
  1663. elif self.print_supplement_section.slug in ('middle-grade', 'middle-grader', 'middle-graders'):
  1664. if self.pub_date.year == 2015:
  1665. return 'middle-grade'
  1666. else:
  1667. return 'children'
  1668.  
  1669. elif self.print_supplement_section.slug == 'nonfiction':
  1670. return 'nonfiction'
  1671.  
  1672. elif self.print_supplement_section.slug == 'picture-books':
  1673. if self.pub_date.year == 2015:
  1674. return 'picture-books'
  1675. else:
  1676. return 'children'
  1677.  
  1678. elif self.print_supplement_section.slug == 'teen':
  1679. return 'teen'
  1680.  
  1681. else:
  1682. return ''
  1683.  
  1684. elif self.discovery:
  1685. return 'indie'
  1686.  
  1687. elif self.is_app_only():
  1688. return 'book-apps'
  1689.  
  1690. elif self.category and self.category.category_name == 'Fiction':
  1691. return 'fiction'
  1692.  
  1693. elif self.category and self.category.category_name == 'Nonfiction':
  1694. return 'nonfiction'
  1695.  
  1696. elif self.stage and self.stage.stage_name == "Children's":
  1697. return 'children'
  1698.  
  1699. else:
  1700. return ''
  1701.  
  1702. def is_in_supplement(self, slug):
  1703. """
  1704. Returns the supplement, if found. False otherwise.
  1705. """
  1706. try:
  1707. return self.print_supplement.get(slug=slug)
  1708. except:
  1709. return False
  1710.  
  1711. @cached_property
  1712. def is_best_of_2015(self):
  1713. """Checking on the current year is more complicated, since we want to ensure that we
  1714. don't leak the winners in sections that aren't live yet.
  1715. """
  1716. from issues.models import IssuePrintEditionSupplementSchedule
  1717.  
  1718. sup = self.is_in_supplement(slug='best-of-2015')
  1719.  
  1720. if sup and sup.is_published():
  1721. # Find out what section it's in
  1722. schedule_slug = self.print_supplement_schedule_slug()
  1723.  
  1724. if schedule_slug: # Which could the an empty string if this Title doesn't have one
  1725. try:
  1726. sched = IssuePrintEditionSupplementSchedule.objects.get(supplement=sup, nav_slug=schedule_slug)
  1727.  
  1728. return sched.is_published()
  1729.  
  1730. except:
  1731. return False
  1732.  
  1733. else:
  1734. # Assume it is if there's no section, but the supplement has been published
  1735. return True # sup.is_published()
  1736.  
  1737. else:
  1738. return False
  1739.  
  1740. @property
  1741. def is_best_of_2014(self):
  1742. return self.is_in_supplement(slug='best-of-2014')
  1743.  
  1744. @property
  1745. def is_best_of_2013(self):
  1746. return self.is_in_supplement(slug='best-of-2013')
  1747.  
  1748. @property
  1749. def is_best_of_2012(self):
  1750. return self.is_in_supplement(slug='2012-best-of')
  1751.  
  1752. @property
  1753. def is_bea_2015(self):
  1754. return self.is_in_supplement(slug='2015-bea-supplement')
  1755.  
  1756. @property
  1757. def is_bea_2014(self):
  1758. return self.is_in_supplement(slug='2014-bea-supplement')
  1759.  
  1760. @property
  1761. def is_fall_preview_2014(self):
  1762. return self.is_in_supplement(slug='2014-fall-preview')
  1763.  
  1764. def get_best_of_supplement(self):
  1765. try:
  1766. return self.print_supplement.get(slug__startswith='best-of-')
  1767. except:
  1768. try:
  1769. return self.print_supplement.get(slug__endswith='-best-of')
  1770. except:
  1771. return None
  1772.  
  1773. def is_best_of(self):
  1774. """Return a boolean. True if this title was ever selected as a Best-of.
  1775. """
  1776. if self.best_of_year is not None:
  1777. return True
  1778.  
  1779. return False
  1780.  
  1781. @cached_property
  1782. def best_of_year(self):
  1783. """Return the year when this title was marked as a Best-of selection. None otherwise.
  1784. """
  1785. if self.is_best_of_2015:
  1786. return 2015
  1787.  
  1788. elif self.is_best_of_2014:
  1789. return 2014
  1790.  
  1791. elif self.is_best_of_2013:
  1792. return 2013
  1793.  
  1794. elif self.is_best_of_2012:
  1795. return 2012
  1796.  
  1797. return None
  1798.  
  1799. def book_samples(self):
  1800. return self.booksample_set.all()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement