# HG changeset patch # User durandn # Date 1481816101 -3600 # Node ID d8fcfac848ed79eba2c55c952648413321f3ba45 # Parent 7204cfdde3be88d81764907e250c93d2d1217c06 added/corrected method strings on models, views and signal handlers diff -r 7204cfdde3be -r d8fcfac848ed src/iconolab/models.py --- a/src/iconolab/models.py Mon Dec 12 17:27:33 2016 +0100 +++ b/src/iconolab/models.py Thu Dec 15 16:35:01 2016 +0100 @@ -10,30 +10,18 @@ logger = logging.getLogger(__name__) -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): + """ + Collection objects are the thematic item repositories in Iconolab + + name: the name displayed in the url and also used to identify the collection + verbose_name: the name displayed in the text of the pages + description: the short description of the collection that will be displayed by default in pages + complete_description: the complete description that will be shown with a "view more" button/link + image/height/width: the collection image that will be shown in the collection description + show_image_on_home: if True, the collection will appear by default on the homepage as one of the bigger images + """ 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="") @@ -48,6 +36,9 @@ class Item(models.Model): + """ + Item objects belong to a collection, are linked to a metadata item, and to one or more images + """ collection = models.ForeignKey(Collection, related_name="items") item_guid = models.UUIDField(default=uuid.uuid4, editable=False) @@ -59,6 +50,9 @@ return self.images.order_by("-name").all() class ItemMetadata(models.Model): + """ + Metadata object for the item class. Each field represents what we can import from the provided .csv files + """ item = models.OneToOneField('Item', related_name='metadatas') authors = models.CharField(max_length=255, default="") school = models.CharField(max_length=255, default="") @@ -82,44 +76,11 @@ 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() +class Image(models.Model): + """ + Each image object is linked to one item, users can create annotations on images + """ - @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') @@ -175,11 +136,58 @@ tag_list += [tag_infos['tag_label'] for tag_infos in revision_tags if tag_infos.get('tag_label') is not None] #deal with return tag_list -class AnnotationManager(models.Manager): + +class ImageStats(models.Model): + """ + Stats objects for a given image, keep count of several values to be displayed in image and item pages + """ + 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() - # Call Annotation.objects.create_annotation to initialize a new Annotation with its associated AnnotationStats and initial AnnotationRevision + @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 AnnotationManager(models.Manager): + """ + Manager class for annotation, it handles annotation creation (with initial revision creation, and + has methods to get a list of annotation commented for a given user, and a list of annotations contributed for a + given user + """ @transaction.atomic def create_annotation(self, author, image, title='', description='', fragment='', tags_json='[]'): + """ + Creates a new Annotation with its associated AnnotationStats and initial AnnotationRevision + """ # Create annotation object new_annotation = Annotation( image=image, @@ -299,7 +307,131 @@ return sorted_annotations_list +class Annotation(models.Model): + """ + Annotation objects are created on a given image, each annotation have a list of revisions to keep track of its history, the latest revision is the 'current revision' + that will be displayed by default in most pages. + + Annotation data (title, description, fragment) is thus stored in the revision. + """ + 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) + comments = GenericRelation('IconolabComment', content_type_field='content_type_id', object_id_field='object_pk') + + 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 ] + + def latest_revision_for_user(self, user): + user_revisions = self.revisions.filter(creator=user) + if user_revisions.exists(): + return user_revisions.filter(creator=author).order_by("-created").first() + return None + + @transaction.atomic + def make_new_revision(self, author, title, description, fragment, tags_json): + """ + Called to create a new revision, potentially from a merge + """ + 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 + + @transaction.atomic + def validate_existing_revision(self, revision_to_validate): + """ + Called when we're validating an awaiting revision whose parent is the current revision AS IT WAS CREATED + """ + 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) + + @transaction.atomic + def reject_existing_revision(self, revision_to_reject): + """ + Called when we reject a revision + """ + 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) + + @transaction.atomic + def merge_existing_revision(self, title, description, fragment, tags, revision_to_merge): + """ + Called when we're validating an awaiting revision whose parent isn't the current revision or if the awaiting revision was modified by the annotation author + """ + 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 AnnotationStats(models.Model): + """ + Stats objects for a given annotation, keep count of several values to be displayed in annotation pages + """ 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) @@ -372,8 +504,12 @@ m2m_object.save() self.set_tags_stats() self.save() - + + class MetaCategoriesCountInfo(models.Model): + """ + M2M class to keep a count of a given metacategory on a given annotation. metacategories are linked to comments, themselve linked to an annotation + """ annotation_stats_obj = models.ForeignKey('AnnotationStats', on_delete=models.CASCADE) metacategory = models.ForeignKey('MetaCategory', on_delete=models.CASCADE) count = models.IntegerField(default=1, blank=False, null=False) @@ -382,115 +518,20 @@ return "metacategory_count_for:"+self.metacategory.label+":on:"+str(self.annotation_stats_obj.annotation.annotation_guid) -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) - comments = GenericRelation('IconolabComment', content_type_field='content_type_id', object_id_field='object_pk') - - objects = AnnotationManager() - - def __str__(self): - return str(self.annotation_guid)+":"+self.current_revision.title +class AnnotationRevision(models.Model): + """ + AnnotationRevisions objects are linked to an annotation and store the data of the annotation at a given time + + A revision is always in one out of multiple states: - @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 ] - - def latest_revision_for_user(self, user): - user_revisions = self.revisions.filter(creator=user) - if user_revisions.exists(): - return user_revisions.filter(creator=author).order_by("-created").first() - return 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: the revision has been submitted but must be validated by the original author of the related annotation + - Accepted: the revision has been accepted *as-is* by the author of the related annotation (this happens automatically + if the revision is created by the author of the annotation) + - Rejected: the revision has been rejected by the author of the related annotation + - Studied: the revision has been studied by the author of the related annotation and was either modified or at the very least compared with the current state + through the merge interface, thus creating a new revision merging the current state with the proposal. At this point the proposal is flagged as "studied" to show + that the author of the original annotation has considered it + """ AWAITING = 0 ACCEPTED = 1 REJECTED = 2 @@ -519,6 +560,20 @@ return str(self.revision_guid)+":"+self.title def set_tags(self, tags_json_string): + """ + This method creates tags object and links them to the revision, from a given json that has the following format: + + [ + { + "tag_input": the tag string that has been provided. If it is an http(s?):// pattern, it means the tag is external, else it means it is a custom tag + "accuracy": the accuracy value provided by the user + "relevancy": the relevancy value provided by the user + }, + { + ... + } + ] + """ try: tags_dict = json.loads(tags_json_string) except ValueError: @@ -556,7 +611,25 @@ ) def get_tags_json(self): - + """ + This method returns the json data that will be sent to the js to display tags for the revision. + + The json data returned will be of the following format: + + [ + { + "tag_label": the tag label for display purposes, + "tag_link": the link of the tag, for instance for dbpedia links, + "accuracy": the accuracy value of the tag, + "relevancy": the relevancy value of the tag, + "is_internal": will be True if the tag is 'internal', meaning specific to Iconolab and + not an external tag like a dbpedia reference for instance + }, + { + ... + } + ] + """ 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)) @@ -619,8 +692,47 @@ pass return json.dumps(final_list) + +class Tag(models.Model): + """ + Tag objects that are linked to revisions. + + Each tag is linked to a specific collection, this is important for internal tags + so each collection can build its own vocabulary + """ + 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): + """ + M2M object for managing tag relation to a revision with its associated relevancy and accuracy + """ + 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 IconolabComment(XtdComment): + """ + Comment objects that extends XtdComment model, itself extending the django-contrib-comments model. + + Each comment can have 0 or 1 revision, if it is a comment created alongside a revision + Each comment can have a set of metacategories + """ revision = models.ForeignKey('AnnotationRevision', related_name='creation_comment', null=True, blank=True) metacategories = models.ManyToManyField('MetaCategory', through='MetaCategoryInfo', through_fields=('comment', 'metacategory')) @@ -638,18 +750,30 @@ return Annotation.objects.get(pk=self.object_pk) return None - # Get page for considered comment, with COMMENTS_PER_PAGE_DEFAULT comments per page def get_comment_page(self): + """ + Shortcut function to get page for considered comment, with COMMENTS_PER_PAGE_DEFAULT comments per page, used for notifications links generation + """ 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 + """ + Metacategories are objects that can be linked to a comment to augment it with meaning (depending on the metacategories defined for a given collection) + + Metacategories can trigger notifications when they are linked to a given coment depending on their trigger_notifications property: + + - NONE : Notifies nobody + - CONTRIBUTORS : Notifies contributors (revision owners) on target annotation + - COMMENTERS : Notifies commenters (contributors + comment owners) on target annotation + - COLLECTION_ADMINS : Notifies collection admins + """ + NONE = 0 + CONTRIBUTORS = 1 + COMMENTERS = 2 + COLLECTION_ADMINS = 3 NOTIFIED_USERS = ( (NONE, 'none'), @@ -667,14 +791,21 @@ class MetaCategoryInfo(models.Model): + """ + M2M class linking comments and metacategories + """ 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): - + """ + This class is supposed to represent added resources to a given comment + Not implemented as of v0.0.19 + """ LINK = 0 IMAGE = 1 PDF = 2 @@ -687,8 +818,14 @@ 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): + """ + UserProfile objects are extensions of user model + + As of v0.0.19 they are used to define collection admins. Each user can thus managed 0-N collections. + """ user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE) managed_collections = models.ManyToManyField('Collection', related_name='admins', blank=True) diff -r 7204cfdde3be -r d8fcfac848ed src/iconolab/signals/handlers.py --- a/src/iconolab/signals/handlers.py Mon Dec 12 17:27:33 2016 +0100 +++ b/src/iconolab/signals/handlers.py Thu Dec 15 16:35:01 2016 +0100 @@ -13,6 +13,9 @@ def increment_stats_on_new_revision(sender, instance, **kwargs): + """ + Signal to increment stats on annotation when a revision is created + """ from iconolab.models import AnnotationRevision if sender == AnnotationRevision: if instance.parent_revision: @@ -34,6 +37,9 @@ image.stats.save() def increment_stats_on_new_comment(sender, instance, created, **kwargs): + """ + Signal to increment stats on annotation when a comment is posted + """ from iconolab.models import IconolabComment if created and sender == IconolabComment: model = apps.get_model(instance.content_type.app_label,instance.content_type.model) @@ -45,6 +51,9 @@ annotation.image.stats.save() def increment_stats_on_new_metacategory(sender, instance, created, **kwargs): + """ + Signal to increment stats on annotation when a metacategory is linked to a comment + """ from iconolab.models import MetaCategoryInfo, MetaCategoriesCountInfo if created and sender == MetaCategoryInfo: metacategory = instance.metacategory @@ -59,6 +68,9 @@ logger.debug("NEW METACATEGORY %r on comment %r on annotation %r", metacategory, comment, annotation) def increment_stats_on_accepted_revision(sender, instance, **kwargs): + """ + Signal to increment stats on annotation when a revision is accepted + """ from iconolab.models import AnnotationRevision if sender == AnnotationRevision: annotation = instance.annotation @@ -67,6 +79,9 @@ annotation.stats.save() def increment_stats_on_rejected_revision(sender, instance, **kwargs): + """ + Signal to increment stats on annotation when a comment is rejected + """ from iconolab.models import AnnotationRevision if sender == AnnotationRevision: annotation = instance.annotation @@ -74,6 +89,9 @@ annotation.stats.save() def increment_annotations_count(sender, instance, created, **kwargs): + """ + Signal to increment stats on image when an annotation is created + """ from iconolab.models import Annotation if created and sender == Annotation: image = instance.image diff -r 7204cfdde3be -r d8fcfac848ed src/iconolab/views/comments.py --- a/src/iconolab/views/comments.py Mon Dec 12 17:27:33 2016 +0100 +++ b/src/iconolab/views/comments.py Thu Dec 15 16:35:01 2016 +0100 @@ -13,9 +13,7 @@ @require_POST def post_comment_iconolab(request, next=None, using=None): ''' - Post a comment. - HTTP POST is required. If ``POST['submit'] == 'preview'`` or if there are - errors a preview template, ``comments/preview.html``, will be rendered. + Rewriting of a django_comments method to link Iconolab metacategories on comment posting ''' # Fill out some initial data fields from an authenticated user, if present data = request.POST.copy() diff -r 7204cfdde3be -r d8fcfac848ed src/iconolab/views/objects.py --- a/src/iconolab/views/objects.py Mon Dec 12 17:27:33 2016 +0100 +++ b/src/iconolab/views/objects.py Thu Dec 15 16:35:01 2016 +0100 @@ -19,7 +19,18 @@ logger = logging.getLogger(__name__) class GlobalHomepageView(View): + """ + View for the opening page of Iconolab. + """ def get(self, request, *args, **kwargs): + """ + Template is iconolab/home.html + + Context variables provided to the template are: + collections_primary: list of collections to display as big images + collections_secondary: list of collections to display as small links at the bottom + homepage = True: used to pass checks in the partials/header.html template to adjust the navbar to the homepage + """ context = {} context['collections_primary'] = Collection.objects.filter(show_image_on_home=True).all() context['collections_secondary'] = Collection.objects.filter(show_image_on_home=False).all() @@ -34,6 +45,9 @@ # Class with check_kwargs method to fetch objects from database depending on what level in the app we're currently at class IconolabObjectView(object): + """ + Superclass that defines method used in all object display views. + """ def check_kwargs(self, kwargs): ''' Returns a boolean depending on wether (True) or not (False) the objects were found and a tuple containing the objects, with a select_related/prefetch_related on relevant related objects @@ -70,7 +84,22 @@ def get_pagination_data(self, list_to_paginate, page, perpage, adjacent_pages_count, perpage_range=[5, 10, 25, 100], trailing_qarg=""): """ - Takes a queryset or a list and returns a dict with pagination data + Takes a queryset or a list and returns a dict with pagination data for display purposes + + Dict will be of the format: + { + page: the page to load (integer) + perpage_range: a list of the page links to display (list of integers) + perpage: the item count per page (integer) + perpage_range: a list of the perpage values to display next to the page list (list of integers) + trailing_qarg: optional trailing qarg for the paginations links (used in collection home to remember the state of each list between page loads) (string) + list: the item list to display (list of objects) + show_first: used in template to display links, will be True if 1 is not in page_range + show_last: used in template to display links, will be True if page_count is not in page_range + ellipsis_first: used in template to display links, will be True if page_range starts at 3 or more + ellipsis_last: used in template to display links, will be True if page_range ends at last_page - 2 or less + + } """ pagination_data = {} pagination_data["page"] = page @@ -96,7 +125,40 @@ return pagination_data class CollectionHomepageView(View, ContextMixin, IconolabObjectView): + """ + View that displays a collection and four panels to show relevant paginated lists for collection: + * item lists + * annotations ordered by creation date + * annotations ordered by revisions count + * annotations where a metacategory that notifies contributors was called + """ def get(self, request, *args, **kwargs): + """ + Template is iconolab/collection_home.html + + Url args are: + - collection_name: 'name' attribute of the requested collection + + Queryargs understood by the view are: + - show : panel that will be shown on page load, one of ['items', 'recent', 'revised', 'contributions'], default to "items" + - items_page : item list page to load + - items_perpage : item count per page + - recent_page : recent annotations list page to load + - recent_perpage : recent annotations count per page + - revised_page : most revised annotations list page to load + - revised_perpage : most revised annotations count per page + - contributions_page : annotations with the most contribution calls list page to load + - contributions_perpage : annotations with the most contribution calls count per page for item list + + Context variables provided to the template are: + - collection: the collection object for the requested collection + - collection_name : the collection_name url arg + - items_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the items list + - recent_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the recent annotations list + - revised_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the revised annotations list + - contributions_pagination_data: pagination data dict in the format of the IconolabObjectView.get_pagination_data() method for the contribution calls annotations list + """ + success, result = self.check_kwargs(kwargs) if success: (collection,) = result @@ -230,7 +292,35 @@ class ShowItemView(View, ContextMixin, IconolabObjectView): + """ + View that displays informations on an item with associated metadatas and stats. Also displays images and annotation list for each image. + """ def get(self, request, *args, **kwargs): + """ + Template is iconolab/item_detail.html + + Url args are: + - collection_name : name of the collection + - item_guid: 'item_guid' attribute of the requested item + + Queryargs understood by the view are: + - show: image_guid for the image to show on load + - page: annotation list page on load for displayed image + - perpage: annotation count per page on load for displayed image + + Context variables provided to the template are: + - collection_name : the collection_name url arg + - item_guid: the item_guid url arg + - collection: the collection object for the requested collection + - item: the item object for the requested item + - display_image: the image_guid for the image to display on load + - images: a list of dict for the item images data in the format: + { + 'obj': the image object, + 'annotations': the list of annotations on that image + } + """ + success, result = self.check_kwargs(kwargs) if success: (collection, item) = result @@ -279,6 +369,9 @@ return render(request, 'iconolab/detail_item.html', context); class ShowImageView(View, ContextMixin, IconolabObjectView): + """ + View that only displays an image and the associated annotations + """ def get(self, request, *args, **kwargs): success, result = self.check_kwargs(kwargs) if success: @@ -293,7 +386,9 @@ return render(request, 'iconolab/detail_image.html', context) class CreateAnnotationView(View, ContextMixin, IconolabObjectView): - + """ + View that displays annotation forms and handles annotation creation + """ def get_context_data(self, **kwargs): context = super(CreateAnnotationView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') @@ -348,7 +443,11 @@ return render(request, 'iconolab/change_annotation.html', context) class ShowAnnotationView(View, ContextMixin, IconolabObjectView): - + """ + View that show a given annotation with the corresponding data, links to submit new revisions and the paginated comments thread. + """ + + def get_context_data(self, **kwargs): context = super(ShowAnnotationView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') @@ -357,6 +456,27 @@ return context def get(self, request, *args, **kwargs): + """ + Template is iconolab/detail_annotations.html + + Url args are: + - collection_name: 'name' attribute of the requested collection + - item_guid: 'item_guid' attribute of the requested item + - annotation_guid: 'annotation_guid' attribute of the requested annotation + + Queryargs understood by the view are: + - page: comment thread page on load + - perpage: comment count per page on load + + Context variables provided to the template are: + - collection: the collection object for the requested collection + - image: the image object for the requested image + - annotation: the annotation object for the requested annotation + - tags_data: a json string describing tags for the annotation current revision + - comments: the paginated comments list for the annotation according page and perpage queryargs + - notification_comments_ids: the ids of the comments that are referenced by a notification for the authenticated user; This allows + us to highlight comments that triggered a notification in the page + """ success, result = self.check_kwargs(kwargs) if success: (collection, image, annotation,) = result @@ -403,7 +523,9 @@ class ReadonlyAnnotationView(View, ContextMixin, IconolabObjectView): - + """ + Same view as ShowAnnotationView but without the comments and links to the forms + """ def get_context_data(self, **kwargs): context = super(ReadonlyAnnotationView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') @@ -412,6 +534,9 @@ return context def get(self, request, *args, **kwargs): + """ + Exactly the same as ShowAnnotationView but without all the data around comments + """ success, result = self.check_kwargs(kwargs) if success: (collection, image, annotation,) = result @@ -430,7 +555,9 @@ return render(request, 'iconolab/detail_annotation_readonly.html', context) class EditAnnotationView(View, ContextMixin, IconolabObjectView): - + """ + View that handles displaying the edition form and editing an annotation + """ def get_context_data(self, **kwargs): context = super(EditAnnotationView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') @@ -490,7 +617,9 @@ class ShowRevisionView(View, ContextMixin, IconolabObjectView): - + """ + View that displays a given revision with its associated data and comment + """ def get_context_data(self, **kwargs): context = super(ShowRevisionView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') @@ -500,6 +629,25 @@ return context def get(self, request, *args, **kwargs): + """ + Template is iconolab/detail_annotations.html + + Url args are: + - collection_name: 'name' attribute of the requested collection + - item_guid: 'item_guid' attribute of the requested item + - annotation_guid: 'annotation_guid' attribute of the requested annotation + - revision_guid: 'revision_guid' attribute of the requested revision + + Context variables provided to the template are: + - collection: the collection object for the requested collection + - image: the image object for the requested image + - annotation: the annotation object for the requested annotation + - revision: the revision object for the requested annotation + - tags_data: a json string describing tags for the annotation current revision + - comment: the comment that was posted alongside the revision + - notified_revision: if True, the revision is linked from one or more unread notifications for the + current user, allowing us to highlight it in the template. + """ success, result = self.check_kwargs(kwargs) if success: (collection, image, annotation, revision,) = result @@ -542,7 +690,10 @@ class MergeProposalView(View, ContextMixin, IconolabObjectView): - + """ + View that displays the merge form, used when a user wants to "study" a revision because it was submitted from an older revision than the current revision (thus + the two revisions don't have the same parents and there is a conflict) + """ def get_context_data(self, **kwargs): context = super(MergeProposalView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') diff -r 7204cfdde3be -r d8fcfac848ed src/iconolab/views/userpages.py --- a/src/iconolab/views/userpages.py Mon Dec 12 17:27:33 2016 +0100 +++ b/src/iconolab/views/userpages.py Thu Dec 15 16:35:01 2016 +0100 @@ -16,6 +16,10 @@ logger = logging.getLogger(__name__) class UserHomeView(DetailView): + """ + Homepage for user account, displays latest unread notifications, latest annotations created recap, latest contributions on annotations recap, + latest annotations commented recap, also provides access to admin interface. + """ model = User slug_field = 'id' @@ -48,7 +52,9 @@ return render(request, 'iconolab/user_home.html', context) class UserNotificationsView(View): - + """ + View that displays the notifications the user received + """ def get(self, request, *args, **kwargs): context = {} notifications = Notification.objects.filter(recipient=request.user) @@ -65,6 +71,9 @@ return render(request, 'iconolab/user_notifications.html', context) class UserAnnotationsView(DetailView): + """ + View that displays the full paginated list of annotations created for the considered user + """ model = User slug_field = 'id' @@ -96,6 +105,9 @@ return render(request, 'iconolab/user_annotations.html', context) class UserCommentedView(DetailView): + """ + View that displays the full paginated list of annotations on which the considered user has commented + """ model = User slug_field = 'id' @@ -121,6 +133,9 @@ return render(request, 'iconolab/user_commented.html', context) class UserContributedView(DetailView): + """ + View that displays the full paginated list of annotations on which the considered user has submitted at least one revision + """ model = User slug_field = 'id' @@ -146,6 +161,9 @@ return render(request, 'iconolab/user_contributed.html', context) class UserCollectionAdminView(DetailView): + """ + View that displays the admin panel, allowing collection admin to filter and order annotations on several criterias + """ model = User slug_field = 'id'