· 5 years ago · Feb 27, 2020, 04:06 PM
1from __future__ import absolute_import, unicode_literals
2
3import itertools
4import json
5import logging
6import plivo
7import nexmo
8import regex
9import six
10import requests
11
12from collections import OrderedDict
13from datetime import datetime
14from decimal import Decimal
15from django import forms
16from django.conf import settings
17from django.contrib import messages
18from django.contrib.auth import authenticate, login, logout
19from django.contrib.auth.models import User, Group
20from django.core.exceptions import ValidationError
21from django.core.urlresolvers import reverse
22from django.core.validators import validate_email
23from django.core.files.base import ContentFile
24from django.db import IntegrityError
25from django.db.models import Sum, Q, F, ExpressionWrapper, IntegerField
26from django.forms import Form
27from django.http import HttpResponse, HttpResponseRedirect, JsonResponse
28from django.utils import timezone
29from django.utils.http import urlquote
30from django.utils.safestring import mark_safe
31from django.utils.text import slugify
32from django.utils.translation import ugettext_lazy as _
33from django.views.decorators.csrf import csrf_exempt
34from django.views.generic import View
35from email.utils import parseaddr
36from functools import cmp_to_key
37from smartmin.views import SmartCRUDL, SmartCreateView, SmartFormView, SmartReadView, SmartUpdateView, SmartListView, SmartTemplateView
38from smartmin.views import SmartModelFormView, SmartModelActionView
39from datetime import timedelta
40from temba.api.models import APIToken
41from temba.campaigns.models import Campaign
42from temba.channels.models import Channel
43from temba.flows.models import Flow, RuleSet
44from temba.links.models import Link
45from temba.formax import FormaxMixin
46from temba.utils import analytics, languages
47from temba.utils.timezones import TimeZoneFormField
48from temba.utils.email import is_valid_address
49from twilio.rest import TwilioRestClient
50from simple_salesforce import Salesforce
51from .models import Org, OrgCache, OrgEvent, TopUp, Invitation, UserSettings, get_stripe_credentials, ACCOUNT_SID, \
52 ACCOUNT_TOKEN
53from .models import MT_SMS_EVENTS, MO_SMS_EVENTS, MT_CALL_EVENTS, MO_CALL_EVENTS, ALARM_EVENTS
54from .models import SUSPENDED, WHITELISTED, RESTORED, NEXMO_UUID, NEXMO_SECRET, NEXMO_KEY
55from .models import TRANSFERTO_AIRTIME_API_TOKEN, TRANSFERTO_ACCOUNT_LOGIN, SMTP_FROM_EMAIL
56from .models import SMTP_HOST, SMTP_USERNAME, SMTP_PASSWORD, SMTP_PORT, SMTP_ENCRYPTION
57from .models import CHATBASE_API_KEY, CHATBASE_VERSION, CHATBASE_AGENT_NAME
58from .models import GIFTCARDS, LOOKUPS, DEFAULT_FIELDS_PAYLOAD_GIFTCARDS, DEFAULT_INDEXES_FIELDS_PAYLOAD_GIFTCARDS
59from .models import DEFAULT_FIELDS_PAYLOAD_LOOKUPS, DEFAULT_INDEXES_FIELDS_PAYLOAD_LOOKUPS
60from .models import SF_INSTANCE_URL
61
62
63def check_login(request):
64 """
65 Simple view that checks whether we actually need to log in. This is needed on the live site
66 because we serve the main page as http:// but the logged in pages as https:// and only store
67 the cookies on the SSL connection. This view will be called in https:// land where we will
68 check whether we are logged in, if so then we will redirect to the LOGIN_URL, otherwise we take
69 them to the normal user login page
70 """
71 if request.user.is_authenticated():
72 return HttpResponseRedirect(settings.LOGIN_REDIRECT_URL)
73 else:
74 return HttpResponseRedirect(settings.LOGIN_URL)
75
76
77class OrgPermsMixin(object):
78 """
79 Get the organisation and the user within the inheriting view so that it be come easy to decide
80 whether this user has a certain permission for that particular organization to perform the view's actions
81 """
82 def get_user(self):
83 return self.request.user
84
85 def derive_org(self):
86 org = None
87 if not self.get_user().is_anonymous():
88 org = self.get_user().get_org()
89 return org
90
91 def pre_process(self, request, *args, **kwargs):
92 user = self.get_user()
93 org = self.derive_org()
94
95 if not org: # pragma: needs cover
96 if user.is_authenticated():
97 if user.is_superuser or user.is_staff:
98 return None
99
100 return HttpResponseRedirect(reverse('orgs.org_choose'))
101 else:
102 return HttpResponseRedirect(settings.LOGIN_URL)
103
104 return self.has_permission_view_objects()
105
106 def has_org_perm(self, permission):
107 if self.org:
108 return self.get_user().has_org_perm(self.org, permission)
109 return False
110
111 def has_permission(self, request, *args, **kwargs):
112 """
113 Figures out if the current user has permissions for this view.
114 """
115 self.kwargs = kwargs
116 self.args = args
117 self.request = request
118 self.org = self.derive_org()
119
120 if self.get_user().is_superuser:
121 return True
122
123 if self.get_user().is_anonymous():
124 return False
125
126 if self.get_user().has_perm(self.permission): # pragma: needs cover
127 return True
128
129 return self.has_org_perm(self.permission)
130
131 def has_permission_view_objects(self):
132 return None
133
134
135class AnonMixin(OrgPermsMixin):
136 """
137 Mixin that makes sure that anonymous orgs cannot add channels (have no permission if anon)
138 """
139 def has_permission(self, request, *args, **kwargs):
140 org = self.derive_org()
141
142 # can this user break anonymity? then we are fine
143 if self.get_user().has_perm('contacts.contact_break_anon'):
144 return True
145
146 # otherwise if this org is anon, no go
147 if not org or org.is_anon:
148 return False
149 else:
150 return super(AnonMixin, self).has_permission(request, *args, **kwargs)
151
152
153class OrgObjPermsMixin(OrgPermsMixin):
154
155 def get_object_org(self):
156 return self.get_object().org
157
158 def has_org_perm(self, codename):
159 has_org_perm = super(OrgObjPermsMixin, self).has_org_perm(codename)
160
161 if has_org_perm:
162 user = self.get_user()
163 return user.get_org() == self.get_object_org()
164
165 return False
166
167 def has_permission(self, request, *args, **kwargs):
168 has_perm = super(OrgObjPermsMixin, self).has_permission(request, *args, **kwargs)
169
170 if has_perm:
171 user = self.get_user()
172
173 # user has global permission
174 if user.has_perm(self.permission):
175 return True
176
177 return user.get_org() == self.get_object_org()
178
179 return False
180
181
182class ModalMixin(SmartFormView):
183
184 def get_context_data(self, **kwargs):
185 context = super(ModalMixin, self).get_context_data(**kwargs)
186
187 if 'HTTP_X_PJAX' in self.request.META and 'HTTP_X_FORMAX' not in self.request.META: # pragma: no cover
188 context['base_template'] = "smartmin/modal.html"
189 if 'success_url' in kwargs: # pragma: no cover
190 context['success_url'] = kwargs['success_url']
191
192 pairs = [urlquote(k) + "=" + urlquote(v) for k, v in six.iteritems(self.request.GET) if k != '_']
193 context['action_url'] = self.request.path + "?" + ("&".join(pairs))
194
195 return context
196
197 def form_valid(self, form):
198 if isinstance(form, forms.ModelForm):
199 self.object = form.save(commit=False)
200
201 try:
202 if isinstance(self, SmartModelFormView):
203 self.object = self.pre_save(self.object)
204 self.save(self.object)
205 self.object = self.post_save(self.object)
206
207 elif isinstance(self, SmartModelActionView):
208 self.execute_action()
209
210 messages.success(self.request, self.derive_success_message())
211
212 if 'HTTP_X_PJAX' not in self.request.META:
213 return HttpResponseRedirect(self.get_success_url())
214 else: # pragma: no cover
215 response = self.render_to_response(self.get_context_data(form=form,
216 success_url=self.get_success_url(),
217 success_script=getattr(self, 'success_script', None)))
218 response['Temba-Success'] = self.get_success_url()
219 return response
220
221 except (IntegrityError, ValueError, ValidationError) as e:
222 message = getattr(e, 'message', str(e).capitalize())
223 self.form.add_error(None, message)
224 return self.render_to_response(self.get_context_data(form=form))
225
226
227class OrgSignupForm(forms.ModelForm):
228 """
229 Signup for new organizations
230 """
231 first_name = forms.CharField(help_text=_("Your first name"))
232 last_name = forms.CharField(help_text=_("Your last name"))
233 email = forms.EmailField(help_text=_("Your email address"))
234 timezone = TimeZoneFormField(help_text=_("The timezone your organization is in"))
235 password = forms.CharField(widget=forms.PasswordInput,
236 help_text=_("Your password, at least eight letters please"))
237 name = forms.CharField(label=_("Organization"),
238 help_text=_("The name of your organization"))
239
240 def __init__(self, *args, **kwargs):
241 if 'branding' in kwargs:
242 del kwargs['branding']
243
244 super(OrgSignupForm, self).__init__(*args, **kwargs)
245
246 def clean_email(self):
247 email = self.cleaned_data['email']
248 if email:
249 if User.objects.filter(username__iexact=email):
250 raise forms.ValidationError(_("That email address is already used"))
251
252 return email.lower()
253
254 def clean_password(self):
255 password = self.cleaned_data['password']
256 if password:
257 if not len(password) >= 8:
258 raise forms.ValidationError(_("Passwords must contain at least 8 letters."))
259 return password
260
261 class Meta:
262 model = Org
263 fields = '__all__'
264
265
266class OrgGrantForm(forms.ModelForm):
267 first_name = forms.CharField(help_text=_("The first name of the organization administrator"))
268 last_name = forms.CharField(help_text=_("Your last name of the organization administrator"))
269 email = forms.EmailField(help_text=_("Their email address"))
270 timezone = TimeZoneFormField(help_text=_("The timezone the organization is in"))
271 password = forms.CharField(widget=forms.PasswordInput, required=False,
272 help_text=_("Their password, at least eight letters please. (leave blank for existing users)"))
273 name = forms.CharField(label=_("Organization"),
274 help_text=_("The name of the new organization"))
275 credits = forms.ChoiceField([], help_text=_("The initial number of credits granted to this organization."))
276
277 def __init__(self, *args, **kwargs):
278 branding = kwargs['branding']
279 del kwargs['branding']
280
281 super(OrgGrantForm, self).__init__(*args, **kwargs)
282
283 welcome_packs = branding['welcome_packs']
284
285 choices = []
286 for pack in welcome_packs:
287 choices.append((str(pack['size']), "%d - %s" % (pack['size'], pack['name'])))
288
289 self.fields['credits'].choices = choices
290
291 def clean(self):
292 data = self.cleaned_data
293
294 email = data.get('email', None)
295 password = data.get('password', None)
296
297 # for granting new accounts, either the email maps to an existing user (and their existing password is used)
298 # or both email and password must be included
299 if email:
300 user = User.objects.filter(username__iexact=email).first()
301 if user: # pragma: needs cover
302 if password:
303 raise ValidationError(_("User already exists, please do not include password."))
304
305 elif not password or len(password) < 8: # pragma: needs cover
306 raise ValidationError(_("Password must be at least 8 characters long"))
307
308 return data
309
310 class Meta:
311 model = Org
312 fields = '__all__'
313
314
315class GiftcardsForm(forms.ModelForm):
316 collection = forms.CharField(required=False, label=_("New Collection"),
317 max_length=30,
318 help_text="Enter a name for your collection. ex: my gifts, new lookup table")
319 remove = forms.CharField(widget=forms.HiddenInput, max_length=6, required=False)
320 index = forms.CharField(widget=forms.HiddenInput, max_length=10, required=False)
321
322 def add_collection_fields(self, collection_type):
323 collections = []
324
325 for collection in self.instance.get_collections(collection_type=collection_type):
326 collections.append(dict(collection=collection))
327
328 self.fields = OrderedDict(self.fields.items())
329 return collections
330
331 def clean_collection(self):
332 new_collection = self.data.get('collection')
333
334 if new_collection in self.instance.get_collections(collection_type=OrgCRUDL.Giftcards.collection_type):
335 raise ValidationError("This collection name has already been used")
336
337 return new_collection[:30] if new_collection else None
338
339 class Meta:
340 model = Org
341 fields = ('id', 'collection', 'remove', 'index')
342
343
344class ChoiceFieldNoValidation(forms.ChoiceField):
345 def validate(self, value):
346 pass
347
348
349class UserCRUDL(SmartCRUDL):
350 model = User
351 actions = ('edit',)
352
353 class Edit(SmartUpdateView):
354 class EditForm(forms.ModelForm):
355 first_name = forms.CharField(label=_("Your First Name (required)"))
356 last_name = forms.CharField(label=_("Your Last Name (required)"))
357 email = forms.EmailField(required=True, label=_("Email"))
358 current_password = forms.CharField(label=_("Current Password (required)"), widget=forms.PasswordInput)
359 new_password = forms.CharField(required=False, label=_("New Password (optional)"), widget=forms.PasswordInput)
360 language = forms.ChoiceField(choices=settings.LANGUAGES, required=True, label=_("Website Language"))
361
362 def clean_new_password(self):
363 password = self.cleaned_data['new_password']
364 if password and not len(password) >= 8:
365 raise forms.ValidationError(_("Passwords must have at least 8 letters."))
366 return password
367
368 def clean_current_password(self):
369 user = self.instance
370 password = self.cleaned_data.get('current_password', None)
371
372 if not user.check_password(password):
373 raise forms.ValidationError(_("Please enter your password to save changes."))
374
375 return password
376
377 def clean_email(self):
378 user = self.instance
379 email = self.cleaned_data['email'].lower()
380
381 if User.objects.filter(username=email).exclude(pk=user.pk):
382 raise forms.ValidationError(_("Sorry, that email address is already taken."))
383
384 return email
385
386 class Meta:
387 model = User
388 fields = ('first_name', 'last_name', 'email', 'current_password', 'new_password', 'language')
389
390 form_class = EditForm
391 permission = 'orgs.org_profile'
392 success_url = '@orgs.org_home'
393 success_message = ''
394
395 @classmethod
396 def derive_url_pattern(cls, path, action):
397 return r'^%s/%s/$' % (path, action)
398
399 def get_object(self, *args, **kwargs):
400 return self.request.user
401
402 def derive_initial(self):
403 initial = super(UserCRUDL.Edit, self).derive_initial()
404 initial['language'] = self.get_object().get_settings().language
405 return initial
406
407 def pre_save(self, obj):
408 obj = super(UserCRUDL.Edit, self).pre_save(obj)
409
410 # keep our username and email in sync
411 obj.username = obj.email
412
413 if self.form.cleaned_data['new_password']:
414 obj.set_password(self.form.cleaned_data['new_password'])
415
416 return obj
417
418 def post_save(self, obj):
419 # save the user settings as well
420 obj = super(UserCRUDL.Edit, self).post_save(obj)
421 user_settings = obj.get_settings()
422 user_settings.language = self.form.cleaned_data['language']
423 user_settings.save()
424 return obj
425
426 def has_permission(self, request, *args, **kwargs):
427 user = self.request.user
428
429 if user.is_anonymous():
430 return False
431
432 org = user.get_org()
433
434 if org:
435 org_users = org.administrators.all() | org.editors.all() | org.viewers.all() | org.surveyors.all()
436
437 if not user.is_authenticated(): # pragma: needs cover
438 return False
439
440 if user in org_users:
441 return True
442
443 return False # pragma: needs cover
444
445
446class InferOrgMixin(object):
447 @classmethod
448 def derive_url_pattern(cls, path, action):
449 return r'^%s/%s/$' % (path, action)
450
451 def get_object(self, *args, **kwargs):
452 return self.request.user.get_org()
453
454
455class PhoneRequiredForm(forms.ModelForm):
456 tel = forms.CharField(max_length=15, label="Phone Number", required=True)
457
458 def clean_tel(self):
459 if 'tel' in self.cleaned_data:
460 tel = self.cleaned_data['tel']
461 if not tel: # pragma: needs cover
462 return tel
463
464 import phonenumbers
465 try:
466 normalized = phonenumbers.parse(tel, None)
467 if not phonenumbers.is_possible_number(normalized): # pragma: needs cover
468 raise forms.ValidationError(_("Invalid phone number, try again."))
469 except Exception: # pragma: no cover
470 raise forms.ValidationError(_("Invalid phone number, try again."))
471 return phonenumbers.format_number(normalized, phonenumbers.PhoneNumberFormat.E164)
472
473 class Meta:
474 model = UserSettings
475 fields = ('tel',)
476
477
478class UserSettingsCRUDL(SmartCRUDL):
479 actions = ('update', 'phone')
480 model = UserSettings
481
482 class Phone(ModalMixin, OrgPermsMixin, SmartUpdateView):
483
484 @classmethod
485 def derive_url_pattern(cls, path, action):
486 return r'^%s/%s/$' % (path, action)
487
488 def get_object(self, *args, **kwargs):
489 return self.request.user.get_settings()
490
491 fields = ('tel',)
492 form_class = PhoneRequiredForm
493 submit_button_name = _("Start Call")
494 success_url = '@orgs.usersettings_phone'
495
496
497class OrgCRUDL(SmartCRUDL):
498 actions = ('signup', 'home', 'webhook', 'edit', 'edit_sub_org', 'join', 'grant', 'accounts', 'create_login',
499 'chatbase', 'choose', 'manage_accounts', 'manage_accounts_sub_org', 'manage', 'update', 'country',
500 'languages', 'clear_cache', 'twilio_connect', 'twilio_account', 'nexmo_configuration', 'nexmo_account',
501 'nexmo_connect', 'sub_orgs', 'create_sub_org', 'export', 'import', 'plivo_connect', 'resthooks',
502 'service', 'surveyor', 'transfer_credits', 'transfer_to_account', 'smtp_server', 'salesforce', 'resend_invitation',
503 'giftcards', 'lookups', 'import_parse_data', 'parse_data_view')
504
505 model = Org
506
507 class Import(InferOrgMixin, OrgPermsMixin, SmartFormView):
508
509 class FlowImportForm(Form):
510 import_file = forms.FileField(help_text=_('The import file'))
511 update = forms.BooleanField(help_text=_('Update all flows and campaigns'), required=False)
512
513 def __init__(self, *args, **kwargs):
514 self.org = kwargs['org']
515 del kwargs['org']
516 super(OrgCRUDL.Import.FlowImportForm, self).__init__(*args, **kwargs)
517
518 def clean_import_file(self):
519 from temba.orgs.models import EARLIEST_IMPORT_VERSION
520
521 # make sure they are in the proper tier
522 if not self.org.is_import_flows_tier():
523 raise ValidationError("Sorry, import is a premium feature")
524
525 # check that it isn't too old
526 data = self.cleaned_data['import_file'].read()
527 json_data = json.loads(data)
528 if Flow.is_before_version(json_data.get('version', 0), EARLIEST_IMPORT_VERSION):
529 raise ValidationError('This file is no longer valid. Please export a new version and try again.')
530
531 return data
532
533 success_message = _("Import successful")
534 form_class = FlowImportForm
535
536 def get_success_url(self): # pragma: needs cover
537 return reverse('orgs.org_home')
538
539 def get_form_kwargs(self):
540 kwargs = super(OrgCRUDL.Import, self).get_form_kwargs()
541 kwargs['org'] = self.request.user.get_org()
542 return kwargs
543
544 def form_valid(self, form):
545 try:
546 org = self.request.user.get_org()
547 data = json.loads(form.cleaned_data['import_file'])
548 org.import_app(data, self.request.user, self.request.branding['link'])
549 except Exception as e:
550 # this is an unexpected error, report it to sentry
551 logger = logging.getLogger(__name__)
552 logger.error('Exception on app import: %s' % six.text_type(e), exc_info=True)
553 form._errors['import_file'] = form.error_class([_("Sorry, your import file is invalid.")])
554 return self.form_invalid(form)
555
556 return super(OrgCRUDL.Import, self).form_valid(form) # pragma: needs cover
557
558 class ImportParseData(InferOrgMixin, OrgPermsMixin, SmartFormView):
559
560 class ImportParseDataForm(Form):
561
562 COLLECTION_TYPE = (
563 ('giftcard', 'Giftcard'),
564 ('lookup', 'Lookup'),
565 )
566
567 collection_type = forms.CharField(label=_('Select the type of database you want to upload your file'),
568 widget=forms.Select(attrs={'onchange': 'updateCollectionType($(this))'}, choices=COLLECTION_TYPE), required=True)
569 collection = ChoiceFieldNoValidation(label=_('Select the database you want to upload your file'), required=True)
570 import_file = forms.FileField(help_text=_('The import file'))
571
572 def __init__(self, *args, **kwargs):
573 self.org = kwargs['org']
574 del kwargs['org']
575
576 super(OrgCRUDL.ImportParseData.ImportParseDataForm, self).__init__(*args, **kwargs)
577
578 config = self.org.config_json()
579 collections = []
580 for item in config.get(GIFTCARDS, []):
581 full_name = OrgCRUDL.Giftcards.get_collection_full_name(self.org.slug, self.org.id, item, GIFTCARDS.lower())
582 collections.append((full_name, item))
583 self.fields['collection'].choices = collections
584
585 def clean_collection(self):
586 if not self.cleaned_data['collection']:
587 raise forms.ValidationError(_('This field is required.'))
588
589 return self.cleaned_data['collection']
590
591 def clean_import_file(self):
592 # Max file size something around 150MB
593 max_file_size = 157286400
594
595 if not regex.match(r'^[A-Za-z0-9_.\-*() ]+$', self.cleaned_data['import_file'].name, regex.V0):
596 raise forms.ValidationError('Please make sure the file name only contains '
597 'alphanumeric characters [0-9a-zA-Z] and '
598 'special characters in -, _, ., (, )')
599
600 collection_type = self.cleaned_data.get('collection_type')
601
602 if not self.cleaned_data['import_file'].name.endswith(('.csv', '.xls', '.xlsx')):
603 raise forms.ValidationError(_('The file must be a CSV or XLS.'))
604
605 if self.cleaned_data['import_file'].size > max_file_size:
606 raise forms.ValidationError(_('Your file exceeds the 150MB file limit. Please submit a support request if you need to upload a file 150MB or larger.'))
607
608 try:
609 needed_check = True if collection_type == 'giftcard' else False
610 Org.get_parse_import_file_headers(ContentFile(self.cleaned_data['import_file'].read()), self.org,
611 needed_check=needed_check)
612 except Exception as e:
613 raise forms.ValidationError(str(e))
614
615 return self.cleaned_data['import_file']
616
617 form_class = ImportParseDataForm
618
619 def get_context_data(self, **kwargs):
620 context = super(OrgCRUDL.ImportParseData, self).get_context_data(**kwargs)
621 org = self.request.user.get_org()
622 context['dayfirst'] = org.get_dayfirst()
623 return context
624
625 def get_success_url(self): # pragma: needs cover
626 return reverse('orgs.org_import_parse_data')
627
628 def derive_success_message(self):
629 user = self.get_user()
630 success_message = _('We are preparing your import. We will e-mail you at %s when it is ready.' % user.email)
631 return success_message
632
633 def get_form_kwargs(self):
634 kwargs = super(OrgCRUDL.ImportParseData, self).get_form_kwargs()
635 kwargs['org'] = self.request.user.get_org()
636 return kwargs
637
638 def form_valid(self, form):
639 import csv
640 from pyexcel_xls import get_data
641 from .tasks import import_data_to_parse
642
643 org = self.request.user.get_org()
644 user = self.get_user()
645
646 try:
647 import_file = form.cleaned_data['import_file']
648 collection_type = form.cleaned_data['collection_type']
649 collection = form.cleaned_data['collection']
650
651 if import_file.name.endswith('.csv'):
652 file_type = 'csv'
653 elif import_file.name.endswith(('.xls', '.xlsx')):
654 file_type = 'xls'
655 else:
656 raise Exception
657
658 parse_headers = {
659 'X-Parse-Application-Id': settings.PARSE_APP_ID,
660 'X-Parse-Master-Key': settings.PARSE_MASTER_KEY,
661 'Content-Type': 'application/json'
662 }
663
664 needed_create_header = False
665
666 parse_url = '%s/schemas/%s' % (settings.PARSE_URL, collection)
667
668 config = self.org.config_json()
669 collection_real_name = None
670
671 if collection_type == 'lookup':
672 needed_create_header = True
673
674 response = requests.get(parse_url, headers=parse_headers)
675 if response.status_code == 200 and 'fields' in response.json():
676 fields = response.json().get('fields')
677
678 for key in fields.keys():
679 if key in ['objectId', 'updatedAt', 'createdAt', 'ACL']:
680 del fields[key]
681 else:
682 del fields[key]['type']
683 fields[key]['__op'] = 'Delete'
684
685 remove_fields = {
686 "className": collection,
687 "fields": fields
688 }
689
690 purge_url = '%s/purge/%s' % (settings.PARSE_URL, collection)
691 response_purge = requests.delete(purge_url, headers=parse_headers)
692
693 if response_purge.status_code in [200, 404]:
694 requests.put(parse_url, data=json.dumps(remove_fields), headers=parse_headers)
695
696 for item in config.get(LOOKUPS, []):
697 full_name = OrgCRUDL.Giftcards.get_collection_full_name(org.slug, org.id, item, LOOKUPS.lower())
698 if full_name == collection:
699 collection_real_name = item
700 break
701
702 else:
703 purge_url = '%s/purge/%s' % (settings.PARSE_URL, collection)
704 requests.delete(purge_url, headers=parse_headers)
705
706 for item in config.get(GIFTCARDS, []):
707 full_name = OrgCRUDL.Giftcards.get_collection_full_name(org.slug, org.id, item, GIFTCARDS.lower())
708 if full_name == collection:
709 collection_real_name = item
710 break
711
712 if file_type == 'csv':
713 spamreader = csv.reader(import_file, delimiter=str(','))
714 else:
715 data = get_data(import_file)
716 spamreader = None
717 if data:
718 for item in data:
719 spamreader = data[item]
720 break
721
722 if spamreader:
723 import_data_to_parse.delay(org.get_branding(), user.email, list(spamreader), parse_url, parse_headers, collection, collection_type.title(), collection_real_name, import_file.name, needed_create_header, org.timezone.zone, org.get_dayfirst())
724
725 except Exception as e:
726 # this is an unexpected error, report it to sentry
727 logger = logging.getLogger(__name__)
728 logger.error('Exception on app import: %s' % six.text_type(e), exc_info=True)
729 form._errors['import_file'] = form.error_class([_("Sorry, your file is invalid. In addition, the file must be a CSV or XLS")])
730 return self.form_invalid(form)
731
732 return super(OrgCRUDL.ImportParseData, self).form_valid(form) # pragma: needs cover
733
734 class Export(InferOrgMixin, OrgPermsMixin, SmartTemplateView):
735
736 def post(self, request, *args, **kwargs):
737 org = self.get_object()
738
739 # fetch the selected flows and campaigns
740 flows = Flow.objects.filter(id__in=self.request.POST.getlist('flows'), org=org, is_active=True)
741 campaigns = Campaign.objects.filter(id__in=self.request.POST.getlist('campaigns'), org=org, is_active=True)
742
743 shorten_url_rulesets = RuleSet.objects.filter(flow__id__in=[flow.id for flow in flows], ruleset_type=RuleSet.TYPE_SHORTEN_URL).only('config').order_by('id')
744 links = []
745 if shorten_url_rulesets:
746 links_uuid = [item.config_json()[RuleSet.TYPE_SHORTEN_URL]['id'] for item in shorten_url_rulesets]
747 links = Link.objects.filter(uuid__in=links_uuid)
748
749 components = set(itertools.chain(flows, campaigns, links))
750
751 # add triggers for the selected flows
752 for flow in flows:
753 components.update(flow.triggers.filter(is_active=True, is_archived=False))
754
755 export = org.export_definitions(request.branding['link'], components)
756 response = JsonResponse(export, json_dumps_params=dict(indent=2))
757 response['Content-Disposition'] = 'attachment; filename=%s.json' % slugify(org.name)
758 return response
759
760 def get_context_data(self, **kwargs):
761 context = super(OrgCRUDL.Export, self).get_context_data(**kwargs)
762
763 org = self.get_object()
764 include_archived = bool(int(self.request.GET.get('archived', 0)))
765
766 buckets, singles = self.generate_export_buckets(org, include_archived)
767
768 context['archived'] = include_archived
769 context['buckets'] = buckets
770 context['singles'] = singles
771 return context
772
773 def generate_export_buckets(self, org, include_archived):
774 """
775 Generates a set of buckets of related exportable flows and campaigns
776 """
777 dependencies = org.generate_dependency_graph(include_archived=include_archived)
778
779 unbucketed = set(dependencies.keys())
780 buckets = []
781
782 # helper method to add a component and its dependencies to a bucket
783 def collect_component(c, bucket):
784 if c in bucket: # pragma: no cover
785 return
786
787 unbucketed.remove(c)
788 bucket.add(c)
789
790 for d in dependencies[c]:
791 if d in unbucketed:
792 collect_component(d, bucket)
793
794 while unbucketed:
795 component = next(iter(unbucketed))
796
797 bucket = set()
798 buckets.append(bucket)
799
800 collect_component(component, bucket)
801
802 # collections with only one non-group component should be merged into a single "everything else" collection
803 non_single_buckets = []
804 singles = set()
805
806 # items within buckets are sorted by type and name
807 def sort_key(c):
808 return c.__class__, c.name.lower()
809
810 # buckets with a single item are merged into a special singles bucket
811 for b in buckets:
812 if len(b) > 1:
813 sorted_bucket = sorted(list(b), key=sort_key)
814 non_single_buckets.append(sorted_bucket)
815 else:
816 singles.update(b)
817
818 # put the buckets with the most items first
819 non_single_buckets = sorted(non_single_buckets, key=lambda b: len(b), reverse=True)
820
821 # sort singles
822 singles = sorted(list(singles), key=sort_key)
823
824 return non_single_buckets, singles
825
826 class TwilioConnect(ModalMixin, InferOrgMixin, OrgPermsMixin, SmartFormView):
827
828 class TwilioConnectForm(forms.Form):
829 account_sid = forms.CharField(help_text=_("Your Twilio Account SID"))
830 account_token = forms.CharField(help_text=_("Your Twilio Account Token"))
831
832 def clean(self):
833 account_sid = self.cleaned_data.get('account_sid', None)
834 account_token = self.cleaned_data.get('account_token', None)
835
836 if not account_sid: # pragma: needs cover
837 raise ValidationError(_("You must enter your Twilio Account SID"))
838
839 if not account_token:
840 raise ValidationError(_("You must enter your Twilio Account Token"))
841
842 try:
843 client = TwilioRestClient(account_sid, account_token)
844
845 # get the actual primary auth tokens from twilio and use them
846 account = client.accounts.get(account_sid)
847 self.cleaned_data['account_sid'] = account.sid
848 self.cleaned_data['account_token'] = account.auth_token
849 except Exception:
850 raise ValidationError(_("The Twilio account SID and Token seem invalid. Please check them again and retry."))
851
852 return self.cleaned_data
853
854 form_class = TwilioConnectForm
855 submit_button_name = "Save"
856 success_url = '@channels.claim_twilio'
857 field_config = dict(account_sid=dict(label=""), account_token=dict(label=""))
858 success_message = "Twilio Account successfully connected."
859
860 def form_valid(self, form):
861 account_sid = form.cleaned_data['account_sid']
862 account_token = form.cleaned_data['account_token']
863
864 org = self.get_object()
865 org.connect_twilio(account_sid, account_token, self.request.user)
866 org.save()
867
868 _next = self.request.GET.get('next', None)
869 redirect_url = _next if _next else self.get_success_url()
870
871 return HttpResponseRedirect(redirect_url)
872
873 class NexmoConfiguration(InferOrgMixin, OrgPermsMixin, SmartReadView):
874
875 def get(self, request, *args, **kwargs):
876 org = self.get_object()
877 domain = org.get_brand_domain()
878
879 nexmo_client = org.get_nexmo_client()
880 if not nexmo_client:
881 return HttpResponseRedirect(reverse("orgs.org_nexmo_connect"))
882
883 nexmo_uuid = org.nexmo_uuid()
884 mo_path = reverse('courier.nx', args=[nexmo_uuid, 'receive'])
885 dl_path = reverse('courier.nx', args=[nexmo_uuid, 'status'])
886 try:
887 nexmo_client.update_account('http://%s%s' % (domain, mo_path),
888 'http://%s%s' % (domain, dl_path))
889
890 return HttpResponseRedirect(reverse("channels.claim_nexmo"))
891
892 except nexmo.Error:
893 return super(OrgCRUDL.NexmoConfiguration, self).get(request, *args, **kwargs)
894
895 def get_context_data(self, **kwargs):
896 context = super(OrgCRUDL.NexmoConfiguration, self).get_context_data(**kwargs)
897
898 org = self.get_object()
899 domain = org.get_brand_domain()
900
901 config = org.config_json()
902 context['nexmo_api_key'] = config[NEXMO_KEY]
903 context['nexmo_api_secret'] = config[NEXMO_SECRET]
904
905 nexmo_uuid = config.get(NEXMO_UUID, None)
906 mo_path = reverse('courier.nx', args=[nexmo_uuid, 'receive'])
907 dl_path = reverse('courier.nx', args=[nexmo_uuid, 'status'])
908 context['mo_path'] = 'https://%s%s' % (domain, mo_path)
909 context['dl_path'] = 'https://%s%s' % (domain, dl_path)
910
911 return context
912
913 class NexmoAccount(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
914 success_message = ""
915
916 class NexmoKeys(forms.ModelForm):
917 api_key = forms.CharField(max_length=128, label=_("API Key"), required=False)
918 api_secret = forms.CharField(max_length=128, label=_("API Secret"), required=False)
919 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
920
921 def clean(self):
922 super(OrgCRUDL.NexmoAccount.NexmoKeys, self).clean()
923 if self.cleaned_data.get('disconnect', 'false') == 'false':
924 api_key = self.cleaned_data.get('api_key', None)
925 api_secret = self.cleaned_data.get('api_secret', None)
926
927 if not api_key:
928 raise ValidationError(_("You must enter your Nexmo Account API Key"))
929
930 if not api_secret: # pragma: needs cover
931 raise ValidationError(_("You must enter your Nexmo Account API Secret"))
932
933 try:
934 from nexmo import Client as NexmoClient
935 client = NexmoClient(key=api_key, secret=api_secret)
936 client.get_balance()
937 except Exception: # pragma: needs cover
938 raise ValidationError(_("Your Nexmo API key and secret seem invalid. Please check them again and retry."))
939
940 return self.cleaned_data
941
942 class Meta:
943 model = Org
944 fields = ('api_key', 'api_secret', 'disconnect')
945
946 form_class = NexmoKeys
947
948 def derive_initial(self):
949 initial = super(OrgCRUDL.NexmoAccount, self).derive_initial()
950 org = self.get_object()
951 config = org.config_json()
952 initial['api_key'] = config.get(NEXMO_KEY, '')
953 initial['api_secret'] = config.get(NEXMO_SECRET, '')
954 initial['disconnect'] = 'false'
955 return initial
956
957 def form_valid(self, form):
958 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
959 user = self.request.user
960 org = user.get_org()
961
962 if disconnect:
963 org.remove_nexmo_account(user)
964 return HttpResponseRedirect(reverse('orgs.org_home'))
965 else:
966 api_key = form.cleaned_data['api_key']
967 api_secret = form.cleaned_data['api_secret']
968
969 org.connect_nexmo(api_key, api_secret, user)
970 return super(OrgCRUDL.NexmoAccount, self).form_valid(form)
971
972 def get_context_data(self, **kwargs):
973 context = super(OrgCRUDL.NexmoAccount, self).get_context_data(**kwargs)
974
975 org = self.get_object()
976 client = org.get_nexmo_client()
977 if client:
978 config = org.config_json()
979 context['api_key'] = config.get(NEXMO_KEY, '--')
980
981 return context
982
983 class NexmoConnect(ModalMixin, InferOrgMixin, OrgPermsMixin, SmartFormView):
984
985 class NexmoConnectForm(forms.Form):
986 api_key = forms.CharField(help_text=_("Your Nexmo API key"))
987 api_secret = forms.CharField(help_text=_("Your Nexmo API secret"))
988
989 def clean(self):
990 super(OrgCRUDL.NexmoConnect.NexmoConnectForm, self).clean()
991
992 api_key = self.cleaned_data.get('api_key', None)
993 api_secret = self.cleaned_data.get('api_secret', None)
994
995 try:
996 from nexmo import Client as NexmoClient
997
998 client = NexmoClient(key=api_key, secret=api_secret)
999 client.get_balance()
1000 except Exception:
1001 raise ValidationError(_("Your Nexmo API key and secret seem invalid. Please check them again and retry."))
1002
1003 return self.cleaned_data
1004
1005 form_class = NexmoConnectForm
1006 submit_button_name = "Save"
1007 success_url = '@orgs.org_nexmo_configuration'
1008 field_config = dict(api_key=dict(label=""), api_secret=dict(label=""))
1009 success_message = "Nexmo Account successfully connected."
1010
1011 def form_valid(self, form):
1012 api_key = form.cleaned_data['api_key']
1013 api_secret = form.cleaned_data['api_secret']
1014
1015 org = self.get_object()
1016
1017 org.connect_nexmo(api_key, api_secret, self.request.user)
1018
1019 org.save()
1020
1021 return HttpResponseRedirect(self.get_success_url())
1022
1023 class PlivoConnect(ModalMixin, InferOrgMixin, OrgPermsMixin, SmartFormView):
1024
1025 class PlivoConnectForm(forms.Form):
1026 auth_id = forms.CharField(help_text=_("Your Plivo AUTH ID"))
1027 auth_token = forms.CharField(help_text=_("Your Plivo AUTH TOKEN"))
1028
1029 def clean(self):
1030 super(OrgCRUDL.PlivoConnect.PlivoConnectForm, self).clean()
1031
1032 auth_id = self.cleaned_data.get('auth_id', None)
1033 auth_token = self.cleaned_data.get('auth_token', None)
1034
1035 try:
1036 client = plivo.RestAPI(auth_id, auth_token)
1037 validation_response = client.get_account()
1038 except Exception: # pragma: needs cover
1039 raise ValidationError(_("Your Plivo AUTH ID and AUTH TOKEN seem invalid. Please check them again and retry."))
1040
1041 if validation_response[0] != 200:
1042 raise ValidationError(_("Your Plivo AUTH ID and AUTH TOKEN seem invalid. Please check them again and retry."))
1043
1044 return self.cleaned_data
1045
1046 form_class = PlivoConnectForm
1047 submit_button_name = "Save"
1048 success_url = '@channels.claim_plivo'
1049 field_config = dict(auth_id=dict(label=""), auth_token=dict(label=""))
1050 success_message = "Plivo credentials verified. You can now add a Plivo channel."
1051
1052 def form_valid(self, form):
1053
1054 auth_id = form.cleaned_data['auth_id']
1055 auth_token = form.cleaned_data['auth_token']
1056
1057 # add the credentials to the session
1058 self.request.session[Channel.CONFIG_PLIVO_AUTH_ID] = auth_id
1059 self.request.session[Channel.CONFIG_PLIVO_AUTH_TOKEN] = auth_token
1060
1061 response = self.render_to_response(self.get_context_data(form=form,
1062 success_url=self.get_success_url(),
1063 success_script=getattr(self, 'success_script', None)))
1064
1065 response['Temba-Success'] = self.get_success_url()
1066 return response
1067
1068 class SmtpServer(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
1069 success_message = ""
1070
1071 class SmtpConfig(forms.ModelForm):
1072 smtp_from_email = forms.CharField(max_length=128, label=_("Email Address"), required=False,
1073 help_text=_("The from email address, can contain a name: ex: Jane Doe <jane@example.org>"))
1074 smtp_host = forms.CharField(max_length=128, label=_("SMTP Host"), required=False)
1075 smtp_username = forms.CharField(max_length=128, label=_("Username"), required=False)
1076 smtp_password = forms.CharField(max_length=128, label=_("Password"), required=False,
1077 help_text=_("Leave blank to keep the existing set password if one exists"),
1078 widget=forms.PasswordInput)
1079 smtp_port = forms.CharField(max_length=128, label=_("Port"), required=False)
1080 smtp_encryption = forms.ChoiceField(choices=(('', _("No encryption")),
1081 ('T', _("Use TLS"))),
1082 required=False, label=_("Encryption"))
1083 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
1084
1085 def clean(self):
1086 super(OrgCRUDL.SmtpServer.SmtpConfig, self).clean()
1087 if self.cleaned_data.get('disconnect', 'false') == 'false':
1088 smtp_from_email = self.cleaned_data.get('smtp_from_email', None)
1089 smtp_host = self.cleaned_data.get('smtp_host', None)
1090 smtp_username = self.cleaned_data.get('smtp_username', None)
1091 smtp_password = self.cleaned_data.get('smtp_password', None)
1092 smtp_port = self.cleaned_data.get('smtp_port', None)
1093
1094 config = self.instance.config_json()
1095 existing_username = config.get(SMTP_USERNAME, '')
1096 if not smtp_password and existing_username == smtp_username:
1097 smtp_password = config.get(SMTP_PASSWORD, '')
1098
1099 if not smtp_from_email:
1100 raise ValidationError(_("You must enter a from email"))
1101
1102 parsed = parseaddr(smtp_from_email)
1103 if not is_valid_address(parsed[1]):
1104 raise ValidationError(_("Please enter a valid email address"))
1105
1106 if not smtp_host:
1107 raise ValidationError(_("You must enter the SMTP host"))
1108
1109 if not smtp_username:
1110 raise ValidationError(_("You must enter the SMTP username"))
1111
1112 if not smtp_password:
1113 raise ValidationError(_("You must enter the SMTP password"))
1114
1115 if not smtp_port:
1116 raise ValidationError(_("You must enter the SMTP port"))
1117
1118 self.cleaned_data['smtp_password'] = smtp_password
1119 return self.cleaned_data
1120
1121 class Meta:
1122 model = Org
1123 fields = ('smtp_from_email', 'smtp_host', 'smtp_username', 'smtp_password', 'smtp_port', 'smtp_encryption', 'disconnect')
1124
1125 form_class = SmtpConfig
1126
1127 def derive_initial(self):
1128 initial = super(OrgCRUDL.SmtpServer, self).derive_initial()
1129 org = self.get_object()
1130 config = org.config_json()
1131 initial['smtp_from_email'] = config.get(SMTP_FROM_EMAIL, '')
1132 initial['smtp_host'] = config.get(SMTP_HOST, '')
1133 initial['smtp_username'] = config.get(SMTP_USERNAME, '')
1134 initial['smtp_password'] = config.get(SMTP_PASSWORD, '')
1135 initial['smtp_port'] = config.get(SMTP_PORT, '')
1136 initial['smtp_encryption'] = config.get(SMTP_ENCRYPTION, '')
1137
1138 initial['disconnect'] = 'false'
1139 return initial
1140
1141 def form_valid(self, form):
1142 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
1143 user = self.request.user
1144 org = user.get_org()
1145
1146 if disconnect:
1147 org.remove_smtp_config(user)
1148 return HttpResponseRedirect(reverse('orgs.org_home'))
1149 else:
1150 smtp_from_email = form.cleaned_data['smtp_from_email']
1151 smtp_host = form.cleaned_data['smtp_host']
1152 smtp_username = form.cleaned_data['smtp_username']
1153 smtp_password = form.cleaned_data['smtp_password']
1154 smtp_port = form.cleaned_data['smtp_port']
1155 smtp_encryption = form.cleaned_data['smtp_encryption']
1156
1157 org.add_smtp_config(smtp_from_email, smtp_host, smtp_username, smtp_password, smtp_port, smtp_encryption, user)
1158
1159 return super(OrgCRUDL.SmtpServer, self).form_valid(form)
1160
1161 def get_context_data(self, **kwargs):
1162 context = super(OrgCRUDL.SmtpServer, self).get_context_data(**kwargs)
1163
1164 org = self.get_object()
1165 if org.has_smtp_config():
1166 config = org.config_json()
1167 from_email = config.get(SMTP_FROM_EMAIL)
1168 else:
1169 from_email = settings.FLOW_FROM_EMAIL
1170
1171 # populate our context with the from email (just the address)
1172 context['flow_from_email'] = parseaddr(from_email)[1]
1173
1174 return context
1175
1176 class Manage(SmartListView):
1177 fields = ('credits', 'used', 'name', 'owner', 'service', 'created_on')
1178 field_config = {'service': {'label': ''}}
1179 default_order = ('-credits', '-created_on',)
1180 search_fields = ('name__icontains', 'created_by__email__iexact', 'config__icontains')
1181 link_fields = ('name', 'owner')
1182 title = "Organizations"
1183
1184 def get_used(self, obj):
1185 if not obj.credits: # pragma: needs cover
1186 used_pct = 0
1187 else:
1188 used_pct = round(100 * float(obj.get_credits_used()) / float(obj.credits))
1189
1190 used_class = 'used-normal'
1191 if used_pct >= 75: # pragma: needs cover
1192 used_class = 'used-warning'
1193 if used_pct >= 90: # pragma: needs cover
1194 used_class = 'used-alert'
1195 return mark_safe("<div class='used-pct %s'>%d%%</div>" % (used_class, used_pct))
1196
1197 def get_credits(self, obj):
1198 if not obj.credits: # pragma: needs cover
1199 obj.credits = 0
1200 return mark_safe("<div class='num-credits'><a href='%s'>%s</a></div>"
1201 % (reverse('orgs.topup_manage') + "?org=%d" % obj.id, format(obj.credits, ",d")))
1202
1203 def get_owner(self, obj):
1204 # default to the created by if there are no admins
1205 owner = obj.latest_admin() or obj.created_by
1206
1207 return mark_safe("<div class='owner-name'>%s %s</div><div class='owner-email'>%s</div>"
1208 % (owner.first_name, owner.last_name, owner))
1209
1210 def get_service(self, obj):
1211 url = reverse('orgs.org_service')
1212
1213 return mark_safe("<a href='%s?organization=%d' class='service posterize btn btn-tiny'>Service</a>"
1214 % (url, obj.id))
1215
1216 def get_name(self, obj):
1217 suspended = ''
1218 if obj.is_suspended():
1219 suspended = '<span class="suspended">(Suspended)</span>'
1220
1221 return mark_safe("<div class='org-name'>%s %s</div><div class='org-timezone'>%s</div>"
1222 % (suspended, obj.name, obj.timezone))
1223
1224 def derive_queryset(self, **kwargs):
1225 queryset = super(OrgCRUDL.Manage, self).derive_queryset(**kwargs)
1226 queryset = queryset.all()
1227
1228 brand = self.request.branding.get('brand')
1229 if brand:
1230 queryset = queryset.filter(brand=brand)
1231
1232 queryset = queryset.annotate(credits=Sum('topups__credits'))
1233 queryset = queryset.annotate(paid=Sum('topups__price'))
1234
1235 return queryset
1236
1237 def get_context_data(self, **kwargs):
1238 context = super(OrgCRUDL.Manage, self).get_context_data(**kwargs)
1239 context['searches'] = []
1240 return context
1241
1242 def lookup_field_link(self, context, field, obj):
1243 if field == 'owner':
1244 return reverse('users.user_update', args=[obj.created_by.pk])
1245 return super(OrgCRUDL.Manage, self).lookup_field_link(context, field, obj)
1246
1247 def get_created_by(self, obj): # pragma: needs cover
1248 return "%s %s - %s" % (obj.created_by.first_name, obj.created_by.last_name, obj.created_by.email)
1249
1250 class Update(SmartUpdateView):
1251 class OrgUpdateForm(forms.ModelForm):
1252 viewers = forms.ModelMultipleChoiceField(User.objects.all(), required=False)
1253 editors = forms.ModelMultipleChoiceField(User.objects.all(), required=False)
1254 surveyors = forms.ModelMultipleChoiceField(User.objects.all(), required=False)
1255 administrators = forms.ModelMultipleChoiceField(User.objects.all(), required=False)
1256 parent = forms.ModelChoiceField(Org.objects.all(), required=False)
1257
1258 class Meta:
1259 model = Org
1260 fields = '__all__'
1261
1262 form_class = OrgUpdateForm
1263
1264 def get_success_url(self):
1265 return reverse('orgs.org_update', args=[self.get_object().pk])
1266
1267 def get_gear_links(self):
1268 links = []
1269
1270 org = self.get_object()
1271
1272 links.append(dict(title=_('Topups'),
1273 style='btn-primary',
1274 href='%s?org=%d' % (reverse("orgs.topup_manage"), org.pk)))
1275
1276 if org.is_suspended():
1277 links.append(dict(title=_('Restore'),
1278 style='btn-secondary',
1279 posterize=True,
1280 href='%s?status=restored' % reverse("orgs.org_update", args=[org.pk])))
1281 else: # pragma: needs cover
1282 links.append(dict(title=_('Suspend'),
1283 style='btn-secondary',
1284 posterize=True,
1285 href='%s?status=suspended' % reverse("orgs.org_update", args=[org.pk])))
1286
1287 if not org.is_whitelisted():
1288 links.append(dict(title=_('Whitelist'),
1289 style='btn-secondary',
1290 posterize=True,
1291 href='%s?status=whitelisted' % reverse("orgs.org_update", args=[org.pk])))
1292
1293 return links
1294
1295 def form_valid(self, form):
1296 self.object = form.save(commit=False)
1297
1298 try:
1299 json.loads(form.cleaned_data.get('config'))
1300 json.loads(form.cleaned_data.get('webhook'))
1301
1302 self.object = self.pre_save(self.object)
1303 self.save(self.object)
1304 self.object = self.post_save(self.object)
1305
1306 messages.success(self.request, self.derive_success_message())
1307 if 'HTTP_X_FORMAX' not in self.request.META:
1308 return HttpResponseRedirect(self.get_success_url())
1309 else:
1310 response = self.render_to_response(self.get_context_data(form=form))
1311 response['REDIRECT'] = self.get_success_url()
1312 return response
1313
1314 except ValueError as e:
1315 message = str(e).capitalize()
1316 errors = self.form._errors.setdefault(forms.forms.NON_FIELD_ERRORS, forms.utils.ErrorList())
1317 errors.append(message)
1318 return self.render_to_response(self.get_context_data(form=form))
1319
1320 except IntegrityError as e:
1321 message = str(e).capitalize()
1322 errors = self.form._errors.setdefault(forms.forms.NON_FIELD_ERRORS, forms.utils.ErrorList())
1323 errors.append(message)
1324 return self.render_to_response(self.get_context_data(form=form))
1325
1326 def post(self, request, *args, **kwargs):
1327 if 'status' in request.POST:
1328 if request.POST.get('status', None) == SUSPENDED:
1329 self.get_object().set_suspended()
1330 elif request.POST.get('status', None) == WHITELISTED:
1331 self.get_object().set_whitelisted()
1332 elif request.POST.get('status', None) == RESTORED:
1333 self.get_object().set_restored()
1334 return HttpResponseRedirect(self.get_success_url())
1335 return super(OrgCRUDL.Update, self).post(request, *args, **kwargs)
1336
1337 class Accounts(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
1338
1339 class PasswordForm(forms.ModelForm):
1340 surveyor_password = forms.CharField(max_length=128)
1341
1342 def clean_surveyor_password(self): # pragma: needs cover
1343 password = self.cleaned_data.get('surveyor_password', '')
1344 existing = Org.objects.filter(surveyor_password=password).exclude(pk=self.instance.pk).first()
1345 if existing:
1346 raise forms.ValidationError(_('This password is not valid. Choose a new password and try again.'))
1347 return password
1348
1349 class Meta:
1350 model = Org
1351 fields = ('surveyor_password',)
1352
1353 form_class = PasswordForm
1354 success_url = "@orgs.org_home"
1355 success_message = ""
1356 submit_button_name = _("Save Changes")
1357 title = 'User Accounts'
1358 fields = ('surveyor_password',)
1359
1360 class ManageAccounts(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
1361
1362 class AccountsForm(forms.ModelForm):
1363 invite_emails = forms.CharField(label=_("Invite people to your organization"), required=False)
1364 invite_group = forms.ChoiceField(choices=(('A', _("Administrators")),
1365 ('E', _("Editors")),
1366 ('V', _("Viewers")),
1367 ('S', _("Surveyors"))),
1368 required=True, initial='V', label=_("User group"))
1369
1370 def add_user_group_fields(self, groups, users):
1371 fields_by_user = {}
1372
1373 for user in users:
1374 fields = []
1375 field_mapping = []
1376
1377 for group in groups:
1378 check_field = forms.BooleanField(required=False)
1379 field_name = "%s_%d" % (group.lower(), user.pk)
1380
1381 field_mapping.append((field_name, check_field))
1382 fields.append(field_name)
1383
1384 self.fields = OrderedDict(self.fields.items() + field_mapping)
1385 fields_by_user[user] = fields
1386 return fields_by_user
1387
1388 def add_invite_remove_fields(self, invites):
1389 fields_by_invite = {}
1390
1391 for invite in invites:
1392 field_name = "%s_%d" % ('remove_invite', invite.pk)
1393 self.fields = OrderedDict(self.fields.items() + [(field_name, forms.BooleanField(required=False))])
1394 fields_by_invite[invite] = field_name
1395
1396 return fields_by_invite
1397
1398 def clean_invite_emails(self):
1399 emails = self.cleaned_data['invite_emails'].lower().strip()
1400 if emails:
1401 email_list = emails.split(',')
1402 for email in email_list:
1403 try:
1404 validate_email(email)
1405 except ValidationError:
1406 raise forms.ValidationError(_("One of the emails you entered is invalid."))
1407 return emails
1408
1409 class Meta:
1410 model = Invitation
1411 fields = ('invite_emails', 'invite_group')
1412
1413 form_class = AccountsForm
1414 success_url = "@orgs.org_manage_accounts"
1415 success_message = ""
1416 submit_button_name = _("Save Changes")
1417 ORG_GROUPS = ('Administrators', 'Editors', 'Viewers', 'Surveyors')
1418 title = 'Manage User Accounts'
1419
1420 @staticmethod
1421 def org_group_set(org, group_name):
1422 return getattr(org, group_name.lower())
1423
1424 def derive_initial(self):
1425 initial = super(OrgCRUDL.ManageAccounts, self).derive_initial()
1426
1427 org = self.get_object()
1428 for group in self.ORG_GROUPS:
1429 users_in_group = self.org_group_set(org, group).all()
1430
1431 for user in users_in_group:
1432 initial['%s_%d' % (group.lower(), user.pk)] = True
1433
1434 return initial
1435
1436 def get_form(self):
1437 form = super(OrgCRUDL.ManageAccounts, self).get_form()
1438
1439 org = self.get_object()
1440 self.org_users = org.get_org_users()
1441 self.fields_by_users = form.add_user_group_fields(self.ORG_GROUPS, self.org_users)
1442
1443 self.invites = Invitation.objects.filter(org=org, is_active=True).order_by('email')
1444 self.fields_by_invite = form.add_invite_remove_fields(self.invites)
1445
1446 return form
1447
1448 def post_save(self, obj):
1449 obj = super(OrgCRUDL.ManageAccounts, self).post_save(obj)
1450
1451 cleaned_data = self.form.cleaned_data
1452 org = self.get_object()
1453
1454 for invite in self.fields_by_invite.keys():
1455 if cleaned_data.get(self.fields_by_invite.get(invite)):
1456 Invitation.objects.filter(org=org, pk=invite.pk).delete()
1457
1458 invite_emails = cleaned_data['invite_emails'].lower().strip()
1459 invite_group = cleaned_data['invite_group']
1460
1461 if invite_emails:
1462 for email in invite_emails.split(','):
1463 # if they already have an invite, update it
1464 invites = Invitation.objects.filter(email=email, org=org).order_by('-pk')
1465 invitation = invites.first()
1466
1467 if invitation:
1468 invites.exclude(pk=invitation.pk).delete() # remove any old invites
1469
1470 invitation.user_group = invite_group
1471 invitation.is_active = True
1472 invitation.save()
1473 else:
1474 invitation = Invitation.create(org, self.request.user, email, invite_group)
1475
1476 invitation.send_invitation()
1477
1478 current_groups = {}
1479 new_groups = {}
1480
1481 for group in self.ORG_GROUPS:
1482 # gather up existing users with their groups
1483 for user in self.org_group_set(org, group).all():
1484 current_groups[user] = group
1485
1486 # parse form fields to get new roles
1487 for field in self.form.cleaned_data:
1488 if field.startswith(group.lower() + '_') and self.form.cleaned_data[field]:
1489 user = User.objects.get(pk=field.split('_')[1])
1490 new_groups[user] = group
1491
1492 for user in current_groups.keys():
1493 current_group = current_groups.get(user)
1494 new_group = new_groups.get(user)
1495
1496 if current_group != new_group:
1497 if current_group:
1498 self.org_group_set(org, current_group).remove(user)
1499 if new_group:
1500 self.org_group_set(org, new_group).add(user)
1501
1502 # when a user's role changes, delete any API tokens they're no longer allowed to have
1503 api_roles = APIToken.get_allowed_roles(org, user)
1504 for token in APIToken.objects.filter(org=org, user=user).exclude(role__in=api_roles):
1505 token.release()
1506
1507 return obj
1508
1509 def get_context_data(self, **kwargs):
1510 context = super(OrgCRUDL.ManageAccounts, self).get_context_data(**kwargs)
1511 org = self.get_object()
1512 context['org'] = org
1513 context['org_users'] = self.org_users
1514 context['group_fields'] = self.fields_by_users
1515 context['invites'] = self.invites
1516 context['invites_fields'] = self.fields_by_invite
1517 return context
1518
1519 def get_success_url(self):
1520 still_in_org = self.request.user in self.get_object().get_org_users()
1521
1522 # if current user no longer belongs to this org, redirect to org chooser
1523 return reverse('orgs.org_manage_accounts') if still_in_org else reverse('orgs.org_choose')
1524
1525 class MultiOrgMixin(OrgPermsMixin):
1526 # if we don't support multi orgs, go home
1527 def pre_process(self, request, *args, **kwargs):
1528 response = super(OrgPermsMixin, self).pre_process(request, *args, **kwargs)
1529 if not response and not request.user.get_org().is_multi_org_tier():
1530 return HttpResponseRedirect(reverse('orgs.org_home'))
1531 return response
1532
1533 class ManageAccountsSubOrg(MultiOrgMixin, ManageAccounts):
1534
1535 def get_context_data(self, **kwargs):
1536 context = super(OrgCRUDL.ManageAccountsSubOrg, self).get_context_data(**kwargs)
1537 org_id = self.request.GET.get('org')
1538 context['parent'] = Org.objects.filter(id=org_id, parent=self.request.user.get_org()).first()
1539 return context
1540
1541 def get_object(self, *args, **kwargs):
1542 org_id = self.request.GET.get('org')
1543 return Org.objects.filter(id=org_id, parent=self.request.user.get_org()).first()
1544
1545 def get_success_url(self): # pragma: needs cover
1546 org_id = self.request.GET.get('org')
1547 return '%s?org=%s' % (reverse('orgs.org_manage_accounts_sub_org'), org_id)
1548
1549 class ResendInvitation(InferOrgMixin, OrgPermsMixin, SmartUpdateView): # pragma: no cover
1550
1551 def dispatch(self, *args, **kwargs):
1552 return super(OrgCRUDL.ResendInvitation, self).dispatch(*args, **kwargs)
1553
1554 def get(self, request, *args, **kwargs):
1555 invite = Invitation.objects.filter(id=int(request.GET.get('invite')), is_active=True).first()
1556
1557 if invite:
1558 invite.send_invitation()
1559 messages.success(request, _('Invitation resent successfully'))
1560 else:
1561 messages.error(request, _('No invitation found'))
1562
1563 return HttpResponseRedirect(reverse('orgs.org_manage_accounts'))
1564
1565 class Service(SmartFormView):
1566 class ServiceForm(forms.Form):
1567 organization = forms.ModelChoiceField(queryset=Org.objects.all(), empty_label=None)
1568
1569 form_class = ServiceForm
1570 success_url = '@msgs.msg_inbox'
1571 fields = ('organization',)
1572
1573 # valid form means we set our org and redirect to their inbox
1574 def form_valid(self, form):
1575 org = form.cleaned_data['organization']
1576 self.request.session['org_id'] = org.pk
1577 return HttpResponseRedirect(self.get_success_url())
1578
1579 # invalid form login 'logs out' the user from the org and takes them to the org manage page
1580 def form_invalid(self, form):
1581 self.request.session['org_id'] = None
1582 return HttpResponseRedirect(reverse('orgs.org_manage'))
1583
1584 class SubOrgs(MultiOrgMixin, InferOrgMixin, SmartListView):
1585
1586 fields = ('credits', 'name', 'manage', 'created_on')
1587 link_fields = ()
1588 title = "Organizations"
1589
1590 def get_gear_links(self):
1591 links = []
1592
1593 if self.has_org_perm("orgs.org_dashboard"):
1594 links.append(dict(title='Dashboard',
1595 href=reverse('dashboard.dashboard_home')))
1596
1597 if self.has_org_perm("orgs.org_create_sub_org"):
1598 links.append(dict(title='New',
1599 js_class='add-sub-org',
1600 href='#'))
1601
1602 if self.has_org_perm("orgs.org_transfer_credits"):
1603 links.append(dict(title='Transfer Credits',
1604 js_class='transfer-credits',
1605 href='#'))
1606
1607 if self.has_org_perm("orgs.org_home"):
1608 links.append(dict(title='Manage Account',
1609 href=reverse('orgs.org_home')))
1610
1611 return links
1612
1613 def get_manage(self, obj):
1614 if obj.parent: # pragma: needs cover
1615 return mark_safe('<a href="%s?org=%s"><div class="btn btn-tiny">Manage Accounts</div></a>'
1616 % (reverse('orgs.org_manage_accounts_sub_org'), obj.id))
1617 return ''
1618
1619 def get_credits(self, obj):
1620 credits = obj.get_credits_remaining()
1621 return mark_safe('<div class="edit-org" data-url="%s?org=%d"><div class="num-credits">%s</div></div>'
1622 % (reverse('orgs.org_edit_sub_org'), obj.id, format(credits, ",d")))
1623
1624 def get_name(self, obj):
1625 org_type = 'child'
1626 if not obj.parent:
1627 org_type = 'parent'
1628
1629 return mark_safe("<div class='%s-org-name'>%s</div><div class='org-timezone'>%s</div>"
1630 % (org_type, obj.name, obj.timezone))
1631
1632 def derive_queryset(self, **kwargs):
1633 queryset = super(OrgCRUDL.SubOrgs, self).derive_queryset(**kwargs)
1634
1635 # all our children and ourselves
1636 org = self.get_object()
1637 ids = [child.id for child in Org.objects.filter(parent=org)]
1638 ids.append(org.id)
1639
1640 queryset = queryset.filter(is_active=True)
1641 queryset = queryset.filter(id__in=ids)
1642 queryset = queryset.annotate(credits=Sum('topups__credits'))
1643 queryset = queryset.annotate(paid=Sum('topups__price'))
1644 return queryset.order_by('-parent', 'name')
1645
1646 def get_context_data(self, **kwargs):
1647 context = super(OrgCRUDL.SubOrgs, self).get_context_data(**kwargs)
1648 context['searches'] = ['Nyaruka', ]
1649 return context
1650
1651 def get_created_by(self, obj): # pragma: needs cover
1652 return "%s %s - %s" % (obj.created_by.first_name, obj.created_by.last_name, obj.created_by.email)
1653
1654 class CreateSubOrg(MultiOrgMixin, ModalMixin, InferOrgMixin, SmartCreateView):
1655
1656 class CreateOrgForm(forms.ModelForm):
1657 name = forms.CharField(label=_("Organization"),
1658 help_text=_("The name of your organization"))
1659
1660 timezone = TimeZoneFormField(help_text=_("The timezone your organization is in"))
1661
1662 class Meta:
1663 model = Org
1664 fields = '__all__'
1665
1666 fields = ('name', 'date_format', 'timezone')
1667 form_class = CreateOrgForm
1668 success_url = '@orgs.org_sub_orgs'
1669 permission = 'orgs.org_create_sub_org'
1670
1671 def derive_initial(self):
1672 initial = super(OrgCRUDL.CreateSubOrg, self).derive_initial()
1673 parent = self.request.user.get_org()
1674 initial['timezone'] = parent.timezone
1675 initial['date_format'] = parent.date_format
1676 return initial
1677
1678 def form_valid(self, form):
1679 self.object = form.save(commit=False)
1680 parent = self.org
1681 parent.create_sub_org(self.object.name, self.object.timezone, self.request.user)
1682 if 'HTTP_X_PJAX' not in self.request.META:
1683 return HttpResponseRedirect(self.get_success_url())
1684 else: # pragma: no cover
1685 response = self.render_to_response(self.get_context_data(form=form,
1686 success_url=self.get_success_url(),
1687 success_script=getattr(self, 'success_script', None)))
1688 response['Temba-Success'] = self.get_success_url()
1689 return response
1690
1691 class Choose(SmartFormView):
1692 class ChooseForm(forms.Form):
1693 organization = forms.ModelChoiceField(queryset=Org.objects.all(), empty_label=None)
1694
1695 form_class = ChooseForm
1696 success_url = '@msgs.msg_inbox'
1697 fields = ('organization',)
1698 title = _("Select your Organization")
1699
1700 def get_user_orgs(self):
1701 host = self.request.branding.get('brand')
1702 return self.request.user.get_user_orgs(host)
1703
1704 def pre_process(self, request, *args, **kwargs):
1705 user = self.request.user
1706 if user.is_authenticated():
1707 user_orgs = self.get_user_orgs()
1708
1709 if user.is_superuser or user.is_staff:
1710 return HttpResponseRedirect(reverse('orgs.org_manage'))
1711
1712 elif user_orgs.count() == 1:
1713 org = user_orgs[0]
1714 self.request.session['org_id'] = org.pk
1715 if org.get_org_surveyors().filter(username=self.request.user.username):
1716 return HttpResponseRedirect(reverse('orgs.org_surveyor'))
1717
1718 return HttpResponseRedirect(self.get_success_url()) # pragma: needs cover
1719
1720 elif user_orgs.count() == 0: # pragma: needs cover
1721 if user.groups.filter(name='Customer Support').first():
1722 return HttpResponseRedirect(reverse('orgs.org_manage'))
1723
1724 # for regular users, if there's no orgs, log them out with a message
1725 messages.info(request, _("No organizations for this account, please contact your administrator."))
1726 logout(request)
1727 return HttpResponseRedirect(reverse('users.user_login'))
1728 return None # pragma: needs cover
1729
1730 def get_context_data(self, **kwargs): # pragma: needs cover
1731 context = super(OrgCRUDL.Choose, self).get_context_data(**kwargs)
1732 context['orgs'] = self.get_user_orgs()
1733 return context
1734
1735 def has_permission(self, request, *args, **kwargs):
1736 return self.request.user.is_authenticated()
1737
1738 def customize_form_field(self, name, field): # pragma: needs cover
1739 if name == 'organization':
1740 field.widget.choices.queryset = self.get_user_orgs()
1741 return field
1742
1743 def form_valid(self, form): # pragma: needs cover
1744 org = form.cleaned_data['organization']
1745
1746 if org in self.get_user_orgs():
1747 self.request.session['org_id'] = org.pk
1748 else:
1749 return HttpResponseRedirect(reverse('orgs.org_choose'))
1750
1751 if org.get_org_surveyors().filter(username=self.request.user.username):
1752 return HttpResponseRedirect(reverse('orgs.org_surveyor'))
1753
1754 return HttpResponseRedirect(self.get_success_url())
1755
1756 class CreateLogin(SmartUpdateView):
1757 title = ""
1758 form_class = OrgSignupForm
1759 fields = ('first_name', 'last_name', 'email', 'password')
1760 success_message = ''
1761 success_url = '@msgs.msg_inbox'
1762 submit_button_name = _("Create")
1763 permission = False
1764
1765 def pre_process(self, request, *args, **kwargs):
1766 org = self.get_object()
1767 if not org: # pragma: needs cover
1768 messages.info(request, _("Your invitation link is invalid. Please contact your organization administrator."))
1769 return HttpResponseRedirect(reverse('public.public_index'))
1770 return None
1771
1772 def pre_save(self, obj):
1773 obj = super(OrgCRUDL.CreateLogin, self).pre_save(obj)
1774
1775 user = Org.create_user(self.form.cleaned_data['email'],
1776 self.form.cleaned_data['password'])
1777
1778 user.first_name = self.form.cleaned_data['first_name']
1779 user.last_name = self.form.cleaned_data['last_name']
1780 user.save()
1781
1782 self.invitation = self.get_invitation()
1783
1784 # log the user in
1785 user = authenticate(username=user.username, password=self.form.cleaned_data['password'])
1786 login(self.request, user)
1787 if self.invitation.user_group == 'A':
1788 obj.administrators.add(user)
1789 elif self.invitation.user_group == 'E': # pragma: needs cover
1790 obj.editors.add(user)
1791 elif self.invitation.user_group == 'S':
1792 obj.surveyors.add(user)
1793 else: # pragma: needs cover
1794 obj.viewers.add(user)
1795
1796 # make the invitation inactive
1797 self.invitation.is_active = False
1798 self.invitation.save()
1799
1800 return obj
1801
1802 def get_success_url(self):
1803 if self.invitation.user_group == 'S':
1804 return reverse('orgs.org_surveyor')
1805 return super(OrgCRUDL.CreateLogin, self).get_success_url()
1806
1807 @classmethod
1808 def derive_url_pattern(cls, path, action):
1809 return r'^%s/%s/(?P<secret>\w+)/$' % (path, action)
1810
1811 def get_invitation(self, **kwargs):
1812 invitation = None
1813 secret = self.kwargs.get('secret')
1814 invitations = Invitation.objects.filter(secret=secret, is_active=True)
1815 if invitations:
1816 invitation = invitations[0]
1817 return invitation
1818
1819 def get_object(self, **kwargs):
1820 invitation = self.get_invitation()
1821 if invitation:
1822 return invitation.org
1823 return None # pragma: needs cover
1824
1825 def derive_title(self):
1826 org = self.get_object()
1827 return _("Join %(name)s") % {'name': org.name}
1828
1829 def get_context_data(self, **kwargs):
1830 context = super(OrgCRUDL.CreateLogin, self).get_context_data(**kwargs)
1831
1832 context['secret'] = self.kwargs.get('secret')
1833 context['org'] = self.get_object()
1834
1835 return context
1836
1837 class Join(SmartUpdateView):
1838 class JoinForm(forms.ModelForm):
1839
1840 class Meta:
1841 model = Org
1842 fields = ()
1843
1844 success_message = ''
1845 form_class = JoinForm
1846 success_url = "@msgs.msg_inbox"
1847 submit_button_name = _("Join")
1848 permission = False
1849
1850 def pre_process(self, request, *args, **kwargs): # pragma: needs cover
1851 secret = self.kwargs.get('secret')
1852
1853 org = self.get_object()
1854 if not org:
1855 messages.info(request, _("Your invitation link has expired. Please contact your organization administrator."))
1856 return HttpResponseRedirect(reverse('public.public_index'))
1857
1858 if not request.user.is_authenticated():
1859 return HttpResponseRedirect(reverse('orgs.org_create_login', args=[secret]))
1860 return None
1861
1862 def derive_title(self): # pragma: needs cover
1863 org = self.get_object()
1864 return _("Join %(name)s") % {'name': org.name}
1865
1866 def save(self, org): # pragma: needs cover
1867 org = self.get_object()
1868 self.invitation = self.get_invitation()
1869 if org:
1870 if self.invitation.user_group == 'A':
1871 org.administrators.add(self.request.user)
1872 elif self.invitation.user_group == 'E':
1873 org.editors.add(self.request.user)
1874 elif self.invitation.user_group == 'S':
1875 org.surveyors.add(self.request.user)
1876 else:
1877 org.viewers.add(self.request.user)
1878
1879 # make the invitation inactive
1880 self.invitation.is_active = False
1881 self.invitation.save()
1882
1883 # set the active org on this user
1884 self.request.user.set_org(org)
1885 self.request.session['org_id'] = org.pk
1886
1887 def get_success_url(self): # pragma: needs cover
1888 if self.invitation.user_group == 'S':
1889 return reverse('orgs.org_surveyor')
1890
1891 return super(OrgCRUDL.Join, self).get_success_url()
1892
1893 @classmethod
1894 def derive_url_pattern(cls, path, action):
1895 return r'^%s/%s/(?P<secret>\w+)/$' % (path, action)
1896
1897 def get_invitation(self, **kwargs): # pragma: needs cover
1898 invitation = None
1899 secret = self.kwargs.get('secret')
1900 invitations = Invitation.objects.filter(secret=secret, is_active=True)
1901 if invitations:
1902 invitation = invitations[0]
1903 return invitation
1904
1905 def get_object(self, **kwargs): # pragma: needs cover
1906 invitation = self.get_invitation()
1907 if invitation:
1908 return invitation.org
1909
1910 def get_context_data(self, **kwargs): # pragma: needs cover
1911 context = super(OrgCRUDL.Join, self).get_context_data(**kwargs)
1912
1913 context['org'] = self.get_object()
1914 return context
1915
1916 class Surveyor(SmartFormView):
1917
1918 class PasswordForm(forms.Form):
1919 surveyor_password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': 'Password'}))
1920
1921 def clean_surveyor_password(self):
1922 password = self.cleaned_data['surveyor_password']
1923 org = Org.objects.filter(surveyor_password=password).first()
1924 if not org:
1925 raise forms.ValidationError(_("Invalid surveyor password, please check with your project leader and try again."))
1926 self.cleaned_data['org'] = org
1927 return password
1928
1929 class RegisterForm(PasswordForm):
1930 surveyor_password = forms.CharField(widget=forms.HiddenInput())
1931 first_name = forms.CharField(help_text=_("Your first name"), widget=forms.TextInput(attrs={'placeholder': 'First Name'}))
1932 last_name = forms.CharField(help_text=_("Your last name"), widget=forms.TextInput(attrs={'placeholder': 'Last Name'}))
1933 email = forms.EmailField(help_text=_("Your email address"), widget=forms.TextInput(attrs={'placeholder': 'Email'}))
1934 password = forms.CharField(widget=forms.PasswordInput(attrs={'placeholder': 'Password'}),
1935 help_text=_("Your password, at least eight letters please"))
1936
1937 def __init__(self, *args, **kwargs):
1938 super(OrgCRUDL.Surveyor.RegisterForm, self).__init__(*args, **kwargs)
1939
1940 def clean_email(self):
1941 email = self.cleaned_data['email']
1942 if email:
1943 if User.objects.filter(username__iexact=email):
1944 raise forms.ValidationError(_("That email address is already used"))
1945
1946 return email.lower()
1947
1948 def clean_password(self):
1949 password = self.cleaned_data['password']
1950 if password:
1951 if not len(password) >= 8:
1952 raise forms.ValidationError(_("Passwords must contain at least 8 letters."))
1953 return password
1954
1955 permission = None
1956 form_class = PasswordForm
1957
1958 def derive_initial(self):
1959 initial = super(OrgCRUDL.Surveyor, self).derive_initial()
1960 initial['surveyor_password'] = self.request.POST.get('surveyor_password', '')
1961 return initial
1962
1963 def get_context_data(self, **kwargs):
1964 context = super(OrgCRUDL.Surveyor, self).get_context_data()
1965 context['form'] = self.form
1966 context['step'] = self.get_step()
1967
1968 for key, field in six.iteritems(self.form.fields):
1969 context[key] = field
1970
1971 return context
1972
1973 def get_success_url(self):
1974 return reverse('orgs.org_surveyor')
1975
1976 def get_form_class(self):
1977 if self.get_step() == 2:
1978 return OrgCRUDL.Surveyor.RegisterForm
1979 else:
1980 return OrgCRUDL.Surveyor.PasswordForm
1981
1982 def get_step(self):
1983 return 2 if 'first_name' in self.request.POST else 1
1984
1985 def form_valid(self, form):
1986 if self.get_step() == 1:
1987
1988 org = self.form.cleaned_data.get('org', None)
1989
1990 context = self.get_context_data()
1991 context['step'] = 2
1992 context['org'] = org
1993
1994 self.form = OrgCRUDL.Surveyor.RegisterForm(initial=self.derive_initial())
1995 context['form'] = self.form
1996
1997 return self.render_to_response(context)
1998 else:
1999
2000 # create our user
2001 username = self.form.cleaned_data['email']
2002 user = Org.create_user(username,
2003 self.form.cleaned_data['password'])
2004
2005 user.first_name = self.form.cleaned_data['first_name']
2006 user.last_name = self.form.cleaned_data['last_name']
2007 user.save()
2008
2009 # log the user in
2010 user = authenticate(username=user.username, password=self.form.cleaned_data['password'])
2011 login(self.request, user)
2012
2013 org = self.form.cleaned_data['org']
2014 org.surveyors.add(user)
2015
2016 surveyors_group = Group.objects.get(name="Surveyors")
2017 token = APIToken.get_or_create(org, user, role=surveyors_group)
2018 response = dict(url=self.get_success_url(), token=token, user=username, org=org.name)
2019 return HttpResponseRedirect('%(url)s?org=%(org)s&token=%(token)s&user=%(user)s' % response)
2020
2021 def form_invalid(self, form):
2022 return super(OrgCRUDL.Surveyor, self).form_invalid(form)
2023
2024 def derive_title(self):
2025 return _('Welcome!')
2026
2027 def get_template_names(self):
2028 if 'android' in self.request.META.get('HTTP_X_REQUESTED_WITH', '') \
2029 or 'mobile' in self.request.GET \
2030 or 'Android' in self.request.META.get('HTTP_USER_AGENT', ''):
2031 return ['orgs/org_surveyor_mobile.haml']
2032 else:
2033 return super(OrgCRUDL.Surveyor, self).get_template_names()
2034
2035 class Grant(SmartCreateView):
2036 title = _("Create Organization Account")
2037 form_class = OrgGrantForm
2038 fields = ('first_name', 'last_name', 'email', 'password', 'name', 'timezone', 'credits')
2039 success_message = 'Organization successfully created.'
2040 submit_button_name = _("Create")
2041 permission = 'orgs.org_grant'
2042 success_url = '@orgs.org_grant'
2043
2044 def create_user(self):
2045 user = User.objects.filter(username__iexact=self.form.cleaned_data['email']).first()
2046 if not user:
2047 user = Org.create_user(self.form.cleaned_data['email'],
2048 self.form.cleaned_data['password'])
2049
2050 user.first_name = self.form.cleaned_data['first_name']
2051 user.last_name = self.form.cleaned_data['last_name']
2052 user.save()
2053
2054 # set our language to the default for the site
2055 language = self.request.branding.get('language', settings.DEFAULT_LANGUAGE)
2056 user_settings = user.get_settings()
2057 user_settings.language = language
2058 user_settings.save()
2059
2060 return user
2061
2062 def get_form_kwargs(self):
2063 kwargs = super(OrgCRUDL.Grant, self).get_form_kwargs()
2064 kwargs['branding'] = self.request.branding
2065 return kwargs
2066
2067 def pre_save(self, obj):
2068 obj = super(OrgCRUDL.Grant, self).pre_save(obj)
2069
2070 self.user = self.create_user()
2071
2072 obj.created_by = self.user
2073 obj.modified_by = self.user
2074
2075 slug = Org.get_unique_slug(self.form.cleaned_data['name'])
2076 obj.slug = slug
2077 obj.brand = self.request.branding.get('host', settings.DEFAULT_BRAND)
2078 return obj
2079
2080 def get_welcome_size(self): # pragma: needs cover
2081 return self.form.cleaned_data['credits']
2082
2083 def post_save(self, obj):
2084 obj = super(OrgCRUDL.Grant, self).post_save(obj)
2085 obj.administrators.add(self.user)
2086
2087 if not self.request.user.is_anonymous() and self.request.user.has_perm('orgs.org_grant'): # pragma: needs cover
2088 obj.administrators.add(self.request.user.pk)
2089
2090 obj.initialize(branding=obj.get_branding(), topup_size=self.get_welcome_size())
2091
2092 return obj
2093
2094 class Signup(Grant):
2095 title = _("Sign Up")
2096 form_class = OrgSignupForm
2097 permission = None
2098 success_message = ''
2099 submit_button_name = _("Save")
2100
2101 def get_success_url(self):
2102 return "%s?start" % reverse('public.public_welcome')
2103
2104 def pre_process(self, request, *args, **kwargs):
2105 # if our brand doesn't allow signups, then redirect to the homepage
2106 if not request.branding.get('allow_signups', False): # pragma: needs cover
2107 return HttpResponseRedirect(reverse('public.public_index'))
2108
2109 else:
2110 return super(OrgCRUDL.Signup, self).pre_process(request, *args, **kwargs)
2111
2112 def derive_initial(self):
2113 initial = super(OrgCRUDL.Signup, self).get_initial()
2114 initial['email'] = self.request.POST.get('email', None)
2115 return initial
2116
2117 def get_welcome_size(self):
2118 welcome_topup_size = self.request.branding.get('welcome_topup', 0)
2119 return welcome_topup_size
2120
2121 def post_save(self, obj):
2122 obj = super(OrgCRUDL.Signup, self).post_save(obj)
2123 self.request.session['org_id'] = obj.pk
2124
2125 user = authenticate(username=self.user.username, password=self.form.cleaned_data['password'])
2126 login(self.request, user)
2127 analytics.track(self.request.user.username, 'temba.org_signup', dict(org=obj.name))
2128
2129 return obj
2130
2131 class Resthooks(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2132 class ResthookForm(forms.ModelForm):
2133 resthook = forms.SlugField(required=False, label=_("New Event"),
2134 help_text="Enter a name for your event. ex: new-registration")
2135
2136 def add_resthook_fields(self):
2137 resthooks = []
2138 field_mapping = []
2139
2140 for resthook in self.instance.get_resthooks():
2141 check_field = forms.BooleanField(required=False)
2142 field_name = "resthook_%d" % resthook.pk
2143
2144 field_mapping.append((field_name, check_field))
2145 resthooks.append(dict(resthook=resthook, field=field_name))
2146
2147 self.fields = OrderedDict(self.fields.items() + field_mapping)
2148 return resthooks
2149
2150 def clean_resthook(self):
2151 new_resthook = self.data.get('resthook')
2152
2153 if new_resthook:
2154 if self.instance.resthooks.filter(is_active=True, slug__iexact=new_resthook):
2155 raise ValidationError("This event name has already been used")
2156
2157 return new_resthook
2158
2159 class Meta:
2160 model = Org
2161 fields = ('id', 'resthook')
2162
2163 form_class = ResthookForm
2164 success_message = ''
2165
2166 def get_form(self):
2167 form = super(OrgCRUDL.Resthooks, self).get_form()
2168 self.current_resthooks = form.add_resthook_fields()
2169 return form
2170
2171 def get_context_data(self, **kwargs):
2172 context = super(OrgCRUDL.Resthooks, self).get_context_data(**kwargs)
2173 context['current_resthooks'] = self.current_resthooks
2174 return context
2175
2176 def pre_save(self, obj):
2177 from temba.api.models import Resthook
2178
2179 new_resthook = self.form.data.get('resthook')
2180 if new_resthook:
2181 Resthook.get_or_create(obj, new_resthook, self.request.user)
2182
2183 # release any resthooks that the user removed
2184 for resthook in self.current_resthooks:
2185 if self.form.data.get(resthook['field']):
2186 resthook['resthook'].release(self.request.user)
2187
2188 return super(OrgCRUDL.Resthooks, self).pre_save(obj)
2189
2190 class Giftcards(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2191 form_class = GiftcardsForm
2192 success_message = ''
2193 success_url = '@orgs.org_home'
2194 collection_type = GIFTCARDS
2195 fields_payload = DEFAULT_FIELDS_PAYLOAD_GIFTCARDS
2196 indexes_payload = DEFAULT_INDEXES_FIELDS_PAYLOAD_GIFTCARDS
2197
2198 def get_form(self):
2199 form = super(OrgCRUDL.Giftcards, self).get_form()
2200 self.current_collections = form.add_collection_fields(collection_type=self.collection_type)
2201 return form
2202
2203 def get_context_data(self, **kwargs):
2204 context = super(OrgCRUDL.Giftcards, self).get_context_data(**kwargs)
2205 context['current_collections'] = self.current_collections
2206 context['view_title'] = 'Gift Card'
2207 context['remove_div_title'] = 'giftcard'
2208 context['view_url'] = reverse('orgs.org_giftcards')
2209 return context
2210
2211 @staticmethod
2212 def get_collection_full_name(org_slug, org_id, name, collection_type=str(GIFTCARDS).lower()):
2213 from django.template.defaultfilters import slugify
2214
2215 slug_new_collection = slugify(name)
2216 collection_full_name = '{}_{}_{}_{}_{}'.format(settings.PARSE_SERVER_NAME, org_slug, org_id, collection_type, slug_new_collection)
2217 collection_full_name = collection_full_name.replace('-', '')
2218
2219 return collection_full_name
2220
2221 def pre_save(self, obj):
2222 new_collection = self.form.data.get('collection')
2223 headers = {
2224 'X-Parse-Application-Id': settings.PARSE_APP_ID,
2225 'X-Parse-Master-Key': settings.PARSE_MASTER_KEY,
2226 'Content-Type': 'application/json'
2227 }
2228
2229 if new_collection:
2230 collection_full_name = OrgCRUDL.Giftcards.get_collection_full_name(org_slug=self.object.slug,
2231 org_id=self.object.id,
2232 name=new_collection,
2233 collection_type=str(self.collection_type).lower())
2234 url = '%s/schemas/%s' % (settings.PARSE_URL, collection_full_name)
2235 data = {
2236 'className': collection_full_name,
2237 'fields': self.fields_payload,
2238 'indexes': self.indexes_payload
2239 }
2240 response = requests.post(url, data=json.dumps(data), headers=headers)
2241 if response.status_code == 200:
2242 self.object.add_collection_to_org(user=self.request.user, name=new_collection,
2243 collection_type=self.collection_type)
2244
2245 remove = self.form.data.get('remove', 'false') == 'true'
2246 index = self.form.data.get('index', None)
2247
2248 if remove and index:
2249 index = int(index)
2250 collections = self.object.get_collections(collection_type=self.collection_type)
2251
2252 try:
2253 collection = collections[index]
2254 except Exception:
2255 collection = None
2256
2257 if collection:
2258 collection_full_name = OrgCRUDL.Giftcards.get_collection_full_name(org_slug=self.object.slug,
2259 org_id=self.object.id,
2260 name=collection,
2261 collection_type=str(self.collection_type).lower())
2262 url = '%s/schemas/%s' % (settings.PARSE_URL, collection_full_name)
2263 purge_url = '%s/purge/%s' % (settings.PARSE_URL, collection_full_name)
2264
2265 response_purge = requests.delete(purge_url, headers=headers)
2266 if response_purge.status_code in [200, 404]:
2267 response = requests.delete(url, headers=headers)
2268
2269 if response.status_code == 200:
2270 self.object.remove_collection_from_org(user=self.request.user, index=index,
2271 collection_type=self.collection_type)
2272
2273 return super(OrgCRUDL.Giftcards, self).pre_save(obj)
2274
2275 class Lookups(Giftcards):
2276 class LookupsForm(GiftcardsForm):
2277
2278 def clean_collection(self):
2279 new_collection = self.data.get('collection')
2280
2281 if new_collection in self.instance.get_collections(collection_type=OrgCRUDL.Lookups.collection_type):
2282 raise ValidationError("This collection name has already been used")
2283
2284 return new_collection[:30] if new_collection else None
2285
2286 class Meta:
2287 model = Org
2288 fields = ('id', 'collection', 'remove', 'index')
2289
2290 form_class = LookupsForm
2291 success_message = ''
2292 success_url = '@orgs.org_home'
2293 collection_type = LOOKUPS
2294 fields_payload = DEFAULT_FIELDS_PAYLOAD_LOOKUPS
2295 indexes_payload = DEFAULT_INDEXES_FIELDS_PAYLOAD_LOOKUPS
2296
2297 def get_form(self):
2298 form = super(OrgCRUDL.Lookups, self).get_form()
2299 self.current_collections = form.add_collection_fields(collection_type=self.collection_type)
2300 return form
2301
2302 def get_context_data(self, **kwargs):
2303 context = super(OrgCRUDL.Lookups, self).get_context_data(**kwargs)
2304 context['current_collections'] = self.current_collections
2305 context['view_title'] = 'Lookup'
2306 context['remove_div_title'] = 'lookup'
2307 context['view_url'] = reverse('orgs.org_lookups')
2308 return context
2309
2310 class ParseDataView(InferOrgMixin, OrgPermsMixin, SmartListView):
2311 paginate_by = 0
2312
2313 def derive_fields(self):
2314 db = self.request.GET.get('db')
2315 type = self.request.GET.get('type')
2316
2317 if type == 'giftcard':
2318 collection_type = str(GIFTCARDS).lower()
2319 else:
2320 collection_type = str(LOOKUPS).lower()
2321
2322 org = self.request.user.get_org()
2323 collection = OrgCRUDL.Giftcards.get_collection_full_name(org_slug=org.slug, org_id=org.id, name=db, collection_type=collection_type)
2324
2325 parse_headers = {
2326 'X-Parse-Application-Id': settings.PARSE_APP_ID,
2327 'X-Parse-Master-Key': settings.PARSE_MASTER_KEY,
2328 'Content-Type': 'application/json'
2329 }
2330
2331 parse_url = '%s/schemas/%s' % (settings.PARSE_URL, collection)
2332
2333 response = requests.get(parse_url, headers=parse_headers)
2334
2335 fields = []
2336 if response.status_code == 200 and 'fields' in response.json():
2337 fields = response.json().get('fields')
2338 fields = [item for item in sorted(fields.keys()) if item not in ['ACL', 'createdAt', 'order']]
2339
2340 return tuple(fields)
2341
2342 def derive_queryset(self, **kwargs):
2343 db = self.request.GET.get('db')
2344 type = self.request.GET.get('type')
2345
2346 if type == 'giftcard':
2347 collection_type = str(GIFTCARDS).lower()
2348 else:
2349 collection_type = str(LOOKUPS).lower()
2350
2351 org = self.request.user.get_org()
2352 collection = OrgCRUDL.Giftcards.get_collection_full_name(org_slug=org.slug, org_id=org.id, name=db, collection_type=collection_type)
2353
2354 parse_headers = {
2355 'X-Parse-Application-Id': settings.PARSE_APP_ID,
2356 'X-Parse-Master-Key': settings.PARSE_MASTER_KEY,
2357 'Content-Type': 'application/json'
2358 }
2359
2360 parse_url = '%s/classes/%s?limit=1000&order=order' % (settings.PARSE_URL, collection)
2361 response = requests.get(parse_url, headers=parse_headers, timeout=60)
2362
2363 if response.status_code == 200 and 'results' in response.json():
2364 return response.json().get('results')
2365
2366 return []
2367
2368 def derive_title(self):
2369 return self.request.GET.get('db')
2370
2371 def lookup_obj_attribute(self, obj, field):
2372 if hasattr(obj, field):
2373 return getattr(obj, field, None)
2374 else:
2375 return None
2376
2377 def get_context_data(self, **kwargs):
2378 context = super(OrgCRUDL.ParseDataView, self).get_context_data(**kwargs)
2379 context['searches'] = []
2380 return context
2381
2382 class Webhook(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2383
2384 class WebhookForm(forms.ModelForm):
2385 webhook = forms.URLField(required=False, label=_("Webhook URL"), help_text="")
2386 headers = forms.CharField(required=False)
2387 mt_sms = forms.BooleanField(required=False, label=_("Incoming SMS"))
2388 mo_sms = forms.BooleanField(required=False, label=_("Outgoing SMS"))
2389 mt_call = forms.BooleanField(required=False, label=_("Incoming Calls"))
2390 mo_call = forms.BooleanField(required=False, label=_("Outgoing Calls"))
2391 alarm = forms.BooleanField(required=False, label=_("Channel Alarms"))
2392
2393 class Meta:
2394 model = Org
2395 fields = ('webhook', 'headers', 'mt_sms', 'mo_sms', 'mt_call', 'mo_call', 'alarm')
2396
2397 def clean_headers(self):
2398 idx = 1
2399 headers = dict()
2400 key = 'header_%d_key' % idx
2401 value = 'header_%d_value' % idx
2402
2403 while key in self.data:
2404 if self.data.get(value, ''):
2405 headers[self.data[key]] = self.data[value]
2406
2407 idx += 1
2408 key = 'header_%d_key' % idx
2409 value = 'header_%d_value' % idx
2410
2411 return headers
2412
2413 form_class = WebhookForm
2414 success_url = '@orgs.org_home'
2415 success_message = ''
2416
2417 def pre_save(self, obj):
2418 obj = super(OrgCRUDL.Webhook, self).pre_save(obj)
2419
2420 data = self.form.cleaned_data
2421
2422 webhook_events = 0
2423 if data['mt_sms']:
2424 webhook_events = MT_SMS_EVENTS
2425 if data['mo_sms']: # pragma: needs cover
2426 webhook_events |= MO_SMS_EVENTS
2427 if data['mt_call']: # pragma: needs cover
2428 webhook_events |= MT_CALL_EVENTS
2429 if data['mo_call']: # pragma: needs cover
2430 webhook_events |= MO_CALL_EVENTS
2431 if data['alarm']: # pragma: needs cover
2432 webhook_events |= ALARM_EVENTS
2433
2434 analytics.track(self.request.user.username, 'temba.org_configured_webhook')
2435
2436 obj.webhook_events = webhook_events
2437
2438 webhook_data = dict()
2439 if data['webhook']:
2440 webhook_data.update({'url': data['webhook']})
2441 webhook_data.update({'method': 'POST'})
2442
2443 if data['headers']:
2444 webhook_data.update({'headers': data['headers']})
2445
2446 obj.webhook = json.dumps(webhook_data)
2447
2448 return obj
2449
2450 def get_context_data(self, **kwargs):
2451 from temba.api.models import WebHookEvent
2452
2453 context = super(OrgCRUDL.Webhook, self).get_context_data(**kwargs)
2454 context['failed_webhooks'] = WebHookEvent.get_recent_errored(self.request.user.get_org()).exists()
2455 return context
2456
2457 class Chatbase(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2458
2459 class ChatbaseForm(forms.ModelForm):
2460 agent_name = forms.CharField(max_length=255, label=_("Agent Name"), required=False,
2461 help_text="Enter your Chatbase Agent's name")
2462 api_key = forms.CharField(max_length=255, label=_("API Key"), required=False,
2463 help_text="You can find your Agent's API Key "
2464 "<a href='https://chatbase.com/agents/main-page' target='_new'>here</a>")
2465 version = forms.CharField(max_length=10, label=_("Version"), required=False, help_text="Any will do, e.g. 1.0, 1.2.1")
2466 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
2467
2468 def clean(self):
2469 super(OrgCRUDL.Chatbase.ChatbaseForm, self).clean()
2470 if self.cleaned_data.get('disconnect', 'false') == 'false':
2471 agent_name = self.cleaned_data.get('agent_name')
2472 api_key = self.cleaned_data.get('api_key')
2473
2474 if not agent_name or not api_key:
2475 raise ValidationError(_("Missing data: Agent Name or API Key."
2476 "Please check them again and retry."))
2477
2478 return self.cleaned_data
2479
2480 class Meta:
2481 model = Org
2482 fields = ('agent_name', 'api_key', 'version', 'disconnect')
2483
2484 success_message = ''
2485 success_url = '@orgs.org_home'
2486 form_class = ChatbaseForm
2487
2488 def derive_initial(self):
2489 initial = super(OrgCRUDL.Chatbase, self).derive_initial()
2490 org = self.get_object()
2491 config = org.config_json()
2492 initial['agent_name'] = config.get(CHATBASE_AGENT_NAME, '')
2493 initial['api_key'] = config.get(CHATBASE_API_KEY, '')
2494 initial['version'] = config.get(CHATBASE_VERSION, '')
2495 initial['disconnect'] = 'false'
2496 return initial
2497
2498 def get_context_data(self, **kwargs):
2499 context = super(OrgCRUDL.Chatbase, self).get_context_data(**kwargs)
2500 (chatbase_api_key, chatbase_version) = self.object.get_chatbase_credentials()
2501 if chatbase_api_key:
2502 config = self.object.config_json()
2503 agent_name = config.get(CHATBASE_AGENT_NAME, None)
2504 context['chatbase_agent_name'] = agent_name
2505
2506 return context
2507
2508 def form_valid(self, form):
2509 user = self.request.user
2510 org = user.get_org()
2511
2512 agent_name = form.cleaned_data.get('agent_name')
2513 api_key = form.cleaned_data.get('api_key')
2514 version = form.cleaned_data.get('version')
2515 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
2516
2517 if disconnect:
2518 org.remove_chatbase_account(user)
2519 return HttpResponseRedirect(reverse('orgs.org_home'))
2520 elif api_key:
2521 org.connect_chatbase(agent_name, api_key, version, user)
2522
2523 return super(OrgCRUDL.Chatbase, self).form_valid(form)
2524
2525 class Salesforce(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2526
2527 class SalesforceForm(forms.ModelForm):
2528 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
2529
2530 def clean(self):
2531 super(OrgCRUDL.Salesforce.SalesforceForm, self).clean()
2532 return self.cleaned_data
2533
2534 class Meta:
2535 model = Org
2536 fields = ('disconnect', )
2537
2538 success_message = ''
2539 success_url = '@orgs.org_home'
2540 form_class = SalesforceForm
2541
2542 def derive_initial(self):
2543 initial = super(OrgCRUDL.Salesforce, self).derive_initial()
2544 org = self.get_object()
2545 config = org.config_json()
2546 initial['instance_url'] = config.get(SF_INSTANCE_URL, None)
2547 initial['disconnect'] = 'false'
2548 return initial
2549
2550 def get(self, request, *args, **kwargs):
2551 org = self.get_object()
2552
2553 redirect_uri = '%s%s' % (settings.BRANDING.get(settings.DEFAULT_BRAND).get('link'), reverse('orgs.org_salesforce'))
2554
2555 sf_code = self.request.GET.get('code', None)
2556
2557 if sf_code:
2558 data = {
2559 'grant_type': 'authorization_code',
2560 'redirect_uri': redirect_uri,
2561 'code': sf_code,
2562 'client_id': settings.SALESFORCE_CONSUMER_KEY,
2563 'client_secret': settings.SALESFORCE_CONSUMER_SECRET
2564 }
2565 headers = {'content-type': 'application/x-www-form-urlencoded'}
2566 headers.update(settings.OUTGOING_REQUEST_HEADERS)
2567 response = requests.post(settings.SALESFORCE_ACCESS_TOKEN_URL, data=data, headers=headers)
2568
2569 if response.status_code == 200:
2570 response = response.json()
2571
2572 try:
2573 sf_instance_url = response.get('instance_url')
2574 sf_access_token = response.get('access_token')
2575
2576 sf = Salesforce(instance_url=sf_instance_url, session_id=sf_access_token)
2577 sf.query("SELECT Id, Email FROM Contact LIMIT 1")
2578
2579 org.connect_salesforce_account(sf_instance_url, sf_access_token, response.get('refresh_token'), self.request.user)
2580 return HttpResponseRedirect(reverse('orgs.org_home'))
2581
2582 except Exception:
2583 messages.error(self.request, _('Your account does not have the required permissions for Import and Export contacts'))
2584
2585 else:
2586 messages.error(self.request, _('There was an error in the Salesforce auth request'))
2587
2588 return super(OrgCRUDL.Salesforce, self).get(request, *args, **kwargs)
2589
2590 def get_context_data(self, **kwargs):
2591 context = super(OrgCRUDL.Salesforce, self).get_context_data(**kwargs)
2592 (sf_instance_url, sf_access_token, sf_refresh_token) = self.object.get_salesforce_credentials()
2593
2594 if sf_instance_url:
2595 context['sf_instance_url'] = sf_instance_url
2596
2597 redirect_uri = '%s%s' % (settings.BRANDING.get(settings.DEFAULT_BRAND).get('link'), reverse('orgs.org_salesforce'))
2598
2599 salesforce_url = settings.SALESFORCE_AUTHORIZE_URL
2600 salesforce_url += '?response_type=code'
2601 salesforce_url += ('&client_id=' + settings.SALESFORCE_CONSUMER_KEY)
2602 salesforce_url += ('&redirect_uri=' + redirect_uri)
2603
2604 context['salesforce_url'] = salesforce_url
2605
2606 return context
2607
2608 def form_valid(self, form):
2609 user = self.request.user
2610 org = user.get_org()
2611
2612 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
2613
2614 if disconnect:
2615 org.remove_salesforce_account(user)
2616 return HttpResponseRedirect(reverse('orgs.org_home'))
2617
2618 return super(OrgCRUDL.Salesforce, self).form_valid(form)
2619
2620 class Home(FormaxMixin, InferOrgMixin, OrgPermsMixin, SmartReadView):
2621 title = _("Your Account")
2622
2623 def get_gear_links(self):
2624 links = []
2625
2626 links.append(dict(title=_('Logout'),
2627 style='btn-primary',
2628 href=reverse("users.user_logout")))
2629
2630 if self.has_org_perm("channels.channel_claim"):
2631 links.append(dict(title=_('Add Channel'),
2632 href=reverse('channels.channel_claim')))
2633
2634 if self.has_org_perm("orgs.org_export"):
2635 links.append(dict(title=_('Export'), href=reverse('orgs.org_export')))
2636
2637 if self.has_org_perm("orgs.org_import"):
2638 links.append(dict(title=_('Import'), href=reverse('orgs.org_import')))
2639
2640 if self.has_org_perm("orgs.org_import_parse_data"):
2641 links.append(dict(title=_('Import Parse Data'), href=reverse('orgs.org_import_parse_data')))
2642
2643 return links
2644
2645 def add_channel_section(self, formax, channel):
2646
2647 if self.has_org_perm('channels.channel_read'):
2648 from temba.channels.views import get_channel_read_url
2649 formax.add_section('channel', get_channel_read_url(channel), icon=channel.get_type().icon, action='link')
2650
2651 def derive_formax_sections(self, formax, context):
2652
2653 # add the channel option if we have one
2654 user = self.request.user
2655 org = user.get_org()
2656
2657 if self.has_org_perm('orgs.topup_list'):
2658 formax.add_section('topups', reverse('orgs.topup_list'), icon='icon-coins', action='link')
2659
2660 if self.has_org_perm("channels.channel_update"):
2661 # get any channel thats not a delegate
2662 channels = Channel.objects.filter(org=org, is_active=True, parent=None).order_by('-role')
2663 for channel in channels:
2664 self.add_channel_section(formax, channel)
2665
2666 twilio_client = org.get_twilio_client()
2667 if twilio_client:
2668 formax.add_section('twilio', reverse('orgs.org_twilio_account'), icon='icon-channel-twilio')
2669
2670 nexmo_client = org.get_nexmo_client()
2671 if nexmo_client: # pragma: needs cover
2672 formax.add_section('nexmo', reverse('orgs.org_nexmo_account'), icon='icon-channel-nexmo')
2673
2674 if self.has_org_perm('orgs.org_profile'):
2675 formax.add_section('user', reverse('orgs.user_edit'), icon='icon-user', action='redirect')
2676
2677 if self.has_org_perm('orgs.org_edit'):
2678 formax.add_section('org', reverse('orgs.org_edit'), icon='icon-office')
2679
2680 if self.has_org_perm('orgs.org_languages'):
2681 formax.add_section('languages', reverse('orgs.org_languages'), icon='icon-language')
2682
2683 if self.has_org_perm('orgs.org_country'):
2684 formax.add_section('country', reverse('orgs.org_country'), icon='icon-location2')
2685
2686 if self.has_org_perm("orgs.org_smtp_server"):
2687 formax.add_section('email', reverse('orgs.org_smtp_server'), icon='icon-envelop')
2688
2689 if self.has_org_perm('orgs.org_transfer_to_account'):
2690 if not self.object.is_connected_to_transferto():
2691 formax.add_section('transferto', reverse('orgs.org_transfer_to_account'), icon='icon-transferto',
2692 action='redirect', button=_("Connect"))
2693 else: # pragma: needs cover
2694 formax.add_section('transferto', reverse('orgs.org_transfer_to_account'), icon='icon-transferto',
2695 action='redirect', nobutton=True)
2696
2697 if self.has_org_perm('orgs.org_chatbase'):
2698 (chatbase_api_key, chatbase_version) = self.object.get_chatbase_credentials()
2699 if not chatbase_api_key:
2700 formax.add_section('chatbase', reverse('orgs.org_chatbase'), icon='icon-chatbase',
2701 action='redirect', button=_("Connect"))
2702 else: # pragma: needs cover
2703 formax.add_section('chatbase', reverse('orgs.org_chatbase'), icon='icon-chatbase',
2704 action='redirect', nobutton=True)
2705
2706 if self.has_org_perm('orgs.org_webhook'):
2707 formax.add_section('webhook', reverse('orgs.org_webhook'), icon='icon-cloud-upload')
2708
2709 if self.has_org_perm('orgs.org_resthooks'):
2710 formax.add_section('resthooks', reverse('orgs.org_resthooks'), icon='icon-cloud-lightning', dependents="resthooks")
2711
2712 # only pro orgs get multiple users
2713 if self.has_org_perm("orgs.org_manage_accounts") and org.is_multi_user_tier():
2714 formax.add_section('accounts', reverse('orgs.org_accounts'), icon='icon-users', action='redirect')
2715
2716 if self.has_org_perm('orgs.org_salesforce'):
2717 formax.add_section('salesforce', reverse('orgs.org_salesforce'), icon='icon-cloud', nobutton=True)
2718
2719 if self.has_org_perm('orgs.org_giftcards'):
2720 formax.add_section('giftcards', reverse('orgs.org_giftcards'), icon='icon-credit-2',
2721 action='redirect')
2722
2723 if self.has_org_perm('orgs.org_lookups'):
2724 formax.add_section('lookups', reverse('orgs.org_lookups'), icon='icon-filter',
2725 action='redirect')
2726
2727 class TransferToAccount(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2728
2729 success_message = ""
2730
2731 class TransferToAccountForm(forms.ModelForm):
2732 account_login = forms.CharField(label=_("Login"), required=False)
2733 airtime_api_token = forms.CharField(label=_("API Token"), required=False)
2734 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
2735
2736 def clean(self):
2737 super(OrgCRUDL.TransferToAccount.TransferToAccountForm, self).clean()
2738 if self.cleaned_data.get('disconnect', 'false') == 'false':
2739 account_login = self.cleaned_data.get('account_login', None)
2740 airtime_api_token = self.cleaned_data.get('airtime_api_token', None)
2741
2742 try:
2743 from temba.airtime.models import AirtimeTransfer
2744 response = AirtimeTransfer.post_transferto_api_response(account_login, airtime_api_token, action='ping')
2745 parsed_response = AirtimeTransfer.parse_transferto_response(response.content)
2746
2747 error_code = int(parsed_response.get('error_code', None))
2748 info_txt = parsed_response.get('info_txt', None)
2749 error_txt = parsed_response.get('error_txt', None)
2750
2751 except Exception:
2752 raise ValidationError(_("Your TransferTo API key and secret seem invalid. "
2753 "Please check them again and retry."))
2754
2755 if error_code != 0 and info_txt != 'pong':
2756 raise ValidationError(_("Connecting to your TransferTo account "
2757 "failed with error text: %s") % error_txt)
2758
2759 return self.cleaned_data
2760
2761 class Meta:
2762 model = Org
2763 fields = ('account_login', 'airtime_api_token', 'disconnect')
2764
2765 form_class = TransferToAccountForm
2766 submit_button_name = "Save"
2767 success_url = '@orgs.org_home'
2768
2769 def get_context_data(self, **kwargs):
2770 context = super(OrgCRUDL.TransferToAccount, self).get_context_data(**kwargs)
2771 if self.object.is_connected_to_transferto():
2772 config = self.object.config_json()
2773 account_login = config.get(TRANSFERTO_ACCOUNT_LOGIN, None)
2774 context['transferto_account_login'] = account_login
2775
2776 return context
2777
2778 def derive_initial(self):
2779 initial = super(OrgCRUDL.TransferToAccount, self).derive_initial()
2780 config = self.object.config_json()
2781 initial['account_login'] = config.get(TRANSFERTO_ACCOUNT_LOGIN, None)
2782 initial['airtime_api_token'] = config.get(TRANSFERTO_AIRTIME_API_TOKEN, None)
2783 initial['disconnect'] = 'false'
2784 return initial
2785
2786 def form_valid(self, form):
2787 user = self.request.user
2788 org = user.get_org()
2789 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
2790 if disconnect:
2791 org.remove_transferto_account(user)
2792 return HttpResponseRedirect(reverse('orgs.org_home'))
2793 else:
2794 account_login = form.cleaned_data['account_login']
2795 airtime_api_token = form.cleaned_data['airtime_api_token']
2796
2797 org.connect_transferto(account_login, airtime_api_token, user)
2798 org.refresh_transferto_account_currency()
2799 return super(OrgCRUDL.TransferToAccount, self).form_valid(form)
2800
2801 class TwilioAccount(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2802
2803 success_message = ''
2804
2805 class TwilioKeys(forms.ModelForm):
2806 account_sid = forms.CharField(max_length=128, label=_("Account SID"), required=False)
2807 account_token = forms.CharField(max_length=128, label=_("Account Token"), required=False)
2808 disconnect = forms.CharField(widget=forms.HiddenInput, max_length=6, required=True)
2809
2810 def clean(self):
2811 super(OrgCRUDL.TwilioAccount.TwilioKeys, self).clean()
2812 if self.cleaned_data.get('disconnect', 'false') == 'false':
2813 account_sid = self.cleaned_data.get('account_sid', None)
2814 account_token = self.cleaned_data.get('account_token', None)
2815
2816 if not account_sid:
2817 raise ValidationError(_("You must enter your Twilio Account SID"))
2818
2819 if not account_token: # pragma: needs cover
2820 raise ValidationError(_("You must enter your Twilio Account Token"))
2821
2822 try:
2823 client = TwilioRestClient(account_sid, account_token)
2824
2825 # get the actual primary auth tokens from twilio and use them
2826 account = client.accounts.get(account_sid)
2827 self.cleaned_data['account_sid'] = account.sid
2828 self.cleaned_data['account_token'] = account.auth_token
2829 except Exception: # pragma: needs cover
2830 raise ValidationError(_("The Twilio account SID and Token seem invalid. Please check them again and retry."))
2831
2832 return self.cleaned_data
2833
2834 class Meta:
2835 model = Org
2836 fields = ('account_sid', 'account_token', 'disconnect')
2837
2838 form_class = TwilioKeys
2839
2840 def get_context_data(self, **kwargs):
2841 context = super(OrgCRUDL.TwilioAccount, self).get_context_data(**kwargs)
2842 client = self.object.get_twilio_client()
2843 if client:
2844 account_sid = client.auth[0]
2845 sid_length = len(account_sid)
2846 context['account_sid'] = '%s%s' % ('\u066D' * (sid_length - 16), account_sid[-16:])
2847 return context
2848
2849 def derive_initial(self):
2850 initial = super(OrgCRUDL.TwilioAccount, self).derive_initial()
2851 config = json.loads(self.object.config)
2852 initial['account_sid'] = config[ACCOUNT_SID]
2853 initial['account_token'] = config[ACCOUNT_TOKEN]
2854 initial['disconnect'] = 'false'
2855 return initial
2856
2857 def form_valid(self, form):
2858 disconnect = form.cleaned_data.get('disconnect', 'false') == 'true'
2859 user = self.request.user
2860 org = user.get_org()
2861
2862 if disconnect:
2863 org.remove_twilio_account(user)
2864 return HttpResponseRedirect(reverse('orgs.org_home'))
2865 else:
2866 account_sid = form.cleaned_data['account_sid']
2867 account_token = form.cleaned_data['account_token']
2868
2869 org.connect_twilio(account_sid, account_token, user)
2870 return super(OrgCRUDL.TwilioAccount, self).form_valid(form)
2871
2872 class Edit(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2873
2874 class OrgForm(forms.ModelForm):
2875 name = forms.CharField(max_length=128, label=_("The name of your organization"), help_text="")
2876 timezone = TimeZoneFormField(label=_("Your organization's timezone"), help_text="")
2877 slug = forms.SlugField(max_length=255, label=_("The slug, or short name for your organization"), help_text="")
2878
2879 class Meta:
2880 model = Org
2881 fields = ('name', 'slug', 'timezone', 'date_format')
2882
2883 success_message = ''
2884 form_class = OrgForm
2885 fields = ('name', 'slug', 'timezone', 'date_format')
2886
2887 def has_permission(self, request, *args, **kwargs):
2888 self.org = self.derive_org()
2889 return self.has_org_perm('orgs.org_edit')
2890
2891 def get_context_data(self, **kwargs):
2892 context = super(OrgCRUDL.Edit, self).get_context_data(**kwargs)
2893 sub_orgs = Org.objects.filter(parent=self.get_object())
2894 context['sub_orgs'] = sub_orgs
2895 return context
2896
2897 class EditSubOrg(ModalMixin, Edit):
2898
2899 success_url = '@orgs.org_sub_orgs'
2900
2901 def get_object(self, *args, **kwargs):
2902 org_id = self.request.GET.get('org')
2903 return Org.objects.filter(id=org_id, parent=self.request.user.get_org()).first()
2904
2905 class TransferCredits(MultiOrgMixin, ModalMixin, InferOrgMixin, SmartFormView):
2906
2907 class TransferForm(forms.Form):
2908
2909 class OrgChoiceField(forms.ModelChoiceField):
2910 def label_from_instance(self, org):
2911 return '%s (%s)' % (org.name, "{:,}".format(org.get_credits_remaining()))
2912
2913 from_org = OrgChoiceField(None, required=True, label=_("From Organization"),
2914 help_text=_("Select which organization to take credits from"))
2915
2916 to_org = OrgChoiceField(None, required=True, label=_("To Organization"),
2917 help_text=_("Select which organization to receive the credits"))
2918
2919 amount = forms.IntegerField(required=True, label=_('Credits'),
2920 help_text=_("How many credits to transfer"),
2921 widget=forms.NumberInput(attrs=dict(min=1)))
2922
2923 def __init__(self, *args, **kwargs):
2924 org = kwargs['org']
2925 del kwargs['org']
2926
2927 super(OrgCRUDL.TransferCredits.TransferForm, self).__init__(*args, **kwargs)
2928
2929 self.fields['from_org'].queryset = Org.objects.filter(Q(parent=org) | Q(id=org.id)).order_by('-parent', 'id')
2930 self.fields['to_org'].queryset = Org.objects.filter(Q(parent=org) | Q(id=org.id)).order_by('-parent', 'id')
2931
2932 def clean(self):
2933 cleaned_data = super(OrgCRUDL.TransferCredits.TransferForm, self).clean()
2934
2935 if 'amount' in cleaned_data and 'from_org' in cleaned_data:
2936 from_org = cleaned_data['from_org']
2937
2938 if cleaned_data['amount'] > from_org.get_credits_remaining():
2939 raise ValidationError(_("Sorry, %(org_name)s doesn't have enough credits for this transfer. Pick a different organization to transfer from or reduce the transfer amount.") % dict(org_name=from_org.name))
2940 elif int(cleaned_data['amount']) < 0:
2941 raise ValidationError(_("Sorry, please enter a value greater than 0."))
2942
2943 success_url = '@orgs.org_sub_orgs'
2944 form_class = TransferForm
2945 fields = ('from_org', 'to_org', 'amount')
2946 permission = 'orgs.org_transfer_credits'
2947
2948 def has_permission(self, request, *args, **kwargs):
2949 self.org = self.request.user.get_org()
2950 return self.request.user.has_perm(self.permission) or self.has_org_perm(self.permission)
2951
2952 def get_form_kwargs(self):
2953 form_kwargs = super(OrgCRUDL.TransferCredits, self).get_form_kwargs()
2954 form_kwargs['org'] = self.get_object()
2955 return form_kwargs
2956
2957 def form_valid(self, form):
2958 from_org = form.cleaned_data['from_org']
2959 to_org = form.cleaned_data['to_org']
2960 amount = form.cleaned_data['amount']
2961
2962 from_org.allocate_credits(from_org.created_by, to_org, amount)
2963
2964 response = self.render_to_response(self.get_context_data(form=form,
2965 success_url=self.get_success_url(),
2966 success_script=getattr(self, 'success_script', None)))
2967
2968 response['Temba-Success'] = self.get_success_url()
2969 return response
2970
2971 class Country(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2972
2973 class CountryForm(forms.ModelForm):
2974 country = forms.ModelChoiceField(
2975 Org.get_possible_countries(), required=False,
2976 label=_("The country used for location values. (optional)"),
2977 help_text="State and district names will be searched against this country."
2978 )
2979
2980 class Meta:
2981 model = Org
2982 fields = ('country',)
2983
2984 success_message = ''
2985 form_class = CountryForm
2986
2987 def has_permission(self, request, *args, **kwargs):
2988 self.org = self.derive_org()
2989 return self.request.user.has_perm('orgs.org_country') or self.has_org_perm('orgs.org_country')
2990
2991 class Languages(InferOrgMixin, OrgPermsMixin, SmartUpdateView):
2992
2993 class LanguagesForm(forms.ModelForm):
2994 primary_lang = forms.CharField(
2995 required=False, label=_('Primary Language'),
2996 help_text=_('The primary language will be used for contacts with no language preference.')
2997 )
2998 languages = forms.CharField(
2999 required=False, label=_('Additional Languages'),
3000 help_text=_('Add any other languages you would like to provide translations for.')
3001 )
3002
3003 def __init__(self, *args, **kwargs):
3004 self.org = kwargs['org']
3005 del kwargs['org']
3006 super(OrgCRUDL.Languages.LanguagesForm, self).__init__(*args, **kwargs)
3007
3008 class Meta:
3009 model = Org
3010 fields = ('primary_lang', 'languages')
3011
3012 success_message = ''
3013 form_class = LanguagesForm
3014
3015 def get_form_kwargs(self):
3016 kwargs = super(OrgCRUDL.Languages, self).get_form_kwargs()
3017 kwargs['org'] = self.request.user.get_org()
3018 return kwargs
3019
3020 def derive_initial(self):
3021
3022 initial = super(OrgCRUDL.Languages, self).derive_initial()
3023 langs = ','.join([lang.iso_code for lang in self.get_object().languages.filter(orgs=None).order_by('name')])
3024 initial['languages'] = langs
3025
3026 if self.object.primary_language:
3027 initial['primary_lang'] = self.object.primary_language.iso_code
3028
3029 return initial
3030
3031 def get_context_data(self, **kwargs):
3032 context = super(OrgCRUDL.Languages, self).get_context_data(**kwargs)
3033 languages = [lang.name for lang in self.request.user.get_org().languages.filter(orgs=None).order_by('name')]
3034 lang_count = len(languages)
3035
3036 if lang_count == 2:
3037 context['languages'] = _(' and ').join(languages)
3038 elif lang_count > 2:
3039 context['languages'] = _('%s and %s') % (', '.join(languages[:-1]), languages[-1])
3040 elif lang_count == 1:
3041 context['languages'] = languages[0]
3042 return context
3043
3044 def get(self, request, *args, **kwargs):
3045
3046 if 'search' in self.request.GET or 'initial' in self.request.GET:
3047 initial = self.request.GET.get('initial', '').split(',')
3048 matches = []
3049
3050 if len(initial) > 0:
3051 for iso_code in initial:
3052 if iso_code:
3053 lang = languages.get_language_name(iso_code)
3054 matches.append(dict(id=iso_code, text=lang))
3055
3056 if len(matches) == 0:
3057 search = self.request.GET.get('search', '').strip().lower()
3058 matches += languages.search_language_names(search)
3059 return JsonResponse(dict(results=matches))
3060
3061 return super(OrgCRUDL.Languages, self).get(request, *args, **kwargs)
3062
3063 def form_valid(self, form):
3064 user = self.request.user
3065 primary = form.cleaned_data['primary_lang']
3066 iso_codes = form.cleaned_data['languages'].split(',')
3067
3068 # remove empty codes and ensure primary is included in list
3069 iso_codes = [code for code in iso_codes if code]
3070 if primary and primary not in iso_codes:
3071 iso_codes.append(primary)
3072
3073 self.object.set_languages(user, iso_codes, primary)
3074
3075 return super(OrgCRUDL.Languages, self).form_valid(form)
3076
3077 def has_permission(self, request, *args, **kwargs):
3078 self.org = self.derive_org()
3079 return self.request.user.has_perm('orgs.org_country') or self.has_org_perm('orgs.org_country')
3080
3081 class ClearCache(SmartUpdateView): # pragma: no cover
3082 fields = ('id',)
3083 success_message = None
3084 success_url = 'id@orgs.org_update'
3085
3086 def pre_process(self, request, *args, **kwargs):
3087 cache = OrgCache(int(request.POST['cache']))
3088 num_deleted = self.get_object().clear_caches([cache])
3089 self.success_message = _("Cleared %s cache for this organization (%d keys)") % (cache.name, num_deleted)
3090
3091
3092class TopUpCRUDL(SmartCRUDL):
3093 actions = ('list', 'create', 'read', 'manage', 'update')
3094 model = TopUp
3095
3096 class Read(OrgPermsMixin, SmartReadView):
3097 def derive_queryset(self, **kwargs): # pragma: needs cover
3098 query = TopUp.objects.filter(is_active=True, org=self.request.user.get_org())
3099 if settings.CREDITS_EXPIRATION:
3100 query = query.order_by('-expires_on')
3101 else:
3102 query = query.order_by('-created_on')
3103 return query
3104
3105 class List(OrgPermsMixin, SmartListView):
3106 def derive_queryset(self, **kwargs):
3107 queryset = TopUp.objects.filter(is_active=True, org=self.request.user.get_org())
3108 return queryset.annotate(credits_remaining=ExpressionWrapper(F('credits') - Sum(F('topupcredits__used')), IntegerField()))
3109
3110 def get_context_data(self, **kwargs):
3111 context = super(TopUpCRUDL.List, self).get_context_data(**kwargs)
3112 context['org'] = self.request.user.get_org()
3113
3114 if settings.CREDITS_EXPIRATION:
3115 context['credits_expiration'] = True
3116
3117 now = timezone.now()
3118 context['now'] = now
3119 context['expiration_period'] = now + timedelta(days=30)
3120
3121 # show our topups in a meaningful order
3122 topups = list(self.get_queryset())
3123
3124 def compare(topup1, topup2): # pragma: no cover
3125
3126 if settings.CREDITS_EXPIRATION:
3127 # non expired first
3128 now = timezone.now()
3129 if topup1.expires_on > now and topup2.expires_on <= now:
3130 return -1
3131 elif topup2.expires_on > now and topup1.expires_on <= now:
3132 return 1
3133
3134 # then push those without credits remaining to the bottom
3135 if topup1.credits_remaining is None:
3136 topup1.credits_remaining = topup1.credits
3137
3138 if topup2.credits_remaining is None:
3139 topup2.credits_remaining = topup2.credits
3140
3141 if topup1.credits_remaining and not topup2.credits_remaining:
3142 return -1
3143 elif topup2.credits_remaining and not topup1.credits_remaining:
3144 return 1
3145
3146 if settings.CREDITS_EXPIRATION:
3147 # sor the rest by their expiration date
3148 if topup1.expires_on > topup2.expires_on:
3149 return -1
3150 elif topup1.expires_on < topup2.expires_on:
3151 return 1
3152
3153 # if we end up with the same expiration, show the oldest first
3154 return topup2.id - topup1.id
3155
3156 topups.sort(key=cmp_to_key(compare))
3157 context['topups'] = topups
3158 return context
3159
3160 def get_template_names(self):
3161 if 'HTTP_X_FORMAX' in self.request.META:
3162 return ['orgs/topup_list_summary.haml']
3163 else:
3164 return super(TopUpCRUDL.List, self).get_template_names()
3165
3166 class Create(SmartCreateView):
3167 """
3168 This is only for root to be able to credit accounts.
3169 """
3170 fields = ('credits', 'price', 'comment')
3171
3172 def get_success_url(self):
3173 return reverse('orgs.topup_manage') + ('?org=%d' % self.object.org.id)
3174
3175 def save(self, obj):
3176 obj.org = Org.objects.get(pk=self.request.GET['org'])
3177 return TopUp.create(self.request.user, price=obj.price, credits=obj.credits, org=obj.org)
3178
3179 def post_save(self, obj):
3180 obj = super(TopUpCRUDL.Create, self).post_save(obj)
3181 obj.org.apply_topups()
3182 return obj
3183
3184 class Update(SmartUpdateView):
3185 fields = ('is_active', 'price', 'credits', 'expires_on')
3186
3187 def get_success_url(self):
3188 return reverse('orgs.topup_manage') + ('?org=%d' % self.object.org.id)
3189
3190 def post_save(self, obj):
3191 obj = super(TopUpCRUDL.Update, self).post_save(obj)
3192 obj.org.update_caches(OrgEvent.topup_updated, obj)
3193 obj.org.apply_topups()
3194 return obj
3195
3196 class Manage(SmartListView):
3197 """
3198 This is only for root to be able to manage topups on an account
3199 """
3200 success_url = '@orgs.org_manage'
3201 fields = ('credits', 'price', 'comment', 'created_on')
3202 if settings.CREDITS_EXPIRATION:
3203 fields = fields + ('expires_on',)
3204 default_order = '-expires_on'
3205 else:
3206 default_order = '-created_on'
3207
3208 def lookup_field_link(self, context, field, obj):
3209 return reverse('orgs.topup_update', args=[obj.id])
3210
3211 def get_price(self, obj):
3212 if obj.price:
3213 return "$%.2f" % (obj.price / 100.0)
3214 else:
3215 return "-"
3216
3217 def get_credits(self, obj):
3218 return format(obj.credits, ",d")
3219
3220 def get_context_data(self, **kwargs):
3221 context = super(TopUpCRUDL.Manage, self).get_context_data(**kwargs)
3222 context['org'] = self.org
3223 return context
3224
3225 def derive_queryset(self):
3226 self.org = Org.objects.get(pk=self.request.GET['org'])
3227 return self.org.topups.all()
3228
3229
3230class StripeHandler(View): # pragma: no cover
3231 """
3232 Handles WebHook events from Stripe. We are interested as to when invoices are
3233 charged by Stripe so we can send the user an invoice email.
3234 """
3235 @csrf_exempt
3236 def dispatch(self, *args, **kwargs):
3237 return super(StripeHandler, self).dispatch(*args, **kwargs)
3238
3239 def get(self, request, *args, **kwargs):
3240 return HttpResponse("ILLEGAL METHOD")
3241
3242 def post(self, request, *args, **kwargs):
3243 import stripe
3244 from temba.orgs.models import Org, TopUp
3245
3246 # stripe delivers a JSON payload
3247 stripe_data = json.loads(request.body)
3248
3249 # but we can't trust just any response, so lets go look up this event
3250 stripe.api_key = get_stripe_credentials()[1]
3251 event = stripe.Event.retrieve(stripe_data['id'])
3252
3253 if not event:
3254 return HttpResponse("Ignored, no event")
3255
3256 if not event.livemode:
3257 return HttpResponse("Ignored, test event")
3258
3259 # we only care about invoices being paid or failing
3260 if event.type == 'charge.succeeded' or event.type == 'charge.failed':
3261 charge = event.data.object
3262 charge_date = datetime.fromtimestamp(charge.created)
3263 description = charge.description
3264 amount = "$%s" % (Decimal(charge.amount) / Decimal(100)).quantize(Decimal(".01"))
3265
3266 # look up our customer
3267 customer = stripe.Customer.retrieve(charge.customer)
3268
3269 # and our org
3270 org = Org.objects.filter(stripe_customer=customer.id).first()
3271 if not org:
3272 return HttpResponse("Ignored, no org for customer")
3273
3274 # look up the topup that matches this charge
3275 topup = TopUp.objects.filter(stripe_charge=charge.id).first()
3276 if topup and event.type == 'charge.failed':
3277 topup.rollback()
3278 topup.save()
3279
3280 # we know this org, trigger an event for a payment succeeding
3281 if org.administrators.all():
3282 if event.type == 'charge_succeeded':
3283 track = "temba.charge_succeeded"
3284 else:
3285 track = "temba.charge_failed"
3286
3287 context = dict(description=description,
3288 invoice_id=charge.id,
3289 invoice_date=charge_date.strftime("%b %e, %Y"),
3290 amount=amount,
3291 org=org.name)
3292
3293 if getattr(charge, 'card', None):
3294 context['cc_last4'] = charge.card.last4
3295 context['cc_type'] = charge.card.type
3296 context['cc_name'] = charge.card.name
3297
3298 else:
3299 context['cc_type'] = 'bitcoin'
3300 context['cc_name'] = charge.source.bitcoin.address
3301
3302 admin_email = org.administrators.all().first().email
3303
3304 analytics.track(admin_email, track, context)
3305 return HttpResponse("Event '%s': %s" % (track, context))
3306
3307 # empty response, 200 lets Stripe know we handled it
3308 return HttpResponse("Ignored, uninteresting event")