src/iconolab/models.py
author durandn
Tue, 11 Oct 2016 12:06:42 +0200
changeset 219 ce1e12435c44
parent 216 1e1d5f30e2f9
child 225 6b304e2c6af4
permissions -rw-r--r--
cleaned up __str__ methods

from django.db import models, transaction
from django.conf import settings
from django.contrib.auth.models import User
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django_comments_xtd.models import XtdComment
from django.utils.text import slugify
import iconolab.signals.handlers as iconolab_signals
import uuid, json, re, requests, urllib


class Tag(models.Model):
    label = models.CharField(max_length=255, blank=True, null=True)
    label_slug = models.SlugField(blank=True, null=True)
    link = models.URLField(unique=True)
    description = models.CharField(max_length=255, blank=True, null=True)
    collection = models.ForeignKey('Collection', blank=True, null=True)
    
    def is_internal(self):
        return self.link.startswith(settings.INTERNAL_TAGS_URL)

    def __str__(self):
        return self.label_slug+":"+self.label

class TaggingInfo(models.Model):
    revision = models.ForeignKey('AnnotationRevision', on_delete=models.CASCADE)
    tag = models.ForeignKey('Tag', on_delete=models.CASCADE)
    accuracy = models.IntegerField()
    relevancy = models.IntegerField()
    
    def __str__(self):
        return str(str(self.tag.label_slug)+":to:"+str(self.revision.revision_guid))


class Collection(models.Model):
    name = models.SlugField(max_length=50, unique=True)
    verbose_name = models.CharField(max_length=50, null=True, blank=True)
    description = models.TextField(null=True, blank=True, default="")
    complete_description = models.TextField(null=True, blank=True, default="")
    image = models.ImageField(upload_to='uploads/', height_field='height', width_field='width', null=True, blank=True)
    height = models.IntegerField(null=True, blank=True)
    width = models.IntegerField(null=True, blank=True)

    def __str__(self):
        return self.name


class Item(models.Model):
    collection = models.ForeignKey(Collection, related_name="items")
    item_guid = models.UUIDField(default=uuid.uuid4, editable=False)
    
    def __str__(self):
        return str(self.item_guid)+":from:"+self.collection.name
    
class ItemMetadata(models.Model):
    item = models.OneToOneField('Item', related_name='metadatas')
    authors = models.CharField(max_length=255, default="")
    school = models.CharField(max_length=255, default="")
    designation = models.CharField(max_length=255, default="")
    datation = models.CharField(max_length=255, default="")
    technics = models.CharField(max_length=255, default="")
    measurements = models.CharField(max_length=255, default="")
    create_or_usage_location = models.CharField(max_length=255, default="")
    discovery_context = models.CharField(max_length=255, default="")
    conservation_location = models.CharField(max_length=255, default="")
    photo_credits = models.CharField(max_length=255, default="")
    inventory_number = models.CharField(max_length=255, default="")
    joconde_ref = models.CharField(max_length=255, default="")
    
    @property
    def get_joconde_url(self):
        return settings.JOCONDE_NOTICE_BASE_URL+self.joconde_ref.rjust(11, '0')
    
    def __str__(self):
        return "metadatas:for:"+str(self.item.item_guid)


class ImageStats(models.Model):
    image = models.OneToOneField('Image', related_name='stats', blank=False, null=False)
    views_count = models.IntegerField(blank=True, null=True, default=0)
    annotations_count = models.IntegerField(blank=True, null=True, default=0)
    submitted_revisions_count = models.IntegerField(blank=True, null=True, default=0)
    comments_count = models.IntegerField(blank=True, null=True, default=0)
    folders_inclusion_count = models.IntegerField(blank=True, null=True, default=0)
    tag_count = models.IntegerField(blank=True, null=True, default=0)
    
    def __str__(self):
        return "stats:for:"+str(self.image.image_guid)
    
    def set_tags_stats(self):
        self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
    
    @transaction.atomic
    def update_stats(self):
        self.annotations_count = 0
        self.submitted_revisions_count = 0
        self.comments_count = 0
        image_annotations = Annotation.objects.filter(image=self.image)
        # views_count - Can't do much about views count
        # annotations_count
        self.annotations_count = image_annotations.count()
        # submitted_revisions_count & comment_count
        for annotation in image_annotations.all():
            annotation_revisions = annotation.revisions
            self.submitted_revisions_count += annotation_revisions.count()
            
            self.comments_count += XtdComment.objects.for_app_models("iconolab.annotation").filter(
                object_pk = annotation.pk,
            ).count()
        # tag_count
        self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().count()
        self.save()


