diff --git a/.gitignore b/.gitignore index a2274b00d145b376cee7323396849040f0118584..2673ca550fb5d82633cc80cf1c145f3c9b1d157e 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ __pycache__ *.json +!**/fixtures/*.json *~ @@ -14,4 +15,4 @@ SCIPOST_JOURNALS UPLOADS docs/_build -local_files \ No newline at end of file +local_files diff --git a/commentaries/factories.py b/commentaries/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..f0bae1a0912e9a18ceb0c6f079918c595534948b --- /dev/null +++ b/commentaries/factories.py @@ -0,0 +1,33 @@ +import factory + +from .models import Commentary, COMMENTARY_TYPES + +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS +from scipost.factories import ContributorFactory +from journals.models import SCIPOST_JOURNALS_DOMAINS + + +class CommentaryFactory(factory.django.DjangoModelFactory): + class Meta: + model = Commentary + abstract = True + + requested_by = factory.SubFactory(ContributorFactory) + vetted_by = factory.SubFactory(ContributorFactory) + type = COMMENTARY_TYPES[0][0] + discipline = SCIPOST_DISCIPLINES[0][0] + domain = SCIPOST_JOURNALS_DOMAINS[0][0] + subject_area = SCIPOST_SUBJECT_AREAS[0][1][0][0] + pub_title = factory.Sequence(lambda n: "Commentary %d" % n) + pub_DOI = '10.1103/PhysRevB.92.214427' + arxiv_identifier = '1610.06911v1' + author_list = factory.Faker('name') + pub_abstract = factory.Faker('text') + + +class EmptyCommentaryFactory(CommentaryFactory): + pub_DOI = None + arxiv_identifier = None + +class VettedCommentaryFactory(CommentaryFactory): + vetted = True diff --git a/commentaries/forms.py b/commentaries/forms.py index 14cd5852633ca67586f10bb5cf3e39f7fcefd1ea..f0dcdf3a30c8497a8d19650abc9fda2c291bda45 100644 --- a/commentaries/forms.py +++ b/commentaries/forms.py @@ -1,7 +1,9 @@ from django import forms +from django.shortcuts import get_object_or_404 from .models import Commentary +from scipost.models import Contributor COMMENTARY_ACTION_CHOICES = ( (0, 'modify'), @@ -17,10 +19,12 @@ COMMENTARY_REFUSAL_CHOICES = ( ) commentary_refusal_dict = dict(COMMENTARY_REFUSAL_CHOICES) + class DOIToQueryForm(forms.Form): doi = forms.CharField(widget=forms.TextInput( {'label': 'DOI', 'placeholder': 'ex.: 10.21468/00.000.000000'})) + class IdentifierToQueryForm(forms.Form): identifier = forms.CharField(widget=forms.TextInput( {'label': 'arXiv identifier', @@ -28,6 +32,8 @@ class IdentifierToQueryForm(forms.Form): class RequestCommentaryForm(forms.ModelForm): + existing_commentary = None + class Meta: model = Commentary fields = ['type', 'discipline', 'domain', 'subject_area', @@ -38,6 +44,7 @@ class RequestCommentaryForm(forms.ModelForm): 'pub_DOI', 'pub_abstract'] def __init__(self, *args, **kwargs): + self.user = kwargs.pop('user', None) super(RequestCommentaryForm, self).__init__(*args, **kwargs) self.fields['metadata'].widget = forms.HiddenInput() self.fields['pub_date'].widget.attrs.update({'placeholder': 'Format: YYYY-MM-DD'}) @@ -46,6 +53,44 @@ class RequestCommentaryForm(forms.ModelForm): self.fields['pub_DOI'].widget.attrs.update({'placeholder': 'ex.: 10.21468/00.000.000000'}) self.fields['pub_abstract'].widget.attrs.update({'cols': 100}) + def clean(self, *args, **kwargs): + cleaned_data = super(RequestCommentaryForm, self).clean(*args, **kwargs) + + # Either Arxiv-ID or DOI is given + if not cleaned_data['arxiv_identifier'] and not cleaned_data['pub_DOI']: + msg = ('You must provide either a DOI (for a published paper) ' + 'or an arXiv identifier (for a preprint).') + self.add_error('arxiv_identifier', msg) + self.add_error('pub_DOI', msg) + elif (cleaned_data['arxiv_identifier'] and + (Commentary.objects + .filter(arxiv_identifier=cleaned_data['arxiv_identifier']).exists())): + msg = 'There already exists a Commentary Page on this preprint, see' + self.existing_commentary = get_object_or_404( + Commentary, + arxiv_identifier=cleaned_data['arxiv_identifier']) + self.add_error('arxiv_identifier', msg) + elif (cleaned_data['pub_DOI'] and + Commentary.objects.filter(pub_DOI=cleaned_data['pub_DOI']).exists()): + msg = 'There already exists a Commentary Page on this publication, see' + self.existing_commentary = get_object_or_404(Commentary, pub_DOI=cleaned_data['pub_DOI']) + self.add_error('pub_DOI', msg) + + # Current user is not known + if not self.user or not Contributor.objects.filter(user=self.user).exists(): + self.add_error(None, 'Sorry, current user is not known to SciPost.') + + + def save(self, *args, **kwargs): + """Prefill instance before save""" + self.requested_by = Contributor.objects.get(user=self.user) + return super(RequestCommentaryForm, self).save(*args, **kwargs) + + def get_existing_commentary(self): + """Get Commentary if found after validation""" + return self.existing_commentary + + class VetCommentaryForm(forms.Form): action_option = forms.ChoiceField(widget=forms.RadioSelect, choices=COMMENTARY_ACTION_CHOICES, @@ -54,7 +99,16 @@ class VetCommentaryForm(forms.Form): email_response_field = forms.CharField(widget=forms.Textarea( attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False) + class CommentarySearchForm(forms.Form): + """Search for Commentary specified by user""" pub_author = forms.CharField(max_length=100, required=False, label="Author(s)") - pub_title_keyword = forms.CharField(max_length=100, label="Title", required=False) + pub_title_keyword = forms.CharField(max_length=100, required=False, label="Title") pub_abstract_keyword = forms.CharField(max_length=1000, required=False, label="Abstract") + + def search_results(self): + """Return all Commentary objects according to search""" + return Commentary.objects.vetted( + pub_title__icontains=self.cleaned_data['pub_title_keyword'], + pub_abstract__icontains=self.cleaned_data['pub_abstract_keyword'], + author_list__icontains=self.cleaned_data['pub_author']).order_by('-pub_date') diff --git a/commentaries/migrations/0013_auto_20161213_2328.py b/commentaries/migrations/0013_auto_20161213_2328.py new file mode 100644 index 0000000000000000000000000000000000000000..ec27da0367e4f87026cfc64b8131232f62bfb3cc --- /dev/null +++ b/commentaries/migrations/0013_auto_20161213_2328.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-13 22:28 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('commentaries', '0012_remove_commentary_specialization'), + ] + + operations = [ + migrations.AddField( + model_name='commentary', + name='created', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AlterField( + model_name='commentary', + name='latest_activity', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/commentaries/models.py b/commentaries/models.py index c216ce3599e806f4ec35c2925dcb8d49c7b3caf2..a42005b4dae74ac4621960f27e9bfc11dcd4bbb0 100644 --- a/commentaries/models.py +++ b/commentaries/models.py @@ -1,70 +1,86 @@ from django.utils import timezone from django.db import models -from django.contrib.auth.models import User from django.contrib.postgres.fields import JSONField from django.template import Template, Context from journals.models import SCIPOST_JOURNALS_DOMAINS, SCIPOST_JOURNALS_SPECIALIZATIONS -from scipost.models import Contributor -from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS - - +from scipost.models import TimeStampedModel, Contributor +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS COMMENTARY_TYPES = ( ('published', 'published paper'), ('preprint', 'arXiv preprint'), ) -class Commentary(models.Model): + +class CommentaryManager(models.Manager): + def vetted(self, **kwargs): + return self.filter(vetted=True, **kwargs) + + def awaiting_vetting(self, **kwargs): + return self.filter(vetted=False, **kwargs) + + +class Commentary(TimeStampedModel): """ A Commentary contains all the contents of a SciPost Commentary page for a given publication. """ - requested_by = models.ForeignKey (Contributor, blank=True, null=True, - on_delete=models.CASCADE, related_name='requested_by') + requested_by = models.ForeignKey( + Contributor, blank=True, null=True, + on_delete=models.CASCADE, related_name='requested_by') vetted = models.BooleanField(default=False) - vetted_by = models.ForeignKey (Contributor, blank=True, null=True, on_delete=models.CASCADE) - type = models.CharField(max_length=9, choices=COMMENTARY_TYPES) # published paper or arxiv preprint + vetted_by = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE) + type = models.CharField(max_length=9, choices=COMMENTARY_TYPES) discipline = models.CharField(max_length=20, choices=SCIPOST_DISCIPLINES, default='physics') domain = models.CharField(max_length=3, choices=SCIPOST_JOURNALS_DOMAINS) -# specialization = models.CharField(max_length=1, choices=SCIPOST_JOURNALS_SPECIALIZATIONS) - subject_area = models.CharField(max_length=10, choices=SCIPOST_SUBJECT_AREAS, default='Phys:QP') + subject_area = models.CharField( + max_length=10, choices=SCIPOST_SUBJECT_AREAS, + default='Phys:QP') open_for_commenting = models.BooleanField(default=True) pub_title = models.CharField(max_length=300, verbose_name='title') - arxiv_identifier = models.CharField(max_length=100, - verbose_name="arXiv identifier (including version nr)", - blank=True, null=True) + arxiv_identifier = models.CharField( + max_length=100, verbose_name="arXiv identifier (including version nr)", + blank=True, null=True) arxiv_link = models.URLField(verbose_name='arXiv link (including version nr)', blank=True) - pub_DOI = models.CharField(max_length=200, verbose_name='DOI of the original publication', - blank=True, null=True) - pub_DOI_link = models.URLField(verbose_name='DOI link to the original publication', blank=True) + pub_DOI = models.CharField( + max_length=200, verbose_name='DOI of the original publication', + blank=True, null=True) + pub_DOI_link = models.URLField( + verbose_name='DOI link to the original publication', + blank=True) metadata = JSONField(default={}, blank=True, null=True) arxiv_or_DOI_string = models.CharField( max_length=100, verbose_name='string form of arxiv nr or DOI for commentary url', default='') author_list = models.CharField(max_length=1000) + # Authors which have been mapped to contributors: - authors = models.ManyToManyField (Contributor, blank=True, - related_name='authors_com') - authors_claims = models.ManyToManyField (Contributor, blank=True, - related_name='authors_com_claims') - authors_false_claims = models.ManyToManyField (Contributor, blank=True, - related_name='authors_com_false_claims') + authors = models.ManyToManyField( + Contributor, blank=True, + related_name='authors_com') + authors_claims = models.ManyToManyField( + Contributor, blank=True, + related_name='authors_com_claims') + authors_false_claims = models.ManyToManyField( + Contributor, blank=True, + related_name='authors_com_false_claims') journal = models.CharField(max_length=300, blank=True, null=True) volume = models.CharField(max_length=50, blank=True, null=True) pages = models.CharField(max_length=50, blank=True, null=True) - pub_date = models.DateField(verbose_name='date of original publication', blank=True, null=True) + pub_date = models.DateField( + verbose_name='date of original publication', + blank=True, null=True) pub_abstract = models.TextField(verbose_name='abstract') - latest_activity = models.DateTimeField(default=timezone.now) + + objects = CommentaryManager() class Meta: verbose_name_plural = 'Commentaries' - def __str__(self): return self.pub_title - def header_as_table(self): # for display in Commentary page itself header = ('<table>' @@ -93,8 +109,8 @@ class Commentary(models.Model): header += '</table>' template = Template(header) context = Context({ - 'pub_title': self.pub_title, 'author_list': self.author_list, - }) + 'pub_title': self.pub_title, 'author_list': self.author_list, + }) if self.type == 'published': context['journal'] = self.journal context['volume'] = self.volume @@ -105,7 +121,6 @@ class Commentary(models.Model): context['arxiv_link'] = self.arxiv_link return template.render(context) - def header_as_li(self): # for display in search lists context = Context({'scipost_url': self.scipost_url(), 'pub_title': self.pub_title, @@ -136,7 +151,6 @@ class Commentary(models.Model): return template.render(context) - def simple_header_as_li(self): # for display in Lists context = Context({'scipost_url': self.scipost_url(), 'pub_title': self.pub_title, @@ -158,7 +172,6 @@ class Commentary(models.Model): template = Template(header) return template.render(context) - def parse_links_into_urls(self): """ Takes the arXiv nr or DOI and turns it into the urls """ if self.pub_DOI: diff --git a/commentaries/test_forms.py b/commentaries/test_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..694ce19ec56c93503b879a51038f068b06947f7c --- /dev/null +++ b/commentaries/test_forms.py @@ -0,0 +1,42 @@ +import factory + +from django.test import TestCase + +from scipost.factories import UserFactory + +from .factories import VettedCommentaryFactory +from .forms import RequestCommentaryForm +from common.helpers import model_form_data + + +class TestRequestCommentaryForm(TestCase): + fixtures = ['permissions', 'groups'] + + def setUp(self): + factory_instance = VettedCommentaryFactory.build() + self.user = UserFactory() + self.valid_form_data = model_form_data(factory_instance, RequestCommentaryForm) + + def test_valid_data_is_valid_for_arxiv(self): + """Test valid form for Arxiv identifier""" + form_data = self.valid_form_data + form_data['pub_DOI'] = '' + form = RequestCommentaryForm(form_data, user=self.user) + self.assertTrue(form.is_valid()) + + def test_valid_data_is_valid_for_DOI(self): + """Test valid form for DOI""" + form_data = self.valid_form_data + form_data['arxiv_identifier'] = '' + form = RequestCommentaryForm(form_data, user=self.user) + self.assertTrue(form.is_valid()) + + # def test_form_has_no_identifiers(self): + # """Test invalid form has no DOI nor Arxiv ID""" + # form_data = self.valid_form_data + # form_data['pub_DOI'] = '' + # form_data['arxiv_identifier'] = '' + # form = RequestCommentaryForm(form_data, user=self.user) + # form_response = form.is_valid() + # print(form_response) + # self.assertFormError(form_response, form, 'arxiv_identifier', None) diff --git a/theses/tests.py b/commentaries/test_models.py similarity index 55% rename from theses/tests.py rename to commentaries/test_models.py index 7ce503c2dd97ba78597f6ff6e4393132753573f6..2e9cb5f6ba351402af656aec1be5d9ac257bc5c0 100644 --- a/theses/tests.py +++ b/commentaries/test_models.py @@ -1,3 +1 @@ from django.test import TestCase - -# Create your tests here. diff --git a/commentaries/test_views.py b/commentaries/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..e6cc591f82f92345fe466d9dad9a23fe0d24063f --- /dev/null +++ b/commentaries/test_views.py @@ -0,0 +1,29 @@ +from django.contrib.auth.models import Group +from django.core.urlresolvers import reverse +from django.test import TestCase + +class RequestCommentaryTest(TestCase): + """Test cases for `request_commentary` view method""" + fixtures = ['permissions', 'groups', 'contributors'] + + def setUp(self): + self.view_url = reverse('commentaries:request_commentary') + self.login_url = reverse('scipost:login') + self.redirected_login_url = '%s?next=%s' % (self.login_url, self.view_url) + + def test_get_requests(self): + """Test different GET requests on view""" + # Anoymous user should redirect to login page + request = self.client.get(self.view_url) + self.assertRedirects(request, self.redirected_login_url) + + # Registered Contributor should get 200 + self.client.login(username="Test", password="testpw") + request = self.client.get(self.view_url) + self.assertEquals(request.status_code, 200) + + def test_post_invalid_forms(self): + """Test different kind of invalid RequestCommentaryForm submits""" + self.client.login(username="Test", password="testpw") + request = self.client.post(self.view_url) + self.assertEquals(request.status_code, 200) diff --git a/commentaries/views.py b/commentaries/views.py index 88f4d7c69b97e0c58870415078bd703ea2afa578..506fd838fc126d015f7c7144b29e4f73a4340b0f 100644 --- a/commentaries/views.py +++ b/commentaries/views.py @@ -6,15 +6,12 @@ import requests from django.db.models import Q from django.utils import timezone from django.shortcuts import get_object_or_404, render -from django.contrib.auth import authenticate, login, logout +from django.contrib.auth import login, logout from django.contrib.auth.decorators import login_required, permission_required -from django.contrib.auth.models import User from django.core.mail import EmailMessage from django.core.urlresolvers import reverse -from django.http import HttpResponse, HttpResponseRedirect +from django.http import HttpResponse from django.shortcuts import redirect -from django.views.decorators.csrf import csrf_protect -from django.db.models import Avg from .models import Commentary from .forms import RequestCommentaryForm, DOIToQueryForm, IdentifierToQueryForm @@ -35,64 +32,29 @@ from scipost.forms import AuthenticationForm @login_required @permission_required('scipost.can_request_commentary_pages', raise_exception=True) def request_commentary(request): + form = RequestCommentaryForm(request.POST or None, user=request.user) if request.method == 'POST': - form = RequestCommentaryForm(request.POST) if form.is_valid(): - errormessage = '' - existing_commentary = None - if not form.cleaned_data['arxiv_identifier'] and not form.cleaned_data['pub_DOI']: - errormessage = ('You must provide either a DOI (for a published paper) ' - 'or an arXiv identifier (for a preprint).') - elif (form.cleaned_data['arxiv_identifier'] and - (Commentary.objects - .filter(arxiv_identifier=form.cleaned_data['arxiv_identifier']).exists())): - errormessage = 'There already exists a Commentary Page on this preprint, see' - existing_commentary = get_object_or_404( - Commentary, - arxiv_identifier=form.cleaned_data['arxiv_identifier']) - elif (form.cleaned_data['pub_DOI'] and - Commentary.objects.filter(pub_DOI=form.cleaned_data['pub_DOI']).exists()): - errormessage = 'There already exists a Commentary Page on this publication, see' - existing_commentary = get_object_or_404(Commentary, pub_DOI=form.cleaned_data['pub_DOI']) - if errormessage: - doiform = DOIToQueryForm() - identifierform = IdentifierToQueryForm() - context = {'form': form, 'doiform': doiform, 'identifierform': identifierform, - 'errormessage': errormessage, - 'existing_commentary': existing_commentary} - return render(request, 'commentaries/request_commentary.html', context) - - # Otherwise we can create the Commentary - contributor = Contributor.objects.get(user=request.user) - commentary = Commentary ( - requested_by = contributor, - type = form.cleaned_data['type'], - discipline = form.cleaned_data['discipline'], - domain = form.cleaned_data['domain'], - subject_area = form.cleaned_data['subject_area'], - pub_title = form.cleaned_data['pub_title'], - arxiv_identifier = form.cleaned_data['arxiv_identifier'], - pub_DOI = form.cleaned_data['pub_DOI'], - metadata = form.cleaned_data['metadata'], - author_list = form.cleaned_data['author_list'], - journal = form.cleaned_data['journal'], - volume = form.cleaned_data['volume'], - pages = form.cleaned_data['pages'], - pub_date = form.cleaned_data['pub_date'], - pub_abstract = form.cleaned_data['pub_abstract'], - latest_activity = timezone.now(), - ) + commentary = form.save(commit=False) commentary.parse_links_into_urls() commentary.save() - + context = {'ack_header': 'Thank you for your request for a Commentary Page', 'ack_message': 'Your request will soon be handled by an Editor. ', 'followup_message': 'Return to your ', 'followup_link': reverse('scipost:personal_page'), 'followup_link_label': 'personal page'} return render(request, 'scipost/acknowledgement.html', context) - else: - form = RequestCommentaryForm() + + else: + doiform = DOIToQueryForm() + existing_commentary = form.get_existing_commentary() + identifierform = IdentifierToQueryForm() + context = {'form': form, 'doiform': doiform, 'identifierform': identifierform, + 'errormessage': form.errors, + 'existing_commentary': existing_commentary} + return render(request, 'commentaries/request_commentary.html', context) + doiform = DOIToQueryForm() identifierform = IdentifierToQueryForm() context = {'form': form, 'doiform': doiform, 'identifierform': identifierform} @@ -120,7 +82,7 @@ def prefill_using_DOI(request): 'errormessage': errormessage, 'existing_commentary': existing_commentary} return render(request, 'commentaries/request_commentary.html', context) - + # Otherwise we query Crossref for the information: try: queryurl = 'http://api.crossref.org/works/%s' % doiform.cleaned_data['doi'] @@ -133,12 +95,12 @@ def prefill_using_DOI(request): for author in doiqueryJSON['message']['author'][1:]: authorlist += ', ' + author['given'] + ' ' + author['family'] journal = doiqueryJSON['message']['container-title'][0] - + try: volume = doiqueryJSON['message']['volume'] except KeyError: volume = '' - + pages = '' try: pages = doiqueryJSON['message']['article-number'] # for Phys Rev @@ -148,7 +110,7 @@ def prefill_using_DOI(request): pages = doiqueryJSON['message']['page'] except KeyError: pass - + pub_date = '' try: pub_date = (str(doiqueryJSON['message']['issued']['date-parts'][0][0]) + '-' + @@ -209,7 +171,7 @@ def prefill_using_identifier(request): queryurl = ('http://export.arxiv.org/api/query?id_list=%s' % identifierform.cleaned_data['identifier']) arxivquery = feedparser.parse(queryurl) - + # If paper has been published, should comment on published version try: arxiv_journal_ref = arxivquery['entries'][0]['arxiv_journal_ref'] @@ -223,7 +185,7 @@ def prefill_using_identifier(request): + '. Please comment on the published version.') except (IndexError, KeyError): pass - + if errormessage: form = RequestCommentaryForm() doiform = DOIToQueryForm() @@ -231,7 +193,7 @@ def prefill_using_identifier(request): 'errormessage': errormessage, 'existing_commentary': existing_commentary} return render(request, 'commentaries/request_commentary.html', context) - + # otherwise prefill the form: metadata = arxivquery pub_title = arxivquery['entries'][0]['title'] @@ -265,7 +227,7 @@ def prefill_using_identifier(request): @permission_required('scipost.can_vet_commentary_requests', raise_exception=True) def vet_commentary_requests(request): contributor = Contributor.objects.get(user=request.user) - commentary_to_vet = Commentary.objects.filter(vetted=False).first() # only handle one at a time + commentary_to_vet = Commentary.objects.awaiting_vetting().first() # only handle one at a time form = VetCommentaryForm() context = {'contributor': contributor, 'commentary_to_vet': commentary_to_vet, 'form': form } return render(request, 'commentaries/vet_commentary_requests.html', context) @@ -348,62 +310,33 @@ def vet_commentary_request_ack(request, commentary_id): 'followup_link_label': 'Commentary requests page'} return render(request, 'scipost/acknowledgement.html', context) - def commentaries(request): - if request.method == 'POST': - form = CommentarySearchForm(request.POST) - if form.is_valid() and form.has_changed(): - commentary_search_list = Commentary.objects.filter( - pub_title__icontains=form.cleaned_data['pub_title_keyword'], - author_list__icontains=form.cleaned_data['pub_author'], - pub_abstract__icontains=form.cleaned_data['pub_abstract_keyword'], - vetted=True, - ) - commentary_search_list.order_by('-pub_date') - else: - commentary_search_list = [] - + """List and search all commentaries""" + form = CommentarySearchForm(request.POST or None) + if form.is_valid() and form.has_changed(): + commentary_search_list = form.search_results() else: - form = CommentarySearchForm() commentary_search_list = [] - comment_recent_list = (Comment.objects.filter(status='1') - .order_by('-date_submitted')[:10]) - - commentary_recent_list = (Commentary.objects.filter(vetted=True) - .order_by('-latest_activity')[:10]) - context = {'form': form, 'commentary_search_list': commentary_search_list, - 'comment_recent_list': comment_recent_list, - 'commentary_recent_list': commentary_recent_list } + comment_recent_list = Comment.objects.filter(status='1').order_by('-date_submitted')[:10] + commentary_recent_list = Commentary.objects.vetted().order_by('-latest_activity')[:10] + context = { + 'form': form, 'commentary_search_list': commentary_search_list, + 'comment_recent_list': comment_recent_list, + 'commentary_recent_list': commentary_recent_list} return render(request, 'commentaries/commentaries.html', context) - def browse(request, discipline, nrweeksback): - if request.method == 'POST': - form = CommentarySearchForm(request.POST) - if form.is_valid() and form.has_changed(): - commentary_search_list = Commentary.objects.filter( - pub_title__icontains=form.cleaned_data['pub_title_keyword'], - author_list__icontains=form.cleaned_data['pub_author'], - pub_abstract__icontains=form.cleaned_data['pub_abstract_keyword'], - vetted=True, - ) - commentary_search_list.order_by('-pub_date') - else: - commentary_search_list = [] - context = {'form': form, 'commentary_search_list': commentary_search_list} - return HttpResponseRedirect(request, 'commentaries/commentaries.html', context) - else: - form = CommentarySearchForm() - commentary_browse_list = Commentary.objects.filter( - vetted=True, discipline=discipline, - latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback)) - ) - context = {'form': form, 'discipline': discipline, 'nrweeksback': nrweeksback, - 'commentary_browse_list': commentary_browse_list } + """List all commentaries for discipline and period""" + commentary_browse_list = Commentary.objects.vetted( + discipline=discipline, + latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback))) + context = { + 'form': CommentarySearchForm(), + 'discipline': discipline, 'nrweeksback': nrweeksback, + 'commentary_browse_list': commentary_browse_list} return render(request, 'commentaries/commentaries.html', context) - def commentary_detail(request, arxiv_or_DOI_string): commentary = get_object_or_404(Commentary, arxiv_or_DOI_string=arxiv_or_DOI_string) comments = commentary.comment_set.all() diff --git a/comments/forms.py b/comments/forms.py index 0e94f326ed13e5e2388230d1c5d4cdf41a19fecf..d87f969a38eb8bb7c03fba8d8fc7bfb632141207 100644 --- a/comments/forms.py +++ b/comments/forms.py @@ -62,7 +62,6 @@ class CommentForm(forms.ModelForm): ) - class VetCommentForm(forms.Form): action_option = forms.ChoiceField(widget=forms.RadioSelect, choices=COMMENT_ACTION_CHOICES, required=True, label='Action') diff --git a/comments/models.py b/comments/models.py index d18801033f5fa367ab1b16bfbf812dd0cb0e25fa..1e6c1c5d1da23a916f201a3e664b24e001ab0d7d 100644 --- a/comments/models.py +++ b/comments/models.py @@ -33,15 +33,11 @@ COMMENT_STATUS = ( ) comment_status_dict = dict(COMMENT_STATUS) + class Comment(models.Model): """ A Comment is an unsollicited note, submitted by a Contributor, on a particular publication or in reply to an earlier Comment. """ - # status: - # 1: vetted - # 0: unvetted - # -1: rejected (unclear) - # -2: rejected (incorrect) - # -3: rejected (not useful) + status = models.SmallIntegerField(default=0) vetted_by = models.ForeignKey(Contributor, blank=True, null=True, on_delete=models.CASCADE, diff --git a/common/__init__.py b/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/common/helpers/__init__.py b/common/helpers/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a29cff832e8239e0ffc0cf1db98d51745c4c88c8 --- /dev/null +++ b/common/helpers/__init__.py @@ -0,0 +1,30 @@ +def model_form_data(model, form_class): + ''' + Returns a dict that can be used to instantiate a form object. + It fills in the model's data, but filters out fields that are not on the form. + Example: + + class Car(models.Model): + brand = CharField(max_length = 50) + fuel_tank_size = FloatField() + # more fields + + class CreateCarForm(forms.ModelForm): + fields = ['brand'] + + my_car = Car(brand='Nissan', fuel_tank_size=60) + + model_form_data(my_car, CreateCarForm) + # returns {'brand': 'Nissan'} + + Note that the returned dict does not have a field 'fuel_tank_size', because it is not + on the form. + ''' + + model_data = model.__dict__ + form_fields = list(form_class().fields.keys()) + return filter_keys(model_data, form_fields) + + +def filter_keys(dictionary, keys_to_keep): + return {key: dictionary[key] for key in keys_to_keep} diff --git a/journals/models.py b/journals/models.py index 8581d9d7915b7f7f4ad6acc16d0d5b2097ad3ed7..80464164d6915e51439452e8d41f5739657fa91a 100644 --- a/journals/models.py +++ b/journals/models.py @@ -3,8 +3,8 @@ from django.db import models from django.template import Template, Context from django.utils import timezone -from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict, TITLE_CHOICES -from scipost.models import ChoiceArrayField, Contributor +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict +from scipost.models import ChoiceArrayField, Contributor, TITLE_CHOICES class UnregisteredAuthor(models.Model): @@ -22,9 +22,11 @@ SCIPOST_JOURNALS = ( ) journals_dict = dict(SCIPOST_JOURNALS) + class JournalNameError(Exception): def __init__(self, name): self.name = name + def __str__(self): return self.name diff --git a/requirements.txt b/requirements.txt index 1dbfb212397ecfd37a21f1fac65f3bafd5975002..b25c7e2fba57bc091dcc0578fac5910b75817117 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,14 +10,18 @@ django-mptt==0.8.6 django-simple-captcha==0.5.3 djangorestframework==3.5.3 docutils==0.12 +factory-boy==2.7.0 +fake-factory==0.7.2 feedparser==5.2.1 imagesize==0.7.1 Jinja2==2.8 Markdown==2.6.7 MarkupSafe==0.23 +pep8==1.7.0 Pillow==3.4.2 psycopg2==2.6.2 Pygments==2.1.3 +python-dateutil==2.6.0 pytz==2016.7 requests==2.12.1 six==1.10.0 diff --git a/scipost/constants.py b/scipost/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..7c7933e18e787769f830f999423730a6af836d82 --- /dev/null +++ b/scipost/constants.py @@ -0,0 +1,120 @@ +SCIPOST_DISCIPLINES = ( + ('physics', 'Physics'), + ('astrophysics', 'Astrophysics'), + ('mathematics', 'Mathematics'), + ('computerscience', 'Computer Science'), + ) +disciplines_dict = dict(SCIPOST_DISCIPLINES) + +SCIPOST_SUBJECT_AREAS = ( + ('Physics', ( + ('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'), + ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'), + ('Phys:BI', 'Biophysics'), + ('Phys:CE', 'Condensed Matter Physics - Experiment'), + ('Phys:CT', 'Condensed Matter Physics - Theory'), + ('Phys:FD', 'Fluid Dynamics'), + ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'), + ('Phys:HE', 'High-Energy Physics - Experiment'), + ('Phys:HT', 'High-Energy Physics- Theory'), + ('Phys:HP', 'High-Energy Physics - Phenomenology'), + ('Phys:MP', 'Mathematical Physics'), + ('Phys:NE', 'Nuclear Physics - Experiment'), + ('Phys:NT', 'Nuclear Physics - Theory'), + ('Phys:QP', 'Quantum Physics'), + ('Phys:SM', 'Statistical and Soft Matter Physics'), + ) + ), + ('Astrophysics', ( + ('Astro:GA', 'Astrophysics of Galaxies'), + ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'), + ('Astro:EP', 'Earth and Planetary Astrophysics'), + ('Astro:HE', 'High Energy Astrophysical Phenomena'), + ('Astro:IM', 'Instrumentation and Methods for Astrophysics'), + ('Astro:SR', 'Solar and Stellar Astrophysics'), + ) + ), + ('Mathematics', ( + ('Math:AG', 'Algebraic Geometry'), + ('Math:AT', 'Algebraic Topology'), + ('Math:AP', 'Analysis of PDEs'), + ('Math:CT', 'Category Theory'), + ('Math:CA', 'Classical Analysis and ODEs'), + ('Math:CO', 'Combinatorics'), + ('Math:AC', 'Commutative Algebra'), + ('Math:CV', 'Complex Variables'), + ('Math:DG', 'Differential Geometry'), + ('Math:DS', 'Dynamical Systems'), + ('Math:FA', 'Functional Analysis'), + ('Math:GM', 'General Mathematics'), + ('Math:GN', 'General Topology'), + ('Math:GT', 'Geometric Topology'), + ('Math:GR', 'Group Theory'), + ('Math:HO', 'History and Overview'), + ('Math:IT', 'Information Theory'), + ('Math:KT', 'K-Theory and Homology'), + ('Math:LO', 'Logic'), + ('Math:MP', 'Mathematical Physics'), + ('Math:MG', 'Metric Geometry'), + ('Math:NT', 'Number Theory'), + ('Math:NA', 'Numerical Analysis'), + ('Math:OA', 'Operator Algebras'), + ('Math:OC', 'Optimization and Control'), + ('Math:PR', 'Probability'), + ('Math:QA', 'Quantum Algebra'), + ('Math:RT', 'Representation Theory'), + ('Math:RA', 'Rings and Algebras'), + ('Math:SP', 'Spectral Theory'), + ('Math:ST', 'Statistics Theory'), + ('Math:SG', 'Symplectic Geometry'), + ) + ), + ('Computer Science', ( + ('Comp:AI', 'Artificial Intelligence'), + ('Comp:CC', 'Computational Complexity'), + ('Comp:CE', 'Computational Engineering, Finance, and Science'), + ('Comp:CG', 'Computational Geometry'), + ('Comp:GT', 'Computer Science and Game Theory'), + ('Comp:CV', 'Computer Vision and Pattern Recognition'), + ('Comp:CY', 'Computers and Society'), + ('Comp:CR', 'Cryptography and Security'), + ('Comp:DS', 'Data Structures and Algorithms'), + ('Comp:DB', 'Databases'), + ('Comp:DL', 'Digital Libraries'), + ('Comp:DM', 'Discrete Mathematics'), + ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'), + ('Comp:ET', 'Emerging Technologies'), + ('Comp:FL', 'Formal Languages and Automata Theory'), + ('Comp:GL', 'General Literature'), + ('Comp:GR', 'Graphics'), + ('Comp:AR', 'Hardware Architecture'), + ('Comp:HC', 'Human-Computer Interaction'), + ('Comp:IR', 'Information Retrieval'), + ('Comp:IT', 'Information Theory'), + ('Comp:LG', 'Learning'), + ('Comp:LO', 'Logic in Computer Science'), + ('Comp:MS', 'Mathematical Software'), + ('Comp:MA', 'Multiagent Systems'), + ('Comp:MM', 'Multimedia'), + ('Comp:NI', 'Networking and Internet Architecture'), + ('Comp:NE', 'Neural and Evolutionary Computing'), + ('Comp:NA', 'Numerical Analysis'), + ('Comp:OS', 'Operating Systems'), + ('Comp:OH', 'Other Computer Science'), + ('Comp:PF', 'Performance'), + ('Comp:PL', 'Programming Languages'), + ('Comp:RO', 'Robotics'), + ('Comp:SI', 'Social and Information Networks'), + ('Comp:SE', 'Software Engineering'), + ('Comp:SD', 'Sound'), + ('Comp:SC', 'Symbolic Computation'), + ('Comp:SY', 'Systems and Control'), + ) + ), +) +subject_areas_raw_dict = dict(SCIPOST_SUBJECT_AREAS) + +# Make dict of the form {'Phys:AT': 'Atomic...', ...} +subject_areas_dict = {} +for k in subject_areas_raw_dict.keys(): + subject_areas_dict.update(dict(subject_areas_raw_dict[k])) diff --git a/scipost/factories.py b/scipost/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..c69b975f60db3f7f1b5952012384a719ecc7c9bb --- /dev/null +++ b/scipost/factories.py @@ -0,0 +1,41 @@ +import factory + +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group, User + +from .models import Contributor + + +class ContributorFactory(factory.django.DjangoModelFactory): + class Meta: + model = Contributor + + title = "MR" + user = factory.SubFactory('scipost.factories.UserFactory', contributor=None) + status = 1 + vetted_by = factory.SubFactory('scipost.factories.ContributorFactory', vetted_by=None) + + +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = get_user_model() + + username = factory.Faker('user_name') + password = factory.Faker('password') + email = factory.Faker('safe_email') + first_name = factory.Faker('first_name') + last_name = factory.Faker('last_name') + # When user object is created, associate new Contributor object to it. + contributor = factory.RelatedFactory(ContributorFactory, 'user') + + @factory.post_generation + def groups(self, create, extracted, **kwargs): + # If the object is not saved, we cannot use many-to-many relationship. + if not create: + return + # If group objects were passed in, use those. + if extracted: + for group in extracted: + self.groups.add(group) + else: + self.groups.add(Group.objects.get(name="Registered Contributors")) diff --git a/scipost/fixtures/contributors.json b/scipost/fixtures/contributors.json new file mode 100644 index 0000000000000000000000000000000000000000..a4863604278b1460829d963d108b4db098579aef --- /dev/null +++ b/scipost/fixtures/contributors.json @@ -0,0 +1,32 @@ +[ + { + "model": "auth.user", + "pk": 1, + "fields": { + "password": "pbkdf2_sha256$30000$iqtXX60Ahqcx$IKfNZNSMbSca/agzPXHTdEej3dXhQi1sK/MCrBTnuW4=", + "last_login": null, + "is_superuser": false, + "username": "Test", + "first_name": "Firstname", + "last_name": "Testuser", + "email": "testuser@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2016-12-14T20:41:31.282Z", + "groups": [ + 6 + ], + "user_permissions": [] + } + }, + { + "model": "scipost.contributor", + "pk": 2, + "fields": { + "user": 1, + "status": 1, + "title": "MR", + "vetted_by": 2 + } + } +] diff --git a/scipost/fixtures/groups.json b/scipost/fixtures/groups.json new file mode 100644 index 0000000000000000000000000000000000000000..82fafdc4738001941947ba92ecf8d9a5749ad11d --- /dev/null +++ b/scipost/fixtures/groups.json @@ -0,0 +1,116 @@ +[ +{ + "model": "auth.group", + "pk": 1, + "fields": { + "name": "SciPost Administrators", + "permissions": [ + 143, + 130, + 131, + 148, + 128, + 147, + 139, + 137, + 140, + 126, + 138, + 142 + ] + } +}, +{ + "model": "auth.group", + "pk": 2, + "fields": { + "name": "Advisory Board", + "permissions": [ + 128 + ] + } +}, +{ + "model": "auth.group", + "pk": 3, + "fields": { + "name": "Editorial Administrators", + "permissions": [ + 143, + 148, + 147, + 149, + 142 + ] + } +}, +{ + "model": "auth.group", + "pk": 4, + "fields": { + "name": "Editorial College", + "permissions": [ + 144, + 145, + 142, + 132 + ] + } +}, +{ + "model": "auth.group", + "pk": 5, + "fields": { + "name": "Vetting Editors", + "permissions": [ + 139, + 137, + 140, + 138 + ] + } +}, +{ + "model": "auth.group", + "pk": 6, + "fields": { + "name": "Registered Contributors", + "permissions": [ + 134, + 146, + 135, + 136, + 133, + 141 + ] + } +}, +{ + "model": "auth.group", + "pk": 7, + "fields": { + "name": "Testers", + "permissions": [] + } +}, +{ + "model": "auth.group", + "pk": 8, + "fields": { + "name": "Ambassadors", + "permissions": [ + 128 + ] + } +}, +{ + "model": "auth.group", + "pk": 9, + "fields": { + "name": "Junior Ambassadors", + "permissions": [ + 127 + ] + } +} +] diff --git a/scipost/fixtures/permissions.json b/scipost/fixtures/permissions.json new file mode 100644 index 0000000000000000000000000000000000000000..a718c3476c978d97c20a15d8b81f01ac0500596f --- /dev/null +++ b/scipost/fixtures/permissions.json @@ -0,0 +1,1343 @@ +[ +{ + "model": "auth.permission", + "pk": 1, + "fields": { + "name": "Can add log entry", + "content_type": 1, + "codename": "add_logentry" + } +}, +{ + "model": "auth.permission", + "pk": 2, + "fields": { + "name": "Can change log entry", + "content_type": 1, + "codename": "change_logentry" + } +}, +{ + "model": "auth.permission", + "pk": 3, + "fields": { + "name": "Can delete log entry", + "content_type": 1, + "codename": "delete_logentry" + } +}, +{ + "model": "auth.permission", + "pk": 4, + "fields": { + "name": "Can add user", + "content_type": 2, + "codename": "add_user" + } +}, +{ + "model": "auth.permission", + "pk": 5, + "fields": { + "name": "Can change user", + "content_type": 2, + "codename": "change_user" + } +}, +{ + "model": "auth.permission", + "pk": 6, + "fields": { + "name": "Can delete user", + "content_type": 2, + "codename": "delete_user" + } +}, +{ + "model": "auth.permission", + "pk": 7, + "fields": { + "name": "Can add group", + "content_type": 3, + "codename": "add_group" + } +}, +{ + "model": "auth.permission", + "pk": 8, + "fields": { + "name": "Can change group", + "content_type": 3, + "codename": "change_group" + } +}, +{ + "model": "auth.permission", + "pk": 9, + "fields": { + "name": "Can delete group", + "content_type": 3, + "codename": "delete_group" + } +}, +{ + "model": "auth.permission", + "pk": 10, + "fields": { + "name": "Can add permission", + "content_type": 4, + "codename": "add_permission" + } +}, +{ + "model": "auth.permission", + "pk": 11, + "fields": { + "name": "Can change permission", + "content_type": 4, + "codename": "change_permission" + } +}, +{ + "model": "auth.permission", + "pk": 12, + "fields": { + "name": "Can delete permission", + "content_type": 4, + "codename": "delete_permission" + } +}, +{ + "model": "auth.permission", + "pk": 13, + "fields": { + "name": "Can add content type", + "content_type": 5, + "codename": "add_contenttype" + } +}, +{ + "model": "auth.permission", + "pk": 14, + "fields": { + "name": "Can change content type", + "content_type": 5, + "codename": "change_contenttype" + } +}, +{ + "model": "auth.permission", + "pk": 15, + "fields": { + "name": "Can delete content type", + "content_type": 5, + "codename": "delete_contenttype" + } +}, +{ + "model": "auth.permission", + "pk": 16, + "fields": { + "name": "Can add session", + "content_type": 6, + "codename": "add_session" + } +}, +{ + "model": "auth.permission", + "pk": 17, + "fields": { + "name": "Can change session", + "content_type": 6, + "codename": "change_session" + } +}, +{ + "model": "auth.permission", + "pk": 18, + "fields": { + "name": "Can delete session", + "content_type": 6, + "codename": "delete_session" + } +}, +{ + "model": "auth.permission", + "pk": 19, + "fields": { + "name": "Can add captcha store", + "content_type": 7, + "codename": "add_captchastore" + } +}, +{ + "model": "auth.permission", + "pk": 20, + "fields": { + "name": "Can change captcha store", + "content_type": 7, + "codename": "change_captchastore" + } +}, +{ + "model": "auth.permission", + "pk": 21, + "fields": { + "name": "Can delete captcha store", + "content_type": 7, + "codename": "delete_captchastore" + } +}, +{ + "model": "auth.permission", + "pk": 22, + "fields": { + "name": "Can add user object permission", + "content_type": 8, + "codename": "add_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 23, + "fields": { + "name": "Can change user object permission", + "content_type": 8, + "codename": "change_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 24, + "fields": { + "name": "Can delete user object permission", + "content_type": 8, + "codename": "delete_userobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 25, + "fields": { + "name": "Can add group object permission", + "content_type": 9, + "codename": "add_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 26, + "fields": { + "name": "Can change group object permission", + "content_type": 9, + "codename": "change_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 27, + "fields": { + "name": "Can delete group object permission", + "content_type": 9, + "codename": "delete_groupobjectpermission" + } +}, +{ + "model": "auth.permission", + "pk": 28, + "fields": { + "name": "Can add commentary", + "content_type": 10, + "codename": "add_commentary" + } +}, +{ + "model": "auth.permission", + "pk": 29, + "fields": { + "name": "Can change commentary", + "content_type": 10, + "codename": "change_commentary" + } +}, +{ + "model": "auth.permission", + "pk": 30, + "fields": { + "name": "Can delete commentary", + "content_type": 10, + "codename": "delete_commentary" + } +}, +{ + "model": "auth.permission", + "pk": 31, + "fields": { + "name": "Can add comment", + "content_type": 11, + "codename": "add_comment" + } +}, +{ + "model": "auth.permission", + "pk": 32, + "fields": { + "name": "Can change comment", + "content_type": 11, + "codename": "change_comment" + } +}, +{ + "model": "auth.permission", + "pk": 33, + "fields": { + "name": "Can delete comment", + "content_type": 11, + "codename": "delete_comment" + } +}, +{ + "model": "auth.permission", + "pk": 34, + "fields": { + "name": "Can add issue", + "content_type": 12, + "codename": "add_issue" + } +}, +{ + "model": "auth.permission", + "pk": 35, + "fields": { + "name": "Can change issue", + "content_type": 12, + "codename": "change_issue" + } +}, +{ + "model": "auth.permission", + "pk": 36, + "fields": { + "name": "Can delete issue", + "content_type": 12, + "codename": "delete_issue" + } +}, +{ + "model": "auth.permission", + "pk": 37, + "fields": { + "name": "Can add journal", + "content_type": 13, + "codename": "add_journal" + } +}, +{ + "model": "auth.permission", + "pk": 38, + "fields": { + "name": "Can change journal", + "content_type": 13, + "codename": "change_journal" + } +}, +{ + "model": "auth.permission", + "pk": 39, + "fields": { + "name": "Can delete journal", + "content_type": 13, + "codename": "delete_journal" + } +}, +{ + "model": "auth.permission", + "pk": 40, + "fields": { + "name": "Can add unregistered author", + "content_type": 14, + "codename": "add_unregisteredauthor" + } +}, +{ + "model": "auth.permission", + "pk": 41, + "fields": { + "name": "Can change unregistered author", + "content_type": 14, + "codename": "change_unregisteredauthor" + } +}, +{ + "model": "auth.permission", + "pk": 42, + "fields": { + "name": "Can delete unregistered author", + "content_type": 14, + "codename": "delete_unregisteredauthor" + } +}, +{ + "model": "auth.permission", + "pk": 43, + "fields": { + "name": "Can add deposit", + "content_type": 15, + "codename": "add_deposit" + } +}, +{ + "model": "auth.permission", + "pk": 44, + "fields": { + "name": "Can change deposit", + "content_type": 15, + "codename": "change_deposit" + } +}, +{ + "model": "auth.permission", + "pk": 45, + "fields": { + "name": "Can delete deposit", + "content_type": 15, + "codename": "delete_deposit" + } +}, +{ + "model": "auth.permission", + "pk": 46, + "fields": { + "name": "Can add publication", + "content_type": 16, + "codename": "add_publication" + } +}, +{ + "model": "auth.permission", + "pk": 47, + "fields": { + "name": "Can change publication", + "content_type": 16, + "codename": "change_publication" + } +}, +{ + "model": "auth.permission", + "pk": 48, + "fields": { + "name": "Can delete publication", + "content_type": 16, + "codename": "delete_publication" + } +}, +{ + "model": "auth.permission", + "pk": 49, + "fields": { + "name": "Can add volume", + "content_type": 17, + "codename": "add_volume" + } +}, +{ + "model": "auth.permission", + "pk": 50, + "fields": { + "name": "Can change volume", + "content_type": 17, + "codename": "change_volume" + } +}, +{ + "model": "auth.permission", + "pk": 51, + "fields": { + "name": "Can delete volume", + "content_type": 17, + "codename": "delete_volume" + } +}, +{ + "model": "auth.permission", + "pk": 52, + "fields": { + "name": "Can add news item", + "content_type": 18, + "codename": "add_newsitem" + } +}, +{ + "model": "auth.permission", + "pk": 53, + "fields": { + "name": "Can change news item", + "content_type": 18, + "codename": "change_newsitem" + } +}, +{ + "model": "auth.permission", + "pk": 54, + "fields": { + "name": "Can delete news item", + "content_type": 18, + "codename": "delete_newsitem" + } +}, +{ + "model": "auth.permission", + "pk": 55, + "fields": { + "name": "Can add precooked email", + "content_type": 19, + "codename": "add_precookedemail" + } +}, +{ + "model": "auth.permission", + "pk": 56, + "fields": { + "name": "Can change precooked email", + "content_type": 19, + "codename": "change_precookedemail" + } +}, +{ + "model": "auth.permission", + "pk": 57, + "fields": { + "name": "Can delete precooked email", + "content_type": 19, + "codename": "delete_precookedemail" + } +}, +{ + "model": "auth.permission", + "pk": 58, + "fields": { + "name": "Can add affiliation object", + "content_type": 20, + "codename": "add_affiliationobject" + } +}, +{ + "model": "auth.permission", + "pk": 59, + "fields": { + "name": "Can change affiliation object", + "content_type": 20, + "codename": "change_affiliationobject" + } +}, +{ + "model": "auth.permission", + "pk": 60, + "fields": { + "name": "Can delete affiliation object", + "content_type": 20, + "codename": "delete_affiliationobject" + } +}, +{ + "model": "auth.permission", + "pk": 61, + "fields": { + "name": "Can add registration invitation", + "content_type": 21, + "codename": "add_registrationinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 62, + "fields": { + "name": "Can change registration invitation", + "content_type": 21, + "codename": "change_registrationinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 63, + "fields": { + "name": "Can delete registration invitation", + "content_type": 21, + "codename": "delete_registrationinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 64, + "fields": { + "name": "Can add spb membership agreement", + "content_type": 22, + "codename": "add_spbmembershipagreement" + } +}, +{ + "model": "auth.permission", + "pk": 65, + "fields": { + "name": "Can change spb membership agreement", + "content_type": 22, + "codename": "change_spbmembershipagreement" + } +}, +{ + "model": "auth.permission", + "pk": 66, + "fields": { + "name": "Can delete spb membership agreement", + "content_type": 22, + "codename": "delete_spbmembershipagreement" + } +}, +{ + "model": "auth.permission", + "pk": 67, + "fields": { + "name": "Can add supporting partner", + "content_type": 23, + "codename": "add_supportingpartner" + } +}, +{ + "model": "auth.permission", + "pk": 68, + "fields": { + "name": "Can change supporting partner", + "content_type": 23, + "codename": "change_supportingpartner" + } +}, +{ + "model": "auth.permission", + "pk": 69, + "fields": { + "name": "Can delete supporting partner", + "content_type": 23, + "codename": "delete_supportingpartner" + } +}, +{ + "model": "auth.permission", + "pk": 70, + "fields": { + "name": "Can add arc", + "content_type": 24, + "codename": "add_arc" + } +}, +{ + "model": "auth.permission", + "pk": 71, + "fields": { + "name": "Can change arc", + "content_type": 24, + "codename": "change_arc" + } +}, +{ + "model": "auth.permission", + "pk": 72, + "fields": { + "name": "Can delete arc", + "content_type": 24, + "codename": "delete_arc" + } +}, +{ + "model": "auth.permission", + "pk": 73, + "fields": { + "name": "Can add graph", + "content_type": 25, + "codename": "add_graph" + } +}, +{ + "model": "auth.permission", + "pk": 74, + "fields": { + "name": "Can view graph", + "content_type": 25, + "codename": "view_graph" + } +}, +{ + "model": "auth.permission", + "pk": 75, + "fields": { + "name": "Can change graph", + "content_type": 25, + "codename": "change_graph" + } +}, +{ + "model": "auth.permission", + "pk": 76, + "fields": { + "name": "Can delete graph", + "content_type": 25, + "codename": "delete_graph" + } +}, +{ + "model": "auth.permission", + "pk": 77, + "fields": { + "name": "Can add contributor", + "content_type": 26, + "codename": "add_contributor" + } +}, +{ + "model": "auth.permission", + "pk": 78, + "fields": { + "name": "Can change contributor", + "content_type": 26, + "codename": "change_contributor" + } +}, +{ + "model": "auth.permission", + "pk": 79, + "fields": { + "name": "Can delete contributor", + "content_type": 26, + "codename": "delete_contributor" + } +}, +{ + "model": "auth.permission", + "pk": 80, + "fields": { + "name": "Can add authorship claim", + "content_type": 27, + "codename": "add_authorshipclaim" + } +}, +{ + "model": "auth.permission", + "pk": 81, + "fields": { + "name": "Can change authorship claim", + "content_type": 27, + "codename": "change_authorshipclaim" + } +}, +{ + "model": "auth.permission", + "pk": 82, + "fields": { + "name": "Can delete authorship claim", + "content_type": 27, + "codename": "delete_authorshipclaim" + } +}, +{ + "model": "auth.permission", + "pk": 83, + "fields": { + "name": "Can add unavailability period", + "content_type": 28, + "codename": "add_unavailabilityperiod" + } +}, +{ + "model": "auth.permission", + "pk": 84, + "fields": { + "name": "Can change unavailability period", + "content_type": 28, + "codename": "change_unavailabilityperiod" + } +}, +{ + "model": "auth.permission", + "pk": 85, + "fields": { + "name": "Can delete unavailability period", + "content_type": 28, + "codename": "delete_unavailabilityperiod" + } +}, +{ + "model": "auth.permission", + "pk": 86, + "fields": { + "name": "Can add list", + "content_type": 29, + "codename": "add_list" + } +}, +{ + "model": "auth.permission", + "pk": 87, + "fields": { + "name": "Can view list", + "content_type": 29, + "codename": "view_list" + } +}, +{ + "model": "auth.permission", + "pk": 88, + "fields": { + "name": "Can change list", + "content_type": 29, + "codename": "change_list" + } +}, +{ + "model": "auth.permission", + "pk": 89, + "fields": { + "name": "Can delete list", + "content_type": 29, + "codename": "delete_list" + } +}, +{ + "model": "auth.permission", + "pk": 90, + "fields": { + "name": "Can add node", + "content_type": 30, + "codename": "add_node" + } +}, +{ + "model": "auth.permission", + "pk": 91, + "fields": { + "name": "Can view node", + "content_type": 30, + "codename": "view_node" + } +}, +{ + "model": "auth.permission", + "pk": 92, + "fields": { + "name": "Can change node", + "content_type": 30, + "codename": "change_node" + } +}, +{ + "model": "auth.permission", + "pk": 93, + "fields": { + "name": "Can delete node", + "content_type": 30, + "codename": "delete_node" + } +}, +{ + "model": "auth.permission", + "pk": 94, + "fields": { + "name": "Can add draft invitation", + "content_type": 31, + "codename": "add_draftinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 95, + "fields": { + "name": "Can change draft invitation", + "content_type": 31, + "codename": "change_draftinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 96, + "fields": { + "name": "Can delete draft invitation", + "content_type": 31, + "codename": "delete_draftinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 97, + "fields": { + "name": "Can add team", + "content_type": 32, + "codename": "add_team" + } +}, +{ + "model": "auth.permission", + "pk": 98, + "fields": { + "name": "Can view team", + "content_type": 32, + "codename": "view_team" + } +}, +{ + "model": "auth.permission", + "pk": 99, + "fields": { + "name": "Can change team", + "content_type": 32, + "codename": "change_team" + } +}, +{ + "model": "auth.permission", + "pk": 100, + "fields": { + "name": "Can delete team", + "content_type": 32, + "codename": "delete_team" + } +}, +{ + "model": "auth.permission", + "pk": 101, + "fields": { + "name": "Can add remark", + "content_type": 33, + "codename": "add_remark" + } +}, +{ + "model": "auth.permission", + "pk": 102, + "fields": { + "name": "Can change remark", + "content_type": 33, + "codename": "change_remark" + } +}, +{ + "model": "auth.permission", + "pk": 103, + "fields": { + "name": "Can delete remark", + "content_type": 33, + "codename": "delete_remark" + } +}, +{ + "model": "auth.permission", + "pk": 104, + "fields": { + "name": "Can add editorial communication", + "content_type": 34, + "codename": "add_editorialcommunication" + } +}, +{ + "model": "auth.permission", + "pk": 105, + "fields": { + "name": "Can change editorial communication", + "content_type": 34, + "codename": "change_editorialcommunication" + } +}, +{ + "model": "auth.permission", + "pk": 106, + "fields": { + "name": "Can delete editorial communication", + "content_type": 34, + "codename": "delete_editorialcommunication" + } +}, +{ + "model": "auth.permission", + "pk": 107, + "fields": { + "name": "Can add referee invitation", + "content_type": 35, + "codename": "add_refereeinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 108, + "fields": { + "name": "Can change referee invitation", + "content_type": 35, + "codename": "change_refereeinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 109, + "fields": { + "name": "Can delete referee invitation", + "content_type": 35, + "codename": "delete_refereeinvitation" + } +}, +{ + "model": "auth.permission", + "pk": 110, + "fields": { + "name": "Can add submission", + "content_type": 36, + "codename": "add_submission" + } +}, +{ + "model": "auth.permission", + "pk": 111, + "fields": { + "name": "Can change submission", + "content_type": 36, + "codename": "change_submission" + } +}, +{ + "model": "auth.permission", + "pk": 112, + "fields": { + "name": "Can delete submission", + "content_type": 36, + "codename": "delete_submission" + } +}, +{ + "model": "auth.permission", + "pk": 113, + "fields": { + "name": "Can take editorial actions", + "content_type": 36, + "codename": "can_take_editorial_actions" + } +}, +{ + "model": "auth.permission", + "pk": 114, + "fields": { + "name": "Can add report", + "content_type": 37, + "codename": "add_report" + } +}, +{ + "model": "auth.permission", + "pk": 115, + "fields": { + "name": "Can change report", + "content_type": 37, + "codename": "change_report" + } +}, +{ + "model": "auth.permission", + "pk": 116, + "fields": { + "name": "Can delete report", + "content_type": 37, + "codename": "delete_report" + } +}, +{ + "model": "auth.permission", + "pk": 117, + "fields": { + "name": "Can add eic recommendation", + "content_type": 38, + "codename": "add_eicrecommendation" + } +}, +{ + "model": "auth.permission", + "pk": 118, + "fields": { + "name": "Can change eic recommendation", + "content_type": 38, + "codename": "change_eicrecommendation" + } +}, +{ + "model": "auth.permission", + "pk": 119, + "fields": { + "name": "Can delete eic recommendation", + "content_type": 38, + "codename": "delete_eicrecommendation" + } +}, +{ + "model": "auth.permission", + "pk": 120, + "fields": { + "name": "Can add editorial assignment", + "content_type": 39, + "codename": "add_editorialassignment" + } +}, +{ + "model": "auth.permission", + "pk": 121, + "fields": { + "name": "Can change editorial assignment", + "content_type": 39, + "codename": "change_editorialassignment" + } +}, +{ + "model": "auth.permission", + "pk": 122, + "fields": { + "name": "Can delete editorial assignment", + "content_type": 39, + "codename": "delete_editorialassignment" + } +}, +{ + "model": "auth.permission", + "pk": 123, + "fields": { + "name": "Can add thesis link", + "content_type": 40, + "codename": "add_thesislink" + } +}, +{ + "model": "auth.permission", + "pk": 124, + "fields": { + "name": "Can change thesis link", + "content_type": 40, + "codename": "change_thesislink" + } +}, +{ + "model": "auth.permission", + "pk": 125, + "fields": { + "name": "Can delete thesis link", + "content_type": 40, + "codename": "delete_thesislink" + } +}, +{ + "model": "auth.permission", + "pk": 126, + "fields": { + "name": "Can vet registration requests", + "content_type": 26, + "codename": "can_vet_registration_requests" + } +}, +{ + "model": "auth.permission", + "pk": 127, + "fields": { + "name": "Can draft registration invitations", + "content_type": 26, + "codename": "can_draft_registration_invitations" + } +}, +{ + "model": "auth.permission", + "pk": 128, + "fields": { + "name": "Can manage registration invitations", + "content_type": 26, + "codename": "can_manage_registration_invitations" + } +}, +{ + "model": "auth.permission", + "pk": 129, + "fields": { + "name": "Can invite Fellows", + "content_type": 26, + "codename": "can_invite_Fellows" + } +}, +{ + "model": "auth.permission", + "pk": 130, + "fields": { + "name": "Can email group members", + "content_type": 26, + "codename": "can_email_group_members" + } +}, +{ + "model": "auth.permission", + "pk": 131, + "fields": { + "name": "Can email particulars", + "content_type": 26, + "codename": "can_email_particulars" + } +}, +{ + "model": "auth.permission", + "pk": 132, + "fields": { + "name": "Can view By-laws of Editorial College", + "content_type": 26, + "codename": "view_bylaws" + } +}, +{ + "model": "auth.permission", + "pk": 133, + "fields": { + "name": "Can submit Comments", + "content_type": 26, + "codename": "can_submit_comments" + } +}, +{ + "model": "auth.permission", + "pk": 134, + "fields": { + "name": "Can express opinion on Comments", + "content_type": 26, + "codename": "can_express_opinion_on_comments" + } +}, +{ + "model": "auth.permission", + "pk": 135, + "fields": { + "name": "Can request opening of Commentara Pages", + "content_type": 26, + "codename": "can_request_commentary_pages" + } +}, +{ + "model": "auth.permission", + "pk": 136, + "fields": { + "name": "Can request Thesis Links", + "content_type": 26, + "codename": "can_request_thesislinks" + } +}, +{ + "model": "auth.permission", + "pk": 137, + "fields": { + "name": "Can vet Commentary page requests", + "content_type": 26, + "codename": "can_vet_commentary_requests" + } +}, +{ + "model": "auth.permission", + "pk": 138, + "fields": { + "name": "Can vet Thesis Link requests", + "content_type": 26, + "codename": "can_vet_thesislink_requests" + } +}, +{ + "model": "auth.permission", + "pk": 139, + "fields": { + "name": "Can vet Authorship claims", + "content_type": 26, + "codename": "can_vet_authorship_claims" + } +}, +{ + "model": "auth.permission", + "pk": 140, + "fields": { + "name": "Can vet submitted Comments", + "content_type": 26, + "codename": "can_vet_comments" + } +}, +{ + "model": "auth.permission", + "pk": 141, + "fields": { + "name": "Can submit manuscript", + "content_type": 26, + "codename": "can_submit_manuscript" + } +}, +{ + "model": "auth.permission", + "pk": 142, + "fields": { + "name": "Can view Submissions Pool", + "content_type": 26, + "codename": "can_view_pool" + } +}, +{ + "model": "auth.permission", + "pk": 143, + "fields": { + "name": "Can assign incoming Submissions to potential Editor-in-charge", + "content_type": 26, + "codename": "can_assign_submissions" + } +}, +{ + "model": "auth.permission", + "pk": 144, + "fields": { + "name": "Can take charge (become Editor-in-charge) of submissions", + "content_type": 26, + "codename": "can_take_charge_of_submissions" + } +}, +{ + "model": "auth.permission", + "pk": 145, + "fields": { + "name": "Can vet submitted Reports", + "content_type": 26, + "codename": "can_vet_submitted_reports" + } +}, +{ + "model": "auth.permission", + "pk": 146, + "fields": { + "name": "Can act as a referee and submit reports on Submissions", + "content_type": 26, + "codename": "can_referee" + } +}, +{ + "model": "auth.permission", + "pk": 147, + "fields": { + "name": "Can prepare recommendations for voting", + "content_type": 26, + "codename": "can_prepare_recommendations_for_voting" + } +}, +{ + "model": "auth.permission", + "pk": 148, + "fields": { + "name": "Can fix the College voting decision", + "content_type": 26, + "codename": "can_fix_College_decision" + } +}, +{ + "model": "auth.permission", + "pk": 149, + "fields": { + "name": "Can publish accepted submission", + "content_type": 26, + "codename": "can_publish_accepted_submission" + } +} +] diff --git a/scipost/forms.py b/scipost/forms.py index 3716ad8d83f4800b3c1e0eed0756279746f16bda..f8d0629bd7684d0ae9d2ee559d420167aadaf496 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -11,6 +11,7 @@ from crispy_forms.helper import FormHelper from crispy_forms.layout import Layout, Div, Field, Fieldset, HTML, Submit from .models import * +from .constants import SCIPOST_DISCIPLINES from journals.models import Publication from submissions.models import SUBMISSION_STATUS_PUBLICLY_UNLISTED diff --git a/scipost/models.py b/scipost/models.py index 2962da77cc4716fcf50d66db6668033d42deb937..033c08ec50202e4cc88e200a4db4f8ee523a5be2 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -10,129 +10,10 @@ from django.utils.safestring import mark_safe from django_countries.fields import CountryField -from scipost.models import * - +from .constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS,\ + disciplines_dict, subject_areas_dict -SCIPOST_DISCIPLINES = ( - ('physics', 'Physics'), - ('astrophysics', 'Astrophysics'), - ('mathematics', 'Mathematics'), - ('computerscience', 'Computer Science'), - ) -disciplines_dict = dict(SCIPOST_DISCIPLINES) - -SCIPOST_SUBJECT_AREAS = ( - ('Physics', ( - ('Phys:AE', 'Atomic, Molecular and Optical Physics - Experiment'), - ('Phys:AT', 'Atomic, Molecular and Optical Physics - Theory'), - ('Phys:BI', 'Biophysics'), - ('Phys:CE', 'Condensed Matter Physics - Experiment'), - ('Phys:CT', 'Condensed Matter Physics - Theory'), - ('Phys:FD', 'Fluid Dynamics'), - ('Phys:GR', 'Gravitation, Cosmology and Astroparticle Physics'), - ('Phys:HE', 'High-Energy Physics - Experiment'), - ('Phys:HT', 'High-Energy Physics- Theory'), - ('Phys:HP', 'High-Energy Physics - Phenomenology'), - ('Phys:MP', 'Mathematical Physics'), - ('Phys:NE', 'Nuclear Physics - Experiment'), - ('Phys:NT', 'Nuclear Physics - Theory'), - ('Phys:QP', 'Quantum Physics'), - ('Phys:SM', 'Statistical and Soft Matter Physics'), - ) - ), - ('Astrophysics', ( - ('Astro:GA', 'Astrophysics of Galaxies'), - ('Astro:CO', 'Cosmology and Nongalactic Astrophysics'), - ('Astro:EP', 'Earth and Planetary Astrophysics'), - ('Astro:HE', 'High Energy Astrophysical Phenomena'), - ('Astro:IM', 'Instrumentation and Methods for Astrophysics'), - ('Astro:SR', 'Solar and Stellar Astrophysics'), - ) - ), - ('Mathematics', ( - ('Math:AG', 'Algebraic Geometry'), - ('Math:AT', 'Algebraic Topology'), - ('Math:AP', 'Analysis of PDEs'), - ('Math:CT', 'Category Theory'), - ('Math:CA', 'Classical Analysis and ODEs'), - ('Math:CO', 'Combinatorics'), - ('Math:AC', 'Commutative Algebra'), - ('Math:CV', 'Complex Variables'), - ('Math:DG', 'Differential Geometry'), - ('Math:DS', 'Dynamical Systems'), - ('Math:FA', 'Functional Analysis'), - ('Math:GM', 'General Mathematics'), - ('Math:GN', 'General Topology'), - ('Math:GT', 'Geometric Topology'), - ('Math:GR', 'Group Theory'), - ('Math:HO', 'History and Overview'), - ('Math:IT', 'Information Theory'), - ('Math:KT', 'K-Theory and Homology'), - ('Math:LO', 'Logic'), - ('Math:MP', 'Mathematical Physics'), - ('Math:MG', 'Metric Geometry'), - ('Math:NT', 'Number Theory'), - ('Math:NA', 'Numerical Analysis'), - ('Math:OA', 'Operator Algebras'), - ('Math:OC', 'Optimization and Control'), - ('Math:PR', 'Probability'), - ('Math:QA', 'Quantum Algebra'), - ('Math:RT', 'Representation Theory'), - ('Math:RA', 'Rings and Algebras'), - ('Math:SP', 'Spectral Theory'), - ('Math:ST', 'Statistics Theory'), - ('Math:SG', 'Symplectic Geometry'), - ) - ), - ('Computer Science', ( - ('Comp:AI', 'Artificial Intelligence'), - ('Comp:CC', 'Computational Complexity'), - ('Comp:CE', 'Computational Engineering, Finance, and Science'), - ('Comp:CG', 'Computational Geometry'), - ('Comp:GT', 'Computer Science and Game Theory'), - ('Comp:CV', 'Computer Vision and Pattern Recognition'), - ('Comp:CY', 'Computers and Society'), - ('Comp:CR', 'Cryptography and Security'), - ('Comp:DS', 'Data Structures and Algorithms'), - ('Comp:DB', 'Databases'), - ('Comp:DL', 'Digital Libraries'), - ('Comp:DM', 'Discrete Mathematics'), - ('Comp:DC', 'Distributed, Parallel, and Cluster Computing'), - ('Comp:ET', 'Emerging Technologies'), - ('Comp:FL', 'Formal Languages and Automata Theory'), - ('Comp:GL', 'General Literature'), - ('Comp:GR', 'Graphics'), - ('Comp:AR', 'Hardware Architecture'), - ('Comp:HC', 'Human-Computer Interaction'), - ('Comp:IR', 'Information Retrieval'), - ('Comp:IT', 'Information Theory'), - ('Comp:LG', 'Learning'), - ('Comp:LO', 'Logic in Computer Science'), - ('Comp:MS', 'Mathematical Software'), - ('Comp:MA', 'Multiagent Systems'), - ('Comp:MM', 'Multimedia'), - ('Comp:NI', 'Networking and Internet Architecture'), - ('Comp:NE', 'Neural and Evolutionary Computing'), - ('Comp:NA', 'Numerical Analysis'), - ('Comp:OS', 'Operating Systems'), - ('Comp:OH', 'Other Computer Science'), - ('Comp:PF', 'Performance'), - ('Comp:PL', 'Programming Languages'), - ('Comp:RO', 'Robotics'), - ('Comp:SI', 'Social and Information Networks'), - ('Comp:SE', 'Software Engineering'), - ('Comp:SD', 'Sound'), - ('Comp:SC', 'Symbolic Computation'), - ('Comp:SY', 'Systems and Control'), - ) - ), -) -subject_areas_raw_dict = dict(SCIPOST_SUBJECT_AREAS) - -# Make dict of the form {'Phys:AT': 'Atomic...', ...} -subject_areas_dict = {} -for k in subject_areas_raw_dict.keys(): - subject_areas_dict.update(dict(subject_areas_raw_dict[k])) +from scipost.models import * class ChoiceArrayField(ArrayField): @@ -179,6 +60,19 @@ TITLE_CHOICES = ( title_dict = dict(TITLE_CHOICES) +class TimeStampedModel(models.Model): + """ + All objects should inherit from this abstract model. + This will ensure the creation of created and modified + timestamps in the objects. + """ + created = models.DateTimeField(auto_now_add=True) + latest_activity = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + class Contributor(models.Model): """ All users of SciPost are Contributors. @@ -212,7 +106,6 @@ class Contributor(models.Model): default=True, verbose_name="I accept to receive SciPost emails") - def __str__(self): return '%s, %s' % (self.user.last_name, self.user.first_name) diff --git a/submissions/models.py b/submissions/models.py index 68a3d1e867b2720f97fd109dee8cd1d8a18d681a..2ea5a6c80cecdb9cdbc796f30b55e1ec87b17d8a 100644 --- a/submissions/models.py +++ b/submissions/models.py @@ -8,8 +8,8 @@ from django.template import Template, Context from .models import * from scipost.models import ChoiceArrayField, Contributor, title_dict, Remark -from scipost.models import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS -from scipost.models import subject_areas_dict, TITLE_CHOICES +from scipost.constants import SCIPOST_DISCIPLINES, SCIPOST_SUBJECT_AREAS, subject_areas_dict +from scipost.models import TITLE_CHOICES from journals.models import SCIPOST_JOURNALS_SUBMIT, SCIPOST_JOURNALS_DOMAINS from journals.models import SCIPOST_JOURNALS_SPECIALIZATIONS from journals.models import journals_submit_dict, journals_domains_dict, journals_spec_dict diff --git a/theses/factories.py b/theses/factories.py new file mode 100644 index 0000000000000000000000000000000000000000..1a12cd026941ad06ef7e4ad82c798b6947813a93 --- /dev/null +++ b/theses/factories.py @@ -0,0 +1,19 @@ +import factory +from .models import ThesisLink +from scipost.factories import ContributorFactory + + +class ThesisLinkFactory(factory.django.DjangoModelFactory): + class Meta: + model = ThesisLink + + requested_by = factory.SubFactory(ContributorFactory) + type = ThesisLink.MASTER_THESIS + title = factory.Sequence(lambda n: "thesis {0}".format(n)) + pub_link = factory.Faker('uri') + author = factory.Faker('name') + supervisor = factory.Faker('name') + institution = factory.Faker('company') + defense_date = factory.Faker('date_time_this_century') + abstract = factory.Faker('text') + domain = 'ET' diff --git a/theses/forms.py b/theses/forms.py index 95849d3b3d8bd0bb00ca8de031ee2326950a7651..13b4a1793a47b46f2e9c993e8c55c2d86ebf0834 100644 --- a/theses/forms.py +++ b/theses/forms.py @@ -1,6 +1,7 @@ from django import forms from .models import * +from .helpers import past_years THESIS_ACTION_CHOICES = ( (0, 'modify'), @@ -14,18 +15,18 @@ THESIS_REFUSAL_CHOICES = ( (-2, 'the external link to this thesis does not work'), ) + class RequestThesisLinkForm(forms.ModelForm): class Meta: model = ThesisLink fields = ['type', 'discipline', 'domain', 'subject_area', 'title', 'author', 'supervisor', 'institution', 'defense_date', 'pub_link', 'abstract'] + widgets = { + 'defense_date': forms.SelectDateWidget(years=past_years(50)), + 'pub_link': forms.TextInput(attrs={'placeholder': 'Full URL'}) + } - def __init__(self, *args, **kwargs): - super(RequestThesisLinkForm, self).__init__(*args, **kwargs) - self.fields['defense_date'].widget.attrs.update({'placeholder': 'Format: YYYY-MM-DD'}) - self.fields['pub_link'].widget.attrs.update({'placeholder': 'Full URL'}) - self.fields['abstract'].widget.attrs.update({'cols': 100}) class VetThesisLinkForm(forms.Form): action_option = forms.ChoiceField(widget=forms.RadioSelect, @@ -35,6 +36,7 @@ class VetThesisLinkForm(forms.Form): email_response_field = forms.CharField(widget=forms.Textarea( attrs={'rows': 5, 'cols': 40}), label='Justification (optional)', required=False) + class ThesisLinkSearchForm(forms.Form): author = forms.CharField(max_length=100, required=False, label="Author") title_keyword = forms.CharField(max_length=100, label="Title", required=False) diff --git a/theses/helpers.py b/theses/helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..7b2961e45306298fd6544c8e515f7a8d22675579 --- /dev/null +++ b/theses/helpers.py @@ -0,0 +1,9 @@ +import datetime + + +def past_years(n): + ''' + Gives back list of integers representing a range of n years, counting down from current year. + ''' + this_year = datetime.datetime.now().year + return range(this_year, this_year - n, -1) diff --git a/theses/migrations/0006_auto_20161219_2012.py b/theses/migrations/0006_auto_20161219_2012.py new file mode 100644 index 0000000000000000000000000000000000000000..935f906aacc44850699e1d60558ba9c3fd845f31 --- /dev/null +++ b/theses/migrations/0006_auto_20161219_2012.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.3 on 2016-12-19 19:12 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('theses', '0005_remove_thesislink_specialization'), + ] + + operations = [ + migrations.AlterField( + model_name='thesislink', + name='domain', + field=models.CharField(choices=[('E', 'Experimental'), ('T', 'Theoretical'), ('C', 'Computational'), ('ET', 'Exp. & Theor.'), ('EC', 'Exp. & Comp.'), ('TC', 'Theor. & Comp.'), ('ETC', 'Exp., Theor. & Comp.')], max_length=3), + ), + ] diff --git a/theses/models.py b/theses/models.py index 37fcb52f07fd7f09fc90c3a53982bd18c9d162e8..50ec6b1f47548e9f43d6f53d31d091883f4ed091 100644 --- a/theses/models.py +++ b/theses/models.py @@ -6,17 +6,21 @@ from django.template import Template, Context from .models import * from journals.models import * +from scipost.constants import SCIPOST_DISCIPLINES, subject_areas_dict, disciplines_dict from scipost.models import * -THESIS_TYPES = ( - ('MA', 'Master\'s'), - ('PhD', 'Ph.D.'), - ('Hab', 'Habilitation'), - ) -thesis_type_dict = dict(THESIS_TYPES) - class ThesisLink(models.Model): + MASTER_THESIS = 'MA' + PHD_THESIS = 'PhD' + HABILITATION_THESIS = 'Hab' + THESIS_TYPES = ( + (MASTER_THESIS, 'Master\'s'), + (PHD_THESIS, 'Ph.D.'), + (HABILITATION_THESIS, 'Habilitation'), + ) + THESIS_TYPES_DICT = dict(THESIS_TYPES) + """ An URL pointing to a thesis """ requested_by = models.ForeignKey( Contributor, blank=True, null=True, @@ -26,13 +30,13 @@ class ThesisLink(models.Model): vetted_by = models.ForeignKey( Contributor, blank=True, null=True, on_delete=models.CASCADE) - type = models.CharField(max_length=3, choices=THESIS_TYPES) + type = models.CharField(choices=THESIS_TYPES, max_length=3) discipline = models.CharField( max_length=20, choices=SCIPOST_DISCIPLINES, default='physics') domain = models.CharField( max_length=3, choices=SCIPOST_JOURNALS_DOMAINS, - blank=True) + blank=False) subject_area = models.CharField( max_length=10, choices=SCIPOST_SUBJECT_AREAS, @@ -85,7 +89,7 @@ class ThesisLink(models.Model): header += '<td>(not claimed)</td>' header += ( '</tr>' - '<tr><td>Type: </td><td></td><td>' + thesis_type_dict[self.type] + + '<tr><td>Type: </td><td></td><td>' + self.THESIS_TYPES_DICT[self.type] + '</td></tr>' '<tr><td>Discipline: </td><td></td><td>' + disciplines_dict[self.discipline] + '</td></tr>' @@ -110,11 +114,13 @@ class ThesisLink(models.Model): 'pub_link': self.pub_link, 'institution': self.institution, 'supervisor': self.supervisor, 'defense_date': self.defense_date, 'latest_activity': self.latest_activity.strftime('%Y-%m-%d %H:%M')}) + print(subject_areas_dict) + print(self.subject_area in subject_areas_dict) header = ( '<li><div class="flex-container">' '<div class="flex-whitebox0"><p><a href="/thesis/{{ id }}" ' 'class="pubtitleli">{{ title }}</a></p>' - '<p>' + thesis_type_dict[self.type] + ' thesis by {{ author }} ' + '<p>' + self.THESIS_TYPES_DICT[self.type] + ' thesis by {{ author }} ' '(supervisor(s): {{ supervisor }}) in ' + disciplines_dict[self.discipline] + ', ' + journals_domains_dict[self.domain] + ' ' + @@ -133,7 +139,7 @@ class ThesisLink(models.Model): '<li><div class="flex-container">' '<div class="flex-whitebox0"><p><a href="/thesis/{{ id }}" ' 'class="pubtitleli">{{ title }}</a></p>' - '<p>' + thesis_type_dict[self.type] + + '<p>' + self.THESIS_TYPES_DICT[self.type] + ' thesis by {{ author }} </div></div></li>') template = Template(header) return template.render(context) diff --git a/theses/test_forms.py b/theses/test_forms.py new file mode 100644 index 0000000000000000000000000000000000000000..ce07aa6214b9811d4b0ca54e09c7b5b304b93901 --- /dev/null +++ b/theses/test_forms.py @@ -0,0 +1,26 @@ +import factory + +from django.test import TestCase + +from .factories import ThesisLinkFactory +from .forms import RequestThesisLinkForm +from common.helpers import model_form_data + + +class TestRequestThesisLink(TestCase): + fixtures = ['permissions', 'groups'] + + def setUp(self): + self.valid_form_data = model_form_data(ThesisLinkFactory(), RequestThesisLinkForm) + + def test_valid_data_is_valid(self): + form_data = self.valid_form_data + form = RequestThesisLinkForm(self.valid_form_data) + self.assertTrue(form.is_valid()) + + def test_empty_domain_is_invalid(self): + form_data = self.valid_form_data + form_data['domain'] = '' + form = RequestThesisLinkForm(form_data) + form.is_valid() + self.assertEqual(form.errors['domain'], ['This field is required.']) diff --git a/theses/test_models.py b/theses/test_models.py new file mode 100644 index 0000000000000000000000000000000000000000..4308ab25f1d22c56e33cb5047cb6f571fe080bf7 --- /dev/null +++ b/theses/test_models.py @@ -0,0 +1,15 @@ +import re + +from django.test import TestCase +from django.core.exceptions import ValidationError + +from .models import ThesisLink +from .factories import ThesisLinkFactory + + +class ThesisLinkTestCase(TestCase): + def test_domain_cannot_be_blank(self): + thesis_link = ThesisLinkFactory() + thesis_link.domain = "" + self.assertRaisesRegexp(ValidationError, re.compile(r'domain'), + thesis_link.full_clean) diff --git a/theses/test_views.py b/theses/test_views.py new file mode 100644 index 0000000000000000000000000000000000000000..7e70a6dd41170d221dfd8a4d586a4a8b0ff0c38c --- /dev/null +++ b/theses/test_views.py @@ -0,0 +1,42 @@ +from django.test import TestCase, RequestFactory +from django.test.client import Client +from django.contrib.auth.models import AnonymousUser +from django.urls import reverse + +from .views import RequestThesisLink +from scipost.factories import UserFactory +from .factories import ThesisLinkFactory +from .models import ThesisLink + + +class TestThesisDetail(TestCase): + fixtures = ['groups', 'permissions'] + + def test_visits_valid_thesis_detail(self): + thesis_link = ThesisLinkFactory() + client = Client() + target = reverse('theses:thesis', kwargs={'thesislink_id': thesis_link.id}) + response = client.post(target) + self.assertEqual(response.status_code, 200) + + +class TestRequestThesisLink(TestCase): + fixtures = ['groups', 'permissions'] + + def setUp(self): + self.client = Client() + + def test_response_when_not_logged_in(self): + '''A visitor that is not logged in cannot view this page.''' + response = self.client.get(reverse('theses:request_thesislink')) + self.assertEqual(response.status_code, 403) + + def test_response_when_logged_in(self): + request = RequestFactory().get(reverse('theses:request_thesislink')) + request.user = UserFactory() + response = RequestThesisLink.as_view()(request) + self.assertEqual(response.status_code, 200) + + def test_redirects_to_acknowledgement_page(self): + response = self.client.post(reverse('theses:request_thesislink'), {}, follow=True) + self.assertRedirects(response, reverse('scipost:acknowledgement')) diff --git a/theses/urls.py b/theses/urls.py index 59758c05e57add2fa5e09e8fdcca125810b36e36..05839aa7550bedf319b58d82bb3e7aa59703cacf 100644 --- a/theses/urls.py +++ b/theses/urls.py @@ -7,9 +7,10 @@ urlpatterns = [ # Thesis Links url(r'^$', views.theses, name='theses'), url(r'^browse/(?P<discipline>[a-z]+)/(?P<nrweeksback>[0-9]+)/$', views.browse, name='browse'), - #url(r'^thesis/(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'), url(r'^(?P<thesislink_id>[0-9]+)/$', views.thesis_detail, name='thesis'), - url(r'^request_thesislink$', views.request_thesislink, name='request_thesislink'), - url(r'^vet_thesislink_requests$', views.vet_thesislink_requests, name='vet_thesislink_requests'), - url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$', views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'), + url(r'^request_thesislink$', views.RequestThesisLink.as_view(), name='request_thesislink'), + url(r'^vet_thesislink_requests$', views.vet_thesislink_requests, + name='vet_thesislink_requests'), + url(r'^vet_thesislink_request_ack/(?P<thesislink_id>[0-9]+)$', + views.vet_thesislink_request_ack, name='vet_thesislink_request_ack'), ] diff --git a/theses/views.py b/theses/views.py index 882c2f192f88353debfe4235bbcf6154ab763b27..7e738e39d719d57235c783350d1a1d2b4c6cb814 100644 --- a/theses/views.py +++ b/theses/views.py @@ -1,4 +1,5 @@ import datetime + from django.utils import timezone from django.shortcuts import get_object_or_404, render from django.contrib.auth import authenticate, login, logout @@ -9,6 +10,8 @@ from django.core.urlresolvers import reverse from django.http import HttpResponse, HttpResponseRedirect from django.views.decorators.csrf import csrf_protect from django.db.models import Avg +from django.views.generic.edit import CreateView +from django.utils.decorators import method_decorator from .models import * from .forms import * @@ -17,55 +20,40 @@ from comments.models import Comment from comments.forms import CommentForm from scipost.forms import TITLE_CHOICES, AuthenticationForm -title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails + +title_dict = dict(TITLE_CHOICES) # Convert titles for use in emails ################ # Theses ################ -@permission_required('scipost.can_request_thesislinks', raise_exception=True) -def request_thesislink(request): - if request.method == 'POST': - form = RequestThesisLinkForm(request.POST) - if form.is_valid(): - contributor = Contributor.objects.get(user=request.user) - thesislink = ThesisLink ( - requested_by = contributor, - type = form.cleaned_data['type'], - discipline = form.cleaned_data['discipline'], - domain = form.cleaned_data['domain'], - subject_area = form.cleaned_data['subject_area'], - title = form.cleaned_data['title'], - author = form.cleaned_data['author'], - supervisor = form.cleaned_data['supervisor'], - institution = form.cleaned_data['institution'], - defense_date = form.cleaned_data['defense_date'], - pub_link = form.cleaned_data['pub_link'], - abstract = form.cleaned_data['abstract'], - latest_activity = timezone.now(), - ) - thesislink.save() - #return HttpResponseRedirect('request_thesislink_ack') - context = {'ack_header': 'Thank you for your request for a Thesis Link', - 'ack_message': 'Your request will soon be handled by an Editor. ', - 'followup_message': 'Return to your ', - 'followup_link': reverse('scipost:personal_page'), - 'followup_link_label': 'personal page'} - return render(request, 'scipost/acknowledgement.html', context) - else: - form = RequestThesisLinkForm() - return render(request, 'theses/request_thesislink.html', {'form': form}) +@method_decorator(permission_required( + 'scipost.can_request_thesislinks', raise_exception=True), name='dispatch') +class RequestThesisLink(CreateView): + form_class = RequestThesisLinkForm + template_name = 'theses/request_thesislink.html' + success_url = '' + + def form_valid(self, form): + context = {'ack_header': 'Thank you for your request for a Thesis Link', + 'ack_message': 'Your request will soon be handled by an Editor. ', + 'followup_message': 'Return to your ', + 'followup_link': reverse('scipost:personal_page'), + 'followup_link_label': 'personal page'} + return render(self.request, 'scipost/acknowledgement.html', context) @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) def vet_thesislink_requests(request): contributor = Contributor.objects.get(user=request.user) - thesislink_to_vet = ThesisLink.objects.filter(vetted=False).first() # only handle one at a time + thesislink_to_vet = ThesisLink.objects.filter( + vetted=False).first() # only handle one at a time form = VetThesisLinkForm() - context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form } + context = {'contributor': contributor, 'thesislink_to_vet': thesislink_to_vet, 'form': form} return render(request, 'theses/vet_thesislink_requests.html', context) + @permission_required('scipost.can_vet_thesislink_requests', raise_exception=True) def vet_thesislink_request_ack(request, thesislink_id): if request.method == 'POST': @@ -112,8 +100,8 @@ def vet_thesislink_request_ack(request, thesislink_id): ['theses@scipost.org'], reply_to=['theses@scipost.org']) # Don't send email yet... only when option 1 has succeeded! - #emailmessage.send(fail_silently=False) - context = {'form': form2 } + # emailmessage.send(fail_silently=False) + context = {'form': form2} return render(request, 'theses/request_thesislink.html', context) elif form.cleaned_data['action_option'] == '2': email_text = ('Dear ' + title_dict[thesislink.requested_by.title] + ' ' @@ -124,7 +112,8 @@ def vet_thesislink_request_ack(request, thesislink_id): + form.cleaned_data['refusal_reason'] + '.\n\nThank you for your interest, \nThe SciPost Team.') if form.cleaned_data['email_response_field']: - email_text += '\n\nFurther explanations: ' + form.cleaned_data['email_response_field'] + email_text += '\n\nFurther explanations: ' + \ + form.cleaned_data['email_response_field'] emailmessage = EmailMessage('SciPost Thesis Link', email_text, 'SciPost Theses <theses@scipost.org>', [thesislink.requested_by.user.email], @@ -133,8 +122,6 @@ def vet_thesislink_request_ack(request, thesislink_id): emailmessage.send(fail_silently=False) thesislink.delete() - #context = {'thesislink_id': thesislink_id } - #return render(request, 'theses/vet_thesislink_request_ack.html', context) context = {'ack_header': 'Thesis Link request vetted.', 'followup_message': 'Return to the ', 'followup_link': reverse('theses:vet_thesislink_requests'), @@ -152,7 +139,7 @@ def theses(request): abstract__icontains=form.cleaned_data['abstract_keyword'], supervisor__icontains=form.cleaned_data['supervisor'], vetted=True, - ) + ) thesislink_search_list.order_by('-pub_date') else: thesislink_search_list = [] @@ -165,7 +152,7 @@ def theses(request): .filter(vetted=True, latest_activity__gte=timezone.now() + datetime.timedelta(days=-7))) context = {'form': form, 'thesislink_search_list': thesislink_search_list, - 'thesislink_recent_list': thesislink_recent_list } + 'thesislink_recent_list': thesislink_recent_list} return render(request, 'theses/theses.html', context) @@ -179,11 +166,11 @@ def browse(request, discipline, nrweeksback): abstract__icontains=form.cleaned_data['abstract_keyword'], supervisor__icontains=form.cleaned_data['supervisor'], vetted=True, - ) + ) thesislink_search_list.order_by('-pub_date') else: thesislink_search_list = [] - context = {'form': form, 'thesislink_search_list': thesislink_search_list } + context = {'form': form, 'thesislink_search_list': thesislink_search_list} return HttpResponseRedirect(request, 'theses/theses.html', context) else: form = ThesisLinkSearchForm() @@ -192,7 +179,7 @@ def browse(request, discipline, nrweeksback): latest_activity__gte=timezone.now() + datetime.timedelta(weeks=-int(nrweeksback)))) context = {'form': form, 'discipline': discipline, 'nrweeksback': nrweeksback, - 'thesislink_browse_list': thesislink_browse_list } + 'thesislink_browse_list': thesislink_browse_list} return render(request, 'theses/theses.html', context) @@ -203,26 +190,27 @@ def thesis_detail(request, thesislink_id): form = CommentForm(request.POST) if form.is_valid(): author = Contributor.objects.get(user=request.user) - newcomment = Comment ( - thesislink = thesislink, - author = author, - is_rem = form.cleaned_data['is_rem'], - is_que = form.cleaned_data['is_que'], - is_ans = form.cleaned_data['is_ans'], - is_obj = form.cleaned_data['is_obj'], - is_rep = form.cleaned_data['is_rep'], - is_val = form.cleaned_data['is_val'], - is_lit = form.cleaned_data['is_lit'], - is_sug = form.cleaned_data['is_sug'], - comment_text = form.cleaned_data['comment_text'], - remarks_for_editors = form.cleaned_data['remarks_for_editors'], - date_submitted = timezone.now(), - ) - newcomment.save() + new_comment = Comment( + thesislink=thesislink, + author=author, + is_rem=form.cleaned_data['is_rem'], + is_que=form.cleaned_data['is_que'], + is_ans=form.cleaned_data['is_ans'], + is_obj=form.cleaned_data['is_obj'], + is_rep=form.cleaned_data['is_rep'], + is_val=form.cleaned_data['is_val'], + is_lit=form.cleaned_data['is_lit'], + is_sug=form.cleaned_data['is_sug'], + comment_text=form.cleaned_data['comment_text'], + remarks_for_editors=form.cleaned_data['remarks_for_editors'], + date_submitted=timezone.now(), + ) + new_comment.save() author.nr_comments = Comment.objects.filter(author=author).count() author.save() request.session['thesislink_id'] = thesislink_id - return HttpResponseRedirect(reverse('comments:comment_submission_ack')) + context = {} + return render(request, 'scipost/acknowledgement.html', context) else: form = CommentForm()