mentions added for all modules comments

This commit is contained in:
Ashwin 2019-02-02 15:13:14 +05:30
parent 1a84d1ca22
commit 918c196ba0
19 changed files with 656 additions and 160 deletions

View file

@ -12,6 +12,8 @@
</nav>
<!-- breadcrumb ends here -->
{% endblock breadcrumb %}
{% block content %}
<!-- ACCOUNTS VIEW STARTS -->
<div class="main_container" id="maincontainer">
@ -1833,6 +1835,7 @@
<!-- ACCOUNTS VIEW ENDS -->
{% endblock content %}
{% block js_block %}
<script type="text/javascript">
$("#form_opp").submit(function(e) {
e.preventDefault();
@ -2022,6 +2025,16 @@
});
</script>
<script type="text/javascript">
var assigned_to_list = "{{ assigned_data }}";
assigned_to_list = JSON.parse(assigned_to_list.replace(/&(l|g|quo)t;/g, function(a,b){
return {
l : '<',
g : '>',
quo : '"'
}[b];
}));
/*Comment Add Js Start*/
$("#comment_form").submit(function(e){
e.preventDefault()
@ -2164,14 +2177,13 @@ $("#attachment_form").submit(function (e) {
)
$("#AttachmentError").html("")
$("#id_attachments").val("")
alert("Attachment Saved")
}
$("#id_attachments").val("")
alert("Attachment Saved")
}
})
})
</script>
<script src="{% static 'js/mention.js' %}"></script>
</script>
{% endblock js_block %}

View file

@ -1,3 +1,4 @@
import json
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
@ -152,6 +153,14 @@ class AccountDetailView(LoginRequiredMixin, DetailView):
comment_permission = True
else:
comment_permission = False
assigned_data = []
for each in context['account_record'].assigned_to.all():
assigned_dict = {}
assigned_dict['id'] = each.id
assigned_dict['name'] = each.email
assigned_data.append(assigned_dict)
context.update({
"comments": account_record.accounts_comments.all(),
"attachments": account_record.account_attachment.all(),
@ -168,6 +177,7 @@ class AccountDetailView(LoginRequiredMixin, DetailView):
"case_priority": PRIORITY_CHOICE,
"case_status": STATUS_CHOICE,
'comment_permission': comment_permission,
"assigned_data": json.dumps(assigned_data)
})
return context

View file

@ -116,7 +116,6 @@
<tr class="case_row{{case.id}}" style="text-align:center;">
<td scope="row">{{ forloop.counter }}</td>
<td><a href="{% url 'cases:view_case' case.id %}">{{ case.name }}</a></td>
<td>{{case.closed_on}}</td>
<td>
{% if case.account %}
<a href="{% url 'accounts:view_account' case.account.id %}">{{ case.account }}</a>

View file

