diff --git a/coldfront/config/base.py b/coldfront/config/base.py index d2dfcd321..07017a493 100644 --- a/coldfront/config/base.py +++ b/coldfront/config/base.py @@ -28,7 +28,9 @@ LANGUAGE_CODE = ENV.str('LANGUAGE_CODE', default='en-us') TIME_ZONE = ENV.str('TIME_ZONE', default='America/New_York') USE_I18N = True -USE_L10N = True +USE_L10N = False +DATE_FORMAT = 'Y-m-d' +DATETIME_FORMAT = 'Y-m-d H:i' USE_TZ = True #------------------------------------------------------------------------------ @@ -53,13 +55,13 @@ # Hack to fix fontawesome. Will be fixed in version 6 sys.modules['fontawesome_free'] = __import__('fontawesome-free') INSTALLED_APPS += [ + 'crispy_bootstrap5', 'crispy_forms', 'sslserver', 'django_q', 'simple_history', 'fontawesome_free', ] - # ColdFront Apps INSTALLED_APPS += [ 'coldfront.core.user', @@ -134,7 +136,10 @@ else: raise ImproperlyConfigured('SITE_TEMPLATES should be a path to a directory') -CRISPY_TEMPLATE_PACK = 'bootstrap4' +CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5" + +CRISPY_TEMPLATE_PACK = "bootstrap5" + SETTINGS_EXPORT = [] STATIC_URL = '/static/' diff --git a/coldfront/config/urls.py b/coldfront/config/urls.py index 353e3602e..5428175e5 100644 --- a/coldfront/config/urls.py +++ b/coldfront/config/urls.py @@ -25,6 +25,8 @@ path('grant/', include('coldfront.core.grant.urls')), path('publication/', include('coldfront.core.publication.urls')), path('research-output/', include('coldfront.core.research_output.urls')), + path('note/', include('coldfront.core.note.urls')), + ] diff --git a/coldfront/core/allocation/forms.py b/coldfront/core/allocation/forms.py index 970024477..44d4d00eb 100644 --- a/coldfront/core/allocation/forms.py +++ b/coldfront/core/allocation/forms.py @@ -36,7 +36,7 @@ def __init__(self, request_user, project_pk, *args, **kwargs): if user_query_set: self.fields['users'].choices = ((user.user.username, "%s %s (%s)" % ( user.user.first_name, user.user.last_name, user.user.username)) for user in user_query_set) - self.fields['users'].help_text = '
Select users in your project to add to this allocation.' + self.fields['users'].help_text = ' + # Username diff --git a/coldfront/core/allocation/templates/allocation/allocation_allocationattribute_delete.html b/coldfront/core/allocation/templates/allocation/allocation_allocationattribute_delete.html index c3ceb2951..7b84bed52 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_allocationattribute_delete.html +++ b/coldfront/core/allocation/templates/allocation/allocation_allocationattribute_delete.html @@ -22,7 +22,7 @@