class Image(models.Model):
    image_guid = models.UUIDField(default=uuid.uuid4, editable=False)
    name = models.CharField(max_length=200)
    media = models.ImageField(upload_to='uploads/', height_field='height', width_field='width')
    item = models.ForeignKey('Item', related_name='images', null=True, blank=True)
    height = models.IntegerField(null=False, blank=False)
    width = models.IntegerField(null=False, blank=False)
    created = models.DateTimeField(auto_now_add=True, null=True)
    
    def __str__(self):
        return str(self.image_guid)+":"+self.name
    
    @property
    def collection(self):
        return self.item.collection.name

    @property
    def title(self):
        return self.item.metadatas.designation

    @property
    def authors(self):
        return self.item.metadatas.authors

    @property
    def school(self):
        return self.item.metadatas.school
        
    @property
    def designation(self):
        return self.item.metadatas.designation

    @property
    def datation(self):
        return self.item.metadatas.datation
    
    @property
    def technics(self):
        return self.item.metadatas.technics

    @property
    def measurements(self):
        return self.item.metadatas.measurements

    @property
    def tag_labels(self):
        tag_list = []
        for annotation in self.annotations.all():
            revision_tags = json.loads(annotation.current_revision.get_tags_json())
            tag_list += [tag_infos['tag_label'] for tag_infos in revision_tags if tag_infos.get('tag_label') is not None] #deal with
            print("tag_list")
            print(tag_list)
        return tag_list

class AnnotationManager(models.Manager):
    
    # Call Annotation.objects.create_annotation to initialize a new Annotation with its associated AnnotationStats and initial AnnotationRevision
    @transaction.atomic
    def create_annotation(self, author, image, title='', description='', fragment='', tags_json='[]'):
        # Create annotation object
        new_annotation = Annotation(
            image=image, 
            author=author
        )
        new_annotation.save()
        
        # Create initial revision
        initial_revision = AnnotationRevision(
            annotation=new_annotation, 
            author=author,
            title=title,
            description=description,
            fragment=fragment,
            state=AnnotationRevision.ACCEPTED
        )
        initial_revision.save()
        initial_revision.set_tags(tags_json)
        
        # Create stats object
        new_annotation_stats = AnnotationStats(annotation=new_annotation)
        new_annotation_stats.save()
        new_annotation_stats.set_tags_stats()
        
        # Link everything to parent
        new_annotation.current_revision = initial_revision
        new_annotation.stats = new_annotation_stats
        new_annotation.save()
        iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=initial_revision)
        return new_annotation


class AnnotationStats(models.Model):
    annotation = models.OneToOneField('Annotation', related_name='stats', blank=False, null=False)
    submitted_revisions_count = models.IntegerField(blank=True, null=True, default=1)
    awaiting_revisions_count = models.IntegerField(blank=True, null=True, default=0)
    accepted_revisions_count = models.IntegerField(blank=True, null=True, default=1)
    contributors_count = models.IntegerField(blank=True, null=True, default=1)
    views_count = models.IntegerField(blank=True, null=True, default=0)
    comments_count = models.IntegerField(blank=True, null=True, default=0)
    tag_count = models.IntegerField(blank=True, null=True, default=0)
    
    def __str__(self):
        return "stats:for:"+str(self.annotation.annotation_guid)
    
    @property
    def contributors(self):
        user_ids_list = self.annotation.revisions.filter(state__in=[AnnotationRevision.ACCEPTED, AnnotationRevision.STUDIED]).values_list("author__id", flat=True)
        return User.objects.filter(id__in=user_ids_list).distinct()
    
    @property
    def commenters(self):
        user_ids_list = IconolabComment.objects.filter(content_type__app_label="iconolab", content_type__model="annotation", object_pk=self.annotation.id).values_list("user__id", flat=True)
        return User.objects.filter(id__in=user_ids_list).distinct()
    
    def set_tags_stats(self):
        self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation = self.annotation).distinct().count()
    
    @transaction.atomic
    def update_stats(self):
        # views_count - Can't do much about views count
        # submitted_revisions_count 
        annotation_revisions = self.annotation.revisions
        self.submitted_revisions_count = annotation_revisions.count()
        # aawaiting_revisions_count
        self.awaiting_revisions_count = annotation_revisions.filter(state=AnnotationRevision.AWAITING).count()
        # accepted_revisions_count
        self.accepted_revisions_count = annotation_revisions.filter(state=AnnotationRevision.ACCEPTED).count() + annotation_revisions.filter(state=AnnotationRevision.STUDIED).count()
        # comment_count
        self.comments_count = XtdComment.objects.for_app_models("iconolab.annotation").filter(
            object_pk = self.annotation.pk,
        ).count()
        # contributors_count
        self.contributors_count = len(self.contributors)
        # tag_count
        self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation = self.annotation).distinct().count()
        
        self.save()


