diff --git a/opentech/apply/dashboard/templates/dashboard/dashboard.html b/opentech/apply/dashboard/templates/dashboard/dashboard.html index 6c6f84d4e84b696b45218bd7aaa12a2ef50bfe6c..dc4ca09908509d0adf3d883ba171d476271299f1 100644 --- a/opentech/apply/dashboard/templates/dashboard/dashboard.html +++ b/opentech/apply/dashboard/templates/dashboard/dashboard.html @@ -54,6 +54,10 @@ {% include "dashboard/includes/waiting-for-review.html" with in_review_count=awating_reviews.count my_review=awaiting_reviews.table display_more=awaiting_reviews.display_more active_statuses_filter=awaiting_reviews.active_statuses_filter %} </div> + <div id="submissions-flagged" class="wrapper wrapper--bottom-space"> + {% include "dashboard/includes/flagged.html" with my_flagged=my_flagged.table display_more=my_flagged.display_more %} + </div> + {% if rounds.closed or rounds.open %} {% include "funds/includes/round-block.html" with closed_rounds=rounds.closed open_rounds=rounds.open title="Your rounds and labs" %} {% endif %} @@ -109,4 +113,5 @@ <script src="{% static 'js/apply/submission-filters.js' %}"></script> <script src="{% static 'js/apply/submission-tooltips.js' %}"></script> <script src="{% static 'js/apply/tabs.js' %}"></script> + <script src="{% static 'js/apply/flag.js' %}"></script> {% endblock %} diff --git a/opentech/apply/dashboard/templates/dashboard/includes/flagged.html b/opentech/apply/dashboard/templates/dashboard/includes/flagged.html new file mode 100644 index 0000000000000000000000000000000000000000..b2c25d15b18f576b9b6a16cd9a90095f5bfa3fe5 --- /dev/null +++ b/opentech/apply/dashboard/templates/dashboard/includes/flagged.html @@ -0,0 +1,14 @@ +{% load render_table from django_tables2 %} + +<h4 class="heading heading--normal"> + My Flagged Submissions +</h4> + +{% if my_flagged.data %} + {% render_table my_flagged %} + {% if display_more %} + <div class="all-submissions-table__more"> + <a href="{% url 'apply:submissions:flagged' %}">Show all</a> + </div> + {% endif %} +{% endif %} diff --git a/opentech/apply/dashboard/views.py b/opentech/apply/dashboard/views.py index 148bddc9bff5fde9602e26662ecb1e5ee1a1ba56..9b963a58f1b778eafe431a0da02875d55bd3a9d1 100644 --- a/opentech/apply/dashboard/views.py +++ b/opentech/apply/dashboard/views.py @@ -38,7 +38,8 @@ class AdminDashboardView(TemplateView): 'my_reviewed': self.get_my_reviewed(self.request, submissions), 'projects': self.get_my_projects(self.request), 'projects_to_approve': self.get_my_projects_to_approve(self.request.user), - 'rounds': self.get_rounds(self.request.user) + 'rounds': self.get_rounds(self.request.user), + 'my_flagged': self.get_my_flagged(self.request, submissions), } current_context = super().get_context_data(**kwargs) return {**current_context, **extra_context} @@ -110,13 +111,24 @@ class AdminDashboardView(TemplateView): } def get_rounds(self, user): + limit = 6 qs = (RoundsAndLabs.objects.with_progress() .active() .order_by('-end_date') .by_lead(user)) return { - 'closed': qs.closed()[:6], - 'open': qs.open()[:6], + 'closed': qs.closed()[:limit], + 'open': qs.open()[:limit], + } + + def get_my_flagged(self, request, qs): + qs = qs.flagged_by(request.user).order_by('-submit_time') + row_attrs = dict({'data-flag-type': 'user'}, **SummarySubmissionsTable._meta.row_attrs) + + limit = 5 + return { + 'table': SummarySubmissionsTable(qs[:limit], prefix='my-flagged-', attrs={'class': 'all-submissions-table flagged-table'}, row_attrs=row_attrs), + 'display_more': qs.count() > limit, } @@ -159,9 +171,10 @@ class ReviewerDashboardView(TemplateView): return render(request, 'dashboard/reviewer_dashboard.html', context) def get_my_reviews(self, user, qs): + limit = 5 my_review_qs = qs.in_review_for(user).order_by('-submit_time') - my_review_table = ReviewerSubmissionsTable(my_review_qs[:5], prefix='my-review-') - display_more = (my_review_qs.count() > 5) + my_review_table = ReviewerSubmissionsTable(my_review_qs[:limit], prefix='my-review-') + display_more = (my_review_qs.count() > limit) return my_review_qs, my_review_table, display_more @@ -177,8 +190,9 @@ class ReviewerDashboardView(TemplateView): filterset = SubmissionReviewerFilterAndSearch(**kwargs) my_reviewed_qs = filterset.qs - my_reviewed_table = ReviewerSubmissionsTable(my_reviewed_qs[:5], prefix='my-reviewed-') - display_more_reviewed = (my_reviewed_qs.count() > 5) + limit = 5 + my_reviewed_table = ReviewerSubmissionsTable(my_reviewed_qs[:limit], prefix='my-reviewed-') + display_more_reviewed = (my_reviewed_qs.count() > limit) return filterset, my_reviewed_qs, my_reviewed_table, display_more_reviewed diff --git a/opentech/apply/flags/__init__.py b/opentech/apply/flags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/flags/migrations/0001_initial.py b/opentech/apply/flags/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..dccfeb0966fc578042b8c854fb7d0469c41f53d3 --- /dev/null +++ b/opentech/apply/flags/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 2.1.11 on 2019-10-29 13:20 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Flag', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('target_object_id', models.PositiveIntegerField()), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('type', models.CharField(choices=[('staff', 'Staff'), ('user', 'User')], default='user', max_length=15)), + ('target_content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/opentech/apply/flags/migrations/__init__.py b/opentech/apply/flags/migrations/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/flags/models.py b/opentech/apply/flags/models.py new file mode 100644 index 0000000000000000000000000000000000000000..726fd0be4a41e1546c38c343d3d915ed859e708e --- /dev/null +++ b/opentech/apply/flags/models.py @@ -0,0 +1,26 @@ +from django.conf import settings +from django.db import models +from django.contrib.contenttypes.fields import GenericForeignKey +from django.contrib.contenttypes.models import ContentType + + +class Flag(models.Model): + STAFF = 'staff' + USER = 'user' + FLAG_TYPES = { + STAFF: 'Staff', + USER: 'User', + } + target_content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) + target_object_id = models.PositiveIntegerField() + target = GenericForeignKey('target_content_type', 'target_object_id') + timestamp = models.DateTimeField(auto_now_add=True) + type = models.CharField( + choices=FLAG_TYPES.items(), + default='user', + max_length=15, + ) + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.PROTECT, + ) diff --git a/opentech/apply/flags/templates/flags/flags.html b/opentech/apply/flags/templates/flags/flags.html new file mode 100644 index 0000000000000000000000000000000000000000..4f68e8e16eae817dec9af75f1d8c1a55447eaa82 --- /dev/null +++ b/opentech/apply/flags/templates/flags/flags.html @@ -0,0 +1,12 @@ +{% load flag_tags %} +<div class="sidebar__inner"> + <h5>Add to your flagged list</h5> + <button class="button button--primary button--full-width button--flag{% if submission|flagged_by:user %} flagged{% endif %}" data-id="{{ submission.id }}" data-type="user">Flag</button> +</div> + +{% if request.user.is_apply_staff %} +<div class="sidebar__inner"> + <h5>Add to staff flagged list</h5> + <button class="button button--primary button--full-width button--flag{% if submission|flagged_staff %} flagged{% endif %}" data-id="{{ submission.id }}" data-type="staff">Staff flag</button> +</div> +{% endif %} diff --git a/opentech/apply/flags/templatetags/__init__.py b/opentech/apply/flags/templatetags/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/opentech/apply/flags/templatetags/flag_tags.py b/opentech/apply/flags/templatetags/flag_tags.py new file mode 100644 index 0000000000000000000000000000000000000000..4a51294a42037eb4d648b4eded5ccfe02fbe921b --- /dev/null +++ b/opentech/apply/flags/templatetags/flag_tags.py @@ -0,0 +1,13 @@ +from django import template + +register = template.Library() + + +@register.filter +def flagged_by(submission, user): + return submission.flagged_by(user) + + +@register.filter +def flagged_staff(submission): + return submission.flagged_staff diff --git a/opentech/apply/flags/urls.py b/opentech/apply/flags/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..ea85dab48e78434371ba1f9759fdefd3cee083f1 --- /dev/null +++ b/opentech/apply/flags/urls.py @@ -0,0 +1,9 @@ +from django.urls import path + +from .views import FlagSubmissionCreateView + +app_name = 'flags' + +urlpatterns = [ + path('<int:submission_pk>/<type>/flag/', FlagSubmissionCreateView.as_view(), name="create_submission_flag"), +] diff --git a/opentech/apply/flags/views.py b/opentech/apply/flags/views.py new file mode 100644 index 0000000000000000000000000000000000000000..776c68992d1a10a40c6d34c5531652651f0441e0 --- /dev/null +++ b/opentech/apply/flags/views.py @@ -0,0 +1,28 @@ +from django.contrib.contenttypes.models import ContentType +from django.utils.decorators import method_decorator +from django.http import JsonResponse, HttpResponseNotAllowed +from django.views import View + +from opentech.apply.funds.models import ApplicationSubmission +from opentech.apply.users.decorators import staff_required + +from .models import Flag + + +@method_decorator(staff_required, name='dispatch') +class FlagSubmissionCreateView(View): + model = Flag + + def post(self, request, type, submission_pk): + if not request.is_ajax(): + return HttpResponseNotAllowed() + + submission_type = ContentType.objects.get_for_model(ApplicationSubmission) + # Trying to get a flag from the table, or create a new one + flag, created = self.model.objects.get_or_create(user=request.user, target_object_id=submission_pk, target_content_type=submission_type, type=type) + # If no new flag has been created, + # Then we believe that the request was to delete the flag. + if not created: + flag.delete() + + return JsonResponse({"result": created}) diff --git a/opentech/apply/funds/models/submissions.py b/opentech/apply/funds/models/submissions.py index 11f98e2b1060877ab5916f40be05a02f0be7ec47..ee66786c37d42d70259d6bf79718b6d781804dff 100644 --- a/opentech/apply/funds/models/submissions.py +++ b/opentech/apply/funds/models/submissions.py @@ -34,6 +34,7 @@ from wagtail.contrib.forms.models import AbstractFormSubmission from opentech.apply.activity.messaging import messenger, MESSAGES from opentech.apply.categories.models import MetaTerm from opentech.apply.determinations.models import Determination +from opentech.apply.flags.models import Flag from opentech.apply.review.models import ReviewOpinion from opentech.apply.review.options import MAYBE, AGREE, DISAGREE from opentech.apply.stream_forms.files import StreamFieldDataEncoder @@ -125,6 +126,12 @@ class ApplicationSubmissionQueryset(JSONOrderable): def reviewed_by(self, user): return self.filter(reviews__author__reviewer=user) + def flagged_by(self, user): + return self.filter(flags__user=user, flags__type=Flag.USER) + + def flagged_staff(self): + return self.filter(flags__type=Flag.STAFF) + def partner_for(self, user): return self.filter(partners=user) @@ -399,6 +406,12 @@ class ApplicationSubmission( related_name='submissions', blank=True, ) + flags = GenericRelation( + Flag, + content_type_field='target_content_type', + object_id_field='target_object_id', + related_query_name='submission', + ) activities = GenericRelation( 'activity.Activity', content_type_field='source_content_type', @@ -650,6 +663,13 @@ class ApplicationSubmission( def reviewed_by(self, user): return self.assigned.reviewed().filter(reviewer=user).exists() + def flagged_by(self, user): + return self.flags.filter(user=user, type=Flag.USER).exists() + + @property + def flagged_staff(self): + return self.flags.filter(type=Flag.STAFF).exists() + def has_permission_to_review(self, user): if user.is_apply_staff: return True diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html index 4aeff8b6464d151641626801271bb18e06d98b87..4fcf1fc15172f635ea3ebe61507c890c700d228e 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_admin_detail.html @@ -28,6 +28,10 @@ {% include "funds/includes/update_meta_terms_form.html" %} {% endblock %} +{% block flags %} + {% include 'flags/flags.html' with submission=object user=request.user %} +{% endblock %} + {% block reviews %} <div class="sidebar__inner"> <h5>Reviews & assignees</h5> @@ -59,15 +63,13 @@ {{ reviewer_form.media.js }} {{ comment_form.media.js }} {{ partner_form.media.js }} + {{ block.super }} <script src="//cdnjs.cloudflare.com/ajax/libs/fancybox/3.4.1/jquery.fancybox.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/jquery-cookie/1.4.1/jquery.cookie.min.js"></script> <script src="{% static 'js/apply/fancybox-global.js' %}"></script> - <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/toggle-actions-panel.js' %}"></script> <script src="{% static 'js/apply/toggle-reviewers.js' %}"></script> <script src="{% static 'js/apply/toggle-sidebar.js' %}"></script> - <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> <script src="{% static 'js/apply/toggle-related.js' %}"></script> - <script src="{% static 'js/apply/edit-comment.js' %}"></script> <script src="{% static 'js/apply/toggle-proposal-info.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_community_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_community_detail.html index 4299c0aad4fd32882bf5f72db675c7cd41f0213d..f6ce10384853bd6ac87963f5ee0e8087b201248e 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_community_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_community_detail.html @@ -19,9 +19,7 @@ {% block extra_js %} {{ reviewer_form.media.js }} {{ comment_form.media.js }} - <script src="{% static 'js/apply/tabs.js' %}"></script> + {{ block.super }} <script src="{% static 'js/apply/toggle-reviewers.js' %}"></script> <script src="{% static 'js/apply/toggle-sidebar.js' %}"></script> - <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> - <script src="{% static 'js/apply/edit-comment.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html index 006202f030cefea19cf4251c10cacfb0b30e5eb0..09c04a170e811ef94abda9f11d513158ac8ee81a 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_detail.html @@ -97,6 +97,9 @@ {% block sidebar_top %} {% endblock %} + {% block flags %} + {% endblock %} + {% if object.project and PROJECTS_ENABLED %} {% include 'funds/includes/project_block.html' %} {% endif %} @@ -105,12 +108,11 @@ {% include 'determinations/includes/applicant_determination_block.html' with submission=object %} {% endblock %} - {% if request.user.is_apply_staff %} - {% block screening_status %} - {% endblock %} - {% block meta_terms %} - {% endblock %} - {% endif %} + {% block screening_status %} + {% endblock %} + + {% block meta_terms %} + {% endblock %} {% block reviews %} {% endblock %} @@ -166,4 +168,5 @@ <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> <script src="{% static 'js/apply/edit-comment.js' %}"></script> + <script src="{% static 'js/apply/flag.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_partner_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_partner_detail.html index 4299c0aad4fd32882bf5f72db675c7cd41f0213d..f6ce10384853bd6ac87963f5ee0e8087b201248e 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_partner_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_partner_detail.html @@ -19,9 +19,7 @@ {% block extra_js %} {{ reviewer_form.media.js }} {{ comment_form.media.js }} - <script src="{% static 'js/apply/tabs.js' %}"></script> + {{ block.super }} <script src="{% static 'js/apply/toggle-reviewers.js' %}"></script> <script src="{% static 'js/apply/toggle-sidebar.js' %}"></script> - <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> - <script src="{% static 'js/apply/edit-comment.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/applicationsubmission_reviewer_detail.html b/opentech/apply/funds/templates/funds/applicationsubmission_reviewer_detail.html index 4299c0aad4fd32882bf5f72db675c7cd41f0213d..f6ce10384853bd6ac87963f5ee0e8087b201248e 100644 --- a/opentech/apply/funds/templates/funds/applicationsubmission_reviewer_detail.html +++ b/opentech/apply/funds/templates/funds/applicationsubmission_reviewer_detail.html @@ -19,9 +19,7 @@ {% block extra_js %} {{ reviewer_form.media.js }} {{ comment_form.media.js }} - <script src="{% static 'js/apply/tabs.js' %}"></script> + {{ block.super }} <script src="{% static 'js/apply/toggle-reviewers.js' %}"></script> <script src="{% static 'js/apply/toggle-sidebar.js' %}"></script> - <script src="{% static 'js/apply/submission-text-cleanup.js' %}"></script> - <script src="{% static 'js/apply/edit-comment.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/base_submissions_table.html b/opentech/apply/funds/templates/funds/base_submissions_table.html index 6b6e6d4763412ff71ed8510cbe604ee26db1417c..cbc14661e487df5828230ddd2cfb7dc624f198c8 100644 --- a/opentech/apply/funds/templates/funds/base_submissions_table.html +++ b/opentech/apply/funds/templates/funds/base_submissions_table.html @@ -26,4 +26,5 @@ <script src="{% static 'js/apply/submission-tooltips.js' %}"></script> <script src="{% static 'js/apply/tabs.js' %}"></script> <script src="{% static 'js/apply/batch-actions.js' %}"></script> + <script src="{% static 'js/apply/flag.js' %}"></script> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/submissions_overview.html b/opentech/apply/funds/templates/funds/submissions_overview.html index 94c9092a804252fa86a7c31685f685e0faf15651..5f927338d8911ee5372f27c754592e870d03761e 100644 --- a/opentech/apply/funds/templates/funds/submissions_overview.html +++ b/opentech/apply/funds/templates/funds/submissions_overview.html @@ -23,12 +23,26 @@ {% endif %} {% block table %} - {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action use_batch_actions=False heading="All Submissions" %} + <div class="wrapper wrapper--bottom-space"> + {% include "funds/includes/table_filter_and_search.html" with filter_form=filter_form search_term=search_term use_search=True filter_action=filter_action use_batch_actions=False heading="All Submissions" %} - {% render_table table %} - <div class="all-submissions-table__more"> - <a href="{% url 'apply:submissions:list' %}">Show all</a> + {% render_table table %} + <div class="all-submissions-table__more"> + <a href="{% url 'apply:submissions:list' %}">Show all</a> + </div> + </div> + + {% if staff_flagged.table.data %} + <div class="wrapper wrapper--bottom-space"> + <h4 class="heading heading--normal">Staff Flagged Submissions</h4> + {% render_table staff_flagged.table %} + {% if staff_flagged.display_more %} + <div class="all-submissions-table__more"> + <a href="{% url 'apply:submissions:staff_flagged' %}">Show all</a> + </div> + {% endif %} </div> + {% endif %} {% endblock %} </div> {% endblock %} diff --git a/opentech/apply/funds/templates/funds/submissions_staff_flagged.html b/opentech/apply/funds/templates/funds/submissions_staff_flagged.html new file mode 100644 index 0000000000000000000000000000000000000000..328d6c7e8647ad0e4df827c709cf13dfd6c82922 --- /dev/null +++ b/opentech/apply/funds/templates/funds/submissions_staff_flagged.html @@ -0,0 +1,7 @@ +{% extends "funds/submissions.html" %} + +{% block page_header %} + <div> + <h1 class="gamma heading heading--no-margin heading--bold">Staff Flagged Submissions</h1> + </div> +{% endblock %} diff --git a/opentech/apply/funds/templates/funds/submissions_user_flagged.html b/opentech/apply/funds/templates/funds/submissions_user_flagged.html new file mode 100644 index 0000000000000000000000000000000000000000..47f7839456b94fec2c92bcba496c4b21f0cfa3a3 --- /dev/null +++ b/opentech/apply/funds/templates/funds/submissions_user_flagged.html @@ -0,0 +1,7 @@ +{% extends "funds/submissions.html" %} + +{% block page_header %} + <div> + <h1 class="gamma heading heading--no-margin heading--bold">My Flagged Submissions</h1> + </div> +{% endblock %} diff --git a/opentech/apply/funds/urls.py b/opentech/apply/funds/urls.py index 122472c123c1d140a63e006fb13dbddedac2c044..82e31476aa4bf6312cac9d9bac97da4ca8f6cd57 100644 --- a/opentech/apply/funds/urls.py +++ b/opentech/apply/funds/urls.py @@ -17,6 +17,8 @@ from .views import ( SubmissionPrivateMediaView, SubmissionDetailPDFView, SubmissionDetailSimplifiedView, + SubmissionUserFlaggedView, + SubmissionStaffFlaggedView, ) from .api_views import ( CommentEdit, @@ -41,6 +43,10 @@ app_name = 'funds' submission_urls = ([ path('', SubmissionOverviewView.as_view(), name="overview"), path('all/', SubmissionListView.as_view(), name="list"), + path('flagged/', include([ + path('', SubmissionUserFlaggedView.as_view(), name="flagged"), + path('staff/', SubmissionStaffFlaggedView.as_view(), name="staff_flagged"), + ])), path('<int:pk>/', include([ path('', SubmissionDetailView.as_view(), name="detail"), path('edit/', SubmissionEditView.as_view(), name="edit"), @@ -58,6 +64,7 @@ submission_urls = ([ path('revisions/', include(revision_urls, namespace="revisions")), ])), path('', include('opentech.apply.determinations.urls', namespace="determinations")), + path('', include('opentech.apply.flags.urls', namespace="flags")), path('<slug:status>/', SubmissionsByStatus.as_view(), name='status'), ], 'submissions') diff --git a/opentech/apply/funds/views.py b/opentech/apply/funds/views.py index c757e048335a60a1954748851c71d341c4d4a027..bd85a504b60df71597c0aa34a736f5c7eaa1ddc2 100644 --- a/opentech/apply/funds/views.py +++ b/opentech/apply/funds/views.py @@ -240,13 +240,15 @@ class SubmissionOverviewView(BaseAdminSubmissionsTable): filter_action = reverse_lazy('funds:submissions:list') def get_table_data(self): - return super().get_table_data().order_by(F('last_update').desc(nulls_last=True))[:5] + limit = 5 + return super().get_table_data().order_by(F('last_update').desc(nulls_last=True))[:limit] def get_context_data(self, **kwargs): + limit = 6 base_query = RoundsAndLabs.objects.with_progress().active().order_by('-end_date') - open_rounds = base_query.open()[:6] + open_rounds = base_query.open()[:limit] open_query = '?round_state=open' - closed_rounds = base_query.closed()[:6] + closed_rounds = base_query.closed()[:limit] closed_query = '?round_state=closed' rounds_title = 'All Rounds and Labs' @@ -265,6 +267,8 @@ class SubmissionOverviewView(BaseAdminSubmissionsTable): for status, data in PHASES_MAPPING.items() } + staff_flagged = self.get_staff_flagged() + return super().get_context_data( open_rounds=open_rounds, open_query=open_query, @@ -272,9 +276,20 @@ class SubmissionOverviewView(BaseAdminSubmissionsTable): closed_query=closed_query, rounds_title=rounds_title, status_counts=grouped_statuses, + staff_flagged=staff_flagged, **kwargs, ) + def get_staff_flagged(self): + qs = super().get_queryset().flagged_staff().order_by('-submit_time') + row_attrs = dict({'data-flag-type': 'staff'}, **SummarySubmissionsTable._meta.row_attrs) + + limit = 5 + return { + 'table': SummarySubmissionsTable(qs[:limit], prefix='staff-flagged-', attrs={'class': 'all-submissions-table flagged-table'}, row_attrs=row_attrs), + 'display_more': qs.count() > limit, + } + class SubmissionAdminListView(BaseAdminSubmissionsTable, DelegateableListView): template_name = 'funds/submissions.html' @@ -294,6 +309,22 @@ class SubmissionListView(ViewDispatcher): reviewer_view = SubmissionReviewerListView +@method_decorator(staff_required, name='dispatch') +class SubmissionStaffFlaggedView(BaseAdminSubmissionsTable): + template_name = 'funds/submissions_staff_flagged.html' + + def get_queryset(self): + return self.filterset_class._meta.model.objects.current().for_table(self.request.user).flagged_staff().order_by('-submit_time') + + +@method_decorator(staff_required, name='dispatch') +class SubmissionUserFlaggedView(BaseAdminSubmissionsTable): + template_name = 'funds/submissions_user_flagged.html' + + def get_queryset(self): + return self.filterset_class._meta.model.objects.current().for_table(self.request.user).flagged_by(self.request.user).order_by('-submit_time') + + @method_decorator(staff_required, name='dispatch') class SubmissionsByRound(BaseAdminSubmissionsTable, DelegateableListView): template_name = 'funds/submissions_by_round.html' diff --git a/opentech/settings/base.py b/opentech/settings/base.py index 9ec76d95997468bbbedb312928356f9e08b3e32b..0aebe1efce6d9f484565bc6ea109210618b4c61d 100644 --- a/opentech/settings/base.py +++ b/opentech/settings/base.py @@ -71,6 +71,7 @@ INSTALLED_APPS = [ 'opentech.apply.categories', 'opentech.apply.funds', 'opentech.apply.dashboard', + 'opentech.apply.flags', 'opentech.apply.home', 'opentech.apply.users', 'opentech.apply.review', diff --git a/opentech/static_src/src/javascript/apply/flag.js b/opentech/static_src/src/javascript/apply/flag.js new file mode 100644 index 0000000000000000000000000000000000000000..932622b582da6ea26d9931ce63d774060f1db262 --- /dev/null +++ b/opentech/static_src/src/javascript/apply/flag.js @@ -0,0 +1,34 @@ +(function ($) { + + 'use strict'; + + $('.flagged-table').find('.all-submissions-table__parent').each(function () { + var $flagged_item = $(this); + var submission_id = $flagged_item.data('record-id'); + var flag_type = $flagged_item.data('flag-type'); + var $button = '<span class="button--float"><button class="button button--flag button--unflag flagged" data-id="' + submission_id + '" data-type="' + flag_type + '">Flag</button></span>'; + $flagged_item.find('td.comments').css('position', 'relative').append($button); + }); + + $('.button--flag').on('click', function (e) { + e.preventDefault(); + + var $current = $(this); + var id = $current.data('id'); + var type = $current.data('type'); + + $.ajax({ + url: '/apply/submissions/' + id + '/' + type + '/flag/', + type: 'POST', + success: function (json) { + if (json.result) { + $current.addClass('flagged'); + } + else { + $current.removeClass('flagged'); + } + } + }); + }); + +})(jQuery); diff --git a/opentech/static_src/src/javascript/js.cookie.min.js b/opentech/static_src/src/javascript/js.cookie.min.js new file mode 100644 index 0000000000000000000000000000000000000000..f5f4c36c17146c4d1fb7346fca83f196de695e8b --- /dev/null +++ b/opentech/static_src/src/javascript/js.cookie.min.js @@ -0,0 +1,3 @@ +/*! js-cookie v2.2.1 | MIT */ + +!function(a){var b;if("function"==typeof define&&define.amd&&(define(a),b=!0),"object"==typeof exports&&(module.exports=a(),b=!0),!b){var c=window.Cookies,d=window.Cookies=a();d.noConflict=function(){return window.Cookies=c,d}}}(function(){function a(){for(var a=0,b={};a<arguments.length;a++){var c=arguments[a];for(var d in c)b[d]=c[d]}return b}function b(a){return a.replace(/(%[0-9A-Z]{2})+/g,decodeURIComponent)}function c(d){function e(){}function f(b,c,f){if("undefined"!=typeof document){f=a({path:"/"},e.defaults,f),"number"==typeof f.expires&&(f.expires=new Date(1*new Date+864e5*f.expires)),f.expires=f.expires?f.expires.toUTCString():"";try{var g=JSON.stringify(c);/^[\{\[]/.test(g)&&(c=g)}catch(j){}c=d.write?d.write(c,b):encodeURIComponent(c+"").replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),b=encodeURIComponent(b+"").replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent).replace(/[\(\)]/g,escape);var h="";for(var i in f)f[i]&&(h+="; "+i,!0!==f[i]&&(h+="="+f[i].split(";")[0]));return document.cookie=b+"="+c+h}}function g(a,c){if("undefined"!=typeof document){for(var e={},f=document.cookie?document.cookie.split("; "):[],g=0;g<f.length;g++){var h=f[g].split("="),i=h.slice(1).join("=");c||'"'!==i.charAt(0)||(i=i.slice(1,-1));try{var j=b(h[0]);if(i=(d.read||d)(i,j)||b(i),c)try{i=JSON.parse(i)}catch(k){}if(e[j]=i,a===j)break}catch(k){}}return a?e[a]:e}}return e.set=f,e.get=function(a){return g(a,!1)},e.getJSON=function(a){return g(a,!0)},e.remove=function(b,c){f(b,"",a(c,{expires:-1}))},e.defaults={},e.withConverter=c,e}return c(function(){})}); \ No newline at end of file diff --git a/opentech/static_src/src/javascript/main.js b/opentech/static_src/src/javascript/main.js index c49fc879d90c48691fcadfe0c68a1e9ec0f2adc1..6dc0b3e5f608ea73ca1aa75e950393e754a11157 100644 --- a/opentech/static_src/src/javascript/main.js +++ b/opentech/static_src/src/javascript/main.js @@ -1,4 +1,3 @@ - (function ($) { 'use strict'; @@ -157,4 +156,18 @@ document.documentElement.style.setProperty('--header-admin-height', headerHeight + adminbarHeight + 'px'); }); + // Setting the CSRF token on AJAX requests. + var csrftoken = window.Cookies.get('csrftoken'); + function csrfSafeMethod(method) { + // these HTTP methods do not require CSRF protection + return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method)); + } + $.ajaxSetup({ + beforeSend: function (xhr, settings) { + if (!csrfSafeMethod(settings.type) && !this.crossDomain) { + xhr.setRequestHeader('X-CSRFToken', csrftoken); + } + } + }); + })(jQuery); diff --git a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss index e1beb948c3a18d620144909ed2d2eba0bf56887f..ec269cd16c687695bcba7835739154ed2a8aae54 100644 --- a/opentech/static_src/src/sass/apply/abstracts/_mixins.scss +++ b/opentech/static_src/src/sass/apply/abstracts/_mixins.scss @@ -106,6 +106,16 @@ } } +// Small button mixin +@mixin button--small { + padding: 2px; + font-size: 12px; + + @include media-query(tablet-landscape) { + padding: 3px 5px; + } +} + // Viewport sized typography mixin that takes a min and max pixel-based value @mixin responsive-font-sizes($min, $max) { diff --git a/opentech/static_src/src/sass/apply/components/_button.scss b/opentech/static_src/src/sass/apply/components/_button.scss index f5b89abb31e2b2d794f975eaf89f6b15934e514a..18cf4fd45886a46f511e7dc5bd4563a6f03f62ce 100644 --- a/opentech/static_src/src/sass/apply/components/_button.scss +++ b/opentech/static_src/src/sass/apply/components/_button.scss @@ -326,4 +326,44 @@ background-color: $color--button-disabled; } } + + &--flag { + &.flagged { + position: relative; + + &::after { + content: '\2691'; + color: $color--tomato; + position: absolute; + top: 4px; + padding-left: 5px; + font-size: map-get($font-sizes, delta); + line-height: 1; + } + } + } + + &--unflag { + @include button($color--light-blue, $color--dark-blue); + @include button--small; + padding-right: 18px; + + @include media-query(tablet-landscape) { + padding-right: 18px; + } + + &.flagged { + &::after { + top: 2px; + padding-left: 3px; + font-size: map-get($font-sizes, zeta); + } + } + } + + &--float { + position: absolute; + top: 2px; + right: 2px; + } } diff --git a/opentech/templates/base-apply.html b/opentech/templates/base-apply.html index 3291995c6148c0442aab8cb5c35c0da23c01a9e6..630279b297e90e889ec799916027312ca00563d8 100644 --- a/opentech/templates/base-apply.html +++ b/opentech/templates/base-apply.html @@ -32,6 +32,7 @@ <link rel="stylesheet" href="{% static 'css/print.css' %}" media="print"> <script src="{% static 'js/jquery.min.js' %}"></script> + <script src="{% static 'js/js.cookie.min.js' %}"></script> <script src="{% static 'js/main.js' %}"></script> </head>