@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% block content %}
<!-- CASE VIEW STARTS -->
<div class="main_container" id="maincontainer">
@ -118,17 +119,17 @@
<input type="hidden" value="{{case_record.id}}" name="caseid">
</form>
<ul class="list-group" id="attachment_div">
{% for attachemnt in attachments %}
<li class="list-group-item list-row" id="attachment{{attachemnt.id}}">
{% for attachment in attachments %}
<li class="list-group-item list-row" id="attachment{{attachment.id}}">
<div class="float-right right-container">
<div class="list-row-buttons btn-group float-right">
<button class="action btn btn-danger" onclick="remove_attachment({{attachemnt.id}})">Remove</button>
<button class="action btn btn-danger" onclick="remove_attachment({{attachment.id}})">Remove</button>
</div>
</div>
<div class="stream-post-container" id="attachment_name{{attachemnt.id}}"><pre><a href="{{ attachemnt.attachment.url }}">{{ attachemnt.file_name }}</a></pre></div>
<div class="stream-post-container" id="attachment_name{{attachment.id}}"><pre><a href="{{ attachment.attachment.url }}">{{ attachment.file_name }}</a></pre></div>
<div class="stream-container">
<pre class="float-left">{{ attachemnt.created_by }}</pre>
<pre class="float-right">{{ attachemnt.created_on }}</pre>
<pre class="float-left">{{ attachment.created_by }}</pre>
<pre class="float-right">{{ attachment.created_on }}</pre>
</div>
</li>
{% endfor %}
@ -149,7 +150,7 @@
<textarea class="form-control" textarea cols="40" rows="3" id="id_comments" name="comment" placeholder="Submit Your Comments Here"></textarea><div><i id="CommentError" style="display:none; color:red"></i></div>
</br>
<div class="buttons_row">
<button class="btn btn-default save" style="text-align: center;" id="comment_submit" type="submit">Submit</button>
<button class="btn btn-default save" id="comment_submit" type="submit">Submit</button>
</div>
</div>
<input type="hidden" value="{{case_record.id}}" name="caseid">
@ -345,6 +346,17 @@
<script src="{% static 'js/cases/view_case.js' %}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.6-rc.0/js/select2.min.js"></script>
<script type="text/javascript">
var assigned_to_list = "{{ assigned_data }}";
assigned_to_list = JSON.parse(assigned_to_list.replace(/&(l|g|quo)t;/g, function(a,b){
return {
l : '<',
g : '>',
quo : '"'
}[b];
}));
$('#remove_case').click(function(e){
var result = confirm("Are You Sure You Want to delete?");
if (result==false) {
@ -389,10 +401,9 @@ function remove_attachment(x) {
"attachment_id": x
}, function (data) {
if (data.error) {
alert(data.error)
alert(data.errors)
} else {
// $("#attachment" + data.aid).remove()
window.location.reload();
$("#attachment" + data.attachment_object).remove()
}
})
}
@ -410,16 +421,23 @@ $("#attachment_form").submit(function (e) {
processData: false,
success: function (data) {
if (data.error) {
$("#AttachmentError").html(data.error).show()
$("#AttachmentError").html(data.errors).show()
} else {
alert(data.message)
$("#attachment_div").prepend(
"<li class='list-group-item list-row' id='attachment" + data.attachment_id + "'>" +
"<div class='float-right right-container'><div class='list-row-buttons btn-group float-right'><button class='action btn btn-danger' onclick='remove_attachment("+data.attachment_id+")''>Remove</button></div></div>" +
"<div class='stream-post-container' id='comment_name"+data.attachment_id+"'><pre><a href='"+data.attachment_url+"'>"+data.attachment+"</a></pre></div>"+
"<div class='stream-container'><pre class='float-left'>"+data.created_by+"</pre><pre class='float-right'>"+data.created_on+"</pre></div>" +
"</li>"
)
$("#AttachmentError").html("")
$("#id_attachments").val("")
window.location.reload();
}
}
})
})
</script>
</script>
<script src="{% static 'js/mention.js' %}"></script>
{% endblock js_block %}

View file

@ -1,3 +1,4 @@
import json
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
@ -143,8 +144,16 @@ class CaseDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super(CaseDetailView, self).get_context_data(**kwargs)
assigned_data = []
for each in context['case_record'].assigned_to.all():
assigned_dict = {}
assigned_dict['id'] = each.id
assigned_dict['name'] = each.email
assigned_data.append(assigned_dict)
context.update({"comments": context["case_record"].cases.all(),
"attachments": context['case_record'].case_attachment.all()})
"attachments": context['case_record'].case_attachment.all(),
"assigned_data": json.dumps(assigned_data)})
return context
@ -292,7 +301,8 @@ class AddCommentView(LoginRequiredMixin, CreateView):
self.case = get_object_or_404(Case, id=request.POST.get('caseid'))
if (
request.user in self.case.assigned_to.all() or
request.user == self.case.created_by
request.user == self.case.created_by or request.user.is_superuser or
request.user.role == 'admin'
):
form = self.get_form()
if form.is_valid():
@ -323,7 +333,8 @@ class UpdateCommentView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
self.comment_obj = get_object_or_404(Comment, id=request.POST.get("commentid"))
if request.user == self.comment_obj.commented_by:
if (request.user == self.comment_obj.commented_by or request.user.is_superuser or
request.user.role == 'admin'):
form = CaseCommentForm(request.POST, instance=self.comment_obj)
if form.is_valid():
return self.form_valid(form)
@ -349,7 +360,8 @@ class DeleteCommentView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(Comment, id=request.POST.get("comment_id"))
if request.user == self.object.commented_by:
if (request.user == self.object.commented_by or request.user.is_superuser or
request.user.role == 'admin'):
self.object.delete()
data = {"cid": request.POST.get("comment_id")}
return JsonResponse(data)
@ -368,7 +380,8 @@ class AddAttachmentView(LoginRequiredMixin, CreateView):
self.case = get_object_or_404(Case, id=request.POST.get('caseid'))
if (
request.user in self.case.assigned_to.all() or
request.user == self.case.created_by
request.user == self.case.created_by or request.user.is_superuser or
request.user.role == 'admin'
):
form = self.get_form()
if form.is_valid():
@ -376,7 +389,7 @@ class AddAttachmentView(LoginRequiredMixin, CreateView):
else:
return self.form_invalid(form)
else:
data = {'error': "You don't have permission to add attachment for this case."}
data = {"error":True, "errors": "You don't have permission to add attachment for this case."}
return JsonResponse(data)
def form_valid(self, form):
@ -385,22 +398,31 @@ class AddAttachmentView(LoginRequiredMixin, CreateView):
attachment.file_name = attachment.attachment.name
attachment.case = self.case
attachment.save()
# return JsonResponse({"error": False, "message": "Attachment Saved"
# })
return JsonResponse({
"message": "Attachment Saved"
"attachment_id": attachment.id,
"attachment": attachment.file_name,
"attachment_url": attachment.attachment.url,
"created_on": attachment.created_on,
"created_by": attachment.created_by.email,
"message": "attachment Created",
"error": False
})
def form_invalid(self, form):
return JsonResponse({"error": form['attachment'].errors})
return JsonResponse({"error":True, "errors": form['attachment'].errors})
class DeleteAttachmentsView(LoginRequiredMixin, View):
def post(self, request, *args, **kwargs):
self.object = get_object_or_404(Attachments, id=request.POST.get("attachment_id"))
if request.user == self.object.created_by:
if (request.user == self.object.created_by or request.user.is_superuser or
request.user.role == 'admin'):
self.object.delete()
data = {"acd": request.POST.get("attachment_id")}
data = {"attachment_object": request.POST.get("attachment_id"), "error": False}
return JsonResponse(data)
else:
data = {'error': "You don't have permission to delete this attachment."}
data = {"error":True, "errors": "You don't have permission to delete this attachment."}
return JsonResponse(data)