class Annotation(models.Model):
    annotation_guid = models.UUIDField(default=uuid.uuid4, editable=False)
    image = models.ForeignKey('Image', related_name='annotations', on_delete=models.CASCADE)
    source_revision = models.ForeignKey('AnnotationRevision', related_name='source_related_annotation', blank=True, null=True)
    current_revision = models.OneToOneField('AnnotationRevision', related_name='current_for_annotation', blank=True, null=True)
    author = models.ForeignKey(User, null=True)
    created = models.DateTimeField(auto_now_add=True, null=True)
    
    objects = AnnotationManager()
    
    def __str__(self):
        return str(self.annotation_guid)+":"+self.current_revision.title
        
    @property
    def awaiting_revisions_count(self):
        return self.revisions.filter(state=AnnotationRevision.AWAITING).distinct().count()
    
    @property
    def accepted_revisions_count(self):
        return self.revisions.filter(state=AnnotationRevision.ACCEPTED).distinct().count()
    
    @property
    def rejected_revisions_count(self):
        return self.revisions.filter(state=AnnotationRevision.REJECTED).distinct().count()
    
    @property
    def studied_revisions_count(self):
        return self.revisions.filter(state=AnnotationRevision.STUDIED).distinct().count()
      
    @property
    def total_revisions_count(self):
        return self.revisions.distinct().count()
   
    @property
    def collection(self):
        return self.image.collection

    @property
    def tag_labels(self):
        current_revision_tags = json.loads(self.current_revision.get_tags_json())
        return [tag_infos['tag_label'] for tag_infos in current_revision_tags if tag_infos.get('tag_label') is not None ]
    
    
    # Call to create a new revision, possibly from a merge
    @transaction.atomic
    def make_new_revision(self, author, title, description, fragment, tags_json):
        if author == self.author:
            # We're creating an automatically accepted revision
            new_revision_state = AnnotationRevision.ACCEPTED
        else:
            # Revision will require validation
            new_revision_state = AnnotationRevision.AWAITING
        new_revision = AnnotationRevision(
            annotation = self,
            parent_revision=self.current_revision,
            title=title,
            description=description,
            author=author,
            fragment=fragment,
            state=new_revision_state
        )
        new_revision.save()
        new_revision.set_tags(tags_json)
        if new_revision.state == AnnotationRevision.ACCEPTED:
            self.current_revision = new_revision
            self.save()
        iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=new_revision)
        return new_revision
    
    # Call when we're validating an awaiting revision whose parent is the current revision AS IT WAS CREATED
    @transaction.atomic
    def validate_existing_revision(self, revision_to_validate):
        if revision_to_validate.parent_revision == self.current_revision and revision_to_validate.state == AnnotationRevision.AWAITING:
            self.current_revision = revision_to_validate
            revision_to_validate.state = AnnotationRevision.ACCEPTED
            revision_to_validate.save()
            self.save()
            iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_validate)
    
    # Call to reject a 
    @transaction.atomic
    def reject_existing_revision(self, revision_to_reject):
        if revision_to_reject.state == AnnotationRevision.AWAITING:
            revision_to_reject.state = AnnotationRevision.REJECTED
            revision_to_reject.save()
            iconolab_signals.revision_rejected.send(sender=AnnotationRevision, instance=revision_to_reject)
    
    # Call when we're validating an awaiting revision whose parent isn't the current revision OR IF IT WAS CHANGED BY THE ANNOTATION AUTHOR
    @transaction.atomic
    def merge_existing_revision(self, title, description, fragment, tags, revision_to_merge):
        merged_revision = self.make_new_revision(author=self.author, title=title, description=description, fragment=fragment, tags_json=tags)
        merged_revision.merge_parent_revision = revision_to_merge
        merged_revision.save()
        revision_to_merge.state = AnnotationRevision.STUDIED
        revision_to_merge.save()
        iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_merge)
        self.current_revision=merged_revision
        self.save()
        return merged_revision


