class RelatedAnnouncement(models.Model): """ Abstract model to link another archive item (e.g. visuals) to an Announcement. The Model should be subclassed and used as a many-to-many intermediary model:: class RelatedAnnouncementImage( RelatedRAnnouncement ): archive_item = models.ForeignKey( Image, verbose_name=('Image') ) class Meta: verbose_name = _('...') """ announcement = TranslationForeignKey( Announcement, verbose_name=_('Related announcement')) # The announcement to link with another archive item. order = models.PositiveSmallIntegerField(blank=True, null=True) # Used to define an order for the archive items, in case this should not be via the alphabetic order of the id. main_visual = models.BooleanField(default=False) # Defines the primary visual for an announcement - the user is responsible for only selecting one main visual. override_id = models.SlugField(blank=True, null=True, verbose_name=_('Override ID')) # In case you ingest a visual into several announcements, this field can be # used to override the id. hide = models.BooleanField(default=False, verbose_name=_('Hide on kiosk')) # Define if the visual should be hidden if used for e.g. the kiosk def __unicode__(self): return ugettext("Archive Item for Announcement %s" % (unicode(self.announcement.id))) class Meta: abstract = True
class ReleaseImageComparison(RelatedRelease): """ Stock Images related to a release. """ archive_item = TranslationForeignKey( ImageComparison, verbose_name=_('Related Image Comparison'))
class ReleaseStockImage(RelatedRelease): """ Stock Images related to a release. """ archive_item = TranslationForeignKey(Image, verbose_name=_('Related Stock Image'))
class ReleaseVideo(RelatedRelease): """ Images related to a release. """ archive_item = TranslationForeignKey(Video, verbose_name=_('Related Video'))
class ReleaseImage(RelatedRelease): """ Images related to a release. """ archive_item = TranslationForeignKey(Image, verbose_name=_('Related Image')) zoomable = models.BooleanField(default=False, verbose_name=_('Zoomable if main'))
class Release(ArchiveModel, TranslationModel): # # Field definitions # id = models.SlugField( primary_key=True, help_text=_(u'Id of release - e.g. heic0801. The id must be unique.')) old_ids = models.CharField( verbose_name=_("Old Ids"), max_length=50, blank=True, help_text=_( u'For backwards compatibility: Historic ids of this press release.' )) # Type of the release - see ReleaseType model for more information. release_type = TranslationForeignKey(ReleaseType, blank=False, null=False, default=1) # Title of of the press release title = models.CharField( max_length=255, db_index=True, help_text= _(u"Title is shown in browser window. Use a good informative title, since search engines normally display the title on their result pages." )) # Subtitle of of the press release subtitle = models.CharField( max_length=255, blank=True, help_text=_(u"Optional subtitle to be shown just above the headline.")) release_city = models.CharField( max_length=100, blank=True, help_text=_( u"The city of the release - e.g. Paris. Can be left blank.")) headline = models.TextField( blank=True, help_text= _(u'HTML code in lead is not allowed. The lead is further more normally shown in search engine results, making the description an effective way of capturing users attention.' )) description = models.TextField(blank=True) notes = models.TextField(blank=True) more_information = models.TextField(blank=True) links = models.TextField(blank=True, help_text=_(u'Help text')) subject_category = metadatafields_trans.TranslationAVMSubjectCategoryField( ) subject_name = metadatafields_trans.TranslationAVMSubjectNameField() facility = metadatafields_trans.TranslationFacilityManyToManyField() instruments = metadatafields_trans.TranslationInstrumentManyToManyField() publications = metadatafields_trans.TranslationAVMPublicationField() # Field for posting disclaimers on a press release - e.g. if it's retracted. disclaimer = models.TextField( blank=True, help_text= _(u'Disclaimer for press release - usually e.g. retractions of previously issued press releases.' )) related_images = TranslationManyToManyField(Image, through='ReleaseImage', only_sources=True) related_videos = TranslationManyToManyField(Video, through='ReleaseVideo', only_sources=True) related_comparisons = TranslationManyToManyField( ImageComparison, through='ReleaseImageComparison', only_sources=True) stock_images = TranslationManyToManyField(Image, only_sources=True, through='ReleaseStockImage', related_name='stock') meltwater_keywords = models.TextField( blank=True, help_text=_(u'Used to store Meltwater Keywords.'), verbose_name=_(u'Keywords')) # # Kids press releases # kids_title = models.CharField(max_length=255, blank=True) kids_description = models.TextField(blank=True) kids_image = models.ForeignKey( Image, blank=True, null=True, related_name='kids_image_release_set', help_text=_(u'Use this to override the default Release image.')) def get_embargo_login(self): return settings.ARCHIVE_EMBARGO_LOGIN def _set_main_image(self, im): self._main_image_cache = im def _get_main_image(self): try: return self._main_image_cache except AttributeError: for visual in self.releaseimage_set.all(): if visual.main_visual: self._main_visual_cache = visual.archive_item return visual.archive_item main_image = property(_get_main_image) def _set_main_video(self, vid): self._main_video_cache = vid def _get_main_video(self): try: return self._main_video_cache except AttributeError: try: vid = ReleaseVideo.objects.filter( main_visual=True, release=self if self.is_source() else self.source).get() self._main_video_cache = vid.archive_item return vid.archive_item except (IndexError, ObjectDoesNotExist, MultipleObjectsReturned): return None main_video = property(_get_main_video) @staticmethod def store_main_visuals(object_list): """ Fetches the main image and videos for all releases in object list using one query, instead of one query per release. """ relids = [x.id if x.is_source() else x.source_id for x in object_list] ims = ReleaseImage.objects.filter( main_visual=True, release__in=relids).select_related('archive_item') vids = ReleaseVideo.objects.filter( main_visual=True, release__in=relids).select_related('archive_item') release_mapping = {} for im in ims: release_mapping[im.release_id] = im.archive_item for obj in object_list: try: im = release_mapping[obj.id if obj.is_source() else obj. source_id] obj._set_main_image(im) except KeyError: obj._set_main_image(None) release_mapping = {} for vid in vids: release_mapping[vid.release] = vid.archive_item for obj in object_list: try: vid = release_mapping[obj.id if obj.is_source() else obj. source_id] obj._set_main_video(vid) except KeyError: obj._set_main_video(None) def rename(self, new_pk): ''' Extend Archive's rename() to send email notification if original is renamed ''' if self.published and self.is_source() and hasattr( settings, 'RELEASE_RENAME_NOTIFY'): msg_subject = 'Press Release renamed: %s -> %s' % (self.pk, new_pk) msg_body = """https://www.eso.org/public/news/%s/""" % new_pk msg_from = getattr(settings, 'DEFAULT_FROM_EMAIL', '') msg_to = getattr(settings, 'RELEASE_RENAME_NOTIFY', '') if msg_from and msg_to: send_mail(msg_subject, msg_body, msg_from, msg_to, fail_silently=False) return super(Release, self).rename(new_pk) def is_published(self): return datetime.now() >= self.release_date @staticmethod def get_latest_release(count=1): qs = Release.objects.filter( release_date__lte=datetime.now(), published=True).order_by('-release_date')[:count] return qs class Meta: ordering = ['-release_date', '-id'] get_latest_by = "release_date" verbose_name = _('Press Release') verbose_name_plural = _('Press Releases') permissions = [ ("view_only_non_default", "Can view only non default language"), ] def get_absolute_url(self): return translation_reverse( 'releases_detail', args=[str(self.id if self.is_source() else self.source.id)], lang=self.lang) def __unicode__(self): return self.id class Translation: fields = [ 'description', 'disclaimer', 'headline', 'links', 'more_information', 'notes', 'release_city', 'subtitle', 'title', 'kids_title', 'kids_description' ] excludes = [ 'published', 'last_modified', 'created', 'meltwater_keywords' ] set_deep_copy = ['releasetranslationcontact_set'] class Archive: doc = ResourceManager(type=types.DocType) pdf = ResourceManager(type=types.PDFType) text = ResourceManager(type=types.TxtType) sciencepapers = ResourceManager(type=types.DirType, verbose_name=_('Science Papers')) class Meta: root = settings.RELEASE_ARCHIVE_ROOT release_date = True embargo_date = True last_modified = True created = True published = True rename_pk = ('releases_release', 'id') rename_fks = ( ('releases_release', 'source_id'), ('releases_release_facility', 'release_id'), ('releases_release_instruments', 'release_id'), ('releases_release_subject_category', 'release_id'), ('releases_release_subject_name', 'release_id'), ('releases_releasecontact', 'release_id'), ('releases_releaseimage', 'release_id'), ('releases_releasevideo', 'release_id'), ('releases_releasetranslation', 'release_id'), ('media_imagecomparison', 'release_date_owner'), ('media_image', 'release_date_owner'), ('media_video', 'release_date_owner'), ) sort_fields = ['last_modified', 'release_date'] clean_html_fields = [ 'description', 'notes', 'more_information', 'links', 'disclaimer' ]
class ImageComparison( ArchiveModel, TranslationModel ): """ Image comparisons allows to compare two images with a nice JavaScript feature that will reveal the two images when hovering the mouse over the image. """ id = archive_fields.IdField() priority = archive_fields.PriorityField( help_text=_( u'Assessment of the quality of the image comparison (100 highest, 0 lowest). Higher priority images are ranked higher in search results than lower priority images.' ) ) featured = models.BooleanField(default=True) title = metadatafields.AVMTitleField() description = metadatafields.AVMDescriptionField() credit = metadatafields.AVMCreditField( default=DEFAULT_CREDIT_FUNC ) image_before = TranslationForeignKey( Image, related_name='imagecomparison_before_set', null=True ) image_after = TranslationForeignKey( Image, related_name='imagecomparison_after_set', null=True ) def __init__( self, *args, **kwargs ): super( ImageComparison, self ).__init__( *args, **kwargs ) if not self.credit: self.credit = DEFAULT_CREDIT def __unicode__( self ): return self.title def height_ratio( self ): return float( self.image_before.height ) / float( self.image_before.width ) def height_1280( self ): return int( 1280 * self.height_ratio() ) def get_absolute_url(self): return translation_reverse( 'imagecomparisons_detail', args=[str( self.id if self.is_source() else self.source.id )], lang=self.lang ) class Meta: ordering = ( '-priority', '-release_date', ) app_label = 'media' permissions = [ ( "view_only_non_default", "Can view only non default language" ), ] class Translation: fields = [ 'title', 'description', 'credit', ] excludes = [ 'published', 'last_modified', 'created', ] class Archive: original = ImageResourceManager( type=types.OriginalImageType ) large = ImageResourceManager( derived='original', type=types.LargeJpegType ) screen = ImageResourceManager( derived='original', type=types.ScreensizeJpegType ) news = ImageResourceManager( derived='original', type=types.NewsJpegType ) newsmini = ImageResourceManager( derived='original', type=types.NewsMiniJpegType ) newsfeature = ImageResourceManager( derived='original', type=types.NewsFeatureType ) medium = ImageResourceManager( derived='original', type=types.MediumJpegType ) wallpaperthumbs = ImageResourceManager( derived='original', type=types.WallpaperThumbnailType ) potwmedium = ImageResourceManager( derived='original', type=types.POTWMediumThumbnailJpegType ) thumbs = ImageResourceManager( derived='original', type=types.ThumbnailJpegType ) thumb350x = ImageResourceManager( derived='original', type=types.Thumb350xType ) screen640 = ImageResourceManager(derived='original', type=types.Screen640Type) thumb300y = ImageResourceManager(derived='original', type=types.Thumb300yType) thumb350x = ImageResourceManager(derived='original', type=types.Thumb350xType) thumb700x = ImageResourceManager(derived='original', type=types.Thumb700xType) class Meta: root = getattr( settings, 'IMAGECOMPARISON_ARCHIVE_ROOT', '' ) release_date = True embargo_date = True release_date_owner = True last_modified = True created = True published = True rename_pk = ( 'media_imagecomparison', 'id' ) rename_fks = ( ( 'media_imagecomparison', 'source_id' ), ( 'media_image', 'release_date_owner' ), ) sort_fields = ['last_modified', 'release_date', ] clean_html_fields = ['description', 'credit']
class TranslationModel(models.Model): """ Base class for models that needs multilingual capabilities. It adds two fields needed internally, a model manager to properly work with the data in the model and a custom save method. Note, you must ensure that the save method is called even in cases where your model overrides the save method or inherit a save method from another base class. The save method will be default call its base class save method, but this can be prevented as well in such cases. The base class also requires you to specify which fields in your model are to be translated, which are to be inherited and which are not to be translated nor inherited. Example:: class SomeModel( SomeMoreImportantModel, TranslationModel ): ... title = ... template = ... release_date = ... ... class Translation: fields = ['title',] excludes = ['template',] Above example specifies that title is to be translated, template not, and that release_date is inherited from the source. """ # # Fields # lang = LanguageField(verbose_name=_('Language'), max_length=7, default=_get_defaut_lang, db_index=True) source = TranslationForeignKey('self', verbose_name=_('Translation source'), related_name='translations', null=True, blank=True, only_sources=False, limit_choices_to={'source__isnull': True}) translation_ready = models.BooleanField( default=False, help_text= _('When you check this box and save this translation, an automatic notification email will be sent.' )) # # Manager # # Object manager to all source objects # # Note, the default manager *must* be a TranslationSourceManager # or a subclass thereof which limits choices to *only* source # objects. The reason for this is that forward/reverse relationship # managers uses the default manager as base class - thus this ensures # that you can only make relations to source objects, which is normally # what you want (i.e. the ForeignKey/ManyToManyField is an inherited # relation). objects = TranslationSourceManager() translation_objects = TranslationOnlyManager() # # Methods # def save(self, propagate=True, **kwargs): """ Save method that ensures the translations are in sync with their source. Note, if `propagate=False` the super class' save method is not called. It just does the update of the translations. This is useful if you Model class is inheriting from several classes and you want to call this save method, but don't want it to propagate. Note, if you source object has not been saved, it will automatically be saved first, since all translations *must* have a source. """ # Language must be set - using default language if # nothing has been set. if not self.lang: self.lang = settings.LANGUAGE_CODE # Propagate inherited properties and foreign keys # from source to translations. source = self.source if settings.USE_I18N: if source: # Save source if it hasn't already been saved. if not source.pk: source.save() self.source = source # Translation object self._update_from_source() else: # Source self._update_translations() if propagate: super(TranslationModel, self).save(**kwargs) # Source objects get the cache cleared in the post_save, but # for translations we do it here: if source and hasattr(self, 'clear_cache'): self.clear_cache() def _excludes(self, exclude=None): """ Return a list of all fields local to the model that is not to be inherited from the source object. """ if exclude is None: exclude = [] return [ 'source', 'lang', 'translation_ready', str(self._meta.pk.name) ] + self.Translation.fields + self.Translation.excludes + exclude def _update_translations(self): """ Update inherited properties and foreign keys in translations using the source. The update is done in one query and it doesn't hit the save method of the translations. """ # Only sources are allowed to call this method. # and, we must be updating the source, not adding it. try: if not self.source and self.pk: # TODO: Tricky - actually self.pk might not be none for a new object # Get all fields to exclude and setup list of values to update. values = {} all_excludes = self._excludes() # Build list inherited properties and foreign keys to save. for field in self._meta.local_fields: if field.name not in all_excludes: try: values[field.name] = getattr(self, field.name) except ObjectDoesNotExist: pass # Update all translations if there is anything to update. if values: self.translations.all().update(**values) except ObjectDoesNotExist: pass def _update_from_source(self): """ Copy all inherited properties from the source ( old data will be overwritten ). """ if self.source: # Get all fields to excludes. all_excludes = self._excludes() # Get value from source. for field in self._meta.fields: if field.name not in all_excludes: setattr(self, field.name, getattr(self.source, field.name)) def is_translation(self): """ Determine if this instance is a translation or source object. """ return self.source_id is not None def is_source(self): """ Determine if this instance is a translation or source object. """ return self.source_id is None def get_source(self): """ Returns the source object, regardless of whether this instance is a translation or a source object. """ return self if self.is_source() else self.source def validate_unique(self, exclude=None): """ Validate that translation language is *not* identical to source language, and that the source id is not matching the a language id pattern (<id>-<code>). """ # Validate if translation language equals source language if self.is_translation() and self.source.lang == self.lang: raise ValidationError({ 'lang': [ "Translation language must not be identical to translation source language." ] }) # Validate source id prefix matches the language if self.is_source() and self.pk: try: m = LANG_SLUG_RE.match(self.pk) if m and self.lang != m.group(1): raise ValidationError( "Language doesn't match ID prefix '-%s'." % m.group(1)) except TypeError: pass # In case of non char id's, just ignore this error. super(TranslationModel, self).validate_unique(exclude=exclude) def get_missing_translations(self, *args, **kwargs): """ Get a list of missing languages """ data = self.get_translations(*args, **kwargs) def missing_filter(x): try: if (hasattr(settings, 'LANGUAGE_DISABLE_TRANSLATIONS') and x[0] in settings.LANGUAGE_DISABLE_TRANSLATIONS ) or x[0] == settings.LANGUAGE_CODE: return False return not data['translations'][x[0]].translation_ready except KeyError: return True return filter(missing_filter, settings.LANGUAGES) def get_translations(self, filter_kwargs=None, include_self=False): """ Get a dictionary of translations organised by: 1) Countries: List of countries - each element in the list represents a country, and is a list of languages in that country (i.e. its a lists of lists of dictionaries):: [ <country>, ... ] where <country> is a list like [ <language >, ... ] where language is a dictionary like with the following keys: object, country, name, lang 2) Main languages: """ if filter_kwargs is None: filter_kwargs = {'published': True, 'translation_ready': True} data = { 'languages': [], 'countries': [], 'translations': {}, } def _ctx_for_translation(t, lang, ctry, name, code): return { 'object': t, 'country': ctry, 'name': name, 'lang': lang, 'full_code': code, } # Retrieve all translations for this object (including the source language) if self.is_translation(): translations = list( self.source.translations.filter( **filter_kwargs)) + [self.source] else: translations = list( self.translations.filter(**filter_kwargs)) + [self] # Make a search dictionary (include source language, and exclude it's own language). searchmap = dict([(t.lang, t) for t in translations if include_self or t.lang != self.lang]) data['translations'] = searchmap # Make list of main languages for lang, ctry, name, code in MAIN_LANGUAGES_LIST: if code in searchmap: data['languages'].append( _ctx_for_translation(searchmap[code], lang, ctry, name, code)) # Make list of countries for ctry, langs in COUNTRIES_LIST: tmp = [] for lang, name, code in langs: if code in searchmap: # Mon Aug 12 10:56:07 CEST 2013 - Mathias Andre # We auto-generate english translations for e.g. USA, IRELAND, # but we don't want these linked on the websites, so we skip them # from the translation list if code.startswith('en') and code != 'en': continue tmp.append( _ctx_for_translation(searchmap[code], lang, ctry, name, code)) if tmp: data['countries'].append(tmp) return data def generate_duplicate_id(self, lang): """ Returns a new id including the given language code """ if self.source: return '%s-%s' % (self.source.pk, lang) else: return '%s-%s' % (self.pk, lang) @classmethod def send_notification_mail(cls, sender, instance, raw=False, **kwargs): """ Send a notification email to a specified address once a translation is ready. """ if raw: return if instance.translation_ready: from_email = settings.DEFAULT_FROM_EMAIL # For http://www.djangoplicity.org/bugs/view.php?id=19639 # We only send notifications for 'main' languages if they # are defined in the settings: notification_langs = getattr(settings, 'LANGUAGE_ENABLE_NOTIFICATIONS', []) if not notification_langs: return # Check whether the 'translation ready' flag has just been set: try: old = sender.objects.get(pk=instance.pk) if old.translation_ready: return except sender.DoesNotExist: pass # Disable signals to prevent loops models.signals.pre_save.disconnect(cls.send_notification_mail, sender=cls) for lang in notification_langs: if not instance.lang.startswith(lang): continue for code, email in settings.LANGUAGES_CONTACTS.iteritems(): if code.startswith(lang) and code != instance.lang: # Check if a translation already exists: try: cls.objects.get(source=instance.source, lang=code) # If there is a translation we continue continue except cls.DoesNotExist: kwargs = { 'pk': instance.generate_duplicate_id(code), 'source': instance.source, 'lang': code, 'created': datetime.today(), 'translation_ready': True, 'published': instance.published, } translation = cls(**kwargs) copy_family_translation_fields( instance, translation) translation.save() if not email: continue try: from django.contrib.sites.models import Site domain = Site.objects.get_current().domain except ImportError: domain = '' subject = "%s translation %s ready" % ( unicode(translation._meta.verbose_name), translation.pk) html_body = render_to_string( 'archives/ready_email.html', { 'type': translation._meta.verbose_name, 'source': instance, 'translation': translation, 'site_url': domain }) text_body = strip_tags(html_body) # Send if subject and html_body and from_email: try: msg = EmailMultiAlternatives( subject, text_body, from_email, [email]) msg.attach_alternative( html_body, 'text/html') msg.send() except Exception: # Just ignore error if SMTP server is down. pass # Re-enable signals models.signals.pre_save.connect(cls.send_notification_mail, sender=cls) def update_family_translations(self, **kwargs): ''' Generate/update translations of similar languages e.g.: de-ch -> de, de-at ''' if not settings.USE_I18N: return main_language = self.lang if '-' in main_language: main_language = main_language.split('-')[0] # Get list of languages in the same family languages = [] for code, dummy_name in settings.LANGUAGES: if code.startswith(main_language) and code != self.lang: languages.append(code) # Search for corresponding Proxy class if self.is_source(): module = inspect.getmodule(self) proxymodel_name = '%sProxy' % self.__class__.__name__ if not hasattr(module, proxymodel_name): return proxymodel = getattr(module, proxymodel_name) source = self else: proxymodel = self.__class__ source = self.source # Disable signals to prevent loops models.signals.pre_save.disconnect( self.__class__.send_notification_mail, sender=self) for lang in languages: try: translation = proxymodel.objects.get(source=self.pk, lang=lang) except ObjectDoesNotExist: kwargs = { 'pk': self.generate_duplicate_id(lang), 'source': source, 'lang': lang, 'created': datetime.today(), 'translation_ready': True, 'published': self.published, } translation = proxymodel(**kwargs) copy_family_translation_fields(self, translation) translation.published = self.published # Carry over 'published' status translation.save() # Re-enable signals models.signals.pre_save.connect(self.__class__.send_notification_mail, sender=self) class Translation: fields = [] # Translated fields - should be overwritten in subclass. excludes = [ ] # Non-inherited fields - should be overwritten in subclass. set_deep_copy = [ ] # 1-to-N relationships that will be cloned for same-family languages # If set to False we don't show the original version if there is no # translation and the original is in a different language than the site # default (e.g.: Don't show 'es-cl' announcements on 'en' announcements list non_default_languages_in_fallback = True class Meta: abstract = True unique_together = ('lang', 'source')
class AnnouncementImageComparison(RelatedAnnouncement): ''' Image Comparisons related to an announcement ''' archive_item = TranslationForeignKey( ImageComparison, verbose_name=_('Related Image Comparison'))
class AnnouncementVideo(RelatedAnnouncement): """ Videos related to an announcement. """ archive_item = TranslationForeignKey(Video, verbose_name=_('Related Video'))
class AnnouncementImage(RelatedAnnouncement): """ Images related to an announcement. """ archive_item = TranslationForeignKey(Image, verbose_name=_('Related Image'))
class Announcement(ArchiveModel, TranslationModel): """ Similar to press releases but with fewer fields. """ id = archive_fields.IdField() title = archive_fields.TitleField() subtitle = models.CharField( max_length=255, blank=True, help_text=_(u"Optional subtitle to be shown just above the headline.")) description = archive_fields.DescriptionField() contacts = models.TextField(blank=True, help_text=_(u'Contacts')) links = models.TextField(blank=True, help_text=_(u'Links')) featured = models.BooleanField(default=True) related_images = TranslationManyToManyField(Image, through='AnnouncementImage') related_videos = TranslationManyToManyField(Video, through='AnnouncementVideo') related_comparisons = TranslationManyToManyField( ImageComparison, through='AnnouncementImageComparison', only_sources=True) announcement_type = TranslationForeignKey(AnnouncementType, blank=True, null=True, default=None) def get_embargo_login(self): return settings.ARCHIVE_EMBARGO_LOGIN def _set_main_visual(self, vis): self._main_visual_cache = vis def _get_main_visual(self): try: return self._main_visual_cache except AttributeError: pass for visual in self.announcementimage_set.all(): if visual.main_visual: self._main_visual_cache = visual.archive_item return visual.archive_item for visual in self.announcementimagecomparison_set.all(): if visual.main_visual: self._main_visual_cache = visual.archive_item return visual.archive_item for visual in self.announcementvideo_set.all(): if visual.main_visual: self._main_visual_cache = visual.archive_item return visual.archive_item return None main_visual = property(_get_main_visual) def rename(self, new_pk): ''' Extend Archive's rename() to send email notification if original is renamed ''' if self.published and self.is_source() and hasattr( settings, 'ANNOUNCEMENT_RENAME_NOTIFY'): msg_subject = 'Announcement renamed: %s -> %s' % (self.pk, new_pk) msg_body = """https://www.eso.org/public/announcements/%s/""" % new_pk msg_from = getattr(settings, 'DEFAULT_FROM_EMAIL', '') msg_to = getattr(settings, 'ANNOUNCEMENT_RENAME_NOTIFY', '') if msg_from and msg_to: send_mail(msg_subject, msg_body, msg_from, msg_to, fail_silently=False) return super(Announcement, self).rename(new_pk) @staticmethod def store_main_visuals(object_list): ''' Fetches the main image for all announcements in object list using one query, instead of one query per announcement. ''' annids = [x.id if x.is_source() else x.source_id for x in object_list] images = (AnnouncementImage.objects.filter( main_visual=True, announcement__in=annids).select_related('archive_item')) videos = (AnnouncementVideo.objects.filter( main_visual=True, announcement__in=annids).select_related('archive_item')) comparisons = (AnnouncementImageComparison.objects.filter( main_visual=True, announcement__in=annids).select_related('archive_item')) announcement_mapping = {} for video in videos: announcement_mapping[video.announcement_id] = video.archive_item for image in images: announcement_mapping[image.announcement_id] = image.archive_item for comparison in comparisons: announcement_mapping[ comparison.announcement_id] = comparison.archive_item for obj in object_list: try: visual = announcement_mapping[obj.id if obj.is_source( ) else obj.source_id] obj._set_main_visual(visual) except KeyError: obj._set_main_visual(None) @staticmethod def get_latest_announcement(len=1, only_featured=False): qs = Announcement.objects.filter(release_date__lte=datetime.now(), published=True) if only_featured: qs = qs.filter(featured=True) return qs.order_by('-release_date')[:len] class Meta: ordering = ['-release_date', '-id'] get_latest_by = "release_date" permissions = [ ("view_only_non_default", "Can view only non default language"), ] def get_absolute_url(self): return translation_reverse( 'announcements_detail', args=[str(self.id if self.is_source() else self.source.id)], lang=self.lang) def __unicode__(self): return u"%s: %s" % (self.id, self.title) class Translation: fields = ['title', 'subtitle', 'description', 'contacts', 'links'] excludes = [ 'published', 'last_modified', 'created', ] non_default_languages_in_fallback = False # Don't show non-en anns. if no en translation is available class Archive: pdf = ResourceManager(type=types.PDFType) pdfsm = ResourceManager(type=types.PDFType) sciencepaper = ResourceManager(type=types.PDFType, verbose_name=_('Science paper')) class Meta: root = ANNOUNCEMENTS_ROOT release_date = True embargo_date = True last_modified = True created = True published = True rename_pk = ('announcements_announcement', 'id') rename_fks = ( ('announcements_announcement', 'source_id'), ('announcements_announcementimage', 'announcement_id'), ('announcements_announcementvideo', 'announcement_id'), ('media_image', 'release_date_owner'), ('media_video', 'release_date_owner'), ('media_imagecomparison', 'release_date_owner'), ) sort_fields = ['last_modified', 'release_date'] clean_html_fields = ['description', 'links', 'contacts']
class VideoSubtitle( ArchiveModel, models.Model ): """ Model for storing subtitles for videos. """ id = archive_fields.IdField() video = TranslationForeignKey( Video ) lang = models.CharField( verbose_name=_( 'Language' ), max_length=7, choices=SUBTITLE_LANGUAGES, default=settings.LANGUAGE_CODE, db_index=True ) def save( self, **kwargs ): """ Override save() to always set id as video_id + lang # TODO: check video renaming """ old_id = self.id self.id = self.video.id + self.lang if old_id != self.id: # delete old try: old = VideoSubtitle.objects.get( id=old_id ) old.delete() except VideoSubtitle.DoesNotExist: pass # In some cases where 'created' was not set upon creation the save # will fail so we manually set it if not self.created: # pylint: disable=E0203 self.created = datetime.datetime.today() super( VideoSubtitle, self ).save( **kwargs ) transaction.on_commit( lambda: update_youtube_caption.delay(self.pk) ) def __unicode__( self ): return dict( SUBTITLE_LANGUAGES ).get( self.lang, self.lang ) def update_youtube_caption(self, logger=None): ''' Upload the given subtitle to YouTube ''' if logger is None: logger = logging.getLogger(__name__) if not youtube_configured or not self.video or not self.video.youtube_video_id: logger.warning('No video or YouTube ID for subtitle "%s"', self.pk) return response = youtube_videos_list( id=self.video.youtube_video_id, part='snippet' ) if not response['items']: raise YouTubeUnknownID('Unkown YouTube ID: "%s"' % self.youtube_video_id) snippet = dict( videoId=self.video.youtube_video_id, language=self.lang, name='', isDraft=False ) # Check if caption already exists response = youtube_captions_list( part='snippet', video_id=self.video.youtube_video_id ) # We use wait_for_resource in case the NFS is slow to synchronise # between the different servers srt = wait_for_resource(self, 'srt') for item in response['items']: if item['snippet']['language'] == self.lang: # Subtitle already exist, we update it youtube_captions_update( part='snippet', body=dict( id=item['id'], snippet=snippet ), media_body=srt.file.name ) break else: # New subtitle, we insert it youtube_captions_insert( part='snippet', body=dict( snippet=snippet ), media_body=srt.file.name ) class Meta: verbose_name = _(u'Video Subtitle') app_label = 'media' class Archive: srt = ResourceManager( type=types.SubtitleType, verbose_name=_( u"Subtitle (srt)" ) ) class Meta: root = settings.VIDEOS_ARCHIVE_ROOT release_date = False embargo_date = False last_modified = True created = True published = True