diff --git a/accounts/templates/view_account.html b/accounts/templates/view_account.html index 2f301637..66117f58 100644 --- a/accounts/templates/view_account.html +++ b/accounts/templates/view_account.html @@ -12,6 +12,8 @@ {% endblock breadcrumb %} + + {% block content %}
@@ -1833,6 +1835,7 @@ {% endblock content %} {% block js_block %} + + - - - {% endblock js_block %} diff --git a/accounts/views.py b/accounts/views.py index 3f440840..6bef7d13 100644 --- a/accounts/views.py +++ b/accounts/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 @@ -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 diff --git a/cases/templates/cases.html b/cases/templates/cases.html index 4e5e8573..b13d0663 100644 --- a/cases/templates/cases.html +++ b/cases/templates/cases.html @@ -116,7 +116,6 @@ {{ forloop.counter }} {{ case.name }} - {{case.closed_on}} {% if case.account %} {{ case.account }} diff --git a/cases/templates/view_case.html b/cases/templates/view_case.html index 3ffe4b61..49de4d0d 100644 --- a/cases/templates/view_case.html +++ b/cases/templates/view_case.html @@ -1,5 +1,6 @@ {% extends 'base.html' %} {% load staticfiles %} + {% block content %}
@@ -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 %} - -
- -
-
-
-
-
- + +
+ +
+
+
+
+
+ -
- - -
-
-
-
-
Filters
-
-
-
-
- - -
-
-
-
-
-
-
-
-
- -
-
-
-
-
- List of Documents - {% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{ documents|length }}{% endif %} -
-
- - - {% if documents|length > 0 %} - - - - - - - - {% endif %} - - - {% if per_page %} - {% paginate per_page documents %} - {% else %} - {% paginate 10 documents %} - {% endif %} - {% for document in documents %} - - - - - - - {% endfor %} - -
S.NoDocument TitleCreated_byActions
{{ forloop.counter }}{{ document.title }}{{ document.created_by }} - Edit - Delete -
-
- {% ifequal documents|length 0 %} -
No Documents Found
- {% endifequal %} -
- {% show_pages %} -
-
-
- + + +
+
+
+
+
+
Filters
+
+
+
+ + +
+
+
+
+ + Clear +
+
+
+
+
+
+
+
+
+
+
+
+
+ List of Documents + {% if show_pageitems %} {% show_pageitems %}{% else %}Total Documents {{documents|length}}{% endif %} +
+
+ + + {% if documents|length > 0 %} + + + + + + + {% endif %} + + + {% if per_page %} + {% paginate per_page documents %} + {% else%} + {% paginate 10 documents %} + {% endif %} + {% for document in documents %} + + + + + + + {% endfor %} + +
S.NoTitleCreated ByActions
{{ forloop.counter }}{% if document.title %}{{ document.title }}{% endif %}{{document.created_by}} + + +
+
+ {% ifequal documents|length 0 %} +
No Documents Found
+ {% endifequal %} +
+ {% show_pages %} +
+
+
+
+
+
+
+
+ + + + +
+ {% 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 %}
@@ -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 %}