diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8cc2a1c..33241ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,27 @@
# VINCE Changelog
+# Version 1.50.0: 2022-07-19
+============================
+
+New MFA reset workflow
+
+Allow comments when re-assigning tickets
+
+Sorting improvements on VINCEComm Dashboard
+
+Add Vul Note download button in VINCETrack
+
+Bug Fixes
+
+# Version 1.49.0: 2022-07-19
+===========================
+
+Contact Management Updates
+
+Dependency Upgrades
+
+Bug Fixes
+
# Version 1.48.0: 2022-05-13
=============================
diff --git a/bakery/static_views.py b/bakery/static_views.py
index 77fd1b1..8b8cf20 100644
--- a/bakery/static_views.py
+++ b/bakery/static_views.py
@@ -14,7 +14,8 @@
from django.http import HttpResponseNotModified
from django.template import Template, Context, TemplateDoesNotExist
from django.utils.http import http_date, parse_http_date
-
+from django.conf import settings
+from django.utils.http import is_same_domain, is_safe_url
def serve(request, path, document_root=None, show_indexes=False, default=''):
"""
@@ -52,7 +53,10 @@ def serve(request, path, document_root=None, show_indexes=False, default=''):
continue
newpath = os.path.join(newpath, part).replace('\\', '/')
if newpath and path != newpath:
- return HttpResponseRedirect(newpath)
+ if is_safe_url(newpath,set(settings.ALLOWED_HOSTS),True):
+ return HttpResponseRedirect(newpath)
+ else:
+ raise Http404("Invalid or Incorrect path found")
fullpath = os.path.join(document_root, newpath)
if os.path.isdir(fullpath) and default:
defaultpath = os.path.join(fullpath, default)
diff --git a/bigvince/settings_.py b/bigvince/settings_.py
index b8fcf83..eef0e43 100644
--- a/bigvince/settings_.py
+++ b/bigvince/settings_.py
@@ -56,7 +56,7 @@
ROOT_DIR = environ.Path(__file__) - 3
# any change that requires database migrations is a minor release
-VERSION = "1.48.0"
+VERSION = "1.50.0"
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/
diff --git a/cogauth/forms.py b/cogauth/forms.py
index 46c15e0..ce8957b 100644
--- a/cogauth/forms.py
+++ b/cogauth/forms.py
@@ -83,6 +83,13 @@ class Meta:
'countrycode': CountrySelectWidget()}
+class COGResetMFA(forms.Form):
+
+ reason = forms.CharField(
+ widget=forms.Textarea(),
+ label=_('Reason for MFA reset'))
+
+
class COGInitialPWResetForm(forms.Form):
username = forms.CharField(max_length=200, required=True, label=_("Email"))
@@ -260,9 +267,11 @@ class SignUpForm(UserCreationForm):
required=False)
email = forms.CharField(
max_length=254,
+ widget=forms.TextInput(attrs={'autocomplete':'username'}),
required=True,
- help_text=_('This will be your login username. Please note that this field is CASE SENSITIVE.'),
+ help_text=_('This will be your personal login username. This field is CASE SENSITIVE. PLEASE NOTE: Each VINCE user account is intended to be tied to a specific individual. If you would like to use an alias (for example, psirt@example.com ) to receive group notifications, please create your account here first, and once your individual account has been approved, you will have the opportunity to create a group, join an existing group, and otherwise manage the email addresses associated with your organization.'),
label="Email address")
+
title = forms.CharField(
max_length=200,
required=False,
@@ -279,7 +288,7 @@ class SignUpForm(UserCreationForm):
password1 = forms.CharField(
max_length=50,
required=True,
- widget=forms.PasswordInput,
+ widget=forms.PasswordInput(attrs={'autocomplete':"new-password"}),
label="New Password",
help_text=_('Password Requirements:
\
Minimum length is 8 characters \
@@ -293,7 +302,7 @@ class SignUpForm(UserCreationForm):
password2 = forms.CharField(
max_length=50,
required=True,
- widget=forms.PasswordInput,
+ widget=forms.PasswordInput(attrs={'autocomplete':"new-password"}),
label="Password confirmation",
help_text=_('Enter the same password as before, for verification')
)
diff --git a/cogauth/templates/cogauth/loginhelp.html b/cogauth/templates/cogauth/loginhelp.html
index 600b670..cfe6581 100644
--- a/cogauth/templates/cogauth/loginhelp.html
+++ b/cogauth/templates/cogauth/loginhelp.html
@@ -7,8 +7,17 @@
{% block content %}
-If you lost your multi-factor authentication (MFA) device, you will need to contact us at {{ CONTACT_EMAIL }} to reset your account.
+{% if showlink %}
+If you lost your multi-factor authentication (MFA) device, you will need to initiate the MFA reset process.
+Reset MFA
+
+{% else %}
+If you lost your multi-factor authentication (MFA) device, you can initiate the reset process with your username and password. First login and then click the Troubleshoot MFA link. You will be provided with further instructions to reset your MFA.
+If you do not have your password, you can reset it here and then attempt to reset your MFA.
+If you still need help, contact us at {{ CONTACT_EMAIL }} .
+
+{% endif %}
{% endblock %}
diff --git a/cogauth/templates/cogauth/profile.html b/cogauth/templates/cogauth/profile.html
index 202b01c..9ef28b0 100644
--- a/cogauth/templates/cogauth/profile.html
+++ b/cogauth/templates/cogauth/profile.html
@@ -75,7 +75,7 @@ {{ coguser.preferred_username }}
{{ coguser.title }}
- Vendor Groups:
+ User Groups:
{{ my_groups }}
diff --git a/cogauth/templates/cogauth/resetmfa.html b/cogauth/templates/cogauth/resetmfa.html
new file mode 100644
index 0000000..c0313d4
--- /dev/null
+++ b/cogauth/templates/cogauth/resetmfa.html
@@ -0,0 +1,22 @@
+{% extends "vince/login.html" %}
+
+{% load i18n static %}
+
+{% block content_title %}VINCE MFA Reset {% endblock %}
+
+
+{% block content %}
+
+ Please let us know why you need us to reset your multi-factor authentication (MFA) device. To continue the reset, you must follow the directions in the email that will be sent to you upon submitting this form. Once we receive confirmation, an analyst will reset the MFA associated with your account during business hours.
+ After the reset is complete, you will be prompted to re-associate your MFA device with your VINCE account upon logging in.
+
+
+
+{% endblock %}
+
+
+
diff --git a/cogauth/templates/cogauth/signup.html b/cogauth/templates/cogauth/signup.html
index f8b3a5a..7c4652b 100644
--- a/cogauth/templates/cogauth/signup.html
+++ b/cogauth/templates/cogauth/signup.html
@@ -3,6 +3,8 @@
{% block extrahead %}
+
+
{% endblock %}
@@ -13,6 +15,10 @@
{% block content %}
+
+VINCE accounts are intended to be tied to a real person. If you would like to establish a group with multiple people (for example, psirt@example.com ) and use an email list or alias for group notifications, please proceed with creating your individual account here, and once your account has been approved, you can request the creation of your group and manage the email addresses associated with your organization.
+
+
+{% if contact.active %}{% else %}{% if vince_users %}
+
+
+
Uh oh! This contact is inactive but has active VINCE users. Please remove VINCE users or activate contact!
+
+
+{% endif %}
+{% endif %}
+
@@ -69,7 +73,7 @@ Contact: {{ contact.vendor_name }}
-
+
@@ -80,49 +84,162 @@ Contact: {{ contact.vendor_name }}
-
- {% if object.active %}
-
{{ object.vendor_name }} (Active)
- {% else %}
-
{{ object.vendor_name }} (Inactive)
- {% endif %}
-
-
-
-
- {% trans "Delete" %} {% if vc_contact %} {% trans "View Case Permissions" %} {% endif %}
-
-
-
- SRMail: {{ object.srmail_peer }} Salutation: {{ object.srmail_salutation }}
-
-
+
+
+ {% if object.active %}
+
{{ object.vendor_name }} (Active)
+ {% else %}
+ {{ object.vendor_name }} (Inactive)
+ {% endif %}
+
+
+
+
+
+
+
+
+
{% trans "Type" %}:
+ {% if object.vendor_type == "Contact" %}
+ User
+ {% else %}
+ {{ object.vendor_type }}
+ {% endif %}
+
+ {% if object.vendor_type == "Vendor" %}
+ A vendor type contact is typically a company or organization that creates or sells products. This type of contact usually consists of 1 or more people and has the ability to manage users in their group in VINCEComm. A vendor should name at least 1 group admin.
+ {% elif object.vendor_type == "Coordinator" %}
+ A coordinator type contact is able to invite/add users to their group. A coordinator group should have at least 1 group admin. Coordinator contacts should not be listed as vendors in cases.
+ {% else %}
+ A "Contact" or "User" type contact is typically a single person's contact information. They will not be able to add users to their group.
+ {% endif %}
+
+
+
({% if vince_users|length == 1 %}1 User{% else %}{{vince_users|length}} Users{% endif %}{% if object.vendor_type == "Contact" %}{% else %}{% if groupadmins|length == 1 %}, 1 Admin{% else %}, {{ groupadmins|length }} Admins{% endif %}{% endif %})
+
+
+
+
+ Location: {{ object.location }} [{{ object.countrycode }}]
+
+
+
+
{% if object.comment %}
-
- {% trans "Comment" %}: {{ object.comment }}
-
+
{% trans "Comment" %}: {{ object.comment }}
{% endif %}
-
- {% trans "Type" %}
- {% if object.vendor_type == "Vendor" %}
- {{ object.vendor_type }} [#{{ object.lotus_id }}]
+
+
+
+
+
+ A Notification-Only email address is typically a team email or email list address. VINCE users with Notification-Only addresses will NOT be able to access cases associated with this contact.
+ To change email type, remove email and re-add email with desired type.
+
+
+
+
+
+
+
+
+ Type
+ Email
+ User (Name)
+ Notifications
+ Actions
+
+
+ {% for email in object.emailcontact_set.all %}
+ {% with t=email.email|vince_user_link %}
+
+
+ {% if email.email_list %}
+ Notification-Only
+ {% elif t %}
+ User
+ {% if email.email in groupadmins %} {% endif %}
+ {% elif object.id|inviteduser:email.email %}
+ Invited User
+ {% else %}
+ Email
+ {% endif %}
+
+
+ {{ email.email }}{% if email.status %}{% else %}(INACTIVE) {% endif %}
+
+ {% if email.email in vince_users and t %}
+ {{ t.vinceprofile.preferred_username }} ({% if t.get_full_name %}{{ t.get_full_name }}{% else %}{{ email.name }}{% endif %})
{% else %}
- {{ object.vendor_type }}
+ {% if email.name %}({{ email.name }}){% endif %}
{% endif %}
-
-
- {% trans "Location" %}
- {{ object.location }} [{{ object.countrycode }}]
-
- {% for email in object.emailcontact_set.all %}
-
- Email {% if email.email_list %}[EMAIL LIST]{% else %} [{{ email.email_type}}]{% endif %}
- {{ email.email_function }}: {{ email.email }}, {{ email.name }} {% if email.status %}{% else %}(INACTIVE) {% endif %}
-
- {% endfor %}
+ {% if email.email_function in "TO,CC" %}Enabled {% else %}Disabled {% endif %}
+ {% if user.usersettings.contacts_write %}
+ {% if email.email_list %}{% else %}
+ {% if email.email in groupadmins %}
+
+ {% elif t %}
+ {% if object.vendor_type == "Contact" %}{% else %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ {% if email.email in vince_users and t %} {% endif %}
+
+
+ {% endwith %}
+ {% endfor %}
+ {% for email in associations %}
+
+ Verification Initiated
+ {{ email.user }}
+
+
+
+ {% endfor %}
+
+
+
+
+
{% for phone in object.phonecontact_set.all %}
- Phone [{{ phone.phone_type }}]
+ Phone [{{ phone.phone_type }}]
{{ phone.country_code }} {{ phone.phone }} {% if phone.comment %}(Comment: {{ phone.comment }}){% endif %}
{% endfor %}
@@ -150,32 +267,16 @@ {{ object.vendor_name }} (Inactive)
{{ web.url }} {% if web.description %}{{ web.description }}{% endif %}
{% endfor %}
-
- Groups
-
- {% for group in groups %}
- {{ group.group.name }} ,
- {% empty %}
- This vendor is not a member of any groups
- {% endfor %}
-
-
-
- VINCEComm Users {% if vince_users %} {% endif %}
- {% for u in vince_users %}
- {{ u.username }} ({{ u.vinceprofile__preferred_username}}) {% endfor %}
-
- Vendor Admins {% if groupadmins %}
- {% endif %}
+ Groups
-
+
- Tags
+ Tags
@@ -278,8 +379,10 @@ No cases found
-
- {% include 'vince/include/contactactivity.html' with object_list=activity_list %}
+
+
+
+ {% include 'vince/include/alt_contact_activity.html' with object_list=activity_list %}
diff --git a/vince/templates/vince/contact_report.html b/vince/templates/vince/contact_report.html
index c3990a6..5e0bdd0 100644
--- a/vince/templates/vince/contact_report.html
+++ b/vince/templates/vince/contact_report.html
@@ -44,6 +44,11 @@ Contact Reports
Users without MFA
+
+
+ Users with Email notificiations disabled
+
+
@@ -59,6 +64,8 @@
Query Results {% if results %}({{ results|length }} resu
{{ x.vendor_name }}
{% endif %}
+ {% empty %}
+ 0 Results
{% endfor %}
diff --git a/vince/templates/vince/contactsresults.html b/vince/templates/vince/contactsresults.html
index d869027..dea9e72 100644
--- a/vince/templates/vince/contactsresults.html
+++ b/vince/templates/vince/contactsresults.html
@@ -20,7 +20,7 @@
{% if contact.vendor_type == "Contact" %}
{{ contact.vendor_name }}
{% autoescape off %}{{ contact.get_tag_html }}{% endautoescape %}
- {% elif contact.vendor_type == "Vendor" %}
+ {% elif contact.vendor_type in "Vendor,Coordinator" %}
{{ contact.vendor_name }}
{% autoescape off %}{{ contact.get_tag_html }}{% endautoescape %}
{% elif contact.srmail_peer_name %}
diff --git a/vince/templates/vince/create_case.html b/vince/templates/vince/create_case.html
index 4533347..e8da863 100644
--- a/vince/templates/vince/create_case.html
+++ b/vince/templates/vince/create_case.html
@@ -65,7 +65,7 @@ {{ REPORT_ID }}: {{ form.vrf_id.value }}
Submitted on: {{ form.date_submitted.value }}
{% else %}
{% if create_case %}
- You requested to create a Case. Please fill in the required information before creating the case.
+ You requested to create a Case. Please fill in the required information before creating the case. All information can be modified later.
{% else %}
You requested to create a Case Request. Please fill in the following information to submit the request.
{% endif %}
@@ -81,7 +81,7 @@
Submitted on: {{ form.date_submitted.value }}
Vulnerability Information
What is the name of the affected product or software? *
- {{ form.product_name.help_text }}
+ {{ form.product_name.help_text }} This will also be used as the Case title.
{% render_field form.product_name class="form-control" %}
@@ -95,66 +95,30 @@
Edit Contact: {{ contact.vend
-
-
-
-
-
+ {% endcomment %}
Crypto
@@ -270,6 +235,7 @@
Edit Contact: {{ contact.vend
+ {% comment "No need for srmail block any longer" %}
SRMail block
@@ -291,6 +257,7 @@
Edit Contact: {{ contact.vend
+ {% endcomment %}
Postal
diff --git a/vince/templates/vince/editgroup.html b/vince/templates/vince/editgroup.html
index 9c84678..a39093b 100644
--- a/vince/templates/vince/editgroup.html
+++ b/vince/templates/vince/editgroup.html
@@ -38,20 +38,12 @@
Edit Group: {{ group.name }}
{% endif %}
-{% if contacts_without_keys or expired_keys_contacts or contacts_without_emails %}
+{% if inactive_contacts %}
WARNING - see below for details
- {% if contacts_without_keys %}
- This group has contact(s) without keys
- {% endif %}
- {% if expired_keys_contacts %}
- This group has contact(s) with expired keys
- {% endif %}
- {% if contacts_without_emails %}
- This group has contact(s) without email addresses
- {% endif %}
+ This group has {{ inactive_contacts }} inactive member(s).
@@ -63,15 +55,6 @@ Edit Group: {{ group.name }}
{% csrf_token %}
-
@@ -81,25 +64,6 @@
Edit Group: {{ group.name }}
-
-
-
+
Add Contacts to the Group:
@@ -142,14 +114,6 @@
Edit Group: {{ group.name }}
-
@@ -158,43 +122,5 @@
Edit Group: {{ group.name }}
-
-{% if contacts_without_keys %}
-
-
-
-
WARNING: The following contact(s) do not have a PGP key on file:
- {% for contact in contacts_without_keys %}
-
{{ contact }}
- {% endfor %}
-
-
-
-{% endif %}
-{% if expired_keys_contacts %}
-
-
-
-
WARNING: The following contact(s) have expired PGP Keys on file:
- {% for contact in expired_keys_contacts %}
-
{{ contact }}
- {% endfor %}
-
-
-
-{% endif %}
-{% if contacts_without_emails %}
-
-
-
-
WARNING: The following contact(s) do not have an EMAIL on file:
- {% for contact in contacts_without_emails %}
-
{{ contact }}
- {% endfor %}
-
-
-
-{% endif %}
-
{% endblock %}
diff --git a/vince/templates/vince/group.html b/vince/templates/vince/group.html
index 4aeb70c..038aead 100644
--- a/vince/templates/vince/group.html
+++ b/vince/templates/vince/group.html
@@ -12,7 +12,7 @@
Contact Group: {{ group.name }}
@@ -25,27 +25,19 @@
Contact Group: {{ group.name }}
-{% if contacts_without_keys or expired_keys_contacts or contacts_without_emails %}
+{% if inactive_contacts %}
-
WARNING - see list below for details
- {% if contacts_without_keys %}
- This group has contact(s) without keys
- {% endif %}
- {% if expired_keys_contacts %}
- This group has contact(s) with expired keys
- {% endif %}
- {% if contacts_without_emails %}
- This group has contact(s) without email addresses
- {% endif %}
+
WARNING
+ This group has {{ inactive_contacts }} inactive member(s).
{% endif %}
-
+
Details
@@ -53,85 +45,94 @@ Contact Group: {{ group.name }}
- {% if object.status == "Active" %}
-
{{ object.name }} Group (Active)
- {% else %}
-
{{ object.name }} Group (Inactive)
- {% endif %}
-
-
-
-
- {% trans "Delete" %}
-
-
-
- SRMail: {{ object.srmail_peer_name }}
-
-
- {% if object.description %}
-
- {% trans "Description" %}: {{ object.description }}
-
- {% endif %}
- {% if object.comment %}
-
- {% trans "Comment" %}: {{ object.comment }}
-
- {% endif %}
-
- {% trans "Type" %}
- {{ object.group_type }}
-
-
- {% trans "Members" %}
-
-
- {% for member in group_members %}
-
- {{member.contact.vendor_name}} {% if member.contact in contacts_without_keys %} NO KEY {% endif %}{% if member.contact in contacts_without_emails %} NO EMAIL {% endif %}
-
- {% endfor %}
- {% if groupmembers %}
-
- GROUPS
-
- {% endif %}
- {% for member in groupmembers %}
-
- {% endfor %}
- {# if forloop.counter|divisibleby:4 #}
-
- {# endif #}
-
+
-
-
- {% for c in cases %}
-
+
+
+ {% for c in cases %}
+
+ {% endfor %}
-
- {% include 'vince/include/groupactivity.html' with object_list=activity %}
+
+
+
+ {% include 'vince/include/alt_contact_activity.html' with activity_list=activity %}
{% endblock %}
diff --git a/vince/templates/vince/include/alt_contact_activity.html b/vince/templates/vince/include/alt_contact_activity.html
new file mode 100644
index 0000000..e91434c
--- /dev/null
+++ b/vince/templates/vince/include/alt_contact_activity.html
@@ -0,0 +1,81 @@
+{% load i18n humanize contact_tags %}
+
{% if title %}{% trans title %}{% else %}{% trans "Activity" %}
+ {% endif %}
+
+
+
+
+
+ {% for item in activity_list %}
+
+
+ {% autoescape off %}{% if item.field %}{{ item.action|contactactionlogo:item.field }}{% else %}{{ item.action|contactactionlogo:"" }}{% endif %}{% endautoescape %}
+
+
+
+ {% if item.action == 1 %}
+ {% if item.contact %}
+ {{item.user.usersettings.preferred_username}} created vendor: {{ item.contact.vendor_name}}
+ {% elif item.group %}
+ {{item.user.usersettings.preferred_username}} created group: {{ item.group.name }}
+ {% endif %}
+ {% elif item.action == 2 %}
+ {% if item.group %}
+ {{item.user.usersettings.preferred_username}} {{ item.text }}
+ {% else %}
+ {{item.user.usersettings.preferred_username}} removed vendor {{ item.contact.vendor_name }}{% endif %}
+ {% elif item.action == 3 %}
+ {% if item.user.usersettings.preferred_username %} {{ item.user.usersettings.preferred_username }} {% else %} {{ item.action.user.vinceprofile.preferred_username }}{% endif %} {{ item.text }}
+ {% if show_contact and item.contact.vendor_name %}
+ for {{ item.contact.vendor_name }}
+ {% endif %}
+
+ {% elif item.action == 4 %}
+ {% if item.user.usersettings.preferred_username %} {{ item.user.usersettings.preferred_username }} {% else %} {{ item.action.user.vinceprofile.preferred_username }}{% endif %} {{ item.text }}
+ {% if show_contact and item.contact.vendor_name %}
+ for {{ item.contact.vendor_name }}
+ {% endif %}
+
+ {% elif item.action == 5 %}
+ {% if item.user.usersettings.preferred_username %} {{ item.user.usersettings.preferred_username }} {% else %} {{ item.action.user.vinceprofile.preferred_username }}{% endif %} {{ item.text }}
+ {% if show_contact and item.contact.vendor_name %}
+ for {{ item.contact.vendor_name }}
+ {% endif %}
+
+ {% elif item.action == 6 %}
+ {% if item.user.usersettings.preferred_username %} {{ item.user.usersettings.preferred_username }} {% else %} {{ item.action.user.vinceprofile.preferred_username }}{% endif %} {{ item.text }}
+ for {{ item.contact.vendor_name }}
+ {% else %}
+ {% if item.field == "NEW" %}
+ {{item.action.user.vinceprofile.preferred_username }} {{ item.field }} {{ item.model }} {{ item.new_value }}
+
+ {% elif item.field == "REMOVED" %}
+ {{item.action.user.vinceprofile.preferred_username }} {{ item.field }} {{ item.model }} {{ item.old_value }}
+
+ {% else %}
+ {{item.action.user.vinceprofile.preferred_username }} modified {{ item.model }}:{{ item.field }}
+ {% if item.old_value and item.new_value %}
+ from {{ item.old_value }} to
+ {{ item.new_value }}
+ {% elif item.old_value %}
+ {{ item.old_value }}
+ {% elif item.new_value %}
+ {{ item.new_value }}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+
+
+
+ {{ item.action_ts|date:"Y-m-d" }}({{ item.action_ts|timesince }} ago)
+
+
+
+
+ {% empty %}
+
No recent activity
+ {% endfor %}
+
+
+
+
diff --git a/vince/templates/vince/initcontactverify.html b/vince/templates/vince/initcontactverify.html
index e5ddba6..5148e9e 100644
--- a/vince/templates/vince/initcontactverify.html
+++ b/vince/templates/vince/initcontactverify.html
@@ -19,7 +19,7 @@
Initialize Contact Verification
-
+{{ emails|json_script:"emails" }}
{% if ca %}
@@ -70,13 +70,25 @@
Initialize Contact Verification
{% endif %}
{% for field in form %}
+
+ {% if field.name == "email" %}
+
{% trans field.label %}{% if field.field.required %} * {% endif %}
+
+ {% if field.errors %}
{{ field.errors }} {% endif %}
+
+ {% else %}
+
+
+ {% endif %}
+
{% endfor %}
diff --git a/vince/templates/vince/newcontact.html b/vince/templates/vince/newcontact.html
index a28607b..9f402b2 100644
--- a/vince/templates/vince/newcontact.html
+++ b/vince/templates/vince/newcontact.html
@@ -1,413 +1,71 @@
-{% extends VINCETRACK_BASE_TEMPLATE %}
+{% extends "vince/modal.html" %}
+{% load i18n widget_tweaks %}
{% load staticfiles %}
-{% block js %}
-{{ block.super }}
-
-
-{% endblock %}
{% block content %}
-{% load widget_tweaks %}
-
-
-
-{% if error %}
-
-{% endif %}
-{% if email_error %}
-
-
-
- ERROR: Check required email fields.
-
-
-
-{% endif %}
-
-
- {% for field in form %}
- {% if field.errors %}
-
-
Please fix the following error:
- {{ field.label }} {{ field.errors }}
-
- {% endif %}
- {% endfor %}
-
-
-
- {% csrf_token %}
-
@@ -18,6 +19,7 @@
{% if u|groupadmin:object.id %} admin - all cases {% elif groupcontact.default_access %} user - all cases {% else %} {% if admin %}user - select {% else %} user {% endif %} {% with my_cases=caseaccess|case_access:u.id %} ({{ my_cases|length }} cases) {% endwith %}{% endif %}
{{ u.date_joined|date:"Y-m-d" }}
{{ u.last_login|date:"Y-m-d, G:i" }}
+
{% if u|notify_emails:object.id %}Enabled {% else %}Disabled {% endif %}
{% if admin %}
{% if user == u %} {% else %}
{% if u|groupadmin:object.id %}
@@ -38,6 +40,7 @@
Eligible*
+
{% if u|notify_emails:object.id %}Enabled {% else %}Disabled {% endif %}
{% if user.username != u.email %}
@@ -52,6 +55,7 @@
Invited**
+
{% if u|notify_emails:object.id %}Enabled {% else %}Disabled {% endif %}
{% if user.username != u.email %}
{% else %}
diff --git a/vinny/templates/vinny/confirm_email_change.html b/vinny/templates/vinny/confirm_email_change.html
new file mode 100644
index 0000000..956e26f
--- /dev/null
+++ b/vinny/templates/vinny/confirm_email_change.html
@@ -0,0 +1,28 @@
+{% load i18n %}
+{% block content %}
+
+
+
+
+
{% if enable %}
+ Are you sure you want to enable email notifications? By enabling, this email will receive all new case notifications and other important VINCE information.
+ {% elif disable %}
+ Are you sure you want to disable email notifications? By disabling email notifications, this user will not be notified of new cases and VINCE will automatically mute case notifications. You can unmute specific case notifications in each individual case discussion.
+ {% endif %}
+
+
+
+
+
+ ×
+
+
+{% endblock %}
diff --git a/vinny/templates/vinny/dashboard.html b/vinny/templates/vinny/dashboard.html
index 90d4699..2dec89d 100644
--- a/vinny/templates/vinny/dashboard.html
+++ b/vinny/templates/vinny/dashboard.html
@@ -19,19 +19,12 @@
Dashboard
-
-
- {% for message in messages %}
-
{{ message }}
- {% endfor %}
-
-
-
-
+
+ {% csrf_token %}
- {% if post.author != user %}
-
- Reply
-
- {% endif %}
{% if post.num_replies > 0 %}
{{ post.num_replies }} replies
diff --git a/vinny/templates/vinny/searchresults.html b/vinny/templates/vinny/searchresults.html
index a5ff6a7..fe82ef8 100644
--- a/vinny/templates/vinny/searchresults.html
+++ b/vinny/templates/vinny/searchresults.html
@@ -18,10 +18,7 @@
{{ ticket.get_title }} {% autoescape off %}{{ ticket.get_status_html }}{% endautoescape %}
-
Last updated {{ ticket.modified|naturaltime }}
- {% if note.dateupdated != note.datefirstpublished %}
-
Updated {{ note.dateupdated|date:"F d, Y" }}
- {% endif %}
+
Last updated {% if ticket.last_post_date %}{{ ticket.last_post_date|naturaltime }}{% else %}{{ ticket.modified|naturaltime }}{% endif %}
{% endfor %}
diff --git a/vinny/templatetags/user_tags.py b/vinny/templatetags/user_tags.py
index d28fe86..e8f9e02 100644
--- a/vinny/templatetags/user_tags.py
+++ b/vinny/templatetags/user_tags.py
@@ -53,3 +53,21 @@ def groupadmin(user, contact):
def case_access(cases, user):
cases = cases.filter(user=user).values_list('casemember__case__vuid', flat=True)
return list(cases)
+
+
+@register.filter
+def notify_emails(user, contact):
+ try:
+ from vinny.models import VinceCommEmail
+ except ImportError:
+ if settings.DEBUG:
+ raise template.TemplateSyntaxError("Error in template tags: Can't load VinceCommEmail.")
+ email = VinceCommEmail.objects.filter(email=user.email, contact=contact).first()
+ if email:
+ if email.email_function in ["TO", "CC"]:
+ return True
+ else:
+ return False
+ return False
+
+
diff --git a/vinny/urls.py b/vinny/urls.py
index fd92735..08434b6 100644
--- a/vinny/urls.py
+++ b/vinny/urls.py
@@ -70,6 +70,7 @@
re_path('^groupadmin/service/create/(?P\d+)/$', views.CreateServiceAccountView.as_view(), name='createservice'),
re_path('groupadmin/(?P\d+)/adduser/', views.AdminAddUserView.as_view(), name='adduser'),
re_path('^groupadmin/(?P\d+)/rmuser/(?P(contact|user))/(?P\d+)/$', views.AdminRemoveUser.as_view(), name='rmuser'),
+ re_path('^groupadmin/(?P\d+)/email/modify/(?P(email|user))/(?P\d+)/$', views.ModifyEmailNotifications.as_view(), name='changeemail'),
path('inbox/', views.InboxView.as_view(), name='inbox'),
re_path('^inbox/(?P(sent))/$', views.InboxView.as_view(), name='inbox'),
path('inbox/filter/', views.SearchThreadsView.as_view(), name='filterthreads'),
diff --git a/vinny/views.py b/vinny/views.py
index 1a8b372..edea7be 100644
--- a/vinny/views.py
+++ b/vinny/views.py
@@ -38,6 +38,7 @@
from django.core.exceptions import ValidationError, PermissionDenied
from django.utils.translation import ugettext as _
from django.utils import timezone
+from django.db.models import Case as DBCase
import pytz
import difflib
import json
@@ -79,7 +80,7 @@
import html
import re
from itertools import chain
-from django.db.models import Q
+from django.db.models import Q, OuterRef, Subquery, Max
from botocore.exceptions import ClientError
from botocore.client import Config
@@ -184,7 +185,7 @@ def _my_cases(user):
return Case.objects.filter(id__in=my_cases)
def _my_active_cases(user):
- return _my_cases(user).filter(status=Case.ACTIVE_STATUS).order_by('-modified')
+ return _my_cases(user).filter(status=Case.ACTIVE_STATUS)
def _my_reports(user):
@@ -239,8 +240,8 @@ def _my_group_id_for_case(user, case):
def _my_contact_group(user):
- admin_groups = VinceCommGroupAdmin.objects.filter(email__email=user.email).values_list('contact__id', flat=True)
- groups = user.groups.filter(groupcontact__contact__vendor_type="Vendor", groupcontact__contact__in=admin_groups).exclude(groupcontact__isnull=True)
+ admin_groups = VinceCommGroupAdmin.objects.filter(email__email=user.email, contact__active=True).values_list('contact__id', flat=True)
+ groups = user.groups.filter(groupcontact__contact__vendor_type__in=["Vendor", "Coordinator"], groupcontact__contact__in=admin_groups).exclude(groupcontact__isnull=True)
my_groups = []
for ug in groups:
my_groups.append(ug.groupcontact.contact)
@@ -612,7 +613,7 @@ def dispatch(self, request, *args, **kwargs):
def get_context_data(self, **kwargs):
context = super(DashboardView, self).get_context_data(**kwargs)
context['unread_msg_count'] = _unread_msg_count(self.request.user)
- context['dashboard']="yes"
+ context['dashboard'] = 'yes'
if settings.DEBUG:
context['devmode'] = True
@@ -632,25 +633,52 @@ def get_context_data(self, **kwargs):
form = CaseRoleForm()
form.fields['owner'].choices = [
(u.id, u.vinceprofile.vince_username) for u in assignable_users]
+
context['form'] = form
- context['cases'] = _my_active_cases(self.request.user)
+ my_cases = _my_active_cases(self.request.user)
+ cases = my_cases.annotate(last_post_date=Max('post__created')).exclude(last_post_date__isnull=True).order_by('-last_post_date')
+ cases_no_posts = my_cases.exclude(id__in=cases).order_by('-modified')
+ context['cases'] = chain(cases, cases_no_posts)
context['pending'] = VTCaseRequest.objects.filter(user=self.request.user, status=0).order_by('-date_submitted')
return context
last_login = self.request.session.get("LAST_LOGIN")
my_cases = _get_my_cases(self.request.user)
- cases = my_cases.order_by('-modified')
+ my_cases = my_cases.filter(status=Case.ACTIVE_STATUS)
+ #get posts in those cases
+ unseen_cases = []
+ context['new_posts'] = 0
+ # build tuple: case, last modified (first post, if no post, details)
+ cases = my_cases.annotate(last_post_date=Max('post__created')).exclude(last_post_date__isnull=True).order_by('-last_post_date')
+ context['num_published'] = my_cases.filter(note__datefirstpublished__isnull=False).count()
+ cases_no_posts = my_cases.exclude(id__in=cases).order_by('-modified')
+ context['cases'] = list(chain(cases, cases_no_posts))
+ context['num_new_cases'] = 0
+
+ for case in my_cases:
+ logger.debug(case)
+ last_post = Post.objects.filter(case=case).order_by('-modified')
+ last_viewed = CaseViewed.objects.filter(user=self.request.user, case=case).first()
+ if last_post and last_viewed:
+ #is there a new post since last viewed?
+ posts = last_post.filter(created__gt=last_viewed.date_viewed)
+ if posts:
+ unseen_cases.append(case.id)
+ context['new_posts'] += posts.count()
+ elif last_viewed == None and last_login != "New":
+ #this user hasn't viewed this case yet
+ context['num_new_cases'] += 1
+ context['new_posts'] += last_post.count()
+ unseen_cases.append(case.id)
+
+ context['unseen_cases'] = unseen_cases
if last_login == "New":
- context['num_new_cases'] = len(cases)
+ context['num_new_cases'] = len(my_cases)
context['new_user'] = True
elif last_login:
context['last_login'] = parse(last_login)
- viewed_cases = CaseViewed.objects.filter(user=self.request.user).values_list('case__id', flat=True)
- context['num_new_cases'] = my_cases.filter(created__gte=context['last_login']).exclude(id__in=viewed_cases).count()
- context['total_cases'] = len(cases)
context['pending'] = VTCaseRequest.objects.filter(user=self.request.user, status=0).order_by('-date_submitted')
- context['cases'] = cases.filter(status=Case.ACTIVE_STATUS)
return context
@@ -1092,7 +1120,7 @@ class AdminView(LoginRequiredMixin, TokenMixin, UserPassesTestMixin, generic.Tem
login_url = "vinny:login"
def test_func(self):
- ga_groups = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.email)
+ ga_groups = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.email, contact__vendor_type__in=["Coordinator", "Vendor"], contact__active=True)
if len(ga_groups) > 0:
if self.kwargs.get('vendor_id'):
admin = VinceCommGroupAdmin.objects.filter(contact__id=self.kwargs.get('vendor_id'), email__email=self.request.user.email).first()
@@ -1105,7 +1133,7 @@ def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
if is_in_group_vincegroupadmin(self.request.user):
- ga_groups = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.email)
+ ga_groups = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.email, contact__vendor_type__in=["Coordinator", "Vendor"], contact__active=True)
if len(ga_groups) > 1:
return redirect("vinny:multiple_admins")
return super().dispatch(request, *args, **kwargs)
@@ -1122,6 +1150,7 @@ def get_context_data(self, **kwargs):
context['users'] = _users_in_group(admin.contact)
context['object'] = admin.contact
context['vendor_name'] = admin.contact.vendor_name
+ context['notification_emails'] = VinceCommEmail.objects.filter(contact=context['object'], email_list=True)
if context['users']:
emaillist = context['users'].values_list('username', flat=True)
context['invited_users'] = VinceCommEmail.objects.filter(invited=True, contact=context['object']).exclude(email__in=emaillist)
@@ -1135,23 +1164,27 @@ def get_context_data(self, **kwargs):
context['groupcontact'] = GroupContact.objects.filter(contact__id=self.kwargs.get('vendor_id')).first()
cases = _cases_for_group(Group.objects.filter(groupcontact=context['groupcontact']).first())
context['caseaccess'] = CaseMemberUserAccess.objects.filter(casemember__case__in=cases)
- context['users'] = _users_in_group(context['groupcontact'].contact)
- context['object'] = context['groupcontact'].contact
- context['vendor_name'] = context['object'].vendor_name
- if context['users']:
- emaillist = context['users'].values_list('username', flat=True)
- context['invited_users'] = VinceCommEmail.objects.filter(invited=True, contact=context['object']).exclude(email__in=emaillist)
- context['eligible_users'] = VinceCommEmail.objects.filter(invited=False, contact=context['object'],email_list=False, status=True).exclude(email__in=emaillist)
- #get service account
- context['service'] = User.objects.filter(groups__in=[context['groupcontact'].group], vinceprofile__service=True).first()
+ if context['groupcontact']:
+ context['users'] = _users_in_group(context['groupcontact'].contact)
+ context['object'] = context['groupcontact'].contact
+
+ context['vendor_name'] = context['object'].vendor_name
+ context['notification_emails'] = VinceCommEmail.objects.filter(contact=context['object'], email_list=True)
+ if context['users']:
+ emaillist = context['users'].values_list('username', flat=True)
+ context['invited_users'] = VinceCommEmail.objects.filter(invited=True, contact=context['object']).exclude(email__in=emaillist)
+ context['eligible_users'] = VinceCommEmail.objects.filter(invited=False, contact=context['object'],email_list=False, status=True).exclude(email__in=emaillist)
+ #get service account
+ context['service'] = User.objects.filter(groups__in=[context['groupcontact'].group], vinceprofile__service=True).first()
elif is_in_group_vincegroupadmin(self.request.user):
- ga_group = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.username).first()
+ ga_group = VinceCommGroupAdmin.objects.filter(email__email=self.request.user.username, contact__vendor_type__in=["Coordinator", "Vendor"], contact__active=True).first()
if ga_group:
context['admin'] = True
context['users'] = _users_in_group(ga_group.contact)
context['object'] = ga_group.contact
context['vendor_name'] = ga_group.contact.vendor_name
+ context['notification_emails'] = VinceCommEmail.objects.filter(contact=context['object'], email_list=True)
if context['users']:
emaillist = context['users'].values_list('username', flat=True)
context['invited_users'] = VinceCommEmail.objects.filter(invited=True, contact=context['object']).exclude(email__in=emaillist)
@@ -1292,7 +1325,9 @@ def post(self, request, *args, **kwargs):
# check if email already exists
old_email = VinceCommEmail.objects.filter(email__iexact=users, contact=my_group).first()
if old_email:
- if old_email.invited:
+ if old_email.email_list:
+ return JsonResponse({'response':'This email has already been added as notification-only email and cannot be used for user access.'}, status=200)
+ elif old_email.invited:
if old_user:
send_templated_mail("vincecomm_add_existing_user", context, [users])
return JsonResponse({'response': 'success'}, status=200)
@@ -1301,9 +1336,9 @@ def post(self, request, *args, **kwargs):
else:
# send email
if old_user:
- # send a different email
- send_templated_mail("vincecomm_add_existing_user", context, [users])
+ return JsonResponse({'response': 'User already exists.'}, status=200)
else:
+ # actually send the invitation email to a user
send_templated_mail("vincecomm_add_user", context, [users])
old_email.invited=True
create_action("f{self.request.user.vinceprofile.vince_username} invited new user {users}", self.request.user)
@@ -1330,6 +1365,56 @@ def post(self, request, *args, **kwargs):
return JsonResponse({'response': 'success'}, status=200)
+class ModifyEmailNotifications(LoginRequiredMixin, TokenMixin, UserPassesTestMixin, generic.TemplateView):
+ login_url = "vinny:login"
+ template_name = "vinny/confirm_email_change.html"
+
+ def test_func(self):
+ if (is_in_group_vincegroupadmin(self.request.user) and PendingTestMixin.test_func(self)):
+ vendor_id = self.kwargs.get('vendor_id')
+ admin = VinceCommGroupAdmin.objects.filter(contact__id=vendor_id, email__email=self.request.user.email).first()
+ if admin:
+ return True
+ return False
+
+ def post(self, request, *args, **kwargs):
+ logger.debug(request.POST)
+ email = get_object_or_404(VinceCommEmail, id=self.kwargs.get('uid'))
+ if email.email_function in ["TO", "CC"]:
+ email.email_function = "EMAIL"
+ email.save()
+ change = contact_update.create_contact_change(email.contact, email.email, "email notifications", "Enabled", "Disabled", self.request.user)
+ else:
+ email.email_function = "TO"
+ email.save()
+ change = contact_update.create_contact_change(email.contact, email.email, "email notifications", "Disabled", "Enabled", self.request.user)
+
+ contact_update.send_ticket([change], email.contact, self.request.user)
+ messages.success(
+ self.request,
+ "Got it! Your preferences have been saved!"
+ )
+ return redirect("vinny:admin", email.contact.id)
+
+ def get_context_data(self, **kwargs):
+ context = super(ModifyEmailNotifications, self).get_context_data(**kwargs)
+ context['unread_msg_count'] = _unread_msg_count(self.request.user)
+ context['adminpage']=1
+ context['type'] = self.kwargs.get('type')
+ if self.kwargs.get('type') == "user":
+ context['user'] = get_object_or_404(User, id=self.kwargs.get('uid'))
+ context['email'] = VinceCommEmail.objects.filter(email=context['user'].email, contact=self.kwargs.get('vendor_id')).first()
+ elif self.kwargs.get('type') == 'email':
+ context['email'] = get_object_or_404(VinceCommEmail, id=self.kwargs.get('uid'))
+
+ if context['email'].email_function in ["TO", "CC"]:
+ context['disable'] = 1
+ else:
+ context['enable'] = 1
+
+ context['post_url'] = reverse("vinny:changeemail", args=[self.kwargs.get('vendor_id'), 'email', context['email'].id])
+ return context
+
class MultipleStatusView(LoginRequiredMixin, TokenMixin, UserPassesTestMixin, generic.TemplateView):
template_name = "vinny/multi_status.html"
login_url = "vinny:login"
@@ -1389,7 +1474,7 @@ def test_func(self):
def get_context_data(self, **kwargs):
context = super(MultipleGroupAdminView, self).get_context_data(**kwargs)
context['unread_msg_count'] = _unread_msg_count(self.request.user)
- context['groups'] = VinceCommGroupAdmin.objects.filter(email__email=self.request.user)
+ context['groups'] = VinceCommGroupAdmin.objects.filter(email__email=self.request.user, contact__vendor_type__in=["Coordinator", "Vendor"], contact__active=True)
context['adminview']=1
return context
@@ -1603,14 +1688,15 @@ def post(self, request, *args, **kwargs):
coordinators = CaseCoordinator.objects.filter(assigned__id__in=owner_list).values_list('case', flat=True)
res = res.filter(id__in=coordinators)
- if self.request.POST['keyword'] != '':
- wordSearch = process_query(self.request.POST['keyword'])
- res = res.extra(where=["search_vector @@ (to_tsquery('english', %s))=true"],params=[wordSearch])
- #search posts
- post_result = PostRevision.objects.filter(post__case__in=my_cases).extra(where=["search_vector @@ (to_tsquery('english', %s))=true"], params=[wordSearch]).values_list('post__case', flat=True)
- if post_result:
- extra_cases = Case.objects.filter(id__in=post_result).exclude(id__in=res)
- res = list(chain(res, extra_cases))
+ if self.request.POST.get('keyword'):
+ if self.request.POST['keyword'] != "":
+ wordSearch = process_query(self.request.POST['keyword'])
+ res = res.extra(where=["search_vector @@ (to_tsquery('english', %s))=true"],params=[wordSearch])
+ #search posts
+ post_result = PostRevision.objects.filter(post__case__in=my_cases).extra(where=["search_vector @@ (to_tsquery('english', %s))=true"], params=[wordSearch]).values_list('post__case', flat=True)
+ if post_result:
+ extra_cases = Case.objects.filter(id__in=post_result).exclude(id__in=res)
+ res = list(chain(res, extra_cases))
paginator = Paginator(res, 10)
return render(request, self.template_name, {'cases': paginator.page(page), 'total': len(res)})
@@ -3044,14 +3130,21 @@ def get_queryset(self):
if cognito_admin_user(self.request):
return Case.objects.all().order_by('-modified')
my_cases = _get_my_cases(self.request.user)
- return my_cases.order_by('-modified')
-
+ my_cases = my_cases.annotate(last_post_date=Max('post__created')).order_by('-last_post_date')
+ # sort by posts/no posts
+ cp = my_cases.exclude(last_post_date__isnull=True)
+ cnop= my_cases.exclude(last_post_date__isnull=False)
+ res = list(chain(cp, cnop))
+ return res
+
+
def post(self, request, *args, **kwargs):
logger.debug(f"{self.__class__.__name__} post: {self.request.POST}")
if cognito_admin_user(self.request):
- res = Case.objects.all().order_by('-modified')
+ res = Case.objects.all().annotate(last_post_date=Max('post__created')).order_by('-last_post_date')
else:
- res = self.get_queryset()
+ res = _get_my_cases(self.request.user)
+ res = res.annotate(last_post_date=Max('post__created')).order_by('-last_post_date')
my_cases = res
page = self.request.POST.get('page', 1)
@@ -3080,6 +3173,16 @@ def post(self, request, *args, **kwargs):
if post_result:
extra_cases = Case.objects.filter(id__in=post_result).exclude(id__in=res)
res = list(chain(res, extra_cases))
+ else:
+ # sort by posts/no posts
+ cp = res.exclude(last_post_date__isnull=True)
+ cnop=res.exclude(last_post_date__isnull=False)
+ res = list(chain(cp, cnop))
+ else:
+ # sort by posts/no posts
+ cp = res.exclude(last_post_date__isnull=True)
+ cnop=res.exclude(last_post_date__isnull=False)
+ res = list(chain(cp, cnop))
paginator = Paginator(res, 10)