class AnnotationRevision(models.Model):
    
    AWAITING = 0
    ACCEPTED = 1
    REJECTED = 2
    STUDIED = 3
    
    REVISION_STATES = (
        (AWAITING, 'awaiting'),
        (ACCEPTED, 'accepted'),
        (REJECTED, 'rejected'),
        (STUDIED, 'studied'),
    )
    
    revision_guid = models.UUIDField(default=uuid.uuid4)
    annotation = models.ForeignKey('Annotation', related_name='revisions', null=False, blank=False)
    parent_revision = models.ForeignKey('AnnotationRevision', related_name='child_revisions', blank=True, null=True)
    merge_parent_revision = models.ForeignKey('AnnotationRevision', related_name='child_revisions_merge', blank=True, null=True)
    author = models.ForeignKey(User, null=True)
    title = models.CharField(max_length=255)
    description = models.TextField(null=True)
    fragment = models.TextField()
    tags = models.ManyToManyField('Tag', through='TaggingInfo', through_fields=('revision', 'tag'))
    state = models.IntegerField(choices=REVISION_STATES, default=AWAITING)
    created = models.DateTimeField(auto_now_add=True, null=True)

    def __str__(self):
        return str(self.revision_guid)+":"+self.title

    def set_tags(self, tags_json_string):
        try:
            tags_dict = json.loads(tags_json_string)
        except ValueError:
            pass
        for tag_data in tags_dict:
            tag_string = tag_data.get("tag_input")
            tag_accuracy = tag_data.get("accuracy", 0)
            tag_relevancy = tag_data.get("relevancy", 0)
            
            if tag_string.startswith("http://") or tag_string.startswith("https://"): #check if url
                if Tag.objects.filter(link=tag_string).exists(): #check if tag already exists
                    tag_obj = Tag.objects.get(link=tag_string)
                else:
                    tag_obj = Tag.objects.create(
                        link = tag_string,
                    )
            else:
                new_tag_link = settings.BASE_URL+'/'+slugify(tag_string)
                if Tag.objects.filter(link=new_tag_link).exists():
                    # Somehow we received a label for an existing tag
                    tag_obj = Tag.objects.get(link=new_tag_link)
                else:
                    tag_obj = Tag.objects.create(
                        label = tag_string,
                        label_slug = slugify(tag_string),
                        description = "",
                        link = settings.INTERNAL_TAGS_URL+'/'+slugify(tag_string),
                        collection = self.annotation.image.item.collection
                    )
            tag_info = TaggingInfo.objects.create(
                tag=tag_obj, 
                revision=self,
                accuracy = tag_accuracy,
                relevancy = tag_relevancy
            )
        
    def get_tags_json(self):
        
        def fetch_from_dbpedia(uri, lang, source):
            sparql_template = 'select distinct * where { <<%uri%>> rdfs:label ?l FILTER( langMatches( lang(?l), "<%lang%>" ) ) }' 
            sparql_query = re.sub("<%uri%>", uri, re.sub("<%lang%>", lang, sparql_template))
            sparql_query_url = source+'sparql'
            try:
                dbpedia_resp = requests.get(
                    sparql_query_url, 
                    params={
                            "query": sparql_query,
                            "format": "json"
                    }
                )
            except:
                # dbpedia is down, will be handled with database label
                pass
            try:
                results = json.loads(dbpedia_resp.text).get("results", {})
            except:
                # if error with json, results is empty
                results = {}
            variable_bindings = results.get("bindings", None)
            label_data = {}
            if variable_bindings:
                label_data = variable_bindings.pop()
            return label_data.get("l", {"value": False}).get("value")
        
        final_list = []
        for tagging_info in self.tagginginfo_set.select_related("tag").all():
            if tagging_info.tag.is_internal():
                final_list.append({
                    "tag_label": tagging_info.tag.label,
                    "tag_link": tagging_info.tag.link,
                    "accuracy": tagging_info.accuracy,
                    "relevancy": tagging_info.relevancy,
                    "is_internal": tagging_info.tag.is_internal()
                })
            else:
                tag_link = tagging_info.tag.link
                #import label from external
                externaL_repos_fetch_dict = {
                    "http://dbpedia.org/": fetch_from_dbpedia,
                    "http://fr.dbpedia.org/": fetch_from_dbpedia
                }
                try:
                    (source, fetch_label) = next(item for item in externaL_repos_fetch_dict.items() if tag_link.startswith(item[0]))
                    tag_label = fetch_label(tag_link, "fr", source)
                    if not tag_label: # Error happened and we got False as a fetch return
                        tag_label = tagging_info.tag.label 
                    else:
                        tagging_info.tag.label = tag_label
                        tagging_info.tag.save()
                    final_list.append({
                        "tag_label": tag_label,
                        "tag_link": tag_link,
                        "accuracy": tagging_info.accuracy,
                        "relevancy": tagging_info.relevancy,
                        "is_internal": tagging_info.tag.is_internal()
                    })
                except StopIteration:
                    pass
        return json.dumps(final_list) 

    
