Don't like ads? PRO users don't see any ads ;-)
Guest

Untitled

By: a guest on Jul 1st, 2012  |  syntax: None  |  size: 92.73 KB  |  hits: 15  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. # Copyright 2009 - Participatory Culture Foundation
  2. #
  3. # This file is part of Miro Community.
  4. #
  5. # Miro Community is free software: you can redistribute it and/or modify it
  6. # under the terms of the GNU Affero General Public License as published by
  7. # the Free Software Foundation, either version 3 of the License, or (at your
  8. # option) any later version.
  9. #
  10. # Miro Community is distributed in the hope that it will be useful,
  11. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13. # GNU Affero General Public License for more details.
  14. #
  15. # You should have received a copy of the GNU Affero General Public License
  16. # along with Miro Community.  If not, see <http://www.gnu.org/licenses/>.
  17.  
  18. import datetime
  19. import email.utils
  20. import httplib
  21. import re
  22. import urllib
  23. import urllib2
  24. import mimetypes
  25. import base64
  26. import os
  27. import logging
  28. import sys
  29. import traceback
  30.  
  31. try:
  32.     from PIL import Image
  33. except ImportError:
  34.     import Image
  35. import time
  36. from BeautifulSoup import BeautifulSoup
  37.  
  38. from django.db import models
  39. from django.conf import settings
  40. from django.contrib.auth.models import User
  41. from django.contrib.comments.moderation import CommentModerator, moderator
  42. from django.contrib.sites.models import Site
  43. from django.contrib.contenttypes.models import ContentType
  44. from django.core.files.base import ContentFile
  45. from django.core.files.storage import default_storage
  46. from django.core.mail import EmailMessage
  47. from django.core.signals import request_finished
  48. import django.dispatch
  49. from django.core.validators import ipv4_re
  50. from django.template import Context, loader
  51. from django.template.defaultfilters import slugify
  52. from django.template.loader import render_to_string
  53. import django.utils.html
  54. from django.utils.safestring import mark_safe
  55. from django.utils.translation import ugettext_lazy as _
  56.  
  57. import bitly
  58. import vidscraper
  59.  
  60. from notification import models as notification
  61. import tagging
  62.  
  63. from localtv.exceptions import InvalidVideo, CannotOpenImageUrl
  64. from localtv.templatetags.filters import sanitize
  65. from localtv import utils
  66. from localtv import settings as lsettings
  67. from localtv.signals import post_video_from_vidscraper, submit_finished
  68. import localtv.tiers
  69.  
  70. def delete_if_exists(path):
  71.     if default_storage.exists(path):
  72.         default_storage.delete(path)
  73.  
  74. EMPTY = object()
  75.  
  76. UNAPPROVED_STATUS_TEXT = _(u'Unapproved')
  77. ACTIVE_STATUS_TEXT = _(u'Active')
  78. REJECTED_STATUS_TEXT = _(u'Rejected')
  79. PENDING_STATUS_TEXT = _(u'Waiting on import to finish')
  80. DISABLED_STATUS_TEXT = _(u'Disabled')
  81.  
  82. THUMB_SIZES = [ # for backwards, compatibility; it's now a class variable
  83.     (534, 430), # behind a video
  84.     (375, 295), # featured on frontpage
  85.     (140, 110),
  86.     (364, 271), # main thumb
  87.     (222, 169), # medium thumb
  88.     (88, 68),   # small thumb
  89.     ]
  90.  
  91. FORCE_HEIGHT_CROP = 1 # arguments for thumbnail resizing
  92. FORCE_HEIGHT_PADDING = 2
  93.  
  94. VIDEO_SERVICE_REGEXES = (
  95.     ('YouTube', r'http://gdata\.youtube\.com/feeds/'),
  96.     ('YouTube', r'http://(www\.)?youtube\.com/'),
  97.     ('blip.tv', r'http://(.+\.)?blip\.tv/'),
  98.     ('Vimeo', r'http://(www\.)?vimeo\.com/'),
  99.     ('Dailymotion', r'http://(www\.)?dailymotion\.com/rss'))
  100.  
  101.  
  102. class BitLyWrappingURLField(models.URLField):
  103.     def get_db_prep_value(self, value, *args, **kwargs):
  104.         if not getattr(settings, 'BITLY_LOGIN'):
  105.             return value
  106.  
  107.         # Workaround for some cases
  108.         if value is None:
  109.             value = ''
  110.  
  111.         if len(value) <= self.max_length: # short enough to save
  112.             return value
  113.         api = bitly.Api(login=settings.BITLY_LOGIN,
  114.                         apikey=settings.BITLY_API_KEY)
  115.         try:
  116.             return unicode(api.shorten(value))
  117.         except bitly.BitlyError:
  118.             return unicode(value)[:self.max_length]
  119.  
  120.  
  121. try:
  122.     from south.modelsinspector import add_introspection_rules
  123. except ImportError:
  124.     pass
  125. else:
  126.     add_introspection_rules([], ["^localtv\.models\.BitLyWrappingURLField"])
  127.  
  128.  
  129. class Thumbnailable(models.Model):
  130.     """
  131.     A type of Model that has thumbnails generated for it.
  132.     """
  133.     has_thumbnail = models.BooleanField(default=False)
  134.     thumbnail_extension = models.CharField(max_length=8, blank=True)
  135.  
  136.     class Meta:
  137.         abstract = True
  138.  
  139.     def save_thumbnail_from_file(self, content_thumb, resize=True):
  140.         """
  141.         Takes an image file-like object and stores it as the thumbnail for this
  142.         video item.
  143.         """
  144.         try:
  145.             pil_image = Image.open(content_thumb)
  146.         except IOError:
  147.             raise CannotOpenImageUrl('An image could not be loaded')
  148.  
  149.         # save an unresized version, overwriting if necessary
  150.         delete_if_exists(
  151.             self.get_original_thumb_storage_path())
  152.  
  153.         self.thumbnail_extension = pil_image.format.lower()
  154.         default_storage.save(
  155.             self.get_original_thumb_storage_path(),
  156.             content_thumb)
  157.  
  158.         if hasattr(content_thumb, 'temporary_file_path'):
  159.             # might have gotten moved by Django's storage system, so it might
  160.             # be invalid now.  to make sure we've got a valid file, we reopen
  161.             # under the new path
  162.             content_thumb.close()
  163.             content_thumb = default_storage.open(
  164.                 self.get_original_thumb_storage_path())
  165.             pil_image = Image.open(content_thumb)
  166.  
  167.         if resize:
  168.             # save any resized versions
  169.             self.resize_thumbnail(pil_image)
  170.         self.has_thumbnail = True
  171.         self.save()
  172.  
  173.     def resize_thumbnail(self, thumb, resized_images=None):
  174.         """
  175.         Creates resized versions of the video's thumbnail image
  176.         """
  177.         if not thumb:
  178.             thumb = Image.open(
  179.                 default_storage.open(self.get_original_thumb_storage_path()))
  180.         if resized_images is None:
  181.             resized_images = utils.resize_image_returning_list_of_strings(
  182.                 thumb, self.THUMB_SIZES)
  183.         for ( (width, height), data) in resized_images:
  184.             # write file, deleting old thumb if it exists
  185.             cf_image = ContentFile(data)
  186.             delete_if_exists(
  187.                 self.get_resized_thumb_storage_path(width, height))
  188.             default_storage.save(
  189.                 self.get_resized_thumb_storage_path(width, height),
  190.                 cf_image)
  191.  
  192.     def get_original_thumb_storage_path(self):
  193.         """
  194.         Return the path for the original thumbnail, relative to the default
  195.         file storage system.
  196.         """
  197.         return 'localtv/%s_thumbs/%s/orig.%s' % (
  198.             self._meta.object_name.lower(),
  199.             self.id, self.thumbnail_extension)
  200.  
  201.     def get_resized_thumb_storage_path(self, width, height):
  202.         """
  203.         Return the path for the a thumbnail of a resized width and height,
  204.         relative to the default file storage system.
  205.         """
  206.         return 'localtv/%s_thumbs/%s/%sx%s.png' % (
  207.             self._meta.object_name.lower(),
  208.             self.id, width, height)
  209.  
  210.     def delete_thumbnails(self):
  211.         self.has_thumbnail = False
  212.         delete_if_exists(self.get_original_thumb_storage_path())
  213.         for size in self.THUMB_SIZES:
  214.             delete_if_exists(
  215.                 self.get_resized_thumb_storage_path(*size[:2]))
  216.         self.thumbnail_extension = ''
  217.         self.save()
  218.  
  219.     def delete(self, *args, **kwargs):
  220.         self.delete_thumbnails()
  221.         super(Thumbnailable, self).delete(*args, **kwargs)
  222.  
  223.  
  224. SITE_LOCATION_CACHE = {}
  225.  
  226.  
  227. class SiteLocationManager(models.Manager):
  228.     def get_current(self):
  229.         sid = settings.SITE_ID
  230.         try:
  231.             # Dig it out of the cache.
  232.             current_site_location = SITE_LOCATION_CACHE[sid]
  233.         except KeyError:
  234.             # Not in the cache? Time to put it in the cache.
  235.             try:
  236.                 # If it is in the DB, get it.
  237.                 current_site_location = self.select_related().get(site__pk=sid)
  238.             except SiteLocation.DoesNotExist:
  239.                 # Otherwise, create it.
  240.                 current_site_location = localtv.models.SiteLocation.objects.create(
  241.                     site=Site.objects.get_current())
  242.  
  243.             SITE_LOCATION_CACHE[sid] = current_site_location
  244.         return current_site_location
  245.  
  246.     def get(self, **kwargs):
  247.         if 'site' in kwargs:
  248.             site = kwargs['site']
  249.             if not isinstance(site, (int, long, basestring)):
  250.                 site = site.id
  251.             site = int(site)
  252.             try:
  253.                 return SITE_LOCATION_CACHE[site]
  254.             except KeyError:
  255.                 pass
  256.         site_location = models.Manager.get(self, **kwargs)
  257.         SITE_LOCATION_CACHE[site_location.site_id] = site_location
  258.         return site_location
  259.  
  260.     def clear_cache(self):
  261.         global SITE_LOCATION_CACHE
  262.         SITE_LOCATION_CACHE = {}
  263.  
  264.  
  265. class SingletonManager(models.Manager):
  266.     def get_current(self):
  267.         current_site_location = SiteLocation._default_manager.db_manager(
  268.             self.db).get_current()
  269.         singleton, created = self.get_or_create(
  270.             sitelocation = current_site_location)
  271.         if created:
  272.             logging.debug("Created %s." % self.model)
  273.         return singleton
  274.  
  275.  
  276. class TierInfo(models.Model):
  277.     payment_due_date = models.DateTimeField(null=True, blank=True)
  278.     free_trial_available = models.BooleanField(default=True)
  279.     free_trial_started_on = models.DateTimeField(null=True, blank=True)
  280.     in_free_trial = models.BooleanField(default=False)
  281.     payment_secret = models.CharField(max_length=255, default='',blank=True) # This is part of payment URLs.
  282.     current_paypal_profile_id = models.CharField(max_length=255, default='',blank=True) # NOTE: When using this, fill it if it seems blank.
  283.     video_allotment_warning_sent = models.BooleanField(default=False)
  284.     free_trial_warning_sent = models.BooleanField(default=False)
  285.     already_sent_welcome_email = models.BooleanField(default=False)
  286.     inactive_site_warning_sent = models.BooleanField(default=False)
  287.     user_has_successfully_performed_a_paypal_transaction = models.BooleanField(default=False)
  288.     already_sent_tiers_compliance_email = models.BooleanField(default=False)
  289.     fully_confirmed_tier_name = models.CharField(max_length=255, default='', blank=True)
  290.     should_send_welcome_email_on_paypal_event = models.BooleanField(default=False)
  291.     waiting_on_payment_until = models.DateTimeField(null=True, blank=True)
  292.     sitelocation = models.OneToOneField('SiteLocation')
  293.     objects = SingletonManager()
  294.  
  295.     def get_payment_secret(self):
  296.         '''The secret had better be non-empty. So we make it non-empty right here.'''
  297.         if self.payment_secret:
  298.             return self.payment_secret
  299.         # Guess we had better fill it.
  300.         self.payment_secret = base64.b64encode(os.urandom(16))
  301.         self.save()
  302.         return self.payment_secret
  303.  
  304.     def site_is_subsidized(self):
  305.         return (self.current_paypal_profile_id == 'subsidized')
  306.  
  307.     def set_to_subsidized(self):
  308.         if self.current_paypal_profile_id:
  309.             raise AssertionError, (
  310.                 "Bailing out: " +
  311.                 "the site already has a payment profile configured: %s" %
  312.                                    self.current_paypal_profile_id)
  313.         self.current_paypal_profile_id = 'subsidized'
  314.  
  315.     def time_until_free_trial_expires(self, now = None):
  316.         if not self.in_free_trial:
  317.             return None
  318.         if not self.payment_due_date:
  319.             return None
  320.  
  321.         if now is None:
  322.             now = datetime.datetime.utcnow()
  323.         return (self.payment_due_date - now)
  324.  
  325.     def use_zendesk(self):
  326.         '''If the site is configured to, we can send notifications of
  327.         tiers-related changes to ZenDesk, the customer support ticketing
  328.         system used by PCF.
  329.  
  330.         A non-PCF deployment of localtv would not want to set the
  331.         LOCALTV_USE_ZENDESK setting. Then this method will return False,
  332.         and the parts of the tiers system that check it will avoid
  333.         making calls out to ZenDesk.'''
  334.         return lsettings.USE_ZENDESK
  335.  
  336.  
  337. class SiteLocation(Thumbnailable):
  338.     """
  339.     An extension to the django.contrib.sites site model, providing
  340.     localtv-specific data.
  341.  
  342.     Fields:
  343.      - site: A link to the django.contrib.sites.models.Site object
  344.      - logo: custom logo image for this site
  345.      - background: custom background image for this site (unused?)
  346.      - admins: a collection of Users who have access to administrate this
  347.        sitelocation
  348.      - status: one of SiteLocation.STATUS_CHOICES
  349.      - sidebar_html: custom html to appear on the right sidebar of many
  350.        user-facing pages.  Can be whatever's most appropriate for the owners of
  351.        said site.
  352.      - footer_html: HTML that appears at the bottom of most user-facing pages.
  353.        Can be whatever's most appropriate for the owners of said site.
  354.      - about_html: HTML to display on the s about page
  355.      - tagline: displays below the s title on most user-facing pages
  356.      - css: The intention here is to allow  to paste in their own CSS
  357.        here from the admin.  Not used presently, though eventually it should
  358.        be.
  359.      - display_submit_button: whether or not we should allow users to see that
  360.        they can submit videos or not (doesn't affect whether or not they
  361.        actually can though)
  362.      - submission_requires_login: whether or not users need to log in to submit
  363.        videos.
  364.      - tier_name: A short string representing the class of site. This relates to paid extras.
  365.     """
  366.     DISABLED = 0
  367.     ACTIVE = 1
  368.  
  369.     STATUS_CHOICES = (
  370.         (DISABLED, DISABLED_STATUS_TEXT),
  371.         (ACTIVE, ACTIVE_STATUS_TEXT),
  372.     )
  373.  
  374.     site = models.ForeignKey(Site, unique=True)
  375.     logo = models.ImageField(upload_to='localtv/site_logos', blank=True)
  376.     background = models.ImageField(upload_to='localtv/site_backgrounds',
  377.                                    blank=True)
  378.     admins = models.ManyToManyField('auth.User', blank=True,
  379.                                     related_name='admin_for')
  380.     status = models.IntegerField(
  381.         choices=STATUS_CHOICES, default=ACTIVE)
  382.     sidebar_html = models.TextField(blank=True)
  383.     footer_html = models.TextField(blank=True)
  384.     about_html = models.TextField(blank=True)
  385.     tagline = models.CharField(max_length=4096, blank=True)
  386.     css = models.TextField(blank=True)
  387.     display_submit_button = models.BooleanField(default=True)
  388.     submission_requires_login = models.BooleanField(default=False)
  389.     playlists_enabled = models.IntegerField(default=1)
  390.     tier_name = models.CharField(max_length=255, default='basic', blank=False, choices=localtv.tiers.CHOICES)
  391.     hide_get_started = models.BooleanField(default=False)
  392.  
  393.     # ordering options
  394.     use_original_date = models.BooleanField(
  395.         default=True,
  396.         help_text="If set, use the original date the video was posted.  "
  397.         "Otherwise, use the date the video was added to this site.")
  398.  
  399.     # comments options
  400.     screen_all_comments = models.BooleanField(
  401.         verbose_name='Hold comments for moderation',
  402.         default=True,
  403.         help_text="Hold all comments for moderation by default?")
  404.     comments_required_login = models.BooleanField(
  405.         default=False,
  406.         verbose_name="Require Login",
  407.         help_text="If True, comments require the user to be logged in.")
  408.  
  409.     objects = SiteLocationManager()
  410.  
  411.     THUMB_SIZES = [
  412.         (88, 68, False),
  413.         (140, 110, False),
  414.         (222, 169, False),
  415.         (130, 110, FORCE_HEIGHT_PADDING) # Facebook
  416.         ]
  417.  
  418.     def __unicode__(self):
  419.         return '%s (%s)' % (self.site.name, self.site.domain)
  420.  
  421.     def add_queued_mail(self, data):
  422.         if not hasattr(self, '_queued_mail'):
  423.             self._queued_mail = []
  424.         self._queued_mail.append(data)
  425.  
  426.     def get_queued_mail_destructively(self):
  427.         ret = getattr(self, '_queued_mail', [])
  428.         self._queued_mail = []
  429.         return ret
  430.  
  431.     @staticmethod
  432.     def enforce_tiers(override_setting=None, using='default'):
  433.         '''If the admin has set LOCALTV_DISABLE_TIERS_ENFORCEMENT to a True value,
  434.         then this function returns False. Otherwise, it returns True.'''
  435.         if override_setting is None:
  436.             disabled = lsettings.DISABLE_TIERS_ENFORCEMENT
  437.         else:
  438.             disabled = override_setting
  439.  
  440.         if disabled:
  441.             # Well, hmm. If the site admin participated in a PayPal transaction, then we
  442.             # actually will enforce the tiers.
  443.             #
  444.             # Go figure.
  445.             tierdata = TierInfo.objects.db_manager(using).get_current()
  446.             if tierdata.user_has_successfully_performed_a_paypal_transaction:
  447.                 return True # enforce it.
  448.  
  449.         # Generally, we just negate the "disabled" boolean.
  450.         return not disabled
  451.  
  452.     def user_is_admin(self, user):
  453.         """
  454.         Return True if the given User is an admin for this SiteLocation.
  455.         """
  456.         if not user.is_authenticated() or not user.is_active:
  457.             return False
  458.  
  459.         if user.is_superuser:
  460.             return True
  461.  
  462.         return bool(self.admins.filter(pk=user.pk).count())
  463.  
  464.     def save(self, *args, **kwargs):
  465.         SITE_LOCATION_CACHE[self.site_id] = self
  466.         return models.Model.save(self, *args, **kwargs)
  467.  
  468.     def get_tier(self):
  469.         return localtv.tiers.Tier(self.tier_name, self)
  470.  
  471.     def get_fully_confirmed_tier(self):
  472.         # If we are in a transitional state, then we would have stored
  473.         # the last fully confirmed tier name in an unusual column.
  474.         tierdata = TierInfo.objects.get_current()
  475.         if tierdata.fully_confirmed_tier_name:
  476.             return localtv.tiers.Tier(tierdata.fully_confirmed_tier_name)
  477.         return None
  478.  
  479.     def get_css_for_display_if_permitted(self):
  480.         '''This function checks the site tier, and if permitted, returns the
  481.         custom CSS the admin has set.
  482.  
  483.         If that is not permitted, it returns the empty unicode string.'''
  484.         if (not self.enforce_tiers() or
  485.             self.get_tier().permit_custom_css()):
  486.             # Sweet.
  487.             return self.css
  488.         else:
  489.             # Silenced.
  490.             return u''
  491.  
  492.     def should_show_dashboard(self):
  493.         '''On /admin/, most sites will see a dashboard that gives them
  494.         information at a glance about the site, including its tier status.
  495.  
  496.         Some sites want to disable that, which they can do by setting the
  497.         LOCALTV_SHOW_ADMIN_DASHBOARD variable to False.
  498.  
  499.         In that case (in the default theme) the left-hand navigation
  500.         will omit the link to the Dashboard, and also the dashboard itself
  501.         will be an empty page with a META REFRESH that points to
  502.         /admin/approve_reject/.'''
  503.         return lsettings.SHOW_ADMIN_DASHBOARD
  504.  
  505.     def should_show_account_level(self):
  506.         '''On /admin/upgrade/, most sites will see an info page that
  507.         shows how to change their account level (AKA site tier).
  508.  
  509.         Some sites want to disable that, which they can do by setting the
  510.         LOCALTV_SHOW_ADMIN_ACCOUNT_LEVEL variable to False.
  511.  
  512.         This simply removes the link from the sidebar; if you visit the
  513.         /admin/upgrade/ page, it renders as usual.'''
  514.         return lsettings.SHOW_ADMIN_ACCOUNT_LEVEL
  515.  
  516.  
  517. class NewsletterSettings(models.Model):
  518.     DISABLED = 0
  519.     FEATURED = 1
  520.     POPULAR = 2
  521.     CUSTOM = 3
  522.     LATEST = 4
  523.    
  524.     STATUS_CHOICES = (
  525.         (DISABLED, DISABLED_STATUS_TEXT),
  526.         (FEATURED, _("5 most recently featured")),
  527.         (POPULAR, _("5 most popular")),
  528.         (LATEST, _("5 latest videos")),
  529.         (CUSTOM, _("Custom selection")),
  530.     )
  531.     sitelocation = models.OneToOneField(SiteLocation)
  532.     status = models.IntegerField(
  533.         choices=STATUS_CHOICES, default=DISABLED,
  534.         help_text='What videos should get sent out in the newsletter?')
  535.  
  536.     # for custom newsletter
  537.     video1 = models.ForeignKey('Video', related_name='newsletter1', null=True,
  538.                                help_text='A URL of a video on your site.')
  539.     video2 = models.ForeignKey('Video', related_name='newsletter2', null=True,
  540.                                help_text='A URL of a video on your site.')
  541.     video3 = models.ForeignKey('Video', related_name='newsletter3', null=True,
  542.                                help_text='A URL of a video on your site.')
  543.     video4 = models.ForeignKey('Video', related_name='newsletter4', null=True,
  544.                                help_text='A URL of a video on your site.')
  545.     video5 = models.ForeignKey('Video', related_name='newsletter5', null=True,
  546.                                help_text='A URL of a video on your site.')
  547.    
  548.     intro = models.CharField(max_length=200, blank=True,
  549.                              help_text=('Include a short introduction to your '
  550.                                         'newsletter. If you will be sending '
  551.                                         'the newsletter automatically, make '
  552.                                         'sure to update this or write '
  553.                                         'something that will be evergreen! '
  554.                                         '(limit 200 characters)'))
  555.     show_icon = models.BooleanField(default=True,
  556.                                     help_text=('Do you want to include your '
  557.                                                'site logo in the newsletter '
  558.                                                'header?'))
  559.  
  560.     twitter_url = models.URLField(verify_exists=False, blank=True,
  561.                                   help_text='e.g. https://twitter.com/#!/mirocommunity')
  562.     facebook_url = models.URLField(verify_exists=False, blank=True,
  563.                                    help_text='e.g. http://www.facebook.com/universalsubtitles')
  564.  
  565.     repeat = models.IntegerField(default=0) # hours between sending
  566.     last_sent = models.DateTimeField(null=True)
  567.  
  568.     objects = SingletonManager()
  569.  
  570.     def videos(self):
  571.         if self.status == NewsletterSettings.DISABLED:
  572.             raise ValueError('no videos for disabled newsletter')
  573.         elif self.status == NewsletterSettings.FEATURED:
  574.             videos = Video.objects.get_featured_videos(self.sitelocation)
  575.         elif self.status == NewsletterSettings.POPULAR:
  576.             # popular over the last week
  577.             videos = Video.objects.get_popular_videos(self.sitelocation)
  578.         elif self.status == NewsletterSettings.LATEST:
  579.             videos = Video.objects.get_latest_videos(self.sitelocation)
  580.         elif self.status == NewsletterSettings.CUSTOM:
  581.             videos = [video for video in (
  582.                     self.video1,
  583.                     self.video2,
  584.                     self.video3,
  585.                     self.video4,
  586.                     self.video5) if video]
  587.         return videos[:5]
  588.  
  589.     def next_send_time(self):
  590.         if not self.repeat:
  591.             return None
  592.         if not self.last_sent:
  593.             dt = datetime.datetime.now()
  594.         else:
  595.             dt = self.last_sent
  596.         return dt + datetime.timedelta(hours=self.repeat)
  597.  
  598.     def send(self):
  599.         from localtv.admin.user_views import _filter_just_humans
  600.         body = self.as_html()
  601.         subject = '[%s] Newsletter for %s' % (self.sitelocation.site.name,
  602.                                               datetime.datetime.now().strftime('%m/%d/%y'))
  603.         notice_type = notification.NoticeType.objects.get(label='newsletter')
  604.         for u in User.objects.exclude(email=None).exclude(email='').filter(
  605.             _filter_just_humans()):
  606.             if notification.get_notification_setting(u, notice_type, "1"):
  607.                 message = EmailMessage(subject, body,
  608.                                        settings.DEFAULT_FROM_EMAIL,
  609.                                        [u.email])
  610.                 message.content_subtype = 'html'
  611.                 message.send(fail_silently=True)
  612.  
  613.     def as_html(self, extra_context=None):
  614.         context = {'newsletter': self,
  615.                    'sitelocation': self.sitelocation,
  616.                    'site': self.sitelocation.site}
  617.         if extra_context:
  618.             context.update(extra_context)
  619.         return render_to_string('localtv/admin/newsletter.html',
  620.                                 context)
  621.  
  622.  
  623. class WidgetSettings(Thumbnailable):
  624.     """
  625.     A Model which represents the options for controlling the widget creator.
  626.     """
  627.     site = models.OneToOneField(Site)
  628.  
  629.     title = models.CharField(max_length=250, blank=True)
  630.     title_editable = models.BooleanField(default=True)
  631.  
  632.     icon = models.ImageField(upload_to='localtv/widget_icon', blank=True)
  633.     icon_editable = models.BooleanField(default=False)
  634.  
  635.     css = models.FileField(upload_to='localtv/widget_css', blank=True)
  636.     css_editable = models.BooleanField(default=False)
  637.  
  638.     bg_color = models.CharField(max_length=20, blank=True)
  639.     bg_color_editable = models.BooleanField(default=False)
  640.  
  641.     text_color = models.CharField(max_length=20, blank=True)
  642.     text_color_editable = models.BooleanField(default=False)
  643.  
  644.     border_color = models.CharField(max_length=20, blank=True)
  645.     border_color_editable = models.BooleanField(default=False)
  646.  
  647.     THUMB_SIZES = [
  648.         (88, 68, False),
  649.         (140, 110, False),
  650.         (222, 169, False),
  651.         ]
  652.  
  653.     def get_title_or_reasonable_default(self):
  654.         # Is the title worth using? If so, use that.
  655.         use_title = True
  656.         if self.title.endswith('example.com'):
  657.             use_title = False
  658.         if not self.title:
  659.             use_title = False
  660.  
  661.         # Okay, so either we return the title, or a sensible default
  662.         if use_title:
  663.             return django.utils.html.escape(self.title)
  664.         return self.generate_reasonable_default_title()
  665.  
  666.     def generate_reasonable_default_title(self):
  667.         prefix = 'Watch Videos on %s'
  668.  
  669.         # Now, work on calculating what goes at the end.
  670.         site = Site.objects.get_current()
  671.  
  672.         # The default suffix is a self-link. If the site name and
  673.         # site domain are plausible, do that.
  674.         if ((site.name and site.name.lower() != 'example.com') and
  675.             (site.domain and site.domain.lower() != 'example.com')):
  676.             suffix = '<a href="http://%s/">%s</a>' % (
  677.                 site.domain, django.utils.html.escape(site.name))
  678.  
  679.         # First, we try the site name, if that's a nice string.
  680.         elif site.name and site.name.lower() != 'example.com':
  681.             suffix = site.name
  682.  
  683.         # Else, we try the site domain, if that's not example.com
  684.         elif site.domain.lower() != 'example.com':
  685.             suffix = site.domain
  686.  
  687.         else:
  688.             suffix = 'our video site'
  689.  
  690.         return prefix % suffix
  691.  
  692.  
  693. class Source(Thumbnailable):
  694.     """
  695.     An abstract base class to represent things which are sources of multiple
  696.     videos.  Current subclasses are Feed and SavedSearch.
  697.     """
  698.     id = models.AutoField(primary_key=True)
  699.     site = models.ForeignKey(Site)
  700.     auto_approve = models.BooleanField(default=False)
  701.     auto_update = models.BooleanField(default=True,
  702.                                       help_text=_("If selected, new videos will"
  703.                                                   " automatically be imported "
  704.                                                   "from this source."))
  705.     user = models.ForeignKey('auth.User', null=True, blank=True)
  706.     auto_categories = models.ManyToManyField("Category", blank=True)
  707.     auto_authors = models.ManyToManyField("auth.User", blank=True,
  708.                                           related_name='auto_%(class)s_set')
  709.  
  710.     THUMB_SIZES = THUMB_SIZES
  711.  
  712.     class Meta:
  713.         abstract = True
  714.  
  715.     def update(self, video_iter, source_import, using='default',
  716.                clear_rejected=True):
  717.         """
  718.         Imports videos from a feed/search.  `videos` is an iterable which
  719.         returns :class:`vidscraper.suites.base.Video` objects.  We use
  720.         :method:`.Video.from_vidscraper_video` to map the Vidscraper fields to
  721.         Video attributes.
  722.  
  723.         If ``clear_rejected`` is ``True``, rejected versions of videos that are
  724.         found in the ``video_iter`` will be deleted and re-imported.
  725.  
  726.         """
  727.         author_pks = list(self.auto_authors.values_list('pk', flat=True))
  728.         category_pks = list(self.auto_categories.values_list('pk', flat=True))
  729.  
  730.         import_opts = source_import.__class__._meta
  731.  
  732.         from localtv.tasks import video_from_vidscraper_video, mark_import_pending
  733.  
  734.         total_videos = 0
  735.  
  736.         for vidscraper_video in video_iter:
  737.             total_videos += 1
  738.             try:
  739.                 video_from_vidscraper_video.delay(
  740.                     vidscraper_video,
  741.                     site_pk=self.site_id,
  742.                     import_app_label=import_opts.app_label,
  743.                     import_model=import_opts.module_name,
  744.                     import_pk=source_import.pk,
  745.                     status=Video.PENDING,
  746.                     author_pks=author_pks,
  747.                     category_pks=category_pks,
  748.                     clear_rejected=clear_rejected,
  749.                     using=using)
  750.             except:
  751.                 source_import.handle_error(
  752.                     'Import task creation failed for %r' % (
  753.                         vidscraper_video.url,),
  754.                     is_skip=True,
  755.                     with_exception=True,
  756.                     using=using)
  757.  
  758.         source_import.__class__._default_manager.using(using).filter(
  759.             pk=source_import.pk
  760.         ).update(
  761.             total_videos=total_videos
  762.         )
  763.         mark_import_pending.delay(import_app_label=import_opts.app_label,
  764.                                   import_model=import_opts.module_name,
  765.                                   import_pk=source_import.pk,
  766.                                   using=using)
  767.  
  768.  
  769. class StatusedThumbnailableQuerySet(models.query.QuerySet):
  770.  
  771.     def unapproved(self):
  772.         return self.filter(status=StatusedThumbnailable.UNAPPROVED)
  773.  
  774.     def active(self):
  775.         return self.filter(status=StatusedThumbnailable.ACTIVE)
  776.  
  777.     def rejected(self):
  778.         return self.filter(status=StatusedThumbnailable.REJECTED)
  779.  
  780.     def pending(self):
  781.         return self.filter(status=StatusedThumbnailable.PENDING)
  782.  
  783.  
  784. class StatusedThumbnailableManager(models.Manager):
  785.  
  786.     def get_query_set(self):
  787.         return StatusedThumbnailableQuerySet(self.model, using=self._db)
  788.  
  789.     def unapproved(self):
  790.         return self.get_query_set().unapproved()
  791.  
  792.     def active(self):
  793.         return self.get_query_set().active()
  794.  
  795.     def rejected(self):
  796.         return self.get_query_set().rejected()
  797.  
  798.     def pending_thumbnail(self):
  799.         return self.get_query_set().pending_thumbnail()
  800.  
  801.  
  802. class StatusedThumbnailable(models.Model):
  803.     """
  804.     Abstract class to provide the ``status`` field for Feeds and Videos.
  805.     """
  806.     #: An admin has not looked at this feed yet.
  807.     UNAPPROVED = 0
  808.     ACTIVE = 1
  809.     #: This feed was rejected by an admin.
  810.     REJECTED = 2
  811.     # This is still being imported
  812.     PENDING = 3
  813.  
  814.     STATUS_CHOICES = (
  815.         (UNAPPROVED, UNAPPROVED_STATUS_TEXT),
  816.         (ACTIVE, ACTIVE_STATUS_TEXT),
  817.         (REJECTED, REJECTED_STATUS_TEXT),
  818.         (PENDING, PENDING_STATUS_TEXT),
  819.     )
  820.  
  821.     objects = StatusedThumbnailableManager()
  822.  
  823.     status = models.IntegerField(
  824.         choices=STATUS_CHOICES, default=UNAPPROVED)
  825.  
  826.     def is_active(self):
  827.         """Shortcut to check the common case of whether a video is active."""
  828.         return self.status == self.ACTIVE
  829.  
  830.     class Meta:
  831.         abstract = True
  832.  
  833.  
  834. class Feed(Source, StatusedThumbnailable):
  835.     """
  836.     Feed to pull videos in from.
  837.  
  838.     If the same feed is used on two different sites, they will require two
  839.     separate entries here.
  840.  
  841.     Fields:
  842.       - feed_url: The location of this field
  843.       - site: which site this feed belongs to
  844.       - name: human readable name for this feed
  845.       - webpage: webpage that this feed\'s content is associated with
  846.       - description: human readable description of this item
  847.       - last_updated: last time we ran self.update_items()
  848.       - when_submitted: when this feed was first registered on this site
  849.       - status: one of Feed.STATUS_CHOICES
  850.       - etag: used to see whether or not the feed has changed since our last
  851.         update.
  852.       - auto_approve: whether or not to set all videos in this feed to approved
  853.         during the import process
  854.       - user: a user that submitted this feed, if any
  855.       - auto_categories: categories that are automatically applied to videos on
  856.         import
  857.       - auto_authors: authors that are automatically applied to videos on
  858.         import
  859.     """
  860.     feed_url = models.URLField(verify_exists=False)
  861.     name = models.CharField(max_length=250)
  862.     webpage = models.URLField(verify_exists=False, blank=True)
  863.     description = models.TextField()
  864.     last_updated = models.DateTimeField()
  865.     when_submitted = models.DateTimeField(auto_now_add=True)
  866.     etag = models.CharField(max_length=250, blank=True)
  867.     avoid_frontpage = models.BooleanField(default=False)
  868.     calculated_source_type = models.CharField(max_length=255, blank=True, default='')
  869.  
  870.     class Meta:
  871.         unique_together = (
  872.             ('feed_url', 'site'))
  873.         get_latest_by = 'last_updated'
  874.  
  875.     def __unicode__(self):
  876.         return self.name
  877.  
  878.     @models.permalink
  879.     def get_absolute_url(self):
  880.         return ('localtv_list_feed', [self.pk])
  881.  
  882.     def update(self, using='default', **kwargs):
  883.         """
  884.         Fetch and import new videos from this feed.
  885.  
  886.         """
  887.         try:
  888.             FeedImport.objects.using(using).get(source=self,
  889.                                                 status=FeedImport.STARTED)
  890.         except FeedImport.DoesNotExist:
  891.             pass
  892.         else:
  893.             logging.debug('Skipping import of %s: already in progress' % self)
  894.             return
  895.  
  896.         feed_import = FeedImport.objects.db_manager(using).create(source=self,
  897.                                                 auto_approve=self.auto_approve)
  898.  
  899.         video_iter = vidscraper.auto_feed(
  900.             self.feed_url,
  901.             crawl=(getattr(self, 'status', True) == 0),
  902.             api_keys={
  903.                 'vimeo_key': getattr(settings, 'VIMEO_API_KEY', None),
  904.                 'vimeo_secret': getattr(settings, 'VIMEO_API_SECRET', None),
  905.                 'ustream_key': getattr(settings, 'USTREAM_API_KEY', None)
  906.             }
  907.         )
  908.  
  909.         try:
  910.             video_iter.load()
  911.         except Exception:
  912.             feed_import.last_activity = datetime.datetime.now()
  913.             feed_import.status = FeedImport.FAILED
  914.             feed_import.save()
  915.             feed_import.handle_error(u'Skipping import of %s: error loading the'
  916.                                      u' feed' % self,
  917.                                      with_exception=True, using=using)
  918.             return
  919.  
  920.         super(Feed, self).update(video_iter, source_import=feed_import,
  921.                                  using=using, **kwargs)
  922.  
  923.         self.etag = getattr(video_iter, 'etag', None) or ''
  924.         self.last_updated = (getattr(video_iter, 'last_modified', None) or
  925.                                  datetime.datetime.now())
  926.         self.save()
  927.  
  928.     def source_type(self):
  929.         return self.calculated_source_type
  930.  
  931.     def _calculate_source_type(self):
  932.         return _feed__calculate_source_type(self)
  933.  
  934.     def video_service(self):
  935.         return feed__video_service(self)
  936.  
  937. def feed__video_service(feed):
  938.     # This implements the video_service method. It's outside the Feed class
  939.     # so we can use it safely from South.
  940.     for service, regexp in VIDEO_SERVICE_REGEXES:
  941.         if re.search(regexp, feed.feed_url, re.I):
  942.             return service
  943.  
  944. def _feed__calculate_source_type(feed):
  945.     # This implements the _calculate_source_type method. It's outside the Feed
  946.     # class so we can use it safely from South.
  947.     video_service = feed__video_service(feed)
  948.     if video_service is None:
  949.         return u'Feed'
  950.     else:
  951.         return u'User: %s' % video_service
  952.  
  953. def pre_save_set_calculated_source_type(instance, **kwargs):
  954.     # Always save the calculated_source_type
  955.     instance.calculated_source_type = _feed__calculate_source_type(instance)
  956.     # Plus, if the name changed, we have to recalculate all the Videos that depend on us.
  957.     try:
  958.         v = Feed.objects.using(instance._state.db).get(id=instance.id)
  959.     except Feed.DoesNotExist:
  960.         return instance
  961.     if v.name != instance.name:
  962.         # recalculate all the sad little videos' calculated_source_type
  963.         for vid in instance.video_set.all():
  964.             vid.save()
  965.     return instance
  966. models.signals.pre_save.connect(pre_save_set_calculated_source_type,
  967.                                 sender=Feed)
  968.  
  969.  
  970. class Category(models.Model):
  971.     """
  972.     A category for videos to be contained in.
  973.  
  974.     Categories and tags aren't too different functionally, but categories are
  975.     more strict as they can't be defined by visitors.  Categories can also be
  976.     hierarchical.
  977.  
  978.     Fields:
  979.      - site: A link to the django.contrib.sites.models.Site object this object
  980.        is bound to
  981.      - name: Name of this category
  982.      - slug: a slugified verison of the name, used to create more friendly URLs
  983.      - logo: An image to associate with this category
  984.      - description: human readable description of this item
  985.      - parent: Reference to another Category.  Allows you to have heirarchical
  986.        categories.
  987.     """
  988.     site = models.ForeignKey(Site)
  989.     name = models.CharField(
  990.         max_length=80, verbose_name='Category Name',
  991.         help_text=_("The name is used to identify the category almost "
  992.                     "everywhere; for example, under a video or in a "
  993.                     "category widget."))
  994.     slug = models.SlugField(
  995.         verbose_name='Category Slug',
  996.         help_text=_("The \"slug\" is the URL-friendly version of the name.  It "
  997.                     "is usually lower-case and contains only letters, numbers "
  998.                     "and hyphens."))
  999.     logo = models.ImageField(
  1000.         upload_to="localtv/category_logos", blank=True,
  1001.         verbose_name='Thumbnail/Logo',
  1002.         help_text=_("Optional. For example: a leaf for 'environment' or the "
  1003.                     "logo of a university department."))
  1004.     description = models.TextField(
  1005.         blank=True, verbose_name='Description (HTML)',
  1006.         help_text=_("Optional. The description is not prominent by default, but"
  1007.                     " some themes may show it."))
  1008.     parent = models.ForeignKey(
  1009.         'self', blank=True, null=True,
  1010.         related_name='child_set',
  1011.         verbose_name='Category Parent',
  1012.         help_text=_("Categories, unlike tags, can have a hierarchy."))
  1013.  
  1014.     # only relevant is voting is enabled for the site
  1015.     contest_mode = models.DateTimeField('Turn on Contest',
  1016.                                         null=True,
  1017.                                         default=None)
  1018.  
  1019.     class Meta:
  1020.         ordering = ['name']
  1021.         unique_together = (
  1022.             ('slug', 'site'),
  1023.             ('name', 'site'))
  1024.  
  1025.     def __unicode__(self):
  1026.         return self.name
  1027.  
  1028.     def depth(self):
  1029.         """
  1030.         Returns the number of parents this category has.  Used for indentation.
  1031.         """
  1032.         depth = 0
  1033.         parent = self.parent
  1034.         while parent is not None:
  1035.             depth += 1
  1036.             parent = parent.parent
  1037.         return depth
  1038.  
  1039.     def dashes(self):
  1040.         return mark_safe('&mdash;' * self.depth())
  1041.  
  1042.     @models.permalink
  1043.     def get_absolute_url(self):
  1044.         return ('localtv_category', [self.slug])
  1045.  
  1046.     @classmethod
  1047.     def in_order(klass, sitelocation, initial=None):
  1048.         objects = []
  1049.         def accumulate(categories):
  1050.             for category in categories:
  1051.                 objects.append(category)
  1052.                 if category.child_set.count():
  1053.                     accumulate(category.child_set.all())
  1054.         if initial is None:
  1055.             initial = klass.objects.filter(site=sitelocation, parent=None)
  1056.         accumulate(initial)
  1057.         return objects
  1058.  
  1059.     def approved_set(self):
  1060.         """
  1061.         Returns active videos for the category and its subcategories, ordered
  1062.         by decreasing best date.
  1063.        
  1064.         """
  1065.         categories = [self] + self.in_order(self.site, self.child_set.all())
  1066.         return Video.objects.active().filter(
  1067.             categories__in=categories).distinct()
  1068.     approved_set = property(approved_set)
  1069.  
  1070.     def unique_error_message(self, model_class, unique_check):
  1071.         return 'Category with this %s already exists.' % (
  1072.             unique_check[0],)
  1073.  
  1074.     def has_votes(self):
  1075.         """
  1076.         Returns True if this category has videos with votes.
  1077.         """
  1078.         if not lsettings.voting_enabled():
  1079.             return False
  1080.         import voting
  1081.         return voting.models.Vote.objects.filter(
  1082.             content_type=ContentType.objects.get_for_model(Video),
  1083.             object_id__in=self.approved_set.values_list('id',
  1084.                                                         flat=True)).exists()
  1085.  
  1086.  
  1087. class SavedSearch(Source):
  1088.     """
  1089.     A set of keywords to regularly pull in new videos from.
  1090.  
  1091.     There's an administrative interface for doing "live searches"
  1092.  
  1093.     Fields:
  1094.      - site: site this savedsearch applies to
  1095.      - query_string: a whitespace-separated list of words to search for.  Words
  1096.        starting with a dash will be processed as negative query terms
  1097.      - when_created: date and time that this search was saved.
  1098.     """
  1099.     query_string = models.TextField()
  1100.     when_created = models.DateTimeField(auto_now_add=True)
  1101.  
  1102.     def __unicode__(self):
  1103.         return self.query_string
  1104.  
  1105.     def update(self, using='default', **kwargs):
  1106.         """
  1107.         Fetch and import new videos from this search.
  1108.  
  1109.         """
  1110.         try:
  1111.             SearchImport.objects.using(using).get(source=self,
  1112.                                                   status=SearchImport.STARTED)
  1113.         except SearchImport.DoesNotExist:
  1114.             pass
  1115.         else:
  1116.             logging.debug('Skipping import of %s: already in progress' % self)
  1117.             return
  1118.  
  1119.         search_import = SearchImport.objects.db_manager(using).create(
  1120.             source=self,
  1121.             auto_approve=self.auto_approve
  1122.         )
  1123.  
  1124.         searches = vidscraper.auto_search(
  1125.             self.query_string,
  1126.             crawl=True,
  1127.             api_keys={
  1128.                 'vimeo_key': getattr(settings, 'VIMEO_API_KEY', None),
  1129.                 'vimeo_secret': getattr(settings, 'VIMEO_API_SECRET', None),
  1130.                 'ustream_key': getattr(settings, 'USTREAM_API_KEY', None)
  1131.             }
  1132.         )
  1133.  
  1134.         # Mark the import as "ended" immediately if none of the searches can
  1135.         # load.
  1136.         should_end = True
  1137.         for video_iter in searches.values():
  1138.             try:
  1139.                 video_iter.load()
  1140.             except Exception:
  1141.                 search_import.handle_error(u'Skipping import of search results '
  1142.                                u'from %s' % video_iter.suite.__class__.__name__,
  1143.                                with_exception=True, using=using)
  1144.                 continue
  1145.             should_end = False
  1146.             super(SavedSearch, self).update(video_iter,
  1147.                                             source_import=search_import,
  1148.                                             using=using, **kwargs)
  1149.         if should_end:
  1150.             search_import.status = SearchImport.FAILED
  1151.             search_import.last_activity = datetime.datetime.now()
  1152.             search_import.save()
  1153.             logging.debug('All searches failed for %s' % self)
  1154.  
  1155.     def source_type(self):
  1156.         return u'Search'
  1157.  
  1158.  
  1159. class SourceImportIndex(models.Model):
  1160.     video = models.OneToOneField('Video', unique=True)
  1161.     index = models.PositiveIntegerField(blank=True, null=True)
  1162.    
  1163.     class Meta:
  1164.         abstract = True
  1165.  
  1166.  
  1167. class FeedImportIndex(SourceImportIndex):
  1168.     source_import = models.ForeignKey('FeedImport', related_name='indexes')
  1169.  
  1170.  
  1171. class SearchImportIndex(SourceImportIndex):
  1172.     source_import = models.ForeignKey('SearchImport', related_name='indexes')
  1173.     #: This is just the name of the suite that was used to get this index.
  1174.     suite = models.CharField(max_length=30)
  1175.  
  1176.  
  1177. class SourceImportError(models.Model):
  1178.     message = models.TextField()
  1179.     traceback = models.TextField(blank=True)
  1180.     is_skip = models.BooleanField(help_text="Whether this error represents a "
  1181.                                             "video that was skipped.")
  1182.     datetime = models.DateTimeField(auto_now_add=True)
  1183.  
  1184.     class Meta:
  1185.         abstract = True
  1186.  
  1187.  
  1188. class FeedImportError(SourceImportError):
  1189.     source_import = models.ForeignKey('FeedImport', related_name='errors')
  1190.  
  1191.  
  1192. class SearchImportError(SourceImportError):
  1193.     source_import = models.ForeignKey('SearchImport', related_name='errors')
  1194.  
  1195.  
  1196. class SourceImport(models.Model):
  1197.     STARTED = 'started'
  1198.     PENDING = 'pending'
  1199.     COMPLETE = 'complete'
  1200.     FAILED = 'failed'
  1201.     STATUS_CHOICES = (
  1202.         (STARTED, _('Started')),
  1203.         (PENDING, _('Pending haystack updates')),
  1204.         (COMPLETE, _('Complete')),
  1205.         (FAILED, _('Failed'))
  1206.     )
  1207.     start = models.DateTimeField(auto_now_add=True)
  1208.     last_activity = models.DateTimeField(blank=True, null=True)
  1209.     total_videos = models.PositiveIntegerField(blank=True, null=True)
  1210.     videos_imported = models.PositiveIntegerField(default=0)
  1211.     videos_skipped = models.PositiveIntegerField(default=0)
  1212.     #: Caches the auto_approve of the search on the import, so that the imported
  1213.     #: videos can be approved en masse at the end of the import based on the
  1214.     #: settings at the beginning of the import.
  1215.     auto_approve = models.BooleanField()
  1216.     status = models.CharField(max_length=10, choices=STATUS_CHOICES,
  1217.                               default=STARTED)
  1218.  
  1219.     class Meta:
  1220.         get_latest_by = 'start'
  1221.         ordering = ['-start']
  1222.         abstract = True
  1223.  
  1224.     def set_video_source(self, video):
  1225.         """
  1226.         Sets the value of the correct field on the ``video`` to mark it as
  1227.         having the same source as this import. Must be implemented by
  1228.         subclasses.
  1229.  
  1230.         """
  1231.         raise NotImplementedError
  1232.  
  1233.     def get_videos(self, using='default'):
  1234.         raise NotImplementedError
  1235.  
  1236.     def handle_error(self, message, is_skip=False, with_exception=False,
  1237.                      using='default'):
  1238.         """
  1239.         Logs the error with the default logger and to the database.
  1240.  
  1241.         :param message: A human-friendly description of the error that does
  1242.                         not contain sensitive information.
  1243.         :param is_skip: ``True`` if the error results in a video being skipped.
  1244.                         Default: False.
  1245.         :param with_exception: ``True`` if exception information should be
  1246.                                recorded. Default: False.
  1247.         :param using: The database to use. Default: 'default'.
  1248.  
  1249.         """
  1250.         if with_exception:
  1251.             exc_info = sys.exc_info()
  1252.             logging.debug(message, exc_info=exc_info)
  1253.             tb = ''.join(traceback.format_exception(*exc_info))
  1254.         else:
  1255.             logging.debug(message)
  1256.             tb = ''
  1257.         self.errors.db_manager(using).create(message=message,
  1258.                                              source_import=self,
  1259.                                              traceback=tb,
  1260.                                              is_skip=is_skip)
  1261.         if is_skip:
  1262.             self.__class__._default_manager.using(using).filter(pk=self.pk
  1263.                         ).update(videos_skipped=models.F('videos_skipped') + 1)
  1264.             from localtv.tasks import mark_import_pending
  1265.             mark_import_pending.delay(import_app_label=self._meta.app_label,
  1266.                                       import_model=self._meta.module_name,
  1267.                                       import_pk=self.pk,
  1268.                                       using=using)
  1269.  
  1270.     def get_index_creation_kwargs(self, video, vidscraper_video):
  1271.         return {
  1272.             'source_import': self,
  1273.             'video': video,
  1274.             'index': vidscraper_video.index
  1275.         }
  1276.  
  1277.     def handle_video(self, video, vidscraper_video, using='default'):
  1278.         """
  1279.         Creates an index instance connecting the video to this import.
  1280.  
  1281.         :param video: The :class:`Video` instance which was imported.
  1282.         :param vidscraper_video: The original video from :mod:`vidscraper`.
  1283.         :param using: The database alias to use. Default: 'default'
  1284.  
  1285.         """
  1286.         self.indexes.db_manager(using).create(
  1287.                     **self.get_index_creation_kwargs(video, vidscraper_video))
  1288.         self.__class__._default_manager.using(using).filter(pk=self.pk
  1289.                     ).update(videos_imported=models.F('videos_imported') + 1)
  1290.         from localtv.tasks import mark_import_pending
  1291.         mark_import_pending.delay(import_app_label=self._meta.app_label,
  1292.                                   import_model=self._meta.module_name,
  1293.                                   import_pk=self.pk,
  1294.                                   using=using)
  1295.  
  1296.  
  1297. class FeedImport(SourceImport):
  1298.     source = models.ForeignKey(Feed, related_name='imports')
  1299.  
  1300.     def set_video_source(self, video):
  1301.         video.feed_id = self.source_id
  1302.  
  1303.     def get_videos(self, using='default'):
  1304.         return Video.objects.using(using).filter(
  1305.                                         feedimportindex__source_import=self)
  1306.  
  1307.  
  1308. class SearchImport(SourceImport):
  1309.     source = models.ForeignKey(SavedSearch, related_name='imports')
  1310.  
  1311.     def set_video_source(self, video):
  1312.         video.search_id = self.source_id
  1313.  
  1314.     def get_videos(self, using='default'):
  1315.         return Video.objects.using(using).filter(
  1316.                                         searchimportindex__source_import=self)
  1317.  
  1318.     def get_index_creation_kwargs(self, video, vidscraper_video):
  1319.         kwargs = super(SearchImport, self).get_index_creation_kwargs(video,
  1320.                                                             vidscraper_video)
  1321.         kwargs['suite'] = vidscraper_video.suite.__class__.__name__
  1322.         return kwargs
  1323.  
  1324.  
  1325. class VideoBase(models.Model):
  1326.     """
  1327.     Base class between Video and OriginalVideo.  It would be simple enough to
  1328.     duplicate these fields, but this way it's easier to add more points of
  1329.     duplication in the future.
  1330.     """
  1331.     name = models.CharField(max_length=250)
  1332.     description = models.TextField(blank=True)
  1333.     thumbnail_url = models.URLField(
  1334.         verify_exists=False, blank=True, max_length=400)
  1335.  
  1336.     class Meta:
  1337.         abstract = True
  1338.  
  1339. class OriginalVideo(VideoBase):
  1340.  
  1341.     VIDEO_ACTIVE, VIDEO_DELETED, VIDEO_DELETE_PENDING = range(3)
  1342.  
  1343.     video = models.OneToOneField('Video', related_name='original')
  1344.     thumbnail_updated = models.DateTimeField(blank=True)
  1345.     remote_video_was_deleted = models.IntegerField(default=VIDEO_ACTIVE)
  1346.     remote_thumbnail_hash = models.CharField(max_length=64, default='')
  1347.  
  1348.     def changed_fields(self, override_vidscraper_result=None):
  1349.         """
  1350.         Check our video for new data.
  1351.         """
  1352.         video = self.video
  1353.         if not video.website_url:
  1354.             # we shouldn't have been created, but either way we can't do
  1355.             # anything here
  1356.             self.delete()
  1357.             return {}
  1358.  
  1359.         remote_video_was_deleted = False
  1360.         fields = ['title', 'description', 'tags', 'thumbnail_url']
  1361.         if override_vidscraper_result is not None:
  1362.             vidscraper_video = override_vidscraper_result
  1363.         else:
  1364.             try:
  1365.                 vidscraper_video = vidscraper.auto_scrape(
  1366.                     video.website_url, fields=fields)
  1367.             except vidscraper.errors.VideoDeleted:
  1368.                 remote_video_was_deleted = True
  1369.  
  1370.         # Now that we have the "scraped_data", analyze it: does it look like
  1371.         # a skeletal video, with no data? Then we infer it was deleted.
  1372.         if remote_video_was_deleted or all(not getattr(vidscraper_video, field)
  1373.                                            for field in fields):
  1374.             remote_video_was_deleted = True
  1375.         # If the scraped_data has all None values, then infer that the remote video was
  1376.         # deleted.
  1377.  
  1378.         if remote_video_was_deleted:
  1379.             if self.remote_video_was_deleted == OriginalVideo.VIDEO_DELETED:
  1380.                 return {} # We already notified the admins of the deletion.
  1381.             else:
  1382.                 return {'deleted': True}
  1383.         elif self.remote_video_was_deleted:
  1384.             return {'deleted': False}
  1385.  
  1386.         changed_fields = {}
  1387.  
  1388.         for field in fields:
  1389.             if field == 'tags': # special case tag checking
  1390.                 if vidscraper_video.tags is None:
  1391.                     # failed to get tags, so don't send a spurious change
  1392.                     # message
  1393.                     continue
  1394.                 new = utils.unicode_set(vidscraper_video.tags)
  1395.                 if getattr(settings, 'FORCE_LOWERCASE_TAGS'):
  1396.                     new = utils.unicode_set(name.lower() for name in new)
  1397.                 old = utils.unicode_set(self.tags)
  1398.                 if new != old:
  1399.                     changed_fields[field] = new
  1400.             elif field == 'thumbnail_url':
  1401.                 if vidscraper_video.thumbnail_url != self.thumbnail_url:
  1402.                     changed_fields[field] = vidscraper_video.thumbnail_url
  1403.                 else:
  1404.                     right_now = datetime.datetime.utcnow()
  1405.                     if self._remote_thumbnail_appears_changed():
  1406.                         changed_fields['thumbnail_updated'] = right_now
  1407.             else:
  1408.                 if field == 'title':
  1409.                     model_field = 'name'
  1410.                 else:
  1411.                     model_field = field
  1412.                 if (utils.normalize_newlines(
  1413.                         getattr(vidscraper_video, field)) !=
  1414.                     utils.normalize_newlines(
  1415.                         getattr(self, model_field))):
  1416.                     changed_fields[model_field] = getattr(vidscraper_video, field)
  1417.  
  1418.         return changed_fields
  1419.  
  1420.     def originals_for_changed_fields(self, changed_fields):
  1421.         '''The OriginalVideo emails need to say not just the new data, but also
  1422.         provide the value that was in the OriginalVideo object just before the
  1423.         email is sent.
  1424.  
  1425.         This function takes a changed_fields dictionary, and uses its keys to
  1426.         figure out what relevant snapshotted information would help the user
  1427.         contextualize the changed_fields data.'''
  1428.         old_fields = {}
  1429.  
  1430.         if 'deleted' in changed_fields:
  1431.             return old_fields
  1432.  
  1433.         for key in changed_fields:
  1434.             old_fields[key] = getattr(self, key)
  1435.  
  1436.         return old_fields
  1437.  
  1438.     def _remote_thumbnail_appears_changed(self):
  1439.         '''This private method checks if the remote thumbnail has been updated.
  1440.  
  1441.         It takes no arguments, because you are only supposed to call it
  1442.         when the remote video service did not give us a new thumbnail URL.
  1443.  
  1444.         It returns a boolean. True, if and only if the remote video has:
  1445.  
  1446.         * a Last-Modified header indicating it has been modified, and
  1447.         * HTTP response body that hashes to a different SHA1 than the
  1448.           one we stored.
  1449.  
  1450.         It treats "self" as read-only.'''
  1451.         # because the data might have changed, check to see if the
  1452.         # thumbnail has been modified
  1453.         made_time = time.mktime(self.thumbnail_updated.utctimetuple())
  1454.         # we take made_time literally, because the localtv app MUST
  1455.         # be storing UTC time data in the column.
  1456.         modified = email.utils.formatdate(made_time,
  1457.                                           usegmt=True)
  1458.         request = urllib2.Request(self.thumbnail_url)
  1459.         request.add_header('If-Modified-Since', modified)
  1460.         try:
  1461.             response = urllib2.build_opener().open(request)
  1462.         except urllib2.HTTPError:
  1463.             # We get this for 304, but we'll just ignore all the other
  1464.             # errors too
  1465.             return False
  1466.         else:
  1467.             if response.info().get('Last-modified', modified) == \
  1468.                     modified:
  1469.                 # hasn't really changed, or doesn't exist
  1470.                 return False
  1471.  
  1472.         # If we get here, then the remote server thinks that the file is fresh.
  1473.         # We should check its SHA1 hash against the one we have stored.
  1474.         new_sha1 = utils.hash_file_obj(response)
  1475.  
  1476.         if new_sha1 == self.remote_thumbnail_hash:
  1477.             # FIXME: Somehow alert downstream layers that it is safe to update
  1478.             # the modified-date in the database.
  1479.             return False # bail out early, empty -- the image is the same
  1480.  
  1481.         # Okay, so the hashes do not match; the remote image truly has changed.
  1482.         # Let's report the timestamp as having a Last-Modified date of right now.
  1483.         return True
  1484.  
  1485.     def send_deleted_notification(self):
  1486.         if self.remote_video_was_deleted == OriginalVideo.VIDEO_DELETE_PENDING:
  1487.             from localtv.utils import send_notice
  1488.             t = loader.get_template('localtv/admin/video_deleted.txt')
  1489.             c = Context({'video': self.video})
  1490.             subject = '[%s] Video Deleted: "%s"' % (
  1491.                 self.video.site.name, self.video.name)
  1492.             message = t.render(c)
  1493.             send_notice('admin_video_updated', subject, message,
  1494.                         sitelocation=SiteLocation.objects.get(
  1495.                     site=self.video.site))
  1496.             # Update the OriginalVideo to show that we sent this notification
  1497.             # out.
  1498.             self.remote_video_was_deleted = OriginalVideo.VIDEO_DELETED
  1499.         else:
  1500.             # send the message next time
  1501.             self.remote_video_was_deleted = OriginalVideo.VIDEO_DELETE_PENDING
  1502.         self.save()
  1503.  
  1504.     def update(self, override_vidscraper_result = None):
  1505.         from localtv.utils import get_or_create_tags
  1506.  
  1507.         changed_fields = self.changed_fields(override_vidscraper_result)
  1508.         if not changed_fields:
  1509.             return # don't need to do anything
  1510.  
  1511.         # Was the remote video deleted?
  1512.         if changed_fields.pop('deleted', None):
  1513.             # Have we already sent the notification
  1514.             # Mark inside the OriginalVideo that the video has been deleted.
  1515.             # Yes? Uh oh.
  1516.             self.send_deleted_notification()
  1517.             return # Stop processing here.
  1518.  
  1519.         original_values = self.originals_for_changed_fields(changed_fields)
  1520.  
  1521.         changed_model = False
  1522.         for field in changed_fields.copy():
  1523.             if field == 'tags': # special case tag equality
  1524.                 if set(self.tags) == set(self.video.tags):
  1525.                     self.tags = self.video.tags = get_or_create_tags(
  1526.                         changed_fields.pop('tags'))
  1527.             elif field in ('thumbnail_url', 'thumbnail_updated'):
  1528.                 if self.thumbnail_url == self.video.thumbnail_url:
  1529.                     value = changed_fields.pop(field)
  1530.                     if field == 'thumbnail_url':
  1531.                         self.thumbnail_url = self.video.thumbnail_url = value
  1532.                     changed_model = True
  1533.                     self.video.save_thumbnail()
  1534.             elif getattr(self, field) == getattr(self.video, field):
  1535.                 value = changed_fields.pop(field)
  1536.                 setattr(self, field, value)
  1537.                 setattr(self.video, field, value)
  1538.                 changed_model = True
  1539.  
  1540.         if self.remote_video_was_deleted:
  1541.             self.remote_video_was_deleted = OriginalVideo.VIDEO_ACTIVE
  1542.             changed_model = True
  1543.  
  1544.         if changed_model:
  1545.             self.save()
  1546.             self.video.save()
  1547.  
  1548.         if not changed_fields: # modified them all
  1549.             return
  1550.  
  1551.         self.send_updated_notification(changed_fields, original_values)
  1552.  
  1553.     def send_updated_notification(self, changed_fields, originals_for_changed_fields):
  1554.         from localtv.utils import send_notice, get_or_create_tags
  1555.  
  1556.         # Create a custom hodge-podge of changed fields and the original values
  1557.         hodge_podge = {}
  1558.         for key in changed_fields:
  1559.             hodge_podge[key] = (
  1560.                 changed_fields[key],
  1561.                 originals_for_changed_fields.get(key, None))
  1562.  
  1563.         t = loader.get_template('localtv/admin/video_updated.txt')
  1564.         c = Context({'video': self.video,
  1565.                      'original': self,
  1566.                      'changed_fields': hodge_podge})
  1567.         subject = '[%s] Video Updated: "%s"' % (
  1568.             self.video.site.name, self.video.name)
  1569.         message = t.render(c)
  1570.         send_notice('admin_video_updated', subject, message,
  1571.                     sitelocation=SiteLocation.objects.get(
  1572.                 site=self.video.site))
  1573.  
  1574.         # And update the self instance to reflect the changes.
  1575.         for field in changed_fields:
  1576.             if field == 'tags':
  1577.                 self.tags = get_or_create_tags(changed_fields[field])
  1578.             else:
  1579.                 setattr(self, field, changed_fields[field])
  1580.         self.save()
  1581.  
  1582.  
  1583. class VideoQuerySet(StatusedThumbnailableQuerySet):
  1584.  
  1585.     def with_best_date(self, use_original_date=True):
  1586.         if use_original_date:
  1587.             published = 'localtv_video.when_published,'
  1588.         else:
  1589.             published = ''
  1590.         return self.extra(select={'best_date': """
  1591. COALESCE(%slocaltv_video.when_approved,
  1592. localtv_video.when_submitted)""" % published})
  1593.  
  1594.     def with_watch_count(self, since=EMPTY):
  1595.         """
  1596.         Returns a QuerySet of videos annotated with a ``watch_count`` of all
  1597.         watches since ``since`` (a datetime, which defaults to seven days ago).
  1598.         """
  1599.         if since is EMPTY:
  1600.             since = datetime.datetime.now() - datetime.timedelta(days=7)
  1601.  
  1602.         return self.extra(
  1603.             select={'watch_count': """SELECT COUNT(*) FROM localtv_watch
  1604. WHERE localtv_video.id = localtv_watch.video_id AND
  1605. localtv_watch.timestamp > %s"""},
  1606.             select_params = (since,)
  1607.         )
  1608.  
  1609.  
  1610. class VideoManager(StatusedThumbnailableManager):
  1611.  
  1612.     def get_query_set(self):
  1613.         return VideoQuerySet(self.model, using=self._db)
  1614.  
  1615.     def with_best_date(self, *args, **kwargs):
  1616.         return self.get_query_set().with_best_date(*args, **kwargs)
  1617.  
  1618.     def popular_since(self, *args, **kwargs):
  1619.         return self.get_query_set().popular_since(*args, **kwargs)
  1620.  
  1621.     def get_sitelocation_videos(self, sitelocation=None):
  1622.         """
  1623.         Returns a QuerySet of videos which are active and tied to the
  1624.         sitelocation. This QuerySet is cached on the request.
  1625.        
  1626.         """
  1627.         if sitelocation is None:
  1628.             sitelocation = SiteLocation.objects.get_current()
  1629.         return self.active().filter(site=sitelocation.site)
  1630.  
  1631.     def get_featured_videos(self, sitelocation=None):
  1632.         """
  1633.         Returns a ``QuerySet`` of active videos which are considered "featured"
  1634.         for the sitelocation.
  1635.  
  1636.         """
  1637.         return self.get_sitelocation_videos(sitelocation).filter(
  1638.             last_featured__isnull=False
  1639.         ).order_by(
  1640.             '-last_featured',
  1641.             '-when_approved',
  1642.             '-when_published',
  1643.             '-when_submitted'
  1644.         )
  1645.  
  1646.     def get_latest_videos(self, sitelocation=None):
  1647.         """
  1648.         Returns a ``QuerySet`` of active videos for the sitelocation, ordered by
  1649.         decreasing ``best_date``.
  1650.        
  1651.         """
  1652.         if sitelocation is None:
  1653.             sitelocation = SiteLocation.objects.get_current()
  1654.         return self.get_sitelocation_videos(sitelocation).with_best_date(
  1655.             sitelocation.use_original_date
  1656.         ).order_by('-best_date')
  1657.  
  1658.     def get_popular_videos(self, sitelocation=None):
  1659.         """
  1660.         Returns a ``QuerySet`` of active videos considered "popular" for the
  1661.         current sitelocation.
  1662.  
  1663.         """
  1664.         return self.get_latest_videos(sitelocation).with_watch_count().order_by(
  1665.             '-watch_count',
  1666.             '-best_date'
  1667.         )
  1668.  
  1669.     def get_category_videos(self, category, sitelocation=None):
  1670.         """
  1671.         Returns a ``QuerySet`` of active videos considered part of the selected
  1672.         category or its descendants for the sitelocation.
  1673.  
  1674.         """
  1675.         if sitelocation is None:
  1676.             sitelocation = SiteLocation.objects.get_current()
  1677.         # category.approved_set already checks active().
  1678.         return category.approved_set.filter(
  1679.             site=sitelocation.site
  1680.         ).with_best_date(
  1681.             sitelocation.use_original_date
  1682.         ).order_by('-best_date')
  1683.  
  1684.     def get_tag_videos(self, tag, sitelocation=None):
  1685.         """
  1686.         Returns a ``QuerySet`` of active videos with the given tag for the
  1687.         sitelocation.
  1688.  
  1689.         """
  1690.         if sitelocation is None:
  1691.             sitelocation = SiteLocation.objects.get_current()
  1692.         return Video.tagged.with_all(tag).active().filter(
  1693.             site=sitelocation.site
  1694.         ).order_by(
  1695.             '-when_approved',
  1696.             '-when_published',
  1697.             '-when_submitted'
  1698.         )
  1699.  
  1700.     def get_author_videos(self, author, sitelocation=None):
  1701.         """
  1702.         Returns a ``QuerySet`` of active videos published or produced by
  1703.         ``author`` related to the sitelocation.
  1704.  
  1705.         """
  1706.         return self.get_latest_videos(sitelocation).filter(
  1707.             models.Q(authors=author) | models.Q(user=author)
  1708.         ).distinct().order_by('-best_date')
  1709.  
  1710.     def in_feed_order(self, feed=None, sitelocation=None):
  1711.         """
  1712.         Returns a ``QuerySet`` of active videos ordered by the order they were
  1713.         in when originally imported.
  1714.         """
  1715.         if sitelocation is None and feed:
  1716.             sitelocation = SiteLocation.objects.get(site=feed.site)
  1717.         if sitelocation:
  1718.             qs = self.get_latest_videos(sitelocation)
  1719.         else:
  1720.             qs = self.all()
  1721.         if feed:
  1722.             qs = qs.filter(feed=feed)
  1723.         return qs.order_by('-feedimportindex__source_import__start',
  1724.                            'feedimportindex__index',
  1725.                            '-id')
  1726.  
  1727.  
  1728. class Video(Thumbnailable, VideoBase, StatusedThumbnailable):
  1729.     """
  1730.     Fields:
  1731.      - name: Name of this video
  1732.      - site: Site this video is attached to
  1733.      - description: Video description
  1734.      - tags: A list of Tag objects associated with this item
  1735.      - categories: Similar to Tags
  1736.      - authors: the person/people responsible for this video
  1737.      - file_url: The file this object points to (if any) ... if not
  1738.        provided, at minimum we need the embed_code for the item.
  1739.      - file_url_length: size of the file, in bytes
  1740.      - file_url_mimetype: mimetype of the file
  1741.      - when_submitted: When this item was first entered into the
  1742.        database
  1743.      - when_approved: When this item was marked to appear publicly on
  1744.        the site
  1745.      - when_published: When this file was published at its original
  1746.        source (if known)
  1747.      - last_featured: last time this item was featured.
  1748.      - status: one of Video.STATUS_CHOICES
  1749.      - feed: which feed this item came from (if any)
  1750.      - website_url: The page that this item is associated with.
  1751.      - embed_code: code used to embed this item.
  1752.      - flash_enclosure_url: Crappy enclosure link that doesn't
  1753.        actually point to a url.. the kind crappy flash video sites
  1754.        give out when they don't actually want their enclosures to
  1755.        point to video files.
  1756.      - guid: data used to identify this video
  1757.      - has_thumbnail: whether or not this video has a thumbnail
  1758.      - thumbnail_url: url to the thumbnail, if such a thing exists
  1759.      - thumbnail_extension: extension of the *internal* thumbnail, saved on the
  1760.        server (usually paired with the id, so we can determine "1123.jpg" or
  1761.        "1186.png"
  1762.      - user: if not None, the user who submitted this video
  1763.      - search: if not None, the SavedSearch from which this video came
  1764.      - video_service_user: if not blank, the username of the user on the video
  1765.        service who owns this video.  We can figure out the service from the
  1766.        website_url.
  1767.      - contact: a free-text field for anonymous users to specify some contact
  1768.        info
  1769.      - notes: a free-text field to add notes about the video
  1770.     """
  1771.     site = models.ForeignKey(Site)
  1772.     categories = models.ManyToManyField(Category, blank=True)
  1773.     authors = models.ManyToManyField('auth.User', blank=True,
  1774.                                      related_name='authored_set')
  1775.     file_url = BitLyWrappingURLField(verify_exists=False, blank=True)
  1776.     file_url_length = models.IntegerField(null=True, blank=True)
  1777.     file_url_mimetype = models.CharField(max_length=60, blank=True)
  1778.     when_modified = models.DateTimeField(auto_now=True,
  1779.                                          db_index=True,
  1780.                                          default=datetime.datetime.now)
  1781.     when_submitted = models.DateTimeField(auto_now_add=True)
  1782.     when_approved = models.DateTimeField(null=True, blank=True)
  1783.     when_published = models.DateTimeField(null=True, blank=True)
  1784.     last_featured = models.DateTimeField(null=True, blank=True)
  1785.     feed = models.ForeignKey(Feed, null=True, blank=True)
  1786.     website_url = BitLyWrappingURLField(verbose_name='Website URL',
  1787.                                         verify_exists=False,
  1788.                                         blank=True)
  1789.     embed_code = models.TextField(blank=True)
  1790.     flash_enclosure_url = BitLyWrappingURLField(verify_exists=False,
  1791.                                                 blank=True)
  1792.     guid = models.CharField(max_length=250, blank=True)
  1793.     user = models.ForeignKey('auth.User', null=True, blank=True)
  1794.     search = models.ForeignKey(SavedSearch, null=True, blank=True)
  1795.     video_service_user = models.CharField(max_length=250, blank=True,
  1796.                                           default='')
  1797.     video_service_url = models.URLField(verify_exists=False, blank=True,
  1798.                                         default='')
  1799.     contact = models.CharField(max_length=250, blank=True,
  1800.                                default='')
  1801.     notes = models.TextField(blank=True)
  1802.     calculated_source_type = models.CharField(max_length=255, blank=True, default='')
  1803.  
  1804.     objects = VideoManager()
  1805.  
  1806.     THUMB_SIZES = THUMB_SIZES
  1807.  
  1808.     class Meta:
  1809.         ordering = ['-when_submitted']
  1810.         get_latest_by = 'when_modified'
  1811.  
  1812.     def __unicode__(self):
  1813.         return self.name
  1814.  
  1815.     @models.permalink
  1816.     def get_absolute_url(self):
  1817.         return ('localtv_view_video', (),
  1818.                 {'video_id': self.id,
  1819.                  'slug': slugify(self.name)[:30]})
  1820.  
  1821.     @classmethod
  1822.     def from_vidscraper_video(cls, video, status=None, commit=True,
  1823.                               using='default', source_import=None, site_pk=None,
  1824.                               authors=None, categories=None):
  1825.         """
  1826.         Builds a :class:`Video` instance from a
  1827.         :class:`vidscraper.suites.base.Video` instance. If `commit` is False,
  1828.         the :class:`Video` will not be saved, and the created instance will have
  1829.         a `save_m2m()` method that must be called after you call `save()`.
  1830.  
  1831.         :raises: :class:`localtv.exceptions.InvalidVideo` if `commit` is
  1832.                  ``True`` and the created :class:`Video` does not have a valid
  1833.                  ``file_url`` or ``embed_code``.
  1834.  
  1835.         """
  1836.         if video.file_url_expires is None:
  1837.             file_url = video.file_url
  1838.         else:
  1839.             file_url = None
  1840.  
  1841.         if status is None:
  1842.             status = cls.UNAPPROVED
  1843.         if site_pk is None:
  1844.             site_pk = settings.SITE_ID
  1845.  
  1846.         now = datetime.datetime.now()
  1847.  
  1848.         instance = cls(
  1849.             guid=video.guid or '',
  1850.             name=video.title or '',
  1851.             description=video.description or '',
  1852.             website_url=video.link or '',
  1853.             when_published=video.publish_datetime,
  1854.             file_url=file_url or '',
  1855.             file_url_mimetype=video.file_url_mimetype or '',
  1856.             file_url_length=video.file_url_length,
  1857.             when_submitted=now,
  1858.             when_approved=now if status == cls.ACTIVE else None,
  1859.             status=status,
  1860.             thumbnail_url=video.thumbnail_url or '',
  1861.             embed_code=video.embed_code or '',
  1862.             flash_enclosure_url=video.flash_enclosure_url or '',
  1863.             video_service_user=video.user or '',
  1864.             video_service_url=video.user_url or '',
  1865.             site_id=site_pk
  1866.         )
  1867.  
  1868.         if instance.description:
  1869.             soup = BeautifulSoup(video.description)
  1870.             for tag in soup.findAll(
  1871.                 'div', {'class': "miro-community-description"}):
  1872.                 instance.description = tag.renderContents()
  1873.                 break
  1874.             instance.description = sanitize(instance.description,
  1875.                                             extra_filters=['img'])
  1876.  
  1877.         instance._vidscraper_video = video
  1878.  
  1879.         if source_import is not None:
  1880.             source_import.set_video_source(instance)
  1881.  
  1882.         def save_m2m():
  1883.             if authors:
  1884.                 instance.authors = authors
  1885.             if categories:
  1886.                 instance.categories = categories
  1887.             if video.tags:
  1888.                 if settings.FORCE_LOWERCASE_TAGS:
  1889.                     fix = lambda t: t.lower().strip()
  1890.                 else:
  1891.                     fix = lambda t: t.strip()
  1892.                 tags = set(fix(tag) for tag in video.tags if tag.strip())
  1893.                 for tag_name in tags:
  1894.                     tag, created = \
  1895.                         tagging.models.Tag._default_manager.db_manager(
  1896.                         using).get_or_create(name=tag_name)
  1897.                     tagging.models.TaggedItem._default_manager.db_manager(
  1898.                         using).create(
  1899.                         tag=tag, object=instance)
  1900.             if source_import is not None:
  1901.                 source_import.handle_video(instance, video, using)
  1902.             post_video_from_vidscraper.send(sender=cls, instance=instance,
  1903.                                             vidscraper_video=video, using=using)
  1904.  
  1905.         if commit:
  1906.             # Only run this check if they want to immediately commit the
  1907.             # instance; otherwise, the calling code is responsible for ensuring
  1908.             # that the instance makes sense before being saved.
  1909.             if not (instance.embed_code or instance.file_url):
  1910.                 raise InvalidVideo
  1911.             instance.save(using=using)
  1912.             save_m2m()
  1913.         else:
  1914.             instance._state.db = using
  1915.             instance.save_m2m = save_m2m
  1916.         return instance
  1917.  
  1918.     def get_tags(self):
  1919.         if self.pk is None:
  1920.             vidscraper_video = getattr(self, '_vidscraper_video', None)
  1921.             return getattr(vidscraper_video, 'tags', None) or []
  1922.         return self.tags
  1923.  
  1924.     def try_to_get_file_url_data(self):
  1925.         """
  1926.         Do a HEAD request on self.file_url to find information about
  1927.         self.file_url_length and self.file_url_mimetype
  1928.  
  1929.         Note that while this method fills in those attributes, it does *NOT*
  1930.         run self.save() ... so be sure to do so after calling this method!
  1931.         """
  1932.         if not self.file_url:
  1933.             return
  1934.  
  1935.         request = urllib2.Request(utils.quote_unicode_url(self.file_url))
  1936.         request.get_method = lambda: 'HEAD'
  1937.         try:
  1938.             http_file = urllib2.urlopen(request)
  1939.         except Exception:
  1940.             pass
  1941.         else:
  1942.             self.file_url_length = http_file.headers.get('content-length')
  1943.             self.file_url_mimetype = http_file.headers.get('content-type', '')
  1944.             if self.file_url_mimetype in ('application/octet-stream', ''):
  1945.                 # We got a not-useful MIME type; guess!
  1946.                 guess = mimetypes.guess_type(self.file_url)
  1947.                 if guess[0] is not None:
  1948.                     self.file_url_mimetype = guess[0]
  1949.  
  1950.     def save_thumbnail(self):
  1951.         """
  1952.         Automatically run the entire file saving process... provided we have a
  1953.         thumbnail_url, that is.
  1954.         """
  1955.         if not self.thumbnail_url:
  1956.             return
  1957.  
  1958.         try:
  1959.             content_thumb = ContentFile(urllib.urlopen(
  1960.                     utils.quote_unicode_url(self.thumbnail_url)).read())
  1961.         except IOError:
  1962.             raise CannotOpenImageUrl('IOError loading %s' % self.thumbnail_url)
  1963.         except httplib.InvalidURL:
  1964.             # if the URL isn't valid, erase it and move on
  1965.             self.thumbnail_url = ''
  1966.             self.has_thumbnail = False
  1967.             self.save()
  1968.         else:
  1969.             try:
  1970.                 self.save_thumbnail_from_file(content_thumb)
  1971.             except Exception:
  1972.                 logging.exception("Error while getting " + repr(self.thumbnail_url))
  1973.  
  1974.     def submitter(self):
  1975.         """
  1976.         Return the user that submitted this video.  If necessary, use the
  1977.         submitter from the originating feed or savedsearch.
  1978.         """
  1979.         if self.user is not None:
  1980.             return self.user
  1981.         elif self.feed is not None:
  1982.             return self.feed.user
  1983.         elif self.search is not None:
  1984.             return self.search.user
  1985.         else:
  1986.             # XXX warning?
  1987.             return None
  1988.  
  1989.     def when(self):
  1990.         """
  1991.         Simple method for getting the when_published date if the video came
  1992.         from a feed or a search, otherwise the when_approved date.
  1993.         """
  1994.         if SiteLocation.objects.using(self._state.db).get(
  1995.             site=self.site_id).use_original_date and \
  1996.             self.when_published:
  1997.             return self.when_published
  1998.         return self.when_approved or self.when_submitted
  1999.  
  2000.     def source_type(self):
  2001.         return video__source_type(self)
  2002.  
  2003.     def video_service(self):
  2004.         return video__video_service(self)
  2005.  
  2006.     def when_prefix(self):
  2007.         """
  2008.         When videos are bulk imported (from a feed or a search), we list the
  2009.         date as "published", otherwise we show 'posted'.
  2010.         """
  2011.  
  2012.         if self.when_published and \
  2013.                 SiteLocation.objects.get(site=self.site_id).use_original_date:
  2014.             return 'published'
  2015.         else:
  2016.             return 'posted'
  2017.  
  2018.     def voting_enabled(self):
  2019.         if not lsettings.voting_enabled():
  2020.             return False
  2021.         return self.categories.filter(contest_mode__isnull=False).exists()
  2022.  
  2023. def video__source_type(self):
  2024.     '''This is not a method of the Video so that we can can call it from South.'''
  2025.     try:
  2026.         if self.id and self.search:
  2027.             return u'Search: %s' % self.search
  2028.         elif self.id and self.feed:
  2029.             if feed__video_service(self.feed):
  2030.                 return u'User: %s: %s' % (
  2031.                     feed__video_service(self.feed),
  2032.                     self.feed.name)
  2033.             else:
  2034.                 return 'Feed: %s' % self.feed.name
  2035.         elif self.video_service_user:
  2036.             return u'User: %s: %s' % (
  2037.                 video__video_service(self),
  2038.                 self.video_service_user)
  2039.         else:
  2040.             return ''
  2041.     except Feed.DoesNotExist:
  2042.         return ''
  2043.  
  2044. def pre_save_video_set_calculated_source_type(instance, **kwargs):
  2045.     # Always recalculate the source_type field.
  2046.     instance.calculated_source_type = video__source_type(instance)
  2047.     return instance
  2048. models.signals.pre_save.connect(pre_save_video_set_calculated_source_type,
  2049.                                 sender=Video)
  2050.  
  2051. def video__video_service(self):
  2052.     '''This is not a method of Video so we can call it from a South migration.'''
  2053.     if not self.website_url:
  2054.         return
  2055.  
  2056.     url = self.website_url
  2057.     for service, regexp in VIDEO_SERVICE_REGEXES:
  2058.         if re.search(regexp, url, re.I):
  2059.             return service
  2060.  
  2061. class Watch(models.Model):
  2062.     """
  2063.     Record of a video being watched.
  2064.  
  2065.     fields:
  2066.      - video: Video that was watched
  2067.      - timestamp: when watched
  2068.      - user: user that watched it, if any
  2069.      - ip_address: IP address of the user
  2070.     """
  2071.     video = models.ForeignKey(Video)
  2072.     timestamp = models.DateTimeField(auto_now_add=True)
  2073.     user = models.ForeignKey('auth.User', blank=True, null=True)
  2074.     ip_address = models.IPAddressField()
  2075.  
  2076.     @classmethod
  2077.     def add(Class, request, video):
  2078.         """
  2079.         Adds a record of a watched video to the database.  If the request came
  2080.         from localhost, check to see if it was forwarded to (hopefully) get the
  2081.         right IP address.
  2082.         """
  2083.         ip = request.META.get('REMOTE_ADDR', '0.0.0.0')
  2084.         if not ipv4_re.match(ip):
  2085.             ip = '0.0.0.0'
  2086.  
  2087.         if hasattr(request, 'user') and request.user.is_authenticated():
  2088.             user = request.user
  2089.         else:
  2090.             user = None
  2091.  
  2092.         try:
  2093.             Class(video=video, user=user, ip_address=ip).save()
  2094.         except Exception:
  2095.             pass
  2096.  
  2097.  
  2098. class VideoModerator(CommentModerator):
  2099.  
  2100.     def allow(self, comment, video, request):
  2101.         sitelocation = SiteLocation.objects.get(site=video.site)
  2102.         if sitelocation.comments_required_login:
  2103.             return request.user and request.user.is_authenticated()
  2104.         else:
  2105.             return True
  2106.  
  2107.     def email(self, comment, video, request):
  2108.         # we do the import in the function because otherwise there's a circular
  2109.         # dependency
  2110.         from localtv.utils import send_notice
  2111.  
  2112.         sitelocation = SiteLocation.objects.get(site=video.site)
  2113.         t = loader.get_template('comments/comment_notification_email.txt')
  2114.         c = Context({ 'comment': comment,
  2115.                       'content_object': video,
  2116.                       'user_is_admin': True})
  2117.         subject = '[%s] New comment posted on "%s"' % (video.site.name,
  2118.                                                        video)
  2119.         message = t.render(c)
  2120.         send_notice('admin_new_comment', subject, message,
  2121.                     sitelocation=sitelocation)
  2122.  
  2123.         admin_new_comment = notification.NoticeType.objects.get(
  2124.             label="admin_new_comment")
  2125.  
  2126.         if video.user and video.user.email:
  2127.             video_comment = notification.NoticeType.objects.get(
  2128.                 label="video_comment")
  2129.             if notification.should_send(video.user, video_comment, "1") and \
  2130.                not notification.should_send(video.user,
  2131.                                             admin_new_comment, "1"):
  2132.                c = Context({ 'comment': comment,
  2133.                              'content_object': video,
  2134.                              'user_is_admin': False})
  2135.                message = t.render(c)
  2136.                EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL,
  2137.                             [video.user.email]).send(fail_silently=True)
  2138.  
  2139.         comment_post_comment = notification.NoticeType.objects.get(
  2140.             label="comment_post_comment")
  2141.         previous_users = set()
  2142.         for previous_comment in comment.__class__.objects.filter(
  2143.             content_type=comment.content_type,
  2144.             object_pk=video.pk,
  2145.             is_public=True,
  2146.             is_removed=False,
  2147.             submit_date__lte=comment.submit_date,
  2148.             user__email__isnull=False).exclude(
  2149.             user__email='').exclude(pk=comment.pk):
  2150.             if (previous_comment.user not in previous_users and
  2151.                 notification.should_send(previous_comment.user,
  2152.                                          comment_post_comment, "1") and
  2153.                 not notification.should_send(previous_comment.user,
  2154.                                              admin_new_comment, "1")):
  2155.                 previous_users.add(previous_comment.user)
  2156.                 c = Context({ 'comment': comment,
  2157.                               'content_object': video,
  2158.                               'user_is_admin': False})
  2159.                 message = t.render(c)
  2160.                 EmailMessage(subject, message, settings.DEFAULT_FROM_EMAIL,
  2161.                              [previous_comment.user.email]).send(fail_silently=True)
  2162.  
  2163.     def moderate(self, comment, video, request):
  2164.         sitelocation = SiteLocation.objects.get(site=video.site)
  2165.         if sitelocation.screen_all_comments:
  2166.             if not getattr(request, 'user'):
  2167.                 return True
  2168.             else:
  2169.                 return not sitelocation.user_is_admin(request.user)
  2170.         else:
  2171.             return False
  2172.  
  2173. moderator.register(Video, VideoModerator)
  2174.  
  2175. tagging.register(Video)
  2176. tagging.register(OriginalVideo)
  2177.  
  2178. def finished(sender, **kwargs):
  2179.     SiteLocation.objects.clear_cache()
  2180. request_finished.connect(finished)
  2181.  
  2182. def tag_unicode(self):
  2183.     # hack to make sure that Unicode data gets returned for all tags
  2184.     if isinstance(self.name, str):
  2185.         self.name = self.name.decode('utf8')
  2186.     return self.name
  2187.  
  2188. tagging.models.Tag.__unicode__ = tag_unicode
  2189.  
  2190. def send_new_video_email(sender, **kwargs):
  2191.     sitelocation = SiteLocation.objects.get(site=sender.site)
  2192.     if sender.is_active():
  2193.         # don't send the e-mail for videos that are already active
  2194.         return
  2195.     t = loader.get_template('localtv/submit_video/new_video_email.txt')
  2196.     c = Context({'video': sender})
  2197.     message = t.render(c)
  2198.     subject = '[%s] New Video in Review Queue: %s' % (sender.site.name,
  2199.                                                           sender)
  2200.     utils.send_notice('admin_new_submission',
  2201.                      subject, message,
  2202.                      sitelocation=sitelocation)
  2203.  
  2204. submit_finished.connect(send_new_video_email, weak=False)
  2205.  
  2206.  
  2207. def create_email_notices(app, created_models, verbosity, **kwargs):
  2208.     notification.create_notice_type('video_comment',
  2209.                                     'New comment on your video',
  2210.                                     'Someone commented on your video',
  2211.                                     default=2,
  2212.                                     verbosity=verbosity)
  2213.     notification.create_notice_type('comment_post_comment',
  2214.                                     'New comment after your comment',
  2215.                                     'Someone commented on a video after you',
  2216.                                     default=2,
  2217.                                     verbosity=verbosity)
  2218.     notification.create_notice_type('video_approved',
  2219.                                     'Your video was approved',
  2220.                                     'An admin approved your video',
  2221.                                     default=2,
  2222.                                     verbosity=verbosity)
  2223.     notification.create_notice_type('newsletter',
  2224.                                     'Newsletter',
  2225.                                     'Receive an occasional newsletter',
  2226.                                     default=2,
  2227.                                     verbosity=verbosity)
  2228.     notification.create_notice_type('admin_new_comment',
  2229.                                     'New comment',
  2230.                                     'A comment was submitted to the site',
  2231.                                     default=1,
  2232.                                     verbosity=verbosity)
  2233.     notification.create_notice_type('admin_new_submission',
  2234.                                     'New Submission',
  2235.                                     'A new video was submitted',
  2236.                                     default=1,
  2237.                                     verbosity=verbosity)
  2238.     notification.create_notice_type('admin_queue_weekly',
  2239.                                         'Weekly Queue Update',
  2240.                                     'A weekly e-mail of the queue status',
  2241.                                     default=1,
  2242.                                     verbosity=verbosity)
  2243.     notification.create_notice_type('admin_queue_daily',
  2244.                                     'Daily Queue Update',
  2245.                                     'A daily e-mail of the queue status',
  2246.                                     default=1,
  2247.                                     verbosity=verbosity)
  2248.     notification.create_notice_type('admin_video_updated',
  2249.                                     'Video Updated',
  2250.                                     'A video from a service was updated',
  2251.                                     default=1,
  2252.                                     verbosity=verbosity)
  2253.     notification.create_notice_type('admin_new_playlist',
  2254.                                     'Request for Playlist Moderation',
  2255.                                     'A new playlist asked to be public',
  2256.                                     default=2,
  2257.                                     verbosity=verbosity)
  2258.  
  2259. models.signals.post_syncdb.connect(create_email_notices)
  2260.  
  2261. def delete_comments(sender, instance, **kwargs):
  2262.     from django.contrib.comments import get_model
  2263.     get_model().objects.filter(object_pk=instance.pk,
  2264.                                content_type__app_label='localtv',
  2265.                                content_type__model='video'
  2266.                                ).delete()
  2267. models.signals.pre_delete.connect(delete_comments,
  2268.                                   sender=Video)
  2269.  
  2270. ### register pre-save handler for Tiers and payment due dates
  2271. models.signals.pre_save.connect(localtv.tiers.pre_save_set_payment_due_date,
  2272.                                 sender=SiteLocation)
  2273. models.signals.pre_save.connect(localtv.tiers.pre_save_adjust_resource_usage,
  2274.                                 sender=SiteLocation)
  2275. models.signals.post_save.connect(localtv.tiers.post_save_send_queued_mail,
  2276.                                  sender=SiteLocation)
  2277.  
  2278. def create_original_video(sender, instance=None, created=False, **kwargs):
  2279.     if not created:
  2280.         return # don't care about saving
  2281.     if not instance.website_url:
  2282.         # we don't know how to scrape this, so ignore it
  2283.         return
  2284.     new_data = dict(
  2285.         (field.name, getattr(instance, field.name))
  2286.         for field in VideoBase._meta.fields)
  2287.     OriginalVideo.objects.db_manager(instance._state.db).create(
  2288.         video=instance,
  2289.         thumbnail_updated=datetime.datetime.now(),
  2290.         **new_data)
  2291.  
  2292. def save_original_tags(sender, instance, created=False, **kwargs):
  2293.     if not created:
  2294.         # not a new tagged item
  2295.         return
  2296.     if not isinstance(instance.object, Video):
  2297.         # not a video
  2298.         return
  2299.     if (instance.object.when_submitted - datetime.datetime.now() >
  2300.         datetime.timedelta(seconds=10)):
  2301.         return
  2302.     try:
  2303.         original = instance.object.original
  2304.     except OriginalVideo.DoesNotExist:
  2305.         return
  2306.     tagging.models.TaggedItem.objects.db_manager(instance._state.db).create(
  2307.         tag=instance.tag, object=original)
  2308.  
  2309. if lsettings.ENABLE_ORIGINAL_VIDEO:
  2310.     models.signals.post_save.connect(create_original_video,
  2311.                                      sender=Video)
  2312.     models.signals.post_save.connect(save_original_tags,
  2313.                                      sender=tagging.models.TaggedItem)
  2314.  
  2315. ### The "stamp" set of features is a performance optimization for large
  2316. ### deployments of Miro Community.
  2317. ###
  2318. ### The VIDEO_PUBLISHED_STAMP updates the mtime of a file whenever a Video instance
  2319. ### is created or modified. If the stamp file is really old, then you can
  2320. ### safely skip running management commands like update_index.
  2321.  
  2322. def video_published_stamp_signal_listener(sender=None, instance=None, created=False, override_date=None, **kwargs):
  2323.     '''The purpose of the change stamp is to create a file on-disk that
  2324.     indicates when a new instance of the Video model has been published
  2325.     or modified.
  2326.  
  2327.     We actually simply update the stamp on every change or deletion to
  2328.     Video instances. This is slightly too aggressive: If a Video comes in
  2329.     from a feed and is not published, we will update the stamp needlessly.
  2330.  
  2331.     That is okay with me for now.
  2332.     '''
  2333.     update_stamp(name='video-published-stamp', override_date=override_date)
  2334.  
  2335. def site_has_at_least_one_feed_stamp_signal_listener(sender=None, instance=None, created=False, override_date=None, **kwargs):
  2336.     '''The purpose of this stamp is to signify to management scripts that this
  2337.     site has at least one Feed.
  2338.  
  2339.     Therefore, it listens to all .save()s on the Feed model and makes sure
  2340.     that the site-has-at-least-one-feed-stamp file exists.
  2341.  
  2342.     The site-has-at-least-one-feed-stamp stamp is unique in that its modification time
  2343.     is not very important.
  2344.     '''
  2345.     update_stamp(name='site-has-at-least-one-feed-stamp', override_date=override_date)
  2346.  
  2347. def site_has_at_least_one_saved_search_stamp_signal_listener(sender=None, instance=None, created=False, override_date=None, **kwargs):
  2348.     '''The purpose of this stamp is to signify to management scripts that this
  2349.     site has at least one SavedSearch.
  2350.  
  2351.     It is mostly the same as site_has_at_least_one_feed_stamp_signal_listener.'''
  2352.     update_stamp(name='site-has-at-least-saved-search-stamp', override_date=override_date)
  2353.  
  2354. def user_modified_stamp_signal_listener(sender=None, instance=None, created=False, override_date=None, **kwargs):
  2355.     '''The purpose of this stamp is to listen to the User model, and whenever
  2356.     a User changes (perhaps due to a change in the last_login value), we create
  2357.     a file on-disk to say so.
  2358.  
  2359.     Note taht this is a little too aggressive: Any change to a User will cause this stamp
  2360.     to get updated, not just last_login-related changes.
  2361.  
  2362.     That is okay with me for now.
  2363.     '''
  2364.     update_stamp(name='user-modified-stamp', override_date=override_date)
  2365.  
  2366. def video_needs_published_date_stamp_signal_listener(instance=None, **kwargs):
  2367.     if instance.when_published is None:
  2368.         update_stamp(name='video-needs-published-date-stamp')
  2369.  
  2370. def create_or_delete_video_needs_published_date_stamp():
  2371.     '''This function takes a look at all the Videos. If there are any
  2372.     that have a NULL value for date_published, it updates the stamp.
  2373.  
  2374.     If not, it deletes the stamp.'''
  2375.     if Video.objects.filter(when_published__isnull=True):
  2376.         update_stamp(name='video-needs-published-date-stamp')
  2377.     else:
  2378.         update_stamp(name='video-needs-published-date-stamp', delete_stamp=True)
  2379.  
  2380. def update_stamp(name, override_date=None, delete_stamp=False):
  2381.     path = os.path.join(settings.MEDIA_ROOT, '.' + name)
  2382.     if delete_stamp:
  2383.         try:
  2384.             os.unlink(path)
  2385.         except OSError, e:
  2386.             if e.errno == 2: # does not exist
  2387.                 pass
  2388.             else:
  2389.                 raise
  2390.         return
  2391.  
  2392.     try:
  2393.         utils.touch(path, override_date=override_date)
  2394.     except Exception, e:
  2395.         logging.error(e)
  2396.  
  2397. if lsettings.ENABLE_CHANGE_STAMPS:
  2398.     models.signals.post_save.connect(video_published_stamp_signal_listener,
  2399.                                      sender=Video)
  2400.     models.signals.post_delete.connect(video_published_stamp_signal_listener,
  2401.                                        sender=Video)
  2402.     models.signals.post_save.connect(user_modified_stamp_signal_listener,
  2403.                                      sender=User)
  2404.     models.signals.post_delete.connect(user_modified_stamp_signal_listener,
  2405.                                        sender=User)
  2406.     models.signals.post_save.connect(site_has_at_least_one_feed_stamp_signal_listener,
  2407.                                      sender=Feed)
  2408.     models.signals.post_save.connect(site_has_at_least_one_saved_search_stamp_signal_listener,
  2409.                                      sender=SavedSearch)
  2410.     models.signals.post_save.connect(video_needs_published_date_stamp_signal_listener,
  2411.                                      sender=Video)