# HG changeset patch # User ymh # Date 1484745090 -3600 # Node ID 9273f1f2c82730f1daaff3d07cfe0e8d52c0d4d4 # Parent 959cbaad20765f3132ada505fb0784026a84f888 clean README text, remove unnecesary settings, refresh base requirements diff -r 959cbaad2076 -r 9273f1f2c827 .hgignore --- a/.hgignore Fri Dec 16 13:24:55 2016 +0100 +++ b/.hgignore Wed Jan 18 14:11:30 2017 +0100 @@ -31,3 +31,5 @@ ^src/MANIFEST ^src/iconolab.egg-info ^src/dist/ +^src/.vscode +^src/requirements/custom.txt$ diff -r 959cbaad2076 -r 9273f1f2c827 src/README.md --- a/src/README.md Fri Dec 16 13:24:55 2016 +0100 +++ b/src/README.md Wed Jan 18 14:11:30 2017 +0100 @@ -1,32 +1,3 @@ -# How to start? - -1. Make sure PIP is installed then install Django and others dependencies with - -``` -pip install -r requirements.txt - -``` - -2. Move to src/iconolab/static/js/iconolab-bundle to install js dependencies. -Make sure your have installed nodejs then run the command bellow - -``` -npm install - -``` -3. To recreate the bundle file that lives in dist/ - -``` -npm run build - -``` - -4. To add a new js module, you can add it to the js/components folder and then run - -``` -npm start -``` - ## ICONOLAB ## ### 1. Configuration and setup @@ -37,23 +8,23 @@ - Create a virtualenv for the project (using virtualenvwrapper is a good idea if possible). Python version is 3.5.1 - Run - pip install -r requirements.txt + pip install -r requirements.txt #### node.js - Make sure nodejs is installed - cd into iconolab/src/iconolab/static/iconolab/js and run - - npm install + + npm install - To recreate the bundle file that lives in dist/ - npm build + npm build - To add a new js module, you can add it to the js/components folder and then run - - npm start + + npm start #### Django project setup @@ -67,14 +38,14 @@ - Run python manage.py createsuperuser - + to create an admin user #### Elasticsearch Some objects in Iconolab are indexed and searched using ElasticSearch. You need to configure Haystack (see dev.py.tmpl, HAYSTACK_CONNECTIONS) and run: - python manage.py rebuild_index + python manage.py rebuild_index ### 2. Development server @@ -83,17 +54,17 @@ - cd into the iconolab/src folder and run - python manage.py runserver - + python manage.py runserver + By default, the app is accessible through http://127.0.0.1:8000/home #### 2.2 Javascript development - cd into the iconolab/src_js/iconolab-bundle folder and run - npm install - npm run start - + npm install + npm run start + This will serve the iconolab.js file in the iconolab/src/iconolab/static/js and update it on changes you make in the js code in src_js so you can edit the code and debug it live in your browser @@ -109,20 +80,22 @@ The following django manage.py command is used to import collection data and images: - python manage.py importimages <:export-csv-path> --delimiter <:delimiter> --encoding <:encoding> --collection-json <:collection_fixture_FILENAME> (OR --collection-id <:collection_id> if collection already exists in db) --metacategories-json <:metacategories_json_FILENAME> +``` +python manage.py importimages <:export-csv-path> --delimiter <:delimiter> --encoding <:encoding> --collection-json <:collection_fixture_FILENAME> (OR --collection-id <:collection_id> if collection already exists in db) --metacategories-json <:metacategories_json_FILENAME> +``` Options: - --delimiter: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator) - --encoding: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859 - --collection-json: the json file to create the collection from - --collection-id: the id of the collection to import into, it must already exist - --metacategories-json: the json file to create metacategories on the collection we're importing into - --jpeg-quality: the jpeg quality: default to the setting IMG_JPG_DEFAULT_QUALITY - --no-jpg-conversion: set to True so the command will not convert the images to jpg. Useful for pre-converted jpeg and especially when importing large image banks - --img-filename-identifier: the column from which the command will try to find images in the folder: use keys from the setting IMPORT_FIELDS_DICT. Default is "INV". - --filename-regexp-prefix: allows you to customize the way the command try to find images by specifying a regexp pattern to match *before* the identifier provided in img-filename-identifier. Defaults to .* - --filename-regexp-suffix: allows you to customize the way the command try to find images by specifying a regexp pattern to match *after* the identifier provided in img-filename-identifier. Defaults to [\.\-_].* - +- ```--delimiter```: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator) +- ```--encoding```: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859 +- ```--collection-json```: the json file to create the collection from +- ```--collection-id```: the id of the collection to import into, it must already exist +- ```--metacategories-json```: the json file to create metacategories on the collection we're importing into +- ```--jpeg-quality```: the jpeg quality: default to the setting IMG_JPG_DEFAULT_QUALITY +- ```--no-jpg-conversion```: set to True so the command will not convert the images to jpg. Useful for pre-converted jpeg and especially when importing large image banks +- ```--img-filename-identifier```: the column from which the command will try to find images in the folder: use keys from the setting IMPORT_FIELDS_DICT. Default is "INV". +- ```--filename-regexp-prefix```: allows you to customize the way the command try to find images by specifying a regexp pattern to match *before* the identifier provided in img-filename-identifier. Defaults to .* +- ```--filename-regexp-suffix```: allows you to customize the way the command try to find images by specifying a regexp pattern to match *after* the identifier provided in img-filename-identifier. Defaults to [\.\-_].* + Notes: * The export csv path will be used to find everything else (images and fixtures files). * If the csv file is not encoded in utf-8, you MUST provide --encoding so the csv file can be read @@ -134,36 +107,38 @@ Another management command allows for editing data using only a .csv file. The command will go through the csv and update the metadatas for every objects it finds in the database with the csv row content. - python manage.py updatecollection --collection-id=<:id> --delimiter=<:delimiter> --encoding=<:encoding> - +``` +python manage.py updatecollection --collection-id=<:id> --delimiter=<:delimiter> --encoding=<:encoding> +``` + Options: - --delimiter: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator) - --encoding: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859 - --collection-id: the id of the collection to import into, it must already exist +- ```--delimiter```: the delimiter for the csv file. For special ascii characters add a # before the code. Supported special chars are 9 (tab), 29 (Group separator), 30 (Record separator), 31 (Unit separator) +- ```--encoding```: the encoding provided if the csv is not in utf-8. Exemple: 8859 for ISO-8859 +- ```--collection-id```: the id of the collection to import into, it must already exist - + ## TO-DOs * Add a stat object for items with the following stats (at least?) - - contributors_count - - annotations_count - - comments_count - - contribution_calls_count - + - contributors_count + - annotations_count + - comments_count + - contribution_calls_count + * Annotation validation: there is an example handler in signals/handler.py for validation an annotation * Admin interface: add a way to extract data for one or more annotation as .csv * Django admin: - - Search annotation/item/image by guid - - More complete infos per row for object lists - + - Search annotation/item/image by guid + - More complete infos per row for object lists + * History view: to be able to visualize the history of a given annotation - + * Zoomed images: - - Zoomed images on annotation pages and item list pages (thumbnail sizes must be sorted out to be pre-generated for the list pages) - + - Zoomed images on annotation pages and item list pages (thumbnail sizes must be sorted out to be pre-generated for the list pages) + * Fragment editor: - - Identify usability issues - - Rectangle selection as default - - Add a way to define two or more shapes for one fragment \ No newline at end of file + - Identify usability issues + - Rectangle selection as default + - Add a way to define two or more shapes for one fragment \ No newline at end of file diff -r 959cbaad2076 -r 9273f1f2c827 src/iconolab/apps.py --- a/src/iconolab/apps.py Fri Dec 16 13:24:55 2016 +0100 +++ b/src/iconolab/apps.py Wed Jan 18 14:11:30 2017 +0100 @@ -1,9 +1,9 @@ from django.apps import AppConfig class IconolabApp(AppConfig): - name = 'iconolab' - verbose_name = 'Iconolab' + name = 'iconolab' + verbose_name = 'Iconolab' - def ready(self): - import iconolab.signals.handlers - import iconolab.templatetags.iconolab_tags \ No newline at end of file + def ready(self): + import iconolab.signals.handlers + import iconolab.templatetags.iconolab_tags diff -r 959cbaad2076 -r 9273f1f2c827 src/iconolab/models.py --- a/src/iconolab/models.py Fri Dec 16 13:24:55 2016 +0100 +++ b/src/iconolab/models.py Wed Jan 18 14:11:30 2017 +0100 @@ -6,7 +6,12 @@ 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, logging +import uuid +import json +import re +import requests +import urllib +import logging logger = logging.getLogger(__name__) @@ -14,19 +19,24 @@ 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 + 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="") 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) + 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) show_image_on_home = models.BooleanField(default=False) @@ -37,21 +47,23 @@ class Item(models.Model): """ - Item objects belong to a collection, are linked to a metadata item, and to one or more images + 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) - + def __str__(self): - return str(self.item_guid)+":from:"+self.collection.name - + return str(self.item_guid) + ":from:" + self.collection.name + @property def images_sorted_by_name(self): 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 + 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="") @@ -67,35 +79,37 @@ 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') - + return settings.JOCONDE_NOTICE_BASE_URL + self.joconde_ref.rjust(11, '0') + def __str__(self): - return "metadatas:for:"+str(self.item.item_guid) + return "metadatas:for:" + str(self.item.item_guid) class Image(models.Model): """ Each image object is linked to one item, users can create annotations on images """ - + 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) + 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 - + return str(self.image_guid) + ":" + self.name + @property def wh_ratio(self): return self.width / self.height - + @property def collection(self): return self.item.collection.name @@ -111,7 +125,7 @@ @property def school(self): return self.item.metadatas.school - + @property def designation(self): return self.item.metadatas.designation @@ -119,7 +133,7 @@ @property def datation(self): return self.item.metadatas.datation - + @property def technics(self): return self.item.metadatas.technics @@ -132,8 +146,10 @@ 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 + 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 return tag_list @@ -141,20 +157,24 @@ """ 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) + 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) + 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) + 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) - + 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() - + self.tag_count = Tag.objects.filter( + tagginginfo__revision__annotation__image=self.image).distinct().count() + @transaction.atomic def update_stats(self): self.annotations_count = 0 @@ -168,18 +188,19 @@ 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, + object_pk=annotation.pk, ).count() # tag_count - self.tag_count = Tag.objects.filter(tagginginfo__revision__annotation__image = self.image).distinct().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 + 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 """ @@ -190,14 +211,14 @@ """ # Create annotation object new_annotation = Annotation( - image=image, + image=image, author=author ) new_annotation.save() - + # Create initial revision initial_revision = AnnotationRevision( - annotation=new_annotation, + annotation=new_annotation, author=author, title=title, description=description, @@ -206,33 +227,36 @@ ) initial_revision.save() initial_revision.set_tags(tags_json) - + # Create stats object new_annotation_stats = AnnotationStats(annotation=new_annotation) new_annotation_stats.set_tags_stats() new_annotation_stats.save() - + # 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) + iconolab_signals.revision_created.send( + sender=AnnotationRevision, instance=initial_revision) return new_annotation @transaction.atomic def get_annotations_contributed_for_user(self, user): """ user is the user whom we want to get the contributed annotations - - Returns the list of all the annotations on which the user submitted a revision but did not create the annotation + + Returns the list of all the annotations on which the user submitted + a revision but did not create the annotation List of dict in the format: - + { "annotation_obj": annotation object, "revisions_count": revisions count for user "awaiting_count": awaiting revisions for user on this annotation "accepted_count": accepted revisions for user - "latest_submitted_revision": date of the latest submitted revision from user on annotation + "latest_submitted_revision": date of the latest submitted revision + from user on annotation } """ latest_revision_on_annotations = [] @@ -243,13 +267,15 @@ 'image__item', 'image__item__collection').distinct() for annotation in user_contributed_annotations.all(): - latest_revision_on_annotations.append(annotation.revisions.filter(author=user).latest(field_name="created")) + latest_revision_on_annotations.append( + annotation.revisions.filter(author=user).latest(field_name="created")) contributed_list = [] if latest_revision_on_annotations: - latest_revision_on_annotations.sort(key=lambda item:item.created, reverse=True) - contributed_list= [ + latest_revision_on_annotations.sort( + key=lambda item: item.created, reverse=True) + contributed_list = [ { - "annotation_obj": revision.annotation, + "annotation_obj": revision.annotation, "revisions_count": revision.annotation.revisions.filter(author=user).count(), "awaiting_count": revision.annotation.revisions.filter(author=user, state=AnnotationRevision.AWAITING).count(), "accepted_count": revision.annotation.revisions.filter(author=user, state=AnnotationRevision.ACCEPTED).count(), @@ -259,33 +285,36 @@ ] logger.debug(contributed_list) return contributed_list - + @transaction.atomic def get_annotations_commented_for_user(self, user, ignore_revisions_comments=True): """ user is the user for which we want to get the commented annotations ignore_revisions_comment allows to filter comments that are associated with a revision - - + + Returns a list of all annotations on which a given user commented with user-comments-related data List of dict in the format: - + { "annotation_obj": annotation object, "comment_count": comment count for user "latest_comment_date": date of the latest comment from user on annotation } """ - user_comments = IconolabComment.objects.filter(user=user, content_type__app_label='iconolab', content_type__model='annotation').order_by('-submit_date') + user_comments = IconolabComment.objects.filter( + user=user, content_type__app_label='iconolab', content_type__model='annotation').order_by('-submit_date') if ignore_revisions_comments: logger.debug(user_comments.count()) user_comments = user_comments.filter(revision__isnull=True) logger.debug(user_comments.count()) - all_user_comments_data = [(comment.object_pk, comment.submit_date) for comment in user_comments] + all_user_comments_data = [ + (comment.object_pk, comment.submit_date) for comment in user_comments] unique_ordered_comments_data = [] - for (id, submit_date) in all_user_comments_data: + for (id, submit_date) in all_user_comments_data: if id not in [item["annotation_id"] for item in unique_ordered_comments_data]: - unique_ordered_comments_data.append({"annotation_id": id, "latest_comment_date": submit_date}) + unique_ordered_comments_data.append( + {"annotation_id": id, "latest_comment_date": submit_date}) commented_annotations = Annotation.objects.filter(id__in=[item["annotation_id"] for item in unique_ordered_comments_data]).prefetch_related( 'current_revision', 'revisions', @@ -296,11 +325,12 @@ sorted_annotations_list = [] logger.debug(unique_ordered_comments_data) for comment_data in unique_ordered_comments_data: - annotation_obj = commented_annotations.get(id=comment_data["annotation_id"]) + annotation_obj = commented_annotations.get( + id=comment_data["annotation_id"]) sorted_annotations_list.append( { - "annotation_obj": annotation_obj, - "comment_count_for_user": user_comments.filter(object_pk=annotation_obj.id).count(), + "annotation_obj": annotation_obj, + "comment_count_for_user": user_comments.filter(object_pk=annotation_obj.id).count(), "latest_comment_date": comment_data["latest_comment_date"] } ) @@ -309,11 +339,11 @@ 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 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. - + Annotations can be considered validated or not depending on the metacategories posted in their comments through the attribute validation_state. Their validation state can also be overriden and in such case we can use validation_state_overriden attribute to remember it in the model (so for instance if an admin un-validates an annotation we could block it from being validated again) @@ -325,55 +355,61 @@ (VALIDATED, 'validated'), ) 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) + 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') - validation_state = models.IntegerField(choices=VALIDATION_STATES, default=UNVALIDATED) + comments = GenericRelation( + 'IconolabComment', content_type_field='content_type_id', object_id_field='object_pk') + validation_state = models.IntegerField( + choices=VALIDATION_STATES, default=UNVALIDATED) validation_state_overriden = models.BooleanField(default=False) - + objects = AnnotationManager() - + def __str__(self): - return str(self.annotation_guid)+":"+self.current_revision.title - + 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 ] - + 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): """ @@ -386,7 +422,7 @@ # Revision will require validation new_revision_state = AnnotationRevision.AWAITING new_revision = AnnotationRevision( - annotation = self, + annotation=self, parent_revision=self.current_revision, title=title, description=description, @@ -399,9 +435,10 @@ if new_revision.state == AnnotationRevision.ACCEPTED: self.current_revision = new_revision self.save() - iconolab_signals.revision_created.send(sender=AnnotationRevision, instance=new_revision) + iconolab_signals.revision_created.send( + sender=AnnotationRevision, instance=new_revision) return new_revision - + @transaction.atomic def validate_existing_revision(self, revision_to_validate): """ @@ -412,8 +449,9 @@ revision_to_validate.state = AnnotationRevision.ACCEPTED revision_to_validate.save() self.save() - iconolab_signals.revision_accepted.send(sender=AnnotationRevision, instance=revision_to_validate) - + iconolab_signals.revision_accepted.send( + sender=AnnotationRevision, instance=revision_to_validate) + @transaction.atomic def reject_existing_revision(self, revision_to_reject): """ @@ -422,20 +460,23 @@ 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) - + 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 = 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 + iconolab_signals.revision_accepted.send( + sender=AnnotationRevision, instance=revision_to_merge) + self.current_revision = merged_revision self.save() return merged_revision @@ -444,100 +485,114 @@ """ 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) - accepted_revisions_count = models.IntegerField(blank=True, null=True, default=1) + 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) - metacategories = models.ManyToManyField('MetaCategory', through='MetaCategoriesCountInfo', through_fields=('annotation_stats_obj', 'metacategory')) - + metacategories = models.ManyToManyField( + 'MetaCategory', through='MetaCategoriesCountInfo', through_fields=('annotation_stats_obj', 'metacategory')) + def __str__(self): - return "stats:for:"+str(self.annotation.annotation_guid) - + 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) + 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) + 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 = self.annotation.current_revision).distinct().count() - + self.tag_count = Tag.objects.filter( + tagginginfo__revision=self.annotation.current_revision).distinct().count() + @property def relevant_tags_count(self, score=settings.RELEVANT_TAGS_MIN_SCORE): return TaggingInfo.objects.filter(revision=self.annotation.current_revision, relevancy__gte=score).distinct().count() - + @property def accurate_tags_count(self, score=settings.ACCURATE_TAGS_MIN_SCORE): return TaggingInfo.objects.filter(revision=self.annotation.current_revision, accuracy__gte=score).distinct().count() - + @transaction.atomic def update_stats(self): # views_count - Can't do much about views count - # submitted_revisions_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() + 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() + 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, + object_pk=self.annotation.pk, ).count() # contributors_count self.contributors_count = len(self.contributors) # tag_count - + annotation_comments_with_metacategories = IconolabComment.objects.filter( - content_type__app_label="iconolab", - content_type__model="annotation", - object_pk=self.annotation.id, + content_type__app_label="iconolab", + content_type__model="annotation", + object_pk=self.annotation.id, metacategories__collection=self.annotation.image.item.collection ) - m2m_objects = MetaCategoriesCountInfo.objects.filter(annotation_stats_obj=self) + m2m_objects = MetaCategoriesCountInfo.objects.filter( + annotation_stats_obj=self) for obj in m2m_objects.all(): obj.count = 0 obj.save() for comment in annotation_comments_with_metacategories.all(): for metacategory in comment.metacategories.all(): if metacategory not in self.metacategories.all(): - MetaCategoriesCountInfo.objects.create(annotation_stats_obj=self, metacategory=metacategory, count=1) + MetaCategoriesCountInfo.objects.create( + annotation_stats_obj=self, metacategory=metacategory, count=1) else: - m2m_object = MetaCategoriesCountInfo.objects.filter(annotation_stats_obj=self, metacategory=metacategory).first() + m2m_object = MetaCategoriesCountInfo.objects.filter( + annotation_stats_obj=self, metacategory=metacategory).first() m2m_object.count += 1 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) + 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) - + def __str__(self): - return "metacategory_count_for:"+self.metacategory.label+":on:"+str(self.annotation_stats_obj.annotation.annotation_guid) + return "metacategory_count_for:" + self.metacategory.label + ":on:" + str(self.annotation_stats_obj.annotation.annotation_guid) 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: - + - 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 + - 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 @@ -548,33 +603,37 @@ 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) + 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')) + 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 + 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 @@ -594,47 +653,50 @@ 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 + + # check if url + if tag_string.startswith("http://") or tag_string.startswith("https://"): + # check if tag already exists + if Tag.objects.filter(link=tag_string).exists(): tag_obj = Tag.objects.get(link=tag_string) else: tag_obj = Tag.objects.create( - link = tag_string, + link=tag_string, ) else: - new_tag_link = settings.BASE_URL+'/'+slugify(tag_string) + 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 + 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, + tag=tag_obj, revision=self, - accuracy = tag_accuracy, - relevancy = tag_relevancy + accuracy=tag_accuracy, + relevancy=tag_relevancy ) - + 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 + "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 }, { @@ -643,15 +705,16 @@ ] """ 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' + 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, + sparql_query_url, params={ - "query": sparql_query, - "format": "json" + "query": sparql_query, + "format": "json" } ) except: @@ -667,7 +730,7 @@ 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(): @@ -680,16 +743,17 @@ }) else: tag_link = tagging_info.tag.link - #import label from external + # 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])) + (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 + 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() @@ -702,13 +766,13 @@ }) except StopIteration: pass - return json.dumps(final_list) + 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 """ @@ -717,77 +781,80 @@ 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 + 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) + 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)) + 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')) - + 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"] - + @property def annotation(self): if self.content_type.app_label == "iconolab" and self.content_type.model == "annotation": return Annotation.objects.get(pk=self.object_pk) return None - + 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 + ).filter(thread_id__gte=self.thread_id).filter(order__lte=self.order).count() + 1) // settings.COMMENTS_PER_PAGE_DEFAULT + 1 - + class MetaCategory(models.Model): """ 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 - + Metacategories can be used to consider an annotation as "validated" if a certain agreement threshold is reached using their validation_value property - + - NEUTRAL : The metacategory doesn't affect the validation state - AGREEMENT : The metacategory can be used to validate the annotation when linked to a comment on said annotation - DISAGREEMENT : The metacategory can be used to unvalidate the annotation when linked to a comment on said annotation - + """ NONE = 0 CONTRIBUTORS = 1 @@ -799,7 +866,7 @@ (COMMENTERS, 'commenters'), (COLLECTION_ADMINS, 'collection admins'), ) - + NEUTRAL = 0 AGREEMENT = 1 DISAGREEMENT = 2 @@ -808,15 +875,17 @@ (AGREEMENT, 'agreement'), (DISAGREEMENT, 'disagreement'), ) - + collection = models.ForeignKey(Collection, related_name="metacategories") label = models.CharField(max_length=255) - triggers_notifications = models.IntegerField(choices=NOTIFIED_USERS, default=NONE) - validation_value = models.IntegerField(choices=VALIDATION_VALUES, default=NEUTRAL) - + triggers_notifications = models.IntegerField( + choices=NOTIFIED_USERS, default=NONE) + validation_value = models.IntegerField( + choices=VALIDATION_VALUES, default=NEUTRAL) + def __str__(self): - return self.label+":"+self.collection.name - + return self.label + ":" + self.collection.name + class MetaCategoryInfo(models.Model): """ @@ -824,9 +893,9 @@ """ 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 + return "metacategory:" + self.metacategory.label + ":on:" + self.comment.id class CommentAttachement(models.Model): @@ -842,20 +911,23 @@ (IMAGE, 'image'), (PDF, 'pdf') ) - - comment = models.ForeignKey('IconolabComment', related_name='attachments', on_delete=models.CASCADE) + + 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) - + user = models.OneToOneField( + User, related_name='profile', on_delete=models.CASCADE) + managed_collections = models.ManyToManyField( + 'Collection', related_name='admins', blank=True) + def __str__(self): - return "profile:"+self.user.username \ No newline at end of file + return "profile:" + self.user.username diff -r 959cbaad2076 -r 9273f1f2c827 src/iconolab/settings/dev.py.tmpl --- a/src/iconolab/settings/dev.py.tmpl Fri Dec 16 13:24:55 2016 +0100 +++ b/src/iconolab/settings/dev.py.tmpl Wed Jan 18 14:11:30 2017 +0100 @@ -9,9 +9,10 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.9/ref/settings/ """ -from iconolab.settings import * +import logging +import os -import os, logging +from iconolab.settings import * # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) diff -r 959cbaad2076 -r 9273f1f2c827 src/iconolab/urls.py --- a/src/iconolab/urls.py Fri Dec 16 13:24:55 2016 +0100 +++ b/src/iconolab/urls.py Wed Jan 18 14:11:30 2017 +0100 @@ -43,7 +43,7 @@ url(r'^collections/(?P[a-z0-9\-]+)/images/(?P[^/]+)/annotations/(?P[^/]+)/revisions/?$', django_views.generic.RedirectView.as_view(pattern_name="annotation_detail")), url(r'^collections/(?P[a-z0-9\-]+)/images/(?P[^/]+)/annotations/(?P[^/]+)/revisions/(?P[^/]+)/detail', views.objects.ShowRevisionView.as_view(), name='revision_detail'), url(r'^collections/(?P[a-z0-9\-]+)/images/(?P[^/]+)/annotations/(?P[^/]+)/revisions/(?P[^/]+)/merge$', login_required(views.objects.MergeProposalView.as_view()), name='annotation_merge'), - + url(r'^user/(?P[a-z0-9\-]+)/home/?$', views.userpages.UserHomeView.as_view(), name="user_home"), url(r'^user/(?P[a-z0-9\-]+)/commented/?$', views.userpages.UserCommentedView.as_view(), name="user_commented"), url(r'^user/(?P[a-z0-9\-]+)/contributed/?$', views.userpages.UserContributedView.as_view(), name="user_contributed"), @@ -51,20 +51,20 @@ url(r'^user/(?P[a-z0-9\-]+)/adminpanel/(?P[a-z0-9\-]+)/$', views.userpages.UserCollectionAdminView.as_view(), name="user_admin_panel"), url(r'^user/notifications/all/?$', login_required(views.userpages.UserNotificationsView.as_view()), name="user_notifications"), url(r'^user/notifications/', include(notifications.urls, namespace='notifications')), - + url(r'^errors/404', views.misc.NotFoundErrorView.as_view(), name="404error"), - + url(r'^help/', views.misc.HelpView.as_view(), name="iconolab_help"), url(r'^glossary/', views.misc.GlossaryView.as_view(), name="iconolab_glossary"), url(r'^credits/', views.misc.CreditsView.as_view(), name="iconolab_credits"), url(r'^contributioncharter/', views.misc.ContributionCharterView.as_view(), name="iconolab_charter"), url(r'^legalmentions/', views.misc.LegalMentionsView.as_view(), name="iconolab_legals"), - + url(r'^account/', include('iconolab.auth.urls', namespace='account')), url(r'^search/', include('iconolab.search_indexes.urls', namespace='search_indexes')), url(r'^comments/', include('django_comments_xtd.urls')), url(r'^comments/annotation/post', views.comments.post_comment_iconolab, name="post_comment"), - + url(r'^compare/$', views.objects.TestView.as_view(), name="compare_view") #url(r'^search/', include('haystack.urls'), name="search_iconolab"), ] diff -r 959cbaad2076 -r 9273f1f2c827 src/iconolab/views/objects.py --- a/src/iconolab/views/objects.py Fri Dec 16 13:24:55 2016 +0100 +++ b/src/iconolab/views/objects.py Wed Jan 18 14:11:30 2017 +0100 @@ -25,11 +25,12 @@ 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 + 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() @@ -50,8 +51,10 @@ """ 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 - following this ordering: (collection, item, image, annotation, revision) + 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 following this ordering: + (collection, item, image, annotation, revision) ''' objects_tuple = () @@ -81,11 +84,11 @@ except (ValueError, AnnotationRevision.DoesNotExist): return False, RedirectView.as_view(url=reverse('404error')) return True, objects_tuple - + 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 for display purposes - + Dict will be of the format: { page: the page to load (integer) @@ -98,7 +101,7 @@ 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 = {} @@ -122,11 +125,11 @@ pagination_data["ellipsis_first"] = pagination_data["show_first"] and (page - adjacent_pages_count != 2) pagination_data["show_last"] = page + adjacent_pages_count < paginator.num_pages pagination_data["ellipsis_last"] = pagination_data["show_last"] and (page + adjacent_pages_count != paginator.num_pages - 1) - return pagination_data - + return pagination_data + class CollectionHomepageView(View, ContextMixin, IconolabObjectView): """ - View that displays a collection and four panels to show relevant paginated lists for collection: + 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 @@ -135,10 +138,10 @@ def get(self, request, *args, **kwargs): """ Template is iconolab/collection_home.html - - Url args are: - - collection_name: 'name' attribute of the requested collection - + + 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 @@ -149,7 +152,7 @@ - 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 @@ -158,7 +161,7 @@ - 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 @@ -167,7 +170,7 @@ context = super(CollectionHomepageView, self).get_context_data(**kwargs) context['collection_name'] = self.kwargs.get('collection_name', '') context['collection'] = collection - + # get Pagination and navigation query args try: items_page = int(request.GET.get('items_page', '1')) @@ -177,7 +180,7 @@ items_per_page = int(request.GET.get('items_perpage', '12')) except ValueError: items_per_page = 12 - + try: recent_page = int(request.GET.get('recent_page', '1')) except ValueError: @@ -186,7 +189,7 @@ recent_per_page = int(request.GET.get('recent_perpage', '10')) except ValueError: recent_per_page = 10 - + try: revised_page = int(request.GET.get('revised_page', '1')) except ValueError: @@ -195,7 +198,7 @@ revised_per_page = int(request.GET.get('revised_perpage', '10')) except ValueError: revised_per_page = 10 - + try: contributions_page = int(request.GET.get('contributions_page', '1')) except ValueError: @@ -204,22 +207,22 @@ contributions_per_page = int(request.GET.get('contributions_perpage', '10')) except ValueError: contributions_per_page = 10 - + active_list = request.GET.get('show', 'items') if active_list not in ['items', 'recent', 'revised', 'contributions']: active_list = 'items' context["active_list"] = active_list - - + + # Pagination values adjacent_pages_count = 2 - + # Paginated objects list items_list = collection.items.order_by("metadatas__inventory_number").all() context["items_pagination_data"] = self.get_pagination_data( - items_list, - items_page, - items_per_page, + items_list, + items_page, + items_per_page, adjacent_pages_count, perpage_range=[6, 12, 48, 192], trailing_qarg="&recent_page="+str(recent_page) @@ -229,16 +232,16 @@ +"&contributions_page="+str(contributions_page) +"&contributions_perpage="+str(contributions_per_page) ) - + # Paginated recent annotations list recent_annotations = Annotation.objects.filter(image__item__collection__name=collection.name).prefetch_related( 'current_revision', 'stats' ).order_by('-current_revision__created') context["recent_pagination_data"] = self.get_pagination_data( - recent_annotations, - recent_page, - recent_per_page, + recent_annotations, + recent_page, + recent_per_page, adjacent_pages_count, trailing_qarg="&items_page="+str(items_page) +"&items_perpage="+str(items_per_page) @@ -247,16 +250,16 @@ +"&contributions_page="+str(contributions_page) +"&contributions_perpage="+str(contributions_per_page) ) - + # Paginated revised annotations list revised_annotations = Annotation.objects.filter(image__item__collection__name=collection.name).prefetch_related( 'current_revision', 'stats' ).annotate(revision_count=Count('revisions')).order_by('-revision_count') context["revised_pagination_data"] = self.get_pagination_data( - revised_annotations, - revised_page, - revised_per_page, + revised_annotations, + revised_page, + revised_per_page, adjacent_pages_count, trailing_qarg="&items_page="+str(items_page) +"&items_perpage="+str(items_per_page) @@ -265,7 +268,7 @@ +"&contributions_page="+str(contributions_page) +"&contributions_perpage="+str(contributions_per_page) ) - + # Paginated contribution calls annotation list contrib_calls_annotations_ids = list(set(MetaCategoryInfo.objects.filter( metacategory__collection__name=collection.name, @@ -275,9 +278,9 @@ collection_ann_dict = dict([(str(annotation.id), annotation) for annotation in collection_annotations]) contributions_annotations = [collection_ann_dict[id] for id in contrib_calls_annotations_ids] context["contributions_pagination_data"] = self.get_pagination_data( - contributions_annotations, - contributions_page, - contributions_per_page, + contributions_annotations, + contributions_page, + contributions_per_page, adjacent_pages_count, trailing_qarg="&items_page="+str(items_page) +"&items_perpage="+str(items_per_page) @@ -286,7 +289,7 @@ +"&revised_page="+str(revised_page) +"&revised_perpage="+str(revised_per_page) ) - + return render(request, 'iconolab/collection_home.html', context) @@ -298,16 +301,16 @@ 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 - + - 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 @@ -320,13 +323,13 @@ 'annotations': the list of annotations on that image } """ - + success, result = self.check_kwargs(kwargs) if success: (collection, item) = result else: return result(request) - + context = super(ShowItemView, self).get_context_data(**kwargs) image_guid_to_display = request.GET.get("show", str(item.images.first().image_guid)) if image_guid_to_display not in [str(guid) for guid in item.images.all().values_list("image_guid", flat=True)]: @@ -340,7 +343,7 @@ displayed_annotations_per_page = int(request.GET.get('perpage', '10')) except ValueError: displayed_annotations_per_page = 10 - + context['collection_name'] = self.kwargs.get('collection_name', '') context['item_guid'] = self.kwargs.get('image_guid', '') context['collection'] = collection @@ -359,7 +362,7 @@ except PageNotAnInteger: annotations = annotations_paginator.page(1) except EmptyPage: - annotations = annotations_paginator.page(recent_paginator.num_pages) + annotations = annotations_paginator.page(recent_paginator.num_pages) context['images'].append({ 'obj' : image, 'annotations': annotations @@ -444,10 +447,11 @@ 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. + 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', '') @@ -458,23 +462,23 @@ 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 - + + 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 + - 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) @@ -535,7 +539,7 @@ def get(self, request, *args, **kwargs): """ - Exactly the same as ShowAnnotationView but without all the data around comments + Exactly the same as ShowAnnotationView but without all the data around comments """ success, result = self.check_kwargs(kwargs) if success: @@ -556,7 +560,7 @@ class EditAnnotationView(View, ContextMixin, IconolabObjectView): """ - View that handles displaying the edition form and editing an annotation + 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) @@ -631,13 +635,13 @@ 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 + + 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 diff -r 959cbaad2076 -r 9273f1f2c827 src/requirements/base.txt --- a/src/requirements/base.txt Fri Dec 16 13:24:55 2016 +0100 +++ b/src/requirements/base.txt Wed Jan 18 14:11:30 2017 +0100 @@ -1,15 +1,17 @@ -Django==1.10 -django-comments-xtd==1.6.0 -django-contrib-comments==1.7.2 -django-haystack==2.5.0 -django-model-utils==2.5.2 +Django==1.10.5 +django-comments-xtd==1.6.3 +django-contrib-comments==1.7.3 +django-haystack==2.6.0 +django-model-utils==2.6.1 django-notifications-hq==1.2 -elasticsearch==2.4.0 +elasticsearch==5.1.0 +iconolab==0.0.19 jsonfield==1.0.3 -Pillow==3.3.1 +olefile==0.44 +Pillow==4.0.0 psycopg2==2.6.2 -pytz==2016.6.1 -requests==2.11.1 +pytz==2016.10 +requests==2.12.4 six==1.10.0 sorl-thumbnail==12.4a1 -urllib3==1.16 +urllib3==1.19.1