@@ -118,17 +119,17 @@
@@ -345,6 +346,17 @@
+
+
+
{% endblock js_block %}
diff --git a/cases/views.py b/cases/views.py
index 74fbd8ee..71835958 100644
--- a/cases/views.py
+++ b/cases/views.py
@@ -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)
\ No newline at end of file
diff --git a/common/models.py b/common/models.py
index 89ccf7c7..41fad5f3 100644
--- a/common/models.py
+++ b/common/models.py
@@ -273,4 +273,4 @@ class Document(models.Model):
def __str__(self):
- return self.title
+ return self.title
\ No newline at end of file
diff --git a/common/templates/doc_list.html b/common/templates/doc_list.html
index ebf88df3..453d888a 100644
--- a/common/templates/doc_list.html
+++ b/common/templates/doc_list.html
@@ -2,102 +2,112 @@
{% load staticfiles %}
{% load paginate %}
{% block content %}
-
- -
- {% for attachemnt in attachments %}
-
- + {% for attachment in attachments %} +
-
- +-
{{ attachemnt.created_by }}-{{ attachemnt.created_on }}+{{ attachment.created_by }}+{{ attachment.created_on }}
{% endfor %}
@@ -149,7 +150,7 @@
-
-
-
-
-
-
-
-
+
+
- Add New Document
-
-
+
+
+
+
{% endblock %}
{% block js_block %}
-{% endblock js_block %}
+{% endblock js_block %}
\ No newline at end of file
diff --git a/common/views.py b/common/views.py
index cc97ecf8..93ba51ce 100644
--- a/common/views.py
+++ b/common/views.py
@@ -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
\ No newline at end of file
diff --git a/contacts/forms.py b/contacts/forms.py
index df549dd8..e6141b33 100644
--- a/contacts/forms.py
+++ b/contacts/forms.py
@@ -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):
diff --git a/contacts/templates/view_contact.html b/contacts/templates/view_contact.html
index 3fa4cbc8..1503bfd0 100644
--- a/contacts/templates/view_contact.html
+++ b/contacts/templates/view_contact.html
@@ -1,5 +1,6 @@
{% extends 'base.html' %}
{% load staticfiles %}
+
{% block breadcrumb %}
{%endblock%}
+
{% block content %}
+
-
+
+
+
+
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
- Filters
-
-
-
-
-
-
-
- List of Documents
- {% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{ documents|length }}{% endif %}
-
-
-
-
- {% if documents|length > 0 %}
-
-
-
- {% ifequal documents|length 0 %}
- | S.No | -Document Title | -Created_by | -Actions | -
|---|---|---|---|
| {{ forloop.counter }} | -{{ document.title }} | -{{ document.created_by }} | -- Edit - Delete - | -
No Documents Found
- {% endifequal %} -
- {% show_pages %}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ List of Documents
+ {% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{documents|length}}{% endif %}
+
+
+
+
+ {% if documents|length > 0 %}
+
+
+ {% ifequal documents|length 0 %}
+ | S.No | +Title | +Created By | +Actions | +
|---|---|---|---|
| {{ forloop.counter }} | +{% if document.title %}{{ document.title }}{% endif %} | +{{document.created_by}} | ++ + + | +
No Documents Found
+ {% endifequal %} +
+ {% show_pages %}
+
+ +
@@ -241,6 +242,16 @@
+
+
{% endblock js_block %}
diff --git a/leads/views.py b/leads/views.py
index b04319d5..83552c63 100644
--- a/leads/views.py
+++ b/leads/views.py
@@ -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
diff --git a/opportunity/templates/view_opportunity.html b/opportunity/templates/view_opportunity.html
index b5203d19..90f5c22a 100644
--- a/opportunity/templates/view_opportunity.html
+++ b/opportunity/templates/view_opportunity.html
@@ -10,6 +10,7 @@
{% endblock%}
+
{% block content%}
@@ -235,6 +236,16 @@
{% endblock %}
{% block js_block %}
+
{% endblock js_block %}
+
diff --git a/opportunity/views.py b/opportunity/views.py
index abb7cc4d..b84d9c52 100644
--- a/opportunity/views.py
+++ b/opportunity/views.py
@@ -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
diff --git a/static/css/mention.scss b/static/css/mention.scss
new file mode 100644
index 00000000..9db94670
--- /dev/null
+++ b/static/css/mention.scss
@@ -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; }
diff --git a/static/js/mention.js b/static/js/mention.js
new file mode 100644
index 00000000..2d9a04ac
--- /dev/null
+++ b/static/js/mention.js
@@ -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
+ }
+})
diff --git a/templates/base.html b/templates/base.html
index 0776c374..cb847237 100644
--- a/templates/base.html
+++ b/templates/base.html
@@ -14,6 +14,8 @@
{% compress css %}
+
+
{% endcompress %}
{% block extralinks %} {% endblock %}