class BaseImportData(models.Model): # dictionary object representing a row in csv row_data = DictField(_('Row Data')) # the original row number in the uploaded csv file row_num = models.IntegerField(_('Row #')) # action_taken can be 'insert', 'update' or 'mixed' action_taken = models.CharField(_('Action Taken'), max_length=20, null=True) error = models.CharField(_('Error'), max_length=500, default='') class Meta: abstract = True
class Image(OrderingBaseModel, ImageModel, TendenciBaseModel): """ A photo with its details """ SAFETY_LEVEL = ( (1, _('Safe')), (2, _('Not Safe')), ) EXIF_KEYS = ('DateTimeOriginal', 'DateTime', 'ApertureValue', 'GPSInfo', 'Make', 'Model', 'Software', 'ExifImageWidth', 'ExifImageHeight', 'XResolution', 'YResolution', 'ResolutionUnit', 'SubjectLocation', 'Orientation') guid = models.CharField(max_length=40, editable=False) title = models.CharField(_('title'), max_length=200) title_slug = models.SlugField(_('slug')) caption = models.TextField(_('caption'), blank=True) date_added = models.DateTimeField(_('date added'), auto_now_add=True, editable=False) is_public = models.BooleanField( _('public'), default=True, help_text=_( 'Public photographs will be displayed in the default views.')) member = models.ForeignKey(User, related_name="added_photos", blank=True, null=True, on_delete=models.SET_NULL) safetylevel = models.IntegerField(_('safety level'), choices=SAFETY_LEVEL, default=3) photoset = models.ManyToManyField(PhotoSet, blank=True, verbose_name=_('photo set')) tags = TagField(blank=True, help_text=_("Comma delimited (eg. mickey, donald, goofy)")) license = models.ForeignKey('License', null=True, blank=True, on_delete=models.SET_NULL) group = models.ForeignKey(Group, null=True, default=None, on_delete=models.SET_NULL, blank=True) exif_data = DictField(_('exif'), null=True) photographer = models.CharField(_('Photographer'), blank=True, null=True, max_length=100) # html-meta tags meta = models.OneToOneField(MetaTags, blank=True, null=True, on_delete=models.SET_NULL) perms = GenericRelation(ObjectPermission, object_id_field="object_id", content_type_field="content_type") def get_meta(self, name): """ This method is standard across all models that are related to the Meta model. Used to generate dynamic methods coupled to this instance. """ return PhotoMeta().get_meta(self, name) class Meta: # permissions = (("view_image", "Can view image"),) app_label = 'photos' def save(self, *args, **kwargs): initial_save = not self.id if not self.id: self.guid = str(uuid.uuid4()) if not self.group: self.group_id = get_default_group() super(Image, self).save(*args, **kwargs) # clear the cache #caching.instance_cache_clear(self, self.pk) #caching.cache_clear(PHOTOS_KEYWORDS_CACHE, key=self.pk) # re-add instance to the cache #caching.instance_cache_add(self, self.pk) if not self.is_public_photo() or not self.is_public_photoset(): if hasattr( settings, 'USE_S3_STORAGE') and settings.USE_S3_STORAGE and hasattr( self.image, 'file'): set_s3_file_permission(self.image.file, public=False) cache_set = cache.get("photos_cache_set.%s" % self.pk) if cache_set is not None: # TODO remove cached images cache.delete_many(cache.get("photos_cache_set.%s" % self.pk)) cache.delete("photos_cache_set.%s" % self.pk) if initial_save: try: exif_exists = self.get_exif_data() if exif_exists: self.save() except AttributeError: pass def delete(self, *args, **kwargs): """ Delete image-file and all resized versions """ super(Image, self).delete(*args, **kwargs) if self.image: cache_path = self.cache_path() # delete cached [resized] versions try: filename_list = default_storage.listdir(cache_path)[1] for filename in filename_list: try: default_storage.delete( os.path.join(cache_path, filename)) except OSError: pass except OSError: pass # delete actual image; do not save() self.instance self.image.delete(save=False) def get_absolute_url(self): try: photo_set = self.photoset.all()[0] except IndexError: return reverse("photo", args=[self.pk]) return reverse('photo', args=[self.pk, photo_set.pk]) def get_exif_data(self): """ Extract EXIF data from image and store in the field exif_data. """ try: img = PILImage.open(default_storage.open(self.image.name)) exif = img._getexif() except (AttributeError, IOError): return False if self.exif_data is None: self.exif_data = {} if exif: for tag, value in exif.items(): key = PILTAGS.get(tag, tag) if key in self.EXIF_KEYS: self.exif_data[key] = value self.exif_data['lat'], self.exif_data['lng'] = self.get_lat_lng( self.exif_data.get('GPSInfo')) self.exif_data['location'] = self.get_location_via_latlng( self.exif_data['lat'], self.exif_data['lng']) return True def get_lat_lng(self, gps_info): """ Calculate the latitude and longitude from gps_info. """ lat, lng = None, None if isinstance(gps_info, dict): try: lat = [float(x) / float(y) for x, y in gps_info[2]] latref = gps_info[1] lng = [float(x) / float(y) for x, y in gps_info[4]] lngref = gps_info[3] except (KeyError, ZeroDivisionError): return None, None lat = lat[0] + lat[1] / 60 + lat[2] / 3600 lng = lng[0] + lng[1] / 60 + lng[2] / 3600 if latref == 'S': lat = -lat if lngref == 'W': lng = -lng return lat, lng def get_location_via_latlng(self, lat, lng): """ Get location via lat and lng. """ if lat and lng: url = 'https://maps.googleapis.com/maps/api/geocode/json?latlng=%s,%s&sensor=false' % ( lat, lng) r = requests.get(url) if r.status_code == 200: data = simplejson.loads(r.content) for result in data.get('results'): types = result.get('types') if types and types[0] == 'postal_code': return result.get('formatted_address') return None def meta_keywords(self): return '' # from base.utils import generate_meta_keywords # keywords = caching.cache_get(PHOTOS_KEYWORDS_CACHE, key=self.pk) # if not keywords: # value = self.title + ' ' + self.caption + ' ' + self.tags # keywords = generate_meta_keywords(value) # caching.cache_add(PHOTOS_KEYWORDS_CACHE, keywords, key=self.pk) # return keywords def check_perm(self, user, permission, *args, **kwargs): """ has_perms(self, user, permission, *args, **kwargs) returns boolean """ if user == self.member or user.has_perm(permission): return True return False def get_next(self, set=None): # decide which set to pull from if set: images = Image.objects.filter(photoset=set, position__gt=self.position) else: images = Image.objects.filter(position__gt=self.position) images = images.values_list("position", flat=True) images = images.order_by('-position') if set and images: [image] = Image.objects.filter(photoset=set, position=min(images))[:1] or [None] return image return None def get_prev(self, set=None): # decide which set to pull from if set: images = Image.objects.filter(photoset=set, position__lt=self.position) else: images = Image.objects.filter(position__lt=self.position) images = images.values_list("position", flat=True) images = images.order_by('-position') if set: try: return Image.objects.get(photoset=set, position=max(images)) except (ValueError, Image.MultipleObjectsReturned): return None return None def get_first(self, set=None): # decide which set to pull from if set: images = Image.objects.filter(photoset=set) else: return None images = images.values_list("position", flat=True) images = images.order_by('-position') if set: try: return Image.objects.get(photoset=set, position=min(images)) except (ValueError, Image.MultipleObjectsReturned): return None return None def get_position(self, set=None): # decide which set to pull from if set: images = Image.objects.filter(photoset=set, position__lte=self.position) else: images = Image.objects.filter(position__lte=self.position) position = images.count() return position def is_public_photo(self): return all([ self.is_public, self.allow_anonymous_view, self.status, self.status_detail.lower() == "active" ]) def is_public_photoset(self): for photo_set in self.photoset.all(): if not all([ self.allow_anonymous_view, self.status, self.status_detail.lower() == "active" ]): return False return True def get_license(self): return self.license or self.default_license() def default_license(self): return License.objects.get(id=1) def file_exists(self): return default_storage.exists(str(self.image)) def default_thumbnail(self): return static('images/default-photo-album-cover.jpg') def get_file_from_remote_storage(self): return BytesIO(default_storage.open(self.image.file.name).read()) def image_dimensions(self): try: if hasattr(settings, 'USE_S3_STORAGE') and settings.USE_S3_STORAGE: with PILImage.open(self.get_file_from_remote_storage()) as im: return im.size else: with PILImage.open(self.image.path) as im: return im.size except Exception: return (0, 0) objects = PhotoManager() def __str__(self): return self.title