src/iconolab/models.py
author durandn
Mon, 11 Jul 2016 14:51:49 +0200
changeset 64 4d1e369e85d4
parent 62 8702ab13783e
child 65 625ed1ba472f
permissions -rw-r--r--
Added a verbose name field for collection objects, home view now gets a list of all collections

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 uuid, json, re, requests, urllib


class Tag(models.Model):
    label = 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)


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()


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.CharField(max_length=255)

    def __str__(self):
        return self.name


class Item(models.Model):
    collection = models.ForeignKey(Collection, related_name="items")

    
class ItemMetadata(models.Model):
    item = models.OneToOneField('Item', related_name='metadatas')
    joconde_ref = models.CharField(max_length=20, null=False, blank=False, unique=True)
    domain = models.CharField(max_length=255)
    title = models.CharField(max_length=255)
    description = models.CharField(max_length=255)


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)

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 self.name

# # Folders
# class Folder(models.Model):
#     label = models.CharField(max_length=255)
#     owner = models.ForeignKey(User)
#     images = models.ManyToManyField(Image)
# 
#     def __str__(self):
#         return label

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)
        
        new_annotation.current_revision = initial_revision
        new_annotation.save()
        
        # Create stats object
        new_annotation_stats = AnnotationStats(annotation=new_annotation)
        new_annotation_stats.save()
        new_annotation.stats = new_annotation_stats
        new_annotation.save()
        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)
    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 contributors(self):
        contributors = []
        for revision in self.annotation.revisions.filter(state__in=[AnnotationRevision.ACCEPTED, AnnotationRevision.STUDIED]):
            if revision.author not in contributors:
                contributors.append(revision.author)
        print(contributors)
        return contributors


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 update_stats(self):
        pass
    
    # 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()
        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()
    
    # 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()
        
    # 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=tags)
        merged_revision.merge_parent_revision = revision_to_merge
        merged_revision.save()
        revision_to_merge.state = AnnotationRevision.STUDIED
        revision_to_merge.save()
        self.current_revision=merged_revision
        self.save()


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 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,
                        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'
            dbpedia_resp = requests.get(
                sparql_query_url, 
                params={
                        "query": sparql_query,
                        "format": "json"
                }
            )
            results = json.loads(dbpedia_resp.text).get("results", {})
            variable_bindings = results.get("bindings")
            if variable_bindings:
                label_json = variable_bindings.pop()
            else:
                label_json = {"l": {"value": "ERROR_LABEL"}}
            return label_json.get("l").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
                })
            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)
                    final_list.append({
                        "tag_label": tag_label,
                        "tag_link": tag_link,
                        "accuracy": tagging_info.accuracy,
                        "relevancy": tagging_info.relevancy
                    })
                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'))

    
class MetaCategory(models.Model):
    collection = models.ForeignKey(Collection)
    label = models.CharField(max_length=255)

    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)


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)