diff --git a/SciPost_v1/settings.py b/SciPost_v1/settings.py index 2cd8bb1716d85215dcd2b19e44996cea52aef773..663a264900986b2295e2280dc8e822bc0a184f56 100644 --- a/SciPost_v1/settings.py +++ b/SciPost_v1/settings.py @@ -60,7 +60,6 @@ INSTALLED_APPS = ( 'django_mathjax', 'captcha', 'crispy_forms', - 'mptt', 'commentaries', 'comments', 'journals', diff --git a/scipost/admin.py b/scipost/admin.py index 0c57c906b26e55f5399acf9bc5358f8e0acf2389..ccfcd132ba3708b9bdf4a9c488c37081e1868784 100644 --- a/scipost/admin.py +++ b/scipost/admin.py @@ -26,3 +26,9 @@ admin.site.register(AuthorshipClaim) #admin.site.register(Opinion) admin.site.register(Permission) + +admin.site.register(Team) + +admin.site.register(Graph) + +admin.site.register(Node) diff --git a/scipost/forms.py b/scipost/forms.py index 9b254c2417115e43b60fb7708e25a5fe1df41692..2f22a11e05e4c50f400ce27b2ec85b52c7a21621 100644 --- a/scipost/forms.py +++ b/scipost/forms.py @@ -1,5 +1,7 @@ from django import forms +from django.db.models import Q + from django_countries import countries from django_countries.widgets import CountrySelectWidget from django_countries.fields import LazyTypedChoiceField @@ -102,3 +104,36 @@ class AuthorshipClaimForm(forms.Form): # class Meta: # model = Assessment # fields = ['relevance', 'importance', 'clarity', 'validity', 'rigour', 'originality', 'significance'] + + +class CreateTeamForm(forms.ModelForm): + class Meta: + model = Team + fields = ['name', ] + + def __init__(self, *args, **kwargs): + super(CreateTeamForm, self).__init__(*args, **kwargs) + self.fields['name'].widget.attrs.update({'size': 30, 'placeholder': 'Descriptive name for the new Team'}) + + +class AddTeamMemberForm(forms.Form): + def __init__(self, *args, **kwargs): + super(AddTeamMemberForm, self).__init__(*args, **kwargs) + self.fields['last_name'].widget.attrs.update({'size': 20, 'placeholder': 'Search in contributors database'}) + + last_name = forms.CharField() + + +class CreateGraphForm(forms.ModelForm): + class Meta: + model = Graph + fields = ['title', 'description', 'private', 'teams_with_access'] + + def __init__(self, *args, **kwargs): + contributor = kwargs.pop('contributor') + super(CreateGraphForm, self).__init__(*args, **kwargs) + self.fields['title'].widget.attrs.update({'size': 30, 'placeholder': 'Descriptive title for the new Graph'}) + self.fields['private'].widget.attrs.update({'placeholder': 'Private?'}) + self.fields['teams_with_access'] = forms.ModelMultipleChoiceField( + queryset=Team.objects.filter(Q(leader=contributor) | Q(members__in=[contributor]))) + self.fields['teams_with_access'].widget.attrs.update({'placeholder': 'Team to be given access rights:'}) diff --git a/scipost/management/commands/add_groups_and_permissions.py b/scipost/management/commands/add_groups_and_permissions.py index 656f947696d0f8571c0d4ab2ff607df06a5b89a1..8d55899986417d60b1e83d4744762dfc55e1ff26 100644 --- a/scipost/management/commands/add_groups_and_permissions.py +++ b/scipost/management/commands/add_groups_and_permissions.py @@ -87,6 +87,20 @@ class Command(BaseCommand): name= 'Can act as a referee and submit reports on Submissions', content_type=content_type) + # Teams + can_create_team, created = Permission.objects.get_or_create( + codename='can_create_team', + name= 'Can create a new Team', + content_type=content_type) + + # Graphs + can_create_graph, created = Permission.objects.get_or_create( + codename='can_create_graph', + name= 'Can create a new Graph', + content_type=content_type) + + + # Assign permissions to groups SciPostAdmin.permissions.add(can_manage_registration_invitations, can_vet_registration_requests, diff --git a/scipost/models.py b/scipost/models.py index eda478fd7b2788d0425ffd7575b0c770bc8ddb3f..18b7cec94a27eee579a1b2f9574ea7d73b93c733 100644 --- a/scipost/models.py +++ b/scipost/models.py @@ -6,8 +6,6 @@ from django.template import Template, Context from django_countries.fields import CountryField -from mptt.models import MPTTModel, TreeForeignKey - from .models import * SCIPOST_DISCIPLINES = ( @@ -267,28 +265,79 @@ class Team(models.Model): leader = models.ForeignKey(Contributor) members = models.ManyToManyField (Contributor, blank=True, related_name='team_members') name = models.CharField(max_length=20) + established = models.DateField(default=timezone.now) def __str__(self): - return name + ' (led by ' + leader.user.first_name + ' ' + leader.user.last_name + ')' - + return self.name + ' (led by ' + self.leader.user.first_name + ' ' + self.leader.user.last_name + ')' + + def header_as_li(self): + context = Context({'name': self.name,}) + output = '<li><p>Team {{ name }}, led by ' + self.leader.user.first_name + ' ' + self.leader.user.last_name + '</p>' + output += '<p>Members: ' + if not self.members.all(): + output += '(none yet, except for the leader)' + else : + for member in self.members.all(): + output += member.user.first_name + ' ' + member.user.last_name + ', ' + output += '</p></li>' + template = Template(output) + return template.render(context) -######### -# Lists # -######### -class Node(MPTTModel): +########## +# Graphs # +########## + +class Graph(models.Model): + """ + A Graph is a collection of Nodes with directed arrows, + representing e.g. a reading list, exploration path, etc. + If private, only the teams in teams_with_access can see/edit it. """ - Node of a list (tree of submissions, commentaries, thesislinks). - Requires django-mptt. + owner = models.ForeignKey(Contributor) + private = models.BooleanField(default=True) + teams_with_access = models.ManyToManyField(Team, blank=True) + title = models.CharField(max_length=100) + description = models.TextField(blank=True, null=True) + created = models.DateTimeField(default=timezone.now) + + def __str__(self): + return self.title[:30] + ' (owner: ' + self.owner.user.first_name + ' ' + self.owner.user.last_name + ')' + + def header_as_li(self): + context = Context({'id': self.id, 'title': self.title, + 'first_name': self.owner.user.first_name, + 'last_name': self.owner.user.last_name}) + template = Template(''' + <li><p>Graph <a href="{% url 'scipost:graph' graph_id=id %}">{{ title }}</a> (owner: {{ first_name }} {{ last_name }})</li> + ''') + return template.render(context) + + def contents(self): + context = Context({}) + output = self.description + template = Template(output) + return template.render(context) + + +class Node(models.Model): + """ + Node of a graph (directed). + Each node is composed of a set of submissions, commentaries, thesislinks. + Accessibility rights are set in the Graph ForeignKey. """ - owner = models.ForeignKey(Team) + graph = models.ForeignKey(Graph, default=None) + added_by = models.ForeignKey(Contributor, default=None) + created = models.DateTimeField(default=timezone.now) name = models.CharField(max_length=100) - private = models.BooleanField(default=True) - parent = TreeForeignKey('self', blank=True, null=True, related_name='children', db_index=True) + arcs_in = models.ManyToManyField('self', blank=True, related_name='node_arcs_in') # arcs pointing into this node description = models.TextField(blank=True, null=True) submissions = models.ManyToManyField('submissions.Submission', blank=True, related_name='node_submissions') commentaries = models.ManyToManyField('commentaries.Commentary', blank=True, related_name='node_commentaries') thesislinks = models.ManyToManyField('theses.ThesisLink', blank=True, related_name='node_thesislinks') annotation = models.TextField(blank=True, null=True) + def __str__(self): + return self.graph.title[:20] + ': ' + self.name[:20] + ' (owner: ' + self.owner.user.first_name + ' ' + self.owner.user.last_name + ')' + diff --git a/scipost/static/scipost/SciPost.css b/scipost/static/scipost/SciPost.css index 650ee53420c276fa74a559e39b68065fb5d9b52c..7666295725e604cba882a58504bc80c4486c0187 100644 --- a/scipost/static/scipost/SciPost.css +++ b/scipost/static/scipost/SciPost.css @@ -52,6 +52,7 @@ hr.hr12 { ul.personalTabMenu { background-color: #dddddd; display: inline-block; + font-size: 12px; padding: 0px; } ul.personalTabMenu li { diff --git a/scipost/templates/scipost/add_team_member.html b/scipost/templates/scipost/add_team_member.html new file mode 100644 index 0000000000000000000000000000000000000000..de02025bc93ae13ea7efe374da945eda207eb5e7 --- /dev/null +++ b/scipost/templates/scipost/add_team_member.html @@ -0,0 +1,35 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: add team member{% endblock pagetitle %} + +{% block bodysup %} + +<section> + <h1>Add Members to Team</h1> + <ul> + {{ team.header_as_li }} + </ul> + + {% if contributors_found %} + <h3>Identified as contributor:</h3> + <table> + {% for contributor in contributors_found %} + <tr><td>{{ contributor.user.first_name }} {{ contributor.user.last_name }}</td><td> </td> + <td><a href="{% url 'scipost:add_team_member' team_id=team.id contributor_id=contributor.id %}">Add to the Team</a></td></tr> + {% endfor %} + </table> + {% elif add_team_member_form.has_changed %} + <p>No Contributor with this last name could be identified.</p> + {% else %} + <p>Add a member to the Team:</p> + <form action="{% url 'scipost:add_team_member' team_id=team.id %}" method="post"> + {% csrf_token %} + {{ add_team_member_form }} + <input type="submit" value="Search" /> + </form> + {% endif %} + + +</section> + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/create_graph.html b/scipost/templates/scipost/create_graph.html new file mode 100644 index 0000000000000000000000000000000000000000..0a731d8072c5776b8d584f9338212447b86ad0ed --- /dev/null +++ b/scipost/templates/scipost/create_graph.html @@ -0,0 +1,22 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: create graph{% endblock pagetitle %} + +{% block bodysup %} + +<section> + {% if graphcreated %} + <p>{{ message }}</p> + {% else %} + <h1>Create a new Graph</h1> + <form action="{% url 'scipost:create_graph' %}" method="post"> + {% csrf_token %} + <table> + {{ create_graph_form.as_table }} + </table> + <input type="submit" value="Create Graph" /> + </form> + {% endif %} +</section> + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/create_team.html b/scipost/templates/scipost/create_team.html new file mode 100644 index 0000000000000000000000000000000000000000..e4e83002e69d1c37e07050aade4575b63be53a1d --- /dev/null +++ b/scipost/templates/scipost/create_team.html @@ -0,0 +1,30 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: create team{% endblock pagetitle %} + +{% block bodysup %} + +<section> + {% if teamcreated %} + <p>{{ message }}</p> + + <form action="{% url 'scipost:add_team_member' team_id=team.id %}" method="post"> + {% csrf_token %} + <table> + {{ add_team_member_form.as_table }} + </table> + <input type="submit" value="Search" /> + </form> + {% else %} + <h1>Create a new collaborative Team</h1> + <form action="{% url 'scipost:create_team' %}" method="post"> + {% csrf_token %} + <table> + {{ create_team_form.as_table }} + </table> + <input type="submit" value="Create Team" /> + </form> + {% endif %} +</section> + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/graph.html b/scipost/templates/scipost/graph.html new file mode 100644 index 0000000000000000000000000000000000000000..8b14e6e4cacdae6374fe9c9493d7a8985f2cc8f1 --- /dev/null +++ b/scipost/templates/scipost/graph.html @@ -0,0 +1,15 @@ +{% extends 'scipost/base.html' %} + +{% block pagetitle %}: graph{% endblock pagetitle %} + +{% block bodysup %} + +<section> + <h1>Graph</h1> + + {{ graph.contents }} + + +</section> + +{% endblock bodysup %} diff --git a/scipost/templates/scipost/personal_page.html b/scipost/templates/scipost/personal_page.html index a5a3ecf9c6b42fc34a90a34e7e01adf93f7a50c6..4225cd0847feb51a3ca0716127c790d829242094 100644 --- a/scipost/templates/scipost/personal_page.html +++ b/scipost/templates/scipost/personal_page.html @@ -60,6 +60,18 @@ $(".TabSection").hide(); $("#AuthorReplies").show(); }); + $("#TeamsTab").click(function(){ + $(".TabItem").attr("class", "TabItem inactive"); + $("#TeamsTab").attr("class", "TabItem active"); + $(".TabSection").hide(); + $("#Teams").show(); + }); + $("#GraphsTab").click(function(){ + $(".TabItem").attr("class", "TabItem inactive"); + $("#GraphsTab").attr("class", "TabItem active"); + $(".TabSection").hide(); + $("#Graphs").show(); + }); }); </script> @@ -83,13 +95,21 @@ <ul class="personalTabMenu"> <li><a class="TabItem" id="AccountTab">Account</a></li> + {% if request.user|is_in_group:'SciPost Administrators' or request.user|is_in_group:'Editorial College' or request.user|is_in_group:'Vetting Editors' %} <li><a class="TabItem" id="EdActionTab">Editorial Actions</a></li> + {% endif %} <li><a class="TabItem" id="RefereeingTab">Refereeing</a></li> <li><a class="TabItem" id="SubmissionsTab">Submissions</a></li> <li><a class="TabItem" id="CommentariesTab">Commentaries</a></li> <li><a class="TabItem" id="ThesesTab">Theses</a></li> <li><a class="TabItem" id="CommentsTab">Comments</a></li> <li><a class="TabItem" id="AuthorRepliesTab">Author Replies</a></li> + {% if perms.scipost.can_create_team %} + <li><a class="TabItem" id="TeamsTab">Teams</a></li> + {% endif %} + {% if perms.scipost.can_create_graph %} + <li><a class="TabItem" id="GraphsTab">Graphs</a></li> + {% endif %} </ul> </section> @@ -326,6 +346,62 @@ </section> {% endif %} +<section class="TabSection" id="Teams"> + <hr class="hr12"> + <div class="flex-greybox"> + <h1>Teams</h1> + <ul> + <li><a href="{% url 'scipost:create_team' %}">Create a new Team</a></li> + </ul> + </div> + {% if teams_led %} + <h3>Teams you are leading:</h3> + <ul> + {% for team in teams_led %} + {{ team.header_as_li }} + <a href="{% url 'scipost:add_team_member' team_id=team.id %}">Add a member</a> + {% endfor %} + </ul> + {% endif %} + {% if teams %} + <h3>Teams of which you are a member:</h3> + <ul> + {% for team in teams %} + {{ team.header_as_li }} + {% endfor %} + </ul> + {% endif %} + +</section> + +<section class="TabSection" id="Graphs"> + <hr class="hr12"> + <div class="flex-greybox"> + <h1>Graphs</h1> + <ul> + <li><a href="{% url 'scipost:create_graph' %}">Create a new Graph</a></li> + </ul> + </div> + + {% if graphs_owned %} + <h3>Graphs you own:</h3> + <ul> + {% for graph in graphs_owned %} + {{ graph.header_as_li }} + {% endfor %} + </ul> + {% endif %} + {% if graphs_private %} + <h3>Private graphs which you can access:</h3> + <ul> + {% for graph in graphs_private %} + {{ graph.header_as_li }} + {% endfor %} + </ul> + {% endif %} + +</section> + {% endif %} {% endblock bodysup %} diff --git a/scipost/urls.py b/scipost/urls.py index adcc02176a004b8b1729fb3131156adf6f5687f0..299141a0a7688e4f3dc4c91e17e278b5eed68bdd 100644 --- a/scipost/urls.py +++ b/scipost/urls.py @@ -60,4 +60,14 @@ urlpatterns = [ # Feeds url(r'^latest_comment/feed/$', LatestCommentFeed()), + + # Teams + url(r'^create_team$', views.create_team, name='create_team'), + url(r'^add_team_member/(?P<team_id>[0-9]+)$', views.add_team_member, name='add_team_member'), + url(r'^add_team_member/(?P<team_id>[0-9]+)/(?P<contributor_id>[0-9]+)$', views.add_team_member, name='add_team_member'), + + # Graphs + url(r'^create_graph$', views.create_graph, name='create_graph'), +# url(r'^add_graph_node/(?P<graph_id>[0-9]+)$', views.add_graph_node, name='add_graph_node'), + url(r'^graph/(?P<graph_id>[0-9]+)$', views.graph, name='graph'), ] diff --git a/scipost/views.py b/scipost/views.py index 3c95c514ec936e023bba5762d50c061dcf9de229..f346e7ddd312064fbb15103fcebca59124917524 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -360,6 +360,10 @@ def personal_page(request): nr_thesis_authorships_to_claim = ThesisLink.objects.filter(author__contains=contributor.user.last_name).exclude(author_as_cont__in=[contributor]).exclude(author_claims__in=[contributor]).exclude(author_false_claims__in=[contributor]).count() own_comments = Comment.objects.filter(author=contributor,is_author_reply=False).order_by('-date_submitted') own_authorreplies = Comment.objects.filter(author=contributor,is_author_reply=True).order_by('-date_submitted') + teams_led = Team.objects.filter(leader=contributor) + teams = Team.objects.filter(members__in=[contributor]) + graphs_owned = Graph.objects.filter(owner=contributor) + graphs_private = Graph.objects.filter(Q(teams_with_access__leader=contributor) | Q(teams_with_access__members__in=[contributor])) context = {'contributor': contributor, 'nr_reg_to_vet': nr_reg_to_vet, 'nr_reg_awaiting_validation': nr_reg_awaiting_validation, 'nr_commentary_page_requests_to_vet': nr_commentary_page_requests_to_vet, @@ -378,7 +382,12 @@ def personal_page(request): 'own_submissions': own_submissions, 'own_commentaries': own_commentaries, 'own_thesislinks': own_thesislinks, - 'own_comments': own_comments, 'own_authorreplies': own_authorreplies} + 'own_comments': own_comments, 'own_authorreplies': own_authorreplies, + 'teams_led': teams_led, + 'teams': teams, + 'graphs_owned': graphs_owned, + 'graphs_private': graphs_private, + } return render(request, 'scipost/personal_page.html', context) else: form = AuthenticationForm() @@ -572,3 +581,87 @@ def contributor_info(request, contributor_id): context = {'form': form} return render(request, 'scipost/login.html', context) + + +######### +# Teams # +######### + +@permission_required('scipost.can_create_team', raise_exception=True) +def create_team(request): + if request.method == "POST": + create_team_form = CreateTeamForm(request.POST) + if create_team_form.is_valid(): + newteam = Team(leader=request.user.contributor, + name=create_team_form.cleaned_data['name'], + established=timezone.now()) + newteam.save() + return redirect(reverse('scipost:add_team_member', kwargs={'team_id': newteam.id})) + else: + create_team_form = CreateTeamForm() + add_team_member_form = AddTeamMemberForm() + context = {'create_team_form': create_team_form, + 'add_team_member_form': add_team_member_form} + return render(request, 'scipost/create_team.html', context) + +@permission_required('scipost.can_create_team', raise_exception=True) +def add_team_member(request, team_id, contributor_id=None): + team = get_object_or_404(Team, pk=team_id) + contributors_found = None + if contributor_id is not None: + contributor = get_object_or_404(Contributor, pk=contributor_id) + team.members.add(contributor) + team.save() + return redirect(reverse('scipost:add_team_member', kwargs={'team_id': team_id})) + if request.method == "POST": + add_team_member_form = AddTeamMemberForm(request.POST) + if add_team_member_form.is_valid(): + contributors_found = Contributor.objects.filter(user__last_name__icontains=add_team_member_form.cleaned_data['last_name']) + else: + add_team_member_form = AddTeamMemberForm() + context = {'team': team, 'add_team_member_form': add_team_member_form, + 'contributors_found': contributors_found} + return render(request, 'scipost/add_team_member.html', context) + + +########## +# Graphs # +########## + +@permission_required('scipost.can_create_graph', raise_exception=True) +def create_graph(request): + graphcreated = False + message = None + if request.method == "POST": + create_graph_form = CreateGraphForm(request.POST, contributor=request.user.contributor) + if create_graph_form.is_valid(): + newgraph = Graph(owner=request.user.contributor, + title=create_graph_form.cleaned_data['title'], + private=create_graph_form.cleaned_data['private'], + teams_with_access=form.cleaned_data['teams_with_access'], + created=timezone.now()) + newgraph.save() + graphcreated = True + message = 'Graph' + create_graph_form.cleaned_data['title'] + ' was successfully created.' + else: + create_graph_form = CreateGraphForm(contributor=request.user.contributor) + context = {'create_graph_form': create_graph_form, 'graphcreated': graphcreated, + 'message': message} + return render(request, 'scipost/create_graph.html', context) + + +@permission_required('scipost.can_create_graph', raise_exception=True) +def graph(request, graph_id, node_id=None): + graph = get_object_or_404(Graph, pk=graph_id) +# if node_id is not None: +# node = get_object_or_404(Node, pk=node_id) +# base_node = Node.objects.filter(graph=graph, arcs_in=None) + context = {'graph': graph} + return render(request, 'scipost/graph.html', context) + +@permission_required('scipost.can_create_graph', raise_exception=True) +def add_node_upstream(request, graph_id, node_id): + """ Adds a node upstream from the one at node_id """ + node = get_object_or_404(Node, pk=node_id) + + diff --git a/submissions/forms.py b/submissions/forms.py index 39ccc8753078b249e04422729eb907a90146a2b0..14136ed30d904ff5499cebea3ca5f55b791b88d1 100644 --- a/submissions/forms.py +++ b/submissions/forms.py @@ -40,8 +40,6 @@ class AssignSubmissionForm(forms.Form): # user__contributor__specializations__contains=[specialization,] # Reactivate later on, once the Editorial College is large enough ), required=True, label='Select an Editor-in-charge') - #editor_in_charge = forms.ModelChoiceField(queryset=...) - class ConsiderAssignmentForm(forms.Form): accept = forms.ChoiceField(widget=forms.RadioSelect, choices=ASSIGNMENT_BOOL, label="Are you willing to take charge of this Submission?")