diff --git a/requirements.txt b/requirements.txt index c1c2113e..f7306ea3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ jsonfield django-tastypie==0.9.14 django-waffle==0.9.1 git+git://github.com/scieloorg/django-htmlmin.git -git+git://github.com/scieloorg/django-cache-machine.git#egg=django-cache-machine +-e git+git://github.com/scieloorg/django-cache-machine.git#egg=django-cache-machine git+git://github.com/scieloorg/packtools.git#egg=packtools Celery django-celery diff --git a/scielomanager/api/tests.py b/scielomanager/api/tests.py index 1b51223b..e9c82e4f 100644 --- a/scielomanager/api/tests.py +++ b/scielomanager/api/tests.py @@ -1236,6 +1236,7 @@ def test_api_v1_data_checkin(self): u'accepted_at', u'article', u'attempt_ref', + u'checked_out', u'created_at', u'expiration_at', u'id', @@ -1243,6 +1244,7 @@ def test_api_v1_data_checkin(self): u'rejected_at', u'rejected_cause', u'reviewed_at', + u'scielo_reviewed_at', u'resource_uri', u'status', u'uploaded_at' diff --git a/scielomanager/articletrack/admin.py b/scielomanager/articletrack/admin.py index 1944655b..e30cb226 100644 --- a/scielomanager/articletrack/admin.py +++ b/scielomanager/articletrack/admin.py @@ -22,7 +22,7 @@ def display_member(self, obj): class CheckinAdmin(admin.ModelAdmin): - list_display = ('package_name', 'attempt_ref', 'uploaded_at', 'created_at') + list_display = ('package_name', 'attempt_ref', 'uploaded_at', 'created_at', 'status',) search_fields = ('package_name',) readonly_fields = ('created_at',) diff --git a/scielomanager/articletrack/balaio.py b/scielomanager/articletrack/balaio.py index b004293b..8b3fd2dc 100644 --- a/scielomanager/articletrack/balaio.py +++ b/scielomanager/articletrack/balaio.py @@ -135,7 +135,7 @@ def is_up(self): """ try: return self.call('status') - except xmlrpclib.Error: + except Exception: # any exception, such as "CONNECTION REFUESED" means API is down return False def get_server(self, uri): diff --git a/scielomanager/articletrack/migrations/0012_auto__add_field_checkin_scielo_reviewed_by__add_field_checkin_scielo_r.py b/scielomanager/articletrack/migrations/0012_auto__add_field_checkin_scielo_reviewed_by__add_field_checkin_scielo_r.py new file mode 100644 index 00000000..74ff4af1 --- /dev/null +++ b/scielomanager/articletrack/migrations/0012_auto__add_field_checkin_scielo_reviewed_by__add_field_checkin_scielo_r.py @@ -0,0 +1,299 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Checkin.scielo_reviewed_by' + db.add_column('articletrack_checkin', 'scielo_reviewed_by', + self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='checkins_scielo_reviewed', null=True, to=orm['auth.User']), + keep_default=False) + + # Adding field 'Checkin.scielo_reviewed_at' + db.add_column('articletrack_checkin', 'scielo_reviewed_at', + self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Checkin.scielo_reviewed_by' + db.delete_column('articletrack_checkin', 'scielo_reviewed_by_id') + + # Deleting field 'Checkin.scielo_reviewed_at' + db.delete_column('articletrack_checkin', 'scielo_reviewed_at') + + + models = { + 'articletrack.article': { + 'Meta': {'object_name': 'Article'}, + 'article_title': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'articlepkg_ref': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'eissn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '9'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_label': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'journal_title': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'journals': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'checkin_articles'", 'null': 'True', 'to': "orm['journalmanager.Journal']"}), + 'pissn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '9'}) + }, + 'articletrack.checkin': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'Checkin'}, + 'accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'accepted_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'article': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkins'", 'null': 'True', 'to': "orm['articletrack.Article']"}), + 'attempt_ref': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expiration_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'rejected_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'rejected_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_rejected'", 'null': 'True', 'to': "orm['auth.User']"}), + 'rejected_cause': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'reviewed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'scielo_reviewed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'scielo_reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_scielo_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '10'}), + 'submitted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_submitted_by'", 'null': 'True', 'to': "orm['auth.User']"}), + 'uploaded_at': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'articletrack.checkinworkflowlog': { + 'Meta': {'ordering': "['created_at']", 'object_name': 'CheckinWorkflowLog'}, + 'checkin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submission_log'", 'to': "orm['articletrack.Checkin']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '10'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkin_log_responsible'", 'null': 'True', 'to': "orm['auth.User']"}) + }, + 'articletrack.comment': { + 'Meta': {'ordering': "['created_at']", 'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments_author'", 'to': "orm['auth.User']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['articletrack.Ticket']"}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'articletrack.notice': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'Notice'}, + 'checkin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notices'", 'to': "orm['articletrack.Checkin']"}), + 'checkpoint': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'stage': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'articletrack.team': { + 'Meta': {'object_name': 'Team'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'team'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}) + }, + 'articletrack.ticket': { + 'Meta': {'ordering': "['started_at']", 'object_name': 'Ticket'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tickets'", 'to': "orm['articletrack.Article']"}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tickets'", 'to': "orm['auth.User']"}), + 'finished_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'started_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '256'}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'journalmanager.collection': { + 'Meta': {'ordering': "['name']", 'object_name': 'Collection'}, + 'acronym': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'address': ('django.db.models.fields.TextField', [], {}), + 'address_complement': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'address_number': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'collection': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_collection'", 'to': "orm['auth.User']", 'through': "orm['journalmanager.UserCollections']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'name_slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'zip_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.institution': { + 'Meta': {'ordering': "['name']", 'object_name': 'Institution'}, + 'acronym': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'address': ('django.db.models.fields.TextField', [], {}), + 'address_complement': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'address_number': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'cel': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'complement': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_trashed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'zip_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.journal': { + 'Meta': {'ordering': "['title']", 'object_name': 'Journal'}, + 'abstract_keyword_languages': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'abstract_keyword_languages'", 'symmetrical': 'False', 'to': "orm['journalmanager.Language']"}), + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'collections': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Collection']", 'through': "orm['journalmanager.Membership']", 'symmetrical': 'False'}), + 'copyrighter': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'cover': ('scielomanager.custom_fields.ContentTypeRestrictedFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'enjoy_creator'", 'to': "orm['auth.User']"}), + 'ctrl_vocabulary': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'current_ahead_documents': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'editor_address': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'editor_address_city': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'editor_address_country': ('scielo_extensions.modelfields.CountryField', [], {'max_length': '2'}), + 'editor_address_state': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'editor_address_zip': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'editor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'editor_name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'editor_phone1': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'editor_phone2': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'editorial_standard': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'editors': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'user_editors'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'eletronic_issn': ('django.db.models.fields.CharField', [], {'max_length': '9', 'db_index': 'True'}), + 'final_num': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'final_vol': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'final_year': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True', 'blank': 'True'}), + 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_coverage': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'init_num': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'init_vol': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'init_year': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'is_indexed_aehci': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_indexed_scie': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_indexed_ssci': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_trashed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'languages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Language']", 'symmetrical': 'False'}), + 'logo': ('scielomanager.custom_fields.ContentTypeRestrictedFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'medline_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'medline_title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'national_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'notes': ('django.db.models.fields.TextField', [], {'max_length': '254', 'null': 'True', 'blank': 'True'}), + 'other_previous_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'previous_ahead_documents': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'previous_title': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'prev_title'", 'null': 'True', 'to': "orm['journalmanager.Journal']"}), + 'print_issn': ('django.db.models.fields.CharField', [], {'max_length': '9', 'db_index': 'True'}), + 'pub_level': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'publication_city': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'publisher_country': ('scielo_extensions.modelfields.CountryField', [], {'max_length': '2'}), + 'publisher_name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'publisher_state': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'scielo_issn': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'secs_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'short_title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'db_index': 'True'}), + 'sponsor': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'journal_sponsor'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['journalmanager.Sponsor']"}), + 'study_areas': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'journals_migration_tmp'", 'null': 'True', 'to': "orm['journalmanager.StudyArea']"}), + 'subject_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'journals'", 'null': 'True', 'to': "orm['journalmanager.SubjectCategory']"}), + 'subject_descriptors': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'title_iso': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'twitter_user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'url_journal': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'url_online_submission': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'use_license': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.UseLicense']"}) + }, + 'journalmanager.language': { + 'Meta': {'ordering': "['name']", 'object_name': 'Language'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '2'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'journalmanager.membership': { + 'Meta': {'unique_together': "(('journal', 'collection'),)", 'object_name': 'Membership'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Collection']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'journal': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Journal']"}), + 'reason': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'since': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'inprogress'", 'max_length': '16'}) + }, + 'journalmanager.sponsor': { + 'Meta': {'ordering': "['name']", 'object_name': 'Sponsor', '_ormbases': ['journalmanager.Institution']}, + 'collections': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Collection']", 'symmetrical': 'False'}), + 'institution_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['journalmanager.Institution']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'journalmanager.studyarea': { + 'Meta': {'object_name': 'StudyArea'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'study_area': ('django.db.models.fields.CharField', [], {'max_length': '256'}) + }, + 'journalmanager.subjectcategory': { + 'Meta': {'object_name': 'SubjectCategory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'term': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}) + }, + 'journalmanager.uselicense': { + 'Meta': {'ordering': "['license_code']", 'object_name': 'UseLicense'}, + 'disclaimer': ('django.db.models.fields.TextField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license_code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reference_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.usercollections': { + 'Meta': {'unique_together': "(('user', 'collection'),)", 'object_name': 'UserCollections'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['articletrack'] \ No newline at end of file diff --git a/scielomanager/articletrack/migrations/0013_auto__add_field_checkin_checked_out.py b/scielomanager/articletrack/migrations/0013_auto__add_field_checkin_checked_out.py new file mode 100644 index 00000000..9061ffe3 --- /dev/null +++ b/scielomanager/articletrack/migrations/0013_auto__add_field_checkin_checked_out.py @@ -0,0 +1,292 @@ +# -*- coding: utf-8 -*- +from south.utils import datetime_utils as datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Adding field 'Checkin.checked_out' + db.add_column('articletrack_checkin', 'checked_out', + self.gf('django.db.models.fields.BooleanField')(default=False), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'Checkin.checked_out' + db.delete_column('articletrack_checkin', 'checked_out') + + + models = { + 'articletrack.article': { + 'Meta': {'object_name': 'Article'}, + 'article_title': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'articlepkg_ref': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'eissn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '9'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'issue_label': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'journal_title': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'journals': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'checkin_articles'", 'null': 'True', 'to': "orm['journalmanager.Journal']"}), + 'pissn': ('django.db.models.fields.CharField', [], {'default': "''", 'max_length': '9'}) + }, + 'articletrack.checkin': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'Checkin'}, + 'accepted_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'accepted_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'null': 'True', 'blank': 'True'}), + 'article': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'checkins'", 'null': 'True', 'to': "orm['articletrack.Article']"}), + 'attempt_ref': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'checked_out': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'expiration_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'rejected_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'rejected_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_rejected'", 'null': 'True', 'to': "orm['auth.User']"}), + 'rejected_cause': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'reviewed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'scielo_reviewed_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'scielo_reviewed_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_scielo_reviewed'", 'null': 'True', 'to': "orm['auth.User']"}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '10'}), + 'submitted_by': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkins_submitted_by'", 'null': 'True', 'to': "orm['auth.User']"}), + 'uploaded_at': ('django.db.models.fields.CharField', [], {'max_length': '128'}) + }, + 'articletrack.checkinworkflowlog': { + 'Meta': {'ordering': "['created_at']", 'object_name': 'CheckinWorkflowLog'}, + 'checkin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'submission_log'", 'to': "orm['articletrack.Checkin']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'description': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'pending'", 'max_length': '10'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'checkin_log_responsible'", 'null': 'True', 'to': "orm['auth.User']"}) + }, + 'articletrack.comment': { + 'Meta': {'ordering': "['created_at']", 'object_name': 'Comment'}, + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments_author'", 'to': "orm['auth.User']"}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'ticket': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'comments'", 'to': "orm['articletrack.Ticket']"}), + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) + }, + 'articletrack.notice': { + 'Meta': {'ordering': "['-created_at']", 'object_name': 'Notice'}, + 'checkin': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'notices'", 'to': "orm['articletrack.Checkin']"}), + 'checkpoint': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'stage': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'status': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'articletrack.team': { + 'Meta': {'object_name': 'Team'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'member': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'team'", 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}) + }, + 'articletrack.ticket': { + 'Meta': {'ordering': "['started_at']", 'object_name': 'Ticket'}, + 'article': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tickets'", 'to': "orm['articletrack.Article']"}), + 'author': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'tickets'", 'to': "orm['auth.User']"}), + 'finished_at': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.TextField', [], {}), + 'started_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '256'}) + }, + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'journalmanager.collection': { + 'Meta': {'ordering': "['name']", 'object_name': 'Collection'}, + 'acronym': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'address': ('django.db.models.fields.TextField', [], {}), + 'address_complement': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'address_number': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'collection': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'user_collection'", 'to': "orm['auth.User']", 'through': "orm['journalmanager.UserCollections']", 'blank': 'True', 'symmetrical': 'False', 'null': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'logo': ('django.db.models.fields.files.ImageField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '128', 'db_index': 'True'}), + 'name_slug': ('django.db.models.fields.SlugField', [], {'max_length': '50', 'unique': 'True', 'null': 'True', 'blank': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}), + 'zip_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.institution': { + 'Meta': {'ordering': "['name']", 'object_name': 'Institution'}, + 'acronym': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '16', 'blank': 'True'}), + 'address': ('django.db.models.fields.TextField', [], {}), + 'address_complement': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), + 'address_number': ('django.db.models.fields.CharField', [], {'max_length': '8'}), + 'cel': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'city': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'complement': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'country': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'fax': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_trashed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'phone': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'state': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'zip_code': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.journal': { + 'Meta': {'ordering': "['title']", 'object_name': 'Journal'}, + 'abstract_keyword_languages': ('django.db.models.fields.related.ManyToManyField', [], {'related_name': "'abstract_keyword_languages'", 'symmetrical': 'False', 'to': "orm['journalmanager.Language']"}), + 'acronym': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'collections': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Collection']", 'through': "orm['journalmanager.Membership']", 'symmetrical': 'False'}), + 'copyrighter': ('django.db.models.fields.CharField', [], {'max_length': '254'}), + 'cover': ('scielomanager.custom_fields.ContentTypeRestrictedFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'creator': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'enjoy_creator'", 'to': "orm['auth.User']"}), + 'ctrl_vocabulary': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'current_ahead_documents': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'editor_address': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'editor_address_city': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'editor_address_country': ('scielo_extensions.modelfields.CountryField', [], {'max_length': '2'}), + 'editor_address_state': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'editor_address_zip': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'editor_email': ('django.db.models.fields.EmailField', [], {'max_length': '75'}), + 'editor_name': ('django.db.models.fields.CharField', [], {'max_length': '512'}), + 'editor_phone1': ('django.db.models.fields.CharField', [], {'max_length': '32'}), + 'editor_phone2': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}), + 'editorial_standard': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'editors': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'user_editors'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['auth.User']"}), + 'eletronic_issn': ('django.db.models.fields.CharField', [], {'max_length': '9', 'db_index': 'True'}), + 'final_num': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'final_vol': ('django.db.models.fields.CharField', [], {'max_length': '16', 'blank': 'True'}), + 'final_year': ('django.db.models.fields.CharField', [], {'max_length': '4', 'null': 'True', 'blank': 'True'}), + 'frequency': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'index_coverage': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'init_num': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'init_vol': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'init_year': ('django.db.models.fields.CharField', [], {'max_length': '4'}), + 'is_indexed_aehci': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_indexed_scie': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_indexed_ssci': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_trashed': ('django.db.models.fields.BooleanField', [], {'default': 'False', 'db_index': 'True'}), + 'languages': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Language']", 'symmetrical': 'False'}), + 'logo': ('scielomanager.custom_fields.ContentTypeRestrictedFileField', [], {'max_length': '100', 'null': 'True', 'blank': 'True'}), + 'medline_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'medline_title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'blank': 'True'}), + 'national_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}), + 'notes': ('django.db.models.fields.TextField', [], {'max_length': '254', 'null': 'True', 'blank': 'True'}), + 'other_previous_title': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'previous_ahead_documents': ('django.db.models.fields.IntegerField', [], {'default': '0', 'max_length': '3', 'null': 'True', 'blank': 'True'}), + 'previous_title': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'prev_title'", 'null': 'True', 'to': "orm['journalmanager.Journal']"}), + 'print_issn': ('django.db.models.fields.CharField', [], {'max_length': '9', 'db_index': 'True'}), + 'pub_level': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'publication_city': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'publisher_country': ('scielo_extensions.modelfields.CountryField', [], {'max_length': '2'}), + 'publisher_name': ('django.db.models.fields.CharField', [], {'max_length': '256'}), + 'publisher_state': ('django.db.models.fields.CharField', [], {'max_length': '64'}), + 'scielo_issn': ('django.db.models.fields.CharField', [], {'max_length': '16'}), + 'secs_code': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), + 'short_title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'null': 'True', 'db_index': 'True'}), + 'sponsor': ('django.db.models.fields.related.ManyToManyField', [], {'blank': 'True', 'related_name': "'journal_sponsor'", 'null': 'True', 'symmetrical': 'False', 'to': "orm['journalmanager.Sponsor']"}), + 'study_areas': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'journals_migration_tmp'", 'null': 'True', 'to': "orm['journalmanager.StudyArea']"}), + 'subject_categories': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'journals'", 'null': 'True', 'to': "orm['journalmanager.SubjectCategory']"}), + 'subject_descriptors': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), + 'title': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'title_iso': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}), + 'twitter_user': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'url_journal': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'url_online_submission': ('django.db.models.fields.CharField', [], {'max_length': '128', 'null': 'True', 'blank': 'True'}), + 'use_license': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.UseLicense']"}) + }, + 'journalmanager.language': { + 'Meta': {'ordering': "['name']", 'object_name': 'Language'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'iso_code': ('django.db.models.fields.CharField', [], {'max_length': '2'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}) + }, + 'journalmanager.membership': { + 'Meta': {'unique_together': "(('journal', 'collection'),)", 'object_name': 'Membership'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Collection']"}), + 'created_by': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'journal': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Journal']"}), + 'reason': ('django.db.models.fields.TextField', [], {'default': "''", 'blank': 'True'}), + 'since': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), + 'status': ('django.db.models.fields.CharField', [], {'default': "'inprogress'", 'max_length': '16'}) + }, + 'journalmanager.sponsor': { + 'Meta': {'ordering': "['name']", 'object_name': 'Sponsor', '_ormbases': ['journalmanager.Institution']}, + 'collections': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['journalmanager.Collection']", 'symmetrical': 'False'}), + 'institution_ptr': ('django.db.models.fields.related.OneToOneField', [], {'to': "orm['journalmanager.Institution']", 'unique': 'True', 'primary_key': 'True'}) + }, + 'journalmanager.studyarea': { + 'Meta': {'object_name': 'StudyArea'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'study_area': ('django.db.models.fields.CharField', [], {'max_length': '256'}) + }, + 'journalmanager.subjectcategory': { + 'Meta': {'object_name': 'SubjectCategory'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'term': ('django.db.models.fields.CharField', [], {'max_length': '256', 'db_index': 'True'}) + }, + 'journalmanager.uselicense': { + 'Meta': {'ordering': "['license_code']", 'object_name': 'UseLicense'}, + 'disclaimer': ('django.db.models.fields.TextField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'license_code': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '64'}), + 'reference_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}) + }, + 'journalmanager.usercollections': { + 'Meta': {'unique_together': "(('user', 'collection'),)", 'object_name': 'UserCollections'}, + 'collection': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['journalmanager.Collection']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_default': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_manager': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}) + } + } + + complete_apps = ['articletrack'] \ No newline at end of file diff --git a/scielomanager/articletrack/models.py b/scielomanager/articletrack/models.py index ef19697b..f561a6b0 100644 --- a/scielomanager/articletrack/models.py +++ b/scielomanager/articletrack/models.py @@ -2,14 +2,13 @@ import caching.base import datetime import logging -from collections import deque from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver from django.utils.translation import ugettext_lazy as _ from django.contrib.auth.models import User -from django.core.exceptions import SuspiciousOperation, ValidationError +from django.core.exceptions import ValidationError from django.conf import settings from articletrack import modelmanagers @@ -22,10 +21,12 @@ MSG_WORKFLOW_ACCEPTED = 'Checkin Accepted' MSG_WORKFLOW_REJECTED = 'Checkin Rejected' -MSG_WORKFLOW_REVIEWED = 'Checkin Reviewed' +MSG_WORKFLOW_REVIEWED_QAL1 = 'Checkin Reviewed - Level 1' +MSG_WORKFLOW_REVIEWED_QAL2 = 'Checkin Reviewed - Level 2 (SciELO)' MSG_WORKFLOW_SENT_TO_PENDING = 'Checkin Sent to Pending' MSG_WORKFLOW_SENT_TO_REVIEW = 'Checkin Sent to Review' MSG_WORKFLOW_EXPIRED = 'Checkin Expired' +MSG_WORKFLOW_CHECKED_OUT = 'Checkin Checked Out' class Team(caching.base.CachingMixin, models.Model): @@ -106,9 +107,12 @@ class Checkin(caching.base.CachingMixin, models.Model): accepted_by = models.ForeignKey(User, null=True, blank=True) accepted_at = models.DateTimeField(null=True, blank=True) - + # QAL1 reviewed_by = models.ForeignKey(User, related_name='checkins_reviewed', null=True, blank=True) reviewed_at = models.DateTimeField(null=True, blank=True) + # QAL2 + scielo_reviewed_by = models.ForeignKey(User, related_name='checkins_scielo_reviewed', null=True, blank=True) + scielo_reviewed_at = models.DateTimeField(null=True, blank=True) rejected_by = models.ForeignKey(User, related_name='checkins_rejected', null=True, blank=True) rejected_at = models.DateTimeField(null=True, blank=True) @@ -118,6 +122,8 @@ class Checkin(caching.base.CachingMixin, models.Model): expiration_at = models.DateTimeField(_(u'Expiration Date'), null=True, blank=True) + checked_out = models.BooleanField(_(u'Checked Out'), default=False) + class Meta: ordering = ['-created_at'] permissions = (("list_checkin", "Can list Checkin"),) @@ -141,19 +147,13 @@ def team_members(self): @property def is_serv_status_completed(self): """ - If: - the count of SERV_END < SERVICE_STATUS_MAX_STAGES, or - the count of SERV_BEGIN < SERVICE_STATUS_MAX_STAGES - then: - the checkin's notices sequence is UNCOMPLETED (possible more notices will arrive) - -> Return False + If the checkin's notices sequence is UNCOMPLETED (probably more notices will arrive) + Return False Else: - if the count of SERV_* is equal to 2 * SERVICE_STATUS_MAX_STAGES, then - call the ``misc.validate_sequence`` function - to check if the checkin's notices sequence is COMPLETED - -> Return True + if the quantity of SERV_* is ok then + Return the result of validate the order of SERV_. else: - -> Return False + Return False """ count_serv_end_notices = self.notices.filter(status__iexact="SERV_END").count() count_serv_begin_notices = self.notices.filter(status__iexact="SERV_BEGIN").count() @@ -212,14 +212,31 @@ def is_accepted(self): return self.status == 'accepted' and bool(self.accepted_by and self.accepted_at) @property - def is_reviewed(self): + def is_level1_reviewed(self): + """ + Checks if this checkin has been reviewed by a QAL1 user + + The condicional is ``status = review`` and has been ``reviewed_by`` and + has any date in ``reviewed_at`` + """ + return bool(self.reviewed_by and self.reviewed_at) + + @property + def is_level2_reviewed(self): + """ + Checks if this checkin has been reviewed by a QAL2 user + + The condicional is ``status = review`` and has been ``scielo_reviewed_by`` and + has any date in ``reviewed_at`` """ - Checks if this checkin has been reviewed + return bool(self.scielo_reviewed_by and self.scielo_reviewed_at) - The condicional is ``status = review`` and has been reviewed_by and - has any date in reviewed_at + @property + def is_full_reviewed(self): """ - return self.status == 'review' and bool(self.reviewed_by and self.reviewed_at) + Checks if this checkin has been reviewed by both user's roles: QAL1 and QAL2 + """ + return self.status == 'review' and self.is_level1_reviewed and self.is_level2_reviewed @property def is_rejected(self): @@ -237,7 +254,7 @@ def can_be_send_to_pending(self): Return True if this checkin have status ``rejected`` does not exist another checkin accepted for the related article. """ - return self.status == 'rejected' and not self.article.is_accepted() + return self.status == 'rejected' @property def can_be_send_to_review(self): @@ -246,7 +263,7 @@ def can_be_send_to_review(self): Return True if this checkin have status ``pending`` and have no errors and does not exist another checkin accepted for the related article. """ - return self.status == 'pending' and self.get_error_level != 'error' and not self.article.is_accepted() + return self.status == 'pending' and self.get_error_level in ['ok', 'warning'] @property def can_be_reviewed(self): @@ -255,16 +272,22 @@ def can_be_reviewed(self): Return True if this checkin is in status ``pending`` and have no errors and does not exist another checkin accepted for the related article. """ - return self.status == 'review' and self.get_error_level != 'error' and not self.article.is_accepted() + return self.status == 'review' and self.get_error_level in ['ok', 'warning'] @property def can_be_accepted(self): """ Check the conditions to enable the process of 'accept' action. - Return True if this checkin is in status ``review`` and self.is_reviewed == True and - does not exist another checkin accepted for the related article. + Return True if this checkin has been reviwed by both (scielo and non scielo parts). """ - return self.status == 'review' and self.is_reviewed and not self.article.is_accepted() + return self.is_full_reviewed + + @property + def can_be_send_to_checkout(self): + """ + Only if status == 'accepted' and self.checked_out == False + """ + return self.status == 'accepted' and not self.checked_out @property def can_be_rejected(self): @@ -274,108 +297,171 @@ def can_be_rejected(self): """ return self.status == 'review' + # ########### # + # VALIDATIONS # + # ########### # + + def _do_basic_validation(self, responsible, action): + """ + Do some general validations required to modify a checkin. + Return a tuple: + - (True, None) if validation is successful + - (False, "Error message") if not. + + Will not be valid when: + - exist any accepted article already, or + - the user `responsible` is not active or + - the user `responsible` dont belong to the corresponding auth.group (depends on `action`) + :param responsible: instance of django.contrib.auth.User + :param action: could be: + ['accept', 'reject', 'review_l1', 'review_l2', 'send_to_review', 'send_to_pending'] + """ + if not responsible.is_active: + return (False, 'User must be active') + + profile = responsible.get_profile() + + if action == 'accept' and not profile.can_accept_checkins: + return (False, 'User can\'t ACCEPT checkins, because doesn\'t have enough permissions') + elif action == 'reject' and not profile.can_reject_checkins: + return (False, 'User can\'t REJECT checkins, because doesn\'t have enough permissions') + elif action == 'review_l1' and not profile.can_review_l1_checkins: + return (False, 'User can\'t REVIEW (Level 1) checkins, because doesn\'t have enough permissions') + elif action == 'review_l2' and not profile.can_review_l2_checkins: + return (False, 'User can\'t REVIEW (Level 2) checkins, because doesn\'t have enough permissions') + elif action == 'send_to_review' and not profile.can_send_checkins_to_review: + return (False, 'User can\'t SEND checkins TO REVIEW, because doesn\'t have enough permissions') + elif action == 'send_to_pending' and not profile.can_send_checkins_to_pending: + return (False, 'User can\'t SEND checkins TO PENDING, because doesn\'t have enough permissions') + + if self.article.is_accepted(): + return (False, 'Can\'t accept more than one checkin per article') + + return (True, None) + + def _do_review_validation(self, responsible, action): + """ + Do some validations required to do a checkin review. + Return a tuple: + - (True, None) if validation is successful + - (False, "Error message") if not: + + if the user `responsible` is not active or if exist any accepted article already, or + :param responsible: instance of django.contrib.auth.User + :param action: could be: + ['review_l1', 'review_l2'] + """ + + if not self.can_be_reviewed: + return (False, 'This checkin does not comply with the conditions to be reviewed') + + return self._do_basic_validation(responsible, action) + + # ####### # + # ACTIONS # + # ####### # + @log_workflow_status(MSG_WORKFLOW_ACCEPTED) def accept(self, responsible): """ Accept the checkin as ready to be part of the collection. Change status of this checkin from 'review' to 'accepted'. - Raises ValueError if self relates to an already accepted article or - if the user `responsible` is not active or if exist any accepted article already. + Raises ValueError if don't comply with required validations. :param responsible: instance of django.contrib.auth.User """ - if not responsible.is_active: - raise ValueError('User must be active') + is_valid, errors = self._do_basic_validation(responsible, 'accept') - if self.article.is_accepted(): - raise ValueError('Cannot accept more than one checkin per article') + if not is_valid: + raise ValueError(errors) elif self.can_be_accepted: self.accepted_by = responsible self.accepted_at = datetime.datetime.now() self.status = 'accepted' self.save() else: - raise ValueError('This checkin does not comply with the conditions to be accepted') + raise ValueError('This checkin do not comply with the conditions to be accepted') @log_workflow_status(MSG_WORKFLOW_SENT_TO_PENDING) def send_to_pending(self, responsible): """ - Send to pending list: change the status to 'pending' if self.can_be_send_to_pending == True else raise - ValueError. + Send to pending list (change the status to 'pending'). - Raises ValueError if self relates to an already accepted article or - if the user `responsible` is not active or if exist any accepted article already. + Raises ValueError if don't comply with required validations. :param responsible: instance of django.contrib.auth.User """ - if not responsible.is_active: - raise ValueError('User must be active') - - if self.article.is_accepted(): - raise ValueError('Cannot accept more than one checkin per article') + is_valid, errors = self._do_basic_validation(responsible, 'send_to_pending') + if not is_valid: + raise ValueError(errors) elif self.can_be_send_to_pending: self.status = 'pending' self.save() else: - raise ValueError('This checkin does not comply with the conditions to change status to "review"') + raise ValueError('This checkin do not comply with the conditions to be moved to pending list') @log_workflow_status(MSG_WORKFLOW_SENT_TO_REVIEW) def send_to_review(self, responsible): """ - Send to review list: change the status to review if self.can_be_send_to_review == True else raise - ValueError. + Send to review list (change the status to 'review') - Raises ValueError if self relates to an already accepted article or - if the user `responsible` is not active or if exist any accepted article already. + Raises ValueError if don't comply with required validations. :param responsible: instance of django.contrib.auth.User """ - if not responsible.is_active: - raise ValueError('User must be active') - - if self.article.is_accepted(): - raise ValueError('Cannot accept more than one checkin per article') + is_valid, errors = self._do_basic_validation(responsible, 'send_to_review') + if not is_valid: + raise ValueError(errors) elif self.can_be_send_to_review: self.status = 'review' self.save() else: - raise ValueError('This checkin does not comply with the conditions to change status to "review"') + raise ValueError('This checkin does not comply with the conditions to be moved to review list') - @log_workflow_status(MSG_WORKFLOW_REVIEWED) - def do_review(self, responsible): + @log_workflow_status(MSG_WORKFLOW_REVIEWED_QAL1) + def do_review_by_level_1(self, responsible): """ - Checkin with status review, are filled with review information (saves reviewer and revisition data) + Checkin with status review, are filled with review information (reviewed_by and reviewed_at) - Raises ValueError if self relates to an already accepted article or - if the user `responsible` is not active or if exist any accepted article already. + Raises ValueError if don't comply with required validations. :param responsible: instance of django.contrib.auth.User """ - if not responsible.is_active: - raise ValueError('User must be active') - - if self.article.is_accepted(): - raise ValueError('Cannot accept more than one checkin per article') - elif self.can_be_reviewed: + is_valid, errors = self._do_review_validation(responsible, 'review_l1') + if is_valid: self.status = 'review' self.reviewed_by = responsible self.reviewed_at = datetime.datetime.now() self.save() else: - raise ValueError('This checkin does not comply with the conditions to be reviewed') + raise ValueError(errors) + + @log_workflow_status(MSG_WORKFLOW_REVIEWED_QAL2) + def do_review_by_level_2(self, responsible): + """ + Checkin with status review, are filled with review information (scielo_reviewed_by and scielo_reviewed_at) + + Raises ValueError if don't comply with required validations. + :param responsible: instance of django.contrib.auth.User + """ + is_valid, errors = self._do_review_validation(responsible, 'review_l2') + if is_valid: + self.status = 'review' + self.scielo_reviewed_by = responsible + self.scielo_reviewed_at = datetime.datetime.now() + self.save() + else: + raise ValueError(errors) @log_workflow_status(MSG_WORKFLOW_REJECTED) def do_reject(self, responsible, reason): """ - Checkins that can_be_rejected == True, is changed to status == 'rejected' - Must be saved the date, the responsible of the action and a reason of rejection. + Reject the checkin. (change the status to 'rejected') + If can be rejected must be saved the date, the responsible of the action and a reason of rejection. - Raises ValueError if self relates to an already accepted article or - if the user `responsible` is not active or if exist any accepted article already. + Raises ValueError if don't comply with required validations. :param responsible: instance of django.contrib.auth.User """ - if not responsible.is_active: - raise ValueError('User must be active') - - if self.article.is_accepted(): - raise ValueError('Cannot accept more than one checkin per article') + is_valid, errors = self._do_basic_validation(responsible, 'reject') + if not is_valid: + raise ValueError(errors) elif self.can_be_rejected: self.status = 'rejected' self.rejected_by = responsible @@ -383,7 +469,7 @@ def do_reject(self, responsible, reason): self.rejected_cause = reason self.save() else: - raise ValueError('This checkin does not comply with the conditions to change status to "rejected"') + raise ValueError('This checkin does not comply with the conditions to be rejected') @log_workflow_status(MSG_WORKFLOW_EXPIRED) def do_expires(self, responsible=None): @@ -397,12 +483,23 @@ def do_expires(self, responsible=None): self.expiration_at = datetime.datetime.now() self.save() + @log_workflow_status(MSG_WORKFLOW_CHECKED_OUT) + def do_mark_as_checked_out(self, responsible=None): + """ + This method will be call before successfully checked out via BalaioRPC api. + Will change to True self.checked_out field only with self.status == accepted. + """ + if self.can_be_send_to_checkout: + self.checked_out = True + self.save() + def clean(self): # validation for status "accepted" if self.status == 'accepted' and not bool(self.accepted_by and self.accepted_at and self.reviewed_by and self.reviewed_at): raise ValidationError('Checkin with "accepted" status must have filled: "accepted_by", \ "accepted_at", "reviewed_by" and "reviewed_at" fields.') + # validation for status "rejected" if self.status == 'rejected' and not bool(self.rejected_by and self.rejected_at and self.rejected_cause): raise ValidationError('Checkin with "rejected" status must have filled: "rejected_by", \ "rejected_at" and "reviewed_cause" fields.') @@ -418,6 +515,8 @@ def save(self, *args, **kwargs): # clear 'review' fields self.reviewed_by = None self.reviewed_at = None + self.scielo_reviewed_by = None + self.scielo_reviewed_at = None # clear 'accepted' fields self.accepted_by = None self.accepted_at = None @@ -441,6 +540,8 @@ def save(self, *args, **kwargs): # clear 'review' fields self.reviewed_by = None self.reviewed_at = None + self.scielo_reviewed_by = None + self.scielo_reviewed_at = None # clear 'accepted' fields self.accepted_by = None self.accepted_at = None diff --git a/scielomanager/articletrack/templates/articletrack/checkin_list.html b/scielomanager/articletrack/templates/articletrack/checkin_list.html index bdb51fee..0d0d6517 100644 --- a/scielomanager/articletrack/templates/articletrack/checkin_list.html +++ b/scielomanager/articletrack/templates/articletrack/checkin_list.html @@ -12,42 +12,61 @@ -{# PENDING LIST #} -{% with status="pending" icon_class="icon-time" list_title="Pending" checkins_collection=checkins_pending pagination_prefix="pending_page" filter_form=pending_filter_form %} - {% include "articletrack/includes/checkin_list_and_filterform.html" %} -{% endwith %} - -{# REJECTED LIST #} -{% with status="rejected" icon_class="icon-eye-close" list_title="Rejected" checkins_collection=checkins_rejected pagination_prefix="rejected_page" filter_form=rejected_filter_form %} - {% include "articletrack/includes/checkin_list_and_filterform.html" %} -{% endwith %} - -{# REVIEW LIST #} -{% with status="review" icon_class="icon-eye-open" list_title="Review" checkins_collection=checkins_review pagination_prefix="review_page" filter_form=review_filter_form %} - {% include "articletrack/includes/checkin_list_and_filterform.html" %} -{% endwith %} - -{# ACCEPTED LIST #} -{% with status="accepted" icon_class="icon-ok-circle" list_title="Accepted" checkins_collection=checkins_accepted pagination_prefix="accepted_page" filter_form=accepted_filter_form %} - {% include "articletrack/includes/checkin_list_and_filterform.html" %} -{% endwith %} + +
+
+ {# PENDING LIST #} + {% with status="pending" icon_class="icon-time" list_title="Pending" checkins_collection=checkins_pending pagination_prefix="pending_page" filter_form=pending_filter_form %} + {% include "articletrack/includes/checkin_list_and_filterform.html" %} + {% endwith %} +
+
+ {# REJECTED LIST #} + {% with status="rejected" icon_class="icon-eye-close" list_title="Rejected" checkins_collection=checkins_rejected pagination_prefix="rejected_page" filter_form=rejected_filter_form %} + {% include "articletrack/includes/checkin_list_and_filterform.html" %} + {% endwith %} +
+
+ {# REVIEW LIST #} + {% with status="review" icon_class="icon-eye-open" list_title="Review" checkins_collection=checkins_review pagination_prefix="review_page" filter_form=review_filter_form %} + {% include "articletrack/includes/checkin_list_and_filterform.html" %} + {% endwith %} +
+
+ {# ACCEPTED LIST #} + {% with status="accepted" icon_class="icon-ok-circle" list_title="Accepted" checkins_collection=checkins_accepted pagination_prefix="accepted_page" filter_form=accepted_filter_form %} + {% include "articletrack/includes/checkin_list_and_filterform.html" %} + {% endwith %} +
+
{% endblock %} {% block extrafooter %} @@ -77,4 +96,4 @@ }); -{% endblock extrafooter %} \ No newline at end of file +{% endblock extrafooter %} diff --git a/scielomanager/articletrack/templates/articletrack/includes/article_and_checkin_information.html b/scielomanager/articletrack/templates/articletrack/includes/article_and_checkin_information.html index 5aea8ccc..300aedf4 100644 --- a/scielomanager/articletrack/templates/articletrack/includes/article_and_checkin_information.html +++ b/scielomanager/articletrack/templates/articletrack/includes/article_and_checkin_information.html @@ -39,6 +39,12 @@

{% trans "Check-in Information" %}:

{% endif %} {{ checkin.get_status_display }} + {% if checkin.status == 'accepted' and checkin.checked_out %} +
+
{% trans "Checked out" %}
+
+ + {% endif %}
{% trans "Package name" %}:
{{ checkin.package_name }}
{% trans "Updated at" %}:
@@ -63,6 +69,15 @@

{% trans "Check-in Information" %}:

{{ checkin.reviewed_at|date:"d/m/Y - H:i" }}
{% endif %} + {%if checkin.scielo_reviewed_by %} +
SciELO review:
+
 
+
{% trans "Reviewed by" %}:
+
{{ checkin.scielo_reviewed_by.get_full_name|default:checkin.scielo_reviewed_by }}
+
{% trans "Reviewed at" %}:
+
{{ checkin.scielo_reviewed_at|date:"d/m/Y - H:i" }}
+ {% endif %} + {% endif %} {% if checkin.is_accepted %} diff --git a/scielomanager/articletrack/templates/articletrack/includes/checkin_list_and_filterform.html b/scielomanager/articletrack/templates/articletrack/includes/checkin_list_and_filterform.html index af494ee6..9fdf752a 100644 --- a/scielomanager/articletrack/templates/articletrack/includes/checkin_list_and_filterform.html +++ b/scielomanager/articletrack/templates/articletrack/includes/checkin_list_and_filterform.html @@ -2,22 +2,18 @@ {% load pagination_tags %} {% load trans_status %} +{% with filter_form as form %} + {% include "articletrack/includes/filter_form.html" %} +{% endwith %} +
-
- -

{% trans list_title %}:

-
-
+
{% simple_pagination checkins_collection pagination_prefix %}
-{% with filter_form as form %} - {% include "articletrack/includes/filter_form.html" %} -{% endwith %} - @@ -87,4 +83,4 @@

{% trans list_title %}:

-
\ No newline at end of file +
diff --git a/scielomanager/articletrack/templates/articletrack/includes/checkin_status_notice_block.html b/scielomanager/articletrack/templates/articletrack/includes/checkin_status_notice_block.html index ae48e9ea..6916131b 100644 --- a/scielomanager/articletrack/templates/articletrack/includes/checkin_status_notice_block.html +++ b/scielomanager/articletrack/templates/articletrack/includes/checkin_status_notice_block.html @@ -6,7 +6,7 @@

{% trans "REJECTED" %}:

- {% trans "This checkin is rejected by" %} + {% trans "This checkin was rejected by" %} {% with checkin.rejected_by as user %} {% include "articletrack/includes/gravatar_tooltip.html" %} {% endwith %} @@ -44,7 +44,7 @@

{% trans "THIS CHECKIN WILL EXPIRE TODAY" %}: {% trans "at" %} {{ checkin.expiration_at|date:"d/m/Y - H:i" }}

- {% trans "Before that date, the checkin will be unreachable." %} + {% trans "After that date, the checkin will be unreachable." %}

diff --git a/scielomanager/articletrack/templates/articletrack/notice_detail.html b/scielomanager/articletrack/templates/articletrack/notice_detail.html index ccaac3a0..c02a9550 100644 --- a/scielomanager/articletrack/templates/articletrack/notice_detail.html +++ b/scielomanager/articletrack/templates/articletrack/notice_detail.html @@ -20,58 +20,88 @@ {% trans "List of Checkins" %}
  • - {% if not checkin.is_accepted %} - +
  • + {% endif %}
  • {% trans "History" %} @@ -119,7 +149,7 @@

    {% trans 'Notices' %}:

    {% trans "System logs" %}
  • -
  • +
  • {% trans "Style Checking" %} {% if not xml_data.can_be_analyzed.0 %} @@ -215,7 +245,7 @@

    {% trans "Related tickets" %}:

    {% trans "Closed" %} {{ closed_tickets|length }}
  • - {% if perms.ticket.can_add %} + {% if perms.articletrack.add_ticket %}
  • {% trans "Create a ticket" %} diff --git a/scielomanager/articletrack/templates/articletrack/ticket_detail.html b/scielomanager/articletrack/templates/articletrack/ticket_detail.html index 7b88bcb9..bd1cd056 100644 --- a/scielomanager/articletrack/templates/articletrack/ticket_detail.html +++ b/scielomanager/articletrack/templates/articletrack/ticket_detail.html @@ -65,7 +65,7 @@

    - {% if perms.ticket.can_change %} + {% if perms.articletrack.change_ticket %}
    @@ -103,12 +103,12 @@



    -
    +

    {% trans "Comments" %}:

    -
    +
    - {% if ticket.is_open and perms.comment.can_add %} + {% if ticket.is_open and perms.articletrack.add_comment %} {% trans "Add Comment" %} diff --git a/scielomanager/articletrack/templates/email/checkin_reviewed.txt b/scielomanager/articletrack/templates/email/checkin_reviewed.txt new file mode 100644 index 00000000..bc3ca65b --- /dev/null +++ b/scielomanager/articletrack/templates/email/checkin_reviewed.txt @@ -0,0 +1,4 @@ +Your article was reviewed {% if checkin.is_level2_reviewed %} by SciELO team {% endif %} +
    +
    +Check: http://{{domain}}{% url notice_detail checkin.id %} diff --git a/scielomanager/articletrack/tests/tests_forms.py b/scielomanager/articletrack/tests/tests_forms.py index 43f6fc4b..7413222c 100644 --- a/scielomanager/articletrack/tests/tests_forms.py +++ b/scielomanager/articletrack/tests/tests_forms.py @@ -1,21 +1,16 @@ # coding:utf-8 -import os -import unittest from random import randint from django_webtest import WebTest from django.core.urlresolvers import reverse from django_factory_boy import auth -from django.test import TestCase from waffle import Flag from journalmanager.tests.modelfactories import CollectionFactory from articletrack.tests import modelfactories -from articletrack import forms -from articletrack import models - +from articletrack.tests.tests_models import create_notices MAX_CHOICES_QTY = 10 @@ -61,6 +56,20 @@ def setUp(self): self.user.user_permissions.add(perm) self.collection = CollectionFactory.create() self.collection.add_user(self.user, is_manager=True) + # Producer definition: + group_producer = auth.GroupF(name='producer') + self.user.groups.add(group_producer) + self.user.save() + # QAL 1 definitions + self.user_qal_1 = auth.UserF(is_active=True) + group_qal_1 = auth.GroupF(name='QAL1') + self.user_qal_1.groups.add(group_qal_1) + self.user_qal_1.save() + # QAL 2 definitions + self.user_qal_2 = auth.UserF(is_active=True) + group_qal_2 = auth.GroupF(name='QAL2') + self.user_qal_2.groups.add(group_qal_2) + self.user_qal_2.save() def tearDown(self): """ @@ -82,14 +91,24 @@ def _make_N_checkins(self, start=0, end=MAX_CHOICES_QTY, checkins_status='pendin for journal in new_checkin.article.journals.all(): journal.join(self.collection, self.user) + create_notices('ok', new_checkin) # all notices have error_level as: ok, to proceed if checkins_status == 'review' or checkins_status == 'rejected' or checkins_status == 'accepted': + self.assertTrue(self.user.get_profile().can_send_checkins_to_review) new_checkin.send_to_review(self.user) - new_checkin.do_review(self.user) + # do review by QAL1 + self.assertTrue(self.user_qal_1.get_profile().can_review_l1_checkins) + new_checkin.do_review_by_level_1(self.user_qal_1) + # do review by QAL2 + self.assertTrue(self.user_qal_2.get_profile().can_review_l2_checkins) + new_checkin.do_review_by_level_2(self.user_qal_2) + if checkins_status == 'rejected': rejection_text = 'your checkin is bad, and you should feel bad!' - new_checkin.do_reject(self.user, rejection_text) + self.assertTrue(self.user_qal_1.get_profile().can_reject_checkins) + new_checkin.do_reject(self.user_qal_1, rejection_text) if checkins_status == 'accepted': - new_checkin.accept(self.user) + self.assertTrue(self.user_qal_2.get_profile().can_accept_checkins) + new_checkin.accept(self.user_qal_2) checkins.append(new_checkin) @@ -313,6 +332,7 @@ def tests_filter_accepted_by_article(self): (a random existing) article """ checkins = self._make_N_checkins(checkins_status='accepted') + random_index = randint(0, len(checkins) - 1) target_checkin = checkins[random_index] @@ -356,7 +376,7 @@ def tests_filter_non_existing_pending_by_package_name(self): Creates various (pending) checkins and apply a filter by (a random existing) package name """ - checkins = self._make_N_checkins() + self._make_N_checkins() target_package_name = '9999.zip' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_pending'] @@ -373,7 +393,7 @@ def tests_filter_non_existing_pending_by_issue_label(self): Creates various (pending) checkins and apply a filter by (a random existing) issue label """ - checkins = self._make_N_checkins() + self._make_N_checkins() target_issue_label = '1999 v.99 n.9' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_pending'] @@ -382,6 +402,9 @@ def tests_filter_non_existing_pending_by_issue_label(self): response_partial = _extract_results_from_body(response) context_checkins = response.context['checkins_pending'].object_list + self.assertEqual(len(context_checkins), 0) + self.assertFalse(u'%s' % target_issue_label in response_partial) + # Review: def tests_filter_non_existing_review_by_package_name(self): @@ -389,7 +412,7 @@ def tests_filter_non_existing_review_by_package_name(self): Creates various (review) checkins and apply a filter by (a random existing) package name """ - checkins = self._make_N_checkins(checkins_status='review') + self._make_N_checkins(checkins_status='review') target_package_name = '9999.zip' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_review'] @@ -406,7 +429,7 @@ def tests_filter_non_existing_review_by_issue_label(self): Creates various (review) checkins and apply a filter by (a random existing) issue label """ - checkins = self._make_N_checkins(checkins_status='review') + self._make_N_checkins(checkins_status='review') target_issue_label = '1999 v.99 n.9' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_review'] @@ -415,6 +438,9 @@ def tests_filter_non_existing_review_by_issue_label(self): response_partial = _extract_results_from_body(response, extract_section='review') context_checkins = response.context['checkins_review'].object_list + self.assertEqual(len(context_checkins), 0) + self.assertFalse(u'%s' % target_issue_label in response_partial) + # Rejected: def tests_filter_non_existing_rejected_by_package_name(self): @@ -422,7 +448,7 @@ def tests_filter_non_existing_rejected_by_package_name(self): Creates various (rejected) checkins and apply a filter by (a random existing) package name """ - checkins = self._make_N_checkins(checkins_status='rejected') + self._make_N_checkins(checkins_status='rejected') target_package_name = '9999.zip' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_rejected'] @@ -439,7 +465,7 @@ def tests_filter_non_existing_rejected_by_issue_label(self): Creates various (rejected) checkins and apply a filter by (a random existing) issue label """ - checkins = self._make_N_checkins(checkins_status='rejected') + self._make_N_checkins(checkins_status='rejected') target_issue_label = '1999 v.99 n.9' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_rejected'] @@ -448,6 +474,9 @@ def tests_filter_non_existing_rejected_by_issue_label(self): response_partial = _extract_results_from_body(response, extract_section='rejected') context_checkins = response.context['checkins_rejected'].object_list + self.assertEqual(len(context_checkins), 0) + self.assertFalse(u'%s' % target_issue_label in response_partial) + # accepted: def tests_filter_non_existing_accepted_by_package_name(self): @@ -455,7 +484,7 @@ def tests_filter_non_existing_accepted_by_package_name(self): Creates various (accepted) checkins and apply a filter by (a random existing) package name """ - checkins = self._make_N_checkins(checkins_status='accepted') + self._make_N_checkins(checkins_status='accepted') target_package_name = '9999.zip' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_accepted'] @@ -472,7 +501,7 @@ def tests_filter_non_existing_accepted_by_issue_label(self): Creates various (accepted) checkins and apply a filter by (a random existing) issue label """ - checkins = self._make_N_checkins(checkins_status='accepted') + self._make_N_checkins(checkins_status='accepted') target_issue_label = '1999 v.99 n.9' page = self.app.get(reverse('checkin_index',), user=self.user) form = page.forms['filter_accepted'] diff --git a/scielomanager/articletrack/tests/tests_models.py b/scielomanager/articletrack/tests/tests_models.py index ad6d79e4..33370954 100644 --- a/scielomanager/articletrack/tests/tests_models.py +++ b/scielomanager/articletrack/tests/tests_models.py @@ -7,6 +7,31 @@ from . import modelfactories from scielomanager.utils import misc +def create_notices(expected_error_level, checkin): + """ + To test some checkin's actions, such as: review, accept, reject, + is required that the checkin, return an expected error level. + So, this function helps, creating Notices objects to retrun + that expected checkin.get_error_level. + """ + serv_status_count = models.SERVICE_STATUS_MAX_STAGES + for step in xrange(0, serv_status_count): + # SERV BEGIN + modelfactories.NoticeFactory( + checkin=checkin, + stage=" ", message=" ", status="SERV_BEGIN", + created_at=datetime.datetime.now()) + # EXPECTED NOTICE + modelfactories.NoticeFactory( + checkin=checkin, + stage=" ", message=" ", status=expected_error_level, + created_at=datetime.datetime.now()) + # SERV BEGIN + modelfactories.NoticeFactory( + checkin=checkin, + stage=" ", message=" ", status="SERV_END", + created_at=datetime.datetime.now()) + class CommentTests(TestCase): @@ -32,8 +57,6 @@ def test_add_member(self): self.assertEqual(team.member.all()[0].username, user.username) -class CheckinTests(TestCase): - def test_team(self): user = auth.UserF(is_active=True) team = modelfactories.TeamFactory(name=u'Team Alcachofra') @@ -68,6 +91,25 @@ def test_team_members(self): self.assertTrue(user2.username in users) self.assertFalse(user3.username in users) +class CheckinTests(TestCase): + + def setUp(self): + self.user = auth.UserF(is_active=True) + # Producer definition: + group_producer = auth.GroupF(name='producer') + self.user.groups.add(group_producer) + self.user.save() + # QAL 1 definitions + self.user_qal_1 = auth.UserF(is_active=True) + group_qal_1 = auth.GroupF(name='QAL1') + self.user_qal_1.groups.add(group_qal_1) + self.user_qal_1.save() + # QAL 2 definitions + self.user_qal_2 = auth.UserF(is_active=True) + self.group_qal_2 = auth.GroupF(name='QAL2') + self.user_qal_2.groups.add(self.group_qal_2) + self.user_qal_2.save() + def test_new_checkin_is_pending_and_clear(self): checkin = modelfactories.CheckinFactory() @@ -75,9 +117,16 @@ def test_new_checkin_is_pending_and_clear(self): self.assertIsNone(checkin.rejected_by) self.assertIsNone(checkin.rejected_at) self.assertIsNone(checkin.rejected_cause) + self.assertFalse(checkin.is_rejected) # reviewd_* is clear self.assertIsNone(checkin.reviewed_by) self.assertIsNone(checkin.reviewed_at) + self.assertIsNone(checkin.scielo_reviewed_by) + self.assertIsNone(checkin.scielo_reviewed_at) + # is_*_reviewed must be false + self.assertFalse(checkin.is_level1_reviewed) + self.assertFalse(checkin.is_level2_reviewed) + self.assertFalse(checkin.is_full_reviewed) # accepted_* is clear self.assertIsNone(checkin.accepted_by) self.assertIsNone(checkin.accepted_at) @@ -85,33 +134,35 @@ def test_new_checkin_is_pending_and_clear(self): self.assertEqual(checkin.status, 'pending') def test_reject_workflow_simple(self): - - user = auth.UserF(is_active=True) checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) + rejection_text = 'your checkin is bad, and you should feel bad!' # http://www.quickmeme.com/Zoidberg-you-should-feel-bad/?upcoming # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user) + checkin.send_to_review(self.user) # can be reviewed and can be rejected, then reject self.assertTrue(checkin.can_be_reviewed) self.assertTrue(checkin.can_be_rejected) - checkin.do_reject(user, rejection_text) + checkin.do_reject(self.user_qal_1, rejection_text) # check status and Integrity self.assertEqual(checkin.status, 'rejected') - self.assertEqual(checkin.rejected_by, user) + self.assertEqual(checkin.rejected_by, self.user_qal_1) self.assertIsNotNone(checkin.rejected_at) # fields related with review and accept, must be clear self.assertIsNone(checkin.reviewed_by) self.assertIsNone(checkin.reviewed_at) + self.assertIsNone(checkin.scielo_reviewed_by) + self.assertIsNone(checkin.scielo_reviewed_at) self.assertIsNone(checkin.accepted_by) self.assertIsNone(checkin.accepted_at) # the checkin is not pending, reviewed, or accepted - self.assertFalse(checkin.is_reviewed) + self.assertFalse(checkin.is_full_reviewed) self.assertFalse(checkin.is_accepted) self.assertFalse(checkin.can_be_accepted) self.assertFalse(checkin.can_be_reviewed) @@ -121,80 +172,106 @@ def test_reject_workflow_simple(self): self.assertEqual(checkin.rejected_cause, rejection_text) def test_accept_workflow_simple(self): - user = auth.UserF(is_active=True) checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user) + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) - # do review + # do review by QAL2 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(user) + checkin.do_review_by_level_2(self.user_qal_2) # do accept self.assertTrue(checkin.can_be_accepted) - checkin.accept(user) + checkin.accept(self.user_qal_2) # fields related with review and accept, must be clear - self.assertEqual(checkin.accepted_by, user) + self.assertEqual(checkin.accepted_by, self.user_qal_2) self.assertIsNotNone(checkin.accepted_at) - self.assertEqual(checkin.reviewed_by, user) + self.assertEqual(checkin.reviewed_by, self.user_qal_1) + self.assertEqual(checkin.scielo_reviewed_by, self.user_qal_2) self.assertIsNotNone(checkin.reviewed_at) + self.assertIsNotNone(checkin.scielo_reviewed_at) # checkin must be accepted self.assertTrue(checkin.is_accepted) def test_accept_raises_ValueError_when_already_accepted(self): - user = auth.UserF(is_active=True) checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user) + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) - # do review + # do review by QAL2 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(user) + checkin.do_review_by_level_2(self.user_qal_2) # do accept self.assertTrue(checkin.can_be_accepted) - checkin.accept(user) + checkin.accept(self.user_qal_2) - self.assertRaises(ValueError, lambda: checkin.accept(user)) + self.assertRaises(ValueError, lambda: checkin.accept(self.user_qal_2)) def test_accept_raises_ValueError_when_user_is_inactive(self): - active_user = auth.UserF.build() - inactive_user = auth.UserF.build(is_active=False) checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) + + # users + active_user = auth.UserF(is_active=True) + active_user.groups.add(self.group_qal_2) + active_user.save() + + inactive_user = auth.UserF(is_active=False) + inactive_user.groups.add(self.group_qal_2) + inactive_user.save() # send to review self.assertTrue(checkin.can_be_send_to_review) checkin.send_to_review(active_user) - # do review + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) + + # do review by QAL2 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(active_user) + checkin.do_review_by_level_2(self.user_qal_2) # do accept self.assertTrue(checkin.can_be_accepted) self.assertRaises(ValueError, lambda: checkin.accept(inactive_user)) def test_is_accepted_method_with_accepted_checkin(self): - user = auth.UserF(is_active=True) checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user) + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) - # do review + # do review by QAL2 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(user) + checkin.do_review_by_level_2(self.user_qal_2) # do accept self.assertTrue(checkin.can_be_accepted) - checkin.accept(user) + checkin.accept(self.user_qal_2) self.assertTrue(checkin.is_accepted) def test_is_accepted_method_without_accepted_checkin(self): @@ -292,6 +369,23 @@ class CheckinWorkflowLogTests(TestCase): This way is possible to audit the actions made with the related checkin """ + def setUp(self): + self.user = auth.UserF(is_active=True) + # Producer definition: + group_producer = auth.GroupF(name='producer') + self.user.groups.add(group_producer) + self.user.save() + # QAL 1 definitions + self.user_qal_1 = auth.UserF(is_active=True) + group_qal_1 = auth.GroupF(name='QAL1') + self.user_qal_1.groups.add(group_qal_1) + self.user_qal_1.save() + # QAL 2 definitions + self.user_qal_2 = auth.UserF(is_active=True) + self.group_qal_2 = auth.GroupF(name='QAL2') + self.user_qal_2.groups.add(self.group_qal_2) + self.user_qal_2.save() + def test_checkinworkflowlog_ordering(self): ordering = models.CheckinWorkflowLog._meta.ordering self.assertEqual(ordering, ['created_at']) @@ -306,105 +400,127 @@ def test_new_checkin_no_log(self): def test_checkin_send_to_review_log(self): checkin = modelfactories.CheckinFactory() - user = auth.UserF(is_active=True) + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user) + checkin.send_to_review(self.user) - logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=user) + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user) self.assertEqual(logs.count(), 1) - self.assertEqual(logs[0].user, user) + self.assertEqual(logs[0].user, self.user) self.assertEqual(logs[0].description, models.MSG_WORKFLOW_SENT_TO_REVIEW) - def test_checkin_do_review_log(self): + def test_checkin_do_review_level1_log(self): checkin = modelfactories.CheckinFactory() - user_send_to_review = auth.UserF(is_active=True) - user_review = auth.UserF(is_active=True) + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user_send_to_review) + checkin.send_to_review(self.user) - # do review + # do review by QAL1 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(user_review) + checkin.do_review_by_level_1(self.user_qal_1) - logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=user_review) + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user_qal_1) self.assertEqual(logs.count(), 1) - self.assertEqual(logs[0].user, user_review) - self.assertEqual(logs[0].description, models.MSG_WORKFLOW_REVIEWED) + self.assertEqual(logs[0].user, self.user_qal_1) + self.assertEqual(logs[0].description, models.MSG_WORKFLOW_REVIEWED_QAL1) + + def test_checkin_do_review_level2_log(self): + checkin = modelfactories.CheckinFactory() + create_notices('ok', checkin) + + # send to review + self.assertTrue(checkin.can_be_send_to_review) + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) + + # do review by QAL2 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_2(self.user_qal_2) + + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user_qal_2) + + self.assertEqual(logs.count(), 1) + self.assertEqual(logs[0].user, self.user_qal_2) + self.assertEqual(logs[0].description, models.MSG_WORKFLOW_REVIEWED_QAL2) def test_checkin_do_accept_log(self): checkin = modelfactories.CheckinFactory() - user_send_to_review = auth.UserF(is_active=True) - user_review = auth.UserF(is_active=True) - user_accept = auth.UserF(is_active=True) + create_notices('ok', checkin) # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user_send_to_review) + checkin.send_to_review(self.user) - # do review + # do review by QAL1 self.assertTrue(checkin.can_be_reviewed) - checkin.do_review(user_review) + checkin.do_review_by_level_1(self.user_qal_1) + + # do review by QAL2 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_2(self.user_qal_2) # do accept self.assertTrue(checkin.can_be_accepted) - checkin.accept(user_accept) + checkin.accept(self.user_qal_2) - logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=user_accept) + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user_qal_2) self.assertEqual(logs.count(), 1) - self.assertEqual(logs[0].user, user_accept) + self.assertEqual(logs[0].user, self.user_qal_2) self.assertEqual(logs[0].description, models.MSG_WORKFLOW_ACCEPTED) def test_checkin_do_reject_log(self): checkin = modelfactories.CheckinFactory() - user_send_to_review = auth.UserF(is_active=True) - user_reject = auth.UserF(is_active=True) + create_notices('ok', checkin) + rejection_text = 'your checkin is bad, and you should feel bad!' # http://www.quickmeme.com/Zoidberg-you-should-feel-bad/?upcoming # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user_send_to_review) + checkin.send_to_review(self.user) # do reject self.assertTrue(checkin.can_be_rejected) - checkin.do_reject(user_reject, rejection_text) + checkin.do_reject(self.user_qal_1, rejection_text) - logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=user_reject) + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user_qal_1) self.assertEqual(logs.count(), 1) - self.assertEqual(logs[0].user, user_reject) + self.assertEqual(logs[0].user, self.user_qal_1) expected_description = "%s - Reason: %s" % (models.MSG_WORKFLOW_REJECTED, checkin.rejected_cause) self.assertEqual(logs[0].description, expected_description) def test_checkin_send_to_pending_log(self): checkin = modelfactories.CheckinFactory() - user1_send_to_review = auth.UserF(is_active=True) - user_reject = auth.UserF(is_active=True) - user2_send_to_review = auth.UserF(is_active=True) + create_notices('ok', checkin) + rejection_text = 'your checkin is bad, and you should feel bad!' # http://www.quickmeme.com/Zoidberg-you-should-feel-bad/?upcoming # send to review self.assertTrue(checkin.can_be_send_to_review) - checkin.send_to_review(user1_send_to_review) + checkin.send_to_review(self.user) # do reject self.assertTrue(checkin.can_be_rejected) - checkin.do_reject(user_reject, rejection_text) + checkin.do_reject(self.user_qal_1, rejection_text) # send to pending self.assertTrue(checkin.can_be_send_to_pending) - checkin.send_to_pending(user2_send_to_review) + checkin.send_to_pending(self.user_qal_2) - logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=user2_send_to_review) + logs = models.CheckinWorkflowLog.objects.filter(checkin=checkin, status=checkin.status, user=self.user_qal_2) self.assertEqual(logs.count(), 1) - self.assertEqual(logs[0].user, user2_send_to_review) + self.assertEqual(logs[0].user, self.user_qal_2) self.assertEqual(logs[0].description, models.MSG_WORKFLOW_SENT_TO_PENDING) def test_do_expires_generate_log_entry(self): @@ -473,11 +589,11 @@ def test_checkin_notices_with_correct_pair_of_service_status_is_completed(self): checkin = modelfactories.CheckinFactory() serv_status_count = models.SERVICE_STATUS_MAX_STAGES for step in xrange(0, serv_status_count): - notice_serv_begin = modelfactories.NoticeFactory( + modelfactories.NoticeFactory( checkin=checkin, stage=" ", message=" ", status="SERV_BEGIN", created_at=datetime.datetime.now()) - notice_serv_end = modelfactories.NoticeFactory( + modelfactories.NoticeFactory( checkin=checkin, stage=" ", message=" ", status="SERV_END", created_at=datetime.datetime.now()) diff --git a/scielomanager/articletrack/tests/tests_pages.py b/scielomanager/articletrack/tests/tests_pages.py index 19f6a7ce..8d67dfc5 100644 --- a/scielomanager/articletrack/tests/tests_pages.py +++ b/scielomanager/articletrack/tests/tests_pages.py @@ -2,6 +2,7 @@ from waffle import Flag from os import path import mocker +import lxml from django_webtest import WebTest from django_factory_boy import auth @@ -9,11 +10,9 @@ from django.template import defaultfilters as filters from django.conf import settings -import mocker - -from . import modelfactories from journalmanager.tests.modelfactories import UserFactory, CollectionFactory -from articletrack.balaio import BalaioAPI +from articletrack.tests.tests_models import create_notices +from . import modelfactories from . import doubles @@ -129,24 +128,48 @@ def test_checkin_history_must_show_all_workflowlogs(self): """ self._addWaffleFlag() checkin = self._makeOne() + create_notices('ok', checkin) rejection_text = 'your checkin is bad, and you should feel bad!' + # users + group_producer = auth.GroupF(name='producer') + self.user.groups.add(group_producer) + self.user.save() + # QAL 1 definitions + user_qal_1 = auth.UserF(is_active=True) + group_qal_1 = auth.GroupF(name='QAL1') + user_qal_1.groups.add(group_qal_1) + user_qal_1.save() + # QAL 2 definitions + user_qal_2 = auth.UserF(is_active=True) + group_qal_2 = auth.GroupF(name='QAL2') + user_qal_2.groups.add(group_qal_2) + user_qal_2.save() + # send to review checkin.send_to_review(self.user) # reject - checkin.do_reject(self.user, rejection_text) + checkin.do_reject(user_qal_1, rejection_text) # send to pending - checkin.send_to_pending(self.user) + checkin.send_to_pending(user_qal_2) # send to review checkin.send_to_review(self.user) - # do review - checkin.do_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(user_qal_1) + + # do review by QAL2 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_2(user_qal_2) + # do accept - checkin.accept(self.user) + self.assertTrue(checkin.can_be_accepted) + checkin.accept(user_qal_2) response = self.app.get(reverse('checkin_history', args=[checkin.pk]), user=self.user) logs = checkin.submission_log.all() - self.assertEqual(6, len(logs)) + self.assertEqual(7, len(logs)) for log in logs: creation_date_formatted = filters.date(log.created_at, settings.DATETIME_FORMAT) @@ -389,3 +412,419 @@ def get_xml_uri(self, attempt_id, target_name): self.assertEqual(xml_data['file_name'], expected_response['filename']) self.assertIsNone(xml_data['validation_errors']) + def test_annotations_of_syntax_error(self): + self._addWaffleFlag() + notice = self._makeOne() + + target_xml = "with_syntax_error.xml" + expected_response = { + "filename": "1415-4757-gmb-37-0210.xml", + "uri": self._get_path_of_test_xml(target_xml) + } + + class BalaioTest(doubles.BalaioAPIDouble): + def get_xml_uri(self, attempt_id, target_name): + return expected_response['uri'] + + balaio = self.mocker.replace('articletrack.balaio.BalaioAPI') + balaio() + self.mocker.result(BalaioTest()) + + syntax_error_data = { + 'message': u'Premature end of data in tag xml line 1, line 1, column 6', + 'line': 1, + 'column': 6, + 'code': 77, + } + + XML = self.mocker.replace('packtools.stylechecker.XML') + XML(expected_response['uri']) + self.mocker.throw( + lxml.etree.XMLSyntaxError( + syntax_error_data['message'], + syntax_error_data['code'], + syntax_error_data['line'], + syntax_error_data['column'], + ) + ) + + self.mocker.replay() + + response = self.app.get( + reverse('notice_detail', args=[notice.checkin.pk]), + user=self.user) + + xml_data = response.context['xml_data'] + + self.assertEqual(response.status_code, 200) + self.assertTrue(xml_data['can_be_analyzed'][0]) + self.assertIsNotNone(xml_data['annotations']) + self.assertEqual(xml_data['uri'], expected_response['uri']) + self.assertEqual(xml_data['file_name'], expected_response['filename']) + self.assertIsNotNone(xml_data['validation_errors']) + self.assertEqual('1', xml_data['validation_errors']['error_lines']) + self.assertEqual(1, len(xml_data['validation_errors']['results'])) + self.assertEqual( + xml_data['validation_errors']['results'], + [ + { + 'column': syntax_error_data['column'], + 'line': syntax_error_data['line'], + 'message': syntax_error_data['message'], + 'level': 'ERROR', + } + ] + ) + + +class CheckinWorkflowTests(WebTest, mocker.MockerTestCase): + + def _mock_balaio_disabled(self): + # to avoid making a request will replace it with a double + balaio = self.mocker.replace('articletrack.balaio.BalaioAPI') + balaio() + self.mocker.result(doubles.BalaioAPIDoubleDisabled()) + self.mocker.replay() + + def _makeOne(self): + checkin = modelfactories.CheckinFactory.create() + + # # Get only the first collection and set to the user + collection = checkin.article.journals.all()[0].collections.all()[0] + collection.add_user(self.user, is_manager=True) + collection.make_default_to_user(self.user) + + collection.add_user(self.user_qal_1, is_manager=True) + collection.make_default_to_user(self.user_qal_1) + + collection.add_user(self.user_qal_2, is_manager=True) + collection.make_default_to_user(self.user_qal_2) + + return checkin + + def _addWaffleFlag(self): + Flag.objects.create(name='articletrack', authenticated=True) + + def setUp(self): + self.user = auth.UserF(is_active=True) + self.collection = CollectionFactory.create() + + # checkin list permission + list_checkin_perm = _makePermission(perm='list_checkin', model='checkin') + # notices_detail view, requires this permission too + list_notice_perm = _makePermission(perm='list_notice', model='notice') + + # users-groups-permissions + self.group_producer = auth.GroupF(name='producer') + self.user.groups.add(self.group_producer) + self.user.save() + + self.user.user_permissions.add(list_notice_perm) + self.user.user_permissions.add(list_checkin_perm) + # QAL 1 definitions + self.user_qal_1 = auth.UserF(is_active=True) + self.group_qal_1 = auth.GroupF(name='QAL1') + self.user_qal_1.groups.add(self.group_qal_1) + self.user_qal_1.save() + + self.user_qal_1.user_permissions.add(list_notice_perm) + self.user_qal_1.user_permissions.add(list_checkin_perm) + # QAL 2 definitions + self.user_qal_2 = auth.UserF(is_active=True) + self.group_qal_2 = auth.GroupF(name='QAL2') + self.user_qal_2.groups.add(self.group_qal_2) + self.user_qal_2.save() + + self.user_qal_2.user_permissions.add(list_notice_perm) + self.user_qal_2.user_permissions.add(list_checkin_perm) + + # add user, user_qal1 and user_qal2 to default collection + self.collection.add_user(self.user) + self.collection.make_default_to_user(self.user) + # add qal1 user to collection + self.collection.add_user(self.user_qal_1) + self.collection.make_default_to_user(self.user_qal_1) + # add qal2 user to collection + self.collection.add_user(self.user_qal_2) + self.collection.make_default_to_user(self.user_qal_2) + + def tearDown(self): + """ + Restore the default values. + """ + + def test_checkin_send_to_review(self): + """ Create a checkin and call the view: checkin_send_to_review """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + + response = self.app.get(reverse('checkin_send_to_review', args=[checkin.pk, ]), user=self.user) + response = response.follow() # "checkin_send_to_review" redirects to: "notice_detail" + + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin was SENT TO REVIEW succesfully.' + self.assertIn(expected_message, response.body) + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'review') + # is not reviewed yet + self.assertFalse(response_checkin.is_level1_reviewed) + self.assertFalse(response_checkin.is_level2_reviewed) + self.assertFalse(response_checkin.is_full_reviewed) + self.assertFalse(response_checkin.can_be_send_to_review) + self.assertFalse(response_checkin.is_accepted) + # can be rejected or reviewed + self.assertTrue(response_checkin.can_be_rejected) + self.assertTrue(response_checkin.can_be_reviewed) + + def test_checkin_reject(self): + """ Create a checkin and call the view: checkin_reject """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + rejection_text = 'your checkin is bad, and you should feel bad!' + # send to review + checkin.send_to_review(self.user) + + form = self.app.get( + reverse('checkin_reject', args=[checkin.pk, ]), + user=self.user).follow().forms['checkin_reject_form'] + form['rejected_cause'] = rejection_text + response = form.submit() + response = response.follow() # "checkin_reject" redirects to: "notice_detail" + + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin REJECTED succesfully.' + self.assertIn(expected_message, response.body) + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'rejected') + + self.assertFalse(response_checkin.is_level1_reviewed) + self.assertFalse(response_checkin.is_level2_reviewed) + self.assertFalse(response_checkin.is_full_reviewed) + self.assertFalse(response_checkin.can_be_send_to_review) + self.assertFalse(response_checkin.is_accepted) + # can't be rejected or reviewed + self.assertFalse(response_checkin.can_be_rejected) + self.assertFalse(response_checkin.can_be_reviewed) + # can be sent to pending + self.assertTrue(response_checkin.can_be_send_to_pending) + + + def test_checkin_send_to_pending(self): + """ Create a checkin and call the view: checkin_send_to_pending """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + rejection_text = 'your checkin is bad, and you should feel bad!' + + # send to review + checkin.send_to_review(self.user) + # reject + checkin.do_reject(self.user_qal_1, rejection_text) + + response = self.app.get(reverse('checkin_send_to_pending', args=[checkin.pk, ]), user=self.user) + response = response.follow() # "checkin_send_to_pending" redirects to: "notice_detail" + + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin was SENT TO PENDING succesfully.' + self.assertIn(expected_message, response.body) + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'pending') + # is not reviewed or rejected or accepted yet + self.assertFalse(response_checkin.is_level1_reviewed) + self.assertFalse(response_checkin.is_level2_reviewed) + self.assertFalse(response_checkin.is_full_reviewed) + self.assertFalse(response_checkin.is_rejected) + self.assertFalse(response_checkin.is_accepted) + # can't be sent to pending again + self.assertFalse(response_checkin.can_be_send_to_pending) + # but can be send to review + self.assertTrue(response_checkin.can_be_send_to_review) + + def test_checkin_review_level1(self): + """ Create a checkin and call the view: checkin_review(level=1) """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + # send to review + checkin.send_to_review(self.user) + + # do review by QAL1 + response = self.app.get(reverse('checkin_review', args=[checkin.pk, 1]), user=self.user_qal_1) + response = response.follow() # "checkin_review" redirects to: "notice_detail" + + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin REVIEWED succesfully.' + self.assertIn(expected_message, response.body) + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'review') + # is only reviewed at level 1 + self.assertTrue(response_checkin.is_level1_reviewed) + self.assertFalse(response_checkin.is_level2_reviewed) + self.assertFalse(response_checkin.is_full_reviewed) + self.assertFalse(response_checkin.can_be_send_to_review) + self.assertFalse(response_checkin.is_accepted) + # can be rejected and reviewed too + self.assertTrue(response_checkin.can_be_rejected) + self.assertTrue(response_checkin.can_be_reviewed) + + def test_checkin_review_level2(self): + """ Create a checkin and call the view: checkin_review(level=2) """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + # send to review + checkin.send_to_review(self.user) + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) + + # mock balaio for checkout step + self._mock_balaio_disabled() + + # do review by user_qal2 + response = self.app.get(reverse('checkin_review', args=[checkin.pk, 2]), user=self.user_qal_2) + + # if ok, will try automatically to call checkin_accept + self.assertEqual(response.status_code, 302) + expected_location = reverse('checkin_accept', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # if ok, will try automatically to call checkin_send_to_checkout + response = response.follow() + expected_location = reverse('checkin_send_to_checkout', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # if ok, will try automatically to call notice_detail + response = response.follow() + expected_location = reverse('notice_detail', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # finally get response and check status + response = response.follow() + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin REVIEWED succesfully.' + self.assertIn(expected_message, response.body) + + expected_message = 'Checkin ACCEPTED succesfully.' + self.assertIn(expected_message, response.body) + + expected_message = "Unable to communicate with the server to proceed to checkout. Please try again later." + self.assertIn(expected_message, response.body) + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'review') + # is reviewed at level 1, level 2, and accepted + self.assertTrue(response_checkin.is_level1_reviewed) + self.assertTrue(response_checkin.is_level2_reviewed) + self.assertTrue(response_checkin.is_accepted) + # can't be rejected, reviewed, or send_to_review, or send_to_pending + self.assertFalse(response_checkin.can_be_send_to_review) + self.assertFalse(response_checkin.can_be_send_to_pending) + self.assertFalse(response_checkin.can_be_rejected) + self.assertFalse(response_checkin.can_be_reviewed) + # checkin can't be checked out, so: + self.assertFalse(response_checkin.checked_out) + + def test_checkin_accept(self): + """ Create a checkin and call the view: checkin_accept """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + # send to review + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) + + # do review by QAL2 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_2(self.user_qal_2) + + # mock balaio for checkout step + self._mock_balaio_disabled() + + # do accept with QAL2 + response = self.app.get(reverse('checkin_accept', args=[checkin.pk, ]), user=self.user_qal_2) + # if ok, will try automatically to call checkin_send_to_checkout + expected_location = reverse('checkin_send_to_checkout', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # if ok, will try automatically to call notice_detail + response = response.follow() + expected_location = reverse('notice_detail', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # finally get response and check status + response = response.follow() + self.assertEqual(response.status_code, 200) + + expected_message = 'Checkin ACCEPTED succesfully.' + self.assertIn(expected_message, response.body) + + expected_message = "Unable to communicate with the server to proceed to checkout. Please try again later." + self.assertIn(expected_message, response.body) + + + response_checkin = response.context['checkin'] + self.assertTrue(response_checkin.status, 'accepted') + # is reviewed at level 1, level 2, and accepted + self.assertTrue(response_checkin.is_level1_reviewed) + self.assertTrue(response_checkin.is_level2_reviewed) + self.assertTrue(response_checkin.is_accepted) + # can't be rejected, reviewed, or send_to_review, or send_to_pending + self.assertFalse(response_checkin.can_be_send_to_review) + self.assertFalse(response_checkin.can_be_send_to_pending) + self.assertFalse(response_checkin.can_be_rejected) + self.assertFalse(response_checkin.can_be_reviewed) + # checkin can't be checked out, so: + self.assertFalse(response_checkin.checked_out) + + def test_checkin_send_to_checkout(self): + """ + Create a checkin and call the view: checkin_send_to_checkout. + Balalio API will be down, so must test that user can possible to call again this view. + """ + self._addWaffleFlag() + checkin = self._makeOne() + create_notices('ok', checkin) + + # send to review + checkin.send_to_review(self.user) + + # do review by QAL1 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_1(self.user_qal_1) + + # do review by QAL2 + self.assertTrue(checkin.can_be_reviewed) + checkin.do_review_by_level_2(self.user_qal_2) + + # do accept + self.assertTrue(checkin.can_be_accepted) + checkin.accept(self.user_qal_2) + + # mock balaio for checkout step + self._mock_balaio_disabled() + + # try to do send to checkout: + response = self.app.get(reverse('checkin_send_to_checkout', args=[checkin.pk, ]), user=self.user_qal_2) + + # if ok, will try automatically to call notice_detail + expected_location = reverse('notice_detail', args=[checkin.pk, ]) + self.assertTrue(response.location.endswith(expected_location)) + + # finally get response and check status + response = response.follow() + self.assertEqual(response.status_code, 200) diff --git a/scielomanager/articletrack/urls.py b/scielomanager/articletrack/urls.py index 52ba7130..5e099f39 100644 --- a/scielomanager/articletrack/urls.py +++ b/scielomanager/articletrack/urls.py @@ -9,10 +9,11 @@ # notice: url(r'^notice/(?P\d+)/$', views.notice_detail, name="notice_detail"), url(r'^checkin/(?P\d+)/reject/$', views.checkin_reject, name="checkin_reject"), - url(r'^checkin/(?P\d+)/review/$', views.checkin_review, name="checkin_review"), + url(r'^checkin/(?P\d+)/review/(?P\d)$', views.checkin_review, name="checkin_review"), url(r'^checkin/(?P\d+)/accept/$', views.checkin_accept, name="checkin_accept"), url(r'^checkin/(?P\d+)/send_to/pending/$', views.checkin_send_to_pending, name="checkin_send_to_pending"), url(r'^checkin/(?P\d+)/send_to/review/$', views.checkin_send_to_review, name="checkin_send_to_review"), + url(r'^checkin/(?P\d+)/send_to/checkout/$', views.checkin_send_to_checkout, name="checkin_send_to_checkout"), url(r'^checkin/(?P\d+)/history/$', views.checkin_history, name="checkin_history"), # TICKETS diff --git a/scielomanager/articletrack/views.py b/scielomanager/articletrack/views.py index cfda5328..dbe872e4 100644 --- a/scielomanager/articletrack/views.py +++ b/scielomanager/articletrack/views.py @@ -2,6 +2,7 @@ import datetime import json import logging +import lxml from waffle.decorators import waffle_flag from django.conf import settings @@ -24,7 +25,7 @@ from . import models from .forms import CommentMessageForm, TicketForm, CheckinListFilterForm, CheckinRejectForm from .balaio import BalaioAPI, BalaioRPC -from validator.utils import extract_validation_errors +from validator.utils import extract_validation_errors, extract_syntax_errors AUTHZ_REDIRECT_URL = '/accounts/unauthorized/' @@ -37,6 +38,28 @@ logger = logging.getLogger(__name__) +def verify_if_user_can_do(action): + """ + Decorator that check if user.get_profile().can_do(``action``) + Pre: Exist a request.user with profile + Post: If can not, will return a message to the user, else: proceed calling the view function: + something like: def checkin_xxxx(request, checkin_id). + """ + def decorator(f): + def decorated(*args, **kwargs): + request = args[0] + checkin_id = kwargs['checkin_id'] + user_can, error = request.user.get_profile().can_do(action) + if user_can: + return f(*args, **kwargs) + else: + messages.error(request, error) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + + return decorated + return decorator + + @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) def checkin_index(request): @@ -85,6 +108,14 @@ def filter_queryset_by_form_fields(queryset, form): objects_review = get_paginated(checkins_review, request.GET.get('review_page', 1)) objects_accepted = get_paginated(checkins_accepted, request.GET.get('accepted_page', 1)) + prefered_tab = 'pending' + if request.method == "GET" and request.GET.keys() and len(request.GET.keys()) > 0: + # get the prefix of first querystring parameter (something like: accepted-article), and get that prefix as a prefered tab + prefered_tab = request.GET.keys()[0].split('-')[0] # keys are lilke: "prefix-fieldname" + if prefered_tab not in ['pending', 'rejected', 'review', 'accepted']: + # maybe the user put something strange in the address bar + prefered_tab = 'pending' + return render_to_response( 'articletrack/checkin_list.html', { @@ -96,6 +127,7 @@ def filter_queryset_by_form_fields(queryset, form): 'rejected_filter_form': rejected_filter_form, 'review_filter_form': review_filter_form, 'accepted_filter_form': accepted_filter_form, + 'prefered_tab': prefered_tab, }, context_instance=RequestContext(request) ) @@ -103,8 +135,14 @@ def filter_queryset_by_form_fields(queryset, form): @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) +@verify_if_user_can_do('reject') def checkin_reject(request, checkin_id): + """ + Excecute checkin.do_reject, obtains the rejection text from the form filled by user. + """ + checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) + if checkin.can_be_rejected: if request.method == 'POST': form = CheckinRejectForm(request.POST) @@ -112,7 +150,18 @@ def checkin_reject(request, checkin_id): rejected_cause = form.cleaned_data['rejected_cause'] try: checkin.do_reject(request.user, rejected_cause) - messages.info(request, MSG_FORM_SAVED) + messages.success(request, _("Checkin REJECTED succesfully.")) + except ValueError as e: + logger.error("ValueError when trying to REJECT the Checkin: (pk: %s). Traceback: %s" % (checkin.pk, e)) + msg = _("Something went wrong when trying to REJECT this Checkin, please try again later.") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + + #Send e-mail only exists submitted_by attribute + if checkin.submitted_by: + subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, + checkin.package_name, + 'Package rejected']) send_to = set([i.email for i in checkin.team_members]) @@ -134,70 +183,76 @@ def checkin_reject(request, checkin_id): send_to ) - except ValueError: - messages.error(request, MSG_FORM_MISSING) else: form_errors = u", ".join([u"%s %s" % (field, error[0]) for field, error in form.errors.items()]) error_msg = "%s %s" % (MSG_FORM_MISSING, form_errors) messages.error(request, error_msg) else: - error_msg = _("This checkin cannot be rejected") + error_msg = _("This checkin doesn't comply the requirements to be rejected") messages.error(request, error_msg) return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) -def checkin_review(request, checkin_id): +def checkin_review(request, checkin_id, level): """ - Excecute checkin.do_review, and if checkin can be accepted and Balaio RPC API - is up, try to proceed to checkout. - This view function send to review and accepted the checkin + Excecute checkin.[do_review_by_level_1|do_review_by_level_2] (depends on ``level`` args), + and if checkin.can_be_accepted, proceed to accept the checkin (redirect to ``checkin_accept``). """ + if level not in ['1','2']: + msg = _("Trying to REVIEW the checkin, but parameters are wrong") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + else: + user_can, error = request.user.get_profile().can_do('review_l%s' % level) + if not user_can: + messages.error(request, error) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) + if checkin.can_be_reviewed: try: - checkin.do_review(request.user) - msg = _("Checkin reviewed succesfully.") - messages.info(request, msg) - except ValueError: - messages.error(request, MSG_FORM_MISSING) + if level == '1': + checkin.do_review_by_level_1(request.user) + else: # level = '2' + checkin.do_review_by_level_2(request.user) - # if Balaio RPC API is up, then try to proceed to Checkout and marked as accepted - rpc_client = BalaioRPC() - if checkin.can_be_accepted and rpc_client.is_up(): - rpc_response = rpc_client.call('proceed_to_checkout', [checkin.attempt_ref, ]) - if rpc_response: - try: - checkin.accept(request.user) - msg = _("Checkin accepted succesfully.") - messages.info(request, msg) - - send_to = [i.username for i in checkin.team_members] + messages.success(request, _("Checkin REVIEWED succesfully.")) + except ValueError as e: + logger.error("ValueError when trying to REVIEW the Checkin: (pk: %s). Traceback: %s" % (checkin.pk, e)) + msg = _("Something went wrong when trying to REVIEW this Checkin, please try again later.") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + # Send e-mail only if has "submitted_by" attribute + if checkin.submitted_by: + subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, + checkin.package_name, + 'Package reviewed']) + if level == '2': # SciELO review + subject += ' by SciELO' + + send_to = [i.email for i in checkin.team_members] - if len(send_to) > 0: - subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, - checkin.package_name, - 'Package accepted']) + if len(send_to) > 0: + tasks.send_mail.delay( + subject, + render_to_string( + 'email/checkin_reviewed.txt', + { + 'checkin': checkin, + 'domain': get_current_site(request) + } + ), + send_to + ) - tasks.send_mail.delay( - subject, - render_to_string( - 'email/accepted.txt', - { - 'checkin': checkin, - 'domain': get_current_site(request) - } - ), - send_to - ) + if checkin.can_be_accepted: + return HttpResponseRedirect(reverse('checkin_accept', args=[checkin_id, ])) - except ValueError as e: - logger.info(_('Could not mark %s as accepted. Traceback: %s') % (checkin, e)) - error_msg = _("An unexpected error, this attempt connot set to checkout. Please try again later.") - messages.error(request, error_msg) else: - error_msg = _("This checkin cannot be reviewed") + error_msg = _("This checkin doesn't comply the requirements to be reviewed") messages.error(request, error_msg) return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) @@ -205,60 +260,75 @@ def checkin_review(request, checkin_id): @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) +@verify_if_user_can_do('accept') def checkin_accept(request, checkin_id): """ Excecute checkin.accept, if checkin can be accepted and Balaio RPC API is up, try to proceed to checkout. """ + checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) - rpc_client = BalaioRPC() - if checkin.can_be_accepted and rpc_client.is_up(): - rpc_response = rpc_client.call('proceed_to_checkout', [checkin.attempt_ref, ]) - if rpc_response: - try: - checkin.accept(request.user) - messages.info(request, MSG_FORM_SAVED) - send_to = set([i.email for i in checkin.team_members]) + if checkin.can_be_accepted: - if len(send_to) > 0: - subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, - checkin.package_name, - 'Package accepted']) + try: + checkin.accept(request.user) + messages.success(request, _("Checkin ACCEPTED succesfully.")) + # Send e-mail only if has "submitted_by" attribute + except ValueError as e: + logger.error("ValueError when trying to ACCEPT the Checkin: (pk: %s). Traceback: %s" % (checkin.pk, e)) + msg = _("Something went wrong when trying to ACCEPT this Checkin, please try again later.") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) - tasks.send_mail.delay( - subject, - render_to_string( - 'email/checkin_accepted.txt', - { - 'checkin': checkin, - 'domain': get_current_site(request) - } - ), - send_to - ) + if checkin.submitted_by: + send_to = set([i.email for i in checkin.team_members]) - except ValueError as e: - logger.info(_('Could not mark %s as accepted. Traceback: %s') % (checkin, e)) - messages.error(request, MSG_FORM_MISSING) - else: - error_msg = _("The API response was unsuccessful") - messages.error(request, error_msg) + if len(send_to) > 0: + subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, + checkin.package_name, + 'Package accepted']) + + tasks.send_mail.delay( + subject, + render_to_string( + 'email/checkin_accepted.txt', + { + 'checkin': checkin, + 'domain': get_current_site(request) + } + ), + send_to + ) + # Proceed to Checkout + if checkin.can_be_send_to_checkout: + return HttpResponseRedirect(reverse('checkin_send_to_checkout', args=[checkin_id, ])) else: - error_msg = _("This checkin cannot be accepted or the API is down.") + error_msg = _("This checkin doesn't comply the requirements to be accepted") messages.error(request, error_msg) return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) +@verify_if_user_can_do('send_to_pending') def checkin_send_to_pending(request, checkin_id): + """ + Excecute checkin.send_to_pending, if checkin can be send to pending + """ checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) if checkin.can_be_send_to_pending: try: checkin.send_to_pending(request.user) - messages.info(request, MSG_FORM_SAVED) + messages.success(request, _("Checkin was SENT TO PENDING succesfully.")) + except ValueError as e: + logger.error("ValueError when trying to SEND TO PENDING the Checkin: (pk: %s). Traceback: %s" % (checkin.pk, e)) + msg = _("Something went wrong when trying to SEND TO PENDING this Checkin, please try again later.") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + #Send e-mail only exists submitted_by attribute + if checkin.submitted_by: send_to = set([i.email for i in checkin.team_members]) if len(send_to) > 0: @@ -278,23 +348,34 @@ def checkin_send_to_pending(request, checkin_id): send_to ) - except ValueError: - messages.error(request, MSG_FORM_MISSING) else: - error_msg = _("This checkin cannot be send to Pending") + error_msg = _("This checkin doesn't comply the requirements to be sent to pending") messages.error(request, error_msg) return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) +@verify_if_user_can_do('send_to_review') def checkin_send_to_review(request, checkin_id): + """ + Excecute checkin.send_to_review, if checkin can be send to review + """ + checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) + if checkin.can_be_send_to_review: try: checkin.send_to_review(request.user) - messages.info(request, MSG_FORM_SAVED) + messages.success(request, _("Checkin was SENT TO REVIEW succesfully.")) + except ValueError as e: + logger.error("ValueError when trying to SEND TO REVIEW the Checkin: (pk: %s). Traceback: %s" % (checkin.pk, e)) + msg = _("Something went wrong when trying to SEND TO REVIEW this Checkin, please try again later.") + messages.error(request, msg) + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + #Send e-mail only exists submitted_by attribute + if checkin.submitted_by: send_to = set([i.email for i in checkin.team_members]) if len(send_to) > 0: @@ -314,14 +395,45 @@ def checkin_send_to_review(request, checkin_id): send_to ) - except ValueError: - messages.error(request, MSG_FORM_MISSING) else: - error_msg = _("This checkin cannot be Reviewed") + error_msg = _("This checkin doesn't comply the requirements to be sent to review") messages.error(request, error_msg) return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) +@waffle_flag('articletrack') +@permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) +@verify_if_user_can_do('send_to_checkout') +def checkin_send_to_checkout(request, checkin_id): + """ + Excecute checkin.send_to_checkout, if checkin can be send to checkout + """ + + checkin = get_object_or_404(models.Checkin.userobjects.active(), pk=checkin_id) + + if checkin.can_be_send_to_checkout: + # TRY to call balaio to process checkin and proceed to checkout + # this code SHOULD BE REFACTORED TO USE CELERY TASKS to call balaio async + rpc_client = BalaioRPC() + if rpc_client.is_up(): + rpc_response = rpc_client.call('proceed_to_checkout', [checkin.attempt_ref, ]) + if rpc_response: + checkin.do_mark_as_checked_out() + messages.success(request, _("Checkin proceed to checkout!")) + else: + logger.error('BalaioRPC API method: "proceed_to_checkout" FAIL for checkin: %s. Response: %s' % (checkin.pk, rpc_response)) + msg = _("Server fail when requested to proceed to checkout. Please try again later.") + messages.info(request, msg) + else: + logger.error('BalaioRPC() connection is down!') + error_msg = _("Unable to communicate with the server to proceed to checkout. Please try again later.") + messages.info(request, error_msg) + else: + error_msg = _("This checkin doesn't comply the requirements to be sent to checkout") + messages.error(request, error_msg) + + return HttpResponseRedirect(reverse('notice_detail', args=[checkin_id, ])) + @waffle_flag('articletrack') @permission_required('articletrack.list_checkin', login_url=AUTHZ_REDIRECT_URL) def checkin_history(request, checkin_id): @@ -346,6 +458,19 @@ def notice_detail(request, checkin_id): opened_tickets = tickets.filter(finished_at__isnull=True) closed_tickets = tickets.filter(finished_at__isnull=False) + profile = request.user.get_profile() + have_actions_to_do = any( + [ + profile.can_reject_checkins and checkin.can_be_rejected, + profile.can_send_checkins_to_pending and checkin.can_be_send_to_pending, + profile.can_send_checkins_to_review and checkin.can_be_send_to_review, + profile.can_review_l1_checkins and checkin.can_be_reviewed, + profile.can_review_l2_checkins and checkin.can_be_reviewed, + profile.can_accept_checkins and checkin.can_be_accepted, + profile.can_send_checkins_to_checkout and checkin.can_be_send_to_checkout, + ] + ) + zip_filename = "%s_%s" % (datetime.date.today().isoformat(), slugify(checkin.article.article_title)) reject_form = CheckinRejectForm() context = { @@ -355,6 +480,7 @@ def notice_detail(request, checkin_id): 'closed_tickets': closed_tickets, 'zip_filename': zip_filename, 'reject_form': reject_form, + 'have_actions_to_do': have_actions_to_do, } balaio = BalaioAPI() @@ -400,6 +526,9 @@ def notice_detail(request, checkin_id): if xml_data['can_be_analyzed'][0]: try: xml_check = stylechecker.XML(xml_data['uri']) + except lxml.etree.XMLSyntaxError as e: + xml_data['annotations'] = e.message + xml_data['validation_errors'] = extract_syntax_errors(e) except Exception as e: # any exception will stop the process xml_data['can_be_analyzed'] = (False, "Error while starting Stylechecker.XML()") logger.error('ValueError while creating: Stylechecker.XML(%s) for checkin.pk == %s. Traceback: %s' % (xml_data['file_name'], checkin.pk, e)) @@ -458,7 +587,7 @@ def ticket_detail(request, ticket_id, template_name='articletrack/ticket_detail. send_to = [] for checkin in ticket.article.checkins.all(): - send_to += [i.username for i in checkin.team_members] + send_to += [i.email for i in checkin.team_members] send_to = set(send_to) send_to.add(ticket.author.email) if len(send_to) > 0: @@ -554,27 +683,27 @@ def ticket_add(request, checkin_id, template_name='articletrack/ticket_add.html' ticket.author = request.user ticket.article = checkin.article ticket.save() + if checkin.submitted_by: + send_to = set([i.email for i in checkin.team_members]) - send_to = set([i.email for i in checkin.team_members]) - - if len(send_to) > 0: - subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, - 'TICKET CREATED']) + if len(send_to) > 0: + subject = ' '.join([settings.EMAIL_SUBJECT_PREFIX, + 'TICKET CREATED']) - tasks.send_mail.delay( - subject, - render_to_string( - 'email/ticket_created.txt', - { - 'ticket': ticket, - 'checkin': checkin, - 'domain': get_current_site(request) - } - ), - send_to - ) + tasks.send_mail.delay( + subject, + render_to_string( + 'email/ticket_created.txt', + { + 'ticket': ticket, + 'checkin': checkin, + 'domain': get_current_site(request) + } + ), + send_to + ) - messages.info(request, MSG_FORM_SAVED) + messages.success(request, MSG_FORM_SAVED) return HttpResponseRedirect(reverse('ticket_detail', args=[ticket.id, ])) else: context['form'] = ticket_form @@ -631,7 +760,7 @@ def ticket_edit(request, ticket_id, template_name='articletrack/ticket_edit.html send_to ) - messages.info(request, MSG_FORM_SAVED) + messages.success(request, MSG_FORM_SAVED) return HttpResponseRedirect(reverse('ticket_detail', args=[ticket.pk])) else: context['form'] = ticket_form diff --git a/scielomanager/fixtures/groups.json b/scielomanager/fixtures/groups.json new file mode 100644 index 00000000..7caca258 --- /dev/null +++ b/scielomanager/fixtures/groups.json @@ -0,0 +1,675 @@ +[ + { + "pk": 4, + "model": "auth.group", + "fields": { + "name": "Librarian", + "permissions": [ + [ + "add_user", + "auth", + "user" + ], + [ + "change_user", + "auth", + "user" + ], + [ + "delete_user", + "auth", + "user" + ], + [ + "add_aheadpressrelease", + "journalmanager", + "aheadpressrelease" + ], + [ + "change_aheadpressrelease", + "journalmanager", + "aheadpressrelease" + ], + [ + "list_collection", + "journalmanager", + "collection" + ], + [ + "add_issue", + "journalmanager", + "issue" + ], + [ + "change_issue", + "journalmanager", + "issue" + ], + [ + "delete_issue", + "journalmanager", + "issue" + ], + [ + "list_issue", + "journalmanager", + "issue" + ], + [ + "add_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "change_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "delete_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "add_journal", + "journalmanager", + "journal" + ], + [ + "change_journal", + "journalmanager", + "journal" + ], + [ + "delete_journal", + "journalmanager", + "journal" + ], + [ + "list_journal", + "journalmanager", + "journal" + ], + [ + "add_journalmission", + "journalmanager", + "journalmission" + ], + [ + "change_journalmission", + "journalmanager", + "journalmission" + ], + [ + "delete_journalmission", + "journalmanager", + "journalmission" + ], + [ + "add_journaltitle", + "journalmanager", + "journaltitle" + ], + [ + "change_journaltitle", + "journalmanager", + "journaltitle" + ], + [ + "delete_journaltitle", + "journalmanager", + "journaltitle" + ], + [ + "add_language", + "journalmanager", + "language" + ], + [ + "change_language", + "journalmanager", + "language" + ], + [ + "delete_language", + "journalmanager", + "language" + ], + [ + "change_pendedform", + "journalmanager", + "pendedform" + ], + [ + "add_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "change_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "list_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "add_pressreleasearticle", + "journalmanager", + "pressreleasearticle" + ], + [ + "change_pressreleasearticle", + "journalmanager", + "pressreleasearticle" + ], + [ + "add_section", + "journalmanager", + "section" + ], + [ + "change_section", + "journalmanager", + "section" + ], + [ + "delete_section", + "journalmanager", + "section" + ], + [ + "list_section", + "journalmanager", + "section" + ], + [ + "add_sectiontitle", + "journalmanager", + "sectiontitle" + ], + [ + "change_sectiontitle", + "journalmanager", + "sectiontitle" + ], + [ + "delete_sectiontitle", + "journalmanager", + "sectiontitle" + ], + [ + "add_sponsor", + "journalmanager", + "sponsor" + ], + [ + "change_sponsor", + "journalmanager", + "sponsor" + ], + [ + "delete_sponsor", + "journalmanager", + "sponsor" + ], + [ + "list_sponsor", + "journalmanager", + "sponsor" + ] + ] + } + }, + { + "pk": 7, + "model": "auth.group", + "fields": { + "name": "Outsourced Technician", + "permissions": [ + [ + "list_collection", + "journalmanager", + "collection" + ], + [ + "add_issue", + "journalmanager", + "issue" + ], + [ + "change_issue", + "journalmanager", + "issue" + ], + [ + "delete_issue", + "journalmanager", + "issue" + ], + [ + "list_issue", + "journalmanager", + "issue" + ], + [ + "add_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "change_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "delete_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "list_journal", + "journalmanager", + "journal" + ], + [ + "add_section", + "journalmanager", + "section" + ], + [ + "change_section", + "journalmanager", + "section" + ], + [ + "delete_section", + "journalmanager", + "section" + ], + [ + "list_section", + "journalmanager", + "section" + ] + ] + } + }, + { + "pk": 6, + "model": "auth.group", + "fields": { + "name": "Technician", + "permissions": [ + [ + "add_aheadpressrelease", + "journalmanager", + "aheadpressrelease" + ], + [ + "change_aheadpressrelease", + "journalmanager", + "aheadpressrelease" + ], + [ + "list_collection", + "journalmanager", + "collection" + ], + [ + "add_issue", + "journalmanager", + "issue" + ], + [ + "change_issue", + "journalmanager", + "issue" + ], + [ + "delete_issue", + "journalmanager", + "issue" + ], + [ + "list_issue", + "journalmanager", + "issue" + ], + [ + "add_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "change_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "delete_issuetitle", + "journalmanager", + "issuetitle" + ], + [ + "list_journal", + "journalmanager", + "journal" + ], + [ + "add_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "change_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "list_pressrelease", + "journalmanager", + "pressrelease" + ], + [ + "add_pressreleasearticle", + "journalmanager", + "pressreleasearticle" + ], + [ + "change_pressreleasearticle", + "journalmanager", + "pressreleasearticle" + ], + [ + "add_pressreleasetranslation", + "journalmanager", + "pressreleasetranslation" + ], + [ + "change_pressreleasetranslation", + "journalmanager", + "pressreleasetranslation" + ], + [ + "add_regularpressrelease", + "journalmanager", + "regularpressrelease" + ], + [ + "change_regularpressrelease", + "journalmanager", + "regularpressrelease" + ], + [ + "add_section", + "journalmanager", + "section" + ], + [ + "change_section", + "journalmanager", + "section" + ], + [ + "delete_section", + "journalmanager", + "section" + ], + [ + "list_section", + "journalmanager", + "section" + ] + ] + } + }, + { + "pk": 5, + "model": "auth.group", + "fields": { + "name": "Trainee", + "permissions": [ + [ + "list_collection", + "journalmanager", + "collection" + ], + [ + "add_issue", + "journalmanager", + "issue" + ], + [ + "change_issue", + "journalmanager", + "issue" + ], + [ + "list_issue", + "journalmanager", + "issue" + ], + [ + "list_journal", + "journalmanager", + "journal" + ], + [ + "add_section", + "journalmanager", + "section" + ], + [ + "change_section", + "journalmanager", + "section" + ], + [ + "delete_section", + "journalmanager", + "section" + ], + [ + "list_section", + "journalmanager", + "section" + ] + ] + } + }, + { + "pk": 8, + "model": "auth.group", + "fields": { + "name": "Producer", + "permissions": [ + [ + "change_checkin", + "articletrack", + "checkin" + ], + [ + "list_checkin", + "articletrack", + "checkin" + ], + [ + "add_checkinworkflowlog", + "articletrack", + "checkinworkflowlog" + ], + [ + "add_comment", + "articletrack", + "comment" + ], + [ + "change_comment", + "articletrack", + "comment" + ], + [ + "list_comment", + "articletrack", + "comment" + ], + [ + "list_notice", + "articletrack", + "notice" + ], + [ + "add_ticket", + "articletrack", + "ticket" + ], + [ + "change_ticket", + "articletrack", + "ticket" + ], + [ + "list_ticket", + "articletrack", + "ticket" + ], + [ + "list_collection", + "journalmanager", + "collection" + ] + ] + } + }, + { + "pk": 9, + "model": "auth.group", + "fields": { + "name": "QAL1", + "permissions": [ + [ + "change_checkin", + "articletrack", + "checkin" + ], + [ + "list_checkin", + "articletrack", + "checkin" + ], + [ + "add_checkinworkflowlog", + "articletrack", + "checkinworkflowlog" + ], + [ + "add_comment", + "articletrack", + "comment" + ], + [ + "change_comment", + "articletrack", + "comment" + ], + [ + "list_comment", + "articletrack", + "comment" + ], + [ + "list_notice", + "articletrack", + "notice" + ], + [ + "add_ticket", + "articletrack", + "ticket" + ], + [ + "change_ticket", + "articletrack", + "ticket" + ], + [ + "list_ticket", + "articletrack", + "ticket" + ], + [ + "list_collection", + "journalmanager", + "collection" + ] + ] + } + }, + { + "pk": 10, + "model": "auth.group", + "fields": { + "name": "QAL2", + "permissions": [ + [ + "change_checkin", + "articletrack", + "checkin" + ], + [ + "list_checkin", + "articletrack", + "checkin" + ], + [ + "add_checkinworkflowlog", + "articletrack", + "checkinworkflowlog" + ], + [ + "add_comment", + "articletrack", + "comment" + ], + [ + "change_comment", + "articletrack", + "comment" + ], + [ + "list_comment", + "articletrack", + "comment" + ], + [ + "list_notice", + "articletrack", + "notice" + ], + [ + "add_ticket", + "articletrack", + "ticket" + ], + [ + "change_ticket", + "articletrack", + "ticket" + ], + [ + "list_ticket", + "articletrack", + "ticket" + ], + [ + "list_collection", + "journalmanager", + "collection" + ] + ] + } + } +] \ No newline at end of file diff --git a/scielomanager/journalmanager/models.py b/scielomanager/journalmanager/models.py index f0d4c679..4120b257 100644 --- a/scielomanager/journalmanager/models.py +++ b/scielomanager/journalmanager/models.py @@ -17,7 +17,7 @@ IntegrityError, DatabaseError, ) - +from django.db.models import Q from django.core.exceptions import ObjectDoesNotExist, ImproperlyConfigured from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes import generic @@ -322,6 +322,69 @@ def get_default_collection(self): uc = UserCollections.objects.get(user=self.user, is_default=True) return uc.collection + @property + def can_reject_checkins(self): + return self.user.groups.filter( + Q(name__iexact='producer') | Q(name__iexact='QAL1') | Q(name__iexact='QAL2') + ).exists() + + @property + def can_accept_checkins(self): + return self.user.groups.filter(name__iexact='QAL2').exists() + + @property + def can_review_l1_checkins(self): + return self.user.groups.filter( + Q(name__iexact='QAL1') | Q(name__iexact='QAL2') + ).exists() + + @property + def can_review_l2_checkins(self): + return self.user.groups.filter(name__iexact='QAL2').exists() + + @property + def can_send_checkins_to_pending(self): + return self.user.groups.filter( + Q(name__iexact='producer') | Q(name__iexact='QAL1') | Q(name__iexact='QAL2') + ).exists() + + @property + def can_send_checkins_to_review(self): + return self.user.groups.filter( + Q(name__iexact='producer') | Q(name__iexact='QAL1') | Q(name__iexact='QAL2') + ).exists() + + @property + def can_send_checkins_to_checkout(self): + return self.user.groups.filter(name__iexact='QAL2').exists() + + def can_do(self, action): + """ + Check if ``self.user`` can do ``action``. + @param actions could be: + ['accept'|'reject'|'review_l1'|'review_l2'|'send_to_review'|'send_to_pending'|'send_to_checkout'] + Return (True, None) if success. Else retrun (False, "Error message") + """ + actions = ['accept','reject','review_l1','review_l2','send_to_review','send_to_pending','send_to_checkout'] + if action not in actions: + return (False, _("User can't do this action: %s" % action)) + + if action == 'accept' and not self.can_accept_checkins: + return (False, _("User can't ACCEPT checkins, because doesn't have enough permissions")) + elif action == 'reject' and not self.can_reject_checkins: + return (False, _("User can't REJECT checkins, because doesn\'t have enough permissions")) + elif action == 'review_l1' and not self.can_review_l1_checkins: + return (False, _("User can't REVIEW (Level 1) checkins, because doesn\'t have enough permissions")) + elif action == 'review_l2' and not self.can_review_l2_checkins: + return (False, _("User can't REVIEW (Level 2) checkins, because doesn\'t have enough permissions")) + elif action == 'send_to_review' and not self.can_send_checkins_to_review: + return (False, _("User can't SEND checkins TO REVIEW, because doesn\'t have enough permissions")) + elif action == 'send_to_pending' and not self.can_send_checkins_to_pending: + return (False, _("User can't SEND checkins TO PENDING, because doesn\'t have enough permissions")) + elif action == 'send_to_checkout' and not self.can_send_checkins_to_checkout: + return (False, _("User can't SEND checkins TO CHECKOUT, because doesn\'t have enough permissions")) + + return (True, None) class Collection(caching.base.CachingMixin, models.Model): # objects = CollectionCustomManager() @@ -595,7 +658,7 @@ def __unicode__(self): return self.title class Meta: - ordering = ['title'] + ordering = ('title', 'id') permissions = (("list_journal", "Can list Journals"), ("list_editor_journal", "Can list editor Journals")) @@ -915,6 +978,7 @@ def _create_code(self, *args, **kwargs): raise DatabaseError(msg) class Meta: + ordering = ('id',) permissions = (("list_section", "Can list Sections"),) def save(self, *args, **kwargs): @@ -965,7 +1029,7 @@ class Issue(caching.base.CachingMixin, models.Model): spe_text = models.CharField(_('Special Text'), max_length=15, null=True, blank=True) class Meta: - ordering = ('created', ) + ordering = ('created', 'id') permissions = (("list_issue", "Can list Issues"), ("reorder_issue", "Can Reorder Issues")) diff --git a/scielomanager/journalmanager/tests/modelfactories.py b/scielomanager/journalmanager/tests/modelfactories.py index abfcc7d4..62a8b035 100644 --- a/scielomanager/journalmanager/tests/modelfactories.py +++ b/scielomanager/journalmanager/tests/modelfactories.py @@ -33,7 +33,7 @@ def _setup_next_sequence(cls): except IndexError: return 0 - username = factory.Sequence(lambda n: "username%s" % n) + username = factory.Sequence(lambda n: "jmanager_username%s" % n) first_name = factory.Sequence(lambda n: "first_name%s" % n) last_name = factory.Sequence(lambda n: "last_name%s" % n) email = factory.Sequence(lambda n: "email%s@example.com" % n) diff --git a/scielomanager/scielomanager/settings.py b/scielomanager/scielomanager/settings.py index 2c296b58..46a2272a 100644 --- a/scielomanager/scielomanager/settings.py +++ b/scielomanager/scielomanager/settings.py @@ -4,6 +4,10 @@ from datetime import timedelta from django.contrib.messages import constants as messages +# XML Catalog env-var is required for sps-stylechecking +from packtools.catalogs import XML_CATALOG +os.environ['XML_CATALOG_FILES'] = XML_CATALOG + DEBUG = False TEMPLATE_DEBUG = DEBUG diff --git a/scielomanager/scielomanager/static/css/style.css b/scielomanager/scielomanager/static/css/style.css index edc0c57f..7b661157 100644 --- a/scielomanager/scielomanager/static/css/style.css +++ b/scielomanager/scielomanager/static/css/style.css @@ -247,7 +247,10 @@ table._listings h4 { .disabled { color: #CCCCCC; margin: 5px 0 5px 20px; +} +.force-no-margin { + margin:0 !important; } .sys-msgs { diff --git a/scielomanager/validator/forms.py b/scielomanager/validator/forms.py index 82785497..2a6d899b 100644 --- a/scielomanager/validator/forms.py +++ b/scielomanager/validator/forms.py @@ -19,7 +19,7 @@ class StyleCheckerForm(forms.Form): def clean_file(self): _file = self.cleaned_data.get('file', None) if _file: - if _file.content_type != 'text/xml': + if _file.content_type not in ['text/xml', 'application/xml',]: raise forms.ValidationError(_(u"This type of file is not allowed! Please select another file.")) if _file.size > settings.VALIDATOR_MAX_UPLOAD_SIZE: @@ -37,4 +37,4 @@ def clean(self): if type == 'file' and not file: raise forms.ValidationError('if trying to validate a File, please upload a valid XML file') - return self.cleaned_data \ No newline at end of file + return self.cleaned_data diff --git a/scielomanager/validator/templates/validator/packtools.html b/scielomanager/validator/templates/validator/packtools.html index 2f06c509..569f5946 100644 --- a/scielomanager/validator/templates/validator/packtools.html +++ b/scielomanager/validator/templates/validator/packtools.html @@ -29,7 +29,7 @@

    {% trans "SciELO Style Checker" %}

    {% endblocktrans %}

    -
    + {% csrf_token %}
    {% if results %} @@ -134,15 +134,15 @@

    {% trans "SciELO Style Checker" %}

    - {% endblock extrafooter %} \ No newline at end of file + {% endblock extrafooter %} diff --git a/scielomanager/validator/tests.py b/scielomanager/validator/tests.py deleted file mode 100644 index 501deb77..00000000 --- a/scielomanager/validator/tests.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -This file demonstrates writing tests using the unittest module. These will pass -when you run "manage.py test". - -Replace this with more appropriate tests for your application. -""" - -from django.test import TestCase - - -class SimpleTest(TestCase): - def test_basic_addition(self): - """ - Tests that 1 + 1 always equals 2. - """ - self.assertEqual(1 + 1, 2) diff --git a/scielomanager/validator/tests/__init__.py b/scielomanager/validator/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/scielomanager/validator/tests/tests_pages.py b/scielomanager/validator/tests/tests_pages.py new file mode 100644 index 00000000..39e26fc6 --- /dev/null +++ b/scielomanager/validator/tests/tests_pages.py @@ -0,0 +1,244 @@ +# coding:utf-8 +from os import path +import unittest +import mocker +from PIL import Image +import StringIO + +from django.core.urlresolvers import reverse +from django_webtest import WebTest +from django.conf import settings +from django_factory_boy import auth +from django.core.files.uploadedfile import InMemoryUploadedFile + +from waffle import Flag + +from articletrack.tests import doubles + + +def _get_test_xml_abspath(filename): + folder_path = settings.PROJECT_PATH + folder_path = settings.PROJECT_PATH.split('/') + folder_path.extend(['articletrack', 'tests', 'xml_tests_files', filename]) + return '/'.join(folder_path) + + +def get_temporary_text_file(): + io = StringIO.StringIO() + io.write('foo') + text_file = InMemoryUploadedFile(io, field_name='file', name='foo.txt', content_type='text', size=io.len, charset='utf8') + text_file.seek(0) + return text_file + +def get_temporary_image_file(): + io = StringIO.StringIO() + size = (200,200) + color = (255,0,0,0) + image = Image.new("RGBA", size, color) + image.save(io, format='PNG') + image_file = InMemoryUploadedFile(io, field_name='file', name='foo.png', content_type='png', size=io.len, charset=None) + image_file.seek(0) + return image_file + +def get_temporary_xml_file(xml_abs_path): + fp = open(xml_abs_path) + io = StringIO.StringIO(fp.read()) + fp.close() + xml_file = InMemoryUploadedFile(io, field_name='file', name='foo.xml', content_type='text/xml', size=io.len, charset='utf8') + xml_file.seek(0) + return xml_file + + +class ValidatorTests(WebTest, mocker.MockerTestCase): + + def _addWaffleFlag(self): + Flag.objects.create(name='packtools_validator', everyone=True) + + def _mocker_replace_stylechecker(self, with_annotations=False): + XML = self.mocker.replace('packtools.stylechecker.XML') + XML(mocker.ANY) + if with_annotations: + self.mocker.result(doubles.StylecheckerAnnotationsDouble(mocker.ANY)) + else: + self.mocker.result(doubles.StylecheckerDouble(mocker.ANY)) + self.mocker.replay() + + def test_status_code_stylechecker_without_waffle_flag(self): + response = self.app.get( + reverse('validator.packtools.stylechecker',), + expect_errors=True + ) + self.assertEqual(response.status_code, 404) + + def test_status_code_stylechecker_with_waffle_flag(self): + self._addWaffleFlag() + response = self.app.get( + reverse('validator.packtools.stylechecker',), + ) + self.assertEqual(response.status_code, 200) + + def test_access_unauthenticated_user(self): + self._addWaffleFlag() + response = self.app.get( + reverse('validator.packtools.stylechecker',), + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'validator/packtools.html') + + def test_access_authenticated_user(self): + self._addWaffleFlag() + self.user = auth.UserF(is_active=True) + response = self.app.get( + reverse('validator.packtools.stylechecker',), + user=self.user + ) + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'validator/packtools.html') + + def test_link_is_present_at_homepage(self): + self._addWaffleFlag() + response = self.app.get( + reverse('index',), + ) + # response redirecto to login + self.assertEqual(response.status_code, 302) + response = response.follow() + self.assertEqual(response.status_code, 200) + self.assertTemplateUsed(response, 'registration/login.html') + expected_url = reverse('validator.packtools.stylechecker',) + response.mustcontain('href="%s"' % expected_url) + + def test_submit_empty_form_is_not_valid(self): + # with + self._addWaffleFlag() + page = self.app.get( + reverse('validator.packtools.stylechecker',), + ) + form = page.forms['stylechecker'] + # when + response = form.submit() + # then + self.assertTemplateUsed(response, 'validator/packtools.html') + self.assertFalse(response.context['form'].is_valid()) + form_errors = response.context['form'].errors + expected_errors = { + '__all__': [u'if trying to validate via URL, please submit a valid URL'] + } + self.assertEqual(form_errors, expected_errors) + self.assertFalse(hasattr(response.context, 'results')) + + def test_submite_invalid_url_then_form_is_not_valid(self): + # with + self._addWaffleFlag() + page = self.app.get( + reverse('validator.packtools.stylechecker',), + ) + form = page.forms['stylechecker'] + form['type'] = 'url' + form['url'] = 'file:///abc.123' + # when + response = form.submit() + # then + self.assertTemplateUsed(response, 'validator/packtools.html') + self.assertFalse(response.context['form'].is_valid()) + form_errors = response.context['form'].errors + expected_errors = { + 'url': [u'Enter a valid URL.'], + '__all__': [u'if trying to validate via URL, please submit a valid URL'] + } + self.assertEqual(form_errors, expected_errors) + self.assertFalse(hasattr(response.context, 'results')) + + def test_submit_valid_url_then_form_is_valid(self): + # with + self._addWaffleFlag() + self._mocker_replace_stylechecker() + # when + page = self.app.get( + reverse('validator.packtools.stylechecker',), + ) + form = page.forms['stylechecker'] + form['type'] = 'url' + form['url'] = 'http://example.com/foo/bar/valid.xml' + response = form.submit() + # then + self.assertTemplateUsed(response, 'validator/packtools.html') + self.assertEqual(response.status_code, 200) + self.assertTrue(response.context['form'].is_valid()) + expected_results = { + 'validation_errors': None, + 'annotations': None, + 'can_be_analyzed': (True, None) + } + self.assertEqual(response.context['results'], expected_results) + + def test_submit_text_file_then_form_not_valid(self): + # with + self._addWaffleFlag() + test_file = get_temporary_text_file() + # when + response = self.client.post( + reverse('validator.packtools.stylechecker',), + { + 'type': 'file', + 'file': test_file + } + ) + # then + self.assertEqual(200, response.status_code) + form = response.context['form'] + self.assertFalse(response.context['form'].is_valid()) + expected_errors = { + '__all__': [u'if trying to validate a File, please upload a valid XML file'], + 'file': [u'This type of file is not allowed! Please select another file.'] + } + self.assertEqual(form.errors, expected_errors) + + def test_submit_image_file_then_form_not_valid(self): + # with + self._addWaffleFlag() + test_file = get_temporary_image_file() + # when + response = self.client.post( + reverse('validator.packtools.stylechecker',), + { + 'type': 'file', + 'file': test_file + } + ) + # then + self.assertEqual(200, response.status_code) + form = response.context['form'] + self.assertFalse(response.context['form'].is_valid()) + expected_errors = { + '__all__': [u'if trying to validate a File, please upload a valid XML file'], + 'file': [u'This type of file is not allowed! Please select another file.'] + } + self.assertEqual(form.errors, expected_errors) + + def test_submit_valid_xml_file_then_form_not_valid(self): + # with + self._addWaffleFlag() + self._mocker_replace_stylechecker(with_annotations=True) + test_file_path = _get_test_xml_abspath('with_style_errors.xml') + test_file = get_temporary_xml_file(test_file_path) + # when + response = self.client.post( + reverse('validator.packtools.stylechecker',), + { + 'type': 'file', + 'file': test_file + } + ) + # then + form = response.context['form'] + results = response.context['results'] + + self.assertEqual(200, response.status_code) + self.assertTrue(response.context['form'].is_valid()) + self.assertEqual((True, None), results['can_be_analyzed']) + self.assertIsNotNone(results['validation_errors']) + self.assertIsNotNone(results['annotations']) + self.assertTrue(len(results['validation_errors']['results']) > 0) + + self.assertTemplateUsed('validator/packtools.html') diff --git a/scielomanager/validator/utils.py b/scielomanager/validator/utils.py index 562179a2..52252145 100644 --- a/scielomanager/validator/utils.py +++ b/scielomanager/validator/utils.py @@ -1,11 +1,32 @@ # coding: utf-8 import logging - +import lxml from packtools import stylechecker logger = logging.getLogger(__name__) +def extract_syntax_errors(syntax_error_exception): + """ + Return a dict with information about the syntax error exception + """ + results = [] + error_lines = [] + if syntax_error_exception.position: + line, column = syntax_error_exception.position + error_data = { + 'line': line or '--', + 'column': column or '--', + 'message': syntax_error_exception.message or '', + 'level': 'ERROR', + } + results.append(error_data) + error_lines.append(str(line)) + return { + 'results': results, + 'error_lines': ", ".join(error_lines) + } + def extract_validation_errors(validation_errors): """ Return a "parsed" dict of validation errors returned by stylechecker @@ -37,6 +58,11 @@ def stylechecker_analyze(data_type, data_input): } try: xml_check = stylechecker.XML(data_input) + except lxml.etree.XMLSyntaxError as e: + results['can_be_analyzed'] = (True, None) + results['annotations'] = e.message + results['validation_errors'] = extract_syntax_errors(e) + return results except Exception as e: # any exception means that cannot be analyzed results['can_be_analyzed'] = (False, "Error while starting Stylechecker.XML()") # logger.error('ValueError while creating: Stylechecker.XML(%s) of type: %s. Traceback: %s' % (data_input, data_type, e)) @@ -48,4 +74,4 @@ def stylechecker_analyze(data_type, data_input): results['annotations'] = str(xml_check) results['validation_errors'] = extract_validation_errors(errors) - return results \ No newline at end of file + return results