class IconolabComment(XtdComment):
    revision = models.ForeignKey('AnnotationRevision', related_name='creation_comment', null=True, blank=True)
    metacategories = models.ManyToManyField('MetaCategory', through='MetaCategoryInfo', through_fields=('comment', 'metacategory'))
    
    objects = XtdComment.objects
    
    def __str__(self):
        return str(self.id)
    
    class Meta:
        ordering = ["thread_id", "id"]
    
    # Get page for considered comment, with COMMENTS_PER_PAGE_DEFAULT comments per page
    def get_comment_page(self):
        return (IconolabComment.objects.for_app_models("iconolab.annotation").filter(
            object_pk=self.object_pk,
        ).filter(thread_id__gte=self.thread_id).filter(order__lte=self.order).count() +1) // settings.COMMENTS_PER_PAGE_DEFAULT + 1

    
class MetaCategory(models.Model):
    NONE = 0 # Notifies nobody
    CONTRIBUTORS = 1 # Notifies contributors (revision owners) on target annotation
    COMMENTERS = 2 # Notifies commenters (contributors + comment owners) on target annotation
    COLLECTION_ADMINS = 3 # Notifies collection admins
    
    NOTIFIED_USERS = (
        (NONE, 'none'),
        (CONTRIBUTORS, 'contributors'),
        (COMMENTERS, 'commenters'),
        (COLLECTION_ADMINS, 'collection admins'),
    )
    
    collection = models.ForeignKey(Collection, related_name="metacategories")
    label = models.CharField(max_length=255)
    triggers_notifications = models.IntegerField(choices=NOTIFIED_USERS, default=NONE)
    
    def __str__(self):
        return self.label
    

class MetaCategoryInfo(models.Model):
    comment = models.ForeignKey('IconolabComment', on_delete=models.CASCADE)
    metacategory = models.ForeignKey('MetaCategory', on_delete=models.CASCADE)
    
    def __str__(self):
        return "metacategory:"+self.metacategory.label+":on:"+self.comment.id

class CommentAttachement(models.Model):
     
    LINK = 0
    IMAGE = 1
    PDF = 2
    COMMENT_CHOICES = (
        (LINK, 'link'),
        (IMAGE, 'image'),
        (PDF, 'pdf')
    )
    
    comment = models.ForeignKey('IconolabComment', related_name='attachments', on_delete=models.CASCADE)
    attachment_type = models.IntegerField(choices=COMMENT_CHOICES, default=0)
    data = models.TextField(blank=False)
    
class UserProfile(models.Model):
    user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
    administers_collection = models.ForeignKey('Collection', related_name='collection', blank=True, null=True)
    
    def __str__(self):
        return "profile:"+self.user.username