diff --git a/README.md b/README.md index ad84c2b..561bbbd 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,13 @@ sudo apt install postgresql xvfb libfontconfig wkhtmltopdf git libpq-dev python3 sudo gem install sass +sudo apt-get install postfix + ``` +#### postfix package installation process +* After running ```sudo apt-get install postfix``` in terminal, select ```Internet``` as a configuration from the pop-up and click ok. Next it will ask to enter the ```System mail name``` which will be the used as a ```From Email Address``` during sending the mail. + #### Install dependencies --- diff --git a/accounts/api_urls.py b/accounts/api_urls.py new file mode 100644 index 0000000..10db4a7 --- /dev/null +++ b/accounts/api_urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from accounts import api_views + +app_name = 'api_accounts' + +urlpatterns = [ + path("accounts-list/", api_views.AccountsListView.as_view()), + path("accounts-create/", api_views.CreateAccountView.as_view()), + path("/view/", api_views.AccountDetailView.as_view()), + path("accounts//update/", api_views.AccountUpdateView.as_view()), + path("accounts//delete/", api_views.AccountDeleteView.as_view()), +] diff --git a/accounts/api_views.py b/accounts/api_views.py new file mode 100644 index 0000000..78fb193 --- /dev/null +++ b/accounts/api_views.py @@ -0,0 +1,500 @@ +import pytz +from django.conf import settings +from django.contrib.sites.shortcuts import get_current_site +from django.core.exceptions import PermissionDenied +from django.db.models import Q +from accounts.forms import ( + AccountForm, +) +from accounts.models import Account, Tags +from accounts.tasks import send_email_to_assigned_user +from accounts import swagger_params +from accounts.serializer import AccountSerializer, TagsSerailizer +from common.models import User, Attachments +from common.utils import ( + COUNTRIES, + INDCHOICES, +) +from common.custom_auth import JSONWebTokenAuthentication +from common.serializer import UserSerializer, CommentSerializer, AttachmentsSerializer +from common.utils import ( + CASE_TYPE, + COUNTRIES, + CURRENCY_CODES, + INDCHOICES, + PRIORITY_CHOICE, + STATUS_CHOICE, +) +from contacts.models import Contact +from leads.models import Lead +from opportunity.models import SOURCES, STAGES, Opportunity +from cases.models import Case +from cases.serializer import CaseSerializer +from contacts.serializer import ContactSerializer +from leads.serializer import LeadSerializer +from teams.serializer import TeamsSerializer +from tasks.serializer import TaskSerializer +from opportunity.serializer import OpportunitySerializer +from invoices.serializer import InvoiceSerailizer +from emails.serializer import EmailSerailizer +from teams.models import Teams + +from rest_framework import status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import IsAuthenticated +from drf_yasg.utils import swagger_auto_schema + + +class AccountsListView(APIView): + + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Account + + def get_queryset(self): + queryset = self.model.objects.all() + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + queryset = queryset.filter( + Q(created_by=self.request.user) | Q( + assigned_to=self.request.user) + ).distinct() + + if self.request.GET.get("tag", None): + queryset = queryset.filter( + tags__in=self.request.GET.getlist("tag")) + + request_post = self.request.POST + if request_post: + if request_post.get("name"): + queryset = queryset.filter( + name__icontains=request_post.get("name")) + if request_post.get("city"): + queryset = queryset.filter( + billing_city__contains=request_post.get("city") + ) + if request_post.get("industry"): + queryset = queryset.filter( + industry__icontains=request_post.get("industry") + ) + if request_post.get("tag"): + queryset = queryset.filter( + tags__in=request_post.getlist("tag")) + + return queryset.filter(company=self.request.company).distinct() + + def get_context_data(self, **kwargs): + context = {} + open_accounts = AccountSerializer( + self.get_queryset().filter(status="open"), many=True).data + close_accounts = AccountSerializer( + self.get_queryset().filter(status="close"), many=True).data + context["accounts_list"] = AccountSerializer( + self.get_queryset(), many=True).data + context["users"] = UserSerializer(User.objects.filter( + is_active=True).order_by("email"), many=True).data + context["open_accounts"] = open_accounts + context["close_accounts"] = close_accounts + context["industries"] = INDCHOICES + context["per_page"] = self.request.POST.get("per_page") + tag_ids = list(set(Account.objects.values_list("tags", flat=True))) + context["tags"] = TagsSerailizer( + Tags.objects.filter(id__in=tag_ids), many=True).data + if self.request.POST.get("tag", None): + context["request_tags"] = self.request.POST.getlist("tag") + elif self.request.GET.get("tag", None): + context["request_tags"] = self.request.GET.getlist("tag") + else: + context["request_tags"] = None + + search = False + if ( + self.request.POST.get("name") + or self.request.POST.get("city") + or self.request.POST.get("industry") + or self.request.POST.get("tag") + ): + search = True + + context["search"] = search + + tab_status = "Open" + if self.request.POST.get("tab_status"): + tab_status = self.request.POST.get("tab_status") + context["tab_status"] = tab_status + TIMEZONE_CHOICES = [(tz, tz) for tz in pytz.common_timezones] + context["timezones"] = TIMEZONE_CHOICES + context["settings_timezone"] = settings.TIME_ZONE + + return context + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_list_post_params) + def post(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class CreateAccountView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Account + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_create_post_params) + def post(self, request): + params = request.query_params if len( + request.data) == 0 else request.data + context = {} + form = AccountForm( + params, request.FILES, request_obj=request, account=True, request_user=self.request.user + ) + # Save Account + if form.is_valid(): + account_object = form.save(commit=False) + account_object.created_by = self.request.user + account_object.company = self.request.company + account_object.save() + + if self.request.POST.get("tags", ""): + tags = self.request.POST.get("tags") + splitted_tags = tags.split(",") + for t in splitted_tags: + tag = Tags.objects.filter(name=t) + if tag: + tag = tag[0] + else: + tag = Tags.objects.create(name=t) + account_object.tags.add(tag) + if self.request.POST.getlist("contacts", []): + account_object.contacts.add( + *self.request.POST.getlist("contacts")) + if self.request.POST.getlist("assigned_to", []): + account_object.assigned_to.add( + *self.request.POST.getlist("assigned_to")) + if self.request.FILES.get("account_attachment"): + attachment = Attachments() + attachment.created_by = self.request.user + attachment.file_name = self.request.FILES.get( + "account_attachment").name + attachment.account = account_object + attachment.attachment = self.request.FILES.get( + "account_attachment") + attachment.save() + if self.request.POST.getlist("teams", []): + user_ids = Teams.objects.filter( + id__in=self.request.POST.getlist("teams") + ).values_list("users", flat=True) + assinged_to_users_ids = account_object.assigned_to.all().values_list( + "id", flat=True + ) + for user_id in user_ids: + if user_id not in assinged_to_users_ids: + account_object.assigned_to.add(user_id) + if self.request.POST.getlist("teams", []): + account_object.teams.add(*self.request.POST.getlist("teams")) + + assigned_to_list = list( + account_object.assigned_to.all().values_list("id", flat=True) + ) + current_site = get_current_site(self.request) + recipients = assigned_to_list + send_email_to_assigned_user.delay( + recipients, + account_object.id, + domain=current_site.domain, + protocol=self.request.scheme, + ) + return Response({'error': False}) + context["errors"] = form.errors + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + def get_context_data(self, **kwargs): + context = {} + if self.request.user.role == "ADMIN" or self.request.user.is_superuser: + self.users = User.objects.filter( + is_active=True, company=self.request.company + ).order_by("email") + elif self.request.user.google.all(): + self.users = [] + else: + self.users = User.objects.filter( + role="ADMIN", company=self.request.company + ).order_by("email") + context["users"] = UserSerializer(self.users, many=True).data + context["industries"] = INDCHOICES + context["countries"] = COUNTRIES + contacts = Contact.objects.filter(company=self.request.company) + leads = Lead.objects.exclude( + status__in=["converted", "closed"], company=self.request.company) + if self.request.user.role == "ADMIN" or self.request.user.is_superuser: + context["leads"] = LeadSerializer(leads, many=True).data + context["contacts"] = ContactSerializer(contacts, many=True).data + else: + leads = filter( + Q(assigned_to__in=[self.request.user]) + | Q(created_by=self.request.user)) + context["leads"] = LeadSerializer(leads, many=True).data + context["lead_count"] = leads.count() + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + context["lead_count"] = ( + Lead.objects.filter( + Q(assigned_to__in=[self.request.user]) + | Q(created_by=self.request.user) + ) + .filter(company=self.request.company,) + .exclude(status="closed") + .count() + ) + contacts = contacts.filter( + Q(assigned_to__in=[self.request.user]) | Q( + created_by=self.request.user) + ) + context["contacts"] = ContactSerializer(contacts, many=True).data + context["contact_count"] = contacts.count() + context["teams"] = TeamsSerializer(Teams.objects.filter( + company=self.request.company,), many=True).data + return context + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_create_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class AccountUpdateView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Account + + def dispatch(self, request, *args, **kwargs): + self.users = User.objects.filter(is_active=True).order_by("email") + self.account_obj = Account.objects.filter(id=kwargs.get('pk')).first() + if self.account_obj.company != request.company: + raise PermissionDenied + return super(AccountUpdateView, self).dispatch(request, *args, **kwargs) + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_update_post_params) + def post(self, request, *args, **kwargs): + params = request.query_params if len( + request.data) == 0 else request.data + context = {} + form = AccountForm(params, request.FILES, instance=self.account_obj, request_obj=request, account=True, request_user=self.request.user + ) + if form.is_valid(): + account_object = form.save(commit=False) + account_object.save() + previous_assigned_to_users = list( + account_object.assigned_to.all().values_list("id", flat=True) + ) + account_object.tags.clear() + if self.request.POST.get("tags", ""): + tags = self.request.POST.get("tags") + splitted_tags = tags.split(",") + for t in splitted_tags: + tag = Tags.objects.filter(name=t.lower()) + if tag: + tag = tag[0] + else: + tag = Tags.objects.create(name=t.lower()) + account_object.tags.add(tag) + if self.request.POST.getlist("contacts", []): + account_object.contacts.clear() + account_object.contacts.add( + *self.request.POST.getlist("contacts")) + if self.request.POST.getlist("assigned_to", []): + account_object.assigned_to.clear() + account_object.assigned_to.add( + *self.request.POST.getlist("assigned_to")) + else: + account_object.assigned_to.clear() + if self.request.FILES.get("account_attachment"): + attachment = Attachments() + attachment.created_by = self.request.user + attachment.file_name = self.request.FILES.get( + "account_attachment").name + attachment.account = account_object + attachment.attachment = self.request.FILES.get( + "account_attachment") + attachment.save() + + if self.request.POST.getlist("teams", []): + account_object.teams.clear() + account_object.teams.add(*self.request.POST.getlist("teams")) + else: + account_object.teams.clear() + if self.request.POST.getlist("teams", []): + user_ids = Teams.objects.filter( + id__in=self.request.POST.getlist("teams") + ).values_list("users", flat=True) + assinged_to_users_ids = account_object.assigned_to.all().values_list( + "id", flat=True + ) + for user_id in user_ids: + if user_id not in assinged_to_users_ids: + account_object.assigned_to.add(user_id) + + assigned_to_list = list( + account_object.assigned_to.all().values_list("id", flat=True) + ) + current_site = get_current_site(self.request) + recipients = list(set(assigned_to_list) - + set(previous_assigned_to_users)) + send_email_to_assigned_user.delay( + recipients, + account_object.id, + domain=current_site.domain, + protocol=self.request.scheme, + ) + return Response({'error': False}) + context['errors'] = form.errors + return Response(context, status=status.HTTP_400_BAD_REQUEST) + + def get_context_data(self, **kwargs): + context = {} + self.users = User.objects.filter(is_active=True).order_by("email") + context["account_obj"] = AccountSerializer(self.account_obj).data + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if (self.request.user != context["account_obj"].created_by) and ( + self.request.user not in context[ + "account_obj"].assigned_to.all() + ): + raise PermissionDenied + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + self.users = self.users.filter( + Q(role="ADMIN") | Q(id__in=[self.request.user.id, ]) + ) + context["users"] = UserSerializer(self.users, many=True).data + context["industries"] = INDCHOICES + context["countries"] = COUNTRIES + context["contact_count"] = Contact.objects.count() + context["edit_view"] = True + leads = Lead.objects.exclude( + status__in=["converted", "closed"]) + if self.request.user.role == "ADMIN": + context["leads"] = LeadSerializer(leads, many=True).data + else: + leads = leads.filter(Q(assigned_to__in=[self.request.user]) | Q( + created_by=self.request.user)) + context["leads"] = LeadSerializer(leads, many=True).data + context["lead_count"] = leads.count() + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + context["lead_count"] = ( + Lead.objects.filter( + Q(assigned_to__in=[self.request.user]) + | Q(created_by=self.request.user) + ) + .exclude(status="closed") + .count() + ) + context["teams"] = TeamsSerializer(Teams.objects.all(), many=True).data + return context + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_update_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class AccountDeleteView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Account + + def get_object(self): + return Account.objects.filter(id=self.kwargs.get('pk')).first() + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_delete_params) + def get(self, request, *args, **kwargs): + self.object = self.get_object() + if self.object.company != request.company: + raise PermissionDenied + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if self.request.user != self.object.created_by: + raise PermissionDenied + self.object.delete() + return Response({'status': 'success'}, status=status.HTTP_204_NO_CONTENT) + + +class AccountDetailView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Account + + def get_object(self): + return Account.objects.filter(id=self.kwargs.get('pk')).first() + + def dispatch(self, request, *args, **kwargs): + self.account = self.get_object() + if self.account.company != request.company: + raise PermissionDenied + return super(AccountDetailView, self).dispatch(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = {} + context["account_obj"] = AccountSerializer(self.account).data + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if not ( + (self.request.user == self.account.created_by) + or (self.request.user in self.account.assigned_to.all()) + ): + raise PermissionDenied + + comment_permission = ( + True + if ( + self.request.user == self.account.created_by + or self.request.user.is_superuser + or self.request.user.role == "ADMIN" + ) + else False + ) + + if self.request.user.is_superuser or self.request.user.role == "ADMIN": + users_mention = list( + User.objects.filter( + is_active=True, company=self.request.company, + ).values("username") + ) + elif self.request.user != self.account.created_by: + if self.account.created_by: + users_mention = [{"username": self.account.created_by.username}] + else: + users_mention = [] + else: + users_mention = [] + + context.update( + { + "comments": CommentSerializer(self.account.accounts_comments.all(), many=True).data, + "attachments": AttachmentsSerializer(self.account.account_attachment.all(), many=True).data, + "opportunity_list": OpportunitySerializer(Opportunity.objects.filter(account=self.account), many=True).data, + "contacts": ContactSerializer(self.account.contacts.all(), many=True).data, + "users": UserSerializer(User.objects.filter( + is_active=True, company=self.request.company, + ).order_by("email"), many=True).data, + "cases": CaseSerializer(Case.objects.filter(account=self.account), many=True).data, + "stages": STAGES, + "sources": SOURCES, + "countries": COUNTRIES, + "currencies": CURRENCY_CODES, + "case_types": CASE_TYPE, + "case_priority": PRIORITY_CHOICE, + "case_status": STATUS_CHOICE, + "comment_permission": comment_permission, + "tasks": TaskSerializer(self.account.accounts_tasks.all(), many=True).data, + "invoices": InvoiceSerailizer(self.account.accounts_invoices.all(), many=True).data, + "emails": EmailSerailizer(self.account.sent_email.all(), many=True).data, + "users_mention": users_mention, + } + ) + return context + + + @swagger_auto_schema(tags=["accounts"], manual_parameters=swagger_params.account_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) diff --git a/accounts/forms.py b/accounts/forms.py index 57b3709..754d0a9 100644 --- a/accounts/forms.py +++ b/accounts/forms.py @@ -84,6 +84,24 @@ def __init__(self, *args, **kwargs): if self.instance.id: self.fields["lead"].required = False self.fields["lead"].required = False + self.company = request_obj.company + + + def clean_name(self): + name = self.cleaned_data.get("name") + if self.instance.id: + if self.instance.name != name: + if not Account.objects.filter( + name__iexact=self.cleaned_data.get("name"), + company=self.company + ).exists(): + return self.cleaned_data.get("name") + raise forms.ValidationError( + "Account already exists with this name") + return self.cleaned_data.get("name") + if not Account.objects.filter(name__iexact=self.cleaned_data.get("name"), company=self.company).exists(): + return self.cleaned_data.get("name") + raise forms.ValidationError("Account already exists with this name") class Meta: model = Account diff --git a/accounts/serializer.py b/accounts/serializer.py new file mode 100644 index 0000000..e50af3d --- /dev/null +++ b/accounts/serializer.py @@ -0,0 +1,85 @@ +from rest_framework import serializers +from accounts.models import Account, Email, Tags +from common.serializer import UserSerializer, CompanySerializer +from leads.serializer import LeadSerializer +from teams.serializer import TeamsSerializer +from contacts.serializer import ContactSerializer + + +class TagsSerailizer(serializers.ModelSerializer): + class Meta: + model = Tags + fields = ( + "name", + "slug" + ) + + +class AccountSerializer(serializers.ModelSerializer): + created_by = UserSerializer() + lead = LeadSerializer() + company = CompanySerializer() + tags = TagsSerailizer(read_only=True, many=True) + assigned_to = UserSerializer(read_only=True, many=True) + contacts = ContactSerializer(read_only=True, many=True) + teams = TeamsSerializer(read_only=True, many=True) + + class Meta: + model = Account + # fields = ‘__all__’ + fields = ( + 'id', + "name", + "email", + "phone", + "industry", + "billing_address_line", + "billing_street", + "billing_city", + "billing_state", + "billing_postcode", + "billing_country", + "website", + "description", + "created_by", + "created_on", + "is_active", + "tags", + "status", + "lead", + "contact_name", + "contacts", + "assigned_to", + "teams", + "company" + ) + + +class EmailSerializer(serializers.ModelSerializer): + + class Meta: + model = Email + fields = ( + "from_account" + "recipients" + "message_subject" + "message_body" + "timezone" + "scheduled_date_time" + "scheduled_later" + "created_on" + "from_email" + "rendered_message_body" + ) + + +class EmailLogSerializer(serializers.ModelSerializer): + email = EmailSerializer() + + class Meta: + model = Email + fields = ( + "email" + "contact" + "is_sent" + ) diff --git a/accounts/swagger_params.py b/accounts/swagger_params.py new file mode 100644 index 0000000..9813bcd --- /dev/null +++ b/accounts/swagger_params.py @@ -0,0 +1,66 @@ +from drf_yasg import openapi + +company_params_in_header = openapi.Parameter( + 'company', openapi.IN_HEADER, required=True, type=openapi.TYPE_STRING) + + +account_list_get_params = [company_params_in_header] + +account_list_post_params = [company_params_in_header] + +account_create_get_params = [company_params_in_header] + +account_create_post_params = [ + company_params_in_header, + openapi.Parameter('name', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('phone', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_address_line', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_street', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_city', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_state', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_postcode', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_country', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('contacts', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), +] + + +account_update_get_params = [company_params_in_header] + +account_update_post_params = [ + company_params_in_header, + openapi.Parameter('name', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('phone', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_address_line', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_street', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_city', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_state', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_postcode', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('billing_country', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('contacts', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), +] + +account_delete_params = [ + company_params_in_header, +] diff --git a/accounts/templates/create_account.html b/accounts/templates/create_account.html index bcd3993..77dda80 100644 --- a/accounts/templates/create_account.html +++ b/accounts/templates/create_account.html @@ -358,6 +358,7 @@

Create a contact $('#submit_btn').removeAttr('disabled') } else { + alert(data.message) window.location = data.success_url; } } diff --git a/accounts/views.py b/accounts/views.py index 4098e38..e9d93d7 100644 --- a/accounts/views.py +++ b/accounts/views.py @@ -236,7 +236,8 @@ def form_valid(self, form): return redirect("accounts:new_account") if self.request.is_ajax(): - data = {"success_url": reverse_lazy("accounts:list"), "error": False} + data = {"success_url": reverse_lazy("accounts:list"), "error": False, + "message": "Account created successfully"} return JsonResponse(data) return redirect("accounts:list") @@ -366,6 +367,7 @@ class AccountUpdateView(SalesAccessRequiredMixin, LoginRequiredMixin, UpdateView def dispatch(self, request, *args, **kwargs): self.users = User.objects.filter(is_active=True).order_by("email") lead_obj = self.get_object() + self.account_status = lead_obj.status if lead_obj.company != request.company: raise PermissionDenied # if self.request.user.role == 'ADMIN' or self.request.user.is_superuser: @@ -453,9 +455,15 @@ def form_valid(self, form): domain=current_site.domain, protocol=self.request.scheme, ) - + message = "Account updated successfully" + if self.account_status != account_object.status: + if account_object.status == "open": + message = "Account opened successfully" + else: + message = "Account closed successfully" if self.request.is_ajax(): - data = {"success_url": reverse_lazy("accounts:list"), "error": False} + data = {"success_url": reverse_lazy("accounts:list"), "error": False, + "message": message} return JsonResponse(data) return redirect("accounts:list") diff --git a/cases/serializer.py b/cases/serializer.py new file mode 100644 index 0000000..6d652a3 --- /dev/null +++ b/cases/serializer.py @@ -0,0 +1,43 @@ +from rest_framework import serializers +from cases.models import Case +from common.serializer import UserSerializer, CompanySerializer +from teams.serializer import TeamsSerializer +from accounts.serializer import AccountSerializer +from contacts.serializer import ContactSerializer + + +class CaseSerializer(serializers.ModelSerializer): + account = AccountSerializer(read_only=True, many=True) + contacts = ContactSerializer(read_only=True, many=True) + assigned_to = UserSerializer(read_only=True, many=True) + created_by = UserSerializer(read_only=True) + teams = TeamsSerializer(read_only=True, many=True) + company = CompanySerializer() + get_team_users = UserSerializer(read_only=True, many=True) + get_team_and_assigned_users = UserSerializer(read_only=True, many=True) + get_assigned_users_not_in_teams = UserSerializer(read_only=True, many=True) + + + class Meta: + model = Case + fields = ( + 'id', + 'name', + 'status', + 'priority', + 'case_type', + 'account', + 'contacts', + 'closed_on', + 'description', + 'assigned_to', + 'created_by', + 'created_on', + 'is_active', + 'teams', + 'company', + 'get_team_users', + 'get_team_and_assigned_users', + "get_assigned_users_not_in_teams", + "created_on_arrow", + ) diff --git a/common/api_urls.py b/common/api_urls.py new file mode 100644 index 0000000..c314dc3 --- /dev/null +++ b/common/api_urls.py @@ -0,0 +1,24 @@ +from django.urls import path +from common import api_views + + +app_name = "api_common" + + +urlpatterns = [ + path("dashboard/", api_views.ApiHomeView.as_view()), + path("registration/", api_views.RegistrationView.as_view()), + path("login/", api_views.LoginView.as_view()), + path("validate-subdomain/", api_views.check_sub_domain), + path("profile/", api_views.ProfileView.as_view()), + path("get_teams_and_users/", api_views.GetTeamsAndUsersView.as_view()), + path("change-password/", api_views.ChangePasswordView.as_view(), name="change_password"), + path("forgot-password/", api_views.ForgotPasswordView.as_view()), + path("reset-password/", api_views.ResetPasswordView.as_view(), name='reset_password'), + path("users/list/", api_views.UsersListView.as_view(), name="users_list"), + path("users//view/", api_views.UserDetailView.as_view(), name="view_user"), + # path("users/create/", api_views.CreateUserView.as_view(), name="create_user"), + path("documents/create/", api_views.DocumentCreate.as_view(), name="create_doc"), + #To be checked + path("users//delete/", api_views.UserDeleteView.as_view(), name="remove_user"), +] diff --git a/common/api_views.py b/common/api_views.py new file mode 100644 index 0000000..5cdf6a0 --- /dev/null +++ b/common/api_views.py @@ -0,0 +1,564 @@ +from django.conf import settings +from drf_yasg.utils import swagger_auto_schema +from django.shortcuts import get_object_or_404 +from rest_framework import status +from django.http import Http404 +from accounts.serializer import AccountSerializer +from contacts.serializer import ContactSerializer +from opportunity.serializer import OpportunitySerializer +from leads.serializer import LeadSerializer +from teams.serializer import TeamsSerializer +from common.serializer import * +from cases.serializer import CaseSerializer +from accounts.models import (Account, Contact) +from opportunity.models import Opportunity +from cases.models import Case +from leads.models import Lead +from teams.models import Teams +from common.forms import DocumentForm +from common.utils import ROLES +from common.models import User, Company +from common.access_decorators_mixins import ( + MarketingAccessRequiredMixin, + SalesAccessRequiredMixin, + admin_login_required, + marketing_access_required, + sales_access_required, +) +from common.tasks import ( + resend_activation_link_to_user, + send_email_to_new_user, + send_email_user_delete, + send_email_user_status, + send_email_to_reset_password +) +from django.utils.translation import gettext as _ + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework_jwt.serializers import jwt_encode_handler +from common.utils import jwt_payload_handler +from rest_framework.exceptions import APIException +from rest_framework.permissions import IsAuthenticated +from common.custom_auth import JSONWebTokenAuthentication +from common import swagger_params +from django.db.models import Q +from rest_framework.decorators import api_view + + +class UserDetailView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["users"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, *args, **kwargs): + company = request.headers['company'] + user_id = kwargs['pk'] + user_obj = get_object_or_404( + User, pk=user_id, company__sub_domain=company) + users_data = [] + for each in User.objects.all(): + assigned_dict = {} + assigned_dict["id"] = each.id + assigned_dict["name"] = each.username + users_data.append(assigned_dict) + context = {} + context["user_obj"] = UserSerializer(user_obj).data + opportunity_list = Opportunity.objects.filter(assigned_to=user_obj.id) + context["opportunity_list"] = OpportunitySerializer( + opportunity_list, many=True).data + contacts = Contact.objects.filter(assigned_to=user_obj.id) + context["contacts"] = ContactSerializer(contacts, many=True).data + cases = Case.objects.filter(assigned_to=user_obj.id) + context["cases"] = CaseSerializer(cases, many=True).data + # "accounts": Account.objects.filter(assigned_to=user_obj.id) + # context["assigned_data"] = json.dumps(users_data) + context["assigned_data"] = users_data + comments = user_obj.user_comments.all() + context["comments"] = CommentSerializer(comments, many=True).data + return Response(context) + + +class GetTeamsAndUsersView(APIView): + + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["users"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, *args, **kwargs): + data = {} + teams = Teams.objects.filter(company=request.company) + # teams_data = [ + # {"team": team.id, "users": [user.id for user in team.users.all()]} + # for team in teams + # ] + teams_data = TeamsSerializer(teams, many=True).data + users = User.objects.filter(company=request.company) + users_data = UserSerializer(users, many=True).data + + users = User.objects.all().values_list("id", flat=True) + data["teams"] = teams_data + data["users_data"] = users_data + data["users"] = list(users) + return Response(data) + + +# to be checked +class UserDeleteView(APIView): + model = User + + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["users"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, *args, **kwargs): + self.object = self.get_object() + current_site = request.get_host() + deleted_by = self.request.user.email + send_email_user_delete.delay( + self.object.email, + deleted_by=deleted_by, + domain=current_site, + protocol=request.scheme, + ) + self.object.delete() + return Response({'status': 'success'}, status=status.HTTP_204_NO_CONTENT) + + +class ChangePasswordView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["profile"], operation_description="This is change password api", + manual_parameters=swagger_params.change_password_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + company = request.headers['company'] + old_password = params.get("old_password", None) + new_password = params.get("new_password", None) + retype_password = params.get("retype_password", None) + company_obj = Company.objects.filter(sub_domain=company).first() + errors = {} + if request.company.id is not company_obj.id: + errors["company"] = "Company header doesnot match." + if old_password: + if not request.user.check_password(old_password): + errors["old_password"] = "old password entered is incorrect." + + if new_password: + if len(new_password) < 8: + errors['new_password'] = "Password must be at least 8 characters long!" + if new_password == old_password: + errors[ + 'new_password'] = "New password and old password should not be same" + if retype_password: + if new_password != retype_password: + errors[ + 'retype_password'] = "new_password and retype_password did not match." + + if errors: + return Response({'status': 'failure', 'errors': errors}, status=400) + user = request.user + user.set_password(new_password) + user.save() + return Response({'status': 'success'}, status=200) + + +# check_header not working +class ApiHomeView(APIView): + + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["dashboard"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, format=None): + + accounts = Account.objects.filter(status="open") + contacts = Contact.objects.all() + leads = Lead.objects.exclude( + status="converted").exclude(status="closed") + opportunities = Opportunity.objects.all() + if self.request.user.role == "ADMIN" or self.request.user.is_superuser: + pass + else: + accounts = accounts.filter(Q(assigned_to__id__in=[self.request.user.id]) | Q( + created_by=self.request.user.id)) + contacts = contacts.filter(Q(assigned_to__id__in=[self.request.user.id]) | Q( + created_by=self.request.user.id)) + leads = leads.filter(Q(assigned_to__id__in=[self.request.user.id]) | Q( + created_by=self.request.user.id)).exclude(status="closed") + opportunities = opportunities.filter( + Q(assigned_to__id__in=[self.request.user.id]) | Q(created_by=self.request.user.id)) + context = {} + context["leads_count"] = leads.count() + context["accounts_count"] = accounts.count() + context["contacts_count"] = contacts.count() + context["opportunities_count"] = opportunities.count() + accounts = AccountSerializer(accounts, many=True).data + opportunities = OpportunitySerializer(opportunities, many=True).data + context["accounts"] = accounts + context["opportunities"] = opportunities + return Response(context) + + +class LoginView(APIView): + + @swagger_auto_schema(tags=["common"], operation_description="This is login api", + manual_parameters=swagger_params.login_page_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + company = request.headers['company'] + username = params.get("email", None) + password = params.get("password", None) + company_obj = Company.objects.filter(sub_domain=company).first() + if not company_obj: + company_field = "doesnot exit" + msg = _('company with this subdomin {company_field}') + msg = msg.format(company_field=company_field) + return Response({'status': 'failure', 'message': msg}, status=400) + # raise APIException(msg) + if not username: + username_field = "User Name/Email" + msg = _('Must include "{username_field}"') + msg = msg.format(username_field=username_field) + return Response({'status': 'failure', 'message': msg}, status=400) + # raise APIException(msg) + + user = User.objects.filter(email=username, company=company_obj).first() + if not user: + return Response({'status': 'failure', 'message': 'user not avaliable in our records'}, status=400) + if user.check_password(password): + payload = jwt_payload_handler(user) + response_data = { + 'token': jwt_encode_handler(payload), + 'status': 'success', + 'company': user.company.id, + 'subdomin': company_obj.sub_domain + } + return Response(response_data) + else: + password_field = "doesnot match" + msg = _('Email and password {password_field}') + msg = msg.format(password_field=password_field) + return Response({'status': 'failure', 'message': msg}, status=400) + + +class RegistrationView(APIView): + + @swagger_auto_schema(tags=["common"], operation_description="This is registration api", + manual_parameters=swagger_params.registration_page_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + sub_domain = params.get("sub_domain", None) + username = params.get("username", None) + email = params.get("email", None) + password = params.get("password", None) + errors = {} + if sub_domain: + if Company.objects.filter(sub_domain__iexact=sub_domain): + errors["sub_domain"] = "company with this subdomin already exit" + + if username: + if User.objects.filter(username__iexact=username): + errors['username'] = "User name already exit." + if email: + if User.objects.filter(email__iexact=email): + errors['email'] = "Email already exit." + + if password: + if len(password) < 8: + errors['password'] = "Password must be at least 8 characters long!" + + if errors: + return Response({'status': 'failure', 'errors': errors}, status=400) + else: + user = User.objects.create(username=username, email=email) + company = Company.objects.create(sub_domain=sub_domain) + company.save() + user.company = company + user.role = "ADMIN" + user.is_superuser = False + user.has_marketing_access = True + user.has_sales_access = True + user.is_admin = True + user.set_password(password) + user.save() + return Response({'status': 'success'}, status=201) + + +@swagger_auto_schema(method='post', tags=["common"], manual_parameters=swagger_params.check_sub_domain_params) +@api_view(['POST']) +def check_sub_domain(request): + if request.method == "POST": + params = request.query_params if len( + request.data) == 0 else request.data + sub_domain = params.get("sub_domain", None) + msg = _('Given sub_domain {domain_name} is {validity_status}') + company = Company.objects.filter(sub_domain=sub_domain).first() + if company: + request.session["company"] = company.id + msg = msg.format(domain_name=sub_domain, validity_status="Valid") + status_code = 200 + status_msg = "success" + else: + msg = msg.format(domain_name=sub_domain, validity_status="Invalid") + status_code = 400 + status_msg = "failure" + return Response({'status': status_msg, 'message': msg}, status=status_code) + + +class ProfileView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["profile"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, format=None): + context = {} + context["user_obj"] = UserSerializer(request.user).data + return Response(context) + + +class UsersListView(APIView): + + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["users"], manual_parameters=swagger_params.user_list_params) + def post(self, request, format=None): + + context = {} + queryset = User.objects.filter(company=self.request.company) + params = request.query_params if len( + request.data) == 0 else request.data + if params: + if params.get("username"): + queryset = queryset.filter( + username__icontains=params.get("username") + ) + if params.get("email"): + queryset = queryset.filter( + email__icontains=params.get("email")) + if params.get("role"): + queryset = queryset.filter(role=params.get("role")) + if params.get("status"): + queryset = queryset.filter(is_active=params.get("status")) + active_users = queryset.filter(is_active=True) + inactive_users = queryset.filter(is_active=False) + context["active_users"] = UserSerializer(active_users, many=True).data + context["inactive_users"] = UserSerializer( + inactive_users, many=True).data + context["per_page"] = self.request.POST.get("per_page") + context["admin_email"] = settings.ADMIN_EMAIL + context["roles"] = ROLES + context["status"] = [("True", "Active"), ("False", "In Active")] + + return Response(context) + + @swagger_auto_schema(tags=["users"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, format=None): + + queryset = User.objects.filter(company=self.request.company) + context = {} + active_users = queryset.filter(is_active=True) + inactive_users = queryset.filter(is_active=False) + context["active_users"] = UserSerializer(active_users, many=True).data + context["inactive_users"] = UserSerializer( + inactive_users, many=True).data + context["per_page"] = self.request.POST.get("per_page") + context["admin_email"] = settings.ADMIN_EMAIL + context["roles"] = ROLES + context["status"] = [("True", "Active"), ("False", "In Active")] + return Response(context) + + +class DocumentCreate(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["documents"], manual_parameters=swagger_params.dashboard_params) + def get(self, request, format=None): + users = [] + if request.user.role == "ADMIN" or request.user.is_superuser: + users = User.objects.filter(is_active=True, company=request.company).order_by( + "email" + ) + else: + users = User.objects.filter(role="ADMIN", company=request.company).order_by( + "email" + ) + context = {} + context["users"] = UserSerializer(users, many=True).data + teams = Teams.objects.filter(company=request.company) + context["teams"] = TeamsSerializer(teams, many=True).data + context['status'] = (("active", "active"), ("inactive", "inactive")) + return Response(context) + + # to be checked file upload + @swagger_auto_schema(tags=["documents"], manual_parameters=swagger_params.document_create_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + context = {} + users = [] + if request.user.role == "ADMIN" or request.user.is_superuser: + users = User.objects.filter(is_active=True, company=request.company).order_by( + "email" + ) + else: + users = User.objects.filter(role="ADMIN", company=request.company).order_by( + "email" + ) + form = DocumentForm( + params, request.FILES, users=users, request_obj=request + ) + if form.is_valid(): + doc = form.save(commit=False) + doc.created_by = request.user + doc.company = request.company + doc.save() + if params.getlist("shared_to"): + doc.shared_to.add(*params.getlist("shared_to")) + if params.getlist("teams", []): + user_ids = Teams.objects.filter( + id__in=params.getlist("teams") + ).values_list("users", flat=True) + assinged_to_users_ids = doc.shared_to.all().values_list("id", flat=True) + for user_id in user_ids: + if user_id not in assinged_to_users_ids: + doc.shared_to.add(user_id) + + if params.getlist("teams", []): + doc.teams.add(*params.getlist("teams")) + + data = {"error": False} + return Response(data) + return Response({"error": True, "errors": form.errors}) + context["errors"] = form.errors + return Response(context) + +# from common.forms import UserForm +# from common.utils import ROLES +# class CreateUserView(APIView): +# # model = User +# # form_class = UserForm +# # template_name = "create.html" +# # success_url = '/users/list/' +# authentication_classes = (JSONWebTokenAuthentication,) +# permission_classes = (IsAuthenticated,) +# # def form_valid(self, form): +# # user = form.save(commit=False) +# # if form.cleaned_data.get("password"): +# # user.set_password(form.cleaned_data.get("password")) +# # user.company = self.request.company +# # user.save() + +# # if self.request.POST.getlist("teams"): +# # for team in self.request.POST.getlist("teams"): +# # Teams.objects.filter(id=team).first().users.add(user) + +# # current_site = self.request.get_host() +# # protocol = self.request.scheme +# # send_email_to_new_user.delay( +# # user.email, self.request.user.email, domain=current_site, protocol=protocol +# # ) + +# # if self.request.is_ajax(): +# # data = {"success_url": reverse_lazy("common:users_list"), "error": False} +# # return Response(data) +# # return super(CreateUserView, self).form_valid(form) + +# # def form_invalid(self, form): +# # response = super(CreateUserView, self).form_invalid(form) +# # if self.request.is_ajax(): +# # return Response({"error": True, "errors": form.errors}) +# # return response + +# # def get_form_kwargs(self): +# # kwargs = super(CreateUserView, self).get_form_kwargs() +# # kwargs.update({"request_user": self.request.user}) +# # return kwargs + +# # def get_context_data(self, **kwargs): +# # context = super(CreateUserView, self).get_context_data(**kwargs) +# # context["user_form"] = context["form"] +# # context["teams"] = Teams.objects.all() +# # if "errors" in kwargs: +# # context["errors"] = kwargs["errors"] +# # return context + +# @swagger_auto_schema(tags=["user create"], manual_parameters=swagger_params.dashboard_params) +# def get(self, request, format=None): +# context = {} +# context['role'] = ROLES +# teams = Teams.objects.filter(company=request.company) +# context["teams"] = TeamsSerializer(teams, many=True).data +# return Response(context) + + +# @swagger_auto_schema(tags=["user create"], operation_description="Create user", +# manual_parameters=swagger_params.user_create_params) +# def post(self, request, format=None): +# params = request.query_params if len(request.data) == 0 else request.data + +# form = UserForm(params, request_user=self.request) + +# if form.is_valid(): +# user = form.save(commit=False) +# if form.cleaned_data.get("password"): +# user.set_password(form.cleaned_data.get("password")) +# user.company = self.request.company +# user.save() + +# if self.request.POST.getlist("teams"): +# for team in self.request.POST.getlist("teams"): +# Teams.objects.filter(id=team).first().users.add(user) + +# data = { "error": False} +# return Response(data) + +# # if self.request.is_ajax(): +# return Response({"error": True, "errors": form.errors}) + + +class ForgotPasswordView(APIView): + + @swagger_auto_schema(tags=["common"], manual_parameters=swagger_params.forgot_password_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + serializer = ForgotPasswordSerializer(data=params) + if serializer.is_valid(): + user = get_object_or_404(User, email=params.get('email')) + current_site = self.request.get_host() + protocol = self.request.scheme + send_email_to_reset_password.delay( + user.email, protocol=protocol, domain=current_site) + data = {"error": False, + "message": "We have sent you an email. please reset password"} + return Response(data) + else: + data = {"error": True, "errors": serializer.errors} + response_status = status.HTTP_400_BAD_REQUEST + return Response(data, status=response_status) + + +class ResetPasswordView(APIView): + + @swagger_auto_schema(tags=["common"], manual_parameters=swagger_params.reset_password_params) + def post(self, request, format=None): + params = request.query_params if len( + request.data) == 0 else request.data + serializer = ResetPasswordSerailizer(data=params) + if serializer.is_valid(): + password = params.get('new_password1') + user = serializer.user + user.set_password(password) + user.save() + data = {"error": False, + "message": "Password Updated Successfully. Please login"} + else: + data = {"error": True, "errors": serializer.errors} + return Response(data, status=status.HTTP_400_BAD_REQUEST) diff --git a/common/custom_auth.py b/common/custom_auth.py new file mode 100644 index 0000000..82992b0 --- /dev/null +++ b/common/custom_auth.py @@ -0,0 +1,119 @@ +import jwt +import base64 + +# from .models import User +# from django.contrib.auth import get_user_model +from django.utils.encoding import smart_text +from django.utils.translation import ugettext as _ +from rest_framework import exceptions +from rest_framework.authentication import ( + BaseAuthentication +) +from rest_framework.authentication import get_authorization_header +from django.conf import settings +from rest_framework_jwt.settings import api_settings + +try: + from threading import local +except ImportError: + from django.utils._threading_local import local +_thread_locals = local() + +jwt_decode_handler = api_settings.JWT_DECODE_HANDLER + + +def set_current_instance_field(name, value): + setattr(_thread_locals, name, value) + + +class BaseJSONWebTokenAuthentication(BaseAuthentication): + """ + Token based authentication using the JSON Web Token standard. + """ + + def authenticate(self, request): + """ + Returns a two-tuple of User and token if a valid signature has been + supplied using JWT-based authentication. Otherwise returns None. + """ + + authorization = request.META.get('HTTP_AUTHORIZATION', None) + self.company = request.META.get('HTTP_COMPANY', None) + jwt_value = self.get_jwt_value(request) + if jwt_value is None: + return None + try: + payload = jwt_decode_handler(jwt_value) + except jwt.ExpiredSignature: + msg = _('Signature has expired.') + raise exceptions.AuthenticationFailed(msg) + except jwt.DecodeError: + msg = _('Error decoding signature.') + raise exceptions.AuthenticationFailed(msg) + except jwt.InvalidTokenError: + raise exceptions.AuthenticationFailed() + + account = self.authenticate_credentials(payload) + + set_current_instance_field('authorization', True) + return account, payload + + def authenticate_credentials(self, payload): + """ + Returns an active user that matches the payload's user id and email. + """ + account_id = payload['id'] + from common.models import User + if not account_id: + msg = _('Invalid payload.') + raise exceptions.AuthenticationFailed(msg) + try: + account = User.objects.get(pk=account_id) + if account.company.sub_domain != self.company: + raise exceptions.AuthenticationFailed("user company doesnot match with header....") + + except User.DoesNotExist: + msg = _('Invalid signature.') + raise exceptions.AuthenticationFailed(msg) + return account + # return True + + +class JSONWebTokenAuthentication(BaseJSONWebTokenAuthentication): + """ + Clients should authenticate by passing the token key in the "Authorization" + HTTP header, prepended with the string specified in the setting + `JWT_AUTH_HEADER_PREFIX`. For example: + Authorization: JWT eyJhbGciOiAiSFMyNTYiLCAidHlwIj + """ + www_authenticate_realm = 'api' + + def get_jwt_value(self, request): + auth = get_authorization_header(request).split() + auth_header_prefix = api_settings.JWT_AUTH_HEADER_PREFIX.lower() + + if not auth: + if api_settings.JWT_AUTH_COOKIE: + return request.COOKIES.get(api_settings.JWT_AUTH_COOKIE) + return None + + if smart_text(auth[0].lower()) != auth_header_prefix: + return None + + if len(auth) == 1: + msg = _('Invalid Authorization header. No credentials provided.') + raise exceptions.AuthenticationFailed(msg) + elif len(auth) > 2: + msg = _('Invalid Authorization header. Credentials string ' + 'should not contain spaces.') + raise exceptions.AuthenticationFailed(msg) + + return auth[1] + + def authenticate_header(self, request): + """ + Return a string to be used as the value of the `WWW-Authenticate` + header in a `401 Unauthenticated` response, or `None` if the + authentication scheme should return `403 Permission Denied` responses. + """ + return '{0} realm="{1}"'.format(api_settings.JWT_AUTH_HEADER_PREFIX, self.www_authenticate_realm) diff --git a/common/middleware/get_company.py b/common/middleware/get_company.py index 4372039..b37339a 100644 --- a/common/middleware/get_company.py +++ b/common/middleware/get_company.py @@ -10,15 +10,24 @@ def __call__(self, request): return response def process_view(self, request, view_func, view_args, view_kwargs): - company_id = request.session.get("company", None) - if company_id: - company = Company.objects.get(id=company_id) - request.company = company - request.session["company"] = company.id - else: - host_name = request.META.get("HTTP_HOST") - subdomain = host_name.split(".")[0] + if request.headers.get('company'): + subdomain = request.headers.get('company') company = Company.objects.filter(sub_domain=subdomain).first() if company: request.company = company + else: + request.company = None + # request.session["company"] = company.id + else: + company_id = request.session.get("company", None) + if company_id: + company = Company.objects.get(id=company_id) + request.company = company request.session["company"] = company.id + else: + host_name = request.META.get("HTTP_HOST") + subdomain = host_name.split(".")[0] + company = Company.objects.filter(sub_domain=subdomain).first() + if company: + request.company = company + request.session["company"] = company.id diff --git a/common/models.py b/common/models.py index b33e8d2..a6dc86d 100644 --- a/common/models.py +++ b/common/models.py @@ -85,6 +85,10 @@ def get_full_name(self): full_name = self.email return full_name + @property + def created_on_arrow(self): + return arrow.get(self.date_joined).humanize() + def __str__(self): return self.email diff --git a/common/serializer.py b/common/serializer.py new file mode 100644 index 0000000..f566d2d --- /dev/null +++ b/common/serializer.py @@ -0,0 +1,206 @@ +from rest_framework import serializers +from common.models import User, Company, Comment, Address, Attachments +from django.utils.http import urlsafe_base64_decode +from django.contrib.auth.tokens import default_token_generator + + +class CompanySerializer(serializers.ModelSerializer): + + class Meta: + model = Company + fields = ( + 'id', + "name", + "address", + "sub_domain", + "user_limit", + "country" + ) + + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = User + fields = ( + 'id', + "file_prepend", + "username", + "first_name", + "last_name", + "email", + "is_active", + "is_admin", + "is_staff", + "date_joined", + "role", + "profile_pic", + "has_sales_access", + "has_marketing_access", + "company", + "get_app_name" + + ) + + +class CommentSerializer(serializers.ModelSerializer): + + class Meta: + model = Comment + fields = "__all__" + + +class CreateUserSerializer(serializers.ModelSerializer): + password = serializers.CharField() + + class Meta: + model = User + fields = ( + "email", + "first_name", + "last_name", + "username", + "role", + "profile_pic", + "has_sales_access", + "has_marketing_access", + ) + + def __init__(self, *args, **kwargs): + self.request_user = kwargs.pop("request_user", None) + super(CreateUserSerializer, self).__init__(*args, **kwargs) + self.fields["first_name"].required = True + if not self.instance.pk: + self.fields["password"].required = True + + # self.fields['password'].required = True + + def validate_password(self): + password = self.cleaned_data.get("password") + if password: + if len(password) < 4: + raise serializers.ValidationError( + "Password must be at least 4 characters long!" + ) + return password + + def validate_has_sales_access(self): + sales = self.cleaned_data.get("has_sales_access", False) + user_role = self.cleaned_data.get("role") + if user_role == "ADMIN": + is_admin = True + else: + is_admin = False + if self.request_user.role == "ADMIN" or self.request_user.is_superuser: + if not is_admin: + marketing = self.data.get("has_marketing_access", False) + if not sales and not marketing: + raise serializers.ValidationError( + "Select atleast one option.") + # if not (self.instance.role == 'ADMIN' or self.instance.is_superuser): + # marketing = self.data.get('has_marketing_access', False) + # if not sales and not marketing: + # raise forms.ValidationError('Select atleast one option.') + if self.request_user.role == "USER": + sales = self.instance.has_sales_access + return sales + + def validate_has_marketing_access(self): + marketing = self.cleaned_data.get("has_marketing_access", False) + if self.request_user.role == "USER": + marketing = self.instance.has_marketing_access + return marketing + + def validate_email(self): + email = self.cleaned_data.get("email") + if self.instance.id: + if self.instance.email != email: + if not User.objects.filter( + email=self.cleaned_data.get("email") + ).exists(): + return self.cleaned_data.get("email") + raise serializers.ValidationError("Email already exists") + else: + return self.cleaned_data.get("email") + else: + if not User.objects.filter(email=self.cleaned_data.get("email")).exists(): + return self.cleaned_data.get("email") + raise serializers.ValidationError( + "User already exists with this email") + + +class ForgotPasswordSerializer(serializers.Serializer): + email = serializers.CharField(max_length=200) + + def validate(self, data): + email = data.get('email') + user = User.objects.filter(email__iexact=email).last() + if not user: + raise serializers.ValidationError( + "You don't have an account. Please create one." + ) + return data + + +class CheckTokenSerializer(serializers.Serializer): + uidb64_regex = r'[0-9A-Za-z_\-]+' + token_regex = r'[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20}' + uidb64 = serializers.RegexField(uidb64_regex) + token = serializers.RegexField(token_regex) + error_message = {'__all__': ('Invalid password reset token')} + + def get_user(self, uidb64): + try: + uid = urlsafe_base64_decode(uidb64).decode() + user = User._default_manager.get(pk=uid) + except (TypeError, ValueError, OverflowError, User.DoesNotExist): + user = None + return user + + +class ResetPasswordSerailizer(CheckTokenSerializer): + new_password1 = serializers.CharField() + new_password2 = serializers.CharField() + + def validate(self, data): + self.user = self.get_user(data.get('uidb64')) + if not self.user: + raise serializers.ValidationError(self.error_message) + is_valid_token = default_token_generator.check_token( + self.user, data.get('token')) + if not is_valid_token: + raise serializers.ValidationError(self.error_message) + new_password2 = data.get('new_password2') + new_password1 = data.get('new_password1') + if new_password1 != new_password2: + raise serializers.ValidationError( + "The two password fields didn't match.") + return new_password2 + + +class AttachmentsSerializer(serializers.ModelSerializer): + + class Meta: + model = Attachments + fields = ['created_by', 'file_name', 'created_on'] + + +class BillingAddressSerializer(serializers.ModelSerializer): + + class Meta: + model = Address + fields = ("address_line", "street", "city", + "state", "postcode", "country") + + def __init__(self, *args, **kwargs): + account_view = kwargs.pop("account", False) + + super(BillingAddressSerializer, self).__init__(*args, **kwargs) + + if account_view: + self.fields["address_line"].required = True + self.fields["street"].required = True + self.fields["city"].required = True + self.fields["state"].required = True + self.fields["postcode"].required = True + self.fields["country"].required = True diff --git a/common/swagger_params.py b/common/swagger_params.py new file mode 100644 index 0000000..e2f3102 --- /dev/null +++ b/common/swagger_params.py @@ -0,0 +1,84 @@ +from drf_yasg import openapi + +company_params_in_header = openapi.Parameter( + 'company', openapi.IN_HEADER, required=True, type=openapi.TYPE_STRING) + + +login_page_params = [ + company_params_in_header, + openapi.Parameter('email', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), + openapi.Parameter('password', openapi.IN_QUERY, + format='password', required=True, type=openapi.TYPE_STRING) +] + +change_password_params = [ + company_params_in_header, + openapi.Parameter('old_password', openapi.IN_QUERY, + format='password', required=True, type=openapi.TYPE_STRING), + openapi.Parameter('new_password', openapi.IN_QUERY, + format='password', required=True, type=openapi.TYPE_STRING), + openapi.Parameter('retype_password', openapi.IN_QUERY, + format='password', required=True, type=openapi.TYPE_STRING), +] + +dashboard_params = [ + company_params_in_header +] + +user_delete_params = [ + company_params_in_header, + openapi.Parameter('user_id', openapi.IN_QUERY, required=True, type=openapi.TYPE_NUMBER), +] + +check_sub_domain_params = [ + openapi.Parameter('sub_domain', openapi.IN_QUERY, + required=True, type=openapi.TYPE_STRING), +] + +registration_page_params = [ + openapi.Parameter('sub_domain', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('username', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('password', openapi.IN_QUERY, format='password', required=True, type=openapi.TYPE_STRING) +] + +forgot_password_params = [ + openapi.Parameter('email', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), +] +reset_password_params = [ + openapi.Parameter('uidb64', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('token', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('new_password1', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('new_password2', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + +] +user_list_params = [ + company_params_in_header, + openapi.Parameter('username', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('role', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('status', openapi.IN_QUERY, type=openapi.TYPE_STRING) +] + +user_create_params = [ + company_params_in_header, + openapi.Parameter('username', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('role', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('password', openapi.IN_QUERY, format='password', required=True, type=openapi.TYPE_STRING), + openapi.Parameter('first_name', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('last_name', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('profile_pic', openapi.IN_QUERY, type=openapi.TYPE_FILE), + openapi.Parameter('has_sales_access', openapi.IN_QUERY, type=openapi.TYPE_BOOLEAN), + openapi.Parameter('has_marketing_access', openapi.IN_QUERY, type=openapi.TYPE_BOOLEAN), + openapi.Parameter('status', openapi.IN_QUERY, type=openapi.TYPE_STRING) +] + +document_create_params = [ + company_params_in_header, + openapi.Parameter('title', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('status', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('shared_to', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('document_file', openapi.IN_QUERY, type=openapi.TYPE_FILE), +] \ No newline at end of file diff --git a/common/tasks.py b/common/tasks.py index 087323d..1bb8adb 100644 --- a/common/tasks.py +++ b/common/tasks.py @@ -1,5 +1,5 @@ import datetime - +from django.conf import settings from celery.task import task from django.contrib.auth.tokens import PasswordResetTokenGenerator from django.core.mail import EmailMessage @@ -12,6 +12,7 @@ from common.models import Comment, Profile, User from common.token_generator import account_activation_token +from django.contrib.auth.tokens import default_token_generator from marketing.models import BlockedDomain, BlockedEmail @@ -108,7 +109,8 @@ def send_email_user_mentions( protocol + "://" + domain - + reverse("opportunity:opp_view", args=(comment.opportunity.id,)) + + reverse("opportunity:opp_view", + args=(comment.opportunity.id,)) ) subject = "New comment on Opportunity. " elif called_from == "cases": @@ -132,7 +134,8 @@ def send_email_user_mentions( protocol + "://" + domain - + reverse("invoices:invoice_details", args=(comment.invoice.id,)) + + reverse("invoices:invoice_details", + args=(comment.invoice.id,)) ) subject = "New comment on Invoice. " elif called_from == "events": @@ -146,7 +149,8 @@ def send_email_user_mentions( else: context["url"] = "" # subject = 'Django CRM : comment ' - blocked_domains = BlockedDomain.objects.values_list("domain", flat=True) + blocked_domains = BlockedDomain.objects.values_list( + "domain", flat=True) blocked_emails = BlockedEmail.objects.values_list("email", flat=True) if recipients: for recipient in recipients: @@ -220,7 +224,8 @@ def send_email_user_delete( recipients = [] recipients.append(user_email) subject = "CRM : Your account is Deleted. " - html_content = render_to_string("user_delete_email.html", context=context) + html_content = render_to_string( + "user_delete_email.html", context=context) if recipients: msg = EmailMessage(subject, html_content, to=recipients) msg.content_subtype = "html" @@ -263,3 +268,27 @@ def resend_activation_link_to_user( msg = EmailMessage(subject, html_content, to=recipients) msg.content_subtype = "html" msg.send() + + +@task +def send_email_to_reset_password( + user_email, domain="demo.django-crm.io", protocol="http" +): + """ Send Mail To Users When their account is created """ + user = User.objects.filter(email=user_email).first() + context = {} + context["user_email"] = user_email + context["url"] = protocol + "://" + domain + context["uid"] = (urlsafe_base64_encode(force_bytes(user.pk)),) + context["token"] = default_token_generator.make_token(user) + context["token"] = context["token"] + context["complete_url"] = context["url"] + '/api-common/reset-password/{uidb64}/{token}/'.format( + uidb64=context["uid"], token=context["token"]) + recipients = [] + recipients.append(user_email) + subject = "Password Reset" + html_content = render_to_string("password_reset_email.html", context=context) + if recipients: + msg = EmailMessage(subject, html_content, to=recipients) + msg.content_subtype = "html" + msg.send() \ No newline at end of file diff --git a/common/templates/list.html b/common/templates/list.html index c856d1c..965eb00 100644 --- a/common/templates/list.html +++ b/common/templates/list.html @@ -152,7 +152,7 @@

Change Password

{% for user in active_users %} {{ user.username }} - {{user.date_joined.date|timesince}} + {{user.created_on_arrow}} {{ user.email }} {% if user.is_superuser %} ADMIN {% else %} {{user.role}} {% endif %} diff --git a/common/templates/password_reset_email.html b/common/templates/password_reset_email.html new file mode 100644 index 0000000..70a9344 --- /dev/null +++ b/common/templates/password_reset_email.html @@ -0,0 +1,21 @@ +{% extends 'root_email_template_new.html' %} + +{% block heading %} +Hi {{email}} +{% endblock heading %} + + +{% ifequal 'activated' message %} +{% block content_body %} +You account has been activated by {{status_changed_user}}. You can login to your account and access all the features of +Django CRM. +{% endblock content_body %} +{% block button_link %} +{% if url %} + +{% endif %} +{% endblock button_link %} +{% endifequal %} diff --git a/common/utils.py b/common/utils.py index bca8ee5..4f8ba98 100644 --- a/common/utils.py +++ b/common/utils.py @@ -1,5 +1,36 @@ import pytz +from datetime import datetime +from datetime import datetime, timedelta + from django.utils.translation import ugettext_lazy as _ +from django.conf import settings +from django.forms.models import model_to_dict +from .custom_auth import BaseJSONWebTokenAuthentication +from rest_framework.authentication import get_authorization_header + + +def jwt_payload_handler(user): + """ Custom payload handler + Token encrypts the dictionary returned by this function, and can be + decoded by rest_framework_jwt.utils.jwt_decode_handler + """ + return { + 'id': user.pk, + # 'name': user.name, + 'email': user.email, + 'company': user.company.id, + "role": user.role, + "has_sales_access": user.has_sales_access, + "has_marketing_access": user.has_marketing_access, + "file_prepend": user.file_prepend, + "username": user.username, + "first_name": user.first_name, + "last_name": user.last_name, + "is_active": user.is_active, + "is_admin": user.is_admin, + "is_staff": user.is_staff, + # "date_joined" +} INDCHOICES = ( @@ -593,4 +624,9 @@ def append_str_to(append_to: str, *args, sep=", ", **kwargs): """ append_to = append_to or "" result_list = [append_to] + list(args) + list(kwargs.values()) - return f"{sep}".join(filter(len, result_list)) + data = False + for item in result_list: + if item: + data = True + break + return f"{sep}".join(filter(len, result_list)) if data else "" diff --git a/common/views.py b/common/views.py index 5b447b2..6ac8f66 100644 --- a/common/views.py +++ b/common/views.py @@ -95,13 +95,17 @@ def landing_page(request): def check_sub_domain(request): if request.method == "GET": - try: - if request.company: - return redirect("common:login") - except: - return render(request, "check_subdomain.html", {}) + # try: + # if request.company: + # return redirect("common:login") + # except: + return render(request, "check_subdomain.html", {}) if request.method == "POST": sub_domain = request.POST.get("sub_domain", "") + if sub_domain == "": + return render( + request, "check_subdomain.html", {"error": "Please mention a sub_domain"} + ) company = Company.objects.filter(sub_domain=sub_domain).first() if company: abs_url = ( @@ -112,7 +116,7 @@ def check_sub_domain(request): + settings.DOMAIN_NAME + reverse("common:login") ) - request.session["company"] = company.id + # request.session["company"] = company.id return HttpResponseRedirect(abs_url) else: return render( @@ -150,10 +154,11 @@ class HomeView(SalesAccessRequiredMixin, LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs): context = super(HomeView, self).get_context_data(**kwargs) - accounts = Account.objects.filter(status="open") - contacts = Contact.objects.all() - leads = Lead.objects.exclude(status="converted").exclude(status="closed") - opportunities = Opportunity.objects.all() + accounts = Account.objects.filter(status="open", company=self.request.company) + contacts = Contact.objects.filter(company=self.request.company) + leads = Lead.objects.filter(company=self.request.company).exclude(Q(status="converted") | + Q(status="closed")) + opportunities = Opportunity.objects.filter(company=self.request.company) if self.request.user.role == "ADMIN" or self.request.user.is_superuser: pass else: @@ -313,6 +318,7 @@ class LogoutView(LoginRequiredMixin, View): def get(self, request, *args, **kwargs): logout(request) request.session.flush() + request.company = None url = request.scheme + "://" + settings.DOMAIN_NAME + "/" return HttpResponseRedirect(url) # return redirect("common:login") @@ -389,6 +395,15 @@ class CompanyLoginView(CreateView): template_name = "company_login.html" def post(self, request, *args, **kwargs): + # add company to request and header + host_name = request.META.get("HTTP_HOST") + subdomain = host_name.split(".")[0] + company = Company.objects.filter(sub_domain=subdomain).first() + if company: + request.company = company + request.session["company"] = company.id + else: + return render(request, "check_subdomain.html", {"error": "Please mention a sub_domain"}) form = CompanyLoginForm(request.POST, request=request) if form.is_valid(): email = form.cleaned_data.get("email", "") @@ -507,7 +522,7 @@ def get_form_kwargs(self): def get_context_data(self, **kwargs): context = super(CreateUserView, self).get_context_data(**kwargs) context["user_form"] = context["form"] - context["teams"] = Teams.objects.all() + context["teams"] = Teams.objects.filter(company=self.request.company) if "errors" in kwargs: context["errors"] = kwargs["errors"] return context @@ -615,7 +630,7 @@ def get_context_data(self, **kwargs): user_profile_name = user_profile_name[-1] context["user_profile_name"] = user_profile_name context["user_form"] = context["form"] - context["teams"] = Teams.objects.all() + context["teams"] = Teams.objects.filter(company=self.request.company) if "errors" in kwargs: context["errors"] = kwargs["errors"] return context diff --git a/contacts/api_urls.py b/contacts/api_urls.py new file mode 100644 index 0000000..1b64022 --- /dev/null +++ b/contacts/api_urls.py @@ -0,0 +1,12 @@ +from django.urls import path +from contacts import api_views + +app_name = 'api_contacts' + +urlpatterns = [ + path("contacts-list/", api_views.ContactsListView.as_view()), + path("create/", api_views.CreateContactView.as_view(), name="add_contact"), + path("/view/", api_views.ContactDetailView.as_view(), name="view_contact"), + path("/edit/", api_views.UpdateContactView.as_view(), name="edit_contact"), + path("/delete/", api_views.RemoveContactView.as_view(), name="remove_contact"), +] diff --git a/contacts/api_views.py b/contacts/api_views.py new file mode 100644 index 0000000..c88d56f --- /dev/null +++ b/contacts/api_views.py @@ -0,0 +1,390 @@ +from common.models import User +from contacts.models import Contact +from teams.models import Teams +from django.db.models import Q +from django.shortcuts import get_object_or_404 +from drf_yasg.utils import swagger_auto_schema +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from common.custom_auth import JSONWebTokenAuthentication +from contacts import swagger_params +from contacts.serializer import * +from rest_framework.views import APIView +from common.utils import COUNTRIES +from common.serializer import (UserSerializer, CommentSerializer, + AttachmentsSerializer, BillingAddressSerializer) +from teams.serializer import TeamsSerializer +from tasks.serializer import TaskSerializer +from django.contrib.sites.shortcuts import get_current_site +from contacts.tasks import send_email_to_assigned_user + + +class ContactsListView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Contact + + def get_queryset(self): + queryset = self.model.objects.filter( + company=self.request.company).order_by("-created_on") + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + queryset = queryset.filter( + Q(assigned_to__in=[self.request.user]) | Q( + created_by=self.request.user) + ) + + request_post = params + if request_post: + if request_post.get("first_name"): + queryset = queryset.filter( + first_name__icontains=request_post.get("first_name") + ) + if request_post.get("city"): + queryset = queryset.filter( + address__city__icontains=request_post.get("city") + ) + if request_post.get("phone"): + queryset = queryset.filter( + phone__icontains=request_post.get("phone")) + if request_post.get("email"): + queryset = queryset.filter( + email__icontains=request_post.get("email")) + if request_post.getlist("assigned_to"): + queryset = queryset.filter( + assigned_to__id__in=request_post.getlist("assigned_to") + ) + return queryset.distinct() + + def get_context_data(self, **kwargs): + context = {} + context["contact_obj_list"] = ContactSerializer( + self.get_queryset(), many=True).data + context["per_page"] = params.get("per_page") + context["users"] = UserSerializer( + User.objects.filter(is_active=True).order_by("username"), many=True).data + context["assignedto_list"] = [ + int(i) for i in params.getlist("assigned_to", []) if i + ] + search = False + if ( + params.get("first_name") + or params.get("city") + or params.get("phone") + or params.get("email") + or params.get("assigned_to") + ): + search = True + context["search"] = search + return context + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_list_post_params) + def post(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class CreateContactView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Contact + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_create_post_params) + def post(self, request, *args, **kwargs): + self.object = None + params = request.query_params if len( + request.data) == 0 else request.data + contact_serializer = CreateContctForm(data=params) + address_serializer = BillingAddressSerializer(data=params) + data = {} + if not contact_serializer.is_valid(): + data['contact_errors'] = contact_serializer.errors + if not address_serializer.is_valid(): + data["address_errors"] = address_serializer.errors, + if data: + data['error'] = True + return Response(data) + address_obj = address_serializer.save() + contact_obj = contact_serializer.save() + contact_obj.address = address_obj + contact_obj.created_by = self.request.user + contact_obj.company = self.request.company + contact_obj.save() + if self.request.GET.get("view_account", None): + if Account.objects.filter( + id=int(self.request.GET.get("view_account")) + ).exists(): + Account.objects.get( + id=int(self.request.GET.get("view_account")) + ).contacts.add(contact_obj) + if params.getlist("assigned_to", []): + contact_obj.assigned_to.add( + *params.getlist("assigned_to")) + if params.getlist("teams", []): + user_ids = Teams.objects.filter( + id__in=params.getlist("teams") + ).values_list("users", flat=True) + assinged_to_users_ids = contact_obj.assigned_to.all().values_list( + "id", flat=True + ) + for user_id in user_ids: + if user_id not in assinged_to_users_ids: + contact_obj.assigned_to.add(user_id) + + if params.getlist("teams", []): + contact_obj.teams.add(*params.getlist("teams")) + + assigned_to_list = list( + contact_obj.assigned_to.all().values_list("id", flat=True) + ) + current_site = get_current_site(self.request) + recipients = assigned_to_list + send_email_to_assigned_user.delay( + recipients, + contact_obj.id, + domain=current_site.domain, + protocol=self.request.scheme, + ) + + if request.FILES.get("contact_attachment"): + attachment = Attachments() + attachment.created_by = request.user + attachment.file_name = request.FILES.get( + "contact_attachment").name + attachment.contact = contact_obj + attachment.attachment = request.FILES.get("contact_attachment") + attachment.save() + + return Response({'error': False}) + + def get_context_data(self, **kwargs): + params = self.request.query_params if len( + self.request.data) == 0 else request.data + context = {} + if self.request.user.role == "ADMIN" or self.request.user.is_superuser: + self.users = User.objects.filter( + is_active=True, company=self.request.company).order_by("email") + else: + self.users = User.objects.filter( + role="ADMIN", company=self.request.company).order_by("email") + context["users"] = UserSerializer(self.users, many=True).data + context["countries"] = COUNTRIES + context["assignedto_list"] = [ + int(i) for i in params.getlist("assigned_to", []) if i + ] + context["teams"] = TeamsSerializer(Teams.objects.filter( + company=self.request.company), many=True).data + return context + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class ContactDetailView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Contact + + def get_context_data(self, **kwargs): + context = {} + contact_obj = get_object_or_404(Contact, pk=kwargs['pk']) + context["contact_obj"] = ContactSerializer(contact_obj).data + user_assgn_list = [ + assigned_to.id for assigned_to in contact_obj.assigned_to.all() + ] + user_assigned_accounts = set( + self.request.user.account_assigned_users.values_list( + "id", flat=True) + ) + contact_accounts = set( + contact_obj.account_contacts.values_list("id", flat=True) + ) + if user_assigned_accounts.intersection(contact_accounts): + user_assgn_list.append(self.request.user.id) + if self.request.user == contact_obj.created_by: + user_assgn_list.append(self.request.user.id) + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if self.request.user.id not in user_assgn_list: + raise PermissionDenied + assigned_data = [] + for each in contact_obj.assigned_to.all(): + assigned_dict = {} + assigned_dict["id"] = each.id + assigned_dict["name"] = each.email + assigned_data.append(assigned_dict) + + if self.request.user.is_superuser or self.request.user.role == "ADMIN": + users_mention = list( + User.objects.filter( + is_active=True, company=self.request.company + ).values("username") + ) + elif self.request.user != contact_obj.created_by: + users_mention = [ + {"username": contact_obj.created_by.username}] + else: + users_mention = list( + contact_obj.assigned_to.all().values("username")) + + context.update( + { + "comments": CommentSerializer(contact_obj.contact_comments.all(), many=True).data, + "attachments": AttachmentsSerializer(contact_obj.contact_attachment.all(), many=True).data, + "assigned_data": assigned_data, + "tasks": TaskSerializer(contact_obj.contacts_tasks.all(), many=True).data, + "users_mention": users_mention, + } + ) + return context + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_detail_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class UpdateContactView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + model = Contact + + def dispatch(self, request, *args, **kwargs): + self.contact = get_object_or_404(Contact, pk=kwargs.get('pk')) + return super(UpdateContactView, self).dispatch(request, *args, **kwargs) + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_create_post_params) + def post(self, request, *args, **kwargs): + params = request.query_params if len( + request.data) == 0 else request.data + self.object = get_object_or_404(Contact, pk=kwargs.get('pk')) + address_obj = self.contact.address + contact_serializer = CreateContctForm( + data=params, instance=self.contact) + address_serializer = BillingAddressSerializer( + data=params, instance=address_obj) + data = {} + if not contact_serializer.is_valid(): + data['contact_errors'] = contact_serializer.errors + if not address_serializer.is_valid(): + data["address_errors"] = address_serializer.errors, + if data: + data['error'] = True + return Response(data) + addres_obj = address_serializer.save() + contact_obj = contact_serializer.save() + contact_obj.address = addres_obj + contact_obj.save() + + previous_assigned_to_users = list( + contact_obj.assigned_to.all().values_list("id", flat=True) + ) + if params.getlist("assigned_to", []): + current_site = get_current_site(request) + + contact_obj.assigned_to.clear() + contact_obj.assigned_to.add( + *params.getlist("assigned_to")) + else: + contact_obj.assigned_to.clear() + + if params.getlist("teams", []): + user_ids = Teams.objects.filter( + id__in=params.getlist("teams") + ).values_list("users", flat=True) + assinged_to_users_ids = contact_obj.assigned_to.all().values_list( + "id", flat=True + ) + for user_id in user_ids: + if user_id not in assinged_to_users_ids: + contact_obj.assigned_to.add(user_id) + + if params.getlist("teams", []): + contact_obj.teams.clear() + contact_obj.teams.add(*params.getlist("teams")) + else: + contact_obj.teams.clear() + + current_site = get_current_site(request) + assigned_to_list = list( + contact_obj.assigned_to.all().values_list("id", flat=True) + ) + recipients = list(set(assigned_to_list) - + set(previous_assigned_to_users)) + send_email_to_assigned_user.delay( + recipients, + contact_obj.id, + domain=current_site.domain, + protocol=request.scheme, + ) + if request.FILES.get("contact_attachment"): + attachment = Attachments() + attachment.created_by = request.user + attachment.file_name = request.FILES.get( + "contact_attachment").name + attachment.contact = contact_obj + attachment.attachment = request.FILES.get( + "contact_attachment") + attachment.save() + return Response({'error': False}) + + def get_context_data(self, **kwargs): + params = self.request.query_params if len( + self.request.data) == 0 else request.data + context = {} + context["contact_obj"] = ContactSerializer(self.contact).data + user_assgn_list = [ + assigned_to.id for assigned_to in self.contact.assigned_to.all() + ] + if self.request.user.role == "ADMIN" or self.request.user.is_superuser: + self.users = User.objects.filter( + is_active=True, company=self.request.company).order_by("email") + else: + self.users = User.objects.filter( + role="ADMIN", company=self.request.company).order_by("email") + if self.request.user == self.contact.created_by: + user_assgn_list.append(self.request.user.id) + if self.request.user.role != "ADMIN" and not self.request.user.is_superuser: + if self.request.user.id not in user_assgn_list: + raise PermissionDenied + context["address_obj"] = BillingAddressSerializer( + self.contact.address).data + context["users"] = UserSerializer(self.users, many=True).data + context["countries"] = COUNTRIES + context["teams"] = TeamsSerializer( + Teams.objects.filter(company=self.request.company), many=True).data + context["assignedto_list"] = [ + int(i) for i in params.getlist("assigned_to", []) if i + ] + return context + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_list_get_params) + def get(self, request, *args, **kwargs): + context = self.get_context_data(**kwargs) + return Response(context) + + +class RemoveContactView(APIView): + authentication_classes = (JSONWebTokenAuthentication,) + permission_classes = (IsAuthenticated,) + + @swagger_auto_schema(tags=["contacts"], manual_parameters=swagger_params.contact_delete_get_params) + def get(self, request, *args, **kwargs): + contact_id = kwargs.get("pk") + self.object = get_object_or_404(Contact, id=contact_id) + if ( + self.request.user.role != "ADMIN" + and not self.request.user.is_superuser + and self.request.user != self.object.created_by + ) or self.object.company != self.request.company: + raise PermissionDenied + else: + if self.object.address_id: + self.object.address.delete() + self.object.delete() + return Response({"error": False}) diff --git a/contacts/forms.py b/contacts/forms.py index 04dc4d6..913f3f2 100644 --- a/contacts/forms.py +++ b/contacts/forms.py @@ -20,7 +20,7 @@ def __init__(self, *args, **kwargs): for key, value in self.fields.items(): if key == "phone": - value.widget.attrs["placeholder"] = "+911234567890" + pass else: value.widget.attrs["placeholder"] = value.label self.fields["teams"].choices = [ diff --git a/contacts/serializer.py b/contacts/serializer.py new file mode 100644 index 0000000..ab696c3 --- /dev/null +++ b/contacts/serializer.py @@ -0,0 +1,51 @@ +from rest_framework import serializers +from contacts.models import Contact +from common.serializer import UserSerializer, CompanySerializer, BillingAddressSerializer +from teams.serializer import TeamsSerializer + + +class ContactSerializer(serializers.ModelSerializer): + created_by = UserSerializer() + company = CompanySerializer() + teams = TeamsSerializer(read_only=True, many=True) + assigned_to = UserSerializer(read_only=True, many=True) + address = BillingAddressSerializer(read_only=True) + get_team_users = UserSerializer(read_only=True, many=True) + get_team_and_assigned_users = UserSerializer(read_only=True, many=True) + get_assigned_users_not_in_teams = UserSerializer(read_only=True, many=True) + + class Meta: + model = Contact + fields = ( + 'id', + "first_name", + "last_name", + "email", + "phone", + "address", + "description", + "assigned_to", + "created_by", + "created_on", + "is_active", + "teams", + "company", + "created_on_arrow", + "get_team_users", + "get_team_and_assigned_users", + "get_assigned_users_not_in_teams", + ) + + +class CreateContctForm(serializers.ModelSerializer): + + class Meta: + model = Contact + fields = ( + "first_name", + "last_name", + "email", + "phone", + "address", + "description", + ) diff --git a/contacts/swagger_params.py b/contacts/swagger_params.py new file mode 100644 index 0000000..5b3c18e --- /dev/null +++ b/contacts/swagger_params.py @@ -0,0 +1,31 @@ +from drf_yasg import openapi + +company_params_in_header = openapi.Parameter( + 'company', openapi.IN_HEADER, required=True, type=openapi.TYPE_STRING) + + +contact_list_get_params = [company_params_in_header] + +contact_list_post_params = [company_params_in_header] + +contact_detail_get_params = [company_params_in_header] + +contact_delete_get_params = [company_params_in_header] + +contact_create_post_params = [ + company_params_in_header, + openapi.Parameter('first_name', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('last_name', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('phone', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('email', openapi.IN_QUERY, required=True, type=openapi.TYPE_STRING), + openapi.Parameter('teams', openapi.IN_QUERY, type=openapi.TYPE_INTEGER), + openapi.Parameter('contact_attachment', openapi.IN_QUERY, type=openapi.TYPE_FILE), + openapi.Parameter('description', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('address_line', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('street', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('city', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('state', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('postcode', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('country', openapi.IN_QUERY, type=openapi.TYPE_STRING), + openapi.Parameter('assigned_to', openapi.IN_QUERY, type=openapi.TYPE_INTEGER), +] diff --git a/contacts/templates/create_contact.html b/contacts/templates/create_contact.html index 8e40c08..adf11bd 100644 --- a/contacts/templates/create_contact.html +++ b/contacts/templates/create_contact.html @@ -1,6 +1,7 @@ {% extends 'sales/base.html' %} {% load static %} {% block breadcrumb %} +