View file

@ -273,4 +273,4 @@ class Document(models.Model):
def __str__(self):
return self.title
return self.title

View file

@ -2,102 +2,112 @@
{% load staticfiles %}
{% load paginate %}
{% block content %}
<!-- main_container starts here -->
<div class="main_container">
<!-- heading_create starts here -->
<div class="row marl heading_create">
<div class="heading col-md-12">
<div class="no-gutters row">
<div class="col-md-6">
</div>
<div class="col-md-6">
<div class="create_new text-right">
<a href="{% url 'common:create_doc'%}"><i class="fa fa-plus"></i> Add New Document</a>
</div>
</div>
<!-- main_container starts here -->
<div class="main_container">
<!-- heading_create starts here -->
<div class="row marl heading_create">
<div class="heading col-md-12">
<div class="no-gutters row">
<div class="col-md-6">
</div>
<div class="col-md-6">
<div class="create_new text-right">
<a href="{% url 'common:create_doc' %}"><i class="fa fa-plus"></i> Add New Document</a>
</div>
</div>
</div>
<!-- heading_create ends here -->
<!-- filter_row starts here -->
<div class="filter_row row marl">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="card-title">Filters</div>
<form id="docs_filter" action="" method="POST">
<div class="row marl">
<div class="filter_col col-4">
<div class="form-group">
<label for="exampleInputEmail1">Document Title</label>
<input type="text" class="form-control" placeholder="Document Title" name="doc_name" value="{{request.POST.doc_name}}">
</div>
</div>
<br clear="all">
</div>
</form>
</div>
</div>
</div>
</div>
<!-- filter_row ends here -->
<div class="table_container_row row marl">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="card-title list">
List of Documents
<span class="float-right total_count">{% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{ documents|length }}{% endif %}</span>
</div>
<div class="table-responsive ">
<table class="table table-bordered table-striped table-condensed">
<thead>
{% if documents|length > 0 %}
<tr>
<th width="5%" style="text-align:center;">S.No</th>
<th width="20%" style="text-align:center;">Document Title</th>
<th width="20%" style="text-align:center;">Created_by</th>
<th width="20%" style="text-align:center;">Actions</th>
</tr>
{% endif %}
</thead>
<tbody>
{% if per_page %}
{% paginate per_page documents %}
{% else %}
{% paginate 10 documents %}
{% endif %}
{% for document in documents %}
<tr class="text-center">
<td scope="row">{{ forloop.counter }}</td>
<td><a href="{% url 'common:view_doc' document.id %}">{{ document.title }}</a></td>
<td>{{ document.created_by }}</td>
<td class="actions">
<a href="{% url 'common:edit_doc' document.id %}" class="btn btn-success edit"><i class="fas fa-pencil-alt"></i>Edit</a>
<a href="{% url 'common:remove_doc' document.id %}" class="btn btn-danger trash"><i class="fas fa-trash-alt"></i>Delete</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% ifequal documents|length 0 %}
<h6 class="text-center">No Documents Found</h6>
{% endifequal %}
<div class="marl row text-center">
{% show_pages %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- main_container ends here -->
<!-- heading_create ends here -->
<!-- filter_row starts here -->
<div class="filter_row row marl">
<div class="col-md-3 col-lg-2 col-xl-2">
<div class="card">
<form id="docs_filter" method="POST" action="">
<div class="card-body">
<div class="card-title">Filters</div>
<div class="row marl">
<div class="filter_col col-md-12">
<div class="form-group">
<label for="exampleInputEmail1">Title</label>
<input type="text" class="form-control" placeholder="Document Title" name="doc_name" {% if request.POST.doc_name %} value="{{request.POST.doc_name}}" {%endif%}/>
</div>
</div>
<div class="filter_col text-center col-12">
<div class="form-group buttons_row">
<button class="btn btn-primary save" type="submit">Search</button>
<a href="{% url 'common:doc_list' %}" class="btn btn-default clear">Clear</a>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="col-md-9 col-lg-10 col-xl-10">
<div class="table_container_row row marl no-gutters">
<div class="col-md-12">
<div class="card">
<div class="card-body">
<div class="panel-heading-list card-title">
List of Documents
<span class="float-right total_count">{% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{documents|length}}{% endif %}</span>
</div>
<div class="table-responsive">
<table class="table ">
<thead>
{% if documents|length > 0 %}
<tr>
<th>S.No</th>
<th>Title</th>
<th>Created By</th>
<th>Actions</th>
</tr>
{% endif %}
</thead>
<tbody>
{% if per_page %}
{% paginate per_page documents %}
{% else%}
{% paginate 10 documents %}
{% endif %}
{% for document in documents %}
<tr id="account_details{{each_account.id}}" class="text-center">
<td scope="row">{{ forloop.counter }}</td>
<td>{% if document.title %}<a href="{% url 'common:view_doc' document.id %}">{{ document.title }}</a>{% endif %}</td>
<td>{{document.created_by}}</td>
<td class="actions">
<a href="{% url 'common:edit_doc' document.id %}" class="btn btn-success edit"><i class="fas fa-pencil-alt"></i></a>
<a href="{% url 'common:remove_doc' document.id %}" class="btn btn-danger delete remove_document"><i class="fas fa-trash-alt"></i></a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% ifequal documents|length 0 %}
<h6 class="text-center">No Documents Found</h6>
{% endifequal %}
<div class="text-center row marl">
{% show_pages %}
</div>
</div>
</div>
<br clear="all">
</div>
</div>
</div>
</div>
<!-- filter_row ends here -->
<!-- accounts list start -->
<!-- accounts list end -->
</div>
<!-- main_container ends here -->
{% endblock %}
{% block js_block %}
<script type="text/javascript">
$('.trash').click(function(e){
$('.remove_document').click(function(e){
e.preventDefault()
url = $(this).attr('href')
if (!confirm('Are you sure you want to delete?'))
@ -105,10 +115,14 @@
window.location = $(this).attr('href')
});
$(".reset").click(function(e){
window.location = "{% url 'common:doc_list'%}"
});
$("a[rel='page']").click(function(e){
e.preventDefault();
$('#docs_filter').attr("action", $(this).attr("href"));
$('#docs_filter').submit();
});
</script>
{% endblock js_block %}
{% endblock js_block %}

View file

@ -359,4 +359,4 @@ def download_document(request, pk):
response = HttpResponse(fh.read(), content_type="application/vnd.ms-excel")
response['Content-Disposition'] = 'inline; filename=' + os.path.basename(file_path)
return response
raise Http404
raise Http404

View file

@ -30,18 +30,6 @@ class ContactForm(forms.ModelForm):
'assigned_to', 'teams', 'first_name', 'last_name', 'account', 'email', 'phone', 'address', 'description'
)
def clean_phone(self):
client_phone = self.cleaned_data.get('phone', None)
try:
if int(client_phone) and not client_phone.isalpha():
ph_length = str(client_phone)
if len(ph_length) < 10 or len(ph_length) > 13:
raise forms.ValidationError('Phone number must be minimum 10 Digits and maximum of 13 Digits')
except (ValueError):
raise forms.ValidationError('Phone Number should contain only Numbers')
return client_phone
class ContactCommentForm(forms.ModelForm):

View file

@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load staticfiles %}
{% block breadcrumb %}
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
@ -203,12 +204,22 @@
<!-- CONTACTS VIEW ENDS -->
{% endblock%}
{% block js_block %}
<script type="text/javascript">
<script type="text/javascript">
function resetForm() {
$('#CommentEditError').hide()
document.getElementById("CommentEditError").reset();
}
var assigned_to_list = "{{ assigned_data }}";
assigned_to_list = JSON.parse(assigned_to_list.replace(/&(l|g|quo)t;/g, function(a,b){
return {
l : '<',
g : '>',
quo : '"'
}[b];
}));
function resetForm() {
$('#CommentEditError').hide()
document.getElementById("CommentEditError").reset();
}
$("#comment_form").submit(function (e) {
e.preventDefault()
@ -306,10 +317,8 @@ $("#attachment_form").submit(function (e) {
contentType: false,
processData: false,
success: function (data) {
alert("Attachment Saved")
if (data.error) {
$("#AttachmentError").html(data.error).show()
alert("Attachment Saved")
} else {
$("#attachment_div").prepend(
"<li class='list-group-item list-row' id='attachment" + data.attachment_id + "'>" +
@ -348,4 +357,6 @@ function remove_attachment(x) {
});
</script>
<script src="{% static 'js/mention.js' %}"></script>
{% endblock js_block %}

View file

@ -1,3 +1,4 @@
import json
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
@ -144,8 +145,17 @@ class ContactDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super(ContactDetailView, self).get_context_data(**kwargs)
assigned_data = []
for each in context['contact_record'].assigned_to.all():
assigned_dict = {}
assigned_dict['id'] = each.id
assigned_dict['name'] = each.email
assigned_data.append(assigned_dict)
context.update({"comments": context["contact_record"].contact_comments.all(),
'attachments':context["contact_record"].contact_attachment.all()
'attachments':context["contact_record"].contact_attachment.all(),
"assigned_data": json.dumps(assigned_data)
})
return context

View file

@ -37,17 +37,6 @@ class LeadForm(forms.ModelForm):
'phone', 'email', 'status', 'source', 'website', 'address', 'description'
)
def clean_phone(self):
client_phone = self.cleaned_data.get('phone', None)
try:
if int(client_phone) and not client_phone.isalpha():
ph_length = str(client_phone)
if len(ph_length) < 10 or len(ph_length) > 13:
raise forms.ValidationError('Phone number must be minimum 10 Digits and maximum of 13 Digits')
except (ValueError):
raise forms.ValidationError('Phone Number should contain only Numbers')
return client_phone
class LeadCommentForm(forms.ModelForm):

View file

@ -9,6 +9,7 @@
</nav>
{%endblock%}
{% block content %}
<div class="main_container" id="maincontainer">
<div class="overview_form_block row marl justify-content-center">
@ -241,6 +242,16 @@
<script src="{% static 'js/calls.js' %}"></script>
<script src="{% static 'js/meetings.js' %}"></script>
<script type="text/javascript">
var assigned_to_list = "{{ assigned_data }}";
assigned_to_list = JSON.parse(assigned_to_list.replace(/&(l|g|quo)t;/g, function(a,b){
return {
l : '<',
g : '>',
quo : '"'
}[b];
}));
function resetForm() {
$('#CommentEditError').hide()
document.getElementById("CommentEditError").reset();
@ -341,10 +352,8 @@ $("#attachment_form").submit(function (e) {
contentType: false,
processData: false,
success: function (data) {
alert("Attachment Saved")
if (data.error) {
$("#AttachmentError").html(data.error).show()
alert("Attachment Saved")
} else {
$("#attachment_div").prepend(
"<li class='list-group-item list-row' id='attachment" + data.attachment_id + "'>" +
@ -378,4 +387,6 @@ function remove_attachment(x) {
}
</script>
<script src="{% static 'js/mention.js' %}"></script>
{% endblock js_block %}

View file

@ -1,3 +1,4 @@
import json
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
@ -177,9 +178,18 @@ class LeadDetailView(LoginRequiredMixin, DetailView):
'form-INITIAL_FORMS': '0',
'form-MAX_NUM_FORMS': '10',
})
assigned_data = []
for each in context['lead_record'].assigned_to.all():
assigned_dict = {}
assigned_dict['id'] = each.id
assigned_dict['name'] = each.email
assigned_data.append(assigned_dict)
context.update({
"attachments": attachments, "comments": comments, "status": LEAD_STATUS, "countries": COUNTRIES,
"reminder_form_set": reminder_form_set, "meetings": meetings, "calls": calls})
"reminder_form_set": reminder_form_set, "meetings": meetings, "calls": calls,
"assigned_data": json.dumps(assigned_data)})
return context

View file

@ -10,6 +10,7 @@
</nav>
<!-- Breadcrumb Ends Here -->
{% endblock%}
{% block content%}
<!-- OPPORTUNITY VIEW STARTS -->
<div class="main_container" id="maincontainer">
@ -235,6 +236,16 @@
{% endblock %}
{% block js_block %}
<script type="text/javascript">
var assigned_to_list = "{{ assigned_data }}";
assigned_to_list = JSON.parse(assigned_to_list.replace(/&(l|g|quo)t;/g, function(a,b){
return {
l : '<',
g : '>',
quo : '"'
}[b];
}));
$(function () {
$('#datetimepicker1').datetimepicker({
'format': 'YYYY-MM-DD',
@ -358,10 +369,8 @@ $("#attachment_form").submit(function (e) {
contentType: false,
processData: false,
success: function (data) {
alert("Attachment Saved")
if (data.error) {
$("#AttachmentError").html(data.error).show()
alert("Attachment Saved")
} else {
$("#attachment_div").prepend(
"<li class='list-group-item list-row' id='attachment" + data.attachment_id + "'>" +
@ -378,5 +387,9 @@ $("#attachment_form").submit(function (e) {
})
});
</script>
<script src="{% static 'js/mention.js' %}"></script>
{% endblock js_block %}

View file

@ -1,3 +1,4 @@
import json
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.sites.shortcuts import get_current_site
from django.core.mail import EmailMessage
@ -143,8 +144,18 @@ class OpportunityDetailView(LoginRequiredMixin, DetailView):
def get_context_data(self, **kwargs):
context = super(OpportunityDetailView, self).get_context_data(**kwargs)
assigned_data = []
for each in context['opportunity_record'].assigned_to.all():
assigned_dict = {}
assigned_dict['id'] = each.id
assigned_dict['name'] = each.email
assigned_data.append(assigned_dict)
comments = context["opportunity_record"].opportunity_comments.all()
context.update({"comments": comments, 'attachments': context["opportunity_record"].opportunity_attachment.all()})
context.update({
"comments": comments, 'attachments': context["opportunity_record"].opportunity_attachment.all(),
"assigned_data": json.dumps(assigned_data)})
return context

70
static/css/mention.scss Normal file
View file

@ -0,0 +1,70 @@
.mention-wrapper {
position: relative;
display: block;
width: 100%;
height: 100%;
}
.mention-wrapper textarea { color: rgba(0, 0, 0, 0.1); }
.mention-display {
color: rgba(0, 0, 0, 0.9);
position: absolute;
display: inline-block;
box-sizing: border-box;
pointer-events: none;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
border-radius: 4px;
word-break: break-word;
-moz-appearance: textfield-multiline;
-webkit-appearance: textarea;
font: medium -moz-fixed;
font: -webkit-small-control;
border: 3px solid transparent;
padding: 10px;
font-size: 16px;
}
.mention-options {
position: absolute;
pointer-events: none;
opacity: 0;
color: rgba(0, 0, 0, 0.9);
width: 100%;
border: 3px solid transparent;
box-sizing: border-box;
}
.mention-options.show {
pointer-events: initial;
opacity: 1;
z-index: 9999;
border: 2px solid #ddd;
background: #ddd;
}
.mention-options .mention-option {
padding: 5px 10px;
background: #FFF;
position: relative;
cursor: pointer;
display: none;
}
.mention-options .mention-option:hover:after, .mention-options .mention-option.active:after {
content: '';
position: absolute;
top: 0px;
left: 0px;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.1);
pointer-events: none;
cursor: pointer;
}
.mention-options .mention-option.show { display: block; }

306
static/js/mention.js Normal file
View file

@ -0,0 +1,306 @@
class Mention {
/**
* @class Mention
* @classdesc Allows using a symbol to display options
*
* @param {Object} settings - Options to initialize the component with
* @param {HTMLElement} settings.input - The textarea to watch {@link Component#input}
* @param {HTMLElement} settings.optionList - Element to display options when matched {@link Component#optionList}
* @param {Array} settings.options - Array of options. Options can have properties {@link Component#options}
* @param {String} [settings.symbol="@"] - The symbol that initials the option list {@link Component#symbol}
* @param {Function} [settings.match] - The function used to match options {@link Component#match}
* @param {Function} [settings.template] - The template function outputs the innerHTML of the optionlist {@link Component#template}
*/
constructor(settings) {
this.options = settings.options || []
this.input = settings.input
this.reverse = settings.reverse
this.symbol = settings.symbol || '@'
this.cursorPosition = 0
this.hover = 0
this.showingOptions = false
this.upDownStay = 0
this.wordAtCursor = {}
this.update = settings.update || function(){}
this.match = settings.match || this.match
this.template = settings.template || this.template
this.startsWith = settings.startsWith || this.startsWith
this.html = {
input: undefined,
display: undefined,
wrapper: undefined,
optionsList: undefined,
options: [],
spans: [] }
this.setupHTML()
this.listen()
}
/**
* Function used to match options based on the word
* @param {String} [word] - The current word ex. @test
* @param {String} [option] - The options being looped
* @return {boolean} - If the word matches the option
*/
match(word, option) {
var optionText = option.name || option
return optionText.toLowerCase() == word.toLowerCase()
}
/**
* Called to see if option starts with the word
* @param {String} [word] - The current word ex. @test
* @param {String} [option] - The options being looped
* @return {boolean} - If the word starts with the option
*/
startsWith(word, option) {
var optionText = option.name || option
return optionText.toLowerCase().startsWith(word.toLowerCase())
}
/**
* Function returns the template (innerHTML) that will be used for each option
* @param {String} [option] - The options being looped
* @return {String} - The innerHTM
*/
template(option) {
return option.name || option
}
/**
* Sets up the HTML. Wrapper, Display, OptionsList, Options
*/
setupHTML() {
this.html.input = this.input
// Global wrapper
this.html.wrapper = document.createElement('div')
this.html.wrapper.classList.add('mention-wrapper')
this.html.input.parentElement.insertBefore(this.html.wrapper, this.html.input)
this.html.wrapper.appendChild(this.html.input)
// Options
this.html.optionsList = document.createElement('div')
this.html.optionsList.classList.add('mention-options')
this.html.wrapper.appendChild(this.html.optionsList)
if(this.reverse) {
this.html.optionsList.classList.add('mention-options-reverse')
this.html.wrapper.insertBefore(this.html.optionsList, this.html.wrapper.firstChild)
}
for(var option of this.options) {
var optionElement = document.createElement('div')
optionElement.classList.add('mention-option')
optionElement.innerHTML = this.template(option)
optionElement.setAttribute('mentiondata', JSON.stringify(option))
this.html.options.push(optionElement)
this.html.optionsList.appendChild(optionElement)
}
}
/**
* Begins listening for events on the input and options
*/
listen() {
this.html.input.addEventListener('input', () => { this.onEventInput() })
this.html.input.addEventListener('keydown', (e) => { this.onEventKeyDown(e) })
this.html.input.addEventListener('keyup', (e) => { this.onEventKeyUp(e) })
this.html.options.forEach((o) => {
o.addEventListener('click', (e) => { this.onEventOptionClick(e.target) })
})
}
/**
* Called when on input.addEventListener('input')
* @param {Event} e - the event passed
*/
onEventInput() {
this.update()
}
/**
* Called when on input.addEventListener('keyup')
* @param {Event} e - the keyboard event passed
*/
onEventKeyDown(e) {
this.upDownStay = e.keyCode == 40 ? 1 : e.keyCode == 38 ? -1 : 0
if(this.reverse) this.upDownStay *= -1
if(this.upDownStay && this.showingOptions) e.preventDefault()
if(e.keyCode == 13 && this.showingOptions) {
e.preventDefault()
var option = this.html.options.find((e) => e.classList.contains('hover'))
if(option) this.onEventOptionClick(option)
}
}
/**
* Called when on input.addEventListener('keydown')
* @param {Event} e - the event passed
*/
onEventKeyUp() {
this.cursorPositionChanged()
this.setHoverOption()
}
/**
* Called when option input.addEventListener('click')
*/
onEventOptionClick(optionEle) {
var word = this.symbol + JSON.parse(optionEle.getAttribute('mentiondata')).name + ' '
var splitInputValue = this.html.input.value.split('')
splitInputValue.splice(this.wordAtCursor.index, this.wordAtCursor.word.length, word)
this.html.input.value = splitInputValue.join('')
this.html.input.focus()
this.setCursorPosition(this.wordAtCursor.index + word.length + 1)
this.toggleOptionList(false)
this.update()
}
/**
* Cursor position changed. Check for input data and toggle options
*/
cursorPositionChanged() {
this.cursorPosition = this.html.input.selectionStart
this.wordAtCursor = this.readWordAtCursor({ cursorPosition: this.cursorPosition, value: this.input.value })
this.toggleOptionList(this.wordAtCursor.word.length && this.wordAtCursor.word[0] == this.symbol)
this.showHideOptions()
}
/**
* Show/Hide the options list
* @param {Boolean} toggle - show or hide
*/
toggleOptionList(toggle) {
this.html.optionsList.classList.remove('show')
if(toggle) this.html.optionsList.classList.add('show')
this.showingOptions = toggle
}
/**
* Loop the options and show/hide options based on match function
*/
showHideOptions() {
for(var option in this.options) {
var word = this.wordAtCursor.word.replace(this.symbol, '')
this.html.options[option].classList.remove('show')
if(this.startsWith(word, this.options[option])) this.html.options[option].classList.add('show')
}
}
/**
* Using up/down arrow selects the next option
*/
setHoverOption() {
var viewableOptions = this.html.options.filter((e) => {
e.classList.remove('hover')
return e.classList.contains('show')
})
if(!viewableOptions.length){
this.toggleOptionList(false)
return
}
this.hover = this.upDownStay ? this.hover + this.upDownStay : 0
if(this.hover < 0){ this.hover = viewableOptions.length - 1 }
if(this.hover == viewableOptions.length) { this.hover = 0}
viewableOptions[this.hover].classList.add('hover')
}
/**
* Sets the cursor position in the text area
* @param {Number} position - the position
*/
setCursorPosition(position) {
this.cursorPosition = position
this.html.input.setSelectionRange(position, position);
}
/**
* From the cursor positoin looks back to match the work and start/end position
* @param {Object} data - Options to initialize the component with
* @param {String} [data.value] - the string to search through
* @param {Number} [data.cursorPosition] - The position of the cursor in the string
*/
readWordAtCursor(data) {
var word = '', index = data.cursorPosition
var valueWithReplacedSpecial = data.value.replace(/\n/g, ' ');
while(index--){
var previousCharacter = valueWithReplacedSpecial[index]
if(previousCharacter == ' ' || index < 0) break
}
while(index++ < valueWithReplacedSpecial.length-1) {
var nextCharacter = valueWithReplacedSpecial[index]
if(nextCharacter == ' ') break
word += nextCharacter
}
return { index: Math.max(index-word.length, 0), word: word }
}
/**
* Loops over the input value.
* @return {matches[]} - Array of matches { word: word, index: index word is at}
*/
findMatches() {
var inputValue = this.html.input.value.split('').concat([' '])
var words = []
var currentWord = ''
for(var index in inputValue) {
var letter = inputValue[index]
var lastLetter = inputValue[index-1] || ' '
var lastLetterIsSpace = [' ', '\\n'].indexOf(lastLetter) > -1 || lastLetter.charCodeAt(0) == 10
var canStartWord = letter == this.symbol && lastLetterIsSpace
if((canStartWord || currentWord.length) && letter != ' ') currentWord += letter
if(currentWord.length && letter == ' '){
words.push({ word: currentWord, index: Math.max(index-currentWord.length, 0) })
currentWord = ''
}
}
return words;
}
/**
* Collects all the real matches
*/
collect() {
return this.findMatches().filter((word) => {
return this.options.some((option) => {
return this.match(word.word.replace(this.symbol, ''), option)
})
})
}
}
if(typeof module != 'undefined') module.exports = Mention
var myMention = new Mention({
input: document.querySelector('#id_comments'),
options: assigned_to_list,
template: function(option) {
return '@' + option.name
}
})
var myMention = new Mention({
input: document.querySelector('#id_editcomment'),
options: assigned_to_list,
// match: function(word, option) {
// return option.name.startsWith(word)
// || option.description.toLowerCase().indexOf(word.toLowerCase()) >= 0
// },
template: function(option) {
return '@' + option.name
}
})

View file

@ -14,6 +14,8 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datetimepicker/4.17.47/css/bootstrap-datetimepicker.min.css" />
{% compress css %}
<link href="{% static 'css/main.scss' %}" type="text/x-scss" rel="stylesheet">
<link href="{% static 'css/mention.scss' %}" type="text/x-scss" rel="stylesheet">
{% endcompress %}
{% block extralinks %} {% endblock %}
</head>