Delete allocation attributes from allocation for project: {{allocation.proje - + Name Value diff --git a/coldfront/core/allocation/templates/allocation/allocation_change.html b/coldfront/core/allocation/templates/allocation/allocation_change.html index 4fd7def34..ccd0791fd 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change.html @@ -13,7 +13,7 @@

Request change to {{ allocation.get_parent_resource }} for project: {{ allocation.project.title }}


-

+

Request changes to an existing allocation using the form below. For each change you must provide a justification.

@@ -53,7 +53,7 @@

Allocation Information

{{ allocation.end_date }} {% if allocation.is_locked and allocation.status.name == 'Active' and allocation.expires_in <= 60 and allocation.expires_in >= 0 %} - + Expires in {{allocation.expires_in}} day{{allocation.expires_in|pluralize}} - Not renewable {% endif %} @@ -73,11 +73,11 @@

Allocation Information

Created: - {{ allocation.created|date:"M. d, Y" }} + {{ allocation.created|date:"Y-m-d" }} Last Modified: - {{ allocation.modified|date:"M. d, Y" }} + {{ allocation.modified|date:"Y-m-d" }} {% if allocation.is_locked %} diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html index cf1249913..c04d4c39b 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_detail.html @@ -74,7 +74,7 @@

Allocation I {{ allocation_change.allocation.end_date }} {% if allocation_change.allocation.is_locked and allocation_change.allocation.status.name == 'Approved' and allocation_change.allocation.expires_in <= 60 and allocation_change.allocation.expires_in >= 0 %} - + Expires in {{allocation_change.allocation.expires_in}} day{{allocation_change.allocation.expires_in|pluralize}} - Not renewable {% endif %} @@ -94,11 +94,11 @@

Allocation I Change Requested: - {{ allocation_change.created|date:"M. d, Y" }} + {{ allocation_change.created|date:"Y-m-d" }} Change Last Modified: - {{ allocation_change.modified|date:"M. d, Y" }} + {{ allocation_change.modified|date:"Y-m-d" }} {% if allocation_change.allocation.is_locked %} @@ -141,7 +141,7 @@

Alloc {% if request.user.is_superuser %} {{form.new_value}} - + @@ -170,7 +170,7 @@

Alloc -

{{allocation_change_form.justification | as_crispy_field}}

+

{{allocation_change_form.justification | as_crispy_field}}


@@ -184,12 +184,12 @@

Actio {% csrf_token %} {{note_form.notes | as_crispy_field}} -
+
{% if allocation_change.status.name == 'Pending' %} - - + + {% endif %} -
diff --git a/coldfront/core/allocation/templates/allocation/allocation_change_list.html b/coldfront/core/allocation/templates/allocation/allocation_change_list.html index 7afd32432..60063c0da 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_change_list.html +++ b/coldfront/core/allocation/templates/allocation/allocation_change_list.html @@ -14,7 +14,7 @@

Allocation Change Requests


-

+

For each allocation change request below, there is the option to activate the allocation request and to view the allocation change's detail page. If a change request is only for an extension to the allocation, they can be approved on this page. However if the change request includes changes to the allocation's attributes, the request must be reviewed and acted upon in its detail page. @@ -38,7 +38,7 @@

Allocation Change Requests

{% for change in allocation_change_list %} {{change.pk}} - {{ change.created|date:"M. d, Y" }} + {{ change.created|date:"Y-m-d" }} {{change.allocation.project.title|truncatechars:50}} {{change.allocation.project.pi.first_name}} {{change.allocation.project.pi.last_name}} ({{change.allocation.project.pi.username}}) @@ -51,7 +51,7 @@

Allocation Change Requests

{% if change.allocationattributechangerequest_set.all %} - + {% endif %} Details
diff --git a/coldfront/core/allocation/templates/allocation/allocation_create.html b/coldfront/core/allocation/templates/allocation/allocation_create.html index aba1fef3b..4ca5ce657 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_create.html +++ b/coldfront/core/allocation/templates/allocation/allocation_create.html @@ -13,7 +13,7 @@

Request New Allocation
Project: {{ project.title }}


-

The following {% settings_value 'CENTER_NAME' %} +

The following {% settings_value 'CENTER_NAME' %} resources are available to request for this project. If you need access to more than one of these, please submit a separate allocation request for each resource. For each request you must provide the justification for how you @@ -26,7 +26,7 @@

Request New Allocation
Project: {{ project.title }}

@@ -39,8 +39,8 @@

Request New Allocation
Project: {{ project.title }}

@@ -67,7 +67,7 @@

var resources_with_eula = {{ resources_with_eula | safe }}; $(document).ready(function () { - $('
Select All Users').insertAfter($("#div_id_users > label")) + $('
Select All Users').insertAfter($("#div_id_users > label")) $("#id_resource").trigger('change'); $("#selectAll").click(function () { diff --git a/coldfront/core/allocation/templates/allocation/allocation_delete_invoice_note.html b/coldfront/core/allocation/templates/allocation/allocation_delete_invoice_note.html index c56f6cd01..bdde88ffe 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_delete_invoice_note.html +++ b/coldfront/core/allocation/templates/allocation/allocation_delete_invoice_note.html @@ -21,7 +21,7 @@

Delete invoice notes for allocation to {{allocation.get_resources_as_string} - + Note Author diff --git a/coldfront/core/allocation/templates/allocation/allocation_detail.html b/coldfront/core/allocation/templates/allocation/allocation_detail.html index b459f0488..ae1bed39b 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_detail.html +++ b/coldfront/core/allocation/templates/allocation/allocation_detail.html @@ -34,7 +34,7 @@

Allocation Detail

Allocation Information

@@ -104,12 +104,12 @@

Allocation Information

{{ allocation.end_date }} {% endif %} {% if allocation.is_locked and allocation.status.name == 'Active' and allocation.expires_in <= 60 and allocation.expires_in >= 0 %} - + Expires in {{allocation.expires_in}} day{{allocation.expires_in|pluralize}} - Not renewable {% elif is_allowed_to_update_project and ALLOCATION_ENABLE_ALLOCATION_RENEWAL and allocation.status.name == 'Active' and allocation.expires_in <= 60 and allocation.expires_in >= 0 %} - + Expires in {{allocation.expires_in}} day{{allocation.expires_in|pluralize}} - Click to renew @@ -128,11 +128,11 @@

Allocation Information

Created: - {{ allocation.created|date:"M. d, Y" }} + {{ allocation.created|date:"Y-m-d" }} Last Modified: - {{ allocation.modified|date:"M. d, Y" }} + {{ allocation.modified|date:"Y-m-d" }} {% if request.user.is_superuser or request.user.is_staff %} @@ -161,10 +161,10 @@

Allocation Information

{% if request.user.is_superuser %} -
+
{% if allocation.status.name == 'New' or allocation.status.name == 'Renewal Requested' %} - - + + {% endif %} {% endif %} @@ -177,7 +177,7 @@

Allocation Information

Allocation Attributes

-
+
{% if request.user.is_superuser %} Add Allocation Attribute @@ -222,7 +222,7 @@

Alloc {% if attributes_with_usage %}
{% for attribute in attributes_with_usage %} -
+

{{attribute}}

@@ -237,7 +237,7 @@

{{attribute}}

-

Allocation Change Requests

{{allocation_changes.count}} +

Allocation Change Requests

{{allocation_changes.count}}
@@ -255,7 +255,7 @@

Alloc {% for change_request in allocation_changes %} - {{ change_request.created|date:"M. d, Y" }} + {{ change_request.created|date:"Y-m-d" }} {% if change_request.status.name == 'Approved' %} {{ change_request.status.name }} {% elif change_request.status.name == 'Denied' %} @@ -288,8 +288,8 @@

Alloc

Users in Allocation

- {{allocation_users.count}} -
+ {{allocation_users.count}} +
{% if allocation.project.status.name != 'Archived' and is_allowed_to_update_project and allocation.status.name in 'Active,New,Renewal Requested' %} Add Users @@ -327,7 +327,7 @@

Users in Al {% else %} {{ user.status.name }} {% endif %} - {{ user.modified|date:"M. d, Y" }} + {{ user.modified|date:"Y-m-d" }} {% endfor %} @@ -340,8 +340,8 @@

Users in Al

Notifications

- {{notes.count}} -
@@ -93,8 +93,8 @@

Allocation Information

Notes from Staff

- {{allocation.allocationusernote_set.count}} -
+ {{allocation.allocationusernote_set.count}} +
Add Note diff --git a/coldfront/core/allocation/templates/allocation/allocation_list.html b/coldfront/core/allocation/templates/allocation/allocation_list.html index 8dcece99c..67163c407 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_list.html +++ b/coldfront/core/allocation/templates/allocation/allocation_list.html @@ -17,12 +17,12 @@

Allocations

-
+
diff --git a/coldfront/core/allocation/templates/allocation/allocation_request_list.html b/coldfront/core/allocation/templates/allocation/allocation_request_list.html index 6e257e0bc..52511984b 100644 --- a/coldfront/core/allocation/templates/allocation/allocation_request_list.html +++ b/coldfront/core/allocation/templates/allocation/allocation_request_list.html @@ -14,11 +14,11 @@

Allocation Requests


-

+

For each allocation request below, there is the option to activate the allocation request and to view the allocation's detail page.

-

+

By default, activating an allocation will make it active for {{ ALLOCATION_DEFAULT_ALLOCATION_LENGTH }} days.

@@ -43,7 +43,7 @@

Allocation Requests

{% for allocation in allocation_list %} {{allocation.pk}} - {{ allocation.created|date:"M. d, Y" }} + {{ allocation.created|date:"Y-m-d" }}
{{allocation.project.title|truncatechars:50}} {{allocation.project.pi.first_name}} {{allocation.project.pi.last_name}} ({{allocation.project.pi.username}}) @@ -56,8 +56,8 @@

Allocation Requests

{% csrf_token %} - - Details + + Details diff --git a/coldfront/core/allocation/views.py b/coldfront/core/allocation/views.py index ecca89ddd..d82b2b709 100644 --- a/coldfront/core/allocation/views.py +++ b/coldfront/core/allocation/views.py @@ -1,3 +1,4 @@ +import csv import datetime import logging from datetime import date @@ -13,7 +14,7 @@ from django.db.models import Q from django.db.models.query import QuerySet from django.forms import formset_factory -from django.http import HttpResponseRedirect, JsonResponse, HttpResponseBadRequest +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse, HttpResponseBadRequest, StreamingHttpResponse from django.shortcuts import get_object_or_404, render from django.urls import reverse, reverse_lazy from django.utils.html import format_html, mark_safe @@ -31,7 +32,7 @@ AllocationAttributeUpdateForm, AllocationForm, AllocationInvoiceNoteDeleteForm, - AllocationInvoiceUpdateForm, + AllocationInvoiceUpdateForm, AllocationNoteCreateForm, AllocationRemoveUserForm, AllocationReviewUserForm, AllocationSearchForm, @@ -59,7 +60,7 @@ from coldfront.core.project.models import (Project, ProjectUser, ProjectPermission, ProjectUserStatusChoice) from coldfront.core.resource.models import Resource -from coldfront.core.utils.common import get_domain_url, import_from_settings +from coldfront.core.utils.common import Echo, get_domain_url, import_from_settings from coldfront.core.utils.mail import send_allocation_admin_email, send_allocation_customer_email ALLOCATION_ENABLE_ALLOCATION_RENEWAL = import_from_settings( @@ -779,7 +780,6 @@ class AllocationAttributeCreateView(LoginRequiredMixin, UserPassesTestMixin, Cre model = AllocationAttribute form_class = AllocationAttributeCreateForm template_name = 'allocation/allocation_allocationattribute_create.html' - def test_func(self): """ UserPassesTestMixin Tests""" @@ -889,47 +889,104 @@ def post(self, request, *args, **kwargs): return HttpResponseRedirect(reverse('allocation-detail', kwargs={'pk': pk})) -class AllocationNoteCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): - model = AllocationUserNote - fields = '__all__' +class AllocationNoteCreateView(LoginRequiredMixin, UserPassesTestMixin, FormView): + form_class = AllocationNoteCreateForm template_name = 'allocation/allocation_note_create.html' - def test_func(self): """ UserPassesTestMixin Tests""" + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) if self.request.user.is_superuser: return True - messages.error( self.request, 'You do not have permission to add allocation notes.') - return False + + else: + messages.error( + self.request, 'You do not have permission to add allocation notes.') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) pk = self.kwargs.get('pk') - allocation_obj = get_object_or_404(Allocation, pk=pk) - context['allocation'] = allocation_obj + project_obj = get_object_or_404(Allocation, pk=pk) + context['allocation'] = project_obj return context - def get_initial(self): - initial = super().get_initial() - pk = self.kwargs.get('pk') - allocation_obj = get_object_or_404(Allocation, pk=pk) - author = self.request.user - initial['allocation'] = allocation_obj - initial['author'] = author - return initial + - def get_form(self, form_class=None): + def get_form(self, form_class=form_class): """Return an instance of the form to be used in this view.""" - form = super().get_form(form_class) - form.fields['allocation'].widget = forms.HiddenInput() - form.fields['author'].widget = forms.HiddenInput() - form.order_fields([ 'allocation', 'author', 'note', 'is_private' ]) - return form + pk = self.kwargs.get('pk') + return form_class(self.kwargs.get('pk'), **self.get_form_kwargs()) + + def form_valid(self, form) -> HttpResponse: + obj = form + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) + form_complete = AllocationNoteCreateForm(allocation_obj.pk,self.request.POST,initial = {"author":self.request.user,"message":obj.data['message']}) + if form_complete.is_valid(): + form_data = form_complete.cleaned_data + new_note_obj = AllocationUserNote.objects.create( + allocation = allocation_obj, + message = form_data["message"], + tags = form_data["tags"], + author = self.request.user, + ) + + # obj.project = allocation_obj + + self.object = obj + return super().form_valid(form) + + def get_success_url(self): return reverse('allocation-detail', kwargs={'pk': self.kwargs.get('pk')}) + +class AllocationNoteDownloadView(LoginRequiredMixin, UserPassesTestMixin, ListView): + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) + + if allocation_obj.project.pi == self.request.user: + return True + + if allocation_obj.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): + return True + + messages.error(self.request, 'You do not have permission to download all notes.') + + def get(self, request, pk): + header = [ + "Comment", + "Administrator", + "Created By", + "Last Modified" + ] + rows = [] + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) + + notes = allocation_obj.allocationusernote_set.all() + + for note in notes: + row = [ + note.message, + note.author, + note.tags, + note.modified + ] + rows.append(row) + rows.insert(0, header) + pseudo_buffer = Echo() + writer = csv.writer(pseudo_buffer) + response = StreamingHttpResponse((writer.writerow(row) for row in rows), + content_type="text/csv") + response['Content-Disposition'] = 'attachment; filename="notes.csv"' + return response + + class AllocationRequestListView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): template_name = 'allocation/allocation_request_list.html' login_url = '/' diff --git a/coldfront/core/grant/templates/grant/grant_delete_grants.html b/coldfront/core/grant/templates/grant/grant_delete_grants.html index fc559a6c8..3303e2645 100644 --- a/coldfront/core/grant/templates/grant/grant_delete_grants.html +++ b/coldfront/core/grant/templates/grant/grant_delete_grants.html @@ -22,7 +22,7 @@

Delete grants from project: {{project.title}}

- + Title Grant Number diff --git a/coldfront/core/grant/templates/grant/grant_report_list.html b/coldfront/core/grant/templates/grant/grant_report_list.html index 2a6f9902c..80674b457 100644 --- a/coldfront/core/grant/templates/grant/grant_report_list.html +++ b/coldfront/core/grant/templates/grant/grant_report_list.html @@ -14,7 +14,7 @@

Grants

- +
{% if formset %} @@ -24,7 +24,7 @@

Grants

- + Grant Title Project PI Faculty Role diff --git a/coldfront/core/note/__init__.py b/coldfront/core/note/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/note/admin.py b/coldfront/core/note/admin.py new file mode 100644 index 000000000..774162408 --- /dev/null +++ b/coldfront/core/note/admin.py @@ -0,0 +1,9 @@ +from django.contrib import admindocs + +from coldfront.core.note.models import Note +from django.contrib import admin + +# Register your models here. +@admin.register(Note) +class NoteAdmin(admin.ModelAdmin): + list_display = ('title','message','pk') diff --git a/coldfront/core/note/apps.py b/coldfront/core/note/apps.py new file mode 100644 index 000000000..44f51d5d8 --- /dev/null +++ b/coldfront/core/note/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class NoteConfig(AppConfig): + # default_auto_field = 'django.db.models.BigAutoField' + name = 'coldfront.core.note' diff --git a/coldfront/core/note/forms.py b/coldfront/core/note/forms.py new file mode 100644 index 000000000..9132f303c --- /dev/null +++ b/coldfront/core/note/forms.py @@ -0,0 +1,40 @@ +from django import forms +from django.db.models.functions import Lower +from django.shortcuts import get_object_or_404 + +from coldfront.core.allocation.models import (Allocation, AllocationAccount, + AllocationAttributeType, + AllocationAttribute, + AllocationStatusChoice, + AllocationNoteTags, + AllocationUserNote) +from coldfront.core.allocation.utils import get_user_resources +from coldfront.core.note.models import NoteTags +from coldfront.core.project.models import Project +from coldfront.core.resource.models import Resource, ResourceType +from coldfront.core.utils.common import import_from_settings + + +class NoteCreateForm(forms.Form): + + title = forms.CharField(max_length=150) + tags = forms.ModelChoiceField( + queryset=NoteTags.objects.all(), empty_label=None) + message = forms.CharField(widget=forms.Textarea) + note_to = forms.MultipleChoiceField( + widget=forms.CheckboxSelectMultiple(attrs={'checked': 'checked'}), required=False) + + + def __init__(self, pk, *args, **kwargs): + super().__init__(*args, **kwargs) + allocation_obj = get_object_or_404(Allocation, pk=pk) + user_query_set = allocation_obj.project.projectuser_set.select_related('user').filter( + status__name__in=['Active', ]).order_by("user__username") + user_query_set = user_query_set.exclude(user=allocation_obj.project.pi) + if user_query_set: + self.fields['note_to'].choices = ((user.user.username, "%s %s (%s)" % ( + user.user.first_name, user.user.last_name, user.user.username)) for user in user_query_set) + self.fields['note_to'].help_text = '
Select users in your project to send this note to. Only they can view this note' + else: + + self.fields['note_to'].widget = forms.HiddenInput() diff --git a/coldfront/core/note/migrations/0001_initial.py b/coldfront/core/note/migrations/0001_initial.py new file mode 100644 index 000000000..c08e74105 --- /dev/null +++ b/coldfront/core/note/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 3.2.20 on 2023-10-28 21:54 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('allocation', '0006_auto_20231028_1751'), + ] + + operations = [ + migrations.CreateModel( + name='NoteTags', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('name', models.CharField(max_length=64)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Note', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('test', models.TextField(default='True')), + ('is_private', models.BooleanField(default=False)), + ('message', models.TextField()), + ('allocation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='allocation.allocation')), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('tags', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='note.notetags')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/coldfront/core/note/migrations/0002_note_title.py b/coldfront/core/note/migrations/0002_note_title.py new file mode 100644 index 000000000..a279d3b80 --- /dev/null +++ b/coldfront/core/note/migrations/0002_note_title.py @@ -0,0 +1,19 @@ +# Generated by Django 3.2.20 on 2023-10-28 21:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('note', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='note', + name='title', + field=models.TextField(default='True'), + preserve_default=False, + ), + ] diff --git a/coldfront/core/note/migrations/0003_auto_20231029_2344.py b/coldfront/core/note/migrations/0003_auto_20231029_2344.py new file mode 100644 index 000000000..30243ff0e --- /dev/null +++ b/coldfront/core/note/migrations/0003_auto_20231029_2344.py @@ -0,0 +1,44 @@ +# Generated by Django 3.2.20 on 2023-10-30 03:44 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('allocation', '0006_auto_20231028_1751'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('project', '0005_auto_20231028_1751'), + ('note', '0002_note_title'), + ] + + operations = [ + migrations.AddField( + model_name='note', + name='project', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='project.project'), + ), + migrations.AlterField( + model_name='note', + name='allocation', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='allocation.allocation'), + ), + migrations.CreateModel( + name='Comment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('is_private', models.BooleanField(default=True)), + ('note', models.TextField()), + ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/coldfront/core/note/migrations/__init__.py b/coldfront/core/note/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/coldfront/core/note/models.py b/coldfront/core/note/models.py new file mode 100644 index 000000000..f84b55179 --- /dev/null +++ b/coldfront/core/note/models.py @@ -0,0 +1,88 @@ +from django.db import models + +from coldfront.core.allocation.models import Allocation, AllocationNoteTags +from coldfront.core.user.models import User +import datetime +import importlib +import logging +from ast import literal_eval +from enum import Enum + +from django.conf import settings +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.html import mark_safe +from django.utils.module_loading import import_string +from model_utils.models import TimeStampedModel +from simple_history.models import HistoricalRecords + +from coldfront.core.project.models import Project, ProjectPermission +from coldfront.core.resource.models import Resource +from coldfront.core.utils.common import import_from_settings +import coldfront.core.attribute_expansion as attribute_expansion + +# Create your models here. + + +class NoteTags(TimeStampedModel): + name = models.CharField(max_length=64) + + class NoteTagManager(models.Manager): + def get_by_natural_key(self, name): + return self.get(name=name) + objects = NoteTagManager() + + def __str__(self): + return self.name + + def natural_key(self): + return (self.name,) + + class Meta: + ordering = ['name', ] + + +class Note(TimeStampedModel): + """ A project user message is a message sent to a user in a project. + + Attributes: + project (Project): links the project the message is from to the message + author (User): represents the user who authored the message + is_private (bool): indicates whether or not the message is private + message (str): text input from the user containing the message + """ + + + allocation = models.ForeignKey(Allocation, on_delete=models.CASCADE,null=True) + project = models.ForeignKey(Project,on_delete=models.CASCADE, null=True) + title = models.TextField() + author = models.ForeignKey(User, on_delete=models.CASCADE) + is_private = models.BooleanField(default=True) + tags = models.ForeignKey(NoteTags, on_delete=models.CASCADE,null=True,default=None) + test = models.TextField(default="True") + is_private = models.BooleanField(default=False) + message = models.TextField() + # note_to = models.ForeignKey(User, on_delete=models.CASCADE, related_name="NoteTo",null=True) + message = models.TextField() + + def __str__(self): + return self.message +class Comment(TimeStampedModel): + """ An allocation user note is a note that an user makes on an allocation. + + Attributes: + allocation (Allocation): links the allocation to the note + author (User): represents the User class of the user who authored the note + is_private (bool): indicates whether or not the note is private + note (str): text input from the user containing the note + """ + + note = models.ForeignKey(Note, on_delete=models.CASCADE) + author = models.ForeignKey(User, on_delete=models.CASCADE) + is_private = models.BooleanField(default=True) + note = models.TextField() + + def __str__(self): + return self.note + \ No newline at end of file diff --git a/coldfront/core/note/templates/note/allocation_note_create.html b/coldfront/core/note/templates/note/allocation_note_create.html new file mode 100644 index 000000000..a376dc8df --- /dev/null +++ b/coldfront/core/note/templates/note/allocation_note_create.html @@ -0,0 +1,22 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load common_tags %} +{% load static %} + + +{% block title %} +Add Allocation Notification +{% endblock %} + + +{% block content %} +

Adding Note

+ +
+ {% csrf_token %} + {{form |crispy}} + + Back to + Allocation
+
+{% endblock %} diff --git a/coldfront/core/note/templates/note/allocation_note_detail.html b/coldfront/core/note/templates/note/allocation_note_detail.html new file mode 100644 index 000000000..887d5816e --- /dev/null +++ b/coldfront/core/note/templates/note/allocation_note_detail.html @@ -0,0 +1,78 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load static %} + + +{% block title %} +Allocation Detail +{% endblock %} + + +{% block content %} + + +
+
+

+ {{ note.tags }}: {{ note.title }} +

+

Last Modified: {{ note.modified|date:"Y-m-d" }}

+

Author: {{ note.author }}

+

{{ note.message }}

+
+
+ + + + +
+
+

Comments:

+ {{notes.count}} +
+ + Add Notification + + {% if request.user.is_superuser %} + Export to CSV + {% if request.user.is_superuser or request.user.role == "Manager" %} + Delete Notes + {% endif %} + {% endif %} + +
+
+
+ {% if comments %} +
+ + + + + + + + + + {% for note in comments %} + {% if not note.is_private or request.user.is_superuser %} + + + + + + {% endif %} + {% endfor %} + +
NoteAdministratorLast Modified
{{ note.note }}{{ note.author.first_name }} {{ note.author.last_name }}{{ note.modified }}
+
+ {% else %} + + {% endif %} +
+
+ +{% endblock %} + diff --git a/coldfront/core/note/templates/note/comment_create.html b/coldfront/core/note/templates/note/comment_create.html new file mode 100644 index 000000000..ed766e16a --- /dev/null +++ b/coldfront/core/note/templates/note/comment_create.html @@ -0,0 +1,23 @@ +{% extends "common/base.html" %} +{% load crispy_forms_tags %} +{% load common_tags %} +{% load static %} + + +{% block title %} +Add Allocation Notification +{% endblock %} + + +{% block content %} +

Adding notification to {{allocation.get_parent_resource}} for PI {{ allocation.project.pi.first_name }} + {{ allocation.project.pi.last_name }} ({{ allocation.project.pi.username }})

+ +
+ {% csrf_token %} + {{form |crispy}} + + Back to + Allocation
+
+{% endblock %} diff --git a/coldfront/core/note/templates/test b/coldfront/core/note/templates/test new file mode 100644 index 000000000..af75ef36a --- /dev/null +++ b/coldfront/core/note/templates/test @@ -0,0 +1,8 @@ +
+

{{ note.tags }}: {{ note.title }}

+
+
{{ note.message }}
+ + Add Notification + +
\ No newline at end of file diff --git a/coldfront/core/note/tests.py b/coldfront/core/note/tests.py new file mode 100644 index 000000000..7ce503c2d --- /dev/null +++ b/coldfront/core/note/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/coldfront/core/note/urls.py b/coldfront/core/note/urls.py new file mode 100644 index 000000000..547fb57e4 --- /dev/null +++ b/coldfront/core/note/urls.py @@ -0,0 +1,12 @@ +from django.urls import path +import coldfront.core.note.views as note_views +import coldfront.core.allocation.views as allocation_views + +urlpatterns = [ + path('add-allocation-note//', note_views.NoteCreateView.as_view(), name='add-allocation-notes_app'), + path('note-detail//', note_views.AllocationNoteDetailView.as_view(), name="allocation-notes-detail"), + path('add-comment//', note_views.CommentCreateView.as_view(), name='add-comment'), + + # path('/', allocation_views.AllocationDetailView.as_view(), + # name='allocation-detail'), +] \ No newline at end of file diff --git a/coldfront/core/note/views.py b/coldfront/core/note/views.py new file mode 100644 index 000000000..9165e26e7 --- /dev/null +++ b/coldfront/core/note/views.py @@ -0,0 +1,287 @@ +from django.shortcuts import render +import csv +import datetime +import logging +from datetime import date +import json + +from dateutil.relativedelta import relativedelta +from django import forms +from django.contrib import messages +from django.contrib.auth import get_user_model +from django.contrib.auth.models import User +from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin +from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator +from django.db.models import Q +from django.db.models.query import QuerySet +from django.forms import formset_factory +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse, HttpResponseBadRequest, StreamingHttpResponse +from django.shortcuts import get_object_or_404, render +from django.urls import reverse, reverse_lazy +from django.utils.html import format_html, mark_safe +from django.views import View +from django.views.generic import ListView, TemplateView +from django.views.generic.edit import CreateView, FormView, UpdateView + +from coldfront.core.note.models import (Note, + Comment) +from coldfront.core.note.forms import AllocationNoteCreateForm, NoteCreateForm +from coldfront.core.allocation.forms import (AllocationAccountForm, + AllocationAddUserForm, + AllocationAttributeCreateForm, + AllocationAttributeDeleteForm, + AllocationChangeForm, + AllocationChangeNoteForm, + AllocationAttributeChangeForm, + AllocationAttributeUpdateForm, + AllocationForm, + AllocationInvoiceNoteDeleteForm, + AllocationInvoiceUpdateForm, + AllocationRemoveUserForm, + AllocationReviewUserForm, + AllocationSearchForm, + AllocationUpdateForm + # AllocationNoteCreateForm + ) +from coldfront.core.allocation.models import (Allocation, + AllocationPermission, + AllocationAccount, + AllocationAttribute, + AllocationAttributeType, + AllocationChangeRequest, + AllocationChangeStatusChoice, + AllocationAttributeChangeRequest, + AllocationStatusChoice, + AllocationUser, + AllocationUserNote, + AllocationUserStatusChoice) +from coldfront.core.allocation.signals import (allocation_new, + allocation_activate, + allocation_activate_user, + allocation_disable, + allocation_remove_user, + allocation_change_approved,) +from coldfront.core.allocation.utils import (generate_guauge_data_from_usage, + get_user_resources) +from coldfront.core.project.models import (Project, ProjectUser, ProjectPermission, + ProjectUserStatusChoice) +from coldfront.core.resource.models import Resource +from coldfront.core.utils.common import Echo, get_domain_url, import_from_settings +from coldfront.core.utils.mail import send_allocation_admin_email, send_allocation_customer_email + +# Create your views here. +class NoteCreateView(LoginRequiredMixin, UserPassesTestMixin, FormView): + form_class = NoteCreateForm + # fields = '__all__' + template_name = 'note/allocation_note_create.html' + model_type = '' + object_relation = {"Allocation":Allocation,"Project":Project} + + + def object_type(self): + if(self.kwargs.get('allocation_pk','Project') == "Project"): + obj = get_object_or_404(Project, pk=self.kwargs.get('project_pk')) + self.model_type = 'Allocation' + else: + obj = get_object_or_404(Project, pk=self.kwargs.get('project_pk')) + self.model_type = 'Project' + return self.model_type, obj + + + + def test_func(self): + """ UserPassesTestMixin Tests""" + + + if(self.kwargs.get('allocation_pk','Project') != "Project"): + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('allocation_pk')) + self.model_type = 'Allocation' + else: + allocation_obj = get_object_or_404(Project, pk=self.kwargs.get('project_pk')) + self.model_type = 'Project' + model_obj = get_object_or_404(Allocation, pk=self.kwargs.get('allocation_pk')) + if self.request.user.is_superuser: + return True + + else: + messages.error( + self.request, 'You do not have permission to add allocation notes.') + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + try: + model_obj = get_object_or_404(Allocation, pk=self.kwargs.get('allocation_pk')) + context['allocation'] = model_obj + context['type'] = "Allocation" + except: + model_obj = get_object_or_404(Project, pk=self.kwargs.get('project_pk')) + context['type'] = "Project" + return context + + + def get_form(self, form_class=form_class): + """Return an instance of the form to be used in this view.""" + if(self.model_type == "Allocation"): + return form_class(self.kwargs.get('allocation_pk'), **self.get_form_kwargs()) + if(self.model_type == "Project"): + return form_class(self.kwargs.get('allocation_pk'), **self.get_form_kwargs()) + + + def form_valid(self, form) -> HttpResponse: + obj = form + object_relation = {"Allocation":Allocation,"Project":Project} + object_name_as_lowercase_string = self.object_type.lower() + model_obj = get_object_or_404(object_relation[self.object_type], pk=self.kwargs.get(object_name_as_lowercase_string+'_pk')) + form_complete = NoteCreateForm(model_obj.pk,self.request.POST,initial = {"author":self.request.user,"message":obj.data['message']}) + if form_complete.is_valid(): + form_data = form_complete.cleaned_data + if(object_name_as_lowercase_string == "allocation"): + new_note_obj = Note.objects.create( + title = form_data["title"], + allocation = model_obj, + message = form_data["message"], + tags = form_data["tags"], + author = self.request.user, + ) + else: + new_note_obj = Note.objects.create( + title = form_data["title"], + project = model_obj, + message = form_data["message"], + tags = form_data["tags"], + author = self.request.user, + ) + + + + self.pk_hold = new_note_obj.pk + self.object = obj + + return super().form_valid(form) + + + def get_success_url(self): + # if() + return reverse('notes-detail', kwargs={'pk': self.pk_hold}) + + + +class AllocationNoteDownloadView(LoginRequiredMixin, UserPassesTestMixin, ListView): + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + + model_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) + + if model_obj.project.pi == self.request.user: + return True + + if model_obj.projectuser_set.filter(user=self.request.user, role__name='Manager', status__name='Active').exists(): + return True + + messages.error(self.request, 'You do not have permission to download all notes.') + + def get(self, request, pk): + header = [ + "Comment", + "Administrator", + "Created By", + "Last Modified" + ] + rows = [] + allocation_obj = get_object_or_404(Allocation, pk=self.kwargs.get('pk')) + + notes = allocation_obj.allocationusernote_set.all() + + for note in notes: + row = [ + note.message, + note.author, + note.tags, + note.modified + ] + rows.append(row) + rows.insert(0, header) + pseudo_buffer = Echo() + writer = csv.writer(pseudo_buffer) + response = StreamingHttpResponse((writer.writerow(row) for row in rows), + content_type="text/csv") + response['Content-Disposition'] = 'attachment; filename="notes.csv"' + return response + +class CommentCreateView(LoginRequiredMixin, UserPassesTestMixin, CreateView): + model = Comment + fields = '__all__' + template_name = 'note/comment_create.html' + + def test_func(self): + """ UserPassesTestMixin Tests""" + if self.request.user.is_superuser: + return True + messages.error( self.request, 'You do not have permission to add allocation notes.') + return False + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get('pk') + note = get_object_or_404(Note, pk=pk) + context['note'] = note + return context + + def get_initial(self): + initial = super().get_initial() + pk = self.kwargs.get('pk') + note_ = get_object_or_404(Note, pk=pk) + author = self.request.user + initial['note_'] = note_ + initial['author'] = author + return initial + + def get_form(self, form_class=None): + """Return an instance of the form to be used in this view.""" + form = super().get_form(form_class) + form.fields['note_'].widget = forms.HiddenInput() + form.fields['author'].widget = forms.HiddenInput() + form.order_fields([ 'note_', 'author', 'note', 'is_private' ]) + return form + + def get_success_url(self): + return reverse('note-detail', kwargs={'pk': self.kwargs.get('pk')}) + +class AllocationNoteDetailView(LoginRequiredMixin, UserPassesTestMixin, TemplateView): + model = Note + template_name = 'note/allocation_note_detail.html' + context_object_name = 'note' + + def test_func(self): + """ UserPassesTestMixin Tests""" + pk = self.kwargs.get('pk') + allocation_obj = get_object_or_404(Note, pk=pk) + + # if self.request.user.has_perm('allocation.can_view_all_allocations'): + # return True + return True + + # return allocation_obj.has_perm(self.request.user, AllocationPermission.USER) + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + pk = self.kwargs.get('pk') + # allocation_obj = get_object_or_404(Allocation, pk=pk) + note_obj = get_object_or_404(Note, pk=pk) + context["note"] = note_obj + + comments = [] + + for comment in note_obj.allocation.allocationusernote_set.all(): + comments.append(comment) + + context["comments"] = comments + + return context + # # set visible usage attributes + # alloc_attr_set = allocation_obj.get_attribute_set(self.request.user) + # attributes_with_usage = [a for a in alloc_attr_set if hasattr(a, 'allocationattributeusage')] + # attributes = alloc_attr_set + + \ No newline at end of file diff --git a/coldfront/core/portal/templates/portal/authorized_home.html b/coldfront/core/portal/templates/portal/authorized_home.html index 5fef17e0e..294338ffc 100644 --- a/coldfront/core/portal/templates/portal/authorized_home.html +++ b/coldfront/core/portal/templates/portal/authorized_home.html @@ -13,9 +13,9 @@

Projects »