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 self.tag.label_slug+":to:"+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)
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_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())
print("tagss")
print(current_revision_tags)
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 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